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,
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(|_: &Open, cx: &mut App| {
736 let app_state = AppState::global(cx);
737 prompt_and_open_paths(
738 app_state,
739 PathPromptOptions {
740 files: true,
741 directories: true,
742 multiple: true,
743 prompt: None,
744 },
745 cx,
746 );
747 })
748 .on_action(|_: &OpenFiles, cx: &mut App| {
749 let directories = cx.can_select_mixed_files_and_dirs();
750 let app_state = AppState::global(cx);
751 prompt_and_open_paths(
752 app_state,
753 PathPromptOptions {
754 files: true,
755 directories,
756 multiple: true,
757 prompt: None,
758 },
759 cx,
760 );
761 });
762}
763
764type BuildProjectItemFn =
765 fn(AnyEntity, Entity<Project>, Option<&Pane>, &mut Window, &mut App) -> Box<dyn ItemHandle>;
766
767type BuildProjectItemForPathFn =
768 fn(
769 &Entity<Project>,
770 &ProjectPath,
771 &mut Window,
772 &mut App,
773 ) -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
774
775#[derive(Clone, Default)]
776struct ProjectItemRegistry {
777 build_project_item_fns_by_type: HashMap<TypeId, BuildProjectItemFn>,
778 build_project_item_for_path_fns: Vec<BuildProjectItemForPathFn>,
779}
780
781impl ProjectItemRegistry {
782 fn register<T: ProjectItem>(&mut self) {
783 self.build_project_item_fns_by_type.insert(
784 TypeId::of::<T::Item>(),
785 |item, project, pane, window, cx| {
786 let item = item.downcast().unwrap();
787 Box::new(cx.new(|cx| T::for_project_item(project, pane, item, window, cx)))
788 as Box<dyn ItemHandle>
789 },
790 );
791 self.build_project_item_for_path_fns
792 .push(|project, project_path, window, cx| {
793 let project_path = project_path.clone();
794 let is_file = project
795 .read(cx)
796 .entry_for_path(&project_path, cx)
797 .is_some_and(|entry| entry.is_file());
798 let entry_abs_path = project.read(cx).absolute_path(&project_path, cx);
799 let is_local = project.read(cx).is_local();
800 let project_item =
801 <T::Item as project::ProjectItem>::try_open(project, &project_path, cx)?;
802 let project = project.clone();
803 Some(window.spawn(cx, async move |cx| {
804 match project_item.await.with_context(|| {
805 format!(
806 "opening project path {:?}",
807 entry_abs_path.as_deref().unwrap_or(&project_path.path.as_std_path())
808 )
809 }) {
810 Ok(project_item) => {
811 let project_item = project_item;
812 let project_entry_id: Option<ProjectEntryId> =
813 project_item.read_with(cx, project::ProjectItem::entry_id);
814 let build_workspace_item = Box::new(
815 |pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
816 Box::new(cx.new(|cx| {
817 T::for_project_item(
818 project,
819 Some(pane),
820 project_item,
821 window,
822 cx,
823 )
824 })) as Box<dyn ItemHandle>
825 },
826 ) as Box<_>;
827 Ok((project_entry_id, build_workspace_item))
828 }
829 Err(e) => {
830 log::warn!("Failed to open a project item: {e:#}");
831 if e.error_code() == ErrorCode::Internal {
832 if let Some(abs_path) =
833 entry_abs_path.as_deref().filter(|_| is_file)
834 {
835 if let Some(broken_project_item_view) =
836 cx.update(|window, cx| {
837 T::for_broken_project_item(
838 abs_path, is_local, &e, window, cx,
839 )
840 })?
841 {
842 let build_workspace_item = Box::new(
843 move |_: &mut Pane, _: &mut Window, cx: &mut Context<Pane>| {
844 cx.new(|_| broken_project_item_view).boxed_clone()
845 },
846 )
847 as Box<_>;
848 return Ok((None, build_workspace_item));
849 }
850 }
851 }
852 Err(e)
853 }
854 }
855 }))
856 });
857 }
858
859 fn open_path(
860 &self,
861 project: &Entity<Project>,
862 path: &ProjectPath,
863 window: &mut Window,
864 cx: &mut App,
865 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
866 let Some(open_project_item) = self
867 .build_project_item_for_path_fns
868 .iter()
869 .rev()
870 .find_map(|open_project_item| open_project_item(project, path, window, cx))
871 else {
872 return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
873 };
874 open_project_item
875 }
876
877 fn build_item<T: project::ProjectItem>(
878 &self,
879 item: Entity<T>,
880 project: Entity<Project>,
881 pane: Option<&Pane>,
882 window: &mut Window,
883 cx: &mut App,
884 ) -> Option<Box<dyn ItemHandle>> {
885 let build = self
886 .build_project_item_fns_by_type
887 .get(&TypeId::of::<T>())?;
888 Some(build(item.into_any(), project, pane, window, cx))
889 }
890}
891
892type WorkspaceItemBuilder =
893 Box<dyn FnOnce(&mut Pane, &mut Window, &mut Context<Pane>) -> Box<dyn ItemHandle>>;
894
895impl Global for ProjectItemRegistry {}
896
897/// Registers a [ProjectItem] for the app. When opening a file, all the registered
898/// items will get a chance to open the file, starting from the project item that
899/// was added last.
900pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
901 cx.default_global::<ProjectItemRegistry>().register::<I>();
902}
903
904#[derive(Default)]
905pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
906
907struct FollowableViewDescriptor {
908 from_state_proto: fn(
909 Entity<Workspace>,
910 ViewId,
911 &mut Option<proto::view::Variant>,
912 &mut Window,
913 &mut App,
914 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
915 to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
916}
917
918impl Global for FollowableViewRegistry {}
919
920impl FollowableViewRegistry {
921 pub fn register<I: FollowableItem>(cx: &mut App) {
922 cx.default_global::<Self>().0.insert(
923 TypeId::of::<I>(),
924 FollowableViewDescriptor {
925 from_state_proto: |workspace, id, state, window, cx| {
926 I::from_state_proto(workspace, id, state, window, cx).map(|task| {
927 cx.foreground_executor()
928 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
929 })
930 },
931 to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
932 },
933 );
934 }
935
936 pub fn from_state_proto(
937 workspace: Entity<Workspace>,
938 view_id: ViewId,
939 mut state: Option<proto::view::Variant>,
940 window: &mut Window,
941 cx: &mut App,
942 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
943 cx.update_default_global(|this: &mut Self, cx| {
944 this.0.values().find_map(|descriptor| {
945 (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, window, cx)
946 })
947 })
948 }
949
950 pub fn to_followable_view(
951 view: impl Into<AnyView>,
952 cx: &App,
953 ) -> Option<Box<dyn FollowableItemHandle>> {
954 let this = cx.try_global::<Self>()?;
955 let view = view.into();
956 let descriptor = this.0.get(&view.entity_type())?;
957 Some((descriptor.to_followable_view)(&view))
958 }
959}
960
961#[derive(Copy, Clone)]
962struct SerializableItemDescriptor {
963 deserialize: fn(
964 Entity<Project>,
965 WeakEntity<Workspace>,
966 WorkspaceId,
967 ItemId,
968 &mut Window,
969 &mut Context<Pane>,
970 ) -> Task<Result<Box<dyn ItemHandle>>>,
971 cleanup: fn(WorkspaceId, Vec<ItemId>, &mut Window, &mut App) -> Task<Result<()>>,
972 view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
973}
974
975#[derive(Default)]
976struct SerializableItemRegistry {
977 descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
978 descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
979}
980
981impl Global for SerializableItemRegistry {}
982
983impl SerializableItemRegistry {
984 fn deserialize(
985 item_kind: &str,
986 project: Entity<Project>,
987 workspace: WeakEntity<Workspace>,
988 workspace_id: WorkspaceId,
989 item_item: ItemId,
990 window: &mut Window,
991 cx: &mut Context<Pane>,
992 ) -> Task<Result<Box<dyn ItemHandle>>> {
993 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
994 return Task::ready(Err(anyhow!(
995 "cannot deserialize {}, descriptor not found",
996 item_kind
997 )));
998 };
999
1000 (descriptor.deserialize)(project, workspace, workspace_id, item_item, window, cx)
1001 }
1002
1003 fn cleanup(
1004 item_kind: &str,
1005 workspace_id: WorkspaceId,
1006 loaded_items: Vec<ItemId>,
1007 window: &mut Window,
1008 cx: &mut App,
1009 ) -> Task<Result<()>> {
1010 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
1011 return Task::ready(Err(anyhow!(
1012 "cannot cleanup {}, descriptor not found",
1013 item_kind
1014 )));
1015 };
1016
1017 (descriptor.cleanup)(workspace_id, loaded_items, window, cx)
1018 }
1019
1020 fn view_to_serializable_item_handle(
1021 view: AnyView,
1022 cx: &App,
1023 ) -> Option<Box<dyn SerializableItemHandle>> {
1024 let this = cx.try_global::<Self>()?;
1025 let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
1026 Some((descriptor.view_to_serializable_item)(view))
1027 }
1028
1029 fn descriptor(item_kind: &str, cx: &App) -> Option<SerializableItemDescriptor> {
1030 let this = cx.try_global::<Self>()?;
1031 this.descriptors_by_kind.get(item_kind).copied()
1032 }
1033}
1034
1035pub fn register_serializable_item<I: SerializableItem>(cx: &mut App) {
1036 let serialized_item_kind = I::serialized_item_kind();
1037
1038 let registry = cx.default_global::<SerializableItemRegistry>();
1039 let descriptor = SerializableItemDescriptor {
1040 deserialize: |project, workspace, workspace_id, item_id, window, cx| {
1041 let task = I::deserialize(project, workspace, workspace_id, item_id, window, cx);
1042 cx.foreground_executor()
1043 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
1044 },
1045 cleanup: |workspace_id, loaded_items, window, cx| {
1046 I::cleanup(workspace_id, loaded_items, window, cx)
1047 },
1048 view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
1049 };
1050 registry
1051 .descriptors_by_kind
1052 .insert(Arc::from(serialized_item_kind), descriptor);
1053 registry
1054 .descriptors_by_type
1055 .insert(TypeId::of::<I>(), descriptor);
1056}
1057
1058pub struct AppState {
1059 pub languages: Arc<LanguageRegistry>,
1060 pub client: Arc<Client>,
1061 pub user_store: Entity<UserStore>,
1062 pub workspace_store: Entity<WorkspaceStore>,
1063 pub fs: Arc<dyn fs::Fs>,
1064 pub build_window_options: fn(Option<Uuid>, &mut App) -> WindowOptions,
1065 pub node_runtime: NodeRuntime,
1066 pub session: Entity<AppSession>,
1067}
1068
1069struct GlobalAppState(Arc<AppState>);
1070
1071impl Global for GlobalAppState {}
1072
1073pub struct WorkspaceStore {
1074 workspaces: HashSet<(gpui::AnyWindowHandle, WeakEntity<Workspace>)>,
1075 client: Arc<Client>,
1076 _subscriptions: Vec<client::Subscription>,
1077}
1078
1079#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
1080pub enum CollaboratorId {
1081 PeerId(PeerId),
1082 Agent,
1083}
1084
1085impl From<PeerId> for CollaboratorId {
1086 fn from(peer_id: PeerId) -> Self {
1087 CollaboratorId::PeerId(peer_id)
1088 }
1089}
1090
1091impl From<&PeerId> for CollaboratorId {
1092 fn from(peer_id: &PeerId) -> Self {
1093 CollaboratorId::PeerId(*peer_id)
1094 }
1095}
1096
1097#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
1098struct Follower {
1099 project_id: Option<u64>,
1100 peer_id: PeerId,
1101}
1102
1103impl AppState {
1104 #[track_caller]
1105 pub fn global(cx: &App) -> Arc<Self> {
1106 cx.global::<GlobalAppState>().0.clone()
1107 }
1108 pub fn try_global(cx: &App) -> Option<Arc<Self>> {
1109 cx.try_global::<GlobalAppState>()
1110 .map(|state| state.0.clone())
1111 }
1112 pub fn set_global(state: Arc<AppState>, cx: &mut App) {
1113 cx.set_global(GlobalAppState(state));
1114 }
1115
1116 #[cfg(any(test, feature = "test-support"))]
1117 pub fn test(cx: &mut App) -> Arc<Self> {
1118 use fs::Fs;
1119 use node_runtime::NodeRuntime;
1120 use session::Session;
1121 use settings::SettingsStore;
1122
1123 if !cx.has_global::<SettingsStore>() {
1124 let settings_store = SettingsStore::test(cx);
1125 cx.set_global(settings_store);
1126 }
1127
1128 let fs = fs::FakeFs::new(cx.background_executor().clone());
1129 <dyn Fs>::set_global(fs.clone(), cx);
1130 let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
1131 let clock = Arc::new(clock::FakeSystemClock::new());
1132 let http_client = http_client::FakeHttpClient::with_404_response();
1133 let client = Client::new(clock, http_client, cx);
1134 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
1135 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1136 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
1137
1138 theme_settings::init(theme::LoadThemes::JustBase, cx);
1139 client::init(&client, cx);
1140
1141 Arc::new(Self {
1142 client,
1143 fs,
1144 languages,
1145 user_store,
1146 workspace_store,
1147 node_runtime: NodeRuntime::unavailable(),
1148 build_window_options: |_, _| Default::default(),
1149 session,
1150 })
1151 }
1152}
1153
1154struct DelayedDebouncedEditAction {
1155 task: Option<Task<()>>,
1156 cancel_channel: Option<oneshot::Sender<()>>,
1157}
1158
1159impl DelayedDebouncedEditAction {
1160 fn new() -> DelayedDebouncedEditAction {
1161 DelayedDebouncedEditAction {
1162 task: None,
1163 cancel_channel: None,
1164 }
1165 }
1166
1167 fn fire_new<F>(
1168 &mut self,
1169 delay: Duration,
1170 window: &mut Window,
1171 cx: &mut Context<Workspace>,
1172 func: F,
1173 ) where
1174 F: 'static
1175 + Send
1176 + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> Task<Result<()>>,
1177 {
1178 if let Some(channel) = self.cancel_channel.take() {
1179 _ = channel.send(());
1180 }
1181
1182 let (sender, mut receiver) = oneshot::channel::<()>();
1183 self.cancel_channel = Some(sender);
1184
1185 let previous_task = self.task.take();
1186 self.task = Some(cx.spawn_in(window, async move |workspace, cx| {
1187 let mut timer = cx.background_executor().timer(delay).fuse();
1188 if let Some(previous_task) = previous_task {
1189 previous_task.await;
1190 }
1191
1192 futures::select_biased! {
1193 _ = receiver => return,
1194 _ = timer => {}
1195 }
1196
1197 if let Some(result) = workspace
1198 .update_in(cx, |workspace, window, cx| (func)(workspace, window, cx))
1199 .log_err()
1200 {
1201 result.await.log_err();
1202 }
1203 }));
1204 }
1205}
1206
1207pub enum Event {
1208 PaneAdded(Entity<Pane>),
1209 PaneRemoved,
1210 ItemAdded {
1211 item: Box<dyn ItemHandle>,
1212 },
1213 ActiveItemChanged,
1214 ItemRemoved {
1215 item_id: EntityId,
1216 },
1217 UserSavedItem {
1218 pane: WeakEntity<Pane>,
1219 item: Box<dyn WeakItemHandle>,
1220 save_intent: SaveIntent,
1221 },
1222 ContactRequestedJoin(u64),
1223 WorkspaceCreated(WeakEntity<Workspace>),
1224 OpenBundledFile {
1225 text: Cow<'static, str>,
1226 title: &'static str,
1227 language: &'static str,
1228 },
1229 ZoomChanged,
1230 ModalOpened,
1231 Activate,
1232 PanelAdded(AnyView),
1233}
1234
1235#[derive(Debug, Clone)]
1236pub enum OpenVisible {
1237 All,
1238 None,
1239 OnlyFiles,
1240 OnlyDirectories,
1241}
1242
1243enum WorkspaceLocation {
1244 // Valid local paths or SSH project to serialize
1245 Location(SerializedWorkspaceLocation, PathList),
1246 // No valid location found hence clear session id
1247 DetachFromSession,
1248 // No valid location found to serialize
1249 None,
1250}
1251
1252type PromptForNewPath = Box<
1253 dyn Fn(
1254 &mut Workspace,
1255 DirectoryLister,
1256 Option<String>,
1257 &mut Window,
1258 &mut Context<Workspace>,
1259 ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
1260>;
1261
1262type PromptForOpenPath = Box<
1263 dyn Fn(
1264 &mut Workspace,
1265 DirectoryLister,
1266 &mut Window,
1267 &mut Context<Workspace>,
1268 ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
1269>;
1270
1271#[derive(Default)]
1272struct DispatchingKeystrokes {
1273 dispatched: HashSet<Vec<Keystroke>>,
1274 queue: VecDeque<Keystroke>,
1275 task: Option<Shared<Task<()>>>,
1276}
1277
1278/// Collects everything project-related for a certain window opened.
1279/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
1280///
1281/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
1282/// The `Workspace` owns everybody's state and serves as a default, "global context",
1283/// that can be used to register a global action to be triggered from any place in the window.
1284pub struct Workspace {
1285 weak_self: WeakEntity<Self>,
1286 workspace_actions: Vec<Box<dyn Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div>>,
1287 zoomed: Option<AnyWeakView>,
1288 previous_dock_drag_coordinates: Option<Point<Pixels>>,
1289 zoomed_position: Option<DockPosition>,
1290 center: PaneGroup,
1291 left_dock: Entity<Dock>,
1292 bottom_dock: Entity<Dock>,
1293 right_dock: Entity<Dock>,
1294 panes: Vec<Entity<Pane>>,
1295 active_worktree_override: Option<WorktreeId>,
1296 panes_by_item: HashMap<EntityId, WeakEntity<Pane>>,
1297 active_pane: Entity<Pane>,
1298 last_active_center_pane: Option<WeakEntity<Pane>>,
1299 last_active_view_id: Option<proto::ViewId>,
1300 status_bar: Entity<StatusBar>,
1301 pub(crate) modal_layer: Entity<ModalLayer>,
1302 toast_layer: Entity<ToastLayer>,
1303 titlebar_item: Option<AnyView>,
1304 notifications: Notifications,
1305 suppressed_notifications: HashSet<NotificationId>,
1306 project: Entity<Project>,
1307 follower_states: HashMap<CollaboratorId, FollowerState>,
1308 last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
1309 window_edited: bool,
1310 last_window_title: Option<String>,
1311 dirty_items: HashMap<EntityId, Subscription>,
1312 active_call: Option<(GlobalAnyActiveCall, Vec<Subscription>)>,
1313 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
1314 database_id: Option<WorkspaceId>,
1315 app_state: Arc<AppState>,
1316 dispatching_keystrokes: Rc<RefCell<DispatchingKeystrokes>>,
1317 _subscriptions: Vec<Subscription>,
1318 _apply_leader_updates: Task<Result<()>>,
1319 _observe_current_user: Task<Result<()>>,
1320 _schedule_serialize_workspace: Option<Task<()>>,
1321 _serialize_workspace_task: Option<Task<()>>,
1322 _schedule_serialize_ssh_paths: Option<Task<()>>,
1323 pane_history_timestamp: Arc<AtomicUsize>,
1324 bounds: Bounds<Pixels>,
1325 pub centered_layout: bool,
1326 bounds_save_task_queued: Option<Task<()>>,
1327 on_prompt_for_new_path: Option<PromptForNewPath>,
1328 on_prompt_for_open_path: Option<PromptForOpenPath>,
1329 terminal_provider: Option<Box<dyn TerminalProvider>>,
1330 debugger_provider: Option<Arc<dyn DebuggerProvider>>,
1331 serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
1332 _items_serializer: Task<Result<()>>,
1333 session_id: Option<String>,
1334 scheduled_tasks: Vec<Task<()>>,
1335 last_open_dock_positions: Vec<DockPosition>,
1336 removing: bool,
1337 _panels_task: Option<Task<Result<()>>>,
1338 sidebar_focus_handle: Option<FocusHandle>,
1339 multi_workspace: Option<WeakEntity<MultiWorkspace>>,
1340}
1341
1342impl EventEmitter<Event> for Workspace {}
1343
1344#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
1345pub struct ViewId {
1346 pub creator: CollaboratorId,
1347 pub id: u64,
1348}
1349
1350pub struct FollowerState {
1351 center_pane: Entity<Pane>,
1352 dock_pane: Option<Entity<Pane>>,
1353 active_view_id: Option<ViewId>,
1354 items_by_leader_view_id: HashMap<ViewId, FollowerView>,
1355}
1356
1357struct FollowerView {
1358 view: Box<dyn FollowableItemHandle>,
1359 location: Option<proto::PanelId>,
1360}
1361
1362impl Workspace {
1363 pub fn new(
1364 workspace_id: Option<WorkspaceId>,
1365 project: Entity<Project>,
1366 app_state: Arc<AppState>,
1367 window: &mut Window,
1368 cx: &mut Context<Self>,
1369 ) -> Self {
1370 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1371 cx.subscribe(&trusted_worktrees, |_, worktrees_store, e, cx| {
1372 if let TrustedWorktreesEvent::Trusted(..) = e {
1373 // Do not persist auto trusted worktrees
1374 if !ProjectSettings::get_global(cx).session.trust_all_worktrees {
1375 worktrees_store.update(cx, |worktrees_store, cx| {
1376 worktrees_store.schedule_serialization(
1377 cx,
1378 |new_trusted_worktrees, cx| {
1379 let timeout =
1380 cx.background_executor().timer(SERIALIZATION_THROTTLE_TIME);
1381 let db = WorkspaceDb::global(cx);
1382 cx.background_spawn(async move {
1383 timeout.await;
1384 db.save_trusted_worktrees(new_trusted_worktrees)
1385 .await
1386 .log_err();
1387 })
1388 },
1389 )
1390 });
1391 }
1392 }
1393 })
1394 .detach();
1395
1396 cx.observe_global::<SettingsStore>(|_, cx| {
1397 if ProjectSettings::get_global(cx).session.trust_all_worktrees {
1398 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1399 trusted_worktrees.update(cx, |trusted_worktrees, cx| {
1400 trusted_worktrees.auto_trust_all(cx);
1401 })
1402 }
1403 }
1404 })
1405 .detach();
1406 }
1407
1408 cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
1409 match event {
1410 project::Event::RemoteIdChanged(_) => {
1411 this.update_window_title(window, cx);
1412 }
1413
1414 project::Event::CollaboratorLeft(peer_id) => {
1415 this.collaborator_left(*peer_id, window, cx);
1416 }
1417
1418 &project::Event::WorktreeRemoved(_) => {
1419 this.update_window_title(window, cx);
1420 this.serialize_workspace(window, cx);
1421 this.update_history(cx);
1422 }
1423
1424 &project::Event::WorktreeAdded(id) => {
1425 this.update_window_title(window, cx);
1426 if this
1427 .project()
1428 .read(cx)
1429 .worktree_for_id(id, cx)
1430 .is_some_and(|wt| wt.read(cx).is_visible())
1431 {
1432 this.serialize_workspace(window, cx);
1433 this.update_history(cx);
1434 }
1435 }
1436 project::Event::WorktreeUpdatedEntries(..) => {
1437 this.update_window_title(window, cx);
1438 this.serialize_workspace(window, cx);
1439 }
1440
1441 project::Event::DisconnectedFromHost => {
1442 this.update_window_edited(window, cx);
1443 let leaders_to_unfollow =
1444 this.follower_states.keys().copied().collect::<Vec<_>>();
1445 for leader_id in leaders_to_unfollow {
1446 this.unfollow(leader_id, window, cx);
1447 }
1448 }
1449
1450 project::Event::DisconnectedFromRemote {
1451 server_not_running: _,
1452 } => {
1453 this.update_window_edited(window, cx);
1454 }
1455
1456 project::Event::Closed => {
1457 window.remove_window();
1458 }
1459
1460 project::Event::DeletedEntry(_, entry_id) => {
1461 for pane in this.panes.iter() {
1462 pane.update(cx, |pane, cx| {
1463 pane.handle_deleted_project_item(*entry_id, window, cx)
1464 });
1465 }
1466 }
1467
1468 project::Event::Toast {
1469 notification_id,
1470 message,
1471 link,
1472 } => this.show_notification(
1473 NotificationId::named(notification_id.clone()),
1474 cx,
1475 |cx| {
1476 let mut notification = MessageNotification::new(message.clone(), cx);
1477 if let Some(link) = link {
1478 notification = notification
1479 .more_info_message(link.label)
1480 .more_info_url(link.url);
1481 }
1482
1483 cx.new(|_| notification)
1484 },
1485 ),
1486
1487 project::Event::HideToast { notification_id } => {
1488 this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
1489 }
1490
1491 project::Event::LanguageServerPrompt(request) => {
1492 struct LanguageServerPrompt;
1493
1494 this.show_notification(
1495 NotificationId::composite::<LanguageServerPrompt>(request.id),
1496 cx,
1497 |cx| {
1498 cx.new(|cx| {
1499 notifications::LanguageServerPrompt::new(request.clone(), cx)
1500 })
1501 },
1502 );
1503 }
1504
1505 project::Event::AgentLocationChanged => {
1506 this.handle_agent_location_changed(window, cx)
1507 }
1508
1509 _ => {}
1510 }
1511 cx.notify()
1512 })
1513 .detach();
1514
1515 cx.subscribe_in(
1516 &project.read(cx).breakpoint_store(),
1517 window,
1518 |workspace, _, event, window, cx| match event {
1519 BreakpointStoreEvent::BreakpointsUpdated(_, _)
1520 | BreakpointStoreEvent::BreakpointsCleared(_) => {
1521 workspace.serialize_workspace(window, cx);
1522 }
1523 BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {}
1524 },
1525 )
1526 .detach();
1527 if let Some(toolchain_store) = project.read(cx).toolchain_store() {
1528 cx.subscribe_in(
1529 &toolchain_store,
1530 window,
1531 |workspace, _, event, window, cx| match event {
1532 ToolchainStoreEvent::CustomToolchainsModified => {
1533 workspace.serialize_workspace(window, cx);
1534 }
1535 _ => {}
1536 },
1537 )
1538 .detach();
1539 }
1540
1541 cx.on_focus_lost(window, |this, window, cx| {
1542 let focus_handle = this.focus_handle(cx);
1543 window.focus(&focus_handle, cx);
1544 })
1545 .detach();
1546
1547 let weak_handle = cx.entity().downgrade();
1548 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
1549
1550 let center_pane = cx.new(|cx| {
1551 let mut center_pane = Pane::new(
1552 weak_handle.clone(),
1553 project.clone(),
1554 pane_history_timestamp.clone(),
1555 None,
1556 NewFile.boxed_clone(),
1557 true,
1558 window,
1559 cx,
1560 );
1561 center_pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
1562 center_pane.set_should_display_welcome_page(true);
1563 center_pane
1564 });
1565 cx.subscribe_in(¢er_pane, window, Self::handle_pane_event)
1566 .detach();
1567
1568 window.focus(¢er_pane.focus_handle(cx), cx);
1569
1570 cx.emit(Event::PaneAdded(center_pane.clone()));
1571
1572 let any_window_handle = window.window_handle();
1573 app_state.workspace_store.update(cx, |store, _| {
1574 store
1575 .workspaces
1576 .insert((any_window_handle, weak_handle.clone()));
1577 });
1578
1579 let mut current_user = app_state.user_store.read(cx).watch_current_user();
1580 let mut connection_status = app_state.client.status();
1581 let _observe_current_user = cx.spawn_in(window, async move |this, cx| {
1582 current_user.next().await;
1583 connection_status.next().await;
1584 let mut stream =
1585 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
1586
1587 while stream.recv().await.is_some() {
1588 this.update(cx, |_, cx| cx.notify())?;
1589 }
1590 anyhow::Ok(())
1591 });
1592
1593 // All leader updates are enqueued and then processed in a single task, so
1594 // that each asynchronous operation can be run in order.
1595 let (leader_updates_tx, mut leader_updates_rx) =
1596 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
1597 let _apply_leader_updates = cx.spawn_in(window, async move |this, cx| {
1598 while let Some((leader_id, update)) = leader_updates_rx.next().await {
1599 Self::process_leader_update(&this, leader_id, update, cx)
1600 .await
1601 .log_err();
1602 }
1603
1604 Ok(())
1605 });
1606
1607 cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
1608 let modal_layer = cx.new(|_| ModalLayer::new());
1609 let toast_layer = cx.new(|_| ToastLayer::new());
1610 cx.subscribe(
1611 &modal_layer,
1612 |_, _, _: &modal_layer::ModalOpenedEvent, cx| {
1613 cx.emit(Event::ModalOpened);
1614 },
1615 )
1616 .detach();
1617
1618 let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx);
1619 let bottom_dock = Dock::new(DockPosition::Bottom, modal_layer.clone(), window, cx);
1620 let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), window, cx);
1621 let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx));
1622 let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx));
1623 let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx));
1624 let multi_workspace = window
1625 .root::<MultiWorkspace>()
1626 .flatten()
1627 .map(|mw| mw.downgrade());
1628 let status_bar = cx.new(|cx| {
1629 let mut status_bar =
1630 StatusBar::new(¢er_pane.clone(), multi_workspace.clone(), window, cx);
1631 status_bar.add_left_item(left_dock_buttons, window, cx);
1632 status_bar.add_right_item(right_dock_buttons, window, cx);
1633 status_bar.add_right_item(bottom_dock_buttons, window, cx);
1634 status_bar
1635 });
1636
1637 let session_id = app_state.session.read(cx).id().to_owned();
1638
1639 let mut active_call = None;
1640 if let Some(call) = GlobalAnyActiveCall::try_global(cx).cloned() {
1641 let subscriptions =
1642 vec![
1643 call.0
1644 .subscribe(window, cx, Box::new(Self::on_active_call_event)),
1645 ];
1646 active_call = Some((call, subscriptions));
1647 }
1648
1649 let (serializable_items_tx, serializable_items_rx) =
1650 mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
1651 let _items_serializer = cx.spawn_in(window, async move |this, cx| {
1652 Self::serialize_items(&this, serializable_items_rx, cx).await
1653 });
1654
1655 let subscriptions = vec![
1656 cx.observe_window_activation(window, Self::on_window_activation_changed),
1657 cx.observe_window_bounds(window, move |this, window, cx| {
1658 if this.bounds_save_task_queued.is_some() {
1659 return;
1660 }
1661 this.bounds_save_task_queued = Some(cx.spawn_in(window, async move |this, cx| {
1662 cx.background_executor()
1663 .timer(Duration::from_millis(100))
1664 .await;
1665 this.update_in(cx, |this, window, cx| {
1666 this.save_window_bounds(window, cx).detach();
1667 this.bounds_save_task_queued.take();
1668 })
1669 .ok();
1670 }));
1671 cx.notify();
1672 }),
1673 cx.observe_window_appearance(window, |_, window, cx| {
1674 let window_appearance = window.appearance();
1675
1676 *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
1677
1678 theme_settings::reload_theme(cx);
1679 theme_settings::reload_icon_theme(cx);
1680 }),
1681 cx.on_release({
1682 let weak_handle = weak_handle.clone();
1683 move |this, cx| {
1684 this.app_state.workspace_store.update(cx, move |store, _| {
1685 store.workspaces.retain(|(_, weak)| weak != &weak_handle);
1686 })
1687 }
1688 }),
1689 ];
1690
1691 cx.defer_in(window, move |this, window, cx| {
1692 this.update_window_title(window, cx);
1693 this.show_initial_notifications(cx);
1694 });
1695
1696 let mut center = PaneGroup::new(center_pane.clone());
1697 center.set_is_center(true);
1698 center.mark_positions(cx);
1699
1700 Workspace {
1701 weak_self: weak_handle.clone(),
1702 zoomed: None,
1703 zoomed_position: None,
1704 previous_dock_drag_coordinates: None,
1705 center,
1706 panes: vec![center_pane.clone()],
1707 panes_by_item: Default::default(),
1708 active_pane: center_pane.clone(),
1709 last_active_center_pane: Some(center_pane.downgrade()),
1710 last_active_view_id: None,
1711 status_bar,
1712 modal_layer,
1713 toast_layer,
1714 titlebar_item: None,
1715 active_worktree_override: None,
1716 notifications: Notifications::default(),
1717 suppressed_notifications: HashSet::default(),
1718 left_dock,
1719 bottom_dock,
1720 right_dock,
1721 _panels_task: None,
1722 project: project.clone(),
1723 follower_states: Default::default(),
1724 last_leaders_by_pane: Default::default(),
1725 dispatching_keystrokes: Default::default(),
1726 window_edited: false,
1727 last_window_title: None,
1728 dirty_items: Default::default(),
1729 active_call,
1730 database_id: workspace_id,
1731 app_state,
1732 _observe_current_user,
1733 _apply_leader_updates,
1734 _schedule_serialize_workspace: None,
1735 _serialize_workspace_task: None,
1736 _schedule_serialize_ssh_paths: None,
1737 leader_updates_tx,
1738 _subscriptions: subscriptions,
1739 pane_history_timestamp,
1740 workspace_actions: Default::default(),
1741 // This data will be incorrect, but it will be overwritten by the time it needs to be used.
1742 bounds: Default::default(),
1743 centered_layout: false,
1744 bounds_save_task_queued: None,
1745 on_prompt_for_new_path: None,
1746 on_prompt_for_open_path: None,
1747 terminal_provider: None,
1748 debugger_provider: None,
1749 serializable_items_tx,
1750 _items_serializer,
1751 session_id: Some(session_id),
1752
1753 scheduled_tasks: Vec::new(),
1754 last_open_dock_positions: Vec::new(),
1755 removing: false,
1756 sidebar_focus_handle: None,
1757 multi_workspace,
1758 }
1759 }
1760
1761 pub fn new_local(
1762 abs_paths: Vec<PathBuf>,
1763 app_state: Arc<AppState>,
1764 requesting_window: Option<WindowHandle<MultiWorkspace>>,
1765 env: Option<HashMap<String, String>>,
1766 init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
1767 activate: bool,
1768 cx: &mut App,
1769 ) -> Task<anyhow::Result<OpenResult>> {
1770 let project_handle = Project::local(
1771 app_state.client.clone(),
1772 app_state.node_runtime.clone(),
1773 app_state.user_store.clone(),
1774 app_state.languages.clone(),
1775 app_state.fs.clone(),
1776 env,
1777 Default::default(),
1778 cx,
1779 );
1780
1781 let db = WorkspaceDb::global(cx);
1782 let kvp = db::kvp::KeyValueStore::global(cx);
1783 cx.spawn(async move |cx| {
1784 let mut paths_to_open = Vec::with_capacity(abs_paths.len());
1785 for path in abs_paths.into_iter() {
1786 if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() {
1787 paths_to_open.push(canonical)
1788 } else {
1789 paths_to_open.push(path)
1790 }
1791 }
1792
1793 let serialized_workspace = db.workspace_for_roots(paths_to_open.as_slice());
1794
1795 if let Some(paths) = serialized_workspace.as_ref().map(|ws| &ws.paths) {
1796 paths_to_open = paths.ordered_paths().cloned().collect();
1797 if !paths.is_lexicographically_ordered() {
1798 project_handle.update(cx, |project, cx| {
1799 project.set_worktrees_reordered(true, cx);
1800 });
1801 }
1802 }
1803
1804 // Get project paths for all of the abs_paths
1805 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
1806 Vec::with_capacity(paths_to_open.len());
1807
1808 for path in paths_to_open.into_iter() {
1809 if let Some((_, project_entry)) = cx
1810 .update(|cx| {
1811 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
1812 })
1813 .await
1814 .log_err()
1815 {
1816 project_paths.push((path, Some(project_entry)));
1817 } else {
1818 project_paths.push((path, None));
1819 }
1820 }
1821
1822 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
1823 serialized_workspace.id
1824 } else {
1825 db.next_id().await.unwrap_or_else(|_| Default::default())
1826 };
1827
1828 let toolchains = db.toolchains(workspace_id).await?;
1829
1830 for (toolchain, worktree_path, path) in toolchains {
1831 let toolchain_path = PathBuf::from(toolchain.path.clone().to_string());
1832 let Some(worktree_id) = project_handle.read_with(cx, |this, cx| {
1833 this.find_worktree(&worktree_path, cx)
1834 .and_then(|(worktree, rel_path)| {
1835 if rel_path.is_empty() {
1836 Some(worktree.read(cx).id())
1837 } else {
1838 None
1839 }
1840 })
1841 }) else {
1842 // We did not find a worktree with a given path, but that's whatever.
1843 continue;
1844 };
1845 if !app_state.fs.is_file(toolchain_path.as_path()).await {
1846 continue;
1847 }
1848
1849 project_handle
1850 .update(cx, |this, cx| {
1851 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
1852 })
1853 .await;
1854 }
1855 if let Some(workspace) = serialized_workspace.as_ref() {
1856 project_handle.update(cx, |this, cx| {
1857 for (scope, toolchains) in &workspace.user_toolchains {
1858 for toolchain in toolchains {
1859 this.add_toolchain(toolchain.clone(), scope.clone(), cx);
1860 }
1861 }
1862 });
1863 }
1864
1865 let (window, workspace): (WindowHandle<MultiWorkspace>, Entity<Workspace>) =
1866 if let Some(window) = requesting_window {
1867 let centered_layout = serialized_workspace
1868 .as_ref()
1869 .map(|w| w.centered_layout)
1870 .unwrap_or(false);
1871
1872 let workspace = window.update(cx, |multi_workspace, window, cx| {
1873 let workspace = cx.new(|cx| {
1874 let mut workspace = Workspace::new(
1875 Some(workspace_id),
1876 project_handle.clone(),
1877 app_state.clone(),
1878 window,
1879 cx,
1880 );
1881
1882 workspace.centered_layout = centered_layout;
1883
1884 // Call init callback to add items before window renders
1885 if let Some(init) = init {
1886 init(&mut workspace, window, cx);
1887 }
1888
1889 workspace
1890 });
1891 if activate {
1892 multi_workspace.activate(workspace.clone(), cx);
1893 } else {
1894 multi_workspace.add_workspace(workspace.clone(), cx);
1895 }
1896 workspace
1897 })?;
1898 (window, workspace)
1899 } else {
1900 let window_bounds_override = window_bounds_env_override();
1901
1902 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
1903 (Some(WindowBounds::Windowed(bounds)), None)
1904 } else if let Some(workspace) = serialized_workspace.as_ref()
1905 && let Some(display) = workspace.display
1906 && let Some(bounds) = workspace.window_bounds.as_ref()
1907 {
1908 // Reopening an existing workspace - restore its saved bounds
1909 (Some(bounds.0), Some(display))
1910 } else if let Some((display, bounds)) =
1911 persistence::read_default_window_bounds(&kvp)
1912 {
1913 // New or empty workspace - use the last known window bounds
1914 (Some(bounds), Some(display))
1915 } else {
1916 // New window - let GPUI's default_bounds() handle cascading
1917 (None, None)
1918 };
1919
1920 // Use the serialized workspace to construct the new window
1921 let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx));
1922 options.window_bounds = window_bounds;
1923 let centered_layout = serialized_workspace
1924 .as_ref()
1925 .map(|w| w.centered_layout)
1926 .unwrap_or(false);
1927 let window = cx.open_window(options, {
1928 let app_state = app_state.clone();
1929 let project_handle = project_handle.clone();
1930 move |window, cx| {
1931 let workspace = cx.new(|cx| {
1932 let mut workspace = Workspace::new(
1933 Some(workspace_id),
1934 project_handle,
1935 app_state,
1936 window,
1937 cx,
1938 );
1939 workspace.centered_layout = centered_layout;
1940
1941 // Call init callback to add items before window renders
1942 if let Some(init) = init {
1943 init(&mut workspace, window, cx);
1944 }
1945
1946 workspace
1947 });
1948 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
1949 }
1950 })?;
1951 let workspace =
1952 window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
1953 multi_workspace.workspace().clone()
1954 })?;
1955 (window, workspace)
1956 };
1957
1958 notify_if_database_failed(window, cx);
1959 // Check if this is an empty workspace (no paths to open)
1960 // An empty workspace is one where project_paths is empty
1961 let is_empty_workspace = project_paths.is_empty();
1962 // Check if serialized workspace has paths before it's moved
1963 let serialized_workspace_has_paths = serialized_workspace
1964 .as_ref()
1965 .map(|ws| !ws.paths.is_empty())
1966 .unwrap_or(false);
1967
1968 let opened_items = window
1969 .update(cx, |_, window, cx| {
1970 workspace.update(cx, |_workspace: &mut Workspace, cx| {
1971 open_items(serialized_workspace, project_paths, window, cx)
1972 })
1973 })?
1974 .await
1975 .unwrap_or_default();
1976
1977 // Restore default dock state for empty workspaces
1978 // Only restore if:
1979 // 1. This is an empty workspace (no paths), AND
1980 // 2. The serialized workspace either doesn't exist or has no paths
1981 if is_empty_workspace && !serialized_workspace_has_paths {
1982 if let Some(default_docks) = persistence::read_default_dock_state(&kvp) {
1983 window
1984 .update(cx, |_, window, cx| {
1985 workspace.update(cx, |workspace, cx| {
1986 for (dock, serialized_dock) in [
1987 (&workspace.right_dock, &default_docks.right),
1988 (&workspace.left_dock, &default_docks.left),
1989 (&workspace.bottom_dock, &default_docks.bottom),
1990 ] {
1991 dock.update(cx, |dock, cx| {
1992 dock.serialized_dock = Some(serialized_dock.clone());
1993 dock.restore_state(window, cx);
1994 });
1995 }
1996 cx.notify();
1997 });
1998 })
1999 .log_err();
2000 }
2001 }
2002
2003 window
2004 .update(cx, |_, _window, cx| {
2005 workspace.update(cx, |this: &mut Workspace, cx| {
2006 this.update_history(cx);
2007 });
2008 })
2009 .log_err();
2010 Ok(OpenResult {
2011 window,
2012 workspace,
2013 opened_items,
2014 })
2015 })
2016 }
2017
2018 pub fn weak_handle(&self) -> WeakEntity<Self> {
2019 self.weak_self.clone()
2020 }
2021
2022 pub fn left_dock(&self) -> &Entity<Dock> {
2023 &self.left_dock
2024 }
2025
2026 pub fn bottom_dock(&self) -> &Entity<Dock> {
2027 &self.bottom_dock
2028 }
2029
2030 pub fn set_bottom_dock_layout(
2031 &mut self,
2032 layout: BottomDockLayout,
2033 window: &mut Window,
2034 cx: &mut Context<Self>,
2035 ) {
2036 let fs = self.project().read(cx).fs();
2037 settings::update_settings_file(fs.clone(), cx, move |content, _cx| {
2038 content.workspace.bottom_dock_layout = Some(layout);
2039 });
2040
2041 cx.notify();
2042 self.serialize_workspace(window, cx);
2043 }
2044
2045 pub fn right_dock(&self) -> &Entity<Dock> {
2046 &self.right_dock
2047 }
2048
2049 pub fn all_docks(&self) -> [&Entity<Dock>; 3] {
2050 [&self.left_dock, &self.bottom_dock, &self.right_dock]
2051 }
2052
2053 pub fn capture_dock_state(&self, _window: &Window, cx: &App) -> DockStructure {
2054 let left_dock = self.left_dock.read(cx);
2055 let left_visible = left_dock.is_open();
2056 let left_active_panel = left_dock
2057 .active_panel()
2058 .map(|panel| panel.persistent_name().to_string());
2059 // `zoomed_position` is kept in sync with individual panel zoom state
2060 // by the dock code in `Dock::new` and `Dock::add_panel`.
2061 let left_dock_zoom = self.zoomed_position == Some(DockPosition::Left);
2062
2063 let right_dock = self.right_dock.read(cx);
2064 let right_visible = right_dock.is_open();
2065 let right_active_panel = right_dock
2066 .active_panel()
2067 .map(|panel| panel.persistent_name().to_string());
2068 let right_dock_zoom = self.zoomed_position == Some(DockPosition::Right);
2069
2070 let bottom_dock = self.bottom_dock.read(cx);
2071 let bottom_visible = bottom_dock.is_open();
2072 let bottom_active_panel = bottom_dock
2073 .active_panel()
2074 .map(|panel| panel.persistent_name().to_string());
2075 let bottom_dock_zoom = self.zoomed_position == Some(DockPosition::Bottom);
2076
2077 DockStructure {
2078 left: DockData {
2079 visible: left_visible,
2080 active_panel: left_active_panel,
2081 zoom: left_dock_zoom,
2082 },
2083 right: DockData {
2084 visible: right_visible,
2085 active_panel: right_active_panel,
2086 zoom: right_dock_zoom,
2087 },
2088 bottom: DockData {
2089 visible: bottom_visible,
2090 active_panel: bottom_active_panel,
2091 zoom: bottom_dock_zoom,
2092 },
2093 }
2094 }
2095
2096 pub fn set_dock_structure(
2097 &self,
2098 docks: DockStructure,
2099 window: &mut Window,
2100 cx: &mut Context<Self>,
2101 ) {
2102 for (dock, data) in [
2103 (&self.left_dock, docks.left),
2104 (&self.bottom_dock, docks.bottom),
2105 (&self.right_dock, docks.right),
2106 ] {
2107 dock.update(cx, |dock, cx| {
2108 dock.serialized_dock = Some(data);
2109 dock.restore_state(window, cx);
2110 });
2111 }
2112 }
2113
2114 pub fn open_item_abs_paths(&self, cx: &App) -> Vec<PathBuf> {
2115 self.items(cx)
2116 .filter_map(|item| {
2117 let project_path = item.project_path(cx)?;
2118 self.project.read(cx).absolute_path(&project_path, cx)
2119 })
2120 .collect()
2121 }
2122
2123 pub fn dock_at_position(&self, position: DockPosition) -> &Entity<Dock> {
2124 match position {
2125 DockPosition::Left => &self.left_dock,
2126 DockPosition::Bottom => &self.bottom_dock,
2127 DockPosition::Right => &self.right_dock,
2128 }
2129 }
2130
2131 pub fn agent_panel_position(&self, cx: &App) -> Option<DockPosition> {
2132 self.all_docks().into_iter().find_map(|dock| {
2133 let dock = dock.read(cx);
2134 dock.has_agent_panel(cx).then_some(dock.position())
2135 })
2136 }
2137
2138 pub fn panel_size_state<T: Panel>(&self, cx: &App) -> Option<dock::PanelSizeState> {
2139 self.all_docks().into_iter().find_map(|dock| {
2140 let dock = dock.read(cx);
2141 let panel = dock.panel::<T>()?;
2142 dock.stored_panel_size_state(&panel)
2143 })
2144 }
2145
2146 pub fn persisted_panel_size_state(
2147 &self,
2148 panel_key: &'static str,
2149 cx: &App,
2150 ) -> Option<dock::PanelSizeState> {
2151 dock::Dock::load_persisted_size_state(self, panel_key, cx)
2152 }
2153
2154 pub fn persist_panel_size_state(
2155 &self,
2156 panel_key: &str,
2157 size_state: dock::PanelSizeState,
2158 cx: &mut App,
2159 ) {
2160 let Some(workspace_id) = self
2161 .database_id()
2162 .map(|id| i64::from(id).to_string())
2163 .or(self.session_id())
2164 else {
2165 return;
2166 };
2167
2168 let kvp = db::kvp::KeyValueStore::global(cx);
2169 let panel_key = panel_key.to_string();
2170 cx.background_spawn(async move {
2171 let scope = kvp.scoped(dock::PANEL_SIZE_STATE_KEY);
2172 scope
2173 .write(
2174 format!("{workspace_id}:{panel_key}"),
2175 serde_json::to_string(&size_state)?,
2176 )
2177 .await
2178 })
2179 .detach_and_log_err(cx);
2180 }
2181
2182 pub fn set_panel_size_state<T: Panel>(
2183 &mut self,
2184 size_state: dock::PanelSizeState,
2185 window: &mut Window,
2186 cx: &mut Context<Self>,
2187 ) -> bool {
2188 let Some(panel) = self.panel::<T>(cx) else {
2189 return false;
2190 };
2191
2192 let dock = self.dock_at_position(panel.position(window, cx));
2193 let did_set = dock.update(cx, |dock, cx| {
2194 dock.set_panel_size_state(&panel, size_state, cx)
2195 });
2196
2197 if did_set {
2198 self.persist_panel_size_state(T::panel_key(), size_state, cx);
2199 }
2200
2201 did_set
2202 }
2203
2204 fn dock_size(&self, dock: &Dock, window: &Window, cx: &App) -> Option<Pixels> {
2205 let panel = dock.active_panel()?;
2206 let size_state = dock
2207 .stored_panel_size_state(panel.as_ref())
2208 .unwrap_or_default();
2209 let position = dock.position();
2210
2211 if position.axis() == Axis::Horizontal
2212 && panel.supports_flexible_size(window, cx)
2213 && let Some(ratio) = size_state
2214 .flexible_size_ratio
2215 .or_else(|| self.default_flexible_dock_ratio(position))
2216 && let Some(available_width) =
2217 self.available_width_for_horizontal_dock(position, window, cx)
2218 {
2219 return Some((available_width * ratio.clamp(0.0, 1.0)).max(RESIZE_HANDLE_SIZE));
2220 }
2221
2222 Some(
2223 size_state
2224 .size
2225 .unwrap_or_else(|| panel.default_size(window, cx)),
2226 )
2227 }
2228
2229 pub fn flexible_dock_ratio_for_size(
2230 &self,
2231 position: DockPosition,
2232 size: Pixels,
2233 window: &Window,
2234 cx: &App,
2235 ) -> Option<f32> {
2236 if position.axis() != Axis::Horizontal {
2237 return None;
2238 }
2239
2240 let available_width = self.available_width_for_horizontal_dock(position, window, cx)?;
2241 let available_width = available_width.max(RESIZE_HANDLE_SIZE);
2242 Some((size / available_width).clamp(0.0, 1.0))
2243 }
2244
2245 fn available_width_for_horizontal_dock(
2246 &self,
2247 position: DockPosition,
2248 window: &Window,
2249 cx: &App,
2250 ) -> Option<Pixels> {
2251 let workspace_width = self.bounds.size.width;
2252 if workspace_width <= Pixels::ZERO {
2253 return None;
2254 }
2255
2256 let opposite_position = match position {
2257 DockPosition::Left => DockPosition::Right,
2258 DockPosition::Right => DockPosition::Left,
2259 DockPosition::Bottom => return None,
2260 };
2261
2262 let opposite_width = self
2263 .dock_at_position(opposite_position)
2264 .read(cx)
2265 .stored_active_panel_size(window, cx)
2266 .unwrap_or(Pixels::ZERO);
2267
2268 Some((workspace_width - opposite_width).max(RESIZE_HANDLE_SIZE))
2269 }
2270
2271 pub fn default_flexible_dock_ratio(&self, position: DockPosition) -> Option<f32> {
2272 if position.axis() != Axis::Horizontal {
2273 return None;
2274 }
2275
2276 let pane = self.last_active_center_pane.clone()?.upgrade()?;
2277 let pane_fraction = self.center.width_fraction_for_pane(&pane).unwrap_or(1.0);
2278 Some((pane_fraction / (1.0 + pane_fraction)).clamp(0.0, 1.0))
2279 }
2280
2281 pub fn is_edited(&self) -> bool {
2282 self.window_edited
2283 }
2284
2285 pub fn add_panel<T: Panel>(
2286 &mut self,
2287 panel: Entity<T>,
2288 window: &mut Window,
2289 cx: &mut Context<Self>,
2290 ) {
2291 let focus_handle = panel.panel_focus_handle(cx);
2292 cx.on_focus_in(&focus_handle, window, Self::handle_panel_focused)
2293 .detach();
2294
2295 let dock_position = panel.position(window, cx);
2296 let dock = self.dock_at_position(dock_position);
2297 let any_panel = panel.to_any();
2298 let persisted_size_state =
2299 self.persisted_panel_size_state(T::panel_key(), cx)
2300 .or_else(|| {
2301 load_legacy_panel_size(T::panel_key(), dock_position, self, cx).map(|size| {
2302 let state = dock::PanelSizeState {
2303 size: Some(size),
2304 flexible_size_ratio: None,
2305 };
2306 self.persist_panel_size_state(T::panel_key(), state, cx);
2307 state
2308 })
2309 });
2310
2311 dock.update(cx, |dock, cx| {
2312 let index = dock.add_panel(panel.clone(), self.weak_self.clone(), window, cx);
2313 if let Some(size_state) = persisted_size_state {
2314 dock.set_panel_size_state(&panel, size_state, cx);
2315 }
2316 index
2317 });
2318
2319 cx.emit(Event::PanelAdded(any_panel));
2320 }
2321
2322 pub fn remove_panel<T: Panel>(
2323 &mut self,
2324 panel: &Entity<T>,
2325 window: &mut Window,
2326 cx: &mut Context<Self>,
2327 ) {
2328 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2329 dock.update(cx, |dock, cx| dock.remove_panel(panel, window, cx));
2330 }
2331 }
2332
2333 pub fn status_bar(&self) -> &Entity<StatusBar> {
2334 &self.status_bar
2335 }
2336
2337 pub fn set_sidebar_focus_handle(&mut self, handle: Option<FocusHandle>) {
2338 self.sidebar_focus_handle = handle;
2339 }
2340
2341 pub fn status_bar_visible(&self, cx: &App) -> bool {
2342 StatusBarSettings::get_global(cx).show
2343 }
2344
2345 pub fn multi_workspace(&self) -> Option<&WeakEntity<MultiWorkspace>> {
2346 self.multi_workspace.as_ref()
2347 }
2348
2349 pub fn set_multi_workspace(
2350 &mut self,
2351 multi_workspace: WeakEntity<MultiWorkspace>,
2352 cx: &mut App,
2353 ) {
2354 self.status_bar.update(cx, |status_bar, cx| {
2355 status_bar.set_multi_workspace(multi_workspace.clone(), cx);
2356 });
2357 self.multi_workspace = Some(multi_workspace);
2358 }
2359
2360 pub fn app_state(&self) -> &Arc<AppState> {
2361 &self.app_state
2362 }
2363
2364 pub fn set_panels_task(&mut self, task: Task<Result<()>>) {
2365 self._panels_task = Some(task);
2366 }
2367
2368 pub fn take_panels_task(&mut self) -> Option<Task<Result<()>>> {
2369 self._panels_task.take()
2370 }
2371
2372 pub fn user_store(&self) -> &Entity<UserStore> {
2373 &self.app_state.user_store
2374 }
2375
2376 pub fn project(&self) -> &Entity<Project> {
2377 &self.project
2378 }
2379
2380 pub fn path_style(&self, cx: &App) -> PathStyle {
2381 self.project.read(cx).path_style(cx)
2382 }
2383
2384 pub fn recently_activated_items(&self, cx: &App) -> HashMap<EntityId, usize> {
2385 let mut history: HashMap<EntityId, usize> = HashMap::default();
2386
2387 for pane_handle in &self.panes {
2388 let pane = pane_handle.read(cx);
2389
2390 for entry in pane.activation_history() {
2391 history.insert(
2392 entry.entity_id,
2393 history
2394 .get(&entry.entity_id)
2395 .cloned()
2396 .unwrap_or(0)
2397 .max(entry.timestamp),
2398 );
2399 }
2400 }
2401
2402 history
2403 }
2404
2405 pub fn recent_active_item_by_type<T: 'static>(&self, cx: &App) -> Option<Entity<T>> {
2406 let mut recent_item: Option<Entity<T>> = None;
2407 let mut recent_timestamp = 0;
2408 for pane_handle in &self.panes {
2409 let pane = pane_handle.read(cx);
2410 let item_map: HashMap<EntityId, &Box<dyn ItemHandle>> =
2411 pane.items().map(|item| (item.item_id(), item)).collect();
2412 for entry in pane.activation_history() {
2413 if entry.timestamp > recent_timestamp
2414 && let Some(&item) = item_map.get(&entry.entity_id)
2415 && let Some(typed_item) = item.act_as::<T>(cx)
2416 {
2417 recent_timestamp = entry.timestamp;
2418 recent_item = Some(typed_item);
2419 }
2420 }
2421 }
2422 recent_item
2423 }
2424
2425 pub fn recent_navigation_history_iter(
2426 &self,
2427 cx: &App,
2428 ) -> impl Iterator<Item = (ProjectPath, Option<PathBuf>)> + use<> {
2429 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
2430 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
2431
2432 for pane in &self.panes {
2433 let pane = pane.read(cx);
2434
2435 pane.nav_history()
2436 .for_each_entry(cx, &mut |entry, (project_path, fs_path)| {
2437 if let Some(fs_path) = &fs_path {
2438 abs_paths_opened
2439 .entry(fs_path.clone())
2440 .or_default()
2441 .insert(project_path.clone());
2442 }
2443 let timestamp = entry.timestamp;
2444 match history.entry(project_path) {
2445 hash_map::Entry::Occupied(mut entry) => {
2446 let (_, old_timestamp) = entry.get();
2447 if ×tamp > old_timestamp {
2448 entry.insert((fs_path, timestamp));
2449 }
2450 }
2451 hash_map::Entry::Vacant(entry) => {
2452 entry.insert((fs_path, timestamp));
2453 }
2454 }
2455 });
2456
2457 if let Some(item) = pane.active_item()
2458 && let Some(project_path) = item.project_path(cx)
2459 {
2460 let fs_path = self.project.read(cx).absolute_path(&project_path, cx);
2461
2462 if let Some(fs_path) = &fs_path {
2463 abs_paths_opened
2464 .entry(fs_path.clone())
2465 .or_default()
2466 .insert(project_path.clone());
2467 }
2468
2469 history.insert(project_path, (fs_path, std::usize::MAX));
2470 }
2471 }
2472
2473 history
2474 .into_iter()
2475 .sorted_by_key(|(_, (_, order))| *order)
2476 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
2477 .rev()
2478 .filter(move |(history_path, abs_path)| {
2479 let latest_project_path_opened = abs_path
2480 .as_ref()
2481 .and_then(|abs_path| abs_paths_opened.get(abs_path))
2482 .and_then(|project_paths| {
2483 project_paths
2484 .iter()
2485 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
2486 });
2487
2488 latest_project_path_opened.is_none_or(|path| path == history_path)
2489 })
2490 }
2491
2492 pub fn recent_navigation_history(
2493 &self,
2494 limit: Option<usize>,
2495 cx: &App,
2496 ) -> Vec<(ProjectPath, Option<PathBuf>)> {
2497 self.recent_navigation_history_iter(cx)
2498 .take(limit.unwrap_or(usize::MAX))
2499 .collect()
2500 }
2501
2502 pub fn clear_navigation_history(&mut self, _window: &mut Window, cx: &mut Context<Workspace>) {
2503 for pane in &self.panes {
2504 pane.update(cx, |pane, cx| pane.nav_history_mut().clear(cx));
2505 }
2506 }
2507
2508 fn navigate_history(
2509 &mut self,
2510 pane: WeakEntity<Pane>,
2511 mode: NavigationMode,
2512 window: &mut Window,
2513 cx: &mut Context<Workspace>,
2514 ) -> Task<Result<()>> {
2515 self.navigate_history_impl(
2516 pane,
2517 mode,
2518 window,
2519 &mut |history, cx| history.pop(mode, cx),
2520 cx,
2521 )
2522 }
2523
2524 fn navigate_tag_history(
2525 &mut self,
2526 pane: WeakEntity<Pane>,
2527 mode: TagNavigationMode,
2528 window: &mut Window,
2529 cx: &mut Context<Workspace>,
2530 ) -> Task<Result<()>> {
2531 self.navigate_history_impl(
2532 pane,
2533 NavigationMode::Normal,
2534 window,
2535 &mut |history, _cx| history.pop_tag(mode),
2536 cx,
2537 )
2538 }
2539
2540 fn navigate_history_impl(
2541 &mut self,
2542 pane: WeakEntity<Pane>,
2543 mode: NavigationMode,
2544 window: &mut Window,
2545 cb: &mut dyn FnMut(&mut NavHistory, &mut App) -> Option<NavigationEntry>,
2546 cx: &mut Context<Workspace>,
2547 ) -> Task<Result<()>> {
2548 let to_load = if let Some(pane) = pane.upgrade() {
2549 pane.update(cx, |pane, cx| {
2550 window.focus(&pane.focus_handle(cx), cx);
2551 loop {
2552 // Retrieve the weak item handle from the history.
2553 let entry = cb(pane.nav_history_mut(), cx)?;
2554
2555 // If the item is still present in this pane, then activate it.
2556 if let Some(index) = entry
2557 .item
2558 .upgrade()
2559 .and_then(|v| pane.index_for_item(v.as_ref()))
2560 {
2561 let prev_active_item_index = pane.active_item_index();
2562 pane.nav_history_mut().set_mode(mode);
2563 pane.activate_item(index, true, true, window, cx);
2564 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2565
2566 let mut navigated = prev_active_item_index != pane.active_item_index();
2567 if let Some(data) = entry.data {
2568 navigated |= pane.active_item()?.navigate(data, window, cx);
2569 }
2570
2571 if navigated {
2572 break None;
2573 }
2574 } else {
2575 // If the item is no longer present in this pane, then retrieve its
2576 // path info in order to reopen it.
2577 break pane
2578 .nav_history()
2579 .path_for_item(entry.item.id())
2580 .map(|(project_path, abs_path)| (project_path, abs_path, entry));
2581 }
2582 }
2583 })
2584 } else {
2585 None
2586 };
2587
2588 if let Some((project_path, abs_path, entry)) = to_load {
2589 // If the item was no longer present, then load it again from its previous path, first try the local path
2590 let open_by_project_path = self.load_path(project_path.clone(), window, cx);
2591
2592 cx.spawn_in(window, async move |workspace, cx| {
2593 let open_by_project_path = open_by_project_path.await;
2594 let mut navigated = false;
2595 match open_by_project_path
2596 .with_context(|| format!("Navigating to {project_path:?}"))
2597 {
2598 Ok((project_entry_id, build_item)) => {
2599 let prev_active_item_id = pane.update(cx, |pane, _| {
2600 pane.nav_history_mut().set_mode(mode);
2601 pane.active_item().map(|p| p.item_id())
2602 })?;
2603
2604 pane.update_in(cx, |pane, window, cx| {
2605 let item = pane.open_item(
2606 project_entry_id,
2607 project_path,
2608 true,
2609 entry.is_preview,
2610 true,
2611 None,
2612 window, cx,
2613 build_item,
2614 );
2615 navigated |= Some(item.item_id()) != prev_active_item_id;
2616 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2617 if let Some(data) = entry.data {
2618 navigated |= item.navigate(data, window, cx);
2619 }
2620 })?;
2621 }
2622 Err(open_by_project_path_e) => {
2623 // Fall back to opening by abs path, in case an external file was opened and closed,
2624 // and its worktree is now dropped
2625 if let Some(abs_path) = abs_path {
2626 let prev_active_item_id = pane.update(cx, |pane, _| {
2627 pane.nav_history_mut().set_mode(mode);
2628 pane.active_item().map(|p| p.item_id())
2629 })?;
2630 let open_by_abs_path = workspace.update_in(cx, |workspace, window, cx| {
2631 workspace.open_abs_path(abs_path.clone(), OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
2632 })?;
2633 match open_by_abs_path
2634 .await
2635 .with_context(|| format!("Navigating to {abs_path:?}"))
2636 {
2637 Ok(item) => {
2638 pane.update_in(cx, |pane, window, cx| {
2639 navigated |= Some(item.item_id()) != prev_active_item_id;
2640 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2641 if let Some(data) = entry.data {
2642 navigated |= item.navigate(data, window, cx);
2643 }
2644 })?;
2645 }
2646 Err(open_by_abs_path_e) => {
2647 log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
2648 }
2649 }
2650 }
2651 }
2652 }
2653
2654 if !navigated {
2655 workspace
2656 .update_in(cx, |workspace, window, cx| {
2657 Self::navigate_history(workspace, pane, mode, window, cx)
2658 })?
2659 .await?;
2660 }
2661
2662 Ok(())
2663 })
2664 } else {
2665 Task::ready(Ok(()))
2666 }
2667 }
2668
2669 pub fn go_back(
2670 &mut self,
2671 pane: WeakEntity<Pane>,
2672 window: &mut Window,
2673 cx: &mut Context<Workspace>,
2674 ) -> Task<Result<()>> {
2675 self.navigate_history(pane, NavigationMode::GoingBack, window, cx)
2676 }
2677
2678 pub fn go_forward(
2679 &mut self,
2680 pane: WeakEntity<Pane>,
2681 window: &mut Window,
2682 cx: &mut Context<Workspace>,
2683 ) -> Task<Result<()>> {
2684 self.navigate_history(pane, NavigationMode::GoingForward, window, cx)
2685 }
2686
2687 pub fn reopen_closed_item(
2688 &mut self,
2689 window: &mut Window,
2690 cx: &mut Context<Workspace>,
2691 ) -> Task<Result<()>> {
2692 self.navigate_history(
2693 self.active_pane().downgrade(),
2694 NavigationMode::ReopeningClosedItem,
2695 window,
2696 cx,
2697 )
2698 }
2699
2700 pub fn client(&self) -> &Arc<Client> {
2701 &self.app_state.client
2702 }
2703
2704 pub fn set_titlebar_item(&mut self, item: AnyView, _: &mut Window, cx: &mut Context<Self>) {
2705 self.titlebar_item = Some(item);
2706 cx.notify();
2707 }
2708
2709 pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
2710 self.on_prompt_for_new_path = Some(prompt)
2711 }
2712
2713 pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) {
2714 self.on_prompt_for_open_path = Some(prompt)
2715 }
2716
2717 pub fn set_terminal_provider(&mut self, provider: impl TerminalProvider + 'static) {
2718 self.terminal_provider = Some(Box::new(provider));
2719 }
2720
2721 pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) {
2722 self.debugger_provider = Some(Arc::new(provider));
2723 }
2724
2725 pub fn debugger_provider(&self) -> Option<Arc<dyn DebuggerProvider>> {
2726 self.debugger_provider.clone()
2727 }
2728
2729 pub fn prompt_for_open_path(
2730 &mut self,
2731 path_prompt_options: PathPromptOptions,
2732 lister: DirectoryLister,
2733 window: &mut Window,
2734 cx: &mut Context<Self>,
2735 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
2736 if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
2737 let prompt = self.on_prompt_for_open_path.take().unwrap();
2738 let rx = prompt(self, lister, window, cx);
2739 self.on_prompt_for_open_path = Some(prompt);
2740 rx
2741 } else {
2742 let (tx, rx) = oneshot::channel();
2743 let abs_path = cx.prompt_for_paths(path_prompt_options);
2744
2745 cx.spawn_in(window, async move |workspace, cx| {
2746 let Ok(result) = abs_path.await else {
2747 return Ok(());
2748 };
2749
2750 match result {
2751 Ok(result) => {
2752 tx.send(result).ok();
2753 }
2754 Err(err) => {
2755 let rx = workspace.update_in(cx, |workspace, window, cx| {
2756 workspace.show_portal_error(err.to_string(), cx);
2757 let prompt = workspace.on_prompt_for_open_path.take().unwrap();
2758 let rx = prompt(workspace, lister, window, cx);
2759 workspace.on_prompt_for_open_path = Some(prompt);
2760 rx
2761 })?;
2762 if let Ok(path) = rx.await {
2763 tx.send(path).ok();
2764 }
2765 }
2766 };
2767 anyhow::Ok(())
2768 })
2769 .detach();
2770
2771 rx
2772 }
2773 }
2774
2775 pub fn prompt_for_new_path(
2776 &mut self,
2777 lister: DirectoryLister,
2778 suggested_name: Option<String>,
2779 window: &mut Window,
2780 cx: &mut Context<Self>,
2781 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
2782 if self.project.read(cx).is_via_collab()
2783 || self.project.read(cx).is_via_remote_server()
2784 || !WorkspaceSettings::get_global(cx).use_system_path_prompts
2785 {
2786 let prompt = self.on_prompt_for_new_path.take().unwrap();
2787 let rx = prompt(self, lister, suggested_name, window, cx);
2788 self.on_prompt_for_new_path = Some(prompt);
2789 return rx;
2790 }
2791
2792 let (tx, rx) = oneshot::channel();
2793 cx.spawn_in(window, async move |workspace, cx| {
2794 let abs_path = workspace.update(cx, |workspace, cx| {
2795 let relative_to = workspace
2796 .most_recent_active_path(cx)
2797 .and_then(|p| p.parent().map(|p| p.to_path_buf()))
2798 .or_else(|| {
2799 let project = workspace.project.read(cx);
2800 project.visible_worktrees(cx).find_map(|worktree| {
2801 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
2802 })
2803 })
2804 .or_else(std::env::home_dir)
2805 .unwrap_or_else(|| PathBuf::from(""));
2806 cx.prompt_for_new_path(&relative_to, suggested_name.as_deref())
2807 })?;
2808 let abs_path = match abs_path.await? {
2809 Ok(path) => path,
2810 Err(err) => {
2811 let rx = workspace.update_in(cx, |workspace, window, cx| {
2812 workspace.show_portal_error(err.to_string(), cx);
2813
2814 let prompt = workspace.on_prompt_for_new_path.take().unwrap();
2815 let rx = prompt(workspace, lister, suggested_name, window, cx);
2816 workspace.on_prompt_for_new_path = Some(prompt);
2817 rx
2818 })?;
2819 if let Ok(path) = rx.await {
2820 tx.send(path).ok();
2821 }
2822 return anyhow::Ok(());
2823 }
2824 };
2825
2826 tx.send(abs_path.map(|path| vec![path])).ok();
2827 anyhow::Ok(())
2828 })
2829 .detach();
2830
2831 rx
2832 }
2833
2834 pub fn titlebar_item(&self) -> Option<AnyView> {
2835 self.titlebar_item.clone()
2836 }
2837
2838 /// Returns the worktree override set by the user (e.g., via the project dropdown).
2839 /// When set, git-related operations should use this worktree instead of deriving
2840 /// the active worktree from the focused file.
2841 pub fn active_worktree_override(&self) -> Option<WorktreeId> {
2842 self.active_worktree_override
2843 }
2844
2845 pub fn set_active_worktree_override(
2846 &mut self,
2847 worktree_id: Option<WorktreeId>,
2848 cx: &mut Context<Self>,
2849 ) {
2850 self.active_worktree_override = worktree_id;
2851 cx.notify();
2852 }
2853
2854 pub fn clear_active_worktree_override(&mut self, cx: &mut Context<Self>) {
2855 self.active_worktree_override = None;
2856 cx.notify();
2857 }
2858
2859 /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
2860 ///
2861 /// If the given workspace has a local project, then it will be passed
2862 /// to the callback. Otherwise, a new empty window will be created.
2863 pub fn with_local_workspace<T, F>(
2864 &mut self,
2865 window: &mut Window,
2866 cx: &mut Context<Self>,
2867 callback: F,
2868 ) -> Task<Result<T>>
2869 where
2870 T: 'static,
2871 F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
2872 {
2873 if self.project.read(cx).is_local() {
2874 Task::ready(Ok(callback(self, window, cx)))
2875 } else {
2876 let env = self.project.read(cx).cli_environment(cx);
2877 let task = Self::new_local(
2878 Vec::new(),
2879 self.app_state.clone(),
2880 None,
2881 env,
2882 None,
2883 true,
2884 cx,
2885 );
2886 cx.spawn_in(window, async move |_vh, cx| {
2887 let OpenResult {
2888 window: multi_workspace_window,
2889 ..
2890 } = task.await?;
2891 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
2892 let workspace = multi_workspace.workspace().clone();
2893 workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
2894 })
2895 })
2896 }
2897 }
2898
2899 /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
2900 ///
2901 /// If the given workspace has a local project, then it will be passed
2902 /// to the callback. Otherwise, a new empty window will be created.
2903 pub fn with_local_or_wsl_workspace<T, F>(
2904 &mut self,
2905 window: &mut Window,
2906 cx: &mut Context<Self>,
2907 callback: F,
2908 ) -> Task<Result<T>>
2909 where
2910 T: 'static,
2911 F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
2912 {
2913 let project = self.project.read(cx);
2914 if project.is_local() || project.is_via_wsl_with_host_interop(cx) {
2915 Task::ready(Ok(callback(self, window, cx)))
2916 } else {
2917 let env = self.project.read(cx).cli_environment(cx);
2918 let task = Self::new_local(
2919 Vec::new(),
2920 self.app_state.clone(),
2921 None,
2922 env,
2923 None,
2924 true,
2925 cx,
2926 );
2927 cx.spawn_in(window, async move |_vh, cx| {
2928 let OpenResult {
2929 window: multi_workspace_window,
2930 ..
2931 } = task.await?;
2932 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
2933 let workspace = multi_workspace.workspace().clone();
2934 workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
2935 })
2936 })
2937 }
2938 }
2939
2940 pub fn worktrees<'a>(&self, cx: &'a App) -> impl 'a + Iterator<Item = Entity<Worktree>> {
2941 self.project.read(cx).worktrees(cx)
2942 }
2943
2944 pub fn visible_worktrees<'a>(
2945 &self,
2946 cx: &'a App,
2947 ) -> impl 'a + Iterator<Item = Entity<Worktree>> {
2948 self.project.read(cx).visible_worktrees(cx)
2949 }
2950
2951 #[cfg(any(test, feature = "test-support"))]
2952 pub fn worktree_scans_complete(&self, cx: &App) -> impl Future<Output = ()> + 'static + use<> {
2953 let futures = self
2954 .worktrees(cx)
2955 .filter_map(|worktree| worktree.read(cx).as_local())
2956 .map(|worktree| worktree.scan_complete())
2957 .collect::<Vec<_>>();
2958 async move {
2959 for future in futures {
2960 future.await;
2961 }
2962 }
2963 }
2964
2965 pub fn close_global(cx: &mut App) {
2966 cx.defer(|cx| {
2967 cx.windows().iter().find(|window| {
2968 window
2969 .update(cx, |_, window, _| {
2970 if window.is_window_active() {
2971 //This can only get called when the window's project connection has been lost
2972 //so we don't need to prompt the user for anything and instead just close the window
2973 window.remove_window();
2974 true
2975 } else {
2976 false
2977 }
2978 })
2979 .unwrap_or(false)
2980 });
2981 });
2982 }
2983
2984 pub fn move_focused_panel_to_next_position(
2985 &mut self,
2986 _: &MoveFocusedPanelToNextPosition,
2987 window: &mut Window,
2988 cx: &mut Context<Self>,
2989 ) {
2990 let docks = self.all_docks();
2991 let active_dock = docks
2992 .into_iter()
2993 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
2994
2995 if let Some(dock) = active_dock {
2996 dock.update(cx, |dock, cx| {
2997 let active_panel = dock
2998 .active_panel()
2999 .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx));
3000
3001 if let Some(panel) = active_panel {
3002 panel.move_to_next_position(window, cx);
3003 }
3004 })
3005 }
3006 }
3007
3008 pub fn prepare_to_close(
3009 &mut self,
3010 close_intent: CloseIntent,
3011 window: &mut Window,
3012 cx: &mut Context<Self>,
3013 ) -> Task<Result<bool>> {
3014 let active_call = self.active_global_call();
3015
3016 cx.spawn_in(window, async move |this, cx| {
3017 this.update(cx, |this, _| {
3018 if close_intent == CloseIntent::CloseWindow {
3019 this.removing = true;
3020 }
3021 })?;
3022
3023 let workspace_count = cx.update(|_window, cx| {
3024 cx.windows()
3025 .iter()
3026 .filter(|window| window.downcast::<MultiWorkspace>().is_some())
3027 .count()
3028 })?;
3029
3030 #[cfg(target_os = "macos")]
3031 let save_last_workspace = false;
3032
3033 // On Linux and Windows, closing the last window should restore the last workspace.
3034 #[cfg(not(target_os = "macos"))]
3035 let save_last_workspace = {
3036 let remaining_workspaces = cx.update(|_window, cx| {
3037 cx.windows()
3038 .iter()
3039 .filter_map(|window| window.downcast::<MultiWorkspace>())
3040 .filter_map(|multi_workspace| {
3041 multi_workspace
3042 .update(cx, |multi_workspace, _, cx| {
3043 multi_workspace.workspace().read(cx).removing
3044 })
3045 .ok()
3046 })
3047 .filter(|removing| !removing)
3048 .count()
3049 })?;
3050
3051 close_intent != CloseIntent::ReplaceWindow && remaining_workspaces == 0
3052 };
3053
3054 if let Some(active_call) = active_call
3055 && workspace_count == 1
3056 && cx
3057 .update(|_window, cx| active_call.0.is_in_room(cx))
3058 .unwrap_or(false)
3059 {
3060 if close_intent == CloseIntent::CloseWindow {
3061 this.update(cx, |_, cx| cx.emit(Event::Activate))?;
3062 let answer = cx.update(|window, cx| {
3063 window.prompt(
3064 PromptLevel::Warning,
3065 "Do you want to leave the current call?",
3066 None,
3067 &["Close window and hang up", "Cancel"],
3068 cx,
3069 )
3070 })?;
3071
3072 if answer.await.log_err() == Some(1) {
3073 return anyhow::Ok(false);
3074 } else {
3075 if let Ok(task) = cx.update(|_window, cx| active_call.0.hang_up(cx)) {
3076 task.await.log_err();
3077 }
3078 }
3079 }
3080 if close_intent == CloseIntent::ReplaceWindow {
3081 _ = cx.update(|_window, cx| {
3082 let multi_workspace = cx
3083 .windows()
3084 .iter()
3085 .filter_map(|window| window.downcast::<MultiWorkspace>())
3086 .next()
3087 .unwrap();
3088 let project = multi_workspace
3089 .read(cx)?
3090 .workspace()
3091 .read(cx)
3092 .project
3093 .clone();
3094 if project.read(cx).is_shared() {
3095 active_call.0.unshare_project(project, cx)?;
3096 }
3097 Ok::<_, anyhow::Error>(())
3098 });
3099 }
3100 }
3101
3102 let save_result = this
3103 .update_in(cx, |this, window, cx| {
3104 this.save_all_internal(SaveIntent::Close, window, cx)
3105 })?
3106 .await;
3107
3108 // If we're not quitting, but closing, we remove the workspace from
3109 // the current session.
3110 if close_intent != CloseIntent::Quit
3111 && !save_last_workspace
3112 && save_result.as_ref().is_ok_and(|&res| res)
3113 {
3114 this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx))?
3115 .await;
3116 }
3117
3118 save_result
3119 })
3120 }
3121
3122 fn save_all(&mut self, action: &SaveAll, window: &mut Window, cx: &mut Context<Self>) {
3123 self.save_all_internal(
3124 action.save_intent.unwrap_or(SaveIntent::SaveAll),
3125 window,
3126 cx,
3127 )
3128 .detach_and_log_err(cx);
3129 }
3130
3131 fn send_keystrokes(
3132 &mut self,
3133 action: &SendKeystrokes,
3134 window: &mut Window,
3135 cx: &mut Context<Self>,
3136 ) {
3137 let keystrokes: Vec<Keystroke> = action
3138 .0
3139 .split(' ')
3140 .flat_map(|k| Keystroke::parse(k).log_err())
3141 .map(|k| {
3142 cx.keyboard_mapper()
3143 .map_key_equivalent(k, false)
3144 .inner()
3145 .clone()
3146 })
3147 .collect();
3148 let _ = self.send_keystrokes_impl(keystrokes, window, cx);
3149 }
3150
3151 pub fn send_keystrokes_impl(
3152 &mut self,
3153 keystrokes: Vec<Keystroke>,
3154 window: &mut Window,
3155 cx: &mut Context<Self>,
3156 ) -> Shared<Task<()>> {
3157 let mut state = self.dispatching_keystrokes.borrow_mut();
3158 if !state.dispatched.insert(keystrokes.clone()) {
3159 cx.propagate();
3160 return state.task.clone().unwrap();
3161 }
3162
3163 state.queue.extend(keystrokes);
3164
3165 let keystrokes = self.dispatching_keystrokes.clone();
3166 if state.task.is_none() {
3167 state.task = Some(
3168 window
3169 .spawn(cx, async move |cx| {
3170 // limit to 100 keystrokes to avoid infinite recursion.
3171 for _ in 0..100 {
3172 let keystroke = {
3173 let mut state = keystrokes.borrow_mut();
3174 let Some(keystroke) = state.queue.pop_front() else {
3175 state.dispatched.clear();
3176 state.task.take();
3177 return;
3178 };
3179 keystroke
3180 };
3181 cx.update(|window, cx| {
3182 let focused = window.focused(cx);
3183 window.dispatch_keystroke(keystroke.clone(), cx);
3184 if window.focused(cx) != focused {
3185 // dispatch_keystroke may cause the focus to change.
3186 // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
3187 // And we need that to happen before the next keystroke to keep vim mode happy...
3188 // (Note that the tests always do this implicitly, so you must manually test with something like:
3189 // "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
3190 // )
3191 window.draw(cx).clear();
3192 }
3193 })
3194 .ok();
3195
3196 // Yield between synthetic keystrokes so deferred focus and
3197 // other effects can settle before dispatching the next key.
3198 yield_now().await;
3199 }
3200
3201 *keystrokes.borrow_mut() = Default::default();
3202 log::error!("over 100 keystrokes passed to send_keystrokes");
3203 })
3204 .shared(),
3205 );
3206 }
3207 state.task.clone().unwrap()
3208 }
3209
3210 fn save_all_internal(
3211 &mut self,
3212 mut save_intent: SaveIntent,
3213 window: &mut Window,
3214 cx: &mut Context<Self>,
3215 ) -> Task<Result<bool>> {
3216 if self.project.read(cx).is_disconnected(cx) {
3217 return Task::ready(Ok(true));
3218 }
3219 let dirty_items = self
3220 .panes
3221 .iter()
3222 .flat_map(|pane| {
3223 pane.read(cx).items().filter_map(|item| {
3224 if item.is_dirty(cx) {
3225 item.tab_content_text(0, cx);
3226 Some((pane.downgrade(), item.boxed_clone()))
3227 } else {
3228 None
3229 }
3230 })
3231 })
3232 .collect::<Vec<_>>();
3233
3234 let project = self.project.clone();
3235 cx.spawn_in(window, async move |workspace, cx| {
3236 let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() {
3237 let (serialize_tasks, remaining_dirty_items) =
3238 workspace.update_in(cx, |workspace, window, cx| {
3239 let mut remaining_dirty_items = Vec::new();
3240 let mut serialize_tasks = Vec::new();
3241 for (pane, item) in dirty_items {
3242 if let Some(task) = item
3243 .to_serializable_item_handle(cx)
3244 .and_then(|handle| handle.serialize(workspace, true, window, cx))
3245 {
3246 serialize_tasks.push(task);
3247 } else {
3248 remaining_dirty_items.push((pane, item));
3249 }
3250 }
3251 (serialize_tasks, remaining_dirty_items)
3252 })?;
3253
3254 futures::future::try_join_all(serialize_tasks).await?;
3255
3256 if !remaining_dirty_items.is_empty() {
3257 workspace.update(cx, |_, cx| cx.emit(Event::Activate))?;
3258 }
3259
3260 if remaining_dirty_items.len() > 1 {
3261 let answer = workspace.update_in(cx, |_, window, cx| {
3262 let detail = Pane::file_names_for_prompt(
3263 &mut remaining_dirty_items.iter().map(|(_, handle)| handle),
3264 cx,
3265 );
3266 window.prompt(
3267 PromptLevel::Warning,
3268 "Do you want to save all changes in the following files?",
3269 Some(&detail),
3270 &["Save all", "Discard all", "Cancel"],
3271 cx,
3272 )
3273 })?;
3274 match answer.await.log_err() {
3275 Some(0) => save_intent = SaveIntent::SaveAll,
3276 Some(1) => save_intent = SaveIntent::Skip,
3277 Some(2) => return Ok(false),
3278 _ => {}
3279 }
3280 }
3281
3282 remaining_dirty_items
3283 } else {
3284 dirty_items
3285 };
3286
3287 for (pane, item) in dirty_items {
3288 let (singleton, project_entry_ids) = cx.update(|_, cx| {
3289 (
3290 item.buffer_kind(cx) == ItemBufferKind::Singleton,
3291 item.project_entry_ids(cx),
3292 )
3293 })?;
3294 if (singleton || !project_entry_ids.is_empty())
3295 && !Pane::save_item(project.clone(), &pane, &*item, save_intent, cx).await?
3296 {
3297 return Ok(false);
3298 }
3299 }
3300 Ok(true)
3301 })
3302 }
3303
3304 pub fn open_workspace_for_paths(
3305 &mut self,
3306 replace_current_window: bool,
3307 paths: Vec<PathBuf>,
3308 window: &mut Window,
3309 cx: &mut Context<Self>,
3310 ) -> Task<Result<Entity<Workspace>>> {
3311 let window_handle = window.window_handle().downcast::<MultiWorkspace>();
3312 let is_remote = self.project.read(cx).is_via_collab();
3313 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
3314 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
3315
3316 let window_to_replace = if replace_current_window {
3317 window_handle
3318 } else if is_remote || has_worktree || has_dirty_items {
3319 None
3320 } else {
3321 window_handle
3322 };
3323 let app_state = self.app_state.clone();
3324
3325 cx.spawn(async move |_, cx| {
3326 let OpenResult { workspace, .. } = cx
3327 .update(|cx| {
3328 open_paths(
3329 &paths,
3330 app_state,
3331 OpenOptions {
3332 replace_window: window_to_replace,
3333 ..Default::default()
3334 },
3335 cx,
3336 )
3337 })
3338 .await?;
3339 Ok(workspace)
3340 })
3341 }
3342
3343 #[allow(clippy::type_complexity)]
3344 pub fn open_paths(
3345 &mut self,
3346 mut abs_paths: Vec<PathBuf>,
3347 options: OpenOptions,
3348 pane: Option<WeakEntity<Pane>>,
3349 window: &mut Window,
3350 cx: &mut Context<Self>,
3351 ) -> Task<Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>> {
3352 let fs = self.app_state.fs.clone();
3353
3354 let caller_ordered_abs_paths = abs_paths.clone();
3355
3356 // Sort the paths to ensure we add worktrees for parents before their children.
3357 abs_paths.sort_unstable();
3358 cx.spawn_in(window, async move |this, cx| {
3359 let mut tasks = Vec::with_capacity(abs_paths.len());
3360
3361 for abs_path in &abs_paths {
3362 let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
3363 OpenVisible::All => Some(true),
3364 OpenVisible::None => Some(false),
3365 OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
3366 Some(Some(metadata)) => Some(!metadata.is_dir),
3367 Some(None) => Some(true),
3368 None => None,
3369 },
3370 OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
3371 Some(Some(metadata)) => Some(metadata.is_dir),
3372 Some(None) => Some(false),
3373 None => None,
3374 },
3375 };
3376 let project_path = match visible {
3377 Some(visible) => match this
3378 .update(cx, |this, cx| {
3379 Workspace::project_path_for_path(
3380 this.project.clone(),
3381 abs_path,
3382 visible,
3383 cx,
3384 )
3385 })
3386 .log_err()
3387 {
3388 Some(project_path) => project_path.await.log_err(),
3389 None => None,
3390 },
3391 None => None,
3392 };
3393
3394 let this = this.clone();
3395 let abs_path: Arc<Path> = SanitizedPath::new(&abs_path).as_path().into();
3396 let fs = fs.clone();
3397 let pane = pane.clone();
3398 let task = cx.spawn(async move |cx| {
3399 let (_worktree, project_path) = project_path?;
3400 if fs.is_dir(&abs_path).await {
3401 // Opening a directory should not race to update the active entry.
3402 // We'll select/reveal a deterministic final entry after all paths finish opening.
3403 None
3404 } else {
3405 Some(
3406 this.update_in(cx, |this, window, cx| {
3407 this.open_path(
3408 project_path,
3409 pane,
3410 options.focus.unwrap_or(true),
3411 window,
3412 cx,
3413 )
3414 })
3415 .ok()?
3416 .await,
3417 )
3418 }
3419 });
3420 tasks.push(task);
3421 }
3422
3423 let results = futures::future::join_all(tasks).await;
3424
3425 // Determine the winner using the fake/abstract FS metadata, not `Path::is_dir`.
3426 let mut winner: Option<(PathBuf, bool)> = None;
3427 for abs_path in caller_ordered_abs_paths.into_iter().rev() {
3428 if let Some(Some(metadata)) = fs.metadata(&abs_path).await.log_err() {
3429 if !metadata.is_dir {
3430 winner = Some((abs_path, false));
3431 break;
3432 }
3433 if winner.is_none() {
3434 winner = Some((abs_path, true));
3435 }
3436 } else if winner.is_none() {
3437 winner = Some((abs_path, false));
3438 }
3439 }
3440
3441 // Compute the winner entry id on the foreground thread and emit once, after all
3442 // paths finish opening. This avoids races between concurrently-opening paths
3443 // (directories in particular) and makes the resulting project panel selection
3444 // deterministic.
3445 if let Some((winner_abs_path, winner_is_dir)) = winner {
3446 'emit_winner: {
3447 let winner_abs_path: Arc<Path> =
3448 SanitizedPath::new(&winner_abs_path).as_path().into();
3449
3450 let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
3451 OpenVisible::All => true,
3452 OpenVisible::None => false,
3453 OpenVisible::OnlyFiles => !winner_is_dir,
3454 OpenVisible::OnlyDirectories => winner_is_dir,
3455 };
3456
3457 let Some(worktree_task) = this
3458 .update(cx, |workspace, cx| {
3459 workspace.project.update(cx, |project, cx| {
3460 project.find_or_create_worktree(
3461 winner_abs_path.as_ref(),
3462 visible,
3463 cx,
3464 )
3465 })
3466 })
3467 .ok()
3468 else {
3469 break 'emit_winner;
3470 };
3471
3472 let Ok((worktree, _)) = worktree_task.await else {
3473 break 'emit_winner;
3474 };
3475
3476 let Ok(Some(entry_id)) = this.update(cx, |_, cx| {
3477 let worktree = worktree.read(cx);
3478 let worktree_abs_path = worktree.abs_path();
3479 let entry = if winner_abs_path.as_ref() == worktree_abs_path.as_ref() {
3480 worktree.root_entry()
3481 } else {
3482 winner_abs_path
3483 .strip_prefix(worktree_abs_path.as_ref())
3484 .ok()
3485 .and_then(|relative_path| {
3486 let relative_path =
3487 RelPath::new(relative_path, PathStyle::local())
3488 .log_err()?;
3489 worktree.entry_for_path(&relative_path)
3490 })
3491 }?;
3492 Some(entry.id)
3493 }) else {
3494 break 'emit_winner;
3495 };
3496
3497 this.update(cx, |workspace, cx| {
3498 workspace.project.update(cx, |_, cx| {
3499 cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
3500 });
3501 })
3502 .ok();
3503 }
3504 }
3505
3506 results
3507 })
3508 }
3509
3510 pub fn open_resolved_path(
3511 &mut self,
3512 path: ResolvedPath,
3513 window: &mut Window,
3514 cx: &mut Context<Self>,
3515 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3516 match path {
3517 ResolvedPath::ProjectPath { project_path, .. } => {
3518 self.open_path(project_path, None, true, window, cx)
3519 }
3520 ResolvedPath::AbsPath { path, .. } => self.open_abs_path(
3521 PathBuf::from(path),
3522 OpenOptions {
3523 visible: Some(OpenVisible::None),
3524 ..Default::default()
3525 },
3526 window,
3527 cx,
3528 ),
3529 }
3530 }
3531
3532 pub fn absolute_path_of_worktree(
3533 &self,
3534 worktree_id: WorktreeId,
3535 cx: &mut Context<Self>,
3536 ) -> Option<PathBuf> {
3537 self.project
3538 .read(cx)
3539 .worktree_for_id(worktree_id, cx)
3540 // TODO: use `abs_path` or `root_dir`
3541 .map(|wt| wt.read(cx).abs_path().as_ref().to_path_buf())
3542 }
3543
3544 pub fn add_folder_to_project(
3545 &mut self,
3546 _: &AddFolderToProject,
3547 window: &mut Window,
3548 cx: &mut Context<Self>,
3549 ) {
3550 let project = self.project.read(cx);
3551 if project.is_via_collab() {
3552 self.show_error(
3553 &anyhow!("You cannot add folders to someone else's project"),
3554 cx,
3555 );
3556 return;
3557 }
3558 let paths = self.prompt_for_open_path(
3559 PathPromptOptions {
3560 files: false,
3561 directories: true,
3562 multiple: true,
3563 prompt: None,
3564 },
3565 DirectoryLister::Project(self.project.clone()),
3566 window,
3567 cx,
3568 );
3569 cx.spawn_in(window, async move |this, cx| {
3570 if let Some(paths) = paths.await.log_err().flatten() {
3571 let results = this
3572 .update_in(cx, |this, window, cx| {
3573 this.open_paths(
3574 paths,
3575 OpenOptions {
3576 visible: Some(OpenVisible::All),
3577 ..Default::default()
3578 },
3579 None,
3580 window,
3581 cx,
3582 )
3583 })?
3584 .await;
3585 for result in results.into_iter().flatten() {
3586 result.log_err();
3587 }
3588 }
3589 anyhow::Ok(())
3590 })
3591 .detach_and_log_err(cx);
3592 }
3593
3594 pub fn project_path_for_path(
3595 project: Entity<Project>,
3596 abs_path: &Path,
3597 visible: bool,
3598 cx: &mut App,
3599 ) -> Task<Result<(Entity<Worktree>, ProjectPath)>> {
3600 let entry = project.update(cx, |project, cx| {
3601 project.find_or_create_worktree(abs_path, visible, cx)
3602 });
3603 cx.spawn(async move |cx| {
3604 let (worktree, path) = entry.await?;
3605 let worktree_id = worktree.read_with(cx, |t, _| t.id());
3606 Ok((worktree, ProjectPath { worktree_id, path }))
3607 })
3608 }
3609
3610 pub fn items<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator<Item = &'a Box<dyn ItemHandle>> {
3611 self.panes.iter().flat_map(|pane| pane.read(cx).items())
3612 }
3613
3614 pub fn item_of_type<T: Item>(&self, cx: &App) -> Option<Entity<T>> {
3615 self.items_of_type(cx).max_by_key(|item| item.item_id())
3616 }
3617
3618 pub fn items_of_type<'a, T: Item>(
3619 &'a self,
3620 cx: &'a App,
3621 ) -> impl 'a + Iterator<Item = Entity<T>> {
3622 self.panes
3623 .iter()
3624 .flat_map(|pane| pane.read(cx).items_of_type())
3625 }
3626
3627 pub fn active_item(&self, cx: &App) -> Option<Box<dyn ItemHandle>> {
3628 self.active_pane().read(cx).active_item()
3629 }
3630
3631 pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
3632 let item = self.active_item(cx)?;
3633 item.to_any_view().downcast::<I>().ok()
3634 }
3635
3636 fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
3637 self.active_item(cx).and_then(|item| item.project_path(cx))
3638 }
3639
3640 pub fn most_recent_active_path(&self, cx: &App) -> Option<PathBuf> {
3641 self.recent_navigation_history_iter(cx)
3642 .filter_map(|(path, abs_path)| {
3643 let worktree = self
3644 .project
3645 .read(cx)
3646 .worktree_for_id(path.worktree_id, cx)?;
3647 if worktree.read(cx).is_visible() {
3648 abs_path
3649 } else {
3650 None
3651 }
3652 })
3653 .next()
3654 }
3655
3656 pub fn save_active_item(
3657 &mut self,
3658 save_intent: SaveIntent,
3659 window: &mut Window,
3660 cx: &mut App,
3661 ) -> Task<Result<()>> {
3662 let project = self.project.clone();
3663 let pane = self.active_pane();
3664 let item = pane.read(cx).active_item();
3665 let pane = pane.downgrade();
3666
3667 window.spawn(cx, async move |cx| {
3668 if let Some(item) = item {
3669 Pane::save_item(project, &pane, item.as_ref(), save_intent, cx)
3670 .await
3671 .map(|_| ())
3672 } else {
3673 Ok(())
3674 }
3675 })
3676 }
3677
3678 pub fn close_inactive_items_and_panes(
3679 &mut self,
3680 action: &CloseInactiveTabsAndPanes,
3681 window: &mut Window,
3682 cx: &mut Context<Self>,
3683 ) {
3684 if let Some(task) = self.close_all_internal(
3685 true,
3686 action.save_intent.unwrap_or(SaveIntent::Close),
3687 window,
3688 cx,
3689 ) {
3690 task.detach_and_log_err(cx)
3691 }
3692 }
3693
3694 pub fn close_all_items_and_panes(
3695 &mut self,
3696 action: &CloseAllItemsAndPanes,
3697 window: &mut Window,
3698 cx: &mut Context<Self>,
3699 ) {
3700 if let Some(task) = self.close_all_internal(
3701 false,
3702 action.save_intent.unwrap_or(SaveIntent::Close),
3703 window,
3704 cx,
3705 ) {
3706 task.detach_and_log_err(cx)
3707 }
3708 }
3709
3710 /// Closes the active item across all panes.
3711 pub fn close_item_in_all_panes(
3712 &mut self,
3713 action: &CloseItemInAllPanes,
3714 window: &mut Window,
3715 cx: &mut Context<Self>,
3716 ) {
3717 let Some(active_item) = self.active_pane().read(cx).active_item() else {
3718 return;
3719 };
3720
3721 let save_intent = action.save_intent.unwrap_or(SaveIntent::Close);
3722 let close_pinned = action.close_pinned;
3723
3724 if let Some(project_path) = active_item.project_path(cx) {
3725 self.close_items_with_project_path(
3726 &project_path,
3727 save_intent,
3728 close_pinned,
3729 window,
3730 cx,
3731 );
3732 } else if close_pinned || !self.active_pane().read(cx).is_active_item_pinned() {
3733 let item_id = active_item.item_id();
3734 self.active_pane().update(cx, |pane, cx| {
3735 pane.close_item_by_id(item_id, save_intent, window, cx)
3736 .detach_and_log_err(cx);
3737 });
3738 }
3739 }
3740
3741 /// Closes all items with the given project path across all panes.
3742 pub fn close_items_with_project_path(
3743 &mut self,
3744 project_path: &ProjectPath,
3745 save_intent: SaveIntent,
3746 close_pinned: bool,
3747 window: &mut Window,
3748 cx: &mut Context<Self>,
3749 ) {
3750 let panes = self.panes().to_vec();
3751 for pane in panes {
3752 pane.update(cx, |pane, cx| {
3753 pane.close_items_for_project_path(
3754 project_path,
3755 save_intent,
3756 close_pinned,
3757 window,
3758 cx,
3759 )
3760 .detach_and_log_err(cx);
3761 });
3762 }
3763 }
3764
3765 fn close_all_internal(
3766 &mut self,
3767 retain_active_pane: bool,
3768 save_intent: SaveIntent,
3769 window: &mut Window,
3770 cx: &mut Context<Self>,
3771 ) -> Option<Task<Result<()>>> {
3772 let current_pane = self.active_pane();
3773
3774 let mut tasks = Vec::new();
3775
3776 if retain_active_pane {
3777 let current_pane_close = current_pane.update(cx, |pane, cx| {
3778 pane.close_other_items(
3779 &CloseOtherItems {
3780 save_intent: None,
3781 close_pinned: false,
3782 },
3783 None,
3784 window,
3785 cx,
3786 )
3787 });
3788
3789 tasks.push(current_pane_close);
3790 }
3791
3792 for pane in self.panes() {
3793 if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
3794 continue;
3795 }
3796
3797 let close_pane_items = pane.update(cx, |pane: &mut Pane, cx| {
3798 pane.close_all_items(
3799 &CloseAllItems {
3800 save_intent: Some(save_intent),
3801 close_pinned: false,
3802 },
3803 window,
3804 cx,
3805 )
3806 });
3807
3808 tasks.push(close_pane_items)
3809 }
3810
3811 if tasks.is_empty() {
3812 None
3813 } else {
3814 Some(cx.spawn_in(window, async move |_, _| {
3815 for task in tasks {
3816 task.await?
3817 }
3818 Ok(())
3819 }))
3820 }
3821 }
3822
3823 pub fn is_dock_at_position_open(&self, position: DockPosition, cx: &mut Context<Self>) -> bool {
3824 self.dock_at_position(position).read(cx).is_open()
3825 }
3826
3827 pub fn toggle_dock(
3828 &mut self,
3829 dock_side: DockPosition,
3830 window: &mut Window,
3831 cx: &mut Context<Self>,
3832 ) {
3833 let mut focus_center = false;
3834 let mut reveal_dock = false;
3835
3836 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
3837 let was_visible = self.is_dock_at_position_open(dock_side, cx) && !other_is_zoomed;
3838
3839 if let Some(panel) = self.dock_at_position(dock_side).read(cx).active_panel() {
3840 telemetry::event!(
3841 "Panel Button Clicked",
3842 name = panel.persistent_name(),
3843 toggle_state = !was_visible
3844 );
3845 }
3846 if was_visible {
3847 self.save_open_dock_positions(cx);
3848 }
3849
3850 let dock = self.dock_at_position(dock_side);
3851 dock.update(cx, |dock, cx| {
3852 dock.set_open(!was_visible, window, cx);
3853
3854 if dock.active_panel().is_none() {
3855 let Some(panel_ix) = dock
3856 .first_enabled_panel_idx(cx)
3857 .log_with_level(log::Level::Info)
3858 else {
3859 return;
3860 };
3861 dock.activate_panel(panel_ix, window, cx);
3862 }
3863
3864 if let Some(active_panel) = dock.active_panel() {
3865 if was_visible {
3866 if active_panel
3867 .panel_focus_handle(cx)
3868 .contains_focused(window, cx)
3869 {
3870 focus_center = true;
3871 }
3872 } else {
3873 let focus_handle = &active_panel.panel_focus_handle(cx);
3874 window.focus(focus_handle, cx);
3875 reveal_dock = true;
3876 }
3877 }
3878 });
3879
3880 if reveal_dock {
3881 self.dismiss_zoomed_items_to_reveal(Some(dock_side), window, cx);
3882 }
3883
3884 if focus_center {
3885 self.active_pane
3886 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
3887 }
3888
3889 cx.notify();
3890 self.serialize_workspace(window, cx);
3891 }
3892
3893 fn active_dock(&self, window: &Window, cx: &Context<Self>) -> Option<&Entity<Dock>> {
3894 self.all_docks().into_iter().find(|&dock| {
3895 dock.read(cx).is_open() && dock.focus_handle(cx).contains_focused(window, cx)
3896 })
3897 }
3898
3899 fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
3900 if let Some(dock) = self.active_dock(window, cx).cloned() {
3901 self.save_open_dock_positions(cx);
3902 dock.update(cx, |dock, cx| {
3903 dock.set_open(false, window, cx);
3904 });
3905 return true;
3906 }
3907 false
3908 }
3909
3910 pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3911 self.save_open_dock_positions(cx);
3912 for dock in self.all_docks() {
3913 dock.update(cx, |dock, cx| {
3914 dock.set_open(false, window, cx);
3915 });
3916 }
3917
3918 cx.focus_self(window);
3919 cx.notify();
3920 self.serialize_workspace(window, cx);
3921 }
3922
3923 fn get_open_dock_positions(&self, cx: &Context<Self>) -> Vec<DockPosition> {
3924 self.all_docks()
3925 .into_iter()
3926 .filter_map(|dock| {
3927 let dock_ref = dock.read(cx);
3928 if dock_ref.is_open() {
3929 Some(dock_ref.position())
3930 } else {
3931 None
3932 }
3933 })
3934 .collect()
3935 }
3936
3937 /// Saves the positions of currently open docks.
3938 ///
3939 /// Updates `last_open_dock_positions` with positions of all currently open
3940 /// docks, to later be restored by the 'Toggle All Docks' action.
3941 fn save_open_dock_positions(&mut self, cx: &mut Context<Self>) {
3942 let open_dock_positions = self.get_open_dock_positions(cx);
3943 if !open_dock_positions.is_empty() {
3944 self.last_open_dock_positions = open_dock_positions;
3945 }
3946 }
3947
3948 /// Toggles all docks between open and closed states.
3949 ///
3950 /// If any docks are open, closes all and remembers their positions. If all
3951 /// docks are closed, restores the last remembered dock configuration.
3952 fn toggle_all_docks(
3953 &mut self,
3954 _: &ToggleAllDocks,
3955 window: &mut Window,
3956 cx: &mut Context<Self>,
3957 ) {
3958 let open_dock_positions = self.get_open_dock_positions(cx);
3959
3960 if !open_dock_positions.is_empty() {
3961 self.close_all_docks(window, cx);
3962 } else if !self.last_open_dock_positions.is_empty() {
3963 self.restore_last_open_docks(window, cx);
3964 }
3965 }
3966
3967 /// Reopens docks from the most recently remembered configuration.
3968 ///
3969 /// Opens all docks whose positions are stored in `last_open_dock_positions`
3970 /// and clears the stored positions.
3971 fn restore_last_open_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3972 let positions_to_open = std::mem::take(&mut self.last_open_dock_positions);
3973
3974 for position in positions_to_open {
3975 let dock = self.dock_at_position(position);
3976 dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
3977 }
3978
3979 cx.focus_self(window);
3980 cx.notify();
3981 self.serialize_workspace(window, cx);
3982 }
3983
3984 /// Transfer focus to the panel of the given type.
3985 pub fn focus_panel<T: Panel>(
3986 &mut self,
3987 window: &mut Window,
3988 cx: &mut Context<Self>,
3989 ) -> Option<Entity<T>> {
3990 let panel = self.focus_or_unfocus_panel::<T>(window, cx, &mut |_, _, _| true)?;
3991 panel.to_any().downcast().ok()
3992 }
3993
3994 /// Focus the panel of the given type if it isn't already focused. If it is
3995 /// already focused, then transfer focus back to the workspace center.
3996 /// When the `close_panel_on_toggle` setting is enabled, also closes the
3997 /// panel when transferring focus back to the center.
3998 pub fn toggle_panel_focus<T: Panel>(
3999 &mut self,
4000 window: &mut Window,
4001 cx: &mut Context<Self>,
4002 ) -> bool {
4003 let mut did_focus_panel = false;
4004 self.focus_or_unfocus_panel::<T>(window, cx, &mut |panel, window, cx| {
4005 did_focus_panel = !panel.panel_focus_handle(cx).contains_focused(window, cx);
4006 did_focus_panel
4007 });
4008
4009 if !did_focus_panel && WorkspaceSettings::get_global(cx).close_panel_on_toggle {
4010 self.close_panel::<T>(window, cx);
4011 }
4012
4013 telemetry::event!(
4014 "Panel Button Clicked",
4015 name = T::persistent_name(),
4016 toggle_state = did_focus_panel
4017 );
4018
4019 did_focus_panel
4020 }
4021
4022 pub fn focus_center_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4023 if let Some(item) = self.active_item(cx) {
4024 item.item_focus_handle(cx).focus(window, cx);
4025 } else {
4026 log::error!("Could not find a focus target when switching focus to the center panes",);
4027 }
4028 }
4029
4030 pub fn activate_panel_for_proto_id(
4031 &mut self,
4032 panel_id: PanelId,
4033 window: &mut Window,
4034 cx: &mut Context<Self>,
4035 ) -> Option<Arc<dyn PanelHandle>> {
4036 let mut panel = None;
4037 for dock in self.all_docks() {
4038 if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
4039 panel = dock.update(cx, |dock, cx| {
4040 dock.activate_panel(panel_index, window, cx);
4041 dock.set_open(true, window, cx);
4042 dock.active_panel().cloned()
4043 });
4044 break;
4045 }
4046 }
4047
4048 if panel.is_some() {
4049 cx.notify();
4050 self.serialize_workspace(window, cx);
4051 }
4052
4053 panel
4054 }
4055
4056 /// Focus or unfocus the given panel type, depending on the given callback.
4057 fn focus_or_unfocus_panel<T: Panel>(
4058 &mut self,
4059 window: &mut Window,
4060 cx: &mut Context<Self>,
4061 should_focus: &mut dyn FnMut(&dyn PanelHandle, &mut Window, &mut Context<Dock>) -> bool,
4062 ) -> Option<Arc<dyn PanelHandle>> {
4063 let mut result_panel = None;
4064 let mut serialize = false;
4065 for dock in self.all_docks() {
4066 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
4067 let mut focus_center = false;
4068 let panel = dock.update(cx, |dock, cx| {
4069 dock.activate_panel(panel_index, window, cx);
4070
4071 let panel = dock.active_panel().cloned();
4072 if let Some(panel) = panel.as_ref() {
4073 if should_focus(&**panel, window, cx) {
4074 dock.set_open(true, window, cx);
4075 panel.panel_focus_handle(cx).focus(window, cx);
4076 } else {
4077 focus_center = true;
4078 }
4079 }
4080 panel
4081 });
4082
4083 if focus_center {
4084 self.active_pane
4085 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
4086 }
4087
4088 result_panel = panel;
4089 serialize = true;
4090 break;
4091 }
4092 }
4093
4094 if serialize {
4095 self.serialize_workspace(window, cx);
4096 }
4097
4098 cx.notify();
4099 result_panel
4100 }
4101
4102 /// Open the panel of the given type
4103 pub fn open_panel<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4104 for dock in self.all_docks() {
4105 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
4106 dock.update(cx, |dock, cx| {
4107 dock.activate_panel(panel_index, window, cx);
4108 dock.set_open(true, window, cx);
4109 });
4110 }
4111 }
4112 }
4113
4114 pub fn close_panel<T: Panel>(&self, window: &mut Window, cx: &mut Context<Self>) {
4115 for dock in self.all_docks().iter() {
4116 dock.update(cx, |dock, cx| {
4117 if dock.panel::<T>().is_some() {
4118 dock.set_open(false, window, cx)
4119 }
4120 })
4121 }
4122 }
4123
4124 pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
4125 self.all_docks()
4126 .iter()
4127 .find_map(|dock| dock.read(cx).panel::<T>())
4128 }
4129
4130 fn dismiss_zoomed_items_to_reveal(
4131 &mut self,
4132 dock_to_reveal: Option<DockPosition>,
4133 window: &mut Window,
4134 cx: &mut Context<Self>,
4135 ) {
4136 // If a center pane is zoomed, unzoom it.
4137 for pane in &self.panes {
4138 if pane != &self.active_pane || dock_to_reveal.is_some() {
4139 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
4140 }
4141 }
4142
4143 // If another dock is zoomed, hide it.
4144 let mut focus_center = false;
4145 for dock in self.all_docks() {
4146 dock.update(cx, |dock, cx| {
4147 if Some(dock.position()) != dock_to_reveal
4148 && let Some(panel) = dock.active_panel()
4149 && panel.is_zoomed(window, cx)
4150 {
4151 focus_center |= panel.panel_focus_handle(cx).contains_focused(window, cx);
4152 dock.set_open(false, window, cx);
4153 }
4154 });
4155 }
4156
4157 if focus_center {
4158 self.active_pane
4159 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
4160 }
4161
4162 if self.zoomed_position != dock_to_reveal {
4163 self.zoomed = None;
4164 self.zoomed_position = None;
4165 cx.emit(Event::ZoomChanged);
4166 }
4167
4168 cx.notify();
4169 }
4170
4171 fn add_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
4172 let pane = cx.new(|cx| {
4173 let mut pane = Pane::new(
4174 self.weak_handle(),
4175 self.project.clone(),
4176 self.pane_history_timestamp.clone(),
4177 None,
4178 NewFile.boxed_clone(),
4179 true,
4180 window,
4181 cx,
4182 );
4183 pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
4184 pane
4185 });
4186 cx.subscribe_in(&pane, window, Self::handle_pane_event)
4187 .detach();
4188 self.panes.push(pane.clone());
4189
4190 window.focus(&pane.focus_handle(cx), cx);
4191
4192 cx.emit(Event::PaneAdded(pane.clone()));
4193 pane
4194 }
4195
4196 pub fn add_item_to_center(
4197 &mut self,
4198 item: Box<dyn ItemHandle>,
4199 window: &mut Window,
4200 cx: &mut Context<Self>,
4201 ) -> bool {
4202 if let Some(center_pane) = self.last_active_center_pane.clone() {
4203 if let Some(center_pane) = center_pane.upgrade() {
4204 center_pane.update(cx, |pane, cx| {
4205 pane.add_item(item, true, true, None, window, cx)
4206 });
4207 true
4208 } else {
4209 false
4210 }
4211 } else {
4212 false
4213 }
4214 }
4215
4216 pub fn add_item_to_active_pane(
4217 &mut self,
4218 item: Box<dyn ItemHandle>,
4219 destination_index: Option<usize>,
4220 focus_item: bool,
4221 window: &mut Window,
4222 cx: &mut App,
4223 ) {
4224 self.add_item(
4225 self.active_pane.clone(),
4226 item,
4227 destination_index,
4228 false,
4229 focus_item,
4230 window,
4231 cx,
4232 )
4233 }
4234
4235 pub fn add_item(
4236 &mut self,
4237 pane: Entity<Pane>,
4238 item: Box<dyn ItemHandle>,
4239 destination_index: Option<usize>,
4240 activate_pane: bool,
4241 focus_item: bool,
4242 window: &mut Window,
4243 cx: &mut App,
4244 ) {
4245 pane.update(cx, |pane, cx| {
4246 pane.add_item(
4247 item,
4248 activate_pane,
4249 focus_item,
4250 destination_index,
4251 window,
4252 cx,
4253 )
4254 });
4255 }
4256
4257 pub fn split_item(
4258 &mut self,
4259 split_direction: SplitDirection,
4260 item: Box<dyn ItemHandle>,
4261 window: &mut Window,
4262 cx: &mut Context<Self>,
4263 ) {
4264 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, window, cx);
4265 self.add_item(new_pane, item, None, true, true, window, cx);
4266 }
4267
4268 pub fn open_abs_path(
4269 &mut self,
4270 abs_path: PathBuf,
4271 options: OpenOptions,
4272 window: &mut Window,
4273 cx: &mut Context<Self>,
4274 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4275 cx.spawn_in(window, async move |workspace, cx| {
4276 let open_paths_task_result = workspace
4277 .update_in(cx, |workspace, window, cx| {
4278 workspace.open_paths(vec![abs_path.clone()], options, None, window, cx)
4279 })
4280 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
4281 .await;
4282 anyhow::ensure!(
4283 open_paths_task_result.len() == 1,
4284 "open abs path {abs_path:?} task returned incorrect number of results"
4285 );
4286 match open_paths_task_result
4287 .into_iter()
4288 .next()
4289 .expect("ensured single task result")
4290 {
4291 Some(open_result) => {
4292 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
4293 }
4294 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
4295 }
4296 })
4297 }
4298
4299 pub fn split_abs_path(
4300 &mut self,
4301 abs_path: PathBuf,
4302 visible: bool,
4303 window: &mut Window,
4304 cx: &mut Context<Self>,
4305 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4306 let project_path_task =
4307 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
4308 cx.spawn_in(window, async move |this, cx| {
4309 let (_, path) = project_path_task.await?;
4310 this.update_in(cx, |this, window, cx| this.split_path(path, window, cx))?
4311 .await
4312 })
4313 }
4314
4315 pub fn open_path(
4316 &mut self,
4317 path: impl Into<ProjectPath>,
4318 pane: Option<WeakEntity<Pane>>,
4319 focus_item: bool,
4320 window: &mut Window,
4321 cx: &mut App,
4322 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4323 self.open_path_preview(path, pane, focus_item, false, true, window, cx)
4324 }
4325
4326 pub fn open_path_preview(
4327 &mut self,
4328 path: impl Into<ProjectPath>,
4329 pane: Option<WeakEntity<Pane>>,
4330 focus_item: bool,
4331 allow_preview: bool,
4332 activate: bool,
4333 window: &mut Window,
4334 cx: &mut App,
4335 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4336 let pane = pane.unwrap_or_else(|| {
4337 self.last_active_center_pane.clone().unwrap_or_else(|| {
4338 self.panes
4339 .first()
4340 .expect("There must be an active pane")
4341 .downgrade()
4342 })
4343 });
4344
4345 let project_path = path.into();
4346 let task = self.load_path(project_path.clone(), window, cx);
4347 window.spawn(cx, async move |cx| {
4348 let (project_entry_id, build_item) = task.await?;
4349
4350 pane.update_in(cx, |pane, window, cx| {
4351 pane.open_item(
4352 project_entry_id,
4353 project_path,
4354 focus_item,
4355 allow_preview,
4356 activate,
4357 None,
4358 window,
4359 cx,
4360 build_item,
4361 )
4362 })
4363 })
4364 }
4365
4366 pub fn split_path(
4367 &mut self,
4368 path: impl Into<ProjectPath>,
4369 window: &mut Window,
4370 cx: &mut Context<Self>,
4371 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4372 self.split_path_preview(path, false, None, window, cx)
4373 }
4374
4375 pub fn split_path_preview(
4376 &mut self,
4377 path: impl Into<ProjectPath>,
4378 allow_preview: bool,
4379 split_direction: Option<SplitDirection>,
4380 window: &mut Window,
4381 cx: &mut Context<Self>,
4382 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4383 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
4384 self.panes
4385 .first()
4386 .expect("There must be an active pane")
4387 .downgrade()
4388 });
4389
4390 if let Member::Pane(center_pane) = &self.center.root
4391 && center_pane.read(cx).items_len() == 0
4392 {
4393 return self.open_path(path, Some(pane), true, window, cx);
4394 }
4395
4396 let project_path = path.into();
4397 let task = self.load_path(project_path.clone(), window, cx);
4398 cx.spawn_in(window, async move |this, cx| {
4399 let (project_entry_id, build_item) = task.await?;
4400 this.update_in(cx, move |this, window, cx| -> Option<_> {
4401 let pane = pane.upgrade()?;
4402 let new_pane = this.split_pane(
4403 pane,
4404 split_direction.unwrap_or(SplitDirection::Right),
4405 window,
4406 cx,
4407 );
4408 new_pane.update(cx, |new_pane, cx| {
4409 Some(new_pane.open_item(
4410 project_entry_id,
4411 project_path,
4412 true,
4413 allow_preview,
4414 true,
4415 None,
4416 window,
4417 cx,
4418 build_item,
4419 ))
4420 })
4421 })
4422 .map(|option| option.context("pane was dropped"))?
4423 })
4424 }
4425
4426 fn load_path(
4427 &mut self,
4428 path: ProjectPath,
4429 window: &mut Window,
4430 cx: &mut App,
4431 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
4432 let registry = cx.default_global::<ProjectItemRegistry>().clone();
4433 registry.open_path(self.project(), &path, window, cx)
4434 }
4435
4436 pub fn find_project_item<T>(
4437 &self,
4438 pane: &Entity<Pane>,
4439 project_item: &Entity<T::Item>,
4440 cx: &App,
4441 ) -> Option<Entity<T>>
4442 where
4443 T: ProjectItem,
4444 {
4445 use project::ProjectItem as _;
4446 let project_item = project_item.read(cx);
4447 let entry_id = project_item.entry_id(cx);
4448 let project_path = project_item.project_path(cx);
4449
4450 let mut item = None;
4451 if let Some(entry_id) = entry_id {
4452 item = pane.read(cx).item_for_entry(entry_id, cx);
4453 }
4454 if item.is_none()
4455 && let Some(project_path) = project_path
4456 {
4457 item = pane.read(cx).item_for_path(project_path, cx);
4458 }
4459
4460 item.and_then(|item| item.downcast::<T>())
4461 }
4462
4463 pub fn is_project_item_open<T>(
4464 &self,
4465 pane: &Entity<Pane>,
4466 project_item: &Entity<T::Item>,
4467 cx: &App,
4468 ) -> bool
4469 where
4470 T: ProjectItem,
4471 {
4472 self.find_project_item::<T>(pane, project_item, cx)
4473 .is_some()
4474 }
4475
4476 pub fn open_project_item<T>(
4477 &mut self,
4478 pane: Entity<Pane>,
4479 project_item: Entity<T::Item>,
4480 activate_pane: bool,
4481 focus_item: bool,
4482 keep_old_preview: bool,
4483 allow_new_preview: bool,
4484 window: &mut Window,
4485 cx: &mut Context<Self>,
4486 ) -> Entity<T>
4487 where
4488 T: ProjectItem,
4489 {
4490 let old_item_id = pane.read(cx).active_item().map(|item| item.item_id());
4491
4492 if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
4493 if !keep_old_preview
4494 && let Some(old_id) = old_item_id
4495 && old_id != item.item_id()
4496 {
4497 // switching to a different item, so unpreview old active item
4498 pane.update(cx, |pane, _| {
4499 pane.unpreview_item_if_preview(old_id);
4500 });
4501 }
4502
4503 self.activate_item(&item, activate_pane, focus_item, window, cx);
4504 if !allow_new_preview {
4505 pane.update(cx, |pane, _| {
4506 pane.unpreview_item_if_preview(item.item_id());
4507 });
4508 }
4509 return item;
4510 }
4511
4512 let item = pane.update(cx, |pane, cx| {
4513 cx.new(|cx| {
4514 T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx)
4515 })
4516 });
4517 let mut destination_index = None;
4518 pane.update(cx, |pane, cx| {
4519 if !keep_old_preview && let Some(old_id) = old_item_id {
4520 pane.unpreview_item_if_preview(old_id);
4521 }
4522 if allow_new_preview {
4523 destination_index = pane.replace_preview_item_id(item.item_id(), window, cx);
4524 }
4525 });
4526
4527 self.add_item(
4528 pane,
4529 Box::new(item.clone()),
4530 destination_index,
4531 activate_pane,
4532 focus_item,
4533 window,
4534 cx,
4535 );
4536 item
4537 }
4538
4539 pub fn open_shared_screen(
4540 &mut self,
4541 peer_id: PeerId,
4542 window: &mut Window,
4543 cx: &mut Context<Self>,
4544 ) {
4545 if let Some(shared_screen) =
4546 self.shared_screen_for_peer(peer_id, &self.active_pane, window, cx)
4547 {
4548 self.active_pane.update(cx, |pane, cx| {
4549 pane.add_item(Box::new(shared_screen), false, true, None, window, cx)
4550 });
4551 }
4552 }
4553
4554 pub fn activate_item(
4555 &mut self,
4556 item: &dyn ItemHandle,
4557 activate_pane: bool,
4558 focus_item: bool,
4559 window: &mut Window,
4560 cx: &mut App,
4561 ) -> bool {
4562 let result = self.panes.iter().find_map(|pane| {
4563 pane.read(cx)
4564 .index_for_item(item)
4565 .map(|ix| (pane.clone(), ix))
4566 });
4567 if let Some((pane, ix)) = result {
4568 pane.update(cx, |pane, cx| {
4569 pane.activate_item(ix, activate_pane, focus_item, window, cx)
4570 });
4571 true
4572 } else {
4573 false
4574 }
4575 }
4576
4577 fn activate_pane_at_index(
4578 &mut self,
4579 action: &ActivatePane,
4580 window: &mut Window,
4581 cx: &mut Context<Self>,
4582 ) {
4583 let panes = self.center.panes();
4584 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
4585 window.focus(&pane.focus_handle(cx), cx);
4586 } else {
4587 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx)
4588 .detach();
4589 }
4590 }
4591
4592 fn move_item_to_pane_at_index(
4593 &mut self,
4594 action: &MoveItemToPane,
4595 window: &mut Window,
4596 cx: &mut Context<Self>,
4597 ) {
4598 let panes = self.center.panes();
4599 let destination = match panes.get(action.destination) {
4600 Some(&destination) => destination.clone(),
4601 None => {
4602 if !action.clone && self.active_pane.read(cx).items_len() < 2 {
4603 return;
4604 }
4605 let direction = SplitDirection::Right;
4606 let split_off_pane = self
4607 .find_pane_in_direction(direction, cx)
4608 .unwrap_or_else(|| self.active_pane.clone());
4609 let new_pane = self.add_pane(window, cx);
4610 self.center.split(&split_off_pane, &new_pane, direction, cx);
4611 new_pane
4612 }
4613 };
4614
4615 if action.clone {
4616 if self
4617 .active_pane
4618 .read(cx)
4619 .active_item()
4620 .is_some_and(|item| item.can_split(cx))
4621 {
4622 clone_active_item(
4623 self.database_id(),
4624 &self.active_pane,
4625 &destination,
4626 action.focus,
4627 window,
4628 cx,
4629 );
4630 return;
4631 }
4632 }
4633 move_active_item(
4634 &self.active_pane,
4635 &destination,
4636 action.focus,
4637 true,
4638 window,
4639 cx,
4640 )
4641 }
4642
4643 pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) {
4644 let panes = self.center.panes();
4645 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
4646 let next_ix = (ix + 1) % panes.len();
4647 let next_pane = panes[next_ix].clone();
4648 window.focus(&next_pane.focus_handle(cx), cx);
4649 }
4650 }
4651
4652 pub fn activate_previous_pane(&mut self, window: &mut Window, cx: &mut App) {
4653 let panes = self.center.panes();
4654 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
4655 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
4656 let prev_pane = panes[prev_ix].clone();
4657 window.focus(&prev_pane.focus_handle(cx), cx);
4658 }
4659 }
4660
4661 pub fn activate_last_pane(&mut self, window: &mut Window, cx: &mut App) {
4662 let last_pane = self.center.last_pane();
4663 window.focus(&last_pane.focus_handle(cx), cx);
4664 }
4665
4666 pub fn activate_pane_in_direction(
4667 &mut self,
4668 direction: SplitDirection,
4669 window: &mut Window,
4670 cx: &mut App,
4671 ) {
4672 use ActivateInDirectionTarget as Target;
4673 enum Origin {
4674 Sidebar,
4675 LeftDock,
4676 RightDock,
4677 BottomDock,
4678 Center,
4679 }
4680
4681 let origin: Origin = if self
4682 .sidebar_focus_handle
4683 .as_ref()
4684 .is_some_and(|h| h.contains_focused(window, cx))
4685 {
4686 Origin::Sidebar
4687 } else {
4688 [
4689 (&self.left_dock, Origin::LeftDock),
4690 (&self.right_dock, Origin::RightDock),
4691 (&self.bottom_dock, Origin::BottomDock),
4692 ]
4693 .into_iter()
4694 .find_map(|(dock, origin)| {
4695 if dock.focus_handle(cx).contains_focused(window, cx) && dock.read(cx).is_open() {
4696 Some(origin)
4697 } else {
4698 None
4699 }
4700 })
4701 .unwrap_or(Origin::Center)
4702 };
4703
4704 let get_last_active_pane = || {
4705 let pane = self
4706 .last_active_center_pane
4707 .clone()
4708 .unwrap_or_else(|| {
4709 self.panes
4710 .first()
4711 .expect("There must be an active pane")
4712 .downgrade()
4713 })
4714 .upgrade()?;
4715 (pane.read(cx).items_len() != 0).then_some(pane)
4716 };
4717
4718 let try_dock =
4719 |dock: &Entity<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
4720
4721 let sidebar_target = self
4722 .sidebar_focus_handle
4723 .as_ref()
4724 .map(|h| Target::Sidebar(h.clone()));
4725
4726 let sidebar_on_left = self
4727 .multi_workspace
4728 .as_ref()
4729 .and_then(|mw| mw.upgrade())
4730 .map_or(true, |mw| mw.read(cx).sidebar_side(cx) == SidebarSide::Left);
4731
4732 let sidebar_on_side = |side: SplitDirection| -> Option<ActivateInDirectionTarget> {
4733 if (side == SplitDirection::Left) == sidebar_on_left {
4734 sidebar_target.clone()
4735 } else {
4736 None
4737 }
4738 };
4739
4740 let target = match (origin, direction) {
4741 // From the sidebar, only the inward direction navigates into
4742 // the workspace. Which direction is "inward" depends on which
4743 // side the sidebar is on.
4744 (Origin::Sidebar, SplitDirection::Right) if sidebar_on_left => {
4745 try_dock(&self.left_dock)
4746 .or_else(|| get_last_active_pane().map(Target::Pane))
4747 .or_else(|| try_dock(&self.bottom_dock))
4748 .or_else(|| try_dock(&self.right_dock))
4749 }
4750 (Origin::Sidebar, SplitDirection::Left) if !sidebar_on_left => {
4751 try_dock(&self.right_dock)
4752 .or_else(|| get_last_active_pane().map(Target::Pane))
4753 .or_else(|| try_dock(&self.bottom_dock))
4754 .or_else(|| try_dock(&self.left_dock))
4755 }
4756
4757 (Origin::Sidebar, _) => None,
4758
4759 // We're in the center, so we first try to go to a different pane,
4760 // otherwise try to go to a dock.
4761 (Origin::Center, direction) => {
4762 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
4763 Some(Target::Pane(pane))
4764 } else {
4765 match direction {
4766 SplitDirection::Up => None,
4767 SplitDirection::Down => try_dock(&self.bottom_dock),
4768 SplitDirection::Left => {
4769 try_dock(&self.left_dock).or_else(|| sidebar_on_side(direction))
4770 }
4771 SplitDirection::Right => {
4772 try_dock(&self.right_dock).or_else(|| sidebar_on_side(direction))
4773 }
4774 }
4775 }
4776 }
4777
4778 (Origin::LeftDock, SplitDirection::Right) => {
4779 if let Some(last_active_pane) = get_last_active_pane() {
4780 Some(Target::Pane(last_active_pane))
4781 } else {
4782 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
4783 }
4784 }
4785
4786 (Origin::LeftDock, SplitDirection::Left) => sidebar_on_side(SplitDirection::Left),
4787
4788 (Origin::LeftDock, SplitDirection::Down)
4789 | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
4790
4791 (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
4792 (Origin::BottomDock, SplitDirection::Left) => {
4793 try_dock(&self.left_dock).or_else(|| sidebar_on_side(SplitDirection::Left))
4794 }
4795 (Origin::BottomDock, SplitDirection::Right) => {
4796 try_dock(&self.right_dock).or_else(|| sidebar_on_side(SplitDirection::Right))
4797 }
4798
4799 (Origin::RightDock, SplitDirection::Left) => {
4800 if let Some(last_active_pane) = get_last_active_pane() {
4801 Some(Target::Pane(last_active_pane))
4802 } else {
4803 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
4804 }
4805 }
4806
4807 (Origin::RightDock, SplitDirection::Right) => sidebar_on_side(SplitDirection::Right),
4808
4809 _ => None,
4810 };
4811
4812 match target {
4813 Some(ActivateInDirectionTarget::Pane(pane)) => {
4814 let pane = pane.read(cx);
4815 if let Some(item) = pane.active_item() {
4816 item.item_focus_handle(cx).focus(window, cx);
4817 } else {
4818 log::error!(
4819 "Could not find a focus target when in switching focus in {direction} direction for a pane",
4820 );
4821 }
4822 }
4823 Some(ActivateInDirectionTarget::Dock(dock)) => {
4824 // Defer this to avoid a panic when the dock's active panel is already on the stack.
4825 window.defer(cx, move |window, cx| {
4826 let dock = dock.read(cx);
4827 if let Some(panel) = dock.active_panel() {
4828 panel.panel_focus_handle(cx).focus(window, cx);
4829 } else {
4830 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
4831 }
4832 })
4833 }
4834 Some(ActivateInDirectionTarget::Sidebar(focus_handle)) => {
4835 focus_handle.focus(window, cx);
4836 }
4837 None => {}
4838 }
4839 }
4840
4841 pub fn move_item_to_pane_in_direction(
4842 &mut self,
4843 action: &MoveItemToPaneInDirection,
4844 window: &mut Window,
4845 cx: &mut Context<Self>,
4846 ) {
4847 let destination = match self.find_pane_in_direction(action.direction, cx) {
4848 Some(destination) => destination,
4849 None => {
4850 if !action.clone && self.active_pane.read(cx).items_len() < 2 {
4851 return;
4852 }
4853 let new_pane = self.add_pane(window, cx);
4854 self.center
4855 .split(&self.active_pane, &new_pane, action.direction, cx);
4856 new_pane
4857 }
4858 };
4859
4860 if action.clone {
4861 if self
4862 .active_pane
4863 .read(cx)
4864 .active_item()
4865 .is_some_and(|item| item.can_split(cx))
4866 {
4867 clone_active_item(
4868 self.database_id(),
4869 &self.active_pane,
4870 &destination,
4871 action.focus,
4872 window,
4873 cx,
4874 );
4875 return;
4876 }
4877 }
4878 move_active_item(
4879 &self.active_pane,
4880 &destination,
4881 action.focus,
4882 true,
4883 window,
4884 cx,
4885 );
4886 }
4887
4888 pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
4889 self.center.bounding_box_for_pane(pane)
4890 }
4891
4892 pub fn find_pane_in_direction(
4893 &mut self,
4894 direction: SplitDirection,
4895 cx: &App,
4896 ) -> Option<Entity<Pane>> {
4897 self.center
4898 .find_pane_in_direction(&self.active_pane, direction, cx)
4899 .cloned()
4900 }
4901
4902 pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
4903 if let Some(to) = self.find_pane_in_direction(direction, cx) {
4904 self.center.swap(&self.active_pane, &to, cx);
4905 cx.notify();
4906 }
4907 }
4908
4909 pub fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
4910 if self
4911 .center
4912 .move_to_border(&self.active_pane, direction, cx)
4913 .unwrap()
4914 {
4915 cx.notify();
4916 }
4917 }
4918
4919 pub fn resize_pane(
4920 &mut self,
4921 axis: gpui::Axis,
4922 amount: Pixels,
4923 window: &mut Window,
4924 cx: &mut Context<Self>,
4925 ) {
4926 let docks = self.all_docks();
4927 let active_dock = docks
4928 .into_iter()
4929 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
4930
4931 if let Some(dock_entity) = active_dock {
4932 let dock = dock_entity.read(cx);
4933 let Some(panel_size) = self.dock_size(&dock, window, cx) else {
4934 return;
4935 };
4936 match dock.position() {
4937 DockPosition::Left => self.resize_left_dock(panel_size + amount, window, cx),
4938 DockPosition::Bottom => self.resize_bottom_dock(panel_size + amount, window, cx),
4939 DockPosition::Right => self.resize_right_dock(panel_size + amount, window, cx),
4940 }
4941 } else {
4942 self.center
4943 .resize(&self.active_pane, axis, amount, &self.bounds, cx);
4944 }
4945 cx.notify();
4946 }
4947
4948 pub fn reset_pane_sizes(&mut self, cx: &mut Context<Self>) {
4949 self.center.reset_pane_sizes(cx);
4950 cx.notify();
4951 }
4952
4953 fn handle_pane_focused(
4954 &mut self,
4955 pane: Entity<Pane>,
4956 window: &mut Window,
4957 cx: &mut Context<Self>,
4958 ) {
4959 // This is explicitly hoisted out of the following check for pane identity as
4960 // terminal panel panes are not registered as a center panes.
4961 self.status_bar.update(cx, |status_bar, cx| {
4962 status_bar.set_active_pane(&pane, window, cx);
4963 });
4964 if self.active_pane != pane {
4965 self.set_active_pane(&pane, window, cx);
4966 }
4967
4968 if self.last_active_center_pane.is_none() {
4969 self.last_active_center_pane = Some(pane.downgrade());
4970 }
4971
4972 // If this pane is in a dock, preserve that dock when dismissing zoomed items.
4973 // This prevents the dock from closing when focus events fire during window activation.
4974 // We also preserve any dock whose active panel itself has focus — this covers
4975 // panels like AgentPanel that don't implement `pane()` but can still be zoomed.
4976 let dock_to_preserve = self.all_docks().iter().find_map(|dock| {
4977 let dock_read = dock.read(cx);
4978 if let Some(panel) = dock_read.active_panel() {
4979 if panel.pane(cx).is_some_and(|dock_pane| dock_pane == pane)
4980 || panel.panel_focus_handle(cx).contains_focused(window, cx)
4981 {
4982 return Some(dock_read.position());
4983 }
4984 }
4985 None
4986 });
4987
4988 self.dismiss_zoomed_items_to_reveal(dock_to_preserve, window, cx);
4989 if pane.read(cx).is_zoomed() {
4990 self.zoomed = Some(pane.downgrade().into());
4991 } else {
4992 self.zoomed = None;
4993 }
4994 self.zoomed_position = None;
4995 cx.emit(Event::ZoomChanged);
4996 self.update_active_view_for_followers(window, cx);
4997 pane.update(cx, |pane, _| {
4998 pane.track_alternate_file_items();
4999 });
5000
5001 cx.notify();
5002 }
5003
5004 fn set_active_pane(
5005 &mut self,
5006 pane: &Entity<Pane>,
5007 window: &mut Window,
5008 cx: &mut Context<Self>,
5009 ) {
5010 self.active_pane = pane.clone();
5011 self.active_item_path_changed(true, window, cx);
5012 self.last_active_center_pane = Some(pane.downgrade());
5013 }
5014
5015 fn handle_panel_focused(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5016 self.update_active_view_for_followers(window, cx);
5017 }
5018
5019 fn handle_pane_event(
5020 &mut self,
5021 pane: &Entity<Pane>,
5022 event: &pane::Event,
5023 window: &mut Window,
5024 cx: &mut Context<Self>,
5025 ) {
5026 let mut serialize_workspace = true;
5027 match event {
5028 pane::Event::AddItem { item } => {
5029 item.added_to_pane(self, pane.clone(), window, cx);
5030 cx.emit(Event::ItemAdded {
5031 item: item.boxed_clone(),
5032 });
5033 }
5034 pane::Event::Split { direction, mode } => {
5035 match mode {
5036 SplitMode::ClonePane => {
5037 self.split_and_clone(pane.clone(), *direction, window, cx)
5038 .detach();
5039 }
5040 SplitMode::EmptyPane => {
5041 self.split_pane(pane.clone(), *direction, window, cx);
5042 }
5043 SplitMode::MovePane => {
5044 self.split_and_move(pane.clone(), *direction, window, cx);
5045 }
5046 };
5047 }
5048 pane::Event::JoinIntoNext => {
5049 self.join_pane_into_next(pane.clone(), window, cx);
5050 }
5051 pane::Event::JoinAll => {
5052 self.join_all_panes(window, cx);
5053 }
5054 pane::Event::Remove { focus_on_pane } => {
5055 self.remove_pane(pane.clone(), focus_on_pane.clone(), window, cx);
5056 }
5057 pane::Event::ActivateItem {
5058 local,
5059 focus_changed,
5060 } => {
5061 window.invalidate_character_coordinates();
5062
5063 pane.update(cx, |pane, _| {
5064 pane.track_alternate_file_items();
5065 });
5066 if *local {
5067 self.unfollow_in_pane(pane, window, cx);
5068 }
5069 serialize_workspace = *focus_changed || pane != self.active_pane();
5070 if pane == self.active_pane() {
5071 self.active_item_path_changed(*focus_changed, window, cx);
5072 self.update_active_view_for_followers(window, cx);
5073 } else if *local {
5074 self.set_active_pane(pane, window, cx);
5075 }
5076 }
5077 pane::Event::UserSavedItem { item, save_intent } => {
5078 cx.emit(Event::UserSavedItem {
5079 pane: pane.downgrade(),
5080 item: item.boxed_clone(),
5081 save_intent: *save_intent,
5082 });
5083 serialize_workspace = false;
5084 }
5085 pane::Event::ChangeItemTitle => {
5086 if *pane == self.active_pane {
5087 self.active_item_path_changed(false, window, cx);
5088 }
5089 serialize_workspace = false;
5090 }
5091 pane::Event::RemovedItem { item } => {
5092 cx.emit(Event::ActiveItemChanged);
5093 self.update_window_edited(window, cx);
5094 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id())
5095 && entry.get().entity_id() == pane.entity_id()
5096 {
5097 entry.remove();
5098 }
5099 cx.emit(Event::ItemRemoved {
5100 item_id: item.item_id(),
5101 });
5102 }
5103 pane::Event::Focus => {
5104 window.invalidate_character_coordinates();
5105 self.handle_pane_focused(pane.clone(), window, cx);
5106 }
5107 pane::Event::ZoomIn => {
5108 if *pane == self.active_pane {
5109 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
5110 if pane.read(cx).has_focus(window, cx) {
5111 self.zoomed = Some(pane.downgrade().into());
5112 self.zoomed_position = None;
5113 cx.emit(Event::ZoomChanged);
5114 }
5115 cx.notify();
5116 }
5117 }
5118 pane::Event::ZoomOut => {
5119 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
5120 if self.zoomed_position.is_none() {
5121 self.zoomed = None;
5122 cx.emit(Event::ZoomChanged);
5123 }
5124 cx.notify();
5125 }
5126 pane::Event::ItemPinned | pane::Event::ItemUnpinned => {}
5127 }
5128
5129 if serialize_workspace {
5130 self.serialize_workspace(window, cx);
5131 }
5132 }
5133
5134 pub fn unfollow_in_pane(
5135 &mut self,
5136 pane: &Entity<Pane>,
5137 window: &mut Window,
5138 cx: &mut Context<Workspace>,
5139 ) -> Option<CollaboratorId> {
5140 let leader_id = self.leader_for_pane(pane)?;
5141 self.unfollow(leader_id, window, cx);
5142 Some(leader_id)
5143 }
5144
5145 pub fn split_pane(
5146 &mut self,
5147 pane_to_split: Entity<Pane>,
5148 split_direction: SplitDirection,
5149 window: &mut Window,
5150 cx: &mut Context<Self>,
5151 ) -> Entity<Pane> {
5152 let new_pane = self.add_pane(window, cx);
5153 self.center
5154 .split(&pane_to_split, &new_pane, split_direction, cx);
5155 cx.notify();
5156 new_pane
5157 }
5158
5159 pub fn split_and_move(
5160 &mut self,
5161 pane: Entity<Pane>,
5162 direction: SplitDirection,
5163 window: &mut Window,
5164 cx: &mut Context<Self>,
5165 ) {
5166 let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx)) else {
5167 return;
5168 };
5169 let new_pane = self.add_pane(window, cx);
5170 new_pane.update(cx, |pane, cx| {
5171 pane.add_item(item, true, true, None, window, cx)
5172 });
5173 self.center.split(&pane, &new_pane, direction, cx);
5174 cx.notify();
5175 }
5176
5177 pub fn split_and_clone(
5178 &mut self,
5179 pane: Entity<Pane>,
5180 direction: SplitDirection,
5181 window: &mut Window,
5182 cx: &mut Context<Self>,
5183 ) -> Task<Option<Entity<Pane>>> {
5184 let Some(item) = pane.read(cx).active_item() else {
5185 return Task::ready(None);
5186 };
5187 if !item.can_split(cx) {
5188 return Task::ready(None);
5189 }
5190 let task = item.clone_on_split(self.database_id(), window, cx);
5191 cx.spawn_in(window, async move |this, cx| {
5192 if let Some(clone) = task.await {
5193 this.update_in(cx, |this, window, cx| {
5194 let new_pane = this.add_pane(window, cx);
5195 let nav_history = pane.read(cx).fork_nav_history();
5196 new_pane.update(cx, |pane, cx| {
5197 pane.set_nav_history(nav_history, cx);
5198 pane.add_item(clone, true, true, None, window, cx)
5199 });
5200 this.center.split(&pane, &new_pane, direction, cx);
5201 cx.notify();
5202 new_pane
5203 })
5204 .ok()
5205 } else {
5206 None
5207 }
5208 })
5209 }
5210
5211 pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5212 let active_item = self.active_pane.read(cx).active_item();
5213 for pane in &self.panes {
5214 join_pane_into_active(&self.active_pane, pane, window, cx);
5215 }
5216 if let Some(active_item) = active_item {
5217 self.activate_item(active_item.as_ref(), true, true, window, cx);
5218 }
5219 cx.notify();
5220 }
5221
5222 pub fn join_pane_into_next(
5223 &mut self,
5224 pane: Entity<Pane>,
5225 window: &mut Window,
5226 cx: &mut Context<Self>,
5227 ) {
5228 let next_pane = self
5229 .find_pane_in_direction(SplitDirection::Right, cx)
5230 .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
5231 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
5232 .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
5233 let Some(next_pane) = next_pane else {
5234 return;
5235 };
5236 move_all_items(&pane, &next_pane, window, cx);
5237 cx.notify();
5238 }
5239
5240 fn remove_pane(
5241 &mut self,
5242 pane: Entity<Pane>,
5243 focus_on: Option<Entity<Pane>>,
5244 window: &mut Window,
5245 cx: &mut Context<Self>,
5246 ) {
5247 if self.center.remove(&pane, cx).unwrap() {
5248 self.force_remove_pane(&pane, &focus_on, window, cx);
5249 self.unfollow_in_pane(&pane, window, cx);
5250 self.last_leaders_by_pane.remove(&pane.downgrade());
5251 for removed_item in pane.read(cx).items() {
5252 self.panes_by_item.remove(&removed_item.item_id());
5253 }
5254
5255 cx.notify();
5256 } else {
5257 self.active_item_path_changed(true, window, cx);
5258 }
5259 cx.emit(Event::PaneRemoved);
5260 }
5261
5262 pub fn panes_mut(&mut self) -> &mut [Entity<Pane>] {
5263 &mut self.panes
5264 }
5265
5266 pub fn panes(&self) -> &[Entity<Pane>] {
5267 &self.panes
5268 }
5269
5270 pub fn active_pane(&self) -> &Entity<Pane> {
5271 &self.active_pane
5272 }
5273
5274 pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> {
5275 for dock in self.all_docks() {
5276 if dock.focus_handle(cx).contains_focused(window, cx)
5277 && let Some(pane) = dock
5278 .read(cx)
5279 .active_panel()
5280 .and_then(|panel| panel.pane(cx))
5281 {
5282 return pane;
5283 }
5284 }
5285 self.active_pane().clone()
5286 }
5287
5288 pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
5289 self.find_pane_in_direction(SplitDirection::Right, cx)
5290 .unwrap_or_else(|| {
5291 self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx)
5292 })
5293 }
5294
5295 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> {
5296 self.pane_for_item_id(handle.item_id())
5297 }
5298
5299 pub fn pane_for_item_id(&self, item_id: EntityId) -> Option<Entity<Pane>> {
5300 let weak_pane = self.panes_by_item.get(&item_id)?;
5301 weak_pane.upgrade()
5302 }
5303
5304 pub fn pane_for_entity_id(&self, entity_id: EntityId) -> Option<Entity<Pane>> {
5305 self.panes
5306 .iter()
5307 .find(|pane| pane.entity_id() == entity_id)
5308 .cloned()
5309 }
5310
5311 fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
5312 self.follower_states.retain(|leader_id, state| {
5313 if *leader_id == CollaboratorId::PeerId(peer_id) {
5314 for item in state.items_by_leader_view_id.values() {
5315 item.view.set_leader_id(None, window, cx);
5316 }
5317 false
5318 } else {
5319 true
5320 }
5321 });
5322 cx.notify();
5323 }
5324
5325 pub fn start_following(
5326 &mut self,
5327 leader_id: impl Into<CollaboratorId>,
5328 window: &mut Window,
5329 cx: &mut Context<Self>,
5330 ) -> Option<Task<Result<()>>> {
5331 let leader_id = leader_id.into();
5332 let pane = self.active_pane().clone();
5333
5334 self.last_leaders_by_pane
5335 .insert(pane.downgrade(), leader_id);
5336 self.unfollow(leader_id, window, cx);
5337 self.unfollow_in_pane(&pane, window, cx);
5338 self.follower_states.insert(
5339 leader_id,
5340 FollowerState {
5341 center_pane: pane.clone(),
5342 dock_pane: None,
5343 active_view_id: None,
5344 items_by_leader_view_id: Default::default(),
5345 },
5346 );
5347 cx.notify();
5348
5349 match leader_id {
5350 CollaboratorId::PeerId(leader_peer_id) => {
5351 let room_id = self.active_call()?.room_id(cx)?;
5352 let project_id = self.project.read(cx).remote_id();
5353 let request = self.app_state.client.request(proto::Follow {
5354 room_id,
5355 project_id,
5356 leader_id: Some(leader_peer_id),
5357 });
5358
5359 Some(cx.spawn_in(window, async move |this, cx| {
5360 let response = request.await?;
5361 this.update(cx, |this, _| {
5362 let state = this
5363 .follower_states
5364 .get_mut(&leader_id)
5365 .context("following interrupted")?;
5366 state.active_view_id = response
5367 .active_view
5368 .as_ref()
5369 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
5370 anyhow::Ok(())
5371 })??;
5372 if let Some(view) = response.active_view {
5373 Self::add_view_from_leader(this.clone(), leader_peer_id, &view, cx).await?;
5374 }
5375 this.update_in(cx, |this, window, cx| {
5376 this.leader_updated(leader_id, window, cx)
5377 })?;
5378 Ok(())
5379 }))
5380 }
5381 CollaboratorId::Agent => {
5382 self.leader_updated(leader_id, window, cx)?;
5383 Some(Task::ready(Ok(())))
5384 }
5385 }
5386 }
5387
5388 pub fn follow_next_collaborator(
5389 &mut self,
5390 _: &FollowNextCollaborator,
5391 window: &mut Window,
5392 cx: &mut Context<Self>,
5393 ) {
5394 let collaborators = self.project.read(cx).collaborators();
5395 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
5396 let mut collaborators = collaborators.keys().copied();
5397 for peer_id in collaborators.by_ref() {
5398 if CollaboratorId::PeerId(peer_id) == leader_id {
5399 break;
5400 }
5401 }
5402 collaborators.next().map(CollaboratorId::PeerId)
5403 } else if let Some(last_leader_id) =
5404 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
5405 {
5406 match last_leader_id {
5407 CollaboratorId::PeerId(peer_id) => {
5408 if collaborators.contains_key(peer_id) {
5409 Some(*last_leader_id)
5410 } else {
5411 None
5412 }
5413 }
5414 CollaboratorId::Agent => Some(CollaboratorId::Agent),
5415 }
5416 } else {
5417 None
5418 };
5419
5420 let pane = self.active_pane.clone();
5421 let Some(leader_id) = next_leader_id.or_else(|| {
5422 Some(CollaboratorId::PeerId(
5423 collaborators.keys().copied().next()?,
5424 ))
5425 }) else {
5426 return;
5427 };
5428 if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
5429 return;
5430 }
5431 if let Some(task) = self.start_following(leader_id, window, cx) {
5432 task.detach_and_log_err(cx)
5433 }
5434 }
5435
5436 pub fn follow(
5437 &mut self,
5438 leader_id: impl Into<CollaboratorId>,
5439 window: &mut Window,
5440 cx: &mut Context<Self>,
5441 ) {
5442 let leader_id = leader_id.into();
5443
5444 if let CollaboratorId::PeerId(peer_id) = leader_id {
5445 let Some(active_call) = GlobalAnyActiveCall::try_global(cx) else {
5446 return;
5447 };
5448 let Some(remote_participant) =
5449 active_call.0.remote_participant_for_peer_id(peer_id, cx)
5450 else {
5451 return;
5452 };
5453
5454 let project = self.project.read(cx);
5455
5456 let other_project_id = match remote_participant.location {
5457 ParticipantLocation::External => None,
5458 ParticipantLocation::UnsharedProject => None,
5459 ParticipantLocation::SharedProject { project_id } => {
5460 if Some(project_id) == project.remote_id() {
5461 None
5462 } else {
5463 Some(project_id)
5464 }
5465 }
5466 };
5467
5468 // if they are active in another project, follow there.
5469 if let Some(project_id) = other_project_id {
5470 let app_state = self.app_state.clone();
5471 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
5472 .detach_and_log_err(cx);
5473 }
5474 }
5475
5476 // if you're already following, find the right pane and focus it.
5477 if let Some(follower_state) = self.follower_states.get(&leader_id) {
5478 window.focus(&follower_state.pane().focus_handle(cx), cx);
5479
5480 return;
5481 }
5482
5483 // Otherwise, follow.
5484 if let Some(task) = self.start_following(leader_id, window, cx) {
5485 task.detach_and_log_err(cx)
5486 }
5487 }
5488
5489 pub fn unfollow(
5490 &mut self,
5491 leader_id: impl Into<CollaboratorId>,
5492 window: &mut Window,
5493 cx: &mut Context<Self>,
5494 ) -> Option<()> {
5495 cx.notify();
5496
5497 let leader_id = leader_id.into();
5498 let state = self.follower_states.remove(&leader_id)?;
5499 for (_, item) in state.items_by_leader_view_id {
5500 item.view.set_leader_id(None, window, cx);
5501 }
5502
5503 if let CollaboratorId::PeerId(leader_peer_id) = leader_id {
5504 let project_id = self.project.read(cx).remote_id();
5505 let room_id = self.active_call()?.room_id(cx)?;
5506 self.app_state
5507 .client
5508 .send(proto::Unfollow {
5509 room_id,
5510 project_id,
5511 leader_id: Some(leader_peer_id),
5512 })
5513 .log_err();
5514 }
5515
5516 Some(())
5517 }
5518
5519 pub fn is_being_followed(&self, id: impl Into<CollaboratorId>) -> bool {
5520 self.follower_states.contains_key(&id.into())
5521 }
5522
5523 fn active_item_path_changed(
5524 &mut self,
5525 focus_changed: bool,
5526 window: &mut Window,
5527 cx: &mut Context<Self>,
5528 ) {
5529 cx.emit(Event::ActiveItemChanged);
5530 let active_entry = self.active_project_path(cx);
5531 self.project.update(cx, |project, cx| {
5532 project.set_active_path(active_entry.clone(), cx)
5533 });
5534
5535 if focus_changed && let Some(project_path) = &active_entry {
5536 let git_store_entity = self.project.read(cx).git_store().clone();
5537 git_store_entity.update(cx, |git_store, cx| {
5538 git_store.set_active_repo_for_path(project_path, cx);
5539 });
5540 }
5541
5542 self.update_window_title(window, cx);
5543 }
5544
5545 fn update_window_title(&mut self, window: &mut Window, cx: &mut App) {
5546 let project = self.project().read(cx);
5547 let mut title = String::new();
5548
5549 for (i, worktree) in project.visible_worktrees(cx).enumerate() {
5550 let name = {
5551 let settings_location = SettingsLocation {
5552 worktree_id: worktree.read(cx).id(),
5553 path: RelPath::empty(),
5554 };
5555
5556 let settings = WorktreeSettings::get(Some(settings_location), cx);
5557 match &settings.project_name {
5558 Some(name) => name.as_str(),
5559 None => worktree.read(cx).root_name_str(),
5560 }
5561 };
5562 if i > 0 {
5563 title.push_str(", ");
5564 }
5565 title.push_str(name);
5566 }
5567
5568 if title.is_empty() {
5569 title = "empty project".to_string();
5570 }
5571
5572 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
5573 let filename = path.path.file_name().or_else(|| {
5574 Some(
5575 project
5576 .worktree_for_id(path.worktree_id, cx)?
5577 .read(cx)
5578 .root_name_str(),
5579 )
5580 });
5581
5582 if let Some(filename) = filename {
5583 title.push_str(" — ");
5584 title.push_str(filename.as_ref());
5585 }
5586 }
5587
5588 if project.is_via_collab() {
5589 title.push_str(" ↙");
5590 } else if project.is_shared() {
5591 title.push_str(" ↗");
5592 }
5593
5594 if let Some(last_title) = self.last_window_title.as_ref()
5595 && &title == last_title
5596 {
5597 return;
5598 }
5599 window.set_window_title(&title);
5600 SystemWindowTabController::update_tab_title(
5601 cx,
5602 window.window_handle().window_id(),
5603 SharedString::from(&title),
5604 );
5605 self.last_window_title = Some(title);
5606 }
5607
5608 fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
5609 let is_edited = !self.project.read(cx).is_disconnected(cx) && !self.dirty_items.is_empty();
5610 if is_edited != self.window_edited {
5611 self.window_edited = is_edited;
5612 window.set_window_edited(self.window_edited)
5613 }
5614 }
5615
5616 fn update_item_dirty_state(
5617 &mut self,
5618 item: &dyn ItemHandle,
5619 window: &mut Window,
5620 cx: &mut App,
5621 ) {
5622 let is_dirty = item.is_dirty(cx);
5623 let item_id = item.item_id();
5624 let was_dirty = self.dirty_items.contains_key(&item_id);
5625 if is_dirty == was_dirty {
5626 return;
5627 }
5628 if was_dirty {
5629 self.dirty_items.remove(&item_id);
5630 self.update_window_edited(window, cx);
5631 return;
5632 }
5633
5634 let workspace = self.weak_handle();
5635 let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
5636 return;
5637 };
5638 let on_release_callback = Box::new(move |cx: &mut App| {
5639 window_handle
5640 .update(cx, |_, window, cx| {
5641 workspace
5642 .update(cx, |workspace, cx| {
5643 workspace.dirty_items.remove(&item_id);
5644 workspace.update_window_edited(window, cx)
5645 })
5646 .ok();
5647 })
5648 .ok();
5649 });
5650
5651 let s = item.on_release(cx, on_release_callback);
5652 self.dirty_items.insert(item_id, s);
5653 self.update_window_edited(window, cx);
5654 }
5655
5656 fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
5657 if self.notifications.is_empty() {
5658 None
5659 } else {
5660 Some(
5661 div()
5662 .absolute()
5663 .right_3()
5664 .bottom_3()
5665 .w_112()
5666 .h_full()
5667 .flex()
5668 .flex_col()
5669 .justify_end()
5670 .gap_2()
5671 .children(
5672 self.notifications
5673 .iter()
5674 .map(|(_, notification)| notification.clone().into_any()),
5675 ),
5676 )
5677 }
5678 }
5679
5680 // RPC handlers
5681
5682 fn active_view_for_follower(
5683 &self,
5684 follower_project_id: Option<u64>,
5685 window: &mut Window,
5686 cx: &mut Context<Self>,
5687 ) -> Option<proto::View> {
5688 let (item, panel_id) = self.active_item_for_followers(window, cx);
5689 let item = item?;
5690 let leader_id = self
5691 .pane_for(&*item)
5692 .and_then(|pane| self.leader_for_pane(&pane));
5693 let leader_peer_id = match leader_id {
5694 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
5695 Some(CollaboratorId::Agent) | None => None,
5696 };
5697
5698 let item_handle = item.to_followable_item_handle(cx)?;
5699 let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
5700 let variant = item_handle.to_state_proto(window, cx)?;
5701
5702 if item_handle.is_project_item(window, cx)
5703 && (follower_project_id.is_none()
5704 || follower_project_id != self.project.read(cx).remote_id())
5705 {
5706 return None;
5707 }
5708
5709 Some(proto::View {
5710 id: id.to_proto(),
5711 leader_id: leader_peer_id,
5712 variant: Some(variant),
5713 panel_id: panel_id.map(|id| id as i32),
5714 })
5715 }
5716
5717 fn handle_follow(
5718 &mut self,
5719 follower_project_id: Option<u64>,
5720 window: &mut Window,
5721 cx: &mut Context<Self>,
5722 ) -> proto::FollowResponse {
5723 let active_view = self.active_view_for_follower(follower_project_id, window, cx);
5724
5725 cx.notify();
5726 proto::FollowResponse {
5727 views: active_view.iter().cloned().collect(),
5728 active_view,
5729 }
5730 }
5731
5732 fn handle_update_followers(
5733 &mut self,
5734 leader_id: PeerId,
5735 message: proto::UpdateFollowers,
5736 _window: &mut Window,
5737 _cx: &mut Context<Self>,
5738 ) {
5739 self.leader_updates_tx
5740 .unbounded_send((leader_id, message))
5741 .ok();
5742 }
5743
5744 async fn process_leader_update(
5745 this: &WeakEntity<Self>,
5746 leader_id: PeerId,
5747 update: proto::UpdateFollowers,
5748 cx: &mut AsyncWindowContext,
5749 ) -> Result<()> {
5750 match update.variant.context("invalid update")? {
5751 proto::update_followers::Variant::CreateView(view) => {
5752 let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
5753 let should_add_view = this.update(cx, |this, _| {
5754 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
5755 anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
5756 } else {
5757 anyhow::Ok(false)
5758 }
5759 })??;
5760
5761 if should_add_view {
5762 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
5763 }
5764 }
5765 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
5766 let should_add_view = this.update(cx, |this, _| {
5767 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
5768 state.active_view_id = update_active_view
5769 .view
5770 .as_ref()
5771 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
5772
5773 if state.active_view_id.is_some_and(|view_id| {
5774 !state.items_by_leader_view_id.contains_key(&view_id)
5775 }) {
5776 anyhow::Ok(true)
5777 } else {
5778 anyhow::Ok(false)
5779 }
5780 } else {
5781 anyhow::Ok(false)
5782 }
5783 })??;
5784
5785 if should_add_view && let Some(view) = update_active_view.view {
5786 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
5787 }
5788 }
5789 proto::update_followers::Variant::UpdateView(update_view) => {
5790 let variant = update_view.variant.context("missing update view variant")?;
5791 let id = update_view.id.context("missing update view id")?;
5792 let mut tasks = Vec::new();
5793 this.update_in(cx, |this, window, cx| {
5794 let project = this.project.clone();
5795 if let Some(state) = this.follower_states.get(&leader_id.into()) {
5796 let view_id = ViewId::from_proto(id.clone())?;
5797 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
5798 tasks.push(item.view.apply_update_proto(
5799 &project,
5800 variant.clone(),
5801 window,
5802 cx,
5803 ));
5804 }
5805 }
5806 anyhow::Ok(())
5807 })??;
5808 try_join_all(tasks).await.log_err();
5809 }
5810 }
5811 this.update_in(cx, |this, window, cx| {
5812 this.leader_updated(leader_id, window, cx)
5813 })?;
5814 Ok(())
5815 }
5816
5817 async fn add_view_from_leader(
5818 this: WeakEntity<Self>,
5819 leader_id: PeerId,
5820 view: &proto::View,
5821 cx: &mut AsyncWindowContext,
5822 ) -> Result<()> {
5823 let this = this.upgrade().context("workspace dropped")?;
5824
5825 let Some(id) = view.id.clone() else {
5826 anyhow::bail!("no id for view");
5827 };
5828 let id = ViewId::from_proto(id)?;
5829 let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
5830
5831 let pane = this.update(cx, |this, _cx| {
5832 let state = this
5833 .follower_states
5834 .get(&leader_id.into())
5835 .context("stopped following")?;
5836 anyhow::Ok(state.pane().clone())
5837 })?;
5838 let existing_item = pane.update_in(cx, |pane, window, cx| {
5839 let client = this.read(cx).client().clone();
5840 pane.items().find_map(|item| {
5841 let item = item.to_followable_item_handle(cx)?;
5842 if item.remote_id(&client, window, cx) == Some(id) {
5843 Some(item)
5844 } else {
5845 None
5846 }
5847 })
5848 })?;
5849 let item = if let Some(existing_item) = existing_item {
5850 existing_item
5851 } else {
5852 let variant = view.variant.clone();
5853 anyhow::ensure!(variant.is_some(), "missing view variant");
5854
5855 let task = cx.update(|window, cx| {
5856 FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx)
5857 })?;
5858
5859 let Some(task) = task else {
5860 anyhow::bail!(
5861 "failed to construct view from leader (maybe from a different version of zed?)"
5862 );
5863 };
5864
5865 let mut new_item = task.await?;
5866 pane.update_in(cx, |pane, window, cx| {
5867 let mut item_to_remove = None;
5868 for (ix, item) in pane.items().enumerate() {
5869 if let Some(item) = item.to_followable_item_handle(cx) {
5870 match new_item.dedup(item.as_ref(), window, cx) {
5871 Some(item::Dedup::KeepExisting) => {
5872 new_item =
5873 item.boxed_clone().to_followable_item_handle(cx).unwrap();
5874 break;
5875 }
5876 Some(item::Dedup::ReplaceExisting) => {
5877 item_to_remove = Some((ix, item.item_id()));
5878 break;
5879 }
5880 None => {}
5881 }
5882 }
5883 }
5884
5885 if let Some((ix, id)) = item_to_remove {
5886 pane.remove_item(id, false, false, window, cx);
5887 pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx);
5888 }
5889 })?;
5890
5891 new_item
5892 };
5893
5894 this.update_in(cx, |this, window, cx| {
5895 let state = this.follower_states.get_mut(&leader_id.into())?;
5896 item.set_leader_id(Some(leader_id.into()), window, cx);
5897 state.items_by_leader_view_id.insert(
5898 id,
5899 FollowerView {
5900 view: item,
5901 location: panel_id,
5902 },
5903 );
5904
5905 Some(())
5906 })
5907 .context("no follower state")?;
5908
5909 Ok(())
5910 }
5911
5912 fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5913 let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else {
5914 return;
5915 };
5916
5917 if let Some(agent_location) = self.project.read(cx).agent_location() {
5918 let buffer_entity_id = agent_location.buffer.entity_id();
5919 let view_id = ViewId {
5920 creator: CollaboratorId::Agent,
5921 id: buffer_entity_id.as_u64(),
5922 };
5923 follower_state.active_view_id = Some(view_id);
5924
5925 let item = match follower_state.items_by_leader_view_id.entry(view_id) {
5926 hash_map::Entry::Occupied(entry) => Some(entry.into_mut()),
5927 hash_map::Entry::Vacant(entry) => {
5928 let existing_view =
5929 follower_state
5930 .center_pane
5931 .read(cx)
5932 .items()
5933 .find_map(|item| {
5934 let item = item.to_followable_item_handle(cx)?;
5935 if item.buffer_kind(cx) == ItemBufferKind::Singleton
5936 && item.project_item_model_ids(cx).as_slice()
5937 == [buffer_entity_id]
5938 {
5939 Some(item)
5940 } else {
5941 None
5942 }
5943 });
5944 let view = existing_view.or_else(|| {
5945 agent_location.buffer.upgrade().and_then(|buffer| {
5946 cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| {
5947 registry.build_item(buffer, self.project.clone(), None, window, cx)
5948 })?
5949 .to_followable_item_handle(cx)
5950 })
5951 });
5952
5953 view.map(|view| {
5954 entry.insert(FollowerView {
5955 view,
5956 location: None,
5957 })
5958 })
5959 }
5960 };
5961
5962 if let Some(item) = item {
5963 item.view
5964 .set_leader_id(Some(CollaboratorId::Agent), window, cx);
5965 item.view
5966 .update_agent_location(agent_location.position, window, cx);
5967 }
5968 } else {
5969 follower_state.active_view_id = None;
5970 }
5971
5972 self.leader_updated(CollaboratorId::Agent, window, cx);
5973 }
5974
5975 pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
5976 let mut is_project_item = true;
5977 let mut update = proto::UpdateActiveView::default();
5978 if window.is_window_active() {
5979 let (active_item, panel_id) = self.active_item_for_followers(window, cx);
5980
5981 if let Some(item) = active_item
5982 && item.item_focus_handle(cx).contains_focused(window, cx)
5983 {
5984 let leader_id = self
5985 .pane_for(&*item)
5986 .and_then(|pane| self.leader_for_pane(&pane));
5987 let leader_peer_id = match leader_id {
5988 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
5989 Some(CollaboratorId::Agent) | None => None,
5990 };
5991
5992 if let Some(item) = item.to_followable_item_handle(cx) {
5993 let id = item
5994 .remote_id(&self.app_state.client, window, cx)
5995 .map(|id| id.to_proto());
5996
5997 if let Some(id) = id
5998 && let Some(variant) = item.to_state_proto(window, cx)
5999 {
6000 let view = Some(proto::View {
6001 id,
6002 leader_id: leader_peer_id,
6003 variant: Some(variant),
6004 panel_id: panel_id.map(|id| id as i32),
6005 });
6006
6007 is_project_item = item.is_project_item(window, cx);
6008 update = proto::UpdateActiveView { view };
6009 };
6010 }
6011 }
6012 }
6013
6014 let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
6015 if active_view_id != self.last_active_view_id.as_ref() {
6016 self.last_active_view_id = active_view_id.cloned();
6017 self.update_followers(
6018 is_project_item,
6019 proto::update_followers::Variant::UpdateActiveView(update),
6020 window,
6021 cx,
6022 );
6023 }
6024 }
6025
6026 fn active_item_for_followers(
6027 &self,
6028 window: &mut Window,
6029 cx: &mut App,
6030 ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
6031 let mut active_item = None;
6032 let mut panel_id = None;
6033 for dock in self.all_docks() {
6034 if dock.focus_handle(cx).contains_focused(window, cx)
6035 && let Some(panel) = dock.read(cx).active_panel()
6036 && let Some(pane) = panel.pane(cx)
6037 && let Some(item) = pane.read(cx).active_item()
6038 {
6039 active_item = Some(item);
6040 panel_id = panel.remote_id();
6041 break;
6042 }
6043 }
6044
6045 if active_item.is_none() {
6046 active_item = self.active_pane().read(cx).active_item();
6047 }
6048 (active_item, panel_id)
6049 }
6050
6051 fn update_followers(
6052 &self,
6053 project_only: bool,
6054 update: proto::update_followers::Variant,
6055 _: &mut Window,
6056 cx: &mut App,
6057 ) -> Option<()> {
6058 // If this update only applies to for followers in the current project,
6059 // then skip it unless this project is shared. If it applies to all
6060 // followers, regardless of project, then set `project_id` to none,
6061 // indicating that it goes to all followers.
6062 let project_id = if project_only {
6063 Some(self.project.read(cx).remote_id()?)
6064 } else {
6065 None
6066 };
6067 self.app_state().workspace_store.update(cx, |store, cx| {
6068 store.update_followers(project_id, update, cx)
6069 })
6070 }
6071
6072 pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<CollaboratorId> {
6073 self.follower_states.iter().find_map(|(leader_id, state)| {
6074 if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
6075 Some(*leader_id)
6076 } else {
6077 None
6078 }
6079 })
6080 }
6081
6082 fn leader_updated(
6083 &mut self,
6084 leader_id: impl Into<CollaboratorId>,
6085 window: &mut Window,
6086 cx: &mut Context<Self>,
6087 ) -> Option<Box<dyn ItemHandle>> {
6088 cx.notify();
6089
6090 let leader_id = leader_id.into();
6091 let (panel_id, item) = match leader_id {
6092 CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?,
6093 CollaboratorId::Agent => (None, self.active_item_for_agent()?),
6094 };
6095
6096 let state = self.follower_states.get(&leader_id)?;
6097 let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
6098 let pane;
6099 if let Some(panel_id) = panel_id {
6100 pane = self
6101 .activate_panel_for_proto_id(panel_id, window, cx)?
6102 .pane(cx)?;
6103 let state = self.follower_states.get_mut(&leader_id)?;
6104 state.dock_pane = Some(pane.clone());
6105 } else {
6106 pane = state.center_pane.clone();
6107 let state = self.follower_states.get_mut(&leader_id)?;
6108 if let Some(dock_pane) = state.dock_pane.take() {
6109 transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx);
6110 }
6111 }
6112
6113 pane.update(cx, |pane, cx| {
6114 let focus_active_item = pane.has_focus(window, cx) || transfer_focus;
6115 if let Some(index) = pane.index_for_item(item.as_ref()) {
6116 pane.activate_item(index, false, false, window, cx);
6117 } else {
6118 pane.add_item(item.boxed_clone(), false, false, None, window, cx)
6119 }
6120
6121 if focus_active_item {
6122 pane.focus_active_item(window, cx)
6123 }
6124 });
6125
6126 Some(item)
6127 }
6128
6129 fn active_item_for_agent(&self) -> Option<Box<dyn ItemHandle>> {
6130 let state = self.follower_states.get(&CollaboratorId::Agent)?;
6131 let active_view_id = state.active_view_id?;
6132 Some(
6133 state
6134 .items_by_leader_view_id
6135 .get(&active_view_id)?
6136 .view
6137 .boxed_clone(),
6138 )
6139 }
6140
6141 fn active_item_for_peer(
6142 &self,
6143 peer_id: PeerId,
6144 window: &mut Window,
6145 cx: &mut Context<Self>,
6146 ) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
6147 let call = self.active_call()?;
6148 let participant = call.remote_participant_for_peer_id(peer_id, cx)?;
6149 let leader_in_this_app;
6150 let leader_in_this_project;
6151 match participant.location {
6152 ParticipantLocation::SharedProject { project_id } => {
6153 leader_in_this_app = true;
6154 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
6155 }
6156 ParticipantLocation::UnsharedProject => {
6157 leader_in_this_app = true;
6158 leader_in_this_project = false;
6159 }
6160 ParticipantLocation::External => {
6161 leader_in_this_app = false;
6162 leader_in_this_project = false;
6163 }
6164 };
6165 let state = self.follower_states.get(&peer_id.into())?;
6166 let mut item_to_activate = None;
6167 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
6168 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id)
6169 && (leader_in_this_project || !item.view.is_project_item(window, cx))
6170 {
6171 item_to_activate = Some((item.location, item.view.boxed_clone()));
6172 }
6173 } else if let Some(shared_screen) =
6174 self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx)
6175 {
6176 item_to_activate = Some((None, Box::new(shared_screen)));
6177 }
6178 item_to_activate
6179 }
6180
6181 fn shared_screen_for_peer(
6182 &self,
6183 peer_id: PeerId,
6184 pane: &Entity<Pane>,
6185 window: &mut Window,
6186 cx: &mut App,
6187 ) -> Option<Entity<SharedScreen>> {
6188 self.active_call()?
6189 .create_shared_screen(peer_id, pane, window, cx)
6190 }
6191
6192 pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
6193 if window.is_window_active() {
6194 self.update_active_view_for_followers(window, cx);
6195
6196 if let Some(database_id) = self.database_id {
6197 let db = WorkspaceDb::global(cx);
6198 cx.background_spawn(async move { db.update_timestamp(database_id).await })
6199 .detach();
6200 }
6201 } else {
6202 for pane in &self.panes {
6203 pane.update(cx, |pane, cx| {
6204 if let Some(item) = pane.active_item() {
6205 item.workspace_deactivated(window, cx);
6206 }
6207 for item in pane.items() {
6208 if matches!(
6209 item.workspace_settings(cx).autosave,
6210 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
6211 ) {
6212 Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
6213 .detach_and_log_err(cx);
6214 }
6215 }
6216 });
6217 }
6218 }
6219 }
6220
6221 pub fn active_call(&self) -> Option<&dyn AnyActiveCall> {
6222 self.active_call.as_ref().map(|(call, _)| &*call.0)
6223 }
6224
6225 pub fn active_global_call(&self) -> Option<GlobalAnyActiveCall> {
6226 self.active_call.as_ref().map(|(call, _)| call.clone())
6227 }
6228
6229 fn on_active_call_event(
6230 &mut self,
6231 event: &ActiveCallEvent,
6232 window: &mut Window,
6233 cx: &mut Context<Self>,
6234 ) {
6235 match event {
6236 ActiveCallEvent::ParticipantLocationChanged { participant_id }
6237 | ActiveCallEvent::RemoteVideoTracksChanged { participant_id } => {
6238 self.leader_updated(participant_id, window, cx);
6239 }
6240 }
6241 }
6242
6243 pub fn database_id(&self) -> Option<WorkspaceId> {
6244 self.database_id
6245 }
6246
6247 #[cfg(any(test, feature = "test-support"))]
6248 pub(crate) fn set_database_id(&mut self, id: WorkspaceId) {
6249 self.database_id = Some(id);
6250 }
6251
6252 pub fn session_id(&self) -> Option<String> {
6253 self.session_id.clone()
6254 }
6255
6256 fn save_window_bounds(&self, window: &mut Window, cx: &mut App) -> Task<()> {
6257 let Some(display) = window.display(cx) else {
6258 return Task::ready(());
6259 };
6260 let Ok(display_uuid) = display.uuid() else {
6261 return Task::ready(());
6262 };
6263
6264 let window_bounds = window.inner_window_bounds();
6265 let database_id = self.database_id;
6266 let has_paths = !self.root_paths(cx).is_empty();
6267 let db = WorkspaceDb::global(cx);
6268 let kvp = db::kvp::KeyValueStore::global(cx);
6269
6270 cx.background_executor().spawn(async move {
6271 if !has_paths {
6272 persistence::write_default_window_bounds(&kvp, window_bounds, display_uuid)
6273 .await
6274 .log_err();
6275 }
6276 if let Some(database_id) = database_id {
6277 db.set_window_open_status(
6278 database_id,
6279 SerializedWindowBounds(window_bounds),
6280 display_uuid,
6281 )
6282 .await
6283 .log_err();
6284 } else {
6285 persistence::write_default_window_bounds(&kvp, window_bounds, display_uuid)
6286 .await
6287 .log_err();
6288 }
6289 })
6290 }
6291
6292 /// Bypass the 200ms serialization throttle and write workspace state to
6293 /// the DB immediately. Returns a task the caller can await to ensure the
6294 /// write completes. Used by the quit handler so the most recent state
6295 /// isn't lost to a pending throttle timer when the process exits.
6296 pub fn flush_serialization(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
6297 self._schedule_serialize_workspace.take();
6298 self._serialize_workspace_task.take();
6299 self.bounds_save_task_queued.take();
6300
6301 let bounds_task = self.save_window_bounds(window, cx);
6302 let serialize_task = self.serialize_workspace_internal(window, cx);
6303 cx.spawn(async move |_| {
6304 bounds_task.await;
6305 serialize_task.await;
6306 })
6307 }
6308
6309 pub fn root_paths(&self, cx: &App) -> Vec<Arc<Path>> {
6310 let project = self.project().read(cx);
6311 project
6312 .visible_worktrees(cx)
6313 .map(|worktree| worktree.read(cx).abs_path())
6314 .collect::<Vec<_>>()
6315 }
6316
6317 fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
6318 match member {
6319 Member::Axis(PaneAxis { members, .. }) => {
6320 for child in members.iter() {
6321 self.remove_panes(child.clone(), window, cx)
6322 }
6323 }
6324 Member::Pane(pane) => {
6325 self.force_remove_pane(&pane, &None, window, cx);
6326 }
6327 }
6328 }
6329
6330 fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
6331 self.session_id.take();
6332 self.serialize_workspace_internal(window, cx)
6333 }
6334
6335 fn force_remove_pane(
6336 &mut self,
6337 pane: &Entity<Pane>,
6338 focus_on: &Option<Entity<Pane>>,
6339 window: &mut Window,
6340 cx: &mut Context<Workspace>,
6341 ) {
6342 self.panes.retain(|p| p != pane);
6343 if let Some(focus_on) = focus_on {
6344 focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
6345 } else if self.active_pane() == pane {
6346 self.panes
6347 .last()
6348 .unwrap()
6349 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
6350 }
6351 if self.last_active_center_pane == Some(pane.downgrade()) {
6352 self.last_active_center_pane = None;
6353 }
6354 cx.notify();
6355 }
6356
6357 fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
6358 if self._schedule_serialize_workspace.is_none() {
6359 self._schedule_serialize_workspace =
6360 Some(cx.spawn_in(window, async move |this, cx| {
6361 cx.background_executor()
6362 .timer(SERIALIZATION_THROTTLE_TIME)
6363 .await;
6364 this.update_in(cx, |this, window, cx| {
6365 this._serialize_workspace_task =
6366 Some(this.serialize_workspace_internal(window, cx));
6367 this._schedule_serialize_workspace.take();
6368 })
6369 .log_err();
6370 }));
6371 }
6372 }
6373
6374 fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
6375 let Some(database_id) = self.database_id() else {
6376 return Task::ready(());
6377 };
6378
6379 fn serialize_pane_handle(
6380 pane_handle: &Entity<Pane>,
6381 window: &mut Window,
6382 cx: &mut App,
6383 ) -> SerializedPane {
6384 let (items, active, pinned_count) = {
6385 let pane = pane_handle.read(cx);
6386 let active_item_id = pane.active_item().map(|item| item.item_id());
6387 (
6388 pane.items()
6389 .filter_map(|handle| {
6390 let handle = handle.to_serializable_item_handle(cx)?;
6391
6392 Some(SerializedItem {
6393 kind: Arc::from(handle.serialized_item_kind()),
6394 item_id: handle.item_id().as_u64(),
6395 active: Some(handle.item_id()) == active_item_id,
6396 preview: pane.is_active_preview_item(handle.item_id()),
6397 })
6398 })
6399 .collect::<Vec<_>>(),
6400 pane.has_focus(window, cx),
6401 pane.pinned_count(),
6402 )
6403 };
6404
6405 SerializedPane::new(items, active, pinned_count)
6406 }
6407
6408 fn build_serialized_pane_group(
6409 pane_group: &Member,
6410 window: &mut Window,
6411 cx: &mut App,
6412 ) -> SerializedPaneGroup {
6413 match pane_group {
6414 Member::Axis(PaneAxis {
6415 axis,
6416 members,
6417 flexes,
6418 bounding_boxes: _,
6419 }) => SerializedPaneGroup::Group {
6420 axis: SerializedAxis(*axis),
6421 children: members
6422 .iter()
6423 .map(|member| build_serialized_pane_group(member, window, cx))
6424 .collect::<Vec<_>>(),
6425 flexes: Some(flexes.lock().clone()),
6426 },
6427 Member::Pane(pane_handle) => {
6428 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
6429 }
6430 }
6431 }
6432
6433 fn build_serialized_docks(
6434 this: &Workspace,
6435 window: &mut Window,
6436 cx: &mut App,
6437 ) -> DockStructure {
6438 this.capture_dock_state(window, cx)
6439 }
6440
6441 match self.workspace_location(cx) {
6442 WorkspaceLocation::Location(location, paths) => {
6443 let breakpoints = self.project.update(cx, |project, cx| {
6444 project
6445 .breakpoint_store()
6446 .read(cx)
6447 .all_source_breakpoints(cx)
6448 });
6449 let user_toolchains = self
6450 .project
6451 .read(cx)
6452 .user_toolchains(cx)
6453 .unwrap_or_default();
6454
6455 let center_group = build_serialized_pane_group(&self.center.root, window, cx);
6456 let docks = build_serialized_docks(self, window, cx);
6457 let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
6458
6459 let serialized_workspace = SerializedWorkspace {
6460 id: database_id,
6461 location,
6462 paths,
6463 center_group,
6464 window_bounds,
6465 display: Default::default(),
6466 docks,
6467 centered_layout: self.centered_layout,
6468 session_id: self.session_id.clone(),
6469 breakpoints,
6470 window_id: Some(window.window_handle().window_id().as_u64()),
6471 user_toolchains,
6472 };
6473
6474 let db = WorkspaceDb::global(cx);
6475 window.spawn(cx, async move |_| {
6476 db.save_workspace(serialized_workspace).await;
6477 })
6478 }
6479 WorkspaceLocation::DetachFromSession => {
6480 let window_bounds = SerializedWindowBounds(window.window_bounds());
6481 let display = window.display(cx).and_then(|d| d.uuid().ok());
6482 // Save dock state for empty local workspaces
6483 let docks = build_serialized_docks(self, window, cx);
6484 let db = WorkspaceDb::global(cx);
6485 let kvp = db::kvp::KeyValueStore::global(cx);
6486 window.spawn(cx, async move |_| {
6487 db.set_window_open_status(
6488 database_id,
6489 window_bounds,
6490 display.unwrap_or_default(),
6491 )
6492 .await
6493 .log_err();
6494 db.set_session_id(database_id, None).await.log_err();
6495 persistence::write_default_dock_state(&kvp, docks)
6496 .await
6497 .log_err();
6498 })
6499 }
6500 WorkspaceLocation::None => {
6501 // Save dock state for empty non-local workspaces
6502 let docks = build_serialized_docks(self, window, cx);
6503 let kvp = db::kvp::KeyValueStore::global(cx);
6504 window.spawn(cx, async move |_| {
6505 persistence::write_default_dock_state(&kvp, docks)
6506 .await
6507 .log_err();
6508 })
6509 }
6510 }
6511 }
6512
6513 fn has_any_items_open(&self, cx: &App) -> bool {
6514 self.panes.iter().any(|pane| pane.read(cx).items_len() > 0)
6515 }
6516
6517 fn workspace_location(&self, cx: &App) -> WorkspaceLocation {
6518 let paths = PathList::new(&self.root_paths(cx));
6519 if let Some(connection) = self.project.read(cx).remote_connection_options(cx) {
6520 WorkspaceLocation::Location(SerializedWorkspaceLocation::Remote(connection), paths)
6521 } else if self.project.read(cx).is_local() {
6522 if !paths.is_empty() || self.has_any_items_open(cx) {
6523 WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths)
6524 } else {
6525 WorkspaceLocation::DetachFromSession
6526 }
6527 } else {
6528 WorkspaceLocation::None
6529 }
6530 }
6531
6532 fn update_history(&self, cx: &mut App) {
6533 let Some(id) = self.database_id() else {
6534 return;
6535 };
6536 if !self.project.read(cx).is_local() {
6537 return;
6538 }
6539 if let Some(manager) = HistoryManager::global(cx) {
6540 let paths = PathList::new(&self.root_paths(cx));
6541 manager.update(cx, |this, cx| {
6542 this.update_history(id, HistoryManagerEntry::new(id, &paths), cx);
6543 });
6544 }
6545 }
6546
6547 async fn serialize_items(
6548 this: &WeakEntity<Self>,
6549 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
6550 cx: &mut AsyncWindowContext,
6551 ) -> Result<()> {
6552 const CHUNK_SIZE: usize = 200;
6553
6554 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
6555
6556 while let Some(items_received) = serializable_items.next().await {
6557 let unique_items =
6558 items_received
6559 .into_iter()
6560 .fold(HashMap::default(), |mut acc, item| {
6561 acc.entry(item.item_id()).or_insert(item);
6562 acc
6563 });
6564
6565 // We use into_iter() here so that the references to the items are moved into
6566 // the tasks and not kept alive while we're sleeping.
6567 for (_, item) in unique_items.into_iter() {
6568 if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
6569 item.serialize(workspace, false, window, cx)
6570 }) {
6571 cx.background_spawn(async move { task.await.log_err() })
6572 .detach();
6573 }
6574 }
6575
6576 cx.background_executor()
6577 .timer(SERIALIZATION_THROTTLE_TIME)
6578 .await;
6579 }
6580
6581 Ok(())
6582 }
6583
6584 pub(crate) fn enqueue_item_serialization(
6585 &mut self,
6586 item: Box<dyn SerializableItemHandle>,
6587 ) -> Result<()> {
6588 self.serializable_items_tx
6589 .unbounded_send(item)
6590 .map_err(|err| anyhow!("failed to send serializable item over channel: {err}"))
6591 }
6592
6593 pub(crate) fn load_workspace(
6594 serialized_workspace: SerializedWorkspace,
6595 paths_to_open: Vec<Option<ProjectPath>>,
6596 window: &mut Window,
6597 cx: &mut Context<Workspace>,
6598 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
6599 cx.spawn_in(window, async move |workspace, cx| {
6600 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
6601
6602 let mut center_group = None;
6603 let mut center_items = None;
6604
6605 // Traverse the splits tree and add to things
6606 if let Some((group, active_pane, items)) = serialized_workspace
6607 .center_group
6608 .deserialize(&project, serialized_workspace.id, workspace.clone(), cx)
6609 .await
6610 {
6611 center_items = Some(items);
6612 center_group = Some((group, active_pane))
6613 }
6614
6615 let mut items_by_project_path = HashMap::default();
6616 let mut item_ids_by_kind = HashMap::default();
6617 let mut all_deserialized_items = Vec::default();
6618 cx.update(|_, cx| {
6619 for item in center_items.unwrap_or_default().into_iter().flatten() {
6620 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
6621 item_ids_by_kind
6622 .entry(serializable_item_handle.serialized_item_kind())
6623 .or_insert(Vec::new())
6624 .push(item.item_id().as_u64() as ItemId);
6625 }
6626
6627 if let Some(project_path) = item.project_path(cx) {
6628 items_by_project_path.insert(project_path, item.clone());
6629 }
6630 all_deserialized_items.push(item);
6631 }
6632 })?;
6633
6634 let opened_items = paths_to_open
6635 .into_iter()
6636 .map(|path_to_open| {
6637 path_to_open
6638 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
6639 })
6640 .collect::<Vec<_>>();
6641
6642 // Remove old panes from workspace panes list
6643 workspace.update_in(cx, |workspace, window, cx| {
6644 if let Some((center_group, active_pane)) = center_group {
6645 workspace.remove_panes(workspace.center.root.clone(), window, cx);
6646
6647 // Swap workspace center group
6648 workspace.center = PaneGroup::with_root(center_group);
6649 workspace.center.set_is_center(true);
6650 workspace.center.mark_positions(cx);
6651
6652 if let Some(active_pane) = active_pane {
6653 workspace.set_active_pane(&active_pane, window, cx);
6654 cx.focus_self(window);
6655 } else {
6656 workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
6657 }
6658 }
6659
6660 let docks = serialized_workspace.docks;
6661
6662 for (dock, serialized_dock) in [
6663 (&mut workspace.right_dock, docks.right),
6664 (&mut workspace.left_dock, docks.left),
6665 (&mut workspace.bottom_dock, docks.bottom),
6666 ]
6667 .iter_mut()
6668 {
6669 dock.update(cx, |dock, cx| {
6670 dock.serialized_dock = Some(serialized_dock.clone());
6671 dock.restore_state(window, cx);
6672 });
6673 }
6674
6675 cx.notify();
6676 })?;
6677
6678 let _ = project
6679 .update(cx, |project, cx| {
6680 project
6681 .breakpoint_store()
6682 .update(cx, |breakpoint_store, cx| {
6683 breakpoint_store
6684 .with_serialized_breakpoints(serialized_workspace.breakpoints, cx)
6685 })
6686 })
6687 .await;
6688
6689 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
6690 // after loading the items, we might have different items and in order to avoid
6691 // the database filling up, we delete items that haven't been loaded now.
6692 //
6693 // The items that have been loaded, have been saved after they've been added to the workspace.
6694 let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
6695 item_ids_by_kind
6696 .into_iter()
6697 .map(|(item_kind, loaded_items)| {
6698 SerializableItemRegistry::cleanup(
6699 item_kind,
6700 serialized_workspace.id,
6701 loaded_items,
6702 window,
6703 cx,
6704 )
6705 .log_err()
6706 })
6707 .collect::<Vec<_>>()
6708 })?;
6709
6710 futures::future::join_all(clean_up_tasks).await;
6711
6712 workspace
6713 .update_in(cx, |workspace, window, cx| {
6714 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
6715 workspace.serialize_workspace_internal(window, cx).detach();
6716
6717 // Ensure that we mark the window as edited if we did load dirty items
6718 workspace.update_window_edited(window, cx);
6719 })
6720 .ok();
6721
6722 Ok(opened_items)
6723 })
6724 }
6725
6726 pub fn key_context(&self, cx: &App) -> KeyContext {
6727 let mut context = KeyContext::new_with_defaults();
6728 context.add("Workspace");
6729 context.set("keyboard_layout", cx.keyboard_layout().name().to_string());
6730 if let Some(status) = self
6731 .debugger_provider
6732 .as_ref()
6733 .and_then(|provider| provider.active_thread_state(cx))
6734 {
6735 match status {
6736 ThreadStatus::Running | ThreadStatus::Stepping => {
6737 context.add("debugger_running");
6738 }
6739 ThreadStatus::Stopped => context.add("debugger_stopped"),
6740 ThreadStatus::Exited | ThreadStatus::Ended => {}
6741 }
6742 }
6743
6744 if self.left_dock.read(cx).is_open() {
6745 if let Some(active_panel) = self.left_dock.read(cx).active_panel() {
6746 context.set("left_dock", active_panel.panel_key());
6747 }
6748 }
6749
6750 if self.right_dock.read(cx).is_open() {
6751 if let Some(active_panel) = self.right_dock.read(cx).active_panel() {
6752 context.set("right_dock", active_panel.panel_key());
6753 }
6754 }
6755
6756 if self.bottom_dock.read(cx).is_open() {
6757 if let Some(active_panel) = self.bottom_dock.read(cx).active_panel() {
6758 context.set("bottom_dock", active_panel.panel_key());
6759 }
6760 }
6761
6762 context
6763 }
6764
6765 /// Multiworkspace uses this to add workspace action handling to itself
6766 pub fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
6767 self.add_workspace_actions_listeners(div, window, cx)
6768 .on_action(cx.listener(
6769 |_workspace, action_sequence: &settings::ActionSequence, window, cx| {
6770 for action in &action_sequence.0 {
6771 window.dispatch_action(action.boxed_clone(), cx);
6772 }
6773 },
6774 ))
6775 .on_action(cx.listener(Self::close_inactive_items_and_panes))
6776 .on_action(cx.listener(Self::close_all_items_and_panes))
6777 .on_action(cx.listener(Self::close_item_in_all_panes))
6778 .on_action(cx.listener(Self::save_all))
6779 .on_action(cx.listener(Self::send_keystrokes))
6780 .on_action(cx.listener(Self::add_folder_to_project))
6781 .on_action(cx.listener(Self::follow_next_collaborator))
6782 .on_action(cx.listener(Self::activate_pane_at_index))
6783 .on_action(cx.listener(Self::move_item_to_pane_at_index))
6784 .on_action(cx.listener(Self::move_focused_panel_to_next_position))
6785 .on_action(cx.listener(Self::toggle_edit_predictions_all_files))
6786 .on_action(cx.listener(Self::toggle_theme_mode))
6787 .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
6788 let pane = workspace.active_pane().clone();
6789 workspace.unfollow_in_pane(&pane, window, cx);
6790 }))
6791 .on_action(cx.listener(|workspace, action: &Save, window, cx| {
6792 workspace
6793 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
6794 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6795 }))
6796 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
6797 workspace
6798 .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
6799 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6800 }))
6801 .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
6802 workspace
6803 .save_active_item(SaveIntent::SaveAs, window, cx)
6804 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6805 }))
6806 .on_action(
6807 cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
6808 workspace.activate_previous_pane(window, cx)
6809 }),
6810 )
6811 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
6812 workspace.activate_next_pane(window, cx)
6813 }))
6814 .on_action(cx.listener(|workspace, _: &ActivateLastPane, window, cx| {
6815 workspace.activate_last_pane(window, cx)
6816 }))
6817 .on_action(
6818 cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
6819 workspace.activate_next_window(cx)
6820 }),
6821 )
6822 .on_action(
6823 cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
6824 workspace.activate_previous_window(cx)
6825 }),
6826 )
6827 .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
6828 workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
6829 }))
6830 .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
6831 workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
6832 }))
6833 .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
6834 workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
6835 }))
6836 .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
6837 workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
6838 }))
6839 .on_action(cx.listener(
6840 |workspace, action: &MoveItemToPaneInDirection, window, cx| {
6841 workspace.move_item_to_pane_in_direction(action, window, cx)
6842 },
6843 ))
6844 .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
6845 workspace.swap_pane_in_direction(SplitDirection::Left, cx)
6846 }))
6847 .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
6848 workspace.swap_pane_in_direction(SplitDirection::Right, cx)
6849 }))
6850 .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
6851 workspace.swap_pane_in_direction(SplitDirection::Up, cx)
6852 }))
6853 .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
6854 workspace.swap_pane_in_direction(SplitDirection::Down, cx)
6855 }))
6856 .on_action(cx.listener(|workspace, _: &SwapPaneAdjacent, window, cx| {
6857 const DIRECTION_PRIORITY: [SplitDirection; 4] = [
6858 SplitDirection::Down,
6859 SplitDirection::Up,
6860 SplitDirection::Right,
6861 SplitDirection::Left,
6862 ];
6863 for dir in DIRECTION_PRIORITY {
6864 if workspace.find_pane_in_direction(dir, cx).is_some() {
6865 workspace.swap_pane_in_direction(dir, cx);
6866 workspace.activate_pane_in_direction(dir.opposite(), window, cx);
6867 break;
6868 }
6869 }
6870 }))
6871 .on_action(cx.listener(|workspace, _: &MovePaneLeft, _, cx| {
6872 workspace.move_pane_to_border(SplitDirection::Left, cx)
6873 }))
6874 .on_action(cx.listener(|workspace, _: &MovePaneRight, _, cx| {
6875 workspace.move_pane_to_border(SplitDirection::Right, cx)
6876 }))
6877 .on_action(cx.listener(|workspace, _: &MovePaneUp, _, cx| {
6878 workspace.move_pane_to_border(SplitDirection::Up, cx)
6879 }))
6880 .on_action(cx.listener(|workspace, _: &MovePaneDown, _, cx| {
6881 workspace.move_pane_to_border(SplitDirection::Down, cx)
6882 }))
6883 .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
6884 this.toggle_dock(DockPosition::Left, window, cx);
6885 }))
6886 .on_action(cx.listener(
6887 |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
6888 workspace.toggle_dock(DockPosition::Right, window, cx);
6889 },
6890 ))
6891 .on_action(cx.listener(
6892 |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
6893 workspace.toggle_dock(DockPosition::Bottom, window, cx);
6894 },
6895 ))
6896 .on_action(cx.listener(
6897 |workspace: &mut Workspace, _: &CloseActiveDock, window, cx| {
6898 if !workspace.close_active_dock(window, cx) {
6899 cx.propagate();
6900 }
6901 },
6902 ))
6903 .on_action(
6904 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
6905 workspace.close_all_docks(window, cx);
6906 }),
6907 )
6908 .on_action(cx.listener(Self::toggle_all_docks))
6909 .on_action(cx.listener(
6910 |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
6911 workspace.clear_all_notifications(cx);
6912 },
6913 ))
6914 .on_action(cx.listener(
6915 |workspace: &mut Workspace, _: &ClearNavigationHistory, window, cx| {
6916 workspace.clear_navigation_history(window, cx);
6917 },
6918 ))
6919 .on_action(cx.listener(
6920 |workspace: &mut Workspace, _: &SuppressNotification, _, cx| {
6921 if let Some((notification_id, _)) = workspace.notifications.pop() {
6922 workspace.suppress_notification(¬ification_id, cx);
6923 }
6924 },
6925 ))
6926 .on_action(cx.listener(
6927 |workspace: &mut Workspace, _: &ToggleWorktreeSecurity, window, cx| {
6928 workspace.show_worktree_trust_security_modal(true, window, cx);
6929 },
6930 ))
6931 .on_action(
6932 cx.listener(|_: &mut Workspace, _: &ClearTrustedWorktrees, _, cx| {
6933 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
6934 trusted_worktrees.update(cx, |trusted_worktrees, _| {
6935 trusted_worktrees.clear_trusted_paths()
6936 });
6937 let db = WorkspaceDb::global(cx);
6938 cx.spawn(async move |_, cx| {
6939 if db.clear_trusted_worktrees().await.log_err().is_some() {
6940 cx.update(|cx| reload(cx));
6941 }
6942 })
6943 .detach();
6944 }
6945 }),
6946 )
6947 .on_action(cx.listener(
6948 |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
6949 workspace.reopen_closed_item(window, cx).detach();
6950 },
6951 ))
6952 .on_action(cx.listener(
6953 |workspace: &mut Workspace, _: &ResetActiveDockSize, window, cx| {
6954 for dock in workspace.all_docks() {
6955 if dock.focus_handle(cx).contains_focused(window, cx) {
6956 let panel = dock.read(cx).active_panel().cloned();
6957 if let Some(panel) = panel {
6958 dock.update(cx, |dock, cx| {
6959 dock.set_panel_size_state(
6960 panel.as_ref(),
6961 dock::PanelSizeState::default(),
6962 cx,
6963 );
6964 });
6965 }
6966 return;
6967 }
6968 }
6969 },
6970 ))
6971 .on_action(cx.listener(
6972 |workspace: &mut Workspace, _: &ResetOpenDocksSize, _window, cx| {
6973 for dock in workspace.all_docks() {
6974 let panel = dock.read(cx).visible_panel().cloned();
6975 if let Some(panel) = panel {
6976 dock.update(cx, |dock, cx| {
6977 dock.set_panel_size_state(
6978 panel.as_ref(),
6979 dock::PanelSizeState::default(),
6980 cx,
6981 );
6982 });
6983 }
6984 }
6985 },
6986 ))
6987 .on_action(cx.listener(
6988 |workspace: &mut Workspace, act: &IncreaseActiveDockSize, window, cx| {
6989 adjust_active_dock_size_by_px(
6990 px_with_ui_font_fallback(act.px, cx),
6991 workspace,
6992 window,
6993 cx,
6994 );
6995 },
6996 ))
6997 .on_action(cx.listener(
6998 |workspace: &mut Workspace, act: &DecreaseActiveDockSize, window, cx| {
6999 adjust_active_dock_size_by_px(
7000 px_with_ui_font_fallback(act.px, cx) * -1.,
7001 workspace,
7002 window,
7003 cx,
7004 );
7005 },
7006 ))
7007 .on_action(cx.listener(
7008 |workspace: &mut Workspace, act: &IncreaseOpenDocksSize, window, cx| {
7009 adjust_open_docks_size_by_px(
7010 px_with_ui_font_fallback(act.px, cx),
7011 workspace,
7012 window,
7013 cx,
7014 );
7015 },
7016 ))
7017 .on_action(cx.listener(
7018 |workspace: &mut Workspace, act: &DecreaseOpenDocksSize, window, cx| {
7019 adjust_open_docks_size_by_px(
7020 px_with_ui_font_fallback(act.px, cx) * -1.,
7021 workspace,
7022 window,
7023 cx,
7024 );
7025 },
7026 ))
7027 .on_action(cx.listener(Workspace::toggle_centered_layout))
7028 .on_action(cx.listener(
7029 |workspace: &mut Workspace, _action: &pane::ActivateNextItem, window, cx| {
7030 if let Some(active_dock) = workspace.active_dock(window, cx) {
7031 let dock = active_dock.read(cx);
7032 if let Some(active_panel) = dock.active_panel() {
7033 if active_panel.pane(cx).is_none() {
7034 let mut recent_pane: Option<Entity<Pane>> = None;
7035 let mut recent_timestamp = 0;
7036 for pane_handle in workspace.panes() {
7037 let pane = pane_handle.read(cx);
7038 for entry in pane.activation_history() {
7039 if entry.timestamp > recent_timestamp {
7040 recent_timestamp = entry.timestamp;
7041 recent_pane = Some(pane_handle.clone());
7042 }
7043 }
7044 }
7045
7046 if let Some(pane) = recent_pane {
7047 pane.update(cx, |pane, cx| {
7048 let current_index = pane.active_item_index();
7049 let items_len = pane.items_len();
7050 if items_len > 0 {
7051 let next_index = if current_index + 1 < items_len {
7052 current_index + 1
7053 } else {
7054 0
7055 };
7056 pane.activate_item(
7057 next_index, false, false, window, cx,
7058 );
7059 }
7060 });
7061 return;
7062 }
7063 }
7064 }
7065 }
7066 cx.propagate();
7067 },
7068 ))
7069 .on_action(cx.listener(
7070 |workspace: &mut Workspace, _action: &pane::ActivatePreviousItem, window, cx| {
7071 if let Some(active_dock) = workspace.active_dock(window, cx) {
7072 let dock = active_dock.read(cx);
7073 if let Some(active_panel) = dock.active_panel() {
7074 if active_panel.pane(cx).is_none() {
7075 let mut recent_pane: Option<Entity<Pane>> = None;
7076 let mut recent_timestamp = 0;
7077 for pane_handle in workspace.panes() {
7078 let pane = pane_handle.read(cx);
7079 for entry in pane.activation_history() {
7080 if entry.timestamp > recent_timestamp {
7081 recent_timestamp = entry.timestamp;
7082 recent_pane = Some(pane_handle.clone());
7083 }
7084 }
7085 }
7086
7087 if let Some(pane) = recent_pane {
7088 pane.update(cx, |pane, cx| {
7089 let current_index = pane.active_item_index();
7090 let items_len = pane.items_len();
7091 if items_len > 0 {
7092 let prev_index = if current_index > 0 {
7093 current_index - 1
7094 } else {
7095 items_len.saturating_sub(1)
7096 };
7097 pane.activate_item(
7098 prev_index, false, false, window, cx,
7099 );
7100 }
7101 });
7102 return;
7103 }
7104 }
7105 }
7106 }
7107 cx.propagate();
7108 },
7109 ))
7110 .on_action(cx.listener(
7111 |workspace: &mut Workspace, action: &pane::CloseActiveItem, window, cx| {
7112 if let Some(active_dock) = workspace.active_dock(window, cx) {
7113 let dock = active_dock.read(cx);
7114 if let Some(active_panel) = dock.active_panel() {
7115 if active_panel.pane(cx).is_none() {
7116 let active_pane = workspace.active_pane().clone();
7117 active_pane.update(cx, |pane, cx| {
7118 pane.close_active_item(action, window, cx)
7119 .detach_and_log_err(cx);
7120 });
7121 return;
7122 }
7123 }
7124 }
7125 cx.propagate();
7126 },
7127 ))
7128 .on_action(
7129 cx.listener(|workspace, _: &ToggleReadOnlyFile, window, cx| {
7130 let pane = workspace.active_pane().clone();
7131 if let Some(item) = pane.read(cx).active_item() {
7132 item.toggle_read_only(window, cx);
7133 }
7134 }),
7135 )
7136 .on_action(cx.listener(|workspace, _: &FocusCenterPane, window, cx| {
7137 workspace.focus_center_pane(window, cx);
7138 }))
7139 .on_action(cx.listener(Workspace::cancel))
7140 }
7141
7142 #[cfg(any(test, feature = "test-support"))]
7143 pub fn set_random_database_id(&mut self) {
7144 self.database_id = Some(WorkspaceId(Uuid::new_v4().as_u64_pair().0 as i64));
7145 }
7146
7147 #[cfg(any(test, feature = "test-support"))]
7148 pub(crate) fn test_new(
7149 project: Entity<Project>,
7150 window: &mut Window,
7151 cx: &mut Context<Self>,
7152 ) -> Self {
7153 use node_runtime::NodeRuntime;
7154 use session::Session;
7155
7156 let client = project.read(cx).client();
7157 let user_store = project.read(cx).user_store();
7158 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
7159 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
7160 window.activate_window();
7161 let app_state = Arc::new(AppState {
7162 languages: project.read(cx).languages().clone(),
7163 workspace_store,
7164 client,
7165 user_store,
7166 fs: project.read(cx).fs().clone(),
7167 build_window_options: |_, _| Default::default(),
7168 node_runtime: NodeRuntime::unavailable(),
7169 session,
7170 });
7171 let workspace = Self::new(Default::default(), project, app_state, window, cx);
7172 workspace
7173 .active_pane
7174 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
7175 workspace
7176 }
7177
7178 pub fn register_action<A: Action>(
7179 &mut self,
7180 callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
7181 ) -> &mut Self {
7182 let callback = Arc::new(callback);
7183
7184 self.workspace_actions.push(Box::new(move |div, _, _, cx| {
7185 let callback = callback.clone();
7186 div.on_action(cx.listener(move |workspace, event, window, cx| {
7187 (callback)(workspace, event, window, cx)
7188 }))
7189 }));
7190 self
7191 }
7192 pub fn register_action_renderer(
7193 &mut self,
7194 callback: impl Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div + 'static,
7195 ) -> &mut Self {
7196 self.workspace_actions.push(Box::new(callback));
7197 self
7198 }
7199
7200 fn add_workspace_actions_listeners(
7201 &self,
7202 mut div: Div,
7203 window: &mut Window,
7204 cx: &mut Context<Self>,
7205 ) -> Div {
7206 for action in self.workspace_actions.iter() {
7207 div = (action)(div, self, window, cx)
7208 }
7209 div
7210 }
7211
7212 pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
7213 self.modal_layer.read(cx).has_active_modal()
7214 }
7215
7216 pub fn is_active_modal_command_palette(&self, cx: &mut App) -> bool {
7217 self.modal_layer
7218 .read(cx)
7219 .is_active_modal_command_palette(cx)
7220 }
7221
7222 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
7223 self.modal_layer.read(cx).active_modal()
7224 }
7225
7226 /// Toggles a modal of type `V`. If a modal of the same type is currently active,
7227 /// it will be hidden. If a different modal is active, it will be replaced with the new one.
7228 /// If no modal is active, the new modal will be shown.
7229 ///
7230 /// If closing the current modal fails (e.g., due to `on_before_dismiss` returning
7231 /// `DismissDecision::Dismiss(false)` or `DismissDecision::Pending`), the new modal
7232 /// will not be shown.
7233 pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
7234 where
7235 B: FnOnce(&mut Window, &mut Context<V>) -> V,
7236 {
7237 self.modal_layer.update(cx, |modal_layer, cx| {
7238 modal_layer.toggle_modal(window, cx, build)
7239 })
7240 }
7241
7242 pub fn hide_modal(&mut self, window: &mut Window, cx: &mut App) -> bool {
7243 self.modal_layer
7244 .update(cx, |modal_layer, cx| modal_layer.hide_modal(window, cx))
7245 }
7246
7247 pub fn toggle_status_toast<V: ToastView>(&mut self, entity: Entity<V>, cx: &mut App) {
7248 self.toast_layer
7249 .update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity))
7250 }
7251
7252 pub fn toggle_centered_layout(
7253 &mut self,
7254 _: &ToggleCenteredLayout,
7255 _: &mut Window,
7256 cx: &mut Context<Self>,
7257 ) {
7258 self.centered_layout = !self.centered_layout;
7259 if let Some(database_id) = self.database_id() {
7260 let db = WorkspaceDb::global(cx);
7261 let centered_layout = self.centered_layout;
7262 cx.background_spawn(async move {
7263 db.set_centered_layout(database_id, centered_layout).await
7264 })
7265 .detach_and_log_err(cx);
7266 }
7267 cx.notify();
7268 }
7269
7270 fn adjust_padding(padding: Option<f32>) -> f32 {
7271 padding
7272 .unwrap_or(CenteredPaddingSettings::default().0)
7273 .clamp(
7274 CenteredPaddingSettings::MIN_PADDING,
7275 CenteredPaddingSettings::MAX_PADDING,
7276 )
7277 }
7278
7279 fn render_dock(
7280 &self,
7281 position: DockPosition,
7282 dock: &Entity<Dock>,
7283 window: &mut Window,
7284 cx: &mut App,
7285 ) -> Option<Div> {
7286 if self.zoomed_position == Some(position) {
7287 return None;
7288 }
7289
7290 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
7291 let pane = panel.pane(cx)?;
7292 let follower_states = &self.follower_states;
7293 leader_border_for_pane(follower_states, &pane, window, cx)
7294 });
7295
7296 let mut container = div()
7297 .flex()
7298 .overflow_hidden()
7299 .flex_none()
7300 .child(dock.clone())
7301 .children(leader_border);
7302
7303 // Apply sizing only when the dock is open. When closed the dock is still
7304 // included in the element tree so its focus handle remains mounted — without
7305 // this, toggle_panel_focus cannot focus the panel when the dock is closed.
7306 let dock = dock.read(cx);
7307 if let Some(panel) = dock.visible_panel() {
7308 let size_state = dock.stored_panel_size_state(panel.as_ref());
7309 if position.axis() == Axis::Horizontal {
7310 if let Some(ratio) = size_state
7311 .and_then(|state| state.flexible_size_ratio)
7312 .or_else(|| self.default_flexible_dock_ratio(position))
7313 && panel.supports_flexible_size(window, cx)
7314 {
7315 let ratio = ratio.clamp(0.001, 0.999);
7316 let grow = ratio / (1.0 - ratio);
7317 let style = container.style();
7318 style.flex_grow = Some(grow);
7319 style.flex_shrink = Some(1.0);
7320 style.flex_basis = Some(relative(0.).into());
7321 } else {
7322 let size = size_state
7323 .and_then(|state| state.size)
7324 .unwrap_or_else(|| panel.default_size(window, cx));
7325 container = container.w(size);
7326 }
7327 } else {
7328 let size = size_state
7329 .and_then(|state| state.size)
7330 .unwrap_or_else(|| panel.default_size(window, cx));
7331 container = container.h(size);
7332 }
7333 }
7334
7335 Some(container)
7336 }
7337
7338 pub fn for_window(window: &Window, cx: &App) -> Option<Entity<Workspace>> {
7339 window
7340 .root::<MultiWorkspace>()
7341 .flatten()
7342 .map(|multi_workspace| multi_workspace.read(cx).workspace().clone())
7343 }
7344
7345 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
7346 self.zoomed.as_ref()
7347 }
7348
7349 pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
7350 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
7351 return;
7352 };
7353 let windows = cx.windows();
7354 let next_window =
7355 SystemWindowTabController::get_next_tab_group_window(cx, current_window_id).or_else(
7356 || {
7357 windows
7358 .iter()
7359 .cycle()
7360 .skip_while(|window| window.window_id() != current_window_id)
7361 .nth(1)
7362 },
7363 );
7364
7365 if let Some(window) = next_window {
7366 window
7367 .update(cx, |_, window, _| window.activate_window())
7368 .ok();
7369 }
7370 }
7371
7372 pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
7373 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
7374 return;
7375 };
7376 let windows = cx.windows();
7377 let prev_window =
7378 SystemWindowTabController::get_prev_tab_group_window(cx, current_window_id).or_else(
7379 || {
7380 windows
7381 .iter()
7382 .rev()
7383 .cycle()
7384 .skip_while(|window| window.window_id() != current_window_id)
7385 .nth(1)
7386 },
7387 );
7388
7389 if let Some(window) = prev_window {
7390 window
7391 .update(cx, |_, window, _| window.activate_window())
7392 .ok();
7393 }
7394 }
7395
7396 pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
7397 if cx.stop_active_drag(window) {
7398 } else if let Some((notification_id, _)) = self.notifications.pop() {
7399 dismiss_app_notification(¬ification_id, cx);
7400 } else {
7401 cx.propagate();
7402 }
7403 }
7404
7405 fn resize_dock(
7406 &mut self,
7407 dock_pos: DockPosition,
7408 new_size: Pixels,
7409 window: &mut Window,
7410 cx: &mut Context<Self>,
7411 ) {
7412 match dock_pos {
7413 DockPosition::Left => self.resize_left_dock(new_size, window, cx),
7414 DockPosition::Right => self.resize_right_dock(new_size, window, cx),
7415 DockPosition::Bottom => self.resize_bottom_dock(new_size, window, cx),
7416 }
7417 }
7418
7419 fn resize_left_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
7420 let workspace_width = self.bounds.size.width;
7421 let mut size = new_size.min(workspace_width - RESIZE_HANDLE_SIZE);
7422
7423 self.right_dock.read_with(cx, |right_dock, cx| {
7424 let right_dock_size = right_dock
7425 .stored_active_panel_size(window, cx)
7426 .unwrap_or(Pixels::ZERO);
7427 if right_dock_size + size > workspace_width {
7428 size = workspace_width - right_dock_size
7429 }
7430 });
7431
7432 let ratio = self.flexible_dock_ratio_for_size(DockPosition::Left, size, window, cx);
7433 self.left_dock.update(cx, |left_dock, cx| {
7434 if WorkspaceSettings::get_global(cx)
7435 .resize_all_panels_in_dock
7436 .contains(&DockPosition::Left)
7437 {
7438 left_dock.resize_all_panels(Some(size), ratio, window, cx);
7439 } else {
7440 left_dock.resize_active_panel(Some(size), ratio, window, cx);
7441 }
7442 });
7443 }
7444
7445 fn resize_right_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
7446 let workspace_width = self.bounds.size.width;
7447 let mut size = new_size.min(workspace_width - RESIZE_HANDLE_SIZE);
7448 self.left_dock.read_with(cx, |left_dock, cx| {
7449 let left_dock_size = left_dock
7450 .stored_active_panel_size(window, cx)
7451 .unwrap_or(Pixels::ZERO);
7452 if left_dock_size + size > workspace_width {
7453 size = workspace_width - left_dock_size
7454 }
7455 });
7456 let ratio = self.flexible_dock_ratio_for_size(DockPosition::Right, size, window, cx);
7457 self.right_dock.update(cx, |right_dock, cx| {
7458 if WorkspaceSettings::get_global(cx)
7459 .resize_all_panels_in_dock
7460 .contains(&DockPosition::Right)
7461 {
7462 right_dock.resize_all_panels(Some(size), ratio, window, cx);
7463 } else {
7464 right_dock.resize_active_panel(Some(size), ratio, window, cx);
7465 }
7466 });
7467 }
7468
7469 fn resize_bottom_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
7470 let size = new_size.min(self.bounds.bottom() - RESIZE_HANDLE_SIZE - self.bounds.top());
7471 self.bottom_dock.update(cx, |bottom_dock, cx| {
7472 if WorkspaceSettings::get_global(cx)
7473 .resize_all_panels_in_dock
7474 .contains(&DockPosition::Bottom)
7475 {
7476 bottom_dock.resize_all_panels(Some(size), None, window, cx);
7477 } else {
7478 bottom_dock.resize_active_panel(Some(size), None, window, cx);
7479 }
7480 });
7481 }
7482
7483 fn toggle_edit_predictions_all_files(
7484 &mut self,
7485 _: &ToggleEditPrediction,
7486 _window: &mut Window,
7487 cx: &mut Context<Self>,
7488 ) {
7489 let fs = self.project().read(cx).fs().clone();
7490 let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx);
7491 update_settings_file(fs, cx, move |file, _| {
7492 file.project.all_languages.defaults.show_edit_predictions = Some(!show_edit_predictions)
7493 });
7494 }
7495
7496 fn toggle_theme_mode(&mut self, _: &ToggleMode, _window: &mut Window, cx: &mut Context<Self>) {
7497 let current_mode = ThemeSettings::get_global(cx).theme.mode();
7498 let next_mode = match current_mode {
7499 Some(theme_settings::ThemeAppearanceMode::Light) => {
7500 theme_settings::ThemeAppearanceMode::Dark
7501 }
7502 Some(theme_settings::ThemeAppearanceMode::Dark) => {
7503 theme_settings::ThemeAppearanceMode::Light
7504 }
7505 Some(theme_settings::ThemeAppearanceMode::System) | None => {
7506 match cx.theme().appearance() {
7507 theme::Appearance::Light => theme_settings::ThemeAppearanceMode::Dark,
7508 theme::Appearance::Dark => theme_settings::ThemeAppearanceMode::Light,
7509 }
7510 }
7511 };
7512
7513 let fs = self.project().read(cx).fs().clone();
7514 settings::update_settings_file(fs, cx, move |settings, _cx| {
7515 theme_settings::set_mode(settings, next_mode);
7516 });
7517 }
7518
7519 pub fn show_worktree_trust_security_modal(
7520 &mut self,
7521 toggle: bool,
7522 window: &mut Window,
7523 cx: &mut Context<Self>,
7524 ) {
7525 if let Some(security_modal) = self.active_modal::<SecurityModal>(cx) {
7526 if toggle {
7527 security_modal.update(cx, |security_modal, cx| {
7528 security_modal.dismiss(cx);
7529 })
7530 } else {
7531 security_modal.update(cx, |security_modal, cx| {
7532 security_modal.refresh_restricted_paths(cx);
7533 });
7534 }
7535 } else {
7536 let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
7537 .map(|trusted_worktrees| {
7538 trusted_worktrees
7539 .read(cx)
7540 .has_restricted_worktrees(&self.project().read(cx).worktree_store(), cx)
7541 })
7542 .unwrap_or(false);
7543 if has_restricted_worktrees {
7544 let project = self.project().read(cx);
7545 let remote_host = project
7546 .remote_connection_options(cx)
7547 .map(RemoteHostLocation::from);
7548 let worktree_store = project.worktree_store().downgrade();
7549 self.toggle_modal(window, cx, |_, cx| {
7550 SecurityModal::new(worktree_store, remote_host, cx)
7551 });
7552 }
7553 }
7554 }
7555}
7556
7557pub trait AnyActiveCall {
7558 fn entity(&self) -> AnyEntity;
7559 fn is_in_room(&self, _: &App) -> bool;
7560 fn room_id(&self, _: &App) -> Option<u64>;
7561 fn channel_id(&self, _: &App) -> Option<ChannelId>;
7562 fn hang_up(&self, _: &mut App) -> Task<Result<()>>;
7563 fn unshare_project(&self, _: Entity<Project>, _: &mut App) -> Result<()>;
7564 fn remote_participant_for_peer_id(&self, _: PeerId, _: &App) -> Option<RemoteCollaborator>;
7565 fn is_sharing_project(&self, _: &App) -> bool;
7566 fn has_remote_participants(&self, _: &App) -> bool;
7567 fn local_participant_is_guest(&self, _: &App) -> bool;
7568 fn client(&self, _: &App) -> Arc<Client>;
7569 fn share_on_join(&self, _: &App) -> bool;
7570 fn join_channel(&self, _: ChannelId, _: &mut App) -> Task<Result<bool>>;
7571 fn room_update_completed(&self, _: &mut App) -> Task<()>;
7572 fn most_active_project(&self, _: &App) -> Option<(u64, u64)>;
7573 fn share_project(&self, _: Entity<Project>, _: &mut App) -> Task<Result<u64>>;
7574 fn join_project(
7575 &self,
7576 _: u64,
7577 _: Arc<LanguageRegistry>,
7578 _: Arc<dyn Fs>,
7579 _: &mut App,
7580 ) -> Task<Result<Entity<Project>>>;
7581 fn peer_id_for_user_in_room(&self, _: u64, _: &App) -> Option<PeerId>;
7582 fn subscribe(
7583 &self,
7584 _: &mut Window,
7585 _: &mut Context<Workspace>,
7586 _: Box<dyn Fn(&mut Workspace, &ActiveCallEvent, &mut Window, &mut Context<Workspace>)>,
7587 ) -> Subscription;
7588 fn create_shared_screen(
7589 &self,
7590 _: PeerId,
7591 _: &Entity<Pane>,
7592 _: &mut Window,
7593 _: &mut App,
7594 ) -> Option<Entity<SharedScreen>>;
7595}
7596
7597#[derive(Clone)]
7598pub struct GlobalAnyActiveCall(pub Arc<dyn AnyActiveCall>);
7599impl Global for GlobalAnyActiveCall {}
7600
7601impl GlobalAnyActiveCall {
7602 pub(crate) fn try_global(cx: &App) -> Option<&Self> {
7603 cx.try_global()
7604 }
7605
7606 pub(crate) fn global(cx: &App) -> &Self {
7607 cx.global()
7608 }
7609}
7610
7611pub fn merge_conflict_notification_id() -> NotificationId {
7612 struct MergeConflictNotification;
7613 NotificationId::unique::<MergeConflictNotification>()
7614}
7615
7616/// Workspace-local view of a remote participant's location.
7617#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7618pub enum ParticipantLocation {
7619 SharedProject { project_id: u64 },
7620 UnsharedProject,
7621 External,
7622}
7623
7624impl ParticipantLocation {
7625 pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
7626 match location
7627 .and_then(|l| l.variant)
7628 .context("participant location was not provided")?
7629 {
7630 proto::participant_location::Variant::SharedProject(project) => {
7631 Ok(Self::SharedProject {
7632 project_id: project.id,
7633 })
7634 }
7635 proto::participant_location::Variant::UnsharedProject(_) => Ok(Self::UnsharedProject),
7636 proto::participant_location::Variant::External(_) => Ok(Self::External),
7637 }
7638 }
7639}
7640/// Workspace-local view of a remote collaborator's state.
7641/// This is the subset of `call::RemoteParticipant` that workspace needs.
7642#[derive(Clone)]
7643pub struct RemoteCollaborator {
7644 pub user: Arc<User>,
7645 pub peer_id: PeerId,
7646 pub location: ParticipantLocation,
7647 pub participant_index: ParticipantIndex,
7648}
7649
7650pub enum ActiveCallEvent {
7651 ParticipantLocationChanged { participant_id: PeerId },
7652 RemoteVideoTracksChanged { participant_id: PeerId },
7653}
7654
7655fn leader_border_for_pane(
7656 follower_states: &HashMap<CollaboratorId, FollowerState>,
7657 pane: &Entity<Pane>,
7658 _: &Window,
7659 cx: &App,
7660) -> Option<Div> {
7661 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
7662 if state.pane() == pane {
7663 Some((*leader_id, state))
7664 } else {
7665 None
7666 }
7667 })?;
7668
7669 let mut leader_color = match leader_id {
7670 CollaboratorId::PeerId(leader_peer_id) => {
7671 let leader = GlobalAnyActiveCall::try_global(cx)?
7672 .0
7673 .remote_participant_for_peer_id(leader_peer_id, cx)?;
7674
7675 cx.theme()
7676 .players()
7677 .color_for_participant(leader.participant_index.0)
7678 .cursor
7679 }
7680 CollaboratorId::Agent => cx.theme().players().agent().cursor,
7681 };
7682 leader_color.fade_out(0.3);
7683 Some(
7684 div()
7685 .absolute()
7686 .size_full()
7687 .left_0()
7688 .top_0()
7689 .border_2()
7690 .border_color(leader_color),
7691 )
7692}
7693
7694fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
7695 ZED_WINDOW_POSITION
7696 .zip(*ZED_WINDOW_SIZE)
7697 .map(|(position, size)| Bounds {
7698 origin: position,
7699 size,
7700 })
7701}
7702
7703fn open_items(
7704 serialized_workspace: Option<SerializedWorkspace>,
7705 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
7706 window: &mut Window,
7707 cx: &mut Context<Workspace>,
7708) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
7709 let restored_items = serialized_workspace.map(|serialized_workspace| {
7710 Workspace::load_workspace(
7711 serialized_workspace,
7712 project_paths_to_open
7713 .iter()
7714 .map(|(_, project_path)| project_path)
7715 .cloned()
7716 .collect(),
7717 window,
7718 cx,
7719 )
7720 });
7721
7722 cx.spawn_in(window, async move |workspace, cx| {
7723 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
7724
7725 if let Some(restored_items) = restored_items {
7726 let restored_items = restored_items.await?;
7727
7728 let restored_project_paths = restored_items
7729 .iter()
7730 .filter_map(|item| {
7731 cx.update(|_, cx| item.as_ref()?.project_path(cx))
7732 .ok()
7733 .flatten()
7734 })
7735 .collect::<HashSet<_>>();
7736
7737 for restored_item in restored_items {
7738 opened_items.push(restored_item.map(Ok));
7739 }
7740
7741 project_paths_to_open
7742 .iter_mut()
7743 .for_each(|(_, project_path)| {
7744 if let Some(project_path_to_open) = project_path
7745 && restored_project_paths.contains(project_path_to_open)
7746 {
7747 *project_path = None;
7748 }
7749 });
7750 } else {
7751 for _ in 0..project_paths_to_open.len() {
7752 opened_items.push(None);
7753 }
7754 }
7755 assert!(opened_items.len() == project_paths_to_open.len());
7756
7757 let tasks =
7758 project_paths_to_open
7759 .into_iter()
7760 .enumerate()
7761 .map(|(ix, (abs_path, project_path))| {
7762 let workspace = workspace.clone();
7763 cx.spawn(async move |cx| {
7764 let file_project_path = project_path?;
7765 let abs_path_task = workspace.update(cx, |workspace, cx| {
7766 workspace.project().update(cx, |project, cx| {
7767 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
7768 })
7769 });
7770
7771 // We only want to open file paths here. If one of the items
7772 // here is a directory, it was already opened further above
7773 // with a `find_or_create_worktree`.
7774 if let Ok(task) = abs_path_task
7775 && task.await.is_none_or(|p| p.is_file())
7776 {
7777 return Some((
7778 ix,
7779 workspace
7780 .update_in(cx, |workspace, window, cx| {
7781 workspace.open_path(
7782 file_project_path,
7783 None,
7784 true,
7785 window,
7786 cx,
7787 )
7788 })
7789 .log_err()?
7790 .await,
7791 ));
7792 }
7793 None
7794 })
7795 });
7796
7797 let tasks = tasks.collect::<Vec<_>>();
7798
7799 let tasks = futures::future::join_all(tasks);
7800 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
7801 opened_items[ix] = Some(path_open_result);
7802 }
7803
7804 Ok(opened_items)
7805 })
7806}
7807
7808#[derive(Clone)]
7809enum ActivateInDirectionTarget {
7810 Pane(Entity<Pane>),
7811 Dock(Entity<Dock>),
7812 Sidebar(FocusHandle),
7813}
7814
7815fn notify_if_database_failed(window: WindowHandle<MultiWorkspace>, cx: &mut AsyncApp) {
7816 window
7817 .update(cx, |multi_workspace, _, cx| {
7818 let workspace = multi_workspace.workspace().clone();
7819 workspace.update(cx, |workspace, cx| {
7820 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
7821 struct DatabaseFailedNotification;
7822
7823 workspace.show_notification(
7824 NotificationId::unique::<DatabaseFailedNotification>(),
7825 cx,
7826 |cx| {
7827 cx.new(|cx| {
7828 MessageNotification::new("Failed to load the database file.", cx)
7829 .primary_message("File an Issue")
7830 .primary_icon(IconName::Plus)
7831 .primary_on_click(|window, cx| {
7832 window.dispatch_action(Box::new(FileBugReport), cx)
7833 })
7834 })
7835 },
7836 );
7837 }
7838 });
7839 })
7840 .log_err();
7841}
7842
7843fn px_with_ui_font_fallback(val: u32, cx: &Context<Workspace>) -> Pixels {
7844 if val == 0 {
7845 ThemeSettings::get_global(cx).ui_font_size(cx)
7846 } else {
7847 px(val as f32)
7848 }
7849}
7850
7851fn adjust_active_dock_size_by_px(
7852 px: Pixels,
7853 workspace: &mut Workspace,
7854 window: &mut Window,
7855 cx: &mut Context<Workspace>,
7856) {
7857 let Some(active_dock) = workspace
7858 .all_docks()
7859 .into_iter()
7860 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx))
7861 else {
7862 return;
7863 };
7864 let dock = active_dock.read(cx);
7865 let Some(panel_size) = workspace.dock_size(&dock, window, cx) else {
7866 return;
7867 };
7868 workspace.resize_dock(dock.position(), panel_size + px, window, cx);
7869}
7870
7871fn adjust_open_docks_size_by_px(
7872 px: Pixels,
7873 workspace: &mut Workspace,
7874 window: &mut Window,
7875 cx: &mut Context<Workspace>,
7876) {
7877 let docks = workspace
7878 .all_docks()
7879 .into_iter()
7880 .filter_map(|dock_entity| {
7881 let dock = dock_entity.read(cx);
7882 if dock.is_open() {
7883 let dock_pos = dock.position();
7884 let panel_size = workspace.dock_size(&dock, window, cx)?;
7885 Some((dock_pos, panel_size + px))
7886 } else {
7887 None
7888 }
7889 })
7890 .collect::<Vec<_>>();
7891
7892 for (position, new_size) in docks {
7893 workspace.resize_dock(position, new_size, window, cx);
7894 }
7895}
7896
7897impl Focusable for Workspace {
7898 fn focus_handle(&self, cx: &App) -> FocusHandle {
7899 self.active_pane.focus_handle(cx)
7900 }
7901}
7902
7903#[derive(Clone)]
7904struct DraggedDock(DockPosition);
7905
7906impl Render for DraggedDock {
7907 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
7908 gpui::Empty
7909 }
7910}
7911
7912impl Render for Workspace {
7913 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
7914 static FIRST_PAINT: AtomicBool = AtomicBool::new(true);
7915 if FIRST_PAINT.swap(false, std::sync::atomic::Ordering::Relaxed) {
7916 log::info!("Rendered first frame");
7917 }
7918
7919 let centered_layout = self.centered_layout
7920 && self.center.panes().len() == 1
7921 && self.active_item(cx).is_some();
7922 let render_padding = |size| {
7923 (size > 0.0).then(|| {
7924 div()
7925 .h_full()
7926 .w(relative(size))
7927 .bg(cx.theme().colors().editor_background)
7928 .border_color(cx.theme().colors().pane_group_border)
7929 })
7930 };
7931 let paddings = if centered_layout {
7932 let settings = WorkspaceSettings::get_global(cx).centered_layout;
7933 (
7934 render_padding(Self::adjust_padding(
7935 settings.left_padding.map(|padding| padding.0),
7936 )),
7937 render_padding(Self::adjust_padding(
7938 settings.right_padding.map(|padding| padding.0),
7939 )),
7940 )
7941 } else {
7942 (None, None)
7943 };
7944 let ui_font = theme_settings::setup_ui_font(window, cx);
7945
7946 let theme = cx.theme().clone();
7947 let colors = theme.colors();
7948 let notification_entities = self
7949 .notifications
7950 .iter()
7951 .map(|(_, notification)| notification.entity_id())
7952 .collect::<Vec<_>>();
7953 let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout;
7954
7955 div()
7956 .relative()
7957 .size_full()
7958 .flex()
7959 .flex_col()
7960 .font(ui_font)
7961 .gap_0()
7962 .justify_start()
7963 .items_start()
7964 .text_color(colors.text)
7965 .overflow_hidden()
7966 .children(self.titlebar_item.clone())
7967 .on_modifiers_changed(move |_, _, cx| {
7968 for &id in ¬ification_entities {
7969 cx.notify(id);
7970 }
7971 })
7972 .child(
7973 div()
7974 .size_full()
7975 .relative()
7976 .flex_1()
7977 .flex()
7978 .flex_col()
7979 .child(
7980 div()
7981 .id("workspace")
7982 .bg(colors.background)
7983 .relative()
7984 .flex_1()
7985 .w_full()
7986 .flex()
7987 .flex_col()
7988 .overflow_hidden()
7989 .border_t_1()
7990 .border_b_1()
7991 .border_color(colors.border)
7992 .child({
7993 let this = cx.entity();
7994 canvas(
7995 move |bounds, window, cx| {
7996 this.update(cx, |this, cx| {
7997 let bounds_changed = this.bounds != bounds;
7998 this.bounds = bounds;
7999
8000 if bounds_changed {
8001 this.left_dock.update(cx, |dock, cx| {
8002 dock.clamp_panel_size(
8003 bounds.size.width,
8004 window,
8005 cx,
8006 )
8007 });
8008
8009 this.right_dock.update(cx, |dock, cx| {
8010 dock.clamp_panel_size(
8011 bounds.size.width,
8012 window,
8013 cx,
8014 )
8015 });
8016
8017 this.bottom_dock.update(cx, |dock, cx| {
8018 dock.clamp_panel_size(
8019 bounds.size.height,
8020 window,
8021 cx,
8022 )
8023 });
8024 }
8025 })
8026 },
8027 |_, _, _, _| {},
8028 )
8029 .absolute()
8030 .size_full()
8031 })
8032 .when(self.zoomed.is_none(), |this| {
8033 this.on_drag_move(cx.listener(
8034 move |workspace,
8035 e: &DragMoveEvent<DraggedDock>,
8036 window,
8037 cx| {
8038 if workspace.previous_dock_drag_coordinates
8039 != Some(e.event.position)
8040 {
8041 workspace.previous_dock_drag_coordinates =
8042 Some(e.event.position);
8043
8044 match e.drag(cx).0 {
8045 DockPosition::Left => {
8046 workspace.resize_left_dock(
8047 e.event.position.x
8048 - workspace.bounds.left(),
8049 window,
8050 cx,
8051 );
8052 }
8053 DockPosition::Right => {
8054 workspace.resize_right_dock(
8055 workspace.bounds.right()
8056 - e.event.position.x,
8057 window,
8058 cx,
8059 );
8060 }
8061 DockPosition::Bottom => {
8062 workspace.resize_bottom_dock(
8063 workspace.bounds.bottom()
8064 - e.event.position.y,
8065 window,
8066 cx,
8067 );
8068 }
8069 };
8070 workspace.serialize_workspace(window, cx);
8071 }
8072 },
8073 ))
8074
8075 })
8076 .child({
8077 match bottom_dock_layout {
8078 BottomDockLayout::Full => div()
8079 .flex()
8080 .flex_col()
8081 .h_full()
8082 .child(
8083 div()
8084 .flex()
8085 .flex_row()
8086 .flex_1()
8087 .overflow_hidden()
8088 .children(self.render_dock(
8089 DockPosition::Left,
8090 &self.left_dock,
8091 window,
8092 cx,
8093 ))
8094
8095 .child(
8096 div()
8097 .flex()
8098 .flex_col()
8099 .flex_1()
8100 .overflow_hidden()
8101 .child(
8102 h_flex()
8103 .flex_1()
8104 .when_some(
8105 paddings.0,
8106 |this, p| {
8107 this.child(
8108 p.border_r_1(),
8109 )
8110 },
8111 )
8112 .child(self.center.render(
8113 self.zoomed.as_ref(),
8114 &PaneRenderContext {
8115 follower_states:
8116 &self.follower_states,
8117 active_call: self.active_call(),
8118 active_pane: &self.active_pane,
8119 app_state: &self.app_state,
8120 project: &self.project,
8121 workspace: &self.weak_self,
8122 },
8123 window,
8124 cx,
8125 ))
8126 .when_some(
8127 paddings.1,
8128 |this, p| {
8129 this.child(
8130 p.border_l_1(),
8131 )
8132 },
8133 ),
8134 ),
8135 )
8136
8137 .children(self.render_dock(
8138 DockPosition::Right,
8139 &self.right_dock,
8140 window,
8141 cx,
8142 )),
8143 )
8144 .child(div().w_full().children(self.render_dock(
8145 DockPosition::Bottom,
8146 &self.bottom_dock,
8147 window,
8148 cx
8149 ))),
8150
8151 BottomDockLayout::LeftAligned => div()
8152 .flex()
8153 .flex_row()
8154 .h_full()
8155 .child(
8156 div()
8157 .flex()
8158 .flex_col()
8159 .flex_1()
8160 .h_full()
8161 .child(
8162 div()
8163 .flex()
8164 .flex_row()
8165 .flex_1()
8166 .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx))
8167
8168 .child(
8169 div()
8170 .flex()
8171 .flex_col()
8172 .flex_1()
8173 .overflow_hidden()
8174 .child(
8175 h_flex()
8176 .flex_1()
8177 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
8178 .child(self.center.render(
8179 self.zoomed.as_ref(),
8180 &PaneRenderContext {
8181 follower_states:
8182 &self.follower_states,
8183 active_call: self.active_call(),
8184 active_pane: &self.active_pane,
8185 app_state: &self.app_state,
8186 project: &self.project,
8187 workspace: &self.weak_self,
8188 },
8189 window,
8190 cx,
8191 ))
8192 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
8193 )
8194 )
8195
8196 )
8197 .child(
8198 div()
8199 .w_full()
8200 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
8201 ),
8202 )
8203 .children(self.render_dock(
8204 DockPosition::Right,
8205 &self.right_dock,
8206 window,
8207 cx,
8208 )),
8209 BottomDockLayout::RightAligned => div()
8210 .flex()
8211 .flex_row()
8212 .h_full()
8213 .children(self.render_dock(
8214 DockPosition::Left,
8215 &self.left_dock,
8216 window,
8217 cx,
8218 ))
8219
8220 .child(
8221 div()
8222 .flex()
8223 .flex_col()
8224 .flex_1()
8225 .h_full()
8226 .child(
8227 div()
8228 .flex()
8229 .flex_row()
8230 .flex_1()
8231 .child(
8232 div()
8233 .flex()
8234 .flex_col()
8235 .flex_1()
8236 .overflow_hidden()
8237 .child(
8238 h_flex()
8239 .flex_1()
8240 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
8241 .child(self.center.render(
8242 self.zoomed.as_ref(),
8243 &PaneRenderContext {
8244 follower_states:
8245 &self.follower_states,
8246 active_call: self.active_call(),
8247 active_pane: &self.active_pane,
8248 app_state: &self.app_state,
8249 project: &self.project,
8250 workspace: &self.weak_self,
8251 },
8252 window,
8253 cx,
8254 ))
8255 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
8256 )
8257 )
8258
8259 .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx))
8260 )
8261 .child(
8262 div()
8263 .w_full()
8264 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
8265 ),
8266 ),
8267 BottomDockLayout::Contained => div()
8268 .flex()
8269 .flex_row()
8270 .h_full()
8271 .children(self.render_dock(
8272 DockPosition::Left,
8273 &self.left_dock,
8274 window,
8275 cx,
8276 ))
8277
8278 .child(
8279 div()
8280 .flex()
8281 .flex_col()
8282 .flex_1()
8283 .overflow_hidden()
8284 .child(
8285 h_flex()
8286 .flex_1()
8287 .when_some(paddings.0, |this, p| {
8288 this.child(p.border_r_1())
8289 })
8290 .child(self.center.render(
8291 self.zoomed.as_ref(),
8292 &PaneRenderContext {
8293 follower_states:
8294 &self.follower_states,
8295 active_call: self.active_call(),
8296 active_pane: &self.active_pane,
8297 app_state: &self.app_state,
8298 project: &self.project,
8299 workspace: &self.weak_self,
8300 },
8301 window,
8302 cx,
8303 ))
8304 .when_some(paddings.1, |this, p| {
8305 this.child(p.border_l_1())
8306 }),
8307 )
8308 .children(self.render_dock(
8309 DockPosition::Bottom,
8310 &self.bottom_dock,
8311 window,
8312 cx,
8313 )),
8314 )
8315
8316 .children(self.render_dock(
8317 DockPosition::Right,
8318 &self.right_dock,
8319 window,
8320 cx,
8321 )),
8322 }
8323 })
8324 .children(self.zoomed.as_ref().and_then(|view| {
8325 let zoomed_view = view.upgrade()?;
8326 let div = div()
8327 .occlude()
8328 .absolute()
8329 .overflow_hidden()
8330 .border_color(colors.border)
8331 .bg(colors.background)
8332 .child(zoomed_view)
8333 .inset_0()
8334 .shadow_lg();
8335
8336 if !WorkspaceSettings::get_global(cx).zoomed_padding {
8337 return Some(div);
8338 }
8339
8340 Some(match self.zoomed_position {
8341 Some(DockPosition::Left) => div.right_2().border_r_1(),
8342 Some(DockPosition::Right) => div.left_2().border_l_1(),
8343 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
8344 None => {
8345 div.top_2().bottom_2().left_2().right_2().border_1()
8346 }
8347 })
8348 }))
8349 .children(self.render_notifications(window, cx)),
8350 )
8351 .when(self.status_bar_visible(cx), |parent| {
8352 parent.child(self.status_bar.clone())
8353 })
8354 .child(self.toast_layer.clone()),
8355 )
8356 }
8357}
8358
8359impl WorkspaceStore {
8360 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
8361 Self {
8362 workspaces: Default::default(),
8363 _subscriptions: vec![
8364 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
8365 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
8366 ],
8367 client,
8368 }
8369 }
8370
8371 pub fn update_followers(
8372 &self,
8373 project_id: Option<u64>,
8374 update: proto::update_followers::Variant,
8375 cx: &App,
8376 ) -> Option<()> {
8377 let active_call = GlobalAnyActiveCall::try_global(cx)?;
8378 let room_id = active_call.0.room_id(cx)?;
8379 self.client
8380 .send(proto::UpdateFollowers {
8381 room_id,
8382 project_id,
8383 variant: Some(update),
8384 })
8385 .log_err()
8386 }
8387
8388 pub async fn handle_follow(
8389 this: Entity<Self>,
8390 envelope: TypedEnvelope<proto::Follow>,
8391 mut cx: AsyncApp,
8392 ) -> Result<proto::FollowResponse> {
8393 this.update(&mut cx, |this, cx| {
8394 let follower = Follower {
8395 project_id: envelope.payload.project_id,
8396 peer_id: envelope.original_sender_id()?,
8397 };
8398
8399 let mut response = proto::FollowResponse::default();
8400
8401 this.workspaces.retain(|(window_handle, weak_workspace)| {
8402 let Some(workspace) = weak_workspace.upgrade() else {
8403 return false;
8404 };
8405 window_handle
8406 .update(cx, |_, window, cx| {
8407 workspace.update(cx, |workspace, cx| {
8408 let handler_response =
8409 workspace.handle_follow(follower.project_id, window, cx);
8410 if let Some(active_view) = handler_response.active_view
8411 && workspace.project.read(cx).remote_id() == follower.project_id
8412 {
8413 response.active_view = Some(active_view)
8414 }
8415 });
8416 })
8417 .is_ok()
8418 });
8419
8420 Ok(response)
8421 })
8422 }
8423
8424 async fn handle_update_followers(
8425 this: Entity<Self>,
8426 envelope: TypedEnvelope<proto::UpdateFollowers>,
8427 mut cx: AsyncApp,
8428 ) -> Result<()> {
8429 let leader_id = envelope.original_sender_id()?;
8430 let update = envelope.payload;
8431
8432 this.update(&mut cx, |this, cx| {
8433 this.workspaces.retain(|(window_handle, weak_workspace)| {
8434 let Some(workspace) = weak_workspace.upgrade() else {
8435 return false;
8436 };
8437 window_handle
8438 .update(cx, |_, window, cx| {
8439 workspace.update(cx, |workspace, cx| {
8440 let project_id = workspace.project.read(cx).remote_id();
8441 if update.project_id != project_id && update.project_id.is_some() {
8442 return;
8443 }
8444 workspace.handle_update_followers(
8445 leader_id,
8446 update.clone(),
8447 window,
8448 cx,
8449 );
8450 });
8451 })
8452 .is_ok()
8453 });
8454 Ok(())
8455 })
8456 }
8457
8458 pub fn workspaces(&self) -> impl Iterator<Item = &WeakEntity<Workspace>> {
8459 self.workspaces.iter().map(|(_, weak)| weak)
8460 }
8461
8462 pub fn workspaces_with_windows(
8463 &self,
8464 ) -> impl Iterator<Item = (gpui::AnyWindowHandle, &WeakEntity<Workspace>)> {
8465 self.workspaces.iter().map(|(window, weak)| (*window, weak))
8466 }
8467}
8468
8469impl ViewId {
8470 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
8471 Ok(Self {
8472 creator: message
8473 .creator
8474 .map(CollaboratorId::PeerId)
8475 .context("creator is missing")?,
8476 id: message.id,
8477 })
8478 }
8479
8480 pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
8481 if let CollaboratorId::PeerId(peer_id) = self.creator {
8482 Some(proto::ViewId {
8483 creator: Some(peer_id),
8484 id: self.id,
8485 })
8486 } else {
8487 None
8488 }
8489 }
8490}
8491
8492impl FollowerState {
8493 fn pane(&self) -> &Entity<Pane> {
8494 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
8495 }
8496}
8497
8498pub trait WorkspaceHandle {
8499 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
8500}
8501
8502impl WorkspaceHandle for Entity<Workspace> {
8503 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
8504 self.read(cx)
8505 .worktrees(cx)
8506 .flat_map(|worktree| {
8507 let worktree_id = worktree.read(cx).id();
8508 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
8509 worktree_id,
8510 path: f.path.clone(),
8511 })
8512 })
8513 .collect::<Vec<_>>()
8514 }
8515}
8516
8517pub async fn last_opened_workspace_location(
8518 db: &WorkspaceDb,
8519 fs: &dyn fs::Fs,
8520) -> Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)> {
8521 db.last_workspace(fs)
8522 .await
8523 .log_err()
8524 .flatten()
8525 .map(|(id, location, paths, _timestamp)| (id, location, paths))
8526}
8527
8528pub async fn last_session_workspace_locations(
8529 db: &WorkspaceDb,
8530 last_session_id: &str,
8531 last_session_window_stack: Option<Vec<WindowId>>,
8532 fs: &dyn fs::Fs,
8533) -> Option<Vec<SessionWorkspace>> {
8534 db.last_session_workspace_locations(last_session_id, last_session_window_stack, fs)
8535 .await
8536 .log_err()
8537}
8538
8539pub struct MultiWorkspaceRestoreResult {
8540 pub window_handle: WindowHandle<MultiWorkspace>,
8541 pub errors: Vec<anyhow::Error>,
8542}
8543
8544pub async fn restore_multiworkspace(
8545 multi_workspace: SerializedMultiWorkspace,
8546 app_state: Arc<AppState>,
8547 cx: &mut AsyncApp,
8548) -> anyhow::Result<MultiWorkspaceRestoreResult> {
8549 let SerializedMultiWorkspace { workspaces, state } = multi_workspace;
8550 let mut group_iter = workspaces.into_iter();
8551 let first = group_iter
8552 .next()
8553 .context("window group must not be empty")?;
8554
8555 let window_handle = if first.paths.is_empty() {
8556 cx.update(|cx| open_workspace_by_id(first.workspace_id, app_state.clone(), None, cx))
8557 .await?
8558 } else {
8559 let OpenResult { window, .. } = cx
8560 .update(|cx| {
8561 Workspace::new_local(
8562 first.paths.paths().to_vec(),
8563 app_state.clone(),
8564 None,
8565 None,
8566 None,
8567 true,
8568 cx,
8569 )
8570 })
8571 .await?;
8572 window
8573 };
8574
8575 let mut errors = Vec::new();
8576
8577 for session_workspace in group_iter {
8578 let error = if session_workspace.paths.is_empty() {
8579 cx.update(|cx| {
8580 open_workspace_by_id(
8581 session_workspace.workspace_id,
8582 app_state.clone(),
8583 Some(window_handle),
8584 cx,
8585 )
8586 })
8587 .await
8588 .err()
8589 } else {
8590 cx.update(|cx| {
8591 Workspace::new_local(
8592 session_workspace.paths.paths().to_vec(),
8593 app_state.clone(),
8594 Some(window_handle),
8595 None,
8596 None,
8597 false,
8598 cx,
8599 )
8600 })
8601 .await
8602 .err()
8603 };
8604
8605 if let Some(error) = error {
8606 errors.push(error);
8607 }
8608 }
8609
8610 if let Some(target_id) = state.active_workspace_id {
8611 window_handle
8612 .update(cx, |multi_workspace, window, cx| {
8613 let target_index = multi_workspace
8614 .workspaces()
8615 .iter()
8616 .position(|ws| ws.read(cx).database_id() == Some(target_id));
8617 if let Some(index) = target_index {
8618 multi_workspace.activate_index(index, window, cx);
8619 } else if !multi_workspace.workspaces().is_empty() {
8620 multi_workspace.activate_index(0, window, cx);
8621 }
8622 })
8623 .ok();
8624 } else {
8625 window_handle
8626 .update(cx, |multi_workspace, window, cx| {
8627 if !multi_workspace.workspaces().is_empty() {
8628 multi_workspace.activate_index(0, window, cx);
8629 }
8630 })
8631 .ok();
8632 }
8633
8634 if state.sidebar_open {
8635 window_handle
8636 .update(cx, |multi_workspace, _, cx| {
8637 multi_workspace.open_sidebar(cx);
8638 })
8639 .ok();
8640 }
8641
8642 window_handle
8643 .update(cx, |_, window, _cx| {
8644 window.activate_window();
8645 })
8646 .ok();
8647
8648 Ok(MultiWorkspaceRestoreResult {
8649 window_handle,
8650 errors,
8651 })
8652}
8653
8654actions!(
8655 collab,
8656 [
8657 /// Opens the channel notes for the current call.
8658 ///
8659 /// Use `collab_panel::OpenSelectedChannelNotes` to open the channel notes for the selected
8660 /// channel in the collab panel.
8661 ///
8662 /// If you want to open a specific channel, use `zed::OpenZedUrl` with a channel notes URL -
8663 /// can be copied via "Copy link to section" in the context menu of the channel notes
8664 /// buffer. These URLs look like `https://zed.dev/channel/channel-name-CHANNEL_ID/notes`.
8665 OpenChannelNotes,
8666 /// Mutes your microphone.
8667 Mute,
8668 /// Deafens yourself (mute both microphone and speakers).
8669 Deafen,
8670 /// Leaves the current call.
8671 LeaveCall,
8672 /// Shares the current project with collaborators.
8673 ShareProject,
8674 /// Shares your screen with collaborators.
8675 ScreenShare,
8676 /// Copies the current room name and session id for debugging purposes.
8677 CopyRoomId,
8678 ]
8679);
8680
8681/// Opens the channel notes for a specific channel by its ID.
8682#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
8683#[action(namespace = collab)]
8684#[serde(deny_unknown_fields)]
8685pub struct OpenChannelNotesById {
8686 pub channel_id: u64,
8687}
8688
8689actions!(
8690 zed,
8691 [
8692 /// Opens the Zed log file.
8693 OpenLog,
8694 /// Reveals the Zed log file in the system file manager.
8695 RevealLogInFileManager
8696 ]
8697);
8698
8699async fn join_channel_internal(
8700 channel_id: ChannelId,
8701 app_state: &Arc<AppState>,
8702 requesting_window: Option<WindowHandle<MultiWorkspace>>,
8703 requesting_workspace: Option<WeakEntity<Workspace>>,
8704 active_call: &dyn AnyActiveCall,
8705 cx: &mut AsyncApp,
8706) -> Result<bool> {
8707 let (should_prompt, already_in_channel) = cx.update(|cx| {
8708 if !active_call.is_in_room(cx) {
8709 return (false, false);
8710 }
8711
8712 let already_in_channel = active_call.channel_id(cx) == Some(channel_id);
8713 let should_prompt = active_call.is_sharing_project(cx)
8714 && active_call.has_remote_participants(cx)
8715 && !already_in_channel;
8716 (should_prompt, already_in_channel)
8717 });
8718
8719 if already_in_channel {
8720 let task = cx.update(|cx| {
8721 if let Some((project, host)) = active_call.most_active_project(cx) {
8722 Some(join_in_room_project(project, host, app_state.clone(), cx))
8723 } else {
8724 None
8725 }
8726 });
8727 if let Some(task) = task {
8728 task.await?;
8729 }
8730 return anyhow::Ok(true);
8731 }
8732
8733 if should_prompt {
8734 if let Some(multi_workspace) = requesting_window {
8735 let answer = multi_workspace
8736 .update(cx, |_, window, cx| {
8737 window.prompt(
8738 PromptLevel::Warning,
8739 "Do you want to switch channels?",
8740 Some("Leaving this call will unshare your current project."),
8741 &["Yes, Join Channel", "Cancel"],
8742 cx,
8743 )
8744 })?
8745 .await;
8746
8747 if answer == Ok(1) {
8748 return Ok(false);
8749 }
8750 } else {
8751 return Ok(false);
8752 }
8753 }
8754
8755 let client = cx.update(|cx| active_call.client(cx));
8756
8757 let mut client_status = client.status();
8758
8759 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
8760 'outer: loop {
8761 let Some(status) = client_status.recv().await else {
8762 anyhow::bail!("error connecting");
8763 };
8764
8765 match status {
8766 Status::Connecting
8767 | Status::Authenticating
8768 | Status::Authenticated
8769 | Status::Reconnecting
8770 | Status::Reauthenticating
8771 | Status::Reauthenticated => continue,
8772 Status::Connected { .. } => break 'outer,
8773 Status::SignedOut | Status::AuthenticationError => {
8774 return Err(ErrorCode::SignedOut.into());
8775 }
8776 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
8777 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
8778 return Err(ErrorCode::Disconnected.into());
8779 }
8780 }
8781 }
8782
8783 let joined = cx
8784 .update(|cx| active_call.join_channel(channel_id, cx))
8785 .await?;
8786
8787 if !joined {
8788 return anyhow::Ok(true);
8789 }
8790
8791 cx.update(|cx| active_call.room_update_completed(cx)).await;
8792
8793 let task = cx.update(|cx| {
8794 if let Some((project, host)) = active_call.most_active_project(cx) {
8795 return Some(join_in_room_project(project, host, app_state.clone(), cx));
8796 }
8797
8798 // If you are the first to join a channel, see if you should share your project.
8799 if !active_call.has_remote_participants(cx)
8800 && !active_call.local_participant_is_guest(cx)
8801 && let Some(workspace) = requesting_workspace.as_ref().and_then(|w| w.upgrade())
8802 {
8803 let project = workspace.update(cx, |workspace, cx| {
8804 let project = workspace.project.read(cx);
8805
8806 if !active_call.share_on_join(cx) {
8807 return None;
8808 }
8809
8810 if (project.is_local() || project.is_via_remote_server())
8811 && project.visible_worktrees(cx).any(|tree| {
8812 tree.read(cx)
8813 .root_entry()
8814 .is_some_and(|entry| entry.is_dir())
8815 })
8816 {
8817 Some(workspace.project.clone())
8818 } else {
8819 None
8820 }
8821 });
8822 if let Some(project) = project {
8823 let share_task = active_call.share_project(project, cx);
8824 return Some(cx.spawn(async move |_cx| -> Result<()> {
8825 share_task.await?;
8826 Ok(())
8827 }));
8828 }
8829 }
8830
8831 None
8832 });
8833 if let Some(task) = task {
8834 task.await?;
8835 return anyhow::Ok(true);
8836 }
8837 anyhow::Ok(false)
8838}
8839
8840pub fn join_channel(
8841 channel_id: ChannelId,
8842 app_state: Arc<AppState>,
8843 requesting_window: Option<WindowHandle<MultiWorkspace>>,
8844 requesting_workspace: Option<WeakEntity<Workspace>>,
8845 cx: &mut App,
8846) -> Task<Result<()>> {
8847 let active_call = GlobalAnyActiveCall::global(cx).clone();
8848 cx.spawn(async move |cx| {
8849 let result = join_channel_internal(
8850 channel_id,
8851 &app_state,
8852 requesting_window,
8853 requesting_workspace,
8854 &*active_call.0,
8855 cx,
8856 )
8857 .await;
8858
8859 // join channel succeeded, and opened a window
8860 if matches!(result, Ok(true)) {
8861 return anyhow::Ok(());
8862 }
8863
8864 // find an existing workspace to focus and show call controls
8865 let mut active_window = requesting_window.or_else(|| activate_any_workspace_window(cx));
8866 if active_window.is_none() {
8867 // no open workspaces, make one to show the error in (blergh)
8868 let OpenResult {
8869 window: window_handle,
8870 ..
8871 } = cx
8872 .update(|cx| {
8873 Workspace::new_local(
8874 vec![],
8875 app_state.clone(),
8876 requesting_window,
8877 None,
8878 None,
8879 true,
8880 cx,
8881 )
8882 })
8883 .await?;
8884
8885 window_handle
8886 .update(cx, |_, window, _cx| {
8887 window.activate_window();
8888 })
8889 .ok();
8890
8891 if result.is_ok() {
8892 cx.update(|cx| {
8893 cx.dispatch_action(&OpenChannelNotes);
8894 });
8895 }
8896
8897 active_window = Some(window_handle);
8898 }
8899
8900 if let Err(err) = result {
8901 log::error!("failed to join channel: {}", err);
8902 if let Some(active_window) = active_window {
8903 active_window
8904 .update(cx, |_, window, cx| {
8905 let detail: SharedString = match err.error_code() {
8906 ErrorCode::SignedOut => "Please sign in to continue.".into(),
8907 ErrorCode::UpgradeRequired => concat!(
8908 "Your are running an unsupported version of Zed. ",
8909 "Please update to continue."
8910 )
8911 .into(),
8912 ErrorCode::NoSuchChannel => concat!(
8913 "No matching channel was found. ",
8914 "Please check the link and try again."
8915 )
8916 .into(),
8917 ErrorCode::Forbidden => concat!(
8918 "This channel is private, and you do not have access. ",
8919 "Please ask someone to add you and try again."
8920 )
8921 .into(),
8922 ErrorCode::Disconnected => {
8923 "Please check your internet connection and try again.".into()
8924 }
8925 _ => format!("{}\n\nPlease try again.", err).into(),
8926 };
8927 window.prompt(
8928 PromptLevel::Critical,
8929 "Failed to join channel",
8930 Some(&detail),
8931 &["Ok"],
8932 cx,
8933 )
8934 })?
8935 .await
8936 .ok();
8937 }
8938 }
8939
8940 // return ok, we showed the error to the user.
8941 anyhow::Ok(())
8942 })
8943}
8944
8945pub async fn get_any_active_multi_workspace(
8946 app_state: Arc<AppState>,
8947 mut cx: AsyncApp,
8948) -> anyhow::Result<WindowHandle<MultiWorkspace>> {
8949 // find an existing workspace to focus and show call controls
8950 let active_window = activate_any_workspace_window(&mut cx);
8951 if active_window.is_none() {
8952 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, None, true, cx))
8953 .await?;
8954 }
8955 activate_any_workspace_window(&mut cx).context("could not open zed")
8956}
8957
8958fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<MultiWorkspace>> {
8959 cx.update(|cx| {
8960 if let Some(workspace_window) = cx
8961 .active_window()
8962 .and_then(|window| window.downcast::<MultiWorkspace>())
8963 {
8964 return Some(workspace_window);
8965 }
8966
8967 for window in cx.windows() {
8968 if let Some(workspace_window) = window.downcast::<MultiWorkspace>() {
8969 workspace_window
8970 .update(cx, |_, window, _| window.activate_window())
8971 .ok();
8972 return Some(workspace_window);
8973 }
8974 }
8975 None
8976 })
8977}
8978
8979pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<MultiWorkspace>> {
8980 workspace_windows_for_location(&SerializedWorkspaceLocation::Local, cx)
8981}
8982
8983pub fn workspace_windows_for_location(
8984 serialized_location: &SerializedWorkspaceLocation,
8985 cx: &App,
8986) -> Vec<WindowHandle<MultiWorkspace>> {
8987 cx.windows()
8988 .into_iter()
8989 .filter_map(|window| window.downcast::<MultiWorkspace>())
8990 .filter(|multi_workspace| {
8991 let same_host = |left: &RemoteConnectionOptions, right: &RemoteConnectionOptions| match (left, right) {
8992 (RemoteConnectionOptions::Ssh(a), RemoteConnectionOptions::Ssh(b)) => {
8993 (&a.host, &a.username, &a.port) == (&b.host, &b.username, &b.port)
8994 }
8995 (RemoteConnectionOptions::Wsl(a), RemoteConnectionOptions::Wsl(b)) => {
8996 // The WSL username is not consistently populated in the workspace location, so ignore it for now.
8997 a.distro_name == b.distro_name
8998 }
8999 (RemoteConnectionOptions::Docker(a), RemoteConnectionOptions::Docker(b)) => {
9000 a.container_id == b.container_id
9001 }
9002 #[cfg(any(test, feature = "test-support"))]
9003 (RemoteConnectionOptions::Mock(a), RemoteConnectionOptions::Mock(b)) => {
9004 a.id == b.id
9005 }
9006 _ => false,
9007 };
9008
9009 multi_workspace.read(cx).is_ok_and(|multi_workspace| {
9010 multi_workspace.workspaces().iter().any(|workspace| {
9011 match workspace.read(cx).workspace_location(cx) {
9012 WorkspaceLocation::Location(location, _) => {
9013 match (&location, serialized_location) {
9014 (
9015 SerializedWorkspaceLocation::Local,
9016 SerializedWorkspaceLocation::Local,
9017 ) => true,
9018 (
9019 SerializedWorkspaceLocation::Remote(a),
9020 SerializedWorkspaceLocation::Remote(b),
9021 ) => same_host(a, b),
9022 _ => false,
9023 }
9024 }
9025 _ => false,
9026 }
9027 })
9028 })
9029 })
9030 .collect()
9031}
9032
9033pub async fn find_existing_workspace(
9034 abs_paths: &[PathBuf],
9035 open_options: &OpenOptions,
9036 location: &SerializedWorkspaceLocation,
9037 cx: &mut AsyncApp,
9038) -> (
9039 Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)>,
9040 OpenVisible,
9041) {
9042 let mut existing: Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)> = None;
9043 let mut open_visible = OpenVisible::All;
9044 let mut best_match = None;
9045
9046 if open_options.open_new_workspace != Some(true) {
9047 cx.update(|cx| {
9048 for window in workspace_windows_for_location(location, cx) {
9049 if let Ok(multi_workspace) = window.read(cx) {
9050 for workspace in multi_workspace.workspaces() {
9051 let project = workspace.read(cx).project.read(cx);
9052 let m = project.visibility_for_paths(
9053 abs_paths,
9054 open_options.open_new_workspace == None,
9055 cx,
9056 );
9057 if m > best_match {
9058 existing = Some((window, workspace.clone()));
9059 best_match = m;
9060 } else if best_match.is_none()
9061 && open_options.open_new_workspace == Some(false)
9062 {
9063 existing = Some((window, workspace.clone()))
9064 }
9065 }
9066 }
9067 }
9068 });
9069
9070 let all_paths_are_files = existing
9071 .as_ref()
9072 .and_then(|(_, target_workspace)| {
9073 cx.update(|cx| {
9074 let workspace = target_workspace.read(cx);
9075 let project = workspace.project.read(cx);
9076 let path_style = workspace.path_style(cx);
9077 Some(!abs_paths.iter().any(|path| {
9078 let path = util::paths::SanitizedPath::new(path);
9079 project.worktrees(cx).any(|worktree| {
9080 let worktree = worktree.read(cx);
9081 let abs_path = worktree.abs_path();
9082 path_style
9083 .strip_prefix(path.as_ref(), abs_path.as_ref())
9084 .and_then(|rel| worktree.entry_for_path(&rel))
9085 .is_some_and(|e| e.is_dir())
9086 })
9087 }))
9088 })
9089 })
9090 .unwrap_or(false);
9091
9092 if open_options.open_new_workspace.is_none()
9093 && existing.is_some()
9094 && open_options.wait
9095 && all_paths_are_files
9096 {
9097 cx.update(|cx| {
9098 let windows = workspace_windows_for_location(location, cx);
9099 let window = cx
9100 .active_window()
9101 .and_then(|window| window.downcast::<MultiWorkspace>())
9102 .filter(|window| windows.contains(window))
9103 .or_else(|| windows.into_iter().next());
9104 if let Some(window) = window {
9105 if let Ok(multi_workspace) = window.read(cx) {
9106 let active_workspace = multi_workspace.workspace().clone();
9107 existing = Some((window, active_workspace));
9108 open_visible = OpenVisible::None;
9109 }
9110 }
9111 });
9112 }
9113 }
9114 (existing, open_visible)
9115}
9116
9117#[derive(Default, Clone)]
9118pub struct OpenOptions {
9119 pub visible: Option<OpenVisible>,
9120 pub focus: Option<bool>,
9121 pub open_new_workspace: Option<bool>,
9122 pub wait: bool,
9123 pub replace_window: Option<WindowHandle<MultiWorkspace>>,
9124 pub env: Option<HashMap<String, String>>,
9125}
9126
9127/// The result of opening a workspace via [`open_paths`], [`Workspace::new_local`],
9128/// or [`Workspace::open_workspace_for_paths`].
9129pub struct OpenResult {
9130 pub window: WindowHandle<MultiWorkspace>,
9131 pub workspace: Entity<Workspace>,
9132 pub opened_items: Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
9133}
9134
9135/// Opens a workspace by its database ID, used for restoring empty workspaces with unsaved content.
9136pub fn open_workspace_by_id(
9137 workspace_id: WorkspaceId,
9138 app_state: Arc<AppState>,
9139 requesting_window: Option<WindowHandle<MultiWorkspace>>,
9140 cx: &mut App,
9141) -> Task<anyhow::Result<WindowHandle<MultiWorkspace>>> {
9142 let project_handle = Project::local(
9143 app_state.client.clone(),
9144 app_state.node_runtime.clone(),
9145 app_state.user_store.clone(),
9146 app_state.languages.clone(),
9147 app_state.fs.clone(),
9148 None,
9149 project::LocalProjectFlags {
9150 init_worktree_trust: true,
9151 ..project::LocalProjectFlags::default()
9152 },
9153 cx,
9154 );
9155
9156 let db = WorkspaceDb::global(cx);
9157 let kvp = db::kvp::KeyValueStore::global(cx);
9158 cx.spawn(async move |cx| {
9159 let serialized_workspace = db
9160 .workspace_for_id(workspace_id)
9161 .with_context(|| format!("Workspace {workspace_id:?} not found"))?;
9162
9163 let centered_layout = serialized_workspace.centered_layout;
9164
9165 let (window, workspace) = if let Some(window) = requesting_window {
9166 let workspace = window.update(cx, |multi_workspace, window, cx| {
9167 let workspace = cx.new(|cx| {
9168 let mut workspace = Workspace::new(
9169 Some(workspace_id),
9170 project_handle.clone(),
9171 app_state.clone(),
9172 window,
9173 cx,
9174 );
9175 workspace.centered_layout = centered_layout;
9176 workspace
9177 });
9178 multi_workspace.add_workspace(workspace.clone(), cx);
9179 workspace
9180 })?;
9181 (window, workspace)
9182 } else {
9183 let window_bounds_override = window_bounds_env_override();
9184
9185 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
9186 (Some(WindowBounds::Windowed(bounds)), None)
9187 } else if let Some(display) = serialized_workspace.display
9188 && let Some(bounds) = serialized_workspace.window_bounds.as_ref()
9189 {
9190 (Some(bounds.0), Some(display))
9191 } else if let Some((display, bounds)) = persistence::read_default_window_bounds(&kvp) {
9192 (Some(bounds), Some(display))
9193 } else {
9194 (None, None)
9195 };
9196
9197 let options = cx.update(|cx| {
9198 let mut options = (app_state.build_window_options)(display, cx);
9199 options.window_bounds = window_bounds;
9200 options
9201 });
9202
9203 let window = cx.open_window(options, {
9204 let app_state = app_state.clone();
9205 let project_handle = project_handle.clone();
9206 move |window, cx| {
9207 let workspace = cx.new(|cx| {
9208 let mut workspace = Workspace::new(
9209 Some(workspace_id),
9210 project_handle,
9211 app_state,
9212 window,
9213 cx,
9214 );
9215 workspace.centered_layout = centered_layout;
9216 workspace
9217 });
9218 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
9219 }
9220 })?;
9221
9222 let workspace = window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
9223 multi_workspace.workspace().clone()
9224 })?;
9225
9226 (window, workspace)
9227 };
9228
9229 notify_if_database_failed(window, cx);
9230
9231 // Restore items from the serialized workspace
9232 window
9233 .update(cx, |_, window, cx| {
9234 workspace.update(cx, |_workspace, cx| {
9235 open_items(Some(serialized_workspace), vec![], window, cx)
9236 })
9237 })?
9238 .await?;
9239
9240 window.update(cx, |_, window, cx| {
9241 workspace.update(cx, |workspace, cx| {
9242 workspace.serialize_workspace(window, cx);
9243 });
9244 })?;
9245
9246 Ok(window)
9247 })
9248}
9249
9250#[allow(clippy::type_complexity)]
9251pub fn open_paths(
9252 abs_paths: &[PathBuf],
9253 app_state: Arc<AppState>,
9254 open_options: OpenOptions,
9255 cx: &mut App,
9256) -> Task<anyhow::Result<OpenResult>> {
9257 let abs_paths = abs_paths.to_vec();
9258 #[cfg(target_os = "windows")]
9259 let wsl_path = abs_paths
9260 .iter()
9261 .find_map(|p| util::paths::WslPath::from_path(p));
9262
9263 cx.spawn(async move |cx| {
9264 let (mut existing, mut open_visible) = find_existing_workspace(
9265 &abs_paths,
9266 &open_options,
9267 &SerializedWorkspaceLocation::Local,
9268 cx,
9269 )
9270 .await;
9271
9272 // Fallback: if no workspace contains the paths and all paths are files,
9273 // prefer an existing local workspace window (active window first).
9274 if open_options.open_new_workspace.is_none() && existing.is_none() {
9275 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
9276 let all_metadatas = futures::future::join_all(all_paths)
9277 .await
9278 .into_iter()
9279 .filter_map(|result| result.ok().flatten())
9280 .collect::<Vec<_>>();
9281
9282 if all_metadatas.iter().all(|file| !file.is_dir) {
9283 cx.update(|cx| {
9284 let windows = workspace_windows_for_location(
9285 &SerializedWorkspaceLocation::Local,
9286 cx,
9287 );
9288 let window = cx
9289 .active_window()
9290 .and_then(|window| window.downcast::<MultiWorkspace>())
9291 .filter(|window| windows.contains(window))
9292 .or_else(|| windows.into_iter().next());
9293 if let Some(window) = window {
9294 if let Ok(multi_workspace) = window.read(cx) {
9295 let active_workspace = multi_workspace.workspace().clone();
9296 existing = Some((window, active_workspace));
9297 open_visible = OpenVisible::None;
9298 }
9299 }
9300 });
9301 }
9302 }
9303
9304 let result = if let Some((existing, target_workspace)) = existing {
9305 let open_task = existing
9306 .update(cx, |multi_workspace, window, cx| {
9307 window.activate_window();
9308 multi_workspace.activate(target_workspace.clone(), cx);
9309 target_workspace.update(cx, |workspace, cx| {
9310 workspace.open_paths(
9311 abs_paths,
9312 OpenOptions {
9313 visible: Some(open_visible),
9314 ..Default::default()
9315 },
9316 None,
9317 window,
9318 cx,
9319 )
9320 })
9321 })?
9322 .await;
9323
9324 _ = existing.update(cx, |multi_workspace, _, cx| {
9325 let workspace = multi_workspace.workspace().clone();
9326 workspace.update(cx, |workspace, cx| {
9327 for item in open_task.iter().flatten() {
9328 if let Err(e) = item {
9329 workspace.show_error(&e, cx);
9330 }
9331 }
9332 });
9333 });
9334
9335 Ok(OpenResult { window: existing, workspace: target_workspace, opened_items: open_task })
9336 } else {
9337 let result = cx
9338 .update(move |cx| {
9339 Workspace::new_local(
9340 abs_paths,
9341 app_state.clone(),
9342 open_options.replace_window,
9343 open_options.env,
9344 None,
9345 true,
9346 cx,
9347 )
9348 })
9349 .await;
9350
9351 if let Ok(ref result) = result {
9352 result.window
9353 .update(cx, |_, window, _cx| {
9354 window.activate_window();
9355 })
9356 .log_err();
9357 }
9358
9359 result
9360 };
9361
9362 #[cfg(target_os = "windows")]
9363 if let Some(util::paths::WslPath{distro, path}) = wsl_path
9364 && let Ok(ref result) = result
9365 {
9366 result.window
9367 .update(cx, move |multi_workspace, _window, cx| {
9368 struct OpenInWsl;
9369 let workspace = multi_workspace.workspace().clone();
9370 workspace.update(cx, |workspace, cx| {
9371 workspace.show_notification(NotificationId::unique::<OpenInWsl>(), cx, move |cx| {
9372 let display_path = util::markdown::MarkdownInlineCode(&path.to_string_lossy());
9373 let msg = format!("{display_path} is inside a WSL filesystem, some features may not work unless you open it with WSL remote");
9374 cx.new(move |cx| {
9375 MessageNotification::new(msg, cx)
9376 .primary_message("Open in WSL")
9377 .primary_icon(IconName::FolderOpen)
9378 .primary_on_click(move |window, cx| {
9379 window.dispatch_action(Box::new(remote::OpenWslPath {
9380 distro: remote::WslConnectionOptions {
9381 distro_name: distro.clone(),
9382 user: None,
9383 },
9384 paths: vec![path.clone().into()],
9385 }), cx)
9386 })
9387 })
9388 });
9389 });
9390 })
9391 .unwrap();
9392 };
9393 result
9394 })
9395}
9396
9397pub fn open_new(
9398 open_options: OpenOptions,
9399 app_state: Arc<AppState>,
9400 cx: &mut App,
9401 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
9402) -> Task<anyhow::Result<()>> {
9403 let task = Workspace::new_local(
9404 Vec::new(),
9405 app_state,
9406 open_options.replace_window,
9407 open_options.env,
9408 Some(Box::new(init)),
9409 true,
9410 cx,
9411 );
9412 cx.spawn(async move |cx| {
9413 let OpenResult { window, .. } = task.await?;
9414 window
9415 .update(cx, |_, window, _cx| {
9416 window.activate_window();
9417 })
9418 .ok();
9419 Ok(())
9420 })
9421}
9422
9423pub fn create_and_open_local_file(
9424 path: &'static Path,
9425 window: &mut Window,
9426 cx: &mut Context<Workspace>,
9427 default_content: impl 'static + Send + FnOnce() -> Rope,
9428) -> Task<Result<Box<dyn ItemHandle>>> {
9429 cx.spawn_in(window, async move |workspace, cx| {
9430 let fs = workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
9431 if !fs.is_file(path).await {
9432 fs.create_file(path, Default::default()).await?;
9433 fs.save(path, &default_content(), Default::default())
9434 .await?;
9435 }
9436
9437 workspace
9438 .update_in(cx, |workspace, window, cx| {
9439 workspace.with_local_or_wsl_workspace(window, cx, |workspace, window, cx| {
9440 let path = workspace
9441 .project
9442 .read_with(cx, |project, cx| project.try_windows_path_to_wsl(path, cx));
9443 cx.spawn_in(window, async move |workspace, cx| {
9444 let path = path.await?;
9445
9446 let path = fs.canonicalize(&path).await.unwrap_or(path);
9447
9448 let mut items = workspace
9449 .update_in(cx, |workspace, window, cx| {
9450 workspace.open_paths(
9451 vec![path.to_path_buf()],
9452 OpenOptions {
9453 visible: Some(OpenVisible::None),
9454 ..Default::default()
9455 },
9456 None,
9457 window,
9458 cx,
9459 )
9460 })?
9461 .await;
9462 let item = items.pop().flatten();
9463 item.with_context(|| format!("path {path:?} is not a file"))?
9464 })
9465 })
9466 })?
9467 .await?
9468 .await
9469 })
9470}
9471
9472pub fn open_remote_project_with_new_connection(
9473 window: WindowHandle<MultiWorkspace>,
9474 remote_connection: Arc<dyn RemoteConnection>,
9475 cancel_rx: oneshot::Receiver<()>,
9476 delegate: Arc<dyn RemoteClientDelegate>,
9477 app_state: Arc<AppState>,
9478 paths: Vec<PathBuf>,
9479 cx: &mut App,
9480) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
9481 cx.spawn(async move |cx| {
9482 let (workspace_id, serialized_workspace) =
9483 deserialize_remote_project(remote_connection.connection_options(), paths.clone(), cx)
9484 .await?;
9485
9486 let session = match cx
9487 .update(|cx| {
9488 remote::RemoteClient::new(
9489 ConnectionIdentifier::Workspace(workspace_id.0),
9490 remote_connection,
9491 cancel_rx,
9492 delegate,
9493 cx,
9494 )
9495 })
9496 .await?
9497 {
9498 Some(result) => result,
9499 None => return Ok(Vec::new()),
9500 };
9501
9502 let project = cx.update(|cx| {
9503 project::Project::remote(
9504 session,
9505 app_state.client.clone(),
9506 app_state.node_runtime.clone(),
9507 app_state.user_store.clone(),
9508 app_state.languages.clone(),
9509 app_state.fs.clone(),
9510 true,
9511 cx,
9512 )
9513 });
9514
9515 open_remote_project_inner(
9516 project,
9517 paths,
9518 workspace_id,
9519 serialized_workspace,
9520 app_state,
9521 window,
9522 cx,
9523 )
9524 .await
9525 })
9526}
9527
9528pub fn open_remote_project_with_existing_connection(
9529 connection_options: RemoteConnectionOptions,
9530 project: Entity<Project>,
9531 paths: Vec<PathBuf>,
9532 app_state: Arc<AppState>,
9533 window: WindowHandle<MultiWorkspace>,
9534 cx: &mut AsyncApp,
9535) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
9536 cx.spawn(async move |cx| {
9537 let (workspace_id, serialized_workspace) =
9538 deserialize_remote_project(connection_options.clone(), paths.clone(), cx).await?;
9539
9540 open_remote_project_inner(
9541 project,
9542 paths,
9543 workspace_id,
9544 serialized_workspace,
9545 app_state,
9546 window,
9547 cx,
9548 )
9549 .await
9550 })
9551}
9552
9553async fn open_remote_project_inner(
9554 project: Entity<Project>,
9555 paths: Vec<PathBuf>,
9556 workspace_id: WorkspaceId,
9557 serialized_workspace: Option<SerializedWorkspace>,
9558 app_state: Arc<AppState>,
9559 window: WindowHandle<MultiWorkspace>,
9560 cx: &mut AsyncApp,
9561) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
9562 let db = cx.update(|cx| WorkspaceDb::global(cx));
9563 let toolchains = db.toolchains(workspace_id).await?;
9564 for (toolchain, worktree_path, path) in toolchains {
9565 project
9566 .update(cx, |this, cx| {
9567 let Some(worktree_id) =
9568 this.find_worktree(&worktree_path, cx)
9569 .and_then(|(worktree, rel_path)| {
9570 if rel_path.is_empty() {
9571 Some(worktree.read(cx).id())
9572 } else {
9573 None
9574 }
9575 })
9576 else {
9577 return Task::ready(None);
9578 };
9579
9580 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
9581 })
9582 .await;
9583 }
9584 let mut project_paths_to_open = vec![];
9585 let mut project_path_errors = vec![];
9586
9587 for path in paths {
9588 let result = cx
9589 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))
9590 .await;
9591 match result {
9592 Ok((_, project_path)) => {
9593 project_paths_to_open.push((path.clone(), Some(project_path)));
9594 }
9595 Err(error) => {
9596 project_path_errors.push(error);
9597 }
9598 };
9599 }
9600
9601 if project_paths_to_open.is_empty() {
9602 return Err(project_path_errors.pop().context("no paths given")?);
9603 }
9604
9605 let workspace = window.update(cx, |multi_workspace, window, cx| {
9606 telemetry::event!("SSH Project Opened");
9607
9608 let new_workspace = cx.new(|cx| {
9609 let mut workspace =
9610 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
9611 workspace.update_history(cx);
9612
9613 if let Some(ref serialized) = serialized_workspace {
9614 workspace.centered_layout = serialized.centered_layout;
9615 }
9616
9617 workspace
9618 });
9619
9620 multi_workspace.activate(new_workspace.clone(), cx);
9621 new_workspace
9622 })?;
9623
9624 let items = window
9625 .update(cx, |_, window, cx| {
9626 window.activate_window();
9627 workspace.update(cx, |_workspace, cx| {
9628 open_items(serialized_workspace, project_paths_to_open, window, cx)
9629 })
9630 })?
9631 .await?;
9632
9633 workspace.update(cx, |workspace, cx| {
9634 for error in project_path_errors {
9635 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
9636 if let Some(path) = error.error_tag("path") {
9637 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
9638 }
9639 } else {
9640 workspace.show_error(&error, cx)
9641 }
9642 }
9643 });
9644
9645 Ok(items.into_iter().map(|item| item?.ok()).collect())
9646}
9647
9648fn deserialize_remote_project(
9649 connection_options: RemoteConnectionOptions,
9650 paths: Vec<PathBuf>,
9651 cx: &AsyncApp,
9652) -> Task<Result<(WorkspaceId, Option<SerializedWorkspace>)>> {
9653 let db = cx.update(|cx| WorkspaceDb::global(cx));
9654 cx.background_spawn(async move {
9655 let remote_connection_id = db
9656 .get_or_create_remote_connection(connection_options)
9657 .await?;
9658
9659 let serialized_workspace = db.remote_workspace_for_roots(&paths, remote_connection_id);
9660
9661 let workspace_id = if let Some(workspace_id) =
9662 serialized_workspace.as_ref().map(|workspace| workspace.id)
9663 {
9664 workspace_id
9665 } else {
9666 db.next_id().await?
9667 };
9668
9669 Ok((workspace_id, serialized_workspace))
9670 })
9671}
9672
9673pub fn join_in_room_project(
9674 project_id: u64,
9675 follow_user_id: u64,
9676 app_state: Arc<AppState>,
9677 cx: &mut App,
9678) -> Task<Result<()>> {
9679 let windows = cx.windows();
9680 cx.spawn(async move |cx| {
9681 let existing_window_and_workspace: Option<(
9682 WindowHandle<MultiWorkspace>,
9683 Entity<Workspace>,
9684 )> = windows.into_iter().find_map(|window_handle| {
9685 window_handle
9686 .downcast::<MultiWorkspace>()
9687 .and_then(|window_handle| {
9688 window_handle
9689 .update(cx, |multi_workspace, _window, cx| {
9690 for workspace in multi_workspace.workspaces() {
9691 if workspace.read(cx).project().read(cx).remote_id()
9692 == Some(project_id)
9693 {
9694 return Some((window_handle, workspace.clone()));
9695 }
9696 }
9697 None
9698 })
9699 .unwrap_or(None)
9700 })
9701 });
9702
9703 let multi_workspace_window = if let Some((existing_window, target_workspace)) =
9704 existing_window_and_workspace
9705 {
9706 existing_window
9707 .update(cx, |multi_workspace, _, cx| {
9708 multi_workspace.activate(target_workspace, cx);
9709 })
9710 .ok();
9711 existing_window
9712 } else {
9713 let active_call = cx.update(|cx| GlobalAnyActiveCall::global(cx).clone());
9714 let project = cx
9715 .update(|cx| {
9716 active_call.0.join_project(
9717 project_id,
9718 app_state.languages.clone(),
9719 app_state.fs.clone(),
9720 cx,
9721 )
9722 })
9723 .await?;
9724
9725 let window_bounds_override = window_bounds_env_override();
9726 cx.update(|cx| {
9727 let mut options = (app_state.build_window_options)(None, cx);
9728 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
9729 cx.open_window(options, |window, cx| {
9730 let workspace = cx.new(|cx| {
9731 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
9732 });
9733 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
9734 })
9735 })?
9736 };
9737
9738 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
9739 cx.activate(true);
9740 window.activate_window();
9741
9742 // We set the active workspace above, so this is the correct workspace.
9743 let workspace = multi_workspace.workspace().clone();
9744 workspace.update(cx, |workspace, cx| {
9745 let follow_peer_id = GlobalAnyActiveCall::try_global(cx)
9746 .and_then(|call| call.0.peer_id_for_user_in_room(follow_user_id, cx))
9747 .or_else(|| {
9748 // If we couldn't follow the given user, follow the host instead.
9749 let collaborator = workspace
9750 .project()
9751 .read(cx)
9752 .collaborators()
9753 .values()
9754 .find(|collaborator| collaborator.is_host)?;
9755 Some(collaborator.peer_id)
9756 });
9757
9758 if let Some(follow_peer_id) = follow_peer_id {
9759 workspace.follow(follow_peer_id, window, cx);
9760 }
9761 });
9762 })?;
9763
9764 anyhow::Ok(())
9765 })
9766}
9767
9768pub fn reload(cx: &mut App) {
9769 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
9770 let mut workspace_windows = cx
9771 .windows()
9772 .into_iter()
9773 .filter_map(|window| window.downcast::<MultiWorkspace>())
9774 .collect::<Vec<_>>();
9775
9776 // If multiple windows have unsaved changes, and need a save prompt,
9777 // prompt in the active window before switching to a different window.
9778 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
9779
9780 let mut prompt = None;
9781 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
9782 prompt = window
9783 .update(cx, |_, window, cx| {
9784 window.prompt(
9785 PromptLevel::Info,
9786 "Are you sure you want to restart?",
9787 None,
9788 &["Restart", "Cancel"],
9789 cx,
9790 )
9791 })
9792 .ok();
9793 }
9794
9795 cx.spawn(async move |cx| {
9796 if let Some(prompt) = prompt {
9797 let answer = prompt.await?;
9798 if answer != 0 {
9799 return anyhow::Ok(());
9800 }
9801 }
9802
9803 // If the user cancels any save prompt, then keep the app open.
9804 for window in workspace_windows {
9805 if let Ok(should_close) = window.update(cx, |multi_workspace, window, cx| {
9806 let workspace = multi_workspace.workspace().clone();
9807 workspace.update(cx, |workspace, cx| {
9808 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
9809 })
9810 }) && !should_close.await?
9811 {
9812 return anyhow::Ok(());
9813 }
9814 }
9815 cx.update(|cx| cx.restart());
9816 anyhow::Ok(())
9817 })
9818 .detach_and_log_err(cx);
9819}
9820
9821fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
9822 let mut parts = value.split(',');
9823 let x: usize = parts.next()?.parse().ok()?;
9824 let y: usize = parts.next()?.parse().ok()?;
9825 Some(point(px(x as f32), px(y as f32)))
9826}
9827
9828fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
9829 let mut parts = value.split(',');
9830 let width: usize = parts.next()?.parse().ok()?;
9831 let height: usize = parts.next()?.parse().ok()?;
9832 Some(size(px(width as f32), px(height as f32)))
9833}
9834
9835/// Add client-side decorations (rounded corners, shadows, resize handling) when
9836/// appropriate.
9837///
9838/// The `border_radius_tiling` parameter allows overriding which corners get
9839/// rounded, independently of the actual window tiling state. This is used
9840/// specifically for the workspace switcher sidebar: when the sidebar is open,
9841/// we want square corners on the left (so the sidebar appears flush with the
9842/// window edge) but we still need the shadow padding for proper visual
9843/// appearance. Unlike actual window tiling, this only affects border radius -
9844/// not padding or shadows.
9845pub fn client_side_decorations(
9846 element: impl IntoElement,
9847 window: &mut Window,
9848 cx: &mut App,
9849 border_radius_tiling: Tiling,
9850) -> Stateful<Div> {
9851 const BORDER_SIZE: Pixels = px(1.0);
9852 let decorations = window.window_decorations();
9853 let tiling = match decorations {
9854 Decorations::Server => Tiling::default(),
9855 Decorations::Client { tiling } => tiling,
9856 };
9857
9858 match decorations {
9859 Decorations::Client { .. } => window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW),
9860 Decorations::Server => window.set_client_inset(px(0.0)),
9861 }
9862
9863 struct GlobalResizeEdge(ResizeEdge);
9864 impl Global for GlobalResizeEdge {}
9865
9866 div()
9867 .id("window-backdrop")
9868 .bg(transparent_black())
9869 .map(|div| match decorations {
9870 Decorations::Server => div,
9871 Decorations::Client { .. } => div
9872 .when(
9873 !(tiling.top
9874 || tiling.right
9875 || border_radius_tiling.top
9876 || border_radius_tiling.right),
9877 |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9878 )
9879 .when(
9880 !(tiling.top
9881 || tiling.left
9882 || border_radius_tiling.top
9883 || border_radius_tiling.left),
9884 |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9885 )
9886 .when(
9887 !(tiling.bottom
9888 || tiling.right
9889 || border_radius_tiling.bottom
9890 || border_radius_tiling.right),
9891 |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9892 )
9893 .when(
9894 !(tiling.bottom
9895 || tiling.left
9896 || border_radius_tiling.bottom
9897 || border_radius_tiling.left),
9898 |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9899 )
9900 .when(!tiling.top, |div| {
9901 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
9902 })
9903 .when(!tiling.bottom, |div| {
9904 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
9905 })
9906 .when(!tiling.left, |div| {
9907 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
9908 })
9909 .when(!tiling.right, |div| {
9910 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
9911 })
9912 .on_mouse_move(move |e, window, cx| {
9913 let size = window.window_bounds().get_bounds().size;
9914 let pos = e.position;
9915
9916 let new_edge =
9917 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
9918
9919 let edge = cx.try_global::<GlobalResizeEdge>();
9920 if new_edge != edge.map(|edge| edge.0) {
9921 window
9922 .window_handle()
9923 .update(cx, |workspace, _, cx| {
9924 cx.notify(workspace.entity_id());
9925 })
9926 .ok();
9927 }
9928 })
9929 .on_mouse_down(MouseButton::Left, move |e, window, _| {
9930 let size = window.window_bounds().get_bounds().size;
9931 let pos = e.position;
9932
9933 let edge = match resize_edge(
9934 pos,
9935 theme::CLIENT_SIDE_DECORATION_SHADOW,
9936 size,
9937 tiling,
9938 ) {
9939 Some(value) => value,
9940 None => return,
9941 };
9942
9943 window.start_window_resize(edge);
9944 }),
9945 })
9946 .size_full()
9947 .child(
9948 div()
9949 .cursor(CursorStyle::Arrow)
9950 .map(|div| match decorations {
9951 Decorations::Server => div,
9952 Decorations::Client { .. } => div
9953 .border_color(cx.theme().colors().border)
9954 .when(
9955 !(tiling.top
9956 || tiling.right
9957 || border_radius_tiling.top
9958 || border_radius_tiling.right),
9959 |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9960 )
9961 .when(
9962 !(tiling.top
9963 || tiling.left
9964 || border_radius_tiling.top
9965 || border_radius_tiling.left),
9966 |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9967 )
9968 .when(
9969 !(tiling.bottom
9970 || tiling.right
9971 || border_radius_tiling.bottom
9972 || border_radius_tiling.right),
9973 |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9974 )
9975 .when(
9976 !(tiling.bottom
9977 || tiling.left
9978 || border_radius_tiling.bottom
9979 || border_radius_tiling.left),
9980 |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9981 )
9982 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
9983 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
9984 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
9985 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
9986 .when(!tiling.is_tiled(), |div| {
9987 div.shadow(vec![gpui::BoxShadow {
9988 color: Hsla {
9989 h: 0.,
9990 s: 0.,
9991 l: 0.,
9992 a: 0.4,
9993 },
9994 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
9995 spread_radius: px(0.),
9996 offset: point(px(0.0), px(0.0)),
9997 }])
9998 }),
9999 })
10000 .on_mouse_move(|_e, _, cx| {
10001 cx.stop_propagation();
10002 })
10003 .size_full()
10004 .child(element),
10005 )
10006 .map(|div| match decorations {
10007 Decorations::Server => div,
10008 Decorations::Client { tiling, .. } => div.child(
10009 canvas(
10010 |_bounds, window, _| {
10011 window.insert_hitbox(
10012 Bounds::new(
10013 point(px(0.0), px(0.0)),
10014 window.window_bounds().get_bounds().size,
10015 ),
10016 HitboxBehavior::Normal,
10017 )
10018 },
10019 move |_bounds, hitbox, window, cx| {
10020 let mouse = window.mouse_position();
10021 let size = window.window_bounds().get_bounds().size;
10022 let Some(edge) =
10023 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
10024 else {
10025 return;
10026 };
10027 cx.set_global(GlobalResizeEdge(edge));
10028 window.set_cursor_style(
10029 match edge {
10030 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
10031 ResizeEdge::Left | ResizeEdge::Right => {
10032 CursorStyle::ResizeLeftRight
10033 }
10034 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
10035 CursorStyle::ResizeUpLeftDownRight
10036 }
10037 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
10038 CursorStyle::ResizeUpRightDownLeft
10039 }
10040 },
10041 &hitbox,
10042 );
10043 },
10044 )
10045 .size_full()
10046 .absolute(),
10047 ),
10048 })
10049}
10050
10051fn resize_edge(
10052 pos: Point<Pixels>,
10053 shadow_size: Pixels,
10054 window_size: Size<Pixels>,
10055 tiling: Tiling,
10056) -> Option<ResizeEdge> {
10057 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
10058 if bounds.contains(&pos) {
10059 return None;
10060 }
10061
10062 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
10063 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
10064 if !tiling.top && top_left_bounds.contains(&pos) {
10065 return Some(ResizeEdge::TopLeft);
10066 }
10067
10068 let top_right_bounds = Bounds::new(
10069 Point::new(window_size.width - corner_size.width, px(0.)),
10070 corner_size,
10071 );
10072 if !tiling.top && top_right_bounds.contains(&pos) {
10073 return Some(ResizeEdge::TopRight);
10074 }
10075
10076 let bottom_left_bounds = Bounds::new(
10077 Point::new(px(0.), window_size.height - corner_size.height),
10078 corner_size,
10079 );
10080 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
10081 return Some(ResizeEdge::BottomLeft);
10082 }
10083
10084 let bottom_right_bounds = Bounds::new(
10085 Point::new(
10086 window_size.width - corner_size.width,
10087 window_size.height - corner_size.height,
10088 ),
10089 corner_size,
10090 );
10091 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
10092 return Some(ResizeEdge::BottomRight);
10093 }
10094
10095 if !tiling.top && pos.y < shadow_size {
10096 Some(ResizeEdge::Top)
10097 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
10098 Some(ResizeEdge::Bottom)
10099 } else if !tiling.left && pos.x < shadow_size {
10100 Some(ResizeEdge::Left)
10101 } else if !tiling.right && pos.x > window_size.width - shadow_size {
10102 Some(ResizeEdge::Right)
10103 } else {
10104 None
10105 }
10106}
10107
10108fn join_pane_into_active(
10109 active_pane: &Entity<Pane>,
10110 pane: &Entity<Pane>,
10111 window: &mut Window,
10112 cx: &mut App,
10113) {
10114 if pane == active_pane {
10115 } else if pane.read(cx).items_len() == 0 {
10116 pane.update(cx, |_, cx| {
10117 cx.emit(pane::Event::Remove {
10118 focus_on_pane: None,
10119 });
10120 })
10121 } else {
10122 move_all_items(pane, active_pane, window, cx);
10123 }
10124}
10125
10126fn move_all_items(
10127 from_pane: &Entity<Pane>,
10128 to_pane: &Entity<Pane>,
10129 window: &mut Window,
10130 cx: &mut App,
10131) {
10132 let destination_is_different = from_pane != to_pane;
10133 let mut moved_items = 0;
10134 for (item_ix, item_handle) in from_pane
10135 .read(cx)
10136 .items()
10137 .enumerate()
10138 .map(|(ix, item)| (ix, item.clone()))
10139 .collect::<Vec<_>>()
10140 {
10141 let ix = item_ix - moved_items;
10142 if destination_is_different {
10143 // Close item from previous pane
10144 from_pane.update(cx, |source, cx| {
10145 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
10146 });
10147 moved_items += 1;
10148 }
10149
10150 // This automatically removes duplicate items in the pane
10151 to_pane.update(cx, |destination, cx| {
10152 destination.add_item(item_handle, true, true, None, window, cx);
10153 window.focus(&destination.focus_handle(cx), cx)
10154 });
10155 }
10156}
10157
10158pub fn move_item(
10159 source: &Entity<Pane>,
10160 destination: &Entity<Pane>,
10161 item_id_to_move: EntityId,
10162 destination_index: usize,
10163 activate: bool,
10164 window: &mut Window,
10165 cx: &mut App,
10166) {
10167 let Some((item_ix, item_handle)) = source
10168 .read(cx)
10169 .items()
10170 .enumerate()
10171 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
10172 .map(|(ix, item)| (ix, item.clone()))
10173 else {
10174 // Tab was closed during drag
10175 return;
10176 };
10177
10178 if source != destination {
10179 // Close item from previous pane
10180 source.update(cx, |source, cx| {
10181 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
10182 });
10183 }
10184
10185 // This automatically removes duplicate items in the pane
10186 destination.update(cx, |destination, cx| {
10187 destination.add_item_inner(
10188 item_handle,
10189 activate,
10190 activate,
10191 activate,
10192 Some(destination_index),
10193 window,
10194 cx,
10195 );
10196 if activate {
10197 window.focus(&destination.focus_handle(cx), cx)
10198 }
10199 });
10200}
10201
10202pub fn move_active_item(
10203 source: &Entity<Pane>,
10204 destination: &Entity<Pane>,
10205 focus_destination: bool,
10206 close_if_empty: bool,
10207 window: &mut Window,
10208 cx: &mut App,
10209) {
10210 if source == destination {
10211 return;
10212 }
10213 let Some(active_item) = source.read(cx).active_item() else {
10214 return;
10215 };
10216 source.update(cx, |source_pane, cx| {
10217 let item_id = active_item.item_id();
10218 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
10219 destination.update(cx, |target_pane, cx| {
10220 target_pane.add_item(
10221 active_item,
10222 focus_destination,
10223 focus_destination,
10224 Some(target_pane.items_len()),
10225 window,
10226 cx,
10227 );
10228 });
10229 });
10230}
10231
10232pub fn clone_active_item(
10233 workspace_id: Option<WorkspaceId>,
10234 source: &Entity<Pane>,
10235 destination: &Entity<Pane>,
10236 focus_destination: bool,
10237 window: &mut Window,
10238 cx: &mut App,
10239) {
10240 if source == destination {
10241 return;
10242 }
10243 let Some(active_item) = source.read(cx).active_item() else {
10244 return;
10245 };
10246 if !active_item.can_split(cx) {
10247 return;
10248 }
10249 let destination = destination.downgrade();
10250 let task = active_item.clone_on_split(workspace_id, window, cx);
10251 window
10252 .spawn(cx, async move |cx| {
10253 let Some(clone) = task.await else {
10254 return;
10255 };
10256 destination
10257 .update_in(cx, |target_pane, window, cx| {
10258 target_pane.add_item(
10259 clone,
10260 focus_destination,
10261 focus_destination,
10262 Some(target_pane.items_len()),
10263 window,
10264 cx,
10265 );
10266 })
10267 .log_err();
10268 })
10269 .detach();
10270}
10271
10272#[derive(Debug)]
10273pub struct WorkspacePosition {
10274 pub window_bounds: Option<WindowBounds>,
10275 pub display: Option<Uuid>,
10276 pub centered_layout: bool,
10277}
10278
10279pub fn remote_workspace_position_from_db(
10280 connection_options: RemoteConnectionOptions,
10281 paths_to_open: &[PathBuf],
10282 cx: &App,
10283) -> Task<Result<WorkspacePosition>> {
10284 let paths = paths_to_open.to_vec();
10285 let db = WorkspaceDb::global(cx);
10286 let kvp = db::kvp::KeyValueStore::global(cx);
10287
10288 cx.background_spawn(async move {
10289 let remote_connection_id = db
10290 .get_or_create_remote_connection(connection_options)
10291 .await
10292 .context("fetching serialized ssh project")?;
10293 let serialized_workspace = db.remote_workspace_for_roots(&paths, remote_connection_id);
10294
10295 let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
10296 (Some(WindowBounds::Windowed(bounds)), None)
10297 } else {
10298 let restorable_bounds = serialized_workspace
10299 .as_ref()
10300 .and_then(|workspace| {
10301 Some((workspace.display?, workspace.window_bounds.map(|b| b.0)?))
10302 })
10303 .or_else(|| persistence::read_default_window_bounds(&kvp));
10304
10305 if let Some((serialized_display, serialized_bounds)) = restorable_bounds {
10306 (Some(serialized_bounds), Some(serialized_display))
10307 } else {
10308 (None, None)
10309 }
10310 };
10311
10312 let centered_layout = serialized_workspace
10313 .as_ref()
10314 .map(|w| w.centered_layout)
10315 .unwrap_or(false);
10316
10317 Ok(WorkspacePosition {
10318 window_bounds,
10319 display,
10320 centered_layout,
10321 })
10322 })
10323}
10324
10325pub fn with_active_or_new_workspace(
10326 cx: &mut App,
10327 f: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + 'static,
10328) {
10329 match cx
10330 .active_window()
10331 .and_then(|w| w.downcast::<MultiWorkspace>())
10332 {
10333 Some(multi_workspace) => {
10334 cx.defer(move |cx| {
10335 multi_workspace
10336 .update(cx, |multi_workspace, window, cx| {
10337 let workspace = multi_workspace.workspace().clone();
10338 workspace.update(cx, |workspace, cx| f(workspace, window, cx));
10339 })
10340 .log_err();
10341 });
10342 }
10343 None => {
10344 let app_state = AppState::global(cx);
10345 open_new(
10346 OpenOptions::default(),
10347 app_state,
10348 cx,
10349 move |workspace, window, cx| f(workspace, window, cx),
10350 )
10351 .detach_and_log_err(cx);
10352 }
10353 }
10354}
10355
10356/// Reads a panel's pixel size from its legacy KVP format and deletes the legacy
10357/// key. This migration path only runs once per panel per workspace.
10358fn load_legacy_panel_size(
10359 panel_key: &str,
10360 dock_position: DockPosition,
10361 workspace: &Workspace,
10362 cx: &mut App,
10363) -> Option<Pixels> {
10364 #[derive(Deserialize)]
10365 struct LegacyPanelState {
10366 #[serde(default)]
10367 width: Option<Pixels>,
10368 #[serde(default)]
10369 height: Option<Pixels>,
10370 }
10371
10372 let workspace_id = workspace
10373 .database_id()
10374 .map(|id| i64::from(id).to_string())
10375 .or_else(|| workspace.session_id())?;
10376
10377 let legacy_key = match panel_key {
10378 "ProjectPanel" => {
10379 format!("{}-{:?}", "ProjectPanel", workspace_id)
10380 }
10381 "OutlinePanel" => {
10382 format!("{}-{:?}", "OutlinePanel", workspace_id)
10383 }
10384 "GitPanel" => {
10385 format!("{}-{:?}", "GitPanel", workspace_id)
10386 }
10387 "TerminalPanel" => {
10388 format!("{:?}-{:?}", "TerminalPanel", workspace_id)
10389 }
10390 _ => return None,
10391 };
10392
10393 let kvp = db::kvp::KeyValueStore::global(cx);
10394 let json = kvp.read_kvp(&legacy_key).log_err().flatten()?;
10395 let state = serde_json::from_str::<LegacyPanelState>(&json).log_err()?;
10396 let size = match dock_position {
10397 DockPosition::Bottom => state.height,
10398 DockPosition::Left | DockPosition::Right => state.width,
10399 }?;
10400
10401 cx.background_spawn(async move { kvp.delete_kvp(legacy_key).await })
10402 .detach_and_log_err(cx);
10403
10404 Some(size)
10405}
10406
10407#[cfg(test)]
10408mod tests {
10409 use std::{cell::RefCell, rc::Rc, sync::Arc, time::Duration};
10410
10411 use super::*;
10412 use crate::{
10413 dock::{PanelEvent, test::TestPanel},
10414 item::{
10415 ItemBufferKind, ItemEvent,
10416 test::{TestItem, TestProjectItem},
10417 },
10418 };
10419 use fs::FakeFs;
10420 use gpui::{
10421 DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
10422 UpdateGlobal, VisualTestContext, px,
10423 };
10424 use project::{Project, ProjectEntryId};
10425 use serde_json::json;
10426 use settings::SettingsStore;
10427 use util::path;
10428 use util::rel_path::rel_path;
10429
10430 #[gpui::test]
10431 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
10432 init_test(cx);
10433
10434 let fs = FakeFs::new(cx.executor());
10435 let project = Project::test(fs, [], cx).await;
10436 let (workspace, cx) =
10437 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10438
10439 // Adding an item with no ambiguity renders the tab without detail.
10440 let item1 = cx.new(|cx| {
10441 let mut item = TestItem::new(cx);
10442 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
10443 item
10444 });
10445 workspace.update_in(cx, |workspace, window, cx| {
10446 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
10447 });
10448 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
10449
10450 // Adding an item that creates ambiguity increases the level of detail on
10451 // both tabs.
10452 let item2 = cx.new_window_entity(|_window, cx| {
10453 let mut item = TestItem::new(cx);
10454 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
10455 item
10456 });
10457 workspace.update_in(cx, |workspace, window, cx| {
10458 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10459 });
10460 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10461 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10462
10463 // Adding an item that creates ambiguity increases the level of detail only
10464 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
10465 // we stop at the highest detail available.
10466 let item3 = cx.new(|cx| {
10467 let mut item = TestItem::new(cx);
10468 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
10469 item
10470 });
10471 workspace.update_in(cx, |workspace, window, cx| {
10472 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
10473 });
10474 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10475 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
10476 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
10477 }
10478
10479 #[gpui::test]
10480 async fn test_tracking_active_path(cx: &mut TestAppContext) {
10481 init_test(cx);
10482
10483 let fs = FakeFs::new(cx.executor());
10484 fs.insert_tree(
10485 "/root1",
10486 json!({
10487 "one.txt": "",
10488 "two.txt": "",
10489 }),
10490 )
10491 .await;
10492 fs.insert_tree(
10493 "/root2",
10494 json!({
10495 "three.txt": "",
10496 }),
10497 )
10498 .await;
10499
10500 let project = Project::test(fs, ["root1".as_ref()], cx).await;
10501 let (workspace, cx) =
10502 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10503 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10504 let worktree_id = project.update(cx, |project, cx| {
10505 project.worktrees(cx).next().unwrap().read(cx).id()
10506 });
10507
10508 let item1 = cx.new(|cx| {
10509 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
10510 });
10511 let item2 = cx.new(|cx| {
10512 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
10513 });
10514
10515 // Add an item to an empty pane
10516 workspace.update_in(cx, |workspace, window, cx| {
10517 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
10518 });
10519 project.update(cx, |project, cx| {
10520 assert_eq!(
10521 project.active_entry(),
10522 project
10523 .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
10524 .map(|e| e.id)
10525 );
10526 });
10527 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
10528
10529 // Add a second item to a non-empty pane
10530 workspace.update_in(cx, |workspace, window, cx| {
10531 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
10532 });
10533 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
10534 project.update(cx, |project, cx| {
10535 assert_eq!(
10536 project.active_entry(),
10537 project
10538 .entry_for_path(&(worktree_id, rel_path("two.txt")).into(), cx)
10539 .map(|e| e.id)
10540 );
10541 });
10542
10543 // Close the active item
10544 pane.update_in(cx, |pane, window, cx| {
10545 pane.close_active_item(&Default::default(), window, cx)
10546 })
10547 .await
10548 .unwrap();
10549 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
10550 project.update(cx, |project, cx| {
10551 assert_eq!(
10552 project.active_entry(),
10553 project
10554 .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
10555 .map(|e| e.id)
10556 );
10557 });
10558
10559 // Add a project folder
10560 project
10561 .update(cx, |project, cx| {
10562 project.find_or_create_worktree("root2", true, cx)
10563 })
10564 .await
10565 .unwrap();
10566 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
10567
10568 // Remove a project folder
10569 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
10570 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
10571 }
10572
10573 #[gpui::test]
10574 async fn test_close_window(cx: &mut TestAppContext) {
10575 init_test(cx);
10576
10577 let fs = FakeFs::new(cx.executor());
10578 fs.insert_tree("/root", json!({ "one": "" })).await;
10579
10580 let project = Project::test(fs, ["root".as_ref()], cx).await;
10581 let (workspace, cx) =
10582 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10583
10584 // When there are no dirty items, there's nothing to do.
10585 let item1 = cx.new(TestItem::new);
10586 workspace.update_in(cx, |w, window, cx| {
10587 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
10588 });
10589 let task = workspace.update_in(cx, |w, window, cx| {
10590 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
10591 });
10592 assert!(task.await.unwrap());
10593
10594 // When there are dirty untitled items, prompt to save each one. If the user
10595 // cancels any prompt, then abort.
10596 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
10597 let item3 = cx.new(|cx| {
10598 TestItem::new(cx)
10599 .with_dirty(true)
10600 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10601 });
10602 workspace.update_in(cx, |w, window, cx| {
10603 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10604 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
10605 });
10606 let task = workspace.update_in(cx, |w, window, cx| {
10607 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
10608 });
10609 cx.executor().run_until_parked();
10610 cx.simulate_prompt_answer("Cancel"); // cancel save all
10611 cx.executor().run_until_parked();
10612 assert!(!cx.has_pending_prompt());
10613 assert!(!task.await.unwrap());
10614 }
10615
10616 #[gpui::test]
10617 async fn test_multi_workspace_close_window_multiple_workspaces_cancel(cx: &mut TestAppContext) {
10618 init_test(cx);
10619
10620 let fs = FakeFs::new(cx.executor());
10621 fs.insert_tree("/root", json!({ "one": "" })).await;
10622
10623 let project_a = Project::test(fs.clone(), ["root".as_ref()], cx).await;
10624 let project_b = Project::test(fs, ["root".as_ref()], cx).await;
10625 let multi_workspace_handle =
10626 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
10627 cx.run_until_parked();
10628
10629 let workspace_a = multi_workspace_handle
10630 .read_with(cx, |mw, _| mw.workspace().clone())
10631 .unwrap();
10632
10633 let workspace_b = multi_workspace_handle
10634 .update(cx, |mw, window, cx| {
10635 mw.test_add_workspace(project_b, window, cx)
10636 })
10637 .unwrap();
10638
10639 // Activate workspace A
10640 multi_workspace_handle
10641 .update(cx, |mw, window, cx| {
10642 mw.activate_index(0, window, cx);
10643 })
10644 .unwrap();
10645
10646 let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
10647
10648 // Workspace A has a clean item
10649 let item_a = cx.new(TestItem::new);
10650 workspace_a.update_in(cx, |w, window, cx| {
10651 w.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
10652 });
10653
10654 // Workspace B has a dirty item
10655 let item_b = cx.new(|cx| TestItem::new(cx).with_dirty(true));
10656 workspace_b.update_in(cx, |w, window, cx| {
10657 w.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx)
10658 });
10659
10660 // Verify workspace A is active
10661 multi_workspace_handle
10662 .read_with(cx, |mw, _| {
10663 assert_eq!(mw.active_workspace_index(), 0);
10664 })
10665 .unwrap();
10666
10667 // Dispatch CloseWindow — workspace A will pass, workspace B will prompt
10668 multi_workspace_handle
10669 .update(cx, |mw, window, cx| {
10670 mw.close_window(&CloseWindow, window, cx);
10671 })
10672 .unwrap();
10673 cx.run_until_parked();
10674
10675 // Workspace B should now be active since it has dirty items that need attention
10676 multi_workspace_handle
10677 .read_with(cx, |mw, _| {
10678 assert_eq!(
10679 mw.active_workspace_index(),
10680 1,
10681 "workspace B should be activated when it prompts"
10682 );
10683 })
10684 .unwrap();
10685
10686 // User cancels the save prompt from workspace B
10687 cx.simulate_prompt_answer("Cancel");
10688 cx.run_until_parked();
10689
10690 // Window should still exist because workspace B's close was cancelled
10691 assert!(
10692 multi_workspace_handle.update(cx, |_, _, _| ()).is_ok(),
10693 "window should still exist after cancelling one workspace's close"
10694 );
10695 }
10696
10697 #[gpui::test]
10698 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
10699 init_test(cx);
10700
10701 // Register TestItem as a serializable item
10702 cx.update(|cx| {
10703 register_serializable_item::<TestItem>(cx);
10704 });
10705
10706 let fs = FakeFs::new(cx.executor());
10707 fs.insert_tree("/root", json!({ "one": "" })).await;
10708
10709 let project = Project::test(fs, ["root".as_ref()], cx).await;
10710 let (workspace, cx) =
10711 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10712
10713 // When there are dirty untitled items, but they can serialize, then there is no prompt.
10714 let item1 = cx.new(|cx| {
10715 TestItem::new(cx)
10716 .with_dirty(true)
10717 .with_serialize(|| Some(Task::ready(Ok(()))))
10718 });
10719 let item2 = cx.new(|cx| {
10720 TestItem::new(cx)
10721 .with_dirty(true)
10722 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10723 .with_serialize(|| Some(Task::ready(Ok(()))))
10724 });
10725 workspace.update_in(cx, |w, window, cx| {
10726 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
10727 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10728 });
10729 let task = workspace.update_in(cx, |w, window, cx| {
10730 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
10731 });
10732 assert!(task.await.unwrap());
10733 }
10734
10735 #[gpui::test]
10736 async fn test_close_pane_items(cx: &mut TestAppContext) {
10737 init_test(cx);
10738
10739 let fs = FakeFs::new(cx.executor());
10740
10741 let project = Project::test(fs, None, cx).await;
10742 let (workspace, cx) =
10743 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10744
10745 let item1 = cx.new(|cx| {
10746 TestItem::new(cx)
10747 .with_dirty(true)
10748 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
10749 });
10750 let item2 = cx.new(|cx| {
10751 TestItem::new(cx)
10752 .with_dirty(true)
10753 .with_conflict(true)
10754 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
10755 });
10756 let item3 = cx.new(|cx| {
10757 TestItem::new(cx)
10758 .with_dirty(true)
10759 .with_conflict(true)
10760 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
10761 });
10762 let item4 = cx.new(|cx| {
10763 TestItem::new(cx).with_dirty(true).with_project_items(&[{
10764 let project_item = TestProjectItem::new_untitled(cx);
10765 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
10766 project_item
10767 }])
10768 });
10769 let pane = workspace.update_in(cx, |workspace, window, cx| {
10770 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
10771 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10772 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
10773 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
10774 workspace.active_pane().clone()
10775 });
10776
10777 let close_items = pane.update_in(cx, |pane, window, cx| {
10778 pane.activate_item(1, true, true, window, cx);
10779 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
10780 let item1_id = item1.item_id();
10781 let item3_id = item3.item_id();
10782 let item4_id = item4.item_id();
10783 pane.close_items(window, cx, SaveIntent::Close, &move |id| {
10784 [item1_id, item3_id, item4_id].contains(&id)
10785 })
10786 });
10787 cx.executor().run_until_parked();
10788
10789 assert!(cx.has_pending_prompt());
10790 cx.simulate_prompt_answer("Save all");
10791
10792 cx.executor().run_until_parked();
10793
10794 // Item 1 is saved. There's a prompt to save item 3.
10795 pane.update(cx, |pane, cx| {
10796 assert_eq!(item1.read(cx).save_count, 1);
10797 assert_eq!(item1.read(cx).save_as_count, 0);
10798 assert_eq!(item1.read(cx).reload_count, 0);
10799 assert_eq!(pane.items_len(), 3);
10800 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
10801 });
10802 assert!(cx.has_pending_prompt());
10803
10804 // Cancel saving item 3.
10805 cx.simulate_prompt_answer("Discard");
10806 cx.executor().run_until_parked();
10807
10808 // Item 3 is reloaded. There's a prompt to save item 4.
10809 pane.update(cx, |pane, cx| {
10810 assert_eq!(item3.read(cx).save_count, 0);
10811 assert_eq!(item3.read(cx).save_as_count, 0);
10812 assert_eq!(item3.read(cx).reload_count, 1);
10813 assert_eq!(pane.items_len(), 2);
10814 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
10815 });
10816
10817 // There's a prompt for a path for item 4.
10818 cx.simulate_new_path_selection(|_| Some(Default::default()));
10819 close_items.await.unwrap();
10820
10821 // The requested items are closed.
10822 pane.update(cx, |pane, cx| {
10823 assert_eq!(item4.read(cx).save_count, 0);
10824 assert_eq!(item4.read(cx).save_as_count, 1);
10825 assert_eq!(item4.read(cx).reload_count, 0);
10826 assert_eq!(pane.items_len(), 1);
10827 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
10828 });
10829 }
10830
10831 #[gpui::test]
10832 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
10833 init_test(cx);
10834
10835 let fs = FakeFs::new(cx.executor());
10836 let project = Project::test(fs, [], cx).await;
10837 let (workspace, cx) =
10838 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10839
10840 // Create several workspace items with single project entries, and two
10841 // workspace items with multiple project entries.
10842 let single_entry_items = (0..=4)
10843 .map(|project_entry_id| {
10844 cx.new(|cx| {
10845 TestItem::new(cx)
10846 .with_dirty(true)
10847 .with_project_items(&[dirty_project_item(
10848 project_entry_id,
10849 &format!("{project_entry_id}.txt"),
10850 cx,
10851 )])
10852 })
10853 })
10854 .collect::<Vec<_>>();
10855 let item_2_3 = cx.new(|cx| {
10856 TestItem::new(cx)
10857 .with_dirty(true)
10858 .with_buffer_kind(ItemBufferKind::Multibuffer)
10859 .with_project_items(&[
10860 single_entry_items[2].read(cx).project_items[0].clone(),
10861 single_entry_items[3].read(cx).project_items[0].clone(),
10862 ])
10863 });
10864 let item_3_4 = cx.new(|cx| {
10865 TestItem::new(cx)
10866 .with_dirty(true)
10867 .with_buffer_kind(ItemBufferKind::Multibuffer)
10868 .with_project_items(&[
10869 single_entry_items[3].read(cx).project_items[0].clone(),
10870 single_entry_items[4].read(cx).project_items[0].clone(),
10871 ])
10872 });
10873
10874 // Create two panes that contain the following project entries:
10875 // left pane:
10876 // multi-entry items: (2, 3)
10877 // single-entry items: 0, 2, 3, 4
10878 // right pane:
10879 // single-entry items: 4, 1
10880 // multi-entry items: (3, 4)
10881 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
10882 let left_pane = workspace.active_pane().clone();
10883 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
10884 workspace.add_item_to_active_pane(
10885 single_entry_items[0].boxed_clone(),
10886 None,
10887 true,
10888 window,
10889 cx,
10890 );
10891 workspace.add_item_to_active_pane(
10892 single_entry_items[2].boxed_clone(),
10893 None,
10894 true,
10895 window,
10896 cx,
10897 );
10898 workspace.add_item_to_active_pane(
10899 single_entry_items[3].boxed_clone(),
10900 None,
10901 true,
10902 window,
10903 cx,
10904 );
10905 workspace.add_item_to_active_pane(
10906 single_entry_items[4].boxed_clone(),
10907 None,
10908 true,
10909 window,
10910 cx,
10911 );
10912
10913 let right_pane =
10914 workspace.split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx);
10915
10916 let boxed_clone = single_entry_items[1].boxed_clone();
10917 let right_pane = window.spawn(cx, async move |cx| {
10918 right_pane.await.inspect(|right_pane| {
10919 right_pane
10920 .update_in(cx, |pane, window, cx| {
10921 pane.add_item(boxed_clone, true, true, None, window, cx);
10922 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
10923 })
10924 .unwrap();
10925 })
10926 });
10927
10928 (left_pane, right_pane)
10929 });
10930 let right_pane = right_pane.await.unwrap();
10931 cx.focus(&right_pane);
10932
10933 let close = right_pane.update_in(cx, |pane, window, cx| {
10934 pane.close_all_items(&CloseAllItems::default(), window, cx)
10935 .unwrap()
10936 });
10937 cx.executor().run_until_parked();
10938
10939 let msg = cx.pending_prompt().unwrap().0;
10940 assert!(msg.contains("1.txt"));
10941 assert!(!msg.contains("2.txt"));
10942 assert!(!msg.contains("3.txt"));
10943 assert!(!msg.contains("4.txt"));
10944
10945 // With best-effort close, cancelling item 1 keeps it open but items 4
10946 // and (3,4) still close since their entries exist in left pane.
10947 cx.simulate_prompt_answer("Cancel");
10948 close.await;
10949
10950 right_pane.read_with(cx, |pane, _| {
10951 assert_eq!(pane.items_len(), 1);
10952 });
10953
10954 // Remove item 3 from left pane, making (2,3) the only item with entry 3.
10955 left_pane
10956 .update_in(cx, |left_pane, window, cx| {
10957 left_pane.close_item_by_id(
10958 single_entry_items[3].entity_id(),
10959 SaveIntent::Skip,
10960 window,
10961 cx,
10962 )
10963 })
10964 .await
10965 .unwrap();
10966
10967 let close = left_pane.update_in(cx, |pane, window, cx| {
10968 pane.close_all_items(&CloseAllItems::default(), window, cx)
10969 .unwrap()
10970 });
10971 cx.executor().run_until_parked();
10972
10973 let details = cx.pending_prompt().unwrap().1;
10974 assert!(details.contains("0.txt"));
10975 assert!(details.contains("3.txt"));
10976 assert!(details.contains("4.txt"));
10977 // Ideally 2.txt wouldn't appear since entry 2 still exists in item 2.
10978 // But we can only save whole items, so saving (2,3) for entry 3 includes 2.
10979 // assert!(!details.contains("2.txt"));
10980
10981 cx.simulate_prompt_answer("Save all");
10982 cx.executor().run_until_parked();
10983 close.await;
10984
10985 left_pane.read_with(cx, |pane, _| {
10986 assert_eq!(pane.items_len(), 0);
10987 });
10988 }
10989
10990 #[gpui::test]
10991 async fn test_autosave(cx: &mut gpui::TestAppContext) {
10992 init_test(cx);
10993
10994 let fs = FakeFs::new(cx.executor());
10995 let project = Project::test(fs, [], cx).await;
10996 let (workspace, cx) =
10997 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10998 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10999
11000 let item = cx.new(|cx| {
11001 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11002 });
11003 let item_id = item.entity_id();
11004 workspace.update_in(cx, |workspace, window, cx| {
11005 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11006 });
11007
11008 // Autosave on window change.
11009 item.update(cx, |item, cx| {
11010 SettingsStore::update_global(cx, |settings, cx| {
11011 settings.update_user_settings(cx, |settings| {
11012 settings.workspace.autosave = Some(AutosaveSetting::OnWindowChange);
11013 })
11014 });
11015 item.is_dirty = true;
11016 });
11017
11018 // Deactivating the window saves the file.
11019 cx.deactivate_window();
11020 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11021
11022 // Re-activating the window doesn't save the file.
11023 cx.update(|window, _| window.activate_window());
11024 cx.executor().run_until_parked();
11025 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11026
11027 // Autosave on focus change.
11028 item.update_in(cx, |item, window, cx| {
11029 cx.focus_self(window);
11030 SettingsStore::update_global(cx, |settings, cx| {
11031 settings.update_user_settings(cx, |settings| {
11032 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11033 })
11034 });
11035 item.is_dirty = true;
11036 });
11037 // Blurring the item saves the file.
11038 item.update_in(cx, |_, window, _| window.blur());
11039 cx.executor().run_until_parked();
11040 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
11041
11042 // Deactivating the window still saves the file.
11043 item.update_in(cx, |item, window, cx| {
11044 cx.focus_self(window);
11045 item.is_dirty = true;
11046 });
11047 cx.deactivate_window();
11048 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
11049
11050 // Autosave after delay.
11051 item.update(cx, |item, cx| {
11052 SettingsStore::update_global(cx, |settings, cx| {
11053 settings.update_user_settings(cx, |settings| {
11054 settings.workspace.autosave = Some(AutosaveSetting::AfterDelay {
11055 milliseconds: 500.into(),
11056 });
11057 })
11058 });
11059 item.is_dirty = true;
11060 cx.emit(ItemEvent::Edit);
11061 });
11062
11063 // Delay hasn't fully expired, so the file is still dirty and unsaved.
11064 cx.executor().advance_clock(Duration::from_millis(250));
11065 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
11066
11067 // After delay expires, the file is saved.
11068 cx.executor().advance_clock(Duration::from_millis(250));
11069 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
11070
11071 // Autosave after delay, should save earlier than delay if tab is closed
11072 item.update(cx, |item, cx| {
11073 item.is_dirty = true;
11074 cx.emit(ItemEvent::Edit);
11075 });
11076 cx.executor().advance_clock(Duration::from_millis(250));
11077 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
11078
11079 // // Ensure auto save with delay saves the item on close, even if the timer hasn't yet run out.
11080 pane.update_in(cx, |pane, window, cx| {
11081 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11082 })
11083 .await
11084 .unwrap();
11085 assert!(!cx.has_pending_prompt());
11086 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
11087
11088 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
11089 workspace.update_in(cx, |workspace, window, cx| {
11090 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11091 });
11092 item.update_in(cx, |item, _window, cx| {
11093 item.is_dirty = true;
11094 for project_item in &mut item.project_items {
11095 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11096 }
11097 });
11098 cx.run_until_parked();
11099 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
11100
11101 // Autosave on focus change, ensuring closing the tab counts as such.
11102 item.update(cx, |item, cx| {
11103 SettingsStore::update_global(cx, |settings, cx| {
11104 settings.update_user_settings(cx, |settings| {
11105 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11106 })
11107 });
11108 item.is_dirty = true;
11109 for project_item in &mut item.project_items {
11110 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11111 }
11112 });
11113
11114 pane.update_in(cx, |pane, window, cx| {
11115 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11116 })
11117 .await
11118 .unwrap();
11119 assert!(!cx.has_pending_prompt());
11120 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11121
11122 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
11123 workspace.update_in(cx, |workspace, window, cx| {
11124 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11125 });
11126 item.update_in(cx, |item, window, cx| {
11127 item.project_items[0].update(cx, |item, _| {
11128 item.entry_id = None;
11129 });
11130 item.is_dirty = true;
11131 window.blur();
11132 });
11133 cx.run_until_parked();
11134 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11135
11136 // Ensure autosave is prevented for deleted files also when closing the buffer.
11137 let _close_items = pane.update_in(cx, |pane, window, cx| {
11138 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11139 });
11140 cx.run_until_parked();
11141 assert!(cx.has_pending_prompt());
11142 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11143 }
11144
11145 #[gpui::test]
11146 async fn test_autosave_on_focus_change_in_multibuffer(cx: &mut gpui::TestAppContext) {
11147 init_test(cx);
11148
11149 let fs = FakeFs::new(cx.executor());
11150 let project = Project::test(fs, [], cx).await;
11151 let (workspace, cx) =
11152 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11153
11154 // Create a multibuffer-like item with two child focus handles,
11155 // simulating individual buffer editors within a multibuffer.
11156 let item = cx.new(|cx| {
11157 TestItem::new(cx)
11158 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11159 .with_child_focus_handles(2, cx)
11160 });
11161 workspace.update_in(cx, |workspace, window, cx| {
11162 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11163 });
11164
11165 // Set autosave to OnFocusChange and focus the first child handle,
11166 // simulating the user's cursor being inside one of the multibuffer's excerpts.
11167 item.update_in(cx, |item, window, cx| {
11168 SettingsStore::update_global(cx, |settings, cx| {
11169 settings.update_user_settings(cx, |settings| {
11170 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11171 })
11172 });
11173 item.is_dirty = true;
11174 window.focus(&item.child_focus_handles[0], cx);
11175 });
11176 cx.executor().run_until_parked();
11177 item.read_with(cx, |item, _| assert_eq!(item.save_count, 0));
11178
11179 // Moving focus from one child to another within the same item should
11180 // NOT trigger autosave — focus is still within the item's focus hierarchy.
11181 item.update_in(cx, |item, window, cx| {
11182 window.focus(&item.child_focus_handles[1], cx);
11183 });
11184 cx.executor().run_until_parked();
11185 item.read_with(cx, |item, _| {
11186 assert_eq!(
11187 item.save_count, 0,
11188 "Switching focus between children within the same item should not autosave"
11189 );
11190 });
11191
11192 // Blurring the item saves the file. This is the core regression scenario:
11193 // with `on_blur`, this would NOT trigger because `on_blur` only fires when
11194 // the item's own focus handle is the leaf that lost focus. In a multibuffer,
11195 // the leaf is always a child focus handle, so `on_blur` never detected
11196 // focus leaving the item.
11197 item.update_in(cx, |_, window, _| window.blur());
11198 cx.executor().run_until_parked();
11199 item.read_with(cx, |item, _| {
11200 assert_eq!(
11201 item.save_count, 1,
11202 "Blurring should trigger autosave when focus was on a child of the item"
11203 );
11204 });
11205
11206 // Deactivating the window should also trigger autosave when a child of
11207 // the multibuffer item currently owns focus.
11208 item.update_in(cx, |item, window, cx| {
11209 item.is_dirty = true;
11210 window.focus(&item.child_focus_handles[0], cx);
11211 });
11212 cx.executor().run_until_parked();
11213 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11214
11215 cx.deactivate_window();
11216 item.read_with(cx, |item, _| {
11217 assert_eq!(
11218 item.save_count, 2,
11219 "Deactivating window should trigger autosave when focus was on a child"
11220 );
11221 });
11222 }
11223
11224 #[gpui::test]
11225 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
11226 init_test(cx);
11227
11228 let fs = FakeFs::new(cx.executor());
11229
11230 let project = Project::test(fs, [], cx).await;
11231 let (workspace, cx) =
11232 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11233
11234 let item = cx.new(|cx| {
11235 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11236 });
11237 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11238 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
11239 let toolbar_notify_count = Rc::new(RefCell::new(0));
11240
11241 workspace.update_in(cx, |workspace, window, cx| {
11242 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11243 let toolbar_notification_count = toolbar_notify_count.clone();
11244 cx.observe_in(&toolbar, window, move |_, _, _, _| {
11245 *toolbar_notification_count.borrow_mut() += 1
11246 })
11247 .detach();
11248 });
11249
11250 pane.read_with(cx, |pane, _| {
11251 assert!(!pane.can_navigate_backward());
11252 assert!(!pane.can_navigate_forward());
11253 });
11254
11255 item.update_in(cx, |item, _, cx| {
11256 item.set_state("one".to_string(), cx);
11257 });
11258
11259 // Toolbar must be notified to re-render the navigation buttons
11260 assert_eq!(*toolbar_notify_count.borrow(), 1);
11261
11262 pane.read_with(cx, |pane, _| {
11263 assert!(pane.can_navigate_backward());
11264 assert!(!pane.can_navigate_forward());
11265 });
11266
11267 workspace
11268 .update_in(cx, |workspace, window, cx| {
11269 workspace.go_back(pane.downgrade(), window, cx)
11270 })
11271 .await
11272 .unwrap();
11273
11274 assert_eq!(*toolbar_notify_count.borrow(), 2);
11275 pane.read_with(cx, |pane, _| {
11276 assert!(!pane.can_navigate_backward());
11277 assert!(pane.can_navigate_forward());
11278 });
11279 }
11280
11281 /// Tests that the navigation history deduplicates entries for the same item.
11282 ///
11283 /// When navigating back and forth between items (e.g., A -> B -> A -> B -> A -> B -> C),
11284 /// the navigation history deduplicates by keeping only the most recent visit to each item,
11285 /// resulting in [A, B, C] instead of [A, B, A, B, A, B, C]. This ensures that Go Back (Ctrl-O)
11286 /// navigates through unique items efficiently: C -> B -> A, rather than bouncing between
11287 /// repeated entries: C -> B -> A -> B -> A -> B -> A.
11288 ///
11289 /// This behavior prevents the navigation history from growing unnecessarily large and provides
11290 /// a better user experience by eliminating redundant navigation steps when jumping between files.
11291 #[gpui::test]
11292 async fn test_navigation_history_deduplication(cx: &mut gpui::TestAppContext) {
11293 init_test(cx);
11294
11295 let fs = FakeFs::new(cx.executor());
11296 let project = Project::test(fs, [], cx).await;
11297 let (workspace, cx) =
11298 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11299
11300 let item_a = cx.new(|cx| {
11301 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "a.txt", cx)])
11302 });
11303 let item_b = cx.new(|cx| {
11304 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "b.txt", cx)])
11305 });
11306 let item_c = cx.new(|cx| {
11307 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "c.txt", cx)])
11308 });
11309
11310 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11311
11312 workspace.update_in(cx, |workspace, window, cx| {
11313 workspace.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx);
11314 workspace.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx);
11315 workspace.add_item_to_active_pane(Box::new(item_c.clone()), None, true, window, cx);
11316 });
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_a, false, false, window, cx);
11330 });
11331 cx.run_until_parked();
11332
11333 workspace.update_in(cx, |workspace, window, cx| {
11334 workspace.activate_item(&item_b, false, false, window, cx);
11335 });
11336 cx.run_until_parked();
11337
11338 workspace.update_in(cx, |workspace, window, cx| {
11339 workspace.activate_item(&item_a, false, false, window, cx);
11340 });
11341 cx.run_until_parked();
11342
11343 workspace.update_in(cx, |workspace, window, cx| {
11344 workspace.activate_item(&item_b, false, false, window, cx);
11345 });
11346 cx.run_until_parked();
11347
11348 workspace.update_in(cx, |workspace, window, cx| {
11349 workspace.activate_item(&item_c, false, false, window, cx);
11350 });
11351 cx.run_until_parked();
11352
11353 let backward_count = pane.read_with(cx, |pane, cx| {
11354 let mut count = 0;
11355 pane.nav_history().for_each_entry(cx, &mut |_, _| {
11356 count += 1;
11357 });
11358 count
11359 });
11360 assert!(
11361 backward_count <= 4,
11362 "Should have at most 4 entries, got {}",
11363 backward_count
11364 );
11365
11366 workspace
11367 .update_in(cx, |workspace, window, cx| {
11368 workspace.go_back(pane.downgrade(), window, cx)
11369 })
11370 .await
11371 .unwrap();
11372
11373 let active_item = workspace.read_with(cx, |workspace, cx| {
11374 workspace.active_item(cx).unwrap().item_id()
11375 });
11376 assert_eq!(
11377 active_item,
11378 item_b.entity_id(),
11379 "After first go_back, should be at item B"
11380 );
11381
11382 workspace
11383 .update_in(cx, |workspace, window, cx| {
11384 workspace.go_back(pane.downgrade(), window, cx)
11385 })
11386 .await
11387 .unwrap();
11388
11389 let active_item = workspace.read_with(cx, |workspace, cx| {
11390 workspace.active_item(cx).unwrap().item_id()
11391 });
11392 assert_eq!(
11393 active_item,
11394 item_a.entity_id(),
11395 "After second go_back, should be at item A"
11396 );
11397
11398 pane.read_with(cx, |pane, _| {
11399 assert!(pane.can_navigate_forward(), "Should be able to go forward");
11400 });
11401 }
11402
11403 #[gpui::test]
11404 async fn test_activate_last_pane(cx: &mut gpui::TestAppContext) {
11405 init_test(cx);
11406 let fs = FakeFs::new(cx.executor());
11407 let project = Project::test(fs, [], cx).await;
11408 let (multi_workspace, cx) =
11409 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
11410 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
11411
11412 workspace.update_in(cx, |workspace, window, cx| {
11413 let first_item = cx.new(|cx| {
11414 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11415 });
11416 workspace.add_item_to_active_pane(Box::new(first_item), None, true, window, cx);
11417 workspace.split_pane(
11418 workspace.active_pane().clone(),
11419 SplitDirection::Right,
11420 window,
11421 cx,
11422 );
11423 workspace.split_pane(
11424 workspace.active_pane().clone(),
11425 SplitDirection::Right,
11426 window,
11427 cx,
11428 );
11429 });
11430
11431 let (first_pane_id, target_last_pane_id) = workspace.update(cx, |workspace, _cx| {
11432 let panes = workspace.center.panes();
11433 assert!(panes.len() >= 2);
11434 (
11435 panes.first().expect("at least one pane").entity_id(),
11436 panes.last().expect("at least one pane").entity_id(),
11437 )
11438 });
11439
11440 workspace.update_in(cx, |workspace, window, cx| {
11441 workspace.activate_pane_at_index(&ActivatePane(0), window, cx);
11442 });
11443 workspace.update(cx, |workspace, _| {
11444 assert_eq!(workspace.active_pane().entity_id(), first_pane_id);
11445 assert_ne!(workspace.active_pane().entity_id(), target_last_pane_id);
11446 });
11447
11448 cx.dispatch_action(ActivateLastPane);
11449
11450 workspace.update(cx, |workspace, _| {
11451 assert_eq!(workspace.active_pane().entity_id(), target_last_pane_id);
11452 });
11453 }
11454
11455 #[gpui::test]
11456 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
11457 init_test(cx);
11458 let fs = FakeFs::new(cx.executor());
11459
11460 let project = Project::test(fs, [], cx).await;
11461 let (workspace, cx) =
11462 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11463
11464 let panel = workspace.update_in(cx, |workspace, window, cx| {
11465 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
11466 workspace.add_panel(panel.clone(), window, cx);
11467
11468 workspace
11469 .right_dock()
11470 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
11471
11472 panel
11473 });
11474
11475 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11476 pane.update_in(cx, |pane, window, cx| {
11477 let item = cx.new(TestItem::new);
11478 pane.add_item(Box::new(item), true, true, None, window, cx);
11479 });
11480
11481 // Transfer focus from center to panel
11482 workspace.update_in(cx, |workspace, window, cx| {
11483 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11484 });
11485
11486 workspace.update_in(cx, |workspace, window, cx| {
11487 assert!(workspace.right_dock().read(cx).is_open());
11488 assert!(!panel.is_zoomed(window, cx));
11489 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11490 });
11491
11492 // Transfer focus from panel to center
11493 workspace.update_in(cx, |workspace, window, cx| {
11494 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11495 });
11496
11497 workspace.update_in(cx, |workspace, window, cx| {
11498 assert!(workspace.right_dock().read(cx).is_open());
11499 assert!(!panel.is_zoomed(window, cx));
11500 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11501 assert!(pane.read(cx).focus_handle(cx).contains_focused(window, cx));
11502 });
11503
11504 // Close the dock
11505 workspace.update_in(cx, |workspace, window, cx| {
11506 workspace.toggle_dock(DockPosition::Right, window, cx);
11507 });
11508
11509 workspace.update_in(cx, |workspace, window, cx| {
11510 assert!(!workspace.right_dock().read(cx).is_open());
11511 assert!(!panel.is_zoomed(window, cx));
11512 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11513 assert!(pane.read(cx).focus_handle(cx).contains_focused(window, cx));
11514 });
11515
11516 // Open the dock
11517 workspace.update_in(cx, |workspace, window, cx| {
11518 workspace.toggle_dock(DockPosition::Right, window, cx);
11519 });
11520
11521 workspace.update_in(cx, |workspace, window, cx| {
11522 assert!(workspace.right_dock().read(cx).is_open());
11523 assert!(!panel.is_zoomed(window, cx));
11524 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11525 });
11526
11527 // Focus and zoom panel
11528 panel.update_in(cx, |panel, window, cx| {
11529 cx.focus_self(window);
11530 panel.set_zoomed(true, window, cx)
11531 });
11532
11533 workspace.update_in(cx, |workspace, window, cx| {
11534 assert!(workspace.right_dock().read(cx).is_open());
11535 assert!(panel.is_zoomed(window, cx));
11536 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11537 });
11538
11539 // Transfer focus to the center closes the dock
11540 workspace.update_in(cx, |workspace, window, cx| {
11541 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11542 });
11543
11544 workspace.update_in(cx, |workspace, window, cx| {
11545 assert!(!workspace.right_dock().read(cx).is_open());
11546 assert!(panel.is_zoomed(window, cx));
11547 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11548 });
11549
11550 // Transferring focus back to the panel keeps it zoomed
11551 workspace.update_in(cx, |workspace, window, cx| {
11552 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11553 });
11554
11555 workspace.update_in(cx, |workspace, window, cx| {
11556 assert!(workspace.right_dock().read(cx).is_open());
11557 assert!(panel.is_zoomed(window, cx));
11558 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11559 });
11560
11561 // Close the dock while it is zoomed
11562 workspace.update_in(cx, |workspace, window, cx| {
11563 workspace.toggle_dock(DockPosition::Right, window, cx)
11564 });
11565
11566 workspace.update_in(cx, |workspace, window, cx| {
11567 assert!(!workspace.right_dock().read(cx).is_open());
11568 assert!(panel.is_zoomed(window, cx));
11569 assert!(workspace.zoomed.is_none());
11570 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11571 });
11572
11573 // Opening the dock, when it's zoomed, retains focus
11574 workspace.update_in(cx, |workspace, window, cx| {
11575 workspace.toggle_dock(DockPosition::Right, window, cx)
11576 });
11577
11578 workspace.update_in(cx, |workspace, window, cx| {
11579 assert!(workspace.right_dock().read(cx).is_open());
11580 assert!(panel.is_zoomed(window, cx));
11581 assert!(workspace.zoomed.is_some());
11582 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11583 });
11584
11585 // Unzoom and close the panel, zoom the active pane.
11586 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
11587 workspace.update_in(cx, |workspace, window, cx| {
11588 workspace.toggle_dock(DockPosition::Right, window, cx)
11589 });
11590 pane.update_in(cx, |pane, window, cx| {
11591 pane.toggle_zoom(&Default::default(), window, cx)
11592 });
11593
11594 // Opening a dock unzooms the pane.
11595 workspace.update_in(cx, |workspace, window, cx| {
11596 workspace.toggle_dock(DockPosition::Right, window, cx)
11597 });
11598 workspace.update_in(cx, |workspace, window, cx| {
11599 let pane = pane.read(cx);
11600 assert!(!pane.is_zoomed());
11601 assert!(!pane.focus_handle(cx).is_focused(window));
11602 assert!(workspace.right_dock().read(cx).is_open());
11603 assert!(workspace.zoomed.is_none());
11604 });
11605 }
11606
11607 #[gpui::test]
11608 async fn test_close_panel_on_toggle(cx: &mut gpui::TestAppContext) {
11609 init_test(cx);
11610 let fs = FakeFs::new(cx.executor());
11611
11612 let project = Project::test(fs, [], cx).await;
11613 let (workspace, cx) =
11614 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11615
11616 let panel = workspace.update_in(cx, |workspace, window, cx| {
11617 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
11618 workspace.add_panel(panel.clone(), window, cx);
11619 panel
11620 });
11621
11622 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11623 pane.update_in(cx, |pane, window, cx| {
11624 let item = cx.new(TestItem::new);
11625 pane.add_item(Box::new(item), true, true, None, window, cx);
11626 });
11627
11628 // Enable close_panel_on_toggle
11629 cx.update_global(|store: &mut SettingsStore, cx| {
11630 store.update_user_settings(cx, |settings| {
11631 settings.workspace.close_panel_on_toggle = Some(true);
11632 });
11633 });
11634
11635 // Panel starts closed. Toggling should open and focus it.
11636 workspace.update_in(cx, |workspace, window, cx| {
11637 assert!(!workspace.right_dock().read(cx).is_open());
11638 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11639 });
11640
11641 workspace.update_in(cx, |workspace, window, cx| {
11642 assert!(
11643 workspace.right_dock().read(cx).is_open(),
11644 "Dock should be open after toggling from center"
11645 );
11646 assert!(
11647 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
11648 "Panel should be focused after toggling from center"
11649 );
11650 });
11651
11652 // Panel is open and focused. Toggling should close the panel and
11653 // return focus to the center.
11654 workspace.update_in(cx, |workspace, window, cx| {
11655 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11656 });
11657
11658 workspace.update_in(cx, |workspace, window, cx| {
11659 assert!(
11660 !workspace.right_dock().read(cx).is_open(),
11661 "Dock should be closed after toggling from focused panel"
11662 );
11663 assert!(
11664 !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
11665 "Panel should not be focused after toggling from focused panel"
11666 );
11667 });
11668
11669 // Open the dock and focus something else so the panel is open but not
11670 // focused. Toggling should focus the panel (not close it).
11671 workspace.update_in(cx, |workspace, window, cx| {
11672 workspace
11673 .right_dock()
11674 .update(cx, |dock, cx| dock.set_open(true, window, cx));
11675 window.focus(&pane.read(cx).focus_handle(cx), cx);
11676 });
11677
11678 workspace.update_in(cx, |workspace, window, cx| {
11679 assert!(workspace.right_dock().read(cx).is_open());
11680 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11681 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11682 });
11683
11684 workspace.update_in(cx, |workspace, window, cx| {
11685 assert!(
11686 workspace.right_dock().read(cx).is_open(),
11687 "Dock should remain open when toggling focuses an open-but-unfocused panel"
11688 );
11689 assert!(
11690 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
11691 "Panel should be focused after toggling an open-but-unfocused panel"
11692 );
11693 });
11694
11695 // Now disable the setting and verify the original behavior: toggling
11696 // from a focused panel moves focus to center but leaves the dock open.
11697 cx.update_global(|store: &mut SettingsStore, cx| {
11698 store.update_user_settings(cx, |settings| {
11699 settings.workspace.close_panel_on_toggle = Some(false);
11700 });
11701 });
11702
11703 workspace.update_in(cx, |workspace, window, cx| {
11704 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11705 });
11706
11707 workspace.update_in(cx, |workspace, window, cx| {
11708 assert!(
11709 workspace.right_dock().read(cx).is_open(),
11710 "Dock should remain open when setting is disabled"
11711 );
11712 assert!(
11713 !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
11714 "Panel should not be focused after toggling with setting disabled"
11715 );
11716 });
11717 }
11718
11719 #[gpui::test]
11720 async fn test_pane_zoom_in_out(cx: &mut TestAppContext) {
11721 init_test(cx);
11722 let fs = FakeFs::new(cx.executor());
11723
11724 let project = Project::test(fs, [], cx).await;
11725 let (workspace, cx) =
11726 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11727
11728 let pane = workspace.update_in(cx, |workspace, _window, _cx| {
11729 workspace.active_pane().clone()
11730 });
11731
11732 // Add an item to the pane so it can be zoomed
11733 workspace.update_in(cx, |workspace, window, cx| {
11734 let item = cx.new(TestItem::new);
11735 workspace.add_item(pane.clone(), Box::new(item), None, true, true, window, cx);
11736 });
11737
11738 // Initially not zoomed
11739 workspace.update_in(cx, |workspace, _window, cx| {
11740 assert!(!pane.read(cx).is_zoomed(), "Pane starts unzoomed");
11741 assert!(
11742 workspace.zoomed.is_none(),
11743 "Workspace should track no zoomed pane"
11744 );
11745 assert!(pane.read(cx).items_len() > 0, "Pane should have items");
11746 });
11747
11748 // Zoom In
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!(
11755 pane.read(cx).is_zoomed(),
11756 "Pane should be zoomed after ZoomIn"
11757 );
11758 assert!(
11759 workspace.zoomed.is_some(),
11760 "Workspace should track the zoomed pane"
11761 );
11762 assert!(
11763 pane.read(cx).focus_handle(cx).contains_focused(window, cx),
11764 "ZoomIn should focus the pane"
11765 );
11766 });
11767
11768 // Zoom In again is a no-op
11769 pane.update_in(cx, |pane, window, cx| {
11770 pane.zoom_in(&crate::ZoomIn, window, cx);
11771 });
11772
11773 workspace.update_in(cx, |workspace, window, cx| {
11774 assert!(pane.read(cx).is_zoomed(), "Second ZoomIn keeps pane zoomed");
11775 assert!(
11776 workspace.zoomed.is_some(),
11777 "Workspace still tracks zoomed pane"
11778 );
11779 assert!(
11780 pane.read(cx).focus_handle(cx).contains_focused(window, cx),
11781 "Pane remains focused after repeated ZoomIn"
11782 );
11783 });
11784
11785 // Zoom Out
11786 pane.update_in(cx, |pane, window, cx| {
11787 pane.zoom_out(&crate::ZoomOut, window, cx);
11788 });
11789
11790 workspace.update_in(cx, |workspace, _window, cx| {
11791 assert!(
11792 !pane.read(cx).is_zoomed(),
11793 "Pane should unzoom after ZoomOut"
11794 );
11795 assert!(
11796 workspace.zoomed.is_none(),
11797 "Workspace clears zoom tracking after ZoomOut"
11798 );
11799 });
11800
11801 // Zoom Out again is a no-op
11802 pane.update_in(cx, |pane, window, cx| {
11803 pane.zoom_out(&crate::ZoomOut, window, cx);
11804 });
11805
11806 workspace.update_in(cx, |workspace, _window, cx| {
11807 assert!(
11808 !pane.read(cx).is_zoomed(),
11809 "Second ZoomOut keeps pane unzoomed"
11810 );
11811 assert!(
11812 workspace.zoomed.is_none(),
11813 "Workspace remains without zoomed pane"
11814 );
11815 });
11816 }
11817
11818 #[gpui::test]
11819 async fn test_toggle_all_docks(cx: &mut gpui::TestAppContext) {
11820 init_test(cx);
11821 let fs = FakeFs::new(cx.executor());
11822
11823 let project = Project::test(fs, [], cx).await;
11824 let (workspace, cx) =
11825 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11826 workspace.update_in(cx, |workspace, window, cx| {
11827 // Open two docks
11828 let left_dock = workspace.dock_at_position(DockPosition::Left);
11829 let right_dock = workspace.dock_at_position(DockPosition::Right);
11830
11831 left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
11832 right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
11833
11834 assert!(left_dock.read(cx).is_open());
11835 assert!(right_dock.read(cx).is_open());
11836 });
11837
11838 workspace.update_in(cx, |workspace, window, cx| {
11839 // Toggle all docks - should close both
11840 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11841
11842 let left_dock = workspace.dock_at_position(DockPosition::Left);
11843 let right_dock = workspace.dock_at_position(DockPosition::Right);
11844 assert!(!left_dock.read(cx).is_open());
11845 assert!(!right_dock.read(cx).is_open());
11846 });
11847
11848 workspace.update_in(cx, |workspace, window, cx| {
11849 // Toggle again - should reopen both
11850 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11851
11852 let left_dock = workspace.dock_at_position(DockPosition::Left);
11853 let right_dock = workspace.dock_at_position(DockPosition::Right);
11854 assert!(left_dock.read(cx).is_open());
11855 assert!(right_dock.read(cx).is_open());
11856 });
11857 }
11858
11859 #[gpui::test]
11860 async fn test_toggle_all_with_manual_close(cx: &mut gpui::TestAppContext) {
11861 init_test(cx);
11862 let fs = FakeFs::new(cx.executor());
11863
11864 let project = Project::test(fs, [], cx).await;
11865 let (workspace, cx) =
11866 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11867 workspace.update_in(cx, |workspace, window, cx| {
11868 // Open two docks
11869 let left_dock = workspace.dock_at_position(DockPosition::Left);
11870 let right_dock = workspace.dock_at_position(DockPosition::Right);
11871
11872 left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
11873 right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
11874
11875 assert!(left_dock.read(cx).is_open());
11876 assert!(right_dock.read(cx).is_open());
11877 });
11878
11879 workspace.update_in(cx, |workspace, window, cx| {
11880 // Close them manually
11881 workspace.toggle_dock(DockPosition::Left, window, cx);
11882 workspace.toggle_dock(DockPosition::Right, window, cx);
11883
11884 let left_dock = workspace.dock_at_position(DockPosition::Left);
11885 let right_dock = workspace.dock_at_position(DockPosition::Right);
11886 assert!(!left_dock.read(cx).is_open());
11887 assert!(!right_dock.read(cx).is_open());
11888 });
11889
11890 workspace.update_in(cx, |workspace, window, cx| {
11891 // Toggle all docks - only last closed (right dock) should reopen
11892 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11893
11894 let left_dock = workspace.dock_at_position(DockPosition::Left);
11895 let right_dock = workspace.dock_at_position(DockPosition::Right);
11896 assert!(!left_dock.read(cx).is_open());
11897 assert!(right_dock.read(cx).is_open());
11898 });
11899 }
11900
11901 #[gpui::test]
11902 async fn test_toggle_all_docks_after_dock_move(cx: &mut gpui::TestAppContext) {
11903 init_test(cx);
11904 let fs = FakeFs::new(cx.executor());
11905 let project = Project::test(fs, [], cx).await;
11906 let (multi_workspace, cx) =
11907 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
11908 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
11909
11910 // Open two docks (left and right) with one panel each
11911 let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| {
11912 let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
11913 workspace.add_panel(left_panel.clone(), window, cx);
11914
11915 let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
11916 workspace.add_panel(right_panel.clone(), window, cx);
11917
11918 workspace.toggle_dock(DockPosition::Left, window, cx);
11919 workspace.toggle_dock(DockPosition::Right, window, cx);
11920
11921 // Verify initial state
11922 assert!(
11923 workspace.left_dock().read(cx).is_open(),
11924 "Left dock should be open"
11925 );
11926 assert_eq!(
11927 workspace
11928 .left_dock()
11929 .read(cx)
11930 .visible_panel()
11931 .unwrap()
11932 .panel_id(),
11933 left_panel.panel_id(),
11934 "Left panel should be visible in left dock"
11935 );
11936 assert!(
11937 workspace.right_dock().read(cx).is_open(),
11938 "Right dock should be open"
11939 );
11940 assert_eq!(
11941 workspace
11942 .right_dock()
11943 .read(cx)
11944 .visible_panel()
11945 .unwrap()
11946 .panel_id(),
11947 right_panel.panel_id(),
11948 "Right panel should be visible in right dock"
11949 );
11950 assert!(
11951 !workspace.bottom_dock().read(cx).is_open(),
11952 "Bottom dock should be closed"
11953 );
11954
11955 (left_panel, right_panel)
11956 });
11957
11958 // Focus the left panel and move it to the next position (bottom dock)
11959 workspace.update_in(cx, |workspace, window, cx| {
11960 workspace.toggle_panel_focus::<TestPanel>(window, cx); // Focus left panel
11961 assert!(
11962 left_panel.read(cx).focus_handle(cx).is_focused(window),
11963 "Left panel should be focused"
11964 );
11965 });
11966
11967 cx.dispatch_action(MoveFocusedPanelToNextPosition);
11968
11969 // Verify the left panel has moved to the bottom dock, and the bottom dock is now open
11970 workspace.update(cx, |workspace, cx| {
11971 assert!(
11972 !workspace.left_dock().read(cx).is_open(),
11973 "Left dock should be closed"
11974 );
11975 assert!(
11976 workspace.bottom_dock().read(cx).is_open(),
11977 "Bottom dock should now be open"
11978 );
11979 assert_eq!(
11980 left_panel.read(cx).position,
11981 DockPosition::Bottom,
11982 "Left panel should now be in the bottom dock"
11983 );
11984 assert_eq!(
11985 workspace
11986 .bottom_dock()
11987 .read(cx)
11988 .visible_panel()
11989 .unwrap()
11990 .panel_id(),
11991 left_panel.panel_id(),
11992 "Left panel should be the visible panel in the bottom dock"
11993 );
11994 });
11995
11996 // Toggle all docks off
11997 workspace.update_in(cx, |workspace, window, cx| {
11998 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11999 assert!(
12000 !workspace.left_dock().read(cx).is_open(),
12001 "Left dock should be closed"
12002 );
12003 assert!(
12004 !workspace.right_dock().read(cx).is_open(),
12005 "Right dock should be closed"
12006 );
12007 assert!(
12008 !workspace.bottom_dock().read(cx).is_open(),
12009 "Bottom dock should be closed"
12010 );
12011 });
12012
12013 // Toggle all docks back on and verify positions are restored
12014 workspace.update_in(cx, |workspace, window, cx| {
12015 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12016 assert!(
12017 !workspace.left_dock().read(cx).is_open(),
12018 "Left dock should remain closed"
12019 );
12020 assert!(
12021 workspace.right_dock().read(cx).is_open(),
12022 "Right dock should remain open"
12023 );
12024 assert!(
12025 workspace.bottom_dock().read(cx).is_open(),
12026 "Bottom dock should remain open"
12027 );
12028 assert_eq!(
12029 left_panel.read(cx).position,
12030 DockPosition::Bottom,
12031 "Left panel should remain in the bottom dock"
12032 );
12033 assert_eq!(
12034 right_panel.read(cx).position,
12035 DockPosition::Right,
12036 "Right panel should remain in the right dock"
12037 );
12038 assert_eq!(
12039 workspace
12040 .bottom_dock()
12041 .read(cx)
12042 .visible_panel()
12043 .unwrap()
12044 .panel_id(),
12045 left_panel.panel_id(),
12046 "Left panel should be the visible panel in the right dock"
12047 );
12048 });
12049 }
12050
12051 #[gpui::test]
12052 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
12053 init_test(cx);
12054
12055 let fs = FakeFs::new(cx.executor());
12056
12057 let project = Project::test(fs, None, cx).await;
12058 let (workspace, cx) =
12059 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12060
12061 // Let's arrange the panes like this:
12062 //
12063 // +-----------------------+
12064 // | top |
12065 // +------+--------+-------+
12066 // | left | center | right |
12067 // +------+--------+-------+
12068 // | bottom |
12069 // +-----------------------+
12070
12071 let top_item = cx.new(|cx| {
12072 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
12073 });
12074 let bottom_item = cx.new(|cx| {
12075 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
12076 });
12077 let left_item = cx.new(|cx| {
12078 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
12079 });
12080 let right_item = cx.new(|cx| {
12081 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
12082 });
12083 let center_item = cx.new(|cx| {
12084 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
12085 });
12086
12087 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12088 let top_pane_id = workspace.active_pane().entity_id();
12089 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
12090 workspace.split_pane(
12091 workspace.active_pane().clone(),
12092 SplitDirection::Down,
12093 window,
12094 cx,
12095 );
12096 top_pane_id
12097 });
12098 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12099 let bottom_pane_id = workspace.active_pane().entity_id();
12100 workspace.add_item_to_active_pane(
12101 Box::new(bottom_item.clone()),
12102 None,
12103 false,
12104 window,
12105 cx,
12106 );
12107 workspace.split_pane(
12108 workspace.active_pane().clone(),
12109 SplitDirection::Up,
12110 window,
12111 cx,
12112 );
12113 bottom_pane_id
12114 });
12115 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12116 let left_pane_id = workspace.active_pane().entity_id();
12117 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
12118 workspace.split_pane(
12119 workspace.active_pane().clone(),
12120 SplitDirection::Right,
12121 window,
12122 cx,
12123 );
12124 left_pane_id
12125 });
12126 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12127 let right_pane_id = workspace.active_pane().entity_id();
12128 workspace.add_item_to_active_pane(
12129 Box::new(right_item.clone()),
12130 None,
12131 false,
12132 window,
12133 cx,
12134 );
12135 workspace.split_pane(
12136 workspace.active_pane().clone(),
12137 SplitDirection::Left,
12138 window,
12139 cx,
12140 );
12141 right_pane_id
12142 });
12143 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12144 let center_pane_id = workspace.active_pane().entity_id();
12145 workspace.add_item_to_active_pane(
12146 Box::new(center_item.clone()),
12147 None,
12148 false,
12149 window,
12150 cx,
12151 );
12152 center_pane_id
12153 });
12154 cx.executor().run_until_parked();
12155
12156 workspace.update_in(cx, |workspace, window, cx| {
12157 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
12158
12159 // Join into next from center pane into right
12160 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12161 });
12162
12163 workspace.update_in(cx, |workspace, window, cx| {
12164 let active_pane = workspace.active_pane();
12165 assert_eq!(right_pane_id, active_pane.entity_id());
12166 assert_eq!(2, active_pane.read(cx).items_len());
12167 let item_ids_in_pane =
12168 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12169 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12170 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12171
12172 // Join into next from right pane into bottom
12173 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12174 });
12175
12176 workspace.update_in(cx, |workspace, window, cx| {
12177 let active_pane = workspace.active_pane();
12178 assert_eq!(bottom_pane_id, active_pane.entity_id());
12179 assert_eq!(3, active_pane.read(cx).items_len());
12180 let item_ids_in_pane =
12181 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12182 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12183 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12184 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12185
12186 // Join into next from bottom pane into left
12187 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12188 });
12189
12190 workspace.update_in(cx, |workspace, window, cx| {
12191 let active_pane = workspace.active_pane();
12192 assert_eq!(left_pane_id, active_pane.entity_id());
12193 assert_eq!(4, active_pane.read(cx).items_len());
12194 let item_ids_in_pane =
12195 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12196 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12197 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12198 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12199 assert!(item_ids_in_pane.contains(&left_item.item_id()));
12200
12201 // Join into next from left pane into top
12202 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12203 });
12204
12205 workspace.update_in(cx, |workspace, window, cx| {
12206 let active_pane = workspace.active_pane();
12207 assert_eq!(top_pane_id, active_pane.entity_id());
12208 assert_eq!(5, active_pane.read(cx).items_len());
12209 let item_ids_in_pane =
12210 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12211 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12212 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12213 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12214 assert!(item_ids_in_pane.contains(&left_item.item_id()));
12215 assert!(item_ids_in_pane.contains(&top_item.item_id()));
12216
12217 // Single pane left: no-op
12218 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
12219 });
12220
12221 workspace.update(cx, |workspace, _cx| {
12222 let active_pane = workspace.active_pane();
12223 assert_eq!(top_pane_id, active_pane.entity_id());
12224 });
12225 }
12226
12227 fn add_an_item_to_active_pane(
12228 cx: &mut VisualTestContext,
12229 workspace: &Entity<Workspace>,
12230 item_id: u64,
12231 ) -> Entity<TestItem> {
12232 let item = cx.new(|cx| {
12233 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
12234 item_id,
12235 "item{item_id}.txt",
12236 cx,
12237 )])
12238 });
12239 workspace.update_in(cx, |workspace, window, cx| {
12240 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
12241 });
12242 item
12243 }
12244
12245 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
12246 workspace.update_in(cx, |workspace, window, cx| {
12247 workspace.split_pane(
12248 workspace.active_pane().clone(),
12249 SplitDirection::Right,
12250 window,
12251 cx,
12252 )
12253 })
12254 }
12255
12256 #[gpui::test]
12257 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
12258 init_test(cx);
12259 let fs = FakeFs::new(cx.executor());
12260 let project = Project::test(fs, None, cx).await;
12261 let (workspace, cx) =
12262 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12263
12264 add_an_item_to_active_pane(cx, &workspace, 1);
12265 split_pane(cx, &workspace);
12266 add_an_item_to_active_pane(cx, &workspace, 2);
12267 split_pane(cx, &workspace); // empty pane
12268 split_pane(cx, &workspace);
12269 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
12270
12271 cx.executor().run_until_parked();
12272
12273 workspace.update(cx, |workspace, cx| {
12274 let num_panes = workspace.panes().len();
12275 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
12276 let active_item = workspace
12277 .active_pane()
12278 .read(cx)
12279 .active_item()
12280 .expect("item is in focus");
12281
12282 assert_eq!(num_panes, 4);
12283 assert_eq!(num_items_in_current_pane, 1);
12284 assert_eq!(active_item.item_id(), last_item.item_id());
12285 });
12286
12287 workspace.update_in(cx, |workspace, window, cx| {
12288 workspace.join_all_panes(window, cx);
12289 });
12290
12291 workspace.update(cx, |workspace, cx| {
12292 let num_panes = workspace.panes().len();
12293 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
12294 let active_item = workspace
12295 .active_pane()
12296 .read(cx)
12297 .active_item()
12298 .expect("item is in focus");
12299
12300 assert_eq!(num_panes, 1);
12301 assert_eq!(num_items_in_current_pane, 3);
12302 assert_eq!(active_item.item_id(), last_item.item_id());
12303 });
12304 }
12305
12306 #[gpui::test]
12307 async fn test_flexible_dock_sizing(cx: &mut gpui::TestAppContext) {
12308 init_test(cx);
12309 let fs = FakeFs::new(cx.executor());
12310
12311 let project = Project::test(fs, [], cx).await;
12312 let (multi_workspace, cx) =
12313 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12314 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12315
12316 workspace.update(cx, |workspace, _cx| {
12317 workspace.bounds.size.width = px(800.);
12318 });
12319
12320 workspace.update_in(cx, |workspace, window, cx| {
12321 let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
12322 workspace.add_panel(panel, window, cx);
12323 workspace.toggle_dock(DockPosition::Right, window, cx);
12324 });
12325
12326 let (panel, resized_width, ratio_basis_width) =
12327 workspace.update_in(cx, |workspace, window, cx| {
12328 let item = cx.new(|cx| {
12329 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
12330 });
12331 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
12332
12333 let dock = workspace.right_dock().read(cx);
12334 let workspace_width = workspace.bounds.size.width;
12335 let initial_width = workspace
12336 .dock_size(&dock, window, cx)
12337 .expect("flexible dock should have an initial width");
12338
12339 assert_eq!(initial_width, workspace_width / 2.);
12340
12341 workspace.resize_right_dock(px(300.), window, cx);
12342
12343 let dock = workspace.right_dock().read(cx);
12344 let resized_width = workspace
12345 .dock_size(&dock, window, cx)
12346 .expect("flexible dock should keep its resized width");
12347
12348 assert_eq!(resized_width, px(300.));
12349
12350 let panel = workspace
12351 .right_dock()
12352 .read(cx)
12353 .visible_panel()
12354 .expect("flexible dock should have a visible panel")
12355 .panel_id();
12356
12357 (panel, resized_width, workspace_width)
12358 });
12359
12360 workspace.update_in(cx, |workspace, window, cx| {
12361 workspace.toggle_dock(DockPosition::Right, window, cx);
12362 workspace.toggle_dock(DockPosition::Right, window, cx);
12363
12364 let dock = workspace.right_dock().read(cx);
12365 let reopened_width = workspace
12366 .dock_size(&dock, window, cx)
12367 .expect("flexible dock should restore when reopened");
12368
12369 assert_eq!(reopened_width, resized_width);
12370
12371 let right_dock = workspace.right_dock().read(cx);
12372 let flexible_panel = right_dock
12373 .visible_panel()
12374 .expect("flexible dock should still have a visible panel");
12375 assert_eq!(flexible_panel.panel_id(), panel);
12376 assert_eq!(
12377 right_dock
12378 .stored_panel_size_state(flexible_panel.as_ref())
12379 .and_then(|size_state| size_state.flexible_size_ratio),
12380 Some(resized_width.to_f64() as f32 / workspace.bounds.size.width.to_f64() as f32)
12381 );
12382 });
12383
12384 workspace.update_in(cx, |workspace, window, cx| {
12385 workspace.split_pane(
12386 workspace.active_pane().clone(),
12387 SplitDirection::Right,
12388 window,
12389 cx,
12390 );
12391
12392 let dock = workspace.right_dock().read(cx);
12393 let split_width = workspace
12394 .dock_size(&dock, window, cx)
12395 .expect("flexible dock should keep its user-resized proportion");
12396
12397 assert_eq!(split_width, px(300.));
12398
12399 workspace.bounds.size.width = px(1600.);
12400
12401 let dock = workspace.right_dock().read(cx);
12402 let resized_window_width = workspace
12403 .dock_size(&dock, window, cx)
12404 .expect("flexible dock should preserve proportional size on window resize");
12405
12406 assert_eq!(
12407 resized_window_width,
12408 workspace.bounds.size.width
12409 * (resized_width.to_f64() as f32 / ratio_basis_width.to_f64() as f32)
12410 );
12411 });
12412 }
12413
12414 #[gpui::test]
12415 async fn test_panel_size_state_persistence(cx: &mut gpui::TestAppContext) {
12416 init_test(cx);
12417 let fs = FakeFs::new(cx.executor());
12418
12419 // Fixed-width panel: pixel size is persisted to KVP and restored on re-add.
12420 {
12421 let project = Project::test(fs.clone(), [], cx).await;
12422 let (multi_workspace, cx) =
12423 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12424 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12425
12426 workspace.update(cx, |workspace, _cx| {
12427 workspace.set_random_database_id();
12428 workspace.bounds.size.width = px(800.);
12429 });
12430
12431 let panel = workspace.update_in(cx, |workspace, window, cx| {
12432 let panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
12433 workspace.add_panel(panel.clone(), window, cx);
12434 workspace.toggle_dock(DockPosition::Left, window, cx);
12435 panel
12436 });
12437
12438 workspace.update_in(cx, |workspace, window, cx| {
12439 workspace.resize_left_dock(px(350.), window, cx);
12440 });
12441
12442 cx.run_until_parked();
12443
12444 let persisted = workspace.read_with(cx, |workspace, cx| {
12445 workspace.persisted_panel_size_state(TestPanel::panel_key(), cx)
12446 });
12447 assert_eq!(
12448 persisted.and_then(|s| s.size),
12449 Some(px(350.)),
12450 "fixed-width panel size should be persisted to KVP"
12451 );
12452
12453 // Remove the panel and re-add a fresh instance with the same key.
12454 // The new instance should have its size state restored from KVP.
12455 workspace.update_in(cx, |workspace, window, cx| {
12456 workspace.remove_panel(&panel, window, cx);
12457 });
12458
12459 workspace.update_in(cx, |workspace, window, cx| {
12460 let new_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
12461 workspace.add_panel(new_panel, window, cx);
12462
12463 let left_dock = workspace.left_dock().read(cx);
12464 let size_state = left_dock
12465 .panel::<TestPanel>()
12466 .and_then(|p| left_dock.stored_panel_size_state(&p));
12467 assert_eq!(
12468 size_state.and_then(|s| s.size),
12469 Some(px(350.)),
12470 "re-added fixed-width panel should restore persisted size from KVP"
12471 );
12472 });
12473 }
12474
12475 // Flexible panel: both pixel size and ratio are persisted and restored.
12476 {
12477 let project = Project::test(fs.clone(), [], cx).await;
12478 let (multi_workspace, cx) =
12479 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12480 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12481
12482 workspace.update(cx, |workspace, _cx| {
12483 workspace.set_random_database_id();
12484 workspace.bounds.size.width = px(800.);
12485 });
12486
12487 let panel = workspace.update_in(cx, |workspace, window, cx| {
12488 let item = cx.new(|cx| {
12489 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
12490 });
12491 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
12492
12493 let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
12494 workspace.add_panel(panel.clone(), window, cx);
12495 workspace.toggle_dock(DockPosition::Right, window, cx);
12496 panel
12497 });
12498
12499 workspace.update_in(cx, |workspace, window, cx| {
12500 workspace.resize_right_dock(px(300.), window, cx);
12501 });
12502
12503 cx.run_until_parked();
12504
12505 let persisted = workspace
12506 .read_with(cx, |workspace, cx| {
12507 workspace.persisted_panel_size_state(TestPanel::panel_key(), cx)
12508 })
12509 .expect("flexible panel state should be persisted to KVP");
12510 assert_eq!(
12511 persisted.size, None,
12512 "flexible panel should not persist a redundant pixel size"
12513 );
12514 let original_ratio = persisted
12515 .flexible_size_ratio
12516 .expect("flexible panel ratio should be persisted");
12517
12518 // Remove the panel and re-add: both size and ratio should be restored.
12519 workspace.update_in(cx, |workspace, window, cx| {
12520 workspace.remove_panel(&panel, window, cx);
12521 });
12522
12523 workspace.update_in(cx, |workspace, window, cx| {
12524 let new_panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
12525 workspace.add_panel(new_panel, window, cx);
12526
12527 let right_dock = workspace.right_dock().read(cx);
12528 let size_state = right_dock
12529 .panel::<TestPanel>()
12530 .and_then(|p| right_dock.stored_panel_size_state(&p))
12531 .expect("re-added flexible panel should have restored size state from KVP");
12532 assert_eq!(
12533 size_state.size, None,
12534 "re-added flexible panel should not have a persisted pixel size"
12535 );
12536 assert_eq!(
12537 size_state.flexible_size_ratio,
12538 Some(original_ratio),
12539 "re-added flexible panel should restore persisted ratio"
12540 );
12541 });
12542 }
12543 }
12544
12545 #[gpui::test]
12546 async fn test_flexible_panel_left_dock_sizing(cx: &mut gpui::TestAppContext) {
12547 init_test(cx);
12548 let fs = FakeFs::new(cx.executor());
12549
12550 let project = Project::test(fs, [], cx).await;
12551 let (multi_workspace, cx) =
12552 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12553 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12554
12555 workspace.update(cx, |workspace, _cx| {
12556 workspace.bounds.size.width = px(900.);
12557 });
12558
12559 // Step 1: Add a tab to the center pane then open a flexible panel in the left
12560 // dock. With one full-width center pane the default ratio is 0.5, so the panel
12561 // and the center pane each take half the workspace width.
12562 workspace.update_in(cx, |workspace, window, cx| {
12563 let item = cx.new(|cx| {
12564 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
12565 });
12566 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
12567
12568 let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Left, 100, cx));
12569 workspace.add_panel(panel, window, cx);
12570 workspace.toggle_dock(DockPosition::Left, window, cx);
12571
12572 let left_dock = workspace.left_dock().read(cx);
12573 let left_width = workspace
12574 .dock_size(&left_dock, window, cx)
12575 .expect("left dock should have an active panel");
12576
12577 assert_eq!(
12578 left_width,
12579 workspace.bounds.size.width / 2.,
12580 "flexible left panel should split evenly with the center pane"
12581 );
12582 });
12583
12584 // Step 2: Split the center pane vertically (top/bottom). Vertical splits do not
12585 // change horizontal width fractions, so the flexible panel stays at the same
12586 // width as each half of the split.
12587 workspace.update_in(cx, |workspace, window, cx| {
12588 workspace.split_pane(
12589 workspace.active_pane().clone(),
12590 SplitDirection::Down,
12591 window,
12592 cx,
12593 );
12594
12595 let left_dock = workspace.left_dock().read(cx);
12596 let left_width = workspace
12597 .dock_size(&left_dock, window, cx)
12598 .expect("left dock should still have an active panel after vertical split");
12599
12600 assert_eq!(
12601 left_width,
12602 workspace.bounds.size.width / 2.,
12603 "flexible left panel width should match each vertically-split pane"
12604 );
12605 });
12606
12607 // Step 3: Open a fixed-width panel in the right dock. The right dock's default
12608 // size reduces the available width, so the flexible left panel and the center
12609 // panes all shrink proportionally to accommodate it.
12610 workspace.update_in(cx, |workspace, window, cx| {
12611 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 200, cx));
12612 workspace.add_panel(panel, window, cx);
12613 workspace.toggle_dock(DockPosition::Right, window, cx);
12614
12615 let right_dock = workspace.right_dock().read(cx);
12616 let right_width = workspace
12617 .dock_size(&right_dock, window, cx)
12618 .expect("right dock should have an active panel");
12619
12620 let left_dock = workspace.left_dock().read(cx);
12621 let left_width = workspace
12622 .dock_size(&left_dock, window, cx)
12623 .expect("left dock should still have an active panel");
12624
12625 let available_width = workspace.bounds.size.width - right_width;
12626 assert_eq!(
12627 left_width,
12628 available_width / 2.,
12629 "flexible left panel should shrink proportionally as the right dock takes space"
12630 );
12631 });
12632 }
12633
12634 struct TestModal(FocusHandle);
12635
12636 impl TestModal {
12637 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
12638 Self(cx.focus_handle())
12639 }
12640 }
12641
12642 impl EventEmitter<DismissEvent> for TestModal {}
12643
12644 impl Focusable for TestModal {
12645 fn focus_handle(&self, _cx: &App) -> FocusHandle {
12646 self.0.clone()
12647 }
12648 }
12649
12650 impl ModalView for TestModal {}
12651
12652 impl Render for TestModal {
12653 fn render(
12654 &mut self,
12655 _window: &mut Window,
12656 _cx: &mut Context<TestModal>,
12657 ) -> impl IntoElement {
12658 div().track_focus(&self.0)
12659 }
12660 }
12661
12662 #[gpui::test]
12663 async fn test_panels(cx: &mut gpui::TestAppContext) {
12664 init_test(cx);
12665 let fs = FakeFs::new(cx.executor());
12666
12667 let project = Project::test(fs, [], cx).await;
12668 let (multi_workspace, cx) =
12669 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12670 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12671
12672 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
12673 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
12674 workspace.add_panel(panel_1.clone(), window, cx);
12675 workspace.toggle_dock(DockPosition::Left, window, cx);
12676 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
12677 workspace.add_panel(panel_2.clone(), window, cx);
12678 workspace.toggle_dock(DockPosition::Right, window, cx);
12679
12680 let left_dock = workspace.left_dock();
12681 assert_eq!(
12682 left_dock.read(cx).visible_panel().unwrap().panel_id(),
12683 panel_1.panel_id()
12684 );
12685 assert_eq!(
12686 workspace.dock_size(&left_dock.read(cx), window, cx),
12687 Some(px(300.))
12688 );
12689
12690 workspace.resize_left_dock(px(1337.), window, cx);
12691 assert_eq!(
12692 workspace
12693 .right_dock()
12694 .read(cx)
12695 .visible_panel()
12696 .unwrap()
12697 .panel_id(),
12698 panel_2.panel_id(),
12699 );
12700
12701 (panel_1, panel_2)
12702 });
12703
12704 // Move panel_1 to the right
12705 panel_1.update_in(cx, |panel_1, window, cx| {
12706 panel_1.set_position(DockPosition::Right, window, cx)
12707 });
12708
12709 workspace.update_in(cx, |workspace, window, cx| {
12710 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
12711 // Since it was the only panel on the left, the left dock should now be closed.
12712 assert!(!workspace.left_dock().read(cx).is_open());
12713 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
12714 let right_dock = workspace.right_dock();
12715 assert_eq!(
12716 right_dock.read(cx).visible_panel().unwrap().panel_id(),
12717 panel_1.panel_id()
12718 );
12719 assert_eq!(
12720 right_dock
12721 .read(cx)
12722 .active_panel_size()
12723 .unwrap()
12724 .size
12725 .unwrap(),
12726 px(1337.)
12727 );
12728
12729 // Now we move panel_2 to the left
12730 panel_2.set_position(DockPosition::Left, window, cx);
12731 });
12732
12733 workspace.update(cx, |workspace, cx| {
12734 // Since panel_2 was not visible on the right, we don't open the left dock.
12735 assert!(!workspace.left_dock().read(cx).is_open());
12736 // And the right dock is unaffected in its displaying of panel_1
12737 assert!(workspace.right_dock().read(cx).is_open());
12738 assert_eq!(
12739 workspace
12740 .right_dock()
12741 .read(cx)
12742 .visible_panel()
12743 .unwrap()
12744 .panel_id(),
12745 panel_1.panel_id(),
12746 );
12747 });
12748
12749 // Move panel_1 back to the left
12750 panel_1.update_in(cx, |panel_1, window, cx| {
12751 panel_1.set_position(DockPosition::Left, window, cx)
12752 });
12753
12754 workspace.update_in(cx, |workspace, window, cx| {
12755 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
12756 let left_dock = workspace.left_dock();
12757 assert!(left_dock.read(cx).is_open());
12758 assert_eq!(
12759 left_dock.read(cx).visible_panel().unwrap().panel_id(),
12760 panel_1.panel_id()
12761 );
12762 assert_eq!(
12763 workspace.dock_size(&left_dock.read(cx), window, cx),
12764 Some(px(1337.))
12765 );
12766 // And the right dock should be closed as it no longer has any panels.
12767 assert!(!workspace.right_dock().read(cx).is_open());
12768
12769 // Now we move panel_1 to the bottom
12770 panel_1.set_position(DockPosition::Bottom, window, cx);
12771 });
12772
12773 workspace.update_in(cx, |workspace, window, cx| {
12774 // Since panel_1 was visible on the left, we close the left dock.
12775 assert!(!workspace.left_dock().read(cx).is_open());
12776 // The bottom dock is sized based on the panel's default size,
12777 // since the panel orientation changed from vertical to horizontal.
12778 let bottom_dock = workspace.bottom_dock();
12779 assert_eq!(
12780 workspace.dock_size(&bottom_dock.read(cx), window, cx),
12781 Some(px(300.))
12782 );
12783 // Close bottom dock and move panel_1 back to the left.
12784 bottom_dock.update(cx, |bottom_dock, cx| {
12785 bottom_dock.set_open(false, window, cx)
12786 });
12787 panel_1.set_position(DockPosition::Left, window, cx);
12788 });
12789
12790 // Emit activated event on panel 1
12791 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
12792
12793 // Now the left dock is open and panel_1 is active and focused.
12794 workspace.update_in(cx, |workspace, window, cx| {
12795 let left_dock = workspace.left_dock();
12796 assert!(left_dock.read(cx).is_open());
12797 assert_eq!(
12798 left_dock.read(cx).visible_panel().unwrap().panel_id(),
12799 panel_1.panel_id(),
12800 );
12801 assert!(panel_1.focus_handle(cx).is_focused(window));
12802 });
12803
12804 // Emit closed event on panel 2, which is not active
12805 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
12806
12807 // Wo don't close the left dock, because panel_2 wasn't the active panel
12808 workspace.update(cx, |workspace, cx| {
12809 let left_dock = workspace.left_dock();
12810 assert!(left_dock.read(cx).is_open());
12811 assert_eq!(
12812 left_dock.read(cx).visible_panel().unwrap().panel_id(),
12813 panel_1.panel_id(),
12814 );
12815 });
12816
12817 // Emitting a ZoomIn event shows the panel as zoomed.
12818 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
12819 workspace.read_with(cx, |workspace, _| {
12820 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
12821 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
12822 });
12823
12824 // Move panel to another dock while it is zoomed
12825 panel_1.update_in(cx, |panel, window, cx| {
12826 panel.set_position(DockPosition::Right, window, cx)
12827 });
12828 workspace.read_with(cx, |workspace, _| {
12829 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
12830
12831 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
12832 });
12833
12834 // This is a helper for getting a:
12835 // - valid focus on an element,
12836 // - that isn't a part of the panes and panels system of the Workspace,
12837 // - and doesn't trigger the 'on_focus_lost' API.
12838 let focus_other_view = {
12839 let workspace = workspace.clone();
12840 move |cx: &mut VisualTestContext| {
12841 workspace.update_in(cx, |workspace, window, cx| {
12842 if workspace.active_modal::<TestModal>(cx).is_some() {
12843 workspace.toggle_modal(window, cx, TestModal::new);
12844 workspace.toggle_modal(window, cx, TestModal::new);
12845 } else {
12846 workspace.toggle_modal(window, cx, TestModal::new);
12847 }
12848 })
12849 }
12850 };
12851
12852 // If focus is transferred to another view that's not a panel or another pane, we still show
12853 // the panel as zoomed.
12854 focus_other_view(cx);
12855 workspace.read_with(cx, |workspace, _| {
12856 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
12857 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
12858 });
12859
12860 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
12861 workspace.update_in(cx, |_workspace, window, cx| {
12862 cx.focus_self(window);
12863 });
12864 workspace.read_with(cx, |workspace, _| {
12865 assert_eq!(workspace.zoomed, None);
12866 assert_eq!(workspace.zoomed_position, None);
12867 });
12868
12869 // If focus is transferred again to another view that's not a panel or a pane, we won't
12870 // show the panel as zoomed because it wasn't zoomed before.
12871 focus_other_view(cx);
12872 workspace.read_with(cx, |workspace, _| {
12873 assert_eq!(workspace.zoomed, None);
12874 assert_eq!(workspace.zoomed_position, None);
12875 });
12876
12877 // When the panel is activated, it is zoomed again.
12878 cx.dispatch_action(ToggleRightDock);
12879 workspace.read_with(cx, |workspace, _| {
12880 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
12881 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
12882 });
12883
12884 // Emitting a ZoomOut event unzooms the panel.
12885 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
12886 workspace.read_with(cx, |workspace, _| {
12887 assert_eq!(workspace.zoomed, None);
12888 assert_eq!(workspace.zoomed_position, None);
12889 });
12890
12891 // Emit closed event on panel 1, which is active
12892 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
12893
12894 // Now the left dock is closed, because panel_1 was the active panel
12895 workspace.update(cx, |workspace, cx| {
12896 let right_dock = workspace.right_dock();
12897 assert!(!right_dock.read(cx).is_open());
12898 });
12899 }
12900
12901 #[gpui::test]
12902 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
12903 init_test(cx);
12904
12905 let fs = FakeFs::new(cx.background_executor.clone());
12906 let project = Project::test(fs, [], cx).await;
12907 let (workspace, cx) =
12908 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12909 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12910
12911 let dirty_regular_buffer = cx.new(|cx| {
12912 TestItem::new(cx)
12913 .with_dirty(true)
12914 .with_label("1.txt")
12915 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
12916 });
12917 let dirty_regular_buffer_2 = cx.new(|cx| {
12918 TestItem::new(cx)
12919 .with_dirty(true)
12920 .with_label("2.txt")
12921 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
12922 });
12923 let dirty_multi_buffer_with_both = cx.new(|cx| {
12924 TestItem::new(cx)
12925 .with_dirty(true)
12926 .with_buffer_kind(ItemBufferKind::Multibuffer)
12927 .with_label("Fake Project Search")
12928 .with_project_items(&[
12929 dirty_regular_buffer.read(cx).project_items[0].clone(),
12930 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
12931 ])
12932 });
12933 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
12934 workspace.update_in(cx, |workspace, window, cx| {
12935 workspace.add_item(
12936 pane.clone(),
12937 Box::new(dirty_regular_buffer.clone()),
12938 None,
12939 false,
12940 false,
12941 window,
12942 cx,
12943 );
12944 workspace.add_item(
12945 pane.clone(),
12946 Box::new(dirty_regular_buffer_2.clone()),
12947 None,
12948 false,
12949 false,
12950 window,
12951 cx,
12952 );
12953 workspace.add_item(
12954 pane.clone(),
12955 Box::new(dirty_multi_buffer_with_both.clone()),
12956 None,
12957 false,
12958 false,
12959 window,
12960 cx,
12961 );
12962 });
12963
12964 pane.update_in(cx, |pane, window, cx| {
12965 pane.activate_item(2, true, true, window, cx);
12966 assert_eq!(
12967 pane.active_item().unwrap().item_id(),
12968 multi_buffer_with_both_files_id,
12969 "Should select the multi buffer in the pane"
12970 );
12971 });
12972 let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
12973 pane.close_other_items(
12974 &CloseOtherItems {
12975 save_intent: Some(SaveIntent::Save),
12976 close_pinned: true,
12977 },
12978 None,
12979 window,
12980 cx,
12981 )
12982 });
12983 cx.background_executor.run_until_parked();
12984 assert!(!cx.has_pending_prompt());
12985 close_all_but_multi_buffer_task
12986 .await
12987 .expect("Closing all buffers but the multi buffer failed");
12988 pane.update(cx, |pane, cx| {
12989 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
12990 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
12991 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
12992 assert_eq!(pane.items_len(), 1);
12993 assert_eq!(
12994 pane.active_item().unwrap().item_id(),
12995 multi_buffer_with_both_files_id,
12996 "Should have only the multi buffer left in the pane"
12997 );
12998 assert!(
12999 dirty_multi_buffer_with_both.read(cx).is_dirty,
13000 "The multi buffer containing the unsaved buffer should still be dirty"
13001 );
13002 });
13003
13004 dirty_regular_buffer.update(cx, |buffer, cx| {
13005 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
13006 });
13007
13008 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13009 pane.close_active_item(
13010 &CloseActiveItem {
13011 save_intent: Some(SaveIntent::Close),
13012 close_pinned: false,
13013 },
13014 window,
13015 cx,
13016 )
13017 });
13018 cx.background_executor.run_until_parked();
13019 assert!(
13020 cx.has_pending_prompt(),
13021 "Dirty multi buffer should prompt a save dialog"
13022 );
13023 cx.simulate_prompt_answer("Save");
13024 cx.background_executor.run_until_parked();
13025 close_multi_buffer_task
13026 .await
13027 .expect("Closing the multi buffer failed");
13028 pane.update(cx, |pane, cx| {
13029 assert_eq!(
13030 dirty_multi_buffer_with_both.read(cx).save_count,
13031 1,
13032 "Multi buffer item should get be saved"
13033 );
13034 // Test impl does not save inner items, so we do not assert them
13035 assert_eq!(
13036 pane.items_len(),
13037 0,
13038 "No more items should be left in the pane"
13039 );
13040 assert!(pane.active_item().is_none());
13041 });
13042 }
13043
13044 #[gpui::test]
13045 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
13046 cx: &mut TestAppContext,
13047 ) {
13048 init_test(cx);
13049
13050 let fs = FakeFs::new(cx.background_executor.clone());
13051 let project = Project::test(fs, [], cx).await;
13052 let (workspace, cx) =
13053 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13054 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13055
13056 let dirty_regular_buffer = cx.new(|cx| {
13057 TestItem::new(cx)
13058 .with_dirty(true)
13059 .with_label("1.txt")
13060 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
13061 });
13062 let dirty_regular_buffer_2 = cx.new(|cx| {
13063 TestItem::new(cx)
13064 .with_dirty(true)
13065 .with_label("2.txt")
13066 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
13067 });
13068 let clear_regular_buffer = cx.new(|cx| {
13069 TestItem::new(cx)
13070 .with_label("3.txt")
13071 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
13072 });
13073
13074 let dirty_multi_buffer_with_both = cx.new(|cx| {
13075 TestItem::new(cx)
13076 .with_dirty(true)
13077 .with_buffer_kind(ItemBufferKind::Multibuffer)
13078 .with_label("Fake Project Search")
13079 .with_project_items(&[
13080 dirty_regular_buffer.read(cx).project_items[0].clone(),
13081 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
13082 clear_regular_buffer.read(cx).project_items[0].clone(),
13083 ])
13084 });
13085 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
13086 workspace.update_in(cx, |workspace, window, cx| {
13087 workspace.add_item(
13088 pane.clone(),
13089 Box::new(dirty_regular_buffer.clone()),
13090 None,
13091 false,
13092 false,
13093 window,
13094 cx,
13095 );
13096 workspace.add_item(
13097 pane.clone(),
13098 Box::new(dirty_multi_buffer_with_both.clone()),
13099 None,
13100 false,
13101 false,
13102 window,
13103 cx,
13104 );
13105 });
13106
13107 pane.update_in(cx, |pane, window, cx| {
13108 pane.activate_item(1, true, true, window, cx);
13109 assert_eq!(
13110 pane.active_item().unwrap().item_id(),
13111 multi_buffer_with_both_files_id,
13112 "Should select the multi buffer in the pane"
13113 );
13114 });
13115 let _close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13116 pane.close_active_item(
13117 &CloseActiveItem {
13118 save_intent: None,
13119 close_pinned: false,
13120 },
13121 window,
13122 cx,
13123 )
13124 });
13125 cx.background_executor.run_until_parked();
13126 assert!(
13127 cx.has_pending_prompt(),
13128 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
13129 );
13130 }
13131
13132 /// Tests that when `close_on_file_delete` is enabled, files are automatically
13133 /// closed when they are deleted from disk.
13134 #[gpui::test]
13135 async fn test_close_on_disk_deletion_enabled(cx: &mut TestAppContext) {
13136 init_test(cx);
13137
13138 // Enable the close_on_disk_deletion setting
13139 cx.update_global(|store: &mut SettingsStore, cx| {
13140 store.update_user_settings(cx, |settings| {
13141 settings.workspace.close_on_file_delete = Some(true);
13142 });
13143 });
13144
13145 let fs = FakeFs::new(cx.background_executor.clone());
13146 let project = Project::test(fs, [], cx).await;
13147 let (workspace, cx) =
13148 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13149 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13150
13151 // Create a test item that simulates a file
13152 let item = cx.new(|cx| {
13153 TestItem::new(cx)
13154 .with_label("test.txt")
13155 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13156 });
13157
13158 // Add item to workspace
13159 workspace.update_in(cx, |workspace, window, cx| {
13160 workspace.add_item(
13161 pane.clone(),
13162 Box::new(item.clone()),
13163 None,
13164 false,
13165 false,
13166 window,
13167 cx,
13168 );
13169 });
13170
13171 // Verify the item is in the pane
13172 pane.read_with(cx, |pane, _| {
13173 assert_eq!(pane.items().count(), 1);
13174 });
13175
13176 // Simulate file deletion by setting the item's deleted state
13177 item.update(cx, |item, _| {
13178 item.set_has_deleted_file(true);
13179 });
13180
13181 // Emit UpdateTab event to trigger the close behavior
13182 cx.run_until_parked();
13183 item.update(cx, |_, cx| {
13184 cx.emit(ItemEvent::UpdateTab);
13185 });
13186
13187 // Allow the close operation to complete
13188 cx.run_until_parked();
13189
13190 // Verify the item was automatically closed
13191 pane.read_with(cx, |pane, _| {
13192 assert_eq!(
13193 pane.items().count(),
13194 0,
13195 "Item should be automatically closed when file is deleted"
13196 );
13197 });
13198 }
13199
13200 /// Tests that when `close_on_file_delete` is disabled (default), files remain
13201 /// open with a strikethrough when they are deleted from disk.
13202 #[gpui::test]
13203 async fn test_close_on_disk_deletion_disabled(cx: &mut TestAppContext) {
13204 init_test(cx);
13205
13206 // Ensure close_on_disk_deletion is disabled (default)
13207 cx.update_global(|store: &mut SettingsStore, cx| {
13208 store.update_user_settings(cx, |settings| {
13209 settings.workspace.close_on_file_delete = Some(false);
13210 });
13211 });
13212
13213 let fs = FakeFs::new(cx.background_executor.clone());
13214 let project = Project::test(fs, [], cx).await;
13215 let (workspace, cx) =
13216 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13217 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13218
13219 // Create a test item that simulates a file
13220 let item = cx.new(|cx| {
13221 TestItem::new(cx)
13222 .with_label("test.txt")
13223 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13224 });
13225
13226 // Add item to workspace
13227 workspace.update_in(cx, |workspace, window, cx| {
13228 workspace.add_item(
13229 pane.clone(),
13230 Box::new(item.clone()),
13231 None,
13232 false,
13233 false,
13234 window,
13235 cx,
13236 );
13237 });
13238
13239 // Verify the item is in the pane
13240 pane.read_with(cx, |pane, _| {
13241 assert_eq!(pane.items().count(), 1);
13242 });
13243
13244 // Simulate file deletion
13245 item.update(cx, |item, _| {
13246 item.set_has_deleted_file(true);
13247 });
13248
13249 // Emit UpdateTab event
13250 cx.run_until_parked();
13251 item.update(cx, |_, cx| {
13252 cx.emit(ItemEvent::UpdateTab);
13253 });
13254
13255 // Allow any potential close operation to complete
13256 cx.run_until_parked();
13257
13258 // Verify the item remains open (with strikethrough)
13259 pane.read_with(cx, |pane, _| {
13260 assert_eq!(
13261 pane.items().count(),
13262 1,
13263 "Item should remain open when close_on_disk_deletion is disabled"
13264 );
13265 });
13266
13267 // Verify the item shows as deleted
13268 item.read_with(cx, |item, _| {
13269 assert!(
13270 item.has_deleted_file,
13271 "Item should be marked as having deleted file"
13272 );
13273 });
13274 }
13275
13276 /// Tests that dirty files are not automatically closed when deleted from disk,
13277 /// even when `close_on_file_delete` is enabled. This ensures users don't lose
13278 /// unsaved changes without being prompted.
13279 #[gpui::test]
13280 async fn test_close_on_disk_deletion_with_dirty_file(cx: &mut TestAppContext) {
13281 init_test(cx);
13282
13283 // Enable the close_on_file_delete setting
13284 cx.update_global(|store: &mut SettingsStore, cx| {
13285 store.update_user_settings(cx, |settings| {
13286 settings.workspace.close_on_file_delete = Some(true);
13287 });
13288 });
13289
13290 let fs = FakeFs::new(cx.background_executor.clone());
13291 let project = Project::test(fs, [], cx).await;
13292 let (workspace, cx) =
13293 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13294 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13295
13296 // Create a dirty test item
13297 let item = cx.new(|cx| {
13298 TestItem::new(cx)
13299 .with_dirty(true)
13300 .with_label("test.txt")
13301 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13302 });
13303
13304 // Add item to workspace
13305 workspace.update_in(cx, |workspace, window, cx| {
13306 workspace.add_item(
13307 pane.clone(),
13308 Box::new(item.clone()),
13309 None,
13310 false,
13311 false,
13312 window,
13313 cx,
13314 );
13315 });
13316
13317 // Simulate file deletion
13318 item.update(cx, |item, _| {
13319 item.set_has_deleted_file(true);
13320 });
13321
13322 // Emit UpdateTab event to trigger the close behavior
13323 cx.run_until_parked();
13324 item.update(cx, |_, cx| {
13325 cx.emit(ItemEvent::UpdateTab);
13326 });
13327
13328 // Allow any potential close operation to complete
13329 cx.run_until_parked();
13330
13331 // Verify the item remains open (dirty files are not auto-closed)
13332 pane.read_with(cx, |pane, _| {
13333 assert_eq!(
13334 pane.items().count(),
13335 1,
13336 "Dirty items should not be automatically closed even when file is deleted"
13337 );
13338 });
13339
13340 // Verify the item is marked as deleted and still dirty
13341 item.read_with(cx, |item, _| {
13342 assert!(
13343 item.has_deleted_file,
13344 "Item should be marked as having deleted file"
13345 );
13346 assert!(item.is_dirty, "Item should still be dirty");
13347 });
13348 }
13349
13350 /// Tests that navigation history is cleaned up when files are auto-closed
13351 /// due to deletion from disk.
13352 #[gpui::test]
13353 async fn test_close_on_disk_deletion_cleans_navigation_history(cx: &mut TestAppContext) {
13354 init_test(cx);
13355
13356 // Enable the close_on_file_delete setting
13357 cx.update_global(|store: &mut SettingsStore, cx| {
13358 store.update_user_settings(cx, |settings| {
13359 settings.workspace.close_on_file_delete = Some(true);
13360 });
13361 });
13362
13363 let fs = FakeFs::new(cx.background_executor.clone());
13364 let project = Project::test(fs, [], cx).await;
13365 let (workspace, cx) =
13366 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13367 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13368
13369 // Create test items
13370 let item1 = cx.new(|cx| {
13371 TestItem::new(cx)
13372 .with_label("test1.txt")
13373 .with_project_items(&[TestProjectItem::new(1, "test1.txt", cx)])
13374 });
13375 let item1_id = item1.item_id();
13376
13377 let item2 = cx.new(|cx| {
13378 TestItem::new(cx)
13379 .with_label("test2.txt")
13380 .with_project_items(&[TestProjectItem::new(2, "test2.txt", cx)])
13381 });
13382
13383 // Add items to workspace
13384 workspace.update_in(cx, |workspace, window, cx| {
13385 workspace.add_item(
13386 pane.clone(),
13387 Box::new(item1.clone()),
13388 None,
13389 false,
13390 false,
13391 window,
13392 cx,
13393 );
13394 workspace.add_item(
13395 pane.clone(),
13396 Box::new(item2.clone()),
13397 None,
13398 false,
13399 false,
13400 window,
13401 cx,
13402 );
13403 });
13404
13405 // Activate item1 to ensure it gets navigation entries
13406 pane.update_in(cx, |pane, window, cx| {
13407 pane.activate_item(0, true, true, window, cx);
13408 });
13409
13410 // Switch to item2 and back to create navigation history
13411 pane.update_in(cx, |pane, window, cx| {
13412 pane.activate_item(1, true, true, window, cx);
13413 });
13414 cx.run_until_parked();
13415
13416 pane.update_in(cx, |pane, window, cx| {
13417 pane.activate_item(0, true, true, window, cx);
13418 });
13419 cx.run_until_parked();
13420
13421 // Simulate file deletion for item1
13422 item1.update(cx, |item, _| {
13423 item.set_has_deleted_file(true);
13424 });
13425
13426 // Emit UpdateTab event to trigger the close behavior
13427 item1.update(cx, |_, cx| {
13428 cx.emit(ItemEvent::UpdateTab);
13429 });
13430 cx.run_until_parked();
13431
13432 // Verify item1 was closed
13433 pane.read_with(cx, |pane, _| {
13434 assert_eq!(
13435 pane.items().count(),
13436 1,
13437 "Should have 1 item remaining after auto-close"
13438 );
13439 });
13440
13441 // Check navigation history after close
13442 let has_item = pane.read_with(cx, |pane, cx| {
13443 let mut has_item = false;
13444 pane.nav_history().for_each_entry(cx, &mut |entry, _| {
13445 if entry.item.id() == item1_id {
13446 has_item = true;
13447 }
13448 });
13449 has_item
13450 });
13451
13452 assert!(
13453 !has_item,
13454 "Navigation history should not contain closed item entries"
13455 );
13456 }
13457
13458 #[gpui::test]
13459 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
13460 cx: &mut TestAppContext,
13461 ) {
13462 init_test(cx);
13463
13464 let fs = FakeFs::new(cx.background_executor.clone());
13465 let project = Project::test(fs, [], cx).await;
13466 let (workspace, cx) =
13467 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13468 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13469
13470 let dirty_regular_buffer = cx.new(|cx| {
13471 TestItem::new(cx)
13472 .with_dirty(true)
13473 .with_label("1.txt")
13474 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
13475 });
13476 let dirty_regular_buffer_2 = cx.new(|cx| {
13477 TestItem::new(cx)
13478 .with_dirty(true)
13479 .with_label("2.txt")
13480 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
13481 });
13482 let clear_regular_buffer = cx.new(|cx| {
13483 TestItem::new(cx)
13484 .with_label("3.txt")
13485 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
13486 });
13487
13488 let dirty_multi_buffer = cx.new(|cx| {
13489 TestItem::new(cx)
13490 .with_dirty(true)
13491 .with_buffer_kind(ItemBufferKind::Multibuffer)
13492 .with_label("Fake Project Search")
13493 .with_project_items(&[
13494 dirty_regular_buffer.read(cx).project_items[0].clone(),
13495 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
13496 clear_regular_buffer.read(cx).project_items[0].clone(),
13497 ])
13498 });
13499 workspace.update_in(cx, |workspace, window, cx| {
13500 workspace.add_item(
13501 pane.clone(),
13502 Box::new(dirty_regular_buffer.clone()),
13503 None,
13504 false,
13505 false,
13506 window,
13507 cx,
13508 );
13509 workspace.add_item(
13510 pane.clone(),
13511 Box::new(dirty_regular_buffer_2.clone()),
13512 None,
13513 false,
13514 false,
13515 window,
13516 cx,
13517 );
13518 workspace.add_item(
13519 pane.clone(),
13520 Box::new(dirty_multi_buffer.clone()),
13521 None,
13522 false,
13523 false,
13524 window,
13525 cx,
13526 );
13527 });
13528
13529 pane.update_in(cx, |pane, window, cx| {
13530 pane.activate_item(2, true, true, window, cx);
13531 assert_eq!(
13532 pane.active_item().unwrap().item_id(),
13533 dirty_multi_buffer.item_id(),
13534 "Should select the multi buffer in the pane"
13535 );
13536 });
13537 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13538 pane.close_active_item(
13539 &CloseActiveItem {
13540 save_intent: None,
13541 close_pinned: false,
13542 },
13543 window,
13544 cx,
13545 )
13546 });
13547 cx.background_executor.run_until_parked();
13548 assert!(
13549 !cx.has_pending_prompt(),
13550 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
13551 );
13552 close_multi_buffer_task
13553 .await
13554 .expect("Closing multi buffer failed");
13555 pane.update(cx, |pane, cx| {
13556 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
13557 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
13558 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
13559 assert_eq!(
13560 pane.items()
13561 .map(|item| item.item_id())
13562 .sorted()
13563 .collect::<Vec<_>>(),
13564 vec![
13565 dirty_regular_buffer.item_id(),
13566 dirty_regular_buffer_2.item_id(),
13567 ],
13568 "Should have no multi buffer left in the pane"
13569 );
13570 assert!(dirty_regular_buffer.read(cx).is_dirty);
13571 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
13572 });
13573 }
13574
13575 #[gpui::test]
13576 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
13577 init_test(cx);
13578 let fs = FakeFs::new(cx.executor());
13579 let project = Project::test(fs, [], cx).await;
13580 let (multi_workspace, cx) =
13581 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
13582 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
13583
13584 // Add a new panel to the right dock, opening the dock and setting the
13585 // focus to the new panel.
13586 let panel = workspace.update_in(cx, |workspace, window, cx| {
13587 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
13588 workspace.add_panel(panel.clone(), window, cx);
13589
13590 workspace
13591 .right_dock()
13592 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
13593
13594 workspace.toggle_panel_focus::<TestPanel>(window, cx);
13595
13596 panel
13597 });
13598
13599 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
13600 // panel to the next valid position which, in this case, is the left
13601 // dock.
13602 cx.dispatch_action(MoveFocusedPanelToNextPosition);
13603 workspace.update(cx, |workspace, cx| {
13604 assert!(workspace.left_dock().read(cx).is_open());
13605 assert_eq!(panel.read(cx).position, DockPosition::Left);
13606 });
13607
13608 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
13609 // panel to the next valid position which, in this case, is the bottom
13610 // dock.
13611 cx.dispatch_action(MoveFocusedPanelToNextPosition);
13612 workspace.update(cx, |workspace, cx| {
13613 assert!(workspace.bottom_dock().read(cx).is_open());
13614 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
13615 });
13616
13617 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
13618 // around moving the panel to its initial position, the right dock.
13619 cx.dispatch_action(MoveFocusedPanelToNextPosition);
13620 workspace.update(cx, |workspace, cx| {
13621 assert!(workspace.right_dock().read(cx).is_open());
13622 assert_eq!(panel.read(cx).position, DockPosition::Right);
13623 });
13624
13625 // Remove focus from the panel, ensuring that, if the panel is not
13626 // focused, the `MoveFocusedPanelToNextPosition` action does not update
13627 // the panel's position, so the panel is still in the right dock.
13628 workspace.update_in(cx, |workspace, window, cx| {
13629 workspace.toggle_panel_focus::<TestPanel>(window, cx);
13630 });
13631
13632 cx.dispatch_action(MoveFocusedPanelToNextPosition);
13633 workspace.update(cx, |workspace, cx| {
13634 assert!(workspace.right_dock().read(cx).is_open());
13635 assert_eq!(panel.read(cx).position, DockPosition::Right);
13636 });
13637 }
13638
13639 #[gpui::test]
13640 async fn test_moving_items_create_panes(cx: &mut TestAppContext) {
13641 init_test(cx);
13642
13643 let fs = FakeFs::new(cx.executor());
13644 let project = Project::test(fs, [], cx).await;
13645 let (workspace, cx) =
13646 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
13647
13648 let item_1 = cx.new(|cx| {
13649 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
13650 });
13651 workspace.update_in(cx, |workspace, window, cx| {
13652 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
13653 workspace.move_item_to_pane_in_direction(
13654 &MoveItemToPaneInDirection {
13655 direction: SplitDirection::Right,
13656 focus: true,
13657 clone: false,
13658 },
13659 window,
13660 cx,
13661 );
13662 workspace.move_item_to_pane_at_index(
13663 &MoveItemToPane {
13664 destination: 3,
13665 focus: true,
13666 clone: false,
13667 },
13668 window,
13669 cx,
13670 );
13671
13672 assert_eq!(workspace.panes.len(), 1, "No new panes were created");
13673 assert_eq!(
13674 pane_items_paths(&workspace.active_pane, cx),
13675 vec!["first.txt".to_string()],
13676 "Single item was not moved anywhere"
13677 );
13678 });
13679
13680 let item_2 = cx.new(|cx| {
13681 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "second.txt", cx)])
13682 });
13683 workspace.update_in(cx, |workspace, window, cx| {
13684 workspace.add_item_to_active_pane(Box::new(item_2), None, true, window, cx);
13685 assert_eq!(
13686 pane_items_paths(&workspace.panes[0], cx),
13687 vec!["first.txt".to_string(), "second.txt".to_string()],
13688 );
13689 workspace.move_item_to_pane_in_direction(
13690 &MoveItemToPaneInDirection {
13691 direction: SplitDirection::Right,
13692 focus: true,
13693 clone: false,
13694 },
13695 window,
13696 cx,
13697 );
13698
13699 assert_eq!(workspace.panes.len(), 2, "A new pane should be created");
13700 assert_eq!(
13701 pane_items_paths(&workspace.panes[0], cx),
13702 vec!["first.txt".to_string()],
13703 "After moving, one item should be left in the original pane"
13704 );
13705 assert_eq!(
13706 pane_items_paths(&workspace.panes[1], cx),
13707 vec!["second.txt".to_string()],
13708 "New item should have been moved to the new pane"
13709 );
13710 });
13711
13712 let item_3 = cx.new(|cx| {
13713 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "third.txt", cx)])
13714 });
13715 workspace.update_in(cx, |workspace, window, cx| {
13716 let original_pane = workspace.panes[0].clone();
13717 workspace.set_active_pane(&original_pane, window, cx);
13718 workspace.add_item_to_active_pane(Box::new(item_3), None, true, window, cx);
13719 assert_eq!(workspace.panes.len(), 2, "No new panes were created");
13720 assert_eq!(
13721 pane_items_paths(&workspace.active_pane, cx),
13722 vec!["first.txt".to_string(), "third.txt".to_string()],
13723 "New pane should be ready to move one item out"
13724 );
13725
13726 workspace.move_item_to_pane_at_index(
13727 &MoveItemToPane {
13728 destination: 3,
13729 focus: true,
13730 clone: false,
13731 },
13732 window,
13733 cx,
13734 );
13735 assert_eq!(workspace.panes.len(), 3, "A new pane should be created");
13736 assert_eq!(
13737 pane_items_paths(&workspace.active_pane, cx),
13738 vec!["first.txt".to_string()],
13739 "After moving, one item should be left in the original pane"
13740 );
13741 assert_eq!(
13742 pane_items_paths(&workspace.panes[1], cx),
13743 vec!["second.txt".to_string()],
13744 "Previously created pane should be unchanged"
13745 );
13746 assert_eq!(
13747 pane_items_paths(&workspace.panes[2], cx),
13748 vec!["third.txt".to_string()],
13749 "New item should have been moved to the new pane"
13750 );
13751 });
13752 }
13753
13754 #[gpui::test]
13755 async fn test_moving_items_can_clone_panes(cx: &mut TestAppContext) {
13756 init_test(cx);
13757
13758 let fs = FakeFs::new(cx.executor());
13759 let project = Project::test(fs, [], cx).await;
13760 let (workspace, cx) =
13761 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
13762
13763 let item_1 = cx.new(|cx| {
13764 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
13765 });
13766 workspace.update_in(cx, |workspace, window, cx| {
13767 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
13768 workspace.move_item_to_pane_in_direction(
13769 &MoveItemToPaneInDirection {
13770 direction: SplitDirection::Right,
13771 focus: true,
13772 clone: true,
13773 },
13774 window,
13775 cx,
13776 );
13777 });
13778 cx.run_until_parked();
13779 workspace.update_in(cx, |workspace, window, cx| {
13780 workspace.move_item_to_pane_at_index(
13781 &MoveItemToPane {
13782 destination: 3,
13783 focus: true,
13784 clone: true,
13785 },
13786 window,
13787 cx,
13788 );
13789 });
13790 cx.run_until_parked();
13791
13792 workspace.update(cx, |workspace, cx| {
13793 assert_eq!(workspace.panes.len(), 3, "Two new panes were created");
13794 for pane in workspace.panes() {
13795 assert_eq!(
13796 pane_items_paths(pane, cx),
13797 vec!["first.txt".to_string()],
13798 "Single item exists in all panes"
13799 );
13800 }
13801 });
13802
13803 // verify that the active pane has been updated after waiting for the
13804 // pane focus event to fire and resolve
13805 workspace.read_with(cx, |workspace, _app| {
13806 assert_eq!(
13807 workspace.active_pane(),
13808 &workspace.panes[2],
13809 "The third pane should be the active one: {:?}",
13810 workspace.panes
13811 );
13812 })
13813 }
13814
13815 #[gpui::test]
13816 async fn test_close_item_in_all_panes(cx: &mut TestAppContext) {
13817 init_test(cx);
13818
13819 let fs = FakeFs::new(cx.executor());
13820 fs.insert_tree("/root", json!({ "test.txt": "" })).await;
13821
13822 let project = Project::test(fs, ["root".as_ref()], cx).await;
13823 let (workspace, cx) =
13824 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
13825
13826 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13827 // Add item to pane A with project path
13828 let item_a = cx.new(|cx| {
13829 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13830 });
13831 workspace.update_in(cx, |workspace, window, cx| {
13832 workspace.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
13833 });
13834
13835 // Split to create pane B
13836 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
13837 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
13838 });
13839
13840 // Add item with SAME project path to pane B, and pin it
13841 let item_b = cx.new(|cx| {
13842 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13843 });
13844 pane_b.update_in(cx, |pane, window, cx| {
13845 pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
13846 pane.set_pinned_count(1);
13847 });
13848
13849 assert_eq!(pane_a.read_with(cx, |pane, _| pane.items_len()), 1);
13850 assert_eq!(pane_b.read_with(cx, |pane, _| pane.items_len()), 1);
13851
13852 // close_pinned: false should only close the unpinned copy
13853 workspace.update_in(cx, |workspace, window, cx| {
13854 workspace.close_item_in_all_panes(
13855 &CloseItemInAllPanes {
13856 save_intent: Some(SaveIntent::Close),
13857 close_pinned: false,
13858 },
13859 window,
13860 cx,
13861 )
13862 });
13863 cx.executor().run_until_parked();
13864
13865 let item_count_a = pane_a.read_with(cx, |pane, _| pane.items_len());
13866 let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
13867 assert_eq!(item_count_a, 0, "Unpinned item in pane A should be closed");
13868 assert_eq!(item_count_b, 1, "Pinned item in pane B should remain");
13869
13870 // Split again, seeing as closing the previous item also closed its
13871 // pane, so only pane remains, which does not allow us to properly test
13872 // that both items close when `close_pinned: true`.
13873 let pane_c = workspace.update_in(cx, |workspace, window, cx| {
13874 workspace.split_pane(pane_b.clone(), SplitDirection::Right, window, cx)
13875 });
13876
13877 // Add an item with the same project path to pane C so that
13878 // close_item_in_all_panes can determine what to close across all panes
13879 // (it reads the active item from the active pane, and split_pane
13880 // creates an empty pane).
13881 let item_c = cx.new(|cx| {
13882 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13883 });
13884 pane_c.update_in(cx, |pane, window, cx| {
13885 pane.add_item(Box::new(item_c.clone()), true, true, None, window, cx);
13886 });
13887
13888 // close_pinned: true should close the pinned copy too
13889 workspace.update_in(cx, |workspace, window, cx| {
13890 let panes_count = workspace.panes().len();
13891 assert_eq!(panes_count, 2, "Workspace should have two panes (B and C)");
13892
13893 workspace.close_item_in_all_panes(
13894 &CloseItemInAllPanes {
13895 save_intent: Some(SaveIntent::Close),
13896 close_pinned: true,
13897 },
13898 window,
13899 cx,
13900 )
13901 });
13902 cx.executor().run_until_parked();
13903
13904 let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
13905 let item_count_c = pane_c.read_with(cx, |pane, _| pane.items_len());
13906 assert_eq!(item_count_b, 0, "Pinned item in pane B should be closed");
13907 assert_eq!(item_count_c, 0, "Unpinned item in pane C should be closed");
13908 }
13909
13910 mod register_project_item_tests {
13911
13912 use super::*;
13913
13914 // View
13915 struct TestPngItemView {
13916 focus_handle: FocusHandle,
13917 }
13918 // Model
13919 struct TestPngItem {}
13920
13921 impl project::ProjectItem for TestPngItem {
13922 fn try_open(
13923 _project: &Entity<Project>,
13924 path: &ProjectPath,
13925 cx: &mut App,
13926 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
13927 if path.path.extension().unwrap() == "png" {
13928 Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestPngItem {}))))
13929 } else {
13930 None
13931 }
13932 }
13933
13934 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
13935 None
13936 }
13937
13938 fn project_path(&self, _: &App) -> Option<ProjectPath> {
13939 None
13940 }
13941
13942 fn is_dirty(&self) -> bool {
13943 false
13944 }
13945 }
13946
13947 impl Item for TestPngItemView {
13948 type Event = ();
13949 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
13950 "".into()
13951 }
13952 }
13953 impl EventEmitter<()> for TestPngItemView {}
13954 impl Focusable for TestPngItemView {
13955 fn focus_handle(&self, _cx: &App) -> FocusHandle {
13956 self.focus_handle.clone()
13957 }
13958 }
13959
13960 impl Render for TestPngItemView {
13961 fn render(
13962 &mut self,
13963 _window: &mut Window,
13964 _cx: &mut Context<Self>,
13965 ) -> impl IntoElement {
13966 Empty
13967 }
13968 }
13969
13970 impl ProjectItem for TestPngItemView {
13971 type Item = TestPngItem;
13972
13973 fn for_project_item(
13974 _project: Entity<Project>,
13975 _pane: Option<&Pane>,
13976 _item: Entity<Self::Item>,
13977 _: &mut Window,
13978 cx: &mut Context<Self>,
13979 ) -> Self
13980 where
13981 Self: Sized,
13982 {
13983 Self {
13984 focus_handle: cx.focus_handle(),
13985 }
13986 }
13987 }
13988
13989 // View
13990 struct TestIpynbItemView {
13991 focus_handle: FocusHandle,
13992 }
13993 // Model
13994 struct TestIpynbItem {}
13995
13996 impl project::ProjectItem for TestIpynbItem {
13997 fn try_open(
13998 _project: &Entity<Project>,
13999 path: &ProjectPath,
14000 cx: &mut App,
14001 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
14002 if path.path.extension().unwrap() == "ipynb" {
14003 Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestIpynbItem {}))))
14004 } else {
14005 None
14006 }
14007 }
14008
14009 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
14010 None
14011 }
14012
14013 fn project_path(&self, _: &App) -> Option<ProjectPath> {
14014 None
14015 }
14016
14017 fn is_dirty(&self) -> bool {
14018 false
14019 }
14020 }
14021
14022 impl Item for TestIpynbItemView {
14023 type Event = ();
14024 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14025 "".into()
14026 }
14027 }
14028 impl EventEmitter<()> for TestIpynbItemView {}
14029 impl Focusable for TestIpynbItemView {
14030 fn focus_handle(&self, _cx: &App) -> FocusHandle {
14031 self.focus_handle.clone()
14032 }
14033 }
14034
14035 impl Render for TestIpynbItemView {
14036 fn render(
14037 &mut self,
14038 _window: &mut Window,
14039 _cx: &mut Context<Self>,
14040 ) -> impl IntoElement {
14041 Empty
14042 }
14043 }
14044
14045 impl ProjectItem for TestIpynbItemView {
14046 type Item = TestIpynbItem;
14047
14048 fn for_project_item(
14049 _project: Entity<Project>,
14050 _pane: Option<&Pane>,
14051 _item: Entity<Self::Item>,
14052 _: &mut Window,
14053 cx: &mut Context<Self>,
14054 ) -> Self
14055 where
14056 Self: Sized,
14057 {
14058 Self {
14059 focus_handle: cx.focus_handle(),
14060 }
14061 }
14062 }
14063
14064 struct TestAlternatePngItemView {
14065 focus_handle: FocusHandle,
14066 }
14067
14068 impl Item for TestAlternatePngItemView {
14069 type Event = ();
14070 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14071 "".into()
14072 }
14073 }
14074
14075 impl EventEmitter<()> for TestAlternatePngItemView {}
14076 impl Focusable for TestAlternatePngItemView {
14077 fn focus_handle(&self, _cx: &App) -> FocusHandle {
14078 self.focus_handle.clone()
14079 }
14080 }
14081
14082 impl Render for TestAlternatePngItemView {
14083 fn render(
14084 &mut self,
14085 _window: &mut Window,
14086 _cx: &mut Context<Self>,
14087 ) -> impl IntoElement {
14088 Empty
14089 }
14090 }
14091
14092 impl ProjectItem for TestAlternatePngItemView {
14093 type Item = TestPngItem;
14094
14095 fn for_project_item(
14096 _project: Entity<Project>,
14097 _pane: Option<&Pane>,
14098 _item: Entity<Self::Item>,
14099 _: &mut Window,
14100 cx: &mut Context<Self>,
14101 ) -> Self
14102 where
14103 Self: Sized,
14104 {
14105 Self {
14106 focus_handle: cx.focus_handle(),
14107 }
14108 }
14109 }
14110
14111 #[gpui::test]
14112 async fn test_register_project_item(cx: &mut TestAppContext) {
14113 init_test(cx);
14114
14115 cx.update(|cx| {
14116 register_project_item::<TestPngItemView>(cx);
14117 register_project_item::<TestIpynbItemView>(cx);
14118 });
14119
14120 let fs = FakeFs::new(cx.executor());
14121 fs.insert_tree(
14122 "/root1",
14123 json!({
14124 "one.png": "BINARYDATAHERE",
14125 "two.ipynb": "{ totally a notebook }",
14126 "three.txt": "editing text, sure why not?"
14127 }),
14128 )
14129 .await;
14130
14131 let project = Project::test(fs, ["root1".as_ref()], cx).await;
14132 let (workspace, cx) =
14133 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14134
14135 let worktree_id = project.update(cx, |project, cx| {
14136 project.worktrees(cx).next().unwrap().read(cx).id()
14137 });
14138
14139 let handle = workspace
14140 .update_in(cx, |workspace, window, cx| {
14141 let project_path = (worktree_id, rel_path("one.png"));
14142 workspace.open_path(project_path, None, true, window, cx)
14143 })
14144 .await
14145 .unwrap();
14146
14147 // Now we can check if the handle we got back errored or not
14148 assert_eq!(
14149 handle.to_any_view().entity_type(),
14150 TypeId::of::<TestPngItemView>()
14151 );
14152
14153 let handle = workspace
14154 .update_in(cx, |workspace, window, cx| {
14155 let project_path = (worktree_id, rel_path("two.ipynb"));
14156 workspace.open_path(project_path, None, true, window, cx)
14157 })
14158 .await
14159 .unwrap();
14160
14161 assert_eq!(
14162 handle.to_any_view().entity_type(),
14163 TypeId::of::<TestIpynbItemView>()
14164 );
14165
14166 let handle = workspace
14167 .update_in(cx, |workspace, window, cx| {
14168 let project_path = (worktree_id, rel_path("three.txt"));
14169 workspace.open_path(project_path, None, true, window, cx)
14170 })
14171 .await;
14172 assert!(handle.is_err());
14173 }
14174
14175 #[gpui::test]
14176 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
14177 init_test(cx);
14178
14179 cx.update(|cx| {
14180 register_project_item::<TestPngItemView>(cx);
14181 register_project_item::<TestAlternatePngItemView>(cx);
14182 });
14183
14184 let fs = FakeFs::new(cx.executor());
14185 fs.insert_tree(
14186 "/root1",
14187 json!({
14188 "one.png": "BINARYDATAHERE",
14189 "two.ipynb": "{ totally a notebook }",
14190 "three.txt": "editing text, sure why not?"
14191 }),
14192 )
14193 .await;
14194 let project = Project::test(fs, ["root1".as_ref()], cx).await;
14195 let (workspace, cx) =
14196 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14197 let worktree_id = project.update(cx, |project, cx| {
14198 project.worktrees(cx).next().unwrap().read(cx).id()
14199 });
14200
14201 let handle = workspace
14202 .update_in(cx, |workspace, window, cx| {
14203 let project_path = (worktree_id, rel_path("one.png"));
14204 workspace.open_path(project_path, None, true, window, cx)
14205 })
14206 .await
14207 .unwrap();
14208
14209 // This _must_ be the second item registered
14210 assert_eq!(
14211 handle.to_any_view().entity_type(),
14212 TypeId::of::<TestAlternatePngItemView>()
14213 );
14214
14215 let handle = workspace
14216 .update_in(cx, |workspace, window, cx| {
14217 let project_path = (worktree_id, rel_path("three.txt"));
14218 workspace.open_path(project_path, None, true, window, cx)
14219 })
14220 .await;
14221 assert!(handle.is_err());
14222 }
14223 }
14224
14225 #[gpui::test]
14226 async fn test_status_bar_visibility(cx: &mut TestAppContext) {
14227 init_test(cx);
14228
14229 let fs = FakeFs::new(cx.executor());
14230 let project = Project::test(fs, [], cx).await;
14231 let (workspace, _cx) =
14232 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14233
14234 // Test with status bar shown (default)
14235 workspace.read_with(cx, |workspace, cx| {
14236 let visible = workspace.status_bar_visible(cx);
14237 assert!(visible, "Status bar should be visible by default");
14238 });
14239
14240 // Test with status bar hidden
14241 cx.update_global(|store: &mut SettingsStore, cx| {
14242 store.update_user_settings(cx, |settings| {
14243 settings.status_bar.get_or_insert_default().show = Some(false);
14244 });
14245 });
14246
14247 workspace.read_with(cx, |workspace, cx| {
14248 let visible = workspace.status_bar_visible(cx);
14249 assert!(!visible, "Status bar should be hidden when show is false");
14250 });
14251
14252 // Test with status bar shown explicitly
14253 cx.update_global(|store: &mut SettingsStore, cx| {
14254 store.update_user_settings(cx, |settings| {
14255 settings.status_bar.get_or_insert_default().show = Some(true);
14256 });
14257 });
14258
14259 workspace.read_with(cx, |workspace, cx| {
14260 let visible = workspace.status_bar_visible(cx);
14261 assert!(visible, "Status bar should be visible when show is true");
14262 });
14263 }
14264
14265 #[gpui::test]
14266 async fn test_pane_close_active_item(cx: &mut TestAppContext) {
14267 init_test(cx);
14268
14269 let fs = FakeFs::new(cx.executor());
14270 let project = Project::test(fs, [], cx).await;
14271 let (multi_workspace, cx) =
14272 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
14273 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
14274 let panel = workspace.update_in(cx, |workspace, window, cx| {
14275 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14276 workspace.add_panel(panel.clone(), window, cx);
14277
14278 workspace
14279 .right_dock()
14280 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
14281
14282 panel
14283 });
14284
14285 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14286 let item_a = cx.new(TestItem::new);
14287 let item_b = cx.new(TestItem::new);
14288 let item_a_id = item_a.entity_id();
14289 let item_b_id = item_b.entity_id();
14290
14291 pane.update_in(cx, |pane, window, cx| {
14292 pane.add_item(Box::new(item_a.clone()), true, true, None, window, cx);
14293 pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
14294 });
14295
14296 pane.read_with(cx, |pane, _| {
14297 assert_eq!(pane.items_len(), 2);
14298 assert_eq!(pane.active_item().unwrap().item_id(), item_b_id);
14299 });
14300
14301 workspace.update_in(cx, |workspace, window, cx| {
14302 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14303 });
14304
14305 workspace.update_in(cx, |_, window, cx| {
14306 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
14307 });
14308
14309 // Assert that the `pane::CloseActiveItem` action is handled at the
14310 // workspace level when one of the dock panels is focused and, in that
14311 // case, the center pane's active item is closed but the focus is not
14312 // moved.
14313 cx.dispatch_action(pane::CloseActiveItem::default());
14314 cx.run_until_parked();
14315
14316 pane.read_with(cx, |pane, _| {
14317 assert_eq!(pane.items_len(), 1);
14318 assert_eq!(pane.active_item().unwrap().item_id(), item_a_id);
14319 });
14320
14321 workspace.update_in(cx, |workspace, window, cx| {
14322 assert!(workspace.right_dock().read(cx).is_open());
14323 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
14324 });
14325 }
14326
14327 #[gpui::test]
14328 async fn test_panel_zoom_preserved_across_workspace_switch(cx: &mut TestAppContext) {
14329 init_test(cx);
14330 let fs = FakeFs::new(cx.executor());
14331
14332 let project_a = Project::test(fs.clone(), [], cx).await;
14333 let project_b = Project::test(fs, [], cx).await;
14334
14335 let multi_workspace_handle =
14336 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
14337 cx.run_until_parked();
14338
14339 let workspace_a = multi_workspace_handle
14340 .read_with(cx, |mw, _| mw.workspace().clone())
14341 .unwrap();
14342
14343 let _workspace_b = multi_workspace_handle
14344 .update(cx, |mw, window, cx| {
14345 mw.test_add_workspace(project_b, window, cx)
14346 })
14347 .unwrap();
14348
14349 // Switch to workspace A
14350 multi_workspace_handle
14351 .update(cx, |mw, window, cx| {
14352 mw.activate_index(0, window, cx);
14353 })
14354 .unwrap();
14355
14356 let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
14357
14358 // Add a panel to workspace A's right dock and open the dock
14359 let panel = workspace_a.update_in(cx, |workspace, window, cx| {
14360 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14361 workspace.add_panel(panel.clone(), window, cx);
14362 workspace
14363 .right_dock()
14364 .update(cx, |dock, cx| dock.set_open(true, window, cx));
14365 panel
14366 });
14367
14368 // Focus the panel through the workspace (matching existing test pattern)
14369 workspace_a.update_in(cx, |workspace, window, cx| {
14370 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14371 });
14372
14373 // Zoom the panel
14374 panel.update_in(cx, |panel, window, cx| {
14375 panel.set_zoomed(true, window, cx);
14376 });
14377
14378 // Verify the panel is zoomed and the dock is open
14379 workspace_a.update_in(cx, |workspace, window, cx| {
14380 assert!(
14381 workspace.right_dock().read(cx).is_open(),
14382 "dock should be open before switch"
14383 );
14384 assert!(
14385 panel.is_zoomed(window, cx),
14386 "panel should be zoomed before switch"
14387 );
14388 assert!(
14389 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
14390 "panel should be focused before switch"
14391 );
14392 });
14393
14394 // Switch to workspace B
14395 multi_workspace_handle
14396 .update(cx, |mw, window, cx| {
14397 mw.activate_index(1, window, cx);
14398 })
14399 .unwrap();
14400 cx.run_until_parked();
14401
14402 // Switch back to workspace A
14403 multi_workspace_handle
14404 .update(cx, |mw, window, cx| {
14405 mw.activate_index(0, window, cx);
14406 })
14407 .unwrap();
14408 cx.run_until_parked();
14409
14410 // Verify the panel is still zoomed and the dock is still open
14411 workspace_a.update_in(cx, |workspace, window, cx| {
14412 assert!(
14413 workspace.right_dock().read(cx).is_open(),
14414 "dock should still be open after switching back"
14415 );
14416 assert!(
14417 panel.is_zoomed(window, cx),
14418 "panel should still be zoomed after switching back"
14419 );
14420 });
14421 }
14422
14423 fn pane_items_paths(pane: &Entity<Pane>, cx: &App) -> Vec<String> {
14424 pane.read(cx)
14425 .items()
14426 .flat_map(|item| {
14427 item.project_paths(cx)
14428 .into_iter()
14429 .map(|path| path.path.display(PathStyle::local()).into_owned())
14430 })
14431 .collect()
14432 }
14433
14434 pub fn init_test(cx: &mut TestAppContext) {
14435 cx.update(|cx| {
14436 let settings_store = SettingsStore::test(cx);
14437 cx.set_global(settings_store);
14438 cx.set_global(db::AppDatabase::test_new());
14439 theme_settings::init(theme::LoadThemes::JustBase, cx);
14440 });
14441 }
14442
14443 #[gpui::test]
14444 async fn test_toggle_theme_mode_persists_and_updates_active_theme(cx: &mut TestAppContext) {
14445 use settings::{ThemeName, ThemeSelection};
14446 use theme::SystemAppearance;
14447 use zed_actions::theme::ToggleMode;
14448
14449 init_test(cx);
14450
14451 let fs = FakeFs::new(cx.executor());
14452 let settings_fs: Arc<dyn fs::Fs> = fs.clone();
14453
14454 fs.insert_tree(path!("/root"), json!({ "file.rs": "fn main() {}\n" }))
14455 .await;
14456
14457 // Build a test project and workspace view so the test can invoke
14458 // the workspace action handler the same way the UI would.
14459 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
14460 let (workspace, cx) =
14461 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14462
14463 // Seed the settings file with a plain static light theme so the
14464 // first toggle always starts from a known persisted state.
14465 workspace.update_in(cx, |_workspace, _window, cx| {
14466 *SystemAppearance::global_mut(cx) = SystemAppearance(theme::Appearance::Light);
14467 settings::update_settings_file(settings_fs.clone(), cx, |settings, _cx| {
14468 settings.theme.theme = Some(ThemeSelection::Static(ThemeName("One Light".into())));
14469 });
14470 });
14471 cx.executor().advance_clock(Duration::from_millis(200));
14472 cx.run_until_parked();
14473
14474 // Confirm the initial persisted settings contain the static theme
14475 // we just wrote before any toggling happens.
14476 let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
14477 assert!(settings_text.contains(r#""theme": "One Light""#));
14478
14479 // Toggle once. This should migrate the persisted theme settings
14480 // into light/dark slots and enable system mode.
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 // 1. Static -> Dynamic
14488 // this assertion checks theme changed from static to dynamic.
14489 let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
14490 let parsed: serde_json::Value = settings::parse_json_with_comments(&settings_text).unwrap();
14491 assert_eq!(
14492 parsed["theme"],
14493 serde_json::json!({
14494 "mode": "system",
14495 "light": "One Light",
14496 "dark": "One Dark"
14497 })
14498 );
14499
14500 // 2. Toggle again, suppose it will change the mode to light
14501 workspace.update_in(cx, |workspace, window, cx| {
14502 workspace.toggle_theme_mode(&ToggleMode, window, cx);
14503 });
14504 cx.executor().advance_clock(Duration::from_millis(200));
14505 cx.run_until_parked();
14506
14507 let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
14508 assert!(settings_text.contains(r#""mode": "light""#));
14509 }
14510
14511 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
14512 let item = TestProjectItem::new(id, path, cx);
14513 item.update(cx, |item, _| {
14514 item.is_dirty = true;
14515 });
14516 item
14517 }
14518
14519 #[gpui::test]
14520 async fn test_zoomed_panel_without_pane_preserved_on_center_focus(
14521 cx: &mut gpui::TestAppContext,
14522 ) {
14523 init_test(cx);
14524 let fs = FakeFs::new(cx.executor());
14525
14526 let project = Project::test(fs, [], cx).await;
14527 let (workspace, cx) =
14528 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
14529
14530 let panel = workspace.update_in(cx, |workspace, window, cx| {
14531 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14532 workspace.add_panel(panel.clone(), window, cx);
14533 workspace
14534 .right_dock()
14535 .update(cx, |dock, cx| dock.set_open(true, window, cx));
14536 panel
14537 });
14538
14539 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14540 pane.update_in(cx, |pane, window, cx| {
14541 let item = cx.new(TestItem::new);
14542 pane.add_item(Box::new(item), true, true, None, window, cx);
14543 });
14544
14545 // Transfer focus to the panel, then zoom it. Using toggle_panel_focus
14546 // mirrors the real-world flow and avoids side effects from directly
14547 // focusing the panel while the center pane is active.
14548 workspace.update_in(cx, |workspace, window, cx| {
14549 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14550 });
14551
14552 panel.update_in(cx, |panel, window, cx| {
14553 panel.set_zoomed(true, window, cx);
14554 });
14555
14556 workspace.update_in(cx, |workspace, window, cx| {
14557 assert!(workspace.right_dock().read(cx).is_open());
14558 assert!(panel.is_zoomed(window, cx));
14559 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
14560 });
14561
14562 // Simulate a spurious pane::Event::Focus on the center pane while the
14563 // panel still has focus. This mirrors what happens during macOS window
14564 // activation: the center pane fires a focus event even though actual
14565 // focus remains on the dock panel.
14566 pane.update_in(cx, |_, _, cx| {
14567 cx.emit(pane::Event::Focus);
14568 });
14569
14570 // The dock must remain open because the panel had focus at the time the
14571 // event was processed. Before the fix, dock_to_preserve was None for
14572 // panels that don't implement pane(), causing the dock to close.
14573 workspace.update_in(cx, |workspace, window, cx| {
14574 assert!(
14575 workspace.right_dock().read(cx).is_open(),
14576 "Dock should stay open when its zoomed panel (without pane()) still has focus"
14577 );
14578 assert!(panel.is_zoomed(window, cx));
14579 });
14580 }
14581
14582 #[gpui::test]
14583 async fn test_panels_stay_open_after_position_change_and_settings_update(
14584 cx: &mut gpui::TestAppContext,
14585 ) {
14586 init_test(cx);
14587 let fs = FakeFs::new(cx.executor());
14588 let project = Project::test(fs, [], cx).await;
14589 let (workspace, cx) =
14590 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
14591
14592 // Add two panels to the left dock and open it.
14593 let (panel_a, panel_b) = workspace.update_in(cx, |workspace, window, cx| {
14594 let panel_a = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
14595 let panel_b = cx.new(|cx| TestPanel::new(DockPosition::Left, 101, cx));
14596 workspace.add_panel(panel_a.clone(), window, cx);
14597 workspace.add_panel(panel_b.clone(), window, cx);
14598 workspace.left_dock().update(cx, |dock, cx| {
14599 dock.set_open(true, window, cx);
14600 dock.activate_panel(0, window, cx);
14601 });
14602 (panel_a, panel_b)
14603 });
14604
14605 workspace.update_in(cx, |workspace, _, cx| {
14606 assert!(workspace.left_dock().read(cx).is_open());
14607 });
14608
14609 // Simulate a feature flag changing default dock positions: both panels
14610 // move from Left to Right.
14611 workspace.update_in(cx, |_workspace, _window, cx| {
14612 panel_a.update(cx, |p, _cx| p.position = DockPosition::Right);
14613 panel_b.update(cx, |p, _cx| p.position = DockPosition::Right);
14614 cx.update_global::<SettingsStore, _>(|_, _| {});
14615 });
14616
14617 // Both panels should now be in the right dock.
14618 workspace.update_in(cx, |workspace, _, cx| {
14619 let right_dock = workspace.right_dock().read(cx);
14620 assert_eq!(right_dock.panels_len(), 2);
14621 });
14622
14623 // Open the right dock and activate panel_b (simulating the user
14624 // opening the panel after it moved).
14625 workspace.update_in(cx, |workspace, window, cx| {
14626 workspace.right_dock().update(cx, |dock, cx| {
14627 dock.set_open(true, window, cx);
14628 dock.activate_panel(1, window, cx);
14629 });
14630 });
14631
14632 // Now trigger another SettingsStore change
14633 workspace.update_in(cx, |_workspace, _window, cx| {
14634 cx.update_global::<SettingsStore, _>(|_, _| {});
14635 });
14636
14637 workspace.update_in(cx, |workspace, _, cx| {
14638 assert!(
14639 workspace.right_dock().read(cx).is_open(),
14640 "Right dock should still be open after a settings change"
14641 );
14642 assert_eq!(
14643 workspace.right_dock().read(cx).panels_len(),
14644 2,
14645 "Both panels should still be in the right dock"
14646 );
14647 });
14648 }
14649}