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 SerializedProjectGroupKey, SerializedWorkspaceLocation, SessionWorkspace,
90 },
91 read_serialized_multi_workspaces, resolve_worktree_workspaces,
92};
93use postage::stream::Stream;
94use project::{
95 DirectoryLister, Project, ProjectEntryId, ProjectGroupKey, ProjectPath, ResolvedPath, Worktree,
96 WorktreeId, WorktreeSettings,
97 debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
98 project_settings::ProjectSettings,
99 toolchain_store::ToolchainStoreEvent,
100 trusted_worktrees::{RemoteHostLocation, TrustedWorktrees, TrustedWorktreesEvent},
101};
102use remote::{
103 RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions,
104 remote_client::ConnectionIdentifier,
105};
106use schemars::JsonSchema;
107use serde::Deserialize;
108use session::AppSession;
109use settings::{
110 CenteredPaddingSettings, Settings, SettingsLocation, SettingsStore, update_settings_file,
111};
112
113use sqlez::{
114 bindable::{Bind, Column, StaticColumnCount},
115 statement::Statement,
116};
117use status_bar::StatusBar;
118pub use status_bar::StatusItemView;
119use std::{
120 any::TypeId,
121 borrow::Cow,
122 cell::RefCell,
123 cmp,
124 collections::VecDeque,
125 env,
126 hash::Hash,
127 path::{Path, PathBuf},
128 process::ExitStatus,
129 rc::Rc,
130 sync::{
131 Arc, LazyLock,
132 atomic::{AtomicBool, AtomicUsize},
133 },
134 time::Duration,
135};
136use task::{DebugScenario, SharedTaskContext, SpawnInTerminal};
137use theme::{ActiveTheme, SystemAppearance};
138use theme_settings::ThemeSettings;
139pub use toolbar::{
140 PaneSearchBarCallbacks, Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
141};
142pub use ui;
143use ui::{Window, prelude::*};
144use util::{
145 ResultExt, TryFutureExt,
146 paths::{PathStyle, SanitizedPath},
147 rel_path::RelPath,
148 serde::default_true,
149};
150use uuid::Uuid;
151pub use workspace_settings::{
152 AutosaveSetting, BottomDockLayout, FocusFollowsMouse, RestoreOnStartupBehavior,
153 StatusBarSettings, TabBarSettings, WorkspaceSettings,
154};
155use zed_actions::{Spawn, feedback::FileBugReport, theme::ToggleMode};
156
157use crate::{dock::PanelSizeState, item::ItemBufferKind, notifications::NotificationId};
158use crate::{
159 persistence::{
160 SerializedAxis,
161 model::{DockData, SerializedItem, SerializedPane, SerializedPaneGroup},
162 },
163 security_modal::SecurityModal,
164};
165
166pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200);
167
168static ZED_WINDOW_SIZE: LazyLock<Option<Size<Pixels>>> = LazyLock::new(|| {
169 env::var("ZED_WINDOW_SIZE")
170 .ok()
171 .as_deref()
172 .and_then(parse_pixel_size_env_var)
173});
174
175static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
176 env::var("ZED_WINDOW_POSITION")
177 .ok()
178 .as_deref()
179 .and_then(parse_pixel_position_env_var)
180});
181
182pub trait TerminalProvider {
183 fn spawn(
184 &self,
185 task: SpawnInTerminal,
186 window: &mut Window,
187 cx: &mut App,
188 ) -> Task<Option<Result<ExitStatus>>>;
189}
190
191pub trait DebuggerProvider {
192 // `active_buffer` is used to resolve build task's name against language-specific tasks.
193 fn start_session(
194 &self,
195 definition: DebugScenario,
196 task_context: SharedTaskContext,
197 active_buffer: Option<Entity<Buffer>>,
198 worktree_id: Option<WorktreeId>,
199 window: &mut Window,
200 cx: &mut App,
201 );
202
203 fn spawn_task_or_modal(
204 &self,
205 workspace: &mut Workspace,
206 action: &Spawn,
207 window: &mut Window,
208 cx: &mut Context<Workspace>,
209 );
210
211 fn task_scheduled(&self, cx: &mut App);
212 fn debug_scenario_scheduled(&self, cx: &mut App);
213 fn debug_scenario_scheduled_last(&self, cx: &App) -> bool;
214
215 fn active_thread_state(&self, cx: &App) -> Option<ThreadStatus>;
216}
217
218/// Opens a file or directory.
219#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
220#[action(namespace = workspace)]
221pub struct Open {
222 /// When true, opens in a new window. When false, adds to the current
223 /// window as a new workspace (multi-workspace).
224 #[serde(default = "Open::default_create_new_window")]
225 pub create_new_window: bool,
226}
227
228impl Open {
229 pub const DEFAULT: Self = Self {
230 create_new_window: true,
231 };
232
233 /// Used by `#[serde(default)]` on the `create_new_window` field so that
234 /// the serde default and `Open::DEFAULT` stay in sync.
235 fn default_create_new_window() -> bool {
236 Self::DEFAULT.create_new_window
237 }
238}
239
240impl Default for Open {
241 fn default() -> Self {
242 Self::DEFAULT
243 }
244}
245
246actions!(
247 workspace,
248 [
249 /// Activates the next pane in the workspace.
250 ActivateNextPane,
251 /// Activates the previous pane in the workspace.
252 ActivatePreviousPane,
253 /// Activates the last pane in the workspace.
254 ActivateLastPane,
255 /// Switches to the next window.
256 ActivateNextWindow,
257 /// Switches to the previous window.
258 ActivatePreviousWindow,
259 /// Adds a folder to the current project.
260 AddFolderToProject,
261 /// Clears all notifications.
262 ClearAllNotifications,
263 /// Clears all navigation history, including forward/backward navigation, recently opened files, and recently closed tabs. **This action is irreversible**.
264 ClearNavigationHistory,
265 /// Closes the active dock.
266 CloseActiveDock,
267 /// Closes all docks.
268 CloseAllDocks,
269 /// Toggles all docks.
270 ToggleAllDocks,
271 /// Closes the current window.
272 CloseWindow,
273 /// Closes the current project.
274 CloseProject,
275 /// Opens the feedback dialog.
276 Feedback,
277 /// Follows the next collaborator in the session.
278 FollowNextCollaborator,
279 /// Moves the focused panel to the next position.
280 MoveFocusedPanelToNextPosition,
281 /// Creates a new file.
282 NewFile,
283 /// Creates a new file in a vertical split.
284 NewFileSplitVertical,
285 /// Creates a new file in a horizontal split.
286 NewFileSplitHorizontal,
287 /// Opens a new search.
288 NewSearch,
289 /// Opens a new window.
290 NewWindow,
291 /// Opens multiple files.
292 OpenFiles,
293 /// Opens the current location in terminal.
294 OpenInTerminal,
295 /// Opens the component preview.
296 OpenComponentPreview,
297 /// Reloads the active item.
298 ReloadActiveItem,
299 /// Resets the active dock to its default size.
300 ResetActiveDockSize,
301 /// Resets all open docks to their default sizes.
302 ResetOpenDocksSize,
303 /// Reloads the application
304 Reload,
305 /// Saves the current file with a new name.
306 SaveAs,
307 /// Saves without formatting.
308 SaveWithoutFormat,
309 /// Shuts down all debug adapters.
310 ShutdownDebugAdapters,
311 /// Suppresses the current notification.
312 SuppressNotification,
313 /// Toggles the bottom dock.
314 ToggleBottomDock,
315 /// Toggles centered layout mode.
316 ToggleCenteredLayout,
317 /// Toggles edit prediction feature globally for all files.
318 ToggleEditPrediction,
319 /// Toggles the left dock.
320 ToggleLeftDock,
321 /// Toggles the right dock.
322 ToggleRightDock,
323 /// Toggles zoom on the active pane.
324 ToggleZoom,
325 /// Toggles read-only mode for the active item (if supported by that item).
326 ToggleReadOnlyFile,
327 /// Zooms in on the active pane.
328 ZoomIn,
329 /// Zooms out of the active pane.
330 ZoomOut,
331 /// If any worktrees are in restricted mode, shows a modal with possible actions.
332 /// If the modal is shown already, closes it without trusting any worktree.
333 ToggleWorktreeSecurity,
334 /// Clears all trusted worktrees, placing them in restricted mode on next open.
335 /// Requires restart to take effect on already opened projects.
336 ClearTrustedWorktrees,
337 /// Stops following a collaborator.
338 Unfollow,
339 /// Restores the banner.
340 RestoreBanner,
341 /// Toggles expansion of the selected item.
342 ToggleExpandItem,
343 ]
344);
345
346/// Activates a specific pane by its index.
347#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
348#[action(namespace = workspace)]
349pub struct ActivatePane(pub usize);
350
351/// Moves an item to a specific pane by index.
352#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
353#[action(namespace = workspace)]
354#[serde(deny_unknown_fields)]
355pub struct MoveItemToPane {
356 #[serde(default = "default_1")]
357 pub destination: usize,
358 #[serde(default = "default_true")]
359 pub focus: bool,
360 #[serde(default)]
361 pub clone: bool,
362}
363
364fn default_1() -> usize {
365 1
366}
367
368/// Moves an item to a pane in the specified direction.
369#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
370#[action(namespace = workspace)]
371#[serde(deny_unknown_fields)]
372pub struct MoveItemToPaneInDirection {
373 #[serde(default = "default_right")]
374 pub direction: SplitDirection,
375 #[serde(default = "default_true")]
376 pub focus: bool,
377 #[serde(default)]
378 pub clone: bool,
379}
380
381/// Creates a new file in a split of the desired direction.
382#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
383#[action(namespace = workspace)]
384#[serde(deny_unknown_fields)]
385pub struct NewFileSplit(pub SplitDirection);
386
387fn default_right() -> SplitDirection {
388 SplitDirection::Right
389}
390
391/// Saves all open files in the workspace.
392#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
393#[action(namespace = workspace)]
394#[serde(deny_unknown_fields)]
395pub struct SaveAll {
396 #[serde(default)]
397 pub save_intent: Option<SaveIntent>,
398}
399
400/// Saves the current file with the specified options.
401#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
402#[action(namespace = workspace)]
403#[serde(deny_unknown_fields)]
404pub struct Save {
405 #[serde(default)]
406 pub save_intent: Option<SaveIntent>,
407}
408
409/// Moves Focus to the central panes in the workspace.
410#[derive(Clone, Debug, PartialEq, Eq, Action)]
411#[action(namespace = workspace)]
412pub struct FocusCenterPane;
413
414/// Closes all items and panes in the workspace.
415#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
416#[action(namespace = workspace)]
417#[serde(deny_unknown_fields)]
418pub struct CloseAllItemsAndPanes {
419 #[serde(default)]
420 pub save_intent: Option<SaveIntent>,
421}
422
423/// Closes all inactive tabs and panes in the workspace.
424#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
425#[action(namespace = workspace)]
426#[serde(deny_unknown_fields)]
427pub struct CloseInactiveTabsAndPanes {
428 #[serde(default)]
429 pub save_intent: Option<SaveIntent>,
430}
431
432/// Closes the active item across all panes.
433#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
434#[action(namespace = workspace)]
435#[serde(deny_unknown_fields)]
436pub struct CloseItemInAllPanes {
437 #[serde(default)]
438 pub save_intent: Option<SaveIntent>,
439 #[serde(default)]
440 pub close_pinned: bool,
441}
442
443/// Sends a sequence of keystrokes to the active element.
444#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
445#[action(namespace = workspace)]
446pub struct SendKeystrokes(pub String);
447
448actions!(
449 project_symbols,
450 [
451 /// Toggles the project symbols search.
452 #[action(name = "Toggle")]
453 ToggleProjectSymbols
454 ]
455);
456
457/// Toggles the file finder interface.
458#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
459#[action(namespace = file_finder, name = "Toggle")]
460#[serde(deny_unknown_fields)]
461pub struct ToggleFileFinder {
462 #[serde(default)]
463 pub separate_history: bool,
464}
465
466/// Opens a new terminal in the center.
467#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
468#[action(namespace = workspace)]
469#[serde(deny_unknown_fields)]
470pub struct NewCenterTerminal {
471 /// If true, creates a local terminal even in remote projects.
472 #[serde(default)]
473 pub local: bool,
474}
475
476/// Opens a new terminal.
477#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
478#[action(namespace = workspace)]
479#[serde(deny_unknown_fields)]
480pub struct NewTerminal {
481 /// If true, creates a local terminal even in remote projects.
482 #[serde(default)]
483 pub local: bool,
484}
485
486/// Increases size of a currently focused dock by a given amount of pixels.
487#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
488#[action(namespace = workspace)]
489#[serde(deny_unknown_fields)]
490pub struct IncreaseActiveDockSize {
491 /// For 0px parameter, uses UI font size value.
492 #[serde(default)]
493 pub px: u32,
494}
495
496/// Decreases size of a currently focused dock by a given amount of pixels.
497#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
498#[action(namespace = workspace)]
499#[serde(deny_unknown_fields)]
500pub struct DecreaseActiveDockSize {
501 /// For 0px parameter, uses UI font size value.
502 #[serde(default)]
503 pub px: u32,
504}
505
506/// Increases size of all currently visible docks uniformly, by a given amount of pixels.
507#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
508#[action(namespace = workspace)]
509#[serde(deny_unknown_fields)]
510pub struct IncreaseOpenDocksSize {
511 /// For 0px parameter, uses UI font size value.
512 #[serde(default)]
513 pub px: u32,
514}
515
516/// Decreases size of all currently visible docks uniformly, by a given amount of pixels.
517#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
518#[action(namespace = workspace)]
519#[serde(deny_unknown_fields)]
520pub struct DecreaseOpenDocksSize {
521 /// For 0px parameter, uses UI font size value.
522 #[serde(default)]
523 pub px: u32,
524}
525
526actions!(
527 workspace,
528 [
529 /// Activates the pane to the left.
530 ActivatePaneLeft,
531 /// Activates the pane to the right.
532 ActivatePaneRight,
533 /// Activates the pane above.
534 ActivatePaneUp,
535 /// Activates the pane below.
536 ActivatePaneDown,
537 /// Swaps the current pane with the one to the left.
538 SwapPaneLeft,
539 /// Swaps the current pane with the one to the right.
540 SwapPaneRight,
541 /// Swaps the current pane with the one above.
542 SwapPaneUp,
543 /// Swaps the current pane with the one below.
544 SwapPaneDown,
545 // Swaps the current pane with the first available adjacent pane (searching in order: below, above, right, left) and activates that pane.
546 SwapPaneAdjacent,
547 /// Move the current pane to be at the far left.
548 MovePaneLeft,
549 /// Move the current pane to be at the far right.
550 MovePaneRight,
551 /// Move the current pane to be at the very top.
552 MovePaneUp,
553 /// Move the current pane to be at the very bottom.
554 MovePaneDown,
555 ]
556);
557
558#[derive(PartialEq, Eq, Debug)]
559pub enum CloseIntent {
560 /// Quit the program entirely.
561 Quit,
562 /// Close a window.
563 CloseWindow,
564 /// Replace the workspace in an existing window.
565 ReplaceWindow,
566}
567
568#[derive(Clone)]
569pub struct Toast {
570 id: NotificationId,
571 msg: Cow<'static, str>,
572 autohide: bool,
573 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut Window, &mut App)>)>,
574}
575
576impl Toast {
577 pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
578 Toast {
579 id,
580 msg: msg.into(),
581 on_click: None,
582 autohide: false,
583 }
584 }
585
586 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
587 where
588 M: Into<Cow<'static, str>>,
589 F: Fn(&mut Window, &mut App) + 'static,
590 {
591 self.on_click = Some((message.into(), Arc::new(on_click)));
592 self
593 }
594
595 pub fn autohide(mut self) -> Self {
596 self.autohide = true;
597 self
598 }
599}
600
601impl PartialEq for Toast {
602 fn eq(&self, other: &Self) -> bool {
603 self.id == other.id
604 && self.msg == other.msg
605 && self.on_click.is_some() == other.on_click.is_some()
606 }
607}
608
609/// Opens a new terminal with the specified working directory.
610#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema, Action)]
611#[action(namespace = workspace)]
612#[serde(deny_unknown_fields)]
613pub struct OpenTerminal {
614 pub working_directory: PathBuf,
615 /// If true, creates a local terminal even in remote projects.
616 #[serde(default)]
617 pub local: bool,
618}
619
620#[derive(
621 Clone,
622 Copy,
623 Debug,
624 Default,
625 Hash,
626 PartialEq,
627 Eq,
628 PartialOrd,
629 Ord,
630 serde::Serialize,
631 serde::Deserialize,
632)]
633pub struct WorkspaceId(i64);
634
635impl WorkspaceId {
636 pub fn from_i64(value: i64) -> Self {
637 Self(value)
638 }
639}
640
641impl StaticColumnCount for WorkspaceId {}
642impl Bind for WorkspaceId {
643 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
644 self.0.bind(statement, start_index)
645 }
646}
647impl Column for WorkspaceId {
648 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
649 i64::column(statement, start_index)
650 .map(|(i, next_index)| (Self(i), next_index))
651 .with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
652 }
653}
654impl From<WorkspaceId> for i64 {
655 fn from(val: WorkspaceId) -> Self {
656 val.0
657 }
658}
659
660fn prompt_and_open_paths(
661 app_state: Arc<AppState>,
662 options: PathPromptOptions,
663 create_new_window: bool,
664 cx: &mut App,
665) {
666 if let Some(workspace_window) = local_workspace_windows(cx).into_iter().next() {
667 workspace_window
668 .update(cx, |multi_workspace, window, cx| {
669 let workspace = multi_workspace.workspace().clone();
670 workspace.update(cx, |workspace, cx| {
671 prompt_for_open_path_and_open(
672 workspace,
673 app_state,
674 options,
675 create_new_window,
676 window,
677 cx,
678 );
679 });
680 })
681 .ok();
682 } else {
683 let task = Workspace::new_local(
684 Vec::new(),
685 app_state.clone(),
686 None,
687 None,
688 None,
689 OpenMode::Activate,
690 cx,
691 );
692 cx.spawn(async move |cx| {
693 let OpenResult { window, .. } = task.await?;
694 window.update(cx, |multi_workspace, window, cx| {
695 window.activate_window();
696 let workspace = multi_workspace.workspace().clone();
697 workspace.update(cx, |workspace, cx| {
698 prompt_for_open_path_and_open(
699 workspace,
700 app_state,
701 options,
702 create_new_window,
703 window,
704 cx,
705 );
706 });
707 })?;
708 anyhow::Ok(())
709 })
710 .detach_and_log_err(cx);
711 }
712}
713
714pub fn prompt_for_open_path_and_open(
715 workspace: &mut Workspace,
716 app_state: Arc<AppState>,
717 options: PathPromptOptions,
718 create_new_window: bool,
719 window: &mut Window,
720 cx: &mut Context<Workspace>,
721) {
722 let paths = workspace.prompt_for_open_path(
723 options,
724 DirectoryLister::Local(workspace.project().clone(), app_state.fs.clone()),
725 window,
726 cx,
727 );
728 let multi_workspace_handle = window.window_handle().downcast::<MultiWorkspace>();
729 cx.spawn_in(window, async move |this, cx| {
730 let Some(paths) = paths.await.log_err().flatten() else {
731 return;
732 };
733 if !create_new_window {
734 if let Some(handle) = multi_workspace_handle {
735 if let Some(task) = handle
736 .update(cx, |multi_workspace, window, cx| {
737 multi_workspace.open_project(paths, OpenMode::Activate, window, cx)
738 })
739 .log_err()
740 {
741 task.await.log_err();
742 }
743 return;
744 }
745 }
746 if let Some(task) = this
747 .update_in(cx, |this, window, cx| {
748 this.open_workspace_for_paths(OpenMode::NewWindow, paths, window, cx)
749 })
750 .log_err()
751 {
752 task.await.log_err();
753 }
754 })
755 .detach();
756}
757
758pub fn init(app_state: Arc<AppState>, cx: &mut App) {
759 component::init();
760 theme_preview::init(cx);
761 toast_layer::init(cx);
762 history_manager::init(app_state.fs.clone(), cx);
763
764 cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx))
765 .on_action(|_: &Reload, cx| reload(cx))
766 .on_action(|action: &Open, cx: &mut App| {
767 let app_state = AppState::global(cx);
768 prompt_and_open_paths(
769 app_state,
770 PathPromptOptions {
771 files: true,
772 directories: true,
773 multiple: true,
774 prompt: None,
775 },
776 action.create_new_window,
777 cx,
778 );
779 })
780 .on_action(|_: &OpenFiles, cx: &mut App| {
781 let directories = cx.can_select_mixed_files_and_dirs();
782 let app_state = AppState::global(cx);
783 prompt_and_open_paths(
784 app_state,
785 PathPromptOptions {
786 files: true,
787 directories,
788 multiple: true,
789 prompt: None,
790 },
791 true,
792 cx,
793 );
794 });
795}
796
797type BuildProjectItemFn =
798 fn(AnyEntity, Entity<Project>, Option<&Pane>, &mut Window, &mut App) -> Box<dyn ItemHandle>;
799
800type BuildProjectItemForPathFn =
801 fn(
802 &Entity<Project>,
803 &ProjectPath,
804 &mut Window,
805 &mut App,
806 ) -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
807
808#[derive(Clone, Default)]
809struct ProjectItemRegistry {
810 build_project_item_fns_by_type: HashMap<TypeId, BuildProjectItemFn>,
811 build_project_item_for_path_fns: Vec<BuildProjectItemForPathFn>,
812}
813
814impl ProjectItemRegistry {
815 fn register<T: ProjectItem>(&mut self) {
816 self.build_project_item_fns_by_type.insert(
817 TypeId::of::<T::Item>(),
818 |item, project, pane, window, cx| {
819 let item = item.downcast().unwrap();
820 Box::new(cx.new(|cx| T::for_project_item(project, pane, item, window, cx)))
821 as Box<dyn ItemHandle>
822 },
823 );
824 self.build_project_item_for_path_fns
825 .push(|project, project_path, window, cx| {
826 let project_path = project_path.clone();
827 let is_file = project
828 .read(cx)
829 .entry_for_path(&project_path, cx)
830 .is_some_and(|entry| entry.is_file());
831 let entry_abs_path = project.read(cx).absolute_path(&project_path, cx);
832 let is_local = project.read(cx).is_local();
833 let project_item =
834 <T::Item as project::ProjectItem>::try_open(project, &project_path, cx)?;
835 let project = project.clone();
836 Some(window.spawn(cx, async move |cx| {
837 match project_item.await.with_context(|| {
838 format!(
839 "opening project path {:?}",
840 entry_abs_path.as_deref().unwrap_or(&project_path.path.as_std_path())
841 )
842 }) {
843 Ok(project_item) => {
844 let project_item = project_item;
845 let project_entry_id: Option<ProjectEntryId> =
846 project_item.read_with(cx, project::ProjectItem::entry_id);
847 let build_workspace_item = Box::new(
848 |pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
849 Box::new(cx.new(|cx| {
850 T::for_project_item(
851 project,
852 Some(pane),
853 project_item,
854 window,
855 cx,
856 )
857 })) as Box<dyn ItemHandle>
858 },
859 ) as Box<_>;
860 Ok((project_entry_id, build_workspace_item))
861 }
862 Err(e) => {
863 log::warn!("Failed to open a project item: {e:#}");
864 if e.error_code() == ErrorCode::Internal {
865 if let Some(abs_path) =
866 entry_abs_path.as_deref().filter(|_| is_file)
867 {
868 if let Some(broken_project_item_view) =
869 cx.update(|window, cx| {
870 T::for_broken_project_item(
871 abs_path, is_local, &e, window, cx,
872 )
873 })?
874 {
875 let build_workspace_item = Box::new(
876 move |_: &mut Pane, _: &mut Window, cx: &mut Context<Pane>| {
877 cx.new(|_| broken_project_item_view).boxed_clone()
878 },
879 )
880 as Box<_>;
881 return Ok((None, build_workspace_item));
882 }
883 }
884 }
885 Err(e)
886 }
887 }
888 }))
889 });
890 }
891
892 fn open_path(
893 &self,
894 project: &Entity<Project>,
895 path: &ProjectPath,
896 window: &mut Window,
897 cx: &mut App,
898 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
899 let Some(open_project_item) = self
900 .build_project_item_for_path_fns
901 .iter()
902 .rev()
903 .find_map(|open_project_item| open_project_item(project, path, window, cx))
904 else {
905 return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
906 };
907 open_project_item
908 }
909
910 fn build_item<T: project::ProjectItem>(
911 &self,
912 item: Entity<T>,
913 project: Entity<Project>,
914 pane: Option<&Pane>,
915 window: &mut Window,
916 cx: &mut App,
917 ) -> Option<Box<dyn ItemHandle>> {
918 let build = self
919 .build_project_item_fns_by_type
920 .get(&TypeId::of::<T>())?;
921 Some(build(item.into_any(), project, pane, window, cx))
922 }
923}
924
925type WorkspaceItemBuilder =
926 Box<dyn FnOnce(&mut Pane, &mut Window, &mut Context<Pane>) -> Box<dyn ItemHandle>>;
927
928impl Global for ProjectItemRegistry {}
929
930/// Registers a [ProjectItem] for the app. When opening a file, all the registered
931/// items will get a chance to open the file, starting from the project item that
932/// was added last.
933pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
934 cx.default_global::<ProjectItemRegistry>().register::<I>();
935}
936
937#[derive(Default)]
938pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
939
940struct FollowableViewDescriptor {
941 from_state_proto: fn(
942 Entity<Workspace>,
943 ViewId,
944 &mut Option<proto::view::Variant>,
945 &mut Window,
946 &mut App,
947 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
948 to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
949}
950
951impl Global for FollowableViewRegistry {}
952
953impl FollowableViewRegistry {
954 pub fn register<I: FollowableItem>(cx: &mut App) {
955 cx.default_global::<Self>().0.insert(
956 TypeId::of::<I>(),
957 FollowableViewDescriptor {
958 from_state_proto: |workspace, id, state, window, cx| {
959 I::from_state_proto(workspace, id, state, window, cx).map(|task| {
960 cx.foreground_executor()
961 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
962 })
963 },
964 to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
965 },
966 );
967 }
968
969 pub fn from_state_proto(
970 workspace: Entity<Workspace>,
971 view_id: ViewId,
972 mut state: Option<proto::view::Variant>,
973 window: &mut Window,
974 cx: &mut App,
975 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
976 cx.update_default_global(|this: &mut Self, cx| {
977 this.0.values().find_map(|descriptor| {
978 (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, window, cx)
979 })
980 })
981 }
982
983 pub fn to_followable_view(
984 view: impl Into<AnyView>,
985 cx: &App,
986 ) -> Option<Box<dyn FollowableItemHandle>> {
987 let this = cx.try_global::<Self>()?;
988 let view = view.into();
989 let descriptor = this.0.get(&view.entity_type())?;
990 Some((descriptor.to_followable_view)(&view))
991 }
992}
993
994#[derive(Copy, Clone)]
995struct SerializableItemDescriptor {
996 deserialize: fn(
997 Entity<Project>,
998 WeakEntity<Workspace>,
999 WorkspaceId,
1000 ItemId,
1001 &mut Window,
1002 &mut Context<Pane>,
1003 ) -> Task<Result<Box<dyn ItemHandle>>>,
1004 cleanup: fn(WorkspaceId, Vec<ItemId>, &mut Window, &mut App) -> Task<Result<()>>,
1005 view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
1006}
1007
1008#[derive(Default)]
1009struct SerializableItemRegistry {
1010 descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
1011 descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
1012}
1013
1014impl Global for SerializableItemRegistry {}
1015
1016impl SerializableItemRegistry {
1017 fn deserialize(
1018 item_kind: &str,
1019 project: Entity<Project>,
1020 workspace: WeakEntity<Workspace>,
1021 workspace_id: WorkspaceId,
1022 item_item: ItemId,
1023 window: &mut Window,
1024 cx: &mut Context<Pane>,
1025 ) -> Task<Result<Box<dyn ItemHandle>>> {
1026 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
1027 return Task::ready(Err(anyhow!(
1028 "cannot deserialize {}, descriptor not found",
1029 item_kind
1030 )));
1031 };
1032
1033 (descriptor.deserialize)(project, workspace, workspace_id, item_item, window, cx)
1034 }
1035
1036 fn cleanup(
1037 item_kind: &str,
1038 workspace_id: WorkspaceId,
1039 loaded_items: Vec<ItemId>,
1040 window: &mut Window,
1041 cx: &mut App,
1042 ) -> Task<Result<()>> {
1043 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
1044 return Task::ready(Err(anyhow!(
1045 "cannot cleanup {}, descriptor not found",
1046 item_kind
1047 )));
1048 };
1049
1050 (descriptor.cleanup)(workspace_id, loaded_items, window, cx)
1051 }
1052
1053 fn view_to_serializable_item_handle(
1054 view: AnyView,
1055 cx: &App,
1056 ) -> Option<Box<dyn SerializableItemHandle>> {
1057 let this = cx.try_global::<Self>()?;
1058 let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
1059 Some((descriptor.view_to_serializable_item)(view))
1060 }
1061
1062 fn descriptor(item_kind: &str, cx: &App) -> Option<SerializableItemDescriptor> {
1063 let this = cx.try_global::<Self>()?;
1064 this.descriptors_by_kind.get(item_kind).copied()
1065 }
1066}
1067
1068pub fn register_serializable_item<I: SerializableItem>(cx: &mut App) {
1069 let serialized_item_kind = I::serialized_item_kind();
1070
1071 let registry = cx.default_global::<SerializableItemRegistry>();
1072 let descriptor = SerializableItemDescriptor {
1073 deserialize: |project, workspace, workspace_id, item_id, window, cx| {
1074 let task = I::deserialize(project, workspace, workspace_id, item_id, window, cx);
1075 cx.foreground_executor()
1076 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
1077 },
1078 cleanup: |workspace_id, loaded_items, window, cx| {
1079 I::cleanup(workspace_id, loaded_items, window, cx)
1080 },
1081 view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
1082 };
1083 registry
1084 .descriptors_by_kind
1085 .insert(Arc::from(serialized_item_kind), descriptor);
1086 registry
1087 .descriptors_by_type
1088 .insert(TypeId::of::<I>(), descriptor);
1089}
1090
1091pub struct AppState {
1092 pub languages: Arc<LanguageRegistry>,
1093 pub client: Arc<Client>,
1094 pub user_store: Entity<UserStore>,
1095 pub workspace_store: Entity<WorkspaceStore>,
1096 pub fs: Arc<dyn fs::Fs>,
1097 pub build_window_options: fn(Option<Uuid>, &mut App) -> WindowOptions,
1098 pub node_runtime: NodeRuntime,
1099 pub session: Entity<AppSession>,
1100}
1101
1102struct GlobalAppState(Arc<AppState>);
1103
1104impl Global for GlobalAppState {}
1105
1106pub struct WorkspaceStore {
1107 workspaces: HashSet<(gpui::AnyWindowHandle, WeakEntity<Workspace>)>,
1108 client: Arc<Client>,
1109 _subscriptions: Vec<client::Subscription>,
1110}
1111
1112#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
1113pub enum CollaboratorId {
1114 PeerId(PeerId),
1115 Agent,
1116}
1117
1118impl From<PeerId> for CollaboratorId {
1119 fn from(peer_id: PeerId) -> Self {
1120 CollaboratorId::PeerId(peer_id)
1121 }
1122}
1123
1124impl From<&PeerId> for CollaboratorId {
1125 fn from(peer_id: &PeerId) -> Self {
1126 CollaboratorId::PeerId(*peer_id)
1127 }
1128}
1129
1130#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
1131struct Follower {
1132 project_id: Option<u64>,
1133 peer_id: PeerId,
1134}
1135
1136impl AppState {
1137 #[track_caller]
1138 pub fn global(cx: &App) -> Arc<Self> {
1139 cx.global::<GlobalAppState>().0.clone()
1140 }
1141 pub fn try_global(cx: &App) -> Option<Arc<Self>> {
1142 cx.try_global::<GlobalAppState>()
1143 .map(|state| state.0.clone())
1144 }
1145 pub fn set_global(state: Arc<AppState>, cx: &mut App) {
1146 cx.set_global(GlobalAppState(state));
1147 }
1148
1149 #[cfg(any(test, feature = "test-support"))]
1150 pub fn test(cx: &mut App) -> Arc<Self> {
1151 use fs::Fs;
1152 use node_runtime::NodeRuntime;
1153 use session::Session;
1154 use settings::SettingsStore;
1155
1156 if !cx.has_global::<SettingsStore>() {
1157 let settings_store = SettingsStore::test(cx);
1158 cx.set_global(settings_store);
1159 }
1160
1161 let fs = fs::FakeFs::new(cx.background_executor().clone());
1162 <dyn Fs>::set_global(fs.clone(), cx);
1163 let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
1164 let clock = Arc::new(clock::FakeSystemClock::new());
1165 let http_client = http_client::FakeHttpClient::with_404_response();
1166 let client = Client::new(clock, http_client, cx);
1167 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
1168 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1169 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
1170
1171 theme_settings::init(theme::LoadThemes::JustBase, cx);
1172 client::init(&client, cx);
1173
1174 Arc::new(Self {
1175 client,
1176 fs,
1177 languages,
1178 user_store,
1179 workspace_store,
1180 node_runtime: NodeRuntime::unavailable(),
1181 build_window_options: |_, _| Default::default(),
1182 session,
1183 })
1184 }
1185}
1186
1187struct DelayedDebouncedEditAction {
1188 task: Option<Task<()>>,
1189 cancel_channel: Option<oneshot::Sender<()>>,
1190}
1191
1192impl DelayedDebouncedEditAction {
1193 fn new() -> DelayedDebouncedEditAction {
1194 DelayedDebouncedEditAction {
1195 task: None,
1196 cancel_channel: None,
1197 }
1198 }
1199
1200 fn fire_new<F>(
1201 &mut self,
1202 delay: Duration,
1203 window: &mut Window,
1204 cx: &mut Context<Workspace>,
1205 func: F,
1206 ) where
1207 F: 'static
1208 + Send
1209 + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> Task<Result<()>>,
1210 {
1211 if let Some(channel) = self.cancel_channel.take() {
1212 _ = channel.send(());
1213 }
1214
1215 let (sender, mut receiver) = oneshot::channel::<()>();
1216 self.cancel_channel = Some(sender);
1217
1218 let previous_task = self.task.take();
1219 self.task = Some(cx.spawn_in(window, async move |workspace, cx| {
1220 let mut timer = cx.background_executor().timer(delay).fuse();
1221 if let Some(previous_task) = previous_task {
1222 previous_task.await;
1223 }
1224
1225 futures::select_biased! {
1226 _ = receiver => return,
1227 _ = timer => {}
1228 }
1229
1230 if let Some(result) = workspace
1231 .update_in(cx, |workspace, window, cx| (func)(workspace, window, cx))
1232 .log_err()
1233 {
1234 result.await.log_err();
1235 }
1236 }));
1237 }
1238}
1239
1240pub enum Event {
1241 PaneAdded(Entity<Pane>),
1242 PaneRemoved,
1243 ItemAdded {
1244 item: Box<dyn ItemHandle>,
1245 },
1246 ActiveItemChanged,
1247 ItemRemoved {
1248 item_id: EntityId,
1249 },
1250 UserSavedItem {
1251 pane: WeakEntity<Pane>,
1252 item: Box<dyn WeakItemHandle>,
1253 save_intent: SaveIntent,
1254 },
1255 ContactRequestedJoin(u64),
1256 WorkspaceCreated(WeakEntity<Workspace>),
1257 OpenBundledFile {
1258 text: Cow<'static, str>,
1259 title: &'static str,
1260 language: &'static str,
1261 },
1262 ZoomChanged,
1263 ModalOpened,
1264 Activate,
1265 PanelAdded(AnyView),
1266}
1267
1268#[derive(Debug, Clone)]
1269pub enum OpenVisible {
1270 All,
1271 None,
1272 OnlyFiles,
1273 OnlyDirectories,
1274}
1275
1276enum WorkspaceLocation {
1277 // Valid local paths or SSH project to serialize
1278 Location(SerializedWorkspaceLocation, PathList),
1279 // No valid location found hence clear session id
1280 DetachFromSession,
1281 // No valid location found to serialize
1282 None,
1283}
1284
1285type PromptForNewPath = Box<
1286 dyn Fn(
1287 &mut Workspace,
1288 DirectoryLister,
1289 Option<String>,
1290 &mut Window,
1291 &mut Context<Workspace>,
1292 ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
1293>;
1294
1295type PromptForOpenPath = Box<
1296 dyn Fn(
1297 &mut Workspace,
1298 DirectoryLister,
1299 &mut Window,
1300 &mut Context<Workspace>,
1301 ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
1302>;
1303
1304#[derive(Default)]
1305struct DispatchingKeystrokes {
1306 dispatched: HashSet<Vec<Keystroke>>,
1307 queue: VecDeque<Keystroke>,
1308 task: Option<Shared<Task<()>>>,
1309}
1310
1311/// Collects everything project-related for a certain window opened.
1312/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
1313///
1314/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
1315/// The `Workspace` owns everybody's state and serves as a default, "global context",
1316/// that can be used to register a global action to be triggered from any place in the window.
1317pub struct Workspace {
1318 weak_self: WeakEntity<Self>,
1319 workspace_actions: Vec<Box<dyn Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div>>,
1320 zoomed: Option<AnyWeakView>,
1321 previous_dock_drag_coordinates: Option<Point<Pixels>>,
1322 zoomed_position: Option<DockPosition>,
1323 center: PaneGroup,
1324 left_dock: Entity<Dock>,
1325 bottom_dock: Entity<Dock>,
1326 right_dock: Entity<Dock>,
1327 panes: Vec<Entity<Pane>>,
1328 active_worktree_override: Option<WorktreeId>,
1329 panes_by_item: HashMap<EntityId, WeakEntity<Pane>>,
1330 active_pane: Entity<Pane>,
1331 last_active_center_pane: Option<WeakEntity<Pane>>,
1332 last_active_view_id: Option<proto::ViewId>,
1333 status_bar: Entity<StatusBar>,
1334 pub(crate) modal_layer: Entity<ModalLayer>,
1335 toast_layer: Entity<ToastLayer>,
1336 titlebar_item: Option<AnyView>,
1337 notifications: Notifications,
1338 suppressed_notifications: HashSet<NotificationId>,
1339 project: Entity<Project>,
1340 follower_states: HashMap<CollaboratorId, FollowerState>,
1341 last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
1342 window_edited: bool,
1343 last_window_title: Option<String>,
1344 dirty_items: HashMap<EntityId, Subscription>,
1345 active_call: Option<(GlobalAnyActiveCall, Vec<Subscription>)>,
1346 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
1347 database_id: Option<WorkspaceId>,
1348 app_state: Arc<AppState>,
1349 dispatching_keystrokes: Rc<RefCell<DispatchingKeystrokes>>,
1350 _subscriptions: Vec<Subscription>,
1351 _apply_leader_updates: Task<Result<()>>,
1352 _observe_current_user: Task<Result<()>>,
1353 _schedule_serialize_workspace: Option<Task<()>>,
1354 _serialize_workspace_task: Option<Task<()>>,
1355 _schedule_serialize_ssh_paths: Option<Task<()>>,
1356 pane_history_timestamp: Arc<AtomicUsize>,
1357 bounds: Bounds<Pixels>,
1358 pub centered_layout: bool,
1359 bounds_save_task_queued: Option<Task<()>>,
1360 on_prompt_for_new_path: Option<PromptForNewPath>,
1361 on_prompt_for_open_path: Option<PromptForOpenPath>,
1362 terminal_provider: Option<Box<dyn TerminalProvider>>,
1363 debugger_provider: Option<Arc<dyn DebuggerProvider>>,
1364 serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
1365 _items_serializer: Task<Result<()>>,
1366 session_id: Option<String>,
1367 scheduled_tasks: Vec<Task<()>>,
1368 last_open_dock_positions: Vec<DockPosition>,
1369 removing: bool,
1370 open_in_dev_container: bool,
1371 _dev_container_task: Option<Task<Result<()>>>,
1372 _panels_task: Option<Task<Result<()>>>,
1373 sidebar_focus_handle: Option<FocusHandle>,
1374 multi_workspace: Option<WeakEntity<MultiWorkspace>>,
1375}
1376
1377impl EventEmitter<Event> for Workspace {}
1378
1379#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
1380pub struct ViewId {
1381 pub creator: CollaboratorId,
1382 pub id: u64,
1383}
1384
1385pub struct FollowerState {
1386 center_pane: Entity<Pane>,
1387 dock_pane: Option<Entity<Pane>>,
1388 active_view_id: Option<ViewId>,
1389 items_by_leader_view_id: HashMap<ViewId, FollowerView>,
1390}
1391
1392struct FollowerView {
1393 view: Box<dyn FollowableItemHandle>,
1394 location: Option<proto::PanelId>,
1395}
1396
1397#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1398pub enum OpenMode {
1399 /// Open the workspace in a new window.
1400 NewWindow,
1401 /// Add to the window's multi workspace without activating it (used during deserialization).
1402 Add,
1403 /// Add to the window's multi workspace and activate it.
1404 #[default]
1405 Activate,
1406}
1407
1408impl Workspace {
1409 pub fn new(
1410 workspace_id: Option<WorkspaceId>,
1411 project: Entity<Project>,
1412 app_state: Arc<AppState>,
1413 window: &mut Window,
1414 cx: &mut Context<Self>,
1415 ) -> Self {
1416 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1417 cx.subscribe(&trusted_worktrees, |_, worktrees_store, e, cx| {
1418 if let TrustedWorktreesEvent::Trusted(..) = e {
1419 // Do not persist auto trusted worktrees
1420 if !ProjectSettings::get_global(cx).session.trust_all_worktrees {
1421 worktrees_store.update(cx, |worktrees_store, cx| {
1422 worktrees_store.schedule_serialization(
1423 cx,
1424 |new_trusted_worktrees, cx| {
1425 let timeout =
1426 cx.background_executor().timer(SERIALIZATION_THROTTLE_TIME);
1427 let db = WorkspaceDb::global(cx);
1428 cx.background_spawn(async move {
1429 timeout.await;
1430 db.save_trusted_worktrees(new_trusted_worktrees)
1431 .await
1432 .log_err();
1433 })
1434 },
1435 )
1436 });
1437 }
1438 }
1439 })
1440 .detach();
1441
1442 cx.observe_global::<SettingsStore>(|_, cx| {
1443 if ProjectSettings::get_global(cx).session.trust_all_worktrees {
1444 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1445 trusted_worktrees.update(cx, |trusted_worktrees, cx| {
1446 trusted_worktrees.auto_trust_all(cx);
1447 })
1448 }
1449 }
1450 })
1451 .detach();
1452 }
1453
1454 cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
1455 match event {
1456 project::Event::RemoteIdChanged(_) => {
1457 this.update_window_title(window, cx);
1458 }
1459
1460 project::Event::CollaboratorLeft(peer_id) => {
1461 this.collaborator_left(*peer_id, window, cx);
1462 }
1463
1464 &project::Event::WorktreeRemoved(_) => {
1465 this.update_window_title(window, cx);
1466 this.serialize_workspace(window, cx);
1467 this.update_history(cx);
1468 }
1469
1470 &project::Event::WorktreeAdded(id) => {
1471 this.update_window_title(window, cx);
1472 if this
1473 .project()
1474 .read(cx)
1475 .worktree_for_id(id, cx)
1476 .is_some_and(|wt| wt.read(cx).is_visible())
1477 {
1478 this.serialize_workspace(window, cx);
1479 this.update_history(cx);
1480 }
1481 }
1482 project::Event::WorktreeUpdatedEntries(..) => {
1483 this.update_window_title(window, cx);
1484 this.serialize_workspace(window, cx);
1485 }
1486
1487 project::Event::DisconnectedFromHost => {
1488 this.update_window_edited(window, cx);
1489 let leaders_to_unfollow =
1490 this.follower_states.keys().copied().collect::<Vec<_>>();
1491 for leader_id in leaders_to_unfollow {
1492 this.unfollow(leader_id, window, cx);
1493 }
1494 }
1495
1496 project::Event::DisconnectedFromRemote {
1497 server_not_running: _,
1498 } => {
1499 this.update_window_edited(window, cx);
1500 }
1501
1502 project::Event::Closed => {
1503 window.remove_window();
1504 }
1505
1506 project::Event::DeletedEntry(_, entry_id) => {
1507 for pane in this.panes.iter() {
1508 pane.update(cx, |pane, cx| {
1509 pane.handle_deleted_project_item(*entry_id, window, cx)
1510 });
1511 }
1512 }
1513
1514 project::Event::Toast {
1515 notification_id,
1516 message,
1517 link,
1518 } => this.show_notification(
1519 NotificationId::named(notification_id.clone()),
1520 cx,
1521 |cx| {
1522 let mut notification = MessageNotification::new(message.clone(), cx);
1523 if let Some(link) = link {
1524 notification = notification
1525 .more_info_message(link.label)
1526 .more_info_url(link.url);
1527 }
1528
1529 cx.new(|_| notification)
1530 },
1531 ),
1532
1533 project::Event::HideToast { notification_id } => {
1534 this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
1535 }
1536
1537 project::Event::LanguageServerPrompt(request) => {
1538 struct LanguageServerPrompt;
1539
1540 this.show_notification(
1541 NotificationId::composite::<LanguageServerPrompt>(request.id),
1542 cx,
1543 |cx| {
1544 cx.new(|cx| {
1545 notifications::LanguageServerPrompt::new(request.clone(), cx)
1546 })
1547 },
1548 );
1549 }
1550
1551 project::Event::AgentLocationChanged => {
1552 this.handle_agent_location_changed(window, cx)
1553 }
1554
1555 _ => {}
1556 }
1557 cx.notify()
1558 })
1559 .detach();
1560
1561 cx.subscribe_in(
1562 &project.read(cx).breakpoint_store(),
1563 window,
1564 |workspace, _, event, window, cx| match event {
1565 BreakpointStoreEvent::BreakpointsUpdated(_, _)
1566 | BreakpointStoreEvent::BreakpointsCleared(_) => {
1567 workspace.serialize_workspace(window, cx);
1568 }
1569 BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {}
1570 },
1571 )
1572 .detach();
1573 if let Some(toolchain_store) = project.read(cx).toolchain_store() {
1574 cx.subscribe_in(
1575 &toolchain_store,
1576 window,
1577 |workspace, _, event, window, cx| match event {
1578 ToolchainStoreEvent::CustomToolchainsModified => {
1579 workspace.serialize_workspace(window, cx);
1580 }
1581 _ => {}
1582 },
1583 )
1584 .detach();
1585 }
1586
1587 cx.on_focus_lost(window, |this, window, cx| {
1588 let focus_handle = this.focus_handle(cx);
1589 window.focus(&focus_handle, cx);
1590 })
1591 .detach();
1592
1593 let weak_handle = cx.entity().downgrade();
1594 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
1595
1596 let center_pane = cx.new(|cx| {
1597 let mut center_pane = Pane::new(
1598 weak_handle.clone(),
1599 project.clone(),
1600 pane_history_timestamp.clone(),
1601 None,
1602 NewFile.boxed_clone(),
1603 true,
1604 window,
1605 cx,
1606 );
1607 center_pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
1608 center_pane.set_should_display_welcome_page(true);
1609 center_pane
1610 });
1611 cx.subscribe_in(¢er_pane, window, Self::handle_pane_event)
1612 .detach();
1613
1614 window.focus(¢er_pane.focus_handle(cx), cx);
1615
1616 cx.emit(Event::PaneAdded(center_pane.clone()));
1617
1618 let any_window_handle = window.window_handle();
1619 app_state.workspace_store.update(cx, |store, _| {
1620 store
1621 .workspaces
1622 .insert((any_window_handle, weak_handle.clone()));
1623 });
1624
1625 let mut current_user = app_state.user_store.read(cx).watch_current_user();
1626 let mut connection_status = app_state.client.status();
1627 let _observe_current_user = cx.spawn_in(window, async move |this, cx| {
1628 current_user.next().await;
1629 connection_status.next().await;
1630 let mut stream =
1631 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
1632
1633 while stream.recv().await.is_some() {
1634 this.update(cx, |_, cx| cx.notify())?;
1635 }
1636 anyhow::Ok(())
1637 });
1638
1639 // All leader updates are enqueued and then processed in a single task, so
1640 // that each asynchronous operation can be run in order.
1641 let (leader_updates_tx, mut leader_updates_rx) =
1642 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
1643 let _apply_leader_updates = cx.spawn_in(window, async move |this, cx| {
1644 while let Some((leader_id, update)) = leader_updates_rx.next().await {
1645 Self::process_leader_update(&this, leader_id, update, cx)
1646 .await
1647 .log_err();
1648 }
1649
1650 Ok(())
1651 });
1652
1653 cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
1654 let modal_layer = cx.new(|_| ModalLayer::new());
1655 let toast_layer = cx.new(|_| ToastLayer::new());
1656 cx.subscribe(
1657 &modal_layer,
1658 |_, _, _: &modal_layer::ModalOpenedEvent, cx| {
1659 cx.emit(Event::ModalOpened);
1660 },
1661 )
1662 .detach();
1663
1664 let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx);
1665 let bottom_dock = Dock::new(DockPosition::Bottom, modal_layer.clone(), window, cx);
1666 let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), window, cx);
1667 let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx));
1668 let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx));
1669 let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx));
1670 let multi_workspace = window
1671 .root::<MultiWorkspace>()
1672 .flatten()
1673 .map(|mw| mw.downgrade());
1674 let status_bar = cx.new(|cx| {
1675 let mut status_bar =
1676 StatusBar::new(¢er_pane.clone(), multi_workspace.clone(), window, cx);
1677 status_bar.add_left_item(left_dock_buttons, window, cx);
1678 status_bar.add_right_item(right_dock_buttons, window, cx);
1679 status_bar.add_right_item(bottom_dock_buttons, window, cx);
1680 status_bar
1681 });
1682
1683 let session_id = app_state.session.read(cx).id().to_owned();
1684
1685 let mut active_call = None;
1686 if let Some(call) = GlobalAnyActiveCall::try_global(cx).cloned() {
1687 let subscriptions =
1688 vec![
1689 call.0
1690 .subscribe(window, cx, Box::new(Self::on_active_call_event)),
1691 ];
1692 active_call = Some((call, subscriptions));
1693 }
1694
1695 let (serializable_items_tx, serializable_items_rx) =
1696 mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
1697 let _items_serializer = cx.spawn_in(window, async move |this, cx| {
1698 Self::serialize_items(&this, serializable_items_rx, cx).await
1699 });
1700
1701 let subscriptions = vec![
1702 cx.observe_window_activation(window, Self::on_window_activation_changed),
1703 cx.observe_window_bounds(window, move |this, window, cx| {
1704 if this.bounds_save_task_queued.is_some() {
1705 return;
1706 }
1707 this.bounds_save_task_queued = Some(cx.spawn_in(window, async move |this, cx| {
1708 cx.background_executor()
1709 .timer(Duration::from_millis(100))
1710 .await;
1711 this.update_in(cx, |this, window, cx| {
1712 this.save_window_bounds(window, cx).detach();
1713 this.bounds_save_task_queued.take();
1714 })
1715 .ok();
1716 }));
1717 cx.notify();
1718 }),
1719 cx.observe_window_appearance(window, |_, window, cx| {
1720 let window_appearance = window.appearance();
1721
1722 *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
1723
1724 theme_settings::reload_theme(cx);
1725 theme_settings::reload_icon_theme(cx);
1726 }),
1727 cx.on_release({
1728 let weak_handle = weak_handle.clone();
1729 move |this, cx| {
1730 this.app_state.workspace_store.update(cx, move |store, _| {
1731 store.workspaces.retain(|(_, weak)| weak != &weak_handle);
1732 })
1733 }
1734 }),
1735 ];
1736
1737 cx.defer_in(window, move |this, window, cx| {
1738 this.update_window_title(window, cx);
1739 this.show_initial_notifications(cx);
1740 });
1741
1742 let mut center = PaneGroup::new(center_pane.clone());
1743 center.set_is_center(true);
1744 center.mark_positions(cx);
1745
1746 Workspace {
1747 weak_self: weak_handle.clone(),
1748 zoomed: None,
1749 zoomed_position: None,
1750 previous_dock_drag_coordinates: None,
1751 center,
1752 panes: vec![center_pane.clone()],
1753 panes_by_item: Default::default(),
1754 active_pane: center_pane.clone(),
1755 last_active_center_pane: Some(center_pane.downgrade()),
1756 last_active_view_id: None,
1757 status_bar,
1758 modal_layer,
1759 toast_layer,
1760 titlebar_item: None,
1761 active_worktree_override: None,
1762 notifications: Notifications::default(),
1763 suppressed_notifications: HashSet::default(),
1764 left_dock,
1765 bottom_dock,
1766 right_dock,
1767 _panels_task: None,
1768 project: project.clone(),
1769 follower_states: Default::default(),
1770 last_leaders_by_pane: Default::default(),
1771 dispatching_keystrokes: Default::default(),
1772 window_edited: false,
1773 last_window_title: None,
1774 dirty_items: Default::default(),
1775 active_call,
1776 database_id: workspace_id,
1777 app_state,
1778 _observe_current_user,
1779 _apply_leader_updates,
1780 _schedule_serialize_workspace: None,
1781 _serialize_workspace_task: None,
1782 _schedule_serialize_ssh_paths: None,
1783 leader_updates_tx,
1784 _subscriptions: subscriptions,
1785 pane_history_timestamp,
1786 workspace_actions: Default::default(),
1787 // This data will be incorrect, but it will be overwritten by the time it needs to be used.
1788 bounds: Default::default(),
1789 centered_layout: false,
1790 bounds_save_task_queued: None,
1791 on_prompt_for_new_path: None,
1792 on_prompt_for_open_path: None,
1793 terminal_provider: None,
1794 debugger_provider: None,
1795 serializable_items_tx,
1796 _items_serializer,
1797 session_id: Some(session_id),
1798
1799 scheduled_tasks: Vec::new(),
1800 last_open_dock_positions: Vec::new(),
1801 removing: false,
1802 sidebar_focus_handle: None,
1803 multi_workspace,
1804 open_in_dev_container: false,
1805 _dev_container_task: None,
1806 }
1807 }
1808
1809 pub fn new_local(
1810 abs_paths: Vec<PathBuf>,
1811 app_state: Arc<AppState>,
1812 requesting_window: Option<WindowHandle<MultiWorkspace>>,
1813 env: Option<HashMap<String, String>>,
1814 init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
1815 open_mode: OpenMode,
1816 cx: &mut App,
1817 ) -> Task<anyhow::Result<OpenResult>> {
1818 let project_handle = Project::local(
1819 app_state.client.clone(),
1820 app_state.node_runtime.clone(),
1821 app_state.user_store.clone(),
1822 app_state.languages.clone(),
1823 app_state.fs.clone(),
1824 env,
1825 Default::default(),
1826 cx,
1827 );
1828
1829 let db = WorkspaceDb::global(cx);
1830 let kvp = db::kvp::KeyValueStore::global(cx);
1831 cx.spawn(async move |cx| {
1832 let mut paths_to_open = Vec::with_capacity(abs_paths.len());
1833 for path in abs_paths.into_iter() {
1834 if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() {
1835 paths_to_open.push(canonical)
1836 } else {
1837 paths_to_open.push(path)
1838 }
1839 }
1840
1841 let serialized_workspace = db.workspace_for_roots(paths_to_open.as_slice());
1842
1843 if let Some(paths) = serialized_workspace.as_ref().map(|ws| &ws.paths) {
1844 paths_to_open = paths.ordered_paths().cloned().collect();
1845 if !paths.is_lexicographically_ordered() {
1846 project_handle.update(cx, |project, cx| {
1847 project.set_worktrees_reordered(true, cx);
1848 });
1849 }
1850 }
1851
1852 // Get project paths for all of the abs_paths
1853 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
1854 Vec::with_capacity(paths_to_open.len());
1855
1856 for path in paths_to_open.into_iter() {
1857 if let Some((_, project_entry)) = cx
1858 .update(|cx| {
1859 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
1860 })
1861 .await
1862 .log_err()
1863 {
1864 project_paths.push((path, Some(project_entry)));
1865 } else {
1866 project_paths.push((path, None));
1867 }
1868 }
1869
1870 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
1871 serialized_workspace.id
1872 } else {
1873 db.next_id().await.unwrap_or_else(|_| Default::default())
1874 };
1875
1876 let toolchains = db.toolchains(workspace_id).await?;
1877
1878 for (toolchain, worktree_path, path) in toolchains {
1879 let toolchain_path = PathBuf::from(toolchain.path.clone().to_string());
1880 let Some(worktree_id) = project_handle.read_with(cx, |this, cx| {
1881 this.find_worktree(&worktree_path, cx)
1882 .and_then(|(worktree, rel_path)| {
1883 if rel_path.is_empty() {
1884 Some(worktree.read(cx).id())
1885 } else {
1886 None
1887 }
1888 })
1889 }) else {
1890 // We did not find a worktree with a given path, but that's whatever.
1891 continue;
1892 };
1893 if !app_state.fs.is_file(toolchain_path.as_path()).await {
1894 continue;
1895 }
1896
1897 project_handle
1898 .update(cx, |this, cx| {
1899 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
1900 })
1901 .await;
1902 }
1903 if let Some(workspace) = serialized_workspace.as_ref() {
1904 project_handle.update(cx, |this, cx| {
1905 for (scope, toolchains) in &workspace.user_toolchains {
1906 for toolchain in toolchains {
1907 this.add_toolchain(toolchain.clone(), scope.clone(), cx);
1908 }
1909 }
1910 });
1911 }
1912
1913 let window_to_replace = match open_mode {
1914 OpenMode::NewWindow => None,
1915 _ => requesting_window,
1916 };
1917 let reuse_existing_window = window_to_replace.is_some();
1918
1919 let (window, workspace): (WindowHandle<MultiWorkspace>, Entity<Workspace>) =
1920 if let Some(window) = window_to_replace {
1921 let centered_layout = serialized_workspace
1922 .as_ref()
1923 .map(|w| w.centered_layout)
1924 .unwrap_or(false);
1925
1926 let workspace = window.update(cx, |_multi_workspace, window, cx| {
1927 let workspace = cx.new(|cx| {
1928 let mut workspace = Workspace::new(
1929 Some(workspace_id),
1930 project_handle.clone(),
1931 app_state.clone(),
1932 window,
1933 cx,
1934 );
1935
1936 workspace.centered_layout = centered_layout;
1937
1938 // Call init callback to add items before window renders
1939 if let Some(init) = init {
1940 init(&mut workspace, window, cx);
1941 }
1942
1943 workspace
1944 });
1945 workspace
1946 })?;
1947 (window, workspace)
1948 } else {
1949 let window_bounds_override = window_bounds_env_override();
1950
1951 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
1952 (Some(WindowBounds::Windowed(bounds)), None)
1953 } else if let Some(workspace) = serialized_workspace.as_ref()
1954 && let Some(display) = workspace.display
1955 && let Some(bounds) = workspace.window_bounds.as_ref()
1956 {
1957 // Reopening an existing workspace - restore its saved bounds
1958 (Some(bounds.0), Some(display))
1959 } else if let Some((display, bounds)) =
1960 persistence::read_default_window_bounds(&kvp)
1961 {
1962 // New or empty workspace - use the last known window bounds
1963 (Some(bounds), Some(display))
1964 } else {
1965 // New window - let GPUI's default_bounds() handle cascading
1966 (None, None)
1967 };
1968
1969 // Use the serialized workspace to construct the new window
1970 let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx));
1971 options.window_bounds = window_bounds;
1972 let centered_layout = serialized_workspace
1973 .as_ref()
1974 .map(|w| w.centered_layout)
1975 .unwrap_or(false);
1976 let window = cx.open_window(options, {
1977 let app_state = app_state.clone();
1978 let project_handle = project_handle.clone();
1979 move |window, cx| {
1980 let workspace = cx.new(|cx| {
1981 let mut workspace = Workspace::new(
1982 Some(workspace_id),
1983 project_handle,
1984 app_state,
1985 window,
1986 cx,
1987 );
1988 workspace.centered_layout = centered_layout;
1989
1990 // Call init callback to add items before window renders
1991 if let Some(init) = init {
1992 init(&mut workspace, window, cx);
1993 }
1994
1995 workspace
1996 });
1997 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
1998 }
1999 })?;
2000 let workspace =
2001 window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
2002 multi_workspace.workspace().clone()
2003 })?;
2004 (window, workspace)
2005 };
2006
2007 notify_if_database_failed(window, cx);
2008 // Check if this is an empty workspace (no paths to open)
2009 // An empty workspace is one where project_paths is empty
2010 let is_empty_workspace = project_paths.is_empty();
2011 // Check if serialized workspace has paths before it's moved
2012 let serialized_workspace_has_paths = serialized_workspace
2013 .as_ref()
2014 .map(|ws| !ws.paths.is_empty())
2015 .unwrap_or(false);
2016
2017 let opened_items = window
2018 .update(cx, |_, window, cx| {
2019 workspace.update(cx, |_workspace: &mut Workspace, cx| {
2020 open_items(serialized_workspace, project_paths, window, cx)
2021 })
2022 })?
2023 .await
2024 .unwrap_or_default();
2025
2026 // Restore default dock state for empty workspaces
2027 // Only restore if:
2028 // 1. This is an empty workspace (no paths), AND
2029 // 2. The serialized workspace either doesn't exist or has no paths
2030 if is_empty_workspace && !serialized_workspace_has_paths {
2031 if let Some(default_docks) = persistence::read_default_dock_state(&kvp) {
2032 window
2033 .update(cx, |_, window, cx| {
2034 workspace.update(cx, |workspace, cx| {
2035 for (dock, serialized_dock) in [
2036 (&workspace.right_dock, &default_docks.right),
2037 (&workspace.left_dock, &default_docks.left),
2038 (&workspace.bottom_dock, &default_docks.bottom),
2039 ] {
2040 dock.update(cx, |dock, cx| {
2041 dock.serialized_dock = Some(serialized_dock.clone());
2042 dock.restore_state(window, cx);
2043 });
2044 }
2045 cx.notify();
2046 });
2047 })
2048 .log_err();
2049 }
2050 }
2051
2052 if reuse_existing_window {
2053 window.update(cx, |multi_workspace, window, cx| match open_mode {
2054 OpenMode::Activate => {
2055 multi_workspace.activate(workspace.clone(), window, cx);
2056 }
2057 OpenMode::Add => {
2058 multi_workspace.add(workspace.clone(), &*window, cx);
2059 }
2060 OpenMode::NewWindow => {
2061 unreachable!()
2062 }
2063 })?;
2064 }
2065
2066 window
2067 .update(cx, |_, _window, cx| {
2068 workspace.update(cx, |this: &mut Workspace, cx| {
2069 this.update_history(cx);
2070 });
2071 })
2072 .log_err();
2073 Ok(OpenResult {
2074 window,
2075 workspace,
2076 opened_items,
2077 })
2078 })
2079 }
2080
2081 pub fn project_group_key(&self, cx: &App) -> ProjectGroupKey {
2082 self.project.read(cx).project_group_key(cx)
2083 }
2084
2085 pub fn weak_handle(&self) -> WeakEntity<Self> {
2086 self.weak_self.clone()
2087 }
2088
2089 pub fn left_dock(&self) -> &Entity<Dock> {
2090 &self.left_dock
2091 }
2092
2093 pub fn bottom_dock(&self) -> &Entity<Dock> {
2094 &self.bottom_dock
2095 }
2096
2097 pub fn set_bottom_dock_layout(
2098 &mut self,
2099 layout: BottomDockLayout,
2100 window: &mut Window,
2101 cx: &mut Context<Self>,
2102 ) {
2103 let fs = self.project().read(cx).fs();
2104 settings::update_settings_file(fs.clone(), cx, move |content, _cx| {
2105 content.workspace.bottom_dock_layout = Some(layout);
2106 });
2107
2108 cx.notify();
2109 self.serialize_workspace(window, cx);
2110 }
2111
2112 pub fn right_dock(&self) -> &Entity<Dock> {
2113 &self.right_dock
2114 }
2115
2116 pub fn all_docks(&self) -> [&Entity<Dock>; 3] {
2117 [&self.left_dock, &self.bottom_dock, &self.right_dock]
2118 }
2119
2120 pub fn capture_dock_state(&self, _window: &Window, cx: &App) -> DockStructure {
2121 let left_dock = self.left_dock.read(cx);
2122 let left_visible = left_dock.is_open();
2123 let left_active_panel = left_dock
2124 .active_panel()
2125 .map(|panel| panel.persistent_name().to_string());
2126 // `zoomed_position` is kept in sync with individual panel zoom state
2127 // by the dock code in `Dock::new` and `Dock::add_panel`.
2128 let left_dock_zoom = self.zoomed_position == Some(DockPosition::Left);
2129
2130 let right_dock = self.right_dock.read(cx);
2131 let right_visible = right_dock.is_open();
2132 let right_active_panel = right_dock
2133 .active_panel()
2134 .map(|panel| panel.persistent_name().to_string());
2135 let right_dock_zoom = self.zoomed_position == Some(DockPosition::Right);
2136
2137 let bottom_dock = self.bottom_dock.read(cx);
2138 let bottom_visible = bottom_dock.is_open();
2139 let bottom_active_panel = bottom_dock
2140 .active_panel()
2141 .map(|panel| panel.persistent_name().to_string());
2142 let bottom_dock_zoom = self.zoomed_position == Some(DockPosition::Bottom);
2143
2144 DockStructure {
2145 left: DockData {
2146 visible: left_visible,
2147 active_panel: left_active_panel,
2148 zoom: left_dock_zoom,
2149 },
2150 right: DockData {
2151 visible: right_visible,
2152 active_panel: right_active_panel,
2153 zoom: right_dock_zoom,
2154 },
2155 bottom: DockData {
2156 visible: bottom_visible,
2157 active_panel: bottom_active_panel,
2158 zoom: bottom_dock_zoom,
2159 },
2160 }
2161 }
2162
2163 pub fn set_dock_structure(
2164 &self,
2165 docks: DockStructure,
2166 window: &mut Window,
2167 cx: &mut Context<Self>,
2168 ) {
2169 for (dock, data) in [
2170 (&self.left_dock, docks.left),
2171 (&self.bottom_dock, docks.bottom),
2172 (&self.right_dock, docks.right),
2173 ] {
2174 dock.update(cx, |dock, cx| {
2175 dock.serialized_dock = Some(data);
2176 dock.restore_state(window, cx);
2177 });
2178 }
2179 }
2180
2181 pub fn open_item_abs_paths(&self, cx: &App) -> Vec<PathBuf> {
2182 self.items(cx)
2183 .filter_map(|item| {
2184 let project_path = item.project_path(cx)?;
2185 self.project.read(cx).absolute_path(&project_path, cx)
2186 })
2187 .collect()
2188 }
2189
2190 pub fn dock_at_position(&self, position: DockPosition) -> &Entity<Dock> {
2191 match position {
2192 DockPosition::Left => &self.left_dock,
2193 DockPosition::Bottom => &self.bottom_dock,
2194 DockPosition::Right => &self.right_dock,
2195 }
2196 }
2197
2198 pub fn agent_panel_position(&self, cx: &App) -> Option<DockPosition> {
2199 self.all_docks().into_iter().find_map(|dock| {
2200 let dock = dock.read(cx);
2201 dock.has_agent_panel(cx).then_some(dock.position())
2202 })
2203 }
2204
2205 pub fn panel_size_state<T: Panel>(&self, cx: &App) -> Option<dock::PanelSizeState> {
2206 self.all_docks().into_iter().find_map(|dock| {
2207 let dock = dock.read(cx);
2208 let panel = dock.panel::<T>()?;
2209 dock.stored_panel_size_state(&panel)
2210 })
2211 }
2212
2213 pub fn persisted_panel_size_state(
2214 &self,
2215 panel_key: &'static str,
2216 cx: &App,
2217 ) -> Option<dock::PanelSizeState> {
2218 dock::Dock::load_persisted_size_state(self, panel_key, cx)
2219 }
2220
2221 pub fn persist_panel_size_state(
2222 &self,
2223 panel_key: &str,
2224 size_state: dock::PanelSizeState,
2225 cx: &mut App,
2226 ) {
2227 let Some(workspace_id) = self
2228 .database_id()
2229 .map(|id| i64::from(id).to_string())
2230 .or(self.session_id())
2231 else {
2232 return;
2233 };
2234
2235 let kvp = db::kvp::KeyValueStore::global(cx);
2236 let panel_key = panel_key.to_string();
2237 cx.background_spawn(async move {
2238 let scope = kvp.scoped(dock::PANEL_SIZE_STATE_KEY);
2239 scope
2240 .write(
2241 format!("{workspace_id}:{panel_key}"),
2242 serde_json::to_string(&size_state)?,
2243 )
2244 .await
2245 })
2246 .detach_and_log_err(cx);
2247 }
2248
2249 pub fn set_panel_size_state<T: Panel>(
2250 &mut self,
2251 size_state: dock::PanelSizeState,
2252 window: &mut Window,
2253 cx: &mut Context<Self>,
2254 ) -> bool {
2255 let Some(panel) = self.panel::<T>(cx) else {
2256 return false;
2257 };
2258
2259 let dock = self.dock_at_position(panel.position(window, cx));
2260 let did_set = dock.update(cx, |dock, cx| {
2261 dock.set_panel_size_state(&panel, size_state, cx)
2262 });
2263
2264 if did_set {
2265 self.persist_panel_size_state(T::panel_key(), size_state, cx);
2266 }
2267
2268 did_set
2269 }
2270
2271 pub fn toggle_dock_panel_flexible_size(
2272 &self,
2273 dock: &Entity<Dock>,
2274 panel: &dyn PanelHandle,
2275 window: &mut Window,
2276 cx: &mut App,
2277 ) {
2278 let position = dock.read(cx).position();
2279 let current_size = self.dock_size(&dock.read(cx), window, cx);
2280 let current_flex =
2281 current_size.and_then(|size| self.dock_flex_for_size(position, size, window, cx));
2282 dock.update(cx, |dock, cx| {
2283 dock.toggle_panel_flexible_size(panel, current_size, current_flex, window, cx);
2284 });
2285 }
2286
2287 fn dock_size(&self, dock: &Dock, window: &Window, cx: &App) -> Option<Pixels> {
2288 let panel = dock.active_panel()?;
2289 let size_state = dock
2290 .stored_panel_size_state(panel.as_ref())
2291 .unwrap_or_default();
2292 let position = dock.position();
2293
2294 let use_flex = panel.has_flexible_size(window, cx);
2295
2296 if position.axis() == Axis::Horizontal
2297 && use_flex
2298 && let Some(flex) = size_state.flex.or_else(|| self.default_dock_flex(position))
2299 {
2300 let workspace_width = self.bounds.size.width;
2301 if workspace_width <= Pixels::ZERO {
2302 return None;
2303 }
2304 let flex = flex.max(0.001);
2305 let opposite = self.opposite_dock_panel_and_size_state(position, window, cx);
2306 if let Some(opposite_flex) = opposite.as_ref().and_then(|(_, s)| s.flex) {
2307 // Both docks are flex items sharing the full workspace width.
2308 let total_flex = flex + 1.0 + opposite_flex;
2309 return Some((flex / total_flex * workspace_width).max(RESIZE_HANDLE_SIZE));
2310 } else {
2311 // Opposite dock is fixed-width; flex items share (W - fixed).
2312 let opposite_fixed = opposite
2313 .map(|(panel, s)| s.size.unwrap_or_else(|| panel.default_size(window, cx)))
2314 .unwrap_or_default();
2315 let available = (workspace_width - opposite_fixed).max(RESIZE_HANDLE_SIZE);
2316 return Some((flex / (flex + 1.0) * available).max(RESIZE_HANDLE_SIZE));
2317 }
2318 }
2319
2320 Some(
2321 size_state
2322 .size
2323 .unwrap_or_else(|| panel.default_size(window, cx)),
2324 )
2325 }
2326
2327 pub fn dock_flex_for_size(
2328 &self,
2329 position: DockPosition,
2330 size: Pixels,
2331 window: &Window,
2332 cx: &App,
2333 ) -> Option<f32> {
2334 if position.axis() != Axis::Horizontal {
2335 return None;
2336 }
2337
2338 let workspace_width = self.bounds.size.width;
2339 if workspace_width <= Pixels::ZERO {
2340 return None;
2341 }
2342
2343 let opposite = self.opposite_dock_panel_and_size_state(position, window, cx);
2344 if let Some(opposite_flex) = opposite.as_ref().and_then(|(_, s)| s.flex) {
2345 let size = size.clamp(px(0.), workspace_width - px(1.));
2346 Some((size * (1.0 + opposite_flex) / (workspace_width - size)).max(0.0))
2347 } else {
2348 let opposite_width = opposite
2349 .map(|(panel, s)| s.size.unwrap_or_else(|| panel.default_size(window, cx)))
2350 .unwrap_or_default();
2351 let available = (workspace_width - opposite_width).max(RESIZE_HANDLE_SIZE);
2352 let remaining = (available - size).max(px(1.));
2353 Some((size / remaining).max(0.0))
2354 }
2355 }
2356
2357 fn opposite_dock_panel_and_size_state(
2358 &self,
2359 position: DockPosition,
2360 window: &Window,
2361 cx: &App,
2362 ) -> Option<(Arc<dyn PanelHandle>, PanelSizeState)> {
2363 let opposite_position = match position {
2364 DockPosition::Left => DockPosition::Right,
2365 DockPosition::Right => DockPosition::Left,
2366 DockPosition::Bottom => return None,
2367 };
2368
2369 let opposite_dock = self.dock_at_position(opposite_position).read(cx);
2370 let panel = opposite_dock.visible_panel()?;
2371 let mut size_state = opposite_dock
2372 .stored_panel_size_state(panel.as_ref())
2373 .unwrap_or_default();
2374 if size_state.flex.is_none() && panel.has_flexible_size(window, cx) {
2375 size_state.flex = self.default_dock_flex(opposite_position);
2376 }
2377 Some((panel.clone(), size_state))
2378 }
2379
2380 pub fn default_dock_flex(&self, position: DockPosition) -> Option<f32> {
2381 if position.axis() != Axis::Horizontal {
2382 return None;
2383 }
2384
2385 let pane = self.last_active_center_pane.clone()?.upgrade()?;
2386 Some(self.center.width_fraction_for_pane(&pane).unwrap_or(1.0))
2387 }
2388
2389 pub fn is_edited(&self) -> bool {
2390 self.window_edited
2391 }
2392
2393 pub fn add_panel<T: Panel>(
2394 &mut self,
2395 panel: Entity<T>,
2396 window: &mut Window,
2397 cx: &mut Context<Self>,
2398 ) {
2399 let focus_handle = panel.panel_focus_handle(cx);
2400 cx.on_focus_in(&focus_handle, window, Self::handle_panel_focused)
2401 .detach();
2402
2403 let dock_position = panel.position(window, cx);
2404 let dock = self.dock_at_position(dock_position);
2405 let any_panel = panel.to_any();
2406 let persisted_size_state =
2407 self.persisted_panel_size_state(T::panel_key(), cx)
2408 .or_else(|| {
2409 load_legacy_panel_size(T::panel_key(), dock_position, self, cx).map(|size| {
2410 let state = dock::PanelSizeState {
2411 size: Some(size),
2412 flex: None,
2413 };
2414 self.persist_panel_size_state(T::panel_key(), state, cx);
2415 state
2416 })
2417 });
2418
2419 dock.update(cx, |dock, cx| {
2420 let index = dock.add_panel(panel.clone(), self.weak_self.clone(), window, cx);
2421 if let Some(size_state) = persisted_size_state {
2422 dock.set_panel_size_state(&panel, size_state, cx);
2423 }
2424 index
2425 });
2426
2427 cx.emit(Event::PanelAdded(any_panel));
2428 }
2429
2430 pub fn remove_panel<T: Panel>(
2431 &mut self,
2432 panel: &Entity<T>,
2433 window: &mut Window,
2434 cx: &mut Context<Self>,
2435 ) {
2436 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2437 dock.update(cx, |dock, cx| dock.remove_panel(panel, window, cx));
2438 }
2439 }
2440
2441 pub fn status_bar(&self) -> &Entity<StatusBar> {
2442 &self.status_bar
2443 }
2444
2445 pub fn set_sidebar_focus_handle(&mut self, handle: Option<FocusHandle>) {
2446 self.sidebar_focus_handle = handle;
2447 }
2448
2449 pub fn status_bar_visible(&self, cx: &App) -> bool {
2450 StatusBarSettings::get_global(cx).show
2451 }
2452
2453 pub fn multi_workspace(&self) -> Option<&WeakEntity<MultiWorkspace>> {
2454 self.multi_workspace.as_ref()
2455 }
2456
2457 pub fn set_multi_workspace(
2458 &mut self,
2459 multi_workspace: WeakEntity<MultiWorkspace>,
2460 cx: &mut App,
2461 ) {
2462 self.status_bar.update(cx, |status_bar, cx| {
2463 status_bar.set_multi_workspace(multi_workspace.clone(), cx);
2464 });
2465 self.multi_workspace = Some(multi_workspace);
2466 }
2467
2468 pub fn app_state(&self) -> &Arc<AppState> {
2469 &self.app_state
2470 }
2471
2472 pub fn set_panels_task(&mut self, task: Task<Result<()>>) {
2473 self._panels_task = Some(task);
2474 }
2475
2476 pub fn take_panels_task(&mut self) -> Option<Task<Result<()>>> {
2477 self._panels_task.take()
2478 }
2479
2480 pub fn user_store(&self) -> &Entity<UserStore> {
2481 &self.app_state.user_store
2482 }
2483
2484 pub fn project(&self) -> &Entity<Project> {
2485 &self.project
2486 }
2487
2488 pub fn path_style(&self, cx: &App) -> PathStyle {
2489 self.project.read(cx).path_style(cx)
2490 }
2491
2492 pub fn recently_activated_items(&self, cx: &App) -> HashMap<EntityId, usize> {
2493 let mut history: HashMap<EntityId, usize> = HashMap::default();
2494
2495 for pane_handle in &self.panes {
2496 let pane = pane_handle.read(cx);
2497
2498 for entry in pane.activation_history() {
2499 history.insert(
2500 entry.entity_id,
2501 history
2502 .get(&entry.entity_id)
2503 .cloned()
2504 .unwrap_or(0)
2505 .max(entry.timestamp),
2506 );
2507 }
2508 }
2509
2510 history
2511 }
2512
2513 pub fn recent_active_item_by_type<T: 'static>(&self, cx: &App) -> Option<Entity<T>> {
2514 let mut recent_item: Option<Entity<T>> = None;
2515 let mut recent_timestamp = 0;
2516 for pane_handle in &self.panes {
2517 let pane = pane_handle.read(cx);
2518 let item_map: HashMap<EntityId, &Box<dyn ItemHandle>> =
2519 pane.items().map(|item| (item.item_id(), item)).collect();
2520 for entry in pane.activation_history() {
2521 if entry.timestamp > recent_timestamp
2522 && let Some(&item) = item_map.get(&entry.entity_id)
2523 && let Some(typed_item) = item.act_as::<T>(cx)
2524 {
2525 recent_timestamp = entry.timestamp;
2526 recent_item = Some(typed_item);
2527 }
2528 }
2529 }
2530 recent_item
2531 }
2532
2533 pub fn recent_navigation_history_iter(
2534 &self,
2535 cx: &App,
2536 ) -> impl Iterator<Item = (ProjectPath, Option<PathBuf>)> + use<> {
2537 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
2538 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
2539
2540 for pane in &self.panes {
2541 let pane = pane.read(cx);
2542
2543 pane.nav_history()
2544 .for_each_entry(cx, &mut |entry, (project_path, fs_path)| {
2545 if let Some(fs_path) = &fs_path {
2546 abs_paths_opened
2547 .entry(fs_path.clone())
2548 .or_default()
2549 .insert(project_path.clone());
2550 }
2551 let timestamp = entry.timestamp;
2552 match history.entry(project_path) {
2553 hash_map::Entry::Occupied(mut entry) => {
2554 let (_, old_timestamp) = entry.get();
2555 if ×tamp > old_timestamp {
2556 entry.insert((fs_path, timestamp));
2557 }
2558 }
2559 hash_map::Entry::Vacant(entry) => {
2560 entry.insert((fs_path, timestamp));
2561 }
2562 }
2563 });
2564
2565 if let Some(item) = pane.active_item()
2566 && let Some(project_path) = item.project_path(cx)
2567 {
2568 let fs_path = self.project.read(cx).absolute_path(&project_path, cx);
2569
2570 if let Some(fs_path) = &fs_path {
2571 abs_paths_opened
2572 .entry(fs_path.clone())
2573 .or_default()
2574 .insert(project_path.clone());
2575 }
2576
2577 history.insert(project_path, (fs_path, std::usize::MAX));
2578 }
2579 }
2580
2581 history
2582 .into_iter()
2583 .sorted_by_key(|(_, (_, order))| *order)
2584 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
2585 .rev()
2586 .filter(move |(history_path, abs_path)| {
2587 let latest_project_path_opened = abs_path
2588 .as_ref()
2589 .and_then(|abs_path| abs_paths_opened.get(abs_path))
2590 .and_then(|project_paths| {
2591 project_paths
2592 .iter()
2593 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
2594 });
2595
2596 latest_project_path_opened.is_none_or(|path| path == history_path)
2597 })
2598 }
2599
2600 pub fn recent_navigation_history(
2601 &self,
2602 limit: Option<usize>,
2603 cx: &App,
2604 ) -> Vec<(ProjectPath, Option<PathBuf>)> {
2605 self.recent_navigation_history_iter(cx)
2606 .take(limit.unwrap_or(usize::MAX))
2607 .collect()
2608 }
2609
2610 pub fn clear_navigation_history(&mut self, _window: &mut Window, cx: &mut Context<Workspace>) {
2611 for pane in &self.panes {
2612 pane.update(cx, |pane, cx| pane.nav_history_mut().clear(cx));
2613 }
2614 }
2615
2616 fn navigate_history(
2617 &mut self,
2618 pane: WeakEntity<Pane>,
2619 mode: NavigationMode,
2620 window: &mut Window,
2621 cx: &mut Context<Workspace>,
2622 ) -> Task<Result<()>> {
2623 self.navigate_history_impl(
2624 pane,
2625 mode,
2626 window,
2627 &mut |history, cx| history.pop(mode, cx),
2628 cx,
2629 )
2630 }
2631
2632 fn navigate_tag_history(
2633 &mut self,
2634 pane: WeakEntity<Pane>,
2635 mode: TagNavigationMode,
2636 window: &mut Window,
2637 cx: &mut Context<Workspace>,
2638 ) -> Task<Result<()>> {
2639 self.navigate_history_impl(
2640 pane,
2641 NavigationMode::Normal,
2642 window,
2643 &mut |history, _cx| history.pop_tag(mode),
2644 cx,
2645 )
2646 }
2647
2648 fn navigate_history_impl(
2649 &mut self,
2650 pane: WeakEntity<Pane>,
2651 mode: NavigationMode,
2652 window: &mut Window,
2653 cb: &mut dyn FnMut(&mut NavHistory, &mut App) -> Option<NavigationEntry>,
2654 cx: &mut Context<Workspace>,
2655 ) -> Task<Result<()>> {
2656 let to_load = if let Some(pane) = pane.upgrade() {
2657 pane.update(cx, |pane, cx| {
2658 window.focus(&pane.focus_handle(cx), cx);
2659 loop {
2660 // Retrieve the weak item handle from the history.
2661 let entry = cb(pane.nav_history_mut(), cx)?;
2662
2663 // If the item is still present in this pane, then activate it.
2664 if let Some(index) = entry
2665 .item
2666 .upgrade()
2667 .and_then(|v| pane.index_for_item(v.as_ref()))
2668 {
2669 let prev_active_item_index = pane.active_item_index();
2670 pane.nav_history_mut().set_mode(mode);
2671 pane.activate_item(index, true, true, window, cx);
2672 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2673
2674 let mut navigated = prev_active_item_index != pane.active_item_index();
2675 if let Some(data) = entry.data {
2676 navigated |= pane.active_item()?.navigate(data, window, cx);
2677 }
2678
2679 if navigated {
2680 break None;
2681 }
2682 } else {
2683 // If the item is no longer present in this pane, then retrieve its
2684 // path info in order to reopen it.
2685 break pane
2686 .nav_history()
2687 .path_for_item(entry.item.id())
2688 .map(|(project_path, abs_path)| (project_path, abs_path, entry));
2689 }
2690 }
2691 })
2692 } else {
2693 None
2694 };
2695
2696 if let Some((project_path, abs_path, entry)) = to_load {
2697 // If the item was no longer present, then load it again from its previous path, first try the local path
2698 let open_by_project_path = self.load_path(project_path.clone(), window, cx);
2699
2700 cx.spawn_in(window, async move |workspace, cx| {
2701 let open_by_project_path = open_by_project_path.await;
2702 let mut navigated = false;
2703 match open_by_project_path
2704 .with_context(|| format!("Navigating to {project_path:?}"))
2705 {
2706 Ok((project_entry_id, build_item)) => {
2707 let prev_active_item_id = pane.update(cx, |pane, _| {
2708 pane.nav_history_mut().set_mode(mode);
2709 pane.active_item().map(|p| p.item_id())
2710 })?;
2711
2712 pane.update_in(cx, |pane, window, cx| {
2713 let item = pane.open_item(
2714 project_entry_id,
2715 project_path,
2716 true,
2717 entry.is_preview,
2718 true,
2719 None,
2720 window, cx,
2721 build_item,
2722 );
2723 navigated |= Some(item.item_id()) != prev_active_item_id;
2724 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2725 if let Some(data) = entry.data {
2726 navigated |= item.navigate(data, window, cx);
2727 }
2728 })?;
2729 }
2730 Err(open_by_project_path_e) => {
2731 // Fall back to opening by abs path, in case an external file was opened and closed,
2732 // and its worktree is now dropped
2733 if let Some(abs_path) = abs_path {
2734 let prev_active_item_id = pane.update(cx, |pane, _| {
2735 pane.nav_history_mut().set_mode(mode);
2736 pane.active_item().map(|p| p.item_id())
2737 })?;
2738 let open_by_abs_path = workspace.update_in(cx, |workspace, window, cx| {
2739 workspace.open_abs_path(abs_path.clone(), OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
2740 })?;
2741 match open_by_abs_path
2742 .await
2743 .with_context(|| format!("Navigating to {abs_path:?}"))
2744 {
2745 Ok(item) => {
2746 pane.update_in(cx, |pane, window, cx| {
2747 navigated |= Some(item.item_id()) != prev_active_item_id;
2748 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2749 if let Some(data) = entry.data {
2750 navigated |= item.navigate(data, window, cx);
2751 }
2752 })?;
2753 }
2754 Err(open_by_abs_path_e) => {
2755 log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
2756 }
2757 }
2758 }
2759 }
2760 }
2761
2762 if !navigated {
2763 workspace
2764 .update_in(cx, |workspace, window, cx| {
2765 Self::navigate_history(workspace, pane, mode, window, cx)
2766 })?
2767 .await?;
2768 }
2769
2770 Ok(())
2771 })
2772 } else {
2773 Task::ready(Ok(()))
2774 }
2775 }
2776
2777 pub fn go_back(
2778 &mut self,
2779 pane: WeakEntity<Pane>,
2780 window: &mut Window,
2781 cx: &mut Context<Workspace>,
2782 ) -> Task<Result<()>> {
2783 self.navigate_history(pane, NavigationMode::GoingBack, window, cx)
2784 }
2785
2786 pub fn go_forward(
2787 &mut self,
2788 pane: WeakEntity<Pane>,
2789 window: &mut Window,
2790 cx: &mut Context<Workspace>,
2791 ) -> Task<Result<()>> {
2792 self.navigate_history(pane, NavigationMode::GoingForward, window, cx)
2793 }
2794
2795 pub fn reopen_closed_item(
2796 &mut self,
2797 window: &mut Window,
2798 cx: &mut Context<Workspace>,
2799 ) -> Task<Result<()>> {
2800 self.navigate_history(
2801 self.active_pane().downgrade(),
2802 NavigationMode::ReopeningClosedItem,
2803 window,
2804 cx,
2805 )
2806 }
2807
2808 pub fn client(&self) -> &Arc<Client> {
2809 &self.app_state.client
2810 }
2811
2812 pub fn set_titlebar_item(&mut self, item: AnyView, _: &mut Window, cx: &mut Context<Self>) {
2813 self.titlebar_item = Some(item);
2814 cx.notify();
2815 }
2816
2817 pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
2818 self.on_prompt_for_new_path = Some(prompt)
2819 }
2820
2821 pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) {
2822 self.on_prompt_for_open_path = Some(prompt)
2823 }
2824
2825 pub fn set_terminal_provider(&mut self, provider: impl TerminalProvider + 'static) {
2826 self.terminal_provider = Some(Box::new(provider));
2827 }
2828
2829 pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) {
2830 self.debugger_provider = Some(Arc::new(provider));
2831 }
2832
2833 pub fn set_open_in_dev_container(&mut self, value: bool) {
2834 self.open_in_dev_container = value;
2835 }
2836
2837 pub fn open_in_dev_container(&self) -> bool {
2838 self.open_in_dev_container
2839 }
2840
2841 pub fn set_dev_container_task(&mut self, task: Task<Result<()>>) {
2842 self._dev_container_task = Some(task);
2843 }
2844
2845 pub fn debugger_provider(&self) -> Option<Arc<dyn DebuggerProvider>> {
2846 self.debugger_provider.clone()
2847 }
2848
2849 pub fn prompt_for_open_path(
2850 &mut self,
2851 path_prompt_options: PathPromptOptions,
2852 lister: DirectoryLister,
2853 window: &mut Window,
2854 cx: &mut Context<Self>,
2855 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
2856 if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
2857 let prompt = self.on_prompt_for_open_path.take().unwrap();
2858 let rx = prompt(self, lister, window, cx);
2859 self.on_prompt_for_open_path = Some(prompt);
2860 rx
2861 } else {
2862 let (tx, rx) = oneshot::channel();
2863 let abs_path = cx.prompt_for_paths(path_prompt_options);
2864
2865 cx.spawn_in(window, async move |workspace, cx| {
2866 let Ok(result) = abs_path.await else {
2867 return Ok(());
2868 };
2869
2870 match result {
2871 Ok(result) => {
2872 tx.send(result).ok();
2873 }
2874 Err(err) => {
2875 let rx = workspace.update_in(cx, |workspace, window, cx| {
2876 workspace.show_portal_error(err.to_string(), cx);
2877 let prompt = workspace.on_prompt_for_open_path.take().unwrap();
2878 let rx = prompt(workspace, lister, window, cx);
2879 workspace.on_prompt_for_open_path = Some(prompt);
2880 rx
2881 })?;
2882 if let Ok(path) = rx.await {
2883 tx.send(path).ok();
2884 }
2885 }
2886 };
2887 anyhow::Ok(())
2888 })
2889 .detach();
2890
2891 rx
2892 }
2893 }
2894
2895 pub fn prompt_for_new_path(
2896 &mut self,
2897 lister: DirectoryLister,
2898 suggested_name: Option<String>,
2899 window: &mut Window,
2900 cx: &mut Context<Self>,
2901 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
2902 if self.project.read(cx).is_via_collab()
2903 || self.project.read(cx).is_via_remote_server()
2904 || !WorkspaceSettings::get_global(cx).use_system_path_prompts
2905 {
2906 let prompt = self.on_prompt_for_new_path.take().unwrap();
2907 let rx = prompt(self, lister, suggested_name, window, cx);
2908 self.on_prompt_for_new_path = Some(prompt);
2909 return rx;
2910 }
2911
2912 let (tx, rx) = oneshot::channel();
2913 cx.spawn_in(window, async move |workspace, cx| {
2914 let abs_path = workspace.update(cx, |workspace, cx| {
2915 let relative_to = workspace
2916 .most_recent_active_path(cx)
2917 .and_then(|p| p.parent().map(|p| p.to_path_buf()))
2918 .or_else(|| {
2919 let project = workspace.project.read(cx);
2920 project.visible_worktrees(cx).find_map(|worktree| {
2921 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
2922 })
2923 })
2924 .or_else(std::env::home_dir)
2925 .unwrap_or_else(|| PathBuf::from(""));
2926 cx.prompt_for_new_path(&relative_to, suggested_name.as_deref())
2927 })?;
2928 let abs_path = match abs_path.await? {
2929 Ok(path) => path,
2930 Err(err) => {
2931 let rx = workspace.update_in(cx, |workspace, window, cx| {
2932 workspace.show_portal_error(err.to_string(), cx);
2933
2934 let prompt = workspace.on_prompt_for_new_path.take().unwrap();
2935 let rx = prompt(workspace, lister, suggested_name, window, cx);
2936 workspace.on_prompt_for_new_path = Some(prompt);
2937 rx
2938 })?;
2939 if let Ok(path) = rx.await {
2940 tx.send(path).ok();
2941 }
2942 return anyhow::Ok(());
2943 }
2944 };
2945
2946 tx.send(abs_path.map(|path| vec![path])).ok();
2947 anyhow::Ok(())
2948 })
2949 .detach();
2950
2951 rx
2952 }
2953
2954 pub fn titlebar_item(&self) -> Option<AnyView> {
2955 self.titlebar_item.clone()
2956 }
2957
2958 /// Returns the worktree override set by the user (e.g., via the project dropdown).
2959 /// When set, git-related operations should use this worktree instead of deriving
2960 /// the active worktree from the focused file.
2961 pub fn active_worktree_override(&self) -> Option<WorktreeId> {
2962 self.active_worktree_override
2963 }
2964
2965 pub fn set_active_worktree_override(
2966 &mut self,
2967 worktree_id: Option<WorktreeId>,
2968 cx: &mut Context<Self>,
2969 ) {
2970 self.active_worktree_override = worktree_id;
2971 cx.notify();
2972 }
2973
2974 pub fn clear_active_worktree_override(&mut self, cx: &mut Context<Self>) {
2975 self.active_worktree_override = None;
2976 cx.notify();
2977 }
2978
2979 /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
2980 ///
2981 /// If the given workspace has a local project, then it will be passed
2982 /// to the callback. Otherwise, a new empty window will be created.
2983 pub fn with_local_workspace<T, F>(
2984 &mut self,
2985 window: &mut Window,
2986 cx: &mut Context<Self>,
2987 callback: F,
2988 ) -> Task<Result<T>>
2989 where
2990 T: 'static,
2991 F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
2992 {
2993 if self.project.read(cx).is_local() {
2994 Task::ready(Ok(callback(self, window, cx)))
2995 } else {
2996 let env = self.project.read(cx).cli_environment(cx);
2997 let task = Self::new_local(
2998 Vec::new(),
2999 self.app_state.clone(),
3000 None,
3001 env,
3002 None,
3003 OpenMode::Activate,
3004 cx,
3005 );
3006 cx.spawn_in(window, async move |_vh, cx| {
3007 let OpenResult {
3008 window: multi_workspace_window,
3009 ..
3010 } = task.await?;
3011 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
3012 let workspace = multi_workspace.workspace().clone();
3013 workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
3014 })
3015 })
3016 }
3017 }
3018
3019 /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
3020 ///
3021 /// If the given workspace has a local project, then it will be passed
3022 /// to the callback. Otherwise, a new empty window will be created.
3023 pub fn with_local_or_wsl_workspace<T, F>(
3024 &mut self,
3025 window: &mut Window,
3026 cx: &mut Context<Self>,
3027 callback: F,
3028 ) -> Task<Result<T>>
3029 where
3030 T: 'static,
3031 F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
3032 {
3033 let project = self.project.read(cx);
3034 if project.is_local() || project.is_via_wsl_with_host_interop(cx) {
3035 Task::ready(Ok(callback(self, window, cx)))
3036 } else {
3037 let env = self.project.read(cx).cli_environment(cx);
3038 let task = Self::new_local(
3039 Vec::new(),
3040 self.app_state.clone(),
3041 None,
3042 env,
3043 None,
3044 OpenMode::Activate,
3045 cx,
3046 );
3047 cx.spawn_in(window, async move |_vh, cx| {
3048 let OpenResult {
3049 window: multi_workspace_window,
3050 ..
3051 } = task.await?;
3052 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
3053 let workspace = multi_workspace.workspace().clone();
3054 workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
3055 })
3056 })
3057 }
3058 }
3059
3060 pub fn worktrees<'a>(&self, cx: &'a App) -> impl 'a + Iterator<Item = Entity<Worktree>> {
3061 self.project.read(cx).worktrees(cx)
3062 }
3063
3064 pub fn visible_worktrees<'a>(
3065 &self,
3066 cx: &'a App,
3067 ) -> impl 'a + Iterator<Item = Entity<Worktree>> {
3068 self.project.read(cx).visible_worktrees(cx)
3069 }
3070
3071 pub fn worktree_scans_complete(&self, cx: &App) -> impl Future<Output = ()> + 'static + use<> {
3072 let futures = self
3073 .worktrees(cx)
3074 .filter_map(|worktree| worktree.read(cx).as_local())
3075 .map(|worktree| worktree.scan_complete())
3076 .collect::<Vec<_>>();
3077 async move {
3078 for future in futures {
3079 future.await;
3080 }
3081 }
3082 }
3083
3084 pub fn close_global(cx: &mut App) {
3085 cx.defer(|cx| {
3086 cx.windows().iter().find(|window| {
3087 window
3088 .update(cx, |_, window, _| {
3089 if window.is_window_active() {
3090 //This can only get called when the window's project connection has been lost
3091 //so we don't need to prompt the user for anything and instead just close the window
3092 window.remove_window();
3093 true
3094 } else {
3095 false
3096 }
3097 })
3098 .unwrap_or(false)
3099 });
3100 });
3101 }
3102
3103 pub fn move_focused_panel_to_next_position(
3104 &mut self,
3105 _: &MoveFocusedPanelToNextPosition,
3106 window: &mut Window,
3107 cx: &mut Context<Self>,
3108 ) {
3109 let docks = self.all_docks();
3110 let active_dock = docks
3111 .into_iter()
3112 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
3113
3114 if let Some(dock) = active_dock {
3115 dock.update(cx, |dock, cx| {
3116 let active_panel = dock
3117 .active_panel()
3118 .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx));
3119
3120 if let Some(panel) = active_panel {
3121 panel.move_to_next_position(window, cx);
3122 }
3123 })
3124 }
3125 }
3126
3127 pub fn prepare_to_close(
3128 &mut self,
3129 close_intent: CloseIntent,
3130 window: &mut Window,
3131 cx: &mut Context<Self>,
3132 ) -> Task<Result<bool>> {
3133 let active_call = self.active_global_call();
3134
3135 cx.spawn_in(window, async move |this, cx| {
3136 this.update(cx, |this, _| {
3137 if close_intent == CloseIntent::CloseWindow {
3138 this.removing = true;
3139 }
3140 })?;
3141
3142 let workspace_count = cx.update(|_window, cx| {
3143 cx.windows()
3144 .iter()
3145 .filter(|window| window.downcast::<MultiWorkspace>().is_some())
3146 .count()
3147 })?;
3148
3149 #[cfg(target_os = "macos")]
3150 let save_last_workspace = false;
3151
3152 // On Linux and Windows, closing the last window should restore the last workspace.
3153 #[cfg(not(target_os = "macos"))]
3154 let save_last_workspace = {
3155 let remaining_workspaces = cx.update(|_window, cx| {
3156 cx.windows()
3157 .iter()
3158 .filter_map(|window| window.downcast::<MultiWorkspace>())
3159 .filter_map(|multi_workspace| {
3160 multi_workspace
3161 .update(cx, |multi_workspace, _, cx| {
3162 multi_workspace.workspace().read(cx).removing
3163 })
3164 .ok()
3165 })
3166 .filter(|removing| !removing)
3167 .count()
3168 })?;
3169
3170 close_intent != CloseIntent::ReplaceWindow && remaining_workspaces == 0
3171 };
3172
3173 if let Some(active_call) = active_call
3174 && workspace_count == 1
3175 && cx
3176 .update(|_window, cx| active_call.0.is_in_room(cx))
3177 .unwrap_or(false)
3178 {
3179 if close_intent == CloseIntent::CloseWindow {
3180 this.update(cx, |_, cx| cx.emit(Event::Activate))?;
3181 let answer = cx.update(|window, cx| {
3182 window.prompt(
3183 PromptLevel::Warning,
3184 "Do you want to leave the current call?",
3185 None,
3186 &["Close window and hang up", "Cancel"],
3187 cx,
3188 )
3189 })?;
3190
3191 if answer.await.log_err() == Some(1) {
3192 return anyhow::Ok(false);
3193 } else {
3194 if let Ok(task) = cx.update(|_window, cx| active_call.0.hang_up(cx)) {
3195 task.await.log_err();
3196 }
3197 }
3198 }
3199 if close_intent == CloseIntent::ReplaceWindow {
3200 _ = cx.update(|_window, cx| {
3201 let multi_workspace = cx
3202 .windows()
3203 .iter()
3204 .filter_map(|window| window.downcast::<MultiWorkspace>())
3205 .next()
3206 .unwrap();
3207 let project = multi_workspace
3208 .read(cx)?
3209 .workspace()
3210 .read(cx)
3211 .project
3212 .clone();
3213 if project.read(cx).is_shared() {
3214 active_call.0.unshare_project(project, cx)?;
3215 }
3216 Ok::<_, anyhow::Error>(())
3217 });
3218 }
3219 }
3220
3221 let save_result = this
3222 .update_in(cx, |this, window, cx| {
3223 this.save_all_internal(SaveIntent::Close, window, cx)
3224 })?
3225 .await;
3226
3227 // If we're not quitting, but closing, we remove the workspace from
3228 // the current session.
3229 if close_intent != CloseIntent::Quit
3230 && !save_last_workspace
3231 && save_result.as_ref().is_ok_and(|&res| res)
3232 {
3233 this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx))?
3234 .await;
3235 }
3236
3237 save_result
3238 })
3239 }
3240
3241 fn save_all(&mut self, action: &SaveAll, window: &mut Window, cx: &mut Context<Self>) {
3242 self.save_all_internal(
3243 action.save_intent.unwrap_or(SaveIntent::SaveAll),
3244 window,
3245 cx,
3246 )
3247 .detach_and_log_err(cx);
3248 }
3249
3250 fn send_keystrokes(
3251 &mut self,
3252 action: &SendKeystrokes,
3253 window: &mut Window,
3254 cx: &mut Context<Self>,
3255 ) {
3256 let keystrokes: Vec<Keystroke> = action
3257 .0
3258 .split(' ')
3259 .flat_map(|k| Keystroke::parse(k).log_err())
3260 .map(|k| {
3261 cx.keyboard_mapper()
3262 .map_key_equivalent(k, false)
3263 .inner()
3264 .clone()
3265 })
3266 .collect();
3267 let _ = self.send_keystrokes_impl(keystrokes, window, cx);
3268 }
3269
3270 pub fn send_keystrokes_impl(
3271 &mut self,
3272 keystrokes: Vec<Keystroke>,
3273 window: &mut Window,
3274 cx: &mut Context<Self>,
3275 ) -> Shared<Task<()>> {
3276 let mut state = self.dispatching_keystrokes.borrow_mut();
3277 if !state.dispatched.insert(keystrokes.clone()) {
3278 cx.propagate();
3279 return state.task.clone().unwrap();
3280 }
3281
3282 state.queue.extend(keystrokes);
3283
3284 let keystrokes = self.dispatching_keystrokes.clone();
3285 if state.task.is_none() {
3286 state.task = Some(
3287 window
3288 .spawn(cx, async move |cx| {
3289 // limit to 100 keystrokes to avoid infinite recursion.
3290 for _ in 0..100 {
3291 let keystroke = {
3292 let mut state = keystrokes.borrow_mut();
3293 let Some(keystroke) = state.queue.pop_front() else {
3294 state.dispatched.clear();
3295 state.task.take();
3296 return;
3297 };
3298 keystroke
3299 };
3300 cx.update(|window, cx| {
3301 let focused = window.focused(cx);
3302 window.dispatch_keystroke(keystroke.clone(), cx);
3303 if window.focused(cx) != focused {
3304 // dispatch_keystroke may cause the focus to change.
3305 // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
3306 // And we need that to happen before the next keystroke to keep vim mode happy...
3307 // (Note that the tests always do this implicitly, so you must manually test with something like:
3308 // "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
3309 // )
3310 window.draw(cx).clear();
3311 }
3312 })
3313 .ok();
3314
3315 // Yield between synthetic keystrokes so deferred focus and
3316 // other effects can settle before dispatching the next key.
3317 yield_now().await;
3318 }
3319
3320 *keystrokes.borrow_mut() = Default::default();
3321 log::error!("over 100 keystrokes passed to send_keystrokes");
3322 })
3323 .shared(),
3324 );
3325 }
3326 state.task.clone().unwrap()
3327 }
3328
3329 /// Prompts the user to save or discard each dirty item, returning
3330 /// `true` if they confirmed (saved/discarded everything) or `false`
3331 /// if they cancelled. Used before removing worktree roots during
3332 /// thread archival.
3333 pub fn prompt_to_save_or_discard_dirty_items(
3334 &mut self,
3335 window: &mut Window,
3336 cx: &mut Context<Self>,
3337 ) -> Task<Result<bool>> {
3338 self.save_all_internal(SaveIntent::Close, window, cx)
3339 }
3340
3341 fn save_all_internal(
3342 &mut self,
3343 mut save_intent: SaveIntent,
3344 window: &mut Window,
3345 cx: &mut Context<Self>,
3346 ) -> Task<Result<bool>> {
3347 if self.project.read(cx).is_disconnected(cx) {
3348 return Task::ready(Ok(true));
3349 }
3350 let dirty_items = self
3351 .panes
3352 .iter()
3353 .flat_map(|pane| {
3354 pane.read(cx).items().filter_map(|item| {
3355 if item.is_dirty(cx) {
3356 item.tab_content_text(0, cx);
3357 Some((pane.downgrade(), item.boxed_clone()))
3358 } else {
3359 None
3360 }
3361 })
3362 })
3363 .collect::<Vec<_>>();
3364
3365 let project = self.project.clone();
3366 cx.spawn_in(window, async move |workspace, cx| {
3367 let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() {
3368 let (serialize_tasks, remaining_dirty_items) =
3369 workspace.update_in(cx, |workspace, window, cx| {
3370 let mut remaining_dirty_items = Vec::new();
3371 let mut serialize_tasks = Vec::new();
3372 for (pane, item) in dirty_items {
3373 if let Some(task) = item
3374 .to_serializable_item_handle(cx)
3375 .and_then(|handle| handle.serialize(workspace, true, window, cx))
3376 {
3377 serialize_tasks.push(task);
3378 } else {
3379 remaining_dirty_items.push((pane, item));
3380 }
3381 }
3382 (serialize_tasks, remaining_dirty_items)
3383 })?;
3384
3385 futures::future::try_join_all(serialize_tasks).await?;
3386
3387 if !remaining_dirty_items.is_empty() {
3388 workspace.update(cx, |_, cx| cx.emit(Event::Activate))?;
3389 }
3390
3391 if remaining_dirty_items.len() > 1 {
3392 let answer = workspace.update_in(cx, |_, window, cx| {
3393 cx.emit(Event::Activate);
3394 let detail = Pane::file_names_for_prompt(
3395 &mut remaining_dirty_items.iter().map(|(_, handle)| handle),
3396 cx,
3397 );
3398 window.prompt(
3399 PromptLevel::Warning,
3400 "Do you want to save all changes in the following files?",
3401 Some(&detail),
3402 &["Save all", "Discard all", "Cancel"],
3403 cx,
3404 )
3405 })?;
3406 match answer.await.log_err() {
3407 Some(0) => save_intent = SaveIntent::SaveAll,
3408 Some(1) => save_intent = SaveIntent::Skip,
3409 Some(2) => return Ok(false),
3410 _ => {}
3411 }
3412 }
3413
3414 remaining_dirty_items
3415 } else {
3416 dirty_items
3417 };
3418
3419 for (pane, item) in dirty_items {
3420 let (singleton, project_entry_ids) = cx.update(|_, cx| {
3421 (
3422 item.buffer_kind(cx) == ItemBufferKind::Singleton,
3423 item.project_entry_ids(cx),
3424 )
3425 })?;
3426 if (singleton || !project_entry_ids.is_empty())
3427 && !Pane::save_item(project.clone(), &pane, &*item, save_intent, cx).await?
3428 {
3429 return Ok(false);
3430 }
3431 }
3432 Ok(true)
3433 })
3434 }
3435
3436 pub fn open_workspace_for_paths(
3437 &mut self,
3438 // replace_current_window: bool,
3439 mut open_mode: OpenMode,
3440 paths: Vec<PathBuf>,
3441 window: &mut Window,
3442 cx: &mut Context<Self>,
3443 ) -> Task<Result<Entity<Workspace>>> {
3444 let requesting_window = window.window_handle().downcast::<MultiWorkspace>();
3445 let is_remote = self.project.read(cx).is_via_collab();
3446 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
3447 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
3448
3449 let workspace_is_empty = !is_remote && !has_worktree && !has_dirty_items;
3450 if workspace_is_empty {
3451 open_mode = OpenMode::Activate;
3452 }
3453
3454 let app_state = self.app_state.clone();
3455
3456 cx.spawn(async move |_, cx| {
3457 let OpenResult { workspace, .. } = cx
3458 .update(|cx| {
3459 open_paths(
3460 &paths,
3461 app_state,
3462 OpenOptions {
3463 requesting_window,
3464 open_mode,
3465 ..Default::default()
3466 },
3467 cx,
3468 )
3469 })
3470 .await?;
3471 Ok(workspace)
3472 })
3473 }
3474
3475 #[allow(clippy::type_complexity)]
3476 pub fn open_paths(
3477 &mut self,
3478 mut abs_paths: Vec<PathBuf>,
3479 options: OpenOptions,
3480 pane: Option<WeakEntity<Pane>>,
3481 window: &mut Window,
3482 cx: &mut Context<Self>,
3483 ) -> Task<Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>> {
3484 let fs = self.app_state.fs.clone();
3485
3486 let caller_ordered_abs_paths = abs_paths.clone();
3487
3488 // Sort the paths to ensure we add worktrees for parents before their children.
3489 abs_paths.sort_unstable();
3490 cx.spawn_in(window, async move |this, cx| {
3491 let mut tasks = Vec::with_capacity(abs_paths.len());
3492
3493 for abs_path in &abs_paths {
3494 let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
3495 OpenVisible::All => Some(true),
3496 OpenVisible::None => Some(false),
3497 OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
3498 Some(Some(metadata)) => Some(!metadata.is_dir),
3499 Some(None) => Some(true),
3500 None => None,
3501 },
3502 OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
3503 Some(Some(metadata)) => Some(metadata.is_dir),
3504 Some(None) => Some(false),
3505 None => None,
3506 },
3507 };
3508 let project_path = match visible {
3509 Some(visible) => match this
3510 .update(cx, |this, cx| {
3511 Workspace::project_path_for_path(
3512 this.project.clone(),
3513 abs_path,
3514 visible,
3515 cx,
3516 )
3517 })
3518 .log_err()
3519 {
3520 Some(project_path) => project_path.await.log_err(),
3521 None => None,
3522 },
3523 None => None,
3524 };
3525
3526 let this = this.clone();
3527 let abs_path: Arc<Path> = SanitizedPath::new(&abs_path).as_path().into();
3528 let fs = fs.clone();
3529 let pane = pane.clone();
3530 let task = cx.spawn(async move |cx| {
3531 let (_worktree, project_path) = project_path?;
3532 if fs.is_dir(&abs_path).await {
3533 // Opening a directory should not race to update the active entry.
3534 // We'll select/reveal a deterministic final entry after all paths finish opening.
3535 None
3536 } else {
3537 Some(
3538 this.update_in(cx, |this, window, cx| {
3539 this.open_path(
3540 project_path,
3541 pane,
3542 options.focus.unwrap_or(true),
3543 window,
3544 cx,
3545 )
3546 })
3547 .ok()?
3548 .await,
3549 )
3550 }
3551 });
3552 tasks.push(task);
3553 }
3554
3555 let results = futures::future::join_all(tasks).await;
3556
3557 // Determine the winner using the fake/abstract FS metadata, not `Path::is_dir`.
3558 let mut winner: Option<(PathBuf, bool)> = None;
3559 for abs_path in caller_ordered_abs_paths.into_iter().rev() {
3560 if let Some(Some(metadata)) = fs.metadata(&abs_path).await.log_err() {
3561 if !metadata.is_dir {
3562 winner = Some((abs_path, false));
3563 break;
3564 }
3565 if winner.is_none() {
3566 winner = Some((abs_path, true));
3567 }
3568 } else if winner.is_none() {
3569 winner = Some((abs_path, false));
3570 }
3571 }
3572
3573 // Compute the winner entry id on the foreground thread and emit once, after all
3574 // paths finish opening. This avoids races between concurrently-opening paths
3575 // (directories in particular) and makes the resulting project panel selection
3576 // deterministic.
3577 if let Some((winner_abs_path, winner_is_dir)) = winner {
3578 'emit_winner: {
3579 let winner_abs_path: Arc<Path> =
3580 SanitizedPath::new(&winner_abs_path).as_path().into();
3581
3582 let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
3583 OpenVisible::All => true,
3584 OpenVisible::None => false,
3585 OpenVisible::OnlyFiles => !winner_is_dir,
3586 OpenVisible::OnlyDirectories => winner_is_dir,
3587 };
3588
3589 let Some(worktree_task) = this
3590 .update(cx, |workspace, cx| {
3591 workspace.project.update(cx, |project, cx| {
3592 project.find_or_create_worktree(
3593 winner_abs_path.as_ref(),
3594 visible,
3595 cx,
3596 )
3597 })
3598 })
3599 .ok()
3600 else {
3601 break 'emit_winner;
3602 };
3603
3604 let Ok((worktree, _)) = worktree_task.await else {
3605 break 'emit_winner;
3606 };
3607
3608 let Ok(Some(entry_id)) = this.update(cx, |_, cx| {
3609 let worktree = worktree.read(cx);
3610 let worktree_abs_path = worktree.abs_path();
3611 let entry = if winner_abs_path.as_ref() == worktree_abs_path.as_ref() {
3612 worktree.root_entry()
3613 } else {
3614 winner_abs_path
3615 .strip_prefix(worktree_abs_path.as_ref())
3616 .ok()
3617 .and_then(|relative_path| {
3618 let relative_path =
3619 RelPath::new(relative_path, PathStyle::local())
3620 .log_err()?;
3621 worktree.entry_for_path(&relative_path)
3622 })
3623 }?;
3624 Some(entry.id)
3625 }) else {
3626 break 'emit_winner;
3627 };
3628
3629 this.update(cx, |workspace, cx| {
3630 workspace.project.update(cx, |_, cx| {
3631 cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
3632 });
3633 })
3634 .ok();
3635 }
3636 }
3637
3638 results
3639 })
3640 }
3641
3642 pub fn open_resolved_path(
3643 &mut self,
3644 path: ResolvedPath,
3645 window: &mut Window,
3646 cx: &mut Context<Self>,
3647 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3648 match path {
3649 ResolvedPath::ProjectPath { project_path, .. } => {
3650 self.open_path(project_path, None, true, window, cx)
3651 }
3652 ResolvedPath::AbsPath { path, .. } => self.open_abs_path(
3653 PathBuf::from(path),
3654 OpenOptions {
3655 visible: Some(OpenVisible::None),
3656 ..Default::default()
3657 },
3658 window,
3659 cx,
3660 ),
3661 }
3662 }
3663
3664 pub fn absolute_path_of_worktree(
3665 &self,
3666 worktree_id: WorktreeId,
3667 cx: &mut Context<Self>,
3668 ) -> Option<PathBuf> {
3669 self.project
3670 .read(cx)
3671 .worktree_for_id(worktree_id, cx)
3672 // TODO: use `abs_path` or `root_dir`
3673 .map(|wt| wt.read(cx).abs_path().as_ref().to_path_buf())
3674 }
3675
3676 pub fn add_folder_to_project(
3677 &mut self,
3678 _: &AddFolderToProject,
3679 window: &mut Window,
3680 cx: &mut Context<Self>,
3681 ) {
3682 let project = self.project.read(cx);
3683 if project.is_via_collab() {
3684 self.show_error(
3685 &anyhow!("You cannot add folders to someone else's project"),
3686 cx,
3687 );
3688 return;
3689 }
3690 let paths = self.prompt_for_open_path(
3691 PathPromptOptions {
3692 files: false,
3693 directories: true,
3694 multiple: true,
3695 prompt: None,
3696 },
3697 DirectoryLister::Project(self.project.clone()),
3698 window,
3699 cx,
3700 );
3701 cx.spawn_in(window, async move |this, cx| {
3702 if let Some(paths) = paths.await.log_err().flatten() {
3703 let results = this
3704 .update_in(cx, |this, window, cx| {
3705 this.open_paths(
3706 paths,
3707 OpenOptions {
3708 visible: Some(OpenVisible::All),
3709 ..Default::default()
3710 },
3711 None,
3712 window,
3713 cx,
3714 )
3715 })?
3716 .await;
3717 for result in results.into_iter().flatten() {
3718 result.log_err();
3719 }
3720 }
3721 anyhow::Ok(())
3722 })
3723 .detach_and_log_err(cx);
3724 }
3725
3726 pub fn project_path_for_path(
3727 project: Entity<Project>,
3728 abs_path: &Path,
3729 visible: bool,
3730 cx: &mut App,
3731 ) -> Task<Result<(Entity<Worktree>, ProjectPath)>> {
3732 let entry = project.update(cx, |project, cx| {
3733 project.find_or_create_worktree(abs_path, visible, cx)
3734 });
3735 cx.spawn(async move |cx| {
3736 let (worktree, path) = entry.await?;
3737 let worktree_id = worktree.read_with(cx, |t, _| t.id());
3738 Ok((worktree, ProjectPath { worktree_id, path }))
3739 })
3740 }
3741
3742 pub fn items<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator<Item = &'a Box<dyn ItemHandle>> {
3743 self.panes.iter().flat_map(|pane| pane.read(cx).items())
3744 }
3745
3746 pub fn item_of_type<T: Item>(&self, cx: &App) -> Option<Entity<T>> {
3747 self.items_of_type(cx).max_by_key(|item| item.item_id())
3748 }
3749
3750 pub fn items_of_type<'a, T: Item>(
3751 &'a self,
3752 cx: &'a App,
3753 ) -> impl 'a + Iterator<Item = Entity<T>> {
3754 self.panes
3755 .iter()
3756 .flat_map(|pane| pane.read(cx).items_of_type())
3757 }
3758
3759 pub fn active_item(&self, cx: &App) -> Option<Box<dyn ItemHandle>> {
3760 self.active_pane().read(cx).active_item()
3761 }
3762
3763 pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
3764 let item = self.active_item(cx)?;
3765 item.to_any_view().downcast::<I>().ok()
3766 }
3767
3768 fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
3769 self.active_item(cx).and_then(|item| item.project_path(cx))
3770 }
3771
3772 pub fn most_recent_active_path(&self, cx: &App) -> Option<PathBuf> {
3773 self.recent_navigation_history_iter(cx)
3774 .filter_map(|(path, abs_path)| {
3775 let worktree = self
3776 .project
3777 .read(cx)
3778 .worktree_for_id(path.worktree_id, cx)?;
3779 if worktree.read(cx).is_visible() {
3780 abs_path
3781 } else {
3782 None
3783 }
3784 })
3785 .next()
3786 }
3787
3788 pub fn save_active_item(
3789 &mut self,
3790 save_intent: SaveIntent,
3791 window: &mut Window,
3792 cx: &mut App,
3793 ) -> Task<Result<()>> {
3794 let project = self.project.clone();
3795 let pane = self.active_pane();
3796 let item = pane.read(cx).active_item();
3797 let pane = pane.downgrade();
3798
3799 window.spawn(cx, async move |cx| {
3800 if let Some(item) = item {
3801 Pane::save_item(project, &pane, item.as_ref(), save_intent, cx)
3802 .await
3803 .map(|_| ())
3804 } else {
3805 Ok(())
3806 }
3807 })
3808 }
3809
3810 pub fn close_inactive_items_and_panes(
3811 &mut self,
3812 action: &CloseInactiveTabsAndPanes,
3813 window: &mut Window,
3814 cx: &mut Context<Self>,
3815 ) {
3816 if let Some(task) = self.close_all_internal(
3817 true,
3818 action.save_intent.unwrap_or(SaveIntent::Close),
3819 window,
3820 cx,
3821 ) {
3822 task.detach_and_log_err(cx)
3823 }
3824 }
3825
3826 pub fn close_all_items_and_panes(
3827 &mut self,
3828 action: &CloseAllItemsAndPanes,
3829 window: &mut Window,
3830 cx: &mut Context<Self>,
3831 ) {
3832 if let Some(task) = self.close_all_internal(
3833 false,
3834 action.save_intent.unwrap_or(SaveIntent::Close),
3835 window,
3836 cx,
3837 ) {
3838 task.detach_and_log_err(cx)
3839 }
3840 }
3841
3842 /// Closes the active item across all panes.
3843 pub fn close_item_in_all_panes(
3844 &mut self,
3845 action: &CloseItemInAllPanes,
3846 window: &mut Window,
3847 cx: &mut Context<Self>,
3848 ) {
3849 let Some(active_item) = self.active_pane().read(cx).active_item() else {
3850 return;
3851 };
3852
3853 let save_intent = action.save_intent.unwrap_or(SaveIntent::Close);
3854 let close_pinned = action.close_pinned;
3855
3856 if let Some(project_path) = active_item.project_path(cx) {
3857 self.close_items_with_project_path(
3858 &project_path,
3859 save_intent,
3860 close_pinned,
3861 window,
3862 cx,
3863 );
3864 } else if close_pinned || !self.active_pane().read(cx).is_active_item_pinned() {
3865 let item_id = active_item.item_id();
3866 self.active_pane().update(cx, |pane, cx| {
3867 pane.close_item_by_id(item_id, save_intent, window, cx)
3868 .detach_and_log_err(cx);
3869 });
3870 }
3871 }
3872
3873 /// Closes all items with the given project path across all panes.
3874 pub fn close_items_with_project_path(
3875 &mut self,
3876 project_path: &ProjectPath,
3877 save_intent: SaveIntent,
3878 close_pinned: bool,
3879 window: &mut Window,
3880 cx: &mut Context<Self>,
3881 ) {
3882 let panes = self.panes().to_vec();
3883 for pane in panes {
3884 pane.update(cx, |pane, cx| {
3885 pane.close_items_for_project_path(
3886 project_path,
3887 save_intent,
3888 close_pinned,
3889 window,
3890 cx,
3891 )
3892 .detach_and_log_err(cx);
3893 });
3894 }
3895 }
3896
3897 fn close_all_internal(
3898 &mut self,
3899 retain_active_pane: bool,
3900 save_intent: SaveIntent,
3901 window: &mut Window,
3902 cx: &mut Context<Self>,
3903 ) -> Option<Task<Result<()>>> {
3904 let current_pane = self.active_pane();
3905
3906 let mut tasks = Vec::new();
3907
3908 if retain_active_pane {
3909 let current_pane_close = current_pane.update(cx, |pane, cx| {
3910 pane.close_other_items(
3911 &CloseOtherItems {
3912 save_intent: None,
3913 close_pinned: false,
3914 },
3915 None,
3916 window,
3917 cx,
3918 )
3919 });
3920
3921 tasks.push(current_pane_close);
3922 }
3923
3924 for pane in self.panes() {
3925 if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
3926 continue;
3927 }
3928
3929 let close_pane_items = pane.update(cx, |pane: &mut Pane, cx| {
3930 pane.close_all_items(
3931 &CloseAllItems {
3932 save_intent: Some(save_intent),
3933 close_pinned: false,
3934 },
3935 window,
3936 cx,
3937 )
3938 });
3939
3940 tasks.push(close_pane_items)
3941 }
3942
3943 if tasks.is_empty() {
3944 None
3945 } else {
3946 Some(cx.spawn_in(window, async move |_, _| {
3947 for task in tasks {
3948 task.await?
3949 }
3950 Ok(())
3951 }))
3952 }
3953 }
3954
3955 pub fn is_dock_at_position_open(&self, position: DockPosition, cx: &mut Context<Self>) -> bool {
3956 self.dock_at_position(position).read(cx).is_open()
3957 }
3958
3959 pub fn toggle_dock(
3960 &mut self,
3961 dock_side: DockPosition,
3962 window: &mut Window,
3963 cx: &mut Context<Self>,
3964 ) {
3965 let mut focus_center = false;
3966 let mut reveal_dock = false;
3967
3968 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
3969 let was_visible = self.is_dock_at_position_open(dock_side, cx) && !other_is_zoomed;
3970
3971 if let Some(panel) = self.dock_at_position(dock_side).read(cx).active_panel() {
3972 telemetry::event!(
3973 "Panel Button Clicked",
3974 name = panel.persistent_name(),
3975 toggle_state = !was_visible
3976 );
3977 }
3978 if was_visible {
3979 self.save_open_dock_positions(cx);
3980 }
3981
3982 let dock = self.dock_at_position(dock_side);
3983 dock.update(cx, |dock, cx| {
3984 dock.set_open(!was_visible, window, cx);
3985
3986 if dock.active_panel().is_none() {
3987 let Some(panel_ix) = dock
3988 .first_enabled_panel_idx(cx)
3989 .log_with_level(log::Level::Info)
3990 else {
3991 return;
3992 };
3993 dock.activate_panel(panel_ix, window, cx);
3994 }
3995
3996 if let Some(active_panel) = dock.active_panel() {
3997 if was_visible {
3998 if active_panel
3999 .panel_focus_handle(cx)
4000 .contains_focused(window, cx)
4001 {
4002 focus_center = true;
4003 }
4004 } else {
4005 let focus_handle = &active_panel.panel_focus_handle(cx);
4006 window.focus(focus_handle, cx);
4007 reveal_dock = true;
4008 }
4009 }
4010 });
4011
4012 if reveal_dock {
4013 self.dismiss_zoomed_items_to_reveal(Some(dock_side), window, cx);
4014 }
4015
4016 if focus_center {
4017 self.active_pane
4018 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
4019 }
4020
4021 cx.notify();
4022 self.serialize_workspace(window, cx);
4023 }
4024
4025 fn active_dock(&self, window: &Window, cx: &Context<Self>) -> Option<&Entity<Dock>> {
4026 self.all_docks().into_iter().find(|&dock| {
4027 dock.read(cx).is_open() && dock.focus_handle(cx).contains_focused(window, cx)
4028 })
4029 }
4030
4031 fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
4032 if let Some(dock) = self.active_dock(window, cx).cloned() {
4033 self.save_open_dock_positions(cx);
4034 dock.update(cx, |dock, cx| {
4035 dock.set_open(false, window, cx);
4036 });
4037 return true;
4038 }
4039 false
4040 }
4041
4042 pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4043 self.save_open_dock_positions(cx);
4044 for dock in self.all_docks() {
4045 dock.update(cx, |dock, cx| {
4046 dock.set_open(false, window, cx);
4047 });
4048 }
4049
4050 cx.focus_self(window);
4051 cx.notify();
4052 self.serialize_workspace(window, cx);
4053 }
4054
4055 fn get_open_dock_positions(&self, cx: &Context<Self>) -> Vec<DockPosition> {
4056 self.all_docks()
4057 .into_iter()
4058 .filter_map(|dock| {
4059 let dock_ref = dock.read(cx);
4060 if dock_ref.is_open() {
4061 Some(dock_ref.position())
4062 } else {
4063 None
4064 }
4065 })
4066 .collect()
4067 }
4068
4069 /// Saves the positions of currently open docks.
4070 ///
4071 /// Updates `last_open_dock_positions` with positions of all currently open
4072 /// docks, to later be restored by the 'Toggle All Docks' action.
4073 fn save_open_dock_positions(&mut self, cx: &mut Context<Self>) {
4074 let open_dock_positions = self.get_open_dock_positions(cx);
4075 if !open_dock_positions.is_empty() {
4076 self.last_open_dock_positions = open_dock_positions;
4077 }
4078 }
4079
4080 /// Toggles all docks between open and closed states.
4081 ///
4082 /// If any docks are open, closes all and remembers their positions. If all
4083 /// docks are closed, restores the last remembered dock configuration.
4084 fn toggle_all_docks(
4085 &mut self,
4086 _: &ToggleAllDocks,
4087 window: &mut Window,
4088 cx: &mut Context<Self>,
4089 ) {
4090 let open_dock_positions = self.get_open_dock_positions(cx);
4091
4092 if !open_dock_positions.is_empty() {
4093 self.close_all_docks(window, cx);
4094 } else if !self.last_open_dock_positions.is_empty() {
4095 self.restore_last_open_docks(window, cx);
4096 }
4097 }
4098
4099 /// Reopens docks from the most recently remembered configuration.
4100 ///
4101 /// Opens all docks whose positions are stored in `last_open_dock_positions`
4102 /// and clears the stored positions.
4103 fn restore_last_open_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4104 let positions_to_open = std::mem::take(&mut self.last_open_dock_positions);
4105
4106 for position in positions_to_open {
4107 let dock = self.dock_at_position(position);
4108 dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
4109 }
4110
4111 cx.focus_self(window);
4112 cx.notify();
4113 self.serialize_workspace(window, cx);
4114 }
4115
4116 /// Transfer focus to the panel of the given type.
4117 pub fn focus_panel<T: Panel>(
4118 &mut self,
4119 window: &mut Window,
4120 cx: &mut Context<Self>,
4121 ) -> Option<Entity<T>> {
4122 let panel = self.focus_or_unfocus_panel::<T>(window, cx, &mut |_, _, _| true)?;
4123 panel.to_any().downcast().ok()
4124 }
4125
4126 /// Focus the panel of the given type if it isn't already focused. If it is
4127 /// already focused, then transfer focus back to the workspace center.
4128 /// When the `close_panel_on_toggle` setting is enabled, also closes the
4129 /// panel when transferring focus back to the center.
4130 pub fn toggle_panel_focus<T: Panel>(
4131 &mut self,
4132 window: &mut Window,
4133 cx: &mut Context<Self>,
4134 ) -> bool {
4135 let mut did_focus_panel = false;
4136 self.focus_or_unfocus_panel::<T>(window, cx, &mut |panel, window, cx| {
4137 did_focus_panel = !panel.panel_focus_handle(cx).contains_focused(window, cx);
4138 did_focus_panel
4139 });
4140
4141 if !did_focus_panel && WorkspaceSettings::get_global(cx).close_panel_on_toggle {
4142 self.close_panel::<T>(window, cx);
4143 }
4144
4145 telemetry::event!(
4146 "Panel Button Clicked",
4147 name = T::persistent_name(),
4148 toggle_state = did_focus_panel
4149 );
4150
4151 did_focus_panel
4152 }
4153
4154 pub fn focus_center_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4155 if let Some(item) = self.active_item(cx) {
4156 item.item_focus_handle(cx).focus(window, cx);
4157 } else {
4158 log::error!("Could not find a focus target when switching focus to the center panes",);
4159 }
4160 }
4161
4162 pub fn activate_panel_for_proto_id(
4163 &mut self,
4164 panel_id: PanelId,
4165 window: &mut Window,
4166 cx: &mut Context<Self>,
4167 ) -> Option<Arc<dyn PanelHandle>> {
4168 let mut panel = None;
4169 for dock in self.all_docks() {
4170 if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
4171 panel = dock.update(cx, |dock, cx| {
4172 dock.activate_panel(panel_index, window, cx);
4173 dock.set_open(true, window, cx);
4174 dock.active_panel().cloned()
4175 });
4176 break;
4177 }
4178 }
4179
4180 if panel.is_some() {
4181 cx.notify();
4182 self.serialize_workspace(window, cx);
4183 }
4184
4185 panel
4186 }
4187
4188 /// Focus or unfocus the given panel type, depending on the given callback.
4189 fn focus_or_unfocus_panel<T: Panel>(
4190 &mut self,
4191 window: &mut Window,
4192 cx: &mut Context<Self>,
4193 should_focus: &mut dyn FnMut(&dyn PanelHandle, &mut Window, &mut Context<Dock>) -> bool,
4194 ) -> Option<Arc<dyn PanelHandle>> {
4195 let mut result_panel = None;
4196 let mut serialize = false;
4197 for dock in self.all_docks() {
4198 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
4199 let mut focus_center = false;
4200 let panel = dock.update(cx, |dock, cx| {
4201 dock.activate_panel(panel_index, window, cx);
4202
4203 let panel = dock.active_panel().cloned();
4204 if let Some(panel) = panel.as_ref() {
4205 if should_focus(&**panel, window, cx) {
4206 dock.set_open(true, window, cx);
4207 panel.panel_focus_handle(cx).focus(window, cx);
4208 } else {
4209 focus_center = true;
4210 }
4211 }
4212 panel
4213 });
4214
4215 if focus_center {
4216 self.active_pane
4217 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
4218 }
4219
4220 result_panel = panel;
4221 serialize = true;
4222 break;
4223 }
4224 }
4225
4226 if serialize {
4227 self.serialize_workspace(window, cx);
4228 }
4229
4230 cx.notify();
4231 result_panel
4232 }
4233
4234 /// Open the panel of the given type
4235 pub fn open_panel<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4236 for dock in self.all_docks() {
4237 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
4238 dock.update(cx, |dock, cx| {
4239 dock.activate_panel(panel_index, window, cx);
4240 dock.set_open(true, window, cx);
4241 });
4242 }
4243 }
4244 }
4245
4246 /// Open the panel of the given type, dismissing any zoomed items that
4247 /// would obscure it (e.g. a zoomed terminal).
4248 pub fn reveal_panel<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4249 let dock_position = self.all_docks().iter().find_map(|dock| {
4250 let dock = dock.read(cx);
4251 dock.panel_index_for_type::<T>().map(|_| dock.position())
4252 });
4253 self.dismiss_zoomed_items_to_reveal(dock_position, window, cx);
4254 self.open_panel::<T>(window, cx);
4255 }
4256
4257 pub fn close_panel<T: Panel>(&self, window: &mut Window, cx: &mut Context<Self>) {
4258 for dock in self.all_docks().iter() {
4259 dock.update(cx, |dock, cx| {
4260 if dock.panel::<T>().is_some() {
4261 dock.set_open(false, window, cx)
4262 }
4263 })
4264 }
4265 }
4266
4267 pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
4268 self.all_docks()
4269 .iter()
4270 .find_map(|dock| dock.read(cx).panel::<T>())
4271 }
4272
4273 fn dismiss_zoomed_items_to_reveal(
4274 &mut self,
4275 dock_to_reveal: Option<DockPosition>,
4276 window: &mut Window,
4277 cx: &mut Context<Self>,
4278 ) {
4279 // If a center pane is zoomed, unzoom it.
4280 for pane in &self.panes {
4281 if pane != &self.active_pane || dock_to_reveal.is_some() {
4282 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
4283 }
4284 }
4285
4286 // If another dock is zoomed, hide it.
4287 let mut focus_center = false;
4288 for dock in self.all_docks() {
4289 dock.update(cx, |dock, cx| {
4290 if Some(dock.position()) != dock_to_reveal
4291 && let Some(panel) = dock.active_panel()
4292 && panel.is_zoomed(window, cx)
4293 {
4294 focus_center |= panel.panel_focus_handle(cx).contains_focused(window, cx);
4295 dock.set_open(false, window, cx);
4296 }
4297 });
4298 }
4299
4300 if focus_center {
4301 self.active_pane
4302 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
4303 }
4304
4305 if self.zoomed_position != dock_to_reveal {
4306 self.zoomed = None;
4307 self.zoomed_position = None;
4308 cx.emit(Event::ZoomChanged);
4309 }
4310
4311 cx.notify();
4312 }
4313
4314 fn add_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
4315 let pane = cx.new(|cx| {
4316 let mut pane = Pane::new(
4317 self.weak_handle(),
4318 self.project.clone(),
4319 self.pane_history_timestamp.clone(),
4320 None,
4321 NewFile.boxed_clone(),
4322 true,
4323 window,
4324 cx,
4325 );
4326 pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
4327 pane
4328 });
4329 cx.subscribe_in(&pane, window, Self::handle_pane_event)
4330 .detach();
4331 self.panes.push(pane.clone());
4332
4333 window.focus(&pane.focus_handle(cx), cx);
4334
4335 cx.emit(Event::PaneAdded(pane.clone()));
4336 pane
4337 }
4338
4339 pub fn add_item_to_center(
4340 &mut self,
4341 item: Box<dyn ItemHandle>,
4342 window: &mut Window,
4343 cx: &mut Context<Self>,
4344 ) -> bool {
4345 if let Some(center_pane) = self.last_active_center_pane.clone() {
4346 if let Some(center_pane) = center_pane.upgrade() {
4347 center_pane.update(cx, |pane, cx| {
4348 pane.add_item(item, true, true, None, window, cx)
4349 });
4350 true
4351 } else {
4352 false
4353 }
4354 } else {
4355 false
4356 }
4357 }
4358
4359 pub fn add_item_to_active_pane(
4360 &mut self,
4361 item: Box<dyn ItemHandle>,
4362 destination_index: Option<usize>,
4363 focus_item: bool,
4364 window: &mut Window,
4365 cx: &mut App,
4366 ) {
4367 self.add_item(
4368 self.active_pane.clone(),
4369 item,
4370 destination_index,
4371 false,
4372 focus_item,
4373 window,
4374 cx,
4375 )
4376 }
4377
4378 pub fn add_item(
4379 &mut self,
4380 pane: Entity<Pane>,
4381 item: Box<dyn ItemHandle>,
4382 destination_index: Option<usize>,
4383 activate_pane: bool,
4384 focus_item: bool,
4385 window: &mut Window,
4386 cx: &mut App,
4387 ) {
4388 pane.update(cx, |pane, cx| {
4389 pane.add_item(
4390 item,
4391 activate_pane,
4392 focus_item,
4393 destination_index,
4394 window,
4395 cx,
4396 )
4397 });
4398 }
4399
4400 pub fn split_item(
4401 &mut self,
4402 split_direction: SplitDirection,
4403 item: Box<dyn ItemHandle>,
4404 window: &mut Window,
4405 cx: &mut Context<Self>,
4406 ) {
4407 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, window, cx);
4408 self.add_item(new_pane, item, None, true, true, window, cx);
4409 }
4410
4411 pub fn open_abs_path(
4412 &mut self,
4413 abs_path: PathBuf,
4414 options: OpenOptions,
4415 window: &mut Window,
4416 cx: &mut Context<Self>,
4417 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4418 cx.spawn_in(window, async move |workspace, cx| {
4419 let open_paths_task_result = workspace
4420 .update_in(cx, |workspace, window, cx| {
4421 workspace.open_paths(vec![abs_path.clone()], options, None, window, cx)
4422 })
4423 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
4424 .await;
4425 anyhow::ensure!(
4426 open_paths_task_result.len() == 1,
4427 "open abs path {abs_path:?} task returned incorrect number of results"
4428 );
4429 match open_paths_task_result
4430 .into_iter()
4431 .next()
4432 .expect("ensured single task result")
4433 {
4434 Some(open_result) => {
4435 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
4436 }
4437 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
4438 }
4439 })
4440 }
4441
4442 pub fn split_abs_path(
4443 &mut self,
4444 abs_path: PathBuf,
4445 visible: bool,
4446 window: &mut Window,
4447 cx: &mut Context<Self>,
4448 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4449 let project_path_task =
4450 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
4451 cx.spawn_in(window, async move |this, cx| {
4452 let (_, path) = project_path_task.await?;
4453 this.update_in(cx, |this, window, cx| this.split_path(path, window, cx))?
4454 .await
4455 })
4456 }
4457
4458 pub fn open_path(
4459 &mut self,
4460 path: impl Into<ProjectPath>,
4461 pane: Option<WeakEntity<Pane>>,
4462 focus_item: bool,
4463 window: &mut Window,
4464 cx: &mut App,
4465 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4466 self.open_path_preview(path, pane, focus_item, false, true, window, cx)
4467 }
4468
4469 pub fn open_path_preview(
4470 &mut self,
4471 path: impl Into<ProjectPath>,
4472 pane: Option<WeakEntity<Pane>>,
4473 focus_item: bool,
4474 allow_preview: bool,
4475 activate: bool,
4476 window: &mut Window,
4477 cx: &mut App,
4478 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4479 let pane = pane.unwrap_or_else(|| {
4480 self.last_active_center_pane.clone().unwrap_or_else(|| {
4481 self.panes
4482 .first()
4483 .expect("There must be an active pane")
4484 .downgrade()
4485 })
4486 });
4487
4488 let project_path = path.into();
4489 let task = self.load_path(project_path.clone(), window, cx);
4490 window.spawn(cx, async move |cx| {
4491 let (project_entry_id, build_item) = task.await?;
4492
4493 pane.update_in(cx, |pane, window, cx| {
4494 pane.open_item(
4495 project_entry_id,
4496 project_path,
4497 focus_item,
4498 allow_preview,
4499 activate,
4500 None,
4501 window,
4502 cx,
4503 build_item,
4504 )
4505 })
4506 })
4507 }
4508
4509 pub fn split_path(
4510 &mut self,
4511 path: impl Into<ProjectPath>,
4512 window: &mut Window,
4513 cx: &mut Context<Self>,
4514 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4515 self.split_path_preview(path, false, None, window, cx)
4516 }
4517
4518 pub fn split_path_preview(
4519 &mut self,
4520 path: impl Into<ProjectPath>,
4521 allow_preview: bool,
4522 split_direction: Option<SplitDirection>,
4523 window: &mut Window,
4524 cx: &mut Context<Self>,
4525 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4526 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
4527 self.panes
4528 .first()
4529 .expect("There must be an active pane")
4530 .downgrade()
4531 });
4532
4533 if let Member::Pane(center_pane) = &self.center.root
4534 && center_pane.read(cx).items_len() == 0
4535 {
4536 return self.open_path(path, Some(pane), true, window, cx);
4537 }
4538
4539 let project_path = path.into();
4540 let task = self.load_path(project_path.clone(), window, cx);
4541 cx.spawn_in(window, async move |this, cx| {
4542 let (project_entry_id, build_item) = task.await?;
4543 this.update_in(cx, move |this, window, cx| -> Option<_> {
4544 let pane = pane.upgrade()?;
4545 let new_pane = this.split_pane(
4546 pane,
4547 split_direction.unwrap_or(SplitDirection::Right),
4548 window,
4549 cx,
4550 );
4551 new_pane.update(cx, |new_pane, cx| {
4552 Some(new_pane.open_item(
4553 project_entry_id,
4554 project_path,
4555 true,
4556 allow_preview,
4557 true,
4558 None,
4559 window,
4560 cx,
4561 build_item,
4562 ))
4563 })
4564 })
4565 .map(|option| option.context("pane was dropped"))?
4566 })
4567 }
4568
4569 fn load_path(
4570 &mut self,
4571 path: ProjectPath,
4572 window: &mut Window,
4573 cx: &mut App,
4574 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
4575 let registry = cx.default_global::<ProjectItemRegistry>().clone();
4576 registry.open_path(self.project(), &path, window, cx)
4577 }
4578
4579 pub fn find_project_item<T>(
4580 &self,
4581 pane: &Entity<Pane>,
4582 project_item: &Entity<T::Item>,
4583 cx: &App,
4584 ) -> Option<Entity<T>>
4585 where
4586 T: ProjectItem,
4587 {
4588 use project::ProjectItem as _;
4589 let project_item = project_item.read(cx);
4590 let entry_id = project_item.entry_id(cx);
4591 let project_path = project_item.project_path(cx);
4592
4593 let mut item = None;
4594 if let Some(entry_id) = entry_id {
4595 item = pane.read(cx).item_for_entry(entry_id, cx);
4596 }
4597 if item.is_none()
4598 && let Some(project_path) = project_path
4599 {
4600 item = pane.read(cx).item_for_path(project_path, cx);
4601 }
4602
4603 item.and_then(|item| item.downcast::<T>())
4604 }
4605
4606 pub fn is_project_item_open<T>(
4607 &self,
4608 pane: &Entity<Pane>,
4609 project_item: &Entity<T::Item>,
4610 cx: &App,
4611 ) -> bool
4612 where
4613 T: ProjectItem,
4614 {
4615 self.find_project_item::<T>(pane, project_item, cx)
4616 .is_some()
4617 }
4618
4619 pub fn open_project_item<T>(
4620 &mut self,
4621 pane: Entity<Pane>,
4622 project_item: Entity<T::Item>,
4623 activate_pane: bool,
4624 focus_item: bool,
4625 keep_old_preview: bool,
4626 allow_new_preview: bool,
4627 window: &mut Window,
4628 cx: &mut Context<Self>,
4629 ) -> Entity<T>
4630 where
4631 T: ProjectItem,
4632 {
4633 let old_item_id = pane.read(cx).active_item().map(|item| item.item_id());
4634
4635 if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
4636 if !keep_old_preview
4637 && let Some(old_id) = old_item_id
4638 && old_id != item.item_id()
4639 {
4640 // switching to a different item, so unpreview old active item
4641 pane.update(cx, |pane, _| {
4642 pane.unpreview_item_if_preview(old_id);
4643 });
4644 }
4645
4646 self.activate_item(&item, activate_pane, focus_item, window, cx);
4647 if !allow_new_preview {
4648 pane.update(cx, |pane, _| {
4649 pane.unpreview_item_if_preview(item.item_id());
4650 });
4651 }
4652 return item;
4653 }
4654
4655 let item = pane.update(cx, |pane, cx| {
4656 cx.new(|cx| {
4657 T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx)
4658 })
4659 });
4660 let mut destination_index = None;
4661 pane.update(cx, |pane, cx| {
4662 if !keep_old_preview && let Some(old_id) = old_item_id {
4663 pane.unpreview_item_if_preview(old_id);
4664 }
4665 if allow_new_preview {
4666 destination_index = pane.replace_preview_item_id(item.item_id(), window, cx);
4667 }
4668 });
4669
4670 self.add_item(
4671 pane,
4672 Box::new(item.clone()),
4673 destination_index,
4674 activate_pane,
4675 focus_item,
4676 window,
4677 cx,
4678 );
4679 item
4680 }
4681
4682 pub fn open_shared_screen(
4683 &mut self,
4684 peer_id: PeerId,
4685 window: &mut Window,
4686 cx: &mut Context<Self>,
4687 ) {
4688 if let Some(shared_screen) =
4689 self.shared_screen_for_peer(peer_id, &self.active_pane, window, cx)
4690 {
4691 self.active_pane.update(cx, |pane, cx| {
4692 pane.add_item(Box::new(shared_screen), false, true, None, window, cx)
4693 });
4694 }
4695 }
4696
4697 pub fn activate_item(
4698 &mut self,
4699 item: &dyn ItemHandle,
4700 activate_pane: bool,
4701 focus_item: bool,
4702 window: &mut Window,
4703 cx: &mut App,
4704 ) -> bool {
4705 let result = self.panes.iter().find_map(|pane| {
4706 pane.read(cx)
4707 .index_for_item(item)
4708 .map(|ix| (pane.clone(), ix))
4709 });
4710 if let Some((pane, ix)) = result {
4711 pane.update(cx, |pane, cx| {
4712 pane.activate_item(ix, activate_pane, focus_item, window, cx)
4713 });
4714 true
4715 } else {
4716 false
4717 }
4718 }
4719
4720 fn activate_pane_at_index(
4721 &mut self,
4722 action: &ActivatePane,
4723 window: &mut Window,
4724 cx: &mut Context<Self>,
4725 ) {
4726 let panes = self.center.panes();
4727 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
4728 window.focus(&pane.focus_handle(cx), cx);
4729 } else {
4730 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx)
4731 .detach();
4732 }
4733 }
4734
4735 fn move_item_to_pane_at_index(
4736 &mut self,
4737 action: &MoveItemToPane,
4738 window: &mut Window,
4739 cx: &mut Context<Self>,
4740 ) {
4741 let panes = self.center.panes();
4742 let destination = match panes.get(action.destination) {
4743 Some(&destination) => destination.clone(),
4744 None => {
4745 if !action.clone && self.active_pane.read(cx).items_len() < 2 {
4746 return;
4747 }
4748 let direction = SplitDirection::Right;
4749 let split_off_pane = self
4750 .find_pane_in_direction(direction, cx)
4751 .unwrap_or_else(|| self.active_pane.clone());
4752 let new_pane = self.add_pane(window, cx);
4753 self.center.split(&split_off_pane, &new_pane, direction, cx);
4754 new_pane
4755 }
4756 };
4757
4758 if action.clone {
4759 if self
4760 .active_pane
4761 .read(cx)
4762 .active_item()
4763 .is_some_and(|item| item.can_split(cx))
4764 {
4765 clone_active_item(
4766 self.database_id(),
4767 &self.active_pane,
4768 &destination,
4769 action.focus,
4770 window,
4771 cx,
4772 );
4773 return;
4774 }
4775 }
4776 move_active_item(
4777 &self.active_pane,
4778 &destination,
4779 action.focus,
4780 true,
4781 window,
4782 cx,
4783 )
4784 }
4785
4786 pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) {
4787 let panes = self.center.panes();
4788 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
4789 let next_ix = (ix + 1) % panes.len();
4790 let next_pane = panes[next_ix].clone();
4791 window.focus(&next_pane.focus_handle(cx), cx);
4792 }
4793 }
4794
4795 pub fn activate_previous_pane(&mut self, window: &mut Window, cx: &mut App) {
4796 let panes = self.center.panes();
4797 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
4798 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
4799 let prev_pane = panes[prev_ix].clone();
4800 window.focus(&prev_pane.focus_handle(cx), cx);
4801 }
4802 }
4803
4804 pub fn activate_last_pane(&mut self, window: &mut Window, cx: &mut App) {
4805 let last_pane = self.center.last_pane();
4806 window.focus(&last_pane.focus_handle(cx), cx);
4807 }
4808
4809 pub fn activate_pane_in_direction(
4810 &mut self,
4811 direction: SplitDirection,
4812 window: &mut Window,
4813 cx: &mut App,
4814 ) {
4815 use ActivateInDirectionTarget as Target;
4816 enum Origin {
4817 Sidebar,
4818 LeftDock,
4819 RightDock,
4820 BottomDock,
4821 Center,
4822 }
4823
4824 let origin: Origin = if self
4825 .sidebar_focus_handle
4826 .as_ref()
4827 .is_some_and(|h| h.contains_focused(window, cx))
4828 {
4829 Origin::Sidebar
4830 } else {
4831 [
4832 (&self.left_dock, Origin::LeftDock),
4833 (&self.right_dock, Origin::RightDock),
4834 (&self.bottom_dock, Origin::BottomDock),
4835 ]
4836 .into_iter()
4837 .find_map(|(dock, origin)| {
4838 if dock.focus_handle(cx).contains_focused(window, cx) && dock.read(cx).is_open() {
4839 Some(origin)
4840 } else {
4841 None
4842 }
4843 })
4844 .unwrap_or(Origin::Center)
4845 };
4846
4847 let get_last_active_pane = || {
4848 let pane = self
4849 .last_active_center_pane
4850 .clone()
4851 .unwrap_or_else(|| {
4852 self.panes
4853 .first()
4854 .expect("There must be an active pane")
4855 .downgrade()
4856 })
4857 .upgrade()?;
4858 (pane.read(cx).items_len() != 0).then_some(pane)
4859 };
4860
4861 let try_dock =
4862 |dock: &Entity<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
4863
4864 let sidebar_target = self
4865 .sidebar_focus_handle
4866 .as_ref()
4867 .map(|h| Target::Sidebar(h.clone()));
4868
4869 let sidebar_on_right = self
4870 .multi_workspace
4871 .as_ref()
4872 .and_then(|mw| mw.upgrade())
4873 .map_or(false, |mw| {
4874 mw.read(cx).sidebar_side(cx) == SidebarSide::Right
4875 });
4876
4877 let away_from_sidebar = if sidebar_on_right {
4878 SplitDirection::Left
4879 } else {
4880 SplitDirection::Right
4881 };
4882
4883 let (near_dock, far_dock) = if sidebar_on_right {
4884 (&self.right_dock, &self.left_dock)
4885 } else {
4886 (&self.left_dock, &self.right_dock)
4887 };
4888
4889 let target = match (origin, direction) {
4890 (Origin::Sidebar, dir) if dir == away_from_sidebar => try_dock(near_dock)
4891 .or_else(|| get_last_active_pane().map(Target::Pane))
4892 .or_else(|| try_dock(&self.bottom_dock))
4893 .or_else(|| try_dock(far_dock)),
4894
4895 (Origin::Sidebar, _) => None,
4896
4897 // We're in the center, so we first try to go to a different pane,
4898 // otherwise try to go to a dock.
4899 (Origin::Center, direction) => {
4900 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
4901 Some(Target::Pane(pane))
4902 } else {
4903 match direction {
4904 SplitDirection::Up => None,
4905 SplitDirection::Down => try_dock(&self.bottom_dock),
4906 SplitDirection::Left => {
4907 let dock_target = try_dock(&self.left_dock);
4908 if sidebar_on_right {
4909 dock_target
4910 } else {
4911 dock_target.or(sidebar_target)
4912 }
4913 }
4914 SplitDirection::Right => {
4915 let dock_target = try_dock(&self.right_dock);
4916 if sidebar_on_right {
4917 dock_target.or(sidebar_target)
4918 } else {
4919 dock_target
4920 }
4921 }
4922 }
4923 }
4924 }
4925
4926 (Origin::LeftDock, SplitDirection::Right) => {
4927 if let Some(last_active_pane) = get_last_active_pane() {
4928 Some(Target::Pane(last_active_pane))
4929 } else {
4930 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
4931 }
4932 }
4933
4934 (Origin::LeftDock, SplitDirection::Left) => {
4935 if sidebar_on_right {
4936 None
4937 } else {
4938 sidebar_target
4939 }
4940 }
4941
4942 (Origin::LeftDock, SplitDirection::Down)
4943 | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
4944
4945 (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
4946 (Origin::BottomDock, SplitDirection::Left) => {
4947 let dock_target = try_dock(&self.left_dock);
4948 if sidebar_on_right {
4949 dock_target
4950 } else {
4951 dock_target.or(sidebar_target)
4952 }
4953 }
4954 (Origin::BottomDock, SplitDirection::Right) => {
4955 let dock_target = try_dock(&self.right_dock);
4956 if sidebar_on_right {
4957 dock_target.or(sidebar_target)
4958 } else {
4959 dock_target
4960 }
4961 }
4962
4963 (Origin::RightDock, SplitDirection::Left) => {
4964 if let Some(last_active_pane) = get_last_active_pane() {
4965 Some(Target::Pane(last_active_pane))
4966 } else {
4967 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
4968 }
4969 }
4970
4971 (Origin::RightDock, SplitDirection::Right) => {
4972 if sidebar_on_right {
4973 sidebar_target
4974 } else {
4975 None
4976 }
4977 }
4978
4979 _ => None,
4980 };
4981
4982 match target {
4983 Some(ActivateInDirectionTarget::Pane(pane)) => {
4984 let pane = pane.read(cx);
4985 if let Some(item) = pane.active_item() {
4986 item.item_focus_handle(cx).focus(window, cx);
4987 } else {
4988 log::error!(
4989 "Could not find a focus target when in switching focus in {direction} direction for a pane",
4990 );
4991 }
4992 }
4993 Some(ActivateInDirectionTarget::Dock(dock)) => {
4994 // Defer this to avoid a panic when the dock's active panel is already on the stack.
4995 window.defer(cx, move |window, cx| {
4996 let dock = dock.read(cx);
4997 if let Some(panel) = dock.active_panel() {
4998 panel.panel_focus_handle(cx).focus(window, cx);
4999 } else {
5000 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
5001 }
5002 })
5003 }
5004 Some(ActivateInDirectionTarget::Sidebar(focus_handle)) => {
5005 focus_handle.focus(window, cx);
5006 }
5007 None => {}
5008 }
5009 }
5010
5011 pub fn move_item_to_pane_in_direction(
5012 &mut self,
5013 action: &MoveItemToPaneInDirection,
5014 window: &mut Window,
5015 cx: &mut Context<Self>,
5016 ) {
5017 let destination = match self.find_pane_in_direction(action.direction, cx) {
5018 Some(destination) => destination,
5019 None => {
5020 if !action.clone && self.active_pane.read(cx).items_len() < 2 {
5021 return;
5022 }
5023 let new_pane = self.add_pane(window, cx);
5024 self.center
5025 .split(&self.active_pane, &new_pane, action.direction, cx);
5026 new_pane
5027 }
5028 };
5029
5030 if action.clone {
5031 if self
5032 .active_pane
5033 .read(cx)
5034 .active_item()
5035 .is_some_and(|item| item.can_split(cx))
5036 {
5037 clone_active_item(
5038 self.database_id(),
5039 &self.active_pane,
5040 &destination,
5041 action.focus,
5042 window,
5043 cx,
5044 );
5045 return;
5046 }
5047 }
5048 move_active_item(
5049 &self.active_pane,
5050 &destination,
5051 action.focus,
5052 true,
5053 window,
5054 cx,
5055 );
5056 }
5057
5058 pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
5059 self.center.bounding_box_for_pane(pane)
5060 }
5061
5062 pub fn find_pane_in_direction(
5063 &mut self,
5064 direction: SplitDirection,
5065 cx: &App,
5066 ) -> Option<Entity<Pane>> {
5067 self.center
5068 .find_pane_in_direction(&self.active_pane, direction, cx)
5069 .cloned()
5070 }
5071
5072 pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
5073 if let Some(to) = self.find_pane_in_direction(direction, cx) {
5074 self.center.swap(&self.active_pane, &to, cx);
5075 cx.notify();
5076 }
5077 }
5078
5079 pub fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
5080 if self
5081 .center
5082 .move_to_border(&self.active_pane, direction, cx)
5083 .unwrap()
5084 {
5085 cx.notify();
5086 }
5087 }
5088
5089 pub fn resize_pane(
5090 &mut self,
5091 axis: gpui::Axis,
5092 amount: Pixels,
5093 window: &mut Window,
5094 cx: &mut Context<Self>,
5095 ) {
5096 let docks = self.all_docks();
5097 let active_dock = docks
5098 .into_iter()
5099 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
5100
5101 if let Some(dock_entity) = active_dock {
5102 let dock = dock_entity.read(cx);
5103 let Some(panel_size) = self.dock_size(&dock, window, cx) else {
5104 return;
5105 };
5106 match dock.position() {
5107 DockPosition::Left => self.resize_left_dock(panel_size + amount, window, cx),
5108 DockPosition::Bottom => self.resize_bottom_dock(panel_size + amount, window, cx),
5109 DockPosition::Right => self.resize_right_dock(panel_size + amount, window, cx),
5110 }
5111 } else {
5112 self.center
5113 .resize(&self.active_pane, axis, amount, &self.bounds, cx);
5114 }
5115 cx.notify();
5116 }
5117
5118 pub fn reset_pane_sizes(&mut self, cx: &mut Context<Self>) {
5119 self.center.reset_pane_sizes(cx);
5120 cx.notify();
5121 }
5122
5123 fn handle_pane_focused(
5124 &mut self,
5125 pane: Entity<Pane>,
5126 window: &mut Window,
5127 cx: &mut Context<Self>,
5128 ) {
5129 // This is explicitly hoisted out of the following check for pane identity as
5130 // terminal panel panes are not registered as a center panes.
5131 self.status_bar.update(cx, |status_bar, cx| {
5132 status_bar.set_active_pane(&pane, window, cx);
5133 });
5134 if self.active_pane != pane {
5135 self.set_active_pane(&pane, window, cx);
5136 }
5137
5138 if self.last_active_center_pane.is_none() {
5139 self.last_active_center_pane = Some(pane.downgrade());
5140 }
5141
5142 // If this pane is in a dock, preserve that dock when dismissing zoomed items.
5143 // This prevents the dock from closing when focus events fire during window activation.
5144 // We also preserve any dock whose active panel itself has focus — this covers
5145 // panels like AgentPanel that don't implement `pane()` but can still be zoomed.
5146 let dock_to_preserve = self.all_docks().iter().find_map(|dock| {
5147 let dock_read = dock.read(cx);
5148 if let Some(panel) = dock_read.active_panel() {
5149 if panel.pane(cx).is_some_and(|dock_pane| dock_pane == pane)
5150 || panel.panel_focus_handle(cx).contains_focused(window, cx)
5151 {
5152 return Some(dock_read.position());
5153 }
5154 }
5155 None
5156 });
5157
5158 self.dismiss_zoomed_items_to_reveal(dock_to_preserve, window, cx);
5159 if pane.read(cx).is_zoomed() {
5160 self.zoomed = Some(pane.downgrade().into());
5161 } else {
5162 self.zoomed = None;
5163 }
5164 self.zoomed_position = None;
5165 cx.emit(Event::ZoomChanged);
5166 self.update_active_view_for_followers(window, cx);
5167 pane.update(cx, |pane, _| {
5168 pane.track_alternate_file_items();
5169 });
5170
5171 cx.notify();
5172 }
5173
5174 fn set_active_pane(
5175 &mut self,
5176 pane: &Entity<Pane>,
5177 window: &mut Window,
5178 cx: &mut Context<Self>,
5179 ) {
5180 self.active_pane = pane.clone();
5181 self.active_item_path_changed(true, window, cx);
5182 self.last_active_center_pane = Some(pane.downgrade());
5183 }
5184
5185 fn handle_panel_focused(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5186 self.update_active_view_for_followers(window, cx);
5187 }
5188
5189 fn handle_pane_event(
5190 &mut self,
5191 pane: &Entity<Pane>,
5192 event: &pane::Event,
5193 window: &mut Window,
5194 cx: &mut Context<Self>,
5195 ) {
5196 let mut serialize_workspace = true;
5197 match event {
5198 pane::Event::AddItem { item } => {
5199 item.added_to_pane(self, pane.clone(), window, cx);
5200 cx.emit(Event::ItemAdded {
5201 item: item.boxed_clone(),
5202 });
5203 }
5204 pane::Event::Split { direction, mode } => {
5205 match mode {
5206 SplitMode::ClonePane => {
5207 self.split_and_clone(pane.clone(), *direction, window, cx)
5208 .detach();
5209 }
5210 SplitMode::EmptyPane => {
5211 self.split_pane(pane.clone(), *direction, window, cx);
5212 }
5213 SplitMode::MovePane => {
5214 self.split_and_move(pane.clone(), *direction, window, cx);
5215 }
5216 };
5217 }
5218 pane::Event::JoinIntoNext => {
5219 self.join_pane_into_next(pane.clone(), window, cx);
5220 }
5221 pane::Event::JoinAll => {
5222 self.join_all_panes(window, cx);
5223 }
5224 pane::Event::Remove { focus_on_pane } => {
5225 self.remove_pane(pane.clone(), focus_on_pane.clone(), window, cx);
5226 }
5227 pane::Event::ActivateItem {
5228 local,
5229 focus_changed,
5230 } => {
5231 window.invalidate_character_coordinates();
5232
5233 pane.update(cx, |pane, _| {
5234 pane.track_alternate_file_items();
5235 });
5236 if *local {
5237 self.unfollow_in_pane(pane, window, cx);
5238 }
5239 serialize_workspace = *focus_changed || pane != self.active_pane();
5240 if pane == self.active_pane() {
5241 self.active_item_path_changed(*focus_changed, window, cx);
5242 self.update_active_view_for_followers(window, cx);
5243 } else if *local {
5244 self.set_active_pane(pane, window, cx);
5245 }
5246 }
5247 pane::Event::UserSavedItem { item, save_intent } => {
5248 cx.emit(Event::UserSavedItem {
5249 pane: pane.downgrade(),
5250 item: item.boxed_clone(),
5251 save_intent: *save_intent,
5252 });
5253 serialize_workspace = false;
5254 }
5255 pane::Event::ChangeItemTitle => {
5256 if *pane == self.active_pane {
5257 self.active_item_path_changed(false, window, cx);
5258 }
5259 serialize_workspace = false;
5260 }
5261 pane::Event::RemovedItem { item } => {
5262 cx.emit(Event::ActiveItemChanged);
5263 self.update_window_edited(window, cx);
5264 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id())
5265 && entry.get().entity_id() == pane.entity_id()
5266 {
5267 entry.remove();
5268 }
5269 cx.emit(Event::ItemRemoved {
5270 item_id: item.item_id(),
5271 });
5272 }
5273 pane::Event::Focus => {
5274 window.invalidate_character_coordinates();
5275 self.handle_pane_focused(pane.clone(), window, cx);
5276 }
5277 pane::Event::ZoomIn => {
5278 if *pane == self.active_pane {
5279 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
5280 if pane.read(cx).has_focus(window, cx) {
5281 self.zoomed = Some(pane.downgrade().into());
5282 self.zoomed_position = None;
5283 cx.emit(Event::ZoomChanged);
5284 }
5285 cx.notify();
5286 }
5287 }
5288 pane::Event::ZoomOut => {
5289 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
5290 if self.zoomed_position.is_none() {
5291 self.zoomed = None;
5292 cx.emit(Event::ZoomChanged);
5293 }
5294 cx.notify();
5295 }
5296 pane::Event::ItemPinned | pane::Event::ItemUnpinned => {}
5297 }
5298
5299 if serialize_workspace {
5300 self.serialize_workspace(window, cx);
5301 }
5302 }
5303
5304 pub fn unfollow_in_pane(
5305 &mut self,
5306 pane: &Entity<Pane>,
5307 window: &mut Window,
5308 cx: &mut Context<Workspace>,
5309 ) -> Option<CollaboratorId> {
5310 let leader_id = self.leader_for_pane(pane)?;
5311 self.unfollow(leader_id, window, cx);
5312 Some(leader_id)
5313 }
5314
5315 pub fn split_pane(
5316 &mut self,
5317 pane_to_split: Entity<Pane>,
5318 split_direction: SplitDirection,
5319 window: &mut Window,
5320 cx: &mut Context<Self>,
5321 ) -> Entity<Pane> {
5322 let new_pane = self.add_pane(window, cx);
5323 self.center
5324 .split(&pane_to_split, &new_pane, split_direction, cx);
5325 cx.notify();
5326 new_pane
5327 }
5328
5329 pub fn split_and_move(
5330 &mut self,
5331 pane: Entity<Pane>,
5332 direction: SplitDirection,
5333 window: &mut Window,
5334 cx: &mut Context<Self>,
5335 ) {
5336 let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx)) else {
5337 return;
5338 };
5339 let new_pane = self.add_pane(window, cx);
5340 new_pane.update(cx, |pane, cx| {
5341 pane.add_item(item, true, true, None, window, cx)
5342 });
5343 self.center.split(&pane, &new_pane, direction, cx);
5344 cx.notify();
5345 }
5346
5347 pub fn split_and_clone(
5348 &mut self,
5349 pane: Entity<Pane>,
5350 direction: SplitDirection,
5351 window: &mut Window,
5352 cx: &mut Context<Self>,
5353 ) -> Task<Option<Entity<Pane>>> {
5354 let Some(item) = pane.read(cx).active_item() else {
5355 return Task::ready(None);
5356 };
5357 if !item.can_split(cx) {
5358 return Task::ready(None);
5359 }
5360 let task = item.clone_on_split(self.database_id(), window, cx);
5361 cx.spawn_in(window, async move |this, cx| {
5362 if let Some(clone) = task.await {
5363 this.update_in(cx, |this, window, cx| {
5364 let new_pane = this.add_pane(window, cx);
5365 let nav_history = pane.read(cx).fork_nav_history();
5366 new_pane.update(cx, |pane, cx| {
5367 pane.set_nav_history(nav_history, cx);
5368 pane.add_item(clone, true, true, None, window, cx)
5369 });
5370 this.center.split(&pane, &new_pane, direction, cx);
5371 cx.notify();
5372 new_pane
5373 })
5374 .ok()
5375 } else {
5376 None
5377 }
5378 })
5379 }
5380
5381 pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5382 let active_item = self.active_pane.read(cx).active_item();
5383 for pane in &self.panes {
5384 join_pane_into_active(&self.active_pane, pane, window, cx);
5385 }
5386 if let Some(active_item) = active_item {
5387 self.activate_item(active_item.as_ref(), true, true, window, cx);
5388 }
5389 cx.notify();
5390 }
5391
5392 pub fn join_pane_into_next(
5393 &mut self,
5394 pane: Entity<Pane>,
5395 window: &mut Window,
5396 cx: &mut Context<Self>,
5397 ) {
5398 let next_pane = self
5399 .find_pane_in_direction(SplitDirection::Right, cx)
5400 .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
5401 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
5402 .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
5403 let Some(next_pane) = next_pane else {
5404 return;
5405 };
5406 move_all_items(&pane, &next_pane, window, cx);
5407 cx.notify();
5408 }
5409
5410 fn remove_pane(
5411 &mut self,
5412 pane: Entity<Pane>,
5413 focus_on: Option<Entity<Pane>>,
5414 window: &mut Window,
5415 cx: &mut Context<Self>,
5416 ) {
5417 if self.center.remove(&pane, cx).unwrap() {
5418 self.force_remove_pane(&pane, &focus_on, window, cx);
5419 self.unfollow_in_pane(&pane, window, cx);
5420 self.last_leaders_by_pane.remove(&pane.downgrade());
5421 for removed_item in pane.read(cx).items() {
5422 self.panes_by_item.remove(&removed_item.item_id());
5423 }
5424
5425 cx.notify();
5426 } else {
5427 self.active_item_path_changed(true, window, cx);
5428 }
5429 cx.emit(Event::PaneRemoved);
5430 }
5431
5432 pub fn panes_mut(&mut self) -> &mut [Entity<Pane>] {
5433 &mut self.panes
5434 }
5435
5436 pub fn panes(&self) -> &[Entity<Pane>] {
5437 &self.panes
5438 }
5439
5440 pub fn active_pane(&self) -> &Entity<Pane> {
5441 &self.active_pane
5442 }
5443
5444 pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> {
5445 for dock in self.all_docks() {
5446 if dock.focus_handle(cx).contains_focused(window, cx)
5447 && let Some(pane) = dock
5448 .read(cx)
5449 .active_panel()
5450 .and_then(|panel| panel.pane(cx))
5451 {
5452 return pane;
5453 }
5454 }
5455 self.active_pane().clone()
5456 }
5457
5458 pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
5459 self.find_pane_in_direction(SplitDirection::Right, cx)
5460 .unwrap_or_else(|| {
5461 self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx)
5462 })
5463 }
5464
5465 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> {
5466 self.pane_for_item_id(handle.item_id())
5467 }
5468
5469 pub fn pane_for_item_id(&self, item_id: EntityId) -> Option<Entity<Pane>> {
5470 let weak_pane = self.panes_by_item.get(&item_id)?;
5471 weak_pane.upgrade()
5472 }
5473
5474 pub fn pane_for_entity_id(&self, entity_id: EntityId) -> Option<Entity<Pane>> {
5475 self.panes
5476 .iter()
5477 .find(|pane| pane.entity_id() == entity_id)
5478 .cloned()
5479 }
5480
5481 fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
5482 self.follower_states.retain(|leader_id, state| {
5483 if *leader_id == CollaboratorId::PeerId(peer_id) {
5484 for item in state.items_by_leader_view_id.values() {
5485 item.view.set_leader_id(None, window, cx);
5486 }
5487 false
5488 } else {
5489 true
5490 }
5491 });
5492 cx.notify();
5493 }
5494
5495 pub fn start_following(
5496 &mut self,
5497 leader_id: impl Into<CollaboratorId>,
5498 window: &mut Window,
5499 cx: &mut Context<Self>,
5500 ) -> Option<Task<Result<()>>> {
5501 let leader_id = leader_id.into();
5502 let pane = self.active_pane().clone();
5503
5504 self.last_leaders_by_pane
5505 .insert(pane.downgrade(), leader_id);
5506 self.unfollow(leader_id, window, cx);
5507 self.unfollow_in_pane(&pane, window, cx);
5508 self.follower_states.insert(
5509 leader_id,
5510 FollowerState {
5511 center_pane: pane.clone(),
5512 dock_pane: None,
5513 active_view_id: None,
5514 items_by_leader_view_id: Default::default(),
5515 },
5516 );
5517 cx.notify();
5518
5519 match leader_id {
5520 CollaboratorId::PeerId(leader_peer_id) => {
5521 let room_id = self.active_call()?.room_id(cx)?;
5522 let project_id = self.project.read(cx).remote_id();
5523 let request = self.app_state.client.request(proto::Follow {
5524 room_id,
5525 project_id,
5526 leader_id: Some(leader_peer_id),
5527 });
5528
5529 Some(cx.spawn_in(window, async move |this, cx| {
5530 let response = request.await?;
5531 this.update(cx, |this, _| {
5532 let state = this
5533 .follower_states
5534 .get_mut(&leader_id)
5535 .context("following interrupted")?;
5536 state.active_view_id = response
5537 .active_view
5538 .as_ref()
5539 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
5540 anyhow::Ok(())
5541 })??;
5542 if let Some(view) = response.active_view {
5543 Self::add_view_from_leader(this.clone(), leader_peer_id, &view, cx).await?;
5544 }
5545 this.update_in(cx, |this, window, cx| {
5546 this.leader_updated(leader_id, window, cx)
5547 })?;
5548 Ok(())
5549 }))
5550 }
5551 CollaboratorId::Agent => {
5552 self.leader_updated(leader_id, window, cx)?;
5553 Some(Task::ready(Ok(())))
5554 }
5555 }
5556 }
5557
5558 pub fn follow_next_collaborator(
5559 &mut self,
5560 _: &FollowNextCollaborator,
5561 window: &mut Window,
5562 cx: &mut Context<Self>,
5563 ) {
5564 let collaborators = self.project.read(cx).collaborators();
5565 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
5566 let mut collaborators = collaborators.keys().copied();
5567 for peer_id in collaborators.by_ref() {
5568 if CollaboratorId::PeerId(peer_id) == leader_id {
5569 break;
5570 }
5571 }
5572 collaborators.next().map(CollaboratorId::PeerId)
5573 } else if let Some(last_leader_id) =
5574 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
5575 {
5576 match last_leader_id {
5577 CollaboratorId::PeerId(peer_id) => {
5578 if collaborators.contains_key(peer_id) {
5579 Some(*last_leader_id)
5580 } else {
5581 None
5582 }
5583 }
5584 CollaboratorId::Agent => Some(CollaboratorId::Agent),
5585 }
5586 } else {
5587 None
5588 };
5589
5590 let pane = self.active_pane.clone();
5591 let Some(leader_id) = next_leader_id.or_else(|| {
5592 Some(CollaboratorId::PeerId(
5593 collaborators.keys().copied().next()?,
5594 ))
5595 }) else {
5596 return;
5597 };
5598 if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
5599 return;
5600 }
5601 if let Some(task) = self.start_following(leader_id, window, cx) {
5602 task.detach_and_log_err(cx)
5603 }
5604 }
5605
5606 pub fn follow(
5607 &mut self,
5608 leader_id: impl Into<CollaboratorId>,
5609 window: &mut Window,
5610 cx: &mut Context<Self>,
5611 ) {
5612 let leader_id = leader_id.into();
5613
5614 if let CollaboratorId::PeerId(peer_id) = leader_id {
5615 let Some(active_call) = GlobalAnyActiveCall::try_global(cx) else {
5616 return;
5617 };
5618 let Some(remote_participant) =
5619 active_call.0.remote_participant_for_peer_id(peer_id, cx)
5620 else {
5621 return;
5622 };
5623
5624 let project = self.project.read(cx);
5625
5626 let other_project_id = match remote_participant.location {
5627 ParticipantLocation::External => None,
5628 ParticipantLocation::UnsharedProject => None,
5629 ParticipantLocation::SharedProject { project_id } => {
5630 if Some(project_id) == project.remote_id() {
5631 None
5632 } else {
5633 Some(project_id)
5634 }
5635 }
5636 };
5637
5638 // if they are active in another project, follow there.
5639 if let Some(project_id) = other_project_id {
5640 let app_state = self.app_state.clone();
5641 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
5642 .detach_and_prompt_err("Failed to join project", window, cx, |error, _, _| {
5643 Some(format!("{error:#}"))
5644 });
5645 }
5646 }
5647
5648 // if you're already following, find the right pane and focus it.
5649 if let Some(follower_state) = self.follower_states.get(&leader_id) {
5650 window.focus(&follower_state.pane().focus_handle(cx), cx);
5651
5652 return;
5653 }
5654
5655 // Otherwise, follow.
5656 if let Some(task) = self.start_following(leader_id, window, cx) {
5657 task.detach_and_log_err(cx)
5658 }
5659 }
5660
5661 pub fn unfollow(
5662 &mut self,
5663 leader_id: impl Into<CollaboratorId>,
5664 window: &mut Window,
5665 cx: &mut Context<Self>,
5666 ) -> Option<()> {
5667 cx.notify();
5668
5669 let leader_id = leader_id.into();
5670 let state = self.follower_states.remove(&leader_id)?;
5671 for (_, item) in state.items_by_leader_view_id {
5672 item.view.set_leader_id(None, window, cx);
5673 }
5674
5675 if let CollaboratorId::PeerId(leader_peer_id) = leader_id {
5676 let project_id = self.project.read(cx).remote_id();
5677 let room_id = self.active_call()?.room_id(cx)?;
5678 self.app_state
5679 .client
5680 .send(proto::Unfollow {
5681 room_id,
5682 project_id,
5683 leader_id: Some(leader_peer_id),
5684 })
5685 .log_err();
5686 }
5687
5688 Some(())
5689 }
5690
5691 pub fn is_being_followed(&self, id: impl Into<CollaboratorId>) -> bool {
5692 self.follower_states.contains_key(&id.into())
5693 }
5694
5695 fn active_item_path_changed(
5696 &mut self,
5697 focus_changed: bool,
5698 window: &mut Window,
5699 cx: &mut Context<Self>,
5700 ) {
5701 cx.emit(Event::ActiveItemChanged);
5702 let active_entry = self.active_project_path(cx);
5703 self.project.update(cx, |project, cx| {
5704 project.set_active_path(active_entry.clone(), cx)
5705 });
5706
5707 if focus_changed && let Some(project_path) = &active_entry {
5708 let git_store_entity = self.project.read(cx).git_store().clone();
5709 git_store_entity.update(cx, |git_store, cx| {
5710 git_store.set_active_repo_for_path(project_path, cx);
5711 });
5712 }
5713
5714 self.update_window_title(window, cx);
5715 }
5716
5717 fn update_window_title(&mut self, window: &mut Window, cx: &mut App) {
5718 let project = self.project().read(cx);
5719 let mut title = String::new();
5720
5721 for (i, worktree) in project.visible_worktrees(cx).enumerate() {
5722 let name = {
5723 let settings_location = SettingsLocation {
5724 worktree_id: worktree.read(cx).id(),
5725 path: RelPath::empty(),
5726 };
5727
5728 let settings = WorktreeSettings::get(Some(settings_location), cx);
5729 match &settings.project_name {
5730 Some(name) => name.as_str(),
5731 None => worktree.read(cx).root_name_str(),
5732 }
5733 };
5734 if i > 0 {
5735 title.push_str(", ");
5736 }
5737 title.push_str(name);
5738 }
5739
5740 if title.is_empty() {
5741 title = "empty project".to_string();
5742 }
5743
5744 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
5745 let filename = path.path.file_name().or_else(|| {
5746 Some(
5747 project
5748 .worktree_for_id(path.worktree_id, cx)?
5749 .read(cx)
5750 .root_name_str(),
5751 )
5752 });
5753
5754 if let Some(filename) = filename {
5755 title.push_str(" — ");
5756 title.push_str(filename.as_ref());
5757 }
5758 }
5759
5760 if project.is_via_collab() {
5761 title.push_str(" ↙");
5762 } else if project.is_shared() {
5763 title.push_str(" ↗");
5764 }
5765
5766 if let Some(last_title) = self.last_window_title.as_ref()
5767 && &title == last_title
5768 {
5769 return;
5770 }
5771 window.set_window_title(&title);
5772 SystemWindowTabController::update_tab_title(
5773 cx,
5774 window.window_handle().window_id(),
5775 SharedString::from(&title),
5776 );
5777 self.last_window_title = Some(title);
5778 }
5779
5780 fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
5781 let is_edited = !self.project.read(cx).is_disconnected(cx) && !self.dirty_items.is_empty();
5782 if is_edited != self.window_edited {
5783 self.window_edited = is_edited;
5784 window.set_window_edited(self.window_edited)
5785 }
5786 }
5787
5788 fn update_item_dirty_state(
5789 &mut self,
5790 item: &dyn ItemHandle,
5791 window: &mut Window,
5792 cx: &mut App,
5793 ) {
5794 let is_dirty = item.is_dirty(cx);
5795 let item_id = item.item_id();
5796 let was_dirty = self.dirty_items.contains_key(&item_id);
5797 if is_dirty == was_dirty {
5798 return;
5799 }
5800 if was_dirty {
5801 self.dirty_items.remove(&item_id);
5802 self.update_window_edited(window, cx);
5803 return;
5804 }
5805
5806 let workspace = self.weak_handle();
5807 let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
5808 return;
5809 };
5810 let on_release_callback = Box::new(move |cx: &mut App| {
5811 window_handle
5812 .update(cx, |_, window, cx| {
5813 workspace
5814 .update(cx, |workspace, cx| {
5815 workspace.dirty_items.remove(&item_id);
5816 workspace.update_window_edited(window, cx)
5817 })
5818 .ok();
5819 })
5820 .ok();
5821 });
5822
5823 let s = item.on_release(cx, on_release_callback);
5824 self.dirty_items.insert(item_id, s);
5825 self.update_window_edited(window, cx);
5826 }
5827
5828 fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
5829 if self.notifications.is_empty() {
5830 None
5831 } else {
5832 Some(
5833 div()
5834 .absolute()
5835 .right_3()
5836 .bottom_3()
5837 .w_112()
5838 .h_full()
5839 .flex()
5840 .flex_col()
5841 .justify_end()
5842 .gap_2()
5843 .children(
5844 self.notifications
5845 .iter()
5846 .map(|(_, notification)| notification.clone().into_any()),
5847 ),
5848 )
5849 }
5850 }
5851
5852 // RPC handlers
5853
5854 fn active_view_for_follower(
5855 &self,
5856 follower_project_id: Option<u64>,
5857 window: &mut Window,
5858 cx: &mut Context<Self>,
5859 ) -> Option<proto::View> {
5860 let (item, panel_id) = self.active_item_for_followers(window, cx);
5861 let item = item?;
5862 let leader_id = self
5863 .pane_for(&*item)
5864 .and_then(|pane| self.leader_for_pane(&pane));
5865 let leader_peer_id = match leader_id {
5866 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
5867 Some(CollaboratorId::Agent) | None => None,
5868 };
5869
5870 let item_handle = item.to_followable_item_handle(cx)?;
5871 let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
5872 let variant = item_handle.to_state_proto(window, cx)?;
5873
5874 if item_handle.is_project_item(window, cx)
5875 && (follower_project_id.is_none()
5876 || follower_project_id != self.project.read(cx).remote_id())
5877 {
5878 return None;
5879 }
5880
5881 Some(proto::View {
5882 id: id.to_proto(),
5883 leader_id: leader_peer_id,
5884 variant: Some(variant),
5885 panel_id: panel_id.map(|id| id as i32),
5886 })
5887 }
5888
5889 fn handle_follow(
5890 &mut self,
5891 follower_project_id: Option<u64>,
5892 window: &mut Window,
5893 cx: &mut Context<Self>,
5894 ) -> proto::FollowResponse {
5895 let active_view = self.active_view_for_follower(follower_project_id, window, cx);
5896
5897 cx.notify();
5898 proto::FollowResponse {
5899 views: active_view.iter().cloned().collect(),
5900 active_view,
5901 }
5902 }
5903
5904 fn handle_update_followers(
5905 &mut self,
5906 leader_id: PeerId,
5907 message: proto::UpdateFollowers,
5908 _window: &mut Window,
5909 _cx: &mut Context<Self>,
5910 ) {
5911 self.leader_updates_tx
5912 .unbounded_send((leader_id, message))
5913 .ok();
5914 }
5915
5916 async fn process_leader_update(
5917 this: &WeakEntity<Self>,
5918 leader_id: PeerId,
5919 update: proto::UpdateFollowers,
5920 cx: &mut AsyncWindowContext,
5921 ) -> Result<()> {
5922 match update.variant.context("invalid update")? {
5923 proto::update_followers::Variant::CreateView(view) => {
5924 let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
5925 let should_add_view = this.update(cx, |this, _| {
5926 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
5927 anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
5928 } else {
5929 anyhow::Ok(false)
5930 }
5931 })??;
5932
5933 if should_add_view {
5934 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
5935 }
5936 }
5937 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
5938 let should_add_view = this.update(cx, |this, _| {
5939 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
5940 state.active_view_id = update_active_view
5941 .view
5942 .as_ref()
5943 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
5944
5945 if state.active_view_id.is_some_and(|view_id| {
5946 !state.items_by_leader_view_id.contains_key(&view_id)
5947 }) {
5948 anyhow::Ok(true)
5949 } else {
5950 anyhow::Ok(false)
5951 }
5952 } else {
5953 anyhow::Ok(false)
5954 }
5955 })??;
5956
5957 if should_add_view && let Some(view) = update_active_view.view {
5958 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
5959 }
5960 }
5961 proto::update_followers::Variant::UpdateView(update_view) => {
5962 let variant = update_view.variant.context("missing update view variant")?;
5963 let id = update_view.id.context("missing update view id")?;
5964 let mut tasks = Vec::new();
5965 this.update_in(cx, |this, window, cx| {
5966 let project = this.project.clone();
5967 if let Some(state) = this.follower_states.get(&leader_id.into()) {
5968 let view_id = ViewId::from_proto(id.clone())?;
5969 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
5970 tasks.push(item.view.apply_update_proto(
5971 &project,
5972 variant.clone(),
5973 window,
5974 cx,
5975 ));
5976 }
5977 }
5978 anyhow::Ok(())
5979 })??;
5980 try_join_all(tasks).await.log_err();
5981 }
5982 }
5983 this.update_in(cx, |this, window, cx| {
5984 this.leader_updated(leader_id, window, cx)
5985 })?;
5986 Ok(())
5987 }
5988
5989 async fn add_view_from_leader(
5990 this: WeakEntity<Self>,
5991 leader_id: PeerId,
5992 view: &proto::View,
5993 cx: &mut AsyncWindowContext,
5994 ) -> Result<()> {
5995 let this = this.upgrade().context("workspace dropped")?;
5996
5997 let Some(id) = view.id.clone() else {
5998 anyhow::bail!("no id for view");
5999 };
6000 let id = ViewId::from_proto(id)?;
6001 let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
6002
6003 let pane = this.update(cx, |this, _cx| {
6004 let state = this
6005 .follower_states
6006 .get(&leader_id.into())
6007 .context("stopped following")?;
6008 anyhow::Ok(state.pane().clone())
6009 })?;
6010 let existing_item = pane.update_in(cx, |pane, window, cx| {
6011 let client = this.read(cx).client().clone();
6012 pane.items().find_map(|item| {
6013 let item = item.to_followable_item_handle(cx)?;
6014 if item.remote_id(&client, window, cx) == Some(id) {
6015 Some(item)
6016 } else {
6017 None
6018 }
6019 })
6020 })?;
6021 let item = if let Some(existing_item) = existing_item {
6022 existing_item
6023 } else {
6024 let variant = view.variant.clone();
6025 anyhow::ensure!(variant.is_some(), "missing view variant");
6026
6027 let task = cx.update(|window, cx| {
6028 FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx)
6029 })?;
6030
6031 let Some(task) = task else {
6032 anyhow::bail!(
6033 "failed to construct view from leader (maybe from a different version of zed?)"
6034 );
6035 };
6036
6037 let mut new_item = task.await?;
6038 pane.update_in(cx, |pane, window, cx| {
6039 let mut item_to_remove = None;
6040 for (ix, item) in pane.items().enumerate() {
6041 if let Some(item) = item.to_followable_item_handle(cx) {
6042 match new_item.dedup(item.as_ref(), window, cx) {
6043 Some(item::Dedup::KeepExisting) => {
6044 new_item =
6045 item.boxed_clone().to_followable_item_handle(cx).unwrap();
6046 break;
6047 }
6048 Some(item::Dedup::ReplaceExisting) => {
6049 item_to_remove = Some((ix, item.item_id()));
6050 break;
6051 }
6052 None => {}
6053 }
6054 }
6055 }
6056
6057 if let Some((ix, id)) = item_to_remove {
6058 pane.remove_item(id, false, false, window, cx);
6059 pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx);
6060 }
6061 })?;
6062
6063 new_item
6064 };
6065
6066 this.update_in(cx, |this, window, cx| {
6067 let state = this.follower_states.get_mut(&leader_id.into())?;
6068 item.set_leader_id(Some(leader_id.into()), window, cx);
6069 state.items_by_leader_view_id.insert(
6070 id,
6071 FollowerView {
6072 view: item,
6073 location: panel_id,
6074 },
6075 );
6076
6077 Some(())
6078 })
6079 .context("no follower state")?;
6080
6081 Ok(())
6082 }
6083
6084 fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
6085 let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else {
6086 return;
6087 };
6088
6089 if let Some(agent_location) = self.project.read(cx).agent_location() {
6090 let buffer_entity_id = agent_location.buffer.entity_id();
6091 let view_id = ViewId {
6092 creator: CollaboratorId::Agent,
6093 id: buffer_entity_id.as_u64(),
6094 };
6095 follower_state.active_view_id = Some(view_id);
6096
6097 let item = match follower_state.items_by_leader_view_id.entry(view_id) {
6098 hash_map::Entry::Occupied(entry) => Some(entry.into_mut()),
6099 hash_map::Entry::Vacant(entry) => {
6100 let existing_view =
6101 follower_state
6102 .center_pane
6103 .read(cx)
6104 .items()
6105 .find_map(|item| {
6106 let item = item.to_followable_item_handle(cx)?;
6107 if item.buffer_kind(cx) == ItemBufferKind::Singleton
6108 && item.project_item_model_ids(cx).as_slice()
6109 == [buffer_entity_id]
6110 {
6111 Some(item)
6112 } else {
6113 None
6114 }
6115 });
6116 let view = existing_view.or_else(|| {
6117 agent_location.buffer.upgrade().and_then(|buffer| {
6118 cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| {
6119 registry.build_item(buffer, self.project.clone(), None, window, cx)
6120 })?
6121 .to_followable_item_handle(cx)
6122 })
6123 });
6124
6125 view.map(|view| {
6126 entry.insert(FollowerView {
6127 view,
6128 location: None,
6129 })
6130 })
6131 }
6132 };
6133
6134 if let Some(item) = item {
6135 item.view
6136 .set_leader_id(Some(CollaboratorId::Agent), window, cx);
6137 item.view
6138 .update_agent_location(agent_location.position, window, cx);
6139 }
6140 } else {
6141 follower_state.active_view_id = None;
6142 }
6143
6144 self.leader_updated(CollaboratorId::Agent, window, cx);
6145 }
6146
6147 pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
6148 let mut is_project_item = true;
6149 let mut update = proto::UpdateActiveView::default();
6150 if window.is_window_active() {
6151 let (active_item, panel_id) = self.active_item_for_followers(window, cx);
6152
6153 if let Some(item) = active_item
6154 && item.item_focus_handle(cx).contains_focused(window, cx)
6155 {
6156 let leader_id = self
6157 .pane_for(&*item)
6158 .and_then(|pane| self.leader_for_pane(&pane));
6159 let leader_peer_id = match leader_id {
6160 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
6161 Some(CollaboratorId::Agent) | None => None,
6162 };
6163
6164 if let Some(item) = item.to_followable_item_handle(cx) {
6165 let id = item
6166 .remote_id(&self.app_state.client, window, cx)
6167 .map(|id| id.to_proto());
6168
6169 if let Some(id) = id
6170 && let Some(variant) = item.to_state_proto(window, cx)
6171 {
6172 let view = Some(proto::View {
6173 id,
6174 leader_id: leader_peer_id,
6175 variant: Some(variant),
6176 panel_id: panel_id.map(|id| id as i32),
6177 });
6178
6179 is_project_item = item.is_project_item(window, cx);
6180 update = proto::UpdateActiveView { view };
6181 };
6182 }
6183 }
6184 }
6185
6186 let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
6187 if active_view_id != self.last_active_view_id.as_ref() {
6188 self.last_active_view_id = active_view_id.cloned();
6189 self.update_followers(
6190 is_project_item,
6191 proto::update_followers::Variant::UpdateActiveView(update),
6192 window,
6193 cx,
6194 );
6195 }
6196 }
6197
6198 fn active_item_for_followers(
6199 &self,
6200 window: &mut Window,
6201 cx: &mut App,
6202 ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
6203 let mut active_item = None;
6204 let mut panel_id = None;
6205 for dock in self.all_docks() {
6206 if dock.focus_handle(cx).contains_focused(window, cx)
6207 && let Some(panel) = dock.read(cx).active_panel()
6208 && let Some(pane) = panel.pane(cx)
6209 && let Some(item) = pane.read(cx).active_item()
6210 {
6211 active_item = Some(item);
6212 panel_id = panel.remote_id();
6213 break;
6214 }
6215 }
6216
6217 if active_item.is_none() {
6218 active_item = self.active_pane().read(cx).active_item();
6219 }
6220 (active_item, panel_id)
6221 }
6222
6223 fn update_followers(
6224 &self,
6225 project_only: bool,
6226 update: proto::update_followers::Variant,
6227 _: &mut Window,
6228 cx: &mut App,
6229 ) -> Option<()> {
6230 // If this update only applies to for followers in the current project,
6231 // then skip it unless this project is shared. If it applies to all
6232 // followers, regardless of project, then set `project_id` to none,
6233 // indicating that it goes to all followers.
6234 let project_id = if project_only {
6235 Some(self.project.read(cx).remote_id()?)
6236 } else {
6237 None
6238 };
6239 self.app_state().workspace_store.update(cx, |store, cx| {
6240 store.update_followers(project_id, update, cx)
6241 })
6242 }
6243
6244 pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<CollaboratorId> {
6245 self.follower_states.iter().find_map(|(leader_id, state)| {
6246 if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
6247 Some(*leader_id)
6248 } else {
6249 None
6250 }
6251 })
6252 }
6253
6254 fn leader_updated(
6255 &mut self,
6256 leader_id: impl Into<CollaboratorId>,
6257 window: &mut Window,
6258 cx: &mut Context<Self>,
6259 ) -> Option<Box<dyn ItemHandle>> {
6260 cx.notify();
6261
6262 let leader_id = leader_id.into();
6263 let (panel_id, item) = match leader_id {
6264 CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?,
6265 CollaboratorId::Agent => (None, self.active_item_for_agent()?),
6266 };
6267
6268 let state = self.follower_states.get(&leader_id)?;
6269 let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
6270 let pane;
6271 if let Some(panel_id) = panel_id {
6272 pane = self
6273 .activate_panel_for_proto_id(panel_id, window, cx)?
6274 .pane(cx)?;
6275 let state = self.follower_states.get_mut(&leader_id)?;
6276 state.dock_pane = Some(pane.clone());
6277 } else {
6278 pane = state.center_pane.clone();
6279 let state = self.follower_states.get_mut(&leader_id)?;
6280 if let Some(dock_pane) = state.dock_pane.take() {
6281 transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx);
6282 }
6283 }
6284
6285 pane.update(cx, |pane, cx| {
6286 let focus_active_item = pane.has_focus(window, cx) || transfer_focus;
6287 if let Some(index) = pane.index_for_item(item.as_ref()) {
6288 pane.activate_item(index, false, false, window, cx);
6289 } else {
6290 pane.add_item(item.boxed_clone(), false, false, None, window, cx)
6291 }
6292
6293 if focus_active_item {
6294 pane.focus_active_item(window, cx)
6295 }
6296 });
6297
6298 Some(item)
6299 }
6300
6301 fn active_item_for_agent(&self) -> Option<Box<dyn ItemHandle>> {
6302 let state = self.follower_states.get(&CollaboratorId::Agent)?;
6303 let active_view_id = state.active_view_id?;
6304 Some(
6305 state
6306 .items_by_leader_view_id
6307 .get(&active_view_id)?
6308 .view
6309 .boxed_clone(),
6310 )
6311 }
6312
6313 fn active_item_for_peer(
6314 &self,
6315 peer_id: PeerId,
6316 window: &mut Window,
6317 cx: &mut Context<Self>,
6318 ) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
6319 let call = self.active_call()?;
6320 let participant = call.remote_participant_for_peer_id(peer_id, cx)?;
6321 let leader_in_this_app;
6322 let leader_in_this_project;
6323 match participant.location {
6324 ParticipantLocation::SharedProject { project_id } => {
6325 leader_in_this_app = true;
6326 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
6327 }
6328 ParticipantLocation::UnsharedProject => {
6329 leader_in_this_app = true;
6330 leader_in_this_project = false;
6331 }
6332 ParticipantLocation::External => {
6333 leader_in_this_app = false;
6334 leader_in_this_project = false;
6335 }
6336 };
6337 let state = self.follower_states.get(&peer_id.into())?;
6338 let mut item_to_activate = None;
6339 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
6340 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id)
6341 && (leader_in_this_project || !item.view.is_project_item(window, cx))
6342 {
6343 item_to_activate = Some((item.location, item.view.boxed_clone()));
6344 }
6345 } else if let Some(shared_screen) =
6346 self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx)
6347 {
6348 item_to_activate = Some((None, Box::new(shared_screen)));
6349 }
6350 item_to_activate
6351 }
6352
6353 fn shared_screen_for_peer(
6354 &self,
6355 peer_id: PeerId,
6356 pane: &Entity<Pane>,
6357 window: &mut Window,
6358 cx: &mut App,
6359 ) -> Option<Entity<SharedScreen>> {
6360 self.active_call()?
6361 .create_shared_screen(peer_id, pane, window, cx)
6362 }
6363
6364 pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
6365 if window.is_window_active() {
6366 self.update_active_view_for_followers(window, cx);
6367
6368 if let Some(database_id) = self.database_id {
6369 let db = WorkspaceDb::global(cx);
6370 cx.background_spawn(async move { db.update_timestamp(database_id).await })
6371 .detach();
6372 }
6373 } else {
6374 for pane in &self.panes {
6375 pane.update(cx, |pane, cx| {
6376 if let Some(item) = pane.active_item() {
6377 item.workspace_deactivated(window, cx);
6378 }
6379 for item in pane.items() {
6380 if matches!(
6381 item.workspace_settings(cx).autosave,
6382 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
6383 ) {
6384 Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
6385 .detach_and_log_err(cx);
6386 }
6387 }
6388 });
6389 }
6390 }
6391 }
6392
6393 pub fn active_call(&self) -> Option<&dyn AnyActiveCall> {
6394 self.active_call.as_ref().map(|(call, _)| &*call.0)
6395 }
6396
6397 pub fn active_global_call(&self) -> Option<GlobalAnyActiveCall> {
6398 self.active_call.as_ref().map(|(call, _)| call.clone())
6399 }
6400
6401 fn on_active_call_event(
6402 &mut self,
6403 event: &ActiveCallEvent,
6404 window: &mut Window,
6405 cx: &mut Context<Self>,
6406 ) {
6407 match event {
6408 ActiveCallEvent::ParticipantLocationChanged { participant_id }
6409 | ActiveCallEvent::RemoteVideoTracksChanged { participant_id } => {
6410 self.leader_updated(participant_id, window, cx);
6411 }
6412 }
6413 }
6414
6415 pub fn database_id(&self) -> Option<WorkspaceId> {
6416 self.database_id
6417 }
6418
6419 #[cfg(any(test, feature = "test-support"))]
6420 pub(crate) fn set_database_id(&mut self, id: WorkspaceId) {
6421 self.database_id = Some(id);
6422 }
6423
6424 pub fn session_id(&self) -> Option<String> {
6425 self.session_id.clone()
6426 }
6427
6428 fn save_window_bounds(&self, window: &mut Window, cx: &mut App) -> Task<()> {
6429 let Some(display) = window.display(cx) else {
6430 return Task::ready(());
6431 };
6432 let Ok(display_uuid) = display.uuid() else {
6433 return Task::ready(());
6434 };
6435
6436 let window_bounds = window.inner_window_bounds();
6437 let database_id = self.database_id;
6438 let has_paths = !self.root_paths(cx).is_empty();
6439 let db = WorkspaceDb::global(cx);
6440 let kvp = db::kvp::KeyValueStore::global(cx);
6441
6442 cx.background_executor().spawn(async move {
6443 if !has_paths {
6444 persistence::write_default_window_bounds(&kvp, window_bounds, display_uuid)
6445 .await
6446 .log_err();
6447 }
6448 if let Some(database_id) = database_id {
6449 db.set_window_open_status(
6450 database_id,
6451 SerializedWindowBounds(window_bounds),
6452 display_uuid,
6453 )
6454 .await
6455 .log_err();
6456 } else {
6457 persistence::write_default_window_bounds(&kvp, window_bounds, display_uuid)
6458 .await
6459 .log_err();
6460 }
6461 })
6462 }
6463
6464 /// Bypass the 200ms serialization throttle and write workspace state to
6465 /// the DB immediately. Returns a task the caller can await to ensure the
6466 /// write completes. Used by the quit handler so the most recent state
6467 /// isn't lost to a pending throttle timer when the process exits.
6468 pub fn flush_serialization(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
6469 self._schedule_serialize_workspace.take();
6470 self._serialize_workspace_task.take();
6471 self.bounds_save_task_queued.take();
6472
6473 let bounds_task = self.save_window_bounds(window, cx);
6474 let serialize_task = self.serialize_workspace_internal(window, cx);
6475 cx.spawn(async move |_| {
6476 bounds_task.await;
6477 serialize_task.await;
6478 })
6479 }
6480
6481 pub fn root_paths(&self, cx: &App) -> Vec<Arc<Path>> {
6482 let project = self.project().read(cx);
6483 project
6484 .visible_worktrees(cx)
6485 .map(|worktree| worktree.read(cx).abs_path())
6486 .collect::<Vec<_>>()
6487 }
6488
6489 fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
6490 match member {
6491 Member::Axis(PaneAxis { members, .. }) => {
6492 for child in members.iter() {
6493 self.remove_panes(child.clone(), window, cx)
6494 }
6495 }
6496 Member::Pane(pane) => {
6497 self.force_remove_pane(&pane, &None, window, cx);
6498 }
6499 }
6500 }
6501
6502 fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
6503 self.session_id.take();
6504 self.serialize_workspace_internal(window, cx)
6505 }
6506
6507 fn force_remove_pane(
6508 &mut self,
6509 pane: &Entity<Pane>,
6510 focus_on: &Option<Entity<Pane>>,
6511 window: &mut Window,
6512 cx: &mut Context<Workspace>,
6513 ) {
6514 self.panes.retain(|p| p != pane);
6515 if let Some(focus_on) = focus_on {
6516 focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
6517 } else if self.active_pane() == pane {
6518 self.panes
6519 .last()
6520 .unwrap()
6521 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
6522 }
6523 if self.last_active_center_pane == Some(pane.downgrade()) {
6524 self.last_active_center_pane = None;
6525 }
6526 cx.notify();
6527 }
6528
6529 fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
6530 if self._schedule_serialize_workspace.is_none() {
6531 self._schedule_serialize_workspace =
6532 Some(cx.spawn_in(window, async move |this, cx| {
6533 cx.background_executor()
6534 .timer(SERIALIZATION_THROTTLE_TIME)
6535 .await;
6536 this.update_in(cx, |this, window, cx| {
6537 this._serialize_workspace_task =
6538 Some(this.serialize_workspace_internal(window, cx));
6539 this._schedule_serialize_workspace.take();
6540 })
6541 .log_err();
6542 }));
6543 }
6544 }
6545
6546 fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
6547 let Some(database_id) = self.database_id() else {
6548 return Task::ready(());
6549 };
6550
6551 fn serialize_pane_handle(
6552 pane_handle: &Entity<Pane>,
6553 window: &mut Window,
6554 cx: &mut App,
6555 ) -> SerializedPane {
6556 let (items, active, pinned_count) = {
6557 let pane = pane_handle.read(cx);
6558 let active_item_id = pane.active_item().map(|item| item.item_id());
6559 (
6560 pane.items()
6561 .filter_map(|handle| {
6562 let handle = handle.to_serializable_item_handle(cx)?;
6563
6564 Some(SerializedItem {
6565 kind: Arc::from(handle.serialized_item_kind()),
6566 item_id: handle.item_id().as_u64(),
6567 active: Some(handle.item_id()) == active_item_id,
6568 preview: pane.is_active_preview_item(handle.item_id()),
6569 })
6570 })
6571 .collect::<Vec<_>>(),
6572 pane.has_focus(window, cx),
6573 pane.pinned_count(),
6574 )
6575 };
6576
6577 SerializedPane::new(items, active, pinned_count)
6578 }
6579
6580 fn build_serialized_pane_group(
6581 pane_group: &Member,
6582 window: &mut Window,
6583 cx: &mut App,
6584 ) -> SerializedPaneGroup {
6585 match pane_group {
6586 Member::Axis(PaneAxis {
6587 axis,
6588 members,
6589 flexes,
6590 bounding_boxes: _,
6591 }) => SerializedPaneGroup::Group {
6592 axis: SerializedAxis(*axis),
6593 children: members
6594 .iter()
6595 .map(|member| build_serialized_pane_group(member, window, cx))
6596 .collect::<Vec<_>>(),
6597 flexes: Some(flexes.lock().clone()),
6598 },
6599 Member::Pane(pane_handle) => {
6600 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
6601 }
6602 }
6603 }
6604
6605 fn build_serialized_docks(
6606 this: &Workspace,
6607 window: &mut Window,
6608 cx: &mut App,
6609 ) -> DockStructure {
6610 this.capture_dock_state(window, cx)
6611 }
6612
6613 match self.workspace_location(cx) {
6614 WorkspaceLocation::Location(location, paths) => {
6615 let breakpoints = self.project.update(cx, |project, cx| {
6616 project
6617 .breakpoint_store()
6618 .read(cx)
6619 .all_source_breakpoints(cx)
6620 });
6621 let user_toolchains = self
6622 .project
6623 .read(cx)
6624 .user_toolchains(cx)
6625 .unwrap_or_default();
6626
6627 let center_group = build_serialized_pane_group(&self.center.root, window, cx);
6628 let docks = build_serialized_docks(self, window, cx);
6629 let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
6630
6631 let serialized_workspace = SerializedWorkspace {
6632 id: database_id,
6633 location,
6634 paths,
6635 center_group,
6636 window_bounds,
6637 display: Default::default(),
6638 docks,
6639 centered_layout: self.centered_layout,
6640 session_id: self.session_id.clone(),
6641 breakpoints,
6642 window_id: Some(window.window_handle().window_id().as_u64()),
6643 user_toolchains,
6644 };
6645
6646 let db = WorkspaceDb::global(cx);
6647 window.spawn(cx, async move |_| {
6648 db.save_workspace(serialized_workspace).await;
6649 })
6650 }
6651 WorkspaceLocation::DetachFromSession => {
6652 let window_bounds = SerializedWindowBounds(window.window_bounds());
6653 let display = window.display(cx).and_then(|d| d.uuid().ok());
6654 // Save dock state for empty local workspaces
6655 let docks = build_serialized_docks(self, window, cx);
6656 let db = WorkspaceDb::global(cx);
6657 let kvp = db::kvp::KeyValueStore::global(cx);
6658 window.spawn(cx, async move |_| {
6659 db.set_window_open_status(
6660 database_id,
6661 window_bounds,
6662 display.unwrap_or_default(),
6663 )
6664 .await
6665 .log_err();
6666 db.set_session_id(database_id, None).await.log_err();
6667 persistence::write_default_dock_state(&kvp, docks)
6668 .await
6669 .log_err();
6670 })
6671 }
6672 WorkspaceLocation::None => {
6673 // Save dock state for empty non-local workspaces
6674 let docks = build_serialized_docks(self, window, cx);
6675 let kvp = db::kvp::KeyValueStore::global(cx);
6676 window.spawn(cx, async move |_| {
6677 persistence::write_default_dock_state(&kvp, docks)
6678 .await
6679 .log_err();
6680 })
6681 }
6682 }
6683 }
6684
6685 fn has_any_items_open(&self, cx: &App) -> bool {
6686 self.panes.iter().any(|pane| pane.read(cx).items_len() > 0)
6687 }
6688
6689 fn workspace_location(&self, cx: &App) -> WorkspaceLocation {
6690 let paths = PathList::new(&self.root_paths(cx));
6691 if let Some(connection) = self.project.read(cx).remote_connection_options(cx) {
6692 WorkspaceLocation::Location(SerializedWorkspaceLocation::Remote(connection), paths)
6693 } else if self.project.read(cx).is_local() {
6694 if !paths.is_empty() || self.has_any_items_open(cx) {
6695 WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths)
6696 } else {
6697 WorkspaceLocation::DetachFromSession
6698 }
6699 } else {
6700 WorkspaceLocation::None
6701 }
6702 }
6703
6704 fn update_history(&self, cx: &mut App) {
6705 let Some(id) = self.database_id() else {
6706 return;
6707 };
6708 if !self.project.read(cx).is_local() {
6709 return;
6710 }
6711 if let Some(manager) = HistoryManager::global(cx) {
6712 let paths = PathList::new(&self.root_paths(cx));
6713 manager.update(cx, |this, cx| {
6714 this.update_history(id, HistoryManagerEntry::new(id, &paths), cx);
6715 });
6716 }
6717 }
6718
6719 async fn serialize_items(
6720 this: &WeakEntity<Self>,
6721 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
6722 cx: &mut AsyncWindowContext,
6723 ) -> Result<()> {
6724 const CHUNK_SIZE: usize = 200;
6725
6726 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
6727
6728 while let Some(items_received) = serializable_items.next().await {
6729 let unique_items =
6730 items_received
6731 .into_iter()
6732 .fold(HashMap::default(), |mut acc, item| {
6733 acc.entry(item.item_id()).or_insert(item);
6734 acc
6735 });
6736
6737 // We use into_iter() here so that the references to the items are moved into
6738 // the tasks and not kept alive while we're sleeping.
6739 for (_, item) in unique_items.into_iter() {
6740 if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
6741 item.serialize(workspace, false, window, cx)
6742 }) {
6743 cx.background_spawn(async move { task.await.log_err() })
6744 .detach();
6745 }
6746 }
6747
6748 cx.background_executor()
6749 .timer(SERIALIZATION_THROTTLE_TIME)
6750 .await;
6751 }
6752
6753 Ok(())
6754 }
6755
6756 pub(crate) fn enqueue_item_serialization(
6757 &mut self,
6758 item: Box<dyn SerializableItemHandle>,
6759 ) -> Result<()> {
6760 self.serializable_items_tx
6761 .unbounded_send(item)
6762 .map_err(|err| anyhow!("failed to send serializable item over channel: {err}"))
6763 }
6764
6765 pub(crate) fn load_workspace(
6766 serialized_workspace: SerializedWorkspace,
6767 paths_to_open: Vec<Option<ProjectPath>>,
6768 window: &mut Window,
6769 cx: &mut Context<Workspace>,
6770 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
6771 cx.spawn_in(window, async move |workspace, cx| {
6772 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
6773
6774 let mut center_group = None;
6775 let mut center_items = None;
6776
6777 // Traverse the splits tree and add to things
6778 if let Some((group, active_pane, items)) = serialized_workspace
6779 .center_group
6780 .deserialize(&project, serialized_workspace.id, workspace.clone(), cx)
6781 .await
6782 {
6783 center_items = Some(items);
6784 center_group = Some((group, active_pane))
6785 }
6786
6787 let mut items_by_project_path = HashMap::default();
6788 let mut item_ids_by_kind = HashMap::default();
6789 let mut all_deserialized_items = Vec::default();
6790 cx.update(|_, cx| {
6791 for item in center_items.unwrap_or_default().into_iter().flatten() {
6792 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
6793 item_ids_by_kind
6794 .entry(serializable_item_handle.serialized_item_kind())
6795 .or_insert(Vec::new())
6796 .push(item.item_id().as_u64() as ItemId);
6797 }
6798
6799 if let Some(project_path) = item.project_path(cx) {
6800 items_by_project_path.insert(project_path, item.clone());
6801 }
6802 all_deserialized_items.push(item);
6803 }
6804 })?;
6805
6806 let opened_items = paths_to_open
6807 .into_iter()
6808 .map(|path_to_open| {
6809 path_to_open
6810 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
6811 })
6812 .collect::<Vec<_>>();
6813
6814 // Remove old panes from workspace panes list
6815 workspace.update_in(cx, |workspace, window, cx| {
6816 if let Some((center_group, active_pane)) = center_group {
6817 workspace.remove_panes(workspace.center.root.clone(), window, cx);
6818
6819 // Swap workspace center group
6820 workspace.center = PaneGroup::with_root(center_group);
6821 workspace.center.set_is_center(true);
6822 workspace.center.mark_positions(cx);
6823
6824 if let Some(active_pane) = active_pane {
6825 workspace.set_active_pane(&active_pane, window, cx);
6826 cx.focus_self(window);
6827 } else {
6828 workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
6829 }
6830 }
6831
6832 let docks = serialized_workspace.docks;
6833
6834 for (dock, serialized_dock) in [
6835 (&mut workspace.right_dock, docks.right),
6836 (&mut workspace.left_dock, docks.left),
6837 (&mut workspace.bottom_dock, docks.bottom),
6838 ]
6839 .iter_mut()
6840 {
6841 dock.update(cx, |dock, cx| {
6842 dock.serialized_dock = Some(serialized_dock.clone());
6843 dock.restore_state(window, cx);
6844 });
6845 }
6846
6847 cx.notify();
6848 })?;
6849
6850 let _ = project
6851 .update(cx, |project, cx| {
6852 project
6853 .breakpoint_store()
6854 .update(cx, |breakpoint_store, cx| {
6855 breakpoint_store
6856 .with_serialized_breakpoints(serialized_workspace.breakpoints, cx)
6857 })
6858 })
6859 .await;
6860
6861 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
6862 // after loading the items, we might have different items and in order to avoid
6863 // the database filling up, we delete items that haven't been loaded now.
6864 //
6865 // The items that have been loaded, have been saved after they've been added to the workspace.
6866 let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
6867 item_ids_by_kind
6868 .into_iter()
6869 .map(|(item_kind, loaded_items)| {
6870 SerializableItemRegistry::cleanup(
6871 item_kind,
6872 serialized_workspace.id,
6873 loaded_items,
6874 window,
6875 cx,
6876 )
6877 .log_err()
6878 })
6879 .collect::<Vec<_>>()
6880 })?;
6881
6882 futures::future::join_all(clean_up_tasks).await;
6883
6884 workspace
6885 .update_in(cx, |workspace, window, cx| {
6886 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
6887 workspace.serialize_workspace_internal(window, cx).detach();
6888
6889 // Ensure that we mark the window as edited if we did load dirty items
6890 workspace.update_window_edited(window, cx);
6891 })
6892 .ok();
6893
6894 Ok(opened_items)
6895 })
6896 }
6897
6898 pub fn key_context(&self, cx: &App) -> KeyContext {
6899 let mut context = KeyContext::new_with_defaults();
6900 context.add("Workspace");
6901 context.set("keyboard_layout", cx.keyboard_layout().name().to_string());
6902 if let Some(status) = self
6903 .debugger_provider
6904 .as_ref()
6905 .and_then(|provider| provider.active_thread_state(cx))
6906 {
6907 match status {
6908 ThreadStatus::Running | ThreadStatus::Stepping => {
6909 context.add("debugger_running");
6910 }
6911 ThreadStatus::Stopped => context.add("debugger_stopped"),
6912 ThreadStatus::Exited | ThreadStatus::Ended => {}
6913 }
6914 }
6915
6916 if self.left_dock.read(cx).is_open() {
6917 if let Some(active_panel) = self.left_dock.read(cx).active_panel() {
6918 context.set("left_dock", active_panel.panel_key());
6919 }
6920 }
6921
6922 if self.right_dock.read(cx).is_open() {
6923 if let Some(active_panel) = self.right_dock.read(cx).active_panel() {
6924 context.set("right_dock", active_panel.panel_key());
6925 }
6926 }
6927
6928 if self.bottom_dock.read(cx).is_open() {
6929 if let Some(active_panel) = self.bottom_dock.read(cx).active_panel() {
6930 context.set("bottom_dock", active_panel.panel_key());
6931 }
6932 }
6933
6934 context
6935 }
6936
6937 /// Multiworkspace uses this to add workspace action handling to itself
6938 pub fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
6939 self.add_workspace_actions_listeners(div, window, cx)
6940 .on_action(cx.listener(
6941 |_workspace, action_sequence: &settings::ActionSequence, window, cx| {
6942 for action in &action_sequence.0 {
6943 window.dispatch_action(action.boxed_clone(), cx);
6944 }
6945 },
6946 ))
6947 .on_action(cx.listener(Self::close_inactive_items_and_panes))
6948 .on_action(cx.listener(Self::close_all_items_and_panes))
6949 .on_action(cx.listener(Self::close_item_in_all_panes))
6950 .on_action(cx.listener(Self::save_all))
6951 .on_action(cx.listener(Self::send_keystrokes))
6952 .on_action(cx.listener(Self::add_folder_to_project))
6953 .on_action(cx.listener(Self::follow_next_collaborator))
6954 .on_action(cx.listener(Self::activate_pane_at_index))
6955 .on_action(cx.listener(Self::move_item_to_pane_at_index))
6956 .on_action(cx.listener(Self::move_focused_panel_to_next_position))
6957 .on_action(cx.listener(Self::toggle_edit_predictions_all_files))
6958 .on_action(cx.listener(Self::toggle_theme_mode))
6959 .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
6960 let pane = workspace.active_pane().clone();
6961 workspace.unfollow_in_pane(&pane, window, cx);
6962 }))
6963 .on_action(cx.listener(|workspace, action: &Save, window, cx| {
6964 workspace
6965 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
6966 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6967 }))
6968 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
6969 workspace
6970 .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
6971 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6972 }))
6973 .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
6974 workspace
6975 .save_active_item(SaveIntent::SaveAs, window, cx)
6976 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6977 }))
6978 .on_action(
6979 cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
6980 workspace.activate_previous_pane(window, cx)
6981 }),
6982 )
6983 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
6984 workspace.activate_next_pane(window, cx)
6985 }))
6986 .on_action(cx.listener(|workspace, _: &ActivateLastPane, window, cx| {
6987 workspace.activate_last_pane(window, cx)
6988 }))
6989 .on_action(
6990 cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
6991 workspace.activate_next_window(cx)
6992 }),
6993 )
6994 .on_action(
6995 cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
6996 workspace.activate_previous_window(cx)
6997 }),
6998 )
6999 .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
7000 workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
7001 }))
7002 .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
7003 workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
7004 }))
7005 .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
7006 workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
7007 }))
7008 .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
7009 workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
7010 }))
7011 .on_action(cx.listener(
7012 |workspace, action: &MoveItemToPaneInDirection, window, cx| {
7013 workspace.move_item_to_pane_in_direction(action, window, cx)
7014 },
7015 ))
7016 .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
7017 workspace.swap_pane_in_direction(SplitDirection::Left, cx)
7018 }))
7019 .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
7020 workspace.swap_pane_in_direction(SplitDirection::Right, cx)
7021 }))
7022 .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
7023 workspace.swap_pane_in_direction(SplitDirection::Up, cx)
7024 }))
7025 .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
7026 workspace.swap_pane_in_direction(SplitDirection::Down, cx)
7027 }))
7028 .on_action(cx.listener(|workspace, _: &SwapPaneAdjacent, window, cx| {
7029 const DIRECTION_PRIORITY: [SplitDirection; 4] = [
7030 SplitDirection::Down,
7031 SplitDirection::Up,
7032 SplitDirection::Right,
7033 SplitDirection::Left,
7034 ];
7035 for dir in DIRECTION_PRIORITY {
7036 if workspace.find_pane_in_direction(dir, cx).is_some() {
7037 workspace.swap_pane_in_direction(dir, cx);
7038 workspace.activate_pane_in_direction(dir.opposite(), window, cx);
7039 break;
7040 }
7041 }
7042 }))
7043 .on_action(cx.listener(|workspace, _: &MovePaneLeft, _, cx| {
7044 workspace.move_pane_to_border(SplitDirection::Left, cx)
7045 }))
7046 .on_action(cx.listener(|workspace, _: &MovePaneRight, _, cx| {
7047 workspace.move_pane_to_border(SplitDirection::Right, cx)
7048 }))
7049 .on_action(cx.listener(|workspace, _: &MovePaneUp, _, cx| {
7050 workspace.move_pane_to_border(SplitDirection::Up, cx)
7051 }))
7052 .on_action(cx.listener(|workspace, _: &MovePaneDown, _, cx| {
7053 workspace.move_pane_to_border(SplitDirection::Down, cx)
7054 }))
7055 .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
7056 this.toggle_dock(DockPosition::Left, window, cx);
7057 }))
7058 .on_action(cx.listener(
7059 |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
7060 workspace.toggle_dock(DockPosition::Right, window, cx);
7061 },
7062 ))
7063 .on_action(cx.listener(
7064 |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
7065 workspace.toggle_dock(DockPosition::Bottom, window, cx);
7066 },
7067 ))
7068 .on_action(cx.listener(
7069 |workspace: &mut Workspace, _: &CloseActiveDock, window, cx| {
7070 if !workspace.close_active_dock(window, cx) {
7071 cx.propagate();
7072 }
7073 },
7074 ))
7075 .on_action(
7076 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
7077 workspace.close_all_docks(window, cx);
7078 }),
7079 )
7080 .on_action(cx.listener(Self::toggle_all_docks))
7081 .on_action(cx.listener(
7082 |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
7083 workspace.clear_all_notifications(cx);
7084 },
7085 ))
7086 .on_action(cx.listener(
7087 |workspace: &mut Workspace, _: &ClearNavigationHistory, window, cx| {
7088 workspace.clear_navigation_history(window, cx);
7089 },
7090 ))
7091 .on_action(cx.listener(
7092 |workspace: &mut Workspace, _: &SuppressNotification, _, cx| {
7093 if let Some((notification_id, _)) = workspace.notifications.pop() {
7094 workspace.suppress_notification(¬ification_id, cx);
7095 }
7096 },
7097 ))
7098 .on_action(cx.listener(
7099 |workspace: &mut Workspace, _: &ToggleWorktreeSecurity, window, cx| {
7100 workspace.show_worktree_trust_security_modal(true, window, cx);
7101 },
7102 ))
7103 .on_action(
7104 cx.listener(|_: &mut Workspace, _: &ClearTrustedWorktrees, _, cx| {
7105 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
7106 trusted_worktrees.update(cx, |trusted_worktrees, _| {
7107 trusted_worktrees.clear_trusted_paths()
7108 });
7109 let db = WorkspaceDb::global(cx);
7110 cx.spawn(async move |_, cx| {
7111 if db.clear_trusted_worktrees().await.log_err().is_some() {
7112 cx.update(|cx| reload(cx));
7113 }
7114 })
7115 .detach();
7116 }
7117 }),
7118 )
7119 .on_action(cx.listener(
7120 |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
7121 workspace.reopen_closed_item(window, cx).detach();
7122 },
7123 ))
7124 .on_action(cx.listener(
7125 |workspace: &mut Workspace, _: &ResetActiveDockSize, window, cx| {
7126 for dock in workspace.all_docks() {
7127 if dock.focus_handle(cx).contains_focused(window, cx) {
7128 let panel = dock.read(cx).active_panel().cloned();
7129 if let Some(panel) = panel {
7130 dock.update(cx, |dock, cx| {
7131 dock.set_panel_size_state(
7132 panel.as_ref(),
7133 dock::PanelSizeState::default(),
7134 cx,
7135 );
7136 });
7137 }
7138 return;
7139 }
7140 }
7141 },
7142 ))
7143 .on_action(cx.listener(
7144 |workspace: &mut Workspace, _: &ResetOpenDocksSize, _window, cx| {
7145 for dock in workspace.all_docks() {
7146 let panel = dock.read(cx).visible_panel().cloned();
7147 if let Some(panel) = panel {
7148 dock.update(cx, |dock, cx| {
7149 dock.set_panel_size_state(
7150 panel.as_ref(),
7151 dock::PanelSizeState::default(),
7152 cx,
7153 );
7154 });
7155 }
7156 }
7157 },
7158 ))
7159 .on_action(cx.listener(
7160 |workspace: &mut Workspace, act: &IncreaseActiveDockSize, window, cx| {
7161 adjust_active_dock_size_by_px(
7162 px_with_ui_font_fallback(act.px, cx),
7163 workspace,
7164 window,
7165 cx,
7166 );
7167 },
7168 ))
7169 .on_action(cx.listener(
7170 |workspace: &mut Workspace, act: &DecreaseActiveDockSize, window, cx| {
7171 adjust_active_dock_size_by_px(
7172 px_with_ui_font_fallback(act.px, cx) * -1.,
7173 workspace,
7174 window,
7175 cx,
7176 );
7177 },
7178 ))
7179 .on_action(cx.listener(
7180 |workspace: &mut Workspace, act: &IncreaseOpenDocksSize, window, cx| {
7181 adjust_open_docks_size_by_px(
7182 px_with_ui_font_fallback(act.px, cx),
7183 workspace,
7184 window,
7185 cx,
7186 );
7187 },
7188 ))
7189 .on_action(cx.listener(
7190 |workspace: &mut Workspace, act: &DecreaseOpenDocksSize, window, cx| {
7191 adjust_open_docks_size_by_px(
7192 px_with_ui_font_fallback(act.px, cx) * -1.,
7193 workspace,
7194 window,
7195 cx,
7196 );
7197 },
7198 ))
7199 .on_action(cx.listener(Workspace::toggle_centered_layout))
7200 .on_action(cx.listener(
7201 |workspace: &mut Workspace, action: &pane::ActivateNextItem, window, cx| {
7202 if let Some(active_dock) = workspace.active_dock(window, cx) {
7203 let dock = active_dock.read(cx);
7204 if let Some(active_panel) = dock.active_panel() {
7205 if active_panel.pane(cx).is_none() {
7206 let mut recent_pane: Option<Entity<Pane>> = None;
7207 let mut recent_timestamp = 0;
7208 for pane_handle in workspace.panes() {
7209 let pane = pane_handle.read(cx);
7210 for entry in pane.activation_history() {
7211 if entry.timestamp > recent_timestamp {
7212 recent_timestamp = entry.timestamp;
7213 recent_pane = Some(pane_handle.clone());
7214 }
7215 }
7216 }
7217
7218 if let Some(pane) = recent_pane {
7219 let wrap_around = action.wrap_around;
7220 pane.update(cx, |pane, cx| {
7221 let current_index = pane.active_item_index();
7222 let items_len = pane.items_len();
7223 if items_len > 0 {
7224 let next_index = if current_index + 1 < items_len {
7225 current_index + 1
7226 } else if wrap_around {
7227 0
7228 } else {
7229 return;
7230 };
7231 pane.activate_item(
7232 next_index, false, false, window, cx,
7233 );
7234 }
7235 });
7236 return;
7237 }
7238 }
7239 }
7240 }
7241 cx.propagate();
7242 },
7243 ))
7244 .on_action(cx.listener(
7245 |workspace: &mut Workspace, action: &pane::ActivatePreviousItem, window, cx| {
7246 if let Some(active_dock) = workspace.active_dock(window, cx) {
7247 let dock = active_dock.read(cx);
7248 if let Some(active_panel) = dock.active_panel() {
7249 if active_panel.pane(cx).is_none() {
7250 let mut recent_pane: Option<Entity<Pane>> = None;
7251 let mut recent_timestamp = 0;
7252 for pane_handle in workspace.panes() {
7253 let pane = pane_handle.read(cx);
7254 for entry in pane.activation_history() {
7255 if entry.timestamp > recent_timestamp {
7256 recent_timestamp = entry.timestamp;
7257 recent_pane = Some(pane_handle.clone());
7258 }
7259 }
7260 }
7261
7262 if let Some(pane) = recent_pane {
7263 let wrap_around = action.wrap_around;
7264 pane.update(cx, |pane, cx| {
7265 let current_index = pane.active_item_index();
7266 let items_len = pane.items_len();
7267 if items_len > 0 {
7268 let prev_index = if current_index > 0 {
7269 current_index - 1
7270 } else if wrap_around {
7271 items_len.saturating_sub(1)
7272 } else {
7273 return;
7274 };
7275 pane.activate_item(
7276 prev_index, false, false, window, cx,
7277 );
7278 }
7279 });
7280 return;
7281 }
7282 }
7283 }
7284 }
7285 cx.propagate();
7286 },
7287 ))
7288 .on_action(cx.listener(
7289 |workspace: &mut Workspace, action: &pane::CloseActiveItem, window, cx| {
7290 if let Some(active_dock) = workspace.active_dock(window, cx) {
7291 let dock = active_dock.read(cx);
7292 if let Some(active_panel) = dock.active_panel() {
7293 if active_panel.pane(cx).is_none() {
7294 let active_pane = workspace.active_pane().clone();
7295 active_pane.update(cx, |pane, cx| {
7296 pane.close_active_item(action, window, cx)
7297 .detach_and_log_err(cx);
7298 });
7299 return;
7300 }
7301 }
7302 }
7303 cx.propagate();
7304 },
7305 ))
7306 .on_action(
7307 cx.listener(|workspace, _: &ToggleReadOnlyFile, window, cx| {
7308 let pane = workspace.active_pane().clone();
7309 if let Some(item) = pane.read(cx).active_item() {
7310 item.toggle_read_only(window, cx);
7311 }
7312 }),
7313 )
7314 .on_action(cx.listener(|workspace, _: &FocusCenterPane, window, cx| {
7315 workspace.focus_center_pane(window, cx);
7316 }))
7317 .on_action(cx.listener(Workspace::cancel))
7318 }
7319
7320 #[cfg(any(test, feature = "test-support"))]
7321 pub fn set_random_database_id(&mut self) {
7322 self.database_id = Some(WorkspaceId(Uuid::new_v4().as_u64_pair().0 as i64));
7323 }
7324
7325 #[cfg(any(test, feature = "test-support"))]
7326 pub(crate) fn test_new(
7327 project: Entity<Project>,
7328 window: &mut Window,
7329 cx: &mut Context<Self>,
7330 ) -> Self {
7331 use node_runtime::NodeRuntime;
7332 use session::Session;
7333
7334 let client = project.read(cx).client();
7335 let user_store = project.read(cx).user_store();
7336 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
7337 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
7338 window.activate_window();
7339 let app_state = Arc::new(AppState {
7340 languages: project.read(cx).languages().clone(),
7341 workspace_store,
7342 client,
7343 user_store,
7344 fs: project.read(cx).fs().clone(),
7345 build_window_options: |_, _| Default::default(),
7346 node_runtime: NodeRuntime::unavailable(),
7347 session,
7348 });
7349 let workspace = Self::new(Default::default(), project, app_state, window, cx);
7350 workspace
7351 .active_pane
7352 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
7353 workspace
7354 }
7355
7356 pub fn register_action<A: Action>(
7357 &mut self,
7358 callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
7359 ) -> &mut Self {
7360 let callback = Arc::new(callback);
7361
7362 self.workspace_actions.push(Box::new(move |div, _, _, cx| {
7363 let callback = callback.clone();
7364 div.on_action(cx.listener(move |workspace, event, window, cx| {
7365 (callback)(workspace, event, window, cx)
7366 }))
7367 }));
7368 self
7369 }
7370 pub fn register_action_renderer(
7371 &mut self,
7372 callback: impl Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div + 'static,
7373 ) -> &mut Self {
7374 self.workspace_actions.push(Box::new(callback));
7375 self
7376 }
7377
7378 fn add_workspace_actions_listeners(
7379 &self,
7380 mut div: Div,
7381 window: &mut Window,
7382 cx: &mut Context<Self>,
7383 ) -> Div {
7384 for action in self.workspace_actions.iter() {
7385 div = (action)(div, self, window, cx)
7386 }
7387 div
7388 }
7389
7390 pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
7391 self.modal_layer.read(cx).has_active_modal()
7392 }
7393
7394 pub fn is_active_modal_command_palette(&self, cx: &mut App) -> bool {
7395 self.modal_layer
7396 .read(cx)
7397 .is_active_modal_command_palette(cx)
7398 }
7399
7400 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
7401 self.modal_layer.read(cx).active_modal()
7402 }
7403
7404 /// Toggles a modal of type `V`. If a modal of the same type is currently active,
7405 /// it will be hidden. If a different modal is active, it will be replaced with the new one.
7406 /// If no modal is active, the new modal will be shown.
7407 ///
7408 /// If closing the current modal fails (e.g., due to `on_before_dismiss` returning
7409 /// `DismissDecision::Dismiss(false)` or `DismissDecision::Pending`), the new modal
7410 /// will not be shown.
7411 pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
7412 where
7413 B: FnOnce(&mut Window, &mut Context<V>) -> V,
7414 {
7415 self.modal_layer.update(cx, |modal_layer, cx| {
7416 modal_layer.toggle_modal(window, cx, build)
7417 })
7418 }
7419
7420 pub fn hide_modal(&mut self, window: &mut Window, cx: &mut App) -> bool {
7421 self.modal_layer
7422 .update(cx, |modal_layer, cx| modal_layer.hide_modal(window, cx))
7423 }
7424
7425 pub fn toggle_status_toast<V: ToastView>(&mut self, entity: Entity<V>, cx: &mut App) {
7426 self.toast_layer
7427 .update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity))
7428 }
7429
7430 pub fn toggle_centered_layout(
7431 &mut self,
7432 _: &ToggleCenteredLayout,
7433 _: &mut Window,
7434 cx: &mut Context<Self>,
7435 ) {
7436 self.centered_layout = !self.centered_layout;
7437 if let Some(database_id) = self.database_id() {
7438 let db = WorkspaceDb::global(cx);
7439 let centered_layout = self.centered_layout;
7440 cx.background_spawn(async move {
7441 db.set_centered_layout(database_id, centered_layout).await
7442 })
7443 .detach_and_log_err(cx);
7444 }
7445 cx.notify();
7446 }
7447
7448 fn adjust_padding(padding: Option<f32>) -> f32 {
7449 padding
7450 .unwrap_or(CenteredPaddingSettings::default().0)
7451 .clamp(
7452 CenteredPaddingSettings::MIN_PADDING,
7453 CenteredPaddingSettings::MAX_PADDING,
7454 )
7455 }
7456
7457 fn render_dock(
7458 &self,
7459 position: DockPosition,
7460 dock: &Entity<Dock>,
7461 window: &mut Window,
7462 cx: &mut App,
7463 ) -> Option<Div> {
7464 if self.zoomed_position == Some(position) {
7465 return None;
7466 }
7467
7468 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
7469 let pane = panel.pane(cx)?;
7470 let follower_states = &self.follower_states;
7471 leader_border_for_pane(follower_states, &pane, window, cx)
7472 });
7473
7474 let mut container = div()
7475 .flex()
7476 .overflow_hidden()
7477 .flex_none()
7478 .child(dock.clone())
7479 .children(leader_border);
7480
7481 // Apply sizing only when the dock is open. When closed the dock is still
7482 // included in the element tree so its focus handle remains mounted — without
7483 // this, toggle_panel_focus cannot focus the panel when the dock is closed.
7484 let dock = dock.read(cx);
7485 if let Some(panel) = dock.visible_panel() {
7486 let size_state = dock.stored_panel_size_state(panel.as_ref());
7487 if position.axis() == Axis::Horizontal {
7488 let use_flexible = panel.has_flexible_size(window, cx);
7489 let flex_grow = if use_flexible {
7490 size_state
7491 .and_then(|state| state.flex)
7492 .or_else(|| self.default_dock_flex(position))
7493 } else {
7494 None
7495 };
7496 if let Some(grow) = flex_grow {
7497 let grow = grow.max(0.001);
7498 let style = container.style();
7499 style.flex_grow = Some(grow);
7500 style.flex_shrink = Some(1.0);
7501 style.flex_basis = Some(relative(0.).into());
7502 } else {
7503 let size = size_state
7504 .and_then(|state| state.size)
7505 .unwrap_or_else(|| panel.default_size(window, cx));
7506 container = container.w(size);
7507 }
7508 } else {
7509 let size = size_state
7510 .and_then(|state| state.size)
7511 .unwrap_or_else(|| panel.default_size(window, cx));
7512 container = container.h(size);
7513 }
7514 }
7515
7516 Some(container)
7517 }
7518
7519 pub fn for_window(window: &Window, cx: &App) -> Option<Entity<Workspace>> {
7520 window
7521 .root::<MultiWorkspace>()
7522 .flatten()
7523 .map(|multi_workspace| multi_workspace.read(cx).workspace().clone())
7524 }
7525
7526 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
7527 self.zoomed.as_ref()
7528 }
7529
7530 pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
7531 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
7532 return;
7533 };
7534 let windows = cx.windows();
7535 let next_window =
7536 SystemWindowTabController::get_next_tab_group_window(cx, current_window_id).or_else(
7537 || {
7538 windows
7539 .iter()
7540 .cycle()
7541 .skip_while(|window| window.window_id() != current_window_id)
7542 .nth(1)
7543 },
7544 );
7545
7546 if let Some(window) = next_window {
7547 window
7548 .update(cx, |_, window, _| window.activate_window())
7549 .ok();
7550 }
7551 }
7552
7553 pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
7554 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
7555 return;
7556 };
7557 let windows = cx.windows();
7558 let prev_window =
7559 SystemWindowTabController::get_prev_tab_group_window(cx, current_window_id).or_else(
7560 || {
7561 windows
7562 .iter()
7563 .rev()
7564 .cycle()
7565 .skip_while(|window| window.window_id() != current_window_id)
7566 .nth(1)
7567 },
7568 );
7569
7570 if let Some(window) = prev_window {
7571 window
7572 .update(cx, |_, window, _| window.activate_window())
7573 .ok();
7574 }
7575 }
7576
7577 pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
7578 if cx.stop_active_drag(window) {
7579 } else if let Some((notification_id, _)) = self.notifications.pop() {
7580 dismiss_app_notification(¬ification_id, cx);
7581 } else {
7582 cx.propagate();
7583 }
7584 }
7585
7586 fn resize_dock(
7587 &mut self,
7588 dock_pos: DockPosition,
7589 new_size: Pixels,
7590 window: &mut Window,
7591 cx: &mut Context<Self>,
7592 ) {
7593 match dock_pos {
7594 DockPosition::Left => self.resize_left_dock(new_size, window, cx),
7595 DockPosition::Right => self.resize_right_dock(new_size, window, cx),
7596 DockPosition::Bottom => self.resize_bottom_dock(new_size, window, cx),
7597 }
7598 }
7599
7600 fn resize_left_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
7601 let workspace_width = self.bounds.size.width;
7602 let mut size = new_size.min(workspace_width - RESIZE_HANDLE_SIZE);
7603
7604 self.right_dock.read_with(cx, |right_dock, cx| {
7605 let right_dock_size = right_dock
7606 .stored_active_panel_size(window, cx)
7607 .unwrap_or(Pixels::ZERO);
7608 if right_dock_size + size > workspace_width {
7609 size = workspace_width - right_dock_size
7610 }
7611 });
7612
7613 let flex_grow = self.dock_flex_for_size(DockPosition::Left, size, window, cx);
7614 self.left_dock.update(cx, |left_dock, cx| {
7615 if WorkspaceSettings::get_global(cx)
7616 .resize_all_panels_in_dock
7617 .contains(&DockPosition::Left)
7618 {
7619 left_dock.resize_all_panels(Some(size), flex_grow, window, cx);
7620 } else {
7621 left_dock.resize_active_panel(Some(size), flex_grow, window, cx);
7622 }
7623 });
7624 }
7625
7626 fn resize_right_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
7627 let workspace_width = self.bounds.size.width;
7628 let mut size = new_size.min(workspace_width - RESIZE_HANDLE_SIZE);
7629 self.left_dock.read_with(cx, |left_dock, cx| {
7630 let left_dock_size = left_dock
7631 .stored_active_panel_size(window, cx)
7632 .unwrap_or(Pixels::ZERO);
7633 if left_dock_size + size > workspace_width {
7634 size = workspace_width - left_dock_size
7635 }
7636 });
7637 let flex_grow = self.dock_flex_for_size(DockPosition::Right, size, window, cx);
7638 self.right_dock.update(cx, |right_dock, cx| {
7639 if WorkspaceSettings::get_global(cx)
7640 .resize_all_panels_in_dock
7641 .contains(&DockPosition::Right)
7642 {
7643 right_dock.resize_all_panels(Some(size), flex_grow, window, cx);
7644 } else {
7645 right_dock.resize_active_panel(Some(size), flex_grow, window, cx);
7646 }
7647 });
7648 }
7649
7650 fn resize_bottom_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
7651 let size = new_size.min(self.bounds.bottom() - RESIZE_HANDLE_SIZE - self.bounds.top());
7652 self.bottom_dock.update(cx, |bottom_dock, cx| {
7653 if WorkspaceSettings::get_global(cx)
7654 .resize_all_panels_in_dock
7655 .contains(&DockPosition::Bottom)
7656 {
7657 bottom_dock.resize_all_panels(Some(size), None, window, cx);
7658 } else {
7659 bottom_dock.resize_active_panel(Some(size), None, window, cx);
7660 }
7661 });
7662 }
7663
7664 fn toggle_edit_predictions_all_files(
7665 &mut self,
7666 _: &ToggleEditPrediction,
7667 _window: &mut Window,
7668 cx: &mut Context<Self>,
7669 ) {
7670 let fs = self.project().read(cx).fs().clone();
7671 let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx);
7672 update_settings_file(fs, cx, move |file, _| {
7673 file.project.all_languages.defaults.show_edit_predictions = Some(!show_edit_predictions)
7674 });
7675 }
7676
7677 fn toggle_theme_mode(&mut self, _: &ToggleMode, _window: &mut Window, cx: &mut Context<Self>) {
7678 let current_mode = ThemeSettings::get_global(cx).theme.mode();
7679 let next_mode = match current_mode {
7680 Some(theme_settings::ThemeAppearanceMode::Light) => {
7681 theme_settings::ThemeAppearanceMode::Dark
7682 }
7683 Some(theme_settings::ThemeAppearanceMode::Dark) => {
7684 theme_settings::ThemeAppearanceMode::Light
7685 }
7686 Some(theme_settings::ThemeAppearanceMode::System) | None => {
7687 match cx.theme().appearance() {
7688 theme::Appearance::Light => theme_settings::ThemeAppearanceMode::Dark,
7689 theme::Appearance::Dark => theme_settings::ThemeAppearanceMode::Light,
7690 }
7691 }
7692 };
7693
7694 let fs = self.project().read(cx).fs().clone();
7695 settings::update_settings_file(fs, cx, move |settings, _cx| {
7696 theme_settings::set_mode(settings, next_mode);
7697 });
7698 }
7699
7700 pub fn show_worktree_trust_security_modal(
7701 &mut self,
7702 toggle: bool,
7703 window: &mut Window,
7704 cx: &mut Context<Self>,
7705 ) {
7706 if let Some(security_modal) = self.active_modal::<SecurityModal>(cx) {
7707 if toggle {
7708 security_modal.update(cx, |security_modal, cx| {
7709 security_modal.dismiss(cx);
7710 })
7711 } else {
7712 security_modal.update(cx, |security_modal, cx| {
7713 security_modal.refresh_restricted_paths(cx);
7714 });
7715 }
7716 } else {
7717 let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
7718 .map(|trusted_worktrees| {
7719 trusted_worktrees
7720 .read(cx)
7721 .has_restricted_worktrees(&self.project().read(cx).worktree_store(), cx)
7722 })
7723 .unwrap_or(false);
7724 if has_restricted_worktrees {
7725 let project = self.project().read(cx);
7726 let remote_host = project
7727 .remote_connection_options(cx)
7728 .map(RemoteHostLocation::from);
7729 let worktree_store = project.worktree_store().downgrade();
7730 self.toggle_modal(window, cx, |_, cx| {
7731 SecurityModal::new(worktree_store, remote_host, cx)
7732 });
7733 }
7734 }
7735 }
7736}
7737
7738pub trait AnyActiveCall {
7739 fn entity(&self) -> AnyEntity;
7740 fn is_in_room(&self, _: &App) -> bool;
7741 fn room_id(&self, _: &App) -> Option<u64>;
7742 fn channel_id(&self, _: &App) -> Option<ChannelId>;
7743 fn hang_up(&self, _: &mut App) -> Task<Result<()>>;
7744 fn unshare_project(&self, _: Entity<Project>, _: &mut App) -> Result<()>;
7745 fn remote_participant_for_peer_id(&self, _: PeerId, _: &App) -> Option<RemoteCollaborator>;
7746 fn is_sharing_project(&self, _: &App) -> bool;
7747 fn has_remote_participants(&self, _: &App) -> bool;
7748 fn local_participant_is_guest(&self, _: &App) -> bool;
7749 fn client(&self, _: &App) -> Arc<Client>;
7750 fn share_on_join(&self, _: &App) -> bool;
7751 fn join_channel(&self, _: ChannelId, _: &mut App) -> Task<Result<bool>>;
7752 fn room_update_completed(&self, _: &mut App) -> Task<()>;
7753 fn most_active_project(&self, _: &App) -> Option<(u64, u64)>;
7754 fn share_project(&self, _: Entity<Project>, _: &mut App) -> Task<Result<u64>>;
7755 fn join_project(
7756 &self,
7757 _: u64,
7758 _: Arc<LanguageRegistry>,
7759 _: Arc<dyn Fs>,
7760 _: &mut App,
7761 ) -> Task<Result<Entity<Project>>>;
7762 fn peer_id_for_user_in_room(&self, _: u64, _: &App) -> Option<PeerId>;
7763 fn subscribe(
7764 &self,
7765 _: &mut Window,
7766 _: &mut Context<Workspace>,
7767 _: Box<dyn Fn(&mut Workspace, &ActiveCallEvent, &mut Window, &mut Context<Workspace>)>,
7768 ) -> Subscription;
7769 fn create_shared_screen(
7770 &self,
7771 _: PeerId,
7772 _: &Entity<Pane>,
7773 _: &mut Window,
7774 _: &mut App,
7775 ) -> Option<Entity<SharedScreen>>;
7776}
7777
7778#[derive(Clone)]
7779pub struct GlobalAnyActiveCall(pub Arc<dyn AnyActiveCall>);
7780impl Global for GlobalAnyActiveCall {}
7781
7782impl GlobalAnyActiveCall {
7783 pub(crate) fn try_global(cx: &App) -> Option<&Self> {
7784 cx.try_global()
7785 }
7786
7787 pub(crate) fn global(cx: &App) -> &Self {
7788 cx.global()
7789 }
7790}
7791
7792/// Workspace-local view of a remote participant's location.
7793#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7794pub enum ParticipantLocation {
7795 SharedProject { project_id: u64 },
7796 UnsharedProject,
7797 External,
7798}
7799
7800impl ParticipantLocation {
7801 pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
7802 match location
7803 .and_then(|l| l.variant)
7804 .context("participant location was not provided")?
7805 {
7806 proto::participant_location::Variant::SharedProject(project) => {
7807 Ok(Self::SharedProject {
7808 project_id: project.id,
7809 })
7810 }
7811 proto::participant_location::Variant::UnsharedProject(_) => Ok(Self::UnsharedProject),
7812 proto::participant_location::Variant::External(_) => Ok(Self::External),
7813 }
7814 }
7815}
7816/// Workspace-local view of a remote collaborator's state.
7817/// This is the subset of `call::RemoteParticipant` that workspace needs.
7818#[derive(Clone)]
7819pub struct RemoteCollaborator {
7820 pub user: Arc<User>,
7821 pub peer_id: PeerId,
7822 pub location: ParticipantLocation,
7823 pub participant_index: ParticipantIndex,
7824}
7825
7826pub enum ActiveCallEvent {
7827 ParticipantLocationChanged { participant_id: PeerId },
7828 RemoteVideoTracksChanged { participant_id: PeerId },
7829}
7830
7831fn leader_border_for_pane(
7832 follower_states: &HashMap<CollaboratorId, FollowerState>,
7833 pane: &Entity<Pane>,
7834 _: &Window,
7835 cx: &App,
7836) -> Option<Div> {
7837 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
7838 if state.pane() == pane {
7839 Some((*leader_id, state))
7840 } else {
7841 None
7842 }
7843 })?;
7844
7845 let mut leader_color = match leader_id {
7846 CollaboratorId::PeerId(leader_peer_id) => {
7847 let leader = GlobalAnyActiveCall::try_global(cx)?
7848 .0
7849 .remote_participant_for_peer_id(leader_peer_id, cx)?;
7850
7851 cx.theme()
7852 .players()
7853 .color_for_participant(leader.participant_index.0)
7854 .cursor
7855 }
7856 CollaboratorId::Agent => cx.theme().players().agent().cursor,
7857 };
7858 leader_color.fade_out(0.3);
7859 Some(
7860 div()
7861 .absolute()
7862 .size_full()
7863 .left_0()
7864 .top_0()
7865 .border_2()
7866 .border_color(leader_color),
7867 )
7868}
7869
7870fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
7871 ZED_WINDOW_POSITION
7872 .zip(*ZED_WINDOW_SIZE)
7873 .map(|(position, size)| Bounds {
7874 origin: position,
7875 size,
7876 })
7877}
7878
7879fn open_items(
7880 serialized_workspace: Option<SerializedWorkspace>,
7881 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
7882 window: &mut Window,
7883 cx: &mut Context<Workspace>,
7884) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
7885 let restored_items = serialized_workspace.map(|serialized_workspace| {
7886 Workspace::load_workspace(
7887 serialized_workspace,
7888 project_paths_to_open
7889 .iter()
7890 .map(|(_, project_path)| project_path)
7891 .cloned()
7892 .collect(),
7893 window,
7894 cx,
7895 )
7896 });
7897
7898 cx.spawn_in(window, async move |workspace, cx| {
7899 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
7900
7901 if let Some(restored_items) = restored_items {
7902 let restored_items = restored_items.await?;
7903
7904 let restored_project_paths = restored_items
7905 .iter()
7906 .filter_map(|item| {
7907 cx.update(|_, cx| item.as_ref()?.project_path(cx))
7908 .ok()
7909 .flatten()
7910 })
7911 .collect::<HashSet<_>>();
7912
7913 for restored_item in restored_items {
7914 opened_items.push(restored_item.map(Ok));
7915 }
7916
7917 project_paths_to_open
7918 .iter_mut()
7919 .for_each(|(_, project_path)| {
7920 if let Some(project_path_to_open) = project_path
7921 && restored_project_paths.contains(project_path_to_open)
7922 {
7923 *project_path = None;
7924 }
7925 });
7926 } else {
7927 for _ in 0..project_paths_to_open.len() {
7928 opened_items.push(None);
7929 }
7930 }
7931 assert!(opened_items.len() == project_paths_to_open.len());
7932
7933 let tasks =
7934 project_paths_to_open
7935 .into_iter()
7936 .enumerate()
7937 .map(|(ix, (abs_path, project_path))| {
7938 let workspace = workspace.clone();
7939 cx.spawn(async move |cx| {
7940 let file_project_path = project_path?;
7941 let abs_path_task = workspace.update(cx, |workspace, cx| {
7942 workspace.project().update(cx, |project, cx| {
7943 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
7944 })
7945 });
7946
7947 // We only want to open file paths here. If one of the items
7948 // here is a directory, it was already opened further above
7949 // with a `find_or_create_worktree`.
7950 if let Ok(task) = abs_path_task
7951 && task.await.is_none_or(|p| p.is_file())
7952 {
7953 return Some((
7954 ix,
7955 workspace
7956 .update_in(cx, |workspace, window, cx| {
7957 workspace.open_path(
7958 file_project_path,
7959 None,
7960 true,
7961 window,
7962 cx,
7963 )
7964 })
7965 .log_err()?
7966 .await,
7967 ));
7968 }
7969 None
7970 })
7971 });
7972
7973 let tasks = tasks.collect::<Vec<_>>();
7974
7975 let tasks = futures::future::join_all(tasks);
7976 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
7977 opened_items[ix] = Some(path_open_result);
7978 }
7979
7980 Ok(opened_items)
7981 })
7982}
7983
7984#[derive(Clone)]
7985enum ActivateInDirectionTarget {
7986 Pane(Entity<Pane>),
7987 Dock(Entity<Dock>),
7988 Sidebar(FocusHandle),
7989}
7990
7991fn notify_if_database_failed(window: WindowHandle<MultiWorkspace>, cx: &mut AsyncApp) {
7992 window
7993 .update(cx, |multi_workspace, _, cx| {
7994 let workspace = multi_workspace.workspace().clone();
7995 workspace.update(cx, |workspace, cx| {
7996 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
7997 struct DatabaseFailedNotification;
7998
7999 workspace.show_notification(
8000 NotificationId::unique::<DatabaseFailedNotification>(),
8001 cx,
8002 |cx| {
8003 cx.new(|cx| {
8004 MessageNotification::new("Failed to load the database file.", cx)
8005 .primary_message("File an Issue")
8006 .primary_icon(IconName::Plus)
8007 .primary_on_click(|window, cx| {
8008 window.dispatch_action(Box::new(FileBugReport), cx)
8009 })
8010 })
8011 },
8012 );
8013 }
8014 });
8015 })
8016 .log_err();
8017}
8018
8019fn px_with_ui_font_fallback(val: u32, cx: &Context<Workspace>) -> Pixels {
8020 if val == 0 {
8021 ThemeSettings::get_global(cx).ui_font_size(cx)
8022 } else {
8023 px(val as f32)
8024 }
8025}
8026
8027fn adjust_active_dock_size_by_px(
8028 px: Pixels,
8029 workspace: &mut Workspace,
8030 window: &mut Window,
8031 cx: &mut Context<Workspace>,
8032) {
8033 let Some(active_dock) = workspace
8034 .all_docks()
8035 .into_iter()
8036 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx))
8037 else {
8038 return;
8039 };
8040 let dock = active_dock.read(cx);
8041 let Some(panel_size) = workspace.dock_size(&dock, window, cx) else {
8042 return;
8043 };
8044 workspace.resize_dock(dock.position(), panel_size + px, window, cx);
8045}
8046
8047fn adjust_open_docks_size_by_px(
8048 px: Pixels,
8049 workspace: &mut Workspace,
8050 window: &mut Window,
8051 cx: &mut Context<Workspace>,
8052) {
8053 let docks = workspace
8054 .all_docks()
8055 .into_iter()
8056 .filter_map(|dock_entity| {
8057 let dock = dock_entity.read(cx);
8058 if dock.is_open() {
8059 let dock_pos = dock.position();
8060 let panel_size = workspace.dock_size(&dock, window, cx)?;
8061 Some((dock_pos, panel_size + px))
8062 } else {
8063 None
8064 }
8065 })
8066 .collect::<Vec<_>>();
8067
8068 for (position, new_size) in docks {
8069 workspace.resize_dock(position, new_size, window, cx);
8070 }
8071}
8072
8073impl Focusable for Workspace {
8074 fn focus_handle(&self, cx: &App) -> FocusHandle {
8075 self.active_pane.focus_handle(cx)
8076 }
8077}
8078
8079#[derive(Clone)]
8080struct DraggedDock(DockPosition);
8081
8082impl Render for DraggedDock {
8083 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
8084 gpui::Empty
8085 }
8086}
8087
8088impl Render for Workspace {
8089 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
8090 static FIRST_PAINT: AtomicBool = AtomicBool::new(true);
8091 if FIRST_PAINT.swap(false, std::sync::atomic::Ordering::Relaxed) {
8092 log::info!("Rendered first frame");
8093 }
8094
8095 let centered_layout = self.centered_layout
8096 && self.center.panes().len() == 1
8097 && self.active_item(cx).is_some();
8098 let render_padding = |size| {
8099 (size > 0.0).then(|| {
8100 div()
8101 .h_full()
8102 .w(relative(size))
8103 .bg(cx.theme().colors().editor_background)
8104 .border_color(cx.theme().colors().pane_group_border)
8105 })
8106 };
8107 let paddings = if centered_layout {
8108 let settings = WorkspaceSettings::get_global(cx).centered_layout;
8109 (
8110 render_padding(Self::adjust_padding(
8111 settings.left_padding.map(|padding| padding.0),
8112 )),
8113 render_padding(Self::adjust_padding(
8114 settings.right_padding.map(|padding| padding.0),
8115 )),
8116 )
8117 } else {
8118 (None, None)
8119 };
8120 let ui_font = theme_settings::setup_ui_font(window, cx);
8121
8122 let theme = cx.theme().clone();
8123 let colors = theme.colors();
8124 let notification_entities = self
8125 .notifications
8126 .iter()
8127 .map(|(_, notification)| notification.entity_id())
8128 .collect::<Vec<_>>();
8129 let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout;
8130
8131 div()
8132 .relative()
8133 .size_full()
8134 .flex()
8135 .flex_col()
8136 .font(ui_font)
8137 .gap_0()
8138 .justify_start()
8139 .items_start()
8140 .text_color(colors.text)
8141 .overflow_hidden()
8142 .children(self.titlebar_item.clone())
8143 .on_modifiers_changed(move |_, _, cx| {
8144 for &id in ¬ification_entities {
8145 cx.notify(id);
8146 }
8147 })
8148 .child(
8149 div()
8150 .size_full()
8151 .relative()
8152 .flex_1()
8153 .flex()
8154 .flex_col()
8155 .child(
8156 div()
8157 .id("workspace")
8158 .bg(colors.background)
8159 .relative()
8160 .flex_1()
8161 .w_full()
8162 .flex()
8163 .flex_col()
8164 .overflow_hidden()
8165 .border_t_1()
8166 .border_b_1()
8167 .border_color(colors.border)
8168 .child({
8169 let this = cx.entity();
8170 canvas(
8171 move |bounds, window, cx| {
8172 this.update(cx, |this, cx| {
8173 let bounds_changed = this.bounds != bounds;
8174 this.bounds = bounds;
8175
8176 if bounds_changed {
8177 this.left_dock.update(cx, |dock, cx| {
8178 dock.clamp_panel_size(
8179 bounds.size.width,
8180 window,
8181 cx,
8182 )
8183 });
8184
8185 this.right_dock.update(cx, |dock, cx| {
8186 dock.clamp_panel_size(
8187 bounds.size.width,
8188 window,
8189 cx,
8190 )
8191 });
8192
8193 this.bottom_dock.update(cx, |dock, cx| {
8194 dock.clamp_panel_size(
8195 bounds.size.height,
8196 window,
8197 cx,
8198 )
8199 });
8200 }
8201 })
8202 },
8203 |_, _, _, _| {},
8204 )
8205 .absolute()
8206 .size_full()
8207 })
8208 .when(self.zoomed.is_none(), |this| {
8209 this.on_drag_move(cx.listener(
8210 move |workspace,
8211 e: &DragMoveEvent<DraggedDock>,
8212 window,
8213 cx| {
8214 if workspace.previous_dock_drag_coordinates
8215 != Some(e.event.position)
8216 {
8217 workspace.previous_dock_drag_coordinates =
8218 Some(e.event.position);
8219
8220 match e.drag(cx).0 {
8221 DockPosition::Left => {
8222 workspace.resize_left_dock(
8223 e.event.position.x
8224 - workspace.bounds.left(),
8225 window,
8226 cx,
8227 );
8228 }
8229 DockPosition::Right => {
8230 workspace.resize_right_dock(
8231 workspace.bounds.right()
8232 - e.event.position.x,
8233 window,
8234 cx,
8235 );
8236 }
8237 DockPosition::Bottom => {
8238 workspace.resize_bottom_dock(
8239 workspace.bounds.bottom()
8240 - e.event.position.y,
8241 window,
8242 cx,
8243 );
8244 }
8245 };
8246 workspace.serialize_workspace(window, cx);
8247 }
8248 },
8249 ))
8250
8251 })
8252 .child({
8253 match bottom_dock_layout {
8254 BottomDockLayout::Full => div()
8255 .flex()
8256 .flex_col()
8257 .h_full()
8258 .child(
8259 div()
8260 .flex()
8261 .flex_row()
8262 .flex_1()
8263 .overflow_hidden()
8264 .children(self.render_dock(
8265 DockPosition::Left,
8266 &self.left_dock,
8267 window,
8268 cx,
8269 ))
8270
8271 .child(
8272 div()
8273 .flex()
8274 .flex_col()
8275 .flex_1()
8276 .overflow_hidden()
8277 .child(
8278 h_flex()
8279 .flex_1()
8280 .when_some(
8281 paddings.0,
8282 |this, p| {
8283 this.child(
8284 p.border_r_1(),
8285 )
8286 },
8287 )
8288 .child(self.center.render(
8289 self.zoomed.as_ref(),
8290 &PaneRenderContext {
8291 follower_states:
8292 &self.follower_states,
8293 active_call: self.active_call(),
8294 active_pane: &self.active_pane,
8295 app_state: &self.app_state,
8296 project: &self.project,
8297 workspace: &self.weak_self,
8298 },
8299 window,
8300 cx,
8301 ))
8302 .when_some(
8303 paddings.1,
8304 |this, p| {
8305 this.child(
8306 p.border_l_1(),
8307 )
8308 },
8309 ),
8310 ),
8311 )
8312
8313 .children(self.render_dock(
8314 DockPosition::Right,
8315 &self.right_dock,
8316 window,
8317 cx,
8318 )),
8319 )
8320 .child(div().w_full().children(self.render_dock(
8321 DockPosition::Bottom,
8322 &self.bottom_dock,
8323 window,
8324 cx
8325 ))),
8326
8327 BottomDockLayout::LeftAligned => div()
8328 .flex()
8329 .flex_row()
8330 .h_full()
8331 .child(
8332 div()
8333 .flex()
8334 .flex_col()
8335 .flex_1()
8336 .h_full()
8337 .child(
8338 div()
8339 .flex()
8340 .flex_row()
8341 .flex_1()
8342 .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx))
8343
8344 .child(
8345 div()
8346 .flex()
8347 .flex_col()
8348 .flex_1()
8349 .overflow_hidden()
8350 .child(
8351 h_flex()
8352 .flex_1()
8353 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
8354 .child(self.center.render(
8355 self.zoomed.as_ref(),
8356 &PaneRenderContext {
8357 follower_states:
8358 &self.follower_states,
8359 active_call: self.active_call(),
8360 active_pane: &self.active_pane,
8361 app_state: &self.app_state,
8362 project: &self.project,
8363 workspace: &self.weak_self,
8364 },
8365 window,
8366 cx,
8367 ))
8368 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
8369 )
8370 )
8371
8372 )
8373 .child(
8374 div()
8375 .w_full()
8376 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
8377 ),
8378 )
8379 .children(self.render_dock(
8380 DockPosition::Right,
8381 &self.right_dock,
8382 window,
8383 cx,
8384 )),
8385 BottomDockLayout::RightAligned => div()
8386 .flex()
8387 .flex_row()
8388 .h_full()
8389 .children(self.render_dock(
8390 DockPosition::Left,
8391 &self.left_dock,
8392 window,
8393 cx,
8394 ))
8395
8396 .child(
8397 div()
8398 .flex()
8399 .flex_col()
8400 .flex_1()
8401 .h_full()
8402 .child(
8403 div()
8404 .flex()
8405 .flex_row()
8406 .flex_1()
8407 .child(
8408 div()
8409 .flex()
8410 .flex_col()
8411 .flex_1()
8412 .overflow_hidden()
8413 .child(
8414 h_flex()
8415 .flex_1()
8416 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
8417 .child(self.center.render(
8418 self.zoomed.as_ref(),
8419 &PaneRenderContext {
8420 follower_states:
8421 &self.follower_states,
8422 active_call: self.active_call(),
8423 active_pane: &self.active_pane,
8424 app_state: &self.app_state,
8425 project: &self.project,
8426 workspace: &self.weak_self,
8427 },
8428 window,
8429 cx,
8430 ))
8431 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
8432 )
8433 )
8434
8435 .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx))
8436 )
8437 .child(
8438 div()
8439 .w_full()
8440 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
8441 ),
8442 ),
8443 BottomDockLayout::Contained => div()
8444 .flex()
8445 .flex_row()
8446 .h_full()
8447 .children(self.render_dock(
8448 DockPosition::Left,
8449 &self.left_dock,
8450 window,
8451 cx,
8452 ))
8453
8454 .child(
8455 div()
8456 .flex()
8457 .flex_col()
8458 .flex_1()
8459 .overflow_hidden()
8460 .child(
8461 h_flex()
8462 .flex_1()
8463 .when_some(paddings.0, |this, p| {
8464 this.child(p.border_r_1())
8465 })
8466 .child(self.center.render(
8467 self.zoomed.as_ref(),
8468 &PaneRenderContext {
8469 follower_states:
8470 &self.follower_states,
8471 active_call: self.active_call(),
8472 active_pane: &self.active_pane,
8473 app_state: &self.app_state,
8474 project: &self.project,
8475 workspace: &self.weak_self,
8476 },
8477 window,
8478 cx,
8479 ))
8480 .when_some(paddings.1, |this, p| {
8481 this.child(p.border_l_1())
8482 }),
8483 )
8484 .children(self.render_dock(
8485 DockPosition::Bottom,
8486 &self.bottom_dock,
8487 window,
8488 cx,
8489 )),
8490 )
8491
8492 .children(self.render_dock(
8493 DockPosition::Right,
8494 &self.right_dock,
8495 window,
8496 cx,
8497 )),
8498 }
8499 })
8500 .children(self.zoomed.as_ref().and_then(|view| {
8501 let zoomed_view = view.upgrade()?;
8502 let div = div()
8503 .occlude()
8504 .absolute()
8505 .overflow_hidden()
8506 .border_color(colors.border)
8507 .bg(colors.background)
8508 .child(zoomed_view)
8509 .inset_0()
8510 .shadow_lg();
8511
8512 if !WorkspaceSettings::get_global(cx).zoomed_padding {
8513 return Some(div);
8514 }
8515
8516 Some(match self.zoomed_position {
8517 Some(DockPosition::Left) => div.right_2().border_r_1(),
8518 Some(DockPosition::Right) => div.left_2().border_l_1(),
8519 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
8520 None => {
8521 div.top_2().bottom_2().left_2().right_2().border_1()
8522 }
8523 })
8524 }))
8525 .children(self.render_notifications(window, cx)),
8526 )
8527 .when(self.status_bar_visible(cx), |parent| {
8528 parent.child(self.status_bar.clone())
8529 })
8530 .child(self.toast_layer.clone()),
8531 )
8532 }
8533}
8534
8535impl WorkspaceStore {
8536 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
8537 Self {
8538 workspaces: Default::default(),
8539 _subscriptions: vec![
8540 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
8541 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
8542 ],
8543 client,
8544 }
8545 }
8546
8547 pub fn update_followers(
8548 &self,
8549 project_id: Option<u64>,
8550 update: proto::update_followers::Variant,
8551 cx: &App,
8552 ) -> Option<()> {
8553 let active_call = GlobalAnyActiveCall::try_global(cx)?;
8554 let room_id = active_call.0.room_id(cx)?;
8555 self.client
8556 .send(proto::UpdateFollowers {
8557 room_id,
8558 project_id,
8559 variant: Some(update),
8560 })
8561 .log_err()
8562 }
8563
8564 pub async fn handle_follow(
8565 this: Entity<Self>,
8566 envelope: TypedEnvelope<proto::Follow>,
8567 mut cx: AsyncApp,
8568 ) -> Result<proto::FollowResponse> {
8569 this.update(&mut cx, |this, cx| {
8570 let follower = Follower {
8571 project_id: envelope.payload.project_id,
8572 peer_id: envelope.original_sender_id()?,
8573 };
8574
8575 let mut response = proto::FollowResponse::default();
8576
8577 this.workspaces.retain(|(window_handle, weak_workspace)| {
8578 let Some(workspace) = weak_workspace.upgrade() else {
8579 return false;
8580 };
8581 window_handle
8582 .update(cx, |_, window, cx| {
8583 workspace.update(cx, |workspace, cx| {
8584 let handler_response =
8585 workspace.handle_follow(follower.project_id, window, cx);
8586 if let Some(active_view) = handler_response.active_view
8587 && workspace.project.read(cx).remote_id() == follower.project_id
8588 {
8589 response.active_view = Some(active_view)
8590 }
8591 });
8592 })
8593 .is_ok()
8594 });
8595
8596 Ok(response)
8597 })
8598 }
8599
8600 async fn handle_update_followers(
8601 this: Entity<Self>,
8602 envelope: TypedEnvelope<proto::UpdateFollowers>,
8603 mut cx: AsyncApp,
8604 ) -> Result<()> {
8605 let leader_id = envelope.original_sender_id()?;
8606 let update = envelope.payload;
8607
8608 this.update(&mut cx, |this, cx| {
8609 this.workspaces.retain(|(window_handle, weak_workspace)| {
8610 let Some(workspace) = weak_workspace.upgrade() else {
8611 return false;
8612 };
8613 window_handle
8614 .update(cx, |_, window, cx| {
8615 workspace.update(cx, |workspace, cx| {
8616 let project_id = workspace.project.read(cx).remote_id();
8617 if update.project_id != project_id && update.project_id.is_some() {
8618 return;
8619 }
8620 workspace.handle_update_followers(
8621 leader_id,
8622 update.clone(),
8623 window,
8624 cx,
8625 );
8626 });
8627 })
8628 .is_ok()
8629 });
8630 Ok(())
8631 })
8632 }
8633
8634 pub fn workspaces(&self) -> impl Iterator<Item = &WeakEntity<Workspace>> {
8635 self.workspaces.iter().map(|(_, weak)| weak)
8636 }
8637
8638 pub fn workspaces_with_windows(
8639 &self,
8640 ) -> impl Iterator<Item = (gpui::AnyWindowHandle, &WeakEntity<Workspace>)> {
8641 self.workspaces.iter().map(|(window, weak)| (*window, weak))
8642 }
8643}
8644
8645impl ViewId {
8646 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
8647 Ok(Self {
8648 creator: message
8649 .creator
8650 .map(CollaboratorId::PeerId)
8651 .context("creator is missing")?,
8652 id: message.id,
8653 })
8654 }
8655
8656 pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
8657 if let CollaboratorId::PeerId(peer_id) = self.creator {
8658 Some(proto::ViewId {
8659 creator: Some(peer_id),
8660 id: self.id,
8661 })
8662 } else {
8663 None
8664 }
8665 }
8666}
8667
8668impl FollowerState {
8669 fn pane(&self) -> &Entity<Pane> {
8670 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
8671 }
8672}
8673
8674pub trait WorkspaceHandle {
8675 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
8676}
8677
8678impl WorkspaceHandle for Entity<Workspace> {
8679 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
8680 self.read(cx)
8681 .worktrees(cx)
8682 .flat_map(|worktree| {
8683 let worktree_id = worktree.read(cx).id();
8684 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
8685 worktree_id,
8686 path: f.path.clone(),
8687 })
8688 })
8689 .collect::<Vec<_>>()
8690 }
8691}
8692
8693pub async fn last_opened_workspace_location(
8694 db: &WorkspaceDb,
8695 fs: &dyn fs::Fs,
8696) -> Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)> {
8697 db.last_workspace(fs)
8698 .await
8699 .log_err()
8700 .flatten()
8701 .map(|(id, location, paths, _timestamp)| (id, location, paths))
8702}
8703
8704pub async fn last_session_workspace_locations(
8705 db: &WorkspaceDb,
8706 last_session_id: &str,
8707 last_session_window_stack: Option<Vec<WindowId>>,
8708 fs: &dyn fs::Fs,
8709) -> Option<Vec<SessionWorkspace>> {
8710 db.last_session_workspace_locations(last_session_id, last_session_window_stack, fs)
8711 .await
8712 .log_err()
8713}
8714
8715pub async fn restore_multiworkspace(
8716 multi_workspace: SerializedMultiWorkspace,
8717 app_state: Arc<AppState>,
8718 cx: &mut AsyncApp,
8719) -> anyhow::Result<WindowHandle<MultiWorkspace>> {
8720 let SerializedMultiWorkspace {
8721 active_workspace,
8722 state,
8723 } = multi_workspace;
8724
8725 let workspace_result = if active_workspace.paths.is_empty() {
8726 cx.update(|cx| {
8727 open_workspace_by_id(active_workspace.workspace_id, app_state.clone(), None, cx)
8728 })
8729 .await
8730 } else {
8731 cx.update(|cx| {
8732 Workspace::new_local(
8733 active_workspace.paths.paths().to_vec(),
8734 app_state.clone(),
8735 None,
8736 None,
8737 None,
8738 OpenMode::Activate,
8739 cx,
8740 )
8741 })
8742 .await
8743 .map(|result| result.window)
8744 };
8745
8746 let window_handle = match workspace_result {
8747 Ok(handle) => handle,
8748 Err(err) => {
8749 log::error!("Failed to restore active workspace: {err:#}");
8750
8751 let mut fallback_handle = None;
8752 for key in &state.project_group_keys {
8753 let key: ProjectGroupKey = key.clone().into();
8754 let paths = key.path_list().paths().to_vec();
8755 match cx
8756 .update(|cx| {
8757 Workspace::new_local(
8758 paths,
8759 app_state.clone(),
8760 None,
8761 None,
8762 None,
8763 OpenMode::Activate,
8764 cx,
8765 )
8766 })
8767 .await
8768 {
8769 Ok(OpenResult { window, .. }) => {
8770 fallback_handle = Some(window);
8771 break;
8772 }
8773 Err(fallback_err) => {
8774 log::error!("Fallback project group also failed: {fallback_err:#}");
8775 }
8776 }
8777 }
8778
8779 fallback_handle.ok_or(err)?
8780 }
8781 };
8782
8783 apply_restored_multiworkspace_state(window_handle, &state, app_state.fs.clone(), cx).await;
8784
8785 window_handle
8786 .update(cx, |_, window, _cx| {
8787 window.activate_window();
8788 })
8789 .ok();
8790
8791 Ok(window_handle)
8792}
8793
8794pub async fn apply_restored_multiworkspace_state(
8795 window_handle: WindowHandle<MultiWorkspace>,
8796 state: &MultiWorkspaceState,
8797 fs: Arc<dyn fs::Fs>,
8798 cx: &mut AsyncApp,
8799) {
8800 let MultiWorkspaceState {
8801 sidebar_open,
8802 project_group_keys,
8803 sidebar_state,
8804 ..
8805 } = state;
8806
8807 if !project_group_keys.is_empty() {
8808 // Resolve linked worktree paths to their main repo paths so
8809 // stale keys from previous sessions get normalized and deduped.
8810 let mut resolved_keys: Vec<ProjectGroupKey> = Vec::new();
8811 for key in project_group_keys
8812 .iter()
8813 .cloned()
8814 .map(ProjectGroupKey::from)
8815 {
8816 if key.path_list().paths().is_empty() {
8817 continue;
8818 }
8819 let mut resolved_paths = Vec::new();
8820 for path in key.path_list().paths() {
8821 if key.host().is_none()
8822 && let Some(common_dir) =
8823 project::discover_root_repo_common_dir(path, fs.as_ref()).await
8824 {
8825 let main_path = common_dir.parent().unwrap_or(&common_dir);
8826 resolved_paths.push(main_path.to_path_buf());
8827 } else {
8828 resolved_paths.push(path.to_path_buf());
8829 }
8830 }
8831 let resolved = ProjectGroupKey::new(key.host(), PathList::new(&resolved_paths));
8832 if !resolved_keys.contains(&resolved) {
8833 resolved_keys.push(resolved);
8834 }
8835 }
8836
8837 window_handle
8838 .update(cx, |multi_workspace, _window, _cx| {
8839 multi_workspace.restore_project_group_keys(resolved_keys);
8840 })
8841 .ok();
8842 }
8843
8844 if *sidebar_open {
8845 window_handle
8846 .update(cx, |multi_workspace, _, cx| {
8847 multi_workspace.open_sidebar(cx);
8848 })
8849 .ok();
8850 }
8851
8852 if let Some(sidebar_state) = sidebar_state {
8853 window_handle
8854 .update(cx, |multi_workspace, window, cx| {
8855 if let Some(sidebar) = multi_workspace.sidebar() {
8856 sidebar.restore_serialized_state(sidebar_state, window, cx);
8857 }
8858 multi_workspace.serialize(cx);
8859 })
8860 .ok();
8861 }
8862}
8863
8864actions!(
8865 collab,
8866 [
8867 /// Opens the channel notes for the current call.
8868 ///
8869 /// Use `collab_panel::OpenSelectedChannelNotes` to open the channel notes for the selected
8870 /// channel in the collab panel.
8871 ///
8872 /// If you want to open a specific channel, use `zed::OpenZedUrl` with a channel notes URL -
8873 /// can be copied via "Copy link to section" in the context menu of the channel notes
8874 /// buffer. These URLs look like `https://zed.dev/channel/channel-name-CHANNEL_ID/notes`.
8875 OpenChannelNotes,
8876 /// Mutes your microphone.
8877 Mute,
8878 /// Deafens yourself (mute both microphone and speakers).
8879 Deafen,
8880 /// Leaves the current call.
8881 LeaveCall,
8882 /// Shares the current project with collaborators.
8883 ShareProject,
8884 /// Shares your screen with collaborators.
8885 ScreenShare,
8886 /// Copies the current room name and session id for debugging purposes.
8887 CopyRoomId,
8888 ]
8889);
8890
8891/// Opens the channel notes for a specific channel by its ID.
8892#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
8893#[action(namespace = collab)]
8894#[serde(deny_unknown_fields)]
8895pub struct OpenChannelNotesById {
8896 pub channel_id: u64,
8897}
8898
8899actions!(
8900 zed,
8901 [
8902 /// Opens the Zed log file.
8903 OpenLog,
8904 /// Reveals the Zed log file in the system file manager.
8905 RevealLogInFileManager
8906 ]
8907);
8908
8909async fn join_channel_internal(
8910 channel_id: ChannelId,
8911 app_state: &Arc<AppState>,
8912 requesting_window: Option<WindowHandle<MultiWorkspace>>,
8913 requesting_workspace: Option<WeakEntity<Workspace>>,
8914 active_call: &dyn AnyActiveCall,
8915 cx: &mut AsyncApp,
8916) -> Result<bool> {
8917 let (should_prompt, already_in_channel) = cx.update(|cx| {
8918 if !active_call.is_in_room(cx) {
8919 return (false, false);
8920 }
8921
8922 let already_in_channel = active_call.channel_id(cx) == Some(channel_id);
8923 let should_prompt = active_call.is_sharing_project(cx)
8924 && active_call.has_remote_participants(cx)
8925 && !already_in_channel;
8926 (should_prompt, already_in_channel)
8927 });
8928
8929 if already_in_channel {
8930 let task = cx.update(|cx| {
8931 if let Some((project, host)) = active_call.most_active_project(cx) {
8932 Some(join_in_room_project(project, host, app_state.clone(), cx))
8933 } else {
8934 None
8935 }
8936 });
8937 if let Some(task) = task {
8938 task.await?;
8939 }
8940 return anyhow::Ok(true);
8941 }
8942
8943 if should_prompt {
8944 if let Some(multi_workspace) = requesting_window {
8945 let answer = multi_workspace
8946 .update(cx, |_, window, cx| {
8947 window.prompt(
8948 PromptLevel::Warning,
8949 "Do you want to switch channels?",
8950 Some("Leaving this call will unshare your current project."),
8951 &["Yes, Join Channel", "Cancel"],
8952 cx,
8953 )
8954 })?
8955 .await;
8956
8957 if answer == Ok(1) {
8958 return Ok(false);
8959 }
8960 } else {
8961 return Ok(false);
8962 }
8963 }
8964
8965 let client = cx.update(|cx| active_call.client(cx));
8966
8967 let mut client_status = client.status();
8968
8969 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
8970 'outer: loop {
8971 let Some(status) = client_status.recv().await else {
8972 anyhow::bail!("error connecting");
8973 };
8974
8975 match status {
8976 Status::Connecting
8977 | Status::Authenticating
8978 | Status::Authenticated
8979 | Status::Reconnecting
8980 | Status::Reauthenticating
8981 | Status::Reauthenticated => continue,
8982 Status::Connected { .. } => break 'outer,
8983 Status::SignedOut | Status::AuthenticationError => {
8984 return Err(ErrorCode::SignedOut.into());
8985 }
8986 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
8987 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
8988 return Err(ErrorCode::Disconnected.into());
8989 }
8990 }
8991 }
8992
8993 let joined = cx
8994 .update(|cx| active_call.join_channel(channel_id, cx))
8995 .await?;
8996
8997 if !joined {
8998 return anyhow::Ok(true);
8999 }
9000
9001 cx.update(|cx| active_call.room_update_completed(cx)).await;
9002
9003 let task = cx.update(|cx| {
9004 if let Some((project, host)) = active_call.most_active_project(cx) {
9005 return Some(join_in_room_project(project, host, app_state.clone(), cx));
9006 }
9007
9008 // If you are the first to join a channel, see if you should share your project.
9009 if !active_call.has_remote_participants(cx)
9010 && !active_call.local_participant_is_guest(cx)
9011 && let Some(workspace) = requesting_workspace.as_ref().and_then(|w| w.upgrade())
9012 {
9013 let project = workspace.update(cx, |workspace, cx| {
9014 let project = workspace.project.read(cx);
9015
9016 if !active_call.share_on_join(cx) {
9017 return None;
9018 }
9019
9020 if (project.is_local() || project.is_via_remote_server())
9021 && project.visible_worktrees(cx).any(|tree| {
9022 tree.read(cx)
9023 .root_entry()
9024 .is_some_and(|entry| entry.is_dir())
9025 })
9026 {
9027 Some(workspace.project.clone())
9028 } else {
9029 None
9030 }
9031 });
9032 if let Some(project) = project {
9033 let share_task = active_call.share_project(project, cx);
9034 return Some(cx.spawn(async move |_cx| -> Result<()> {
9035 share_task.await?;
9036 Ok(())
9037 }));
9038 }
9039 }
9040
9041 None
9042 });
9043 if let Some(task) = task {
9044 task.await?;
9045 return anyhow::Ok(true);
9046 }
9047 anyhow::Ok(false)
9048}
9049
9050pub fn join_channel(
9051 channel_id: ChannelId,
9052 app_state: Arc<AppState>,
9053 requesting_window: Option<WindowHandle<MultiWorkspace>>,
9054 requesting_workspace: Option<WeakEntity<Workspace>>,
9055 cx: &mut App,
9056) -> Task<Result<()>> {
9057 let active_call = GlobalAnyActiveCall::global(cx).clone();
9058 cx.spawn(async move |cx| {
9059 let result = join_channel_internal(
9060 channel_id,
9061 &app_state,
9062 requesting_window,
9063 requesting_workspace,
9064 &*active_call.0,
9065 cx,
9066 )
9067 .await;
9068
9069 // join channel succeeded, and opened a window
9070 if matches!(result, Ok(true)) {
9071 return anyhow::Ok(());
9072 }
9073
9074 // find an existing workspace to focus and show call controls
9075 let mut active_window = requesting_window.or_else(|| activate_any_workspace_window(cx));
9076 if active_window.is_none() {
9077 // no open workspaces, make one to show the error in (blergh)
9078 let OpenResult {
9079 window: window_handle,
9080 ..
9081 } = cx
9082 .update(|cx| {
9083 Workspace::new_local(
9084 vec![],
9085 app_state.clone(),
9086 requesting_window,
9087 None,
9088 None,
9089 OpenMode::Activate,
9090 cx,
9091 )
9092 })
9093 .await?;
9094
9095 window_handle
9096 .update(cx, |_, window, _cx| {
9097 window.activate_window();
9098 })
9099 .ok();
9100
9101 if result.is_ok() {
9102 cx.update(|cx| {
9103 cx.dispatch_action(&OpenChannelNotes);
9104 });
9105 }
9106
9107 active_window = Some(window_handle);
9108 }
9109
9110 if let Err(err) = result {
9111 log::error!("failed to join channel: {}", err);
9112 if let Some(active_window) = active_window {
9113 active_window
9114 .update(cx, |_, window, cx| {
9115 let detail: SharedString = match err.error_code() {
9116 ErrorCode::SignedOut => "Please sign in to continue.".into(),
9117 ErrorCode::UpgradeRequired => concat!(
9118 "Your are running an unsupported version of Zed. ",
9119 "Please update to continue."
9120 )
9121 .into(),
9122 ErrorCode::NoSuchChannel => concat!(
9123 "No matching channel was found. ",
9124 "Please check the link and try again."
9125 )
9126 .into(),
9127 ErrorCode::Forbidden => concat!(
9128 "This channel is private, and you do not have access. ",
9129 "Please ask someone to add you and try again."
9130 )
9131 .into(),
9132 ErrorCode::Disconnected => {
9133 "Please check your internet connection and try again.".into()
9134 }
9135 _ => format!("{}\n\nPlease try again.", err).into(),
9136 };
9137 window.prompt(
9138 PromptLevel::Critical,
9139 "Failed to join channel",
9140 Some(&detail),
9141 &["Ok"],
9142 cx,
9143 )
9144 })?
9145 .await
9146 .ok();
9147 }
9148 }
9149
9150 // return ok, we showed the error to the user.
9151 anyhow::Ok(())
9152 })
9153}
9154
9155pub async fn get_any_active_multi_workspace(
9156 app_state: Arc<AppState>,
9157 mut cx: AsyncApp,
9158) -> anyhow::Result<WindowHandle<MultiWorkspace>> {
9159 // find an existing workspace to focus and show call controls
9160 let active_window = activate_any_workspace_window(&mut cx);
9161 if active_window.is_none() {
9162 cx.update(|cx| {
9163 Workspace::new_local(
9164 vec![],
9165 app_state.clone(),
9166 None,
9167 None,
9168 None,
9169 OpenMode::Activate,
9170 cx,
9171 )
9172 })
9173 .await?;
9174 }
9175 activate_any_workspace_window(&mut cx).context("could not open zed")
9176}
9177
9178fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<MultiWorkspace>> {
9179 cx.update(|cx| {
9180 if let Some(workspace_window) = cx
9181 .active_window()
9182 .and_then(|window| window.downcast::<MultiWorkspace>())
9183 {
9184 return Some(workspace_window);
9185 }
9186
9187 for window in cx.windows() {
9188 if let Some(workspace_window) = window.downcast::<MultiWorkspace>() {
9189 workspace_window
9190 .update(cx, |_, window, _| window.activate_window())
9191 .ok();
9192 return Some(workspace_window);
9193 }
9194 }
9195 None
9196 })
9197}
9198
9199pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<MultiWorkspace>> {
9200 workspace_windows_for_location(&SerializedWorkspaceLocation::Local, cx)
9201}
9202
9203pub fn workspace_windows_for_location(
9204 serialized_location: &SerializedWorkspaceLocation,
9205 cx: &App,
9206) -> Vec<WindowHandle<MultiWorkspace>> {
9207 cx.windows()
9208 .into_iter()
9209 .filter_map(|window| window.downcast::<MultiWorkspace>())
9210 .filter(|multi_workspace| {
9211 let same_host = |left: &RemoteConnectionOptions, right: &RemoteConnectionOptions| match (left, right) {
9212 (RemoteConnectionOptions::Ssh(a), RemoteConnectionOptions::Ssh(b)) => {
9213 (&a.host, &a.username, &a.port) == (&b.host, &b.username, &b.port)
9214 }
9215 (RemoteConnectionOptions::Wsl(a), RemoteConnectionOptions::Wsl(b)) => {
9216 // The WSL username is not consistently populated in the workspace location, so ignore it for now.
9217 a.distro_name == b.distro_name
9218 }
9219 (RemoteConnectionOptions::Docker(a), RemoteConnectionOptions::Docker(b)) => {
9220 a.container_id == b.container_id
9221 }
9222 #[cfg(any(test, feature = "test-support"))]
9223 (RemoteConnectionOptions::Mock(a), RemoteConnectionOptions::Mock(b)) => {
9224 a.id == b.id
9225 }
9226 _ => false,
9227 };
9228
9229 multi_workspace.read(cx).is_ok_and(|multi_workspace| {
9230 multi_workspace.workspaces().any(|workspace| {
9231 match workspace.read(cx).workspace_location(cx) {
9232 WorkspaceLocation::Location(location, _) => {
9233 match (&location, serialized_location) {
9234 (
9235 SerializedWorkspaceLocation::Local,
9236 SerializedWorkspaceLocation::Local,
9237 ) => true,
9238 (
9239 SerializedWorkspaceLocation::Remote(a),
9240 SerializedWorkspaceLocation::Remote(b),
9241 ) => same_host(a, b),
9242 _ => false,
9243 }
9244 }
9245 _ => false,
9246 }
9247 })
9248 })
9249 })
9250 .collect()
9251}
9252
9253pub async fn find_existing_workspace(
9254 abs_paths: &[PathBuf],
9255 open_options: &OpenOptions,
9256 location: &SerializedWorkspaceLocation,
9257 cx: &mut AsyncApp,
9258) -> (
9259 Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)>,
9260 OpenVisible,
9261) {
9262 let mut existing: Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)> = None;
9263 let mut open_visible = OpenVisible::All;
9264 let mut best_match = None;
9265
9266 cx.update(|cx| {
9267 for window in workspace_windows_for_location(location, cx) {
9268 if let Ok(multi_workspace) = window.read(cx) {
9269 for workspace in multi_workspace.workspaces() {
9270 let project = workspace.read(cx).project.read(cx);
9271 let m = project.visibility_for_paths(
9272 abs_paths,
9273 open_options.open_new_workspace == None,
9274 cx,
9275 );
9276 if m > best_match {
9277 existing = Some((window, workspace.clone()));
9278 best_match = m;
9279 } else if best_match.is_none() && open_options.open_new_workspace == Some(false)
9280 {
9281 existing = Some((window, workspace.clone()))
9282 }
9283 }
9284 }
9285 }
9286 });
9287
9288 // With -n, only reuse a window if the path is genuinely contained
9289 // within an existing worktree (don't fall back to any arbitrary window).
9290 if open_options.open_new_workspace == Some(true) && best_match.is_none() {
9291 existing = None;
9292 }
9293
9294 if open_options.open_new_workspace != Some(true) {
9295 let all_paths_are_files = existing
9296 .as_ref()
9297 .and_then(|(_, target_workspace)| {
9298 cx.update(|cx| {
9299 let workspace = target_workspace.read(cx);
9300 let project = workspace.project.read(cx);
9301 let path_style = workspace.path_style(cx);
9302 Some(!abs_paths.iter().any(|path| {
9303 let path = util::paths::SanitizedPath::new(path);
9304 project.worktrees(cx).any(|worktree| {
9305 let worktree = worktree.read(cx);
9306 let abs_path = worktree.abs_path();
9307 path_style
9308 .strip_prefix(path.as_ref(), abs_path.as_ref())
9309 .and_then(|rel| worktree.entry_for_path(&rel))
9310 .is_some_and(|e| e.is_dir())
9311 })
9312 }))
9313 })
9314 })
9315 .unwrap_or(false);
9316
9317 if open_options.open_new_workspace.is_none()
9318 && existing.is_some()
9319 && open_options.wait
9320 && all_paths_are_files
9321 {
9322 cx.update(|cx| {
9323 let windows = workspace_windows_for_location(location, cx);
9324 let window = cx
9325 .active_window()
9326 .and_then(|window| window.downcast::<MultiWorkspace>())
9327 .filter(|window| windows.contains(window))
9328 .or_else(|| windows.into_iter().next());
9329 if let Some(window) = window {
9330 if let Ok(multi_workspace) = window.read(cx) {
9331 let active_workspace = multi_workspace.workspace().clone();
9332 existing = Some((window, active_workspace));
9333 open_visible = OpenVisible::None;
9334 }
9335 }
9336 });
9337 }
9338 }
9339 (existing, open_visible)
9340}
9341
9342#[derive(Default, Clone)]
9343pub struct OpenOptions {
9344 pub visible: Option<OpenVisible>,
9345 pub focus: Option<bool>,
9346 pub open_new_workspace: Option<bool>,
9347 pub wait: bool,
9348 pub requesting_window: Option<WindowHandle<MultiWorkspace>>,
9349 pub open_mode: OpenMode,
9350 pub env: Option<HashMap<String, String>>,
9351 pub open_in_dev_container: bool,
9352}
9353
9354/// The result of opening a workspace via [`open_paths`], [`Workspace::new_local`],
9355/// or [`Workspace::open_workspace_for_paths`].
9356pub struct OpenResult {
9357 pub window: WindowHandle<MultiWorkspace>,
9358 pub workspace: Entity<Workspace>,
9359 pub opened_items: Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
9360}
9361
9362/// Opens a workspace by its database ID, used for restoring empty workspaces with unsaved content.
9363pub fn open_workspace_by_id(
9364 workspace_id: WorkspaceId,
9365 app_state: Arc<AppState>,
9366 requesting_window: Option<WindowHandle<MultiWorkspace>>,
9367 cx: &mut App,
9368) -> Task<anyhow::Result<WindowHandle<MultiWorkspace>>> {
9369 let project_handle = Project::local(
9370 app_state.client.clone(),
9371 app_state.node_runtime.clone(),
9372 app_state.user_store.clone(),
9373 app_state.languages.clone(),
9374 app_state.fs.clone(),
9375 None,
9376 project::LocalProjectFlags {
9377 init_worktree_trust: true,
9378 ..project::LocalProjectFlags::default()
9379 },
9380 cx,
9381 );
9382
9383 let db = WorkspaceDb::global(cx);
9384 let kvp = db::kvp::KeyValueStore::global(cx);
9385 cx.spawn(async move |cx| {
9386 let serialized_workspace = db
9387 .workspace_for_id(workspace_id)
9388 .with_context(|| format!("Workspace {workspace_id:?} not found"))?;
9389
9390 let centered_layout = serialized_workspace.centered_layout;
9391
9392 let (window, workspace) = if let Some(window) = requesting_window {
9393 let workspace = window.update(cx, |multi_workspace, window, cx| {
9394 let workspace = cx.new(|cx| {
9395 let mut workspace = Workspace::new(
9396 Some(workspace_id),
9397 project_handle.clone(),
9398 app_state.clone(),
9399 window,
9400 cx,
9401 );
9402 workspace.centered_layout = centered_layout;
9403 workspace
9404 });
9405 multi_workspace.add(workspace.clone(), &*window, cx);
9406 workspace
9407 })?;
9408 (window, workspace)
9409 } else {
9410 let window_bounds_override = window_bounds_env_override();
9411
9412 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
9413 (Some(WindowBounds::Windowed(bounds)), None)
9414 } else if let Some(display) = serialized_workspace.display
9415 && let Some(bounds) = serialized_workspace.window_bounds.as_ref()
9416 {
9417 (Some(bounds.0), Some(display))
9418 } else if let Some((display, bounds)) = persistence::read_default_window_bounds(&kvp) {
9419 (Some(bounds), Some(display))
9420 } else {
9421 (None, None)
9422 };
9423
9424 let options = cx.update(|cx| {
9425 let mut options = (app_state.build_window_options)(display, cx);
9426 options.window_bounds = window_bounds;
9427 options
9428 });
9429
9430 let window = cx.open_window(options, {
9431 let app_state = app_state.clone();
9432 let project_handle = project_handle.clone();
9433 move |window, cx| {
9434 let workspace = cx.new(|cx| {
9435 let mut workspace = Workspace::new(
9436 Some(workspace_id),
9437 project_handle,
9438 app_state,
9439 window,
9440 cx,
9441 );
9442 workspace.centered_layout = centered_layout;
9443 workspace
9444 });
9445 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
9446 }
9447 })?;
9448
9449 let workspace = window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
9450 multi_workspace.workspace().clone()
9451 })?;
9452
9453 (window, workspace)
9454 };
9455
9456 notify_if_database_failed(window, cx);
9457
9458 // Restore items from the serialized workspace
9459 window
9460 .update(cx, |_, window, cx| {
9461 workspace.update(cx, |_workspace, cx| {
9462 open_items(Some(serialized_workspace), vec![], window, cx)
9463 })
9464 })?
9465 .await?;
9466
9467 window.update(cx, |_, window, cx| {
9468 workspace.update(cx, |workspace, cx| {
9469 workspace.serialize_workspace(window, cx);
9470 });
9471 })?;
9472
9473 Ok(window)
9474 })
9475}
9476
9477#[allow(clippy::type_complexity)]
9478pub fn open_paths(
9479 abs_paths: &[PathBuf],
9480 app_state: Arc<AppState>,
9481 mut open_options: OpenOptions,
9482 cx: &mut App,
9483) -> Task<anyhow::Result<OpenResult>> {
9484 let abs_paths = abs_paths.to_vec();
9485 #[cfg(target_os = "windows")]
9486 let wsl_path = abs_paths
9487 .iter()
9488 .find_map(|p| util::paths::WslPath::from_path(p));
9489
9490 cx.spawn(async move |cx| {
9491 let (mut existing, mut open_visible) = find_existing_workspace(
9492 &abs_paths,
9493 &open_options,
9494 &SerializedWorkspaceLocation::Local,
9495 cx,
9496 )
9497 .await;
9498
9499 // Fallback: if no workspace contains the paths and all paths are files,
9500 // prefer an existing local workspace window (active window first).
9501 if open_options.open_new_workspace.is_none() && existing.is_none() {
9502 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
9503 let all_metadatas = futures::future::join_all(all_paths)
9504 .await
9505 .into_iter()
9506 .filter_map(|result| result.ok().flatten());
9507
9508 if all_metadatas.into_iter().all(|file| !file.is_dir) {
9509 cx.update(|cx| {
9510 let windows = workspace_windows_for_location(
9511 &SerializedWorkspaceLocation::Local,
9512 cx,
9513 );
9514 let window = cx
9515 .active_window()
9516 .and_then(|window| window.downcast::<MultiWorkspace>())
9517 .filter(|window| windows.contains(window))
9518 .or_else(|| windows.into_iter().next());
9519 if let Some(window) = window {
9520 if let Ok(multi_workspace) = window.read(cx) {
9521 let active_workspace = multi_workspace.workspace().clone();
9522 existing = Some((window, active_workspace));
9523 open_visible = OpenVisible::None;
9524 }
9525 }
9526 });
9527 }
9528 }
9529
9530 // Fallback for directories: when no flag is specified and no existing
9531 // workspace matched, add the directory as a new workspace in the
9532 // active window's MultiWorkspace (instead of opening a new window).
9533 if open_options.open_new_workspace.is_none() && existing.is_none() {
9534 let target_window = cx.update(|cx| {
9535 let windows = workspace_windows_for_location(
9536 &SerializedWorkspaceLocation::Local,
9537 cx,
9538 );
9539 let window = cx
9540 .active_window()
9541 .and_then(|window| window.downcast::<MultiWorkspace>())
9542 .filter(|window| windows.contains(window))
9543 .or_else(|| windows.into_iter().next());
9544 window.filter(|window| {
9545 window.read(cx).is_ok_and(|mw| mw.multi_workspace_enabled(cx))
9546 })
9547 });
9548
9549 if let Some(window) = target_window {
9550 open_options.requesting_window = Some(window);
9551 window
9552 .update(cx, |multi_workspace, _, cx| {
9553 multi_workspace.open_sidebar(cx);
9554 })
9555 .log_err();
9556 }
9557 }
9558
9559 let open_in_dev_container = open_options.open_in_dev_container;
9560
9561 let result = if let Some((existing, target_workspace)) = existing {
9562 let open_task = existing
9563 .update(cx, |multi_workspace, window, cx| {
9564 window.activate_window();
9565 multi_workspace.activate(target_workspace.clone(), window, cx);
9566 target_workspace.update(cx, |workspace, cx| {
9567 if open_in_dev_container {
9568 workspace.set_open_in_dev_container(true);
9569 }
9570 workspace.open_paths(
9571 abs_paths,
9572 OpenOptions {
9573 visible: Some(open_visible),
9574 ..Default::default()
9575 },
9576 None,
9577 window,
9578 cx,
9579 )
9580 })
9581 })?
9582 .await;
9583
9584 _ = existing.update(cx, |multi_workspace, _, cx| {
9585 let workspace = multi_workspace.workspace().clone();
9586 workspace.update(cx, |workspace, cx| {
9587 for item in open_task.iter().flatten() {
9588 if let Err(e) = item {
9589 workspace.show_error(&e, cx);
9590 }
9591 }
9592 });
9593 });
9594
9595 Ok(OpenResult { window: existing, workspace: target_workspace, opened_items: open_task })
9596 } else {
9597 let init = if open_in_dev_container {
9598 Some(Box::new(|workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context<Workspace>| {
9599 workspace.set_open_in_dev_container(true);
9600 }) as Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>)
9601 } else {
9602 None
9603 };
9604 let result = cx
9605 .update(move |cx| {
9606 Workspace::new_local(
9607 abs_paths,
9608 app_state.clone(),
9609 open_options.requesting_window,
9610 open_options.env,
9611 init,
9612 open_options.open_mode,
9613 cx,
9614 )
9615 })
9616 .await;
9617
9618 if let Ok(ref result) = result {
9619 result.window
9620 .update(cx, |_, window, _cx| {
9621 window.activate_window();
9622 })
9623 .log_err();
9624 }
9625
9626 result
9627 };
9628
9629 #[cfg(target_os = "windows")]
9630 if let Some(util::paths::WslPath{distro, path}) = wsl_path
9631 && let Ok(ref result) = result
9632 {
9633 result.window
9634 .update(cx, move |multi_workspace, _window, cx| {
9635 struct OpenInWsl;
9636 let workspace = multi_workspace.workspace().clone();
9637 workspace.update(cx, |workspace, cx| {
9638 workspace.show_notification(NotificationId::unique::<OpenInWsl>(), cx, move |cx| {
9639 let display_path = util::markdown::MarkdownInlineCode(&path.to_string_lossy());
9640 let msg = format!("{display_path} is inside a WSL filesystem, some features may not work unless you open it with WSL remote");
9641 cx.new(move |cx| {
9642 MessageNotification::new(msg, cx)
9643 .primary_message("Open in WSL")
9644 .primary_icon(IconName::FolderOpen)
9645 .primary_on_click(move |window, cx| {
9646 window.dispatch_action(Box::new(remote::OpenWslPath {
9647 distro: remote::WslConnectionOptions {
9648 distro_name: distro.clone(),
9649 user: None,
9650 },
9651 paths: vec![path.clone().into()],
9652 }), cx)
9653 })
9654 })
9655 });
9656 });
9657 })
9658 .unwrap();
9659 };
9660 result
9661 })
9662}
9663
9664pub fn open_new(
9665 open_options: OpenOptions,
9666 app_state: Arc<AppState>,
9667 cx: &mut App,
9668 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
9669) -> Task<anyhow::Result<()>> {
9670 let addition = open_options.open_mode;
9671 let task = Workspace::new_local(
9672 Vec::new(),
9673 app_state,
9674 open_options.requesting_window,
9675 open_options.env,
9676 Some(Box::new(init)),
9677 addition,
9678 cx,
9679 );
9680 cx.spawn(async move |cx| {
9681 let OpenResult { window, .. } = task.await?;
9682 window
9683 .update(cx, |_, window, _cx| {
9684 window.activate_window();
9685 })
9686 .ok();
9687 Ok(())
9688 })
9689}
9690
9691pub fn create_and_open_local_file(
9692 path: &'static Path,
9693 window: &mut Window,
9694 cx: &mut Context<Workspace>,
9695 default_content: impl 'static + Send + FnOnce() -> Rope,
9696) -> Task<Result<Box<dyn ItemHandle>>> {
9697 cx.spawn_in(window, async move |workspace, cx| {
9698 let fs = workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
9699 if !fs.is_file(path).await {
9700 fs.create_file(path, Default::default()).await?;
9701 fs.save(path, &default_content(), Default::default())
9702 .await?;
9703 }
9704
9705 workspace
9706 .update_in(cx, |workspace, window, cx| {
9707 workspace.with_local_or_wsl_workspace(window, cx, |workspace, window, cx| {
9708 let path = workspace
9709 .project
9710 .read_with(cx, |project, cx| project.try_windows_path_to_wsl(path, cx));
9711 cx.spawn_in(window, async move |workspace, cx| {
9712 let path = path.await?;
9713
9714 let path = fs.canonicalize(&path).await.unwrap_or(path);
9715
9716 let mut items = workspace
9717 .update_in(cx, |workspace, window, cx| {
9718 workspace.open_paths(
9719 vec![path.to_path_buf()],
9720 OpenOptions {
9721 visible: Some(OpenVisible::None),
9722 ..Default::default()
9723 },
9724 None,
9725 window,
9726 cx,
9727 )
9728 })?
9729 .await;
9730 let item = items.pop().flatten();
9731 item.with_context(|| format!("path {path:?} is not a file"))?
9732 })
9733 })
9734 })?
9735 .await?
9736 .await
9737 })
9738}
9739
9740pub fn open_remote_project_with_new_connection(
9741 window: WindowHandle<MultiWorkspace>,
9742 remote_connection: Arc<dyn RemoteConnection>,
9743 cancel_rx: oneshot::Receiver<()>,
9744 delegate: Arc<dyn RemoteClientDelegate>,
9745 app_state: Arc<AppState>,
9746 paths: Vec<PathBuf>,
9747 cx: &mut App,
9748) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
9749 cx.spawn(async move |cx| {
9750 let (workspace_id, serialized_workspace) =
9751 deserialize_remote_project(remote_connection.connection_options(), paths.clone(), cx)
9752 .await?;
9753
9754 let session = match cx
9755 .update(|cx| {
9756 remote::RemoteClient::new(
9757 ConnectionIdentifier::Workspace(workspace_id.0),
9758 remote_connection,
9759 cancel_rx,
9760 delegate,
9761 cx,
9762 )
9763 })
9764 .await?
9765 {
9766 Some(result) => result,
9767 None => return Ok(Vec::new()),
9768 };
9769
9770 let project = cx.update(|cx| {
9771 project::Project::remote(
9772 session,
9773 app_state.client.clone(),
9774 app_state.node_runtime.clone(),
9775 app_state.user_store.clone(),
9776 app_state.languages.clone(),
9777 app_state.fs.clone(),
9778 true,
9779 cx,
9780 )
9781 });
9782
9783 open_remote_project_inner(
9784 project,
9785 paths,
9786 workspace_id,
9787 serialized_workspace,
9788 app_state,
9789 window,
9790 None,
9791 cx,
9792 )
9793 .await
9794 })
9795}
9796
9797pub fn open_remote_project_with_existing_connection(
9798 connection_options: RemoteConnectionOptions,
9799 project: Entity<Project>,
9800 paths: Vec<PathBuf>,
9801 app_state: Arc<AppState>,
9802 window: WindowHandle<MultiWorkspace>,
9803 provisional_project_group_key: Option<ProjectGroupKey>,
9804 cx: &mut AsyncApp,
9805) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
9806 cx.spawn(async move |cx| {
9807 let (workspace_id, serialized_workspace) =
9808 deserialize_remote_project(connection_options.clone(), paths.clone(), cx).await?;
9809
9810 open_remote_project_inner(
9811 project,
9812 paths,
9813 workspace_id,
9814 serialized_workspace,
9815 app_state,
9816 window,
9817 provisional_project_group_key,
9818 cx,
9819 )
9820 .await
9821 })
9822}
9823
9824async fn open_remote_project_inner(
9825 project: Entity<Project>,
9826 paths: Vec<PathBuf>,
9827 workspace_id: WorkspaceId,
9828 serialized_workspace: Option<SerializedWorkspace>,
9829 app_state: Arc<AppState>,
9830 window: WindowHandle<MultiWorkspace>,
9831 provisional_project_group_key: Option<ProjectGroupKey>,
9832 cx: &mut AsyncApp,
9833) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
9834 let db = cx.update(|cx| WorkspaceDb::global(cx));
9835 let toolchains = db.toolchains(workspace_id).await?;
9836 for (toolchain, worktree_path, path) in toolchains {
9837 project
9838 .update(cx, |this, cx| {
9839 let Some(worktree_id) =
9840 this.find_worktree(&worktree_path, cx)
9841 .and_then(|(worktree, rel_path)| {
9842 if rel_path.is_empty() {
9843 Some(worktree.read(cx).id())
9844 } else {
9845 None
9846 }
9847 })
9848 else {
9849 return Task::ready(None);
9850 };
9851
9852 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
9853 })
9854 .await;
9855 }
9856 let mut project_paths_to_open = vec![];
9857 let mut project_path_errors = vec![];
9858
9859 for path in paths {
9860 let result = cx
9861 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))
9862 .await;
9863 match result {
9864 Ok((_, project_path)) => {
9865 project_paths_to_open.push((path.clone(), Some(project_path)));
9866 }
9867 Err(error) => {
9868 project_path_errors.push(error);
9869 }
9870 };
9871 }
9872
9873 if project_paths_to_open.is_empty() {
9874 return Err(project_path_errors.pop().context("no paths given")?);
9875 }
9876
9877 let workspace = window.update(cx, |multi_workspace, window, cx| {
9878 telemetry::event!("SSH Project Opened");
9879
9880 let new_workspace = cx.new(|cx| {
9881 let mut workspace =
9882 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
9883 workspace.update_history(cx);
9884
9885 if let Some(ref serialized) = serialized_workspace {
9886 workspace.centered_layout = serialized.centered_layout;
9887 }
9888
9889 workspace
9890 });
9891
9892 if let Some(project_group_key) = provisional_project_group_key.clone() {
9893 multi_workspace.set_provisional_project_group_key(&new_workspace, project_group_key);
9894 }
9895 multi_workspace.activate(new_workspace.clone(), window, cx);
9896 new_workspace
9897 })?;
9898
9899 let items = window
9900 .update(cx, |_, window, cx| {
9901 window.activate_window();
9902 workspace.update(cx, |_workspace, cx| {
9903 open_items(serialized_workspace, project_paths_to_open, window, cx)
9904 })
9905 })?
9906 .await?;
9907
9908 workspace.update(cx, |workspace, cx| {
9909 for error in project_path_errors {
9910 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
9911 if let Some(path) = error.error_tag("path") {
9912 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
9913 }
9914 } else {
9915 workspace.show_error(&error, cx)
9916 }
9917 }
9918 });
9919
9920 Ok(items.into_iter().map(|item| item?.ok()).collect())
9921}
9922
9923fn deserialize_remote_project(
9924 connection_options: RemoteConnectionOptions,
9925 paths: Vec<PathBuf>,
9926 cx: &AsyncApp,
9927) -> Task<Result<(WorkspaceId, Option<SerializedWorkspace>)>> {
9928 let db = cx.update(|cx| WorkspaceDb::global(cx));
9929 cx.background_spawn(async move {
9930 let remote_connection_id = db
9931 .get_or_create_remote_connection(connection_options)
9932 .await?;
9933
9934 let serialized_workspace = db.remote_workspace_for_roots(&paths, remote_connection_id);
9935
9936 let workspace_id = if let Some(workspace_id) =
9937 serialized_workspace.as_ref().map(|workspace| workspace.id)
9938 {
9939 workspace_id
9940 } else {
9941 db.next_id().await?
9942 };
9943
9944 Ok((workspace_id, serialized_workspace))
9945 })
9946}
9947
9948pub fn join_in_room_project(
9949 project_id: u64,
9950 follow_user_id: u64,
9951 app_state: Arc<AppState>,
9952 cx: &mut App,
9953) -> Task<Result<()>> {
9954 let windows = cx.windows();
9955 cx.spawn(async move |cx| {
9956 let existing_window_and_workspace: Option<(
9957 WindowHandle<MultiWorkspace>,
9958 Entity<Workspace>,
9959 )> = windows.into_iter().find_map(|window_handle| {
9960 window_handle
9961 .downcast::<MultiWorkspace>()
9962 .and_then(|window_handle| {
9963 window_handle
9964 .update(cx, |multi_workspace, _window, cx| {
9965 for workspace in multi_workspace.workspaces() {
9966 if workspace.read(cx).project().read(cx).remote_id()
9967 == Some(project_id)
9968 {
9969 return Some((window_handle, workspace.clone()));
9970 }
9971 }
9972 None
9973 })
9974 .unwrap_or(None)
9975 })
9976 });
9977
9978 let multi_workspace_window = if let Some((existing_window, target_workspace)) =
9979 existing_window_and_workspace
9980 {
9981 existing_window
9982 .update(cx, |multi_workspace, window, cx| {
9983 multi_workspace.activate(target_workspace, window, cx);
9984 })
9985 .ok();
9986 existing_window
9987 } else {
9988 let active_call = cx.update(|cx| GlobalAnyActiveCall::global(cx).clone());
9989 let project = cx
9990 .update(|cx| {
9991 active_call.0.join_project(
9992 project_id,
9993 app_state.languages.clone(),
9994 app_state.fs.clone(),
9995 cx,
9996 )
9997 })
9998 .await?;
9999
10000 let window_bounds_override = window_bounds_env_override();
10001 cx.update(|cx| {
10002 let mut options = (app_state.build_window_options)(None, cx);
10003 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
10004 cx.open_window(options, |window, cx| {
10005 let workspace = cx.new(|cx| {
10006 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
10007 });
10008 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
10009 })
10010 })?
10011 };
10012
10013 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
10014 cx.activate(true);
10015 window.activate_window();
10016
10017 // We set the active workspace above, so this is the correct workspace.
10018 let workspace = multi_workspace.workspace().clone();
10019 workspace.update(cx, |workspace, cx| {
10020 let follow_peer_id = GlobalAnyActiveCall::try_global(cx)
10021 .and_then(|call| call.0.peer_id_for_user_in_room(follow_user_id, cx))
10022 .or_else(|| {
10023 // If we couldn't follow the given user, follow the host instead.
10024 let collaborator = workspace
10025 .project()
10026 .read(cx)
10027 .collaborators()
10028 .values()
10029 .find(|collaborator| collaborator.is_host)?;
10030 Some(collaborator.peer_id)
10031 });
10032
10033 if let Some(follow_peer_id) = follow_peer_id {
10034 workspace.follow(follow_peer_id, window, cx);
10035 }
10036 });
10037 })?;
10038
10039 anyhow::Ok(())
10040 })
10041}
10042
10043pub fn reload(cx: &mut App) {
10044 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
10045 let mut workspace_windows = cx
10046 .windows()
10047 .into_iter()
10048 .filter_map(|window| window.downcast::<MultiWorkspace>())
10049 .collect::<Vec<_>>();
10050
10051 // If multiple windows have unsaved changes, and need a save prompt,
10052 // prompt in the active window before switching to a different window.
10053 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
10054
10055 let mut prompt = None;
10056 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
10057 prompt = window
10058 .update(cx, |_, window, cx| {
10059 window.prompt(
10060 PromptLevel::Info,
10061 "Are you sure you want to restart?",
10062 None,
10063 &["Restart", "Cancel"],
10064 cx,
10065 )
10066 })
10067 .ok();
10068 }
10069
10070 cx.spawn(async move |cx| {
10071 if let Some(prompt) = prompt {
10072 let answer = prompt.await?;
10073 if answer != 0 {
10074 return anyhow::Ok(());
10075 }
10076 }
10077
10078 // If the user cancels any save prompt, then keep the app open.
10079 for window in workspace_windows {
10080 if let Ok(should_close) = window.update(cx, |multi_workspace, window, cx| {
10081 let workspace = multi_workspace.workspace().clone();
10082 workspace.update(cx, |workspace, cx| {
10083 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
10084 })
10085 }) && !should_close.await?
10086 {
10087 return anyhow::Ok(());
10088 }
10089 }
10090 cx.update(|cx| cx.restart());
10091 anyhow::Ok(())
10092 })
10093 .detach_and_log_err(cx);
10094}
10095
10096fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
10097 let mut parts = value.split(',');
10098 let x: usize = parts.next()?.parse().ok()?;
10099 let y: usize = parts.next()?.parse().ok()?;
10100 Some(point(px(x as f32), px(y as f32)))
10101}
10102
10103fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
10104 let mut parts = value.split(',');
10105 let width: usize = parts.next()?.parse().ok()?;
10106 let height: usize = parts.next()?.parse().ok()?;
10107 Some(size(px(width as f32), px(height as f32)))
10108}
10109
10110/// Add client-side decorations (rounded corners, shadows, resize handling) when
10111/// appropriate.
10112///
10113/// The `border_radius_tiling` parameter allows overriding which corners get
10114/// rounded, independently of the actual window tiling state. This is used
10115/// specifically for the workspace switcher sidebar: when the sidebar is open,
10116/// we want square corners on the left (so the sidebar appears flush with the
10117/// window edge) but we still need the shadow padding for proper visual
10118/// appearance. Unlike actual window tiling, this only affects border radius -
10119/// not padding or shadows.
10120pub fn client_side_decorations(
10121 element: impl IntoElement,
10122 window: &mut Window,
10123 cx: &mut App,
10124 border_radius_tiling: Tiling,
10125) -> Stateful<Div> {
10126 const BORDER_SIZE: Pixels = px(1.0);
10127 let decorations = window.window_decorations();
10128 let tiling = match decorations {
10129 Decorations::Server => Tiling::default(),
10130 Decorations::Client { tiling } => tiling,
10131 };
10132
10133 match decorations {
10134 Decorations::Client { .. } => window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW),
10135 Decorations::Server => window.set_client_inset(px(0.0)),
10136 }
10137
10138 struct GlobalResizeEdge(ResizeEdge);
10139 impl Global for GlobalResizeEdge {}
10140
10141 div()
10142 .id("window-backdrop")
10143 .bg(transparent_black())
10144 .map(|div| match decorations {
10145 Decorations::Server => div,
10146 Decorations::Client { .. } => div
10147 .when(
10148 !(tiling.top
10149 || tiling.right
10150 || border_radius_tiling.top
10151 || border_radius_tiling.right),
10152 |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10153 )
10154 .when(
10155 !(tiling.top
10156 || tiling.left
10157 || border_radius_tiling.top
10158 || border_radius_tiling.left),
10159 |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10160 )
10161 .when(
10162 !(tiling.bottom
10163 || tiling.right
10164 || border_radius_tiling.bottom
10165 || border_radius_tiling.right),
10166 |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10167 )
10168 .when(
10169 !(tiling.bottom
10170 || tiling.left
10171 || border_radius_tiling.bottom
10172 || border_radius_tiling.left),
10173 |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10174 )
10175 .when(!tiling.top, |div| {
10176 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
10177 })
10178 .when(!tiling.bottom, |div| {
10179 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
10180 })
10181 .when(!tiling.left, |div| {
10182 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
10183 })
10184 .when(!tiling.right, |div| {
10185 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
10186 })
10187 .on_mouse_move(move |e, window, cx| {
10188 let size = window.window_bounds().get_bounds().size;
10189 let pos = e.position;
10190
10191 let new_edge =
10192 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
10193
10194 let edge = cx.try_global::<GlobalResizeEdge>();
10195 if new_edge != edge.map(|edge| edge.0) {
10196 window
10197 .window_handle()
10198 .update(cx, |workspace, _, cx| {
10199 cx.notify(workspace.entity_id());
10200 })
10201 .ok();
10202 }
10203 })
10204 .on_mouse_down(MouseButton::Left, move |e, window, _| {
10205 let size = window.window_bounds().get_bounds().size;
10206 let pos = e.position;
10207
10208 let edge = match resize_edge(
10209 pos,
10210 theme::CLIENT_SIDE_DECORATION_SHADOW,
10211 size,
10212 tiling,
10213 ) {
10214 Some(value) => value,
10215 None => return,
10216 };
10217
10218 window.start_window_resize(edge);
10219 }),
10220 })
10221 .size_full()
10222 .child(
10223 div()
10224 .cursor(CursorStyle::Arrow)
10225 .map(|div| match decorations {
10226 Decorations::Server => div,
10227 Decorations::Client { .. } => div
10228 .border_color(cx.theme().colors().border)
10229 .when(
10230 !(tiling.top
10231 || tiling.right
10232 || border_radius_tiling.top
10233 || border_radius_tiling.right),
10234 |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10235 )
10236 .when(
10237 !(tiling.top
10238 || tiling.left
10239 || border_radius_tiling.top
10240 || border_radius_tiling.left),
10241 |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10242 )
10243 .when(
10244 !(tiling.bottom
10245 || tiling.right
10246 || border_radius_tiling.bottom
10247 || border_radius_tiling.right),
10248 |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10249 )
10250 .when(
10251 !(tiling.bottom
10252 || tiling.left
10253 || border_radius_tiling.bottom
10254 || border_radius_tiling.left),
10255 |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10256 )
10257 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
10258 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
10259 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
10260 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
10261 .when(!tiling.is_tiled(), |div| {
10262 div.shadow(vec![gpui::BoxShadow {
10263 color: Hsla {
10264 h: 0.,
10265 s: 0.,
10266 l: 0.,
10267 a: 0.4,
10268 },
10269 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
10270 spread_radius: px(0.),
10271 offset: point(px(0.0), px(0.0)),
10272 }])
10273 }),
10274 })
10275 .on_mouse_move(|_e, _, cx| {
10276 cx.stop_propagation();
10277 })
10278 .size_full()
10279 .child(element),
10280 )
10281 .map(|div| match decorations {
10282 Decorations::Server => div,
10283 Decorations::Client { tiling, .. } => div.child(
10284 canvas(
10285 |_bounds, window, _| {
10286 window.insert_hitbox(
10287 Bounds::new(
10288 point(px(0.0), px(0.0)),
10289 window.window_bounds().get_bounds().size,
10290 ),
10291 HitboxBehavior::Normal,
10292 )
10293 },
10294 move |_bounds, hitbox, window, cx| {
10295 let mouse = window.mouse_position();
10296 let size = window.window_bounds().get_bounds().size;
10297 let Some(edge) =
10298 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
10299 else {
10300 return;
10301 };
10302 cx.set_global(GlobalResizeEdge(edge));
10303 window.set_cursor_style(
10304 match edge {
10305 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
10306 ResizeEdge::Left | ResizeEdge::Right => {
10307 CursorStyle::ResizeLeftRight
10308 }
10309 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
10310 CursorStyle::ResizeUpLeftDownRight
10311 }
10312 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
10313 CursorStyle::ResizeUpRightDownLeft
10314 }
10315 },
10316 &hitbox,
10317 );
10318 },
10319 )
10320 .size_full()
10321 .absolute(),
10322 ),
10323 })
10324}
10325
10326fn resize_edge(
10327 pos: Point<Pixels>,
10328 shadow_size: Pixels,
10329 window_size: Size<Pixels>,
10330 tiling: Tiling,
10331) -> Option<ResizeEdge> {
10332 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
10333 if bounds.contains(&pos) {
10334 return None;
10335 }
10336
10337 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
10338 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
10339 if !tiling.top && top_left_bounds.contains(&pos) {
10340 return Some(ResizeEdge::TopLeft);
10341 }
10342
10343 let top_right_bounds = Bounds::new(
10344 Point::new(window_size.width - corner_size.width, px(0.)),
10345 corner_size,
10346 );
10347 if !tiling.top && top_right_bounds.contains(&pos) {
10348 return Some(ResizeEdge::TopRight);
10349 }
10350
10351 let bottom_left_bounds = Bounds::new(
10352 Point::new(px(0.), window_size.height - corner_size.height),
10353 corner_size,
10354 );
10355 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
10356 return Some(ResizeEdge::BottomLeft);
10357 }
10358
10359 let bottom_right_bounds = Bounds::new(
10360 Point::new(
10361 window_size.width - corner_size.width,
10362 window_size.height - corner_size.height,
10363 ),
10364 corner_size,
10365 );
10366 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
10367 return Some(ResizeEdge::BottomRight);
10368 }
10369
10370 if !tiling.top && pos.y < shadow_size {
10371 Some(ResizeEdge::Top)
10372 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
10373 Some(ResizeEdge::Bottom)
10374 } else if !tiling.left && pos.x < shadow_size {
10375 Some(ResizeEdge::Left)
10376 } else if !tiling.right && pos.x > window_size.width - shadow_size {
10377 Some(ResizeEdge::Right)
10378 } else {
10379 None
10380 }
10381}
10382
10383fn join_pane_into_active(
10384 active_pane: &Entity<Pane>,
10385 pane: &Entity<Pane>,
10386 window: &mut Window,
10387 cx: &mut App,
10388) {
10389 if pane == active_pane {
10390 } else if pane.read(cx).items_len() == 0 {
10391 pane.update(cx, |_, cx| {
10392 cx.emit(pane::Event::Remove {
10393 focus_on_pane: None,
10394 });
10395 })
10396 } else {
10397 move_all_items(pane, active_pane, window, cx);
10398 }
10399}
10400
10401fn move_all_items(
10402 from_pane: &Entity<Pane>,
10403 to_pane: &Entity<Pane>,
10404 window: &mut Window,
10405 cx: &mut App,
10406) {
10407 let destination_is_different = from_pane != to_pane;
10408 let mut moved_items = 0;
10409 for (item_ix, item_handle) in from_pane
10410 .read(cx)
10411 .items()
10412 .enumerate()
10413 .map(|(ix, item)| (ix, item.clone()))
10414 .collect::<Vec<_>>()
10415 {
10416 let ix = item_ix - moved_items;
10417 if destination_is_different {
10418 // Close item from previous pane
10419 from_pane.update(cx, |source, cx| {
10420 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
10421 });
10422 moved_items += 1;
10423 }
10424
10425 // This automatically removes duplicate items in the pane
10426 to_pane.update(cx, |destination, cx| {
10427 destination.add_item(item_handle, true, true, None, window, cx);
10428 window.focus(&destination.focus_handle(cx), cx)
10429 });
10430 }
10431}
10432
10433pub fn move_item(
10434 source: &Entity<Pane>,
10435 destination: &Entity<Pane>,
10436 item_id_to_move: EntityId,
10437 destination_index: usize,
10438 activate: bool,
10439 window: &mut Window,
10440 cx: &mut App,
10441) {
10442 let Some((item_ix, item_handle)) = source
10443 .read(cx)
10444 .items()
10445 .enumerate()
10446 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
10447 .map(|(ix, item)| (ix, item.clone()))
10448 else {
10449 // Tab was closed during drag
10450 return;
10451 };
10452
10453 if source != destination {
10454 // Close item from previous pane
10455 source.update(cx, |source, cx| {
10456 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
10457 });
10458 }
10459
10460 // This automatically removes duplicate items in the pane
10461 destination.update(cx, |destination, cx| {
10462 destination.add_item_inner(
10463 item_handle,
10464 activate,
10465 activate,
10466 activate,
10467 Some(destination_index),
10468 window,
10469 cx,
10470 );
10471 if activate {
10472 window.focus(&destination.focus_handle(cx), cx)
10473 }
10474 });
10475}
10476
10477pub fn move_active_item(
10478 source: &Entity<Pane>,
10479 destination: &Entity<Pane>,
10480 focus_destination: bool,
10481 close_if_empty: bool,
10482 window: &mut Window,
10483 cx: &mut App,
10484) {
10485 if source == destination {
10486 return;
10487 }
10488 let Some(active_item) = source.read(cx).active_item() else {
10489 return;
10490 };
10491 source.update(cx, |source_pane, cx| {
10492 let item_id = active_item.item_id();
10493 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
10494 destination.update(cx, |target_pane, cx| {
10495 target_pane.add_item(
10496 active_item,
10497 focus_destination,
10498 focus_destination,
10499 Some(target_pane.items_len()),
10500 window,
10501 cx,
10502 );
10503 });
10504 });
10505}
10506
10507pub fn clone_active_item(
10508 workspace_id: Option<WorkspaceId>,
10509 source: &Entity<Pane>,
10510 destination: &Entity<Pane>,
10511 focus_destination: bool,
10512 window: &mut Window,
10513 cx: &mut App,
10514) {
10515 if source == destination {
10516 return;
10517 }
10518 let Some(active_item) = source.read(cx).active_item() else {
10519 return;
10520 };
10521 if !active_item.can_split(cx) {
10522 return;
10523 }
10524 let destination = destination.downgrade();
10525 let task = active_item.clone_on_split(workspace_id, window, cx);
10526 window
10527 .spawn(cx, async move |cx| {
10528 let Some(clone) = task.await else {
10529 return;
10530 };
10531 destination
10532 .update_in(cx, |target_pane, window, cx| {
10533 target_pane.add_item(
10534 clone,
10535 focus_destination,
10536 focus_destination,
10537 Some(target_pane.items_len()),
10538 window,
10539 cx,
10540 );
10541 })
10542 .log_err();
10543 })
10544 .detach();
10545}
10546
10547#[derive(Debug)]
10548pub struct WorkspacePosition {
10549 pub window_bounds: Option<WindowBounds>,
10550 pub display: Option<Uuid>,
10551 pub centered_layout: bool,
10552}
10553
10554pub fn remote_workspace_position_from_db(
10555 connection_options: RemoteConnectionOptions,
10556 paths_to_open: &[PathBuf],
10557 cx: &App,
10558) -> Task<Result<WorkspacePosition>> {
10559 let paths = paths_to_open.to_vec();
10560 let db = WorkspaceDb::global(cx);
10561 let kvp = db::kvp::KeyValueStore::global(cx);
10562
10563 cx.background_spawn(async move {
10564 let remote_connection_id = db
10565 .get_or_create_remote_connection(connection_options)
10566 .await
10567 .context("fetching serialized ssh project")?;
10568 let serialized_workspace = db.remote_workspace_for_roots(&paths, remote_connection_id);
10569
10570 let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
10571 (Some(WindowBounds::Windowed(bounds)), None)
10572 } else {
10573 let restorable_bounds = serialized_workspace
10574 .as_ref()
10575 .and_then(|workspace| {
10576 Some((workspace.display?, workspace.window_bounds.map(|b| b.0)?))
10577 })
10578 .or_else(|| persistence::read_default_window_bounds(&kvp));
10579
10580 if let Some((serialized_display, serialized_bounds)) = restorable_bounds {
10581 (Some(serialized_bounds), Some(serialized_display))
10582 } else {
10583 (None, None)
10584 }
10585 };
10586
10587 let centered_layout = serialized_workspace
10588 .as_ref()
10589 .map(|w| w.centered_layout)
10590 .unwrap_or(false);
10591
10592 Ok(WorkspacePosition {
10593 window_bounds,
10594 display,
10595 centered_layout,
10596 })
10597 })
10598}
10599
10600pub fn with_active_or_new_workspace(
10601 cx: &mut App,
10602 f: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + 'static,
10603) {
10604 match cx
10605 .active_window()
10606 .and_then(|w| w.downcast::<MultiWorkspace>())
10607 {
10608 Some(multi_workspace) => {
10609 cx.defer(move |cx| {
10610 multi_workspace
10611 .update(cx, |multi_workspace, window, cx| {
10612 let workspace = multi_workspace.workspace().clone();
10613 workspace.update(cx, |workspace, cx| f(workspace, window, cx));
10614 })
10615 .log_err();
10616 });
10617 }
10618 None => {
10619 let app_state = AppState::global(cx);
10620 open_new(
10621 OpenOptions::default(),
10622 app_state,
10623 cx,
10624 move |workspace, window, cx| f(workspace, window, cx),
10625 )
10626 .detach_and_log_err(cx);
10627 }
10628 }
10629}
10630
10631/// Reads a panel's pixel size from its legacy KVP format and deletes the legacy
10632/// key. This migration path only runs once per panel per workspace.
10633fn load_legacy_panel_size(
10634 panel_key: &str,
10635 dock_position: DockPosition,
10636 workspace: &Workspace,
10637 cx: &mut App,
10638) -> Option<Pixels> {
10639 #[derive(Deserialize)]
10640 struct LegacyPanelState {
10641 #[serde(default)]
10642 width: Option<Pixels>,
10643 #[serde(default)]
10644 height: Option<Pixels>,
10645 }
10646
10647 let workspace_id = workspace
10648 .database_id()
10649 .map(|id| i64::from(id).to_string())
10650 .or_else(|| workspace.session_id())?;
10651
10652 let legacy_key = match panel_key {
10653 "ProjectPanel" => {
10654 format!("{}-{:?}", "ProjectPanel", workspace_id)
10655 }
10656 "OutlinePanel" => {
10657 format!("{}-{:?}", "OutlinePanel", workspace_id)
10658 }
10659 "GitPanel" => {
10660 format!("{}-{:?}", "GitPanel", workspace_id)
10661 }
10662 "TerminalPanel" => {
10663 format!("{:?}-{:?}", "TerminalPanel", workspace_id)
10664 }
10665 _ => return None,
10666 };
10667
10668 let kvp = db::kvp::KeyValueStore::global(cx);
10669 let json = kvp.read_kvp(&legacy_key).log_err().flatten()?;
10670 let state = serde_json::from_str::<LegacyPanelState>(&json).log_err()?;
10671 let size = match dock_position {
10672 DockPosition::Bottom => state.height,
10673 DockPosition::Left | DockPosition::Right => state.width,
10674 }?;
10675
10676 cx.background_spawn(async move { kvp.delete_kvp(legacy_key).await })
10677 .detach_and_log_err(cx);
10678
10679 Some(size)
10680}
10681
10682#[cfg(test)]
10683mod tests {
10684 use std::{cell::RefCell, rc::Rc, sync::Arc, time::Duration};
10685
10686 use super::*;
10687 use crate::{
10688 dock::{PanelEvent, test::TestPanel},
10689 item::{
10690 ItemBufferKind, ItemEvent,
10691 test::{TestItem, TestProjectItem},
10692 },
10693 };
10694 use fs::FakeFs;
10695 use gpui::{
10696 DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
10697 UpdateGlobal, VisualTestContext, px,
10698 };
10699 use project::{Project, ProjectEntryId};
10700 use serde_json::json;
10701 use settings::SettingsStore;
10702 use util::path;
10703 use util::rel_path::rel_path;
10704
10705 #[gpui::test]
10706 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
10707 init_test(cx);
10708
10709 let fs = FakeFs::new(cx.executor());
10710 let project = Project::test(fs, [], cx).await;
10711 let (workspace, cx) =
10712 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10713
10714 // Adding an item with no ambiguity renders the tab without detail.
10715 let item1 = cx.new(|cx| {
10716 let mut item = TestItem::new(cx);
10717 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
10718 item
10719 });
10720 workspace.update_in(cx, |workspace, window, cx| {
10721 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
10722 });
10723 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
10724
10725 // Adding an item that creates ambiguity increases the level of detail on
10726 // both tabs.
10727 let item2 = cx.new_window_entity(|_window, cx| {
10728 let mut item = TestItem::new(cx);
10729 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
10730 item
10731 });
10732 workspace.update_in(cx, |workspace, window, cx| {
10733 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10734 });
10735 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10736 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10737
10738 // Adding an item that creates ambiguity increases the level of detail only
10739 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
10740 // we stop at the highest detail available.
10741 let item3 = cx.new(|cx| {
10742 let mut item = TestItem::new(cx);
10743 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
10744 item
10745 });
10746 workspace.update_in(cx, |workspace, window, cx| {
10747 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
10748 });
10749 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10750 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
10751 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
10752 }
10753
10754 #[gpui::test]
10755 async fn test_tracking_active_path(cx: &mut TestAppContext) {
10756 init_test(cx);
10757
10758 let fs = FakeFs::new(cx.executor());
10759 fs.insert_tree(
10760 "/root1",
10761 json!({
10762 "one.txt": "",
10763 "two.txt": "",
10764 }),
10765 )
10766 .await;
10767 fs.insert_tree(
10768 "/root2",
10769 json!({
10770 "three.txt": "",
10771 }),
10772 )
10773 .await;
10774
10775 let project = Project::test(fs, ["root1".as_ref()], cx).await;
10776 let (workspace, cx) =
10777 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10778 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10779 let worktree_id = project.update(cx, |project, cx| {
10780 project.worktrees(cx).next().unwrap().read(cx).id()
10781 });
10782
10783 let item1 = cx.new(|cx| {
10784 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
10785 });
10786 let item2 = cx.new(|cx| {
10787 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
10788 });
10789
10790 // Add an item to an empty pane
10791 workspace.update_in(cx, |workspace, window, cx| {
10792 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
10793 });
10794 project.update(cx, |project, cx| {
10795 assert_eq!(
10796 project.active_entry(),
10797 project
10798 .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
10799 .map(|e| e.id)
10800 );
10801 });
10802 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
10803
10804 // Add a second item to a non-empty pane
10805 workspace.update_in(cx, |workspace, window, cx| {
10806 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
10807 });
10808 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
10809 project.update(cx, |project, cx| {
10810 assert_eq!(
10811 project.active_entry(),
10812 project
10813 .entry_for_path(&(worktree_id, rel_path("two.txt")).into(), cx)
10814 .map(|e| e.id)
10815 );
10816 });
10817
10818 // Close the active item
10819 pane.update_in(cx, |pane, window, cx| {
10820 pane.close_active_item(&Default::default(), window, cx)
10821 })
10822 .await
10823 .unwrap();
10824 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
10825 project.update(cx, |project, cx| {
10826 assert_eq!(
10827 project.active_entry(),
10828 project
10829 .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
10830 .map(|e| e.id)
10831 );
10832 });
10833
10834 // Add a project folder
10835 project
10836 .update(cx, |project, cx| {
10837 project.find_or_create_worktree("root2", true, cx)
10838 })
10839 .await
10840 .unwrap();
10841 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
10842
10843 // Remove a project folder
10844 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
10845 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
10846 }
10847
10848 #[gpui::test]
10849 async fn test_close_window(cx: &mut TestAppContext) {
10850 init_test(cx);
10851
10852 let fs = FakeFs::new(cx.executor());
10853 fs.insert_tree("/root", json!({ "one": "" })).await;
10854
10855 let project = Project::test(fs, ["root".as_ref()], cx).await;
10856 let (workspace, cx) =
10857 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10858
10859 // When there are no dirty items, there's nothing to do.
10860 let item1 = cx.new(TestItem::new);
10861 workspace.update_in(cx, |w, window, cx| {
10862 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
10863 });
10864 let task = workspace.update_in(cx, |w, window, cx| {
10865 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
10866 });
10867 assert!(task.await.unwrap());
10868
10869 // When there are dirty untitled items, prompt to save each one. If the user
10870 // cancels any prompt, then abort.
10871 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
10872 let item3 = cx.new(|cx| {
10873 TestItem::new(cx)
10874 .with_dirty(true)
10875 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10876 });
10877 workspace.update_in(cx, |w, window, cx| {
10878 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10879 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
10880 });
10881 let task = workspace.update_in(cx, |w, window, cx| {
10882 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
10883 });
10884 cx.executor().run_until_parked();
10885 cx.simulate_prompt_answer("Cancel"); // cancel save all
10886 cx.executor().run_until_parked();
10887 assert!(!cx.has_pending_prompt());
10888 assert!(!task.await.unwrap());
10889 }
10890
10891 #[gpui::test]
10892 async fn test_multi_workspace_close_window_multiple_workspaces_cancel(cx: &mut TestAppContext) {
10893 init_test(cx);
10894
10895 let fs = FakeFs::new(cx.executor());
10896 fs.insert_tree("/root", json!({ "one": "" })).await;
10897
10898 let project_a = Project::test(fs.clone(), ["root".as_ref()], cx).await;
10899 let project_b = Project::test(fs, ["root".as_ref()], cx).await;
10900 let multi_workspace_handle =
10901 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
10902 cx.run_until_parked();
10903
10904 multi_workspace_handle
10905 .update(cx, |mw, _window, cx| {
10906 mw.open_sidebar(cx);
10907 })
10908 .unwrap();
10909
10910 let workspace_a = multi_workspace_handle
10911 .read_with(cx, |mw, _| mw.workspace().clone())
10912 .unwrap();
10913
10914 let workspace_b = multi_workspace_handle
10915 .update(cx, |mw, window, cx| {
10916 mw.test_add_workspace(project_b, window, cx)
10917 })
10918 .unwrap();
10919
10920 // Activate workspace A
10921 multi_workspace_handle
10922 .update(cx, |mw, window, cx| {
10923 let workspace = mw.workspaces().next().unwrap().clone();
10924 mw.activate(workspace, window, cx);
10925 })
10926 .unwrap();
10927
10928 let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
10929
10930 // Workspace A has a clean item
10931 let item_a = cx.new(TestItem::new);
10932 workspace_a.update_in(cx, |w, window, cx| {
10933 w.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
10934 });
10935
10936 // Workspace B has a dirty item
10937 let item_b = cx.new(|cx| TestItem::new(cx).with_dirty(true));
10938 workspace_b.update_in(cx, |w, window, cx| {
10939 w.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx)
10940 });
10941
10942 // Verify workspace A is active
10943 multi_workspace_handle
10944 .read_with(cx, |mw, _| {
10945 assert_eq!(mw.workspace(), &workspace_a);
10946 })
10947 .unwrap();
10948
10949 // Dispatch CloseWindow — workspace A will pass, workspace B will prompt
10950 multi_workspace_handle
10951 .update(cx, |mw, window, cx| {
10952 mw.close_window(&CloseWindow, window, cx);
10953 })
10954 .unwrap();
10955 cx.run_until_parked();
10956
10957 // Workspace B should now be active since it has dirty items that need attention
10958 multi_workspace_handle
10959 .read_with(cx, |mw, _| {
10960 assert_eq!(
10961 mw.workspace(),
10962 &workspace_b,
10963 "workspace B should be activated when it prompts"
10964 );
10965 })
10966 .unwrap();
10967
10968 // User cancels the save prompt from workspace B
10969 cx.simulate_prompt_answer("Cancel");
10970 cx.run_until_parked();
10971
10972 // Window should still exist because workspace B's close was cancelled
10973 assert!(
10974 multi_workspace_handle.update(cx, |_, _, _| ()).is_ok(),
10975 "window should still exist after cancelling one workspace's close"
10976 );
10977 }
10978
10979 #[gpui::test]
10980 async fn test_remove_workspace_prompts_for_unsaved_changes(cx: &mut TestAppContext) {
10981 init_test(cx);
10982
10983 let fs = FakeFs::new(cx.executor());
10984 fs.insert_tree("/root", json!({ "one": "" })).await;
10985
10986 let project_a = Project::test(fs.clone(), ["root".as_ref()], cx).await;
10987 let project_b = Project::test(fs.clone(), ["root".as_ref()], cx).await;
10988 let multi_workspace_handle =
10989 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
10990 cx.run_until_parked();
10991
10992 multi_workspace_handle
10993 .update(cx, |mw, _window, cx| mw.open_sidebar(cx))
10994 .unwrap();
10995
10996 let workspace_a = multi_workspace_handle
10997 .read_with(cx, |mw, _| mw.workspace().clone())
10998 .unwrap();
10999
11000 let workspace_b = multi_workspace_handle
11001 .update(cx, |mw, window, cx| {
11002 mw.test_add_workspace(project_b, window, cx)
11003 })
11004 .unwrap();
11005
11006 // Activate workspace A.
11007 multi_workspace_handle
11008 .update(cx, |mw, window, cx| {
11009 mw.activate(workspace_a.clone(), window, cx);
11010 })
11011 .unwrap();
11012
11013 let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
11014
11015 // Workspace B has a dirty item.
11016 let item_b = cx.new(|cx| TestItem::new(cx).with_dirty(true));
11017 workspace_b.update_in(cx, |w, window, cx| {
11018 w.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx)
11019 });
11020
11021 // Try to remove workspace B. It should prompt because of the dirty item.
11022 let remove_task = multi_workspace_handle
11023 .update(cx, |mw, window, cx| {
11024 mw.remove([workspace_b.clone()], |_, _, _| unreachable!(), window, cx)
11025 })
11026 .unwrap();
11027 cx.run_until_parked();
11028
11029 // The prompt should have activated workspace B.
11030 multi_workspace_handle
11031 .read_with(cx, |mw, _| {
11032 assert_eq!(
11033 mw.workspace(),
11034 &workspace_b,
11035 "workspace B should be active while prompting"
11036 );
11037 })
11038 .unwrap();
11039
11040 // Cancel the prompt — user stays on workspace B.
11041 cx.simulate_prompt_answer("Cancel");
11042 cx.run_until_parked();
11043 let removed = remove_task.await.unwrap();
11044 assert!(!removed, "removal should have been cancelled");
11045
11046 multi_workspace_handle
11047 .read_with(cx, |mw, _| {
11048 assert_eq!(
11049 mw.workspace(),
11050 &workspace_b,
11051 "user should stay on workspace B after cancelling"
11052 );
11053 assert_eq!(mw.workspaces().count(), 2, "both workspaces should remain");
11054 })
11055 .unwrap();
11056
11057 // Try again. This time accept the prompt.
11058 let remove_task = multi_workspace_handle
11059 .update(cx, |mw, window, cx| {
11060 // First switch back to A.
11061 mw.activate(workspace_a.clone(), window, cx);
11062 mw.remove([workspace_b.clone()], |_, _, _| unreachable!(), window, cx)
11063 })
11064 .unwrap();
11065 cx.run_until_parked();
11066
11067 // Accept the save prompt.
11068 cx.simulate_prompt_answer("Don't Save");
11069 cx.run_until_parked();
11070 let removed = remove_task.await.unwrap();
11071 assert!(removed, "removal should have succeeded");
11072
11073 // Should be back on workspace A, and B should be gone.
11074 multi_workspace_handle
11075 .read_with(cx, |mw, _| {
11076 assert_eq!(
11077 mw.workspace(),
11078 &workspace_a,
11079 "should be back on workspace A after removing B"
11080 );
11081 assert_eq!(mw.workspaces().count(), 1, "only workspace A should remain");
11082 })
11083 .unwrap();
11084 }
11085
11086 #[gpui::test]
11087 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
11088 init_test(cx);
11089
11090 // Register TestItem as a serializable item
11091 cx.update(|cx| {
11092 register_serializable_item::<TestItem>(cx);
11093 });
11094
11095 let fs = FakeFs::new(cx.executor());
11096 fs.insert_tree("/root", json!({ "one": "" })).await;
11097
11098 let project = Project::test(fs, ["root".as_ref()], cx).await;
11099 let (workspace, cx) =
11100 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
11101
11102 // When there are dirty untitled items, but they can serialize, then there is no prompt.
11103 let item1 = cx.new(|cx| {
11104 TestItem::new(cx)
11105 .with_dirty(true)
11106 .with_serialize(|| Some(Task::ready(Ok(()))))
11107 });
11108 let item2 = cx.new(|cx| {
11109 TestItem::new(cx)
11110 .with_dirty(true)
11111 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11112 .with_serialize(|| Some(Task::ready(Ok(()))))
11113 });
11114 workspace.update_in(cx, |w, window, cx| {
11115 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
11116 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
11117 });
11118 let task = workspace.update_in(cx, |w, window, cx| {
11119 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
11120 });
11121 assert!(task.await.unwrap());
11122 }
11123
11124 #[gpui::test]
11125 async fn test_close_pane_items(cx: &mut TestAppContext) {
11126 init_test(cx);
11127
11128 let fs = FakeFs::new(cx.executor());
11129
11130 let project = Project::test(fs, None, cx).await;
11131 let (workspace, cx) =
11132 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11133
11134 let item1 = cx.new(|cx| {
11135 TestItem::new(cx)
11136 .with_dirty(true)
11137 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
11138 });
11139 let item2 = cx.new(|cx| {
11140 TestItem::new(cx)
11141 .with_dirty(true)
11142 .with_conflict(true)
11143 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
11144 });
11145 let item3 = cx.new(|cx| {
11146 TestItem::new(cx)
11147 .with_dirty(true)
11148 .with_conflict(true)
11149 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
11150 });
11151 let item4 = cx.new(|cx| {
11152 TestItem::new(cx).with_dirty(true).with_project_items(&[{
11153 let project_item = TestProjectItem::new_untitled(cx);
11154 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11155 project_item
11156 }])
11157 });
11158 let pane = workspace.update_in(cx, |workspace, window, cx| {
11159 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
11160 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
11161 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
11162 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
11163 workspace.active_pane().clone()
11164 });
11165
11166 let close_items = pane.update_in(cx, |pane, window, cx| {
11167 pane.activate_item(1, true, true, window, cx);
11168 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
11169 let item1_id = item1.item_id();
11170 let item3_id = item3.item_id();
11171 let item4_id = item4.item_id();
11172 pane.close_items(window, cx, SaveIntent::Close, &move |id| {
11173 [item1_id, item3_id, item4_id].contains(&id)
11174 })
11175 });
11176 cx.executor().run_until_parked();
11177
11178 assert!(cx.has_pending_prompt());
11179 cx.simulate_prompt_answer("Save all");
11180
11181 cx.executor().run_until_parked();
11182
11183 // Item 1 is saved. There's a prompt to save item 3.
11184 pane.update(cx, |pane, cx| {
11185 assert_eq!(item1.read(cx).save_count, 1);
11186 assert_eq!(item1.read(cx).save_as_count, 0);
11187 assert_eq!(item1.read(cx).reload_count, 0);
11188 assert_eq!(pane.items_len(), 3);
11189 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
11190 });
11191 assert!(cx.has_pending_prompt());
11192
11193 // Cancel saving item 3.
11194 cx.simulate_prompt_answer("Discard");
11195 cx.executor().run_until_parked();
11196
11197 // Item 3 is reloaded. There's a prompt to save item 4.
11198 pane.update(cx, |pane, cx| {
11199 assert_eq!(item3.read(cx).save_count, 0);
11200 assert_eq!(item3.read(cx).save_as_count, 0);
11201 assert_eq!(item3.read(cx).reload_count, 1);
11202 assert_eq!(pane.items_len(), 2);
11203 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
11204 });
11205
11206 // There's a prompt for a path for item 4.
11207 cx.simulate_new_path_selection(|_| Some(Default::default()));
11208 close_items.await.unwrap();
11209
11210 // The requested items are closed.
11211 pane.update(cx, |pane, cx| {
11212 assert_eq!(item4.read(cx).save_count, 0);
11213 assert_eq!(item4.read(cx).save_as_count, 1);
11214 assert_eq!(item4.read(cx).reload_count, 0);
11215 assert_eq!(pane.items_len(), 1);
11216 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
11217 });
11218 }
11219
11220 #[gpui::test]
11221 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
11222 init_test(cx);
11223
11224 let fs = FakeFs::new(cx.executor());
11225 let project = Project::test(fs, [], cx).await;
11226 let (workspace, cx) =
11227 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11228
11229 // Create several workspace items with single project entries, and two
11230 // workspace items with multiple project entries.
11231 let single_entry_items = (0..=4)
11232 .map(|project_entry_id| {
11233 cx.new(|cx| {
11234 TestItem::new(cx)
11235 .with_dirty(true)
11236 .with_project_items(&[dirty_project_item(
11237 project_entry_id,
11238 &format!("{project_entry_id}.txt"),
11239 cx,
11240 )])
11241 })
11242 })
11243 .collect::<Vec<_>>();
11244 let item_2_3 = cx.new(|cx| {
11245 TestItem::new(cx)
11246 .with_dirty(true)
11247 .with_buffer_kind(ItemBufferKind::Multibuffer)
11248 .with_project_items(&[
11249 single_entry_items[2].read(cx).project_items[0].clone(),
11250 single_entry_items[3].read(cx).project_items[0].clone(),
11251 ])
11252 });
11253 let item_3_4 = cx.new(|cx| {
11254 TestItem::new(cx)
11255 .with_dirty(true)
11256 .with_buffer_kind(ItemBufferKind::Multibuffer)
11257 .with_project_items(&[
11258 single_entry_items[3].read(cx).project_items[0].clone(),
11259 single_entry_items[4].read(cx).project_items[0].clone(),
11260 ])
11261 });
11262
11263 // Create two panes that contain the following project entries:
11264 // left pane:
11265 // multi-entry items: (2, 3)
11266 // single-entry items: 0, 2, 3, 4
11267 // right pane:
11268 // single-entry items: 4, 1
11269 // multi-entry items: (3, 4)
11270 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
11271 let left_pane = workspace.active_pane().clone();
11272 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
11273 workspace.add_item_to_active_pane(
11274 single_entry_items[0].boxed_clone(),
11275 None,
11276 true,
11277 window,
11278 cx,
11279 );
11280 workspace.add_item_to_active_pane(
11281 single_entry_items[2].boxed_clone(),
11282 None,
11283 true,
11284 window,
11285 cx,
11286 );
11287 workspace.add_item_to_active_pane(
11288 single_entry_items[3].boxed_clone(),
11289 None,
11290 true,
11291 window,
11292 cx,
11293 );
11294 workspace.add_item_to_active_pane(
11295 single_entry_items[4].boxed_clone(),
11296 None,
11297 true,
11298 window,
11299 cx,
11300 );
11301
11302 let right_pane =
11303 workspace.split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx);
11304
11305 let boxed_clone = single_entry_items[1].boxed_clone();
11306 let right_pane = window.spawn(cx, async move |cx| {
11307 right_pane.await.inspect(|right_pane| {
11308 right_pane
11309 .update_in(cx, |pane, window, cx| {
11310 pane.add_item(boxed_clone, true, true, None, window, cx);
11311 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
11312 })
11313 .unwrap();
11314 })
11315 });
11316
11317 (left_pane, right_pane)
11318 });
11319 let right_pane = right_pane.await.unwrap();
11320 cx.focus(&right_pane);
11321
11322 let close = right_pane.update_in(cx, |pane, window, cx| {
11323 pane.close_all_items(&CloseAllItems::default(), window, cx)
11324 .unwrap()
11325 });
11326 cx.executor().run_until_parked();
11327
11328 let msg = cx.pending_prompt().unwrap().0;
11329 assert!(msg.contains("1.txt"));
11330 assert!(!msg.contains("2.txt"));
11331 assert!(!msg.contains("3.txt"));
11332 assert!(!msg.contains("4.txt"));
11333
11334 // With best-effort close, cancelling item 1 keeps it open but items 4
11335 // and (3,4) still close since their entries exist in left pane.
11336 cx.simulate_prompt_answer("Cancel");
11337 close.await;
11338
11339 right_pane.read_with(cx, |pane, _| {
11340 assert_eq!(pane.items_len(), 1);
11341 });
11342
11343 // Remove item 3 from left pane, making (2,3) the only item with entry 3.
11344 left_pane
11345 .update_in(cx, |left_pane, window, cx| {
11346 left_pane.close_item_by_id(
11347 single_entry_items[3].entity_id(),
11348 SaveIntent::Skip,
11349 window,
11350 cx,
11351 )
11352 })
11353 .await
11354 .unwrap();
11355
11356 let close = left_pane.update_in(cx, |pane, window, cx| {
11357 pane.close_all_items(&CloseAllItems::default(), window, cx)
11358 .unwrap()
11359 });
11360 cx.executor().run_until_parked();
11361
11362 let details = cx.pending_prompt().unwrap().1;
11363 assert!(details.contains("0.txt"));
11364 assert!(details.contains("3.txt"));
11365 assert!(details.contains("4.txt"));
11366 // Ideally 2.txt wouldn't appear since entry 2 still exists in item 2.
11367 // But we can only save whole items, so saving (2,3) for entry 3 includes 2.
11368 // assert!(!details.contains("2.txt"));
11369
11370 cx.simulate_prompt_answer("Save all");
11371 cx.executor().run_until_parked();
11372 close.await;
11373
11374 left_pane.read_with(cx, |pane, _| {
11375 assert_eq!(pane.items_len(), 0);
11376 });
11377 }
11378
11379 #[gpui::test]
11380 async fn test_autosave(cx: &mut gpui::TestAppContext) {
11381 init_test(cx);
11382
11383 let fs = FakeFs::new(cx.executor());
11384 let project = Project::test(fs, [], cx).await;
11385 let (workspace, cx) =
11386 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11387 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11388
11389 let item = cx.new(|cx| {
11390 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11391 });
11392 let item_id = item.entity_id();
11393 workspace.update_in(cx, |workspace, window, cx| {
11394 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11395 });
11396
11397 // Autosave on window change.
11398 item.update(cx, |item, cx| {
11399 SettingsStore::update_global(cx, |settings, cx| {
11400 settings.update_user_settings(cx, |settings| {
11401 settings.workspace.autosave = Some(AutosaveSetting::OnWindowChange);
11402 })
11403 });
11404 item.is_dirty = true;
11405 });
11406
11407 // Deactivating the window saves the file.
11408 cx.deactivate_window();
11409 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11410
11411 // Re-activating the window doesn't save the file.
11412 cx.update(|window, _| window.activate_window());
11413 cx.executor().run_until_parked();
11414 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11415
11416 // Autosave on focus change.
11417 item.update_in(cx, |item, window, cx| {
11418 cx.focus_self(window);
11419 SettingsStore::update_global(cx, |settings, cx| {
11420 settings.update_user_settings(cx, |settings| {
11421 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11422 })
11423 });
11424 item.is_dirty = true;
11425 });
11426 // Blurring the item saves the file.
11427 item.update_in(cx, |_, window, _| window.blur());
11428 cx.executor().run_until_parked();
11429 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
11430
11431 // Deactivating the window still saves the file.
11432 item.update_in(cx, |item, window, cx| {
11433 cx.focus_self(window);
11434 item.is_dirty = true;
11435 });
11436 cx.deactivate_window();
11437 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
11438
11439 // Autosave after delay.
11440 item.update(cx, |item, cx| {
11441 SettingsStore::update_global(cx, |settings, cx| {
11442 settings.update_user_settings(cx, |settings| {
11443 settings.workspace.autosave = Some(AutosaveSetting::AfterDelay {
11444 milliseconds: 500.into(),
11445 });
11446 })
11447 });
11448 item.is_dirty = true;
11449 cx.emit(ItemEvent::Edit);
11450 });
11451
11452 // Delay hasn't fully expired, so the file is still dirty and unsaved.
11453 cx.executor().advance_clock(Duration::from_millis(250));
11454 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
11455
11456 // After delay expires, the file is saved.
11457 cx.executor().advance_clock(Duration::from_millis(250));
11458 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
11459
11460 // Autosave after delay, should save earlier than delay if tab is closed
11461 item.update(cx, |item, cx| {
11462 item.is_dirty = true;
11463 cx.emit(ItemEvent::Edit);
11464 });
11465 cx.executor().advance_clock(Duration::from_millis(250));
11466 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
11467
11468 // // Ensure auto save with delay saves the item on close, even if the timer hasn't yet run out.
11469 pane.update_in(cx, |pane, window, cx| {
11470 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11471 })
11472 .await
11473 .unwrap();
11474 assert!(!cx.has_pending_prompt());
11475 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
11476
11477 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
11478 workspace.update_in(cx, |workspace, window, cx| {
11479 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11480 });
11481 item.update_in(cx, |item, _window, cx| {
11482 item.is_dirty = true;
11483 for project_item in &mut item.project_items {
11484 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11485 }
11486 });
11487 cx.run_until_parked();
11488 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
11489
11490 // Autosave on focus change, ensuring closing the tab counts as such.
11491 item.update(cx, |item, cx| {
11492 SettingsStore::update_global(cx, |settings, cx| {
11493 settings.update_user_settings(cx, |settings| {
11494 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11495 })
11496 });
11497 item.is_dirty = true;
11498 for project_item in &mut item.project_items {
11499 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11500 }
11501 });
11502
11503 pane.update_in(cx, |pane, window, cx| {
11504 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11505 })
11506 .await
11507 .unwrap();
11508 assert!(!cx.has_pending_prompt());
11509 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11510
11511 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
11512 workspace.update_in(cx, |workspace, window, cx| {
11513 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11514 });
11515 item.update_in(cx, |item, window, cx| {
11516 item.project_items[0].update(cx, |item, _| {
11517 item.entry_id = None;
11518 });
11519 item.is_dirty = true;
11520 window.blur();
11521 });
11522 cx.run_until_parked();
11523 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11524
11525 // Ensure autosave is prevented for deleted files also when closing the buffer.
11526 let _close_items = pane.update_in(cx, |pane, window, cx| {
11527 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11528 });
11529 cx.run_until_parked();
11530 assert!(cx.has_pending_prompt());
11531 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11532 }
11533
11534 #[gpui::test]
11535 async fn test_autosave_on_focus_change_in_multibuffer(cx: &mut gpui::TestAppContext) {
11536 init_test(cx);
11537
11538 let fs = FakeFs::new(cx.executor());
11539 let project = Project::test(fs, [], cx).await;
11540 let (workspace, cx) =
11541 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11542
11543 // Create a multibuffer-like item with two child focus handles,
11544 // simulating individual buffer editors within a multibuffer.
11545 let item = cx.new(|cx| {
11546 TestItem::new(cx)
11547 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11548 .with_child_focus_handles(2, cx)
11549 });
11550 workspace.update_in(cx, |workspace, window, cx| {
11551 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11552 });
11553
11554 // Set autosave to OnFocusChange and focus the first child handle,
11555 // simulating the user's cursor being inside one of the multibuffer's excerpts.
11556 item.update_in(cx, |item, window, cx| {
11557 SettingsStore::update_global(cx, |settings, cx| {
11558 settings.update_user_settings(cx, |settings| {
11559 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11560 })
11561 });
11562 item.is_dirty = true;
11563 window.focus(&item.child_focus_handles[0], cx);
11564 });
11565 cx.executor().run_until_parked();
11566 item.read_with(cx, |item, _| assert_eq!(item.save_count, 0));
11567
11568 // Moving focus from one child to another within the same item should
11569 // NOT trigger autosave — focus is still within the item's focus hierarchy.
11570 item.update_in(cx, |item, window, cx| {
11571 window.focus(&item.child_focus_handles[1], cx);
11572 });
11573 cx.executor().run_until_parked();
11574 item.read_with(cx, |item, _| {
11575 assert_eq!(
11576 item.save_count, 0,
11577 "Switching focus between children within the same item should not autosave"
11578 );
11579 });
11580
11581 // Blurring the item saves the file. This is the core regression scenario:
11582 // with `on_blur`, this would NOT trigger because `on_blur` only fires when
11583 // the item's own focus handle is the leaf that lost focus. In a multibuffer,
11584 // the leaf is always a child focus handle, so `on_blur` never detected
11585 // focus leaving the item.
11586 item.update_in(cx, |_, window, _| window.blur());
11587 cx.executor().run_until_parked();
11588 item.read_with(cx, |item, _| {
11589 assert_eq!(
11590 item.save_count, 1,
11591 "Blurring should trigger autosave when focus was on a child of the item"
11592 );
11593 });
11594
11595 // Deactivating the window should also trigger autosave when a child of
11596 // the multibuffer item currently owns focus.
11597 item.update_in(cx, |item, window, cx| {
11598 item.is_dirty = true;
11599 window.focus(&item.child_focus_handles[0], cx);
11600 });
11601 cx.executor().run_until_parked();
11602 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11603
11604 cx.deactivate_window();
11605 item.read_with(cx, |item, _| {
11606 assert_eq!(
11607 item.save_count, 2,
11608 "Deactivating window should trigger autosave when focus was on a child"
11609 );
11610 });
11611 }
11612
11613 #[gpui::test]
11614 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
11615 init_test(cx);
11616
11617 let fs = FakeFs::new(cx.executor());
11618
11619 let project = Project::test(fs, [], cx).await;
11620 let (workspace, cx) =
11621 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11622
11623 let item = cx.new(|cx| {
11624 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11625 });
11626 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11627 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
11628 let toolbar_notify_count = Rc::new(RefCell::new(0));
11629
11630 workspace.update_in(cx, |workspace, window, cx| {
11631 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11632 let toolbar_notification_count = toolbar_notify_count.clone();
11633 cx.observe_in(&toolbar, window, move |_, _, _, _| {
11634 *toolbar_notification_count.borrow_mut() += 1
11635 })
11636 .detach();
11637 });
11638
11639 pane.read_with(cx, |pane, _| {
11640 assert!(!pane.can_navigate_backward());
11641 assert!(!pane.can_navigate_forward());
11642 });
11643
11644 item.update_in(cx, |item, _, cx| {
11645 item.set_state("one".to_string(), cx);
11646 });
11647
11648 // Toolbar must be notified to re-render the navigation buttons
11649 assert_eq!(*toolbar_notify_count.borrow(), 1);
11650
11651 pane.read_with(cx, |pane, _| {
11652 assert!(pane.can_navigate_backward());
11653 assert!(!pane.can_navigate_forward());
11654 });
11655
11656 workspace
11657 .update_in(cx, |workspace, window, cx| {
11658 workspace.go_back(pane.downgrade(), window, cx)
11659 })
11660 .await
11661 .unwrap();
11662
11663 assert_eq!(*toolbar_notify_count.borrow(), 2);
11664 pane.read_with(cx, |pane, _| {
11665 assert!(!pane.can_navigate_backward());
11666 assert!(pane.can_navigate_forward());
11667 });
11668 }
11669
11670 /// Tests that the navigation history deduplicates entries for the same item.
11671 ///
11672 /// When navigating back and forth between items (e.g., A -> B -> A -> B -> A -> B -> C),
11673 /// the navigation history deduplicates by keeping only the most recent visit to each item,
11674 /// resulting in [A, B, C] instead of [A, B, A, B, A, B, C]. This ensures that Go Back (Ctrl-O)
11675 /// navigates through unique items efficiently: C -> B -> A, rather than bouncing between
11676 /// repeated entries: C -> B -> A -> B -> A -> B -> A.
11677 ///
11678 /// This behavior prevents the navigation history from growing unnecessarily large and provides
11679 /// a better user experience by eliminating redundant navigation steps when jumping between files.
11680 #[gpui::test]
11681 async fn test_navigation_history_deduplication(cx: &mut gpui::TestAppContext) {
11682 init_test(cx);
11683
11684 let fs = FakeFs::new(cx.executor());
11685 let project = Project::test(fs, [], cx).await;
11686 let (workspace, cx) =
11687 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11688
11689 let item_a = cx.new(|cx| {
11690 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "a.txt", cx)])
11691 });
11692 let item_b = cx.new(|cx| {
11693 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "b.txt", cx)])
11694 });
11695 let item_c = cx.new(|cx| {
11696 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "c.txt", cx)])
11697 });
11698
11699 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11700
11701 workspace.update_in(cx, |workspace, window, cx| {
11702 workspace.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx);
11703 workspace.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx);
11704 workspace.add_item_to_active_pane(Box::new(item_c.clone()), None, true, window, cx);
11705 });
11706
11707 workspace.update_in(cx, |workspace, window, cx| {
11708 workspace.activate_item(&item_a, false, false, window, cx);
11709 });
11710 cx.run_until_parked();
11711
11712 workspace.update_in(cx, |workspace, window, cx| {
11713 workspace.activate_item(&item_b, false, false, window, cx);
11714 });
11715 cx.run_until_parked();
11716
11717 workspace.update_in(cx, |workspace, window, cx| {
11718 workspace.activate_item(&item_a, false, false, window, cx);
11719 });
11720 cx.run_until_parked();
11721
11722 workspace.update_in(cx, |workspace, window, cx| {
11723 workspace.activate_item(&item_b, false, false, window, cx);
11724 });
11725 cx.run_until_parked();
11726
11727 workspace.update_in(cx, |workspace, window, cx| {
11728 workspace.activate_item(&item_a, false, false, window, cx);
11729 });
11730 cx.run_until_parked();
11731
11732 workspace.update_in(cx, |workspace, window, cx| {
11733 workspace.activate_item(&item_b, false, false, window, cx);
11734 });
11735 cx.run_until_parked();
11736
11737 workspace.update_in(cx, |workspace, window, cx| {
11738 workspace.activate_item(&item_c, false, false, window, cx);
11739 });
11740 cx.run_until_parked();
11741
11742 let backward_count = pane.read_with(cx, |pane, cx| {
11743 let mut count = 0;
11744 pane.nav_history().for_each_entry(cx, &mut |_, _| {
11745 count += 1;
11746 });
11747 count
11748 });
11749 assert!(
11750 backward_count <= 4,
11751 "Should have at most 4 entries, got {}",
11752 backward_count
11753 );
11754
11755 workspace
11756 .update_in(cx, |workspace, window, cx| {
11757 workspace.go_back(pane.downgrade(), window, cx)
11758 })
11759 .await
11760 .unwrap();
11761
11762 let active_item = workspace.read_with(cx, |workspace, cx| {
11763 workspace.active_item(cx).unwrap().item_id()
11764 });
11765 assert_eq!(
11766 active_item,
11767 item_b.entity_id(),
11768 "After first go_back, should be at item B"
11769 );
11770
11771 workspace
11772 .update_in(cx, |workspace, window, cx| {
11773 workspace.go_back(pane.downgrade(), window, cx)
11774 })
11775 .await
11776 .unwrap();
11777
11778 let active_item = workspace.read_with(cx, |workspace, cx| {
11779 workspace.active_item(cx).unwrap().item_id()
11780 });
11781 assert_eq!(
11782 active_item,
11783 item_a.entity_id(),
11784 "After second go_back, should be at item A"
11785 );
11786
11787 pane.read_with(cx, |pane, _| {
11788 assert!(pane.can_navigate_forward(), "Should be able to go forward");
11789 });
11790 }
11791
11792 #[gpui::test]
11793 async fn test_activate_last_pane(cx: &mut gpui::TestAppContext) {
11794 init_test(cx);
11795 let fs = FakeFs::new(cx.executor());
11796 let project = Project::test(fs, [], cx).await;
11797 let (multi_workspace, cx) =
11798 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
11799 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
11800
11801 workspace.update_in(cx, |workspace, window, cx| {
11802 let first_item = cx.new(|cx| {
11803 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11804 });
11805 workspace.add_item_to_active_pane(Box::new(first_item), None, true, window, cx);
11806 workspace.split_pane(
11807 workspace.active_pane().clone(),
11808 SplitDirection::Right,
11809 window,
11810 cx,
11811 );
11812 workspace.split_pane(
11813 workspace.active_pane().clone(),
11814 SplitDirection::Right,
11815 window,
11816 cx,
11817 );
11818 });
11819
11820 let (first_pane_id, target_last_pane_id) = workspace.update(cx, |workspace, _cx| {
11821 let panes = workspace.center.panes();
11822 assert!(panes.len() >= 2);
11823 (
11824 panes.first().expect("at least one pane").entity_id(),
11825 panes.last().expect("at least one pane").entity_id(),
11826 )
11827 });
11828
11829 workspace.update_in(cx, |workspace, window, cx| {
11830 workspace.activate_pane_at_index(&ActivatePane(0), window, cx);
11831 });
11832 workspace.update(cx, |workspace, _| {
11833 assert_eq!(workspace.active_pane().entity_id(), first_pane_id);
11834 assert_ne!(workspace.active_pane().entity_id(), target_last_pane_id);
11835 });
11836
11837 cx.dispatch_action(ActivateLastPane);
11838
11839 workspace.update(cx, |workspace, _| {
11840 assert_eq!(workspace.active_pane().entity_id(), target_last_pane_id);
11841 });
11842 }
11843
11844 #[gpui::test]
11845 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
11846 init_test(cx);
11847 let fs = FakeFs::new(cx.executor());
11848
11849 let project = Project::test(fs, [], cx).await;
11850 let (workspace, cx) =
11851 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11852
11853 let panel = workspace.update_in(cx, |workspace, window, cx| {
11854 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
11855 workspace.add_panel(panel.clone(), window, cx);
11856
11857 workspace
11858 .right_dock()
11859 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
11860
11861 panel
11862 });
11863
11864 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11865 pane.update_in(cx, |pane, window, cx| {
11866 let item = cx.new(TestItem::new);
11867 pane.add_item(Box::new(item), true, true, None, window, cx);
11868 });
11869
11870 // Transfer focus from center to panel
11871 workspace.update_in(cx, |workspace, window, cx| {
11872 workspace.toggle_panel_focus::<TestPanel>(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 });
11880
11881 // Transfer focus from panel to center
11882 workspace.update_in(cx, |workspace, window, cx| {
11883 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11884 });
11885
11886 workspace.update_in(cx, |workspace, window, cx| {
11887 assert!(workspace.right_dock().read(cx).is_open());
11888 assert!(!panel.is_zoomed(window, cx));
11889 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11890 assert!(pane.read(cx).focus_handle(cx).contains_focused(window, cx));
11891 });
11892
11893 // Close the dock
11894 workspace.update_in(cx, |workspace, window, cx| {
11895 workspace.toggle_dock(DockPosition::Right, window, cx);
11896 });
11897
11898 workspace.update_in(cx, |workspace, window, cx| {
11899 assert!(!workspace.right_dock().read(cx).is_open());
11900 assert!(!panel.is_zoomed(window, cx));
11901 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11902 assert!(pane.read(cx).focus_handle(cx).contains_focused(window, cx));
11903 });
11904
11905 // Open the dock
11906 workspace.update_in(cx, |workspace, window, cx| {
11907 workspace.toggle_dock(DockPosition::Right, 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 // Focus and zoom panel
11917 panel.update_in(cx, |panel, window, cx| {
11918 cx.focus_self(window);
11919 panel.set_zoomed(true, window, cx)
11920 });
11921
11922 workspace.update_in(cx, |workspace, window, cx| {
11923 assert!(workspace.right_dock().read(cx).is_open());
11924 assert!(panel.is_zoomed(window, cx));
11925 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11926 });
11927
11928 // Transfer focus to the center closes the dock
11929 workspace.update_in(cx, |workspace, window, cx| {
11930 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11931 });
11932
11933 workspace.update_in(cx, |workspace, window, cx| {
11934 assert!(!workspace.right_dock().read(cx).is_open());
11935 assert!(panel.is_zoomed(window, cx));
11936 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11937 });
11938
11939 // Transferring focus back to the panel keeps it zoomed
11940 workspace.update_in(cx, |workspace, window, cx| {
11941 workspace.toggle_panel_focus::<TestPanel>(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!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11948 });
11949
11950 // Close the dock while it is zoomed
11951 workspace.update_in(cx, |workspace, window, cx| {
11952 workspace.toggle_dock(DockPosition::Right, window, cx)
11953 });
11954
11955 workspace.update_in(cx, |workspace, window, cx| {
11956 assert!(!workspace.right_dock().read(cx).is_open());
11957 assert!(panel.is_zoomed(window, cx));
11958 assert!(workspace.zoomed.is_none());
11959 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11960 });
11961
11962 // Opening the dock, when it's zoomed, retains focus
11963 workspace.update_in(cx, |workspace, window, cx| {
11964 workspace.toggle_dock(DockPosition::Right, window, cx)
11965 });
11966
11967 workspace.update_in(cx, |workspace, window, cx| {
11968 assert!(workspace.right_dock().read(cx).is_open());
11969 assert!(panel.is_zoomed(window, cx));
11970 assert!(workspace.zoomed.is_some());
11971 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11972 });
11973
11974 // Unzoom and close the panel, zoom the active pane.
11975 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
11976 workspace.update_in(cx, |workspace, window, cx| {
11977 workspace.toggle_dock(DockPosition::Right, window, cx)
11978 });
11979 pane.update_in(cx, |pane, window, cx| {
11980 pane.toggle_zoom(&Default::default(), window, cx)
11981 });
11982
11983 // Opening a dock unzooms the pane.
11984 workspace.update_in(cx, |workspace, window, cx| {
11985 workspace.toggle_dock(DockPosition::Right, window, cx)
11986 });
11987 workspace.update_in(cx, |workspace, window, cx| {
11988 let pane = pane.read(cx);
11989 assert!(!pane.is_zoomed());
11990 assert!(!pane.focus_handle(cx).is_focused(window));
11991 assert!(workspace.right_dock().read(cx).is_open());
11992 assert!(workspace.zoomed.is_none());
11993 });
11994 }
11995
11996 #[gpui::test]
11997 async fn test_close_panel_on_toggle(cx: &mut gpui::TestAppContext) {
11998 init_test(cx);
11999 let fs = FakeFs::new(cx.executor());
12000
12001 let project = Project::test(fs, [], cx).await;
12002 let (workspace, cx) =
12003 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12004
12005 let panel = workspace.update_in(cx, |workspace, window, cx| {
12006 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
12007 workspace.add_panel(panel.clone(), window, cx);
12008 panel
12009 });
12010
12011 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12012 pane.update_in(cx, |pane, window, cx| {
12013 let item = cx.new(TestItem::new);
12014 pane.add_item(Box::new(item), true, true, None, window, cx);
12015 });
12016
12017 // Enable close_panel_on_toggle
12018 cx.update_global(|store: &mut SettingsStore, cx| {
12019 store.update_user_settings(cx, |settings| {
12020 settings.workspace.close_panel_on_toggle = Some(true);
12021 });
12022 });
12023
12024 // Panel starts closed. Toggling should open and focus it.
12025 workspace.update_in(cx, |workspace, window, cx| {
12026 assert!(!workspace.right_dock().read(cx).is_open());
12027 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12028 });
12029
12030 workspace.update_in(cx, |workspace, window, cx| {
12031 assert!(
12032 workspace.right_dock().read(cx).is_open(),
12033 "Dock should be open after toggling from center"
12034 );
12035 assert!(
12036 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12037 "Panel should be focused after toggling from center"
12038 );
12039 });
12040
12041 // Panel is open and focused. Toggling should close the panel and
12042 // return focus to the center.
12043 workspace.update_in(cx, |workspace, window, cx| {
12044 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12045 });
12046
12047 workspace.update_in(cx, |workspace, window, cx| {
12048 assert!(
12049 !workspace.right_dock().read(cx).is_open(),
12050 "Dock should be closed after toggling from focused panel"
12051 );
12052 assert!(
12053 !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12054 "Panel should not be focused after toggling from focused panel"
12055 );
12056 });
12057
12058 // Open the dock and focus something else so the panel is open but not
12059 // focused. Toggling should focus the panel (not close it).
12060 workspace.update_in(cx, |workspace, window, cx| {
12061 workspace
12062 .right_dock()
12063 .update(cx, |dock, cx| dock.set_open(true, window, cx));
12064 window.focus(&pane.read(cx).focus_handle(cx), cx);
12065 });
12066
12067 workspace.update_in(cx, |workspace, window, cx| {
12068 assert!(workspace.right_dock().read(cx).is_open());
12069 assert!(!panel.read(cx).focus_handle(cx).contains_focused(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 toggling focuses an open-but-unfocused panel"
12077 );
12078 assert!(
12079 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12080 "Panel should be focused after toggling an open-but-unfocused panel"
12081 );
12082 });
12083
12084 // Now disable the setting and verify the original behavior: toggling
12085 // from a focused panel moves focus to center but leaves the dock open.
12086 cx.update_global(|store: &mut SettingsStore, cx| {
12087 store.update_user_settings(cx, |settings| {
12088 settings.workspace.close_panel_on_toggle = Some(false);
12089 });
12090 });
12091
12092 workspace.update_in(cx, |workspace, window, cx| {
12093 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12094 });
12095
12096 workspace.update_in(cx, |workspace, window, cx| {
12097 assert!(
12098 workspace.right_dock().read(cx).is_open(),
12099 "Dock should remain open when setting is disabled"
12100 );
12101 assert!(
12102 !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12103 "Panel should not be focused after toggling with setting disabled"
12104 );
12105 });
12106 }
12107
12108 #[gpui::test]
12109 async fn test_pane_zoom_in_out(cx: &mut TestAppContext) {
12110 init_test(cx);
12111 let fs = FakeFs::new(cx.executor());
12112
12113 let project = Project::test(fs, [], cx).await;
12114 let (workspace, cx) =
12115 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12116
12117 let pane = workspace.update_in(cx, |workspace, _window, _cx| {
12118 workspace.active_pane().clone()
12119 });
12120
12121 // Add an item to the pane so it can be zoomed
12122 workspace.update_in(cx, |workspace, window, cx| {
12123 let item = cx.new(TestItem::new);
12124 workspace.add_item(pane.clone(), Box::new(item), None, true, true, window, cx);
12125 });
12126
12127 // Initially not zoomed
12128 workspace.update_in(cx, |workspace, _window, cx| {
12129 assert!(!pane.read(cx).is_zoomed(), "Pane starts unzoomed");
12130 assert!(
12131 workspace.zoomed.is_none(),
12132 "Workspace should track no zoomed pane"
12133 );
12134 assert!(pane.read(cx).items_len() > 0, "Pane should have items");
12135 });
12136
12137 // Zoom In
12138 pane.update_in(cx, |pane, window, cx| {
12139 pane.zoom_in(&crate::ZoomIn, window, cx);
12140 });
12141
12142 workspace.update_in(cx, |workspace, window, cx| {
12143 assert!(
12144 pane.read(cx).is_zoomed(),
12145 "Pane should be zoomed after ZoomIn"
12146 );
12147 assert!(
12148 workspace.zoomed.is_some(),
12149 "Workspace should track the zoomed pane"
12150 );
12151 assert!(
12152 pane.read(cx).focus_handle(cx).contains_focused(window, cx),
12153 "ZoomIn should focus the pane"
12154 );
12155 });
12156
12157 // Zoom In again is a no-op
12158 pane.update_in(cx, |pane, window, cx| {
12159 pane.zoom_in(&crate::ZoomIn, window, cx);
12160 });
12161
12162 workspace.update_in(cx, |workspace, window, cx| {
12163 assert!(pane.read(cx).is_zoomed(), "Second ZoomIn keeps pane zoomed");
12164 assert!(
12165 workspace.zoomed.is_some(),
12166 "Workspace still tracks zoomed pane"
12167 );
12168 assert!(
12169 pane.read(cx).focus_handle(cx).contains_focused(window, cx),
12170 "Pane remains focused after repeated ZoomIn"
12171 );
12172 });
12173
12174 // Zoom Out
12175 pane.update_in(cx, |pane, window, cx| {
12176 pane.zoom_out(&crate::ZoomOut, window, cx);
12177 });
12178
12179 workspace.update_in(cx, |workspace, _window, cx| {
12180 assert!(
12181 !pane.read(cx).is_zoomed(),
12182 "Pane should unzoom after ZoomOut"
12183 );
12184 assert!(
12185 workspace.zoomed.is_none(),
12186 "Workspace clears zoom tracking after ZoomOut"
12187 );
12188 });
12189
12190 // Zoom Out again is a no-op
12191 pane.update_in(cx, |pane, window, cx| {
12192 pane.zoom_out(&crate::ZoomOut, window, cx);
12193 });
12194
12195 workspace.update_in(cx, |workspace, _window, cx| {
12196 assert!(
12197 !pane.read(cx).is_zoomed(),
12198 "Second ZoomOut keeps pane unzoomed"
12199 );
12200 assert!(
12201 workspace.zoomed.is_none(),
12202 "Workspace remains without zoomed pane"
12203 );
12204 });
12205 }
12206
12207 #[gpui::test]
12208 async fn test_toggle_all_docks(cx: &mut gpui::TestAppContext) {
12209 init_test(cx);
12210 let fs = FakeFs::new(cx.executor());
12211
12212 let project = Project::test(fs, [], cx).await;
12213 let (workspace, cx) =
12214 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12215 workspace.update_in(cx, |workspace, window, cx| {
12216 // Open two docks
12217 let left_dock = workspace.dock_at_position(DockPosition::Left);
12218 let right_dock = workspace.dock_at_position(DockPosition::Right);
12219
12220 left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12221 right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12222
12223 assert!(left_dock.read(cx).is_open());
12224 assert!(right_dock.read(cx).is_open());
12225 });
12226
12227 workspace.update_in(cx, |workspace, window, cx| {
12228 // Toggle all docks - should close both
12229 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12230
12231 let left_dock = workspace.dock_at_position(DockPosition::Left);
12232 let right_dock = workspace.dock_at_position(DockPosition::Right);
12233 assert!(!left_dock.read(cx).is_open());
12234 assert!(!right_dock.read(cx).is_open());
12235 });
12236
12237 workspace.update_in(cx, |workspace, window, cx| {
12238 // Toggle again - should reopen both
12239 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12240
12241 let left_dock = workspace.dock_at_position(DockPosition::Left);
12242 let right_dock = workspace.dock_at_position(DockPosition::Right);
12243 assert!(left_dock.read(cx).is_open());
12244 assert!(right_dock.read(cx).is_open());
12245 });
12246 }
12247
12248 #[gpui::test]
12249 async fn test_toggle_all_with_manual_close(cx: &mut gpui::TestAppContext) {
12250 init_test(cx);
12251 let fs = FakeFs::new(cx.executor());
12252
12253 let project = Project::test(fs, [], cx).await;
12254 let (workspace, cx) =
12255 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12256 workspace.update_in(cx, |workspace, window, cx| {
12257 // Open two docks
12258 let left_dock = workspace.dock_at_position(DockPosition::Left);
12259 let right_dock = workspace.dock_at_position(DockPosition::Right);
12260
12261 left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12262 right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12263
12264 assert!(left_dock.read(cx).is_open());
12265 assert!(right_dock.read(cx).is_open());
12266 });
12267
12268 workspace.update_in(cx, |workspace, window, cx| {
12269 // Close them manually
12270 workspace.toggle_dock(DockPosition::Left, window, cx);
12271 workspace.toggle_dock(DockPosition::Right, window, cx);
12272
12273 let left_dock = workspace.dock_at_position(DockPosition::Left);
12274 let right_dock = workspace.dock_at_position(DockPosition::Right);
12275 assert!(!left_dock.read(cx).is_open());
12276 assert!(!right_dock.read(cx).is_open());
12277 });
12278
12279 workspace.update_in(cx, |workspace, window, cx| {
12280 // Toggle all docks - only last closed (right dock) should reopen
12281 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12282
12283 let left_dock = workspace.dock_at_position(DockPosition::Left);
12284 let right_dock = workspace.dock_at_position(DockPosition::Right);
12285 assert!(!left_dock.read(cx).is_open());
12286 assert!(right_dock.read(cx).is_open());
12287 });
12288 }
12289
12290 #[gpui::test]
12291 async fn test_toggle_all_docks_after_dock_move(cx: &mut gpui::TestAppContext) {
12292 init_test(cx);
12293 let fs = FakeFs::new(cx.executor());
12294 let project = Project::test(fs, [], cx).await;
12295 let (multi_workspace, cx) =
12296 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12297 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12298
12299 // Open two docks (left and right) with one panel each
12300 let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| {
12301 let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
12302 workspace.add_panel(left_panel.clone(), window, cx);
12303
12304 let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
12305 workspace.add_panel(right_panel.clone(), window, cx);
12306
12307 workspace.toggle_dock(DockPosition::Left, window, cx);
12308 workspace.toggle_dock(DockPosition::Right, window, cx);
12309
12310 // Verify initial state
12311 assert!(
12312 workspace.left_dock().read(cx).is_open(),
12313 "Left dock should be open"
12314 );
12315 assert_eq!(
12316 workspace
12317 .left_dock()
12318 .read(cx)
12319 .visible_panel()
12320 .unwrap()
12321 .panel_id(),
12322 left_panel.panel_id(),
12323 "Left panel should be visible in left dock"
12324 );
12325 assert!(
12326 workspace.right_dock().read(cx).is_open(),
12327 "Right dock should be open"
12328 );
12329 assert_eq!(
12330 workspace
12331 .right_dock()
12332 .read(cx)
12333 .visible_panel()
12334 .unwrap()
12335 .panel_id(),
12336 right_panel.panel_id(),
12337 "Right panel should be visible in right dock"
12338 );
12339 assert!(
12340 !workspace.bottom_dock().read(cx).is_open(),
12341 "Bottom dock should be closed"
12342 );
12343
12344 (left_panel, right_panel)
12345 });
12346
12347 // Focus the left panel and move it to the next position (bottom dock)
12348 workspace.update_in(cx, |workspace, window, cx| {
12349 workspace.toggle_panel_focus::<TestPanel>(window, cx); // Focus left panel
12350 assert!(
12351 left_panel.read(cx).focus_handle(cx).is_focused(window),
12352 "Left panel should be focused"
12353 );
12354 });
12355
12356 cx.dispatch_action(MoveFocusedPanelToNextPosition);
12357
12358 // Verify the left panel has moved to the bottom dock, and the bottom dock is now open
12359 workspace.update(cx, |workspace, cx| {
12360 assert!(
12361 !workspace.left_dock().read(cx).is_open(),
12362 "Left dock should be closed"
12363 );
12364 assert!(
12365 workspace.bottom_dock().read(cx).is_open(),
12366 "Bottom dock should now be open"
12367 );
12368 assert_eq!(
12369 left_panel.read(cx).position,
12370 DockPosition::Bottom,
12371 "Left panel should now be in the bottom dock"
12372 );
12373 assert_eq!(
12374 workspace
12375 .bottom_dock()
12376 .read(cx)
12377 .visible_panel()
12378 .unwrap()
12379 .panel_id(),
12380 left_panel.panel_id(),
12381 "Left panel should be the visible panel in the bottom dock"
12382 );
12383 });
12384
12385 // Toggle all docks off
12386 workspace.update_in(cx, |workspace, window, cx| {
12387 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12388 assert!(
12389 !workspace.left_dock().read(cx).is_open(),
12390 "Left dock should be closed"
12391 );
12392 assert!(
12393 !workspace.right_dock().read(cx).is_open(),
12394 "Right dock should be closed"
12395 );
12396 assert!(
12397 !workspace.bottom_dock().read(cx).is_open(),
12398 "Bottom dock should be closed"
12399 );
12400 });
12401
12402 // Toggle all docks back on and verify positions are restored
12403 workspace.update_in(cx, |workspace, window, cx| {
12404 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12405 assert!(
12406 !workspace.left_dock().read(cx).is_open(),
12407 "Left dock should remain closed"
12408 );
12409 assert!(
12410 workspace.right_dock().read(cx).is_open(),
12411 "Right dock should remain open"
12412 );
12413 assert!(
12414 workspace.bottom_dock().read(cx).is_open(),
12415 "Bottom dock should remain open"
12416 );
12417 assert_eq!(
12418 left_panel.read(cx).position,
12419 DockPosition::Bottom,
12420 "Left panel should remain in the bottom dock"
12421 );
12422 assert_eq!(
12423 right_panel.read(cx).position,
12424 DockPosition::Right,
12425 "Right panel should remain in the right dock"
12426 );
12427 assert_eq!(
12428 workspace
12429 .bottom_dock()
12430 .read(cx)
12431 .visible_panel()
12432 .unwrap()
12433 .panel_id(),
12434 left_panel.panel_id(),
12435 "Left panel should be the visible panel in the right dock"
12436 );
12437 });
12438 }
12439
12440 #[gpui::test]
12441 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
12442 init_test(cx);
12443
12444 let fs = FakeFs::new(cx.executor());
12445
12446 let project = Project::test(fs, None, cx).await;
12447 let (workspace, cx) =
12448 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12449
12450 // Let's arrange the panes like this:
12451 //
12452 // +-----------------------+
12453 // | top |
12454 // +------+--------+-------+
12455 // | left | center | right |
12456 // +------+--------+-------+
12457 // | bottom |
12458 // +-----------------------+
12459
12460 let top_item = cx.new(|cx| {
12461 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
12462 });
12463 let bottom_item = cx.new(|cx| {
12464 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
12465 });
12466 let left_item = cx.new(|cx| {
12467 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
12468 });
12469 let right_item = cx.new(|cx| {
12470 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
12471 });
12472 let center_item = cx.new(|cx| {
12473 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
12474 });
12475
12476 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12477 let top_pane_id = workspace.active_pane().entity_id();
12478 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
12479 workspace.split_pane(
12480 workspace.active_pane().clone(),
12481 SplitDirection::Down,
12482 window,
12483 cx,
12484 );
12485 top_pane_id
12486 });
12487 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12488 let bottom_pane_id = workspace.active_pane().entity_id();
12489 workspace.add_item_to_active_pane(
12490 Box::new(bottom_item.clone()),
12491 None,
12492 false,
12493 window,
12494 cx,
12495 );
12496 workspace.split_pane(
12497 workspace.active_pane().clone(),
12498 SplitDirection::Up,
12499 window,
12500 cx,
12501 );
12502 bottom_pane_id
12503 });
12504 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12505 let left_pane_id = workspace.active_pane().entity_id();
12506 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
12507 workspace.split_pane(
12508 workspace.active_pane().clone(),
12509 SplitDirection::Right,
12510 window,
12511 cx,
12512 );
12513 left_pane_id
12514 });
12515 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12516 let right_pane_id = workspace.active_pane().entity_id();
12517 workspace.add_item_to_active_pane(
12518 Box::new(right_item.clone()),
12519 None,
12520 false,
12521 window,
12522 cx,
12523 );
12524 workspace.split_pane(
12525 workspace.active_pane().clone(),
12526 SplitDirection::Left,
12527 window,
12528 cx,
12529 );
12530 right_pane_id
12531 });
12532 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12533 let center_pane_id = workspace.active_pane().entity_id();
12534 workspace.add_item_to_active_pane(
12535 Box::new(center_item.clone()),
12536 None,
12537 false,
12538 window,
12539 cx,
12540 );
12541 center_pane_id
12542 });
12543 cx.executor().run_until_parked();
12544
12545 workspace.update_in(cx, |workspace, window, cx| {
12546 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
12547
12548 // Join into next from center pane into right
12549 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12550 });
12551
12552 workspace.update_in(cx, |workspace, window, cx| {
12553 let active_pane = workspace.active_pane();
12554 assert_eq!(right_pane_id, active_pane.entity_id());
12555 assert_eq!(2, active_pane.read(cx).items_len());
12556 let item_ids_in_pane =
12557 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12558 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12559 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12560
12561 // Join into next from right pane into bottom
12562 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12563 });
12564
12565 workspace.update_in(cx, |workspace, window, cx| {
12566 let active_pane = workspace.active_pane();
12567 assert_eq!(bottom_pane_id, active_pane.entity_id());
12568 assert_eq!(3, active_pane.read(cx).items_len());
12569 let item_ids_in_pane =
12570 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12571 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12572 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12573 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12574
12575 // Join into next from bottom pane into left
12576 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12577 });
12578
12579 workspace.update_in(cx, |workspace, window, cx| {
12580 let active_pane = workspace.active_pane();
12581 assert_eq!(left_pane_id, active_pane.entity_id());
12582 assert_eq!(4, active_pane.read(cx).items_len());
12583 let item_ids_in_pane =
12584 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12585 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12586 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12587 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12588 assert!(item_ids_in_pane.contains(&left_item.item_id()));
12589
12590 // Join into next from left pane into top
12591 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12592 });
12593
12594 workspace.update_in(cx, |workspace, window, cx| {
12595 let active_pane = workspace.active_pane();
12596 assert_eq!(top_pane_id, active_pane.entity_id());
12597 assert_eq!(5, active_pane.read(cx).items_len());
12598 let item_ids_in_pane =
12599 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12600 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
12601 assert!(item_ids_in_pane.contains(&right_item.item_id()));
12602 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12603 assert!(item_ids_in_pane.contains(&left_item.item_id()));
12604 assert!(item_ids_in_pane.contains(&top_item.item_id()));
12605
12606 // Single pane left: no-op
12607 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
12608 });
12609
12610 workspace.update(cx, |workspace, _cx| {
12611 let active_pane = workspace.active_pane();
12612 assert_eq!(top_pane_id, active_pane.entity_id());
12613 });
12614 }
12615
12616 fn add_an_item_to_active_pane(
12617 cx: &mut VisualTestContext,
12618 workspace: &Entity<Workspace>,
12619 item_id: u64,
12620 ) -> Entity<TestItem> {
12621 let item = cx.new(|cx| {
12622 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
12623 item_id,
12624 "item{item_id}.txt",
12625 cx,
12626 )])
12627 });
12628 workspace.update_in(cx, |workspace, window, cx| {
12629 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
12630 });
12631 item
12632 }
12633
12634 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
12635 workspace.update_in(cx, |workspace, window, cx| {
12636 workspace.split_pane(
12637 workspace.active_pane().clone(),
12638 SplitDirection::Right,
12639 window,
12640 cx,
12641 )
12642 })
12643 }
12644
12645 #[gpui::test]
12646 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
12647 init_test(cx);
12648 let fs = FakeFs::new(cx.executor());
12649 let project = Project::test(fs, None, cx).await;
12650 let (workspace, cx) =
12651 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12652
12653 add_an_item_to_active_pane(cx, &workspace, 1);
12654 split_pane(cx, &workspace);
12655 add_an_item_to_active_pane(cx, &workspace, 2);
12656 split_pane(cx, &workspace); // empty pane
12657 split_pane(cx, &workspace);
12658 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
12659
12660 cx.executor().run_until_parked();
12661
12662 workspace.update(cx, |workspace, cx| {
12663 let num_panes = workspace.panes().len();
12664 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
12665 let active_item = workspace
12666 .active_pane()
12667 .read(cx)
12668 .active_item()
12669 .expect("item is in focus");
12670
12671 assert_eq!(num_panes, 4);
12672 assert_eq!(num_items_in_current_pane, 1);
12673 assert_eq!(active_item.item_id(), last_item.item_id());
12674 });
12675
12676 workspace.update_in(cx, |workspace, window, cx| {
12677 workspace.join_all_panes(window, cx);
12678 });
12679
12680 workspace.update(cx, |workspace, cx| {
12681 let num_panes = workspace.panes().len();
12682 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
12683 let active_item = workspace
12684 .active_pane()
12685 .read(cx)
12686 .active_item()
12687 .expect("item is in focus");
12688
12689 assert_eq!(num_panes, 1);
12690 assert_eq!(num_items_in_current_pane, 3);
12691 assert_eq!(active_item.item_id(), last_item.item_id());
12692 });
12693 }
12694
12695 #[gpui::test]
12696 async fn test_flexible_dock_sizing(cx: &mut gpui::TestAppContext) {
12697 init_test(cx);
12698 let fs = FakeFs::new(cx.executor());
12699
12700 let project = Project::test(fs, [], cx).await;
12701 let (multi_workspace, cx) =
12702 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12703 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12704
12705 workspace.update(cx, |workspace, _cx| {
12706 workspace.bounds.size.width = px(800.);
12707 });
12708
12709 workspace.update_in(cx, |workspace, window, cx| {
12710 let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
12711 workspace.add_panel(panel, window, cx);
12712 workspace.toggle_dock(DockPosition::Right, window, cx);
12713 });
12714
12715 let (panel, resized_width, ratio_basis_width) =
12716 workspace.update_in(cx, |workspace, window, cx| {
12717 let item = cx.new(|cx| {
12718 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
12719 });
12720 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
12721
12722 let dock = workspace.right_dock().read(cx);
12723 let workspace_width = workspace.bounds.size.width;
12724 let initial_width = workspace
12725 .dock_size(&dock, window, cx)
12726 .expect("flexible dock should have an initial width");
12727
12728 assert_eq!(initial_width, workspace_width / 2.);
12729
12730 workspace.resize_right_dock(px(300.), window, cx);
12731
12732 let dock = workspace.right_dock().read(cx);
12733 let resized_width = workspace
12734 .dock_size(&dock, window, cx)
12735 .expect("flexible dock should keep its resized width");
12736
12737 assert_eq!(resized_width, px(300.));
12738
12739 let panel = workspace
12740 .right_dock()
12741 .read(cx)
12742 .visible_panel()
12743 .expect("flexible dock should have a visible panel")
12744 .panel_id();
12745
12746 (panel, resized_width, workspace_width)
12747 });
12748
12749 workspace.update_in(cx, |workspace, window, cx| {
12750 workspace.toggle_dock(DockPosition::Right, window, cx);
12751 workspace.toggle_dock(DockPosition::Right, window, cx);
12752
12753 let dock = workspace.right_dock().read(cx);
12754 let reopened_width = workspace
12755 .dock_size(&dock, window, cx)
12756 .expect("flexible dock should restore when reopened");
12757
12758 assert_eq!(reopened_width, resized_width);
12759
12760 let right_dock = workspace.right_dock().read(cx);
12761 let flexible_panel = right_dock
12762 .visible_panel()
12763 .expect("flexible dock should still have a visible panel");
12764 assert_eq!(flexible_panel.panel_id(), panel);
12765 assert_eq!(
12766 right_dock
12767 .stored_panel_size_state(flexible_panel.as_ref())
12768 .and_then(|size_state| size_state.flex),
12769 Some(
12770 resized_width.to_f64() as f32
12771 / (workspace.bounds.size.width - resized_width).to_f64() as f32
12772 )
12773 );
12774 });
12775
12776 workspace.update_in(cx, |workspace, window, cx| {
12777 workspace.split_pane(
12778 workspace.active_pane().clone(),
12779 SplitDirection::Right,
12780 window,
12781 cx,
12782 );
12783
12784 let dock = workspace.right_dock().read(cx);
12785 let split_width = workspace
12786 .dock_size(&dock, window, cx)
12787 .expect("flexible dock should keep its user-resized proportion");
12788
12789 assert_eq!(split_width, px(300.));
12790
12791 workspace.bounds.size.width = px(1600.);
12792
12793 let dock = workspace.right_dock().read(cx);
12794 let resized_window_width = workspace
12795 .dock_size(&dock, window, cx)
12796 .expect("flexible dock should preserve proportional size on window resize");
12797
12798 assert_eq!(
12799 resized_window_width,
12800 workspace.bounds.size.width
12801 * (resized_width.to_f64() as f32 / ratio_basis_width.to_f64() as f32)
12802 );
12803 });
12804 }
12805
12806 #[gpui::test]
12807 async fn test_panel_size_state_persistence(cx: &mut gpui::TestAppContext) {
12808 init_test(cx);
12809 let fs = FakeFs::new(cx.executor());
12810
12811 // Fixed-width panel: pixel size is persisted to KVP and restored on re-add.
12812 {
12813 let project = Project::test(fs.clone(), [], cx).await;
12814 let (multi_workspace, cx) =
12815 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12816 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12817
12818 workspace.update(cx, |workspace, _cx| {
12819 workspace.set_random_database_id();
12820 workspace.bounds.size.width = px(800.);
12821 });
12822
12823 let panel = workspace.update_in(cx, |workspace, window, cx| {
12824 let panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
12825 workspace.add_panel(panel.clone(), window, cx);
12826 workspace.toggle_dock(DockPosition::Left, window, cx);
12827 panel
12828 });
12829
12830 workspace.update_in(cx, |workspace, window, cx| {
12831 workspace.resize_left_dock(px(350.), window, cx);
12832 });
12833
12834 cx.run_until_parked();
12835
12836 let persisted = workspace.read_with(cx, |workspace, cx| {
12837 workspace.persisted_panel_size_state(TestPanel::panel_key(), cx)
12838 });
12839 assert_eq!(
12840 persisted.and_then(|s| s.size),
12841 Some(px(350.)),
12842 "fixed-width panel size should be persisted to KVP"
12843 );
12844
12845 // Remove the panel and re-add a fresh instance with the same key.
12846 // The new instance should have its size state restored from KVP.
12847 workspace.update_in(cx, |workspace, window, cx| {
12848 workspace.remove_panel(&panel, window, cx);
12849 });
12850
12851 workspace.update_in(cx, |workspace, window, cx| {
12852 let new_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
12853 workspace.add_panel(new_panel, window, cx);
12854
12855 let left_dock = workspace.left_dock().read(cx);
12856 let size_state = left_dock
12857 .panel::<TestPanel>()
12858 .and_then(|p| left_dock.stored_panel_size_state(&p));
12859 assert_eq!(
12860 size_state.and_then(|s| s.size),
12861 Some(px(350.)),
12862 "re-added fixed-width panel should restore persisted size from KVP"
12863 );
12864 });
12865 }
12866
12867 // Flexible panel: both pixel size and ratio are persisted and restored.
12868 {
12869 let project = Project::test(fs.clone(), [], cx).await;
12870 let (multi_workspace, cx) =
12871 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12872 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12873
12874 workspace.update(cx, |workspace, _cx| {
12875 workspace.set_random_database_id();
12876 workspace.bounds.size.width = px(800.);
12877 });
12878
12879 let panel = workspace.update_in(cx, |workspace, window, cx| {
12880 let item = cx.new(|cx| {
12881 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
12882 });
12883 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
12884
12885 let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
12886 workspace.add_panel(panel.clone(), window, cx);
12887 workspace.toggle_dock(DockPosition::Right, window, cx);
12888 panel
12889 });
12890
12891 workspace.update_in(cx, |workspace, window, cx| {
12892 workspace.resize_right_dock(px(300.), window, cx);
12893 });
12894
12895 cx.run_until_parked();
12896
12897 let persisted = workspace
12898 .read_with(cx, |workspace, cx| {
12899 workspace.persisted_panel_size_state(TestPanel::panel_key(), cx)
12900 })
12901 .expect("flexible panel state should be persisted to KVP");
12902 assert_eq!(
12903 persisted.size, None,
12904 "flexible panel should not persist a redundant pixel size"
12905 );
12906 let original_ratio = persisted.flex.expect("panel's flex should be persisted");
12907
12908 // Remove the panel and re-add: both size and ratio should be restored.
12909 workspace.update_in(cx, |workspace, window, cx| {
12910 workspace.remove_panel(&panel, window, cx);
12911 });
12912
12913 workspace.update_in(cx, |workspace, window, cx| {
12914 let new_panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
12915 workspace.add_panel(new_panel, window, cx);
12916
12917 let right_dock = workspace.right_dock().read(cx);
12918 let size_state = right_dock
12919 .panel::<TestPanel>()
12920 .and_then(|p| right_dock.stored_panel_size_state(&p))
12921 .expect("re-added flexible panel should have restored size state from KVP");
12922 assert_eq!(
12923 size_state.size, None,
12924 "re-added flexible panel should not have a persisted pixel size"
12925 );
12926 assert_eq!(
12927 size_state.flex,
12928 Some(original_ratio),
12929 "re-added flexible panel should restore persisted flex"
12930 );
12931 });
12932 }
12933 }
12934
12935 #[gpui::test]
12936 async fn test_flexible_panel_left_dock_sizing(cx: &mut gpui::TestAppContext) {
12937 init_test(cx);
12938 let fs = FakeFs::new(cx.executor());
12939
12940 let project = Project::test(fs, [], cx).await;
12941 let (multi_workspace, cx) =
12942 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12943 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12944
12945 workspace.update(cx, |workspace, _cx| {
12946 workspace.bounds.size.width = px(900.);
12947 });
12948
12949 // Step 1: Add a tab to the center pane then open a flexible panel in the left
12950 // dock. With one full-width center pane the default ratio is 0.5, so the panel
12951 // and the center pane each take half the workspace width.
12952 workspace.update_in(cx, |workspace, window, cx| {
12953 let item = cx.new(|cx| {
12954 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
12955 });
12956 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
12957
12958 let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Left, 100, cx));
12959 workspace.add_panel(panel, window, cx);
12960 workspace.toggle_dock(DockPosition::Left, window, cx);
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 have an active panel");
12966
12967 assert_eq!(
12968 left_width,
12969 workspace.bounds.size.width / 2.,
12970 "flexible left panel should split evenly with the center pane"
12971 );
12972 });
12973
12974 // Step 2: Split the center pane vertically (top/bottom). Vertical splits do not
12975 // change horizontal width fractions, so the flexible panel stays at the same
12976 // width as each half of the split.
12977 workspace.update_in(cx, |workspace, window, cx| {
12978 workspace.split_pane(
12979 workspace.active_pane().clone(),
12980 SplitDirection::Down,
12981 window,
12982 cx,
12983 );
12984
12985 let left_dock = workspace.left_dock().read(cx);
12986 let left_width = workspace
12987 .dock_size(&left_dock, window, cx)
12988 .expect("left dock should still have an active panel after vertical split");
12989
12990 assert_eq!(
12991 left_width,
12992 workspace.bounds.size.width / 2.,
12993 "flexible left panel width should match each vertically-split pane"
12994 );
12995 });
12996
12997 // Step 3: Open a fixed-width panel in the right dock. The right dock's default
12998 // size reduces the available width, so the flexible left panel and the center
12999 // panes all shrink proportionally to accommodate it.
13000 workspace.update_in(cx, |workspace, window, cx| {
13001 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 200, cx));
13002 workspace.add_panel(panel, window, cx);
13003 workspace.toggle_dock(DockPosition::Right, window, cx);
13004
13005 let right_dock = workspace.right_dock().read(cx);
13006 let right_width = workspace
13007 .dock_size(&right_dock, window, cx)
13008 .expect("right dock should have an active panel");
13009
13010 let left_dock = workspace.left_dock().read(cx);
13011 let left_width = workspace
13012 .dock_size(&left_dock, window, cx)
13013 .expect("left dock should still have an active panel");
13014
13015 let available_width = workspace.bounds.size.width - right_width;
13016 assert_eq!(
13017 left_width,
13018 available_width / 2.,
13019 "flexible left panel should shrink proportionally as the right dock takes space"
13020 );
13021 });
13022
13023 // Step 4: Toggle the right dock's panel to flexible. Now both docks use
13024 // flex sizing and the workspace width is divided among left-flex, center
13025 // (implicit flex 1.0), and right-flex.
13026 workspace.update_in(cx, |workspace, window, cx| {
13027 let right_dock = workspace.right_dock().clone();
13028 let right_panel = right_dock
13029 .read(cx)
13030 .visible_panel()
13031 .expect("right dock should have a visible panel")
13032 .clone();
13033 workspace.toggle_dock_panel_flexible_size(
13034 &right_dock,
13035 right_panel.as_ref(),
13036 window,
13037 cx,
13038 );
13039
13040 let right_dock = right_dock.read(cx);
13041 let right_panel = right_dock
13042 .visible_panel()
13043 .expect("right dock should still have a visible panel");
13044 assert!(
13045 right_panel.has_flexible_size(window, cx),
13046 "right panel should now be flexible"
13047 );
13048
13049 let right_size_state = right_dock
13050 .stored_panel_size_state(right_panel.as_ref())
13051 .expect("right panel should have a stored size state after toggling");
13052 let right_flex = right_size_state
13053 .flex
13054 .expect("right panel should have a flex value after toggling");
13055
13056 let left_dock = workspace.left_dock().read(cx);
13057 let left_width = workspace
13058 .dock_size(&left_dock, window, cx)
13059 .expect("left dock should still have an active panel");
13060 let right_width = workspace
13061 .dock_size(&right_dock, window, cx)
13062 .expect("right dock should still have an active panel");
13063
13064 let left_flex = workspace
13065 .default_dock_flex(DockPosition::Left)
13066 .expect("left dock should have a default flex");
13067
13068 let total_flex = left_flex + 1.0 + right_flex;
13069 let expected_left = left_flex / total_flex * workspace.bounds.size.width;
13070 let expected_right = right_flex / total_flex * workspace.bounds.size.width;
13071 assert_eq!(
13072 left_width, expected_left,
13073 "flexible left panel should share workspace width via flex ratios"
13074 );
13075 assert_eq!(
13076 right_width, expected_right,
13077 "flexible right panel should share workspace width via flex ratios"
13078 );
13079 });
13080 }
13081
13082 struct TestModal(FocusHandle);
13083
13084 impl TestModal {
13085 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
13086 Self(cx.focus_handle())
13087 }
13088 }
13089
13090 impl EventEmitter<DismissEvent> for TestModal {}
13091
13092 impl Focusable for TestModal {
13093 fn focus_handle(&self, _cx: &App) -> FocusHandle {
13094 self.0.clone()
13095 }
13096 }
13097
13098 impl ModalView for TestModal {}
13099
13100 impl Render for TestModal {
13101 fn render(
13102 &mut self,
13103 _window: &mut Window,
13104 _cx: &mut Context<TestModal>,
13105 ) -> impl IntoElement {
13106 div().track_focus(&self.0)
13107 }
13108 }
13109
13110 #[gpui::test]
13111 async fn test_panels(cx: &mut gpui::TestAppContext) {
13112 init_test(cx);
13113 let fs = FakeFs::new(cx.executor());
13114
13115 let project = Project::test(fs, [], cx).await;
13116 let (multi_workspace, cx) =
13117 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
13118 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
13119
13120 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
13121 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
13122 workspace.add_panel(panel_1.clone(), window, cx);
13123 workspace.toggle_dock(DockPosition::Left, window, cx);
13124 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
13125 workspace.add_panel(panel_2.clone(), window, cx);
13126 workspace.toggle_dock(DockPosition::Right, window, cx);
13127
13128 let left_dock = workspace.left_dock();
13129 assert_eq!(
13130 left_dock.read(cx).visible_panel().unwrap().panel_id(),
13131 panel_1.panel_id()
13132 );
13133 assert_eq!(
13134 workspace.dock_size(&left_dock.read(cx), window, cx),
13135 Some(px(300.))
13136 );
13137
13138 workspace.resize_left_dock(px(1337.), window, cx);
13139 assert_eq!(
13140 workspace
13141 .right_dock()
13142 .read(cx)
13143 .visible_panel()
13144 .unwrap()
13145 .panel_id(),
13146 panel_2.panel_id(),
13147 );
13148
13149 (panel_1, panel_2)
13150 });
13151
13152 // Move panel_1 to the right
13153 panel_1.update_in(cx, |panel_1, window, cx| {
13154 panel_1.set_position(DockPosition::Right, window, cx)
13155 });
13156
13157 workspace.update_in(cx, |workspace, window, cx| {
13158 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
13159 // Since it was the only panel on the left, the left dock should now be closed.
13160 assert!(!workspace.left_dock().read(cx).is_open());
13161 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
13162 let right_dock = workspace.right_dock();
13163 assert_eq!(
13164 right_dock.read(cx).visible_panel().unwrap().panel_id(),
13165 panel_1.panel_id()
13166 );
13167 assert_eq!(
13168 right_dock
13169 .read(cx)
13170 .active_panel_size()
13171 .unwrap()
13172 .size
13173 .unwrap(),
13174 px(1337.)
13175 );
13176
13177 // Now we move panel_2 to the left
13178 panel_2.set_position(DockPosition::Left, window, cx);
13179 });
13180
13181 workspace.update(cx, |workspace, cx| {
13182 // Since panel_2 was not visible on the right, we don't open the left dock.
13183 assert!(!workspace.left_dock().read(cx).is_open());
13184 // And the right dock is unaffected in its displaying of panel_1
13185 assert!(workspace.right_dock().read(cx).is_open());
13186 assert_eq!(
13187 workspace
13188 .right_dock()
13189 .read(cx)
13190 .visible_panel()
13191 .unwrap()
13192 .panel_id(),
13193 panel_1.panel_id(),
13194 );
13195 });
13196
13197 // Move panel_1 back to the left
13198 panel_1.update_in(cx, |panel_1, window, cx| {
13199 panel_1.set_position(DockPosition::Left, window, cx)
13200 });
13201
13202 workspace.update_in(cx, |workspace, window, cx| {
13203 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
13204 let left_dock = workspace.left_dock();
13205 assert!(left_dock.read(cx).is_open());
13206 assert_eq!(
13207 left_dock.read(cx).visible_panel().unwrap().panel_id(),
13208 panel_1.panel_id()
13209 );
13210 assert_eq!(
13211 workspace.dock_size(&left_dock.read(cx), window, cx),
13212 Some(px(1337.))
13213 );
13214 // And the right dock should be closed as it no longer has any panels.
13215 assert!(!workspace.right_dock().read(cx).is_open());
13216
13217 // Now we move panel_1 to the bottom
13218 panel_1.set_position(DockPosition::Bottom, window, cx);
13219 });
13220
13221 workspace.update_in(cx, |workspace, window, cx| {
13222 // Since panel_1 was visible on the left, we close the left dock.
13223 assert!(!workspace.left_dock().read(cx).is_open());
13224 // The bottom dock is sized based on the panel's default size,
13225 // since the panel orientation changed from vertical to horizontal.
13226 let bottom_dock = workspace.bottom_dock();
13227 assert_eq!(
13228 workspace.dock_size(&bottom_dock.read(cx), window, cx),
13229 Some(px(300.))
13230 );
13231 // Close bottom dock and move panel_1 back to the left.
13232 bottom_dock.update(cx, |bottom_dock, cx| {
13233 bottom_dock.set_open(false, window, cx)
13234 });
13235 panel_1.set_position(DockPosition::Left, window, cx);
13236 });
13237
13238 // Emit activated event on panel 1
13239 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
13240
13241 // Now the left dock is open and panel_1 is active and focused.
13242 workspace.update_in(cx, |workspace, window, cx| {
13243 let left_dock = workspace.left_dock();
13244 assert!(left_dock.read(cx).is_open());
13245 assert_eq!(
13246 left_dock.read(cx).visible_panel().unwrap().panel_id(),
13247 panel_1.panel_id(),
13248 );
13249 assert!(panel_1.focus_handle(cx).is_focused(window));
13250 });
13251
13252 // Emit closed event on panel 2, which is not active
13253 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
13254
13255 // Wo don't close the left dock, because panel_2 wasn't the active panel
13256 workspace.update(cx, |workspace, cx| {
13257 let left_dock = workspace.left_dock();
13258 assert!(left_dock.read(cx).is_open());
13259 assert_eq!(
13260 left_dock.read(cx).visible_panel().unwrap().panel_id(),
13261 panel_1.panel_id(),
13262 );
13263 });
13264
13265 // Emitting a ZoomIn event shows the panel as zoomed.
13266 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
13267 workspace.read_with(cx, |workspace, _| {
13268 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13269 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
13270 });
13271
13272 // Move panel to another dock while it is zoomed
13273 panel_1.update_in(cx, |panel, window, cx| {
13274 panel.set_position(DockPosition::Right, window, cx)
13275 });
13276 workspace.read_with(cx, |workspace, _| {
13277 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13278
13279 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
13280 });
13281
13282 // This is a helper for getting a:
13283 // - valid focus on an element,
13284 // - that isn't a part of the panes and panels system of the Workspace,
13285 // - and doesn't trigger the 'on_focus_lost' API.
13286 let focus_other_view = {
13287 let workspace = workspace.clone();
13288 move |cx: &mut VisualTestContext| {
13289 workspace.update_in(cx, |workspace, window, cx| {
13290 if workspace.active_modal::<TestModal>(cx).is_some() {
13291 workspace.toggle_modal(window, cx, TestModal::new);
13292 workspace.toggle_modal(window, cx, TestModal::new);
13293 } else {
13294 workspace.toggle_modal(window, cx, TestModal::new);
13295 }
13296 })
13297 }
13298 };
13299
13300 // If focus is transferred to another view that's not a panel or another pane, we still show
13301 // the panel as zoomed.
13302 focus_other_view(cx);
13303 workspace.read_with(cx, |workspace, _| {
13304 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13305 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
13306 });
13307
13308 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
13309 workspace.update_in(cx, |_workspace, window, cx| {
13310 cx.focus_self(window);
13311 });
13312 workspace.read_with(cx, |workspace, _| {
13313 assert_eq!(workspace.zoomed, None);
13314 assert_eq!(workspace.zoomed_position, None);
13315 });
13316
13317 // If focus is transferred again to another view that's not a panel or a pane, we won't
13318 // show the panel as zoomed because it wasn't zoomed before.
13319 focus_other_view(cx);
13320 workspace.read_with(cx, |workspace, _| {
13321 assert_eq!(workspace.zoomed, None);
13322 assert_eq!(workspace.zoomed_position, None);
13323 });
13324
13325 // When the panel is activated, it is zoomed again.
13326 cx.dispatch_action(ToggleRightDock);
13327 workspace.read_with(cx, |workspace, _| {
13328 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13329 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
13330 });
13331
13332 // Emitting a ZoomOut event unzooms the panel.
13333 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
13334 workspace.read_with(cx, |workspace, _| {
13335 assert_eq!(workspace.zoomed, None);
13336 assert_eq!(workspace.zoomed_position, None);
13337 });
13338
13339 // Emit closed event on panel 1, which is active
13340 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
13341
13342 // Now the left dock is closed, because panel_1 was the active panel
13343 workspace.update(cx, |workspace, cx| {
13344 let right_dock = workspace.right_dock();
13345 assert!(!right_dock.read(cx).is_open());
13346 });
13347 }
13348
13349 #[gpui::test]
13350 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
13351 init_test(cx);
13352
13353 let fs = FakeFs::new(cx.background_executor.clone());
13354 let project = Project::test(fs, [], cx).await;
13355 let (workspace, cx) =
13356 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13357 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13358
13359 let dirty_regular_buffer = cx.new(|cx| {
13360 TestItem::new(cx)
13361 .with_dirty(true)
13362 .with_label("1.txt")
13363 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
13364 });
13365 let dirty_regular_buffer_2 = cx.new(|cx| {
13366 TestItem::new(cx)
13367 .with_dirty(true)
13368 .with_label("2.txt")
13369 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
13370 });
13371 let dirty_multi_buffer_with_both = cx.new(|cx| {
13372 TestItem::new(cx)
13373 .with_dirty(true)
13374 .with_buffer_kind(ItemBufferKind::Multibuffer)
13375 .with_label("Fake Project Search")
13376 .with_project_items(&[
13377 dirty_regular_buffer.read(cx).project_items[0].clone(),
13378 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
13379 ])
13380 });
13381 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
13382 workspace.update_in(cx, |workspace, window, cx| {
13383 workspace.add_item(
13384 pane.clone(),
13385 Box::new(dirty_regular_buffer.clone()),
13386 None,
13387 false,
13388 false,
13389 window,
13390 cx,
13391 );
13392 workspace.add_item(
13393 pane.clone(),
13394 Box::new(dirty_regular_buffer_2.clone()),
13395 None,
13396 false,
13397 false,
13398 window,
13399 cx,
13400 );
13401 workspace.add_item(
13402 pane.clone(),
13403 Box::new(dirty_multi_buffer_with_both.clone()),
13404 None,
13405 false,
13406 false,
13407 window,
13408 cx,
13409 );
13410 });
13411
13412 pane.update_in(cx, |pane, window, cx| {
13413 pane.activate_item(2, true, true, window, cx);
13414 assert_eq!(
13415 pane.active_item().unwrap().item_id(),
13416 multi_buffer_with_both_files_id,
13417 "Should select the multi buffer in the pane"
13418 );
13419 });
13420 let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13421 pane.close_other_items(
13422 &CloseOtherItems {
13423 save_intent: Some(SaveIntent::Save),
13424 close_pinned: true,
13425 },
13426 None,
13427 window,
13428 cx,
13429 )
13430 });
13431 cx.background_executor.run_until_parked();
13432 assert!(!cx.has_pending_prompt());
13433 close_all_but_multi_buffer_task
13434 .await
13435 .expect("Closing all buffers but the multi buffer failed");
13436 pane.update(cx, |pane, cx| {
13437 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
13438 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
13439 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
13440 assert_eq!(pane.items_len(), 1);
13441 assert_eq!(
13442 pane.active_item().unwrap().item_id(),
13443 multi_buffer_with_both_files_id,
13444 "Should have only the multi buffer left in the pane"
13445 );
13446 assert!(
13447 dirty_multi_buffer_with_both.read(cx).is_dirty,
13448 "The multi buffer containing the unsaved buffer should still be dirty"
13449 );
13450 });
13451
13452 dirty_regular_buffer.update(cx, |buffer, cx| {
13453 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
13454 });
13455
13456 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13457 pane.close_active_item(
13458 &CloseActiveItem {
13459 save_intent: Some(SaveIntent::Close),
13460 close_pinned: false,
13461 },
13462 window,
13463 cx,
13464 )
13465 });
13466 cx.background_executor.run_until_parked();
13467 assert!(
13468 cx.has_pending_prompt(),
13469 "Dirty multi buffer should prompt a save dialog"
13470 );
13471 cx.simulate_prompt_answer("Save");
13472 cx.background_executor.run_until_parked();
13473 close_multi_buffer_task
13474 .await
13475 .expect("Closing the multi buffer failed");
13476 pane.update(cx, |pane, cx| {
13477 assert_eq!(
13478 dirty_multi_buffer_with_both.read(cx).save_count,
13479 1,
13480 "Multi buffer item should get be saved"
13481 );
13482 // Test impl does not save inner items, so we do not assert them
13483 assert_eq!(
13484 pane.items_len(),
13485 0,
13486 "No more items should be left in the pane"
13487 );
13488 assert!(pane.active_item().is_none());
13489 });
13490 }
13491
13492 #[gpui::test]
13493 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
13494 cx: &mut TestAppContext,
13495 ) {
13496 init_test(cx);
13497
13498 let fs = FakeFs::new(cx.background_executor.clone());
13499 let project = Project::test(fs, [], cx).await;
13500 let (workspace, cx) =
13501 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13502 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13503
13504 let dirty_regular_buffer = cx.new(|cx| {
13505 TestItem::new(cx)
13506 .with_dirty(true)
13507 .with_label("1.txt")
13508 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
13509 });
13510 let dirty_regular_buffer_2 = cx.new(|cx| {
13511 TestItem::new(cx)
13512 .with_dirty(true)
13513 .with_label("2.txt")
13514 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
13515 });
13516 let clear_regular_buffer = cx.new(|cx| {
13517 TestItem::new(cx)
13518 .with_label("3.txt")
13519 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
13520 });
13521
13522 let dirty_multi_buffer_with_both = cx.new(|cx| {
13523 TestItem::new(cx)
13524 .with_dirty(true)
13525 .with_buffer_kind(ItemBufferKind::Multibuffer)
13526 .with_label("Fake Project Search")
13527 .with_project_items(&[
13528 dirty_regular_buffer.read(cx).project_items[0].clone(),
13529 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
13530 clear_regular_buffer.read(cx).project_items[0].clone(),
13531 ])
13532 });
13533 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
13534 workspace.update_in(cx, |workspace, window, cx| {
13535 workspace.add_item(
13536 pane.clone(),
13537 Box::new(dirty_regular_buffer.clone()),
13538 None,
13539 false,
13540 false,
13541 window,
13542 cx,
13543 );
13544 workspace.add_item(
13545 pane.clone(),
13546 Box::new(dirty_multi_buffer_with_both.clone()),
13547 None,
13548 false,
13549 false,
13550 window,
13551 cx,
13552 );
13553 });
13554
13555 pane.update_in(cx, |pane, window, cx| {
13556 pane.activate_item(1, true, true, window, cx);
13557 assert_eq!(
13558 pane.active_item().unwrap().item_id(),
13559 multi_buffer_with_both_files_id,
13560 "Should select the multi buffer in the pane"
13561 );
13562 });
13563 let _close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13564 pane.close_active_item(
13565 &CloseActiveItem {
13566 save_intent: None,
13567 close_pinned: false,
13568 },
13569 window,
13570 cx,
13571 )
13572 });
13573 cx.background_executor.run_until_parked();
13574 assert!(
13575 cx.has_pending_prompt(),
13576 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
13577 );
13578 }
13579
13580 /// Tests that when `close_on_file_delete` is enabled, files are automatically
13581 /// closed when they are deleted from disk.
13582 #[gpui::test]
13583 async fn test_close_on_disk_deletion_enabled(cx: &mut TestAppContext) {
13584 init_test(cx);
13585
13586 // Enable the close_on_disk_deletion setting
13587 cx.update_global(|store: &mut SettingsStore, cx| {
13588 store.update_user_settings(cx, |settings| {
13589 settings.workspace.close_on_file_delete = Some(true);
13590 });
13591 });
13592
13593 let fs = FakeFs::new(cx.background_executor.clone());
13594 let project = Project::test(fs, [], cx).await;
13595 let (workspace, cx) =
13596 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13597 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13598
13599 // Create a test item that simulates a file
13600 let item = cx.new(|cx| {
13601 TestItem::new(cx)
13602 .with_label("test.txt")
13603 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13604 });
13605
13606 // Add item to workspace
13607 workspace.update_in(cx, |workspace, window, cx| {
13608 workspace.add_item(
13609 pane.clone(),
13610 Box::new(item.clone()),
13611 None,
13612 false,
13613 false,
13614 window,
13615 cx,
13616 );
13617 });
13618
13619 // Verify the item is in the pane
13620 pane.read_with(cx, |pane, _| {
13621 assert_eq!(pane.items().count(), 1);
13622 });
13623
13624 // Simulate file deletion by setting the item's deleted state
13625 item.update(cx, |item, _| {
13626 item.set_has_deleted_file(true);
13627 });
13628
13629 // Emit UpdateTab event to trigger the close behavior
13630 cx.run_until_parked();
13631 item.update(cx, |_, cx| {
13632 cx.emit(ItemEvent::UpdateTab);
13633 });
13634
13635 // Allow the close operation to complete
13636 cx.run_until_parked();
13637
13638 // Verify the item was automatically closed
13639 pane.read_with(cx, |pane, _| {
13640 assert_eq!(
13641 pane.items().count(),
13642 0,
13643 "Item should be automatically closed when file is deleted"
13644 );
13645 });
13646 }
13647
13648 /// Tests that when `close_on_file_delete` is disabled (default), files remain
13649 /// open with a strikethrough when they are deleted from disk.
13650 #[gpui::test]
13651 async fn test_close_on_disk_deletion_disabled(cx: &mut TestAppContext) {
13652 init_test(cx);
13653
13654 // Ensure close_on_disk_deletion is disabled (default)
13655 cx.update_global(|store: &mut SettingsStore, cx| {
13656 store.update_user_settings(cx, |settings| {
13657 settings.workspace.close_on_file_delete = Some(false);
13658 });
13659 });
13660
13661 let fs = FakeFs::new(cx.background_executor.clone());
13662 let project = Project::test(fs, [], cx).await;
13663 let (workspace, cx) =
13664 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13665 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13666
13667 // Create a test item that simulates a file
13668 let item = cx.new(|cx| {
13669 TestItem::new(cx)
13670 .with_label("test.txt")
13671 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13672 });
13673
13674 // Add item to workspace
13675 workspace.update_in(cx, |workspace, window, cx| {
13676 workspace.add_item(
13677 pane.clone(),
13678 Box::new(item.clone()),
13679 None,
13680 false,
13681 false,
13682 window,
13683 cx,
13684 );
13685 });
13686
13687 // Verify the item is in the pane
13688 pane.read_with(cx, |pane, _| {
13689 assert_eq!(pane.items().count(), 1);
13690 });
13691
13692 // Simulate file deletion
13693 item.update(cx, |item, _| {
13694 item.set_has_deleted_file(true);
13695 });
13696
13697 // Emit UpdateTab event
13698 cx.run_until_parked();
13699 item.update(cx, |_, cx| {
13700 cx.emit(ItemEvent::UpdateTab);
13701 });
13702
13703 // Allow any potential close operation to complete
13704 cx.run_until_parked();
13705
13706 // Verify the item remains open (with strikethrough)
13707 pane.read_with(cx, |pane, _| {
13708 assert_eq!(
13709 pane.items().count(),
13710 1,
13711 "Item should remain open when close_on_disk_deletion is disabled"
13712 );
13713 });
13714
13715 // Verify the item shows as deleted
13716 item.read_with(cx, |item, _| {
13717 assert!(
13718 item.has_deleted_file,
13719 "Item should be marked as having deleted file"
13720 );
13721 });
13722 }
13723
13724 /// Tests that dirty files are not automatically closed when deleted from disk,
13725 /// even when `close_on_file_delete` is enabled. This ensures users don't lose
13726 /// unsaved changes without being prompted.
13727 #[gpui::test]
13728 async fn test_close_on_disk_deletion_with_dirty_file(cx: &mut TestAppContext) {
13729 init_test(cx);
13730
13731 // Enable the close_on_file_delete setting
13732 cx.update_global(|store: &mut SettingsStore, cx| {
13733 store.update_user_settings(cx, |settings| {
13734 settings.workspace.close_on_file_delete = Some(true);
13735 });
13736 });
13737
13738 let fs = FakeFs::new(cx.background_executor.clone());
13739 let project = Project::test(fs, [], cx).await;
13740 let (workspace, cx) =
13741 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13742 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13743
13744 // Create a dirty test item
13745 let item = cx.new(|cx| {
13746 TestItem::new(cx)
13747 .with_dirty(true)
13748 .with_label("test.txt")
13749 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13750 });
13751
13752 // Add item to workspace
13753 workspace.update_in(cx, |workspace, window, cx| {
13754 workspace.add_item(
13755 pane.clone(),
13756 Box::new(item.clone()),
13757 None,
13758 false,
13759 false,
13760 window,
13761 cx,
13762 );
13763 });
13764
13765 // Simulate file deletion
13766 item.update(cx, |item, _| {
13767 item.set_has_deleted_file(true);
13768 });
13769
13770 // Emit UpdateTab event to trigger the close behavior
13771 cx.run_until_parked();
13772 item.update(cx, |_, cx| {
13773 cx.emit(ItemEvent::UpdateTab);
13774 });
13775
13776 // Allow any potential close operation to complete
13777 cx.run_until_parked();
13778
13779 // Verify the item remains open (dirty files are not auto-closed)
13780 pane.read_with(cx, |pane, _| {
13781 assert_eq!(
13782 pane.items().count(),
13783 1,
13784 "Dirty items should not be automatically closed even when file is deleted"
13785 );
13786 });
13787
13788 // Verify the item is marked as deleted and still dirty
13789 item.read_with(cx, |item, _| {
13790 assert!(
13791 item.has_deleted_file,
13792 "Item should be marked as having deleted file"
13793 );
13794 assert!(item.is_dirty, "Item should still be dirty");
13795 });
13796 }
13797
13798 /// Tests that navigation history is cleaned up when files are auto-closed
13799 /// due to deletion from disk.
13800 #[gpui::test]
13801 async fn test_close_on_disk_deletion_cleans_navigation_history(cx: &mut TestAppContext) {
13802 init_test(cx);
13803
13804 // Enable the close_on_file_delete setting
13805 cx.update_global(|store: &mut SettingsStore, cx| {
13806 store.update_user_settings(cx, |settings| {
13807 settings.workspace.close_on_file_delete = Some(true);
13808 });
13809 });
13810
13811 let fs = FakeFs::new(cx.background_executor.clone());
13812 let project = Project::test(fs, [], cx).await;
13813 let (workspace, cx) =
13814 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13815 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13816
13817 // Create test items
13818 let item1 = cx.new(|cx| {
13819 TestItem::new(cx)
13820 .with_label("test1.txt")
13821 .with_project_items(&[TestProjectItem::new(1, "test1.txt", cx)])
13822 });
13823 let item1_id = item1.item_id();
13824
13825 let item2 = cx.new(|cx| {
13826 TestItem::new(cx)
13827 .with_label("test2.txt")
13828 .with_project_items(&[TestProjectItem::new(2, "test2.txt", cx)])
13829 });
13830
13831 // Add items to workspace
13832 workspace.update_in(cx, |workspace, window, cx| {
13833 workspace.add_item(
13834 pane.clone(),
13835 Box::new(item1.clone()),
13836 None,
13837 false,
13838 false,
13839 window,
13840 cx,
13841 );
13842 workspace.add_item(
13843 pane.clone(),
13844 Box::new(item2.clone()),
13845 None,
13846 false,
13847 false,
13848 window,
13849 cx,
13850 );
13851 });
13852
13853 // Activate item1 to ensure it gets navigation entries
13854 pane.update_in(cx, |pane, window, cx| {
13855 pane.activate_item(0, true, true, window, cx);
13856 });
13857
13858 // Switch to item2 and back to create navigation history
13859 pane.update_in(cx, |pane, window, cx| {
13860 pane.activate_item(1, true, true, window, cx);
13861 });
13862 cx.run_until_parked();
13863
13864 pane.update_in(cx, |pane, window, cx| {
13865 pane.activate_item(0, true, true, window, cx);
13866 });
13867 cx.run_until_parked();
13868
13869 // Simulate file deletion for item1
13870 item1.update(cx, |item, _| {
13871 item.set_has_deleted_file(true);
13872 });
13873
13874 // Emit UpdateTab event to trigger the close behavior
13875 item1.update(cx, |_, cx| {
13876 cx.emit(ItemEvent::UpdateTab);
13877 });
13878 cx.run_until_parked();
13879
13880 // Verify item1 was closed
13881 pane.read_with(cx, |pane, _| {
13882 assert_eq!(
13883 pane.items().count(),
13884 1,
13885 "Should have 1 item remaining after auto-close"
13886 );
13887 });
13888
13889 // Check navigation history after close
13890 let has_item = pane.read_with(cx, |pane, cx| {
13891 let mut has_item = false;
13892 pane.nav_history().for_each_entry(cx, &mut |entry, _| {
13893 if entry.item.id() == item1_id {
13894 has_item = true;
13895 }
13896 });
13897 has_item
13898 });
13899
13900 assert!(
13901 !has_item,
13902 "Navigation history should not contain closed item entries"
13903 );
13904 }
13905
13906 #[gpui::test]
13907 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
13908 cx: &mut TestAppContext,
13909 ) {
13910 init_test(cx);
13911
13912 let fs = FakeFs::new(cx.background_executor.clone());
13913 let project = Project::test(fs, [], cx).await;
13914 let (workspace, cx) =
13915 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13916 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13917
13918 let dirty_regular_buffer = cx.new(|cx| {
13919 TestItem::new(cx)
13920 .with_dirty(true)
13921 .with_label("1.txt")
13922 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
13923 });
13924 let dirty_regular_buffer_2 = cx.new(|cx| {
13925 TestItem::new(cx)
13926 .with_dirty(true)
13927 .with_label("2.txt")
13928 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
13929 });
13930 let clear_regular_buffer = cx.new(|cx| {
13931 TestItem::new(cx)
13932 .with_label("3.txt")
13933 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
13934 });
13935
13936 let dirty_multi_buffer = cx.new(|cx| {
13937 TestItem::new(cx)
13938 .with_dirty(true)
13939 .with_buffer_kind(ItemBufferKind::Multibuffer)
13940 .with_label("Fake Project Search")
13941 .with_project_items(&[
13942 dirty_regular_buffer.read(cx).project_items[0].clone(),
13943 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
13944 clear_regular_buffer.read(cx).project_items[0].clone(),
13945 ])
13946 });
13947 workspace.update_in(cx, |workspace, window, cx| {
13948 workspace.add_item(
13949 pane.clone(),
13950 Box::new(dirty_regular_buffer.clone()),
13951 None,
13952 false,
13953 false,
13954 window,
13955 cx,
13956 );
13957 workspace.add_item(
13958 pane.clone(),
13959 Box::new(dirty_regular_buffer_2.clone()),
13960 None,
13961 false,
13962 false,
13963 window,
13964 cx,
13965 );
13966 workspace.add_item(
13967 pane.clone(),
13968 Box::new(dirty_multi_buffer.clone()),
13969 None,
13970 false,
13971 false,
13972 window,
13973 cx,
13974 );
13975 });
13976
13977 pane.update_in(cx, |pane, window, cx| {
13978 pane.activate_item(2, true, true, window, cx);
13979 assert_eq!(
13980 pane.active_item().unwrap().item_id(),
13981 dirty_multi_buffer.item_id(),
13982 "Should select the multi buffer in the pane"
13983 );
13984 });
13985 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13986 pane.close_active_item(
13987 &CloseActiveItem {
13988 save_intent: None,
13989 close_pinned: false,
13990 },
13991 window,
13992 cx,
13993 )
13994 });
13995 cx.background_executor.run_until_parked();
13996 assert!(
13997 !cx.has_pending_prompt(),
13998 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
13999 );
14000 close_multi_buffer_task
14001 .await
14002 .expect("Closing multi buffer failed");
14003 pane.update(cx, |pane, cx| {
14004 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
14005 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
14006 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
14007 assert_eq!(
14008 pane.items()
14009 .map(|item| item.item_id())
14010 .sorted()
14011 .collect::<Vec<_>>(),
14012 vec![
14013 dirty_regular_buffer.item_id(),
14014 dirty_regular_buffer_2.item_id(),
14015 ],
14016 "Should have no multi buffer left in the pane"
14017 );
14018 assert!(dirty_regular_buffer.read(cx).is_dirty);
14019 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
14020 });
14021 }
14022
14023 #[gpui::test]
14024 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
14025 init_test(cx);
14026 let fs = FakeFs::new(cx.executor());
14027 let project = Project::test(fs, [], cx).await;
14028 let (multi_workspace, cx) =
14029 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
14030 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
14031
14032 // Add a new panel to the right dock, opening the dock and setting the
14033 // focus to the new panel.
14034 let panel = workspace.update_in(cx, |workspace, window, cx| {
14035 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14036 workspace.add_panel(panel.clone(), window, cx);
14037
14038 workspace
14039 .right_dock()
14040 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
14041
14042 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14043
14044 panel
14045 });
14046
14047 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
14048 // panel to the next valid position which, in this case, is the left
14049 // dock.
14050 cx.dispatch_action(MoveFocusedPanelToNextPosition);
14051 workspace.update(cx, |workspace, cx| {
14052 assert!(workspace.left_dock().read(cx).is_open());
14053 assert_eq!(panel.read(cx).position, DockPosition::Left);
14054 });
14055
14056 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
14057 // panel to the next valid position which, in this case, is the bottom
14058 // dock.
14059 cx.dispatch_action(MoveFocusedPanelToNextPosition);
14060 workspace.update(cx, |workspace, cx| {
14061 assert!(workspace.bottom_dock().read(cx).is_open());
14062 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
14063 });
14064
14065 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
14066 // around moving the panel to its initial position, the right dock.
14067 cx.dispatch_action(MoveFocusedPanelToNextPosition);
14068 workspace.update(cx, |workspace, cx| {
14069 assert!(workspace.right_dock().read(cx).is_open());
14070 assert_eq!(panel.read(cx).position, DockPosition::Right);
14071 });
14072
14073 // Remove focus from the panel, ensuring that, if the panel is not
14074 // focused, the `MoveFocusedPanelToNextPosition` action does not update
14075 // the panel's position, so the panel is still in the right dock.
14076 workspace.update_in(cx, |workspace, window, cx| {
14077 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14078 });
14079
14080 cx.dispatch_action(MoveFocusedPanelToNextPosition);
14081 workspace.update(cx, |workspace, cx| {
14082 assert!(workspace.right_dock().read(cx).is_open());
14083 assert_eq!(panel.read(cx).position, DockPosition::Right);
14084 });
14085 }
14086
14087 #[gpui::test]
14088 async fn test_moving_items_create_panes(cx: &mut TestAppContext) {
14089 init_test(cx);
14090
14091 let fs = FakeFs::new(cx.executor());
14092 let project = Project::test(fs, [], cx).await;
14093 let (workspace, cx) =
14094 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14095
14096 let item_1 = cx.new(|cx| {
14097 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
14098 });
14099 workspace.update_in(cx, |workspace, window, cx| {
14100 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
14101 workspace.move_item_to_pane_in_direction(
14102 &MoveItemToPaneInDirection {
14103 direction: SplitDirection::Right,
14104 focus: true,
14105 clone: false,
14106 },
14107 window,
14108 cx,
14109 );
14110 workspace.move_item_to_pane_at_index(
14111 &MoveItemToPane {
14112 destination: 3,
14113 focus: true,
14114 clone: false,
14115 },
14116 window,
14117 cx,
14118 );
14119
14120 assert_eq!(workspace.panes.len(), 1, "No new panes were created");
14121 assert_eq!(
14122 pane_items_paths(&workspace.active_pane, cx),
14123 vec!["first.txt".to_string()],
14124 "Single item was not moved anywhere"
14125 );
14126 });
14127
14128 let item_2 = cx.new(|cx| {
14129 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "second.txt", cx)])
14130 });
14131 workspace.update_in(cx, |workspace, window, cx| {
14132 workspace.add_item_to_active_pane(Box::new(item_2), None, true, window, cx);
14133 assert_eq!(
14134 pane_items_paths(&workspace.panes[0], cx),
14135 vec!["first.txt".to_string(), "second.txt".to_string()],
14136 );
14137 workspace.move_item_to_pane_in_direction(
14138 &MoveItemToPaneInDirection {
14139 direction: SplitDirection::Right,
14140 focus: true,
14141 clone: false,
14142 },
14143 window,
14144 cx,
14145 );
14146
14147 assert_eq!(workspace.panes.len(), 2, "A new pane should be created");
14148 assert_eq!(
14149 pane_items_paths(&workspace.panes[0], cx),
14150 vec!["first.txt".to_string()],
14151 "After moving, one item should be left in the original pane"
14152 );
14153 assert_eq!(
14154 pane_items_paths(&workspace.panes[1], cx),
14155 vec!["second.txt".to_string()],
14156 "New item should have been moved to the new pane"
14157 );
14158 });
14159
14160 let item_3 = cx.new(|cx| {
14161 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "third.txt", cx)])
14162 });
14163 workspace.update_in(cx, |workspace, window, cx| {
14164 let original_pane = workspace.panes[0].clone();
14165 workspace.set_active_pane(&original_pane, window, cx);
14166 workspace.add_item_to_active_pane(Box::new(item_3), None, true, window, cx);
14167 assert_eq!(workspace.panes.len(), 2, "No new panes were created");
14168 assert_eq!(
14169 pane_items_paths(&workspace.active_pane, cx),
14170 vec!["first.txt".to_string(), "third.txt".to_string()],
14171 "New pane should be ready to move one item out"
14172 );
14173
14174 workspace.move_item_to_pane_at_index(
14175 &MoveItemToPane {
14176 destination: 3,
14177 focus: true,
14178 clone: false,
14179 },
14180 window,
14181 cx,
14182 );
14183 assert_eq!(workspace.panes.len(), 3, "A new pane should be created");
14184 assert_eq!(
14185 pane_items_paths(&workspace.active_pane, cx),
14186 vec!["first.txt".to_string()],
14187 "After moving, one item should be left in the original pane"
14188 );
14189 assert_eq!(
14190 pane_items_paths(&workspace.panes[1], cx),
14191 vec!["second.txt".to_string()],
14192 "Previously created pane should be unchanged"
14193 );
14194 assert_eq!(
14195 pane_items_paths(&workspace.panes[2], cx),
14196 vec!["third.txt".to_string()],
14197 "New item should have been moved to the new pane"
14198 );
14199 });
14200 }
14201
14202 #[gpui::test]
14203 async fn test_moving_items_can_clone_panes(cx: &mut TestAppContext) {
14204 init_test(cx);
14205
14206 let fs = FakeFs::new(cx.executor());
14207 let project = Project::test(fs, [], cx).await;
14208 let (workspace, cx) =
14209 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14210
14211 let item_1 = cx.new(|cx| {
14212 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
14213 });
14214 workspace.update_in(cx, |workspace, window, cx| {
14215 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
14216 workspace.move_item_to_pane_in_direction(
14217 &MoveItemToPaneInDirection {
14218 direction: SplitDirection::Right,
14219 focus: true,
14220 clone: true,
14221 },
14222 window,
14223 cx,
14224 );
14225 });
14226 cx.run_until_parked();
14227 workspace.update_in(cx, |workspace, window, cx| {
14228 workspace.move_item_to_pane_at_index(
14229 &MoveItemToPane {
14230 destination: 3,
14231 focus: true,
14232 clone: true,
14233 },
14234 window,
14235 cx,
14236 );
14237 });
14238 cx.run_until_parked();
14239
14240 workspace.update(cx, |workspace, cx| {
14241 assert_eq!(workspace.panes.len(), 3, "Two new panes were created");
14242 for pane in workspace.panes() {
14243 assert_eq!(
14244 pane_items_paths(pane, cx),
14245 vec!["first.txt".to_string()],
14246 "Single item exists in all panes"
14247 );
14248 }
14249 });
14250
14251 // verify that the active pane has been updated after waiting for the
14252 // pane focus event to fire and resolve
14253 workspace.read_with(cx, |workspace, _app| {
14254 assert_eq!(
14255 workspace.active_pane(),
14256 &workspace.panes[2],
14257 "The third pane should be the active one: {:?}",
14258 workspace.panes
14259 );
14260 })
14261 }
14262
14263 #[gpui::test]
14264 async fn test_close_item_in_all_panes(cx: &mut TestAppContext) {
14265 init_test(cx);
14266
14267 let fs = FakeFs::new(cx.executor());
14268 fs.insert_tree("/root", json!({ "test.txt": "" })).await;
14269
14270 let project = Project::test(fs, ["root".as_ref()], cx).await;
14271 let (workspace, cx) =
14272 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14273
14274 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14275 // Add item to pane A with project path
14276 let item_a = cx.new(|cx| {
14277 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
14278 });
14279 workspace.update_in(cx, |workspace, window, cx| {
14280 workspace.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
14281 });
14282
14283 // Split to create pane B
14284 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
14285 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
14286 });
14287
14288 // Add item with SAME project path to pane B, and pin it
14289 let item_b = cx.new(|cx| {
14290 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
14291 });
14292 pane_b.update_in(cx, |pane, window, cx| {
14293 pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
14294 pane.set_pinned_count(1);
14295 });
14296
14297 assert_eq!(pane_a.read_with(cx, |pane, _| pane.items_len()), 1);
14298 assert_eq!(pane_b.read_with(cx, |pane, _| pane.items_len()), 1);
14299
14300 // close_pinned: false should only close the unpinned copy
14301 workspace.update_in(cx, |workspace, window, cx| {
14302 workspace.close_item_in_all_panes(
14303 &CloseItemInAllPanes {
14304 save_intent: Some(SaveIntent::Close),
14305 close_pinned: false,
14306 },
14307 window,
14308 cx,
14309 )
14310 });
14311 cx.executor().run_until_parked();
14312
14313 let item_count_a = pane_a.read_with(cx, |pane, _| pane.items_len());
14314 let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
14315 assert_eq!(item_count_a, 0, "Unpinned item in pane A should be closed");
14316 assert_eq!(item_count_b, 1, "Pinned item in pane B should remain");
14317
14318 // Split again, seeing as closing the previous item also closed its
14319 // pane, so only pane remains, which does not allow us to properly test
14320 // that both items close when `close_pinned: true`.
14321 let pane_c = workspace.update_in(cx, |workspace, window, cx| {
14322 workspace.split_pane(pane_b.clone(), SplitDirection::Right, window, cx)
14323 });
14324
14325 // Add an item with the same project path to pane C so that
14326 // close_item_in_all_panes can determine what to close across all panes
14327 // (it reads the active item from the active pane, and split_pane
14328 // creates an empty pane).
14329 let item_c = cx.new(|cx| {
14330 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
14331 });
14332 pane_c.update_in(cx, |pane, window, cx| {
14333 pane.add_item(Box::new(item_c.clone()), true, true, None, window, cx);
14334 });
14335
14336 // close_pinned: true should close the pinned copy too
14337 workspace.update_in(cx, |workspace, window, cx| {
14338 let panes_count = workspace.panes().len();
14339 assert_eq!(panes_count, 2, "Workspace should have two panes (B and C)");
14340
14341 workspace.close_item_in_all_panes(
14342 &CloseItemInAllPanes {
14343 save_intent: Some(SaveIntent::Close),
14344 close_pinned: true,
14345 },
14346 window,
14347 cx,
14348 )
14349 });
14350 cx.executor().run_until_parked();
14351
14352 let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
14353 let item_count_c = pane_c.read_with(cx, |pane, _| pane.items_len());
14354 assert_eq!(item_count_b, 0, "Pinned item in pane B should be closed");
14355 assert_eq!(item_count_c, 0, "Unpinned item in pane C should be closed");
14356 }
14357
14358 mod register_project_item_tests {
14359
14360 use super::*;
14361
14362 // View
14363 struct TestPngItemView {
14364 focus_handle: FocusHandle,
14365 }
14366 // Model
14367 struct TestPngItem {}
14368
14369 impl project::ProjectItem for TestPngItem {
14370 fn try_open(
14371 _project: &Entity<Project>,
14372 path: &ProjectPath,
14373 cx: &mut App,
14374 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
14375 if path.path.extension().unwrap() == "png" {
14376 Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestPngItem {}))))
14377 } else {
14378 None
14379 }
14380 }
14381
14382 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
14383 None
14384 }
14385
14386 fn project_path(&self, _: &App) -> Option<ProjectPath> {
14387 None
14388 }
14389
14390 fn is_dirty(&self) -> bool {
14391 false
14392 }
14393 }
14394
14395 impl Item for TestPngItemView {
14396 type Event = ();
14397 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14398 "".into()
14399 }
14400 }
14401 impl EventEmitter<()> for TestPngItemView {}
14402 impl Focusable for TestPngItemView {
14403 fn focus_handle(&self, _cx: &App) -> FocusHandle {
14404 self.focus_handle.clone()
14405 }
14406 }
14407
14408 impl Render for TestPngItemView {
14409 fn render(
14410 &mut self,
14411 _window: &mut Window,
14412 _cx: &mut Context<Self>,
14413 ) -> impl IntoElement {
14414 Empty
14415 }
14416 }
14417
14418 impl ProjectItem for TestPngItemView {
14419 type Item = TestPngItem;
14420
14421 fn for_project_item(
14422 _project: Entity<Project>,
14423 _pane: Option<&Pane>,
14424 _item: Entity<Self::Item>,
14425 _: &mut Window,
14426 cx: &mut Context<Self>,
14427 ) -> Self
14428 where
14429 Self: Sized,
14430 {
14431 Self {
14432 focus_handle: cx.focus_handle(),
14433 }
14434 }
14435 }
14436
14437 // View
14438 struct TestIpynbItemView {
14439 focus_handle: FocusHandle,
14440 }
14441 // Model
14442 struct TestIpynbItem {}
14443
14444 impl project::ProjectItem for TestIpynbItem {
14445 fn try_open(
14446 _project: &Entity<Project>,
14447 path: &ProjectPath,
14448 cx: &mut App,
14449 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
14450 if path.path.extension().unwrap() == "ipynb" {
14451 Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestIpynbItem {}))))
14452 } else {
14453 None
14454 }
14455 }
14456
14457 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
14458 None
14459 }
14460
14461 fn project_path(&self, _: &App) -> Option<ProjectPath> {
14462 None
14463 }
14464
14465 fn is_dirty(&self) -> bool {
14466 false
14467 }
14468 }
14469
14470 impl Item for TestIpynbItemView {
14471 type Event = ();
14472 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14473 "".into()
14474 }
14475 }
14476 impl EventEmitter<()> for TestIpynbItemView {}
14477 impl Focusable for TestIpynbItemView {
14478 fn focus_handle(&self, _cx: &App) -> FocusHandle {
14479 self.focus_handle.clone()
14480 }
14481 }
14482
14483 impl Render for TestIpynbItemView {
14484 fn render(
14485 &mut self,
14486 _window: &mut Window,
14487 _cx: &mut Context<Self>,
14488 ) -> impl IntoElement {
14489 Empty
14490 }
14491 }
14492
14493 impl ProjectItem for TestIpynbItemView {
14494 type Item = TestIpynbItem;
14495
14496 fn for_project_item(
14497 _project: Entity<Project>,
14498 _pane: Option<&Pane>,
14499 _item: Entity<Self::Item>,
14500 _: &mut Window,
14501 cx: &mut Context<Self>,
14502 ) -> Self
14503 where
14504 Self: Sized,
14505 {
14506 Self {
14507 focus_handle: cx.focus_handle(),
14508 }
14509 }
14510 }
14511
14512 struct TestAlternatePngItemView {
14513 focus_handle: FocusHandle,
14514 }
14515
14516 impl Item for TestAlternatePngItemView {
14517 type Event = ();
14518 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14519 "".into()
14520 }
14521 }
14522
14523 impl EventEmitter<()> for TestAlternatePngItemView {}
14524 impl Focusable for TestAlternatePngItemView {
14525 fn focus_handle(&self, _cx: &App) -> FocusHandle {
14526 self.focus_handle.clone()
14527 }
14528 }
14529
14530 impl Render for TestAlternatePngItemView {
14531 fn render(
14532 &mut self,
14533 _window: &mut Window,
14534 _cx: &mut Context<Self>,
14535 ) -> impl IntoElement {
14536 Empty
14537 }
14538 }
14539
14540 impl ProjectItem for TestAlternatePngItemView {
14541 type Item = TestPngItem;
14542
14543 fn for_project_item(
14544 _project: Entity<Project>,
14545 _pane: Option<&Pane>,
14546 _item: Entity<Self::Item>,
14547 _: &mut Window,
14548 cx: &mut Context<Self>,
14549 ) -> Self
14550 where
14551 Self: Sized,
14552 {
14553 Self {
14554 focus_handle: cx.focus_handle(),
14555 }
14556 }
14557 }
14558
14559 #[gpui::test]
14560 async fn test_register_project_item(cx: &mut TestAppContext) {
14561 init_test(cx);
14562
14563 cx.update(|cx| {
14564 register_project_item::<TestPngItemView>(cx);
14565 register_project_item::<TestIpynbItemView>(cx);
14566 });
14567
14568 let fs = FakeFs::new(cx.executor());
14569 fs.insert_tree(
14570 "/root1",
14571 json!({
14572 "one.png": "BINARYDATAHERE",
14573 "two.ipynb": "{ totally a notebook }",
14574 "three.txt": "editing text, sure why not?"
14575 }),
14576 )
14577 .await;
14578
14579 let project = Project::test(fs, ["root1".as_ref()], cx).await;
14580 let (workspace, cx) =
14581 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14582
14583 let worktree_id = project.update(cx, |project, cx| {
14584 project.worktrees(cx).next().unwrap().read(cx).id()
14585 });
14586
14587 let handle = workspace
14588 .update_in(cx, |workspace, window, cx| {
14589 let project_path = (worktree_id, rel_path("one.png"));
14590 workspace.open_path(project_path, None, true, window, cx)
14591 })
14592 .await
14593 .unwrap();
14594
14595 // Now we can check if the handle we got back errored or not
14596 assert_eq!(
14597 handle.to_any_view().entity_type(),
14598 TypeId::of::<TestPngItemView>()
14599 );
14600
14601 let handle = workspace
14602 .update_in(cx, |workspace, window, cx| {
14603 let project_path = (worktree_id, rel_path("two.ipynb"));
14604 workspace.open_path(project_path, None, true, window, cx)
14605 })
14606 .await
14607 .unwrap();
14608
14609 assert_eq!(
14610 handle.to_any_view().entity_type(),
14611 TypeId::of::<TestIpynbItemView>()
14612 );
14613
14614 let handle = workspace
14615 .update_in(cx, |workspace, window, cx| {
14616 let project_path = (worktree_id, rel_path("three.txt"));
14617 workspace.open_path(project_path, None, true, window, cx)
14618 })
14619 .await;
14620 assert!(handle.is_err());
14621 }
14622
14623 #[gpui::test]
14624 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
14625 init_test(cx);
14626
14627 cx.update(|cx| {
14628 register_project_item::<TestPngItemView>(cx);
14629 register_project_item::<TestAlternatePngItemView>(cx);
14630 });
14631
14632 let fs = FakeFs::new(cx.executor());
14633 fs.insert_tree(
14634 "/root1",
14635 json!({
14636 "one.png": "BINARYDATAHERE",
14637 "two.ipynb": "{ totally a notebook }",
14638 "three.txt": "editing text, sure why not?"
14639 }),
14640 )
14641 .await;
14642 let project = Project::test(fs, ["root1".as_ref()], cx).await;
14643 let (workspace, cx) =
14644 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14645 let worktree_id = project.update(cx, |project, cx| {
14646 project.worktrees(cx).next().unwrap().read(cx).id()
14647 });
14648
14649 let handle = workspace
14650 .update_in(cx, |workspace, window, cx| {
14651 let project_path = (worktree_id, rel_path("one.png"));
14652 workspace.open_path(project_path, None, true, window, cx)
14653 })
14654 .await
14655 .unwrap();
14656
14657 // This _must_ be the second item registered
14658 assert_eq!(
14659 handle.to_any_view().entity_type(),
14660 TypeId::of::<TestAlternatePngItemView>()
14661 );
14662
14663 let handle = workspace
14664 .update_in(cx, |workspace, window, cx| {
14665 let project_path = (worktree_id, rel_path("three.txt"));
14666 workspace.open_path(project_path, None, true, window, cx)
14667 })
14668 .await;
14669 assert!(handle.is_err());
14670 }
14671 }
14672
14673 #[gpui::test]
14674 async fn test_status_bar_visibility(cx: &mut TestAppContext) {
14675 init_test(cx);
14676
14677 let fs = FakeFs::new(cx.executor());
14678 let project = Project::test(fs, [], cx).await;
14679 let (workspace, _cx) =
14680 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14681
14682 // Test with status bar shown (default)
14683 workspace.read_with(cx, |workspace, cx| {
14684 let visible = workspace.status_bar_visible(cx);
14685 assert!(visible, "Status bar should be visible by default");
14686 });
14687
14688 // Test with status bar hidden
14689 cx.update_global(|store: &mut SettingsStore, cx| {
14690 store.update_user_settings(cx, |settings| {
14691 settings.status_bar.get_or_insert_default().show = Some(false);
14692 });
14693 });
14694
14695 workspace.read_with(cx, |workspace, cx| {
14696 let visible = workspace.status_bar_visible(cx);
14697 assert!(!visible, "Status bar should be hidden when show is false");
14698 });
14699
14700 // Test with status bar shown explicitly
14701 cx.update_global(|store: &mut SettingsStore, cx| {
14702 store.update_user_settings(cx, |settings| {
14703 settings.status_bar.get_or_insert_default().show = Some(true);
14704 });
14705 });
14706
14707 workspace.read_with(cx, |workspace, cx| {
14708 let visible = workspace.status_bar_visible(cx);
14709 assert!(visible, "Status bar should be visible when show is true");
14710 });
14711 }
14712
14713 #[gpui::test]
14714 async fn test_pane_close_active_item(cx: &mut TestAppContext) {
14715 init_test(cx);
14716
14717 let fs = FakeFs::new(cx.executor());
14718 let project = Project::test(fs, [], cx).await;
14719 let (multi_workspace, cx) =
14720 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
14721 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
14722 let panel = workspace.update_in(cx, |workspace, window, cx| {
14723 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14724 workspace.add_panel(panel.clone(), window, cx);
14725
14726 workspace
14727 .right_dock()
14728 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
14729
14730 panel
14731 });
14732
14733 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14734 let item_a = cx.new(TestItem::new);
14735 let item_b = cx.new(TestItem::new);
14736 let item_a_id = item_a.entity_id();
14737 let item_b_id = item_b.entity_id();
14738
14739 pane.update_in(cx, |pane, window, cx| {
14740 pane.add_item(Box::new(item_a.clone()), true, true, None, window, cx);
14741 pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
14742 });
14743
14744 pane.read_with(cx, |pane, _| {
14745 assert_eq!(pane.items_len(), 2);
14746 assert_eq!(pane.active_item().unwrap().item_id(), item_b_id);
14747 });
14748
14749 workspace.update_in(cx, |workspace, window, cx| {
14750 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14751 });
14752
14753 workspace.update_in(cx, |_, window, cx| {
14754 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
14755 });
14756
14757 // Assert that the `pane::CloseActiveItem` action is handled at the
14758 // workspace level when one of the dock panels is focused and, in that
14759 // case, the center pane's active item is closed but the focus is not
14760 // moved.
14761 cx.dispatch_action(pane::CloseActiveItem::default());
14762 cx.run_until_parked();
14763
14764 pane.read_with(cx, |pane, _| {
14765 assert_eq!(pane.items_len(), 1);
14766 assert_eq!(pane.active_item().unwrap().item_id(), item_a_id);
14767 });
14768
14769 workspace.update_in(cx, |workspace, window, cx| {
14770 assert!(workspace.right_dock().read(cx).is_open());
14771 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
14772 });
14773 }
14774
14775 #[gpui::test]
14776 async fn test_panel_zoom_preserved_across_workspace_switch(cx: &mut TestAppContext) {
14777 init_test(cx);
14778 let fs = FakeFs::new(cx.executor());
14779
14780 let project_a = Project::test(fs.clone(), [], cx).await;
14781 let project_b = Project::test(fs, [], cx).await;
14782
14783 let multi_workspace_handle =
14784 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
14785 cx.run_until_parked();
14786
14787 multi_workspace_handle
14788 .update(cx, |mw, _window, cx| {
14789 mw.open_sidebar(cx);
14790 })
14791 .unwrap();
14792
14793 let workspace_a = multi_workspace_handle
14794 .read_with(cx, |mw, _| mw.workspace().clone())
14795 .unwrap();
14796
14797 let _workspace_b = multi_workspace_handle
14798 .update(cx, |mw, window, cx| {
14799 mw.test_add_workspace(project_b, window, cx)
14800 })
14801 .unwrap();
14802
14803 // Switch to workspace A
14804 multi_workspace_handle
14805 .update(cx, |mw, window, cx| {
14806 let workspace = mw.workspaces().next().unwrap().clone();
14807 mw.activate(workspace, window, cx);
14808 })
14809 .unwrap();
14810
14811 let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
14812
14813 // Add a panel to workspace A's right dock and open the dock
14814 let panel = workspace_a.update_in(cx, |workspace, window, cx| {
14815 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14816 workspace.add_panel(panel.clone(), window, cx);
14817 workspace
14818 .right_dock()
14819 .update(cx, |dock, cx| dock.set_open(true, window, cx));
14820 panel
14821 });
14822
14823 // Focus the panel through the workspace (matching existing test pattern)
14824 workspace_a.update_in(cx, |workspace, window, cx| {
14825 workspace.toggle_panel_focus::<TestPanel>(window, cx);
14826 });
14827
14828 // Zoom the panel
14829 panel.update_in(cx, |panel, window, cx| {
14830 panel.set_zoomed(true, window, cx);
14831 });
14832
14833 // Verify the panel is zoomed and the dock is open
14834 workspace_a.update_in(cx, |workspace, window, cx| {
14835 assert!(
14836 workspace.right_dock().read(cx).is_open(),
14837 "dock should be open before switch"
14838 );
14839 assert!(
14840 panel.is_zoomed(window, cx),
14841 "panel should be zoomed before switch"
14842 );
14843 assert!(
14844 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
14845 "panel should be focused before switch"
14846 );
14847 });
14848
14849 // Switch to workspace B
14850 multi_workspace_handle
14851 .update(cx, |mw, window, cx| {
14852 let workspace = mw.workspaces().nth(1).unwrap().clone();
14853 mw.activate(workspace, window, cx);
14854 })
14855 .unwrap();
14856 cx.run_until_parked();
14857
14858 // Switch back to workspace A
14859 multi_workspace_handle
14860 .update(cx, |mw, window, cx| {
14861 let workspace = mw.workspaces().next().unwrap().clone();
14862 mw.activate(workspace, window, cx);
14863 })
14864 .unwrap();
14865 cx.run_until_parked();
14866
14867 // Verify the panel is still zoomed and the dock is still open
14868 workspace_a.update_in(cx, |workspace, window, cx| {
14869 assert!(
14870 workspace.right_dock().read(cx).is_open(),
14871 "dock should still be open after switching back"
14872 );
14873 assert!(
14874 panel.is_zoomed(window, cx),
14875 "panel should still be zoomed after switching back"
14876 );
14877 });
14878 }
14879
14880 fn pane_items_paths(pane: &Entity<Pane>, cx: &App) -> Vec<String> {
14881 pane.read(cx)
14882 .items()
14883 .flat_map(|item| {
14884 item.project_paths(cx)
14885 .into_iter()
14886 .map(|path| path.path.display(PathStyle::local()).into_owned())
14887 })
14888 .collect()
14889 }
14890
14891 pub fn init_test(cx: &mut TestAppContext) {
14892 cx.update(|cx| {
14893 let settings_store = SettingsStore::test(cx);
14894 cx.set_global(settings_store);
14895 cx.set_global(db::AppDatabase::test_new());
14896 theme_settings::init(theme::LoadThemes::JustBase, cx);
14897 });
14898 }
14899
14900 #[gpui::test]
14901 async fn test_toggle_theme_mode_persists_and_updates_active_theme(cx: &mut TestAppContext) {
14902 use settings::{ThemeName, ThemeSelection};
14903 use theme::SystemAppearance;
14904 use zed_actions::theme::ToggleMode;
14905
14906 init_test(cx);
14907
14908 let fs = FakeFs::new(cx.executor());
14909 let settings_fs: Arc<dyn fs::Fs> = fs.clone();
14910
14911 fs.insert_tree(path!("/root"), json!({ "file.rs": "fn main() {}\n" }))
14912 .await;
14913
14914 // Build a test project and workspace view so the test can invoke
14915 // the workspace action handler the same way the UI would.
14916 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
14917 let (workspace, cx) =
14918 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14919
14920 // Seed the settings file with a plain static light theme so the
14921 // first toggle always starts from a known persisted state.
14922 workspace.update_in(cx, |_workspace, _window, cx| {
14923 *SystemAppearance::global_mut(cx) = SystemAppearance(theme::Appearance::Light);
14924 settings::update_settings_file(settings_fs.clone(), cx, |settings, _cx| {
14925 settings.theme.theme = Some(ThemeSelection::Static(ThemeName("One Light".into())));
14926 });
14927 });
14928 cx.executor().advance_clock(Duration::from_millis(200));
14929 cx.run_until_parked();
14930
14931 // Confirm the initial persisted settings contain the static theme
14932 // we just wrote before any toggling happens.
14933 let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
14934 assert!(settings_text.contains(r#""theme": "One Light""#));
14935
14936 // Toggle once. This should migrate the persisted theme settings
14937 // into light/dark slots and enable system mode.
14938 workspace.update_in(cx, |workspace, window, cx| {
14939 workspace.toggle_theme_mode(&ToggleMode, window, cx);
14940 });
14941 cx.executor().advance_clock(Duration::from_millis(200));
14942 cx.run_until_parked();
14943
14944 // 1. Static -> Dynamic
14945 // this assertion checks theme changed from static to dynamic.
14946 let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
14947 let parsed: serde_json::Value = settings::parse_json_with_comments(&settings_text).unwrap();
14948 assert_eq!(
14949 parsed["theme"],
14950 serde_json::json!({
14951 "mode": "system",
14952 "light": "One Light",
14953 "dark": "One Dark"
14954 })
14955 );
14956
14957 // 2. Toggle again, suppose it will change the mode to light
14958 workspace.update_in(cx, |workspace, window, cx| {
14959 workspace.toggle_theme_mode(&ToggleMode, window, cx);
14960 });
14961 cx.executor().advance_clock(Duration::from_millis(200));
14962 cx.run_until_parked();
14963
14964 let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
14965 assert!(settings_text.contains(r#""mode": "light""#));
14966 }
14967
14968 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
14969 let item = TestProjectItem::new(id, path, cx);
14970 item.update(cx, |item, _| {
14971 item.is_dirty = true;
14972 });
14973 item
14974 }
14975
14976 #[gpui::test]
14977 async fn test_zoomed_panel_without_pane_preserved_on_center_focus(
14978 cx: &mut gpui::TestAppContext,
14979 ) {
14980 init_test(cx);
14981 let fs = FakeFs::new(cx.executor());
14982
14983 let project = Project::test(fs, [], cx).await;
14984 let (workspace, cx) =
14985 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
14986
14987 let panel = workspace.update_in(cx, |workspace, window, cx| {
14988 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14989 workspace.add_panel(panel.clone(), window, cx);
14990 workspace
14991 .right_dock()
14992 .update(cx, |dock, cx| dock.set_open(true, window, cx));
14993 panel
14994 });
14995
14996 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14997 pane.update_in(cx, |pane, window, cx| {
14998 let item = cx.new(TestItem::new);
14999 pane.add_item(Box::new(item), true, true, None, window, cx);
15000 });
15001
15002 // Transfer focus to the panel, then zoom it. Using toggle_panel_focus
15003 // mirrors the real-world flow and avoids side effects from directly
15004 // focusing the panel while the center pane is active.
15005 workspace.update_in(cx, |workspace, window, cx| {
15006 workspace.toggle_panel_focus::<TestPanel>(window, cx);
15007 });
15008
15009 panel.update_in(cx, |panel, window, cx| {
15010 panel.set_zoomed(true, window, cx);
15011 });
15012
15013 workspace.update_in(cx, |workspace, window, cx| {
15014 assert!(workspace.right_dock().read(cx).is_open());
15015 assert!(panel.is_zoomed(window, cx));
15016 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
15017 });
15018
15019 // Simulate a spurious pane::Event::Focus on the center pane while the
15020 // panel still has focus. This mirrors what happens during macOS window
15021 // activation: the center pane fires a focus event even though actual
15022 // focus remains on the dock panel.
15023 pane.update_in(cx, |_, _, cx| {
15024 cx.emit(pane::Event::Focus);
15025 });
15026
15027 // The dock must remain open because the panel had focus at the time the
15028 // event was processed. Before the fix, dock_to_preserve was None for
15029 // panels that don't implement pane(), causing the dock to close.
15030 workspace.update_in(cx, |workspace, window, cx| {
15031 assert!(
15032 workspace.right_dock().read(cx).is_open(),
15033 "Dock should stay open when its zoomed panel (without pane()) still has focus"
15034 );
15035 assert!(panel.is_zoomed(window, cx));
15036 });
15037 }
15038
15039 #[gpui::test]
15040 async fn test_panels_stay_open_after_position_change_and_settings_update(
15041 cx: &mut gpui::TestAppContext,
15042 ) {
15043 init_test(cx);
15044 let fs = FakeFs::new(cx.executor());
15045 let project = Project::test(fs, [], cx).await;
15046 let (workspace, cx) =
15047 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
15048
15049 // Add two panels to the left dock and open it.
15050 let (panel_a, panel_b) = workspace.update_in(cx, |workspace, window, cx| {
15051 let panel_a = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
15052 let panel_b = cx.new(|cx| TestPanel::new(DockPosition::Left, 101, cx));
15053 workspace.add_panel(panel_a.clone(), window, cx);
15054 workspace.add_panel(panel_b.clone(), window, cx);
15055 workspace.left_dock().update(cx, |dock, cx| {
15056 dock.set_open(true, window, cx);
15057 dock.activate_panel(0, window, cx);
15058 });
15059 (panel_a, panel_b)
15060 });
15061
15062 workspace.update_in(cx, |workspace, _, cx| {
15063 assert!(workspace.left_dock().read(cx).is_open());
15064 });
15065
15066 // Simulate a feature flag changing default dock positions: both panels
15067 // move from Left to Right.
15068 workspace.update_in(cx, |_workspace, _window, cx| {
15069 panel_a.update(cx, |p, _cx| p.position = DockPosition::Right);
15070 panel_b.update(cx, |p, _cx| p.position = DockPosition::Right);
15071 cx.update_global::<SettingsStore, _>(|_, _| {});
15072 });
15073
15074 // Both panels should now be in the right dock.
15075 workspace.update_in(cx, |workspace, _, cx| {
15076 let right_dock = workspace.right_dock().read(cx);
15077 assert_eq!(right_dock.panels_len(), 2);
15078 });
15079
15080 // Open the right dock and activate panel_b (simulating the user
15081 // opening the panel after it moved).
15082 workspace.update_in(cx, |workspace, window, cx| {
15083 workspace.right_dock().update(cx, |dock, cx| {
15084 dock.set_open(true, window, cx);
15085 dock.activate_panel(1, window, cx);
15086 });
15087 });
15088
15089 // Now trigger another SettingsStore change
15090 workspace.update_in(cx, |_workspace, _window, cx| {
15091 cx.update_global::<SettingsStore, _>(|_, _| {});
15092 });
15093
15094 workspace.update_in(cx, |workspace, _, cx| {
15095 assert!(
15096 workspace.right_dock().read(cx).is_open(),
15097 "Right dock should still be open after a settings change"
15098 );
15099 assert_eq!(
15100 workspace.right_dock().read(cx).panels_len(),
15101 2,
15102 "Both panels should still be in the right dock"
15103 );
15104 });
15105 }
15106}