1pub mod active_file_name;
2pub mod dock;
3pub mod history_manager;
4pub mod invalid_item_view;
5pub mod item;
6mod modal_layer;
7mod multi_workspace;
8#[cfg(test)]
9mod multi_workspace_tests;
10pub mod notifications;
11pub mod pane;
12pub mod pane_group;
13pub mod path_list {
14 pub use util::path_list::{PathList, SerializedPathList};
15}
16mod persistence;
17pub mod searchable;
18mod security_modal;
19pub mod shared_screen;
20use db::smol::future::yield_now;
21pub use shared_screen::SharedScreen;
22pub mod focus_follows_mouse;
23mod status_bar;
24pub mod tasks;
25mod theme_preview;
26mod toast_layer;
27mod toolbar;
28pub mod welcome;
29mod workspace_settings;
30
31pub use crate::notifications::NotificationFrame;
32pub use dock::Panel;
33pub use multi_workspace::{
34 CloseWorkspaceSidebar, DraggedSidebar, FocusWorkspaceSidebar, MoveProjectToNewWindow,
35 MultiWorkspace, MultiWorkspaceEvent, NewThread, NextProject, NextThread, PreviousProject,
36 PreviousThread, ShowFewerThreads, ShowMoreThreads, Sidebar, SidebarEvent, SidebarHandle,
37 SidebarRenderState, SidebarSide, ToggleWorkspaceSidebar, sidebar_side_context_menu,
38};
39pub use path_list::{PathList, SerializedPathList};
40pub use toast_layer::{ToastAction, ToastLayer, ToastView};
41
42use anyhow::{Context as _, Result, anyhow};
43use client::{
44 ChannelId, Client, ErrorExt, ParticipantIndex, Status, TypedEnvelope, User, UserStore,
45 proto::{self, ErrorCode, PanelId, PeerId},
46};
47use collections::{HashMap, HashSet, hash_map};
48use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
49use fs::Fs;
50use futures::{
51 Future, FutureExt, StreamExt,
52 channel::{
53 mpsc::{self, UnboundedReceiver, UnboundedSender},
54 oneshot,
55 },
56 future::{Shared, try_join_all},
57};
58use gpui::{
59 Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Axis, Bounds,
60 Context, CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
61 Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView, MouseButton,
62 PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription,
63 SystemWindowTabController, Task, Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId,
64 WindowOptions, actions, canvas, point, relative, size, transparent_black,
65};
66pub use history_manager::*;
67pub use item::{
68 FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
69 ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
70};
71use itertools::Itertools;
72use language::{Buffer, LanguageRegistry, Rope, language_settings::all_language_settings};
73pub use modal_layer::*;
74use node_runtime::NodeRuntime;
75use notifications::{
76 DetachAndPromptErr, Notifications, dismiss_app_notification,
77 simple_message_notification::MessageNotification,
78};
79pub use pane::*;
80pub use pane_group::{
81 ActivePaneDecorator, HANDLE_HITBOX_SIZE, Member, PaneAxis, PaneGroup, PaneRenderContext,
82 SplitDirection,
83};
84use persistence::{SerializedWindowBounds, model::SerializedWorkspace};
85pub use persistence::{
86 WorkspaceDb, delete_unloaded_items,
87 model::{
88 DockStructure, ItemId, MultiWorkspaceState, SerializedMultiWorkspace,
89 SerializedProjectGroupKey, SerializedWorkspaceLocation, SessionWorkspace,
90 },
91 read_serialized_multi_workspaces, resolve_worktree_workspaces,
92};
93use postage::stream::Stream;
94use project::{
95 DirectoryLister, Project, ProjectEntryId, ProjectGroupKey, ProjectPath, ResolvedPath, Worktree,
96 WorktreeId, WorktreeSettings,
97 debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
98 project_settings::ProjectSettings,
99 toolchain_store::ToolchainStoreEvent,
100 trusted_worktrees::{RemoteHostLocation, TrustedWorktrees, TrustedWorktreesEvent},
101};
102use remote::{
103 RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions,
104 remote_client::ConnectionIdentifier,
105};
106use schemars::JsonSchema;
107use serde::Deserialize;
108use session::AppSession;
109use settings::{
110 CenteredPaddingSettings, Settings, SettingsLocation, SettingsStore, update_settings_file,
111};
112
113use sqlez::{
114 bindable::{Bind, Column, StaticColumnCount},
115 statement::Statement,
116};
117use status_bar::StatusBar;
118pub use status_bar::StatusItemView;
119use std::{
120 any::TypeId,
121 borrow::Cow,
122 cell::RefCell,
123 cmp,
124 collections::VecDeque,
125 env,
126 hash::Hash,
127 path::{Path, PathBuf},
128 process::ExitStatus,
129 rc::Rc,
130 sync::{
131 Arc, LazyLock,
132 atomic::{AtomicBool, AtomicUsize},
133 },
134 time::Duration,
135};
136use task::{DebugScenario, SharedTaskContext, SpawnInTerminal};
137use theme::{ActiveTheme, SystemAppearance};
138use theme_settings::ThemeSettings;
139pub use toolbar::{
140 PaneSearchBarCallbacks, Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
141};
142pub use ui;
143use ui::{Window, prelude::*};
144use util::{
145 ResultExt, TryFutureExt,
146 paths::{PathStyle, SanitizedPath},
147 rel_path::RelPath,
148 serde::default_true,
149};
150use uuid::Uuid;
151pub use workspace_settings::{
152 AutosaveSetting, BottomDockLayout, FocusFollowsMouse, RestoreOnStartupBehavior,
153 StatusBarSettings, TabBarSettings, WorkspaceSettings,
154};
155use zed_actions::{Spawn, feedback::FileBugReport, theme::ToggleMode};
156
157use crate::{dock::PanelSizeState, item::ItemBufferKind, notifications::NotificationId};
158use crate::{
159 persistence::{
160 SerializedAxis,
161 model::{DockData, SerializedItem, SerializedPane, SerializedPaneGroup},
162 },
163 security_modal::SecurityModal,
164};
165
166pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200);
167
168static ZED_WINDOW_SIZE: LazyLock<Option<Size<Pixels>>> = LazyLock::new(|| {
169 env::var("ZED_WINDOW_SIZE")
170 .ok()
171 .as_deref()
172 .and_then(parse_pixel_size_env_var)
173});
174
175static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
176 env::var("ZED_WINDOW_POSITION")
177 .ok()
178 .as_deref()
179 .and_then(parse_pixel_position_env_var)
180});
181
182pub trait TerminalProvider {
183 fn spawn(
184 &self,
185 task: SpawnInTerminal,
186 window: &mut Window,
187 cx: &mut App,
188 ) -> Task<Option<Result<ExitStatus>>>;
189}
190
191pub trait DebuggerProvider {
192 // `active_buffer` is used to resolve build task's name against language-specific tasks.
193 fn start_session(
194 &self,
195 definition: DebugScenario,
196 task_context: SharedTaskContext,
197 active_buffer: Option<Entity<Buffer>>,
198 worktree_id: Option<WorktreeId>,
199 window: &mut Window,
200 cx: &mut App,
201 );
202
203 fn spawn_task_or_modal(
204 &self,
205 workspace: &mut Workspace,
206 action: &Spawn,
207 window: &mut Window,
208 cx: &mut Context<Workspace>,
209 );
210
211 fn task_scheduled(&self, cx: &mut App);
212 fn debug_scenario_scheduled(&self, cx: &mut App);
213 fn debug_scenario_scheduled_last(&self, cx: &App) -> bool;
214
215 fn active_thread_state(&self, cx: &App) -> Option<ThreadStatus>;
216}
217
218/// Opens a file or directory.
219#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
220#[action(namespace = workspace)]
221pub struct Open {
222 /// When true, opens in a new window. When false, adds to the current
223 /// window as a new workspace (multi-workspace).
224 #[serde(default = "Open::default_create_new_window")]
225 pub create_new_window: bool,
226}
227
228impl Open {
229 pub const DEFAULT: Self = Self {
230 create_new_window: true,
231 };
232
233 /// Used by `#[serde(default)]` on the `create_new_window` field so that
234 /// the serde default and `Open::DEFAULT` stay in sync.
235 fn default_create_new_window() -> bool {
236 Self::DEFAULT.create_new_window
237 }
238}
239
240impl Default for Open {
241 fn default() -> Self {
242 Self::DEFAULT
243 }
244}
245
246actions!(
247 workspace,
248 [
249 /// Activates the next pane in the workspace.
250 ActivateNextPane,
251 /// Activates the previous pane in the workspace.
252 ActivatePreviousPane,
253 /// Activates the last pane in the workspace.
254 ActivateLastPane,
255 /// Switches to the next window.
256 ActivateNextWindow,
257 /// Switches to the previous window.
258 ActivatePreviousWindow,
259 /// Adds a folder to the current project.
260 AddFolderToProject,
261 /// Clears all notifications.
262 ClearAllNotifications,
263 /// Clears all navigation history, including forward/backward navigation, recently opened files, and recently closed tabs. **This action is irreversible**.
264 ClearNavigationHistory,
265 /// Closes the active dock.
266 CloseActiveDock,
267 /// Closes all docks.
268 CloseAllDocks,
269 /// Toggles all docks.
270 ToggleAllDocks,
271 /// Closes the current window.
272 CloseWindow,
273 /// Closes the current project.
274 CloseProject,
275 /// Opens the feedback dialog.
276 Feedback,
277 /// Follows the next collaborator in the session.
278 FollowNextCollaborator,
279 /// Moves the focused panel to the next position.
280 MoveFocusedPanelToNextPosition,
281 /// Creates a new file.
282 NewFile,
283 /// Creates a new file in a vertical split.
284 NewFileSplitVertical,
285 /// Creates a new file in a horizontal split.
286 NewFileSplitHorizontal,
287 /// Opens a new search.
288 NewSearch,
289 /// Opens a new window.
290 NewWindow,
291 /// Opens multiple files.
292 OpenFiles,
293 /// Opens the current location in terminal.
294 OpenInTerminal,
295 /// Opens the component preview.
296 OpenComponentPreview,
297 /// Reloads the active item.
298 ReloadActiveItem,
299 /// Resets the active dock to its default size.
300 ResetActiveDockSize,
301 /// Resets all open docks to their default sizes.
302 ResetOpenDocksSize,
303 /// Reloads the application
304 Reload,
305 /// Saves the current file with a new name.
306 SaveAs,
307 /// Saves without formatting.
308 SaveWithoutFormat,
309 /// Shuts down all debug adapters.
310 ShutdownDebugAdapters,
311 /// Suppresses the current notification.
312 SuppressNotification,
313 /// Toggles the bottom dock.
314 ToggleBottomDock,
315 /// Toggles centered layout mode.
316 ToggleCenteredLayout,
317 /// Toggles edit prediction feature globally for all files.
318 ToggleEditPrediction,
319 /// Toggles the left dock.
320 ToggleLeftDock,
321 /// Toggles the right dock.
322 ToggleRightDock,
323 /// Toggles zoom on the active pane.
324 ToggleZoom,
325 /// Toggles read-only mode for the active item (if supported by that item).
326 ToggleReadOnlyFile,
327 /// Zooms in on the active pane.
328 ZoomIn,
329 /// Zooms out of the active pane.
330 ZoomOut,
331 /// If any worktrees are in restricted mode, shows a modal with possible actions.
332 /// If the modal is shown already, closes it without trusting any worktree.
333 ToggleWorktreeSecurity,
334 /// Clears all trusted worktrees, placing them in restricted mode on next open.
335 /// Requires restart to take effect on already opened projects.
336 ClearTrustedWorktrees,
337 /// Stops following a collaborator.
338 Unfollow,
339 /// Restores the banner.
340 RestoreBanner,
341 /// Toggles expansion of the selected item.
342 ToggleExpandItem,
343 ]
344);
345
346/// Activates a specific pane by its index.
347#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
348#[action(namespace = workspace)]
349pub struct ActivatePane(pub usize);
350
351/// Moves an item to a specific pane by index.
352#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
353#[action(namespace = workspace)]
354#[serde(deny_unknown_fields)]
355pub struct MoveItemToPane {
356 #[serde(default = "default_1")]
357 pub destination: usize,
358 #[serde(default = "default_true")]
359 pub focus: bool,
360 #[serde(default)]
361 pub clone: bool,
362}
363
364fn default_1() -> usize {
365 1
366}
367
368/// Moves an item to a pane in the specified direction.
369#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
370#[action(namespace = workspace)]
371#[serde(deny_unknown_fields)]
372pub struct MoveItemToPaneInDirection {
373 #[serde(default = "default_right")]
374 pub direction: SplitDirection,
375 #[serde(default = "default_true")]
376 pub focus: bool,
377 #[serde(default)]
378 pub clone: bool,
379}
380
381/// Creates a new file in a split of the desired direction.
382#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
383#[action(namespace = workspace)]
384#[serde(deny_unknown_fields)]
385pub struct NewFileSplit(pub SplitDirection);
386
387fn default_right() -> SplitDirection {
388 SplitDirection::Right
389}
390
391/// Saves all open files in the workspace.
392#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
393#[action(namespace = workspace)]
394#[serde(deny_unknown_fields)]
395pub struct SaveAll {
396 #[serde(default)]
397 pub save_intent: Option<SaveIntent>,
398}
399
400/// Saves the current file with the specified options.
401#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
402#[action(namespace = workspace)]
403#[serde(deny_unknown_fields)]
404pub struct Save {
405 #[serde(default)]
406 pub save_intent: Option<SaveIntent>,
407}
408
409/// Moves Focus to the central panes in the workspace.
410#[derive(Clone, Debug, PartialEq, Eq, Action)]
411#[action(namespace = workspace)]
412pub struct FocusCenterPane;
413
414/// Closes all items and panes in the workspace.
415#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
416#[action(namespace = workspace)]
417#[serde(deny_unknown_fields)]
418pub struct CloseAllItemsAndPanes {
419 #[serde(default)]
420 pub save_intent: Option<SaveIntent>,
421}
422
423/// Closes all inactive tabs and panes in the workspace.
424#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
425#[action(namespace = workspace)]
426#[serde(deny_unknown_fields)]
427pub struct CloseInactiveTabsAndPanes {
428 #[serde(default)]
429 pub save_intent: Option<SaveIntent>,
430}
431
432/// Closes the active item across all panes.
433#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
434#[action(namespace = workspace)]
435#[serde(deny_unknown_fields)]
436pub struct CloseItemInAllPanes {
437 #[serde(default)]
438 pub save_intent: Option<SaveIntent>,
439 #[serde(default)]
440 pub close_pinned: bool,
441}
442
443/// Sends a sequence of keystrokes to the active element.
444#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
445#[action(namespace = workspace)]
446pub struct SendKeystrokes(pub String);
447
448actions!(
449 project_symbols,
450 [
451 /// Toggles the project symbols search.
452 #[action(name = "Toggle")]
453 ToggleProjectSymbols
454 ]
455);
456
457/// Toggles the file finder interface.
458#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
459#[action(namespace = file_finder, name = "Toggle")]
460#[serde(deny_unknown_fields)]
461pub struct ToggleFileFinder {
462 #[serde(default)]
463 pub separate_history: bool,
464}
465
466/// Opens a new terminal in the center.
467#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
468#[action(namespace = workspace)]
469#[serde(deny_unknown_fields)]
470pub struct NewCenterTerminal {
471 /// If true, creates a local terminal even in remote projects.
472 #[serde(default)]
473 pub local: bool,
474}
475
476/// Opens a new terminal.
477#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
478#[action(namespace = workspace)]
479#[serde(deny_unknown_fields)]
480pub struct NewTerminal {
481 /// If true, creates a local terminal even in remote projects.
482 #[serde(default)]
483 pub local: bool,
484}
485
486/// Increases size of a currently focused dock by a given amount of pixels.
487#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
488#[action(namespace = workspace)]
489#[serde(deny_unknown_fields)]
490pub struct IncreaseActiveDockSize {
491 /// For 0px parameter, uses UI font size value.
492 #[serde(default)]
493 pub px: u32,
494}
495
496/// Decreases size of a currently focused dock by a given amount of pixels.
497#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
498#[action(namespace = workspace)]
499#[serde(deny_unknown_fields)]
500pub struct DecreaseActiveDockSize {
501 /// For 0px parameter, uses UI font size value.
502 #[serde(default)]
503 pub px: u32,
504}
505
506/// Increases size of all currently visible docks uniformly, by a given amount of pixels.
507#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
508#[action(namespace = workspace)]
509#[serde(deny_unknown_fields)]
510pub struct IncreaseOpenDocksSize {
511 /// For 0px parameter, uses UI font size value.
512 #[serde(default)]
513 pub px: u32,
514}
515
516/// Decreases size of all currently visible docks uniformly, by a given amount of pixels.
517#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
518#[action(namespace = workspace)]
519#[serde(deny_unknown_fields)]
520pub struct DecreaseOpenDocksSize {
521 /// For 0px parameter, uses UI font size value.
522 #[serde(default)]
523 pub px: u32,
524}
525
526actions!(
527 workspace,
528 [
529 /// Activates the pane to the left.
530 ActivatePaneLeft,
531 /// Activates the pane to the right.
532 ActivatePaneRight,
533 /// Activates the pane above.
534 ActivatePaneUp,
535 /// Activates the pane below.
536 ActivatePaneDown,
537 /// Swaps the current pane with the one to the left.
538 SwapPaneLeft,
539 /// Swaps the current pane with the one to the right.
540 SwapPaneRight,
541 /// Swaps the current pane with the one above.
542 SwapPaneUp,
543 /// Swaps the current pane with the one below.
544 SwapPaneDown,
545 // Swaps the current pane with the first available adjacent pane (searching in order: below, above, right, left) and activates that pane.
546 SwapPaneAdjacent,
547 /// Move the current pane to be at the far left.
548 MovePaneLeft,
549 /// Move the current pane to be at the far right.
550 MovePaneRight,
551 /// Move the current pane to be at the very top.
552 MovePaneUp,
553 /// Move the current pane to be at the very bottom.
554 MovePaneDown,
555 ]
556);
557
558#[derive(PartialEq, Eq, Debug)]
559pub enum CloseIntent {
560 /// Quit the program entirely.
561 Quit,
562 /// Close a window.
563 CloseWindow,
564 /// Replace the workspace in an existing window.
565 ReplaceWindow,
566}
567
568#[derive(Clone)]
569pub struct Toast {
570 id: NotificationId,
571 msg: Cow<'static, str>,
572 autohide: bool,
573 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut Window, &mut App)>)>,
574}
575
576impl Toast {
577 pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
578 Toast {
579 id,
580 msg: msg.into(),
581 on_click: None,
582 autohide: false,
583 }
584 }
585
586 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
587 where
588 M: Into<Cow<'static, str>>,
589 F: Fn(&mut Window, &mut App) + 'static,
590 {
591 self.on_click = Some((message.into(), Arc::new(on_click)));
592 self
593 }
594
595 pub fn autohide(mut self) -> Self {
596 self.autohide = true;
597 self
598 }
599}
600
601impl PartialEq for Toast {
602 fn eq(&self, other: &Self) -> bool {
603 self.id == other.id
604 && self.msg == other.msg
605 && self.on_click.is_some() == other.on_click.is_some()
606 }
607}
608
609/// Opens a new terminal with the specified working directory.
610#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema, Action)]
611#[action(namespace = workspace)]
612#[serde(deny_unknown_fields)]
613pub struct OpenTerminal {
614 pub working_directory: PathBuf,
615 /// If true, creates a local terminal even in remote projects.
616 #[serde(default)]
617 pub local: bool,
618}
619
620#[derive(
621 Clone,
622 Copy,
623 Debug,
624 Default,
625 Hash,
626 PartialEq,
627 Eq,
628 PartialOrd,
629 Ord,
630 serde::Serialize,
631 serde::Deserialize,
632)]
633pub struct WorkspaceId(i64);
634
635impl WorkspaceId {
636 pub fn from_i64(value: i64) -> Self {
637 Self(value)
638 }
639}
640
641impl StaticColumnCount for WorkspaceId {}
642impl Bind for WorkspaceId {
643 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
644 self.0.bind(statement, start_index)
645 }
646}
647impl Column for WorkspaceId {
648 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
649 i64::column(statement, start_index)
650 .map(|(i, next_index)| (Self(i), next_index))
651 .with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
652 }
653}
654impl From<WorkspaceId> for i64 {
655 fn from(val: WorkspaceId) -> Self {
656 val.0
657 }
658}
659
660fn prompt_and_open_paths(
661 app_state: Arc<AppState>,
662 options: PathPromptOptions,
663 create_new_window: bool,
664 cx: &mut App,
665) {
666 if let Some(workspace_window) = local_workspace_windows(cx).into_iter().next() {
667 workspace_window
668 .update(cx, |multi_workspace, window, cx| {
669 let workspace = multi_workspace.workspace().clone();
670 workspace.update(cx, |workspace, cx| {
671 prompt_for_open_path_and_open(
672 workspace,
673 app_state,
674 options,
675 create_new_window,
676 window,
677 cx,
678 );
679 });
680 })
681 .ok();
682 } else {
683 let task = Workspace::new_local(
684 Vec::new(),
685 app_state.clone(),
686 None,
687 None,
688 None,
689 OpenMode::Activate,
690 cx,
691 );
692 cx.spawn(async move |cx| {
693 let OpenResult { window, .. } = task.await?;
694 window.update(cx, |multi_workspace, window, cx| {
695 window.activate_window();
696 let workspace = multi_workspace.workspace().clone();
697 workspace.update(cx, |workspace, cx| {
698 prompt_for_open_path_and_open(
699 workspace,
700 app_state,
701 options,
702 create_new_window,
703 window,
704 cx,
705 );
706 });
707 })?;
708 anyhow::Ok(())
709 })
710 .detach_and_log_err(cx);
711 }
712}
713
714pub fn prompt_for_open_path_and_open(
715 workspace: &mut Workspace,
716 app_state: Arc<AppState>,
717 options: PathPromptOptions,
718 create_new_window: bool,
719 window: &mut Window,
720 cx: &mut Context<Workspace>,
721) {
722 let paths = workspace.prompt_for_open_path(
723 options,
724 DirectoryLister::Local(workspace.project().clone(), app_state.fs.clone()),
725 window,
726 cx,
727 );
728 let multi_workspace_handle = window.window_handle().downcast::<MultiWorkspace>();
729 cx.spawn_in(window, async move |this, cx| {
730 let Some(paths) = paths.await.log_err().flatten() else {
731 return;
732 };
733 if !create_new_window {
734 if let Some(handle) = multi_workspace_handle {
735 if let Some(task) = handle
736 .update(cx, |multi_workspace, window, cx| {
737 multi_workspace.open_project(paths, OpenMode::Activate, window, cx)
738 })
739 .log_err()
740 {
741 task.await.log_err();
742 }
743 return;
744 }
745 }
746 if let Some(task) = this
747 .update_in(cx, |this, window, cx| {
748 this.open_workspace_for_paths(OpenMode::NewWindow, paths, window, cx)
749 })
750 .log_err()
751 {
752 task.await.log_err();
753 }
754 })
755 .detach();
756}
757
758pub fn init(app_state: Arc<AppState>, cx: &mut App) {
759 component::init();
760 theme_preview::init(cx);
761 toast_layer::init(cx);
762 history_manager::init(app_state.fs.clone(), cx);
763
764 cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx))
765 .on_action(|_: &Reload, cx| reload(cx))
766 .on_action(|action: &Open, cx: &mut App| {
767 let app_state = AppState::global(cx);
768 prompt_and_open_paths(
769 app_state,
770 PathPromptOptions {
771 files: true,
772 directories: true,
773 multiple: true,
774 prompt: None,
775 },
776 action.create_new_window,
777 cx,
778 );
779 })
780 .on_action(|_: &OpenFiles, cx: &mut App| {
781 let directories = cx.can_select_mixed_files_and_dirs();
782 let app_state = AppState::global(cx);
783 prompt_and_open_paths(
784 app_state,
785 PathPromptOptions {
786 files: true,
787 directories,
788 multiple: true,
789 prompt: None,
790 },
791 true,
792 cx,
793 );
794 });
795}
796
797type BuildProjectItemFn =
798 fn(AnyEntity, Entity<Project>, Option<&Pane>, &mut Window, &mut App) -> Box<dyn ItemHandle>;
799
800type BuildProjectItemForPathFn =
801 fn(
802 &Entity<Project>,
803 &ProjectPath,
804 &mut Window,
805 &mut App,
806 ) -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
807
808#[derive(Clone, Default)]
809struct ProjectItemRegistry {
810 build_project_item_fns_by_type: HashMap<TypeId, BuildProjectItemFn>,
811 build_project_item_for_path_fns: Vec<BuildProjectItemForPathFn>,
812}
813
814impl ProjectItemRegistry {
815 fn register<T: ProjectItem>(&mut self) {
816 self.build_project_item_fns_by_type.insert(
817 TypeId::of::<T::Item>(),
818 |item, project, pane, window, cx| {
819 let item = item.downcast().unwrap();
820 Box::new(cx.new(|cx| T::for_project_item(project, pane, item, window, cx)))
821 as Box<dyn ItemHandle>
822 },
823 );
824 self.build_project_item_for_path_fns
825 .push(|project, project_path, window, cx| {
826 let project_path = project_path.clone();
827 let is_file = project
828 .read(cx)
829 .entry_for_path(&project_path, cx)
830 .is_some_and(|entry| entry.is_file());
831 let entry_abs_path = project.read(cx).absolute_path(&project_path, cx);
832 let is_local = project.read(cx).is_local();
833 let project_item =
834 <T::Item as project::ProjectItem>::try_open(project, &project_path, cx)?;
835 let project = project.clone();
836 Some(window.spawn(cx, async move |cx| {
837 match project_item.await.with_context(|| {
838 format!(
839 "opening project path {:?}",
840 entry_abs_path.as_deref().unwrap_or(&project_path.path.as_std_path())
841 )
842 }) {
843 Ok(project_item) => {
844 let project_item = project_item;
845 let project_entry_id: Option<ProjectEntryId> =
846 project_item.read_with(cx, project::ProjectItem::entry_id);
847 let build_workspace_item = Box::new(
848 |pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
849 Box::new(cx.new(|cx| {
850 T::for_project_item(
851 project,
852 Some(pane),
853 project_item,
854 window,
855 cx,
856 )
857 })) as Box<dyn ItemHandle>
858 },
859 ) as Box<_>;
860 Ok((project_entry_id, build_workspace_item))
861 }
862 Err(e) => {
863 log::warn!("Failed to open a project item: {e:#}");
864 if e.error_code() == ErrorCode::Internal {
865 if let Some(abs_path) =
866 entry_abs_path.as_deref().filter(|_| is_file)
867 {
868 if let Some(broken_project_item_view) =
869 cx.update(|window, cx| {
870 T::for_broken_project_item(
871 abs_path, is_local, &e, window, cx,
872 )
873 })?
874 {
875 let build_workspace_item = Box::new(
876 move |_: &mut Pane, _: &mut Window, cx: &mut Context<Pane>| {
877 cx.new(|_| broken_project_item_view).boxed_clone()
878 },
879 )
880 as Box<_>;
881 return Ok((None, build_workspace_item));
882 }
883 }
884 }
885 Err(e)
886 }
887 }
888 }))
889 });
890 }
891
892 fn open_path(
893 &self,
894 project: &Entity<Project>,
895 path: &ProjectPath,
896 window: &mut Window,
897 cx: &mut App,
898 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
899 let Some(open_project_item) = self
900 .build_project_item_for_path_fns
901 .iter()
902 .rev()
903 .find_map(|open_project_item| open_project_item(project, path, window, cx))
904 else {
905 return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
906 };
907 open_project_item
908 }
909
910 fn build_item<T: project::ProjectItem>(
911 &self,
912 item: Entity<T>,
913 project: Entity<Project>,
914 pane: Option<&Pane>,
915 window: &mut Window,
916 cx: &mut App,
917 ) -> Option<Box<dyn ItemHandle>> {
918 let build = self
919 .build_project_item_fns_by_type
920 .get(&TypeId::of::<T>())?;
921 Some(build(item.into_any(), project, pane, window, cx))
922 }
923}
924
925type WorkspaceItemBuilder =
926 Box<dyn FnOnce(&mut Pane, &mut Window, &mut Context<Pane>) -> Box<dyn ItemHandle>>;
927
928impl Global for ProjectItemRegistry {}
929
930/// Registers a [ProjectItem] for the app. When opening a file, all the registered
931/// items will get a chance to open the file, starting from the project item that
932/// was added last.
933pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
934 cx.default_global::<ProjectItemRegistry>().register::<I>();
935}
936
937#[derive(Default)]
938pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
939
940struct FollowableViewDescriptor {
941 from_state_proto: fn(
942 Entity<Workspace>,
943 ViewId,
944 &mut Option<proto::view::Variant>,
945 &mut Window,
946 &mut App,
947 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
948 to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
949}
950
951impl Global for FollowableViewRegistry {}
952
953impl FollowableViewRegistry {
954 pub fn register<I: FollowableItem>(cx: &mut App) {
955 cx.default_global::<Self>().0.insert(
956 TypeId::of::<I>(),
957 FollowableViewDescriptor {
958 from_state_proto: |workspace, id, state, window, cx| {
959 I::from_state_proto(workspace, id, state, window, cx).map(|task| {
960 cx.foreground_executor()
961 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
962 })
963 },
964 to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
965 },
966 );
967 }
968
969 pub fn from_state_proto(
970 workspace: Entity<Workspace>,
971 view_id: ViewId,
972 mut state: Option<proto::view::Variant>,
973 window: &mut Window,
974 cx: &mut App,
975 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
976 cx.update_default_global(|this: &mut Self, cx| {
977 this.0.values().find_map(|descriptor| {
978 (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, window, cx)
979 })
980 })
981 }
982
983 pub fn to_followable_view(
984 view: impl Into<AnyView>,
985 cx: &App,
986 ) -> Option<Box<dyn FollowableItemHandle>> {
987 let this = cx.try_global::<Self>()?;
988 let view = view.into();
989 let descriptor = this.0.get(&view.entity_type())?;
990 Some((descriptor.to_followable_view)(&view))
991 }
992}
993
994#[derive(Copy, Clone)]
995struct SerializableItemDescriptor {
996 deserialize: fn(
997 Entity<Project>,
998 WeakEntity<Workspace>,
999 WorkspaceId,
1000 ItemId,
1001 &mut Window,
1002 &mut Context<Pane>,
1003 ) -> Task<Result<Box<dyn ItemHandle>>>,
1004 cleanup: fn(WorkspaceId, Vec<ItemId>, &mut Window, &mut App) -> Task<Result<()>>,
1005 view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
1006}
1007
1008#[derive(Default)]
1009struct SerializableItemRegistry {
1010 descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
1011 descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
1012}
1013
1014impl Global for SerializableItemRegistry {}
1015
1016impl SerializableItemRegistry {
1017 fn deserialize(
1018 item_kind: &str,
1019 project: Entity<Project>,
1020 workspace: WeakEntity<Workspace>,
1021 workspace_id: WorkspaceId,
1022 item_item: ItemId,
1023 window: &mut Window,
1024 cx: &mut Context<Pane>,
1025 ) -> Task<Result<Box<dyn ItemHandle>>> {
1026 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
1027 return Task::ready(Err(anyhow!(
1028 "cannot deserialize {}, descriptor not found",
1029 item_kind
1030 )));
1031 };
1032
1033 (descriptor.deserialize)(project, workspace, workspace_id, item_item, window, cx)
1034 }
1035
1036 fn cleanup(
1037 item_kind: &str,
1038 workspace_id: WorkspaceId,
1039 loaded_items: Vec<ItemId>,
1040 window: &mut Window,
1041 cx: &mut App,
1042 ) -> Task<Result<()>> {
1043 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
1044 return Task::ready(Err(anyhow!(
1045 "cannot cleanup {}, descriptor not found",
1046 item_kind
1047 )));
1048 };
1049
1050 (descriptor.cleanup)(workspace_id, loaded_items, window, cx)
1051 }
1052
1053 fn view_to_serializable_item_handle(
1054 view: AnyView,
1055 cx: &App,
1056 ) -> Option<Box<dyn SerializableItemHandle>> {
1057 let this = cx.try_global::<Self>()?;
1058 let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
1059 Some((descriptor.view_to_serializable_item)(view))
1060 }
1061
1062 fn descriptor(item_kind: &str, cx: &App) -> Option<SerializableItemDescriptor> {
1063 let this = cx.try_global::<Self>()?;
1064 this.descriptors_by_kind.get(item_kind).copied()
1065 }
1066}
1067
1068pub fn register_serializable_item<I: SerializableItem>(cx: &mut App) {
1069 let serialized_item_kind = I::serialized_item_kind();
1070
1071 let registry = cx.default_global::<SerializableItemRegistry>();
1072 let descriptor = SerializableItemDescriptor {
1073 deserialize: |project, workspace, workspace_id, item_id, window, cx| {
1074 let task = I::deserialize(project, workspace, workspace_id, item_id, window, cx);
1075 cx.foreground_executor()
1076 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
1077 },
1078 cleanup: |workspace_id, loaded_items, window, cx| {
1079 I::cleanup(workspace_id, loaded_items, window, cx)
1080 },
1081 view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
1082 };
1083 registry
1084 .descriptors_by_kind
1085 .insert(Arc::from(serialized_item_kind), descriptor);
1086 registry
1087 .descriptors_by_type
1088 .insert(TypeId::of::<I>(), descriptor);
1089}
1090
1091pub struct AppState {
1092 pub languages: Arc<LanguageRegistry>,
1093 pub client: Arc<Client>,
1094 pub user_store: Entity<UserStore>,
1095 pub workspace_store: Entity<WorkspaceStore>,
1096 pub fs: Arc<dyn fs::Fs>,
1097 pub build_window_options: fn(Option<Uuid>, &mut App) -> WindowOptions,
1098 pub node_runtime: NodeRuntime,
1099 pub session: Entity<AppSession>,
1100}
1101
1102struct GlobalAppState(Arc<AppState>);
1103
1104impl Global for GlobalAppState {}
1105
1106pub struct WorkspaceStore {
1107 workspaces: HashSet<(gpui::AnyWindowHandle, WeakEntity<Workspace>)>,
1108 client: Arc<Client>,
1109 _subscriptions: Vec<client::Subscription>,
1110}
1111
1112#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
1113pub enum CollaboratorId {
1114 PeerId(PeerId),
1115 Agent,
1116}
1117
1118impl From<PeerId> for CollaboratorId {
1119 fn from(peer_id: PeerId) -> Self {
1120 CollaboratorId::PeerId(peer_id)
1121 }
1122}
1123
1124impl From<&PeerId> for CollaboratorId {
1125 fn from(peer_id: &PeerId) -> Self {
1126 CollaboratorId::PeerId(*peer_id)
1127 }
1128}
1129
1130#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
1131struct Follower {
1132 project_id: Option<u64>,
1133 peer_id: PeerId,
1134}
1135
1136impl AppState {
1137 #[track_caller]
1138 pub fn global(cx: &App) -> Arc<Self> {
1139 cx.global::<GlobalAppState>().0.clone()
1140 }
1141 pub fn try_global(cx: &App) -> Option<Arc<Self>> {
1142 cx.try_global::<GlobalAppState>()
1143 .map(|state| state.0.clone())
1144 }
1145 pub fn set_global(state: Arc<AppState>, cx: &mut App) {
1146 cx.set_global(GlobalAppState(state));
1147 }
1148
1149 #[cfg(any(test, feature = "test-support"))]
1150 pub fn test(cx: &mut App) -> Arc<Self> {
1151 use fs::Fs;
1152 use node_runtime::NodeRuntime;
1153 use session::Session;
1154 use settings::SettingsStore;
1155
1156 if !cx.has_global::<SettingsStore>() {
1157 let settings_store = SettingsStore::test(cx);
1158 cx.set_global(settings_store);
1159 }
1160
1161 let fs = fs::FakeFs::new(cx.background_executor().clone());
1162 <dyn Fs>::set_global(fs.clone(), cx);
1163 let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
1164 let clock = Arc::new(clock::FakeSystemClock::new());
1165 let http_client = http_client::FakeHttpClient::with_404_response();
1166 let client = Client::new(clock, http_client, cx);
1167 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
1168 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1169 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
1170
1171 theme_settings::init(theme::LoadThemes::JustBase, cx);
1172 client::init(&client, cx);
1173
1174 Arc::new(Self {
1175 client,
1176 fs,
1177 languages,
1178 user_store,
1179 workspace_store,
1180 node_runtime: NodeRuntime::unavailable(),
1181 build_window_options: |_, _| Default::default(),
1182 session,
1183 })
1184 }
1185}
1186
1187struct DelayedDebouncedEditAction {
1188 task: Option<Task<()>>,
1189 cancel_channel: Option<oneshot::Sender<()>>,
1190}
1191
1192impl DelayedDebouncedEditAction {
1193 fn new() -> DelayedDebouncedEditAction {
1194 DelayedDebouncedEditAction {
1195 task: None,
1196 cancel_channel: None,
1197 }
1198 }
1199
1200 fn fire_new<F>(
1201 &mut self,
1202 delay: Duration,
1203 window: &mut Window,
1204 cx: &mut Context<Workspace>,
1205 func: F,
1206 ) where
1207 F: 'static
1208 + Send
1209 + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> Task<Result<()>>,
1210 {
1211 if let Some(channel) = self.cancel_channel.take() {
1212 _ = channel.send(());
1213 }
1214
1215 let (sender, mut receiver) = oneshot::channel::<()>();
1216 self.cancel_channel = Some(sender);
1217
1218 let previous_task = self.task.take();
1219 self.task = Some(cx.spawn_in(window, async move |workspace, cx| {
1220 let mut timer = cx.background_executor().timer(delay).fuse();
1221 if let Some(previous_task) = previous_task {
1222 previous_task.await;
1223 }
1224
1225 futures::select_biased! {
1226 _ = receiver => return,
1227 _ = timer => {}
1228 }
1229
1230 if let Some(result) = workspace
1231 .update_in(cx, |workspace, window, cx| (func)(workspace, window, cx))
1232 .log_err()
1233 {
1234 result.await.log_err();
1235 }
1236 }));
1237 }
1238}
1239
1240pub enum Event {
1241 PaneAdded(Entity<Pane>),
1242 PaneRemoved,
1243 ItemAdded {
1244 item: Box<dyn ItemHandle>,
1245 },
1246 ActiveItemChanged,
1247 ItemRemoved {
1248 item_id: EntityId,
1249 },
1250 UserSavedItem {
1251 pane: WeakEntity<Pane>,
1252 item: Box<dyn WeakItemHandle>,
1253 save_intent: SaveIntent,
1254 },
1255 ContactRequestedJoin(u64),
1256 WorkspaceCreated(WeakEntity<Workspace>),
1257 OpenBundledFile {
1258 text: Cow<'static, str>,
1259 title: &'static str,
1260 language: &'static str,
1261 },
1262 ZoomChanged,
1263 ModalOpened,
1264 Activate,
1265 PanelAdded(AnyView),
1266}
1267
1268#[derive(Debug, Clone)]
1269pub enum OpenVisible {
1270 All,
1271 None,
1272 OnlyFiles,
1273 OnlyDirectories,
1274}
1275
1276enum WorkspaceLocation {
1277 // Valid local paths or SSH project to serialize
1278 Location(SerializedWorkspaceLocation, PathList),
1279 // No valid location found hence clear session id
1280 DetachFromSession,
1281 // No valid location found to serialize
1282 None,
1283}
1284
1285type PromptForNewPath = Box<
1286 dyn Fn(
1287 &mut Workspace,
1288 DirectoryLister,
1289 Option<String>,
1290 &mut Window,
1291 &mut Context<Workspace>,
1292 ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
1293>;
1294
1295type PromptForOpenPath = Box<
1296 dyn Fn(
1297 &mut Workspace,
1298 DirectoryLister,
1299 &mut Window,
1300 &mut Context<Workspace>,
1301 ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
1302>;
1303
1304#[derive(Default)]
1305struct DispatchingKeystrokes {
1306 dispatched: HashSet<Vec<Keystroke>>,
1307 queue: VecDeque<Keystroke>,
1308 task: Option<Shared<Task<()>>>,
1309}
1310
1311/// Collects everything project-related for a certain window opened.
1312/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
1313///
1314/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
1315/// The `Workspace` owns everybody's state and serves as a default, "global context",
1316/// that can be used to register a global action to be triggered from any place in the window.
1317pub struct Workspace {
1318 weak_self: WeakEntity<Self>,
1319 workspace_actions: Vec<Box<dyn Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div>>,
1320 zoomed: Option<AnyWeakView>,
1321 previous_dock_drag_coordinates: Option<Point<Pixels>>,
1322 zoomed_position: Option<DockPosition>,
1323 center: PaneGroup,
1324 left_dock: Entity<Dock>,
1325 bottom_dock: Entity<Dock>,
1326 right_dock: Entity<Dock>,
1327 panes: Vec<Entity<Pane>>,
1328 active_worktree_override: Option<WorktreeId>,
1329 panes_by_item: HashMap<EntityId, WeakEntity<Pane>>,
1330 active_pane: Entity<Pane>,
1331 last_active_center_pane: Option<WeakEntity<Pane>>,
1332 last_active_view_id: Option<proto::ViewId>,
1333 status_bar: Entity<StatusBar>,
1334 pub(crate) modal_layer: Entity<ModalLayer>,
1335 toast_layer: Entity<ToastLayer>,
1336 titlebar_item: Option<AnyView>,
1337 notifications: Notifications,
1338 suppressed_notifications: HashSet<NotificationId>,
1339 project: Entity<Project>,
1340 follower_states: HashMap<CollaboratorId, FollowerState>,
1341 last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
1342 window_edited: bool,
1343 last_window_title: Option<String>,
1344 dirty_items: HashMap<EntityId, Subscription>,
1345 active_call: Option<(GlobalAnyActiveCall, Vec<Subscription>)>,
1346 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
1347 database_id: Option<WorkspaceId>,
1348 app_state: Arc<AppState>,
1349 dispatching_keystrokes: Rc<RefCell<DispatchingKeystrokes>>,
1350 _subscriptions: Vec<Subscription>,
1351 _apply_leader_updates: Task<Result<()>>,
1352 _observe_current_user: Task<Result<()>>,
1353 _schedule_serialize_workspace: Option<Task<()>>,
1354 _serialize_workspace_task: Option<Task<()>>,
1355 _schedule_serialize_ssh_paths: Option<Task<()>>,
1356 pane_history_timestamp: Arc<AtomicUsize>,
1357 bounds: Bounds<Pixels>,
1358 pub centered_layout: bool,
1359 bounds_save_task_queued: Option<Task<()>>,
1360 on_prompt_for_new_path: Option<PromptForNewPath>,
1361 on_prompt_for_open_path: Option<PromptForOpenPath>,
1362 terminal_provider: Option<Box<dyn TerminalProvider>>,
1363 debugger_provider: Option<Arc<dyn DebuggerProvider>>,
1364 serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
1365 _items_serializer: Task<Result<()>>,
1366 session_id: Option<String>,
1367 scheduled_tasks: Vec<Task<()>>,
1368 last_open_dock_positions: Vec<DockPosition>,
1369 removing: bool,
1370 open_in_dev_container: bool,
1371 _dev_container_task: Option<Task<Result<()>>>,
1372 _panels_task: Option<Task<Result<()>>>,
1373 sidebar_focus_handle: Option<FocusHandle>,
1374 multi_workspace: Option<WeakEntity<MultiWorkspace>>,
1375}
1376
1377impl EventEmitter<Event> for Workspace {}
1378
1379#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
1380pub struct ViewId {
1381 pub creator: CollaboratorId,
1382 pub id: u64,
1383}
1384
1385pub struct FollowerState {
1386 center_pane: Entity<Pane>,
1387 dock_pane: Option<Entity<Pane>>,
1388 active_view_id: Option<ViewId>,
1389 items_by_leader_view_id: HashMap<ViewId, FollowerView>,
1390}
1391
1392struct FollowerView {
1393 view: Box<dyn FollowableItemHandle>,
1394 location: Option<proto::PanelId>,
1395}
1396
1397#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1398pub enum OpenMode {
1399 /// Open the workspace in a new window.
1400 NewWindow,
1401 /// Add to the window's multi workspace without activating it (used during deserialization).
1402 Add,
1403 /// Add to the window's multi workspace and activate it.
1404 #[default]
1405 Activate,
1406}
1407
1408impl Workspace {
1409 pub fn new(
1410 workspace_id: Option<WorkspaceId>,
1411 project: Entity<Project>,
1412 app_state: Arc<AppState>,
1413 window: &mut Window,
1414 cx: &mut Context<Self>,
1415 ) -> Self {
1416 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1417 cx.subscribe(&trusted_worktrees, |_, worktrees_store, e, cx| {
1418 if let TrustedWorktreesEvent::Trusted(..) = e {
1419 // Do not persist auto trusted worktrees
1420 if !ProjectSettings::get_global(cx).session.trust_all_worktrees {
1421 worktrees_store.update(cx, |worktrees_store, cx| {
1422 worktrees_store.schedule_serialization(
1423 cx,
1424 |new_trusted_worktrees, cx| {
1425 let timeout =
1426 cx.background_executor().timer(SERIALIZATION_THROTTLE_TIME);
1427 let db = WorkspaceDb::global(cx);
1428 cx.background_spawn(async move {
1429 timeout.await;
1430 db.save_trusted_worktrees(new_trusted_worktrees)
1431 .await
1432 .log_err();
1433 })
1434 },
1435 )
1436 });
1437 }
1438 }
1439 })
1440 .detach();
1441
1442 cx.observe_global::<SettingsStore>(|_, cx| {
1443 if ProjectSettings::get_global(cx).session.trust_all_worktrees {
1444 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1445 trusted_worktrees.update(cx, |trusted_worktrees, cx| {
1446 trusted_worktrees.auto_trust_all(cx);
1447 })
1448 }
1449 }
1450 })
1451 .detach();
1452 }
1453
1454 cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
1455 match event {
1456 project::Event::RemoteIdChanged(_) => {
1457 this.update_window_title(window, cx);
1458 }
1459
1460 project::Event::CollaboratorLeft(peer_id) => {
1461 this.collaborator_left(*peer_id, window, cx);
1462 }
1463
1464 &project::Event::WorktreeRemoved(_) => {
1465 this.update_window_title(window, cx);
1466 this.serialize_workspace(window, cx);
1467 this.update_history(cx);
1468 }
1469
1470 &project::Event::WorktreeAdded(id) => {
1471 this.update_window_title(window, cx);
1472 if this
1473 .project()
1474 .read(cx)
1475 .worktree_for_id(id, cx)
1476 .is_some_and(|wt| wt.read(cx).is_visible())
1477 {
1478 this.serialize_workspace(window, cx);
1479 this.update_history(cx);
1480 }
1481 }
1482 project::Event::WorktreeUpdatedEntries(..) => {
1483 this.update_window_title(window, cx);
1484 this.serialize_workspace(window, cx);
1485 }
1486
1487 project::Event::DisconnectedFromHost => {
1488 this.update_window_edited(window, cx);
1489 let leaders_to_unfollow =
1490 this.follower_states.keys().copied().collect::<Vec<_>>();
1491 for leader_id in leaders_to_unfollow {
1492 this.unfollow(leader_id, window, cx);
1493 }
1494 }
1495
1496 project::Event::DisconnectedFromRemote {
1497 server_not_running: _,
1498 } => {
1499 this.update_window_edited(window, cx);
1500 }
1501
1502 project::Event::Closed => {
1503 window.remove_window();
1504 }
1505
1506 project::Event::DeletedEntry(_, entry_id) => {
1507 for pane in this.panes.iter() {
1508 pane.update(cx, |pane, cx| {
1509 pane.handle_deleted_project_item(*entry_id, window, cx)
1510 });
1511 }
1512 }
1513
1514 project::Event::Toast {
1515 notification_id,
1516 message,
1517 link,
1518 } => this.show_notification(
1519 NotificationId::named(notification_id.clone()),
1520 cx,
1521 |cx| {
1522 let mut notification = MessageNotification::new(message.clone(), cx);
1523 if let Some(link) = link {
1524 notification = notification
1525 .more_info_message(link.label)
1526 .more_info_url(link.url);
1527 }
1528
1529 cx.new(|_| notification)
1530 },
1531 ),
1532
1533 project::Event::HideToast { notification_id } => {
1534 this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
1535 }
1536
1537 project::Event::LanguageServerPrompt(request) => {
1538 struct LanguageServerPrompt;
1539
1540 this.show_notification(
1541 NotificationId::composite::<LanguageServerPrompt>(request.id),
1542 cx,
1543 |cx| {
1544 cx.new(|cx| {
1545 notifications::LanguageServerPrompt::new(request.clone(), cx)
1546 })
1547 },
1548 );
1549 }
1550
1551 project::Event::AgentLocationChanged => {
1552 this.handle_agent_location_changed(window, cx)
1553 }
1554
1555 _ => {}
1556 }
1557 cx.notify()
1558 })
1559 .detach();
1560
1561 cx.subscribe_in(
1562 &project.read(cx).breakpoint_store(),
1563 window,
1564 |workspace, _, event, window, cx| match event {
1565 BreakpointStoreEvent::BreakpointsUpdated(_, _)
1566 | BreakpointStoreEvent::BreakpointsCleared(_) => {
1567 workspace.serialize_workspace(window, cx);
1568 }
1569 BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {}
1570 },
1571 )
1572 .detach();
1573 if let Some(toolchain_store) = project.read(cx).toolchain_store() {
1574 cx.subscribe_in(
1575 &toolchain_store,
1576 window,
1577 |workspace, _, event, window, cx| match event {
1578 ToolchainStoreEvent::CustomToolchainsModified => {
1579 workspace.serialize_workspace(window, cx);
1580 }
1581 _ => {}
1582 },
1583 )
1584 .detach();
1585 }
1586
1587 cx.on_focus_lost(window, |this, window, cx| {
1588 let focus_handle = this.focus_handle(cx);
1589 window.focus(&focus_handle, cx);
1590 })
1591 .detach();
1592
1593 let weak_handle = cx.entity().downgrade();
1594 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
1595
1596 let center_pane = cx.new(|cx| {
1597 let mut center_pane = Pane::new(
1598 weak_handle.clone(),
1599 project.clone(),
1600 pane_history_timestamp.clone(),
1601 None,
1602 NewFile.boxed_clone(),
1603 true,
1604 window,
1605 cx,
1606 );
1607 center_pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
1608 center_pane.set_should_display_welcome_page(true);
1609 center_pane
1610 });
1611 cx.subscribe_in(¢er_pane, window, Self::handle_pane_event)
1612 .detach();
1613
1614 window.focus(¢er_pane.focus_handle(cx), cx);
1615
1616 cx.emit(Event::PaneAdded(center_pane.clone()));
1617
1618 let any_window_handle = window.window_handle();
1619 app_state.workspace_store.update(cx, |store, _| {
1620 store
1621 .workspaces
1622 .insert((any_window_handle, weak_handle.clone()));
1623 });
1624
1625 let mut current_user = app_state.user_store.read(cx).watch_current_user();
1626 let mut connection_status = app_state.client.status();
1627 let _observe_current_user = cx.spawn_in(window, async move |this, cx| {
1628 current_user.next().await;
1629 connection_status.next().await;
1630 let mut stream =
1631 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
1632
1633 while stream.recv().await.is_some() {
1634 this.update(cx, |_, cx| cx.notify())?;
1635 }
1636 anyhow::Ok(())
1637 });
1638
1639 // All leader updates are enqueued and then processed in a single task, so
1640 // that each asynchronous operation can be run in order.
1641 let (leader_updates_tx, mut leader_updates_rx) =
1642 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
1643 let _apply_leader_updates = cx.spawn_in(window, async move |this, cx| {
1644 while let Some((leader_id, update)) = leader_updates_rx.next().await {
1645 Self::process_leader_update(&this, leader_id, update, cx)
1646 .await
1647 .log_err();
1648 }
1649
1650 Ok(())
1651 });
1652
1653 cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
1654 let modal_layer = cx.new(|_| ModalLayer::new());
1655 let toast_layer = cx.new(|_| ToastLayer::new());
1656 cx.subscribe(
1657 &modal_layer,
1658 |_, _, _: &modal_layer::ModalOpenedEvent, cx| {
1659 cx.emit(Event::ModalOpened);
1660 },
1661 )
1662 .detach();
1663
1664 let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx);
1665 let bottom_dock = Dock::new(DockPosition::Bottom, modal_layer.clone(), window, cx);
1666 let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), window, cx);
1667 let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx));
1668 let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx));
1669 let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx));
1670 let multi_workspace = window
1671 .root::<MultiWorkspace>()
1672 .flatten()
1673 .map(|mw| mw.downgrade());
1674 let status_bar = cx.new(|cx| {
1675 let mut status_bar =
1676 StatusBar::new(¢er_pane.clone(), multi_workspace.clone(), window, cx);
1677 status_bar.add_left_item(left_dock_buttons, window, cx);
1678 status_bar.add_right_item(right_dock_buttons, window, cx);
1679 status_bar.add_right_item(bottom_dock_buttons, window, cx);
1680 status_bar
1681 });
1682
1683 let session_id = app_state.session.read(cx).id().to_owned();
1684
1685 let mut active_call = None;
1686 if let Some(call) = GlobalAnyActiveCall::try_global(cx).cloned() {
1687 let subscriptions =
1688 vec![
1689 call.0
1690 .subscribe(window, cx, Box::new(Self::on_active_call_event)),
1691 ];
1692 active_call = Some((call, subscriptions));
1693 }
1694
1695 let (serializable_items_tx, serializable_items_rx) =
1696 mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
1697 let _items_serializer = cx.spawn_in(window, async move |this, cx| {
1698 Self::serialize_items(&this, serializable_items_rx, cx).await
1699 });
1700
1701 let subscriptions = vec![
1702 cx.observe_window_activation(window, Self::on_window_activation_changed),
1703 cx.observe_window_bounds(window, move |this, window, cx| {
1704 if this.bounds_save_task_queued.is_some() {
1705 return;
1706 }
1707 this.bounds_save_task_queued = Some(cx.spawn_in(window, async move |this, cx| {
1708 cx.background_executor()
1709 .timer(Duration::from_millis(100))
1710 .await;
1711 this.update_in(cx, |this, window, cx| {
1712 this.save_window_bounds(window, cx).detach();
1713 this.bounds_save_task_queued.take();
1714 })
1715 .ok();
1716 }));
1717 cx.notify();
1718 }),
1719 cx.observe_window_appearance(window, |_, window, cx| {
1720 let window_appearance = window.appearance();
1721
1722 *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
1723
1724 theme_settings::reload_theme(cx);
1725 theme_settings::reload_icon_theme(cx);
1726 }),
1727 cx.on_release({
1728 let weak_handle = weak_handle.clone();
1729 move |this, cx| {
1730 this.app_state.workspace_store.update(cx, move |store, _| {
1731 store.workspaces.retain(|(_, weak)| weak != &weak_handle);
1732 })
1733 }
1734 }),
1735 ];
1736
1737 cx.defer_in(window, move |this, window, cx| {
1738 this.update_window_title(window, cx);
1739 this.show_initial_notifications(cx);
1740 });
1741
1742 let mut center = PaneGroup::new(center_pane.clone());
1743 center.set_is_center(true);
1744 center.mark_positions(cx);
1745
1746 Workspace {
1747 weak_self: weak_handle.clone(),
1748 zoomed: None,
1749 zoomed_position: None,
1750 previous_dock_drag_coordinates: None,
1751 center,
1752 panes: vec![center_pane.clone()],
1753 panes_by_item: Default::default(),
1754 active_pane: center_pane.clone(),
1755 last_active_center_pane: Some(center_pane.downgrade()),
1756 last_active_view_id: None,
1757 status_bar,
1758 modal_layer,
1759 toast_layer,
1760 titlebar_item: None,
1761 active_worktree_override: None,
1762 notifications: Notifications::default(),
1763 suppressed_notifications: HashSet::default(),
1764 left_dock,
1765 bottom_dock,
1766 right_dock,
1767 _panels_task: None,
1768 project: project.clone(),
1769 follower_states: Default::default(),
1770 last_leaders_by_pane: Default::default(),
1771 dispatching_keystrokes: Default::default(),
1772 window_edited: false,
1773 last_window_title: None,
1774 dirty_items: Default::default(),
1775 active_call,
1776 database_id: workspace_id,
1777 app_state,
1778 _observe_current_user,
1779 _apply_leader_updates,
1780 _schedule_serialize_workspace: None,
1781 _serialize_workspace_task: None,
1782 _schedule_serialize_ssh_paths: None,
1783 leader_updates_tx,
1784 _subscriptions: subscriptions,
1785 pane_history_timestamp,
1786 workspace_actions: Default::default(),
1787 // This data will be incorrect, but it will be overwritten by the time it needs to be used.
1788 bounds: Default::default(),
1789 centered_layout: false,
1790 bounds_save_task_queued: None,
1791 on_prompt_for_new_path: None,
1792 on_prompt_for_open_path: None,
1793 terminal_provider: None,
1794 debugger_provider: None,
1795 serializable_items_tx,
1796 _items_serializer,
1797 session_id: Some(session_id),
1798
1799 scheduled_tasks: Vec::new(),
1800 last_open_dock_positions: Vec::new(),
1801 removing: false,
1802 sidebar_focus_handle: None,
1803 multi_workspace,
1804 open_in_dev_container: false,
1805 _dev_container_task: None,
1806 }
1807 }
1808
1809 pub fn new_local(
1810 abs_paths: Vec<PathBuf>,
1811 app_state: Arc<AppState>,
1812 requesting_window: Option<WindowHandle<MultiWorkspace>>,
1813 env: Option<HashMap<String, String>>,
1814 init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
1815 open_mode: OpenMode,
1816 cx: &mut App,
1817 ) -> Task<anyhow::Result<OpenResult>> {
1818 let project_handle = Project::local(
1819 app_state.client.clone(),
1820 app_state.node_runtime.clone(),
1821 app_state.user_store.clone(),
1822 app_state.languages.clone(),
1823 app_state.fs.clone(),
1824 env,
1825 Default::default(),
1826 cx,
1827 );
1828
1829 let db = WorkspaceDb::global(cx);
1830 let kvp = db::kvp::KeyValueStore::global(cx);
1831 cx.spawn(async move |cx| {
1832 let mut paths_to_open = Vec::with_capacity(abs_paths.len());
1833 for path in abs_paths.into_iter() {
1834 if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() {
1835 paths_to_open.push(canonical)
1836 } else {
1837 paths_to_open.push(path)
1838 }
1839 }
1840
1841 let serialized_workspace = db.workspace_for_roots(paths_to_open.as_slice());
1842
1843 if let Some(paths) = serialized_workspace.as_ref().map(|ws| &ws.paths) {
1844 paths_to_open = paths.ordered_paths().cloned().collect();
1845 if !paths.is_lexicographically_ordered() {
1846 project_handle.update(cx, |project, cx| {
1847 project.set_worktrees_reordered(true, cx);
1848 });
1849 }
1850 }
1851
1852 // Get project paths for all of the abs_paths
1853 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
1854 Vec::with_capacity(paths_to_open.len());
1855
1856 for path in paths_to_open.into_iter() {
1857 if let Some((_, project_entry)) = cx
1858 .update(|cx| {
1859 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
1860 })
1861 .await
1862 .log_err()
1863 {
1864 project_paths.push((path, Some(project_entry)));
1865 } else {
1866 project_paths.push((path, None));
1867 }
1868 }
1869
1870 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
1871 serialized_workspace.id
1872 } else {
1873 db.next_id().await.unwrap_or_else(|_| Default::default())
1874 };
1875
1876 let toolchains = db.toolchains(workspace_id).await?;
1877
1878 for (toolchain, worktree_path, path) in toolchains {
1879 let toolchain_path = PathBuf::from(toolchain.path.clone().to_string());
1880 let Some(worktree_id) = project_handle.read_with(cx, |this, cx| {
1881 this.find_worktree(&worktree_path, cx)
1882 .and_then(|(worktree, rel_path)| {
1883 if rel_path.is_empty() {
1884 Some(worktree.read(cx).id())
1885 } else {
1886 None
1887 }
1888 })
1889 }) else {
1890 // We did not find a worktree with a given path, but that's whatever.
1891 continue;
1892 };
1893 if !app_state.fs.is_file(toolchain_path.as_path()).await {
1894 continue;
1895 }
1896
1897 project_handle
1898 .update(cx, |this, cx| {
1899 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
1900 })
1901 .await;
1902 }
1903 if let Some(workspace) = serialized_workspace.as_ref() {
1904 project_handle.update(cx, |this, cx| {
1905 for (scope, toolchains) in &workspace.user_toolchains {
1906 for toolchain in toolchains {
1907 this.add_toolchain(toolchain.clone(), scope.clone(), cx);
1908 }
1909 }
1910 });
1911 }
1912
1913 let window_to_replace = match open_mode {
1914 OpenMode::NewWindow => None,
1915 _ => requesting_window,
1916 };
1917
1918 let (window, workspace): (WindowHandle<MultiWorkspace>, Entity<Workspace>) =
1919 if let Some(window) = window_to_replace {
1920 let centered_layout = serialized_workspace
1921 .as_ref()
1922 .map(|w| w.centered_layout)
1923 .unwrap_or(false);
1924
1925 let workspace = window.update(cx, |multi_workspace, window, cx| {
1926 let workspace = cx.new(|cx| {
1927 let mut workspace = Workspace::new(
1928 Some(workspace_id),
1929 project_handle.clone(),
1930 app_state.clone(),
1931 window,
1932 cx,
1933 );
1934
1935 workspace.centered_layout = centered_layout;
1936
1937 // Call init callback to add items before window renders
1938 if let Some(init) = init {
1939 init(&mut workspace, window, cx);
1940 }
1941
1942 workspace
1943 });
1944 match open_mode {
1945 OpenMode::Activate => {
1946 multi_workspace.activate(workspace.clone(), window, cx);
1947 }
1948 OpenMode::Add => {
1949 multi_workspace.add(workspace.clone(), &*window, cx);
1950 }
1951 OpenMode::NewWindow => {
1952 unreachable!()
1953 }
1954 }
1955 workspace
1956 })?;
1957 (window, workspace)
1958 } else {
1959 let window_bounds_override = window_bounds_env_override();
1960
1961 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
1962 (Some(WindowBounds::Windowed(bounds)), None)
1963 } else if let Some(workspace) = serialized_workspace.as_ref()
1964 && let Some(display) = workspace.display
1965 && let Some(bounds) = workspace.window_bounds.as_ref()
1966 {
1967 // Reopening an existing workspace - restore its saved bounds
1968 (Some(bounds.0), Some(display))
1969 } else if let Some((display, bounds)) =
1970 persistence::read_default_window_bounds(&kvp)
1971 {
1972 // New or empty workspace - use the last known window bounds
1973 (Some(bounds), Some(display))
1974 } else {
1975 // New window - let GPUI's default_bounds() handle cascading
1976 (None, None)
1977 };
1978
1979 // Use the serialized workspace to construct the new window
1980 let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx));
1981 options.window_bounds = window_bounds;
1982 let centered_layout = serialized_workspace
1983 .as_ref()
1984 .map(|w| w.centered_layout)
1985 .unwrap_or(false);
1986 let window = cx.open_window(options, {
1987 let app_state = app_state.clone();
1988 let project_handle = project_handle.clone();
1989 move |window, cx| {
1990 let workspace = cx.new(|cx| {
1991 let mut workspace = Workspace::new(
1992 Some(workspace_id),
1993 project_handle,
1994 app_state,
1995 window,
1996 cx,
1997 );
1998 workspace.centered_layout = centered_layout;
1999
2000 // Call init callback to add items before window renders
2001 if let Some(init) = init {
2002 init(&mut workspace, window, cx);
2003 }
2004
2005 workspace
2006 });
2007 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
2008 }
2009 })?;
2010 let workspace =
2011 window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
2012 multi_workspace.workspace().clone()
2013 })?;
2014 (window, workspace)
2015 };
2016
2017 notify_if_database_failed(window, cx);
2018 // Check if this is an empty workspace (no paths to open)
2019 // An empty workspace is one where project_paths is empty
2020 let is_empty_workspace = project_paths.is_empty();
2021 // Check if serialized workspace has paths before it's moved
2022 let serialized_workspace_has_paths = serialized_workspace
2023 .as_ref()
2024 .map(|ws| !ws.paths.is_empty())
2025 .unwrap_or(false);
2026
2027 let opened_items = window
2028 .update(cx, |_, window, cx| {
2029 workspace.update(cx, |_workspace: &mut Workspace, cx| {
2030 open_items(serialized_workspace, project_paths, window, cx)
2031 })
2032 })?
2033 .await
2034 .unwrap_or_default();
2035
2036 // Restore default dock state for empty workspaces
2037 // Only restore if:
2038 // 1. This is an empty workspace (no paths), AND
2039 // 2. The serialized workspace either doesn't exist or has no paths
2040 if is_empty_workspace && !serialized_workspace_has_paths {
2041 if let Some(default_docks) = persistence::read_default_dock_state(&kvp) {
2042 window
2043 .update(cx, |_, window, cx| {
2044 workspace.update(cx, |workspace, cx| {
2045 for (dock, serialized_dock) in [
2046 (&workspace.right_dock, &default_docks.right),
2047 (&workspace.left_dock, &default_docks.left),
2048 (&workspace.bottom_dock, &default_docks.bottom),
2049 ] {
2050 dock.update(cx, |dock, cx| {
2051 dock.serialized_dock = Some(serialized_dock.clone());
2052 dock.restore_state(window, cx);
2053 });
2054 }
2055 cx.notify();
2056 });
2057 })
2058 .log_err();
2059 }
2060 }
2061
2062 window
2063 .update(cx, |_, _window, cx| {
2064 workspace.update(cx, |this: &mut Workspace, cx| {
2065 this.update_history(cx);
2066 });
2067 })
2068 .log_err();
2069 Ok(OpenResult {
2070 window,
2071 workspace,
2072 opened_items,
2073 })
2074 })
2075 }
2076
2077 pub fn project_group_key(&self, cx: &App) -> ProjectGroupKey {
2078 self.project.read(cx).project_group_key(cx)
2079 }
2080
2081 pub fn weak_handle(&self) -> WeakEntity<Self> {
2082 self.weak_self.clone()
2083 }
2084
2085 pub fn left_dock(&self) -> &Entity<Dock> {
2086 &self.left_dock
2087 }
2088
2089 pub fn bottom_dock(&self) -> &Entity<Dock> {
2090 &self.bottom_dock
2091 }
2092
2093 pub fn set_bottom_dock_layout(
2094 &mut self,
2095 layout: BottomDockLayout,
2096 window: &mut Window,
2097 cx: &mut Context<Self>,
2098 ) {
2099 let fs = self.project().read(cx).fs();
2100 settings::update_settings_file(fs.clone(), cx, move |content, _cx| {
2101 content.workspace.bottom_dock_layout = Some(layout);
2102 });
2103
2104 cx.notify();
2105 self.serialize_workspace(window, cx);
2106 }
2107
2108 pub fn right_dock(&self) -> &Entity<Dock> {
2109 &self.right_dock
2110 }
2111
2112 pub fn all_docks(&self) -> [&Entity<Dock>; 3] {
2113 [&self.left_dock, &self.bottom_dock, &self.right_dock]
2114 }
2115
2116 pub fn capture_dock_state(&self, _window: &Window, cx: &App) -> DockStructure {
2117 let left_dock = self.left_dock.read(cx);
2118 let left_visible = left_dock.is_open();
2119 let left_active_panel = left_dock
2120 .active_panel()
2121 .map(|panel| panel.persistent_name().to_string());
2122 // `zoomed_position` is kept in sync with individual panel zoom state
2123 // by the dock code in `Dock::new` and `Dock::add_panel`.
2124 let left_dock_zoom = self.zoomed_position == Some(DockPosition::Left);
2125
2126 let right_dock = self.right_dock.read(cx);
2127 let right_visible = right_dock.is_open();
2128 let right_active_panel = right_dock
2129 .active_panel()
2130 .map(|panel| panel.persistent_name().to_string());
2131 let right_dock_zoom = self.zoomed_position == Some(DockPosition::Right);
2132
2133 let bottom_dock = self.bottom_dock.read(cx);
2134 let bottom_visible = bottom_dock.is_open();
2135 let bottom_active_panel = bottom_dock
2136 .active_panel()
2137 .map(|panel| panel.persistent_name().to_string());
2138 let bottom_dock_zoom = self.zoomed_position == Some(DockPosition::Bottom);
2139
2140 DockStructure {
2141 left: DockData {
2142 visible: left_visible,
2143 active_panel: left_active_panel,
2144 zoom: left_dock_zoom,
2145 },
2146 right: DockData {
2147 visible: right_visible,
2148 active_panel: right_active_panel,
2149 zoom: right_dock_zoom,
2150 },
2151 bottom: DockData {
2152 visible: bottom_visible,
2153 active_panel: bottom_active_panel,
2154 zoom: bottom_dock_zoom,
2155 },
2156 }
2157 }
2158
2159 pub fn set_dock_structure(
2160 &self,
2161 docks: DockStructure,
2162 window: &mut Window,
2163 cx: &mut Context<Self>,
2164 ) {
2165 for (dock, data) in [
2166 (&self.left_dock, docks.left),
2167 (&self.bottom_dock, docks.bottom),
2168 (&self.right_dock, docks.right),
2169 ] {
2170 dock.update(cx, |dock, cx| {
2171 dock.serialized_dock = Some(data);
2172 dock.restore_state(window, cx);
2173 });
2174 }
2175 }
2176
2177 pub fn open_item_abs_paths(&self, cx: &App) -> Vec<PathBuf> {
2178 self.items(cx)
2179 .filter_map(|item| {
2180 let project_path = item.project_path(cx)?;
2181 self.project.read(cx).absolute_path(&project_path, cx)
2182 })
2183 .collect()
2184 }
2185
2186 pub fn dock_at_position(&self, position: DockPosition) -> &Entity<Dock> {
2187 match position {
2188 DockPosition::Left => &self.left_dock,
2189 DockPosition::Bottom => &self.bottom_dock,
2190 DockPosition::Right => &self.right_dock,
2191 }
2192 }
2193
2194 pub fn agent_panel_position(&self, cx: &App) -> Option<DockPosition> {
2195 self.all_docks().into_iter().find_map(|dock| {
2196 let dock = dock.read(cx);
2197 dock.has_agent_panel(cx).then_some(dock.position())
2198 })
2199 }
2200
2201 pub fn panel_size_state<T: Panel>(&self, cx: &App) -> Option<dock::PanelSizeState> {
2202 self.all_docks().into_iter().find_map(|dock| {
2203 let dock = dock.read(cx);
2204 let panel = dock.panel::<T>()?;
2205 dock.stored_panel_size_state(&panel)
2206 })
2207 }
2208
2209 pub fn persisted_panel_size_state(
2210 &self,
2211 panel_key: &'static str,
2212 cx: &App,
2213 ) -> Option<dock::PanelSizeState> {
2214 dock::Dock::load_persisted_size_state(self, panel_key, cx)
2215 }
2216
2217 pub fn persist_panel_size_state(
2218 &self,
2219 panel_key: &str,
2220 size_state: dock::PanelSizeState,
2221 cx: &mut App,
2222 ) {
2223 let Some(workspace_id) = self
2224 .database_id()
2225 .map(|id| i64::from(id).to_string())
2226 .or(self.session_id())
2227 else {
2228 return;
2229 };
2230
2231 let kvp = db::kvp::KeyValueStore::global(cx);
2232 let panel_key = panel_key.to_string();
2233 cx.background_spawn(async move {
2234 let scope = kvp.scoped(dock::PANEL_SIZE_STATE_KEY);
2235 scope
2236 .write(
2237 format!("{workspace_id}:{panel_key}"),
2238 serde_json::to_string(&size_state)?,
2239 )
2240 .await
2241 })
2242 .detach_and_log_err(cx);
2243 }
2244
2245 pub fn set_panel_size_state<T: Panel>(
2246 &mut self,
2247 size_state: dock::PanelSizeState,
2248 window: &mut Window,
2249 cx: &mut Context<Self>,
2250 ) -> bool {
2251 let Some(panel) = self.panel::<T>(cx) else {
2252 return false;
2253 };
2254
2255 let dock = self.dock_at_position(panel.position(window, cx));
2256 let did_set = dock.update(cx, |dock, cx| {
2257 dock.set_panel_size_state(&panel, size_state, cx)
2258 });
2259
2260 if did_set {
2261 self.persist_panel_size_state(T::panel_key(), size_state, cx);
2262 }
2263
2264 did_set
2265 }
2266
2267 pub fn toggle_dock_panel_flexible_size(
2268 &self,
2269 dock: &Entity<Dock>,
2270 panel: &dyn PanelHandle,
2271 window: &mut Window,
2272 cx: &mut App,
2273 ) {
2274 let position = dock.read(cx).position();
2275 let current_size = self.dock_size(&dock.read(cx), window, cx);
2276 let current_flex =
2277 current_size.and_then(|size| self.dock_flex_for_size(position, size, window, cx));
2278 dock.update(cx, |dock, cx| {
2279 dock.toggle_panel_flexible_size(panel, current_size, current_flex, window, cx);
2280 });
2281 }
2282
2283 fn dock_size(&self, dock: &Dock, window: &Window, cx: &App) -> Option<Pixels> {
2284 let panel = dock.active_panel()?;
2285 let size_state = dock
2286 .stored_panel_size_state(panel.as_ref())
2287 .unwrap_or_default();
2288 let position = dock.position();
2289
2290 let use_flex = panel.has_flexible_size(window, cx);
2291
2292 if position.axis() == Axis::Horizontal
2293 && use_flex
2294 && let Some(flex) = size_state.flex.or_else(|| self.default_dock_flex(position))
2295 {
2296 let workspace_width = self.bounds.size.width;
2297 if workspace_width <= Pixels::ZERO {
2298 return None;
2299 }
2300 let flex = flex.max(0.001);
2301 let opposite = self.opposite_dock_panel_and_size_state(position, window, cx);
2302 if let Some(opposite_flex) = opposite.as_ref().and_then(|(_, s)| s.flex) {
2303 // Both docks are flex items sharing the full workspace width.
2304 let total_flex = flex + 1.0 + opposite_flex;
2305 return Some((flex / total_flex * workspace_width).max(RESIZE_HANDLE_SIZE));
2306 } else {
2307 // Opposite dock is fixed-width; flex items share (W - fixed).
2308 let opposite_fixed = opposite
2309 .map(|(panel, s)| s.size.unwrap_or_else(|| panel.default_size(window, cx)))
2310 .unwrap_or_default();
2311 let available = (workspace_width - opposite_fixed).max(RESIZE_HANDLE_SIZE);
2312 return Some((flex / (flex + 1.0) * available).max(RESIZE_HANDLE_SIZE));
2313 }
2314 }
2315
2316 Some(
2317 size_state
2318 .size
2319 .unwrap_or_else(|| panel.default_size(window, cx)),
2320 )
2321 }
2322
2323 pub fn dock_flex_for_size(
2324 &self,
2325 position: DockPosition,
2326 size: Pixels,
2327 window: &Window,
2328 cx: &App,
2329 ) -> Option<f32> {
2330 if position.axis() != Axis::Horizontal {
2331 return None;
2332 }
2333
2334 let workspace_width = self.bounds.size.width;
2335 if workspace_width <= Pixels::ZERO {
2336 return None;
2337 }
2338
2339 let opposite = self.opposite_dock_panel_and_size_state(position, window, cx);
2340 if let Some(opposite_flex) = opposite.as_ref().and_then(|(_, s)| s.flex) {
2341 let size = size.clamp(px(0.), workspace_width - px(1.));
2342 Some((size * (1.0 + opposite_flex) / (workspace_width - size)).max(0.0))
2343 } else {
2344 let opposite_width = opposite
2345 .map(|(panel, s)| s.size.unwrap_or_else(|| panel.default_size(window, cx)))
2346 .unwrap_or_default();
2347 let available = (workspace_width - opposite_width).max(RESIZE_HANDLE_SIZE);
2348 let remaining = (available - size).max(px(1.));
2349 Some((size / remaining).max(0.0))
2350 }
2351 }
2352
2353 fn opposite_dock_panel_and_size_state(
2354 &self,
2355 position: DockPosition,
2356 window: &Window,
2357 cx: &App,
2358 ) -> Option<(Arc<dyn PanelHandle>, PanelSizeState)> {
2359 let opposite_position = match position {
2360 DockPosition::Left => DockPosition::Right,
2361 DockPosition::Right => DockPosition::Left,
2362 DockPosition::Bottom => return None,
2363 };
2364
2365 let opposite_dock = self.dock_at_position(opposite_position).read(cx);
2366 let panel = opposite_dock.visible_panel()?;
2367 let mut size_state = opposite_dock
2368 .stored_panel_size_state(panel.as_ref())
2369 .unwrap_or_default();
2370 if size_state.flex.is_none() && panel.has_flexible_size(window, cx) {
2371 size_state.flex = self.default_dock_flex(opposite_position);
2372 }
2373 Some((panel.clone(), size_state))
2374 }
2375
2376 pub fn default_dock_flex(&self, position: DockPosition) -> Option<f32> {
2377 if position.axis() != Axis::Horizontal {
2378 return None;
2379 }
2380
2381 let pane = self.last_active_center_pane.clone()?.upgrade()?;
2382 Some(self.center.width_fraction_for_pane(&pane).unwrap_or(1.0))
2383 }
2384
2385 pub fn is_edited(&self) -> bool {
2386 self.window_edited
2387 }
2388
2389 pub fn add_panel<T: Panel>(
2390 &mut self,
2391 panel: Entity<T>,
2392 window: &mut Window,
2393 cx: &mut Context<Self>,
2394 ) {
2395 let focus_handle = panel.panel_focus_handle(cx);
2396 cx.on_focus_in(&focus_handle, window, Self::handle_panel_focused)
2397 .detach();
2398
2399 let dock_position = panel.position(window, cx);
2400 let dock = self.dock_at_position(dock_position);
2401 let any_panel = panel.to_any();
2402 let persisted_size_state =
2403 self.persisted_panel_size_state(T::panel_key(), cx)
2404 .or_else(|| {
2405 load_legacy_panel_size(T::panel_key(), dock_position, self, cx).map(|size| {
2406 let state = dock::PanelSizeState {
2407 size: Some(size),
2408 flex: None,
2409 };
2410 self.persist_panel_size_state(T::panel_key(), state, cx);
2411 state
2412 })
2413 });
2414
2415 dock.update(cx, |dock, cx| {
2416 let index = dock.add_panel(panel.clone(), self.weak_self.clone(), window, cx);
2417 if let Some(size_state) = persisted_size_state {
2418 dock.set_panel_size_state(&panel, size_state, cx);
2419 }
2420 index
2421 });
2422
2423 cx.emit(Event::PanelAdded(any_panel));
2424 }
2425
2426 pub fn remove_panel<T: Panel>(
2427 &mut self,
2428 panel: &Entity<T>,
2429 window: &mut Window,
2430 cx: &mut Context<Self>,
2431 ) {
2432 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2433 dock.update(cx, |dock, cx| dock.remove_panel(panel, window, cx));
2434 }
2435 }
2436
2437 pub fn status_bar(&self) -> &Entity<StatusBar> {
2438 &self.status_bar
2439 }
2440
2441 pub fn set_sidebar_focus_handle(&mut self, handle: Option<FocusHandle>) {
2442 self.sidebar_focus_handle = handle;
2443 }
2444
2445 pub fn status_bar_visible(&self, cx: &App) -> bool {
2446 StatusBarSettings::get_global(cx).show
2447 }
2448
2449 pub fn multi_workspace(&self) -> Option<&WeakEntity<MultiWorkspace>> {
2450 self.multi_workspace.as_ref()
2451 }
2452
2453 pub fn set_multi_workspace(
2454 &mut self,
2455 multi_workspace: WeakEntity<MultiWorkspace>,
2456 cx: &mut App,
2457 ) {
2458 self.status_bar.update(cx, |status_bar, cx| {
2459 status_bar.set_multi_workspace(multi_workspace.clone(), cx);
2460 });
2461 self.multi_workspace = Some(multi_workspace);
2462 }
2463
2464 pub fn app_state(&self) -> &Arc<AppState> {
2465 &self.app_state
2466 }
2467
2468 pub fn set_panels_task(&mut self, task: Task<Result<()>>) {
2469 self._panels_task = Some(task);
2470 }
2471
2472 pub fn take_panels_task(&mut self) -> Option<Task<Result<()>>> {
2473 self._panels_task.take()
2474 }
2475
2476 pub fn user_store(&self) -> &Entity<UserStore> {
2477 &self.app_state.user_store
2478 }
2479
2480 pub fn project(&self) -> &Entity<Project> {
2481 &self.project
2482 }
2483
2484 pub fn path_style(&self, cx: &App) -> PathStyle {
2485 self.project.read(cx).path_style(cx)
2486 }
2487
2488 pub fn recently_activated_items(&self, cx: &App) -> HashMap<EntityId, usize> {
2489 let mut history: HashMap<EntityId, usize> = HashMap::default();
2490
2491 for pane_handle in &self.panes {
2492 let pane = pane_handle.read(cx);
2493
2494 for entry in pane.activation_history() {
2495 history.insert(
2496 entry.entity_id,
2497 history
2498 .get(&entry.entity_id)
2499 .cloned()
2500 .unwrap_or(0)
2501 .max(entry.timestamp),
2502 );
2503 }
2504 }
2505
2506 history
2507 }
2508
2509 pub fn recent_active_item_by_type<T: 'static>(&self, cx: &App) -> Option<Entity<T>> {
2510 let mut recent_item: Option<Entity<T>> = None;
2511 let mut recent_timestamp = 0;
2512 for pane_handle in &self.panes {
2513 let pane = pane_handle.read(cx);
2514 let item_map: HashMap<EntityId, &Box<dyn ItemHandle>> =
2515 pane.items().map(|item| (item.item_id(), item)).collect();
2516 for entry in pane.activation_history() {
2517 if entry.timestamp > recent_timestamp
2518 && let Some(&item) = item_map.get(&entry.entity_id)
2519 && let Some(typed_item) = item.act_as::<T>(cx)
2520 {
2521 recent_timestamp = entry.timestamp;
2522 recent_item = Some(typed_item);
2523 }
2524 }
2525 }
2526 recent_item
2527 }
2528
2529 pub fn recent_navigation_history_iter(
2530 &self,
2531 cx: &App,
2532 ) -> impl Iterator<Item = (ProjectPath, Option<PathBuf>)> + use<> {
2533 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
2534 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
2535
2536 for pane in &self.panes {
2537 let pane = pane.read(cx);
2538
2539 pane.nav_history()
2540 .for_each_entry(cx, &mut |entry, (project_path, fs_path)| {
2541 if let Some(fs_path) = &fs_path {
2542 abs_paths_opened
2543 .entry(fs_path.clone())
2544 .or_default()
2545 .insert(project_path.clone());
2546 }
2547 let timestamp = entry.timestamp;
2548 match history.entry(project_path) {
2549 hash_map::Entry::Occupied(mut entry) => {
2550 let (_, old_timestamp) = entry.get();
2551 if ×tamp > old_timestamp {
2552 entry.insert((fs_path, timestamp));
2553 }
2554 }
2555 hash_map::Entry::Vacant(entry) => {
2556 entry.insert((fs_path, timestamp));
2557 }
2558 }
2559 });
2560
2561 if let Some(item) = pane.active_item()
2562 && let Some(project_path) = item.project_path(cx)
2563 {
2564 let fs_path = self.project.read(cx).absolute_path(&project_path, cx);
2565
2566 if let Some(fs_path) = &fs_path {
2567 abs_paths_opened
2568 .entry(fs_path.clone())
2569 .or_default()
2570 .insert(project_path.clone());
2571 }
2572
2573 history.insert(project_path, (fs_path, std::usize::MAX));
2574 }
2575 }
2576
2577 history
2578 .into_iter()
2579 .sorted_by_key(|(_, (_, order))| *order)
2580 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
2581 .rev()
2582 .filter(move |(history_path, abs_path)| {
2583 let latest_project_path_opened = abs_path
2584 .as_ref()
2585 .and_then(|abs_path| abs_paths_opened.get(abs_path))
2586 .and_then(|project_paths| {
2587 project_paths
2588 .iter()
2589 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
2590 });
2591
2592 latest_project_path_opened.is_none_or(|path| path == history_path)
2593 })
2594 }
2595
2596 pub fn recent_navigation_history(
2597 &self,
2598 limit: Option<usize>,
2599 cx: &App,
2600 ) -> Vec<(ProjectPath, Option<PathBuf>)> {
2601 self.recent_navigation_history_iter(cx)
2602 .take(limit.unwrap_or(usize::MAX))
2603 .collect()
2604 }
2605
2606 pub fn clear_navigation_history(&mut self, _window: &mut Window, cx: &mut Context<Workspace>) {
2607 for pane in &self.panes {
2608 pane.update(cx, |pane, cx| pane.nav_history_mut().clear(cx));
2609 }
2610 }
2611
2612 fn navigate_history(
2613 &mut self,
2614 pane: WeakEntity<Pane>,
2615 mode: NavigationMode,
2616 window: &mut Window,
2617 cx: &mut Context<Workspace>,
2618 ) -> Task<Result<()>> {
2619 self.navigate_history_impl(
2620 pane,
2621 mode,
2622 window,
2623 &mut |history, cx| history.pop(mode, cx),
2624 cx,
2625 )
2626 }
2627
2628 fn navigate_tag_history(
2629 &mut self,
2630 pane: WeakEntity<Pane>,
2631 mode: TagNavigationMode,
2632 window: &mut Window,
2633 cx: &mut Context<Workspace>,
2634 ) -> Task<Result<()>> {
2635 self.navigate_history_impl(
2636 pane,
2637 NavigationMode::Normal,
2638 window,
2639 &mut |history, _cx| history.pop_tag(mode),
2640 cx,
2641 )
2642 }
2643
2644 fn navigate_history_impl(
2645 &mut self,
2646 pane: WeakEntity<Pane>,
2647 mode: NavigationMode,
2648 window: &mut Window,
2649 cb: &mut dyn FnMut(&mut NavHistory, &mut App) -> Option<NavigationEntry>,
2650 cx: &mut Context<Workspace>,
2651 ) -> Task<Result<()>> {
2652 let to_load = if let Some(pane) = pane.upgrade() {
2653 pane.update(cx, |pane, cx| {
2654 window.focus(&pane.focus_handle(cx), cx);
2655 loop {
2656 // Retrieve the weak item handle from the history.
2657 let entry = cb(pane.nav_history_mut(), cx)?;
2658
2659 // If the item is still present in this pane, then activate it.
2660 if let Some(index) = entry
2661 .item
2662 .upgrade()
2663 .and_then(|v| pane.index_for_item(v.as_ref()))
2664 {
2665 let prev_active_item_index = pane.active_item_index();
2666 pane.nav_history_mut().set_mode(mode);
2667 pane.activate_item(index, true, true, window, cx);
2668 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2669
2670 let mut navigated = prev_active_item_index != pane.active_item_index();
2671 if let Some(data) = entry.data {
2672 navigated |= pane.active_item()?.navigate(data, window, cx);
2673 }
2674
2675 if navigated {
2676 break None;
2677 }
2678 } else {
2679 // If the item is no longer present in this pane, then retrieve its
2680 // path info in order to reopen it.
2681 break pane
2682 .nav_history()
2683 .path_for_item(entry.item.id())
2684 .map(|(project_path, abs_path)| (project_path, abs_path, entry));
2685 }
2686 }
2687 })
2688 } else {
2689 None
2690 };
2691
2692 if let Some((project_path, abs_path, entry)) = to_load {
2693 // If the item was no longer present, then load it again from its previous path, first try the local path
2694 let open_by_project_path = self.load_path(project_path.clone(), window, cx);
2695
2696 cx.spawn_in(window, async move |workspace, cx| {
2697 let open_by_project_path = open_by_project_path.await;
2698 let mut navigated = false;
2699 match open_by_project_path
2700 .with_context(|| format!("Navigating to {project_path:?}"))
2701 {
2702 Ok((project_entry_id, build_item)) => {
2703 let prev_active_item_id = pane.update(cx, |pane, _| {
2704 pane.nav_history_mut().set_mode(mode);
2705 pane.active_item().map(|p| p.item_id())
2706 })?;
2707
2708 pane.update_in(cx, |pane, window, cx| {
2709 let item = pane.open_item(
2710 project_entry_id,
2711 project_path,
2712 true,
2713 entry.is_preview,
2714 true,
2715 None,
2716 window, cx,
2717 build_item,
2718 );
2719 navigated |= Some(item.item_id()) != prev_active_item_id;
2720 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2721 if let Some(data) = entry.data {
2722 navigated |= item.navigate(data, window, cx);
2723 }
2724 })?;
2725 }
2726 Err(open_by_project_path_e) => {
2727 // Fall back to opening by abs path, in case an external file was opened and closed,
2728 // and its worktree is now dropped
2729 if let Some(abs_path) = abs_path {
2730 let prev_active_item_id = pane.update(cx, |pane, _| {
2731 pane.nav_history_mut().set_mode(mode);
2732 pane.active_item().map(|p| p.item_id())
2733 })?;
2734 let open_by_abs_path = workspace.update_in(cx, |workspace, window, cx| {
2735 workspace.open_abs_path(abs_path.clone(), OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
2736 })?;
2737 match open_by_abs_path
2738 .await
2739 .with_context(|| format!("Navigating to {abs_path:?}"))
2740 {
2741 Ok(item) => {
2742 pane.update_in(cx, |pane, window, cx| {
2743 navigated |= Some(item.item_id()) != prev_active_item_id;
2744 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2745 if let Some(data) = entry.data {
2746 navigated |= item.navigate(data, window, cx);
2747 }
2748 })?;
2749 }
2750 Err(open_by_abs_path_e) => {
2751 log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
2752 }
2753 }
2754 }
2755 }
2756 }
2757
2758 if !navigated {
2759 workspace
2760 .update_in(cx, |workspace, window, cx| {
2761 Self::navigate_history(workspace, pane, mode, window, cx)
2762 })?
2763 .await?;
2764 }
2765
2766 Ok(())
2767 })
2768 } else {
2769 Task::ready(Ok(()))
2770 }
2771 }
2772
2773 pub fn go_back(
2774 &mut self,
2775 pane: WeakEntity<Pane>,
2776 window: &mut Window,
2777 cx: &mut Context<Workspace>,
2778 ) -> Task<Result<()>> {
2779 self.navigate_history(pane, NavigationMode::GoingBack, window, cx)
2780 }
2781
2782 pub fn go_forward(
2783 &mut self,
2784 pane: WeakEntity<Pane>,
2785 window: &mut Window,
2786 cx: &mut Context<Workspace>,
2787 ) -> Task<Result<()>> {
2788 self.navigate_history(pane, NavigationMode::GoingForward, window, cx)
2789 }
2790
2791 pub fn reopen_closed_item(
2792 &mut self,
2793 window: &mut Window,
2794 cx: &mut Context<Workspace>,
2795 ) -> Task<Result<()>> {
2796 self.navigate_history(
2797 self.active_pane().downgrade(),
2798 NavigationMode::ReopeningClosedItem,
2799 window,
2800 cx,
2801 )
2802 }
2803
2804 pub fn client(&self) -> &Arc<Client> {
2805 &self.app_state.client
2806 }
2807
2808 pub fn set_titlebar_item(&mut self, item: AnyView, _: &mut Window, cx: &mut Context<Self>) {
2809 self.titlebar_item = Some(item);
2810 cx.notify();
2811 }
2812
2813 pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
2814 self.on_prompt_for_new_path = Some(prompt)
2815 }
2816
2817 pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) {
2818 self.on_prompt_for_open_path = Some(prompt)
2819 }
2820
2821 pub fn set_terminal_provider(&mut self, provider: impl TerminalProvider + 'static) {
2822 self.terminal_provider = Some(Box::new(provider));
2823 }
2824
2825 pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) {
2826 self.debugger_provider = Some(Arc::new(provider));
2827 }
2828
2829 pub fn set_open_in_dev_container(&mut self, value: bool) {
2830 self.open_in_dev_container = value;
2831 }
2832
2833 pub fn open_in_dev_container(&self) -> bool {
2834 self.open_in_dev_container
2835 }
2836
2837 pub fn set_dev_container_task(&mut self, task: Task<Result<()>>) {
2838 self._dev_container_task = Some(task);
2839 }
2840
2841 pub fn debugger_provider(&self) -> Option<Arc<dyn DebuggerProvider>> {
2842 self.debugger_provider.clone()
2843 }
2844
2845 pub fn prompt_for_open_path(
2846 &mut self,
2847 path_prompt_options: PathPromptOptions,
2848 lister: DirectoryLister,
2849 window: &mut Window,
2850 cx: &mut Context<Self>,
2851 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
2852 if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
2853 let prompt = self.on_prompt_for_open_path.take().unwrap();
2854 let rx = prompt(self, lister, window, cx);
2855 self.on_prompt_for_open_path = Some(prompt);
2856 rx
2857 } else {
2858 let (tx, rx) = oneshot::channel();
2859 let abs_path = cx.prompt_for_paths(path_prompt_options);
2860
2861 cx.spawn_in(window, async move |workspace, cx| {
2862 let Ok(result) = abs_path.await else {
2863 return Ok(());
2864 };
2865
2866 match result {
2867 Ok(result) => {
2868 tx.send(result).ok();
2869 }
2870 Err(err) => {
2871 let rx = workspace.update_in(cx, |workspace, window, cx| {
2872 workspace.show_portal_error(err.to_string(), cx);
2873 let prompt = workspace.on_prompt_for_open_path.take().unwrap();
2874 let rx = prompt(workspace, lister, window, cx);
2875 workspace.on_prompt_for_open_path = Some(prompt);
2876 rx
2877 })?;
2878 if let Ok(path) = rx.await {
2879 tx.send(path).ok();
2880 }
2881 }
2882 };
2883 anyhow::Ok(())
2884 })
2885 .detach();
2886
2887 rx
2888 }
2889 }
2890
2891 pub fn prompt_for_new_path(
2892 &mut self,
2893 lister: DirectoryLister,
2894 suggested_name: Option<String>,
2895 window: &mut Window,
2896 cx: &mut Context<Self>,
2897 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
2898 if self.project.read(cx).is_via_collab()
2899 || self.project.read(cx).is_via_remote_server()
2900 || !WorkspaceSettings::get_global(cx).use_system_path_prompts
2901 {
2902 let prompt = self.on_prompt_for_new_path.take().unwrap();
2903 let rx = prompt(self, lister, suggested_name, window, cx);
2904 self.on_prompt_for_new_path = Some(prompt);
2905 return rx;
2906 }
2907
2908 let (tx, rx) = oneshot::channel();
2909 cx.spawn_in(window, async move |workspace, cx| {
2910 let abs_path = workspace.update(cx, |workspace, cx| {
2911 let relative_to = workspace
2912 .most_recent_active_path(cx)
2913 .and_then(|p| p.parent().map(|p| p.to_path_buf()))
2914 .or_else(|| {
2915 let project = workspace.project.read(cx);
2916 project.visible_worktrees(cx).find_map(|worktree| {
2917 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
2918 })
2919 })
2920 .or_else(std::env::home_dir)
2921 .unwrap_or_else(|| PathBuf::from(""));
2922 cx.prompt_for_new_path(&relative_to, suggested_name.as_deref())
2923 })?;
2924 let abs_path = match abs_path.await? {
2925 Ok(path) => path,
2926 Err(err) => {
2927 let rx = workspace.update_in(cx, |workspace, window, cx| {
2928 workspace.show_portal_error(err.to_string(), cx);
2929
2930 let prompt = workspace.on_prompt_for_new_path.take().unwrap();
2931 let rx = prompt(workspace, lister, suggested_name, window, cx);
2932 workspace.on_prompt_for_new_path = Some(prompt);
2933 rx
2934 })?;
2935 if let Ok(path) = rx.await {
2936 tx.send(path).ok();
2937 }
2938 return anyhow::Ok(());
2939 }
2940 };
2941
2942 tx.send(abs_path.map(|path| vec![path])).ok();
2943 anyhow::Ok(())
2944 })
2945 .detach();
2946
2947 rx
2948 }
2949
2950 pub fn titlebar_item(&self) -> Option<AnyView> {
2951 self.titlebar_item.clone()
2952 }
2953
2954 /// Returns the worktree override set by the user (e.g., via the project dropdown).
2955 /// When set, git-related operations should use this worktree instead of deriving
2956 /// the active worktree from the focused file.
2957 pub fn active_worktree_override(&self) -> Option<WorktreeId> {
2958 self.active_worktree_override
2959 }
2960
2961 pub fn set_active_worktree_override(
2962 &mut self,
2963 worktree_id: Option<WorktreeId>,
2964 cx: &mut Context<Self>,
2965 ) {
2966 self.active_worktree_override = worktree_id;
2967 cx.notify();
2968 }
2969
2970 pub fn clear_active_worktree_override(&mut self, cx: &mut Context<Self>) {
2971 self.active_worktree_override = None;
2972 cx.notify();
2973 }
2974
2975 /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
2976 ///
2977 /// If the given workspace has a local project, then it will be passed
2978 /// to the callback. Otherwise, a new empty window will be created.
2979 pub fn with_local_workspace<T, F>(
2980 &mut self,
2981 window: &mut Window,
2982 cx: &mut Context<Self>,
2983 callback: F,
2984 ) -> Task<Result<T>>
2985 where
2986 T: 'static,
2987 F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
2988 {
2989 if self.project.read(cx).is_local() {
2990 Task::ready(Ok(callback(self, window, cx)))
2991 } else {
2992 let env = self.project.read(cx).cli_environment(cx);
2993 let task = Self::new_local(
2994 Vec::new(),
2995 self.app_state.clone(),
2996 None,
2997 env,
2998 None,
2999 OpenMode::Activate,
3000 cx,
3001 );
3002 cx.spawn_in(window, async move |_vh, cx| {
3003 let OpenResult {
3004 window: multi_workspace_window,
3005 ..
3006 } = task.await?;
3007 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
3008 let workspace = multi_workspace.workspace().clone();
3009 workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
3010 })
3011 })
3012 }
3013 }
3014
3015 /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
3016 ///
3017 /// If the given workspace has a local project, then it will be passed
3018 /// to the callback. Otherwise, a new empty window will be created.
3019 pub fn with_local_or_wsl_workspace<T, F>(
3020 &mut self,
3021 window: &mut Window,
3022 cx: &mut Context<Self>,
3023 callback: F,
3024 ) -> Task<Result<T>>
3025 where
3026 T: 'static,
3027 F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
3028 {
3029 let project = self.project.read(cx);
3030 if project.is_local() || project.is_via_wsl_with_host_interop(cx) {
3031 Task::ready(Ok(callback(self, window, cx)))
3032 } else {
3033 let env = self.project.read(cx).cli_environment(cx);
3034 let task = Self::new_local(
3035 Vec::new(),
3036 self.app_state.clone(),
3037 None,
3038 env,
3039 None,
3040 OpenMode::Activate,
3041 cx,
3042 );
3043 cx.spawn_in(window, async move |_vh, cx| {
3044 let OpenResult {
3045 window: multi_workspace_window,
3046 ..
3047 } = task.await?;
3048 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
3049 let workspace = multi_workspace.workspace().clone();
3050 workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
3051 })
3052 })
3053 }
3054 }
3055
3056 pub fn worktrees<'a>(&self, cx: &'a App) -> impl 'a + Iterator<Item = Entity<Worktree>> {
3057 self.project.read(cx).worktrees(cx)
3058 }
3059
3060 pub fn visible_worktrees<'a>(
3061 &self,
3062 cx: &'a App,
3063 ) -> impl 'a + Iterator<Item = Entity<Worktree>> {
3064 self.project.read(cx).visible_worktrees(cx)
3065 }
3066
3067 pub fn worktree_scans_complete(&self, cx: &App) -> impl Future<Output = ()> + 'static + use<> {
3068 let futures = self
3069 .worktrees(cx)
3070 .filter_map(|worktree| worktree.read(cx).as_local())
3071 .map(|worktree| worktree.scan_complete())
3072 .collect::<Vec<_>>();
3073 async move {
3074 for future in futures {
3075 future.await;
3076 }
3077 }
3078 }
3079
3080 pub fn close_global(cx: &mut App) {
3081 cx.defer(|cx| {
3082 cx.windows().iter().find(|window| {
3083 window
3084 .update(cx, |_, window, _| {
3085 if window.is_window_active() {
3086 //This can only get called when the window's project connection has been lost
3087 //so we don't need to prompt the user for anything and instead just close the window
3088 window.remove_window();
3089 true
3090 } else {
3091 false
3092 }
3093 })
3094 .unwrap_or(false)
3095 });
3096 });
3097 }
3098
3099 pub fn move_focused_panel_to_next_position(
3100 &mut self,
3101 _: &MoveFocusedPanelToNextPosition,
3102 window: &mut Window,
3103 cx: &mut Context<Self>,
3104 ) {
3105 let docks = self.all_docks();
3106 let active_dock = docks
3107 .into_iter()
3108 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
3109
3110 if let Some(dock) = active_dock {
3111 dock.update(cx, |dock, cx| {
3112 let active_panel = dock
3113 .active_panel()
3114 .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx));
3115
3116 if let Some(panel) = active_panel {
3117 panel.move_to_next_position(window, cx);
3118 }
3119 })
3120 }
3121 }
3122
3123 pub fn prepare_to_close(
3124 &mut self,
3125 close_intent: CloseIntent,
3126 window: &mut Window,
3127 cx: &mut Context<Self>,
3128 ) -> Task<Result<bool>> {
3129 let active_call = self.active_global_call();
3130
3131 cx.spawn_in(window, async move |this, cx| {
3132 this.update(cx, |this, _| {
3133 if close_intent == CloseIntent::CloseWindow {
3134 this.removing = true;
3135 }
3136 })?;
3137
3138 let workspace_count = cx.update(|_window, cx| {
3139 cx.windows()
3140 .iter()
3141 .filter(|window| window.downcast::<MultiWorkspace>().is_some())
3142 .count()
3143 })?;
3144
3145 #[cfg(target_os = "macos")]
3146 let save_last_workspace = false;
3147
3148 // On Linux and Windows, closing the last window should restore the last workspace.
3149 #[cfg(not(target_os = "macos"))]
3150 let save_last_workspace = {
3151 let remaining_workspaces = cx.update(|_window, cx| {
3152 cx.windows()
3153 .iter()
3154 .filter_map(|window| window.downcast::<MultiWorkspace>())
3155 .filter_map(|multi_workspace| {
3156 multi_workspace
3157 .update(cx, |multi_workspace, _, cx| {
3158 multi_workspace.workspace().read(cx).removing
3159 })
3160 .ok()
3161 })
3162 .filter(|removing| !removing)
3163 .count()
3164 })?;
3165
3166 close_intent != CloseIntent::ReplaceWindow && remaining_workspaces == 0
3167 };
3168
3169 if let Some(active_call) = active_call
3170 && workspace_count == 1
3171 && cx
3172 .update(|_window, cx| active_call.0.is_in_room(cx))
3173 .unwrap_or(false)
3174 {
3175 if close_intent == CloseIntent::CloseWindow {
3176 this.update(cx, |_, cx| cx.emit(Event::Activate))?;
3177 let answer = cx.update(|window, cx| {
3178 window.prompt(
3179 PromptLevel::Warning,
3180 "Do you want to leave the current call?",
3181 None,
3182 &["Close window and hang up", "Cancel"],
3183 cx,
3184 )
3185 })?;
3186
3187 if answer.await.log_err() == Some(1) {
3188 return anyhow::Ok(false);
3189 } else {
3190 if let Ok(task) = cx.update(|_window, cx| active_call.0.hang_up(cx)) {
3191 task.await.log_err();
3192 }
3193 }
3194 }
3195 if close_intent == CloseIntent::ReplaceWindow {
3196 _ = cx.update(|_window, cx| {
3197 let multi_workspace = cx
3198 .windows()
3199 .iter()
3200 .filter_map(|window| window.downcast::<MultiWorkspace>())
3201 .next()
3202 .unwrap();
3203 let project = multi_workspace
3204 .read(cx)?
3205 .workspace()
3206 .read(cx)
3207 .project
3208 .clone();
3209 if project.read(cx).is_shared() {
3210 active_call.0.unshare_project(project, cx)?;
3211 }
3212 Ok::<_, anyhow::Error>(())
3213 });
3214 }
3215 }
3216
3217 let save_result = this
3218 .update_in(cx, |this, window, cx| {
3219 this.save_all_internal(SaveIntent::Close, window, cx)
3220 })?
3221 .await;
3222
3223 // If we're not quitting, but closing, we remove the workspace from
3224 // the current session.
3225 if close_intent != CloseIntent::Quit
3226 && !save_last_workspace
3227 && save_result.as_ref().is_ok_and(|&res| res)
3228 {
3229 this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx))?
3230 .await;
3231 }
3232
3233 save_result
3234 })
3235 }
3236
3237 fn save_all(&mut self, action: &SaveAll, window: &mut Window, cx: &mut Context<Self>) {
3238 self.save_all_internal(
3239 action.save_intent.unwrap_or(SaveIntent::SaveAll),
3240 window,
3241 cx,
3242 )
3243 .detach_and_log_err(cx);
3244 }
3245
3246 fn send_keystrokes(
3247 &mut self,
3248 action: &SendKeystrokes,
3249 window: &mut Window,
3250 cx: &mut Context<Self>,
3251 ) {
3252 let keystrokes: Vec<Keystroke> = action
3253 .0
3254 .split(' ')
3255 .flat_map(|k| Keystroke::parse(k).log_err())
3256 .map(|k| {
3257 cx.keyboard_mapper()
3258 .map_key_equivalent(k, false)
3259 .inner()
3260 .clone()
3261 })
3262 .collect();
3263 let _ = self.send_keystrokes_impl(keystrokes, window, cx);
3264 }
3265
3266 pub fn send_keystrokes_impl(
3267 &mut self,
3268 keystrokes: Vec<Keystroke>,
3269 window: &mut Window,
3270 cx: &mut Context<Self>,
3271 ) -> Shared<Task<()>> {
3272 let mut state = self.dispatching_keystrokes.borrow_mut();
3273 if !state.dispatched.insert(keystrokes.clone()) {
3274 cx.propagate();
3275 return state.task.clone().unwrap();
3276 }
3277
3278 state.queue.extend(keystrokes);
3279
3280 let keystrokes = self.dispatching_keystrokes.clone();
3281 if state.task.is_none() {
3282 state.task = Some(
3283 window
3284 .spawn(cx, async move |cx| {
3285 // limit to 100 keystrokes to avoid infinite recursion.
3286 for _ in 0..100 {
3287 let keystroke = {
3288 let mut state = keystrokes.borrow_mut();
3289 let Some(keystroke) = state.queue.pop_front() else {
3290 state.dispatched.clear();
3291 state.task.take();
3292 return;
3293 };
3294 keystroke
3295 };
3296 cx.update(|window, cx| {
3297 let focused = window.focused(cx);
3298 window.dispatch_keystroke(keystroke.clone(), cx);
3299 if window.focused(cx) != focused {
3300 // dispatch_keystroke may cause the focus to change.
3301 // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
3302 // And we need that to happen before the next keystroke to keep vim mode happy...
3303 // (Note that the tests always do this implicitly, so you must manually test with something like:
3304 // "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
3305 // )
3306 window.draw(cx).clear();
3307 }
3308 })
3309 .ok();
3310
3311 // Yield between synthetic keystrokes so deferred focus and
3312 // other effects can settle before dispatching the next key.
3313 yield_now().await;
3314 }
3315
3316 *keystrokes.borrow_mut() = Default::default();
3317 log::error!("over 100 keystrokes passed to send_keystrokes");
3318 })
3319 .shared(),
3320 );
3321 }
3322 state.task.clone().unwrap()
3323 }
3324
3325 /// Prompts the user to save or discard each dirty item, returning
3326 /// `true` if they confirmed (saved/discarded everything) or `false`
3327 /// if they cancelled. Used before removing worktree roots during
3328 /// thread archival.
3329 pub fn prompt_to_save_or_discard_dirty_items(
3330 &mut self,
3331 window: &mut Window,
3332 cx: &mut Context<Self>,
3333 ) -> Task<Result<bool>> {
3334 self.save_all_internal(SaveIntent::Close, window, cx)
3335 }
3336
3337 fn save_all_internal(
3338 &mut self,
3339 mut save_intent: SaveIntent,
3340 window: &mut Window,
3341 cx: &mut Context<Self>,
3342 ) -> Task<Result<bool>> {
3343 if self.project.read(cx).is_disconnected(cx) {
3344 return Task::ready(Ok(true));
3345 }
3346 let dirty_items = self
3347 .panes
3348 .iter()
3349 .flat_map(|pane| {
3350 pane.read(cx).items().filter_map(|item| {
3351 if item.is_dirty(cx) {
3352 item.tab_content_text(0, cx);
3353 Some((pane.downgrade(), item.boxed_clone()))
3354 } else {
3355 None
3356 }
3357 })
3358 })
3359 .collect::<Vec<_>>();
3360
3361 let project = self.project.clone();
3362 cx.spawn_in(window, async move |workspace, cx| {
3363 let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() {
3364 let (serialize_tasks, remaining_dirty_items) =
3365 workspace.update_in(cx, |workspace, window, cx| {
3366 let mut remaining_dirty_items = Vec::new();
3367 let mut serialize_tasks = Vec::new();
3368 for (pane, item) in dirty_items {
3369 if let Some(task) = item
3370 .to_serializable_item_handle(cx)
3371 .and_then(|handle| handle.serialize(workspace, true, window, cx))
3372 {
3373 serialize_tasks.push(task);
3374 } else {
3375 remaining_dirty_items.push((pane, item));
3376 }
3377 }
3378 (serialize_tasks, remaining_dirty_items)
3379 })?;
3380
3381 futures::future::try_join_all(serialize_tasks).await?;
3382
3383 if !remaining_dirty_items.is_empty() {
3384 workspace.update(cx, |_, cx| cx.emit(Event::Activate))?;
3385 }
3386
3387 if remaining_dirty_items.len() > 1 {
3388 let answer = workspace.update_in(cx, |_, window, cx| {
3389 cx.emit(Event::Activate);
3390 let detail = Pane::file_names_for_prompt(
3391 &mut remaining_dirty_items.iter().map(|(_, handle)| handle),
3392 cx,
3393 );
3394 window.prompt(
3395 PromptLevel::Warning,
3396 "Do you want to save all changes in the following files?",
3397 Some(&detail),
3398 &["Save all", "Discard all", "Cancel"],
3399 cx,
3400 )
3401 })?;
3402 match answer.await.log_err() {
3403 Some(0) => save_intent = SaveIntent::SaveAll,
3404 Some(1) => save_intent = SaveIntent::Skip,
3405 Some(2) => return Ok(false),
3406 _ => {}
3407 }
3408 }
3409
3410 remaining_dirty_items
3411 } else {
3412 dirty_items
3413 };
3414
3415 for (pane, item) in dirty_items {
3416 let (singleton, project_entry_ids) = cx.update(|_, cx| {
3417 (
3418 item.buffer_kind(cx) == ItemBufferKind::Singleton,
3419 item.project_entry_ids(cx),
3420 )
3421 })?;
3422 if (singleton || !project_entry_ids.is_empty())
3423 && !Pane::save_item(project.clone(), &pane, &*item, save_intent, cx).await?
3424 {
3425 return Ok(false);
3426 }
3427 }
3428 Ok(true)
3429 })
3430 }
3431
3432 pub fn open_workspace_for_paths(
3433 &mut self,
3434 // replace_current_window: bool,
3435 mut open_mode: OpenMode,
3436 paths: Vec<PathBuf>,
3437 window: &mut Window,
3438 cx: &mut Context<Self>,
3439 ) -> Task<Result<Entity<Workspace>>> {
3440 let requesting_window = window.window_handle().downcast::<MultiWorkspace>();
3441 let is_remote = self.project.read(cx).is_via_collab();
3442 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
3443 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
3444
3445 let workspace_is_empty = !is_remote && !has_worktree && !has_dirty_items;
3446 if workspace_is_empty {
3447 open_mode = OpenMode::Activate;
3448 }
3449
3450 let app_state = self.app_state.clone();
3451
3452 cx.spawn(async move |_, cx| {
3453 let OpenResult { workspace, .. } = cx
3454 .update(|cx| {
3455 open_paths(
3456 &paths,
3457 app_state,
3458 OpenOptions {
3459 requesting_window,
3460 open_mode,
3461 ..Default::default()
3462 },
3463 cx,
3464 )
3465 })
3466 .await?;
3467 Ok(workspace)
3468 })
3469 }
3470
3471 #[allow(clippy::type_complexity)]
3472 pub fn open_paths(
3473 &mut self,
3474 mut abs_paths: Vec<PathBuf>,
3475 options: OpenOptions,
3476 pane: Option<WeakEntity<Pane>>,
3477 window: &mut Window,
3478 cx: &mut Context<Self>,
3479 ) -> Task<Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>> {
3480 let fs = self.app_state.fs.clone();
3481
3482 let caller_ordered_abs_paths = abs_paths.clone();
3483
3484 // Sort the paths to ensure we add worktrees for parents before their children.
3485 abs_paths.sort_unstable();
3486 cx.spawn_in(window, async move |this, cx| {
3487 let mut tasks = Vec::with_capacity(abs_paths.len());
3488
3489 for abs_path in &abs_paths {
3490 let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
3491 OpenVisible::All => Some(true),
3492 OpenVisible::None => Some(false),
3493 OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
3494 Some(Some(metadata)) => Some(!metadata.is_dir),
3495 Some(None) => Some(true),
3496 None => None,
3497 },
3498 OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
3499 Some(Some(metadata)) => Some(metadata.is_dir),
3500 Some(None) => Some(false),
3501 None => None,
3502 },
3503 };
3504 let project_path = match visible {
3505 Some(visible) => match this
3506 .update(cx, |this, cx| {
3507 Workspace::project_path_for_path(
3508 this.project.clone(),
3509 abs_path,
3510 visible,
3511 cx,
3512 )
3513 })
3514 .log_err()
3515 {
3516 Some(project_path) => project_path.await.log_err(),
3517 None => None,
3518 },
3519 None => None,
3520 };
3521
3522 let this = this.clone();
3523 let abs_path: Arc<Path> = SanitizedPath::new(&abs_path).as_path().into();
3524 let fs = fs.clone();
3525 let pane = pane.clone();
3526 let task = cx.spawn(async move |cx| {
3527 let (_worktree, project_path) = project_path?;
3528 if fs.is_dir(&abs_path).await {
3529 // Opening a directory should not race to update the active entry.
3530 // We'll select/reveal a deterministic final entry after all paths finish opening.
3531 None
3532 } else {
3533 Some(
3534 this.update_in(cx, |this, window, cx| {
3535 this.open_path(
3536 project_path,
3537 pane,
3538 options.focus.unwrap_or(true),
3539 window,
3540 cx,
3541 )
3542 })
3543 .ok()?
3544 .await,
3545 )
3546 }
3547 });
3548 tasks.push(task);
3549 }
3550
3551 let results = futures::future::join_all(tasks).await;
3552
3553 // Determine the winner using the fake/abstract FS metadata, not `Path::is_dir`.
3554 let mut winner: Option<(PathBuf, bool)> = None;
3555 for abs_path in caller_ordered_abs_paths.into_iter().rev() {
3556 if let Some(Some(metadata)) = fs.metadata(&abs_path).await.log_err() {
3557 if !metadata.is_dir {
3558 winner = Some((abs_path, false));
3559 break;
3560 }
3561 if winner.is_none() {
3562 winner = Some((abs_path, true));
3563 }
3564 } else if winner.is_none() {
3565 winner = Some((abs_path, false));
3566 }
3567 }
3568
3569 // Compute the winner entry id on the foreground thread and emit once, after all
3570 // paths finish opening. This avoids races between concurrently-opening paths
3571 // (directories in particular) and makes the resulting project panel selection
3572 // deterministic.
3573 if let Some((winner_abs_path, winner_is_dir)) = winner {
3574 'emit_winner: {
3575 let winner_abs_path: Arc<Path> =
3576 SanitizedPath::new(&winner_abs_path).as_path().into();
3577
3578 let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
3579 OpenVisible::All => true,
3580 OpenVisible::None => false,
3581 OpenVisible::OnlyFiles => !winner_is_dir,
3582 OpenVisible::OnlyDirectories => winner_is_dir,
3583 };
3584
3585 let Some(worktree_task) = this
3586 .update(cx, |workspace, cx| {
3587 workspace.project.update(cx, |project, cx| {
3588 project.find_or_create_worktree(
3589 winner_abs_path.as_ref(),
3590 visible,
3591 cx,
3592 )
3593 })
3594 })
3595 .ok()
3596 else {
3597 break 'emit_winner;
3598 };
3599
3600 let Ok((worktree, _)) = worktree_task.await else {
3601 break 'emit_winner;
3602 };
3603
3604 let Ok(Some(entry_id)) = this.update(cx, |_, cx| {
3605 let worktree = worktree.read(cx);
3606 let worktree_abs_path = worktree.abs_path();
3607 let entry = if winner_abs_path.as_ref() == worktree_abs_path.as_ref() {
3608 worktree.root_entry()
3609 } else {
3610 winner_abs_path
3611 .strip_prefix(worktree_abs_path.as_ref())
3612 .ok()
3613 .and_then(|relative_path| {
3614 let relative_path =
3615 RelPath::new(relative_path, PathStyle::local())
3616 .log_err()?;
3617 worktree.entry_for_path(&relative_path)
3618 })
3619 }?;
3620 Some(entry.id)
3621 }) else {
3622 break 'emit_winner;
3623 };
3624
3625 this.update(cx, |workspace, cx| {
3626 workspace.project.update(cx, |_, cx| {
3627 cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
3628 });
3629 })
3630 .ok();
3631 }
3632 }
3633
3634 results
3635 })
3636 }
3637
3638 pub fn open_resolved_path(
3639 &mut self,
3640 path: ResolvedPath,
3641 window: &mut Window,
3642 cx: &mut Context<Self>,
3643 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3644 match path {
3645 ResolvedPath::ProjectPath { project_path, .. } => {
3646 self.open_path(project_path, None, true, window, cx)
3647 }
3648 ResolvedPath::AbsPath { path, .. } => self.open_abs_path(
3649 PathBuf::from(path),
3650 OpenOptions {
3651 visible: Some(OpenVisible::None),
3652 ..Default::default()
3653 },
3654 window,
3655 cx,
3656 ),
3657 }
3658 }
3659
3660 pub fn absolute_path_of_worktree(
3661 &self,
3662 worktree_id: WorktreeId,
3663 cx: &mut Context<Self>,
3664 ) -> Option<PathBuf> {
3665 self.project
3666 .read(cx)
3667 .worktree_for_id(worktree_id, cx)
3668 // TODO: use `abs_path` or `root_dir`
3669 .map(|wt| wt.read(cx).abs_path().as_ref().to_path_buf())
3670 }
3671
3672 pub fn add_folder_to_project(
3673 &mut self,
3674 _: &AddFolderToProject,
3675 window: &mut Window,
3676 cx: &mut Context<Self>,
3677 ) {
3678 let project = self.project.read(cx);
3679 if project.is_via_collab() {
3680 self.show_error(
3681 &anyhow!("You cannot add folders to someone else's project"),
3682 cx,
3683 );
3684 return;
3685 }
3686 let paths = self.prompt_for_open_path(
3687 PathPromptOptions {
3688 files: false,
3689 directories: true,
3690 multiple: true,
3691 prompt: None,
3692 },
3693 DirectoryLister::Project(self.project.clone()),
3694 window,
3695 cx,
3696 );
3697 cx.spawn_in(window, async move |this, cx| {
3698 if let Some(paths) = paths.await.log_err().flatten() {
3699 let results = this
3700 .update_in(cx, |this, window, cx| {
3701 this.open_paths(
3702 paths,
3703 OpenOptions {
3704 visible: Some(OpenVisible::All),
3705 ..Default::default()
3706 },
3707 None,
3708 window,
3709 cx,
3710 )
3711 })?
3712 .await;
3713 for result in results.into_iter().flatten() {
3714 result.log_err();
3715 }
3716 }
3717 anyhow::Ok(())
3718 })
3719 .detach_and_log_err(cx);
3720 }
3721
3722 pub fn project_path_for_path(
3723 project: Entity<Project>,
3724 abs_path: &Path,
3725 visible: bool,
3726 cx: &mut App,
3727 ) -> Task<Result<(Entity<Worktree>, ProjectPath)>> {
3728 let entry = project.update(cx, |project, cx| {
3729 project.find_or_create_worktree(abs_path, visible, cx)
3730 });
3731 cx.spawn(async move |cx| {
3732 let (worktree, path) = entry.await?;
3733 let worktree_id = worktree.read_with(cx, |t, _| t.id());
3734 Ok((worktree, ProjectPath { worktree_id, path }))
3735 })
3736 }
3737
3738 pub fn items<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator<Item = &'a Box<dyn ItemHandle>> {
3739 self.panes.iter().flat_map(|pane| pane.read(cx).items())
3740 }
3741
3742 pub fn item_of_type<T: Item>(&self, cx: &App) -> Option<Entity<T>> {
3743 self.items_of_type(cx).max_by_key(|item| item.item_id())
3744 }
3745
3746 pub fn items_of_type<'a, T: Item>(
3747 &'a self,
3748 cx: &'a App,
3749 ) -> impl 'a + Iterator<Item = Entity<T>> {
3750 self.panes
3751 .iter()
3752 .flat_map(|pane| pane.read(cx).items_of_type())
3753 }
3754
3755 pub fn active_item(&self, cx: &App) -> Option<Box<dyn ItemHandle>> {
3756 self.active_pane().read(cx).active_item()
3757 }
3758
3759 pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
3760 let item = self.active_item(cx)?;
3761 item.to_any_view().downcast::<I>().ok()
3762 }
3763
3764 fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
3765 self.active_item(cx).and_then(|item| item.project_path(cx))
3766 }
3767
3768 pub fn most_recent_active_path(&self, cx: &App) -> Option<PathBuf> {
3769 self.recent_navigation_history_iter(cx)
3770 .filter_map(|(path, abs_path)| {
3771 let worktree = self
3772 .project
3773 .read(cx)
3774 .worktree_for_id(path.worktree_id, cx)?;
3775 if worktree.read(cx).is_visible() {
3776 abs_path
3777 } else {
3778 None
3779 }
3780 })
3781 .next()
3782 }
3783
3784 pub fn save_active_item(
3785 &mut self,
3786 save_intent: SaveIntent,
3787 window: &mut Window,
3788 cx: &mut App,
3789 ) -> Task<Result<()>> {
3790 let project = self.project.clone();
3791 let pane = self.active_pane();
3792 let item = pane.read(cx).active_item();
3793 let pane = pane.downgrade();
3794
3795 window.spawn(cx, async move |cx| {
3796 if let Some(item) = item {
3797 Pane::save_item(project, &pane, item.as_ref(), save_intent, cx)
3798 .await
3799 .map(|_| ())
3800 } else {
3801 Ok(())
3802 }
3803 })
3804 }
3805
3806 pub fn close_inactive_items_and_panes(
3807 &mut self,
3808 action: &CloseInactiveTabsAndPanes,
3809 window: &mut Window,
3810 cx: &mut Context<Self>,
3811 ) {
3812 if let Some(task) = self.close_all_internal(
3813 true,
3814 action.save_intent.unwrap_or(SaveIntent::Close),
3815 window,
3816 cx,
3817 ) {
3818 task.detach_and_log_err(cx)
3819 }
3820 }
3821
3822 pub fn close_all_items_and_panes(
3823 &mut self,
3824 action: &CloseAllItemsAndPanes,
3825 window: &mut Window,
3826 cx: &mut Context<Self>,
3827 ) {
3828 if let Some(task) = self.close_all_internal(
3829 false,
3830 action.save_intent.unwrap_or(SaveIntent::Close),
3831 window,
3832 cx,
3833 ) {
3834 task.detach_and_log_err(cx)
3835 }
3836 }
3837
3838 /// Closes the active item across all panes.
3839 pub fn close_item_in_all_panes(
3840 &mut self,
3841 action: &CloseItemInAllPanes,
3842 window: &mut Window,
3843 cx: &mut Context<Self>,
3844 ) {
3845 let Some(active_item) = self.active_pane().read(cx).active_item() else {
3846 return;
3847 };
3848
3849 let save_intent = action.save_intent.unwrap_or(SaveIntent::Close);
3850 let close_pinned = action.close_pinned;
3851
3852 if let Some(project_path) = active_item.project_path(cx) {
3853 self.close_items_with_project_path(
3854 &project_path,
3855 save_intent,
3856 close_pinned,
3857 window,
3858 cx,
3859 );
3860 } else if close_pinned || !self.active_pane().read(cx).is_active_item_pinned() {
3861 let item_id = active_item.item_id();
3862 self.active_pane().update(cx, |pane, cx| {
3863 pane.close_item_by_id(item_id, save_intent, window, cx)
3864 .detach_and_log_err(cx);
3865 });
3866 }
3867 }
3868
3869 /// Closes all items with the given project path across all panes.
3870 pub fn close_items_with_project_path(
3871 &mut self,
3872 project_path: &ProjectPath,
3873 save_intent: SaveIntent,
3874 close_pinned: bool,
3875 window: &mut Window,
3876 cx: &mut Context<Self>,
3877 ) {
3878 let panes = self.panes().to_vec();
3879 for pane in panes {
3880 pane.update(cx, |pane, cx| {
3881 pane.close_items_for_project_path(
3882 project_path,
3883 save_intent,
3884 close_pinned,
3885 window,
3886 cx,
3887 )
3888 .detach_and_log_err(cx);
3889 });
3890 }
3891 }
3892
3893 fn close_all_internal(
3894 &mut self,
3895 retain_active_pane: bool,
3896 save_intent: SaveIntent,
3897 window: &mut Window,
3898 cx: &mut Context<Self>,
3899 ) -> Option<Task<Result<()>>> {
3900 let current_pane = self.active_pane();
3901
3902 let mut tasks = Vec::new();
3903
3904 if retain_active_pane {
3905 let current_pane_close = current_pane.update(cx, |pane, cx| {
3906 pane.close_other_items(
3907 &CloseOtherItems {
3908 save_intent: None,
3909 close_pinned: false,
3910 },
3911 None,
3912 window,
3913 cx,
3914 )
3915 });
3916
3917 tasks.push(current_pane_close);
3918 }
3919
3920 for pane in self.panes() {
3921 if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
3922 continue;
3923 }
3924
3925 let close_pane_items = pane.update(cx, |pane: &mut Pane, cx| {
3926 pane.close_all_items(
3927 &CloseAllItems {
3928 save_intent: Some(save_intent),
3929 close_pinned: false,
3930 },
3931 window,
3932 cx,
3933 )
3934 });
3935
3936 tasks.push(close_pane_items)
3937 }
3938
3939 if tasks.is_empty() {
3940 None
3941 } else {
3942 Some(cx.spawn_in(window, async move |_, _| {
3943 for task in tasks {
3944 task.await?
3945 }
3946 Ok(())
3947 }))
3948 }
3949 }
3950
3951 pub fn is_dock_at_position_open(&self, position: DockPosition, cx: &mut Context<Self>) -> bool {
3952 self.dock_at_position(position).read(cx).is_open()
3953 }
3954
3955 pub fn toggle_dock(
3956 &mut self,
3957 dock_side: DockPosition,
3958 window: &mut Window,
3959 cx: &mut Context<Self>,
3960 ) {
3961 let mut focus_center = false;
3962 let mut reveal_dock = false;
3963
3964 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
3965 let was_visible = self.is_dock_at_position_open(dock_side, cx) && !other_is_zoomed;
3966
3967 if let Some(panel) = self.dock_at_position(dock_side).read(cx).active_panel() {
3968 telemetry::event!(
3969 "Panel Button Clicked",
3970 name = panel.persistent_name(),
3971 toggle_state = !was_visible
3972 );
3973 }
3974 if was_visible {
3975 self.save_open_dock_positions(cx);
3976 }
3977
3978 let dock = self.dock_at_position(dock_side);
3979 dock.update(cx, |dock, cx| {
3980 dock.set_open(!was_visible, window, cx);
3981
3982 if dock.active_panel().is_none() {
3983 let Some(panel_ix) = dock
3984 .first_enabled_panel_idx(cx)
3985 .log_with_level(log::Level::Info)
3986 else {
3987 return;
3988 };
3989 dock.activate_panel(panel_ix, window, cx);
3990 }
3991
3992 if let Some(active_panel) = dock.active_panel() {
3993 if was_visible {
3994 if active_panel
3995 .panel_focus_handle(cx)
3996 .contains_focused(window, cx)
3997 {
3998 focus_center = true;
3999 }
4000 } else {
4001 let focus_handle = &active_panel.panel_focus_handle(cx);
4002 window.focus(focus_handle, cx);
4003 reveal_dock = true;
4004 }
4005 }
4006 });
4007
4008 if reveal_dock {
4009 self.dismiss_zoomed_items_to_reveal(Some(dock_side), window, cx);
4010 }
4011
4012 if focus_center {
4013 self.active_pane
4014 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
4015 }
4016
4017 cx.notify();
4018 self.serialize_workspace(window, cx);
4019 }
4020
4021 fn active_dock(&self, window: &Window, cx: &Context<Self>) -> Option<&Entity<Dock>> {
4022 self.all_docks().into_iter().find(|&dock| {
4023 dock.read(cx).is_open() && dock.focus_handle(cx).contains_focused(window, cx)
4024 })
4025 }
4026
4027 fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
4028 if let Some(dock) = self.active_dock(window, cx).cloned() {
4029 self.save_open_dock_positions(cx);
4030 dock.update(cx, |dock, cx| {
4031 dock.set_open(false, window, cx);
4032 });
4033 return true;
4034 }
4035 false
4036 }
4037
4038 pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4039 self.save_open_dock_positions(cx);
4040 for dock in self.all_docks() {
4041 dock.update(cx, |dock, cx| {
4042 dock.set_open(false, window, cx);
4043 });
4044 }
4045
4046 cx.focus_self(window);
4047 cx.notify();
4048 self.serialize_workspace(window, cx);
4049 }
4050
4051 fn get_open_dock_positions(&self, cx: &Context<Self>) -> Vec<DockPosition> {
4052 self.all_docks()
4053 .into_iter()
4054 .filter_map(|dock| {
4055 let dock_ref = dock.read(cx);
4056 if dock_ref.is_open() {
4057 Some(dock_ref.position())
4058 } else {
4059 None
4060 }
4061 })
4062 .collect()
4063 }
4064
4065 /// Saves the positions of currently open docks.
4066 ///
4067 /// Updates `last_open_dock_positions` with positions of all currently open
4068 /// docks, to later be restored by the 'Toggle All Docks' action.
4069 fn save_open_dock_positions(&mut self, cx: &mut Context<Self>) {
4070 let open_dock_positions = self.get_open_dock_positions(cx);
4071 if !open_dock_positions.is_empty() {
4072 self.last_open_dock_positions = open_dock_positions;
4073 }
4074 }
4075
4076 /// Toggles all docks between open and closed states.
4077 ///
4078 /// If any docks are open, closes all and remembers their positions. If all
4079 /// docks are closed, restores the last remembered dock configuration.
4080 fn toggle_all_docks(
4081 &mut self,
4082 _: &ToggleAllDocks,
4083 window: &mut Window,
4084 cx: &mut Context<Self>,
4085 ) {
4086 let open_dock_positions = self.get_open_dock_positions(cx);
4087
4088 if !open_dock_positions.is_empty() {
4089 self.close_all_docks(window, cx);
4090 } else if !self.last_open_dock_positions.is_empty() {
4091 self.restore_last_open_docks(window, cx);
4092 }
4093 }
4094
4095 /// Reopens docks from the most recently remembered configuration.
4096 ///
4097 /// Opens all docks whose positions are stored in `last_open_dock_positions`
4098 /// and clears the stored positions.
4099 fn restore_last_open_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4100 let positions_to_open = std::mem::take(&mut self.last_open_dock_positions);
4101
4102 for position in positions_to_open {
4103 let dock = self.dock_at_position(position);
4104 dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
4105 }
4106
4107 cx.focus_self(window);
4108 cx.notify();
4109 self.serialize_workspace(window, cx);
4110 }
4111
4112 /// Transfer focus to the panel of the given type.
4113 pub fn focus_panel<T: Panel>(
4114 &mut self,
4115 window: &mut Window,
4116 cx: &mut Context<Self>,
4117 ) -> Option<Entity<T>> {
4118 let panel = self.focus_or_unfocus_panel::<T>(window, cx, &mut |_, _, _| true)?;
4119 panel.to_any().downcast().ok()
4120 }
4121
4122 /// Focus the panel of the given type if it isn't already focused. If it is
4123 /// already focused, then transfer focus back to the workspace center.
4124 /// When the `close_panel_on_toggle` setting is enabled, also closes the
4125 /// panel when transferring focus back to the center.
4126 pub fn toggle_panel_focus<T: Panel>(
4127 &mut self,
4128 window: &mut Window,
4129 cx: &mut Context<Self>,
4130 ) -> bool {
4131 let mut did_focus_panel = false;
4132 self.focus_or_unfocus_panel::<T>(window, cx, &mut |panel, window, cx| {
4133 did_focus_panel = !panel.panel_focus_handle(cx).contains_focused(window, cx);
4134 did_focus_panel
4135 });
4136
4137 if !did_focus_panel && WorkspaceSettings::get_global(cx).close_panel_on_toggle {
4138 self.close_panel::<T>(window, cx);
4139 }
4140
4141 telemetry::event!(
4142 "Panel Button Clicked",
4143 name = T::persistent_name(),
4144 toggle_state = did_focus_panel
4145 );
4146
4147 did_focus_panel
4148 }
4149
4150 pub fn focus_center_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4151 if let Some(item) = self.active_item(cx) {
4152 item.item_focus_handle(cx).focus(window, cx);
4153 } else {
4154 log::error!("Could not find a focus target when switching focus to the center panes",);
4155 }
4156 }
4157
4158 pub fn activate_panel_for_proto_id(
4159 &mut self,
4160 panel_id: PanelId,
4161 window: &mut Window,
4162 cx: &mut Context<Self>,
4163 ) -> Option<Arc<dyn PanelHandle>> {
4164 let mut panel = None;
4165 for dock in self.all_docks() {
4166 if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
4167 panel = dock.update(cx, |dock, cx| {
4168 dock.activate_panel(panel_index, window, cx);
4169 dock.set_open(true, window, cx);
4170 dock.active_panel().cloned()
4171 });
4172 break;
4173 }
4174 }
4175
4176 if panel.is_some() {
4177 cx.notify();
4178 self.serialize_workspace(window, cx);
4179 }
4180
4181 panel
4182 }
4183
4184 /// Focus or unfocus the given panel type, depending on the given callback.
4185 fn focus_or_unfocus_panel<T: Panel>(
4186 &mut self,
4187 window: &mut Window,
4188 cx: &mut Context<Self>,
4189 should_focus: &mut dyn FnMut(&dyn PanelHandle, &mut Window, &mut Context<Dock>) -> bool,
4190 ) -> Option<Arc<dyn PanelHandle>> {
4191 let mut result_panel = None;
4192 let mut serialize = false;
4193 for dock in self.all_docks() {
4194 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
4195 let mut focus_center = false;
4196 let panel = dock.update(cx, |dock, cx| {
4197 dock.activate_panel(panel_index, window, cx);
4198
4199 let panel = dock.active_panel().cloned();
4200 if let Some(panel) = panel.as_ref() {
4201 if should_focus(&**panel, window, cx) {
4202 dock.set_open(true, window, cx);
4203 panel.panel_focus_handle(cx).focus(window, cx);
4204 } else {
4205 focus_center = true;
4206 }
4207 }
4208 panel
4209 });
4210
4211 if focus_center {
4212 self.active_pane
4213 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
4214 }
4215
4216 result_panel = panel;
4217 serialize = true;
4218 break;
4219 }
4220 }
4221
4222 if serialize {
4223 self.serialize_workspace(window, cx);
4224 }
4225
4226 cx.notify();
4227 result_panel
4228 }
4229
4230 /// Open the panel of the given type
4231 pub fn open_panel<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4232 for dock in self.all_docks() {
4233 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
4234 dock.update(cx, |dock, cx| {
4235 dock.activate_panel(panel_index, window, cx);
4236 dock.set_open(true, window, cx);
4237 });
4238 }
4239 }
4240 }
4241
4242 /// Open the panel of the given type, dismissing any zoomed items that
4243 /// would obscure it (e.g. a zoomed terminal).
4244 pub fn reveal_panel<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4245 let dock_position = self.all_docks().iter().find_map(|dock| {
4246 let dock = dock.read(cx);
4247 dock.panel_index_for_type::<T>().map(|_| dock.position())
4248 });
4249 self.dismiss_zoomed_items_to_reveal(dock_position, window, cx);
4250 self.open_panel::<T>(window, cx);
4251 }
4252
4253 pub fn close_panel<T: Panel>(&self, window: &mut Window, cx: &mut Context<Self>) {
4254 for dock in self.all_docks().iter() {
4255 dock.update(cx, |dock, cx| {
4256 if dock.panel::<T>().is_some() {
4257 dock.set_open(false, window, cx)
4258 }
4259 })
4260 }
4261 }
4262
4263 pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
4264 self.all_docks()
4265 .iter()
4266 .find_map(|dock| dock.read(cx).panel::<T>())
4267 }
4268
4269 fn dismiss_zoomed_items_to_reveal(
4270 &mut self,
4271 dock_to_reveal: Option<DockPosition>,
4272 window: &mut Window,
4273 cx: &mut Context<Self>,
4274 ) {
4275 // If a center pane is zoomed, unzoom it.
4276 for pane in &self.panes {
4277 if pane != &self.active_pane || dock_to_reveal.is_some() {
4278 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
4279 }
4280 }
4281
4282 // If another dock is zoomed, hide it.
4283 let mut focus_center = false;
4284 for dock in self.all_docks() {
4285 dock.update(cx, |dock, cx| {
4286 if Some(dock.position()) != dock_to_reveal
4287 && let Some(panel) = dock.active_panel()
4288 && panel.is_zoomed(window, cx)
4289 {
4290 focus_center |= panel.panel_focus_handle(cx).contains_focused(window, cx);
4291 dock.set_open(false, window, cx);
4292 }
4293 });
4294 }
4295
4296 if focus_center {
4297 self.active_pane
4298 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
4299 }
4300
4301 if self.zoomed_position != dock_to_reveal {
4302 self.zoomed = None;
4303 self.zoomed_position = None;
4304 cx.emit(Event::ZoomChanged);
4305 }
4306
4307 cx.notify();
4308 }
4309
4310 fn add_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
4311 let pane = cx.new(|cx| {
4312 let mut pane = Pane::new(
4313 self.weak_handle(),
4314 self.project.clone(),
4315 self.pane_history_timestamp.clone(),
4316 None,
4317 NewFile.boxed_clone(),
4318 true,
4319 window,
4320 cx,
4321 );
4322 pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
4323 pane
4324 });
4325 cx.subscribe_in(&pane, window, Self::handle_pane_event)
4326 .detach();
4327 self.panes.push(pane.clone());
4328
4329 window.focus(&pane.focus_handle(cx), cx);
4330
4331 cx.emit(Event::PaneAdded(pane.clone()));
4332 pane
4333 }
4334
4335 pub fn add_item_to_center(
4336 &mut self,
4337 item: Box<dyn ItemHandle>,
4338 window: &mut Window,
4339 cx: &mut Context<Self>,
4340 ) -> bool {
4341 if let Some(center_pane) = self.last_active_center_pane.clone() {
4342 if let Some(center_pane) = center_pane.upgrade() {
4343 center_pane.update(cx, |pane, cx| {
4344 pane.add_item(item, true, true, None, window, cx)
4345 });
4346 true
4347 } else {
4348 false
4349 }
4350 } else {
4351 false
4352 }
4353 }
4354
4355 pub fn add_item_to_active_pane(
4356 &mut self,
4357 item: Box<dyn ItemHandle>,
4358 destination_index: Option<usize>,
4359 focus_item: bool,
4360 window: &mut Window,
4361 cx: &mut App,
4362 ) {
4363 self.add_item(
4364 self.active_pane.clone(),
4365 item,
4366 destination_index,
4367 false,
4368 focus_item,
4369 window,
4370 cx,
4371 )
4372 }
4373
4374 pub fn add_item(
4375 &mut self,
4376 pane: Entity<Pane>,
4377 item: Box<dyn ItemHandle>,
4378 destination_index: Option<usize>,
4379 activate_pane: bool,
4380 focus_item: bool,
4381 window: &mut Window,
4382 cx: &mut App,
4383 ) {
4384 pane.update(cx, |pane, cx| {
4385 pane.add_item(
4386 item,
4387 activate_pane,
4388 focus_item,
4389 destination_index,
4390 window,
4391 cx,
4392 )
4393 });
4394 }
4395
4396 pub fn split_item(
4397 &mut self,
4398 split_direction: SplitDirection,
4399 item: Box<dyn ItemHandle>,
4400 window: &mut Window,
4401 cx: &mut Context<Self>,
4402 ) {
4403 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, window, cx);
4404 self.add_item(new_pane, item, None, true, true, window, cx);
4405 }
4406
4407 pub fn open_abs_path(
4408 &mut self,
4409 abs_path: PathBuf,
4410 options: OpenOptions,
4411 window: &mut Window,
4412 cx: &mut Context<Self>,
4413 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4414 cx.spawn_in(window, async move |workspace, cx| {
4415 let open_paths_task_result = workspace
4416 .update_in(cx, |workspace, window, cx| {
4417 workspace.open_paths(vec![abs_path.clone()], options, None, window, cx)
4418 })
4419 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
4420 .await;
4421 anyhow::ensure!(
4422 open_paths_task_result.len() == 1,
4423 "open abs path {abs_path:?} task returned incorrect number of results"
4424 );
4425 match open_paths_task_result
4426 .into_iter()
4427 .next()
4428 .expect("ensured single task result")
4429 {
4430 Some(open_result) => {
4431 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
4432 }
4433 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
4434 }
4435 })
4436 }
4437
4438 pub fn split_abs_path(
4439 &mut self,
4440 abs_path: PathBuf,
4441 visible: bool,
4442 window: &mut Window,
4443 cx: &mut Context<Self>,
4444 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4445 let project_path_task =
4446 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
4447 cx.spawn_in(window, async move |this, cx| {
4448 let (_, path) = project_path_task.await?;
4449 this.update_in(cx, |this, window, cx| this.split_path(path, window, cx))?
4450 .await
4451 })
4452 }
4453
4454 pub fn open_path(
4455 &mut self,
4456 path: impl Into<ProjectPath>,
4457 pane: Option<WeakEntity<Pane>>,
4458 focus_item: bool,
4459 window: &mut Window,
4460 cx: &mut App,
4461 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4462 self.open_path_preview(path, pane, focus_item, false, true, window, cx)
4463 }
4464
4465 pub fn open_path_preview(
4466 &mut self,
4467 path: impl Into<ProjectPath>,
4468 pane: Option<WeakEntity<Pane>>,
4469 focus_item: bool,
4470 allow_preview: bool,
4471 activate: bool,
4472 window: &mut Window,
4473 cx: &mut App,
4474 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4475 let pane = pane.unwrap_or_else(|| {
4476 self.last_active_center_pane.clone().unwrap_or_else(|| {
4477 self.panes
4478 .first()
4479 .expect("There must be an active pane")
4480 .downgrade()
4481 })
4482 });
4483
4484 let project_path = path.into();
4485 let task = self.load_path(project_path.clone(), window, cx);
4486 window.spawn(cx, async move |cx| {
4487 let (project_entry_id, build_item) = task.await?;
4488
4489 pane.update_in(cx, |pane, window, cx| {
4490 pane.open_item(
4491 project_entry_id,
4492 project_path,
4493 focus_item,
4494 allow_preview,
4495 activate,
4496 None,
4497 window,
4498 cx,
4499 build_item,
4500 )
4501 })
4502 })
4503 }
4504
4505 pub fn split_path(
4506 &mut self,
4507 path: impl Into<ProjectPath>,
4508 window: &mut Window,
4509 cx: &mut Context<Self>,
4510 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4511 self.split_path_preview(path, false, None, window, cx)
4512 }
4513
4514 pub fn split_path_preview(
4515 &mut self,
4516 path: impl Into<ProjectPath>,
4517 allow_preview: bool,
4518 split_direction: Option<SplitDirection>,
4519 window: &mut Window,
4520 cx: &mut Context<Self>,
4521 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4522 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
4523 self.panes
4524 .first()
4525 .expect("There must be an active pane")
4526 .downgrade()
4527 });
4528
4529 if let Member::Pane(center_pane) = &self.center.root
4530 && center_pane.read(cx).items_len() == 0
4531 {
4532 return self.open_path(path, Some(pane), true, window, cx);
4533 }
4534
4535 let project_path = path.into();
4536 let task = self.load_path(project_path.clone(), window, cx);
4537 cx.spawn_in(window, async move |this, cx| {
4538 let (project_entry_id, build_item) = task.await?;
4539 this.update_in(cx, move |this, window, cx| -> Option<_> {
4540 let pane = pane.upgrade()?;
4541 let new_pane = this.split_pane(
4542 pane,
4543 split_direction.unwrap_or(SplitDirection::Right),
4544 window,
4545 cx,
4546 );
4547 new_pane.update(cx, |new_pane, cx| {
4548 Some(new_pane.open_item(
4549 project_entry_id,
4550 project_path,
4551 true,
4552 allow_preview,
4553 true,
4554 None,
4555 window,
4556 cx,
4557 build_item,
4558 ))
4559 })
4560 })
4561 .map(|option| option.context("pane was dropped"))?
4562 })
4563 }
4564
4565 fn load_path(
4566 &mut self,
4567 path: ProjectPath,
4568 window: &mut Window,
4569 cx: &mut App,
4570 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
4571 let registry = cx.default_global::<ProjectItemRegistry>().clone();
4572 registry.open_path(self.project(), &path, window, cx)
4573 }
4574
4575 pub fn find_project_item<T>(
4576 &self,
4577 pane: &Entity<Pane>,
4578 project_item: &Entity<T::Item>,
4579 cx: &App,
4580 ) -> Option<Entity<T>>
4581 where
4582 T: ProjectItem,
4583 {
4584 use project::ProjectItem as _;
4585 let project_item = project_item.read(cx);
4586 let entry_id = project_item.entry_id(cx);
4587 let project_path = project_item.project_path(cx);
4588
4589 let mut item = None;
4590 if let Some(entry_id) = entry_id {
4591 item = pane.read(cx).item_for_entry(entry_id, cx);
4592 }
4593 if item.is_none()
4594 && let Some(project_path) = project_path
4595 {
4596 item = pane.read(cx).item_for_path(project_path, cx);
4597 }
4598
4599 item.and_then(|item| item.downcast::<T>())
4600 }
4601
4602 pub fn is_project_item_open<T>(
4603 &self,
4604 pane: &Entity<Pane>,
4605 project_item: &Entity<T::Item>,
4606 cx: &App,
4607 ) -> bool
4608 where
4609 T: ProjectItem,
4610 {
4611 self.find_project_item::<T>(pane, project_item, cx)
4612 .is_some()
4613 }
4614
4615 pub fn open_project_item<T>(
4616 &mut self,
4617 pane: Entity<Pane>,
4618 project_item: Entity<T::Item>,
4619 activate_pane: bool,
4620 focus_item: bool,
4621 keep_old_preview: bool,
4622 allow_new_preview: bool,
4623 window: &mut Window,
4624 cx: &mut Context<Self>,
4625 ) -> Entity<T>
4626 where
4627 T: ProjectItem,
4628 {
4629 let old_item_id = pane.read(cx).active_item().map(|item| item.item_id());
4630
4631 if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
4632 if !keep_old_preview
4633 && let Some(old_id) = old_item_id
4634 && old_id != item.item_id()
4635 {
4636 // switching to a different item, so unpreview old active item
4637 pane.update(cx, |pane, _| {
4638 pane.unpreview_item_if_preview(old_id);
4639 });
4640 }
4641
4642 self.activate_item(&item, activate_pane, focus_item, window, cx);
4643 if !allow_new_preview {
4644 pane.update(cx, |pane, _| {
4645 pane.unpreview_item_if_preview(item.item_id());
4646 });
4647 }
4648 return item;
4649 }
4650
4651 let item = pane.update(cx, |pane, cx| {
4652 cx.new(|cx| {
4653 T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx)
4654 })
4655 });
4656 let mut destination_index = None;
4657 pane.update(cx, |pane, cx| {
4658 if !keep_old_preview && let Some(old_id) = old_item_id {
4659 pane.unpreview_item_if_preview(old_id);
4660 }
4661 if allow_new_preview {
4662 destination_index = pane.replace_preview_item_id(item.item_id(), window, cx);
4663 }
4664 });
4665
4666 self.add_item(
4667 pane,
4668 Box::new(item.clone()),
4669 destination_index,
4670 activate_pane,
4671 focus_item,
4672 window,
4673 cx,
4674 );
4675 item
4676 }
4677
4678 pub fn open_shared_screen(
4679 &mut self,
4680 peer_id: PeerId,
4681 window: &mut Window,
4682 cx: &mut Context<Self>,
4683 ) {
4684 if let Some(shared_screen) =
4685 self.shared_screen_for_peer(peer_id, &self.active_pane, window, cx)
4686 {
4687 self.active_pane.update(cx, |pane, cx| {
4688 pane.add_item(Box::new(shared_screen), false, true, None, window, cx)
4689 });
4690 }
4691 }
4692
4693 pub fn activate_item(
4694 &mut self,
4695 item: &dyn ItemHandle,
4696 activate_pane: bool,
4697 focus_item: bool,
4698 window: &mut Window,
4699 cx: &mut App,
4700 ) -> bool {
4701 let result = self.panes.iter().find_map(|pane| {
4702 pane.read(cx)
4703 .index_for_item(item)
4704 .map(|ix| (pane.clone(), ix))
4705 });
4706 if let Some((pane, ix)) = result {
4707 pane.update(cx, |pane, cx| {
4708 pane.activate_item(ix, activate_pane, focus_item, window, cx)
4709 });
4710 true
4711 } else {
4712 false
4713 }
4714 }
4715
4716 fn activate_pane_at_index(
4717 &mut self,
4718 action: &ActivatePane,
4719 window: &mut Window,
4720 cx: &mut Context<Self>,
4721 ) {
4722 let panes = self.center.panes();
4723 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
4724 window.focus(&pane.focus_handle(cx), cx);
4725 } else {
4726 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx)
4727 .detach();
4728 }
4729 }
4730
4731 fn move_item_to_pane_at_index(
4732 &mut self,
4733 action: &MoveItemToPane,
4734 window: &mut Window,
4735 cx: &mut Context<Self>,
4736 ) {
4737 let panes = self.center.panes();
4738 let destination = match panes.get(action.destination) {
4739 Some(&destination) => destination.clone(),
4740 None => {
4741 if !action.clone && self.active_pane.read(cx).items_len() < 2 {
4742 return;
4743 }
4744 let direction = SplitDirection::Right;
4745 let split_off_pane = self
4746 .find_pane_in_direction(direction, cx)
4747 .unwrap_or_else(|| self.active_pane.clone());
4748 let new_pane = self.add_pane(window, cx);
4749 self.center.split(&split_off_pane, &new_pane, direction, cx);
4750 new_pane
4751 }
4752 };
4753
4754 if action.clone {
4755 if self
4756 .active_pane
4757 .read(cx)
4758 .active_item()
4759 .is_some_and(|item| item.can_split(cx))
4760 {
4761 clone_active_item(
4762 self.database_id(),
4763 &self.active_pane,
4764 &destination,
4765 action.focus,
4766 window,
4767 cx,
4768 );
4769 return;
4770 }
4771 }
4772 move_active_item(
4773 &self.active_pane,
4774 &destination,
4775 action.focus,
4776 true,
4777 window,
4778 cx,
4779 )
4780 }
4781
4782 pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) {
4783 let panes = self.center.panes();
4784 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
4785 let next_ix = (ix + 1) % panes.len();
4786 let next_pane = panes[next_ix].clone();
4787 window.focus(&next_pane.focus_handle(cx), cx);
4788 }
4789 }
4790
4791 pub fn activate_previous_pane(&mut self, window: &mut Window, cx: &mut App) {
4792 let panes = self.center.panes();
4793 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
4794 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
4795 let prev_pane = panes[prev_ix].clone();
4796 window.focus(&prev_pane.focus_handle(cx), cx);
4797 }
4798 }
4799
4800 pub fn activate_last_pane(&mut self, window: &mut Window, cx: &mut App) {
4801 let last_pane = self.center.last_pane();
4802 window.focus(&last_pane.focus_handle(cx), cx);
4803 }
4804
4805 pub fn activate_pane_in_direction(
4806 &mut self,
4807 direction: SplitDirection,
4808 window: &mut Window,
4809 cx: &mut App,
4810 ) {
4811 use ActivateInDirectionTarget as Target;
4812 enum Origin {
4813 Sidebar,
4814 LeftDock,
4815 RightDock,
4816 BottomDock,
4817 Center,
4818 }
4819
4820 let origin: Origin = if self
4821 .sidebar_focus_handle
4822 .as_ref()
4823 .is_some_and(|h| h.contains_focused(window, cx))
4824 {
4825 Origin::Sidebar
4826 } else {
4827 [
4828 (&self.left_dock, Origin::LeftDock),
4829 (&self.right_dock, Origin::RightDock),
4830 (&self.bottom_dock, Origin::BottomDock),
4831 ]
4832 .into_iter()
4833 .find_map(|(dock, origin)| {
4834 if dock.focus_handle(cx).contains_focused(window, cx) && dock.read(cx).is_open() {
4835 Some(origin)
4836 } else {
4837 None
4838 }
4839 })
4840 .unwrap_or(Origin::Center)
4841 };
4842
4843 let get_last_active_pane = || {
4844 let pane = self
4845 .last_active_center_pane
4846 .clone()
4847 .unwrap_or_else(|| {
4848 self.panes
4849 .first()
4850 .expect("There must be an active pane")
4851 .downgrade()
4852 })
4853 .upgrade()?;
4854 (pane.read(cx).items_len() != 0).then_some(pane)
4855 };
4856
4857 let try_dock =
4858 |dock: &Entity<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
4859
4860 let sidebar_target = self
4861 .sidebar_focus_handle
4862 .as_ref()
4863 .map(|h| Target::Sidebar(h.clone()));
4864
4865 let sidebar_on_right = self
4866 .multi_workspace
4867 .as_ref()
4868 .and_then(|mw| mw.upgrade())
4869 .map_or(false, |mw| {
4870 mw.read(cx).sidebar_side(cx) == SidebarSide::Right
4871 });
4872
4873 let away_from_sidebar = if sidebar_on_right {
4874 SplitDirection::Left
4875 } else {
4876 SplitDirection::Right
4877 };
4878
4879 let (near_dock, far_dock) = if sidebar_on_right {
4880 (&self.right_dock, &self.left_dock)
4881 } else {
4882 (&self.left_dock, &self.right_dock)
4883 };
4884
4885 let target = match (origin, direction) {
4886 (Origin::Sidebar, dir) if dir == away_from_sidebar => try_dock(near_dock)
4887 .or_else(|| get_last_active_pane().map(Target::Pane))
4888 .or_else(|| try_dock(&self.bottom_dock))
4889 .or_else(|| try_dock(far_dock)),
4890
4891 (Origin::Sidebar, _) => None,
4892
4893 // We're in the center, so we first try to go to a different pane,
4894 // otherwise try to go to a dock.
4895 (Origin::Center, direction) => {
4896 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
4897 Some(Target::Pane(pane))
4898 } else {
4899 match direction {
4900 SplitDirection::Up => None,
4901 SplitDirection::Down => try_dock(&self.bottom_dock),
4902 SplitDirection::Left => {
4903 let dock_target = try_dock(&self.left_dock);
4904 if sidebar_on_right {
4905 dock_target
4906 } else {
4907 dock_target.or(sidebar_target)
4908 }
4909 }
4910 SplitDirection::Right => {
4911 let dock_target = try_dock(&self.right_dock);
4912 if sidebar_on_right {
4913 dock_target.or(sidebar_target)
4914 } else {
4915 dock_target
4916 }
4917 }
4918 }
4919 }
4920 }
4921
4922 (Origin::LeftDock, SplitDirection::Right) => {
4923 if let Some(last_active_pane) = get_last_active_pane() {
4924 Some(Target::Pane(last_active_pane))
4925 } else {
4926 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
4927 }
4928 }
4929
4930 (Origin::LeftDock, SplitDirection::Left) => {
4931 if sidebar_on_right {
4932 None
4933 } else {
4934 sidebar_target
4935 }
4936 }
4937
4938 (Origin::LeftDock, SplitDirection::Down)
4939 | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
4940
4941 (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
4942 (Origin::BottomDock, SplitDirection::Left) => {
4943 let dock_target = try_dock(&self.left_dock);
4944 if sidebar_on_right {
4945 dock_target
4946 } else {
4947 dock_target.or(sidebar_target)
4948 }
4949 }
4950 (Origin::BottomDock, SplitDirection::Right) => {
4951 let dock_target = try_dock(&self.right_dock);
4952 if sidebar_on_right {
4953 dock_target.or(sidebar_target)
4954 } else {
4955 dock_target
4956 }
4957 }
4958
4959 (Origin::RightDock, SplitDirection::Left) => {
4960 if let Some(last_active_pane) = get_last_active_pane() {
4961 Some(Target::Pane(last_active_pane))
4962 } else {
4963 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
4964 }
4965 }
4966
4967 (Origin::RightDock, SplitDirection::Right) => {
4968 if sidebar_on_right {
4969 sidebar_target
4970 } else {
4971 None
4972 }
4973 }
4974
4975 _ => None,
4976 };
4977
4978 match target {
4979 Some(ActivateInDirectionTarget::Pane(pane)) => {
4980 let pane = pane.read(cx);
4981 if let Some(item) = pane.active_item() {
4982 item.item_focus_handle(cx).focus(window, cx);
4983 } else {
4984 log::error!(
4985 "Could not find a focus target when in switching focus in {direction} direction for a pane",
4986 );
4987 }
4988 }
4989 Some(ActivateInDirectionTarget::Dock(dock)) => {
4990 // Defer this to avoid a panic when the dock's active panel is already on the stack.
4991 window.defer(cx, move |window, cx| {
4992 let dock = dock.read(cx);
4993 if let Some(panel) = dock.active_panel() {
4994 panel.panel_focus_handle(cx).focus(window, cx);
4995 } else {
4996 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
4997 }
4998 })
4999 }
5000 Some(ActivateInDirectionTarget::Sidebar(focus_handle)) => {
5001 focus_handle.focus(window, cx);
5002 }
5003 None => {}
5004 }
5005 }
5006
5007 pub fn move_item_to_pane_in_direction(
5008 &mut self,
5009 action: &MoveItemToPaneInDirection,
5010 window: &mut Window,
5011 cx: &mut Context<Self>,
5012 ) {
5013 let destination = match self.find_pane_in_direction(action.direction, cx) {
5014 Some(destination) => destination,
5015 None => {
5016 if !action.clone && self.active_pane.read(cx).items_len() < 2 {
5017 return;
5018 }
5019 let new_pane = self.add_pane(window, cx);
5020 self.center
5021 .split(&self.active_pane, &new_pane, action.direction, cx);
5022 new_pane
5023 }
5024 };
5025
5026 if action.clone {
5027 if self
5028 .active_pane
5029 .read(cx)
5030 .active_item()
5031 .is_some_and(|item| item.can_split(cx))
5032 {
5033 clone_active_item(
5034 self.database_id(),
5035 &self.active_pane,
5036 &destination,
5037 action.focus,
5038 window,
5039 cx,
5040 );
5041 return;
5042 }
5043 }
5044 move_active_item(
5045 &self.active_pane,
5046 &destination,
5047 action.focus,
5048 true,
5049 window,
5050 cx,
5051 );
5052 }
5053
5054 pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
5055 self.center.bounding_box_for_pane(pane)
5056 }
5057
5058 pub fn find_pane_in_direction(
5059 &mut self,
5060 direction: SplitDirection,
5061 cx: &App,
5062 ) -> Option<Entity<Pane>> {
5063 self.center
5064 .find_pane_in_direction(&self.active_pane, direction, cx)
5065 .cloned()
5066 }
5067
5068 pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
5069 if let Some(to) = self.find_pane_in_direction(direction, cx) {
5070 self.center.swap(&self.active_pane, &to, cx);
5071 cx.notify();
5072 }
5073 }
5074
5075 pub fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
5076 if self
5077 .center
5078 .move_to_border(&self.active_pane, direction, cx)
5079 .unwrap()
5080 {
5081 cx.notify();
5082 }
5083 }
5084
5085 pub fn resize_pane(
5086 &mut self,
5087 axis: gpui::Axis,
5088 amount: Pixels,
5089 window: &mut Window,
5090 cx: &mut Context<Self>,
5091 ) {
5092 let docks = self.all_docks();
5093 let active_dock = docks
5094 .into_iter()
5095 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
5096
5097 if let Some(dock_entity) = active_dock {
5098 let dock = dock_entity.read(cx);
5099 let Some(panel_size) = self.dock_size(&dock, window, cx) else {
5100 return;
5101 };
5102 match dock.position() {
5103 DockPosition::Left => self.resize_left_dock(panel_size + amount, window, cx),
5104 DockPosition::Bottom => self.resize_bottom_dock(panel_size + amount, window, cx),
5105 DockPosition::Right => self.resize_right_dock(panel_size + amount, window, cx),
5106 }
5107 } else {
5108 self.center
5109 .resize(&self.active_pane, axis, amount, &self.bounds, cx);
5110 }
5111 cx.notify();
5112 }
5113
5114 pub fn reset_pane_sizes(&mut self, cx: &mut Context<Self>) {
5115 self.center.reset_pane_sizes(cx);
5116 cx.notify();
5117 }
5118
5119 fn handle_pane_focused(
5120 &mut self,
5121 pane: Entity<Pane>,
5122 window: &mut Window,
5123 cx: &mut Context<Self>,
5124 ) {
5125 // This is explicitly hoisted out of the following check for pane identity as
5126 // terminal panel panes are not registered as a center panes.
5127 self.status_bar.update(cx, |status_bar, cx| {
5128 status_bar.set_active_pane(&pane, window, cx);
5129 });
5130 if self.active_pane != pane {
5131 self.set_active_pane(&pane, window, cx);
5132 }
5133
5134 if self.last_active_center_pane.is_none() {
5135 self.last_active_center_pane = Some(pane.downgrade());
5136 }
5137
5138 // If this pane is in a dock, preserve that dock when dismissing zoomed items.
5139 // This prevents the dock from closing when focus events fire during window activation.
5140 // We also preserve any dock whose active panel itself has focus — this covers
5141 // panels like AgentPanel that don't implement `pane()` but can still be zoomed.
5142 let dock_to_preserve = self.all_docks().iter().find_map(|dock| {
5143 let dock_read = dock.read(cx);
5144 if let Some(panel) = dock_read.active_panel() {
5145 if panel.pane(cx).is_some_and(|dock_pane| dock_pane == pane)
5146 || panel.panel_focus_handle(cx).contains_focused(window, cx)
5147 {
5148 return Some(dock_read.position());
5149 }
5150 }
5151 None
5152 });
5153
5154 self.dismiss_zoomed_items_to_reveal(dock_to_preserve, window, cx);
5155 if pane.read(cx).is_zoomed() {
5156 self.zoomed = Some(pane.downgrade().into());
5157 } else {
5158 self.zoomed = None;
5159 }
5160 self.zoomed_position = None;
5161 cx.emit(Event::ZoomChanged);
5162 self.update_active_view_for_followers(window, cx);
5163 pane.update(cx, |pane, _| {
5164 pane.track_alternate_file_items();
5165 });
5166
5167 cx.notify();
5168 }
5169
5170 fn set_active_pane(
5171 &mut self,
5172 pane: &Entity<Pane>,
5173 window: &mut Window,
5174 cx: &mut Context<Self>,
5175 ) {
5176 self.active_pane = pane.clone();
5177 self.active_item_path_changed(true, window, cx);
5178 self.last_active_center_pane = Some(pane.downgrade());
5179 }
5180
5181 fn handle_panel_focused(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5182 self.update_active_view_for_followers(window, cx);
5183 }
5184
5185 fn handle_pane_event(
5186 &mut self,
5187 pane: &Entity<Pane>,
5188 event: &pane::Event,
5189 window: &mut Window,
5190 cx: &mut Context<Self>,
5191 ) {
5192 let mut serialize_workspace = true;
5193 match event {
5194 pane::Event::AddItem { item } => {
5195 item.added_to_pane(self, pane.clone(), window, cx);
5196 cx.emit(Event::ItemAdded {
5197 item: item.boxed_clone(),
5198 });
5199 }
5200 pane::Event::Split { direction, mode } => {
5201 match mode {
5202 SplitMode::ClonePane => {
5203 self.split_and_clone(pane.clone(), *direction, window, cx)
5204 .detach();
5205 }
5206 SplitMode::EmptyPane => {
5207 self.split_pane(pane.clone(), *direction, window, cx);
5208 }
5209 SplitMode::MovePane => {
5210 self.split_and_move(pane.clone(), *direction, window, cx);
5211 }
5212 };
5213 }
5214 pane::Event::JoinIntoNext => {
5215 self.join_pane_into_next(pane.clone(), window, cx);
5216 }
5217 pane::Event::JoinAll => {
5218 self.join_all_panes(window, cx);
5219 }
5220 pane::Event::Remove { focus_on_pane } => {
5221 self.remove_pane(pane.clone(), focus_on_pane.clone(), window, cx);
5222 }
5223 pane::Event::ActivateItem {
5224 local,
5225 focus_changed,
5226 } => {
5227 window.invalidate_character_coordinates();
5228
5229 pane.update(cx, |pane, _| {
5230 pane.track_alternate_file_items();
5231 });
5232 if *local {
5233 self.unfollow_in_pane(pane, window, cx);
5234 }
5235 serialize_workspace = *focus_changed || pane != self.active_pane();
5236 if pane == self.active_pane() {
5237 self.active_item_path_changed(*focus_changed, window, cx);
5238 self.update_active_view_for_followers(window, cx);
5239 } else if *local {
5240 self.set_active_pane(pane, window, cx);
5241 }
5242 }
5243 pane::Event::UserSavedItem { item, save_intent } => {
5244 cx.emit(Event::UserSavedItem {
5245 pane: pane.downgrade(),
5246 item: item.boxed_clone(),
5247 save_intent: *save_intent,
5248 });
5249 serialize_workspace = false;
5250 }
5251 pane::Event::ChangeItemTitle => {
5252 if *pane == self.active_pane {
5253 self.active_item_path_changed(false, window, cx);
5254 }
5255 serialize_workspace = false;
5256 }
5257 pane::Event::RemovedItem { item } => {
5258 cx.emit(Event::ActiveItemChanged);
5259 self.update_window_edited(window, cx);
5260 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id())
5261 && entry.get().entity_id() == pane.entity_id()
5262 {
5263 entry.remove();
5264 }
5265 cx.emit(Event::ItemRemoved {
5266 item_id: item.item_id(),
5267 });
5268 }
5269 pane::Event::Focus => {
5270 window.invalidate_character_coordinates();
5271 self.handle_pane_focused(pane.clone(), window, cx);
5272 }
5273 pane::Event::ZoomIn => {
5274 if *pane == self.active_pane {
5275 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
5276 if pane.read(cx).has_focus(window, cx) {
5277 self.zoomed = Some(pane.downgrade().into());
5278 self.zoomed_position = None;
5279 cx.emit(Event::ZoomChanged);
5280 }
5281 cx.notify();
5282 }
5283 }
5284 pane::Event::ZoomOut => {
5285 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
5286 if self.zoomed_position.is_none() {
5287 self.zoomed = None;
5288 cx.emit(Event::ZoomChanged);
5289 }
5290 cx.notify();
5291 }
5292 pane::Event::ItemPinned | pane::Event::ItemUnpinned => {}
5293 }
5294
5295 if serialize_workspace {
5296 self.serialize_workspace(window, cx);
5297 }
5298 }
5299
5300 pub fn unfollow_in_pane(
5301 &mut self,
5302 pane: &Entity<Pane>,
5303 window: &mut Window,
5304 cx: &mut Context<Workspace>,
5305 ) -> Option<CollaboratorId> {
5306 let leader_id = self.leader_for_pane(pane)?;
5307 self.unfollow(leader_id, window, cx);
5308 Some(leader_id)
5309 }
5310
5311 pub fn split_pane(
5312 &mut self,
5313 pane_to_split: Entity<Pane>,
5314 split_direction: SplitDirection,
5315 window: &mut Window,
5316 cx: &mut Context<Self>,
5317 ) -> Entity<Pane> {
5318 let new_pane = self.add_pane(window, cx);
5319 self.center
5320 .split(&pane_to_split, &new_pane, split_direction, cx);
5321 cx.notify();
5322 new_pane
5323 }
5324
5325 pub fn split_and_move(
5326 &mut self,
5327 pane: Entity<Pane>,
5328 direction: SplitDirection,
5329 window: &mut Window,
5330 cx: &mut Context<Self>,
5331 ) {
5332 let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx)) else {
5333 return;
5334 };
5335 let new_pane = self.add_pane(window, cx);
5336 new_pane.update(cx, |pane, cx| {
5337 pane.add_item(item, true, true, None, window, cx)
5338 });
5339 self.center.split(&pane, &new_pane, direction, cx);
5340 cx.notify();
5341 }
5342
5343 pub fn split_and_clone(
5344 &mut self,
5345 pane: Entity<Pane>,
5346 direction: SplitDirection,
5347 window: &mut Window,
5348 cx: &mut Context<Self>,
5349 ) -> Task<Option<Entity<Pane>>> {
5350 let Some(item) = pane.read(cx).active_item() else {
5351 return Task::ready(None);
5352 };
5353 if !item.can_split(cx) {
5354 return Task::ready(None);
5355 }
5356 let task = item.clone_on_split(self.database_id(), window, cx);
5357 cx.spawn_in(window, async move |this, cx| {
5358 if let Some(clone) = task.await {
5359 this.update_in(cx, |this, window, cx| {
5360 let new_pane = this.add_pane(window, cx);
5361 let nav_history = pane.read(cx).fork_nav_history();
5362 new_pane.update(cx, |pane, cx| {
5363 pane.set_nav_history(nav_history, cx);
5364 pane.add_item(clone, true, true, None, window, cx)
5365 });
5366 this.center.split(&pane, &new_pane, direction, cx);
5367 cx.notify();
5368 new_pane
5369 })
5370 .ok()
5371 } else {
5372 None
5373 }
5374 })
5375 }
5376
5377 pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5378 let active_item = self.active_pane.read(cx).active_item();
5379 for pane in &self.panes {
5380 join_pane_into_active(&self.active_pane, pane, window, cx);
5381 }
5382 if let Some(active_item) = active_item {
5383 self.activate_item(active_item.as_ref(), true, true, window, cx);
5384 }
5385 cx.notify();
5386 }
5387
5388 pub fn join_pane_into_next(
5389 &mut self,
5390 pane: Entity<Pane>,
5391 window: &mut Window,
5392 cx: &mut Context<Self>,
5393 ) {
5394 let next_pane = self
5395 .find_pane_in_direction(SplitDirection::Right, cx)
5396 .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
5397 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
5398 .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
5399 let Some(next_pane) = next_pane else {
5400 return;
5401 };
5402 move_all_items(&pane, &next_pane, window, cx);
5403 cx.notify();
5404 }
5405
5406 fn remove_pane(
5407 &mut self,
5408 pane: Entity<Pane>,
5409 focus_on: Option<Entity<Pane>>,
5410 window: &mut Window,
5411 cx: &mut Context<Self>,
5412 ) {
5413 if self.center.remove(&pane, cx).unwrap() {
5414 self.force_remove_pane(&pane, &focus_on, window, cx);
5415 self.unfollow_in_pane(&pane, window, cx);
5416 self.last_leaders_by_pane.remove(&pane.downgrade());
5417 for removed_item in pane.read(cx).items() {
5418 self.panes_by_item.remove(&removed_item.item_id());
5419 }
5420
5421 cx.notify();
5422 } else {
5423 self.active_item_path_changed(true, window, cx);
5424 }
5425 cx.emit(Event::PaneRemoved);
5426 }
5427
5428 pub fn panes_mut(&mut self) -> &mut [Entity<Pane>] {
5429 &mut self.panes
5430 }
5431
5432 pub fn panes(&self) -> &[Entity<Pane>] {
5433 &self.panes
5434 }
5435
5436 pub fn active_pane(&self) -> &Entity<Pane> {
5437 &self.active_pane
5438 }
5439
5440 pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> {
5441 for dock in self.all_docks() {
5442 if dock.focus_handle(cx).contains_focused(window, cx)
5443 && let Some(pane) = dock
5444 .read(cx)
5445 .active_panel()
5446 .and_then(|panel| panel.pane(cx))
5447 {
5448 return pane;
5449 }
5450 }
5451 self.active_pane().clone()
5452 }
5453
5454 pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
5455 self.find_pane_in_direction(SplitDirection::Right, cx)
5456 .unwrap_or_else(|| {
5457 self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx)
5458 })
5459 }
5460
5461 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> {
5462 self.pane_for_item_id(handle.item_id())
5463 }
5464
5465 pub fn pane_for_item_id(&self, item_id: EntityId) -> Option<Entity<Pane>> {
5466 let weak_pane = self.panes_by_item.get(&item_id)?;
5467 weak_pane.upgrade()
5468 }
5469
5470 pub fn pane_for_entity_id(&self, entity_id: EntityId) -> Option<Entity<Pane>> {
5471 self.panes
5472 .iter()
5473 .find(|pane| pane.entity_id() == entity_id)
5474 .cloned()
5475 }
5476
5477 fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
5478 self.follower_states.retain(|leader_id, state| {
5479 if *leader_id == CollaboratorId::PeerId(peer_id) {
5480 for item in state.items_by_leader_view_id.values() {
5481 item.view.set_leader_id(None, window, cx);
5482 }
5483 false
5484 } else {
5485 true
5486 }
5487 });
5488 cx.notify();
5489 }
5490
5491 pub fn start_following(
5492 &mut self,
5493 leader_id: impl Into<CollaboratorId>,
5494 window: &mut Window,
5495 cx: &mut Context<Self>,
5496 ) -> Option<Task<Result<()>>> {
5497 let leader_id = leader_id.into();
5498 let pane = self.active_pane().clone();
5499
5500 self.last_leaders_by_pane
5501 .insert(pane.downgrade(), leader_id);
5502 self.unfollow(leader_id, window, cx);
5503 self.unfollow_in_pane(&pane, window, cx);
5504 self.follower_states.insert(
5505 leader_id,
5506 FollowerState {
5507 center_pane: pane.clone(),
5508 dock_pane: None,
5509 active_view_id: None,
5510 items_by_leader_view_id: Default::default(),
5511 },
5512 );
5513 cx.notify();
5514
5515 match leader_id {
5516 CollaboratorId::PeerId(leader_peer_id) => {
5517 let room_id = self.active_call()?.room_id(cx)?;
5518 let project_id = self.project.read(cx).remote_id();
5519 let request = self.app_state.client.request(proto::Follow {
5520 room_id,
5521 project_id,
5522 leader_id: Some(leader_peer_id),
5523 });
5524
5525 Some(cx.spawn_in(window, async move |this, cx| {
5526 let response = request.await?;
5527 this.update(cx, |this, _| {
5528 let state = this
5529 .follower_states
5530 .get_mut(&leader_id)
5531 .context("following interrupted")?;
5532 state.active_view_id = response
5533 .active_view
5534 .as_ref()
5535 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
5536 anyhow::Ok(())
5537 })??;
5538 if let Some(view) = response.active_view {
5539 Self::add_view_from_leader(this.clone(), leader_peer_id, &view, cx).await?;
5540 }
5541 this.update_in(cx, |this, window, cx| {
5542 this.leader_updated(leader_id, window, cx)
5543 })?;
5544 Ok(())
5545 }))
5546 }
5547 CollaboratorId::Agent => {
5548 self.leader_updated(leader_id, window, cx)?;
5549 Some(Task::ready(Ok(())))
5550 }
5551 }
5552 }
5553
5554 pub fn follow_next_collaborator(
5555 &mut self,
5556 _: &FollowNextCollaborator,
5557 window: &mut Window,
5558 cx: &mut Context<Self>,
5559 ) {
5560 let collaborators = self.project.read(cx).collaborators();
5561 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
5562 let mut collaborators = collaborators.keys().copied();
5563 for peer_id in collaborators.by_ref() {
5564 if CollaboratorId::PeerId(peer_id) == leader_id {
5565 break;
5566 }
5567 }
5568 collaborators.next().map(CollaboratorId::PeerId)
5569 } else if let Some(last_leader_id) =
5570 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
5571 {
5572 match last_leader_id {
5573 CollaboratorId::PeerId(peer_id) => {
5574 if collaborators.contains_key(peer_id) {
5575 Some(*last_leader_id)
5576 } else {
5577 None
5578 }
5579 }
5580 CollaboratorId::Agent => Some(CollaboratorId::Agent),
5581 }
5582 } else {
5583 None
5584 };
5585
5586 let pane = self.active_pane.clone();
5587 let Some(leader_id) = next_leader_id.or_else(|| {
5588 Some(CollaboratorId::PeerId(
5589 collaborators.keys().copied().next()?,
5590 ))
5591 }) else {
5592 return;
5593 };
5594 if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
5595 return;
5596 }
5597 if let Some(task) = self.start_following(leader_id, window, cx) {
5598 task.detach_and_log_err(cx)
5599 }
5600 }
5601
5602 pub fn follow(
5603 &mut self,
5604 leader_id: impl Into<CollaboratorId>,
5605 window: &mut Window,
5606 cx: &mut Context<Self>,
5607 ) {
5608 let leader_id = leader_id.into();
5609
5610 if let CollaboratorId::PeerId(peer_id) = leader_id {
5611 let Some(active_call) = GlobalAnyActiveCall::try_global(cx) else {
5612 return;
5613 };
5614 let Some(remote_participant) =
5615 active_call.0.remote_participant_for_peer_id(peer_id, cx)
5616 else {
5617 return;
5618 };
5619
5620 let project = self.project.read(cx);
5621
5622 let other_project_id = match remote_participant.location {
5623 ParticipantLocation::External => None,
5624 ParticipantLocation::UnsharedProject => None,
5625 ParticipantLocation::SharedProject { project_id } => {
5626 if Some(project_id) == project.remote_id() {
5627 None
5628 } else {
5629 Some(project_id)
5630 }
5631 }
5632 };
5633
5634 // if they are active in another project, follow there.
5635 if let Some(project_id) = other_project_id {
5636 let app_state = self.app_state.clone();
5637 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
5638 .detach_and_prompt_err("Failed to join project", window, cx, |error, _, _| {
5639 Some(format!("{error:#}"))
5640 });
5641 }
5642 }
5643
5644 // if you're already following, find the right pane and focus it.
5645 if let Some(follower_state) = self.follower_states.get(&leader_id) {
5646 window.focus(&follower_state.pane().focus_handle(cx), cx);
5647
5648 return;
5649 }
5650
5651 // Otherwise, follow.
5652 if let Some(task) = self.start_following(leader_id, window, cx) {
5653 task.detach_and_log_err(cx)
5654 }
5655 }
5656
5657 pub fn unfollow(
5658 &mut self,
5659 leader_id: impl Into<CollaboratorId>,
5660 window: &mut Window,
5661 cx: &mut Context<Self>,
5662 ) -> Option<()> {
5663 cx.notify();
5664
5665 let leader_id = leader_id.into();
5666 let state = self.follower_states.remove(&leader_id)?;
5667 for (_, item) in state.items_by_leader_view_id {
5668 item.view.set_leader_id(None, window, cx);
5669 }
5670
5671 if let CollaboratorId::PeerId(leader_peer_id) = leader_id {
5672 let project_id = self.project.read(cx).remote_id();
5673 let room_id = self.active_call()?.room_id(cx)?;
5674 self.app_state
5675 .client
5676 .send(proto::Unfollow {
5677 room_id,
5678 project_id,
5679 leader_id: Some(leader_peer_id),
5680 })
5681 .log_err();
5682 }
5683
5684 Some(())
5685 }
5686
5687 pub fn is_being_followed(&self, id: impl Into<CollaboratorId>) -> bool {
5688 self.follower_states.contains_key(&id.into())
5689 }
5690
5691 fn active_item_path_changed(
5692 &mut self,
5693 focus_changed: bool,
5694 window: &mut Window,
5695 cx: &mut Context<Self>,
5696 ) {
5697 cx.emit(Event::ActiveItemChanged);
5698 let active_entry = self.active_project_path(cx);
5699 self.project.update(cx, |project, cx| {
5700 project.set_active_path(active_entry.clone(), cx)
5701 });
5702
5703 if focus_changed && let Some(project_path) = &active_entry {
5704 let git_store_entity = self.project.read(cx).git_store().clone();
5705 git_store_entity.update(cx, |git_store, cx| {
5706 git_store.set_active_repo_for_path(project_path, cx);
5707 });
5708 }
5709
5710 self.update_window_title(window, cx);
5711 }
5712
5713 fn update_window_title(&mut self, window: &mut Window, cx: &mut App) {
5714 let project = self.project().read(cx);
5715 let mut title = String::new();
5716
5717 for (i, worktree) in project.visible_worktrees(cx).enumerate() {
5718 let name = {
5719 let settings_location = SettingsLocation {
5720 worktree_id: worktree.read(cx).id(),
5721 path: RelPath::empty(),
5722 };
5723
5724 let settings = WorktreeSettings::get(Some(settings_location), cx);
5725 match &settings.project_name {
5726 Some(name) => name.as_str(),
5727 None => worktree.read(cx).root_name_str(),
5728 }
5729 };
5730 if i > 0 {
5731 title.push_str(", ");
5732 }
5733 title.push_str(name);
5734 }
5735
5736 if title.is_empty() {
5737 title = "empty project".to_string();
5738 }
5739
5740 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
5741 let filename = path.path.file_name().or_else(|| {
5742 Some(
5743 project
5744 .worktree_for_id(path.worktree_id, cx)?
5745 .read(cx)
5746 .root_name_str(),
5747 )
5748 });
5749
5750 if let Some(filename) = filename {
5751 title.push_str(" — ");
5752 title.push_str(filename.as_ref());
5753 }
5754 }
5755
5756 if project.is_via_collab() {
5757 title.push_str(" ↙");
5758 } else if project.is_shared() {
5759 title.push_str(" ↗");
5760 }
5761
5762 if let Some(last_title) = self.last_window_title.as_ref()
5763 && &title == last_title
5764 {
5765 return;
5766 }
5767 window.set_window_title(&title);
5768 SystemWindowTabController::update_tab_title(
5769 cx,
5770 window.window_handle().window_id(),
5771 SharedString::from(&title),
5772 );
5773 self.last_window_title = Some(title);
5774 }
5775
5776 fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
5777 let is_edited = !self.project.read(cx).is_disconnected(cx) && !self.dirty_items.is_empty();
5778 if is_edited != self.window_edited {
5779 self.window_edited = is_edited;
5780 window.set_window_edited(self.window_edited)
5781 }
5782 }
5783
5784 fn update_item_dirty_state(
5785 &mut self,
5786 item: &dyn ItemHandle,
5787 window: &mut Window,
5788 cx: &mut App,
5789 ) {
5790 let is_dirty = item.is_dirty(cx);
5791 let item_id = item.item_id();
5792 let was_dirty = self.dirty_items.contains_key(&item_id);
5793 if is_dirty == was_dirty {
5794 return;
5795 }
5796 if was_dirty {
5797 self.dirty_items.remove(&item_id);
5798 self.update_window_edited(window, cx);
5799 return;
5800 }
5801
5802 let workspace = self.weak_handle();
5803 let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
5804 return;
5805 };
5806 let on_release_callback = Box::new(move |cx: &mut App| {
5807 window_handle
5808 .update(cx, |_, window, cx| {
5809 workspace
5810 .update(cx, |workspace, cx| {
5811 workspace.dirty_items.remove(&item_id);
5812 workspace.update_window_edited(window, cx)
5813 })
5814 .ok();
5815 })
5816 .ok();
5817 });
5818
5819 let s = item.on_release(cx, on_release_callback);
5820 self.dirty_items.insert(item_id, s);
5821 self.update_window_edited(window, cx);
5822 }
5823
5824 fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
5825 if self.notifications.is_empty() {
5826 None
5827 } else {
5828 Some(
5829 div()
5830 .absolute()
5831 .right_3()
5832 .bottom_3()
5833 .w_112()
5834 .h_full()
5835 .flex()
5836 .flex_col()
5837 .justify_end()
5838 .gap_2()
5839 .children(
5840 self.notifications
5841 .iter()
5842 .map(|(_, notification)| notification.clone().into_any()),
5843 ),
5844 )
5845 }
5846 }
5847
5848 // RPC handlers
5849
5850 fn active_view_for_follower(
5851 &self,
5852 follower_project_id: Option<u64>,
5853 window: &mut Window,
5854 cx: &mut Context<Self>,
5855 ) -> Option<proto::View> {
5856 let (item, panel_id) = self.active_item_for_followers(window, cx);
5857 let item = item?;
5858 let leader_id = self
5859 .pane_for(&*item)
5860 .and_then(|pane| self.leader_for_pane(&pane));
5861 let leader_peer_id = match leader_id {
5862 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
5863 Some(CollaboratorId::Agent) | None => None,
5864 };
5865
5866 let item_handle = item.to_followable_item_handle(cx)?;
5867 let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
5868 let variant = item_handle.to_state_proto(window, cx)?;
5869
5870 if item_handle.is_project_item(window, cx)
5871 && (follower_project_id.is_none()
5872 || follower_project_id != self.project.read(cx).remote_id())
5873 {
5874 return None;
5875 }
5876
5877 Some(proto::View {
5878 id: id.to_proto(),
5879 leader_id: leader_peer_id,
5880 variant: Some(variant),
5881 panel_id: panel_id.map(|id| id as i32),
5882 })
5883 }
5884
5885 fn handle_follow(
5886 &mut self,
5887 follower_project_id: Option<u64>,
5888 window: &mut Window,
5889 cx: &mut Context<Self>,
5890 ) -> proto::FollowResponse {
5891 let active_view = self.active_view_for_follower(follower_project_id, window, cx);
5892
5893 cx.notify();
5894 proto::FollowResponse {
5895 views: active_view.iter().cloned().collect(),
5896 active_view,
5897 }
5898 }
5899
5900 fn handle_update_followers(
5901 &mut self,
5902 leader_id: PeerId,
5903 message: proto::UpdateFollowers,
5904 _window: &mut Window,
5905 _cx: &mut Context<Self>,
5906 ) {
5907 self.leader_updates_tx
5908 .unbounded_send((leader_id, message))
5909 .ok();
5910 }
5911
5912 async fn process_leader_update(
5913 this: &WeakEntity<Self>,
5914 leader_id: PeerId,
5915 update: proto::UpdateFollowers,
5916 cx: &mut AsyncWindowContext,
5917 ) -> Result<()> {
5918 match update.variant.context("invalid update")? {
5919 proto::update_followers::Variant::CreateView(view) => {
5920 let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
5921 let should_add_view = this.update(cx, |this, _| {
5922 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
5923 anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
5924 } else {
5925 anyhow::Ok(false)
5926 }
5927 })??;
5928
5929 if should_add_view {
5930 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
5931 }
5932 }
5933 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
5934 let should_add_view = this.update(cx, |this, _| {
5935 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
5936 state.active_view_id = update_active_view
5937 .view
5938 .as_ref()
5939 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
5940
5941 if state.active_view_id.is_some_and(|view_id| {
5942 !state.items_by_leader_view_id.contains_key(&view_id)
5943 }) {
5944 anyhow::Ok(true)
5945 } else {
5946 anyhow::Ok(false)
5947 }
5948 } else {
5949 anyhow::Ok(false)
5950 }
5951 })??;
5952
5953 if should_add_view && let Some(view) = update_active_view.view {
5954 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
5955 }
5956 }
5957 proto::update_followers::Variant::UpdateView(update_view) => {
5958 let variant = update_view.variant.context("missing update view variant")?;
5959 let id = update_view.id.context("missing update view id")?;
5960 let mut tasks = Vec::new();
5961 this.update_in(cx, |this, window, cx| {
5962 let project = this.project.clone();
5963 if let Some(state) = this.follower_states.get(&leader_id.into()) {
5964 let view_id = ViewId::from_proto(id.clone())?;
5965 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
5966 tasks.push(item.view.apply_update_proto(
5967 &project,
5968 variant.clone(),
5969 window,
5970 cx,
5971 ));
5972 }
5973 }
5974 anyhow::Ok(())
5975 })??;
5976 try_join_all(tasks).await.log_err();
5977 }
5978 }
5979 this.update_in(cx, |this, window, cx| {
5980 this.leader_updated(leader_id, window, cx)
5981 })?;
5982 Ok(())
5983 }
5984
5985 async fn add_view_from_leader(
5986 this: WeakEntity<Self>,
5987 leader_id: PeerId,
5988 view: &proto::View,
5989 cx: &mut AsyncWindowContext,
5990 ) -> Result<()> {
5991 let this = this.upgrade().context("workspace dropped")?;
5992
5993 let Some(id) = view.id.clone() else {
5994 anyhow::bail!("no id for view");
5995 };
5996 let id = ViewId::from_proto(id)?;
5997 let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
5998
5999 let pane = this.update(cx, |this, _cx| {
6000 let state = this
6001 .follower_states
6002 .get(&leader_id.into())
6003 .context("stopped following")?;
6004 anyhow::Ok(state.pane().clone())
6005 })?;
6006 let existing_item = pane.update_in(cx, |pane, window, cx| {
6007 let client = this.read(cx).client().clone();
6008 pane.items().find_map(|item| {
6009 let item = item.to_followable_item_handle(cx)?;
6010 if item.remote_id(&client, window, cx) == Some(id) {
6011 Some(item)
6012 } else {
6013 None
6014 }
6015 })
6016 })?;
6017 let item = if let Some(existing_item) = existing_item {
6018 existing_item
6019 } else {
6020 let variant = view.variant.clone();
6021 anyhow::ensure!(variant.is_some(), "missing view variant");
6022
6023 let task = cx.update(|window, cx| {
6024 FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx)
6025 })?;
6026
6027 let Some(task) = task else {
6028 anyhow::bail!(
6029 "failed to construct view from leader (maybe from a different version of zed?)"
6030 );
6031 };
6032
6033 let mut new_item = task.await?;
6034 pane.update_in(cx, |pane, window, cx| {
6035 let mut item_to_remove = None;
6036 for (ix, item) in pane.items().enumerate() {
6037 if let Some(item) = item.to_followable_item_handle(cx) {
6038 match new_item.dedup(item.as_ref(), window, cx) {
6039 Some(item::Dedup::KeepExisting) => {
6040 new_item =
6041 item.boxed_clone().to_followable_item_handle(cx).unwrap();
6042 break;
6043 }
6044 Some(item::Dedup::ReplaceExisting) => {
6045 item_to_remove = Some((ix, item.item_id()));
6046 break;
6047 }
6048 None => {}
6049 }
6050 }
6051 }
6052
6053 if let Some((ix, id)) = item_to_remove {
6054 pane.remove_item(id, false, false, window, cx);
6055 pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx);
6056 }
6057 })?;
6058
6059 new_item
6060 };
6061
6062 this.update_in(cx, |this, window, cx| {
6063 let state = this.follower_states.get_mut(&leader_id.into())?;
6064 item.set_leader_id(Some(leader_id.into()), window, cx);
6065 state.items_by_leader_view_id.insert(
6066 id,
6067 FollowerView {
6068 view: item,
6069 location: panel_id,
6070 },
6071 );
6072
6073 Some(())
6074 })
6075 .context("no follower state")?;
6076
6077 Ok(())
6078 }
6079
6080 fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
6081 let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else {
6082 return;
6083 };
6084
6085 if let Some(agent_location) = self.project.read(cx).agent_location() {
6086 let buffer_entity_id = agent_location.buffer.entity_id();
6087 let view_id = ViewId {
6088 creator: CollaboratorId::Agent,
6089 id: buffer_entity_id.as_u64(),
6090 };
6091 follower_state.active_view_id = Some(view_id);
6092
6093 let item = match follower_state.items_by_leader_view_id.entry(view_id) {
6094 hash_map::Entry::Occupied(entry) => Some(entry.into_mut()),
6095 hash_map::Entry::Vacant(entry) => {
6096 let existing_view =
6097 follower_state
6098 .center_pane
6099 .read(cx)
6100 .items()
6101 .find_map(|item| {
6102 let item = item.to_followable_item_handle(cx)?;
6103 if item.buffer_kind(cx) == ItemBufferKind::Singleton
6104 && item.project_item_model_ids(cx).as_slice()
6105 == [buffer_entity_id]
6106 {
6107 Some(item)
6108 } else {
6109 None
6110 }
6111 });
6112 let view = existing_view.or_else(|| {
6113 agent_location.buffer.upgrade().and_then(|buffer| {
6114 cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| {
6115 registry.build_item(buffer, self.project.clone(), None, window, cx)
6116 })?
6117 .to_followable_item_handle(cx)
6118 })
6119 });
6120
6121 view.map(|view| {
6122 entry.insert(FollowerView {
6123 view,
6124 location: None,
6125 })
6126 })
6127 }
6128 };
6129
6130 if let Some(item) = item {
6131 item.view
6132 .set_leader_id(Some(CollaboratorId::Agent), window, cx);
6133 item.view
6134 .update_agent_location(agent_location.position, window, cx);
6135 }
6136 } else {
6137 follower_state.active_view_id = None;
6138 }
6139
6140 self.leader_updated(CollaboratorId::Agent, window, cx);
6141 }
6142
6143 pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
6144 let mut is_project_item = true;
6145 let mut update = proto::UpdateActiveView::default();
6146 if window.is_window_active() {
6147 let (active_item, panel_id) = self.active_item_for_followers(window, cx);
6148
6149 if let Some(item) = active_item
6150 && item.item_focus_handle(cx).contains_focused(window, cx)
6151 {
6152 let leader_id = self
6153 .pane_for(&*item)
6154 .and_then(|pane| self.leader_for_pane(&pane));
6155 let leader_peer_id = match leader_id {
6156 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
6157 Some(CollaboratorId::Agent) | None => None,
6158 };
6159
6160 if let Some(item) = item.to_followable_item_handle(cx) {
6161 let id = item
6162 .remote_id(&self.app_state.client, window, cx)
6163 .map(|id| id.to_proto());
6164
6165 if let Some(id) = id
6166 && let Some(variant) = item.to_state_proto(window, cx)
6167 {
6168 let view = Some(proto::View {
6169 id,
6170 leader_id: leader_peer_id,
6171 variant: Some(variant),
6172 panel_id: panel_id.map(|id| id as i32),
6173 });
6174
6175 is_project_item = item.is_project_item(window, cx);
6176 update = proto::UpdateActiveView { view };
6177 };
6178 }
6179 }
6180 }
6181
6182 let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
6183 if active_view_id != self.last_active_view_id.as_ref() {
6184 self.last_active_view_id = active_view_id.cloned();
6185 self.update_followers(
6186 is_project_item,
6187 proto::update_followers::Variant::UpdateActiveView(update),
6188 window,
6189 cx,
6190 );
6191 }
6192 }
6193
6194 fn active_item_for_followers(
6195 &self,
6196 window: &mut Window,
6197 cx: &mut App,
6198 ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
6199 let mut active_item = None;
6200 let mut panel_id = None;
6201 for dock in self.all_docks() {
6202 if dock.focus_handle(cx).contains_focused(window, cx)
6203 && let Some(panel) = dock.read(cx).active_panel()
6204 && let Some(pane) = panel.pane(cx)
6205 && let Some(item) = pane.read(cx).active_item()
6206 {
6207 active_item = Some(item);
6208 panel_id = panel.remote_id();
6209 break;
6210 }
6211 }
6212
6213 if active_item.is_none() {
6214 active_item = self.active_pane().read(cx).active_item();
6215 }
6216 (active_item, panel_id)
6217 }
6218
6219 fn update_followers(
6220 &self,
6221 project_only: bool,
6222 update: proto::update_followers::Variant,
6223 _: &mut Window,
6224 cx: &mut App,
6225 ) -> Option<()> {
6226 // If this update only applies to for followers in the current project,
6227 // then skip it unless this project is shared. If it applies to all
6228 // followers, regardless of project, then set `project_id` to none,
6229 // indicating that it goes to all followers.
6230 let project_id = if project_only {
6231 Some(self.project.read(cx).remote_id()?)
6232 } else {
6233 None
6234 };
6235 self.app_state().workspace_store.update(cx, |store, cx| {
6236 store.update_followers(project_id, update, cx)
6237 })
6238 }
6239
6240 pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<CollaboratorId> {
6241 self.follower_states.iter().find_map(|(leader_id, state)| {
6242 if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
6243 Some(*leader_id)
6244 } else {
6245 None
6246 }
6247 })
6248 }
6249
6250 fn leader_updated(
6251 &mut self,
6252 leader_id: impl Into<CollaboratorId>,
6253 window: &mut Window,
6254 cx: &mut Context<Self>,
6255 ) -> Option<Box<dyn ItemHandle>> {
6256 cx.notify();
6257
6258 let leader_id = leader_id.into();
6259 let (panel_id, item) = match leader_id {
6260 CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?,
6261 CollaboratorId::Agent => (None, self.active_item_for_agent()?),
6262 };
6263
6264 let state = self.follower_states.get(&leader_id)?;
6265 let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
6266 let pane;
6267 if let Some(panel_id) = panel_id {
6268 pane = self
6269 .activate_panel_for_proto_id(panel_id, window, cx)?
6270 .pane(cx)?;
6271 let state = self.follower_states.get_mut(&leader_id)?;
6272 state.dock_pane = Some(pane.clone());
6273 } else {
6274 pane = state.center_pane.clone();
6275 let state = self.follower_states.get_mut(&leader_id)?;
6276 if let Some(dock_pane) = state.dock_pane.take() {
6277 transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx);
6278 }
6279 }
6280
6281 pane.update(cx, |pane, cx| {
6282 let focus_active_item = pane.has_focus(window, cx) || transfer_focus;
6283 if let Some(index) = pane.index_for_item(item.as_ref()) {
6284 pane.activate_item(index, false, false, window, cx);
6285 } else {
6286 pane.add_item(item.boxed_clone(), false, false, None, window, cx)
6287 }
6288
6289 if focus_active_item {
6290 pane.focus_active_item(window, cx)
6291 }
6292 });
6293
6294 Some(item)
6295 }
6296
6297 fn active_item_for_agent(&self) -> Option<Box<dyn ItemHandle>> {
6298 let state = self.follower_states.get(&CollaboratorId::Agent)?;
6299 let active_view_id = state.active_view_id?;
6300 Some(
6301 state
6302 .items_by_leader_view_id
6303 .get(&active_view_id)?
6304 .view
6305 .boxed_clone(),
6306 )
6307 }
6308
6309 fn active_item_for_peer(
6310 &self,
6311 peer_id: PeerId,
6312 window: &mut Window,
6313 cx: &mut Context<Self>,
6314 ) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
6315 let call = self.active_call()?;
6316 let participant = call.remote_participant_for_peer_id(peer_id, cx)?;
6317 let leader_in_this_app;
6318 let leader_in_this_project;
6319 match participant.location {
6320 ParticipantLocation::SharedProject { project_id } => {
6321 leader_in_this_app = true;
6322 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
6323 }
6324 ParticipantLocation::UnsharedProject => {
6325 leader_in_this_app = true;
6326 leader_in_this_project = false;
6327 }
6328 ParticipantLocation::External => {
6329 leader_in_this_app = false;
6330 leader_in_this_project = false;
6331 }
6332 };
6333 let state = self.follower_states.get(&peer_id.into())?;
6334 let mut item_to_activate = None;
6335 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
6336 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id)
6337 && (leader_in_this_project || !item.view.is_project_item(window, cx))
6338 {
6339 item_to_activate = Some((item.location, item.view.boxed_clone()));
6340 }
6341 } else if let Some(shared_screen) =
6342 self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx)
6343 {
6344 item_to_activate = Some((None, Box::new(shared_screen)));
6345 }
6346 item_to_activate
6347 }
6348
6349 fn shared_screen_for_peer(
6350 &self,
6351 peer_id: PeerId,
6352 pane: &Entity<Pane>,
6353 window: &mut Window,
6354 cx: &mut App,
6355 ) -> Option<Entity<SharedScreen>> {
6356 self.active_call()?
6357 .create_shared_screen(peer_id, pane, window, cx)
6358 }
6359
6360 pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
6361 if window.is_window_active() {
6362 self.update_active_view_for_followers(window, cx);
6363
6364 if let Some(database_id) = self.database_id {
6365 let db = WorkspaceDb::global(cx);
6366 cx.background_spawn(async move { db.update_timestamp(database_id).await })
6367 .detach();
6368 }
6369 } else {
6370 for pane in &self.panes {
6371 pane.update(cx, |pane, cx| {
6372 if let Some(item) = pane.active_item() {
6373 item.workspace_deactivated(window, cx);
6374 }
6375 for item in pane.items() {
6376 if matches!(
6377 item.workspace_settings(cx).autosave,
6378 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
6379 ) {
6380 Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
6381 .detach_and_log_err(cx);
6382 }
6383 }
6384 });
6385 }
6386 }
6387 }
6388
6389 pub fn active_call(&self) -> Option<&dyn AnyActiveCall> {
6390 self.active_call.as_ref().map(|(call, _)| &*call.0)
6391 }
6392
6393 pub fn active_global_call(&self) -> Option<GlobalAnyActiveCall> {
6394 self.active_call.as_ref().map(|(call, _)| call.clone())
6395 }
6396
6397 fn on_active_call_event(
6398 &mut self,
6399 event: &ActiveCallEvent,
6400 window: &mut Window,
6401 cx: &mut Context<Self>,
6402 ) {
6403 match event {
6404 ActiveCallEvent::ParticipantLocationChanged { participant_id }
6405 | ActiveCallEvent::RemoteVideoTracksChanged { participant_id } => {
6406 self.leader_updated(participant_id, window, cx);
6407 }
6408 }
6409 }
6410
6411 pub fn database_id(&self) -> Option<WorkspaceId> {
6412 self.database_id
6413 }
6414
6415 #[cfg(any(test, feature = "test-support"))]
6416 pub(crate) fn set_database_id(&mut self, id: WorkspaceId) {
6417 self.database_id = Some(id);
6418 }
6419
6420 pub fn session_id(&self) -> Option<String> {
6421 self.session_id.clone()
6422 }
6423
6424 fn save_window_bounds(&self, window: &mut Window, cx: &mut App) -> Task<()> {
6425 let Some(display) = window.display(cx) else {
6426 return Task::ready(());
6427 };
6428 let Ok(display_uuid) = display.uuid() else {
6429 return Task::ready(());
6430 };
6431
6432 let window_bounds = window.inner_window_bounds();
6433 let database_id = self.database_id;
6434 let has_paths = !self.root_paths(cx).is_empty();
6435 let db = WorkspaceDb::global(cx);
6436 let kvp = db::kvp::KeyValueStore::global(cx);
6437
6438 cx.background_executor().spawn(async move {
6439 if !has_paths {
6440 persistence::write_default_window_bounds(&kvp, window_bounds, display_uuid)
6441 .await
6442 .log_err();
6443 }
6444 if let Some(database_id) = database_id {
6445 db.set_window_open_status(
6446 database_id,
6447 SerializedWindowBounds(window_bounds),
6448 display_uuid,
6449 )
6450 .await
6451 .log_err();
6452 } else {
6453 persistence::write_default_window_bounds(&kvp, window_bounds, display_uuid)
6454 .await
6455 .log_err();
6456 }
6457 })
6458 }
6459
6460 /// Bypass the 200ms serialization throttle and write workspace state to
6461 /// the DB immediately. Returns a task the caller can await to ensure the
6462 /// write completes. Used by the quit handler so the most recent state
6463 /// isn't lost to a pending throttle timer when the process exits.
6464 pub fn flush_serialization(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
6465 self._schedule_serialize_workspace.take();
6466 self._serialize_workspace_task.take();
6467 self.bounds_save_task_queued.take();
6468
6469 let bounds_task = self.save_window_bounds(window, cx);
6470 let serialize_task = self.serialize_workspace_internal(window, cx);
6471 cx.spawn(async move |_| {
6472 bounds_task.await;
6473 serialize_task.await;
6474 })
6475 }
6476
6477 pub fn root_paths(&self, cx: &App) -> Vec<Arc<Path>> {
6478 let project = self.project().read(cx);
6479 project
6480 .visible_worktrees(cx)
6481 .map(|worktree| worktree.read(cx).abs_path())
6482 .collect::<Vec<_>>()
6483 }
6484
6485 fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
6486 match member {
6487 Member::Axis(PaneAxis { members, .. }) => {
6488 for child in members.iter() {
6489 self.remove_panes(child.clone(), window, cx)
6490 }
6491 }
6492 Member::Pane(pane) => {
6493 self.force_remove_pane(&pane, &None, window, cx);
6494 }
6495 }
6496 }
6497
6498 fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
6499 self.session_id.take();
6500 self.serialize_workspace_internal(window, cx)
6501 }
6502
6503 fn force_remove_pane(
6504 &mut self,
6505 pane: &Entity<Pane>,
6506 focus_on: &Option<Entity<Pane>>,
6507 window: &mut Window,
6508 cx: &mut Context<Workspace>,
6509 ) {
6510 self.panes.retain(|p| p != pane);
6511 if let Some(focus_on) = focus_on {
6512 focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
6513 } else if self.active_pane() == pane {
6514 self.panes
6515 .last()
6516 .unwrap()
6517 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
6518 }
6519 if self.last_active_center_pane == Some(pane.downgrade()) {
6520 self.last_active_center_pane = None;
6521 }
6522 cx.notify();
6523 }
6524
6525 fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
6526 if self._schedule_serialize_workspace.is_none() {
6527 self._schedule_serialize_workspace =
6528 Some(cx.spawn_in(window, async move |this, cx| {
6529 cx.background_executor()
6530 .timer(SERIALIZATION_THROTTLE_TIME)
6531 .await;
6532 this.update_in(cx, |this, window, cx| {
6533 this._serialize_workspace_task =
6534 Some(this.serialize_workspace_internal(window, cx));
6535 this._schedule_serialize_workspace.take();
6536 })
6537 .log_err();
6538 }));
6539 }
6540 }
6541
6542 fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
6543 let Some(database_id) = self.database_id() else {
6544 return Task::ready(());
6545 };
6546
6547 fn serialize_pane_handle(
6548 pane_handle: &Entity<Pane>,
6549 window: &mut Window,
6550 cx: &mut App,
6551 ) -> SerializedPane {
6552 let (items, active, pinned_count) = {
6553 let pane = pane_handle.read(cx);
6554 let active_item_id = pane.active_item().map(|item| item.item_id());
6555 (
6556 pane.items()
6557 .filter_map(|handle| {
6558 let handle = handle.to_serializable_item_handle(cx)?;
6559
6560 Some(SerializedItem {
6561 kind: Arc::from(handle.serialized_item_kind()),
6562 item_id: handle.item_id().as_u64(),
6563 active: Some(handle.item_id()) == active_item_id,
6564 preview: pane.is_active_preview_item(handle.item_id()),
6565 })
6566 })
6567 .collect::<Vec<_>>(),
6568 pane.has_focus(window, cx),
6569 pane.pinned_count(),
6570 )
6571 };
6572
6573 SerializedPane::new(items, active, pinned_count)
6574 }
6575
6576 fn build_serialized_pane_group(
6577 pane_group: &Member,
6578 window: &mut Window,
6579 cx: &mut App,
6580 ) -> SerializedPaneGroup {
6581 match pane_group {
6582 Member::Axis(PaneAxis {
6583 axis,
6584 members,
6585 flexes,
6586 bounding_boxes: _,
6587 }) => SerializedPaneGroup::Group {
6588 axis: SerializedAxis(*axis),
6589 children: members
6590 .iter()
6591 .map(|member| build_serialized_pane_group(member, window, cx))
6592 .collect::<Vec<_>>(),
6593 flexes: Some(flexes.lock().clone()),
6594 },
6595 Member::Pane(pane_handle) => {
6596 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
6597 }
6598 }
6599 }
6600
6601 fn build_serialized_docks(
6602 this: &Workspace,
6603 window: &mut Window,
6604 cx: &mut App,
6605 ) -> DockStructure {
6606 this.capture_dock_state(window, cx)
6607 }
6608
6609 match self.workspace_location(cx) {
6610 WorkspaceLocation::Location(location, paths) => {
6611 let breakpoints = self.project.update(cx, |project, cx| {
6612 project
6613 .breakpoint_store()
6614 .read(cx)
6615 .all_source_breakpoints(cx)
6616 });
6617 let user_toolchains = self
6618 .project
6619 .read(cx)
6620 .user_toolchains(cx)
6621 .unwrap_or_default();
6622
6623 let center_group = build_serialized_pane_group(&self.center.root, window, cx);
6624 let docks = build_serialized_docks(self, window, cx);
6625 let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
6626
6627 let serialized_workspace = SerializedWorkspace {
6628 id: database_id,
6629 location,
6630 paths,
6631 center_group,
6632 window_bounds,
6633 display: Default::default(),
6634 docks,
6635 centered_layout: self.centered_layout,
6636 session_id: self.session_id.clone(),
6637 breakpoints,
6638 window_id: Some(window.window_handle().window_id().as_u64()),
6639 user_toolchains,
6640 };
6641
6642 let db = WorkspaceDb::global(cx);
6643 window.spawn(cx, async move |_| {
6644 db.save_workspace(serialized_workspace).await;
6645 })
6646 }
6647 WorkspaceLocation::DetachFromSession => {
6648 let window_bounds = SerializedWindowBounds(window.window_bounds());
6649 let display = window.display(cx).and_then(|d| d.uuid().ok());
6650 // Save dock state for empty local workspaces
6651 let docks = build_serialized_docks(self, window, cx);
6652 let db = WorkspaceDb::global(cx);
6653 let kvp = db::kvp::KeyValueStore::global(cx);
6654 window.spawn(cx, async move |_| {
6655 db.set_window_open_status(
6656 database_id,
6657 window_bounds,
6658 display.unwrap_or_default(),
6659 )
6660 .await
6661 .log_err();
6662 db.set_session_id(database_id, None).await.log_err();
6663 persistence::write_default_dock_state(&kvp, docks)
6664 .await
6665 .log_err();
6666 })
6667 }
6668 WorkspaceLocation::None => {
6669 // Save dock state for empty non-local workspaces
6670 let docks = build_serialized_docks(self, window, cx);
6671 let kvp = db::kvp::KeyValueStore::global(cx);
6672 window.spawn(cx, async move |_| {
6673 persistence::write_default_dock_state(&kvp, docks)
6674 .await
6675 .log_err();
6676 })
6677 }
6678 }
6679 }
6680
6681 fn has_any_items_open(&self, cx: &App) -> bool {
6682 self.panes.iter().any(|pane| pane.read(cx).items_len() > 0)
6683 }
6684
6685 fn workspace_location(&self, cx: &App) -> WorkspaceLocation {
6686 let paths = PathList::new(&self.root_paths(cx));
6687 if let Some(connection) = self.project.read(cx).remote_connection_options(cx) {
6688 WorkspaceLocation::Location(SerializedWorkspaceLocation::Remote(connection), paths)
6689 } else if self.project.read(cx).is_local() {
6690 if !paths.is_empty() || self.has_any_items_open(cx) {
6691 WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths)
6692 } else {
6693 WorkspaceLocation::DetachFromSession
6694 }
6695 } else {
6696 WorkspaceLocation::None
6697 }
6698 }
6699
6700 fn update_history(&self, cx: &mut App) {
6701 let Some(id) = self.database_id() else {
6702 return;
6703 };
6704 if !self.project.read(cx).is_local() {
6705 return;
6706 }
6707 if let Some(manager) = HistoryManager::global(cx) {
6708 let paths = PathList::new(&self.root_paths(cx));
6709 manager.update(cx, |this, cx| {
6710 this.update_history(id, HistoryManagerEntry::new(id, &paths), cx);
6711 });
6712 }
6713 }
6714
6715 async fn serialize_items(
6716 this: &WeakEntity<Self>,
6717 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
6718 cx: &mut AsyncWindowContext,
6719 ) -> Result<()> {
6720 const CHUNK_SIZE: usize = 200;
6721
6722 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
6723
6724 while let Some(items_received) = serializable_items.next().await {
6725 let unique_items =
6726 items_received
6727 .into_iter()
6728 .fold(HashMap::default(), |mut acc, item| {
6729 acc.entry(item.item_id()).or_insert(item);
6730 acc
6731 });
6732
6733 // We use into_iter() here so that the references to the items are moved into
6734 // the tasks and not kept alive while we're sleeping.
6735 for (_, item) in unique_items.into_iter() {
6736 if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
6737 item.serialize(workspace, false, window, cx)
6738 }) {
6739 cx.background_spawn(async move { task.await.log_err() })
6740 .detach();
6741 }
6742 }
6743
6744 cx.background_executor()
6745 .timer(SERIALIZATION_THROTTLE_TIME)
6746 .await;
6747 }
6748
6749 Ok(())
6750 }
6751
6752 pub(crate) fn enqueue_item_serialization(
6753 &mut self,
6754 item: Box<dyn SerializableItemHandle>,
6755 ) -> Result<()> {
6756 self.serializable_items_tx
6757 .unbounded_send(item)
6758 .map_err(|err| anyhow!("failed to send serializable item over channel: {err}"))
6759 }
6760
6761 pub(crate) fn load_workspace(
6762 serialized_workspace: SerializedWorkspace,
6763 paths_to_open: Vec<Option<ProjectPath>>,
6764 window: &mut Window,
6765 cx: &mut Context<Workspace>,
6766 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
6767 cx.spawn_in(window, async move |workspace, cx| {
6768 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
6769
6770 let mut center_group = None;
6771 let mut center_items = None;
6772
6773 // Traverse the splits tree and add to things
6774 if let Some((group, active_pane, items)) = serialized_workspace
6775 .center_group
6776 .deserialize(&project, serialized_workspace.id, workspace.clone(), cx)
6777 .await
6778 {
6779 center_items = Some(items);
6780 center_group = Some((group, active_pane))
6781 }
6782
6783 let mut items_by_project_path = HashMap::default();
6784 let mut item_ids_by_kind = HashMap::default();
6785 let mut all_deserialized_items = Vec::default();
6786 cx.update(|_, cx| {
6787 for item in center_items.unwrap_or_default().into_iter().flatten() {
6788 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
6789 item_ids_by_kind
6790 .entry(serializable_item_handle.serialized_item_kind())
6791 .or_insert(Vec::new())
6792 .push(item.item_id().as_u64() as ItemId);
6793 }
6794
6795 if let Some(project_path) = item.project_path(cx) {
6796 items_by_project_path.insert(project_path, item.clone());
6797 }
6798 all_deserialized_items.push(item);
6799 }
6800 })?;
6801
6802 let opened_items = paths_to_open
6803 .into_iter()
6804 .map(|path_to_open| {
6805 path_to_open
6806 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
6807 })
6808 .collect::<Vec<_>>();
6809
6810 // Remove old panes from workspace panes list
6811 workspace.update_in(cx, |workspace, window, cx| {
6812 if let Some((center_group, active_pane)) = center_group {
6813 workspace.remove_panes(workspace.center.root.clone(), window, cx);
6814
6815 // Swap workspace center group
6816 workspace.center = PaneGroup::with_root(center_group);
6817 workspace.center.set_is_center(true);
6818 workspace.center.mark_positions(cx);
6819
6820 if let Some(active_pane) = active_pane {
6821 workspace.set_active_pane(&active_pane, window, cx);
6822 cx.focus_self(window);
6823 } else {
6824 workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
6825 }
6826 }
6827
6828 let docks = serialized_workspace.docks;
6829
6830 for (dock, serialized_dock) in [
6831 (&mut workspace.right_dock, docks.right),
6832 (&mut workspace.left_dock, docks.left),
6833 (&mut workspace.bottom_dock, docks.bottom),
6834 ]
6835 .iter_mut()
6836 {
6837 dock.update(cx, |dock, cx| {
6838 dock.serialized_dock = Some(serialized_dock.clone());
6839 dock.restore_state(window, cx);
6840 });
6841 }
6842
6843 cx.notify();
6844 })?;
6845
6846 let _ = project
6847 .update(cx, |project, cx| {
6848 project
6849 .breakpoint_store()
6850 .update(cx, |breakpoint_store, cx| {
6851 breakpoint_store
6852 .with_serialized_breakpoints(serialized_workspace.breakpoints, cx)
6853 })
6854 })
6855 .await;
6856
6857 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
6858 // after loading the items, we might have different items and in order to avoid
6859 // the database filling up, we delete items that haven't been loaded now.
6860 //
6861 // The items that have been loaded, have been saved after they've been added to the workspace.
6862 let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
6863 item_ids_by_kind
6864 .into_iter()
6865 .map(|(item_kind, loaded_items)| {
6866 SerializableItemRegistry::cleanup(
6867 item_kind,
6868 serialized_workspace.id,
6869 loaded_items,
6870 window,
6871 cx,
6872 )
6873 .log_err()
6874 })
6875 .collect::<Vec<_>>()
6876 })?;
6877
6878 futures::future::join_all(clean_up_tasks).await;
6879
6880 workspace
6881 .update_in(cx, |workspace, window, cx| {
6882 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
6883 workspace.serialize_workspace_internal(window, cx).detach();
6884
6885 // Ensure that we mark the window as edited if we did load dirty items
6886 workspace.update_window_edited(window, cx);
6887 })
6888 .ok();
6889
6890 Ok(opened_items)
6891 })
6892 }
6893
6894 pub fn key_context(&self, cx: &App) -> KeyContext {
6895 let mut context = KeyContext::new_with_defaults();
6896 context.add("Workspace");
6897 context.set("keyboard_layout", cx.keyboard_layout().name().to_string());
6898 if let Some(status) = self
6899 .debugger_provider
6900 .as_ref()
6901 .and_then(|provider| provider.active_thread_state(cx))
6902 {
6903 match status {
6904 ThreadStatus::Running | ThreadStatus::Stepping => {
6905 context.add("debugger_running");
6906 }
6907 ThreadStatus::Stopped => context.add("debugger_stopped"),
6908 ThreadStatus::Exited | ThreadStatus::Ended => {}
6909 }
6910 }
6911
6912 if self.left_dock.read(cx).is_open() {
6913 if let Some(active_panel) = self.left_dock.read(cx).active_panel() {
6914 context.set("left_dock", active_panel.panel_key());
6915 }
6916 }
6917
6918 if self.right_dock.read(cx).is_open() {
6919 if let Some(active_panel) = self.right_dock.read(cx).active_panel() {
6920 context.set("right_dock", active_panel.panel_key());
6921 }
6922 }
6923
6924 if self.bottom_dock.read(cx).is_open() {
6925 if let Some(active_panel) = self.bottom_dock.read(cx).active_panel() {
6926 context.set("bottom_dock", active_panel.panel_key());
6927 }
6928 }
6929
6930 context
6931 }
6932
6933 /// Multiworkspace uses this to add workspace action handling to itself
6934 pub fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
6935 self.add_workspace_actions_listeners(div, window, cx)
6936 .on_action(cx.listener(
6937 |_workspace, action_sequence: &settings::ActionSequence, window, cx| {
6938 for action in &action_sequence.0 {
6939 window.dispatch_action(action.boxed_clone(), cx);
6940 }
6941 },
6942 ))
6943 .on_action(cx.listener(Self::close_inactive_items_and_panes))
6944 .on_action(cx.listener(Self::close_all_items_and_panes))
6945 .on_action(cx.listener(Self::close_item_in_all_panes))
6946 .on_action(cx.listener(Self::save_all))
6947 .on_action(cx.listener(Self::send_keystrokes))
6948 .on_action(cx.listener(Self::add_folder_to_project))
6949 .on_action(cx.listener(Self::follow_next_collaborator))
6950 .on_action(cx.listener(Self::activate_pane_at_index))
6951 .on_action(cx.listener(Self::move_item_to_pane_at_index))
6952 .on_action(cx.listener(Self::move_focused_panel_to_next_position))
6953 .on_action(cx.listener(Self::toggle_edit_predictions_all_files))
6954 .on_action(cx.listener(Self::toggle_theme_mode))
6955 .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
6956 let pane = workspace.active_pane().clone();
6957 workspace.unfollow_in_pane(&pane, window, cx);
6958 }))
6959 .on_action(cx.listener(|workspace, action: &Save, window, cx| {
6960 workspace
6961 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
6962 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6963 }))
6964 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
6965 workspace
6966 .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
6967 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6968 }))
6969 .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
6970 workspace
6971 .save_active_item(SaveIntent::SaveAs, window, cx)
6972 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6973 }))
6974 .on_action(
6975 cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
6976 workspace.activate_previous_pane(window, cx)
6977 }),
6978 )
6979 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
6980 workspace.activate_next_pane(window, cx)
6981 }))
6982 .on_action(cx.listener(|workspace, _: &ActivateLastPane, window, cx| {
6983 workspace.activate_last_pane(window, cx)
6984 }))
6985 .on_action(
6986 cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
6987 workspace.activate_next_window(cx)
6988 }),
6989 )
6990 .on_action(
6991 cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
6992 workspace.activate_previous_window(cx)
6993 }),
6994 )
6995 .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
6996 workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
6997 }))
6998 .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
6999 workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
7000 }))
7001 .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
7002 workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
7003 }))
7004 .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
7005 workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
7006 }))
7007 .on_action(cx.listener(
7008 |workspace, action: &MoveItemToPaneInDirection, window, cx| {
7009 workspace.move_item_to_pane_in_direction(action, window, cx)
7010 },
7011 ))
7012 .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
7013 workspace.swap_pane_in_direction(SplitDirection::Left, cx)
7014 }))
7015 .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
7016 workspace.swap_pane_in_direction(SplitDirection::Right, cx)
7017 }))
7018 .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
7019 workspace.swap_pane_in_direction(SplitDirection::Up, cx)
7020 }))
7021 .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
7022 workspace.swap_pane_in_direction(SplitDirection::Down, cx)
7023 }))
7024 .on_action(cx.listener(|workspace, _: &SwapPaneAdjacent, window, cx| {
7025 const DIRECTION_PRIORITY: [SplitDirection; 4] = [
7026 SplitDirection::Down,
7027 SplitDirection::Up,
7028 SplitDirection::Right,
7029 SplitDirection::Left,
7030 ];
7031 for dir in DIRECTION_PRIORITY {
7032 if workspace.find_pane_in_direction(dir, cx).is_some() {
7033 workspace.swap_pane_in_direction(dir, cx);
7034 workspace.activate_pane_in_direction(dir.opposite(), window, cx);
7035 break;
7036 }
7037 }
7038 }))
7039 .on_action(cx.listener(|workspace, _: &MovePaneLeft, _, cx| {
7040 workspace.move_pane_to_border(SplitDirection::Left, cx)
7041 }))
7042 .on_action(cx.listener(|workspace, _: &MovePaneRight, _, cx| {
7043 workspace.move_pane_to_border(SplitDirection::Right, cx)
7044 }))
7045 .on_action(cx.listener(|workspace, _: &MovePaneUp, _, cx| {
7046 workspace.move_pane_to_border(SplitDirection::Up, cx)
7047 }))
7048 .on_action(cx.listener(|workspace, _: &MovePaneDown, _, cx| {
7049 workspace.move_pane_to_border(SplitDirection::Down, cx)
7050 }))
7051 .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
7052 this.toggle_dock(DockPosition::Left, window, cx);
7053 }))
7054 .on_action(cx.listener(
7055 |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
7056 workspace.toggle_dock(DockPosition::Right, window, cx);
7057 },
7058 ))
7059 .on_action(cx.listener(
7060 |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
7061 workspace.toggle_dock(DockPosition::Bottom, window, cx);
7062 },
7063 ))
7064 .on_action(cx.listener(
7065 |workspace: &mut Workspace, _: &CloseActiveDock, window, cx| {
7066 if !workspace.close_active_dock(window, cx) {
7067 cx.propagate();
7068 }
7069 },
7070 ))
7071 .on_action(
7072 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
7073 workspace.close_all_docks(window, cx);
7074 }),
7075 )
7076 .on_action(cx.listener(Self::toggle_all_docks))
7077 .on_action(cx.listener(
7078 |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
7079 workspace.clear_all_notifications(cx);
7080 },
7081 ))
7082 .on_action(cx.listener(
7083 |workspace: &mut Workspace, _: &ClearNavigationHistory, window, cx| {
7084 workspace.clear_navigation_history(window, cx);
7085 },
7086 ))
7087 .on_action(cx.listener(
7088 |workspace: &mut Workspace, _: &SuppressNotification, _, cx| {
7089 if let Some((notification_id, _)) = workspace.notifications.pop() {
7090 workspace.suppress_notification(¬ification_id, cx);
7091 }
7092 },
7093 ))
7094 .on_action(cx.listener(
7095 |workspace: &mut Workspace, _: &ToggleWorktreeSecurity, window, cx| {
7096 workspace.show_worktree_trust_security_modal(true, window, cx);
7097 },
7098 ))
7099 .on_action(
7100 cx.listener(|_: &mut Workspace, _: &ClearTrustedWorktrees, _, cx| {
7101 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
7102 trusted_worktrees.update(cx, |trusted_worktrees, _| {
7103 trusted_worktrees.clear_trusted_paths()
7104 });
7105 let db = WorkspaceDb::global(cx);
7106 cx.spawn(async move |_, cx| {
7107 if db.clear_trusted_worktrees().await.log_err().is_some() {
7108 cx.update(|cx| reload(cx));
7109 }
7110 })
7111 .detach();
7112 }
7113 }),
7114 )
7115 .on_action(cx.listener(
7116 |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
7117 workspace.reopen_closed_item(window, cx).detach();
7118 },
7119 ))
7120 .on_action(cx.listener(
7121 |workspace: &mut Workspace, _: &ResetActiveDockSize, window, cx| {
7122 for dock in workspace.all_docks() {
7123 if dock.focus_handle(cx).contains_focused(window, cx) {
7124 let panel = dock.read(cx).active_panel().cloned();
7125 if let Some(panel) = panel {
7126 dock.update(cx, |dock, cx| {
7127 dock.set_panel_size_state(
7128 panel.as_ref(),
7129 dock::PanelSizeState::default(),
7130 cx,
7131 );
7132 });
7133 }
7134 return;
7135 }
7136 }
7137 },
7138 ))
7139 .on_action(cx.listener(
7140 |workspace: &mut Workspace, _: &ResetOpenDocksSize, _window, cx| {
7141 for dock in workspace.all_docks() {
7142 let panel = dock.read(cx).visible_panel().cloned();
7143 if let Some(panel) = panel {
7144 dock.update(cx, |dock, cx| {
7145 dock.set_panel_size_state(
7146 panel.as_ref(),
7147 dock::PanelSizeState::default(),
7148 cx,
7149 );
7150 });
7151 }
7152 }
7153 },
7154 ))
7155 .on_action(cx.listener(
7156 |workspace: &mut Workspace, act: &IncreaseActiveDockSize, window, cx| {
7157 adjust_active_dock_size_by_px(
7158 px_with_ui_font_fallback(act.px, cx),
7159 workspace,
7160 window,
7161 cx,
7162 );
7163 },
7164 ))
7165 .on_action(cx.listener(
7166 |workspace: &mut Workspace, act: &DecreaseActiveDockSize, window, cx| {
7167 adjust_active_dock_size_by_px(
7168 px_with_ui_font_fallback(act.px, cx) * -1.,
7169 workspace,
7170 window,
7171 cx,
7172 );
7173 },
7174 ))
7175 .on_action(cx.listener(
7176 |workspace: &mut Workspace, act: &IncreaseOpenDocksSize, window, cx| {
7177 adjust_open_docks_size_by_px(
7178 px_with_ui_font_fallback(act.px, cx),
7179 workspace,
7180 window,
7181 cx,
7182 );
7183 },
7184 ))
7185 .on_action(cx.listener(
7186 |workspace: &mut Workspace, act: &DecreaseOpenDocksSize, window, cx| {
7187 adjust_open_docks_size_by_px(
7188 px_with_ui_font_fallback(act.px, cx) * -1.,
7189 workspace,
7190 window,
7191 cx,
7192 );
7193 },
7194 ))
7195 .on_action(cx.listener(Workspace::toggle_centered_layout))
7196 .on_action(cx.listener(
7197 |workspace: &mut Workspace, action: &pane::ActivateNextItem, window, cx| {
7198 if let Some(active_dock) = workspace.active_dock(window, cx) {
7199 let dock = active_dock.read(cx);
7200 if let Some(active_panel) = dock.active_panel() {
7201 if active_panel.pane(cx).is_none() {
7202 let mut recent_pane: Option<Entity<Pane>> = None;
7203 let mut recent_timestamp = 0;
7204 for pane_handle in workspace.panes() {
7205 let pane = pane_handle.read(cx);
7206 for entry in pane.activation_history() {
7207 if entry.timestamp > recent_timestamp {
7208 recent_timestamp = entry.timestamp;
7209 recent_pane = Some(pane_handle.clone());
7210 }
7211 }
7212 }
7213
7214 if let Some(pane) = recent_pane {
7215 let wrap_around = action.wrap_around;
7216 pane.update(cx, |pane, cx| {
7217 let current_index = pane.active_item_index();
7218 let items_len = pane.items_len();
7219 if items_len > 0 {
7220 let next_index = if current_index + 1 < items_len {
7221 current_index + 1
7222 } else if wrap_around {
7223 0
7224 } else {
7225 return;
7226 };
7227 pane.activate_item(
7228 next_index, false, false, window, cx,
7229 );
7230 }
7231 });
7232 return;
7233 }
7234 }
7235 }
7236 }
7237 cx.propagate();
7238 },
7239 ))
7240 .on_action(cx.listener(
7241 |workspace: &mut Workspace, action: &pane::ActivatePreviousItem, window, cx| {
7242 if let Some(active_dock) = workspace.active_dock(window, cx) {
7243 let dock = active_dock.read(cx);
7244 if let Some(active_panel) = dock.active_panel() {
7245 if active_panel.pane(cx).is_none() {
7246 let mut recent_pane: Option<Entity<Pane>> = None;
7247 let mut recent_timestamp = 0;
7248 for pane_handle in workspace.panes() {
7249 let pane = pane_handle.read(cx);
7250 for entry in pane.activation_history() {
7251 if entry.timestamp > recent_timestamp {
7252 recent_timestamp = entry.timestamp;
7253 recent_pane = Some(pane_handle.clone());
7254 }
7255 }
7256 }
7257
7258 if let Some(pane) = recent_pane {
7259 let wrap_around = action.wrap_around;
7260 pane.update(cx, |pane, cx| {
7261 let current_index = pane.active_item_index();
7262 let items_len = pane.items_len();
7263 if items_len > 0 {
7264 let prev_index = if current_index > 0 {
7265 current_index - 1
7266 } else if wrap_around {
7267 items_len.saturating_sub(1)
7268 } else {
7269 return;
7270 };
7271 pane.activate_item(
7272 prev_index, false, false, window, cx,
7273 );
7274 }
7275 });
7276 return;
7277 }
7278 }
7279 }
7280 }
7281 cx.propagate();
7282 },
7283 ))
7284 .on_action(cx.listener(
7285 |workspace: &mut Workspace, action: &pane::CloseActiveItem, window, cx| {
7286 if let Some(active_dock) = workspace.active_dock(window, cx) {
7287 let dock = active_dock.read(cx);
7288 if let Some(active_panel) = dock.active_panel() {
7289 if active_panel.pane(cx).is_none() {
7290 let active_pane = workspace.active_pane().clone();
7291 active_pane.update(cx, |pane, cx| {
7292 pane.close_active_item(action, window, cx)
7293 .detach_and_log_err(cx);
7294 });
7295 return;
7296 }
7297 }
7298 }
7299 cx.propagate();
7300 },
7301 ))
7302 .on_action(
7303 cx.listener(|workspace, _: &ToggleReadOnlyFile, window, cx| {
7304 let pane = workspace.active_pane().clone();
7305 if let Some(item) = pane.read(cx).active_item() {
7306 item.toggle_read_only(window, cx);
7307 }
7308 }),
7309 )
7310 .on_action(cx.listener(|workspace, _: &FocusCenterPane, window, cx| {
7311 workspace.focus_center_pane(window, cx);
7312 }))
7313 .on_action(cx.listener(Workspace::cancel))
7314 }
7315
7316 #[cfg(any(test, feature = "test-support"))]
7317 pub fn set_random_database_id(&mut self) {
7318 self.database_id = Some(WorkspaceId(Uuid::new_v4().as_u64_pair().0 as i64));
7319 }
7320
7321 #[cfg(any(test, feature = "test-support"))]
7322 pub(crate) fn test_new(
7323 project: Entity<Project>,
7324 window: &mut Window,
7325 cx: &mut Context<Self>,
7326 ) -> Self {
7327 use node_runtime::NodeRuntime;
7328 use session::Session;
7329
7330 let client = project.read(cx).client();
7331 let user_store = project.read(cx).user_store();
7332 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
7333 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
7334 window.activate_window();
7335 let app_state = Arc::new(AppState {
7336 languages: project.read(cx).languages().clone(),
7337 workspace_store,
7338 client,
7339 user_store,
7340 fs: project.read(cx).fs().clone(),
7341 build_window_options: |_, _| Default::default(),
7342 node_runtime: NodeRuntime::unavailable(),
7343 session,
7344 });
7345 let workspace = Self::new(Default::default(), project, app_state, window, cx);
7346 workspace
7347 .active_pane
7348 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
7349 workspace
7350 }
7351
7352 pub fn register_action<A: Action>(
7353 &mut self,
7354 callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
7355 ) -> &mut Self {
7356 let callback = Arc::new(callback);
7357
7358 self.workspace_actions.push(Box::new(move |div, _, _, cx| {
7359 let callback = callback.clone();
7360 div.on_action(cx.listener(move |workspace, event, window, cx| {
7361 (callback)(workspace, event, window, cx)
7362 }))
7363 }));
7364 self
7365 }
7366 pub fn register_action_renderer(
7367 &mut self,
7368 callback: impl Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div + 'static,
7369 ) -> &mut Self {
7370 self.workspace_actions.push(Box::new(callback));
7371 self
7372 }
7373
7374 fn add_workspace_actions_listeners(
7375 &self,
7376 mut div: Div,
7377 window: &mut Window,
7378 cx: &mut Context<Self>,
7379 ) -> Div {
7380 for action in self.workspace_actions.iter() {
7381 div = (action)(div, self, window, cx)
7382 }
7383 div
7384 }
7385
7386 pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
7387 self.modal_layer.read(cx).has_active_modal()
7388 }
7389
7390 pub fn is_active_modal_command_palette(&self, cx: &mut App) -> bool {
7391 self.modal_layer
7392 .read(cx)
7393 .is_active_modal_command_palette(cx)
7394 }
7395
7396 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
7397 self.modal_layer.read(cx).active_modal()
7398 }
7399
7400 /// Toggles a modal of type `V`. If a modal of the same type is currently active,
7401 /// it will be hidden. If a different modal is active, it will be replaced with the new one.
7402 /// If no modal is active, the new modal will be shown.
7403 ///
7404 /// If closing the current modal fails (e.g., due to `on_before_dismiss` returning
7405 /// `DismissDecision::Dismiss(false)` or `DismissDecision::Pending`), the new modal
7406 /// will not be shown.
7407 pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
7408 where
7409 B: FnOnce(&mut Window, &mut Context<V>) -> V,
7410 {
7411 self.modal_layer.update(cx, |modal_layer, cx| {
7412 modal_layer.toggle_modal(window, cx, build)
7413 })
7414 }
7415
7416 pub fn hide_modal(&mut self, window: &mut Window, cx: &mut App) -> bool {
7417 self.modal_layer
7418 .update(cx, |modal_layer, cx| modal_layer.hide_modal(window, cx))
7419 }
7420
7421 pub fn toggle_status_toast<V: ToastView>(&mut self, entity: Entity<V>, cx: &mut App) {
7422 self.toast_layer
7423 .update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity))
7424 }
7425
7426 pub fn toggle_centered_layout(
7427 &mut self,
7428 _: &ToggleCenteredLayout,
7429 _: &mut Window,
7430 cx: &mut Context<Self>,
7431 ) {
7432 self.centered_layout = !self.centered_layout;
7433 if let Some(database_id) = self.database_id() {
7434 let db = WorkspaceDb::global(cx);
7435 let centered_layout = self.centered_layout;
7436 cx.background_spawn(async move {
7437 db.set_centered_layout(database_id, centered_layout).await
7438 })
7439 .detach_and_log_err(cx);
7440 }
7441 cx.notify();
7442 }
7443
7444 fn adjust_padding(padding: Option<f32>) -> f32 {
7445 padding
7446 .unwrap_or(CenteredPaddingSettings::default().0)
7447 .clamp(
7448 CenteredPaddingSettings::MIN_PADDING,
7449 CenteredPaddingSettings::MAX_PADDING,
7450 )
7451 }
7452
7453 fn render_dock(
7454 &self,
7455 position: DockPosition,
7456 dock: &Entity<Dock>,
7457 window: &mut Window,
7458 cx: &mut App,
7459 ) -> Option<Div> {
7460 if self.zoomed_position == Some(position) {
7461 return None;
7462 }
7463
7464 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
7465 let pane = panel.pane(cx)?;
7466 let follower_states = &self.follower_states;
7467 leader_border_for_pane(follower_states, &pane, window, cx)
7468 });
7469
7470 let mut container = div()
7471 .flex()
7472 .overflow_hidden()
7473 .flex_none()
7474 .child(dock.clone())
7475 .children(leader_border);
7476
7477 // Apply sizing only when the dock is open. When closed the dock is still
7478 // included in the element tree so its focus handle remains mounted — without
7479 // this, toggle_panel_focus cannot focus the panel when the dock is closed.
7480 let dock = dock.read(cx);
7481 if let Some(panel) = dock.visible_panel() {
7482 let size_state = dock.stored_panel_size_state(panel.as_ref());
7483 if position.axis() == Axis::Horizontal {
7484 let use_flexible = panel.has_flexible_size(window, cx);
7485 let flex_grow = if use_flexible {
7486 size_state
7487 .and_then(|state| state.flex)
7488 .or_else(|| self.default_dock_flex(position))
7489 } else {
7490 None
7491 };
7492 if let Some(grow) = flex_grow {
7493 let grow = grow.max(0.001);
7494 let style = container.style();
7495 style.flex_grow = Some(grow);
7496 style.flex_shrink = Some(1.0);
7497 style.flex_basis = Some(relative(0.).into());
7498 } else {
7499 let size = size_state
7500 .and_then(|state| state.size)
7501 .unwrap_or_else(|| panel.default_size(window, cx));
7502 container = container.w(size);
7503 }
7504 } else {
7505 let size = size_state
7506 .and_then(|state| state.size)
7507 .unwrap_or_else(|| panel.default_size(window, cx));
7508 container = container.h(size);
7509 }
7510 }
7511
7512 Some(container)
7513 }
7514
7515 pub fn for_window(window: &Window, cx: &App) -> Option<Entity<Workspace>> {
7516 window
7517 .root::<MultiWorkspace>()
7518 .flatten()
7519 .map(|multi_workspace| multi_workspace.read(cx).workspace().clone())
7520 }
7521
7522 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
7523 self.zoomed.as_ref()
7524 }
7525
7526 pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
7527 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
7528 return;
7529 };
7530 let windows = cx.windows();
7531 let next_window =
7532 SystemWindowTabController::get_next_tab_group_window(cx, current_window_id).or_else(
7533 || {
7534 windows
7535 .iter()
7536 .cycle()
7537 .skip_while(|window| window.window_id() != current_window_id)
7538 .nth(1)
7539 },
7540 );
7541
7542 if let Some(window) = next_window {
7543 window
7544 .update(cx, |_, window, _| window.activate_window())
7545 .ok();
7546 }
7547 }
7548
7549 pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
7550 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
7551 return;
7552 };
7553 let windows = cx.windows();
7554 let prev_window =
7555 SystemWindowTabController::get_prev_tab_group_window(cx, current_window_id).or_else(
7556 || {
7557 windows
7558 .iter()
7559 .rev()
7560 .cycle()
7561 .skip_while(|window| window.window_id() != current_window_id)
7562 .nth(1)
7563 },
7564 );
7565
7566 if let Some(window) = prev_window {
7567 window
7568 .update(cx, |_, window, _| window.activate_window())
7569 .ok();
7570 }
7571 }
7572
7573 pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
7574 if cx.stop_active_drag(window) {
7575 } else if let Some((notification_id, _)) = self.notifications.pop() {
7576 dismiss_app_notification(¬ification_id, cx);
7577 } else {
7578 cx.propagate();
7579 }
7580 }
7581
7582 fn resize_dock(
7583 &mut self,
7584 dock_pos: DockPosition,
7585 new_size: Pixels,
7586 window: &mut Window,
7587 cx: &mut Context<Self>,
7588 ) {
7589 match dock_pos {
7590 DockPosition::Left => self.resize_left_dock(new_size, window, cx),
7591 DockPosition::Right => self.resize_right_dock(new_size, window, cx),
7592 DockPosition::Bottom => self.resize_bottom_dock(new_size, window, cx),
7593 }
7594 }
7595
7596 fn resize_left_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
7597 let workspace_width = self.bounds.size.width;
7598 let mut size = new_size.min(workspace_width - RESIZE_HANDLE_SIZE);
7599
7600 self.right_dock.read_with(cx, |right_dock, cx| {
7601 let right_dock_size = right_dock
7602 .stored_active_panel_size(window, cx)
7603 .unwrap_or(Pixels::ZERO);
7604 if right_dock_size + size > workspace_width {
7605 size = workspace_width - right_dock_size
7606 }
7607 });
7608
7609 let flex_grow = self.dock_flex_for_size(DockPosition::Left, size, window, cx);
7610 self.left_dock.update(cx, |left_dock, cx| {
7611 if WorkspaceSettings::get_global(cx)
7612 .resize_all_panels_in_dock
7613 .contains(&DockPosition::Left)
7614 {
7615 left_dock.resize_all_panels(Some(size), flex_grow, window, cx);
7616 } else {
7617 left_dock.resize_active_panel(Some(size), flex_grow, window, cx);
7618 }
7619 });
7620 }
7621
7622 fn resize_right_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
7623 let workspace_width = self.bounds.size.width;
7624 let mut size = new_size.min(workspace_width - RESIZE_HANDLE_SIZE);
7625 self.left_dock.read_with(cx, |left_dock, cx| {
7626 let left_dock_size = left_dock
7627 .stored_active_panel_size(window, cx)
7628 .unwrap_or(Pixels::ZERO);
7629 if left_dock_size + size > workspace_width {
7630 size = workspace_width - left_dock_size
7631 }
7632 });
7633 let flex_grow = self.dock_flex_for_size(DockPosition::Right, size, window, cx);
7634 self.right_dock.update(cx, |right_dock, cx| {
7635 if WorkspaceSettings::get_global(cx)
7636 .resize_all_panels_in_dock
7637 .contains(&DockPosition::Right)
7638 {
7639 right_dock.resize_all_panels(Some(size), flex_grow, window, cx);
7640 } else {
7641 right_dock.resize_active_panel(Some(size), flex_grow, window, cx);
7642 }
7643 });
7644 }
7645
7646 fn resize_bottom_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
7647 let size = new_size.min(self.bounds.bottom() - RESIZE_HANDLE_SIZE - self.bounds.top());
7648 self.bottom_dock.update(cx, |bottom_dock, cx| {
7649 if WorkspaceSettings::get_global(cx)
7650 .resize_all_panels_in_dock
7651 .contains(&DockPosition::Bottom)
7652 {
7653 bottom_dock.resize_all_panels(Some(size), None, window, cx);
7654 } else {
7655 bottom_dock.resize_active_panel(Some(size), None, window, cx);
7656 }
7657 });
7658 }
7659
7660 fn toggle_edit_predictions_all_files(
7661 &mut self,
7662 _: &ToggleEditPrediction,
7663 _window: &mut Window,
7664 cx: &mut Context<Self>,
7665 ) {
7666 let fs = self.project().read(cx).fs().clone();
7667 let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx);
7668 update_settings_file(fs, cx, move |file, _| {
7669 file.project.all_languages.defaults.show_edit_predictions = Some(!show_edit_predictions)
7670 });
7671 }
7672
7673 fn toggle_theme_mode(&mut self, _: &ToggleMode, _window: &mut Window, cx: &mut Context<Self>) {
7674 let current_mode = ThemeSettings::get_global(cx).theme.mode();
7675 let next_mode = match current_mode {
7676 Some(theme_settings::ThemeAppearanceMode::Light) => {
7677 theme_settings::ThemeAppearanceMode::Dark
7678 }
7679 Some(theme_settings::ThemeAppearanceMode::Dark) => {
7680 theme_settings::ThemeAppearanceMode::Light
7681 }
7682 Some(theme_settings::ThemeAppearanceMode::System) | None => {
7683 match cx.theme().appearance() {
7684 theme::Appearance::Light => theme_settings::ThemeAppearanceMode::Dark,
7685 theme::Appearance::Dark => theme_settings::ThemeAppearanceMode::Light,
7686 }
7687 }
7688 };
7689
7690 let fs = self.project().read(cx).fs().clone();
7691 settings::update_settings_file(fs, cx, move |settings, _cx| {
7692 theme_settings::set_mode(settings, next_mode);
7693 });
7694 }
7695
7696 pub fn show_worktree_trust_security_modal(
7697 &mut self,
7698 toggle: bool,
7699 window: &mut Window,
7700 cx: &mut Context<Self>,
7701 ) {
7702 if let Some(security_modal) = self.active_modal::<SecurityModal>(cx) {
7703 if toggle {
7704 security_modal.update(cx, |security_modal, cx| {
7705 security_modal.dismiss(cx);
7706 })
7707 } else {
7708 security_modal.update(cx, |security_modal, cx| {
7709 security_modal.refresh_restricted_paths(cx);
7710 });
7711 }
7712 } else {
7713 let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
7714 .map(|trusted_worktrees| {
7715 trusted_worktrees
7716 .read(cx)
7717 .has_restricted_worktrees(&self.project().read(cx).worktree_store(), cx)
7718 })
7719 .unwrap_or(false);
7720 if has_restricted_worktrees {
7721 let project = self.project().read(cx);
7722 let remote_host = project
7723 .remote_connection_options(cx)
7724 .map(RemoteHostLocation::from);
7725 let worktree_store = project.worktree_store().downgrade();
7726 self.toggle_modal(window, cx, |_, cx| {
7727 SecurityModal::new(worktree_store, remote_host, cx)
7728 });
7729 }
7730 }
7731 }
7732}
7733
7734pub trait AnyActiveCall {
7735 fn entity(&self) -> AnyEntity;
7736 fn is_in_room(&self, _: &App) -> bool;
7737 fn room_id(&self, _: &App) -> Option<u64>;
7738 fn channel_id(&self, _: &App) -> Option<ChannelId>;
7739 fn hang_up(&self, _: &mut App) -> Task<Result<()>>;
7740 fn unshare_project(&self, _: Entity<Project>, _: &mut App) -> Result<()>;
7741 fn remote_participant_for_peer_id(&self, _: PeerId, _: &App) -> Option<RemoteCollaborator>;
7742 fn is_sharing_project(&self, _: &App) -> bool;
7743 fn has_remote_participants(&self, _: &App) -> bool;
7744 fn local_participant_is_guest(&self, _: &App) -> bool;
7745 fn client(&self, _: &App) -> Arc<Client>;
7746 fn share_on_join(&self, _: &App) -> bool;
7747 fn join_channel(&self, _: ChannelId, _: &mut App) -> Task<Result<bool>>;
7748 fn room_update_completed(&self, _: &mut App) -> Task<()>;
7749 fn most_active_project(&self, _: &App) -> Option<(u64, u64)>;
7750 fn share_project(&self, _: Entity<Project>, _: &mut App) -> Task<Result<u64>>;
7751 fn join_project(
7752 &self,
7753 _: u64,
7754 _: Arc<LanguageRegistry>,
7755 _: Arc<dyn Fs>,
7756 _: &mut App,
7757 ) -> Task<Result<Entity<Project>>>;
7758 fn peer_id_for_user_in_room(&self, _: u64, _: &App) -> Option<PeerId>;
7759 fn subscribe(
7760 &self,
7761 _: &mut Window,
7762 _: &mut Context<Workspace>,
7763 _: Box<dyn Fn(&mut Workspace, &ActiveCallEvent, &mut Window, &mut Context<Workspace>)>,
7764 ) -> Subscription;
7765 fn create_shared_screen(
7766 &self,
7767 _: PeerId,
7768 _: &Entity<Pane>,
7769 _: &mut Window,
7770 _: &mut App,
7771 ) -> Option<Entity<SharedScreen>>;
7772}
7773
7774#[derive(Clone)]
7775pub struct GlobalAnyActiveCall(pub Arc<dyn AnyActiveCall>);
7776impl Global for GlobalAnyActiveCall {}
7777
7778impl GlobalAnyActiveCall {
7779 pub(crate) fn try_global(cx: &App) -> Option<&Self> {
7780 cx.try_global()
7781 }
7782
7783 pub(crate) fn global(cx: &App) -> &Self {
7784 cx.global()
7785 }
7786}
7787
7788/// Workspace-local view of a remote participant's location.
7789#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7790pub enum ParticipantLocation {
7791 SharedProject { project_id: u64 },
7792 UnsharedProject,
7793 External,
7794}
7795
7796impl ParticipantLocation {
7797 pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
7798 match location
7799 .and_then(|l| l.variant)
7800 .context("participant location was not provided")?
7801 {
7802 proto::participant_location::Variant::SharedProject(project) => {
7803 Ok(Self::SharedProject {
7804 project_id: project.id,
7805 })
7806 }
7807 proto::participant_location::Variant::UnsharedProject(_) => Ok(Self::UnsharedProject),
7808 proto::participant_location::Variant::External(_) => Ok(Self::External),
7809 }
7810 }
7811}
7812/// Workspace-local view of a remote collaborator's state.
7813/// This is the subset of `call::RemoteParticipant` that workspace needs.
7814#[derive(Clone)]
7815pub struct RemoteCollaborator {
7816 pub user: Arc<User>,
7817 pub peer_id: PeerId,
7818 pub location: ParticipantLocation,
7819 pub participant_index: ParticipantIndex,
7820}
7821
7822pub enum ActiveCallEvent {
7823 ParticipantLocationChanged { participant_id: PeerId },
7824 RemoteVideoTracksChanged { participant_id: PeerId },
7825}
7826
7827fn leader_border_for_pane(
7828 follower_states: &HashMap<CollaboratorId, FollowerState>,
7829 pane: &Entity<Pane>,
7830 _: &Window,
7831 cx: &App,
7832) -> Option<Div> {
7833 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
7834 if state.pane() == pane {
7835 Some((*leader_id, state))
7836 } else {
7837 None
7838 }
7839 })?;
7840
7841 let mut leader_color = match leader_id {
7842 CollaboratorId::PeerId(leader_peer_id) => {
7843 let leader = GlobalAnyActiveCall::try_global(cx)?
7844 .0
7845 .remote_participant_for_peer_id(leader_peer_id, cx)?;
7846
7847 cx.theme()
7848 .players()
7849 .color_for_participant(leader.participant_index.0)
7850 .cursor
7851 }
7852 CollaboratorId::Agent => cx.theme().players().agent().cursor,
7853 };
7854 leader_color.fade_out(0.3);
7855 Some(
7856 div()
7857 .absolute()
7858 .size_full()
7859 .left_0()
7860 .top_0()
7861 .border_2()
7862 .border_color(leader_color),
7863 )
7864}
7865
7866fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
7867 ZED_WINDOW_POSITION
7868 .zip(*ZED_WINDOW_SIZE)
7869 .map(|(position, size)| Bounds {
7870 origin: position,
7871 size,
7872 })
7873}
7874
7875fn open_items(
7876 serialized_workspace: Option<SerializedWorkspace>,
7877 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
7878 window: &mut Window,
7879 cx: &mut Context<Workspace>,
7880) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
7881 let restored_items = serialized_workspace.map(|serialized_workspace| {
7882 Workspace::load_workspace(
7883 serialized_workspace,
7884 project_paths_to_open
7885 .iter()
7886 .map(|(_, project_path)| project_path)
7887 .cloned()
7888 .collect(),
7889 window,
7890 cx,
7891 )
7892 });
7893
7894 cx.spawn_in(window, async move |workspace, cx| {
7895 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
7896
7897 if let Some(restored_items) = restored_items {
7898 let restored_items = restored_items.await?;
7899
7900 let restored_project_paths = restored_items
7901 .iter()
7902 .filter_map(|item| {
7903 cx.update(|_, cx| item.as_ref()?.project_path(cx))
7904 .ok()
7905 .flatten()
7906 })
7907 .collect::<HashSet<_>>();
7908
7909 for restored_item in restored_items {
7910 opened_items.push(restored_item.map(Ok));
7911 }
7912
7913 project_paths_to_open
7914 .iter_mut()
7915 .for_each(|(_, project_path)| {
7916 if let Some(project_path_to_open) = project_path
7917 && restored_project_paths.contains(project_path_to_open)
7918 {
7919 *project_path = None;
7920 }
7921 });
7922 } else {
7923 for _ in 0..project_paths_to_open.len() {
7924 opened_items.push(None);
7925 }
7926 }
7927 assert!(opened_items.len() == project_paths_to_open.len());
7928
7929 let tasks =
7930 project_paths_to_open
7931 .into_iter()
7932 .enumerate()
7933 .map(|(ix, (abs_path, project_path))| {
7934 let workspace = workspace.clone();
7935 cx.spawn(async move |cx| {
7936 let file_project_path = project_path?;
7937 let abs_path_task = workspace.update(cx, |workspace, cx| {
7938 workspace.project().update(cx, |project, cx| {
7939 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
7940 })
7941 });
7942
7943 // We only want to open file paths here. If one of the items
7944 // here is a directory, it was already opened further above
7945 // with a `find_or_create_worktree`.
7946 if let Ok(task) = abs_path_task
7947 && task.await.is_none_or(|p| p.is_file())
7948 {
7949 return Some((
7950 ix,
7951 workspace
7952 .update_in(cx, |workspace, window, cx| {
7953 workspace.open_path(
7954 file_project_path,
7955 None,
7956 true,
7957 window,
7958 cx,
7959 )
7960 })
7961 .log_err()?
7962 .await,
7963 ));
7964 }
7965 None
7966 })
7967 });
7968
7969 let tasks = tasks.collect::<Vec<_>>();
7970
7971 let tasks = futures::future::join_all(tasks);
7972 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
7973 opened_items[ix] = Some(path_open_result);
7974 }
7975
7976 Ok(opened_items)
7977 })
7978}
7979
7980#[derive(Clone)]
7981enum ActivateInDirectionTarget {
7982 Pane(Entity<Pane>),
7983 Dock(Entity<Dock>),
7984 Sidebar(FocusHandle),
7985}
7986
7987fn notify_if_database_failed(window: WindowHandle<MultiWorkspace>, cx: &mut AsyncApp) {
7988 window
7989 .update(cx, |multi_workspace, _, cx| {
7990 let workspace = multi_workspace.workspace().clone();
7991 workspace.update(cx, |workspace, cx| {
7992 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
7993 struct DatabaseFailedNotification;
7994
7995 workspace.show_notification(
7996 NotificationId::unique::<DatabaseFailedNotification>(),
7997 cx,
7998 |cx| {
7999 cx.new(|cx| {
8000 MessageNotification::new("Failed to load the database file.", cx)
8001 .primary_message("File an Issue")
8002 .primary_icon(IconName::Plus)
8003 .primary_on_click(|window, cx| {
8004 window.dispatch_action(Box::new(FileBugReport), cx)
8005 })
8006 })
8007 },
8008 );
8009 }
8010 });
8011 })
8012 .log_err();
8013}
8014
8015fn px_with_ui_font_fallback(val: u32, cx: &Context<Workspace>) -> Pixels {
8016 if val == 0 {
8017 ThemeSettings::get_global(cx).ui_font_size(cx)
8018 } else {
8019 px(val as f32)
8020 }
8021}
8022
8023fn adjust_active_dock_size_by_px(
8024 px: Pixels,
8025 workspace: &mut Workspace,
8026 window: &mut Window,
8027 cx: &mut Context<Workspace>,
8028) {
8029 let Some(active_dock) = workspace
8030 .all_docks()
8031 .into_iter()
8032 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx))
8033 else {
8034 return;
8035 };
8036 let dock = active_dock.read(cx);
8037 let Some(panel_size) = workspace.dock_size(&dock, window, cx) else {
8038 return;
8039 };
8040 workspace.resize_dock(dock.position(), panel_size + px, window, cx);
8041}
8042
8043fn adjust_open_docks_size_by_px(
8044 px: Pixels,
8045 workspace: &mut Workspace,
8046 window: &mut Window,
8047 cx: &mut Context<Workspace>,
8048) {
8049 let docks = workspace
8050 .all_docks()
8051 .into_iter()
8052 .filter_map(|dock_entity| {
8053 let dock = dock_entity.read(cx);
8054 if dock.is_open() {
8055 let dock_pos = dock.position();
8056 let panel_size = workspace.dock_size(&dock, window, cx)?;
8057 Some((dock_pos, panel_size + px))
8058 } else {
8059 None
8060 }
8061 })
8062 .collect::<Vec<_>>();
8063
8064 for (position, new_size) in docks {
8065 workspace.resize_dock(position, new_size, window, cx);
8066 }
8067}
8068
8069impl Focusable for Workspace {
8070 fn focus_handle(&self, cx: &App) -> FocusHandle {
8071 self.active_pane.focus_handle(cx)
8072 }
8073}
8074
8075#[derive(Clone)]
8076struct DraggedDock(DockPosition);
8077
8078impl Render for DraggedDock {
8079 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
8080 gpui::Empty
8081 }
8082}
8083
8084impl Render for Workspace {
8085 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
8086 static FIRST_PAINT: AtomicBool = AtomicBool::new(true);
8087 if FIRST_PAINT.swap(false, std::sync::atomic::Ordering::Relaxed) {
8088 log::info!("Rendered first frame");
8089 }
8090
8091 let centered_layout = self.centered_layout
8092 && self.center.panes().len() == 1
8093 && self.active_item(cx).is_some();
8094 let render_padding = |size| {
8095 (size > 0.0).then(|| {
8096 div()
8097 .h_full()
8098 .w(relative(size))
8099 .bg(cx.theme().colors().editor_background)
8100 .border_color(cx.theme().colors().pane_group_border)
8101 })
8102 };
8103 let paddings = if centered_layout {
8104 let settings = WorkspaceSettings::get_global(cx).centered_layout;
8105 (
8106 render_padding(Self::adjust_padding(
8107 settings.left_padding.map(|padding| padding.0),
8108 )),
8109 render_padding(Self::adjust_padding(
8110 settings.right_padding.map(|padding| padding.0),
8111 )),
8112 )
8113 } else {
8114 (None, None)
8115 };
8116 let ui_font = theme_settings::setup_ui_font(window, cx);
8117
8118 let theme = cx.theme().clone();
8119 let colors = theme.colors();
8120 let notification_entities = self
8121 .notifications
8122 .iter()
8123 .map(|(_, notification)| notification.entity_id())
8124 .collect::<Vec<_>>();
8125 let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout;
8126
8127 div()
8128 .relative()
8129 .size_full()
8130 .flex()
8131 .flex_col()
8132 .font(ui_font)
8133 .gap_0()
8134 .justify_start()
8135 .items_start()
8136 .text_color(colors.text)
8137 .overflow_hidden()
8138 .children(self.titlebar_item.clone())
8139 .on_modifiers_changed(move |_, _, cx| {
8140 for &id in ¬ification_entities {
8141 cx.notify(id);
8142 }
8143 })
8144 .child(
8145 div()
8146 .size_full()
8147 .relative()
8148 .flex_1()
8149 .flex()
8150 .flex_col()
8151 .child(
8152 div()
8153 .id("workspace")
8154 .bg(colors.background)
8155 .relative()
8156 .flex_1()
8157 .w_full()
8158 .flex()
8159 .flex_col()
8160 .overflow_hidden()
8161 .border_t_1()
8162 .border_b_1()
8163 .border_color(colors.border)
8164 .child({
8165 let this = cx.entity();
8166 canvas(
8167 move |bounds, window, cx| {
8168 this.update(cx, |this, cx| {
8169 let bounds_changed = this.bounds != bounds;
8170 this.bounds = bounds;
8171
8172 if bounds_changed {
8173 this.left_dock.update(cx, |dock, cx| {
8174 dock.clamp_panel_size(
8175 bounds.size.width,
8176 window,
8177 cx,
8178 )
8179 });
8180
8181 this.right_dock.update(cx, |dock, cx| {
8182 dock.clamp_panel_size(
8183 bounds.size.width,
8184 window,
8185 cx,
8186 )
8187 });
8188
8189 this.bottom_dock.update(cx, |dock, cx| {
8190 dock.clamp_panel_size(
8191 bounds.size.height,
8192 window,
8193 cx,
8194 )
8195 });
8196 }
8197 })
8198 },
8199 |_, _, _, _| {},
8200 )
8201 .absolute()
8202 .size_full()
8203 })
8204 .when(self.zoomed.is_none(), |this| {
8205 this.on_drag_move(cx.listener(
8206 move |workspace,
8207 e: &DragMoveEvent<DraggedDock>,
8208 window,
8209 cx| {
8210 if workspace.previous_dock_drag_coordinates
8211 != Some(e.event.position)
8212 {
8213 workspace.previous_dock_drag_coordinates =
8214 Some(e.event.position);
8215
8216 match e.drag(cx).0 {
8217 DockPosition::Left => {
8218 workspace.resize_left_dock(
8219 e.event.position.x
8220 - workspace.bounds.left(),
8221 window,
8222 cx,
8223 );
8224 }
8225 DockPosition::Right => {
8226 workspace.resize_right_dock(
8227 workspace.bounds.right()
8228 - e.event.position.x,
8229 window,
8230 cx,
8231 );
8232 }
8233 DockPosition::Bottom => {
8234 workspace.resize_bottom_dock(
8235 workspace.bounds.bottom()
8236 - e.event.position.y,
8237 window,
8238 cx,
8239 );
8240 }
8241 };
8242 workspace.serialize_workspace(window, cx);
8243 }
8244 },
8245 ))
8246
8247 })
8248 .child({
8249 match bottom_dock_layout {
8250 BottomDockLayout::Full => div()
8251 .flex()
8252 .flex_col()
8253 .h_full()
8254 .child(
8255 div()
8256 .flex()
8257 .flex_row()
8258 .flex_1()
8259 .overflow_hidden()
8260 .children(self.render_dock(
8261 DockPosition::Left,
8262 &self.left_dock,
8263 window,
8264 cx,
8265 ))
8266
8267 .child(
8268 div()
8269 .flex()
8270 .flex_col()
8271 .flex_1()
8272 .overflow_hidden()
8273 .child(
8274 h_flex()
8275 .flex_1()
8276 .when_some(
8277 paddings.0,
8278 |this, p| {
8279 this.child(
8280 p.border_r_1(),
8281 )
8282 },
8283 )
8284 .child(self.center.render(
8285 self.zoomed.as_ref(),
8286 &PaneRenderContext {
8287 follower_states:
8288 &self.follower_states,
8289 active_call: self.active_call(),
8290 active_pane: &self.active_pane,
8291 app_state: &self.app_state,
8292 project: &self.project,
8293 workspace: &self.weak_self,
8294 },
8295 window,
8296 cx,
8297 ))
8298 .when_some(
8299 paddings.1,
8300 |this, p| {
8301 this.child(
8302 p.border_l_1(),
8303 )
8304 },
8305 ),
8306 ),
8307 )
8308
8309 .children(self.render_dock(
8310 DockPosition::Right,
8311 &self.right_dock,
8312 window,
8313 cx,
8314 )),
8315 )
8316 .child(div().w_full().children(self.render_dock(
8317 DockPosition::Bottom,
8318 &self.bottom_dock,
8319 window,
8320 cx
8321 ))),
8322
8323 BottomDockLayout::LeftAligned => div()
8324 .flex()
8325 .flex_row()
8326 .h_full()
8327 .child(
8328 div()
8329 .flex()
8330 .flex_col()
8331 .flex_1()
8332 .h_full()
8333 .child(
8334 div()
8335 .flex()
8336 .flex_row()
8337 .flex_1()
8338 .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx))
8339
8340 .child(
8341 div()
8342 .flex()
8343 .flex_col()
8344 .flex_1()
8345 .overflow_hidden()
8346 .child(
8347 h_flex()
8348 .flex_1()
8349 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
8350 .child(self.center.render(
8351 self.zoomed.as_ref(),
8352 &PaneRenderContext {
8353 follower_states:
8354 &self.follower_states,
8355 active_call: self.active_call(),
8356 active_pane: &self.active_pane,
8357 app_state: &self.app_state,
8358 project: &self.project,
8359 workspace: &self.weak_self,
8360 },
8361 window,
8362 cx,
8363 ))
8364 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
8365 )
8366 )
8367
8368 )
8369 .child(
8370 div()
8371 .w_full()
8372 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
8373 ),
8374 )
8375 .children(self.render_dock(
8376 DockPosition::Right,
8377 &self.right_dock,
8378 window,
8379 cx,
8380 )),
8381 BottomDockLayout::RightAligned => div()
8382 .flex()
8383 .flex_row()
8384 .h_full()
8385 .children(self.render_dock(
8386 DockPosition::Left,
8387 &self.left_dock,
8388 window,
8389 cx,
8390 ))
8391
8392 .child(
8393 div()
8394 .flex()
8395 .flex_col()
8396 .flex_1()
8397 .h_full()
8398 .child(
8399 div()
8400 .flex()
8401 .flex_row()
8402 .flex_1()
8403 .child(
8404 div()
8405 .flex()
8406 .flex_col()
8407 .flex_1()
8408 .overflow_hidden()
8409 .child(
8410 h_flex()
8411 .flex_1()
8412 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
8413 .child(self.center.render(
8414 self.zoomed.as_ref(),
8415 &PaneRenderContext {
8416 follower_states:
8417 &self.follower_states,
8418 active_call: self.active_call(),
8419 active_pane: &self.active_pane,
8420 app_state: &self.app_state,
8421 project: &self.project,
8422 workspace: &self.weak_self,
8423 },
8424 window,
8425 cx,
8426 ))
8427 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
8428 )
8429 )
8430
8431 .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx))
8432 )
8433 .child(
8434 div()
8435 .w_full()
8436 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
8437 ),
8438 ),
8439 BottomDockLayout::Contained => div()
8440 .flex()
8441 .flex_row()
8442 .h_full()
8443 .children(self.render_dock(
8444 DockPosition::Left,
8445 &self.left_dock,
8446 window,
8447 cx,
8448 ))
8449
8450 .child(
8451 div()
8452 .flex()
8453 .flex_col()
8454 .flex_1()
8455 .overflow_hidden()
8456 .child(
8457 h_flex()
8458 .flex_1()
8459 .when_some(paddings.0, |this, p| {
8460 this.child(p.border_r_1())
8461 })
8462 .child(self.center.render(
8463 self.zoomed.as_ref(),
8464 &PaneRenderContext {
8465 follower_states:
8466 &self.follower_states,
8467 active_call: self.active_call(),
8468 active_pane: &self.active_pane,
8469 app_state: &self.app_state,
8470 project: &self.project,
8471 workspace: &self.weak_self,
8472 },
8473 window,
8474 cx,
8475 ))
8476 .when_some(paddings.1, |this, p| {
8477 this.child(p.border_l_1())
8478 }),
8479 )
8480 .children(self.render_dock(
8481 DockPosition::Bottom,
8482 &self.bottom_dock,
8483 window,
8484 cx,
8485 )),
8486 )
8487
8488 .children(self.render_dock(
8489 DockPosition::Right,
8490 &self.right_dock,
8491 window,
8492 cx,
8493 )),
8494 }
8495 })
8496 .children(self.zoomed.as_ref().and_then(|view| {
8497 let zoomed_view = view.upgrade()?;
8498 let div = div()
8499 .occlude()
8500 .absolute()
8501 .overflow_hidden()
8502 .border_color(colors.border)
8503 .bg(colors.background)
8504 .child(zoomed_view)
8505 .inset_0()
8506 .shadow_lg();
8507
8508 if !WorkspaceSettings::get_global(cx).zoomed_padding {
8509 return Some(div);
8510 }
8511
8512 Some(match self.zoomed_position {
8513 Some(DockPosition::Left) => div.right_2().border_r_1(),
8514 Some(DockPosition::Right) => div.left_2().border_l_1(),
8515 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
8516 None => {
8517 div.top_2().bottom_2().left_2().right_2().border_1()
8518 }
8519 })
8520 }))
8521 .children(self.render_notifications(window, cx)),
8522 )
8523 .when(self.status_bar_visible(cx), |parent| {
8524 parent.child(self.status_bar.clone())
8525 })
8526 .child(self.toast_layer.clone()),
8527 )
8528 }
8529}
8530
8531impl WorkspaceStore {
8532 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
8533 Self {
8534 workspaces: Default::default(),
8535 _subscriptions: vec![
8536 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
8537 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
8538 ],
8539 client,
8540 }
8541 }
8542
8543 pub fn update_followers(
8544 &self,
8545 project_id: Option<u64>,
8546 update: proto::update_followers::Variant,
8547 cx: &App,
8548 ) -> Option<()> {
8549 let active_call = GlobalAnyActiveCall::try_global(cx)?;
8550 let room_id = active_call.0.room_id(cx)?;
8551 self.client
8552 .send(proto::UpdateFollowers {
8553 room_id,
8554 project_id,
8555 variant: Some(update),
8556 })
8557 .log_err()
8558 }
8559
8560 pub async fn handle_follow(
8561 this: Entity<Self>,
8562 envelope: TypedEnvelope<proto::Follow>,
8563 mut cx: AsyncApp,
8564 ) -> Result<proto::FollowResponse> {
8565 this.update(&mut cx, |this, cx| {
8566 let follower = Follower {
8567 project_id: envelope.payload.project_id,
8568 peer_id: envelope.original_sender_id()?,
8569 };
8570
8571 let mut response = proto::FollowResponse::default();
8572
8573 this.workspaces.retain(|(window_handle, weak_workspace)| {
8574 let Some(workspace) = weak_workspace.upgrade() else {
8575 return false;
8576 };
8577 window_handle
8578 .update(cx, |_, window, cx| {
8579 workspace.update(cx, |workspace, cx| {
8580 let handler_response =
8581 workspace.handle_follow(follower.project_id, window, cx);
8582 if let Some(active_view) = handler_response.active_view
8583 && workspace.project.read(cx).remote_id() == follower.project_id
8584 {
8585 response.active_view = Some(active_view)
8586 }
8587 });
8588 })
8589 .is_ok()
8590 });
8591
8592 Ok(response)
8593 })
8594 }
8595
8596 async fn handle_update_followers(
8597 this: Entity<Self>,
8598 envelope: TypedEnvelope<proto::UpdateFollowers>,
8599 mut cx: AsyncApp,
8600 ) -> Result<()> {
8601 let leader_id = envelope.original_sender_id()?;
8602 let update = envelope.payload;
8603
8604 this.update(&mut cx, |this, cx| {
8605 this.workspaces.retain(|(window_handle, weak_workspace)| {
8606 let Some(workspace) = weak_workspace.upgrade() else {
8607 return false;
8608 };
8609 window_handle
8610 .update(cx, |_, window, cx| {
8611 workspace.update(cx, |workspace, cx| {
8612 let project_id = workspace.project.read(cx).remote_id();
8613 if update.project_id != project_id && update.project_id.is_some() {
8614 return;
8615 }
8616 workspace.handle_update_followers(
8617 leader_id,
8618 update.clone(),
8619 window,
8620 cx,
8621 );
8622 });
8623 })
8624 .is_ok()
8625 });
8626 Ok(())
8627 })
8628 }
8629
8630 pub fn workspaces(&self) -> impl Iterator<Item = &WeakEntity<Workspace>> {
8631 self.workspaces.iter().map(|(_, weak)| weak)
8632 }
8633
8634 pub fn workspaces_with_windows(
8635 &self,
8636 ) -> impl Iterator<Item = (gpui::AnyWindowHandle, &WeakEntity<Workspace>)> {
8637 self.workspaces.iter().map(|(window, weak)| (*window, weak))
8638 }
8639}
8640
8641impl ViewId {
8642 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
8643 Ok(Self {
8644 creator: message
8645 .creator
8646 .map(CollaboratorId::PeerId)
8647 .context("creator is missing")?,
8648 id: message.id,
8649 })
8650 }
8651
8652 pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
8653 if let CollaboratorId::PeerId(peer_id) = self.creator {
8654 Some(proto::ViewId {
8655 creator: Some(peer_id),
8656 id: self.id,
8657 })
8658 } else {
8659 None
8660 }
8661 }
8662}
8663
8664impl FollowerState {
8665 fn pane(&self) -> &Entity<Pane> {
8666 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
8667 }
8668}
8669
8670pub trait WorkspaceHandle {
8671 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
8672}
8673
8674impl WorkspaceHandle for Entity<Workspace> {
8675 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
8676 self.read(cx)
8677 .worktrees(cx)
8678 .flat_map(|worktree| {
8679 let worktree_id = worktree.read(cx).id();
8680 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
8681 worktree_id,
8682 path: f.path.clone(),
8683 })
8684 })
8685 .collect::<Vec<_>>()
8686 }
8687}
8688
8689pub async fn last_opened_workspace_location(
8690 db: &WorkspaceDb,
8691 fs: &dyn fs::Fs,
8692) -> Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)> {
8693 db.last_workspace(fs)
8694 .await
8695 .log_err()
8696 .flatten()
8697 .map(|(id, location, paths, _timestamp)| (id, location, paths))
8698}
8699
8700pub async fn last_session_workspace_locations(
8701 db: &WorkspaceDb,
8702 last_session_id: &str,
8703 last_session_window_stack: Option<Vec<WindowId>>,
8704 fs: &dyn fs::Fs,
8705) -> Option<Vec<SessionWorkspace>> {
8706 db.last_session_workspace_locations(last_session_id, last_session_window_stack, fs)
8707 .await
8708 .log_err()
8709}
8710
8711pub async fn restore_multiworkspace(
8712 multi_workspace: SerializedMultiWorkspace,
8713 app_state: Arc<AppState>,
8714 cx: &mut AsyncApp,
8715) -> anyhow::Result<WindowHandle<MultiWorkspace>> {
8716 let SerializedMultiWorkspace {
8717 active_workspace,
8718 state,
8719 } = multi_workspace;
8720
8721 let workspace_result = if active_workspace.paths.is_empty() {
8722 cx.update(|cx| {
8723 open_workspace_by_id(active_workspace.workspace_id, app_state.clone(), None, cx)
8724 })
8725 .await
8726 } else {
8727 cx.update(|cx| {
8728 Workspace::new_local(
8729 active_workspace.paths.paths().to_vec(),
8730 app_state.clone(),
8731 None,
8732 None,
8733 None,
8734 OpenMode::Activate,
8735 cx,
8736 )
8737 })
8738 .await
8739 .map(|result| result.window)
8740 };
8741
8742 let window_handle = match workspace_result {
8743 Ok(handle) => handle,
8744 Err(err) => {
8745 log::error!("Failed to restore active workspace: {err:#}");
8746
8747 let mut fallback_handle = None;
8748 for key in &state.project_group_keys {
8749 let key: ProjectGroupKey = key.clone().into();
8750 let paths = key.path_list().paths().to_vec();
8751 match cx
8752 .update(|cx| {
8753 Workspace::new_local(
8754 paths,
8755 app_state.clone(),
8756 None,
8757 None,
8758 None,
8759 OpenMode::Activate,
8760 cx,
8761 )
8762 })
8763 .await
8764 {
8765 Ok(OpenResult { window, .. }) => {
8766 fallback_handle = Some(window);
8767 break;
8768 }
8769 Err(fallback_err) => {
8770 log::error!("Fallback project group also failed: {fallback_err:#}");
8771 }
8772 }
8773 }
8774
8775 fallback_handle.ok_or(err)?
8776 }
8777 };
8778
8779 apply_restored_multiworkspace_state(window_handle, &state, app_state.fs.clone(), cx).await;
8780
8781 window_handle
8782 .update(cx, |_, window, _cx| {
8783 window.activate_window();
8784 })
8785 .ok();
8786
8787 Ok(window_handle)
8788}
8789
8790pub async fn apply_restored_multiworkspace_state(
8791 window_handle: WindowHandle<MultiWorkspace>,
8792 state: &MultiWorkspaceState,
8793 fs: Arc<dyn fs::Fs>,
8794 cx: &mut AsyncApp,
8795) {
8796 let MultiWorkspaceState {
8797 sidebar_open,
8798 project_group_keys,
8799 sidebar_state,
8800 ..
8801 } = state;
8802
8803 if !project_group_keys.is_empty() {
8804 // Resolve linked worktree paths to their main repo paths so
8805 // stale keys from previous sessions get normalized and deduped.
8806 let mut resolved_keys: Vec<ProjectGroupKey> = Vec::new();
8807 for key in project_group_keys
8808 .iter()
8809 .cloned()
8810 .map(ProjectGroupKey::from)
8811 {
8812 if key.path_list().paths().is_empty() {
8813 continue;
8814 }
8815 let mut resolved_paths = Vec::new();
8816 for path in key.path_list().paths() {
8817 if key.host().is_none()
8818 && let Some(common_dir) =
8819 project::discover_root_repo_common_dir(path, fs.as_ref()).await
8820 {
8821 let main_path = common_dir.parent().unwrap_or(&common_dir);
8822 resolved_paths.push(main_path.to_path_buf());
8823 } else {
8824 resolved_paths.push(path.to_path_buf());
8825 }
8826 }
8827 let resolved = ProjectGroupKey::new(key.host(), PathList::new(&resolved_paths));
8828 if !resolved_keys.contains(&resolved) {
8829 resolved_keys.push(resolved);
8830 }
8831 }
8832
8833 window_handle
8834 .update(cx, |multi_workspace, _window, _cx| {
8835 multi_workspace.restore_project_group_keys(resolved_keys);
8836 })
8837 .ok();
8838 }
8839
8840 if *sidebar_open {
8841 window_handle
8842 .update(cx, |multi_workspace, _, cx| {
8843 multi_workspace.open_sidebar(cx);
8844 })
8845 .ok();
8846 }
8847
8848 if let Some(sidebar_state) = sidebar_state {
8849 window_handle
8850 .update(cx, |multi_workspace, window, cx| {
8851 if let Some(sidebar) = multi_workspace.sidebar() {
8852 sidebar.restore_serialized_state(sidebar_state, window, cx);
8853 }
8854 multi_workspace.serialize(cx);
8855 })
8856 .ok();
8857 }
8858}
8859
8860actions!(
8861 collab,
8862 [
8863 /// Opens the channel notes for the current call.
8864 ///
8865 /// Use `collab_panel::OpenSelectedChannelNotes` to open the channel notes for the selected
8866 /// channel in the collab panel.
8867 ///
8868 /// If you want to open a specific channel, use `zed::OpenZedUrl` with a channel notes URL -
8869 /// can be copied via "Copy link to section" in the context menu of the channel notes
8870 /// buffer. These URLs look like `https://zed.dev/channel/channel-name-CHANNEL_ID/notes`.
8871 OpenChannelNotes,
8872 /// Mutes your microphone.
8873 Mute,
8874 /// Deafens yourself (mute both microphone and speakers).
8875 Deafen,
8876 /// Leaves the current call.
8877 LeaveCall,
8878 /// Shares the current project with collaborators.
8879 ShareProject,
8880 /// Shares your screen with collaborators.
8881 ScreenShare,
8882 /// Copies the current room name and session id for debugging purposes.
8883 CopyRoomId,
8884 ]
8885);
8886
8887/// Opens the channel notes for a specific channel by its ID.
8888#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
8889#[action(namespace = collab)]
8890#[serde(deny_unknown_fields)]
8891pub struct OpenChannelNotesById {
8892 pub channel_id: u64,
8893}
8894
8895actions!(
8896 zed,
8897 [
8898 /// Opens the Zed log file.
8899 OpenLog,
8900 /// Reveals the Zed log file in the system file manager.
8901 RevealLogInFileManager
8902 ]
8903);
8904
8905async fn join_channel_internal(
8906 channel_id: ChannelId,
8907 app_state: &Arc<AppState>,
8908 requesting_window: Option<WindowHandle<MultiWorkspace>>,
8909 requesting_workspace: Option<WeakEntity<Workspace>>,
8910 active_call: &dyn AnyActiveCall,
8911 cx: &mut AsyncApp,
8912) -> Result<bool> {
8913 let (should_prompt, already_in_channel) = cx.update(|cx| {
8914 if !active_call.is_in_room(cx) {
8915 return (false, false);
8916 }
8917
8918 let already_in_channel = active_call.channel_id(cx) == Some(channel_id);
8919 let should_prompt = active_call.is_sharing_project(cx)
8920 && active_call.has_remote_participants(cx)
8921 && !already_in_channel;
8922 (should_prompt, already_in_channel)
8923 });
8924
8925 if already_in_channel {
8926 let task = cx.update(|cx| {
8927 if let Some((project, host)) = active_call.most_active_project(cx) {
8928 Some(join_in_room_project(project, host, app_state.clone(), cx))
8929 } else {
8930 None
8931 }
8932 });
8933 if let Some(task) = task {
8934 task.await?;
8935 }
8936 return anyhow::Ok(true);
8937 }
8938
8939 if should_prompt {
8940 if let Some(multi_workspace) = requesting_window {
8941 let answer = multi_workspace
8942 .update(cx, |_, window, cx| {
8943 window.prompt(
8944 PromptLevel::Warning,
8945 "Do you want to switch channels?",
8946 Some("Leaving this call will unshare your current project."),
8947 &["Yes, Join Channel", "Cancel"],
8948 cx,
8949 )
8950 })?
8951 .await;
8952
8953 if answer == Ok(1) {
8954 return Ok(false);
8955 }
8956 } else {
8957 return Ok(false);
8958 }
8959 }
8960
8961 let client = cx.update(|cx| active_call.client(cx));
8962
8963 let mut client_status = client.status();
8964
8965 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
8966 'outer: loop {
8967 let Some(status) = client_status.recv().await else {
8968 anyhow::bail!("error connecting");
8969 };
8970
8971 match status {
8972 Status::Connecting
8973 | Status::Authenticating
8974 | Status::Authenticated
8975 | Status::Reconnecting
8976 | Status::Reauthenticating
8977 | Status::Reauthenticated => continue,
8978 Status::Connected { .. } => break 'outer,
8979 Status::SignedOut | Status::AuthenticationError => {
8980 return Err(ErrorCode::SignedOut.into());
8981 }
8982 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
8983 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
8984 return Err(ErrorCode::Disconnected.into());
8985 }
8986 }
8987 }
8988
8989 let joined = cx
8990 .update(|cx| active_call.join_channel(channel_id, cx))
8991 .await?;
8992
8993 if !joined {
8994 return anyhow::Ok(true);
8995 }
8996
8997 cx.update(|cx| active_call.room_update_completed(cx)).await;
8998
8999 let task = cx.update(|cx| {
9000 if let Some((project, host)) = active_call.most_active_project(cx) {
9001 return Some(join_in_room_project(project, host, app_state.clone(), cx));
9002 }
9003
9004 // If you are the first to join a channel, see if you should share your project.
9005 if !active_call.has_remote_participants(cx)
9006 && !active_call.local_participant_is_guest(cx)
9007 && let Some(workspace) = requesting_workspace.as_ref().and_then(|w| w.upgrade())
9008 {
9009 let project = workspace.update(cx, |workspace, cx| {
9010 let project = workspace.project.read(cx);
9011
9012 if !active_call.share_on_join(cx) {
9013 return None;
9014 }
9015
9016 if (project.is_local() || project.is_via_remote_server())
9017 && project.visible_worktrees(cx).any(|tree| {
9018 tree.read(cx)
9019 .root_entry()
9020 .is_some_and(|entry| entry.is_dir())
9021 })
9022 {
9023 Some(workspace.project.clone())
9024 } else {
9025 None
9026 }
9027 });
9028 if let Some(project) = project {
9029 let share_task = active_call.share_project(project, cx);
9030 return Some(cx.spawn(async move |_cx| -> Result<()> {
9031 share_task.await?;
9032 Ok(())
9033 }));
9034 }
9035 }
9036
9037 None
9038 });
9039 if let Some(task) = task {
9040 task.await?;
9041 return anyhow::Ok(true);
9042 }
9043 anyhow::Ok(false)
9044}
9045
9046pub fn join_channel(
9047 channel_id: ChannelId,
9048 app_state: Arc<AppState>,
9049 requesting_window: Option<WindowHandle<MultiWorkspace>>,
9050 requesting_workspace: Option<WeakEntity<Workspace>>,
9051 cx: &mut App,
9052) -> Task<Result<()>> {
9053 let active_call = GlobalAnyActiveCall::global(cx).clone();
9054 cx.spawn(async move |cx| {
9055 let result = join_channel_internal(
9056 channel_id,
9057 &app_state,
9058 requesting_window,
9059 requesting_workspace,
9060 &*active_call.0,
9061 cx,
9062 )
9063 .await;
9064
9065 // join channel succeeded, and opened a window
9066 if matches!(result, Ok(true)) {
9067 return anyhow::Ok(());
9068 }
9069
9070 // find an existing workspace to focus and show call controls
9071 let mut active_window = requesting_window.or_else(|| activate_any_workspace_window(cx));
9072 if active_window.is_none() {
9073 // no open workspaces, make one to show the error in (blergh)
9074 let OpenResult {
9075 window: window_handle,
9076 ..
9077 } = cx
9078 .update(|cx| {
9079 Workspace::new_local(
9080 vec![],
9081 app_state.clone(),
9082 requesting_window,
9083 None,
9084 None,
9085 OpenMode::Activate,
9086 cx,
9087 )
9088 })
9089 .await?;
9090
9091 window_handle
9092 .update(cx, |_, window, _cx| {
9093 window.activate_window();
9094 })
9095 .ok();
9096
9097 if result.is_ok() {
9098 cx.update(|cx| {
9099 cx.dispatch_action(&OpenChannelNotes);
9100 });
9101 }
9102
9103 active_window = Some(window_handle);
9104 }
9105
9106 if let Err(err) = result {
9107 log::error!("failed to join channel: {}", err);
9108 if let Some(active_window) = active_window {
9109 active_window
9110 .update(cx, |_, window, cx| {
9111 let detail: SharedString = match err.error_code() {
9112 ErrorCode::SignedOut => "Please sign in to continue.".into(),
9113 ErrorCode::UpgradeRequired => concat!(
9114 "Your are running an unsupported version of Zed. ",
9115 "Please update to continue."
9116 )
9117 .into(),
9118 ErrorCode::NoSuchChannel => concat!(
9119 "No matching channel was found. ",
9120 "Please check the link and try again."
9121 )
9122 .into(),
9123 ErrorCode::Forbidden => concat!(
9124 "This channel is private, and you do not have access. ",
9125 "Please ask someone to add you and try again."
9126 )
9127 .into(),
9128 ErrorCode::Disconnected => {
9129 "Please check your internet connection and try again.".into()
9130 }
9131 _ => format!("{}\n\nPlease try again.", err).into(),
9132 };
9133 window.prompt(
9134 PromptLevel::Critical,
9135 "Failed to join channel",
9136 Some(&detail),
9137 &["Ok"],
9138 cx,
9139 )
9140 })?
9141 .await
9142 .ok();
9143 }
9144 }
9145
9146 // return ok, we showed the error to the user.
9147 anyhow::Ok(())
9148 })
9149}
9150
9151pub async fn get_any_active_multi_workspace(
9152 app_state: Arc<AppState>,
9153 mut cx: AsyncApp,
9154) -> anyhow::Result<WindowHandle<MultiWorkspace>> {
9155 // find an existing workspace to focus and show call controls
9156 let active_window = activate_any_workspace_window(&mut cx);
9157 if active_window.is_none() {
9158 cx.update(|cx| {
9159 Workspace::new_local(
9160 vec![],
9161 app_state.clone(),
9162 None,
9163 None,
9164 None,
9165 OpenMode::Activate,
9166 cx,
9167 )
9168 })
9169 .await?;
9170 }
9171 activate_any_workspace_window(&mut cx).context("could not open zed")
9172}
9173
9174fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<MultiWorkspace>> {
9175 cx.update(|cx| {
9176 if let Some(workspace_window) = cx
9177 .active_window()
9178 .and_then(|window| window.downcast::<MultiWorkspace>())
9179 {
9180 return Some(workspace_window);
9181 }
9182
9183 for window in cx.windows() {
9184 if let Some(workspace_window) = window.downcast::<MultiWorkspace>() {
9185 workspace_window
9186 .update(cx, |_, window, _| window.activate_window())
9187 .ok();
9188 return Some(workspace_window);
9189 }
9190 }
9191 None
9192 })
9193}
9194
9195pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<MultiWorkspace>> {
9196 workspace_windows_for_location(&SerializedWorkspaceLocation::Local, cx)
9197}
9198
9199pub fn workspace_windows_for_location(
9200 serialized_location: &SerializedWorkspaceLocation,
9201 cx: &App,
9202) -> Vec<WindowHandle<MultiWorkspace>> {
9203 cx.windows()
9204 .into_iter()
9205 .filter_map(|window| window.downcast::<MultiWorkspace>())
9206 .filter(|multi_workspace| {
9207 let same_host = |left: &RemoteConnectionOptions, right: &RemoteConnectionOptions| match (left, right) {
9208 (RemoteConnectionOptions::Ssh(a), RemoteConnectionOptions::Ssh(b)) => {
9209 (&a.host, &a.username, &a.port) == (&b.host, &b.username, &b.port)
9210 }
9211 (RemoteConnectionOptions::Wsl(a), RemoteConnectionOptions::Wsl(b)) => {
9212 // The WSL username is not consistently populated in the workspace location, so ignore it for now.
9213 a.distro_name == b.distro_name
9214 }
9215 (RemoteConnectionOptions::Docker(a), RemoteConnectionOptions::Docker(b)) => {
9216 a.container_id == b.container_id
9217 }
9218 #[cfg(any(test, feature = "test-support"))]
9219 (RemoteConnectionOptions::Mock(a), RemoteConnectionOptions::Mock(b)) => {
9220 a.id == b.id
9221 }
9222 _ => false,
9223 };
9224
9225 multi_workspace.read(cx).is_ok_and(|multi_workspace| {
9226 multi_workspace.workspaces().any(|workspace| {
9227 match workspace.read(cx).workspace_location(cx) {
9228 WorkspaceLocation::Location(location, _) => {
9229 match (&location, serialized_location) {
9230 (
9231 SerializedWorkspaceLocation::Local,
9232 SerializedWorkspaceLocation::Local,
9233 ) => true,
9234 (
9235 SerializedWorkspaceLocation::Remote(a),
9236 SerializedWorkspaceLocation::Remote(b),
9237 ) => same_host(a, b),
9238 _ => false,
9239 }
9240 }
9241 _ => false,
9242 }
9243 })
9244 })
9245 })
9246 .collect()
9247}
9248
9249pub async fn find_existing_workspace(
9250 abs_paths: &[PathBuf],
9251 open_options: &OpenOptions,
9252 location: &SerializedWorkspaceLocation,
9253 cx: &mut AsyncApp,
9254) -> (
9255 Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)>,
9256 OpenVisible,
9257) {
9258 let mut existing: Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)> = None;
9259 let mut open_visible = OpenVisible::All;
9260 let mut best_match = None;
9261
9262 cx.update(|cx| {
9263 for window in workspace_windows_for_location(location, cx) {
9264 if let Ok(multi_workspace) = window.read(cx) {
9265 for workspace in multi_workspace.workspaces() {
9266 let project = workspace.read(cx).project.read(cx);
9267 let m = project.visibility_for_paths(
9268 abs_paths,
9269 open_options.open_new_workspace == None,
9270 cx,
9271 );
9272 if m > best_match {
9273 existing = Some((window, workspace.clone()));
9274 best_match = m;
9275 } else if best_match.is_none() && open_options.open_new_workspace == Some(false)
9276 {
9277 existing = Some((window, workspace.clone()))
9278 }
9279 }
9280 }
9281 }
9282 });
9283
9284 // With -n, only reuse a window if the path is genuinely contained
9285 // within an existing worktree (don't fall back to any arbitrary window).
9286 if open_options.open_new_workspace == Some(true) && best_match.is_none() {
9287 existing = None;
9288 }
9289
9290 if open_options.open_new_workspace != Some(true) {
9291 let all_paths_are_files = existing
9292 .as_ref()
9293 .and_then(|(_, target_workspace)| {
9294 cx.update(|cx| {
9295 let workspace = target_workspace.read(cx);
9296 let project = workspace.project.read(cx);
9297 let path_style = workspace.path_style(cx);
9298 Some(!abs_paths.iter().any(|path| {
9299 let path = util::paths::SanitizedPath::new(path);
9300 project.worktrees(cx).any(|worktree| {
9301 let worktree = worktree.read(cx);
9302 let abs_path = worktree.abs_path();
9303 path_style
9304 .strip_prefix(path.as_ref(), abs_path.as_ref())
9305 .and_then(|rel| worktree.entry_for_path(&rel))
9306 .is_some_and(|e| e.is_dir())
9307 })
9308 }))
9309 })
9310 })
9311 .unwrap_or(false);
9312
9313 if open_options.open_new_workspace.is_none()
9314 && existing.is_some()
9315 && open_options.wait
9316 && all_paths_are_files
9317 {
9318 cx.update(|cx| {
9319 let windows = workspace_windows_for_location(location, cx);
9320 let window = cx
9321 .active_window()
9322 .and_then(|window| window.downcast::<MultiWorkspace>())
9323 .filter(|window| windows.contains(window))
9324 .or_else(|| windows.into_iter().next());
9325 if let Some(window) = window {
9326 if let Ok(multi_workspace) = window.read(cx) {
9327 let active_workspace = multi_workspace.workspace().clone();
9328 existing = Some((window, active_workspace));
9329 open_visible = OpenVisible::None;
9330 }
9331 }
9332 });
9333 }
9334 }
9335 (existing, open_visible)
9336}
9337
9338#[derive(Default, Clone)]
9339pub struct OpenOptions {
9340 pub visible: Option<OpenVisible>,
9341 pub focus: Option<bool>,
9342 pub open_new_workspace: Option<bool>,
9343 pub wait: bool,
9344 pub requesting_window: Option<WindowHandle<MultiWorkspace>>,
9345 pub open_mode: OpenMode,
9346 pub env: Option<HashMap<String, String>>,
9347 pub open_in_dev_container: bool,
9348}
9349
9350/// The result of opening a workspace via [`open_paths`], [`Workspace::new_local`],
9351/// or [`Workspace::open_workspace_for_paths`].
9352pub struct OpenResult {
9353 pub window: WindowHandle<MultiWorkspace>,
9354 pub workspace: Entity<Workspace>,
9355 pub opened_items: Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
9356}
9357
9358/// Opens a workspace by its database ID, used for restoring empty workspaces with unsaved content.
9359pub fn open_workspace_by_id(
9360 workspace_id: WorkspaceId,
9361 app_state: Arc<AppState>,
9362 requesting_window: Option<WindowHandle<MultiWorkspace>>,
9363 cx: &mut App,
9364) -> Task<anyhow::Result<WindowHandle<MultiWorkspace>>> {
9365 let project_handle = Project::local(
9366 app_state.client.clone(),
9367 app_state.node_runtime.clone(),
9368 app_state.user_store.clone(),
9369 app_state.languages.clone(),
9370 app_state.fs.clone(),
9371 None,
9372 project::LocalProjectFlags {
9373 init_worktree_trust: true,
9374 ..project::LocalProjectFlags::default()
9375 },
9376 cx,
9377 );
9378
9379 let db = WorkspaceDb::global(cx);
9380 let kvp = db::kvp::KeyValueStore::global(cx);
9381 cx.spawn(async move |cx| {
9382 let serialized_workspace = db
9383 .workspace_for_id(workspace_id)
9384 .with_context(|| format!("Workspace {workspace_id:?} not found"))?;
9385
9386 let centered_layout = serialized_workspace.centered_layout;
9387
9388 let (window, workspace) = if let Some(window) = requesting_window {
9389 let workspace = window.update(cx, |multi_workspace, window, cx| {
9390 let workspace = cx.new(|cx| {
9391 let mut workspace = Workspace::new(
9392 Some(workspace_id),
9393 project_handle.clone(),
9394 app_state.clone(),
9395 window,
9396 cx,
9397 );
9398 workspace.centered_layout = centered_layout;
9399 workspace
9400 });
9401 multi_workspace.add(workspace.clone(), &*window, cx);
9402 workspace
9403 })?;
9404 (window, workspace)
9405 } else {
9406 let window_bounds_override = window_bounds_env_override();
9407
9408 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
9409 (Some(WindowBounds::Windowed(bounds)), None)
9410 } else if let Some(display) = serialized_workspace.display
9411 && let Some(bounds) = serialized_workspace.window_bounds.as_ref()
9412 {
9413 (Some(bounds.0), Some(display))
9414 } else if let Some((display, bounds)) = persistence::read_default_window_bounds(&kvp) {
9415 (Some(bounds), Some(display))
9416 } else {
9417 (None, None)
9418 };
9419
9420 let options = cx.update(|cx| {
9421 let mut options = (app_state.build_window_options)(display, cx);
9422 options.window_bounds = window_bounds;
9423 options
9424 });
9425
9426 let window = cx.open_window(options, {
9427 let app_state = app_state.clone();
9428 let project_handle = project_handle.clone();
9429 move |window, cx| {
9430 let workspace = cx.new(|cx| {
9431 let mut workspace = Workspace::new(
9432 Some(workspace_id),
9433 project_handle,
9434 app_state,
9435 window,
9436 cx,
9437 );
9438 workspace.centered_layout = centered_layout;
9439 workspace
9440 });
9441 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
9442 }
9443 })?;
9444
9445 let workspace = window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
9446 multi_workspace.workspace().clone()
9447 })?;
9448
9449 (window, workspace)
9450 };
9451
9452 notify_if_database_failed(window, cx);
9453
9454 // Restore items from the serialized workspace
9455 window
9456 .update(cx, |_, window, cx| {
9457 workspace.update(cx, |_workspace, cx| {
9458 open_items(Some(serialized_workspace), vec![], window, cx)
9459 })
9460 })?
9461 .await?;
9462
9463 window.update(cx, |_, window, cx| {
9464 workspace.update(cx, |workspace, cx| {
9465 workspace.serialize_workspace(window, cx);
9466 });
9467 })?;
9468
9469 Ok(window)
9470 })
9471}
9472
9473#[allow(clippy::type_complexity)]
9474pub fn open_paths(
9475 abs_paths: &[PathBuf],
9476 app_state: Arc<AppState>,
9477 mut open_options: OpenOptions,
9478 cx: &mut App,
9479) -> Task<anyhow::Result<OpenResult>> {
9480 let abs_paths = abs_paths.to_vec();
9481 #[cfg(target_os = "windows")]
9482 let wsl_path = abs_paths
9483 .iter()
9484 .find_map(|p| util::paths::WslPath::from_path(p));
9485
9486 cx.spawn(async move |cx| {
9487 let (mut existing, mut open_visible) = find_existing_workspace(
9488 &abs_paths,
9489 &open_options,
9490 &SerializedWorkspaceLocation::Local,
9491 cx,
9492 )
9493 .await;
9494
9495 // Fallback: if no workspace contains the paths and all paths are files,
9496 // prefer an existing local workspace window (active window first).
9497 if open_options.open_new_workspace.is_none() && existing.is_none() {
9498 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
9499 let all_metadatas = futures::future::join_all(all_paths)
9500 .await
9501 .into_iter()
9502 .filter_map(|result| result.ok().flatten());
9503
9504 if all_metadatas.into_iter().all(|file| !file.is_dir) {
9505 cx.update(|cx| {
9506 let windows = workspace_windows_for_location(
9507 &SerializedWorkspaceLocation::Local,
9508 cx,
9509 );
9510 let window = cx
9511 .active_window()
9512 .and_then(|window| window.downcast::<MultiWorkspace>())
9513 .filter(|window| windows.contains(window))
9514 .or_else(|| windows.into_iter().next());
9515 if let Some(window) = window {
9516 if let Ok(multi_workspace) = window.read(cx) {
9517 let active_workspace = multi_workspace.workspace().clone();
9518 existing = Some((window, active_workspace));
9519 open_visible = OpenVisible::None;
9520 }
9521 }
9522 });
9523 }
9524 }
9525
9526 // Fallback for directories: when no flag is specified and no existing
9527 // workspace matched, add the directory as a new workspace in the
9528 // active window's MultiWorkspace (instead of opening a new window).
9529 if open_options.open_new_workspace.is_none() && existing.is_none() {
9530 let target_window = cx.update(|cx| {
9531 let windows = workspace_windows_for_location(
9532 &SerializedWorkspaceLocation::Local,
9533 cx,
9534 );
9535 let window = cx
9536 .active_window()
9537 .and_then(|window| window.downcast::<MultiWorkspace>())
9538 .filter(|window| windows.contains(window))
9539 .or_else(|| windows.into_iter().next());
9540 window.filter(|window| {
9541 window.read(cx).is_ok_and(|mw| mw.multi_workspace_enabled(cx))
9542 })
9543 });
9544
9545 if let Some(window) = target_window {
9546 open_options.requesting_window = Some(window);
9547 window
9548 .update(cx, |multi_workspace, _, cx| {
9549 multi_workspace.open_sidebar(cx);
9550 })
9551 .log_err();
9552 }
9553 }
9554
9555 let open_in_dev_container = open_options.open_in_dev_container;
9556
9557 let result = if let Some((existing, target_workspace)) = existing {
9558 let open_task = existing
9559 .update(cx, |multi_workspace, window, cx| {
9560 window.activate_window();
9561 multi_workspace.activate(target_workspace.clone(), window, cx);
9562 target_workspace.update(cx, |workspace, cx| {
9563 if open_in_dev_container {
9564 workspace.set_open_in_dev_container(true);
9565 }
9566 workspace.open_paths(
9567 abs_paths,
9568 OpenOptions {
9569 visible: Some(open_visible),
9570 ..Default::default()
9571 },
9572 None,
9573 window,
9574 cx,
9575 )
9576 })
9577 })?
9578 .await;
9579
9580 _ = existing.update(cx, |multi_workspace, _, cx| {
9581 let workspace = multi_workspace.workspace().clone();
9582 workspace.update(cx, |workspace, cx| {
9583 for item in open_task.iter().flatten() {
9584 if let Err(e) = item {
9585 workspace.show_error(&e, cx);
9586 }
9587 }
9588 });
9589 });
9590
9591 Ok(OpenResult { window: existing, workspace: target_workspace, opened_items: open_task })
9592 } else {
9593 let init = if open_in_dev_container {
9594 Some(Box::new(|workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context<Workspace>| {
9595 workspace.set_open_in_dev_container(true);
9596 }) as Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>)
9597 } else {
9598 None
9599 };
9600 let result = cx
9601 .update(move |cx| {
9602 Workspace::new_local(
9603 abs_paths,
9604 app_state.clone(),
9605 open_options.requesting_window,
9606 open_options.env,
9607 init,
9608 open_options.open_mode,
9609 cx,
9610 )
9611 })
9612 .await;
9613
9614 if let Ok(ref result) = result {
9615 result.window
9616 .update(cx, |_, window, _cx| {
9617 window.activate_window();
9618 })
9619 .log_err();
9620 }
9621
9622 result
9623 };
9624
9625 #[cfg(target_os = "windows")]
9626 if let Some(util::paths::WslPath{distro, path}) = wsl_path
9627 && let Ok(ref result) = result
9628 {
9629 result.window
9630 .update(cx, move |multi_workspace, _window, cx| {
9631 struct OpenInWsl;
9632 let workspace = multi_workspace.workspace().clone();
9633 workspace.update(cx, |workspace, cx| {
9634 workspace.show_notification(NotificationId::unique::<OpenInWsl>(), cx, move |cx| {
9635 let display_path = util::markdown::MarkdownInlineCode(&path.to_string_lossy());
9636 let msg = format!("{display_path} is inside a WSL filesystem, some features may not work unless you open it with WSL remote");
9637 cx.new(move |cx| {
9638 MessageNotification::new(msg, cx)
9639 .primary_message("Open in WSL")
9640 .primary_icon(IconName::FolderOpen)
9641 .primary_on_click(move |window, cx| {
9642 window.dispatch_action(Box::new(remote::OpenWslPath {
9643 distro: remote::WslConnectionOptions {
9644 distro_name: distro.clone(),
9645 user: None,
9646 },
9647 paths: vec![path.clone().into()],
9648 }), cx)
9649 })
9650 })
9651 });
9652 });
9653 })
9654 .unwrap();
9655 };
9656 result
9657 })
9658}
9659
9660pub fn open_new(
9661 open_options: OpenOptions,
9662 app_state: Arc<AppState>,
9663 cx: &mut App,
9664 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
9665) -> Task<anyhow::Result<()>> {
9666 let addition = open_options.open_mode;
9667 let task = Workspace::new_local(
9668 Vec::new(),
9669 app_state,
9670 open_options.requesting_window,
9671 open_options.env,
9672 Some(Box::new(init)),
9673 addition,
9674 cx,
9675 );
9676 cx.spawn(async move |cx| {
9677 let OpenResult { window, .. } = task.await?;
9678 window
9679 .update(cx, |_, window, _cx| {
9680 window.activate_window();
9681 })
9682 .ok();
9683 Ok(())
9684 })
9685}
9686
9687pub fn create_and_open_local_file(
9688 path: &'static Path,
9689 window: &mut Window,
9690 cx: &mut Context<Workspace>,
9691 default_content: impl 'static + Send + FnOnce() -> Rope,
9692) -> Task<Result<Box<dyn ItemHandle>>> {
9693 cx.spawn_in(window, async move |workspace, cx| {
9694 let fs = workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
9695 if !fs.is_file(path).await {
9696 fs.create_file(path, Default::default()).await?;
9697 fs.save(path, &default_content(), Default::default())
9698 .await?;
9699 }
9700
9701 workspace
9702 .update_in(cx, |workspace, window, cx| {
9703 workspace.with_local_or_wsl_workspace(window, cx, |workspace, window, cx| {
9704 let path = workspace
9705 .project
9706 .read_with(cx, |project, cx| project.try_windows_path_to_wsl(path, cx));
9707 cx.spawn_in(window, async move |workspace, cx| {
9708 let path = path.await?;
9709
9710 let path = fs.canonicalize(&path).await.unwrap_or(path);
9711
9712 let mut items = workspace
9713 .update_in(cx, |workspace, window, cx| {
9714 workspace.open_paths(
9715 vec![path.to_path_buf()],
9716 OpenOptions {
9717 visible: Some(OpenVisible::None),
9718 ..Default::default()
9719 },
9720 None,
9721 window,
9722 cx,
9723 )
9724 })?
9725 .await;
9726 let item = items.pop().flatten();
9727 item.with_context(|| format!("path {path:?} is not a file"))?
9728 })
9729 })
9730 })?
9731 .await?
9732 .await
9733 })
9734}
9735
9736pub fn open_remote_project_with_new_connection(
9737 window: WindowHandle<MultiWorkspace>,
9738 remote_connection: Arc<dyn RemoteConnection>,
9739 cancel_rx: oneshot::Receiver<()>,
9740 delegate: Arc<dyn RemoteClientDelegate>,
9741 app_state: Arc<AppState>,
9742 paths: Vec<PathBuf>,
9743 cx: &mut App,
9744) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
9745 cx.spawn(async move |cx| {
9746 let (workspace_id, serialized_workspace) =
9747 deserialize_remote_project(remote_connection.connection_options(), paths.clone(), cx)
9748 .await?;
9749
9750 let session = match cx
9751 .update(|cx| {
9752 remote::RemoteClient::new(
9753 ConnectionIdentifier::Workspace(workspace_id.0),
9754 remote_connection,
9755 cancel_rx,
9756 delegate,
9757 cx,
9758 )
9759 })
9760 .await?
9761 {
9762 Some(result) => result,
9763 None => return Ok(Vec::new()),
9764 };
9765
9766 let project = cx.update(|cx| {
9767 project::Project::remote(
9768 session,
9769 app_state.client.clone(),
9770 app_state.node_runtime.clone(),
9771 app_state.user_store.clone(),
9772 app_state.languages.clone(),
9773 app_state.fs.clone(),
9774 true,
9775 cx,
9776 )
9777 });
9778
9779 open_remote_project_inner(
9780 project,
9781 paths,
9782 workspace_id,
9783 serialized_workspace,
9784 app_state,
9785 window,
9786 None,
9787 cx,
9788 )
9789 .await
9790 })
9791}
9792
9793pub fn open_remote_project_with_existing_connection(
9794 connection_options: RemoteConnectionOptions,
9795 project: Entity<Project>,
9796 paths: Vec<PathBuf>,
9797 app_state: Arc<AppState>,
9798 window: WindowHandle<MultiWorkspace>,
9799 provisional_project_group_key: Option<ProjectGroupKey>,
9800 cx: &mut AsyncApp,
9801) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
9802 cx.spawn(async move |cx| {
9803 let (workspace_id, serialized_workspace) =
9804 deserialize_remote_project(connection_options.clone(), paths.clone(), cx).await?;
9805
9806 open_remote_project_inner(
9807 project,
9808 paths,
9809 workspace_id,
9810 serialized_workspace,
9811 app_state,
9812 window,
9813 provisional_project_group_key,
9814 cx,
9815 )
9816 .await
9817 })
9818}
9819
9820async fn open_remote_project_inner(
9821 project: Entity<Project>,
9822 paths: Vec<PathBuf>,
9823 workspace_id: WorkspaceId,
9824 serialized_workspace: Option<SerializedWorkspace>,
9825 app_state: Arc<AppState>,
9826 window: WindowHandle<MultiWorkspace>,
9827 provisional_project_group_key: Option<ProjectGroupKey>,
9828 cx: &mut AsyncApp,
9829) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
9830 let db = cx.update(|cx| WorkspaceDb::global(cx));
9831 let toolchains = db.toolchains(workspace_id).await?;
9832 for (toolchain, worktree_path, path) in toolchains {
9833 project
9834 .update(cx, |this, cx| {
9835 let Some(worktree_id) =
9836 this.find_worktree(&worktree_path, cx)
9837 .and_then(|(worktree, rel_path)| {
9838 if rel_path.is_empty() {
9839 Some(worktree.read(cx).id())
9840 } else {
9841 None
9842 }
9843 })
9844 else {
9845 return Task::ready(None);
9846 };
9847
9848 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
9849 })
9850 .await;
9851 }
9852 let mut project_paths_to_open = vec![];
9853 let mut project_path_errors = vec![];
9854
9855 for path in paths {
9856 let result = cx
9857 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))
9858 .await;
9859 match result {
9860 Ok((_, project_path)) => {
9861 project_paths_to_open.push((path.clone(), Some(project_path)));
9862 }
9863 Err(error) => {
9864 project_path_errors.push(error);
9865 }
9866 };
9867 }
9868
9869 if project_paths_to_open.is_empty() {
9870 return Err(project_path_errors.pop().context("no paths given")?);
9871 }
9872
9873 let workspace = window.update(cx, |multi_workspace, window, cx| {
9874 telemetry::event!("SSH Project Opened");
9875
9876 let new_workspace = cx.new(|cx| {
9877 let mut workspace =
9878 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
9879 workspace.update_history(cx);
9880
9881 if let Some(ref serialized) = serialized_workspace {
9882 workspace.centered_layout = serialized.centered_layout;
9883 }
9884
9885 workspace
9886 });
9887
9888 if let Some(project_group_key) = provisional_project_group_key.clone() {
9889 multi_workspace.set_provisional_project_group_key(&new_workspace, project_group_key);
9890 }
9891 multi_workspace.activate(new_workspace.clone(), window, cx);
9892 new_workspace
9893 })?;
9894
9895 let items = window
9896 .update(cx, |_, window, cx| {
9897 window.activate_window();
9898 workspace.update(cx, |_workspace, cx| {
9899 open_items(serialized_workspace, project_paths_to_open, window, cx)
9900 })
9901 })?
9902 .await?;
9903
9904 workspace.update(cx, |workspace, cx| {
9905 for error in project_path_errors {
9906 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
9907 if let Some(path) = error.error_tag("path") {
9908 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
9909 }
9910 } else {
9911 workspace.show_error(&error, cx)
9912 }
9913 }
9914 });
9915
9916 Ok(items.into_iter().map(|item| item?.ok()).collect())
9917}
9918
9919fn deserialize_remote_project(
9920 connection_options: RemoteConnectionOptions,
9921 paths: Vec<PathBuf>,
9922 cx: &AsyncApp,
9923) -> Task<Result<(WorkspaceId, Option<SerializedWorkspace>)>> {
9924 let db = cx.update(|cx| WorkspaceDb::global(cx));
9925 cx.background_spawn(async move {
9926 let remote_connection_id = db
9927 .get_or_create_remote_connection(connection_options)
9928 .await?;
9929
9930 let serialized_workspace = db.remote_workspace_for_roots(&paths, remote_connection_id);
9931
9932 let workspace_id = if let Some(workspace_id) =
9933 serialized_workspace.as_ref().map(|workspace| workspace.id)
9934 {
9935 workspace_id
9936 } else {
9937 db.next_id().await?
9938 };
9939
9940 Ok((workspace_id, serialized_workspace))
9941 })
9942}
9943
9944pub fn join_in_room_project(
9945 project_id: u64,
9946 follow_user_id: u64,
9947 app_state: Arc<AppState>,
9948 cx: &mut App,
9949) -> Task<Result<()>> {
9950 let windows = cx.windows();
9951 cx.spawn(async move |cx| {
9952 let existing_window_and_workspace: Option<(
9953 WindowHandle<MultiWorkspace>,
9954 Entity<Workspace>,
9955 )> = windows.into_iter().find_map(|window_handle| {
9956 window_handle
9957 .downcast::<MultiWorkspace>()
9958 .and_then(|window_handle| {
9959 window_handle
9960 .update(cx, |multi_workspace, _window, cx| {
9961 for workspace in multi_workspace.workspaces() {
9962 if workspace.read(cx).project().read(cx).remote_id()
9963 == Some(project_id)
9964 {
9965 return Some((window_handle, workspace.clone()));
9966 }
9967 }
9968 None
9969 })
9970 .unwrap_or(None)
9971 })
9972 });
9973
9974 let multi_workspace_window = if let Some((existing_window, target_workspace)) =
9975 existing_window_and_workspace
9976 {
9977 existing_window
9978 .update(cx, |multi_workspace, window, cx| {
9979 multi_workspace.activate(target_workspace, window, cx);
9980 })
9981 .ok();
9982 existing_window
9983 } else {
9984 let active_call = cx.update(|cx| GlobalAnyActiveCall::global(cx).clone());
9985 let project = cx
9986 .update(|cx| {
9987 active_call.0.join_project(
9988 project_id,
9989 app_state.languages.clone(),
9990 app_state.fs.clone(),
9991 cx,
9992 )
9993 })
9994 .await?;
9995
9996 let window_bounds_override = window_bounds_env_override();
9997 cx.update(|cx| {
9998 let mut options = (app_state.build_window_options)(None, cx);
9999 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
10000 cx.open_window(options, |window, cx| {
10001 let workspace = cx.new(|cx| {
10002 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
10003 });
10004 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
10005 })
10006 })?
10007 };
10008
10009 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
10010 cx.activate(true);
10011 window.activate_window();
10012
10013 // We set the active workspace above, so this is the correct workspace.
10014 let workspace = multi_workspace.workspace().clone();
10015 workspace.update(cx, |workspace, cx| {
10016 let follow_peer_id = GlobalAnyActiveCall::try_global(cx)
10017 .and_then(|call| call.0.peer_id_for_user_in_room(follow_user_id, cx))
10018 .or_else(|| {
10019 // If we couldn't follow the given user, follow the host instead.
10020 let collaborator = workspace
10021 .project()
10022 .read(cx)
10023 .collaborators()
10024 .values()
10025 .find(|collaborator| collaborator.is_host)?;
10026 Some(collaborator.peer_id)
10027 });
10028
10029 if let Some(follow_peer_id) = follow_peer_id {
10030 workspace.follow(follow_peer_id, window, cx);
10031 }
10032 });
10033 })?;
10034
10035 anyhow::Ok(())
10036 })
10037}
10038
10039pub fn reload(cx: &mut App) {
10040 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
10041 let mut workspace_windows = cx
10042 .windows()
10043 .into_iter()
10044 .filter_map(|window| window.downcast::<MultiWorkspace>())
10045 .collect::<Vec<_>>();
10046
10047 // If multiple windows have unsaved changes, and need a save prompt,
10048 // prompt in the active window before switching to a different window.
10049 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
10050
10051 let mut prompt = None;
10052 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
10053 prompt = window
10054 .update(cx, |_, window, cx| {
10055 window.prompt(
10056 PromptLevel::Info,
10057 "Are you sure you want to restart?",
10058 None,
10059 &["Restart", "Cancel"],
10060 cx,
10061 )
10062 })
10063 .ok();
10064 }
10065
10066 cx.spawn(async move |cx| {
10067 if let Some(prompt) = prompt {
10068 let answer = prompt.await?;
10069 if answer != 0 {
10070 return anyhow::Ok(());
10071 }
10072 }
10073
10074 // If the user cancels any save prompt, then keep the app open.
10075 for window in workspace_windows {
10076 if let Ok(should_close) = window.update(cx, |multi_workspace, window, cx| {
10077 let workspace = multi_workspace.workspace().clone();
10078 workspace.update(cx, |workspace, cx| {
10079 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
10080 })
10081 }) && !should_close.await?
10082 {
10083 return anyhow::Ok(());
10084 }
10085 }
10086 cx.update(|cx| cx.restart());
10087 anyhow::Ok(())
10088 })
10089 .detach_and_log_err(cx);
10090}
10091
10092fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
10093 let mut parts = value.split(',');
10094 let x: usize = parts.next()?.parse().ok()?;
10095 let y: usize = parts.next()?.parse().ok()?;
10096 Some(point(px(x as f32), px(y as f32)))
10097}
10098
10099fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
10100 let mut parts = value.split(',');
10101 let width: usize = parts.next()?.parse().ok()?;
10102 let height: usize = parts.next()?.parse().ok()?;
10103 Some(size(px(width as f32), px(height as f32)))
10104}
10105
10106/// Add client-side decorations (rounded corners, shadows, resize handling) when
10107/// appropriate.
10108///
10109/// The `border_radius_tiling` parameter allows overriding which corners get
10110/// rounded, independently of the actual window tiling state. This is used
10111/// specifically for the workspace switcher sidebar: when the sidebar is open,
10112/// we want square corners on the left (so the sidebar appears flush with the
10113/// window edge) but we still need the shadow padding for proper visual
10114/// appearance. Unlike actual window tiling, this only affects border radius -
10115/// not padding or shadows.
10116pub fn client_side_decorations(
10117 element: impl IntoElement,
10118 window: &mut Window,
10119 cx: &mut App,
10120 border_radius_tiling: Tiling,
10121) -> Stateful<Div> {
10122 const BORDER_SIZE: Pixels = px(1.0);
10123 let decorations = window.window_decorations();
10124 let tiling = match decorations {
10125 Decorations::Server => Tiling::default(),
10126 Decorations::Client { tiling } => tiling,
10127 };
10128
10129 match decorations {
10130 Decorations::Client { .. } => window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW),
10131 Decorations::Server => window.set_client_inset(px(0.0)),
10132 }
10133
10134 struct GlobalResizeEdge(ResizeEdge);
10135 impl Global for GlobalResizeEdge {}
10136
10137 div()
10138 .id("window-backdrop")
10139 .bg(transparent_black())
10140 .map(|div| match decorations {
10141 Decorations::Server => div,
10142 Decorations::Client { .. } => div
10143 .when(
10144 !(tiling.top
10145 || tiling.right
10146 || border_radius_tiling.top
10147 || border_radius_tiling.right),
10148 |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10149 )
10150 .when(
10151 !(tiling.top
10152 || tiling.left
10153 || border_radius_tiling.top
10154 || border_radius_tiling.left),
10155 |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10156 )
10157 .when(
10158 !(tiling.bottom
10159 || tiling.right
10160 || border_radius_tiling.bottom
10161 || border_radius_tiling.right),
10162 |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10163 )
10164 .when(
10165 !(tiling.bottom
10166 || tiling.left
10167 || border_radius_tiling.bottom
10168 || border_radius_tiling.left),
10169 |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10170 )
10171 .when(!tiling.top, |div| {
10172 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
10173 })
10174 .when(!tiling.bottom, |div| {
10175 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
10176 })
10177 .when(!tiling.left, |div| {
10178 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
10179 })
10180 .when(!tiling.right, |div| {
10181 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
10182 })
10183 .on_mouse_move(move |e, window, cx| {
10184 let size = window.window_bounds().get_bounds().size;
10185 let pos = e.position;
10186
10187 let new_edge =
10188 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
10189
10190 let edge = cx.try_global::<GlobalResizeEdge>();
10191 if new_edge != edge.map(|edge| edge.0) {
10192 window
10193 .window_handle()
10194 .update(cx, |workspace, _, cx| {
10195 cx.notify(workspace.entity_id());
10196 })
10197 .ok();
10198 }
10199 })
10200 .on_mouse_down(MouseButton::Left, move |e, window, _| {
10201 let size = window.window_bounds().get_bounds().size;
10202 let pos = e.position;
10203
10204 let edge = match resize_edge(
10205 pos,
10206 theme::CLIENT_SIDE_DECORATION_SHADOW,
10207 size,
10208 tiling,
10209 ) {
10210 Some(value) => value,
10211 None => return,
10212 };
10213
10214 window.start_window_resize(edge);
10215 }),
10216 })
10217 .size_full()
10218 .child(
10219 div()
10220 .cursor(CursorStyle::Arrow)
10221 .map(|div| match decorations {
10222 Decorations::Server => div,
10223 Decorations::Client { .. } => div
10224 .border_color(cx.theme().colors().border)
10225 .when(
10226 !(tiling.top
10227 || tiling.right
10228 || border_radius_tiling.top
10229 || border_radius_tiling.right),
10230 |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10231 )
10232 .when(
10233 !(tiling.top
10234 || tiling.left
10235 || border_radius_tiling.top
10236 || border_radius_tiling.left),
10237 |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10238 )
10239 .when(
10240 !(tiling.bottom
10241 || tiling.right
10242 || border_radius_tiling.bottom
10243 || border_radius_tiling.right),
10244 |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10245 )
10246 .when(
10247 !(tiling.bottom
10248 || tiling.left
10249 || border_radius_tiling.bottom
10250 || border_radius_tiling.left),
10251 |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10252 )
10253 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
10254 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
10255 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
10256 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
10257 .when(!tiling.is_tiled(), |div| {
10258 div.shadow(vec![gpui::BoxShadow {
10259 color: Hsla {
10260 h: 0.,
10261 s: 0.,
10262 l: 0.,
10263 a: 0.4,
10264 },
10265 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
10266 spread_radius: px(0.),
10267 offset: point(px(0.0), px(0.0)),
10268 }])
10269 }),
10270 })
10271 .on_mouse_move(|_e, _, cx| {
10272 cx.stop_propagation();
10273 })
10274 .size_full()
10275 .child(element),
10276 )
10277 .map(|div| match decorations {
10278 Decorations::Server => div,
10279 Decorations::Client { tiling, .. } => div.child(
10280 canvas(
10281 |_bounds, window, _| {
10282 window.insert_hitbox(
10283 Bounds::new(
10284 point(px(0.0), px(0.0)),
10285 window.window_bounds().get_bounds().size,
10286 ),
10287 HitboxBehavior::Normal,
10288 )
10289 },
10290 move |_bounds, hitbox, window, cx| {
10291 let mouse = window.mouse_position();
10292 let size = window.window_bounds().get_bounds().size;
10293 let Some(edge) =
10294 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
10295 else {
10296 return;
10297 };
10298 cx.set_global(GlobalResizeEdge(edge));
10299 window.set_cursor_style(
10300 match edge {
10301 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
10302 ResizeEdge::Left | ResizeEdge::Right => {
10303 CursorStyle::ResizeLeftRight
10304 }
10305 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
10306 CursorStyle::ResizeUpLeftDownRight
10307 }
10308 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
10309 CursorStyle::ResizeUpRightDownLeft
10310 }
10311 },
10312 &hitbox,
10313 );
10314 },
10315 )
10316 .size_full()
10317 .absolute(),
10318 ),
10319 })
10320}
10321
10322fn resize_edge(
10323 pos: Point<Pixels>,
10324 shadow_size: Pixels,
10325 window_size: Size<Pixels>,
10326 tiling: Tiling,
10327) -> Option<ResizeEdge> {
10328 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
10329 if bounds.contains(&pos) {
10330 return None;
10331 }
10332
10333 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
10334 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
10335 if !tiling.top && top_left_bounds.contains(&pos) {
10336 return Some(ResizeEdge::TopLeft);
10337 }
10338
10339 let top_right_bounds = Bounds::new(
10340 Point::new(window_size.width - corner_size.width, px(0.)),
10341 corner_size,
10342 );
10343 if !tiling.top && top_right_bounds.contains(&pos) {
10344 return Some(ResizeEdge::TopRight);
10345 }
10346
10347 let bottom_left_bounds = Bounds::new(
10348 Point::new(px(0.), window_size.height - corner_size.height),
10349 corner_size,
10350 );
10351 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
10352 return Some(ResizeEdge::BottomLeft);
10353 }
10354
10355 let bottom_right_bounds = Bounds::new(
10356 Point::new(
10357 window_size.width - corner_size.width,
10358 window_size.height - corner_size.height,
10359 ),
10360 corner_size,
10361 );
10362 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
10363 return Some(ResizeEdge::BottomRight);
10364 }
10365
10366 if !tiling.top && pos.y < shadow_size {
10367 Some(ResizeEdge::Top)
10368 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
10369 Some(ResizeEdge::Bottom)
10370 } else if !tiling.left && pos.x < shadow_size {
10371 Some(ResizeEdge::Left)
10372 } else if !tiling.right && pos.x > window_size.width - shadow_size {
10373 Some(ResizeEdge::Right)
10374 } else {
10375 None
10376 }
10377}
10378
10379fn join_pane_into_active(
10380 active_pane: &Entity<Pane>,
10381 pane: &Entity<Pane>,
10382 window: &mut Window,
10383 cx: &mut App,
10384) {
10385 if pane == active_pane {
10386 } else if pane.read(cx).items_len() == 0 {
10387 pane.update(cx, |_, cx| {
10388 cx.emit(pane::Event::Remove {
10389 focus_on_pane: None,
10390 });
10391 })
10392 } else {
10393 move_all_items(pane, active_pane, window, cx);
10394 }
10395}
10396
10397fn move_all_items(
10398 from_pane: &Entity<Pane>,
10399 to_pane: &Entity<Pane>,
10400 window: &mut Window,
10401 cx: &mut App,
10402) {
10403 let destination_is_different = from_pane != to_pane;
10404 let mut moved_items = 0;
10405 for (item_ix, item_handle) in from_pane
10406 .read(cx)
10407 .items()
10408 .enumerate()
10409 .map(|(ix, item)| (ix, item.clone()))
10410 .collect::<Vec<_>>()
10411 {
10412 let ix = item_ix - moved_items;
10413 if destination_is_different {
10414 // Close item from previous pane
10415 from_pane.update(cx, |source, cx| {
10416 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
10417 });
10418 moved_items += 1;
10419 }
10420
10421 // This automatically removes duplicate items in the pane
10422 to_pane.update(cx, |destination, cx| {
10423 destination.add_item(item_handle, true, true, None, window, cx);
10424 window.focus(&destination.focus_handle(cx), cx)
10425 });
10426 }
10427}
10428
10429pub fn move_item(
10430 source: &Entity<Pane>,
10431 destination: &Entity<Pane>,
10432 item_id_to_move: EntityId,
10433 destination_index: usize,
10434 activate: bool,
10435 window: &mut Window,
10436 cx: &mut App,
10437) {
10438 let Some((item_ix, item_handle)) = source
10439 .read(cx)
10440 .items()
10441 .enumerate()
10442 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
10443 .map(|(ix, item)| (ix, item.clone()))
10444 else {
10445 // Tab was closed during drag
10446 return;
10447 };
10448
10449 if source != destination {
10450 // Close item from previous pane
10451 source.update(cx, |source, cx| {
10452 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
10453 });
10454 }
10455
10456 // This automatically removes duplicate items in the pane
10457 destination.update(cx, |destination, cx| {
10458 destination.add_item_inner(
10459 item_handle,
10460 activate,
10461 activate,
10462 activate,
10463 Some(destination_index),
10464 window,
10465 cx,
10466 );
10467 if activate {
10468 window.focus(&destination.focus_handle(cx), cx)
10469 }
10470 });
10471}
10472
10473pub fn move_active_item(
10474 source: &Entity<Pane>,
10475 destination: &Entity<Pane>,
10476 focus_destination: bool,
10477 close_if_empty: bool,
10478 window: &mut Window,
10479 cx: &mut App,
10480) {
10481 if source == destination {
10482 return;
10483 }
10484 let Some(active_item) = source.read(cx).active_item() else {
10485 return;
10486 };
10487 source.update(cx, |source_pane, cx| {
10488 let item_id = active_item.item_id();
10489 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
10490 destination.update(cx, |target_pane, cx| {
10491 target_pane.add_item(
10492 active_item,
10493 focus_destination,
10494 focus_destination,
10495 Some(target_pane.items_len()),
10496 window,
10497 cx,
10498 );
10499 });
10500 });
10501}
10502
10503pub fn clone_active_item(
10504 workspace_id: Option<WorkspaceId>,
10505 source: &Entity<Pane>,
10506 destination: &Entity<Pane>,
10507 focus_destination: bool,
10508 window: &mut Window,
10509 cx: &mut App,
10510) {
10511 if source == destination {
10512 return;
10513 }
10514 let Some(active_item) = source.read(cx).active_item() else {
10515 return;
10516 };
10517 if !active_item.can_split(cx) {
10518 return;
10519 }
10520 let destination = destination.downgrade();
10521 let task = active_item.clone_on_split(workspace_id, window, cx);
10522 window
10523 .spawn(cx, async move |cx| {
10524 let Some(clone) = task.await else {
10525 return;
10526 };
10527 destination
10528 .update_in(cx, |target_pane, window, cx| {
10529 target_pane.add_item(
10530 clone,
10531 focus_destination,
10532 focus_destination,
10533 Some(target_pane.items_len()),
10534 window,
10535 cx,
10536 );
10537 })
10538 .log_err();
10539 })
10540 .detach();
10541}
10542
10543#[derive(Debug)]
10544pub struct WorkspacePosition {
10545 pub window_bounds: Option<WindowBounds>,
10546 pub display: Option<Uuid>,
10547 pub centered_layout: bool,
10548}
10549
10550pub fn remote_workspace_position_from_db(
10551 connection_options: RemoteConnectionOptions,
10552 paths_to_open: &[PathBuf],
10553 cx: &App,
10554) -> Task<Result<WorkspacePosition>> {
10555 let paths = paths_to_open.to_vec();
10556 let db = WorkspaceDb::global(cx);
10557 let kvp = db::kvp::KeyValueStore::global(cx);
10558
10559 cx.background_spawn(async move {
10560 let remote_connection_id = db
10561 .get_or_create_remote_connection(connection_options)
10562 .await
10563 .context("fetching serialized ssh project")?;
10564 let serialized_workspace = db.remote_workspace_for_roots(&paths, remote_connection_id);
10565
10566 let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
10567 (Some(WindowBounds::Windowed(bounds)), None)
10568 } else {
10569 let restorable_bounds = serialized_workspace
10570 .as_ref()
10571 .and_then(|workspace| {
10572 Some((workspace.display?, workspace.window_bounds.map(|b| b.0)?))
10573 })
10574 .or_else(|| persistence::read_default_window_bounds(&kvp));
10575
10576 if let Some((serialized_display, serialized_bounds)) = restorable_bounds {
10577 (Some(serialized_bounds), Some(serialized_display))
10578 } else {
10579 (None, None)
10580 }
10581 };
10582
10583 let centered_layout = serialized_workspace
10584 .as_ref()
10585 .map(|w| w.centered_layout)
10586 .unwrap_or(false);
10587
10588 Ok(WorkspacePosition {
10589 window_bounds,
10590 display,
10591 centered_layout,
10592 })
10593 })
10594}
10595
10596pub fn with_active_or_new_workspace(
10597 cx: &mut App,
10598 f: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + 'static,
10599) {
10600 match cx
10601 .active_window()
10602 .and_then(|w| w.downcast::<MultiWorkspace>())
10603 {
10604 Some(multi_workspace) => {
10605 cx.defer(move |cx| {
10606 multi_workspace
10607 .update(cx, |multi_workspace, window, cx| {
10608 let workspace = multi_workspace.workspace().clone();
10609 workspace.update(cx, |workspace, cx| f(workspace, window, cx));
10610 })
10611 .log_err();
10612 });
10613 }
10614 None => {
10615 let app_state = AppState::global(cx);
10616 open_new(
10617 OpenOptions::default(),
10618 app_state,
10619 cx,
10620 move |workspace, window, cx| f(workspace, window, cx),
10621 )
10622 .detach_and_log_err(cx);
10623 }
10624 }
10625}
10626
10627/// Reads a panel's pixel size from its legacy KVP format and deletes the legacy
10628/// key. This migration path only runs once per panel per workspace.
10629fn load_legacy_panel_size(
10630 panel_key: &str,
10631 dock_position: DockPosition,
10632 workspace: &Workspace,
10633 cx: &mut App,
10634) -> Option<Pixels> {
10635 #[derive(Deserialize)]
10636 struct LegacyPanelState {
10637 #[serde(default)]
10638 width: Option<Pixels>,
10639 #[serde(default)]
10640 height: Option<Pixels>,
10641 }
10642
10643 let workspace_id = workspace
10644 .database_id()
10645 .map(|id| i64::from(id).to_string())
10646 .or_else(|| workspace.session_id())?;
10647
10648 let legacy_key = match panel_key {
10649 "ProjectPanel" => {
10650 format!("{}-{:?}", "ProjectPanel", workspace_id)
10651 }
10652 "OutlinePanel" => {
10653 format!("{}-{:?}", "OutlinePanel", workspace_id)
10654 }
10655 "GitPanel" => {
10656 format!("{}-{:?}", "GitPanel", workspace_id)
10657 }
10658 "TerminalPanel" => {
10659 format!("{:?}-{:?}", "TerminalPanel", workspace_id)
10660 }
10661 _ => return None,
10662 };
10663
10664 let kvp = db::kvp::KeyValueStore::global(cx);
10665 let json = kvp.read_kvp(&legacy_key).log_err().flatten()?;
10666 let state = serde_json::from_str::<LegacyPanelState>(&json).log_err()?;
10667 let size = match dock_position {
10668 DockPosition::Bottom => state.height,
10669 DockPosition::Left | DockPosition::Right => state.width,
10670 }?;
10671
10672 cx.background_spawn(async move { kvp.delete_kvp(legacy_key).await })
10673 .detach_and_log_err(cx);
10674
10675 Some(size)
10676}
10677
10678#[cfg(test)]
10679mod tests {
10680 use std::{cell::RefCell, rc::Rc, sync::Arc, time::Duration};
10681
10682 use super::*;
10683 use crate::{
10684 dock::{PanelEvent, test::TestPanel},
10685 item::{
10686 ItemBufferKind, ItemEvent,
10687 test::{TestItem, TestProjectItem},
10688 },
10689 };
10690 use fs::FakeFs;
10691 use gpui::{
10692 DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
10693 UpdateGlobal, VisualTestContext, px,
10694 };
10695 use project::{Project, ProjectEntryId};
10696 use serde_json::json;
10697 use settings::SettingsStore;
10698 use util::path;
10699 use util::rel_path::rel_path;
10700
10701 #[gpui::test]
10702 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
10703 init_test(cx);
10704
10705 let fs = FakeFs::new(cx.executor());
10706 let project = Project::test(fs, [], cx).await;
10707 let (workspace, cx) =
10708 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10709
10710 // Adding an item with no ambiguity renders the tab without detail.
10711 let item1 = cx.new(|cx| {
10712 let mut item = TestItem::new(cx);
10713 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
10714 item
10715 });
10716 workspace.update_in(cx, |workspace, window, cx| {
10717 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
10718 });
10719 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
10720
10721 // Adding an item that creates ambiguity increases the level of detail on
10722 // both tabs.
10723 let item2 = cx.new_window_entity(|_window, cx| {
10724 let mut item = TestItem::new(cx);
10725 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
10726 item
10727 });
10728 workspace.update_in(cx, |workspace, window, cx| {
10729 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10730 });
10731 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10732 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10733
10734 // Adding an item that creates ambiguity increases the level of detail only
10735 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
10736 // we stop at the highest detail available.
10737 let item3 = cx.new(|cx| {
10738 let mut item = TestItem::new(cx);
10739 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
10740 item
10741 });
10742 workspace.update_in(cx, |workspace, window, cx| {
10743 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
10744 });
10745 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10746 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
10747 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
10748 }
10749
10750 #[gpui::test]
10751 async fn test_tracking_active_path(cx: &mut TestAppContext) {
10752 init_test(cx);
10753
10754 let fs = FakeFs::new(cx.executor());
10755 fs.insert_tree(
10756 "/root1",
10757 json!({
10758 "one.txt": "",
10759 "two.txt": "",
10760 }),
10761 )
10762 .await;
10763 fs.insert_tree(
10764 "/root2",
10765 json!({
10766 "three.txt": "",
10767 }),
10768 )
10769 .await;
10770
10771 let project = Project::test(fs, ["root1".as_ref()], cx).await;
10772 let (workspace, cx) =
10773 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10774 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10775 let worktree_id = project.update(cx, |project, cx| {
10776 project.worktrees(cx).next().unwrap().read(cx).id()
10777 });
10778
10779 let item1 = cx.new(|cx| {
10780 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
10781 });
10782 let item2 = cx.new(|cx| {
10783 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
10784 });
10785
10786 // Add an item to an empty pane
10787 workspace.update_in(cx, |workspace, window, cx| {
10788 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
10789 });
10790 project.update(cx, |project, cx| {
10791 assert_eq!(
10792 project.active_entry(),
10793 project
10794 .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
10795 .map(|e| e.id)
10796 );
10797 });
10798 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
10799
10800 // Add a second item to a non-empty pane
10801 workspace.update_in(cx, |workspace, window, cx| {
10802 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
10803 });
10804 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
10805 project.update(cx, |project, cx| {
10806 assert_eq!(
10807 project.active_entry(),
10808 project
10809 .entry_for_path(&(worktree_id, rel_path("two.txt")).into(), cx)
10810 .map(|e| e.id)
10811 );
10812 });
10813
10814 // Close the active item
10815 pane.update_in(cx, |pane, window, cx| {
10816 pane.close_active_item(&Default::default(), window, cx)
10817 })
10818 .await
10819 .unwrap();
10820 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
10821 project.update(cx, |project, cx| {
10822 assert_eq!(
10823 project.active_entry(),
10824 project
10825 .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
10826 .map(|e| e.id)
10827 );
10828 });
10829
10830 // Add a project folder
10831 project
10832 .update(cx, |project, cx| {
10833 project.find_or_create_worktree("root2", true, cx)
10834 })
10835 .await
10836 .unwrap();
10837 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
10838
10839 // Remove a project folder
10840 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
10841 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
10842 }
10843
10844 #[gpui::test]
10845 async fn test_close_window(cx: &mut TestAppContext) {
10846 init_test(cx);
10847
10848 let fs = FakeFs::new(cx.executor());
10849 fs.insert_tree("/root", json!({ "one": "" })).await;
10850
10851 let project = Project::test(fs, ["root".as_ref()], cx).await;
10852 let (workspace, cx) =
10853 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10854
10855 // When there are no dirty items, there's nothing to do.
10856 let item1 = cx.new(TestItem::new);
10857 workspace.update_in(cx, |w, window, cx| {
10858 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
10859 });
10860 let task = workspace.update_in(cx, |w, window, cx| {
10861 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
10862 });
10863 assert!(task.await.unwrap());
10864
10865 // When there are dirty untitled items, prompt to save each one. If the user
10866 // cancels any prompt, then abort.
10867 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
10868 let item3 = cx.new(|cx| {
10869 TestItem::new(cx)
10870 .with_dirty(true)
10871 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10872 });
10873 workspace.update_in(cx, |w, window, cx| {
10874 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10875 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
10876 });
10877 let task = workspace.update_in(cx, |w, window, cx| {
10878 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
10879 });
10880 cx.executor().run_until_parked();
10881 cx.simulate_prompt_answer("Cancel"); // cancel save all
10882 cx.executor().run_until_parked();
10883 assert!(!cx.has_pending_prompt());
10884 assert!(!task.await.unwrap());
10885 }
10886
10887 #[gpui::test]
10888 async fn test_multi_workspace_close_window_multiple_workspaces_cancel(cx: &mut TestAppContext) {
10889 init_test(cx);
10890
10891 let fs = FakeFs::new(cx.executor());
10892 fs.insert_tree("/root", json!({ "one": "" })).await;
10893
10894 let project_a = Project::test(fs.clone(), ["root".as_ref()], cx).await;
10895 let project_b = Project::test(fs, ["root".as_ref()], cx).await;
10896 let multi_workspace_handle =
10897 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
10898 cx.run_until_parked();
10899
10900 multi_workspace_handle
10901 .update(cx, |mw, _window, cx| {
10902 mw.open_sidebar(cx);
10903 })
10904 .unwrap();
10905
10906 let workspace_a = multi_workspace_handle
10907 .read_with(cx, |mw, _| mw.workspace().clone())
10908 .unwrap();
10909
10910 let workspace_b = multi_workspace_handle
10911 .update(cx, |mw, window, cx| {
10912 mw.test_add_workspace(project_b, window, cx)
10913 })
10914 .unwrap();
10915
10916 // Activate workspace A
10917 multi_workspace_handle
10918 .update(cx, |mw, window, cx| {
10919 let workspace = mw.workspaces().next().unwrap().clone();
10920 mw.activate(workspace, window, cx);
10921 })
10922 .unwrap();
10923
10924 let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
10925
10926 // Workspace A has a clean item
10927 let item_a = cx.new(TestItem::new);
10928 workspace_a.update_in(cx, |w, window, cx| {
10929 w.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
10930 });
10931
10932 // Workspace B has a dirty item
10933 let item_b = cx.new(|cx| TestItem::new(cx).with_dirty(true));
10934 workspace_b.update_in(cx, |w, window, cx| {
10935 w.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx)
10936 });
10937
10938 // Verify workspace A is active
10939 multi_workspace_handle
10940 .read_with(cx, |mw, _| {
10941 assert_eq!(mw.workspace(), &workspace_a);
10942 })
10943 .unwrap();
10944
10945 // Dispatch CloseWindow — workspace A will pass, workspace B will prompt
10946 multi_workspace_handle
10947 .update(cx, |mw, window, cx| {
10948 mw.close_window(&CloseWindow, window, cx);
10949 })
10950 .unwrap();
10951 cx.run_until_parked();
10952
10953 // Workspace B should now be active since it has dirty items that need attention
10954 multi_workspace_handle
10955 .read_with(cx, |mw, _| {
10956 assert_eq!(
10957 mw.workspace(),
10958 &workspace_b,
10959 "workspace B should be activated when it prompts"
10960 );
10961 })
10962 .unwrap();
10963
10964 // User cancels the save prompt from workspace B
10965 cx.simulate_prompt_answer("Cancel");
10966 cx.run_until_parked();
10967
10968 // Window should still exist because workspace B's close was cancelled
10969 assert!(
10970 multi_workspace_handle.update(cx, |_, _, _| ()).is_ok(),
10971 "window should still exist after cancelling one workspace's close"
10972 );
10973 }
10974
10975 #[gpui::test]
10976 async fn test_remove_workspace_prompts_for_unsaved_changes(cx: &mut TestAppContext) {
10977 init_test(cx);
10978
10979 let fs = FakeFs::new(cx.executor());
10980 fs.insert_tree("/root", json!({ "one": "" })).await;
10981
10982 let project_a = Project::test(fs.clone(), ["root".as_ref()], cx).await;
10983 let project_b = Project::test(fs.clone(), ["root".as_ref()], cx).await;
10984 let multi_workspace_handle =
10985 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
10986 cx.run_until_parked();
10987
10988 multi_workspace_handle
10989 .update(cx, |mw, _window, cx| mw.open_sidebar(cx))
10990 .unwrap();
10991
10992 let workspace_a = multi_workspace_handle
10993 .read_with(cx, |mw, _| mw.workspace().clone())
10994 .unwrap();
10995
10996 let workspace_b = multi_workspace_handle
10997 .update(cx, |mw, window, cx| {
10998 mw.test_add_workspace(project_b, window, cx)
10999 })
11000 .unwrap();
11001
11002 // Activate workspace A.
11003 multi_workspace_handle
11004 .update(cx, |mw, window, cx| {
11005 mw.activate(workspace_a.clone(), window, cx);
11006 })
11007 .unwrap();
11008
11009 let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
11010
11011 // Workspace B has a dirty item.
11012 let item_b = cx.new(|cx| TestItem::new(cx).with_dirty(true));
11013 workspace_b.update_in(cx, |w, window, cx| {
11014 w.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx)
11015 });
11016
11017 // Try to remove workspace B. It should prompt because of the dirty item.
11018 let remove_task = multi_workspace_handle
11019 .update(cx, |mw, window, cx| {
11020 mw.remove([workspace_b.clone()], |_, _, _| unreachable!(), window, cx)
11021 })
11022 .unwrap();
11023 cx.run_until_parked();
11024
11025 // The prompt should have activated workspace B.
11026 multi_workspace_handle
11027 .read_with(cx, |mw, _| {
11028 assert_eq!(
11029 mw.workspace(),
11030 &workspace_b,
11031 "workspace B should be active while prompting"
11032 );
11033 })
11034 .unwrap();
11035
11036 // Cancel the prompt — user stays on workspace B.
11037 cx.simulate_prompt_answer("Cancel");
11038 cx.run_until_parked();
11039 let removed = remove_task.await.unwrap();
11040 assert!(!removed, "removal should have been cancelled");
11041
11042 multi_workspace_handle
11043 .read_with(cx, |mw, _| {
11044 assert_eq!(
11045 mw.workspace(),
11046 &workspace_b,
11047 "user should stay on workspace B after cancelling"
11048 );
11049 assert_eq!(mw.workspaces().count(), 2, "both workspaces should remain");
11050 })
11051 .unwrap();
11052
11053 // Try again. This time accept the prompt.
11054 let remove_task = multi_workspace_handle
11055 .update(cx, |mw, window, cx| {
11056 // First switch back to A.
11057 mw.activate(workspace_a.clone(), window, cx);
11058 mw.remove([workspace_b.clone()], |_, _, _| unreachable!(), window, cx)
11059 })
11060 .unwrap();
11061 cx.run_until_parked();
11062
11063 // Accept the save prompt.
11064 cx.simulate_prompt_answer("Don't Save");
11065 cx.run_until_parked();
11066 let removed = remove_task.await.unwrap();
11067 assert!(removed, "removal should have succeeded");
11068
11069 // Should be back on workspace A, and B should be gone.
11070 multi_workspace_handle
11071 .read_with(cx, |mw, _| {
11072 assert_eq!(
11073 mw.workspace(),
11074 &workspace_a,
11075 "should be back on workspace A after removing B"
11076 );
11077 assert_eq!(mw.workspaces().count(), 1, "only workspace A should remain");
11078 })
11079 .unwrap();
11080 }
11081
11082 #[gpui::test]
11083 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
11084 init_test(cx);
11085
11086 // Register TestItem as a serializable item
11087 cx.update(|cx| {
11088 register_serializable_item::<TestItem>(cx);
11089 });
11090
11091 let fs = FakeFs::new(cx.executor());
11092 fs.insert_tree("/root", json!({ "one": "" })).await;
11093
11094 let project = Project::test(fs, ["root".as_ref()], cx).await;
11095 let (workspace, cx) =
11096 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
11097
11098 // When there are dirty untitled items, but they can serialize, then there is no prompt.
11099 let item1 = cx.new(|cx| {
11100 TestItem::new(cx)
11101 .with_dirty(true)
11102 .with_serialize(|| Some(Task::ready(Ok(()))))
11103 });
11104 let item2 = cx.new(|cx| {
11105 TestItem::new(cx)
11106 .with_dirty(true)
11107 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11108 .with_serialize(|| Some(Task::ready(Ok(()))))
11109 });
11110 workspace.update_in(cx, |w, window, cx| {
11111 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
11112 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
11113 });
11114 let task = workspace.update_in(cx, |w, window, cx| {
11115 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
11116 });
11117 assert!(task.await.unwrap());
11118 }
11119
11120 #[gpui::test]
11121 async fn test_close_pane_items(cx: &mut TestAppContext) {
11122 init_test(cx);
11123
11124 let fs = FakeFs::new(cx.executor());
11125
11126 let project = Project::test(fs, None, cx).await;
11127 let (workspace, cx) =
11128 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11129
11130 let item1 = cx.new(|cx| {
11131 TestItem::new(cx)
11132 .with_dirty(true)
11133 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
11134 });
11135 let item2 = cx.new(|cx| {
11136 TestItem::new(cx)
11137 .with_dirty(true)
11138 .with_conflict(true)
11139 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
11140 });
11141 let item3 = cx.new(|cx| {
11142 TestItem::new(cx)
11143 .with_dirty(true)
11144 .with_conflict(true)
11145 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
11146 });
11147 let item4 = cx.new(|cx| {
11148 TestItem::new(cx).with_dirty(true).with_project_items(&[{
11149 let project_item = TestProjectItem::new_untitled(cx);
11150 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11151 project_item
11152 }])
11153 });
11154 let pane = workspace.update_in(cx, |workspace, window, cx| {
11155 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
11156 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
11157 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
11158 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
11159 workspace.active_pane().clone()
11160 });
11161
11162 let close_items = pane.update_in(cx, |pane, window, cx| {
11163 pane.activate_item(1, true, true, window, cx);
11164 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
11165 let item1_id = item1.item_id();
11166 let item3_id = item3.item_id();
11167 let item4_id = item4.item_id();
11168 pane.close_items(window, cx, SaveIntent::Close, &move |id| {
11169 [item1_id, item3_id, item4_id].contains(&id)
11170 })
11171 });
11172 cx.executor().run_until_parked();
11173
11174 assert!(cx.has_pending_prompt());
11175 cx.simulate_prompt_answer("Save all");
11176
11177 cx.executor().run_until_parked();
11178
11179 // Item 1 is saved. There's a prompt to save item 3.
11180 pane.update(cx, |pane, cx| {
11181 assert_eq!(item1.read(cx).save_count, 1);
11182 assert_eq!(item1.read(cx).save_as_count, 0);
11183 assert_eq!(item1.read(cx).reload_count, 0);
11184 assert_eq!(pane.items_len(), 3);
11185 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
11186 });
11187 assert!(cx.has_pending_prompt());
11188
11189 // Cancel saving item 3.
11190 cx.simulate_prompt_answer("Discard");
11191 cx.executor().run_until_parked();
11192
11193 // Item 3 is reloaded. There's a prompt to save item 4.
11194 pane.update(cx, |pane, cx| {
11195 assert_eq!(item3.read(cx).save_count, 0);
11196 assert_eq!(item3.read(cx).save_as_count, 0);
11197 assert_eq!(item3.read(cx).reload_count, 1);
11198 assert_eq!(pane.items_len(), 2);
11199 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
11200 });
11201
11202 // There's a prompt for a path for item 4.
11203 cx.simulate_new_path_selection(|_| Some(Default::default()));
11204 close_items.await.unwrap();
11205
11206 // The requested items are closed.
11207 pane.update(cx, |pane, cx| {
11208 assert_eq!(item4.read(cx).save_count, 0);
11209 assert_eq!(item4.read(cx).save_as_count, 1);
11210 assert_eq!(item4.read(cx).reload_count, 0);
11211 assert_eq!(pane.items_len(), 1);
11212 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
11213 });
11214 }
11215
11216 #[gpui::test]
11217 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
11218 init_test(cx);
11219
11220 let fs = FakeFs::new(cx.executor());
11221 let project = Project::test(fs, [], cx).await;
11222 let (workspace, cx) =
11223 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11224
11225 // Create several workspace items with single project entries, and two
11226 // workspace items with multiple project entries.
11227 let single_entry_items = (0..=4)
11228 .map(|project_entry_id| {
11229 cx.new(|cx| {
11230 TestItem::new(cx)
11231 .with_dirty(true)
11232 .with_project_items(&[dirty_project_item(
11233 project_entry_id,
11234 &format!("{project_entry_id}.txt"),
11235 cx,
11236 )])
11237 })
11238 })
11239 .collect::<Vec<_>>();
11240 let item_2_3 = cx.new(|cx| {
11241 TestItem::new(cx)
11242 .with_dirty(true)
11243 .with_buffer_kind(ItemBufferKind::Multibuffer)
11244 .with_project_items(&[
11245 single_entry_items[2].read(cx).project_items[0].clone(),
11246 single_entry_items[3].read(cx).project_items[0].clone(),
11247 ])
11248 });
11249 let item_3_4 = cx.new(|cx| {
11250 TestItem::new(cx)
11251 .with_dirty(true)
11252 .with_buffer_kind(ItemBufferKind::Multibuffer)
11253 .with_project_items(&[
11254 single_entry_items[3].read(cx).project_items[0].clone(),
11255 single_entry_items[4].read(cx).project_items[0].clone(),
11256 ])
11257 });
11258
11259 // Create two panes that contain the following project entries:
11260 // left pane:
11261 // multi-entry items: (2, 3)
11262 // single-entry items: 0, 2, 3, 4
11263 // right pane:
11264 // single-entry items: 4, 1
11265 // multi-entry items: (3, 4)
11266 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
11267 let left_pane = workspace.active_pane().clone();
11268 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
11269 workspace.add_item_to_active_pane(
11270 single_entry_items[0].boxed_clone(),
11271 None,
11272 true,
11273 window,
11274 cx,
11275 );
11276 workspace.add_item_to_active_pane(
11277 single_entry_items[2].boxed_clone(),
11278 None,
11279 true,
11280 window,
11281 cx,
11282 );
11283 workspace.add_item_to_active_pane(
11284 single_entry_items[3].boxed_clone(),
11285 None,
11286 true,
11287 window,
11288 cx,
11289 );
11290 workspace.add_item_to_active_pane(
11291 single_entry_items[4].boxed_clone(),
11292 None,
11293 true,
11294 window,
11295 cx,
11296 );
11297
11298 let right_pane =
11299 workspace.split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx);
11300
11301 let boxed_clone = single_entry_items[1].boxed_clone();
11302 let right_pane = window.spawn(cx, async move |cx| {
11303 right_pane.await.inspect(|right_pane| {
11304 right_pane
11305 .update_in(cx, |pane, window, cx| {
11306 pane.add_item(boxed_clone, true, true, None, window, cx);
11307 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
11308 })
11309 .unwrap();
11310 })
11311 });
11312
11313 (left_pane, right_pane)
11314 });
11315 let right_pane = right_pane.await.unwrap();
11316 cx.focus(&right_pane);
11317
11318 let close = right_pane.update_in(cx, |pane, window, cx| {
11319 pane.close_all_items(&CloseAllItems::default(), window, cx)
11320 .unwrap()
11321 });
11322 cx.executor().run_until_parked();
11323
11324 let msg = cx.pending_prompt().unwrap().0;
11325 assert!(msg.contains("1.txt"));
11326 assert!(!msg.contains("2.txt"));
11327 assert!(!msg.contains("3.txt"));
11328 assert!(!msg.contains("4.txt"));
11329
11330 // With best-effort close, cancelling item 1 keeps it open but items 4
11331 // and (3,4) still close since their entries exist in left pane.
11332 cx.simulate_prompt_answer("Cancel");
11333 close.await;
11334
11335 right_pane.read_with(cx, |pane, _| {
11336 assert_eq!(pane.items_len(), 1);
11337 });
11338
11339 // Remove item 3 from left pane, making (2,3) the only item with entry 3.
11340 left_pane
11341 .update_in(cx, |left_pane, window, cx| {
11342 left_pane.close_item_by_id(
11343 single_entry_items[3].entity_id(),
11344 SaveIntent::Skip,
11345 window,
11346 cx,
11347 )
11348 })
11349 .await
11350 .unwrap();
11351
11352 let close = left_pane.update_in(cx, |pane, window, cx| {
11353 pane.close_all_items(&CloseAllItems::default(), window, cx)
11354 .unwrap()
11355 });
11356 cx.executor().run_until_parked();
11357
11358 let details = cx.pending_prompt().unwrap().1;
11359 assert!(details.contains("0.txt"));
11360 assert!(details.contains("3.txt"));
11361 assert!(details.contains("4.txt"));
11362 // Ideally 2.txt wouldn't appear since entry 2 still exists in item 2.
11363 // But we can only save whole items, so saving (2,3) for entry 3 includes 2.
11364 // assert!(!details.contains("2.txt"));
11365
11366 cx.simulate_prompt_answer("Save all");
11367 cx.executor().run_until_parked();
11368 close.await;
11369
11370 left_pane.read_with(cx, |pane, _| {
11371 assert_eq!(pane.items_len(), 0);
11372 });
11373 }
11374
11375 #[gpui::test]
11376 async fn test_autosave(cx: &mut gpui::TestAppContext) {
11377 init_test(cx);
11378
11379 let fs = FakeFs::new(cx.executor());
11380 let project = Project::test(fs, [], cx).await;
11381 let (workspace, cx) =
11382 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11383 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11384
11385 let item = cx.new(|cx| {
11386 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11387 });
11388 let item_id = item.entity_id();
11389 workspace.update_in(cx, |workspace, window, cx| {
11390 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11391 });
11392
11393 // Autosave on window change.
11394 item.update(cx, |item, cx| {
11395 SettingsStore::update_global(cx, |settings, cx| {
11396 settings.update_user_settings(cx, |settings| {
11397 settings.workspace.autosave = Some(AutosaveSetting::OnWindowChange);
11398 })
11399 });
11400 item.is_dirty = true;
11401 });
11402
11403 // Deactivating the window saves the file.
11404 cx.deactivate_window();
11405 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11406
11407 // Re-activating the window doesn't save the file.
11408 cx.update(|window, _| window.activate_window());
11409 cx.executor().run_until_parked();
11410 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11411
11412 // Autosave on focus change.
11413 item.update_in(cx, |item, window, cx| {
11414 cx.focus_self(window);
11415 SettingsStore::update_global(cx, |settings, cx| {
11416 settings.update_user_settings(cx, |settings| {
11417 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11418 })
11419 });
11420 item.is_dirty = true;
11421 });
11422 // Blurring the item saves the file.
11423 item.update_in(cx, |_, window, _| window.blur());
11424 cx.executor().run_until_parked();
11425 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
11426
11427 // Deactivating the window still saves the file.
11428 item.update_in(cx, |item, window, cx| {
11429 cx.focus_self(window);
11430 item.is_dirty = true;
11431 });
11432 cx.deactivate_window();
11433 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
11434
11435 // Autosave after delay.
11436 item.update(cx, |item, cx| {
11437 SettingsStore::update_global(cx, |settings, cx| {
11438 settings.update_user_settings(cx, |settings| {
11439 settings.workspace.autosave = Some(AutosaveSetting::AfterDelay {
11440 milliseconds: 500.into(),
11441 });
11442 })
11443 });
11444 item.is_dirty = true;
11445 cx.emit(ItemEvent::Edit);
11446 });
11447
11448 // Delay hasn't fully expired, so the file is still dirty and unsaved.
11449 cx.executor().advance_clock(Duration::from_millis(250));
11450 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
11451
11452 // After delay expires, the file is saved.
11453 cx.executor().advance_clock(Duration::from_millis(250));
11454 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
11455
11456 // Autosave after delay, should save earlier than delay if tab is closed
11457 item.update(cx, |item, cx| {
11458 item.is_dirty = true;
11459 cx.emit(ItemEvent::Edit);
11460 });
11461 cx.executor().advance_clock(Duration::from_millis(250));
11462 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
11463
11464 // // Ensure auto save with delay saves the item on close, even if the timer hasn't yet run out.
11465 pane.update_in(cx, |pane, window, cx| {
11466 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11467 })
11468 .await
11469 .unwrap();
11470 assert!(!cx.has_pending_prompt());
11471 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
11472
11473 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
11474 workspace.update_in(cx, |workspace, window, cx| {
11475 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11476 });
11477 item.update_in(cx, |item, _window, cx| {
11478 item.is_dirty = true;
11479 for project_item in &mut item.project_items {
11480 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11481 }
11482 });
11483 cx.run_until_parked();
11484 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
11485
11486 // Autosave on focus change, ensuring closing the tab counts as such.
11487 item.update(cx, |item, cx| {
11488 SettingsStore::update_global(cx, |settings, cx| {
11489 settings.update_user_settings(cx, |settings| {
11490 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11491 })
11492 });
11493 item.is_dirty = true;
11494 for project_item in &mut item.project_items {
11495 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11496 }
11497 });
11498
11499 pane.update_in(cx, |pane, window, cx| {
11500 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11501 })
11502 .await
11503 .unwrap();
11504 assert!(!cx.has_pending_prompt());
11505 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11506
11507 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
11508 workspace.update_in(cx, |workspace, window, cx| {
11509 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11510 });
11511 item.update_in(cx, |item, window, cx| {
11512 item.project_items[0].update(cx, |item, _| {
11513 item.entry_id = None;
11514 });
11515 item.is_dirty = true;
11516 window.blur();
11517 });
11518 cx.run_until_parked();
11519 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11520
11521 // Ensure autosave is prevented for deleted files also when closing the buffer.
11522 let _close_items = pane.update_in(cx, |pane, window, cx| {
11523 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11524 });
11525 cx.run_until_parked();
11526 assert!(cx.has_pending_prompt());
11527 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11528 }
11529
11530 #[gpui::test]
11531 async fn test_autosave_on_focus_change_in_multibuffer(cx: &mut gpui::TestAppContext) {
11532 init_test(cx);
11533
11534 let fs = FakeFs::new(cx.executor());
11535 let project = Project::test(fs, [], cx).await;
11536 let (workspace, cx) =
11537 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11538
11539 // Create a multibuffer-like item with two child focus handles,
11540 // simulating individual buffer editors within a multibuffer.
11541 let item = cx.new(|cx| {
11542 TestItem::new(cx)
11543 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11544 .with_child_focus_handles(2, cx)
11545 });
11546 workspace.update_in(cx, |workspace, window, cx| {
11547 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11548 });
11549
11550 // Set autosave to OnFocusChange and focus the first child handle,
11551 // simulating the user's cursor being inside one of the multibuffer's excerpts.
11552 item.update_in(cx, |item, window, cx| {
11553 SettingsStore::update_global(cx, |settings, cx| {
11554 settings.update_user_settings(cx, |settings| {
11555 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11556 })
11557 });
11558 item.is_dirty = true;
11559 window.focus(&item.child_focus_handles[0], cx);
11560 });
11561 cx.executor().run_until_parked();
11562 item.read_with(cx, |item, _| assert_eq!(item.save_count, 0));
11563
11564 // Moving focus from one child to another within the same item should
11565 // NOT trigger autosave — focus is still within the item's focus hierarchy.
11566 item.update_in(cx, |item, window, cx| {
11567 window.focus(&item.child_focus_handles[1], cx);
11568 });
11569 cx.executor().run_until_parked();
11570 item.read_with(cx, |item, _| {
11571 assert_eq!(
11572 item.save_count, 0,
11573 "Switching focus between children within the same item should not autosave"
11574 );
11575 });
11576
11577 // Blurring the item saves the file. This is the core regression scenario:
11578 // with `on_blur`, this would NOT trigger because `on_blur` only fires when
11579 // the item's own focus handle is the leaf that lost focus. In a multibuffer,
11580 // the leaf is always a child focus handle, so `on_blur` never detected
11581 // focus leaving the item.
11582 item.update_in(cx, |_, window, _| window.blur());
11583 cx.executor().run_until_parked();
11584 item.read_with(cx, |item, _| {
11585 assert_eq!(
11586 item.save_count, 1,
11587 "Blurring should trigger autosave when focus was on a child of the item"
11588 );
11589 });
11590
11591 // Deactivating the window should also trigger autosave when a child of
11592 // the multibuffer item currently owns focus.
11593 item.update_in(cx, |item, window, cx| {
11594 item.is_dirty = true;
11595 window.focus(&item.child_focus_handles[0], cx);
11596 });
11597 cx.executor().run_until_parked();
11598 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11599
11600 cx.deactivate_window();
11601 item.read_with(cx, |item, _| {
11602 assert_eq!(
11603 item.save_count, 2,
11604 "Deactivating window should trigger autosave when focus was on a child"
11605 );
11606 });
11607 }
11608
11609 #[gpui::test]
11610 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
11611 init_test(cx);
11612
11613 let fs = FakeFs::new(cx.executor());
11614
11615 let project = Project::test(fs, [], cx).await;
11616 let (workspace, cx) =
11617 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11618
11619 let item = cx.new(|cx| {
11620 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11621 });
11622 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11623 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
11624 let toolbar_notify_count = Rc::new(RefCell::new(0));
11625
11626 workspace.update_in(cx, |workspace, window, cx| {
11627 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11628 let toolbar_notification_count = toolbar_notify_count.clone();
11629 cx.observe_in(&toolbar, window, move |_, _, _, _| {
11630 *toolbar_notification_count.borrow_mut() += 1
11631 })
11632 .detach();
11633 });
11634
11635 pane.read_with(cx, |pane, _| {
11636 assert!(!pane.can_navigate_backward());
11637 assert!(!pane.can_navigate_forward());
11638 });
11639
11640 item.update_in(cx, |item, _, cx| {
11641 item.set_state("one".to_string(), cx);
11642 });
11643
11644 // Toolbar must be notified to re-render the navigation buttons
11645 assert_eq!(*toolbar_notify_count.borrow(), 1);
11646
11647 pane.read_with(cx, |pane, _| {
11648 assert!(pane.can_navigate_backward());
11649 assert!(!pane.can_navigate_forward());
11650 });
11651
11652 workspace
11653 .update_in(cx, |workspace, window, cx| {
11654 workspace.go_back(pane.downgrade(), window, cx)
11655 })
11656 .await
11657 .unwrap();
11658
11659 assert_eq!(*toolbar_notify_count.borrow(), 2);
11660 pane.read_with(cx, |pane, _| {
11661 assert!(!pane.can_navigate_backward());
11662 assert!(pane.can_navigate_forward());
11663 });
11664 }
11665
11666 /// Tests that the navigation history deduplicates entries for the same item.
11667 ///
11668 /// When navigating back and forth between items (e.g., A -> B -> A -> B -> A -> B -> C),
11669 /// the navigation history deduplicates by keeping only the most recent visit to each item,
11670 /// resulting in [A, B, C] instead of [A, B, A, B, A, B, C]. This ensures that Go Back (Ctrl-O)
11671 /// navigates through unique items efficiently: C -> B -> A, rather than bouncing between
11672 /// repeated entries: C -> B -> A -> B -> A -> B -> A.
11673 ///
11674 /// This behavior prevents the navigation history from growing unnecessarily large and provides
11675 /// a better user experience by eliminating redundant navigation steps when jumping between files.
11676 #[gpui::test]
11677 async fn test_navigation_history_deduplication(cx: &mut gpui::TestAppContext) {
11678 init_test(cx);
11679
11680 let fs = FakeFs::new(cx.executor());
11681 let project = Project::test(fs, [], cx).await;
11682 let (workspace, cx) =
11683 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11684
11685 let item_a = cx.new(|cx| {
11686 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "a.txt", cx)])
11687 });
11688 let item_b = cx.new(|cx| {
11689 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "b.txt", cx)])
11690 });
11691 let item_c = cx.new(|cx| {
11692 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "c.txt", cx)])
11693 });
11694
11695 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11696
11697 workspace.update_in(cx, |workspace, window, cx| {
11698 workspace.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx);
11699 workspace.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx);
11700 workspace.add_item_to_active_pane(Box::new(item_c.clone()), None, true, window, cx);
11701 });
11702
11703 workspace.update_in(cx, |workspace, window, cx| {
11704 workspace.activate_item(&item_a, false, false, window, cx);
11705 });
11706 cx.run_until_parked();
11707
11708 workspace.update_in(cx, |workspace, window, cx| {
11709 workspace.activate_item(&item_b, false, false, window, cx);
11710 });
11711 cx.run_until_parked();
11712
11713 workspace.update_in(cx, |workspace, window, cx| {
11714 workspace.activate_item(&item_a, false, false, window, cx);
11715 });
11716 cx.run_until_parked();
11717
11718 workspace.update_in(cx, |workspace, window, cx| {
11719 workspace.activate_item(&item_b, false, false, window, cx);
11720 });
11721 cx.run_until_parked();
11722
11723 workspace.update_in(cx, |workspace, window, cx| {
11724 workspace.activate_item(&item_a, false, false, window, cx);
11725 });
11726 cx.run_until_parked();
11727
11728 workspace.update_in(cx, |workspace, window, cx| {
11729 workspace.activate_item(&item_b, false, false, window, cx);
11730 });
11731 cx.run_until_parked();
11732
11733 workspace.update_in(cx, |workspace, window, cx| {
11734 workspace.activate_item(&item_c, false, false, window, cx);
11735 });
11736 cx.run_until_parked();
11737
11738 let backward_count = pane.read_with(cx, |pane, cx| {
11739 let mut count = 0;
11740 pane.nav_history().for_each_entry(cx, &mut |_, _| {
11741 count += 1;
11742 });
11743 count
11744 });
11745 assert!(
11746 backward_count <= 4,
11747 "Should have at most 4 entries, got {}",
11748 backward_count
11749 );
11750
11751 workspace
11752 .update_in(cx, |workspace, window, cx| {
11753 workspace.go_back(pane.downgrade(), window, cx)
11754 })
11755 .await
11756 .unwrap();
11757
11758 let active_item = workspace.read_with(cx, |workspace, cx| {
11759 workspace.active_item(cx).unwrap().item_id()
11760 });
11761 assert_eq!(
11762 active_item,
11763 item_b.entity_id(),
11764 "After first go_back, should be at item B"
11765 );
11766
11767 workspace
11768 .update_in(cx, |workspace, window, cx| {
11769 workspace.go_back(pane.downgrade(), window, cx)
11770 })
11771 .await
11772 .unwrap();
11773
11774 let active_item = workspace.read_with(cx, |workspace, cx| {
11775 workspace.active_item(cx).unwrap().item_id()
11776 });
11777 assert_eq!(
11778 active_item,
11779 item_a.entity_id(),
11780 "After second go_back, should be at item A"
11781 );
11782
11783 pane.read_with(cx, |pane, _| {
11784 assert!(pane.can_navigate_forward(), "Should be able to go forward");
11785 });
11786 }
11787
11788 #[gpui::test]
11789 async fn test_activate_last_pane(cx: &mut gpui::TestAppContext) {
11790 init_test(cx);
11791 let fs = FakeFs::new(cx.executor());
11792 let project = Project::test(fs, [], cx).await;
11793 let (multi_workspace, cx) =
11794 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
11795 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
11796
11797 workspace.update_in(cx, |workspace, window, cx| {
11798 let first_item = cx.new(|cx| {
11799 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11800 });
11801 workspace.add_item_to_active_pane(Box::new(first_item), None, true, window, cx);
11802 workspace.split_pane(
11803 workspace.active_pane().clone(),
11804 SplitDirection::Right,
11805 window,
11806 cx,
11807 );
11808 workspace.split_pane(
11809 workspace.active_pane().clone(),
11810 SplitDirection::Right,
11811 window,
11812 cx,
11813 );
11814 });
11815
11816 let (first_pane_id, target_last_pane_id) = workspace.update(cx, |workspace, _cx| {
11817 let panes = workspace.center.panes();
11818 assert!(panes.len() >= 2);
11819 (
11820 panes.first().expect("at least one pane").entity_id(),
11821 panes.last().expect("at least one pane").entity_id(),
11822 )
11823 });
11824
11825 workspace.update_in(cx, |workspace, window, cx| {
11826 workspace.activate_pane_at_index(&ActivatePane(0), window, cx);
11827 });
11828 workspace.update(cx, |workspace, _| {
11829 assert_eq!(workspace.active_pane().entity_id(), first_pane_id);
11830 assert_ne!(workspace.active_pane().entity_id(), target_last_pane_id);
11831 });
11832
11833 cx.dispatch_action(ActivateLastPane);
11834
11835 workspace.update(cx, |workspace, _| {
11836 assert_eq!(workspace.active_pane().entity_id(), target_last_pane_id);
11837 });
11838 }
11839
11840 #[gpui::test]
11841 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
11842 init_test(cx);
11843 let fs = FakeFs::new(cx.executor());
11844
11845 let project = Project::test(fs, [], cx).await;
11846 let (workspace, cx) =
11847 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11848
11849 let panel = workspace.update_in(cx, |workspace, window, cx| {
11850 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
11851 workspace.add_panel(panel.clone(), window, cx);
11852
11853 workspace
11854 .right_dock()
11855 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
11856
11857 panel
11858 });
11859
11860 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11861 pane.update_in(cx, |pane, window, cx| {
11862 let item = cx.new(TestItem::new);
11863 pane.add_item(Box::new(item), true, true, None, window, cx);
11864 });
11865
11866 // Transfer focus from center to panel
11867 workspace.update_in(cx, |workspace, window, cx| {
11868 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11869 });
11870
11871 workspace.update_in(cx, |workspace, window, cx| {
11872 assert!(workspace.right_dock().read(cx).is_open());
11873 assert!(!panel.is_zoomed(window, cx));
11874 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11875 });
11876
11877 // Transfer focus from panel to center
11878 workspace.update_in(cx, |workspace, window, cx| {
11879 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11880 });
11881
11882 workspace.update_in(cx, |workspace, window, cx| {
11883 assert!(workspace.right_dock().read(cx).is_open());
11884 assert!(!panel.is_zoomed(window, cx));
11885 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11886 assert!(pane.read(cx).focus_handle(cx).contains_focused(window, cx));
11887 });
11888
11889 // Close the dock
11890 workspace.update_in(cx, |workspace, window, cx| {
11891 workspace.toggle_dock(DockPosition::Right, window, cx);
11892 });
11893
11894 workspace.update_in(cx, |workspace, window, cx| {
11895 assert!(!workspace.right_dock().read(cx).is_open());
11896 assert!(!panel.is_zoomed(window, cx));
11897 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11898 assert!(pane.read(cx).focus_handle(cx).contains_focused(window, cx));
11899 });
11900
11901 // Open the dock
11902 workspace.update_in(cx, |workspace, window, cx| {
11903 workspace.toggle_dock(DockPosition::Right, window, cx);
11904 });
11905
11906 workspace.update_in(cx, |workspace, window, cx| {
11907 assert!(workspace.right_dock().read(cx).is_open());
11908 assert!(!panel.is_zoomed(window, cx));
11909 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11910 });
11911
11912 // Focus and zoom panel
11913 panel.update_in(cx, |panel, window, cx| {
11914 cx.focus_self(window);
11915 panel.set_zoomed(true, window, cx)
11916 });
11917
11918 workspace.update_in(cx, |workspace, window, cx| {
11919 assert!(workspace.right_dock().read(cx).is_open());
11920 assert!(panel.is_zoomed(window, cx));
11921 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11922 });
11923
11924 // Transfer focus to the center closes the dock
11925 workspace.update_in(cx, |workspace, window, cx| {
11926 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11927 });
11928
11929 workspace.update_in(cx, |workspace, window, cx| {
11930 assert!(!workspace.right_dock().read(cx).is_open());
11931 assert!(panel.is_zoomed(window, cx));
11932 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11933 });
11934
11935 // Transferring focus back to the panel keeps it zoomed
11936 workspace.update_in(cx, |workspace, window, cx| {
11937 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11938 });
11939
11940 workspace.update_in(cx, |workspace, window, cx| {
11941 assert!(workspace.right_dock().read(cx).is_open());
11942 assert!(panel.is_zoomed(window, cx));
11943 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11944 });
11945
11946 // Close the dock while it is zoomed
11947 workspace.update_in(cx, |workspace, window, cx| {
11948 workspace.toggle_dock(DockPosition::Right, window, cx)
11949 });
11950
11951 workspace.update_in(cx, |workspace, window, cx| {
11952 assert!(!workspace.right_dock().read(cx).is_open());
11953 assert!(panel.is_zoomed(window, cx));
11954 assert!(workspace.zoomed.is_none());
11955 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11956 });
11957
11958 // Opening the dock, when it's zoomed, retains focus
11959 workspace.update_in(cx, |workspace, window, cx| {
11960 workspace.toggle_dock(DockPosition::Right, window, cx)
11961 });
11962
11963 workspace.update_in(cx, |workspace, window, cx| {
11964 assert!(workspace.right_dock().read(cx).is_open());
11965 assert!(panel.is_zoomed(window, cx));
11966 assert!(workspace.zoomed.is_some());
11967 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11968 });
11969
11970 // Unzoom and close the panel, zoom the active pane.
11971 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
11972 workspace.update_in(cx, |workspace, window, cx| {
11973 workspace.toggle_dock(DockPosition::Right, window, cx)
11974 });
11975 pane.update_in(cx, |pane, window, cx| {
11976 pane.toggle_zoom(&Default::default(), window, cx)
11977 });
11978
11979 // Opening a dock unzooms the pane.
11980 workspace.update_in(cx, |workspace, window, cx| {
11981 workspace.toggle_dock(DockPosition::Right, window, cx)
11982 });
11983 workspace.update_in(cx, |workspace, window, cx| {
11984 let pane = pane.read(cx);
11985 assert!(!pane.is_zoomed());
11986 assert!(!pane.focus_handle(cx).is_focused(window));
11987 assert!(workspace.right_dock().read(cx).is_open());
11988 assert!(workspace.zoomed.is_none());
11989 });
11990 }
11991
11992 #[gpui::test]
11993 async fn test_close_panel_on_toggle(cx: &mut gpui::TestAppContext) {
11994 init_test(cx);
11995 let fs = FakeFs::new(cx.executor());
11996
11997 let project = Project::test(fs, [], cx).await;
11998 let (workspace, cx) =
11999 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12000
12001 let panel = workspace.update_in(cx, |workspace, window, cx| {
12002 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
12003 workspace.add_panel(panel.clone(), window, cx);
12004 panel
12005 });
12006
12007 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12008 pane.update_in(cx, |pane, window, cx| {
12009 let item = cx.new(TestItem::new);
12010 pane.add_item(Box::new(item), true, true, None, window, cx);
12011 });
12012
12013 // Enable close_panel_on_toggle
12014 cx.update_global(|store: &mut SettingsStore, cx| {
12015 store.update_user_settings(cx, |settings| {
12016 settings.workspace.close_panel_on_toggle = Some(true);
12017 });
12018 });
12019
12020 // Panel starts closed. Toggling should open and focus it.
12021 workspace.update_in(cx, |workspace, window, cx| {
12022 assert!(!workspace.right_dock().read(cx).is_open());
12023 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12024 });
12025
12026 workspace.update_in(cx, |workspace, window, cx| {
12027 assert!(
12028 workspace.right_dock().read(cx).is_open(),
12029 "Dock should be open after toggling from center"
12030 );
12031 assert!(
12032 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12033 "Panel should be focused after toggling from center"
12034 );
12035 });
12036
12037 // Panel is open and focused. Toggling should close the panel and
12038 // return focus to the center.
12039 workspace.update_in(cx, |workspace, window, cx| {
12040 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12041 });
12042
12043 workspace.update_in(cx, |workspace, window, cx| {
12044 assert!(
12045 !workspace.right_dock().read(cx).is_open(),
12046 "Dock should be closed after toggling from focused panel"
12047 );
12048 assert!(
12049 !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12050 "Panel should not be focused after toggling from focused panel"
12051 );
12052 });
12053
12054 // Open the dock and focus something else so the panel is open but not
12055 // focused. Toggling should focus the panel (not close it).
12056 workspace.update_in(cx, |workspace, window, cx| {
12057 workspace
12058 .right_dock()
12059 .update(cx, |dock, cx| dock.set_open(true, window, cx));
12060 window.focus(&pane.read(cx).focus_handle(cx), cx);
12061 });
12062
12063 workspace.update_in(cx, |workspace, window, cx| {
12064 assert!(workspace.right_dock().read(cx).is_open());
12065 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12066 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12067 });
12068
12069 workspace.update_in(cx, |workspace, window, cx| {
12070 assert!(
12071 workspace.right_dock().read(cx).is_open(),
12072 "Dock should remain open when toggling focuses an open-but-unfocused panel"
12073 );
12074 assert!(
12075 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12076 "Panel should be focused after toggling an open-but-unfocused panel"
12077 );
12078 });
12079
12080 // Now disable the setting and verify the original behavior: toggling
12081 // from a focused panel moves focus to center but leaves the dock open.
12082 cx.update_global(|store: &mut SettingsStore, cx| {
12083 store.update_user_settings(cx, |settings| {
12084 settings.workspace.close_panel_on_toggle = Some(false);
12085 });
12086 });
12087
12088 workspace.update_in(cx, |workspace, window, cx| {
12089 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12090 });
12091
12092 workspace.update_in(cx, |workspace, window, cx| {
12093 assert!(
12094 workspace.right_dock().read(cx).is_open(),
12095 "Dock should remain open when setting is disabled"
12096 );
12097 assert!(
12098 !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12099 "Panel should not be focused after toggling with setting disabled"
12100 );
12101 });
12102 }
12103
12104 #[gpui::test]
12105 async fn test_pane_zoom_in_out(cx: &mut TestAppContext) {
12106 init_test(cx);
12107 let fs = FakeFs::new(cx.executor());
12108
12109 let project = Project::test(fs, [], cx).await;
12110 let (workspace, cx) =
12111 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12112
12113 let pane = workspace.update_in(cx, |workspace, _window, _cx| {
12114 workspace.active_pane().clone()
12115 });
12116
12117 // Add an item to the pane so it can be zoomed
12118 workspace.update_in(cx, |workspace, window, cx| {
12119 let item = cx.new(TestItem::new);
12120 workspace.add_item(pane.clone(), Box::new(item), None, true, true, window, cx);
12121 });
12122
12123 // Initially not zoomed
12124 workspace.update_in(cx, |workspace, _window, cx| {
12125 assert!(!pane.read(cx).is_zoomed(), "Pane starts unzoomed");
12126 assert!(
12127 workspace.zoomed.is_none(),
12128 "Workspace should track no zoomed pane"
12129 );
12130 assert!(pane.read(cx).items_len() > 0, "Pane should have items");
12131 });
12132
12133 // Zoom In
12134 pane.update_in(cx, |pane, window, cx| {
12135 pane.zoom_in(&crate::ZoomIn, window, cx);
12136 });
12137
12138 workspace.update_in(cx, |workspace, window, cx| {
12139 assert!(
12140 pane.read(cx).is_zoomed(),
12141 "Pane should be zoomed after ZoomIn"
12142 );
12143 assert!(
12144 workspace.zoomed.is_some(),
12145 "Workspace should track the zoomed pane"
12146 );
12147 assert!(
12148 pane.read(cx).focus_handle(cx).contains_focused(window, cx),
12149 "ZoomIn should focus the pane"
12150 );
12151 });
12152
12153 // Zoom In again is a no-op
12154 pane.update_in(cx, |pane, window, cx| {
12155 pane.zoom_in(&crate::ZoomIn, window, cx);
12156 });
12157
12158 workspace.update_in(cx, |workspace, window, cx| {
12159 assert!(pane.read(cx).is_zoomed(), "Second ZoomIn keeps pane zoomed");
12160 assert!(
12161 workspace.zoomed.is_some(),
12162 "Workspace still tracks zoomed pane"
12163 );
12164 assert!(
12165 pane.read(cx).focus_handle(cx).contains_focused(window, cx),
12166 "Pane remains focused after repeated ZoomIn"
12167 );
12168 });
12169
12170 // Zoom Out
12171 pane.update_in(cx, |pane, window, cx| {
12172 pane.zoom_out(&crate::ZoomOut, window, cx);
12173 });
12174
12175 workspace.update_in(cx, |workspace, _window, cx| {
12176 assert!(
12177 !pane.read(cx).is_zoomed(),
12178 "Pane should unzoom after ZoomOut"
12179 );
12180 assert!(
12181 workspace.zoomed.is_none(),
12182 "Workspace clears zoom tracking after ZoomOut"
12183 );
12184 });
12185
12186 // Zoom Out again is a no-op
12187 pane.update_in(cx, |pane, window, cx| {
12188 pane.zoom_out(&crate::ZoomOut, window, cx);
12189 });
12190
12191 workspace.update_in(cx, |workspace, _window, cx| {
12192 assert!(
12193 !pane.read(cx).is_zoomed(),
12194 "Second ZoomOut keeps pane unzoomed"
12195 );
12196 assert!(
12197 workspace.zoomed.is_none(),
12198 "Workspace remains without zoomed pane"
12199 );
12200 });
12201 }
12202
12203 #[gpui::test]
12204 async fn test_toggle_all_docks(cx: &mut gpui::TestAppContext) {
12205 init_test(cx);
12206 let fs = FakeFs::new(cx.executor());
12207
12208 let project = Project::test(fs, [], cx).await;
12209 let (workspace, cx) =
12210 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12211 workspace.update_in(cx, |workspace, window, cx| {
12212 // Open two docks
12213 let left_dock = workspace.dock_at_position(DockPosition::Left);
12214 let right_dock = workspace.dock_at_position(DockPosition::Right);
12215
12216 left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12217 right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12218
12219 assert!(left_dock.read(cx).is_open());
12220 assert!(right_dock.read(cx).is_open());
12221 });
12222
12223 workspace.update_in(cx, |workspace, window, cx| {
12224 // Toggle all docks - should close both
12225 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12226
12227 let left_dock = workspace.dock_at_position(DockPosition::Left);
12228 let right_dock = workspace.dock_at_position(DockPosition::Right);
12229 assert!(!left_dock.read(cx).is_open());
12230 assert!(!right_dock.read(cx).is_open());
12231 });
12232
12233 workspace.update_in(cx, |workspace, window, cx| {
12234 // Toggle again - should reopen both
12235 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12236
12237 let left_dock = workspace.dock_at_position(DockPosition::Left);
12238 let right_dock = workspace.dock_at_position(DockPosition::Right);
12239 assert!(left_dock.read(cx).is_open());
12240 assert!(right_dock.read(cx).is_open());
12241 });
12242 }
12243
12244 #[gpui::test]
12245 async fn test_toggle_all_with_manual_close(cx: &mut gpui::TestAppContext) {
12246 init_test(cx);
12247 let fs = FakeFs::new(cx.executor());
12248
12249 let project = Project::test(fs, [], cx).await;
12250 let (workspace, cx) =
12251 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12252 workspace.update_in(cx, |workspace, window, cx| {
12253 // Open two docks
12254 let left_dock = workspace.dock_at_position(DockPosition::Left);
12255 let right_dock = workspace.dock_at_position(DockPosition::Right);
12256
12257 left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12258 right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12259
12260 assert!(left_dock.read(cx).is_open());
12261 assert!(right_dock.read(cx).is_open());
12262 });
12263
12264 workspace.update_in(cx, |workspace, window, cx| {
12265 // Close them manually
12266 workspace.toggle_dock(DockPosition::Left, window, cx);
12267 workspace.toggle_dock(DockPosition::Right, window, cx);
12268
12269 let left_dock = workspace.dock_at_position(DockPosition::Left);
12270 let right_dock = workspace.dock_at_position(DockPosition::Right);
12271 assert!(!left_dock.read(cx).is_open());
12272 assert!(!right_dock.read(cx).is_open());
12273 });
12274
12275 workspace.update_in(cx, |workspace, window, cx| {
12276 // Toggle all docks - only last closed (right dock) should reopen
12277 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12278
12279 let left_dock = workspace.dock_at_position(DockPosition::Left);
12280 let right_dock = workspace.dock_at_position(DockPosition::Right);
12281 assert!(!left_dock.read(cx).is_open());
12282 assert!(right_dock.read(cx).is_open());
12283 });
12284 }
12285
12286 #[gpui::test]
12287 async fn test_toggle_all_docks_after_dock_move(cx: &mut gpui::TestAppContext) {
12288 init_test(cx);
12289 let fs = FakeFs::new(cx.executor());
12290 let project = Project::test(fs, [], cx).await;
12291 let (multi_workspace, cx) =
12292 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12293 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12294
12295 // Open two docks (left and right) with one panel each
12296 let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| {
12297 let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
12298 workspace.add_panel(left_panel.clone(), window, cx);
12299
12300 let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
12301 workspace.add_panel(right_panel.clone(), window, cx);
12302
12303 workspace.toggle_dock(DockPosition::Left, window, cx);
12304 workspace.toggle_dock(DockPosition::Right, window, cx);
12305
12306 // Verify initial state
12307 assert!(
12308 workspace.left_dock().read(cx).is_open(),
12309 "Left dock should be open"
12310 );
12311 assert_eq!(
12312 workspace
12313 .left_dock()
12314 .read(cx)
12315 .visible_panel()
12316 .unwrap()
12317 .panel_id(),
12318 left_panel.panel_id(),
12319 "Left panel should be visible in left dock"
12320 );
12321 assert!(
12322 workspace.right_dock().read(cx).is_open(),
12323 "Right dock should be open"
12324 );
12325 assert_eq!(
12326 workspace
12327 .right_dock()
12328 .read(cx)
12329 .visible_panel()
12330 .unwrap()
12331 .panel_id(),
12332 right_panel.panel_id(),
12333 "Right panel should be visible in right dock"
12334 );
12335 assert!(
12336 !workspace.bottom_dock().read(cx).is_open(),
12337 "Bottom dock should be closed"
12338 );
12339
12340 (left_panel, right_panel)
12341 });
12342
12343 // Focus the left panel and move it to the next position (bottom dock)
12344 workspace.update_in(cx, |workspace, window, cx| {
12345 workspace.toggle_panel_focus::<TestPanel>(window, cx); // Focus left panel
12346 assert!(
12347 left_panel.read(cx).focus_handle(cx).is_focused(window),
12348 "Left panel should be focused"
12349 );
12350 });
12351
12352 cx.dispatch_action(MoveFocusedPanelToNextPosition);
12353
12354 // Verify the left panel has moved to the bottom dock, and the bottom dock is now open
12355 workspace.update(cx, |workspace, cx| {
12356 assert!(
12357 !workspace.left_dock().read(cx).is_open(),
12358 "Left dock should be closed"
12359 );
12360 assert!(
12361 workspace.bottom_dock().read(cx).is_open(),
12362 "Bottom dock should now be open"
12363 );
12364 assert_eq!(
12365 left_panel.read(cx).position,
12366 DockPosition::Bottom,
12367 "Left panel should now be in the bottom dock"
12368 );
12369 assert_eq!(
12370 workspace
12371 .bottom_dock()
12372 .read(cx)
12373 .visible_panel()
12374 .unwrap()
12375 .panel_id(),
12376 left_panel.panel_id(),
12377 "Left panel should be the visible panel in the bottom dock"
12378 );
12379 });
12380
12381 // Toggle all docks off
12382 workspace.update_in(cx, |workspace, window, cx| {
12383 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12384 assert!(
12385 !workspace.left_dock().read(cx).is_open(),
12386 "Left dock should be closed"
12387 );
12388 assert!(
12389 !workspace.right_dock().read(cx).is_open(),
12390 "Right dock should be closed"
12391 );
12392 assert!(
12393 !workspace.bottom_dock().read(cx).is_open(),
12394 "Bottom dock should be closed"
12395 );
12396 });
12397
12398 // Toggle all docks back on and verify positions are restored
12399 workspace.update_in(cx, |workspace, window, cx| {
12400 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12401 assert!(
12402 !workspace.left_dock().read(cx).is_open(),
12403 "Left dock should remain closed"
12404 );
12405 assert!(
12406 workspace.right_dock().read(cx).is_open(),
12407 "Right dock should remain open"
12408 );
12409 assert!(
12410 workspace.bottom_dock().read(cx).is_open(),
12411 "Bottom dock should remain open"
12412 );
12413 assert_eq!(
12414 left_panel.read(cx).position,
12415 DockPosition::Bottom,
12416 "Left panel should remain in the bottom dock"
12417 );
12418 assert_eq!(
12419 right_panel.read(cx).position,
12420 DockPosition::Right,
12421 "Right panel should remain in the right dock"
12422 );
12423 assert_eq!(
12424 workspace
12425 .bottom_dock()
12426 .read(cx)
12427 .visible_panel()
12428 .unwrap()
12429 .panel_id(),
12430 left_panel.panel_id(),
12431 "Left panel should be the visible panel in the right dock"
12432 );
12433 });
12434 }
12435
12436 #[gpui::test]
12437 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
12438 init_test(cx);
12439
12440 let fs = FakeFs::new(cx.executor());
12441
12442 let project = Project::test(fs, None, cx).await;
12443 let (workspace, cx) =
12444 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12445
12446 // Let's arrange the panes like this:
12447 //
12448 // +-----------------------+
12449 // | top |
12450 // +------+--------+-------+
12451 // | left | center | right |
12452 // +------+--------+-------+
12453 // | bottom |
12454 // +-----------------------+
12455
12456 let top_item = cx.new(|cx| {
12457 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
12458 });
12459 let bottom_item = cx.new(|cx| {
12460 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
12461 });
12462 let left_item = cx.new(|cx| {
12463 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
12464 });
12465 let right_item = cx.new(|cx| {
12466 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
12467 });
12468 let center_item = cx.new(|cx| {
12469 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
12470 });
12471
12472 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12473 let top_pane_id = workspace.active_pane().entity_id();
12474 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
12475 workspace.split_pane(
12476 workspace.active_pane().clone(),
12477 SplitDirection::Down,
12478 window,
12479 cx,
12480 );
12481 top_pane_id
12482 });
12483 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12484 let bottom_pane_id = workspace.active_pane().entity_id();
12485 workspace.add_item_to_active_pane(
12486 Box::new(bottom_item.clone()),
12487 None,
12488 false,
12489 window,
12490 cx,
12491 );
12492 workspace.split_pane(
12493 workspace.active_pane().clone(),
12494 SplitDirection::Up,
12495 window,
12496 cx,
12497 );
12498 bottom_pane_id
12499 });
12500 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12501 let left_pane_id = workspace.active_pane().entity_id();
12502 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
12503 workspace.split_pane(
12504 workspace.active_pane().clone(),
12505 SplitDirection::Right,
12506 window,
12507 cx,
12508 );
12509 left_pane_id
12510 });
12511 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12512 let right_pane_id = workspace.active_pane().entity_id();
12513 workspace.add_item_to_active_pane(
12514 Box::new(right_item.clone()),
12515 None,
12516 false,
12517 window,
12518 cx,
12519 );
12520 workspace.split_pane(
12521 workspace.active_pane().clone(),
12522 SplitDirection::Left,
12523 window,
12524 cx,
12525 );
12526 right_pane_id
12527 });
12528 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12529 let center_pane_id = workspace.active_pane().entity_id();
12530 workspace.add_item_to_active_pane(
12531 Box::new(center_item.clone()),
12532 None,
12533 false,
12534 window,
12535 cx,
12536 );
12537 center_pane_id
12538 });
12539 cx.executor().run_until_parked();
12540
12541 workspace.update_in(cx, |workspace, window, cx| {
12542 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
12543
12544 // Join into next from center pane into right
12545 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12546 });
12547
12548 workspace.update_in(cx, |workspace, window, cx| {
12549 let active_pane = workspace.active_pane();
12550 assert_eq!(right_pane_id, active_pane.entity_id());
12551 assert_eq!(2, active_pane.read(cx).items_len());
12552 let item_ids_in_pane =
12553 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12554 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12555 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12556
12557 // Join into next from right pane into bottom
12558 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12559 });
12560
12561 workspace.update_in(cx, |workspace, window, cx| {
12562 let active_pane = workspace.active_pane();
12563 assert_eq!(bottom_pane_id, active_pane.entity_id());
12564 assert_eq!(3, active_pane.read(cx).items_len());
12565 let item_ids_in_pane =
12566 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12567 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12568 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12569 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12570
12571 // Join into next from bottom pane into left
12572 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12573 });
12574
12575 workspace.update_in(cx, |workspace, window, cx| {
12576 let active_pane = workspace.active_pane();
12577 assert_eq!(left_pane_id, active_pane.entity_id());
12578 assert_eq!(4, active_pane.read(cx).items_len());
12579 let item_ids_in_pane =
12580 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12581 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12582 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12583 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12584 assert!(item_ids_in_pane.contains(&left_item.item_id()));
12585
12586 // Join into next from left pane into top
12587 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12588 });
12589
12590 workspace.update_in(cx, |workspace, window, cx| {
12591 let active_pane = workspace.active_pane();
12592 assert_eq!(top_pane_id, active_pane.entity_id());
12593 assert_eq!(5, active_pane.read(cx).items_len());
12594 let item_ids_in_pane =
12595 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12596 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12597 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12598 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12599 assert!(item_ids_in_pane.contains(&left_item.item_id()));
12600 assert!(item_ids_in_pane.contains(&top_item.item_id()));
12601
12602 // Single pane left: no-op
12603 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
12604 });
12605
12606 workspace.update(cx, |workspace, _cx| {
12607 let active_pane = workspace.active_pane();
12608 assert_eq!(top_pane_id, active_pane.entity_id());
12609 });
12610 }
12611
12612 fn add_an_item_to_active_pane(
12613 cx: &mut VisualTestContext,
12614 workspace: &Entity<Workspace>,
12615 item_id: u64,
12616 ) -> Entity<TestItem> {
12617 let item = cx.new(|cx| {
12618 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
12619 item_id,
12620 "item{item_id}.txt",
12621 cx,
12622 )])
12623 });
12624 workspace.update_in(cx, |workspace, window, cx| {
12625 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
12626 });
12627 item
12628 }
12629
12630 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
12631 workspace.update_in(cx, |workspace, window, cx| {
12632 workspace.split_pane(
12633 workspace.active_pane().clone(),
12634 SplitDirection::Right,
12635 window,
12636 cx,
12637 )
12638 })
12639 }
12640
12641 #[gpui::test]
12642 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
12643 init_test(cx);
12644 let fs = FakeFs::new(cx.executor());
12645 let project = Project::test(fs, None, cx).await;
12646 let (workspace, cx) =
12647 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12648
12649 add_an_item_to_active_pane(cx, &workspace, 1);
12650 split_pane(cx, &workspace);
12651 add_an_item_to_active_pane(cx, &workspace, 2);
12652 split_pane(cx, &workspace); // empty pane
12653 split_pane(cx, &workspace);
12654 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
12655
12656 cx.executor().run_until_parked();
12657
12658 workspace.update(cx, |workspace, cx| {
12659 let num_panes = workspace.panes().len();
12660 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
12661 let active_item = workspace
12662 .active_pane()
12663 .read(cx)
12664 .active_item()
12665 .expect("item is in focus");
12666
12667 assert_eq!(num_panes, 4);
12668 assert_eq!(num_items_in_current_pane, 1);
12669 assert_eq!(active_item.item_id(), last_item.item_id());
12670 });
12671
12672 workspace.update_in(cx, |workspace, window, cx| {
12673 workspace.join_all_panes(window, cx);
12674 });
12675
12676 workspace.update(cx, |workspace, cx| {
12677 let num_panes = workspace.panes().len();
12678 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
12679 let active_item = workspace
12680 .active_pane()
12681 .read(cx)
12682 .active_item()
12683 .expect("item is in focus");
12684
12685 assert_eq!(num_panes, 1);
12686 assert_eq!(num_items_in_current_pane, 3);
12687 assert_eq!(active_item.item_id(), last_item.item_id());
12688 });
12689 }
12690
12691 #[gpui::test]
12692 async fn test_flexible_dock_sizing(cx: &mut gpui::TestAppContext) {
12693 init_test(cx);
12694 let fs = FakeFs::new(cx.executor());
12695
12696 let project = Project::test(fs, [], cx).await;
12697 let (multi_workspace, cx) =
12698 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12699 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12700
12701 workspace.update(cx, |workspace, _cx| {
12702 workspace.bounds.size.width = px(800.);
12703 });
12704
12705 workspace.update_in(cx, |workspace, window, cx| {
12706 let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
12707 workspace.add_panel(panel, window, cx);
12708 workspace.toggle_dock(DockPosition::Right, window, cx);
12709 });
12710
12711 let (panel, resized_width, ratio_basis_width) =
12712 workspace.update_in(cx, |workspace, window, cx| {
12713 let item = cx.new(|cx| {
12714 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
12715 });
12716 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
12717
12718 let dock = workspace.right_dock().read(cx);
12719 let workspace_width = workspace.bounds.size.width;
12720 let initial_width = workspace
12721 .dock_size(&dock, window, cx)
12722 .expect("flexible dock should have an initial width");
12723
12724 assert_eq!(initial_width, workspace_width / 2.);
12725
12726 workspace.resize_right_dock(px(300.), window, cx);
12727
12728 let dock = workspace.right_dock().read(cx);
12729 let resized_width = workspace
12730 .dock_size(&dock, window, cx)
12731 .expect("flexible dock should keep its resized width");
12732
12733 assert_eq!(resized_width, px(300.));
12734
12735 let panel = workspace
12736 .right_dock()
12737 .read(cx)
12738 .visible_panel()
12739 .expect("flexible dock should have a visible panel")
12740 .panel_id();
12741
12742 (panel, resized_width, workspace_width)
12743 });
12744
12745 workspace.update_in(cx, |workspace, window, cx| {
12746 workspace.toggle_dock(DockPosition::Right, window, cx);
12747 workspace.toggle_dock(DockPosition::Right, window, cx);
12748
12749 let dock = workspace.right_dock().read(cx);
12750 let reopened_width = workspace
12751 .dock_size(&dock, window, cx)
12752 .expect("flexible dock should restore when reopened");
12753
12754 assert_eq!(reopened_width, resized_width);
12755
12756 let right_dock = workspace.right_dock().read(cx);
12757 let flexible_panel = right_dock
12758 .visible_panel()
12759 .expect("flexible dock should still have a visible panel");
12760 assert_eq!(flexible_panel.panel_id(), panel);
12761 assert_eq!(
12762 right_dock
12763 .stored_panel_size_state(flexible_panel.as_ref())
12764 .and_then(|size_state| size_state.flex),
12765 Some(
12766 resized_width.to_f64() as f32
12767 / (workspace.bounds.size.width - resized_width).to_f64() as f32
12768 )
12769 );
12770 });
12771
12772 workspace.update_in(cx, |workspace, window, cx| {
12773 workspace.split_pane(
12774 workspace.active_pane().clone(),
12775 SplitDirection::Right,
12776 window,
12777 cx,
12778 );
12779
12780 let dock = workspace.right_dock().read(cx);
12781 let split_width = workspace
12782 .dock_size(&dock, window, cx)
12783 .expect("flexible dock should keep its user-resized proportion");
12784
12785 assert_eq!(split_width, px(300.));
12786
12787 workspace.bounds.size.width = px(1600.);
12788
12789 let dock = workspace.right_dock().read(cx);
12790 let resized_window_width = workspace
12791 .dock_size(&dock, window, cx)
12792 .expect("flexible dock should preserve proportional size on window resize");
12793
12794 assert_eq!(
12795 resized_window_width,
12796 workspace.bounds.size.width
12797 * (resized_width.to_f64() as f32 / ratio_basis_width.to_f64() as f32)
12798 );
12799 });
12800 }
12801
12802 #[gpui::test]
12803 async fn test_panel_size_state_persistence(cx: &mut gpui::TestAppContext) {
12804 init_test(cx);
12805 let fs = FakeFs::new(cx.executor());
12806
12807 // Fixed-width panel: pixel size is persisted to KVP and restored on re-add.
12808 {
12809 let project = Project::test(fs.clone(), [], cx).await;
12810 let (multi_workspace, cx) =
12811 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12812 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12813
12814 workspace.update(cx, |workspace, _cx| {
12815 workspace.set_random_database_id();
12816 workspace.bounds.size.width = px(800.);
12817 });
12818
12819 let panel = workspace.update_in(cx, |workspace, window, cx| {
12820 let panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
12821 workspace.add_panel(panel.clone(), window, cx);
12822 workspace.toggle_dock(DockPosition::Left, window, cx);
12823 panel
12824 });
12825
12826 workspace.update_in(cx, |workspace, window, cx| {
12827 workspace.resize_left_dock(px(350.), window, cx);
12828 });
12829
12830 cx.run_until_parked();
12831
12832 let persisted = workspace.read_with(cx, |workspace, cx| {
12833 workspace.persisted_panel_size_state(TestPanel::panel_key(), cx)
12834 });
12835 assert_eq!(
12836 persisted.and_then(|s| s.size),
12837 Some(px(350.)),
12838 "fixed-width panel size should be persisted to KVP"
12839 );
12840
12841 // Remove the panel and re-add a fresh instance with the same key.
12842 // The new instance should have its size state restored from KVP.
12843 workspace.update_in(cx, |workspace, window, cx| {
12844 workspace.remove_panel(&panel, window, cx);
12845 });
12846
12847 workspace.update_in(cx, |workspace, window, cx| {
12848 let new_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
12849 workspace.add_panel(new_panel, window, cx);
12850
12851 let left_dock = workspace.left_dock().read(cx);
12852 let size_state = left_dock
12853 .panel::<TestPanel>()
12854 .and_then(|p| left_dock.stored_panel_size_state(&p));
12855 assert_eq!(
12856 size_state.and_then(|s| s.size),
12857 Some(px(350.)),
12858 "re-added fixed-width panel should restore persisted size from KVP"
12859 );
12860 });
12861 }
12862
12863 // Flexible panel: both pixel size and ratio are persisted and restored.
12864 {
12865 let project = Project::test(fs.clone(), [], cx).await;
12866 let (multi_workspace, cx) =
12867 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12868 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12869
12870 workspace.update(cx, |workspace, _cx| {
12871 workspace.set_random_database_id();
12872 workspace.bounds.size.width = px(800.);
12873 });
12874
12875 let panel = workspace.update_in(cx, |workspace, window, cx| {
12876 let item = cx.new(|cx| {
12877 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
12878 });
12879 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
12880
12881 let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
12882 workspace.add_panel(panel.clone(), window, cx);
12883 workspace.toggle_dock(DockPosition::Right, window, cx);
12884 panel
12885 });
12886
12887 workspace.update_in(cx, |workspace, window, cx| {
12888 workspace.resize_right_dock(px(300.), window, cx);
12889 });
12890
12891 cx.run_until_parked();
12892
12893 let persisted = workspace
12894 .read_with(cx, |workspace, cx| {
12895 workspace.persisted_panel_size_state(TestPanel::panel_key(), cx)
12896 })
12897 .expect("flexible panel state should be persisted to KVP");
12898 assert_eq!(
12899 persisted.size, None,
12900 "flexible panel should not persist a redundant pixel size"
12901 );
12902 let original_ratio = persisted.flex.expect("panel's flex should be persisted");
12903
12904 // Remove the panel and re-add: both size and ratio should be restored.
12905 workspace.update_in(cx, |workspace, window, cx| {
12906 workspace.remove_panel(&panel, window, cx);
12907 });
12908
12909 workspace.update_in(cx, |workspace, window, cx| {
12910 let new_panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
12911 workspace.add_panel(new_panel, window, cx);
12912
12913 let right_dock = workspace.right_dock().read(cx);
12914 let size_state = right_dock
12915 .panel::<TestPanel>()
12916 .and_then(|p| right_dock.stored_panel_size_state(&p))
12917 .expect("re-added flexible panel should have restored size state from KVP");
12918 assert_eq!(
12919 size_state.size, None,
12920 "re-added flexible panel should not have a persisted pixel size"
12921 );
12922 assert_eq!(
12923 size_state.flex,
12924 Some(original_ratio),
12925 "re-added flexible panel should restore persisted flex"
12926 );
12927 });
12928 }
12929 }
12930
12931 #[gpui::test]
12932 async fn test_flexible_panel_left_dock_sizing(cx: &mut gpui::TestAppContext) {
12933 init_test(cx);
12934 let fs = FakeFs::new(cx.executor());
12935
12936 let project = Project::test(fs, [], cx).await;
12937 let (multi_workspace, cx) =
12938 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12939 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12940
12941 workspace.update(cx, |workspace, _cx| {
12942 workspace.bounds.size.width = px(900.);
12943 });
12944
12945 // Step 1: Add a tab to the center pane then open a flexible panel in the left
12946 // dock. With one full-width center pane the default ratio is 0.5, so the panel
12947 // and the center pane each take half the workspace width.
12948 workspace.update_in(cx, |workspace, window, cx| {
12949 let item = cx.new(|cx| {
12950 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
12951 });
12952 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
12953
12954 let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Left, 100, cx));
12955 workspace.add_panel(panel, window, cx);
12956 workspace.toggle_dock(DockPosition::Left, window, cx);
12957
12958 let left_dock = workspace.left_dock().read(cx);
12959 let left_width = workspace
12960 .dock_size(&left_dock, window, cx)
12961 .expect("left dock should have an active panel");
12962
12963 assert_eq!(
12964 left_width,
12965 workspace.bounds.size.width / 2.,
12966 "flexible left panel should split evenly with the center pane"
12967 );
12968 });
12969
12970 // Step 2: Split the center pane vertically (top/bottom). Vertical splits do not
12971 // change horizontal width fractions, so the flexible panel stays at the same
12972 // width as each half of the split.
12973 workspace.update_in(cx, |workspace, window, cx| {
12974 workspace.split_pane(
12975 workspace.active_pane().clone(),
12976 SplitDirection::Down,
12977 window,
12978 cx,
12979 );
12980
12981 let left_dock = workspace.left_dock().read(cx);
12982 let left_width = workspace
12983 .dock_size(&left_dock, window, cx)
12984 .expect("left dock should still have an active panel after vertical split");
12985
12986 assert_eq!(
12987 left_width,
12988 workspace.bounds.size.width / 2.,
12989 "flexible left panel width should match each vertically-split pane"
12990 );
12991 });
12992
12993 // Step 3: Open a fixed-width panel in the right dock. The right dock's default
12994 // size reduces the available width, so the flexible left panel and the center
12995 // panes all shrink proportionally to accommodate it.
12996 workspace.update_in(cx, |workspace, window, cx| {
12997 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 200, cx));
12998 workspace.add_panel(panel, window, cx);
12999 workspace.toggle_dock(DockPosition::Right, window, cx);
13000
13001 let right_dock = workspace.right_dock().read(cx);
13002 let right_width = workspace
13003 .dock_size(&right_dock, window, cx)
13004 .expect("right dock should have an active panel");
13005
13006 let left_dock = workspace.left_dock().read(cx);
13007 let left_width = workspace
13008 .dock_size(&left_dock, window, cx)
13009 .expect("left dock should still have an active panel");
13010
13011 let available_width = workspace.bounds.size.width - right_width;
13012 assert_eq!(
13013 left_width,
13014 available_width / 2.,
13015 "flexible left panel should shrink proportionally as the right dock takes space"
13016 );
13017 });
13018
13019 // Step 4: Toggle the right dock's panel to flexible. Now both docks use
13020 // flex sizing and the workspace width is divided among left-flex, center
13021 // (implicit flex 1.0), and right-flex.
13022 workspace.update_in(cx, |workspace, window, cx| {
13023 let right_dock = workspace.right_dock().clone();
13024 let right_panel = right_dock
13025 .read(cx)
13026 .visible_panel()
13027 .expect("right dock should have a visible panel")
13028 .clone();
13029 workspace.toggle_dock_panel_flexible_size(
13030 &right_dock,
13031 right_panel.as_ref(),
13032 window,
13033 cx,
13034 );
13035
13036 let right_dock = right_dock.read(cx);
13037 let right_panel = right_dock
13038 .visible_panel()
13039 .expect("right dock should still have a visible panel");
13040 assert!(
13041 right_panel.has_flexible_size(window, cx),
13042 "right panel should now be flexible"
13043 );
13044
13045 let right_size_state = right_dock
13046 .stored_panel_size_state(right_panel.as_ref())
13047 .expect("right panel should have a stored size state after toggling");
13048 let right_flex = right_size_state
13049 .flex
13050 .expect("right panel should have a flex value after toggling");
13051
13052 let left_dock = workspace.left_dock().read(cx);
13053 let left_width = workspace
13054 .dock_size(&left_dock, window, cx)
13055 .expect("left dock should still have an active panel");
13056 let right_width = workspace
13057 .dock_size(&right_dock, window, cx)
13058 .expect("right dock should still have an active panel");
13059
13060 let left_flex = workspace
13061 .default_dock_flex(DockPosition::Left)
13062 .expect("left dock should have a default flex");
13063
13064 let total_flex = left_flex + 1.0 + right_flex;
13065 let expected_left = left_flex / total_flex * workspace.bounds.size.width;
13066 let expected_right = right_flex / total_flex * workspace.bounds.size.width;
13067 assert_eq!(
13068 left_width, expected_left,
13069 "flexible left panel should share workspace width via flex ratios"
13070 );
13071 assert_eq!(
13072 right_width, expected_right,
13073 "flexible right panel should share workspace width via flex ratios"
13074 );
13075 });
13076 }
13077
13078 struct TestModal(FocusHandle);
13079
13080 impl TestModal {
13081 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
13082 Self(cx.focus_handle())
13083 }
13084 }
13085
13086 impl EventEmitter<DismissEvent> for TestModal {}
13087
13088 impl Focusable for TestModal {
13089 fn focus_handle(&self, _cx: &App) -> FocusHandle {
13090 self.0.clone()
13091 }
13092 }
13093
13094 impl ModalView for TestModal {}
13095
13096 impl Render for TestModal {
13097 fn render(
13098 &mut self,
13099 _window: &mut Window,
13100 _cx: &mut Context<TestModal>,
13101 ) -> impl IntoElement {
13102 div().track_focus(&self.0)
13103 }
13104 }
13105
13106 #[gpui::test]
13107 async fn test_panels(cx: &mut gpui::TestAppContext) {
13108 init_test(cx);
13109 let fs = FakeFs::new(cx.executor());
13110
13111 let project = Project::test(fs, [], cx).await;
13112 let (multi_workspace, cx) =
13113 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
13114 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
13115
13116 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
13117 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
13118 workspace.add_panel(panel_1.clone(), window, cx);
13119 workspace.toggle_dock(DockPosition::Left, window, cx);
13120 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
13121 workspace.add_panel(panel_2.clone(), window, cx);
13122 workspace.toggle_dock(DockPosition::Right, window, cx);
13123
13124 let left_dock = workspace.left_dock();
13125 assert_eq!(
13126 left_dock.read(cx).visible_panel().unwrap().panel_id(),
13127 panel_1.panel_id()
13128 );
13129 assert_eq!(
13130 workspace.dock_size(&left_dock.read(cx), window, cx),
13131 Some(px(300.))
13132 );
13133
13134 workspace.resize_left_dock(px(1337.), window, cx);
13135 assert_eq!(
13136 workspace
13137 .right_dock()
13138 .read(cx)
13139 .visible_panel()
13140 .unwrap()
13141 .panel_id(),
13142 panel_2.panel_id(),
13143 );
13144
13145 (panel_1, panel_2)
13146 });
13147
13148 // Move panel_1 to the right
13149 panel_1.update_in(cx, |panel_1, window, cx| {
13150 panel_1.set_position(DockPosition::Right, window, cx)
13151 });
13152
13153 workspace.update_in(cx, |workspace, window, cx| {
13154 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
13155 // Since it was the only panel on the left, the left dock should now be closed.
13156 assert!(!workspace.left_dock().read(cx).is_open());
13157 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
13158 let right_dock = workspace.right_dock();
13159 assert_eq!(
13160 right_dock.read(cx).visible_panel().unwrap().panel_id(),
13161 panel_1.panel_id()
13162 );
13163 assert_eq!(
13164 right_dock
13165 .read(cx)
13166 .active_panel_size()
13167 .unwrap()
13168 .size
13169 .unwrap(),
13170 px(1337.)
13171 );
13172
13173 // Now we move panel_2 to the left
13174 panel_2.set_position(DockPosition::Left, window, cx);
13175 });
13176
13177 workspace.update(cx, |workspace, cx| {
13178 // Since panel_2 was not visible on the right, we don't open the left dock.
13179 assert!(!workspace.left_dock().read(cx).is_open());
13180 // And the right dock is unaffected in its displaying of panel_1
13181 assert!(workspace.right_dock().read(cx).is_open());
13182 assert_eq!(
13183 workspace
13184 .right_dock()
13185 .read(cx)
13186 .visible_panel()
13187 .unwrap()
13188 .panel_id(),
13189 panel_1.panel_id(),
13190 );
13191 });
13192
13193 // Move panel_1 back to the left
13194 panel_1.update_in(cx, |panel_1, window, cx| {
13195 panel_1.set_position(DockPosition::Left, window, cx)
13196 });
13197
13198 workspace.update_in(cx, |workspace, window, cx| {
13199 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
13200 let left_dock = workspace.left_dock();
13201 assert!(left_dock.read(cx).is_open());
13202 assert_eq!(
13203 left_dock.read(cx).visible_panel().unwrap().panel_id(),
13204 panel_1.panel_id()
13205 );
13206 assert_eq!(
13207 workspace.dock_size(&left_dock.read(cx), window, cx),
13208 Some(px(1337.))
13209 );
13210 // And the right dock should be closed as it no longer has any panels.
13211 assert!(!workspace.right_dock().read(cx).is_open());
13212
13213 // Now we move panel_1 to the bottom
13214 panel_1.set_position(DockPosition::Bottom, window, cx);
13215 });
13216
13217 workspace.update_in(cx, |workspace, window, cx| {
13218 // Since panel_1 was visible on the left, we close the left dock.
13219 assert!(!workspace.left_dock().read(cx).is_open());
13220 // The bottom dock is sized based on the panel's default size,
13221 // since the panel orientation changed from vertical to horizontal.
13222 let bottom_dock = workspace.bottom_dock();
13223 assert_eq!(
13224 workspace.dock_size(&bottom_dock.read(cx), window, cx),
13225 Some(px(300.))
13226 );
13227 // Close bottom dock and move panel_1 back to the left.
13228 bottom_dock.update(cx, |bottom_dock, cx| {
13229 bottom_dock.set_open(false, window, cx)
13230 });
13231 panel_1.set_position(DockPosition::Left, window, cx);
13232 });
13233
13234 // Emit activated event on panel 1
13235 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
13236
13237 // Now the left dock is open and panel_1 is active and focused.
13238 workspace.update_in(cx, |workspace, window, cx| {
13239 let left_dock = workspace.left_dock();
13240 assert!(left_dock.read(cx).is_open());
13241 assert_eq!(
13242 left_dock.read(cx).visible_panel().unwrap().panel_id(),
13243 panel_1.panel_id(),
13244 );
13245 assert!(panel_1.focus_handle(cx).is_focused(window));
13246 });
13247
13248 // Emit closed event on panel 2, which is not active
13249 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
13250
13251 // Wo don't close the left dock, because panel_2 wasn't the active panel
13252 workspace.update(cx, |workspace, cx| {
13253 let left_dock = workspace.left_dock();
13254 assert!(left_dock.read(cx).is_open());
13255 assert_eq!(
13256 left_dock.read(cx).visible_panel().unwrap().panel_id(),
13257 panel_1.panel_id(),
13258 );
13259 });
13260
13261 // Emitting a ZoomIn event shows the panel as zoomed.
13262 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
13263 workspace.read_with(cx, |workspace, _| {
13264 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13265 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
13266 });
13267
13268 // Move panel to another dock while it is zoomed
13269 panel_1.update_in(cx, |panel, window, cx| {
13270 panel.set_position(DockPosition::Right, window, cx)
13271 });
13272 workspace.read_with(cx, |workspace, _| {
13273 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13274
13275 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
13276 });
13277
13278 // This is a helper for getting a:
13279 // - valid focus on an element,
13280 // - that isn't a part of the panes and panels system of the Workspace,
13281 // - and doesn't trigger the 'on_focus_lost' API.
13282 let focus_other_view = {
13283 let workspace = workspace.clone();
13284 move |cx: &mut VisualTestContext| {
13285 workspace.update_in(cx, |workspace, window, cx| {
13286 if workspace.active_modal::<TestModal>(cx).is_some() {
13287 workspace.toggle_modal(window, cx, TestModal::new);
13288 workspace.toggle_modal(window, cx, TestModal::new);
13289 } else {
13290 workspace.toggle_modal(window, cx, TestModal::new);
13291 }
13292 })
13293 }
13294 };
13295
13296 // If focus is transferred to another view that's not a panel or another pane, we still show
13297 // the panel as zoomed.
13298 focus_other_view(cx);
13299 workspace.read_with(cx, |workspace, _| {
13300 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13301 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
13302 });
13303
13304 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
13305 workspace.update_in(cx, |_workspace, window, cx| {
13306 cx.focus_self(window);
13307 });
13308 workspace.read_with(cx, |workspace, _| {
13309 assert_eq!(workspace.zoomed, None);
13310 assert_eq!(workspace.zoomed_position, None);
13311 });
13312
13313 // If focus is transferred again to another view that's not a panel or a pane, we won't
13314 // show the panel as zoomed because it wasn't zoomed before.
13315 focus_other_view(cx);
13316 workspace.read_with(cx, |workspace, _| {
13317 assert_eq!(workspace.zoomed, None);
13318 assert_eq!(workspace.zoomed_position, None);
13319 });
13320
13321 // When the panel is activated, it is zoomed again.
13322 cx.dispatch_action(ToggleRightDock);
13323 workspace.read_with(cx, |workspace, _| {
13324 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13325 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
13326 });
13327
13328 // Emitting a ZoomOut event unzooms the panel.
13329 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
13330 workspace.read_with(cx, |workspace, _| {
13331 assert_eq!(workspace.zoomed, None);
13332 assert_eq!(workspace.zoomed_position, None);
13333 });
13334
13335 // Emit closed event on panel 1, which is active
13336 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
13337
13338 // Now the left dock is closed, because panel_1 was the active panel
13339 workspace.update(cx, |workspace, cx| {
13340 let right_dock = workspace.right_dock();
13341 assert!(!right_dock.read(cx).is_open());
13342 });
13343 }
13344
13345 #[gpui::test]
13346 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
13347 init_test(cx);
13348
13349 let fs = FakeFs::new(cx.background_executor.clone());
13350 let project = Project::test(fs, [], cx).await;
13351 let (workspace, cx) =
13352 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13353 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13354
13355 let dirty_regular_buffer = cx.new(|cx| {
13356 TestItem::new(cx)
13357 .with_dirty(true)
13358 .with_label("1.txt")
13359 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
13360 });
13361 let dirty_regular_buffer_2 = cx.new(|cx| {
13362 TestItem::new(cx)
13363 .with_dirty(true)
13364 .with_label("2.txt")
13365 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
13366 });
13367 let dirty_multi_buffer_with_both = cx.new(|cx| {
13368 TestItem::new(cx)
13369 .with_dirty(true)
13370 .with_buffer_kind(ItemBufferKind::Multibuffer)
13371 .with_label("Fake Project Search")
13372 .with_project_items(&[
13373 dirty_regular_buffer.read(cx).project_items[0].clone(),
13374 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
13375 ])
13376 });
13377 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
13378 workspace.update_in(cx, |workspace, window, cx| {
13379 workspace.add_item(
13380 pane.clone(),
13381 Box::new(dirty_regular_buffer.clone()),
13382 None,
13383 false,
13384 false,
13385 window,
13386 cx,
13387 );
13388 workspace.add_item(
13389 pane.clone(),
13390 Box::new(dirty_regular_buffer_2.clone()),
13391 None,
13392 false,
13393 false,
13394 window,
13395 cx,
13396 );
13397 workspace.add_item(
13398 pane.clone(),
13399 Box::new(dirty_multi_buffer_with_both.clone()),
13400 None,
13401 false,
13402 false,
13403 window,
13404 cx,
13405 );
13406 });
13407
13408 pane.update_in(cx, |pane, window, cx| {
13409 pane.activate_item(2, true, true, window, cx);
13410 assert_eq!(
13411 pane.active_item().unwrap().item_id(),
13412 multi_buffer_with_both_files_id,
13413 "Should select the multi buffer in the pane"
13414 );
13415 });
13416 let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13417 pane.close_other_items(
13418 &CloseOtherItems {
13419 save_intent: Some(SaveIntent::Save),
13420 close_pinned: true,
13421 },
13422 None,
13423 window,
13424 cx,
13425 )
13426 });
13427 cx.background_executor.run_until_parked();
13428 assert!(!cx.has_pending_prompt());
13429 close_all_but_multi_buffer_task
13430 .await
13431 .expect("Closing all buffers but the multi buffer failed");
13432 pane.update(cx, |pane, cx| {
13433 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
13434 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
13435 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
13436 assert_eq!(pane.items_len(), 1);
13437 assert_eq!(
13438 pane.active_item().unwrap().item_id(),
13439 multi_buffer_with_both_files_id,
13440 "Should have only the multi buffer left in the pane"
13441 );
13442 assert!(
13443 dirty_multi_buffer_with_both.read(cx).is_dirty,
13444 "The multi buffer containing the unsaved buffer should still be dirty"
13445 );
13446 });
13447
13448 dirty_regular_buffer.update(cx, |buffer, cx| {
13449 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
13450 });
13451
13452 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13453 pane.close_active_item(
13454 &CloseActiveItem {
13455 save_intent: Some(SaveIntent::Close),
13456 close_pinned: false,
13457 },
13458 window,
13459 cx,
13460 )
13461 });
13462 cx.background_executor.run_until_parked();
13463 assert!(
13464 cx.has_pending_prompt(),
13465 "Dirty multi buffer should prompt a save dialog"
13466 );
13467 cx.simulate_prompt_answer("Save");
13468 cx.background_executor.run_until_parked();
13469 close_multi_buffer_task
13470 .await
13471 .expect("Closing the multi buffer failed");
13472 pane.update(cx, |pane, cx| {
13473 assert_eq!(
13474 dirty_multi_buffer_with_both.read(cx).save_count,
13475 1,
13476 "Multi buffer item should get be saved"
13477 );
13478 // Test impl does not save inner items, so we do not assert them
13479 assert_eq!(
13480 pane.items_len(),
13481 0,
13482 "No more items should be left in the pane"
13483 );
13484 assert!(pane.active_item().is_none());
13485 });
13486 }
13487
13488 #[gpui::test]
13489 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
13490 cx: &mut TestAppContext,
13491 ) {
13492 init_test(cx);
13493
13494 let fs = FakeFs::new(cx.background_executor.clone());
13495 let project = Project::test(fs, [], cx).await;
13496 let (workspace, cx) =
13497 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13498 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13499
13500 let dirty_regular_buffer = cx.new(|cx| {
13501 TestItem::new(cx)
13502 .with_dirty(true)
13503 .with_label("1.txt")
13504 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
13505 });
13506 let dirty_regular_buffer_2 = cx.new(|cx| {
13507 TestItem::new(cx)
13508 .with_dirty(true)
13509 .with_label("2.txt")
13510 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
13511 });
13512 let clear_regular_buffer = cx.new(|cx| {
13513 TestItem::new(cx)
13514 .with_label("3.txt")
13515 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
13516 });
13517
13518 let dirty_multi_buffer_with_both = cx.new(|cx| {
13519 TestItem::new(cx)
13520 .with_dirty(true)
13521 .with_buffer_kind(ItemBufferKind::Multibuffer)
13522 .with_label("Fake Project Search")
13523 .with_project_items(&[
13524 dirty_regular_buffer.read(cx).project_items[0].clone(),
13525 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
13526 clear_regular_buffer.read(cx).project_items[0].clone(),
13527 ])
13528 });
13529 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
13530 workspace.update_in(cx, |workspace, window, cx| {
13531 workspace.add_item(
13532 pane.clone(),
13533 Box::new(dirty_regular_buffer.clone()),
13534 None,
13535 false,
13536 false,
13537 window,
13538 cx,
13539 );
13540 workspace.add_item(
13541 pane.clone(),
13542 Box::new(dirty_multi_buffer_with_both.clone()),
13543 None,
13544 false,
13545 false,
13546 window,
13547 cx,
13548 );
13549 });
13550
13551 pane.update_in(cx, |pane, window, cx| {
13552 pane.activate_item(1, true, true, window, cx);
13553 assert_eq!(
13554 pane.active_item().unwrap().item_id(),
13555 multi_buffer_with_both_files_id,
13556 "Should select the multi buffer in the pane"
13557 );
13558 });
13559 let _close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13560 pane.close_active_item(
13561 &CloseActiveItem {
13562 save_intent: None,
13563 close_pinned: false,
13564 },
13565 window,
13566 cx,
13567 )
13568 });
13569 cx.background_executor.run_until_parked();
13570 assert!(
13571 cx.has_pending_prompt(),
13572 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
13573 );
13574 }
13575
13576 /// Tests that when `close_on_file_delete` is enabled, files are automatically
13577 /// closed when they are deleted from disk.
13578 #[gpui::test]
13579 async fn test_close_on_disk_deletion_enabled(cx: &mut TestAppContext) {
13580 init_test(cx);
13581
13582 // Enable the close_on_disk_deletion setting
13583 cx.update_global(|store: &mut SettingsStore, cx| {
13584 store.update_user_settings(cx, |settings| {
13585 settings.workspace.close_on_file_delete = Some(true);
13586 });
13587 });
13588
13589 let fs = FakeFs::new(cx.background_executor.clone());
13590 let project = Project::test(fs, [], cx).await;
13591 let (workspace, cx) =
13592 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13593 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13594
13595 // Create a test item that simulates a file
13596 let item = cx.new(|cx| {
13597 TestItem::new(cx)
13598 .with_label("test.txt")
13599 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13600 });
13601
13602 // Add item to workspace
13603 workspace.update_in(cx, |workspace, window, cx| {
13604 workspace.add_item(
13605 pane.clone(),
13606 Box::new(item.clone()),
13607 None,
13608 false,
13609 false,
13610 window,
13611 cx,
13612 );
13613 });
13614
13615 // Verify the item is in the pane
13616 pane.read_with(cx, |pane, _| {
13617 assert_eq!(pane.items().count(), 1);
13618 });
13619
13620 // Simulate file deletion by setting the item's deleted state
13621 item.update(cx, |item, _| {
13622 item.set_has_deleted_file(true);
13623 });
13624
13625 // Emit UpdateTab event to trigger the close behavior
13626 cx.run_until_parked();
13627 item.update(cx, |_, cx| {
13628 cx.emit(ItemEvent::UpdateTab);
13629 });
13630
13631 // Allow the close operation to complete
13632 cx.run_until_parked();
13633
13634 // Verify the item was automatically closed
13635 pane.read_with(cx, |pane, _| {
13636 assert_eq!(
13637 pane.items().count(),
13638 0,
13639 "Item should be automatically closed when file is deleted"
13640 );
13641 });
13642 }
13643
13644 /// Tests that when `close_on_file_delete` is disabled (default), files remain
13645 /// open with a strikethrough when they are deleted from disk.
13646 #[gpui::test]
13647 async fn test_close_on_disk_deletion_disabled(cx: &mut TestAppContext) {
13648 init_test(cx);
13649
13650 // Ensure close_on_disk_deletion is disabled (default)
13651 cx.update_global(|store: &mut SettingsStore, cx| {
13652 store.update_user_settings(cx, |settings| {
13653 settings.workspace.close_on_file_delete = Some(false);
13654 });
13655 });
13656
13657 let fs = FakeFs::new(cx.background_executor.clone());
13658 let project = Project::test(fs, [], cx).await;
13659 let (workspace, cx) =
13660 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13661 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13662
13663 // Create a test item that simulates a file
13664 let item = cx.new(|cx| {
13665 TestItem::new(cx)
13666 .with_label("test.txt")
13667 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13668 });
13669
13670 // Add item to workspace
13671 workspace.update_in(cx, |workspace, window, cx| {
13672 workspace.add_item(
13673 pane.clone(),
13674 Box::new(item.clone()),
13675 None,
13676 false,
13677 false,
13678 window,
13679 cx,
13680 );
13681 });
13682
13683 // Verify the item is in the pane
13684 pane.read_with(cx, |pane, _| {
13685 assert_eq!(pane.items().count(), 1);
13686 });
13687
13688 // Simulate file deletion
13689 item.update(cx, |item, _| {
13690 item.set_has_deleted_file(true);
13691 });
13692
13693 // Emit UpdateTab event
13694 cx.run_until_parked();
13695 item.update(cx, |_, cx| {
13696 cx.emit(ItemEvent::UpdateTab);
13697 });
13698
13699 // Allow any potential close operation to complete
13700 cx.run_until_parked();
13701
13702 // Verify the item remains open (with strikethrough)
13703 pane.read_with(cx, |pane, _| {
13704 assert_eq!(
13705 pane.items().count(),
13706 1,
13707 "Item should remain open when close_on_disk_deletion is disabled"
13708 );
13709 });
13710
13711 // Verify the item shows as deleted
13712 item.read_with(cx, |item, _| {
13713 assert!(
13714 item.has_deleted_file,
13715 "Item should be marked as having deleted file"
13716 );
13717 });
13718 }
13719
13720 /// Tests that dirty files are not automatically closed when deleted from disk,
13721 /// even when `close_on_file_delete` is enabled. This ensures users don't lose
13722 /// unsaved changes without being prompted.
13723 #[gpui::test]
13724 async fn test_close_on_disk_deletion_with_dirty_file(cx: &mut TestAppContext) {
13725 init_test(cx);
13726
13727 // Enable the close_on_file_delete setting
13728 cx.update_global(|store: &mut SettingsStore, cx| {
13729 store.update_user_settings(cx, |settings| {
13730 settings.workspace.close_on_file_delete = Some(true);
13731 });
13732 });
13733
13734 let fs = FakeFs::new(cx.background_executor.clone());
13735 let project = Project::test(fs, [], cx).await;
13736 let (workspace, cx) =
13737 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13738 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13739
13740 // Create a dirty test item
13741 let item = cx.new(|cx| {
13742 TestItem::new(cx)
13743 .with_dirty(true)
13744 .with_label("test.txt")
13745 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13746 });
13747
13748 // Add item to workspace
13749 workspace.update_in(cx, |workspace, window, cx| {
13750 workspace.add_item(
13751 pane.clone(),
13752 Box::new(item.clone()),
13753 None,
13754 false,
13755 false,
13756 window,
13757 cx,
13758 );
13759 });
13760
13761 // Simulate file deletion
13762 item.update(cx, |item, _| {
13763 item.set_has_deleted_file(true);
13764 });
13765
13766 // Emit UpdateTab event to trigger the close behavior
13767 cx.run_until_parked();
13768 item.update(cx, |_, cx| {
13769 cx.emit(ItemEvent::UpdateTab);
13770 });
13771
13772 // Allow any potential close operation to complete
13773 cx.run_until_parked();
13774
13775 // Verify the item remains open (dirty files are not auto-closed)
13776 pane.read_with(cx, |pane, _| {
13777 assert_eq!(
13778 pane.items().count(),
13779 1,
13780 "Dirty items should not be automatically closed even when file is deleted"
13781 );
13782 });
13783
13784 // Verify the item is marked as deleted and still dirty
13785 item.read_with(cx, |item, _| {
13786 assert!(
13787 item.has_deleted_file,
13788 "Item should be marked as having deleted file"
13789 );
13790 assert!(item.is_dirty, "Item should still be dirty");
13791 });
13792 }
13793
13794 /// Tests that navigation history is cleaned up when files are auto-closed
13795 /// due to deletion from disk.
13796 #[gpui::test]
13797 async fn test_close_on_disk_deletion_cleans_navigation_history(cx: &mut TestAppContext) {
13798 init_test(cx);
13799
13800 // Enable the close_on_file_delete setting
13801 cx.update_global(|store: &mut SettingsStore, cx| {
13802 store.update_user_settings(cx, |settings| {
13803 settings.workspace.close_on_file_delete = Some(true);
13804 });
13805 });
13806
13807 let fs = FakeFs::new(cx.background_executor.clone());
13808 let project = Project::test(fs, [], cx).await;
13809 let (workspace, cx) =
13810 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13811 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13812
13813 // Create test items
13814 let item1 = cx.new(|cx| {
13815 TestItem::new(cx)
13816 .with_label("test1.txt")
13817 .with_project_items(&[TestProjectItem::new(1, "test1.txt", cx)])
13818 });
13819 let item1_id = item1.item_id();
13820
13821 let item2 = cx.new(|cx| {
13822 TestItem::new(cx)
13823 .with_label("test2.txt")
13824 .with_project_items(&[TestProjectItem::new(2, "test2.txt", cx)])
13825 });
13826
13827 // Add items to workspace
13828 workspace.update_in(cx, |workspace, window, cx| {
13829 workspace.add_item(
13830 pane.clone(),
13831 Box::new(item1.clone()),
13832 None,
13833 false,
13834 false,
13835 window,
13836 cx,
13837 );
13838 workspace.add_item(
13839 pane.clone(),
13840 Box::new(item2.clone()),
13841 None,
13842 false,
13843 false,
13844 window,
13845 cx,
13846 );
13847 });
13848
13849 // Activate item1 to ensure it gets navigation entries
13850 pane.update_in(cx, |pane, window, cx| {
13851 pane.activate_item(0, true, true, window, cx);
13852 });
13853
13854 // Switch to item2 and back to create navigation history
13855 pane.update_in(cx, |pane, window, cx| {
13856 pane.activate_item(1, true, true, window, cx);
13857 });
13858 cx.run_until_parked();
13859
13860 pane.update_in(cx, |pane, window, cx| {
13861 pane.activate_item(0, true, true, window, cx);
13862 });
13863 cx.run_until_parked();
13864
13865 // Simulate file deletion for item1
13866 item1.update(cx, |item, _| {
13867 item.set_has_deleted_file(true);
13868 });
13869
13870 // Emit UpdateTab event to trigger the close behavior
13871 item1.update(cx, |_, cx| {
13872 cx.emit(ItemEvent::UpdateTab);
13873 });
13874 cx.run_until_parked();
13875
13876 // Verify item1 was closed
13877 pane.read_with(cx, |pane, _| {
13878 assert_eq!(
13879 pane.items().count(),
13880 1,
13881 "Should have 1 item remaining after auto-close"
13882 );
13883 });
13884
13885 // Check navigation history after close
13886 let has_item = pane.read_with(cx, |pane, cx| {
13887 let mut has_item = false;
13888 pane.nav_history().for_each_entry(cx, &mut |entry, _| {
13889 if entry.item.id() == item1_id {
13890 has_item = true;
13891 }
13892 });
13893 has_item
13894 });
13895
13896 assert!(
13897 !has_item,
13898 "Navigation history should not contain closed item entries"
13899 );
13900 }
13901
13902 #[gpui::test]
13903 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
13904 cx: &mut TestAppContext,
13905 ) {
13906 init_test(cx);
13907
13908 let fs = FakeFs::new(cx.background_executor.clone());
13909 let project = Project::test(fs, [], cx).await;
13910 let (workspace, cx) =
13911 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13912 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13913
13914 let dirty_regular_buffer = cx.new(|cx| {
13915 TestItem::new(cx)
13916 .with_dirty(true)
13917 .with_label("1.txt")
13918 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
13919 });
13920 let dirty_regular_buffer_2 = cx.new(|cx| {
13921 TestItem::new(cx)
13922 .with_dirty(true)
13923 .with_label("2.txt")
13924 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
13925 });
13926 let clear_regular_buffer = cx.new(|cx| {
13927 TestItem::new(cx)
13928 .with_label("3.txt")
13929 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
13930 });
13931
13932 let dirty_multi_buffer = cx.new(|cx| {
13933 TestItem::new(cx)
13934 .with_dirty(true)
13935 .with_buffer_kind(ItemBufferKind::Multibuffer)
13936 .with_label("Fake Project Search")
13937 .with_project_items(&[
13938 dirty_regular_buffer.read(cx).project_items[0].clone(),
13939 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
13940 clear_regular_buffer.read(cx).project_items[0].clone(),
13941 ])
13942 });
13943 workspace.update_in(cx, |workspace, window, cx| {
13944 workspace.add_item(
13945 pane.clone(),
13946 Box::new(dirty_regular_buffer.clone()),
13947 None,
13948 false,
13949 false,
13950 window,
13951 cx,
13952 );
13953 workspace.add_item(
13954 pane.clone(),
13955 Box::new(dirty_regular_buffer_2.clone()),
13956 None,
13957 false,
13958 false,
13959 window,
13960 cx,
13961 );
13962 workspace.add_item(
13963 pane.clone(),
13964 Box::new(dirty_multi_buffer.clone()),
13965 None,
13966 false,
13967 false,
13968 window,
13969 cx,
13970 );
13971 });
13972
13973 pane.update_in(cx, |pane, window, cx| {
13974 pane.activate_item(2, true, true, window, cx);
13975 assert_eq!(
13976 pane.active_item().unwrap().item_id(),
13977 dirty_multi_buffer.item_id(),
13978 "Should select the multi buffer in the pane"
13979 );
13980 });
13981 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13982 pane.close_active_item(
13983 &CloseActiveItem {
13984 save_intent: None,
13985 close_pinned: false,
13986 },
13987 window,
13988 cx,
13989 )
13990 });
13991 cx.background_executor.run_until_parked();
13992 assert!(
13993 !cx.has_pending_prompt(),
13994 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
13995 );
13996 close_multi_buffer_task
13997 .await
13998 .expect("Closing multi buffer failed");
13999 pane.update(cx, |pane, cx| {
14000 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
14001 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
14002 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
14003 assert_eq!(
14004 pane.items()
14005 .map(|item| item.item_id())
14006 .sorted()
14007 .collect::<Vec<_>>(),
14008 vec![
14009 dirty_regular_buffer.item_id(),
14010 dirty_regular_buffer_2.item_id(),
14011 ],
14012 "Should have no multi buffer left in the pane"
14013 );
14014 assert!(dirty_regular_buffer.read(cx).is_dirty);
14015 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
14016 });
14017 }
14018
14019 #[gpui::test]
14020 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
14021 init_test(cx);
14022 let fs = FakeFs::new(cx.executor());
14023 let project = Project::test(fs, [], cx).await;
14024 let (multi_workspace, cx) =
14025 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
14026 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
14027
14028 // Add a new panel to the right dock, opening the dock and setting the
14029 // focus to the new panel.
14030 let panel = workspace.update_in(cx, |workspace, window, cx| {
14031 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14032 workspace.add_panel(panel.clone(), window, cx);
14033
14034 workspace
14035 .right_dock()
14036 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
14037
14038 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14039
14040 panel
14041 });
14042
14043 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
14044 // panel to the next valid position which, in this case, is the left
14045 // dock.
14046 cx.dispatch_action(MoveFocusedPanelToNextPosition);
14047 workspace.update(cx, |workspace, cx| {
14048 assert!(workspace.left_dock().read(cx).is_open());
14049 assert_eq!(panel.read(cx).position, DockPosition::Left);
14050 });
14051
14052 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
14053 // panel to the next valid position which, in this case, is the bottom
14054 // dock.
14055 cx.dispatch_action(MoveFocusedPanelToNextPosition);
14056 workspace.update(cx, |workspace, cx| {
14057 assert!(workspace.bottom_dock().read(cx).is_open());
14058 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
14059 });
14060
14061 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
14062 // around moving the panel to its initial position, the right dock.
14063 cx.dispatch_action(MoveFocusedPanelToNextPosition);
14064 workspace.update(cx, |workspace, cx| {
14065 assert!(workspace.right_dock().read(cx).is_open());
14066 assert_eq!(panel.read(cx).position, DockPosition::Right);
14067 });
14068
14069 // Remove focus from the panel, ensuring that, if the panel is not
14070 // focused, the `MoveFocusedPanelToNextPosition` action does not update
14071 // the panel's position, so the panel is still in the right dock.
14072 workspace.update_in(cx, |workspace, window, cx| {
14073 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14074 });
14075
14076 cx.dispatch_action(MoveFocusedPanelToNextPosition);
14077 workspace.update(cx, |workspace, cx| {
14078 assert!(workspace.right_dock().read(cx).is_open());
14079 assert_eq!(panel.read(cx).position, DockPosition::Right);
14080 });
14081 }
14082
14083 #[gpui::test]
14084 async fn test_moving_items_create_panes(cx: &mut TestAppContext) {
14085 init_test(cx);
14086
14087 let fs = FakeFs::new(cx.executor());
14088 let project = Project::test(fs, [], cx).await;
14089 let (workspace, cx) =
14090 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14091
14092 let item_1 = cx.new(|cx| {
14093 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
14094 });
14095 workspace.update_in(cx, |workspace, window, cx| {
14096 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
14097 workspace.move_item_to_pane_in_direction(
14098 &MoveItemToPaneInDirection {
14099 direction: SplitDirection::Right,
14100 focus: true,
14101 clone: false,
14102 },
14103 window,
14104 cx,
14105 );
14106 workspace.move_item_to_pane_at_index(
14107 &MoveItemToPane {
14108 destination: 3,
14109 focus: true,
14110 clone: false,
14111 },
14112 window,
14113 cx,
14114 );
14115
14116 assert_eq!(workspace.panes.len(), 1, "No new panes were created");
14117 assert_eq!(
14118 pane_items_paths(&workspace.active_pane, cx),
14119 vec!["first.txt".to_string()],
14120 "Single item was not moved anywhere"
14121 );
14122 });
14123
14124 let item_2 = cx.new(|cx| {
14125 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "second.txt", cx)])
14126 });
14127 workspace.update_in(cx, |workspace, window, cx| {
14128 workspace.add_item_to_active_pane(Box::new(item_2), None, true, window, cx);
14129 assert_eq!(
14130 pane_items_paths(&workspace.panes[0], cx),
14131 vec!["first.txt".to_string(), "second.txt".to_string()],
14132 );
14133 workspace.move_item_to_pane_in_direction(
14134 &MoveItemToPaneInDirection {
14135 direction: SplitDirection::Right,
14136 focus: true,
14137 clone: false,
14138 },
14139 window,
14140 cx,
14141 );
14142
14143 assert_eq!(workspace.panes.len(), 2, "A new pane should be created");
14144 assert_eq!(
14145 pane_items_paths(&workspace.panes[0], cx),
14146 vec!["first.txt".to_string()],
14147 "After moving, one item should be left in the original pane"
14148 );
14149 assert_eq!(
14150 pane_items_paths(&workspace.panes[1], cx),
14151 vec!["second.txt".to_string()],
14152 "New item should have been moved to the new pane"
14153 );
14154 });
14155
14156 let item_3 = cx.new(|cx| {
14157 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "third.txt", cx)])
14158 });
14159 workspace.update_in(cx, |workspace, window, cx| {
14160 let original_pane = workspace.panes[0].clone();
14161 workspace.set_active_pane(&original_pane, window, cx);
14162 workspace.add_item_to_active_pane(Box::new(item_3), None, true, window, cx);
14163 assert_eq!(workspace.panes.len(), 2, "No new panes were created");
14164 assert_eq!(
14165 pane_items_paths(&workspace.active_pane, cx),
14166 vec!["first.txt".to_string(), "third.txt".to_string()],
14167 "New pane should be ready to move one item out"
14168 );
14169
14170 workspace.move_item_to_pane_at_index(
14171 &MoveItemToPane {
14172 destination: 3,
14173 focus: true,
14174 clone: false,
14175 },
14176 window,
14177 cx,
14178 );
14179 assert_eq!(workspace.panes.len(), 3, "A new pane should be created");
14180 assert_eq!(
14181 pane_items_paths(&workspace.active_pane, cx),
14182 vec!["first.txt".to_string()],
14183 "After moving, one item should be left in the original pane"
14184 );
14185 assert_eq!(
14186 pane_items_paths(&workspace.panes[1], cx),
14187 vec!["second.txt".to_string()],
14188 "Previously created pane should be unchanged"
14189 );
14190 assert_eq!(
14191 pane_items_paths(&workspace.panes[2], cx),
14192 vec!["third.txt".to_string()],
14193 "New item should have been moved to the new pane"
14194 );
14195 });
14196 }
14197
14198 #[gpui::test]
14199 async fn test_moving_items_can_clone_panes(cx: &mut TestAppContext) {
14200 init_test(cx);
14201
14202 let fs = FakeFs::new(cx.executor());
14203 let project = Project::test(fs, [], cx).await;
14204 let (workspace, cx) =
14205 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14206
14207 let item_1 = cx.new(|cx| {
14208 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
14209 });
14210 workspace.update_in(cx, |workspace, window, cx| {
14211 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
14212 workspace.move_item_to_pane_in_direction(
14213 &MoveItemToPaneInDirection {
14214 direction: SplitDirection::Right,
14215 focus: true,
14216 clone: true,
14217 },
14218 window,
14219 cx,
14220 );
14221 });
14222 cx.run_until_parked();
14223 workspace.update_in(cx, |workspace, window, cx| {
14224 workspace.move_item_to_pane_at_index(
14225 &MoveItemToPane {
14226 destination: 3,
14227 focus: true,
14228 clone: true,
14229 },
14230 window,
14231 cx,
14232 );
14233 });
14234 cx.run_until_parked();
14235
14236 workspace.update(cx, |workspace, cx| {
14237 assert_eq!(workspace.panes.len(), 3, "Two new panes were created");
14238 for pane in workspace.panes() {
14239 assert_eq!(
14240 pane_items_paths(pane, cx),
14241 vec!["first.txt".to_string()],
14242 "Single item exists in all panes"
14243 );
14244 }
14245 });
14246
14247 // verify that the active pane has been updated after waiting for the
14248 // pane focus event to fire and resolve
14249 workspace.read_with(cx, |workspace, _app| {
14250 assert_eq!(
14251 workspace.active_pane(),
14252 &workspace.panes[2],
14253 "The third pane should be the active one: {:?}",
14254 workspace.panes
14255 );
14256 })
14257 }
14258
14259 #[gpui::test]
14260 async fn test_close_item_in_all_panes(cx: &mut TestAppContext) {
14261 init_test(cx);
14262
14263 let fs = FakeFs::new(cx.executor());
14264 fs.insert_tree("/root", json!({ "test.txt": "" })).await;
14265
14266 let project = Project::test(fs, ["root".as_ref()], cx).await;
14267 let (workspace, cx) =
14268 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14269
14270 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14271 // Add item to pane A with project path
14272 let item_a = cx.new(|cx| {
14273 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
14274 });
14275 workspace.update_in(cx, |workspace, window, cx| {
14276 workspace.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
14277 });
14278
14279 // Split to create pane B
14280 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
14281 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
14282 });
14283
14284 // Add item with SAME project path to pane B, and pin it
14285 let item_b = cx.new(|cx| {
14286 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
14287 });
14288 pane_b.update_in(cx, |pane, window, cx| {
14289 pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
14290 pane.set_pinned_count(1);
14291 });
14292
14293 assert_eq!(pane_a.read_with(cx, |pane, _| pane.items_len()), 1);
14294 assert_eq!(pane_b.read_with(cx, |pane, _| pane.items_len()), 1);
14295
14296 // close_pinned: false should only close the unpinned copy
14297 workspace.update_in(cx, |workspace, window, cx| {
14298 workspace.close_item_in_all_panes(
14299 &CloseItemInAllPanes {
14300 save_intent: Some(SaveIntent::Close),
14301 close_pinned: false,
14302 },
14303 window,
14304 cx,
14305 )
14306 });
14307 cx.executor().run_until_parked();
14308
14309 let item_count_a = pane_a.read_with(cx, |pane, _| pane.items_len());
14310 let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
14311 assert_eq!(item_count_a, 0, "Unpinned item in pane A should be closed");
14312 assert_eq!(item_count_b, 1, "Pinned item in pane B should remain");
14313
14314 // Split again, seeing as closing the previous item also closed its
14315 // pane, so only pane remains, which does not allow us to properly test
14316 // that both items close when `close_pinned: true`.
14317 let pane_c = workspace.update_in(cx, |workspace, window, cx| {
14318 workspace.split_pane(pane_b.clone(), SplitDirection::Right, window, cx)
14319 });
14320
14321 // Add an item with the same project path to pane C so that
14322 // close_item_in_all_panes can determine what to close across all panes
14323 // (it reads the active item from the active pane, and split_pane
14324 // creates an empty pane).
14325 let item_c = cx.new(|cx| {
14326 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
14327 });
14328 pane_c.update_in(cx, |pane, window, cx| {
14329 pane.add_item(Box::new(item_c.clone()), true, true, None, window, cx);
14330 });
14331
14332 // close_pinned: true should close the pinned copy too
14333 workspace.update_in(cx, |workspace, window, cx| {
14334 let panes_count = workspace.panes().len();
14335 assert_eq!(panes_count, 2, "Workspace should have two panes (B and C)");
14336
14337 workspace.close_item_in_all_panes(
14338 &CloseItemInAllPanes {
14339 save_intent: Some(SaveIntent::Close),
14340 close_pinned: true,
14341 },
14342 window,
14343 cx,
14344 )
14345 });
14346 cx.executor().run_until_parked();
14347
14348 let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
14349 let item_count_c = pane_c.read_with(cx, |pane, _| pane.items_len());
14350 assert_eq!(item_count_b, 0, "Pinned item in pane B should be closed");
14351 assert_eq!(item_count_c, 0, "Unpinned item in pane C should be closed");
14352 }
14353
14354 mod register_project_item_tests {
14355
14356 use super::*;
14357
14358 // View
14359 struct TestPngItemView {
14360 focus_handle: FocusHandle,
14361 }
14362 // Model
14363 struct TestPngItem {}
14364
14365 impl project::ProjectItem for TestPngItem {
14366 fn try_open(
14367 _project: &Entity<Project>,
14368 path: &ProjectPath,
14369 cx: &mut App,
14370 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
14371 if path.path.extension().unwrap() == "png" {
14372 Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestPngItem {}))))
14373 } else {
14374 None
14375 }
14376 }
14377
14378 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
14379 None
14380 }
14381
14382 fn project_path(&self, _: &App) -> Option<ProjectPath> {
14383 None
14384 }
14385
14386 fn is_dirty(&self) -> bool {
14387 false
14388 }
14389 }
14390
14391 impl Item for TestPngItemView {
14392 type Event = ();
14393 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14394 "".into()
14395 }
14396 }
14397 impl EventEmitter<()> for TestPngItemView {}
14398 impl Focusable for TestPngItemView {
14399 fn focus_handle(&self, _cx: &App) -> FocusHandle {
14400 self.focus_handle.clone()
14401 }
14402 }
14403
14404 impl Render for TestPngItemView {
14405 fn render(
14406 &mut self,
14407 _window: &mut Window,
14408 _cx: &mut Context<Self>,
14409 ) -> impl IntoElement {
14410 Empty
14411 }
14412 }
14413
14414 impl ProjectItem for TestPngItemView {
14415 type Item = TestPngItem;
14416
14417 fn for_project_item(
14418 _project: Entity<Project>,
14419 _pane: Option<&Pane>,
14420 _item: Entity<Self::Item>,
14421 _: &mut Window,
14422 cx: &mut Context<Self>,
14423 ) -> Self
14424 where
14425 Self: Sized,
14426 {
14427 Self {
14428 focus_handle: cx.focus_handle(),
14429 }
14430 }
14431 }
14432
14433 // View
14434 struct TestIpynbItemView {
14435 focus_handle: FocusHandle,
14436 }
14437 // Model
14438 struct TestIpynbItem {}
14439
14440 impl project::ProjectItem for TestIpynbItem {
14441 fn try_open(
14442 _project: &Entity<Project>,
14443 path: &ProjectPath,
14444 cx: &mut App,
14445 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
14446 if path.path.extension().unwrap() == "ipynb" {
14447 Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestIpynbItem {}))))
14448 } else {
14449 None
14450 }
14451 }
14452
14453 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
14454 None
14455 }
14456
14457 fn project_path(&self, _: &App) -> Option<ProjectPath> {
14458 None
14459 }
14460
14461 fn is_dirty(&self) -> bool {
14462 false
14463 }
14464 }
14465
14466 impl Item for TestIpynbItemView {
14467 type Event = ();
14468 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14469 "".into()
14470 }
14471 }
14472 impl EventEmitter<()> for TestIpynbItemView {}
14473 impl Focusable for TestIpynbItemView {
14474 fn focus_handle(&self, _cx: &App) -> FocusHandle {
14475 self.focus_handle.clone()
14476 }
14477 }
14478
14479 impl Render for TestIpynbItemView {
14480 fn render(
14481 &mut self,
14482 _window: &mut Window,
14483 _cx: &mut Context<Self>,
14484 ) -> impl IntoElement {
14485 Empty
14486 }
14487 }
14488
14489 impl ProjectItem for TestIpynbItemView {
14490 type Item = TestIpynbItem;
14491
14492 fn for_project_item(
14493 _project: Entity<Project>,
14494 _pane: Option<&Pane>,
14495 _item: Entity<Self::Item>,
14496 _: &mut Window,
14497 cx: &mut Context<Self>,
14498 ) -> Self
14499 where
14500 Self: Sized,
14501 {
14502 Self {
14503 focus_handle: cx.focus_handle(),
14504 }
14505 }
14506 }
14507
14508 struct TestAlternatePngItemView {
14509 focus_handle: FocusHandle,
14510 }
14511
14512 impl Item for TestAlternatePngItemView {
14513 type Event = ();
14514 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14515 "".into()
14516 }
14517 }
14518
14519 impl EventEmitter<()> for TestAlternatePngItemView {}
14520 impl Focusable for TestAlternatePngItemView {
14521 fn focus_handle(&self, _cx: &App) -> FocusHandle {
14522 self.focus_handle.clone()
14523 }
14524 }
14525
14526 impl Render for TestAlternatePngItemView {
14527 fn render(
14528 &mut self,
14529 _window: &mut Window,
14530 _cx: &mut Context<Self>,
14531 ) -> impl IntoElement {
14532 Empty
14533 }
14534 }
14535
14536 impl ProjectItem for TestAlternatePngItemView {
14537 type Item = TestPngItem;
14538
14539 fn for_project_item(
14540 _project: Entity<Project>,
14541 _pane: Option<&Pane>,
14542 _item: Entity<Self::Item>,
14543 _: &mut Window,
14544 cx: &mut Context<Self>,
14545 ) -> Self
14546 where
14547 Self: Sized,
14548 {
14549 Self {
14550 focus_handle: cx.focus_handle(),
14551 }
14552 }
14553 }
14554
14555 #[gpui::test]
14556 async fn test_register_project_item(cx: &mut TestAppContext) {
14557 init_test(cx);
14558
14559 cx.update(|cx| {
14560 register_project_item::<TestPngItemView>(cx);
14561 register_project_item::<TestIpynbItemView>(cx);
14562 });
14563
14564 let fs = FakeFs::new(cx.executor());
14565 fs.insert_tree(
14566 "/root1",
14567 json!({
14568 "one.png": "BINARYDATAHERE",
14569 "two.ipynb": "{ totally a notebook }",
14570 "three.txt": "editing text, sure why not?"
14571 }),
14572 )
14573 .await;
14574
14575 let project = Project::test(fs, ["root1".as_ref()], cx).await;
14576 let (workspace, cx) =
14577 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14578
14579 let worktree_id = project.update(cx, |project, cx| {
14580 project.worktrees(cx).next().unwrap().read(cx).id()
14581 });
14582
14583 let handle = workspace
14584 .update_in(cx, |workspace, window, cx| {
14585 let project_path = (worktree_id, rel_path("one.png"));
14586 workspace.open_path(project_path, None, true, window, cx)
14587 })
14588 .await
14589 .unwrap();
14590
14591 // Now we can check if the handle we got back errored or not
14592 assert_eq!(
14593 handle.to_any_view().entity_type(),
14594 TypeId::of::<TestPngItemView>()
14595 );
14596
14597 let handle = workspace
14598 .update_in(cx, |workspace, window, cx| {
14599 let project_path = (worktree_id, rel_path("two.ipynb"));
14600 workspace.open_path(project_path, None, true, window, cx)
14601 })
14602 .await
14603 .unwrap();
14604
14605 assert_eq!(
14606 handle.to_any_view().entity_type(),
14607 TypeId::of::<TestIpynbItemView>()
14608 );
14609
14610 let handle = workspace
14611 .update_in(cx, |workspace, window, cx| {
14612 let project_path = (worktree_id, rel_path("three.txt"));
14613 workspace.open_path(project_path, None, true, window, cx)
14614 })
14615 .await;
14616 assert!(handle.is_err());
14617 }
14618
14619 #[gpui::test]
14620 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
14621 init_test(cx);
14622
14623 cx.update(|cx| {
14624 register_project_item::<TestPngItemView>(cx);
14625 register_project_item::<TestAlternatePngItemView>(cx);
14626 });
14627
14628 let fs = FakeFs::new(cx.executor());
14629 fs.insert_tree(
14630 "/root1",
14631 json!({
14632 "one.png": "BINARYDATAHERE",
14633 "two.ipynb": "{ totally a notebook }",
14634 "three.txt": "editing text, sure why not?"
14635 }),
14636 )
14637 .await;
14638 let project = Project::test(fs, ["root1".as_ref()], cx).await;
14639 let (workspace, cx) =
14640 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14641 let worktree_id = project.update(cx, |project, cx| {
14642 project.worktrees(cx).next().unwrap().read(cx).id()
14643 });
14644
14645 let handle = workspace
14646 .update_in(cx, |workspace, window, cx| {
14647 let project_path = (worktree_id, rel_path("one.png"));
14648 workspace.open_path(project_path, None, true, window, cx)
14649 })
14650 .await
14651 .unwrap();
14652
14653 // This _must_ be the second item registered
14654 assert_eq!(
14655 handle.to_any_view().entity_type(),
14656 TypeId::of::<TestAlternatePngItemView>()
14657 );
14658
14659 let handle = workspace
14660 .update_in(cx, |workspace, window, cx| {
14661 let project_path = (worktree_id, rel_path("three.txt"));
14662 workspace.open_path(project_path, None, true, window, cx)
14663 })
14664 .await;
14665 assert!(handle.is_err());
14666 }
14667 }
14668
14669 #[gpui::test]
14670 async fn test_status_bar_visibility(cx: &mut TestAppContext) {
14671 init_test(cx);
14672
14673 let fs = FakeFs::new(cx.executor());
14674 let project = Project::test(fs, [], cx).await;
14675 let (workspace, _cx) =
14676 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14677
14678 // Test with status bar shown (default)
14679 workspace.read_with(cx, |workspace, cx| {
14680 let visible = workspace.status_bar_visible(cx);
14681 assert!(visible, "Status bar should be visible by default");
14682 });
14683
14684 // Test with status bar hidden
14685 cx.update_global(|store: &mut SettingsStore, cx| {
14686 store.update_user_settings(cx, |settings| {
14687 settings.status_bar.get_or_insert_default().show = Some(false);
14688 });
14689 });
14690
14691 workspace.read_with(cx, |workspace, cx| {
14692 let visible = workspace.status_bar_visible(cx);
14693 assert!(!visible, "Status bar should be hidden when show is false");
14694 });
14695
14696 // Test with status bar shown explicitly
14697 cx.update_global(|store: &mut SettingsStore, cx| {
14698 store.update_user_settings(cx, |settings| {
14699 settings.status_bar.get_or_insert_default().show = Some(true);
14700 });
14701 });
14702
14703 workspace.read_with(cx, |workspace, cx| {
14704 let visible = workspace.status_bar_visible(cx);
14705 assert!(visible, "Status bar should be visible when show is true");
14706 });
14707 }
14708
14709 #[gpui::test]
14710 async fn test_pane_close_active_item(cx: &mut TestAppContext) {
14711 init_test(cx);
14712
14713 let fs = FakeFs::new(cx.executor());
14714 let project = Project::test(fs, [], cx).await;
14715 let (multi_workspace, cx) =
14716 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
14717 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
14718 let panel = workspace.update_in(cx, |workspace, window, cx| {
14719 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14720 workspace.add_panel(panel.clone(), window, cx);
14721
14722 workspace
14723 .right_dock()
14724 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
14725
14726 panel
14727 });
14728
14729 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14730 let item_a = cx.new(TestItem::new);
14731 let item_b = cx.new(TestItem::new);
14732 let item_a_id = item_a.entity_id();
14733 let item_b_id = item_b.entity_id();
14734
14735 pane.update_in(cx, |pane, window, cx| {
14736 pane.add_item(Box::new(item_a.clone()), true, true, None, window, cx);
14737 pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
14738 });
14739
14740 pane.read_with(cx, |pane, _| {
14741 assert_eq!(pane.items_len(), 2);
14742 assert_eq!(pane.active_item().unwrap().item_id(), item_b_id);
14743 });
14744
14745 workspace.update_in(cx, |workspace, window, cx| {
14746 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14747 });
14748
14749 workspace.update_in(cx, |_, window, cx| {
14750 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
14751 });
14752
14753 // Assert that the `pane::CloseActiveItem` action is handled at the
14754 // workspace level when one of the dock panels is focused and, in that
14755 // case, the center pane's active item is closed but the focus is not
14756 // moved.
14757 cx.dispatch_action(pane::CloseActiveItem::default());
14758 cx.run_until_parked();
14759
14760 pane.read_with(cx, |pane, _| {
14761 assert_eq!(pane.items_len(), 1);
14762 assert_eq!(pane.active_item().unwrap().item_id(), item_a_id);
14763 });
14764
14765 workspace.update_in(cx, |workspace, window, cx| {
14766 assert!(workspace.right_dock().read(cx).is_open());
14767 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
14768 });
14769 }
14770
14771 #[gpui::test]
14772 async fn test_panel_zoom_preserved_across_workspace_switch(cx: &mut TestAppContext) {
14773 init_test(cx);
14774 let fs = FakeFs::new(cx.executor());
14775
14776 let project_a = Project::test(fs.clone(), [], cx).await;
14777 let project_b = Project::test(fs, [], cx).await;
14778
14779 let multi_workspace_handle =
14780 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
14781 cx.run_until_parked();
14782
14783 multi_workspace_handle
14784 .update(cx, |mw, _window, cx| {
14785 mw.open_sidebar(cx);
14786 })
14787 .unwrap();
14788
14789 let workspace_a = multi_workspace_handle
14790 .read_with(cx, |mw, _| mw.workspace().clone())
14791 .unwrap();
14792
14793 let _workspace_b = multi_workspace_handle
14794 .update(cx, |mw, window, cx| {
14795 mw.test_add_workspace(project_b, window, cx)
14796 })
14797 .unwrap();
14798
14799 // Switch to workspace A
14800 multi_workspace_handle
14801 .update(cx, |mw, window, cx| {
14802 let workspace = mw.workspaces().next().unwrap().clone();
14803 mw.activate(workspace, window, cx);
14804 })
14805 .unwrap();
14806
14807 let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
14808
14809 // Add a panel to workspace A's right dock and open the dock
14810 let panel = workspace_a.update_in(cx, |workspace, window, cx| {
14811 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14812 workspace.add_panel(panel.clone(), window, cx);
14813 workspace
14814 .right_dock()
14815 .update(cx, |dock, cx| dock.set_open(true, window, cx));
14816 panel
14817 });
14818
14819 // Focus the panel through the workspace (matching existing test pattern)
14820 workspace_a.update_in(cx, |workspace, window, cx| {
14821 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14822 });
14823
14824 // Zoom the panel
14825 panel.update_in(cx, |panel, window, cx| {
14826 panel.set_zoomed(true, window, cx);
14827 });
14828
14829 // Verify the panel is zoomed and the dock is open
14830 workspace_a.update_in(cx, |workspace, window, cx| {
14831 assert!(
14832 workspace.right_dock().read(cx).is_open(),
14833 "dock should be open before switch"
14834 );
14835 assert!(
14836 panel.is_zoomed(window, cx),
14837 "panel should be zoomed before switch"
14838 );
14839 assert!(
14840 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
14841 "panel should be focused before switch"
14842 );
14843 });
14844
14845 // Switch to workspace B
14846 multi_workspace_handle
14847 .update(cx, |mw, window, cx| {
14848 let workspace = mw.workspaces().nth(1).unwrap().clone();
14849 mw.activate(workspace, window, cx);
14850 })
14851 .unwrap();
14852 cx.run_until_parked();
14853
14854 // Switch back to workspace A
14855 multi_workspace_handle
14856 .update(cx, |mw, window, cx| {
14857 let workspace = mw.workspaces().next().unwrap().clone();
14858 mw.activate(workspace, window, cx);
14859 })
14860 .unwrap();
14861 cx.run_until_parked();
14862
14863 // Verify the panel is still zoomed and the dock is still open
14864 workspace_a.update_in(cx, |workspace, window, cx| {
14865 assert!(
14866 workspace.right_dock().read(cx).is_open(),
14867 "dock should still be open after switching back"
14868 );
14869 assert!(
14870 panel.is_zoomed(window, cx),
14871 "panel should still be zoomed after switching back"
14872 );
14873 });
14874 }
14875
14876 fn pane_items_paths(pane: &Entity<Pane>, cx: &App) -> Vec<String> {
14877 pane.read(cx)
14878 .items()
14879 .flat_map(|item| {
14880 item.project_paths(cx)
14881 .into_iter()
14882 .map(|path| path.path.display(PathStyle::local()).into_owned())
14883 })
14884 .collect()
14885 }
14886
14887 pub fn init_test(cx: &mut TestAppContext) {
14888 cx.update(|cx| {
14889 let settings_store = SettingsStore::test(cx);
14890 cx.set_global(settings_store);
14891 cx.set_global(db::AppDatabase::test_new());
14892 theme_settings::init(theme::LoadThemes::JustBase, cx);
14893 });
14894 }
14895
14896 #[gpui::test]
14897 async fn test_toggle_theme_mode_persists_and_updates_active_theme(cx: &mut TestAppContext) {
14898 use settings::{ThemeName, ThemeSelection};
14899 use theme::SystemAppearance;
14900 use zed_actions::theme::ToggleMode;
14901
14902 init_test(cx);
14903
14904 let fs = FakeFs::new(cx.executor());
14905 let settings_fs: Arc<dyn fs::Fs> = fs.clone();
14906
14907 fs.insert_tree(path!("/root"), json!({ "file.rs": "fn main() {}\n" }))
14908 .await;
14909
14910 // Build a test project and workspace view so the test can invoke
14911 // the workspace action handler the same way the UI would.
14912 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
14913 let (workspace, cx) =
14914 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14915
14916 // Seed the settings file with a plain static light theme so the
14917 // first toggle always starts from a known persisted state.
14918 workspace.update_in(cx, |_workspace, _window, cx| {
14919 *SystemAppearance::global_mut(cx) = SystemAppearance(theme::Appearance::Light);
14920 settings::update_settings_file(settings_fs.clone(), cx, |settings, _cx| {
14921 settings.theme.theme = Some(ThemeSelection::Static(ThemeName("One Light".into())));
14922 });
14923 });
14924 cx.executor().advance_clock(Duration::from_millis(200));
14925 cx.run_until_parked();
14926
14927 // Confirm the initial persisted settings contain the static theme
14928 // we just wrote before any toggling happens.
14929 let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
14930 assert!(settings_text.contains(r#""theme": "One Light""#));
14931
14932 // Toggle once. This should migrate the persisted theme settings
14933 // into light/dark slots and enable system mode.
14934 workspace.update_in(cx, |workspace, window, cx| {
14935 workspace.toggle_theme_mode(&ToggleMode, window, cx);
14936 });
14937 cx.executor().advance_clock(Duration::from_millis(200));
14938 cx.run_until_parked();
14939
14940 // 1. Static -> Dynamic
14941 // this assertion checks theme changed from static to dynamic.
14942 let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
14943 let parsed: serde_json::Value = settings::parse_json_with_comments(&settings_text).unwrap();
14944 assert_eq!(
14945 parsed["theme"],
14946 serde_json::json!({
14947 "mode": "system",
14948 "light": "One Light",
14949 "dark": "One Dark"
14950 })
14951 );
14952
14953 // 2. Toggle again, suppose it will change the mode to light
14954 workspace.update_in(cx, |workspace, window, cx| {
14955 workspace.toggle_theme_mode(&ToggleMode, window, cx);
14956 });
14957 cx.executor().advance_clock(Duration::from_millis(200));
14958 cx.run_until_parked();
14959
14960 let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
14961 assert!(settings_text.contains(r#""mode": "light""#));
14962 }
14963
14964 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
14965 let item = TestProjectItem::new(id, path, cx);
14966 item.update(cx, |item, _| {
14967 item.is_dirty = true;
14968 });
14969 item
14970 }
14971
14972 #[gpui::test]
14973 async fn test_zoomed_panel_without_pane_preserved_on_center_focus(
14974 cx: &mut gpui::TestAppContext,
14975 ) {
14976 init_test(cx);
14977 let fs = FakeFs::new(cx.executor());
14978
14979 let project = Project::test(fs, [], cx).await;
14980 let (workspace, cx) =
14981 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
14982
14983 let panel = workspace.update_in(cx, |workspace, window, cx| {
14984 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14985 workspace.add_panel(panel.clone(), window, cx);
14986 workspace
14987 .right_dock()
14988 .update(cx, |dock, cx| dock.set_open(true, window, cx));
14989 panel
14990 });
14991
14992 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14993 pane.update_in(cx, |pane, window, cx| {
14994 let item = cx.new(TestItem::new);
14995 pane.add_item(Box::new(item), true, true, None, window, cx);
14996 });
14997
14998 // Transfer focus to the panel, then zoom it. Using toggle_panel_focus
14999 // mirrors the real-world flow and avoids side effects from directly
15000 // focusing the panel while the center pane is active.
15001 workspace.update_in(cx, |workspace, window, cx| {
15002 workspace.toggle_panel_focus::<TestPanel>(window, cx);
15003 });
15004
15005 panel.update_in(cx, |panel, window, cx| {
15006 panel.set_zoomed(true, window, cx);
15007 });
15008
15009 workspace.update_in(cx, |workspace, window, cx| {
15010 assert!(workspace.right_dock().read(cx).is_open());
15011 assert!(panel.is_zoomed(window, cx));
15012 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
15013 });
15014
15015 // Simulate a spurious pane::Event::Focus on the center pane while the
15016 // panel still has focus. This mirrors what happens during macOS window
15017 // activation: the center pane fires a focus event even though actual
15018 // focus remains on the dock panel.
15019 pane.update_in(cx, |_, _, cx| {
15020 cx.emit(pane::Event::Focus);
15021 });
15022
15023 // The dock must remain open because the panel had focus at the time the
15024 // event was processed. Before the fix, dock_to_preserve was None for
15025 // panels that don't implement pane(), causing the dock to close.
15026 workspace.update_in(cx, |workspace, window, cx| {
15027 assert!(
15028 workspace.right_dock().read(cx).is_open(),
15029 "Dock should stay open when its zoomed panel (without pane()) still has focus"
15030 );
15031 assert!(panel.is_zoomed(window, cx));
15032 });
15033 }
15034
15035 #[gpui::test]
15036 async fn test_panels_stay_open_after_position_change_and_settings_update(
15037 cx: &mut gpui::TestAppContext,
15038 ) {
15039 init_test(cx);
15040 let fs = FakeFs::new(cx.executor());
15041 let project = Project::test(fs, [], cx).await;
15042 let (workspace, cx) =
15043 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
15044
15045 // Add two panels to the left dock and open it.
15046 let (panel_a, panel_b) = workspace.update_in(cx, |workspace, window, cx| {
15047 let panel_a = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
15048 let panel_b = cx.new(|cx| TestPanel::new(DockPosition::Left, 101, cx));
15049 workspace.add_panel(panel_a.clone(), window, cx);
15050 workspace.add_panel(panel_b.clone(), window, cx);
15051 workspace.left_dock().update(cx, |dock, cx| {
15052 dock.set_open(true, window, cx);
15053 dock.activate_panel(0, window, cx);
15054 });
15055 (panel_a, panel_b)
15056 });
15057
15058 workspace.update_in(cx, |workspace, _, cx| {
15059 assert!(workspace.left_dock().read(cx).is_open());
15060 });
15061
15062 // Simulate a feature flag changing default dock positions: both panels
15063 // move from Left to Right.
15064 workspace.update_in(cx, |_workspace, _window, cx| {
15065 panel_a.update(cx, |p, _cx| p.position = DockPosition::Right);
15066 panel_b.update(cx, |p, _cx| p.position = DockPosition::Right);
15067 cx.update_global::<SettingsStore, _>(|_, _| {});
15068 });
15069
15070 // Both panels should now be in the right dock.
15071 workspace.update_in(cx, |workspace, _, cx| {
15072 let right_dock = workspace.right_dock().read(cx);
15073 assert_eq!(right_dock.panels_len(), 2);
15074 });
15075
15076 // Open the right dock and activate panel_b (simulating the user
15077 // opening the panel after it moved).
15078 workspace.update_in(cx, |workspace, window, cx| {
15079 workspace.right_dock().update(cx, |dock, cx| {
15080 dock.set_open(true, window, cx);
15081 dock.activate_panel(1, window, cx);
15082 });
15083 });
15084
15085 // Now trigger another SettingsStore change
15086 workspace.update_in(cx, |_workspace, _window, cx| {
15087 cx.update_global::<SettingsStore, _>(|_, _| {});
15088 });
15089
15090 workspace.update_in(cx, |workspace, _, cx| {
15091 assert!(
15092 workspace.right_dock().read(cx).is_open(),
15093 "Right dock should still be open after a settings change"
15094 );
15095 assert_eq!(
15096 workspace.right_dock().read(cx).panels_len(),
15097 2,
15098 "Both panels should still be in the right dock"
15099 );
15100 });
15101 }
15102}