1pub mod active_file_name;
2pub mod dock;
3pub mod history_manager;
4pub mod invalid_item_view;
5pub mod item;
6mod modal_layer;
7mod multi_workspace;
8#[cfg(test)]
9mod multi_workspace_tests;
10pub mod notifications;
11pub mod pane;
12pub mod pane_group;
13pub mod path_list {
14 pub use util::path_list::{PathList, SerializedPathList};
15}
16mod persistence;
17pub mod searchable;
18mod security_modal;
19pub mod shared_screen;
20use db::smol::future::yield_now;
21pub use shared_screen::SharedScreen;
22pub mod focus_follows_mouse;
23mod status_bar;
24pub mod tasks;
25mod theme_preview;
26mod toast_layer;
27mod toolbar;
28pub mod welcome;
29mod workspace_settings;
30
31pub use crate::notifications::NotificationFrame;
32pub use dock::Panel;
33pub use multi_workspace::{
34 CloseWorkspaceSidebar, DraggedSidebar, FocusWorkspaceSidebar, MoveProjectToNewWindow,
35 MultiWorkspace, MultiWorkspaceEvent, NewThread, NextProject, NextThread, PreviousProject,
36 PreviousThread, ProjectGroup, ProjectGroupKey, SerializedProjectGroupState, Sidebar,
37 SidebarEvent, SidebarHandle, SidebarRenderState, SidebarSide, ToggleWorkspaceSidebar,
38 sidebar_side_context_menu,
39};
40pub use path_list::{PathList, SerializedPathList};
41pub use remote::{
42 RemoteConnectionIdentity, remote_connection_identity, same_remote_connection_identity,
43};
44pub use toast_layer::{ToastAction, ToastLayer, ToastView};
45
46use anyhow::{Context as _, Result, anyhow};
47use client::{
48 ChannelId, Client, ErrorExt, ParticipantIndex, Status, TypedEnvelope, User, UserStore,
49 proto::{self, ErrorCode, PanelId, PeerId},
50};
51use collections::{HashMap, HashSet, hash_map};
52use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
53use fs::Fs;
54use futures::{
55 Future, FutureExt, StreamExt,
56 channel::{
57 mpsc::{self, UnboundedReceiver, UnboundedSender},
58 oneshot,
59 },
60 future::{Shared, try_join_all},
61};
62use gpui::{
63 Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Axis, Bounds,
64 Context, CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
65 Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView, MouseButton,
66 PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription,
67 SystemWindowTabController, Task, Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId,
68 WindowOptions, actions, canvas, point, relative, size, transparent_black,
69};
70pub use history_manager::*;
71pub use item::{
72 FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
73 ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
74};
75use itertools::Itertools;
76use language::{Buffer, LanguageRegistry, Rope, language_settings::all_language_settings};
77pub use modal_layer::*;
78use node_runtime::NodeRuntime;
79use notifications::{
80 DetachAndPromptErr, Notifications, dismiss_app_notification,
81 simple_message_notification::MessageNotification,
82};
83pub use pane::*;
84pub use pane_group::{
85 ActivePaneDecorator, HANDLE_HITBOX_SIZE, Member, PaneAxis, PaneGroup, PaneRenderContext,
86 SplitDirection,
87};
88use persistence::{SerializedWindowBounds, model::SerializedWorkspace};
89pub use persistence::{
90 WorkspaceDb, delete_unloaded_items,
91 model::{
92 DockData, DockStructure, ItemId, MultiWorkspaceState, SerializedMultiWorkspace,
93 SerializedProjectGroup, SerializedWorkspaceLocation, SessionWorkspace,
94 },
95 read_serialized_multi_workspaces, resolve_worktree_workspaces,
96};
97use postage::stream::Stream;
98use project::{
99 DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
100 WorktreeSettings,
101 debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
102 project_settings::ProjectSettings,
103 toolchain_store::ToolchainStoreEvent,
104 trusted_worktrees::{RemoteHostLocation, TrustedWorktrees, TrustedWorktreesEvent},
105};
106use remote::{
107 RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions,
108 remote_client::ConnectionIdentifier,
109};
110use schemars::JsonSchema;
111use serde::Deserialize;
112use session::AppSession;
113use settings::{
114 CenteredPaddingSettings, Settings, SettingsLocation, SettingsStore, update_settings_file,
115};
116
117use sqlez::{
118 bindable::{Bind, Column, StaticColumnCount},
119 statement::Statement,
120};
121use status_bar::StatusBar;
122pub use status_bar::StatusItemView;
123use std::{
124 any::TypeId,
125 borrow::Cow,
126 cell::RefCell,
127 cmp,
128 collections::VecDeque,
129 env,
130 hash::Hash,
131 path::{Path, PathBuf},
132 process::ExitStatus,
133 rc::Rc,
134 sync::{
135 Arc, LazyLock,
136 atomic::{AtomicBool, AtomicUsize},
137 },
138 time::Duration,
139};
140use task::{DebugScenario, SharedTaskContext, SpawnInTerminal};
141use theme::{ActiveTheme, SystemAppearance};
142use theme_settings::ThemeSettings;
143pub use toolbar::{
144 PaneSearchBarCallbacks, Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
145};
146pub use ui;
147use ui::{Window, prelude::*};
148use util::{
149 ResultExt, TryFutureExt,
150 paths::{PathStyle, SanitizedPath},
151 rel_path::RelPath,
152 serde::default_true,
153};
154use uuid::Uuid;
155pub use workspace_settings::{
156 AutosaveSetting, BottomDockLayout, FocusFollowsMouse, RestoreOnStartupBehavior,
157 StatusBarSettings, TabBarSettings, WorkspaceSettings,
158};
159use zed_actions::{Spawn, feedback::FileBugReport, theme::ToggleMode};
160
161use crate::{dock::PanelSizeState, item::ItemBufferKind, notifications::NotificationId};
162use crate::{
163 persistence::{
164 SerializedAxis,
165 model::{SerializedItem, SerializedPane, SerializedPaneGroup},
166 },
167 security_modal::SecurityModal,
168};
169
170pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200);
171
172static ZED_WINDOW_SIZE: LazyLock<Option<Size<Pixels>>> = LazyLock::new(|| {
173 env::var("ZED_WINDOW_SIZE")
174 .ok()
175 .as_deref()
176 .and_then(parse_pixel_size_env_var)
177});
178
179static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
180 env::var("ZED_WINDOW_POSITION")
181 .ok()
182 .as_deref()
183 .and_then(parse_pixel_position_env_var)
184});
185
186pub trait TerminalProvider {
187 fn spawn(
188 &self,
189 task: SpawnInTerminal,
190 window: &mut Window,
191 cx: &mut App,
192 ) -> Task<Option<Result<ExitStatus>>>;
193}
194
195pub trait DebuggerProvider {
196 // `active_buffer` is used to resolve build task's name against language-specific tasks.
197 fn start_session(
198 &self,
199 definition: DebugScenario,
200 task_context: SharedTaskContext,
201 active_buffer: Option<Entity<Buffer>>,
202 worktree_id: Option<WorktreeId>,
203 window: &mut Window,
204 cx: &mut App,
205 );
206
207 fn spawn_task_or_modal(
208 &self,
209 workspace: &mut Workspace,
210 action: &Spawn,
211 window: &mut Window,
212 cx: &mut Context<Workspace>,
213 );
214
215 fn task_scheduled(&self, cx: &mut App);
216 fn debug_scenario_scheduled(&self, cx: &mut App);
217 fn debug_scenario_scheduled_last(&self, cx: &App) -> bool;
218
219 fn active_thread_state(&self, cx: &App) -> Option<ThreadStatus>;
220}
221
222/// Opens a file or directory.
223#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
224#[action(namespace = workspace)]
225pub struct Open {
226 /// When true, opens in a new window. When false, adds to the current
227 /// window as a new workspace (multi-workspace).
228 #[serde(default = "Open::default_create_new_window")]
229 pub create_new_window: bool,
230}
231
232impl Open {
233 pub const DEFAULT: Self = Self {
234 create_new_window: false,
235 };
236
237 /// Used by `#[serde(default)]` on the `create_new_window` field so that
238 /// the serde default and `Open::DEFAULT` stay in sync.
239 fn default_create_new_window() -> bool {
240 Self::DEFAULT.create_new_window
241 }
242}
243
244impl Default for Open {
245 fn default() -> Self {
246 Self::DEFAULT
247 }
248}
249
250actions!(
251 workspace,
252 [
253 /// Activates the next pane in the workspace.
254 ActivateNextPane,
255 /// Activates the previous pane in the workspace.
256 ActivatePreviousPane,
257 /// Activates the last pane in the workspace.
258 ActivateLastPane,
259 /// Switches to the next window.
260 ActivateNextWindow,
261 /// Switches to the previous window.
262 ActivatePreviousWindow,
263 /// Adds a folder to the current project.
264 AddFolderToProject,
265 /// Clears all bookmarks in the project.
266 ClearBookmarks,
267 /// Clears all notifications.
268 ClearAllNotifications,
269 /// Clears all navigation history, including forward/backward navigation, recently opened files, and recently closed tabs. **This action is irreversible**.
270 ClearNavigationHistory,
271 /// Closes the active dock.
272 CloseActiveDock,
273 /// Closes all docks.
274 CloseAllDocks,
275 /// Toggles all docks.
276 ToggleAllDocks,
277 /// Closes the current window.
278 CloseWindow,
279 /// Closes the current project.
280 CloseProject,
281 /// Opens the feedback dialog.
282 Feedback,
283 /// Follows the next collaborator in the session.
284 FollowNextCollaborator,
285 /// Moves the focused panel to the next position.
286 MoveFocusedPanelToNextPosition,
287 /// Creates a new file.
288 NewFile,
289 /// Creates a new file in a vertical split.
290 NewFileSplitVertical,
291 /// Creates a new file in a horizontal split.
292 NewFileSplitHorizontal,
293 /// Opens a new search.
294 NewSearch,
295 /// Opens a new window.
296 NewWindow,
297 /// Opens multiple files.
298 OpenFiles,
299 /// Opens the current location in terminal.
300 OpenInTerminal,
301 /// Opens the component preview.
302 OpenComponentPreview,
303 /// Reloads the active item.
304 ReloadActiveItem,
305 /// Resets the active dock to its default size.
306 ResetActiveDockSize,
307 /// Resets all open docks to their default sizes.
308 ResetOpenDocksSize,
309 /// Reloads the application
310 Reload,
311 /// Formats and saves the current file, regardless of the format_on_save setting.
312 FormatAndSave,
313 /// Saves the current file with a new name.
314 SaveAs,
315 /// Saves without formatting.
316 SaveWithoutFormat,
317 /// Shuts down all debug adapters.
318 ShutdownDebugAdapters,
319 /// Suppresses the current notification.
320 SuppressNotification,
321 /// Toggles the bottom dock.
322 ToggleBottomDock,
323 /// Toggles centered layout mode.
324 ToggleCenteredLayout,
325 /// Toggles edit prediction feature globally for all files.
326 ToggleEditPrediction,
327 /// Toggles the left dock.
328 ToggleLeftDock,
329 /// Toggles the right dock.
330 ToggleRightDock,
331 /// Toggles zoom on the active pane.
332 ToggleZoom,
333 /// Toggles read-only mode for the active item (if supported by that item).
334 ToggleReadOnlyFile,
335 /// Zooms in on the active pane.
336 ZoomIn,
337 /// Zooms out of the active pane.
338 ZoomOut,
339 /// If any worktrees are in restricted mode, shows a modal with possible actions.
340 /// If the modal is shown already, closes it without trusting any worktree.
341 ToggleWorktreeSecurity,
342 /// Clears all trusted worktrees, placing them in restricted mode on next open.
343 /// Requires restart to take effect on already opened projects.
344 ClearTrustedWorktrees,
345 /// Stops following a collaborator.
346 Unfollow,
347 /// Restores the banner.
348 RestoreBanner,
349 /// Toggles expansion of the selected item.
350 ToggleExpandItem,
351 ]
352);
353
354/// Activates a specific pane by its index.
355#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
356#[action(namespace = workspace)]
357pub struct ActivatePane(pub usize);
358
359/// Moves an item to a specific pane by index.
360#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
361#[action(namespace = workspace)]
362#[serde(deny_unknown_fields)]
363pub struct MoveItemToPane {
364 #[serde(default = "default_1")]
365 pub destination: usize,
366 #[serde(default = "default_true")]
367 pub focus: bool,
368 #[serde(default)]
369 pub clone: bool,
370}
371
372fn default_1() -> usize {
373 1
374}
375
376/// Moves an item to a pane in the specified direction.
377#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
378#[action(namespace = workspace)]
379#[serde(deny_unknown_fields)]
380pub struct MoveItemToPaneInDirection {
381 #[serde(default = "default_right")]
382 pub direction: SplitDirection,
383 #[serde(default = "default_true")]
384 pub focus: bool,
385 #[serde(default)]
386 pub clone: bool,
387}
388
389/// Creates a new file in a split of the desired direction.
390#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
391#[action(namespace = workspace)]
392#[serde(deny_unknown_fields)]
393pub struct NewFileSplit(pub SplitDirection);
394
395fn default_right() -> SplitDirection {
396 SplitDirection::Right
397}
398
399/// Saves all open files in the workspace.
400#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
401#[action(namespace = workspace)]
402#[serde(deny_unknown_fields)]
403pub struct SaveAll {
404 #[serde(default)]
405 pub save_intent: Option<SaveIntent>,
406}
407
408/// Saves the current file with the specified options.
409#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
410#[action(namespace = workspace)]
411#[serde(deny_unknown_fields)]
412pub struct Save {
413 #[serde(default)]
414 pub save_intent: Option<SaveIntent>,
415}
416
417/// Moves Focus to the central panes in the workspace.
418#[derive(Clone, Debug, PartialEq, Eq, Action)]
419#[action(namespace = workspace)]
420pub struct FocusCenterPane;
421
422/// Closes all items and panes in the workspace.
423#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
424#[action(namespace = workspace)]
425#[serde(deny_unknown_fields)]
426pub struct CloseAllItemsAndPanes {
427 #[serde(default)]
428 pub save_intent: Option<SaveIntent>,
429}
430
431/// Closes all inactive tabs and panes in the workspace.
432#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
433#[action(namespace = workspace)]
434#[serde(deny_unknown_fields)]
435pub struct CloseInactiveTabsAndPanes {
436 #[serde(default)]
437 pub save_intent: Option<SaveIntent>,
438}
439
440/// Closes the active item across all panes.
441#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
442#[action(namespace = workspace)]
443#[serde(deny_unknown_fields)]
444pub struct CloseItemInAllPanes {
445 #[serde(default)]
446 pub save_intent: Option<SaveIntent>,
447 #[serde(default)]
448 pub close_pinned: bool,
449}
450
451/// Sends a sequence of keystrokes to the active element.
452#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
453#[action(namespace = workspace)]
454pub struct SendKeystrokes(pub String);
455
456actions!(
457 project_symbols,
458 [
459 /// Toggles the project symbols search.
460 #[action(name = "Toggle")]
461 ToggleProjectSymbols
462 ]
463);
464
465/// Toggles the file finder interface.
466#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
467#[action(namespace = file_finder, name = "Toggle")]
468#[serde(deny_unknown_fields)]
469pub struct ToggleFileFinder {
470 #[serde(default)]
471 pub separate_history: bool,
472}
473
474/// Opens a new terminal in the center.
475#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
476#[action(namespace = workspace)]
477#[serde(deny_unknown_fields)]
478pub struct NewCenterTerminal {
479 /// If true, creates a local terminal even in remote projects.
480 #[serde(default)]
481 pub local: bool,
482}
483
484/// Opens a new terminal.
485#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
486#[action(namespace = workspace)]
487#[serde(deny_unknown_fields)]
488pub struct NewTerminal {
489 /// If true, creates a local terminal even in remote projects.
490 #[serde(default)]
491 pub local: bool,
492}
493
494/// Increases size of a currently focused dock by a given amount of pixels.
495#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
496#[action(namespace = workspace)]
497#[serde(deny_unknown_fields)]
498pub struct IncreaseActiveDockSize {
499 /// For 0px parameter, uses UI font size value.
500 #[serde(default)]
501 pub px: u32,
502}
503
504/// Decreases size of a currently focused dock by a given amount of pixels.
505#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
506#[action(namespace = workspace)]
507#[serde(deny_unknown_fields)]
508pub struct DecreaseActiveDockSize {
509 /// For 0px parameter, uses UI font size value.
510 #[serde(default)]
511 pub px: u32,
512}
513
514/// Increases size of all currently visible docks uniformly, by a given amount of pixels.
515#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
516#[action(namespace = workspace)]
517#[serde(deny_unknown_fields)]
518pub struct IncreaseOpenDocksSize {
519 /// For 0px parameter, uses UI font size value.
520 #[serde(default)]
521 pub px: u32,
522}
523
524/// Decreases size of all currently visible docks uniformly, by a given amount of pixels.
525#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
526#[action(namespace = workspace)]
527#[serde(deny_unknown_fields)]
528pub struct DecreaseOpenDocksSize {
529 /// For 0px parameter, uses UI font size value.
530 #[serde(default)]
531 pub px: u32,
532}
533
534actions!(
535 workspace,
536 [
537 /// Activates the pane to the left.
538 ActivatePaneLeft,
539 /// Activates the pane to the right.
540 ActivatePaneRight,
541 /// Activates the pane above.
542 ActivatePaneUp,
543 /// Activates the pane below.
544 ActivatePaneDown,
545 /// Swaps the current pane with the one to the left.
546 SwapPaneLeft,
547 /// Swaps the current pane with the one to the right.
548 SwapPaneRight,
549 /// Swaps the current pane with the one above.
550 SwapPaneUp,
551 /// Swaps the current pane with the one below.
552 SwapPaneDown,
553 // Swaps the current pane with the first available adjacent pane (searching in order: below, above, right, left) and activates that pane.
554 SwapPaneAdjacent,
555 /// Move the current pane to be at the far left.
556 MovePaneLeft,
557 /// Move the current pane to be at the far right.
558 MovePaneRight,
559 /// Move the current pane to be at the very top.
560 MovePaneUp,
561 /// Move the current pane to be at the very bottom.
562 MovePaneDown,
563 ]
564);
565
566#[derive(PartialEq, Eq, Debug)]
567pub enum CloseIntent {
568 /// Quit the program entirely.
569 Quit,
570 /// Close a window.
571 CloseWindow,
572 /// Replace the workspace in an existing window.
573 ReplaceWindow,
574}
575
576#[derive(Clone)]
577pub struct Toast {
578 id: NotificationId,
579 msg: Cow<'static, str>,
580 autohide: bool,
581 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut Window, &mut App)>)>,
582}
583
584impl Toast {
585 pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
586 Toast {
587 id,
588 msg: msg.into(),
589 on_click: None,
590 autohide: false,
591 }
592 }
593
594 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
595 where
596 M: Into<Cow<'static, str>>,
597 F: Fn(&mut Window, &mut App) + 'static,
598 {
599 self.on_click = Some((message.into(), Arc::new(on_click)));
600 self
601 }
602
603 pub fn autohide(mut self) -> Self {
604 self.autohide = true;
605 self
606 }
607}
608
609impl PartialEq for Toast {
610 fn eq(&self, other: &Self) -> bool {
611 self.id == other.id
612 && self.msg == other.msg
613 && self.on_click.is_some() == other.on_click.is_some()
614 }
615}
616
617/// Opens a new terminal with the specified working directory.
618#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema, Action)]
619#[action(namespace = workspace)]
620#[serde(deny_unknown_fields)]
621pub struct OpenTerminal {
622 pub working_directory: PathBuf,
623 /// If true, creates a local terminal even in remote projects.
624 #[serde(default)]
625 pub local: bool,
626}
627
628#[derive(
629 Clone,
630 Copy,
631 Debug,
632 Default,
633 Hash,
634 PartialEq,
635 Eq,
636 PartialOrd,
637 Ord,
638 serde::Serialize,
639 serde::Deserialize,
640)]
641pub struct WorkspaceId(i64);
642
643impl WorkspaceId {
644 pub fn from_i64(value: i64) -> Self {
645 Self(value)
646 }
647}
648
649impl StaticColumnCount for WorkspaceId {}
650impl Bind for WorkspaceId {
651 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
652 self.0.bind(statement, start_index)
653 }
654}
655impl Column for WorkspaceId {
656 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
657 i64::column(statement, start_index)
658 .map(|(i, next_index)| (Self(i), next_index))
659 .with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
660 }
661}
662impl From<WorkspaceId> for i64 {
663 fn from(val: WorkspaceId) -> Self {
664 val.0
665 }
666}
667
668fn prompt_and_open_paths(
669 app_state: Arc<AppState>,
670 options: PathPromptOptions,
671 create_new_window: bool,
672 cx: &mut App,
673) {
674 if let Some(workspace_window) = local_workspace_windows(cx).into_iter().next() {
675 workspace_window
676 .update(cx, |multi_workspace, window, cx| {
677 let workspace = multi_workspace.workspace().clone();
678 workspace.update(cx, |workspace, cx| {
679 prompt_for_open_path_and_open(
680 workspace,
681 app_state,
682 options,
683 create_new_window,
684 window,
685 cx,
686 );
687 });
688 })
689 .ok();
690 } else {
691 let task = Workspace::new_local(
692 Vec::new(),
693 app_state.clone(),
694 None,
695 None,
696 None,
697 OpenMode::Activate,
698 cx,
699 );
700 cx.spawn(async move |cx| {
701 let OpenResult { window, .. } = task.await?;
702 window.update(cx, |multi_workspace, window, cx| {
703 window.activate_window();
704 let workspace = multi_workspace.workspace().clone();
705 workspace.update(cx, |workspace, cx| {
706 prompt_for_open_path_and_open(
707 workspace,
708 app_state,
709 options,
710 create_new_window,
711 window,
712 cx,
713 );
714 });
715 })?;
716 anyhow::Ok(())
717 })
718 .detach_and_log_err(cx);
719 }
720}
721
722pub fn prompt_for_open_path_and_open(
723 workspace: &mut Workspace,
724 app_state: Arc<AppState>,
725 options: PathPromptOptions,
726 create_new_window: bool,
727 window: &mut Window,
728 cx: &mut Context<Workspace>,
729) {
730 let paths = workspace.prompt_for_open_path(
731 options,
732 DirectoryLister::Local(workspace.project().clone(), app_state.fs.clone()),
733 window,
734 cx,
735 );
736 let multi_workspace_handle = window.window_handle().downcast::<MultiWorkspace>();
737 cx.spawn_in(window, async move |this, cx| {
738 let Some(paths) = paths.await.log_err().flatten() else {
739 return;
740 };
741 if !create_new_window {
742 if let Some(handle) = multi_workspace_handle {
743 if let Some(task) = handle
744 .update(cx, |multi_workspace, window, cx| {
745 multi_workspace.open_project(paths, OpenMode::Activate, window, cx)
746 })
747 .log_err()
748 {
749 task.await.log_err();
750 }
751 return;
752 }
753 }
754 if let Some(task) = this
755 .update_in(cx, |this, window, cx| {
756 this.open_workspace_for_paths(OpenMode::NewWindow, paths, window, cx)
757 })
758 .log_err()
759 {
760 task.await.log_err();
761 }
762 })
763 .detach();
764}
765
766pub fn init(app_state: Arc<AppState>, cx: &mut App) {
767 component::init();
768 theme_preview::init(cx);
769 toast_layer::init(cx);
770 history_manager::init(app_state.fs.clone(), cx);
771
772 cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx))
773 .on_action(|_: &Reload, cx| reload(cx))
774 .on_action(|action: &Open, cx: &mut App| {
775 let app_state = AppState::global(cx);
776 prompt_and_open_paths(
777 app_state,
778 PathPromptOptions {
779 files: true,
780 directories: true,
781 multiple: true,
782 prompt: None,
783 },
784 action.create_new_window,
785 cx,
786 );
787 })
788 .on_action(|_: &OpenFiles, cx: &mut App| {
789 let directories = cx.can_select_mixed_files_and_dirs();
790 let app_state = AppState::global(cx);
791 prompt_and_open_paths(
792 app_state,
793 PathPromptOptions {
794 files: true,
795 directories,
796 multiple: true,
797 prompt: None,
798 },
799 true,
800 cx,
801 );
802 });
803}
804
805type BuildProjectItemFn =
806 fn(AnyEntity, Entity<Project>, Option<&Pane>, &mut Window, &mut App) -> Box<dyn ItemHandle>;
807
808type BuildProjectItemForPathFn =
809 fn(
810 &Entity<Project>,
811 &ProjectPath,
812 &mut Window,
813 &mut App,
814 ) -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
815
816#[derive(Clone, Default)]
817struct ProjectItemRegistry {
818 build_project_item_fns_by_type: HashMap<TypeId, BuildProjectItemFn>,
819 build_project_item_for_path_fns: Vec<BuildProjectItemForPathFn>,
820}
821
822impl ProjectItemRegistry {
823 fn register<T: ProjectItem>(&mut self) {
824 self.build_project_item_fns_by_type.insert(
825 TypeId::of::<T::Item>(),
826 |item, project, pane, window, cx| {
827 let item = item.downcast().unwrap();
828 Box::new(cx.new(|cx| T::for_project_item(project, pane, item, window, cx)))
829 as Box<dyn ItemHandle>
830 },
831 );
832 self.build_project_item_for_path_fns
833 .push(|project, project_path, window, cx| {
834 let project_path = project_path.clone();
835 let is_file = project
836 .read(cx)
837 .entry_for_path(&project_path, cx)
838 .is_some_and(|entry| entry.is_file());
839 let entry_abs_path = project.read(cx).absolute_path(&project_path, cx);
840 let is_local = project.read(cx).is_local();
841 let project_item =
842 <T::Item as project::ProjectItem>::try_open(project, &project_path, cx)?;
843 let project = project.clone();
844 Some(window.spawn(cx, async move |cx| {
845 match project_item.await.with_context(|| {
846 format!(
847 "opening project path {:?}",
848 entry_abs_path.as_deref().unwrap_or(&project_path.path.as_std_path())
849 )
850 }) {
851 Ok(project_item) => {
852 let project_item = project_item;
853 let project_entry_id: Option<ProjectEntryId> =
854 project_item.read_with(cx, project::ProjectItem::entry_id);
855 let build_workspace_item = Box::new(
856 |pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
857 Box::new(cx.new(|cx| {
858 T::for_project_item(
859 project,
860 Some(pane),
861 project_item,
862 window,
863 cx,
864 )
865 })) as Box<dyn ItemHandle>
866 },
867 ) as Box<_>;
868 Ok((project_entry_id, build_workspace_item))
869 }
870 Err(e) => {
871 log::warn!("Failed to open a project item: {e:#}");
872 if e.error_code() == ErrorCode::Internal {
873 if let Some(abs_path) =
874 entry_abs_path.as_deref().filter(|_| is_file)
875 {
876 if let Some(broken_project_item_view) =
877 cx.update(|window, cx| {
878 T::for_broken_project_item(
879 abs_path, is_local, &e, window, cx,
880 )
881 })?
882 {
883 let build_workspace_item = Box::new(
884 move |_: &mut Pane, _: &mut Window, cx: &mut Context<Pane>| {
885 cx.new(|_| broken_project_item_view).boxed_clone()
886 },
887 )
888 as Box<_>;
889 return Ok((None, build_workspace_item));
890 }
891 }
892 }
893 Err(e)
894 }
895 }
896 }))
897 });
898 }
899
900 fn open_path(
901 &self,
902 project: &Entity<Project>,
903 path: &ProjectPath,
904 window: &mut Window,
905 cx: &mut App,
906 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
907 let Some(open_project_item) = self
908 .build_project_item_for_path_fns
909 .iter()
910 .rev()
911 .find_map(|open_project_item| open_project_item(project, path, window, cx))
912 else {
913 return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
914 };
915 open_project_item
916 }
917
918 fn build_item<T: project::ProjectItem>(
919 &self,
920 item: Entity<T>,
921 project: Entity<Project>,
922 pane: Option<&Pane>,
923 window: &mut Window,
924 cx: &mut App,
925 ) -> Option<Box<dyn ItemHandle>> {
926 let build = self
927 .build_project_item_fns_by_type
928 .get(&TypeId::of::<T>())?;
929 Some(build(item.into_any(), project, pane, window, cx))
930 }
931}
932
933type WorkspaceItemBuilder =
934 Box<dyn FnOnce(&mut Pane, &mut Window, &mut Context<Pane>) -> Box<dyn ItemHandle>>;
935
936impl Global for ProjectItemRegistry {}
937
938/// Registers a [ProjectItem] for the app. When opening a file, all the registered
939/// items will get a chance to open the file, starting from the project item that
940/// was added last.
941pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
942 cx.default_global::<ProjectItemRegistry>().register::<I>();
943}
944
945#[derive(Default)]
946pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
947
948struct FollowableViewDescriptor {
949 from_state_proto: fn(
950 Entity<Workspace>,
951 ViewId,
952 &mut Option<proto::view::Variant>,
953 &mut Window,
954 &mut App,
955 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
956 to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
957}
958
959impl Global for FollowableViewRegistry {}
960
961impl FollowableViewRegistry {
962 pub fn register<I: FollowableItem>(cx: &mut App) {
963 cx.default_global::<Self>().0.insert(
964 TypeId::of::<I>(),
965 FollowableViewDescriptor {
966 from_state_proto: |workspace, id, state, window, cx| {
967 I::from_state_proto(workspace, id, state, window, cx).map(|task| {
968 cx.foreground_executor()
969 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
970 })
971 },
972 to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
973 },
974 );
975 }
976
977 pub fn from_state_proto(
978 workspace: Entity<Workspace>,
979 view_id: ViewId,
980 mut state: Option<proto::view::Variant>,
981 window: &mut Window,
982 cx: &mut App,
983 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
984 cx.update_default_global(|this: &mut Self, cx| {
985 this.0.values().find_map(|descriptor| {
986 (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, window, cx)
987 })
988 })
989 }
990
991 pub fn to_followable_view(
992 view: impl Into<AnyView>,
993 cx: &App,
994 ) -> Option<Box<dyn FollowableItemHandle>> {
995 let this = cx.try_global::<Self>()?;
996 let view = view.into();
997 let descriptor = this.0.get(&view.entity_type())?;
998 Some((descriptor.to_followable_view)(&view))
999 }
1000}
1001
1002#[derive(Copy, Clone)]
1003struct SerializableItemDescriptor {
1004 deserialize: fn(
1005 Entity<Project>,
1006 WeakEntity<Workspace>,
1007 WorkspaceId,
1008 ItemId,
1009 &mut Window,
1010 &mut Context<Pane>,
1011 ) -> Task<Result<Box<dyn ItemHandle>>>,
1012 cleanup: fn(WorkspaceId, Vec<ItemId>, &mut Window, &mut App) -> Task<Result<()>>,
1013 view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
1014}
1015
1016#[derive(Default)]
1017struct SerializableItemRegistry {
1018 descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
1019 descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
1020}
1021
1022impl Global for SerializableItemRegistry {}
1023
1024impl SerializableItemRegistry {
1025 fn deserialize(
1026 item_kind: &str,
1027 project: Entity<Project>,
1028 workspace: WeakEntity<Workspace>,
1029 workspace_id: WorkspaceId,
1030 item_item: ItemId,
1031 window: &mut Window,
1032 cx: &mut Context<Pane>,
1033 ) -> Task<Result<Box<dyn ItemHandle>>> {
1034 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
1035 return Task::ready(Err(anyhow!(
1036 "cannot deserialize {}, descriptor not found",
1037 item_kind
1038 )));
1039 };
1040
1041 (descriptor.deserialize)(project, workspace, workspace_id, item_item, window, cx)
1042 }
1043
1044 fn cleanup(
1045 item_kind: &str,
1046 workspace_id: WorkspaceId,
1047 loaded_items: Vec<ItemId>,
1048 window: &mut Window,
1049 cx: &mut App,
1050 ) -> Task<Result<()>> {
1051 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
1052 return Task::ready(Err(anyhow!(
1053 "cannot cleanup {}, descriptor not found",
1054 item_kind
1055 )));
1056 };
1057
1058 (descriptor.cleanup)(workspace_id, loaded_items, window, cx)
1059 }
1060
1061 fn view_to_serializable_item_handle(
1062 view: AnyView,
1063 cx: &App,
1064 ) -> Option<Box<dyn SerializableItemHandle>> {
1065 let this = cx.try_global::<Self>()?;
1066 let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
1067 Some((descriptor.view_to_serializable_item)(view))
1068 }
1069
1070 fn descriptor(item_kind: &str, cx: &App) -> Option<SerializableItemDescriptor> {
1071 let this = cx.try_global::<Self>()?;
1072 this.descriptors_by_kind.get(item_kind).copied()
1073 }
1074}
1075
1076pub fn register_serializable_item<I: SerializableItem>(cx: &mut App) {
1077 let serialized_item_kind = I::serialized_item_kind();
1078
1079 let registry = cx.default_global::<SerializableItemRegistry>();
1080 let descriptor = SerializableItemDescriptor {
1081 deserialize: |project, workspace, workspace_id, item_id, window, cx| {
1082 let task = I::deserialize(project, workspace, workspace_id, item_id, window, cx);
1083 cx.foreground_executor()
1084 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
1085 },
1086 cleanup: |workspace_id, loaded_items, window, cx| {
1087 I::cleanup(workspace_id, loaded_items, window, cx)
1088 },
1089 view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
1090 };
1091 registry
1092 .descriptors_by_kind
1093 .insert(Arc::from(serialized_item_kind), descriptor);
1094 registry
1095 .descriptors_by_type
1096 .insert(TypeId::of::<I>(), descriptor);
1097}
1098
1099pub struct AppState {
1100 pub languages: Arc<LanguageRegistry>,
1101 pub client: Arc<Client>,
1102 pub user_store: Entity<UserStore>,
1103 pub workspace_store: Entity<WorkspaceStore>,
1104 pub fs: Arc<dyn fs::Fs>,
1105 pub build_window_options: fn(Option<Uuid>, &mut App) -> WindowOptions,
1106 pub node_runtime: NodeRuntime,
1107 pub session: Entity<AppSession>,
1108}
1109
1110struct GlobalAppState(Arc<AppState>);
1111
1112impl Global for GlobalAppState {}
1113
1114/// Tracks worktree creation progress for the workspace.
1115/// Read by the title bar to show a loading indicator on the worktree button.
1116#[derive(Default)]
1117pub struct ActiveWorktreeCreation {
1118 pub label: Option<SharedString>,
1119 pub is_switch: bool,
1120}
1121
1122/// Captured workspace state used when switching between worktrees.
1123/// Stores the layout and open files so they can be restored in the new workspace.
1124pub struct PreviousWorkspaceState {
1125 pub dock_structure: DockStructure,
1126 pub open_file_paths: Vec<PathBuf>,
1127 pub active_file_path: Option<PathBuf>,
1128 pub focused_dock: Option<DockPosition>,
1129}
1130
1131pub struct WorkspaceStore {
1132 workspaces: HashSet<(gpui::AnyWindowHandle, WeakEntity<Workspace>)>,
1133 client: Arc<Client>,
1134 _subscriptions: Vec<client::Subscription>,
1135}
1136
1137#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
1138pub enum CollaboratorId {
1139 PeerId(PeerId),
1140 Agent,
1141}
1142
1143impl From<PeerId> for CollaboratorId {
1144 fn from(peer_id: PeerId) -> Self {
1145 CollaboratorId::PeerId(peer_id)
1146 }
1147}
1148
1149impl From<&PeerId> for CollaboratorId {
1150 fn from(peer_id: &PeerId) -> Self {
1151 CollaboratorId::PeerId(*peer_id)
1152 }
1153}
1154
1155#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
1156struct Follower {
1157 project_id: Option<u64>,
1158 peer_id: PeerId,
1159}
1160
1161impl AppState {
1162 #[track_caller]
1163 pub fn global(cx: &App) -> Arc<Self> {
1164 cx.global::<GlobalAppState>().0.clone()
1165 }
1166 pub fn try_global(cx: &App) -> Option<Arc<Self>> {
1167 cx.try_global::<GlobalAppState>()
1168 .map(|state| state.0.clone())
1169 }
1170 pub fn set_global(state: Arc<AppState>, cx: &mut App) {
1171 cx.set_global(GlobalAppState(state));
1172 }
1173
1174 #[cfg(any(test, feature = "test-support"))]
1175 pub fn test(cx: &mut App) -> Arc<Self> {
1176 use fs::Fs;
1177 use node_runtime::NodeRuntime;
1178 use session::Session;
1179 use settings::SettingsStore;
1180
1181 if !cx.has_global::<SettingsStore>() {
1182 let settings_store = SettingsStore::test(cx);
1183 cx.set_global(settings_store);
1184 }
1185
1186 let fs = fs::FakeFs::new(cx.background_executor().clone());
1187 <dyn Fs>::set_global(fs.clone(), cx);
1188 let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
1189 let clock = Arc::new(clock::FakeSystemClock::new());
1190 let http_client = http_client::FakeHttpClient::with_404_response();
1191 let client = Client::new(clock, http_client, cx);
1192 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
1193 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1194 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
1195
1196 theme_settings::init(theme::LoadThemes::JustBase, cx);
1197 client::init(&client, cx);
1198
1199 Arc::new(Self {
1200 client,
1201 fs,
1202 languages,
1203 user_store,
1204 workspace_store,
1205 node_runtime: NodeRuntime::unavailable(),
1206 build_window_options: |_, _| Default::default(),
1207 session,
1208 })
1209 }
1210}
1211
1212struct DelayedDebouncedEditAction {
1213 task: Option<Task<()>>,
1214 cancel_channel: Option<oneshot::Sender<()>>,
1215}
1216
1217impl DelayedDebouncedEditAction {
1218 fn new() -> DelayedDebouncedEditAction {
1219 DelayedDebouncedEditAction {
1220 task: None,
1221 cancel_channel: None,
1222 }
1223 }
1224
1225 fn fire_new<F>(
1226 &mut self,
1227 delay: Duration,
1228 window: &mut Window,
1229 cx: &mut Context<Workspace>,
1230 func: F,
1231 ) where
1232 F: 'static
1233 + Send
1234 + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> Task<Result<()>>,
1235 {
1236 if let Some(channel) = self.cancel_channel.take() {
1237 _ = channel.send(());
1238 }
1239
1240 let (sender, mut receiver) = oneshot::channel::<()>();
1241 self.cancel_channel = Some(sender);
1242
1243 let previous_task = self.task.take();
1244 self.task = Some(cx.spawn_in(window, async move |workspace, cx| {
1245 let mut timer = cx.background_executor().timer(delay).fuse();
1246 if let Some(previous_task) = previous_task {
1247 previous_task.await;
1248 }
1249
1250 futures::select_biased! {
1251 _ = receiver => return,
1252 _ = timer => {}
1253 }
1254
1255 if let Some(result) = workspace
1256 .update_in(cx, |workspace, window, cx| (func)(workspace, window, cx))
1257 .log_err()
1258 {
1259 result.await.log_err();
1260 }
1261 }));
1262 }
1263}
1264
1265pub enum Event {
1266 PaneAdded(Entity<Pane>),
1267 PaneRemoved,
1268 ItemAdded {
1269 item: Box<dyn ItemHandle>,
1270 },
1271 ActiveItemChanged,
1272 ItemRemoved {
1273 item_id: EntityId,
1274 },
1275 UserSavedItem {
1276 pane: WeakEntity<Pane>,
1277 item: Box<dyn WeakItemHandle>,
1278 save_intent: SaveIntent,
1279 },
1280 ContactRequestedJoin(u64),
1281 WorkspaceCreated(WeakEntity<Workspace>),
1282 OpenBundledFile {
1283 text: Cow<'static, str>,
1284 title: &'static str,
1285 language: &'static str,
1286 },
1287 ZoomChanged,
1288 ModalOpened,
1289 Activate,
1290 PanelAdded(AnyView),
1291 WorktreeCreationChanged,
1292}
1293
1294#[derive(Debug, Clone)]
1295pub enum OpenVisible {
1296 All,
1297 None,
1298 OnlyFiles,
1299 OnlyDirectories,
1300}
1301
1302enum WorkspaceLocation {
1303 // Valid local paths or SSH project to serialize
1304 Location(SerializedWorkspaceLocation, PathList),
1305 // No valid location found hence clear session id
1306 DetachFromSession,
1307 // No valid location found to serialize
1308 None,
1309}
1310
1311type PromptForNewPath = Box<
1312 dyn Fn(
1313 &mut Workspace,
1314 DirectoryLister,
1315 Option<String>,
1316 &mut Window,
1317 &mut Context<Workspace>,
1318 ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
1319>;
1320
1321type PromptForOpenPath = Box<
1322 dyn Fn(
1323 &mut Workspace,
1324 DirectoryLister,
1325 &mut Window,
1326 &mut Context<Workspace>,
1327 ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
1328>;
1329
1330#[derive(Default)]
1331struct DispatchingKeystrokes {
1332 dispatched: HashSet<Vec<Keystroke>>,
1333 queue: VecDeque<Keystroke>,
1334 task: Option<Shared<Task<()>>>,
1335}
1336
1337/// Collects everything project-related for a certain window opened.
1338/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
1339///
1340/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
1341/// The `Workspace` owns everybody's state and serves as a default, "global context",
1342/// that can be used to register a global action to be triggered from any place in the window.
1343pub struct Workspace {
1344 weak_self: WeakEntity<Self>,
1345 workspace_actions: Vec<Box<dyn Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div>>,
1346 zoomed: Option<AnyWeakView>,
1347 previous_dock_drag_coordinates: Option<Point<Pixels>>,
1348 zoomed_position: Option<DockPosition>,
1349 center: PaneGroup,
1350 left_dock: Entity<Dock>,
1351 bottom_dock: Entity<Dock>,
1352 right_dock: Entity<Dock>,
1353 panes: Vec<Entity<Pane>>,
1354 panes_by_item: HashMap<EntityId, WeakEntity<Pane>>,
1355 active_pane: Entity<Pane>,
1356 last_active_center_pane: Option<WeakEntity<Pane>>,
1357 last_active_view_id: Option<proto::ViewId>,
1358 status_bar: Entity<StatusBar>,
1359 pub(crate) modal_layer: Entity<ModalLayer>,
1360 toast_layer: Entity<ToastLayer>,
1361 titlebar_item: Option<AnyView>,
1362 notifications: Notifications,
1363 suppressed_notifications: HashSet<NotificationId>,
1364 project: Entity<Project>,
1365 follower_states: HashMap<CollaboratorId, FollowerState>,
1366 last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
1367 window_edited: bool,
1368 last_window_title: Option<String>,
1369 dirty_items: HashMap<EntityId, Subscription>,
1370 active_call: Option<(GlobalAnyActiveCall, Vec<Subscription>)>,
1371 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
1372 database_id: Option<WorkspaceId>,
1373 app_state: Arc<AppState>,
1374 dispatching_keystrokes: Rc<RefCell<DispatchingKeystrokes>>,
1375 _subscriptions: Vec<Subscription>,
1376 _apply_leader_updates: Task<Result<()>>,
1377 _observe_current_user: Task<Result<()>>,
1378 _schedule_serialize_workspace: Option<Task<()>>,
1379 _serialize_workspace_task: Option<Task<()>>,
1380 _schedule_serialize_ssh_paths: Option<Task<()>>,
1381 pane_history_timestamp: Arc<AtomicUsize>,
1382 bounds: Bounds<Pixels>,
1383 pub centered_layout: bool,
1384 bounds_save_task_queued: Option<Task<()>>,
1385 on_prompt_for_new_path: Option<PromptForNewPath>,
1386 on_prompt_for_open_path: Option<PromptForOpenPath>,
1387 terminal_provider: Option<Box<dyn TerminalProvider>>,
1388 debugger_provider: Option<Arc<dyn DebuggerProvider>>,
1389 serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
1390 _items_serializer: Task<Result<()>>,
1391 session_id: Option<String>,
1392 scheduled_tasks: Vec<Task<()>>,
1393 last_open_dock_positions: Vec<DockPosition>,
1394 removing: bool,
1395 open_in_dev_container: bool,
1396 _dev_container_task: Option<Task<Result<()>>>,
1397 _panels_task: Option<Task<Result<()>>>,
1398 sidebar_focus_handle: Option<FocusHandle>,
1399 multi_workspace: Option<WeakEntity<MultiWorkspace>>,
1400 active_worktree_creation: ActiveWorktreeCreation,
1401}
1402
1403impl EventEmitter<Event> for Workspace {}
1404
1405#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
1406pub struct ViewId {
1407 pub creator: CollaboratorId,
1408 pub id: u64,
1409}
1410
1411pub struct FollowerState {
1412 center_pane: Entity<Pane>,
1413 dock_pane: Option<Entity<Pane>>,
1414 active_view_id: Option<ViewId>,
1415 items_by_leader_view_id: HashMap<ViewId, FollowerView>,
1416}
1417
1418struct FollowerView {
1419 view: Box<dyn FollowableItemHandle>,
1420 location: Option<proto::PanelId>,
1421}
1422
1423#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1424pub enum OpenMode {
1425 /// Open the workspace in a new window.
1426 NewWindow,
1427 /// Add to the window's multi workspace without activating it (used during deserialization).
1428 Add,
1429 /// Add to the window's multi workspace and activate it.
1430 #[default]
1431 Activate,
1432}
1433
1434impl Workspace {
1435 pub fn new(
1436 workspace_id: Option<WorkspaceId>,
1437 project: Entity<Project>,
1438 app_state: Arc<AppState>,
1439 window: &mut Window,
1440 cx: &mut Context<Self>,
1441 ) -> Self {
1442 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1443 cx.subscribe(&trusted_worktrees, |_, worktrees_store, e, cx| {
1444 if let TrustedWorktreesEvent::Trusted(..) = e {
1445 // Do not persist auto trusted worktrees
1446 if !ProjectSettings::get_global(cx).session.trust_all_worktrees {
1447 worktrees_store.update(cx, |worktrees_store, cx| {
1448 worktrees_store.schedule_serialization(
1449 cx,
1450 |new_trusted_worktrees, cx| {
1451 let timeout =
1452 cx.background_executor().timer(SERIALIZATION_THROTTLE_TIME);
1453 let db = WorkspaceDb::global(cx);
1454 cx.background_spawn(async move {
1455 timeout.await;
1456 db.save_trusted_worktrees(new_trusted_worktrees)
1457 .await
1458 .log_err();
1459 })
1460 },
1461 )
1462 });
1463 }
1464 }
1465 })
1466 .detach();
1467
1468 cx.observe_global::<SettingsStore>(|_, cx| {
1469 if ProjectSettings::get_global(cx).session.trust_all_worktrees {
1470 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1471 trusted_worktrees.update(cx, |trusted_worktrees, cx| {
1472 trusted_worktrees.auto_trust_all(cx);
1473 })
1474 }
1475 }
1476 })
1477 .detach();
1478 }
1479
1480 cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
1481 match event {
1482 project::Event::RemoteIdChanged(_) => {
1483 this.update_window_title(window, cx);
1484 }
1485
1486 project::Event::CollaboratorLeft(peer_id) => {
1487 this.collaborator_left(*peer_id, window, cx);
1488 }
1489
1490 &project::Event::WorktreeRemoved(_) => {
1491 this.update_window_title(window, cx);
1492 this.serialize_workspace(window, cx);
1493 this.update_history(cx);
1494 }
1495
1496 &project::Event::WorktreeAdded(id) => {
1497 this.update_window_title(window, cx);
1498 if this
1499 .project()
1500 .read(cx)
1501 .worktree_for_id(id, cx)
1502 .is_some_and(|wt| wt.read(cx).is_visible())
1503 {
1504 this.serialize_workspace(window, cx);
1505 this.update_history(cx);
1506 }
1507 }
1508 project::Event::WorktreeUpdatedEntries(..) => {
1509 this.update_window_title(window, cx);
1510 this.serialize_workspace(window, cx);
1511 }
1512
1513 project::Event::DisconnectedFromHost => {
1514 this.update_window_edited(window, cx);
1515 let leaders_to_unfollow =
1516 this.follower_states.keys().copied().collect::<Vec<_>>();
1517 for leader_id in leaders_to_unfollow {
1518 this.unfollow(leader_id, window, cx);
1519 }
1520 }
1521
1522 project::Event::DisconnectedFromRemote {
1523 server_not_running: _,
1524 } => {
1525 this.update_window_edited(window, cx);
1526 }
1527
1528 project::Event::Closed => {
1529 window.remove_window();
1530 }
1531
1532 project::Event::DeletedEntry(_, entry_id) => {
1533 for pane in this.panes.iter() {
1534 pane.update(cx, |pane, cx| {
1535 pane.handle_deleted_project_item(*entry_id, window, cx)
1536 });
1537 }
1538 }
1539
1540 project::Event::Toast {
1541 notification_id,
1542 message,
1543 link,
1544 } => this.show_notification(
1545 NotificationId::named(notification_id.clone()),
1546 cx,
1547 |cx| {
1548 let mut notification = MessageNotification::new(message.clone(), cx);
1549 if let Some(link) = link {
1550 notification = notification
1551 .more_info_message(link.label)
1552 .more_info_url(link.url);
1553 }
1554
1555 cx.new(|_| notification)
1556 },
1557 ),
1558
1559 project::Event::HideToast { notification_id } => {
1560 this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
1561 }
1562
1563 project::Event::LanguageServerPrompt(request) => {
1564 struct LanguageServerPrompt;
1565
1566 this.show_notification(
1567 NotificationId::composite::<LanguageServerPrompt>(request.id),
1568 cx,
1569 |cx| {
1570 cx.new(|cx| {
1571 notifications::LanguageServerPrompt::new(request.clone(), cx)
1572 })
1573 },
1574 );
1575 }
1576
1577 project::Event::AgentLocationChanged => {
1578 this.handle_agent_location_changed(window, cx)
1579 }
1580
1581 _ => {}
1582 }
1583 cx.notify()
1584 })
1585 .detach();
1586
1587 cx.subscribe_in(
1588 &project.read(cx).breakpoint_store(),
1589 window,
1590 |workspace, _, event, window, cx| match event {
1591 BreakpointStoreEvent::BreakpointsUpdated(_, _)
1592 | BreakpointStoreEvent::BreakpointsCleared(_) => {
1593 workspace.serialize_workspace(window, cx);
1594 }
1595 BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {}
1596 },
1597 )
1598 .detach();
1599 if let Some(toolchain_store) = project.read(cx).toolchain_store() {
1600 cx.subscribe_in(
1601 &toolchain_store,
1602 window,
1603 |workspace, _, event, window, cx| match event {
1604 ToolchainStoreEvent::CustomToolchainsModified => {
1605 workspace.serialize_workspace(window, cx);
1606 }
1607 _ => {}
1608 },
1609 )
1610 .detach();
1611 }
1612
1613 cx.on_focus_lost(window, |this, window, cx| {
1614 let focus_handle = this.focus_handle(cx);
1615 window.focus(&focus_handle, cx);
1616 })
1617 .detach();
1618
1619 let weak_handle = cx.entity().downgrade();
1620 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
1621
1622 let center_pane = cx.new(|cx| {
1623 let mut center_pane = Pane::new(
1624 weak_handle.clone(),
1625 project.clone(),
1626 pane_history_timestamp.clone(),
1627 None,
1628 NewFile.boxed_clone(),
1629 true,
1630 window,
1631 cx,
1632 );
1633 center_pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
1634 center_pane.set_should_display_welcome_page(true);
1635 center_pane
1636 });
1637 cx.subscribe_in(¢er_pane, window, Self::handle_pane_event)
1638 .detach();
1639
1640 window.focus(¢er_pane.focus_handle(cx), cx);
1641
1642 cx.emit(Event::PaneAdded(center_pane.clone()));
1643
1644 let any_window_handle = window.window_handle();
1645 app_state.workspace_store.update(cx, |store, _| {
1646 store
1647 .workspaces
1648 .insert((any_window_handle, weak_handle.clone()));
1649 });
1650
1651 let mut current_user = app_state.user_store.read(cx).watch_current_user();
1652 let mut connection_status = app_state.client.status();
1653 let _observe_current_user = cx.spawn_in(window, async move |this, cx| {
1654 current_user.next().await;
1655 connection_status.next().await;
1656 let mut stream =
1657 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
1658
1659 while stream.recv().await.is_some() {
1660 this.update(cx, |_, cx| cx.notify())?;
1661 }
1662 anyhow::Ok(())
1663 });
1664
1665 // All leader updates are enqueued and then processed in a single task, so
1666 // that each asynchronous operation can be run in order.
1667 let (leader_updates_tx, mut leader_updates_rx) =
1668 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
1669 let _apply_leader_updates = cx.spawn_in(window, async move |this, cx| {
1670 while let Some((leader_id, update)) = leader_updates_rx.next().await {
1671 Self::process_leader_update(&this, leader_id, update, cx)
1672 .await
1673 .log_err();
1674 }
1675
1676 Ok(())
1677 });
1678
1679 cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
1680 let modal_layer = cx.new(|_| ModalLayer::new());
1681 let toast_layer = cx.new(|_| ToastLayer::new());
1682 cx.subscribe(
1683 &modal_layer,
1684 |_, _, _: &modal_layer::ModalOpenedEvent, cx| {
1685 cx.emit(Event::ModalOpened);
1686 },
1687 )
1688 .detach();
1689
1690 let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx);
1691 let bottom_dock = Dock::new(DockPosition::Bottom, modal_layer.clone(), window, cx);
1692 let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), window, cx);
1693 let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx));
1694 let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx));
1695 let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx));
1696 let multi_workspace = window
1697 .root::<MultiWorkspace>()
1698 .flatten()
1699 .map(|mw| mw.downgrade());
1700 let status_bar = cx.new(|cx| {
1701 let mut status_bar =
1702 StatusBar::new(¢er_pane.clone(), multi_workspace.clone(), window, cx);
1703 status_bar.add_left_item(left_dock_buttons, window, cx);
1704 status_bar.add_right_item(right_dock_buttons, window, cx);
1705 status_bar.add_right_item(bottom_dock_buttons, window, cx);
1706 status_bar
1707 });
1708
1709 let session_id = app_state.session.read(cx).id().to_owned();
1710
1711 let mut active_call = None;
1712 if let Some(call) = GlobalAnyActiveCall::try_global(cx).cloned() {
1713 let subscriptions =
1714 vec![
1715 call.0
1716 .subscribe(window, cx, Box::new(Self::on_active_call_event)),
1717 ];
1718 active_call = Some((call, subscriptions));
1719 }
1720
1721 let (serializable_items_tx, serializable_items_rx) =
1722 mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
1723 let _items_serializer = cx.spawn_in(window, async move |this, cx| {
1724 Self::serialize_items(&this, serializable_items_rx, cx).await
1725 });
1726
1727 let subscriptions = vec![
1728 cx.observe_window_activation(window, Self::on_window_activation_changed),
1729 cx.observe_window_bounds(window, move |this, window, cx| {
1730 if this.bounds_save_task_queued.is_some() {
1731 return;
1732 }
1733 this.bounds_save_task_queued = Some(cx.spawn_in(window, async move |this, cx| {
1734 cx.background_executor()
1735 .timer(Duration::from_millis(100))
1736 .await;
1737 this.update_in(cx, |this, window, cx| {
1738 this.save_window_bounds(window, cx).detach();
1739 this.bounds_save_task_queued.take();
1740 })
1741 .ok();
1742 }));
1743 cx.notify();
1744 }),
1745 cx.observe_window_appearance(window, |_, window, cx| {
1746 let window_appearance = window.appearance();
1747
1748 *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
1749
1750 theme_settings::reload_theme(cx);
1751 theme_settings::reload_icon_theme(cx);
1752 }),
1753 cx.on_release({
1754 let weak_handle = weak_handle.clone();
1755 move |this, cx| {
1756 this.app_state.workspace_store.update(cx, move |store, _| {
1757 store.workspaces.retain(|(_, weak)| weak != &weak_handle);
1758 })
1759 }
1760 }),
1761 ];
1762
1763 cx.defer_in(window, move |this, window, cx| {
1764 this.update_window_title(window, cx);
1765 this.show_initial_notifications(cx);
1766 });
1767
1768 let mut center = PaneGroup::new(center_pane.clone());
1769 center.set_is_center(true);
1770 center.mark_positions(cx);
1771
1772 Workspace {
1773 weak_self: weak_handle.clone(),
1774 zoomed: None,
1775 zoomed_position: None,
1776 previous_dock_drag_coordinates: None,
1777 center,
1778 panes: vec![center_pane.clone()],
1779 panes_by_item: Default::default(),
1780 active_pane: center_pane.clone(),
1781 last_active_center_pane: Some(center_pane.downgrade()),
1782 last_active_view_id: None,
1783 status_bar,
1784 modal_layer,
1785 toast_layer,
1786 titlebar_item: None,
1787 notifications: Notifications::default(),
1788 suppressed_notifications: HashSet::default(),
1789 left_dock,
1790 bottom_dock,
1791 right_dock,
1792 _panels_task: None,
1793 project: project.clone(),
1794 follower_states: Default::default(),
1795 last_leaders_by_pane: Default::default(),
1796 dispatching_keystrokes: Default::default(),
1797 window_edited: false,
1798 last_window_title: None,
1799 dirty_items: Default::default(),
1800 active_call,
1801 database_id: workspace_id,
1802 app_state,
1803 _observe_current_user,
1804 _apply_leader_updates,
1805 _schedule_serialize_workspace: None,
1806 _serialize_workspace_task: None,
1807 _schedule_serialize_ssh_paths: None,
1808 leader_updates_tx,
1809 _subscriptions: subscriptions,
1810 pane_history_timestamp,
1811 workspace_actions: Default::default(),
1812 // This data will be incorrect, but it will be overwritten by the time it needs to be used.
1813 bounds: Default::default(),
1814 centered_layout: false,
1815 bounds_save_task_queued: None,
1816 on_prompt_for_new_path: None,
1817 on_prompt_for_open_path: None,
1818 terminal_provider: None,
1819 debugger_provider: None,
1820 serializable_items_tx,
1821 _items_serializer,
1822 session_id: Some(session_id),
1823
1824 scheduled_tasks: Vec::new(),
1825 last_open_dock_positions: Vec::new(),
1826 removing: false,
1827 sidebar_focus_handle: None,
1828 multi_workspace,
1829 active_worktree_creation: ActiveWorktreeCreation::default(),
1830 open_in_dev_container: false,
1831 _dev_container_task: None,
1832 }
1833 }
1834
1835 pub fn new_local(
1836 abs_paths: Vec<PathBuf>,
1837 app_state: Arc<AppState>,
1838 requesting_window: Option<WindowHandle<MultiWorkspace>>,
1839 env: Option<HashMap<String, String>>,
1840 init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
1841 open_mode: OpenMode,
1842 cx: &mut App,
1843 ) -> Task<anyhow::Result<OpenResult>> {
1844 let project_handle = Project::local(
1845 app_state.client.clone(),
1846 app_state.node_runtime.clone(),
1847 app_state.user_store.clone(),
1848 app_state.languages.clone(),
1849 app_state.fs.clone(),
1850 env,
1851 Default::default(),
1852 cx,
1853 );
1854
1855 let db = WorkspaceDb::global(cx);
1856 let kvp = db::kvp::KeyValueStore::global(cx);
1857 cx.spawn(async move |cx| {
1858 let mut paths_to_open = Vec::with_capacity(abs_paths.len());
1859 for path in abs_paths.into_iter() {
1860 if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() {
1861 paths_to_open.push(canonical)
1862 } else {
1863 paths_to_open.push(path)
1864 }
1865 }
1866
1867 let serialized_workspace = db.workspace_for_roots(paths_to_open.as_slice());
1868
1869 if let Some(paths) = serialized_workspace.as_ref().map(|ws| &ws.paths) {
1870 paths_to_open = paths.ordered_paths().cloned().collect();
1871 if !paths.is_lexicographically_ordered() {
1872 project_handle.update(cx, |project, cx| {
1873 project.set_worktrees_reordered(true, cx);
1874 });
1875 }
1876 }
1877
1878 // Get project paths for all of the abs_paths
1879 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
1880 Vec::with_capacity(paths_to_open.len());
1881
1882 for path in paths_to_open.into_iter() {
1883 if let Some((_, project_entry)) = cx
1884 .update(|cx| {
1885 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
1886 })
1887 .await
1888 .log_err()
1889 {
1890 project_paths.push((path, Some(project_entry)));
1891 } else {
1892 project_paths.push((path, None));
1893 }
1894 }
1895
1896 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
1897 serialized_workspace.id
1898 } else {
1899 db.next_id().await.unwrap_or_else(|_| Default::default())
1900 };
1901
1902 let toolchains = db.toolchains(workspace_id).await?;
1903
1904 for (toolchain, worktree_path, path) in toolchains {
1905 let toolchain_path = PathBuf::from(toolchain.path.clone().to_string());
1906 let Some(worktree_id) = project_handle.read_with(cx, |this, cx| {
1907 this.find_worktree(&worktree_path, cx)
1908 .and_then(|(worktree, rel_path)| {
1909 if rel_path.is_empty() {
1910 Some(worktree.read(cx).id())
1911 } else {
1912 None
1913 }
1914 })
1915 }) else {
1916 // We did not find a worktree with a given path, but that's whatever.
1917 continue;
1918 };
1919 if !app_state.fs.is_file(toolchain_path.as_path()).await {
1920 continue;
1921 }
1922
1923 project_handle
1924 .update(cx, |this, cx| {
1925 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
1926 })
1927 .await;
1928 }
1929 if let Some(workspace) = serialized_workspace.as_ref() {
1930 project_handle.update(cx, |this, cx| {
1931 for (scope, toolchains) in &workspace.user_toolchains {
1932 for toolchain in toolchains {
1933 this.add_toolchain(toolchain.clone(), scope.clone(), cx);
1934 }
1935 }
1936 });
1937 }
1938
1939 let window_to_replace = match open_mode {
1940 OpenMode::NewWindow => None,
1941 _ => requesting_window,
1942 };
1943
1944 let (window, workspace): (WindowHandle<MultiWorkspace>, Entity<Workspace>) =
1945 if let Some(window) = window_to_replace {
1946 let centered_layout = serialized_workspace
1947 .as_ref()
1948 .map(|w| w.centered_layout)
1949 .unwrap_or(false);
1950
1951 let workspace = window.update(cx, |multi_workspace, window, cx| {
1952 let workspace = cx.new(|cx| {
1953 let mut workspace = Workspace::new(
1954 Some(workspace_id),
1955 project_handle.clone(),
1956 app_state.clone(),
1957 window,
1958 cx,
1959 );
1960
1961 workspace.centered_layout = centered_layout;
1962
1963 // Call init callback to add items before window renders
1964 if let Some(init) = init {
1965 init(&mut workspace, window, cx);
1966 }
1967
1968 workspace
1969 });
1970 match open_mode {
1971 OpenMode::Activate => {
1972 multi_workspace.activate(workspace.clone(), None, window, cx);
1973 }
1974 OpenMode::Add => {
1975 multi_workspace.add(workspace.clone(), &*window, cx);
1976 }
1977 OpenMode::NewWindow => {
1978 unreachable!()
1979 }
1980 }
1981 workspace
1982 })?;
1983 (window, workspace)
1984 } else {
1985 let window_bounds_override = window_bounds_env_override();
1986
1987 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
1988 (Some(WindowBounds::Windowed(bounds)), None)
1989 } else if let Some(workspace) = serialized_workspace.as_ref()
1990 && let Some(display) = workspace.display
1991 && let Some(bounds) = workspace.window_bounds.as_ref()
1992 {
1993 // Reopening an existing workspace - restore its saved bounds
1994 (Some(bounds.0), Some(display))
1995 } else if let Some((display, bounds)) =
1996 persistence::read_default_window_bounds(&kvp)
1997 {
1998 // New or empty workspace - use the last known window bounds
1999 (Some(bounds), Some(display))
2000 } else {
2001 // New window - let GPUI's default_bounds() handle cascading
2002 (None, None)
2003 };
2004
2005 // Use the serialized workspace to construct the new window
2006 let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx));
2007 options.window_bounds = window_bounds;
2008 let centered_layout = serialized_workspace
2009 .as_ref()
2010 .map(|w| w.centered_layout)
2011 .unwrap_or(false);
2012 let window = cx.open_window(options, {
2013 let app_state = app_state.clone();
2014 let project_handle = project_handle.clone();
2015 move |window, cx| {
2016 let workspace = cx.new(|cx| {
2017 let mut workspace = Workspace::new(
2018 Some(workspace_id),
2019 project_handle,
2020 app_state,
2021 window,
2022 cx,
2023 );
2024 workspace.centered_layout = centered_layout;
2025
2026 // Call init callback to add items before window renders
2027 if let Some(init) = init {
2028 init(&mut workspace, window, cx);
2029 }
2030
2031 workspace
2032 });
2033 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
2034 }
2035 })?;
2036 let workspace =
2037 window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
2038 multi_workspace.workspace().clone()
2039 })?;
2040 (window, workspace)
2041 };
2042
2043 notify_if_database_failed(window, cx);
2044 // Check if this is an empty workspace (no paths to open)
2045 // An empty workspace is one where project_paths is empty
2046 let is_empty_workspace = project_paths.is_empty();
2047 // Check if serialized workspace has paths before it's moved
2048 let serialized_workspace_has_paths = serialized_workspace
2049 .as_ref()
2050 .map(|ws| !ws.paths.is_empty())
2051 .unwrap_or(false);
2052
2053 let opened_items = window
2054 .update(cx, |_, window, cx| {
2055 workspace.update(cx, |_workspace: &mut Workspace, cx| {
2056 open_items(serialized_workspace, project_paths, window, cx)
2057 })
2058 })?
2059 .await
2060 .unwrap_or_default();
2061
2062 // Restore default dock state for empty workspaces
2063 // Only restore if:
2064 // 1. This is an empty workspace (no paths), AND
2065 // 2. The serialized workspace either doesn't exist or has no paths
2066 if is_empty_workspace && !serialized_workspace_has_paths {
2067 if let Some(default_docks) = persistence::read_default_dock_state(&kvp) {
2068 window
2069 .update(cx, |_, window, cx| {
2070 workspace.update(cx, |workspace, cx| {
2071 for (dock, serialized_dock) in [
2072 (&workspace.right_dock, &default_docks.right),
2073 (&workspace.left_dock, &default_docks.left),
2074 (&workspace.bottom_dock, &default_docks.bottom),
2075 ] {
2076 dock.update(cx, |dock, cx| {
2077 dock.serialized_dock = Some(serialized_dock.clone());
2078 dock.restore_state(window, cx);
2079 });
2080 }
2081 cx.notify();
2082 });
2083 })
2084 .log_err();
2085 }
2086 }
2087
2088 window
2089 .update(cx, |_, _window, cx| {
2090 workspace.update(cx, |this: &mut Workspace, cx| {
2091 this.update_history(cx);
2092 });
2093 })
2094 .log_err();
2095
2096 if open_mode == OpenMode::NewWindow {
2097 window
2098 .update(cx, |_, window, _cx| {
2099 window.activate_window();
2100 })
2101 .log_err();
2102 }
2103
2104 Ok(OpenResult {
2105 window,
2106 workspace,
2107 opened_items,
2108 })
2109 })
2110 }
2111
2112 pub fn project_group_key(&self, cx: &App) -> ProjectGroupKey {
2113 self.project.read(cx).project_group_key(cx)
2114 }
2115
2116 pub fn weak_handle(&self) -> WeakEntity<Self> {
2117 self.weak_self.clone()
2118 }
2119
2120 pub fn left_dock(&self) -> &Entity<Dock> {
2121 &self.left_dock
2122 }
2123
2124 pub fn bottom_dock(&self) -> &Entity<Dock> {
2125 &self.bottom_dock
2126 }
2127
2128 pub fn set_bottom_dock_layout(
2129 &mut self,
2130 layout: BottomDockLayout,
2131 window: &mut Window,
2132 cx: &mut Context<Self>,
2133 ) {
2134 let fs = self.project().read(cx).fs();
2135 settings::update_settings_file(fs.clone(), cx, move |content, _cx| {
2136 content.workspace.bottom_dock_layout = Some(layout);
2137 });
2138
2139 cx.notify();
2140 self.serialize_workspace(window, cx);
2141 }
2142
2143 pub fn right_dock(&self) -> &Entity<Dock> {
2144 &self.right_dock
2145 }
2146
2147 pub fn all_docks(&self) -> [&Entity<Dock>; 3] {
2148 [&self.left_dock, &self.bottom_dock, &self.right_dock]
2149 }
2150
2151 pub fn capture_dock_state(&self, _window: &Window, cx: &App) -> DockStructure {
2152 let left_dock = self.left_dock.read(cx);
2153 let left_visible = left_dock.is_open();
2154 let left_active_panel = left_dock
2155 .active_panel()
2156 .map(|panel| panel.persistent_name().to_string());
2157 // `zoomed_position` is kept in sync with individual panel zoom state
2158 // by the dock code in `Dock::new` and `Dock::add_panel`.
2159 let left_dock_zoom = self.zoomed_position == Some(DockPosition::Left);
2160
2161 let right_dock = self.right_dock.read(cx);
2162 let right_visible = right_dock.is_open();
2163 let right_active_panel = right_dock
2164 .active_panel()
2165 .map(|panel| panel.persistent_name().to_string());
2166 let right_dock_zoom = self.zoomed_position == Some(DockPosition::Right);
2167
2168 let bottom_dock = self.bottom_dock.read(cx);
2169 let bottom_visible = bottom_dock.is_open();
2170 let bottom_active_panel = bottom_dock
2171 .active_panel()
2172 .map(|panel| panel.persistent_name().to_string());
2173 let bottom_dock_zoom = self.zoomed_position == Some(DockPosition::Bottom);
2174
2175 DockStructure {
2176 left: DockData {
2177 visible: left_visible,
2178 active_panel: left_active_panel,
2179 zoom: left_dock_zoom,
2180 },
2181 right: DockData {
2182 visible: right_visible,
2183 active_panel: right_active_panel,
2184 zoom: right_dock_zoom,
2185 },
2186 bottom: DockData {
2187 visible: bottom_visible,
2188 active_panel: bottom_active_panel,
2189 zoom: bottom_dock_zoom,
2190 },
2191 }
2192 }
2193
2194 pub fn set_dock_structure(
2195 &self,
2196 docks: DockStructure,
2197 window: &mut Window,
2198 cx: &mut Context<Self>,
2199 ) {
2200 for (dock, data) in [
2201 (&self.left_dock, docks.left),
2202 (&self.bottom_dock, docks.bottom),
2203 (&self.right_dock, docks.right),
2204 ] {
2205 dock.update(cx, |dock, cx| {
2206 dock.serialized_dock = Some(data);
2207 dock.restore_state(window, cx);
2208 });
2209 }
2210 }
2211
2212 /// Returns which dock currently has focus, or `None` if focus is in the
2213 /// center pane or elsewhere. Does NOT fall back to any global state.
2214 pub fn focused_dock_position(&self, window: &Window, cx: &App) -> Option<DockPosition> {
2215 [
2216 (DockPosition::Left, &self.left_dock),
2217 (DockPosition::Right, &self.right_dock),
2218 (DockPosition::Bottom, &self.bottom_dock),
2219 ]
2220 .into_iter()
2221 .find(|(_, dock)| {
2222 dock.read(cx).is_open() && dock.focus_handle(cx).contains_focused(window, cx)
2223 })
2224 .map(|(position, _)| position)
2225 }
2226
2227 pub fn active_worktree_creation(&self) -> &ActiveWorktreeCreation {
2228 &self.active_worktree_creation
2229 }
2230
2231 pub fn set_active_worktree_creation(
2232 &mut self,
2233 label: Option<SharedString>,
2234 is_switch: bool,
2235 cx: &mut Context<Self>,
2236 ) {
2237 self.active_worktree_creation.label = label;
2238 self.active_worktree_creation.is_switch = is_switch;
2239 cx.emit(Event::WorktreeCreationChanged);
2240 cx.notify();
2241 }
2242
2243 /// Captures the current workspace state for restoring after a worktree switch.
2244 /// This includes dock layout, open file paths, and the active file path.
2245 pub fn capture_state_for_worktree_switch(
2246 &self,
2247 window: &Window,
2248 fallback_focused_dock: Option<DockPosition>,
2249 cx: &App,
2250 ) -> PreviousWorkspaceState {
2251 let dock_structure = self.capture_dock_state(window, cx);
2252 let open_file_paths = self.open_item_abs_paths(cx);
2253 let active_file_path = self
2254 .active_item(cx)
2255 .and_then(|item| item.project_path(cx))
2256 .and_then(|pp| self.project().read(cx).absolute_path(&pp, cx));
2257
2258 let focused_dock = self
2259 .focused_dock_position(window, cx)
2260 .or(fallback_focused_dock);
2261
2262 PreviousWorkspaceState {
2263 dock_structure,
2264 open_file_paths,
2265 active_file_path,
2266 focused_dock,
2267 }
2268 }
2269
2270 pub fn open_item_abs_paths(&self, cx: &App) -> Vec<PathBuf> {
2271 self.items(cx)
2272 .filter_map(|item| {
2273 let project_path = item.project_path(cx)?;
2274 self.project.read(cx).absolute_path(&project_path, cx)
2275 })
2276 .collect()
2277 }
2278
2279 pub fn dock_at_position(&self, position: DockPosition) -> &Entity<Dock> {
2280 match position {
2281 DockPosition::Left => &self.left_dock,
2282 DockPosition::Bottom => &self.bottom_dock,
2283 DockPosition::Right => &self.right_dock,
2284 }
2285 }
2286
2287 pub fn agent_panel_position(&self, cx: &App) -> Option<DockPosition> {
2288 self.all_docks().into_iter().find_map(|dock| {
2289 let dock = dock.read(cx);
2290 dock.has_agent_panel(cx).then_some(dock.position())
2291 })
2292 }
2293
2294 pub fn panel_size_state<T: Panel>(&self, cx: &App) -> Option<dock::PanelSizeState> {
2295 self.all_docks().into_iter().find_map(|dock| {
2296 let dock = dock.read(cx);
2297 let panel = dock.panel::<T>()?;
2298 dock.stored_panel_size_state(&panel)
2299 })
2300 }
2301
2302 pub fn persisted_panel_size_state(
2303 &self,
2304 panel_key: &'static str,
2305 cx: &App,
2306 ) -> Option<dock::PanelSizeState> {
2307 dock::Dock::load_persisted_size_state(self, panel_key, cx)
2308 }
2309
2310 pub fn persist_panel_size_state(
2311 &self,
2312 panel_key: &str,
2313 size_state: dock::PanelSizeState,
2314 cx: &mut App,
2315 ) {
2316 let Some(workspace_id) = self
2317 .database_id()
2318 .map(|id| i64::from(id).to_string())
2319 .or(self.session_id())
2320 else {
2321 return;
2322 };
2323
2324 let kvp = db::kvp::KeyValueStore::global(cx);
2325 let panel_key = panel_key.to_string();
2326 cx.background_spawn(async move {
2327 let scope = kvp.scoped(dock::PANEL_SIZE_STATE_KEY);
2328 scope
2329 .write(
2330 format!("{workspace_id}:{panel_key}"),
2331 serde_json::to_string(&size_state)?,
2332 )
2333 .await
2334 })
2335 .detach_and_log_err(cx);
2336 }
2337
2338 pub fn set_panel_size_state<T: Panel>(
2339 &mut self,
2340 size_state: dock::PanelSizeState,
2341 window: &mut Window,
2342 cx: &mut Context<Self>,
2343 ) -> bool {
2344 let Some(panel) = self.panel::<T>(cx) else {
2345 return false;
2346 };
2347
2348 let dock = self.dock_at_position(panel.position(window, cx));
2349 let did_set = dock.update(cx, |dock, cx| {
2350 dock.set_panel_size_state(&panel, size_state, cx)
2351 });
2352
2353 if did_set {
2354 self.persist_panel_size_state(T::panel_key(), size_state, cx);
2355 }
2356
2357 did_set
2358 }
2359
2360 pub fn toggle_dock_panel_flexible_size(
2361 &self,
2362 dock: &Entity<Dock>,
2363 panel: &dyn PanelHandle,
2364 window: &mut Window,
2365 cx: &mut App,
2366 ) {
2367 let position = dock.read(cx).position();
2368 let current_size = self.dock_size(&dock.read(cx), window, cx);
2369 let current_flex =
2370 current_size.and_then(|size| self.dock_flex_for_size(position, size, window, cx));
2371 dock.update(cx, |dock, cx| {
2372 dock.toggle_panel_flexible_size(panel, current_size, current_flex, window, cx);
2373 });
2374 }
2375
2376 fn dock_size(&self, dock: &Dock, window: &Window, cx: &App) -> Option<Pixels> {
2377 let panel = dock.active_panel()?;
2378 let size_state = dock
2379 .stored_panel_size_state(panel.as_ref())
2380 .unwrap_or_default();
2381 let position = dock.position();
2382
2383 let use_flex = panel.has_flexible_size(window, cx);
2384
2385 if position.axis() == Axis::Horizontal
2386 && use_flex
2387 && let Some(flex) = size_state.flex.or_else(|| self.default_dock_flex(position))
2388 {
2389 let workspace_width = self.bounds.size.width;
2390 if workspace_width <= Pixels::ZERO {
2391 return None;
2392 }
2393 let flex = flex.max(0.001);
2394 let center_column_count = self.center_full_height_column_count();
2395 let opposite = self.opposite_dock_panel_and_size_state(position, window, cx);
2396 if let Some(opposite_flex) = opposite.as_ref().and_then(|(_, s)| s.flex) {
2397 let total_flex = flex + center_column_count + opposite_flex;
2398 return Some((flex / total_flex * workspace_width).max(RESIZE_HANDLE_SIZE));
2399 } else {
2400 let opposite_fixed = opposite
2401 .map(|(panel, s)| s.size.unwrap_or_else(|| panel.default_size(window, cx)))
2402 .unwrap_or_default();
2403 let available = (workspace_width - opposite_fixed).max(RESIZE_HANDLE_SIZE);
2404 return Some(
2405 (flex / (flex + center_column_count) * available).max(RESIZE_HANDLE_SIZE),
2406 );
2407 }
2408 }
2409
2410 Some(
2411 size_state
2412 .size
2413 .unwrap_or_else(|| panel.default_size(window, cx)),
2414 )
2415 }
2416
2417 pub fn dock_flex_for_size(
2418 &self,
2419 position: DockPosition,
2420 size: Pixels,
2421 window: &Window,
2422 cx: &App,
2423 ) -> Option<f32> {
2424 if position.axis() != Axis::Horizontal {
2425 return None;
2426 }
2427
2428 let workspace_width = self.bounds.size.width;
2429 if workspace_width <= Pixels::ZERO {
2430 return None;
2431 }
2432
2433 let center_column_count = self.center_full_height_column_count();
2434 let opposite = self.opposite_dock_panel_and_size_state(position, window, cx);
2435 if let Some(opposite_flex) = opposite.as_ref().and_then(|(_, s)| s.flex) {
2436 let size = size.clamp(px(0.), workspace_width - px(1.));
2437 Some((size * (center_column_count + opposite_flex) / (workspace_width - size)).max(0.0))
2438 } else {
2439 let opposite_width = opposite
2440 .map(|(panel, s)| s.size.unwrap_or_else(|| panel.default_size(window, cx)))
2441 .unwrap_or_default();
2442 let available = (workspace_width - opposite_width).max(RESIZE_HANDLE_SIZE);
2443 let remaining = (available - size).max(px(1.));
2444 Some((size * center_column_count / remaining).max(0.0))
2445 }
2446 }
2447
2448 fn opposite_dock_panel_and_size_state(
2449 &self,
2450 position: DockPosition,
2451 window: &Window,
2452 cx: &App,
2453 ) -> Option<(Arc<dyn PanelHandle>, PanelSizeState)> {
2454 let opposite_position = match position {
2455 DockPosition::Left => DockPosition::Right,
2456 DockPosition::Right => DockPosition::Left,
2457 DockPosition::Bottom => return None,
2458 };
2459
2460 let opposite_dock = self.dock_at_position(opposite_position).read(cx);
2461 let panel = opposite_dock.visible_panel()?;
2462 let mut size_state = opposite_dock
2463 .stored_panel_size_state(panel.as_ref())
2464 .unwrap_or_default();
2465 if size_state.flex.is_none() && panel.has_flexible_size(window, cx) {
2466 size_state.flex = self.default_dock_flex(opposite_position);
2467 }
2468 Some((panel.clone(), size_state))
2469 }
2470
2471 fn center_full_height_column_count(&self) -> f32 {
2472 self.center.full_height_column_count().max(1) as f32
2473 }
2474
2475 pub fn default_dock_flex(&self, position: DockPosition) -> Option<f32> {
2476 if position.axis() != Axis::Horizontal {
2477 return None;
2478 }
2479
2480 Some(1.0)
2481 }
2482
2483 pub fn is_edited(&self) -> bool {
2484 self.window_edited
2485 }
2486
2487 pub fn add_panel<T: Panel>(
2488 &mut self,
2489 panel: Entity<T>,
2490 window: &mut Window,
2491 cx: &mut Context<Self>,
2492 ) {
2493 let focus_handle = panel.panel_focus_handle(cx);
2494 cx.on_focus_in(&focus_handle, window, Self::handle_panel_focused)
2495 .detach();
2496
2497 let dock_position = panel.position(window, cx);
2498 let dock = self.dock_at_position(dock_position);
2499 let any_panel = panel.to_any();
2500 let persisted_size_state =
2501 self.persisted_panel_size_state(T::panel_key(), cx)
2502 .or_else(|| {
2503 load_legacy_panel_size(T::panel_key(), dock_position, self, cx).map(|size| {
2504 let state = dock::PanelSizeState {
2505 size: Some(size),
2506 flex: None,
2507 };
2508 self.persist_panel_size_state(T::panel_key(), state, cx);
2509 state
2510 })
2511 });
2512
2513 dock.update(cx, |dock, cx| {
2514 let index = dock.add_panel(panel.clone(), self.weak_self.clone(), window, cx);
2515 if let Some(size_state) = persisted_size_state {
2516 dock.set_panel_size_state(&panel, size_state, cx);
2517 }
2518 index
2519 });
2520
2521 cx.emit(Event::PanelAdded(any_panel));
2522 }
2523
2524 pub fn remove_panel<T: Panel>(
2525 &mut self,
2526 panel: &Entity<T>,
2527 window: &mut Window,
2528 cx: &mut Context<Self>,
2529 ) {
2530 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2531 dock.update(cx, |dock, cx| dock.remove_panel(panel, window, cx));
2532 }
2533 }
2534
2535 pub fn status_bar(&self) -> &Entity<StatusBar> {
2536 &self.status_bar
2537 }
2538
2539 pub fn set_sidebar_focus_handle(&mut self, handle: Option<FocusHandle>) {
2540 self.sidebar_focus_handle = handle;
2541 }
2542
2543 pub fn status_bar_visible(&self, cx: &App) -> bool {
2544 StatusBarSettings::get_global(cx).show
2545 }
2546
2547 pub fn multi_workspace(&self) -> Option<&WeakEntity<MultiWorkspace>> {
2548 self.multi_workspace.as_ref()
2549 }
2550
2551 pub fn set_multi_workspace(
2552 &mut self,
2553 multi_workspace: WeakEntity<MultiWorkspace>,
2554 cx: &mut App,
2555 ) {
2556 self.status_bar.update(cx, |status_bar, cx| {
2557 status_bar.set_multi_workspace(multi_workspace.clone(), cx);
2558 });
2559 self.multi_workspace = Some(multi_workspace);
2560 }
2561
2562 pub fn app_state(&self) -> &Arc<AppState> {
2563 &self.app_state
2564 }
2565
2566 pub fn set_panels_task(&mut self, task: Task<Result<()>>) {
2567 self._panels_task = Some(task);
2568 }
2569
2570 pub fn take_panels_task(&mut self) -> Option<Task<Result<()>>> {
2571 self._panels_task.take()
2572 }
2573
2574 pub fn user_store(&self) -> &Entity<UserStore> {
2575 &self.app_state.user_store
2576 }
2577
2578 pub fn project(&self) -> &Entity<Project> {
2579 &self.project
2580 }
2581
2582 pub fn path_style(&self, cx: &App) -> PathStyle {
2583 self.project.read(cx).path_style(cx)
2584 }
2585
2586 pub fn recently_activated_items(&self, cx: &App) -> HashMap<EntityId, usize> {
2587 let mut history: HashMap<EntityId, usize> = HashMap::default();
2588
2589 for pane_handle in &self.panes {
2590 let pane = pane_handle.read(cx);
2591
2592 for entry in pane.activation_history() {
2593 history.insert(
2594 entry.entity_id,
2595 history
2596 .get(&entry.entity_id)
2597 .cloned()
2598 .unwrap_or(0)
2599 .max(entry.timestamp),
2600 );
2601 }
2602 }
2603
2604 history
2605 }
2606
2607 pub fn recent_active_item_by_type<T: 'static>(&self, cx: &App) -> Option<Entity<T>> {
2608 let mut recent_item: Option<Entity<T>> = None;
2609 let mut recent_timestamp = 0;
2610 for pane_handle in &self.panes {
2611 let pane = pane_handle.read(cx);
2612 let item_map: HashMap<EntityId, &Box<dyn ItemHandle>> =
2613 pane.items().map(|item| (item.item_id(), item)).collect();
2614 for entry in pane.activation_history() {
2615 if entry.timestamp > recent_timestamp
2616 && let Some(&item) = item_map.get(&entry.entity_id)
2617 && let Some(typed_item) = item.act_as::<T>(cx)
2618 {
2619 recent_timestamp = entry.timestamp;
2620 recent_item = Some(typed_item);
2621 }
2622 }
2623 }
2624 recent_item
2625 }
2626
2627 pub fn recent_navigation_history_iter(
2628 &self,
2629 cx: &App,
2630 ) -> impl Iterator<Item = (ProjectPath, Option<PathBuf>)> + use<> {
2631 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
2632 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
2633
2634 for pane in &self.panes {
2635 let pane = pane.read(cx);
2636
2637 pane.nav_history()
2638 .for_each_entry(cx, &mut |entry, (project_path, fs_path)| {
2639 if let Some(fs_path) = &fs_path {
2640 abs_paths_opened
2641 .entry(fs_path.clone())
2642 .or_default()
2643 .insert(project_path.clone());
2644 }
2645 let timestamp = entry.timestamp;
2646 match history.entry(project_path) {
2647 hash_map::Entry::Occupied(mut entry) => {
2648 let (_, old_timestamp) = entry.get();
2649 if ×tamp > old_timestamp {
2650 entry.insert((fs_path, timestamp));
2651 }
2652 }
2653 hash_map::Entry::Vacant(entry) => {
2654 entry.insert((fs_path, timestamp));
2655 }
2656 }
2657 });
2658
2659 if let Some(item) = pane.active_item()
2660 && let Some(project_path) = item.project_path(cx)
2661 {
2662 let fs_path = self.project.read(cx).absolute_path(&project_path, cx);
2663
2664 if let Some(fs_path) = &fs_path {
2665 abs_paths_opened
2666 .entry(fs_path.clone())
2667 .or_default()
2668 .insert(project_path.clone());
2669 }
2670
2671 history.insert(project_path, (fs_path, std::usize::MAX));
2672 }
2673 }
2674
2675 history
2676 .into_iter()
2677 .sorted_by_key(|(_, (_, order))| *order)
2678 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
2679 .rev()
2680 .filter(move |(history_path, abs_path)| {
2681 let latest_project_path_opened = abs_path
2682 .as_ref()
2683 .and_then(|abs_path| abs_paths_opened.get(abs_path))
2684 .and_then(|project_paths| {
2685 project_paths
2686 .iter()
2687 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
2688 });
2689
2690 latest_project_path_opened.is_none_or(|path| path == history_path)
2691 })
2692 }
2693
2694 pub fn recent_navigation_history(
2695 &self,
2696 limit: Option<usize>,
2697 cx: &App,
2698 ) -> Vec<(ProjectPath, Option<PathBuf>)> {
2699 self.recent_navigation_history_iter(cx)
2700 .take(limit.unwrap_or(usize::MAX))
2701 .collect()
2702 }
2703
2704 pub fn clear_navigation_history(&mut self, _window: &mut Window, cx: &mut Context<Workspace>) {
2705 for pane in &self.panes {
2706 pane.update(cx, |pane, cx| pane.nav_history_mut().clear(cx));
2707 }
2708 }
2709
2710 fn navigate_history(
2711 &mut self,
2712 pane: WeakEntity<Pane>,
2713 mode: NavigationMode,
2714 window: &mut Window,
2715 cx: &mut Context<Workspace>,
2716 ) -> Task<Result<()>> {
2717 self.navigate_history_impl(
2718 pane,
2719 mode,
2720 window,
2721 &mut |history, cx| history.pop(mode, cx),
2722 cx,
2723 )
2724 }
2725
2726 fn navigate_tag_history(
2727 &mut self,
2728 pane: WeakEntity<Pane>,
2729 mode: TagNavigationMode,
2730 window: &mut Window,
2731 cx: &mut Context<Workspace>,
2732 ) -> Task<Result<()>> {
2733 self.navigate_history_impl(
2734 pane,
2735 NavigationMode::Normal,
2736 window,
2737 &mut |history, _cx| history.pop_tag(mode),
2738 cx,
2739 )
2740 }
2741
2742 fn navigate_history_impl(
2743 &mut self,
2744 pane: WeakEntity<Pane>,
2745 mode: NavigationMode,
2746 window: &mut Window,
2747 cb: &mut dyn FnMut(&mut NavHistory, &mut App) -> Option<NavigationEntry>,
2748 cx: &mut Context<Workspace>,
2749 ) -> Task<Result<()>> {
2750 let to_load = if let Some(pane) = pane.upgrade() {
2751 pane.update(cx, |pane, cx| {
2752 window.focus(&pane.focus_handle(cx), cx);
2753 loop {
2754 // Retrieve the weak item handle from the history.
2755 let entry = cb(pane.nav_history_mut(), cx)?;
2756
2757 // If the item is still present in this pane, then activate it.
2758 if let Some(index) = entry
2759 .item
2760 .upgrade()
2761 .and_then(|v| pane.index_for_item(v.as_ref()))
2762 {
2763 let prev_active_item_index = pane.active_item_index();
2764 pane.nav_history_mut().set_mode(mode);
2765 pane.activate_item(index, true, true, window, cx);
2766 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2767
2768 let mut navigated = prev_active_item_index != pane.active_item_index();
2769 if let Some(data) = entry.data {
2770 navigated |= pane.active_item()?.navigate(data, window, cx);
2771 }
2772
2773 if navigated {
2774 break None;
2775 }
2776 } else {
2777 // If the item is no longer present in this pane, then retrieve its
2778 // path info in order to reopen it.
2779 break pane
2780 .nav_history()
2781 .path_for_item(entry.item.id())
2782 .map(|(project_path, abs_path)| (project_path, abs_path, entry));
2783 }
2784 }
2785 })
2786 } else {
2787 None
2788 };
2789
2790 if let Some((project_path, abs_path, entry)) = to_load {
2791 // If the item was no longer present, then load it again from its previous path, first try the local path
2792 let open_by_project_path = self.load_path(project_path.clone(), window, cx);
2793
2794 cx.spawn_in(window, async move |workspace, cx| {
2795 let open_by_project_path = open_by_project_path.await;
2796 let mut navigated = false;
2797 match open_by_project_path
2798 .with_context(|| format!("Navigating to {project_path:?}"))
2799 {
2800 Ok((project_entry_id, build_item)) => {
2801 let prev_active_item_id = pane.update(cx, |pane, _| {
2802 pane.nav_history_mut().set_mode(mode);
2803 pane.active_item().map(|p| p.item_id())
2804 })?;
2805
2806 pane.update_in(cx, |pane, window, cx| {
2807 let item = pane.open_item(
2808 project_entry_id,
2809 project_path,
2810 true,
2811 entry.is_preview,
2812 true,
2813 None,
2814 window, cx,
2815 build_item,
2816 );
2817 navigated |= Some(item.item_id()) != prev_active_item_id;
2818 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2819 if let Some(data) = entry.data {
2820 navigated |= item.navigate(data, window, cx);
2821 }
2822 })?;
2823 }
2824 Err(open_by_project_path_e) => {
2825 // Fall back to opening by abs path, in case an external file was opened and closed,
2826 // and its worktree is now dropped
2827 if let Some(abs_path) = abs_path {
2828 let prev_active_item_id = pane.update(cx, |pane, _| {
2829 pane.nav_history_mut().set_mode(mode);
2830 pane.active_item().map(|p| p.item_id())
2831 })?;
2832 let open_by_abs_path = workspace.update_in(cx, |workspace, window, cx| {
2833 workspace.open_abs_path(abs_path.clone(), OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
2834 })?;
2835 match open_by_abs_path
2836 .await
2837 .with_context(|| format!("Navigating to {abs_path:?}"))
2838 {
2839 Ok(item) => {
2840 pane.update_in(cx, |pane, window, cx| {
2841 navigated |= Some(item.item_id()) != prev_active_item_id;
2842 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2843 if let Some(data) = entry.data {
2844 navigated |= item.navigate(data, window, cx);
2845 }
2846 })?;
2847 }
2848 Err(open_by_abs_path_e) => {
2849 log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
2850 }
2851 }
2852 }
2853 }
2854 }
2855
2856 if !navigated {
2857 workspace
2858 .update_in(cx, |workspace, window, cx| {
2859 Self::navigate_history(workspace, pane, mode, window, cx)
2860 })?
2861 .await?;
2862 }
2863
2864 Ok(())
2865 })
2866 } else {
2867 Task::ready(Ok(()))
2868 }
2869 }
2870
2871 pub fn go_back(
2872 &mut self,
2873 pane: WeakEntity<Pane>,
2874 window: &mut Window,
2875 cx: &mut Context<Workspace>,
2876 ) -> Task<Result<()>> {
2877 self.navigate_history(pane, NavigationMode::GoingBack, window, cx)
2878 }
2879
2880 pub fn go_forward(
2881 &mut self,
2882 pane: WeakEntity<Pane>,
2883 window: &mut Window,
2884 cx: &mut Context<Workspace>,
2885 ) -> Task<Result<()>> {
2886 self.navigate_history(pane, NavigationMode::GoingForward, window, cx)
2887 }
2888
2889 pub fn reopen_closed_item(
2890 &mut self,
2891 window: &mut Window,
2892 cx: &mut Context<Workspace>,
2893 ) -> Task<Result<()>> {
2894 self.navigate_history(
2895 self.active_pane().downgrade(),
2896 NavigationMode::ReopeningClosedItem,
2897 window,
2898 cx,
2899 )
2900 }
2901
2902 pub fn client(&self) -> &Arc<Client> {
2903 &self.app_state.client
2904 }
2905
2906 pub fn set_titlebar_item(&mut self, item: AnyView, _: &mut Window, cx: &mut Context<Self>) {
2907 self.titlebar_item = Some(item);
2908 cx.notify();
2909 }
2910
2911 pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
2912 self.on_prompt_for_new_path = Some(prompt)
2913 }
2914
2915 pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) {
2916 self.on_prompt_for_open_path = Some(prompt)
2917 }
2918
2919 pub fn set_terminal_provider(&mut self, provider: impl TerminalProvider + 'static) {
2920 self.terminal_provider = Some(Box::new(provider));
2921 }
2922
2923 pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) {
2924 self.debugger_provider = Some(Arc::new(provider));
2925 }
2926
2927 pub fn set_open_in_dev_container(&mut self, value: bool) {
2928 self.open_in_dev_container = value;
2929 }
2930
2931 pub fn open_in_dev_container(&self) -> bool {
2932 self.open_in_dev_container
2933 }
2934
2935 pub fn set_dev_container_task(&mut self, task: Task<Result<()>>) {
2936 self._dev_container_task = Some(task);
2937 }
2938
2939 pub fn debugger_provider(&self) -> Option<Arc<dyn DebuggerProvider>> {
2940 self.debugger_provider.clone()
2941 }
2942
2943 pub fn prompt_for_open_path(
2944 &mut self,
2945 path_prompt_options: PathPromptOptions,
2946 lister: DirectoryLister,
2947 window: &mut Window,
2948 cx: &mut Context<Self>,
2949 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
2950 // TODO: If `on_prompt_for_open_path` is set, we should always use it
2951 // rather than gating on `use_system_path_prompts`. This would let tests
2952 // inject a mock without also having to disable the setting.
2953 if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
2954 let prompt = self.on_prompt_for_open_path.take().unwrap();
2955 let rx = prompt(self, lister, window, cx);
2956 self.on_prompt_for_open_path = Some(prompt);
2957 rx
2958 } else {
2959 let (tx, rx) = oneshot::channel();
2960 let abs_path = cx.prompt_for_paths(path_prompt_options);
2961
2962 cx.spawn_in(window, async move |workspace, cx| {
2963 let Ok(result) = abs_path.await else {
2964 return Ok(());
2965 };
2966
2967 match result {
2968 Ok(result) => {
2969 tx.send(result).ok();
2970 }
2971 Err(err) => {
2972 let rx = workspace.update_in(cx, |workspace, window, cx| {
2973 workspace.show_portal_error(err.to_string(), cx);
2974 let prompt = workspace.on_prompt_for_open_path.take().unwrap();
2975 let rx = prompt(workspace, lister, window, cx);
2976 workspace.on_prompt_for_open_path = Some(prompt);
2977 rx
2978 })?;
2979 if let Ok(path) = rx.await {
2980 tx.send(path).ok();
2981 }
2982 }
2983 };
2984 anyhow::Ok(())
2985 })
2986 .detach();
2987
2988 rx
2989 }
2990 }
2991
2992 pub fn prompt_for_new_path(
2993 &mut self,
2994 lister: DirectoryLister,
2995 suggested_name: Option<String>,
2996 window: &mut Window,
2997 cx: &mut Context<Self>,
2998 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
2999 if self.project.read(cx).is_via_collab()
3000 || self.project.read(cx).is_via_remote_server()
3001 || !WorkspaceSettings::get_global(cx).use_system_path_prompts
3002 {
3003 let prompt = self.on_prompt_for_new_path.take().unwrap();
3004 let rx = prompt(self, lister, suggested_name, window, cx);
3005 self.on_prompt_for_new_path = Some(prompt);
3006 return rx;
3007 }
3008
3009 let (tx, rx) = oneshot::channel();
3010 cx.spawn_in(window, async move |workspace, cx| {
3011 let abs_path = workspace.update(cx, |workspace, cx| {
3012 let relative_to = workspace
3013 .most_recent_active_path(cx)
3014 .and_then(|p| p.parent().map(|p| p.to_path_buf()))
3015 .or_else(|| {
3016 let project = workspace.project.read(cx);
3017 project.visible_worktrees(cx).find_map(|worktree| {
3018 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
3019 })
3020 })
3021 .or_else(std::env::home_dir)
3022 .unwrap_or_else(|| PathBuf::from(""));
3023 cx.prompt_for_new_path(&relative_to, suggested_name.as_deref())
3024 })?;
3025 let abs_path = match abs_path.await? {
3026 Ok(path) => path,
3027 Err(err) => {
3028 let rx = workspace.update_in(cx, |workspace, window, cx| {
3029 workspace.show_portal_error(err.to_string(), cx);
3030
3031 let prompt = workspace.on_prompt_for_new_path.take().unwrap();
3032 let rx = prompt(workspace, lister, suggested_name, window, cx);
3033 workspace.on_prompt_for_new_path = Some(prompt);
3034 rx
3035 })?;
3036 if let Ok(path) = rx.await {
3037 tx.send(path).ok();
3038 }
3039 return anyhow::Ok(());
3040 }
3041 };
3042
3043 tx.send(abs_path.map(|path| vec![path])).ok();
3044 anyhow::Ok(())
3045 })
3046 .detach();
3047
3048 rx
3049 }
3050
3051 pub fn titlebar_item(&self) -> Option<AnyView> {
3052 self.titlebar_item.clone()
3053 }
3054
3055 /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
3056 ///
3057 /// If the given workspace has a local project, then it will be passed
3058 /// to the callback. Otherwise, a new empty window will be created.
3059 pub fn with_local_workspace<T, F>(
3060 &mut self,
3061 window: &mut Window,
3062 cx: &mut Context<Self>,
3063 callback: F,
3064 ) -> Task<Result<T>>
3065 where
3066 T: 'static,
3067 F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
3068 {
3069 if self.project.read(cx).is_local() {
3070 Task::ready(Ok(callback(self, window, cx)))
3071 } else {
3072 let env = self.project.read(cx).cli_environment(cx);
3073 let task = Self::new_local(
3074 Vec::new(),
3075 self.app_state.clone(),
3076 None,
3077 env,
3078 None,
3079 OpenMode::Activate,
3080 cx,
3081 );
3082 cx.spawn_in(window, async move |_vh, cx| {
3083 let OpenResult {
3084 window: multi_workspace_window,
3085 ..
3086 } = task.await?;
3087 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
3088 let workspace = multi_workspace.workspace().clone();
3089 workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
3090 })
3091 })
3092 }
3093 }
3094
3095 /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
3096 ///
3097 /// If the given workspace has a local project, then it will be passed
3098 /// to the callback. Otherwise, a new empty window will be created.
3099 pub fn with_local_or_wsl_workspace<T, F>(
3100 &mut self,
3101 window: &mut Window,
3102 cx: &mut Context<Self>,
3103 callback: F,
3104 ) -> Task<Result<T>>
3105 where
3106 T: 'static,
3107 F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
3108 {
3109 let project = self.project.read(cx);
3110 if project.is_local() || project.is_via_wsl_with_host_interop(cx) {
3111 Task::ready(Ok(callback(self, window, cx)))
3112 } else {
3113 let env = self.project.read(cx).cli_environment(cx);
3114 let task = Self::new_local(
3115 Vec::new(),
3116 self.app_state.clone(),
3117 None,
3118 env,
3119 None,
3120 OpenMode::Activate,
3121 cx,
3122 );
3123 cx.spawn_in(window, async move |_vh, cx| {
3124 let OpenResult {
3125 window: multi_workspace_window,
3126 ..
3127 } = task.await?;
3128 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
3129 let workspace = multi_workspace.workspace().clone();
3130 workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
3131 })
3132 })
3133 }
3134 }
3135
3136 pub fn worktrees<'a>(&self, cx: &'a App) -> impl 'a + Iterator<Item = Entity<Worktree>> {
3137 self.project.read(cx).worktrees(cx)
3138 }
3139
3140 pub fn visible_worktrees<'a>(
3141 &self,
3142 cx: &'a App,
3143 ) -> impl 'a + Iterator<Item = Entity<Worktree>> {
3144 self.project.read(cx).visible_worktrees(cx)
3145 }
3146
3147 pub fn worktree_scans_complete(&self, cx: &App) -> impl Future<Output = ()> + 'static + use<> {
3148 let futures = self
3149 .worktrees(cx)
3150 .filter_map(|worktree| worktree.read(cx).as_local())
3151 .map(|worktree| worktree.scan_complete())
3152 .collect::<Vec<_>>();
3153 async move {
3154 for future in futures {
3155 future.await;
3156 }
3157 }
3158 }
3159
3160 pub fn close_global(cx: &mut App) {
3161 cx.defer(|cx| {
3162 cx.windows().iter().find(|window| {
3163 window
3164 .update(cx, |_, window, _| {
3165 if window.is_window_active() {
3166 //This can only get called when the window's project connection has been lost
3167 //so we don't need to prompt the user for anything and instead just close the window
3168 window.remove_window();
3169 true
3170 } else {
3171 false
3172 }
3173 })
3174 .unwrap_or(false)
3175 });
3176 });
3177 }
3178
3179 pub fn move_focused_panel_to_next_position(
3180 &mut self,
3181 _: &MoveFocusedPanelToNextPosition,
3182 window: &mut Window,
3183 cx: &mut Context<Self>,
3184 ) {
3185 let docks = self.all_docks();
3186 let active_dock = docks
3187 .into_iter()
3188 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
3189
3190 if let Some(dock) = active_dock {
3191 dock.update(cx, |dock, cx| {
3192 let active_panel = dock
3193 .active_panel()
3194 .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx));
3195
3196 if let Some(panel) = active_panel {
3197 panel.move_to_next_position(window, cx);
3198 }
3199 })
3200 }
3201 }
3202
3203 pub fn prepare_to_close(
3204 &mut self,
3205 close_intent: CloseIntent,
3206 window: &mut Window,
3207 cx: &mut Context<Self>,
3208 ) -> Task<Result<bool>> {
3209 let active_call = self.active_global_call();
3210
3211 cx.spawn_in(window, async move |this, cx| {
3212 this.update(cx, |this, _| {
3213 if close_intent == CloseIntent::CloseWindow {
3214 this.removing = true;
3215 }
3216 })?;
3217
3218 let workspace_count = cx.update(|_window, cx| {
3219 cx.windows()
3220 .iter()
3221 .filter(|window| window.downcast::<MultiWorkspace>().is_some())
3222 .count()
3223 })?;
3224
3225 #[cfg(target_os = "macos")]
3226 let save_last_workspace = false;
3227
3228 // On Linux and Windows, closing the last window should restore the last workspace.
3229 #[cfg(not(target_os = "macos"))]
3230 let save_last_workspace = {
3231 let remaining_workspaces = cx.update(|_window, cx| {
3232 cx.windows()
3233 .iter()
3234 .filter_map(|window| window.downcast::<MultiWorkspace>())
3235 .filter_map(|multi_workspace| {
3236 multi_workspace
3237 .update(cx, |multi_workspace, _, cx| {
3238 multi_workspace.workspace().read(cx).removing
3239 })
3240 .ok()
3241 })
3242 .filter(|removing| !removing)
3243 .count()
3244 })?;
3245
3246 close_intent != CloseIntent::ReplaceWindow && remaining_workspaces == 0
3247 };
3248
3249 if let Some(active_call) = active_call
3250 && workspace_count == 1
3251 && cx
3252 .update(|_window, cx| active_call.0.is_in_room(cx))
3253 .unwrap_or(false)
3254 {
3255 if close_intent == CloseIntent::CloseWindow {
3256 this.update(cx, |_, cx| cx.emit(Event::Activate))?;
3257 let answer = cx.update(|window, cx| {
3258 window.prompt(
3259 PromptLevel::Warning,
3260 "Do you want to leave the current call?",
3261 None,
3262 &["Close window and hang up", "Cancel"],
3263 cx,
3264 )
3265 })?;
3266
3267 if answer.await.log_err() == Some(1) {
3268 return anyhow::Ok(false);
3269 } else {
3270 if let Ok(task) = cx.update(|_window, cx| active_call.0.hang_up(cx)) {
3271 task.await.log_err();
3272 }
3273 }
3274 }
3275 if close_intent == CloseIntent::ReplaceWindow {
3276 _ = cx.update(|_window, cx| {
3277 let multi_workspace = cx
3278 .windows()
3279 .iter()
3280 .filter_map(|window| window.downcast::<MultiWorkspace>())
3281 .next()
3282 .unwrap();
3283 let project = multi_workspace
3284 .read(cx)?
3285 .workspace()
3286 .read(cx)
3287 .project
3288 .clone();
3289 if project.read(cx).is_shared() {
3290 active_call.0.unshare_project(project, cx)?;
3291 }
3292 Ok::<_, anyhow::Error>(())
3293 });
3294 }
3295 }
3296
3297 let save_result = this
3298 .update_in(cx, |this, window, cx| {
3299 this.save_all_internal(SaveIntent::Close, window, cx)
3300 })?
3301 .await;
3302
3303 // If we're not quitting, but closing, we remove the workspace from
3304 // the current session.
3305 if close_intent != CloseIntent::Quit
3306 && !save_last_workspace
3307 && save_result.as_ref().is_ok_and(|&res| res)
3308 {
3309 this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx))?
3310 .await;
3311 }
3312
3313 save_result
3314 })
3315 }
3316
3317 fn save_all(&mut self, action: &SaveAll, window: &mut Window, cx: &mut Context<Self>) {
3318 self.save_all_internal(
3319 action.save_intent.unwrap_or(SaveIntent::SaveAll),
3320 window,
3321 cx,
3322 )
3323 .detach_and_log_err(cx);
3324 }
3325
3326 fn send_keystrokes(
3327 &mut self,
3328 action: &SendKeystrokes,
3329 window: &mut Window,
3330 cx: &mut Context<Self>,
3331 ) {
3332 let keystrokes: Vec<Keystroke> = action
3333 .0
3334 .split(' ')
3335 .flat_map(|k| Keystroke::parse(k).log_err())
3336 .map(|k| {
3337 cx.keyboard_mapper()
3338 .map_key_equivalent(k, false)
3339 .inner()
3340 .clone()
3341 })
3342 .collect();
3343 let _ = self.send_keystrokes_impl(keystrokes, window, cx);
3344 }
3345
3346 pub fn send_keystrokes_impl(
3347 &mut self,
3348 keystrokes: Vec<Keystroke>,
3349 window: &mut Window,
3350 cx: &mut Context<Self>,
3351 ) -> Shared<Task<()>> {
3352 let mut state = self.dispatching_keystrokes.borrow_mut();
3353 if !state.dispatched.insert(keystrokes.clone()) {
3354 cx.propagate();
3355 return state.task.clone().unwrap();
3356 }
3357
3358 state.queue.extend(keystrokes);
3359
3360 let keystrokes = self.dispatching_keystrokes.clone();
3361 if state.task.is_none() {
3362 state.task = Some(
3363 window
3364 .spawn(cx, async move |cx| {
3365 // limit to 100 keystrokes to avoid infinite recursion.
3366 for _ in 0..100 {
3367 let keystroke = {
3368 let mut state = keystrokes.borrow_mut();
3369 let Some(keystroke) = state.queue.pop_front() else {
3370 state.dispatched.clear();
3371 state.task.take();
3372 return;
3373 };
3374 keystroke
3375 };
3376 let focus_changed = cx
3377 .update(|window, cx| {
3378 let focused = window.focused(cx);
3379 window.dispatch_keystroke(keystroke.clone(), cx);
3380 if window.focused(cx) != focused {
3381 // dispatch_keystroke may cause the focus to change.
3382 // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
3383 // And we need that to happen before the next keystroke to keep vim mode happy...
3384 // (Note that the tests always do this implicitly, so you must manually test with something like:
3385 // "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
3386 // )
3387 window.draw(cx).clear();
3388 return true;
3389 }
3390 false
3391 })
3392 .unwrap_or(false);
3393
3394 if focus_changed {
3395 yield_now().await;
3396 }
3397 }
3398
3399 *keystrokes.borrow_mut() = Default::default();
3400 log::error!("over 100 keystrokes passed to send_keystrokes");
3401 })
3402 .shared(),
3403 );
3404 }
3405 state.task.clone().unwrap()
3406 }
3407
3408 /// Prompts the user to save or discard each dirty item, returning
3409 /// `true` if they confirmed (saved/discarded everything) or `false`
3410 /// if they cancelled. Used before removing worktree roots during
3411 /// thread archival.
3412 pub fn prompt_to_save_or_discard_dirty_items(
3413 &mut self,
3414 window: &mut Window,
3415 cx: &mut Context<Self>,
3416 ) -> Task<Result<bool>> {
3417 self.save_all_internal(SaveIntent::Close, window, cx)
3418 }
3419
3420 fn save_all_internal(
3421 &mut self,
3422 mut save_intent: SaveIntent,
3423 window: &mut Window,
3424 cx: &mut Context<Self>,
3425 ) -> Task<Result<bool>> {
3426 if self.project.read(cx).is_disconnected(cx) {
3427 return Task::ready(Ok(true));
3428 }
3429 let dirty_items = self
3430 .panes
3431 .iter()
3432 .flat_map(|pane| {
3433 pane.read(cx).items().filter_map(|item| {
3434 if item.is_dirty(cx) {
3435 item.tab_content_text(0, cx);
3436 Some((pane.downgrade(), item.boxed_clone()))
3437 } else {
3438 None
3439 }
3440 })
3441 })
3442 .collect::<Vec<_>>();
3443
3444 let project = self.project.clone();
3445 cx.spawn_in(window, async move |workspace, cx| {
3446 let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() {
3447 let mut serialize_tasks = Vec::new();
3448 let mut remaining_dirty_items = Vec::new();
3449 workspace.update_in(cx, |workspace, window, cx| {
3450 for (pane, item) in dirty_items {
3451 if let Some(task) = item
3452 .to_serializable_item_handle(cx)
3453 .and_then(|handle| handle.serialize(workspace, true, window, cx))
3454 {
3455 serialize_tasks.push((pane, item, task));
3456 } else {
3457 remaining_dirty_items.push((pane, item));
3458 }
3459 }
3460 })?;
3461
3462 for (pane, item, task) in serialize_tasks {
3463 if task.await.log_err().is_none() {
3464 remaining_dirty_items.push((pane, item));
3465 }
3466 }
3467
3468 if !remaining_dirty_items.is_empty() {
3469 workspace.update(cx, |_, cx| cx.emit(Event::Activate))?;
3470 }
3471
3472 if remaining_dirty_items.len() > 1 {
3473 let answer = workspace.update_in(cx, |_, window, cx| {
3474 cx.emit(Event::Activate);
3475 let detail = Pane::file_names_for_prompt(
3476 &mut remaining_dirty_items.iter().map(|(_, handle)| handle),
3477 cx,
3478 );
3479 window.prompt(
3480 PromptLevel::Warning,
3481 "Do you want to save all changes in the following files?",
3482 Some(&detail),
3483 &["Save all", "Discard all", "Cancel"],
3484 cx,
3485 )
3486 })?;
3487 match answer.await.log_err() {
3488 Some(0) => save_intent = SaveIntent::SaveAll,
3489 Some(1) => save_intent = SaveIntent::Skip,
3490 Some(2) => return Ok(false),
3491 _ => {}
3492 }
3493 }
3494
3495 remaining_dirty_items
3496 } else {
3497 dirty_items
3498 };
3499
3500 for (pane, item) in dirty_items {
3501 let (singleton, project_entry_ids) = cx.update(|_, cx| {
3502 (
3503 item.buffer_kind(cx) == ItemBufferKind::Singleton,
3504 item.project_entry_ids(cx),
3505 )
3506 })?;
3507 if (singleton || !project_entry_ids.is_empty())
3508 && !Pane::save_item(project.clone(), &pane, &*item, save_intent, cx).await?
3509 {
3510 return Ok(false);
3511 }
3512 }
3513 Ok(true)
3514 })
3515 }
3516
3517 pub fn open_workspace_for_paths(
3518 &mut self,
3519 // replace_current_window: bool,
3520 mut open_mode: OpenMode,
3521 paths: Vec<PathBuf>,
3522 window: &mut Window,
3523 cx: &mut Context<Self>,
3524 ) -> Task<Result<Entity<Workspace>>> {
3525 let requesting_window = window.window_handle().downcast::<MultiWorkspace>();
3526 let is_remote = self.project.read(cx).is_via_collab();
3527 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
3528 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
3529
3530 let workspace_is_empty = !is_remote && !has_worktree && !has_dirty_items;
3531 if workspace_is_empty {
3532 open_mode = OpenMode::Activate;
3533 }
3534
3535 let app_state = self.app_state.clone();
3536
3537 cx.spawn(async move |_, cx| {
3538 let OpenResult { workspace, .. } = cx
3539 .update(|cx| {
3540 open_paths(
3541 &paths,
3542 app_state,
3543 OpenOptions {
3544 requesting_window,
3545 open_mode,
3546 workspace_matching: if open_mode == OpenMode::NewWindow {
3547 WorkspaceMatching::None
3548 } else {
3549 WorkspaceMatching::default()
3550 },
3551 ..Default::default()
3552 },
3553 cx,
3554 )
3555 })
3556 .await?;
3557 Ok(workspace)
3558 })
3559 }
3560
3561 #[allow(clippy::type_complexity)]
3562 pub fn open_paths(
3563 &mut self,
3564 mut abs_paths: Vec<PathBuf>,
3565 options: OpenOptions,
3566 pane: Option<WeakEntity<Pane>>,
3567 window: &mut Window,
3568 cx: &mut Context<Self>,
3569 ) -> Task<Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>> {
3570 let fs = self.app_state.fs.clone();
3571
3572 let caller_ordered_abs_paths = abs_paths.clone();
3573
3574 // Sort the paths to ensure we add worktrees for parents before their children.
3575 abs_paths.sort_unstable();
3576 cx.spawn_in(window, async move |this, cx| {
3577 let mut tasks = Vec::with_capacity(abs_paths.len());
3578
3579 for abs_path in &abs_paths {
3580 let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
3581 OpenVisible::All => Some(true),
3582 OpenVisible::None => Some(false),
3583 OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
3584 Some(Some(metadata)) => Some(!metadata.is_dir),
3585 Some(None) => Some(true),
3586 None => None,
3587 },
3588 OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
3589 Some(Some(metadata)) => Some(metadata.is_dir),
3590 Some(None) => Some(false),
3591 None => None,
3592 },
3593 };
3594 let project_path = match visible {
3595 Some(visible) => match this
3596 .update(cx, |this, cx| {
3597 Workspace::project_path_for_path(
3598 this.project.clone(),
3599 abs_path,
3600 visible,
3601 cx,
3602 )
3603 })
3604 .log_err()
3605 {
3606 Some(project_path) => project_path.await.log_err(),
3607 None => None,
3608 },
3609 None => None,
3610 };
3611
3612 let this = this.clone();
3613 let abs_path: Arc<Path> = SanitizedPath::new(&abs_path).as_path().into();
3614 let fs = fs.clone();
3615 let pane = pane.clone();
3616 let task = cx.spawn(async move |cx| {
3617 let (_worktree, project_path) = project_path?;
3618 if fs.is_dir(&abs_path).await {
3619 // Opening a directory should not race to update the active entry.
3620 // We'll select/reveal a deterministic final entry after all paths finish opening.
3621 None
3622 } else {
3623 Some(
3624 this.update_in(cx, |this, window, cx| {
3625 this.open_path(
3626 project_path,
3627 pane,
3628 options.focus.unwrap_or(true),
3629 window,
3630 cx,
3631 )
3632 })
3633 .ok()?
3634 .await,
3635 )
3636 }
3637 });
3638 tasks.push(task);
3639 }
3640
3641 let results = futures::future::join_all(tasks).await;
3642
3643 // Determine the winner using the fake/abstract FS metadata, not `Path::is_dir`.
3644 let mut winner: Option<(PathBuf, bool)> = None;
3645 for abs_path in caller_ordered_abs_paths.into_iter().rev() {
3646 if let Some(Some(metadata)) = fs.metadata(&abs_path).await.log_err() {
3647 if !metadata.is_dir {
3648 winner = Some((abs_path, false));
3649 break;
3650 }
3651 if winner.is_none() {
3652 winner = Some((abs_path, true));
3653 }
3654 } else if winner.is_none() {
3655 winner = Some((abs_path, false));
3656 }
3657 }
3658
3659 // Compute the winner entry id on the foreground thread and emit once, after all
3660 // paths finish opening. This avoids races between concurrently-opening paths
3661 // (directories in particular) and makes the resulting project panel selection
3662 // deterministic.
3663 if let Some((winner_abs_path, winner_is_dir)) = winner {
3664 'emit_winner: {
3665 let winner_abs_path: Arc<Path> =
3666 SanitizedPath::new(&winner_abs_path).as_path().into();
3667
3668 let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
3669 OpenVisible::All => true,
3670 OpenVisible::None => false,
3671 OpenVisible::OnlyFiles => !winner_is_dir,
3672 OpenVisible::OnlyDirectories => winner_is_dir,
3673 };
3674
3675 let Some(worktree_task) = this
3676 .update(cx, |workspace, cx| {
3677 workspace.project.update(cx, |project, cx| {
3678 project.find_or_create_worktree(
3679 winner_abs_path.as_ref(),
3680 visible,
3681 cx,
3682 )
3683 })
3684 })
3685 .ok()
3686 else {
3687 break 'emit_winner;
3688 };
3689
3690 let Ok((worktree, _)) = worktree_task.await else {
3691 break 'emit_winner;
3692 };
3693
3694 let Ok(Some(entry_id)) = this.update(cx, |_, cx| {
3695 let worktree = worktree.read(cx);
3696 let worktree_abs_path = worktree.abs_path();
3697 let entry = if winner_abs_path.as_ref() == worktree_abs_path.as_ref() {
3698 worktree.root_entry()
3699 } else {
3700 winner_abs_path
3701 .strip_prefix(worktree_abs_path.as_ref())
3702 .ok()
3703 .and_then(|relative_path| {
3704 let relative_path =
3705 RelPath::new(relative_path, PathStyle::local())
3706 .log_err()?;
3707 worktree.entry_for_path(&relative_path)
3708 })
3709 }?;
3710 Some(entry.id)
3711 }) else {
3712 break 'emit_winner;
3713 };
3714
3715 this.update(cx, |workspace, cx| {
3716 workspace.project.update(cx, |_, cx| {
3717 cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
3718 });
3719 })
3720 .ok();
3721 }
3722 }
3723
3724 results
3725 })
3726 }
3727
3728 pub fn open_resolved_path(
3729 &mut self,
3730 path: ResolvedPath,
3731 window: &mut Window,
3732 cx: &mut Context<Self>,
3733 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3734 match path {
3735 ResolvedPath::ProjectPath { project_path, .. } => {
3736 self.open_path(project_path, None, true, window, cx)
3737 }
3738 ResolvedPath::AbsPath { path, .. } => self.open_abs_path(
3739 PathBuf::from(path),
3740 OpenOptions {
3741 visible: Some(OpenVisible::None),
3742 ..Default::default()
3743 },
3744 window,
3745 cx,
3746 ),
3747 }
3748 }
3749
3750 pub fn absolute_path_of_worktree(
3751 &self,
3752 worktree_id: WorktreeId,
3753 cx: &mut Context<Self>,
3754 ) -> Option<PathBuf> {
3755 self.project
3756 .read(cx)
3757 .worktree_for_id(worktree_id, cx)
3758 // TODO: use `abs_path` or `root_dir`
3759 .map(|wt| wt.read(cx).abs_path().as_ref().to_path_buf())
3760 }
3761
3762 pub fn add_folder_to_project(
3763 &mut self,
3764 _: &AddFolderToProject,
3765 window: &mut Window,
3766 cx: &mut Context<Self>,
3767 ) {
3768 let project = self.project.read(cx);
3769 if project.is_via_collab() {
3770 self.show_error(
3771 &anyhow!("You cannot add folders to someone else's project"),
3772 cx,
3773 );
3774 return;
3775 }
3776 let paths = self.prompt_for_open_path(
3777 PathPromptOptions {
3778 files: false,
3779 directories: true,
3780 multiple: true,
3781 prompt: None,
3782 },
3783 DirectoryLister::Project(self.project.clone()),
3784 window,
3785 cx,
3786 );
3787 cx.spawn_in(window, async move |this, cx| {
3788 if let Some(paths) = paths.await.log_err().flatten() {
3789 let results = this
3790 .update_in(cx, |this, window, cx| {
3791 this.open_paths(
3792 paths,
3793 OpenOptions {
3794 visible: Some(OpenVisible::All),
3795 ..Default::default()
3796 },
3797 None,
3798 window,
3799 cx,
3800 )
3801 })?
3802 .await;
3803 for result in results.into_iter().flatten() {
3804 result.log_err();
3805 }
3806 }
3807 anyhow::Ok(())
3808 })
3809 .detach_and_log_err(cx);
3810 }
3811
3812 pub fn project_path_for_path(
3813 project: Entity<Project>,
3814 abs_path: &Path,
3815 visible: bool,
3816 cx: &mut App,
3817 ) -> Task<Result<(Entity<Worktree>, ProjectPath)>> {
3818 let entry = project.update(cx, |project, cx| {
3819 project.find_or_create_worktree(abs_path, visible, cx)
3820 });
3821 cx.spawn(async move |cx| {
3822 let (worktree, path) = entry.await?;
3823 let worktree_id = worktree.read_with(cx, |t, _| t.id());
3824 Ok((worktree, ProjectPath { worktree_id, path }))
3825 })
3826 }
3827
3828 pub fn items<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator<Item = &'a Box<dyn ItemHandle>> {
3829 self.panes.iter().flat_map(|pane| pane.read(cx).items())
3830 }
3831
3832 pub fn item_of_type<T: Item>(&self, cx: &App) -> Option<Entity<T>> {
3833 self.items_of_type(cx).max_by_key(|item| item.item_id())
3834 }
3835
3836 pub fn items_of_type<'a, T: Item>(
3837 &'a self,
3838 cx: &'a App,
3839 ) -> impl 'a + Iterator<Item = Entity<T>> {
3840 self.panes
3841 .iter()
3842 .flat_map(|pane| pane.read(cx).items_of_type())
3843 }
3844
3845 pub fn active_item(&self, cx: &App) -> Option<Box<dyn ItemHandle>> {
3846 self.active_pane().read(cx).active_item()
3847 }
3848
3849 pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
3850 let item = self.active_item(cx)?;
3851 item.to_any_view().downcast::<I>().ok()
3852 }
3853
3854 fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
3855 self.active_item(cx).and_then(|item| item.project_path(cx))
3856 }
3857
3858 pub fn most_recent_active_path(&self, cx: &App) -> Option<PathBuf> {
3859 self.recent_navigation_history_iter(cx)
3860 .filter_map(|(path, abs_path)| {
3861 let worktree = self
3862 .project
3863 .read(cx)
3864 .worktree_for_id(path.worktree_id, cx)?;
3865 if !worktree.read(cx).is_visible() {
3866 return None;
3867 }
3868 let settings_location = SettingsLocation {
3869 worktree_id: path.worktree_id,
3870 path: &path.path,
3871 };
3872 if WorktreeSettings::get(Some(settings_location), cx).is_path_read_only(&path.path)
3873 {
3874 return None;
3875 }
3876 abs_path
3877 })
3878 .next()
3879 }
3880
3881 pub fn save_active_item(
3882 &mut self,
3883 save_intent: SaveIntent,
3884 window: &mut Window,
3885 cx: &mut App,
3886 ) -> Task<Result<()>> {
3887 let project = self.project.clone();
3888 let pane = self.active_pane();
3889 let item = pane.read(cx).active_item();
3890 let pane = pane.downgrade();
3891
3892 window.spawn(cx, async move |cx| {
3893 if let Some(item) = item {
3894 Pane::save_item(project, &pane, item.as_ref(), save_intent, cx)
3895 .await
3896 .map(|_| ())
3897 } else {
3898 Ok(())
3899 }
3900 })
3901 }
3902
3903 pub fn close_inactive_items_and_panes(
3904 &mut self,
3905 action: &CloseInactiveTabsAndPanes,
3906 window: &mut Window,
3907 cx: &mut Context<Self>,
3908 ) {
3909 if let Some(task) = self.close_all_internal(
3910 true,
3911 action.save_intent.unwrap_or(SaveIntent::Close),
3912 window,
3913 cx,
3914 ) {
3915 task.detach_and_log_err(cx)
3916 }
3917 }
3918
3919 pub fn close_all_items_and_panes(
3920 &mut self,
3921 action: &CloseAllItemsAndPanes,
3922 window: &mut Window,
3923 cx: &mut Context<Self>,
3924 ) {
3925 if let Some(task) = self.close_all_internal(
3926 false,
3927 action.save_intent.unwrap_or(SaveIntent::Close),
3928 window,
3929 cx,
3930 ) {
3931 task.detach_and_log_err(cx)
3932 }
3933 }
3934
3935 /// Closes the active item across all panes.
3936 pub fn close_item_in_all_panes(
3937 &mut self,
3938 action: &CloseItemInAllPanes,
3939 window: &mut Window,
3940 cx: &mut Context<Self>,
3941 ) {
3942 let Some(active_item) = self.active_pane().read(cx).active_item() else {
3943 return;
3944 };
3945
3946 let save_intent = action.save_intent.unwrap_or(SaveIntent::Close);
3947 let close_pinned = action.close_pinned;
3948
3949 if let Some(project_path) = active_item.project_path(cx) {
3950 self.close_items_with_project_path(
3951 &project_path,
3952 save_intent,
3953 close_pinned,
3954 window,
3955 cx,
3956 );
3957 } else if close_pinned || !self.active_pane().read(cx).is_active_item_pinned() {
3958 let item_id = active_item.item_id();
3959 self.active_pane().update(cx, |pane, cx| {
3960 pane.close_item_by_id(item_id, save_intent, window, cx)
3961 .detach_and_log_err(cx);
3962 });
3963 }
3964 }
3965
3966 /// Closes all items with the given project path across all panes.
3967 pub fn close_items_with_project_path(
3968 &mut self,
3969 project_path: &ProjectPath,
3970 save_intent: SaveIntent,
3971 close_pinned: bool,
3972 window: &mut Window,
3973 cx: &mut Context<Self>,
3974 ) {
3975 let panes = self.panes().to_vec();
3976 for pane in panes {
3977 pane.update(cx, |pane, cx| {
3978 pane.close_items_for_project_path(
3979 project_path,
3980 save_intent,
3981 close_pinned,
3982 window,
3983 cx,
3984 )
3985 .detach_and_log_err(cx);
3986 });
3987 }
3988 }
3989
3990 fn close_all_internal(
3991 &mut self,
3992 retain_active_pane: bool,
3993 save_intent: SaveIntent,
3994 window: &mut Window,
3995 cx: &mut Context<Self>,
3996 ) -> Option<Task<Result<()>>> {
3997 let current_pane = self.active_pane();
3998
3999 let mut tasks = Vec::new();
4000
4001 if retain_active_pane {
4002 let current_pane_close = current_pane.update(cx, |pane, cx| {
4003 pane.close_other_items(
4004 &CloseOtherItems {
4005 save_intent: None,
4006 close_pinned: false,
4007 },
4008 None,
4009 window,
4010 cx,
4011 )
4012 });
4013
4014 tasks.push(current_pane_close);
4015 }
4016
4017 for pane in self.panes() {
4018 if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
4019 continue;
4020 }
4021
4022 let close_pane_items = pane.update(cx, |pane: &mut Pane, cx| {
4023 pane.close_all_items(
4024 &CloseAllItems {
4025 save_intent: Some(save_intent),
4026 close_pinned: false,
4027 },
4028 window,
4029 cx,
4030 )
4031 });
4032
4033 tasks.push(close_pane_items)
4034 }
4035
4036 if tasks.is_empty() {
4037 None
4038 } else {
4039 Some(cx.spawn_in(window, async move |_, _| {
4040 for task in tasks {
4041 task.await?
4042 }
4043 Ok(())
4044 }))
4045 }
4046 }
4047
4048 pub fn is_dock_at_position_open(&self, position: DockPosition, cx: &mut Context<Self>) -> bool {
4049 self.dock_at_position(position).read(cx).is_open()
4050 }
4051
4052 pub fn toggle_dock(
4053 &mut self,
4054 dock_side: DockPosition,
4055 window: &mut Window,
4056 cx: &mut Context<Self>,
4057 ) {
4058 let mut focus_center = false;
4059 let mut reveal_dock = false;
4060
4061 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
4062 let was_visible = self.is_dock_at_position_open(dock_side, cx) && !other_is_zoomed;
4063
4064 if let Some(panel) = self.dock_at_position(dock_side).read(cx).active_panel() {
4065 telemetry::event!(
4066 "Panel Button Clicked",
4067 name = panel.persistent_name(),
4068 toggle_state = !was_visible
4069 );
4070 }
4071 if was_visible {
4072 self.save_open_dock_positions(cx);
4073 }
4074
4075 let dock = self.dock_at_position(dock_side);
4076 dock.update(cx, |dock, cx| {
4077 dock.set_open(!was_visible, window, cx);
4078
4079 if dock.active_panel().is_none() {
4080 let Some(panel_ix) = dock
4081 .first_enabled_panel_idx(cx)
4082 .log_with_level(log::Level::Info)
4083 else {
4084 return;
4085 };
4086 dock.activate_panel(panel_ix, window, cx);
4087 }
4088
4089 if let Some(active_panel) = dock.active_panel() {
4090 if was_visible {
4091 if active_panel
4092 .panel_focus_handle(cx)
4093 .contains_focused(window, cx)
4094 {
4095 focus_center = true;
4096 }
4097 } else {
4098 let focus_handle = &active_panel.panel_focus_handle(cx);
4099 window.focus(focus_handle, cx);
4100 reveal_dock = true;
4101 }
4102 }
4103 });
4104
4105 if reveal_dock {
4106 self.dismiss_zoomed_items_to_reveal(Some(dock_side), window, cx);
4107 }
4108
4109 if focus_center {
4110 self.active_pane
4111 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
4112 }
4113
4114 cx.notify();
4115 self.serialize_workspace(window, cx);
4116 }
4117
4118 fn active_dock(&self, window: &Window, cx: &Context<Self>) -> Option<&Entity<Dock>> {
4119 self.all_docks().into_iter().find(|&dock| {
4120 dock.read(cx).is_open() && dock.focus_handle(cx).contains_focused(window, cx)
4121 })
4122 }
4123
4124 fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
4125 if let Some(dock) = self.active_dock(window, cx).cloned() {
4126 self.save_open_dock_positions(cx);
4127 dock.update(cx, |dock, cx| {
4128 dock.set_open(false, window, cx);
4129 });
4130 return true;
4131 }
4132 false
4133 }
4134
4135 pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4136 self.save_open_dock_positions(cx);
4137 for dock in self.all_docks() {
4138 dock.update(cx, |dock, cx| {
4139 dock.set_open(false, window, cx);
4140 });
4141 }
4142
4143 cx.focus_self(window);
4144 cx.notify();
4145 self.serialize_workspace(window, cx);
4146 }
4147
4148 fn get_open_dock_positions(&self, cx: &Context<Self>) -> Vec<DockPosition> {
4149 self.all_docks()
4150 .into_iter()
4151 .filter_map(|dock| {
4152 let dock_ref = dock.read(cx);
4153 if dock_ref.is_open() {
4154 Some(dock_ref.position())
4155 } else {
4156 None
4157 }
4158 })
4159 .collect()
4160 }
4161
4162 /// Saves the positions of currently open docks.
4163 ///
4164 /// Updates `last_open_dock_positions` with positions of all currently open
4165 /// docks, to later be restored by the 'Toggle All Docks' action.
4166 fn save_open_dock_positions(&mut self, cx: &mut Context<Self>) {
4167 let open_dock_positions = self.get_open_dock_positions(cx);
4168 if !open_dock_positions.is_empty() {
4169 self.last_open_dock_positions = open_dock_positions;
4170 }
4171 }
4172
4173 /// Toggles all docks between open and closed states.
4174 ///
4175 /// If any docks are open, closes all and remembers their positions. If all
4176 /// docks are closed, restores the last remembered dock configuration.
4177 fn toggle_all_docks(
4178 &mut self,
4179 _: &ToggleAllDocks,
4180 window: &mut Window,
4181 cx: &mut Context<Self>,
4182 ) {
4183 let open_dock_positions = self.get_open_dock_positions(cx);
4184
4185 if !open_dock_positions.is_empty() {
4186 self.close_all_docks(window, cx);
4187 } else if !self.last_open_dock_positions.is_empty() {
4188 self.restore_last_open_docks(window, cx);
4189 }
4190 }
4191
4192 /// Reopens docks from the most recently remembered configuration.
4193 ///
4194 /// Opens all docks whose positions are stored in `last_open_dock_positions`
4195 /// and clears the stored positions.
4196 fn restore_last_open_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4197 let positions_to_open = std::mem::take(&mut self.last_open_dock_positions);
4198
4199 for position in positions_to_open {
4200 let dock = self.dock_at_position(position);
4201 dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
4202 }
4203
4204 cx.focus_self(window);
4205 cx.notify();
4206 self.serialize_workspace(window, cx);
4207 }
4208
4209 /// Transfer focus to the panel of the given type.
4210 pub fn focus_panel<T: Panel>(
4211 &mut self,
4212 window: &mut Window,
4213 cx: &mut Context<Self>,
4214 ) -> Option<Entity<T>> {
4215 let panel = self.focus_or_unfocus_panel::<T>(window, cx, &mut |_, _, _| true)?;
4216 panel.to_any().downcast().ok()
4217 }
4218
4219 /// Focus the panel of the given type if it isn't already focused. If it is
4220 /// already focused, then transfer focus back to the workspace center.
4221 /// When the `close_panel_on_toggle` setting is enabled, also closes the
4222 /// panel when transferring focus back to the center.
4223 pub fn toggle_panel_focus<T: Panel>(
4224 &mut self,
4225 window: &mut Window,
4226 cx: &mut Context<Self>,
4227 ) -> bool {
4228 let mut did_focus_panel = false;
4229 self.focus_or_unfocus_panel::<T>(window, cx, &mut |panel, window, cx| {
4230 did_focus_panel = !panel.panel_focus_handle(cx).contains_focused(window, cx);
4231 did_focus_panel
4232 });
4233
4234 if !did_focus_panel && WorkspaceSettings::get_global(cx).close_panel_on_toggle {
4235 self.close_panel::<T>(window, cx);
4236 }
4237
4238 telemetry::event!(
4239 "Panel Button Clicked",
4240 name = T::persistent_name(),
4241 toggle_state = did_focus_panel
4242 );
4243
4244 did_focus_panel
4245 }
4246
4247 pub fn focus_center_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4248 if let Some(item) = self.active_item(cx) {
4249 item.item_focus_handle(cx).focus(window, cx);
4250 } else {
4251 log::error!("Could not find a focus target when switching focus to the center panes",);
4252 }
4253 }
4254
4255 pub fn activate_panel_for_proto_id(
4256 &mut self,
4257 panel_id: PanelId,
4258 window: &mut Window,
4259 cx: &mut Context<Self>,
4260 ) -> Option<Arc<dyn PanelHandle>> {
4261 let mut panel = None;
4262 for dock in self.all_docks() {
4263 if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
4264 panel = dock.update(cx, |dock, cx| {
4265 dock.activate_panel(panel_index, window, cx);
4266 dock.set_open(true, window, cx);
4267 dock.active_panel().cloned()
4268 });
4269 break;
4270 }
4271 }
4272
4273 if panel.is_some() {
4274 cx.notify();
4275 self.serialize_workspace(window, cx);
4276 }
4277
4278 panel
4279 }
4280
4281 /// Focus or unfocus the given panel type, depending on the given callback.
4282 fn focus_or_unfocus_panel<T: Panel>(
4283 &mut self,
4284 window: &mut Window,
4285 cx: &mut Context<Self>,
4286 should_focus: &mut dyn FnMut(&dyn PanelHandle, &mut Window, &mut Context<Dock>) -> bool,
4287 ) -> Option<Arc<dyn PanelHandle>> {
4288 let mut result_panel = None;
4289 let mut serialize = false;
4290 for dock in self.all_docks() {
4291 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
4292 let mut focus_center = false;
4293 let panel = dock.update(cx, |dock, cx| {
4294 dock.activate_panel(panel_index, window, cx);
4295
4296 let panel = dock.active_panel().cloned();
4297 if let Some(panel) = panel.as_ref() {
4298 if should_focus(&**panel, window, cx) {
4299 dock.set_open(true, window, cx);
4300 panel.panel_focus_handle(cx).focus(window, cx);
4301 } else {
4302 focus_center = true;
4303 }
4304 }
4305 panel
4306 });
4307
4308 if focus_center {
4309 self.active_pane
4310 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
4311 }
4312
4313 result_panel = panel;
4314 serialize = true;
4315 break;
4316 }
4317 }
4318
4319 if serialize {
4320 self.serialize_workspace(window, cx);
4321 }
4322
4323 cx.notify();
4324 result_panel
4325 }
4326
4327 /// Open the panel of the given type
4328 pub fn open_panel<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4329 for dock in self.all_docks() {
4330 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
4331 dock.update(cx, |dock, cx| {
4332 dock.activate_panel(panel_index, window, cx);
4333 dock.set_open(true, window, cx);
4334 });
4335 }
4336 }
4337 }
4338
4339 /// Open the panel of the given type, dismissing any zoomed items that
4340 /// would obscure it (e.g. a zoomed terminal).
4341 pub fn reveal_panel<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4342 let dock_position = self.all_docks().iter().find_map(|dock| {
4343 let dock = dock.read(cx);
4344 dock.panel_index_for_type::<T>().map(|_| dock.position())
4345 });
4346 self.dismiss_zoomed_items_to_reveal(dock_position, window, cx);
4347 self.open_panel::<T>(window, cx);
4348 }
4349
4350 pub fn close_panel<T: Panel>(&self, window: &mut Window, cx: &mut Context<Self>) {
4351 for dock in self.all_docks().iter() {
4352 dock.update(cx, |dock, cx| {
4353 if dock.panel::<T>().is_some() {
4354 dock.set_open(false, window, cx)
4355 }
4356 })
4357 }
4358 }
4359
4360 pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
4361 self.all_docks()
4362 .iter()
4363 .find_map(|dock| dock.read(cx).panel::<T>())
4364 }
4365
4366 fn dismiss_zoomed_items_to_reveal(
4367 &mut self,
4368 dock_to_reveal: Option<DockPosition>,
4369 window: &mut Window,
4370 cx: &mut Context<Self>,
4371 ) {
4372 // If a center pane is zoomed, unzoom it.
4373 for pane in &self.panes {
4374 if pane != &self.active_pane || dock_to_reveal.is_some() {
4375 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
4376 }
4377 }
4378
4379 // If another dock is zoomed, hide it.
4380 let mut focus_center = false;
4381 for dock in self.all_docks() {
4382 dock.update(cx, |dock, cx| {
4383 if Some(dock.position()) != dock_to_reveal
4384 && let Some(panel) = dock.active_panel()
4385 && panel.is_zoomed(window, cx)
4386 {
4387 focus_center |= panel.panel_focus_handle(cx).contains_focused(window, cx);
4388 dock.set_open(false, window, cx);
4389 }
4390 });
4391 }
4392
4393 if focus_center {
4394 self.active_pane
4395 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
4396 }
4397
4398 if self.zoomed_position != dock_to_reveal {
4399 self.zoomed = None;
4400 self.zoomed_position = None;
4401 cx.emit(Event::ZoomChanged);
4402 }
4403
4404 cx.notify();
4405 }
4406
4407 fn add_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
4408 let pane = cx.new(|cx| {
4409 let mut pane = Pane::new(
4410 self.weak_handle(),
4411 self.project.clone(),
4412 self.pane_history_timestamp.clone(),
4413 None,
4414 NewFile.boxed_clone(),
4415 true,
4416 window,
4417 cx,
4418 );
4419 pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
4420 pane
4421 });
4422 cx.subscribe_in(&pane, window, Self::handle_pane_event)
4423 .detach();
4424 self.panes.push(pane.clone());
4425
4426 window.focus(&pane.focus_handle(cx), cx);
4427
4428 cx.emit(Event::PaneAdded(pane.clone()));
4429 pane
4430 }
4431
4432 pub fn add_item_to_center(
4433 &mut self,
4434 item: Box<dyn ItemHandle>,
4435 window: &mut Window,
4436 cx: &mut Context<Self>,
4437 ) -> bool {
4438 if let Some(center_pane) = self.last_active_center_pane.clone() {
4439 if let Some(center_pane) = center_pane.upgrade() {
4440 center_pane.update(cx, |pane, cx| {
4441 pane.add_item(item, true, true, None, window, cx)
4442 });
4443 true
4444 } else {
4445 false
4446 }
4447 } else {
4448 false
4449 }
4450 }
4451
4452 pub fn add_item_to_active_pane(
4453 &mut self,
4454 item: Box<dyn ItemHandle>,
4455 destination_index: Option<usize>,
4456 focus_item: bool,
4457 window: &mut Window,
4458 cx: &mut App,
4459 ) {
4460 self.add_item(
4461 self.active_pane.clone(),
4462 item,
4463 destination_index,
4464 false,
4465 focus_item,
4466 window,
4467 cx,
4468 )
4469 }
4470
4471 pub fn add_item(
4472 &mut self,
4473 pane: Entity<Pane>,
4474 item: Box<dyn ItemHandle>,
4475 destination_index: Option<usize>,
4476 activate_pane: bool,
4477 focus_item: bool,
4478 window: &mut Window,
4479 cx: &mut App,
4480 ) {
4481 pane.update(cx, |pane, cx| {
4482 pane.add_item(
4483 item,
4484 activate_pane,
4485 focus_item,
4486 destination_index,
4487 window,
4488 cx,
4489 )
4490 });
4491 }
4492
4493 pub fn split_item(
4494 &mut self,
4495 split_direction: SplitDirection,
4496 item: Box<dyn ItemHandle>,
4497 window: &mut Window,
4498 cx: &mut Context<Self>,
4499 ) {
4500 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, window, cx);
4501 self.add_item(new_pane, item, None, true, true, window, cx);
4502 }
4503
4504 pub fn open_abs_path(
4505 &mut self,
4506 abs_path: PathBuf,
4507 options: OpenOptions,
4508 window: &mut Window,
4509 cx: &mut Context<Self>,
4510 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4511 cx.spawn_in(window, async move |workspace, cx| {
4512 let open_paths_task_result = workspace
4513 .update_in(cx, |workspace, window, cx| {
4514 workspace.open_paths(vec![abs_path.clone()], options, None, window, cx)
4515 })
4516 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
4517 .await;
4518 anyhow::ensure!(
4519 open_paths_task_result.len() == 1,
4520 "open abs path {abs_path:?} task returned incorrect number of results"
4521 );
4522 match open_paths_task_result
4523 .into_iter()
4524 .next()
4525 .expect("ensured single task result")
4526 {
4527 Some(open_result) => {
4528 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
4529 }
4530 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
4531 }
4532 })
4533 }
4534
4535 pub fn split_abs_path(
4536 &mut self,
4537 abs_path: PathBuf,
4538 visible: bool,
4539 window: &mut Window,
4540 cx: &mut Context<Self>,
4541 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4542 let project_path_task =
4543 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
4544 cx.spawn_in(window, async move |this, cx| {
4545 let (_, path) = project_path_task.await?;
4546 this.update_in(cx, |this, window, cx| this.split_path(path, window, cx))?
4547 .await
4548 })
4549 }
4550
4551 pub fn open_path(
4552 &mut self,
4553 path: impl Into<ProjectPath>,
4554 pane: Option<WeakEntity<Pane>>,
4555 focus_item: bool,
4556 window: &mut Window,
4557 cx: &mut App,
4558 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4559 self.open_path_preview(path, pane, focus_item, false, true, window, cx)
4560 }
4561
4562 pub fn open_path_preview(
4563 &mut self,
4564 path: impl Into<ProjectPath>,
4565 pane: Option<WeakEntity<Pane>>,
4566 focus_item: bool,
4567 allow_preview: bool,
4568 activate: bool,
4569 window: &mut Window,
4570 cx: &mut App,
4571 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4572 let pane = pane.unwrap_or_else(|| {
4573 self.last_active_center_pane.clone().unwrap_or_else(|| {
4574 self.panes
4575 .first()
4576 .expect("There must be an active pane")
4577 .downgrade()
4578 })
4579 });
4580
4581 let project_path = path.into();
4582 let task = self.load_path(project_path.clone(), window, cx);
4583 window.spawn(cx, async move |cx| {
4584 let (project_entry_id, build_item) = task.await?;
4585
4586 pane.update_in(cx, |pane, window, cx| {
4587 pane.open_item(
4588 project_entry_id,
4589 project_path,
4590 focus_item,
4591 allow_preview,
4592 activate,
4593 None,
4594 window,
4595 cx,
4596 build_item,
4597 )
4598 })
4599 })
4600 }
4601
4602 pub fn split_path(
4603 &mut self,
4604 path: impl Into<ProjectPath>,
4605 window: &mut Window,
4606 cx: &mut Context<Self>,
4607 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4608 self.split_path_preview(path, false, None, window, cx)
4609 }
4610
4611 pub fn split_path_preview(
4612 &mut self,
4613 path: impl Into<ProjectPath>,
4614 allow_preview: bool,
4615 split_direction: Option<SplitDirection>,
4616 window: &mut Window,
4617 cx: &mut Context<Self>,
4618 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4619 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
4620 self.panes
4621 .first()
4622 .expect("There must be an active pane")
4623 .downgrade()
4624 });
4625
4626 if let Member::Pane(center_pane) = &self.center.root
4627 && center_pane.read(cx).items_len() == 0
4628 {
4629 return self.open_path(path, Some(pane), true, window, cx);
4630 }
4631
4632 let project_path = path.into();
4633 let task = self.load_path(project_path.clone(), window, cx);
4634 cx.spawn_in(window, async move |this, cx| {
4635 let (project_entry_id, build_item) = task.await?;
4636 this.update_in(cx, move |this, window, cx| -> Option<_> {
4637 let pane = pane.upgrade()?;
4638 let new_pane = this.split_pane(
4639 pane,
4640 split_direction.unwrap_or(SplitDirection::Right),
4641 window,
4642 cx,
4643 );
4644 new_pane.update(cx, |new_pane, cx| {
4645 Some(new_pane.open_item(
4646 project_entry_id,
4647 project_path,
4648 true,
4649 allow_preview,
4650 true,
4651 None,
4652 window,
4653 cx,
4654 build_item,
4655 ))
4656 })
4657 })
4658 .map(|option| option.context("pane was dropped"))?
4659 })
4660 }
4661
4662 fn load_path(
4663 &mut self,
4664 path: ProjectPath,
4665 window: &mut Window,
4666 cx: &mut App,
4667 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
4668 let registry = cx.default_global::<ProjectItemRegistry>().clone();
4669 registry.open_path(self.project(), &path, window, cx)
4670 }
4671
4672 pub fn find_project_item<T>(
4673 &self,
4674 pane: &Entity<Pane>,
4675 project_item: &Entity<T::Item>,
4676 cx: &App,
4677 ) -> Option<Entity<T>>
4678 where
4679 T: ProjectItem,
4680 {
4681 use project::ProjectItem as _;
4682 let project_item = project_item.read(cx);
4683 let entry_id = project_item.entry_id(cx);
4684 let project_path = project_item.project_path(cx);
4685
4686 let mut item = None;
4687 if let Some(entry_id) = entry_id {
4688 item = pane.read(cx).item_for_entry(entry_id, cx);
4689 }
4690 if item.is_none()
4691 && let Some(project_path) = project_path
4692 {
4693 item = pane.read(cx).item_for_path(project_path, cx);
4694 }
4695
4696 item.and_then(|item| item.downcast::<T>())
4697 }
4698
4699 pub fn is_project_item_open<T>(
4700 &self,
4701 pane: &Entity<Pane>,
4702 project_item: &Entity<T::Item>,
4703 cx: &App,
4704 ) -> bool
4705 where
4706 T: ProjectItem,
4707 {
4708 self.find_project_item::<T>(pane, project_item, cx)
4709 .is_some()
4710 }
4711
4712 pub fn open_project_item<T>(
4713 &mut self,
4714 pane: Entity<Pane>,
4715 project_item: Entity<T::Item>,
4716 activate_pane: bool,
4717 focus_item: bool,
4718 keep_old_preview: bool,
4719 allow_new_preview: bool,
4720 window: &mut Window,
4721 cx: &mut Context<Self>,
4722 ) -> Entity<T>
4723 where
4724 T: ProjectItem,
4725 {
4726 let old_item_id = pane.read(cx).active_item().map(|item| item.item_id());
4727
4728 if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
4729 if !keep_old_preview
4730 && let Some(old_id) = old_item_id
4731 && old_id != item.item_id()
4732 {
4733 // switching to a different item, so unpreview old active item
4734 pane.update(cx, |pane, _| {
4735 pane.unpreview_item_if_preview(old_id);
4736 });
4737 }
4738
4739 self.activate_item(&item, activate_pane, focus_item, window, cx);
4740 if !allow_new_preview {
4741 pane.update(cx, |pane, _| {
4742 pane.unpreview_item_if_preview(item.item_id());
4743 });
4744 }
4745 return item;
4746 }
4747
4748 let item = pane.update(cx, |pane, cx| {
4749 cx.new(|cx| {
4750 T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx)
4751 })
4752 });
4753 let mut destination_index = None;
4754 pane.update(cx, |pane, cx| {
4755 if !keep_old_preview && let Some(old_id) = old_item_id {
4756 pane.unpreview_item_if_preview(old_id);
4757 }
4758 if allow_new_preview {
4759 destination_index = pane.replace_preview_item_id(item.item_id(), window, cx);
4760 }
4761 });
4762
4763 self.add_item(
4764 pane,
4765 Box::new(item.clone()),
4766 destination_index,
4767 activate_pane,
4768 focus_item,
4769 window,
4770 cx,
4771 );
4772 item
4773 }
4774
4775 pub fn open_shared_screen(
4776 &mut self,
4777 peer_id: PeerId,
4778 window: &mut Window,
4779 cx: &mut Context<Self>,
4780 ) {
4781 if let Some(shared_screen) =
4782 self.shared_screen_for_peer(peer_id, &self.active_pane, window, cx)
4783 {
4784 self.active_pane.update(cx, |pane, cx| {
4785 pane.add_item(Box::new(shared_screen), false, true, None, window, cx)
4786 });
4787 }
4788 }
4789
4790 pub fn activate_item(
4791 &mut self,
4792 item: &dyn ItemHandle,
4793 activate_pane: bool,
4794 focus_item: bool,
4795 window: &mut Window,
4796 cx: &mut App,
4797 ) -> bool {
4798 let result = self.panes.iter().find_map(|pane| {
4799 pane.read(cx)
4800 .index_for_item(item)
4801 .map(|ix| (pane.clone(), ix))
4802 });
4803 if let Some((pane, ix)) = result {
4804 pane.update(cx, |pane, cx| {
4805 pane.activate_item(ix, activate_pane, focus_item, window, cx)
4806 });
4807 true
4808 } else {
4809 false
4810 }
4811 }
4812
4813 fn activate_pane_at_index(
4814 &mut self,
4815 action: &ActivatePane,
4816 window: &mut Window,
4817 cx: &mut Context<Self>,
4818 ) {
4819 let panes = self.center.panes();
4820 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
4821 window.focus(&pane.focus_handle(cx), cx);
4822 } else {
4823 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx)
4824 .detach();
4825 }
4826 }
4827
4828 fn move_item_to_pane_at_index(
4829 &mut self,
4830 action: &MoveItemToPane,
4831 window: &mut Window,
4832 cx: &mut Context<Self>,
4833 ) {
4834 let panes = self.center.panes();
4835 let destination = match panes.get(action.destination) {
4836 Some(&destination) => destination.clone(),
4837 None => {
4838 if !action.clone && self.active_pane.read(cx).items_len() < 2 {
4839 return;
4840 }
4841 let direction = SplitDirection::Right;
4842 let split_off_pane = self
4843 .find_pane_in_direction(direction, cx)
4844 .unwrap_or_else(|| self.active_pane.clone());
4845 let new_pane = self.add_pane(window, cx);
4846 self.center.split(&split_off_pane, &new_pane, direction, cx);
4847 new_pane
4848 }
4849 };
4850
4851 if action.clone {
4852 if self
4853 .active_pane
4854 .read(cx)
4855 .active_item()
4856 .is_some_and(|item| item.can_split(cx))
4857 {
4858 clone_active_item(
4859 self.database_id(),
4860 &self.active_pane,
4861 &destination,
4862 action.focus,
4863 window,
4864 cx,
4865 );
4866 return;
4867 }
4868 }
4869 move_active_item(
4870 &self.active_pane,
4871 &destination,
4872 action.focus,
4873 true,
4874 window,
4875 cx,
4876 )
4877 }
4878
4879 pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) {
4880 let panes = self.center.panes();
4881 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
4882 let next_ix = (ix + 1) % panes.len();
4883 let next_pane = panes[next_ix].clone();
4884 window.focus(&next_pane.focus_handle(cx), cx);
4885 }
4886 }
4887
4888 pub fn activate_previous_pane(&mut self, window: &mut Window, cx: &mut App) {
4889 let panes = self.center.panes();
4890 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
4891 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
4892 let prev_pane = panes[prev_ix].clone();
4893 window.focus(&prev_pane.focus_handle(cx), cx);
4894 }
4895 }
4896
4897 pub fn activate_last_pane(&mut self, window: &mut Window, cx: &mut App) {
4898 let last_pane = self.center.last_pane();
4899 window.focus(&last_pane.focus_handle(cx), cx);
4900 }
4901
4902 pub fn activate_pane_in_direction(
4903 &mut self,
4904 direction: SplitDirection,
4905 window: &mut Window,
4906 cx: &mut App,
4907 ) {
4908 use ActivateInDirectionTarget as Target;
4909 enum Origin {
4910 Sidebar,
4911 LeftDock,
4912 RightDock,
4913 BottomDock,
4914 Center,
4915 }
4916
4917 let origin: Origin = if self
4918 .sidebar_focus_handle
4919 .as_ref()
4920 .is_some_and(|h| h.contains_focused(window, cx))
4921 {
4922 Origin::Sidebar
4923 } else {
4924 [
4925 (&self.left_dock, Origin::LeftDock),
4926 (&self.right_dock, Origin::RightDock),
4927 (&self.bottom_dock, Origin::BottomDock),
4928 ]
4929 .into_iter()
4930 .find_map(|(dock, origin)| {
4931 if dock.focus_handle(cx).contains_focused(window, cx) && dock.read(cx).is_open() {
4932 Some(origin)
4933 } else {
4934 None
4935 }
4936 })
4937 .unwrap_or(Origin::Center)
4938 };
4939
4940 let get_last_active_pane = || {
4941 let pane = self
4942 .last_active_center_pane
4943 .clone()
4944 .unwrap_or_else(|| {
4945 self.panes
4946 .first()
4947 .expect("There must be an active pane")
4948 .downgrade()
4949 })
4950 .upgrade()?;
4951 (pane.read(cx).items_len() != 0).then_some(pane)
4952 };
4953
4954 let try_dock =
4955 |dock: &Entity<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
4956
4957 let sidebar_target = self
4958 .sidebar_focus_handle
4959 .as_ref()
4960 .map(|h| Target::Sidebar(h.clone()));
4961
4962 let sidebar_on_right = self
4963 .multi_workspace
4964 .as_ref()
4965 .and_then(|mw| mw.upgrade())
4966 .map_or(false, |mw| {
4967 mw.read(cx).sidebar_side(cx) == SidebarSide::Right
4968 });
4969
4970 let away_from_sidebar = if sidebar_on_right {
4971 SplitDirection::Left
4972 } else {
4973 SplitDirection::Right
4974 };
4975
4976 let (near_dock, far_dock) = if sidebar_on_right {
4977 (&self.right_dock, &self.left_dock)
4978 } else {
4979 (&self.left_dock, &self.right_dock)
4980 };
4981
4982 let target = match (origin, direction) {
4983 (Origin::Sidebar, dir) if dir == away_from_sidebar => try_dock(near_dock)
4984 .or_else(|| get_last_active_pane().map(Target::Pane))
4985 .or_else(|| try_dock(&self.bottom_dock))
4986 .or_else(|| try_dock(far_dock)),
4987
4988 (Origin::Sidebar, _) => None,
4989
4990 // We're in the center, so we first try to go to a different pane,
4991 // otherwise try to go to a dock.
4992 (Origin::Center, direction) => {
4993 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
4994 Some(Target::Pane(pane))
4995 } else {
4996 match direction {
4997 SplitDirection::Up => None,
4998 SplitDirection::Down => try_dock(&self.bottom_dock),
4999 SplitDirection::Left => {
5000 let dock_target = try_dock(&self.left_dock);
5001 if sidebar_on_right {
5002 dock_target
5003 } else {
5004 dock_target.or(sidebar_target)
5005 }
5006 }
5007 SplitDirection::Right => {
5008 let dock_target = try_dock(&self.right_dock);
5009 if sidebar_on_right {
5010 dock_target.or(sidebar_target)
5011 } else {
5012 dock_target
5013 }
5014 }
5015 }
5016 }
5017 }
5018
5019 (Origin::LeftDock, SplitDirection::Right) => {
5020 if let Some(last_active_pane) = get_last_active_pane() {
5021 Some(Target::Pane(last_active_pane))
5022 } else {
5023 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
5024 }
5025 }
5026
5027 (Origin::LeftDock, SplitDirection::Left) => {
5028 if sidebar_on_right {
5029 None
5030 } else {
5031 sidebar_target
5032 }
5033 }
5034
5035 (Origin::LeftDock, SplitDirection::Down)
5036 | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
5037
5038 (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
5039 (Origin::BottomDock, SplitDirection::Left) => {
5040 let dock_target = try_dock(&self.left_dock);
5041 if sidebar_on_right {
5042 dock_target
5043 } else {
5044 dock_target.or(sidebar_target)
5045 }
5046 }
5047 (Origin::BottomDock, SplitDirection::Right) => {
5048 let dock_target = try_dock(&self.right_dock);
5049 if sidebar_on_right {
5050 dock_target.or(sidebar_target)
5051 } else {
5052 dock_target
5053 }
5054 }
5055
5056 (Origin::RightDock, SplitDirection::Left) => {
5057 if let Some(last_active_pane) = get_last_active_pane() {
5058 Some(Target::Pane(last_active_pane))
5059 } else {
5060 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
5061 }
5062 }
5063
5064 (Origin::RightDock, SplitDirection::Right) => {
5065 if sidebar_on_right {
5066 sidebar_target
5067 } else {
5068 None
5069 }
5070 }
5071
5072 _ => None,
5073 };
5074
5075 match target {
5076 Some(ActivateInDirectionTarget::Pane(pane)) => {
5077 let pane = pane.read(cx);
5078 if let Some(item) = pane.active_item() {
5079 item.item_focus_handle(cx).focus(window, cx);
5080 } else {
5081 log::error!(
5082 "Could not find a focus target when in switching focus in {direction} direction for a pane",
5083 );
5084 }
5085 }
5086 Some(ActivateInDirectionTarget::Dock(dock)) => {
5087 // Defer this to avoid a panic when the dock's active panel is already on the stack.
5088 window.defer(cx, move |window, cx| {
5089 let dock = dock.read(cx);
5090 if let Some(panel) = dock.active_panel() {
5091 panel.panel_focus_handle(cx).focus(window, cx);
5092 } else {
5093 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
5094 }
5095 })
5096 }
5097 Some(ActivateInDirectionTarget::Sidebar(focus_handle)) => {
5098 focus_handle.focus(window, cx);
5099 }
5100 None => {}
5101 }
5102 }
5103
5104 pub fn move_item_to_pane_in_direction(
5105 &mut self,
5106 action: &MoveItemToPaneInDirection,
5107 window: &mut Window,
5108 cx: &mut Context<Self>,
5109 ) {
5110 let destination = match self.find_pane_in_direction(action.direction, cx) {
5111 Some(destination) => destination,
5112 None => {
5113 if !action.clone && self.active_pane.read(cx).items_len() < 2 {
5114 return;
5115 }
5116 let new_pane = self.add_pane(window, cx);
5117 self.center
5118 .split(&self.active_pane, &new_pane, action.direction, cx);
5119 new_pane
5120 }
5121 };
5122
5123 if action.clone {
5124 if self
5125 .active_pane
5126 .read(cx)
5127 .active_item()
5128 .is_some_and(|item| item.can_split(cx))
5129 {
5130 clone_active_item(
5131 self.database_id(),
5132 &self.active_pane,
5133 &destination,
5134 action.focus,
5135 window,
5136 cx,
5137 );
5138 return;
5139 }
5140 }
5141 move_active_item(
5142 &self.active_pane,
5143 &destination,
5144 action.focus,
5145 true,
5146 window,
5147 cx,
5148 );
5149 }
5150
5151 pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
5152 self.center.bounding_box_for_pane(pane)
5153 }
5154
5155 pub fn find_pane_in_direction(
5156 &mut self,
5157 direction: SplitDirection,
5158 cx: &App,
5159 ) -> Option<Entity<Pane>> {
5160 self.center
5161 .find_pane_in_direction(&self.active_pane, direction, cx)
5162 .cloned()
5163 }
5164
5165 pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
5166 if let Some(to) = self.find_pane_in_direction(direction, cx) {
5167 self.center.swap(&self.active_pane, &to, cx);
5168 cx.notify();
5169 }
5170 }
5171
5172 pub fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
5173 if self
5174 .center
5175 .move_to_border(&self.active_pane, direction, cx)
5176 .unwrap()
5177 {
5178 cx.notify();
5179 }
5180 }
5181
5182 pub fn resize_pane(
5183 &mut self,
5184 axis: gpui::Axis,
5185 amount: Pixels,
5186 window: &mut Window,
5187 cx: &mut Context<Self>,
5188 ) {
5189 let docks = self.all_docks();
5190 let active_dock = docks
5191 .into_iter()
5192 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
5193
5194 if let Some(dock_entity) = active_dock {
5195 let dock = dock_entity.read(cx);
5196 let Some(panel_size) = self.dock_size(&dock, window, cx) else {
5197 return;
5198 };
5199 match dock.position() {
5200 DockPosition::Left => self.resize_left_dock(panel_size + amount, window, cx),
5201 DockPosition::Bottom => self.resize_bottom_dock(panel_size + amount, window, cx),
5202 DockPosition::Right => self.resize_right_dock(panel_size + amount, window, cx),
5203 }
5204 } else {
5205 self.center
5206 .resize(&self.active_pane, axis, amount, &self.bounds, cx);
5207 }
5208 cx.notify();
5209 }
5210
5211 pub fn reset_pane_sizes(&mut self, cx: &mut Context<Self>) {
5212 self.center.reset_pane_sizes(cx);
5213 cx.notify();
5214 }
5215
5216 fn handle_pane_focused(
5217 &mut self,
5218 pane: Entity<Pane>,
5219 window: &mut Window,
5220 cx: &mut Context<Self>,
5221 ) {
5222 // This is explicitly hoisted out of the following check for pane identity as
5223 // terminal panel panes are not registered as a center panes.
5224 self.status_bar.update(cx, |status_bar, cx| {
5225 status_bar.set_active_pane(&pane, window, cx);
5226 });
5227 if self.active_pane != pane {
5228 self.set_active_pane(&pane, window, cx);
5229 }
5230
5231 if self.last_active_center_pane.is_none() {
5232 self.last_active_center_pane = Some(pane.downgrade());
5233 }
5234
5235 // If this pane is in a dock, preserve that dock when dismissing zoomed items.
5236 // This prevents the dock from closing when focus events fire during window activation.
5237 // We also preserve any dock whose active panel itself has focus — this covers
5238 // panels like AgentPanel that don't implement `pane()` but can still be zoomed.
5239 let dock_to_preserve = self.all_docks().iter().find_map(|dock| {
5240 let dock_read = dock.read(cx);
5241 if let Some(panel) = dock_read.active_panel() {
5242 if panel.pane(cx).is_some_and(|dock_pane| dock_pane == pane)
5243 || panel.panel_focus_handle(cx).contains_focused(window, cx)
5244 {
5245 return Some(dock_read.position());
5246 }
5247 }
5248 None
5249 });
5250
5251 self.dismiss_zoomed_items_to_reveal(dock_to_preserve, window, cx);
5252 if pane.read(cx).is_zoomed() {
5253 self.zoomed = Some(pane.downgrade().into());
5254 } else {
5255 self.zoomed = None;
5256 }
5257 self.zoomed_position = None;
5258 cx.emit(Event::ZoomChanged);
5259 self.update_active_view_for_followers(window, cx);
5260 pane.update(cx, |pane, _| {
5261 pane.track_alternate_file_items();
5262 });
5263
5264 cx.notify();
5265 }
5266
5267 fn set_active_pane(
5268 &mut self,
5269 pane: &Entity<Pane>,
5270 window: &mut Window,
5271 cx: &mut Context<Self>,
5272 ) {
5273 self.active_pane = pane.clone();
5274 self.active_item_path_changed(true, window, cx);
5275 self.last_active_center_pane = Some(pane.downgrade());
5276 }
5277
5278 fn handle_panel_focused(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5279 self.update_active_view_for_followers(window, cx);
5280 }
5281
5282 fn handle_pane_event(
5283 &mut self,
5284 pane: &Entity<Pane>,
5285 event: &pane::Event,
5286 window: &mut Window,
5287 cx: &mut Context<Self>,
5288 ) {
5289 let mut serialize_workspace = true;
5290 match event {
5291 pane::Event::AddItem { item } => {
5292 item.added_to_pane(self, pane.clone(), window, cx);
5293 cx.emit(Event::ItemAdded {
5294 item: item.boxed_clone(),
5295 });
5296 }
5297 pane::Event::Split { direction, mode } => {
5298 match mode {
5299 SplitMode::ClonePane => {
5300 self.split_and_clone(pane.clone(), *direction, window, cx)
5301 .detach();
5302 }
5303 SplitMode::EmptyPane => {
5304 self.split_pane(pane.clone(), *direction, window, cx);
5305 }
5306 SplitMode::MovePane => {
5307 self.split_and_move(pane.clone(), *direction, window, cx);
5308 }
5309 };
5310 }
5311 pane::Event::JoinIntoNext => {
5312 self.join_pane_into_next(pane.clone(), window, cx);
5313 }
5314 pane::Event::JoinAll => {
5315 self.join_all_panes(window, cx);
5316 }
5317 pane::Event::Remove { focus_on_pane } => {
5318 self.remove_pane(pane.clone(), focus_on_pane.clone(), window, cx);
5319 }
5320 pane::Event::ActivateItem {
5321 local,
5322 focus_changed,
5323 } => {
5324 window.invalidate_character_coordinates();
5325
5326 pane.update(cx, |pane, _| {
5327 pane.track_alternate_file_items();
5328 });
5329 if *local {
5330 self.unfollow_in_pane(pane, window, cx);
5331 }
5332 serialize_workspace = *focus_changed || pane != self.active_pane();
5333 if pane == self.active_pane() {
5334 self.active_item_path_changed(*focus_changed, window, cx);
5335 self.update_active_view_for_followers(window, cx);
5336 } else if *local {
5337 self.set_active_pane(pane, window, cx);
5338 }
5339 }
5340 pane::Event::UserSavedItem { item, save_intent } => {
5341 cx.emit(Event::UserSavedItem {
5342 pane: pane.downgrade(),
5343 item: item.boxed_clone(),
5344 save_intent: *save_intent,
5345 });
5346 serialize_workspace = false;
5347 }
5348 pane::Event::ChangeItemTitle => {
5349 if *pane == self.active_pane {
5350 self.active_item_path_changed(false, window, cx);
5351 }
5352 serialize_workspace = false;
5353 }
5354 pane::Event::RemovedItem { item } => {
5355 cx.emit(Event::ActiveItemChanged);
5356 self.update_window_edited(window, cx);
5357 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id())
5358 && entry.get().entity_id() == pane.entity_id()
5359 {
5360 entry.remove();
5361 }
5362 cx.emit(Event::ItemRemoved {
5363 item_id: item.item_id(),
5364 });
5365 }
5366 pane::Event::Focus => {
5367 window.invalidate_character_coordinates();
5368 self.handle_pane_focused(pane.clone(), window, cx);
5369 }
5370 pane::Event::ZoomIn => {
5371 if *pane == self.active_pane {
5372 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
5373 if pane.read(cx).has_focus(window, cx) {
5374 self.zoomed = Some(pane.downgrade().into());
5375 self.zoomed_position = None;
5376 cx.emit(Event::ZoomChanged);
5377 }
5378 cx.notify();
5379 }
5380 }
5381 pane::Event::ZoomOut => {
5382 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
5383 if self.zoomed_position.is_none() {
5384 self.zoomed = None;
5385 cx.emit(Event::ZoomChanged);
5386 }
5387 cx.notify();
5388 }
5389 pane::Event::ItemPinned | pane::Event::ItemUnpinned => {}
5390 }
5391
5392 if serialize_workspace {
5393 self.serialize_workspace(window, cx);
5394 }
5395 }
5396
5397 pub fn unfollow_in_pane(
5398 &mut self,
5399 pane: &Entity<Pane>,
5400 window: &mut Window,
5401 cx: &mut Context<Workspace>,
5402 ) -> Option<CollaboratorId> {
5403 let leader_id = self.leader_for_pane(pane)?;
5404 self.unfollow(leader_id, window, cx);
5405 Some(leader_id)
5406 }
5407
5408 pub fn split_pane(
5409 &mut self,
5410 pane_to_split: Entity<Pane>,
5411 split_direction: SplitDirection,
5412 window: &mut Window,
5413 cx: &mut Context<Self>,
5414 ) -> Entity<Pane> {
5415 let new_pane = self.add_pane(window, cx);
5416 self.center
5417 .split(&pane_to_split, &new_pane, split_direction, cx);
5418 cx.notify();
5419 new_pane
5420 }
5421
5422 pub fn split_and_move(
5423 &mut self,
5424 pane: Entity<Pane>,
5425 direction: SplitDirection,
5426 window: &mut Window,
5427 cx: &mut Context<Self>,
5428 ) {
5429 let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx)) else {
5430 return;
5431 };
5432 let new_pane = self.add_pane(window, cx);
5433 new_pane.update(cx, |pane, cx| {
5434 pane.add_item(item, true, true, None, window, cx)
5435 });
5436 self.center.split(&pane, &new_pane, direction, cx);
5437 cx.notify();
5438 }
5439
5440 pub fn split_and_clone(
5441 &mut self,
5442 pane: Entity<Pane>,
5443 direction: SplitDirection,
5444 window: &mut Window,
5445 cx: &mut Context<Self>,
5446 ) -> Task<Option<Entity<Pane>>> {
5447 let Some(item) = pane.read(cx).active_item() else {
5448 return Task::ready(None);
5449 };
5450 if !item.can_split(cx) {
5451 return Task::ready(None);
5452 }
5453 let task = item.clone_on_split(self.database_id(), window, cx);
5454 cx.spawn_in(window, async move |this, cx| {
5455 if let Some(clone) = task.await {
5456 this.update_in(cx, |this, window, cx| {
5457 let new_pane = this.add_pane(window, cx);
5458 let nav_history = pane.read(cx).fork_nav_history();
5459 new_pane.update(cx, |pane, cx| {
5460 pane.set_nav_history(nav_history, cx);
5461 pane.add_item(clone, true, true, None, window, cx)
5462 });
5463 this.center.split(&pane, &new_pane, direction, cx);
5464 cx.notify();
5465 new_pane
5466 })
5467 .ok()
5468 } else {
5469 None
5470 }
5471 })
5472 }
5473
5474 pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5475 let active_item = self.active_pane.read(cx).active_item();
5476 for pane in &self.panes {
5477 join_pane_into_active(&self.active_pane, pane, window, cx);
5478 }
5479 if let Some(active_item) = active_item {
5480 self.activate_item(active_item.as_ref(), true, true, window, cx);
5481 }
5482 cx.notify();
5483 }
5484
5485 pub fn join_pane_into_next(
5486 &mut self,
5487 pane: Entity<Pane>,
5488 window: &mut Window,
5489 cx: &mut Context<Self>,
5490 ) {
5491 let next_pane = self
5492 .find_pane_in_direction(SplitDirection::Right, cx)
5493 .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
5494 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
5495 .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
5496 let Some(next_pane) = next_pane else {
5497 return;
5498 };
5499 move_all_items(&pane, &next_pane, window, cx);
5500 cx.notify();
5501 }
5502
5503 fn remove_pane(
5504 &mut self,
5505 pane: Entity<Pane>,
5506 focus_on: Option<Entity<Pane>>,
5507 window: &mut Window,
5508 cx: &mut Context<Self>,
5509 ) {
5510 if self.center.remove(&pane, cx).unwrap() {
5511 self.force_remove_pane(&pane, &focus_on, window, cx);
5512 self.unfollow_in_pane(&pane, window, cx);
5513 self.last_leaders_by_pane.remove(&pane.downgrade());
5514 for removed_item in pane.read(cx).items() {
5515 self.panes_by_item.remove(&removed_item.item_id());
5516 }
5517
5518 cx.notify();
5519 } else {
5520 self.active_item_path_changed(true, window, cx);
5521 }
5522 cx.emit(Event::PaneRemoved);
5523 }
5524
5525 pub fn panes_mut(&mut self) -> &mut [Entity<Pane>] {
5526 &mut self.panes
5527 }
5528
5529 pub fn panes(&self) -> &[Entity<Pane>] {
5530 &self.panes
5531 }
5532
5533 pub fn active_pane(&self) -> &Entity<Pane> {
5534 &self.active_pane
5535 }
5536
5537 pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> {
5538 for dock in self.all_docks() {
5539 if dock.focus_handle(cx).contains_focused(window, cx)
5540 && let Some(pane) = dock
5541 .read(cx)
5542 .active_panel()
5543 .and_then(|panel| panel.pane(cx))
5544 {
5545 return pane;
5546 }
5547 }
5548 self.active_pane().clone()
5549 }
5550
5551 pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
5552 self.find_pane_in_direction(SplitDirection::Right, cx)
5553 .unwrap_or_else(|| {
5554 self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx)
5555 })
5556 }
5557
5558 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> {
5559 self.pane_for_item_id(handle.item_id())
5560 }
5561
5562 pub fn pane_for_item_id(&self, item_id: EntityId) -> Option<Entity<Pane>> {
5563 let weak_pane = self.panes_by_item.get(&item_id)?;
5564 weak_pane.upgrade()
5565 }
5566
5567 pub fn pane_for_entity_id(&self, entity_id: EntityId) -> Option<Entity<Pane>> {
5568 self.panes
5569 .iter()
5570 .find(|pane| pane.entity_id() == entity_id)
5571 .cloned()
5572 }
5573
5574 fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
5575 self.follower_states.retain(|leader_id, state| {
5576 if *leader_id == CollaboratorId::PeerId(peer_id) {
5577 for item in state.items_by_leader_view_id.values() {
5578 item.view.set_leader_id(None, window, cx);
5579 }
5580 false
5581 } else {
5582 true
5583 }
5584 });
5585 cx.notify();
5586 }
5587
5588 pub fn start_following(
5589 &mut self,
5590 leader_id: impl Into<CollaboratorId>,
5591 window: &mut Window,
5592 cx: &mut Context<Self>,
5593 ) -> Option<Task<Result<()>>> {
5594 let leader_id = leader_id.into();
5595 let pane = self.active_pane().clone();
5596
5597 self.last_leaders_by_pane
5598 .insert(pane.downgrade(), leader_id);
5599 self.unfollow(leader_id, window, cx);
5600 self.unfollow_in_pane(&pane, window, cx);
5601 self.follower_states.insert(
5602 leader_id,
5603 FollowerState {
5604 center_pane: pane.clone(),
5605 dock_pane: None,
5606 active_view_id: None,
5607 items_by_leader_view_id: Default::default(),
5608 },
5609 );
5610 cx.notify();
5611
5612 match leader_id {
5613 CollaboratorId::PeerId(leader_peer_id) => {
5614 let room_id = self.active_call()?.room_id(cx)?;
5615 let project_id = self.project.read(cx).remote_id();
5616 let request = self.app_state.client.request(proto::Follow {
5617 room_id,
5618 project_id,
5619 leader_id: Some(leader_peer_id),
5620 });
5621
5622 Some(cx.spawn_in(window, async move |this, cx| {
5623 let response = request.await?;
5624 this.update(cx, |this, _| {
5625 let state = this
5626 .follower_states
5627 .get_mut(&leader_id)
5628 .context("following interrupted")?;
5629 state.active_view_id = response
5630 .active_view
5631 .as_ref()
5632 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
5633 anyhow::Ok(())
5634 })??;
5635 if let Some(view) = response.active_view {
5636 Self::add_view_from_leader(this.clone(), leader_peer_id, &view, cx).await?;
5637 }
5638 this.update_in(cx, |this, window, cx| {
5639 this.leader_updated(leader_id, window, cx)
5640 })?;
5641 Ok(())
5642 }))
5643 }
5644 CollaboratorId::Agent => {
5645 self.leader_updated(leader_id, window, cx)?;
5646 Some(Task::ready(Ok(())))
5647 }
5648 }
5649 }
5650
5651 pub fn follow_next_collaborator(
5652 &mut self,
5653 _: &FollowNextCollaborator,
5654 window: &mut Window,
5655 cx: &mut Context<Self>,
5656 ) {
5657 let collaborators = self.project.read(cx).collaborators();
5658 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
5659 let mut collaborators = collaborators.keys().copied();
5660 for peer_id in collaborators.by_ref() {
5661 if CollaboratorId::PeerId(peer_id) == leader_id {
5662 break;
5663 }
5664 }
5665 collaborators.next().map(CollaboratorId::PeerId)
5666 } else if let Some(last_leader_id) =
5667 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
5668 {
5669 match last_leader_id {
5670 CollaboratorId::PeerId(peer_id) => {
5671 if collaborators.contains_key(peer_id) {
5672 Some(*last_leader_id)
5673 } else {
5674 None
5675 }
5676 }
5677 CollaboratorId::Agent => Some(CollaboratorId::Agent),
5678 }
5679 } else {
5680 None
5681 };
5682
5683 let pane = self.active_pane.clone();
5684 let Some(leader_id) = next_leader_id.or_else(|| {
5685 Some(CollaboratorId::PeerId(
5686 collaborators.keys().copied().next()?,
5687 ))
5688 }) else {
5689 return;
5690 };
5691 if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
5692 return;
5693 }
5694 if let Some(task) = self.start_following(leader_id, window, cx) {
5695 task.detach_and_log_err(cx)
5696 }
5697 }
5698
5699 pub fn follow(
5700 &mut self,
5701 leader_id: impl Into<CollaboratorId>,
5702 window: &mut Window,
5703 cx: &mut Context<Self>,
5704 ) {
5705 let leader_id = leader_id.into();
5706
5707 if let CollaboratorId::PeerId(peer_id) = leader_id {
5708 let Some(active_call) = GlobalAnyActiveCall::try_global(cx) else {
5709 return;
5710 };
5711 let Some(remote_participant) =
5712 active_call.0.remote_participant_for_peer_id(peer_id, cx)
5713 else {
5714 return;
5715 };
5716
5717 let project = self.project.read(cx);
5718
5719 let other_project_id = match remote_participant.location {
5720 ParticipantLocation::External => None,
5721 ParticipantLocation::UnsharedProject => None,
5722 ParticipantLocation::SharedProject { project_id } => {
5723 if Some(project_id) == project.remote_id() {
5724 None
5725 } else {
5726 Some(project_id)
5727 }
5728 }
5729 };
5730
5731 // if they are active in another project, follow there.
5732 if let Some(project_id) = other_project_id {
5733 let app_state = self.app_state.clone();
5734 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
5735 .detach_and_prompt_err("Failed to join project", window, cx, |error, _, _| {
5736 Some(format!("{error:#}"))
5737 });
5738 }
5739 }
5740
5741 // if you're already following, find the right pane and focus it.
5742 if let Some(follower_state) = self.follower_states.get(&leader_id) {
5743 window.focus(&follower_state.pane().focus_handle(cx), cx);
5744
5745 return;
5746 }
5747
5748 // Otherwise, follow.
5749 if let Some(task) = self.start_following(leader_id, window, cx) {
5750 task.detach_and_log_err(cx)
5751 }
5752 }
5753
5754 pub fn unfollow(
5755 &mut self,
5756 leader_id: impl Into<CollaboratorId>,
5757 window: &mut Window,
5758 cx: &mut Context<Self>,
5759 ) -> Option<()> {
5760 cx.notify();
5761
5762 let leader_id = leader_id.into();
5763 let state = self.follower_states.remove(&leader_id)?;
5764 for (_, item) in state.items_by_leader_view_id {
5765 item.view.set_leader_id(None, window, cx);
5766 }
5767
5768 if let CollaboratorId::PeerId(leader_peer_id) = leader_id {
5769 let project_id = self.project.read(cx).remote_id();
5770 let room_id = self.active_call()?.room_id(cx)?;
5771 self.app_state
5772 .client
5773 .send(proto::Unfollow {
5774 room_id,
5775 project_id,
5776 leader_id: Some(leader_peer_id),
5777 })
5778 .log_err();
5779 }
5780
5781 Some(())
5782 }
5783
5784 pub fn is_being_followed(&self, id: impl Into<CollaboratorId>) -> bool {
5785 self.follower_states.contains_key(&id.into())
5786 }
5787
5788 fn active_item_path_changed(
5789 &mut self,
5790 focus_changed: bool,
5791 window: &mut Window,
5792 cx: &mut Context<Self>,
5793 ) {
5794 cx.emit(Event::ActiveItemChanged);
5795 let active_entry = self.active_project_path(cx);
5796 self.project.update(cx, |project, cx| {
5797 project.set_active_path(active_entry.clone(), cx)
5798 });
5799
5800 if focus_changed && let Some(project_path) = &active_entry {
5801 let git_store_entity = self.project.read(cx).git_store().clone();
5802 git_store_entity.update(cx, |git_store, cx| {
5803 git_store.set_active_repo_for_path(project_path, cx);
5804 });
5805 }
5806
5807 self.update_window_title(window, cx);
5808 }
5809
5810 fn update_window_title(&mut self, window: &mut Window, cx: &mut App) {
5811 let project = self.project().read(cx);
5812 let mut title = String::new();
5813
5814 for (i, worktree) in project.visible_worktrees(cx).enumerate() {
5815 let name = {
5816 let settings_location = SettingsLocation {
5817 worktree_id: worktree.read(cx).id(),
5818 path: RelPath::empty(),
5819 };
5820
5821 let settings = WorktreeSettings::get(Some(settings_location), cx);
5822 match &settings.project_name {
5823 Some(name) => name.as_str(),
5824 None => worktree.read(cx).root_name_str(),
5825 }
5826 };
5827 if i > 0 {
5828 title.push_str(", ");
5829 }
5830 title.push_str(name);
5831 }
5832
5833 if title.is_empty() {
5834 title = "empty project".to_string();
5835 }
5836
5837 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
5838 let filename = path.path.file_name().or_else(|| {
5839 Some(
5840 project
5841 .worktree_for_id(path.worktree_id, cx)?
5842 .read(cx)
5843 .root_name_str(),
5844 )
5845 });
5846
5847 if let Some(filename) = filename {
5848 title.push_str(" — ");
5849 title.push_str(filename.as_ref());
5850 }
5851 }
5852
5853 if project.is_via_collab() {
5854 title.push_str(" ↙");
5855 } else if project.is_shared() {
5856 title.push_str(" ↗");
5857 }
5858
5859 if let Some(last_title) = self.last_window_title.as_ref()
5860 && &title == last_title
5861 {
5862 return;
5863 }
5864 window.set_window_title(&title);
5865 SystemWindowTabController::update_tab_title(
5866 cx,
5867 window.window_handle().window_id(),
5868 SharedString::from(&title),
5869 );
5870 self.last_window_title = Some(title);
5871 }
5872
5873 fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
5874 let is_edited = !self.project.read(cx).is_disconnected(cx) && !self.dirty_items.is_empty();
5875 if is_edited != self.window_edited {
5876 self.window_edited = is_edited;
5877 window.set_window_edited(self.window_edited)
5878 }
5879 }
5880
5881 fn update_item_dirty_state(
5882 &mut self,
5883 item: &dyn ItemHandle,
5884 window: &mut Window,
5885 cx: &mut App,
5886 ) {
5887 let is_dirty = item.is_dirty(cx);
5888 let item_id = item.item_id();
5889 let was_dirty = self.dirty_items.contains_key(&item_id);
5890 if is_dirty == was_dirty {
5891 return;
5892 }
5893 if was_dirty {
5894 self.dirty_items.remove(&item_id);
5895 self.update_window_edited(window, cx);
5896 return;
5897 }
5898
5899 let workspace = self.weak_handle();
5900 let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
5901 return;
5902 };
5903 let on_release_callback = Box::new(move |cx: &mut App| {
5904 window_handle
5905 .update(cx, |_, window, cx| {
5906 workspace
5907 .update(cx, |workspace, cx| {
5908 workspace.dirty_items.remove(&item_id);
5909 workspace.update_window_edited(window, cx)
5910 })
5911 .ok();
5912 })
5913 .ok();
5914 });
5915
5916 let s = item.on_release(cx, on_release_callback);
5917 self.dirty_items.insert(item_id, s);
5918 self.update_window_edited(window, cx);
5919 }
5920
5921 fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
5922 if self.notifications.is_empty() {
5923 None
5924 } else {
5925 Some(
5926 div()
5927 .absolute()
5928 .right_3()
5929 .bottom_3()
5930 .w_112()
5931 .h_full()
5932 .flex()
5933 .flex_col()
5934 .justify_end()
5935 .gap_2()
5936 .children(
5937 self.notifications
5938 .iter()
5939 .map(|(_, notification)| notification.clone().into_any()),
5940 ),
5941 )
5942 }
5943 }
5944
5945 // RPC handlers
5946
5947 fn active_view_for_follower(
5948 &self,
5949 follower_project_id: Option<u64>,
5950 window: &mut Window,
5951 cx: &mut Context<Self>,
5952 ) -> Option<proto::View> {
5953 let (item, panel_id) = self.active_item_for_followers(window, cx);
5954 let item = item?;
5955 let leader_id = self
5956 .pane_for(&*item)
5957 .and_then(|pane| self.leader_for_pane(&pane));
5958 let leader_peer_id = match leader_id {
5959 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
5960 Some(CollaboratorId::Agent) | None => None,
5961 };
5962
5963 let item_handle = item.to_followable_item_handle(cx)?;
5964 let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
5965 let variant = item_handle.to_state_proto(window, cx)?;
5966
5967 if item_handle.is_project_item(window, cx)
5968 && (follower_project_id.is_none()
5969 || follower_project_id != self.project.read(cx).remote_id())
5970 {
5971 return None;
5972 }
5973
5974 Some(proto::View {
5975 id: id.to_proto(),
5976 leader_id: leader_peer_id,
5977 variant: Some(variant),
5978 panel_id: panel_id.map(|id| id as i32),
5979 })
5980 }
5981
5982 fn handle_follow(
5983 &mut self,
5984 follower_project_id: Option<u64>,
5985 window: &mut Window,
5986 cx: &mut Context<Self>,
5987 ) -> proto::FollowResponse {
5988 let active_view = self.active_view_for_follower(follower_project_id, window, cx);
5989
5990 cx.notify();
5991 proto::FollowResponse {
5992 views: active_view.iter().cloned().collect(),
5993 active_view,
5994 }
5995 }
5996
5997 fn handle_update_followers(
5998 &mut self,
5999 leader_id: PeerId,
6000 message: proto::UpdateFollowers,
6001 _window: &mut Window,
6002 _cx: &mut Context<Self>,
6003 ) {
6004 self.leader_updates_tx
6005 .unbounded_send((leader_id, message))
6006 .ok();
6007 }
6008
6009 async fn process_leader_update(
6010 this: &WeakEntity<Self>,
6011 leader_id: PeerId,
6012 update: proto::UpdateFollowers,
6013 cx: &mut AsyncWindowContext,
6014 ) -> Result<()> {
6015 match update.variant.context("invalid update")? {
6016 proto::update_followers::Variant::CreateView(view) => {
6017 let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
6018 let should_add_view = this.update(cx, |this, _| {
6019 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
6020 anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
6021 } else {
6022 anyhow::Ok(false)
6023 }
6024 })??;
6025
6026 if should_add_view {
6027 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
6028 }
6029 }
6030 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
6031 let should_add_view = this.update(cx, |this, _| {
6032 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
6033 state.active_view_id = update_active_view
6034 .view
6035 .as_ref()
6036 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
6037
6038 if state.active_view_id.is_some_and(|view_id| {
6039 !state.items_by_leader_view_id.contains_key(&view_id)
6040 }) {
6041 anyhow::Ok(true)
6042 } else {
6043 anyhow::Ok(false)
6044 }
6045 } else {
6046 anyhow::Ok(false)
6047 }
6048 })??;
6049
6050 if should_add_view && let Some(view) = update_active_view.view {
6051 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
6052 }
6053 }
6054 proto::update_followers::Variant::UpdateView(update_view) => {
6055 let variant = update_view.variant.context("missing update view variant")?;
6056 let id = update_view.id.context("missing update view id")?;
6057 let mut tasks = Vec::new();
6058 this.update_in(cx, |this, window, cx| {
6059 let project = this.project.clone();
6060 if let Some(state) = this.follower_states.get(&leader_id.into()) {
6061 let view_id = ViewId::from_proto(id.clone())?;
6062 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
6063 tasks.push(item.view.apply_update_proto(
6064 &project,
6065 variant.clone(),
6066 window,
6067 cx,
6068 ));
6069 }
6070 }
6071 anyhow::Ok(())
6072 })??;
6073 try_join_all(tasks).await.log_err();
6074 }
6075 }
6076 this.update_in(cx, |this, window, cx| {
6077 this.leader_updated(leader_id, window, cx)
6078 })?;
6079 Ok(())
6080 }
6081
6082 async fn add_view_from_leader(
6083 this: WeakEntity<Self>,
6084 leader_id: PeerId,
6085 view: &proto::View,
6086 cx: &mut AsyncWindowContext,
6087 ) -> Result<()> {
6088 let this = this.upgrade().context("workspace dropped")?;
6089
6090 let Some(id) = view.id.clone() else {
6091 anyhow::bail!("no id for view");
6092 };
6093 let id = ViewId::from_proto(id)?;
6094 let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
6095
6096 let pane = this.update(cx, |this, _cx| {
6097 let state = this
6098 .follower_states
6099 .get(&leader_id.into())
6100 .context("stopped following")?;
6101 anyhow::Ok(state.pane().clone())
6102 })?;
6103 let existing_item = pane.update_in(cx, |pane, window, cx| {
6104 let client = this.read(cx).client().clone();
6105 pane.items().find_map(|item| {
6106 let item = item.to_followable_item_handle(cx)?;
6107 if item.remote_id(&client, window, cx) == Some(id) {
6108 Some(item)
6109 } else {
6110 None
6111 }
6112 })
6113 })?;
6114 let item = if let Some(existing_item) = existing_item {
6115 existing_item
6116 } else {
6117 let variant = view.variant.clone();
6118 anyhow::ensure!(variant.is_some(), "missing view variant");
6119
6120 let task = cx.update(|window, cx| {
6121 FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx)
6122 })?;
6123
6124 let Some(task) = task else {
6125 anyhow::bail!(
6126 "failed to construct view from leader (maybe from a different version of zed?)"
6127 );
6128 };
6129
6130 let mut new_item = task.await?;
6131 pane.update_in(cx, |pane, window, cx| {
6132 let mut item_to_remove = None;
6133 for (ix, item) in pane.items().enumerate() {
6134 if let Some(item) = item.to_followable_item_handle(cx) {
6135 match new_item.dedup(item.as_ref(), window, cx) {
6136 Some(item::Dedup::KeepExisting) => {
6137 new_item =
6138 item.boxed_clone().to_followable_item_handle(cx).unwrap();
6139 break;
6140 }
6141 Some(item::Dedup::ReplaceExisting) => {
6142 item_to_remove = Some((ix, item.item_id()));
6143 break;
6144 }
6145 None => {}
6146 }
6147 }
6148 }
6149
6150 if let Some((ix, id)) = item_to_remove {
6151 pane.remove_item(id, false, false, window, cx);
6152 pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx);
6153 }
6154 })?;
6155
6156 new_item
6157 };
6158
6159 this.update_in(cx, |this, window, cx| {
6160 let state = this.follower_states.get_mut(&leader_id.into())?;
6161 item.set_leader_id(Some(leader_id.into()), window, cx);
6162 state.items_by_leader_view_id.insert(
6163 id,
6164 FollowerView {
6165 view: item,
6166 location: panel_id,
6167 },
6168 );
6169
6170 Some(())
6171 })
6172 .context("no follower state")?;
6173
6174 Ok(())
6175 }
6176
6177 fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
6178 let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else {
6179 return;
6180 };
6181
6182 if let Some(agent_location) = self.project.read(cx).agent_location() {
6183 let buffer_entity_id = agent_location.buffer.entity_id();
6184 let view_id = ViewId {
6185 creator: CollaboratorId::Agent,
6186 id: buffer_entity_id.as_u64(),
6187 };
6188 follower_state.active_view_id = Some(view_id);
6189
6190 let item = match follower_state.items_by_leader_view_id.entry(view_id) {
6191 hash_map::Entry::Occupied(entry) => Some(entry.into_mut()),
6192 hash_map::Entry::Vacant(entry) => {
6193 let existing_view =
6194 follower_state
6195 .center_pane
6196 .read(cx)
6197 .items()
6198 .find_map(|item| {
6199 let item = item.to_followable_item_handle(cx)?;
6200 if item.buffer_kind(cx) == ItemBufferKind::Singleton
6201 && item.project_item_model_ids(cx).as_slice()
6202 == [buffer_entity_id]
6203 {
6204 Some(item)
6205 } else {
6206 None
6207 }
6208 });
6209 let view = existing_view.or_else(|| {
6210 agent_location.buffer.upgrade().and_then(|buffer| {
6211 cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| {
6212 registry.build_item(buffer, self.project.clone(), None, window, cx)
6213 })?
6214 .to_followable_item_handle(cx)
6215 })
6216 });
6217
6218 view.map(|view| {
6219 entry.insert(FollowerView {
6220 view,
6221 location: None,
6222 })
6223 })
6224 }
6225 };
6226
6227 if let Some(item) = item {
6228 item.view
6229 .set_leader_id(Some(CollaboratorId::Agent), window, cx);
6230 item.view
6231 .update_agent_location(agent_location.position, window, cx);
6232 }
6233 } else {
6234 follower_state.active_view_id = None;
6235 }
6236
6237 self.leader_updated(CollaboratorId::Agent, window, cx);
6238 }
6239
6240 pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
6241 let mut is_project_item = true;
6242 let mut update = proto::UpdateActiveView::default();
6243 if window.is_window_active() {
6244 let (active_item, panel_id) = self.active_item_for_followers(window, cx);
6245
6246 if let Some(item) = active_item
6247 && item.item_focus_handle(cx).contains_focused(window, cx)
6248 {
6249 let leader_id = self
6250 .pane_for(&*item)
6251 .and_then(|pane| self.leader_for_pane(&pane));
6252 let leader_peer_id = match leader_id {
6253 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
6254 Some(CollaboratorId::Agent) | None => None,
6255 };
6256
6257 if let Some(item) = item.to_followable_item_handle(cx) {
6258 let id = item
6259 .remote_id(&self.app_state.client, window, cx)
6260 .map(|id| id.to_proto());
6261
6262 if let Some(id) = id
6263 && let Some(variant) = item.to_state_proto(window, cx)
6264 {
6265 let view = Some(proto::View {
6266 id,
6267 leader_id: leader_peer_id,
6268 variant: Some(variant),
6269 panel_id: panel_id.map(|id| id as i32),
6270 });
6271
6272 is_project_item = item.is_project_item(window, cx);
6273 update = proto::UpdateActiveView { view };
6274 };
6275 }
6276 }
6277 }
6278
6279 let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
6280 if active_view_id != self.last_active_view_id.as_ref() {
6281 self.last_active_view_id = active_view_id.cloned();
6282 self.update_followers(
6283 is_project_item,
6284 proto::update_followers::Variant::UpdateActiveView(update),
6285 window,
6286 cx,
6287 );
6288 }
6289 }
6290
6291 fn active_item_for_followers(
6292 &self,
6293 window: &mut Window,
6294 cx: &mut App,
6295 ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
6296 let mut active_item = None;
6297 let mut panel_id = None;
6298 for dock in self.all_docks() {
6299 if dock.focus_handle(cx).contains_focused(window, cx)
6300 && let Some(panel) = dock.read(cx).active_panel()
6301 && let Some(pane) = panel.pane(cx)
6302 && let Some(item) = pane.read(cx).active_item()
6303 {
6304 active_item = Some(item);
6305 panel_id = panel.remote_id();
6306 break;
6307 }
6308 }
6309
6310 if active_item.is_none() {
6311 active_item = self.active_pane().read(cx).active_item();
6312 }
6313 (active_item, panel_id)
6314 }
6315
6316 fn update_followers(
6317 &self,
6318 project_only: bool,
6319 update: proto::update_followers::Variant,
6320 _: &mut Window,
6321 cx: &mut App,
6322 ) -> Option<()> {
6323 // If this update only applies to for followers in the current project,
6324 // then skip it unless this project is shared. If it applies to all
6325 // followers, regardless of project, then set `project_id` to none,
6326 // indicating that it goes to all followers.
6327 let project_id = if project_only {
6328 Some(self.project.read(cx).remote_id()?)
6329 } else {
6330 None
6331 };
6332 self.app_state().workspace_store.update(cx, |store, cx| {
6333 store.update_followers(project_id, update, cx)
6334 })
6335 }
6336
6337 pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<CollaboratorId> {
6338 self.follower_states.iter().find_map(|(leader_id, state)| {
6339 if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
6340 Some(*leader_id)
6341 } else {
6342 None
6343 }
6344 })
6345 }
6346
6347 fn leader_updated(
6348 &mut self,
6349 leader_id: impl Into<CollaboratorId>,
6350 window: &mut Window,
6351 cx: &mut Context<Self>,
6352 ) -> Option<Box<dyn ItemHandle>> {
6353 cx.notify();
6354
6355 let leader_id = leader_id.into();
6356 let (panel_id, item) = match leader_id {
6357 CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?,
6358 CollaboratorId::Agent => (None, self.active_item_for_agent()?),
6359 };
6360
6361 let state = self.follower_states.get(&leader_id)?;
6362 let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
6363 let pane;
6364 if let Some(panel_id) = panel_id {
6365 pane = self
6366 .activate_panel_for_proto_id(panel_id, window, cx)?
6367 .pane(cx)?;
6368 let state = self.follower_states.get_mut(&leader_id)?;
6369 state.dock_pane = Some(pane.clone());
6370 } else {
6371 pane = state.center_pane.clone();
6372 let state = self.follower_states.get_mut(&leader_id)?;
6373 if let Some(dock_pane) = state.dock_pane.take() {
6374 transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx);
6375 }
6376 }
6377
6378 pane.update(cx, |pane, cx| {
6379 let focus_active_item = pane.has_focus(window, cx) || transfer_focus;
6380 if let Some(index) = pane.index_for_item(item.as_ref()) {
6381 pane.activate_item(index, false, false, window, cx);
6382 } else {
6383 pane.add_item(item.boxed_clone(), false, false, None, window, cx)
6384 }
6385
6386 if focus_active_item {
6387 pane.focus_active_item(window, cx)
6388 }
6389 });
6390
6391 Some(item)
6392 }
6393
6394 fn active_item_for_agent(&self) -> Option<Box<dyn ItemHandle>> {
6395 let state = self.follower_states.get(&CollaboratorId::Agent)?;
6396 let active_view_id = state.active_view_id?;
6397 Some(
6398 state
6399 .items_by_leader_view_id
6400 .get(&active_view_id)?
6401 .view
6402 .boxed_clone(),
6403 )
6404 }
6405
6406 fn active_item_for_peer(
6407 &self,
6408 peer_id: PeerId,
6409 window: &mut Window,
6410 cx: &mut Context<Self>,
6411 ) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
6412 let call = self.active_call()?;
6413 let participant = call.remote_participant_for_peer_id(peer_id, cx)?;
6414 let leader_in_this_app;
6415 let leader_in_this_project;
6416 match participant.location {
6417 ParticipantLocation::SharedProject { project_id } => {
6418 leader_in_this_app = true;
6419 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
6420 }
6421 ParticipantLocation::UnsharedProject => {
6422 leader_in_this_app = true;
6423 leader_in_this_project = false;
6424 }
6425 ParticipantLocation::External => {
6426 leader_in_this_app = false;
6427 leader_in_this_project = false;
6428 }
6429 };
6430 let state = self.follower_states.get(&peer_id.into())?;
6431 let mut item_to_activate = None;
6432 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
6433 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id)
6434 && (leader_in_this_project || !item.view.is_project_item(window, cx))
6435 {
6436 item_to_activate = Some((item.location, item.view.boxed_clone()));
6437 }
6438 } else if let Some(shared_screen) =
6439 self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx)
6440 {
6441 item_to_activate = Some((None, Box::new(shared_screen)));
6442 }
6443 item_to_activate
6444 }
6445
6446 fn shared_screen_for_peer(
6447 &self,
6448 peer_id: PeerId,
6449 pane: &Entity<Pane>,
6450 window: &mut Window,
6451 cx: &mut App,
6452 ) -> Option<Entity<SharedScreen>> {
6453 self.active_call()?
6454 .create_shared_screen(peer_id, pane, window, cx)
6455 }
6456
6457 pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
6458 if window.is_window_active() {
6459 self.update_active_view_for_followers(window, cx);
6460
6461 if let Some(database_id) = self.database_id {
6462 let db = WorkspaceDb::global(cx);
6463 cx.background_spawn(async move { db.update_timestamp(database_id).await })
6464 .detach();
6465 }
6466 } else {
6467 for pane in &self.panes {
6468 pane.update(cx, |pane, cx| {
6469 if let Some(item) = pane.active_item() {
6470 item.workspace_deactivated(window, cx);
6471 }
6472 for item in pane.items() {
6473 if matches!(
6474 item.workspace_settings(cx).autosave,
6475 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
6476 ) {
6477 Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
6478 .detach_and_log_err(cx);
6479 }
6480 }
6481 });
6482 }
6483 }
6484 }
6485
6486 pub fn active_call(&self) -> Option<&dyn AnyActiveCall> {
6487 self.active_call.as_ref().map(|(call, _)| &*call.0)
6488 }
6489
6490 pub fn active_global_call(&self) -> Option<GlobalAnyActiveCall> {
6491 self.active_call.as_ref().map(|(call, _)| call.clone())
6492 }
6493
6494 fn on_active_call_event(
6495 &mut self,
6496 event: &ActiveCallEvent,
6497 window: &mut Window,
6498 cx: &mut Context<Self>,
6499 ) {
6500 match event {
6501 ActiveCallEvent::ParticipantLocationChanged { participant_id }
6502 | ActiveCallEvent::RemoteVideoTracksChanged { participant_id } => {
6503 self.leader_updated(participant_id, window, cx);
6504 }
6505 }
6506 }
6507
6508 pub fn database_id(&self) -> Option<WorkspaceId> {
6509 self.database_id
6510 }
6511
6512 #[cfg(any(test, feature = "test-support"))]
6513 pub(crate) fn set_database_id(&mut self, id: WorkspaceId) {
6514 self.database_id = Some(id);
6515 }
6516
6517 pub fn session_id(&self) -> Option<String> {
6518 self.session_id.clone()
6519 }
6520
6521 fn save_window_bounds(&self, window: &mut Window, cx: &mut App) -> Task<()> {
6522 let Some(display) = window.display(cx) else {
6523 return Task::ready(());
6524 };
6525 let Ok(display_uuid) = display.uuid() else {
6526 return Task::ready(());
6527 };
6528
6529 let window_bounds = window.inner_window_bounds();
6530 let database_id = self.database_id;
6531 let has_paths = !self.root_paths(cx).is_empty();
6532 let db = WorkspaceDb::global(cx);
6533 let kvp = db::kvp::KeyValueStore::global(cx);
6534
6535 cx.background_executor().spawn(async move {
6536 if !has_paths {
6537 persistence::write_default_window_bounds(&kvp, window_bounds, display_uuid)
6538 .await
6539 .log_err();
6540 }
6541 if let Some(database_id) = database_id {
6542 db.set_window_open_status(
6543 database_id,
6544 SerializedWindowBounds(window_bounds),
6545 display_uuid,
6546 )
6547 .await
6548 .log_err();
6549 } else {
6550 persistence::write_default_window_bounds(&kvp, window_bounds, display_uuid)
6551 .await
6552 .log_err();
6553 }
6554 })
6555 }
6556
6557 /// Bypass the 200ms serialization throttle and write workspace state to
6558 /// the DB immediately. Returns a task the caller can await to ensure the
6559 /// write completes. Used by the quit handler so the most recent state
6560 /// isn't lost to a pending throttle timer when the process exits.
6561 pub fn flush_serialization(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
6562 self._schedule_serialize_workspace.take();
6563 self._serialize_workspace_task.take();
6564 self.bounds_save_task_queued.take();
6565
6566 let bounds_task = self.save_window_bounds(window, cx);
6567 let serialize_task = self.serialize_workspace_internal(window, cx);
6568 cx.spawn(async move |_| {
6569 bounds_task.await;
6570 serialize_task.await;
6571 })
6572 }
6573
6574 pub fn root_paths(&self, cx: &App) -> Vec<Arc<Path>> {
6575 let project = self.project().read(cx);
6576 project
6577 .visible_worktrees(cx)
6578 .map(|worktree| worktree.read(cx).abs_path())
6579 .collect::<Vec<_>>()
6580 }
6581
6582 fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
6583 match member {
6584 Member::Axis(PaneAxis { members, .. }) => {
6585 for child in members.iter() {
6586 self.remove_panes(child.clone(), window, cx)
6587 }
6588 }
6589 Member::Pane(pane) => {
6590 self.force_remove_pane(&pane, &None, window, cx);
6591 }
6592 }
6593 }
6594
6595 fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
6596 self.session_id.take();
6597 self.serialize_workspace_internal(window, cx)
6598 }
6599
6600 fn force_remove_pane(
6601 &mut self,
6602 pane: &Entity<Pane>,
6603 focus_on: &Option<Entity<Pane>>,
6604 window: &mut Window,
6605 cx: &mut Context<Workspace>,
6606 ) {
6607 self.panes.retain(|p| p != pane);
6608 if let Some(focus_on) = focus_on {
6609 focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
6610 } else if self.active_pane() == pane {
6611 let fallback_pane = self.panes.last().unwrap().clone();
6612 if self.has_active_modal(window, cx) {
6613 self.set_active_pane(&fallback_pane, window, cx);
6614 } else {
6615 fallback_pane.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
6616 }
6617 }
6618 if self.last_active_center_pane == Some(pane.downgrade()) {
6619 self.last_active_center_pane = None;
6620 }
6621 cx.notify();
6622 }
6623
6624 fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
6625 if self._schedule_serialize_workspace.is_none() {
6626 self._schedule_serialize_workspace =
6627 Some(cx.spawn_in(window, async move |this, cx| {
6628 cx.background_executor()
6629 .timer(SERIALIZATION_THROTTLE_TIME)
6630 .await;
6631 this.update_in(cx, |this, window, cx| {
6632 this._serialize_workspace_task =
6633 Some(this.serialize_workspace_internal(window, cx));
6634 this._schedule_serialize_workspace.take();
6635 })
6636 .log_err();
6637 }));
6638 }
6639 }
6640
6641 fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
6642 let Some(database_id) = self.database_id() else {
6643 return Task::ready(());
6644 };
6645
6646 fn serialize_pane_handle(
6647 pane_handle: &Entity<Pane>,
6648 window: &mut Window,
6649 cx: &mut App,
6650 ) -> SerializedPane {
6651 let (items, active, pinned_count) = {
6652 let pane = pane_handle.read(cx);
6653 let active_item_id = pane.active_item().map(|item| item.item_id());
6654 (
6655 pane.items()
6656 .filter_map(|handle| {
6657 let handle = handle.to_serializable_item_handle(cx)?;
6658
6659 Some(SerializedItem {
6660 kind: Arc::from(handle.serialized_item_kind()),
6661 item_id: handle.item_id().as_u64(),
6662 active: Some(handle.item_id()) == active_item_id,
6663 preview: pane.is_active_preview_item(handle.item_id()),
6664 })
6665 })
6666 .collect::<Vec<_>>(),
6667 pane.has_focus(window, cx),
6668 pane.pinned_count(),
6669 )
6670 };
6671
6672 SerializedPane::new(items, active, pinned_count)
6673 }
6674
6675 fn build_serialized_pane_group(
6676 pane_group: &Member,
6677 window: &mut Window,
6678 cx: &mut App,
6679 ) -> SerializedPaneGroup {
6680 match pane_group {
6681 Member::Axis(PaneAxis {
6682 axis,
6683 members,
6684 flexes,
6685 bounding_boxes: _,
6686 }) => SerializedPaneGroup::Group {
6687 axis: SerializedAxis(*axis),
6688 children: members
6689 .iter()
6690 .map(|member| build_serialized_pane_group(member, window, cx))
6691 .collect::<Vec<_>>(),
6692 flexes: Some(flexes.lock().clone()),
6693 },
6694 Member::Pane(pane_handle) => {
6695 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
6696 }
6697 }
6698 }
6699
6700 fn build_serialized_docks(
6701 this: &Workspace,
6702 window: &mut Window,
6703 cx: &mut App,
6704 ) -> DockStructure {
6705 this.capture_dock_state(window, cx)
6706 }
6707
6708 match self.workspace_location(cx) {
6709 WorkspaceLocation::Location(location, paths) => {
6710 let bookmarks = self.project.update(cx, |project, cx| {
6711 project
6712 .bookmark_store()
6713 .read(cx)
6714 .all_serialized_bookmarks(cx)
6715 });
6716
6717 let breakpoints = self.project.update(cx, |project, cx| {
6718 project
6719 .breakpoint_store()
6720 .read(cx)
6721 .all_source_breakpoints(cx)
6722 });
6723 let user_toolchains = self
6724 .project
6725 .read(cx)
6726 .user_toolchains(cx)
6727 .unwrap_or_default();
6728
6729 let center_group = build_serialized_pane_group(&self.center.root, window, cx);
6730 let docks = build_serialized_docks(self, window, cx);
6731 let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
6732
6733 let serialized_workspace = SerializedWorkspace {
6734 id: database_id,
6735 location,
6736 paths,
6737 center_group,
6738 window_bounds,
6739 display: Default::default(),
6740 docks,
6741 centered_layout: self.centered_layout,
6742 session_id: self.session_id.clone(),
6743 bookmarks,
6744 breakpoints,
6745 window_id: Some(window.window_handle().window_id().as_u64()),
6746 user_toolchains,
6747 };
6748
6749 let db = WorkspaceDb::global(cx);
6750 window.spawn(cx, async move |_| {
6751 db.save_workspace(serialized_workspace).await;
6752 })
6753 }
6754 WorkspaceLocation::DetachFromSession => {
6755 let window_bounds = SerializedWindowBounds(window.window_bounds());
6756 let display = window.display(cx).and_then(|d| d.uuid().ok());
6757 // Save dock state for empty local workspaces
6758 let docks = build_serialized_docks(self, window, cx);
6759 let db = WorkspaceDb::global(cx);
6760 let kvp = db::kvp::KeyValueStore::global(cx);
6761 window.spawn(cx, async move |_| {
6762 db.set_window_open_status(
6763 database_id,
6764 window_bounds,
6765 display.unwrap_or_default(),
6766 )
6767 .await
6768 .log_err();
6769 db.set_session_id(database_id, None).await.log_err();
6770 persistence::write_default_dock_state(&kvp, docks)
6771 .await
6772 .log_err();
6773 })
6774 }
6775 WorkspaceLocation::None => {
6776 // Save dock state for empty non-local workspaces
6777 let docks = build_serialized_docks(self, window, cx);
6778 let kvp = db::kvp::KeyValueStore::global(cx);
6779 window.spawn(cx, async move |_| {
6780 persistence::write_default_dock_state(&kvp, docks)
6781 .await
6782 .log_err();
6783 })
6784 }
6785 }
6786 }
6787
6788 fn has_any_items_open(&self, cx: &App) -> bool {
6789 self.panes.iter().any(|pane| pane.read(cx).items_len() > 0)
6790 }
6791
6792 fn workspace_location(&self, cx: &App) -> WorkspaceLocation {
6793 let paths = PathList::new(&self.root_paths(cx));
6794 if let Some(connection) = self.project.read(cx).remote_connection_options(cx) {
6795 WorkspaceLocation::Location(SerializedWorkspaceLocation::Remote(connection), paths)
6796 } else if self.project.read(cx).is_local() {
6797 if !paths.is_empty() || self.has_any_items_open(cx) {
6798 WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths)
6799 } else {
6800 WorkspaceLocation::DetachFromSession
6801 }
6802 } else {
6803 WorkspaceLocation::None
6804 }
6805 }
6806
6807 fn update_history(&self, cx: &mut App) {
6808 let Some(id) = self.database_id() else {
6809 return;
6810 };
6811 if !self.project.read(cx).is_local() {
6812 return;
6813 }
6814 if let Some(manager) = HistoryManager::global(cx) {
6815 let paths = PathList::new(&self.root_paths(cx));
6816 manager.update(cx, |this, cx| {
6817 this.update_history(id, HistoryManagerEntry::new(id, &paths), cx);
6818 });
6819 }
6820 }
6821
6822 async fn serialize_items(
6823 this: &WeakEntity<Self>,
6824 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
6825 cx: &mut AsyncWindowContext,
6826 ) -> Result<()> {
6827 const CHUNK_SIZE: usize = 200;
6828
6829 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
6830
6831 while let Some(items_received) = serializable_items.next().await {
6832 let unique_items =
6833 items_received
6834 .into_iter()
6835 .fold(HashMap::default(), |mut acc, item| {
6836 acc.entry(item.item_id()).or_insert(item);
6837 acc
6838 });
6839
6840 // We use into_iter() here so that the references to the items are moved into
6841 // the tasks and not kept alive while we're sleeping.
6842 for (_, item) in unique_items.into_iter() {
6843 if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
6844 item.serialize(workspace, false, window, cx)
6845 }) {
6846 cx.background_spawn(async move { task.await.log_err() })
6847 .detach();
6848 }
6849 }
6850
6851 cx.background_executor()
6852 .timer(SERIALIZATION_THROTTLE_TIME)
6853 .await;
6854 }
6855
6856 Ok(())
6857 }
6858
6859 pub(crate) fn enqueue_item_serialization(
6860 &mut self,
6861 item: Box<dyn SerializableItemHandle>,
6862 ) -> Result<()> {
6863 self.serializable_items_tx
6864 .unbounded_send(item)
6865 .map_err(|err| anyhow!("failed to send serializable item over channel: {err}"))
6866 }
6867
6868 pub(crate) fn load_workspace(
6869 serialized_workspace: SerializedWorkspace,
6870 paths_to_open: Vec<Option<ProjectPath>>,
6871 window: &mut Window,
6872 cx: &mut Context<Workspace>,
6873 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
6874 cx.spawn_in(window, async move |workspace, cx| {
6875 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
6876
6877 let mut center_group = None;
6878 let mut center_items = None;
6879
6880 // Traverse the splits tree and add to things
6881 if let Some((group, active_pane, items)) = serialized_workspace
6882 .center_group
6883 .deserialize(&project, serialized_workspace.id, workspace.clone(), cx)
6884 .await
6885 {
6886 center_items = Some(items);
6887 center_group = Some((group, active_pane))
6888 }
6889
6890 let mut items_by_project_path = HashMap::default();
6891 let mut item_ids_by_kind = HashMap::default();
6892 let mut all_deserialized_items = Vec::default();
6893 cx.update(|_, cx| {
6894 for item in center_items.unwrap_or_default().into_iter().flatten() {
6895 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
6896 item_ids_by_kind
6897 .entry(serializable_item_handle.serialized_item_kind())
6898 .or_insert(Vec::new())
6899 .push(item.item_id().as_u64() as ItemId);
6900 }
6901
6902 if let Some(project_path) = item.project_path(cx) {
6903 items_by_project_path.insert(project_path, item.clone());
6904 }
6905 all_deserialized_items.push(item);
6906 }
6907 })?;
6908
6909 let opened_items = paths_to_open
6910 .into_iter()
6911 .map(|path_to_open| {
6912 path_to_open
6913 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
6914 })
6915 .collect::<Vec<_>>();
6916
6917 // Remove old panes from workspace panes list
6918 workspace.update_in(cx, |workspace, window, cx| {
6919 if let Some((center_group, active_pane)) = center_group {
6920 workspace.remove_panes(workspace.center.root.clone(), window, cx);
6921
6922 // Swap workspace center group
6923 workspace.center = PaneGroup::with_root(center_group);
6924 workspace.center.set_is_center(true);
6925 workspace.center.mark_positions(cx);
6926
6927 if let Some(active_pane) = active_pane {
6928 workspace.set_active_pane(&active_pane, window, cx);
6929 cx.focus_self(window);
6930 } else {
6931 workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
6932 }
6933 }
6934
6935 let docks = serialized_workspace.docks;
6936
6937 for (dock, serialized_dock) in [
6938 (&mut workspace.right_dock, docks.right),
6939 (&mut workspace.left_dock, docks.left),
6940 (&mut workspace.bottom_dock, docks.bottom),
6941 ]
6942 .iter_mut()
6943 {
6944 dock.update(cx, |dock, cx| {
6945 dock.serialized_dock = Some(serialized_dock.clone());
6946 dock.restore_state(window, cx);
6947 });
6948 }
6949
6950 cx.notify();
6951 })?;
6952
6953 project
6954 .update(cx, |project, cx| {
6955 project.bookmark_store().update(cx, |bookmark_store, cx| {
6956 bookmark_store.load_serialized_bookmarks(serialized_workspace.bookmarks, cx)
6957 })
6958 })
6959 .await
6960 .log_err();
6961
6962 let _ = project
6963 .update(cx, |project, cx| {
6964 project
6965 .breakpoint_store()
6966 .update(cx, |breakpoint_store, cx| {
6967 breakpoint_store
6968 .with_serialized_breakpoints(serialized_workspace.breakpoints, cx)
6969 })
6970 })
6971 .await;
6972
6973 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
6974 // after loading the items, we might have different items and in order to avoid
6975 // the database filling up, we delete items that haven't been loaded now.
6976 //
6977 // The items that have been loaded, have been saved after they've been added to the workspace.
6978 let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
6979 item_ids_by_kind
6980 .into_iter()
6981 .map(|(item_kind, loaded_items)| {
6982 SerializableItemRegistry::cleanup(
6983 item_kind,
6984 serialized_workspace.id,
6985 loaded_items,
6986 window,
6987 cx,
6988 )
6989 .log_err()
6990 })
6991 .collect::<Vec<_>>()
6992 })?;
6993
6994 futures::future::join_all(clean_up_tasks).await;
6995
6996 workspace
6997 .update_in(cx, |workspace, window, cx| {
6998 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
6999 workspace.serialize_workspace_internal(window, cx).detach();
7000
7001 // Ensure that we mark the window as edited if we did load dirty items
7002 workspace.update_window_edited(window, cx);
7003 })
7004 .ok();
7005
7006 Ok(opened_items)
7007 })
7008 }
7009
7010 pub fn key_context(&self, cx: &App) -> KeyContext {
7011 let mut context = KeyContext::new_with_defaults();
7012 context.add("Workspace");
7013 context.set("keyboard_layout", cx.keyboard_layout().name().to_string());
7014 if let Some(status) = self
7015 .debugger_provider
7016 .as_ref()
7017 .and_then(|provider| provider.active_thread_state(cx))
7018 {
7019 match status {
7020 ThreadStatus::Running | ThreadStatus::Stepping => {
7021 context.add("debugger_running");
7022 }
7023 ThreadStatus::Stopped => context.add("debugger_stopped"),
7024 ThreadStatus::Exited | ThreadStatus::Ended => {}
7025 }
7026 }
7027
7028 if self.left_dock.read(cx).is_open() {
7029 if let Some(active_panel) = self.left_dock.read(cx).active_panel() {
7030 context.set("left_dock", active_panel.panel_key());
7031 }
7032 }
7033
7034 if self.right_dock.read(cx).is_open() {
7035 if let Some(active_panel) = self.right_dock.read(cx).active_panel() {
7036 context.set("right_dock", active_panel.panel_key());
7037 }
7038 }
7039
7040 if self.bottom_dock.read(cx).is_open() {
7041 if let Some(active_panel) = self.bottom_dock.read(cx).active_panel() {
7042 context.set("bottom_dock", active_panel.panel_key());
7043 }
7044 }
7045
7046 context
7047 }
7048
7049 /// Multiworkspace uses this to add workspace action handling to itself
7050 pub fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
7051 self.add_workspace_actions_listeners(div, window, cx)
7052 .on_action(cx.listener(
7053 |_workspace, action_sequence: &settings::ActionSequence, window, cx| {
7054 for action in &action_sequence.0 {
7055 window.dispatch_action(action.boxed_clone(), cx);
7056 }
7057 },
7058 ))
7059 .on_action(cx.listener(Self::close_inactive_items_and_panes))
7060 .on_action(cx.listener(Self::close_all_items_and_panes))
7061 .on_action(cx.listener(Self::close_item_in_all_panes))
7062 .on_action(cx.listener(Self::save_all))
7063 .on_action(cx.listener(Self::send_keystrokes))
7064 .on_action(cx.listener(Self::add_folder_to_project))
7065 .on_action(cx.listener(Self::follow_next_collaborator))
7066 .on_action(cx.listener(Self::activate_pane_at_index))
7067 .on_action(cx.listener(Self::move_item_to_pane_at_index))
7068 .on_action(cx.listener(Self::move_focused_panel_to_next_position))
7069 .on_action(cx.listener(Self::toggle_edit_predictions_all_files))
7070 .on_action(cx.listener(Self::toggle_theme_mode))
7071 .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
7072 let pane = workspace.active_pane().clone();
7073 workspace.unfollow_in_pane(&pane, window, cx);
7074 }))
7075 .on_action(cx.listener(|workspace, action: &Save, window, cx| {
7076 workspace
7077 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
7078 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
7079 }))
7080 .on_action(cx.listener(|workspace, _: &FormatAndSave, window, cx| {
7081 workspace
7082 .save_active_item(SaveIntent::FormatAndSave, window, cx)
7083 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
7084 }))
7085 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
7086 workspace
7087 .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
7088 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
7089 }))
7090 .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
7091 workspace
7092 .save_active_item(SaveIntent::SaveAs, window, cx)
7093 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
7094 }))
7095 .on_action(
7096 cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
7097 workspace.activate_previous_pane(window, cx)
7098 }),
7099 )
7100 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
7101 workspace.activate_next_pane(window, cx)
7102 }))
7103 .on_action(cx.listener(|workspace, _: &ActivateLastPane, window, cx| {
7104 workspace.activate_last_pane(window, cx)
7105 }))
7106 .on_action(
7107 cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
7108 workspace.activate_next_window(cx)
7109 }),
7110 )
7111 .on_action(
7112 cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
7113 workspace.activate_previous_window(cx)
7114 }),
7115 )
7116 .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
7117 workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
7118 }))
7119 .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
7120 workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
7121 }))
7122 .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
7123 workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
7124 }))
7125 .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
7126 workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
7127 }))
7128 .on_action(cx.listener(
7129 |workspace, action: &MoveItemToPaneInDirection, window, cx| {
7130 workspace.move_item_to_pane_in_direction(action, window, cx)
7131 },
7132 ))
7133 .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
7134 workspace.swap_pane_in_direction(SplitDirection::Left, cx)
7135 }))
7136 .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
7137 workspace.swap_pane_in_direction(SplitDirection::Right, cx)
7138 }))
7139 .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
7140 workspace.swap_pane_in_direction(SplitDirection::Up, cx)
7141 }))
7142 .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
7143 workspace.swap_pane_in_direction(SplitDirection::Down, cx)
7144 }))
7145 .on_action(cx.listener(|workspace, _: &SwapPaneAdjacent, window, cx| {
7146 const DIRECTION_PRIORITY: [SplitDirection; 4] = [
7147 SplitDirection::Down,
7148 SplitDirection::Up,
7149 SplitDirection::Right,
7150 SplitDirection::Left,
7151 ];
7152 for dir in DIRECTION_PRIORITY {
7153 if workspace.find_pane_in_direction(dir, cx).is_some() {
7154 workspace.swap_pane_in_direction(dir, cx);
7155 workspace.activate_pane_in_direction(dir.opposite(), window, cx);
7156 break;
7157 }
7158 }
7159 }))
7160 .on_action(cx.listener(|workspace, _: &MovePaneLeft, _, cx| {
7161 workspace.move_pane_to_border(SplitDirection::Left, cx)
7162 }))
7163 .on_action(cx.listener(|workspace, _: &MovePaneRight, _, cx| {
7164 workspace.move_pane_to_border(SplitDirection::Right, cx)
7165 }))
7166 .on_action(cx.listener(|workspace, _: &MovePaneUp, _, cx| {
7167 workspace.move_pane_to_border(SplitDirection::Up, cx)
7168 }))
7169 .on_action(cx.listener(|workspace, _: &MovePaneDown, _, cx| {
7170 workspace.move_pane_to_border(SplitDirection::Down, cx)
7171 }))
7172 .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
7173 this.toggle_dock(DockPosition::Left, window, cx);
7174 }))
7175 .on_action(cx.listener(
7176 |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
7177 workspace.toggle_dock(DockPosition::Right, window, cx);
7178 },
7179 ))
7180 .on_action(cx.listener(
7181 |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
7182 workspace.toggle_dock(DockPosition::Bottom, window, cx);
7183 },
7184 ))
7185 .on_action(cx.listener(
7186 |workspace: &mut Workspace, _: &CloseActiveDock, window, cx| {
7187 if !workspace.close_active_dock(window, cx) {
7188 cx.propagate();
7189 }
7190 },
7191 ))
7192 .on_action(
7193 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
7194 workspace.close_all_docks(window, cx);
7195 }),
7196 )
7197 .on_action(cx.listener(Self::toggle_all_docks))
7198 .on_action(cx.listener(
7199 |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
7200 workspace.clear_all_notifications(cx);
7201 },
7202 ))
7203 .on_action(cx.listener(
7204 |workspace: &mut Workspace, _: &ClearNavigationHistory, window, cx| {
7205 workspace.clear_navigation_history(window, cx);
7206 },
7207 ))
7208 .on_action(cx.listener(
7209 |workspace: &mut Workspace, _: &SuppressNotification, _, cx| {
7210 if let Some((notification_id, _)) = workspace.notifications.pop() {
7211 workspace.suppress_notification(¬ification_id, cx);
7212 }
7213 },
7214 ))
7215 .on_action(cx.listener(
7216 |workspace: &mut Workspace, _: &ToggleWorktreeSecurity, window, cx| {
7217 workspace.show_worktree_trust_security_modal(true, window, cx);
7218 },
7219 ))
7220 .on_action(
7221 cx.listener(|_: &mut Workspace, _: &ClearTrustedWorktrees, _, cx| {
7222 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
7223 trusted_worktrees.update(cx, |trusted_worktrees, _| {
7224 trusted_worktrees.clear_trusted_paths()
7225 });
7226 let db = WorkspaceDb::global(cx);
7227 cx.spawn(async move |_, cx| {
7228 if db.clear_trusted_worktrees().await.log_err().is_some() {
7229 cx.update(|cx| reload(cx));
7230 }
7231 })
7232 .detach();
7233 }
7234 }),
7235 )
7236 .on_action(cx.listener(
7237 |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
7238 workspace.reopen_closed_item(window, cx).detach();
7239 },
7240 ))
7241 .on_action(cx.listener(
7242 |workspace: &mut Workspace, _: &ResetActiveDockSize, window, cx| {
7243 for dock in workspace.all_docks() {
7244 if dock.focus_handle(cx).contains_focused(window, cx) {
7245 let panel = dock.read(cx).active_panel().cloned();
7246 if let Some(panel) = panel {
7247 dock.update(cx, |dock, cx| {
7248 dock.set_panel_size_state(
7249 panel.as_ref(),
7250 dock::PanelSizeState::default(),
7251 cx,
7252 );
7253 });
7254 }
7255 return;
7256 }
7257 }
7258 },
7259 ))
7260 .on_action(cx.listener(
7261 |workspace: &mut Workspace, _: &ResetOpenDocksSize, _window, cx| {
7262 for dock in workspace.all_docks() {
7263 let panel = dock.read(cx).visible_panel().cloned();
7264 if let Some(panel) = panel {
7265 dock.update(cx, |dock, cx| {
7266 dock.set_panel_size_state(
7267 panel.as_ref(),
7268 dock::PanelSizeState::default(),
7269 cx,
7270 );
7271 });
7272 }
7273 }
7274 },
7275 ))
7276 .on_action(cx.listener(
7277 |workspace: &mut Workspace, act: &IncreaseActiveDockSize, window, cx| {
7278 adjust_active_dock_size_by_px(
7279 px_with_ui_font_fallback(act.px, cx),
7280 workspace,
7281 window,
7282 cx,
7283 );
7284 },
7285 ))
7286 .on_action(cx.listener(
7287 |workspace: &mut Workspace, act: &DecreaseActiveDockSize, window, cx| {
7288 adjust_active_dock_size_by_px(
7289 px_with_ui_font_fallback(act.px, cx) * -1.,
7290 workspace,
7291 window,
7292 cx,
7293 );
7294 },
7295 ))
7296 .on_action(cx.listener(
7297 |workspace: &mut Workspace, act: &IncreaseOpenDocksSize, window, cx| {
7298 adjust_open_docks_size_by_px(
7299 px_with_ui_font_fallback(act.px, cx),
7300 workspace,
7301 window,
7302 cx,
7303 );
7304 },
7305 ))
7306 .on_action(cx.listener(
7307 |workspace: &mut Workspace, act: &DecreaseOpenDocksSize, window, cx| {
7308 adjust_open_docks_size_by_px(
7309 px_with_ui_font_fallback(act.px, cx) * -1.,
7310 workspace,
7311 window,
7312 cx,
7313 );
7314 },
7315 ))
7316 .on_action(cx.listener(Workspace::toggle_centered_layout))
7317 .on_action(cx.listener(
7318 |workspace: &mut Workspace, action: &pane::ActivateNextItem, window, cx| {
7319 if let Some(active_dock) = workspace.active_dock(window, cx) {
7320 let dock = active_dock.read(cx);
7321 if let Some(active_panel) = dock.active_panel() {
7322 if active_panel.pane(cx).is_none() {
7323 let mut recent_pane: Option<Entity<Pane>> = None;
7324 let mut recent_timestamp = 0;
7325 for pane_handle in workspace.panes() {
7326 let pane = pane_handle.read(cx);
7327 for entry in pane.activation_history() {
7328 if entry.timestamp > recent_timestamp {
7329 recent_timestamp = entry.timestamp;
7330 recent_pane = Some(pane_handle.clone());
7331 }
7332 }
7333 }
7334
7335 if let Some(pane) = recent_pane {
7336 let wrap_around = action.wrap_around;
7337 pane.update(cx, |pane, cx| {
7338 let current_index = pane.active_item_index();
7339 let items_len = pane.items_len();
7340 if items_len > 0 {
7341 let next_index = if current_index + 1 < items_len {
7342 current_index + 1
7343 } else if wrap_around {
7344 0
7345 } else {
7346 return;
7347 };
7348 pane.activate_item(
7349 next_index, false, false, window, cx,
7350 );
7351 }
7352 });
7353 return;
7354 }
7355 }
7356 }
7357 }
7358 cx.propagate();
7359 },
7360 ))
7361 .on_action(cx.listener(
7362 |workspace: &mut Workspace, action: &pane::ActivatePreviousItem, window, cx| {
7363 if let Some(active_dock) = workspace.active_dock(window, cx) {
7364 let dock = active_dock.read(cx);
7365 if let Some(active_panel) = dock.active_panel() {
7366 if active_panel.pane(cx).is_none() {
7367 let mut recent_pane: Option<Entity<Pane>> = None;
7368 let mut recent_timestamp = 0;
7369 for pane_handle in workspace.panes() {
7370 let pane = pane_handle.read(cx);
7371 for entry in pane.activation_history() {
7372 if entry.timestamp > recent_timestamp {
7373 recent_timestamp = entry.timestamp;
7374 recent_pane = Some(pane_handle.clone());
7375 }
7376 }
7377 }
7378
7379 if let Some(pane) = recent_pane {
7380 let wrap_around = action.wrap_around;
7381 pane.update(cx, |pane, cx| {
7382 let current_index = pane.active_item_index();
7383 let items_len = pane.items_len();
7384 if items_len > 0 {
7385 let prev_index = if current_index > 0 {
7386 current_index - 1
7387 } else if wrap_around {
7388 items_len.saturating_sub(1)
7389 } else {
7390 return;
7391 };
7392 pane.activate_item(
7393 prev_index, false, false, window, cx,
7394 );
7395 }
7396 });
7397 return;
7398 }
7399 }
7400 }
7401 }
7402 cx.propagate();
7403 },
7404 ))
7405 .on_action(cx.listener(
7406 |workspace: &mut Workspace, action: &pane::CloseActiveItem, window, cx| {
7407 if let Some(active_dock) = workspace.active_dock(window, cx) {
7408 let dock = active_dock.read(cx);
7409 if let Some(active_panel) = dock.active_panel() {
7410 if active_panel.pane(cx).is_none() {
7411 let active_pane = workspace.active_pane().clone();
7412 active_pane.update(cx, |pane, cx| {
7413 pane.close_active_item(action, window, cx)
7414 .detach_and_log_err(cx);
7415 });
7416 return;
7417 }
7418 }
7419 }
7420 cx.propagate();
7421 },
7422 ))
7423 .on_action(
7424 cx.listener(|workspace, _: &ToggleReadOnlyFile, window, cx| {
7425 let pane = workspace.active_pane().clone();
7426 if let Some(item) = pane.read(cx).active_item() {
7427 item.toggle_read_only(window, cx);
7428 }
7429 }),
7430 )
7431 .on_action(cx.listener(|workspace, _: &FocusCenterPane, window, cx| {
7432 workspace.focus_center_pane(window, cx);
7433 }))
7434 .on_action(cx.listener(Workspace::clear_bookmarks))
7435 .on_action(cx.listener(Workspace::cancel))
7436 }
7437
7438 #[cfg(any(test, feature = "test-support"))]
7439 pub fn set_random_database_id(&mut self) {
7440 self.database_id = Some(WorkspaceId(Uuid::new_v4().as_u64_pair().0 as i64));
7441 }
7442
7443 #[cfg(any(test, feature = "test-support"))]
7444 pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
7445 use node_runtime::NodeRuntime;
7446 use session::Session;
7447
7448 let client = project.read(cx).client();
7449 let user_store = project.read(cx).user_store();
7450 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
7451 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
7452 window.activate_window();
7453 let app_state = Arc::new(AppState {
7454 languages: project.read(cx).languages().clone(),
7455 workspace_store,
7456 client,
7457 user_store,
7458 fs: project.read(cx).fs().clone(),
7459 build_window_options: |_, _| Default::default(),
7460 node_runtime: NodeRuntime::unavailable(),
7461 session,
7462 });
7463 let workspace = Self::new(Default::default(), project, app_state, window, cx);
7464 workspace
7465 .active_pane
7466 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
7467 workspace
7468 }
7469
7470 pub fn register_action<A: Action>(
7471 &mut self,
7472 callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
7473 ) -> &mut Self {
7474 let callback = Arc::new(callback);
7475
7476 self.workspace_actions.push(Box::new(move |div, _, _, cx| {
7477 let callback = callback.clone();
7478 div.on_action(cx.listener(move |workspace, event, window, cx| {
7479 (callback)(workspace, event, window, cx)
7480 }))
7481 }));
7482 self
7483 }
7484 pub fn register_action_renderer(
7485 &mut self,
7486 callback: impl Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div + 'static,
7487 ) -> &mut Self {
7488 self.workspace_actions.push(Box::new(callback));
7489 self
7490 }
7491
7492 fn add_workspace_actions_listeners(
7493 &self,
7494 mut div: Div,
7495 window: &mut Window,
7496 cx: &mut Context<Self>,
7497 ) -> Div {
7498 for action in self.workspace_actions.iter() {
7499 div = (action)(div, self, window, cx)
7500 }
7501 div
7502 }
7503
7504 pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
7505 self.modal_layer.read(cx).has_active_modal()
7506 }
7507
7508 pub fn is_active_modal_command_palette(&self, cx: &mut App) -> bool {
7509 self.modal_layer
7510 .read(cx)
7511 .is_active_modal_command_palette(cx)
7512 }
7513
7514 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
7515 self.modal_layer.read(cx).active_modal()
7516 }
7517
7518 /// Toggles a modal of type `V`. If a modal of the same type is currently active,
7519 /// it will be hidden. If a different modal is active, it will be replaced with the new one.
7520 /// If no modal is active, the new modal will be shown.
7521 ///
7522 /// If closing the current modal fails (e.g., due to `on_before_dismiss` returning
7523 /// `DismissDecision::Dismiss(false)` or `DismissDecision::Pending`), the new modal
7524 /// will not be shown.
7525 pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
7526 where
7527 B: FnOnce(&mut Window, &mut Context<V>) -> V,
7528 {
7529 self.modal_layer.update(cx, |modal_layer, cx| {
7530 modal_layer.toggle_modal(window, cx, build)
7531 })
7532 }
7533
7534 pub fn hide_modal(&mut self, window: &mut Window, cx: &mut App) -> bool {
7535 self.modal_layer
7536 .update(cx, |modal_layer, cx| modal_layer.hide_modal(window, cx))
7537 }
7538
7539 pub fn toggle_status_toast<V: ToastView>(&mut self, entity: Entity<V>, cx: &mut App) {
7540 self.toast_layer
7541 .update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity))
7542 }
7543
7544 pub fn toggle_centered_layout(
7545 &mut self,
7546 _: &ToggleCenteredLayout,
7547 _: &mut Window,
7548 cx: &mut Context<Self>,
7549 ) {
7550 self.centered_layout = !self.centered_layout;
7551 if let Some(database_id) = self.database_id() {
7552 let db = WorkspaceDb::global(cx);
7553 let centered_layout = self.centered_layout;
7554 cx.background_spawn(async move {
7555 db.set_centered_layout(database_id, centered_layout).await
7556 })
7557 .detach_and_log_err(cx);
7558 }
7559 cx.notify();
7560 }
7561
7562 pub fn clear_bookmarks(&mut self, _: &ClearBookmarks, _: &mut Window, cx: &mut Context<Self>) {
7563 self.project()
7564 .read(cx)
7565 .bookmark_store()
7566 .update(cx, |bookmark_store, cx| {
7567 bookmark_store.clear_bookmarks(cx);
7568 });
7569 }
7570
7571 fn adjust_padding(padding: Option<f32>) -> f32 {
7572 padding
7573 .unwrap_or(CenteredPaddingSettings::default().0)
7574 .clamp(
7575 CenteredPaddingSettings::MIN_PADDING,
7576 CenteredPaddingSettings::MAX_PADDING,
7577 )
7578 }
7579
7580 fn render_dock(
7581 &self,
7582 position: DockPosition,
7583 dock: &Entity<Dock>,
7584 window: &mut Window,
7585 cx: &mut App,
7586 ) -> Option<Div> {
7587 if self.zoomed_position == Some(position) {
7588 return None;
7589 }
7590
7591 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
7592 let pane = panel.pane(cx)?;
7593 let follower_states = &self.follower_states;
7594 leader_border_for_pane(follower_states, &pane, window, cx)
7595 });
7596
7597 let mut container = div()
7598 .flex()
7599 .overflow_hidden()
7600 .flex_none()
7601 .child(dock.clone())
7602 .children(leader_border);
7603
7604 // Apply sizing only when the dock is open. When closed the dock is still
7605 // included in the element tree so its focus handle remains mounted — without
7606 // this, toggle_panel_focus cannot focus the panel when the dock is closed.
7607 let dock = dock.read(cx);
7608 if let Some(panel) = dock.visible_panel() {
7609 let size_state = dock.stored_panel_size_state(panel.as_ref());
7610 let min_size = panel.min_size(window, cx);
7611 if position.axis() == Axis::Horizontal {
7612 let use_flexible = panel.has_flexible_size(window, cx);
7613 let flex_grow = if use_flexible {
7614 size_state
7615 .and_then(|state| state.flex)
7616 .or_else(|| self.default_dock_flex(position))
7617 } else {
7618 None
7619 };
7620 if let Some(grow) = flex_grow {
7621 let grow = (grow / self.center_full_height_column_count()).max(0.001);
7622 let style = container.style();
7623 style.flex_grow = Some(grow);
7624 style.flex_shrink = Some(1.0);
7625 style.flex_basis = Some(relative(0.).into());
7626 } else {
7627 let size = size_state
7628 .and_then(|state| state.size)
7629 .unwrap_or_else(|| panel.default_size(window, cx));
7630 container = container.w(size);
7631 }
7632 if let Some(min) = min_size {
7633 container = container.min_w(min);
7634 }
7635 } else {
7636 let size = size_state
7637 .and_then(|state| state.size)
7638 .unwrap_or_else(|| panel.default_size(window, cx));
7639 container = container.h(size);
7640 }
7641 }
7642
7643 Some(container)
7644 }
7645
7646 pub fn for_window(window: &Window, cx: &App) -> Option<Entity<Workspace>> {
7647 window
7648 .root::<MultiWorkspace>()
7649 .flatten()
7650 .map(|multi_workspace| multi_workspace.read(cx).workspace().clone())
7651 }
7652
7653 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
7654 self.zoomed.as_ref()
7655 }
7656
7657 pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
7658 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
7659 return;
7660 };
7661 let windows = cx.windows();
7662 let next_window =
7663 SystemWindowTabController::get_next_tab_group_window(cx, current_window_id).or_else(
7664 || {
7665 windows
7666 .iter()
7667 .cycle()
7668 .skip_while(|window| window.window_id() != current_window_id)
7669 .nth(1)
7670 },
7671 );
7672
7673 if let Some(window) = next_window {
7674 window
7675 .update(cx, |_, window, _| window.activate_window())
7676 .ok();
7677 }
7678 }
7679
7680 pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
7681 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
7682 return;
7683 };
7684 let windows = cx.windows();
7685 let prev_window =
7686 SystemWindowTabController::get_prev_tab_group_window(cx, current_window_id).or_else(
7687 || {
7688 windows
7689 .iter()
7690 .rev()
7691 .cycle()
7692 .skip_while(|window| window.window_id() != current_window_id)
7693 .nth(1)
7694 },
7695 );
7696
7697 if let Some(window) = prev_window {
7698 window
7699 .update(cx, |_, window, _| window.activate_window())
7700 .ok();
7701 }
7702 }
7703
7704 pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
7705 if cx.stop_active_drag(window) {
7706 } else if let Some((notification_id, _)) = self.notifications.pop() {
7707 dismiss_app_notification(¬ification_id, cx);
7708 } else {
7709 cx.propagate();
7710 }
7711 }
7712
7713 fn resize_dock(
7714 &mut self,
7715 dock_pos: DockPosition,
7716 new_size: Pixels,
7717 window: &mut Window,
7718 cx: &mut Context<Self>,
7719 ) {
7720 match dock_pos {
7721 DockPosition::Left => self.resize_left_dock(new_size, window, cx),
7722 DockPosition::Right => self.resize_right_dock(new_size, window, cx),
7723 DockPosition::Bottom => self.resize_bottom_dock(new_size, window, cx),
7724 }
7725 }
7726
7727 fn resize_left_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
7728 let workspace_width = self.bounds.size.width;
7729 let mut size = new_size.min(workspace_width - RESIZE_HANDLE_SIZE);
7730
7731 self.right_dock.read_with(cx, |right_dock, cx| {
7732 let right_dock_size = right_dock
7733 .stored_active_panel_size(window, cx)
7734 .unwrap_or(Pixels::ZERO);
7735 if right_dock_size + size > workspace_width {
7736 size = workspace_width - right_dock_size
7737 }
7738 });
7739
7740 let flex_grow = self.dock_flex_for_size(DockPosition::Left, size, window, cx);
7741 self.left_dock.update(cx, |left_dock, cx| {
7742 if WorkspaceSettings::get_global(cx)
7743 .resize_all_panels_in_dock
7744 .contains(&DockPosition::Left)
7745 {
7746 left_dock.resize_all_panels(Some(size), flex_grow, window, cx);
7747 } else {
7748 left_dock.resize_active_panel(Some(size), flex_grow, window, cx);
7749 }
7750 });
7751 }
7752
7753 fn resize_right_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
7754 let workspace_width = self.bounds.size.width;
7755 let mut size = new_size.min(workspace_width - RESIZE_HANDLE_SIZE);
7756 self.left_dock.read_with(cx, |left_dock, cx| {
7757 let left_dock_size = left_dock
7758 .stored_active_panel_size(window, cx)
7759 .unwrap_or(Pixels::ZERO);
7760 if left_dock_size + size > workspace_width {
7761 size = workspace_width - left_dock_size
7762 }
7763 });
7764 let flex_grow = self.dock_flex_for_size(DockPosition::Right, size, window, cx);
7765 self.right_dock.update(cx, |right_dock, cx| {
7766 if WorkspaceSettings::get_global(cx)
7767 .resize_all_panels_in_dock
7768 .contains(&DockPosition::Right)
7769 {
7770 right_dock.resize_all_panels(Some(size), flex_grow, window, cx);
7771 } else {
7772 right_dock.resize_active_panel(Some(size), flex_grow, window, cx);
7773 }
7774 });
7775 }
7776
7777 fn resize_bottom_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
7778 let size = new_size.min(self.bounds.bottom() - RESIZE_HANDLE_SIZE - self.bounds.top());
7779 self.bottom_dock.update(cx, |bottom_dock, cx| {
7780 if WorkspaceSettings::get_global(cx)
7781 .resize_all_panels_in_dock
7782 .contains(&DockPosition::Bottom)
7783 {
7784 bottom_dock.resize_all_panels(Some(size), None, window, cx);
7785 } else {
7786 bottom_dock.resize_active_panel(Some(size), None, window, cx);
7787 }
7788 });
7789 }
7790
7791 fn toggle_edit_predictions_all_files(
7792 &mut self,
7793 _: &ToggleEditPrediction,
7794 _window: &mut Window,
7795 cx: &mut Context<Self>,
7796 ) {
7797 let fs = self.project().read(cx).fs().clone();
7798 let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx);
7799 update_settings_file(fs, cx, move |file, _| {
7800 file.project.all_languages.defaults.show_edit_predictions = Some(!show_edit_predictions)
7801 });
7802 }
7803
7804 fn toggle_theme_mode(&mut self, _: &ToggleMode, _window: &mut Window, cx: &mut Context<Self>) {
7805 let current_mode = ThemeSettings::get_global(cx).theme.mode();
7806 let next_mode = match current_mode {
7807 Some(theme_settings::ThemeAppearanceMode::Light) => {
7808 theme_settings::ThemeAppearanceMode::Dark
7809 }
7810 Some(theme_settings::ThemeAppearanceMode::Dark) => {
7811 theme_settings::ThemeAppearanceMode::Light
7812 }
7813 Some(theme_settings::ThemeAppearanceMode::System) | None => {
7814 match cx.theme().appearance() {
7815 theme::Appearance::Light => theme_settings::ThemeAppearanceMode::Dark,
7816 theme::Appearance::Dark => theme_settings::ThemeAppearanceMode::Light,
7817 }
7818 }
7819 };
7820
7821 let fs = self.project().read(cx).fs().clone();
7822 settings::update_settings_file(fs, cx, move |settings, _cx| {
7823 theme_settings::set_mode(settings, next_mode);
7824 });
7825 }
7826
7827 pub fn show_worktree_trust_security_modal(
7828 &mut self,
7829 toggle: bool,
7830 window: &mut Window,
7831 cx: &mut Context<Self>,
7832 ) {
7833 if let Some(security_modal) = self.active_modal::<SecurityModal>(cx) {
7834 if toggle {
7835 security_modal.update(cx, |security_modal, cx| {
7836 security_modal.dismiss(cx);
7837 })
7838 } else {
7839 security_modal.update(cx, |security_modal, cx| {
7840 security_modal.refresh_restricted_paths(cx);
7841 });
7842 }
7843 } else {
7844 let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
7845 .map(|trusted_worktrees| {
7846 trusted_worktrees
7847 .read(cx)
7848 .has_restricted_worktrees(&self.project().read(cx).worktree_store(), cx)
7849 })
7850 .unwrap_or(false);
7851 if has_restricted_worktrees {
7852 let project = self.project().read(cx);
7853 let remote_host = project
7854 .remote_connection_options(cx)
7855 .map(RemoteHostLocation::from);
7856 let worktree_store = project.worktree_store().downgrade();
7857 self.toggle_modal(window, cx, |_, cx| {
7858 SecurityModal::new(worktree_store, remote_host, cx)
7859 });
7860 }
7861 }
7862 }
7863}
7864
7865pub trait AnyActiveCall {
7866 fn entity(&self) -> AnyEntity;
7867 fn is_in_room(&self, _: &App) -> bool;
7868 fn room_id(&self, _: &App) -> Option<u64>;
7869 fn channel_id(&self, _: &App) -> Option<ChannelId>;
7870 fn hang_up(&self, _: &mut App) -> Task<Result<()>>;
7871 fn unshare_project(&self, _: Entity<Project>, _: &mut App) -> Result<()>;
7872 fn remote_participant_for_peer_id(&self, _: PeerId, _: &App) -> Option<RemoteCollaborator>;
7873 fn is_sharing_project(&self, _: &App) -> bool;
7874 fn has_remote_participants(&self, _: &App) -> bool;
7875 fn local_participant_is_guest(&self, _: &App) -> bool;
7876 fn client(&self, _: &App) -> Arc<Client>;
7877 fn share_on_join(&self, _: &App) -> bool;
7878 fn join_channel(&self, _: ChannelId, _: &mut App) -> Task<Result<bool>>;
7879 fn room_update_completed(&self, _: &mut App) -> Task<()>;
7880 fn most_active_project(&self, _: &App) -> Option<(u64, u64)>;
7881 fn share_project(&self, _: Entity<Project>, _: &mut App) -> Task<Result<u64>>;
7882 fn join_project(
7883 &self,
7884 _: u64,
7885 _: Arc<LanguageRegistry>,
7886 _: Arc<dyn Fs>,
7887 _: &mut App,
7888 ) -> Task<Result<Entity<Project>>>;
7889 fn peer_id_for_user_in_room(&self, _: u64, _: &App) -> Option<PeerId>;
7890 fn subscribe(
7891 &self,
7892 _: &mut Window,
7893 _: &mut Context<Workspace>,
7894 _: Box<dyn Fn(&mut Workspace, &ActiveCallEvent, &mut Window, &mut Context<Workspace>)>,
7895 ) -> Subscription;
7896 fn create_shared_screen(
7897 &self,
7898 _: PeerId,
7899 _: &Entity<Pane>,
7900 _: &mut Window,
7901 _: &mut App,
7902 ) -> Option<Entity<SharedScreen>>;
7903}
7904
7905#[derive(Clone)]
7906pub struct GlobalAnyActiveCall(pub Arc<dyn AnyActiveCall>);
7907impl Global for GlobalAnyActiveCall {}
7908
7909impl GlobalAnyActiveCall {
7910 pub(crate) fn try_global(cx: &App) -> Option<&Self> {
7911 cx.try_global()
7912 }
7913
7914 pub(crate) fn global(cx: &App) -> &Self {
7915 cx.global()
7916 }
7917}
7918
7919/// Workspace-local view of a remote participant's location.
7920#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7921pub enum ParticipantLocation {
7922 SharedProject { project_id: u64 },
7923 UnsharedProject,
7924 External,
7925}
7926
7927impl ParticipantLocation {
7928 pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
7929 match location
7930 .and_then(|l| l.variant)
7931 .context("participant location was not provided")?
7932 {
7933 proto::participant_location::Variant::SharedProject(project) => {
7934 Ok(Self::SharedProject {
7935 project_id: project.id,
7936 })
7937 }
7938 proto::participant_location::Variant::UnsharedProject(_) => Ok(Self::UnsharedProject),
7939 proto::participant_location::Variant::External(_) => Ok(Self::External),
7940 }
7941 }
7942}
7943/// Workspace-local view of a remote collaborator's state.
7944/// This is the subset of `call::RemoteParticipant` that workspace needs.
7945#[derive(Clone)]
7946pub struct RemoteCollaborator {
7947 pub user: Arc<User>,
7948 pub peer_id: PeerId,
7949 pub location: ParticipantLocation,
7950 pub participant_index: ParticipantIndex,
7951}
7952
7953pub enum ActiveCallEvent {
7954 ParticipantLocationChanged { participant_id: PeerId },
7955 RemoteVideoTracksChanged { participant_id: PeerId },
7956}
7957
7958fn leader_border_for_pane(
7959 follower_states: &HashMap<CollaboratorId, FollowerState>,
7960 pane: &Entity<Pane>,
7961 _: &Window,
7962 cx: &App,
7963) -> Option<Div> {
7964 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
7965 if state.pane() == pane {
7966 Some((*leader_id, state))
7967 } else {
7968 None
7969 }
7970 })?;
7971
7972 let mut leader_color = match leader_id {
7973 CollaboratorId::PeerId(leader_peer_id) => {
7974 let leader = GlobalAnyActiveCall::try_global(cx)?
7975 .0
7976 .remote_participant_for_peer_id(leader_peer_id, cx)?;
7977
7978 cx.theme()
7979 .players()
7980 .color_for_participant(leader.participant_index.0)
7981 .cursor
7982 }
7983 CollaboratorId::Agent => cx.theme().players().agent().cursor,
7984 };
7985 leader_color.fade_out(0.3);
7986 Some(
7987 div()
7988 .absolute()
7989 .size_full()
7990 .left_0()
7991 .top_0()
7992 .border_2()
7993 .border_color(leader_color),
7994 )
7995}
7996
7997fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
7998 ZED_WINDOW_POSITION
7999 .zip(*ZED_WINDOW_SIZE)
8000 .map(|(position, size)| Bounds {
8001 origin: position,
8002 size,
8003 })
8004}
8005
8006fn open_items(
8007 serialized_workspace: Option<SerializedWorkspace>,
8008 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
8009 window: &mut Window,
8010 cx: &mut Context<Workspace>,
8011) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
8012 let restored_items = serialized_workspace.map(|serialized_workspace| {
8013 Workspace::load_workspace(
8014 serialized_workspace,
8015 project_paths_to_open
8016 .iter()
8017 .map(|(_, project_path)| project_path)
8018 .cloned()
8019 .collect(),
8020 window,
8021 cx,
8022 )
8023 });
8024
8025 cx.spawn_in(window, async move |workspace, cx| {
8026 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
8027
8028 if let Some(restored_items) = restored_items {
8029 let restored_items = restored_items.await?;
8030
8031 let restored_project_paths = restored_items
8032 .iter()
8033 .filter_map(|item| {
8034 cx.update(|_, cx| item.as_ref()?.project_path(cx))
8035 .ok()
8036 .flatten()
8037 })
8038 .collect::<HashSet<_>>();
8039
8040 for restored_item in restored_items {
8041 opened_items.push(restored_item.map(Ok));
8042 }
8043
8044 project_paths_to_open
8045 .iter_mut()
8046 .for_each(|(_, project_path)| {
8047 if let Some(project_path_to_open) = project_path
8048 && restored_project_paths.contains(project_path_to_open)
8049 {
8050 *project_path = None;
8051 }
8052 });
8053 } else {
8054 for _ in 0..project_paths_to_open.len() {
8055 opened_items.push(None);
8056 }
8057 }
8058 assert!(opened_items.len() == project_paths_to_open.len());
8059
8060 let tasks =
8061 project_paths_to_open
8062 .into_iter()
8063 .enumerate()
8064 .map(|(ix, (abs_path, project_path))| {
8065 let workspace = workspace.clone();
8066 cx.spawn(async move |cx| {
8067 let file_project_path = project_path?;
8068 let abs_path_task = workspace.update(cx, |workspace, cx| {
8069 workspace.project().update(cx, |project, cx| {
8070 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
8071 })
8072 });
8073
8074 // We only want to open file paths here. If one of the items
8075 // here is a directory, it was already opened further above
8076 // with a `find_or_create_worktree`.
8077 if let Ok(task) = abs_path_task
8078 && task.await.is_none_or(|p| p.is_file())
8079 {
8080 return Some((
8081 ix,
8082 workspace
8083 .update_in(cx, |workspace, window, cx| {
8084 workspace.open_path(
8085 file_project_path,
8086 None,
8087 true,
8088 window,
8089 cx,
8090 )
8091 })
8092 .log_err()?
8093 .await,
8094 ));
8095 }
8096 None
8097 })
8098 });
8099
8100 let tasks = tasks.collect::<Vec<_>>();
8101
8102 let tasks = futures::future::join_all(tasks);
8103 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
8104 opened_items[ix] = Some(path_open_result);
8105 }
8106
8107 Ok(opened_items)
8108 })
8109}
8110
8111#[derive(Clone)]
8112enum ActivateInDirectionTarget {
8113 Pane(Entity<Pane>),
8114 Dock(Entity<Dock>),
8115 Sidebar(FocusHandle),
8116}
8117
8118fn notify_if_database_failed(window: WindowHandle<MultiWorkspace>, cx: &mut AsyncApp) {
8119 window
8120 .update(cx, |multi_workspace, _, cx| {
8121 let workspace = multi_workspace.workspace().clone();
8122 workspace.update(cx, |workspace, cx| {
8123 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
8124 struct DatabaseFailedNotification;
8125
8126 workspace.show_notification(
8127 NotificationId::unique::<DatabaseFailedNotification>(),
8128 cx,
8129 |cx| {
8130 cx.new(|cx| {
8131 MessageNotification::new("Failed to load the database file.", cx)
8132 .primary_message("File an Issue")
8133 .primary_icon(IconName::Plus)
8134 .primary_on_click(|window, cx| {
8135 window.dispatch_action(Box::new(FileBugReport), cx)
8136 })
8137 })
8138 },
8139 );
8140 }
8141 });
8142 })
8143 .log_err();
8144}
8145
8146fn px_with_ui_font_fallback(val: u32, cx: &Context<Workspace>) -> Pixels {
8147 if val == 0 {
8148 ThemeSettings::get_global(cx).ui_font_size(cx)
8149 } else {
8150 px(val as f32)
8151 }
8152}
8153
8154fn adjust_active_dock_size_by_px(
8155 px: Pixels,
8156 workspace: &mut Workspace,
8157 window: &mut Window,
8158 cx: &mut Context<Workspace>,
8159) {
8160 let Some(active_dock) = workspace
8161 .all_docks()
8162 .into_iter()
8163 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx))
8164 else {
8165 return;
8166 };
8167 let dock = active_dock.read(cx);
8168 let Some(panel_size) = workspace.dock_size(&dock, window, cx) else {
8169 return;
8170 };
8171 workspace.resize_dock(dock.position(), panel_size + px, window, cx);
8172}
8173
8174fn adjust_open_docks_size_by_px(
8175 px: Pixels,
8176 workspace: &mut Workspace,
8177 window: &mut Window,
8178 cx: &mut Context<Workspace>,
8179) {
8180 let docks = workspace
8181 .all_docks()
8182 .into_iter()
8183 .filter_map(|dock_entity| {
8184 let dock = dock_entity.read(cx);
8185 if dock.is_open() {
8186 let dock_pos = dock.position();
8187 let panel_size = workspace.dock_size(&dock, window, cx)?;
8188 Some((dock_pos, panel_size + px))
8189 } else {
8190 None
8191 }
8192 })
8193 .collect::<Vec<_>>();
8194
8195 for (position, new_size) in docks {
8196 workspace.resize_dock(position, new_size, window, cx);
8197 }
8198}
8199
8200impl Focusable for Workspace {
8201 fn focus_handle(&self, cx: &App) -> FocusHandle {
8202 self.active_pane.focus_handle(cx)
8203 }
8204}
8205
8206#[derive(Clone)]
8207struct DraggedDock(DockPosition);
8208
8209impl Render for DraggedDock {
8210 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
8211 gpui::Empty
8212 }
8213}
8214
8215impl Render for Workspace {
8216 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
8217 static FIRST_PAINT: AtomicBool = AtomicBool::new(true);
8218 if FIRST_PAINT.swap(false, std::sync::atomic::Ordering::Relaxed) {
8219 log::info!("Rendered first frame");
8220 }
8221
8222 let centered_layout = self.centered_layout
8223 && self.center.panes().len() == 1
8224 && self.active_item(cx).is_some();
8225 let render_padding = |size| {
8226 (size > 0.0).then(|| {
8227 div()
8228 .h_full()
8229 .w(relative(size))
8230 .bg(cx.theme().colors().editor_background)
8231 .border_color(cx.theme().colors().pane_group_border)
8232 })
8233 };
8234 let paddings = if centered_layout {
8235 let settings = WorkspaceSettings::get_global(cx).centered_layout;
8236 (
8237 render_padding(Self::adjust_padding(
8238 settings.left_padding.map(|padding| padding.0),
8239 )),
8240 render_padding(Self::adjust_padding(
8241 settings.right_padding.map(|padding| padding.0),
8242 )),
8243 )
8244 } else {
8245 (None, None)
8246 };
8247 let ui_font = theme_settings::setup_ui_font(window, cx);
8248
8249 let theme = cx.theme().clone();
8250 let colors = theme.colors();
8251 let notification_entities = self
8252 .notifications
8253 .iter()
8254 .map(|(_, notification)| notification.entity_id())
8255 .collect::<Vec<_>>();
8256 let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout;
8257
8258 let pane_render_context = PaneRenderContext {
8259 follower_states: &self.follower_states,
8260 active_call: self.active_call(),
8261 active_pane: &self.active_pane,
8262 app_state: &self.app_state,
8263 project: &self.project,
8264 workspace: &self.weak_self,
8265 };
8266
8267 div()
8268 .relative()
8269 .size_full()
8270 .flex()
8271 .flex_col()
8272 .font(ui_font)
8273 .gap_0()
8274 .justify_start()
8275 .items_start()
8276 .text_color(colors.text)
8277 .overflow_hidden()
8278 .children(self.titlebar_item.clone())
8279 .on_modifiers_changed(move |_, _, cx| {
8280 for &id in ¬ification_entities {
8281 cx.notify(id);
8282 }
8283 })
8284 .child(
8285 div()
8286 .size_full()
8287 .relative()
8288 .flex_1()
8289 .flex()
8290 .flex_col()
8291 .child(
8292 div()
8293 .id("workspace")
8294 .bg(colors.background)
8295 .relative()
8296 .flex_1()
8297 .w_full()
8298 .flex()
8299 .flex_col()
8300 .overflow_hidden()
8301 .border_t_1()
8302 .border_b_1()
8303 .border_color(colors.border)
8304 .child({
8305 let this = cx.entity();
8306 canvas(
8307 move |bounds, window, cx| {
8308 this.update(cx, |this, cx| {
8309 let bounds_changed = this.bounds != bounds;
8310 this.bounds = bounds;
8311
8312 if bounds_changed {
8313 this.left_dock.update(cx, |dock, cx| {
8314 dock.clamp_panel_size(
8315 bounds.size.width,
8316 window,
8317 cx,
8318 )
8319 });
8320
8321 this.right_dock.update(cx, |dock, cx| {
8322 dock.clamp_panel_size(
8323 bounds.size.width,
8324 window,
8325 cx,
8326 )
8327 });
8328
8329 this.bottom_dock.update(cx, |dock, cx| {
8330 dock.clamp_panel_size(
8331 bounds.size.height,
8332 window,
8333 cx,
8334 )
8335 });
8336 }
8337 })
8338 },
8339 |_, _, _, _| {},
8340 )
8341 .absolute()
8342 .size_full()
8343 })
8344 .when(self.zoomed.is_none(), |this| {
8345 this.on_drag_move(cx.listener(
8346 move |workspace, e: &DragMoveEvent<DraggedDock>, window, cx| {
8347 if workspace.previous_dock_drag_coordinates
8348 != Some(e.event.position)
8349 {
8350 workspace.previous_dock_drag_coordinates =
8351 Some(e.event.position);
8352
8353 match e.drag(cx).0 {
8354 DockPosition::Left => {
8355 workspace.resize_left_dock(
8356 e.event.position.x
8357 - workspace.bounds.left(),
8358 window,
8359 cx,
8360 );
8361 }
8362 DockPosition::Right => {
8363 workspace.resize_right_dock(
8364 workspace.bounds.right()
8365 - e.event.position.x,
8366 window,
8367 cx,
8368 );
8369 }
8370 DockPosition::Bottom => {
8371 workspace.resize_bottom_dock(
8372 workspace.bounds.bottom()
8373 - e.event.position.y,
8374 window,
8375 cx,
8376 );
8377 }
8378 };
8379 workspace.serialize_workspace(window, cx);
8380 }
8381 },
8382 ))
8383 })
8384 .child({
8385 match bottom_dock_layout {
8386 BottomDockLayout::Full => div()
8387 .flex()
8388 .flex_col()
8389 .h_full()
8390 .child(
8391 div()
8392 .flex()
8393 .flex_row()
8394 .flex_1()
8395 .overflow_hidden()
8396 .children(self.render_dock(
8397 DockPosition::Left,
8398 &self.left_dock,
8399 window,
8400 cx,
8401 ))
8402 .child(
8403 div()
8404 .flex()
8405 .flex_col()
8406 .flex_1()
8407 .overflow_hidden()
8408 .child(
8409 h_flex()
8410 .flex_1()
8411 .when_some(paddings.0, |this, p| {
8412 this.child(p.border_r_1())
8413 })
8414 .child(self.center.render(
8415 self.zoomed.as_ref(),
8416 &pane_render_context,
8417 window,
8418 cx,
8419 ))
8420 .when_some(
8421 paddings.1,
8422 |this, p| {
8423 this.child(p.border_l_1())
8424 },
8425 ),
8426 ),
8427 )
8428 .children(self.render_dock(
8429 DockPosition::Right,
8430 &self.right_dock,
8431 window,
8432 cx,
8433 )),
8434 )
8435 .child(div().w_full().children(self.render_dock(
8436 DockPosition::Bottom,
8437 &self.bottom_dock,
8438 window,
8439 cx,
8440 ))),
8441
8442 BottomDockLayout::LeftAligned => div()
8443 .flex()
8444 .flex_row()
8445 .h_full()
8446 .child(
8447 div()
8448 .flex()
8449 .flex_col()
8450 .flex_1()
8451 .h_full()
8452 .child(
8453 div()
8454 .flex()
8455 .flex_row()
8456 .flex_1()
8457 .children(self.render_dock(
8458 DockPosition::Left,
8459 &self.left_dock,
8460 window,
8461 cx,
8462 ))
8463 .child(
8464 div()
8465 .flex()
8466 .flex_col()
8467 .flex_1()
8468 .overflow_hidden()
8469 .child(
8470 h_flex()
8471 .flex_1()
8472 .when_some(
8473 paddings.0,
8474 |this, p| {
8475 this.child(
8476 p.border_r_1(),
8477 )
8478 },
8479 )
8480 .child(self.center.render(
8481 self.zoomed.as_ref(),
8482 &pane_render_context,
8483 window,
8484 cx,
8485 ))
8486 .when_some(
8487 paddings.1,
8488 |this, p| {
8489 this.child(
8490 p.border_l_1(),
8491 )
8492 },
8493 ),
8494 ),
8495 ),
8496 )
8497 .child(div().w_full().children(self.render_dock(
8498 DockPosition::Bottom,
8499 &self.bottom_dock,
8500 window,
8501 cx,
8502 ))),
8503 )
8504 .children(self.render_dock(
8505 DockPosition::Right,
8506 &self.right_dock,
8507 window,
8508 cx,
8509 )),
8510 BottomDockLayout::RightAligned => div()
8511 .flex()
8512 .flex_row()
8513 .h_full()
8514 .children(self.render_dock(
8515 DockPosition::Left,
8516 &self.left_dock,
8517 window,
8518 cx,
8519 ))
8520 .child(
8521 div()
8522 .flex()
8523 .flex_col()
8524 .flex_1()
8525 .h_full()
8526 .child(
8527 div()
8528 .flex()
8529 .flex_row()
8530 .flex_1()
8531 .child(
8532 div()
8533 .flex()
8534 .flex_col()
8535 .flex_1()
8536 .overflow_hidden()
8537 .child(
8538 h_flex()
8539 .flex_1()
8540 .when_some(
8541 paddings.0,
8542 |this, p| {
8543 this.child(
8544 p.border_r_1(),
8545 )
8546 },
8547 )
8548 .child(self.center.render(
8549 self.zoomed.as_ref(),
8550 &pane_render_context,
8551 window,
8552 cx,
8553 ))
8554 .when_some(
8555 paddings.1,
8556 |this, p| {
8557 this.child(
8558 p.border_l_1(),
8559 )
8560 },
8561 ),
8562 ),
8563 )
8564 .children(self.render_dock(
8565 DockPosition::Right,
8566 &self.right_dock,
8567 window,
8568 cx,
8569 )),
8570 )
8571 .child(div().w_full().children(self.render_dock(
8572 DockPosition::Bottom,
8573 &self.bottom_dock,
8574 window,
8575 cx,
8576 ))),
8577 ),
8578 BottomDockLayout::Contained => div()
8579 .flex()
8580 .flex_row()
8581 .h_full()
8582 .children(self.render_dock(
8583 DockPosition::Left,
8584 &self.left_dock,
8585 window,
8586 cx,
8587 ))
8588 .child(
8589 div()
8590 .flex()
8591 .flex_col()
8592 .flex_1()
8593 .overflow_hidden()
8594 .child(
8595 h_flex()
8596 .flex_1()
8597 .when_some(paddings.0, |this, p| {
8598 this.child(p.border_r_1())
8599 })
8600 .child(self.center.render(
8601 self.zoomed.as_ref(),
8602 &pane_render_context,
8603 window,
8604 cx,
8605 ))
8606 .when_some(paddings.1, |this, p| {
8607 this.child(p.border_l_1())
8608 }),
8609 )
8610 .children(self.render_dock(
8611 DockPosition::Bottom,
8612 &self.bottom_dock,
8613 window,
8614 cx,
8615 )),
8616 )
8617 .children(self.render_dock(
8618 DockPosition::Right,
8619 &self.right_dock,
8620 window,
8621 cx,
8622 )),
8623 }
8624 })
8625 .children(self.zoomed.as_ref().and_then(|view| {
8626 let zoomed_view = view.upgrade()?;
8627 let div = div()
8628 .occlude()
8629 .absolute()
8630 .overflow_hidden()
8631 .border_color(colors.border)
8632 .bg(colors.background)
8633 .child(zoomed_view)
8634 .inset_0()
8635 .shadow_lg();
8636
8637 if !WorkspaceSettings::get_global(cx).zoomed_padding {
8638 return Some(div);
8639 }
8640
8641 Some(match self.zoomed_position {
8642 Some(DockPosition::Left) => div.right_2().border_r_1(),
8643 Some(DockPosition::Right) => div.left_2().border_l_1(),
8644 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
8645 None => div.top_2().bottom_2().left_2().right_2().border_1(),
8646 })
8647 }))
8648 .children(self.render_notifications(window, cx)),
8649 )
8650 .when(self.status_bar_visible(cx), |parent| {
8651 parent.child(self.status_bar.clone())
8652 })
8653 .child(self.toast_layer.clone()),
8654 )
8655 }
8656}
8657
8658impl WorkspaceStore {
8659 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
8660 Self {
8661 workspaces: Default::default(),
8662 _subscriptions: vec![
8663 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
8664 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
8665 ],
8666 client,
8667 }
8668 }
8669
8670 pub fn update_followers(
8671 &self,
8672 project_id: Option<u64>,
8673 update: proto::update_followers::Variant,
8674 cx: &App,
8675 ) -> Option<()> {
8676 let active_call = GlobalAnyActiveCall::try_global(cx)?;
8677 let room_id = active_call.0.room_id(cx)?;
8678 self.client
8679 .send(proto::UpdateFollowers {
8680 room_id,
8681 project_id,
8682 variant: Some(update),
8683 })
8684 .log_err()
8685 }
8686
8687 pub async fn handle_follow(
8688 this: Entity<Self>,
8689 envelope: TypedEnvelope<proto::Follow>,
8690 mut cx: AsyncApp,
8691 ) -> Result<proto::FollowResponse> {
8692 this.update(&mut cx, |this, cx| {
8693 let follower = Follower {
8694 project_id: envelope.payload.project_id,
8695 peer_id: envelope.original_sender_id()?,
8696 };
8697
8698 let mut response = proto::FollowResponse::default();
8699
8700 this.workspaces.retain(|(window_handle, weak_workspace)| {
8701 let Some(workspace) = weak_workspace.upgrade() else {
8702 return false;
8703 };
8704 window_handle
8705 .update(cx, |_, window, cx| {
8706 workspace.update(cx, |workspace, cx| {
8707 let handler_response =
8708 workspace.handle_follow(follower.project_id, window, cx);
8709 if let Some(active_view) = handler_response.active_view
8710 && workspace.project.read(cx).remote_id() == follower.project_id
8711 {
8712 response.active_view = Some(active_view)
8713 }
8714 });
8715 })
8716 .is_ok()
8717 });
8718
8719 Ok(response)
8720 })
8721 }
8722
8723 async fn handle_update_followers(
8724 this: Entity<Self>,
8725 envelope: TypedEnvelope<proto::UpdateFollowers>,
8726 mut cx: AsyncApp,
8727 ) -> Result<()> {
8728 let leader_id = envelope.original_sender_id()?;
8729 let update = envelope.payload;
8730
8731 this.update(&mut cx, |this, cx| {
8732 this.workspaces.retain(|(window_handle, weak_workspace)| {
8733 let Some(workspace) = weak_workspace.upgrade() else {
8734 return false;
8735 };
8736 window_handle
8737 .update(cx, |_, window, cx| {
8738 workspace.update(cx, |workspace, cx| {
8739 let project_id = workspace.project.read(cx).remote_id();
8740 if update.project_id != project_id && update.project_id.is_some() {
8741 return;
8742 }
8743 workspace.handle_update_followers(
8744 leader_id,
8745 update.clone(),
8746 window,
8747 cx,
8748 );
8749 });
8750 })
8751 .is_ok()
8752 });
8753 Ok(())
8754 })
8755 }
8756
8757 pub fn workspaces(&self) -> impl Iterator<Item = &WeakEntity<Workspace>> {
8758 self.workspaces.iter().map(|(_, weak)| weak)
8759 }
8760
8761 pub fn workspaces_with_windows(
8762 &self,
8763 ) -> impl Iterator<Item = (gpui::AnyWindowHandle, &WeakEntity<Workspace>)> {
8764 self.workspaces.iter().map(|(window, weak)| (*window, weak))
8765 }
8766}
8767
8768impl ViewId {
8769 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
8770 Ok(Self {
8771 creator: message
8772 .creator
8773 .map(CollaboratorId::PeerId)
8774 .context("creator is missing")?,
8775 id: message.id,
8776 })
8777 }
8778
8779 pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
8780 if let CollaboratorId::PeerId(peer_id) = self.creator {
8781 Some(proto::ViewId {
8782 creator: Some(peer_id),
8783 id: self.id,
8784 })
8785 } else {
8786 None
8787 }
8788 }
8789}
8790
8791impl FollowerState {
8792 fn pane(&self) -> &Entity<Pane> {
8793 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
8794 }
8795}
8796
8797pub trait WorkspaceHandle {
8798 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
8799}
8800
8801impl WorkspaceHandle for Entity<Workspace> {
8802 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
8803 self.read(cx)
8804 .worktrees(cx)
8805 .flat_map(|worktree| {
8806 let worktree_id = worktree.read(cx).id();
8807 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
8808 worktree_id,
8809 path: f.path.clone(),
8810 })
8811 })
8812 .collect::<Vec<_>>()
8813 }
8814}
8815
8816pub async fn last_opened_workspace_location(
8817 db: &WorkspaceDb,
8818 fs: &dyn fs::Fs,
8819) -> Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)> {
8820 db.last_workspace(fs)
8821 .await
8822 .log_err()
8823 .flatten()
8824 .map(|(id, location, paths, _timestamp)| (id, location, paths))
8825}
8826
8827pub async fn last_session_workspace_locations(
8828 db: &WorkspaceDb,
8829 last_session_id: &str,
8830 last_session_window_stack: Option<Vec<WindowId>>,
8831 fs: &dyn fs::Fs,
8832) -> Option<Vec<SessionWorkspace>> {
8833 db.last_session_workspace_locations(last_session_id, last_session_window_stack, fs)
8834 .await
8835 .log_err()
8836}
8837
8838pub async fn restore_multiworkspace(
8839 multi_workspace: SerializedMultiWorkspace,
8840 app_state: Arc<AppState>,
8841 cx: &mut AsyncApp,
8842) -> anyhow::Result<WindowHandle<MultiWorkspace>> {
8843 let SerializedMultiWorkspace {
8844 active_workspace,
8845 state,
8846 } = multi_workspace;
8847
8848 let workspace_result = if active_workspace.paths.is_empty() {
8849 cx.update(|cx| {
8850 open_workspace_by_id(active_workspace.workspace_id, app_state.clone(), None, cx)
8851 })
8852 .await
8853 } else {
8854 cx.update(|cx| {
8855 Workspace::new_local(
8856 active_workspace.paths.paths().to_vec(),
8857 app_state.clone(),
8858 None,
8859 None,
8860 None,
8861 OpenMode::Activate,
8862 cx,
8863 )
8864 })
8865 .await
8866 .map(|result| result.window)
8867 };
8868
8869 let window_handle = match workspace_result {
8870 Ok(handle) => handle,
8871 Err(err) => {
8872 log::error!("Failed to restore active workspace: {err:#}");
8873
8874 let mut fallback_handle = None;
8875 for key in &state.project_groups {
8876 let key: ProjectGroupKey = key.clone().into();
8877 let paths = key.path_list().paths().to_vec();
8878 match cx
8879 .update(|cx| {
8880 Workspace::new_local(
8881 paths,
8882 app_state.clone(),
8883 None,
8884 None,
8885 None,
8886 OpenMode::Activate,
8887 cx,
8888 )
8889 })
8890 .await
8891 {
8892 Ok(OpenResult { window, .. }) => {
8893 fallback_handle = Some(window);
8894 break;
8895 }
8896 Err(fallback_err) => {
8897 log::error!("Fallback project group also failed: {fallback_err:#}");
8898 }
8899 }
8900 }
8901
8902 fallback_handle.ok_or(err)?
8903 }
8904 };
8905
8906 apply_restored_multiworkspace_state(window_handle, &state, app_state.fs.clone(), cx).await;
8907
8908 window_handle
8909 .update(cx, |_, window, _cx| {
8910 window.activate_window();
8911 })
8912 .ok();
8913
8914 Ok(window_handle)
8915}
8916
8917pub async fn apply_restored_multiworkspace_state(
8918 window_handle: WindowHandle<MultiWorkspace>,
8919 state: &MultiWorkspaceState,
8920 fs: Arc<dyn fs::Fs>,
8921 cx: &mut AsyncApp,
8922) {
8923 let MultiWorkspaceState {
8924 sidebar_open,
8925 project_groups,
8926 sidebar_state,
8927 ..
8928 } = state;
8929
8930 if !project_groups.is_empty() {
8931 // Resolve linked worktree paths to their main repo paths so
8932 // stale keys from previous sessions get normalized and deduped.
8933 let mut resolved_groups: Vec<SerializedProjectGroupState> = Vec::new();
8934 for serialized in project_groups.iter().cloned() {
8935 let SerializedProjectGroupState { key, expanded } = serialized.into_restored_state();
8936 if key.path_list().paths().is_empty() {
8937 continue;
8938 }
8939 let mut resolved_paths = Vec::new();
8940 for path in key.path_list().paths() {
8941 if key.host().is_none()
8942 && let Some(common_dir) =
8943 project::discover_root_repo_common_dir(path, fs.as_ref()).await
8944 {
8945 let main_path = common_dir.parent().unwrap_or(&common_dir);
8946 resolved_paths.push(main_path.to_path_buf());
8947 } else {
8948 resolved_paths.push(path.to_path_buf());
8949 }
8950 }
8951 let resolved = ProjectGroupKey::new(key.host(), PathList::new(&resolved_paths));
8952 if !resolved_groups.iter().any(|g| g.key == resolved) {
8953 resolved_groups.push(SerializedProjectGroupState {
8954 key: resolved,
8955 expanded,
8956 });
8957 }
8958 }
8959
8960 window_handle
8961 .update(cx, |multi_workspace, _window, cx| {
8962 multi_workspace.restore_project_groups(resolved_groups, cx);
8963 })
8964 .ok();
8965 }
8966
8967 if *sidebar_open {
8968 window_handle
8969 .update(cx, |multi_workspace, _, cx| {
8970 multi_workspace.restore_open_sidebar(cx);
8971 })
8972 .ok();
8973 }
8974
8975 if let Some(sidebar_state) = sidebar_state {
8976 window_handle
8977 .update(cx, |multi_workspace, window, cx| {
8978 if let Some(sidebar) = multi_workspace.sidebar() {
8979 sidebar.restore_serialized_state(sidebar_state, window, cx);
8980 }
8981 multi_workspace.serialize(cx);
8982 })
8983 .ok();
8984 }
8985}
8986
8987actions!(
8988 collab,
8989 [
8990 /// Opens the channel notes for the current call.
8991 ///
8992 /// Use `collab_panel::OpenSelectedChannelNotes` to open the channel notes for the selected
8993 /// channel in the collab panel.
8994 ///
8995 /// If you want to open a specific channel, use `zed::OpenZedUrl` with a channel notes URL -
8996 /// can be copied via "Copy link to section" in the context menu of the channel notes
8997 /// buffer. These URLs look like `https://zed.dev/channel/channel-name-CHANNEL_ID/notes`.
8998 OpenChannelNotes,
8999 /// Mutes your microphone.
9000 Mute,
9001 /// Deafens yourself (mute both microphone and speakers).
9002 Deafen,
9003 /// Leaves the current call.
9004 LeaveCall,
9005 /// Shares the current project with collaborators.
9006 ShareProject,
9007 /// Shares your screen with collaborators.
9008 ScreenShare,
9009 /// Copies the current room name and session id for debugging purposes.
9010 CopyRoomId,
9011 ]
9012);
9013
9014/// Opens the channel notes for a specific channel by its ID.
9015#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
9016#[action(namespace = collab)]
9017#[serde(deny_unknown_fields)]
9018pub struct OpenChannelNotesById {
9019 pub channel_id: u64,
9020}
9021
9022actions!(
9023 zed,
9024 [
9025 /// Opens the Zed log file.
9026 OpenLog,
9027 /// Reveals the Zed log file in the system file manager.
9028 RevealLogInFileManager
9029 ]
9030);
9031
9032async fn join_channel_internal(
9033 channel_id: ChannelId,
9034 app_state: &Arc<AppState>,
9035 requesting_window: Option<WindowHandle<MultiWorkspace>>,
9036 requesting_workspace: Option<WeakEntity<Workspace>>,
9037 active_call: &dyn AnyActiveCall,
9038 cx: &mut AsyncApp,
9039) -> Result<bool> {
9040 let (should_prompt, already_in_channel) = cx.update(|cx| {
9041 if !active_call.is_in_room(cx) {
9042 return (false, false);
9043 }
9044
9045 let already_in_channel = active_call.channel_id(cx) == Some(channel_id);
9046 let should_prompt = active_call.is_sharing_project(cx)
9047 && active_call.has_remote_participants(cx)
9048 && !already_in_channel;
9049 (should_prompt, already_in_channel)
9050 });
9051
9052 if already_in_channel {
9053 let task = cx.update(|cx| {
9054 if let Some((project, host)) = active_call.most_active_project(cx) {
9055 Some(join_in_room_project(project, host, app_state.clone(), cx))
9056 } else {
9057 None
9058 }
9059 });
9060 if let Some(task) = task {
9061 task.await?;
9062 }
9063 return anyhow::Ok(true);
9064 }
9065
9066 if should_prompt {
9067 if let Some(multi_workspace) = requesting_window {
9068 let answer = multi_workspace
9069 .update(cx, |_, window, cx| {
9070 window.prompt(
9071 PromptLevel::Warning,
9072 "Do you want to switch channels?",
9073 Some("Leaving this call will unshare your current project."),
9074 &["Yes, Join Channel", "Cancel"],
9075 cx,
9076 )
9077 })?
9078 .await;
9079
9080 if answer == Ok(1) {
9081 return Ok(false);
9082 }
9083 } else {
9084 return Ok(false);
9085 }
9086 }
9087
9088 let client = cx.update(|cx| active_call.client(cx));
9089
9090 let mut client_status = client.status();
9091
9092 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
9093 'outer: loop {
9094 let Some(status) = client_status.recv().await else {
9095 anyhow::bail!("error connecting");
9096 };
9097
9098 match status {
9099 Status::Connecting
9100 | Status::Authenticating
9101 | Status::Authenticated
9102 | Status::Reconnecting
9103 | Status::Reauthenticating
9104 | Status::Reauthenticated => continue,
9105 Status::Connected { .. } => break 'outer,
9106 Status::SignedOut | Status::AuthenticationError => {
9107 return Err(ErrorCode::SignedOut.into());
9108 }
9109 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
9110 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
9111 return Err(ErrorCode::Disconnected.into());
9112 }
9113 }
9114 }
9115
9116 let joined = cx
9117 .update(|cx| active_call.join_channel(channel_id, cx))
9118 .await?;
9119
9120 if !joined {
9121 return anyhow::Ok(true);
9122 }
9123
9124 cx.update(|cx| active_call.room_update_completed(cx)).await;
9125
9126 let task = cx.update(|cx| {
9127 if let Some((project, host)) = active_call.most_active_project(cx) {
9128 return Some(join_in_room_project(project, host, app_state.clone(), cx));
9129 }
9130
9131 // If you are the first to join a channel, see if you should share your project.
9132 if !active_call.has_remote_participants(cx)
9133 && !active_call.local_participant_is_guest(cx)
9134 && let Some(workspace) = requesting_workspace.as_ref().and_then(|w| w.upgrade())
9135 {
9136 let project = workspace.update(cx, |workspace, cx| {
9137 let project = workspace.project.read(cx);
9138
9139 if !active_call.share_on_join(cx) {
9140 return None;
9141 }
9142
9143 if (project.is_local() || project.is_via_remote_server())
9144 && project.visible_worktrees(cx).any(|tree| {
9145 tree.read(cx)
9146 .root_entry()
9147 .is_some_and(|entry| entry.is_dir())
9148 })
9149 {
9150 Some(workspace.project.clone())
9151 } else {
9152 None
9153 }
9154 });
9155 if let Some(project) = project {
9156 let share_task = active_call.share_project(project, cx);
9157 return Some(cx.spawn(async move |_cx| -> Result<()> {
9158 share_task.await?;
9159 Ok(())
9160 }));
9161 }
9162 }
9163
9164 None
9165 });
9166 if let Some(task) = task {
9167 task.await?;
9168 return anyhow::Ok(true);
9169 }
9170 anyhow::Ok(false)
9171}
9172
9173pub fn join_channel(
9174 channel_id: ChannelId,
9175 app_state: Arc<AppState>,
9176 requesting_window: Option<WindowHandle<MultiWorkspace>>,
9177 requesting_workspace: Option<WeakEntity<Workspace>>,
9178 cx: &mut App,
9179) -> Task<Result<()>> {
9180 let active_call = GlobalAnyActiveCall::global(cx).clone();
9181 cx.spawn(async move |cx| {
9182 let result = join_channel_internal(
9183 channel_id,
9184 &app_state,
9185 requesting_window,
9186 requesting_workspace,
9187 &*active_call.0,
9188 cx,
9189 )
9190 .await;
9191
9192 // join channel succeeded, and opened a window
9193 if matches!(result, Ok(true)) {
9194 return anyhow::Ok(());
9195 }
9196
9197 // find an existing workspace to focus and show call controls
9198 let mut active_window = requesting_window.or_else(|| activate_any_workspace_window(cx));
9199 if active_window.is_none() {
9200 // no open workspaces, make one to show the error in (blergh)
9201 let OpenResult {
9202 window: window_handle,
9203 ..
9204 } = cx
9205 .update(|cx| {
9206 Workspace::new_local(
9207 vec![],
9208 app_state.clone(),
9209 requesting_window,
9210 None,
9211 None,
9212 OpenMode::Activate,
9213 cx,
9214 )
9215 })
9216 .await?;
9217
9218 window_handle
9219 .update(cx, |_, window, _cx| {
9220 window.activate_window();
9221 })
9222 .ok();
9223
9224 if result.is_ok() {
9225 cx.update(|cx| {
9226 cx.dispatch_action(&OpenChannelNotes);
9227 });
9228 }
9229
9230 active_window = Some(window_handle);
9231 }
9232
9233 if let Err(err) = result {
9234 log::error!("failed to join channel: {}", err);
9235 if let Some(active_window) = active_window {
9236 active_window
9237 .update(cx, |_, window, cx| {
9238 let detail: SharedString = match err.error_code() {
9239 ErrorCode::SignedOut => "Please sign in to continue.".into(),
9240 ErrorCode::UpgradeRequired => concat!(
9241 "Your are running an unsupported version of Zed. ",
9242 "Please update to continue."
9243 )
9244 .into(),
9245 ErrorCode::NoSuchChannel => concat!(
9246 "No matching channel was found. ",
9247 "Please check the link and try again."
9248 )
9249 .into(),
9250 ErrorCode::Forbidden => concat!(
9251 "This channel is private, and you do not have access. ",
9252 "Please ask someone to add you and try again."
9253 )
9254 .into(),
9255 ErrorCode::Disconnected => {
9256 "Please check your internet connection and try again.".into()
9257 }
9258 _ => format!("{}\n\nPlease try again.", err).into(),
9259 };
9260 window.prompt(
9261 PromptLevel::Critical,
9262 "Failed to join channel",
9263 Some(&detail),
9264 &["Ok"],
9265 cx,
9266 )
9267 })?
9268 .await
9269 .ok();
9270 }
9271 }
9272
9273 // return ok, we showed the error to the user.
9274 anyhow::Ok(())
9275 })
9276}
9277
9278pub async fn get_any_active_multi_workspace(
9279 app_state: Arc<AppState>,
9280 mut cx: AsyncApp,
9281) -> anyhow::Result<WindowHandle<MultiWorkspace>> {
9282 // find an existing workspace to focus and show call controls
9283 let active_window = activate_any_workspace_window(&mut cx);
9284 if active_window.is_none() {
9285 cx.update(|cx| {
9286 Workspace::new_local(
9287 vec![],
9288 app_state.clone(),
9289 None,
9290 None,
9291 None,
9292 OpenMode::Activate,
9293 cx,
9294 )
9295 })
9296 .await?;
9297 }
9298 activate_any_workspace_window(&mut cx).context("could not open zed")
9299}
9300
9301fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<MultiWorkspace>> {
9302 cx.update(|cx| {
9303 if let Some(workspace_window) = cx
9304 .active_window()
9305 .and_then(|window| window.downcast::<MultiWorkspace>())
9306 {
9307 return Some(workspace_window);
9308 }
9309
9310 for window in cx.windows() {
9311 if let Some(workspace_window) = window.downcast::<MultiWorkspace>() {
9312 workspace_window
9313 .update(cx, |_, window, _| window.activate_window())
9314 .ok();
9315 return Some(workspace_window);
9316 }
9317 }
9318 None
9319 })
9320}
9321
9322pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<MultiWorkspace>> {
9323 workspace_windows_for_location(&SerializedWorkspaceLocation::Local, cx)
9324}
9325
9326pub fn workspace_windows_for_location(
9327 serialized_location: &SerializedWorkspaceLocation,
9328 cx: &App,
9329) -> Vec<WindowHandle<MultiWorkspace>> {
9330 cx.windows()
9331 .into_iter()
9332 .filter_map(|window| window.downcast::<MultiWorkspace>())
9333 .filter(|multi_workspace| {
9334 let same_host = |left: &RemoteConnectionOptions, right: &RemoteConnectionOptions| match (left, right) {
9335 (RemoteConnectionOptions::Ssh(a), RemoteConnectionOptions::Ssh(b)) => {
9336 (&a.host, &a.username, &a.port) == (&b.host, &b.username, &b.port)
9337 }
9338 (RemoteConnectionOptions::Wsl(a), RemoteConnectionOptions::Wsl(b)) => {
9339 // The WSL username is not consistently populated in the workspace location, so ignore it for now.
9340 a.distro_name == b.distro_name
9341 }
9342 (RemoteConnectionOptions::Docker(a), RemoteConnectionOptions::Docker(b)) => {
9343 a.container_id == b.container_id
9344 }
9345 #[cfg(any(test, feature = "test-support"))]
9346 (RemoteConnectionOptions::Mock(a), RemoteConnectionOptions::Mock(b)) => {
9347 a.id == b.id
9348 }
9349 _ => false,
9350 };
9351
9352 multi_workspace.read(cx).is_ok_and(|multi_workspace| {
9353 multi_workspace.workspaces().any(|workspace| {
9354 match workspace.read(cx).workspace_location(cx) {
9355 WorkspaceLocation::Location(location, _) => {
9356 match (&location, serialized_location) {
9357 (
9358 SerializedWorkspaceLocation::Local,
9359 SerializedWorkspaceLocation::Local,
9360 ) => true,
9361 (
9362 SerializedWorkspaceLocation::Remote(a),
9363 SerializedWorkspaceLocation::Remote(b),
9364 ) => same_host(a, b),
9365 _ => false,
9366 }
9367 }
9368 _ => false,
9369 }
9370 })
9371 })
9372 })
9373 .collect()
9374}
9375
9376pub async fn find_existing_workspace(
9377 abs_paths: &[PathBuf],
9378 open_options: &OpenOptions,
9379 location: &SerializedWorkspaceLocation,
9380 cx: &mut AsyncApp,
9381) -> (
9382 Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)>,
9383 OpenVisible,
9384) {
9385 let mut existing: Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)> = None;
9386 let mut open_visible = OpenVisible::All;
9387 let mut best_match = None;
9388
9389 if open_options.workspace_matching != WorkspaceMatching::None {
9390 cx.update(|cx| {
9391 for window in workspace_windows_for_location(location, cx) {
9392 if let Ok(multi_workspace) = window.read(cx) {
9393 for workspace in multi_workspace.workspaces() {
9394 let project = workspace.read(cx).project.read(cx);
9395 let m = project.visibility_for_paths(
9396 abs_paths,
9397 open_options.workspace_matching != WorkspaceMatching::MatchSubdirectory,
9398 cx,
9399 );
9400 if m > best_match {
9401 existing = Some((window, workspace.clone()));
9402 best_match = m;
9403 } else if best_match.is_none()
9404 && open_options.workspace_matching
9405 == WorkspaceMatching::MatchSubdirectory
9406 {
9407 existing = Some((window, workspace.clone()))
9408 }
9409 }
9410 }
9411 }
9412 });
9413
9414 let all_paths_are_files = existing
9415 .as_ref()
9416 .and_then(|(_, target_workspace)| {
9417 cx.update(|cx| {
9418 let workspace = target_workspace.read(cx);
9419 let project = workspace.project.read(cx);
9420 let path_style = workspace.path_style(cx);
9421 Some(!abs_paths.iter().any(|path| {
9422 let path = util::paths::SanitizedPath::new(path);
9423 project.worktrees(cx).any(|worktree| {
9424 let worktree = worktree.read(cx);
9425 let abs_path = worktree.abs_path();
9426 path_style
9427 .strip_prefix(path.as_ref(), abs_path.as_ref())
9428 .and_then(|rel| worktree.entry_for_path(&rel))
9429 .is_some_and(|e| e.is_dir())
9430 })
9431 }))
9432 })
9433 })
9434 .unwrap_or(false);
9435
9436 if open_options.wait && existing.is_some() && all_paths_are_files {
9437 cx.update(|cx| {
9438 let windows = workspace_windows_for_location(location, cx);
9439 let window = cx
9440 .active_window()
9441 .and_then(|window| window.downcast::<MultiWorkspace>())
9442 .filter(|window| windows.contains(window))
9443 .or_else(|| windows.into_iter().next());
9444 if let Some(window) = window {
9445 if let Ok(multi_workspace) = window.read(cx) {
9446 let active_workspace = multi_workspace.workspace().clone();
9447 existing = Some((window, active_workspace));
9448 open_visible = OpenVisible::None;
9449 }
9450 }
9451 });
9452 }
9453 }
9454 (existing, open_visible)
9455}
9456
9457/// Controls whether to reuse an existing workspace whose worktrees contain the
9458/// given paths, and how broadly to match.
9459#[derive(Clone, Debug, Default, PartialEq, Eq)]
9460pub enum WorkspaceMatching {
9461 /// Always open a new workspace. No matching against existing worktrees.
9462 None,
9463 /// Match paths against existing worktree roots and files within them.
9464 #[default]
9465 MatchExact,
9466 /// Match paths against existing worktrees including subdirectories, and
9467 /// fall back to any existing window if no worktree matched.
9468 ///
9469 /// For example, `zed -a foo/bar` will activate the `bar` workspace if it
9470 /// exists, otherwise it will open a new window with `foo/bar` as the root.
9471 MatchSubdirectory,
9472}
9473
9474#[derive(Clone)]
9475pub struct OpenOptions {
9476 pub visible: Option<OpenVisible>,
9477 pub focus: Option<bool>,
9478 pub workspace_matching: WorkspaceMatching,
9479 /// Whether to add unmatched directories to the existing window's sidebar
9480 /// rather than opening a new window. Defaults to true, matching the default
9481 /// `cli_default_open_behavior` setting.
9482 pub add_dirs_to_sidebar: bool,
9483 pub wait: bool,
9484 pub requesting_window: Option<WindowHandle<MultiWorkspace>>,
9485 pub open_mode: OpenMode,
9486 pub env: Option<HashMap<String, String>>,
9487 pub open_in_dev_container: bool,
9488}
9489
9490impl Default for OpenOptions {
9491 fn default() -> Self {
9492 Self {
9493 visible: None,
9494 focus: None,
9495 workspace_matching: WorkspaceMatching::default(),
9496 add_dirs_to_sidebar: true,
9497 wait: false,
9498 requesting_window: None,
9499 open_mode: OpenMode::default(),
9500 env: None,
9501 open_in_dev_container: false,
9502 }
9503 }
9504}
9505
9506impl OpenOptions {
9507 fn should_reuse_existing_window(&self) -> bool {
9508 self.workspace_matching != WorkspaceMatching::None && self.open_mode != OpenMode::NewWindow
9509 }
9510}
9511
9512/// The result of opening a workspace via [`open_paths`], [`Workspace::new_local`],
9513/// or [`Workspace::open_workspace_for_paths`].
9514pub struct OpenResult {
9515 pub window: WindowHandle<MultiWorkspace>,
9516 pub workspace: Entity<Workspace>,
9517 pub opened_items: Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
9518}
9519
9520/// Opens a workspace by its database ID, used for restoring empty workspaces with unsaved content.
9521pub fn open_workspace_by_id(
9522 workspace_id: WorkspaceId,
9523 app_state: Arc<AppState>,
9524 requesting_window: Option<WindowHandle<MultiWorkspace>>,
9525 cx: &mut App,
9526) -> Task<anyhow::Result<WindowHandle<MultiWorkspace>>> {
9527 let project_handle = Project::local(
9528 app_state.client.clone(),
9529 app_state.node_runtime.clone(),
9530 app_state.user_store.clone(),
9531 app_state.languages.clone(),
9532 app_state.fs.clone(),
9533 None,
9534 project::LocalProjectFlags {
9535 init_worktree_trust: true,
9536 ..project::LocalProjectFlags::default()
9537 },
9538 cx,
9539 );
9540
9541 let db = WorkspaceDb::global(cx);
9542 let kvp = db::kvp::KeyValueStore::global(cx);
9543 cx.spawn(async move |cx| {
9544 let serialized_workspace = db
9545 .workspace_for_id(workspace_id)
9546 .with_context(|| format!("Workspace {workspace_id:?} not found"))?;
9547
9548 let centered_layout = serialized_workspace.centered_layout;
9549
9550 let (window, workspace) = if let Some(window) = requesting_window {
9551 let workspace = window.update(cx, |multi_workspace, window, cx| {
9552 let workspace = cx.new(|cx| {
9553 let mut workspace = Workspace::new(
9554 Some(workspace_id),
9555 project_handle.clone(),
9556 app_state.clone(),
9557 window,
9558 cx,
9559 );
9560 workspace.centered_layout = centered_layout;
9561 workspace
9562 });
9563 multi_workspace.add(workspace.clone(), &*window, cx);
9564 workspace
9565 })?;
9566 (window, workspace)
9567 } else {
9568 let window_bounds_override = window_bounds_env_override();
9569
9570 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
9571 (Some(WindowBounds::Windowed(bounds)), None)
9572 } else if let Some(display) = serialized_workspace.display
9573 && let Some(bounds) = serialized_workspace.window_bounds.as_ref()
9574 {
9575 (Some(bounds.0), Some(display))
9576 } else if let Some((display, bounds)) = persistence::read_default_window_bounds(&kvp) {
9577 (Some(bounds), Some(display))
9578 } else {
9579 (None, None)
9580 };
9581
9582 let options = cx.update(|cx| {
9583 let mut options = (app_state.build_window_options)(display, cx);
9584 options.window_bounds = window_bounds;
9585 options
9586 });
9587
9588 let window = cx.open_window(options, {
9589 let app_state = app_state.clone();
9590 let project_handle = project_handle.clone();
9591 move |window, cx| {
9592 let workspace = cx.new(|cx| {
9593 let mut workspace = Workspace::new(
9594 Some(workspace_id),
9595 project_handle,
9596 app_state,
9597 window,
9598 cx,
9599 );
9600 workspace.centered_layout = centered_layout;
9601 workspace
9602 });
9603 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
9604 }
9605 })?;
9606
9607 let workspace = window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
9608 multi_workspace.workspace().clone()
9609 })?;
9610
9611 (window, workspace)
9612 };
9613
9614 notify_if_database_failed(window, cx);
9615
9616 // Restore items from the serialized workspace
9617 window
9618 .update(cx, |_, window, cx| {
9619 workspace.update(cx, |_workspace, cx| {
9620 open_items(Some(serialized_workspace), vec![], window, cx)
9621 })
9622 })?
9623 .await?;
9624
9625 window.update(cx, |_, window, cx| {
9626 workspace.update(cx, |workspace, cx| {
9627 workspace.serialize_workspace(window, cx);
9628 });
9629 })?;
9630
9631 Ok(window)
9632 })
9633}
9634
9635#[allow(clippy::type_complexity)]
9636pub fn open_paths(
9637 abs_paths: &[PathBuf],
9638 app_state: Arc<AppState>,
9639 mut open_options: OpenOptions,
9640 cx: &mut App,
9641) -> Task<anyhow::Result<OpenResult>> {
9642 let abs_paths = abs_paths.to_vec();
9643 #[cfg(target_os = "windows")]
9644 let wsl_path = abs_paths
9645 .iter()
9646 .find_map(|p| util::paths::WslPath::from_path(p));
9647
9648 cx.spawn(async move |cx| {
9649 let (mut existing, mut open_visible) = find_existing_workspace(
9650 &abs_paths,
9651 &open_options,
9652 &SerializedWorkspaceLocation::Local,
9653 cx,
9654 )
9655 .await;
9656
9657 // Fallback: if no workspace contains the paths and all paths are files,
9658 // prefer an existing local workspace window (active window first).
9659 if open_options.should_reuse_existing_window() && existing.is_none() {
9660 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
9661 let all_metadatas = futures::future::join_all(all_paths)
9662 .await
9663 .into_iter()
9664 .filter_map(|result| result.ok().flatten());
9665
9666 if all_metadatas.into_iter().all(|file| !file.is_dir) {
9667 cx.update(|cx| {
9668 let windows = workspace_windows_for_location(
9669 &SerializedWorkspaceLocation::Local,
9670 cx,
9671 );
9672 let window = cx
9673 .active_window()
9674 .and_then(|window| window.downcast::<MultiWorkspace>())
9675 .filter(|window| windows.contains(window))
9676 .or_else(|| windows.into_iter().next());
9677 if let Some(window) = window {
9678 if let Ok(multi_workspace) = window.read(cx) {
9679 let active_workspace = multi_workspace.workspace().clone();
9680 existing = Some((window, active_workspace));
9681 open_visible = OpenVisible::None;
9682 }
9683 }
9684 });
9685 }
9686 }
9687
9688 // Fallback for directories: when no flag is specified and no existing
9689 // workspace matched, check the user's setting to decide whether to add
9690 // the directory as a new workspace in the active window's MultiWorkspace
9691 // or open a new window.
9692 // Skip when requesting_window is already set: the caller (e.g.
9693 // open_workspace_for_paths reusing an empty window) already chose the
9694 // target window, so we must not open the sidebar as a side-effect.
9695 if open_options.should_reuse_existing_window()
9696 && existing.is_none()
9697 && open_options.requesting_window.is_none()
9698 {
9699 let use_existing_window = open_options.add_dirs_to_sidebar;
9700
9701 if use_existing_window {
9702 let target_window = cx.update(|cx| {
9703 let windows = workspace_windows_for_location(
9704 &SerializedWorkspaceLocation::Local,
9705 cx,
9706 );
9707 let window = cx
9708 .active_window()
9709 .and_then(|window| window.downcast::<MultiWorkspace>())
9710 .filter(|window| windows.contains(window))
9711 .or_else(|| windows.into_iter().next());
9712 window.filter(|window| {
9713 window
9714 .read(cx)
9715 .is_ok_and(|mw| mw.multi_workspace_enabled(cx))
9716 })
9717 });
9718
9719 if let Some(window) = target_window {
9720 open_options.requesting_window = Some(window);
9721 window
9722 .update(cx, |multi_workspace, _, cx| {
9723 multi_workspace.open_sidebar(cx);
9724 })
9725 .log_err();
9726 }
9727 }
9728 }
9729
9730 let open_in_dev_container = open_options.open_in_dev_container;
9731
9732 let result = if let Some((existing, target_workspace)) = existing {
9733 let open_task = existing
9734 .update(cx, |multi_workspace, window, cx| {
9735 window.activate_window();
9736 multi_workspace.activate(target_workspace.clone(), None, window, cx);
9737 target_workspace.update(cx, |workspace, cx| {
9738 if open_in_dev_container {
9739 workspace.set_open_in_dev_container(true);
9740 }
9741 workspace.open_paths(
9742 abs_paths,
9743 OpenOptions {
9744 visible: Some(open_visible),
9745 ..Default::default()
9746 },
9747 None,
9748 window,
9749 cx,
9750 )
9751 })
9752 })?
9753 .await;
9754
9755 _ = existing.update(cx, |multi_workspace, _, cx| {
9756 let workspace = multi_workspace.workspace().clone();
9757 workspace.update(cx, |workspace, cx| {
9758 for item in open_task.iter().flatten() {
9759 if let Err(e) = item {
9760 workspace.show_error(&e, cx);
9761 }
9762 }
9763 });
9764 });
9765
9766 Ok(OpenResult { window: existing, workspace: target_workspace, opened_items: open_task })
9767 } else {
9768 let init = if open_in_dev_container {
9769 Some(Box::new(|workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context<Workspace>| {
9770 workspace.set_open_in_dev_container(true);
9771 }) as Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>)
9772 } else {
9773 None
9774 };
9775 let result = cx
9776 .update(move |cx| {
9777 Workspace::new_local(
9778 abs_paths,
9779 app_state.clone(),
9780 open_options.requesting_window,
9781 open_options.env,
9782 init,
9783 open_options.open_mode,
9784 cx,
9785 )
9786 })
9787 .await;
9788
9789 if let Ok(ref result) = result {
9790 result.window
9791 .update(cx, |_, window, _cx| {
9792 window.activate_window();
9793 })
9794 .log_err();
9795 }
9796
9797 result
9798 };
9799
9800 #[cfg(target_os = "windows")]
9801 if let Some(util::paths::WslPath{distro, path}) = wsl_path
9802 && let Ok(ref result) = result
9803 {
9804 result.window
9805 .update(cx, move |multi_workspace, _window, cx| {
9806 struct OpenInWsl;
9807 let workspace = multi_workspace.workspace().clone();
9808 workspace.update(cx, |workspace, cx| {
9809 workspace.show_notification(NotificationId::unique::<OpenInWsl>(), cx, move |cx| {
9810 let display_path = util::markdown::MarkdownInlineCode(&path.to_string_lossy());
9811 let msg = format!("{display_path} is inside a WSL filesystem, some features may not work unless you open it with WSL remote");
9812 cx.new(move |cx| {
9813 MessageNotification::new(msg, cx)
9814 .primary_message("Open in WSL")
9815 .primary_icon(IconName::FolderOpen)
9816 .primary_on_click(move |window, cx| {
9817 window.dispatch_action(Box::new(remote::OpenWslPath {
9818 distro: remote::WslConnectionOptions {
9819 distro_name: distro.clone(),
9820 user: None,
9821 },
9822 paths: vec![path.clone().into()],
9823 }), cx)
9824 })
9825 })
9826 });
9827 });
9828 })
9829 .unwrap();
9830 };
9831 result
9832 })
9833}
9834
9835pub fn open_new(
9836 open_options: OpenOptions,
9837 app_state: Arc<AppState>,
9838 cx: &mut App,
9839 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
9840) -> Task<anyhow::Result<()>> {
9841 let addition = open_options.open_mode;
9842 let task = Workspace::new_local(
9843 Vec::new(),
9844 app_state,
9845 open_options.requesting_window,
9846 open_options.env,
9847 Some(Box::new(init)),
9848 addition,
9849 cx,
9850 );
9851 cx.spawn(async move |cx| {
9852 let OpenResult { window, .. } = task.await?;
9853 window
9854 .update(cx, |_, window, _cx| {
9855 window.activate_window();
9856 })
9857 .ok();
9858 Ok(())
9859 })
9860}
9861
9862pub fn create_and_open_local_file(
9863 path: &'static Path,
9864 window: &mut Window,
9865 cx: &mut Context<Workspace>,
9866 default_content: impl 'static + Send + FnOnce() -> Rope,
9867) -> Task<Result<Box<dyn ItemHandle>>> {
9868 cx.spawn_in(window, async move |workspace, cx| {
9869 let fs = workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
9870 if !fs.is_file(path).await {
9871 fs.create_file(path, Default::default()).await?;
9872 fs.save(path, &default_content(), Default::default())
9873 .await?;
9874 }
9875
9876 workspace
9877 .update_in(cx, |workspace, window, cx| {
9878 workspace.with_local_or_wsl_workspace(window, cx, |workspace, window, cx| {
9879 let path = workspace
9880 .project
9881 .read_with(cx, |project, cx| project.try_windows_path_to_wsl(path, cx));
9882 cx.spawn_in(window, async move |workspace, cx| {
9883 let path = path.await?;
9884
9885 let path = fs.canonicalize(&path).await.unwrap_or(path);
9886
9887 let mut items = workspace
9888 .update_in(cx, |workspace, window, cx| {
9889 workspace.open_paths(
9890 vec![path.to_path_buf()],
9891 OpenOptions {
9892 visible: Some(OpenVisible::None),
9893 ..Default::default()
9894 },
9895 None,
9896 window,
9897 cx,
9898 )
9899 })?
9900 .await;
9901 let item = items.pop().flatten();
9902 item.with_context(|| format!("path {path:?} is not a file"))?
9903 })
9904 })
9905 })?
9906 .await?
9907 .await
9908 })
9909}
9910
9911pub fn open_remote_project_with_new_connection(
9912 window: WindowHandle<MultiWorkspace>,
9913 remote_connection: Arc<dyn RemoteConnection>,
9914 cancel_rx: oneshot::Receiver<()>,
9915 delegate: Arc<dyn RemoteClientDelegate>,
9916 app_state: Arc<AppState>,
9917 paths: Vec<PathBuf>,
9918 cx: &mut App,
9919) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
9920 cx.spawn(async move |cx| {
9921 let (workspace_id, serialized_workspace) =
9922 deserialize_remote_project(remote_connection.connection_options(), paths.clone(), cx)
9923 .await?;
9924
9925 let session = match cx
9926 .update(|cx| {
9927 remote::RemoteClient::new(
9928 ConnectionIdentifier::Workspace(workspace_id.0),
9929 remote_connection,
9930 cancel_rx,
9931 delegate,
9932 cx,
9933 )
9934 })
9935 .await?
9936 {
9937 Some(result) => result,
9938 None => return Ok(Vec::new()),
9939 };
9940
9941 let project = cx.update(|cx| {
9942 project::Project::remote(
9943 session,
9944 app_state.client.clone(),
9945 app_state.node_runtime.clone(),
9946 app_state.user_store.clone(),
9947 app_state.languages.clone(),
9948 app_state.fs.clone(),
9949 true,
9950 cx,
9951 )
9952 });
9953
9954 open_remote_project_inner(
9955 project,
9956 paths,
9957 workspace_id,
9958 serialized_workspace,
9959 app_state,
9960 window,
9961 None,
9962 None,
9963 cx,
9964 )
9965 .await
9966 })
9967}
9968
9969pub fn open_remote_project_with_existing_connection(
9970 connection_options: RemoteConnectionOptions,
9971 project: Entity<Project>,
9972 paths: Vec<PathBuf>,
9973 app_state: Arc<AppState>,
9974 window: WindowHandle<MultiWorkspace>,
9975 provisional_project_group_key: Option<ProjectGroupKey>,
9976 source_workspace: Option<WeakEntity<Workspace>>,
9977 cx: &mut AsyncApp,
9978) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
9979 cx.spawn(async move |cx| {
9980 let (workspace_id, serialized_workspace) =
9981 deserialize_remote_project(connection_options.clone(), paths.clone(), cx).await?;
9982
9983 open_remote_project_inner(
9984 project,
9985 paths,
9986 workspace_id,
9987 serialized_workspace,
9988 app_state,
9989 window,
9990 provisional_project_group_key,
9991 source_workspace,
9992 cx,
9993 )
9994 .await
9995 })
9996}
9997
9998async fn open_remote_project_inner(
9999 project: Entity<Project>,
10000 paths: Vec<PathBuf>,
10001 workspace_id: WorkspaceId,
10002 serialized_workspace: Option<SerializedWorkspace>,
10003 app_state: Arc<AppState>,
10004 window: WindowHandle<MultiWorkspace>,
10005 provisional_project_group_key: Option<ProjectGroupKey>,
10006 source_workspace: Option<WeakEntity<Workspace>>,
10007 cx: &mut AsyncApp,
10008) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
10009 let db = cx.update(|cx| WorkspaceDb::global(cx));
10010 let toolchains = db.toolchains(workspace_id).await?;
10011 for (toolchain, worktree_path, path) in toolchains {
10012 project
10013 .update(cx, |this, cx| {
10014 let Some(worktree_id) =
10015 this.find_worktree(&worktree_path, cx)
10016 .and_then(|(worktree, rel_path)| {
10017 if rel_path.is_empty() {
10018 Some(worktree.read(cx).id())
10019 } else {
10020 None
10021 }
10022 })
10023 else {
10024 return Task::ready(None);
10025 };
10026
10027 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
10028 })
10029 .await;
10030 }
10031 let mut project_paths_to_open = vec![];
10032 let mut project_path_errors = vec![];
10033
10034 for path in paths {
10035 let result = cx
10036 .update(|cx| {
10037 Workspace::project_path_for_path(project.clone(), path.as_path(), true, cx)
10038 })
10039 .await;
10040 match result {
10041 Ok((_, project_path)) => {
10042 project_paths_to_open.push((path, Some(project_path)));
10043 }
10044 Err(error) => {
10045 project_path_errors.push(error);
10046 }
10047 };
10048 }
10049
10050 if project_paths_to_open.is_empty() {
10051 return Err(project_path_errors.pop().context("no paths given")?);
10052 }
10053
10054 let workspace = window.update(cx, |multi_workspace, window, cx| {
10055 telemetry::event!("SSH Project Opened");
10056
10057 let new_workspace = cx.new(|cx| {
10058 let mut workspace =
10059 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
10060 workspace.update_history(cx);
10061
10062 if let Some(ref serialized) = serialized_workspace {
10063 workspace.centered_layout = serialized.centered_layout;
10064 }
10065
10066 workspace
10067 });
10068
10069 if let Some(project_group_key) = provisional_project_group_key.clone() {
10070 multi_workspace.activate_provisional_workspace(
10071 new_workspace.clone(),
10072 project_group_key,
10073 window,
10074 cx,
10075 );
10076 } else {
10077 multi_workspace.activate(new_workspace.clone(), source_workspace, window, cx);
10078 }
10079 new_workspace
10080 })?;
10081
10082 let items = window
10083 .update(cx, |_, window, cx| {
10084 window.activate_window();
10085 workspace.update(cx, |_workspace, cx| {
10086 open_items(serialized_workspace, project_paths_to_open, window, cx)
10087 })
10088 })?
10089 .await?;
10090
10091 workspace.update(cx, |workspace, cx| {
10092 for error in project_path_errors {
10093 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
10094 if let Some(path) = error.error_tag("path") {
10095 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
10096 }
10097 } else {
10098 workspace.show_error(&error, cx)
10099 }
10100 }
10101 });
10102
10103 Ok(items.into_iter().map(|item| item?.ok()).collect())
10104}
10105
10106fn deserialize_remote_project(
10107 connection_options: RemoteConnectionOptions,
10108 paths: Vec<PathBuf>,
10109 cx: &AsyncApp,
10110) -> Task<Result<(WorkspaceId, Option<SerializedWorkspace>)>> {
10111 let db = cx.update(|cx| WorkspaceDb::global(cx));
10112 cx.background_spawn(async move {
10113 let remote_connection_id = db
10114 .get_or_create_remote_connection(connection_options)
10115 .await?;
10116
10117 let serialized_workspace = db.remote_workspace_for_roots(&paths, remote_connection_id);
10118
10119 let workspace_id = if let Some(workspace_id) =
10120 serialized_workspace.as_ref().map(|workspace| workspace.id)
10121 {
10122 workspace_id
10123 } else {
10124 db.next_id().await?
10125 };
10126
10127 Ok((workspace_id, serialized_workspace))
10128 })
10129}
10130
10131pub fn join_in_room_project(
10132 project_id: u64,
10133 follow_user_id: u64,
10134 app_state: Arc<AppState>,
10135 cx: &mut App,
10136) -> Task<Result<()>> {
10137 let windows = cx.windows();
10138 cx.spawn(async move |cx| {
10139 let existing_window_and_workspace: Option<(
10140 WindowHandle<MultiWorkspace>,
10141 Entity<Workspace>,
10142 )> = windows.into_iter().find_map(|window_handle| {
10143 window_handle
10144 .downcast::<MultiWorkspace>()
10145 .and_then(|window_handle| {
10146 window_handle
10147 .update(cx, |multi_workspace, _window, cx| {
10148 multi_workspace
10149 .workspaces()
10150 .find(|workspace| {
10151 workspace.read(cx).project().read(cx).remote_id()
10152 == Some(project_id)
10153 })
10154 .map(|workspace| (window_handle, workspace.clone()))
10155 })
10156 .unwrap_or(None)
10157 })
10158 });
10159
10160 let multi_workspace_window = if let Some((existing_window, target_workspace)) =
10161 existing_window_and_workspace
10162 {
10163 existing_window
10164 .update(cx, |multi_workspace, window, cx| {
10165 multi_workspace.activate(target_workspace, None, window, cx);
10166 })
10167 .ok();
10168 existing_window
10169 } else {
10170 let active_call = cx.update(|cx| GlobalAnyActiveCall::global(cx).clone());
10171 let project = cx
10172 .update(|cx| {
10173 active_call.0.join_project(
10174 project_id,
10175 app_state.languages.clone(),
10176 app_state.fs.clone(),
10177 cx,
10178 )
10179 })
10180 .await?;
10181
10182 let window_bounds_override = window_bounds_env_override();
10183 cx.update(|cx| {
10184 let mut options = (app_state.build_window_options)(None, cx);
10185 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
10186 cx.open_window(options, |window, cx| {
10187 let workspace = cx.new(|cx| {
10188 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
10189 });
10190 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
10191 })
10192 })?
10193 };
10194
10195 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
10196 cx.activate(true);
10197 window.activate_window();
10198
10199 // We set the active workspace above, so this is the correct workspace.
10200 let workspace = multi_workspace.workspace().clone();
10201 workspace.update(cx, |workspace, cx| {
10202 let follow_peer_id = GlobalAnyActiveCall::try_global(cx)
10203 .and_then(|call| call.0.peer_id_for_user_in_room(follow_user_id, cx))
10204 .or_else(|| {
10205 // If we couldn't follow the given user, follow the host instead.
10206 let collaborator = workspace
10207 .project()
10208 .read(cx)
10209 .collaborators()
10210 .values()
10211 .find(|collaborator| collaborator.is_host)?;
10212 Some(collaborator.peer_id)
10213 });
10214
10215 if let Some(follow_peer_id) = follow_peer_id {
10216 workspace.follow(follow_peer_id, window, cx);
10217 }
10218 });
10219 })?;
10220
10221 anyhow::Ok(())
10222 })
10223}
10224
10225pub fn reload(cx: &mut App) {
10226 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
10227 let mut workspace_windows = cx
10228 .windows()
10229 .into_iter()
10230 .filter_map(|window| window.downcast::<MultiWorkspace>())
10231 .collect::<Vec<_>>();
10232
10233 // If multiple windows have unsaved changes, and need a save prompt,
10234 // prompt in the active window before switching to a different window.
10235 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
10236
10237 let mut prompt = None;
10238 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
10239 prompt = window
10240 .update(cx, |_, window, cx| {
10241 window.prompt(
10242 PromptLevel::Info,
10243 "Are you sure you want to restart?",
10244 None,
10245 &["Restart", "Cancel"],
10246 cx,
10247 )
10248 })
10249 .ok();
10250 }
10251
10252 cx.spawn(async move |cx| {
10253 if let Some(prompt) = prompt {
10254 let answer = prompt.await?;
10255 if answer != 0 {
10256 return anyhow::Ok(());
10257 }
10258 }
10259
10260 // If the user cancels any save prompt, then keep the app open.
10261 for window in workspace_windows {
10262 if let Ok(should_close) = window.update(cx, |multi_workspace, window, cx| {
10263 let workspace = multi_workspace.workspace().clone();
10264 workspace.update(cx, |workspace, cx| {
10265 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
10266 })
10267 }) && !should_close.await?
10268 {
10269 return anyhow::Ok(());
10270 }
10271 }
10272 cx.update(|cx| cx.restart());
10273 anyhow::Ok(())
10274 })
10275 .detach_and_log_err(cx);
10276}
10277
10278fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
10279 let mut parts = value.split(',');
10280 let x: usize = parts.next()?.parse().ok()?;
10281 let y: usize = parts.next()?.parse().ok()?;
10282 Some(point(px(x as f32), px(y as f32)))
10283}
10284
10285fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
10286 let mut parts = value.split(',');
10287 let width: usize = parts.next()?.parse().ok()?;
10288 let height: usize = parts.next()?.parse().ok()?;
10289 Some(size(px(width as f32), px(height as f32)))
10290}
10291
10292/// Add client-side decorations (rounded corners, shadows, resize handling) when
10293/// appropriate.
10294///
10295/// The `border_radius_tiling` parameter allows overriding which corners get
10296/// rounded, independently of the actual window tiling state. This is used
10297/// specifically for the workspace switcher sidebar: when the sidebar is open,
10298/// we want square corners on the left (so the sidebar appears flush with the
10299/// window edge) but we still need the shadow padding for proper visual
10300/// appearance. Unlike actual window tiling, this only affects border radius -
10301/// not padding or shadows.
10302pub fn client_side_decorations(
10303 element: impl IntoElement,
10304 window: &mut Window,
10305 cx: &mut App,
10306 border_radius_tiling: Tiling,
10307) -> Stateful<Div> {
10308 const BORDER_SIZE: Pixels = px(1.0);
10309 let decorations = window.window_decorations();
10310 let tiling = match decorations {
10311 Decorations::Server => Tiling::default(),
10312 Decorations::Client { tiling } => tiling,
10313 };
10314
10315 match decorations {
10316 Decorations::Client { .. } => window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW),
10317 Decorations::Server => window.set_client_inset(px(0.0)),
10318 }
10319
10320 struct GlobalResizeEdge(ResizeEdge);
10321 impl Global for GlobalResizeEdge {}
10322
10323 div()
10324 .id("window-backdrop")
10325 .bg(transparent_black())
10326 .map(|div| match decorations {
10327 Decorations::Server => div,
10328 Decorations::Client { .. } => div
10329 .when(
10330 !(tiling.top
10331 || tiling.right
10332 || border_radius_tiling.top
10333 || border_radius_tiling.right),
10334 |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10335 )
10336 .when(
10337 !(tiling.top
10338 || tiling.left
10339 || border_radius_tiling.top
10340 || border_radius_tiling.left),
10341 |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10342 )
10343 .when(
10344 !(tiling.bottom
10345 || tiling.right
10346 || border_radius_tiling.bottom
10347 || border_radius_tiling.right),
10348 |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10349 )
10350 .when(
10351 !(tiling.bottom
10352 || tiling.left
10353 || border_radius_tiling.bottom
10354 || border_radius_tiling.left),
10355 |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10356 )
10357 .when(!tiling.top, |div| {
10358 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
10359 })
10360 .when(!tiling.bottom, |div| {
10361 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
10362 })
10363 .when(!tiling.left, |div| {
10364 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
10365 })
10366 .when(!tiling.right, |div| {
10367 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
10368 })
10369 .on_mouse_move(move |e, window, cx| {
10370 let size = window.window_bounds().get_bounds().size;
10371 let pos = e.position;
10372
10373 let new_edge =
10374 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
10375
10376 let edge = cx.try_global::<GlobalResizeEdge>();
10377 if new_edge != edge.map(|edge| edge.0) {
10378 window
10379 .window_handle()
10380 .update(cx, |workspace, _, cx| {
10381 cx.notify(workspace.entity_id());
10382 })
10383 .ok();
10384 }
10385 })
10386 .on_mouse_down(MouseButton::Left, move |e, window, _| {
10387 let size = window.window_bounds().get_bounds().size;
10388 let pos = e.position;
10389
10390 let edge = match resize_edge(
10391 pos,
10392 theme::CLIENT_SIDE_DECORATION_SHADOW,
10393 size,
10394 tiling,
10395 ) {
10396 Some(value) => value,
10397 None => return,
10398 };
10399
10400 window.start_window_resize(edge);
10401 }),
10402 })
10403 .size_full()
10404 .child(
10405 div()
10406 .cursor(CursorStyle::Arrow)
10407 .map(|div| match decorations {
10408 Decorations::Server => div,
10409 Decorations::Client { .. } => div
10410 .border_color(cx.theme().colors().border)
10411 .when(
10412 !(tiling.top
10413 || tiling.right
10414 || border_radius_tiling.top
10415 || border_radius_tiling.right),
10416 |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10417 )
10418 .when(
10419 !(tiling.top
10420 || tiling.left
10421 || border_radius_tiling.top
10422 || border_radius_tiling.left),
10423 |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10424 )
10425 .when(
10426 !(tiling.bottom
10427 || tiling.right
10428 || border_radius_tiling.bottom
10429 || border_radius_tiling.right),
10430 |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10431 )
10432 .when(
10433 !(tiling.bottom
10434 || tiling.left
10435 || border_radius_tiling.bottom
10436 || border_radius_tiling.left),
10437 |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10438 )
10439 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
10440 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
10441 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
10442 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
10443 .when(!tiling.is_tiled(), |div| {
10444 div.shadow(vec![gpui::BoxShadow {
10445 color: Hsla {
10446 h: 0.,
10447 s: 0.,
10448 l: 0.,
10449 a: 0.4,
10450 },
10451 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
10452 spread_radius: px(0.),
10453 offset: point(px(0.0), px(0.0)),
10454 }])
10455 }),
10456 })
10457 .on_mouse_move(|_e, _, cx| {
10458 cx.stop_propagation();
10459 })
10460 .size_full()
10461 .child(element),
10462 )
10463 .map(|div| match decorations {
10464 Decorations::Server => div,
10465 Decorations::Client { tiling, .. } => div.child(
10466 canvas(
10467 |_bounds, window, _| {
10468 window.insert_hitbox(
10469 Bounds::new(
10470 point(px(0.0), px(0.0)),
10471 window.window_bounds().get_bounds().size,
10472 ),
10473 HitboxBehavior::Normal,
10474 )
10475 },
10476 move |_bounds, hitbox, window, cx| {
10477 let mouse = window.mouse_position();
10478 let size = window.window_bounds().get_bounds().size;
10479 let Some(edge) =
10480 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
10481 else {
10482 return;
10483 };
10484 cx.set_global(GlobalResizeEdge(edge));
10485 window.set_cursor_style(
10486 match edge {
10487 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
10488 ResizeEdge::Left | ResizeEdge::Right => {
10489 CursorStyle::ResizeLeftRight
10490 }
10491 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
10492 CursorStyle::ResizeUpLeftDownRight
10493 }
10494 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
10495 CursorStyle::ResizeUpRightDownLeft
10496 }
10497 },
10498 &hitbox,
10499 );
10500 },
10501 )
10502 .size_full()
10503 .absolute(),
10504 ),
10505 })
10506}
10507
10508fn resize_edge(
10509 pos: Point<Pixels>,
10510 shadow_size: Pixels,
10511 window_size: Size<Pixels>,
10512 tiling: Tiling,
10513) -> Option<ResizeEdge> {
10514 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
10515 if bounds.contains(&pos) {
10516 return None;
10517 }
10518
10519 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
10520 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
10521 if !tiling.top && top_left_bounds.contains(&pos) {
10522 return Some(ResizeEdge::TopLeft);
10523 }
10524
10525 let top_right_bounds = Bounds::new(
10526 Point::new(window_size.width - corner_size.width, px(0.)),
10527 corner_size,
10528 );
10529 if !tiling.top && top_right_bounds.contains(&pos) {
10530 return Some(ResizeEdge::TopRight);
10531 }
10532
10533 let bottom_left_bounds = Bounds::new(
10534 Point::new(px(0.), window_size.height - corner_size.height),
10535 corner_size,
10536 );
10537 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
10538 return Some(ResizeEdge::BottomLeft);
10539 }
10540
10541 let bottom_right_bounds = Bounds::new(
10542 Point::new(
10543 window_size.width - corner_size.width,
10544 window_size.height - corner_size.height,
10545 ),
10546 corner_size,
10547 );
10548 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
10549 return Some(ResizeEdge::BottomRight);
10550 }
10551
10552 if !tiling.top && pos.y < shadow_size {
10553 Some(ResizeEdge::Top)
10554 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
10555 Some(ResizeEdge::Bottom)
10556 } else if !tiling.left && pos.x < shadow_size {
10557 Some(ResizeEdge::Left)
10558 } else if !tiling.right && pos.x > window_size.width - shadow_size {
10559 Some(ResizeEdge::Right)
10560 } else {
10561 None
10562 }
10563}
10564
10565fn join_pane_into_active(
10566 active_pane: &Entity<Pane>,
10567 pane: &Entity<Pane>,
10568 window: &mut Window,
10569 cx: &mut App,
10570) {
10571 if pane == active_pane {
10572 } else if pane.read(cx).items_len() == 0 {
10573 pane.update(cx, |_, cx| {
10574 cx.emit(pane::Event::Remove {
10575 focus_on_pane: None,
10576 });
10577 })
10578 } else {
10579 move_all_items(pane, active_pane, window, cx);
10580 }
10581}
10582
10583fn move_all_items(
10584 from_pane: &Entity<Pane>,
10585 to_pane: &Entity<Pane>,
10586 window: &mut Window,
10587 cx: &mut App,
10588) {
10589 let destination_is_different = from_pane != to_pane;
10590 let mut moved_items = 0;
10591 for (item_ix, item_handle) in from_pane
10592 .read(cx)
10593 .items()
10594 .enumerate()
10595 .map(|(ix, item)| (ix, item.clone()))
10596 .collect::<Vec<_>>()
10597 {
10598 let ix = item_ix - moved_items;
10599 if destination_is_different {
10600 // Close item from previous pane
10601 from_pane.update(cx, |source, cx| {
10602 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
10603 });
10604 moved_items += 1;
10605 }
10606
10607 // This automatically removes duplicate items in the pane
10608 to_pane.update(cx, |destination, cx| {
10609 destination.add_item(item_handle, true, true, None, window, cx);
10610 window.focus(&destination.focus_handle(cx), cx)
10611 });
10612 }
10613}
10614
10615pub fn move_item(
10616 source: &Entity<Pane>,
10617 destination: &Entity<Pane>,
10618 item_id_to_move: EntityId,
10619 destination_index: usize,
10620 activate: bool,
10621 window: &mut Window,
10622 cx: &mut App,
10623) {
10624 let Some((item_ix, item_handle)) = source
10625 .read(cx)
10626 .items()
10627 .enumerate()
10628 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
10629 .map(|(ix, item)| (ix, item.clone()))
10630 else {
10631 // Tab was closed during drag
10632 return;
10633 };
10634
10635 if source != destination {
10636 // Close item from previous pane
10637 source.update(cx, |source, cx| {
10638 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
10639 });
10640 }
10641
10642 // This automatically removes duplicate items in the pane
10643 destination.update(cx, |destination, cx| {
10644 destination.add_item_inner(
10645 item_handle,
10646 activate,
10647 activate,
10648 activate,
10649 Some(destination_index),
10650 window,
10651 cx,
10652 );
10653 if activate {
10654 window.focus(&destination.focus_handle(cx), cx)
10655 }
10656 });
10657}
10658
10659pub fn move_active_item(
10660 source: &Entity<Pane>,
10661 destination: &Entity<Pane>,
10662 focus_destination: bool,
10663 close_if_empty: bool,
10664 window: &mut Window,
10665 cx: &mut App,
10666) {
10667 if source == destination {
10668 return;
10669 }
10670 let Some(active_item) = source.read(cx).active_item() else {
10671 return;
10672 };
10673 source.update(cx, |source_pane, cx| {
10674 let item_id = active_item.item_id();
10675 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
10676 destination.update(cx, |target_pane, cx| {
10677 target_pane.add_item(
10678 active_item,
10679 focus_destination,
10680 focus_destination,
10681 Some(target_pane.items_len()),
10682 window,
10683 cx,
10684 );
10685 });
10686 });
10687}
10688
10689pub fn clone_active_item(
10690 workspace_id: Option<WorkspaceId>,
10691 source: &Entity<Pane>,
10692 destination: &Entity<Pane>,
10693 focus_destination: bool,
10694 window: &mut Window,
10695 cx: &mut App,
10696) {
10697 if source == destination {
10698 return;
10699 }
10700 let Some(active_item) = source.read(cx).active_item() else {
10701 return;
10702 };
10703 if !active_item.can_split(cx) {
10704 return;
10705 }
10706 let destination = destination.downgrade();
10707 let task = active_item.clone_on_split(workspace_id, window, cx);
10708 window
10709 .spawn(cx, async move |cx| {
10710 let Some(clone) = task.await else {
10711 return;
10712 };
10713 destination
10714 .update_in(cx, |target_pane, window, cx| {
10715 target_pane.add_item(
10716 clone,
10717 focus_destination,
10718 focus_destination,
10719 Some(target_pane.items_len()),
10720 window,
10721 cx,
10722 );
10723 })
10724 .log_err();
10725 })
10726 .detach();
10727}
10728
10729#[derive(Debug)]
10730pub struct WorkspacePosition {
10731 pub window_bounds: Option<WindowBounds>,
10732 pub display: Option<Uuid>,
10733 pub centered_layout: bool,
10734}
10735
10736pub fn remote_workspace_position_from_db(
10737 connection_options: RemoteConnectionOptions,
10738 paths_to_open: &[PathBuf],
10739 cx: &App,
10740) -> Task<Result<WorkspacePosition>> {
10741 let paths = paths_to_open.to_vec();
10742 let db = WorkspaceDb::global(cx);
10743 let kvp = db::kvp::KeyValueStore::global(cx);
10744
10745 cx.background_spawn(async move {
10746 let remote_connection_id = db
10747 .get_or_create_remote_connection(connection_options)
10748 .await
10749 .context("fetching serialized ssh project")?;
10750 let serialized_workspace = db.remote_workspace_for_roots(&paths, remote_connection_id);
10751
10752 let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
10753 (Some(WindowBounds::Windowed(bounds)), None)
10754 } else {
10755 let restorable_bounds = serialized_workspace
10756 .as_ref()
10757 .and_then(|workspace| {
10758 Some((workspace.display?, workspace.window_bounds.map(|b| b.0)?))
10759 })
10760 .or_else(|| persistence::read_default_window_bounds(&kvp));
10761
10762 if let Some((serialized_display, serialized_bounds)) = restorable_bounds {
10763 (Some(serialized_bounds), Some(serialized_display))
10764 } else {
10765 (None, None)
10766 }
10767 };
10768
10769 let centered_layout = serialized_workspace
10770 .as_ref()
10771 .map(|w| w.centered_layout)
10772 .unwrap_or(false);
10773
10774 Ok(WorkspacePosition {
10775 window_bounds,
10776 display,
10777 centered_layout,
10778 })
10779 })
10780}
10781
10782pub fn with_active_or_new_workspace(
10783 cx: &mut App,
10784 f: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + 'static,
10785) {
10786 match cx
10787 .active_window()
10788 .and_then(|w| w.downcast::<MultiWorkspace>())
10789 {
10790 Some(multi_workspace) => {
10791 cx.defer(move |cx| {
10792 multi_workspace
10793 .update(cx, |multi_workspace, window, cx| {
10794 let workspace = multi_workspace.workspace().clone();
10795 workspace.update(cx, |workspace, cx| f(workspace, window, cx));
10796 })
10797 .log_err();
10798 });
10799 }
10800 None => {
10801 let app_state = AppState::global(cx);
10802 open_new(
10803 OpenOptions::default(),
10804 app_state,
10805 cx,
10806 move |workspace, window, cx| f(workspace, window, cx),
10807 )
10808 .detach_and_log_err(cx);
10809 }
10810 }
10811}
10812
10813/// Reads a panel's pixel size from its legacy KVP format and deletes the legacy
10814/// key. This migration path only runs once per panel per workspace.
10815fn load_legacy_panel_size(
10816 panel_key: &str,
10817 dock_position: DockPosition,
10818 workspace: &Workspace,
10819 cx: &mut App,
10820) -> Option<Pixels> {
10821 #[derive(Deserialize)]
10822 struct LegacyPanelState {
10823 #[serde(default)]
10824 width: Option<Pixels>,
10825 #[serde(default)]
10826 height: Option<Pixels>,
10827 }
10828
10829 let workspace_id = workspace
10830 .database_id()
10831 .map(|id| i64::from(id).to_string())
10832 .or_else(|| workspace.session_id())?;
10833
10834 let legacy_key = match panel_key {
10835 "ProjectPanel" => {
10836 format!("{}-{:?}", "ProjectPanel", workspace_id)
10837 }
10838 "OutlinePanel" => {
10839 format!("{}-{:?}", "OutlinePanel", workspace_id)
10840 }
10841 "GitPanel" => {
10842 format!("{}-{:?}", "GitPanel", workspace_id)
10843 }
10844 "TerminalPanel" => {
10845 format!("{:?}-{:?}", "TerminalPanel", workspace_id)
10846 }
10847 _ => return None,
10848 };
10849
10850 let kvp = db::kvp::KeyValueStore::global(cx);
10851 let json = kvp.read_kvp(&legacy_key).log_err().flatten()?;
10852 let state = serde_json::from_str::<LegacyPanelState>(&json).log_err()?;
10853 let size = match dock_position {
10854 DockPosition::Bottom => state.height,
10855 DockPosition::Left | DockPosition::Right => state.width,
10856 }?;
10857
10858 cx.background_spawn(async move { kvp.delete_kvp(legacy_key).await })
10859 .detach_and_log_err(cx);
10860
10861 Some(size)
10862}
10863
10864#[cfg(test)]
10865mod tests {
10866 use std::{cell::RefCell, rc::Rc, sync::Arc, time::Duration};
10867
10868 use super::*;
10869 use crate::{
10870 dock::{PanelEvent, test::TestPanel},
10871 item::{
10872 ItemBufferKind, ItemEvent,
10873 test::{TestItem, TestProjectItem},
10874 },
10875 };
10876 use fs::FakeFs;
10877 use gpui::{
10878 DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
10879 UpdateGlobal, VisualTestContext, px,
10880 };
10881 use project::{Project, ProjectEntryId};
10882 use serde_json::json;
10883 use settings::SettingsStore;
10884 use util::path;
10885 use util::rel_path::rel_path;
10886
10887 #[gpui::test]
10888 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
10889 init_test(cx);
10890
10891 let fs = FakeFs::new(cx.executor());
10892 let project = Project::test(fs, [], cx).await;
10893 let (workspace, cx) =
10894 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10895
10896 // Adding an item with no ambiguity renders the tab without detail.
10897 let item1 = cx.new(|cx| {
10898 let mut item = TestItem::new(cx);
10899 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
10900 item
10901 });
10902 workspace.update_in(cx, |workspace, window, cx| {
10903 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
10904 });
10905 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
10906
10907 // Adding an item that creates ambiguity increases the level of detail on
10908 // both tabs.
10909 let item2 = cx.new_window_entity(|_window, cx| {
10910 let mut item = TestItem::new(cx);
10911 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
10912 item
10913 });
10914 workspace.update_in(cx, |workspace, window, cx| {
10915 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10916 });
10917 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10918 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10919
10920 // Adding an item that creates ambiguity increases the level of detail only
10921 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
10922 // we stop at the highest detail available.
10923 let item3 = cx.new(|cx| {
10924 let mut item = TestItem::new(cx);
10925 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
10926 item
10927 });
10928 workspace.update_in(cx, |workspace, window, cx| {
10929 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
10930 });
10931 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10932 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
10933 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
10934 }
10935
10936 #[gpui::test]
10937 async fn test_tracking_active_path(cx: &mut TestAppContext) {
10938 init_test(cx);
10939
10940 let fs = FakeFs::new(cx.executor());
10941 fs.insert_tree(
10942 "/root1",
10943 json!({
10944 "one.txt": "",
10945 "two.txt": "",
10946 }),
10947 )
10948 .await;
10949 fs.insert_tree(
10950 "/root2",
10951 json!({
10952 "three.txt": "",
10953 }),
10954 )
10955 .await;
10956
10957 let project = Project::test(fs, ["root1".as_ref()], cx).await;
10958 let (workspace, cx) =
10959 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10960 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10961 let worktree_id = project.update(cx, |project, cx| {
10962 project.worktrees(cx).next().unwrap().read(cx).id()
10963 });
10964
10965 let item1 = cx.new(|cx| {
10966 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
10967 });
10968 let item2 = cx.new(|cx| {
10969 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
10970 });
10971
10972 // Add an item to an empty pane
10973 workspace.update_in(cx, |workspace, window, cx| {
10974 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
10975 });
10976 project.update(cx, |project, cx| {
10977 assert_eq!(
10978 project.active_entry(),
10979 project
10980 .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
10981 .map(|e| e.id)
10982 );
10983 });
10984 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
10985
10986 // Add a second item to a non-empty pane
10987 workspace.update_in(cx, |workspace, window, cx| {
10988 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
10989 });
10990 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
10991 project.update(cx, |project, cx| {
10992 assert_eq!(
10993 project.active_entry(),
10994 project
10995 .entry_for_path(&(worktree_id, rel_path("two.txt")).into(), cx)
10996 .map(|e| e.id)
10997 );
10998 });
10999
11000 // Close the active item
11001 pane.update_in(cx, |pane, window, cx| {
11002 pane.close_active_item(&Default::default(), window, cx)
11003 })
11004 .await
11005 .unwrap();
11006 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
11007 project.update(cx, |project, cx| {
11008 assert_eq!(
11009 project.active_entry(),
11010 project
11011 .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
11012 .map(|e| e.id)
11013 );
11014 });
11015
11016 // Add a project folder
11017 project
11018 .update(cx, |project, cx| {
11019 project.find_or_create_worktree("root2", true, cx)
11020 })
11021 .await
11022 .unwrap();
11023 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
11024
11025 // Remove a project folder
11026 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
11027 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
11028 }
11029
11030 #[gpui::test]
11031 async fn test_close_window(cx: &mut TestAppContext) {
11032 init_test(cx);
11033
11034 let fs = FakeFs::new(cx.executor());
11035 fs.insert_tree("/root", json!({ "one": "" })).await;
11036
11037 let project = Project::test(fs, ["root".as_ref()], cx).await;
11038 let (workspace, cx) =
11039 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
11040
11041 // When there are no dirty items, there's nothing to do.
11042 let item1 = cx.new(TestItem::new);
11043 workspace.update_in(cx, |w, window, cx| {
11044 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
11045 });
11046 let task = workspace.update_in(cx, |w, window, cx| {
11047 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
11048 });
11049 assert!(task.await.unwrap());
11050
11051 // When there are dirty untitled items, prompt to save each one. If the user
11052 // cancels any prompt, then abort.
11053 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
11054 let item3 = cx.new(|cx| {
11055 TestItem::new(cx)
11056 .with_dirty(true)
11057 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11058 });
11059 workspace.update_in(cx, |w, window, cx| {
11060 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
11061 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
11062 });
11063 let task = workspace.update_in(cx, |w, window, cx| {
11064 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
11065 });
11066 cx.executor().run_until_parked();
11067 cx.simulate_prompt_answer("Cancel"); // cancel save all
11068 cx.executor().run_until_parked();
11069 assert!(!cx.has_pending_prompt());
11070 assert!(!task.await.unwrap());
11071 }
11072
11073 #[gpui::test]
11074 async fn test_multi_workspace_close_window_multiple_workspaces_cancel(cx: &mut TestAppContext) {
11075 init_test(cx);
11076
11077 let fs = FakeFs::new(cx.executor());
11078 fs.insert_tree("/root", json!({ "one": "" })).await;
11079
11080 let project_a = Project::test(fs.clone(), ["root".as_ref()], cx).await;
11081 let project_b = Project::test(fs, ["root".as_ref()], cx).await;
11082 let multi_workspace_handle =
11083 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
11084 cx.run_until_parked();
11085
11086 multi_workspace_handle
11087 .update(cx, |mw, _window, cx| {
11088 mw.open_sidebar(cx);
11089 })
11090 .unwrap();
11091
11092 let workspace_a = multi_workspace_handle
11093 .read_with(cx, |mw, _| mw.workspace().clone())
11094 .unwrap();
11095
11096 let workspace_b = multi_workspace_handle
11097 .update(cx, |mw, window, cx| {
11098 mw.test_add_workspace(project_b, window, cx)
11099 })
11100 .unwrap();
11101
11102 // Activate workspace A
11103 multi_workspace_handle
11104 .update(cx, |mw, window, cx| {
11105 mw.activate(workspace_a.clone(), None, window, cx);
11106 })
11107 .unwrap();
11108
11109 let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
11110
11111 // Workspace A has a clean item
11112 let item_a = cx.new(TestItem::new);
11113 workspace_a.update_in(cx, |w, window, cx| {
11114 w.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
11115 });
11116
11117 // Workspace B has a dirty item
11118 let item_b = cx.new(|cx| TestItem::new(cx).with_dirty(true));
11119 workspace_b.update_in(cx, |w, window, cx| {
11120 w.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx)
11121 });
11122
11123 // Verify workspace A is active
11124 multi_workspace_handle
11125 .read_with(cx, |mw, _| {
11126 assert_eq!(mw.workspace(), &workspace_a);
11127 })
11128 .unwrap();
11129
11130 // Dispatch CloseWindow — workspace A will pass, workspace B will prompt
11131 multi_workspace_handle
11132 .update(cx, |mw, window, cx| {
11133 mw.close_window(&CloseWindow, window, cx);
11134 })
11135 .unwrap();
11136 cx.run_until_parked();
11137
11138 // Workspace B should now be active since it has dirty items that need attention
11139 multi_workspace_handle
11140 .read_with(cx, |mw, _| {
11141 assert_eq!(
11142 mw.workspace(),
11143 &workspace_b,
11144 "workspace B should be activated when it prompts"
11145 );
11146 })
11147 .unwrap();
11148
11149 // User cancels the save prompt from workspace B
11150 cx.simulate_prompt_answer("Cancel");
11151 cx.run_until_parked();
11152
11153 // Window should still exist because workspace B's close was cancelled
11154 assert!(
11155 multi_workspace_handle.update(cx, |_, _, _| ()).is_ok(),
11156 "window should still exist after cancelling one workspace's close"
11157 );
11158 }
11159
11160 #[gpui::test]
11161 async fn test_remove_workspace_prompts_for_unsaved_changes(cx: &mut TestAppContext) {
11162 init_test(cx);
11163
11164 let fs = FakeFs::new(cx.executor());
11165 fs.insert_tree("/root", json!({ "one": "" })).await;
11166
11167 let project_a = Project::test(fs.clone(), ["root".as_ref()], cx).await;
11168 let project_b = Project::test(fs.clone(), ["root".as_ref()], cx).await;
11169 let multi_workspace_handle =
11170 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
11171 cx.run_until_parked();
11172
11173 multi_workspace_handle
11174 .update(cx, |mw, _window, cx| mw.open_sidebar(cx))
11175 .unwrap();
11176
11177 let workspace_a = multi_workspace_handle
11178 .read_with(cx, |mw, _| mw.workspace().clone())
11179 .unwrap();
11180
11181 let workspace_b = multi_workspace_handle
11182 .update(cx, |mw, window, cx| {
11183 mw.test_add_workspace(project_b, window, cx)
11184 })
11185 .unwrap();
11186
11187 // Activate workspace A.
11188 multi_workspace_handle
11189 .update(cx, |mw, window, cx| {
11190 mw.activate(workspace_a.clone(), None, window, cx);
11191 })
11192 .unwrap();
11193
11194 let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
11195
11196 // Workspace B has a dirty item.
11197 let item_b = cx.new(|cx| TestItem::new(cx).with_dirty(true));
11198 workspace_b.update_in(cx, |w, window, cx| {
11199 w.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx)
11200 });
11201
11202 // Try to remove workspace B. It should prompt because of the dirty item.
11203 let remove_task = multi_workspace_handle
11204 .update(cx, |mw, window, cx| {
11205 mw.remove([workspace_b.clone()], |_, _, _| unreachable!(), window, cx)
11206 })
11207 .unwrap();
11208 cx.run_until_parked();
11209
11210 // The prompt should have activated workspace B.
11211 multi_workspace_handle
11212 .read_with(cx, |mw, _| {
11213 assert_eq!(
11214 mw.workspace(),
11215 &workspace_b,
11216 "workspace B should be active while prompting"
11217 );
11218 })
11219 .unwrap();
11220
11221 // Cancel the prompt — user stays on workspace B.
11222 cx.simulate_prompt_answer("Cancel");
11223 cx.run_until_parked();
11224 let removed = remove_task.await.unwrap();
11225 assert!(!removed, "removal should have been cancelled");
11226
11227 multi_workspace_handle
11228 .read_with(cx, |mw, _cx| {
11229 assert_eq!(
11230 mw.workspace(),
11231 &workspace_b,
11232 "user should stay on workspace B after cancelling"
11233 );
11234 assert_eq!(mw.workspaces().count(), 2, "both workspaces should remain");
11235 })
11236 .unwrap();
11237
11238 // Try again. This time accept the prompt.
11239 let remove_task = multi_workspace_handle
11240 .update(cx, |mw, window, cx| {
11241 // First switch back to A.
11242 mw.activate(workspace_a.clone(), None, window, cx);
11243 mw.remove([workspace_b.clone()], |_, _, _| unreachable!(), window, cx)
11244 })
11245 .unwrap();
11246 cx.run_until_parked();
11247
11248 // Accept the save prompt.
11249 cx.simulate_prompt_answer("Don't Save");
11250 cx.run_until_parked();
11251 let removed = remove_task.await.unwrap();
11252 assert!(removed, "removal should have succeeded");
11253
11254 // Should be back on workspace A, and B should be gone.
11255 multi_workspace_handle
11256 .read_with(cx, |mw, _cx| {
11257 assert_eq!(
11258 mw.workspace(),
11259 &workspace_a,
11260 "should be back on workspace A after removing B"
11261 );
11262 assert_eq!(mw.workspaces().count(), 1, "only workspace A should remain");
11263 })
11264 .unwrap();
11265 }
11266
11267 #[gpui::test]
11268 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
11269 init_test(cx);
11270
11271 // Register TestItem as a serializable item
11272 cx.update(|cx| {
11273 register_serializable_item::<TestItem>(cx);
11274 });
11275
11276 let fs = FakeFs::new(cx.executor());
11277 fs.insert_tree("/root", json!({ "one": "" })).await;
11278
11279 let project = Project::test(fs, ["root".as_ref()], cx).await;
11280 let (workspace, cx) =
11281 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
11282
11283 // When there are dirty untitled items, but they can serialize, then there is no prompt.
11284 let item1 = cx.new(|cx| {
11285 TestItem::new(cx)
11286 .with_dirty(true)
11287 .with_serialize(|| Some(Task::ready(Ok(()))))
11288 });
11289 let item2 = cx.new(|cx| {
11290 TestItem::new(cx)
11291 .with_dirty(true)
11292 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11293 .with_serialize(|| Some(Task::ready(Ok(()))))
11294 });
11295 workspace.update_in(cx, |w, window, cx| {
11296 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
11297 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
11298 });
11299 let task = workspace.update_in(cx, |w, window, cx| {
11300 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
11301 });
11302 assert!(task.await.unwrap());
11303 }
11304
11305 #[gpui::test]
11306 async fn test_close_window_with_failing_serialization(cx: &mut TestAppContext) {
11307 init_test(cx);
11308
11309 cx.update(|cx| {
11310 register_serializable_item::<TestItem>(cx);
11311 });
11312
11313 let fs = FakeFs::new(cx.executor());
11314 let project = Project::test(fs, None, cx).await;
11315 let (workspace, cx) =
11316 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
11317
11318 let item = cx.new(|cx| {
11319 TestItem::new(cx).with_dirty(true).with_serialize(|| {
11320 Some(Task::ready(Err(anyhow::anyhow!(
11321 "FOREIGN KEY constraint failed"
11322 ))))
11323 })
11324 });
11325 workspace.update_in(cx, |w, window, cx| {
11326 w.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11327 });
11328
11329 let task = workspace.update_in(cx, |w, window, cx| {
11330 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
11331 });
11332 cx.executor().run_until_parked();
11333
11334 // The failing serialization must not short-circuit the close; a
11335 // save/discard prompt must be shown for the dirty scratch item.
11336 assert!(
11337 cx.has_pending_prompt(),
11338 "a save/discard prompt should be shown for the dirty scratch item \
11339 when its serialization fails"
11340 );
11341 cx.simulate_prompt_answer("Don't Save");
11342 cx.executor().run_until_parked();
11343
11344 // Preparing to close succeeds, even though serialization failed.
11345 assert!(task.await.unwrap());
11346 }
11347
11348 #[gpui::test]
11349 async fn test_close_pane_items(cx: &mut TestAppContext) {
11350 init_test(cx);
11351
11352 let fs = FakeFs::new(cx.executor());
11353
11354 let project = Project::test(fs, None, cx).await;
11355 let (workspace, cx) =
11356 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11357
11358 let item1 = cx.new(|cx| {
11359 TestItem::new(cx)
11360 .with_dirty(true)
11361 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
11362 });
11363 let item2 = cx.new(|cx| {
11364 TestItem::new(cx)
11365 .with_dirty(true)
11366 .with_conflict(true)
11367 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
11368 });
11369 let item3 = cx.new(|cx| {
11370 TestItem::new(cx)
11371 .with_dirty(true)
11372 .with_conflict(true)
11373 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
11374 });
11375 let item4 = cx.new(|cx| {
11376 TestItem::new(cx).with_dirty(true).with_project_items(&[{
11377 let project_item = TestProjectItem::new_untitled(cx);
11378 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11379 project_item
11380 }])
11381 });
11382 let pane = workspace.update_in(cx, |workspace, window, cx| {
11383 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
11384 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
11385 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
11386 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
11387 workspace.active_pane().clone()
11388 });
11389
11390 let close_items = pane.update_in(cx, |pane, window, cx| {
11391 pane.activate_item(1, true, true, window, cx);
11392 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
11393 let item1_id = item1.item_id();
11394 let item3_id = item3.item_id();
11395 let item4_id = item4.item_id();
11396 pane.close_items(window, cx, SaveIntent::Close, &move |id| {
11397 [item1_id, item3_id, item4_id].contains(&id)
11398 })
11399 });
11400 cx.executor().run_until_parked();
11401
11402 assert!(cx.has_pending_prompt());
11403 cx.simulate_prompt_answer("Save all");
11404
11405 cx.executor().run_until_parked();
11406
11407 // Item 1 is saved. There's a prompt to save item 3.
11408 pane.update(cx, |pane, cx| {
11409 assert_eq!(item1.read(cx).save_count, 1);
11410 assert_eq!(item1.read(cx).save_as_count, 0);
11411 assert_eq!(item1.read(cx).reload_count, 0);
11412 assert_eq!(pane.items_len(), 3);
11413 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
11414 });
11415 assert!(cx.has_pending_prompt());
11416
11417 // Cancel saving item 3.
11418 cx.simulate_prompt_answer("Discard");
11419 cx.executor().run_until_parked();
11420
11421 // Item 3 is reloaded. There's a prompt to save item 4.
11422 pane.update(cx, |pane, cx| {
11423 assert_eq!(item3.read(cx).save_count, 0);
11424 assert_eq!(item3.read(cx).save_as_count, 0);
11425 assert_eq!(item3.read(cx).reload_count, 1);
11426 assert_eq!(pane.items_len(), 2);
11427 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
11428 });
11429
11430 // There's a prompt for a path for item 4.
11431 cx.simulate_new_path_selection(|_| Some(Default::default()));
11432 close_items.await.unwrap();
11433
11434 // The requested items are closed.
11435 pane.update(cx, |pane, cx| {
11436 assert_eq!(item4.read(cx).save_count, 0);
11437 assert_eq!(item4.read(cx).save_as_count, 1);
11438 assert_eq!(item4.read(cx).reload_count, 0);
11439 assert_eq!(pane.items_len(), 1);
11440 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
11441 });
11442 }
11443
11444 #[gpui::test]
11445 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
11446 init_test(cx);
11447
11448 let fs = FakeFs::new(cx.executor());
11449 let project = Project::test(fs, [], cx).await;
11450 let (workspace, cx) =
11451 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11452
11453 // Create several workspace items with single project entries, and two
11454 // workspace items with multiple project entries.
11455 let single_entry_items = (0..=4)
11456 .map(|project_entry_id| {
11457 cx.new(|cx| {
11458 TestItem::new(cx)
11459 .with_dirty(true)
11460 .with_project_items(&[dirty_project_item(
11461 project_entry_id,
11462 &format!("{project_entry_id}.txt"),
11463 cx,
11464 )])
11465 })
11466 })
11467 .collect::<Vec<_>>();
11468 let item_2_3 = cx.new(|cx| {
11469 TestItem::new(cx)
11470 .with_dirty(true)
11471 .with_buffer_kind(ItemBufferKind::Multibuffer)
11472 .with_project_items(&[
11473 single_entry_items[2].read(cx).project_items[0].clone(),
11474 single_entry_items[3].read(cx).project_items[0].clone(),
11475 ])
11476 });
11477 let item_3_4 = cx.new(|cx| {
11478 TestItem::new(cx)
11479 .with_dirty(true)
11480 .with_buffer_kind(ItemBufferKind::Multibuffer)
11481 .with_project_items(&[
11482 single_entry_items[3].read(cx).project_items[0].clone(),
11483 single_entry_items[4].read(cx).project_items[0].clone(),
11484 ])
11485 });
11486
11487 // Create two panes that contain the following project entries:
11488 // left pane:
11489 // multi-entry items: (2, 3)
11490 // single-entry items: 0, 2, 3, 4
11491 // right pane:
11492 // single-entry items: 4, 1
11493 // multi-entry items: (3, 4)
11494 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
11495 let left_pane = workspace.active_pane().clone();
11496 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
11497 workspace.add_item_to_active_pane(
11498 single_entry_items[0].boxed_clone(),
11499 None,
11500 true,
11501 window,
11502 cx,
11503 );
11504 workspace.add_item_to_active_pane(
11505 single_entry_items[2].boxed_clone(),
11506 None,
11507 true,
11508 window,
11509 cx,
11510 );
11511 workspace.add_item_to_active_pane(
11512 single_entry_items[3].boxed_clone(),
11513 None,
11514 true,
11515 window,
11516 cx,
11517 );
11518 workspace.add_item_to_active_pane(
11519 single_entry_items[4].boxed_clone(),
11520 None,
11521 true,
11522 window,
11523 cx,
11524 );
11525
11526 let right_pane =
11527 workspace.split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx);
11528
11529 let boxed_clone = single_entry_items[1].boxed_clone();
11530 let right_pane = window.spawn(cx, async move |cx| {
11531 right_pane.await.inspect(|right_pane| {
11532 right_pane
11533 .update_in(cx, |pane, window, cx| {
11534 pane.add_item(boxed_clone, true, true, None, window, cx);
11535 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
11536 })
11537 .unwrap();
11538 })
11539 });
11540
11541 (left_pane, right_pane)
11542 });
11543 let right_pane = right_pane.await.unwrap();
11544 cx.focus(&right_pane);
11545
11546 let close = right_pane.update_in(cx, |pane, window, cx| {
11547 pane.close_all_items(&CloseAllItems::default(), window, cx)
11548 .unwrap()
11549 });
11550 cx.executor().run_until_parked();
11551
11552 let msg = cx.pending_prompt().unwrap().0;
11553 assert!(msg.contains("1.txt"));
11554 assert!(!msg.contains("2.txt"));
11555 assert!(!msg.contains("3.txt"));
11556 assert!(!msg.contains("4.txt"));
11557
11558 // With best-effort close, cancelling item 1 keeps it open but items 4
11559 // and (3,4) still close since their entries exist in left pane.
11560 cx.simulate_prompt_answer("Cancel");
11561 close.await;
11562
11563 right_pane.read_with(cx, |pane, _| {
11564 assert_eq!(pane.items_len(), 1);
11565 });
11566
11567 // Remove item 3 from left pane, making (2,3) the only item with entry 3.
11568 left_pane
11569 .update_in(cx, |left_pane, window, cx| {
11570 left_pane.close_item_by_id(
11571 single_entry_items[3].entity_id(),
11572 SaveIntent::Skip,
11573 window,
11574 cx,
11575 )
11576 })
11577 .await
11578 .unwrap();
11579
11580 let close = left_pane.update_in(cx, |pane, window, cx| {
11581 pane.close_all_items(&CloseAllItems::default(), window, cx)
11582 .unwrap()
11583 });
11584 cx.executor().run_until_parked();
11585
11586 let details = cx.pending_prompt().unwrap().1;
11587 assert!(details.contains("0.txt"));
11588 assert!(details.contains("3.txt"));
11589 assert!(details.contains("4.txt"));
11590 // Ideally 2.txt wouldn't appear since entry 2 still exists in item 2.
11591 // But we can only save whole items, so saving (2,3) for entry 3 includes 2.
11592 // assert!(!details.contains("2.txt"));
11593
11594 cx.simulate_prompt_answer("Save all");
11595 cx.executor().run_until_parked();
11596 close.await;
11597
11598 left_pane.read_with(cx, |pane, _| {
11599 assert_eq!(pane.items_len(), 0);
11600 });
11601 }
11602
11603 #[gpui::test]
11604 async fn test_autosave(cx: &mut gpui::TestAppContext) {
11605 init_test(cx);
11606
11607 let fs = FakeFs::new(cx.executor());
11608 let project = Project::test(fs, [], cx).await;
11609 let (workspace, cx) =
11610 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11611 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11612
11613 let item = cx.new(|cx| {
11614 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11615 });
11616 let item_id = item.entity_id();
11617 workspace.update_in(cx, |workspace, window, cx| {
11618 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11619 });
11620
11621 // Autosave on window change.
11622 item.update(cx, |item, cx| {
11623 SettingsStore::update_global(cx, |settings, cx| {
11624 settings.update_user_settings(cx, |settings| {
11625 settings.workspace.autosave = Some(AutosaveSetting::OnWindowChange);
11626 })
11627 });
11628 item.is_dirty = true;
11629 });
11630
11631 // Deactivating the window saves the file.
11632 cx.deactivate_window();
11633 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11634
11635 // Re-activating the window doesn't save the file.
11636 cx.update(|window, _| window.activate_window());
11637 cx.executor().run_until_parked();
11638 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11639
11640 // Autosave on focus change.
11641 item.update_in(cx, |item, window, cx| {
11642 cx.focus_self(window);
11643 SettingsStore::update_global(cx, |settings, cx| {
11644 settings.update_user_settings(cx, |settings| {
11645 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11646 })
11647 });
11648 item.is_dirty = true;
11649 });
11650 // Blurring the item saves the file.
11651 item.update_in(cx, |_, window, _| window.blur());
11652 cx.executor().run_until_parked();
11653 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
11654
11655 // Deactivating the window still saves the file.
11656 item.update_in(cx, |item, window, cx| {
11657 cx.focus_self(window);
11658 item.is_dirty = true;
11659 });
11660 cx.deactivate_window();
11661 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
11662
11663 // Autosave after delay.
11664 item.update(cx, |item, cx| {
11665 SettingsStore::update_global(cx, |settings, cx| {
11666 settings.update_user_settings(cx, |settings| {
11667 settings.workspace.autosave = Some(AutosaveSetting::AfterDelay {
11668 milliseconds: 500.into(),
11669 });
11670 })
11671 });
11672 item.is_dirty = true;
11673 cx.emit(ItemEvent::Edit);
11674 });
11675
11676 // Delay hasn't fully expired, so the file is still dirty and unsaved.
11677 cx.executor().advance_clock(Duration::from_millis(250));
11678 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
11679
11680 // After delay expires, the file is saved.
11681 cx.executor().advance_clock(Duration::from_millis(250));
11682 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
11683
11684 // Autosave after delay, should save earlier than delay if tab is closed
11685 item.update(cx, |item, cx| {
11686 item.is_dirty = true;
11687 cx.emit(ItemEvent::Edit);
11688 });
11689 cx.executor().advance_clock(Duration::from_millis(250));
11690 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
11691
11692 // // Ensure auto save with delay saves the item on close, even if the timer hasn't yet run out.
11693 pane.update_in(cx, |pane, window, cx| {
11694 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11695 })
11696 .await
11697 .unwrap();
11698 assert!(!cx.has_pending_prompt());
11699 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
11700
11701 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
11702 workspace.update_in(cx, |workspace, window, cx| {
11703 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11704 });
11705 item.update_in(cx, |item, _window, cx| {
11706 item.is_dirty = true;
11707 for project_item in &mut item.project_items {
11708 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11709 }
11710 });
11711 cx.run_until_parked();
11712 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
11713
11714 // Autosave on focus change, ensuring closing the tab counts as such.
11715 item.update(cx, |item, cx| {
11716 SettingsStore::update_global(cx, |settings, cx| {
11717 settings.update_user_settings(cx, |settings| {
11718 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11719 })
11720 });
11721 item.is_dirty = true;
11722 for project_item in &mut item.project_items {
11723 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11724 }
11725 });
11726
11727 pane.update_in(cx, |pane, window, cx| {
11728 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11729 })
11730 .await
11731 .unwrap();
11732 assert!(!cx.has_pending_prompt());
11733 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11734
11735 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
11736 workspace.update_in(cx, |workspace, window, cx| {
11737 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11738 });
11739 item.update_in(cx, |item, window, cx| {
11740 item.project_items[0].update(cx, |item, _| {
11741 item.entry_id = None;
11742 });
11743 item.is_dirty = true;
11744 window.blur();
11745 });
11746 cx.run_until_parked();
11747 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11748
11749 // Ensure autosave is prevented for deleted files also when closing the buffer.
11750 let _close_items = pane.update_in(cx, |pane, window, cx| {
11751 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11752 });
11753 cx.run_until_parked();
11754 assert!(cx.has_pending_prompt());
11755 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11756 }
11757
11758 #[gpui::test]
11759 async fn test_autosave_on_focus_change_in_multibuffer(cx: &mut gpui::TestAppContext) {
11760 init_test(cx);
11761
11762 let fs = FakeFs::new(cx.executor());
11763 let project = Project::test(fs, [], cx).await;
11764 let (workspace, cx) =
11765 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11766
11767 // Create a multibuffer-like item with two child focus handles,
11768 // simulating individual buffer editors within a multibuffer.
11769 let item = cx.new(|cx| {
11770 TestItem::new(cx)
11771 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11772 .with_child_focus_handles(2, cx)
11773 });
11774 workspace.update_in(cx, |workspace, window, cx| {
11775 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11776 });
11777
11778 // Set autosave to OnFocusChange and focus the first child handle,
11779 // simulating the user's cursor being inside one of the multibuffer's excerpts.
11780 item.update_in(cx, |item, window, cx| {
11781 SettingsStore::update_global(cx, |settings, cx| {
11782 settings.update_user_settings(cx, |settings| {
11783 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11784 })
11785 });
11786 item.is_dirty = true;
11787 window.focus(&item.child_focus_handles[0], cx);
11788 });
11789 cx.executor().run_until_parked();
11790 item.read_with(cx, |item, _| assert_eq!(item.save_count, 0));
11791
11792 // Moving focus from one child to another within the same item should
11793 // NOT trigger autosave — focus is still within the item's focus hierarchy.
11794 item.update_in(cx, |item, window, cx| {
11795 window.focus(&item.child_focus_handles[1], cx);
11796 });
11797 cx.executor().run_until_parked();
11798 item.read_with(cx, |item, _| {
11799 assert_eq!(
11800 item.save_count, 0,
11801 "Switching focus between children within the same item should not autosave"
11802 );
11803 });
11804
11805 // Blurring the item saves the file. This is the core regression scenario:
11806 // with `on_blur`, this would NOT trigger because `on_blur` only fires when
11807 // the item's own focus handle is the leaf that lost focus. In a multibuffer,
11808 // the leaf is always a child focus handle, so `on_blur` never detected
11809 // focus leaving the item.
11810 item.update_in(cx, |_, window, _| window.blur());
11811 cx.executor().run_until_parked();
11812 item.read_with(cx, |item, _| {
11813 assert_eq!(
11814 item.save_count, 1,
11815 "Blurring should trigger autosave when focus was on a child of the item"
11816 );
11817 });
11818
11819 // Deactivating the window should also trigger autosave when a child of
11820 // the multibuffer item currently owns focus.
11821 item.update_in(cx, |item, window, cx| {
11822 item.is_dirty = true;
11823 window.focus(&item.child_focus_handles[0], cx);
11824 });
11825 cx.executor().run_until_parked();
11826 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11827
11828 cx.deactivate_window();
11829 item.read_with(cx, |item, _| {
11830 assert_eq!(
11831 item.save_count, 2,
11832 "Deactivating window should trigger autosave when focus was on a child"
11833 );
11834 });
11835 }
11836
11837 #[gpui::test]
11838 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
11839 init_test(cx);
11840
11841 let fs = FakeFs::new(cx.executor());
11842
11843 let project = Project::test(fs, [], cx).await;
11844 let (workspace, cx) =
11845 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11846
11847 let item = cx.new(|cx| {
11848 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11849 });
11850 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11851 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
11852 let toolbar_notify_count = Rc::new(RefCell::new(0));
11853
11854 workspace.update_in(cx, |workspace, window, cx| {
11855 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11856 let toolbar_notification_count = toolbar_notify_count.clone();
11857 cx.observe_in(&toolbar, window, move |_, _, _, _| {
11858 *toolbar_notification_count.borrow_mut() += 1
11859 })
11860 .detach();
11861 });
11862
11863 pane.read_with(cx, |pane, _| {
11864 assert!(!pane.can_navigate_backward());
11865 assert!(!pane.can_navigate_forward());
11866 });
11867
11868 item.update_in(cx, |item, _, cx| {
11869 item.set_state("one".to_string(), cx);
11870 });
11871
11872 // Toolbar must be notified to re-render the navigation buttons
11873 assert_eq!(*toolbar_notify_count.borrow(), 1);
11874
11875 pane.read_with(cx, |pane, _| {
11876 assert!(pane.can_navigate_backward());
11877 assert!(!pane.can_navigate_forward());
11878 });
11879
11880 workspace
11881 .update_in(cx, |workspace, window, cx| {
11882 workspace.go_back(pane.downgrade(), window, cx)
11883 })
11884 .await
11885 .unwrap();
11886
11887 assert_eq!(*toolbar_notify_count.borrow(), 2);
11888 pane.read_with(cx, |pane, _| {
11889 assert!(!pane.can_navigate_backward());
11890 assert!(pane.can_navigate_forward());
11891 });
11892 }
11893
11894 /// Tests that the navigation history deduplicates entries for the same item.
11895 ///
11896 /// When navigating back and forth between items (e.g., A -> B -> A -> B -> A -> B -> C),
11897 /// the navigation history deduplicates by keeping only the most recent visit to each item,
11898 /// resulting in [A, B, C] instead of [A, B, A, B, A, B, C]. This ensures that Go Back (Ctrl-O)
11899 /// navigates through unique items efficiently: C -> B -> A, rather than bouncing between
11900 /// repeated entries: C -> B -> A -> B -> A -> B -> A.
11901 ///
11902 /// This behavior prevents the navigation history from growing unnecessarily large and provides
11903 /// a better user experience by eliminating redundant navigation steps when jumping between files.
11904 #[gpui::test]
11905 async fn test_navigation_history_deduplication(cx: &mut gpui::TestAppContext) {
11906 init_test(cx);
11907
11908 let fs = FakeFs::new(cx.executor());
11909 let project = Project::test(fs, [], cx).await;
11910 let (workspace, cx) =
11911 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11912
11913 let item_a = cx.new(|cx| {
11914 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "a.txt", cx)])
11915 });
11916 let item_b = cx.new(|cx| {
11917 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "b.txt", cx)])
11918 });
11919 let item_c = cx.new(|cx| {
11920 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "c.txt", cx)])
11921 });
11922
11923 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11924
11925 workspace.update_in(cx, |workspace, window, cx| {
11926 workspace.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx);
11927 workspace.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx);
11928 workspace.add_item_to_active_pane(Box::new(item_c.clone()), None, true, window, cx);
11929 });
11930
11931 workspace.update_in(cx, |workspace, window, cx| {
11932 workspace.activate_item(&item_a, false, false, window, cx);
11933 });
11934 cx.run_until_parked();
11935
11936 workspace.update_in(cx, |workspace, window, cx| {
11937 workspace.activate_item(&item_b, false, false, window, cx);
11938 });
11939 cx.run_until_parked();
11940
11941 workspace.update_in(cx, |workspace, window, cx| {
11942 workspace.activate_item(&item_a, false, false, window, cx);
11943 });
11944 cx.run_until_parked();
11945
11946 workspace.update_in(cx, |workspace, window, cx| {
11947 workspace.activate_item(&item_b, false, false, window, cx);
11948 });
11949 cx.run_until_parked();
11950
11951 workspace.update_in(cx, |workspace, window, cx| {
11952 workspace.activate_item(&item_a, false, false, window, cx);
11953 });
11954 cx.run_until_parked();
11955
11956 workspace.update_in(cx, |workspace, window, cx| {
11957 workspace.activate_item(&item_b, false, false, window, cx);
11958 });
11959 cx.run_until_parked();
11960
11961 workspace.update_in(cx, |workspace, window, cx| {
11962 workspace.activate_item(&item_c, false, false, window, cx);
11963 });
11964 cx.run_until_parked();
11965
11966 let backward_count = pane.read_with(cx, |pane, cx| {
11967 let mut count = 0;
11968 pane.nav_history().for_each_entry(cx, &mut |_, _| {
11969 count += 1;
11970 });
11971 count
11972 });
11973 assert!(
11974 backward_count <= 4,
11975 "Should have at most 4 entries, got {}",
11976 backward_count
11977 );
11978
11979 workspace
11980 .update_in(cx, |workspace, window, cx| {
11981 workspace.go_back(pane.downgrade(), window, cx)
11982 })
11983 .await
11984 .unwrap();
11985
11986 let active_item = workspace.read_with(cx, |workspace, cx| {
11987 workspace.active_item(cx).unwrap().item_id()
11988 });
11989 assert_eq!(
11990 active_item,
11991 item_b.entity_id(),
11992 "After first go_back, should be at item B"
11993 );
11994
11995 workspace
11996 .update_in(cx, |workspace, window, cx| {
11997 workspace.go_back(pane.downgrade(), window, cx)
11998 })
11999 .await
12000 .unwrap();
12001
12002 let active_item = workspace.read_with(cx, |workspace, cx| {
12003 workspace.active_item(cx).unwrap().item_id()
12004 });
12005 assert_eq!(
12006 active_item,
12007 item_a.entity_id(),
12008 "After second go_back, should be at item A"
12009 );
12010
12011 pane.read_with(cx, |pane, _| {
12012 assert!(pane.can_navigate_forward(), "Should be able to go forward");
12013 });
12014 }
12015
12016 #[gpui::test]
12017 async fn test_activate_last_pane(cx: &mut gpui::TestAppContext) {
12018 init_test(cx);
12019 let fs = FakeFs::new(cx.executor());
12020 let project = Project::test(fs, [], cx).await;
12021 let (multi_workspace, cx) =
12022 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12023 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12024
12025 workspace.update_in(cx, |workspace, window, cx| {
12026 let first_item = cx.new(|cx| {
12027 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
12028 });
12029 workspace.add_item_to_active_pane(Box::new(first_item), None, true, window, cx);
12030 workspace.split_pane(
12031 workspace.active_pane().clone(),
12032 SplitDirection::Right,
12033 window,
12034 cx,
12035 );
12036 workspace.split_pane(
12037 workspace.active_pane().clone(),
12038 SplitDirection::Right,
12039 window,
12040 cx,
12041 );
12042 });
12043
12044 let (first_pane_id, target_last_pane_id) = workspace.update(cx, |workspace, _cx| {
12045 let panes = workspace.center.panes();
12046 assert!(panes.len() >= 2);
12047 (
12048 panes.first().expect("at least one pane").entity_id(),
12049 panes.last().expect("at least one pane").entity_id(),
12050 )
12051 });
12052
12053 workspace.update_in(cx, |workspace, window, cx| {
12054 workspace.activate_pane_at_index(&ActivatePane(0), window, cx);
12055 });
12056 workspace.update(cx, |workspace, _| {
12057 assert_eq!(workspace.active_pane().entity_id(), first_pane_id);
12058 assert_ne!(workspace.active_pane().entity_id(), target_last_pane_id);
12059 });
12060
12061 cx.dispatch_action(ActivateLastPane);
12062
12063 workspace.update(cx, |workspace, _| {
12064 assert_eq!(workspace.active_pane().entity_id(), target_last_pane_id);
12065 });
12066 }
12067
12068 #[gpui::test]
12069 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
12070 init_test(cx);
12071 let fs = FakeFs::new(cx.executor());
12072
12073 let project = Project::test(fs, [], cx).await;
12074 let (workspace, cx) =
12075 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12076
12077 let panel = workspace.update_in(cx, |workspace, window, cx| {
12078 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
12079 workspace.add_panel(panel.clone(), window, cx);
12080
12081 workspace
12082 .right_dock()
12083 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
12084
12085 panel
12086 });
12087
12088 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12089 pane.update_in(cx, |pane, window, cx| {
12090 let item = cx.new(TestItem::new);
12091 pane.add_item(Box::new(item), true, true, None, window, cx);
12092 });
12093
12094 // Transfer focus from center to panel
12095 workspace.update_in(cx, |workspace, window, cx| {
12096 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12097 });
12098
12099 workspace.update_in(cx, |workspace, window, cx| {
12100 assert!(workspace.right_dock().read(cx).is_open());
12101 assert!(!panel.is_zoomed(window, cx));
12102 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12103 });
12104
12105 // Transfer focus from panel to center
12106 workspace.update_in(cx, |workspace, window, cx| {
12107 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12108 });
12109
12110 workspace.update_in(cx, |workspace, window, cx| {
12111 assert!(workspace.right_dock().read(cx).is_open());
12112 assert!(!panel.is_zoomed(window, cx));
12113 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12114 assert!(pane.read(cx).focus_handle(cx).contains_focused(window, cx));
12115 });
12116
12117 // Close the dock
12118 workspace.update_in(cx, |workspace, window, cx| {
12119 workspace.toggle_dock(DockPosition::Right, window, cx);
12120 });
12121
12122 workspace.update_in(cx, |workspace, window, cx| {
12123 assert!(!workspace.right_dock().read(cx).is_open());
12124 assert!(!panel.is_zoomed(window, cx));
12125 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12126 assert!(pane.read(cx).focus_handle(cx).contains_focused(window, cx));
12127 });
12128
12129 // Open the dock
12130 workspace.update_in(cx, |workspace, window, cx| {
12131 workspace.toggle_dock(DockPosition::Right, window, cx);
12132 });
12133
12134 workspace.update_in(cx, |workspace, window, cx| {
12135 assert!(workspace.right_dock().read(cx).is_open());
12136 assert!(!panel.is_zoomed(window, cx));
12137 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12138 });
12139
12140 // Focus and zoom panel
12141 panel.update_in(cx, |panel, window, cx| {
12142 cx.focus_self(window);
12143 panel.set_zoomed(true, window, cx)
12144 });
12145
12146 workspace.update_in(cx, |workspace, window, cx| {
12147 assert!(workspace.right_dock().read(cx).is_open());
12148 assert!(panel.is_zoomed(window, cx));
12149 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12150 });
12151
12152 // Transfer focus to the center closes the dock
12153 workspace.update_in(cx, |workspace, window, cx| {
12154 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12155 });
12156
12157 workspace.update_in(cx, |workspace, window, cx| {
12158 assert!(!workspace.right_dock().read(cx).is_open());
12159 assert!(panel.is_zoomed(window, cx));
12160 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12161 });
12162
12163 // Transferring focus back to the panel keeps it zoomed
12164 workspace.update_in(cx, |workspace, window, cx| {
12165 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12166 });
12167
12168 workspace.update_in(cx, |workspace, window, cx| {
12169 assert!(workspace.right_dock().read(cx).is_open());
12170 assert!(panel.is_zoomed(window, cx));
12171 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12172 });
12173
12174 // Close the dock while it is zoomed
12175 workspace.update_in(cx, |workspace, window, cx| {
12176 workspace.toggle_dock(DockPosition::Right, window, cx)
12177 });
12178
12179 workspace.update_in(cx, |workspace, window, cx| {
12180 assert!(!workspace.right_dock().read(cx).is_open());
12181 assert!(panel.is_zoomed(window, cx));
12182 assert!(workspace.zoomed.is_none());
12183 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12184 });
12185
12186 // Opening the dock, when it's zoomed, retains focus
12187 workspace.update_in(cx, |workspace, window, cx| {
12188 workspace.toggle_dock(DockPosition::Right, window, cx)
12189 });
12190
12191 workspace.update_in(cx, |workspace, window, cx| {
12192 assert!(workspace.right_dock().read(cx).is_open());
12193 assert!(panel.is_zoomed(window, cx));
12194 assert!(workspace.zoomed.is_some());
12195 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12196 });
12197
12198 // Unzoom and close the panel, zoom the active pane.
12199 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
12200 workspace.update_in(cx, |workspace, window, cx| {
12201 workspace.toggle_dock(DockPosition::Right, window, cx)
12202 });
12203 pane.update_in(cx, |pane, window, cx| {
12204 pane.toggle_zoom(&Default::default(), window, cx)
12205 });
12206
12207 // Opening a dock unzooms the pane.
12208 workspace.update_in(cx, |workspace, window, cx| {
12209 workspace.toggle_dock(DockPosition::Right, window, cx)
12210 });
12211 workspace.update_in(cx, |workspace, window, cx| {
12212 let pane = pane.read(cx);
12213 assert!(!pane.is_zoomed());
12214 assert!(!pane.focus_handle(cx).is_focused(window));
12215 assert!(workspace.right_dock().read(cx).is_open());
12216 assert!(workspace.zoomed.is_none());
12217 });
12218 }
12219
12220 #[gpui::test]
12221 async fn test_close_panel_on_toggle(cx: &mut gpui::TestAppContext) {
12222 init_test(cx);
12223 let fs = FakeFs::new(cx.executor());
12224
12225 let project = Project::test(fs, [], cx).await;
12226 let (workspace, cx) =
12227 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12228
12229 let panel = workspace.update_in(cx, |workspace, window, cx| {
12230 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
12231 workspace.add_panel(panel.clone(), window, cx);
12232 panel
12233 });
12234
12235 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12236 pane.update_in(cx, |pane, window, cx| {
12237 let item = cx.new(TestItem::new);
12238 pane.add_item(Box::new(item), true, true, None, window, cx);
12239 });
12240
12241 // Enable close_panel_on_toggle
12242 cx.update_global(|store: &mut SettingsStore, cx| {
12243 store.update_user_settings(cx, |settings| {
12244 settings.workspace.close_panel_on_toggle = Some(true);
12245 });
12246 });
12247
12248 // Panel starts closed. Toggling should open and focus it.
12249 workspace.update_in(cx, |workspace, window, cx| {
12250 assert!(!workspace.right_dock().read(cx).is_open());
12251 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12252 });
12253
12254 workspace.update_in(cx, |workspace, window, cx| {
12255 assert!(
12256 workspace.right_dock().read(cx).is_open(),
12257 "Dock should be open after toggling from center"
12258 );
12259 assert!(
12260 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12261 "Panel should be focused after toggling from center"
12262 );
12263 });
12264
12265 // Panel is open and focused. Toggling should close the panel and
12266 // return focus to the center.
12267 workspace.update_in(cx, |workspace, window, cx| {
12268 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12269 });
12270
12271 workspace.update_in(cx, |workspace, window, cx| {
12272 assert!(
12273 !workspace.right_dock().read(cx).is_open(),
12274 "Dock should be closed after toggling from focused panel"
12275 );
12276 assert!(
12277 !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12278 "Panel should not be focused after toggling from focused panel"
12279 );
12280 });
12281
12282 // Open the dock and focus something else so the panel is open but not
12283 // focused. Toggling should focus the panel (not close it).
12284 workspace.update_in(cx, |workspace, window, cx| {
12285 workspace
12286 .right_dock()
12287 .update(cx, |dock, cx| dock.set_open(true, window, cx));
12288 window.focus(&pane.read(cx).focus_handle(cx), cx);
12289 });
12290
12291 workspace.update_in(cx, |workspace, window, cx| {
12292 assert!(workspace.right_dock().read(cx).is_open());
12293 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12294 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12295 });
12296
12297 workspace.update_in(cx, |workspace, window, cx| {
12298 assert!(
12299 workspace.right_dock().read(cx).is_open(),
12300 "Dock should remain open when toggling focuses an open-but-unfocused panel"
12301 );
12302 assert!(
12303 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12304 "Panel should be focused after toggling an open-but-unfocused panel"
12305 );
12306 });
12307
12308 // Now disable the setting and verify the original behavior: toggling
12309 // from a focused panel moves focus to center but leaves the dock open.
12310 cx.update_global(|store: &mut SettingsStore, cx| {
12311 store.update_user_settings(cx, |settings| {
12312 settings.workspace.close_panel_on_toggle = Some(false);
12313 });
12314 });
12315
12316 workspace.update_in(cx, |workspace, window, cx| {
12317 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12318 });
12319
12320 workspace.update_in(cx, |workspace, window, cx| {
12321 assert!(
12322 workspace.right_dock().read(cx).is_open(),
12323 "Dock should remain open when setting is disabled"
12324 );
12325 assert!(
12326 !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12327 "Panel should not be focused after toggling with setting disabled"
12328 );
12329 });
12330 }
12331
12332 #[gpui::test]
12333 async fn test_pane_zoom_in_out(cx: &mut TestAppContext) {
12334 init_test(cx);
12335 let fs = FakeFs::new(cx.executor());
12336
12337 let project = Project::test(fs, [], cx).await;
12338 let (workspace, cx) =
12339 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12340
12341 let pane = workspace.update_in(cx, |workspace, _window, _cx| {
12342 workspace.active_pane().clone()
12343 });
12344
12345 // Add an item to the pane so it can be zoomed
12346 workspace.update_in(cx, |workspace, window, cx| {
12347 let item = cx.new(TestItem::new);
12348 workspace.add_item(pane.clone(), Box::new(item), None, true, true, window, cx);
12349 });
12350
12351 // Initially not zoomed
12352 workspace.update_in(cx, |workspace, _window, cx| {
12353 assert!(!pane.read(cx).is_zoomed(), "Pane starts unzoomed");
12354 assert!(
12355 workspace.zoomed.is_none(),
12356 "Workspace should track no zoomed pane"
12357 );
12358 assert!(pane.read(cx).items_len() > 0, "Pane should have items");
12359 });
12360
12361 // Zoom In
12362 pane.update_in(cx, |pane, window, cx| {
12363 pane.zoom_in(&crate::ZoomIn, window, cx);
12364 });
12365
12366 workspace.update_in(cx, |workspace, window, cx| {
12367 assert!(
12368 pane.read(cx).is_zoomed(),
12369 "Pane should be zoomed after ZoomIn"
12370 );
12371 assert!(
12372 workspace.zoomed.is_some(),
12373 "Workspace should track the zoomed pane"
12374 );
12375 assert!(
12376 pane.read(cx).focus_handle(cx).contains_focused(window, cx),
12377 "ZoomIn should focus the pane"
12378 );
12379 });
12380
12381 // Zoom In again is a no-op
12382 pane.update_in(cx, |pane, window, cx| {
12383 pane.zoom_in(&crate::ZoomIn, window, cx);
12384 });
12385
12386 workspace.update_in(cx, |workspace, window, cx| {
12387 assert!(pane.read(cx).is_zoomed(), "Second ZoomIn keeps pane zoomed");
12388 assert!(
12389 workspace.zoomed.is_some(),
12390 "Workspace still tracks zoomed pane"
12391 );
12392 assert!(
12393 pane.read(cx).focus_handle(cx).contains_focused(window, cx),
12394 "Pane remains focused after repeated ZoomIn"
12395 );
12396 });
12397
12398 // Zoom Out
12399 pane.update_in(cx, |pane, window, cx| {
12400 pane.zoom_out(&crate::ZoomOut, window, cx);
12401 });
12402
12403 workspace.update_in(cx, |workspace, _window, cx| {
12404 assert!(
12405 !pane.read(cx).is_zoomed(),
12406 "Pane should unzoom after ZoomOut"
12407 );
12408 assert!(
12409 workspace.zoomed.is_none(),
12410 "Workspace clears zoom tracking after ZoomOut"
12411 );
12412 });
12413
12414 // Zoom Out again is a no-op
12415 pane.update_in(cx, |pane, window, cx| {
12416 pane.zoom_out(&crate::ZoomOut, window, cx);
12417 });
12418
12419 workspace.update_in(cx, |workspace, _window, cx| {
12420 assert!(
12421 !pane.read(cx).is_zoomed(),
12422 "Second ZoomOut keeps pane unzoomed"
12423 );
12424 assert!(
12425 workspace.zoomed.is_none(),
12426 "Workspace remains without zoomed pane"
12427 );
12428 });
12429 }
12430
12431 #[gpui::test]
12432 async fn test_toggle_all_docks(cx: &mut gpui::TestAppContext) {
12433 init_test(cx);
12434 let fs = FakeFs::new(cx.executor());
12435
12436 let project = Project::test(fs, [], cx).await;
12437 let (workspace, cx) =
12438 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12439 workspace.update_in(cx, |workspace, window, cx| {
12440 // Open two docks
12441 let left_dock = workspace.dock_at_position(DockPosition::Left);
12442 let right_dock = workspace.dock_at_position(DockPosition::Right);
12443
12444 left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12445 right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12446
12447 assert!(left_dock.read(cx).is_open());
12448 assert!(right_dock.read(cx).is_open());
12449 });
12450
12451 workspace.update_in(cx, |workspace, window, cx| {
12452 // Toggle all docks - should close both
12453 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12454
12455 let left_dock = workspace.dock_at_position(DockPosition::Left);
12456 let right_dock = workspace.dock_at_position(DockPosition::Right);
12457 assert!(!left_dock.read(cx).is_open());
12458 assert!(!right_dock.read(cx).is_open());
12459 });
12460
12461 workspace.update_in(cx, |workspace, window, cx| {
12462 // Toggle again - should reopen both
12463 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12464
12465 let left_dock = workspace.dock_at_position(DockPosition::Left);
12466 let right_dock = workspace.dock_at_position(DockPosition::Right);
12467 assert!(left_dock.read(cx).is_open());
12468 assert!(right_dock.read(cx).is_open());
12469 });
12470 }
12471
12472 #[gpui::test]
12473 async fn test_toggle_all_with_manual_close(cx: &mut gpui::TestAppContext) {
12474 init_test(cx);
12475 let fs = FakeFs::new(cx.executor());
12476
12477 let project = Project::test(fs, [], cx).await;
12478 let (workspace, cx) =
12479 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12480 workspace.update_in(cx, |workspace, window, cx| {
12481 // Open two docks
12482 let left_dock = workspace.dock_at_position(DockPosition::Left);
12483 let right_dock = workspace.dock_at_position(DockPosition::Right);
12484
12485 left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12486 right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12487
12488 assert!(left_dock.read(cx).is_open());
12489 assert!(right_dock.read(cx).is_open());
12490 });
12491
12492 workspace.update_in(cx, |workspace, window, cx| {
12493 // Close them manually
12494 workspace.toggle_dock(DockPosition::Left, window, cx);
12495 workspace.toggle_dock(DockPosition::Right, window, cx);
12496
12497 let left_dock = workspace.dock_at_position(DockPosition::Left);
12498 let right_dock = workspace.dock_at_position(DockPosition::Right);
12499 assert!(!left_dock.read(cx).is_open());
12500 assert!(!right_dock.read(cx).is_open());
12501 });
12502
12503 workspace.update_in(cx, |workspace, window, cx| {
12504 // Toggle all docks - only last closed (right dock) should reopen
12505 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12506
12507 let left_dock = workspace.dock_at_position(DockPosition::Left);
12508 let right_dock = workspace.dock_at_position(DockPosition::Right);
12509 assert!(!left_dock.read(cx).is_open());
12510 assert!(right_dock.read(cx).is_open());
12511 });
12512 }
12513
12514 #[gpui::test]
12515 async fn test_toggle_all_docks_after_dock_move(cx: &mut gpui::TestAppContext) {
12516 init_test(cx);
12517 let fs = FakeFs::new(cx.executor());
12518 let project = Project::test(fs, [], cx).await;
12519 let (multi_workspace, cx) =
12520 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12521 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12522
12523 // Open two docks (left and right) with one panel each
12524 let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| {
12525 let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
12526 workspace.add_panel(left_panel.clone(), window, cx);
12527
12528 let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
12529 workspace.add_panel(right_panel.clone(), window, cx);
12530
12531 workspace.toggle_dock(DockPosition::Left, window, cx);
12532 workspace.toggle_dock(DockPosition::Right, window, cx);
12533
12534 // Verify initial state
12535 assert!(
12536 workspace.left_dock().read(cx).is_open(),
12537 "Left dock should be open"
12538 );
12539 assert_eq!(
12540 workspace
12541 .left_dock()
12542 .read(cx)
12543 .visible_panel()
12544 .unwrap()
12545 .panel_id(),
12546 left_panel.panel_id(),
12547 "Left panel should be visible in left dock"
12548 );
12549 assert!(
12550 workspace.right_dock().read(cx).is_open(),
12551 "Right dock should be open"
12552 );
12553 assert_eq!(
12554 workspace
12555 .right_dock()
12556 .read(cx)
12557 .visible_panel()
12558 .unwrap()
12559 .panel_id(),
12560 right_panel.panel_id(),
12561 "Right panel should be visible in right dock"
12562 );
12563 assert!(
12564 !workspace.bottom_dock().read(cx).is_open(),
12565 "Bottom dock should be closed"
12566 );
12567
12568 (left_panel, right_panel)
12569 });
12570
12571 // Focus the left panel and move it to the next position (bottom dock)
12572 workspace.update_in(cx, |workspace, window, cx| {
12573 workspace.toggle_panel_focus::<TestPanel>(window, cx); // Focus left panel
12574 assert!(
12575 left_panel.read(cx).focus_handle(cx).is_focused(window),
12576 "Left panel should be focused"
12577 );
12578 });
12579
12580 cx.dispatch_action(MoveFocusedPanelToNextPosition);
12581
12582 // Verify the left panel has moved to the bottom dock, and the bottom dock is now open
12583 workspace.update(cx, |workspace, cx| {
12584 assert!(
12585 !workspace.left_dock().read(cx).is_open(),
12586 "Left dock should be closed"
12587 );
12588 assert!(
12589 workspace.bottom_dock().read(cx).is_open(),
12590 "Bottom dock should now be open"
12591 );
12592 assert_eq!(
12593 left_panel.read(cx).position,
12594 DockPosition::Bottom,
12595 "Left panel should now be in the bottom dock"
12596 );
12597 assert_eq!(
12598 workspace
12599 .bottom_dock()
12600 .read(cx)
12601 .visible_panel()
12602 .unwrap()
12603 .panel_id(),
12604 left_panel.panel_id(),
12605 "Left panel should be the visible panel in the bottom dock"
12606 );
12607 });
12608
12609 // Toggle all docks off
12610 workspace.update_in(cx, |workspace, window, cx| {
12611 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12612 assert!(
12613 !workspace.left_dock().read(cx).is_open(),
12614 "Left dock should be closed"
12615 );
12616 assert!(
12617 !workspace.right_dock().read(cx).is_open(),
12618 "Right dock should be closed"
12619 );
12620 assert!(
12621 !workspace.bottom_dock().read(cx).is_open(),
12622 "Bottom dock should be closed"
12623 );
12624 });
12625
12626 // Toggle all docks back on and verify positions are restored
12627 workspace.update_in(cx, |workspace, window, cx| {
12628 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12629 assert!(
12630 !workspace.left_dock().read(cx).is_open(),
12631 "Left dock should remain closed"
12632 );
12633 assert!(
12634 workspace.right_dock().read(cx).is_open(),
12635 "Right dock should remain open"
12636 );
12637 assert!(
12638 workspace.bottom_dock().read(cx).is_open(),
12639 "Bottom dock should remain open"
12640 );
12641 assert_eq!(
12642 left_panel.read(cx).position,
12643 DockPosition::Bottom,
12644 "Left panel should remain in the bottom dock"
12645 );
12646 assert_eq!(
12647 right_panel.read(cx).position,
12648 DockPosition::Right,
12649 "Right panel should remain in the right dock"
12650 );
12651 assert_eq!(
12652 workspace
12653 .bottom_dock()
12654 .read(cx)
12655 .visible_panel()
12656 .unwrap()
12657 .panel_id(),
12658 left_panel.panel_id(),
12659 "Left panel should be the visible panel in the right dock"
12660 );
12661 });
12662 }
12663
12664 #[gpui::test]
12665 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
12666 init_test(cx);
12667
12668 let fs = FakeFs::new(cx.executor());
12669
12670 let project = Project::test(fs, None, cx).await;
12671 let (workspace, cx) =
12672 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12673
12674 // Let's arrange the panes like this:
12675 //
12676 // +-----------------------+
12677 // | top |
12678 // +------+--------+-------+
12679 // | left | center | right |
12680 // +------+--------+-------+
12681 // | bottom |
12682 // +-----------------------+
12683
12684 let top_item = cx.new(|cx| {
12685 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
12686 });
12687 let bottom_item = cx.new(|cx| {
12688 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
12689 });
12690 let left_item = cx.new(|cx| {
12691 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
12692 });
12693 let right_item = cx.new(|cx| {
12694 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
12695 });
12696 let center_item = cx.new(|cx| {
12697 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
12698 });
12699
12700 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12701 let top_pane_id = workspace.active_pane().entity_id();
12702 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
12703 workspace.split_pane(
12704 workspace.active_pane().clone(),
12705 SplitDirection::Down,
12706 window,
12707 cx,
12708 );
12709 top_pane_id
12710 });
12711 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12712 let bottom_pane_id = workspace.active_pane().entity_id();
12713 workspace.add_item_to_active_pane(
12714 Box::new(bottom_item.clone()),
12715 None,
12716 false,
12717 window,
12718 cx,
12719 );
12720 workspace.split_pane(
12721 workspace.active_pane().clone(),
12722 SplitDirection::Up,
12723 window,
12724 cx,
12725 );
12726 bottom_pane_id
12727 });
12728 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12729 let left_pane_id = workspace.active_pane().entity_id();
12730 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
12731 workspace.split_pane(
12732 workspace.active_pane().clone(),
12733 SplitDirection::Right,
12734 window,
12735 cx,
12736 );
12737 left_pane_id
12738 });
12739 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12740 let right_pane_id = workspace.active_pane().entity_id();
12741 workspace.add_item_to_active_pane(
12742 Box::new(right_item.clone()),
12743 None,
12744 false,
12745 window,
12746 cx,
12747 );
12748 workspace.split_pane(
12749 workspace.active_pane().clone(),
12750 SplitDirection::Left,
12751 window,
12752 cx,
12753 );
12754 right_pane_id
12755 });
12756 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12757 let center_pane_id = workspace.active_pane().entity_id();
12758 workspace.add_item_to_active_pane(
12759 Box::new(center_item.clone()),
12760 None,
12761 false,
12762 window,
12763 cx,
12764 );
12765 center_pane_id
12766 });
12767 cx.executor().run_until_parked();
12768
12769 workspace.update_in(cx, |workspace, window, cx| {
12770 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
12771
12772 // Join into next from center pane into right
12773 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12774 });
12775
12776 workspace.update_in(cx, |workspace, window, cx| {
12777 let active_pane = workspace.active_pane();
12778 assert_eq!(right_pane_id, active_pane.entity_id());
12779 assert_eq!(2, active_pane.read(cx).items_len());
12780 let item_ids_in_pane =
12781 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12782 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12783 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12784
12785 // Join into next from right pane into bottom
12786 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12787 });
12788
12789 workspace.update_in(cx, |workspace, window, cx| {
12790 let active_pane = workspace.active_pane();
12791 assert_eq!(bottom_pane_id, active_pane.entity_id());
12792 assert_eq!(3, active_pane.read(cx).items_len());
12793 let item_ids_in_pane =
12794 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12795 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12796 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12797 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12798
12799 // Join into next from bottom pane into left
12800 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12801 });
12802
12803 workspace.update_in(cx, |workspace, window, cx| {
12804 let active_pane = workspace.active_pane();
12805 assert_eq!(left_pane_id, active_pane.entity_id());
12806 assert_eq!(4, active_pane.read(cx).items_len());
12807 let item_ids_in_pane =
12808 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12809 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12810 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12811 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12812 assert!(item_ids_in_pane.contains(&left_item.item_id()));
12813
12814 // Join into next from left pane into top
12815 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12816 });
12817
12818 workspace.update_in(cx, |workspace, window, cx| {
12819 let active_pane = workspace.active_pane();
12820 assert_eq!(top_pane_id, active_pane.entity_id());
12821 assert_eq!(5, active_pane.read(cx).items_len());
12822 let item_ids_in_pane =
12823 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12824 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12825 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12826 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12827 assert!(item_ids_in_pane.contains(&left_item.item_id()));
12828 assert!(item_ids_in_pane.contains(&top_item.item_id()));
12829
12830 // Single pane left: no-op
12831 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
12832 });
12833
12834 workspace.update(cx, |workspace, _cx| {
12835 let active_pane = workspace.active_pane();
12836 assert_eq!(top_pane_id, active_pane.entity_id());
12837 });
12838 }
12839
12840 fn add_an_item_to_active_pane(
12841 cx: &mut VisualTestContext,
12842 workspace: &Entity<Workspace>,
12843 item_id: u64,
12844 ) -> Entity<TestItem> {
12845 let item = cx.new(|cx| {
12846 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
12847 item_id,
12848 "item{item_id}.txt",
12849 cx,
12850 )])
12851 });
12852 workspace.update_in(cx, |workspace, window, cx| {
12853 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
12854 });
12855 item
12856 }
12857
12858 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
12859 workspace.update_in(cx, |workspace, window, cx| {
12860 workspace.split_pane(
12861 workspace.active_pane().clone(),
12862 SplitDirection::Right,
12863 window,
12864 cx,
12865 )
12866 })
12867 }
12868
12869 #[gpui::test]
12870 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
12871 init_test(cx);
12872 let fs = FakeFs::new(cx.executor());
12873 let project = Project::test(fs, None, cx).await;
12874 let (workspace, cx) =
12875 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12876
12877 add_an_item_to_active_pane(cx, &workspace, 1);
12878 split_pane(cx, &workspace);
12879 add_an_item_to_active_pane(cx, &workspace, 2);
12880 split_pane(cx, &workspace); // empty pane
12881 split_pane(cx, &workspace);
12882 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
12883
12884 cx.executor().run_until_parked();
12885
12886 workspace.update(cx, |workspace, cx| {
12887 let num_panes = workspace.panes().len();
12888 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
12889 let active_item = workspace
12890 .active_pane()
12891 .read(cx)
12892 .active_item()
12893 .expect("item is in focus");
12894
12895 assert_eq!(num_panes, 4);
12896 assert_eq!(num_items_in_current_pane, 1);
12897 assert_eq!(active_item.item_id(), last_item.item_id());
12898 });
12899
12900 workspace.update_in(cx, |workspace, window, cx| {
12901 workspace.join_all_panes(window, cx);
12902 });
12903
12904 workspace.update(cx, |workspace, cx| {
12905 let num_panes = workspace.panes().len();
12906 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
12907 let active_item = workspace
12908 .active_pane()
12909 .read(cx)
12910 .active_item()
12911 .expect("item is in focus");
12912
12913 assert_eq!(num_panes, 1);
12914 assert_eq!(num_items_in_current_pane, 3);
12915 assert_eq!(active_item.item_id(), last_item.item_id());
12916 });
12917 }
12918
12919 #[gpui::test]
12920 async fn test_flexible_dock_sizing(cx: &mut gpui::TestAppContext) {
12921 init_test(cx);
12922 let fs = FakeFs::new(cx.executor());
12923
12924 let project = Project::test(fs, [], cx).await;
12925 let (multi_workspace, cx) =
12926 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12927 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12928
12929 workspace.update(cx, |workspace, _cx| {
12930 workspace.set_random_database_id();
12931 });
12932
12933 workspace.update_in(cx, |workspace, window, cx| {
12934 let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
12935 workspace.add_panel(panel.clone(), window, cx);
12936 workspace.toggle_dock(DockPosition::Right, window, cx);
12937
12938 let right_dock = workspace.right_dock().clone();
12939 right_dock.update(cx, |dock, cx| {
12940 dock.set_panel_size_state(
12941 &panel,
12942 dock::PanelSizeState {
12943 size: None,
12944 flex: Some(1.0),
12945 },
12946 cx,
12947 );
12948 });
12949 });
12950
12951 workspace.update_in(cx, |workspace, window, cx| {
12952 let item = cx.new(|cx| {
12953 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
12954 });
12955 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
12956 workspace.bounds.size.width = px(1920.);
12957
12958 let dock = workspace.right_dock().read(cx);
12959 let initial_width = workspace
12960 .dock_size(&dock, window, cx)
12961 .expect("flexible dock should have an initial width");
12962
12963 assert_eq!(initial_width, px(960.));
12964 });
12965
12966 workspace.update_in(cx, |workspace, window, cx| {
12967 workspace.split_pane(
12968 workspace.active_pane().clone(),
12969 SplitDirection::Right,
12970 window,
12971 cx,
12972 );
12973
12974 let center_column_count = workspace.center.full_height_column_count();
12975 assert_eq!(center_column_count, 2);
12976
12977 let dock = workspace.right_dock().read(cx);
12978 assert_eq!(workspace.dock_size(&dock, window, cx).unwrap(), px(640.));
12979
12980 workspace.bounds.size.width = px(2400.);
12981
12982 let dock = workspace.right_dock().read(cx);
12983 assert_eq!(workspace.dock_size(&dock, window, cx).unwrap(), px(800.));
12984 });
12985 }
12986
12987 #[gpui::test]
12988 async fn test_panel_size_state_persistence(cx: &mut gpui::TestAppContext) {
12989 init_test(cx);
12990 let fs = FakeFs::new(cx.executor());
12991
12992 // Fixed-width panel: pixel size is persisted to KVP and restored on re-add.
12993 {
12994 let project = Project::test(fs.clone(), [], cx).await;
12995 let (multi_workspace, cx) =
12996 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12997 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12998
12999 workspace.update(cx, |workspace, _cx| {
13000 workspace.set_random_database_id();
13001 workspace.bounds.size.width = px(800.);
13002 });
13003
13004 let panel = workspace.update_in(cx, |workspace, window, cx| {
13005 let panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
13006 workspace.add_panel(panel.clone(), window, cx);
13007 workspace.toggle_dock(DockPosition::Left, window, cx);
13008 panel
13009 });
13010
13011 workspace.update_in(cx, |workspace, window, cx| {
13012 workspace.resize_left_dock(px(350.), window, cx);
13013 });
13014
13015 cx.run_until_parked();
13016
13017 let persisted = workspace.read_with(cx, |workspace, cx| {
13018 workspace.persisted_panel_size_state(TestPanel::panel_key(), cx)
13019 });
13020 assert_eq!(
13021 persisted.and_then(|s| s.size),
13022 Some(px(350.)),
13023 "fixed-width panel size should be persisted to KVP"
13024 );
13025
13026 // Remove the panel and re-add a fresh instance with the same key.
13027 // The new instance should have its size state restored from KVP.
13028 workspace.update_in(cx, |workspace, window, cx| {
13029 workspace.remove_panel(&panel, window, cx);
13030 });
13031
13032 workspace.update_in(cx, |workspace, window, cx| {
13033 let new_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
13034 workspace.add_panel(new_panel, window, cx);
13035
13036 let left_dock = workspace.left_dock().read(cx);
13037 let size_state = left_dock
13038 .panel::<TestPanel>()
13039 .and_then(|p| left_dock.stored_panel_size_state(&p));
13040 assert_eq!(
13041 size_state.and_then(|s| s.size),
13042 Some(px(350.)),
13043 "re-added fixed-width panel should restore persisted size from KVP"
13044 );
13045 });
13046 }
13047
13048 // Flexible panel: both pixel size and ratio are persisted and restored.
13049 {
13050 let project = Project::test(fs.clone(), [], cx).await;
13051 let (multi_workspace, cx) =
13052 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
13053 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
13054
13055 workspace.update(cx, |workspace, _cx| {
13056 workspace.set_random_database_id();
13057 workspace.bounds.size.width = px(800.);
13058 });
13059
13060 let panel = workspace.update_in(cx, |workspace, window, cx| {
13061 let item = cx.new(|cx| {
13062 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
13063 });
13064 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
13065
13066 let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
13067 workspace.add_panel(panel.clone(), window, cx);
13068 workspace.toggle_dock(DockPosition::Right, window, cx);
13069 panel
13070 });
13071
13072 workspace.update_in(cx, |workspace, window, cx| {
13073 workspace.resize_right_dock(px(300.), window, cx);
13074 });
13075
13076 cx.run_until_parked();
13077
13078 let persisted = workspace
13079 .read_with(cx, |workspace, cx| {
13080 workspace.persisted_panel_size_state(TestPanel::panel_key(), cx)
13081 })
13082 .expect("flexible panel state should be persisted to KVP");
13083 assert_eq!(
13084 persisted.size, None,
13085 "flexible panel should not persist a redundant pixel size"
13086 );
13087 let original_ratio = persisted.flex.expect("panel's flex should be persisted");
13088
13089 // Remove the panel and re-add: both size and ratio should be restored.
13090 workspace.update_in(cx, |workspace, window, cx| {
13091 workspace.remove_panel(&panel, window, cx);
13092 });
13093
13094 workspace.update_in(cx, |workspace, window, cx| {
13095 let new_panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
13096 workspace.add_panel(new_panel, window, cx);
13097
13098 let right_dock = workspace.right_dock().read(cx);
13099 let size_state = right_dock
13100 .panel::<TestPanel>()
13101 .and_then(|p| right_dock.stored_panel_size_state(&p))
13102 .expect("re-added flexible panel should have restored size state from KVP");
13103 assert_eq!(
13104 size_state.size, None,
13105 "re-added flexible panel should not have a persisted pixel size"
13106 );
13107 assert_eq!(
13108 size_state.flex,
13109 Some(original_ratio),
13110 "re-added flexible panel should restore persisted flex"
13111 );
13112 });
13113 }
13114 }
13115
13116 #[gpui::test]
13117 async fn test_flexible_panel_left_dock_sizing(cx: &mut gpui::TestAppContext) {
13118 init_test(cx);
13119 let fs = FakeFs::new(cx.executor());
13120
13121 let project = Project::test(fs, [], cx).await;
13122 let (multi_workspace, cx) =
13123 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
13124 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
13125
13126 workspace.update(cx, |workspace, _cx| {
13127 workspace.bounds.size.width = px(900.);
13128 });
13129
13130 // Step 1: Add a tab to the center pane then open a flexible panel in the left
13131 // dock. With one full-width center pane the default ratio is 0.5, so the panel
13132 // and the center pane each take half the workspace width.
13133 workspace.update_in(cx, |workspace, window, cx| {
13134 let item = cx.new(|cx| {
13135 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
13136 });
13137 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
13138
13139 let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Left, 100, cx));
13140 workspace.add_panel(panel, window, cx);
13141 workspace.toggle_dock(DockPosition::Left, window, cx);
13142
13143 let left_dock = workspace.left_dock().read(cx);
13144 let left_width = workspace
13145 .dock_size(&left_dock, window, cx)
13146 .expect("left dock should have an active panel");
13147
13148 assert_eq!(
13149 left_width,
13150 workspace.bounds.size.width / 2.,
13151 "flexible left panel should split evenly with the center pane"
13152 );
13153 });
13154
13155 // Step 2: Split the center pane left/right. The flexible panel is treated as one
13156 // average center column, so with two center columns it should take one third of
13157 // the workspace width.
13158 workspace.update_in(cx, |workspace, window, cx| {
13159 workspace.split_pane(
13160 workspace.active_pane().clone(),
13161 SplitDirection::Right,
13162 window,
13163 cx,
13164 );
13165
13166 let left_dock = workspace.left_dock().read(cx);
13167 let left_width = workspace
13168 .dock_size(&left_dock, window, cx)
13169 .expect("left dock should still have an active panel after horizontal split");
13170
13171 assert_eq!(
13172 left_width,
13173 workspace.bounds.size.width / 3.,
13174 "flexible left panel width should match the average center column width"
13175 );
13176 });
13177
13178 // Step 3: Split the active center pane vertically (top/bottom). Vertical splits do
13179 // not change the number of center columns, so the flexible panel width stays the same.
13180 workspace.update_in(cx, |workspace, window, cx| {
13181 workspace.split_pane(
13182 workspace.active_pane().clone(),
13183 SplitDirection::Down,
13184 window,
13185 cx,
13186 );
13187
13188 let left_dock = workspace.left_dock().read(cx);
13189 let left_width = workspace
13190 .dock_size(&left_dock, window, cx)
13191 .expect("left dock should still have an active panel after vertical split");
13192
13193 assert_eq!(
13194 left_width,
13195 workspace.bounds.size.width / 3.,
13196 "flexible left panel width should still match the average center column width"
13197 );
13198 });
13199
13200 // Step 4: Open a fixed-width panel in the right dock. The right dock's default
13201 // size reduces the available width, so the flexible left panel keeps matching one
13202 // average center column within the remaining space.
13203 workspace.update_in(cx, |workspace, window, cx| {
13204 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 200, cx));
13205 workspace.add_panel(panel, window, cx);
13206 workspace.toggle_dock(DockPosition::Right, window, cx);
13207
13208 let right_dock = workspace.right_dock().read(cx);
13209 let right_width = workspace
13210 .dock_size(&right_dock, window, cx)
13211 .expect("right dock should have an active panel");
13212
13213 let left_dock = workspace.left_dock().read(cx);
13214 let left_width = workspace
13215 .dock_size(&left_dock, window, cx)
13216 .expect("left dock should still have an active panel");
13217
13218 let available_width = workspace.bounds.size.width - right_width;
13219 assert_eq!(
13220 left_width,
13221 available_width / 3.,
13222 "flexible left panel should keep matching one average center column"
13223 );
13224 });
13225
13226 // Step 5: Toggle the right dock's panel to flexible. Now both docks use
13227 // column-equivalent flex sizing and the workspace width is divided among
13228 // left-flex, two center columns, and right-flex.
13229 workspace.update_in(cx, |workspace, window, cx| {
13230 let right_dock = workspace.right_dock().clone();
13231 let right_panel = right_dock
13232 .read(cx)
13233 .visible_panel()
13234 .expect("right dock should have a visible panel")
13235 .clone();
13236 workspace.toggle_dock_panel_flexible_size(
13237 &right_dock,
13238 right_panel.as_ref(),
13239 window,
13240 cx,
13241 );
13242
13243 let right_dock = right_dock.read(cx);
13244 let right_panel = right_dock
13245 .visible_panel()
13246 .expect("right dock should still have a visible panel");
13247 assert!(
13248 right_panel.has_flexible_size(window, cx),
13249 "right panel should now be flexible"
13250 );
13251
13252 let right_size_state = right_dock
13253 .stored_panel_size_state(right_panel.as_ref())
13254 .expect("right panel should have a stored size state after toggling");
13255 let right_flex = right_size_state
13256 .flex
13257 .expect("right panel should have a flex value after toggling");
13258
13259 let left_dock = workspace.left_dock().read(cx);
13260 let left_width = workspace
13261 .dock_size(&left_dock, window, cx)
13262 .expect("left dock should still have an active panel");
13263 let right_width = workspace
13264 .dock_size(&right_dock, window, cx)
13265 .expect("right dock should still have an active panel");
13266
13267 let left_flex = workspace
13268 .default_dock_flex(DockPosition::Left)
13269 .expect("left dock should have a default flex");
13270 let center_column_count = workspace.center.full_height_column_count() as f32;
13271
13272 let total_flex = left_flex + center_column_count + right_flex;
13273 let expected_left = left_flex / total_flex * workspace.bounds.size.width;
13274 let expected_right = right_flex / total_flex * workspace.bounds.size.width;
13275 assert_eq!(
13276 left_width, expected_left,
13277 "flexible left panel should share workspace width via flex ratios"
13278 );
13279 assert_eq!(
13280 right_width, expected_right,
13281 "flexible right panel should share workspace width via flex ratios"
13282 );
13283 });
13284 }
13285
13286 struct TestModal(FocusHandle);
13287
13288 impl TestModal {
13289 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
13290 Self(cx.focus_handle())
13291 }
13292 }
13293
13294 impl EventEmitter<DismissEvent> for TestModal {}
13295
13296 impl Focusable for TestModal {
13297 fn focus_handle(&self, _cx: &App) -> FocusHandle {
13298 self.0.clone()
13299 }
13300 }
13301
13302 impl ModalView for TestModal {}
13303
13304 impl Render for TestModal {
13305 fn render(
13306 &mut self,
13307 _window: &mut Window,
13308 _cx: &mut Context<TestModal>,
13309 ) -> impl IntoElement {
13310 div().track_focus(&self.0)
13311 }
13312 }
13313
13314 #[gpui::test]
13315 async fn test_panels(cx: &mut gpui::TestAppContext) {
13316 init_test(cx);
13317 let fs = FakeFs::new(cx.executor());
13318
13319 let project = Project::test(fs, [], cx).await;
13320 let (multi_workspace, cx) =
13321 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
13322 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
13323
13324 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
13325 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
13326 workspace.add_panel(panel_1.clone(), window, cx);
13327 workspace.toggle_dock(DockPosition::Left, window, cx);
13328 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
13329 workspace.add_panel(panel_2.clone(), window, cx);
13330 workspace.toggle_dock(DockPosition::Right, window, cx);
13331
13332 let left_dock = workspace.left_dock();
13333 assert_eq!(
13334 left_dock.read(cx).visible_panel().unwrap().panel_id(),
13335 panel_1.panel_id()
13336 );
13337 assert_eq!(
13338 workspace.dock_size(&left_dock.read(cx), window, cx),
13339 Some(px(300.))
13340 );
13341
13342 workspace.resize_left_dock(px(1337.), window, cx);
13343 assert_eq!(
13344 workspace
13345 .right_dock()
13346 .read(cx)
13347 .visible_panel()
13348 .unwrap()
13349 .panel_id(),
13350 panel_2.panel_id(),
13351 );
13352
13353 (panel_1, panel_2)
13354 });
13355
13356 // Move panel_1 to the right
13357 panel_1.update_in(cx, |panel_1, window, cx| {
13358 panel_1.set_position(DockPosition::Right, window, cx)
13359 });
13360
13361 workspace.update_in(cx, |workspace, window, cx| {
13362 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
13363 // Since it was the only panel on the left, the left dock should now be closed.
13364 assert!(!workspace.left_dock().read(cx).is_open());
13365 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
13366 let right_dock = workspace.right_dock();
13367 assert_eq!(
13368 right_dock.read(cx).visible_panel().unwrap().panel_id(),
13369 panel_1.panel_id()
13370 );
13371 assert_eq!(
13372 right_dock
13373 .read(cx)
13374 .active_panel_size()
13375 .unwrap()
13376 .size
13377 .unwrap(),
13378 px(1337.)
13379 );
13380
13381 // Now we move panel_2 to the left
13382 panel_2.set_position(DockPosition::Left, window, cx);
13383 });
13384
13385 workspace.update(cx, |workspace, cx| {
13386 // Since panel_2 was not visible on the right, we don't open the left dock.
13387 assert!(!workspace.left_dock().read(cx).is_open());
13388 // And the right dock is unaffected in its displaying of panel_1
13389 assert!(workspace.right_dock().read(cx).is_open());
13390 assert_eq!(
13391 workspace
13392 .right_dock()
13393 .read(cx)
13394 .visible_panel()
13395 .unwrap()
13396 .panel_id(),
13397 panel_1.panel_id(),
13398 );
13399 });
13400
13401 // Move panel_1 back to the left
13402 panel_1.update_in(cx, |panel_1, window, cx| {
13403 panel_1.set_position(DockPosition::Left, window, cx)
13404 });
13405
13406 workspace.update_in(cx, |workspace, window, cx| {
13407 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
13408 let left_dock = workspace.left_dock();
13409 assert!(left_dock.read(cx).is_open());
13410 assert_eq!(
13411 left_dock.read(cx).visible_panel().unwrap().panel_id(),
13412 panel_1.panel_id()
13413 );
13414 assert_eq!(
13415 workspace.dock_size(&left_dock.read(cx), window, cx),
13416 Some(px(1337.))
13417 );
13418 // And the right dock should be closed as it no longer has any panels.
13419 assert!(!workspace.right_dock().read(cx).is_open());
13420
13421 // Now we move panel_1 to the bottom
13422 panel_1.set_position(DockPosition::Bottom, window, cx);
13423 });
13424
13425 workspace.update_in(cx, |workspace, window, cx| {
13426 // Since panel_1 was visible on the left, we close the left dock.
13427 assert!(!workspace.left_dock().read(cx).is_open());
13428 // The bottom dock is sized based on the panel's default size,
13429 // since the panel orientation changed from vertical to horizontal.
13430 let bottom_dock = workspace.bottom_dock();
13431 assert_eq!(
13432 workspace.dock_size(&bottom_dock.read(cx), window, cx),
13433 Some(px(300.))
13434 );
13435 // Close bottom dock and move panel_1 back to the left.
13436 bottom_dock.update(cx, |bottom_dock, cx| {
13437 bottom_dock.set_open(false, window, cx)
13438 });
13439 panel_1.set_position(DockPosition::Left, window, cx);
13440 });
13441
13442 // Emit activated event on panel 1
13443 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
13444
13445 // Now the left dock is open and panel_1 is active and focused.
13446 workspace.update_in(cx, |workspace, window, cx| {
13447 let left_dock = workspace.left_dock();
13448 assert!(left_dock.read(cx).is_open());
13449 assert_eq!(
13450 left_dock.read(cx).visible_panel().unwrap().panel_id(),
13451 panel_1.panel_id(),
13452 );
13453 assert!(panel_1.focus_handle(cx).is_focused(window));
13454 });
13455
13456 // Emit closed event on panel 2, which is not active
13457 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
13458
13459 // Wo don't close the left dock, because panel_2 wasn't the active panel
13460 workspace.update(cx, |workspace, cx| {
13461 let left_dock = workspace.left_dock();
13462 assert!(left_dock.read(cx).is_open());
13463 assert_eq!(
13464 left_dock.read(cx).visible_panel().unwrap().panel_id(),
13465 panel_1.panel_id(),
13466 );
13467 });
13468
13469 // Emitting a ZoomIn event shows the panel as zoomed.
13470 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
13471 workspace.read_with(cx, |workspace, _| {
13472 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13473 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
13474 });
13475
13476 // Move panel to another dock while it is zoomed
13477 panel_1.update_in(cx, |panel, window, cx| {
13478 panel.set_position(DockPosition::Right, window, cx)
13479 });
13480 workspace.read_with(cx, |workspace, _| {
13481 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13482
13483 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
13484 });
13485
13486 // This is a helper for getting a:
13487 // - valid focus on an element,
13488 // - that isn't a part of the panes and panels system of the Workspace,
13489 // - and doesn't trigger the 'on_focus_lost' API.
13490 let focus_other_view = {
13491 let workspace = workspace.clone();
13492 move |cx: &mut VisualTestContext| {
13493 workspace.update_in(cx, |workspace, window, cx| {
13494 if workspace.active_modal::<TestModal>(cx).is_some() {
13495 workspace.toggle_modal(window, cx, TestModal::new);
13496 workspace.toggle_modal(window, cx, TestModal::new);
13497 } else {
13498 workspace.toggle_modal(window, cx, TestModal::new);
13499 }
13500 })
13501 }
13502 };
13503
13504 // If focus is transferred to another view that's not a panel or another pane, we still show
13505 // the panel as zoomed.
13506 focus_other_view(cx);
13507 workspace.read_with(cx, |workspace, _| {
13508 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13509 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
13510 });
13511
13512 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
13513 workspace.update_in(cx, |_workspace, window, cx| {
13514 cx.focus_self(window);
13515 });
13516 workspace.read_with(cx, |workspace, _| {
13517 assert_eq!(workspace.zoomed, None);
13518 assert_eq!(workspace.zoomed_position, None);
13519 });
13520
13521 // If focus is transferred again to another view that's not a panel or a pane, we won't
13522 // show the panel as zoomed because it wasn't zoomed before.
13523 focus_other_view(cx);
13524 workspace.read_with(cx, |workspace, _| {
13525 assert_eq!(workspace.zoomed, None);
13526 assert_eq!(workspace.zoomed_position, None);
13527 });
13528
13529 // When the panel is activated, it is zoomed again.
13530 cx.dispatch_action(ToggleRightDock);
13531 workspace.read_with(cx, |workspace, _| {
13532 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13533 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
13534 });
13535
13536 // Emitting a ZoomOut event unzooms the panel.
13537 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
13538 workspace.read_with(cx, |workspace, _| {
13539 assert_eq!(workspace.zoomed, None);
13540 assert_eq!(workspace.zoomed_position, None);
13541 });
13542
13543 // Emit closed event on panel 1, which is active
13544 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
13545
13546 // Now the left dock is closed, because panel_1 was the active panel
13547 workspace.update(cx, |workspace, cx| {
13548 let right_dock = workspace.right_dock();
13549 assert!(!right_dock.read(cx).is_open());
13550 });
13551 }
13552
13553 #[gpui::test]
13554 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
13555 init_test(cx);
13556
13557 let fs = FakeFs::new(cx.background_executor.clone());
13558 let project = Project::test(fs, [], cx).await;
13559 let (workspace, cx) =
13560 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13561 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13562
13563 let dirty_regular_buffer = cx.new(|cx| {
13564 TestItem::new(cx)
13565 .with_dirty(true)
13566 .with_label("1.txt")
13567 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
13568 });
13569 let dirty_regular_buffer_2 = cx.new(|cx| {
13570 TestItem::new(cx)
13571 .with_dirty(true)
13572 .with_label("2.txt")
13573 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
13574 });
13575 let dirty_multi_buffer_with_both = cx.new(|cx| {
13576 TestItem::new(cx)
13577 .with_dirty(true)
13578 .with_buffer_kind(ItemBufferKind::Multibuffer)
13579 .with_label("Fake Project Search")
13580 .with_project_items(&[
13581 dirty_regular_buffer.read(cx).project_items[0].clone(),
13582 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
13583 ])
13584 });
13585 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
13586 workspace.update_in(cx, |workspace, window, cx| {
13587 workspace.add_item(
13588 pane.clone(),
13589 Box::new(dirty_regular_buffer.clone()),
13590 None,
13591 false,
13592 false,
13593 window,
13594 cx,
13595 );
13596 workspace.add_item(
13597 pane.clone(),
13598 Box::new(dirty_regular_buffer_2.clone()),
13599 None,
13600 false,
13601 false,
13602 window,
13603 cx,
13604 );
13605 workspace.add_item(
13606 pane.clone(),
13607 Box::new(dirty_multi_buffer_with_both.clone()),
13608 None,
13609 false,
13610 false,
13611 window,
13612 cx,
13613 );
13614 });
13615
13616 pane.update_in(cx, |pane, window, cx| {
13617 pane.activate_item(2, true, true, window, cx);
13618 assert_eq!(
13619 pane.active_item().unwrap().item_id(),
13620 multi_buffer_with_both_files_id,
13621 "Should select the multi buffer in the pane"
13622 );
13623 });
13624 let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13625 pane.close_other_items(
13626 &CloseOtherItems {
13627 save_intent: Some(SaveIntent::Save),
13628 close_pinned: true,
13629 },
13630 None,
13631 window,
13632 cx,
13633 )
13634 });
13635 cx.background_executor.run_until_parked();
13636 assert!(!cx.has_pending_prompt());
13637 close_all_but_multi_buffer_task
13638 .await
13639 .expect("Closing all buffers but the multi buffer failed");
13640 pane.update(cx, |pane, cx| {
13641 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
13642 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
13643 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
13644 assert_eq!(pane.items_len(), 1);
13645 assert_eq!(
13646 pane.active_item().unwrap().item_id(),
13647 multi_buffer_with_both_files_id,
13648 "Should have only the multi buffer left in the pane"
13649 );
13650 assert!(
13651 dirty_multi_buffer_with_both.read(cx).is_dirty,
13652 "The multi buffer containing the unsaved buffer should still be dirty"
13653 );
13654 });
13655
13656 dirty_regular_buffer.update(cx, |buffer, cx| {
13657 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
13658 });
13659
13660 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13661 pane.close_active_item(
13662 &CloseActiveItem {
13663 save_intent: Some(SaveIntent::Close),
13664 close_pinned: false,
13665 },
13666 window,
13667 cx,
13668 )
13669 });
13670 cx.background_executor.run_until_parked();
13671 assert!(
13672 cx.has_pending_prompt(),
13673 "Dirty multi buffer should prompt a save dialog"
13674 );
13675 cx.simulate_prompt_answer("Save");
13676 cx.background_executor.run_until_parked();
13677 close_multi_buffer_task
13678 .await
13679 .expect("Closing the multi buffer failed");
13680 pane.update(cx, |pane, cx| {
13681 assert_eq!(
13682 dirty_multi_buffer_with_both.read(cx).save_count,
13683 1,
13684 "Multi buffer item should get be saved"
13685 );
13686 // Test impl does not save inner items, so we do not assert them
13687 assert_eq!(
13688 pane.items_len(),
13689 0,
13690 "No more items should be left in the pane"
13691 );
13692 assert!(pane.active_item().is_none());
13693 });
13694 }
13695
13696 #[gpui::test]
13697 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
13698 cx: &mut TestAppContext,
13699 ) {
13700 init_test(cx);
13701
13702 let fs = FakeFs::new(cx.background_executor.clone());
13703 let project = Project::test(fs, [], cx).await;
13704 let (workspace, cx) =
13705 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13706 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13707
13708 let dirty_regular_buffer = cx.new(|cx| {
13709 TestItem::new(cx)
13710 .with_dirty(true)
13711 .with_label("1.txt")
13712 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
13713 });
13714 let dirty_regular_buffer_2 = cx.new(|cx| {
13715 TestItem::new(cx)
13716 .with_dirty(true)
13717 .with_label("2.txt")
13718 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
13719 });
13720 let clear_regular_buffer = cx.new(|cx| {
13721 TestItem::new(cx)
13722 .with_label("3.txt")
13723 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
13724 });
13725
13726 let dirty_multi_buffer_with_both = cx.new(|cx| {
13727 TestItem::new(cx)
13728 .with_dirty(true)
13729 .with_buffer_kind(ItemBufferKind::Multibuffer)
13730 .with_label("Fake Project Search")
13731 .with_project_items(&[
13732 dirty_regular_buffer.read(cx).project_items[0].clone(),
13733 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
13734 clear_regular_buffer.read(cx).project_items[0].clone(),
13735 ])
13736 });
13737 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
13738 workspace.update_in(cx, |workspace, window, cx| {
13739 workspace.add_item(
13740 pane.clone(),
13741 Box::new(dirty_regular_buffer.clone()),
13742 None,
13743 false,
13744 false,
13745 window,
13746 cx,
13747 );
13748 workspace.add_item(
13749 pane.clone(),
13750 Box::new(dirty_multi_buffer_with_both.clone()),
13751 None,
13752 false,
13753 false,
13754 window,
13755 cx,
13756 );
13757 });
13758
13759 pane.update_in(cx, |pane, window, cx| {
13760 pane.activate_item(1, true, true, window, cx);
13761 assert_eq!(
13762 pane.active_item().unwrap().item_id(),
13763 multi_buffer_with_both_files_id,
13764 "Should select the multi buffer in the pane"
13765 );
13766 });
13767 let _close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13768 pane.close_active_item(
13769 &CloseActiveItem {
13770 save_intent: None,
13771 close_pinned: false,
13772 },
13773 window,
13774 cx,
13775 )
13776 });
13777 cx.background_executor.run_until_parked();
13778 assert!(
13779 cx.has_pending_prompt(),
13780 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
13781 );
13782 }
13783
13784 /// Tests that when `close_on_file_delete` is enabled, files are automatically
13785 /// closed when they are deleted from disk.
13786 #[gpui::test]
13787 async fn test_close_on_disk_deletion_enabled(cx: &mut TestAppContext) {
13788 init_test(cx);
13789
13790 // Enable the close_on_disk_deletion setting
13791 cx.update_global(|store: &mut SettingsStore, cx| {
13792 store.update_user_settings(cx, |settings| {
13793 settings.workspace.close_on_file_delete = Some(true);
13794 });
13795 });
13796
13797 let fs = FakeFs::new(cx.background_executor.clone());
13798 let project = Project::test(fs, [], cx).await;
13799 let (workspace, cx) =
13800 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13801 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13802
13803 // Create a test item that simulates a file
13804 let item = cx.new(|cx| {
13805 TestItem::new(cx)
13806 .with_label("test.txt")
13807 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13808 });
13809
13810 // Add item to workspace
13811 workspace.update_in(cx, |workspace, window, cx| {
13812 workspace.add_item(
13813 pane.clone(),
13814 Box::new(item.clone()),
13815 None,
13816 false,
13817 false,
13818 window,
13819 cx,
13820 );
13821 });
13822
13823 // Verify the item is in the pane
13824 pane.read_with(cx, |pane, _| {
13825 assert_eq!(pane.items().count(), 1);
13826 });
13827
13828 // Simulate file deletion by setting the item's deleted state
13829 item.update(cx, |item, _| {
13830 item.set_has_deleted_file(true);
13831 });
13832
13833 // Emit UpdateTab event to trigger the close behavior
13834 cx.run_until_parked();
13835 item.update(cx, |_, cx| {
13836 cx.emit(ItemEvent::UpdateTab);
13837 });
13838
13839 // Allow the close operation to complete
13840 cx.run_until_parked();
13841
13842 // Verify the item was automatically closed
13843 pane.read_with(cx, |pane, _| {
13844 assert_eq!(
13845 pane.items().count(),
13846 0,
13847 "Item should be automatically closed when file is deleted"
13848 );
13849 });
13850 }
13851
13852 /// Tests that when `close_on_file_delete` is disabled (default), files remain
13853 /// open with a strikethrough when they are deleted from disk.
13854 #[gpui::test]
13855 async fn test_close_on_disk_deletion_disabled(cx: &mut TestAppContext) {
13856 init_test(cx);
13857
13858 // Ensure close_on_disk_deletion is disabled (default)
13859 cx.update_global(|store: &mut SettingsStore, cx| {
13860 store.update_user_settings(cx, |settings| {
13861 settings.workspace.close_on_file_delete = Some(false);
13862 });
13863 });
13864
13865 let fs = FakeFs::new(cx.background_executor.clone());
13866 let project = Project::test(fs, [], cx).await;
13867 let (workspace, cx) =
13868 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13869 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13870
13871 // Create a test item that simulates a file
13872 let item = cx.new(|cx| {
13873 TestItem::new(cx)
13874 .with_label("test.txt")
13875 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13876 });
13877
13878 // Add item to workspace
13879 workspace.update_in(cx, |workspace, window, cx| {
13880 workspace.add_item(
13881 pane.clone(),
13882 Box::new(item.clone()),
13883 None,
13884 false,
13885 false,
13886 window,
13887 cx,
13888 );
13889 });
13890
13891 // Verify the item is in the pane
13892 pane.read_with(cx, |pane, _| {
13893 assert_eq!(pane.items().count(), 1);
13894 });
13895
13896 // Simulate file deletion
13897 item.update(cx, |item, _| {
13898 item.set_has_deleted_file(true);
13899 });
13900
13901 // Emit UpdateTab event
13902 cx.run_until_parked();
13903 item.update(cx, |_, cx| {
13904 cx.emit(ItemEvent::UpdateTab);
13905 });
13906
13907 // Allow any potential close operation to complete
13908 cx.run_until_parked();
13909
13910 // Verify the item remains open (with strikethrough)
13911 pane.read_with(cx, |pane, _| {
13912 assert_eq!(
13913 pane.items().count(),
13914 1,
13915 "Item should remain open when close_on_disk_deletion is disabled"
13916 );
13917 });
13918
13919 // Verify the item shows as deleted
13920 item.read_with(cx, |item, _| {
13921 assert!(
13922 item.has_deleted_file,
13923 "Item should be marked as having deleted file"
13924 );
13925 });
13926 }
13927
13928 /// Tests that dirty files are not automatically closed when deleted from disk,
13929 /// even when `close_on_file_delete` is enabled. This ensures users don't lose
13930 /// unsaved changes without being prompted.
13931 #[gpui::test]
13932 async fn test_close_on_disk_deletion_with_dirty_file(cx: &mut TestAppContext) {
13933 init_test(cx);
13934
13935 // Enable the close_on_file_delete setting
13936 cx.update_global(|store: &mut SettingsStore, cx| {
13937 store.update_user_settings(cx, |settings| {
13938 settings.workspace.close_on_file_delete = Some(true);
13939 });
13940 });
13941
13942 let fs = FakeFs::new(cx.background_executor.clone());
13943 let project = Project::test(fs, [], cx).await;
13944 let (workspace, cx) =
13945 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13946 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13947
13948 // Create a dirty test item
13949 let item = cx.new(|cx| {
13950 TestItem::new(cx)
13951 .with_dirty(true)
13952 .with_label("test.txt")
13953 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13954 });
13955
13956 // Add item to workspace
13957 workspace.update_in(cx, |workspace, window, cx| {
13958 workspace.add_item(
13959 pane.clone(),
13960 Box::new(item.clone()),
13961 None,
13962 false,
13963 false,
13964 window,
13965 cx,
13966 );
13967 });
13968
13969 // Simulate file deletion
13970 item.update(cx, |item, _| {
13971 item.set_has_deleted_file(true);
13972 });
13973
13974 // Emit UpdateTab event to trigger the close behavior
13975 cx.run_until_parked();
13976 item.update(cx, |_, cx| {
13977 cx.emit(ItemEvent::UpdateTab);
13978 });
13979
13980 // Allow any potential close operation to complete
13981 cx.run_until_parked();
13982
13983 // Verify the item remains open (dirty files are not auto-closed)
13984 pane.read_with(cx, |pane, _| {
13985 assert_eq!(
13986 pane.items().count(),
13987 1,
13988 "Dirty items should not be automatically closed even when file is deleted"
13989 );
13990 });
13991
13992 // Verify the item is marked as deleted and still dirty
13993 item.read_with(cx, |item, _| {
13994 assert!(
13995 item.has_deleted_file,
13996 "Item should be marked as having deleted file"
13997 );
13998 assert!(item.is_dirty, "Item should still be dirty");
13999 });
14000 }
14001
14002 /// Tests that navigation history is cleaned up when files are auto-closed
14003 /// due to deletion from disk.
14004 #[gpui::test]
14005 async fn test_close_on_disk_deletion_cleans_navigation_history(cx: &mut TestAppContext) {
14006 init_test(cx);
14007
14008 // Enable the close_on_file_delete setting
14009 cx.update_global(|store: &mut SettingsStore, cx| {
14010 store.update_user_settings(cx, |settings| {
14011 settings.workspace.close_on_file_delete = Some(true);
14012 });
14013 });
14014
14015 let fs = FakeFs::new(cx.background_executor.clone());
14016 let project = Project::test(fs, [], cx).await;
14017 let (workspace, cx) =
14018 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
14019 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14020
14021 // Create test items
14022 let item1 = cx.new(|cx| {
14023 TestItem::new(cx)
14024 .with_label("test1.txt")
14025 .with_project_items(&[TestProjectItem::new(1, "test1.txt", cx)])
14026 });
14027 let item1_id = item1.item_id();
14028
14029 let item2 = cx.new(|cx| {
14030 TestItem::new(cx)
14031 .with_label("test2.txt")
14032 .with_project_items(&[TestProjectItem::new(2, "test2.txt", cx)])
14033 });
14034
14035 // Add items to workspace
14036 workspace.update_in(cx, |workspace, window, cx| {
14037 workspace.add_item(
14038 pane.clone(),
14039 Box::new(item1.clone()),
14040 None,
14041 false,
14042 false,
14043 window,
14044 cx,
14045 );
14046 workspace.add_item(
14047 pane.clone(),
14048 Box::new(item2.clone()),
14049 None,
14050 false,
14051 false,
14052 window,
14053 cx,
14054 );
14055 });
14056
14057 // Activate item1 to ensure it gets navigation entries
14058 pane.update_in(cx, |pane, window, cx| {
14059 pane.activate_item(0, true, true, window, cx);
14060 });
14061
14062 // Switch to item2 and back to create navigation history
14063 pane.update_in(cx, |pane, window, cx| {
14064 pane.activate_item(1, true, true, window, cx);
14065 });
14066 cx.run_until_parked();
14067
14068 pane.update_in(cx, |pane, window, cx| {
14069 pane.activate_item(0, true, true, window, cx);
14070 });
14071 cx.run_until_parked();
14072
14073 // Simulate file deletion for item1
14074 item1.update(cx, |item, _| {
14075 item.set_has_deleted_file(true);
14076 });
14077
14078 // Emit UpdateTab event to trigger the close behavior
14079 item1.update(cx, |_, cx| {
14080 cx.emit(ItemEvent::UpdateTab);
14081 });
14082 cx.run_until_parked();
14083
14084 // Verify item1 was closed
14085 pane.read_with(cx, |pane, _| {
14086 assert_eq!(
14087 pane.items().count(),
14088 1,
14089 "Should have 1 item remaining after auto-close"
14090 );
14091 });
14092
14093 // Check navigation history after close
14094 let has_item = pane.read_with(cx, |pane, cx| {
14095 let mut has_item = false;
14096 pane.nav_history().for_each_entry(cx, &mut |entry, _| {
14097 if entry.item.id() == item1_id {
14098 has_item = true;
14099 }
14100 });
14101 has_item
14102 });
14103
14104 assert!(
14105 !has_item,
14106 "Navigation history should not contain closed item entries"
14107 );
14108 }
14109
14110 #[gpui::test]
14111 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
14112 cx: &mut TestAppContext,
14113 ) {
14114 init_test(cx);
14115
14116 let fs = FakeFs::new(cx.background_executor.clone());
14117 let project = Project::test(fs, [], cx).await;
14118 let (workspace, cx) =
14119 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
14120 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14121
14122 let dirty_regular_buffer = cx.new(|cx| {
14123 TestItem::new(cx)
14124 .with_dirty(true)
14125 .with_label("1.txt")
14126 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
14127 });
14128 let dirty_regular_buffer_2 = cx.new(|cx| {
14129 TestItem::new(cx)
14130 .with_dirty(true)
14131 .with_label("2.txt")
14132 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
14133 });
14134 let clear_regular_buffer = cx.new(|cx| {
14135 TestItem::new(cx)
14136 .with_label("3.txt")
14137 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
14138 });
14139
14140 let dirty_multi_buffer = cx.new(|cx| {
14141 TestItem::new(cx)
14142 .with_dirty(true)
14143 .with_buffer_kind(ItemBufferKind::Multibuffer)
14144 .with_label("Fake Project Search")
14145 .with_project_items(&[
14146 dirty_regular_buffer.read(cx).project_items[0].clone(),
14147 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
14148 clear_regular_buffer.read(cx).project_items[0].clone(),
14149 ])
14150 });
14151 workspace.update_in(cx, |workspace, window, cx| {
14152 workspace.add_item(
14153 pane.clone(),
14154 Box::new(dirty_regular_buffer.clone()),
14155 None,
14156 false,
14157 false,
14158 window,
14159 cx,
14160 );
14161 workspace.add_item(
14162 pane.clone(),
14163 Box::new(dirty_regular_buffer_2.clone()),
14164 None,
14165 false,
14166 false,
14167 window,
14168 cx,
14169 );
14170 workspace.add_item(
14171 pane.clone(),
14172 Box::new(dirty_multi_buffer.clone()),
14173 None,
14174 false,
14175 false,
14176 window,
14177 cx,
14178 );
14179 });
14180
14181 pane.update_in(cx, |pane, window, cx| {
14182 pane.activate_item(2, true, true, window, cx);
14183 assert_eq!(
14184 pane.active_item().unwrap().item_id(),
14185 dirty_multi_buffer.item_id(),
14186 "Should select the multi buffer in the pane"
14187 );
14188 });
14189 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
14190 pane.close_active_item(
14191 &CloseActiveItem {
14192 save_intent: None,
14193 close_pinned: false,
14194 },
14195 window,
14196 cx,
14197 )
14198 });
14199 cx.background_executor.run_until_parked();
14200 assert!(
14201 !cx.has_pending_prompt(),
14202 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
14203 );
14204 close_multi_buffer_task
14205 .await
14206 .expect("Closing multi buffer failed");
14207 pane.update(cx, |pane, cx| {
14208 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
14209 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
14210 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
14211 assert_eq!(
14212 pane.items()
14213 .map(|item| item.item_id())
14214 .sorted()
14215 .collect::<Vec<_>>(),
14216 vec![
14217 dirty_regular_buffer.item_id(),
14218 dirty_regular_buffer_2.item_id(),
14219 ],
14220 "Should have no multi buffer left in the pane"
14221 );
14222 assert!(dirty_regular_buffer.read(cx).is_dirty);
14223 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
14224 });
14225 }
14226
14227 #[gpui::test]
14228 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
14229 init_test(cx);
14230 let fs = FakeFs::new(cx.executor());
14231 let project = Project::test(fs, [], cx).await;
14232 let (multi_workspace, cx) =
14233 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
14234 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
14235
14236 // Add a new panel to the right dock, opening the dock and setting the
14237 // focus to the new panel.
14238 let panel = workspace.update_in(cx, |workspace, window, cx| {
14239 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14240 workspace.add_panel(panel.clone(), window, cx);
14241
14242 workspace
14243 .right_dock()
14244 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
14245
14246 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14247
14248 panel
14249 });
14250
14251 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
14252 // panel to the next valid position which, in this case, is the left
14253 // dock.
14254 cx.dispatch_action(MoveFocusedPanelToNextPosition);
14255 workspace.update(cx, |workspace, cx| {
14256 assert!(workspace.left_dock().read(cx).is_open());
14257 assert_eq!(panel.read(cx).position, DockPosition::Left);
14258 });
14259
14260 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
14261 // panel to the next valid position which, in this case, is the bottom
14262 // dock.
14263 cx.dispatch_action(MoveFocusedPanelToNextPosition);
14264 workspace.update(cx, |workspace, cx| {
14265 assert!(workspace.bottom_dock().read(cx).is_open());
14266 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
14267 });
14268
14269 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
14270 // around moving the panel to its initial position, the right dock.
14271 cx.dispatch_action(MoveFocusedPanelToNextPosition);
14272 workspace.update(cx, |workspace, cx| {
14273 assert!(workspace.right_dock().read(cx).is_open());
14274 assert_eq!(panel.read(cx).position, DockPosition::Right);
14275 });
14276
14277 // Remove focus from the panel, ensuring that, if the panel is not
14278 // focused, the `MoveFocusedPanelToNextPosition` action does not update
14279 // the panel's position, so the panel is still in the right dock.
14280 workspace.update_in(cx, |workspace, window, cx| {
14281 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14282 });
14283
14284 cx.dispatch_action(MoveFocusedPanelToNextPosition);
14285 workspace.update(cx, |workspace, cx| {
14286 assert!(workspace.right_dock().read(cx).is_open());
14287 assert_eq!(panel.read(cx).position, DockPosition::Right);
14288 });
14289 }
14290
14291 #[gpui::test]
14292 async fn test_moving_items_create_panes(cx: &mut TestAppContext) {
14293 init_test(cx);
14294
14295 let fs = FakeFs::new(cx.executor());
14296 let project = Project::test(fs, [], cx).await;
14297 let (workspace, cx) =
14298 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14299
14300 let item_1 = cx.new(|cx| {
14301 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
14302 });
14303 workspace.update_in(cx, |workspace, window, cx| {
14304 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
14305 workspace.move_item_to_pane_in_direction(
14306 &MoveItemToPaneInDirection {
14307 direction: SplitDirection::Right,
14308 focus: true,
14309 clone: false,
14310 },
14311 window,
14312 cx,
14313 );
14314 workspace.move_item_to_pane_at_index(
14315 &MoveItemToPane {
14316 destination: 3,
14317 focus: true,
14318 clone: false,
14319 },
14320 window,
14321 cx,
14322 );
14323
14324 assert_eq!(workspace.panes.len(), 1, "No new panes were created");
14325 assert_eq!(
14326 pane_items_paths(&workspace.active_pane, cx),
14327 vec!["first.txt".to_string()],
14328 "Single item was not moved anywhere"
14329 );
14330 });
14331
14332 let item_2 = cx.new(|cx| {
14333 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "second.txt", cx)])
14334 });
14335 workspace.update_in(cx, |workspace, window, cx| {
14336 workspace.add_item_to_active_pane(Box::new(item_2), None, true, window, cx);
14337 assert_eq!(
14338 pane_items_paths(&workspace.panes[0], cx),
14339 vec!["first.txt".to_string(), "second.txt".to_string()],
14340 );
14341 workspace.move_item_to_pane_in_direction(
14342 &MoveItemToPaneInDirection {
14343 direction: SplitDirection::Right,
14344 focus: true,
14345 clone: false,
14346 },
14347 window,
14348 cx,
14349 );
14350
14351 assert_eq!(workspace.panes.len(), 2, "A new pane should be created");
14352 assert_eq!(
14353 pane_items_paths(&workspace.panes[0], cx),
14354 vec!["first.txt".to_string()],
14355 "After moving, one item should be left in the original pane"
14356 );
14357 assert_eq!(
14358 pane_items_paths(&workspace.panes[1], cx),
14359 vec!["second.txt".to_string()],
14360 "New item should have been moved to the new pane"
14361 );
14362 });
14363
14364 let item_3 = cx.new(|cx| {
14365 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "third.txt", cx)])
14366 });
14367 workspace.update_in(cx, |workspace, window, cx| {
14368 let original_pane = workspace.panes[0].clone();
14369 workspace.set_active_pane(&original_pane, window, cx);
14370 workspace.add_item_to_active_pane(Box::new(item_3), None, true, window, cx);
14371 assert_eq!(workspace.panes.len(), 2, "No new panes were created");
14372 assert_eq!(
14373 pane_items_paths(&workspace.active_pane, cx),
14374 vec!["first.txt".to_string(), "third.txt".to_string()],
14375 "New pane should be ready to move one item out"
14376 );
14377
14378 workspace.move_item_to_pane_at_index(
14379 &MoveItemToPane {
14380 destination: 3,
14381 focus: true,
14382 clone: false,
14383 },
14384 window,
14385 cx,
14386 );
14387 assert_eq!(workspace.panes.len(), 3, "A new pane should be created");
14388 assert_eq!(
14389 pane_items_paths(&workspace.active_pane, cx),
14390 vec!["first.txt".to_string()],
14391 "After moving, one item should be left in the original pane"
14392 );
14393 assert_eq!(
14394 pane_items_paths(&workspace.panes[1], cx),
14395 vec!["second.txt".to_string()],
14396 "Previously created pane should be unchanged"
14397 );
14398 assert_eq!(
14399 pane_items_paths(&workspace.panes[2], cx),
14400 vec!["third.txt".to_string()],
14401 "New item should have been moved to the new pane"
14402 );
14403 });
14404 }
14405
14406 #[gpui::test]
14407 async fn test_moving_items_can_clone_panes(cx: &mut TestAppContext) {
14408 init_test(cx);
14409
14410 let fs = FakeFs::new(cx.executor());
14411 let project = Project::test(fs, [], cx).await;
14412 let (workspace, cx) =
14413 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14414
14415 let item_1 = cx.new(|cx| {
14416 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
14417 });
14418 workspace.update_in(cx, |workspace, window, cx| {
14419 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
14420 workspace.move_item_to_pane_in_direction(
14421 &MoveItemToPaneInDirection {
14422 direction: SplitDirection::Right,
14423 focus: true,
14424 clone: true,
14425 },
14426 window,
14427 cx,
14428 );
14429 });
14430 cx.run_until_parked();
14431 workspace.update_in(cx, |workspace, window, cx| {
14432 workspace.move_item_to_pane_at_index(
14433 &MoveItemToPane {
14434 destination: 3,
14435 focus: true,
14436 clone: true,
14437 },
14438 window,
14439 cx,
14440 );
14441 });
14442 cx.run_until_parked();
14443
14444 workspace.update(cx, |workspace, cx| {
14445 assert_eq!(workspace.panes.len(), 3, "Two new panes were created");
14446 for pane in workspace.panes() {
14447 assert_eq!(
14448 pane_items_paths(pane, cx),
14449 vec!["first.txt".to_string()],
14450 "Single item exists in all panes"
14451 );
14452 }
14453 });
14454
14455 // verify that the active pane has been updated after waiting for the
14456 // pane focus event to fire and resolve
14457 workspace.read_with(cx, |workspace, _app| {
14458 assert_eq!(
14459 workspace.active_pane(),
14460 &workspace.panes[2],
14461 "The third pane should be the active one: {:?}",
14462 workspace.panes
14463 );
14464 })
14465 }
14466
14467 #[gpui::test]
14468 async fn test_close_item_in_all_panes(cx: &mut TestAppContext) {
14469 init_test(cx);
14470
14471 let fs = FakeFs::new(cx.executor());
14472 fs.insert_tree("/root", json!({ "test.txt": "" })).await;
14473
14474 let project = Project::test(fs, ["root".as_ref()], cx).await;
14475 let (workspace, cx) =
14476 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14477
14478 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14479 // Add item to pane A with project path
14480 let item_a = cx.new(|cx| {
14481 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
14482 });
14483 workspace.update_in(cx, |workspace, window, cx| {
14484 workspace.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
14485 });
14486
14487 // Split to create pane B
14488 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
14489 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
14490 });
14491
14492 // Add item with SAME project path to pane B, and pin it
14493 let item_b = cx.new(|cx| {
14494 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
14495 });
14496 pane_b.update_in(cx, |pane, window, cx| {
14497 pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
14498 pane.set_pinned_count(1);
14499 });
14500
14501 assert_eq!(pane_a.read_with(cx, |pane, _| pane.items_len()), 1);
14502 assert_eq!(pane_b.read_with(cx, |pane, _| pane.items_len()), 1);
14503
14504 // close_pinned: false should only close the unpinned copy
14505 workspace.update_in(cx, |workspace, window, cx| {
14506 workspace.close_item_in_all_panes(
14507 &CloseItemInAllPanes {
14508 save_intent: Some(SaveIntent::Close),
14509 close_pinned: false,
14510 },
14511 window,
14512 cx,
14513 )
14514 });
14515 cx.executor().run_until_parked();
14516
14517 let item_count_a = pane_a.read_with(cx, |pane, _| pane.items_len());
14518 let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
14519 assert_eq!(item_count_a, 0, "Unpinned item in pane A should be closed");
14520 assert_eq!(item_count_b, 1, "Pinned item in pane B should remain");
14521
14522 // Split again, seeing as closing the previous item also closed its
14523 // pane, so only pane remains, which does not allow us to properly test
14524 // that both items close when `close_pinned: true`.
14525 let pane_c = workspace.update_in(cx, |workspace, window, cx| {
14526 workspace.split_pane(pane_b.clone(), SplitDirection::Right, window, cx)
14527 });
14528
14529 // Add an item with the same project path to pane C so that
14530 // close_item_in_all_panes can determine what to close across all panes
14531 // (it reads the active item from the active pane, and split_pane
14532 // creates an empty pane).
14533 let item_c = cx.new(|cx| {
14534 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
14535 });
14536 pane_c.update_in(cx, |pane, window, cx| {
14537 pane.add_item(Box::new(item_c.clone()), true, true, None, window, cx);
14538 });
14539
14540 // close_pinned: true should close the pinned copy too
14541 workspace.update_in(cx, |workspace, window, cx| {
14542 let panes_count = workspace.panes().len();
14543 assert_eq!(panes_count, 2, "Workspace should have two panes (B and C)");
14544
14545 workspace.close_item_in_all_panes(
14546 &CloseItemInAllPanes {
14547 save_intent: Some(SaveIntent::Close),
14548 close_pinned: true,
14549 },
14550 window,
14551 cx,
14552 )
14553 });
14554 cx.executor().run_until_parked();
14555
14556 let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
14557 let item_count_c = pane_c.read_with(cx, |pane, _| pane.items_len());
14558 assert_eq!(item_count_b, 0, "Pinned item in pane B should be closed");
14559 assert_eq!(item_count_c, 0, "Unpinned item in pane C should be closed");
14560 }
14561
14562 mod register_project_item_tests {
14563
14564 use super::*;
14565
14566 // View
14567 struct TestPngItemView {
14568 focus_handle: FocusHandle,
14569 }
14570 // Model
14571 struct TestPngItem {}
14572
14573 impl project::ProjectItem for TestPngItem {
14574 fn try_open(
14575 _project: &Entity<Project>,
14576 path: &ProjectPath,
14577 cx: &mut App,
14578 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
14579 if path.path.extension().unwrap() == "png" {
14580 Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestPngItem {}))))
14581 } else {
14582 None
14583 }
14584 }
14585
14586 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
14587 None
14588 }
14589
14590 fn project_path(&self, _: &App) -> Option<ProjectPath> {
14591 None
14592 }
14593
14594 fn is_dirty(&self) -> bool {
14595 false
14596 }
14597 }
14598
14599 impl Item for TestPngItemView {
14600 type Event = ();
14601 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14602 "".into()
14603 }
14604 }
14605 impl EventEmitter<()> for TestPngItemView {}
14606 impl Focusable for TestPngItemView {
14607 fn focus_handle(&self, _cx: &App) -> FocusHandle {
14608 self.focus_handle.clone()
14609 }
14610 }
14611
14612 impl Render for TestPngItemView {
14613 fn render(
14614 &mut self,
14615 _window: &mut Window,
14616 _cx: &mut Context<Self>,
14617 ) -> impl IntoElement {
14618 Empty
14619 }
14620 }
14621
14622 impl ProjectItem for TestPngItemView {
14623 type Item = TestPngItem;
14624
14625 fn for_project_item(
14626 _project: Entity<Project>,
14627 _pane: Option<&Pane>,
14628 _item: Entity<Self::Item>,
14629 _: &mut Window,
14630 cx: &mut Context<Self>,
14631 ) -> Self
14632 where
14633 Self: Sized,
14634 {
14635 Self {
14636 focus_handle: cx.focus_handle(),
14637 }
14638 }
14639 }
14640
14641 // View
14642 struct TestIpynbItemView {
14643 focus_handle: FocusHandle,
14644 }
14645 // Model
14646 struct TestIpynbItem {}
14647
14648 impl project::ProjectItem for TestIpynbItem {
14649 fn try_open(
14650 _project: &Entity<Project>,
14651 path: &ProjectPath,
14652 cx: &mut App,
14653 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
14654 if path.path.extension().unwrap() == "ipynb" {
14655 Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestIpynbItem {}))))
14656 } else {
14657 None
14658 }
14659 }
14660
14661 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
14662 None
14663 }
14664
14665 fn project_path(&self, _: &App) -> Option<ProjectPath> {
14666 None
14667 }
14668
14669 fn is_dirty(&self) -> bool {
14670 false
14671 }
14672 }
14673
14674 impl Item for TestIpynbItemView {
14675 type Event = ();
14676 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14677 "".into()
14678 }
14679 }
14680 impl EventEmitter<()> for TestIpynbItemView {}
14681 impl Focusable for TestIpynbItemView {
14682 fn focus_handle(&self, _cx: &App) -> FocusHandle {
14683 self.focus_handle.clone()
14684 }
14685 }
14686
14687 impl Render for TestIpynbItemView {
14688 fn render(
14689 &mut self,
14690 _window: &mut Window,
14691 _cx: &mut Context<Self>,
14692 ) -> impl IntoElement {
14693 Empty
14694 }
14695 }
14696
14697 impl ProjectItem for TestIpynbItemView {
14698 type Item = TestIpynbItem;
14699
14700 fn for_project_item(
14701 _project: Entity<Project>,
14702 _pane: Option<&Pane>,
14703 _item: Entity<Self::Item>,
14704 _: &mut Window,
14705 cx: &mut Context<Self>,
14706 ) -> Self
14707 where
14708 Self: Sized,
14709 {
14710 Self {
14711 focus_handle: cx.focus_handle(),
14712 }
14713 }
14714 }
14715
14716 struct TestAlternatePngItemView {
14717 focus_handle: FocusHandle,
14718 }
14719
14720 impl Item for TestAlternatePngItemView {
14721 type Event = ();
14722 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14723 "".into()
14724 }
14725 }
14726
14727 impl EventEmitter<()> for TestAlternatePngItemView {}
14728 impl Focusable for TestAlternatePngItemView {
14729 fn focus_handle(&self, _cx: &App) -> FocusHandle {
14730 self.focus_handle.clone()
14731 }
14732 }
14733
14734 impl Render for TestAlternatePngItemView {
14735 fn render(
14736 &mut self,
14737 _window: &mut Window,
14738 _cx: &mut Context<Self>,
14739 ) -> impl IntoElement {
14740 Empty
14741 }
14742 }
14743
14744 impl ProjectItem for TestAlternatePngItemView {
14745 type Item = TestPngItem;
14746
14747 fn for_project_item(
14748 _project: Entity<Project>,
14749 _pane: Option<&Pane>,
14750 _item: Entity<Self::Item>,
14751 _: &mut Window,
14752 cx: &mut Context<Self>,
14753 ) -> Self
14754 where
14755 Self: Sized,
14756 {
14757 Self {
14758 focus_handle: cx.focus_handle(),
14759 }
14760 }
14761 }
14762
14763 #[gpui::test]
14764 async fn test_register_project_item(cx: &mut TestAppContext) {
14765 init_test(cx);
14766
14767 cx.update(|cx| {
14768 register_project_item::<TestPngItemView>(cx);
14769 register_project_item::<TestIpynbItemView>(cx);
14770 });
14771
14772 let fs = FakeFs::new(cx.executor());
14773 fs.insert_tree(
14774 "/root1",
14775 json!({
14776 "one.png": "BINARYDATAHERE",
14777 "two.ipynb": "{ totally a notebook }",
14778 "three.txt": "editing text, sure why not?"
14779 }),
14780 )
14781 .await;
14782
14783 let project = Project::test(fs, ["root1".as_ref()], cx).await;
14784 let (workspace, cx) =
14785 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14786
14787 let worktree_id = project.update(cx, |project, cx| {
14788 project.worktrees(cx).next().unwrap().read(cx).id()
14789 });
14790
14791 let handle = workspace
14792 .update_in(cx, |workspace, window, cx| {
14793 let project_path = (worktree_id, rel_path("one.png"));
14794 workspace.open_path(project_path, None, true, window, cx)
14795 })
14796 .await
14797 .unwrap();
14798
14799 // Now we can check if the handle we got back errored or not
14800 assert_eq!(
14801 handle.to_any_view().entity_type(),
14802 TypeId::of::<TestPngItemView>()
14803 );
14804
14805 let handle = workspace
14806 .update_in(cx, |workspace, window, cx| {
14807 let project_path = (worktree_id, rel_path("two.ipynb"));
14808 workspace.open_path(project_path, None, true, window, cx)
14809 })
14810 .await
14811 .unwrap();
14812
14813 assert_eq!(
14814 handle.to_any_view().entity_type(),
14815 TypeId::of::<TestIpynbItemView>()
14816 );
14817
14818 let handle = workspace
14819 .update_in(cx, |workspace, window, cx| {
14820 let project_path = (worktree_id, rel_path("three.txt"));
14821 workspace.open_path(project_path, None, true, window, cx)
14822 })
14823 .await;
14824 assert!(handle.is_err());
14825 }
14826
14827 #[gpui::test]
14828 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
14829 init_test(cx);
14830
14831 cx.update(|cx| {
14832 register_project_item::<TestPngItemView>(cx);
14833 register_project_item::<TestAlternatePngItemView>(cx);
14834 });
14835
14836 let fs = FakeFs::new(cx.executor());
14837 fs.insert_tree(
14838 "/root1",
14839 json!({
14840 "one.png": "BINARYDATAHERE",
14841 "two.ipynb": "{ totally a notebook }",
14842 "three.txt": "editing text, sure why not?"
14843 }),
14844 )
14845 .await;
14846 let project = Project::test(fs, ["root1".as_ref()], cx).await;
14847 let (workspace, cx) =
14848 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14849 let worktree_id = project.update(cx, |project, cx| {
14850 project.worktrees(cx).next().unwrap().read(cx).id()
14851 });
14852
14853 let handle = workspace
14854 .update_in(cx, |workspace, window, cx| {
14855 let project_path = (worktree_id, rel_path("one.png"));
14856 workspace.open_path(project_path, None, true, window, cx)
14857 })
14858 .await
14859 .unwrap();
14860
14861 // This _must_ be the second item registered
14862 assert_eq!(
14863 handle.to_any_view().entity_type(),
14864 TypeId::of::<TestAlternatePngItemView>()
14865 );
14866
14867 let handle = workspace
14868 .update_in(cx, |workspace, window, cx| {
14869 let project_path = (worktree_id, rel_path("three.txt"));
14870 workspace.open_path(project_path, None, true, window, cx)
14871 })
14872 .await;
14873 assert!(handle.is_err());
14874 }
14875 }
14876
14877 #[gpui::test]
14878 async fn test_status_bar_visibility(cx: &mut TestAppContext) {
14879 init_test(cx);
14880
14881 let fs = FakeFs::new(cx.executor());
14882 let project = Project::test(fs, [], cx).await;
14883 let (workspace, _cx) =
14884 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14885
14886 // Test with status bar shown (default)
14887 workspace.read_with(cx, |workspace, cx| {
14888 let visible = workspace.status_bar_visible(cx);
14889 assert!(visible, "Status bar should be visible by default");
14890 });
14891
14892 // Test with status bar hidden
14893 cx.update_global(|store: &mut SettingsStore, cx| {
14894 store.update_user_settings(cx, |settings| {
14895 settings.status_bar.get_or_insert_default().show = Some(false);
14896 });
14897 });
14898
14899 workspace.read_with(cx, |workspace, cx| {
14900 let visible = workspace.status_bar_visible(cx);
14901 assert!(!visible, "Status bar should be hidden when show is false");
14902 });
14903
14904 // Test with status bar shown explicitly
14905 cx.update_global(|store: &mut SettingsStore, cx| {
14906 store.update_user_settings(cx, |settings| {
14907 settings.status_bar.get_or_insert_default().show = Some(true);
14908 });
14909 });
14910
14911 workspace.read_with(cx, |workspace, cx| {
14912 let visible = workspace.status_bar_visible(cx);
14913 assert!(visible, "Status bar should be visible when show is true");
14914 });
14915 }
14916
14917 #[gpui::test]
14918 async fn test_pane_close_active_item(cx: &mut TestAppContext) {
14919 init_test(cx);
14920
14921 let fs = FakeFs::new(cx.executor());
14922 let project = Project::test(fs, [], cx).await;
14923 let (multi_workspace, cx) =
14924 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
14925 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
14926 let panel = workspace.update_in(cx, |workspace, window, cx| {
14927 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14928 workspace.add_panel(panel.clone(), window, cx);
14929
14930 workspace
14931 .right_dock()
14932 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
14933
14934 panel
14935 });
14936
14937 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14938 let item_a = cx.new(TestItem::new);
14939 let item_b = cx.new(TestItem::new);
14940 let item_a_id = item_a.entity_id();
14941 let item_b_id = item_b.entity_id();
14942
14943 pane.update_in(cx, |pane, window, cx| {
14944 pane.add_item(Box::new(item_a.clone()), true, true, None, window, cx);
14945 pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
14946 });
14947
14948 pane.read_with(cx, |pane, _| {
14949 assert_eq!(pane.items_len(), 2);
14950 assert_eq!(pane.active_item().unwrap().item_id(), item_b_id);
14951 });
14952
14953 workspace.update_in(cx, |workspace, window, cx| {
14954 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14955 });
14956
14957 workspace.update_in(cx, |_, window, cx| {
14958 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
14959 });
14960
14961 // Assert that the `pane::CloseActiveItem` action is handled at the
14962 // workspace level when one of the dock panels is focused and, in that
14963 // case, the center pane's active item is closed but the focus is not
14964 // moved.
14965 cx.dispatch_action(pane::CloseActiveItem::default());
14966 cx.run_until_parked();
14967
14968 pane.read_with(cx, |pane, _| {
14969 assert_eq!(pane.items_len(), 1);
14970 assert_eq!(pane.active_item().unwrap().item_id(), item_a_id);
14971 });
14972
14973 workspace.update_in(cx, |workspace, window, cx| {
14974 assert!(workspace.right_dock().read(cx).is_open());
14975 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
14976 });
14977 }
14978
14979 #[gpui::test]
14980 async fn test_panel_zoom_preserved_across_workspace_switch(cx: &mut TestAppContext) {
14981 init_test(cx);
14982 let fs = FakeFs::new(cx.executor());
14983
14984 let project_a = Project::test(fs.clone(), [], cx).await;
14985 let project_b = Project::test(fs, [], cx).await;
14986
14987 let multi_workspace_handle =
14988 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
14989 cx.run_until_parked();
14990
14991 multi_workspace_handle
14992 .update(cx, |mw, _window, cx| {
14993 mw.open_sidebar(cx);
14994 })
14995 .unwrap();
14996
14997 let workspace_a = multi_workspace_handle
14998 .read_with(cx, |mw, _| mw.workspace().clone())
14999 .unwrap();
15000
15001 let _workspace_b = multi_workspace_handle
15002 .update(cx, |mw, window, cx| {
15003 mw.test_add_workspace(project_b, window, cx)
15004 })
15005 .unwrap();
15006
15007 // Switch to workspace A
15008 multi_workspace_handle
15009 .update(cx, |mw, window, cx| {
15010 let workspace = mw.workspaces().next().unwrap().clone();
15011 mw.activate(workspace, None, window, cx);
15012 })
15013 .unwrap();
15014
15015 let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
15016
15017 // Add a panel to workspace A's right dock and open the dock
15018 let panel = workspace_a.update_in(cx, |workspace, window, cx| {
15019 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
15020 workspace.add_panel(panel.clone(), window, cx);
15021 workspace
15022 .right_dock()
15023 .update(cx, |dock, cx| dock.set_open(true, window, cx));
15024 panel
15025 });
15026
15027 // Focus the panel through the workspace (matching existing test pattern)
15028 workspace_a.update_in(cx, |workspace, window, cx| {
15029 workspace.toggle_panel_focus::<TestPanel>(window, cx);
15030 });
15031
15032 // Zoom the panel
15033 panel.update_in(cx, |panel, window, cx| {
15034 panel.set_zoomed(true, window, cx);
15035 });
15036
15037 // Verify the panel is zoomed and the dock is open
15038 workspace_a.update_in(cx, |workspace, window, cx| {
15039 assert!(
15040 workspace.right_dock().read(cx).is_open(),
15041 "dock should be open before switch"
15042 );
15043 assert!(
15044 panel.is_zoomed(window, cx),
15045 "panel should be zoomed before switch"
15046 );
15047 assert!(
15048 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
15049 "panel should be focused before switch"
15050 );
15051 });
15052
15053 // Switch to workspace B
15054 multi_workspace_handle
15055 .update(cx, |mw, window, cx| {
15056 let workspace = mw.workspaces().nth(1).unwrap().clone();
15057 mw.activate(workspace, None, window, cx);
15058 })
15059 .unwrap();
15060 cx.run_until_parked();
15061
15062 // Switch back to workspace A
15063 multi_workspace_handle
15064 .update(cx, |mw, window, cx| {
15065 let workspace = mw.workspaces().next().unwrap().clone();
15066 mw.activate(workspace, None, window, cx);
15067 })
15068 .unwrap();
15069 cx.run_until_parked();
15070
15071 // Verify the panel is still zoomed and the dock is still open
15072 workspace_a.update_in(cx, |workspace, window, cx| {
15073 assert!(
15074 workspace.right_dock().read(cx).is_open(),
15075 "dock should still be open after switching back"
15076 );
15077 assert!(
15078 panel.is_zoomed(window, cx),
15079 "panel should still be zoomed after switching back"
15080 );
15081 });
15082 }
15083
15084 fn pane_items_paths(pane: &Entity<Pane>, cx: &App) -> Vec<String> {
15085 pane.read(cx)
15086 .items()
15087 .flat_map(|item| {
15088 item.project_paths(cx)
15089 .into_iter()
15090 .map(|path| path.path.display(PathStyle::local()).into_owned())
15091 })
15092 .collect()
15093 }
15094
15095 pub fn init_test(cx: &mut TestAppContext) {
15096 cx.update(|cx| {
15097 let settings_store = SettingsStore::test(cx);
15098 cx.set_global(settings_store);
15099 cx.set_global(db::AppDatabase::test_new());
15100 theme_settings::init(theme::LoadThemes::JustBase, cx);
15101 });
15102 }
15103
15104 #[gpui::test]
15105 async fn test_toggle_theme_mode_persists_and_updates_active_theme(cx: &mut TestAppContext) {
15106 use settings::{ThemeName, ThemeSelection};
15107 use theme::SystemAppearance;
15108 use zed_actions::theme::ToggleMode;
15109
15110 init_test(cx);
15111
15112 let fs = FakeFs::new(cx.executor());
15113 let settings_fs: Arc<dyn fs::Fs> = fs.clone();
15114
15115 fs.insert_tree(path!("/root"), json!({ "file.rs": "fn main() {}\n" }))
15116 .await;
15117
15118 // Build a test project and workspace view so the test can invoke
15119 // the workspace action handler the same way the UI would.
15120 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
15121 let (workspace, cx) =
15122 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
15123
15124 // Seed the settings file with a plain static light theme so the
15125 // first toggle always starts from a known persisted state.
15126 workspace.update_in(cx, |_workspace, _window, cx| {
15127 *SystemAppearance::global_mut(cx) = SystemAppearance(theme::Appearance::Light);
15128 settings::update_settings_file(settings_fs.clone(), cx, |settings, _cx| {
15129 settings.theme.theme = Some(ThemeSelection::Static(ThemeName("One Light".into())));
15130 });
15131 });
15132 cx.executor().advance_clock(Duration::from_millis(200));
15133 cx.run_until_parked();
15134
15135 // Confirm the initial persisted settings contain the static theme
15136 // we just wrote before any toggling happens.
15137 let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
15138 assert!(settings_text.contains(r#""theme": "One Light""#));
15139
15140 // Toggle once. This should migrate the persisted theme settings
15141 // into light/dark slots and enable system mode.
15142 workspace.update_in(cx, |workspace, window, cx| {
15143 workspace.toggle_theme_mode(&ToggleMode, window, cx);
15144 });
15145 cx.executor().advance_clock(Duration::from_millis(200));
15146 cx.run_until_parked();
15147
15148 // 1. Static -> Dynamic
15149 // this assertion checks theme changed from static to dynamic.
15150 let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
15151 let parsed: serde_json::Value = settings::parse_json_with_comments(&settings_text).unwrap();
15152 assert_eq!(
15153 parsed["theme"],
15154 serde_json::json!({
15155 "mode": "system",
15156 "light": "One Light",
15157 "dark": "One Dark"
15158 })
15159 );
15160
15161 // 2. Toggle again, suppose it will change the mode to light
15162 workspace.update_in(cx, |workspace, window, cx| {
15163 workspace.toggle_theme_mode(&ToggleMode, window, cx);
15164 });
15165 cx.executor().advance_clock(Duration::from_millis(200));
15166 cx.run_until_parked();
15167
15168 let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
15169 assert!(settings_text.contains(r#""mode": "light""#));
15170 }
15171
15172 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
15173 let item = TestProjectItem::new(id, path, cx);
15174 item.update(cx, |item, _| {
15175 item.is_dirty = true;
15176 });
15177 item
15178 }
15179
15180 #[gpui::test]
15181 async fn test_zoomed_panel_without_pane_preserved_on_center_focus(
15182 cx: &mut gpui::TestAppContext,
15183 ) {
15184 init_test(cx);
15185 let fs = FakeFs::new(cx.executor());
15186
15187 let project = Project::test(fs, [], cx).await;
15188 let (workspace, cx) =
15189 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
15190
15191 let panel = workspace.update_in(cx, |workspace, window, cx| {
15192 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
15193 workspace.add_panel(panel.clone(), window, cx);
15194 workspace
15195 .right_dock()
15196 .update(cx, |dock, cx| dock.set_open(true, window, cx));
15197 panel
15198 });
15199
15200 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
15201 pane.update_in(cx, |pane, window, cx| {
15202 let item = cx.new(TestItem::new);
15203 pane.add_item(Box::new(item), true, true, None, window, cx);
15204 });
15205
15206 // Transfer focus to the panel, then zoom it. Using toggle_panel_focus
15207 // mirrors the real-world flow and avoids side effects from directly
15208 // focusing the panel while the center pane is active.
15209 workspace.update_in(cx, |workspace, window, cx| {
15210 workspace.toggle_panel_focus::<TestPanel>(window, cx);
15211 });
15212
15213 panel.update_in(cx, |panel, window, cx| {
15214 panel.set_zoomed(true, window, cx);
15215 });
15216
15217 workspace.update_in(cx, |workspace, window, cx| {
15218 assert!(workspace.right_dock().read(cx).is_open());
15219 assert!(panel.is_zoomed(window, cx));
15220 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
15221 });
15222
15223 // Simulate a spurious pane::Event::Focus on the center pane while the
15224 // panel still has focus. This mirrors what happens during macOS window
15225 // activation: the center pane fires a focus event even though actual
15226 // focus remains on the dock panel.
15227 pane.update_in(cx, |_, _, cx| {
15228 cx.emit(pane::Event::Focus);
15229 });
15230
15231 // The dock must remain open because the panel had focus at the time the
15232 // event was processed. Before the fix, dock_to_preserve was None for
15233 // panels that don't implement pane(), causing the dock to close.
15234 workspace.update_in(cx, |workspace, window, cx| {
15235 assert!(
15236 workspace.right_dock().read(cx).is_open(),
15237 "Dock should stay open when its zoomed panel (without pane()) still has focus"
15238 );
15239 assert!(panel.is_zoomed(window, cx));
15240 });
15241 }
15242
15243 #[gpui::test]
15244 async fn test_panels_stay_open_after_position_change_and_settings_update(
15245 cx: &mut gpui::TestAppContext,
15246 ) {
15247 init_test(cx);
15248 let fs = FakeFs::new(cx.executor());
15249 let project = Project::test(fs, [], cx).await;
15250 let (workspace, cx) =
15251 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
15252
15253 // Add two panels to the left dock and open it.
15254 let (panel_a, panel_b) = workspace.update_in(cx, |workspace, window, cx| {
15255 let panel_a = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
15256 let panel_b = cx.new(|cx| TestPanel::new(DockPosition::Left, 101, cx));
15257 workspace.add_panel(panel_a.clone(), window, cx);
15258 workspace.add_panel(panel_b.clone(), window, cx);
15259 workspace.left_dock().update(cx, |dock, cx| {
15260 dock.set_open(true, window, cx);
15261 dock.activate_panel(0, window, cx);
15262 });
15263 (panel_a, panel_b)
15264 });
15265
15266 workspace.update_in(cx, |workspace, _, cx| {
15267 assert!(workspace.left_dock().read(cx).is_open());
15268 });
15269
15270 // Simulate a feature flag changing default dock positions: both panels
15271 // move from Left to Right.
15272 workspace.update_in(cx, |_workspace, _window, cx| {
15273 panel_a.update(cx, |p, _cx| p.position = DockPosition::Right);
15274 panel_b.update(cx, |p, _cx| p.position = DockPosition::Right);
15275 cx.update_global::<SettingsStore, _>(|_, _| {});
15276 });
15277
15278 // Both panels should now be in the right dock.
15279 workspace.update_in(cx, |workspace, _, cx| {
15280 let right_dock = workspace.right_dock().read(cx);
15281 assert_eq!(right_dock.panels_len(), 2);
15282 });
15283
15284 // Open the right dock and activate panel_b (simulating the user
15285 // opening the panel after it moved).
15286 workspace.update_in(cx, |workspace, window, cx| {
15287 workspace.right_dock().update(cx, |dock, cx| {
15288 dock.set_open(true, window, cx);
15289 dock.activate_panel(1, window, cx);
15290 });
15291 });
15292
15293 // Now trigger another SettingsStore change
15294 workspace.update_in(cx, |_workspace, _window, cx| {
15295 cx.update_global::<SettingsStore, _>(|_, _| {});
15296 });
15297
15298 workspace.update_in(cx, |workspace, _, cx| {
15299 assert!(
15300 workspace.right_dock().read(cx).is_open(),
15301 "Right dock should still be open after a settings change"
15302 );
15303 assert_eq!(
15304 workspace.right_dock().read(cx).panels_len(),
15305 2,
15306 "Both panels should still be in the right dock"
15307 );
15308 });
15309 }
15310
15311 #[gpui::test]
15312 async fn test_most_recent_active_path_skips_read_only_paths(cx: &mut TestAppContext) {
15313 init_test(cx);
15314
15315 let fs = FakeFs::new(cx.executor());
15316 fs.insert_tree(
15317 path!("/project"),
15318 json!({
15319 "src": { "main.py": "" },
15320 ".venv": { "lib": { "dep.py": "" } },
15321 }),
15322 )
15323 .await;
15324
15325 let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
15326 let (workspace, cx) =
15327 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
15328 let worktree_id = project.update(cx, |project, cx| {
15329 project.worktrees(cx).next().unwrap().read(cx).id()
15330 });
15331
15332 // Configure .venv as read-only
15333 workspace.update_in(cx, |_workspace, _window, cx| {
15334 cx.update_global::<SettingsStore, _>(|store, cx| {
15335 store
15336 .set_user_settings(r#"{"read_only_files": ["**/.venv/**"]}"#, cx)
15337 .ok();
15338 });
15339 });
15340
15341 let item_dep = cx.new(|cx| {
15342 TestItem::new(cx).with_project_items(&[TestProjectItem::new_in_worktree(
15343 1001,
15344 ".venv/lib/dep.py",
15345 worktree_id,
15346 cx,
15347 )])
15348 });
15349
15350 // dep.py is active but matches read_only_files → should be skipped
15351 workspace.update_in(cx, |workspace, window, cx| {
15352 workspace.add_item_to_active_pane(Box::new(item_dep.clone()), None, true, window, cx);
15353 });
15354 let path = workspace.read_with(cx, |workspace, cx| workspace.most_recent_active_path(cx));
15355 assert_eq!(path, None);
15356 }
15357}