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;
8pub mod notifications;
9pub mod pane;
10pub mod pane_group;
11pub mod path_list {
12 pub use util::path_list::{PathList, SerializedPathList};
13}
14mod persistence;
15pub mod searchable;
16mod security_modal;
17pub mod shared_screen;
18use db::smol::future::yield_now;
19pub use shared_screen::SharedScreen;
20mod status_bar;
21pub mod tasks;
22mod theme_preview;
23mod toast_layer;
24mod toolbar;
25pub mod welcome;
26mod workspace_settings;
27
28pub use crate::notifications::NotificationFrame;
29pub use dock::Panel;
30pub use multi_workspace::{
31 CloseWorkspaceSidebar, DraggedSidebar, FocusWorkspaceSidebar, MultiWorkspace,
32 MultiWorkspaceEvent, NextWorkspace, PreviousWorkspace, Sidebar, SidebarHandle,
33 SidebarRenderState, SidebarSide, ToggleWorkspaceSidebar, sidebar_side_context_menu,
34};
35pub use path_list::{PathList, SerializedPathList};
36pub use toast_layer::{ToastAction, ToastLayer, ToastView};
37
38use anyhow::{Context as _, Result, anyhow};
39use client::{
40 ChannelId, Client, ErrorExt, ParticipantIndex, Status, TypedEnvelope, User, UserStore,
41 proto::{self, ErrorCode, PanelId, PeerId},
42};
43use collections::{HashMap, HashSet, hash_map};
44use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
45use fs::Fs;
46use futures::{
47 Future, FutureExt, StreamExt,
48 channel::{
49 mpsc::{self, UnboundedReceiver, UnboundedSender},
50 oneshot,
51 },
52 future::{Shared, try_join_all},
53};
54use gpui::{
55 Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Axis, Bounds,
56 Context, CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
57 Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView, MouseButton,
58 PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription,
59 SystemWindowTabController, Task, Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId,
60 WindowOptions, actions, canvas, point, relative, size, transparent_black,
61};
62pub use history_manager::*;
63pub use item::{
64 FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
65 ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
66};
67use itertools::Itertools;
68use language::{Buffer, LanguageRegistry, Rope, language_settings::all_language_settings};
69pub use modal_layer::*;
70use node_runtime::NodeRuntime;
71use notifications::{
72 DetachAndPromptErr, Notifications, dismiss_app_notification,
73 simple_message_notification::MessageNotification,
74};
75pub use pane::*;
76pub use pane_group::{
77 ActivePaneDecorator, HANDLE_HITBOX_SIZE, Member, PaneAxis, PaneGroup, PaneRenderContext,
78 SplitDirection,
79};
80use persistence::{SerializedWindowBounds, model::SerializedWorkspace};
81pub use persistence::{
82 WorkspaceDb, delete_unloaded_items,
83 model::{
84 DockStructure, ItemId, SerializedMultiWorkspace, SerializedWorkspaceLocation,
85 SessionWorkspace,
86 },
87 read_serialized_multi_workspaces, resolve_worktree_workspaces,
88};
89use postage::stream::Stream;
90use project::{
91 DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
92 WorktreeSettings,
93 debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
94 project_settings::ProjectSettings,
95 toolchain_store::ToolchainStoreEvent,
96 trusted_worktrees::{RemoteHostLocation, TrustedWorktrees, TrustedWorktreesEvent},
97};
98use remote::{
99 RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions,
100 remote_client::ConnectionIdentifier,
101};
102use schemars::JsonSchema;
103use serde::Deserialize;
104use session::AppSession;
105use settings::{
106 CenteredPaddingSettings, Settings, SettingsLocation, SettingsStore, update_settings_file,
107};
108
109use sqlez::{
110 bindable::{Bind, Column, StaticColumnCount},
111 statement::Statement,
112};
113use status_bar::StatusBar;
114pub use status_bar::StatusItemView;
115use std::{
116 any::TypeId,
117 borrow::Cow,
118 cell::RefCell,
119 cmp,
120 collections::VecDeque,
121 env,
122 hash::Hash,
123 path::{Path, PathBuf},
124 process::ExitStatus,
125 rc::Rc,
126 sync::{
127 Arc, LazyLock, Weak,
128 atomic::{AtomicBool, AtomicUsize},
129 },
130 time::Duration,
131};
132use task::{DebugScenario, SharedTaskContext, SpawnInTerminal};
133use theme::{ActiveTheme, SystemAppearance};
134use theme_settings::ThemeSettings;
135pub use toolbar::{
136 PaneSearchBarCallbacks, Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
137};
138pub use ui;
139use ui::{Window, prelude::*};
140use util::{
141 ResultExt, TryFutureExt,
142 paths::{PathStyle, SanitizedPath},
143 rel_path::RelPath,
144 serde::default_true,
145};
146use uuid::Uuid;
147pub use workspace_settings::{
148 AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, StatusBarSettings, TabBarSettings,
149 WorkspaceSettings,
150};
151use zed_actions::{Spawn, feedback::FileBugReport, theme::ToggleMode};
152
153use crate::{item::ItemBufferKind, notifications::NotificationId};
154use crate::{
155 persistence::{
156 SerializedAxis,
157 model::{DockData, SerializedItem, SerializedPane, SerializedPaneGroup},
158 },
159 security_modal::SecurityModal,
160};
161
162pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200);
163
164static ZED_WINDOW_SIZE: LazyLock<Option<Size<Pixels>>> = LazyLock::new(|| {
165 env::var("ZED_WINDOW_SIZE")
166 .ok()
167 .as_deref()
168 .and_then(parse_pixel_size_env_var)
169});
170
171static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
172 env::var("ZED_WINDOW_POSITION")
173 .ok()
174 .as_deref()
175 .and_then(parse_pixel_position_env_var)
176});
177
178pub trait TerminalProvider {
179 fn spawn(
180 &self,
181 task: SpawnInTerminal,
182 window: &mut Window,
183 cx: &mut App,
184 ) -> Task<Option<Result<ExitStatus>>>;
185}
186
187pub trait DebuggerProvider {
188 // `active_buffer` is used to resolve build task's name against language-specific tasks.
189 fn start_session(
190 &self,
191 definition: DebugScenario,
192 task_context: SharedTaskContext,
193 active_buffer: Option<Entity<Buffer>>,
194 worktree_id: Option<WorktreeId>,
195 window: &mut Window,
196 cx: &mut App,
197 );
198
199 fn spawn_task_or_modal(
200 &self,
201 workspace: &mut Workspace,
202 action: &Spawn,
203 window: &mut Window,
204 cx: &mut Context<Workspace>,
205 );
206
207 fn task_scheduled(&self, cx: &mut App);
208 fn debug_scenario_scheduled(&self, cx: &mut App);
209 fn debug_scenario_scheduled_last(&self, cx: &App) -> bool;
210
211 fn active_thread_state(&self, cx: &App) -> Option<ThreadStatus>;
212}
213
214/// Opens a file or directory.
215#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
216#[action(namespace = workspace)]
217pub struct Open {
218 /// When true, opens in a new window. When false, adds to the current
219 /// window as a new workspace (multi-workspace).
220 #[serde(default = "Open::default_create_new_window")]
221 pub create_new_window: bool,
222}
223
224impl Open {
225 pub const DEFAULT: Self = Self {
226 create_new_window: true,
227 };
228
229 /// Used by `#[serde(default)]` on the `create_new_window` field so that
230 /// the serde default and `Open::DEFAULT` stay in sync.
231 fn default_create_new_window() -> bool {
232 Self::DEFAULT.create_new_window
233 }
234}
235
236impl Default for Open {
237 fn default() -> Self {
238 Self::DEFAULT
239 }
240}
241
242actions!(
243 workspace,
244 [
245 /// Activates the next pane in the workspace.
246 ActivateNextPane,
247 /// Activates the previous pane in the workspace.
248 ActivatePreviousPane,
249 /// Activates the last pane in the workspace.
250 ActivateLastPane,
251 /// Switches to the next window.
252 ActivateNextWindow,
253 /// Switches to the previous window.
254 ActivatePreviousWindow,
255 /// Adds a folder to the current project.
256 AddFolderToProject,
257 /// Clears all notifications.
258 ClearAllNotifications,
259 /// Clears all navigation history, including forward/backward navigation, recently opened files, and recently closed tabs. **This action is irreversible**.
260 ClearNavigationHistory,
261 /// Closes the active dock.
262 CloseActiveDock,
263 /// Closes all docks.
264 CloseAllDocks,
265 /// Toggles all docks.
266 ToggleAllDocks,
267 /// Closes the current window.
268 CloseWindow,
269 /// Closes the current project.
270 CloseProject,
271 /// Opens the feedback dialog.
272 Feedback,
273 /// Follows the next collaborator in the session.
274 FollowNextCollaborator,
275 /// Moves the focused panel to the next position.
276 MoveFocusedPanelToNextPosition,
277 /// Creates a new file.
278 NewFile,
279 /// Creates a new file in a vertical split.
280 NewFileSplitVertical,
281 /// Creates a new file in a horizontal split.
282 NewFileSplitHorizontal,
283 /// Opens a new search.
284 NewSearch,
285 /// Opens a new window.
286 NewWindow,
287 /// Opens multiple files.
288 OpenFiles,
289 /// Opens the current location in terminal.
290 OpenInTerminal,
291 /// Opens the component preview.
292 OpenComponentPreview,
293 /// Reloads the active item.
294 ReloadActiveItem,
295 /// Resets the active dock to its default size.
296 ResetActiveDockSize,
297 /// Resets all open docks to their default sizes.
298 ResetOpenDocksSize,
299 /// Reloads the application
300 Reload,
301 /// Saves the current file with a new name.
302 SaveAs,
303 /// Saves without formatting.
304 SaveWithoutFormat,
305 /// Shuts down all debug adapters.
306 ShutdownDebugAdapters,
307 /// Suppresses the current notification.
308 SuppressNotification,
309 /// Toggles the bottom dock.
310 ToggleBottomDock,
311 /// Toggles centered layout mode.
312 ToggleCenteredLayout,
313 /// Toggles edit prediction feature globally for all files.
314 ToggleEditPrediction,
315 /// Toggles the left dock.
316 ToggleLeftDock,
317 /// Toggles the right dock.
318 ToggleRightDock,
319 /// Toggles zoom on the active pane.
320 ToggleZoom,
321 /// Toggles read-only mode for the active item (if supported by that item).
322 ToggleReadOnlyFile,
323 /// Zooms in on the active pane.
324 ZoomIn,
325 /// Zooms out of the active pane.
326 ZoomOut,
327 /// If any worktrees are in restricted mode, shows a modal with possible actions.
328 /// If the modal is shown already, closes it without trusting any worktree.
329 ToggleWorktreeSecurity,
330 /// Clears all trusted worktrees, placing them in restricted mode on next open.
331 /// Requires restart to take effect on already opened projects.
332 ClearTrustedWorktrees,
333 /// Stops following a collaborator.
334 Unfollow,
335 /// Restores the banner.
336 RestoreBanner,
337 /// Toggles expansion of the selected item.
338 ToggleExpandItem,
339 ]
340);
341
342/// Activates a specific pane by its index.
343#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
344#[action(namespace = workspace)]
345pub struct ActivatePane(pub usize);
346
347/// Moves an item to a specific pane by index.
348#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
349#[action(namespace = workspace)]
350#[serde(deny_unknown_fields)]
351pub struct MoveItemToPane {
352 #[serde(default = "default_1")]
353 pub destination: usize,
354 #[serde(default = "default_true")]
355 pub focus: bool,
356 #[serde(default)]
357 pub clone: bool,
358}
359
360fn default_1() -> usize {
361 1
362}
363
364/// Moves an item to a pane in the specified direction.
365#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
366#[action(namespace = workspace)]
367#[serde(deny_unknown_fields)]
368pub struct MoveItemToPaneInDirection {
369 #[serde(default = "default_right")]
370 pub direction: SplitDirection,
371 #[serde(default = "default_true")]
372 pub focus: bool,
373 #[serde(default)]
374 pub clone: bool,
375}
376
377/// Creates a new file in a split of the desired direction.
378#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
379#[action(namespace = workspace)]
380#[serde(deny_unknown_fields)]
381pub struct NewFileSplit(pub SplitDirection);
382
383fn default_right() -> SplitDirection {
384 SplitDirection::Right
385}
386
387/// Saves all open files in the workspace.
388#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
389#[action(namespace = workspace)]
390#[serde(deny_unknown_fields)]
391pub struct SaveAll {
392 #[serde(default)]
393 pub save_intent: Option<SaveIntent>,
394}
395
396/// Saves the current file with the specified options.
397#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
398#[action(namespace = workspace)]
399#[serde(deny_unknown_fields)]
400pub struct Save {
401 #[serde(default)]
402 pub save_intent: Option<SaveIntent>,
403}
404
405/// Moves Focus to the central panes in the workspace.
406#[derive(Clone, Debug, PartialEq, Eq, Action)]
407#[action(namespace = workspace)]
408pub struct FocusCenterPane;
409
410/// Closes all items and panes in the workspace.
411#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
412#[action(namespace = workspace)]
413#[serde(deny_unknown_fields)]
414pub struct CloseAllItemsAndPanes {
415 #[serde(default)]
416 pub save_intent: Option<SaveIntent>,
417}
418
419/// Closes all inactive tabs and panes in the workspace.
420#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
421#[action(namespace = workspace)]
422#[serde(deny_unknown_fields)]
423pub struct CloseInactiveTabsAndPanes {
424 #[serde(default)]
425 pub save_intent: Option<SaveIntent>,
426}
427
428/// Closes the active item across all panes.
429#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
430#[action(namespace = workspace)]
431#[serde(deny_unknown_fields)]
432pub struct CloseItemInAllPanes {
433 #[serde(default)]
434 pub save_intent: Option<SaveIntent>,
435 #[serde(default)]
436 pub close_pinned: bool,
437}
438
439/// Sends a sequence of keystrokes to the active element.
440#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
441#[action(namespace = workspace)]
442pub struct SendKeystrokes(pub String);
443
444actions!(
445 project_symbols,
446 [
447 /// Toggles the project symbols search.
448 #[action(name = "Toggle")]
449 ToggleProjectSymbols
450 ]
451);
452
453/// Toggles the file finder interface.
454#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
455#[action(namespace = file_finder, name = "Toggle")]
456#[serde(deny_unknown_fields)]
457pub struct ToggleFileFinder {
458 #[serde(default)]
459 pub separate_history: bool,
460}
461
462/// Opens a new terminal in the center.
463#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
464#[action(namespace = workspace)]
465#[serde(deny_unknown_fields)]
466pub struct NewCenterTerminal {
467 /// If true, creates a local terminal even in remote projects.
468 #[serde(default)]
469 pub local: bool,
470}
471
472/// Opens a new terminal.
473#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
474#[action(namespace = workspace)]
475#[serde(deny_unknown_fields)]
476pub struct NewTerminal {
477 /// If true, creates a local terminal even in remote projects.
478 #[serde(default)]
479 pub local: bool,
480}
481
482/// Increases size of a currently focused dock by a given amount of pixels.
483#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
484#[action(namespace = workspace)]
485#[serde(deny_unknown_fields)]
486pub struct IncreaseActiveDockSize {
487 /// For 0px parameter, uses UI font size value.
488 #[serde(default)]
489 pub px: u32,
490}
491
492/// Decreases size of a currently focused dock by a given amount of pixels.
493#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
494#[action(namespace = workspace)]
495#[serde(deny_unknown_fields)]
496pub struct DecreaseActiveDockSize {
497 /// For 0px parameter, uses UI font size value.
498 #[serde(default)]
499 pub px: u32,
500}
501
502/// Increases size of all currently visible docks uniformly, by a given amount of pixels.
503#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
504#[action(namespace = workspace)]
505#[serde(deny_unknown_fields)]
506pub struct IncreaseOpenDocksSize {
507 /// For 0px parameter, uses UI font size value.
508 #[serde(default)]
509 pub px: u32,
510}
511
512/// Decreases size of all currently visible docks uniformly, by a given amount of pixels.
513#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
514#[action(namespace = workspace)]
515#[serde(deny_unknown_fields)]
516pub struct DecreaseOpenDocksSize {
517 /// For 0px parameter, uses UI font size value.
518 #[serde(default)]
519 pub px: u32,
520}
521
522actions!(
523 workspace,
524 [
525 /// Activates the pane to the left.
526 ActivatePaneLeft,
527 /// Activates the pane to the right.
528 ActivatePaneRight,
529 /// Activates the pane above.
530 ActivatePaneUp,
531 /// Activates the pane below.
532 ActivatePaneDown,
533 /// Swaps the current pane with the one to the left.
534 SwapPaneLeft,
535 /// Swaps the current pane with the one to the right.
536 SwapPaneRight,
537 /// Swaps the current pane with the one above.
538 SwapPaneUp,
539 /// Swaps the current pane with the one below.
540 SwapPaneDown,
541 // Swaps the current pane with the first available adjacent pane (searching in order: below, above, right, left) and activates that pane.
542 SwapPaneAdjacent,
543 /// Move the current pane to be at the far left.
544 MovePaneLeft,
545 /// Move the current pane to be at the far right.
546 MovePaneRight,
547 /// Move the current pane to be at the very top.
548 MovePaneUp,
549 /// Move the current pane to be at the very bottom.
550 MovePaneDown,
551 ]
552);
553
554#[derive(PartialEq, Eq, Debug)]
555pub enum CloseIntent {
556 /// Quit the program entirely.
557 Quit,
558 /// Close a window.
559 CloseWindow,
560 /// Replace the workspace in an existing window.
561 ReplaceWindow,
562}
563
564#[derive(Clone)]
565pub struct Toast {
566 id: NotificationId,
567 msg: Cow<'static, str>,
568 autohide: bool,
569 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut Window, &mut App)>)>,
570}
571
572impl Toast {
573 pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
574 Toast {
575 id,
576 msg: msg.into(),
577 on_click: None,
578 autohide: false,
579 }
580 }
581
582 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
583 where
584 M: Into<Cow<'static, str>>,
585 F: Fn(&mut Window, &mut App) + 'static,
586 {
587 self.on_click = Some((message.into(), Arc::new(on_click)));
588 self
589 }
590
591 pub fn autohide(mut self) -> Self {
592 self.autohide = true;
593 self
594 }
595}
596
597impl PartialEq for Toast {
598 fn eq(&self, other: &Self) -> bool {
599 self.id == other.id
600 && self.msg == other.msg
601 && self.on_click.is_some() == other.on_click.is_some()
602 }
603}
604
605/// Opens a new terminal with the specified working directory.
606#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema, Action)]
607#[action(namespace = workspace)]
608#[serde(deny_unknown_fields)]
609pub struct OpenTerminal {
610 pub working_directory: PathBuf,
611 /// If true, creates a local terminal even in remote projects.
612 #[serde(default)]
613 pub local: bool,
614}
615
616#[derive(
617 Clone,
618 Copy,
619 Debug,
620 Default,
621 Hash,
622 PartialEq,
623 Eq,
624 PartialOrd,
625 Ord,
626 serde::Serialize,
627 serde::Deserialize,
628)]
629pub struct WorkspaceId(i64);
630
631impl WorkspaceId {
632 pub fn from_i64(value: i64) -> Self {
633 Self(value)
634 }
635}
636
637impl StaticColumnCount for WorkspaceId {}
638impl Bind for WorkspaceId {
639 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
640 self.0.bind(statement, start_index)
641 }
642}
643impl Column for WorkspaceId {
644 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
645 i64::column(statement, start_index)
646 .map(|(i, next_index)| (Self(i), next_index))
647 .with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
648 }
649}
650impl From<WorkspaceId> for i64 {
651 fn from(val: WorkspaceId) -> Self {
652 val.0
653 }
654}
655
656fn prompt_and_open_paths(app_state: Arc<AppState>, options: PathPromptOptions, cx: &mut App) {
657 if let Some(workspace_window) = local_workspace_windows(cx).into_iter().next() {
658 workspace_window
659 .update(cx, |multi_workspace, window, cx| {
660 let workspace = multi_workspace.workspace().clone();
661 workspace.update(cx, |workspace, cx| {
662 prompt_for_open_path_and_open(workspace, app_state, options, true, window, cx);
663 });
664 })
665 .ok();
666 } else {
667 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, None, None, true, cx);
668 cx.spawn(async move |cx| {
669 let OpenResult { window, .. } = task.await?;
670 window.update(cx, |multi_workspace, window, cx| {
671 window.activate_window();
672 let workspace = multi_workspace.workspace().clone();
673 workspace.update(cx, |workspace, cx| {
674 prompt_for_open_path_and_open(workspace, app_state, options, true, window, cx);
675 });
676 })?;
677 anyhow::Ok(())
678 })
679 .detach_and_log_err(cx);
680 }
681}
682
683pub fn prompt_for_open_path_and_open(
684 workspace: &mut Workspace,
685 app_state: Arc<AppState>,
686 options: PathPromptOptions,
687 create_new_window: bool,
688 window: &mut Window,
689 cx: &mut Context<Workspace>,
690) {
691 let paths = workspace.prompt_for_open_path(
692 options,
693 DirectoryLister::Local(workspace.project().clone(), app_state.fs.clone()),
694 window,
695 cx,
696 );
697 let multi_workspace_handle = window.window_handle().downcast::<MultiWorkspace>();
698 cx.spawn_in(window, async move |this, cx| {
699 let Some(paths) = paths.await.log_err().flatten() else {
700 return;
701 };
702 if !create_new_window {
703 if let Some(handle) = multi_workspace_handle {
704 if let Some(task) = handle
705 .update(cx, |multi_workspace, window, cx| {
706 multi_workspace.open_project(paths, window, cx)
707 })
708 .log_err()
709 {
710 task.await.log_err();
711 }
712 return;
713 }
714 }
715 if let Some(task) = this
716 .update_in(cx, |this, window, cx| {
717 this.open_workspace_for_paths(false, paths, window, cx)
718 })
719 .log_err()
720 {
721 task.await.log_err();
722 }
723 })
724 .detach();
725}
726
727pub fn init(app_state: Arc<AppState>, cx: &mut App) {
728 component::init();
729 theme_preview::init(cx);
730 toast_layer::init(cx);
731 history_manager::init(app_state.fs.clone(), cx);
732
733 cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx))
734 .on_action(|_: &Reload, cx| reload(cx))
735 .on_action({
736 let app_state = Arc::downgrade(&app_state);
737 move |_: &Open, cx: &mut App| {
738 if let Some(app_state) = app_state.upgrade() {
739 prompt_and_open_paths(
740 app_state,
741 PathPromptOptions {
742 files: true,
743 directories: true,
744 multiple: true,
745 prompt: None,
746 },
747 cx,
748 );
749 }
750 }
751 })
752 .on_action({
753 let app_state = Arc::downgrade(&app_state);
754 move |_: &OpenFiles, cx: &mut App| {
755 let directories = cx.can_select_mixed_files_and_dirs();
756 if let Some(app_state) = app_state.upgrade() {
757 prompt_and_open_paths(
758 app_state,
759 PathPromptOptions {
760 files: true,
761 directories,
762 multiple: true,
763 prompt: None,
764 },
765 cx,
766 );
767 }
768 }
769 });
770}
771
772type BuildProjectItemFn =
773 fn(AnyEntity, Entity<Project>, Option<&Pane>, &mut Window, &mut App) -> Box<dyn ItemHandle>;
774
775type BuildProjectItemForPathFn =
776 fn(
777 &Entity<Project>,
778 &ProjectPath,
779 &mut Window,
780 &mut App,
781 ) -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
782
783#[derive(Clone, Default)]
784struct ProjectItemRegistry {
785 build_project_item_fns_by_type: HashMap<TypeId, BuildProjectItemFn>,
786 build_project_item_for_path_fns: Vec<BuildProjectItemForPathFn>,
787}
788
789impl ProjectItemRegistry {
790 fn register<T: ProjectItem>(&mut self) {
791 self.build_project_item_fns_by_type.insert(
792 TypeId::of::<T::Item>(),
793 |item, project, pane, window, cx| {
794 let item = item.downcast().unwrap();
795 Box::new(cx.new(|cx| T::for_project_item(project, pane, item, window, cx)))
796 as Box<dyn ItemHandle>
797 },
798 );
799 self.build_project_item_for_path_fns
800 .push(|project, project_path, window, cx| {
801 let project_path = project_path.clone();
802 let is_file = project
803 .read(cx)
804 .entry_for_path(&project_path, cx)
805 .is_some_and(|entry| entry.is_file());
806 let entry_abs_path = project.read(cx).absolute_path(&project_path, cx);
807 let is_local = project.read(cx).is_local();
808 let project_item =
809 <T::Item as project::ProjectItem>::try_open(project, &project_path, cx)?;
810 let project = project.clone();
811 Some(window.spawn(cx, async move |cx| {
812 match project_item.await.with_context(|| {
813 format!(
814 "opening project path {:?}",
815 entry_abs_path.as_deref().unwrap_or(&project_path.path.as_std_path())
816 )
817 }) {
818 Ok(project_item) => {
819 let project_item = project_item;
820 let project_entry_id: Option<ProjectEntryId> =
821 project_item.read_with(cx, project::ProjectItem::entry_id);
822 let build_workspace_item = Box::new(
823 |pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
824 Box::new(cx.new(|cx| {
825 T::for_project_item(
826 project,
827 Some(pane),
828 project_item,
829 window,
830 cx,
831 )
832 })) as Box<dyn ItemHandle>
833 },
834 ) as Box<_>;
835 Ok((project_entry_id, build_workspace_item))
836 }
837 Err(e) => {
838 log::warn!("Failed to open a project item: {e:#}");
839 if e.error_code() == ErrorCode::Internal {
840 if let Some(abs_path) =
841 entry_abs_path.as_deref().filter(|_| is_file)
842 {
843 if let Some(broken_project_item_view) =
844 cx.update(|window, cx| {
845 T::for_broken_project_item(
846 abs_path, is_local, &e, window, cx,
847 )
848 })?
849 {
850 let build_workspace_item = Box::new(
851 move |_: &mut Pane, _: &mut Window, cx: &mut Context<Pane>| {
852 cx.new(|_| broken_project_item_view).boxed_clone()
853 },
854 )
855 as Box<_>;
856 return Ok((None, build_workspace_item));
857 }
858 }
859 }
860 Err(e)
861 }
862 }
863 }))
864 });
865 }
866
867 fn open_path(
868 &self,
869 project: &Entity<Project>,
870 path: &ProjectPath,
871 window: &mut Window,
872 cx: &mut App,
873 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
874 let Some(open_project_item) = self
875 .build_project_item_for_path_fns
876 .iter()
877 .rev()
878 .find_map(|open_project_item| open_project_item(project, path, window, cx))
879 else {
880 return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
881 };
882 open_project_item
883 }
884
885 fn build_item<T: project::ProjectItem>(
886 &self,
887 item: Entity<T>,
888 project: Entity<Project>,
889 pane: Option<&Pane>,
890 window: &mut Window,
891 cx: &mut App,
892 ) -> Option<Box<dyn ItemHandle>> {
893 let build = self
894 .build_project_item_fns_by_type
895 .get(&TypeId::of::<T>())?;
896 Some(build(item.into_any(), project, pane, window, cx))
897 }
898}
899
900type WorkspaceItemBuilder =
901 Box<dyn FnOnce(&mut Pane, &mut Window, &mut Context<Pane>) -> Box<dyn ItemHandle>>;
902
903impl Global for ProjectItemRegistry {}
904
905/// Registers a [ProjectItem] for the app. When opening a file, all the registered
906/// items will get a chance to open the file, starting from the project item that
907/// was added last.
908pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
909 cx.default_global::<ProjectItemRegistry>().register::<I>();
910}
911
912#[derive(Default)]
913pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
914
915struct FollowableViewDescriptor {
916 from_state_proto: fn(
917 Entity<Workspace>,
918 ViewId,
919 &mut Option<proto::view::Variant>,
920 &mut Window,
921 &mut App,
922 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
923 to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
924}
925
926impl Global for FollowableViewRegistry {}
927
928impl FollowableViewRegistry {
929 pub fn register<I: FollowableItem>(cx: &mut App) {
930 cx.default_global::<Self>().0.insert(
931 TypeId::of::<I>(),
932 FollowableViewDescriptor {
933 from_state_proto: |workspace, id, state, window, cx| {
934 I::from_state_proto(workspace, id, state, window, cx).map(|task| {
935 cx.foreground_executor()
936 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
937 })
938 },
939 to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
940 },
941 );
942 }
943
944 pub fn from_state_proto(
945 workspace: Entity<Workspace>,
946 view_id: ViewId,
947 mut state: Option<proto::view::Variant>,
948 window: &mut Window,
949 cx: &mut App,
950 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
951 cx.update_default_global(|this: &mut Self, cx| {
952 this.0.values().find_map(|descriptor| {
953 (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, window, cx)
954 })
955 })
956 }
957
958 pub fn to_followable_view(
959 view: impl Into<AnyView>,
960 cx: &App,
961 ) -> Option<Box<dyn FollowableItemHandle>> {
962 let this = cx.try_global::<Self>()?;
963 let view = view.into();
964 let descriptor = this.0.get(&view.entity_type())?;
965 Some((descriptor.to_followable_view)(&view))
966 }
967}
968
969#[derive(Copy, Clone)]
970struct SerializableItemDescriptor {
971 deserialize: fn(
972 Entity<Project>,
973 WeakEntity<Workspace>,
974 WorkspaceId,
975 ItemId,
976 &mut Window,
977 &mut Context<Pane>,
978 ) -> Task<Result<Box<dyn ItemHandle>>>,
979 cleanup: fn(WorkspaceId, Vec<ItemId>, &mut Window, &mut App) -> Task<Result<()>>,
980 view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
981}
982
983#[derive(Default)]
984struct SerializableItemRegistry {
985 descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
986 descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
987}
988
989impl Global for SerializableItemRegistry {}
990
991impl SerializableItemRegistry {
992 fn deserialize(
993 item_kind: &str,
994 project: Entity<Project>,
995 workspace: WeakEntity<Workspace>,
996 workspace_id: WorkspaceId,
997 item_item: ItemId,
998 window: &mut Window,
999 cx: &mut Context<Pane>,
1000 ) -> Task<Result<Box<dyn ItemHandle>>> {
1001 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
1002 return Task::ready(Err(anyhow!(
1003 "cannot deserialize {}, descriptor not found",
1004 item_kind
1005 )));
1006 };
1007
1008 (descriptor.deserialize)(project, workspace, workspace_id, item_item, window, cx)
1009 }
1010
1011 fn cleanup(
1012 item_kind: &str,
1013 workspace_id: WorkspaceId,
1014 loaded_items: Vec<ItemId>,
1015 window: &mut Window,
1016 cx: &mut App,
1017 ) -> Task<Result<()>> {
1018 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
1019 return Task::ready(Err(anyhow!(
1020 "cannot cleanup {}, descriptor not found",
1021 item_kind
1022 )));
1023 };
1024
1025 (descriptor.cleanup)(workspace_id, loaded_items, window, cx)
1026 }
1027
1028 fn view_to_serializable_item_handle(
1029 view: AnyView,
1030 cx: &App,
1031 ) -> Option<Box<dyn SerializableItemHandle>> {
1032 let this = cx.try_global::<Self>()?;
1033 let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
1034 Some((descriptor.view_to_serializable_item)(view))
1035 }
1036
1037 fn descriptor(item_kind: &str, cx: &App) -> Option<SerializableItemDescriptor> {
1038 let this = cx.try_global::<Self>()?;
1039 this.descriptors_by_kind.get(item_kind).copied()
1040 }
1041}
1042
1043pub fn register_serializable_item<I: SerializableItem>(cx: &mut App) {
1044 let serialized_item_kind = I::serialized_item_kind();
1045
1046 let registry = cx.default_global::<SerializableItemRegistry>();
1047 let descriptor = SerializableItemDescriptor {
1048 deserialize: |project, workspace, workspace_id, item_id, window, cx| {
1049 let task = I::deserialize(project, workspace, workspace_id, item_id, window, cx);
1050 cx.foreground_executor()
1051 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
1052 },
1053 cleanup: |workspace_id, loaded_items, window, cx| {
1054 I::cleanup(workspace_id, loaded_items, window, cx)
1055 },
1056 view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
1057 };
1058 registry
1059 .descriptors_by_kind
1060 .insert(Arc::from(serialized_item_kind), descriptor);
1061 registry
1062 .descriptors_by_type
1063 .insert(TypeId::of::<I>(), descriptor);
1064}
1065
1066pub struct AppState {
1067 pub languages: Arc<LanguageRegistry>,
1068 pub client: Arc<Client>,
1069 pub user_store: Entity<UserStore>,
1070 pub workspace_store: Entity<WorkspaceStore>,
1071 pub fs: Arc<dyn fs::Fs>,
1072 pub build_window_options: fn(Option<Uuid>, &mut App) -> WindowOptions,
1073 pub node_runtime: NodeRuntime,
1074 pub session: Entity<AppSession>,
1075}
1076
1077struct GlobalAppState(Weak<AppState>);
1078
1079impl Global for GlobalAppState {}
1080
1081pub struct WorkspaceStore {
1082 workspaces: HashSet<(gpui::AnyWindowHandle, WeakEntity<Workspace>)>,
1083 client: Arc<Client>,
1084 _subscriptions: Vec<client::Subscription>,
1085}
1086
1087#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
1088pub enum CollaboratorId {
1089 PeerId(PeerId),
1090 Agent,
1091}
1092
1093impl From<PeerId> for CollaboratorId {
1094 fn from(peer_id: PeerId) -> Self {
1095 CollaboratorId::PeerId(peer_id)
1096 }
1097}
1098
1099impl From<&PeerId> for CollaboratorId {
1100 fn from(peer_id: &PeerId) -> Self {
1101 CollaboratorId::PeerId(*peer_id)
1102 }
1103}
1104
1105#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
1106struct Follower {
1107 project_id: Option<u64>,
1108 peer_id: PeerId,
1109}
1110
1111impl AppState {
1112 #[track_caller]
1113 pub fn global(cx: &App) -> Weak<Self> {
1114 cx.global::<GlobalAppState>().0.clone()
1115 }
1116 pub fn try_global(cx: &App) -> Option<Weak<Self>> {
1117 cx.try_global::<GlobalAppState>()
1118 .map(|state| state.0.clone())
1119 }
1120 pub fn set_global(state: Weak<AppState>, cx: &mut App) {
1121 cx.set_global(GlobalAppState(state));
1122 }
1123
1124 #[cfg(any(test, feature = "test-support"))]
1125 pub fn test(cx: &mut App) -> Arc<Self> {
1126 use fs::Fs;
1127 use node_runtime::NodeRuntime;
1128 use session::Session;
1129 use settings::SettingsStore;
1130
1131 if !cx.has_global::<SettingsStore>() {
1132 let settings_store = SettingsStore::test(cx);
1133 cx.set_global(settings_store);
1134 }
1135
1136 let fs = fs::FakeFs::new(cx.background_executor().clone());
1137 <dyn Fs>::set_global(fs.clone(), cx);
1138 let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
1139 let clock = Arc::new(clock::FakeSystemClock::new());
1140 let http_client = http_client::FakeHttpClient::with_404_response();
1141 let client = Client::new(clock, http_client, cx);
1142 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
1143 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1144 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
1145
1146 theme_settings::init(theme::LoadThemes::JustBase, cx);
1147 client::init(&client, cx);
1148
1149 Arc::new(Self {
1150 client,
1151 fs,
1152 languages,
1153 user_store,
1154 workspace_store,
1155 node_runtime: NodeRuntime::unavailable(),
1156 build_window_options: |_, _| Default::default(),
1157 session,
1158 })
1159 }
1160}
1161
1162struct DelayedDebouncedEditAction {
1163 task: Option<Task<()>>,
1164 cancel_channel: Option<oneshot::Sender<()>>,
1165}
1166
1167impl DelayedDebouncedEditAction {
1168 fn new() -> DelayedDebouncedEditAction {
1169 DelayedDebouncedEditAction {
1170 task: None,
1171 cancel_channel: None,
1172 }
1173 }
1174
1175 fn fire_new<F>(
1176 &mut self,
1177 delay: Duration,
1178 window: &mut Window,
1179 cx: &mut Context<Workspace>,
1180 func: F,
1181 ) where
1182 F: 'static
1183 + Send
1184 + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> Task<Result<()>>,
1185 {
1186 if let Some(channel) = self.cancel_channel.take() {
1187 _ = channel.send(());
1188 }
1189
1190 let (sender, mut receiver) = oneshot::channel::<()>();
1191 self.cancel_channel = Some(sender);
1192
1193 let previous_task = self.task.take();
1194 self.task = Some(cx.spawn_in(window, async move |workspace, cx| {
1195 let mut timer = cx.background_executor().timer(delay).fuse();
1196 if let Some(previous_task) = previous_task {
1197 previous_task.await;
1198 }
1199
1200 futures::select_biased! {
1201 _ = receiver => return,
1202 _ = timer => {}
1203 }
1204
1205 if let Some(result) = workspace
1206 .update_in(cx, |workspace, window, cx| (func)(workspace, window, cx))
1207 .log_err()
1208 {
1209 result.await.log_err();
1210 }
1211 }));
1212 }
1213}
1214
1215pub enum Event {
1216 PaneAdded(Entity<Pane>),
1217 PaneRemoved,
1218 ItemAdded {
1219 item: Box<dyn ItemHandle>,
1220 },
1221 ActiveItemChanged,
1222 ItemRemoved {
1223 item_id: EntityId,
1224 },
1225 UserSavedItem {
1226 pane: WeakEntity<Pane>,
1227 item: Box<dyn WeakItemHandle>,
1228 save_intent: SaveIntent,
1229 },
1230 ContactRequestedJoin(u64),
1231 WorkspaceCreated(WeakEntity<Workspace>),
1232 OpenBundledFile {
1233 text: Cow<'static, str>,
1234 title: &'static str,
1235 language: &'static str,
1236 },
1237 ZoomChanged,
1238 ModalOpened,
1239 Activate,
1240 PanelAdded(AnyView),
1241}
1242
1243#[derive(Debug, Clone)]
1244pub enum OpenVisible {
1245 All,
1246 None,
1247 OnlyFiles,
1248 OnlyDirectories,
1249}
1250
1251enum WorkspaceLocation {
1252 // Valid local paths or SSH project to serialize
1253 Location(SerializedWorkspaceLocation, PathList),
1254 // No valid location found hence clear session id
1255 DetachFromSession,
1256 // No valid location found to serialize
1257 None,
1258}
1259
1260type PromptForNewPath = Box<
1261 dyn Fn(
1262 &mut Workspace,
1263 DirectoryLister,
1264 Option<String>,
1265 &mut Window,
1266 &mut Context<Workspace>,
1267 ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
1268>;
1269
1270type PromptForOpenPath = Box<
1271 dyn Fn(
1272 &mut Workspace,
1273 DirectoryLister,
1274 &mut Window,
1275 &mut Context<Workspace>,
1276 ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
1277>;
1278
1279#[derive(Default)]
1280struct DispatchingKeystrokes {
1281 dispatched: HashSet<Vec<Keystroke>>,
1282 queue: VecDeque<Keystroke>,
1283 task: Option<Shared<Task<()>>>,
1284}
1285
1286/// Collects everything project-related for a certain window opened.
1287/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
1288///
1289/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
1290/// The `Workspace` owns everybody's state and serves as a default, "global context",
1291/// that can be used to register a global action to be triggered from any place in the window.
1292pub struct Workspace {
1293 weak_self: WeakEntity<Self>,
1294 workspace_actions: Vec<Box<dyn Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div>>,
1295 zoomed: Option<AnyWeakView>,
1296 previous_dock_drag_coordinates: Option<Point<Pixels>>,
1297 zoomed_position: Option<DockPosition>,
1298 center: PaneGroup,
1299 left_dock: Entity<Dock>,
1300 bottom_dock: Entity<Dock>,
1301 right_dock: Entity<Dock>,
1302 panes: Vec<Entity<Pane>>,
1303 active_worktree_override: Option<WorktreeId>,
1304 panes_by_item: HashMap<EntityId, WeakEntity<Pane>>,
1305 active_pane: Entity<Pane>,
1306 last_active_center_pane: Option<WeakEntity<Pane>>,
1307 last_active_view_id: Option<proto::ViewId>,
1308 status_bar: Entity<StatusBar>,
1309 pub(crate) modal_layer: Entity<ModalLayer>,
1310 toast_layer: Entity<ToastLayer>,
1311 titlebar_item: Option<AnyView>,
1312 notifications: Notifications,
1313 suppressed_notifications: HashSet<NotificationId>,
1314 project: Entity<Project>,
1315 follower_states: HashMap<CollaboratorId, FollowerState>,
1316 last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
1317 window_edited: bool,
1318 last_window_title: Option<String>,
1319 dirty_items: HashMap<EntityId, Subscription>,
1320 active_call: Option<(GlobalAnyActiveCall, Vec<Subscription>)>,
1321 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
1322 database_id: Option<WorkspaceId>,
1323 app_state: Arc<AppState>,
1324 dispatching_keystrokes: Rc<RefCell<DispatchingKeystrokes>>,
1325 _subscriptions: Vec<Subscription>,
1326 _apply_leader_updates: Task<Result<()>>,
1327 _observe_current_user: Task<Result<()>>,
1328 _schedule_serialize_workspace: Option<Task<()>>,
1329 _serialize_workspace_task: Option<Task<()>>,
1330 _schedule_serialize_ssh_paths: Option<Task<()>>,
1331 pane_history_timestamp: Arc<AtomicUsize>,
1332 bounds: Bounds<Pixels>,
1333 pub centered_layout: bool,
1334 bounds_save_task_queued: Option<Task<()>>,
1335 on_prompt_for_new_path: Option<PromptForNewPath>,
1336 on_prompt_for_open_path: Option<PromptForOpenPath>,
1337 terminal_provider: Option<Box<dyn TerminalProvider>>,
1338 debugger_provider: Option<Arc<dyn DebuggerProvider>>,
1339 serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
1340 _items_serializer: Task<Result<()>>,
1341 session_id: Option<String>,
1342 scheduled_tasks: Vec<Task<()>>,
1343 last_open_dock_positions: Vec<DockPosition>,
1344 removing: bool,
1345 _panels_task: Option<Task<Result<()>>>,
1346 sidebar_focus_handle: Option<FocusHandle>,
1347 multi_workspace: Option<WeakEntity<MultiWorkspace>>,
1348}
1349
1350impl EventEmitter<Event> for Workspace {}
1351
1352#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
1353pub struct ViewId {
1354 pub creator: CollaboratorId,
1355 pub id: u64,
1356}
1357
1358pub struct FollowerState {
1359 center_pane: Entity<Pane>,
1360 dock_pane: Option<Entity<Pane>>,
1361 active_view_id: Option<ViewId>,
1362 items_by_leader_view_id: HashMap<ViewId, FollowerView>,
1363}
1364
1365struct FollowerView {
1366 view: Box<dyn FollowableItemHandle>,
1367 location: Option<proto::PanelId>,
1368}
1369
1370impl Workspace {
1371 pub fn new(
1372 workspace_id: Option<WorkspaceId>,
1373 project: Entity<Project>,
1374 app_state: Arc<AppState>,
1375 window: &mut Window,
1376 cx: &mut Context<Self>,
1377 ) -> Self {
1378 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1379 cx.subscribe(&trusted_worktrees, |_, worktrees_store, e, cx| {
1380 if let TrustedWorktreesEvent::Trusted(..) = e {
1381 // Do not persist auto trusted worktrees
1382 if !ProjectSettings::get_global(cx).session.trust_all_worktrees {
1383 worktrees_store.update(cx, |worktrees_store, cx| {
1384 worktrees_store.schedule_serialization(
1385 cx,
1386 |new_trusted_worktrees, cx| {
1387 let timeout =
1388 cx.background_executor().timer(SERIALIZATION_THROTTLE_TIME);
1389 let db = WorkspaceDb::global(cx);
1390 cx.background_spawn(async move {
1391 timeout.await;
1392 db.save_trusted_worktrees(new_trusted_worktrees)
1393 .await
1394 .log_err();
1395 })
1396 },
1397 )
1398 });
1399 }
1400 }
1401 })
1402 .detach();
1403
1404 cx.observe_global::<SettingsStore>(|_, cx| {
1405 if ProjectSettings::get_global(cx).session.trust_all_worktrees {
1406 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1407 trusted_worktrees.update(cx, |trusted_worktrees, cx| {
1408 trusted_worktrees.auto_trust_all(cx);
1409 })
1410 }
1411 }
1412 })
1413 .detach();
1414 }
1415
1416 cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
1417 match event {
1418 project::Event::RemoteIdChanged(_) => {
1419 this.update_window_title(window, cx);
1420 }
1421
1422 project::Event::CollaboratorLeft(peer_id) => {
1423 this.collaborator_left(*peer_id, window, cx);
1424 }
1425
1426 &project::Event::WorktreeRemoved(_) => {
1427 this.update_window_title(window, cx);
1428 this.serialize_workspace(window, cx);
1429 this.update_history(cx);
1430 }
1431
1432 &project::Event::WorktreeAdded(id) => {
1433 this.update_window_title(window, cx);
1434 if this
1435 .project()
1436 .read(cx)
1437 .worktree_for_id(id, cx)
1438 .is_some_and(|wt| wt.read(cx).is_visible())
1439 {
1440 this.serialize_workspace(window, cx);
1441 this.update_history(cx);
1442 }
1443 }
1444 project::Event::WorktreeUpdatedEntries(..) => {
1445 this.update_window_title(window, cx);
1446 this.serialize_workspace(window, cx);
1447 }
1448
1449 project::Event::DisconnectedFromHost => {
1450 this.update_window_edited(window, cx);
1451 let leaders_to_unfollow =
1452 this.follower_states.keys().copied().collect::<Vec<_>>();
1453 for leader_id in leaders_to_unfollow {
1454 this.unfollow(leader_id, window, cx);
1455 }
1456 }
1457
1458 project::Event::DisconnectedFromRemote {
1459 server_not_running: _,
1460 } => {
1461 this.update_window_edited(window, cx);
1462 }
1463
1464 project::Event::Closed => {
1465 window.remove_window();
1466 }
1467
1468 project::Event::DeletedEntry(_, entry_id) => {
1469 for pane in this.panes.iter() {
1470 pane.update(cx, |pane, cx| {
1471 pane.handle_deleted_project_item(*entry_id, window, cx)
1472 });
1473 }
1474 }
1475
1476 project::Event::Toast {
1477 notification_id,
1478 message,
1479 link,
1480 } => this.show_notification(
1481 NotificationId::named(notification_id.clone()),
1482 cx,
1483 |cx| {
1484 let mut notification = MessageNotification::new(message.clone(), cx);
1485 if let Some(link) = link {
1486 notification = notification
1487 .more_info_message(link.label)
1488 .more_info_url(link.url);
1489 }
1490
1491 cx.new(|_| notification)
1492 },
1493 ),
1494
1495 project::Event::HideToast { notification_id } => {
1496 this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
1497 }
1498
1499 project::Event::LanguageServerPrompt(request) => {
1500 struct LanguageServerPrompt;
1501
1502 this.show_notification(
1503 NotificationId::composite::<LanguageServerPrompt>(request.id),
1504 cx,
1505 |cx| {
1506 cx.new(|cx| {
1507 notifications::LanguageServerPrompt::new(request.clone(), cx)
1508 })
1509 },
1510 );
1511 }
1512
1513 project::Event::AgentLocationChanged => {
1514 this.handle_agent_location_changed(window, cx)
1515 }
1516
1517 _ => {}
1518 }
1519 cx.notify()
1520 })
1521 .detach();
1522
1523 cx.subscribe_in(
1524 &project.read(cx).breakpoint_store(),
1525 window,
1526 |workspace, _, event, window, cx| match event {
1527 BreakpointStoreEvent::BreakpointsUpdated(_, _)
1528 | BreakpointStoreEvent::BreakpointsCleared(_) => {
1529 workspace.serialize_workspace(window, cx);
1530 }
1531 BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {}
1532 },
1533 )
1534 .detach();
1535 if let Some(toolchain_store) = project.read(cx).toolchain_store() {
1536 cx.subscribe_in(
1537 &toolchain_store,
1538 window,
1539 |workspace, _, event, window, cx| match event {
1540 ToolchainStoreEvent::CustomToolchainsModified => {
1541 workspace.serialize_workspace(window, cx);
1542 }
1543 _ => {}
1544 },
1545 )
1546 .detach();
1547 }
1548
1549 cx.on_focus_lost(window, |this, window, cx| {
1550 let focus_handle = this.focus_handle(cx);
1551 window.focus(&focus_handle, cx);
1552 })
1553 .detach();
1554
1555 let weak_handle = cx.entity().downgrade();
1556 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
1557
1558 let center_pane = cx.new(|cx| {
1559 let mut center_pane = Pane::new(
1560 weak_handle.clone(),
1561 project.clone(),
1562 pane_history_timestamp.clone(),
1563 None,
1564 NewFile.boxed_clone(),
1565 true,
1566 window,
1567 cx,
1568 );
1569 center_pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
1570 center_pane.set_should_display_welcome_page(true);
1571 center_pane
1572 });
1573 cx.subscribe_in(¢er_pane, window, Self::handle_pane_event)
1574 .detach();
1575
1576 window.focus(¢er_pane.focus_handle(cx), cx);
1577
1578 cx.emit(Event::PaneAdded(center_pane.clone()));
1579
1580 let any_window_handle = window.window_handle();
1581 app_state.workspace_store.update(cx, |store, _| {
1582 store
1583 .workspaces
1584 .insert((any_window_handle, weak_handle.clone()));
1585 });
1586
1587 let mut current_user = app_state.user_store.read(cx).watch_current_user();
1588 let mut connection_status = app_state.client.status();
1589 let _observe_current_user = cx.spawn_in(window, async move |this, cx| {
1590 current_user.next().await;
1591 connection_status.next().await;
1592 let mut stream =
1593 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
1594
1595 while stream.recv().await.is_some() {
1596 this.update(cx, |_, cx| cx.notify())?;
1597 }
1598 anyhow::Ok(())
1599 });
1600
1601 // All leader updates are enqueued and then processed in a single task, so
1602 // that each asynchronous operation can be run in order.
1603 let (leader_updates_tx, mut leader_updates_rx) =
1604 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
1605 let _apply_leader_updates = cx.spawn_in(window, async move |this, cx| {
1606 while let Some((leader_id, update)) = leader_updates_rx.next().await {
1607 Self::process_leader_update(&this, leader_id, update, cx)
1608 .await
1609 .log_err();
1610 }
1611
1612 Ok(())
1613 });
1614
1615 cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
1616 let modal_layer = cx.new(|_| ModalLayer::new());
1617 let toast_layer = cx.new(|_| ToastLayer::new());
1618 cx.subscribe(
1619 &modal_layer,
1620 |_, _, _: &modal_layer::ModalOpenedEvent, cx| {
1621 cx.emit(Event::ModalOpened);
1622 },
1623 )
1624 .detach();
1625
1626 let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx);
1627 let bottom_dock = Dock::new(DockPosition::Bottom, modal_layer.clone(), window, cx);
1628 let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), window, cx);
1629 let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx));
1630 let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx));
1631 let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx));
1632 let multi_workspace = window
1633 .root::<MultiWorkspace>()
1634 .flatten()
1635 .map(|mw| mw.downgrade());
1636 let status_bar = cx.new(|cx| {
1637 let mut status_bar =
1638 StatusBar::new(¢er_pane.clone(), multi_workspace.clone(), window, cx);
1639 status_bar.add_left_item(left_dock_buttons, window, cx);
1640 status_bar.add_right_item(right_dock_buttons, window, cx);
1641 status_bar.add_right_item(bottom_dock_buttons, window, cx);
1642 status_bar
1643 });
1644
1645 let session_id = app_state.session.read(cx).id().to_owned();
1646
1647 let mut active_call = None;
1648 if let Some(call) = GlobalAnyActiveCall::try_global(cx).cloned() {
1649 let subscriptions =
1650 vec![
1651 call.0
1652 .subscribe(window, cx, Box::new(Self::on_active_call_event)),
1653 ];
1654 active_call = Some((call, subscriptions));
1655 }
1656
1657 let (serializable_items_tx, serializable_items_rx) =
1658 mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
1659 let _items_serializer = cx.spawn_in(window, async move |this, cx| {
1660 Self::serialize_items(&this, serializable_items_rx, cx).await
1661 });
1662
1663 let subscriptions = vec![
1664 cx.observe_window_activation(window, Self::on_window_activation_changed),
1665 cx.observe_window_bounds(window, move |this, window, cx| {
1666 if this.bounds_save_task_queued.is_some() {
1667 return;
1668 }
1669 this.bounds_save_task_queued = Some(cx.spawn_in(window, async move |this, cx| {
1670 cx.background_executor()
1671 .timer(Duration::from_millis(100))
1672 .await;
1673 this.update_in(cx, |this, window, cx| {
1674 this.save_window_bounds(window, cx).detach();
1675 this.bounds_save_task_queued.take();
1676 })
1677 .ok();
1678 }));
1679 cx.notify();
1680 }),
1681 cx.observe_window_appearance(window, |_, window, cx| {
1682 let window_appearance = window.appearance();
1683
1684 *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
1685
1686 theme_settings::reload_theme(cx);
1687 theme_settings::reload_icon_theme(cx);
1688 }),
1689 cx.on_release({
1690 let weak_handle = weak_handle.clone();
1691 move |this, cx| {
1692 this.app_state.workspace_store.update(cx, move |store, _| {
1693 store.workspaces.retain(|(_, weak)| weak != &weak_handle);
1694 })
1695 }
1696 }),
1697 ];
1698
1699 cx.defer_in(window, move |this, window, cx| {
1700 this.update_window_title(window, cx);
1701 this.show_initial_notifications(cx);
1702 });
1703
1704 let mut center = PaneGroup::new(center_pane.clone());
1705 center.set_is_center(true);
1706 center.mark_positions(cx);
1707
1708 Workspace {
1709 weak_self: weak_handle.clone(),
1710 zoomed: None,
1711 zoomed_position: None,
1712 previous_dock_drag_coordinates: None,
1713 center,
1714 panes: vec![center_pane.clone()],
1715 panes_by_item: Default::default(),
1716 active_pane: center_pane.clone(),
1717 last_active_center_pane: Some(center_pane.downgrade()),
1718 last_active_view_id: None,
1719 status_bar,
1720 modal_layer,
1721 toast_layer,
1722 titlebar_item: None,
1723 active_worktree_override: None,
1724 notifications: Notifications::default(),
1725 suppressed_notifications: HashSet::default(),
1726 left_dock,
1727 bottom_dock,
1728 right_dock,
1729 _panels_task: None,
1730 project: project.clone(),
1731 follower_states: Default::default(),
1732 last_leaders_by_pane: Default::default(),
1733 dispatching_keystrokes: Default::default(),
1734 window_edited: false,
1735 last_window_title: None,
1736 dirty_items: Default::default(),
1737 active_call,
1738 database_id: workspace_id,
1739 app_state,
1740 _observe_current_user,
1741 _apply_leader_updates,
1742 _schedule_serialize_workspace: None,
1743 _serialize_workspace_task: None,
1744 _schedule_serialize_ssh_paths: None,
1745 leader_updates_tx,
1746 _subscriptions: subscriptions,
1747 pane_history_timestamp,
1748 workspace_actions: Default::default(),
1749 // This data will be incorrect, but it will be overwritten by the time it needs to be used.
1750 bounds: Default::default(),
1751 centered_layout: false,
1752 bounds_save_task_queued: None,
1753 on_prompt_for_new_path: None,
1754 on_prompt_for_open_path: None,
1755 terminal_provider: None,
1756 debugger_provider: None,
1757 serializable_items_tx,
1758 _items_serializer,
1759 session_id: Some(session_id),
1760
1761 scheduled_tasks: Vec::new(),
1762 last_open_dock_positions: Vec::new(),
1763 removing: false,
1764 sidebar_focus_handle: None,
1765 multi_workspace,
1766 }
1767 }
1768
1769 pub fn new_local(
1770 abs_paths: Vec<PathBuf>,
1771 app_state: Arc<AppState>,
1772 requesting_window: Option<WindowHandle<MultiWorkspace>>,
1773 env: Option<HashMap<String, String>>,
1774 init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
1775 activate: bool,
1776 cx: &mut App,
1777 ) -> Task<anyhow::Result<OpenResult>> {
1778 let project_handle = Project::local(
1779 app_state.client.clone(),
1780 app_state.node_runtime.clone(),
1781 app_state.user_store.clone(),
1782 app_state.languages.clone(),
1783 app_state.fs.clone(),
1784 env,
1785 Default::default(),
1786 cx,
1787 );
1788
1789 let db = WorkspaceDb::global(cx);
1790 let kvp = db::kvp::KeyValueStore::global(cx);
1791 cx.spawn(async move |cx| {
1792 let mut paths_to_open = Vec::with_capacity(abs_paths.len());
1793 for path in abs_paths.into_iter() {
1794 if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() {
1795 paths_to_open.push(canonical)
1796 } else {
1797 paths_to_open.push(path)
1798 }
1799 }
1800
1801 let serialized_workspace = db.workspace_for_roots(paths_to_open.as_slice());
1802
1803 if let Some(paths) = serialized_workspace.as_ref().map(|ws| &ws.paths) {
1804 paths_to_open = paths.ordered_paths().cloned().collect();
1805 if !paths.is_lexicographically_ordered() {
1806 project_handle.update(cx, |project, cx| {
1807 project.set_worktrees_reordered(true, cx);
1808 });
1809 }
1810 }
1811
1812 // Get project paths for all of the abs_paths
1813 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
1814 Vec::with_capacity(paths_to_open.len());
1815
1816 for path in paths_to_open.into_iter() {
1817 if let Some((_, project_entry)) = cx
1818 .update(|cx| {
1819 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
1820 })
1821 .await
1822 .log_err()
1823 {
1824 project_paths.push((path, Some(project_entry)));
1825 } else {
1826 project_paths.push((path, None));
1827 }
1828 }
1829
1830 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
1831 serialized_workspace.id
1832 } else {
1833 db.next_id().await.unwrap_or_else(|_| Default::default())
1834 };
1835
1836 let toolchains = db.toolchains(workspace_id).await?;
1837
1838 for (toolchain, worktree_path, path) in toolchains {
1839 let toolchain_path = PathBuf::from(toolchain.path.clone().to_string());
1840 let Some(worktree_id) = project_handle.read_with(cx, |this, cx| {
1841 this.find_worktree(&worktree_path, cx)
1842 .and_then(|(worktree, rel_path)| {
1843 if rel_path.is_empty() {
1844 Some(worktree.read(cx).id())
1845 } else {
1846 None
1847 }
1848 })
1849 }) else {
1850 // We did not find a worktree with a given path, but that's whatever.
1851 continue;
1852 };
1853 if !app_state.fs.is_file(toolchain_path.as_path()).await {
1854 continue;
1855 }
1856
1857 project_handle
1858 .update(cx, |this, cx| {
1859 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
1860 })
1861 .await;
1862 }
1863 if let Some(workspace) = serialized_workspace.as_ref() {
1864 project_handle.update(cx, |this, cx| {
1865 for (scope, toolchains) in &workspace.user_toolchains {
1866 for toolchain in toolchains {
1867 this.add_toolchain(toolchain.clone(), scope.clone(), cx);
1868 }
1869 }
1870 });
1871 }
1872
1873 let (window, workspace): (WindowHandle<MultiWorkspace>, Entity<Workspace>) =
1874 if let Some(window) = requesting_window {
1875 let centered_layout = serialized_workspace
1876 .as_ref()
1877 .map(|w| w.centered_layout)
1878 .unwrap_or(false);
1879
1880 let workspace = window.update(cx, |multi_workspace, window, cx| {
1881 let workspace = cx.new(|cx| {
1882 let mut workspace = Workspace::new(
1883 Some(workspace_id),
1884 project_handle.clone(),
1885 app_state.clone(),
1886 window,
1887 cx,
1888 );
1889
1890 workspace.centered_layout = centered_layout;
1891
1892 // Call init callback to add items before window renders
1893 if let Some(init) = init {
1894 init(&mut workspace, window, cx);
1895 }
1896
1897 workspace
1898 });
1899 if activate {
1900 multi_workspace.activate(workspace.clone(), cx);
1901 } else {
1902 multi_workspace.add_workspace(workspace.clone(), cx);
1903 }
1904 workspace
1905 })?;
1906 (window, workspace)
1907 } else {
1908 let window_bounds_override = window_bounds_env_override();
1909
1910 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
1911 (Some(WindowBounds::Windowed(bounds)), None)
1912 } else if let Some(workspace) = serialized_workspace.as_ref()
1913 && let Some(display) = workspace.display
1914 && let Some(bounds) = workspace.window_bounds.as_ref()
1915 {
1916 // Reopening an existing workspace - restore its saved bounds
1917 (Some(bounds.0), Some(display))
1918 } else if let Some((display, bounds)) =
1919 persistence::read_default_window_bounds(&kvp)
1920 {
1921 // New or empty workspace - use the last known window bounds
1922 (Some(bounds), Some(display))
1923 } else {
1924 // New window - let GPUI's default_bounds() handle cascading
1925 (None, None)
1926 };
1927
1928 // Use the serialized workspace to construct the new window
1929 let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx));
1930 options.window_bounds = window_bounds;
1931 let centered_layout = serialized_workspace
1932 .as_ref()
1933 .map(|w| w.centered_layout)
1934 .unwrap_or(false);
1935 let window = cx.open_window(options, {
1936 let app_state = app_state.clone();
1937 let project_handle = project_handle.clone();
1938 move |window, cx| {
1939 let workspace = cx.new(|cx| {
1940 let mut workspace = Workspace::new(
1941 Some(workspace_id),
1942 project_handle,
1943 app_state,
1944 window,
1945 cx,
1946 );
1947 workspace.centered_layout = centered_layout;
1948
1949 // Call init callback to add items before window renders
1950 if let Some(init) = init {
1951 init(&mut workspace, window, cx);
1952 }
1953
1954 workspace
1955 });
1956 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
1957 }
1958 })?;
1959 let workspace =
1960 window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
1961 multi_workspace.workspace().clone()
1962 })?;
1963 (window, workspace)
1964 };
1965
1966 notify_if_database_failed(window, cx);
1967 // Check if this is an empty workspace (no paths to open)
1968 // An empty workspace is one where project_paths is empty
1969 let is_empty_workspace = project_paths.is_empty();
1970 // Check if serialized workspace has paths before it's moved
1971 let serialized_workspace_has_paths = serialized_workspace
1972 .as_ref()
1973 .map(|ws| !ws.paths.is_empty())
1974 .unwrap_or(false);
1975
1976 let opened_items = window
1977 .update(cx, |_, window, cx| {
1978 workspace.update(cx, |_workspace: &mut Workspace, cx| {
1979 open_items(serialized_workspace, project_paths, window, cx)
1980 })
1981 })?
1982 .await
1983 .unwrap_or_default();
1984
1985 // Restore default dock state for empty workspaces
1986 // Only restore if:
1987 // 1. This is an empty workspace (no paths), AND
1988 // 2. The serialized workspace either doesn't exist or has no paths
1989 if is_empty_workspace && !serialized_workspace_has_paths {
1990 if let Some(default_docks) = persistence::read_default_dock_state(&kvp) {
1991 window
1992 .update(cx, |_, window, cx| {
1993 workspace.update(cx, |workspace, cx| {
1994 for (dock, serialized_dock) in [
1995 (&workspace.right_dock, &default_docks.right),
1996 (&workspace.left_dock, &default_docks.left),
1997 (&workspace.bottom_dock, &default_docks.bottom),
1998 ] {
1999 dock.update(cx, |dock, cx| {
2000 dock.serialized_dock = Some(serialized_dock.clone());
2001 dock.restore_state(window, cx);
2002 });
2003 }
2004 cx.notify();
2005 });
2006 })
2007 .log_err();
2008 }
2009 }
2010
2011 window
2012 .update(cx, |_, _window, cx| {
2013 workspace.update(cx, |this: &mut Workspace, cx| {
2014 this.update_history(cx);
2015 });
2016 })
2017 .log_err();
2018 Ok(OpenResult {
2019 window,
2020 workspace,
2021 opened_items,
2022 })
2023 })
2024 }
2025
2026 pub fn weak_handle(&self) -> WeakEntity<Self> {
2027 self.weak_self.clone()
2028 }
2029
2030 pub fn left_dock(&self) -> &Entity<Dock> {
2031 &self.left_dock
2032 }
2033
2034 pub fn bottom_dock(&self) -> &Entity<Dock> {
2035 &self.bottom_dock
2036 }
2037
2038 pub fn set_bottom_dock_layout(
2039 &mut self,
2040 layout: BottomDockLayout,
2041 window: &mut Window,
2042 cx: &mut Context<Self>,
2043 ) {
2044 let fs = self.project().read(cx).fs();
2045 settings::update_settings_file(fs.clone(), cx, move |content, _cx| {
2046 content.workspace.bottom_dock_layout = Some(layout);
2047 });
2048
2049 cx.notify();
2050 self.serialize_workspace(window, cx);
2051 }
2052
2053 pub fn right_dock(&self) -> &Entity<Dock> {
2054 &self.right_dock
2055 }
2056
2057 pub fn all_docks(&self) -> [&Entity<Dock>; 3] {
2058 [&self.left_dock, &self.bottom_dock, &self.right_dock]
2059 }
2060
2061 pub fn capture_dock_state(&self, _window: &Window, cx: &App) -> DockStructure {
2062 let left_dock = self.left_dock.read(cx);
2063 let left_visible = left_dock.is_open();
2064 let left_active_panel = left_dock
2065 .active_panel()
2066 .map(|panel| panel.persistent_name().to_string());
2067 // `zoomed_position` is kept in sync with individual panel zoom state
2068 // by the dock code in `Dock::new` and `Dock::add_panel`.
2069 let left_dock_zoom = self.zoomed_position == Some(DockPosition::Left);
2070
2071 let right_dock = self.right_dock.read(cx);
2072 let right_visible = right_dock.is_open();
2073 let right_active_panel = right_dock
2074 .active_panel()
2075 .map(|panel| panel.persistent_name().to_string());
2076 let right_dock_zoom = self.zoomed_position == Some(DockPosition::Right);
2077
2078 let bottom_dock = self.bottom_dock.read(cx);
2079 let bottom_visible = bottom_dock.is_open();
2080 let bottom_active_panel = bottom_dock
2081 .active_panel()
2082 .map(|panel| panel.persistent_name().to_string());
2083 let bottom_dock_zoom = self.zoomed_position == Some(DockPosition::Bottom);
2084
2085 DockStructure {
2086 left: DockData {
2087 visible: left_visible,
2088 active_panel: left_active_panel,
2089 zoom: left_dock_zoom,
2090 },
2091 right: DockData {
2092 visible: right_visible,
2093 active_panel: right_active_panel,
2094 zoom: right_dock_zoom,
2095 },
2096 bottom: DockData {
2097 visible: bottom_visible,
2098 active_panel: bottom_active_panel,
2099 zoom: bottom_dock_zoom,
2100 },
2101 }
2102 }
2103
2104 pub fn set_dock_structure(
2105 &self,
2106 docks: DockStructure,
2107 window: &mut Window,
2108 cx: &mut Context<Self>,
2109 ) {
2110 for (dock, data) in [
2111 (&self.left_dock, docks.left),
2112 (&self.bottom_dock, docks.bottom),
2113 (&self.right_dock, docks.right),
2114 ] {
2115 dock.update(cx, |dock, cx| {
2116 dock.serialized_dock = Some(data);
2117 dock.restore_state(window, cx);
2118 });
2119 }
2120 }
2121
2122 pub fn open_item_abs_paths(&self, cx: &App) -> Vec<PathBuf> {
2123 self.items(cx)
2124 .filter_map(|item| {
2125 let project_path = item.project_path(cx)?;
2126 self.project.read(cx).absolute_path(&project_path, cx)
2127 })
2128 .collect()
2129 }
2130
2131 pub fn dock_at_position(&self, position: DockPosition) -> &Entity<Dock> {
2132 match position {
2133 DockPosition::Left => &self.left_dock,
2134 DockPosition::Bottom => &self.bottom_dock,
2135 DockPosition::Right => &self.right_dock,
2136 }
2137 }
2138
2139 pub fn agent_panel_position(&self, cx: &App) -> Option<DockPosition> {
2140 self.all_docks().into_iter().find_map(|dock| {
2141 let dock = dock.read(cx);
2142 dock.has_agent_panel(cx).then_some(dock.position())
2143 })
2144 }
2145
2146 pub fn panel_size_state<T: Panel>(&self, cx: &App) -> Option<dock::PanelSizeState> {
2147 self.all_docks().into_iter().find_map(|dock| {
2148 let dock = dock.read(cx);
2149 let panel = dock.panel::<T>()?;
2150 dock.stored_panel_size_state(&panel)
2151 })
2152 }
2153
2154 pub fn persisted_panel_size_state(
2155 &self,
2156 panel_key: &'static str,
2157 cx: &App,
2158 ) -> Option<dock::PanelSizeState> {
2159 dock::Dock::load_persisted_size_state(self, panel_key, cx)
2160 }
2161
2162 pub fn persist_panel_size_state(
2163 &self,
2164 panel_key: &str,
2165 size_state: dock::PanelSizeState,
2166 cx: &mut App,
2167 ) {
2168 let Some(workspace_id) = self
2169 .database_id()
2170 .map(|id| i64::from(id).to_string())
2171 .or(self.session_id())
2172 else {
2173 return;
2174 };
2175
2176 let kvp = db::kvp::KeyValueStore::global(cx);
2177 let panel_key = panel_key.to_string();
2178 cx.background_spawn(async move {
2179 let scope = kvp.scoped(dock::PANEL_SIZE_STATE_KEY);
2180 scope
2181 .write(
2182 format!("{workspace_id}:{panel_key}"),
2183 serde_json::to_string(&size_state)?,
2184 )
2185 .await
2186 })
2187 .detach_and_log_err(cx);
2188 }
2189
2190 pub fn set_panel_size_state<T: Panel>(
2191 &mut self,
2192 size_state: dock::PanelSizeState,
2193 window: &mut Window,
2194 cx: &mut Context<Self>,
2195 ) -> bool {
2196 let Some(panel) = self.panel::<T>(cx) else {
2197 return false;
2198 };
2199
2200 let dock = self.dock_at_position(panel.position(window, cx));
2201 let did_set = dock.update(cx, |dock, cx| {
2202 dock.set_panel_size_state(&panel, size_state, cx)
2203 });
2204
2205 if did_set {
2206 self.persist_panel_size_state(T::panel_key(), size_state, cx);
2207 }
2208
2209 did_set
2210 }
2211
2212 fn dock_size(&self, dock: &Dock, window: &Window, cx: &App) -> Option<Pixels> {
2213 let panel = dock.active_panel()?;
2214 let size_state = dock
2215 .stored_panel_size_state(panel.as_ref())
2216 .unwrap_or_default();
2217 let position = dock.position();
2218
2219 if position.axis() == Axis::Horizontal
2220 && panel.supports_flexible_size(window, cx)
2221 && let Some(ratio) = size_state
2222 .flexible_size_ratio
2223 .or_else(|| self.default_flexible_dock_ratio(position))
2224 && let Some(available_width) =
2225 self.available_width_for_horizontal_dock(position, window, cx)
2226 {
2227 return Some((available_width * ratio.clamp(0.0, 1.0)).max(RESIZE_HANDLE_SIZE));
2228 }
2229
2230 Some(
2231 size_state
2232 .size
2233 .unwrap_or_else(|| panel.default_size(window, cx)),
2234 )
2235 }
2236
2237 pub fn flexible_dock_ratio_for_size(
2238 &self,
2239 position: DockPosition,
2240 size: Pixels,
2241 window: &Window,
2242 cx: &App,
2243 ) -> Option<f32> {
2244 if position.axis() != Axis::Horizontal {
2245 return None;
2246 }
2247
2248 let available_width = self.available_width_for_horizontal_dock(position, window, cx)?;
2249 let available_width = available_width.max(RESIZE_HANDLE_SIZE);
2250 Some((size / available_width).clamp(0.0, 1.0))
2251 }
2252
2253 fn available_width_for_horizontal_dock(
2254 &self,
2255 position: DockPosition,
2256 window: &Window,
2257 cx: &App,
2258 ) -> Option<Pixels> {
2259 let workspace_width = self.bounds.size.width;
2260 if workspace_width <= Pixels::ZERO {
2261 return None;
2262 }
2263
2264 let opposite_position = match position {
2265 DockPosition::Left => DockPosition::Right,
2266 DockPosition::Right => DockPosition::Left,
2267 DockPosition::Bottom => return None,
2268 };
2269
2270 let opposite_width = self
2271 .dock_at_position(opposite_position)
2272 .read(cx)
2273 .stored_active_panel_size(window, cx)
2274 .unwrap_or(Pixels::ZERO);
2275
2276 Some((workspace_width - opposite_width).max(RESIZE_HANDLE_SIZE))
2277 }
2278
2279 pub fn default_flexible_dock_ratio(&self, position: DockPosition) -> Option<f32> {
2280 if position.axis() != Axis::Horizontal {
2281 return None;
2282 }
2283
2284 let pane = self.last_active_center_pane.clone()?.upgrade()?;
2285 let pane_fraction = self.center.width_fraction_for_pane(&pane).unwrap_or(1.0);
2286 Some((pane_fraction / (1.0 + pane_fraction)).clamp(0.0, 1.0))
2287 }
2288
2289 pub fn is_edited(&self) -> bool {
2290 self.window_edited
2291 }
2292
2293 pub fn add_panel<T: Panel>(
2294 &mut self,
2295 panel: Entity<T>,
2296 window: &mut Window,
2297 cx: &mut Context<Self>,
2298 ) {
2299 let focus_handle = panel.panel_focus_handle(cx);
2300 cx.on_focus_in(&focus_handle, window, Self::handle_panel_focused)
2301 .detach();
2302
2303 let dock_position = panel.position(window, cx);
2304 let dock = self.dock_at_position(dock_position);
2305 let any_panel = panel.to_any();
2306 let persisted_size_state =
2307 self.persisted_panel_size_state(T::panel_key(), cx)
2308 .or_else(|| {
2309 load_legacy_panel_size(T::panel_key(), dock_position, self, cx).map(|size| {
2310 let state = dock::PanelSizeState {
2311 size: Some(size),
2312 flexible_size_ratio: None,
2313 };
2314 self.persist_panel_size_state(T::panel_key(), state, cx);
2315 state
2316 })
2317 });
2318
2319 dock.update(cx, |dock, cx| {
2320 let index = dock.add_panel(panel.clone(), self.weak_self.clone(), window, cx);
2321 if let Some(size_state) = persisted_size_state {
2322 dock.set_panel_size_state(&panel, size_state, cx);
2323 }
2324 index
2325 });
2326
2327 cx.emit(Event::PanelAdded(any_panel));
2328 }
2329
2330 pub fn remove_panel<T: Panel>(
2331 &mut self,
2332 panel: &Entity<T>,
2333 window: &mut Window,
2334 cx: &mut Context<Self>,
2335 ) {
2336 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2337 dock.update(cx, |dock, cx| dock.remove_panel(panel, window, cx));
2338 }
2339 }
2340
2341 pub fn status_bar(&self) -> &Entity<StatusBar> {
2342 &self.status_bar
2343 }
2344
2345 pub fn set_sidebar_focus_handle(&mut self, handle: Option<FocusHandle>) {
2346 self.sidebar_focus_handle = handle;
2347 }
2348
2349 pub fn status_bar_visible(&self, cx: &App) -> bool {
2350 StatusBarSettings::get_global(cx).show
2351 }
2352
2353 pub fn multi_workspace(&self) -> Option<&WeakEntity<MultiWorkspace>> {
2354 self.multi_workspace.as_ref()
2355 }
2356
2357 pub fn set_multi_workspace(
2358 &mut self,
2359 multi_workspace: WeakEntity<MultiWorkspace>,
2360 cx: &mut App,
2361 ) {
2362 self.status_bar.update(cx, |status_bar, cx| {
2363 status_bar.set_multi_workspace(multi_workspace.clone(), cx);
2364 });
2365 self.multi_workspace = Some(multi_workspace);
2366 }
2367
2368 pub fn app_state(&self) -> &Arc<AppState> {
2369 &self.app_state
2370 }
2371
2372 pub fn set_panels_task(&mut self, task: Task<Result<()>>) {
2373 self._panels_task = Some(task);
2374 }
2375
2376 pub fn take_panels_task(&mut self) -> Option<Task<Result<()>>> {
2377 self._panels_task.take()
2378 }
2379
2380 pub fn user_store(&self) -> &Entity<UserStore> {
2381 &self.app_state.user_store
2382 }
2383
2384 pub fn project(&self) -> &Entity<Project> {
2385 &self.project
2386 }
2387
2388 pub fn path_style(&self, cx: &App) -> PathStyle {
2389 self.project.read(cx).path_style(cx)
2390 }
2391
2392 pub fn recently_activated_items(&self, cx: &App) -> HashMap<EntityId, usize> {
2393 let mut history: HashMap<EntityId, usize> = HashMap::default();
2394
2395 for pane_handle in &self.panes {
2396 let pane = pane_handle.read(cx);
2397
2398 for entry in pane.activation_history() {
2399 history.insert(
2400 entry.entity_id,
2401 history
2402 .get(&entry.entity_id)
2403 .cloned()
2404 .unwrap_or(0)
2405 .max(entry.timestamp),
2406 );
2407 }
2408 }
2409
2410 history
2411 }
2412
2413 pub fn recent_active_item_by_type<T: 'static>(&self, cx: &App) -> Option<Entity<T>> {
2414 let mut recent_item: Option<Entity<T>> = None;
2415 let mut recent_timestamp = 0;
2416 for pane_handle in &self.panes {
2417 let pane = pane_handle.read(cx);
2418 let item_map: HashMap<EntityId, &Box<dyn ItemHandle>> =
2419 pane.items().map(|item| (item.item_id(), item)).collect();
2420 for entry in pane.activation_history() {
2421 if entry.timestamp > recent_timestamp
2422 && let Some(&item) = item_map.get(&entry.entity_id)
2423 && let Some(typed_item) = item.act_as::<T>(cx)
2424 {
2425 recent_timestamp = entry.timestamp;
2426 recent_item = Some(typed_item);
2427 }
2428 }
2429 }
2430 recent_item
2431 }
2432
2433 pub fn recent_navigation_history_iter(
2434 &self,
2435 cx: &App,
2436 ) -> impl Iterator<Item = (ProjectPath, Option<PathBuf>)> + use<> {
2437 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
2438 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
2439
2440 for pane in &self.panes {
2441 let pane = pane.read(cx);
2442
2443 pane.nav_history()
2444 .for_each_entry(cx, &mut |entry, (project_path, fs_path)| {
2445 if let Some(fs_path) = &fs_path {
2446 abs_paths_opened
2447 .entry(fs_path.clone())
2448 .or_default()
2449 .insert(project_path.clone());
2450 }
2451 let timestamp = entry.timestamp;
2452 match history.entry(project_path) {
2453 hash_map::Entry::Occupied(mut entry) => {
2454 let (_, old_timestamp) = entry.get();
2455 if ×tamp > old_timestamp {
2456 entry.insert((fs_path, timestamp));
2457 }
2458 }
2459 hash_map::Entry::Vacant(entry) => {
2460 entry.insert((fs_path, timestamp));
2461 }
2462 }
2463 });
2464
2465 if let Some(item) = pane.active_item()
2466 && let Some(project_path) = item.project_path(cx)
2467 {
2468 let fs_path = self.project.read(cx).absolute_path(&project_path, cx);
2469
2470 if let Some(fs_path) = &fs_path {
2471 abs_paths_opened
2472 .entry(fs_path.clone())
2473 .or_default()
2474 .insert(project_path.clone());
2475 }
2476
2477 history.insert(project_path, (fs_path, std::usize::MAX));
2478 }
2479 }
2480
2481 history
2482 .into_iter()
2483 .sorted_by_key(|(_, (_, order))| *order)
2484 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
2485 .rev()
2486 .filter(move |(history_path, abs_path)| {
2487 let latest_project_path_opened = abs_path
2488 .as_ref()
2489 .and_then(|abs_path| abs_paths_opened.get(abs_path))
2490 .and_then(|project_paths| {
2491 project_paths
2492 .iter()
2493 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
2494 });
2495
2496 latest_project_path_opened.is_none_or(|path| path == history_path)
2497 })
2498 }
2499
2500 pub fn recent_navigation_history(
2501 &self,
2502 limit: Option<usize>,
2503 cx: &App,
2504 ) -> Vec<(ProjectPath, Option<PathBuf>)> {
2505 self.recent_navigation_history_iter(cx)
2506 .take(limit.unwrap_or(usize::MAX))
2507 .collect()
2508 }
2509
2510 pub fn clear_navigation_history(&mut self, _window: &mut Window, cx: &mut Context<Workspace>) {
2511 for pane in &self.panes {
2512 pane.update(cx, |pane, cx| pane.nav_history_mut().clear(cx));
2513 }
2514 }
2515
2516 fn navigate_history(
2517 &mut self,
2518 pane: WeakEntity<Pane>,
2519 mode: NavigationMode,
2520 window: &mut Window,
2521 cx: &mut Context<Workspace>,
2522 ) -> Task<Result<()>> {
2523 self.navigate_history_impl(
2524 pane,
2525 mode,
2526 window,
2527 &mut |history, cx| history.pop(mode, cx),
2528 cx,
2529 )
2530 }
2531
2532 fn navigate_tag_history(
2533 &mut self,
2534 pane: WeakEntity<Pane>,
2535 mode: TagNavigationMode,
2536 window: &mut Window,
2537 cx: &mut Context<Workspace>,
2538 ) -> Task<Result<()>> {
2539 self.navigate_history_impl(
2540 pane,
2541 NavigationMode::Normal,
2542 window,
2543 &mut |history, _cx| history.pop_tag(mode),
2544 cx,
2545 )
2546 }
2547
2548 fn navigate_history_impl(
2549 &mut self,
2550 pane: WeakEntity<Pane>,
2551 mode: NavigationMode,
2552 window: &mut Window,
2553 cb: &mut dyn FnMut(&mut NavHistory, &mut App) -> Option<NavigationEntry>,
2554 cx: &mut Context<Workspace>,
2555 ) -> Task<Result<()>> {
2556 let to_load = if let Some(pane) = pane.upgrade() {
2557 pane.update(cx, |pane, cx| {
2558 window.focus(&pane.focus_handle(cx), cx);
2559 loop {
2560 // Retrieve the weak item handle from the history.
2561 let entry = cb(pane.nav_history_mut(), cx)?;
2562
2563 // If the item is still present in this pane, then activate it.
2564 if let Some(index) = entry
2565 .item
2566 .upgrade()
2567 .and_then(|v| pane.index_for_item(v.as_ref()))
2568 {
2569 let prev_active_item_index = pane.active_item_index();
2570 pane.nav_history_mut().set_mode(mode);
2571 pane.activate_item(index, true, true, window, cx);
2572 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2573
2574 let mut navigated = prev_active_item_index != pane.active_item_index();
2575 if let Some(data) = entry.data {
2576 navigated |= pane.active_item()?.navigate(data, window, cx);
2577 }
2578
2579 if navigated {
2580 break None;
2581 }
2582 } else {
2583 // If the item is no longer present in this pane, then retrieve its
2584 // path info in order to reopen it.
2585 break pane
2586 .nav_history()
2587 .path_for_item(entry.item.id())
2588 .map(|(project_path, abs_path)| (project_path, abs_path, entry));
2589 }
2590 }
2591 })
2592 } else {
2593 None
2594 };
2595
2596 if let Some((project_path, abs_path, entry)) = to_load {
2597 // If the item was no longer present, then load it again from its previous path, first try the local path
2598 let open_by_project_path = self.load_path(project_path.clone(), window, cx);
2599
2600 cx.spawn_in(window, async move |workspace, cx| {
2601 let open_by_project_path = open_by_project_path.await;
2602 let mut navigated = false;
2603 match open_by_project_path
2604 .with_context(|| format!("Navigating to {project_path:?}"))
2605 {
2606 Ok((project_entry_id, build_item)) => {
2607 let prev_active_item_id = pane.update(cx, |pane, _| {
2608 pane.nav_history_mut().set_mode(mode);
2609 pane.active_item().map(|p| p.item_id())
2610 })?;
2611
2612 pane.update_in(cx, |pane, window, cx| {
2613 let item = pane.open_item(
2614 project_entry_id,
2615 project_path,
2616 true,
2617 entry.is_preview,
2618 true,
2619 None,
2620 window, cx,
2621 build_item,
2622 );
2623 navigated |= Some(item.item_id()) != prev_active_item_id;
2624 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2625 if let Some(data) = entry.data {
2626 navigated |= item.navigate(data, window, cx);
2627 }
2628 })?;
2629 }
2630 Err(open_by_project_path_e) => {
2631 // Fall back to opening by abs path, in case an external file was opened and closed,
2632 // and its worktree is now dropped
2633 if let Some(abs_path) = abs_path {
2634 let prev_active_item_id = pane.update(cx, |pane, _| {
2635 pane.nav_history_mut().set_mode(mode);
2636 pane.active_item().map(|p| p.item_id())
2637 })?;
2638 let open_by_abs_path = workspace.update_in(cx, |workspace, window, cx| {
2639 workspace.open_abs_path(abs_path.clone(), OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
2640 })?;
2641 match open_by_abs_path
2642 .await
2643 .with_context(|| format!("Navigating to {abs_path:?}"))
2644 {
2645 Ok(item) => {
2646 pane.update_in(cx, |pane, window, cx| {
2647 navigated |= Some(item.item_id()) != prev_active_item_id;
2648 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2649 if let Some(data) = entry.data {
2650 navigated |= item.navigate(data, window, cx);
2651 }
2652 })?;
2653 }
2654 Err(open_by_abs_path_e) => {
2655 log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
2656 }
2657 }
2658 }
2659 }
2660 }
2661
2662 if !navigated {
2663 workspace
2664 .update_in(cx, |workspace, window, cx| {
2665 Self::navigate_history(workspace, pane, mode, window, cx)
2666 })?
2667 .await?;
2668 }
2669
2670 Ok(())
2671 })
2672 } else {
2673 Task::ready(Ok(()))
2674 }
2675 }
2676
2677 pub fn go_back(
2678 &mut self,
2679 pane: WeakEntity<Pane>,
2680 window: &mut Window,
2681 cx: &mut Context<Workspace>,
2682 ) -> Task<Result<()>> {
2683 self.navigate_history(pane, NavigationMode::GoingBack, window, cx)
2684 }
2685
2686 pub fn go_forward(
2687 &mut self,
2688 pane: WeakEntity<Pane>,
2689 window: &mut Window,
2690 cx: &mut Context<Workspace>,
2691 ) -> Task<Result<()>> {
2692 self.navigate_history(pane, NavigationMode::GoingForward, window, cx)
2693 }
2694
2695 pub fn reopen_closed_item(
2696 &mut self,
2697 window: &mut Window,
2698 cx: &mut Context<Workspace>,
2699 ) -> Task<Result<()>> {
2700 self.navigate_history(
2701 self.active_pane().downgrade(),
2702 NavigationMode::ReopeningClosedItem,
2703 window,
2704 cx,
2705 )
2706 }
2707
2708 pub fn client(&self) -> &Arc<Client> {
2709 &self.app_state.client
2710 }
2711
2712 pub fn set_titlebar_item(&mut self, item: AnyView, _: &mut Window, cx: &mut Context<Self>) {
2713 self.titlebar_item = Some(item);
2714 cx.notify();
2715 }
2716
2717 pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
2718 self.on_prompt_for_new_path = Some(prompt)
2719 }
2720
2721 pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) {
2722 self.on_prompt_for_open_path = Some(prompt)
2723 }
2724
2725 pub fn set_terminal_provider(&mut self, provider: impl TerminalProvider + 'static) {
2726 self.terminal_provider = Some(Box::new(provider));
2727 }
2728
2729 pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) {
2730 self.debugger_provider = Some(Arc::new(provider));
2731 }
2732
2733 pub fn debugger_provider(&self) -> Option<Arc<dyn DebuggerProvider>> {
2734 self.debugger_provider.clone()
2735 }
2736
2737 pub fn prompt_for_open_path(
2738 &mut self,
2739 path_prompt_options: PathPromptOptions,
2740 lister: DirectoryLister,
2741 window: &mut Window,
2742 cx: &mut Context<Self>,
2743 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
2744 if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
2745 let prompt = self.on_prompt_for_open_path.take().unwrap();
2746 let rx = prompt(self, lister, window, cx);
2747 self.on_prompt_for_open_path = Some(prompt);
2748 rx
2749 } else {
2750 let (tx, rx) = oneshot::channel();
2751 let abs_path = cx.prompt_for_paths(path_prompt_options);
2752
2753 cx.spawn_in(window, async move |workspace, cx| {
2754 let Ok(result) = abs_path.await else {
2755 return Ok(());
2756 };
2757
2758 match result {
2759 Ok(result) => {
2760 tx.send(result).ok();
2761 }
2762 Err(err) => {
2763 let rx = workspace.update_in(cx, |workspace, window, cx| {
2764 workspace.show_portal_error(err.to_string(), cx);
2765 let prompt = workspace.on_prompt_for_open_path.take().unwrap();
2766 let rx = prompt(workspace, lister, window, cx);
2767 workspace.on_prompt_for_open_path = Some(prompt);
2768 rx
2769 })?;
2770 if let Ok(path) = rx.await {
2771 tx.send(path).ok();
2772 }
2773 }
2774 };
2775 anyhow::Ok(())
2776 })
2777 .detach();
2778
2779 rx
2780 }
2781 }
2782
2783 pub fn prompt_for_new_path(
2784 &mut self,
2785 lister: DirectoryLister,
2786 suggested_name: Option<String>,
2787 window: &mut Window,
2788 cx: &mut Context<Self>,
2789 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
2790 if self.project.read(cx).is_via_collab()
2791 || self.project.read(cx).is_via_remote_server()
2792 || !WorkspaceSettings::get_global(cx).use_system_path_prompts
2793 {
2794 let prompt = self.on_prompt_for_new_path.take().unwrap();
2795 let rx = prompt(self, lister, suggested_name, window, cx);
2796 self.on_prompt_for_new_path = Some(prompt);
2797 return rx;
2798 }
2799
2800 let (tx, rx) = oneshot::channel();
2801 cx.spawn_in(window, async move |workspace, cx| {
2802 let abs_path = workspace.update(cx, |workspace, cx| {
2803 let relative_to = workspace
2804 .most_recent_active_path(cx)
2805 .and_then(|p| p.parent().map(|p| p.to_path_buf()))
2806 .or_else(|| {
2807 let project = workspace.project.read(cx);
2808 project.visible_worktrees(cx).find_map(|worktree| {
2809 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
2810 })
2811 })
2812 .or_else(std::env::home_dir)
2813 .unwrap_or_else(|| PathBuf::from(""));
2814 cx.prompt_for_new_path(&relative_to, suggested_name.as_deref())
2815 })?;
2816 let abs_path = match abs_path.await? {
2817 Ok(path) => path,
2818 Err(err) => {
2819 let rx = workspace.update_in(cx, |workspace, window, cx| {
2820 workspace.show_portal_error(err.to_string(), cx);
2821
2822 let prompt = workspace.on_prompt_for_new_path.take().unwrap();
2823 let rx = prompt(workspace, lister, suggested_name, window, cx);
2824 workspace.on_prompt_for_new_path = Some(prompt);
2825 rx
2826 })?;
2827 if let Ok(path) = rx.await {
2828 tx.send(path).ok();
2829 }
2830 return anyhow::Ok(());
2831 }
2832 };
2833
2834 tx.send(abs_path.map(|path| vec![path])).ok();
2835 anyhow::Ok(())
2836 })
2837 .detach();
2838
2839 rx
2840 }
2841
2842 pub fn titlebar_item(&self) -> Option<AnyView> {
2843 self.titlebar_item.clone()
2844 }
2845
2846 /// Returns the worktree override set by the user (e.g., via the project dropdown).
2847 /// When set, git-related operations should use this worktree instead of deriving
2848 /// the active worktree from the focused file.
2849 pub fn active_worktree_override(&self) -> Option<WorktreeId> {
2850 self.active_worktree_override
2851 }
2852
2853 pub fn set_active_worktree_override(
2854 &mut self,
2855 worktree_id: Option<WorktreeId>,
2856 cx: &mut Context<Self>,
2857 ) {
2858 self.active_worktree_override = worktree_id;
2859 cx.notify();
2860 }
2861
2862 pub fn clear_active_worktree_override(&mut self, cx: &mut Context<Self>) {
2863 self.active_worktree_override = None;
2864 cx.notify();
2865 }
2866
2867 /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
2868 ///
2869 /// If the given workspace has a local project, then it will be passed
2870 /// to the callback. Otherwise, a new empty window will be created.
2871 pub fn with_local_workspace<T, F>(
2872 &mut self,
2873 window: &mut Window,
2874 cx: &mut Context<Self>,
2875 callback: F,
2876 ) -> Task<Result<T>>
2877 where
2878 T: 'static,
2879 F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
2880 {
2881 if self.project.read(cx).is_local() {
2882 Task::ready(Ok(callback(self, window, cx)))
2883 } else {
2884 let env = self.project.read(cx).cli_environment(cx);
2885 let task = Self::new_local(
2886 Vec::new(),
2887 self.app_state.clone(),
2888 None,
2889 env,
2890 None,
2891 true,
2892 cx,
2893 );
2894 cx.spawn_in(window, async move |_vh, cx| {
2895 let OpenResult {
2896 window: multi_workspace_window,
2897 ..
2898 } = task.await?;
2899 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
2900 let workspace = multi_workspace.workspace().clone();
2901 workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
2902 })
2903 })
2904 }
2905 }
2906
2907 /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
2908 ///
2909 /// If the given workspace has a local project, then it will be passed
2910 /// to the callback. Otherwise, a new empty window will be created.
2911 pub fn with_local_or_wsl_workspace<T, F>(
2912 &mut self,
2913 window: &mut Window,
2914 cx: &mut Context<Self>,
2915 callback: F,
2916 ) -> Task<Result<T>>
2917 where
2918 T: 'static,
2919 F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
2920 {
2921 let project = self.project.read(cx);
2922 if project.is_local() || project.is_via_wsl_with_host_interop(cx) {
2923 Task::ready(Ok(callback(self, window, cx)))
2924 } else {
2925 let env = self.project.read(cx).cli_environment(cx);
2926 let task = Self::new_local(
2927 Vec::new(),
2928 self.app_state.clone(),
2929 None,
2930 env,
2931 None,
2932 true,
2933 cx,
2934 );
2935 cx.spawn_in(window, async move |_vh, cx| {
2936 let OpenResult {
2937 window: multi_workspace_window,
2938 ..
2939 } = task.await?;
2940 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
2941 let workspace = multi_workspace.workspace().clone();
2942 workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
2943 })
2944 })
2945 }
2946 }
2947
2948 pub fn worktrees<'a>(&self, cx: &'a App) -> impl 'a + Iterator<Item = Entity<Worktree>> {
2949 self.project.read(cx).worktrees(cx)
2950 }
2951
2952 pub fn visible_worktrees<'a>(
2953 &self,
2954 cx: &'a App,
2955 ) -> impl 'a + Iterator<Item = Entity<Worktree>> {
2956 self.project.read(cx).visible_worktrees(cx)
2957 }
2958
2959 #[cfg(any(test, feature = "test-support"))]
2960 pub fn worktree_scans_complete(&self, cx: &App) -> impl Future<Output = ()> + 'static + use<> {
2961 let futures = self
2962 .worktrees(cx)
2963 .filter_map(|worktree| worktree.read(cx).as_local())
2964 .map(|worktree| worktree.scan_complete())
2965 .collect::<Vec<_>>();
2966 async move {
2967 for future in futures {
2968 future.await;
2969 }
2970 }
2971 }
2972
2973 pub fn close_global(cx: &mut App) {
2974 cx.defer(|cx| {
2975 cx.windows().iter().find(|window| {
2976 window
2977 .update(cx, |_, window, _| {
2978 if window.is_window_active() {
2979 //This can only get called when the window's project connection has been lost
2980 //so we don't need to prompt the user for anything and instead just close the window
2981 window.remove_window();
2982 true
2983 } else {
2984 false
2985 }
2986 })
2987 .unwrap_or(false)
2988 });
2989 });
2990 }
2991
2992 pub fn move_focused_panel_to_next_position(
2993 &mut self,
2994 _: &MoveFocusedPanelToNextPosition,
2995 window: &mut Window,
2996 cx: &mut Context<Self>,
2997 ) {
2998 let docks = self.all_docks();
2999 let active_dock = docks
3000 .into_iter()
3001 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
3002
3003 if let Some(dock) = active_dock {
3004 dock.update(cx, |dock, cx| {
3005 let active_panel = dock
3006 .active_panel()
3007 .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx));
3008
3009 if let Some(panel) = active_panel {
3010 panel.move_to_next_position(window, cx);
3011 }
3012 })
3013 }
3014 }
3015
3016 pub fn prepare_to_close(
3017 &mut self,
3018 close_intent: CloseIntent,
3019 window: &mut Window,
3020 cx: &mut Context<Self>,
3021 ) -> Task<Result<bool>> {
3022 let active_call = self.active_global_call();
3023
3024 cx.spawn_in(window, async move |this, cx| {
3025 this.update(cx, |this, _| {
3026 if close_intent == CloseIntent::CloseWindow {
3027 this.removing = true;
3028 }
3029 })?;
3030
3031 let workspace_count = cx.update(|_window, cx| {
3032 cx.windows()
3033 .iter()
3034 .filter(|window| window.downcast::<MultiWorkspace>().is_some())
3035 .count()
3036 })?;
3037
3038 #[cfg(target_os = "macos")]
3039 let save_last_workspace = false;
3040
3041 // On Linux and Windows, closing the last window should restore the last workspace.
3042 #[cfg(not(target_os = "macos"))]
3043 let save_last_workspace = {
3044 let remaining_workspaces = cx.update(|_window, cx| {
3045 cx.windows()
3046 .iter()
3047 .filter_map(|window| window.downcast::<MultiWorkspace>())
3048 .filter_map(|multi_workspace| {
3049 multi_workspace
3050 .update(cx, |multi_workspace, _, cx| {
3051 multi_workspace.workspace().read(cx).removing
3052 })
3053 .ok()
3054 })
3055 .filter(|removing| !removing)
3056 .count()
3057 })?;
3058
3059 close_intent != CloseIntent::ReplaceWindow && remaining_workspaces == 0
3060 };
3061
3062 if let Some(active_call) = active_call
3063 && workspace_count == 1
3064 && cx
3065 .update(|_window, cx| active_call.0.is_in_room(cx))
3066 .unwrap_or(false)
3067 {
3068 if close_intent == CloseIntent::CloseWindow {
3069 this.update(cx, |_, cx| cx.emit(Event::Activate))?;
3070 let answer = cx.update(|window, cx| {
3071 window.prompt(
3072 PromptLevel::Warning,
3073 "Do you want to leave the current call?",
3074 None,
3075 &["Close window and hang up", "Cancel"],
3076 cx,
3077 )
3078 })?;
3079
3080 if answer.await.log_err() == Some(1) {
3081 return anyhow::Ok(false);
3082 } else {
3083 if let Ok(task) = cx.update(|_window, cx| active_call.0.hang_up(cx)) {
3084 task.await.log_err();
3085 }
3086 }
3087 }
3088 if close_intent == CloseIntent::ReplaceWindow {
3089 _ = cx.update(|_window, cx| {
3090 let multi_workspace = cx
3091 .windows()
3092 .iter()
3093 .filter_map(|window| window.downcast::<MultiWorkspace>())
3094 .next()
3095 .unwrap();
3096 let project = multi_workspace
3097 .read(cx)?
3098 .workspace()
3099 .read(cx)
3100 .project
3101 .clone();
3102 if project.read(cx).is_shared() {
3103 active_call.0.unshare_project(project, cx)?;
3104 }
3105 Ok::<_, anyhow::Error>(())
3106 });
3107 }
3108 }
3109
3110 let save_result = this
3111 .update_in(cx, |this, window, cx| {
3112 this.save_all_internal(SaveIntent::Close, window, cx)
3113 })?
3114 .await;
3115
3116 // If we're not quitting, but closing, we remove the workspace from
3117 // the current session.
3118 if close_intent != CloseIntent::Quit
3119 && !save_last_workspace
3120 && save_result.as_ref().is_ok_and(|&res| res)
3121 {
3122 this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx))?
3123 .await;
3124 }
3125
3126 save_result
3127 })
3128 }
3129
3130 fn save_all(&mut self, action: &SaveAll, window: &mut Window, cx: &mut Context<Self>) {
3131 self.save_all_internal(
3132 action.save_intent.unwrap_or(SaveIntent::SaveAll),
3133 window,
3134 cx,
3135 )
3136 .detach_and_log_err(cx);
3137 }
3138
3139 fn send_keystrokes(
3140 &mut self,
3141 action: &SendKeystrokes,
3142 window: &mut Window,
3143 cx: &mut Context<Self>,
3144 ) {
3145 let keystrokes: Vec<Keystroke> = action
3146 .0
3147 .split(' ')
3148 .flat_map(|k| Keystroke::parse(k).log_err())
3149 .map(|k| {
3150 cx.keyboard_mapper()
3151 .map_key_equivalent(k, false)
3152 .inner()
3153 .clone()
3154 })
3155 .collect();
3156 let _ = self.send_keystrokes_impl(keystrokes, window, cx);
3157 }
3158
3159 pub fn send_keystrokes_impl(
3160 &mut self,
3161 keystrokes: Vec<Keystroke>,
3162 window: &mut Window,
3163 cx: &mut Context<Self>,
3164 ) -> Shared<Task<()>> {
3165 let mut state = self.dispatching_keystrokes.borrow_mut();
3166 if !state.dispatched.insert(keystrokes.clone()) {
3167 cx.propagate();
3168 return state.task.clone().unwrap();
3169 }
3170
3171 state.queue.extend(keystrokes);
3172
3173 let keystrokes = self.dispatching_keystrokes.clone();
3174 if state.task.is_none() {
3175 state.task = Some(
3176 window
3177 .spawn(cx, async move |cx| {
3178 // limit to 100 keystrokes to avoid infinite recursion.
3179 for _ in 0..100 {
3180 let keystroke = {
3181 let mut state = keystrokes.borrow_mut();
3182 let Some(keystroke) = state.queue.pop_front() else {
3183 state.dispatched.clear();
3184 state.task.take();
3185 return;
3186 };
3187 keystroke
3188 };
3189 cx.update(|window, cx| {
3190 let focused = window.focused(cx);
3191 window.dispatch_keystroke(keystroke.clone(), cx);
3192 if window.focused(cx) != focused {
3193 // dispatch_keystroke may cause the focus to change.
3194 // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
3195 // And we need that to happen before the next keystroke to keep vim mode happy...
3196 // (Note that the tests always do this implicitly, so you must manually test with something like:
3197 // "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
3198 // )
3199 window.draw(cx).clear();
3200 }
3201 })
3202 .ok();
3203
3204 // Yield between synthetic keystrokes so deferred focus and
3205 // other effects can settle before dispatching the next key.
3206 yield_now().await;
3207 }
3208
3209 *keystrokes.borrow_mut() = Default::default();
3210 log::error!("over 100 keystrokes passed to send_keystrokes");
3211 })
3212 .shared(),
3213 );
3214 }
3215 state.task.clone().unwrap()
3216 }
3217
3218 fn save_all_internal(
3219 &mut self,
3220 mut save_intent: SaveIntent,
3221 window: &mut Window,
3222 cx: &mut Context<Self>,
3223 ) -> Task<Result<bool>> {
3224 if self.project.read(cx).is_disconnected(cx) {
3225 return Task::ready(Ok(true));
3226 }
3227 let dirty_items = self
3228 .panes
3229 .iter()
3230 .flat_map(|pane| {
3231 pane.read(cx).items().filter_map(|item| {
3232 if item.is_dirty(cx) {
3233 item.tab_content_text(0, cx);
3234 Some((pane.downgrade(), item.boxed_clone()))
3235 } else {
3236 None
3237 }
3238 })
3239 })
3240 .collect::<Vec<_>>();
3241
3242 let project = self.project.clone();
3243 cx.spawn_in(window, async move |workspace, cx| {
3244 let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() {
3245 let (serialize_tasks, remaining_dirty_items) =
3246 workspace.update_in(cx, |workspace, window, cx| {
3247 let mut remaining_dirty_items = Vec::new();
3248 let mut serialize_tasks = Vec::new();
3249 for (pane, item) in dirty_items {
3250 if let Some(task) = item
3251 .to_serializable_item_handle(cx)
3252 .and_then(|handle| handle.serialize(workspace, true, window, cx))
3253 {
3254 serialize_tasks.push(task);
3255 } else {
3256 remaining_dirty_items.push((pane, item));
3257 }
3258 }
3259 (serialize_tasks, remaining_dirty_items)
3260 })?;
3261
3262 futures::future::try_join_all(serialize_tasks).await?;
3263
3264 if !remaining_dirty_items.is_empty() {
3265 workspace.update(cx, |_, cx| cx.emit(Event::Activate))?;
3266 }
3267
3268 if remaining_dirty_items.len() > 1 {
3269 let answer = workspace.update_in(cx, |_, window, cx| {
3270 let detail = Pane::file_names_for_prompt(
3271 &mut remaining_dirty_items.iter().map(|(_, handle)| handle),
3272 cx,
3273 );
3274 window.prompt(
3275 PromptLevel::Warning,
3276 "Do you want to save all changes in the following files?",
3277 Some(&detail),
3278 &["Save all", "Discard all", "Cancel"],
3279 cx,
3280 )
3281 })?;
3282 match answer.await.log_err() {
3283 Some(0) => save_intent = SaveIntent::SaveAll,
3284 Some(1) => save_intent = SaveIntent::Skip,
3285 Some(2) => return Ok(false),
3286 _ => {}
3287 }
3288 }
3289
3290 remaining_dirty_items
3291 } else {
3292 dirty_items
3293 };
3294
3295 for (pane, item) in dirty_items {
3296 let (singleton, project_entry_ids) = cx.update(|_, cx| {
3297 (
3298 item.buffer_kind(cx) == ItemBufferKind::Singleton,
3299 item.project_entry_ids(cx),
3300 )
3301 })?;
3302 if (singleton || !project_entry_ids.is_empty())
3303 && !Pane::save_item(project.clone(), &pane, &*item, save_intent, cx).await?
3304 {
3305 return Ok(false);
3306 }
3307 }
3308 Ok(true)
3309 })
3310 }
3311
3312 pub fn open_workspace_for_paths(
3313 &mut self,
3314 replace_current_window: bool,
3315 paths: Vec<PathBuf>,
3316 window: &mut Window,
3317 cx: &mut Context<Self>,
3318 ) -> Task<Result<Entity<Workspace>>> {
3319 let window_handle = window.window_handle().downcast::<MultiWorkspace>();
3320 let is_remote = self.project.read(cx).is_via_collab();
3321 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
3322 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
3323
3324 let window_to_replace = if replace_current_window {
3325 window_handle
3326 } else if is_remote || has_worktree || has_dirty_items {
3327 None
3328 } else {
3329 window_handle
3330 };
3331 let app_state = self.app_state.clone();
3332
3333 cx.spawn(async move |_, cx| {
3334 let OpenResult { workspace, .. } = cx
3335 .update(|cx| {
3336 open_paths(
3337 &paths,
3338 app_state,
3339 OpenOptions {
3340 replace_window: window_to_replace,
3341 ..Default::default()
3342 },
3343 cx,
3344 )
3345 })
3346 .await?;
3347 Ok(workspace)
3348 })
3349 }
3350
3351 #[allow(clippy::type_complexity)]
3352 pub fn open_paths(
3353 &mut self,
3354 mut abs_paths: Vec<PathBuf>,
3355 options: OpenOptions,
3356 pane: Option<WeakEntity<Pane>>,
3357 window: &mut Window,
3358 cx: &mut Context<Self>,
3359 ) -> Task<Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>> {
3360 let fs = self.app_state.fs.clone();
3361
3362 let caller_ordered_abs_paths = abs_paths.clone();
3363
3364 // Sort the paths to ensure we add worktrees for parents before their children.
3365 abs_paths.sort_unstable();
3366 cx.spawn_in(window, async move |this, cx| {
3367 let mut tasks = Vec::with_capacity(abs_paths.len());
3368
3369 for abs_path in &abs_paths {
3370 let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
3371 OpenVisible::All => Some(true),
3372 OpenVisible::None => Some(false),
3373 OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
3374 Some(Some(metadata)) => Some(!metadata.is_dir),
3375 Some(None) => Some(true),
3376 None => None,
3377 },
3378 OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
3379 Some(Some(metadata)) => Some(metadata.is_dir),
3380 Some(None) => Some(false),
3381 None => None,
3382 },
3383 };
3384 let project_path = match visible {
3385 Some(visible) => match this
3386 .update(cx, |this, cx| {
3387 Workspace::project_path_for_path(
3388 this.project.clone(),
3389 abs_path,
3390 visible,
3391 cx,
3392 )
3393 })
3394 .log_err()
3395 {
3396 Some(project_path) => project_path.await.log_err(),
3397 None => None,
3398 },
3399 None => None,
3400 };
3401
3402 let this = this.clone();
3403 let abs_path: Arc<Path> = SanitizedPath::new(&abs_path).as_path().into();
3404 let fs = fs.clone();
3405 let pane = pane.clone();
3406 let task = cx.spawn(async move |cx| {
3407 let (_worktree, project_path) = project_path?;
3408 if fs.is_dir(&abs_path).await {
3409 // Opening a directory should not race to update the active entry.
3410 // We'll select/reveal a deterministic final entry after all paths finish opening.
3411 None
3412 } else {
3413 Some(
3414 this.update_in(cx, |this, window, cx| {
3415 this.open_path(
3416 project_path,
3417 pane,
3418 options.focus.unwrap_or(true),
3419 window,
3420 cx,
3421 )
3422 })
3423 .ok()?
3424 .await,
3425 )
3426 }
3427 });
3428 tasks.push(task);
3429 }
3430
3431 let results = futures::future::join_all(tasks).await;
3432
3433 // Determine the winner using the fake/abstract FS metadata, not `Path::is_dir`.
3434 let mut winner: Option<(PathBuf, bool)> = None;
3435 for abs_path in caller_ordered_abs_paths.into_iter().rev() {
3436 if let Some(Some(metadata)) = fs.metadata(&abs_path).await.log_err() {
3437 if !metadata.is_dir {
3438 winner = Some((abs_path, false));
3439 break;
3440 }
3441 if winner.is_none() {
3442 winner = Some((abs_path, true));
3443 }
3444 } else if winner.is_none() {
3445 winner = Some((abs_path, false));
3446 }
3447 }
3448
3449 // Compute the winner entry id on the foreground thread and emit once, after all
3450 // paths finish opening. This avoids races between concurrently-opening paths
3451 // (directories in particular) and makes the resulting project panel selection
3452 // deterministic.
3453 if let Some((winner_abs_path, winner_is_dir)) = winner {
3454 'emit_winner: {
3455 let winner_abs_path: Arc<Path> =
3456 SanitizedPath::new(&winner_abs_path).as_path().into();
3457
3458 let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
3459 OpenVisible::All => true,
3460 OpenVisible::None => false,
3461 OpenVisible::OnlyFiles => !winner_is_dir,
3462 OpenVisible::OnlyDirectories => winner_is_dir,
3463 };
3464
3465 let Some(worktree_task) = this
3466 .update(cx, |workspace, cx| {
3467 workspace.project.update(cx, |project, cx| {
3468 project.find_or_create_worktree(
3469 winner_abs_path.as_ref(),
3470 visible,
3471 cx,
3472 )
3473 })
3474 })
3475 .ok()
3476 else {
3477 break 'emit_winner;
3478 };
3479
3480 let Ok((worktree, _)) = worktree_task.await else {
3481 break 'emit_winner;
3482 };
3483
3484 let Ok(Some(entry_id)) = this.update(cx, |_, cx| {
3485 let worktree = worktree.read(cx);
3486 let worktree_abs_path = worktree.abs_path();
3487 let entry = if winner_abs_path.as_ref() == worktree_abs_path.as_ref() {
3488 worktree.root_entry()
3489 } else {
3490 winner_abs_path
3491 .strip_prefix(worktree_abs_path.as_ref())
3492 .ok()
3493 .and_then(|relative_path| {
3494 let relative_path =
3495 RelPath::new(relative_path, PathStyle::local())
3496 .log_err()?;
3497 worktree.entry_for_path(&relative_path)
3498 })
3499 }?;
3500 Some(entry.id)
3501 }) else {
3502 break 'emit_winner;
3503 };
3504
3505 this.update(cx, |workspace, cx| {
3506 workspace.project.update(cx, |_, cx| {
3507 cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
3508 });
3509 })
3510 .ok();
3511 }
3512 }
3513
3514 results
3515 })
3516 }
3517
3518 pub fn open_resolved_path(
3519 &mut self,
3520 path: ResolvedPath,
3521 window: &mut Window,
3522 cx: &mut Context<Self>,
3523 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3524 match path {
3525 ResolvedPath::ProjectPath { project_path, .. } => {
3526 self.open_path(project_path, None, true, window, cx)
3527 }
3528 ResolvedPath::AbsPath { path, .. } => self.open_abs_path(
3529 PathBuf::from(path),
3530 OpenOptions {
3531 visible: Some(OpenVisible::None),
3532 ..Default::default()
3533 },
3534 window,
3535 cx,
3536 ),
3537 }
3538 }
3539
3540 pub fn absolute_path_of_worktree(
3541 &self,
3542 worktree_id: WorktreeId,
3543 cx: &mut Context<Self>,
3544 ) -> Option<PathBuf> {
3545 self.project
3546 .read(cx)
3547 .worktree_for_id(worktree_id, cx)
3548 // TODO: use `abs_path` or `root_dir`
3549 .map(|wt| wt.read(cx).abs_path().as_ref().to_path_buf())
3550 }
3551
3552 pub fn add_folder_to_project(
3553 &mut self,
3554 _: &AddFolderToProject,
3555 window: &mut Window,
3556 cx: &mut Context<Self>,
3557 ) {
3558 let project = self.project.read(cx);
3559 if project.is_via_collab() {
3560 self.show_error(
3561 &anyhow!("You cannot add folders to someone else's project"),
3562 cx,
3563 );
3564 return;
3565 }
3566 let paths = self.prompt_for_open_path(
3567 PathPromptOptions {
3568 files: false,
3569 directories: true,
3570 multiple: true,
3571 prompt: None,
3572 },
3573 DirectoryLister::Project(self.project.clone()),
3574 window,
3575 cx,
3576 );
3577 cx.spawn_in(window, async move |this, cx| {
3578 if let Some(paths) = paths.await.log_err().flatten() {
3579 let results = this
3580 .update_in(cx, |this, window, cx| {
3581 this.open_paths(
3582 paths,
3583 OpenOptions {
3584 visible: Some(OpenVisible::All),
3585 ..Default::default()
3586 },
3587 None,
3588 window,
3589 cx,
3590 )
3591 })?
3592 .await;
3593 for result in results.into_iter().flatten() {
3594 result.log_err();
3595 }
3596 }
3597 anyhow::Ok(())
3598 })
3599 .detach_and_log_err(cx);
3600 }
3601
3602 pub fn project_path_for_path(
3603 project: Entity<Project>,
3604 abs_path: &Path,
3605 visible: bool,
3606 cx: &mut App,
3607 ) -> Task<Result<(Entity<Worktree>, ProjectPath)>> {
3608 let entry = project.update(cx, |project, cx| {
3609 project.find_or_create_worktree(abs_path, visible, cx)
3610 });
3611 cx.spawn(async move |cx| {
3612 let (worktree, path) = entry.await?;
3613 let worktree_id = worktree.read_with(cx, |t, _| t.id());
3614 Ok((worktree, ProjectPath { worktree_id, path }))
3615 })
3616 }
3617
3618 pub fn items<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator<Item = &'a Box<dyn ItemHandle>> {
3619 self.panes.iter().flat_map(|pane| pane.read(cx).items())
3620 }
3621
3622 pub fn item_of_type<T: Item>(&self, cx: &App) -> Option<Entity<T>> {
3623 self.items_of_type(cx).max_by_key(|item| item.item_id())
3624 }
3625
3626 pub fn items_of_type<'a, T: Item>(
3627 &'a self,
3628 cx: &'a App,
3629 ) -> impl 'a + Iterator<Item = Entity<T>> {
3630 self.panes
3631 .iter()
3632 .flat_map(|pane| pane.read(cx).items_of_type())
3633 }
3634
3635 pub fn active_item(&self, cx: &App) -> Option<Box<dyn ItemHandle>> {
3636 self.active_pane().read(cx).active_item()
3637 }
3638
3639 pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
3640 let item = self.active_item(cx)?;
3641 item.to_any_view().downcast::<I>().ok()
3642 }
3643
3644 fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
3645 self.active_item(cx).and_then(|item| item.project_path(cx))
3646 }
3647
3648 pub fn most_recent_active_path(&self, cx: &App) -> Option<PathBuf> {
3649 self.recent_navigation_history_iter(cx)
3650 .filter_map(|(path, abs_path)| {
3651 let worktree = self
3652 .project
3653 .read(cx)
3654 .worktree_for_id(path.worktree_id, cx)?;
3655 if worktree.read(cx).is_visible() {
3656 abs_path
3657 } else {
3658 None
3659 }
3660 })
3661 .next()
3662 }
3663
3664 pub fn save_active_item(
3665 &mut self,
3666 save_intent: SaveIntent,
3667 window: &mut Window,
3668 cx: &mut App,
3669 ) -> Task<Result<()>> {
3670 let project = self.project.clone();
3671 let pane = self.active_pane();
3672 let item = pane.read(cx).active_item();
3673 let pane = pane.downgrade();
3674
3675 window.spawn(cx, async move |cx| {
3676 if let Some(item) = item {
3677 Pane::save_item(project, &pane, item.as_ref(), save_intent, cx)
3678 .await
3679 .map(|_| ())
3680 } else {
3681 Ok(())
3682 }
3683 })
3684 }
3685
3686 pub fn close_inactive_items_and_panes(
3687 &mut self,
3688 action: &CloseInactiveTabsAndPanes,
3689 window: &mut Window,
3690 cx: &mut Context<Self>,
3691 ) {
3692 if let Some(task) = self.close_all_internal(
3693 true,
3694 action.save_intent.unwrap_or(SaveIntent::Close),
3695 window,
3696 cx,
3697 ) {
3698 task.detach_and_log_err(cx)
3699 }
3700 }
3701
3702 pub fn close_all_items_and_panes(
3703 &mut self,
3704 action: &CloseAllItemsAndPanes,
3705 window: &mut Window,
3706 cx: &mut Context<Self>,
3707 ) {
3708 if let Some(task) = self.close_all_internal(
3709 false,
3710 action.save_intent.unwrap_or(SaveIntent::Close),
3711 window,
3712 cx,
3713 ) {
3714 task.detach_and_log_err(cx)
3715 }
3716 }
3717
3718 /// Closes the active item across all panes.
3719 pub fn close_item_in_all_panes(
3720 &mut self,
3721 action: &CloseItemInAllPanes,
3722 window: &mut Window,
3723 cx: &mut Context<Self>,
3724 ) {
3725 let Some(active_item) = self.active_pane().read(cx).active_item() else {
3726 return;
3727 };
3728
3729 let save_intent = action.save_intent.unwrap_or(SaveIntent::Close);
3730 let close_pinned = action.close_pinned;
3731
3732 if let Some(project_path) = active_item.project_path(cx) {
3733 self.close_items_with_project_path(
3734 &project_path,
3735 save_intent,
3736 close_pinned,
3737 window,
3738 cx,
3739 );
3740 } else if close_pinned || !self.active_pane().read(cx).is_active_item_pinned() {
3741 let item_id = active_item.item_id();
3742 self.active_pane().update(cx, |pane, cx| {
3743 pane.close_item_by_id(item_id, save_intent, window, cx)
3744 .detach_and_log_err(cx);
3745 });
3746 }
3747 }
3748
3749 /// Closes all items with the given project path across all panes.
3750 pub fn close_items_with_project_path(
3751 &mut self,
3752 project_path: &ProjectPath,
3753 save_intent: SaveIntent,
3754 close_pinned: bool,
3755 window: &mut Window,
3756 cx: &mut Context<Self>,
3757 ) {
3758 let panes = self.panes().to_vec();
3759 for pane in panes {
3760 pane.update(cx, |pane, cx| {
3761 pane.close_items_for_project_path(
3762 project_path,
3763 save_intent,
3764 close_pinned,
3765 window,
3766 cx,
3767 )
3768 .detach_and_log_err(cx);
3769 });
3770 }
3771 }
3772
3773 fn close_all_internal(
3774 &mut self,
3775 retain_active_pane: bool,
3776 save_intent: SaveIntent,
3777 window: &mut Window,
3778 cx: &mut Context<Self>,
3779 ) -> Option<Task<Result<()>>> {
3780 let current_pane = self.active_pane();
3781
3782 let mut tasks = Vec::new();
3783
3784 if retain_active_pane {
3785 let current_pane_close = current_pane.update(cx, |pane, cx| {
3786 pane.close_other_items(
3787 &CloseOtherItems {
3788 save_intent: None,
3789 close_pinned: false,
3790 },
3791 None,
3792 window,
3793 cx,
3794 )
3795 });
3796
3797 tasks.push(current_pane_close);
3798 }
3799
3800 for pane in self.panes() {
3801 if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
3802 continue;
3803 }
3804
3805 let close_pane_items = pane.update(cx, |pane: &mut Pane, cx| {
3806 pane.close_all_items(
3807 &CloseAllItems {
3808 save_intent: Some(save_intent),
3809 close_pinned: false,
3810 },
3811 window,
3812 cx,
3813 )
3814 });
3815
3816 tasks.push(close_pane_items)
3817 }
3818
3819 if tasks.is_empty() {
3820 None
3821 } else {
3822 Some(cx.spawn_in(window, async move |_, _| {
3823 for task in tasks {
3824 task.await?
3825 }
3826 Ok(())
3827 }))
3828 }
3829 }
3830
3831 pub fn is_dock_at_position_open(&self, position: DockPosition, cx: &mut Context<Self>) -> bool {
3832 self.dock_at_position(position).read(cx).is_open()
3833 }
3834
3835 pub fn toggle_dock(
3836 &mut self,
3837 dock_side: DockPosition,
3838 window: &mut Window,
3839 cx: &mut Context<Self>,
3840 ) {
3841 let mut focus_center = false;
3842 let mut reveal_dock = false;
3843
3844 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
3845 let was_visible = self.is_dock_at_position_open(dock_side, cx) && !other_is_zoomed;
3846
3847 if let Some(panel) = self.dock_at_position(dock_side).read(cx).active_panel() {
3848 telemetry::event!(
3849 "Panel Button Clicked",
3850 name = panel.persistent_name(),
3851 toggle_state = !was_visible
3852 );
3853 }
3854 if was_visible {
3855 self.save_open_dock_positions(cx);
3856 }
3857
3858 let dock = self.dock_at_position(dock_side);
3859 dock.update(cx, |dock, cx| {
3860 dock.set_open(!was_visible, window, cx);
3861
3862 if dock.active_panel().is_none() {
3863 let Some(panel_ix) = dock
3864 .first_enabled_panel_idx(cx)
3865 .log_with_level(log::Level::Info)
3866 else {
3867 return;
3868 };
3869 dock.activate_panel(panel_ix, window, cx);
3870 }
3871
3872 if let Some(active_panel) = dock.active_panel() {
3873 if was_visible {
3874 if active_panel
3875 .panel_focus_handle(cx)
3876 .contains_focused(window, cx)
3877 {
3878 focus_center = true;
3879 }
3880 } else {
3881 let focus_handle = &active_panel.panel_focus_handle(cx);
3882 window.focus(focus_handle, cx);
3883 reveal_dock = true;
3884 }
3885 }
3886 });
3887
3888 if reveal_dock {
3889 self.dismiss_zoomed_items_to_reveal(Some(dock_side), window, cx);
3890 }
3891
3892 if focus_center {
3893 self.active_pane
3894 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
3895 }
3896
3897 cx.notify();
3898 self.serialize_workspace(window, cx);
3899 }
3900
3901 fn active_dock(&self, window: &Window, cx: &Context<Self>) -> Option<&Entity<Dock>> {
3902 self.all_docks().into_iter().find(|&dock| {
3903 dock.read(cx).is_open() && dock.focus_handle(cx).contains_focused(window, cx)
3904 })
3905 }
3906
3907 fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
3908 if let Some(dock) = self.active_dock(window, cx).cloned() {
3909 self.save_open_dock_positions(cx);
3910 dock.update(cx, |dock, cx| {
3911 dock.set_open(false, window, cx);
3912 });
3913 return true;
3914 }
3915 false
3916 }
3917
3918 pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3919 self.save_open_dock_positions(cx);
3920 for dock in self.all_docks() {
3921 dock.update(cx, |dock, cx| {
3922 dock.set_open(false, window, cx);
3923 });
3924 }
3925
3926 cx.focus_self(window);
3927 cx.notify();
3928 self.serialize_workspace(window, cx);
3929 }
3930
3931 fn get_open_dock_positions(&self, cx: &Context<Self>) -> Vec<DockPosition> {
3932 self.all_docks()
3933 .into_iter()
3934 .filter_map(|dock| {
3935 let dock_ref = dock.read(cx);
3936 if dock_ref.is_open() {
3937 Some(dock_ref.position())
3938 } else {
3939 None
3940 }
3941 })
3942 .collect()
3943 }
3944
3945 /// Saves the positions of currently open docks.
3946 ///
3947 /// Updates `last_open_dock_positions` with positions of all currently open
3948 /// docks, to later be restored by the 'Toggle All Docks' action.
3949 fn save_open_dock_positions(&mut self, cx: &mut Context<Self>) {
3950 let open_dock_positions = self.get_open_dock_positions(cx);
3951 if !open_dock_positions.is_empty() {
3952 self.last_open_dock_positions = open_dock_positions;
3953 }
3954 }
3955
3956 /// Toggles all docks between open and closed states.
3957 ///
3958 /// If any docks are open, closes all and remembers their positions. If all
3959 /// docks are closed, restores the last remembered dock configuration.
3960 fn toggle_all_docks(
3961 &mut self,
3962 _: &ToggleAllDocks,
3963 window: &mut Window,
3964 cx: &mut Context<Self>,
3965 ) {
3966 let open_dock_positions = self.get_open_dock_positions(cx);
3967
3968 if !open_dock_positions.is_empty() {
3969 self.close_all_docks(window, cx);
3970 } else if !self.last_open_dock_positions.is_empty() {
3971 self.restore_last_open_docks(window, cx);
3972 }
3973 }
3974
3975 /// Reopens docks from the most recently remembered configuration.
3976 ///
3977 /// Opens all docks whose positions are stored in `last_open_dock_positions`
3978 /// and clears the stored positions.
3979 fn restore_last_open_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3980 let positions_to_open = std::mem::take(&mut self.last_open_dock_positions);
3981
3982 for position in positions_to_open {
3983 let dock = self.dock_at_position(position);
3984 dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
3985 }
3986
3987 cx.focus_self(window);
3988 cx.notify();
3989 self.serialize_workspace(window, cx);
3990 }
3991
3992 /// Transfer focus to the panel of the given type.
3993 pub fn focus_panel<T: Panel>(
3994 &mut self,
3995 window: &mut Window,
3996 cx: &mut Context<Self>,
3997 ) -> Option<Entity<T>> {
3998 let panel = self.focus_or_unfocus_panel::<T>(window, cx, &mut |_, _, _| true)?;
3999 panel.to_any().downcast().ok()
4000 }
4001
4002 /// Focus the panel of the given type if it isn't already focused. If it is
4003 /// already focused, then transfer focus back to the workspace center.
4004 /// When the `close_panel_on_toggle` setting is enabled, also closes the
4005 /// panel when transferring focus back to the center.
4006 pub fn toggle_panel_focus<T: Panel>(
4007 &mut self,
4008 window: &mut Window,
4009 cx: &mut Context<Self>,
4010 ) -> bool {
4011 let mut did_focus_panel = false;
4012 self.focus_or_unfocus_panel::<T>(window, cx, &mut |panel, window, cx| {
4013 did_focus_panel = !panel.panel_focus_handle(cx).contains_focused(window, cx);
4014 did_focus_panel
4015 });
4016
4017 if !did_focus_panel && WorkspaceSettings::get_global(cx).close_panel_on_toggle {
4018 self.close_panel::<T>(window, cx);
4019 }
4020
4021 telemetry::event!(
4022 "Panel Button Clicked",
4023 name = T::persistent_name(),
4024 toggle_state = did_focus_panel
4025 );
4026
4027 did_focus_panel
4028 }
4029
4030 pub fn focus_center_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4031 if let Some(item) = self.active_item(cx) {
4032 item.item_focus_handle(cx).focus(window, cx);
4033 } else {
4034 log::error!("Could not find a focus target when switching focus to the center panes",);
4035 }
4036 }
4037
4038 pub fn activate_panel_for_proto_id(
4039 &mut self,
4040 panel_id: PanelId,
4041 window: &mut Window,
4042 cx: &mut Context<Self>,
4043 ) -> Option<Arc<dyn PanelHandle>> {
4044 let mut panel = None;
4045 for dock in self.all_docks() {
4046 if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
4047 panel = dock.update(cx, |dock, cx| {
4048 dock.activate_panel(panel_index, window, cx);
4049 dock.set_open(true, window, cx);
4050 dock.active_panel().cloned()
4051 });
4052 break;
4053 }
4054 }
4055
4056 if panel.is_some() {
4057 cx.notify();
4058 self.serialize_workspace(window, cx);
4059 }
4060
4061 panel
4062 }
4063
4064 /// Focus or unfocus the given panel type, depending on the given callback.
4065 fn focus_or_unfocus_panel<T: Panel>(
4066 &mut self,
4067 window: &mut Window,
4068 cx: &mut Context<Self>,
4069 should_focus: &mut dyn FnMut(&dyn PanelHandle, &mut Window, &mut Context<Dock>) -> bool,
4070 ) -> Option<Arc<dyn PanelHandle>> {
4071 let mut result_panel = None;
4072 let mut serialize = false;
4073 for dock in self.all_docks() {
4074 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
4075 let mut focus_center = false;
4076 let panel = dock.update(cx, |dock, cx| {
4077 dock.activate_panel(panel_index, window, cx);
4078
4079 let panel = dock.active_panel().cloned();
4080 if let Some(panel) = panel.as_ref() {
4081 if should_focus(&**panel, window, cx) {
4082 dock.set_open(true, window, cx);
4083 panel.panel_focus_handle(cx).focus(window, cx);
4084 } else {
4085 focus_center = true;
4086 }
4087 }
4088 panel
4089 });
4090
4091 if focus_center {
4092 self.active_pane
4093 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
4094 }
4095
4096 result_panel = panel;
4097 serialize = true;
4098 break;
4099 }
4100 }
4101
4102 if serialize {
4103 self.serialize_workspace(window, cx);
4104 }
4105
4106 cx.notify();
4107 result_panel
4108 }
4109
4110 /// Open the panel of the given type
4111 pub fn open_panel<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4112 for dock in self.all_docks() {
4113 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
4114 dock.update(cx, |dock, cx| {
4115 dock.activate_panel(panel_index, window, cx);
4116 dock.set_open(true, window, cx);
4117 });
4118 }
4119 }
4120 }
4121
4122 pub fn close_panel<T: Panel>(&self, window: &mut Window, cx: &mut Context<Self>) {
4123 for dock in self.all_docks().iter() {
4124 dock.update(cx, |dock, cx| {
4125 if dock.panel::<T>().is_some() {
4126 dock.set_open(false, window, cx)
4127 }
4128 })
4129 }
4130 }
4131
4132 pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
4133 self.all_docks()
4134 .iter()
4135 .find_map(|dock| dock.read(cx).panel::<T>())
4136 }
4137
4138 fn dismiss_zoomed_items_to_reveal(
4139 &mut self,
4140 dock_to_reveal: Option<DockPosition>,
4141 window: &mut Window,
4142 cx: &mut Context<Self>,
4143 ) {
4144 // If a center pane is zoomed, unzoom it.
4145 for pane in &self.panes {
4146 if pane != &self.active_pane || dock_to_reveal.is_some() {
4147 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
4148 }
4149 }
4150
4151 // If another dock is zoomed, hide it.
4152 let mut focus_center = false;
4153 for dock in self.all_docks() {
4154 dock.update(cx, |dock, cx| {
4155 if Some(dock.position()) != dock_to_reveal
4156 && let Some(panel) = dock.active_panel()
4157 && panel.is_zoomed(window, cx)
4158 {
4159 focus_center |= panel.panel_focus_handle(cx).contains_focused(window, cx);
4160 dock.set_open(false, window, cx);
4161 }
4162 });
4163 }
4164
4165 if focus_center {
4166 self.active_pane
4167 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
4168 }
4169
4170 if self.zoomed_position != dock_to_reveal {
4171 self.zoomed = None;
4172 self.zoomed_position = None;
4173 cx.emit(Event::ZoomChanged);
4174 }
4175
4176 cx.notify();
4177 }
4178
4179 fn add_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
4180 let pane = cx.new(|cx| {
4181 let mut pane = Pane::new(
4182 self.weak_handle(),
4183 self.project.clone(),
4184 self.pane_history_timestamp.clone(),
4185 None,
4186 NewFile.boxed_clone(),
4187 true,
4188 window,
4189 cx,
4190 );
4191 pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
4192 pane
4193 });
4194 cx.subscribe_in(&pane, window, Self::handle_pane_event)
4195 .detach();
4196 self.panes.push(pane.clone());
4197
4198 window.focus(&pane.focus_handle(cx), cx);
4199
4200 cx.emit(Event::PaneAdded(pane.clone()));
4201 pane
4202 }
4203
4204 pub fn add_item_to_center(
4205 &mut self,
4206 item: Box<dyn ItemHandle>,
4207 window: &mut Window,
4208 cx: &mut Context<Self>,
4209 ) -> bool {
4210 if let Some(center_pane) = self.last_active_center_pane.clone() {
4211 if let Some(center_pane) = center_pane.upgrade() {
4212 center_pane.update(cx, |pane, cx| {
4213 pane.add_item(item, true, true, None, window, cx)
4214 });
4215 true
4216 } else {
4217 false
4218 }
4219 } else {
4220 false
4221 }
4222 }
4223
4224 pub fn add_item_to_active_pane(
4225 &mut self,
4226 item: Box<dyn ItemHandle>,
4227 destination_index: Option<usize>,
4228 focus_item: bool,
4229 window: &mut Window,
4230 cx: &mut App,
4231 ) {
4232 self.add_item(
4233 self.active_pane.clone(),
4234 item,
4235 destination_index,
4236 false,
4237 focus_item,
4238 window,
4239 cx,
4240 )
4241 }
4242
4243 pub fn add_item(
4244 &mut self,
4245 pane: Entity<Pane>,
4246 item: Box<dyn ItemHandle>,
4247 destination_index: Option<usize>,
4248 activate_pane: bool,
4249 focus_item: bool,
4250 window: &mut Window,
4251 cx: &mut App,
4252 ) {
4253 pane.update(cx, |pane, cx| {
4254 pane.add_item(
4255 item,
4256 activate_pane,
4257 focus_item,
4258 destination_index,
4259 window,
4260 cx,
4261 )
4262 });
4263 }
4264
4265 pub fn split_item(
4266 &mut self,
4267 split_direction: SplitDirection,
4268 item: Box<dyn ItemHandle>,
4269 window: &mut Window,
4270 cx: &mut Context<Self>,
4271 ) {
4272 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, window, cx);
4273 self.add_item(new_pane, item, None, true, true, window, cx);
4274 }
4275
4276 pub fn open_abs_path(
4277 &mut self,
4278 abs_path: PathBuf,
4279 options: OpenOptions,
4280 window: &mut Window,
4281 cx: &mut Context<Self>,
4282 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4283 cx.spawn_in(window, async move |workspace, cx| {
4284 let open_paths_task_result = workspace
4285 .update_in(cx, |workspace, window, cx| {
4286 workspace.open_paths(vec![abs_path.clone()], options, None, window, cx)
4287 })
4288 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
4289 .await;
4290 anyhow::ensure!(
4291 open_paths_task_result.len() == 1,
4292 "open abs path {abs_path:?} task returned incorrect number of results"
4293 );
4294 match open_paths_task_result
4295 .into_iter()
4296 .next()
4297 .expect("ensured single task result")
4298 {
4299 Some(open_result) => {
4300 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
4301 }
4302 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
4303 }
4304 })
4305 }
4306
4307 pub fn split_abs_path(
4308 &mut self,
4309 abs_path: PathBuf,
4310 visible: bool,
4311 window: &mut Window,
4312 cx: &mut Context<Self>,
4313 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4314 let project_path_task =
4315 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
4316 cx.spawn_in(window, async move |this, cx| {
4317 let (_, path) = project_path_task.await?;
4318 this.update_in(cx, |this, window, cx| this.split_path(path, window, cx))?
4319 .await
4320 })
4321 }
4322
4323 pub fn open_path(
4324 &mut self,
4325 path: impl Into<ProjectPath>,
4326 pane: Option<WeakEntity<Pane>>,
4327 focus_item: bool,
4328 window: &mut Window,
4329 cx: &mut App,
4330 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4331 self.open_path_preview(path, pane, focus_item, false, true, window, cx)
4332 }
4333
4334 pub fn open_path_preview(
4335 &mut self,
4336 path: impl Into<ProjectPath>,
4337 pane: Option<WeakEntity<Pane>>,
4338 focus_item: bool,
4339 allow_preview: bool,
4340 activate: bool,
4341 window: &mut Window,
4342 cx: &mut App,
4343 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4344 let pane = pane.unwrap_or_else(|| {
4345 self.last_active_center_pane.clone().unwrap_or_else(|| {
4346 self.panes
4347 .first()
4348 .expect("There must be an active pane")
4349 .downgrade()
4350 })
4351 });
4352
4353 let project_path = path.into();
4354 let task = self.load_path(project_path.clone(), window, cx);
4355 window.spawn(cx, async move |cx| {
4356 let (project_entry_id, build_item) = task.await?;
4357
4358 pane.update_in(cx, |pane, window, cx| {
4359 pane.open_item(
4360 project_entry_id,
4361 project_path,
4362 focus_item,
4363 allow_preview,
4364 activate,
4365 None,
4366 window,
4367 cx,
4368 build_item,
4369 )
4370 })
4371 })
4372 }
4373
4374 pub fn split_path(
4375 &mut self,
4376 path: impl Into<ProjectPath>,
4377 window: &mut Window,
4378 cx: &mut Context<Self>,
4379 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4380 self.split_path_preview(path, false, None, window, cx)
4381 }
4382
4383 pub fn split_path_preview(
4384 &mut self,
4385 path: impl Into<ProjectPath>,
4386 allow_preview: bool,
4387 split_direction: Option<SplitDirection>,
4388 window: &mut Window,
4389 cx: &mut Context<Self>,
4390 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4391 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
4392 self.panes
4393 .first()
4394 .expect("There must be an active pane")
4395 .downgrade()
4396 });
4397
4398 if let Member::Pane(center_pane) = &self.center.root
4399 && center_pane.read(cx).items_len() == 0
4400 {
4401 return self.open_path(path, Some(pane), true, window, cx);
4402 }
4403
4404 let project_path = path.into();
4405 let task = self.load_path(project_path.clone(), window, cx);
4406 cx.spawn_in(window, async move |this, cx| {
4407 let (project_entry_id, build_item) = task.await?;
4408 this.update_in(cx, move |this, window, cx| -> Option<_> {
4409 let pane = pane.upgrade()?;
4410 let new_pane = this.split_pane(
4411 pane,
4412 split_direction.unwrap_or(SplitDirection::Right),
4413 window,
4414 cx,
4415 );
4416 new_pane.update(cx, |new_pane, cx| {
4417 Some(new_pane.open_item(
4418 project_entry_id,
4419 project_path,
4420 true,
4421 allow_preview,
4422 true,
4423 None,
4424 window,
4425 cx,
4426 build_item,
4427 ))
4428 })
4429 })
4430 .map(|option| option.context("pane was dropped"))?
4431 })
4432 }
4433
4434 fn load_path(
4435 &mut self,
4436 path: ProjectPath,
4437 window: &mut Window,
4438 cx: &mut App,
4439 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
4440 let registry = cx.default_global::<ProjectItemRegistry>().clone();
4441 registry.open_path(self.project(), &path, window, cx)
4442 }
4443
4444 pub fn find_project_item<T>(
4445 &self,
4446 pane: &Entity<Pane>,
4447 project_item: &Entity<T::Item>,
4448 cx: &App,
4449 ) -> Option<Entity<T>>
4450 where
4451 T: ProjectItem,
4452 {
4453 use project::ProjectItem as _;
4454 let project_item = project_item.read(cx);
4455 let entry_id = project_item.entry_id(cx);
4456 let project_path = project_item.project_path(cx);
4457
4458 let mut item = None;
4459 if let Some(entry_id) = entry_id {
4460 item = pane.read(cx).item_for_entry(entry_id, cx);
4461 }
4462 if item.is_none()
4463 && let Some(project_path) = project_path
4464 {
4465 item = pane.read(cx).item_for_path(project_path, cx);
4466 }
4467
4468 item.and_then(|item| item.downcast::<T>())
4469 }
4470
4471 pub fn is_project_item_open<T>(
4472 &self,
4473 pane: &Entity<Pane>,
4474 project_item: &Entity<T::Item>,
4475 cx: &App,
4476 ) -> bool
4477 where
4478 T: ProjectItem,
4479 {
4480 self.find_project_item::<T>(pane, project_item, cx)
4481 .is_some()
4482 }
4483
4484 pub fn open_project_item<T>(
4485 &mut self,
4486 pane: Entity<Pane>,
4487 project_item: Entity<T::Item>,
4488 activate_pane: bool,
4489 focus_item: bool,
4490 keep_old_preview: bool,
4491 allow_new_preview: bool,
4492 window: &mut Window,
4493 cx: &mut Context<Self>,
4494 ) -> Entity<T>
4495 where
4496 T: ProjectItem,
4497 {
4498 let old_item_id = pane.read(cx).active_item().map(|item| item.item_id());
4499
4500 if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
4501 if !keep_old_preview
4502 && let Some(old_id) = old_item_id
4503 && old_id != item.item_id()
4504 {
4505 // switching to a different item, so unpreview old active item
4506 pane.update(cx, |pane, _| {
4507 pane.unpreview_item_if_preview(old_id);
4508 });
4509 }
4510
4511 self.activate_item(&item, activate_pane, focus_item, window, cx);
4512 if !allow_new_preview {
4513 pane.update(cx, |pane, _| {
4514 pane.unpreview_item_if_preview(item.item_id());
4515 });
4516 }
4517 return item;
4518 }
4519
4520 let item = pane.update(cx, |pane, cx| {
4521 cx.new(|cx| {
4522 T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx)
4523 })
4524 });
4525 let mut destination_index = None;
4526 pane.update(cx, |pane, cx| {
4527 if !keep_old_preview && let Some(old_id) = old_item_id {
4528 pane.unpreview_item_if_preview(old_id);
4529 }
4530 if allow_new_preview {
4531 destination_index = pane.replace_preview_item_id(item.item_id(), window, cx);
4532 }
4533 });
4534
4535 self.add_item(
4536 pane,
4537 Box::new(item.clone()),
4538 destination_index,
4539 activate_pane,
4540 focus_item,
4541 window,
4542 cx,
4543 );
4544 item
4545 }
4546
4547 pub fn open_shared_screen(
4548 &mut self,
4549 peer_id: PeerId,
4550 window: &mut Window,
4551 cx: &mut Context<Self>,
4552 ) {
4553 if let Some(shared_screen) =
4554 self.shared_screen_for_peer(peer_id, &self.active_pane, window, cx)
4555 {
4556 self.active_pane.update(cx, |pane, cx| {
4557 pane.add_item(Box::new(shared_screen), false, true, None, window, cx)
4558 });
4559 }
4560 }
4561
4562 pub fn activate_item(
4563 &mut self,
4564 item: &dyn ItemHandle,
4565 activate_pane: bool,
4566 focus_item: bool,
4567 window: &mut Window,
4568 cx: &mut App,
4569 ) -> bool {
4570 let result = self.panes.iter().find_map(|pane| {
4571 pane.read(cx)
4572 .index_for_item(item)
4573 .map(|ix| (pane.clone(), ix))
4574 });
4575 if let Some((pane, ix)) = result {
4576 pane.update(cx, |pane, cx| {
4577 pane.activate_item(ix, activate_pane, focus_item, window, cx)
4578 });
4579 true
4580 } else {
4581 false
4582 }
4583 }
4584
4585 fn activate_pane_at_index(
4586 &mut self,
4587 action: &ActivatePane,
4588 window: &mut Window,
4589 cx: &mut Context<Self>,
4590 ) {
4591 let panes = self.center.panes();
4592 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
4593 window.focus(&pane.focus_handle(cx), cx);
4594 } else {
4595 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx)
4596 .detach();
4597 }
4598 }
4599
4600 fn move_item_to_pane_at_index(
4601 &mut self,
4602 action: &MoveItemToPane,
4603 window: &mut Window,
4604 cx: &mut Context<Self>,
4605 ) {
4606 let panes = self.center.panes();
4607 let destination = match panes.get(action.destination) {
4608 Some(&destination) => destination.clone(),
4609 None => {
4610 if !action.clone && self.active_pane.read(cx).items_len() < 2 {
4611 return;
4612 }
4613 let direction = SplitDirection::Right;
4614 let split_off_pane = self
4615 .find_pane_in_direction(direction, cx)
4616 .unwrap_or_else(|| self.active_pane.clone());
4617 let new_pane = self.add_pane(window, cx);
4618 self.center.split(&split_off_pane, &new_pane, direction, cx);
4619 new_pane
4620 }
4621 };
4622
4623 if action.clone {
4624 if self
4625 .active_pane
4626 .read(cx)
4627 .active_item()
4628 .is_some_and(|item| item.can_split(cx))
4629 {
4630 clone_active_item(
4631 self.database_id(),
4632 &self.active_pane,
4633 &destination,
4634 action.focus,
4635 window,
4636 cx,
4637 );
4638 return;
4639 }
4640 }
4641 move_active_item(
4642 &self.active_pane,
4643 &destination,
4644 action.focus,
4645 true,
4646 window,
4647 cx,
4648 )
4649 }
4650
4651 pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) {
4652 let panes = self.center.panes();
4653 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
4654 let next_ix = (ix + 1) % panes.len();
4655 let next_pane = panes[next_ix].clone();
4656 window.focus(&next_pane.focus_handle(cx), cx);
4657 }
4658 }
4659
4660 pub fn activate_previous_pane(&mut self, window: &mut Window, cx: &mut App) {
4661 let panes = self.center.panes();
4662 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
4663 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
4664 let prev_pane = panes[prev_ix].clone();
4665 window.focus(&prev_pane.focus_handle(cx), cx);
4666 }
4667 }
4668
4669 pub fn activate_last_pane(&mut self, window: &mut Window, cx: &mut App) {
4670 let last_pane = self.center.last_pane();
4671 window.focus(&last_pane.focus_handle(cx), cx);
4672 }
4673
4674 pub fn activate_pane_in_direction(
4675 &mut self,
4676 direction: SplitDirection,
4677 window: &mut Window,
4678 cx: &mut App,
4679 ) {
4680 use ActivateInDirectionTarget as Target;
4681 enum Origin {
4682 Sidebar,
4683 LeftDock,
4684 RightDock,
4685 BottomDock,
4686 Center,
4687 }
4688
4689 let origin: Origin = if self
4690 .sidebar_focus_handle
4691 .as_ref()
4692 .is_some_and(|h| h.contains_focused(window, cx))
4693 {
4694 Origin::Sidebar
4695 } else {
4696 [
4697 (&self.left_dock, Origin::LeftDock),
4698 (&self.right_dock, Origin::RightDock),
4699 (&self.bottom_dock, Origin::BottomDock),
4700 ]
4701 .into_iter()
4702 .find_map(|(dock, origin)| {
4703 if dock.focus_handle(cx).contains_focused(window, cx) && dock.read(cx).is_open() {
4704 Some(origin)
4705 } else {
4706 None
4707 }
4708 })
4709 .unwrap_or(Origin::Center)
4710 };
4711
4712 let get_last_active_pane = || {
4713 let pane = self
4714 .last_active_center_pane
4715 .clone()
4716 .unwrap_or_else(|| {
4717 self.panes
4718 .first()
4719 .expect("There must be an active pane")
4720 .downgrade()
4721 })
4722 .upgrade()?;
4723 (pane.read(cx).items_len() != 0).then_some(pane)
4724 };
4725
4726 let try_dock =
4727 |dock: &Entity<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
4728
4729 let sidebar_target = self
4730 .sidebar_focus_handle
4731 .as_ref()
4732 .map(|h| Target::Sidebar(h.clone()));
4733
4734 let target = match (origin, direction) {
4735 // From the sidebar, only Right navigates into the workspace.
4736 (Origin::Sidebar, SplitDirection::Right) => try_dock(&self.left_dock)
4737 .or_else(|| get_last_active_pane().map(Target::Pane))
4738 .or_else(|| try_dock(&self.bottom_dock))
4739 .or_else(|| try_dock(&self.right_dock)),
4740
4741 (Origin::Sidebar, _) => None,
4742
4743 // We're in the center, so we first try to go to a different pane,
4744 // otherwise try to go to a dock.
4745 (Origin::Center, direction) => {
4746 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
4747 Some(Target::Pane(pane))
4748 } else {
4749 match direction {
4750 SplitDirection::Up => None,
4751 SplitDirection::Down => try_dock(&self.bottom_dock),
4752 SplitDirection::Left => try_dock(&self.left_dock).or(sidebar_target),
4753 SplitDirection::Right => try_dock(&self.right_dock),
4754 }
4755 }
4756 }
4757
4758 (Origin::LeftDock, SplitDirection::Right) => {
4759 if let Some(last_active_pane) = get_last_active_pane() {
4760 Some(Target::Pane(last_active_pane))
4761 } else {
4762 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
4763 }
4764 }
4765
4766 (Origin::LeftDock, SplitDirection::Left) => sidebar_target,
4767
4768 (Origin::LeftDock, SplitDirection::Down)
4769 | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
4770
4771 (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
4772 (Origin::BottomDock, SplitDirection::Left) => {
4773 try_dock(&self.left_dock).or(sidebar_target)
4774 }
4775 (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
4776
4777 (Origin::RightDock, SplitDirection::Left) => {
4778 if let Some(last_active_pane) = get_last_active_pane() {
4779 Some(Target::Pane(last_active_pane))
4780 } else {
4781 try_dock(&self.bottom_dock)
4782 .or_else(|| try_dock(&self.left_dock))
4783 .or(sidebar_target)
4784 }
4785 }
4786
4787 _ => None,
4788 };
4789
4790 match target {
4791 Some(ActivateInDirectionTarget::Pane(pane)) => {
4792 let pane = pane.read(cx);
4793 if let Some(item) = pane.active_item() {
4794 item.item_focus_handle(cx).focus(window, cx);
4795 } else {
4796 log::error!(
4797 "Could not find a focus target when in switching focus in {direction} direction for a pane",
4798 );
4799 }
4800 }
4801 Some(ActivateInDirectionTarget::Dock(dock)) => {
4802 // Defer this to avoid a panic when the dock's active panel is already on the stack.
4803 window.defer(cx, move |window, cx| {
4804 let dock = dock.read(cx);
4805 if let Some(panel) = dock.active_panel() {
4806 panel.panel_focus_handle(cx).focus(window, cx);
4807 } else {
4808 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
4809 }
4810 })
4811 }
4812 Some(ActivateInDirectionTarget::Sidebar(focus_handle)) => {
4813 focus_handle.focus(window, cx);
4814 }
4815 None => {}
4816 }
4817 }
4818
4819 pub fn move_item_to_pane_in_direction(
4820 &mut self,
4821 action: &MoveItemToPaneInDirection,
4822 window: &mut Window,
4823 cx: &mut Context<Self>,
4824 ) {
4825 let destination = match self.find_pane_in_direction(action.direction, cx) {
4826 Some(destination) => destination,
4827 None => {
4828 if !action.clone && self.active_pane.read(cx).items_len() < 2 {
4829 return;
4830 }
4831 let new_pane = self.add_pane(window, cx);
4832 self.center
4833 .split(&self.active_pane, &new_pane, action.direction, cx);
4834 new_pane
4835 }
4836 };
4837
4838 if action.clone {
4839 if self
4840 .active_pane
4841 .read(cx)
4842 .active_item()
4843 .is_some_and(|item| item.can_split(cx))
4844 {
4845 clone_active_item(
4846 self.database_id(),
4847 &self.active_pane,
4848 &destination,
4849 action.focus,
4850 window,
4851 cx,
4852 );
4853 return;
4854 }
4855 }
4856 move_active_item(
4857 &self.active_pane,
4858 &destination,
4859 action.focus,
4860 true,
4861 window,
4862 cx,
4863 );
4864 }
4865
4866 pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
4867 self.center.bounding_box_for_pane(pane)
4868 }
4869
4870 pub fn find_pane_in_direction(
4871 &mut self,
4872 direction: SplitDirection,
4873 cx: &App,
4874 ) -> Option<Entity<Pane>> {
4875 self.center
4876 .find_pane_in_direction(&self.active_pane, direction, cx)
4877 .cloned()
4878 }
4879
4880 pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
4881 if let Some(to) = self.find_pane_in_direction(direction, cx) {
4882 self.center.swap(&self.active_pane, &to, cx);
4883 cx.notify();
4884 }
4885 }
4886
4887 pub fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
4888 if self
4889 .center
4890 .move_to_border(&self.active_pane, direction, cx)
4891 .unwrap()
4892 {
4893 cx.notify();
4894 }
4895 }
4896
4897 pub fn resize_pane(
4898 &mut self,
4899 axis: gpui::Axis,
4900 amount: Pixels,
4901 window: &mut Window,
4902 cx: &mut Context<Self>,
4903 ) {
4904 let docks = self.all_docks();
4905 let active_dock = docks
4906 .into_iter()
4907 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
4908
4909 if let Some(dock_entity) = active_dock {
4910 let dock = dock_entity.read(cx);
4911 let Some(panel_size) = self.dock_size(&dock, window, cx) else {
4912 return;
4913 };
4914 match dock.position() {
4915 DockPosition::Left => self.resize_left_dock(panel_size + amount, window, cx),
4916 DockPosition::Bottom => self.resize_bottom_dock(panel_size + amount, window, cx),
4917 DockPosition::Right => self.resize_right_dock(panel_size + amount, window, cx),
4918 }
4919 } else {
4920 self.center
4921 .resize(&self.active_pane, axis, amount, &self.bounds, cx);
4922 }
4923 cx.notify();
4924 }
4925
4926 pub fn reset_pane_sizes(&mut self, cx: &mut Context<Self>) {
4927 self.center.reset_pane_sizes(cx);
4928 cx.notify();
4929 }
4930
4931 fn handle_pane_focused(
4932 &mut self,
4933 pane: Entity<Pane>,
4934 window: &mut Window,
4935 cx: &mut Context<Self>,
4936 ) {
4937 // This is explicitly hoisted out of the following check for pane identity as
4938 // terminal panel panes are not registered as a center panes.
4939 self.status_bar.update(cx, |status_bar, cx| {
4940 status_bar.set_active_pane(&pane, window, cx);
4941 });
4942 if self.active_pane != pane {
4943 self.set_active_pane(&pane, window, cx);
4944 }
4945
4946 if self.last_active_center_pane.is_none() {
4947 self.last_active_center_pane = Some(pane.downgrade());
4948 }
4949
4950 // If this pane is in a dock, preserve that dock when dismissing zoomed items.
4951 // This prevents the dock from closing when focus events fire during window activation.
4952 // We also preserve any dock whose active panel itself has focus — this covers
4953 // panels like AgentPanel that don't implement `pane()` but can still be zoomed.
4954 let dock_to_preserve = self.all_docks().iter().find_map(|dock| {
4955 let dock_read = dock.read(cx);
4956 if let Some(panel) = dock_read.active_panel() {
4957 if panel.pane(cx).is_some_and(|dock_pane| dock_pane == pane)
4958 || panel.panel_focus_handle(cx).contains_focused(window, cx)
4959 {
4960 return Some(dock_read.position());
4961 }
4962 }
4963 None
4964 });
4965
4966 self.dismiss_zoomed_items_to_reveal(dock_to_preserve, window, cx);
4967 if pane.read(cx).is_zoomed() {
4968 self.zoomed = Some(pane.downgrade().into());
4969 } else {
4970 self.zoomed = None;
4971 }
4972 self.zoomed_position = None;
4973 cx.emit(Event::ZoomChanged);
4974 self.update_active_view_for_followers(window, cx);
4975 pane.update(cx, |pane, _| {
4976 pane.track_alternate_file_items();
4977 });
4978
4979 cx.notify();
4980 }
4981
4982 fn set_active_pane(
4983 &mut self,
4984 pane: &Entity<Pane>,
4985 window: &mut Window,
4986 cx: &mut Context<Self>,
4987 ) {
4988 self.active_pane = pane.clone();
4989 self.active_item_path_changed(true, window, cx);
4990 self.last_active_center_pane = Some(pane.downgrade());
4991 }
4992
4993 fn handle_panel_focused(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4994 self.update_active_view_for_followers(window, cx);
4995 }
4996
4997 fn handle_pane_event(
4998 &mut self,
4999 pane: &Entity<Pane>,
5000 event: &pane::Event,
5001 window: &mut Window,
5002 cx: &mut Context<Self>,
5003 ) {
5004 let mut serialize_workspace = true;
5005 match event {
5006 pane::Event::AddItem { item } => {
5007 item.added_to_pane(self, pane.clone(), window, cx);
5008 cx.emit(Event::ItemAdded {
5009 item: item.boxed_clone(),
5010 });
5011 }
5012 pane::Event::Split { direction, mode } => {
5013 match mode {
5014 SplitMode::ClonePane => {
5015 self.split_and_clone(pane.clone(), *direction, window, cx)
5016 .detach();
5017 }
5018 SplitMode::EmptyPane => {
5019 self.split_pane(pane.clone(), *direction, window, cx);
5020 }
5021 SplitMode::MovePane => {
5022 self.split_and_move(pane.clone(), *direction, window, cx);
5023 }
5024 };
5025 }
5026 pane::Event::JoinIntoNext => {
5027 self.join_pane_into_next(pane.clone(), window, cx);
5028 }
5029 pane::Event::JoinAll => {
5030 self.join_all_panes(window, cx);
5031 }
5032 pane::Event::Remove { focus_on_pane } => {
5033 self.remove_pane(pane.clone(), focus_on_pane.clone(), window, cx);
5034 }
5035 pane::Event::ActivateItem {
5036 local,
5037 focus_changed,
5038 } => {
5039 window.invalidate_character_coordinates();
5040
5041 pane.update(cx, |pane, _| {
5042 pane.track_alternate_file_items();
5043 });
5044 if *local {
5045 self.unfollow_in_pane(pane, window, cx);
5046 }
5047 serialize_workspace = *focus_changed || pane != self.active_pane();
5048 if pane == self.active_pane() {
5049 self.active_item_path_changed(*focus_changed, window, cx);
5050 self.update_active_view_for_followers(window, cx);
5051 } else if *local {
5052 self.set_active_pane(pane, window, cx);
5053 }
5054 }
5055 pane::Event::UserSavedItem { item, save_intent } => {
5056 cx.emit(Event::UserSavedItem {
5057 pane: pane.downgrade(),
5058 item: item.boxed_clone(),
5059 save_intent: *save_intent,
5060 });
5061 serialize_workspace = false;
5062 }
5063 pane::Event::ChangeItemTitle => {
5064 if *pane == self.active_pane {
5065 self.active_item_path_changed(false, window, cx);
5066 }
5067 serialize_workspace = false;
5068 }
5069 pane::Event::RemovedItem { item } => {
5070 cx.emit(Event::ActiveItemChanged);
5071 self.update_window_edited(window, cx);
5072 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id())
5073 && entry.get().entity_id() == pane.entity_id()
5074 {
5075 entry.remove();
5076 }
5077 cx.emit(Event::ItemRemoved {
5078 item_id: item.item_id(),
5079 });
5080 }
5081 pane::Event::Focus => {
5082 window.invalidate_character_coordinates();
5083 self.handle_pane_focused(pane.clone(), window, cx);
5084 }
5085 pane::Event::ZoomIn => {
5086 if *pane == self.active_pane {
5087 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
5088 if pane.read(cx).has_focus(window, cx) {
5089 self.zoomed = Some(pane.downgrade().into());
5090 self.zoomed_position = None;
5091 cx.emit(Event::ZoomChanged);
5092 }
5093 cx.notify();
5094 }
5095 }
5096 pane::Event::ZoomOut => {
5097 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
5098 if self.zoomed_position.is_none() {
5099 self.zoomed = None;
5100 cx.emit(Event::ZoomChanged);
5101 }
5102 cx.notify();
5103 }
5104 pane::Event::ItemPinned | pane::Event::ItemUnpinned => {}
5105 }
5106
5107 if serialize_workspace {
5108 self.serialize_workspace(window, cx);
5109 }
5110 }
5111
5112 pub fn unfollow_in_pane(
5113 &mut self,
5114 pane: &Entity<Pane>,
5115 window: &mut Window,
5116 cx: &mut Context<Workspace>,
5117 ) -> Option<CollaboratorId> {
5118 let leader_id = self.leader_for_pane(pane)?;
5119 self.unfollow(leader_id, window, cx);
5120 Some(leader_id)
5121 }
5122
5123 pub fn split_pane(
5124 &mut self,
5125 pane_to_split: Entity<Pane>,
5126 split_direction: SplitDirection,
5127 window: &mut Window,
5128 cx: &mut Context<Self>,
5129 ) -> Entity<Pane> {
5130 let new_pane = self.add_pane(window, cx);
5131 self.center
5132 .split(&pane_to_split, &new_pane, split_direction, cx);
5133 cx.notify();
5134 new_pane
5135 }
5136
5137 pub fn split_and_move(
5138 &mut self,
5139 pane: Entity<Pane>,
5140 direction: SplitDirection,
5141 window: &mut Window,
5142 cx: &mut Context<Self>,
5143 ) {
5144 let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx)) else {
5145 return;
5146 };
5147 let new_pane = self.add_pane(window, cx);
5148 new_pane.update(cx, |pane, cx| {
5149 pane.add_item(item, true, true, None, window, cx)
5150 });
5151 self.center.split(&pane, &new_pane, direction, cx);
5152 cx.notify();
5153 }
5154
5155 pub fn split_and_clone(
5156 &mut self,
5157 pane: Entity<Pane>,
5158 direction: SplitDirection,
5159 window: &mut Window,
5160 cx: &mut Context<Self>,
5161 ) -> Task<Option<Entity<Pane>>> {
5162 let Some(item) = pane.read(cx).active_item() else {
5163 return Task::ready(None);
5164 };
5165 if !item.can_split(cx) {
5166 return Task::ready(None);
5167 }
5168 let task = item.clone_on_split(self.database_id(), window, cx);
5169 cx.spawn_in(window, async move |this, cx| {
5170 if let Some(clone) = task.await {
5171 this.update_in(cx, |this, window, cx| {
5172 let new_pane = this.add_pane(window, cx);
5173 let nav_history = pane.read(cx).fork_nav_history();
5174 new_pane.update(cx, |pane, cx| {
5175 pane.set_nav_history(nav_history, cx);
5176 pane.add_item(clone, true, true, None, window, cx)
5177 });
5178 this.center.split(&pane, &new_pane, direction, cx);
5179 cx.notify();
5180 new_pane
5181 })
5182 .ok()
5183 } else {
5184 None
5185 }
5186 })
5187 }
5188
5189 pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5190 let active_item = self.active_pane.read(cx).active_item();
5191 for pane in &self.panes {
5192 join_pane_into_active(&self.active_pane, pane, window, cx);
5193 }
5194 if let Some(active_item) = active_item {
5195 self.activate_item(active_item.as_ref(), true, true, window, cx);
5196 }
5197 cx.notify();
5198 }
5199
5200 pub fn join_pane_into_next(
5201 &mut self,
5202 pane: Entity<Pane>,
5203 window: &mut Window,
5204 cx: &mut Context<Self>,
5205 ) {
5206 let next_pane = self
5207 .find_pane_in_direction(SplitDirection::Right, cx)
5208 .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
5209 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
5210 .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
5211 let Some(next_pane) = next_pane else {
5212 return;
5213 };
5214 move_all_items(&pane, &next_pane, window, cx);
5215 cx.notify();
5216 }
5217
5218 fn remove_pane(
5219 &mut self,
5220 pane: Entity<Pane>,
5221 focus_on: Option<Entity<Pane>>,
5222 window: &mut Window,
5223 cx: &mut Context<Self>,
5224 ) {
5225 if self.center.remove(&pane, cx).unwrap() {
5226 self.force_remove_pane(&pane, &focus_on, window, cx);
5227 self.unfollow_in_pane(&pane, window, cx);
5228 self.last_leaders_by_pane.remove(&pane.downgrade());
5229 for removed_item in pane.read(cx).items() {
5230 self.panes_by_item.remove(&removed_item.item_id());
5231 }
5232
5233 cx.notify();
5234 } else {
5235 self.active_item_path_changed(true, window, cx);
5236 }
5237 cx.emit(Event::PaneRemoved);
5238 }
5239
5240 pub fn panes_mut(&mut self) -> &mut [Entity<Pane>] {
5241 &mut self.panes
5242 }
5243
5244 pub fn panes(&self) -> &[Entity<Pane>] {
5245 &self.panes
5246 }
5247
5248 pub fn active_pane(&self) -> &Entity<Pane> {
5249 &self.active_pane
5250 }
5251
5252 pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> {
5253 for dock in self.all_docks() {
5254 if dock.focus_handle(cx).contains_focused(window, cx)
5255 && let Some(pane) = dock
5256 .read(cx)
5257 .active_panel()
5258 .and_then(|panel| panel.pane(cx))
5259 {
5260 return pane;
5261 }
5262 }
5263 self.active_pane().clone()
5264 }
5265
5266 pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
5267 self.find_pane_in_direction(SplitDirection::Right, cx)
5268 .unwrap_or_else(|| {
5269 self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx)
5270 })
5271 }
5272
5273 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> {
5274 self.pane_for_item_id(handle.item_id())
5275 }
5276
5277 pub fn pane_for_item_id(&self, item_id: EntityId) -> Option<Entity<Pane>> {
5278 let weak_pane = self.panes_by_item.get(&item_id)?;
5279 weak_pane.upgrade()
5280 }
5281
5282 pub fn pane_for_entity_id(&self, entity_id: EntityId) -> Option<Entity<Pane>> {
5283 self.panes
5284 .iter()
5285 .find(|pane| pane.entity_id() == entity_id)
5286 .cloned()
5287 }
5288
5289 fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
5290 self.follower_states.retain(|leader_id, state| {
5291 if *leader_id == CollaboratorId::PeerId(peer_id) {
5292 for item in state.items_by_leader_view_id.values() {
5293 item.view.set_leader_id(None, window, cx);
5294 }
5295 false
5296 } else {
5297 true
5298 }
5299 });
5300 cx.notify();
5301 }
5302
5303 pub fn start_following(
5304 &mut self,
5305 leader_id: impl Into<CollaboratorId>,
5306 window: &mut Window,
5307 cx: &mut Context<Self>,
5308 ) -> Option<Task<Result<()>>> {
5309 let leader_id = leader_id.into();
5310 let pane = self.active_pane().clone();
5311
5312 self.last_leaders_by_pane
5313 .insert(pane.downgrade(), leader_id);
5314 self.unfollow(leader_id, window, cx);
5315 self.unfollow_in_pane(&pane, window, cx);
5316 self.follower_states.insert(
5317 leader_id,
5318 FollowerState {
5319 center_pane: pane.clone(),
5320 dock_pane: None,
5321 active_view_id: None,
5322 items_by_leader_view_id: Default::default(),
5323 },
5324 );
5325 cx.notify();
5326
5327 match leader_id {
5328 CollaboratorId::PeerId(leader_peer_id) => {
5329 let room_id = self.active_call()?.room_id(cx)?;
5330 let project_id = self.project.read(cx).remote_id();
5331 let request = self.app_state.client.request(proto::Follow {
5332 room_id,
5333 project_id,
5334 leader_id: Some(leader_peer_id),
5335 });
5336
5337 Some(cx.spawn_in(window, async move |this, cx| {
5338 let response = request.await?;
5339 this.update(cx, |this, _| {
5340 let state = this
5341 .follower_states
5342 .get_mut(&leader_id)
5343 .context("following interrupted")?;
5344 state.active_view_id = response
5345 .active_view
5346 .as_ref()
5347 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
5348 anyhow::Ok(())
5349 })??;
5350 if let Some(view) = response.active_view {
5351 Self::add_view_from_leader(this.clone(), leader_peer_id, &view, cx).await?;
5352 }
5353 this.update_in(cx, |this, window, cx| {
5354 this.leader_updated(leader_id, window, cx)
5355 })?;
5356 Ok(())
5357 }))
5358 }
5359 CollaboratorId::Agent => {
5360 self.leader_updated(leader_id, window, cx)?;
5361 Some(Task::ready(Ok(())))
5362 }
5363 }
5364 }
5365
5366 pub fn follow_next_collaborator(
5367 &mut self,
5368 _: &FollowNextCollaborator,
5369 window: &mut Window,
5370 cx: &mut Context<Self>,
5371 ) {
5372 let collaborators = self.project.read(cx).collaborators();
5373 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
5374 let mut collaborators = collaborators.keys().copied();
5375 for peer_id in collaborators.by_ref() {
5376 if CollaboratorId::PeerId(peer_id) == leader_id {
5377 break;
5378 }
5379 }
5380 collaborators.next().map(CollaboratorId::PeerId)
5381 } else if let Some(last_leader_id) =
5382 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
5383 {
5384 match last_leader_id {
5385 CollaboratorId::PeerId(peer_id) => {
5386 if collaborators.contains_key(peer_id) {
5387 Some(*last_leader_id)
5388 } else {
5389 None
5390 }
5391 }
5392 CollaboratorId::Agent => Some(CollaboratorId::Agent),
5393 }
5394 } else {
5395 None
5396 };
5397
5398 let pane = self.active_pane.clone();
5399 let Some(leader_id) = next_leader_id.or_else(|| {
5400 Some(CollaboratorId::PeerId(
5401 collaborators.keys().copied().next()?,
5402 ))
5403 }) else {
5404 return;
5405 };
5406 if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
5407 return;
5408 }
5409 if let Some(task) = self.start_following(leader_id, window, cx) {
5410 task.detach_and_log_err(cx)
5411 }
5412 }
5413
5414 pub fn follow(
5415 &mut self,
5416 leader_id: impl Into<CollaboratorId>,
5417 window: &mut Window,
5418 cx: &mut Context<Self>,
5419 ) {
5420 let leader_id = leader_id.into();
5421
5422 if let CollaboratorId::PeerId(peer_id) = leader_id {
5423 let Some(active_call) = GlobalAnyActiveCall::try_global(cx) else {
5424 return;
5425 };
5426 let Some(remote_participant) =
5427 active_call.0.remote_participant_for_peer_id(peer_id, cx)
5428 else {
5429 return;
5430 };
5431
5432 let project = self.project.read(cx);
5433
5434 let other_project_id = match remote_participant.location {
5435 ParticipantLocation::External => None,
5436 ParticipantLocation::UnsharedProject => None,
5437 ParticipantLocation::SharedProject { project_id } => {
5438 if Some(project_id) == project.remote_id() {
5439 None
5440 } else {
5441 Some(project_id)
5442 }
5443 }
5444 };
5445
5446 // if they are active in another project, follow there.
5447 if let Some(project_id) = other_project_id {
5448 let app_state = self.app_state.clone();
5449 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
5450 .detach_and_log_err(cx);
5451 }
5452 }
5453
5454 // if you're already following, find the right pane and focus it.
5455 if let Some(follower_state) = self.follower_states.get(&leader_id) {
5456 window.focus(&follower_state.pane().focus_handle(cx), cx);
5457
5458 return;
5459 }
5460
5461 // Otherwise, follow.
5462 if let Some(task) = self.start_following(leader_id, window, cx) {
5463 task.detach_and_log_err(cx)
5464 }
5465 }
5466
5467 pub fn unfollow(
5468 &mut self,
5469 leader_id: impl Into<CollaboratorId>,
5470 window: &mut Window,
5471 cx: &mut Context<Self>,
5472 ) -> Option<()> {
5473 cx.notify();
5474
5475 let leader_id = leader_id.into();
5476 let state = self.follower_states.remove(&leader_id)?;
5477 for (_, item) in state.items_by_leader_view_id {
5478 item.view.set_leader_id(None, window, cx);
5479 }
5480
5481 if let CollaboratorId::PeerId(leader_peer_id) = leader_id {
5482 let project_id = self.project.read(cx).remote_id();
5483 let room_id = self.active_call()?.room_id(cx)?;
5484 self.app_state
5485 .client
5486 .send(proto::Unfollow {
5487 room_id,
5488 project_id,
5489 leader_id: Some(leader_peer_id),
5490 })
5491 .log_err();
5492 }
5493
5494 Some(())
5495 }
5496
5497 pub fn is_being_followed(&self, id: impl Into<CollaboratorId>) -> bool {
5498 self.follower_states.contains_key(&id.into())
5499 }
5500
5501 fn active_item_path_changed(
5502 &mut self,
5503 focus_changed: bool,
5504 window: &mut Window,
5505 cx: &mut Context<Self>,
5506 ) {
5507 cx.emit(Event::ActiveItemChanged);
5508 let active_entry = self.active_project_path(cx);
5509 self.project.update(cx, |project, cx| {
5510 project.set_active_path(active_entry.clone(), cx)
5511 });
5512
5513 if focus_changed && let Some(project_path) = &active_entry {
5514 let git_store_entity = self.project.read(cx).git_store().clone();
5515 git_store_entity.update(cx, |git_store, cx| {
5516 git_store.set_active_repo_for_path(project_path, cx);
5517 });
5518 }
5519
5520 self.update_window_title(window, cx);
5521 }
5522
5523 fn update_window_title(&mut self, window: &mut Window, cx: &mut App) {
5524 let project = self.project().read(cx);
5525 let mut title = String::new();
5526
5527 for (i, worktree) in project.visible_worktrees(cx).enumerate() {
5528 let name = {
5529 let settings_location = SettingsLocation {
5530 worktree_id: worktree.read(cx).id(),
5531 path: RelPath::empty(),
5532 };
5533
5534 let settings = WorktreeSettings::get(Some(settings_location), cx);
5535 match &settings.project_name {
5536 Some(name) => name.as_str(),
5537 None => worktree.read(cx).root_name_str(),
5538 }
5539 };
5540 if i > 0 {
5541 title.push_str(", ");
5542 }
5543 title.push_str(name);
5544 }
5545
5546 if title.is_empty() {
5547 title = "empty project".to_string();
5548 }
5549
5550 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
5551 let filename = path.path.file_name().or_else(|| {
5552 Some(
5553 project
5554 .worktree_for_id(path.worktree_id, cx)?
5555 .read(cx)
5556 .root_name_str(),
5557 )
5558 });
5559
5560 if let Some(filename) = filename {
5561 title.push_str(" — ");
5562 title.push_str(filename.as_ref());
5563 }
5564 }
5565
5566 if project.is_via_collab() {
5567 title.push_str(" ↙");
5568 } else if project.is_shared() {
5569 title.push_str(" ↗");
5570 }
5571
5572 if let Some(last_title) = self.last_window_title.as_ref()
5573 && &title == last_title
5574 {
5575 return;
5576 }
5577 window.set_window_title(&title);
5578 SystemWindowTabController::update_tab_title(
5579 cx,
5580 window.window_handle().window_id(),
5581 SharedString::from(&title),
5582 );
5583 self.last_window_title = Some(title);
5584 }
5585
5586 fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
5587 let is_edited = !self.project.read(cx).is_disconnected(cx) && !self.dirty_items.is_empty();
5588 if is_edited != self.window_edited {
5589 self.window_edited = is_edited;
5590 window.set_window_edited(self.window_edited)
5591 }
5592 }
5593
5594 fn update_item_dirty_state(
5595 &mut self,
5596 item: &dyn ItemHandle,
5597 window: &mut Window,
5598 cx: &mut App,
5599 ) {
5600 let is_dirty = item.is_dirty(cx);
5601 let item_id = item.item_id();
5602 let was_dirty = self.dirty_items.contains_key(&item_id);
5603 if is_dirty == was_dirty {
5604 return;
5605 }
5606 if was_dirty {
5607 self.dirty_items.remove(&item_id);
5608 self.update_window_edited(window, cx);
5609 return;
5610 }
5611
5612 let workspace = self.weak_handle();
5613 let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
5614 return;
5615 };
5616 let on_release_callback = Box::new(move |cx: &mut App| {
5617 window_handle
5618 .update(cx, |_, window, cx| {
5619 workspace
5620 .update(cx, |workspace, cx| {
5621 workspace.dirty_items.remove(&item_id);
5622 workspace.update_window_edited(window, cx)
5623 })
5624 .ok();
5625 })
5626 .ok();
5627 });
5628
5629 let s = item.on_release(cx, on_release_callback);
5630 self.dirty_items.insert(item_id, s);
5631 self.update_window_edited(window, cx);
5632 }
5633
5634 fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
5635 if self.notifications.is_empty() {
5636 None
5637 } else {
5638 Some(
5639 div()
5640 .absolute()
5641 .right_3()
5642 .bottom_3()
5643 .w_112()
5644 .h_full()
5645 .flex()
5646 .flex_col()
5647 .justify_end()
5648 .gap_2()
5649 .children(
5650 self.notifications
5651 .iter()
5652 .map(|(_, notification)| notification.clone().into_any()),
5653 ),
5654 )
5655 }
5656 }
5657
5658 // RPC handlers
5659
5660 fn active_view_for_follower(
5661 &self,
5662 follower_project_id: Option<u64>,
5663 window: &mut Window,
5664 cx: &mut Context<Self>,
5665 ) -> Option<proto::View> {
5666 let (item, panel_id) = self.active_item_for_followers(window, cx);
5667 let item = item?;
5668 let leader_id = self
5669 .pane_for(&*item)
5670 .and_then(|pane| self.leader_for_pane(&pane));
5671 let leader_peer_id = match leader_id {
5672 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
5673 Some(CollaboratorId::Agent) | None => None,
5674 };
5675
5676 let item_handle = item.to_followable_item_handle(cx)?;
5677 let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
5678 let variant = item_handle.to_state_proto(window, cx)?;
5679
5680 if item_handle.is_project_item(window, cx)
5681 && (follower_project_id.is_none()
5682 || follower_project_id != self.project.read(cx).remote_id())
5683 {
5684 return None;
5685 }
5686
5687 Some(proto::View {
5688 id: id.to_proto(),
5689 leader_id: leader_peer_id,
5690 variant: Some(variant),
5691 panel_id: panel_id.map(|id| id as i32),
5692 })
5693 }
5694
5695 fn handle_follow(
5696 &mut self,
5697 follower_project_id: Option<u64>,
5698 window: &mut Window,
5699 cx: &mut Context<Self>,
5700 ) -> proto::FollowResponse {
5701 let active_view = self.active_view_for_follower(follower_project_id, window, cx);
5702
5703 cx.notify();
5704 proto::FollowResponse {
5705 views: active_view.iter().cloned().collect(),
5706 active_view,
5707 }
5708 }
5709
5710 fn handle_update_followers(
5711 &mut self,
5712 leader_id: PeerId,
5713 message: proto::UpdateFollowers,
5714 _window: &mut Window,
5715 _cx: &mut Context<Self>,
5716 ) {
5717 self.leader_updates_tx
5718 .unbounded_send((leader_id, message))
5719 .ok();
5720 }
5721
5722 async fn process_leader_update(
5723 this: &WeakEntity<Self>,
5724 leader_id: PeerId,
5725 update: proto::UpdateFollowers,
5726 cx: &mut AsyncWindowContext,
5727 ) -> Result<()> {
5728 match update.variant.context("invalid update")? {
5729 proto::update_followers::Variant::CreateView(view) => {
5730 let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
5731 let should_add_view = this.update(cx, |this, _| {
5732 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
5733 anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
5734 } else {
5735 anyhow::Ok(false)
5736 }
5737 })??;
5738
5739 if should_add_view {
5740 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
5741 }
5742 }
5743 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
5744 let should_add_view = this.update(cx, |this, _| {
5745 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
5746 state.active_view_id = update_active_view
5747 .view
5748 .as_ref()
5749 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
5750
5751 if state.active_view_id.is_some_and(|view_id| {
5752 !state.items_by_leader_view_id.contains_key(&view_id)
5753 }) {
5754 anyhow::Ok(true)
5755 } else {
5756 anyhow::Ok(false)
5757 }
5758 } else {
5759 anyhow::Ok(false)
5760 }
5761 })??;
5762
5763 if should_add_view && let Some(view) = update_active_view.view {
5764 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
5765 }
5766 }
5767 proto::update_followers::Variant::UpdateView(update_view) => {
5768 let variant = update_view.variant.context("missing update view variant")?;
5769 let id = update_view.id.context("missing update view id")?;
5770 let mut tasks = Vec::new();
5771 this.update_in(cx, |this, window, cx| {
5772 let project = this.project.clone();
5773 if let Some(state) = this.follower_states.get(&leader_id.into()) {
5774 let view_id = ViewId::from_proto(id.clone())?;
5775 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
5776 tasks.push(item.view.apply_update_proto(
5777 &project,
5778 variant.clone(),
5779 window,
5780 cx,
5781 ));
5782 }
5783 }
5784 anyhow::Ok(())
5785 })??;
5786 try_join_all(tasks).await.log_err();
5787 }
5788 }
5789 this.update_in(cx, |this, window, cx| {
5790 this.leader_updated(leader_id, window, cx)
5791 })?;
5792 Ok(())
5793 }
5794
5795 async fn add_view_from_leader(
5796 this: WeakEntity<Self>,
5797 leader_id: PeerId,
5798 view: &proto::View,
5799 cx: &mut AsyncWindowContext,
5800 ) -> Result<()> {
5801 let this = this.upgrade().context("workspace dropped")?;
5802
5803 let Some(id) = view.id.clone() else {
5804 anyhow::bail!("no id for view");
5805 };
5806 let id = ViewId::from_proto(id)?;
5807 let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
5808
5809 let pane = this.update(cx, |this, _cx| {
5810 let state = this
5811 .follower_states
5812 .get(&leader_id.into())
5813 .context("stopped following")?;
5814 anyhow::Ok(state.pane().clone())
5815 })?;
5816 let existing_item = pane.update_in(cx, |pane, window, cx| {
5817 let client = this.read(cx).client().clone();
5818 pane.items().find_map(|item| {
5819 let item = item.to_followable_item_handle(cx)?;
5820 if item.remote_id(&client, window, cx) == Some(id) {
5821 Some(item)
5822 } else {
5823 None
5824 }
5825 })
5826 })?;
5827 let item = if let Some(existing_item) = existing_item {
5828 existing_item
5829 } else {
5830 let variant = view.variant.clone();
5831 anyhow::ensure!(variant.is_some(), "missing view variant");
5832
5833 let task = cx.update(|window, cx| {
5834 FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx)
5835 })?;
5836
5837 let Some(task) = task else {
5838 anyhow::bail!(
5839 "failed to construct view from leader (maybe from a different version of zed?)"
5840 );
5841 };
5842
5843 let mut new_item = task.await?;
5844 pane.update_in(cx, |pane, window, cx| {
5845 let mut item_to_remove = None;
5846 for (ix, item) in pane.items().enumerate() {
5847 if let Some(item) = item.to_followable_item_handle(cx) {
5848 match new_item.dedup(item.as_ref(), window, cx) {
5849 Some(item::Dedup::KeepExisting) => {
5850 new_item =
5851 item.boxed_clone().to_followable_item_handle(cx).unwrap();
5852 break;
5853 }
5854 Some(item::Dedup::ReplaceExisting) => {
5855 item_to_remove = Some((ix, item.item_id()));
5856 break;
5857 }
5858 None => {}
5859 }
5860 }
5861 }
5862
5863 if let Some((ix, id)) = item_to_remove {
5864 pane.remove_item(id, false, false, window, cx);
5865 pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx);
5866 }
5867 })?;
5868
5869 new_item
5870 };
5871
5872 this.update_in(cx, |this, window, cx| {
5873 let state = this.follower_states.get_mut(&leader_id.into())?;
5874 item.set_leader_id(Some(leader_id.into()), window, cx);
5875 state.items_by_leader_view_id.insert(
5876 id,
5877 FollowerView {
5878 view: item,
5879 location: panel_id,
5880 },
5881 );
5882
5883 Some(())
5884 })
5885 .context("no follower state")?;
5886
5887 Ok(())
5888 }
5889
5890 fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5891 let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else {
5892 return;
5893 };
5894
5895 if let Some(agent_location) = self.project.read(cx).agent_location() {
5896 let buffer_entity_id = agent_location.buffer.entity_id();
5897 let view_id = ViewId {
5898 creator: CollaboratorId::Agent,
5899 id: buffer_entity_id.as_u64(),
5900 };
5901 follower_state.active_view_id = Some(view_id);
5902
5903 let item = match follower_state.items_by_leader_view_id.entry(view_id) {
5904 hash_map::Entry::Occupied(entry) => Some(entry.into_mut()),
5905 hash_map::Entry::Vacant(entry) => {
5906 let existing_view =
5907 follower_state
5908 .center_pane
5909 .read(cx)
5910 .items()
5911 .find_map(|item| {
5912 let item = item.to_followable_item_handle(cx)?;
5913 if item.buffer_kind(cx) == ItemBufferKind::Singleton
5914 && item.project_item_model_ids(cx).as_slice()
5915 == [buffer_entity_id]
5916 {
5917 Some(item)
5918 } else {
5919 None
5920 }
5921 });
5922 let view = existing_view.or_else(|| {
5923 agent_location.buffer.upgrade().and_then(|buffer| {
5924 cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| {
5925 registry.build_item(buffer, self.project.clone(), None, window, cx)
5926 })?
5927 .to_followable_item_handle(cx)
5928 })
5929 });
5930
5931 view.map(|view| {
5932 entry.insert(FollowerView {
5933 view,
5934 location: None,
5935 })
5936 })
5937 }
5938 };
5939
5940 if let Some(item) = item {
5941 item.view
5942 .set_leader_id(Some(CollaboratorId::Agent), window, cx);
5943 item.view
5944 .update_agent_location(agent_location.position, window, cx);
5945 }
5946 } else {
5947 follower_state.active_view_id = None;
5948 }
5949
5950 self.leader_updated(CollaboratorId::Agent, window, cx);
5951 }
5952
5953 pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
5954 let mut is_project_item = true;
5955 let mut update = proto::UpdateActiveView::default();
5956 if window.is_window_active() {
5957 let (active_item, panel_id) = self.active_item_for_followers(window, cx);
5958
5959 if let Some(item) = active_item
5960 && item.item_focus_handle(cx).contains_focused(window, cx)
5961 {
5962 let leader_id = self
5963 .pane_for(&*item)
5964 .and_then(|pane| self.leader_for_pane(&pane));
5965 let leader_peer_id = match leader_id {
5966 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
5967 Some(CollaboratorId::Agent) | None => None,
5968 };
5969
5970 if let Some(item) = item.to_followable_item_handle(cx) {
5971 let id = item
5972 .remote_id(&self.app_state.client, window, cx)
5973 .map(|id| id.to_proto());
5974
5975 if let Some(id) = id
5976 && let Some(variant) = item.to_state_proto(window, cx)
5977 {
5978 let view = Some(proto::View {
5979 id,
5980 leader_id: leader_peer_id,
5981 variant: Some(variant),
5982 panel_id: panel_id.map(|id| id as i32),
5983 });
5984
5985 is_project_item = item.is_project_item(window, cx);
5986 update = proto::UpdateActiveView { view };
5987 };
5988 }
5989 }
5990 }
5991
5992 let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
5993 if active_view_id != self.last_active_view_id.as_ref() {
5994 self.last_active_view_id = active_view_id.cloned();
5995 self.update_followers(
5996 is_project_item,
5997 proto::update_followers::Variant::UpdateActiveView(update),
5998 window,
5999 cx,
6000 );
6001 }
6002 }
6003
6004 fn active_item_for_followers(
6005 &self,
6006 window: &mut Window,
6007 cx: &mut App,
6008 ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
6009 let mut active_item = None;
6010 let mut panel_id = None;
6011 for dock in self.all_docks() {
6012 if dock.focus_handle(cx).contains_focused(window, cx)
6013 && let Some(panel) = dock.read(cx).active_panel()
6014 && let Some(pane) = panel.pane(cx)
6015 && let Some(item) = pane.read(cx).active_item()
6016 {
6017 active_item = Some(item);
6018 panel_id = panel.remote_id();
6019 break;
6020 }
6021 }
6022
6023 if active_item.is_none() {
6024 active_item = self.active_pane().read(cx).active_item();
6025 }
6026 (active_item, panel_id)
6027 }
6028
6029 fn update_followers(
6030 &self,
6031 project_only: bool,
6032 update: proto::update_followers::Variant,
6033 _: &mut Window,
6034 cx: &mut App,
6035 ) -> Option<()> {
6036 // If this update only applies to for followers in the current project,
6037 // then skip it unless this project is shared. If it applies to all
6038 // followers, regardless of project, then set `project_id` to none,
6039 // indicating that it goes to all followers.
6040 let project_id = if project_only {
6041 Some(self.project.read(cx).remote_id()?)
6042 } else {
6043 None
6044 };
6045 self.app_state().workspace_store.update(cx, |store, cx| {
6046 store.update_followers(project_id, update, cx)
6047 })
6048 }
6049
6050 pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<CollaboratorId> {
6051 self.follower_states.iter().find_map(|(leader_id, state)| {
6052 if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
6053 Some(*leader_id)
6054 } else {
6055 None
6056 }
6057 })
6058 }
6059
6060 fn leader_updated(
6061 &mut self,
6062 leader_id: impl Into<CollaboratorId>,
6063 window: &mut Window,
6064 cx: &mut Context<Self>,
6065 ) -> Option<Box<dyn ItemHandle>> {
6066 cx.notify();
6067
6068 let leader_id = leader_id.into();
6069 let (panel_id, item) = match leader_id {
6070 CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?,
6071 CollaboratorId::Agent => (None, self.active_item_for_agent()?),
6072 };
6073
6074 let state = self.follower_states.get(&leader_id)?;
6075 let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
6076 let pane;
6077 if let Some(panel_id) = panel_id {
6078 pane = self
6079 .activate_panel_for_proto_id(panel_id, window, cx)?
6080 .pane(cx)?;
6081 let state = self.follower_states.get_mut(&leader_id)?;
6082 state.dock_pane = Some(pane.clone());
6083 } else {
6084 pane = state.center_pane.clone();
6085 let state = self.follower_states.get_mut(&leader_id)?;
6086 if let Some(dock_pane) = state.dock_pane.take() {
6087 transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx);
6088 }
6089 }
6090
6091 pane.update(cx, |pane, cx| {
6092 let focus_active_item = pane.has_focus(window, cx) || transfer_focus;
6093 if let Some(index) = pane.index_for_item(item.as_ref()) {
6094 pane.activate_item(index, false, false, window, cx);
6095 } else {
6096 pane.add_item(item.boxed_clone(), false, false, None, window, cx)
6097 }
6098
6099 if focus_active_item {
6100 pane.focus_active_item(window, cx)
6101 }
6102 });
6103
6104 Some(item)
6105 }
6106
6107 fn active_item_for_agent(&self) -> Option<Box<dyn ItemHandle>> {
6108 let state = self.follower_states.get(&CollaboratorId::Agent)?;
6109 let active_view_id = state.active_view_id?;
6110 Some(
6111 state
6112 .items_by_leader_view_id
6113 .get(&active_view_id)?
6114 .view
6115 .boxed_clone(),
6116 )
6117 }
6118
6119 fn active_item_for_peer(
6120 &self,
6121 peer_id: PeerId,
6122 window: &mut Window,
6123 cx: &mut Context<Self>,
6124 ) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
6125 let call = self.active_call()?;
6126 let participant = call.remote_participant_for_peer_id(peer_id, cx)?;
6127 let leader_in_this_app;
6128 let leader_in_this_project;
6129 match participant.location {
6130 ParticipantLocation::SharedProject { project_id } => {
6131 leader_in_this_app = true;
6132 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
6133 }
6134 ParticipantLocation::UnsharedProject => {
6135 leader_in_this_app = true;
6136 leader_in_this_project = false;
6137 }
6138 ParticipantLocation::External => {
6139 leader_in_this_app = false;
6140 leader_in_this_project = false;
6141 }
6142 };
6143 let state = self.follower_states.get(&peer_id.into())?;
6144 let mut item_to_activate = None;
6145 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
6146 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id)
6147 && (leader_in_this_project || !item.view.is_project_item(window, cx))
6148 {
6149 item_to_activate = Some((item.location, item.view.boxed_clone()));
6150 }
6151 } else if let Some(shared_screen) =
6152 self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx)
6153 {
6154 item_to_activate = Some((None, Box::new(shared_screen)));
6155 }
6156 item_to_activate
6157 }
6158
6159 fn shared_screen_for_peer(
6160 &self,
6161 peer_id: PeerId,
6162 pane: &Entity<Pane>,
6163 window: &mut Window,
6164 cx: &mut App,
6165 ) -> Option<Entity<SharedScreen>> {
6166 self.active_call()?
6167 .create_shared_screen(peer_id, pane, window, cx)
6168 }
6169
6170 pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
6171 if window.is_window_active() {
6172 self.update_active_view_for_followers(window, cx);
6173
6174 if let Some(database_id) = self.database_id {
6175 let db = WorkspaceDb::global(cx);
6176 cx.background_spawn(async move { db.update_timestamp(database_id).await })
6177 .detach();
6178 }
6179 } else {
6180 for pane in &self.panes {
6181 pane.update(cx, |pane, cx| {
6182 if let Some(item) = pane.active_item() {
6183 item.workspace_deactivated(window, cx);
6184 }
6185 for item in pane.items() {
6186 if matches!(
6187 item.workspace_settings(cx).autosave,
6188 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
6189 ) {
6190 Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
6191 .detach_and_log_err(cx);
6192 }
6193 }
6194 });
6195 }
6196 }
6197 }
6198
6199 pub fn active_call(&self) -> Option<&dyn AnyActiveCall> {
6200 self.active_call.as_ref().map(|(call, _)| &*call.0)
6201 }
6202
6203 pub fn active_global_call(&self) -> Option<GlobalAnyActiveCall> {
6204 self.active_call.as_ref().map(|(call, _)| call.clone())
6205 }
6206
6207 fn on_active_call_event(
6208 &mut self,
6209 event: &ActiveCallEvent,
6210 window: &mut Window,
6211 cx: &mut Context<Self>,
6212 ) {
6213 match event {
6214 ActiveCallEvent::ParticipantLocationChanged { participant_id }
6215 | ActiveCallEvent::RemoteVideoTracksChanged { participant_id } => {
6216 self.leader_updated(participant_id, window, cx);
6217 }
6218 }
6219 }
6220
6221 pub fn database_id(&self) -> Option<WorkspaceId> {
6222 self.database_id
6223 }
6224
6225 #[cfg(any(test, feature = "test-support"))]
6226 pub(crate) fn set_database_id(&mut self, id: WorkspaceId) {
6227 self.database_id = Some(id);
6228 }
6229
6230 pub fn session_id(&self) -> Option<String> {
6231 self.session_id.clone()
6232 }
6233
6234 fn save_window_bounds(&self, window: &mut Window, cx: &mut App) -> Task<()> {
6235 let Some(display) = window.display(cx) else {
6236 return Task::ready(());
6237 };
6238 let Ok(display_uuid) = display.uuid() else {
6239 return Task::ready(());
6240 };
6241
6242 let window_bounds = window.inner_window_bounds();
6243 let database_id = self.database_id;
6244 let has_paths = !self.root_paths(cx).is_empty();
6245 let db = WorkspaceDb::global(cx);
6246 let kvp = db::kvp::KeyValueStore::global(cx);
6247
6248 cx.background_executor().spawn(async move {
6249 if !has_paths {
6250 persistence::write_default_window_bounds(&kvp, window_bounds, display_uuid)
6251 .await
6252 .log_err();
6253 }
6254 if let Some(database_id) = database_id {
6255 db.set_window_open_status(
6256 database_id,
6257 SerializedWindowBounds(window_bounds),
6258 display_uuid,
6259 )
6260 .await
6261 .log_err();
6262 } else {
6263 persistence::write_default_window_bounds(&kvp, window_bounds, display_uuid)
6264 .await
6265 .log_err();
6266 }
6267 })
6268 }
6269
6270 /// Bypass the 200ms serialization throttle and write workspace state to
6271 /// the DB immediately. Returns a task the caller can await to ensure the
6272 /// write completes. Used by the quit handler so the most recent state
6273 /// isn't lost to a pending throttle timer when the process exits.
6274 pub fn flush_serialization(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
6275 self._schedule_serialize_workspace.take();
6276 self._serialize_workspace_task.take();
6277 self.bounds_save_task_queued.take();
6278
6279 let bounds_task = self.save_window_bounds(window, cx);
6280 let serialize_task = self.serialize_workspace_internal(window, cx);
6281 cx.spawn(async move |_| {
6282 bounds_task.await;
6283 serialize_task.await;
6284 })
6285 }
6286
6287 pub fn root_paths(&self, cx: &App) -> Vec<Arc<Path>> {
6288 let project = self.project().read(cx);
6289 project
6290 .visible_worktrees(cx)
6291 .map(|worktree| worktree.read(cx).abs_path())
6292 .collect::<Vec<_>>()
6293 }
6294
6295 fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
6296 match member {
6297 Member::Axis(PaneAxis { members, .. }) => {
6298 for child in members.iter() {
6299 self.remove_panes(child.clone(), window, cx)
6300 }
6301 }
6302 Member::Pane(pane) => {
6303 self.force_remove_pane(&pane, &None, window, cx);
6304 }
6305 }
6306 }
6307
6308 fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
6309 self.session_id.take();
6310 self.serialize_workspace_internal(window, cx)
6311 }
6312
6313 fn force_remove_pane(
6314 &mut self,
6315 pane: &Entity<Pane>,
6316 focus_on: &Option<Entity<Pane>>,
6317 window: &mut Window,
6318 cx: &mut Context<Workspace>,
6319 ) {
6320 self.panes.retain(|p| p != pane);
6321 if let Some(focus_on) = focus_on {
6322 focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
6323 } else if self.active_pane() == pane {
6324 self.panes
6325 .last()
6326 .unwrap()
6327 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
6328 }
6329 if self.last_active_center_pane == Some(pane.downgrade()) {
6330 self.last_active_center_pane = None;
6331 }
6332 cx.notify();
6333 }
6334
6335 fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
6336 if self._schedule_serialize_workspace.is_none() {
6337 self._schedule_serialize_workspace =
6338 Some(cx.spawn_in(window, async move |this, cx| {
6339 cx.background_executor()
6340 .timer(SERIALIZATION_THROTTLE_TIME)
6341 .await;
6342 this.update_in(cx, |this, window, cx| {
6343 this._serialize_workspace_task =
6344 Some(this.serialize_workspace_internal(window, cx));
6345 this._schedule_serialize_workspace.take();
6346 })
6347 .log_err();
6348 }));
6349 }
6350 }
6351
6352 fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
6353 let Some(database_id) = self.database_id() else {
6354 return Task::ready(());
6355 };
6356
6357 fn serialize_pane_handle(
6358 pane_handle: &Entity<Pane>,
6359 window: &mut Window,
6360 cx: &mut App,
6361 ) -> SerializedPane {
6362 let (items, active, pinned_count) = {
6363 let pane = pane_handle.read(cx);
6364 let active_item_id = pane.active_item().map(|item| item.item_id());
6365 (
6366 pane.items()
6367 .filter_map(|handle| {
6368 let handle = handle.to_serializable_item_handle(cx)?;
6369
6370 Some(SerializedItem {
6371 kind: Arc::from(handle.serialized_item_kind()),
6372 item_id: handle.item_id().as_u64(),
6373 active: Some(handle.item_id()) == active_item_id,
6374 preview: pane.is_active_preview_item(handle.item_id()),
6375 })
6376 })
6377 .collect::<Vec<_>>(),
6378 pane.has_focus(window, cx),
6379 pane.pinned_count(),
6380 )
6381 };
6382
6383 SerializedPane::new(items, active, pinned_count)
6384 }
6385
6386 fn build_serialized_pane_group(
6387 pane_group: &Member,
6388 window: &mut Window,
6389 cx: &mut App,
6390 ) -> SerializedPaneGroup {
6391 match pane_group {
6392 Member::Axis(PaneAxis {
6393 axis,
6394 members,
6395 flexes,
6396 bounding_boxes: _,
6397 }) => SerializedPaneGroup::Group {
6398 axis: SerializedAxis(*axis),
6399 children: members
6400 .iter()
6401 .map(|member| build_serialized_pane_group(member, window, cx))
6402 .collect::<Vec<_>>(),
6403 flexes: Some(flexes.lock().clone()),
6404 },
6405 Member::Pane(pane_handle) => {
6406 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
6407 }
6408 }
6409 }
6410
6411 fn build_serialized_docks(
6412 this: &Workspace,
6413 window: &mut Window,
6414 cx: &mut App,
6415 ) -> DockStructure {
6416 this.capture_dock_state(window, cx)
6417 }
6418
6419 match self.workspace_location(cx) {
6420 WorkspaceLocation::Location(location, paths) => {
6421 let breakpoints = self.project.update(cx, |project, cx| {
6422 project
6423 .breakpoint_store()
6424 .read(cx)
6425 .all_source_breakpoints(cx)
6426 });
6427 let user_toolchains = self
6428 .project
6429 .read(cx)
6430 .user_toolchains(cx)
6431 .unwrap_or_default();
6432
6433 let center_group = build_serialized_pane_group(&self.center.root, window, cx);
6434 let docks = build_serialized_docks(self, window, cx);
6435 let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
6436
6437 let serialized_workspace = SerializedWorkspace {
6438 id: database_id,
6439 location,
6440 paths,
6441 center_group,
6442 window_bounds,
6443 display: Default::default(),
6444 docks,
6445 centered_layout: self.centered_layout,
6446 session_id: self.session_id.clone(),
6447 breakpoints,
6448 window_id: Some(window.window_handle().window_id().as_u64()),
6449 user_toolchains,
6450 };
6451
6452 let db = WorkspaceDb::global(cx);
6453 window.spawn(cx, async move |_| {
6454 db.save_workspace(serialized_workspace).await;
6455 })
6456 }
6457 WorkspaceLocation::DetachFromSession => {
6458 let window_bounds = SerializedWindowBounds(window.window_bounds());
6459 let display = window.display(cx).and_then(|d| d.uuid().ok());
6460 // Save dock state for empty local workspaces
6461 let docks = build_serialized_docks(self, window, cx);
6462 let db = WorkspaceDb::global(cx);
6463 let kvp = db::kvp::KeyValueStore::global(cx);
6464 window.spawn(cx, async move |_| {
6465 db.set_window_open_status(
6466 database_id,
6467 window_bounds,
6468 display.unwrap_or_default(),
6469 )
6470 .await
6471 .log_err();
6472 db.set_session_id(database_id, None).await.log_err();
6473 persistence::write_default_dock_state(&kvp, docks)
6474 .await
6475 .log_err();
6476 })
6477 }
6478 WorkspaceLocation::None => {
6479 // Save dock state for empty non-local workspaces
6480 let docks = build_serialized_docks(self, window, cx);
6481 let kvp = db::kvp::KeyValueStore::global(cx);
6482 window.spawn(cx, async move |_| {
6483 persistence::write_default_dock_state(&kvp, docks)
6484 .await
6485 .log_err();
6486 })
6487 }
6488 }
6489 }
6490
6491 fn has_any_items_open(&self, cx: &App) -> bool {
6492 self.panes.iter().any(|pane| pane.read(cx).items_len() > 0)
6493 }
6494
6495 fn workspace_location(&self, cx: &App) -> WorkspaceLocation {
6496 let paths = PathList::new(&self.root_paths(cx));
6497 if let Some(connection) = self.project.read(cx).remote_connection_options(cx) {
6498 WorkspaceLocation::Location(SerializedWorkspaceLocation::Remote(connection), paths)
6499 } else if self.project.read(cx).is_local() {
6500 if !paths.is_empty() || self.has_any_items_open(cx) {
6501 WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths)
6502 } else {
6503 WorkspaceLocation::DetachFromSession
6504 }
6505 } else {
6506 WorkspaceLocation::None
6507 }
6508 }
6509
6510 fn update_history(&self, cx: &mut App) {
6511 let Some(id) = self.database_id() else {
6512 return;
6513 };
6514 if !self.project.read(cx).is_local() {
6515 return;
6516 }
6517 if let Some(manager) = HistoryManager::global(cx) {
6518 let paths = PathList::new(&self.root_paths(cx));
6519 manager.update(cx, |this, cx| {
6520 this.update_history(id, HistoryManagerEntry::new(id, &paths), cx);
6521 });
6522 }
6523 }
6524
6525 async fn serialize_items(
6526 this: &WeakEntity<Self>,
6527 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
6528 cx: &mut AsyncWindowContext,
6529 ) -> Result<()> {
6530 const CHUNK_SIZE: usize = 200;
6531
6532 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
6533
6534 while let Some(items_received) = serializable_items.next().await {
6535 let unique_items =
6536 items_received
6537 .into_iter()
6538 .fold(HashMap::default(), |mut acc, item| {
6539 acc.entry(item.item_id()).or_insert(item);
6540 acc
6541 });
6542
6543 // We use into_iter() here so that the references to the items are moved into
6544 // the tasks and not kept alive while we're sleeping.
6545 for (_, item) in unique_items.into_iter() {
6546 if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
6547 item.serialize(workspace, false, window, cx)
6548 }) {
6549 cx.background_spawn(async move { task.await.log_err() })
6550 .detach();
6551 }
6552 }
6553
6554 cx.background_executor()
6555 .timer(SERIALIZATION_THROTTLE_TIME)
6556 .await;
6557 }
6558
6559 Ok(())
6560 }
6561
6562 pub(crate) fn enqueue_item_serialization(
6563 &mut self,
6564 item: Box<dyn SerializableItemHandle>,
6565 ) -> Result<()> {
6566 self.serializable_items_tx
6567 .unbounded_send(item)
6568 .map_err(|err| anyhow!("failed to send serializable item over channel: {err}"))
6569 }
6570
6571 pub(crate) fn load_workspace(
6572 serialized_workspace: SerializedWorkspace,
6573 paths_to_open: Vec<Option<ProjectPath>>,
6574 window: &mut Window,
6575 cx: &mut Context<Workspace>,
6576 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
6577 cx.spawn_in(window, async move |workspace, cx| {
6578 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
6579
6580 let mut center_group = None;
6581 let mut center_items = None;
6582
6583 // Traverse the splits tree and add to things
6584 if let Some((group, active_pane, items)) = serialized_workspace
6585 .center_group
6586 .deserialize(&project, serialized_workspace.id, workspace.clone(), cx)
6587 .await
6588 {
6589 center_items = Some(items);
6590 center_group = Some((group, active_pane))
6591 }
6592
6593 let mut items_by_project_path = HashMap::default();
6594 let mut item_ids_by_kind = HashMap::default();
6595 let mut all_deserialized_items = Vec::default();
6596 cx.update(|_, cx| {
6597 for item in center_items.unwrap_or_default().into_iter().flatten() {
6598 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
6599 item_ids_by_kind
6600 .entry(serializable_item_handle.serialized_item_kind())
6601 .or_insert(Vec::new())
6602 .push(item.item_id().as_u64() as ItemId);
6603 }
6604
6605 if let Some(project_path) = item.project_path(cx) {
6606 items_by_project_path.insert(project_path, item.clone());
6607 }
6608 all_deserialized_items.push(item);
6609 }
6610 })?;
6611
6612 let opened_items = paths_to_open
6613 .into_iter()
6614 .map(|path_to_open| {
6615 path_to_open
6616 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
6617 })
6618 .collect::<Vec<_>>();
6619
6620 // Remove old panes from workspace panes list
6621 workspace.update_in(cx, |workspace, window, cx| {
6622 if let Some((center_group, active_pane)) = center_group {
6623 workspace.remove_panes(workspace.center.root.clone(), window, cx);
6624
6625 // Swap workspace center group
6626 workspace.center = PaneGroup::with_root(center_group);
6627 workspace.center.set_is_center(true);
6628 workspace.center.mark_positions(cx);
6629
6630 if let Some(active_pane) = active_pane {
6631 workspace.set_active_pane(&active_pane, window, cx);
6632 cx.focus_self(window);
6633 } else {
6634 workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
6635 }
6636 }
6637
6638 let docks = serialized_workspace.docks;
6639
6640 for (dock, serialized_dock) in [
6641 (&mut workspace.right_dock, docks.right),
6642 (&mut workspace.left_dock, docks.left),
6643 (&mut workspace.bottom_dock, docks.bottom),
6644 ]
6645 .iter_mut()
6646 {
6647 dock.update(cx, |dock, cx| {
6648 dock.serialized_dock = Some(serialized_dock.clone());
6649 dock.restore_state(window, cx);
6650 });
6651 }
6652
6653 cx.notify();
6654 })?;
6655
6656 let _ = project
6657 .update(cx, |project, cx| {
6658 project
6659 .breakpoint_store()
6660 .update(cx, |breakpoint_store, cx| {
6661 breakpoint_store
6662 .with_serialized_breakpoints(serialized_workspace.breakpoints, cx)
6663 })
6664 })
6665 .await;
6666
6667 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
6668 // after loading the items, we might have different items and in order to avoid
6669 // the database filling up, we delete items that haven't been loaded now.
6670 //
6671 // The items that have been loaded, have been saved after they've been added to the workspace.
6672 let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
6673 item_ids_by_kind
6674 .into_iter()
6675 .map(|(item_kind, loaded_items)| {
6676 SerializableItemRegistry::cleanup(
6677 item_kind,
6678 serialized_workspace.id,
6679 loaded_items,
6680 window,
6681 cx,
6682 )
6683 .log_err()
6684 })
6685 .collect::<Vec<_>>()
6686 })?;
6687
6688 futures::future::join_all(clean_up_tasks).await;
6689
6690 workspace
6691 .update_in(cx, |workspace, window, cx| {
6692 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
6693 workspace.serialize_workspace_internal(window, cx).detach();
6694
6695 // Ensure that we mark the window as edited if we did load dirty items
6696 workspace.update_window_edited(window, cx);
6697 })
6698 .ok();
6699
6700 Ok(opened_items)
6701 })
6702 }
6703
6704 pub fn key_context(&self, cx: &App) -> KeyContext {
6705 let mut context = KeyContext::new_with_defaults();
6706 context.add("Workspace");
6707 context.set("keyboard_layout", cx.keyboard_layout().name().to_string());
6708 if let Some(status) = self
6709 .debugger_provider
6710 .as_ref()
6711 .and_then(|provider| provider.active_thread_state(cx))
6712 {
6713 match status {
6714 ThreadStatus::Running | ThreadStatus::Stepping => {
6715 context.add("debugger_running");
6716 }
6717 ThreadStatus::Stopped => context.add("debugger_stopped"),
6718 ThreadStatus::Exited | ThreadStatus::Ended => {}
6719 }
6720 }
6721
6722 if self.left_dock.read(cx).is_open() {
6723 if let Some(active_panel) = self.left_dock.read(cx).active_panel() {
6724 context.set("left_dock", active_panel.panel_key());
6725 }
6726 }
6727
6728 if self.right_dock.read(cx).is_open() {
6729 if let Some(active_panel) = self.right_dock.read(cx).active_panel() {
6730 context.set("right_dock", active_panel.panel_key());
6731 }
6732 }
6733
6734 if self.bottom_dock.read(cx).is_open() {
6735 if let Some(active_panel) = self.bottom_dock.read(cx).active_panel() {
6736 context.set("bottom_dock", active_panel.panel_key());
6737 }
6738 }
6739
6740 context
6741 }
6742
6743 /// Multiworkspace uses this to add workspace action handling to itself
6744 pub fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
6745 self.add_workspace_actions_listeners(div, window, cx)
6746 .on_action(cx.listener(
6747 |_workspace, action_sequence: &settings::ActionSequence, window, cx| {
6748 for action in &action_sequence.0 {
6749 window.dispatch_action(action.boxed_clone(), cx);
6750 }
6751 },
6752 ))
6753 .on_action(cx.listener(Self::close_inactive_items_and_panes))
6754 .on_action(cx.listener(Self::close_all_items_and_panes))
6755 .on_action(cx.listener(Self::close_item_in_all_panes))
6756 .on_action(cx.listener(Self::save_all))
6757 .on_action(cx.listener(Self::send_keystrokes))
6758 .on_action(cx.listener(Self::add_folder_to_project))
6759 .on_action(cx.listener(Self::follow_next_collaborator))
6760 .on_action(cx.listener(Self::activate_pane_at_index))
6761 .on_action(cx.listener(Self::move_item_to_pane_at_index))
6762 .on_action(cx.listener(Self::move_focused_panel_to_next_position))
6763 .on_action(cx.listener(Self::toggle_edit_predictions_all_files))
6764 .on_action(cx.listener(Self::toggle_theme_mode))
6765 .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
6766 let pane = workspace.active_pane().clone();
6767 workspace.unfollow_in_pane(&pane, window, cx);
6768 }))
6769 .on_action(cx.listener(|workspace, action: &Save, window, cx| {
6770 workspace
6771 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
6772 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6773 }))
6774 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
6775 workspace
6776 .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
6777 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6778 }))
6779 .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
6780 workspace
6781 .save_active_item(SaveIntent::SaveAs, window, cx)
6782 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6783 }))
6784 .on_action(
6785 cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
6786 workspace.activate_previous_pane(window, cx)
6787 }),
6788 )
6789 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
6790 workspace.activate_next_pane(window, cx)
6791 }))
6792 .on_action(cx.listener(|workspace, _: &ActivateLastPane, window, cx| {
6793 workspace.activate_last_pane(window, cx)
6794 }))
6795 .on_action(
6796 cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
6797 workspace.activate_next_window(cx)
6798 }),
6799 )
6800 .on_action(
6801 cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
6802 workspace.activate_previous_window(cx)
6803 }),
6804 )
6805 .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
6806 workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
6807 }))
6808 .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
6809 workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
6810 }))
6811 .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
6812 workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
6813 }))
6814 .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
6815 workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
6816 }))
6817 .on_action(cx.listener(
6818 |workspace, action: &MoveItemToPaneInDirection, window, cx| {
6819 workspace.move_item_to_pane_in_direction(action, window, cx)
6820 },
6821 ))
6822 .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
6823 workspace.swap_pane_in_direction(SplitDirection::Left, cx)
6824 }))
6825 .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
6826 workspace.swap_pane_in_direction(SplitDirection::Right, cx)
6827 }))
6828 .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
6829 workspace.swap_pane_in_direction(SplitDirection::Up, cx)
6830 }))
6831 .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
6832 workspace.swap_pane_in_direction(SplitDirection::Down, cx)
6833 }))
6834 .on_action(cx.listener(|workspace, _: &SwapPaneAdjacent, window, cx| {
6835 const DIRECTION_PRIORITY: [SplitDirection; 4] = [
6836 SplitDirection::Down,
6837 SplitDirection::Up,
6838 SplitDirection::Right,
6839 SplitDirection::Left,
6840 ];
6841 for dir in DIRECTION_PRIORITY {
6842 if workspace.find_pane_in_direction(dir, cx).is_some() {
6843 workspace.swap_pane_in_direction(dir, cx);
6844 workspace.activate_pane_in_direction(dir.opposite(), window, cx);
6845 break;
6846 }
6847 }
6848 }))
6849 .on_action(cx.listener(|workspace, _: &MovePaneLeft, _, cx| {
6850 workspace.move_pane_to_border(SplitDirection::Left, cx)
6851 }))
6852 .on_action(cx.listener(|workspace, _: &MovePaneRight, _, cx| {
6853 workspace.move_pane_to_border(SplitDirection::Right, cx)
6854 }))
6855 .on_action(cx.listener(|workspace, _: &MovePaneUp, _, cx| {
6856 workspace.move_pane_to_border(SplitDirection::Up, cx)
6857 }))
6858 .on_action(cx.listener(|workspace, _: &MovePaneDown, _, cx| {
6859 workspace.move_pane_to_border(SplitDirection::Down, cx)
6860 }))
6861 .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
6862 this.toggle_dock(DockPosition::Left, window, cx);
6863 }))
6864 .on_action(cx.listener(
6865 |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
6866 workspace.toggle_dock(DockPosition::Right, window, cx);
6867 },
6868 ))
6869 .on_action(cx.listener(
6870 |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
6871 workspace.toggle_dock(DockPosition::Bottom, window, cx);
6872 },
6873 ))
6874 .on_action(cx.listener(
6875 |workspace: &mut Workspace, _: &CloseActiveDock, window, cx| {
6876 if !workspace.close_active_dock(window, cx) {
6877 cx.propagate();
6878 }
6879 },
6880 ))
6881 .on_action(
6882 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
6883 workspace.close_all_docks(window, cx);
6884 }),
6885 )
6886 .on_action(cx.listener(Self::toggle_all_docks))
6887 .on_action(cx.listener(
6888 |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
6889 workspace.clear_all_notifications(cx);
6890 },
6891 ))
6892 .on_action(cx.listener(
6893 |workspace: &mut Workspace, _: &ClearNavigationHistory, window, cx| {
6894 workspace.clear_navigation_history(window, cx);
6895 },
6896 ))
6897 .on_action(cx.listener(
6898 |workspace: &mut Workspace, _: &SuppressNotification, _, cx| {
6899 if let Some((notification_id, _)) = workspace.notifications.pop() {
6900 workspace.suppress_notification(¬ification_id, cx);
6901 }
6902 },
6903 ))
6904 .on_action(cx.listener(
6905 |workspace: &mut Workspace, _: &ToggleWorktreeSecurity, window, cx| {
6906 workspace.show_worktree_trust_security_modal(true, window, cx);
6907 },
6908 ))
6909 .on_action(
6910 cx.listener(|_: &mut Workspace, _: &ClearTrustedWorktrees, _, cx| {
6911 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
6912 trusted_worktrees.update(cx, |trusted_worktrees, _| {
6913 trusted_worktrees.clear_trusted_paths()
6914 });
6915 let db = WorkspaceDb::global(cx);
6916 cx.spawn(async move |_, cx| {
6917 if db.clear_trusted_worktrees().await.log_err().is_some() {
6918 cx.update(|cx| reload(cx));
6919 }
6920 })
6921 .detach();
6922 }
6923 }),
6924 )
6925 .on_action(cx.listener(
6926 |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
6927 workspace.reopen_closed_item(window, cx).detach();
6928 },
6929 ))
6930 .on_action(cx.listener(
6931 |workspace: &mut Workspace, _: &ResetActiveDockSize, window, cx| {
6932 for dock in workspace.all_docks() {
6933 if dock.focus_handle(cx).contains_focused(window, cx) {
6934 let panel = dock.read(cx).active_panel().cloned();
6935 if let Some(panel) = panel {
6936 dock.update(cx, |dock, cx| {
6937 dock.set_panel_size_state(
6938 panel.as_ref(),
6939 dock::PanelSizeState::default(),
6940 cx,
6941 );
6942 });
6943 }
6944 return;
6945 }
6946 }
6947 },
6948 ))
6949 .on_action(cx.listener(
6950 |workspace: &mut Workspace, _: &ResetOpenDocksSize, _window, cx| {
6951 for dock in workspace.all_docks() {
6952 let panel = dock.read(cx).visible_panel().cloned();
6953 if let Some(panel) = panel {
6954 dock.update(cx, |dock, cx| {
6955 dock.set_panel_size_state(
6956 panel.as_ref(),
6957 dock::PanelSizeState::default(),
6958 cx,
6959 );
6960 });
6961 }
6962 }
6963 },
6964 ))
6965 .on_action(cx.listener(
6966 |workspace: &mut Workspace, act: &IncreaseActiveDockSize, window, cx| {
6967 adjust_active_dock_size_by_px(
6968 px_with_ui_font_fallback(act.px, cx),
6969 workspace,
6970 window,
6971 cx,
6972 );
6973 },
6974 ))
6975 .on_action(cx.listener(
6976 |workspace: &mut Workspace, act: &DecreaseActiveDockSize, window, cx| {
6977 adjust_active_dock_size_by_px(
6978 px_with_ui_font_fallback(act.px, cx) * -1.,
6979 workspace,
6980 window,
6981 cx,
6982 );
6983 },
6984 ))
6985 .on_action(cx.listener(
6986 |workspace: &mut Workspace, act: &IncreaseOpenDocksSize, window, cx| {
6987 adjust_open_docks_size_by_px(
6988 px_with_ui_font_fallback(act.px, cx),
6989 workspace,
6990 window,
6991 cx,
6992 );
6993 },
6994 ))
6995 .on_action(cx.listener(
6996 |workspace: &mut Workspace, act: &DecreaseOpenDocksSize, window, cx| {
6997 adjust_open_docks_size_by_px(
6998 px_with_ui_font_fallback(act.px, cx) * -1.,
6999 workspace,
7000 window,
7001 cx,
7002 );
7003 },
7004 ))
7005 .on_action(cx.listener(Workspace::toggle_centered_layout))
7006 .on_action(cx.listener(
7007 |workspace: &mut Workspace, _action: &pane::ActivateNextItem, window, cx| {
7008 if let Some(active_dock) = workspace.active_dock(window, cx) {
7009 let dock = active_dock.read(cx);
7010 if let Some(active_panel) = dock.active_panel() {
7011 if active_panel.pane(cx).is_none() {
7012 let mut recent_pane: Option<Entity<Pane>> = None;
7013 let mut recent_timestamp = 0;
7014 for pane_handle in workspace.panes() {
7015 let pane = pane_handle.read(cx);
7016 for entry in pane.activation_history() {
7017 if entry.timestamp > recent_timestamp {
7018 recent_timestamp = entry.timestamp;
7019 recent_pane = Some(pane_handle.clone());
7020 }
7021 }
7022 }
7023
7024 if let Some(pane) = recent_pane {
7025 pane.update(cx, |pane, cx| {
7026 let current_index = pane.active_item_index();
7027 let items_len = pane.items_len();
7028 if items_len > 0 {
7029 let next_index = if current_index + 1 < items_len {
7030 current_index + 1
7031 } else {
7032 0
7033 };
7034 pane.activate_item(
7035 next_index, false, false, window, cx,
7036 );
7037 }
7038 });
7039 return;
7040 }
7041 }
7042 }
7043 }
7044 cx.propagate();
7045 },
7046 ))
7047 .on_action(cx.listener(
7048 |workspace: &mut Workspace, _action: &pane::ActivatePreviousItem, window, cx| {
7049 if let Some(active_dock) = workspace.active_dock(window, cx) {
7050 let dock = active_dock.read(cx);
7051 if let Some(active_panel) = dock.active_panel() {
7052 if active_panel.pane(cx).is_none() {
7053 let mut recent_pane: Option<Entity<Pane>> = None;
7054 let mut recent_timestamp = 0;
7055 for pane_handle in workspace.panes() {
7056 let pane = pane_handle.read(cx);
7057 for entry in pane.activation_history() {
7058 if entry.timestamp > recent_timestamp {
7059 recent_timestamp = entry.timestamp;
7060 recent_pane = Some(pane_handle.clone());
7061 }
7062 }
7063 }
7064
7065 if let Some(pane) = recent_pane {
7066 pane.update(cx, |pane, cx| {
7067 let current_index = pane.active_item_index();
7068 let items_len = pane.items_len();
7069 if items_len > 0 {
7070 let prev_index = if current_index > 0 {
7071 current_index - 1
7072 } else {
7073 items_len.saturating_sub(1)
7074 };
7075 pane.activate_item(
7076 prev_index, false, false, window, cx,
7077 );
7078 }
7079 });
7080 return;
7081 }
7082 }
7083 }
7084 }
7085 cx.propagate();
7086 },
7087 ))
7088 .on_action(cx.listener(
7089 |workspace: &mut Workspace, action: &pane::CloseActiveItem, window, cx| {
7090 if let Some(active_dock) = workspace.active_dock(window, cx) {
7091 let dock = active_dock.read(cx);
7092 if let Some(active_panel) = dock.active_panel() {
7093 if active_panel.pane(cx).is_none() {
7094 let active_pane = workspace.active_pane().clone();
7095 active_pane.update(cx, |pane, cx| {
7096 pane.close_active_item(action, window, cx)
7097 .detach_and_log_err(cx);
7098 });
7099 return;
7100 }
7101 }
7102 }
7103 cx.propagate();
7104 },
7105 ))
7106 .on_action(
7107 cx.listener(|workspace, _: &ToggleReadOnlyFile, window, cx| {
7108 let pane = workspace.active_pane().clone();
7109 if let Some(item) = pane.read(cx).active_item() {
7110 item.toggle_read_only(window, cx);
7111 }
7112 }),
7113 )
7114 .on_action(cx.listener(|workspace, _: &FocusCenterPane, window, cx| {
7115 workspace.focus_center_pane(window, cx);
7116 }))
7117 .on_action(cx.listener(Workspace::cancel))
7118 }
7119
7120 #[cfg(any(test, feature = "test-support"))]
7121 pub fn set_random_database_id(&mut self) {
7122 self.database_id = Some(WorkspaceId(Uuid::new_v4().as_u64_pair().0 as i64));
7123 }
7124
7125 #[cfg(any(test, feature = "test-support"))]
7126 pub(crate) fn test_new(
7127 project: Entity<Project>,
7128 window: &mut Window,
7129 cx: &mut Context<Self>,
7130 ) -> Self {
7131 use node_runtime::NodeRuntime;
7132 use session::Session;
7133
7134 let client = project.read(cx).client();
7135 let user_store = project.read(cx).user_store();
7136 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
7137 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
7138 window.activate_window();
7139 let app_state = Arc::new(AppState {
7140 languages: project.read(cx).languages().clone(),
7141 workspace_store,
7142 client,
7143 user_store,
7144 fs: project.read(cx).fs().clone(),
7145 build_window_options: |_, _| Default::default(),
7146 node_runtime: NodeRuntime::unavailable(),
7147 session,
7148 });
7149 let workspace = Self::new(Default::default(), project, app_state, window, cx);
7150 workspace
7151 .active_pane
7152 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
7153 workspace
7154 }
7155
7156 pub fn register_action<A: Action>(
7157 &mut self,
7158 callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
7159 ) -> &mut Self {
7160 let callback = Arc::new(callback);
7161
7162 self.workspace_actions.push(Box::new(move |div, _, _, cx| {
7163 let callback = callback.clone();
7164 div.on_action(cx.listener(move |workspace, event, window, cx| {
7165 (callback)(workspace, event, window, cx)
7166 }))
7167 }));
7168 self
7169 }
7170 pub fn register_action_renderer(
7171 &mut self,
7172 callback: impl Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div + 'static,
7173 ) -> &mut Self {
7174 self.workspace_actions.push(Box::new(callback));
7175 self
7176 }
7177
7178 fn add_workspace_actions_listeners(
7179 &self,
7180 mut div: Div,
7181 window: &mut Window,
7182 cx: &mut Context<Self>,
7183 ) -> Div {
7184 for action in self.workspace_actions.iter() {
7185 div = (action)(div, self, window, cx)
7186 }
7187 div
7188 }
7189
7190 pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
7191 self.modal_layer.read(cx).has_active_modal()
7192 }
7193
7194 pub fn is_active_modal_command_palette(&self, cx: &mut App) -> bool {
7195 self.modal_layer
7196 .read(cx)
7197 .is_active_modal_command_palette(cx)
7198 }
7199
7200 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
7201 self.modal_layer.read(cx).active_modal()
7202 }
7203
7204 /// Toggles a modal of type `V`. If a modal of the same type is currently active,
7205 /// it will be hidden. If a different modal is active, it will be replaced with the new one.
7206 /// If no modal is active, the new modal will be shown.
7207 ///
7208 /// If closing the current modal fails (e.g., due to `on_before_dismiss` returning
7209 /// `DismissDecision::Dismiss(false)` or `DismissDecision::Pending`), the new modal
7210 /// will not be shown.
7211 pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
7212 where
7213 B: FnOnce(&mut Window, &mut Context<V>) -> V,
7214 {
7215 self.modal_layer.update(cx, |modal_layer, cx| {
7216 modal_layer.toggle_modal(window, cx, build)
7217 })
7218 }
7219
7220 pub fn hide_modal(&mut self, window: &mut Window, cx: &mut App) -> bool {
7221 self.modal_layer
7222 .update(cx, |modal_layer, cx| modal_layer.hide_modal(window, cx))
7223 }
7224
7225 pub fn toggle_status_toast<V: ToastView>(&mut self, entity: Entity<V>, cx: &mut App) {
7226 self.toast_layer
7227 .update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity))
7228 }
7229
7230 pub fn toggle_centered_layout(
7231 &mut self,
7232 _: &ToggleCenteredLayout,
7233 _: &mut Window,
7234 cx: &mut Context<Self>,
7235 ) {
7236 self.centered_layout = !self.centered_layout;
7237 if let Some(database_id) = self.database_id() {
7238 let db = WorkspaceDb::global(cx);
7239 let centered_layout = self.centered_layout;
7240 cx.background_spawn(async move {
7241 db.set_centered_layout(database_id, centered_layout).await
7242 })
7243 .detach_and_log_err(cx);
7244 }
7245 cx.notify();
7246 }
7247
7248 fn adjust_padding(padding: Option<f32>) -> f32 {
7249 padding
7250 .unwrap_or(CenteredPaddingSettings::default().0)
7251 .clamp(
7252 CenteredPaddingSettings::MIN_PADDING,
7253 CenteredPaddingSettings::MAX_PADDING,
7254 )
7255 }
7256
7257 fn render_dock(
7258 &self,
7259 position: DockPosition,
7260 dock: &Entity<Dock>,
7261 window: &mut Window,
7262 cx: &mut App,
7263 ) -> Option<Div> {
7264 if self.zoomed_position == Some(position) {
7265 return None;
7266 }
7267
7268 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
7269 let pane = panel.pane(cx)?;
7270 let follower_states = &self.follower_states;
7271 leader_border_for_pane(follower_states, &pane, window, cx)
7272 });
7273
7274 let mut container = div()
7275 .flex()
7276 .overflow_hidden()
7277 .flex_none()
7278 .child(dock.clone())
7279 .children(leader_border);
7280
7281 // Apply sizing only when the dock is open. When closed the dock is still
7282 // included in the element tree so its focus handle remains mounted — without
7283 // this, toggle_panel_focus cannot focus the panel when the dock is closed.
7284 let dock = dock.read(cx);
7285 if let Some(panel) = dock.visible_panel() {
7286 let size_state = dock.stored_panel_size_state(panel.as_ref());
7287 if position.axis() == Axis::Horizontal {
7288 if let Some(ratio) = size_state
7289 .and_then(|state| state.flexible_size_ratio)
7290 .or_else(|| self.default_flexible_dock_ratio(position))
7291 && panel.supports_flexible_size(window, cx)
7292 {
7293 let ratio = ratio.clamp(0.001, 0.999);
7294 let grow = ratio / (1.0 - ratio);
7295 let style = container.style();
7296 style.flex_grow = Some(grow);
7297 style.flex_shrink = Some(1.0);
7298 style.flex_basis = Some(relative(0.).into());
7299 } else {
7300 let size = size_state
7301 .and_then(|state| state.size)
7302 .unwrap_or_else(|| panel.default_size(window, cx));
7303 container = container.w(size);
7304 }
7305 } else {
7306 let size = size_state
7307 .and_then(|state| state.size)
7308 .unwrap_or_else(|| panel.default_size(window, cx));
7309 container = container.h(size);
7310 }
7311 }
7312
7313 Some(container)
7314 }
7315
7316 pub fn for_window(window: &Window, cx: &App) -> Option<Entity<Workspace>> {
7317 window
7318 .root::<MultiWorkspace>()
7319 .flatten()
7320 .map(|multi_workspace| multi_workspace.read(cx).workspace().clone())
7321 }
7322
7323 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
7324 self.zoomed.as_ref()
7325 }
7326
7327 pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
7328 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
7329 return;
7330 };
7331 let windows = cx.windows();
7332 let next_window =
7333 SystemWindowTabController::get_next_tab_group_window(cx, current_window_id).or_else(
7334 || {
7335 windows
7336 .iter()
7337 .cycle()
7338 .skip_while(|window| window.window_id() != current_window_id)
7339 .nth(1)
7340 },
7341 );
7342
7343 if let Some(window) = next_window {
7344 window
7345 .update(cx, |_, window, _| window.activate_window())
7346 .ok();
7347 }
7348 }
7349
7350 pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
7351 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
7352 return;
7353 };
7354 let windows = cx.windows();
7355 let prev_window =
7356 SystemWindowTabController::get_prev_tab_group_window(cx, current_window_id).or_else(
7357 || {
7358 windows
7359 .iter()
7360 .rev()
7361 .cycle()
7362 .skip_while(|window| window.window_id() != current_window_id)
7363 .nth(1)
7364 },
7365 );
7366
7367 if let Some(window) = prev_window {
7368 window
7369 .update(cx, |_, window, _| window.activate_window())
7370 .ok();
7371 }
7372 }
7373
7374 pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
7375 if cx.stop_active_drag(window) {
7376 } else if let Some((notification_id, _)) = self.notifications.pop() {
7377 dismiss_app_notification(¬ification_id, cx);
7378 } else {
7379 cx.propagate();
7380 }
7381 }
7382
7383 fn resize_dock(
7384 &mut self,
7385 dock_pos: DockPosition,
7386 new_size: Pixels,
7387 window: &mut Window,
7388 cx: &mut Context<Self>,
7389 ) {
7390 match dock_pos {
7391 DockPosition::Left => self.resize_left_dock(new_size, window, cx),
7392 DockPosition::Right => self.resize_right_dock(new_size, window, cx),
7393 DockPosition::Bottom => self.resize_bottom_dock(new_size, window, cx),
7394 }
7395 }
7396
7397 fn resize_left_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
7398 let workspace_width = self.bounds.size.width;
7399 let mut size = new_size.min(workspace_width - RESIZE_HANDLE_SIZE);
7400
7401 self.right_dock.read_with(cx, |right_dock, cx| {
7402 let right_dock_size = right_dock
7403 .stored_active_panel_size(window, cx)
7404 .unwrap_or(Pixels::ZERO);
7405 if right_dock_size + size > workspace_width {
7406 size = workspace_width - right_dock_size
7407 }
7408 });
7409
7410 let ratio = self.flexible_dock_ratio_for_size(DockPosition::Left, size, window, cx);
7411 self.left_dock.update(cx, |left_dock, cx| {
7412 if WorkspaceSettings::get_global(cx)
7413 .resize_all_panels_in_dock
7414 .contains(&DockPosition::Left)
7415 {
7416 left_dock.resize_all_panels(Some(size), ratio, window, cx);
7417 } else {
7418 left_dock.resize_active_panel(Some(size), ratio, window, cx);
7419 }
7420 });
7421 }
7422
7423 fn resize_right_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
7424 let workspace_width = self.bounds.size.width;
7425 let mut size = new_size.min(workspace_width - RESIZE_HANDLE_SIZE);
7426 self.left_dock.read_with(cx, |left_dock, cx| {
7427 let left_dock_size = left_dock
7428 .stored_active_panel_size(window, cx)
7429 .unwrap_or(Pixels::ZERO);
7430 if left_dock_size + size > workspace_width {
7431 size = workspace_width - left_dock_size
7432 }
7433 });
7434 let ratio = self.flexible_dock_ratio_for_size(DockPosition::Right, size, window, cx);
7435 self.right_dock.update(cx, |right_dock, cx| {
7436 if WorkspaceSettings::get_global(cx)
7437 .resize_all_panels_in_dock
7438 .contains(&DockPosition::Right)
7439 {
7440 right_dock.resize_all_panels(Some(size), ratio, window, cx);
7441 } else {
7442 right_dock.resize_active_panel(Some(size), ratio, window, cx);
7443 }
7444 });
7445 }
7446
7447 fn resize_bottom_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
7448 let size = new_size.min(self.bounds.bottom() - RESIZE_HANDLE_SIZE - self.bounds.top());
7449 self.bottom_dock.update(cx, |bottom_dock, cx| {
7450 if WorkspaceSettings::get_global(cx)
7451 .resize_all_panels_in_dock
7452 .contains(&DockPosition::Bottom)
7453 {
7454 bottom_dock.resize_all_panels(Some(size), None, window, cx);
7455 } else {
7456 bottom_dock.resize_active_panel(Some(size), None, window, cx);
7457 }
7458 });
7459 }
7460
7461 fn toggle_edit_predictions_all_files(
7462 &mut self,
7463 _: &ToggleEditPrediction,
7464 _window: &mut Window,
7465 cx: &mut Context<Self>,
7466 ) {
7467 let fs = self.project().read(cx).fs().clone();
7468 let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx);
7469 update_settings_file(fs, cx, move |file, _| {
7470 file.project.all_languages.defaults.show_edit_predictions = Some(!show_edit_predictions)
7471 });
7472 }
7473
7474 fn toggle_theme_mode(&mut self, _: &ToggleMode, _window: &mut Window, cx: &mut Context<Self>) {
7475 let current_mode = ThemeSettings::get_global(cx).theme.mode();
7476 let next_mode = match current_mode {
7477 Some(theme_settings::ThemeAppearanceMode::Light) => {
7478 theme_settings::ThemeAppearanceMode::Dark
7479 }
7480 Some(theme_settings::ThemeAppearanceMode::Dark) => {
7481 theme_settings::ThemeAppearanceMode::Light
7482 }
7483 Some(theme_settings::ThemeAppearanceMode::System) | None => {
7484 match cx.theme().appearance() {
7485 theme::Appearance::Light => theme_settings::ThemeAppearanceMode::Dark,
7486 theme::Appearance::Dark => theme_settings::ThemeAppearanceMode::Light,
7487 }
7488 }
7489 };
7490
7491 let fs = self.project().read(cx).fs().clone();
7492 settings::update_settings_file(fs, cx, move |settings, _cx| {
7493 theme_settings::set_mode(settings, next_mode);
7494 });
7495 }
7496
7497 pub fn show_worktree_trust_security_modal(
7498 &mut self,
7499 toggle: bool,
7500 window: &mut Window,
7501 cx: &mut Context<Self>,
7502 ) {
7503 if let Some(security_modal) = self.active_modal::<SecurityModal>(cx) {
7504 if toggle {
7505 security_modal.update(cx, |security_modal, cx| {
7506 security_modal.dismiss(cx);
7507 })
7508 } else {
7509 security_modal.update(cx, |security_modal, cx| {
7510 security_modal.refresh_restricted_paths(cx);
7511 });
7512 }
7513 } else {
7514 let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
7515 .map(|trusted_worktrees| {
7516 trusted_worktrees
7517 .read(cx)
7518 .has_restricted_worktrees(&self.project().read(cx).worktree_store(), cx)
7519 })
7520 .unwrap_or(false);
7521 if has_restricted_worktrees {
7522 let project = self.project().read(cx);
7523 let remote_host = project
7524 .remote_connection_options(cx)
7525 .map(RemoteHostLocation::from);
7526 let worktree_store = project.worktree_store().downgrade();
7527 self.toggle_modal(window, cx, |_, cx| {
7528 SecurityModal::new(worktree_store, remote_host, cx)
7529 });
7530 }
7531 }
7532 }
7533}
7534
7535pub trait AnyActiveCall {
7536 fn entity(&self) -> AnyEntity;
7537 fn is_in_room(&self, _: &App) -> bool;
7538 fn room_id(&self, _: &App) -> Option<u64>;
7539 fn channel_id(&self, _: &App) -> Option<ChannelId>;
7540 fn hang_up(&self, _: &mut App) -> Task<Result<()>>;
7541 fn unshare_project(&self, _: Entity<Project>, _: &mut App) -> Result<()>;
7542 fn remote_participant_for_peer_id(&self, _: PeerId, _: &App) -> Option<RemoteCollaborator>;
7543 fn is_sharing_project(&self, _: &App) -> bool;
7544 fn has_remote_participants(&self, _: &App) -> bool;
7545 fn local_participant_is_guest(&self, _: &App) -> bool;
7546 fn client(&self, _: &App) -> Arc<Client>;
7547 fn share_on_join(&self, _: &App) -> bool;
7548 fn join_channel(&self, _: ChannelId, _: &mut App) -> Task<Result<bool>>;
7549 fn room_update_completed(&self, _: &mut App) -> Task<()>;
7550 fn most_active_project(&self, _: &App) -> Option<(u64, u64)>;
7551 fn share_project(&self, _: Entity<Project>, _: &mut App) -> Task<Result<u64>>;
7552 fn join_project(
7553 &self,
7554 _: u64,
7555 _: Arc<LanguageRegistry>,
7556 _: Arc<dyn Fs>,
7557 _: &mut App,
7558 ) -> Task<Result<Entity<Project>>>;
7559 fn peer_id_for_user_in_room(&self, _: u64, _: &App) -> Option<PeerId>;
7560 fn subscribe(
7561 &self,
7562 _: &mut Window,
7563 _: &mut Context<Workspace>,
7564 _: Box<dyn Fn(&mut Workspace, &ActiveCallEvent, &mut Window, &mut Context<Workspace>)>,
7565 ) -> Subscription;
7566 fn create_shared_screen(
7567 &self,
7568 _: PeerId,
7569 _: &Entity<Pane>,
7570 _: &mut Window,
7571 _: &mut App,
7572 ) -> Option<Entity<SharedScreen>>;
7573}
7574
7575#[derive(Clone)]
7576pub struct GlobalAnyActiveCall(pub Arc<dyn AnyActiveCall>);
7577impl Global for GlobalAnyActiveCall {}
7578
7579impl GlobalAnyActiveCall {
7580 pub(crate) fn try_global(cx: &App) -> Option<&Self> {
7581 cx.try_global()
7582 }
7583
7584 pub(crate) fn global(cx: &App) -> &Self {
7585 cx.global()
7586 }
7587}
7588
7589pub fn merge_conflict_notification_id() -> NotificationId {
7590 struct MergeConflictNotification;
7591 NotificationId::unique::<MergeConflictNotification>()
7592}
7593
7594/// Workspace-local view of a remote participant's location.
7595#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7596pub enum ParticipantLocation {
7597 SharedProject { project_id: u64 },
7598 UnsharedProject,
7599 External,
7600}
7601
7602impl ParticipantLocation {
7603 pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
7604 match location
7605 .and_then(|l| l.variant)
7606 .context("participant location was not provided")?
7607 {
7608 proto::participant_location::Variant::SharedProject(project) => {
7609 Ok(Self::SharedProject {
7610 project_id: project.id,
7611 })
7612 }
7613 proto::participant_location::Variant::UnsharedProject(_) => Ok(Self::UnsharedProject),
7614 proto::participant_location::Variant::External(_) => Ok(Self::External),
7615 }
7616 }
7617}
7618/// Workspace-local view of a remote collaborator's state.
7619/// This is the subset of `call::RemoteParticipant` that workspace needs.
7620#[derive(Clone)]
7621pub struct RemoteCollaborator {
7622 pub user: Arc<User>,
7623 pub peer_id: PeerId,
7624 pub location: ParticipantLocation,
7625 pub participant_index: ParticipantIndex,
7626}
7627
7628pub enum ActiveCallEvent {
7629 ParticipantLocationChanged { participant_id: PeerId },
7630 RemoteVideoTracksChanged { participant_id: PeerId },
7631}
7632
7633fn leader_border_for_pane(
7634 follower_states: &HashMap<CollaboratorId, FollowerState>,
7635 pane: &Entity<Pane>,
7636 _: &Window,
7637 cx: &App,
7638) -> Option<Div> {
7639 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
7640 if state.pane() == pane {
7641 Some((*leader_id, state))
7642 } else {
7643 None
7644 }
7645 })?;
7646
7647 let mut leader_color = match leader_id {
7648 CollaboratorId::PeerId(leader_peer_id) => {
7649 let leader = GlobalAnyActiveCall::try_global(cx)?
7650 .0
7651 .remote_participant_for_peer_id(leader_peer_id, cx)?;
7652
7653 cx.theme()
7654 .players()
7655 .color_for_participant(leader.participant_index.0)
7656 .cursor
7657 }
7658 CollaboratorId::Agent => cx.theme().players().agent().cursor,
7659 };
7660 leader_color.fade_out(0.3);
7661 Some(
7662 div()
7663 .absolute()
7664 .size_full()
7665 .left_0()
7666 .top_0()
7667 .border_2()
7668 .border_color(leader_color),
7669 )
7670}
7671
7672fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
7673 ZED_WINDOW_POSITION
7674 .zip(*ZED_WINDOW_SIZE)
7675 .map(|(position, size)| Bounds {
7676 origin: position,
7677 size,
7678 })
7679}
7680
7681fn open_items(
7682 serialized_workspace: Option<SerializedWorkspace>,
7683 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
7684 window: &mut Window,
7685 cx: &mut Context<Workspace>,
7686) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
7687 let restored_items = serialized_workspace.map(|serialized_workspace| {
7688 Workspace::load_workspace(
7689 serialized_workspace,
7690 project_paths_to_open
7691 .iter()
7692 .map(|(_, project_path)| project_path)
7693 .cloned()
7694 .collect(),
7695 window,
7696 cx,
7697 )
7698 });
7699
7700 cx.spawn_in(window, async move |workspace, cx| {
7701 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
7702
7703 if let Some(restored_items) = restored_items {
7704 let restored_items = restored_items.await?;
7705
7706 let restored_project_paths = restored_items
7707 .iter()
7708 .filter_map(|item| {
7709 cx.update(|_, cx| item.as_ref()?.project_path(cx))
7710 .ok()
7711 .flatten()
7712 })
7713 .collect::<HashSet<_>>();
7714
7715 for restored_item in restored_items {
7716 opened_items.push(restored_item.map(Ok));
7717 }
7718
7719 project_paths_to_open
7720 .iter_mut()
7721 .for_each(|(_, project_path)| {
7722 if let Some(project_path_to_open) = project_path
7723 && restored_project_paths.contains(project_path_to_open)
7724 {
7725 *project_path = None;
7726 }
7727 });
7728 } else {
7729 for _ in 0..project_paths_to_open.len() {
7730 opened_items.push(None);
7731 }
7732 }
7733 assert!(opened_items.len() == project_paths_to_open.len());
7734
7735 let tasks =
7736 project_paths_to_open
7737 .into_iter()
7738 .enumerate()
7739 .map(|(ix, (abs_path, project_path))| {
7740 let workspace = workspace.clone();
7741 cx.spawn(async move |cx| {
7742 let file_project_path = project_path?;
7743 let abs_path_task = workspace.update(cx, |workspace, cx| {
7744 workspace.project().update(cx, |project, cx| {
7745 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
7746 })
7747 });
7748
7749 // We only want to open file paths here. If one of the items
7750 // here is a directory, it was already opened further above
7751 // with a `find_or_create_worktree`.
7752 if let Ok(task) = abs_path_task
7753 && task.await.is_none_or(|p| p.is_file())
7754 {
7755 return Some((
7756 ix,
7757 workspace
7758 .update_in(cx, |workspace, window, cx| {
7759 workspace.open_path(
7760 file_project_path,
7761 None,
7762 true,
7763 window,
7764 cx,
7765 )
7766 })
7767 .log_err()?
7768 .await,
7769 ));
7770 }
7771 None
7772 })
7773 });
7774
7775 let tasks = tasks.collect::<Vec<_>>();
7776
7777 let tasks = futures::future::join_all(tasks);
7778 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
7779 opened_items[ix] = Some(path_open_result);
7780 }
7781
7782 Ok(opened_items)
7783 })
7784}
7785
7786#[derive(Clone)]
7787enum ActivateInDirectionTarget {
7788 Pane(Entity<Pane>),
7789 Dock(Entity<Dock>),
7790 Sidebar(FocusHandle),
7791}
7792
7793fn notify_if_database_failed(window: WindowHandle<MultiWorkspace>, cx: &mut AsyncApp) {
7794 window
7795 .update(cx, |multi_workspace, _, cx| {
7796 let workspace = multi_workspace.workspace().clone();
7797 workspace.update(cx, |workspace, cx| {
7798 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
7799 struct DatabaseFailedNotification;
7800
7801 workspace.show_notification(
7802 NotificationId::unique::<DatabaseFailedNotification>(),
7803 cx,
7804 |cx| {
7805 cx.new(|cx| {
7806 MessageNotification::new("Failed to load the database file.", cx)
7807 .primary_message("File an Issue")
7808 .primary_icon(IconName::Plus)
7809 .primary_on_click(|window, cx| {
7810 window.dispatch_action(Box::new(FileBugReport), cx)
7811 })
7812 })
7813 },
7814 );
7815 }
7816 });
7817 })
7818 .log_err();
7819}
7820
7821fn px_with_ui_font_fallback(val: u32, cx: &Context<Workspace>) -> Pixels {
7822 if val == 0 {
7823 ThemeSettings::get_global(cx).ui_font_size(cx)
7824 } else {
7825 px(val as f32)
7826 }
7827}
7828
7829fn adjust_active_dock_size_by_px(
7830 px: Pixels,
7831 workspace: &mut Workspace,
7832 window: &mut Window,
7833 cx: &mut Context<Workspace>,
7834) {
7835 let Some(active_dock) = workspace
7836 .all_docks()
7837 .into_iter()
7838 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx))
7839 else {
7840 return;
7841 };
7842 let dock = active_dock.read(cx);
7843 let Some(panel_size) = workspace.dock_size(&dock, window, cx) else {
7844 return;
7845 };
7846 workspace.resize_dock(dock.position(), panel_size + px, window, cx);
7847}
7848
7849fn adjust_open_docks_size_by_px(
7850 px: Pixels,
7851 workspace: &mut Workspace,
7852 window: &mut Window,
7853 cx: &mut Context<Workspace>,
7854) {
7855 let docks = workspace
7856 .all_docks()
7857 .into_iter()
7858 .filter_map(|dock_entity| {
7859 let dock = dock_entity.read(cx);
7860 if dock.is_open() {
7861 let dock_pos = dock.position();
7862 let panel_size = workspace.dock_size(&dock, window, cx)?;
7863 Some((dock_pos, panel_size + px))
7864 } else {
7865 None
7866 }
7867 })
7868 .collect::<Vec<_>>();
7869
7870 for (position, new_size) in docks {
7871 workspace.resize_dock(position, new_size, window, cx);
7872 }
7873}
7874
7875impl Focusable for Workspace {
7876 fn focus_handle(&self, cx: &App) -> FocusHandle {
7877 self.active_pane.focus_handle(cx)
7878 }
7879}
7880
7881#[derive(Clone)]
7882struct DraggedDock(DockPosition);
7883
7884impl Render for DraggedDock {
7885 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
7886 gpui::Empty
7887 }
7888}
7889
7890impl Render for Workspace {
7891 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
7892 static FIRST_PAINT: AtomicBool = AtomicBool::new(true);
7893 if FIRST_PAINT.swap(false, std::sync::atomic::Ordering::Relaxed) {
7894 log::info!("Rendered first frame");
7895 }
7896
7897 let centered_layout = self.centered_layout
7898 && self.center.panes().len() == 1
7899 && self.active_item(cx).is_some();
7900 let render_padding = |size| {
7901 (size > 0.0).then(|| {
7902 div()
7903 .h_full()
7904 .w(relative(size))
7905 .bg(cx.theme().colors().editor_background)
7906 .border_color(cx.theme().colors().pane_group_border)
7907 })
7908 };
7909 let paddings = if centered_layout {
7910 let settings = WorkspaceSettings::get_global(cx).centered_layout;
7911 (
7912 render_padding(Self::adjust_padding(
7913 settings.left_padding.map(|padding| padding.0),
7914 )),
7915 render_padding(Self::adjust_padding(
7916 settings.right_padding.map(|padding| padding.0),
7917 )),
7918 )
7919 } else {
7920 (None, None)
7921 };
7922 let ui_font = theme_settings::setup_ui_font(window, cx);
7923
7924 let theme = cx.theme().clone();
7925 let colors = theme.colors();
7926 let notification_entities = self
7927 .notifications
7928 .iter()
7929 .map(|(_, notification)| notification.entity_id())
7930 .collect::<Vec<_>>();
7931 let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout;
7932
7933 div()
7934 .relative()
7935 .size_full()
7936 .flex()
7937 .flex_col()
7938 .font(ui_font)
7939 .gap_0()
7940 .justify_start()
7941 .items_start()
7942 .text_color(colors.text)
7943 .overflow_hidden()
7944 .children(self.titlebar_item.clone())
7945 .on_modifiers_changed(move |_, _, cx| {
7946 for &id in ¬ification_entities {
7947 cx.notify(id);
7948 }
7949 })
7950 .child(
7951 div()
7952 .size_full()
7953 .relative()
7954 .flex_1()
7955 .flex()
7956 .flex_col()
7957 .child(
7958 div()
7959 .id("workspace")
7960 .bg(colors.background)
7961 .relative()
7962 .flex_1()
7963 .w_full()
7964 .flex()
7965 .flex_col()
7966 .overflow_hidden()
7967 .border_t_1()
7968 .border_b_1()
7969 .border_color(colors.border)
7970 .child({
7971 let this = cx.entity();
7972 canvas(
7973 move |bounds, window, cx| {
7974 this.update(cx, |this, cx| {
7975 let bounds_changed = this.bounds != bounds;
7976 this.bounds = bounds;
7977
7978 if bounds_changed {
7979 this.left_dock.update(cx, |dock, cx| {
7980 dock.clamp_panel_size(
7981 bounds.size.width,
7982 window,
7983 cx,
7984 )
7985 });
7986
7987 this.right_dock.update(cx, |dock, cx| {
7988 dock.clamp_panel_size(
7989 bounds.size.width,
7990 window,
7991 cx,
7992 )
7993 });
7994
7995 this.bottom_dock.update(cx, |dock, cx| {
7996 dock.clamp_panel_size(
7997 bounds.size.height,
7998 window,
7999 cx,
8000 )
8001 });
8002 }
8003 })
8004 },
8005 |_, _, _, _| {},
8006 )
8007 .absolute()
8008 .size_full()
8009 })
8010 .when(self.zoomed.is_none(), |this| {
8011 this.on_drag_move(cx.listener(
8012 move |workspace,
8013 e: &DragMoveEvent<DraggedDock>,
8014 window,
8015 cx| {
8016 if workspace.previous_dock_drag_coordinates
8017 != Some(e.event.position)
8018 {
8019 workspace.previous_dock_drag_coordinates =
8020 Some(e.event.position);
8021
8022 match e.drag(cx).0 {
8023 DockPosition::Left => {
8024 workspace.resize_left_dock(
8025 e.event.position.x
8026 - workspace.bounds.left(),
8027 window,
8028 cx,
8029 );
8030 }
8031 DockPosition::Right => {
8032 workspace.resize_right_dock(
8033 workspace.bounds.right()
8034 - e.event.position.x,
8035 window,
8036 cx,
8037 );
8038 }
8039 DockPosition::Bottom => {
8040 workspace.resize_bottom_dock(
8041 workspace.bounds.bottom()
8042 - e.event.position.y,
8043 window,
8044 cx,
8045 );
8046 }
8047 };
8048 workspace.serialize_workspace(window, cx);
8049 }
8050 },
8051 ))
8052
8053 })
8054 .child({
8055 match bottom_dock_layout {
8056 BottomDockLayout::Full => div()
8057 .flex()
8058 .flex_col()
8059 .h_full()
8060 .child(
8061 div()
8062 .flex()
8063 .flex_row()
8064 .flex_1()
8065 .overflow_hidden()
8066 .children(self.render_dock(
8067 DockPosition::Left,
8068 &self.left_dock,
8069 window,
8070 cx,
8071 ))
8072
8073 .child(
8074 div()
8075 .flex()
8076 .flex_col()
8077 .flex_1()
8078 .overflow_hidden()
8079 .child(
8080 h_flex()
8081 .flex_1()
8082 .when_some(
8083 paddings.0,
8084 |this, p| {
8085 this.child(
8086 p.border_r_1(),
8087 )
8088 },
8089 )
8090 .child(self.center.render(
8091 self.zoomed.as_ref(),
8092 &PaneRenderContext {
8093 follower_states:
8094 &self.follower_states,
8095 active_call: self.active_call(),
8096 active_pane: &self.active_pane,
8097 app_state: &self.app_state,
8098 project: &self.project,
8099 workspace: &self.weak_self,
8100 },
8101 window,
8102 cx,
8103 ))
8104 .when_some(
8105 paddings.1,
8106 |this, p| {
8107 this.child(
8108 p.border_l_1(),
8109 )
8110 },
8111 ),
8112 ),
8113 )
8114
8115 .children(self.render_dock(
8116 DockPosition::Right,
8117 &self.right_dock,
8118 window,
8119 cx,
8120 )),
8121 )
8122 .child(div().w_full().children(self.render_dock(
8123 DockPosition::Bottom,
8124 &self.bottom_dock,
8125 window,
8126 cx
8127 ))),
8128
8129 BottomDockLayout::LeftAligned => div()
8130 .flex()
8131 .flex_row()
8132 .h_full()
8133 .child(
8134 div()
8135 .flex()
8136 .flex_col()
8137 .flex_1()
8138 .h_full()
8139 .child(
8140 div()
8141 .flex()
8142 .flex_row()
8143 .flex_1()
8144 .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx))
8145
8146 .child(
8147 div()
8148 .flex()
8149 .flex_col()
8150 .flex_1()
8151 .overflow_hidden()
8152 .child(
8153 h_flex()
8154 .flex_1()
8155 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
8156 .child(self.center.render(
8157 self.zoomed.as_ref(),
8158 &PaneRenderContext {
8159 follower_states:
8160 &self.follower_states,
8161 active_call: self.active_call(),
8162 active_pane: &self.active_pane,
8163 app_state: &self.app_state,
8164 project: &self.project,
8165 workspace: &self.weak_self,
8166 },
8167 window,
8168 cx,
8169 ))
8170 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
8171 )
8172 )
8173
8174 )
8175 .child(
8176 div()
8177 .w_full()
8178 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
8179 ),
8180 )
8181 .children(self.render_dock(
8182 DockPosition::Right,
8183 &self.right_dock,
8184 window,
8185 cx,
8186 )),
8187 BottomDockLayout::RightAligned => div()
8188 .flex()
8189 .flex_row()
8190 .h_full()
8191 .children(self.render_dock(
8192 DockPosition::Left,
8193 &self.left_dock,
8194 window,
8195 cx,
8196 ))
8197
8198 .child(
8199 div()
8200 .flex()
8201 .flex_col()
8202 .flex_1()
8203 .h_full()
8204 .child(
8205 div()
8206 .flex()
8207 .flex_row()
8208 .flex_1()
8209 .child(
8210 div()
8211 .flex()
8212 .flex_col()
8213 .flex_1()
8214 .overflow_hidden()
8215 .child(
8216 h_flex()
8217 .flex_1()
8218 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
8219 .child(self.center.render(
8220 self.zoomed.as_ref(),
8221 &PaneRenderContext {
8222 follower_states:
8223 &self.follower_states,
8224 active_call: self.active_call(),
8225 active_pane: &self.active_pane,
8226 app_state: &self.app_state,
8227 project: &self.project,
8228 workspace: &self.weak_self,
8229 },
8230 window,
8231 cx,
8232 ))
8233 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
8234 )
8235 )
8236
8237 .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx))
8238 )
8239 .child(
8240 div()
8241 .w_full()
8242 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
8243 ),
8244 ),
8245 BottomDockLayout::Contained => div()
8246 .flex()
8247 .flex_row()
8248 .h_full()
8249 .children(self.render_dock(
8250 DockPosition::Left,
8251 &self.left_dock,
8252 window,
8253 cx,
8254 ))
8255
8256 .child(
8257 div()
8258 .flex()
8259 .flex_col()
8260 .flex_1()
8261 .overflow_hidden()
8262 .child(
8263 h_flex()
8264 .flex_1()
8265 .when_some(paddings.0, |this, p| {
8266 this.child(p.border_r_1())
8267 })
8268 .child(self.center.render(
8269 self.zoomed.as_ref(),
8270 &PaneRenderContext {
8271 follower_states:
8272 &self.follower_states,
8273 active_call: self.active_call(),
8274 active_pane: &self.active_pane,
8275 app_state: &self.app_state,
8276 project: &self.project,
8277 workspace: &self.weak_self,
8278 },
8279 window,
8280 cx,
8281 ))
8282 .when_some(paddings.1, |this, p| {
8283 this.child(p.border_l_1())
8284 }),
8285 )
8286 .children(self.render_dock(
8287 DockPosition::Bottom,
8288 &self.bottom_dock,
8289 window,
8290 cx,
8291 )),
8292 )
8293
8294 .children(self.render_dock(
8295 DockPosition::Right,
8296 &self.right_dock,
8297 window,
8298 cx,
8299 )),
8300 }
8301 })
8302 .children(self.zoomed.as_ref().and_then(|view| {
8303 let zoomed_view = view.upgrade()?;
8304 let div = div()
8305 .occlude()
8306 .absolute()
8307 .overflow_hidden()
8308 .border_color(colors.border)
8309 .bg(colors.background)
8310 .child(zoomed_view)
8311 .inset_0()
8312 .shadow_lg();
8313
8314 if !WorkspaceSettings::get_global(cx).zoomed_padding {
8315 return Some(div);
8316 }
8317
8318 Some(match self.zoomed_position {
8319 Some(DockPosition::Left) => div.right_2().border_r_1(),
8320 Some(DockPosition::Right) => div.left_2().border_l_1(),
8321 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
8322 None => {
8323 div.top_2().bottom_2().left_2().right_2().border_1()
8324 }
8325 })
8326 }))
8327 .children(self.render_notifications(window, cx)),
8328 )
8329 .when(self.status_bar_visible(cx), |parent| {
8330 parent.child(self.status_bar.clone())
8331 })
8332 .child(self.toast_layer.clone()),
8333 )
8334 }
8335}
8336
8337impl WorkspaceStore {
8338 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
8339 Self {
8340 workspaces: Default::default(),
8341 _subscriptions: vec![
8342 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
8343 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
8344 ],
8345 client,
8346 }
8347 }
8348
8349 pub fn update_followers(
8350 &self,
8351 project_id: Option<u64>,
8352 update: proto::update_followers::Variant,
8353 cx: &App,
8354 ) -> Option<()> {
8355 let active_call = GlobalAnyActiveCall::try_global(cx)?;
8356 let room_id = active_call.0.room_id(cx)?;
8357 self.client
8358 .send(proto::UpdateFollowers {
8359 room_id,
8360 project_id,
8361 variant: Some(update),
8362 })
8363 .log_err()
8364 }
8365
8366 pub async fn handle_follow(
8367 this: Entity<Self>,
8368 envelope: TypedEnvelope<proto::Follow>,
8369 mut cx: AsyncApp,
8370 ) -> Result<proto::FollowResponse> {
8371 this.update(&mut cx, |this, cx| {
8372 let follower = Follower {
8373 project_id: envelope.payload.project_id,
8374 peer_id: envelope.original_sender_id()?,
8375 };
8376
8377 let mut response = proto::FollowResponse::default();
8378
8379 this.workspaces.retain(|(window_handle, weak_workspace)| {
8380 let Some(workspace) = weak_workspace.upgrade() else {
8381 return false;
8382 };
8383 window_handle
8384 .update(cx, |_, window, cx| {
8385 workspace.update(cx, |workspace, cx| {
8386 let handler_response =
8387 workspace.handle_follow(follower.project_id, window, cx);
8388 if let Some(active_view) = handler_response.active_view
8389 && workspace.project.read(cx).remote_id() == follower.project_id
8390 {
8391 response.active_view = Some(active_view)
8392 }
8393 });
8394 })
8395 .is_ok()
8396 });
8397
8398 Ok(response)
8399 })
8400 }
8401
8402 async fn handle_update_followers(
8403 this: Entity<Self>,
8404 envelope: TypedEnvelope<proto::UpdateFollowers>,
8405 mut cx: AsyncApp,
8406 ) -> Result<()> {
8407 let leader_id = envelope.original_sender_id()?;
8408 let update = envelope.payload;
8409
8410 this.update(&mut cx, |this, cx| {
8411 this.workspaces.retain(|(window_handle, weak_workspace)| {
8412 let Some(workspace) = weak_workspace.upgrade() else {
8413 return false;
8414 };
8415 window_handle
8416 .update(cx, |_, window, cx| {
8417 workspace.update(cx, |workspace, cx| {
8418 let project_id = workspace.project.read(cx).remote_id();
8419 if update.project_id != project_id && update.project_id.is_some() {
8420 return;
8421 }
8422 workspace.handle_update_followers(
8423 leader_id,
8424 update.clone(),
8425 window,
8426 cx,
8427 );
8428 });
8429 })
8430 .is_ok()
8431 });
8432 Ok(())
8433 })
8434 }
8435
8436 pub fn workspaces(&self) -> impl Iterator<Item = &WeakEntity<Workspace>> {
8437 self.workspaces.iter().map(|(_, weak)| weak)
8438 }
8439
8440 pub fn workspaces_with_windows(
8441 &self,
8442 ) -> impl Iterator<Item = (gpui::AnyWindowHandle, &WeakEntity<Workspace>)> {
8443 self.workspaces.iter().map(|(window, weak)| (*window, weak))
8444 }
8445}
8446
8447impl ViewId {
8448 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
8449 Ok(Self {
8450 creator: message
8451 .creator
8452 .map(CollaboratorId::PeerId)
8453 .context("creator is missing")?,
8454 id: message.id,
8455 })
8456 }
8457
8458 pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
8459 if let CollaboratorId::PeerId(peer_id) = self.creator {
8460 Some(proto::ViewId {
8461 creator: Some(peer_id),
8462 id: self.id,
8463 })
8464 } else {
8465 None
8466 }
8467 }
8468}
8469
8470impl FollowerState {
8471 fn pane(&self) -> &Entity<Pane> {
8472 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
8473 }
8474}
8475
8476pub trait WorkspaceHandle {
8477 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
8478}
8479
8480impl WorkspaceHandle for Entity<Workspace> {
8481 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
8482 self.read(cx)
8483 .worktrees(cx)
8484 .flat_map(|worktree| {
8485 let worktree_id = worktree.read(cx).id();
8486 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
8487 worktree_id,
8488 path: f.path.clone(),
8489 })
8490 })
8491 .collect::<Vec<_>>()
8492 }
8493}
8494
8495pub async fn last_opened_workspace_location(
8496 db: &WorkspaceDb,
8497 fs: &dyn fs::Fs,
8498) -> Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)> {
8499 db.last_workspace(fs)
8500 .await
8501 .log_err()
8502 .flatten()
8503 .map(|(id, location, paths, _timestamp)| (id, location, paths))
8504}
8505
8506pub async fn last_session_workspace_locations(
8507 db: &WorkspaceDb,
8508 last_session_id: &str,
8509 last_session_window_stack: Option<Vec<WindowId>>,
8510 fs: &dyn fs::Fs,
8511) -> Option<Vec<SessionWorkspace>> {
8512 db.last_session_workspace_locations(last_session_id, last_session_window_stack, fs)
8513 .await
8514 .log_err()
8515}
8516
8517pub struct MultiWorkspaceRestoreResult {
8518 pub window_handle: WindowHandle<MultiWorkspace>,
8519 pub errors: Vec<anyhow::Error>,
8520}
8521
8522pub async fn restore_multiworkspace(
8523 multi_workspace: SerializedMultiWorkspace,
8524 app_state: Arc<AppState>,
8525 cx: &mut AsyncApp,
8526) -> anyhow::Result<MultiWorkspaceRestoreResult> {
8527 let SerializedMultiWorkspace { workspaces, state } = multi_workspace;
8528 let mut group_iter = workspaces.into_iter();
8529 let first = group_iter
8530 .next()
8531 .context("window group must not be empty")?;
8532
8533 let window_handle = if first.paths.is_empty() {
8534 cx.update(|cx| open_workspace_by_id(first.workspace_id, app_state.clone(), None, cx))
8535 .await?
8536 } else {
8537 let OpenResult { window, .. } = cx
8538 .update(|cx| {
8539 Workspace::new_local(
8540 first.paths.paths().to_vec(),
8541 app_state.clone(),
8542 None,
8543 None,
8544 None,
8545 true,
8546 cx,
8547 )
8548 })
8549 .await?;
8550 window
8551 };
8552
8553 let mut errors = Vec::new();
8554
8555 for session_workspace in group_iter {
8556 let error = if session_workspace.paths.is_empty() {
8557 cx.update(|cx| {
8558 open_workspace_by_id(
8559 session_workspace.workspace_id,
8560 app_state.clone(),
8561 Some(window_handle),
8562 cx,
8563 )
8564 })
8565 .await
8566 .err()
8567 } else {
8568 cx.update(|cx| {
8569 Workspace::new_local(
8570 session_workspace.paths.paths().to_vec(),
8571 app_state.clone(),
8572 Some(window_handle),
8573 None,
8574 None,
8575 false,
8576 cx,
8577 )
8578 })
8579 .await
8580 .err()
8581 };
8582
8583 if let Some(error) = error {
8584 errors.push(error);
8585 }
8586 }
8587
8588 if let Some(target_id) = state.active_workspace_id {
8589 window_handle
8590 .update(cx, |multi_workspace, window, cx| {
8591 let target_index = multi_workspace
8592 .workspaces()
8593 .iter()
8594 .position(|ws| ws.read(cx).database_id() == Some(target_id));
8595 if let Some(index) = target_index {
8596 multi_workspace.activate_index(index, window, cx);
8597 } else if !multi_workspace.workspaces().is_empty() {
8598 multi_workspace.activate_index(0, window, cx);
8599 }
8600 })
8601 .ok();
8602 } else {
8603 window_handle
8604 .update(cx, |multi_workspace, window, cx| {
8605 if !multi_workspace.workspaces().is_empty() {
8606 multi_workspace.activate_index(0, window, cx);
8607 }
8608 })
8609 .ok();
8610 }
8611
8612 if state.sidebar_open {
8613 window_handle
8614 .update(cx, |multi_workspace, _, cx| {
8615 multi_workspace.open_sidebar(cx);
8616 })
8617 .ok();
8618 }
8619
8620 window_handle
8621 .update(cx, |_, window, _cx| {
8622 window.activate_window();
8623 })
8624 .ok();
8625
8626 Ok(MultiWorkspaceRestoreResult {
8627 window_handle,
8628 errors,
8629 })
8630}
8631
8632actions!(
8633 collab,
8634 [
8635 /// Opens the channel notes for the current call.
8636 ///
8637 /// Use `collab_panel::OpenSelectedChannelNotes` to open the channel notes for the selected
8638 /// channel in the collab panel.
8639 ///
8640 /// If you want to open a specific channel, use `zed::OpenZedUrl` with a channel notes URL -
8641 /// can be copied via "Copy link to section" in the context menu of the channel notes
8642 /// buffer. These URLs look like `https://zed.dev/channel/channel-name-CHANNEL_ID/notes`.
8643 OpenChannelNotes,
8644 /// Mutes your microphone.
8645 Mute,
8646 /// Deafens yourself (mute both microphone and speakers).
8647 Deafen,
8648 /// Leaves the current call.
8649 LeaveCall,
8650 /// Shares the current project with collaborators.
8651 ShareProject,
8652 /// Shares your screen with collaborators.
8653 ScreenShare,
8654 /// Copies the current room name and session id for debugging purposes.
8655 CopyRoomId,
8656 ]
8657);
8658
8659/// Opens the channel notes for a specific channel by its ID.
8660#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
8661#[action(namespace = collab)]
8662#[serde(deny_unknown_fields)]
8663pub struct OpenChannelNotesById {
8664 pub channel_id: u64,
8665}
8666
8667actions!(
8668 zed,
8669 [
8670 /// Opens the Zed log file.
8671 OpenLog,
8672 /// Reveals the Zed log file in the system file manager.
8673 RevealLogInFileManager
8674 ]
8675);
8676
8677async fn join_channel_internal(
8678 channel_id: ChannelId,
8679 app_state: &Arc<AppState>,
8680 requesting_window: Option<WindowHandle<MultiWorkspace>>,
8681 requesting_workspace: Option<WeakEntity<Workspace>>,
8682 active_call: &dyn AnyActiveCall,
8683 cx: &mut AsyncApp,
8684) -> Result<bool> {
8685 let (should_prompt, already_in_channel) = cx.update(|cx| {
8686 if !active_call.is_in_room(cx) {
8687 return (false, false);
8688 }
8689
8690 let already_in_channel = active_call.channel_id(cx) == Some(channel_id);
8691 let should_prompt = active_call.is_sharing_project(cx)
8692 && active_call.has_remote_participants(cx)
8693 && !already_in_channel;
8694 (should_prompt, already_in_channel)
8695 });
8696
8697 if already_in_channel {
8698 let task = cx.update(|cx| {
8699 if let Some((project, host)) = active_call.most_active_project(cx) {
8700 Some(join_in_room_project(project, host, app_state.clone(), cx))
8701 } else {
8702 None
8703 }
8704 });
8705 if let Some(task) = task {
8706 task.await?;
8707 }
8708 return anyhow::Ok(true);
8709 }
8710
8711 if should_prompt {
8712 if let Some(multi_workspace) = requesting_window {
8713 let answer = multi_workspace
8714 .update(cx, |_, window, cx| {
8715 window.prompt(
8716 PromptLevel::Warning,
8717 "Do you want to switch channels?",
8718 Some("Leaving this call will unshare your current project."),
8719 &["Yes, Join Channel", "Cancel"],
8720 cx,
8721 )
8722 })?
8723 .await;
8724
8725 if answer == Ok(1) {
8726 return Ok(false);
8727 }
8728 } else {
8729 return Ok(false);
8730 }
8731 }
8732
8733 let client = cx.update(|cx| active_call.client(cx));
8734
8735 let mut client_status = client.status();
8736
8737 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
8738 'outer: loop {
8739 let Some(status) = client_status.recv().await else {
8740 anyhow::bail!("error connecting");
8741 };
8742
8743 match status {
8744 Status::Connecting
8745 | Status::Authenticating
8746 | Status::Authenticated
8747 | Status::Reconnecting
8748 | Status::Reauthenticating
8749 | Status::Reauthenticated => continue,
8750 Status::Connected { .. } => break 'outer,
8751 Status::SignedOut | Status::AuthenticationError => {
8752 return Err(ErrorCode::SignedOut.into());
8753 }
8754 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
8755 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
8756 return Err(ErrorCode::Disconnected.into());
8757 }
8758 }
8759 }
8760
8761 let joined = cx
8762 .update(|cx| active_call.join_channel(channel_id, cx))
8763 .await?;
8764
8765 if !joined {
8766 return anyhow::Ok(true);
8767 }
8768
8769 cx.update(|cx| active_call.room_update_completed(cx)).await;
8770
8771 let task = cx.update(|cx| {
8772 if let Some((project, host)) = active_call.most_active_project(cx) {
8773 return Some(join_in_room_project(project, host, app_state.clone(), cx));
8774 }
8775
8776 // If you are the first to join a channel, see if you should share your project.
8777 if !active_call.has_remote_participants(cx)
8778 && !active_call.local_participant_is_guest(cx)
8779 && let Some(workspace) = requesting_workspace.as_ref().and_then(|w| w.upgrade())
8780 {
8781 let project = workspace.update(cx, |workspace, cx| {
8782 let project = workspace.project.read(cx);
8783
8784 if !active_call.share_on_join(cx) {
8785 return None;
8786 }
8787
8788 if (project.is_local() || project.is_via_remote_server())
8789 && project.visible_worktrees(cx).any(|tree| {
8790 tree.read(cx)
8791 .root_entry()
8792 .is_some_and(|entry| entry.is_dir())
8793 })
8794 {
8795 Some(workspace.project.clone())
8796 } else {
8797 None
8798 }
8799 });
8800 if let Some(project) = project {
8801 let share_task = active_call.share_project(project, cx);
8802 return Some(cx.spawn(async move |_cx| -> Result<()> {
8803 share_task.await?;
8804 Ok(())
8805 }));
8806 }
8807 }
8808
8809 None
8810 });
8811 if let Some(task) = task {
8812 task.await?;
8813 return anyhow::Ok(true);
8814 }
8815 anyhow::Ok(false)
8816}
8817
8818pub fn join_channel(
8819 channel_id: ChannelId,
8820 app_state: Arc<AppState>,
8821 requesting_window: Option<WindowHandle<MultiWorkspace>>,
8822 requesting_workspace: Option<WeakEntity<Workspace>>,
8823 cx: &mut App,
8824) -> Task<Result<()>> {
8825 let active_call = GlobalAnyActiveCall::global(cx).clone();
8826 cx.spawn(async move |cx| {
8827 let result = join_channel_internal(
8828 channel_id,
8829 &app_state,
8830 requesting_window,
8831 requesting_workspace,
8832 &*active_call.0,
8833 cx,
8834 )
8835 .await;
8836
8837 // join channel succeeded, and opened a window
8838 if matches!(result, Ok(true)) {
8839 return anyhow::Ok(());
8840 }
8841
8842 // find an existing workspace to focus and show call controls
8843 let mut active_window = requesting_window.or_else(|| activate_any_workspace_window(cx));
8844 if active_window.is_none() {
8845 // no open workspaces, make one to show the error in (blergh)
8846 let OpenResult {
8847 window: window_handle,
8848 ..
8849 } = cx
8850 .update(|cx| {
8851 Workspace::new_local(
8852 vec![],
8853 app_state.clone(),
8854 requesting_window,
8855 None,
8856 None,
8857 true,
8858 cx,
8859 )
8860 })
8861 .await?;
8862
8863 window_handle
8864 .update(cx, |_, window, _cx| {
8865 window.activate_window();
8866 })
8867 .ok();
8868
8869 if result.is_ok() {
8870 cx.update(|cx| {
8871 cx.dispatch_action(&OpenChannelNotes);
8872 });
8873 }
8874
8875 active_window = Some(window_handle);
8876 }
8877
8878 if let Err(err) = result {
8879 log::error!("failed to join channel: {}", err);
8880 if let Some(active_window) = active_window {
8881 active_window
8882 .update(cx, |_, window, cx| {
8883 let detail: SharedString = match err.error_code() {
8884 ErrorCode::SignedOut => "Please sign in to continue.".into(),
8885 ErrorCode::UpgradeRequired => concat!(
8886 "Your are running an unsupported version of Zed. ",
8887 "Please update to continue."
8888 )
8889 .into(),
8890 ErrorCode::NoSuchChannel => concat!(
8891 "No matching channel was found. ",
8892 "Please check the link and try again."
8893 )
8894 .into(),
8895 ErrorCode::Forbidden => concat!(
8896 "This channel is private, and you do not have access. ",
8897 "Please ask someone to add you and try again."
8898 )
8899 .into(),
8900 ErrorCode::Disconnected => {
8901 "Please check your internet connection and try again.".into()
8902 }
8903 _ => format!("{}\n\nPlease try again.", err).into(),
8904 };
8905 window.prompt(
8906 PromptLevel::Critical,
8907 "Failed to join channel",
8908 Some(&detail),
8909 &["Ok"],
8910 cx,
8911 )
8912 })?
8913 .await
8914 .ok();
8915 }
8916 }
8917
8918 // return ok, we showed the error to the user.
8919 anyhow::Ok(())
8920 })
8921}
8922
8923pub async fn get_any_active_multi_workspace(
8924 app_state: Arc<AppState>,
8925 mut cx: AsyncApp,
8926) -> anyhow::Result<WindowHandle<MultiWorkspace>> {
8927 // find an existing workspace to focus and show call controls
8928 let active_window = activate_any_workspace_window(&mut cx);
8929 if active_window.is_none() {
8930 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, None, true, cx))
8931 .await?;
8932 }
8933 activate_any_workspace_window(&mut cx).context("could not open zed")
8934}
8935
8936fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<MultiWorkspace>> {
8937 cx.update(|cx| {
8938 if let Some(workspace_window) = cx
8939 .active_window()
8940 .and_then(|window| window.downcast::<MultiWorkspace>())
8941 {
8942 return Some(workspace_window);
8943 }
8944
8945 for window in cx.windows() {
8946 if let Some(workspace_window) = window.downcast::<MultiWorkspace>() {
8947 workspace_window
8948 .update(cx, |_, window, _| window.activate_window())
8949 .ok();
8950 return Some(workspace_window);
8951 }
8952 }
8953 None
8954 })
8955}
8956
8957pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<MultiWorkspace>> {
8958 workspace_windows_for_location(&SerializedWorkspaceLocation::Local, cx)
8959}
8960
8961pub fn workspace_windows_for_location(
8962 serialized_location: &SerializedWorkspaceLocation,
8963 cx: &App,
8964) -> Vec<WindowHandle<MultiWorkspace>> {
8965 cx.windows()
8966 .into_iter()
8967 .filter_map(|window| window.downcast::<MultiWorkspace>())
8968 .filter(|multi_workspace| {
8969 let same_host = |left: &RemoteConnectionOptions, right: &RemoteConnectionOptions| match (left, right) {
8970 (RemoteConnectionOptions::Ssh(a), RemoteConnectionOptions::Ssh(b)) => {
8971 (&a.host, &a.username, &a.port) == (&b.host, &b.username, &b.port)
8972 }
8973 (RemoteConnectionOptions::Wsl(a), RemoteConnectionOptions::Wsl(b)) => {
8974 // The WSL username is not consistently populated in the workspace location, so ignore it for now.
8975 a.distro_name == b.distro_name
8976 }
8977 (RemoteConnectionOptions::Docker(a), RemoteConnectionOptions::Docker(b)) => {
8978 a.container_id == b.container_id
8979 }
8980 #[cfg(any(test, feature = "test-support"))]
8981 (RemoteConnectionOptions::Mock(a), RemoteConnectionOptions::Mock(b)) => {
8982 a.id == b.id
8983 }
8984 _ => false,
8985 };
8986
8987 multi_workspace.read(cx).is_ok_and(|multi_workspace| {
8988 multi_workspace.workspaces().iter().any(|workspace| {
8989 match workspace.read(cx).workspace_location(cx) {
8990 WorkspaceLocation::Location(location, _) => {
8991 match (&location, serialized_location) {
8992 (
8993 SerializedWorkspaceLocation::Local,
8994 SerializedWorkspaceLocation::Local,
8995 ) => true,
8996 (
8997 SerializedWorkspaceLocation::Remote(a),
8998 SerializedWorkspaceLocation::Remote(b),
8999 ) => same_host(a, b),
9000 _ => false,
9001 }
9002 }
9003 _ => false,
9004 }
9005 })
9006 })
9007 })
9008 .collect()
9009}
9010
9011pub async fn find_existing_workspace(
9012 abs_paths: &[PathBuf],
9013 open_options: &OpenOptions,
9014 location: &SerializedWorkspaceLocation,
9015 cx: &mut AsyncApp,
9016) -> (
9017 Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)>,
9018 OpenVisible,
9019) {
9020 let mut existing: Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)> = None;
9021 let mut open_visible = OpenVisible::All;
9022 let mut best_match = None;
9023
9024 if open_options.open_new_workspace != Some(true) {
9025 cx.update(|cx| {
9026 for window in workspace_windows_for_location(location, cx) {
9027 if let Ok(multi_workspace) = window.read(cx) {
9028 for workspace in multi_workspace.workspaces() {
9029 let project = workspace.read(cx).project.read(cx);
9030 let m = project.visibility_for_paths(
9031 abs_paths,
9032 open_options.open_new_workspace == None,
9033 cx,
9034 );
9035 if m > best_match {
9036 existing = Some((window, workspace.clone()));
9037 best_match = m;
9038 } else if best_match.is_none()
9039 && open_options.open_new_workspace == Some(false)
9040 {
9041 existing = Some((window, workspace.clone()))
9042 }
9043 }
9044 }
9045 }
9046 });
9047
9048 let all_paths_are_files = existing
9049 .as_ref()
9050 .and_then(|(_, target_workspace)| {
9051 cx.update(|cx| {
9052 let workspace = target_workspace.read(cx);
9053 let project = workspace.project.read(cx);
9054 let path_style = workspace.path_style(cx);
9055 Some(!abs_paths.iter().any(|path| {
9056 let path = util::paths::SanitizedPath::new(path);
9057 project.worktrees(cx).any(|worktree| {
9058 let worktree = worktree.read(cx);
9059 let abs_path = worktree.abs_path();
9060 path_style
9061 .strip_prefix(path.as_ref(), abs_path.as_ref())
9062 .and_then(|rel| worktree.entry_for_path(&rel))
9063 .is_some_and(|e| e.is_dir())
9064 })
9065 }))
9066 })
9067 })
9068 .unwrap_or(false);
9069
9070 if open_options.open_new_workspace.is_none()
9071 && existing.is_some()
9072 && open_options.wait
9073 && all_paths_are_files
9074 {
9075 cx.update(|cx| {
9076 let windows = workspace_windows_for_location(location, cx);
9077 let window = cx
9078 .active_window()
9079 .and_then(|window| window.downcast::<MultiWorkspace>())
9080 .filter(|window| windows.contains(window))
9081 .or_else(|| windows.into_iter().next());
9082 if let Some(window) = window {
9083 if let Ok(multi_workspace) = window.read(cx) {
9084 let active_workspace = multi_workspace.workspace().clone();
9085 existing = Some((window, active_workspace));
9086 open_visible = OpenVisible::None;
9087 }
9088 }
9089 });
9090 }
9091 }
9092 (existing, open_visible)
9093}
9094
9095#[derive(Default, Clone)]
9096pub struct OpenOptions {
9097 pub visible: Option<OpenVisible>,
9098 pub focus: Option<bool>,
9099 pub open_new_workspace: Option<bool>,
9100 pub wait: bool,
9101 pub replace_window: Option<WindowHandle<MultiWorkspace>>,
9102 pub env: Option<HashMap<String, String>>,
9103}
9104
9105/// The result of opening a workspace via [`open_paths`], [`Workspace::new_local`],
9106/// or [`Workspace::open_workspace_for_paths`].
9107pub struct OpenResult {
9108 pub window: WindowHandle<MultiWorkspace>,
9109 pub workspace: Entity<Workspace>,
9110 pub opened_items: Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
9111}
9112
9113/// Opens a workspace by its database ID, used for restoring empty workspaces with unsaved content.
9114pub fn open_workspace_by_id(
9115 workspace_id: WorkspaceId,
9116 app_state: Arc<AppState>,
9117 requesting_window: Option<WindowHandle<MultiWorkspace>>,
9118 cx: &mut App,
9119) -> Task<anyhow::Result<WindowHandle<MultiWorkspace>>> {
9120 let project_handle = Project::local(
9121 app_state.client.clone(),
9122 app_state.node_runtime.clone(),
9123 app_state.user_store.clone(),
9124 app_state.languages.clone(),
9125 app_state.fs.clone(),
9126 None,
9127 project::LocalProjectFlags {
9128 init_worktree_trust: true,
9129 ..project::LocalProjectFlags::default()
9130 },
9131 cx,
9132 );
9133
9134 let db = WorkspaceDb::global(cx);
9135 let kvp = db::kvp::KeyValueStore::global(cx);
9136 cx.spawn(async move |cx| {
9137 let serialized_workspace = db
9138 .workspace_for_id(workspace_id)
9139 .with_context(|| format!("Workspace {workspace_id:?} not found"))?;
9140
9141 let centered_layout = serialized_workspace.centered_layout;
9142
9143 let (window, workspace) = if let Some(window) = requesting_window {
9144 let workspace = window.update(cx, |multi_workspace, window, cx| {
9145 let workspace = cx.new(|cx| {
9146 let mut workspace = Workspace::new(
9147 Some(workspace_id),
9148 project_handle.clone(),
9149 app_state.clone(),
9150 window,
9151 cx,
9152 );
9153 workspace.centered_layout = centered_layout;
9154 workspace
9155 });
9156 multi_workspace.add_workspace(workspace.clone(), cx);
9157 workspace
9158 })?;
9159 (window, workspace)
9160 } else {
9161 let window_bounds_override = window_bounds_env_override();
9162
9163 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
9164 (Some(WindowBounds::Windowed(bounds)), None)
9165 } else if let Some(display) = serialized_workspace.display
9166 && let Some(bounds) = serialized_workspace.window_bounds.as_ref()
9167 {
9168 (Some(bounds.0), Some(display))
9169 } else if let Some((display, bounds)) = persistence::read_default_window_bounds(&kvp) {
9170 (Some(bounds), Some(display))
9171 } else {
9172 (None, None)
9173 };
9174
9175 let options = cx.update(|cx| {
9176 let mut options = (app_state.build_window_options)(display, cx);
9177 options.window_bounds = window_bounds;
9178 options
9179 });
9180
9181 let window = cx.open_window(options, {
9182 let app_state = app_state.clone();
9183 let project_handle = project_handle.clone();
9184 move |window, cx| {
9185 let workspace = cx.new(|cx| {
9186 let mut workspace = Workspace::new(
9187 Some(workspace_id),
9188 project_handle,
9189 app_state,
9190 window,
9191 cx,
9192 );
9193 workspace.centered_layout = centered_layout;
9194 workspace
9195 });
9196 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
9197 }
9198 })?;
9199
9200 let workspace = window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
9201 multi_workspace.workspace().clone()
9202 })?;
9203
9204 (window, workspace)
9205 };
9206
9207 notify_if_database_failed(window, cx);
9208
9209 // Restore items from the serialized workspace
9210 window
9211 .update(cx, |_, window, cx| {
9212 workspace.update(cx, |_workspace, cx| {
9213 open_items(Some(serialized_workspace), vec![], window, cx)
9214 })
9215 })?
9216 .await?;
9217
9218 window.update(cx, |_, window, cx| {
9219 workspace.update(cx, |workspace, cx| {
9220 workspace.serialize_workspace(window, cx);
9221 });
9222 })?;
9223
9224 Ok(window)
9225 })
9226}
9227
9228#[allow(clippy::type_complexity)]
9229pub fn open_paths(
9230 abs_paths: &[PathBuf],
9231 app_state: Arc<AppState>,
9232 open_options: OpenOptions,
9233 cx: &mut App,
9234) -> Task<anyhow::Result<OpenResult>> {
9235 let abs_paths = abs_paths.to_vec();
9236 #[cfg(target_os = "windows")]
9237 let wsl_path = abs_paths
9238 .iter()
9239 .find_map(|p| util::paths::WslPath::from_path(p));
9240
9241 cx.spawn(async move |cx| {
9242 let (mut existing, mut open_visible) = find_existing_workspace(
9243 &abs_paths,
9244 &open_options,
9245 &SerializedWorkspaceLocation::Local,
9246 cx,
9247 )
9248 .await;
9249
9250 // Fallback: if no workspace contains the paths and all paths are files,
9251 // prefer an existing local workspace window (active window first).
9252 if open_options.open_new_workspace.is_none() && existing.is_none() {
9253 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
9254 let all_metadatas = futures::future::join_all(all_paths)
9255 .await
9256 .into_iter()
9257 .filter_map(|result| result.ok().flatten())
9258 .collect::<Vec<_>>();
9259
9260 if all_metadatas.iter().all(|file| !file.is_dir) {
9261 cx.update(|cx| {
9262 let windows = workspace_windows_for_location(
9263 &SerializedWorkspaceLocation::Local,
9264 cx,
9265 );
9266 let window = cx
9267 .active_window()
9268 .and_then(|window| window.downcast::<MultiWorkspace>())
9269 .filter(|window| windows.contains(window))
9270 .or_else(|| windows.into_iter().next());
9271 if let Some(window) = window {
9272 if let Ok(multi_workspace) = window.read(cx) {
9273 let active_workspace = multi_workspace.workspace().clone();
9274 existing = Some((window, active_workspace));
9275 open_visible = OpenVisible::None;
9276 }
9277 }
9278 });
9279 }
9280 }
9281
9282 let result = if let Some((existing, target_workspace)) = existing {
9283 let open_task = existing
9284 .update(cx, |multi_workspace, window, cx| {
9285 window.activate_window();
9286 multi_workspace.activate(target_workspace.clone(), cx);
9287 target_workspace.update(cx, |workspace, cx| {
9288 workspace.open_paths(
9289 abs_paths,
9290 OpenOptions {
9291 visible: Some(open_visible),
9292 ..Default::default()
9293 },
9294 None,
9295 window,
9296 cx,
9297 )
9298 })
9299 })?
9300 .await;
9301
9302 _ = existing.update(cx, |multi_workspace, _, cx| {
9303 let workspace = multi_workspace.workspace().clone();
9304 workspace.update(cx, |workspace, cx| {
9305 for item in open_task.iter().flatten() {
9306 if let Err(e) = item {
9307 workspace.show_error(&e, cx);
9308 }
9309 }
9310 });
9311 });
9312
9313 Ok(OpenResult { window: existing, workspace: target_workspace, opened_items: open_task })
9314 } else {
9315 let result = cx
9316 .update(move |cx| {
9317 Workspace::new_local(
9318 abs_paths,
9319 app_state.clone(),
9320 open_options.replace_window,
9321 open_options.env,
9322 None,
9323 true,
9324 cx,
9325 )
9326 })
9327 .await;
9328
9329 if let Ok(ref result) = result {
9330 result.window
9331 .update(cx, |_, window, _cx| {
9332 window.activate_window();
9333 })
9334 .log_err();
9335 }
9336
9337 result
9338 };
9339
9340 #[cfg(target_os = "windows")]
9341 if let Some(util::paths::WslPath{distro, path}) = wsl_path
9342 && let Ok(ref result) = result
9343 {
9344 result.window
9345 .update(cx, move |multi_workspace, _window, cx| {
9346 struct OpenInWsl;
9347 let workspace = multi_workspace.workspace().clone();
9348 workspace.update(cx, |workspace, cx| {
9349 workspace.show_notification(NotificationId::unique::<OpenInWsl>(), cx, move |cx| {
9350 let display_path = util::markdown::MarkdownInlineCode(&path.to_string_lossy());
9351 let msg = format!("{display_path} is inside a WSL filesystem, some features may not work unless you open it with WSL remote");
9352 cx.new(move |cx| {
9353 MessageNotification::new(msg, cx)
9354 .primary_message("Open in WSL")
9355 .primary_icon(IconName::FolderOpen)
9356 .primary_on_click(move |window, cx| {
9357 window.dispatch_action(Box::new(remote::OpenWslPath {
9358 distro: remote::WslConnectionOptions {
9359 distro_name: distro.clone(),
9360 user: None,
9361 },
9362 paths: vec![path.clone().into()],
9363 }), cx)
9364 })
9365 })
9366 });
9367 });
9368 })
9369 .unwrap();
9370 };
9371 result
9372 })
9373}
9374
9375pub fn open_new(
9376 open_options: OpenOptions,
9377 app_state: Arc<AppState>,
9378 cx: &mut App,
9379 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
9380) -> Task<anyhow::Result<()>> {
9381 let task = Workspace::new_local(
9382 Vec::new(),
9383 app_state,
9384 open_options.replace_window,
9385 open_options.env,
9386 Some(Box::new(init)),
9387 true,
9388 cx,
9389 );
9390 cx.spawn(async move |cx| {
9391 let OpenResult { window, .. } = task.await?;
9392 window
9393 .update(cx, |_, window, _cx| {
9394 window.activate_window();
9395 })
9396 .ok();
9397 Ok(())
9398 })
9399}
9400
9401pub fn create_and_open_local_file(
9402 path: &'static Path,
9403 window: &mut Window,
9404 cx: &mut Context<Workspace>,
9405 default_content: impl 'static + Send + FnOnce() -> Rope,
9406) -> Task<Result<Box<dyn ItemHandle>>> {
9407 cx.spawn_in(window, async move |workspace, cx| {
9408 let fs = workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
9409 if !fs.is_file(path).await {
9410 fs.create_file(path, Default::default()).await?;
9411 fs.save(path, &default_content(), Default::default())
9412 .await?;
9413 }
9414
9415 workspace
9416 .update_in(cx, |workspace, window, cx| {
9417 workspace.with_local_or_wsl_workspace(window, cx, |workspace, window, cx| {
9418 let path = workspace
9419 .project
9420 .read_with(cx, |project, cx| project.try_windows_path_to_wsl(path, cx));
9421 cx.spawn_in(window, async move |workspace, cx| {
9422 let path = path.await?;
9423
9424 let path = fs.canonicalize(&path).await.unwrap_or(path);
9425
9426 let mut items = workspace
9427 .update_in(cx, |workspace, window, cx| {
9428 workspace.open_paths(
9429 vec![path.to_path_buf()],
9430 OpenOptions {
9431 visible: Some(OpenVisible::None),
9432 ..Default::default()
9433 },
9434 None,
9435 window,
9436 cx,
9437 )
9438 })?
9439 .await;
9440 let item = items.pop().flatten();
9441 item.with_context(|| format!("path {path:?} is not a file"))?
9442 })
9443 })
9444 })?
9445 .await?
9446 .await
9447 })
9448}
9449
9450pub fn open_remote_project_with_new_connection(
9451 window: WindowHandle<MultiWorkspace>,
9452 remote_connection: Arc<dyn RemoteConnection>,
9453 cancel_rx: oneshot::Receiver<()>,
9454 delegate: Arc<dyn RemoteClientDelegate>,
9455 app_state: Arc<AppState>,
9456 paths: Vec<PathBuf>,
9457 cx: &mut App,
9458) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
9459 cx.spawn(async move |cx| {
9460 let (workspace_id, serialized_workspace) =
9461 deserialize_remote_project(remote_connection.connection_options(), paths.clone(), cx)
9462 .await?;
9463
9464 let session = match cx
9465 .update(|cx| {
9466 remote::RemoteClient::new(
9467 ConnectionIdentifier::Workspace(workspace_id.0),
9468 remote_connection,
9469 cancel_rx,
9470 delegate,
9471 cx,
9472 )
9473 })
9474 .await?
9475 {
9476 Some(result) => result,
9477 None => return Ok(Vec::new()),
9478 };
9479
9480 let project = cx.update(|cx| {
9481 project::Project::remote(
9482 session,
9483 app_state.client.clone(),
9484 app_state.node_runtime.clone(),
9485 app_state.user_store.clone(),
9486 app_state.languages.clone(),
9487 app_state.fs.clone(),
9488 true,
9489 cx,
9490 )
9491 });
9492
9493 open_remote_project_inner(
9494 project,
9495 paths,
9496 workspace_id,
9497 serialized_workspace,
9498 app_state,
9499 window,
9500 cx,
9501 )
9502 .await
9503 })
9504}
9505
9506pub fn open_remote_project_with_existing_connection(
9507 connection_options: RemoteConnectionOptions,
9508 project: Entity<Project>,
9509 paths: Vec<PathBuf>,
9510 app_state: Arc<AppState>,
9511 window: WindowHandle<MultiWorkspace>,
9512 cx: &mut AsyncApp,
9513) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
9514 cx.spawn(async move |cx| {
9515 let (workspace_id, serialized_workspace) =
9516 deserialize_remote_project(connection_options.clone(), paths.clone(), cx).await?;
9517
9518 open_remote_project_inner(
9519 project,
9520 paths,
9521 workspace_id,
9522 serialized_workspace,
9523 app_state,
9524 window,
9525 cx,
9526 )
9527 .await
9528 })
9529}
9530
9531async fn open_remote_project_inner(
9532 project: Entity<Project>,
9533 paths: Vec<PathBuf>,
9534 workspace_id: WorkspaceId,
9535 serialized_workspace: Option<SerializedWorkspace>,
9536 app_state: Arc<AppState>,
9537 window: WindowHandle<MultiWorkspace>,
9538 cx: &mut AsyncApp,
9539) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
9540 let db = cx.update(|cx| WorkspaceDb::global(cx));
9541 let toolchains = db.toolchains(workspace_id).await?;
9542 for (toolchain, worktree_path, path) in toolchains {
9543 project
9544 .update(cx, |this, cx| {
9545 let Some(worktree_id) =
9546 this.find_worktree(&worktree_path, cx)
9547 .and_then(|(worktree, rel_path)| {
9548 if rel_path.is_empty() {
9549 Some(worktree.read(cx).id())
9550 } else {
9551 None
9552 }
9553 })
9554 else {
9555 return Task::ready(None);
9556 };
9557
9558 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
9559 })
9560 .await;
9561 }
9562 let mut project_paths_to_open = vec![];
9563 let mut project_path_errors = vec![];
9564
9565 for path in paths {
9566 let result = cx
9567 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))
9568 .await;
9569 match result {
9570 Ok((_, project_path)) => {
9571 project_paths_to_open.push((path.clone(), Some(project_path)));
9572 }
9573 Err(error) => {
9574 project_path_errors.push(error);
9575 }
9576 };
9577 }
9578
9579 if project_paths_to_open.is_empty() {
9580 return Err(project_path_errors.pop().context("no paths given")?);
9581 }
9582
9583 let workspace = window.update(cx, |multi_workspace, window, cx| {
9584 telemetry::event!("SSH Project Opened");
9585
9586 let new_workspace = cx.new(|cx| {
9587 let mut workspace =
9588 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
9589 workspace.update_history(cx);
9590
9591 if let Some(ref serialized) = serialized_workspace {
9592 workspace.centered_layout = serialized.centered_layout;
9593 }
9594
9595 workspace
9596 });
9597
9598 multi_workspace.activate(new_workspace.clone(), cx);
9599 new_workspace
9600 })?;
9601
9602 let items = window
9603 .update(cx, |_, window, cx| {
9604 window.activate_window();
9605 workspace.update(cx, |_workspace, cx| {
9606 open_items(serialized_workspace, project_paths_to_open, window, cx)
9607 })
9608 })?
9609 .await?;
9610
9611 workspace.update(cx, |workspace, cx| {
9612 for error in project_path_errors {
9613 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
9614 if let Some(path) = error.error_tag("path") {
9615 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
9616 }
9617 } else {
9618 workspace.show_error(&error, cx)
9619 }
9620 }
9621 });
9622
9623 Ok(items.into_iter().map(|item| item?.ok()).collect())
9624}
9625
9626fn deserialize_remote_project(
9627 connection_options: RemoteConnectionOptions,
9628 paths: Vec<PathBuf>,
9629 cx: &AsyncApp,
9630) -> Task<Result<(WorkspaceId, Option<SerializedWorkspace>)>> {
9631 let db = cx.update(|cx| WorkspaceDb::global(cx));
9632 cx.background_spawn(async move {
9633 let remote_connection_id = db
9634 .get_or_create_remote_connection(connection_options)
9635 .await?;
9636
9637 let serialized_workspace = db.remote_workspace_for_roots(&paths, remote_connection_id);
9638
9639 let workspace_id = if let Some(workspace_id) =
9640 serialized_workspace.as_ref().map(|workspace| workspace.id)
9641 {
9642 workspace_id
9643 } else {
9644 db.next_id().await?
9645 };
9646
9647 Ok((workspace_id, serialized_workspace))
9648 })
9649}
9650
9651pub fn join_in_room_project(
9652 project_id: u64,
9653 follow_user_id: u64,
9654 app_state: Arc<AppState>,
9655 cx: &mut App,
9656) -> Task<Result<()>> {
9657 let windows = cx.windows();
9658 cx.spawn(async move |cx| {
9659 let existing_window_and_workspace: Option<(
9660 WindowHandle<MultiWorkspace>,
9661 Entity<Workspace>,
9662 )> = windows.into_iter().find_map(|window_handle| {
9663 window_handle
9664 .downcast::<MultiWorkspace>()
9665 .and_then(|window_handle| {
9666 window_handle
9667 .update(cx, |multi_workspace, _window, cx| {
9668 for workspace in multi_workspace.workspaces() {
9669 if workspace.read(cx).project().read(cx).remote_id()
9670 == Some(project_id)
9671 {
9672 return Some((window_handle, workspace.clone()));
9673 }
9674 }
9675 None
9676 })
9677 .unwrap_or(None)
9678 })
9679 });
9680
9681 let multi_workspace_window = if let Some((existing_window, target_workspace)) =
9682 existing_window_and_workspace
9683 {
9684 existing_window
9685 .update(cx, |multi_workspace, _, cx| {
9686 multi_workspace.activate(target_workspace, cx);
9687 })
9688 .ok();
9689 existing_window
9690 } else {
9691 let active_call = cx.update(|cx| GlobalAnyActiveCall::global(cx).clone());
9692 let project = cx
9693 .update(|cx| {
9694 active_call.0.join_project(
9695 project_id,
9696 app_state.languages.clone(),
9697 app_state.fs.clone(),
9698 cx,
9699 )
9700 })
9701 .await?;
9702
9703 let window_bounds_override = window_bounds_env_override();
9704 cx.update(|cx| {
9705 let mut options = (app_state.build_window_options)(None, cx);
9706 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
9707 cx.open_window(options, |window, cx| {
9708 let workspace = cx.new(|cx| {
9709 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
9710 });
9711 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
9712 })
9713 })?
9714 };
9715
9716 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
9717 cx.activate(true);
9718 window.activate_window();
9719
9720 // We set the active workspace above, so this is the correct workspace.
9721 let workspace = multi_workspace.workspace().clone();
9722 workspace.update(cx, |workspace, cx| {
9723 let follow_peer_id = GlobalAnyActiveCall::try_global(cx)
9724 .and_then(|call| call.0.peer_id_for_user_in_room(follow_user_id, cx))
9725 .or_else(|| {
9726 // If we couldn't follow the given user, follow the host instead.
9727 let collaborator = workspace
9728 .project()
9729 .read(cx)
9730 .collaborators()
9731 .values()
9732 .find(|collaborator| collaborator.is_host)?;
9733 Some(collaborator.peer_id)
9734 });
9735
9736 if let Some(follow_peer_id) = follow_peer_id {
9737 workspace.follow(follow_peer_id, window, cx);
9738 }
9739 });
9740 })?;
9741
9742 anyhow::Ok(())
9743 })
9744}
9745
9746pub fn reload(cx: &mut App) {
9747 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
9748 let mut workspace_windows = cx
9749 .windows()
9750 .into_iter()
9751 .filter_map(|window| window.downcast::<MultiWorkspace>())
9752 .collect::<Vec<_>>();
9753
9754 // If multiple windows have unsaved changes, and need a save prompt,
9755 // prompt in the active window before switching to a different window.
9756 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
9757
9758 let mut prompt = None;
9759 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
9760 prompt = window
9761 .update(cx, |_, window, cx| {
9762 window.prompt(
9763 PromptLevel::Info,
9764 "Are you sure you want to restart?",
9765 None,
9766 &["Restart", "Cancel"],
9767 cx,
9768 )
9769 })
9770 .ok();
9771 }
9772
9773 cx.spawn(async move |cx| {
9774 if let Some(prompt) = prompt {
9775 let answer = prompt.await?;
9776 if answer != 0 {
9777 return anyhow::Ok(());
9778 }
9779 }
9780
9781 // If the user cancels any save prompt, then keep the app open.
9782 for window in workspace_windows {
9783 if let Ok(should_close) = window.update(cx, |multi_workspace, window, cx| {
9784 let workspace = multi_workspace.workspace().clone();
9785 workspace.update(cx, |workspace, cx| {
9786 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
9787 })
9788 }) && !should_close.await?
9789 {
9790 return anyhow::Ok(());
9791 }
9792 }
9793 cx.update(|cx| cx.restart());
9794 anyhow::Ok(())
9795 })
9796 .detach_and_log_err(cx);
9797}
9798
9799fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
9800 let mut parts = value.split(',');
9801 let x: usize = parts.next()?.parse().ok()?;
9802 let y: usize = parts.next()?.parse().ok()?;
9803 Some(point(px(x as f32), px(y as f32)))
9804}
9805
9806fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
9807 let mut parts = value.split(',');
9808 let width: usize = parts.next()?.parse().ok()?;
9809 let height: usize = parts.next()?.parse().ok()?;
9810 Some(size(px(width as f32), px(height as f32)))
9811}
9812
9813/// Add client-side decorations (rounded corners, shadows, resize handling) when
9814/// appropriate.
9815///
9816/// The `border_radius_tiling` parameter allows overriding which corners get
9817/// rounded, independently of the actual window tiling state. This is used
9818/// specifically for the workspace switcher sidebar: when the sidebar is open,
9819/// we want square corners on the left (so the sidebar appears flush with the
9820/// window edge) but we still need the shadow padding for proper visual
9821/// appearance. Unlike actual window tiling, this only affects border radius -
9822/// not padding or shadows.
9823pub fn client_side_decorations(
9824 element: impl IntoElement,
9825 window: &mut Window,
9826 cx: &mut App,
9827 border_radius_tiling: Tiling,
9828) -> Stateful<Div> {
9829 const BORDER_SIZE: Pixels = px(1.0);
9830 let decorations = window.window_decorations();
9831 let tiling = match decorations {
9832 Decorations::Server => Tiling::default(),
9833 Decorations::Client { tiling } => tiling,
9834 };
9835
9836 match decorations {
9837 Decorations::Client { .. } => window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW),
9838 Decorations::Server => window.set_client_inset(px(0.0)),
9839 }
9840
9841 struct GlobalResizeEdge(ResizeEdge);
9842 impl Global for GlobalResizeEdge {}
9843
9844 div()
9845 .id("window-backdrop")
9846 .bg(transparent_black())
9847 .map(|div| match decorations {
9848 Decorations::Server => div,
9849 Decorations::Client { .. } => div
9850 .when(
9851 !(tiling.top
9852 || tiling.right
9853 || border_radius_tiling.top
9854 || border_radius_tiling.right),
9855 |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9856 )
9857 .when(
9858 !(tiling.top
9859 || tiling.left
9860 || border_radius_tiling.top
9861 || border_radius_tiling.left),
9862 |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9863 )
9864 .when(
9865 !(tiling.bottom
9866 || tiling.right
9867 || border_radius_tiling.bottom
9868 || border_radius_tiling.right),
9869 |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9870 )
9871 .when(
9872 !(tiling.bottom
9873 || tiling.left
9874 || border_radius_tiling.bottom
9875 || border_radius_tiling.left),
9876 |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9877 )
9878 .when(!tiling.top, |div| {
9879 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
9880 })
9881 .when(!tiling.bottom, |div| {
9882 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
9883 })
9884 .when(!tiling.left, |div| {
9885 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
9886 })
9887 .when(!tiling.right, |div| {
9888 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
9889 })
9890 .on_mouse_move(move |e, window, cx| {
9891 let size = window.window_bounds().get_bounds().size;
9892 let pos = e.position;
9893
9894 let new_edge =
9895 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
9896
9897 let edge = cx.try_global::<GlobalResizeEdge>();
9898 if new_edge != edge.map(|edge| edge.0) {
9899 window
9900 .window_handle()
9901 .update(cx, |workspace, _, cx| {
9902 cx.notify(workspace.entity_id());
9903 })
9904 .ok();
9905 }
9906 })
9907 .on_mouse_down(MouseButton::Left, move |e, window, _| {
9908 let size = window.window_bounds().get_bounds().size;
9909 let pos = e.position;
9910
9911 let edge = match resize_edge(
9912 pos,
9913 theme::CLIENT_SIDE_DECORATION_SHADOW,
9914 size,
9915 tiling,
9916 ) {
9917 Some(value) => value,
9918 None => return,
9919 };
9920
9921 window.start_window_resize(edge);
9922 }),
9923 })
9924 .size_full()
9925 .child(
9926 div()
9927 .cursor(CursorStyle::Arrow)
9928 .map(|div| match decorations {
9929 Decorations::Server => div,
9930 Decorations::Client { .. } => div
9931 .border_color(cx.theme().colors().border)
9932 .when(
9933 !(tiling.top
9934 || tiling.right
9935 || border_radius_tiling.top
9936 || border_radius_tiling.right),
9937 |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9938 )
9939 .when(
9940 !(tiling.top
9941 || tiling.left
9942 || border_radius_tiling.top
9943 || border_radius_tiling.left),
9944 |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9945 )
9946 .when(
9947 !(tiling.bottom
9948 || tiling.right
9949 || border_radius_tiling.bottom
9950 || border_radius_tiling.right),
9951 |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9952 )
9953 .when(
9954 !(tiling.bottom
9955 || tiling.left
9956 || border_radius_tiling.bottom
9957 || border_radius_tiling.left),
9958 |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9959 )
9960 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
9961 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
9962 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
9963 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
9964 .when(!tiling.is_tiled(), |div| {
9965 div.shadow(vec![gpui::BoxShadow {
9966 color: Hsla {
9967 h: 0.,
9968 s: 0.,
9969 l: 0.,
9970 a: 0.4,
9971 },
9972 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
9973 spread_radius: px(0.),
9974 offset: point(px(0.0), px(0.0)),
9975 }])
9976 }),
9977 })
9978 .on_mouse_move(|_e, _, cx| {
9979 cx.stop_propagation();
9980 })
9981 .size_full()
9982 .child(element),
9983 )
9984 .map(|div| match decorations {
9985 Decorations::Server => div,
9986 Decorations::Client { tiling, .. } => div.child(
9987 canvas(
9988 |_bounds, window, _| {
9989 window.insert_hitbox(
9990 Bounds::new(
9991 point(px(0.0), px(0.0)),
9992 window.window_bounds().get_bounds().size,
9993 ),
9994 HitboxBehavior::Normal,
9995 )
9996 },
9997 move |_bounds, hitbox, window, cx| {
9998 let mouse = window.mouse_position();
9999 let size = window.window_bounds().get_bounds().size;
10000 let Some(edge) =
10001 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
10002 else {
10003 return;
10004 };
10005 cx.set_global(GlobalResizeEdge(edge));
10006 window.set_cursor_style(
10007 match edge {
10008 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
10009 ResizeEdge::Left | ResizeEdge::Right => {
10010 CursorStyle::ResizeLeftRight
10011 }
10012 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
10013 CursorStyle::ResizeUpLeftDownRight
10014 }
10015 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
10016 CursorStyle::ResizeUpRightDownLeft
10017 }
10018 },
10019 &hitbox,
10020 );
10021 },
10022 )
10023 .size_full()
10024 .absolute(),
10025 ),
10026 })
10027}
10028
10029fn resize_edge(
10030 pos: Point<Pixels>,
10031 shadow_size: Pixels,
10032 window_size: Size<Pixels>,
10033 tiling: Tiling,
10034) -> Option<ResizeEdge> {
10035 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
10036 if bounds.contains(&pos) {
10037 return None;
10038 }
10039
10040 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
10041 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
10042 if !tiling.top && top_left_bounds.contains(&pos) {
10043 return Some(ResizeEdge::TopLeft);
10044 }
10045
10046 let top_right_bounds = Bounds::new(
10047 Point::new(window_size.width - corner_size.width, px(0.)),
10048 corner_size,
10049 );
10050 if !tiling.top && top_right_bounds.contains(&pos) {
10051 return Some(ResizeEdge::TopRight);
10052 }
10053
10054 let bottom_left_bounds = Bounds::new(
10055 Point::new(px(0.), window_size.height - corner_size.height),
10056 corner_size,
10057 );
10058 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
10059 return Some(ResizeEdge::BottomLeft);
10060 }
10061
10062 let bottom_right_bounds = Bounds::new(
10063 Point::new(
10064 window_size.width - corner_size.width,
10065 window_size.height - corner_size.height,
10066 ),
10067 corner_size,
10068 );
10069 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
10070 return Some(ResizeEdge::BottomRight);
10071 }
10072
10073 if !tiling.top && pos.y < shadow_size {
10074 Some(ResizeEdge::Top)
10075 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
10076 Some(ResizeEdge::Bottom)
10077 } else if !tiling.left && pos.x < shadow_size {
10078 Some(ResizeEdge::Left)
10079 } else if !tiling.right && pos.x > window_size.width - shadow_size {
10080 Some(ResizeEdge::Right)
10081 } else {
10082 None
10083 }
10084}
10085
10086fn join_pane_into_active(
10087 active_pane: &Entity<Pane>,
10088 pane: &Entity<Pane>,
10089 window: &mut Window,
10090 cx: &mut App,
10091) {
10092 if pane == active_pane {
10093 } else if pane.read(cx).items_len() == 0 {
10094 pane.update(cx, |_, cx| {
10095 cx.emit(pane::Event::Remove {
10096 focus_on_pane: None,
10097 });
10098 })
10099 } else {
10100 move_all_items(pane, active_pane, window, cx);
10101 }
10102}
10103
10104fn move_all_items(
10105 from_pane: &Entity<Pane>,
10106 to_pane: &Entity<Pane>,
10107 window: &mut Window,
10108 cx: &mut App,
10109) {
10110 let destination_is_different = from_pane != to_pane;
10111 let mut moved_items = 0;
10112 for (item_ix, item_handle) in from_pane
10113 .read(cx)
10114 .items()
10115 .enumerate()
10116 .map(|(ix, item)| (ix, item.clone()))
10117 .collect::<Vec<_>>()
10118 {
10119 let ix = item_ix - moved_items;
10120 if destination_is_different {
10121 // Close item from previous pane
10122 from_pane.update(cx, |source, cx| {
10123 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
10124 });
10125 moved_items += 1;
10126 }
10127
10128 // This automatically removes duplicate items in the pane
10129 to_pane.update(cx, |destination, cx| {
10130 destination.add_item(item_handle, true, true, None, window, cx);
10131 window.focus(&destination.focus_handle(cx), cx)
10132 });
10133 }
10134}
10135
10136pub fn move_item(
10137 source: &Entity<Pane>,
10138 destination: &Entity<Pane>,
10139 item_id_to_move: EntityId,
10140 destination_index: usize,
10141 activate: bool,
10142 window: &mut Window,
10143 cx: &mut App,
10144) {
10145 let Some((item_ix, item_handle)) = source
10146 .read(cx)
10147 .items()
10148 .enumerate()
10149 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
10150 .map(|(ix, item)| (ix, item.clone()))
10151 else {
10152 // Tab was closed during drag
10153 return;
10154 };
10155
10156 if source != destination {
10157 // Close item from previous pane
10158 source.update(cx, |source, cx| {
10159 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
10160 });
10161 }
10162
10163 // This automatically removes duplicate items in the pane
10164 destination.update(cx, |destination, cx| {
10165 destination.add_item_inner(
10166 item_handle,
10167 activate,
10168 activate,
10169 activate,
10170 Some(destination_index),
10171 window,
10172 cx,
10173 );
10174 if activate {
10175 window.focus(&destination.focus_handle(cx), cx)
10176 }
10177 });
10178}
10179
10180pub fn move_active_item(
10181 source: &Entity<Pane>,
10182 destination: &Entity<Pane>,
10183 focus_destination: bool,
10184 close_if_empty: bool,
10185 window: &mut Window,
10186 cx: &mut App,
10187) {
10188 if source == destination {
10189 return;
10190 }
10191 let Some(active_item) = source.read(cx).active_item() else {
10192 return;
10193 };
10194 source.update(cx, |source_pane, cx| {
10195 let item_id = active_item.item_id();
10196 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
10197 destination.update(cx, |target_pane, cx| {
10198 target_pane.add_item(
10199 active_item,
10200 focus_destination,
10201 focus_destination,
10202 Some(target_pane.items_len()),
10203 window,
10204 cx,
10205 );
10206 });
10207 });
10208}
10209
10210pub fn clone_active_item(
10211 workspace_id: Option<WorkspaceId>,
10212 source: &Entity<Pane>,
10213 destination: &Entity<Pane>,
10214 focus_destination: bool,
10215 window: &mut Window,
10216 cx: &mut App,
10217) {
10218 if source == destination {
10219 return;
10220 }
10221 let Some(active_item) = source.read(cx).active_item() else {
10222 return;
10223 };
10224 if !active_item.can_split(cx) {
10225 return;
10226 }
10227 let destination = destination.downgrade();
10228 let task = active_item.clone_on_split(workspace_id, window, cx);
10229 window
10230 .spawn(cx, async move |cx| {
10231 let Some(clone) = task.await else {
10232 return;
10233 };
10234 destination
10235 .update_in(cx, |target_pane, window, cx| {
10236 target_pane.add_item(
10237 clone,
10238 focus_destination,
10239 focus_destination,
10240 Some(target_pane.items_len()),
10241 window,
10242 cx,
10243 );
10244 })
10245 .log_err();
10246 })
10247 .detach();
10248}
10249
10250#[derive(Debug)]
10251pub struct WorkspacePosition {
10252 pub window_bounds: Option<WindowBounds>,
10253 pub display: Option<Uuid>,
10254 pub centered_layout: bool,
10255}
10256
10257pub fn remote_workspace_position_from_db(
10258 connection_options: RemoteConnectionOptions,
10259 paths_to_open: &[PathBuf],
10260 cx: &App,
10261) -> Task<Result<WorkspacePosition>> {
10262 let paths = paths_to_open.to_vec();
10263 let db = WorkspaceDb::global(cx);
10264 let kvp = db::kvp::KeyValueStore::global(cx);
10265
10266 cx.background_spawn(async move {
10267 let remote_connection_id = db
10268 .get_or_create_remote_connection(connection_options)
10269 .await
10270 .context("fetching serialized ssh project")?;
10271 let serialized_workspace = db.remote_workspace_for_roots(&paths, remote_connection_id);
10272
10273 let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
10274 (Some(WindowBounds::Windowed(bounds)), None)
10275 } else {
10276 let restorable_bounds = serialized_workspace
10277 .as_ref()
10278 .and_then(|workspace| {
10279 Some((workspace.display?, workspace.window_bounds.map(|b| b.0)?))
10280 })
10281 .or_else(|| persistence::read_default_window_bounds(&kvp));
10282
10283 if let Some((serialized_display, serialized_bounds)) = restorable_bounds {
10284 (Some(serialized_bounds), Some(serialized_display))
10285 } else {
10286 (None, None)
10287 }
10288 };
10289
10290 let centered_layout = serialized_workspace
10291 .as_ref()
10292 .map(|w| w.centered_layout)
10293 .unwrap_or(false);
10294
10295 Ok(WorkspacePosition {
10296 window_bounds,
10297 display,
10298 centered_layout,
10299 })
10300 })
10301}
10302
10303pub fn with_active_or_new_workspace(
10304 cx: &mut App,
10305 f: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + 'static,
10306) {
10307 match cx
10308 .active_window()
10309 .and_then(|w| w.downcast::<MultiWorkspace>())
10310 {
10311 Some(multi_workspace) => {
10312 cx.defer(move |cx| {
10313 multi_workspace
10314 .update(cx, |multi_workspace, window, cx| {
10315 let workspace = multi_workspace.workspace().clone();
10316 workspace.update(cx, |workspace, cx| f(workspace, window, cx));
10317 })
10318 .log_err();
10319 });
10320 }
10321 None => {
10322 let app_state = AppState::global(cx);
10323 if let Some(app_state) = app_state.upgrade() {
10324 open_new(
10325 OpenOptions::default(),
10326 app_state,
10327 cx,
10328 move |workspace, window, cx| f(workspace, window, cx),
10329 )
10330 .detach_and_log_err(cx);
10331 }
10332 }
10333 }
10334}
10335
10336/// Reads a panel's pixel size from its legacy KVP format and deletes the legacy
10337/// key. This migration path only runs once per panel per workspace.
10338fn load_legacy_panel_size(
10339 panel_key: &str,
10340 dock_position: DockPosition,
10341 workspace: &Workspace,
10342 cx: &mut App,
10343) -> Option<Pixels> {
10344 #[derive(Deserialize)]
10345 struct LegacyPanelState {
10346 #[serde(default)]
10347 width: Option<Pixels>,
10348 #[serde(default)]
10349 height: Option<Pixels>,
10350 }
10351
10352 let workspace_id = workspace
10353 .database_id()
10354 .map(|id| i64::from(id).to_string())
10355 .or_else(|| workspace.session_id())?;
10356
10357 let legacy_key = match panel_key {
10358 "ProjectPanel" => {
10359 format!("{}-{:?}", "ProjectPanel", workspace_id)
10360 }
10361 "OutlinePanel" => {
10362 format!("{}-{:?}", "OutlinePanel", workspace_id)
10363 }
10364 "GitPanel" => {
10365 format!("{}-{:?}", "GitPanel", workspace_id)
10366 }
10367 "TerminalPanel" => {
10368 format!("{:?}-{:?}", "TerminalPanel", workspace_id)
10369 }
10370 _ => return None,
10371 };
10372
10373 let kvp = db::kvp::KeyValueStore::global(cx);
10374 let json = kvp.read_kvp(&legacy_key).log_err().flatten()?;
10375 let state = serde_json::from_str::<LegacyPanelState>(&json).log_err()?;
10376 let size = match dock_position {
10377 DockPosition::Bottom => state.height,
10378 DockPosition::Left | DockPosition::Right => state.width,
10379 }?;
10380
10381 cx.background_spawn(async move { kvp.delete_kvp(legacy_key).await })
10382 .detach_and_log_err(cx);
10383
10384 Some(size)
10385}
10386
10387#[cfg(test)]
10388mod tests {
10389 use std::{cell::RefCell, rc::Rc, sync::Arc, time::Duration};
10390
10391 use super::*;
10392 use crate::{
10393 dock::{PanelEvent, test::TestPanel},
10394 item::{
10395 ItemBufferKind, ItemEvent,
10396 test::{TestItem, TestProjectItem},
10397 },
10398 };
10399 use fs::FakeFs;
10400 use gpui::{
10401 DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
10402 UpdateGlobal, VisualTestContext, px,
10403 };
10404 use project::{Project, ProjectEntryId};
10405 use serde_json::json;
10406 use settings::SettingsStore;
10407 use util::path;
10408 use util::rel_path::rel_path;
10409
10410 #[gpui::test]
10411 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
10412 init_test(cx);
10413
10414 let fs = FakeFs::new(cx.executor());
10415 let project = Project::test(fs, [], cx).await;
10416 let (workspace, cx) =
10417 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10418
10419 // Adding an item with no ambiguity renders the tab without detail.
10420 let item1 = cx.new(|cx| {
10421 let mut item = TestItem::new(cx);
10422 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
10423 item
10424 });
10425 workspace.update_in(cx, |workspace, window, cx| {
10426 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
10427 });
10428 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
10429
10430 // Adding an item that creates ambiguity increases the level of detail on
10431 // both tabs.
10432 let item2 = cx.new_window_entity(|_window, cx| {
10433 let mut item = TestItem::new(cx);
10434 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
10435 item
10436 });
10437 workspace.update_in(cx, |workspace, window, cx| {
10438 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10439 });
10440 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10441 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10442
10443 // Adding an item that creates ambiguity increases the level of detail only
10444 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
10445 // we stop at the highest detail available.
10446 let item3 = cx.new(|cx| {
10447 let mut item = TestItem::new(cx);
10448 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
10449 item
10450 });
10451 workspace.update_in(cx, |workspace, window, cx| {
10452 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
10453 });
10454 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10455 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
10456 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
10457 }
10458
10459 #[gpui::test]
10460 async fn test_tracking_active_path(cx: &mut TestAppContext) {
10461 init_test(cx);
10462
10463 let fs = FakeFs::new(cx.executor());
10464 fs.insert_tree(
10465 "/root1",
10466 json!({
10467 "one.txt": "",
10468 "two.txt": "",
10469 }),
10470 )
10471 .await;
10472 fs.insert_tree(
10473 "/root2",
10474 json!({
10475 "three.txt": "",
10476 }),
10477 )
10478 .await;
10479
10480 let project = Project::test(fs, ["root1".as_ref()], cx).await;
10481 let (workspace, cx) =
10482 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10483 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10484 let worktree_id = project.update(cx, |project, cx| {
10485 project.worktrees(cx).next().unwrap().read(cx).id()
10486 });
10487
10488 let item1 = cx.new(|cx| {
10489 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
10490 });
10491 let item2 = cx.new(|cx| {
10492 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
10493 });
10494
10495 // Add an item to an empty pane
10496 workspace.update_in(cx, |workspace, window, cx| {
10497 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
10498 });
10499 project.update(cx, |project, cx| {
10500 assert_eq!(
10501 project.active_entry(),
10502 project
10503 .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
10504 .map(|e| e.id)
10505 );
10506 });
10507 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
10508
10509 // Add a second item to a non-empty pane
10510 workspace.update_in(cx, |workspace, window, cx| {
10511 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
10512 });
10513 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
10514 project.update(cx, |project, cx| {
10515 assert_eq!(
10516 project.active_entry(),
10517 project
10518 .entry_for_path(&(worktree_id, rel_path("two.txt")).into(), cx)
10519 .map(|e| e.id)
10520 );
10521 });
10522
10523 // Close the active item
10524 pane.update_in(cx, |pane, window, cx| {
10525 pane.close_active_item(&Default::default(), window, cx)
10526 })
10527 .await
10528 .unwrap();
10529 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
10530 project.update(cx, |project, cx| {
10531 assert_eq!(
10532 project.active_entry(),
10533 project
10534 .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
10535 .map(|e| e.id)
10536 );
10537 });
10538
10539 // Add a project folder
10540 project
10541 .update(cx, |project, cx| {
10542 project.find_or_create_worktree("root2", true, cx)
10543 })
10544 .await
10545 .unwrap();
10546 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
10547
10548 // Remove a project folder
10549 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
10550 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
10551 }
10552
10553 #[gpui::test]
10554 async fn test_close_window(cx: &mut TestAppContext) {
10555 init_test(cx);
10556
10557 let fs = FakeFs::new(cx.executor());
10558 fs.insert_tree("/root", json!({ "one": "" })).await;
10559
10560 let project = Project::test(fs, ["root".as_ref()], cx).await;
10561 let (workspace, cx) =
10562 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10563
10564 // When there are no dirty items, there's nothing to do.
10565 let item1 = cx.new(TestItem::new);
10566 workspace.update_in(cx, |w, window, cx| {
10567 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
10568 });
10569 let task = workspace.update_in(cx, |w, window, cx| {
10570 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
10571 });
10572 assert!(task.await.unwrap());
10573
10574 // When there are dirty untitled items, prompt to save each one. If the user
10575 // cancels any prompt, then abort.
10576 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
10577 let item3 = cx.new(|cx| {
10578 TestItem::new(cx)
10579 .with_dirty(true)
10580 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10581 });
10582 workspace.update_in(cx, |w, window, cx| {
10583 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10584 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
10585 });
10586 let task = workspace.update_in(cx, |w, window, cx| {
10587 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
10588 });
10589 cx.executor().run_until_parked();
10590 cx.simulate_prompt_answer("Cancel"); // cancel save all
10591 cx.executor().run_until_parked();
10592 assert!(!cx.has_pending_prompt());
10593 assert!(!task.await.unwrap());
10594 }
10595
10596 #[gpui::test]
10597 async fn test_multi_workspace_close_window_multiple_workspaces_cancel(cx: &mut TestAppContext) {
10598 init_test(cx);
10599
10600 let fs = FakeFs::new(cx.executor());
10601 fs.insert_tree("/root", json!({ "one": "" })).await;
10602
10603 let project_a = Project::test(fs.clone(), ["root".as_ref()], cx).await;
10604 let project_b = Project::test(fs, ["root".as_ref()], cx).await;
10605 let multi_workspace_handle =
10606 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
10607 cx.run_until_parked();
10608
10609 let workspace_a = multi_workspace_handle
10610 .read_with(cx, |mw, _| mw.workspace().clone())
10611 .unwrap();
10612
10613 let workspace_b = multi_workspace_handle
10614 .update(cx, |mw, window, cx| {
10615 mw.test_add_workspace(project_b, window, cx)
10616 })
10617 .unwrap();
10618
10619 // Activate workspace A
10620 multi_workspace_handle
10621 .update(cx, |mw, window, cx| {
10622 mw.activate_index(0, window, cx);
10623 })
10624 .unwrap();
10625
10626 let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
10627
10628 // Workspace A has a clean item
10629 let item_a = cx.new(TestItem::new);
10630 workspace_a.update_in(cx, |w, window, cx| {
10631 w.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
10632 });
10633
10634 // Workspace B has a dirty item
10635 let item_b = cx.new(|cx| TestItem::new(cx).with_dirty(true));
10636 workspace_b.update_in(cx, |w, window, cx| {
10637 w.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx)
10638 });
10639
10640 // Verify workspace A is active
10641 multi_workspace_handle
10642 .read_with(cx, |mw, _| {
10643 assert_eq!(mw.active_workspace_index(), 0);
10644 })
10645 .unwrap();
10646
10647 // Dispatch CloseWindow — workspace A will pass, workspace B will prompt
10648 multi_workspace_handle
10649 .update(cx, |mw, window, cx| {
10650 mw.close_window(&CloseWindow, window, cx);
10651 })
10652 .unwrap();
10653 cx.run_until_parked();
10654
10655 // Workspace B should now be active since it has dirty items that need attention
10656 multi_workspace_handle
10657 .read_with(cx, |mw, _| {
10658 assert_eq!(
10659 mw.active_workspace_index(),
10660 1,
10661 "workspace B should be activated when it prompts"
10662 );
10663 })
10664 .unwrap();
10665
10666 // User cancels the save prompt from workspace B
10667 cx.simulate_prompt_answer("Cancel");
10668 cx.run_until_parked();
10669
10670 // Window should still exist because workspace B's close was cancelled
10671 assert!(
10672 multi_workspace_handle.update(cx, |_, _, _| ()).is_ok(),
10673 "window should still exist after cancelling one workspace's close"
10674 );
10675 }
10676
10677 #[gpui::test]
10678 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
10679 init_test(cx);
10680
10681 // Register TestItem as a serializable item
10682 cx.update(|cx| {
10683 register_serializable_item::<TestItem>(cx);
10684 });
10685
10686 let fs = FakeFs::new(cx.executor());
10687 fs.insert_tree("/root", json!({ "one": "" })).await;
10688
10689 let project = Project::test(fs, ["root".as_ref()], cx).await;
10690 let (workspace, cx) =
10691 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10692
10693 // When there are dirty untitled items, but they can serialize, then there is no prompt.
10694 let item1 = cx.new(|cx| {
10695 TestItem::new(cx)
10696 .with_dirty(true)
10697 .with_serialize(|| Some(Task::ready(Ok(()))))
10698 });
10699 let item2 = cx.new(|cx| {
10700 TestItem::new(cx)
10701 .with_dirty(true)
10702 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10703 .with_serialize(|| Some(Task::ready(Ok(()))))
10704 });
10705 workspace.update_in(cx, |w, window, cx| {
10706 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
10707 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10708 });
10709 let task = workspace.update_in(cx, |w, window, cx| {
10710 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
10711 });
10712 assert!(task.await.unwrap());
10713 }
10714
10715 #[gpui::test]
10716 async fn test_close_pane_items(cx: &mut TestAppContext) {
10717 init_test(cx);
10718
10719 let fs = FakeFs::new(cx.executor());
10720
10721 let project = Project::test(fs, None, cx).await;
10722 let (workspace, cx) =
10723 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10724
10725 let item1 = cx.new(|cx| {
10726 TestItem::new(cx)
10727 .with_dirty(true)
10728 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
10729 });
10730 let item2 = cx.new(|cx| {
10731 TestItem::new(cx)
10732 .with_dirty(true)
10733 .with_conflict(true)
10734 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
10735 });
10736 let item3 = cx.new(|cx| {
10737 TestItem::new(cx)
10738 .with_dirty(true)
10739 .with_conflict(true)
10740 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
10741 });
10742 let item4 = cx.new(|cx| {
10743 TestItem::new(cx).with_dirty(true).with_project_items(&[{
10744 let project_item = TestProjectItem::new_untitled(cx);
10745 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
10746 project_item
10747 }])
10748 });
10749 let pane = workspace.update_in(cx, |workspace, window, cx| {
10750 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
10751 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10752 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
10753 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
10754 workspace.active_pane().clone()
10755 });
10756
10757 let close_items = pane.update_in(cx, |pane, window, cx| {
10758 pane.activate_item(1, true, true, window, cx);
10759 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
10760 let item1_id = item1.item_id();
10761 let item3_id = item3.item_id();
10762 let item4_id = item4.item_id();
10763 pane.close_items(window, cx, SaveIntent::Close, &move |id| {
10764 [item1_id, item3_id, item4_id].contains(&id)
10765 })
10766 });
10767 cx.executor().run_until_parked();
10768
10769 assert!(cx.has_pending_prompt());
10770 cx.simulate_prompt_answer("Save all");
10771
10772 cx.executor().run_until_parked();
10773
10774 // Item 1 is saved. There's a prompt to save item 3.
10775 pane.update(cx, |pane, cx| {
10776 assert_eq!(item1.read(cx).save_count, 1);
10777 assert_eq!(item1.read(cx).save_as_count, 0);
10778 assert_eq!(item1.read(cx).reload_count, 0);
10779 assert_eq!(pane.items_len(), 3);
10780 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
10781 });
10782 assert!(cx.has_pending_prompt());
10783
10784 // Cancel saving item 3.
10785 cx.simulate_prompt_answer("Discard");
10786 cx.executor().run_until_parked();
10787
10788 // Item 3 is reloaded. There's a prompt to save item 4.
10789 pane.update(cx, |pane, cx| {
10790 assert_eq!(item3.read(cx).save_count, 0);
10791 assert_eq!(item3.read(cx).save_as_count, 0);
10792 assert_eq!(item3.read(cx).reload_count, 1);
10793 assert_eq!(pane.items_len(), 2);
10794 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
10795 });
10796
10797 // There's a prompt for a path for item 4.
10798 cx.simulate_new_path_selection(|_| Some(Default::default()));
10799 close_items.await.unwrap();
10800
10801 // The requested items are closed.
10802 pane.update(cx, |pane, cx| {
10803 assert_eq!(item4.read(cx).save_count, 0);
10804 assert_eq!(item4.read(cx).save_as_count, 1);
10805 assert_eq!(item4.read(cx).reload_count, 0);
10806 assert_eq!(pane.items_len(), 1);
10807 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
10808 });
10809 }
10810
10811 #[gpui::test]
10812 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
10813 init_test(cx);
10814
10815 let fs = FakeFs::new(cx.executor());
10816 let project = Project::test(fs, [], cx).await;
10817 let (workspace, cx) =
10818 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10819
10820 // Create several workspace items with single project entries, and two
10821 // workspace items with multiple project entries.
10822 let single_entry_items = (0..=4)
10823 .map(|project_entry_id| {
10824 cx.new(|cx| {
10825 TestItem::new(cx)
10826 .with_dirty(true)
10827 .with_project_items(&[dirty_project_item(
10828 project_entry_id,
10829 &format!("{project_entry_id}.txt"),
10830 cx,
10831 )])
10832 })
10833 })
10834 .collect::<Vec<_>>();
10835 let item_2_3 = cx.new(|cx| {
10836 TestItem::new(cx)
10837 .with_dirty(true)
10838 .with_buffer_kind(ItemBufferKind::Multibuffer)
10839 .with_project_items(&[
10840 single_entry_items[2].read(cx).project_items[0].clone(),
10841 single_entry_items[3].read(cx).project_items[0].clone(),
10842 ])
10843 });
10844 let item_3_4 = cx.new(|cx| {
10845 TestItem::new(cx)
10846 .with_dirty(true)
10847 .with_buffer_kind(ItemBufferKind::Multibuffer)
10848 .with_project_items(&[
10849 single_entry_items[3].read(cx).project_items[0].clone(),
10850 single_entry_items[4].read(cx).project_items[0].clone(),
10851 ])
10852 });
10853
10854 // Create two panes that contain the following project entries:
10855 // left pane:
10856 // multi-entry items: (2, 3)
10857 // single-entry items: 0, 2, 3, 4
10858 // right pane:
10859 // single-entry items: 4, 1
10860 // multi-entry items: (3, 4)
10861 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
10862 let left_pane = workspace.active_pane().clone();
10863 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
10864 workspace.add_item_to_active_pane(
10865 single_entry_items[0].boxed_clone(),
10866 None,
10867 true,
10868 window,
10869 cx,
10870 );
10871 workspace.add_item_to_active_pane(
10872 single_entry_items[2].boxed_clone(),
10873 None,
10874 true,
10875 window,
10876 cx,
10877 );
10878 workspace.add_item_to_active_pane(
10879 single_entry_items[3].boxed_clone(),
10880 None,
10881 true,
10882 window,
10883 cx,
10884 );
10885 workspace.add_item_to_active_pane(
10886 single_entry_items[4].boxed_clone(),
10887 None,
10888 true,
10889 window,
10890 cx,
10891 );
10892
10893 let right_pane =
10894 workspace.split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx);
10895
10896 let boxed_clone = single_entry_items[1].boxed_clone();
10897 let right_pane = window.spawn(cx, async move |cx| {
10898 right_pane.await.inspect(|right_pane| {
10899 right_pane
10900 .update_in(cx, |pane, window, cx| {
10901 pane.add_item(boxed_clone, true, true, None, window, cx);
10902 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
10903 })
10904 .unwrap();
10905 })
10906 });
10907
10908 (left_pane, right_pane)
10909 });
10910 let right_pane = right_pane.await.unwrap();
10911 cx.focus(&right_pane);
10912
10913 let close = right_pane.update_in(cx, |pane, window, cx| {
10914 pane.close_all_items(&CloseAllItems::default(), window, cx)
10915 .unwrap()
10916 });
10917 cx.executor().run_until_parked();
10918
10919 let msg = cx.pending_prompt().unwrap().0;
10920 assert!(msg.contains("1.txt"));
10921 assert!(!msg.contains("2.txt"));
10922 assert!(!msg.contains("3.txt"));
10923 assert!(!msg.contains("4.txt"));
10924
10925 // With best-effort close, cancelling item 1 keeps it open but items 4
10926 // and (3,4) still close since their entries exist in left pane.
10927 cx.simulate_prompt_answer("Cancel");
10928 close.await;
10929
10930 right_pane.read_with(cx, |pane, _| {
10931 assert_eq!(pane.items_len(), 1);
10932 });
10933
10934 // Remove item 3 from left pane, making (2,3) the only item with entry 3.
10935 left_pane
10936 .update_in(cx, |left_pane, window, cx| {
10937 left_pane.close_item_by_id(
10938 single_entry_items[3].entity_id(),
10939 SaveIntent::Skip,
10940 window,
10941 cx,
10942 )
10943 })
10944 .await
10945 .unwrap();
10946
10947 let close = left_pane.update_in(cx, |pane, window, cx| {
10948 pane.close_all_items(&CloseAllItems::default(), window, cx)
10949 .unwrap()
10950 });
10951 cx.executor().run_until_parked();
10952
10953 let details = cx.pending_prompt().unwrap().1;
10954 assert!(details.contains("0.txt"));
10955 assert!(details.contains("3.txt"));
10956 assert!(details.contains("4.txt"));
10957 // Ideally 2.txt wouldn't appear since entry 2 still exists in item 2.
10958 // But we can only save whole items, so saving (2,3) for entry 3 includes 2.
10959 // assert!(!details.contains("2.txt"));
10960
10961 cx.simulate_prompt_answer("Save all");
10962 cx.executor().run_until_parked();
10963 close.await;
10964
10965 left_pane.read_with(cx, |pane, _| {
10966 assert_eq!(pane.items_len(), 0);
10967 });
10968 }
10969
10970 #[gpui::test]
10971 async fn test_autosave(cx: &mut gpui::TestAppContext) {
10972 init_test(cx);
10973
10974 let fs = FakeFs::new(cx.executor());
10975 let project = Project::test(fs, [], cx).await;
10976 let (workspace, cx) =
10977 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10978 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10979
10980 let item = cx.new(|cx| {
10981 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10982 });
10983 let item_id = item.entity_id();
10984 workspace.update_in(cx, |workspace, window, cx| {
10985 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
10986 });
10987
10988 // Autosave on window change.
10989 item.update(cx, |item, cx| {
10990 SettingsStore::update_global(cx, |settings, cx| {
10991 settings.update_user_settings(cx, |settings| {
10992 settings.workspace.autosave = Some(AutosaveSetting::OnWindowChange);
10993 })
10994 });
10995 item.is_dirty = true;
10996 });
10997
10998 // Deactivating the window saves the file.
10999 cx.deactivate_window();
11000 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11001
11002 // Re-activating the window doesn't save the file.
11003 cx.update(|window, _| window.activate_window());
11004 cx.executor().run_until_parked();
11005 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11006
11007 // Autosave on focus change.
11008 item.update_in(cx, |item, window, cx| {
11009 cx.focus_self(window);
11010 SettingsStore::update_global(cx, |settings, cx| {
11011 settings.update_user_settings(cx, |settings| {
11012 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11013 })
11014 });
11015 item.is_dirty = true;
11016 });
11017 // Blurring the item saves the file.
11018 item.update_in(cx, |_, window, _| window.blur());
11019 cx.executor().run_until_parked();
11020 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
11021
11022 // Deactivating the window still saves the file.
11023 item.update_in(cx, |item, window, cx| {
11024 cx.focus_self(window);
11025 item.is_dirty = true;
11026 });
11027 cx.deactivate_window();
11028 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
11029
11030 // Autosave after delay.
11031 item.update(cx, |item, cx| {
11032 SettingsStore::update_global(cx, |settings, cx| {
11033 settings.update_user_settings(cx, |settings| {
11034 settings.workspace.autosave = Some(AutosaveSetting::AfterDelay {
11035 milliseconds: 500.into(),
11036 });
11037 })
11038 });
11039 item.is_dirty = true;
11040 cx.emit(ItemEvent::Edit);
11041 });
11042
11043 // Delay hasn't fully expired, so the file is still dirty and unsaved.
11044 cx.executor().advance_clock(Duration::from_millis(250));
11045 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
11046
11047 // After delay expires, the file is saved.
11048 cx.executor().advance_clock(Duration::from_millis(250));
11049 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
11050
11051 // Autosave after delay, should save earlier than delay if tab is closed
11052 item.update(cx, |item, cx| {
11053 item.is_dirty = true;
11054 cx.emit(ItemEvent::Edit);
11055 });
11056 cx.executor().advance_clock(Duration::from_millis(250));
11057 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
11058
11059 // // Ensure auto save with delay saves the item on close, even if the timer hasn't yet run out.
11060 pane.update_in(cx, |pane, window, cx| {
11061 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11062 })
11063 .await
11064 .unwrap();
11065 assert!(!cx.has_pending_prompt());
11066 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
11067
11068 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
11069 workspace.update_in(cx, |workspace, window, cx| {
11070 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11071 });
11072 item.update_in(cx, |item, _window, cx| {
11073 item.is_dirty = true;
11074 for project_item in &mut item.project_items {
11075 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11076 }
11077 });
11078 cx.run_until_parked();
11079 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
11080
11081 // Autosave on focus change, ensuring closing the tab counts as such.
11082 item.update(cx, |item, cx| {
11083 SettingsStore::update_global(cx, |settings, cx| {
11084 settings.update_user_settings(cx, |settings| {
11085 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11086 })
11087 });
11088 item.is_dirty = true;
11089 for project_item in &mut item.project_items {
11090 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11091 }
11092 });
11093
11094 pane.update_in(cx, |pane, window, cx| {
11095 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11096 })
11097 .await
11098 .unwrap();
11099 assert!(!cx.has_pending_prompt());
11100 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11101
11102 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
11103 workspace.update_in(cx, |workspace, window, cx| {
11104 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11105 });
11106 item.update_in(cx, |item, window, cx| {
11107 item.project_items[0].update(cx, |item, _| {
11108 item.entry_id = None;
11109 });
11110 item.is_dirty = true;
11111 window.blur();
11112 });
11113 cx.run_until_parked();
11114 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11115
11116 // Ensure autosave is prevented for deleted files also when closing the buffer.
11117 let _close_items = pane.update_in(cx, |pane, window, cx| {
11118 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11119 });
11120 cx.run_until_parked();
11121 assert!(cx.has_pending_prompt());
11122 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11123 }
11124
11125 #[gpui::test]
11126 async fn test_autosave_on_focus_change_in_multibuffer(cx: &mut gpui::TestAppContext) {
11127 init_test(cx);
11128
11129 let fs = FakeFs::new(cx.executor());
11130 let project = Project::test(fs, [], cx).await;
11131 let (workspace, cx) =
11132 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11133
11134 // Create a multibuffer-like item with two child focus handles,
11135 // simulating individual buffer editors within a multibuffer.
11136 let item = cx.new(|cx| {
11137 TestItem::new(cx)
11138 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11139 .with_child_focus_handles(2, cx)
11140 });
11141 workspace.update_in(cx, |workspace, window, cx| {
11142 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11143 });
11144
11145 // Set autosave to OnFocusChange and focus the first child handle,
11146 // simulating the user's cursor being inside one of the multibuffer's excerpts.
11147 item.update_in(cx, |item, window, cx| {
11148 SettingsStore::update_global(cx, |settings, cx| {
11149 settings.update_user_settings(cx, |settings| {
11150 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11151 })
11152 });
11153 item.is_dirty = true;
11154 window.focus(&item.child_focus_handles[0], cx);
11155 });
11156 cx.executor().run_until_parked();
11157 item.read_with(cx, |item, _| assert_eq!(item.save_count, 0));
11158
11159 // Moving focus from one child to another within the same item should
11160 // NOT trigger autosave — focus is still within the item's focus hierarchy.
11161 item.update_in(cx, |item, window, cx| {
11162 window.focus(&item.child_focus_handles[1], cx);
11163 });
11164 cx.executor().run_until_parked();
11165 item.read_with(cx, |item, _| {
11166 assert_eq!(
11167 item.save_count, 0,
11168 "Switching focus between children within the same item should not autosave"
11169 );
11170 });
11171
11172 // Blurring the item saves the file. This is the core regression scenario:
11173 // with `on_blur`, this would NOT trigger because `on_blur` only fires when
11174 // the item's own focus handle is the leaf that lost focus. In a multibuffer,
11175 // the leaf is always a child focus handle, so `on_blur` never detected
11176 // focus leaving the item.
11177 item.update_in(cx, |_, window, _| window.blur());
11178 cx.executor().run_until_parked();
11179 item.read_with(cx, |item, _| {
11180 assert_eq!(
11181 item.save_count, 1,
11182 "Blurring should trigger autosave when focus was on a child of the item"
11183 );
11184 });
11185
11186 // Deactivating the window should also trigger autosave when a child of
11187 // the multibuffer item currently owns focus.
11188 item.update_in(cx, |item, window, cx| {
11189 item.is_dirty = true;
11190 window.focus(&item.child_focus_handles[0], cx);
11191 });
11192 cx.executor().run_until_parked();
11193 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11194
11195 cx.deactivate_window();
11196 item.read_with(cx, |item, _| {
11197 assert_eq!(
11198 item.save_count, 2,
11199 "Deactivating window should trigger autosave when focus was on a child"
11200 );
11201 });
11202 }
11203
11204 #[gpui::test]
11205 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
11206 init_test(cx);
11207
11208 let fs = FakeFs::new(cx.executor());
11209
11210 let project = Project::test(fs, [], cx).await;
11211 let (workspace, cx) =
11212 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11213
11214 let item = cx.new(|cx| {
11215 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11216 });
11217 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11218 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
11219 let toolbar_notify_count = Rc::new(RefCell::new(0));
11220
11221 workspace.update_in(cx, |workspace, window, cx| {
11222 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11223 let toolbar_notification_count = toolbar_notify_count.clone();
11224 cx.observe_in(&toolbar, window, move |_, _, _, _| {
11225 *toolbar_notification_count.borrow_mut() += 1
11226 })
11227 .detach();
11228 });
11229
11230 pane.read_with(cx, |pane, _| {
11231 assert!(!pane.can_navigate_backward());
11232 assert!(!pane.can_navigate_forward());
11233 });
11234
11235 item.update_in(cx, |item, _, cx| {
11236 item.set_state("one".to_string(), cx);
11237 });
11238
11239 // Toolbar must be notified to re-render the navigation buttons
11240 assert_eq!(*toolbar_notify_count.borrow(), 1);
11241
11242 pane.read_with(cx, |pane, _| {
11243 assert!(pane.can_navigate_backward());
11244 assert!(!pane.can_navigate_forward());
11245 });
11246
11247 workspace
11248 .update_in(cx, |workspace, window, cx| {
11249 workspace.go_back(pane.downgrade(), window, cx)
11250 })
11251 .await
11252 .unwrap();
11253
11254 assert_eq!(*toolbar_notify_count.borrow(), 2);
11255 pane.read_with(cx, |pane, _| {
11256 assert!(!pane.can_navigate_backward());
11257 assert!(pane.can_navigate_forward());
11258 });
11259 }
11260
11261 /// Tests that the navigation history deduplicates entries for the same item.
11262 ///
11263 /// When navigating back and forth between items (e.g., A -> B -> A -> B -> A -> B -> C),
11264 /// the navigation history deduplicates by keeping only the most recent visit to each item,
11265 /// resulting in [A, B, C] instead of [A, B, A, B, A, B, C]. This ensures that Go Back (Ctrl-O)
11266 /// navigates through unique items efficiently: C -> B -> A, rather than bouncing between
11267 /// repeated entries: C -> B -> A -> B -> A -> B -> A.
11268 ///
11269 /// This behavior prevents the navigation history from growing unnecessarily large and provides
11270 /// a better user experience by eliminating redundant navigation steps when jumping between files.
11271 #[gpui::test]
11272 async fn test_navigation_history_deduplication(cx: &mut gpui::TestAppContext) {
11273 init_test(cx);
11274
11275 let fs = FakeFs::new(cx.executor());
11276 let project = Project::test(fs, [], cx).await;
11277 let (workspace, cx) =
11278 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11279
11280 let item_a = cx.new(|cx| {
11281 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "a.txt", cx)])
11282 });
11283 let item_b = cx.new(|cx| {
11284 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "b.txt", cx)])
11285 });
11286 let item_c = cx.new(|cx| {
11287 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "c.txt", cx)])
11288 });
11289
11290 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11291
11292 workspace.update_in(cx, |workspace, window, cx| {
11293 workspace.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx);
11294 workspace.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx);
11295 workspace.add_item_to_active_pane(Box::new(item_c.clone()), None, true, window, cx);
11296 });
11297
11298 workspace.update_in(cx, |workspace, window, cx| {
11299 workspace.activate_item(&item_a, false, false, window, cx);
11300 });
11301 cx.run_until_parked();
11302
11303 workspace.update_in(cx, |workspace, window, cx| {
11304 workspace.activate_item(&item_b, false, false, window, cx);
11305 });
11306 cx.run_until_parked();
11307
11308 workspace.update_in(cx, |workspace, window, cx| {
11309 workspace.activate_item(&item_a, false, false, window, cx);
11310 });
11311 cx.run_until_parked();
11312
11313 workspace.update_in(cx, |workspace, window, cx| {
11314 workspace.activate_item(&item_b, false, false, window, cx);
11315 });
11316 cx.run_until_parked();
11317
11318 workspace.update_in(cx, |workspace, window, cx| {
11319 workspace.activate_item(&item_a, false, false, window, cx);
11320 });
11321 cx.run_until_parked();
11322
11323 workspace.update_in(cx, |workspace, window, cx| {
11324 workspace.activate_item(&item_b, false, false, window, cx);
11325 });
11326 cx.run_until_parked();
11327
11328 workspace.update_in(cx, |workspace, window, cx| {
11329 workspace.activate_item(&item_c, false, false, window, cx);
11330 });
11331 cx.run_until_parked();
11332
11333 let backward_count = pane.read_with(cx, |pane, cx| {
11334 let mut count = 0;
11335 pane.nav_history().for_each_entry(cx, &mut |_, _| {
11336 count += 1;
11337 });
11338 count
11339 });
11340 assert!(
11341 backward_count <= 4,
11342 "Should have at most 4 entries, got {}",
11343 backward_count
11344 );
11345
11346 workspace
11347 .update_in(cx, |workspace, window, cx| {
11348 workspace.go_back(pane.downgrade(), window, cx)
11349 })
11350 .await
11351 .unwrap();
11352
11353 let active_item = workspace.read_with(cx, |workspace, cx| {
11354 workspace.active_item(cx).unwrap().item_id()
11355 });
11356 assert_eq!(
11357 active_item,
11358 item_b.entity_id(),
11359 "After first go_back, should be at item B"
11360 );
11361
11362 workspace
11363 .update_in(cx, |workspace, window, cx| {
11364 workspace.go_back(pane.downgrade(), window, cx)
11365 })
11366 .await
11367 .unwrap();
11368
11369 let active_item = workspace.read_with(cx, |workspace, cx| {
11370 workspace.active_item(cx).unwrap().item_id()
11371 });
11372 assert_eq!(
11373 active_item,
11374 item_a.entity_id(),
11375 "After second go_back, should be at item A"
11376 );
11377
11378 pane.read_with(cx, |pane, _| {
11379 assert!(pane.can_navigate_forward(), "Should be able to go forward");
11380 });
11381 }
11382
11383 #[gpui::test]
11384 async fn test_activate_last_pane(cx: &mut gpui::TestAppContext) {
11385 init_test(cx);
11386 let fs = FakeFs::new(cx.executor());
11387 let project = Project::test(fs, [], cx).await;
11388 let (multi_workspace, cx) =
11389 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
11390 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
11391
11392 workspace.update_in(cx, |workspace, window, cx| {
11393 let first_item = cx.new(|cx| {
11394 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11395 });
11396 workspace.add_item_to_active_pane(Box::new(first_item), None, true, window, cx);
11397 workspace.split_pane(
11398 workspace.active_pane().clone(),
11399 SplitDirection::Right,
11400 window,
11401 cx,
11402 );
11403 workspace.split_pane(
11404 workspace.active_pane().clone(),
11405 SplitDirection::Right,
11406 window,
11407 cx,
11408 );
11409 });
11410
11411 let (first_pane_id, target_last_pane_id) = workspace.update(cx, |workspace, _cx| {
11412 let panes = workspace.center.panes();
11413 assert!(panes.len() >= 2);
11414 (
11415 panes.first().expect("at least one pane").entity_id(),
11416 panes.last().expect("at least one pane").entity_id(),
11417 )
11418 });
11419
11420 workspace.update_in(cx, |workspace, window, cx| {
11421 workspace.activate_pane_at_index(&ActivatePane(0), window, cx);
11422 });
11423 workspace.update(cx, |workspace, _| {
11424 assert_eq!(workspace.active_pane().entity_id(), first_pane_id);
11425 assert_ne!(workspace.active_pane().entity_id(), target_last_pane_id);
11426 });
11427
11428 cx.dispatch_action(ActivateLastPane);
11429
11430 workspace.update(cx, |workspace, _| {
11431 assert_eq!(workspace.active_pane().entity_id(), target_last_pane_id);
11432 });
11433 }
11434
11435 #[gpui::test]
11436 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
11437 init_test(cx);
11438 let fs = FakeFs::new(cx.executor());
11439
11440 let project = Project::test(fs, [], cx).await;
11441 let (workspace, cx) =
11442 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11443
11444 let panel = workspace.update_in(cx, |workspace, window, cx| {
11445 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
11446 workspace.add_panel(panel.clone(), window, cx);
11447
11448 workspace
11449 .right_dock()
11450 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
11451
11452 panel
11453 });
11454
11455 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11456 pane.update_in(cx, |pane, window, cx| {
11457 let item = cx.new(TestItem::new);
11458 pane.add_item(Box::new(item), true, true, None, window, cx);
11459 });
11460
11461 // Transfer focus from center to panel
11462 workspace.update_in(cx, |workspace, window, cx| {
11463 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11464 });
11465
11466 workspace.update_in(cx, |workspace, window, cx| {
11467 assert!(workspace.right_dock().read(cx).is_open());
11468 assert!(!panel.is_zoomed(window, cx));
11469 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11470 });
11471
11472 // Transfer focus from panel to center
11473 workspace.update_in(cx, |workspace, window, cx| {
11474 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11475 });
11476
11477 workspace.update_in(cx, |workspace, window, cx| {
11478 assert!(workspace.right_dock().read(cx).is_open());
11479 assert!(!panel.is_zoomed(window, cx));
11480 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11481 assert!(pane.read(cx).focus_handle(cx).contains_focused(window, cx));
11482 });
11483
11484 // Close the dock
11485 workspace.update_in(cx, |workspace, window, cx| {
11486 workspace.toggle_dock(DockPosition::Right, window, cx);
11487 });
11488
11489 workspace.update_in(cx, |workspace, window, cx| {
11490 assert!(!workspace.right_dock().read(cx).is_open());
11491 assert!(!panel.is_zoomed(window, cx));
11492 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11493 assert!(pane.read(cx).focus_handle(cx).contains_focused(window, cx));
11494 });
11495
11496 // Open the dock
11497 workspace.update_in(cx, |workspace, window, cx| {
11498 workspace.toggle_dock(DockPosition::Right, window, cx);
11499 });
11500
11501 workspace.update_in(cx, |workspace, window, cx| {
11502 assert!(workspace.right_dock().read(cx).is_open());
11503 assert!(!panel.is_zoomed(window, cx));
11504 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11505 });
11506
11507 // Focus and zoom panel
11508 panel.update_in(cx, |panel, window, cx| {
11509 cx.focus_self(window);
11510 panel.set_zoomed(true, window, cx)
11511 });
11512
11513 workspace.update_in(cx, |workspace, window, cx| {
11514 assert!(workspace.right_dock().read(cx).is_open());
11515 assert!(panel.is_zoomed(window, cx));
11516 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11517 });
11518
11519 // Transfer focus to the center closes the dock
11520 workspace.update_in(cx, |workspace, window, cx| {
11521 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11522 });
11523
11524 workspace.update_in(cx, |workspace, window, cx| {
11525 assert!(!workspace.right_dock().read(cx).is_open());
11526 assert!(panel.is_zoomed(window, cx));
11527 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11528 });
11529
11530 // Transferring focus back to the panel keeps it zoomed
11531 workspace.update_in(cx, |workspace, window, cx| {
11532 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11533 });
11534
11535 workspace.update_in(cx, |workspace, window, cx| {
11536 assert!(workspace.right_dock().read(cx).is_open());
11537 assert!(panel.is_zoomed(window, cx));
11538 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11539 });
11540
11541 // Close the dock while it is zoomed
11542 workspace.update_in(cx, |workspace, window, cx| {
11543 workspace.toggle_dock(DockPosition::Right, window, cx)
11544 });
11545
11546 workspace.update_in(cx, |workspace, window, cx| {
11547 assert!(!workspace.right_dock().read(cx).is_open());
11548 assert!(panel.is_zoomed(window, cx));
11549 assert!(workspace.zoomed.is_none());
11550 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11551 });
11552
11553 // Opening the dock, when it's zoomed, retains focus
11554 workspace.update_in(cx, |workspace, window, cx| {
11555 workspace.toggle_dock(DockPosition::Right, window, cx)
11556 });
11557
11558 workspace.update_in(cx, |workspace, window, cx| {
11559 assert!(workspace.right_dock().read(cx).is_open());
11560 assert!(panel.is_zoomed(window, cx));
11561 assert!(workspace.zoomed.is_some());
11562 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11563 });
11564
11565 // Unzoom and close the panel, zoom the active pane.
11566 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
11567 workspace.update_in(cx, |workspace, window, cx| {
11568 workspace.toggle_dock(DockPosition::Right, window, cx)
11569 });
11570 pane.update_in(cx, |pane, window, cx| {
11571 pane.toggle_zoom(&Default::default(), window, cx)
11572 });
11573
11574 // Opening a dock unzooms the pane.
11575 workspace.update_in(cx, |workspace, window, cx| {
11576 workspace.toggle_dock(DockPosition::Right, window, cx)
11577 });
11578 workspace.update_in(cx, |workspace, window, cx| {
11579 let pane = pane.read(cx);
11580 assert!(!pane.is_zoomed());
11581 assert!(!pane.focus_handle(cx).is_focused(window));
11582 assert!(workspace.right_dock().read(cx).is_open());
11583 assert!(workspace.zoomed.is_none());
11584 });
11585 }
11586
11587 #[gpui::test]
11588 async fn test_close_panel_on_toggle(cx: &mut gpui::TestAppContext) {
11589 init_test(cx);
11590 let fs = FakeFs::new(cx.executor());
11591
11592 let project = Project::test(fs, [], cx).await;
11593 let (workspace, cx) =
11594 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11595
11596 let panel = workspace.update_in(cx, |workspace, window, cx| {
11597 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
11598 workspace.add_panel(panel.clone(), window, cx);
11599 panel
11600 });
11601
11602 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11603 pane.update_in(cx, |pane, window, cx| {
11604 let item = cx.new(TestItem::new);
11605 pane.add_item(Box::new(item), true, true, None, window, cx);
11606 });
11607
11608 // Enable close_panel_on_toggle
11609 cx.update_global(|store: &mut SettingsStore, cx| {
11610 store.update_user_settings(cx, |settings| {
11611 settings.workspace.close_panel_on_toggle = Some(true);
11612 });
11613 });
11614
11615 // Panel starts closed. Toggling should open and focus it.
11616 workspace.update_in(cx, |workspace, window, cx| {
11617 assert!(!workspace.right_dock().read(cx).is_open());
11618 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11619 });
11620
11621 workspace.update_in(cx, |workspace, window, cx| {
11622 assert!(
11623 workspace.right_dock().read(cx).is_open(),
11624 "Dock should be open after toggling from center"
11625 );
11626 assert!(
11627 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
11628 "Panel should be focused after toggling from center"
11629 );
11630 });
11631
11632 // Panel is open and focused. Toggling should close the panel and
11633 // return focus to the center.
11634 workspace.update_in(cx, |workspace, window, cx| {
11635 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11636 });
11637
11638 workspace.update_in(cx, |workspace, window, cx| {
11639 assert!(
11640 !workspace.right_dock().read(cx).is_open(),
11641 "Dock should be closed after toggling from focused panel"
11642 );
11643 assert!(
11644 !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
11645 "Panel should not be focused after toggling from focused panel"
11646 );
11647 });
11648
11649 // Open the dock and focus something else so the panel is open but not
11650 // focused. Toggling should focus the panel (not close it).
11651 workspace.update_in(cx, |workspace, window, cx| {
11652 workspace
11653 .right_dock()
11654 .update(cx, |dock, cx| dock.set_open(true, window, cx));
11655 window.focus(&pane.read(cx).focus_handle(cx), cx);
11656 });
11657
11658 workspace.update_in(cx, |workspace, window, cx| {
11659 assert!(workspace.right_dock().read(cx).is_open());
11660 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11661 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11662 });
11663
11664 workspace.update_in(cx, |workspace, window, cx| {
11665 assert!(
11666 workspace.right_dock().read(cx).is_open(),
11667 "Dock should remain open when toggling focuses an open-but-unfocused panel"
11668 );
11669 assert!(
11670 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
11671 "Panel should be focused after toggling an open-but-unfocused panel"
11672 );
11673 });
11674
11675 // Now disable the setting and verify the original behavior: toggling
11676 // from a focused panel moves focus to center but leaves the dock open.
11677 cx.update_global(|store: &mut SettingsStore, cx| {
11678 store.update_user_settings(cx, |settings| {
11679 settings.workspace.close_panel_on_toggle = Some(false);
11680 });
11681 });
11682
11683 workspace.update_in(cx, |workspace, window, cx| {
11684 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11685 });
11686
11687 workspace.update_in(cx, |workspace, window, cx| {
11688 assert!(
11689 workspace.right_dock().read(cx).is_open(),
11690 "Dock should remain open when setting is disabled"
11691 );
11692 assert!(
11693 !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
11694 "Panel should not be focused after toggling with setting disabled"
11695 );
11696 });
11697 }
11698
11699 #[gpui::test]
11700 async fn test_pane_zoom_in_out(cx: &mut TestAppContext) {
11701 init_test(cx);
11702 let fs = FakeFs::new(cx.executor());
11703
11704 let project = Project::test(fs, [], cx).await;
11705 let (workspace, cx) =
11706 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11707
11708 let pane = workspace.update_in(cx, |workspace, _window, _cx| {
11709 workspace.active_pane().clone()
11710 });
11711
11712 // Add an item to the pane so it can be zoomed
11713 workspace.update_in(cx, |workspace, window, cx| {
11714 let item = cx.new(TestItem::new);
11715 workspace.add_item(pane.clone(), Box::new(item), None, true, true, window, cx);
11716 });
11717
11718 // Initially not zoomed
11719 workspace.update_in(cx, |workspace, _window, cx| {
11720 assert!(!pane.read(cx).is_zoomed(), "Pane starts unzoomed");
11721 assert!(
11722 workspace.zoomed.is_none(),
11723 "Workspace should track no zoomed pane"
11724 );
11725 assert!(pane.read(cx).items_len() > 0, "Pane should have items");
11726 });
11727
11728 // Zoom In
11729 pane.update_in(cx, |pane, window, cx| {
11730 pane.zoom_in(&crate::ZoomIn, window, cx);
11731 });
11732
11733 workspace.update_in(cx, |workspace, window, cx| {
11734 assert!(
11735 pane.read(cx).is_zoomed(),
11736 "Pane should be zoomed after ZoomIn"
11737 );
11738 assert!(
11739 workspace.zoomed.is_some(),
11740 "Workspace should track the zoomed pane"
11741 );
11742 assert!(
11743 pane.read(cx).focus_handle(cx).contains_focused(window, cx),
11744 "ZoomIn should focus the pane"
11745 );
11746 });
11747
11748 // Zoom In again is a no-op
11749 pane.update_in(cx, |pane, window, cx| {
11750 pane.zoom_in(&crate::ZoomIn, window, cx);
11751 });
11752
11753 workspace.update_in(cx, |workspace, window, cx| {
11754 assert!(pane.read(cx).is_zoomed(), "Second ZoomIn keeps pane zoomed");
11755 assert!(
11756 workspace.zoomed.is_some(),
11757 "Workspace still tracks zoomed pane"
11758 );
11759 assert!(
11760 pane.read(cx).focus_handle(cx).contains_focused(window, cx),
11761 "Pane remains focused after repeated ZoomIn"
11762 );
11763 });
11764
11765 // Zoom Out
11766 pane.update_in(cx, |pane, window, cx| {
11767 pane.zoom_out(&crate::ZoomOut, window, cx);
11768 });
11769
11770 workspace.update_in(cx, |workspace, _window, cx| {
11771 assert!(
11772 !pane.read(cx).is_zoomed(),
11773 "Pane should unzoom after ZoomOut"
11774 );
11775 assert!(
11776 workspace.zoomed.is_none(),
11777 "Workspace clears zoom tracking after ZoomOut"
11778 );
11779 });
11780
11781 // Zoom Out again is a no-op
11782 pane.update_in(cx, |pane, window, cx| {
11783 pane.zoom_out(&crate::ZoomOut, window, cx);
11784 });
11785
11786 workspace.update_in(cx, |workspace, _window, cx| {
11787 assert!(
11788 !pane.read(cx).is_zoomed(),
11789 "Second ZoomOut keeps pane unzoomed"
11790 );
11791 assert!(
11792 workspace.zoomed.is_none(),
11793 "Workspace remains without zoomed pane"
11794 );
11795 });
11796 }
11797
11798 #[gpui::test]
11799 async fn test_toggle_all_docks(cx: &mut gpui::TestAppContext) {
11800 init_test(cx);
11801 let fs = FakeFs::new(cx.executor());
11802
11803 let project = Project::test(fs, [], cx).await;
11804 let (workspace, cx) =
11805 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11806 workspace.update_in(cx, |workspace, window, cx| {
11807 // Open two docks
11808 let left_dock = workspace.dock_at_position(DockPosition::Left);
11809 let right_dock = workspace.dock_at_position(DockPosition::Right);
11810
11811 left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
11812 right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
11813
11814 assert!(left_dock.read(cx).is_open());
11815 assert!(right_dock.read(cx).is_open());
11816 });
11817
11818 workspace.update_in(cx, |workspace, window, cx| {
11819 // Toggle all docks - should close both
11820 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11821
11822 let left_dock = workspace.dock_at_position(DockPosition::Left);
11823 let right_dock = workspace.dock_at_position(DockPosition::Right);
11824 assert!(!left_dock.read(cx).is_open());
11825 assert!(!right_dock.read(cx).is_open());
11826 });
11827
11828 workspace.update_in(cx, |workspace, window, cx| {
11829 // Toggle again - should reopen both
11830 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11831
11832 let left_dock = workspace.dock_at_position(DockPosition::Left);
11833 let right_dock = workspace.dock_at_position(DockPosition::Right);
11834 assert!(left_dock.read(cx).is_open());
11835 assert!(right_dock.read(cx).is_open());
11836 });
11837 }
11838
11839 #[gpui::test]
11840 async fn test_toggle_all_with_manual_close(cx: &mut gpui::TestAppContext) {
11841 init_test(cx);
11842 let fs = FakeFs::new(cx.executor());
11843
11844 let project = Project::test(fs, [], cx).await;
11845 let (workspace, cx) =
11846 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11847 workspace.update_in(cx, |workspace, window, cx| {
11848 // Open two docks
11849 let left_dock = workspace.dock_at_position(DockPosition::Left);
11850 let right_dock = workspace.dock_at_position(DockPosition::Right);
11851
11852 left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
11853 right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
11854
11855 assert!(left_dock.read(cx).is_open());
11856 assert!(right_dock.read(cx).is_open());
11857 });
11858
11859 workspace.update_in(cx, |workspace, window, cx| {
11860 // Close them manually
11861 workspace.toggle_dock(DockPosition::Left, window, cx);
11862 workspace.toggle_dock(DockPosition::Right, window, cx);
11863
11864 let left_dock = workspace.dock_at_position(DockPosition::Left);
11865 let right_dock = workspace.dock_at_position(DockPosition::Right);
11866 assert!(!left_dock.read(cx).is_open());
11867 assert!(!right_dock.read(cx).is_open());
11868 });
11869
11870 workspace.update_in(cx, |workspace, window, cx| {
11871 // Toggle all docks - only last closed (right dock) should reopen
11872 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11873
11874 let left_dock = workspace.dock_at_position(DockPosition::Left);
11875 let right_dock = workspace.dock_at_position(DockPosition::Right);
11876 assert!(!left_dock.read(cx).is_open());
11877 assert!(right_dock.read(cx).is_open());
11878 });
11879 }
11880
11881 #[gpui::test]
11882 async fn test_toggle_all_docks_after_dock_move(cx: &mut gpui::TestAppContext) {
11883 init_test(cx);
11884 let fs = FakeFs::new(cx.executor());
11885 let project = Project::test(fs, [], cx).await;
11886 let (multi_workspace, cx) =
11887 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
11888 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
11889
11890 // Open two docks (left and right) with one panel each
11891 let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| {
11892 let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
11893 workspace.add_panel(left_panel.clone(), window, cx);
11894
11895 let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
11896 workspace.add_panel(right_panel.clone(), window, cx);
11897
11898 workspace.toggle_dock(DockPosition::Left, window, cx);
11899 workspace.toggle_dock(DockPosition::Right, window, cx);
11900
11901 // Verify initial state
11902 assert!(
11903 workspace.left_dock().read(cx).is_open(),
11904 "Left dock should be open"
11905 );
11906 assert_eq!(
11907 workspace
11908 .left_dock()
11909 .read(cx)
11910 .visible_panel()
11911 .unwrap()
11912 .panel_id(),
11913 left_panel.panel_id(),
11914 "Left panel should be visible in left dock"
11915 );
11916 assert!(
11917 workspace.right_dock().read(cx).is_open(),
11918 "Right dock should be open"
11919 );
11920 assert_eq!(
11921 workspace
11922 .right_dock()
11923 .read(cx)
11924 .visible_panel()
11925 .unwrap()
11926 .panel_id(),
11927 right_panel.panel_id(),
11928 "Right panel should be visible in right dock"
11929 );
11930 assert!(
11931 !workspace.bottom_dock().read(cx).is_open(),
11932 "Bottom dock should be closed"
11933 );
11934
11935 (left_panel, right_panel)
11936 });
11937
11938 // Focus the left panel and move it to the next position (bottom dock)
11939 workspace.update_in(cx, |workspace, window, cx| {
11940 workspace.toggle_panel_focus::<TestPanel>(window, cx); // Focus left panel
11941 assert!(
11942 left_panel.read(cx).focus_handle(cx).is_focused(window),
11943 "Left panel should be focused"
11944 );
11945 });
11946
11947 cx.dispatch_action(MoveFocusedPanelToNextPosition);
11948
11949 // Verify the left panel has moved to the bottom dock, and the bottom dock is now open
11950 workspace.update(cx, |workspace, cx| {
11951 assert!(
11952 !workspace.left_dock().read(cx).is_open(),
11953 "Left dock should be closed"
11954 );
11955 assert!(
11956 workspace.bottom_dock().read(cx).is_open(),
11957 "Bottom dock should now be open"
11958 );
11959 assert_eq!(
11960 left_panel.read(cx).position,
11961 DockPosition::Bottom,
11962 "Left panel should now be in the bottom dock"
11963 );
11964 assert_eq!(
11965 workspace
11966 .bottom_dock()
11967 .read(cx)
11968 .visible_panel()
11969 .unwrap()
11970 .panel_id(),
11971 left_panel.panel_id(),
11972 "Left panel should be the visible panel in the bottom dock"
11973 );
11974 });
11975
11976 // Toggle all docks off
11977 workspace.update_in(cx, |workspace, window, cx| {
11978 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11979 assert!(
11980 !workspace.left_dock().read(cx).is_open(),
11981 "Left dock should be closed"
11982 );
11983 assert!(
11984 !workspace.right_dock().read(cx).is_open(),
11985 "Right dock should be closed"
11986 );
11987 assert!(
11988 !workspace.bottom_dock().read(cx).is_open(),
11989 "Bottom dock should be closed"
11990 );
11991 });
11992
11993 // Toggle all docks back on and verify positions are restored
11994 workspace.update_in(cx, |workspace, window, cx| {
11995 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11996 assert!(
11997 !workspace.left_dock().read(cx).is_open(),
11998 "Left dock should remain closed"
11999 );
12000 assert!(
12001 workspace.right_dock().read(cx).is_open(),
12002 "Right dock should remain open"
12003 );
12004 assert!(
12005 workspace.bottom_dock().read(cx).is_open(),
12006 "Bottom dock should remain open"
12007 );
12008 assert_eq!(
12009 left_panel.read(cx).position,
12010 DockPosition::Bottom,
12011 "Left panel should remain in the bottom dock"
12012 );
12013 assert_eq!(
12014 right_panel.read(cx).position,
12015 DockPosition::Right,
12016 "Right panel should remain in the right dock"
12017 );
12018 assert_eq!(
12019 workspace
12020 .bottom_dock()
12021 .read(cx)
12022 .visible_panel()
12023 .unwrap()
12024 .panel_id(),
12025 left_panel.panel_id(),
12026 "Left panel should be the visible panel in the right dock"
12027 );
12028 });
12029 }
12030
12031 #[gpui::test]
12032 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
12033 init_test(cx);
12034
12035 let fs = FakeFs::new(cx.executor());
12036
12037 let project = Project::test(fs, None, cx).await;
12038 let (workspace, cx) =
12039 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12040
12041 // Let's arrange the panes like this:
12042 //
12043 // +-----------------------+
12044 // | top |
12045 // +------+--------+-------+
12046 // | left | center | right |
12047 // +------+--------+-------+
12048 // | bottom |
12049 // +-----------------------+
12050
12051 let top_item = cx.new(|cx| {
12052 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
12053 });
12054 let bottom_item = cx.new(|cx| {
12055 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
12056 });
12057 let left_item = cx.new(|cx| {
12058 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
12059 });
12060 let right_item = cx.new(|cx| {
12061 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
12062 });
12063 let center_item = cx.new(|cx| {
12064 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
12065 });
12066
12067 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12068 let top_pane_id = workspace.active_pane().entity_id();
12069 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
12070 workspace.split_pane(
12071 workspace.active_pane().clone(),
12072 SplitDirection::Down,
12073 window,
12074 cx,
12075 );
12076 top_pane_id
12077 });
12078 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12079 let bottom_pane_id = workspace.active_pane().entity_id();
12080 workspace.add_item_to_active_pane(
12081 Box::new(bottom_item.clone()),
12082 None,
12083 false,
12084 window,
12085 cx,
12086 );
12087 workspace.split_pane(
12088 workspace.active_pane().clone(),
12089 SplitDirection::Up,
12090 window,
12091 cx,
12092 );
12093 bottom_pane_id
12094 });
12095 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12096 let left_pane_id = workspace.active_pane().entity_id();
12097 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
12098 workspace.split_pane(
12099 workspace.active_pane().clone(),
12100 SplitDirection::Right,
12101 window,
12102 cx,
12103 );
12104 left_pane_id
12105 });
12106 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12107 let right_pane_id = workspace.active_pane().entity_id();
12108 workspace.add_item_to_active_pane(
12109 Box::new(right_item.clone()),
12110 None,
12111 false,
12112 window,
12113 cx,
12114 );
12115 workspace.split_pane(
12116 workspace.active_pane().clone(),
12117 SplitDirection::Left,
12118 window,
12119 cx,
12120 );
12121 right_pane_id
12122 });
12123 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12124 let center_pane_id = workspace.active_pane().entity_id();
12125 workspace.add_item_to_active_pane(
12126 Box::new(center_item.clone()),
12127 None,
12128 false,
12129 window,
12130 cx,
12131 );
12132 center_pane_id
12133 });
12134 cx.executor().run_until_parked();
12135
12136 workspace.update_in(cx, |workspace, window, cx| {
12137 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
12138
12139 // Join into next from center pane into right
12140 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12141 });
12142
12143 workspace.update_in(cx, |workspace, window, cx| {
12144 let active_pane = workspace.active_pane();
12145 assert_eq!(right_pane_id, active_pane.entity_id());
12146 assert_eq!(2, active_pane.read(cx).items_len());
12147 let item_ids_in_pane =
12148 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12149 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12150 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12151
12152 // Join into next from right pane into bottom
12153 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12154 });
12155
12156 workspace.update_in(cx, |workspace, window, cx| {
12157 let active_pane = workspace.active_pane();
12158 assert_eq!(bottom_pane_id, active_pane.entity_id());
12159 assert_eq!(3, active_pane.read(cx).items_len());
12160 let item_ids_in_pane =
12161 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12162 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12163 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12164 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12165
12166 // Join into next from bottom pane into left
12167 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12168 });
12169
12170 workspace.update_in(cx, |workspace, window, cx| {
12171 let active_pane = workspace.active_pane();
12172 assert_eq!(left_pane_id, active_pane.entity_id());
12173 assert_eq!(4, active_pane.read(cx).items_len());
12174 let item_ids_in_pane =
12175 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12176 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12177 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12178 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12179 assert!(item_ids_in_pane.contains(&left_item.item_id()));
12180
12181 // Join into next from left pane into top
12182 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12183 });
12184
12185 workspace.update_in(cx, |workspace, window, cx| {
12186 let active_pane = workspace.active_pane();
12187 assert_eq!(top_pane_id, active_pane.entity_id());
12188 assert_eq!(5, active_pane.read(cx).items_len());
12189 let item_ids_in_pane =
12190 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12191 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12192 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12193 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12194 assert!(item_ids_in_pane.contains(&left_item.item_id()));
12195 assert!(item_ids_in_pane.contains(&top_item.item_id()));
12196
12197 // Single pane left: no-op
12198 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
12199 });
12200
12201 workspace.update(cx, |workspace, _cx| {
12202 let active_pane = workspace.active_pane();
12203 assert_eq!(top_pane_id, active_pane.entity_id());
12204 });
12205 }
12206
12207 fn add_an_item_to_active_pane(
12208 cx: &mut VisualTestContext,
12209 workspace: &Entity<Workspace>,
12210 item_id: u64,
12211 ) -> Entity<TestItem> {
12212 let item = cx.new(|cx| {
12213 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
12214 item_id,
12215 "item{item_id}.txt",
12216 cx,
12217 )])
12218 });
12219 workspace.update_in(cx, |workspace, window, cx| {
12220 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
12221 });
12222 item
12223 }
12224
12225 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
12226 workspace.update_in(cx, |workspace, window, cx| {
12227 workspace.split_pane(
12228 workspace.active_pane().clone(),
12229 SplitDirection::Right,
12230 window,
12231 cx,
12232 )
12233 })
12234 }
12235
12236 #[gpui::test]
12237 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
12238 init_test(cx);
12239 let fs = FakeFs::new(cx.executor());
12240 let project = Project::test(fs, None, cx).await;
12241 let (workspace, cx) =
12242 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12243
12244 add_an_item_to_active_pane(cx, &workspace, 1);
12245 split_pane(cx, &workspace);
12246 add_an_item_to_active_pane(cx, &workspace, 2);
12247 split_pane(cx, &workspace); // empty pane
12248 split_pane(cx, &workspace);
12249 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
12250
12251 cx.executor().run_until_parked();
12252
12253 workspace.update(cx, |workspace, cx| {
12254 let num_panes = workspace.panes().len();
12255 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
12256 let active_item = workspace
12257 .active_pane()
12258 .read(cx)
12259 .active_item()
12260 .expect("item is in focus");
12261
12262 assert_eq!(num_panes, 4);
12263 assert_eq!(num_items_in_current_pane, 1);
12264 assert_eq!(active_item.item_id(), last_item.item_id());
12265 });
12266
12267 workspace.update_in(cx, |workspace, window, cx| {
12268 workspace.join_all_panes(window, cx);
12269 });
12270
12271 workspace.update(cx, |workspace, cx| {
12272 let num_panes = workspace.panes().len();
12273 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
12274 let active_item = workspace
12275 .active_pane()
12276 .read(cx)
12277 .active_item()
12278 .expect("item is in focus");
12279
12280 assert_eq!(num_panes, 1);
12281 assert_eq!(num_items_in_current_pane, 3);
12282 assert_eq!(active_item.item_id(), last_item.item_id());
12283 });
12284 }
12285
12286 #[gpui::test]
12287 async fn test_flexible_dock_sizing(cx: &mut gpui::TestAppContext) {
12288 init_test(cx);
12289 let fs = FakeFs::new(cx.executor());
12290
12291 let project = Project::test(fs, [], cx).await;
12292 let (multi_workspace, cx) =
12293 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12294 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12295
12296 workspace.update(cx, |workspace, _cx| {
12297 workspace.bounds.size.width = px(800.);
12298 });
12299
12300 workspace.update_in(cx, |workspace, window, cx| {
12301 let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
12302 workspace.add_panel(panel, window, cx);
12303 workspace.toggle_dock(DockPosition::Right, window, cx);
12304 });
12305
12306 let (panel, resized_width, ratio_basis_width) =
12307 workspace.update_in(cx, |workspace, window, cx| {
12308 let item = cx.new(|cx| {
12309 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
12310 });
12311 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
12312
12313 let dock = workspace.right_dock().read(cx);
12314 let workspace_width = workspace.bounds.size.width;
12315 let initial_width = workspace
12316 .dock_size(&dock, window, cx)
12317 .expect("flexible dock should have an initial width");
12318
12319 assert_eq!(initial_width, workspace_width / 2.);
12320
12321 workspace.resize_right_dock(px(300.), window, cx);
12322
12323 let dock = workspace.right_dock().read(cx);
12324 let resized_width = workspace
12325 .dock_size(&dock, window, cx)
12326 .expect("flexible dock should keep its resized width");
12327
12328 assert_eq!(resized_width, px(300.));
12329
12330 let panel = workspace
12331 .right_dock()
12332 .read(cx)
12333 .visible_panel()
12334 .expect("flexible dock should have a visible panel")
12335 .panel_id();
12336
12337 (panel, resized_width, workspace_width)
12338 });
12339
12340 workspace.update_in(cx, |workspace, window, cx| {
12341 workspace.toggle_dock(DockPosition::Right, window, cx);
12342 workspace.toggle_dock(DockPosition::Right, window, cx);
12343
12344 let dock = workspace.right_dock().read(cx);
12345 let reopened_width = workspace
12346 .dock_size(&dock, window, cx)
12347 .expect("flexible dock should restore when reopened");
12348
12349 assert_eq!(reopened_width, resized_width);
12350
12351 let right_dock = workspace.right_dock().read(cx);
12352 let flexible_panel = right_dock
12353 .visible_panel()
12354 .expect("flexible dock should still have a visible panel");
12355 assert_eq!(flexible_panel.panel_id(), panel);
12356 assert_eq!(
12357 right_dock
12358 .stored_panel_size_state(flexible_panel.as_ref())
12359 .and_then(|size_state| size_state.flexible_size_ratio),
12360 Some(resized_width.to_f64() as f32 / workspace.bounds.size.width.to_f64() as f32)
12361 );
12362 });
12363
12364 workspace.update_in(cx, |workspace, window, cx| {
12365 workspace.split_pane(
12366 workspace.active_pane().clone(),
12367 SplitDirection::Right,
12368 window,
12369 cx,
12370 );
12371
12372 let dock = workspace.right_dock().read(cx);
12373 let split_width = workspace
12374 .dock_size(&dock, window, cx)
12375 .expect("flexible dock should keep its user-resized proportion");
12376
12377 assert_eq!(split_width, px(300.));
12378
12379 workspace.bounds.size.width = px(1600.);
12380
12381 let dock = workspace.right_dock().read(cx);
12382 let resized_window_width = workspace
12383 .dock_size(&dock, window, cx)
12384 .expect("flexible dock should preserve proportional size on window resize");
12385
12386 assert_eq!(
12387 resized_window_width,
12388 workspace.bounds.size.width
12389 * (resized_width.to_f64() as f32 / ratio_basis_width.to_f64() as f32)
12390 );
12391 });
12392 }
12393
12394 #[gpui::test]
12395 async fn test_panel_size_state_persistence(cx: &mut gpui::TestAppContext) {
12396 init_test(cx);
12397 let fs = FakeFs::new(cx.executor());
12398
12399 // Fixed-width panel: pixel size is persisted to KVP and restored on re-add.
12400 {
12401 let project = Project::test(fs.clone(), [], cx).await;
12402 let (multi_workspace, cx) =
12403 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12404 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12405
12406 workspace.update(cx, |workspace, _cx| {
12407 workspace.set_random_database_id();
12408 workspace.bounds.size.width = px(800.);
12409 });
12410
12411 let panel = workspace.update_in(cx, |workspace, window, cx| {
12412 let panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
12413 workspace.add_panel(panel.clone(), window, cx);
12414 workspace.toggle_dock(DockPosition::Left, window, cx);
12415 panel
12416 });
12417
12418 workspace.update_in(cx, |workspace, window, cx| {
12419 workspace.resize_left_dock(px(350.), window, cx);
12420 });
12421
12422 cx.run_until_parked();
12423
12424 let persisted = workspace.read_with(cx, |workspace, cx| {
12425 workspace.persisted_panel_size_state(TestPanel::panel_key(), cx)
12426 });
12427 assert_eq!(
12428 persisted.and_then(|s| s.size),
12429 Some(px(350.)),
12430 "fixed-width panel size should be persisted to KVP"
12431 );
12432
12433 // Remove the panel and re-add a fresh instance with the same key.
12434 // The new instance should have its size state restored from KVP.
12435 workspace.update_in(cx, |workspace, window, cx| {
12436 workspace.remove_panel(&panel, window, cx);
12437 });
12438
12439 workspace.update_in(cx, |workspace, window, cx| {
12440 let new_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
12441 workspace.add_panel(new_panel, window, cx);
12442
12443 let left_dock = workspace.left_dock().read(cx);
12444 let size_state = left_dock
12445 .panel::<TestPanel>()
12446 .and_then(|p| left_dock.stored_panel_size_state(&p));
12447 assert_eq!(
12448 size_state.and_then(|s| s.size),
12449 Some(px(350.)),
12450 "re-added fixed-width panel should restore persisted size from KVP"
12451 );
12452 });
12453 }
12454
12455 // Flexible panel: both pixel size and ratio are persisted and restored.
12456 {
12457 let project = Project::test(fs.clone(), [], cx).await;
12458 let (multi_workspace, cx) =
12459 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12460 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12461
12462 workspace.update(cx, |workspace, _cx| {
12463 workspace.set_random_database_id();
12464 workspace.bounds.size.width = px(800.);
12465 });
12466
12467 let panel = workspace.update_in(cx, |workspace, window, cx| {
12468 let item = cx.new(|cx| {
12469 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
12470 });
12471 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
12472
12473 let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
12474 workspace.add_panel(panel.clone(), window, cx);
12475 workspace.toggle_dock(DockPosition::Right, window, cx);
12476 panel
12477 });
12478
12479 workspace.update_in(cx, |workspace, window, cx| {
12480 workspace.resize_right_dock(px(300.), window, cx);
12481 });
12482
12483 cx.run_until_parked();
12484
12485 let persisted = workspace
12486 .read_with(cx, |workspace, cx| {
12487 workspace.persisted_panel_size_state(TestPanel::panel_key(), cx)
12488 })
12489 .expect("flexible panel state should be persisted to KVP");
12490 assert_eq!(
12491 persisted.size, None,
12492 "flexible panel should not persist a redundant pixel size"
12493 );
12494 let original_ratio = persisted
12495 .flexible_size_ratio
12496 .expect("flexible panel ratio should be persisted");
12497
12498 // Remove the panel and re-add: both size and ratio should be restored.
12499 workspace.update_in(cx, |workspace, window, cx| {
12500 workspace.remove_panel(&panel, window, cx);
12501 });
12502
12503 workspace.update_in(cx, |workspace, window, cx| {
12504 let new_panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
12505 workspace.add_panel(new_panel, window, cx);
12506
12507 let right_dock = workspace.right_dock().read(cx);
12508 let size_state = right_dock
12509 .panel::<TestPanel>()
12510 .and_then(|p| right_dock.stored_panel_size_state(&p))
12511 .expect("re-added flexible panel should have restored size state from KVP");
12512 assert_eq!(
12513 size_state.size, None,
12514 "re-added flexible panel should not have a persisted pixel size"
12515 );
12516 assert_eq!(
12517 size_state.flexible_size_ratio,
12518 Some(original_ratio),
12519 "re-added flexible panel should restore persisted ratio"
12520 );
12521 });
12522 }
12523 }
12524
12525 #[gpui::test]
12526 async fn test_flexible_panel_left_dock_sizing(cx: &mut gpui::TestAppContext) {
12527 init_test(cx);
12528 let fs = FakeFs::new(cx.executor());
12529
12530 let project = Project::test(fs, [], cx).await;
12531 let (multi_workspace, cx) =
12532 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12533 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12534
12535 workspace.update(cx, |workspace, _cx| {
12536 workspace.bounds.size.width = px(900.);
12537 });
12538
12539 // Step 1: Add a tab to the center pane then open a flexible panel in the left
12540 // dock. With one full-width center pane the default ratio is 0.5, so the panel
12541 // and the center pane each take half the workspace width.
12542 workspace.update_in(cx, |workspace, window, cx| {
12543 let item = cx.new(|cx| {
12544 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
12545 });
12546 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
12547
12548 let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Left, 100, cx));
12549 workspace.add_panel(panel, window, cx);
12550 workspace.toggle_dock(DockPosition::Left, window, cx);
12551
12552 let left_dock = workspace.left_dock().read(cx);
12553 let left_width = workspace
12554 .dock_size(&left_dock, window, cx)
12555 .expect("left dock should have an active panel");
12556
12557 assert_eq!(
12558 left_width,
12559 workspace.bounds.size.width / 2.,
12560 "flexible left panel should split evenly with the center pane"
12561 );
12562 });
12563
12564 // Step 2: Split the center pane vertically (top/bottom). Vertical splits do not
12565 // change horizontal width fractions, so the flexible panel stays at the same
12566 // width as each half of the split.
12567 workspace.update_in(cx, |workspace, window, cx| {
12568 workspace.split_pane(
12569 workspace.active_pane().clone(),
12570 SplitDirection::Down,
12571 window,
12572 cx,
12573 );
12574
12575 let left_dock = workspace.left_dock().read(cx);
12576 let left_width = workspace
12577 .dock_size(&left_dock, window, cx)
12578 .expect("left dock should still have an active panel after vertical split");
12579
12580 assert_eq!(
12581 left_width,
12582 workspace.bounds.size.width / 2.,
12583 "flexible left panel width should match each vertically-split pane"
12584 );
12585 });
12586
12587 // Step 3: Open a fixed-width panel in the right dock. The right dock's default
12588 // size reduces the available width, so the flexible left panel and the center
12589 // panes all shrink proportionally to accommodate it.
12590 workspace.update_in(cx, |workspace, window, cx| {
12591 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 200, cx));
12592 workspace.add_panel(panel, window, cx);
12593 workspace.toggle_dock(DockPosition::Right, window, cx);
12594
12595 let right_dock = workspace.right_dock().read(cx);
12596 let right_width = workspace
12597 .dock_size(&right_dock, window, cx)
12598 .expect("right dock should have an active panel");
12599
12600 let left_dock = workspace.left_dock().read(cx);
12601 let left_width = workspace
12602 .dock_size(&left_dock, window, cx)
12603 .expect("left dock should still have an active panel");
12604
12605 let available_width = workspace.bounds.size.width - right_width;
12606 assert_eq!(
12607 left_width,
12608 available_width / 2.,
12609 "flexible left panel should shrink proportionally as the right dock takes space"
12610 );
12611 });
12612 }
12613
12614 struct TestModal(FocusHandle);
12615
12616 impl TestModal {
12617 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
12618 Self(cx.focus_handle())
12619 }
12620 }
12621
12622 impl EventEmitter<DismissEvent> for TestModal {}
12623
12624 impl Focusable for TestModal {
12625 fn focus_handle(&self, _cx: &App) -> FocusHandle {
12626 self.0.clone()
12627 }
12628 }
12629
12630 impl ModalView for TestModal {}
12631
12632 impl Render for TestModal {
12633 fn render(
12634 &mut self,
12635 _window: &mut Window,
12636 _cx: &mut Context<TestModal>,
12637 ) -> impl IntoElement {
12638 div().track_focus(&self.0)
12639 }
12640 }
12641
12642 #[gpui::test]
12643 async fn test_panels(cx: &mut gpui::TestAppContext) {
12644 init_test(cx);
12645 let fs = FakeFs::new(cx.executor());
12646
12647 let project = Project::test(fs, [], cx).await;
12648 let (multi_workspace, cx) =
12649 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12650 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12651
12652 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
12653 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
12654 workspace.add_panel(panel_1.clone(), window, cx);
12655 workspace.toggle_dock(DockPosition::Left, window, cx);
12656 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
12657 workspace.add_panel(panel_2.clone(), window, cx);
12658 workspace.toggle_dock(DockPosition::Right, window, cx);
12659
12660 let left_dock = workspace.left_dock();
12661 assert_eq!(
12662 left_dock.read(cx).visible_panel().unwrap().panel_id(),
12663 panel_1.panel_id()
12664 );
12665 assert_eq!(
12666 workspace.dock_size(&left_dock.read(cx), window, cx),
12667 Some(px(300.))
12668 );
12669
12670 workspace.resize_left_dock(px(1337.), window, cx);
12671 assert_eq!(
12672 workspace
12673 .right_dock()
12674 .read(cx)
12675 .visible_panel()
12676 .unwrap()
12677 .panel_id(),
12678 panel_2.panel_id(),
12679 );
12680
12681 (panel_1, panel_2)
12682 });
12683
12684 // Move panel_1 to the right
12685 panel_1.update_in(cx, |panel_1, window, cx| {
12686 panel_1.set_position(DockPosition::Right, window, cx)
12687 });
12688
12689 workspace.update_in(cx, |workspace, window, cx| {
12690 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
12691 // Since it was the only panel on the left, the left dock should now be closed.
12692 assert!(!workspace.left_dock().read(cx).is_open());
12693 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
12694 let right_dock = workspace.right_dock();
12695 assert_eq!(
12696 right_dock.read(cx).visible_panel().unwrap().panel_id(),
12697 panel_1.panel_id()
12698 );
12699 assert_eq!(
12700 right_dock
12701 .read(cx)
12702 .active_panel_size()
12703 .unwrap()
12704 .size
12705 .unwrap(),
12706 px(1337.)
12707 );
12708
12709 // Now we move panel_2 to the left
12710 panel_2.set_position(DockPosition::Left, window, cx);
12711 });
12712
12713 workspace.update(cx, |workspace, cx| {
12714 // Since panel_2 was not visible on the right, we don't open the left dock.
12715 assert!(!workspace.left_dock().read(cx).is_open());
12716 // And the right dock is unaffected in its displaying of panel_1
12717 assert!(workspace.right_dock().read(cx).is_open());
12718 assert_eq!(
12719 workspace
12720 .right_dock()
12721 .read(cx)
12722 .visible_panel()
12723 .unwrap()
12724 .panel_id(),
12725 panel_1.panel_id(),
12726 );
12727 });
12728
12729 // Move panel_1 back to the left
12730 panel_1.update_in(cx, |panel_1, window, cx| {
12731 panel_1.set_position(DockPosition::Left, window, cx)
12732 });
12733
12734 workspace.update_in(cx, |workspace, window, cx| {
12735 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
12736 let left_dock = workspace.left_dock();
12737 assert!(left_dock.read(cx).is_open());
12738 assert_eq!(
12739 left_dock.read(cx).visible_panel().unwrap().panel_id(),
12740 panel_1.panel_id()
12741 );
12742 assert_eq!(
12743 workspace.dock_size(&left_dock.read(cx), window, cx),
12744 Some(px(1337.))
12745 );
12746 // And the right dock should be closed as it no longer has any panels.
12747 assert!(!workspace.right_dock().read(cx).is_open());
12748
12749 // Now we move panel_1 to the bottom
12750 panel_1.set_position(DockPosition::Bottom, window, cx);
12751 });
12752
12753 workspace.update_in(cx, |workspace, window, cx| {
12754 // Since panel_1 was visible on the left, we close the left dock.
12755 assert!(!workspace.left_dock().read(cx).is_open());
12756 // The bottom dock is sized based on the panel's default size,
12757 // since the panel orientation changed from vertical to horizontal.
12758 let bottom_dock = workspace.bottom_dock();
12759 assert_eq!(
12760 workspace.dock_size(&bottom_dock.read(cx), window, cx),
12761 Some(px(300.))
12762 );
12763 // Close bottom dock and move panel_1 back to the left.
12764 bottom_dock.update(cx, |bottom_dock, cx| {
12765 bottom_dock.set_open(false, window, cx)
12766 });
12767 panel_1.set_position(DockPosition::Left, window, cx);
12768 });
12769
12770 // Emit activated event on panel 1
12771 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
12772
12773 // Now the left dock is open and panel_1 is active and focused.
12774 workspace.update_in(cx, |workspace, window, cx| {
12775 let left_dock = workspace.left_dock();
12776 assert!(left_dock.read(cx).is_open());
12777 assert_eq!(
12778 left_dock.read(cx).visible_panel().unwrap().panel_id(),
12779 panel_1.panel_id(),
12780 );
12781 assert!(panel_1.focus_handle(cx).is_focused(window));
12782 });
12783
12784 // Emit closed event on panel 2, which is not active
12785 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
12786
12787 // Wo don't close the left dock, because panel_2 wasn't the active panel
12788 workspace.update(cx, |workspace, cx| {
12789 let left_dock = workspace.left_dock();
12790 assert!(left_dock.read(cx).is_open());
12791 assert_eq!(
12792 left_dock.read(cx).visible_panel().unwrap().panel_id(),
12793 panel_1.panel_id(),
12794 );
12795 });
12796
12797 // Emitting a ZoomIn event shows the panel as zoomed.
12798 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
12799 workspace.read_with(cx, |workspace, _| {
12800 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
12801 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
12802 });
12803
12804 // Move panel to another dock while it is zoomed
12805 panel_1.update_in(cx, |panel, window, cx| {
12806 panel.set_position(DockPosition::Right, window, cx)
12807 });
12808 workspace.read_with(cx, |workspace, _| {
12809 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
12810
12811 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
12812 });
12813
12814 // This is a helper for getting a:
12815 // - valid focus on an element,
12816 // - that isn't a part of the panes and panels system of the Workspace,
12817 // - and doesn't trigger the 'on_focus_lost' API.
12818 let focus_other_view = {
12819 let workspace = workspace.clone();
12820 move |cx: &mut VisualTestContext| {
12821 workspace.update_in(cx, |workspace, window, cx| {
12822 if workspace.active_modal::<TestModal>(cx).is_some() {
12823 workspace.toggle_modal(window, cx, TestModal::new);
12824 workspace.toggle_modal(window, cx, TestModal::new);
12825 } else {
12826 workspace.toggle_modal(window, cx, TestModal::new);
12827 }
12828 })
12829 }
12830 };
12831
12832 // If focus is transferred to another view that's not a panel or another pane, we still show
12833 // the panel as zoomed.
12834 focus_other_view(cx);
12835 workspace.read_with(cx, |workspace, _| {
12836 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
12837 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
12838 });
12839
12840 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
12841 workspace.update_in(cx, |_workspace, window, cx| {
12842 cx.focus_self(window);
12843 });
12844 workspace.read_with(cx, |workspace, _| {
12845 assert_eq!(workspace.zoomed, None);
12846 assert_eq!(workspace.zoomed_position, None);
12847 });
12848
12849 // If focus is transferred again to another view that's not a panel or a pane, we won't
12850 // show the panel as zoomed because it wasn't zoomed before.
12851 focus_other_view(cx);
12852 workspace.read_with(cx, |workspace, _| {
12853 assert_eq!(workspace.zoomed, None);
12854 assert_eq!(workspace.zoomed_position, None);
12855 });
12856
12857 // When the panel is activated, it is zoomed again.
12858 cx.dispatch_action(ToggleRightDock);
12859 workspace.read_with(cx, |workspace, _| {
12860 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
12861 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
12862 });
12863
12864 // Emitting a ZoomOut event unzooms the panel.
12865 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
12866 workspace.read_with(cx, |workspace, _| {
12867 assert_eq!(workspace.zoomed, None);
12868 assert_eq!(workspace.zoomed_position, None);
12869 });
12870
12871 // Emit closed event on panel 1, which is active
12872 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
12873
12874 // Now the left dock is closed, because panel_1 was the active panel
12875 workspace.update(cx, |workspace, cx| {
12876 let right_dock = workspace.right_dock();
12877 assert!(!right_dock.read(cx).is_open());
12878 });
12879 }
12880
12881 #[gpui::test]
12882 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
12883 init_test(cx);
12884
12885 let fs = FakeFs::new(cx.background_executor.clone());
12886 let project = Project::test(fs, [], cx).await;
12887 let (workspace, cx) =
12888 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12889 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12890
12891 let dirty_regular_buffer = cx.new(|cx| {
12892 TestItem::new(cx)
12893 .with_dirty(true)
12894 .with_label("1.txt")
12895 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
12896 });
12897 let dirty_regular_buffer_2 = cx.new(|cx| {
12898 TestItem::new(cx)
12899 .with_dirty(true)
12900 .with_label("2.txt")
12901 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
12902 });
12903 let dirty_multi_buffer_with_both = cx.new(|cx| {
12904 TestItem::new(cx)
12905 .with_dirty(true)
12906 .with_buffer_kind(ItemBufferKind::Multibuffer)
12907 .with_label("Fake Project Search")
12908 .with_project_items(&[
12909 dirty_regular_buffer.read(cx).project_items[0].clone(),
12910 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
12911 ])
12912 });
12913 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
12914 workspace.update_in(cx, |workspace, window, cx| {
12915 workspace.add_item(
12916 pane.clone(),
12917 Box::new(dirty_regular_buffer.clone()),
12918 None,
12919 false,
12920 false,
12921 window,
12922 cx,
12923 );
12924 workspace.add_item(
12925 pane.clone(),
12926 Box::new(dirty_regular_buffer_2.clone()),
12927 None,
12928 false,
12929 false,
12930 window,
12931 cx,
12932 );
12933 workspace.add_item(
12934 pane.clone(),
12935 Box::new(dirty_multi_buffer_with_both.clone()),
12936 None,
12937 false,
12938 false,
12939 window,
12940 cx,
12941 );
12942 });
12943
12944 pane.update_in(cx, |pane, window, cx| {
12945 pane.activate_item(2, true, true, window, cx);
12946 assert_eq!(
12947 pane.active_item().unwrap().item_id(),
12948 multi_buffer_with_both_files_id,
12949 "Should select the multi buffer in the pane"
12950 );
12951 });
12952 let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
12953 pane.close_other_items(
12954 &CloseOtherItems {
12955 save_intent: Some(SaveIntent::Save),
12956 close_pinned: true,
12957 },
12958 None,
12959 window,
12960 cx,
12961 )
12962 });
12963 cx.background_executor.run_until_parked();
12964 assert!(!cx.has_pending_prompt());
12965 close_all_but_multi_buffer_task
12966 .await
12967 .expect("Closing all buffers but the multi buffer failed");
12968 pane.update(cx, |pane, cx| {
12969 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
12970 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
12971 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
12972 assert_eq!(pane.items_len(), 1);
12973 assert_eq!(
12974 pane.active_item().unwrap().item_id(),
12975 multi_buffer_with_both_files_id,
12976 "Should have only the multi buffer left in the pane"
12977 );
12978 assert!(
12979 dirty_multi_buffer_with_both.read(cx).is_dirty,
12980 "The multi buffer containing the unsaved buffer should still be dirty"
12981 );
12982 });
12983
12984 dirty_regular_buffer.update(cx, |buffer, cx| {
12985 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
12986 });
12987
12988 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
12989 pane.close_active_item(
12990 &CloseActiveItem {
12991 save_intent: Some(SaveIntent::Close),
12992 close_pinned: false,
12993 },
12994 window,
12995 cx,
12996 )
12997 });
12998 cx.background_executor.run_until_parked();
12999 assert!(
13000 cx.has_pending_prompt(),
13001 "Dirty multi buffer should prompt a save dialog"
13002 );
13003 cx.simulate_prompt_answer("Save");
13004 cx.background_executor.run_until_parked();
13005 close_multi_buffer_task
13006 .await
13007 .expect("Closing the multi buffer failed");
13008 pane.update(cx, |pane, cx| {
13009 assert_eq!(
13010 dirty_multi_buffer_with_both.read(cx).save_count,
13011 1,
13012 "Multi buffer item should get be saved"
13013 );
13014 // Test impl does not save inner items, so we do not assert them
13015 assert_eq!(
13016 pane.items_len(),
13017 0,
13018 "No more items should be left in the pane"
13019 );
13020 assert!(pane.active_item().is_none());
13021 });
13022 }
13023
13024 #[gpui::test]
13025 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
13026 cx: &mut TestAppContext,
13027 ) {
13028 init_test(cx);
13029
13030 let fs = FakeFs::new(cx.background_executor.clone());
13031 let project = Project::test(fs, [], cx).await;
13032 let (workspace, cx) =
13033 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13034 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13035
13036 let dirty_regular_buffer = cx.new(|cx| {
13037 TestItem::new(cx)
13038 .with_dirty(true)
13039 .with_label("1.txt")
13040 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
13041 });
13042 let dirty_regular_buffer_2 = cx.new(|cx| {
13043 TestItem::new(cx)
13044 .with_dirty(true)
13045 .with_label("2.txt")
13046 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
13047 });
13048 let clear_regular_buffer = cx.new(|cx| {
13049 TestItem::new(cx)
13050 .with_label("3.txt")
13051 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
13052 });
13053
13054 let dirty_multi_buffer_with_both = cx.new(|cx| {
13055 TestItem::new(cx)
13056 .with_dirty(true)
13057 .with_buffer_kind(ItemBufferKind::Multibuffer)
13058 .with_label("Fake Project Search")
13059 .with_project_items(&[
13060 dirty_regular_buffer.read(cx).project_items[0].clone(),
13061 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
13062 clear_regular_buffer.read(cx).project_items[0].clone(),
13063 ])
13064 });
13065 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
13066 workspace.update_in(cx, |workspace, window, cx| {
13067 workspace.add_item(
13068 pane.clone(),
13069 Box::new(dirty_regular_buffer.clone()),
13070 None,
13071 false,
13072 false,
13073 window,
13074 cx,
13075 );
13076 workspace.add_item(
13077 pane.clone(),
13078 Box::new(dirty_multi_buffer_with_both.clone()),
13079 None,
13080 false,
13081 false,
13082 window,
13083 cx,
13084 );
13085 });
13086
13087 pane.update_in(cx, |pane, window, cx| {
13088 pane.activate_item(1, true, true, window, cx);
13089 assert_eq!(
13090 pane.active_item().unwrap().item_id(),
13091 multi_buffer_with_both_files_id,
13092 "Should select the multi buffer in the pane"
13093 );
13094 });
13095 let _close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13096 pane.close_active_item(
13097 &CloseActiveItem {
13098 save_intent: None,
13099 close_pinned: false,
13100 },
13101 window,
13102 cx,
13103 )
13104 });
13105 cx.background_executor.run_until_parked();
13106 assert!(
13107 cx.has_pending_prompt(),
13108 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
13109 );
13110 }
13111
13112 /// Tests that when `close_on_file_delete` is enabled, files are automatically
13113 /// closed when they are deleted from disk.
13114 #[gpui::test]
13115 async fn test_close_on_disk_deletion_enabled(cx: &mut TestAppContext) {
13116 init_test(cx);
13117
13118 // Enable the close_on_disk_deletion setting
13119 cx.update_global(|store: &mut SettingsStore, cx| {
13120 store.update_user_settings(cx, |settings| {
13121 settings.workspace.close_on_file_delete = Some(true);
13122 });
13123 });
13124
13125 let fs = FakeFs::new(cx.background_executor.clone());
13126 let project = Project::test(fs, [], cx).await;
13127 let (workspace, cx) =
13128 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13129 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13130
13131 // Create a test item that simulates a file
13132 let item = cx.new(|cx| {
13133 TestItem::new(cx)
13134 .with_label("test.txt")
13135 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13136 });
13137
13138 // Add item to workspace
13139 workspace.update_in(cx, |workspace, window, cx| {
13140 workspace.add_item(
13141 pane.clone(),
13142 Box::new(item.clone()),
13143 None,
13144 false,
13145 false,
13146 window,
13147 cx,
13148 );
13149 });
13150
13151 // Verify the item is in the pane
13152 pane.read_with(cx, |pane, _| {
13153 assert_eq!(pane.items().count(), 1);
13154 });
13155
13156 // Simulate file deletion by setting the item's deleted state
13157 item.update(cx, |item, _| {
13158 item.set_has_deleted_file(true);
13159 });
13160
13161 // Emit UpdateTab event to trigger the close behavior
13162 cx.run_until_parked();
13163 item.update(cx, |_, cx| {
13164 cx.emit(ItemEvent::UpdateTab);
13165 });
13166
13167 // Allow the close operation to complete
13168 cx.run_until_parked();
13169
13170 // Verify the item was automatically closed
13171 pane.read_with(cx, |pane, _| {
13172 assert_eq!(
13173 pane.items().count(),
13174 0,
13175 "Item should be automatically closed when file is deleted"
13176 );
13177 });
13178 }
13179
13180 /// Tests that when `close_on_file_delete` is disabled (default), files remain
13181 /// open with a strikethrough when they are deleted from disk.
13182 #[gpui::test]
13183 async fn test_close_on_disk_deletion_disabled(cx: &mut TestAppContext) {
13184 init_test(cx);
13185
13186 // Ensure close_on_disk_deletion is disabled (default)
13187 cx.update_global(|store: &mut SettingsStore, cx| {
13188 store.update_user_settings(cx, |settings| {
13189 settings.workspace.close_on_file_delete = Some(false);
13190 });
13191 });
13192
13193 let fs = FakeFs::new(cx.background_executor.clone());
13194 let project = Project::test(fs, [], cx).await;
13195 let (workspace, cx) =
13196 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13197 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13198
13199 // Create a test item that simulates a file
13200 let item = cx.new(|cx| {
13201 TestItem::new(cx)
13202 .with_label("test.txt")
13203 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13204 });
13205
13206 // Add item to workspace
13207 workspace.update_in(cx, |workspace, window, cx| {
13208 workspace.add_item(
13209 pane.clone(),
13210 Box::new(item.clone()),
13211 None,
13212 false,
13213 false,
13214 window,
13215 cx,
13216 );
13217 });
13218
13219 // Verify the item is in the pane
13220 pane.read_with(cx, |pane, _| {
13221 assert_eq!(pane.items().count(), 1);
13222 });
13223
13224 // Simulate file deletion
13225 item.update(cx, |item, _| {
13226 item.set_has_deleted_file(true);
13227 });
13228
13229 // Emit UpdateTab event
13230 cx.run_until_parked();
13231 item.update(cx, |_, cx| {
13232 cx.emit(ItemEvent::UpdateTab);
13233 });
13234
13235 // Allow any potential close operation to complete
13236 cx.run_until_parked();
13237
13238 // Verify the item remains open (with strikethrough)
13239 pane.read_with(cx, |pane, _| {
13240 assert_eq!(
13241 pane.items().count(),
13242 1,
13243 "Item should remain open when close_on_disk_deletion is disabled"
13244 );
13245 });
13246
13247 // Verify the item shows as deleted
13248 item.read_with(cx, |item, _| {
13249 assert!(
13250 item.has_deleted_file,
13251 "Item should be marked as having deleted file"
13252 );
13253 });
13254 }
13255
13256 /// Tests that dirty files are not automatically closed when deleted from disk,
13257 /// even when `close_on_file_delete` is enabled. This ensures users don't lose
13258 /// unsaved changes without being prompted.
13259 #[gpui::test]
13260 async fn test_close_on_disk_deletion_with_dirty_file(cx: &mut TestAppContext) {
13261 init_test(cx);
13262
13263 // Enable the close_on_file_delete setting
13264 cx.update_global(|store: &mut SettingsStore, cx| {
13265 store.update_user_settings(cx, |settings| {
13266 settings.workspace.close_on_file_delete = Some(true);
13267 });
13268 });
13269
13270 let fs = FakeFs::new(cx.background_executor.clone());
13271 let project = Project::test(fs, [], cx).await;
13272 let (workspace, cx) =
13273 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13274 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13275
13276 // Create a dirty test item
13277 let item = cx.new(|cx| {
13278 TestItem::new(cx)
13279 .with_dirty(true)
13280 .with_label("test.txt")
13281 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13282 });
13283
13284 // Add item to workspace
13285 workspace.update_in(cx, |workspace, window, cx| {
13286 workspace.add_item(
13287 pane.clone(),
13288 Box::new(item.clone()),
13289 None,
13290 false,
13291 false,
13292 window,
13293 cx,
13294 );
13295 });
13296
13297 // Simulate file deletion
13298 item.update(cx, |item, _| {
13299 item.set_has_deleted_file(true);
13300 });
13301
13302 // Emit UpdateTab event to trigger the close behavior
13303 cx.run_until_parked();
13304 item.update(cx, |_, cx| {
13305 cx.emit(ItemEvent::UpdateTab);
13306 });
13307
13308 // Allow any potential close operation to complete
13309 cx.run_until_parked();
13310
13311 // Verify the item remains open (dirty files are not auto-closed)
13312 pane.read_with(cx, |pane, _| {
13313 assert_eq!(
13314 pane.items().count(),
13315 1,
13316 "Dirty items should not be automatically closed even when file is deleted"
13317 );
13318 });
13319
13320 // Verify the item is marked as deleted and still dirty
13321 item.read_with(cx, |item, _| {
13322 assert!(
13323 item.has_deleted_file,
13324 "Item should be marked as having deleted file"
13325 );
13326 assert!(item.is_dirty, "Item should still be dirty");
13327 });
13328 }
13329
13330 /// Tests that navigation history is cleaned up when files are auto-closed
13331 /// due to deletion from disk.
13332 #[gpui::test]
13333 async fn test_close_on_disk_deletion_cleans_navigation_history(cx: &mut TestAppContext) {
13334 init_test(cx);
13335
13336 // Enable the close_on_file_delete setting
13337 cx.update_global(|store: &mut SettingsStore, cx| {
13338 store.update_user_settings(cx, |settings| {
13339 settings.workspace.close_on_file_delete = Some(true);
13340 });
13341 });
13342
13343 let fs = FakeFs::new(cx.background_executor.clone());
13344 let project = Project::test(fs, [], cx).await;
13345 let (workspace, cx) =
13346 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13347 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13348
13349 // Create test items
13350 let item1 = cx.new(|cx| {
13351 TestItem::new(cx)
13352 .with_label("test1.txt")
13353 .with_project_items(&[TestProjectItem::new(1, "test1.txt", cx)])
13354 });
13355 let item1_id = item1.item_id();
13356
13357 let item2 = cx.new(|cx| {
13358 TestItem::new(cx)
13359 .with_label("test2.txt")
13360 .with_project_items(&[TestProjectItem::new(2, "test2.txt", cx)])
13361 });
13362
13363 // Add items to workspace
13364 workspace.update_in(cx, |workspace, window, cx| {
13365 workspace.add_item(
13366 pane.clone(),
13367 Box::new(item1.clone()),
13368 None,
13369 false,
13370 false,
13371 window,
13372 cx,
13373 );
13374 workspace.add_item(
13375 pane.clone(),
13376 Box::new(item2.clone()),
13377 None,
13378 false,
13379 false,
13380 window,
13381 cx,
13382 );
13383 });
13384
13385 // Activate item1 to ensure it gets navigation entries
13386 pane.update_in(cx, |pane, window, cx| {
13387 pane.activate_item(0, true, true, window, cx);
13388 });
13389
13390 // Switch to item2 and back to create navigation history
13391 pane.update_in(cx, |pane, window, cx| {
13392 pane.activate_item(1, true, true, window, cx);
13393 });
13394 cx.run_until_parked();
13395
13396 pane.update_in(cx, |pane, window, cx| {
13397 pane.activate_item(0, true, true, window, cx);
13398 });
13399 cx.run_until_parked();
13400
13401 // Simulate file deletion for item1
13402 item1.update(cx, |item, _| {
13403 item.set_has_deleted_file(true);
13404 });
13405
13406 // Emit UpdateTab event to trigger the close behavior
13407 item1.update(cx, |_, cx| {
13408 cx.emit(ItemEvent::UpdateTab);
13409 });
13410 cx.run_until_parked();
13411
13412 // Verify item1 was closed
13413 pane.read_with(cx, |pane, _| {
13414 assert_eq!(
13415 pane.items().count(),
13416 1,
13417 "Should have 1 item remaining after auto-close"
13418 );
13419 });
13420
13421 // Check navigation history after close
13422 let has_item = pane.read_with(cx, |pane, cx| {
13423 let mut has_item = false;
13424 pane.nav_history().for_each_entry(cx, &mut |entry, _| {
13425 if entry.item.id() == item1_id {
13426 has_item = true;
13427 }
13428 });
13429 has_item
13430 });
13431
13432 assert!(
13433 !has_item,
13434 "Navigation history should not contain closed item entries"
13435 );
13436 }
13437
13438 #[gpui::test]
13439 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
13440 cx: &mut TestAppContext,
13441 ) {
13442 init_test(cx);
13443
13444 let fs = FakeFs::new(cx.background_executor.clone());
13445 let project = Project::test(fs, [], cx).await;
13446 let (workspace, cx) =
13447 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13448 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13449
13450 let dirty_regular_buffer = cx.new(|cx| {
13451 TestItem::new(cx)
13452 .with_dirty(true)
13453 .with_label("1.txt")
13454 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
13455 });
13456 let dirty_regular_buffer_2 = cx.new(|cx| {
13457 TestItem::new(cx)
13458 .with_dirty(true)
13459 .with_label("2.txt")
13460 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
13461 });
13462 let clear_regular_buffer = cx.new(|cx| {
13463 TestItem::new(cx)
13464 .with_label("3.txt")
13465 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
13466 });
13467
13468 let dirty_multi_buffer = cx.new(|cx| {
13469 TestItem::new(cx)
13470 .with_dirty(true)
13471 .with_buffer_kind(ItemBufferKind::Multibuffer)
13472 .with_label("Fake Project Search")
13473 .with_project_items(&[
13474 dirty_regular_buffer.read(cx).project_items[0].clone(),
13475 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
13476 clear_regular_buffer.read(cx).project_items[0].clone(),
13477 ])
13478 });
13479 workspace.update_in(cx, |workspace, window, cx| {
13480 workspace.add_item(
13481 pane.clone(),
13482 Box::new(dirty_regular_buffer.clone()),
13483 None,
13484 false,
13485 false,
13486 window,
13487 cx,
13488 );
13489 workspace.add_item(
13490 pane.clone(),
13491 Box::new(dirty_regular_buffer_2.clone()),
13492 None,
13493 false,
13494 false,
13495 window,
13496 cx,
13497 );
13498 workspace.add_item(
13499 pane.clone(),
13500 Box::new(dirty_multi_buffer.clone()),
13501 None,
13502 false,
13503 false,
13504 window,
13505 cx,
13506 );
13507 });
13508
13509 pane.update_in(cx, |pane, window, cx| {
13510 pane.activate_item(2, true, true, window, cx);
13511 assert_eq!(
13512 pane.active_item().unwrap().item_id(),
13513 dirty_multi_buffer.item_id(),
13514 "Should select the multi buffer in the pane"
13515 );
13516 });
13517 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13518 pane.close_active_item(
13519 &CloseActiveItem {
13520 save_intent: None,
13521 close_pinned: false,
13522 },
13523 window,
13524 cx,
13525 )
13526 });
13527 cx.background_executor.run_until_parked();
13528 assert!(
13529 !cx.has_pending_prompt(),
13530 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
13531 );
13532 close_multi_buffer_task
13533 .await
13534 .expect("Closing multi buffer failed");
13535 pane.update(cx, |pane, cx| {
13536 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
13537 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
13538 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
13539 assert_eq!(
13540 pane.items()
13541 .map(|item| item.item_id())
13542 .sorted()
13543 .collect::<Vec<_>>(),
13544 vec![
13545 dirty_regular_buffer.item_id(),
13546 dirty_regular_buffer_2.item_id(),
13547 ],
13548 "Should have no multi buffer left in the pane"
13549 );
13550 assert!(dirty_regular_buffer.read(cx).is_dirty);
13551 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
13552 });
13553 }
13554
13555 #[gpui::test]
13556 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
13557 init_test(cx);
13558 let fs = FakeFs::new(cx.executor());
13559 let project = Project::test(fs, [], cx).await;
13560 let (multi_workspace, cx) =
13561 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
13562 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
13563
13564 // Add a new panel to the right dock, opening the dock and setting the
13565 // focus to the new panel.
13566 let panel = workspace.update_in(cx, |workspace, window, cx| {
13567 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
13568 workspace.add_panel(panel.clone(), window, cx);
13569
13570 workspace
13571 .right_dock()
13572 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
13573
13574 workspace.toggle_panel_focus::<TestPanel>(window, cx);
13575
13576 panel
13577 });
13578
13579 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
13580 // panel to the next valid position which, in this case, is the left
13581 // dock.
13582 cx.dispatch_action(MoveFocusedPanelToNextPosition);
13583 workspace.update(cx, |workspace, cx| {
13584 assert!(workspace.left_dock().read(cx).is_open());
13585 assert_eq!(panel.read(cx).position, DockPosition::Left);
13586 });
13587
13588 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
13589 // panel to the next valid position which, in this case, is the bottom
13590 // dock.
13591 cx.dispatch_action(MoveFocusedPanelToNextPosition);
13592 workspace.update(cx, |workspace, cx| {
13593 assert!(workspace.bottom_dock().read(cx).is_open());
13594 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
13595 });
13596
13597 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
13598 // around moving the panel to its initial position, the right dock.
13599 cx.dispatch_action(MoveFocusedPanelToNextPosition);
13600 workspace.update(cx, |workspace, cx| {
13601 assert!(workspace.right_dock().read(cx).is_open());
13602 assert_eq!(panel.read(cx).position, DockPosition::Right);
13603 });
13604
13605 // Remove focus from the panel, ensuring that, if the panel is not
13606 // focused, the `MoveFocusedPanelToNextPosition` action does not update
13607 // the panel's position, so the panel is still in the right dock.
13608 workspace.update_in(cx, |workspace, window, cx| {
13609 workspace.toggle_panel_focus::<TestPanel>(window, cx);
13610 });
13611
13612 cx.dispatch_action(MoveFocusedPanelToNextPosition);
13613 workspace.update(cx, |workspace, cx| {
13614 assert!(workspace.right_dock().read(cx).is_open());
13615 assert_eq!(panel.read(cx).position, DockPosition::Right);
13616 });
13617 }
13618
13619 #[gpui::test]
13620 async fn test_moving_items_create_panes(cx: &mut TestAppContext) {
13621 init_test(cx);
13622
13623 let fs = FakeFs::new(cx.executor());
13624 let project = Project::test(fs, [], cx).await;
13625 let (workspace, cx) =
13626 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
13627
13628 let item_1 = cx.new(|cx| {
13629 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
13630 });
13631 workspace.update_in(cx, |workspace, window, cx| {
13632 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
13633 workspace.move_item_to_pane_in_direction(
13634 &MoveItemToPaneInDirection {
13635 direction: SplitDirection::Right,
13636 focus: true,
13637 clone: false,
13638 },
13639 window,
13640 cx,
13641 );
13642 workspace.move_item_to_pane_at_index(
13643 &MoveItemToPane {
13644 destination: 3,
13645 focus: true,
13646 clone: false,
13647 },
13648 window,
13649 cx,
13650 );
13651
13652 assert_eq!(workspace.panes.len(), 1, "No new panes were created");
13653 assert_eq!(
13654 pane_items_paths(&workspace.active_pane, cx),
13655 vec!["first.txt".to_string()],
13656 "Single item was not moved anywhere"
13657 );
13658 });
13659
13660 let item_2 = cx.new(|cx| {
13661 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "second.txt", cx)])
13662 });
13663 workspace.update_in(cx, |workspace, window, cx| {
13664 workspace.add_item_to_active_pane(Box::new(item_2), None, true, window, cx);
13665 assert_eq!(
13666 pane_items_paths(&workspace.panes[0], cx),
13667 vec!["first.txt".to_string(), "second.txt".to_string()],
13668 );
13669 workspace.move_item_to_pane_in_direction(
13670 &MoveItemToPaneInDirection {
13671 direction: SplitDirection::Right,
13672 focus: true,
13673 clone: false,
13674 },
13675 window,
13676 cx,
13677 );
13678
13679 assert_eq!(workspace.panes.len(), 2, "A new pane should be created");
13680 assert_eq!(
13681 pane_items_paths(&workspace.panes[0], cx),
13682 vec!["first.txt".to_string()],
13683 "After moving, one item should be left in the original pane"
13684 );
13685 assert_eq!(
13686 pane_items_paths(&workspace.panes[1], cx),
13687 vec!["second.txt".to_string()],
13688 "New item should have been moved to the new pane"
13689 );
13690 });
13691
13692 let item_3 = cx.new(|cx| {
13693 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "third.txt", cx)])
13694 });
13695 workspace.update_in(cx, |workspace, window, cx| {
13696 let original_pane = workspace.panes[0].clone();
13697 workspace.set_active_pane(&original_pane, window, cx);
13698 workspace.add_item_to_active_pane(Box::new(item_3), None, true, window, cx);
13699 assert_eq!(workspace.panes.len(), 2, "No new panes were created");
13700 assert_eq!(
13701 pane_items_paths(&workspace.active_pane, cx),
13702 vec!["first.txt".to_string(), "third.txt".to_string()],
13703 "New pane should be ready to move one item out"
13704 );
13705
13706 workspace.move_item_to_pane_at_index(
13707 &MoveItemToPane {
13708 destination: 3,
13709 focus: true,
13710 clone: false,
13711 },
13712 window,
13713 cx,
13714 );
13715 assert_eq!(workspace.panes.len(), 3, "A new pane should be created");
13716 assert_eq!(
13717 pane_items_paths(&workspace.active_pane, cx),
13718 vec!["first.txt".to_string()],
13719 "After moving, one item should be left in the original pane"
13720 );
13721 assert_eq!(
13722 pane_items_paths(&workspace.panes[1], cx),
13723 vec!["second.txt".to_string()],
13724 "Previously created pane should be unchanged"
13725 );
13726 assert_eq!(
13727 pane_items_paths(&workspace.panes[2], cx),
13728 vec!["third.txt".to_string()],
13729 "New item should have been moved to the new pane"
13730 );
13731 });
13732 }
13733
13734 #[gpui::test]
13735 async fn test_moving_items_can_clone_panes(cx: &mut TestAppContext) {
13736 init_test(cx);
13737
13738 let fs = FakeFs::new(cx.executor());
13739 let project = Project::test(fs, [], cx).await;
13740 let (workspace, cx) =
13741 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
13742
13743 let item_1 = cx.new(|cx| {
13744 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
13745 });
13746 workspace.update_in(cx, |workspace, window, cx| {
13747 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
13748 workspace.move_item_to_pane_in_direction(
13749 &MoveItemToPaneInDirection {
13750 direction: SplitDirection::Right,
13751 focus: true,
13752 clone: true,
13753 },
13754 window,
13755 cx,
13756 );
13757 });
13758 cx.run_until_parked();
13759 workspace.update_in(cx, |workspace, window, cx| {
13760 workspace.move_item_to_pane_at_index(
13761 &MoveItemToPane {
13762 destination: 3,
13763 focus: true,
13764 clone: true,
13765 },
13766 window,
13767 cx,
13768 );
13769 });
13770 cx.run_until_parked();
13771
13772 workspace.update(cx, |workspace, cx| {
13773 assert_eq!(workspace.panes.len(), 3, "Two new panes were created");
13774 for pane in workspace.panes() {
13775 assert_eq!(
13776 pane_items_paths(pane, cx),
13777 vec!["first.txt".to_string()],
13778 "Single item exists in all panes"
13779 );
13780 }
13781 });
13782
13783 // verify that the active pane has been updated after waiting for the
13784 // pane focus event to fire and resolve
13785 workspace.read_with(cx, |workspace, _app| {
13786 assert_eq!(
13787 workspace.active_pane(),
13788 &workspace.panes[2],
13789 "The third pane should be the active one: {:?}",
13790 workspace.panes
13791 );
13792 })
13793 }
13794
13795 #[gpui::test]
13796 async fn test_close_item_in_all_panes(cx: &mut TestAppContext) {
13797 init_test(cx);
13798
13799 let fs = FakeFs::new(cx.executor());
13800 fs.insert_tree("/root", json!({ "test.txt": "" })).await;
13801
13802 let project = Project::test(fs, ["root".as_ref()], cx).await;
13803 let (workspace, cx) =
13804 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
13805
13806 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13807 // Add item to pane A with project path
13808 let item_a = cx.new(|cx| {
13809 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13810 });
13811 workspace.update_in(cx, |workspace, window, cx| {
13812 workspace.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
13813 });
13814
13815 // Split to create pane B
13816 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
13817 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
13818 });
13819
13820 // Add item with SAME project path to pane B, and pin it
13821 let item_b = cx.new(|cx| {
13822 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13823 });
13824 pane_b.update_in(cx, |pane, window, cx| {
13825 pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
13826 pane.set_pinned_count(1);
13827 });
13828
13829 assert_eq!(pane_a.read_with(cx, |pane, _| pane.items_len()), 1);
13830 assert_eq!(pane_b.read_with(cx, |pane, _| pane.items_len()), 1);
13831
13832 // close_pinned: false should only close the unpinned copy
13833 workspace.update_in(cx, |workspace, window, cx| {
13834 workspace.close_item_in_all_panes(
13835 &CloseItemInAllPanes {
13836 save_intent: Some(SaveIntent::Close),
13837 close_pinned: false,
13838 },
13839 window,
13840 cx,
13841 )
13842 });
13843 cx.executor().run_until_parked();
13844
13845 let item_count_a = pane_a.read_with(cx, |pane, _| pane.items_len());
13846 let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
13847 assert_eq!(item_count_a, 0, "Unpinned item in pane A should be closed");
13848 assert_eq!(item_count_b, 1, "Pinned item in pane B should remain");
13849
13850 // Split again, seeing as closing the previous item also closed its
13851 // pane, so only pane remains, which does not allow us to properly test
13852 // that both items close when `close_pinned: true`.
13853 let pane_c = workspace.update_in(cx, |workspace, window, cx| {
13854 workspace.split_pane(pane_b.clone(), SplitDirection::Right, window, cx)
13855 });
13856
13857 // Add an item with the same project path to pane C so that
13858 // close_item_in_all_panes can determine what to close across all panes
13859 // (it reads the active item from the active pane, and split_pane
13860 // creates an empty pane).
13861 let item_c = cx.new(|cx| {
13862 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13863 });
13864 pane_c.update_in(cx, |pane, window, cx| {
13865 pane.add_item(Box::new(item_c.clone()), true, true, None, window, cx);
13866 });
13867
13868 // close_pinned: true should close the pinned copy too
13869 workspace.update_in(cx, |workspace, window, cx| {
13870 let panes_count = workspace.panes().len();
13871 assert_eq!(panes_count, 2, "Workspace should have two panes (B and C)");
13872
13873 workspace.close_item_in_all_panes(
13874 &CloseItemInAllPanes {
13875 save_intent: Some(SaveIntent::Close),
13876 close_pinned: true,
13877 },
13878 window,
13879 cx,
13880 )
13881 });
13882 cx.executor().run_until_parked();
13883
13884 let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
13885 let item_count_c = pane_c.read_with(cx, |pane, _| pane.items_len());
13886 assert_eq!(item_count_b, 0, "Pinned item in pane B should be closed");
13887 assert_eq!(item_count_c, 0, "Unpinned item in pane C should be closed");
13888 }
13889
13890 mod register_project_item_tests {
13891
13892 use super::*;
13893
13894 // View
13895 struct TestPngItemView {
13896 focus_handle: FocusHandle,
13897 }
13898 // Model
13899 struct TestPngItem {}
13900
13901 impl project::ProjectItem for TestPngItem {
13902 fn try_open(
13903 _project: &Entity<Project>,
13904 path: &ProjectPath,
13905 cx: &mut App,
13906 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
13907 if path.path.extension().unwrap() == "png" {
13908 Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestPngItem {}))))
13909 } else {
13910 None
13911 }
13912 }
13913
13914 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
13915 None
13916 }
13917
13918 fn project_path(&self, _: &App) -> Option<ProjectPath> {
13919 None
13920 }
13921
13922 fn is_dirty(&self) -> bool {
13923 false
13924 }
13925 }
13926
13927 impl Item for TestPngItemView {
13928 type Event = ();
13929 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
13930 "".into()
13931 }
13932 }
13933 impl EventEmitter<()> for TestPngItemView {}
13934 impl Focusable for TestPngItemView {
13935 fn focus_handle(&self, _cx: &App) -> FocusHandle {
13936 self.focus_handle.clone()
13937 }
13938 }
13939
13940 impl Render for TestPngItemView {
13941 fn render(
13942 &mut self,
13943 _window: &mut Window,
13944 _cx: &mut Context<Self>,
13945 ) -> impl IntoElement {
13946 Empty
13947 }
13948 }
13949
13950 impl ProjectItem for TestPngItemView {
13951 type Item = TestPngItem;
13952
13953 fn for_project_item(
13954 _project: Entity<Project>,
13955 _pane: Option<&Pane>,
13956 _item: Entity<Self::Item>,
13957 _: &mut Window,
13958 cx: &mut Context<Self>,
13959 ) -> Self
13960 where
13961 Self: Sized,
13962 {
13963 Self {
13964 focus_handle: cx.focus_handle(),
13965 }
13966 }
13967 }
13968
13969 // View
13970 struct TestIpynbItemView {
13971 focus_handle: FocusHandle,
13972 }
13973 // Model
13974 struct TestIpynbItem {}
13975
13976 impl project::ProjectItem for TestIpynbItem {
13977 fn try_open(
13978 _project: &Entity<Project>,
13979 path: &ProjectPath,
13980 cx: &mut App,
13981 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
13982 if path.path.extension().unwrap() == "ipynb" {
13983 Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestIpynbItem {}))))
13984 } else {
13985 None
13986 }
13987 }
13988
13989 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
13990 None
13991 }
13992
13993 fn project_path(&self, _: &App) -> Option<ProjectPath> {
13994 None
13995 }
13996
13997 fn is_dirty(&self) -> bool {
13998 false
13999 }
14000 }
14001
14002 impl Item for TestIpynbItemView {
14003 type Event = ();
14004 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14005 "".into()
14006 }
14007 }
14008 impl EventEmitter<()> for TestIpynbItemView {}
14009 impl Focusable for TestIpynbItemView {
14010 fn focus_handle(&self, _cx: &App) -> FocusHandle {
14011 self.focus_handle.clone()
14012 }
14013 }
14014
14015 impl Render for TestIpynbItemView {
14016 fn render(
14017 &mut self,
14018 _window: &mut Window,
14019 _cx: &mut Context<Self>,
14020 ) -> impl IntoElement {
14021 Empty
14022 }
14023 }
14024
14025 impl ProjectItem for TestIpynbItemView {
14026 type Item = TestIpynbItem;
14027
14028 fn for_project_item(
14029 _project: Entity<Project>,
14030 _pane: Option<&Pane>,
14031 _item: Entity<Self::Item>,
14032 _: &mut Window,
14033 cx: &mut Context<Self>,
14034 ) -> Self
14035 where
14036 Self: Sized,
14037 {
14038 Self {
14039 focus_handle: cx.focus_handle(),
14040 }
14041 }
14042 }
14043
14044 struct TestAlternatePngItemView {
14045 focus_handle: FocusHandle,
14046 }
14047
14048 impl Item for TestAlternatePngItemView {
14049 type Event = ();
14050 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14051 "".into()
14052 }
14053 }
14054
14055 impl EventEmitter<()> for TestAlternatePngItemView {}
14056 impl Focusable for TestAlternatePngItemView {
14057 fn focus_handle(&self, _cx: &App) -> FocusHandle {
14058 self.focus_handle.clone()
14059 }
14060 }
14061
14062 impl Render for TestAlternatePngItemView {
14063 fn render(
14064 &mut self,
14065 _window: &mut Window,
14066 _cx: &mut Context<Self>,
14067 ) -> impl IntoElement {
14068 Empty
14069 }
14070 }
14071
14072 impl ProjectItem for TestAlternatePngItemView {
14073 type Item = TestPngItem;
14074
14075 fn for_project_item(
14076 _project: Entity<Project>,
14077 _pane: Option<&Pane>,
14078 _item: Entity<Self::Item>,
14079 _: &mut Window,
14080 cx: &mut Context<Self>,
14081 ) -> Self
14082 where
14083 Self: Sized,
14084 {
14085 Self {
14086 focus_handle: cx.focus_handle(),
14087 }
14088 }
14089 }
14090
14091 #[gpui::test]
14092 async fn test_register_project_item(cx: &mut TestAppContext) {
14093 init_test(cx);
14094
14095 cx.update(|cx| {
14096 register_project_item::<TestPngItemView>(cx);
14097 register_project_item::<TestIpynbItemView>(cx);
14098 });
14099
14100 let fs = FakeFs::new(cx.executor());
14101 fs.insert_tree(
14102 "/root1",
14103 json!({
14104 "one.png": "BINARYDATAHERE",
14105 "two.ipynb": "{ totally a notebook }",
14106 "three.txt": "editing text, sure why not?"
14107 }),
14108 )
14109 .await;
14110
14111 let project = Project::test(fs, ["root1".as_ref()], cx).await;
14112 let (workspace, cx) =
14113 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14114
14115 let worktree_id = project.update(cx, |project, cx| {
14116 project.worktrees(cx).next().unwrap().read(cx).id()
14117 });
14118
14119 let handle = workspace
14120 .update_in(cx, |workspace, window, cx| {
14121 let project_path = (worktree_id, rel_path("one.png"));
14122 workspace.open_path(project_path, None, true, window, cx)
14123 })
14124 .await
14125 .unwrap();
14126
14127 // Now we can check if the handle we got back errored or not
14128 assert_eq!(
14129 handle.to_any_view().entity_type(),
14130 TypeId::of::<TestPngItemView>()
14131 );
14132
14133 let handle = workspace
14134 .update_in(cx, |workspace, window, cx| {
14135 let project_path = (worktree_id, rel_path("two.ipynb"));
14136 workspace.open_path(project_path, None, true, window, cx)
14137 })
14138 .await
14139 .unwrap();
14140
14141 assert_eq!(
14142 handle.to_any_view().entity_type(),
14143 TypeId::of::<TestIpynbItemView>()
14144 );
14145
14146 let handle = workspace
14147 .update_in(cx, |workspace, window, cx| {
14148 let project_path = (worktree_id, rel_path("three.txt"));
14149 workspace.open_path(project_path, None, true, window, cx)
14150 })
14151 .await;
14152 assert!(handle.is_err());
14153 }
14154
14155 #[gpui::test]
14156 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
14157 init_test(cx);
14158
14159 cx.update(|cx| {
14160 register_project_item::<TestPngItemView>(cx);
14161 register_project_item::<TestAlternatePngItemView>(cx);
14162 });
14163
14164 let fs = FakeFs::new(cx.executor());
14165 fs.insert_tree(
14166 "/root1",
14167 json!({
14168 "one.png": "BINARYDATAHERE",
14169 "two.ipynb": "{ totally a notebook }",
14170 "three.txt": "editing text, sure why not?"
14171 }),
14172 )
14173 .await;
14174 let project = Project::test(fs, ["root1".as_ref()], cx).await;
14175 let (workspace, cx) =
14176 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14177 let worktree_id = project.update(cx, |project, cx| {
14178 project.worktrees(cx).next().unwrap().read(cx).id()
14179 });
14180
14181 let handle = workspace
14182 .update_in(cx, |workspace, window, cx| {
14183 let project_path = (worktree_id, rel_path("one.png"));
14184 workspace.open_path(project_path, None, true, window, cx)
14185 })
14186 .await
14187 .unwrap();
14188
14189 // This _must_ be the second item registered
14190 assert_eq!(
14191 handle.to_any_view().entity_type(),
14192 TypeId::of::<TestAlternatePngItemView>()
14193 );
14194
14195 let handle = workspace
14196 .update_in(cx, |workspace, window, cx| {
14197 let project_path = (worktree_id, rel_path("three.txt"));
14198 workspace.open_path(project_path, None, true, window, cx)
14199 })
14200 .await;
14201 assert!(handle.is_err());
14202 }
14203 }
14204
14205 #[gpui::test]
14206 async fn test_status_bar_visibility(cx: &mut TestAppContext) {
14207 init_test(cx);
14208
14209 let fs = FakeFs::new(cx.executor());
14210 let project = Project::test(fs, [], cx).await;
14211 let (workspace, _cx) =
14212 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14213
14214 // Test with status bar shown (default)
14215 workspace.read_with(cx, |workspace, cx| {
14216 let visible = workspace.status_bar_visible(cx);
14217 assert!(visible, "Status bar should be visible by default");
14218 });
14219
14220 // Test with status bar hidden
14221 cx.update_global(|store: &mut SettingsStore, cx| {
14222 store.update_user_settings(cx, |settings| {
14223 settings.status_bar.get_or_insert_default().show = Some(false);
14224 });
14225 });
14226
14227 workspace.read_with(cx, |workspace, cx| {
14228 let visible = workspace.status_bar_visible(cx);
14229 assert!(!visible, "Status bar should be hidden when show is false");
14230 });
14231
14232 // Test with status bar shown explicitly
14233 cx.update_global(|store: &mut SettingsStore, cx| {
14234 store.update_user_settings(cx, |settings| {
14235 settings.status_bar.get_or_insert_default().show = Some(true);
14236 });
14237 });
14238
14239 workspace.read_with(cx, |workspace, cx| {
14240 let visible = workspace.status_bar_visible(cx);
14241 assert!(visible, "Status bar should be visible when show is true");
14242 });
14243 }
14244
14245 #[gpui::test]
14246 async fn test_pane_close_active_item(cx: &mut TestAppContext) {
14247 init_test(cx);
14248
14249 let fs = FakeFs::new(cx.executor());
14250 let project = Project::test(fs, [], cx).await;
14251 let (multi_workspace, cx) =
14252 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
14253 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
14254 let panel = workspace.update_in(cx, |workspace, window, cx| {
14255 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14256 workspace.add_panel(panel.clone(), window, cx);
14257
14258 workspace
14259 .right_dock()
14260 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
14261
14262 panel
14263 });
14264
14265 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14266 let item_a = cx.new(TestItem::new);
14267 let item_b = cx.new(TestItem::new);
14268 let item_a_id = item_a.entity_id();
14269 let item_b_id = item_b.entity_id();
14270
14271 pane.update_in(cx, |pane, window, cx| {
14272 pane.add_item(Box::new(item_a.clone()), true, true, None, window, cx);
14273 pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
14274 });
14275
14276 pane.read_with(cx, |pane, _| {
14277 assert_eq!(pane.items_len(), 2);
14278 assert_eq!(pane.active_item().unwrap().item_id(), item_b_id);
14279 });
14280
14281 workspace.update_in(cx, |workspace, window, cx| {
14282 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14283 });
14284
14285 workspace.update_in(cx, |_, window, cx| {
14286 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
14287 });
14288
14289 // Assert that the `pane::CloseActiveItem` action is handled at the
14290 // workspace level when one of the dock panels is focused and, in that
14291 // case, the center pane's active item is closed but the focus is not
14292 // moved.
14293 cx.dispatch_action(pane::CloseActiveItem::default());
14294 cx.run_until_parked();
14295
14296 pane.read_with(cx, |pane, _| {
14297 assert_eq!(pane.items_len(), 1);
14298 assert_eq!(pane.active_item().unwrap().item_id(), item_a_id);
14299 });
14300
14301 workspace.update_in(cx, |workspace, window, cx| {
14302 assert!(workspace.right_dock().read(cx).is_open());
14303 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
14304 });
14305 }
14306
14307 #[gpui::test]
14308 async fn test_panel_zoom_preserved_across_workspace_switch(cx: &mut TestAppContext) {
14309 init_test(cx);
14310 let fs = FakeFs::new(cx.executor());
14311
14312 let project_a = Project::test(fs.clone(), [], cx).await;
14313 let project_b = Project::test(fs, [], cx).await;
14314
14315 let multi_workspace_handle =
14316 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
14317 cx.run_until_parked();
14318
14319 let workspace_a = multi_workspace_handle
14320 .read_with(cx, |mw, _| mw.workspace().clone())
14321 .unwrap();
14322
14323 let _workspace_b = multi_workspace_handle
14324 .update(cx, |mw, window, cx| {
14325 mw.test_add_workspace(project_b, window, cx)
14326 })
14327 .unwrap();
14328
14329 // Switch to workspace A
14330 multi_workspace_handle
14331 .update(cx, |mw, window, cx| {
14332 mw.activate_index(0, window, cx);
14333 })
14334 .unwrap();
14335
14336 let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
14337
14338 // Add a panel to workspace A's right dock and open the dock
14339 let panel = workspace_a.update_in(cx, |workspace, window, cx| {
14340 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14341 workspace.add_panel(panel.clone(), window, cx);
14342 workspace
14343 .right_dock()
14344 .update(cx, |dock, cx| dock.set_open(true, window, cx));
14345 panel
14346 });
14347
14348 // Focus the panel through the workspace (matching existing test pattern)
14349 workspace_a.update_in(cx, |workspace, window, cx| {
14350 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14351 });
14352
14353 // Zoom the panel
14354 panel.update_in(cx, |panel, window, cx| {
14355 panel.set_zoomed(true, window, cx);
14356 });
14357
14358 // Verify the panel is zoomed and the dock is open
14359 workspace_a.update_in(cx, |workspace, window, cx| {
14360 assert!(
14361 workspace.right_dock().read(cx).is_open(),
14362 "dock should be open before switch"
14363 );
14364 assert!(
14365 panel.is_zoomed(window, cx),
14366 "panel should be zoomed before switch"
14367 );
14368 assert!(
14369 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
14370 "panel should be focused before switch"
14371 );
14372 });
14373
14374 // Switch to workspace B
14375 multi_workspace_handle
14376 .update(cx, |mw, window, cx| {
14377 mw.activate_index(1, window, cx);
14378 })
14379 .unwrap();
14380 cx.run_until_parked();
14381
14382 // Switch back to workspace A
14383 multi_workspace_handle
14384 .update(cx, |mw, window, cx| {
14385 mw.activate_index(0, window, cx);
14386 })
14387 .unwrap();
14388 cx.run_until_parked();
14389
14390 // Verify the panel is still zoomed and the dock is still open
14391 workspace_a.update_in(cx, |workspace, window, cx| {
14392 assert!(
14393 workspace.right_dock().read(cx).is_open(),
14394 "dock should still be open after switching back"
14395 );
14396 assert!(
14397 panel.is_zoomed(window, cx),
14398 "panel should still be zoomed after switching back"
14399 );
14400 });
14401 }
14402
14403 fn pane_items_paths(pane: &Entity<Pane>, cx: &App) -> Vec<String> {
14404 pane.read(cx)
14405 .items()
14406 .flat_map(|item| {
14407 item.project_paths(cx)
14408 .into_iter()
14409 .map(|path| path.path.display(PathStyle::local()).into_owned())
14410 })
14411 .collect()
14412 }
14413
14414 pub fn init_test(cx: &mut TestAppContext) {
14415 cx.update(|cx| {
14416 let settings_store = SettingsStore::test(cx);
14417 cx.set_global(settings_store);
14418 cx.set_global(db::AppDatabase::test_new());
14419 theme_settings::init(theme::LoadThemes::JustBase, cx);
14420 });
14421 }
14422
14423 #[gpui::test]
14424 async fn test_toggle_theme_mode_persists_and_updates_active_theme(cx: &mut TestAppContext) {
14425 use settings::{ThemeName, ThemeSelection};
14426 use theme::SystemAppearance;
14427 use zed_actions::theme::ToggleMode;
14428
14429 init_test(cx);
14430
14431 let fs = FakeFs::new(cx.executor());
14432 let settings_fs: Arc<dyn fs::Fs> = fs.clone();
14433
14434 fs.insert_tree(path!("/root"), json!({ "file.rs": "fn main() {}\n" }))
14435 .await;
14436
14437 // Build a test project and workspace view so the test can invoke
14438 // the workspace action handler the same way the UI would.
14439 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
14440 let (workspace, cx) =
14441 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14442
14443 // Seed the settings file with a plain static light theme so the
14444 // first toggle always starts from a known persisted state.
14445 workspace.update_in(cx, |_workspace, _window, cx| {
14446 *SystemAppearance::global_mut(cx) = SystemAppearance(theme::Appearance::Light);
14447 settings::update_settings_file(settings_fs.clone(), cx, |settings, _cx| {
14448 settings.theme.theme = Some(ThemeSelection::Static(ThemeName("One Light".into())));
14449 });
14450 });
14451 cx.executor().advance_clock(Duration::from_millis(200));
14452 cx.run_until_parked();
14453
14454 // Confirm the initial persisted settings contain the static theme
14455 // we just wrote before any toggling happens.
14456 let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
14457 assert!(settings_text.contains(r#""theme": "One Light""#));
14458
14459 // Toggle once. This should migrate the persisted theme settings
14460 // into light/dark slots and enable system mode.
14461 workspace.update_in(cx, |workspace, window, cx| {
14462 workspace.toggle_theme_mode(&ToggleMode, window, cx);
14463 });
14464 cx.executor().advance_clock(Duration::from_millis(200));
14465 cx.run_until_parked();
14466
14467 // 1. Static -> Dynamic
14468 // this assertion checks theme changed from static to dynamic.
14469 let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
14470 let parsed: serde_json::Value = settings::parse_json_with_comments(&settings_text).unwrap();
14471 assert_eq!(
14472 parsed["theme"],
14473 serde_json::json!({
14474 "mode": "system",
14475 "light": "One Light",
14476 "dark": "One Dark"
14477 })
14478 );
14479
14480 // 2. Toggle again, suppose it will change the mode to light
14481 workspace.update_in(cx, |workspace, window, cx| {
14482 workspace.toggle_theme_mode(&ToggleMode, window, cx);
14483 });
14484 cx.executor().advance_clock(Duration::from_millis(200));
14485 cx.run_until_parked();
14486
14487 let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
14488 assert!(settings_text.contains(r#""mode": "light""#));
14489 }
14490
14491 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
14492 let item = TestProjectItem::new(id, path, cx);
14493 item.update(cx, |item, _| {
14494 item.is_dirty = true;
14495 });
14496 item
14497 }
14498
14499 #[gpui::test]
14500 async fn test_zoomed_panel_without_pane_preserved_on_center_focus(
14501 cx: &mut gpui::TestAppContext,
14502 ) {
14503 init_test(cx);
14504 let fs = FakeFs::new(cx.executor());
14505
14506 let project = Project::test(fs, [], cx).await;
14507 let (workspace, cx) =
14508 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
14509
14510 let panel = workspace.update_in(cx, |workspace, window, cx| {
14511 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14512 workspace.add_panel(panel.clone(), window, cx);
14513 workspace
14514 .right_dock()
14515 .update(cx, |dock, cx| dock.set_open(true, window, cx));
14516 panel
14517 });
14518
14519 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14520 pane.update_in(cx, |pane, window, cx| {
14521 let item = cx.new(TestItem::new);
14522 pane.add_item(Box::new(item), true, true, None, window, cx);
14523 });
14524
14525 // Transfer focus to the panel, then zoom it. Using toggle_panel_focus
14526 // mirrors the real-world flow and avoids side effects from directly
14527 // focusing the panel while the center pane is active.
14528 workspace.update_in(cx, |workspace, window, cx| {
14529 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14530 });
14531
14532 panel.update_in(cx, |panel, window, cx| {
14533 panel.set_zoomed(true, window, cx);
14534 });
14535
14536 workspace.update_in(cx, |workspace, window, cx| {
14537 assert!(workspace.right_dock().read(cx).is_open());
14538 assert!(panel.is_zoomed(window, cx));
14539 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
14540 });
14541
14542 // Simulate a spurious pane::Event::Focus on the center pane while the
14543 // panel still has focus. This mirrors what happens during macOS window
14544 // activation: the center pane fires a focus event even though actual
14545 // focus remains on the dock panel.
14546 pane.update_in(cx, |_, _, cx| {
14547 cx.emit(pane::Event::Focus);
14548 });
14549
14550 // The dock must remain open because the panel had focus at the time the
14551 // event was processed. Before the fix, dock_to_preserve was None for
14552 // panels that don't implement pane(), causing the dock to close.
14553 workspace.update_in(cx, |workspace, window, cx| {
14554 assert!(
14555 workspace.right_dock().read(cx).is_open(),
14556 "Dock should stay open when its zoomed panel (without pane()) still has focus"
14557 );
14558 assert!(panel.is_zoomed(window, cx));
14559 });
14560 }
14561
14562 #[gpui::test]
14563 async fn test_panels_stay_open_after_position_change_and_settings_update(
14564 cx: &mut gpui::TestAppContext,
14565 ) {
14566 init_test(cx);
14567 let fs = FakeFs::new(cx.executor());
14568 let project = Project::test(fs, [], cx).await;
14569 let (workspace, cx) =
14570 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
14571
14572 // Add two panels to the left dock and open it.
14573 let (panel_a, panel_b) = workspace.update_in(cx, |workspace, window, cx| {
14574 let panel_a = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
14575 let panel_b = cx.new(|cx| TestPanel::new(DockPosition::Left, 101, cx));
14576 workspace.add_panel(panel_a.clone(), window, cx);
14577 workspace.add_panel(panel_b.clone(), window, cx);
14578 workspace.left_dock().update(cx, |dock, cx| {
14579 dock.set_open(true, window, cx);
14580 dock.activate_panel(0, window, cx);
14581 });
14582 (panel_a, panel_b)
14583 });
14584
14585 workspace.update_in(cx, |workspace, _, cx| {
14586 assert!(workspace.left_dock().read(cx).is_open());
14587 });
14588
14589 // Simulate a feature flag changing default dock positions: both panels
14590 // move from Left to Right.
14591 workspace.update_in(cx, |_workspace, _window, cx| {
14592 panel_a.update(cx, |p, _cx| p.position = DockPosition::Right);
14593 panel_b.update(cx, |p, _cx| p.position = DockPosition::Right);
14594 cx.update_global::<SettingsStore, _>(|_, _| {});
14595 });
14596
14597 // Both panels should now be in the right dock.
14598 workspace.update_in(cx, |workspace, _, cx| {
14599 let right_dock = workspace.right_dock().read(cx);
14600 assert_eq!(right_dock.panels_len(), 2);
14601 });
14602
14603 // Open the right dock and activate panel_b (simulating the user
14604 // opening the panel after it moved).
14605 workspace.update_in(cx, |workspace, window, cx| {
14606 workspace.right_dock().update(cx, |dock, cx| {
14607 dock.set_open(true, window, cx);
14608 dock.activate_panel(1, window, cx);
14609 });
14610 });
14611
14612 // Now trigger another SettingsStore change
14613 workspace.update_in(cx, |_workspace, _window, cx| {
14614 cx.update_global::<SettingsStore, _>(|_, _| {});
14615 });
14616
14617 workspace.update_in(cx, |workspace, _, cx| {
14618 assert!(
14619 workspace.right_dock().read(cx).is_open(),
14620 "Right dock should still be open after a settings change"
14621 );
14622 assert_eq!(
14623 workspace.right_dock().read(cx).panels_len(),
14624 2,
14625 "Both panels should still be in the right dock"
14626 );
14627 });
14628 }
14629}