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, MultiWorkspace,
35 MultiWorkspaceEvent, NewThread, NextProject, NextThread, PreviousProject, PreviousThread,
36 ShowFewerThreads, ShowMoreThreads, Sidebar, SidebarEvent, SidebarHandle, SidebarRenderState,
37 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 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 let MultiWorkspaceState {
8721 sidebar_open,
8722 project_group_keys,
8723 sidebar_state,
8724 ..
8725 } = state;
8726
8727 let workspace_result = if active_workspace.paths.is_empty() {
8728 cx.update(|cx| {
8729 open_workspace_by_id(active_workspace.workspace_id, app_state.clone(), None, cx)
8730 })
8731 .await
8732 } else {
8733 cx.update(|cx| {
8734 Workspace::new_local(
8735 active_workspace.paths.paths().to_vec(),
8736 app_state.clone(),
8737 None,
8738 None,
8739 None,
8740 OpenMode::Activate,
8741 cx,
8742 )
8743 })
8744 .await
8745 .map(|result| result.window)
8746 };
8747
8748 let window_handle = match workspace_result {
8749 Ok(handle) => handle,
8750 Err(err) => {
8751 log::error!("Failed to restore active workspace: {err:#}");
8752
8753 // Try each project group's paths as a fallback.
8754 let mut fallback_handle = None;
8755 for key in &project_group_keys {
8756 let key: ProjectGroupKey = key.clone().into();
8757 let paths = key.path_list().paths().to_vec();
8758 match cx
8759 .update(|cx| {
8760 Workspace::new_local(
8761 paths,
8762 app_state.clone(),
8763 None,
8764 None,
8765 None,
8766 OpenMode::Activate,
8767 cx,
8768 )
8769 })
8770 .await
8771 {
8772 Ok(OpenResult { window, .. }) => {
8773 fallback_handle = Some(window);
8774 break;
8775 }
8776 Err(fallback_err) => {
8777 log::error!("Fallback project group also failed: {fallback_err:#}");
8778 }
8779 }
8780 }
8781
8782 fallback_handle.ok_or(err)?
8783 }
8784 };
8785
8786 if !project_group_keys.is_empty() {
8787 let fs = app_state.fs.clone();
8788
8789 // Resolve linked worktree paths to their main repo paths so
8790 // stale keys from previous sessions get normalized and deduped.
8791 let mut resolved_keys: Vec<ProjectGroupKey> = Vec::new();
8792 for key in project_group_keys.into_iter().map(ProjectGroupKey::from) {
8793 if key.path_list().paths().is_empty() {
8794 continue;
8795 }
8796 let mut resolved_paths = Vec::new();
8797 for path in key.path_list().paths() {
8798 if let Some(common_dir) =
8799 project::discover_root_repo_common_dir(path, fs.as_ref()).await
8800 {
8801 let main_path = common_dir.parent().unwrap_or(&common_dir);
8802 resolved_paths.push(main_path.to_path_buf());
8803 } else {
8804 resolved_paths.push(path.to_path_buf());
8805 }
8806 }
8807 let resolved = ProjectGroupKey::new(key.host(), PathList::new(&resolved_paths));
8808 if !resolved_keys.contains(&resolved) {
8809 resolved_keys.push(resolved);
8810 }
8811 }
8812
8813 window_handle
8814 .update(cx, |multi_workspace, _window, _cx| {
8815 multi_workspace.restore_project_group_keys(resolved_keys);
8816 })
8817 .ok();
8818 }
8819
8820 if sidebar_open {
8821 window_handle
8822 .update(cx, |multi_workspace, _, cx| {
8823 multi_workspace.open_sidebar(cx);
8824 })
8825 .ok();
8826 }
8827
8828 if let Some(sidebar_state) = sidebar_state {
8829 window_handle
8830 .update(cx, |multi_workspace, window, cx| {
8831 if let Some(sidebar) = multi_workspace.sidebar() {
8832 sidebar.restore_serialized_state(&sidebar_state, window, cx);
8833 }
8834 multi_workspace.serialize(cx);
8835 })
8836 .ok();
8837 }
8838
8839 window_handle
8840 .update(cx, |_, window, _cx| {
8841 window.activate_window();
8842 })
8843 .ok();
8844
8845 Ok(window_handle)
8846}
8847
8848actions!(
8849 collab,
8850 [
8851 /// Opens the channel notes for the current call.
8852 ///
8853 /// Use `collab_panel::OpenSelectedChannelNotes` to open the channel notes for the selected
8854 /// channel in the collab panel.
8855 ///
8856 /// If you want to open a specific channel, use `zed::OpenZedUrl` with a channel notes URL -
8857 /// can be copied via "Copy link to section" in the context menu of the channel notes
8858 /// buffer. These URLs look like `https://zed.dev/channel/channel-name-CHANNEL_ID/notes`.
8859 OpenChannelNotes,
8860 /// Mutes your microphone.
8861 Mute,
8862 /// Deafens yourself (mute both microphone and speakers).
8863 Deafen,
8864 /// Leaves the current call.
8865 LeaveCall,
8866 /// Shares the current project with collaborators.
8867 ShareProject,
8868 /// Shares your screen with collaborators.
8869 ScreenShare,
8870 /// Copies the current room name and session id for debugging purposes.
8871 CopyRoomId,
8872 ]
8873);
8874
8875/// Opens the channel notes for a specific channel by its ID.
8876#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
8877#[action(namespace = collab)]
8878#[serde(deny_unknown_fields)]
8879pub struct OpenChannelNotesById {
8880 pub channel_id: u64,
8881}
8882
8883actions!(
8884 zed,
8885 [
8886 /// Opens the Zed log file.
8887 OpenLog,
8888 /// Reveals the Zed log file in the system file manager.
8889 RevealLogInFileManager
8890 ]
8891);
8892
8893async fn join_channel_internal(
8894 channel_id: ChannelId,
8895 app_state: &Arc<AppState>,
8896 requesting_window: Option<WindowHandle<MultiWorkspace>>,
8897 requesting_workspace: Option<WeakEntity<Workspace>>,
8898 active_call: &dyn AnyActiveCall,
8899 cx: &mut AsyncApp,
8900) -> Result<bool> {
8901 let (should_prompt, already_in_channel) = cx.update(|cx| {
8902 if !active_call.is_in_room(cx) {
8903 return (false, false);
8904 }
8905
8906 let already_in_channel = active_call.channel_id(cx) == Some(channel_id);
8907 let should_prompt = active_call.is_sharing_project(cx)
8908 && active_call.has_remote_participants(cx)
8909 && !already_in_channel;
8910 (should_prompt, already_in_channel)
8911 });
8912
8913 if already_in_channel {
8914 let task = cx.update(|cx| {
8915 if let Some((project, host)) = active_call.most_active_project(cx) {
8916 Some(join_in_room_project(project, host, app_state.clone(), cx))
8917 } else {
8918 None
8919 }
8920 });
8921 if let Some(task) = task {
8922 task.await?;
8923 }
8924 return anyhow::Ok(true);
8925 }
8926
8927 if should_prompt {
8928 if let Some(multi_workspace) = requesting_window {
8929 let answer = multi_workspace
8930 .update(cx, |_, window, cx| {
8931 window.prompt(
8932 PromptLevel::Warning,
8933 "Do you want to switch channels?",
8934 Some("Leaving this call will unshare your current project."),
8935 &["Yes, Join Channel", "Cancel"],
8936 cx,
8937 )
8938 })?
8939 .await;
8940
8941 if answer == Ok(1) {
8942 return Ok(false);
8943 }
8944 } else {
8945 return Ok(false);
8946 }
8947 }
8948
8949 let client = cx.update(|cx| active_call.client(cx));
8950
8951 let mut client_status = client.status();
8952
8953 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
8954 'outer: loop {
8955 let Some(status) = client_status.recv().await else {
8956 anyhow::bail!("error connecting");
8957 };
8958
8959 match status {
8960 Status::Connecting
8961 | Status::Authenticating
8962 | Status::Authenticated
8963 | Status::Reconnecting
8964 | Status::Reauthenticating
8965 | Status::Reauthenticated => continue,
8966 Status::Connected { .. } => break 'outer,
8967 Status::SignedOut | Status::AuthenticationError => {
8968 return Err(ErrorCode::SignedOut.into());
8969 }
8970 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
8971 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
8972 return Err(ErrorCode::Disconnected.into());
8973 }
8974 }
8975 }
8976
8977 let joined = cx
8978 .update(|cx| active_call.join_channel(channel_id, cx))
8979 .await?;
8980
8981 if !joined {
8982 return anyhow::Ok(true);
8983 }
8984
8985 cx.update(|cx| active_call.room_update_completed(cx)).await;
8986
8987 let task = cx.update(|cx| {
8988 if let Some((project, host)) = active_call.most_active_project(cx) {
8989 return Some(join_in_room_project(project, host, app_state.clone(), cx));
8990 }
8991
8992 // If you are the first to join a channel, see if you should share your project.
8993 if !active_call.has_remote_participants(cx)
8994 && !active_call.local_participant_is_guest(cx)
8995 && let Some(workspace) = requesting_workspace.as_ref().and_then(|w| w.upgrade())
8996 {
8997 let project = workspace.update(cx, |workspace, cx| {
8998 let project = workspace.project.read(cx);
8999
9000 if !active_call.share_on_join(cx) {
9001 return None;
9002 }
9003
9004 if (project.is_local() || project.is_via_remote_server())
9005 && project.visible_worktrees(cx).any(|tree| {
9006 tree.read(cx)
9007 .root_entry()
9008 .is_some_and(|entry| entry.is_dir())
9009 })
9010 {
9011 Some(workspace.project.clone())
9012 } else {
9013 None
9014 }
9015 });
9016 if let Some(project) = project {
9017 let share_task = active_call.share_project(project, cx);
9018 return Some(cx.spawn(async move |_cx| -> Result<()> {
9019 share_task.await?;
9020 Ok(())
9021 }));
9022 }
9023 }
9024
9025 None
9026 });
9027 if let Some(task) = task {
9028 task.await?;
9029 return anyhow::Ok(true);
9030 }
9031 anyhow::Ok(false)
9032}
9033
9034pub fn join_channel(
9035 channel_id: ChannelId,
9036 app_state: Arc<AppState>,
9037 requesting_window: Option<WindowHandle<MultiWorkspace>>,
9038 requesting_workspace: Option<WeakEntity<Workspace>>,
9039 cx: &mut App,
9040) -> Task<Result<()>> {
9041 let active_call = GlobalAnyActiveCall::global(cx).clone();
9042 cx.spawn(async move |cx| {
9043 let result = join_channel_internal(
9044 channel_id,
9045 &app_state,
9046 requesting_window,
9047 requesting_workspace,
9048 &*active_call.0,
9049 cx,
9050 )
9051 .await;
9052
9053 // join channel succeeded, and opened a window
9054 if matches!(result, Ok(true)) {
9055 return anyhow::Ok(());
9056 }
9057
9058 // find an existing workspace to focus and show call controls
9059 let mut active_window = requesting_window.or_else(|| activate_any_workspace_window(cx));
9060 if active_window.is_none() {
9061 // no open workspaces, make one to show the error in (blergh)
9062 let OpenResult {
9063 window: window_handle,
9064 ..
9065 } = cx
9066 .update(|cx| {
9067 Workspace::new_local(
9068 vec![],
9069 app_state.clone(),
9070 requesting_window,
9071 None,
9072 None,
9073 OpenMode::Activate,
9074 cx,
9075 )
9076 })
9077 .await?;
9078
9079 window_handle
9080 .update(cx, |_, window, _cx| {
9081 window.activate_window();
9082 })
9083 .ok();
9084
9085 if result.is_ok() {
9086 cx.update(|cx| {
9087 cx.dispatch_action(&OpenChannelNotes);
9088 });
9089 }
9090
9091 active_window = Some(window_handle);
9092 }
9093
9094 if let Err(err) = result {
9095 log::error!("failed to join channel: {}", err);
9096 if let Some(active_window) = active_window {
9097 active_window
9098 .update(cx, |_, window, cx| {
9099 let detail: SharedString = match err.error_code() {
9100 ErrorCode::SignedOut => "Please sign in to continue.".into(),
9101 ErrorCode::UpgradeRequired => concat!(
9102 "Your are running an unsupported version of Zed. ",
9103 "Please update to continue."
9104 )
9105 .into(),
9106 ErrorCode::NoSuchChannel => concat!(
9107 "No matching channel was found. ",
9108 "Please check the link and try again."
9109 )
9110 .into(),
9111 ErrorCode::Forbidden => concat!(
9112 "This channel is private, and you do not have access. ",
9113 "Please ask someone to add you and try again."
9114 )
9115 .into(),
9116 ErrorCode::Disconnected => {
9117 "Please check your internet connection and try again.".into()
9118 }
9119 _ => format!("{}\n\nPlease try again.", err).into(),
9120 };
9121 window.prompt(
9122 PromptLevel::Critical,
9123 "Failed to join channel",
9124 Some(&detail),
9125 &["Ok"],
9126 cx,
9127 )
9128 })?
9129 .await
9130 .ok();
9131 }
9132 }
9133
9134 // return ok, we showed the error to the user.
9135 anyhow::Ok(())
9136 })
9137}
9138
9139pub async fn get_any_active_multi_workspace(
9140 app_state: Arc<AppState>,
9141 mut cx: AsyncApp,
9142) -> anyhow::Result<WindowHandle<MultiWorkspace>> {
9143 // find an existing workspace to focus and show call controls
9144 let active_window = activate_any_workspace_window(&mut cx);
9145 if active_window.is_none() {
9146 cx.update(|cx| {
9147 Workspace::new_local(
9148 vec![],
9149 app_state.clone(),
9150 None,
9151 None,
9152 None,
9153 OpenMode::Activate,
9154 cx,
9155 )
9156 })
9157 .await?;
9158 }
9159 activate_any_workspace_window(&mut cx).context("could not open zed")
9160}
9161
9162fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<MultiWorkspace>> {
9163 cx.update(|cx| {
9164 if let Some(workspace_window) = cx
9165 .active_window()
9166 .and_then(|window| window.downcast::<MultiWorkspace>())
9167 {
9168 return Some(workspace_window);
9169 }
9170
9171 for window in cx.windows() {
9172 if let Some(workspace_window) = window.downcast::<MultiWorkspace>() {
9173 workspace_window
9174 .update(cx, |_, window, _| window.activate_window())
9175 .ok();
9176 return Some(workspace_window);
9177 }
9178 }
9179 None
9180 })
9181}
9182
9183pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<MultiWorkspace>> {
9184 workspace_windows_for_location(&SerializedWorkspaceLocation::Local, cx)
9185}
9186
9187pub fn workspace_windows_for_location(
9188 serialized_location: &SerializedWorkspaceLocation,
9189 cx: &App,
9190) -> Vec<WindowHandle<MultiWorkspace>> {
9191 cx.windows()
9192 .into_iter()
9193 .filter_map(|window| window.downcast::<MultiWorkspace>())
9194 .filter(|multi_workspace| {
9195 let same_host = |left: &RemoteConnectionOptions, right: &RemoteConnectionOptions| match (left, right) {
9196 (RemoteConnectionOptions::Ssh(a), RemoteConnectionOptions::Ssh(b)) => {
9197 (&a.host, &a.username, &a.port) == (&b.host, &b.username, &b.port)
9198 }
9199 (RemoteConnectionOptions::Wsl(a), RemoteConnectionOptions::Wsl(b)) => {
9200 // The WSL username is not consistently populated in the workspace location, so ignore it for now.
9201 a.distro_name == b.distro_name
9202 }
9203 (RemoteConnectionOptions::Docker(a), RemoteConnectionOptions::Docker(b)) => {
9204 a.container_id == b.container_id
9205 }
9206 #[cfg(any(test, feature = "test-support"))]
9207 (RemoteConnectionOptions::Mock(a), RemoteConnectionOptions::Mock(b)) => {
9208 a.id == b.id
9209 }
9210 _ => false,
9211 };
9212
9213 multi_workspace.read(cx).is_ok_and(|multi_workspace| {
9214 multi_workspace.workspaces().any(|workspace| {
9215 match workspace.read(cx).workspace_location(cx) {
9216 WorkspaceLocation::Location(location, _) => {
9217 match (&location, serialized_location) {
9218 (
9219 SerializedWorkspaceLocation::Local,
9220 SerializedWorkspaceLocation::Local,
9221 ) => true,
9222 (
9223 SerializedWorkspaceLocation::Remote(a),
9224 SerializedWorkspaceLocation::Remote(b),
9225 ) => same_host(a, b),
9226 _ => false,
9227 }
9228 }
9229 _ => false,
9230 }
9231 })
9232 })
9233 })
9234 .collect()
9235}
9236
9237pub async fn find_existing_workspace(
9238 abs_paths: &[PathBuf],
9239 open_options: &OpenOptions,
9240 location: &SerializedWorkspaceLocation,
9241 cx: &mut AsyncApp,
9242) -> (
9243 Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)>,
9244 OpenVisible,
9245) {
9246 let mut existing: Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)> = None;
9247 let mut open_visible = OpenVisible::All;
9248 let mut best_match = None;
9249
9250 cx.update(|cx| {
9251 for window in workspace_windows_for_location(location, cx) {
9252 if let Ok(multi_workspace) = window.read(cx) {
9253 for workspace in multi_workspace.workspaces() {
9254 let project = workspace.read(cx).project.read(cx);
9255 let m = project.visibility_for_paths(
9256 abs_paths,
9257 open_options.open_new_workspace == None,
9258 cx,
9259 );
9260 if m > best_match {
9261 existing = Some((window, workspace.clone()));
9262 best_match = m;
9263 } else if best_match.is_none() && open_options.open_new_workspace == Some(false)
9264 {
9265 existing = Some((window, workspace.clone()))
9266 }
9267 }
9268 }
9269 }
9270 });
9271
9272 // With -n, only reuse a window if the path is genuinely contained
9273 // within an existing worktree (don't fall back to any arbitrary window).
9274 if open_options.open_new_workspace == Some(true) && best_match.is_none() {
9275 existing = None;
9276 }
9277
9278 if open_options.open_new_workspace != Some(true) {
9279 let all_paths_are_files = existing
9280 .as_ref()
9281 .and_then(|(_, target_workspace)| {
9282 cx.update(|cx| {
9283 let workspace = target_workspace.read(cx);
9284 let project = workspace.project.read(cx);
9285 let path_style = workspace.path_style(cx);
9286 Some(!abs_paths.iter().any(|path| {
9287 let path = util::paths::SanitizedPath::new(path);
9288 project.worktrees(cx).any(|worktree| {
9289 let worktree = worktree.read(cx);
9290 let abs_path = worktree.abs_path();
9291 path_style
9292 .strip_prefix(path.as_ref(), abs_path.as_ref())
9293 .and_then(|rel| worktree.entry_for_path(&rel))
9294 .is_some_and(|e| e.is_dir())
9295 })
9296 }))
9297 })
9298 })
9299 .unwrap_or(false);
9300
9301 if open_options.open_new_workspace.is_none()
9302 && existing.is_some()
9303 && open_options.wait
9304 && all_paths_are_files
9305 {
9306 cx.update(|cx| {
9307 let windows = workspace_windows_for_location(location, cx);
9308 let window = cx
9309 .active_window()
9310 .and_then(|window| window.downcast::<MultiWorkspace>())
9311 .filter(|window| windows.contains(window))
9312 .or_else(|| windows.into_iter().next());
9313 if let Some(window) = window {
9314 if let Ok(multi_workspace) = window.read(cx) {
9315 let active_workspace = multi_workspace.workspace().clone();
9316 existing = Some((window, active_workspace));
9317 open_visible = OpenVisible::None;
9318 }
9319 }
9320 });
9321 }
9322 }
9323 (existing, open_visible)
9324}
9325
9326#[derive(Default, Clone)]
9327pub struct OpenOptions {
9328 pub visible: Option<OpenVisible>,
9329 pub focus: Option<bool>,
9330 pub open_new_workspace: Option<bool>,
9331 pub wait: bool,
9332 pub requesting_window: Option<WindowHandle<MultiWorkspace>>,
9333 pub open_mode: OpenMode,
9334 pub env: Option<HashMap<String, String>>,
9335 pub open_in_dev_container: bool,
9336}
9337
9338/// The result of opening a workspace via [`open_paths`], [`Workspace::new_local`],
9339/// or [`Workspace::open_workspace_for_paths`].
9340pub struct OpenResult {
9341 pub window: WindowHandle<MultiWorkspace>,
9342 pub workspace: Entity<Workspace>,
9343 pub opened_items: Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
9344}
9345
9346/// Opens a workspace by its database ID, used for restoring empty workspaces with unsaved content.
9347pub fn open_workspace_by_id(
9348 workspace_id: WorkspaceId,
9349 app_state: Arc<AppState>,
9350 requesting_window: Option<WindowHandle<MultiWorkspace>>,
9351 cx: &mut App,
9352) -> Task<anyhow::Result<WindowHandle<MultiWorkspace>>> {
9353 let project_handle = Project::local(
9354 app_state.client.clone(),
9355 app_state.node_runtime.clone(),
9356 app_state.user_store.clone(),
9357 app_state.languages.clone(),
9358 app_state.fs.clone(),
9359 None,
9360 project::LocalProjectFlags {
9361 init_worktree_trust: true,
9362 ..project::LocalProjectFlags::default()
9363 },
9364 cx,
9365 );
9366
9367 let db = WorkspaceDb::global(cx);
9368 let kvp = db::kvp::KeyValueStore::global(cx);
9369 cx.spawn(async move |cx| {
9370 let serialized_workspace = db
9371 .workspace_for_id(workspace_id)
9372 .with_context(|| format!("Workspace {workspace_id:?} not found"))?;
9373
9374 let centered_layout = serialized_workspace.centered_layout;
9375
9376 let (window, workspace) = if let Some(window) = requesting_window {
9377 let workspace = window.update(cx, |multi_workspace, window, cx| {
9378 let workspace = cx.new(|cx| {
9379 let mut workspace = Workspace::new(
9380 Some(workspace_id),
9381 project_handle.clone(),
9382 app_state.clone(),
9383 window,
9384 cx,
9385 );
9386 workspace.centered_layout = centered_layout;
9387 workspace
9388 });
9389 multi_workspace.add(workspace.clone(), &*window, cx);
9390 workspace
9391 })?;
9392 (window, workspace)
9393 } else {
9394 let window_bounds_override = window_bounds_env_override();
9395
9396 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
9397 (Some(WindowBounds::Windowed(bounds)), None)
9398 } else if let Some(display) = serialized_workspace.display
9399 && let Some(bounds) = serialized_workspace.window_bounds.as_ref()
9400 {
9401 (Some(bounds.0), Some(display))
9402 } else if let Some((display, bounds)) = persistence::read_default_window_bounds(&kvp) {
9403 (Some(bounds), Some(display))
9404 } else {
9405 (None, None)
9406 };
9407
9408 let options = cx.update(|cx| {
9409 let mut options = (app_state.build_window_options)(display, cx);
9410 options.window_bounds = window_bounds;
9411 options
9412 });
9413
9414 let window = cx.open_window(options, {
9415 let app_state = app_state.clone();
9416 let project_handle = project_handle.clone();
9417 move |window, cx| {
9418 let workspace = cx.new(|cx| {
9419 let mut workspace = Workspace::new(
9420 Some(workspace_id),
9421 project_handle,
9422 app_state,
9423 window,
9424 cx,
9425 );
9426 workspace.centered_layout = centered_layout;
9427 workspace
9428 });
9429 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
9430 }
9431 })?;
9432
9433 let workspace = window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
9434 multi_workspace.workspace().clone()
9435 })?;
9436
9437 (window, workspace)
9438 };
9439
9440 notify_if_database_failed(window, cx);
9441
9442 // Restore items from the serialized workspace
9443 window
9444 .update(cx, |_, window, cx| {
9445 workspace.update(cx, |_workspace, cx| {
9446 open_items(Some(serialized_workspace), vec![], window, cx)
9447 })
9448 })?
9449 .await?;
9450
9451 window.update(cx, |_, window, cx| {
9452 workspace.update(cx, |workspace, cx| {
9453 workspace.serialize_workspace(window, cx);
9454 });
9455 })?;
9456
9457 Ok(window)
9458 })
9459}
9460
9461#[allow(clippy::type_complexity)]
9462pub fn open_paths(
9463 abs_paths: &[PathBuf],
9464 app_state: Arc<AppState>,
9465 mut open_options: OpenOptions,
9466 cx: &mut App,
9467) -> Task<anyhow::Result<OpenResult>> {
9468 let abs_paths = abs_paths.to_vec();
9469 #[cfg(target_os = "windows")]
9470 let wsl_path = abs_paths
9471 .iter()
9472 .find_map(|p| util::paths::WslPath::from_path(p));
9473
9474 cx.spawn(async move |cx| {
9475 let (mut existing, mut open_visible) = find_existing_workspace(
9476 &abs_paths,
9477 &open_options,
9478 &SerializedWorkspaceLocation::Local,
9479 cx,
9480 )
9481 .await;
9482
9483 // Fallback: if no workspace contains the paths and all paths are files,
9484 // prefer an existing local workspace window (active window first).
9485 if open_options.open_new_workspace.is_none() && existing.is_none() {
9486 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
9487 let all_metadatas = futures::future::join_all(all_paths)
9488 .await
9489 .into_iter()
9490 .filter_map(|result| result.ok().flatten());
9491
9492 if all_metadatas.into_iter().all(|file| !file.is_dir) {
9493 cx.update(|cx| {
9494 let windows = workspace_windows_for_location(
9495 &SerializedWorkspaceLocation::Local,
9496 cx,
9497 );
9498 let window = cx
9499 .active_window()
9500 .and_then(|window| window.downcast::<MultiWorkspace>())
9501 .filter(|window| windows.contains(window))
9502 .or_else(|| windows.into_iter().next());
9503 if let Some(window) = window {
9504 if let Ok(multi_workspace) = window.read(cx) {
9505 let active_workspace = multi_workspace.workspace().clone();
9506 existing = Some((window, active_workspace));
9507 open_visible = OpenVisible::None;
9508 }
9509 }
9510 });
9511 }
9512 }
9513
9514 // Fallback for directories: when no flag is specified and no existing
9515 // workspace matched, add the directory as a new workspace in the
9516 // active window's MultiWorkspace (instead of opening a new window).
9517 if open_options.open_new_workspace.is_none() && existing.is_none() {
9518 let target_window = cx.update(|cx| {
9519 let windows = workspace_windows_for_location(
9520 &SerializedWorkspaceLocation::Local,
9521 cx,
9522 );
9523 let window = cx
9524 .active_window()
9525 .and_then(|window| window.downcast::<MultiWorkspace>())
9526 .filter(|window| windows.contains(window))
9527 .or_else(|| windows.into_iter().next());
9528 window.filter(|window| {
9529 window.read(cx).is_ok_and(|mw| mw.multi_workspace_enabled(cx))
9530 })
9531 });
9532
9533 if let Some(window) = target_window {
9534 open_options.requesting_window = Some(window);
9535 window
9536 .update(cx, |multi_workspace, _, cx| {
9537 multi_workspace.open_sidebar(cx);
9538 })
9539 .log_err();
9540 }
9541 }
9542
9543 let open_in_dev_container = open_options.open_in_dev_container;
9544
9545 let result = if let Some((existing, target_workspace)) = existing {
9546 let open_task = existing
9547 .update(cx, |multi_workspace, window, cx| {
9548 window.activate_window();
9549 multi_workspace.activate(target_workspace.clone(), window, cx);
9550 target_workspace.update(cx, |workspace, cx| {
9551 if open_in_dev_container {
9552 workspace.set_open_in_dev_container(true);
9553 }
9554 workspace.open_paths(
9555 abs_paths,
9556 OpenOptions {
9557 visible: Some(open_visible),
9558 ..Default::default()
9559 },
9560 None,
9561 window,
9562 cx,
9563 )
9564 })
9565 })?
9566 .await;
9567
9568 _ = existing.update(cx, |multi_workspace, _, cx| {
9569 let workspace = multi_workspace.workspace().clone();
9570 workspace.update(cx, |workspace, cx| {
9571 for item in open_task.iter().flatten() {
9572 if let Err(e) = item {
9573 workspace.show_error(&e, cx);
9574 }
9575 }
9576 });
9577 });
9578
9579 Ok(OpenResult { window: existing, workspace: target_workspace, opened_items: open_task })
9580 } else {
9581 let init = if open_in_dev_container {
9582 Some(Box::new(|workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context<Workspace>| {
9583 workspace.set_open_in_dev_container(true);
9584 }) as Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>)
9585 } else {
9586 None
9587 };
9588 let result = cx
9589 .update(move |cx| {
9590 Workspace::new_local(
9591 abs_paths,
9592 app_state.clone(),
9593 open_options.requesting_window,
9594 open_options.env,
9595 init,
9596 open_options.open_mode,
9597 cx,
9598 )
9599 })
9600 .await;
9601
9602 if let Ok(ref result) = result {
9603 result.window
9604 .update(cx, |_, window, _cx| {
9605 window.activate_window();
9606 })
9607 .log_err();
9608 }
9609
9610 result
9611 };
9612
9613 #[cfg(target_os = "windows")]
9614 if let Some(util::paths::WslPath{distro, path}) = wsl_path
9615 && let Ok(ref result) = result
9616 {
9617 result.window
9618 .update(cx, move |multi_workspace, _window, cx| {
9619 struct OpenInWsl;
9620 let workspace = multi_workspace.workspace().clone();
9621 workspace.update(cx, |workspace, cx| {
9622 workspace.show_notification(NotificationId::unique::<OpenInWsl>(), cx, move |cx| {
9623 let display_path = util::markdown::MarkdownInlineCode(&path.to_string_lossy());
9624 let msg = format!("{display_path} is inside a WSL filesystem, some features may not work unless you open it with WSL remote");
9625 cx.new(move |cx| {
9626 MessageNotification::new(msg, cx)
9627 .primary_message("Open in WSL")
9628 .primary_icon(IconName::FolderOpen)
9629 .primary_on_click(move |window, cx| {
9630 window.dispatch_action(Box::new(remote::OpenWslPath {
9631 distro: remote::WslConnectionOptions {
9632 distro_name: distro.clone(),
9633 user: None,
9634 },
9635 paths: vec![path.clone().into()],
9636 }), cx)
9637 })
9638 })
9639 });
9640 });
9641 })
9642 .unwrap();
9643 };
9644 result
9645 })
9646}
9647
9648pub fn open_new(
9649 open_options: OpenOptions,
9650 app_state: Arc<AppState>,
9651 cx: &mut App,
9652 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
9653) -> Task<anyhow::Result<()>> {
9654 let addition = open_options.open_mode;
9655 let task = Workspace::new_local(
9656 Vec::new(),
9657 app_state,
9658 open_options.requesting_window,
9659 open_options.env,
9660 Some(Box::new(init)),
9661 addition,
9662 cx,
9663 );
9664 cx.spawn(async move |cx| {
9665 let OpenResult { window, .. } = task.await?;
9666 window
9667 .update(cx, |_, window, _cx| {
9668 window.activate_window();
9669 })
9670 .ok();
9671 Ok(())
9672 })
9673}
9674
9675pub fn create_and_open_local_file(
9676 path: &'static Path,
9677 window: &mut Window,
9678 cx: &mut Context<Workspace>,
9679 default_content: impl 'static + Send + FnOnce() -> Rope,
9680) -> Task<Result<Box<dyn ItemHandle>>> {
9681 cx.spawn_in(window, async move |workspace, cx| {
9682 let fs = workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
9683 if !fs.is_file(path).await {
9684 fs.create_file(path, Default::default()).await?;
9685 fs.save(path, &default_content(), Default::default())
9686 .await?;
9687 }
9688
9689 workspace
9690 .update_in(cx, |workspace, window, cx| {
9691 workspace.with_local_or_wsl_workspace(window, cx, |workspace, window, cx| {
9692 let path = workspace
9693 .project
9694 .read_with(cx, |project, cx| project.try_windows_path_to_wsl(path, cx));
9695 cx.spawn_in(window, async move |workspace, cx| {
9696 let path = path.await?;
9697
9698 let path = fs.canonicalize(&path).await.unwrap_or(path);
9699
9700 let mut items = workspace
9701 .update_in(cx, |workspace, window, cx| {
9702 workspace.open_paths(
9703 vec![path.to_path_buf()],
9704 OpenOptions {
9705 visible: Some(OpenVisible::None),
9706 ..Default::default()
9707 },
9708 None,
9709 window,
9710 cx,
9711 )
9712 })?
9713 .await;
9714 let item = items.pop().flatten();
9715 item.with_context(|| format!("path {path:?} is not a file"))?
9716 })
9717 })
9718 })?
9719 .await?
9720 .await
9721 })
9722}
9723
9724pub fn open_remote_project_with_new_connection(
9725 window: WindowHandle<MultiWorkspace>,
9726 remote_connection: Arc<dyn RemoteConnection>,
9727 cancel_rx: oneshot::Receiver<()>,
9728 delegate: Arc<dyn RemoteClientDelegate>,
9729 app_state: Arc<AppState>,
9730 paths: Vec<PathBuf>,
9731 cx: &mut App,
9732) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
9733 cx.spawn(async move |cx| {
9734 let (workspace_id, serialized_workspace) =
9735 deserialize_remote_project(remote_connection.connection_options(), paths.clone(), cx)
9736 .await?;
9737
9738 let session = match cx
9739 .update(|cx| {
9740 remote::RemoteClient::new(
9741 ConnectionIdentifier::Workspace(workspace_id.0),
9742 remote_connection,
9743 cancel_rx,
9744 delegate,
9745 cx,
9746 )
9747 })
9748 .await?
9749 {
9750 Some(result) => result,
9751 None => return Ok(Vec::new()),
9752 };
9753
9754 let project = cx.update(|cx| {
9755 project::Project::remote(
9756 session,
9757 app_state.client.clone(),
9758 app_state.node_runtime.clone(),
9759 app_state.user_store.clone(),
9760 app_state.languages.clone(),
9761 app_state.fs.clone(),
9762 true,
9763 cx,
9764 )
9765 });
9766
9767 open_remote_project_inner(
9768 project,
9769 paths,
9770 workspace_id,
9771 serialized_workspace,
9772 app_state,
9773 window,
9774 cx,
9775 )
9776 .await
9777 })
9778}
9779
9780pub fn open_remote_project_with_existing_connection(
9781 connection_options: RemoteConnectionOptions,
9782 project: Entity<Project>,
9783 paths: Vec<PathBuf>,
9784 app_state: Arc<AppState>,
9785 window: WindowHandle<MultiWorkspace>,
9786 cx: &mut AsyncApp,
9787) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
9788 cx.spawn(async move |cx| {
9789 let (workspace_id, serialized_workspace) =
9790 deserialize_remote_project(connection_options.clone(), paths.clone(), cx).await?;
9791
9792 open_remote_project_inner(
9793 project,
9794 paths,
9795 workspace_id,
9796 serialized_workspace,
9797 app_state,
9798 window,
9799 cx,
9800 )
9801 .await
9802 })
9803}
9804
9805async fn open_remote_project_inner(
9806 project: Entity<Project>,
9807 paths: Vec<PathBuf>,
9808 workspace_id: WorkspaceId,
9809 serialized_workspace: Option<SerializedWorkspace>,
9810 app_state: Arc<AppState>,
9811 window: WindowHandle<MultiWorkspace>,
9812 cx: &mut AsyncApp,
9813) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
9814 let db = cx.update(|cx| WorkspaceDb::global(cx));
9815 let toolchains = db.toolchains(workspace_id).await?;
9816 for (toolchain, worktree_path, path) in toolchains {
9817 project
9818 .update(cx, |this, cx| {
9819 let Some(worktree_id) =
9820 this.find_worktree(&worktree_path, cx)
9821 .and_then(|(worktree, rel_path)| {
9822 if rel_path.is_empty() {
9823 Some(worktree.read(cx).id())
9824 } else {
9825 None
9826 }
9827 })
9828 else {
9829 return Task::ready(None);
9830 };
9831
9832 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
9833 })
9834 .await;
9835 }
9836 let mut project_paths_to_open = vec![];
9837 let mut project_path_errors = vec![];
9838
9839 for path in paths {
9840 let result = cx
9841 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))
9842 .await;
9843 match result {
9844 Ok((_, project_path)) => {
9845 project_paths_to_open.push((path.clone(), Some(project_path)));
9846 }
9847 Err(error) => {
9848 project_path_errors.push(error);
9849 }
9850 };
9851 }
9852
9853 if project_paths_to_open.is_empty() {
9854 return Err(project_path_errors.pop().context("no paths given")?);
9855 }
9856
9857 let workspace = window.update(cx, |multi_workspace, window, cx| {
9858 telemetry::event!("SSH Project Opened");
9859
9860 let new_workspace = cx.new(|cx| {
9861 let mut workspace =
9862 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
9863 workspace.update_history(cx);
9864
9865 if let Some(ref serialized) = serialized_workspace {
9866 workspace.centered_layout = serialized.centered_layout;
9867 }
9868
9869 workspace
9870 });
9871
9872 multi_workspace.activate(new_workspace.clone(), window, cx);
9873 new_workspace
9874 })?;
9875
9876 let items = window
9877 .update(cx, |_, window, cx| {
9878 window.activate_window();
9879 workspace.update(cx, |_workspace, cx| {
9880 open_items(serialized_workspace, project_paths_to_open, window, cx)
9881 })
9882 })?
9883 .await?;
9884
9885 workspace.update(cx, |workspace, cx| {
9886 for error in project_path_errors {
9887 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
9888 if let Some(path) = error.error_tag("path") {
9889 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
9890 }
9891 } else {
9892 workspace.show_error(&error, cx)
9893 }
9894 }
9895 });
9896
9897 Ok(items.into_iter().map(|item| item?.ok()).collect())
9898}
9899
9900fn deserialize_remote_project(
9901 connection_options: RemoteConnectionOptions,
9902 paths: Vec<PathBuf>,
9903 cx: &AsyncApp,
9904) -> Task<Result<(WorkspaceId, Option<SerializedWorkspace>)>> {
9905 let db = cx.update(|cx| WorkspaceDb::global(cx));
9906 cx.background_spawn(async move {
9907 let remote_connection_id = db
9908 .get_or_create_remote_connection(connection_options)
9909 .await?;
9910
9911 let serialized_workspace = db.remote_workspace_for_roots(&paths, remote_connection_id);
9912
9913 let workspace_id = if let Some(workspace_id) =
9914 serialized_workspace.as_ref().map(|workspace| workspace.id)
9915 {
9916 workspace_id
9917 } else {
9918 db.next_id().await?
9919 };
9920
9921 Ok((workspace_id, serialized_workspace))
9922 })
9923}
9924
9925pub fn join_in_room_project(
9926 project_id: u64,
9927 follow_user_id: u64,
9928 app_state: Arc<AppState>,
9929 cx: &mut App,
9930) -> Task<Result<()>> {
9931 let windows = cx.windows();
9932 cx.spawn(async move |cx| {
9933 let existing_window_and_workspace: Option<(
9934 WindowHandle<MultiWorkspace>,
9935 Entity<Workspace>,
9936 )> = windows.into_iter().find_map(|window_handle| {
9937 window_handle
9938 .downcast::<MultiWorkspace>()
9939 .and_then(|window_handle| {
9940 window_handle
9941 .update(cx, |multi_workspace, _window, cx| {
9942 for workspace in multi_workspace.workspaces() {
9943 if workspace.read(cx).project().read(cx).remote_id()
9944 == Some(project_id)
9945 {
9946 return Some((window_handle, workspace.clone()));
9947 }
9948 }
9949 None
9950 })
9951 .unwrap_or(None)
9952 })
9953 });
9954
9955 let multi_workspace_window = if let Some((existing_window, target_workspace)) =
9956 existing_window_and_workspace
9957 {
9958 existing_window
9959 .update(cx, |multi_workspace, window, cx| {
9960 multi_workspace.activate(target_workspace, window, cx);
9961 })
9962 .ok();
9963 existing_window
9964 } else {
9965 let active_call = cx.update(|cx| GlobalAnyActiveCall::global(cx).clone());
9966 let project = cx
9967 .update(|cx| {
9968 active_call.0.join_project(
9969 project_id,
9970 app_state.languages.clone(),
9971 app_state.fs.clone(),
9972 cx,
9973 )
9974 })
9975 .await?;
9976
9977 let window_bounds_override = window_bounds_env_override();
9978 cx.update(|cx| {
9979 let mut options = (app_state.build_window_options)(None, cx);
9980 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
9981 cx.open_window(options, |window, cx| {
9982 let workspace = cx.new(|cx| {
9983 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
9984 });
9985 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
9986 })
9987 })?
9988 };
9989
9990 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
9991 cx.activate(true);
9992 window.activate_window();
9993
9994 // We set the active workspace above, so this is the correct workspace.
9995 let workspace = multi_workspace.workspace().clone();
9996 workspace.update(cx, |workspace, cx| {
9997 let follow_peer_id = GlobalAnyActiveCall::try_global(cx)
9998 .and_then(|call| call.0.peer_id_for_user_in_room(follow_user_id, cx))
9999 .or_else(|| {
10000 // If we couldn't follow the given user, follow the host instead.
10001 let collaborator = workspace
10002 .project()
10003 .read(cx)
10004 .collaborators()
10005 .values()
10006 .find(|collaborator| collaborator.is_host)?;
10007 Some(collaborator.peer_id)
10008 });
10009
10010 if let Some(follow_peer_id) = follow_peer_id {
10011 workspace.follow(follow_peer_id, window, cx);
10012 }
10013 });
10014 })?;
10015
10016 anyhow::Ok(())
10017 })
10018}
10019
10020pub fn reload(cx: &mut App) {
10021 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
10022 let mut workspace_windows = cx
10023 .windows()
10024 .into_iter()
10025 .filter_map(|window| window.downcast::<MultiWorkspace>())
10026 .collect::<Vec<_>>();
10027
10028 // If multiple windows have unsaved changes, and need a save prompt,
10029 // prompt in the active window before switching to a different window.
10030 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
10031
10032 let mut prompt = None;
10033 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
10034 prompt = window
10035 .update(cx, |_, window, cx| {
10036 window.prompt(
10037 PromptLevel::Info,
10038 "Are you sure you want to restart?",
10039 None,
10040 &["Restart", "Cancel"],
10041 cx,
10042 )
10043 })
10044 .ok();
10045 }
10046
10047 cx.spawn(async move |cx| {
10048 if let Some(prompt) = prompt {
10049 let answer = prompt.await?;
10050 if answer != 0 {
10051 return anyhow::Ok(());
10052 }
10053 }
10054
10055 // If the user cancels any save prompt, then keep the app open.
10056 for window in workspace_windows {
10057 if let Ok(should_close) = window.update(cx, |multi_workspace, window, cx| {
10058 let workspace = multi_workspace.workspace().clone();
10059 workspace.update(cx, |workspace, cx| {
10060 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
10061 })
10062 }) && !should_close.await?
10063 {
10064 return anyhow::Ok(());
10065 }
10066 }
10067 cx.update(|cx| cx.restart());
10068 anyhow::Ok(())
10069 })
10070 .detach_and_log_err(cx);
10071}
10072
10073fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
10074 let mut parts = value.split(',');
10075 let x: usize = parts.next()?.parse().ok()?;
10076 let y: usize = parts.next()?.parse().ok()?;
10077 Some(point(px(x as f32), px(y as f32)))
10078}
10079
10080fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
10081 let mut parts = value.split(',');
10082 let width: usize = parts.next()?.parse().ok()?;
10083 let height: usize = parts.next()?.parse().ok()?;
10084 Some(size(px(width as f32), px(height as f32)))
10085}
10086
10087/// Add client-side decorations (rounded corners, shadows, resize handling) when
10088/// appropriate.
10089///
10090/// The `border_radius_tiling` parameter allows overriding which corners get
10091/// rounded, independently of the actual window tiling state. This is used
10092/// specifically for the workspace switcher sidebar: when the sidebar is open,
10093/// we want square corners on the left (so the sidebar appears flush with the
10094/// window edge) but we still need the shadow padding for proper visual
10095/// appearance. Unlike actual window tiling, this only affects border radius -
10096/// not padding or shadows.
10097pub fn client_side_decorations(
10098 element: impl IntoElement,
10099 window: &mut Window,
10100 cx: &mut App,
10101 border_radius_tiling: Tiling,
10102) -> Stateful<Div> {
10103 const BORDER_SIZE: Pixels = px(1.0);
10104 let decorations = window.window_decorations();
10105 let tiling = match decorations {
10106 Decorations::Server => Tiling::default(),
10107 Decorations::Client { tiling } => tiling,
10108 };
10109
10110 match decorations {
10111 Decorations::Client { .. } => window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW),
10112 Decorations::Server => window.set_client_inset(px(0.0)),
10113 }
10114
10115 struct GlobalResizeEdge(ResizeEdge);
10116 impl Global for GlobalResizeEdge {}
10117
10118 div()
10119 .id("window-backdrop")
10120 .bg(transparent_black())
10121 .map(|div| match decorations {
10122 Decorations::Server => div,
10123 Decorations::Client { .. } => div
10124 .when(
10125 !(tiling.top
10126 || tiling.right
10127 || border_radius_tiling.top
10128 || border_radius_tiling.right),
10129 |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10130 )
10131 .when(
10132 !(tiling.top
10133 || tiling.left
10134 || border_radius_tiling.top
10135 || border_radius_tiling.left),
10136 |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10137 )
10138 .when(
10139 !(tiling.bottom
10140 || tiling.right
10141 || border_radius_tiling.bottom
10142 || border_radius_tiling.right),
10143 |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10144 )
10145 .when(
10146 !(tiling.bottom
10147 || tiling.left
10148 || border_radius_tiling.bottom
10149 || border_radius_tiling.left),
10150 |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10151 )
10152 .when(!tiling.top, |div| {
10153 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
10154 })
10155 .when(!tiling.bottom, |div| {
10156 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
10157 })
10158 .when(!tiling.left, |div| {
10159 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
10160 })
10161 .when(!tiling.right, |div| {
10162 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
10163 })
10164 .on_mouse_move(move |e, window, cx| {
10165 let size = window.window_bounds().get_bounds().size;
10166 let pos = e.position;
10167
10168 let new_edge =
10169 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
10170
10171 let edge = cx.try_global::<GlobalResizeEdge>();
10172 if new_edge != edge.map(|edge| edge.0) {
10173 window
10174 .window_handle()
10175 .update(cx, |workspace, _, cx| {
10176 cx.notify(workspace.entity_id());
10177 })
10178 .ok();
10179 }
10180 })
10181 .on_mouse_down(MouseButton::Left, move |e, window, _| {
10182 let size = window.window_bounds().get_bounds().size;
10183 let pos = e.position;
10184
10185 let edge = match resize_edge(
10186 pos,
10187 theme::CLIENT_SIDE_DECORATION_SHADOW,
10188 size,
10189 tiling,
10190 ) {
10191 Some(value) => value,
10192 None => return,
10193 };
10194
10195 window.start_window_resize(edge);
10196 }),
10197 })
10198 .size_full()
10199 .child(
10200 div()
10201 .cursor(CursorStyle::Arrow)
10202 .map(|div| match decorations {
10203 Decorations::Server => div,
10204 Decorations::Client { .. } => div
10205 .border_color(cx.theme().colors().border)
10206 .when(
10207 !(tiling.top
10208 || tiling.right
10209 || border_radius_tiling.top
10210 || border_radius_tiling.right),
10211 |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10212 )
10213 .when(
10214 !(tiling.top
10215 || tiling.left
10216 || border_radius_tiling.top
10217 || border_radius_tiling.left),
10218 |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10219 )
10220 .when(
10221 !(tiling.bottom
10222 || tiling.right
10223 || border_radius_tiling.bottom
10224 || border_radius_tiling.right),
10225 |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10226 )
10227 .when(
10228 !(tiling.bottom
10229 || tiling.left
10230 || border_radius_tiling.bottom
10231 || border_radius_tiling.left),
10232 |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10233 )
10234 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
10235 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
10236 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
10237 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
10238 .when(!tiling.is_tiled(), |div| {
10239 div.shadow(vec![gpui::BoxShadow {
10240 color: Hsla {
10241 h: 0.,
10242 s: 0.,
10243 l: 0.,
10244 a: 0.4,
10245 },
10246 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
10247 spread_radius: px(0.),
10248 offset: point(px(0.0), px(0.0)),
10249 }])
10250 }),
10251 })
10252 .on_mouse_move(|_e, _, cx| {
10253 cx.stop_propagation();
10254 })
10255 .size_full()
10256 .child(element),
10257 )
10258 .map(|div| match decorations {
10259 Decorations::Server => div,
10260 Decorations::Client { tiling, .. } => div.child(
10261 canvas(
10262 |_bounds, window, _| {
10263 window.insert_hitbox(
10264 Bounds::new(
10265 point(px(0.0), px(0.0)),
10266 window.window_bounds().get_bounds().size,
10267 ),
10268 HitboxBehavior::Normal,
10269 )
10270 },
10271 move |_bounds, hitbox, window, cx| {
10272 let mouse = window.mouse_position();
10273 let size = window.window_bounds().get_bounds().size;
10274 let Some(edge) =
10275 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
10276 else {
10277 return;
10278 };
10279 cx.set_global(GlobalResizeEdge(edge));
10280 window.set_cursor_style(
10281 match edge {
10282 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
10283 ResizeEdge::Left | ResizeEdge::Right => {
10284 CursorStyle::ResizeLeftRight
10285 }
10286 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
10287 CursorStyle::ResizeUpLeftDownRight
10288 }
10289 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
10290 CursorStyle::ResizeUpRightDownLeft
10291 }
10292 },
10293 &hitbox,
10294 );
10295 },
10296 )
10297 .size_full()
10298 .absolute(),
10299 ),
10300 })
10301}
10302
10303fn resize_edge(
10304 pos: Point<Pixels>,
10305 shadow_size: Pixels,
10306 window_size: Size<Pixels>,
10307 tiling: Tiling,
10308) -> Option<ResizeEdge> {
10309 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
10310 if bounds.contains(&pos) {
10311 return None;
10312 }
10313
10314 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
10315 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
10316 if !tiling.top && top_left_bounds.contains(&pos) {
10317 return Some(ResizeEdge::TopLeft);
10318 }
10319
10320 let top_right_bounds = Bounds::new(
10321 Point::new(window_size.width - corner_size.width, px(0.)),
10322 corner_size,
10323 );
10324 if !tiling.top && top_right_bounds.contains(&pos) {
10325 return Some(ResizeEdge::TopRight);
10326 }
10327
10328 let bottom_left_bounds = Bounds::new(
10329 Point::new(px(0.), window_size.height - corner_size.height),
10330 corner_size,
10331 );
10332 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
10333 return Some(ResizeEdge::BottomLeft);
10334 }
10335
10336 let bottom_right_bounds = Bounds::new(
10337 Point::new(
10338 window_size.width - corner_size.width,
10339 window_size.height - corner_size.height,
10340 ),
10341 corner_size,
10342 );
10343 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
10344 return Some(ResizeEdge::BottomRight);
10345 }
10346
10347 if !tiling.top && pos.y < shadow_size {
10348 Some(ResizeEdge::Top)
10349 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
10350 Some(ResizeEdge::Bottom)
10351 } else if !tiling.left && pos.x < shadow_size {
10352 Some(ResizeEdge::Left)
10353 } else if !tiling.right && pos.x > window_size.width - shadow_size {
10354 Some(ResizeEdge::Right)
10355 } else {
10356 None
10357 }
10358}
10359
10360fn join_pane_into_active(
10361 active_pane: &Entity<Pane>,
10362 pane: &Entity<Pane>,
10363 window: &mut Window,
10364 cx: &mut App,
10365) {
10366 if pane == active_pane {
10367 } else if pane.read(cx).items_len() == 0 {
10368 pane.update(cx, |_, cx| {
10369 cx.emit(pane::Event::Remove {
10370 focus_on_pane: None,
10371 });
10372 })
10373 } else {
10374 move_all_items(pane, active_pane, window, cx);
10375 }
10376}
10377
10378fn move_all_items(
10379 from_pane: &Entity<Pane>,
10380 to_pane: &Entity<Pane>,
10381 window: &mut Window,
10382 cx: &mut App,
10383) {
10384 let destination_is_different = from_pane != to_pane;
10385 let mut moved_items = 0;
10386 for (item_ix, item_handle) in from_pane
10387 .read(cx)
10388 .items()
10389 .enumerate()
10390 .map(|(ix, item)| (ix, item.clone()))
10391 .collect::<Vec<_>>()
10392 {
10393 let ix = item_ix - moved_items;
10394 if destination_is_different {
10395 // Close item from previous pane
10396 from_pane.update(cx, |source, cx| {
10397 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
10398 });
10399 moved_items += 1;
10400 }
10401
10402 // This automatically removes duplicate items in the pane
10403 to_pane.update(cx, |destination, cx| {
10404 destination.add_item(item_handle, true, true, None, window, cx);
10405 window.focus(&destination.focus_handle(cx), cx)
10406 });
10407 }
10408}
10409
10410pub fn move_item(
10411 source: &Entity<Pane>,
10412 destination: &Entity<Pane>,
10413 item_id_to_move: EntityId,
10414 destination_index: usize,
10415 activate: bool,
10416 window: &mut Window,
10417 cx: &mut App,
10418) {
10419 let Some((item_ix, item_handle)) = source
10420 .read(cx)
10421 .items()
10422 .enumerate()
10423 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
10424 .map(|(ix, item)| (ix, item.clone()))
10425 else {
10426 // Tab was closed during drag
10427 return;
10428 };
10429
10430 if source != destination {
10431 // Close item from previous pane
10432 source.update(cx, |source, cx| {
10433 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
10434 });
10435 }
10436
10437 // This automatically removes duplicate items in the pane
10438 destination.update(cx, |destination, cx| {
10439 destination.add_item_inner(
10440 item_handle,
10441 activate,
10442 activate,
10443 activate,
10444 Some(destination_index),
10445 window,
10446 cx,
10447 );
10448 if activate {
10449 window.focus(&destination.focus_handle(cx), cx)
10450 }
10451 });
10452}
10453
10454pub fn move_active_item(
10455 source: &Entity<Pane>,
10456 destination: &Entity<Pane>,
10457 focus_destination: bool,
10458 close_if_empty: bool,
10459 window: &mut Window,
10460 cx: &mut App,
10461) {
10462 if source == destination {
10463 return;
10464 }
10465 let Some(active_item) = source.read(cx).active_item() else {
10466 return;
10467 };
10468 source.update(cx, |source_pane, cx| {
10469 let item_id = active_item.item_id();
10470 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
10471 destination.update(cx, |target_pane, cx| {
10472 target_pane.add_item(
10473 active_item,
10474 focus_destination,
10475 focus_destination,
10476 Some(target_pane.items_len()),
10477 window,
10478 cx,
10479 );
10480 });
10481 });
10482}
10483
10484pub fn clone_active_item(
10485 workspace_id: Option<WorkspaceId>,
10486 source: &Entity<Pane>,
10487 destination: &Entity<Pane>,
10488 focus_destination: bool,
10489 window: &mut Window,
10490 cx: &mut App,
10491) {
10492 if source == destination {
10493 return;
10494 }
10495 let Some(active_item) = source.read(cx).active_item() else {
10496 return;
10497 };
10498 if !active_item.can_split(cx) {
10499 return;
10500 }
10501 let destination = destination.downgrade();
10502 let task = active_item.clone_on_split(workspace_id, window, cx);
10503 window
10504 .spawn(cx, async move |cx| {
10505 let Some(clone) = task.await else {
10506 return;
10507 };
10508 destination
10509 .update_in(cx, |target_pane, window, cx| {
10510 target_pane.add_item(
10511 clone,
10512 focus_destination,
10513 focus_destination,
10514 Some(target_pane.items_len()),
10515 window,
10516 cx,
10517 );
10518 })
10519 .log_err();
10520 })
10521 .detach();
10522}
10523
10524#[derive(Debug)]
10525pub struct WorkspacePosition {
10526 pub window_bounds: Option<WindowBounds>,
10527 pub display: Option<Uuid>,
10528 pub centered_layout: bool,
10529}
10530
10531pub fn remote_workspace_position_from_db(
10532 connection_options: RemoteConnectionOptions,
10533 paths_to_open: &[PathBuf],
10534 cx: &App,
10535) -> Task<Result<WorkspacePosition>> {
10536 let paths = paths_to_open.to_vec();
10537 let db = WorkspaceDb::global(cx);
10538 let kvp = db::kvp::KeyValueStore::global(cx);
10539
10540 cx.background_spawn(async move {
10541 let remote_connection_id = db
10542 .get_or_create_remote_connection(connection_options)
10543 .await
10544 .context("fetching serialized ssh project")?;
10545 let serialized_workspace = db.remote_workspace_for_roots(&paths, remote_connection_id);
10546
10547 let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
10548 (Some(WindowBounds::Windowed(bounds)), None)
10549 } else {
10550 let restorable_bounds = serialized_workspace
10551 .as_ref()
10552 .and_then(|workspace| {
10553 Some((workspace.display?, workspace.window_bounds.map(|b| b.0)?))
10554 })
10555 .or_else(|| persistence::read_default_window_bounds(&kvp));
10556
10557 if let Some((serialized_display, serialized_bounds)) = restorable_bounds {
10558 (Some(serialized_bounds), Some(serialized_display))
10559 } else {
10560 (None, None)
10561 }
10562 };
10563
10564 let centered_layout = serialized_workspace
10565 .as_ref()
10566 .map(|w| w.centered_layout)
10567 .unwrap_or(false);
10568
10569 Ok(WorkspacePosition {
10570 window_bounds,
10571 display,
10572 centered_layout,
10573 })
10574 })
10575}
10576
10577pub fn with_active_or_new_workspace(
10578 cx: &mut App,
10579 f: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + 'static,
10580) {
10581 match cx
10582 .active_window()
10583 .and_then(|w| w.downcast::<MultiWorkspace>())
10584 {
10585 Some(multi_workspace) => {
10586 cx.defer(move |cx| {
10587 multi_workspace
10588 .update(cx, |multi_workspace, window, cx| {
10589 let workspace = multi_workspace.workspace().clone();
10590 workspace.update(cx, |workspace, cx| f(workspace, window, cx));
10591 })
10592 .log_err();
10593 });
10594 }
10595 None => {
10596 let app_state = AppState::global(cx);
10597 open_new(
10598 OpenOptions::default(),
10599 app_state,
10600 cx,
10601 move |workspace, window, cx| f(workspace, window, cx),
10602 )
10603 .detach_and_log_err(cx);
10604 }
10605 }
10606}
10607
10608/// Reads a panel's pixel size from its legacy KVP format and deletes the legacy
10609/// key. This migration path only runs once per panel per workspace.
10610fn load_legacy_panel_size(
10611 panel_key: &str,
10612 dock_position: DockPosition,
10613 workspace: &Workspace,
10614 cx: &mut App,
10615) -> Option<Pixels> {
10616 #[derive(Deserialize)]
10617 struct LegacyPanelState {
10618 #[serde(default)]
10619 width: Option<Pixels>,
10620 #[serde(default)]
10621 height: Option<Pixels>,
10622 }
10623
10624 let workspace_id = workspace
10625 .database_id()
10626 .map(|id| i64::from(id).to_string())
10627 .or_else(|| workspace.session_id())?;
10628
10629 let legacy_key = match panel_key {
10630 "ProjectPanel" => {
10631 format!("{}-{:?}", "ProjectPanel", workspace_id)
10632 }
10633 "OutlinePanel" => {
10634 format!("{}-{:?}", "OutlinePanel", workspace_id)
10635 }
10636 "GitPanel" => {
10637 format!("{}-{:?}", "GitPanel", workspace_id)
10638 }
10639 "TerminalPanel" => {
10640 format!("{:?}-{:?}", "TerminalPanel", workspace_id)
10641 }
10642 _ => return None,
10643 };
10644
10645 let kvp = db::kvp::KeyValueStore::global(cx);
10646 let json = kvp.read_kvp(&legacy_key).log_err().flatten()?;
10647 let state = serde_json::from_str::<LegacyPanelState>(&json).log_err()?;
10648 let size = match dock_position {
10649 DockPosition::Bottom => state.height,
10650 DockPosition::Left | DockPosition::Right => state.width,
10651 }?;
10652
10653 cx.background_spawn(async move { kvp.delete_kvp(legacy_key).await })
10654 .detach_and_log_err(cx);
10655
10656 Some(size)
10657}
10658
10659#[cfg(test)]
10660mod tests {
10661 use std::{cell::RefCell, rc::Rc, sync::Arc, time::Duration};
10662
10663 use super::*;
10664 use crate::{
10665 dock::{PanelEvent, test::TestPanel},
10666 item::{
10667 ItemBufferKind, ItemEvent,
10668 test::{TestItem, TestProjectItem},
10669 },
10670 };
10671 use fs::FakeFs;
10672 use gpui::{
10673 DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
10674 UpdateGlobal, VisualTestContext, px,
10675 };
10676 use project::{Project, ProjectEntryId};
10677 use serde_json::json;
10678 use settings::SettingsStore;
10679 use util::path;
10680 use util::rel_path::rel_path;
10681
10682 #[gpui::test]
10683 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
10684 init_test(cx);
10685
10686 let fs = FakeFs::new(cx.executor());
10687 let project = Project::test(fs, [], cx).await;
10688 let (workspace, cx) =
10689 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10690
10691 // Adding an item with no ambiguity renders the tab without detail.
10692 let item1 = cx.new(|cx| {
10693 let mut item = TestItem::new(cx);
10694 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
10695 item
10696 });
10697 workspace.update_in(cx, |workspace, window, cx| {
10698 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
10699 });
10700 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
10701
10702 // Adding an item that creates ambiguity increases the level of detail on
10703 // both tabs.
10704 let item2 = cx.new_window_entity(|_window, cx| {
10705 let mut item = TestItem::new(cx);
10706 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
10707 item
10708 });
10709 workspace.update_in(cx, |workspace, window, cx| {
10710 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10711 });
10712 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10713 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10714
10715 // Adding an item that creates ambiguity increases the level of detail only
10716 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
10717 // we stop at the highest detail available.
10718 let item3 = cx.new(|cx| {
10719 let mut item = TestItem::new(cx);
10720 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
10721 item
10722 });
10723 workspace.update_in(cx, |workspace, window, cx| {
10724 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
10725 });
10726 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10727 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
10728 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
10729 }
10730
10731 #[gpui::test]
10732 async fn test_tracking_active_path(cx: &mut TestAppContext) {
10733 init_test(cx);
10734
10735 let fs = FakeFs::new(cx.executor());
10736 fs.insert_tree(
10737 "/root1",
10738 json!({
10739 "one.txt": "",
10740 "two.txt": "",
10741 }),
10742 )
10743 .await;
10744 fs.insert_tree(
10745 "/root2",
10746 json!({
10747 "three.txt": "",
10748 }),
10749 )
10750 .await;
10751
10752 let project = Project::test(fs, ["root1".as_ref()], cx).await;
10753 let (workspace, cx) =
10754 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10755 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10756 let worktree_id = project.update(cx, |project, cx| {
10757 project.worktrees(cx).next().unwrap().read(cx).id()
10758 });
10759
10760 let item1 = cx.new(|cx| {
10761 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
10762 });
10763 let item2 = cx.new(|cx| {
10764 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
10765 });
10766
10767 // Add an item to an empty pane
10768 workspace.update_in(cx, |workspace, window, cx| {
10769 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
10770 });
10771 project.update(cx, |project, cx| {
10772 assert_eq!(
10773 project.active_entry(),
10774 project
10775 .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
10776 .map(|e| e.id)
10777 );
10778 });
10779 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
10780
10781 // Add a second item to a non-empty pane
10782 workspace.update_in(cx, |workspace, window, cx| {
10783 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
10784 });
10785 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
10786 project.update(cx, |project, cx| {
10787 assert_eq!(
10788 project.active_entry(),
10789 project
10790 .entry_for_path(&(worktree_id, rel_path("two.txt")).into(), cx)
10791 .map(|e| e.id)
10792 );
10793 });
10794
10795 // Close the active item
10796 pane.update_in(cx, |pane, window, cx| {
10797 pane.close_active_item(&Default::default(), window, cx)
10798 })
10799 .await
10800 .unwrap();
10801 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
10802 project.update(cx, |project, cx| {
10803 assert_eq!(
10804 project.active_entry(),
10805 project
10806 .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
10807 .map(|e| e.id)
10808 );
10809 });
10810
10811 // Add a project folder
10812 project
10813 .update(cx, |project, cx| {
10814 project.find_or_create_worktree("root2", true, cx)
10815 })
10816 .await
10817 .unwrap();
10818 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
10819
10820 // Remove a project folder
10821 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
10822 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
10823 }
10824
10825 #[gpui::test]
10826 async fn test_close_window(cx: &mut TestAppContext) {
10827 init_test(cx);
10828
10829 let fs = FakeFs::new(cx.executor());
10830 fs.insert_tree("/root", json!({ "one": "" })).await;
10831
10832 let project = Project::test(fs, ["root".as_ref()], cx).await;
10833 let (workspace, cx) =
10834 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10835
10836 // When there are no dirty items, there's nothing to do.
10837 let item1 = cx.new(TestItem::new);
10838 workspace.update_in(cx, |w, window, cx| {
10839 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
10840 });
10841 let task = workspace.update_in(cx, |w, window, cx| {
10842 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
10843 });
10844 assert!(task.await.unwrap());
10845
10846 // When there are dirty untitled items, prompt to save each one. If the user
10847 // cancels any prompt, then abort.
10848 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
10849 let item3 = cx.new(|cx| {
10850 TestItem::new(cx)
10851 .with_dirty(true)
10852 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10853 });
10854 workspace.update_in(cx, |w, window, cx| {
10855 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10856 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
10857 });
10858 let task = workspace.update_in(cx, |w, window, cx| {
10859 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
10860 });
10861 cx.executor().run_until_parked();
10862 cx.simulate_prompt_answer("Cancel"); // cancel save all
10863 cx.executor().run_until_parked();
10864 assert!(!cx.has_pending_prompt());
10865 assert!(!task.await.unwrap());
10866 }
10867
10868 #[gpui::test]
10869 async fn test_multi_workspace_close_window_multiple_workspaces_cancel(cx: &mut TestAppContext) {
10870 init_test(cx);
10871
10872 let fs = FakeFs::new(cx.executor());
10873 fs.insert_tree("/root", json!({ "one": "" })).await;
10874
10875 let project_a = Project::test(fs.clone(), ["root".as_ref()], cx).await;
10876 let project_b = Project::test(fs, ["root".as_ref()], cx).await;
10877 let multi_workspace_handle =
10878 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
10879 cx.run_until_parked();
10880
10881 multi_workspace_handle
10882 .update(cx, |mw, _window, cx| {
10883 mw.open_sidebar(cx);
10884 })
10885 .unwrap();
10886
10887 let workspace_a = multi_workspace_handle
10888 .read_with(cx, |mw, _| mw.workspace().clone())
10889 .unwrap();
10890
10891 let workspace_b = multi_workspace_handle
10892 .update(cx, |mw, window, cx| {
10893 mw.test_add_workspace(project_b, window, cx)
10894 })
10895 .unwrap();
10896
10897 // Activate workspace A
10898 multi_workspace_handle
10899 .update(cx, |mw, window, cx| {
10900 let workspace = mw.workspaces().next().unwrap().clone();
10901 mw.activate(workspace, window, cx);
10902 })
10903 .unwrap();
10904
10905 let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
10906
10907 // Workspace A has a clean item
10908 let item_a = cx.new(TestItem::new);
10909 workspace_a.update_in(cx, |w, window, cx| {
10910 w.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
10911 });
10912
10913 // Workspace B has a dirty item
10914 let item_b = cx.new(|cx| TestItem::new(cx).with_dirty(true));
10915 workspace_b.update_in(cx, |w, window, cx| {
10916 w.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx)
10917 });
10918
10919 // Verify workspace A is active
10920 multi_workspace_handle
10921 .read_with(cx, |mw, _| {
10922 assert_eq!(mw.workspace(), &workspace_a);
10923 })
10924 .unwrap();
10925
10926 // Dispatch CloseWindow — workspace A will pass, workspace B will prompt
10927 multi_workspace_handle
10928 .update(cx, |mw, window, cx| {
10929 mw.close_window(&CloseWindow, window, cx);
10930 })
10931 .unwrap();
10932 cx.run_until_parked();
10933
10934 // Workspace B should now be active since it has dirty items that need attention
10935 multi_workspace_handle
10936 .read_with(cx, |mw, _| {
10937 assert_eq!(
10938 mw.workspace(),
10939 &workspace_b,
10940 "workspace B should be activated when it prompts"
10941 );
10942 })
10943 .unwrap();
10944
10945 // User cancels the save prompt from workspace B
10946 cx.simulate_prompt_answer("Cancel");
10947 cx.run_until_parked();
10948
10949 // Window should still exist because workspace B's close was cancelled
10950 assert!(
10951 multi_workspace_handle.update(cx, |_, _, _| ()).is_ok(),
10952 "window should still exist after cancelling one workspace's close"
10953 );
10954 }
10955
10956 #[gpui::test]
10957 async fn test_remove_workspace_prompts_for_unsaved_changes(cx: &mut TestAppContext) {
10958 init_test(cx);
10959
10960 let fs = FakeFs::new(cx.executor());
10961 fs.insert_tree("/root", json!({ "one": "" })).await;
10962
10963 let project_a = Project::test(fs.clone(), ["root".as_ref()], cx).await;
10964 let project_b = Project::test(fs.clone(), ["root".as_ref()], cx).await;
10965 let multi_workspace_handle =
10966 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
10967 cx.run_until_parked();
10968
10969 multi_workspace_handle
10970 .update(cx, |mw, _window, cx| mw.open_sidebar(cx))
10971 .unwrap();
10972
10973 let workspace_a = multi_workspace_handle
10974 .read_with(cx, |mw, _| mw.workspace().clone())
10975 .unwrap();
10976
10977 let workspace_b = multi_workspace_handle
10978 .update(cx, |mw, window, cx| {
10979 mw.test_add_workspace(project_b, window, cx)
10980 })
10981 .unwrap();
10982
10983 // Activate workspace A.
10984 multi_workspace_handle
10985 .update(cx, |mw, window, cx| {
10986 mw.activate(workspace_a.clone(), window, cx);
10987 })
10988 .unwrap();
10989
10990 let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
10991
10992 // Workspace B has a dirty item.
10993 let item_b = cx.new(|cx| TestItem::new(cx).with_dirty(true));
10994 workspace_b.update_in(cx, |w, window, cx| {
10995 w.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx)
10996 });
10997
10998 // Try to remove workspace B. It should prompt because of the dirty item.
10999 let remove_task = multi_workspace_handle
11000 .update(cx, |mw, window, cx| {
11001 mw.remove([workspace_b.clone()], |_, _, _| unreachable!(), window, cx)
11002 })
11003 .unwrap();
11004 cx.run_until_parked();
11005
11006 // The prompt should have activated workspace B.
11007 multi_workspace_handle
11008 .read_with(cx, |mw, _| {
11009 assert_eq!(
11010 mw.workspace(),
11011 &workspace_b,
11012 "workspace B should be active while prompting"
11013 );
11014 })
11015 .unwrap();
11016
11017 // Cancel the prompt — user stays on workspace B.
11018 cx.simulate_prompt_answer("Cancel");
11019 cx.run_until_parked();
11020 let removed = remove_task.await.unwrap();
11021 assert!(!removed, "removal should have been cancelled");
11022
11023 multi_workspace_handle
11024 .read_with(cx, |mw, _| {
11025 assert_eq!(
11026 mw.workspace(),
11027 &workspace_b,
11028 "user should stay on workspace B after cancelling"
11029 );
11030 assert_eq!(mw.workspaces().count(), 2, "both workspaces should remain");
11031 })
11032 .unwrap();
11033
11034 // Try again. This time accept the prompt.
11035 let remove_task = multi_workspace_handle
11036 .update(cx, |mw, window, cx| {
11037 // First switch back to A.
11038 mw.activate(workspace_a.clone(), window, cx);
11039 mw.remove([workspace_b.clone()], |_, _, _| unreachable!(), window, cx)
11040 })
11041 .unwrap();
11042 cx.run_until_parked();
11043
11044 // Accept the save prompt.
11045 cx.simulate_prompt_answer("Don't Save");
11046 cx.run_until_parked();
11047 let removed = remove_task.await.unwrap();
11048 assert!(removed, "removal should have succeeded");
11049
11050 // Should be back on workspace A, and B should be gone.
11051 multi_workspace_handle
11052 .read_with(cx, |mw, _| {
11053 assert_eq!(
11054 mw.workspace(),
11055 &workspace_a,
11056 "should be back on workspace A after removing B"
11057 );
11058 assert_eq!(mw.workspaces().count(), 1, "only workspace A should remain");
11059 })
11060 .unwrap();
11061 }
11062
11063 #[gpui::test]
11064 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
11065 init_test(cx);
11066
11067 // Register TestItem as a serializable item
11068 cx.update(|cx| {
11069 register_serializable_item::<TestItem>(cx);
11070 });
11071
11072 let fs = FakeFs::new(cx.executor());
11073 fs.insert_tree("/root", json!({ "one": "" })).await;
11074
11075 let project = Project::test(fs, ["root".as_ref()], cx).await;
11076 let (workspace, cx) =
11077 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
11078
11079 // When there are dirty untitled items, but they can serialize, then there is no prompt.
11080 let item1 = cx.new(|cx| {
11081 TestItem::new(cx)
11082 .with_dirty(true)
11083 .with_serialize(|| Some(Task::ready(Ok(()))))
11084 });
11085 let item2 = cx.new(|cx| {
11086 TestItem::new(cx)
11087 .with_dirty(true)
11088 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11089 .with_serialize(|| Some(Task::ready(Ok(()))))
11090 });
11091 workspace.update_in(cx, |w, window, cx| {
11092 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
11093 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
11094 });
11095 let task = workspace.update_in(cx, |w, window, cx| {
11096 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
11097 });
11098 assert!(task.await.unwrap());
11099 }
11100
11101 #[gpui::test]
11102 async fn test_close_pane_items(cx: &mut TestAppContext) {
11103 init_test(cx);
11104
11105 let fs = FakeFs::new(cx.executor());
11106
11107 let project = Project::test(fs, None, cx).await;
11108 let (workspace, cx) =
11109 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11110
11111 let item1 = cx.new(|cx| {
11112 TestItem::new(cx)
11113 .with_dirty(true)
11114 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
11115 });
11116 let item2 = cx.new(|cx| {
11117 TestItem::new(cx)
11118 .with_dirty(true)
11119 .with_conflict(true)
11120 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
11121 });
11122 let item3 = cx.new(|cx| {
11123 TestItem::new(cx)
11124 .with_dirty(true)
11125 .with_conflict(true)
11126 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
11127 });
11128 let item4 = cx.new(|cx| {
11129 TestItem::new(cx).with_dirty(true).with_project_items(&[{
11130 let project_item = TestProjectItem::new_untitled(cx);
11131 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11132 project_item
11133 }])
11134 });
11135 let pane = workspace.update_in(cx, |workspace, window, cx| {
11136 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
11137 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
11138 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
11139 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
11140 workspace.active_pane().clone()
11141 });
11142
11143 let close_items = pane.update_in(cx, |pane, window, cx| {
11144 pane.activate_item(1, true, true, window, cx);
11145 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
11146 let item1_id = item1.item_id();
11147 let item3_id = item3.item_id();
11148 let item4_id = item4.item_id();
11149 pane.close_items(window, cx, SaveIntent::Close, &move |id| {
11150 [item1_id, item3_id, item4_id].contains(&id)
11151 })
11152 });
11153 cx.executor().run_until_parked();
11154
11155 assert!(cx.has_pending_prompt());
11156 cx.simulate_prompt_answer("Save all");
11157
11158 cx.executor().run_until_parked();
11159
11160 // Item 1 is saved. There's a prompt to save item 3.
11161 pane.update(cx, |pane, cx| {
11162 assert_eq!(item1.read(cx).save_count, 1);
11163 assert_eq!(item1.read(cx).save_as_count, 0);
11164 assert_eq!(item1.read(cx).reload_count, 0);
11165 assert_eq!(pane.items_len(), 3);
11166 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
11167 });
11168 assert!(cx.has_pending_prompt());
11169
11170 // Cancel saving item 3.
11171 cx.simulate_prompt_answer("Discard");
11172 cx.executor().run_until_parked();
11173
11174 // Item 3 is reloaded. There's a prompt to save item 4.
11175 pane.update(cx, |pane, cx| {
11176 assert_eq!(item3.read(cx).save_count, 0);
11177 assert_eq!(item3.read(cx).save_as_count, 0);
11178 assert_eq!(item3.read(cx).reload_count, 1);
11179 assert_eq!(pane.items_len(), 2);
11180 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
11181 });
11182
11183 // There's a prompt for a path for item 4.
11184 cx.simulate_new_path_selection(|_| Some(Default::default()));
11185 close_items.await.unwrap();
11186
11187 // The requested items are closed.
11188 pane.update(cx, |pane, cx| {
11189 assert_eq!(item4.read(cx).save_count, 0);
11190 assert_eq!(item4.read(cx).save_as_count, 1);
11191 assert_eq!(item4.read(cx).reload_count, 0);
11192 assert_eq!(pane.items_len(), 1);
11193 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
11194 });
11195 }
11196
11197 #[gpui::test]
11198 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
11199 init_test(cx);
11200
11201 let fs = FakeFs::new(cx.executor());
11202 let project = Project::test(fs, [], cx).await;
11203 let (workspace, cx) =
11204 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11205
11206 // Create several workspace items with single project entries, and two
11207 // workspace items with multiple project entries.
11208 let single_entry_items = (0..=4)
11209 .map(|project_entry_id| {
11210 cx.new(|cx| {
11211 TestItem::new(cx)
11212 .with_dirty(true)
11213 .with_project_items(&[dirty_project_item(
11214 project_entry_id,
11215 &format!("{project_entry_id}.txt"),
11216 cx,
11217 )])
11218 })
11219 })
11220 .collect::<Vec<_>>();
11221 let item_2_3 = cx.new(|cx| {
11222 TestItem::new(cx)
11223 .with_dirty(true)
11224 .with_buffer_kind(ItemBufferKind::Multibuffer)
11225 .with_project_items(&[
11226 single_entry_items[2].read(cx).project_items[0].clone(),
11227 single_entry_items[3].read(cx).project_items[0].clone(),
11228 ])
11229 });
11230 let item_3_4 = cx.new(|cx| {
11231 TestItem::new(cx)
11232 .with_dirty(true)
11233 .with_buffer_kind(ItemBufferKind::Multibuffer)
11234 .with_project_items(&[
11235 single_entry_items[3].read(cx).project_items[0].clone(),
11236 single_entry_items[4].read(cx).project_items[0].clone(),
11237 ])
11238 });
11239
11240 // Create two panes that contain the following project entries:
11241 // left pane:
11242 // multi-entry items: (2, 3)
11243 // single-entry items: 0, 2, 3, 4
11244 // right pane:
11245 // single-entry items: 4, 1
11246 // multi-entry items: (3, 4)
11247 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
11248 let left_pane = workspace.active_pane().clone();
11249 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
11250 workspace.add_item_to_active_pane(
11251 single_entry_items[0].boxed_clone(),
11252 None,
11253 true,
11254 window,
11255 cx,
11256 );
11257 workspace.add_item_to_active_pane(
11258 single_entry_items[2].boxed_clone(),
11259 None,
11260 true,
11261 window,
11262 cx,
11263 );
11264 workspace.add_item_to_active_pane(
11265 single_entry_items[3].boxed_clone(),
11266 None,
11267 true,
11268 window,
11269 cx,
11270 );
11271 workspace.add_item_to_active_pane(
11272 single_entry_items[4].boxed_clone(),
11273 None,
11274 true,
11275 window,
11276 cx,
11277 );
11278
11279 let right_pane =
11280 workspace.split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx);
11281
11282 let boxed_clone = single_entry_items[1].boxed_clone();
11283 let right_pane = window.spawn(cx, async move |cx| {
11284 right_pane.await.inspect(|right_pane| {
11285 right_pane
11286 .update_in(cx, |pane, window, cx| {
11287 pane.add_item(boxed_clone, true, true, None, window, cx);
11288 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
11289 })
11290 .unwrap();
11291 })
11292 });
11293
11294 (left_pane, right_pane)
11295 });
11296 let right_pane = right_pane.await.unwrap();
11297 cx.focus(&right_pane);
11298
11299 let close = right_pane.update_in(cx, |pane, window, cx| {
11300 pane.close_all_items(&CloseAllItems::default(), window, cx)
11301 .unwrap()
11302 });
11303 cx.executor().run_until_parked();
11304
11305 let msg = cx.pending_prompt().unwrap().0;
11306 assert!(msg.contains("1.txt"));
11307 assert!(!msg.contains("2.txt"));
11308 assert!(!msg.contains("3.txt"));
11309 assert!(!msg.contains("4.txt"));
11310
11311 // With best-effort close, cancelling item 1 keeps it open but items 4
11312 // and (3,4) still close since their entries exist in left pane.
11313 cx.simulate_prompt_answer("Cancel");
11314 close.await;
11315
11316 right_pane.read_with(cx, |pane, _| {
11317 assert_eq!(pane.items_len(), 1);
11318 });
11319
11320 // Remove item 3 from left pane, making (2,3) the only item with entry 3.
11321 left_pane
11322 .update_in(cx, |left_pane, window, cx| {
11323 left_pane.close_item_by_id(
11324 single_entry_items[3].entity_id(),
11325 SaveIntent::Skip,
11326 window,
11327 cx,
11328 )
11329 })
11330 .await
11331 .unwrap();
11332
11333 let close = left_pane.update_in(cx, |pane, window, cx| {
11334 pane.close_all_items(&CloseAllItems::default(), window, cx)
11335 .unwrap()
11336 });
11337 cx.executor().run_until_parked();
11338
11339 let details = cx.pending_prompt().unwrap().1;
11340 assert!(details.contains("0.txt"));
11341 assert!(details.contains("3.txt"));
11342 assert!(details.contains("4.txt"));
11343 // Ideally 2.txt wouldn't appear since entry 2 still exists in item 2.
11344 // But we can only save whole items, so saving (2,3) for entry 3 includes 2.
11345 // assert!(!details.contains("2.txt"));
11346
11347 cx.simulate_prompt_answer("Save all");
11348 cx.executor().run_until_parked();
11349 close.await;
11350
11351 left_pane.read_with(cx, |pane, _| {
11352 assert_eq!(pane.items_len(), 0);
11353 });
11354 }
11355
11356 #[gpui::test]
11357 async fn test_autosave(cx: &mut gpui::TestAppContext) {
11358 init_test(cx);
11359
11360 let fs = FakeFs::new(cx.executor());
11361 let project = Project::test(fs, [], cx).await;
11362 let (workspace, cx) =
11363 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11364 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11365
11366 let item = cx.new(|cx| {
11367 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11368 });
11369 let item_id = item.entity_id();
11370 workspace.update_in(cx, |workspace, window, cx| {
11371 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11372 });
11373
11374 // Autosave on window change.
11375 item.update(cx, |item, cx| {
11376 SettingsStore::update_global(cx, |settings, cx| {
11377 settings.update_user_settings(cx, |settings| {
11378 settings.workspace.autosave = Some(AutosaveSetting::OnWindowChange);
11379 })
11380 });
11381 item.is_dirty = true;
11382 });
11383
11384 // Deactivating the window saves the file.
11385 cx.deactivate_window();
11386 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11387
11388 // Re-activating the window doesn't save the file.
11389 cx.update(|window, _| window.activate_window());
11390 cx.executor().run_until_parked();
11391 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11392
11393 // Autosave on focus change.
11394 item.update_in(cx, |item, window, cx| {
11395 cx.focus_self(window);
11396 SettingsStore::update_global(cx, |settings, cx| {
11397 settings.update_user_settings(cx, |settings| {
11398 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11399 })
11400 });
11401 item.is_dirty = true;
11402 });
11403 // Blurring the item saves the file.
11404 item.update_in(cx, |_, window, _| window.blur());
11405 cx.executor().run_until_parked();
11406 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
11407
11408 // Deactivating the window still saves the file.
11409 item.update_in(cx, |item, window, cx| {
11410 cx.focus_self(window);
11411 item.is_dirty = true;
11412 });
11413 cx.deactivate_window();
11414 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
11415
11416 // Autosave after delay.
11417 item.update(cx, |item, cx| {
11418 SettingsStore::update_global(cx, |settings, cx| {
11419 settings.update_user_settings(cx, |settings| {
11420 settings.workspace.autosave = Some(AutosaveSetting::AfterDelay {
11421 milliseconds: 500.into(),
11422 });
11423 })
11424 });
11425 item.is_dirty = true;
11426 cx.emit(ItemEvent::Edit);
11427 });
11428
11429 // Delay hasn't fully expired, so the file is still dirty and unsaved.
11430 cx.executor().advance_clock(Duration::from_millis(250));
11431 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
11432
11433 // After delay expires, the file is saved.
11434 cx.executor().advance_clock(Duration::from_millis(250));
11435 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
11436
11437 // Autosave after delay, should save earlier than delay if tab is closed
11438 item.update(cx, |item, cx| {
11439 item.is_dirty = true;
11440 cx.emit(ItemEvent::Edit);
11441 });
11442 cx.executor().advance_clock(Duration::from_millis(250));
11443 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
11444
11445 // // Ensure auto save with delay saves the item on close, even if the timer hasn't yet run out.
11446 pane.update_in(cx, |pane, window, cx| {
11447 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11448 })
11449 .await
11450 .unwrap();
11451 assert!(!cx.has_pending_prompt());
11452 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
11453
11454 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
11455 workspace.update_in(cx, |workspace, window, cx| {
11456 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11457 });
11458 item.update_in(cx, |item, _window, cx| {
11459 item.is_dirty = true;
11460 for project_item in &mut item.project_items {
11461 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11462 }
11463 });
11464 cx.run_until_parked();
11465 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
11466
11467 // Autosave on focus change, ensuring closing the tab counts as such.
11468 item.update(cx, |item, cx| {
11469 SettingsStore::update_global(cx, |settings, cx| {
11470 settings.update_user_settings(cx, |settings| {
11471 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11472 })
11473 });
11474 item.is_dirty = true;
11475 for project_item in &mut item.project_items {
11476 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11477 }
11478 });
11479
11480 pane.update_in(cx, |pane, window, cx| {
11481 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11482 })
11483 .await
11484 .unwrap();
11485 assert!(!cx.has_pending_prompt());
11486 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11487
11488 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
11489 workspace.update_in(cx, |workspace, window, cx| {
11490 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11491 });
11492 item.update_in(cx, |item, window, cx| {
11493 item.project_items[0].update(cx, |item, _| {
11494 item.entry_id = None;
11495 });
11496 item.is_dirty = true;
11497 window.blur();
11498 });
11499 cx.run_until_parked();
11500 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11501
11502 // Ensure autosave is prevented for deleted files also when closing the buffer.
11503 let _close_items = pane.update_in(cx, |pane, window, cx| {
11504 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11505 });
11506 cx.run_until_parked();
11507 assert!(cx.has_pending_prompt());
11508 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11509 }
11510
11511 #[gpui::test]
11512 async fn test_autosave_on_focus_change_in_multibuffer(cx: &mut gpui::TestAppContext) {
11513 init_test(cx);
11514
11515 let fs = FakeFs::new(cx.executor());
11516 let project = Project::test(fs, [], cx).await;
11517 let (workspace, cx) =
11518 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11519
11520 // Create a multibuffer-like item with two child focus handles,
11521 // simulating individual buffer editors within a multibuffer.
11522 let item = cx.new(|cx| {
11523 TestItem::new(cx)
11524 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11525 .with_child_focus_handles(2, cx)
11526 });
11527 workspace.update_in(cx, |workspace, window, cx| {
11528 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11529 });
11530
11531 // Set autosave to OnFocusChange and focus the first child handle,
11532 // simulating the user's cursor being inside one of the multibuffer's excerpts.
11533 item.update_in(cx, |item, window, cx| {
11534 SettingsStore::update_global(cx, |settings, cx| {
11535 settings.update_user_settings(cx, |settings| {
11536 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11537 })
11538 });
11539 item.is_dirty = true;
11540 window.focus(&item.child_focus_handles[0], cx);
11541 });
11542 cx.executor().run_until_parked();
11543 item.read_with(cx, |item, _| assert_eq!(item.save_count, 0));
11544
11545 // Moving focus from one child to another within the same item should
11546 // NOT trigger autosave — focus is still within the item's focus hierarchy.
11547 item.update_in(cx, |item, window, cx| {
11548 window.focus(&item.child_focus_handles[1], cx);
11549 });
11550 cx.executor().run_until_parked();
11551 item.read_with(cx, |item, _| {
11552 assert_eq!(
11553 item.save_count, 0,
11554 "Switching focus between children within the same item should not autosave"
11555 );
11556 });
11557
11558 // Blurring the item saves the file. This is the core regression scenario:
11559 // with `on_blur`, this would NOT trigger because `on_blur` only fires when
11560 // the item's own focus handle is the leaf that lost focus. In a multibuffer,
11561 // the leaf is always a child focus handle, so `on_blur` never detected
11562 // focus leaving the item.
11563 item.update_in(cx, |_, window, _| window.blur());
11564 cx.executor().run_until_parked();
11565 item.read_with(cx, |item, _| {
11566 assert_eq!(
11567 item.save_count, 1,
11568 "Blurring should trigger autosave when focus was on a child of the item"
11569 );
11570 });
11571
11572 // Deactivating the window should also trigger autosave when a child of
11573 // the multibuffer item currently owns focus.
11574 item.update_in(cx, |item, window, cx| {
11575 item.is_dirty = true;
11576 window.focus(&item.child_focus_handles[0], cx);
11577 });
11578 cx.executor().run_until_parked();
11579 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11580
11581 cx.deactivate_window();
11582 item.read_with(cx, |item, _| {
11583 assert_eq!(
11584 item.save_count, 2,
11585 "Deactivating window should trigger autosave when focus was on a child"
11586 );
11587 });
11588 }
11589
11590 #[gpui::test]
11591 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
11592 init_test(cx);
11593
11594 let fs = FakeFs::new(cx.executor());
11595
11596 let project = Project::test(fs, [], cx).await;
11597 let (workspace, cx) =
11598 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11599
11600 let item = cx.new(|cx| {
11601 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11602 });
11603 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11604 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
11605 let toolbar_notify_count = Rc::new(RefCell::new(0));
11606
11607 workspace.update_in(cx, |workspace, window, cx| {
11608 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11609 let toolbar_notification_count = toolbar_notify_count.clone();
11610 cx.observe_in(&toolbar, window, move |_, _, _, _| {
11611 *toolbar_notification_count.borrow_mut() += 1
11612 })
11613 .detach();
11614 });
11615
11616 pane.read_with(cx, |pane, _| {
11617 assert!(!pane.can_navigate_backward());
11618 assert!(!pane.can_navigate_forward());
11619 });
11620
11621 item.update_in(cx, |item, _, cx| {
11622 item.set_state("one".to_string(), cx);
11623 });
11624
11625 // Toolbar must be notified to re-render the navigation buttons
11626 assert_eq!(*toolbar_notify_count.borrow(), 1);
11627
11628 pane.read_with(cx, |pane, _| {
11629 assert!(pane.can_navigate_backward());
11630 assert!(!pane.can_navigate_forward());
11631 });
11632
11633 workspace
11634 .update_in(cx, |workspace, window, cx| {
11635 workspace.go_back(pane.downgrade(), window, cx)
11636 })
11637 .await
11638 .unwrap();
11639
11640 assert_eq!(*toolbar_notify_count.borrow(), 2);
11641 pane.read_with(cx, |pane, _| {
11642 assert!(!pane.can_navigate_backward());
11643 assert!(pane.can_navigate_forward());
11644 });
11645 }
11646
11647 /// Tests that the navigation history deduplicates entries for the same item.
11648 ///
11649 /// When navigating back and forth between items (e.g., A -> B -> A -> B -> A -> B -> C),
11650 /// the navigation history deduplicates by keeping only the most recent visit to each item,
11651 /// resulting in [A, B, C] instead of [A, B, A, B, A, B, C]. This ensures that Go Back (Ctrl-O)
11652 /// navigates through unique items efficiently: C -> B -> A, rather than bouncing between
11653 /// repeated entries: C -> B -> A -> B -> A -> B -> A.
11654 ///
11655 /// This behavior prevents the navigation history from growing unnecessarily large and provides
11656 /// a better user experience by eliminating redundant navigation steps when jumping between files.
11657 #[gpui::test]
11658 async fn test_navigation_history_deduplication(cx: &mut gpui::TestAppContext) {
11659 init_test(cx);
11660
11661 let fs = FakeFs::new(cx.executor());
11662 let project = Project::test(fs, [], cx).await;
11663 let (workspace, cx) =
11664 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11665
11666 let item_a = cx.new(|cx| {
11667 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "a.txt", cx)])
11668 });
11669 let item_b = cx.new(|cx| {
11670 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "b.txt", cx)])
11671 });
11672 let item_c = cx.new(|cx| {
11673 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "c.txt", cx)])
11674 });
11675
11676 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11677
11678 workspace.update_in(cx, |workspace, window, cx| {
11679 workspace.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx);
11680 workspace.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx);
11681 workspace.add_item_to_active_pane(Box::new(item_c.clone()), None, true, window, cx);
11682 });
11683
11684 workspace.update_in(cx, |workspace, window, cx| {
11685 workspace.activate_item(&item_a, false, false, window, cx);
11686 });
11687 cx.run_until_parked();
11688
11689 workspace.update_in(cx, |workspace, window, cx| {
11690 workspace.activate_item(&item_b, false, false, window, cx);
11691 });
11692 cx.run_until_parked();
11693
11694 workspace.update_in(cx, |workspace, window, cx| {
11695 workspace.activate_item(&item_a, false, false, window, cx);
11696 });
11697 cx.run_until_parked();
11698
11699 workspace.update_in(cx, |workspace, window, cx| {
11700 workspace.activate_item(&item_b, false, false, window, cx);
11701 });
11702 cx.run_until_parked();
11703
11704 workspace.update_in(cx, |workspace, window, cx| {
11705 workspace.activate_item(&item_a, false, false, window, cx);
11706 });
11707 cx.run_until_parked();
11708
11709 workspace.update_in(cx, |workspace, window, cx| {
11710 workspace.activate_item(&item_b, false, false, window, cx);
11711 });
11712 cx.run_until_parked();
11713
11714 workspace.update_in(cx, |workspace, window, cx| {
11715 workspace.activate_item(&item_c, false, false, window, cx);
11716 });
11717 cx.run_until_parked();
11718
11719 let backward_count = pane.read_with(cx, |pane, cx| {
11720 let mut count = 0;
11721 pane.nav_history().for_each_entry(cx, &mut |_, _| {
11722 count += 1;
11723 });
11724 count
11725 });
11726 assert!(
11727 backward_count <= 4,
11728 "Should have at most 4 entries, got {}",
11729 backward_count
11730 );
11731
11732 workspace
11733 .update_in(cx, |workspace, window, cx| {
11734 workspace.go_back(pane.downgrade(), window, cx)
11735 })
11736 .await
11737 .unwrap();
11738
11739 let active_item = workspace.read_with(cx, |workspace, cx| {
11740 workspace.active_item(cx).unwrap().item_id()
11741 });
11742 assert_eq!(
11743 active_item,
11744 item_b.entity_id(),
11745 "After first go_back, should be at item B"
11746 );
11747
11748 workspace
11749 .update_in(cx, |workspace, window, cx| {
11750 workspace.go_back(pane.downgrade(), window, cx)
11751 })
11752 .await
11753 .unwrap();
11754
11755 let active_item = workspace.read_with(cx, |workspace, cx| {
11756 workspace.active_item(cx).unwrap().item_id()
11757 });
11758 assert_eq!(
11759 active_item,
11760 item_a.entity_id(),
11761 "After second go_back, should be at item A"
11762 );
11763
11764 pane.read_with(cx, |pane, _| {
11765 assert!(pane.can_navigate_forward(), "Should be able to go forward");
11766 });
11767 }
11768
11769 #[gpui::test]
11770 async fn test_activate_last_pane(cx: &mut gpui::TestAppContext) {
11771 init_test(cx);
11772 let fs = FakeFs::new(cx.executor());
11773 let project = Project::test(fs, [], cx).await;
11774 let (multi_workspace, cx) =
11775 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
11776 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
11777
11778 workspace.update_in(cx, |workspace, window, cx| {
11779 let first_item = cx.new(|cx| {
11780 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11781 });
11782 workspace.add_item_to_active_pane(Box::new(first_item), None, true, window, cx);
11783 workspace.split_pane(
11784 workspace.active_pane().clone(),
11785 SplitDirection::Right,
11786 window,
11787 cx,
11788 );
11789 workspace.split_pane(
11790 workspace.active_pane().clone(),
11791 SplitDirection::Right,
11792 window,
11793 cx,
11794 );
11795 });
11796
11797 let (first_pane_id, target_last_pane_id) = workspace.update(cx, |workspace, _cx| {
11798 let panes = workspace.center.panes();
11799 assert!(panes.len() >= 2);
11800 (
11801 panes.first().expect("at least one pane").entity_id(),
11802 panes.last().expect("at least one pane").entity_id(),
11803 )
11804 });
11805
11806 workspace.update_in(cx, |workspace, window, cx| {
11807 workspace.activate_pane_at_index(&ActivatePane(0), window, cx);
11808 });
11809 workspace.update(cx, |workspace, _| {
11810 assert_eq!(workspace.active_pane().entity_id(), first_pane_id);
11811 assert_ne!(workspace.active_pane().entity_id(), target_last_pane_id);
11812 });
11813
11814 cx.dispatch_action(ActivateLastPane);
11815
11816 workspace.update(cx, |workspace, _| {
11817 assert_eq!(workspace.active_pane().entity_id(), target_last_pane_id);
11818 });
11819 }
11820
11821 #[gpui::test]
11822 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
11823 init_test(cx);
11824 let fs = FakeFs::new(cx.executor());
11825
11826 let project = Project::test(fs, [], cx).await;
11827 let (workspace, cx) =
11828 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11829
11830 let panel = workspace.update_in(cx, |workspace, window, cx| {
11831 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
11832 workspace.add_panel(panel.clone(), window, cx);
11833
11834 workspace
11835 .right_dock()
11836 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
11837
11838 panel
11839 });
11840
11841 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11842 pane.update_in(cx, |pane, window, cx| {
11843 let item = cx.new(TestItem::new);
11844 pane.add_item(Box::new(item), true, true, None, window, cx);
11845 });
11846
11847 // Transfer focus from center to panel
11848 workspace.update_in(cx, |workspace, window, cx| {
11849 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11850 });
11851
11852 workspace.update_in(cx, |workspace, window, cx| {
11853 assert!(workspace.right_dock().read(cx).is_open());
11854 assert!(!panel.is_zoomed(window, cx));
11855 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11856 });
11857
11858 // Transfer focus from panel to center
11859 workspace.update_in(cx, |workspace, window, cx| {
11860 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11861 });
11862
11863 workspace.update_in(cx, |workspace, window, cx| {
11864 assert!(workspace.right_dock().read(cx).is_open());
11865 assert!(!panel.is_zoomed(window, cx));
11866 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11867 assert!(pane.read(cx).focus_handle(cx).contains_focused(window, cx));
11868 });
11869
11870 // Close the dock
11871 workspace.update_in(cx, |workspace, window, cx| {
11872 workspace.toggle_dock(DockPosition::Right, window, cx);
11873 });
11874
11875 workspace.update_in(cx, |workspace, window, cx| {
11876 assert!(!workspace.right_dock().read(cx).is_open());
11877 assert!(!panel.is_zoomed(window, cx));
11878 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11879 assert!(pane.read(cx).focus_handle(cx).contains_focused(window, cx));
11880 });
11881
11882 // Open the dock
11883 workspace.update_in(cx, |workspace, window, cx| {
11884 workspace.toggle_dock(DockPosition::Right, window, cx);
11885 });
11886
11887 workspace.update_in(cx, |workspace, window, cx| {
11888 assert!(workspace.right_dock().read(cx).is_open());
11889 assert!(!panel.is_zoomed(window, cx));
11890 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11891 });
11892
11893 // Focus and zoom panel
11894 panel.update_in(cx, |panel, window, cx| {
11895 cx.focus_self(window);
11896 panel.set_zoomed(true, window, cx)
11897 });
11898
11899 workspace.update_in(cx, |workspace, window, cx| {
11900 assert!(workspace.right_dock().read(cx).is_open());
11901 assert!(panel.is_zoomed(window, cx));
11902 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11903 });
11904
11905 // Transfer focus to the center closes the dock
11906 workspace.update_in(cx, |workspace, window, cx| {
11907 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11908 });
11909
11910 workspace.update_in(cx, |workspace, window, cx| {
11911 assert!(!workspace.right_dock().read(cx).is_open());
11912 assert!(panel.is_zoomed(window, cx));
11913 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11914 });
11915
11916 // Transferring focus back to the panel keeps it zoomed
11917 workspace.update_in(cx, |workspace, window, cx| {
11918 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11919 });
11920
11921 workspace.update_in(cx, |workspace, window, cx| {
11922 assert!(workspace.right_dock().read(cx).is_open());
11923 assert!(panel.is_zoomed(window, cx));
11924 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11925 });
11926
11927 // Close the dock while it is zoomed
11928 workspace.update_in(cx, |workspace, window, cx| {
11929 workspace.toggle_dock(DockPosition::Right, window, cx)
11930 });
11931
11932 workspace.update_in(cx, |workspace, window, cx| {
11933 assert!(!workspace.right_dock().read(cx).is_open());
11934 assert!(panel.is_zoomed(window, cx));
11935 assert!(workspace.zoomed.is_none());
11936 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11937 });
11938
11939 // Opening the dock, when it's zoomed, retains focus
11940 workspace.update_in(cx, |workspace, window, cx| {
11941 workspace.toggle_dock(DockPosition::Right, window, cx)
11942 });
11943
11944 workspace.update_in(cx, |workspace, window, cx| {
11945 assert!(workspace.right_dock().read(cx).is_open());
11946 assert!(panel.is_zoomed(window, cx));
11947 assert!(workspace.zoomed.is_some());
11948 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11949 });
11950
11951 // Unzoom and close the panel, zoom the active pane.
11952 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
11953 workspace.update_in(cx, |workspace, window, cx| {
11954 workspace.toggle_dock(DockPosition::Right, window, cx)
11955 });
11956 pane.update_in(cx, |pane, window, cx| {
11957 pane.toggle_zoom(&Default::default(), window, cx)
11958 });
11959
11960 // Opening a dock unzooms the pane.
11961 workspace.update_in(cx, |workspace, window, cx| {
11962 workspace.toggle_dock(DockPosition::Right, window, cx)
11963 });
11964 workspace.update_in(cx, |workspace, window, cx| {
11965 let pane = pane.read(cx);
11966 assert!(!pane.is_zoomed());
11967 assert!(!pane.focus_handle(cx).is_focused(window));
11968 assert!(workspace.right_dock().read(cx).is_open());
11969 assert!(workspace.zoomed.is_none());
11970 });
11971 }
11972
11973 #[gpui::test]
11974 async fn test_close_panel_on_toggle(cx: &mut gpui::TestAppContext) {
11975 init_test(cx);
11976 let fs = FakeFs::new(cx.executor());
11977
11978 let project = Project::test(fs, [], cx).await;
11979 let (workspace, cx) =
11980 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11981
11982 let panel = workspace.update_in(cx, |workspace, window, cx| {
11983 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
11984 workspace.add_panel(panel.clone(), window, cx);
11985 panel
11986 });
11987
11988 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11989 pane.update_in(cx, |pane, window, cx| {
11990 let item = cx.new(TestItem::new);
11991 pane.add_item(Box::new(item), true, true, None, window, cx);
11992 });
11993
11994 // Enable close_panel_on_toggle
11995 cx.update_global(|store: &mut SettingsStore, cx| {
11996 store.update_user_settings(cx, |settings| {
11997 settings.workspace.close_panel_on_toggle = Some(true);
11998 });
11999 });
12000
12001 // Panel starts closed. Toggling should open and focus it.
12002 workspace.update_in(cx, |workspace, window, cx| {
12003 assert!(!workspace.right_dock().read(cx).is_open());
12004 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12005 });
12006
12007 workspace.update_in(cx, |workspace, window, cx| {
12008 assert!(
12009 workspace.right_dock().read(cx).is_open(),
12010 "Dock should be open after toggling from center"
12011 );
12012 assert!(
12013 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12014 "Panel should be focused after toggling from center"
12015 );
12016 });
12017
12018 // Panel is open and focused. Toggling should close the panel and
12019 // return focus to the center.
12020 workspace.update_in(cx, |workspace, window, cx| {
12021 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12022 });
12023
12024 workspace.update_in(cx, |workspace, window, cx| {
12025 assert!(
12026 !workspace.right_dock().read(cx).is_open(),
12027 "Dock should be closed after toggling from focused panel"
12028 );
12029 assert!(
12030 !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12031 "Panel should not be focused after toggling from focused panel"
12032 );
12033 });
12034
12035 // Open the dock and focus something else so the panel is open but not
12036 // focused. Toggling should focus the panel (not close it).
12037 workspace.update_in(cx, |workspace, window, cx| {
12038 workspace
12039 .right_dock()
12040 .update(cx, |dock, cx| dock.set_open(true, window, cx));
12041 window.focus(&pane.read(cx).focus_handle(cx), cx);
12042 });
12043
12044 workspace.update_in(cx, |workspace, window, cx| {
12045 assert!(workspace.right_dock().read(cx).is_open());
12046 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12047 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12048 });
12049
12050 workspace.update_in(cx, |workspace, window, cx| {
12051 assert!(
12052 workspace.right_dock().read(cx).is_open(),
12053 "Dock should remain open when toggling focuses an open-but-unfocused panel"
12054 );
12055 assert!(
12056 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12057 "Panel should be focused after toggling an open-but-unfocused panel"
12058 );
12059 });
12060
12061 // Now disable the setting and verify the original behavior: toggling
12062 // from a focused panel moves focus to center but leaves the dock open.
12063 cx.update_global(|store: &mut SettingsStore, cx| {
12064 store.update_user_settings(cx, |settings| {
12065 settings.workspace.close_panel_on_toggle = Some(false);
12066 });
12067 });
12068
12069 workspace.update_in(cx, |workspace, window, cx| {
12070 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12071 });
12072
12073 workspace.update_in(cx, |workspace, window, cx| {
12074 assert!(
12075 workspace.right_dock().read(cx).is_open(),
12076 "Dock should remain open when setting is disabled"
12077 );
12078 assert!(
12079 !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12080 "Panel should not be focused after toggling with setting disabled"
12081 );
12082 });
12083 }
12084
12085 #[gpui::test]
12086 async fn test_pane_zoom_in_out(cx: &mut TestAppContext) {
12087 init_test(cx);
12088 let fs = FakeFs::new(cx.executor());
12089
12090 let project = Project::test(fs, [], cx).await;
12091 let (workspace, cx) =
12092 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12093
12094 let pane = workspace.update_in(cx, |workspace, _window, _cx| {
12095 workspace.active_pane().clone()
12096 });
12097
12098 // Add an item to the pane so it can be zoomed
12099 workspace.update_in(cx, |workspace, window, cx| {
12100 let item = cx.new(TestItem::new);
12101 workspace.add_item(pane.clone(), Box::new(item), None, true, true, window, cx);
12102 });
12103
12104 // Initially not zoomed
12105 workspace.update_in(cx, |workspace, _window, cx| {
12106 assert!(!pane.read(cx).is_zoomed(), "Pane starts unzoomed");
12107 assert!(
12108 workspace.zoomed.is_none(),
12109 "Workspace should track no zoomed pane"
12110 );
12111 assert!(pane.read(cx).items_len() > 0, "Pane should have items");
12112 });
12113
12114 // Zoom In
12115 pane.update_in(cx, |pane, window, cx| {
12116 pane.zoom_in(&crate::ZoomIn, window, cx);
12117 });
12118
12119 workspace.update_in(cx, |workspace, window, cx| {
12120 assert!(
12121 pane.read(cx).is_zoomed(),
12122 "Pane should be zoomed after ZoomIn"
12123 );
12124 assert!(
12125 workspace.zoomed.is_some(),
12126 "Workspace should track the zoomed pane"
12127 );
12128 assert!(
12129 pane.read(cx).focus_handle(cx).contains_focused(window, cx),
12130 "ZoomIn should focus the pane"
12131 );
12132 });
12133
12134 // Zoom In again is a no-op
12135 pane.update_in(cx, |pane, window, cx| {
12136 pane.zoom_in(&crate::ZoomIn, window, cx);
12137 });
12138
12139 workspace.update_in(cx, |workspace, window, cx| {
12140 assert!(pane.read(cx).is_zoomed(), "Second ZoomIn keeps pane zoomed");
12141 assert!(
12142 workspace.zoomed.is_some(),
12143 "Workspace still tracks zoomed pane"
12144 );
12145 assert!(
12146 pane.read(cx).focus_handle(cx).contains_focused(window, cx),
12147 "Pane remains focused after repeated ZoomIn"
12148 );
12149 });
12150
12151 // Zoom Out
12152 pane.update_in(cx, |pane, window, cx| {
12153 pane.zoom_out(&crate::ZoomOut, window, cx);
12154 });
12155
12156 workspace.update_in(cx, |workspace, _window, cx| {
12157 assert!(
12158 !pane.read(cx).is_zoomed(),
12159 "Pane should unzoom after ZoomOut"
12160 );
12161 assert!(
12162 workspace.zoomed.is_none(),
12163 "Workspace clears zoom tracking after ZoomOut"
12164 );
12165 });
12166
12167 // Zoom Out again is a no-op
12168 pane.update_in(cx, |pane, window, cx| {
12169 pane.zoom_out(&crate::ZoomOut, window, cx);
12170 });
12171
12172 workspace.update_in(cx, |workspace, _window, cx| {
12173 assert!(
12174 !pane.read(cx).is_zoomed(),
12175 "Second ZoomOut keeps pane unzoomed"
12176 );
12177 assert!(
12178 workspace.zoomed.is_none(),
12179 "Workspace remains without zoomed pane"
12180 );
12181 });
12182 }
12183
12184 #[gpui::test]
12185 async fn test_toggle_all_docks(cx: &mut gpui::TestAppContext) {
12186 init_test(cx);
12187 let fs = FakeFs::new(cx.executor());
12188
12189 let project = Project::test(fs, [], cx).await;
12190 let (workspace, cx) =
12191 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12192 workspace.update_in(cx, |workspace, window, cx| {
12193 // Open two docks
12194 let left_dock = workspace.dock_at_position(DockPosition::Left);
12195 let right_dock = workspace.dock_at_position(DockPosition::Right);
12196
12197 left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12198 right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12199
12200 assert!(left_dock.read(cx).is_open());
12201 assert!(right_dock.read(cx).is_open());
12202 });
12203
12204 workspace.update_in(cx, |workspace, window, cx| {
12205 // Toggle all docks - should close both
12206 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12207
12208 let left_dock = workspace.dock_at_position(DockPosition::Left);
12209 let right_dock = workspace.dock_at_position(DockPosition::Right);
12210 assert!(!left_dock.read(cx).is_open());
12211 assert!(!right_dock.read(cx).is_open());
12212 });
12213
12214 workspace.update_in(cx, |workspace, window, cx| {
12215 // Toggle again - should reopen both
12216 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12217
12218 let left_dock = workspace.dock_at_position(DockPosition::Left);
12219 let right_dock = workspace.dock_at_position(DockPosition::Right);
12220 assert!(left_dock.read(cx).is_open());
12221 assert!(right_dock.read(cx).is_open());
12222 });
12223 }
12224
12225 #[gpui::test]
12226 async fn test_toggle_all_with_manual_close(cx: &mut gpui::TestAppContext) {
12227 init_test(cx);
12228 let fs = FakeFs::new(cx.executor());
12229
12230 let project = Project::test(fs, [], cx).await;
12231 let (workspace, cx) =
12232 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12233 workspace.update_in(cx, |workspace, window, cx| {
12234 // Open two docks
12235 let left_dock = workspace.dock_at_position(DockPosition::Left);
12236 let right_dock = workspace.dock_at_position(DockPosition::Right);
12237
12238 left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12239 right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12240
12241 assert!(left_dock.read(cx).is_open());
12242 assert!(right_dock.read(cx).is_open());
12243 });
12244
12245 workspace.update_in(cx, |workspace, window, cx| {
12246 // Close them manually
12247 workspace.toggle_dock(DockPosition::Left, window, cx);
12248 workspace.toggle_dock(DockPosition::Right, window, cx);
12249
12250 let left_dock = workspace.dock_at_position(DockPosition::Left);
12251 let right_dock = workspace.dock_at_position(DockPosition::Right);
12252 assert!(!left_dock.read(cx).is_open());
12253 assert!(!right_dock.read(cx).is_open());
12254 });
12255
12256 workspace.update_in(cx, |workspace, window, cx| {
12257 // Toggle all docks - only last closed (right dock) should reopen
12258 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12259
12260 let left_dock = workspace.dock_at_position(DockPosition::Left);
12261 let right_dock = workspace.dock_at_position(DockPosition::Right);
12262 assert!(!left_dock.read(cx).is_open());
12263 assert!(right_dock.read(cx).is_open());
12264 });
12265 }
12266
12267 #[gpui::test]
12268 async fn test_toggle_all_docks_after_dock_move(cx: &mut gpui::TestAppContext) {
12269 init_test(cx);
12270 let fs = FakeFs::new(cx.executor());
12271 let project = Project::test(fs, [], cx).await;
12272 let (multi_workspace, cx) =
12273 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12274 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12275
12276 // Open two docks (left and right) with one panel each
12277 let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| {
12278 let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
12279 workspace.add_panel(left_panel.clone(), window, cx);
12280
12281 let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
12282 workspace.add_panel(right_panel.clone(), window, cx);
12283
12284 workspace.toggle_dock(DockPosition::Left, window, cx);
12285 workspace.toggle_dock(DockPosition::Right, window, cx);
12286
12287 // Verify initial state
12288 assert!(
12289 workspace.left_dock().read(cx).is_open(),
12290 "Left dock should be open"
12291 );
12292 assert_eq!(
12293 workspace
12294 .left_dock()
12295 .read(cx)
12296 .visible_panel()
12297 .unwrap()
12298 .panel_id(),
12299 left_panel.panel_id(),
12300 "Left panel should be visible in left dock"
12301 );
12302 assert!(
12303 workspace.right_dock().read(cx).is_open(),
12304 "Right dock should be open"
12305 );
12306 assert_eq!(
12307 workspace
12308 .right_dock()
12309 .read(cx)
12310 .visible_panel()
12311 .unwrap()
12312 .panel_id(),
12313 right_panel.panel_id(),
12314 "Right panel should be visible in right dock"
12315 );
12316 assert!(
12317 !workspace.bottom_dock().read(cx).is_open(),
12318 "Bottom dock should be closed"
12319 );
12320
12321 (left_panel, right_panel)
12322 });
12323
12324 // Focus the left panel and move it to the next position (bottom dock)
12325 workspace.update_in(cx, |workspace, window, cx| {
12326 workspace.toggle_panel_focus::<TestPanel>(window, cx); // Focus left panel
12327 assert!(
12328 left_panel.read(cx).focus_handle(cx).is_focused(window),
12329 "Left panel should be focused"
12330 );
12331 });
12332
12333 cx.dispatch_action(MoveFocusedPanelToNextPosition);
12334
12335 // Verify the left panel has moved to the bottom dock, and the bottom dock is now open
12336 workspace.update(cx, |workspace, cx| {
12337 assert!(
12338 !workspace.left_dock().read(cx).is_open(),
12339 "Left dock should be closed"
12340 );
12341 assert!(
12342 workspace.bottom_dock().read(cx).is_open(),
12343 "Bottom dock should now be open"
12344 );
12345 assert_eq!(
12346 left_panel.read(cx).position,
12347 DockPosition::Bottom,
12348 "Left panel should now be in the bottom dock"
12349 );
12350 assert_eq!(
12351 workspace
12352 .bottom_dock()
12353 .read(cx)
12354 .visible_panel()
12355 .unwrap()
12356 .panel_id(),
12357 left_panel.panel_id(),
12358 "Left panel should be the visible panel in the bottom dock"
12359 );
12360 });
12361
12362 // Toggle all docks off
12363 workspace.update_in(cx, |workspace, window, cx| {
12364 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12365 assert!(
12366 !workspace.left_dock().read(cx).is_open(),
12367 "Left dock should be closed"
12368 );
12369 assert!(
12370 !workspace.right_dock().read(cx).is_open(),
12371 "Right dock should be closed"
12372 );
12373 assert!(
12374 !workspace.bottom_dock().read(cx).is_open(),
12375 "Bottom dock should be closed"
12376 );
12377 });
12378
12379 // Toggle all docks back on and verify positions are restored
12380 workspace.update_in(cx, |workspace, window, cx| {
12381 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12382 assert!(
12383 !workspace.left_dock().read(cx).is_open(),
12384 "Left dock should remain closed"
12385 );
12386 assert!(
12387 workspace.right_dock().read(cx).is_open(),
12388 "Right dock should remain open"
12389 );
12390 assert!(
12391 workspace.bottom_dock().read(cx).is_open(),
12392 "Bottom dock should remain open"
12393 );
12394 assert_eq!(
12395 left_panel.read(cx).position,
12396 DockPosition::Bottom,
12397 "Left panel should remain in the bottom dock"
12398 );
12399 assert_eq!(
12400 right_panel.read(cx).position,
12401 DockPosition::Right,
12402 "Right panel should remain in the right dock"
12403 );
12404 assert_eq!(
12405 workspace
12406 .bottom_dock()
12407 .read(cx)
12408 .visible_panel()
12409 .unwrap()
12410 .panel_id(),
12411 left_panel.panel_id(),
12412 "Left panel should be the visible panel in the right dock"
12413 );
12414 });
12415 }
12416
12417 #[gpui::test]
12418 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
12419 init_test(cx);
12420
12421 let fs = FakeFs::new(cx.executor());
12422
12423 let project = Project::test(fs, None, cx).await;
12424 let (workspace, cx) =
12425 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12426
12427 // Let's arrange the panes like this:
12428 //
12429 // +-----------------------+
12430 // | top |
12431 // +------+--------+-------+
12432 // | left | center | right |
12433 // +------+--------+-------+
12434 // | bottom |
12435 // +-----------------------+
12436
12437 let top_item = cx.new(|cx| {
12438 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
12439 });
12440 let bottom_item = cx.new(|cx| {
12441 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
12442 });
12443 let left_item = cx.new(|cx| {
12444 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
12445 });
12446 let right_item = cx.new(|cx| {
12447 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
12448 });
12449 let center_item = cx.new(|cx| {
12450 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
12451 });
12452
12453 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12454 let top_pane_id = workspace.active_pane().entity_id();
12455 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
12456 workspace.split_pane(
12457 workspace.active_pane().clone(),
12458 SplitDirection::Down,
12459 window,
12460 cx,
12461 );
12462 top_pane_id
12463 });
12464 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12465 let bottom_pane_id = workspace.active_pane().entity_id();
12466 workspace.add_item_to_active_pane(
12467 Box::new(bottom_item.clone()),
12468 None,
12469 false,
12470 window,
12471 cx,
12472 );
12473 workspace.split_pane(
12474 workspace.active_pane().clone(),
12475 SplitDirection::Up,
12476 window,
12477 cx,
12478 );
12479 bottom_pane_id
12480 });
12481 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12482 let left_pane_id = workspace.active_pane().entity_id();
12483 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
12484 workspace.split_pane(
12485 workspace.active_pane().clone(),
12486 SplitDirection::Right,
12487 window,
12488 cx,
12489 );
12490 left_pane_id
12491 });
12492 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12493 let right_pane_id = workspace.active_pane().entity_id();
12494 workspace.add_item_to_active_pane(
12495 Box::new(right_item.clone()),
12496 None,
12497 false,
12498 window,
12499 cx,
12500 );
12501 workspace.split_pane(
12502 workspace.active_pane().clone(),
12503 SplitDirection::Left,
12504 window,
12505 cx,
12506 );
12507 right_pane_id
12508 });
12509 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12510 let center_pane_id = workspace.active_pane().entity_id();
12511 workspace.add_item_to_active_pane(
12512 Box::new(center_item.clone()),
12513 None,
12514 false,
12515 window,
12516 cx,
12517 );
12518 center_pane_id
12519 });
12520 cx.executor().run_until_parked();
12521
12522 workspace.update_in(cx, |workspace, window, cx| {
12523 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
12524
12525 // Join into next from center pane into right
12526 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12527 });
12528
12529 workspace.update_in(cx, |workspace, window, cx| {
12530 let active_pane = workspace.active_pane();
12531 assert_eq!(right_pane_id, active_pane.entity_id());
12532 assert_eq!(2, active_pane.read(cx).items_len());
12533 let item_ids_in_pane =
12534 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12535 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12536 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12537
12538 // Join into next from right pane into bottom
12539 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12540 });
12541
12542 workspace.update_in(cx, |workspace, window, cx| {
12543 let active_pane = workspace.active_pane();
12544 assert_eq!(bottom_pane_id, active_pane.entity_id());
12545 assert_eq!(3, active_pane.read(cx).items_len());
12546 let item_ids_in_pane =
12547 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12548 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12549 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12550 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12551
12552 // Join into next from bottom pane into left
12553 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12554 });
12555
12556 workspace.update_in(cx, |workspace, window, cx| {
12557 let active_pane = workspace.active_pane();
12558 assert_eq!(left_pane_id, active_pane.entity_id());
12559 assert_eq!(4, active_pane.read(cx).items_len());
12560 let item_ids_in_pane =
12561 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12562 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12563 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12564 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12565 assert!(item_ids_in_pane.contains(&left_item.item_id()));
12566
12567 // Join into next from left pane into top
12568 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12569 });
12570
12571 workspace.update_in(cx, |workspace, window, cx| {
12572 let active_pane = workspace.active_pane();
12573 assert_eq!(top_pane_id, active_pane.entity_id());
12574 assert_eq!(5, active_pane.read(cx).items_len());
12575 let item_ids_in_pane =
12576 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12577 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12578 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12579 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12580 assert!(item_ids_in_pane.contains(&left_item.item_id()));
12581 assert!(item_ids_in_pane.contains(&top_item.item_id()));
12582
12583 // Single pane left: no-op
12584 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
12585 });
12586
12587 workspace.update(cx, |workspace, _cx| {
12588 let active_pane = workspace.active_pane();
12589 assert_eq!(top_pane_id, active_pane.entity_id());
12590 });
12591 }
12592
12593 fn add_an_item_to_active_pane(
12594 cx: &mut VisualTestContext,
12595 workspace: &Entity<Workspace>,
12596 item_id: u64,
12597 ) -> Entity<TestItem> {
12598 let item = cx.new(|cx| {
12599 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
12600 item_id,
12601 "item{item_id}.txt",
12602 cx,
12603 )])
12604 });
12605 workspace.update_in(cx, |workspace, window, cx| {
12606 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
12607 });
12608 item
12609 }
12610
12611 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
12612 workspace.update_in(cx, |workspace, window, cx| {
12613 workspace.split_pane(
12614 workspace.active_pane().clone(),
12615 SplitDirection::Right,
12616 window,
12617 cx,
12618 )
12619 })
12620 }
12621
12622 #[gpui::test]
12623 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
12624 init_test(cx);
12625 let fs = FakeFs::new(cx.executor());
12626 let project = Project::test(fs, None, cx).await;
12627 let (workspace, cx) =
12628 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12629
12630 add_an_item_to_active_pane(cx, &workspace, 1);
12631 split_pane(cx, &workspace);
12632 add_an_item_to_active_pane(cx, &workspace, 2);
12633 split_pane(cx, &workspace); // empty pane
12634 split_pane(cx, &workspace);
12635 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
12636
12637 cx.executor().run_until_parked();
12638
12639 workspace.update(cx, |workspace, cx| {
12640 let num_panes = workspace.panes().len();
12641 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
12642 let active_item = workspace
12643 .active_pane()
12644 .read(cx)
12645 .active_item()
12646 .expect("item is in focus");
12647
12648 assert_eq!(num_panes, 4);
12649 assert_eq!(num_items_in_current_pane, 1);
12650 assert_eq!(active_item.item_id(), last_item.item_id());
12651 });
12652
12653 workspace.update_in(cx, |workspace, window, cx| {
12654 workspace.join_all_panes(window, cx);
12655 });
12656
12657 workspace.update(cx, |workspace, cx| {
12658 let num_panes = workspace.panes().len();
12659 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
12660 let active_item = workspace
12661 .active_pane()
12662 .read(cx)
12663 .active_item()
12664 .expect("item is in focus");
12665
12666 assert_eq!(num_panes, 1);
12667 assert_eq!(num_items_in_current_pane, 3);
12668 assert_eq!(active_item.item_id(), last_item.item_id());
12669 });
12670 }
12671
12672 #[gpui::test]
12673 async fn test_flexible_dock_sizing(cx: &mut gpui::TestAppContext) {
12674 init_test(cx);
12675 let fs = FakeFs::new(cx.executor());
12676
12677 let project = Project::test(fs, [], cx).await;
12678 let (multi_workspace, cx) =
12679 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12680 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12681
12682 workspace.update(cx, |workspace, _cx| {
12683 workspace.bounds.size.width = px(800.);
12684 });
12685
12686 workspace.update_in(cx, |workspace, window, cx| {
12687 let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
12688 workspace.add_panel(panel, window, cx);
12689 workspace.toggle_dock(DockPosition::Right, window, cx);
12690 });
12691
12692 let (panel, resized_width, ratio_basis_width) =
12693 workspace.update_in(cx, |workspace, window, cx| {
12694 let item = cx.new(|cx| {
12695 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
12696 });
12697 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
12698
12699 let dock = workspace.right_dock().read(cx);
12700 let workspace_width = workspace.bounds.size.width;
12701 let initial_width = workspace
12702 .dock_size(&dock, window, cx)
12703 .expect("flexible dock should have an initial width");
12704
12705 assert_eq!(initial_width, workspace_width / 2.);
12706
12707 workspace.resize_right_dock(px(300.), window, cx);
12708
12709 let dock = workspace.right_dock().read(cx);
12710 let resized_width = workspace
12711 .dock_size(&dock, window, cx)
12712 .expect("flexible dock should keep its resized width");
12713
12714 assert_eq!(resized_width, px(300.));
12715
12716 let panel = workspace
12717 .right_dock()
12718 .read(cx)
12719 .visible_panel()
12720 .expect("flexible dock should have a visible panel")
12721 .panel_id();
12722
12723 (panel, resized_width, workspace_width)
12724 });
12725
12726 workspace.update_in(cx, |workspace, window, cx| {
12727 workspace.toggle_dock(DockPosition::Right, window, cx);
12728 workspace.toggle_dock(DockPosition::Right, window, cx);
12729
12730 let dock = workspace.right_dock().read(cx);
12731 let reopened_width = workspace
12732 .dock_size(&dock, window, cx)
12733 .expect("flexible dock should restore when reopened");
12734
12735 assert_eq!(reopened_width, resized_width);
12736
12737 let right_dock = workspace.right_dock().read(cx);
12738 let flexible_panel = right_dock
12739 .visible_panel()
12740 .expect("flexible dock should still have a visible panel");
12741 assert_eq!(flexible_panel.panel_id(), panel);
12742 assert_eq!(
12743 right_dock
12744 .stored_panel_size_state(flexible_panel.as_ref())
12745 .and_then(|size_state| size_state.flex),
12746 Some(
12747 resized_width.to_f64() as f32
12748 / (workspace.bounds.size.width - resized_width).to_f64() as f32
12749 )
12750 );
12751 });
12752
12753 workspace.update_in(cx, |workspace, window, cx| {
12754 workspace.split_pane(
12755 workspace.active_pane().clone(),
12756 SplitDirection::Right,
12757 window,
12758 cx,
12759 );
12760
12761 let dock = workspace.right_dock().read(cx);
12762 let split_width = workspace
12763 .dock_size(&dock, window, cx)
12764 .expect("flexible dock should keep its user-resized proportion");
12765
12766 assert_eq!(split_width, px(300.));
12767
12768 workspace.bounds.size.width = px(1600.);
12769
12770 let dock = workspace.right_dock().read(cx);
12771 let resized_window_width = workspace
12772 .dock_size(&dock, window, cx)
12773 .expect("flexible dock should preserve proportional size on window resize");
12774
12775 assert_eq!(
12776 resized_window_width,
12777 workspace.bounds.size.width
12778 * (resized_width.to_f64() as f32 / ratio_basis_width.to_f64() as f32)
12779 );
12780 });
12781 }
12782
12783 #[gpui::test]
12784 async fn test_panel_size_state_persistence(cx: &mut gpui::TestAppContext) {
12785 init_test(cx);
12786 let fs = FakeFs::new(cx.executor());
12787
12788 // Fixed-width panel: pixel size is persisted to KVP and restored on re-add.
12789 {
12790 let project = Project::test(fs.clone(), [], cx).await;
12791 let (multi_workspace, cx) =
12792 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12793 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12794
12795 workspace.update(cx, |workspace, _cx| {
12796 workspace.set_random_database_id();
12797 workspace.bounds.size.width = px(800.);
12798 });
12799
12800 let panel = workspace.update_in(cx, |workspace, window, cx| {
12801 let panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
12802 workspace.add_panel(panel.clone(), window, cx);
12803 workspace.toggle_dock(DockPosition::Left, window, cx);
12804 panel
12805 });
12806
12807 workspace.update_in(cx, |workspace, window, cx| {
12808 workspace.resize_left_dock(px(350.), window, cx);
12809 });
12810
12811 cx.run_until_parked();
12812
12813 let persisted = workspace.read_with(cx, |workspace, cx| {
12814 workspace.persisted_panel_size_state(TestPanel::panel_key(), cx)
12815 });
12816 assert_eq!(
12817 persisted.and_then(|s| s.size),
12818 Some(px(350.)),
12819 "fixed-width panel size should be persisted to KVP"
12820 );
12821
12822 // Remove the panel and re-add a fresh instance with the same key.
12823 // The new instance should have its size state restored from KVP.
12824 workspace.update_in(cx, |workspace, window, cx| {
12825 workspace.remove_panel(&panel, window, cx);
12826 });
12827
12828 workspace.update_in(cx, |workspace, window, cx| {
12829 let new_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
12830 workspace.add_panel(new_panel, window, cx);
12831
12832 let left_dock = workspace.left_dock().read(cx);
12833 let size_state = left_dock
12834 .panel::<TestPanel>()
12835 .and_then(|p| left_dock.stored_panel_size_state(&p));
12836 assert_eq!(
12837 size_state.and_then(|s| s.size),
12838 Some(px(350.)),
12839 "re-added fixed-width panel should restore persisted size from KVP"
12840 );
12841 });
12842 }
12843
12844 // Flexible panel: both pixel size and ratio are persisted and restored.
12845 {
12846 let project = Project::test(fs.clone(), [], cx).await;
12847 let (multi_workspace, cx) =
12848 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12849 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12850
12851 workspace.update(cx, |workspace, _cx| {
12852 workspace.set_random_database_id();
12853 workspace.bounds.size.width = px(800.);
12854 });
12855
12856 let panel = workspace.update_in(cx, |workspace, window, cx| {
12857 let item = cx.new(|cx| {
12858 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
12859 });
12860 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
12861
12862 let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
12863 workspace.add_panel(panel.clone(), window, cx);
12864 workspace.toggle_dock(DockPosition::Right, window, cx);
12865 panel
12866 });
12867
12868 workspace.update_in(cx, |workspace, window, cx| {
12869 workspace.resize_right_dock(px(300.), window, cx);
12870 });
12871
12872 cx.run_until_parked();
12873
12874 let persisted = workspace
12875 .read_with(cx, |workspace, cx| {
12876 workspace.persisted_panel_size_state(TestPanel::panel_key(), cx)
12877 })
12878 .expect("flexible panel state should be persisted to KVP");
12879 assert_eq!(
12880 persisted.size, None,
12881 "flexible panel should not persist a redundant pixel size"
12882 );
12883 let original_ratio = persisted.flex.expect("panel's flex should be persisted");
12884
12885 // Remove the panel and re-add: both size and ratio should be restored.
12886 workspace.update_in(cx, |workspace, window, cx| {
12887 workspace.remove_panel(&panel, window, cx);
12888 });
12889
12890 workspace.update_in(cx, |workspace, window, cx| {
12891 let new_panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
12892 workspace.add_panel(new_panel, window, cx);
12893
12894 let right_dock = workspace.right_dock().read(cx);
12895 let size_state = right_dock
12896 .panel::<TestPanel>()
12897 .and_then(|p| right_dock.stored_panel_size_state(&p))
12898 .expect("re-added flexible panel should have restored size state from KVP");
12899 assert_eq!(
12900 size_state.size, None,
12901 "re-added flexible panel should not have a persisted pixel size"
12902 );
12903 assert_eq!(
12904 size_state.flex,
12905 Some(original_ratio),
12906 "re-added flexible panel should restore persisted flex"
12907 );
12908 });
12909 }
12910 }
12911
12912 #[gpui::test]
12913 async fn test_flexible_panel_left_dock_sizing(cx: &mut gpui::TestAppContext) {
12914 init_test(cx);
12915 let fs = FakeFs::new(cx.executor());
12916
12917 let project = Project::test(fs, [], cx).await;
12918 let (multi_workspace, cx) =
12919 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12920 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12921
12922 workspace.update(cx, |workspace, _cx| {
12923 workspace.bounds.size.width = px(900.);
12924 });
12925
12926 // Step 1: Add a tab to the center pane then open a flexible panel in the left
12927 // dock. With one full-width center pane the default ratio is 0.5, so the panel
12928 // and the center pane each take half the workspace width.
12929 workspace.update_in(cx, |workspace, window, cx| {
12930 let item = cx.new(|cx| {
12931 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
12932 });
12933 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
12934
12935 let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Left, 100, cx));
12936 workspace.add_panel(panel, window, cx);
12937 workspace.toggle_dock(DockPosition::Left, window, cx);
12938
12939 let left_dock = workspace.left_dock().read(cx);
12940 let left_width = workspace
12941 .dock_size(&left_dock, window, cx)
12942 .expect("left dock should have an active panel");
12943
12944 assert_eq!(
12945 left_width,
12946 workspace.bounds.size.width / 2.,
12947 "flexible left panel should split evenly with the center pane"
12948 );
12949 });
12950
12951 // Step 2: Split the center pane vertically (top/bottom). Vertical splits do not
12952 // change horizontal width fractions, so the flexible panel stays at the same
12953 // width as each half of the split.
12954 workspace.update_in(cx, |workspace, window, cx| {
12955 workspace.split_pane(
12956 workspace.active_pane().clone(),
12957 SplitDirection::Down,
12958 window,
12959 cx,
12960 );
12961
12962 let left_dock = workspace.left_dock().read(cx);
12963 let left_width = workspace
12964 .dock_size(&left_dock, window, cx)
12965 .expect("left dock should still have an active panel after vertical split");
12966
12967 assert_eq!(
12968 left_width,
12969 workspace.bounds.size.width / 2.,
12970 "flexible left panel width should match each vertically-split pane"
12971 );
12972 });
12973
12974 // Step 3: Open a fixed-width panel in the right dock. The right dock's default
12975 // size reduces the available width, so the flexible left panel and the center
12976 // panes all shrink proportionally to accommodate it.
12977 workspace.update_in(cx, |workspace, window, cx| {
12978 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 200, cx));
12979 workspace.add_panel(panel, window, cx);
12980 workspace.toggle_dock(DockPosition::Right, window, cx);
12981
12982 let right_dock = workspace.right_dock().read(cx);
12983 let right_width = workspace
12984 .dock_size(&right_dock, window, cx)
12985 .expect("right dock should have an active panel");
12986
12987 let left_dock = workspace.left_dock().read(cx);
12988 let left_width = workspace
12989 .dock_size(&left_dock, window, cx)
12990 .expect("left dock should still have an active panel");
12991
12992 let available_width = workspace.bounds.size.width - right_width;
12993 assert_eq!(
12994 left_width,
12995 available_width / 2.,
12996 "flexible left panel should shrink proportionally as the right dock takes space"
12997 );
12998 });
12999
13000 // Step 4: Toggle the right dock's panel to flexible. Now both docks use
13001 // flex sizing and the workspace width is divided among left-flex, center
13002 // (implicit flex 1.0), and right-flex.
13003 workspace.update_in(cx, |workspace, window, cx| {
13004 let right_dock = workspace.right_dock().clone();
13005 let right_panel = right_dock
13006 .read(cx)
13007 .visible_panel()
13008 .expect("right dock should have a visible panel")
13009 .clone();
13010 workspace.toggle_dock_panel_flexible_size(
13011 &right_dock,
13012 right_panel.as_ref(),
13013 window,
13014 cx,
13015 );
13016
13017 let right_dock = right_dock.read(cx);
13018 let right_panel = right_dock
13019 .visible_panel()
13020 .expect("right dock should still have a visible panel");
13021 assert!(
13022 right_panel.has_flexible_size(window, cx),
13023 "right panel should now be flexible"
13024 );
13025
13026 let right_size_state = right_dock
13027 .stored_panel_size_state(right_panel.as_ref())
13028 .expect("right panel should have a stored size state after toggling");
13029 let right_flex = right_size_state
13030 .flex
13031 .expect("right panel should have a flex value after toggling");
13032
13033 let left_dock = workspace.left_dock().read(cx);
13034 let left_width = workspace
13035 .dock_size(&left_dock, window, cx)
13036 .expect("left dock should still have an active panel");
13037 let right_width = workspace
13038 .dock_size(&right_dock, window, cx)
13039 .expect("right dock should still have an active panel");
13040
13041 let left_flex = workspace
13042 .default_dock_flex(DockPosition::Left)
13043 .expect("left dock should have a default flex");
13044
13045 let total_flex = left_flex + 1.0 + right_flex;
13046 let expected_left = left_flex / total_flex * workspace.bounds.size.width;
13047 let expected_right = right_flex / total_flex * workspace.bounds.size.width;
13048 assert_eq!(
13049 left_width, expected_left,
13050 "flexible left panel should share workspace width via flex ratios"
13051 );
13052 assert_eq!(
13053 right_width, expected_right,
13054 "flexible right panel should share workspace width via flex ratios"
13055 );
13056 });
13057 }
13058
13059 struct TestModal(FocusHandle);
13060
13061 impl TestModal {
13062 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
13063 Self(cx.focus_handle())
13064 }
13065 }
13066
13067 impl EventEmitter<DismissEvent> for TestModal {}
13068
13069 impl Focusable for TestModal {
13070 fn focus_handle(&self, _cx: &App) -> FocusHandle {
13071 self.0.clone()
13072 }
13073 }
13074
13075 impl ModalView for TestModal {}
13076
13077 impl Render for TestModal {
13078 fn render(
13079 &mut self,
13080 _window: &mut Window,
13081 _cx: &mut Context<TestModal>,
13082 ) -> impl IntoElement {
13083 div().track_focus(&self.0)
13084 }
13085 }
13086
13087 #[gpui::test]
13088 async fn test_panels(cx: &mut gpui::TestAppContext) {
13089 init_test(cx);
13090 let fs = FakeFs::new(cx.executor());
13091
13092 let project = Project::test(fs, [], cx).await;
13093 let (multi_workspace, cx) =
13094 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
13095 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
13096
13097 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
13098 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
13099 workspace.add_panel(panel_1.clone(), window, cx);
13100 workspace.toggle_dock(DockPosition::Left, window, cx);
13101 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
13102 workspace.add_panel(panel_2.clone(), window, cx);
13103 workspace.toggle_dock(DockPosition::Right, window, cx);
13104
13105 let left_dock = workspace.left_dock();
13106 assert_eq!(
13107 left_dock.read(cx).visible_panel().unwrap().panel_id(),
13108 panel_1.panel_id()
13109 );
13110 assert_eq!(
13111 workspace.dock_size(&left_dock.read(cx), window, cx),
13112 Some(px(300.))
13113 );
13114
13115 workspace.resize_left_dock(px(1337.), window, cx);
13116 assert_eq!(
13117 workspace
13118 .right_dock()
13119 .read(cx)
13120 .visible_panel()
13121 .unwrap()
13122 .panel_id(),
13123 panel_2.panel_id(),
13124 );
13125
13126 (panel_1, panel_2)
13127 });
13128
13129 // Move panel_1 to the right
13130 panel_1.update_in(cx, |panel_1, window, cx| {
13131 panel_1.set_position(DockPosition::Right, window, cx)
13132 });
13133
13134 workspace.update_in(cx, |workspace, window, cx| {
13135 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
13136 // Since it was the only panel on the left, the left dock should now be closed.
13137 assert!(!workspace.left_dock().read(cx).is_open());
13138 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
13139 let right_dock = workspace.right_dock();
13140 assert_eq!(
13141 right_dock.read(cx).visible_panel().unwrap().panel_id(),
13142 panel_1.panel_id()
13143 );
13144 assert_eq!(
13145 right_dock
13146 .read(cx)
13147 .active_panel_size()
13148 .unwrap()
13149 .size
13150 .unwrap(),
13151 px(1337.)
13152 );
13153
13154 // Now we move panel_2 to the left
13155 panel_2.set_position(DockPosition::Left, window, cx);
13156 });
13157
13158 workspace.update(cx, |workspace, cx| {
13159 // Since panel_2 was not visible on the right, we don't open the left dock.
13160 assert!(!workspace.left_dock().read(cx).is_open());
13161 // And the right dock is unaffected in its displaying of panel_1
13162 assert!(workspace.right_dock().read(cx).is_open());
13163 assert_eq!(
13164 workspace
13165 .right_dock()
13166 .read(cx)
13167 .visible_panel()
13168 .unwrap()
13169 .panel_id(),
13170 panel_1.panel_id(),
13171 );
13172 });
13173
13174 // Move panel_1 back to the left
13175 panel_1.update_in(cx, |panel_1, window, cx| {
13176 panel_1.set_position(DockPosition::Left, window, cx)
13177 });
13178
13179 workspace.update_in(cx, |workspace, window, cx| {
13180 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
13181 let left_dock = workspace.left_dock();
13182 assert!(left_dock.read(cx).is_open());
13183 assert_eq!(
13184 left_dock.read(cx).visible_panel().unwrap().panel_id(),
13185 panel_1.panel_id()
13186 );
13187 assert_eq!(
13188 workspace.dock_size(&left_dock.read(cx), window, cx),
13189 Some(px(1337.))
13190 );
13191 // And the right dock should be closed as it no longer has any panels.
13192 assert!(!workspace.right_dock().read(cx).is_open());
13193
13194 // Now we move panel_1 to the bottom
13195 panel_1.set_position(DockPosition::Bottom, window, cx);
13196 });
13197
13198 workspace.update_in(cx, |workspace, window, cx| {
13199 // Since panel_1 was visible on the left, we close the left dock.
13200 assert!(!workspace.left_dock().read(cx).is_open());
13201 // The bottom dock is sized based on the panel's default size,
13202 // since the panel orientation changed from vertical to horizontal.
13203 let bottom_dock = workspace.bottom_dock();
13204 assert_eq!(
13205 workspace.dock_size(&bottom_dock.read(cx), window, cx),
13206 Some(px(300.))
13207 );
13208 // Close bottom dock and move panel_1 back to the left.
13209 bottom_dock.update(cx, |bottom_dock, cx| {
13210 bottom_dock.set_open(false, window, cx)
13211 });
13212 panel_1.set_position(DockPosition::Left, window, cx);
13213 });
13214
13215 // Emit activated event on panel 1
13216 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
13217
13218 // Now the left dock is open and panel_1 is active and focused.
13219 workspace.update_in(cx, |workspace, window, cx| {
13220 let left_dock = workspace.left_dock();
13221 assert!(left_dock.read(cx).is_open());
13222 assert_eq!(
13223 left_dock.read(cx).visible_panel().unwrap().panel_id(),
13224 panel_1.panel_id(),
13225 );
13226 assert!(panel_1.focus_handle(cx).is_focused(window));
13227 });
13228
13229 // Emit closed event on panel 2, which is not active
13230 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
13231
13232 // Wo don't close the left dock, because panel_2 wasn't the active panel
13233 workspace.update(cx, |workspace, cx| {
13234 let left_dock = workspace.left_dock();
13235 assert!(left_dock.read(cx).is_open());
13236 assert_eq!(
13237 left_dock.read(cx).visible_panel().unwrap().panel_id(),
13238 panel_1.panel_id(),
13239 );
13240 });
13241
13242 // Emitting a ZoomIn event shows the panel as zoomed.
13243 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
13244 workspace.read_with(cx, |workspace, _| {
13245 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13246 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
13247 });
13248
13249 // Move panel to another dock while it is zoomed
13250 panel_1.update_in(cx, |panel, window, cx| {
13251 panel.set_position(DockPosition::Right, window, cx)
13252 });
13253 workspace.read_with(cx, |workspace, _| {
13254 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13255
13256 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
13257 });
13258
13259 // This is a helper for getting a:
13260 // - valid focus on an element,
13261 // - that isn't a part of the panes and panels system of the Workspace,
13262 // - and doesn't trigger the 'on_focus_lost' API.
13263 let focus_other_view = {
13264 let workspace = workspace.clone();
13265 move |cx: &mut VisualTestContext| {
13266 workspace.update_in(cx, |workspace, window, cx| {
13267 if workspace.active_modal::<TestModal>(cx).is_some() {
13268 workspace.toggle_modal(window, cx, TestModal::new);
13269 workspace.toggle_modal(window, cx, TestModal::new);
13270 } else {
13271 workspace.toggle_modal(window, cx, TestModal::new);
13272 }
13273 })
13274 }
13275 };
13276
13277 // If focus is transferred to another view that's not a panel or another pane, we still show
13278 // the panel as zoomed.
13279 focus_other_view(cx);
13280 workspace.read_with(cx, |workspace, _| {
13281 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13282 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
13283 });
13284
13285 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
13286 workspace.update_in(cx, |_workspace, window, cx| {
13287 cx.focus_self(window);
13288 });
13289 workspace.read_with(cx, |workspace, _| {
13290 assert_eq!(workspace.zoomed, None);
13291 assert_eq!(workspace.zoomed_position, None);
13292 });
13293
13294 // If focus is transferred again to another view that's not a panel or a pane, we won't
13295 // show the panel as zoomed because it wasn't zoomed before.
13296 focus_other_view(cx);
13297 workspace.read_with(cx, |workspace, _| {
13298 assert_eq!(workspace.zoomed, None);
13299 assert_eq!(workspace.zoomed_position, None);
13300 });
13301
13302 // When the panel is activated, it is zoomed again.
13303 cx.dispatch_action(ToggleRightDock);
13304 workspace.read_with(cx, |workspace, _| {
13305 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13306 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
13307 });
13308
13309 // Emitting a ZoomOut event unzooms the panel.
13310 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
13311 workspace.read_with(cx, |workspace, _| {
13312 assert_eq!(workspace.zoomed, None);
13313 assert_eq!(workspace.zoomed_position, None);
13314 });
13315
13316 // Emit closed event on panel 1, which is active
13317 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
13318
13319 // Now the left dock is closed, because panel_1 was the active panel
13320 workspace.update(cx, |workspace, cx| {
13321 let right_dock = workspace.right_dock();
13322 assert!(!right_dock.read(cx).is_open());
13323 });
13324 }
13325
13326 #[gpui::test]
13327 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
13328 init_test(cx);
13329
13330 let fs = FakeFs::new(cx.background_executor.clone());
13331 let project = Project::test(fs, [], cx).await;
13332 let (workspace, cx) =
13333 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13334 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13335
13336 let dirty_regular_buffer = cx.new(|cx| {
13337 TestItem::new(cx)
13338 .with_dirty(true)
13339 .with_label("1.txt")
13340 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
13341 });
13342 let dirty_regular_buffer_2 = cx.new(|cx| {
13343 TestItem::new(cx)
13344 .with_dirty(true)
13345 .with_label("2.txt")
13346 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
13347 });
13348 let dirty_multi_buffer_with_both = cx.new(|cx| {
13349 TestItem::new(cx)
13350 .with_dirty(true)
13351 .with_buffer_kind(ItemBufferKind::Multibuffer)
13352 .with_label("Fake Project Search")
13353 .with_project_items(&[
13354 dirty_regular_buffer.read(cx).project_items[0].clone(),
13355 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
13356 ])
13357 });
13358 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
13359 workspace.update_in(cx, |workspace, window, cx| {
13360 workspace.add_item(
13361 pane.clone(),
13362 Box::new(dirty_regular_buffer.clone()),
13363 None,
13364 false,
13365 false,
13366 window,
13367 cx,
13368 );
13369 workspace.add_item(
13370 pane.clone(),
13371 Box::new(dirty_regular_buffer_2.clone()),
13372 None,
13373 false,
13374 false,
13375 window,
13376 cx,
13377 );
13378 workspace.add_item(
13379 pane.clone(),
13380 Box::new(dirty_multi_buffer_with_both.clone()),
13381 None,
13382 false,
13383 false,
13384 window,
13385 cx,
13386 );
13387 });
13388
13389 pane.update_in(cx, |pane, window, cx| {
13390 pane.activate_item(2, true, true, window, cx);
13391 assert_eq!(
13392 pane.active_item().unwrap().item_id(),
13393 multi_buffer_with_both_files_id,
13394 "Should select the multi buffer in the pane"
13395 );
13396 });
13397 let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13398 pane.close_other_items(
13399 &CloseOtherItems {
13400 save_intent: Some(SaveIntent::Save),
13401 close_pinned: true,
13402 },
13403 None,
13404 window,
13405 cx,
13406 )
13407 });
13408 cx.background_executor.run_until_parked();
13409 assert!(!cx.has_pending_prompt());
13410 close_all_but_multi_buffer_task
13411 .await
13412 .expect("Closing all buffers but the multi buffer failed");
13413 pane.update(cx, |pane, cx| {
13414 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
13415 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
13416 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
13417 assert_eq!(pane.items_len(), 1);
13418 assert_eq!(
13419 pane.active_item().unwrap().item_id(),
13420 multi_buffer_with_both_files_id,
13421 "Should have only the multi buffer left in the pane"
13422 );
13423 assert!(
13424 dirty_multi_buffer_with_both.read(cx).is_dirty,
13425 "The multi buffer containing the unsaved buffer should still be dirty"
13426 );
13427 });
13428
13429 dirty_regular_buffer.update(cx, |buffer, cx| {
13430 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
13431 });
13432
13433 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13434 pane.close_active_item(
13435 &CloseActiveItem {
13436 save_intent: Some(SaveIntent::Close),
13437 close_pinned: false,
13438 },
13439 window,
13440 cx,
13441 )
13442 });
13443 cx.background_executor.run_until_parked();
13444 assert!(
13445 cx.has_pending_prompt(),
13446 "Dirty multi buffer should prompt a save dialog"
13447 );
13448 cx.simulate_prompt_answer("Save");
13449 cx.background_executor.run_until_parked();
13450 close_multi_buffer_task
13451 .await
13452 .expect("Closing the multi buffer failed");
13453 pane.update(cx, |pane, cx| {
13454 assert_eq!(
13455 dirty_multi_buffer_with_both.read(cx).save_count,
13456 1,
13457 "Multi buffer item should get be saved"
13458 );
13459 // Test impl does not save inner items, so we do not assert them
13460 assert_eq!(
13461 pane.items_len(),
13462 0,
13463 "No more items should be left in the pane"
13464 );
13465 assert!(pane.active_item().is_none());
13466 });
13467 }
13468
13469 #[gpui::test]
13470 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
13471 cx: &mut TestAppContext,
13472 ) {
13473 init_test(cx);
13474
13475 let fs = FakeFs::new(cx.background_executor.clone());
13476 let project = Project::test(fs, [], cx).await;
13477 let (workspace, cx) =
13478 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13479 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13480
13481 let dirty_regular_buffer = cx.new(|cx| {
13482 TestItem::new(cx)
13483 .with_dirty(true)
13484 .with_label("1.txt")
13485 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
13486 });
13487 let dirty_regular_buffer_2 = cx.new(|cx| {
13488 TestItem::new(cx)
13489 .with_dirty(true)
13490 .with_label("2.txt")
13491 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
13492 });
13493 let clear_regular_buffer = cx.new(|cx| {
13494 TestItem::new(cx)
13495 .with_label("3.txt")
13496 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
13497 });
13498
13499 let dirty_multi_buffer_with_both = cx.new(|cx| {
13500 TestItem::new(cx)
13501 .with_dirty(true)
13502 .with_buffer_kind(ItemBufferKind::Multibuffer)
13503 .with_label("Fake Project Search")
13504 .with_project_items(&[
13505 dirty_regular_buffer.read(cx).project_items[0].clone(),
13506 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
13507 clear_regular_buffer.read(cx).project_items[0].clone(),
13508 ])
13509 });
13510 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
13511 workspace.update_in(cx, |workspace, window, cx| {
13512 workspace.add_item(
13513 pane.clone(),
13514 Box::new(dirty_regular_buffer.clone()),
13515 None,
13516 false,
13517 false,
13518 window,
13519 cx,
13520 );
13521 workspace.add_item(
13522 pane.clone(),
13523 Box::new(dirty_multi_buffer_with_both.clone()),
13524 None,
13525 false,
13526 false,
13527 window,
13528 cx,
13529 );
13530 });
13531
13532 pane.update_in(cx, |pane, window, cx| {
13533 pane.activate_item(1, true, true, window, cx);
13534 assert_eq!(
13535 pane.active_item().unwrap().item_id(),
13536 multi_buffer_with_both_files_id,
13537 "Should select the multi buffer in the pane"
13538 );
13539 });
13540 let _close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13541 pane.close_active_item(
13542 &CloseActiveItem {
13543 save_intent: None,
13544 close_pinned: false,
13545 },
13546 window,
13547 cx,
13548 )
13549 });
13550 cx.background_executor.run_until_parked();
13551 assert!(
13552 cx.has_pending_prompt(),
13553 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
13554 );
13555 }
13556
13557 /// Tests that when `close_on_file_delete` is enabled, files are automatically
13558 /// closed when they are deleted from disk.
13559 #[gpui::test]
13560 async fn test_close_on_disk_deletion_enabled(cx: &mut TestAppContext) {
13561 init_test(cx);
13562
13563 // Enable the close_on_disk_deletion setting
13564 cx.update_global(|store: &mut SettingsStore, cx| {
13565 store.update_user_settings(cx, |settings| {
13566 settings.workspace.close_on_file_delete = Some(true);
13567 });
13568 });
13569
13570 let fs = FakeFs::new(cx.background_executor.clone());
13571 let project = Project::test(fs, [], cx).await;
13572 let (workspace, cx) =
13573 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13574 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13575
13576 // Create a test item that simulates a file
13577 let item = cx.new(|cx| {
13578 TestItem::new(cx)
13579 .with_label("test.txt")
13580 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13581 });
13582
13583 // Add item to workspace
13584 workspace.update_in(cx, |workspace, window, cx| {
13585 workspace.add_item(
13586 pane.clone(),
13587 Box::new(item.clone()),
13588 None,
13589 false,
13590 false,
13591 window,
13592 cx,
13593 );
13594 });
13595
13596 // Verify the item is in the pane
13597 pane.read_with(cx, |pane, _| {
13598 assert_eq!(pane.items().count(), 1);
13599 });
13600
13601 // Simulate file deletion by setting the item's deleted state
13602 item.update(cx, |item, _| {
13603 item.set_has_deleted_file(true);
13604 });
13605
13606 // Emit UpdateTab event to trigger the close behavior
13607 cx.run_until_parked();
13608 item.update(cx, |_, cx| {
13609 cx.emit(ItemEvent::UpdateTab);
13610 });
13611
13612 // Allow the close operation to complete
13613 cx.run_until_parked();
13614
13615 // Verify the item was automatically closed
13616 pane.read_with(cx, |pane, _| {
13617 assert_eq!(
13618 pane.items().count(),
13619 0,
13620 "Item should be automatically closed when file is deleted"
13621 );
13622 });
13623 }
13624
13625 /// Tests that when `close_on_file_delete` is disabled (default), files remain
13626 /// open with a strikethrough when they are deleted from disk.
13627 #[gpui::test]
13628 async fn test_close_on_disk_deletion_disabled(cx: &mut TestAppContext) {
13629 init_test(cx);
13630
13631 // Ensure close_on_disk_deletion is disabled (default)
13632 cx.update_global(|store: &mut SettingsStore, cx| {
13633 store.update_user_settings(cx, |settings| {
13634 settings.workspace.close_on_file_delete = Some(false);
13635 });
13636 });
13637
13638 let fs = FakeFs::new(cx.background_executor.clone());
13639 let project = Project::test(fs, [], cx).await;
13640 let (workspace, cx) =
13641 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13642 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13643
13644 // Create a test item that simulates a file
13645 let item = cx.new(|cx| {
13646 TestItem::new(cx)
13647 .with_label("test.txt")
13648 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13649 });
13650
13651 // Add item to workspace
13652 workspace.update_in(cx, |workspace, window, cx| {
13653 workspace.add_item(
13654 pane.clone(),
13655 Box::new(item.clone()),
13656 None,
13657 false,
13658 false,
13659 window,
13660 cx,
13661 );
13662 });
13663
13664 // Verify the item is in the pane
13665 pane.read_with(cx, |pane, _| {
13666 assert_eq!(pane.items().count(), 1);
13667 });
13668
13669 // Simulate file deletion
13670 item.update(cx, |item, _| {
13671 item.set_has_deleted_file(true);
13672 });
13673
13674 // Emit UpdateTab event
13675 cx.run_until_parked();
13676 item.update(cx, |_, cx| {
13677 cx.emit(ItemEvent::UpdateTab);
13678 });
13679
13680 // Allow any potential close operation to complete
13681 cx.run_until_parked();
13682
13683 // Verify the item remains open (with strikethrough)
13684 pane.read_with(cx, |pane, _| {
13685 assert_eq!(
13686 pane.items().count(),
13687 1,
13688 "Item should remain open when close_on_disk_deletion is disabled"
13689 );
13690 });
13691
13692 // Verify the item shows as deleted
13693 item.read_with(cx, |item, _| {
13694 assert!(
13695 item.has_deleted_file,
13696 "Item should be marked as having deleted file"
13697 );
13698 });
13699 }
13700
13701 /// Tests that dirty files are not automatically closed when deleted from disk,
13702 /// even when `close_on_file_delete` is enabled. This ensures users don't lose
13703 /// unsaved changes without being prompted.
13704 #[gpui::test]
13705 async fn test_close_on_disk_deletion_with_dirty_file(cx: &mut TestAppContext) {
13706 init_test(cx);
13707
13708 // Enable the close_on_file_delete setting
13709 cx.update_global(|store: &mut SettingsStore, cx| {
13710 store.update_user_settings(cx, |settings| {
13711 settings.workspace.close_on_file_delete = Some(true);
13712 });
13713 });
13714
13715 let fs = FakeFs::new(cx.background_executor.clone());
13716 let project = Project::test(fs, [], cx).await;
13717 let (workspace, cx) =
13718 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13719 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13720
13721 // Create a dirty test item
13722 let item = cx.new(|cx| {
13723 TestItem::new(cx)
13724 .with_dirty(true)
13725 .with_label("test.txt")
13726 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13727 });
13728
13729 // Add item to workspace
13730 workspace.update_in(cx, |workspace, window, cx| {
13731 workspace.add_item(
13732 pane.clone(),
13733 Box::new(item.clone()),
13734 None,
13735 false,
13736 false,
13737 window,
13738 cx,
13739 );
13740 });
13741
13742 // Simulate file deletion
13743 item.update(cx, |item, _| {
13744 item.set_has_deleted_file(true);
13745 });
13746
13747 // Emit UpdateTab event to trigger the close behavior
13748 cx.run_until_parked();
13749 item.update(cx, |_, cx| {
13750 cx.emit(ItemEvent::UpdateTab);
13751 });
13752
13753 // Allow any potential close operation to complete
13754 cx.run_until_parked();
13755
13756 // Verify the item remains open (dirty files are not auto-closed)
13757 pane.read_with(cx, |pane, _| {
13758 assert_eq!(
13759 pane.items().count(),
13760 1,
13761 "Dirty items should not be automatically closed even when file is deleted"
13762 );
13763 });
13764
13765 // Verify the item is marked as deleted and still dirty
13766 item.read_with(cx, |item, _| {
13767 assert!(
13768 item.has_deleted_file,
13769 "Item should be marked as having deleted file"
13770 );
13771 assert!(item.is_dirty, "Item should still be dirty");
13772 });
13773 }
13774
13775 /// Tests that navigation history is cleaned up when files are auto-closed
13776 /// due to deletion from disk.
13777 #[gpui::test]
13778 async fn test_close_on_disk_deletion_cleans_navigation_history(cx: &mut TestAppContext) {
13779 init_test(cx);
13780
13781 // Enable the close_on_file_delete setting
13782 cx.update_global(|store: &mut SettingsStore, cx| {
13783 store.update_user_settings(cx, |settings| {
13784 settings.workspace.close_on_file_delete = Some(true);
13785 });
13786 });
13787
13788 let fs = FakeFs::new(cx.background_executor.clone());
13789 let project = Project::test(fs, [], cx).await;
13790 let (workspace, cx) =
13791 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13792 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13793
13794 // Create test items
13795 let item1 = cx.new(|cx| {
13796 TestItem::new(cx)
13797 .with_label("test1.txt")
13798 .with_project_items(&[TestProjectItem::new(1, "test1.txt", cx)])
13799 });
13800 let item1_id = item1.item_id();
13801
13802 let item2 = cx.new(|cx| {
13803 TestItem::new(cx)
13804 .with_label("test2.txt")
13805 .with_project_items(&[TestProjectItem::new(2, "test2.txt", cx)])
13806 });
13807
13808 // Add items to workspace
13809 workspace.update_in(cx, |workspace, window, cx| {
13810 workspace.add_item(
13811 pane.clone(),
13812 Box::new(item1.clone()),
13813 None,
13814 false,
13815 false,
13816 window,
13817 cx,
13818 );
13819 workspace.add_item(
13820 pane.clone(),
13821 Box::new(item2.clone()),
13822 None,
13823 false,
13824 false,
13825 window,
13826 cx,
13827 );
13828 });
13829
13830 // Activate item1 to ensure it gets navigation entries
13831 pane.update_in(cx, |pane, window, cx| {
13832 pane.activate_item(0, true, true, window, cx);
13833 });
13834
13835 // Switch to item2 and back to create navigation history
13836 pane.update_in(cx, |pane, window, cx| {
13837 pane.activate_item(1, true, true, window, cx);
13838 });
13839 cx.run_until_parked();
13840
13841 pane.update_in(cx, |pane, window, cx| {
13842 pane.activate_item(0, true, true, window, cx);
13843 });
13844 cx.run_until_parked();
13845
13846 // Simulate file deletion for item1
13847 item1.update(cx, |item, _| {
13848 item.set_has_deleted_file(true);
13849 });
13850
13851 // Emit UpdateTab event to trigger the close behavior
13852 item1.update(cx, |_, cx| {
13853 cx.emit(ItemEvent::UpdateTab);
13854 });
13855 cx.run_until_parked();
13856
13857 // Verify item1 was closed
13858 pane.read_with(cx, |pane, _| {
13859 assert_eq!(
13860 pane.items().count(),
13861 1,
13862 "Should have 1 item remaining after auto-close"
13863 );
13864 });
13865
13866 // Check navigation history after close
13867 let has_item = pane.read_with(cx, |pane, cx| {
13868 let mut has_item = false;
13869 pane.nav_history().for_each_entry(cx, &mut |entry, _| {
13870 if entry.item.id() == item1_id {
13871 has_item = true;
13872 }
13873 });
13874 has_item
13875 });
13876
13877 assert!(
13878 !has_item,
13879 "Navigation history should not contain closed item entries"
13880 );
13881 }
13882
13883 #[gpui::test]
13884 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
13885 cx: &mut TestAppContext,
13886 ) {
13887 init_test(cx);
13888
13889 let fs = FakeFs::new(cx.background_executor.clone());
13890 let project = Project::test(fs, [], cx).await;
13891 let (workspace, cx) =
13892 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13893 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13894
13895 let dirty_regular_buffer = cx.new(|cx| {
13896 TestItem::new(cx)
13897 .with_dirty(true)
13898 .with_label("1.txt")
13899 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
13900 });
13901 let dirty_regular_buffer_2 = cx.new(|cx| {
13902 TestItem::new(cx)
13903 .with_dirty(true)
13904 .with_label("2.txt")
13905 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
13906 });
13907 let clear_regular_buffer = cx.new(|cx| {
13908 TestItem::new(cx)
13909 .with_label("3.txt")
13910 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
13911 });
13912
13913 let dirty_multi_buffer = cx.new(|cx| {
13914 TestItem::new(cx)
13915 .with_dirty(true)
13916 .with_buffer_kind(ItemBufferKind::Multibuffer)
13917 .with_label("Fake Project Search")
13918 .with_project_items(&[
13919 dirty_regular_buffer.read(cx).project_items[0].clone(),
13920 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
13921 clear_regular_buffer.read(cx).project_items[0].clone(),
13922 ])
13923 });
13924 workspace.update_in(cx, |workspace, window, cx| {
13925 workspace.add_item(
13926 pane.clone(),
13927 Box::new(dirty_regular_buffer.clone()),
13928 None,
13929 false,
13930 false,
13931 window,
13932 cx,
13933 );
13934 workspace.add_item(
13935 pane.clone(),
13936 Box::new(dirty_regular_buffer_2.clone()),
13937 None,
13938 false,
13939 false,
13940 window,
13941 cx,
13942 );
13943 workspace.add_item(
13944 pane.clone(),
13945 Box::new(dirty_multi_buffer.clone()),
13946 None,
13947 false,
13948 false,
13949 window,
13950 cx,
13951 );
13952 });
13953
13954 pane.update_in(cx, |pane, window, cx| {
13955 pane.activate_item(2, true, true, window, cx);
13956 assert_eq!(
13957 pane.active_item().unwrap().item_id(),
13958 dirty_multi_buffer.item_id(),
13959 "Should select the multi buffer in the pane"
13960 );
13961 });
13962 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13963 pane.close_active_item(
13964 &CloseActiveItem {
13965 save_intent: None,
13966 close_pinned: false,
13967 },
13968 window,
13969 cx,
13970 )
13971 });
13972 cx.background_executor.run_until_parked();
13973 assert!(
13974 !cx.has_pending_prompt(),
13975 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
13976 );
13977 close_multi_buffer_task
13978 .await
13979 .expect("Closing multi buffer failed");
13980 pane.update(cx, |pane, cx| {
13981 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
13982 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
13983 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
13984 assert_eq!(
13985 pane.items()
13986 .map(|item| item.item_id())
13987 .sorted()
13988 .collect::<Vec<_>>(),
13989 vec![
13990 dirty_regular_buffer.item_id(),
13991 dirty_regular_buffer_2.item_id(),
13992 ],
13993 "Should have no multi buffer left in the pane"
13994 );
13995 assert!(dirty_regular_buffer.read(cx).is_dirty);
13996 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
13997 });
13998 }
13999
14000 #[gpui::test]
14001 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
14002 init_test(cx);
14003 let fs = FakeFs::new(cx.executor());
14004 let project = Project::test(fs, [], cx).await;
14005 let (multi_workspace, cx) =
14006 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
14007 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
14008
14009 // Add a new panel to the right dock, opening the dock and setting the
14010 // focus to the new panel.
14011 let panel = workspace.update_in(cx, |workspace, window, cx| {
14012 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14013 workspace.add_panel(panel.clone(), window, cx);
14014
14015 workspace
14016 .right_dock()
14017 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
14018
14019 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14020
14021 panel
14022 });
14023
14024 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
14025 // panel to the next valid position which, in this case, is the left
14026 // dock.
14027 cx.dispatch_action(MoveFocusedPanelToNextPosition);
14028 workspace.update(cx, |workspace, cx| {
14029 assert!(workspace.left_dock().read(cx).is_open());
14030 assert_eq!(panel.read(cx).position, DockPosition::Left);
14031 });
14032
14033 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
14034 // panel to the next valid position which, in this case, is the bottom
14035 // dock.
14036 cx.dispatch_action(MoveFocusedPanelToNextPosition);
14037 workspace.update(cx, |workspace, cx| {
14038 assert!(workspace.bottom_dock().read(cx).is_open());
14039 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
14040 });
14041
14042 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
14043 // around moving the panel to its initial position, the right dock.
14044 cx.dispatch_action(MoveFocusedPanelToNextPosition);
14045 workspace.update(cx, |workspace, cx| {
14046 assert!(workspace.right_dock().read(cx).is_open());
14047 assert_eq!(panel.read(cx).position, DockPosition::Right);
14048 });
14049
14050 // Remove focus from the panel, ensuring that, if the panel is not
14051 // focused, the `MoveFocusedPanelToNextPosition` action does not update
14052 // the panel's position, so the panel is still in the right dock.
14053 workspace.update_in(cx, |workspace, window, cx| {
14054 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14055 });
14056
14057 cx.dispatch_action(MoveFocusedPanelToNextPosition);
14058 workspace.update(cx, |workspace, cx| {
14059 assert!(workspace.right_dock().read(cx).is_open());
14060 assert_eq!(panel.read(cx).position, DockPosition::Right);
14061 });
14062 }
14063
14064 #[gpui::test]
14065 async fn test_moving_items_create_panes(cx: &mut TestAppContext) {
14066 init_test(cx);
14067
14068 let fs = FakeFs::new(cx.executor());
14069 let project = Project::test(fs, [], cx).await;
14070 let (workspace, cx) =
14071 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14072
14073 let item_1 = cx.new(|cx| {
14074 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
14075 });
14076 workspace.update_in(cx, |workspace, window, cx| {
14077 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
14078 workspace.move_item_to_pane_in_direction(
14079 &MoveItemToPaneInDirection {
14080 direction: SplitDirection::Right,
14081 focus: true,
14082 clone: false,
14083 },
14084 window,
14085 cx,
14086 );
14087 workspace.move_item_to_pane_at_index(
14088 &MoveItemToPane {
14089 destination: 3,
14090 focus: true,
14091 clone: false,
14092 },
14093 window,
14094 cx,
14095 );
14096
14097 assert_eq!(workspace.panes.len(), 1, "No new panes were created");
14098 assert_eq!(
14099 pane_items_paths(&workspace.active_pane, cx),
14100 vec!["first.txt".to_string()],
14101 "Single item was not moved anywhere"
14102 );
14103 });
14104
14105 let item_2 = cx.new(|cx| {
14106 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "second.txt", cx)])
14107 });
14108 workspace.update_in(cx, |workspace, window, cx| {
14109 workspace.add_item_to_active_pane(Box::new(item_2), None, true, window, cx);
14110 assert_eq!(
14111 pane_items_paths(&workspace.panes[0], cx),
14112 vec!["first.txt".to_string(), "second.txt".to_string()],
14113 );
14114 workspace.move_item_to_pane_in_direction(
14115 &MoveItemToPaneInDirection {
14116 direction: SplitDirection::Right,
14117 focus: true,
14118 clone: false,
14119 },
14120 window,
14121 cx,
14122 );
14123
14124 assert_eq!(workspace.panes.len(), 2, "A new pane should be created");
14125 assert_eq!(
14126 pane_items_paths(&workspace.panes[0], cx),
14127 vec!["first.txt".to_string()],
14128 "After moving, one item should be left in the original pane"
14129 );
14130 assert_eq!(
14131 pane_items_paths(&workspace.panes[1], cx),
14132 vec!["second.txt".to_string()],
14133 "New item should have been moved to the new pane"
14134 );
14135 });
14136
14137 let item_3 = cx.new(|cx| {
14138 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "third.txt", cx)])
14139 });
14140 workspace.update_in(cx, |workspace, window, cx| {
14141 let original_pane = workspace.panes[0].clone();
14142 workspace.set_active_pane(&original_pane, window, cx);
14143 workspace.add_item_to_active_pane(Box::new(item_3), None, true, window, cx);
14144 assert_eq!(workspace.panes.len(), 2, "No new panes were created");
14145 assert_eq!(
14146 pane_items_paths(&workspace.active_pane, cx),
14147 vec!["first.txt".to_string(), "third.txt".to_string()],
14148 "New pane should be ready to move one item out"
14149 );
14150
14151 workspace.move_item_to_pane_at_index(
14152 &MoveItemToPane {
14153 destination: 3,
14154 focus: true,
14155 clone: false,
14156 },
14157 window,
14158 cx,
14159 );
14160 assert_eq!(workspace.panes.len(), 3, "A new pane should be created");
14161 assert_eq!(
14162 pane_items_paths(&workspace.active_pane, cx),
14163 vec!["first.txt".to_string()],
14164 "After moving, one item should be left in the original pane"
14165 );
14166 assert_eq!(
14167 pane_items_paths(&workspace.panes[1], cx),
14168 vec!["second.txt".to_string()],
14169 "Previously created pane should be unchanged"
14170 );
14171 assert_eq!(
14172 pane_items_paths(&workspace.panes[2], cx),
14173 vec!["third.txt".to_string()],
14174 "New item should have been moved to the new pane"
14175 );
14176 });
14177 }
14178
14179 #[gpui::test]
14180 async fn test_moving_items_can_clone_panes(cx: &mut TestAppContext) {
14181 init_test(cx);
14182
14183 let fs = FakeFs::new(cx.executor());
14184 let project = Project::test(fs, [], cx).await;
14185 let (workspace, cx) =
14186 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14187
14188 let item_1 = cx.new(|cx| {
14189 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
14190 });
14191 workspace.update_in(cx, |workspace, window, cx| {
14192 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
14193 workspace.move_item_to_pane_in_direction(
14194 &MoveItemToPaneInDirection {
14195 direction: SplitDirection::Right,
14196 focus: true,
14197 clone: true,
14198 },
14199 window,
14200 cx,
14201 );
14202 });
14203 cx.run_until_parked();
14204 workspace.update_in(cx, |workspace, window, cx| {
14205 workspace.move_item_to_pane_at_index(
14206 &MoveItemToPane {
14207 destination: 3,
14208 focus: true,
14209 clone: true,
14210 },
14211 window,
14212 cx,
14213 );
14214 });
14215 cx.run_until_parked();
14216
14217 workspace.update(cx, |workspace, cx| {
14218 assert_eq!(workspace.panes.len(), 3, "Two new panes were created");
14219 for pane in workspace.panes() {
14220 assert_eq!(
14221 pane_items_paths(pane, cx),
14222 vec!["first.txt".to_string()],
14223 "Single item exists in all panes"
14224 );
14225 }
14226 });
14227
14228 // verify that the active pane has been updated after waiting for the
14229 // pane focus event to fire and resolve
14230 workspace.read_with(cx, |workspace, _app| {
14231 assert_eq!(
14232 workspace.active_pane(),
14233 &workspace.panes[2],
14234 "The third pane should be the active one: {:?}",
14235 workspace.panes
14236 );
14237 })
14238 }
14239
14240 #[gpui::test]
14241 async fn test_close_item_in_all_panes(cx: &mut TestAppContext) {
14242 init_test(cx);
14243
14244 let fs = FakeFs::new(cx.executor());
14245 fs.insert_tree("/root", json!({ "test.txt": "" })).await;
14246
14247 let project = Project::test(fs, ["root".as_ref()], cx).await;
14248 let (workspace, cx) =
14249 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14250
14251 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14252 // Add item to pane A with project path
14253 let item_a = cx.new(|cx| {
14254 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
14255 });
14256 workspace.update_in(cx, |workspace, window, cx| {
14257 workspace.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
14258 });
14259
14260 // Split to create pane B
14261 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
14262 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
14263 });
14264
14265 // Add item with SAME project path to pane B, and pin it
14266 let item_b = cx.new(|cx| {
14267 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
14268 });
14269 pane_b.update_in(cx, |pane, window, cx| {
14270 pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
14271 pane.set_pinned_count(1);
14272 });
14273
14274 assert_eq!(pane_a.read_with(cx, |pane, _| pane.items_len()), 1);
14275 assert_eq!(pane_b.read_with(cx, |pane, _| pane.items_len()), 1);
14276
14277 // close_pinned: false should only close the unpinned copy
14278 workspace.update_in(cx, |workspace, window, cx| {
14279 workspace.close_item_in_all_panes(
14280 &CloseItemInAllPanes {
14281 save_intent: Some(SaveIntent::Close),
14282 close_pinned: false,
14283 },
14284 window,
14285 cx,
14286 )
14287 });
14288 cx.executor().run_until_parked();
14289
14290 let item_count_a = pane_a.read_with(cx, |pane, _| pane.items_len());
14291 let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
14292 assert_eq!(item_count_a, 0, "Unpinned item in pane A should be closed");
14293 assert_eq!(item_count_b, 1, "Pinned item in pane B should remain");
14294
14295 // Split again, seeing as closing the previous item also closed its
14296 // pane, so only pane remains, which does not allow us to properly test
14297 // that both items close when `close_pinned: true`.
14298 let pane_c = workspace.update_in(cx, |workspace, window, cx| {
14299 workspace.split_pane(pane_b.clone(), SplitDirection::Right, window, cx)
14300 });
14301
14302 // Add an item with the same project path to pane C so that
14303 // close_item_in_all_panes can determine what to close across all panes
14304 // (it reads the active item from the active pane, and split_pane
14305 // creates an empty pane).
14306 let item_c = cx.new(|cx| {
14307 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
14308 });
14309 pane_c.update_in(cx, |pane, window, cx| {
14310 pane.add_item(Box::new(item_c.clone()), true, true, None, window, cx);
14311 });
14312
14313 // close_pinned: true should close the pinned copy too
14314 workspace.update_in(cx, |workspace, window, cx| {
14315 let panes_count = workspace.panes().len();
14316 assert_eq!(panes_count, 2, "Workspace should have two panes (B and C)");
14317
14318 workspace.close_item_in_all_panes(
14319 &CloseItemInAllPanes {
14320 save_intent: Some(SaveIntent::Close),
14321 close_pinned: true,
14322 },
14323 window,
14324 cx,
14325 )
14326 });
14327 cx.executor().run_until_parked();
14328
14329 let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
14330 let item_count_c = pane_c.read_with(cx, |pane, _| pane.items_len());
14331 assert_eq!(item_count_b, 0, "Pinned item in pane B should be closed");
14332 assert_eq!(item_count_c, 0, "Unpinned item in pane C should be closed");
14333 }
14334
14335 mod register_project_item_tests {
14336
14337 use super::*;
14338
14339 // View
14340 struct TestPngItemView {
14341 focus_handle: FocusHandle,
14342 }
14343 // Model
14344 struct TestPngItem {}
14345
14346 impl project::ProjectItem for TestPngItem {
14347 fn try_open(
14348 _project: &Entity<Project>,
14349 path: &ProjectPath,
14350 cx: &mut App,
14351 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
14352 if path.path.extension().unwrap() == "png" {
14353 Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestPngItem {}))))
14354 } else {
14355 None
14356 }
14357 }
14358
14359 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
14360 None
14361 }
14362
14363 fn project_path(&self, _: &App) -> Option<ProjectPath> {
14364 None
14365 }
14366
14367 fn is_dirty(&self) -> bool {
14368 false
14369 }
14370 }
14371
14372 impl Item for TestPngItemView {
14373 type Event = ();
14374 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14375 "".into()
14376 }
14377 }
14378 impl EventEmitter<()> for TestPngItemView {}
14379 impl Focusable for TestPngItemView {
14380 fn focus_handle(&self, _cx: &App) -> FocusHandle {
14381 self.focus_handle.clone()
14382 }
14383 }
14384
14385 impl Render for TestPngItemView {
14386 fn render(
14387 &mut self,
14388 _window: &mut Window,
14389 _cx: &mut Context<Self>,
14390 ) -> impl IntoElement {
14391 Empty
14392 }
14393 }
14394
14395 impl ProjectItem for TestPngItemView {
14396 type Item = TestPngItem;
14397
14398 fn for_project_item(
14399 _project: Entity<Project>,
14400 _pane: Option<&Pane>,
14401 _item: Entity<Self::Item>,
14402 _: &mut Window,
14403 cx: &mut Context<Self>,
14404 ) -> Self
14405 where
14406 Self: Sized,
14407 {
14408 Self {
14409 focus_handle: cx.focus_handle(),
14410 }
14411 }
14412 }
14413
14414 // View
14415 struct TestIpynbItemView {
14416 focus_handle: FocusHandle,
14417 }
14418 // Model
14419 struct TestIpynbItem {}
14420
14421 impl project::ProjectItem for TestIpynbItem {
14422 fn try_open(
14423 _project: &Entity<Project>,
14424 path: &ProjectPath,
14425 cx: &mut App,
14426 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
14427 if path.path.extension().unwrap() == "ipynb" {
14428 Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestIpynbItem {}))))
14429 } else {
14430 None
14431 }
14432 }
14433
14434 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
14435 None
14436 }
14437
14438 fn project_path(&self, _: &App) -> Option<ProjectPath> {
14439 None
14440 }
14441
14442 fn is_dirty(&self) -> bool {
14443 false
14444 }
14445 }
14446
14447 impl Item for TestIpynbItemView {
14448 type Event = ();
14449 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14450 "".into()
14451 }
14452 }
14453 impl EventEmitter<()> for TestIpynbItemView {}
14454 impl Focusable for TestIpynbItemView {
14455 fn focus_handle(&self, _cx: &App) -> FocusHandle {
14456 self.focus_handle.clone()
14457 }
14458 }
14459
14460 impl Render for TestIpynbItemView {
14461 fn render(
14462 &mut self,
14463 _window: &mut Window,
14464 _cx: &mut Context<Self>,
14465 ) -> impl IntoElement {
14466 Empty
14467 }
14468 }
14469
14470 impl ProjectItem for TestIpynbItemView {
14471 type Item = TestIpynbItem;
14472
14473 fn for_project_item(
14474 _project: Entity<Project>,
14475 _pane: Option<&Pane>,
14476 _item: Entity<Self::Item>,
14477 _: &mut Window,
14478 cx: &mut Context<Self>,
14479 ) -> Self
14480 where
14481 Self: Sized,
14482 {
14483 Self {
14484 focus_handle: cx.focus_handle(),
14485 }
14486 }
14487 }
14488
14489 struct TestAlternatePngItemView {
14490 focus_handle: FocusHandle,
14491 }
14492
14493 impl Item for TestAlternatePngItemView {
14494 type Event = ();
14495 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14496 "".into()
14497 }
14498 }
14499
14500 impl EventEmitter<()> for TestAlternatePngItemView {}
14501 impl Focusable for TestAlternatePngItemView {
14502 fn focus_handle(&self, _cx: &App) -> FocusHandle {
14503 self.focus_handle.clone()
14504 }
14505 }
14506
14507 impl Render for TestAlternatePngItemView {
14508 fn render(
14509 &mut self,
14510 _window: &mut Window,
14511 _cx: &mut Context<Self>,
14512 ) -> impl IntoElement {
14513 Empty
14514 }
14515 }
14516
14517 impl ProjectItem for TestAlternatePngItemView {
14518 type Item = TestPngItem;
14519
14520 fn for_project_item(
14521 _project: Entity<Project>,
14522 _pane: Option<&Pane>,
14523 _item: Entity<Self::Item>,
14524 _: &mut Window,
14525 cx: &mut Context<Self>,
14526 ) -> Self
14527 where
14528 Self: Sized,
14529 {
14530 Self {
14531 focus_handle: cx.focus_handle(),
14532 }
14533 }
14534 }
14535
14536 #[gpui::test]
14537 async fn test_register_project_item(cx: &mut TestAppContext) {
14538 init_test(cx);
14539
14540 cx.update(|cx| {
14541 register_project_item::<TestPngItemView>(cx);
14542 register_project_item::<TestIpynbItemView>(cx);
14543 });
14544
14545 let fs = FakeFs::new(cx.executor());
14546 fs.insert_tree(
14547 "/root1",
14548 json!({
14549 "one.png": "BINARYDATAHERE",
14550 "two.ipynb": "{ totally a notebook }",
14551 "three.txt": "editing text, sure why not?"
14552 }),
14553 )
14554 .await;
14555
14556 let project = Project::test(fs, ["root1".as_ref()], cx).await;
14557 let (workspace, cx) =
14558 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14559
14560 let worktree_id = project.update(cx, |project, cx| {
14561 project.worktrees(cx).next().unwrap().read(cx).id()
14562 });
14563
14564 let handle = workspace
14565 .update_in(cx, |workspace, window, cx| {
14566 let project_path = (worktree_id, rel_path("one.png"));
14567 workspace.open_path(project_path, None, true, window, cx)
14568 })
14569 .await
14570 .unwrap();
14571
14572 // Now we can check if the handle we got back errored or not
14573 assert_eq!(
14574 handle.to_any_view().entity_type(),
14575 TypeId::of::<TestPngItemView>()
14576 );
14577
14578 let handle = workspace
14579 .update_in(cx, |workspace, window, cx| {
14580 let project_path = (worktree_id, rel_path("two.ipynb"));
14581 workspace.open_path(project_path, None, true, window, cx)
14582 })
14583 .await
14584 .unwrap();
14585
14586 assert_eq!(
14587 handle.to_any_view().entity_type(),
14588 TypeId::of::<TestIpynbItemView>()
14589 );
14590
14591 let handle = workspace
14592 .update_in(cx, |workspace, window, cx| {
14593 let project_path = (worktree_id, rel_path("three.txt"));
14594 workspace.open_path(project_path, None, true, window, cx)
14595 })
14596 .await;
14597 assert!(handle.is_err());
14598 }
14599
14600 #[gpui::test]
14601 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
14602 init_test(cx);
14603
14604 cx.update(|cx| {
14605 register_project_item::<TestPngItemView>(cx);
14606 register_project_item::<TestAlternatePngItemView>(cx);
14607 });
14608
14609 let fs = FakeFs::new(cx.executor());
14610 fs.insert_tree(
14611 "/root1",
14612 json!({
14613 "one.png": "BINARYDATAHERE",
14614 "two.ipynb": "{ totally a notebook }",
14615 "three.txt": "editing text, sure why not?"
14616 }),
14617 )
14618 .await;
14619 let project = Project::test(fs, ["root1".as_ref()], cx).await;
14620 let (workspace, cx) =
14621 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14622 let worktree_id = project.update(cx, |project, cx| {
14623 project.worktrees(cx).next().unwrap().read(cx).id()
14624 });
14625
14626 let handle = workspace
14627 .update_in(cx, |workspace, window, cx| {
14628 let project_path = (worktree_id, rel_path("one.png"));
14629 workspace.open_path(project_path, None, true, window, cx)
14630 })
14631 .await
14632 .unwrap();
14633
14634 // This _must_ be the second item registered
14635 assert_eq!(
14636 handle.to_any_view().entity_type(),
14637 TypeId::of::<TestAlternatePngItemView>()
14638 );
14639
14640 let handle = workspace
14641 .update_in(cx, |workspace, window, cx| {
14642 let project_path = (worktree_id, rel_path("three.txt"));
14643 workspace.open_path(project_path, None, true, window, cx)
14644 })
14645 .await;
14646 assert!(handle.is_err());
14647 }
14648 }
14649
14650 #[gpui::test]
14651 async fn test_status_bar_visibility(cx: &mut TestAppContext) {
14652 init_test(cx);
14653
14654 let fs = FakeFs::new(cx.executor());
14655 let project = Project::test(fs, [], cx).await;
14656 let (workspace, _cx) =
14657 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14658
14659 // Test with status bar shown (default)
14660 workspace.read_with(cx, |workspace, cx| {
14661 let visible = workspace.status_bar_visible(cx);
14662 assert!(visible, "Status bar should be visible by default");
14663 });
14664
14665 // Test with status bar hidden
14666 cx.update_global(|store: &mut SettingsStore, cx| {
14667 store.update_user_settings(cx, |settings| {
14668 settings.status_bar.get_or_insert_default().show = Some(false);
14669 });
14670 });
14671
14672 workspace.read_with(cx, |workspace, cx| {
14673 let visible = workspace.status_bar_visible(cx);
14674 assert!(!visible, "Status bar should be hidden when show is false");
14675 });
14676
14677 // Test with status bar shown explicitly
14678 cx.update_global(|store: &mut SettingsStore, cx| {
14679 store.update_user_settings(cx, |settings| {
14680 settings.status_bar.get_or_insert_default().show = Some(true);
14681 });
14682 });
14683
14684 workspace.read_with(cx, |workspace, cx| {
14685 let visible = workspace.status_bar_visible(cx);
14686 assert!(visible, "Status bar should be visible when show is true");
14687 });
14688 }
14689
14690 #[gpui::test]
14691 async fn test_pane_close_active_item(cx: &mut TestAppContext) {
14692 init_test(cx);
14693
14694 let fs = FakeFs::new(cx.executor());
14695 let project = Project::test(fs, [], cx).await;
14696 let (multi_workspace, cx) =
14697 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
14698 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
14699 let panel = workspace.update_in(cx, |workspace, window, cx| {
14700 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14701 workspace.add_panel(panel.clone(), window, cx);
14702
14703 workspace
14704 .right_dock()
14705 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
14706
14707 panel
14708 });
14709
14710 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14711 let item_a = cx.new(TestItem::new);
14712 let item_b = cx.new(TestItem::new);
14713 let item_a_id = item_a.entity_id();
14714 let item_b_id = item_b.entity_id();
14715
14716 pane.update_in(cx, |pane, window, cx| {
14717 pane.add_item(Box::new(item_a.clone()), true, true, None, window, cx);
14718 pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
14719 });
14720
14721 pane.read_with(cx, |pane, _| {
14722 assert_eq!(pane.items_len(), 2);
14723 assert_eq!(pane.active_item().unwrap().item_id(), item_b_id);
14724 });
14725
14726 workspace.update_in(cx, |workspace, window, cx| {
14727 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14728 });
14729
14730 workspace.update_in(cx, |_, window, cx| {
14731 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
14732 });
14733
14734 // Assert that the `pane::CloseActiveItem` action is handled at the
14735 // workspace level when one of the dock panels is focused and, in that
14736 // case, the center pane's active item is closed but the focus is not
14737 // moved.
14738 cx.dispatch_action(pane::CloseActiveItem::default());
14739 cx.run_until_parked();
14740
14741 pane.read_with(cx, |pane, _| {
14742 assert_eq!(pane.items_len(), 1);
14743 assert_eq!(pane.active_item().unwrap().item_id(), item_a_id);
14744 });
14745
14746 workspace.update_in(cx, |workspace, window, cx| {
14747 assert!(workspace.right_dock().read(cx).is_open());
14748 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
14749 });
14750 }
14751
14752 #[gpui::test]
14753 async fn test_panel_zoom_preserved_across_workspace_switch(cx: &mut TestAppContext) {
14754 init_test(cx);
14755 let fs = FakeFs::new(cx.executor());
14756
14757 let project_a = Project::test(fs.clone(), [], cx).await;
14758 let project_b = Project::test(fs, [], cx).await;
14759
14760 let multi_workspace_handle =
14761 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
14762 cx.run_until_parked();
14763
14764 multi_workspace_handle
14765 .update(cx, |mw, _window, cx| {
14766 mw.open_sidebar(cx);
14767 })
14768 .unwrap();
14769
14770 let workspace_a = multi_workspace_handle
14771 .read_with(cx, |mw, _| mw.workspace().clone())
14772 .unwrap();
14773
14774 let _workspace_b = multi_workspace_handle
14775 .update(cx, |mw, window, cx| {
14776 mw.test_add_workspace(project_b, window, cx)
14777 })
14778 .unwrap();
14779
14780 // Switch to workspace A
14781 multi_workspace_handle
14782 .update(cx, |mw, window, cx| {
14783 let workspace = mw.workspaces().next().unwrap().clone();
14784 mw.activate(workspace, window, cx);
14785 })
14786 .unwrap();
14787
14788 let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
14789
14790 // Add a panel to workspace A's right dock and open the dock
14791 let panel = workspace_a.update_in(cx, |workspace, window, cx| {
14792 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14793 workspace.add_panel(panel.clone(), window, cx);
14794 workspace
14795 .right_dock()
14796 .update(cx, |dock, cx| dock.set_open(true, window, cx));
14797 panel
14798 });
14799
14800 // Focus the panel through the workspace (matching existing test pattern)
14801 workspace_a.update_in(cx, |workspace, window, cx| {
14802 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14803 });
14804
14805 // Zoom the panel
14806 panel.update_in(cx, |panel, window, cx| {
14807 panel.set_zoomed(true, window, cx);
14808 });
14809
14810 // Verify the panel is zoomed and the dock is open
14811 workspace_a.update_in(cx, |workspace, window, cx| {
14812 assert!(
14813 workspace.right_dock().read(cx).is_open(),
14814 "dock should be open before switch"
14815 );
14816 assert!(
14817 panel.is_zoomed(window, cx),
14818 "panel should be zoomed before switch"
14819 );
14820 assert!(
14821 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
14822 "panel should be focused before switch"
14823 );
14824 });
14825
14826 // Switch to workspace B
14827 multi_workspace_handle
14828 .update(cx, |mw, window, cx| {
14829 let workspace = mw.workspaces().nth(1).unwrap().clone();
14830 mw.activate(workspace, window, cx);
14831 })
14832 .unwrap();
14833 cx.run_until_parked();
14834
14835 // Switch back to workspace A
14836 multi_workspace_handle
14837 .update(cx, |mw, window, cx| {
14838 let workspace = mw.workspaces().next().unwrap().clone();
14839 mw.activate(workspace, window, cx);
14840 })
14841 .unwrap();
14842 cx.run_until_parked();
14843
14844 // Verify the panel is still zoomed and the dock is still open
14845 workspace_a.update_in(cx, |workspace, window, cx| {
14846 assert!(
14847 workspace.right_dock().read(cx).is_open(),
14848 "dock should still be open after switching back"
14849 );
14850 assert!(
14851 panel.is_zoomed(window, cx),
14852 "panel should still be zoomed after switching back"
14853 );
14854 });
14855 }
14856
14857 fn pane_items_paths(pane: &Entity<Pane>, cx: &App) -> Vec<String> {
14858 pane.read(cx)
14859 .items()
14860 .flat_map(|item| {
14861 item.project_paths(cx)
14862 .into_iter()
14863 .map(|path| path.path.display(PathStyle::local()).into_owned())
14864 })
14865 .collect()
14866 }
14867
14868 pub fn init_test(cx: &mut TestAppContext) {
14869 cx.update(|cx| {
14870 let settings_store = SettingsStore::test(cx);
14871 cx.set_global(settings_store);
14872 cx.set_global(db::AppDatabase::test_new());
14873 theme_settings::init(theme::LoadThemes::JustBase, cx);
14874 });
14875 }
14876
14877 #[gpui::test]
14878 async fn test_toggle_theme_mode_persists_and_updates_active_theme(cx: &mut TestAppContext) {
14879 use settings::{ThemeName, ThemeSelection};
14880 use theme::SystemAppearance;
14881 use zed_actions::theme::ToggleMode;
14882
14883 init_test(cx);
14884
14885 let fs = FakeFs::new(cx.executor());
14886 let settings_fs: Arc<dyn fs::Fs> = fs.clone();
14887
14888 fs.insert_tree(path!("/root"), json!({ "file.rs": "fn main() {}\n" }))
14889 .await;
14890
14891 // Build a test project and workspace view so the test can invoke
14892 // the workspace action handler the same way the UI would.
14893 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
14894 let (workspace, cx) =
14895 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14896
14897 // Seed the settings file with a plain static light theme so the
14898 // first toggle always starts from a known persisted state.
14899 workspace.update_in(cx, |_workspace, _window, cx| {
14900 *SystemAppearance::global_mut(cx) = SystemAppearance(theme::Appearance::Light);
14901 settings::update_settings_file(settings_fs.clone(), cx, |settings, _cx| {
14902 settings.theme.theme = Some(ThemeSelection::Static(ThemeName("One Light".into())));
14903 });
14904 });
14905 cx.executor().advance_clock(Duration::from_millis(200));
14906 cx.run_until_parked();
14907
14908 // Confirm the initial persisted settings contain the static theme
14909 // we just wrote before any toggling happens.
14910 let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
14911 assert!(settings_text.contains(r#""theme": "One Light""#));
14912
14913 // Toggle once. This should migrate the persisted theme settings
14914 // into light/dark slots and enable system mode.
14915 workspace.update_in(cx, |workspace, window, cx| {
14916 workspace.toggle_theme_mode(&ToggleMode, window, cx);
14917 });
14918 cx.executor().advance_clock(Duration::from_millis(200));
14919 cx.run_until_parked();
14920
14921 // 1. Static -> Dynamic
14922 // this assertion checks theme changed from static to dynamic.
14923 let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
14924 let parsed: serde_json::Value = settings::parse_json_with_comments(&settings_text).unwrap();
14925 assert_eq!(
14926 parsed["theme"],
14927 serde_json::json!({
14928 "mode": "system",
14929 "light": "One Light",
14930 "dark": "One Dark"
14931 })
14932 );
14933
14934 // 2. Toggle again, suppose it will change the mode to light
14935 workspace.update_in(cx, |workspace, window, cx| {
14936 workspace.toggle_theme_mode(&ToggleMode, window, cx);
14937 });
14938 cx.executor().advance_clock(Duration::from_millis(200));
14939 cx.run_until_parked();
14940
14941 let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
14942 assert!(settings_text.contains(r#""mode": "light""#));
14943 }
14944
14945 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
14946 let item = TestProjectItem::new(id, path, cx);
14947 item.update(cx, |item, _| {
14948 item.is_dirty = true;
14949 });
14950 item
14951 }
14952
14953 #[gpui::test]
14954 async fn test_zoomed_panel_without_pane_preserved_on_center_focus(
14955 cx: &mut gpui::TestAppContext,
14956 ) {
14957 init_test(cx);
14958 let fs = FakeFs::new(cx.executor());
14959
14960 let project = Project::test(fs, [], cx).await;
14961 let (workspace, cx) =
14962 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
14963
14964 let panel = workspace.update_in(cx, |workspace, window, cx| {
14965 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14966 workspace.add_panel(panel.clone(), window, cx);
14967 workspace
14968 .right_dock()
14969 .update(cx, |dock, cx| dock.set_open(true, window, cx));
14970 panel
14971 });
14972
14973 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14974 pane.update_in(cx, |pane, window, cx| {
14975 let item = cx.new(TestItem::new);
14976 pane.add_item(Box::new(item), true, true, None, window, cx);
14977 });
14978
14979 // Transfer focus to the panel, then zoom it. Using toggle_panel_focus
14980 // mirrors the real-world flow and avoids side effects from directly
14981 // focusing the panel while the center pane is active.
14982 workspace.update_in(cx, |workspace, window, cx| {
14983 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14984 });
14985
14986 panel.update_in(cx, |panel, window, cx| {
14987 panel.set_zoomed(true, window, cx);
14988 });
14989
14990 workspace.update_in(cx, |workspace, window, cx| {
14991 assert!(workspace.right_dock().read(cx).is_open());
14992 assert!(panel.is_zoomed(window, cx));
14993 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
14994 });
14995
14996 // Simulate a spurious pane::Event::Focus on the center pane while the
14997 // panel still has focus. This mirrors what happens during macOS window
14998 // activation: the center pane fires a focus event even though actual
14999 // focus remains on the dock panel.
15000 pane.update_in(cx, |_, _, cx| {
15001 cx.emit(pane::Event::Focus);
15002 });
15003
15004 // The dock must remain open because the panel had focus at the time the
15005 // event was processed. Before the fix, dock_to_preserve was None for
15006 // panels that don't implement pane(), causing the dock to close.
15007 workspace.update_in(cx, |workspace, window, cx| {
15008 assert!(
15009 workspace.right_dock().read(cx).is_open(),
15010 "Dock should stay open when its zoomed panel (without pane()) still has focus"
15011 );
15012 assert!(panel.is_zoomed(window, cx));
15013 });
15014 }
15015
15016 #[gpui::test]
15017 async fn test_panels_stay_open_after_position_change_and_settings_update(
15018 cx: &mut gpui::TestAppContext,
15019 ) {
15020 init_test(cx);
15021 let fs = FakeFs::new(cx.executor());
15022 let project = Project::test(fs, [], cx).await;
15023 let (workspace, cx) =
15024 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
15025
15026 // Add two panels to the left dock and open it.
15027 let (panel_a, panel_b) = workspace.update_in(cx, |workspace, window, cx| {
15028 let panel_a = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
15029 let panel_b = cx.new(|cx| TestPanel::new(DockPosition::Left, 101, cx));
15030 workspace.add_panel(panel_a.clone(), window, cx);
15031 workspace.add_panel(panel_b.clone(), window, cx);
15032 workspace.left_dock().update(cx, |dock, cx| {
15033 dock.set_open(true, window, cx);
15034 dock.activate_panel(0, window, cx);
15035 });
15036 (panel_a, panel_b)
15037 });
15038
15039 workspace.update_in(cx, |workspace, _, cx| {
15040 assert!(workspace.left_dock().read(cx).is_open());
15041 });
15042
15043 // Simulate a feature flag changing default dock positions: both panels
15044 // move from Left to Right.
15045 workspace.update_in(cx, |_workspace, _window, cx| {
15046 panel_a.update(cx, |p, _cx| p.position = DockPosition::Right);
15047 panel_b.update(cx, |p, _cx| p.position = DockPosition::Right);
15048 cx.update_global::<SettingsStore, _>(|_, _| {});
15049 });
15050
15051 // Both panels should now be in the right dock.
15052 workspace.update_in(cx, |workspace, _, cx| {
15053 let right_dock = workspace.right_dock().read(cx);
15054 assert_eq!(right_dock.panels_len(), 2);
15055 });
15056
15057 // Open the right dock and activate panel_b (simulating the user
15058 // opening the panel after it moved).
15059 workspace.update_in(cx, |workspace, window, cx| {
15060 workspace.right_dock().update(cx, |dock, cx| {
15061 dock.set_open(true, window, cx);
15062 dock.activate_panel(1, window, cx);
15063 });
15064 });
15065
15066 // Now trigger another SettingsStore change
15067 workspace.update_in(cx, |_workspace, _window, cx| {
15068 cx.update_global::<SettingsStore, _>(|_, _| {});
15069 });
15070
15071 workspace.update_in(cx, |workspace, _, cx| {
15072 assert!(
15073 workspace.right_dock().read(cx).is_open(),
15074 "Right dock should still be open after a settings change"
15075 );
15076 assert_eq!(
15077 workspace.right_dock().read(cx).panels_len(),
15078 2,
15079 "Both panels should still be in the right dock"
15080 );
15081 });
15082 }
15083}