1pub mod dock;
2pub mod history_manager;
3pub mod invalid_item_view;
4pub mod item;
5mod modal_layer;
6mod multi_workspace;
7pub mod notifications;
8pub mod pane;
9pub mod pane_group;
10mod path_list;
11mod persistence;
12pub mod searchable;
13mod security_modal;
14pub mod shared_screen;
15mod status_bar;
16pub mod tasks;
17mod theme_preview;
18mod toast_layer;
19mod toolbar;
20pub mod welcome;
21mod workspace_settings;
22
23pub use crate::notifications::NotificationFrame;
24pub use dock::Panel;
25pub use multi_workspace::{
26 DraggedSidebar, FocusWorkspaceSidebar, MultiWorkspace, NewWorkspaceInWindow,
27 NextWorkspaceInWindow, PreviousWorkspaceInWindow, Sidebar, SidebarEvent, SidebarHandle,
28 ToggleWorkspaceSidebar,
29};
30pub use path_list::PathList;
31pub use toast_layer::{ToastAction, ToastLayer, ToastView};
32
33use anyhow::{Context as _, Result, anyhow};
34use call::{ActiveCall, call_settings::CallSettings};
35use client::{
36 ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore,
37 proto::{self, ErrorCode, PanelId, PeerId},
38};
39use collections::{HashMap, HashSet, hash_map};
40use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
41use futures::{
42 Future, FutureExt, StreamExt,
43 channel::{
44 mpsc::{self, UnboundedReceiver, UnboundedSender},
45 oneshot,
46 },
47 future::{Shared, try_join_all},
48};
49use gpui::{
50 Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context,
51 CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
52 Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView, MouseButton,
53 PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription,
54 SystemWindowTabController, Task, Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId,
55 WindowOptions, actions, canvas, point, relative, size, transparent_black,
56};
57pub use history_manager::*;
58pub use item::{
59 FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
60 ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
61};
62use itertools::Itertools;
63use language::{Buffer, LanguageRegistry, Rope, language_settings::all_language_settings};
64pub use modal_layer::*;
65use node_runtime::NodeRuntime;
66use notifications::{
67 DetachAndPromptErr, Notifications, dismiss_app_notification,
68 simple_message_notification::MessageNotification,
69};
70pub use pane::*;
71pub use pane_group::{
72 ActivePaneDecorator, HANDLE_HITBOX_SIZE, Member, PaneAxis, PaneGroup, PaneRenderContext,
73 SplitDirection,
74};
75use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace};
76pub use persistence::{
77 DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items,
78 model::{ItemId, SerializedMultiWorkspace, SerializedWorkspaceLocation, SessionWorkspace},
79 read_serialized_multi_workspaces,
80};
81use postage::stream::Stream;
82use project::{
83 DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
84 WorktreeSettings,
85 debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
86 project_settings::ProjectSettings,
87 toolchain_store::ToolchainStoreEvent,
88 trusted_worktrees::{RemoteHostLocation, TrustedWorktrees, TrustedWorktreesEvent},
89};
90use remote::{
91 RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions,
92 remote_client::ConnectionIdentifier,
93};
94use schemars::JsonSchema;
95use serde::Deserialize;
96use session::AppSession;
97use settings::{
98 CenteredPaddingSettings, Settings, SettingsLocation, SettingsStore, update_settings_file,
99};
100use shared_screen::SharedScreen;
101use sqlez::{
102 bindable::{Bind, Column, StaticColumnCount},
103 statement::Statement,
104};
105use status_bar::StatusBar;
106pub use status_bar::StatusItemView;
107use std::{
108 any::TypeId,
109 borrow::Cow,
110 cell::RefCell,
111 cmp,
112 collections::VecDeque,
113 env,
114 hash::Hash,
115 path::{Path, PathBuf},
116 process::ExitStatus,
117 rc::Rc,
118 sync::{
119 Arc, LazyLock, Weak,
120 atomic::{AtomicBool, AtomicUsize},
121 },
122 time::Duration,
123};
124use task::{DebugScenario, SharedTaskContext, SpawnInTerminal};
125use theme::{ActiveTheme, GlobalTheme, SystemAppearance, ThemeSettings};
126pub use toolbar::{
127 PaneSearchBarCallbacks, Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
128};
129pub use ui;
130use ui::{Window, prelude::*};
131use util::{
132 ResultExt, TryFutureExt,
133 paths::{PathStyle, SanitizedPath},
134 rel_path::RelPath,
135 serde::default_true,
136};
137use uuid::Uuid;
138pub use workspace_settings::{
139 AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, StatusBarSettings, TabBarSettings,
140 WorkspaceSettings,
141};
142use zed_actions::{Spawn, feedback::FileBugReport};
143
144use crate::{item::ItemBufferKind, notifications::NotificationId};
145use crate::{
146 persistence::{
147 SerializedAxis,
148 model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
149 },
150 security_modal::SecurityModal,
151};
152
153pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200);
154
155static ZED_WINDOW_SIZE: LazyLock<Option<Size<Pixels>>> = LazyLock::new(|| {
156 env::var("ZED_WINDOW_SIZE")
157 .ok()
158 .as_deref()
159 .and_then(parse_pixel_size_env_var)
160});
161
162static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
163 env::var("ZED_WINDOW_POSITION")
164 .ok()
165 .as_deref()
166 .and_then(parse_pixel_position_env_var)
167});
168
169pub trait TerminalProvider {
170 fn spawn(
171 &self,
172 task: SpawnInTerminal,
173 window: &mut Window,
174 cx: &mut App,
175 ) -> Task<Option<Result<ExitStatus>>>;
176}
177
178pub trait DebuggerProvider {
179 // `active_buffer` is used to resolve build task's name against language-specific tasks.
180 fn start_session(
181 &self,
182 definition: DebugScenario,
183 task_context: SharedTaskContext,
184 active_buffer: Option<Entity<Buffer>>,
185 worktree_id: Option<WorktreeId>,
186 window: &mut Window,
187 cx: &mut App,
188 );
189
190 fn spawn_task_or_modal(
191 &self,
192 workspace: &mut Workspace,
193 action: &Spawn,
194 window: &mut Window,
195 cx: &mut Context<Workspace>,
196 );
197
198 fn task_scheduled(&self, cx: &mut App);
199 fn debug_scenario_scheduled(&self, cx: &mut App);
200 fn debug_scenario_scheduled_last(&self, cx: &App) -> bool;
201
202 fn active_thread_state(&self, cx: &App) -> Option<ThreadStatus>;
203}
204
205actions!(
206 workspace,
207 [
208 /// Activates the next pane in the workspace.
209 ActivateNextPane,
210 /// Activates the previous pane in the workspace.
211 ActivatePreviousPane,
212 /// Switches to the next window.
213 ActivateNextWindow,
214 /// Switches to the previous window.
215 ActivatePreviousWindow,
216 /// Adds a folder to the current project.
217 AddFolderToProject,
218 /// Clears all notifications.
219 ClearAllNotifications,
220 /// Clears all navigation history, including forward/backward navigation, recently opened files, and recently closed tabs. **This action is irreversible**.
221 ClearNavigationHistory,
222 /// Closes the active dock.
223 CloseActiveDock,
224 /// Closes all docks.
225 CloseAllDocks,
226 /// Toggles all docks.
227 ToggleAllDocks,
228 /// Closes the current window.
229 CloseWindow,
230 /// Closes the current project.
231 CloseProject,
232 /// Opens the feedback dialog.
233 Feedback,
234 /// Follows the next collaborator in the session.
235 FollowNextCollaborator,
236 /// Moves the focused panel to the next position.
237 MoveFocusedPanelToNextPosition,
238 /// Creates a new file.
239 NewFile,
240 /// Creates a new file in a vertical split.
241 NewFileSplitVertical,
242 /// Creates a new file in a horizontal split.
243 NewFileSplitHorizontal,
244 /// Opens a new search.
245 NewSearch,
246 /// Opens a new window.
247 NewWindow,
248 /// Opens a file or directory.
249 Open,
250 /// Opens multiple files.
251 OpenFiles,
252 /// Opens the current location in terminal.
253 OpenInTerminal,
254 /// Opens the component preview.
255 OpenComponentPreview,
256 /// Reloads the active item.
257 ReloadActiveItem,
258 /// Resets the active dock to its default size.
259 ResetActiveDockSize,
260 /// Resets all open docks to their default sizes.
261 ResetOpenDocksSize,
262 /// Reloads the application
263 Reload,
264 /// Saves the current file with a new name.
265 SaveAs,
266 /// Saves without formatting.
267 SaveWithoutFormat,
268 /// Shuts down all debug adapters.
269 ShutdownDebugAdapters,
270 /// Suppresses the current notification.
271 SuppressNotification,
272 /// Toggles the bottom dock.
273 ToggleBottomDock,
274 /// Toggles centered layout mode.
275 ToggleCenteredLayout,
276 /// Toggles edit prediction feature globally for all files.
277 ToggleEditPrediction,
278 /// Toggles the left dock.
279 ToggleLeftDock,
280 /// Toggles the right dock.
281 ToggleRightDock,
282 /// Toggles zoom on the active pane.
283 ToggleZoom,
284 /// Toggles read-only mode for the active item (if supported by that item).
285 ToggleReadOnlyFile,
286 /// Zooms in on the active pane.
287 ZoomIn,
288 /// Zooms out of the active pane.
289 ZoomOut,
290 /// If any worktrees are in restricted mode, shows a modal with possible actions.
291 /// If the modal is shown already, closes it without trusting any worktree.
292 ToggleWorktreeSecurity,
293 /// Clears all trusted worktrees, placing them in restricted mode on next open.
294 /// Requires restart to take effect on already opened projects.
295 ClearTrustedWorktrees,
296 /// Stops following a collaborator.
297 Unfollow,
298 /// Restores the banner.
299 RestoreBanner,
300 /// Toggles expansion of the selected item.
301 ToggleExpandItem,
302 ]
303);
304
305/// Activates a specific pane by its index.
306#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
307#[action(namespace = workspace)]
308pub struct ActivatePane(pub usize);
309
310/// Moves an item to a specific pane by index.
311#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
312#[action(namespace = workspace)]
313#[serde(deny_unknown_fields)]
314pub struct MoveItemToPane {
315 #[serde(default = "default_1")]
316 pub destination: usize,
317 #[serde(default = "default_true")]
318 pub focus: bool,
319 #[serde(default)]
320 pub clone: bool,
321}
322
323fn default_1() -> usize {
324 1
325}
326
327/// Moves an item to a pane in the specified direction.
328#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
329#[action(namespace = workspace)]
330#[serde(deny_unknown_fields)]
331pub struct MoveItemToPaneInDirection {
332 #[serde(default = "default_right")]
333 pub direction: SplitDirection,
334 #[serde(default = "default_true")]
335 pub focus: bool,
336 #[serde(default)]
337 pub clone: bool,
338}
339
340/// Creates a new file in a split of the desired direction.
341#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
342#[action(namespace = workspace)]
343#[serde(deny_unknown_fields)]
344pub struct NewFileSplit(pub SplitDirection);
345
346fn default_right() -> SplitDirection {
347 SplitDirection::Right
348}
349
350/// Saves all open files in the workspace.
351#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
352#[action(namespace = workspace)]
353#[serde(deny_unknown_fields)]
354pub struct SaveAll {
355 #[serde(default)]
356 pub save_intent: Option<SaveIntent>,
357}
358
359/// Saves the current file with the specified options.
360#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
361#[action(namespace = workspace)]
362#[serde(deny_unknown_fields)]
363pub struct Save {
364 #[serde(default)]
365 pub save_intent: Option<SaveIntent>,
366}
367
368/// Closes all items and panes in the workspace.
369#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
370#[action(namespace = workspace)]
371#[serde(deny_unknown_fields)]
372pub struct CloseAllItemsAndPanes {
373 #[serde(default)]
374 pub save_intent: Option<SaveIntent>,
375}
376
377/// Closes all inactive tabs and panes in the workspace.
378#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
379#[action(namespace = workspace)]
380#[serde(deny_unknown_fields)]
381pub struct CloseInactiveTabsAndPanes {
382 #[serde(default)]
383 pub save_intent: Option<SaveIntent>,
384}
385
386/// Closes the active item across all panes.
387#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
388#[action(namespace = workspace)]
389#[serde(deny_unknown_fields)]
390pub struct CloseItemInAllPanes {
391 #[serde(default)]
392 pub save_intent: Option<SaveIntent>,
393 #[serde(default)]
394 pub close_pinned: bool,
395}
396
397/// Sends a sequence of keystrokes to the active element.
398#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
399#[action(namespace = workspace)]
400pub struct SendKeystrokes(pub String);
401
402actions!(
403 project_symbols,
404 [
405 /// Toggles the project symbols search.
406 #[action(name = "Toggle")]
407 ToggleProjectSymbols
408 ]
409);
410
411/// Toggles the file finder interface.
412#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
413#[action(namespace = file_finder, name = "Toggle")]
414#[serde(deny_unknown_fields)]
415pub struct ToggleFileFinder {
416 #[serde(default)]
417 pub separate_history: bool,
418}
419
420/// Opens a new terminal in the center.
421#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
422#[action(namespace = workspace)]
423#[serde(deny_unknown_fields)]
424pub struct NewCenterTerminal {
425 /// If true, creates a local terminal even in remote projects.
426 #[serde(default)]
427 pub local: bool,
428}
429
430/// Opens a new terminal.
431#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
432#[action(namespace = workspace)]
433#[serde(deny_unknown_fields)]
434pub struct NewTerminal {
435 /// If true, creates a local terminal even in remote projects.
436 #[serde(default)]
437 pub local: bool,
438}
439
440/// Increases size of a currently focused dock by a given amount of pixels.
441#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
442#[action(namespace = workspace)]
443#[serde(deny_unknown_fields)]
444pub struct IncreaseActiveDockSize {
445 /// For 0px parameter, uses UI font size value.
446 #[serde(default)]
447 pub px: u32,
448}
449
450/// Decreases size of a currently focused dock by a given amount of pixels.
451#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
452#[action(namespace = workspace)]
453#[serde(deny_unknown_fields)]
454pub struct DecreaseActiveDockSize {
455 /// For 0px parameter, uses UI font size value.
456 #[serde(default)]
457 pub px: u32,
458}
459
460/// Increases size of all currently visible docks uniformly, by a given amount of pixels.
461#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
462#[action(namespace = workspace)]
463#[serde(deny_unknown_fields)]
464pub struct IncreaseOpenDocksSize {
465 /// For 0px parameter, uses UI font size value.
466 #[serde(default)]
467 pub px: u32,
468}
469
470/// Decreases size of all currently visible docks uniformly, by a given amount of pixels.
471#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
472#[action(namespace = workspace)]
473#[serde(deny_unknown_fields)]
474pub struct DecreaseOpenDocksSize {
475 /// For 0px parameter, uses UI font size value.
476 #[serde(default)]
477 pub px: u32,
478}
479
480actions!(
481 workspace,
482 [
483 /// Activates the pane to the left.
484 ActivatePaneLeft,
485 /// Activates the pane to the right.
486 ActivatePaneRight,
487 /// Activates the pane above.
488 ActivatePaneUp,
489 /// Activates the pane below.
490 ActivatePaneDown,
491 /// Swaps the current pane with the one to the left.
492 SwapPaneLeft,
493 /// Swaps the current pane with the one to the right.
494 SwapPaneRight,
495 /// Swaps the current pane with the one above.
496 SwapPaneUp,
497 /// Swaps the current pane with the one below.
498 SwapPaneDown,
499 // Swaps the current pane with the first available adjacent pane (searching in order: below, above, right, left) and activates that pane.
500 SwapPaneAdjacent,
501 /// Move the current pane to be at the far left.
502 MovePaneLeft,
503 /// Move the current pane to be at the far right.
504 MovePaneRight,
505 /// Move the current pane to be at the very top.
506 MovePaneUp,
507 /// Move the current pane to be at the very bottom.
508 MovePaneDown,
509 ]
510);
511
512#[derive(PartialEq, Eq, Debug)]
513pub enum CloseIntent {
514 /// Quit the program entirely.
515 Quit,
516 /// Close a window.
517 CloseWindow,
518 /// Replace the workspace in an existing window.
519 ReplaceWindow,
520}
521
522#[derive(Clone)]
523pub struct Toast {
524 id: NotificationId,
525 msg: Cow<'static, str>,
526 autohide: bool,
527 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut Window, &mut App)>)>,
528}
529
530impl Toast {
531 pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
532 Toast {
533 id,
534 msg: msg.into(),
535 on_click: None,
536 autohide: false,
537 }
538 }
539
540 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
541 where
542 M: Into<Cow<'static, str>>,
543 F: Fn(&mut Window, &mut App) + 'static,
544 {
545 self.on_click = Some((message.into(), Arc::new(on_click)));
546 self
547 }
548
549 pub fn autohide(mut self) -> Self {
550 self.autohide = true;
551 self
552 }
553}
554
555impl PartialEq for Toast {
556 fn eq(&self, other: &Self) -> bool {
557 self.id == other.id
558 && self.msg == other.msg
559 && self.on_click.is_some() == other.on_click.is_some()
560 }
561}
562
563/// Opens a new terminal with the specified working directory.
564#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema, Action)]
565#[action(namespace = workspace)]
566#[serde(deny_unknown_fields)]
567pub struct OpenTerminal {
568 pub working_directory: PathBuf,
569 /// If true, creates a local terminal even in remote projects.
570 #[serde(default)]
571 pub local: bool,
572}
573
574#[derive(
575 Clone,
576 Copy,
577 Debug,
578 Default,
579 Hash,
580 PartialEq,
581 Eq,
582 PartialOrd,
583 Ord,
584 serde::Serialize,
585 serde::Deserialize,
586)]
587pub struct WorkspaceId(i64);
588
589impl WorkspaceId {
590 pub fn from_i64(value: i64) -> Self {
591 Self(value)
592 }
593}
594
595impl StaticColumnCount for WorkspaceId {}
596impl Bind for WorkspaceId {
597 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
598 self.0.bind(statement, start_index)
599 }
600}
601impl Column for WorkspaceId {
602 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
603 i64::column(statement, start_index)
604 .map(|(i, next_index)| (Self(i), next_index))
605 .with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
606 }
607}
608impl From<WorkspaceId> for i64 {
609 fn from(val: WorkspaceId) -> Self {
610 val.0
611 }
612}
613
614fn prompt_and_open_paths(app_state: Arc<AppState>, options: PathPromptOptions, cx: &mut App) {
615 if let Some(workspace_window) = local_workspace_windows(cx).into_iter().next() {
616 workspace_window
617 .update(cx, |multi_workspace, window, cx| {
618 let workspace = multi_workspace.workspace().clone();
619 workspace.update(cx, |workspace, cx| {
620 prompt_for_open_path_and_open(workspace, app_state, options, window, cx);
621 });
622 })
623 .ok();
624 } else {
625 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, None, None, cx);
626 cx.spawn(async move |cx| {
627 let (window, _) = task.await?;
628 window.update(cx, |multi_workspace, window, cx| {
629 window.activate_window();
630 let workspace = multi_workspace.workspace().clone();
631 workspace.update(cx, |workspace, cx| {
632 prompt_for_open_path_and_open(workspace, app_state, options, window, cx);
633 });
634 })?;
635 anyhow::Ok(())
636 })
637 .detach_and_log_err(cx);
638 }
639}
640
641pub fn prompt_for_open_path_and_open(
642 workspace: &mut Workspace,
643 app_state: Arc<AppState>,
644 options: PathPromptOptions,
645 window: &mut Window,
646 cx: &mut Context<Workspace>,
647) {
648 let paths = workspace.prompt_for_open_path(
649 options,
650 DirectoryLister::Local(workspace.project().clone(), app_state.fs.clone()),
651 window,
652 cx,
653 );
654 cx.spawn_in(window, async move |this, cx| {
655 let Some(paths) = paths.await.log_err().flatten() else {
656 return;
657 };
658 if let Some(task) = this
659 .update_in(cx, |this, window, cx| {
660 this.open_workspace_for_paths(false, paths, window, cx)
661 })
662 .log_err()
663 {
664 task.await.log_err();
665 }
666 })
667 .detach();
668}
669
670pub fn init(app_state: Arc<AppState>, cx: &mut App) {
671 component::init();
672 theme_preview::init(cx);
673 toast_layer::init(cx);
674 history_manager::init(app_state.fs.clone(), cx);
675
676 cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx))
677 .on_action(|_: &Reload, cx| reload(cx))
678 .on_action({
679 let app_state = Arc::downgrade(&app_state);
680 move |_: &Open, cx: &mut App| {
681 if let Some(app_state) = app_state.upgrade() {
682 prompt_and_open_paths(
683 app_state,
684 PathPromptOptions {
685 files: true,
686 directories: true,
687 multiple: true,
688 prompt: None,
689 },
690 cx,
691 );
692 }
693 }
694 })
695 .on_action({
696 let app_state = Arc::downgrade(&app_state);
697 move |_: &OpenFiles, cx: &mut App| {
698 let directories = cx.can_select_mixed_files_and_dirs();
699 if let Some(app_state) = app_state.upgrade() {
700 prompt_and_open_paths(
701 app_state,
702 PathPromptOptions {
703 files: true,
704 directories,
705 multiple: true,
706 prompt: None,
707 },
708 cx,
709 );
710 }
711 }
712 });
713}
714
715type BuildProjectItemFn =
716 fn(AnyEntity, Entity<Project>, Option<&Pane>, &mut Window, &mut App) -> Box<dyn ItemHandle>;
717
718type BuildProjectItemForPathFn =
719 fn(
720 &Entity<Project>,
721 &ProjectPath,
722 &mut Window,
723 &mut App,
724 ) -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
725
726#[derive(Clone, Default)]
727struct ProjectItemRegistry {
728 build_project_item_fns_by_type: HashMap<TypeId, BuildProjectItemFn>,
729 build_project_item_for_path_fns: Vec<BuildProjectItemForPathFn>,
730}
731
732impl ProjectItemRegistry {
733 fn register<T: ProjectItem>(&mut self) {
734 self.build_project_item_fns_by_type.insert(
735 TypeId::of::<T::Item>(),
736 |item, project, pane, window, cx| {
737 let item = item.downcast().unwrap();
738 Box::new(cx.new(|cx| T::for_project_item(project, pane, item, window, cx)))
739 as Box<dyn ItemHandle>
740 },
741 );
742 self.build_project_item_for_path_fns
743 .push(|project, project_path, window, cx| {
744 let project_path = project_path.clone();
745 let is_file = project
746 .read(cx)
747 .entry_for_path(&project_path, cx)
748 .is_some_and(|entry| entry.is_file());
749 let entry_abs_path = project.read(cx).absolute_path(&project_path, cx);
750 let is_local = project.read(cx).is_local();
751 let project_item =
752 <T::Item as project::ProjectItem>::try_open(project, &project_path, cx)?;
753 let project = project.clone();
754 Some(window.spawn(cx, async move |cx| {
755 match project_item.await.with_context(|| {
756 format!(
757 "opening project path {:?}",
758 entry_abs_path.as_deref().unwrap_or(&project_path.path.as_std_path())
759 )
760 }) {
761 Ok(project_item) => {
762 let project_item = project_item;
763 let project_entry_id: Option<ProjectEntryId> =
764 project_item.read_with(cx, project::ProjectItem::entry_id);
765 let build_workspace_item = Box::new(
766 |pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
767 Box::new(cx.new(|cx| {
768 T::for_project_item(
769 project,
770 Some(pane),
771 project_item,
772 window,
773 cx,
774 )
775 })) as Box<dyn ItemHandle>
776 },
777 ) as Box<_>;
778 Ok((project_entry_id, build_workspace_item))
779 }
780 Err(e) => {
781 log::warn!("Failed to open a project item: {e:#}");
782 if e.error_code() == ErrorCode::Internal {
783 if let Some(abs_path) =
784 entry_abs_path.as_deref().filter(|_| is_file)
785 {
786 if let Some(broken_project_item_view) =
787 cx.update(|window, cx| {
788 T::for_broken_project_item(
789 abs_path, is_local, &e, window, cx,
790 )
791 })?
792 {
793 let build_workspace_item = Box::new(
794 move |_: &mut Pane, _: &mut Window, cx: &mut Context<Pane>| {
795 cx.new(|_| broken_project_item_view).boxed_clone()
796 },
797 )
798 as Box<_>;
799 return Ok((None, build_workspace_item));
800 }
801 }
802 }
803 Err(e)
804 }
805 }
806 }))
807 });
808 }
809
810 fn open_path(
811 &self,
812 project: &Entity<Project>,
813 path: &ProjectPath,
814 window: &mut Window,
815 cx: &mut App,
816 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
817 let Some(open_project_item) = self
818 .build_project_item_for_path_fns
819 .iter()
820 .rev()
821 .find_map(|open_project_item| open_project_item(project, path, window, cx))
822 else {
823 return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
824 };
825 open_project_item
826 }
827
828 fn build_item<T: project::ProjectItem>(
829 &self,
830 item: Entity<T>,
831 project: Entity<Project>,
832 pane: Option<&Pane>,
833 window: &mut Window,
834 cx: &mut App,
835 ) -> Option<Box<dyn ItemHandle>> {
836 let build = self
837 .build_project_item_fns_by_type
838 .get(&TypeId::of::<T>())?;
839 Some(build(item.into_any(), project, pane, window, cx))
840 }
841}
842
843type WorkspaceItemBuilder =
844 Box<dyn FnOnce(&mut Pane, &mut Window, &mut Context<Pane>) -> Box<dyn ItemHandle>>;
845
846impl Global for ProjectItemRegistry {}
847
848/// Registers a [ProjectItem] for the app. When opening a file, all the registered
849/// items will get a chance to open the file, starting from the project item that
850/// was added last.
851pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
852 cx.default_global::<ProjectItemRegistry>().register::<I>();
853}
854
855#[derive(Default)]
856pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
857
858struct FollowableViewDescriptor {
859 from_state_proto: fn(
860 Entity<Workspace>,
861 ViewId,
862 &mut Option<proto::view::Variant>,
863 &mut Window,
864 &mut App,
865 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
866 to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
867}
868
869impl Global for FollowableViewRegistry {}
870
871impl FollowableViewRegistry {
872 pub fn register<I: FollowableItem>(cx: &mut App) {
873 cx.default_global::<Self>().0.insert(
874 TypeId::of::<I>(),
875 FollowableViewDescriptor {
876 from_state_proto: |workspace, id, state, window, cx| {
877 I::from_state_proto(workspace, id, state, window, cx).map(|task| {
878 cx.foreground_executor()
879 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
880 })
881 },
882 to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
883 },
884 );
885 }
886
887 pub fn from_state_proto(
888 workspace: Entity<Workspace>,
889 view_id: ViewId,
890 mut state: Option<proto::view::Variant>,
891 window: &mut Window,
892 cx: &mut App,
893 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
894 cx.update_default_global(|this: &mut Self, cx| {
895 this.0.values().find_map(|descriptor| {
896 (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, window, cx)
897 })
898 })
899 }
900
901 pub fn to_followable_view(
902 view: impl Into<AnyView>,
903 cx: &App,
904 ) -> Option<Box<dyn FollowableItemHandle>> {
905 let this = cx.try_global::<Self>()?;
906 let view = view.into();
907 let descriptor = this.0.get(&view.entity_type())?;
908 Some((descriptor.to_followable_view)(&view))
909 }
910}
911
912#[derive(Copy, Clone)]
913struct SerializableItemDescriptor {
914 deserialize: fn(
915 Entity<Project>,
916 WeakEntity<Workspace>,
917 WorkspaceId,
918 ItemId,
919 &mut Window,
920 &mut Context<Pane>,
921 ) -> Task<Result<Box<dyn ItemHandle>>>,
922 cleanup: fn(WorkspaceId, Vec<ItemId>, &mut Window, &mut App) -> Task<Result<()>>,
923 view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
924}
925
926#[derive(Default)]
927struct SerializableItemRegistry {
928 descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
929 descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
930}
931
932impl Global for SerializableItemRegistry {}
933
934impl SerializableItemRegistry {
935 fn deserialize(
936 item_kind: &str,
937 project: Entity<Project>,
938 workspace: WeakEntity<Workspace>,
939 workspace_id: WorkspaceId,
940 item_item: ItemId,
941 window: &mut Window,
942 cx: &mut Context<Pane>,
943 ) -> Task<Result<Box<dyn ItemHandle>>> {
944 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
945 return Task::ready(Err(anyhow!(
946 "cannot deserialize {}, descriptor not found",
947 item_kind
948 )));
949 };
950
951 (descriptor.deserialize)(project, workspace, workspace_id, item_item, window, cx)
952 }
953
954 fn cleanup(
955 item_kind: &str,
956 workspace_id: WorkspaceId,
957 loaded_items: Vec<ItemId>,
958 window: &mut Window,
959 cx: &mut App,
960 ) -> Task<Result<()>> {
961 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
962 return Task::ready(Err(anyhow!(
963 "cannot cleanup {}, descriptor not found",
964 item_kind
965 )));
966 };
967
968 (descriptor.cleanup)(workspace_id, loaded_items, window, cx)
969 }
970
971 fn view_to_serializable_item_handle(
972 view: AnyView,
973 cx: &App,
974 ) -> Option<Box<dyn SerializableItemHandle>> {
975 let this = cx.try_global::<Self>()?;
976 let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
977 Some((descriptor.view_to_serializable_item)(view))
978 }
979
980 fn descriptor(item_kind: &str, cx: &App) -> Option<SerializableItemDescriptor> {
981 let this = cx.try_global::<Self>()?;
982 this.descriptors_by_kind.get(item_kind).copied()
983 }
984}
985
986pub fn register_serializable_item<I: SerializableItem>(cx: &mut App) {
987 let serialized_item_kind = I::serialized_item_kind();
988
989 let registry = cx.default_global::<SerializableItemRegistry>();
990 let descriptor = SerializableItemDescriptor {
991 deserialize: |project, workspace, workspace_id, item_id, window, cx| {
992 let task = I::deserialize(project, workspace, workspace_id, item_id, window, cx);
993 cx.foreground_executor()
994 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
995 },
996 cleanup: |workspace_id, loaded_items, window, cx| {
997 I::cleanup(workspace_id, loaded_items, window, cx)
998 },
999 view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
1000 };
1001 registry
1002 .descriptors_by_kind
1003 .insert(Arc::from(serialized_item_kind), descriptor);
1004 registry
1005 .descriptors_by_type
1006 .insert(TypeId::of::<I>(), descriptor);
1007}
1008
1009pub struct AppState {
1010 pub languages: Arc<LanguageRegistry>,
1011 pub client: Arc<Client>,
1012 pub user_store: Entity<UserStore>,
1013 pub workspace_store: Entity<WorkspaceStore>,
1014 pub fs: Arc<dyn fs::Fs>,
1015 pub build_window_options: fn(Option<Uuid>, &mut App) -> WindowOptions,
1016 pub node_runtime: NodeRuntime,
1017 pub session: Entity<AppSession>,
1018}
1019
1020struct GlobalAppState(Weak<AppState>);
1021
1022impl Global for GlobalAppState {}
1023
1024pub struct WorkspaceStore {
1025 workspaces: HashSet<(gpui::AnyWindowHandle, WeakEntity<Workspace>)>,
1026 client: Arc<Client>,
1027 _subscriptions: Vec<client::Subscription>,
1028}
1029
1030#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
1031pub enum CollaboratorId {
1032 PeerId(PeerId),
1033 Agent,
1034}
1035
1036impl From<PeerId> for CollaboratorId {
1037 fn from(peer_id: PeerId) -> Self {
1038 CollaboratorId::PeerId(peer_id)
1039 }
1040}
1041
1042impl From<&PeerId> for CollaboratorId {
1043 fn from(peer_id: &PeerId) -> Self {
1044 CollaboratorId::PeerId(*peer_id)
1045 }
1046}
1047
1048#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
1049struct Follower {
1050 project_id: Option<u64>,
1051 peer_id: PeerId,
1052}
1053
1054impl AppState {
1055 #[track_caller]
1056 pub fn global(cx: &App) -> Weak<Self> {
1057 cx.global::<GlobalAppState>().0.clone()
1058 }
1059 pub fn try_global(cx: &App) -> Option<Weak<Self>> {
1060 cx.try_global::<GlobalAppState>()
1061 .map(|state| state.0.clone())
1062 }
1063 pub fn set_global(state: Weak<AppState>, cx: &mut App) {
1064 cx.set_global(GlobalAppState(state));
1065 }
1066
1067 #[cfg(any(test, feature = "test-support"))]
1068 pub fn test(cx: &mut App) -> Arc<Self> {
1069 use fs::Fs;
1070 use node_runtime::NodeRuntime;
1071 use session::Session;
1072 use settings::SettingsStore;
1073
1074 if !cx.has_global::<SettingsStore>() {
1075 let settings_store = SettingsStore::test(cx);
1076 cx.set_global(settings_store);
1077 }
1078
1079 let fs = fs::FakeFs::new(cx.background_executor().clone());
1080 <dyn Fs>::set_global(fs.clone(), cx);
1081 let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
1082 let clock = Arc::new(clock::FakeSystemClock::new());
1083 let http_client = http_client::FakeHttpClient::with_404_response();
1084 let client = Client::new(clock, http_client, cx);
1085 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
1086 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1087 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
1088
1089 theme::init(theme::LoadThemes::JustBase, cx);
1090 client::init(&client, cx);
1091
1092 Arc::new(Self {
1093 client,
1094 fs,
1095 languages,
1096 user_store,
1097 workspace_store,
1098 node_runtime: NodeRuntime::unavailable(),
1099 build_window_options: |_, _| Default::default(),
1100 session,
1101 })
1102 }
1103}
1104
1105struct DelayedDebouncedEditAction {
1106 task: Option<Task<()>>,
1107 cancel_channel: Option<oneshot::Sender<()>>,
1108}
1109
1110impl DelayedDebouncedEditAction {
1111 fn new() -> DelayedDebouncedEditAction {
1112 DelayedDebouncedEditAction {
1113 task: None,
1114 cancel_channel: None,
1115 }
1116 }
1117
1118 fn fire_new<F>(
1119 &mut self,
1120 delay: Duration,
1121 window: &mut Window,
1122 cx: &mut Context<Workspace>,
1123 func: F,
1124 ) where
1125 F: 'static
1126 + Send
1127 + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> Task<Result<()>>,
1128 {
1129 if let Some(channel) = self.cancel_channel.take() {
1130 _ = channel.send(());
1131 }
1132
1133 let (sender, mut receiver) = oneshot::channel::<()>();
1134 self.cancel_channel = Some(sender);
1135
1136 let previous_task = self.task.take();
1137 self.task = Some(cx.spawn_in(window, async move |workspace, cx| {
1138 let mut timer = cx.background_executor().timer(delay).fuse();
1139 if let Some(previous_task) = previous_task {
1140 previous_task.await;
1141 }
1142
1143 futures::select_biased! {
1144 _ = receiver => return,
1145 _ = timer => {}
1146 }
1147
1148 if let Some(result) = workspace
1149 .update_in(cx, |workspace, window, cx| (func)(workspace, window, cx))
1150 .log_err()
1151 {
1152 result.await.log_err();
1153 }
1154 }));
1155 }
1156}
1157
1158pub enum Event {
1159 PaneAdded(Entity<Pane>),
1160 PaneRemoved,
1161 ItemAdded {
1162 item: Box<dyn ItemHandle>,
1163 },
1164 ActiveItemChanged,
1165 ItemRemoved {
1166 item_id: EntityId,
1167 },
1168 UserSavedItem {
1169 pane: WeakEntity<Pane>,
1170 item: Box<dyn WeakItemHandle>,
1171 save_intent: SaveIntent,
1172 },
1173 ContactRequestedJoin(u64),
1174 WorkspaceCreated(WeakEntity<Workspace>),
1175 OpenBundledFile {
1176 text: Cow<'static, str>,
1177 title: &'static str,
1178 language: &'static str,
1179 },
1180 ZoomChanged,
1181 ModalOpened,
1182}
1183
1184#[derive(Debug, Clone)]
1185pub enum OpenVisible {
1186 All,
1187 None,
1188 OnlyFiles,
1189 OnlyDirectories,
1190}
1191
1192enum WorkspaceLocation {
1193 // Valid local paths or SSH project to serialize
1194 Location(SerializedWorkspaceLocation, PathList),
1195 // No valid location found hence clear session id
1196 DetachFromSession,
1197 // No valid location found to serialize
1198 None,
1199}
1200
1201type PromptForNewPath = Box<
1202 dyn Fn(
1203 &mut Workspace,
1204 DirectoryLister,
1205 Option<String>,
1206 &mut Window,
1207 &mut Context<Workspace>,
1208 ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
1209>;
1210
1211type PromptForOpenPath = Box<
1212 dyn Fn(
1213 &mut Workspace,
1214 DirectoryLister,
1215 &mut Window,
1216 &mut Context<Workspace>,
1217 ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
1218>;
1219
1220#[derive(Default)]
1221struct DispatchingKeystrokes {
1222 dispatched: HashSet<Vec<Keystroke>>,
1223 queue: VecDeque<Keystroke>,
1224 task: Option<Shared<Task<()>>>,
1225}
1226
1227/// Collects everything project-related for a certain window opened.
1228/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
1229///
1230/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
1231/// The `Workspace` owns everybody's state and serves as a default, "global context",
1232/// that can be used to register a global action to be triggered from any place in the window.
1233pub struct Workspace {
1234 weak_self: WeakEntity<Self>,
1235 workspace_actions: Vec<Box<dyn Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div>>,
1236 zoomed: Option<AnyWeakView>,
1237 previous_dock_drag_coordinates: Option<Point<Pixels>>,
1238 zoomed_position: Option<DockPosition>,
1239 center: PaneGroup,
1240 left_dock: Entity<Dock>,
1241 bottom_dock: Entity<Dock>,
1242 right_dock: Entity<Dock>,
1243 panes: Vec<Entity<Pane>>,
1244 active_worktree_override: Option<WorktreeId>,
1245 panes_by_item: HashMap<EntityId, WeakEntity<Pane>>,
1246 active_pane: Entity<Pane>,
1247 last_active_center_pane: Option<WeakEntity<Pane>>,
1248 last_active_view_id: Option<proto::ViewId>,
1249 status_bar: Entity<StatusBar>,
1250 modal_layer: Entity<ModalLayer>,
1251 toast_layer: Entity<ToastLayer>,
1252 titlebar_item: Option<AnyView>,
1253 notifications: Notifications,
1254 suppressed_notifications: HashSet<NotificationId>,
1255 project: Entity<Project>,
1256 follower_states: HashMap<CollaboratorId, FollowerState>,
1257 last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
1258 window_edited: bool,
1259 last_window_title: Option<String>,
1260 dirty_items: HashMap<EntityId, Subscription>,
1261 active_call: Option<(Entity<ActiveCall>, Vec<Subscription>)>,
1262 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
1263 database_id: Option<WorkspaceId>,
1264 app_state: Arc<AppState>,
1265 dispatching_keystrokes: Rc<RefCell<DispatchingKeystrokes>>,
1266 _subscriptions: Vec<Subscription>,
1267 _apply_leader_updates: Task<Result<()>>,
1268 _observe_current_user: Task<Result<()>>,
1269 _schedule_serialize_workspace: Option<Task<()>>,
1270 _serialize_workspace_task: Option<Task<()>>,
1271 _schedule_serialize_ssh_paths: Option<Task<()>>,
1272 pane_history_timestamp: Arc<AtomicUsize>,
1273 bounds: Bounds<Pixels>,
1274 pub centered_layout: bool,
1275 bounds_save_task_queued: Option<Task<()>>,
1276 on_prompt_for_new_path: Option<PromptForNewPath>,
1277 on_prompt_for_open_path: Option<PromptForOpenPath>,
1278 terminal_provider: Option<Box<dyn TerminalProvider>>,
1279 debugger_provider: Option<Arc<dyn DebuggerProvider>>,
1280 serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
1281 _items_serializer: Task<Result<()>>,
1282 session_id: Option<String>,
1283 scheduled_tasks: Vec<Task<()>>,
1284 last_open_dock_positions: Vec<DockPosition>,
1285 removing: bool,
1286}
1287
1288impl EventEmitter<Event> for Workspace {}
1289
1290#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
1291pub struct ViewId {
1292 pub creator: CollaboratorId,
1293 pub id: u64,
1294}
1295
1296pub struct FollowerState {
1297 center_pane: Entity<Pane>,
1298 dock_pane: Option<Entity<Pane>>,
1299 active_view_id: Option<ViewId>,
1300 items_by_leader_view_id: HashMap<ViewId, FollowerView>,
1301}
1302
1303struct FollowerView {
1304 view: Box<dyn FollowableItemHandle>,
1305 location: Option<proto::PanelId>,
1306}
1307
1308impl Workspace {
1309 pub fn new(
1310 workspace_id: Option<WorkspaceId>,
1311 project: Entity<Project>,
1312 app_state: Arc<AppState>,
1313 window: &mut Window,
1314 cx: &mut Context<Self>,
1315 ) -> Self {
1316 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1317 cx.subscribe(&trusted_worktrees, |_, worktrees_store, e, cx| {
1318 if let TrustedWorktreesEvent::Trusted(..) = e {
1319 // Do not persist auto trusted worktrees
1320 if !ProjectSettings::get_global(cx).session.trust_all_worktrees {
1321 worktrees_store.update(cx, |worktrees_store, cx| {
1322 worktrees_store.schedule_serialization(
1323 cx,
1324 |new_trusted_worktrees, cx| {
1325 let timeout =
1326 cx.background_executor().timer(SERIALIZATION_THROTTLE_TIME);
1327 cx.background_spawn(async move {
1328 timeout.await;
1329 persistence::DB
1330 .save_trusted_worktrees(new_trusted_worktrees)
1331 .await
1332 .log_err();
1333 })
1334 },
1335 )
1336 });
1337 }
1338 }
1339 })
1340 .detach();
1341
1342 cx.observe_global::<SettingsStore>(|_, cx| {
1343 if ProjectSettings::get_global(cx).session.trust_all_worktrees {
1344 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1345 trusted_worktrees.update(cx, |trusted_worktrees, cx| {
1346 trusted_worktrees.auto_trust_all(cx);
1347 })
1348 }
1349 }
1350 })
1351 .detach();
1352 }
1353
1354 cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
1355 match event {
1356 project::Event::RemoteIdChanged(_) => {
1357 this.update_window_title(window, cx);
1358 }
1359
1360 project::Event::CollaboratorLeft(peer_id) => {
1361 this.collaborator_left(*peer_id, window, cx);
1362 }
1363
1364 &project::Event::WorktreeRemoved(id) | &project::Event::WorktreeAdded(id) => {
1365 this.update_window_title(window, cx);
1366 if this
1367 .project()
1368 .read(cx)
1369 .worktree_for_id(id, cx)
1370 .is_some_and(|wt| wt.read(cx).is_visible())
1371 {
1372 this.serialize_workspace(window, cx);
1373 this.update_history(cx);
1374 }
1375 }
1376 project::Event::WorktreeUpdatedEntries(..) => {
1377 this.update_window_title(window, cx);
1378 this.serialize_workspace(window, cx);
1379 }
1380
1381 project::Event::DisconnectedFromHost => {
1382 this.update_window_edited(window, cx);
1383 let leaders_to_unfollow =
1384 this.follower_states.keys().copied().collect::<Vec<_>>();
1385 for leader_id in leaders_to_unfollow {
1386 this.unfollow(leader_id, window, cx);
1387 }
1388 }
1389
1390 project::Event::DisconnectedFromRemote {
1391 server_not_running: _,
1392 } => {
1393 this.update_window_edited(window, cx);
1394 }
1395
1396 project::Event::Closed => {
1397 window.remove_window();
1398 }
1399
1400 project::Event::DeletedEntry(_, entry_id) => {
1401 for pane in this.panes.iter() {
1402 pane.update(cx, |pane, cx| {
1403 pane.handle_deleted_project_item(*entry_id, window, cx)
1404 });
1405 }
1406 }
1407
1408 project::Event::Toast {
1409 notification_id,
1410 message,
1411 link,
1412 } => this.show_notification(
1413 NotificationId::named(notification_id.clone()),
1414 cx,
1415 |cx| {
1416 let mut notification = MessageNotification::new(message.clone(), cx);
1417 if let Some(link) = link {
1418 notification = notification
1419 .more_info_message(link.label)
1420 .more_info_url(link.url);
1421 }
1422
1423 cx.new(|_| notification)
1424 },
1425 ),
1426
1427 project::Event::HideToast { notification_id } => {
1428 this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
1429 }
1430
1431 project::Event::LanguageServerPrompt(request) => {
1432 struct LanguageServerPrompt;
1433
1434 this.show_notification(
1435 NotificationId::composite::<LanguageServerPrompt>(request.id),
1436 cx,
1437 |cx| {
1438 cx.new(|cx| {
1439 notifications::LanguageServerPrompt::new(request.clone(), cx)
1440 })
1441 },
1442 );
1443 }
1444
1445 project::Event::AgentLocationChanged => {
1446 this.handle_agent_location_changed(window, cx)
1447 }
1448
1449 _ => {}
1450 }
1451 cx.notify()
1452 })
1453 .detach();
1454
1455 cx.subscribe_in(
1456 &project.read(cx).breakpoint_store(),
1457 window,
1458 |workspace, _, event, window, cx| match event {
1459 BreakpointStoreEvent::BreakpointsUpdated(_, _)
1460 | BreakpointStoreEvent::BreakpointsCleared(_) => {
1461 workspace.serialize_workspace(window, cx);
1462 }
1463 BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {}
1464 },
1465 )
1466 .detach();
1467 if let Some(toolchain_store) = project.read(cx).toolchain_store() {
1468 cx.subscribe_in(
1469 &toolchain_store,
1470 window,
1471 |workspace, _, event, window, cx| match event {
1472 ToolchainStoreEvent::CustomToolchainsModified => {
1473 workspace.serialize_workspace(window, cx);
1474 }
1475 _ => {}
1476 },
1477 )
1478 .detach();
1479 }
1480
1481 cx.on_focus_lost(window, |this, window, cx| {
1482 let focus_handle = this.focus_handle(cx);
1483 window.focus(&focus_handle, cx);
1484 })
1485 .detach();
1486
1487 let weak_handle = cx.entity().downgrade();
1488 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
1489
1490 let center_pane = cx.new(|cx| {
1491 let mut center_pane = Pane::new(
1492 weak_handle.clone(),
1493 project.clone(),
1494 pane_history_timestamp.clone(),
1495 None,
1496 NewFile.boxed_clone(),
1497 true,
1498 window,
1499 cx,
1500 );
1501 center_pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
1502 center_pane.set_should_display_welcome_page(true);
1503 center_pane
1504 });
1505 cx.subscribe_in(¢er_pane, window, Self::handle_pane_event)
1506 .detach();
1507
1508 window.focus(¢er_pane.focus_handle(cx), cx);
1509
1510 cx.emit(Event::PaneAdded(center_pane.clone()));
1511
1512 let any_window_handle = window.window_handle();
1513 app_state.workspace_store.update(cx, |store, _| {
1514 store
1515 .workspaces
1516 .insert((any_window_handle, weak_handle.clone()));
1517 });
1518
1519 let mut current_user = app_state.user_store.read(cx).watch_current_user();
1520 let mut connection_status = app_state.client.status();
1521 let _observe_current_user = cx.spawn_in(window, async move |this, cx| {
1522 current_user.next().await;
1523 connection_status.next().await;
1524 let mut stream =
1525 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
1526
1527 while stream.recv().await.is_some() {
1528 this.update(cx, |_, cx| cx.notify())?;
1529 }
1530 anyhow::Ok(())
1531 });
1532
1533 // All leader updates are enqueued and then processed in a single task, so
1534 // that each asynchronous operation can be run in order.
1535 let (leader_updates_tx, mut leader_updates_rx) =
1536 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
1537 let _apply_leader_updates = cx.spawn_in(window, async move |this, cx| {
1538 while let Some((leader_id, update)) = leader_updates_rx.next().await {
1539 Self::process_leader_update(&this, leader_id, update, cx)
1540 .await
1541 .log_err();
1542 }
1543
1544 Ok(())
1545 });
1546
1547 cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
1548 let modal_layer = cx.new(|_| ModalLayer::new());
1549 let toast_layer = cx.new(|_| ToastLayer::new());
1550 cx.subscribe(
1551 &modal_layer,
1552 |_, _, _: &modal_layer::ModalOpenedEvent, cx| {
1553 cx.emit(Event::ModalOpened);
1554 },
1555 )
1556 .detach();
1557
1558 let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx);
1559 let bottom_dock = Dock::new(DockPosition::Bottom, modal_layer.clone(), window, cx);
1560 let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), window, cx);
1561 let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx));
1562 let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx));
1563 let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx));
1564 let status_bar = cx.new(|cx| {
1565 let mut status_bar = StatusBar::new(¢er_pane.clone(), window, cx);
1566 status_bar.add_left_item(left_dock_buttons, window, cx);
1567 status_bar.add_right_item(right_dock_buttons, window, cx);
1568 status_bar.add_right_item(bottom_dock_buttons, window, cx);
1569 status_bar
1570 });
1571
1572 let session_id = app_state.session.read(cx).id().to_owned();
1573
1574 let mut active_call = None;
1575 if let Some(call) = ActiveCall::try_global(cx) {
1576 let subscriptions = vec![cx.subscribe_in(&call, window, Self::on_active_call_event)];
1577 active_call = Some((call, subscriptions));
1578 }
1579
1580 let (serializable_items_tx, serializable_items_rx) =
1581 mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
1582 let _items_serializer = cx.spawn_in(window, async move |this, cx| {
1583 Self::serialize_items(&this, serializable_items_rx, cx).await
1584 });
1585
1586 let subscriptions = vec![
1587 cx.observe_window_activation(window, Self::on_window_activation_changed),
1588 cx.observe_window_bounds(window, move |this, window, cx| {
1589 if this.bounds_save_task_queued.is_some() {
1590 return;
1591 }
1592 this.bounds_save_task_queued = Some(cx.spawn_in(window, async move |this, cx| {
1593 cx.background_executor()
1594 .timer(Duration::from_millis(100))
1595 .await;
1596 this.update_in(cx, |this, window, cx| {
1597 if let Some(display) = window.display(cx)
1598 && let Ok(display_uuid) = display.uuid()
1599 {
1600 let window_bounds = window.inner_window_bounds();
1601 let has_paths = !this.root_paths(cx).is_empty();
1602 if !has_paths {
1603 cx.background_executor()
1604 .spawn(persistence::write_default_window_bounds(
1605 window_bounds,
1606 display_uuid,
1607 ))
1608 .detach_and_log_err(cx);
1609 }
1610 if let Some(database_id) = workspace_id {
1611 cx.background_executor()
1612 .spawn(DB.set_window_open_status(
1613 database_id,
1614 SerializedWindowBounds(window_bounds),
1615 display_uuid,
1616 ))
1617 .detach_and_log_err(cx);
1618 } else {
1619 cx.background_executor()
1620 .spawn(persistence::write_default_window_bounds(
1621 window_bounds,
1622 display_uuid,
1623 ))
1624 .detach_and_log_err(cx);
1625 }
1626 }
1627 this.bounds_save_task_queued.take();
1628 })
1629 .ok();
1630 }));
1631 cx.notify();
1632 }),
1633 cx.observe_window_appearance(window, |_, window, cx| {
1634 let window_appearance = window.appearance();
1635
1636 *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
1637
1638 GlobalTheme::reload_theme(cx);
1639 GlobalTheme::reload_icon_theme(cx);
1640 }),
1641 cx.on_release({
1642 let weak_handle = weak_handle.clone();
1643 move |this, cx| {
1644 this.app_state.workspace_store.update(cx, move |store, _| {
1645 store.workspaces.retain(|(_, weak)| weak != &weak_handle);
1646 })
1647 }
1648 }),
1649 ];
1650
1651 cx.defer_in(window, move |this, window, cx| {
1652 this.update_window_title(window, cx);
1653 this.show_initial_notifications(cx);
1654 });
1655
1656 let mut center = PaneGroup::new(center_pane.clone());
1657 center.set_is_center(true);
1658 center.mark_positions(cx);
1659
1660 Workspace {
1661 weak_self: weak_handle.clone(),
1662 zoomed: None,
1663 zoomed_position: None,
1664 previous_dock_drag_coordinates: None,
1665 center,
1666 panes: vec![center_pane.clone()],
1667 panes_by_item: Default::default(),
1668 active_pane: center_pane.clone(),
1669 last_active_center_pane: Some(center_pane.downgrade()),
1670 last_active_view_id: None,
1671 status_bar,
1672 modal_layer,
1673 toast_layer,
1674 titlebar_item: None,
1675 active_worktree_override: None,
1676 notifications: Notifications::default(),
1677 suppressed_notifications: HashSet::default(),
1678 left_dock,
1679 bottom_dock,
1680 right_dock,
1681 project: project.clone(),
1682 follower_states: Default::default(),
1683 last_leaders_by_pane: Default::default(),
1684 dispatching_keystrokes: Default::default(),
1685 window_edited: false,
1686 last_window_title: None,
1687 dirty_items: Default::default(),
1688 active_call,
1689 database_id: workspace_id,
1690 app_state,
1691 _observe_current_user,
1692 _apply_leader_updates,
1693 _schedule_serialize_workspace: None,
1694 _serialize_workspace_task: None,
1695 _schedule_serialize_ssh_paths: None,
1696 leader_updates_tx,
1697 _subscriptions: subscriptions,
1698 pane_history_timestamp,
1699 workspace_actions: Default::default(),
1700 // This data will be incorrect, but it will be overwritten by the time it needs to be used.
1701 bounds: Default::default(),
1702 centered_layout: false,
1703 bounds_save_task_queued: None,
1704 on_prompt_for_new_path: None,
1705 on_prompt_for_open_path: None,
1706 terminal_provider: None,
1707 debugger_provider: None,
1708 serializable_items_tx,
1709 _items_serializer,
1710 session_id: Some(session_id),
1711
1712 scheduled_tasks: Vec::new(),
1713 last_open_dock_positions: Vec::new(),
1714 removing: false,
1715 }
1716 }
1717
1718 pub fn new_local(
1719 abs_paths: Vec<PathBuf>,
1720 app_state: Arc<AppState>,
1721 requesting_window: Option<WindowHandle<MultiWorkspace>>,
1722 env: Option<HashMap<String, String>>,
1723 init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
1724 cx: &mut App,
1725 ) -> Task<
1726 anyhow::Result<(
1727 WindowHandle<MultiWorkspace>,
1728 Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
1729 )>,
1730 > {
1731 let project_handle = Project::local(
1732 app_state.client.clone(),
1733 app_state.node_runtime.clone(),
1734 app_state.user_store.clone(),
1735 app_state.languages.clone(),
1736 app_state.fs.clone(),
1737 env,
1738 Default::default(),
1739 cx,
1740 );
1741
1742 cx.spawn(async move |cx| {
1743 let mut paths_to_open = Vec::with_capacity(abs_paths.len());
1744 for path in abs_paths.into_iter() {
1745 if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() {
1746 paths_to_open.push(canonical)
1747 } else {
1748 paths_to_open.push(path)
1749 }
1750 }
1751
1752 let serialized_workspace =
1753 persistence::DB.workspace_for_roots(paths_to_open.as_slice());
1754
1755 if let Some(paths) = serialized_workspace.as_ref().map(|ws| &ws.paths) {
1756 paths_to_open = paths.ordered_paths().cloned().collect();
1757 if !paths.is_lexicographically_ordered() {
1758 project_handle.update(cx, |project, cx| {
1759 project.set_worktrees_reordered(true, cx);
1760 });
1761 }
1762 }
1763
1764 // Get project paths for all of the abs_paths
1765 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
1766 Vec::with_capacity(paths_to_open.len());
1767
1768 for path in paths_to_open.into_iter() {
1769 if let Some((_, project_entry)) = cx
1770 .update(|cx| {
1771 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
1772 })
1773 .await
1774 .log_err()
1775 {
1776 project_paths.push((path, Some(project_entry)));
1777 } else {
1778 project_paths.push((path, None));
1779 }
1780 }
1781
1782 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
1783 serialized_workspace.id
1784 } else {
1785 DB.next_id().await.unwrap_or_else(|_| Default::default())
1786 };
1787
1788 let toolchains = DB.toolchains(workspace_id).await?;
1789
1790 for (toolchain, worktree_path, path) in toolchains {
1791 let toolchain_path = PathBuf::from(toolchain.path.clone().to_string());
1792 let Some(worktree_id) = project_handle.read_with(cx, |this, cx| {
1793 this.find_worktree(&worktree_path, cx)
1794 .and_then(|(worktree, rel_path)| {
1795 if rel_path.is_empty() {
1796 Some(worktree.read(cx).id())
1797 } else {
1798 None
1799 }
1800 })
1801 }) else {
1802 // We did not find a worktree with a given path, but that's whatever.
1803 continue;
1804 };
1805 if !app_state.fs.is_file(toolchain_path.as_path()).await {
1806 continue;
1807 }
1808
1809 project_handle
1810 .update(cx, |this, cx| {
1811 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
1812 })
1813 .await;
1814 }
1815 if let Some(workspace) = serialized_workspace.as_ref() {
1816 project_handle.update(cx, |this, cx| {
1817 for (scope, toolchains) in &workspace.user_toolchains {
1818 for toolchain in toolchains {
1819 this.add_toolchain(toolchain.clone(), scope.clone(), cx);
1820 }
1821 }
1822 });
1823 }
1824
1825 let (window, workspace): (WindowHandle<MultiWorkspace>, Entity<Workspace>) =
1826 if let Some(window) = requesting_window {
1827 let centered_layout = serialized_workspace
1828 .as_ref()
1829 .map(|w| w.centered_layout)
1830 .unwrap_or(false);
1831
1832 let workspace = window.update(cx, |multi_workspace, window, cx| {
1833 let workspace = cx.new(|cx| {
1834 let mut workspace = Workspace::new(
1835 Some(workspace_id),
1836 project_handle.clone(),
1837 app_state.clone(),
1838 window,
1839 cx,
1840 );
1841
1842 workspace.centered_layout = centered_layout;
1843
1844 // Call init callback to add items before window renders
1845 if let Some(init) = init {
1846 init(&mut workspace, window, cx);
1847 }
1848
1849 workspace
1850 });
1851 multi_workspace.activate(workspace.clone(), cx);
1852 workspace
1853 })?;
1854 (window, workspace)
1855 } else {
1856 let window_bounds_override = window_bounds_env_override();
1857
1858 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
1859 (Some(WindowBounds::Windowed(bounds)), None)
1860 } else if let Some(workspace) = serialized_workspace.as_ref()
1861 && let Some(display) = workspace.display
1862 && let Some(bounds) = workspace.window_bounds.as_ref()
1863 {
1864 // Reopening an existing workspace - restore its saved bounds
1865 (Some(bounds.0), Some(display))
1866 } else if let Some((display, bounds)) =
1867 persistence::read_default_window_bounds()
1868 {
1869 // New or empty workspace - use the last known window bounds
1870 (Some(bounds), Some(display))
1871 } else {
1872 // New window - let GPUI's default_bounds() handle cascading
1873 (None, None)
1874 };
1875
1876 // Use the serialized workspace to construct the new window
1877 let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx));
1878 options.window_bounds = window_bounds;
1879 let centered_layout = serialized_workspace
1880 .as_ref()
1881 .map(|w| w.centered_layout)
1882 .unwrap_or(false);
1883 let window = cx.open_window(options, {
1884 let app_state = app_state.clone();
1885 let project_handle = project_handle.clone();
1886 move |window, cx| {
1887 let workspace = cx.new(|cx| {
1888 let mut workspace = Workspace::new(
1889 Some(workspace_id),
1890 project_handle,
1891 app_state,
1892 window,
1893 cx,
1894 );
1895 workspace.centered_layout = centered_layout;
1896
1897 // Call init callback to add items before window renders
1898 if let Some(init) = init {
1899 init(&mut workspace, window, cx);
1900 }
1901
1902 workspace
1903 });
1904 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
1905 }
1906 })?;
1907 let workspace =
1908 window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
1909 multi_workspace.workspace().clone()
1910 })?;
1911 (window, workspace)
1912 };
1913
1914 notify_if_database_failed(window, cx);
1915 // Check if this is an empty workspace (no paths to open)
1916 // An empty workspace is one where project_paths is empty
1917 let is_empty_workspace = project_paths.is_empty();
1918 // Check if serialized workspace has paths before it's moved
1919 let serialized_workspace_has_paths = serialized_workspace
1920 .as_ref()
1921 .map(|ws| !ws.paths.is_empty())
1922 .unwrap_or(false);
1923
1924 let opened_items = window
1925 .update(cx, |_, window, cx| {
1926 workspace.update(cx, |_workspace: &mut Workspace, cx| {
1927 open_items(serialized_workspace, project_paths, window, cx)
1928 })
1929 })?
1930 .await
1931 .unwrap_or_default();
1932
1933 // Restore default dock state for empty workspaces
1934 // Only restore if:
1935 // 1. This is an empty workspace (no paths), AND
1936 // 2. The serialized workspace either doesn't exist or has no paths
1937 if is_empty_workspace && !serialized_workspace_has_paths {
1938 if let Some(default_docks) = persistence::read_default_dock_state() {
1939 window
1940 .update(cx, |_, window, cx| {
1941 workspace.update(cx, |workspace, cx| {
1942 for (dock, serialized_dock) in [
1943 (&workspace.right_dock, &default_docks.right),
1944 (&workspace.left_dock, &default_docks.left),
1945 (&workspace.bottom_dock, &default_docks.bottom),
1946 ] {
1947 dock.update(cx, |dock, cx| {
1948 dock.serialized_dock = Some(serialized_dock.clone());
1949 dock.restore_state(window, cx);
1950 });
1951 }
1952 cx.notify();
1953 });
1954 })
1955 .log_err();
1956 }
1957 }
1958
1959 window
1960 .update(cx, |_, _window, cx| {
1961 workspace.update(cx, |this: &mut Workspace, cx| {
1962 this.update_history(cx);
1963 });
1964 })
1965 .log_err();
1966 Ok((window, opened_items))
1967 })
1968 }
1969
1970 pub fn weak_handle(&self) -> WeakEntity<Self> {
1971 self.weak_self.clone()
1972 }
1973
1974 pub fn left_dock(&self) -> &Entity<Dock> {
1975 &self.left_dock
1976 }
1977
1978 pub fn bottom_dock(&self) -> &Entity<Dock> {
1979 &self.bottom_dock
1980 }
1981
1982 pub fn set_bottom_dock_layout(
1983 &mut self,
1984 layout: BottomDockLayout,
1985 window: &mut Window,
1986 cx: &mut Context<Self>,
1987 ) {
1988 let fs = self.project().read(cx).fs();
1989 settings::update_settings_file(fs.clone(), cx, move |content, _cx| {
1990 content.workspace.bottom_dock_layout = Some(layout);
1991 });
1992
1993 cx.notify();
1994 self.serialize_workspace(window, cx);
1995 }
1996
1997 pub fn right_dock(&self) -> &Entity<Dock> {
1998 &self.right_dock
1999 }
2000
2001 pub fn all_docks(&self) -> [&Entity<Dock>; 3] {
2002 [&self.left_dock, &self.bottom_dock, &self.right_dock]
2003 }
2004
2005 pub fn dock_at_position(&self, position: DockPosition) -> &Entity<Dock> {
2006 match position {
2007 DockPosition::Left => &self.left_dock,
2008 DockPosition::Bottom => &self.bottom_dock,
2009 DockPosition::Right => &self.right_dock,
2010 }
2011 }
2012
2013 pub fn is_edited(&self) -> bool {
2014 self.window_edited
2015 }
2016
2017 pub fn add_panel<T: Panel>(
2018 &mut self,
2019 panel: Entity<T>,
2020 window: &mut Window,
2021 cx: &mut Context<Self>,
2022 ) {
2023 let focus_handle = panel.panel_focus_handle(cx);
2024 cx.on_focus_in(&focus_handle, window, Self::handle_panel_focused)
2025 .detach();
2026
2027 let dock_position = panel.position(window, cx);
2028 let dock = self.dock_at_position(dock_position);
2029
2030 dock.update(cx, |dock, cx| {
2031 dock.add_panel(panel, self.weak_self.clone(), window, cx)
2032 });
2033 }
2034
2035 pub fn remove_panel<T: Panel>(
2036 &mut self,
2037 panel: &Entity<T>,
2038 window: &mut Window,
2039 cx: &mut Context<Self>,
2040 ) {
2041 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2042 dock.update(cx, |dock, cx| dock.remove_panel(panel, window, cx));
2043 }
2044 }
2045
2046 pub fn status_bar(&self) -> &Entity<StatusBar> {
2047 &self.status_bar
2048 }
2049
2050 pub fn set_workspace_sidebar_open(&self, open: bool, cx: &mut App) {
2051 self.status_bar.update(cx, |status_bar, cx| {
2052 status_bar.set_workspace_sidebar_open(open, cx);
2053 });
2054 }
2055
2056 pub fn status_bar_visible(&self, cx: &App) -> bool {
2057 StatusBarSettings::get_global(cx).show
2058 }
2059
2060 pub fn app_state(&self) -> &Arc<AppState> {
2061 &self.app_state
2062 }
2063
2064 pub fn user_store(&self) -> &Entity<UserStore> {
2065 &self.app_state.user_store
2066 }
2067
2068 pub fn project(&self) -> &Entity<Project> {
2069 &self.project
2070 }
2071
2072 pub fn path_style(&self, cx: &App) -> PathStyle {
2073 self.project.read(cx).path_style(cx)
2074 }
2075
2076 pub fn recently_activated_items(&self, cx: &App) -> HashMap<EntityId, usize> {
2077 let mut history: HashMap<EntityId, usize> = HashMap::default();
2078
2079 for pane_handle in &self.panes {
2080 let pane = pane_handle.read(cx);
2081
2082 for entry in pane.activation_history() {
2083 history.insert(
2084 entry.entity_id,
2085 history
2086 .get(&entry.entity_id)
2087 .cloned()
2088 .unwrap_or(0)
2089 .max(entry.timestamp),
2090 );
2091 }
2092 }
2093
2094 history
2095 }
2096
2097 pub fn recent_active_item_by_type<T: 'static>(&self, cx: &App) -> Option<Entity<T>> {
2098 let mut recent_item: Option<Entity<T>> = None;
2099 let mut recent_timestamp = 0;
2100 for pane_handle in &self.panes {
2101 let pane = pane_handle.read(cx);
2102 let item_map: HashMap<EntityId, &Box<dyn ItemHandle>> =
2103 pane.items().map(|item| (item.item_id(), item)).collect();
2104 for entry in pane.activation_history() {
2105 if entry.timestamp > recent_timestamp
2106 && let Some(&item) = item_map.get(&entry.entity_id)
2107 && let Some(typed_item) = item.act_as::<T>(cx)
2108 {
2109 recent_timestamp = entry.timestamp;
2110 recent_item = Some(typed_item);
2111 }
2112 }
2113 }
2114 recent_item
2115 }
2116
2117 pub fn recent_navigation_history_iter(
2118 &self,
2119 cx: &App,
2120 ) -> impl Iterator<Item = (ProjectPath, Option<PathBuf>)> + use<> {
2121 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
2122 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
2123
2124 for pane in &self.panes {
2125 let pane = pane.read(cx);
2126
2127 pane.nav_history()
2128 .for_each_entry(cx, &mut |entry, (project_path, fs_path)| {
2129 if let Some(fs_path) = &fs_path {
2130 abs_paths_opened
2131 .entry(fs_path.clone())
2132 .or_default()
2133 .insert(project_path.clone());
2134 }
2135 let timestamp = entry.timestamp;
2136 match history.entry(project_path) {
2137 hash_map::Entry::Occupied(mut entry) => {
2138 let (_, old_timestamp) = entry.get();
2139 if ×tamp > old_timestamp {
2140 entry.insert((fs_path, timestamp));
2141 }
2142 }
2143 hash_map::Entry::Vacant(entry) => {
2144 entry.insert((fs_path, timestamp));
2145 }
2146 }
2147 });
2148
2149 if let Some(item) = pane.active_item()
2150 && let Some(project_path) = item.project_path(cx)
2151 {
2152 let fs_path = self.project.read(cx).absolute_path(&project_path, cx);
2153
2154 if let Some(fs_path) = &fs_path {
2155 abs_paths_opened
2156 .entry(fs_path.clone())
2157 .or_default()
2158 .insert(project_path.clone());
2159 }
2160
2161 history.insert(project_path, (fs_path, std::usize::MAX));
2162 }
2163 }
2164
2165 history
2166 .into_iter()
2167 .sorted_by_key(|(_, (_, order))| *order)
2168 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
2169 .rev()
2170 .filter(move |(history_path, abs_path)| {
2171 let latest_project_path_opened = abs_path
2172 .as_ref()
2173 .and_then(|abs_path| abs_paths_opened.get(abs_path))
2174 .and_then(|project_paths| {
2175 project_paths
2176 .iter()
2177 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
2178 });
2179
2180 latest_project_path_opened.is_none_or(|path| path == history_path)
2181 })
2182 }
2183
2184 pub fn recent_navigation_history(
2185 &self,
2186 limit: Option<usize>,
2187 cx: &App,
2188 ) -> Vec<(ProjectPath, Option<PathBuf>)> {
2189 self.recent_navigation_history_iter(cx)
2190 .take(limit.unwrap_or(usize::MAX))
2191 .collect()
2192 }
2193
2194 pub fn clear_navigation_history(&mut self, _window: &mut Window, cx: &mut Context<Workspace>) {
2195 for pane in &self.panes {
2196 pane.update(cx, |pane, cx| pane.nav_history_mut().clear(cx));
2197 }
2198 }
2199
2200 fn navigate_history(
2201 &mut self,
2202 pane: WeakEntity<Pane>,
2203 mode: NavigationMode,
2204 window: &mut Window,
2205 cx: &mut Context<Workspace>,
2206 ) -> Task<Result<()>> {
2207 self.navigate_history_impl(
2208 pane,
2209 mode,
2210 window,
2211 &mut |history, cx| history.pop(mode, cx),
2212 cx,
2213 )
2214 }
2215
2216 fn navigate_tag_history(
2217 &mut self,
2218 pane: WeakEntity<Pane>,
2219 mode: TagNavigationMode,
2220 window: &mut Window,
2221 cx: &mut Context<Workspace>,
2222 ) -> Task<Result<()>> {
2223 self.navigate_history_impl(
2224 pane,
2225 NavigationMode::Normal,
2226 window,
2227 &mut |history, _cx| history.pop_tag(mode),
2228 cx,
2229 )
2230 }
2231
2232 fn navigate_history_impl(
2233 &mut self,
2234 pane: WeakEntity<Pane>,
2235 mode: NavigationMode,
2236 window: &mut Window,
2237 cb: &mut dyn FnMut(&mut NavHistory, &mut App) -> Option<NavigationEntry>,
2238 cx: &mut Context<Workspace>,
2239 ) -> Task<Result<()>> {
2240 let to_load = if let Some(pane) = pane.upgrade() {
2241 pane.update(cx, |pane, cx| {
2242 window.focus(&pane.focus_handle(cx), cx);
2243 loop {
2244 // Retrieve the weak item handle from the history.
2245 let entry = cb(pane.nav_history_mut(), cx)?;
2246
2247 // If the item is still present in this pane, then activate it.
2248 if let Some(index) = entry
2249 .item
2250 .upgrade()
2251 .and_then(|v| pane.index_for_item(v.as_ref()))
2252 {
2253 let prev_active_item_index = pane.active_item_index();
2254 pane.nav_history_mut().set_mode(mode);
2255 pane.activate_item(index, true, true, window, cx);
2256 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2257
2258 let mut navigated = prev_active_item_index != pane.active_item_index();
2259 if let Some(data) = entry.data {
2260 navigated |= pane.active_item()?.navigate(data, window, cx);
2261 }
2262
2263 if navigated {
2264 break None;
2265 }
2266 } else {
2267 // If the item is no longer present in this pane, then retrieve its
2268 // path info in order to reopen it.
2269 break pane
2270 .nav_history()
2271 .path_for_item(entry.item.id())
2272 .map(|(project_path, abs_path)| (project_path, abs_path, entry));
2273 }
2274 }
2275 })
2276 } else {
2277 None
2278 };
2279
2280 if let Some((project_path, abs_path, entry)) = to_load {
2281 // If the item was no longer present, then load it again from its previous path, first try the local path
2282 let open_by_project_path = self.load_path(project_path.clone(), window, cx);
2283
2284 cx.spawn_in(window, async move |workspace, cx| {
2285 let open_by_project_path = open_by_project_path.await;
2286 let mut navigated = false;
2287 match open_by_project_path
2288 .with_context(|| format!("Navigating to {project_path:?}"))
2289 {
2290 Ok((project_entry_id, build_item)) => {
2291 let prev_active_item_id = pane.update(cx, |pane, _| {
2292 pane.nav_history_mut().set_mode(mode);
2293 pane.active_item().map(|p| p.item_id())
2294 })?;
2295
2296 pane.update_in(cx, |pane, window, cx| {
2297 let item = pane.open_item(
2298 project_entry_id,
2299 project_path,
2300 true,
2301 entry.is_preview,
2302 true,
2303 None,
2304 window, cx,
2305 build_item,
2306 );
2307 navigated |= Some(item.item_id()) != prev_active_item_id;
2308 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2309 if let Some(data) = entry.data {
2310 navigated |= item.navigate(data, window, cx);
2311 }
2312 })?;
2313 }
2314 Err(open_by_project_path_e) => {
2315 // Fall back to opening by abs path, in case an external file was opened and closed,
2316 // and its worktree is now dropped
2317 if let Some(abs_path) = abs_path {
2318 let prev_active_item_id = pane.update(cx, |pane, _| {
2319 pane.nav_history_mut().set_mode(mode);
2320 pane.active_item().map(|p| p.item_id())
2321 })?;
2322 let open_by_abs_path = workspace.update_in(cx, |workspace, window, cx| {
2323 workspace.open_abs_path(abs_path.clone(), OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
2324 })?;
2325 match open_by_abs_path
2326 .await
2327 .with_context(|| format!("Navigating to {abs_path:?}"))
2328 {
2329 Ok(item) => {
2330 pane.update_in(cx, |pane, window, cx| {
2331 navigated |= Some(item.item_id()) != prev_active_item_id;
2332 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2333 if let Some(data) = entry.data {
2334 navigated |= item.navigate(data, window, cx);
2335 }
2336 })?;
2337 }
2338 Err(open_by_abs_path_e) => {
2339 log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
2340 }
2341 }
2342 }
2343 }
2344 }
2345
2346 if !navigated {
2347 workspace
2348 .update_in(cx, |workspace, window, cx| {
2349 Self::navigate_history(workspace, pane, mode, window, cx)
2350 })?
2351 .await?;
2352 }
2353
2354 Ok(())
2355 })
2356 } else {
2357 Task::ready(Ok(()))
2358 }
2359 }
2360
2361 pub fn go_back(
2362 &mut self,
2363 pane: WeakEntity<Pane>,
2364 window: &mut Window,
2365 cx: &mut Context<Workspace>,
2366 ) -> Task<Result<()>> {
2367 self.navigate_history(pane, NavigationMode::GoingBack, window, cx)
2368 }
2369
2370 pub fn go_forward(
2371 &mut self,
2372 pane: WeakEntity<Pane>,
2373 window: &mut Window,
2374 cx: &mut Context<Workspace>,
2375 ) -> Task<Result<()>> {
2376 self.navigate_history(pane, NavigationMode::GoingForward, window, cx)
2377 }
2378
2379 pub fn reopen_closed_item(
2380 &mut self,
2381 window: &mut Window,
2382 cx: &mut Context<Workspace>,
2383 ) -> Task<Result<()>> {
2384 self.navigate_history(
2385 self.active_pane().downgrade(),
2386 NavigationMode::ReopeningClosedItem,
2387 window,
2388 cx,
2389 )
2390 }
2391
2392 pub fn client(&self) -> &Arc<Client> {
2393 &self.app_state.client
2394 }
2395
2396 pub fn set_titlebar_item(&mut self, item: AnyView, _: &mut Window, cx: &mut Context<Self>) {
2397 self.titlebar_item = Some(item);
2398 cx.notify();
2399 }
2400
2401 pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
2402 self.on_prompt_for_new_path = Some(prompt)
2403 }
2404
2405 pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) {
2406 self.on_prompt_for_open_path = Some(prompt)
2407 }
2408
2409 pub fn set_terminal_provider(&mut self, provider: impl TerminalProvider + 'static) {
2410 self.terminal_provider = Some(Box::new(provider));
2411 }
2412
2413 pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) {
2414 self.debugger_provider = Some(Arc::new(provider));
2415 }
2416
2417 pub fn debugger_provider(&self) -> Option<Arc<dyn DebuggerProvider>> {
2418 self.debugger_provider.clone()
2419 }
2420
2421 pub fn prompt_for_open_path(
2422 &mut self,
2423 path_prompt_options: PathPromptOptions,
2424 lister: DirectoryLister,
2425 window: &mut Window,
2426 cx: &mut Context<Self>,
2427 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
2428 if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
2429 let prompt = self.on_prompt_for_open_path.take().unwrap();
2430 let rx = prompt(self, lister, window, cx);
2431 self.on_prompt_for_open_path = Some(prompt);
2432 rx
2433 } else {
2434 let (tx, rx) = oneshot::channel();
2435 let abs_path = cx.prompt_for_paths(path_prompt_options);
2436
2437 cx.spawn_in(window, async move |workspace, cx| {
2438 let Ok(result) = abs_path.await else {
2439 return Ok(());
2440 };
2441
2442 match result {
2443 Ok(result) => {
2444 tx.send(result).ok();
2445 }
2446 Err(err) => {
2447 let rx = workspace.update_in(cx, |workspace, window, cx| {
2448 workspace.show_portal_error(err.to_string(), cx);
2449 let prompt = workspace.on_prompt_for_open_path.take().unwrap();
2450 let rx = prompt(workspace, lister, window, cx);
2451 workspace.on_prompt_for_open_path = Some(prompt);
2452 rx
2453 })?;
2454 if let Ok(path) = rx.await {
2455 tx.send(path).ok();
2456 }
2457 }
2458 };
2459 anyhow::Ok(())
2460 })
2461 .detach();
2462
2463 rx
2464 }
2465 }
2466
2467 pub fn prompt_for_new_path(
2468 &mut self,
2469 lister: DirectoryLister,
2470 suggested_name: Option<String>,
2471 window: &mut Window,
2472 cx: &mut Context<Self>,
2473 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
2474 if self.project.read(cx).is_via_collab()
2475 || self.project.read(cx).is_via_remote_server()
2476 || !WorkspaceSettings::get_global(cx).use_system_path_prompts
2477 {
2478 let prompt = self.on_prompt_for_new_path.take().unwrap();
2479 let rx = prompt(self, lister, suggested_name, window, cx);
2480 self.on_prompt_for_new_path = Some(prompt);
2481 return rx;
2482 }
2483
2484 let (tx, rx) = oneshot::channel();
2485 cx.spawn_in(window, async move |workspace, cx| {
2486 let abs_path = workspace.update(cx, |workspace, cx| {
2487 let relative_to = workspace
2488 .most_recent_active_path(cx)
2489 .and_then(|p| p.parent().map(|p| p.to_path_buf()))
2490 .or_else(|| {
2491 let project = workspace.project.read(cx);
2492 project.visible_worktrees(cx).find_map(|worktree| {
2493 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
2494 })
2495 })
2496 .or_else(std::env::home_dir)
2497 .unwrap_or_else(|| PathBuf::from(""));
2498 cx.prompt_for_new_path(&relative_to, suggested_name.as_deref())
2499 })?;
2500 let abs_path = match abs_path.await? {
2501 Ok(path) => path,
2502 Err(err) => {
2503 let rx = workspace.update_in(cx, |workspace, window, cx| {
2504 workspace.show_portal_error(err.to_string(), cx);
2505
2506 let prompt = workspace.on_prompt_for_new_path.take().unwrap();
2507 let rx = prompt(workspace, lister, suggested_name, window, cx);
2508 workspace.on_prompt_for_new_path = Some(prompt);
2509 rx
2510 })?;
2511 if let Ok(path) = rx.await {
2512 tx.send(path).ok();
2513 }
2514 return anyhow::Ok(());
2515 }
2516 };
2517
2518 tx.send(abs_path.map(|path| vec![path])).ok();
2519 anyhow::Ok(())
2520 })
2521 .detach();
2522
2523 rx
2524 }
2525
2526 pub fn titlebar_item(&self) -> Option<AnyView> {
2527 self.titlebar_item.clone()
2528 }
2529
2530 /// Returns the worktree override set by the user (e.g., via the project dropdown).
2531 /// When set, git-related operations should use this worktree instead of deriving
2532 /// the active worktree from the focused file.
2533 pub fn active_worktree_override(&self) -> Option<WorktreeId> {
2534 self.active_worktree_override
2535 }
2536
2537 pub fn set_active_worktree_override(
2538 &mut self,
2539 worktree_id: Option<WorktreeId>,
2540 cx: &mut Context<Self>,
2541 ) {
2542 self.active_worktree_override = worktree_id;
2543 cx.notify();
2544 }
2545
2546 pub fn clear_active_worktree_override(&mut self, cx: &mut Context<Self>) {
2547 self.active_worktree_override = None;
2548 cx.notify();
2549 }
2550
2551 /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
2552 ///
2553 /// If the given workspace has a local project, then it will be passed
2554 /// to the callback. Otherwise, a new empty window will be created.
2555 pub fn with_local_workspace<T, F>(
2556 &mut self,
2557 window: &mut Window,
2558 cx: &mut Context<Self>,
2559 callback: F,
2560 ) -> Task<Result<T>>
2561 where
2562 T: 'static,
2563 F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
2564 {
2565 if self.project.read(cx).is_local() {
2566 Task::ready(Ok(callback(self, window, cx)))
2567 } else {
2568 let env = self.project.read(cx).cli_environment(cx);
2569 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, None, cx);
2570 cx.spawn_in(window, async move |_vh, cx| {
2571 let (multi_workspace_window, _) = task.await?;
2572 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
2573 let workspace = multi_workspace.workspace().clone();
2574 workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
2575 })
2576 })
2577 }
2578 }
2579
2580 /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
2581 ///
2582 /// If the given workspace has a local project, then it will be passed
2583 /// to the callback. Otherwise, a new empty window will be created.
2584 pub fn with_local_or_wsl_workspace<T, F>(
2585 &mut self,
2586 window: &mut Window,
2587 cx: &mut Context<Self>,
2588 callback: F,
2589 ) -> Task<Result<T>>
2590 where
2591 T: 'static,
2592 F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
2593 {
2594 let project = self.project.read(cx);
2595 if project.is_local() || project.is_via_wsl_with_host_interop(cx) {
2596 Task::ready(Ok(callback(self, window, cx)))
2597 } else {
2598 let env = self.project.read(cx).cli_environment(cx);
2599 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, None, cx);
2600 cx.spawn_in(window, async move |_vh, cx| {
2601 let (multi_workspace_window, _) = task.await?;
2602 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
2603 let workspace = multi_workspace.workspace().clone();
2604 workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
2605 })
2606 })
2607 }
2608 }
2609
2610 pub fn worktrees<'a>(&self, cx: &'a App) -> impl 'a + Iterator<Item = Entity<Worktree>> {
2611 self.project.read(cx).worktrees(cx)
2612 }
2613
2614 pub fn visible_worktrees<'a>(
2615 &self,
2616 cx: &'a App,
2617 ) -> impl 'a + Iterator<Item = Entity<Worktree>> {
2618 self.project.read(cx).visible_worktrees(cx)
2619 }
2620
2621 #[cfg(any(test, feature = "test-support"))]
2622 pub fn worktree_scans_complete(&self, cx: &App) -> impl Future<Output = ()> + 'static + use<> {
2623 let futures = self
2624 .worktrees(cx)
2625 .filter_map(|worktree| worktree.read(cx).as_local())
2626 .map(|worktree| worktree.scan_complete())
2627 .collect::<Vec<_>>();
2628 async move {
2629 for future in futures {
2630 future.await;
2631 }
2632 }
2633 }
2634
2635 pub fn close_global(cx: &mut App) {
2636 cx.defer(|cx| {
2637 cx.windows().iter().find(|window| {
2638 window
2639 .update(cx, |_, window, _| {
2640 if window.is_window_active() {
2641 //This can only get called when the window's project connection has been lost
2642 //so we don't need to prompt the user for anything and instead just close the window
2643 window.remove_window();
2644 true
2645 } else {
2646 false
2647 }
2648 })
2649 .unwrap_or(false)
2650 });
2651 });
2652 }
2653
2654 pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
2655 let prepare = self.prepare_to_close(CloseIntent::CloseWindow, window, cx);
2656 cx.spawn_in(window, async move |_, cx| {
2657 if prepare.await? {
2658 cx.update(|window, _cx| window.remove_window())?;
2659 }
2660 anyhow::Ok(())
2661 })
2662 .detach_and_log_err(cx)
2663 }
2664
2665 pub fn move_focused_panel_to_next_position(
2666 &mut self,
2667 _: &MoveFocusedPanelToNextPosition,
2668 window: &mut Window,
2669 cx: &mut Context<Self>,
2670 ) {
2671 let docks = self.all_docks();
2672 let active_dock = docks
2673 .into_iter()
2674 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
2675
2676 if let Some(dock) = active_dock {
2677 dock.update(cx, |dock, cx| {
2678 let active_panel = dock
2679 .active_panel()
2680 .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx));
2681
2682 if let Some(panel) = active_panel {
2683 panel.move_to_next_position(window, cx);
2684 }
2685 })
2686 }
2687 }
2688
2689 pub fn prepare_to_close(
2690 &mut self,
2691 close_intent: CloseIntent,
2692 window: &mut Window,
2693 cx: &mut Context<Self>,
2694 ) -> Task<Result<bool>> {
2695 let active_call = self.active_call().cloned();
2696
2697 cx.spawn_in(window, async move |this, cx| {
2698 this.update(cx, |this, _| {
2699 if close_intent == CloseIntent::CloseWindow {
2700 this.removing = true;
2701 }
2702 })?;
2703
2704 let workspace_count = cx.update(|_window, cx| {
2705 cx.windows()
2706 .iter()
2707 .filter(|window| window.downcast::<MultiWorkspace>().is_some())
2708 .count()
2709 })?;
2710
2711 #[cfg(target_os = "macos")]
2712 let save_last_workspace = false;
2713
2714 // On Linux and Windows, closing the last window should restore the last workspace.
2715 #[cfg(not(target_os = "macos"))]
2716 let save_last_workspace = {
2717 let remaining_workspaces = cx.update(|_window, cx| {
2718 cx.windows()
2719 .iter()
2720 .filter_map(|window| window.downcast::<MultiWorkspace>())
2721 .filter_map(|multi_workspace| {
2722 multi_workspace
2723 .update(cx, |multi_workspace, _, cx| {
2724 multi_workspace.workspace().read(cx).removing
2725 })
2726 .ok()
2727 })
2728 .filter(|removing| !removing)
2729 .count()
2730 })?;
2731
2732 close_intent != CloseIntent::ReplaceWindow && remaining_workspaces == 0
2733 };
2734
2735 if let Some(active_call) = active_call
2736 && workspace_count == 1
2737 && active_call.read_with(cx, |call, _| call.room().is_some())
2738 {
2739 if close_intent == CloseIntent::CloseWindow {
2740 let answer = cx.update(|window, cx| {
2741 window.prompt(
2742 PromptLevel::Warning,
2743 "Do you want to leave the current call?",
2744 None,
2745 &["Close window and hang up", "Cancel"],
2746 cx,
2747 )
2748 })?;
2749
2750 if answer.await.log_err() == Some(1) {
2751 return anyhow::Ok(false);
2752 } else {
2753 active_call
2754 .update(cx, |call, cx| call.hang_up(cx))
2755 .await
2756 .log_err();
2757 }
2758 }
2759 if close_intent == CloseIntent::ReplaceWindow {
2760 _ = active_call.update(cx, |this, cx| {
2761 let multi_workspace = cx
2762 .windows()
2763 .iter()
2764 .filter_map(|window| window.downcast::<MultiWorkspace>())
2765 .next()
2766 .unwrap();
2767 let project = multi_workspace
2768 .read(cx)?
2769 .workspace()
2770 .read(cx)
2771 .project
2772 .clone();
2773 if project.read(cx).is_shared() {
2774 this.unshare_project(project, cx)?;
2775 }
2776 Ok::<_, anyhow::Error>(())
2777 })?;
2778 }
2779 }
2780
2781 let save_result = this
2782 .update_in(cx, |this, window, cx| {
2783 this.save_all_internal(SaveIntent::Close, window, cx)
2784 })?
2785 .await;
2786
2787 // If we're not quitting, but closing, we remove the workspace from
2788 // the current session.
2789 if close_intent != CloseIntent::Quit
2790 && !save_last_workspace
2791 && save_result.as_ref().is_ok_and(|&res| res)
2792 {
2793 this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx))?
2794 .await;
2795 }
2796
2797 save_result
2798 })
2799 }
2800
2801 fn save_all(&mut self, action: &SaveAll, window: &mut Window, cx: &mut Context<Self>) {
2802 self.save_all_internal(
2803 action.save_intent.unwrap_or(SaveIntent::SaveAll),
2804 window,
2805 cx,
2806 )
2807 .detach_and_log_err(cx);
2808 }
2809
2810 fn send_keystrokes(
2811 &mut self,
2812 action: &SendKeystrokes,
2813 window: &mut Window,
2814 cx: &mut Context<Self>,
2815 ) {
2816 let keystrokes: Vec<Keystroke> = action
2817 .0
2818 .split(' ')
2819 .flat_map(|k| Keystroke::parse(k).log_err())
2820 .map(|k| {
2821 cx.keyboard_mapper()
2822 .map_key_equivalent(k, false)
2823 .inner()
2824 .clone()
2825 })
2826 .collect();
2827 let _ = self.send_keystrokes_impl(keystrokes, window, cx);
2828 }
2829
2830 pub fn send_keystrokes_impl(
2831 &mut self,
2832 keystrokes: Vec<Keystroke>,
2833 window: &mut Window,
2834 cx: &mut Context<Self>,
2835 ) -> Shared<Task<()>> {
2836 let mut state = self.dispatching_keystrokes.borrow_mut();
2837 if !state.dispatched.insert(keystrokes.clone()) {
2838 cx.propagate();
2839 return state.task.clone().unwrap();
2840 }
2841
2842 state.queue.extend(keystrokes);
2843
2844 let keystrokes = self.dispatching_keystrokes.clone();
2845 if state.task.is_none() {
2846 state.task = Some(
2847 window
2848 .spawn(cx, async move |cx| {
2849 // limit to 100 keystrokes to avoid infinite recursion.
2850 for _ in 0..100 {
2851 let mut state = keystrokes.borrow_mut();
2852 let Some(keystroke) = state.queue.pop_front() else {
2853 state.dispatched.clear();
2854 state.task.take();
2855 return;
2856 };
2857 drop(state);
2858 cx.update(|window, cx| {
2859 let focused = window.focused(cx);
2860 window.dispatch_keystroke(keystroke.clone(), cx);
2861 if window.focused(cx) != focused {
2862 // dispatch_keystroke may cause the focus to change.
2863 // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
2864 // And we need that to happen before the next keystroke to keep vim mode happy...
2865 // (Note that the tests always do this implicitly, so you must manually test with something like:
2866 // "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
2867 // )
2868 window.draw(cx).clear();
2869 }
2870 })
2871 .ok();
2872 }
2873
2874 *keystrokes.borrow_mut() = Default::default();
2875 log::error!("over 100 keystrokes passed to send_keystrokes");
2876 })
2877 .shared(),
2878 );
2879 }
2880 state.task.clone().unwrap()
2881 }
2882
2883 fn save_all_internal(
2884 &mut self,
2885 mut save_intent: SaveIntent,
2886 window: &mut Window,
2887 cx: &mut Context<Self>,
2888 ) -> Task<Result<bool>> {
2889 if self.project.read(cx).is_disconnected(cx) {
2890 return Task::ready(Ok(true));
2891 }
2892 let dirty_items = self
2893 .panes
2894 .iter()
2895 .flat_map(|pane| {
2896 pane.read(cx).items().filter_map(|item| {
2897 if item.is_dirty(cx) {
2898 item.tab_content_text(0, cx);
2899 Some((pane.downgrade(), item.boxed_clone()))
2900 } else {
2901 None
2902 }
2903 })
2904 })
2905 .collect::<Vec<_>>();
2906
2907 let project = self.project.clone();
2908 cx.spawn_in(window, async move |workspace, cx| {
2909 let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() {
2910 let (serialize_tasks, remaining_dirty_items) =
2911 workspace.update_in(cx, |workspace, window, cx| {
2912 let mut remaining_dirty_items = Vec::new();
2913 let mut serialize_tasks = Vec::new();
2914 for (pane, item) in dirty_items {
2915 if let Some(task) = item
2916 .to_serializable_item_handle(cx)
2917 .and_then(|handle| handle.serialize(workspace, true, window, cx))
2918 {
2919 serialize_tasks.push(task);
2920 } else {
2921 remaining_dirty_items.push((pane, item));
2922 }
2923 }
2924 (serialize_tasks, remaining_dirty_items)
2925 })?;
2926
2927 futures::future::try_join_all(serialize_tasks).await?;
2928
2929 if remaining_dirty_items.len() > 1 {
2930 let answer = workspace.update_in(cx, |_, window, cx| {
2931 let detail = Pane::file_names_for_prompt(
2932 &mut remaining_dirty_items.iter().map(|(_, handle)| handle),
2933 cx,
2934 );
2935 window.prompt(
2936 PromptLevel::Warning,
2937 "Do you want to save all changes in the following files?",
2938 Some(&detail),
2939 &["Save all", "Discard all", "Cancel"],
2940 cx,
2941 )
2942 })?;
2943 match answer.await.log_err() {
2944 Some(0) => save_intent = SaveIntent::SaveAll,
2945 Some(1) => save_intent = SaveIntent::Skip,
2946 Some(2) => return Ok(false),
2947 _ => {}
2948 }
2949 }
2950
2951 remaining_dirty_items
2952 } else {
2953 dirty_items
2954 };
2955
2956 for (pane, item) in dirty_items {
2957 let (singleton, project_entry_ids) = cx.update(|_, cx| {
2958 (
2959 item.buffer_kind(cx) == ItemBufferKind::Singleton,
2960 item.project_entry_ids(cx),
2961 )
2962 })?;
2963 if (singleton || !project_entry_ids.is_empty())
2964 && !Pane::save_item(project.clone(), &pane, &*item, save_intent, cx).await?
2965 {
2966 return Ok(false);
2967 }
2968 }
2969 Ok(true)
2970 })
2971 }
2972
2973 pub fn open_workspace_for_paths(
2974 &mut self,
2975 replace_current_window: bool,
2976 paths: Vec<PathBuf>,
2977 window: &mut Window,
2978 cx: &mut Context<Self>,
2979 ) -> Task<Result<()>> {
2980 let window_handle = window.window_handle().downcast::<MultiWorkspace>();
2981 let is_remote = self.project.read(cx).is_via_collab();
2982 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
2983 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
2984
2985 let window_to_replace = if replace_current_window {
2986 window_handle
2987 } else if is_remote || has_worktree || has_dirty_items {
2988 None
2989 } else {
2990 window_handle
2991 };
2992 let app_state = self.app_state.clone();
2993
2994 cx.spawn(async move |_, cx| {
2995 cx.update(|cx| {
2996 open_paths(
2997 &paths,
2998 app_state,
2999 OpenOptions {
3000 replace_window: window_to_replace,
3001 ..Default::default()
3002 },
3003 cx,
3004 )
3005 })
3006 .await?;
3007 Ok(())
3008 })
3009 }
3010
3011 #[allow(clippy::type_complexity)]
3012 pub fn open_paths(
3013 &mut self,
3014 mut abs_paths: Vec<PathBuf>,
3015 options: OpenOptions,
3016 pane: Option<WeakEntity<Pane>>,
3017 window: &mut Window,
3018 cx: &mut Context<Self>,
3019 ) -> Task<Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>> {
3020 let fs = self.app_state.fs.clone();
3021
3022 let caller_ordered_abs_paths = abs_paths.clone();
3023
3024 // Sort the paths to ensure we add worktrees for parents before their children.
3025 abs_paths.sort_unstable();
3026 cx.spawn_in(window, async move |this, cx| {
3027 let mut tasks = Vec::with_capacity(abs_paths.len());
3028
3029 for abs_path in &abs_paths {
3030 let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
3031 OpenVisible::All => Some(true),
3032 OpenVisible::None => Some(false),
3033 OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
3034 Some(Some(metadata)) => Some(!metadata.is_dir),
3035 Some(None) => Some(true),
3036 None => None,
3037 },
3038 OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
3039 Some(Some(metadata)) => Some(metadata.is_dir),
3040 Some(None) => Some(false),
3041 None => None,
3042 },
3043 };
3044 let project_path = match visible {
3045 Some(visible) => match this
3046 .update(cx, |this, cx| {
3047 Workspace::project_path_for_path(
3048 this.project.clone(),
3049 abs_path,
3050 visible,
3051 cx,
3052 )
3053 })
3054 .log_err()
3055 {
3056 Some(project_path) => project_path.await.log_err(),
3057 None => None,
3058 },
3059 None => None,
3060 };
3061
3062 let this = this.clone();
3063 let abs_path: Arc<Path> = SanitizedPath::new(&abs_path).as_path().into();
3064 let fs = fs.clone();
3065 let pane = pane.clone();
3066 let task = cx.spawn(async move |cx| {
3067 let (_worktree, project_path) = project_path?;
3068 if fs.is_dir(&abs_path).await {
3069 // Opening a directory should not race to update the active entry.
3070 // We'll select/reveal a deterministic final entry after all paths finish opening.
3071 None
3072 } else {
3073 Some(
3074 this.update_in(cx, |this, window, cx| {
3075 this.open_path(
3076 project_path,
3077 pane,
3078 options.focus.unwrap_or(true),
3079 window,
3080 cx,
3081 )
3082 })
3083 .ok()?
3084 .await,
3085 )
3086 }
3087 });
3088 tasks.push(task);
3089 }
3090
3091 let results = futures::future::join_all(tasks).await;
3092
3093 // Determine the winner using the fake/abstract FS metadata, not `Path::is_dir`.
3094 let mut winner: Option<(PathBuf, bool)> = None;
3095 for abs_path in caller_ordered_abs_paths.into_iter().rev() {
3096 if let Some(Some(metadata)) = fs.metadata(&abs_path).await.log_err() {
3097 if !metadata.is_dir {
3098 winner = Some((abs_path, false));
3099 break;
3100 }
3101 if winner.is_none() {
3102 winner = Some((abs_path, true));
3103 }
3104 } else if winner.is_none() {
3105 winner = Some((abs_path, false));
3106 }
3107 }
3108
3109 // Compute the winner entry id on the foreground thread and emit once, after all
3110 // paths finish opening. This avoids races between concurrently-opening paths
3111 // (directories in particular) and makes the resulting project panel selection
3112 // deterministic.
3113 if let Some((winner_abs_path, winner_is_dir)) = winner {
3114 'emit_winner: {
3115 let winner_abs_path: Arc<Path> =
3116 SanitizedPath::new(&winner_abs_path).as_path().into();
3117
3118 let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
3119 OpenVisible::All => true,
3120 OpenVisible::None => false,
3121 OpenVisible::OnlyFiles => !winner_is_dir,
3122 OpenVisible::OnlyDirectories => winner_is_dir,
3123 };
3124
3125 let Some(worktree_task) = this
3126 .update(cx, |workspace, cx| {
3127 workspace.project.update(cx, |project, cx| {
3128 project.find_or_create_worktree(
3129 winner_abs_path.as_ref(),
3130 visible,
3131 cx,
3132 )
3133 })
3134 })
3135 .ok()
3136 else {
3137 break 'emit_winner;
3138 };
3139
3140 let Ok((worktree, _)) = worktree_task.await else {
3141 break 'emit_winner;
3142 };
3143
3144 let Ok(Some(entry_id)) = this.update(cx, |_, cx| {
3145 let worktree = worktree.read(cx);
3146 let worktree_abs_path = worktree.abs_path();
3147 let entry = if winner_abs_path.as_ref() == worktree_abs_path.as_ref() {
3148 worktree.root_entry()
3149 } else {
3150 winner_abs_path
3151 .strip_prefix(worktree_abs_path.as_ref())
3152 .ok()
3153 .and_then(|relative_path| {
3154 let relative_path =
3155 RelPath::new(relative_path, PathStyle::local())
3156 .log_err()?;
3157 worktree.entry_for_path(&relative_path)
3158 })
3159 }?;
3160 Some(entry.id)
3161 }) else {
3162 break 'emit_winner;
3163 };
3164
3165 this.update(cx, |workspace, cx| {
3166 workspace.project.update(cx, |_, cx| {
3167 cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
3168 });
3169 })
3170 .ok();
3171 }
3172 }
3173
3174 results
3175 })
3176 }
3177
3178 pub fn open_resolved_path(
3179 &mut self,
3180 path: ResolvedPath,
3181 window: &mut Window,
3182 cx: &mut Context<Self>,
3183 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3184 match path {
3185 ResolvedPath::ProjectPath { project_path, .. } => {
3186 self.open_path(project_path, None, true, window, cx)
3187 }
3188 ResolvedPath::AbsPath { path, .. } => self.open_abs_path(
3189 PathBuf::from(path),
3190 OpenOptions {
3191 visible: Some(OpenVisible::None),
3192 ..Default::default()
3193 },
3194 window,
3195 cx,
3196 ),
3197 }
3198 }
3199
3200 pub fn absolute_path_of_worktree(
3201 &self,
3202 worktree_id: WorktreeId,
3203 cx: &mut Context<Self>,
3204 ) -> Option<PathBuf> {
3205 self.project
3206 .read(cx)
3207 .worktree_for_id(worktree_id, cx)
3208 // TODO: use `abs_path` or `root_dir`
3209 .map(|wt| wt.read(cx).abs_path().as_ref().to_path_buf())
3210 }
3211
3212 fn add_folder_to_project(
3213 &mut self,
3214 _: &AddFolderToProject,
3215 window: &mut Window,
3216 cx: &mut Context<Self>,
3217 ) {
3218 let project = self.project.read(cx);
3219 if project.is_via_collab() {
3220 self.show_error(
3221 &anyhow!("You cannot add folders to someone else's project"),
3222 cx,
3223 );
3224 return;
3225 }
3226 let paths = self.prompt_for_open_path(
3227 PathPromptOptions {
3228 files: false,
3229 directories: true,
3230 multiple: true,
3231 prompt: None,
3232 },
3233 DirectoryLister::Project(self.project.clone()),
3234 window,
3235 cx,
3236 );
3237 cx.spawn_in(window, async move |this, cx| {
3238 if let Some(paths) = paths.await.log_err().flatten() {
3239 let results = this
3240 .update_in(cx, |this, window, cx| {
3241 this.open_paths(
3242 paths,
3243 OpenOptions {
3244 visible: Some(OpenVisible::All),
3245 ..Default::default()
3246 },
3247 None,
3248 window,
3249 cx,
3250 )
3251 })?
3252 .await;
3253 for result in results.into_iter().flatten() {
3254 result.log_err();
3255 }
3256 }
3257 anyhow::Ok(())
3258 })
3259 .detach_and_log_err(cx);
3260 }
3261
3262 pub fn project_path_for_path(
3263 project: Entity<Project>,
3264 abs_path: &Path,
3265 visible: bool,
3266 cx: &mut App,
3267 ) -> Task<Result<(Entity<Worktree>, ProjectPath)>> {
3268 let entry = project.update(cx, |project, cx| {
3269 project.find_or_create_worktree(abs_path, visible, cx)
3270 });
3271 cx.spawn(async move |cx| {
3272 let (worktree, path) = entry.await?;
3273 let worktree_id = worktree.read_with(cx, |t, _| t.id());
3274 Ok((worktree, ProjectPath { worktree_id, path }))
3275 })
3276 }
3277
3278 pub fn items<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator<Item = &'a Box<dyn ItemHandle>> {
3279 self.panes.iter().flat_map(|pane| pane.read(cx).items())
3280 }
3281
3282 pub fn item_of_type<T: Item>(&self, cx: &App) -> Option<Entity<T>> {
3283 self.items_of_type(cx).max_by_key(|item| item.item_id())
3284 }
3285
3286 pub fn items_of_type<'a, T: Item>(
3287 &'a self,
3288 cx: &'a App,
3289 ) -> impl 'a + Iterator<Item = Entity<T>> {
3290 self.panes
3291 .iter()
3292 .flat_map(|pane| pane.read(cx).items_of_type())
3293 }
3294
3295 pub fn active_item(&self, cx: &App) -> Option<Box<dyn ItemHandle>> {
3296 self.active_pane().read(cx).active_item()
3297 }
3298
3299 pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
3300 let item = self.active_item(cx)?;
3301 item.to_any_view().downcast::<I>().ok()
3302 }
3303
3304 fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
3305 self.active_item(cx).and_then(|item| item.project_path(cx))
3306 }
3307
3308 pub fn most_recent_active_path(&self, cx: &App) -> Option<PathBuf> {
3309 self.recent_navigation_history_iter(cx)
3310 .filter_map(|(path, abs_path)| {
3311 let worktree = self
3312 .project
3313 .read(cx)
3314 .worktree_for_id(path.worktree_id, cx)?;
3315 if worktree.read(cx).is_visible() {
3316 abs_path
3317 } else {
3318 None
3319 }
3320 })
3321 .next()
3322 }
3323
3324 pub fn save_active_item(
3325 &mut self,
3326 save_intent: SaveIntent,
3327 window: &mut Window,
3328 cx: &mut App,
3329 ) -> Task<Result<()>> {
3330 let project = self.project.clone();
3331 let pane = self.active_pane();
3332 let item = pane.read(cx).active_item();
3333 let pane = pane.downgrade();
3334
3335 window.spawn(cx, async move |cx| {
3336 if let Some(item) = item {
3337 Pane::save_item(project, &pane, item.as_ref(), save_intent, cx)
3338 .await
3339 .map(|_| ())
3340 } else {
3341 Ok(())
3342 }
3343 })
3344 }
3345
3346 pub fn close_inactive_items_and_panes(
3347 &mut self,
3348 action: &CloseInactiveTabsAndPanes,
3349 window: &mut Window,
3350 cx: &mut Context<Self>,
3351 ) {
3352 if let Some(task) = self.close_all_internal(
3353 true,
3354 action.save_intent.unwrap_or(SaveIntent::Close),
3355 window,
3356 cx,
3357 ) {
3358 task.detach_and_log_err(cx)
3359 }
3360 }
3361
3362 pub fn close_all_items_and_panes(
3363 &mut self,
3364 action: &CloseAllItemsAndPanes,
3365 window: &mut Window,
3366 cx: &mut Context<Self>,
3367 ) {
3368 if let Some(task) = self.close_all_internal(
3369 false,
3370 action.save_intent.unwrap_or(SaveIntent::Close),
3371 window,
3372 cx,
3373 ) {
3374 task.detach_and_log_err(cx)
3375 }
3376 }
3377
3378 /// Closes the active item across all panes.
3379 pub fn close_item_in_all_panes(
3380 &mut self,
3381 action: &CloseItemInAllPanes,
3382 window: &mut Window,
3383 cx: &mut Context<Self>,
3384 ) {
3385 let Some(active_item) = self.active_pane().read(cx).active_item() else {
3386 return;
3387 };
3388
3389 let save_intent = action.save_intent.unwrap_or(SaveIntent::Close);
3390 let close_pinned = action.close_pinned;
3391
3392 if let Some(project_path) = active_item.project_path(cx) {
3393 self.close_items_with_project_path(
3394 &project_path,
3395 save_intent,
3396 close_pinned,
3397 window,
3398 cx,
3399 );
3400 } else if close_pinned || !self.active_pane().read(cx).is_active_item_pinned() {
3401 let item_id = active_item.item_id();
3402 self.active_pane().update(cx, |pane, cx| {
3403 pane.close_item_by_id(item_id, save_intent, window, cx)
3404 .detach_and_log_err(cx);
3405 });
3406 }
3407 }
3408
3409 /// Closes all items with the given project path across all panes.
3410 pub fn close_items_with_project_path(
3411 &mut self,
3412 project_path: &ProjectPath,
3413 save_intent: SaveIntent,
3414 close_pinned: bool,
3415 window: &mut Window,
3416 cx: &mut Context<Self>,
3417 ) {
3418 let panes = self.panes().to_vec();
3419 for pane in panes {
3420 pane.update(cx, |pane, cx| {
3421 pane.close_items_for_project_path(
3422 project_path,
3423 save_intent,
3424 close_pinned,
3425 window,
3426 cx,
3427 )
3428 .detach_and_log_err(cx);
3429 });
3430 }
3431 }
3432
3433 fn close_all_internal(
3434 &mut self,
3435 retain_active_pane: bool,
3436 save_intent: SaveIntent,
3437 window: &mut Window,
3438 cx: &mut Context<Self>,
3439 ) -> Option<Task<Result<()>>> {
3440 let current_pane = self.active_pane();
3441
3442 let mut tasks = Vec::new();
3443
3444 if retain_active_pane {
3445 let current_pane_close = current_pane.update(cx, |pane, cx| {
3446 pane.close_other_items(
3447 &CloseOtherItems {
3448 save_intent: None,
3449 close_pinned: false,
3450 },
3451 None,
3452 window,
3453 cx,
3454 )
3455 });
3456
3457 tasks.push(current_pane_close);
3458 }
3459
3460 for pane in self.panes() {
3461 if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
3462 continue;
3463 }
3464
3465 let close_pane_items = pane.update(cx, |pane: &mut Pane, cx| {
3466 pane.close_all_items(
3467 &CloseAllItems {
3468 save_intent: Some(save_intent),
3469 close_pinned: false,
3470 },
3471 window,
3472 cx,
3473 )
3474 });
3475
3476 tasks.push(close_pane_items)
3477 }
3478
3479 if tasks.is_empty() {
3480 None
3481 } else {
3482 Some(cx.spawn_in(window, async move |_, _| {
3483 for task in tasks {
3484 task.await?
3485 }
3486 Ok(())
3487 }))
3488 }
3489 }
3490
3491 pub fn is_dock_at_position_open(&self, position: DockPosition, cx: &mut Context<Self>) -> bool {
3492 self.dock_at_position(position).read(cx).is_open()
3493 }
3494
3495 pub fn toggle_dock(
3496 &mut self,
3497 dock_side: DockPosition,
3498 window: &mut Window,
3499 cx: &mut Context<Self>,
3500 ) {
3501 let mut focus_center = false;
3502 let mut reveal_dock = false;
3503
3504 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
3505 let was_visible = self.is_dock_at_position_open(dock_side, cx) && !other_is_zoomed;
3506
3507 if let Some(panel) = self.dock_at_position(dock_side).read(cx).active_panel() {
3508 telemetry::event!(
3509 "Panel Button Clicked",
3510 name = panel.persistent_name(),
3511 toggle_state = !was_visible
3512 );
3513 }
3514 if was_visible {
3515 self.save_open_dock_positions(cx);
3516 }
3517
3518 let dock = self.dock_at_position(dock_side);
3519 dock.update(cx, |dock, cx| {
3520 dock.set_open(!was_visible, window, cx);
3521
3522 if dock.active_panel().is_none() {
3523 let Some(panel_ix) = dock
3524 .first_enabled_panel_idx(cx)
3525 .log_with_level(log::Level::Info)
3526 else {
3527 return;
3528 };
3529 dock.activate_panel(panel_ix, window, cx);
3530 }
3531
3532 if let Some(active_panel) = dock.active_panel() {
3533 if was_visible {
3534 if active_panel
3535 .panel_focus_handle(cx)
3536 .contains_focused(window, cx)
3537 {
3538 focus_center = true;
3539 }
3540 } else {
3541 let focus_handle = &active_panel.panel_focus_handle(cx);
3542 window.focus(focus_handle, cx);
3543 reveal_dock = true;
3544 }
3545 }
3546 });
3547
3548 if reveal_dock {
3549 self.dismiss_zoomed_items_to_reveal(Some(dock_side), window, cx);
3550 }
3551
3552 if focus_center {
3553 self.active_pane
3554 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
3555 }
3556
3557 cx.notify();
3558 self.serialize_workspace(window, cx);
3559 }
3560
3561 fn active_dock(&self, window: &Window, cx: &Context<Self>) -> Option<&Entity<Dock>> {
3562 self.all_docks().into_iter().find(|&dock| {
3563 dock.read(cx).is_open() && dock.focus_handle(cx).contains_focused(window, cx)
3564 })
3565 }
3566
3567 fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
3568 if let Some(dock) = self.active_dock(window, cx).cloned() {
3569 self.save_open_dock_positions(cx);
3570 dock.update(cx, |dock, cx| {
3571 dock.set_open(false, window, cx);
3572 });
3573 return true;
3574 }
3575 false
3576 }
3577
3578 pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3579 self.save_open_dock_positions(cx);
3580 for dock in self.all_docks() {
3581 dock.update(cx, |dock, cx| {
3582 dock.set_open(false, window, cx);
3583 });
3584 }
3585
3586 cx.focus_self(window);
3587 cx.notify();
3588 self.serialize_workspace(window, cx);
3589 }
3590
3591 fn get_open_dock_positions(&self, cx: &Context<Self>) -> Vec<DockPosition> {
3592 self.all_docks()
3593 .into_iter()
3594 .filter_map(|dock| {
3595 let dock_ref = dock.read(cx);
3596 if dock_ref.is_open() {
3597 Some(dock_ref.position())
3598 } else {
3599 None
3600 }
3601 })
3602 .collect()
3603 }
3604
3605 /// Saves the positions of currently open docks.
3606 ///
3607 /// Updates `last_open_dock_positions` with positions of all currently open
3608 /// docks, to later be restored by the 'Toggle All Docks' action.
3609 fn save_open_dock_positions(&mut self, cx: &mut Context<Self>) {
3610 let open_dock_positions = self.get_open_dock_positions(cx);
3611 if !open_dock_positions.is_empty() {
3612 self.last_open_dock_positions = open_dock_positions;
3613 }
3614 }
3615
3616 /// Toggles all docks between open and closed states.
3617 ///
3618 /// If any docks are open, closes all and remembers their positions. If all
3619 /// docks are closed, restores the last remembered dock configuration.
3620 fn toggle_all_docks(
3621 &mut self,
3622 _: &ToggleAllDocks,
3623 window: &mut Window,
3624 cx: &mut Context<Self>,
3625 ) {
3626 let open_dock_positions = self.get_open_dock_positions(cx);
3627
3628 if !open_dock_positions.is_empty() {
3629 self.close_all_docks(window, cx);
3630 } else if !self.last_open_dock_positions.is_empty() {
3631 self.restore_last_open_docks(window, cx);
3632 }
3633 }
3634
3635 /// Reopens docks from the most recently remembered configuration.
3636 ///
3637 /// Opens all docks whose positions are stored in `last_open_dock_positions`
3638 /// and clears the stored positions.
3639 fn restore_last_open_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3640 let positions_to_open = std::mem::take(&mut self.last_open_dock_positions);
3641
3642 for position in positions_to_open {
3643 let dock = self.dock_at_position(position);
3644 dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
3645 }
3646
3647 cx.focus_self(window);
3648 cx.notify();
3649 self.serialize_workspace(window, cx);
3650 }
3651
3652 /// Transfer focus to the panel of the given type.
3653 pub fn focus_panel<T: Panel>(
3654 &mut self,
3655 window: &mut Window,
3656 cx: &mut Context<Self>,
3657 ) -> Option<Entity<T>> {
3658 let panel = self.focus_or_unfocus_panel::<T>(window, cx, &mut |_, _, _| true)?;
3659 panel.to_any().downcast().ok()
3660 }
3661
3662 /// Focus the panel of the given type if it isn't already focused. If it is
3663 /// already focused, then transfer focus back to the workspace center.
3664 /// When the `close_panel_on_toggle` setting is enabled, also closes the
3665 /// panel when transferring focus back to the center.
3666 pub fn toggle_panel_focus<T: Panel>(
3667 &mut self,
3668 window: &mut Window,
3669 cx: &mut Context<Self>,
3670 ) -> bool {
3671 let mut did_focus_panel = false;
3672 self.focus_or_unfocus_panel::<T>(window, cx, &mut |panel, window, cx| {
3673 did_focus_panel = !panel.panel_focus_handle(cx).contains_focused(window, cx);
3674 did_focus_panel
3675 });
3676
3677 if !did_focus_panel && WorkspaceSettings::get_global(cx).close_panel_on_toggle {
3678 self.close_panel::<T>(window, cx);
3679 }
3680
3681 telemetry::event!(
3682 "Panel Button Clicked",
3683 name = T::persistent_name(),
3684 toggle_state = did_focus_panel
3685 );
3686
3687 did_focus_panel
3688 }
3689
3690 pub fn activate_panel_for_proto_id(
3691 &mut self,
3692 panel_id: PanelId,
3693 window: &mut Window,
3694 cx: &mut Context<Self>,
3695 ) -> Option<Arc<dyn PanelHandle>> {
3696 let mut panel = None;
3697 for dock in self.all_docks() {
3698 if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
3699 panel = dock.update(cx, |dock, cx| {
3700 dock.activate_panel(panel_index, window, cx);
3701 dock.set_open(true, window, cx);
3702 dock.active_panel().cloned()
3703 });
3704 break;
3705 }
3706 }
3707
3708 if panel.is_some() {
3709 cx.notify();
3710 self.serialize_workspace(window, cx);
3711 }
3712
3713 panel
3714 }
3715
3716 /// Focus or unfocus the given panel type, depending on the given callback.
3717 fn focus_or_unfocus_panel<T: Panel>(
3718 &mut self,
3719 window: &mut Window,
3720 cx: &mut Context<Self>,
3721 should_focus: &mut dyn FnMut(&dyn PanelHandle, &mut Window, &mut Context<Dock>) -> bool,
3722 ) -> Option<Arc<dyn PanelHandle>> {
3723 let mut result_panel = None;
3724 let mut serialize = false;
3725 for dock in self.all_docks() {
3726 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
3727 let mut focus_center = false;
3728 let panel = dock.update(cx, |dock, cx| {
3729 dock.activate_panel(panel_index, window, cx);
3730
3731 let panel = dock.active_panel().cloned();
3732 if let Some(panel) = panel.as_ref() {
3733 if should_focus(&**panel, window, cx) {
3734 dock.set_open(true, window, cx);
3735 panel.panel_focus_handle(cx).focus(window, cx);
3736 } else {
3737 focus_center = true;
3738 }
3739 }
3740 panel
3741 });
3742
3743 if focus_center {
3744 self.active_pane
3745 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
3746 }
3747
3748 result_panel = panel;
3749 serialize = true;
3750 break;
3751 }
3752 }
3753
3754 if serialize {
3755 self.serialize_workspace(window, cx);
3756 }
3757
3758 cx.notify();
3759 result_panel
3760 }
3761
3762 /// Open the panel of the given type
3763 pub fn open_panel<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3764 for dock in self.all_docks() {
3765 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
3766 dock.update(cx, |dock, cx| {
3767 dock.activate_panel(panel_index, window, cx);
3768 dock.set_open(true, window, cx);
3769 });
3770 }
3771 }
3772 }
3773
3774 pub fn close_panel<T: Panel>(&self, window: &mut Window, cx: &mut Context<Self>) {
3775 for dock in self.all_docks().iter() {
3776 dock.update(cx, |dock, cx| {
3777 if dock.panel::<T>().is_some() {
3778 dock.set_open(false, window, cx)
3779 }
3780 })
3781 }
3782 }
3783
3784 pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
3785 self.all_docks()
3786 .iter()
3787 .find_map(|dock| dock.read(cx).panel::<T>())
3788 }
3789
3790 fn dismiss_zoomed_items_to_reveal(
3791 &mut self,
3792 dock_to_reveal: Option<DockPosition>,
3793 window: &mut Window,
3794 cx: &mut Context<Self>,
3795 ) {
3796 // If a center pane is zoomed, unzoom it.
3797 for pane in &self.panes {
3798 if pane != &self.active_pane || dock_to_reveal.is_some() {
3799 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3800 }
3801 }
3802
3803 // If another dock is zoomed, hide it.
3804 let mut focus_center = false;
3805 for dock in self.all_docks() {
3806 dock.update(cx, |dock, cx| {
3807 if Some(dock.position()) != dock_to_reveal
3808 && let Some(panel) = dock.active_panel()
3809 && panel.is_zoomed(window, cx)
3810 {
3811 focus_center |= panel.panel_focus_handle(cx).contains_focused(window, cx);
3812 dock.set_open(false, window, cx);
3813 }
3814 });
3815 }
3816
3817 if focus_center {
3818 self.active_pane
3819 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
3820 }
3821
3822 if self.zoomed_position != dock_to_reveal {
3823 self.zoomed = None;
3824 self.zoomed_position = None;
3825 cx.emit(Event::ZoomChanged);
3826 }
3827
3828 cx.notify();
3829 }
3830
3831 fn add_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
3832 let pane = cx.new(|cx| {
3833 let mut pane = Pane::new(
3834 self.weak_handle(),
3835 self.project.clone(),
3836 self.pane_history_timestamp.clone(),
3837 None,
3838 NewFile.boxed_clone(),
3839 true,
3840 window,
3841 cx,
3842 );
3843 pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
3844 pane
3845 });
3846 cx.subscribe_in(&pane, window, Self::handle_pane_event)
3847 .detach();
3848 self.panes.push(pane.clone());
3849
3850 window.focus(&pane.focus_handle(cx), cx);
3851
3852 cx.emit(Event::PaneAdded(pane.clone()));
3853 pane
3854 }
3855
3856 pub fn add_item_to_center(
3857 &mut self,
3858 item: Box<dyn ItemHandle>,
3859 window: &mut Window,
3860 cx: &mut Context<Self>,
3861 ) -> bool {
3862 if let Some(center_pane) = self.last_active_center_pane.clone() {
3863 if let Some(center_pane) = center_pane.upgrade() {
3864 center_pane.update(cx, |pane, cx| {
3865 pane.add_item(item, true, true, None, window, cx)
3866 });
3867 true
3868 } else {
3869 false
3870 }
3871 } else {
3872 false
3873 }
3874 }
3875
3876 pub fn add_item_to_active_pane(
3877 &mut self,
3878 item: Box<dyn ItemHandle>,
3879 destination_index: Option<usize>,
3880 focus_item: bool,
3881 window: &mut Window,
3882 cx: &mut App,
3883 ) {
3884 self.add_item(
3885 self.active_pane.clone(),
3886 item,
3887 destination_index,
3888 false,
3889 focus_item,
3890 window,
3891 cx,
3892 )
3893 }
3894
3895 pub fn add_item(
3896 &mut self,
3897 pane: Entity<Pane>,
3898 item: Box<dyn ItemHandle>,
3899 destination_index: Option<usize>,
3900 activate_pane: bool,
3901 focus_item: bool,
3902 window: &mut Window,
3903 cx: &mut App,
3904 ) {
3905 pane.update(cx, |pane, cx| {
3906 pane.add_item(
3907 item,
3908 activate_pane,
3909 focus_item,
3910 destination_index,
3911 window,
3912 cx,
3913 )
3914 });
3915 }
3916
3917 pub fn split_item(
3918 &mut self,
3919 split_direction: SplitDirection,
3920 item: Box<dyn ItemHandle>,
3921 window: &mut Window,
3922 cx: &mut Context<Self>,
3923 ) {
3924 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, window, cx);
3925 self.add_item(new_pane, item, None, true, true, window, cx);
3926 }
3927
3928 pub fn open_abs_path(
3929 &mut self,
3930 abs_path: PathBuf,
3931 options: OpenOptions,
3932 window: &mut Window,
3933 cx: &mut Context<Self>,
3934 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3935 cx.spawn_in(window, async move |workspace, cx| {
3936 let open_paths_task_result = workspace
3937 .update_in(cx, |workspace, window, cx| {
3938 workspace.open_paths(vec![abs_path.clone()], options, None, window, cx)
3939 })
3940 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
3941 .await;
3942 anyhow::ensure!(
3943 open_paths_task_result.len() == 1,
3944 "open abs path {abs_path:?} task returned incorrect number of results"
3945 );
3946 match open_paths_task_result
3947 .into_iter()
3948 .next()
3949 .expect("ensured single task result")
3950 {
3951 Some(open_result) => {
3952 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
3953 }
3954 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
3955 }
3956 })
3957 }
3958
3959 pub fn split_abs_path(
3960 &mut self,
3961 abs_path: PathBuf,
3962 visible: bool,
3963 window: &mut Window,
3964 cx: &mut Context<Self>,
3965 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3966 let project_path_task =
3967 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
3968 cx.spawn_in(window, async move |this, cx| {
3969 let (_, path) = project_path_task.await?;
3970 this.update_in(cx, |this, window, cx| this.split_path(path, window, cx))?
3971 .await
3972 })
3973 }
3974
3975 pub fn open_path(
3976 &mut self,
3977 path: impl Into<ProjectPath>,
3978 pane: Option<WeakEntity<Pane>>,
3979 focus_item: bool,
3980 window: &mut Window,
3981 cx: &mut App,
3982 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3983 self.open_path_preview(path, pane, focus_item, false, true, window, cx)
3984 }
3985
3986 pub fn open_path_preview(
3987 &mut self,
3988 path: impl Into<ProjectPath>,
3989 pane: Option<WeakEntity<Pane>>,
3990 focus_item: bool,
3991 allow_preview: bool,
3992 activate: bool,
3993 window: &mut Window,
3994 cx: &mut App,
3995 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3996 let pane = pane.unwrap_or_else(|| {
3997 self.last_active_center_pane.clone().unwrap_or_else(|| {
3998 self.panes
3999 .first()
4000 .expect("There must be an active pane")
4001 .downgrade()
4002 })
4003 });
4004
4005 let project_path = path.into();
4006 let task = self.load_path(project_path.clone(), window, cx);
4007 window.spawn(cx, async move |cx| {
4008 let (project_entry_id, build_item) = task.await?;
4009
4010 pane.update_in(cx, |pane, window, cx| {
4011 pane.open_item(
4012 project_entry_id,
4013 project_path,
4014 focus_item,
4015 allow_preview,
4016 activate,
4017 None,
4018 window,
4019 cx,
4020 build_item,
4021 )
4022 })
4023 })
4024 }
4025
4026 pub fn split_path(
4027 &mut self,
4028 path: impl Into<ProjectPath>,
4029 window: &mut Window,
4030 cx: &mut Context<Self>,
4031 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4032 self.split_path_preview(path, false, None, window, cx)
4033 }
4034
4035 pub fn split_path_preview(
4036 &mut self,
4037 path: impl Into<ProjectPath>,
4038 allow_preview: bool,
4039 split_direction: Option<SplitDirection>,
4040 window: &mut Window,
4041 cx: &mut Context<Self>,
4042 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4043 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
4044 self.panes
4045 .first()
4046 .expect("There must be an active pane")
4047 .downgrade()
4048 });
4049
4050 if let Member::Pane(center_pane) = &self.center.root
4051 && center_pane.read(cx).items_len() == 0
4052 {
4053 return self.open_path(path, Some(pane), true, window, cx);
4054 }
4055
4056 let project_path = path.into();
4057 let task = self.load_path(project_path.clone(), window, cx);
4058 cx.spawn_in(window, async move |this, cx| {
4059 let (project_entry_id, build_item) = task.await?;
4060 this.update_in(cx, move |this, window, cx| -> Option<_> {
4061 let pane = pane.upgrade()?;
4062 let new_pane = this.split_pane(
4063 pane,
4064 split_direction.unwrap_or(SplitDirection::Right),
4065 window,
4066 cx,
4067 );
4068 new_pane.update(cx, |new_pane, cx| {
4069 Some(new_pane.open_item(
4070 project_entry_id,
4071 project_path,
4072 true,
4073 allow_preview,
4074 true,
4075 None,
4076 window,
4077 cx,
4078 build_item,
4079 ))
4080 })
4081 })
4082 .map(|option| option.context("pane was dropped"))?
4083 })
4084 }
4085
4086 fn load_path(
4087 &mut self,
4088 path: ProjectPath,
4089 window: &mut Window,
4090 cx: &mut App,
4091 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
4092 let registry = cx.default_global::<ProjectItemRegistry>().clone();
4093 registry.open_path(self.project(), &path, window, cx)
4094 }
4095
4096 pub fn find_project_item<T>(
4097 &self,
4098 pane: &Entity<Pane>,
4099 project_item: &Entity<T::Item>,
4100 cx: &App,
4101 ) -> Option<Entity<T>>
4102 where
4103 T: ProjectItem,
4104 {
4105 use project::ProjectItem as _;
4106 let project_item = project_item.read(cx);
4107 let entry_id = project_item.entry_id(cx);
4108 let project_path = project_item.project_path(cx);
4109
4110 let mut item = None;
4111 if let Some(entry_id) = entry_id {
4112 item = pane.read(cx).item_for_entry(entry_id, cx);
4113 }
4114 if item.is_none()
4115 && let Some(project_path) = project_path
4116 {
4117 item = pane.read(cx).item_for_path(project_path, cx);
4118 }
4119
4120 item.and_then(|item| item.downcast::<T>())
4121 }
4122
4123 pub fn is_project_item_open<T>(
4124 &self,
4125 pane: &Entity<Pane>,
4126 project_item: &Entity<T::Item>,
4127 cx: &App,
4128 ) -> bool
4129 where
4130 T: ProjectItem,
4131 {
4132 self.find_project_item::<T>(pane, project_item, cx)
4133 .is_some()
4134 }
4135
4136 pub fn open_project_item<T>(
4137 &mut self,
4138 pane: Entity<Pane>,
4139 project_item: Entity<T::Item>,
4140 activate_pane: bool,
4141 focus_item: bool,
4142 keep_old_preview: bool,
4143 allow_new_preview: bool,
4144 window: &mut Window,
4145 cx: &mut Context<Self>,
4146 ) -> Entity<T>
4147 where
4148 T: ProjectItem,
4149 {
4150 let old_item_id = pane.read(cx).active_item().map(|item| item.item_id());
4151
4152 if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
4153 if !keep_old_preview
4154 && let Some(old_id) = old_item_id
4155 && old_id != item.item_id()
4156 {
4157 // switching to a different item, so unpreview old active item
4158 pane.update(cx, |pane, _| {
4159 pane.unpreview_item_if_preview(old_id);
4160 });
4161 }
4162
4163 self.activate_item(&item, activate_pane, focus_item, window, cx);
4164 if !allow_new_preview {
4165 pane.update(cx, |pane, _| {
4166 pane.unpreview_item_if_preview(item.item_id());
4167 });
4168 }
4169 return item;
4170 }
4171
4172 let item = pane.update(cx, |pane, cx| {
4173 cx.new(|cx| {
4174 T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx)
4175 })
4176 });
4177 let mut destination_index = None;
4178 pane.update(cx, |pane, cx| {
4179 if !keep_old_preview && let Some(old_id) = old_item_id {
4180 pane.unpreview_item_if_preview(old_id);
4181 }
4182 if allow_new_preview {
4183 destination_index = pane.replace_preview_item_id(item.item_id(), window, cx);
4184 }
4185 });
4186
4187 self.add_item(
4188 pane,
4189 Box::new(item.clone()),
4190 destination_index,
4191 activate_pane,
4192 focus_item,
4193 window,
4194 cx,
4195 );
4196 item
4197 }
4198
4199 pub fn open_shared_screen(
4200 &mut self,
4201 peer_id: PeerId,
4202 window: &mut Window,
4203 cx: &mut Context<Self>,
4204 ) {
4205 if let Some(shared_screen) =
4206 self.shared_screen_for_peer(peer_id, &self.active_pane, window, cx)
4207 {
4208 self.active_pane.update(cx, |pane, cx| {
4209 pane.add_item(Box::new(shared_screen), false, true, None, window, cx)
4210 });
4211 }
4212 }
4213
4214 pub fn activate_item(
4215 &mut self,
4216 item: &dyn ItemHandle,
4217 activate_pane: bool,
4218 focus_item: bool,
4219 window: &mut Window,
4220 cx: &mut App,
4221 ) -> bool {
4222 let result = self.panes.iter().find_map(|pane| {
4223 pane.read(cx)
4224 .index_for_item(item)
4225 .map(|ix| (pane.clone(), ix))
4226 });
4227 if let Some((pane, ix)) = result {
4228 pane.update(cx, |pane, cx| {
4229 pane.activate_item(ix, activate_pane, focus_item, window, cx)
4230 });
4231 true
4232 } else {
4233 false
4234 }
4235 }
4236
4237 fn activate_pane_at_index(
4238 &mut self,
4239 action: &ActivatePane,
4240 window: &mut Window,
4241 cx: &mut Context<Self>,
4242 ) {
4243 let panes = self.center.panes();
4244 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
4245 window.focus(&pane.focus_handle(cx), cx);
4246 } else {
4247 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx)
4248 .detach();
4249 }
4250 }
4251
4252 fn move_item_to_pane_at_index(
4253 &mut self,
4254 action: &MoveItemToPane,
4255 window: &mut Window,
4256 cx: &mut Context<Self>,
4257 ) {
4258 let panes = self.center.panes();
4259 let destination = match panes.get(action.destination) {
4260 Some(&destination) => destination.clone(),
4261 None => {
4262 if !action.clone && self.active_pane.read(cx).items_len() < 2 {
4263 return;
4264 }
4265 let direction = SplitDirection::Right;
4266 let split_off_pane = self
4267 .find_pane_in_direction(direction, cx)
4268 .unwrap_or_else(|| self.active_pane.clone());
4269 let new_pane = self.add_pane(window, cx);
4270 if self
4271 .center
4272 .split(&split_off_pane, &new_pane, direction, cx)
4273 .log_err()
4274 .is_none()
4275 {
4276 return;
4277 };
4278 new_pane
4279 }
4280 };
4281
4282 if action.clone {
4283 if self
4284 .active_pane
4285 .read(cx)
4286 .active_item()
4287 .is_some_and(|item| item.can_split(cx))
4288 {
4289 clone_active_item(
4290 self.database_id(),
4291 &self.active_pane,
4292 &destination,
4293 action.focus,
4294 window,
4295 cx,
4296 );
4297 return;
4298 }
4299 }
4300 move_active_item(
4301 &self.active_pane,
4302 &destination,
4303 action.focus,
4304 true,
4305 window,
4306 cx,
4307 )
4308 }
4309
4310 pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) {
4311 let panes = self.center.panes();
4312 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
4313 let next_ix = (ix + 1) % panes.len();
4314 let next_pane = panes[next_ix].clone();
4315 window.focus(&next_pane.focus_handle(cx), cx);
4316 }
4317 }
4318
4319 pub fn activate_previous_pane(&mut self, window: &mut Window, cx: &mut App) {
4320 let panes = self.center.panes();
4321 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
4322 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
4323 let prev_pane = panes[prev_ix].clone();
4324 window.focus(&prev_pane.focus_handle(cx), cx);
4325 }
4326 }
4327
4328 pub fn activate_pane_in_direction(
4329 &mut self,
4330 direction: SplitDirection,
4331 window: &mut Window,
4332 cx: &mut App,
4333 ) {
4334 use ActivateInDirectionTarget as Target;
4335 enum Origin {
4336 LeftDock,
4337 RightDock,
4338 BottomDock,
4339 Center,
4340 }
4341
4342 let origin: Origin = [
4343 (&self.left_dock, Origin::LeftDock),
4344 (&self.right_dock, Origin::RightDock),
4345 (&self.bottom_dock, Origin::BottomDock),
4346 ]
4347 .into_iter()
4348 .find_map(|(dock, origin)| {
4349 if dock.focus_handle(cx).contains_focused(window, cx) && dock.read(cx).is_open() {
4350 Some(origin)
4351 } else {
4352 None
4353 }
4354 })
4355 .unwrap_or(Origin::Center);
4356
4357 let get_last_active_pane = || {
4358 let pane = self
4359 .last_active_center_pane
4360 .clone()
4361 .unwrap_or_else(|| {
4362 self.panes
4363 .first()
4364 .expect("There must be an active pane")
4365 .downgrade()
4366 })
4367 .upgrade()?;
4368 (pane.read(cx).items_len() != 0).then_some(pane)
4369 };
4370
4371 let try_dock =
4372 |dock: &Entity<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
4373
4374 let target = match (origin, direction) {
4375 // We're in the center, so we first try to go to a different pane,
4376 // otherwise try to go to a dock.
4377 (Origin::Center, direction) => {
4378 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
4379 Some(Target::Pane(pane))
4380 } else {
4381 match direction {
4382 SplitDirection::Up => None,
4383 SplitDirection::Down => try_dock(&self.bottom_dock),
4384 SplitDirection::Left => try_dock(&self.left_dock),
4385 SplitDirection::Right => try_dock(&self.right_dock),
4386 }
4387 }
4388 }
4389
4390 (Origin::LeftDock, SplitDirection::Right) => {
4391 if let Some(last_active_pane) = get_last_active_pane() {
4392 Some(Target::Pane(last_active_pane))
4393 } else {
4394 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
4395 }
4396 }
4397
4398 (Origin::LeftDock, SplitDirection::Down)
4399 | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
4400
4401 (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
4402 (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
4403 (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
4404
4405 (Origin::RightDock, SplitDirection::Left) => {
4406 if let Some(last_active_pane) = get_last_active_pane() {
4407 Some(Target::Pane(last_active_pane))
4408 } else {
4409 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
4410 }
4411 }
4412
4413 _ => None,
4414 };
4415
4416 match target {
4417 Some(ActivateInDirectionTarget::Pane(pane)) => {
4418 let pane = pane.read(cx);
4419 if let Some(item) = pane.active_item() {
4420 item.item_focus_handle(cx).focus(window, cx);
4421 } else {
4422 log::error!(
4423 "Could not find a focus target when in switching focus in {direction} direction for a pane",
4424 );
4425 }
4426 }
4427 Some(ActivateInDirectionTarget::Dock(dock)) => {
4428 // Defer this to avoid a panic when the dock's active panel is already on the stack.
4429 window.defer(cx, move |window, cx| {
4430 let dock = dock.read(cx);
4431 if let Some(panel) = dock.active_panel() {
4432 panel.panel_focus_handle(cx).focus(window, cx);
4433 } else {
4434 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
4435 }
4436 })
4437 }
4438 None => {}
4439 }
4440 }
4441
4442 pub fn move_item_to_pane_in_direction(
4443 &mut self,
4444 action: &MoveItemToPaneInDirection,
4445 window: &mut Window,
4446 cx: &mut Context<Self>,
4447 ) {
4448 let destination = match self.find_pane_in_direction(action.direction, cx) {
4449 Some(destination) => destination,
4450 None => {
4451 if !action.clone && self.active_pane.read(cx).items_len() < 2 {
4452 return;
4453 }
4454 let new_pane = self.add_pane(window, cx);
4455 if self
4456 .center
4457 .split(&self.active_pane, &new_pane, action.direction, cx)
4458 .log_err()
4459 .is_none()
4460 {
4461 return;
4462 };
4463 new_pane
4464 }
4465 };
4466
4467 if action.clone {
4468 if self
4469 .active_pane
4470 .read(cx)
4471 .active_item()
4472 .is_some_and(|item| item.can_split(cx))
4473 {
4474 clone_active_item(
4475 self.database_id(),
4476 &self.active_pane,
4477 &destination,
4478 action.focus,
4479 window,
4480 cx,
4481 );
4482 return;
4483 }
4484 }
4485 move_active_item(
4486 &self.active_pane,
4487 &destination,
4488 action.focus,
4489 true,
4490 window,
4491 cx,
4492 );
4493 }
4494
4495 pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
4496 self.center.bounding_box_for_pane(pane)
4497 }
4498
4499 pub fn find_pane_in_direction(
4500 &mut self,
4501 direction: SplitDirection,
4502 cx: &App,
4503 ) -> Option<Entity<Pane>> {
4504 self.center
4505 .find_pane_in_direction(&self.active_pane, direction, cx)
4506 .cloned()
4507 }
4508
4509 pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
4510 if let Some(to) = self.find_pane_in_direction(direction, cx) {
4511 self.center.swap(&self.active_pane, &to, cx);
4512 cx.notify();
4513 }
4514 }
4515
4516 pub fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
4517 if self
4518 .center
4519 .move_to_border(&self.active_pane, direction, cx)
4520 .unwrap()
4521 {
4522 cx.notify();
4523 }
4524 }
4525
4526 pub fn resize_pane(
4527 &mut self,
4528 axis: gpui::Axis,
4529 amount: Pixels,
4530 window: &mut Window,
4531 cx: &mut Context<Self>,
4532 ) {
4533 let docks = self.all_docks();
4534 let active_dock = docks
4535 .into_iter()
4536 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
4537
4538 if let Some(dock) = active_dock {
4539 let Some(panel_size) = dock.read(cx).active_panel_size(window, cx) else {
4540 return;
4541 };
4542 match dock.read(cx).position() {
4543 DockPosition::Left => self.resize_left_dock(panel_size + amount, window, cx),
4544 DockPosition::Bottom => self.resize_bottom_dock(panel_size + amount, window, cx),
4545 DockPosition::Right => self.resize_right_dock(panel_size + amount, window, cx),
4546 }
4547 } else {
4548 self.center
4549 .resize(&self.active_pane, axis, amount, &self.bounds, cx);
4550 }
4551 cx.notify();
4552 }
4553
4554 pub fn reset_pane_sizes(&mut self, cx: &mut Context<Self>) {
4555 self.center.reset_pane_sizes(cx);
4556 cx.notify();
4557 }
4558
4559 fn handle_pane_focused(
4560 &mut self,
4561 pane: Entity<Pane>,
4562 window: &mut Window,
4563 cx: &mut Context<Self>,
4564 ) {
4565 // This is explicitly hoisted out of the following check for pane identity as
4566 // terminal panel panes are not registered as a center panes.
4567 self.status_bar.update(cx, |status_bar, cx| {
4568 status_bar.set_active_pane(&pane, window, cx);
4569 });
4570 if self.active_pane != pane {
4571 self.set_active_pane(&pane, window, cx);
4572 }
4573
4574 if self.last_active_center_pane.is_none() {
4575 self.last_active_center_pane = Some(pane.downgrade());
4576 }
4577
4578 // If this pane is in a dock, preserve that dock when dismissing zoomed items.
4579 // This prevents the dock from closing when focus events fire during window activation.
4580 // We also preserve any dock whose active panel itself has focus — this covers
4581 // panels like AgentPanel that don't implement `pane()` but can still be zoomed.
4582 let dock_to_preserve = self.all_docks().iter().find_map(|dock| {
4583 let dock_read = dock.read(cx);
4584 if let Some(panel) = dock_read.active_panel() {
4585 if panel.pane(cx).is_some_and(|dock_pane| dock_pane == pane)
4586 || panel.panel_focus_handle(cx).contains_focused(window, cx)
4587 {
4588 return Some(dock_read.position());
4589 }
4590 }
4591 None
4592 });
4593
4594 self.dismiss_zoomed_items_to_reveal(dock_to_preserve, window, cx);
4595 if pane.read(cx).is_zoomed() {
4596 self.zoomed = Some(pane.downgrade().into());
4597 } else {
4598 self.zoomed = None;
4599 }
4600 self.zoomed_position = None;
4601 cx.emit(Event::ZoomChanged);
4602 self.update_active_view_for_followers(window, cx);
4603 pane.update(cx, |pane, _| {
4604 pane.track_alternate_file_items();
4605 });
4606
4607 cx.notify();
4608 }
4609
4610 fn set_active_pane(
4611 &mut self,
4612 pane: &Entity<Pane>,
4613 window: &mut Window,
4614 cx: &mut Context<Self>,
4615 ) {
4616 self.active_pane = pane.clone();
4617 self.active_item_path_changed(true, window, cx);
4618 self.last_active_center_pane = Some(pane.downgrade());
4619 }
4620
4621 fn handle_panel_focused(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4622 self.update_active_view_for_followers(window, cx);
4623 }
4624
4625 fn handle_pane_event(
4626 &mut self,
4627 pane: &Entity<Pane>,
4628 event: &pane::Event,
4629 window: &mut Window,
4630 cx: &mut Context<Self>,
4631 ) {
4632 let mut serialize_workspace = true;
4633 match event {
4634 pane::Event::AddItem { item } => {
4635 item.added_to_pane(self, pane.clone(), window, cx);
4636 cx.emit(Event::ItemAdded {
4637 item: item.boxed_clone(),
4638 });
4639 }
4640 pane::Event::Split { direction, mode } => {
4641 match mode {
4642 SplitMode::ClonePane => {
4643 self.split_and_clone(pane.clone(), *direction, window, cx)
4644 .detach();
4645 }
4646 SplitMode::EmptyPane => {
4647 self.split_pane(pane.clone(), *direction, window, cx);
4648 }
4649 SplitMode::MovePane => {
4650 self.split_and_move(pane.clone(), *direction, window, cx);
4651 }
4652 };
4653 }
4654 pane::Event::JoinIntoNext => {
4655 self.join_pane_into_next(pane.clone(), window, cx);
4656 }
4657 pane::Event::JoinAll => {
4658 self.join_all_panes(window, cx);
4659 }
4660 pane::Event::Remove { focus_on_pane } => {
4661 self.remove_pane(pane.clone(), focus_on_pane.clone(), window, cx);
4662 }
4663 pane::Event::ActivateItem {
4664 local,
4665 focus_changed,
4666 } => {
4667 window.invalidate_character_coordinates();
4668
4669 pane.update(cx, |pane, _| {
4670 pane.track_alternate_file_items();
4671 });
4672 if *local {
4673 self.unfollow_in_pane(pane, window, cx);
4674 }
4675 serialize_workspace = *focus_changed || pane != self.active_pane();
4676 if pane == self.active_pane() {
4677 self.active_item_path_changed(*focus_changed, window, cx);
4678 self.update_active_view_for_followers(window, cx);
4679 } else if *local {
4680 self.set_active_pane(pane, window, cx);
4681 }
4682 }
4683 pane::Event::UserSavedItem { item, save_intent } => {
4684 cx.emit(Event::UserSavedItem {
4685 pane: pane.downgrade(),
4686 item: item.boxed_clone(),
4687 save_intent: *save_intent,
4688 });
4689 serialize_workspace = false;
4690 }
4691 pane::Event::ChangeItemTitle => {
4692 if *pane == self.active_pane {
4693 self.active_item_path_changed(false, window, cx);
4694 }
4695 serialize_workspace = false;
4696 }
4697 pane::Event::RemovedItem { item } => {
4698 cx.emit(Event::ActiveItemChanged);
4699 self.update_window_edited(window, cx);
4700 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id())
4701 && entry.get().entity_id() == pane.entity_id()
4702 {
4703 entry.remove();
4704 }
4705 cx.emit(Event::ItemRemoved {
4706 item_id: item.item_id(),
4707 });
4708 }
4709 pane::Event::Focus => {
4710 window.invalidate_character_coordinates();
4711 self.handle_pane_focused(pane.clone(), window, cx);
4712 }
4713 pane::Event::ZoomIn => {
4714 if *pane == self.active_pane {
4715 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
4716 if pane.read(cx).has_focus(window, cx) {
4717 self.zoomed = Some(pane.downgrade().into());
4718 self.zoomed_position = None;
4719 cx.emit(Event::ZoomChanged);
4720 }
4721 cx.notify();
4722 }
4723 }
4724 pane::Event::ZoomOut => {
4725 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
4726 if self.zoomed_position.is_none() {
4727 self.zoomed = None;
4728 cx.emit(Event::ZoomChanged);
4729 }
4730 cx.notify();
4731 }
4732 pane::Event::ItemPinned | pane::Event::ItemUnpinned => {}
4733 }
4734
4735 if serialize_workspace {
4736 self.serialize_workspace(window, cx);
4737 }
4738 }
4739
4740 pub fn unfollow_in_pane(
4741 &mut self,
4742 pane: &Entity<Pane>,
4743 window: &mut Window,
4744 cx: &mut Context<Workspace>,
4745 ) -> Option<CollaboratorId> {
4746 let leader_id = self.leader_for_pane(pane)?;
4747 self.unfollow(leader_id, window, cx);
4748 Some(leader_id)
4749 }
4750
4751 pub fn split_pane(
4752 &mut self,
4753 pane_to_split: Entity<Pane>,
4754 split_direction: SplitDirection,
4755 window: &mut Window,
4756 cx: &mut Context<Self>,
4757 ) -> Entity<Pane> {
4758 let new_pane = self.add_pane(window, cx);
4759 self.center
4760 .split(&pane_to_split, &new_pane, split_direction, cx)
4761 .unwrap();
4762 cx.notify();
4763 new_pane
4764 }
4765
4766 pub fn split_and_move(
4767 &mut self,
4768 pane: Entity<Pane>,
4769 direction: SplitDirection,
4770 window: &mut Window,
4771 cx: &mut Context<Self>,
4772 ) {
4773 let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx)) else {
4774 return;
4775 };
4776 let new_pane = self.add_pane(window, cx);
4777 new_pane.update(cx, |pane, cx| {
4778 pane.add_item(item, true, true, None, window, cx)
4779 });
4780 self.center.split(&pane, &new_pane, direction, cx).unwrap();
4781 cx.notify();
4782 }
4783
4784 pub fn split_and_clone(
4785 &mut self,
4786 pane: Entity<Pane>,
4787 direction: SplitDirection,
4788 window: &mut Window,
4789 cx: &mut Context<Self>,
4790 ) -> Task<Option<Entity<Pane>>> {
4791 let Some(item) = pane.read(cx).active_item() else {
4792 return Task::ready(None);
4793 };
4794 if !item.can_split(cx) {
4795 return Task::ready(None);
4796 }
4797 let task = item.clone_on_split(self.database_id(), window, cx);
4798 cx.spawn_in(window, async move |this, cx| {
4799 if let Some(clone) = task.await {
4800 this.update_in(cx, |this, window, cx| {
4801 let new_pane = this.add_pane(window, cx);
4802 let nav_history = pane.read(cx).fork_nav_history();
4803 new_pane.update(cx, |pane, cx| {
4804 pane.set_nav_history(nav_history, cx);
4805 pane.add_item(clone, true, true, None, window, cx)
4806 });
4807 this.center.split(&pane, &new_pane, direction, cx).unwrap();
4808 cx.notify();
4809 new_pane
4810 })
4811 .ok()
4812 } else {
4813 None
4814 }
4815 })
4816 }
4817
4818 pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4819 let active_item = self.active_pane.read(cx).active_item();
4820 for pane in &self.panes {
4821 join_pane_into_active(&self.active_pane, pane, window, cx);
4822 }
4823 if let Some(active_item) = active_item {
4824 self.activate_item(active_item.as_ref(), true, true, window, cx);
4825 }
4826 cx.notify();
4827 }
4828
4829 pub fn join_pane_into_next(
4830 &mut self,
4831 pane: Entity<Pane>,
4832 window: &mut Window,
4833 cx: &mut Context<Self>,
4834 ) {
4835 let next_pane = self
4836 .find_pane_in_direction(SplitDirection::Right, cx)
4837 .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
4838 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
4839 .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
4840 let Some(next_pane) = next_pane else {
4841 return;
4842 };
4843 move_all_items(&pane, &next_pane, window, cx);
4844 cx.notify();
4845 }
4846
4847 fn remove_pane(
4848 &mut self,
4849 pane: Entity<Pane>,
4850 focus_on: Option<Entity<Pane>>,
4851 window: &mut Window,
4852 cx: &mut Context<Self>,
4853 ) {
4854 if self.center.remove(&pane, cx).unwrap() {
4855 self.force_remove_pane(&pane, &focus_on, window, cx);
4856 self.unfollow_in_pane(&pane, window, cx);
4857 self.last_leaders_by_pane.remove(&pane.downgrade());
4858 for removed_item in pane.read(cx).items() {
4859 self.panes_by_item.remove(&removed_item.item_id());
4860 }
4861
4862 cx.notify();
4863 } else {
4864 self.active_item_path_changed(true, window, cx);
4865 }
4866 cx.emit(Event::PaneRemoved);
4867 }
4868
4869 pub fn panes_mut(&mut self) -> &mut [Entity<Pane>] {
4870 &mut self.panes
4871 }
4872
4873 pub fn panes(&self) -> &[Entity<Pane>] {
4874 &self.panes
4875 }
4876
4877 pub fn active_pane(&self) -> &Entity<Pane> {
4878 &self.active_pane
4879 }
4880
4881 pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> {
4882 for dock in self.all_docks() {
4883 if dock.focus_handle(cx).contains_focused(window, cx)
4884 && let Some(pane) = dock
4885 .read(cx)
4886 .active_panel()
4887 .and_then(|panel| panel.pane(cx))
4888 {
4889 return pane;
4890 }
4891 }
4892 self.active_pane().clone()
4893 }
4894
4895 pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
4896 self.find_pane_in_direction(SplitDirection::Right, cx)
4897 .unwrap_or_else(|| {
4898 self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx)
4899 })
4900 }
4901
4902 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> {
4903 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
4904 weak_pane.upgrade()
4905 }
4906
4907 fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
4908 self.follower_states.retain(|leader_id, state| {
4909 if *leader_id == CollaboratorId::PeerId(peer_id) {
4910 for item in state.items_by_leader_view_id.values() {
4911 item.view.set_leader_id(None, window, cx);
4912 }
4913 false
4914 } else {
4915 true
4916 }
4917 });
4918 cx.notify();
4919 }
4920
4921 pub fn start_following(
4922 &mut self,
4923 leader_id: impl Into<CollaboratorId>,
4924 window: &mut Window,
4925 cx: &mut Context<Self>,
4926 ) -> Option<Task<Result<()>>> {
4927 let leader_id = leader_id.into();
4928 let pane = self.active_pane().clone();
4929
4930 self.last_leaders_by_pane
4931 .insert(pane.downgrade(), leader_id);
4932 self.unfollow(leader_id, window, cx);
4933 self.unfollow_in_pane(&pane, window, cx);
4934 self.follower_states.insert(
4935 leader_id,
4936 FollowerState {
4937 center_pane: pane.clone(),
4938 dock_pane: None,
4939 active_view_id: None,
4940 items_by_leader_view_id: Default::default(),
4941 },
4942 );
4943 cx.notify();
4944
4945 match leader_id {
4946 CollaboratorId::PeerId(leader_peer_id) => {
4947 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
4948 let project_id = self.project.read(cx).remote_id();
4949 let request = self.app_state.client.request(proto::Follow {
4950 room_id,
4951 project_id,
4952 leader_id: Some(leader_peer_id),
4953 });
4954
4955 Some(cx.spawn_in(window, async move |this, cx| {
4956 let response = request.await?;
4957 this.update(cx, |this, _| {
4958 let state = this
4959 .follower_states
4960 .get_mut(&leader_id)
4961 .context("following interrupted")?;
4962 state.active_view_id = response
4963 .active_view
4964 .as_ref()
4965 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
4966 anyhow::Ok(())
4967 })??;
4968 if let Some(view) = response.active_view {
4969 Self::add_view_from_leader(this.clone(), leader_peer_id, &view, cx).await?;
4970 }
4971 this.update_in(cx, |this, window, cx| {
4972 this.leader_updated(leader_id, window, cx)
4973 })?;
4974 Ok(())
4975 }))
4976 }
4977 CollaboratorId::Agent => {
4978 self.leader_updated(leader_id, window, cx)?;
4979 Some(Task::ready(Ok(())))
4980 }
4981 }
4982 }
4983
4984 pub fn follow_next_collaborator(
4985 &mut self,
4986 _: &FollowNextCollaborator,
4987 window: &mut Window,
4988 cx: &mut Context<Self>,
4989 ) {
4990 let collaborators = self.project.read(cx).collaborators();
4991 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
4992 let mut collaborators = collaborators.keys().copied();
4993 for peer_id in collaborators.by_ref() {
4994 if CollaboratorId::PeerId(peer_id) == leader_id {
4995 break;
4996 }
4997 }
4998 collaborators.next().map(CollaboratorId::PeerId)
4999 } else if let Some(last_leader_id) =
5000 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
5001 {
5002 match last_leader_id {
5003 CollaboratorId::PeerId(peer_id) => {
5004 if collaborators.contains_key(peer_id) {
5005 Some(*last_leader_id)
5006 } else {
5007 None
5008 }
5009 }
5010 CollaboratorId::Agent => Some(CollaboratorId::Agent),
5011 }
5012 } else {
5013 None
5014 };
5015
5016 let pane = self.active_pane.clone();
5017 let Some(leader_id) = next_leader_id.or_else(|| {
5018 Some(CollaboratorId::PeerId(
5019 collaborators.keys().copied().next()?,
5020 ))
5021 }) else {
5022 return;
5023 };
5024 if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
5025 return;
5026 }
5027 if let Some(task) = self.start_following(leader_id, window, cx) {
5028 task.detach_and_log_err(cx)
5029 }
5030 }
5031
5032 pub fn follow(
5033 &mut self,
5034 leader_id: impl Into<CollaboratorId>,
5035 window: &mut Window,
5036 cx: &mut Context<Self>,
5037 ) {
5038 let leader_id = leader_id.into();
5039
5040 if let CollaboratorId::PeerId(peer_id) = leader_id {
5041 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
5042 return;
5043 };
5044 let room = room.read(cx);
5045 let Some(remote_participant) = room.remote_participant_for_peer_id(peer_id) else {
5046 return;
5047 };
5048
5049 let project = self.project.read(cx);
5050
5051 let other_project_id = match remote_participant.location {
5052 call::ParticipantLocation::External => None,
5053 call::ParticipantLocation::UnsharedProject => None,
5054 call::ParticipantLocation::SharedProject { project_id } => {
5055 if Some(project_id) == project.remote_id() {
5056 None
5057 } else {
5058 Some(project_id)
5059 }
5060 }
5061 };
5062
5063 // if they are active in another project, follow there.
5064 if let Some(project_id) = other_project_id {
5065 let app_state = self.app_state.clone();
5066 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
5067 .detach_and_log_err(cx);
5068 }
5069 }
5070
5071 // if you're already following, find the right pane and focus it.
5072 if let Some(follower_state) = self.follower_states.get(&leader_id) {
5073 window.focus(&follower_state.pane().focus_handle(cx), cx);
5074
5075 return;
5076 }
5077
5078 // Otherwise, follow.
5079 if let Some(task) = self.start_following(leader_id, window, cx) {
5080 task.detach_and_log_err(cx)
5081 }
5082 }
5083
5084 pub fn unfollow(
5085 &mut self,
5086 leader_id: impl Into<CollaboratorId>,
5087 window: &mut Window,
5088 cx: &mut Context<Self>,
5089 ) -> Option<()> {
5090 cx.notify();
5091
5092 let leader_id = leader_id.into();
5093 let state = self.follower_states.remove(&leader_id)?;
5094 for (_, item) in state.items_by_leader_view_id {
5095 item.view.set_leader_id(None, window, cx);
5096 }
5097
5098 if let CollaboratorId::PeerId(leader_peer_id) = leader_id {
5099 let project_id = self.project.read(cx).remote_id();
5100 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
5101 self.app_state
5102 .client
5103 .send(proto::Unfollow {
5104 room_id,
5105 project_id,
5106 leader_id: Some(leader_peer_id),
5107 })
5108 .log_err();
5109 }
5110
5111 Some(())
5112 }
5113
5114 pub fn is_being_followed(&self, id: impl Into<CollaboratorId>) -> bool {
5115 self.follower_states.contains_key(&id.into())
5116 }
5117
5118 fn active_item_path_changed(
5119 &mut self,
5120 focus_changed: bool,
5121 window: &mut Window,
5122 cx: &mut Context<Self>,
5123 ) {
5124 cx.emit(Event::ActiveItemChanged);
5125 let active_entry = self.active_project_path(cx);
5126 self.project.update(cx, |project, cx| {
5127 project.set_active_path(active_entry.clone(), cx)
5128 });
5129
5130 if focus_changed && let Some(project_path) = &active_entry {
5131 let git_store_entity = self.project.read(cx).git_store().clone();
5132 git_store_entity.update(cx, |git_store, cx| {
5133 git_store.set_active_repo_for_path(project_path, cx);
5134 });
5135 }
5136
5137 self.update_window_title(window, cx);
5138 }
5139
5140 fn update_window_title(&mut self, window: &mut Window, cx: &mut App) {
5141 let project = self.project().read(cx);
5142 let mut title = String::new();
5143
5144 for (i, worktree) in project.visible_worktrees(cx).enumerate() {
5145 let name = {
5146 let settings_location = SettingsLocation {
5147 worktree_id: worktree.read(cx).id(),
5148 path: RelPath::empty(),
5149 };
5150
5151 let settings = WorktreeSettings::get(Some(settings_location), cx);
5152 match &settings.project_name {
5153 Some(name) => name.as_str(),
5154 None => worktree.read(cx).root_name_str(),
5155 }
5156 };
5157 if i > 0 {
5158 title.push_str(", ");
5159 }
5160 title.push_str(name);
5161 }
5162
5163 if title.is_empty() {
5164 title = "empty project".to_string();
5165 }
5166
5167 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
5168 let filename = path.path.file_name().or_else(|| {
5169 Some(
5170 project
5171 .worktree_for_id(path.worktree_id, cx)?
5172 .read(cx)
5173 .root_name_str(),
5174 )
5175 });
5176
5177 if let Some(filename) = filename {
5178 title.push_str(" — ");
5179 title.push_str(filename.as_ref());
5180 }
5181 }
5182
5183 if project.is_via_collab() {
5184 title.push_str(" ↙");
5185 } else if project.is_shared() {
5186 title.push_str(" ↗");
5187 }
5188
5189 if let Some(last_title) = self.last_window_title.as_ref()
5190 && &title == last_title
5191 {
5192 return;
5193 }
5194 window.set_window_title(&title);
5195 SystemWindowTabController::update_tab_title(
5196 cx,
5197 window.window_handle().window_id(),
5198 SharedString::from(&title),
5199 );
5200 self.last_window_title = Some(title);
5201 }
5202
5203 fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
5204 let is_edited = !self.project.read(cx).is_disconnected(cx) && !self.dirty_items.is_empty();
5205 if is_edited != self.window_edited {
5206 self.window_edited = is_edited;
5207 window.set_window_edited(self.window_edited)
5208 }
5209 }
5210
5211 fn update_item_dirty_state(
5212 &mut self,
5213 item: &dyn ItemHandle,
5214 window: &mut Window,
5215 cx: &mut App,
5216 ) {
5217 let is_dirty = item.is_dirty(cx);
5218 let item_id = item.item_id();
5219 let was_dirty = self.dirty_items.contains_key(&item_id);
5220 if is_dirty == was_dirty {
5221 return;
5222 }
5223 if was_dirty {
5224 self.dirty_items.remove(&item_id);
5225 self.update_window_edited(window, cx);
5226 return;
5227 }
5228
5229 let workspace = self.weak_handle();
5230 let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
5231 return;
5232 };
5233 let on_release_callback = Box::new(move |cx: &mut App| {
5234 window_handle
5235 .update(cx, |_, window, cx| {
5236 workspace
5237 .update(cx, |workspace, cx| {
5238 workspace.dirty_items.remove(&item_id);
5239 workspace.update_window_edited(window, cx)
5240 })
5241 .ok();
5242 })
5243 .ok();
5244 });
5245
5246 let s = item.on_release(cx, on_release_callback);
5247 self.dirty_items.insert(item_id, s);
5248 self.update_window_edited(window, cx);
5249 }
5250
5251 fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
5252 if self.notifications.is_empty() {
5253 None
5254 } else {
5255 Some(
5256 div()
5257 .absolute()
5258 .right_3()
5259 .bottom_3()
5260 .w_112()
5261 .h_full()
5262 .flex()
5263 .flex_col()
5264 .justify_end()
5265 .gap_2()
5266 .children(
5267 self.notifications
5268 .iter()
5269 .map(|(_, notification)| notification.clone().into_any()),
5270 ),
5271 )
5272 }
5273 }
5274
5275 // RPC handlers
5276
5277 fn active_view_for_follower(
5278 &self,
5279 follower_project_id: Option<u64>,
5280 window: &mut Window,
5281 cx: &mut Context<Self>,
5282 ) -> Option<proto::View> {
5283 let (item, panel_id) = self.active_item_for_followers(window, cx);
5284 let item = item?;
5285 let leader_id = self
5286 .pane_for(&*item)
5287 .and_then(|pane| self.leader_for_pane(&pane));
5288 let leader_peer_id = match leader_id {
5289 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
5290 Some(CollaboratorId::Agent) | None => None,
5291 };
5292
5293 let item_handle = item.to_followable_item_handle(cx)?;
5294 let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
5295 let variant = item_handle.to_state_proto(window, cx)?;
5296
5297 if item_handle.is_project_item(window, cx)
5298 && (follower_project_id.is_none()
5299 || follower_project_id != self.project.read(cx).remote_id())
5300 {
5301 return None;
5302 }
5303
5304 Some(proto::View {
5305 id: id.to_proto(),
5306 leader_id: leader_peer_id,
5307 variant: Some(variant),
5308 panel_id: panel_id.map(|id| id as i32),
5309 })
5310 }
5311
5312 fn handle_follow(
5313 &mut self,
5314 follower_project_id: Option<u64>,
5315 window: &mut Window,
5316 cx: &mut Context<Self>,
5317 ) -> proto::FollowResponse {
5318 let active_view = self.active_view_for_follower(follower_project_id, window, cx);
5319
5320 cx.notify();
5321 proto::FollowResponse {
5322 views: active_view.iter().cloned().collect(),
5323 active_view,
5324 }
5325 }
5326
5327 fn handle_update_followers(
5328 &mut self,
5329 leader_id: PeerId,
5330 message: proto::UpdateFollowers,
5331 _window: &mut Window,
5332 _cx: &mut Context<Self>,
5333 ) {
5334 self.leader_updates_tx
5335 .unbounded_send((leader_id, message))
5336 .ok();
5337 }
5338
5339 async fn process_leader_update(
5340 this: &WeakEntity<Self>,
5341 leader_id: PeerId,
5342 update: proto::UpdateFollowers,
5343 cx: &mut AsyncWindowContext,
5344 ) -> Result<()> {
5345 match update.variant.context("invalid update")? {
5346 proto::update_followers::Variant::CreateView(view) => {
5347 let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
5348 let should_add_view = this.update(cx, |this, _| {
5349 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
5350 anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
5351 } else {
5352 anyhow::Ok(false)
5353 }
5354 })??;
5355
5356 if should_add_view {
5357 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
5358 }
5359 }
5360 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
5361 let should_add_view = this.update(cx, |this, _| {
5362 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
5363 state.active_view_id = update_active_view
5364 .view
5365 .as_ref()
5366 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
5367
5368 if state.active_view_id.is_some_and(|view_id| {
5369 !state.items_by_leader_view_id.contains_key(&view_id)
5370 }) {
5371 anyhow::Ok(true)
5372 } else {
5373 anyhow::Ok(false)
5374 }
5375 } else {
5376 anyhow::Ok(false)
5377 }
5378 })??;
5379
5380 if should_add_view && let Some(view) = update_active_view.view {
5381 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
5382 }
5383 }
5384 proto::update_followers::Variant::UpdateView(update_view) => {
5385 let variant = update_view.variant.context("missing update view variant")?;
5386 let id = update_view.id.context("missing update view id")?;
5387 let mut tasks = Vec::new();
5388 this.update_in(cx, |this, window, cx| {
5389 let project = this.project.clone();
5390 if let Some(state) = this.follower_states.get(&leader_id.into()) {
5391 let view_id = ViewId::from_proto(id.clone())?;
5392 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
5393 tasks.push(item.view.apply_update_proto(
5394 &project,
5395 variant.clone(),
5396 window,
5397 cx,
5398 ));
5399 }
5400 }
5401 anyhow::Ok(())
5402 })??;
5403 try_join_all(tasks).await.log_err();
5404 }
5405 }
5406 this.update_in(cx, |this, window, cx| {
5407 this.leader_updated(leader_id, window, cx)
5408 })?;
5409 Ok(())
5410 }
5411
5412 async fn add_view_from_leader(
5413 this: WeakEntity<Self>,
5414 leader_id: PeerId,
5415 view: &proto::View,
5416 cx: &mut AsyncWindowContext,
5417 ) -> Result<()> {
5418 let this = this.upgrade().context("workspace dropped")?;
5419
5420 let Some(id) = view.id.clone() else {
5421 anyhow::bail!("no id for view");
5422 };
5423 let id = ViewId::from_proto(id)?;
5424 let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
5425
5426 let pane = this.update(cx, |this, _cx| {
5427 let state = this
5428 .follower_states
5429 .get(&leader_id.into())
5430 .context("stopped following")?;
5431 anyhow::Ok(state.pane().clone())
5432 })?;
5433 let existing_item = pane.update_in(cx, |pane, window, cx| {
5434 let client = this.read(cx).client().clone();
5435 pane.items().find_map(|item| {
5436 let item = item.to_followable_item_handle(cx)?;
5437 if item.remote_id(&client, window, cx) == Some(id) {
5438 Some(item)
5439 } else {
5440 None
5441 }
5442 })
5443 })?;
5444 let item = if let Some(existing_item) = existing_item {
5445 existing_item
5446 } else {
5447 let variant = view.variant.clone();
5448 anyhow::ensure!(variant.is_some(), "missing view variant");
5449
5450 let task = cx.update(|window, cx| {
5451 FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx)
5452 })?;
5453
5454 let Some(task) = task else {
5455 anyhow::bail!(
5456 "failed to construct view from leader (maybe from a different version of zed?)"
5457 );
5458 };
5459
5460 let mut new_item = task.await?;
5461 pane.update_in(cx, |pane, window, cx| {
5462 let mut item_to_remove = None;
5463 for (ix, item) in pane.items().enumerate() {
5464 if let Some(item) = item.to_followable_item_handle(cx) {
5465 match new_item.dedup(item.as_ref(), window, cx) {
5466 Some(item::Dedup::KeepExisting) => {
5467 new_item =
5468 item.boxed_clone().to_followable_item_handle(cx).unwrap();
5469 break;
5470 }
5471 Some(item::Dedup::ReplaceExisting) => {
5472 item_to_remove = Some((ix, item.item_id()));
5473 break;
5474 }
5475 None => {}
5476 }
5477 }
5478 }
5479
5480 if let Some((ix, id)) = item_to_remove {
5481 pane.remove_item(id, false, false, window, cx);
5482 pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx);
5483 }
5484 })?;
5485
5486 new_item
5487 };
5488
5489 this.update_in(cx, |this, window, cx| {
5490 let state = this.follower_states.get_mut(&leader_id.into())?;
5491 item.set_leader_id(Some(leader_id.into()), window, cx);
5492 state.items_by_leader_view_id.insert(
5493 id,
5494 FollowerView {
5495 view: item,
5496 location: panel_id,
5497 },
5498 );
5499
5500 Some(())
5501 })
5502 .context("no follower state")?;
5503
5504 Ok(())
5505 }
5506
5507 fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5508 let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else {
5509 return;
5510 };
5511
5512 if let Some(agent_location) = self.project.read(cx).agent_location() {
5513 let buffer_entity_id = agent_location.buffer.entity_id();
5514 let view_id = ViewId {
5515 creator: CollaboratorId::Agent,
5516 id: buffer_entity_id.as_u64(),
5517 };
5518 follower_state.active_view_id = Some(view_id);
5519
5520 let item = match follower_state.items_by_leader_view_id.entry(view_id) {
5521 hash_map::Entry::Occupied(entry) => Some(entry.into_mut()),
5522 hash_map::Entry::Vacant(entry) => {
5523 let existing_view =
5524 follower_state
5525 .center_pane
5526 .read(cx)
5527 .items()
5528 .find_map(|item| {
5529 let item = item.to_followable_item_handle(cx)?;
5530 if item.buffer_kind(cx) == ItemBufferKind::Singleton
5531 && item.project_item_model_ids(cx).as_slice()
5532 == [buffer_entity_id]
5533 {
5534 Some(item)
5535 } else {
5536 None
5537 }
5538 });
5539 let view = existing_view.or_else(|| {
5540 agent_location.buffer.upgrade().and_then(|buffer| {
5541 cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| {
5542 registry.build_item(buffer, self.project.clone(), None, window, cx)
5543 })?
5544 .to_followable_item_handle(cx)
5545 })
5546 });
5547
5548 view.map(|view| {
5549 entry.insert(FollowerView {
5550 view,
5551 location: None,
5552 })
5553 })
5554 }
5555 };
5556
5557 if let Some(item) = item {
5558 item.view
5559 .set_leader_id(Some(CollaboratorId::Agent), window, cx);
5560 item.view
5561 .update_agent_location(agent_location.position, window, cx);
5562 }
5563 } else {
5564 follower_state.active_view_id = None;
5565 }
5566
5567 self.leader_updated(CollaboratorId::Agent, window, cx);
5568 }
5569
5570 pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
5571 let mut is_project_item = true;
5572 let mut update = proto::UpdateActiveView::default();
5573 if window.is_window_active() {
5574 let (active_item, panel_id) = self.active_item_for_followers(window, cx);
5575
5576 if let Some(item) = active_item
5577 && item.item_focus_handle(cx).contains_focused(window, cx)
5578 {
5579 let leader_id = self
5580 .pane_for(&*item)
5581 .and_then(|pane| self.leader_for_pane(&pane));
5582 let leader_peer_id = match leader_id {
5583 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
5584 Some(CollaboratorId::Agent) | None => None,
5585 };
5586
5587 if let Some(item) = item.to_followable_item_handle(cx) {
5588 let id = item
5589 .remote_id(&self.app_state.client, window, cx)
5590 .map(|id| id.to_proto());
5591
5592 if let Some(id) = id
5593 && let Some(variant) = item.to_state_proto(window, cx)
5594 {
5595 let view = Some(proto::View {
5596 id,
5597 leader_id: leader_peer_id,
5598 variant: Some(variant),
5599 panel_id: panel_id.map(|id| id as i32),
5600 });
5601
5602 is_project_item = item.is_project_item(window, cx);
5603 update = proto::UpdateActiveView { view };
5604 };
5605 }
5606 }
5607 }
5608
5609 let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
5610 if active_view_id != self.last_active_view_id.as_ref() {
5611 self.last_active_view_id = active_view_id.cloned();
5612 self.update_followers(
5613 is_project_item,
5614 proto::update_followers::Variant::UpdateActiveView(update),
5615 window,
5616 cx,
5617 );
5618 }
5619 }
5620
5621 fn active_item_for_followers(
5622 &self,
5623 window: &mut Window,
5624 cx: &mut App,
5625 ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
5626 let mut active_item = None;
5627 let mut panel_id = None;
5628 for dock in self.all_docks() {
5629 if dock.focus_handle(cx).contains_focused(window, cx)
5630 && let Some(panel) = dock.read(cx).active_panel()
5631 && let Some(pane) = panel.pane(cx)
5632 && let Some(item) = pane.read(cx).active_item()
5633 {
5634 active_item = Some(item);
5635 panel_id = panel.remote_id();
5636 break;
5637 }
5638 }
5639
5640 if active_item.is_none() {
5641 active_item = self.active_pane().read(cx).active_item();
5642 }
5643 (active_item, panel_id)
5644 }
5645
5646 fn update_followers(
5647 &self,
5648 project_only: bool,
5649 update: proto::update_followers::Variant,
5650 _: &mut Window,
5651 cx: &mut App,
5652 ) -> Option<()> {
5653 // If this update only applies to for followers in the current project,
5654 // then skip it unless this project is shared. If it applies to all
5655 // followers, regardless of project, then set `project_id` to none,
5656 // indicating that it goes to all followers.
5657 let project_id = if project_only {
5658 Some(self.project.read(cx).remote_id()?)
5659 } else {
5660 None
5661 };
5662 self.app_state().workspace_store.update(cx, |store, cx| {
5663 store.update_followers(project_id, update, cx)
5664 })
5665 }
5666
5667 pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<CollaboratorId> {
5668 self.follower_states.iter().find_map(|(leader_id, state)| {
5669 if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
5670 Some(*leader_id)
5671 } else {
5672 None
5673 }
5674 })
5675 }
5676
5677 fn leader_updated(
5678 &mut self,
5679 leader_id: impl Into<CollaboratorId>,
5680 window: &mut Window,
5681 cx: &mut Context<Self>,
5682 ) -> Option<Box<dyn ItemHandle>> {
5683 cx.notify();
5684
5685 let leader_id = leader_id.into();
5686 let (panel_id, item) = match leader_id {
5687 CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?,
5688 CollaboratorId::Agent => (None, self.active_item_for_agent()?),
5689 };
5690
5691 let state = self.follower_states.get(&leader_id)?;
5692 let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
5693 let pane;
5694 if let Some(panel_id) = panel_id {
5695 pane = self
5696 .activate_panel_for_proto_id(panel_id, window, cx)?
5697 .pane(cx)?;
5698 let state = self.follower_states.get_mut(&leader_id)?;
5699 state.dock_pane = Some(pane.clone());
5700 } else {
5701 pane = state.center_pane.clone();
5702 let state = self.follower_states.get_mut(&leader_id)?;
5703 if let Some(dock_pane) = state.dock_pane.take() {
5704 transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx);
5705 }
5706 }
5707
5708 pane.update(cx, |pane, cx| {
5709 let focus_active_item = pane.has_focus(window, cx) || transfer_focus;
5710 if let Some(index) = pane.index_for_item(item.as_ref()) {
5711 pane.activate_item(index, false, false, window, cx);
5712 } else {
5713 pane.add_item(item.boxed_clone(), false, false, None, window, cx)
5714 }
5715
5716 if focus_active_item {
5717 pane.focus_active_item(window, cx)
5718 }
5719 });
5720
5721 Some(item)
5722 }
5723
5724 fn active_item_for_agent(&self) -> Option<Box<dyn ItemHandle>> {
5725 let state = self.follower_states.get(&CollaboratorId::Agent)?;
5726 let active_view_id = state.active_view_id?;
5727 Some(
5728 state
5729 .items_by_leader_view_id
5730 .get(&active_view_id)?
5731 .view
5732 .boxed_clone(),
5733 )
5734 }
5735
5736 fn active_item_for_peer(
5737 &self,
5738 peer_id: PeerId,
5739 window: &mut Window,
5740 cx: &mut Context<Self>,
5741 ) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
5742 let call = self.active_call()?;
5743 let room = call.read(cx).room()?.read(cx);
5744 let participant = room.remote_participant_for_peer_id(peer_id)?;
5745 let leader_in_this_app;
5746 let leader_in_this_project;
5747 match participant.location {
5748 call::ParticipantLocation::SharedProject { project_id } => {
5749 leader_in_this_app = true;
5750 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
5751 }
5752 call::ParticipantLocation::UnsharedProject => {
5753 leader_in_this_app = true;
5754 leader_in_this_project = false;
5755 }
5756 call::ParticipantLocation::External => {
5757 leader_in_this_app = false;
5758 leader_in_this_project = false;
5759 }
5760 };
5761 let state = self.follower_states.get(&peer_id.into())?;
5762 let mut item_to_activate = None;
5763 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
5764 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id)
5765 && (leader_in_this_project || !item.view.is_project_item(window, cx))
5766 {
5767 item_to_activate = Some((item.location, item.view.boxed_clone()));
5768 }
5769 } else if let Some(shared_screen) =
5770 self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx)
5771 {
5772 item_to_activate = Some((None, Box::new(shared_screen)));
5773 }
5774 item_to_activate
5775 }
5776
5777 fn shared_screen_for_peer(
5778 &self,
5779 peer_id: PeerId,
5780 pane: &Entity<Pane>,
5781 window: &mut Window,
5782 cx: &mut App,
5783 ) -> Option<Entity<SharedScreen>> {
5784 let call = self.active_call()?;
5785 let room = call.read(cx).room()?.clone();
5786 let participant = room.read(cx).remote_participant_for_peer_id(peer_id)?;
5787 let track = participant.video_tracks.values().next()?.clone();
5788 let user = participant.user.clone();
5789
5790 for item in pane.read(cx).items_of_type::<SharedScreen>() {
5791 if item.read(cx).peer_id == peer_id {
5792 return Some(item);
5793 }
5794 }
5795
5796 Some(cx.new(|cx| SharedScreen::new(track, peer_id, user.clone(), room.clone(), window, cx)))
5797 }
5798
5799 pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5800 if window.is_window_active() {
5801 self.update_active_view_for_followers(window, cx);
5802
5803 if let Some(database_id) = self.database_id {
5804 cx.background_spawn(persistence::DB.update_timestamp(database_id))
5805 .detach();
5806 }
5807 } else {
5808 for pane in &self.panes {
5809 pane.update(cx, |pane, cx| {
5810 if let Some(item) = pane.active_item() {
5811 item.workspace_deactivated(window, cx);
5812 }
5813 for item in pane.items() {
5814 if matches!(
5815 item.workspace_settings(cx).autosave,
5816 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
5817 ) {
5818 Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
5819 .detach_and_log_err(cx);
5820 }
5821 }
5822 });
5823 }
5824 }
5825 }
5826
5827 pub fn active_call(&self) -> Option<&Entity<ActiveCall>> {
5828 self.active_call.as_ref().map(|(call, _)| call)
5829 }
5830
5831 fn on_active_call_event(
5832 &mut self,
5833 _: &Entity<ActiveCall>,
5834 event: &call::room::Event,
5835 window: &mut Window,
5836 cx: &mut Context<Self>,
5837 ) {
5838 match event {
5839 call::room::Event::ParticipantLocationChanged { participant_id }
5840 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
5841 self.leader_updated(participant_id, window, cx);
5842 }
5843 _ => {}
5844 }
5845 }
5846
5847 pub fn database_id(&self) -> Option<WorkspaceId> {
5848 self.database_id
5849 }
5850
5851 pub(crate) fn set_database_id(&mut self, id: WorkspaceId) {
5852 self.database_id = Some(id);
5853 }
5854
5855 pub fn session_id(&self) -> Option<String> {
5856 self.session_id.clone()
5857 }
5858
5859 /// Bypass the 200ms serialization throttle and write workspace state to
5860 /// the DB immediately. Returns a task the caller can await to ensure the
5861 /// write completes. Used by the quit handler so the most recent state
5862 /// isn't lost to a pending throttle timer when the process exits.
5863 pub fn flush_serialization(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
5864 self._schedule_serialize_workspace.take();
5865 self._serialize_workspace_task.take();
5866 self.serialize_workspace_internal(window, cx)
5867 }
5868
5869 pub fn root_paths(&self, cx: &App) -> Vec<Arc<Path>> {
5870 let project = self.project().read(cx);
5871 project
5872 .visible_worktrees(cx)
5873 .map(|worktree| worktree.read(cx).abs_path())
5874 .collect::<Vec<_>>()
5875 }
5876
5877 fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
5878 match member {
5879 Member::Axis(PaneAxis { members, .. }) => {
5880 for child in members.iter() {
5881 self.remove_panes(child.clone(), window, cx)
5882 }
5883 }
5884 Member::Pane(pane) => {
5885 self.force_remove_pane(&pane, &None, window, cx);
5886 }
5887 }
5888 }
5889
5890 fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
5891 self.session_id.take();
5892 self.serialize_workspace_internal(window, cx)
5893 }
5894
5895 fn force_remove_pane(
5896 &mut self,
5897 pane: &Entity<Pane>,
5898 focus_on: &Option<Entity<Pane>>,
5899 window: &mut Window,
5900 cx: &mut Context<Workspace>,
5901 ) {
5902 self.panes.retain(|p| p != pane);
5903 if let Some(focus_on) = focus_on {
5904 focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
5905 } else if self.active_pane() == pane {
5906 self.panes
5907 .last()
5908 .unwrap()
5909 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
5910 }
5911 if self.last_active_center_pane == Some(pane.downgrade()) {
5912 self.last_active_center_pane = None;
5913 }
5914 cx.notify();
5915 }
5916
5917 fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5918 if self._schedule_serialize_workspace.is_none() {
5919 self._schedule_serialize_workspace =
5920 Some(cx.spawn_in(window, async move |this, cx| {
5921 cx.background_executor()
5922 .timer(SERIALIZATION_THROTTLE_TIME)
5923 .await;
5924 this.update_in(cx, |this, window, cx| {
5925 this._serialize_workspace_task =
5926 Some(this.serialize_workspace_internal(window, cx));
5927 this._schedule_serialize_workspace.take();
5928 })
5929 .log_err();
5930 }));
5931 }
5932 }
5933
5934 fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
5935 let Some(database_id) = self.database_id() else {
5936 return Task::ready(());
5937 };
5938
5939 fn serialize_pane_handle(
5940 pane_handle: &Entity<Pane>,
5941 window: &mut Window,
5942 cx: &mut App,
5943 ) -> SerializedPane {
5944 let (items, active, pinned_count) = {
5945 let pane = pane_handle.read(cx);
5946 let active_item_id = pane.active_item().map(|item| item.item_id());
5947 (
5948 pane.items()
5949 .filter_map(|handle| {
5950 let handle = handle.to_serializable_item_handle(cx)?;
5951
5952 Some(SerializedItem {
5953 kind: Arc::from(handle.serialized_item_kind()),
5954 item_id: handle.item_id().as_u64(),
5955 active: Some(handle.item_id()) == active_item_id,
5956 preview: pane.is_active_preview_item(handle.item_id()),
5957 })
5958 })
5959 .collect::<Vec<_>>(),
5960 pane.has_focus(window, cx),
5961 pane.pinned_count(),
5962 )
5963 };
5964
5965 SerializedPane::new(items, active, pinned_count)
5966 }
5967
5968 fn build_serialized_pane_group(
5969 pane_group: &Member,
5970 window: &mut Window,
5971 cx: &mut App,
5972 ) -> SerializedPaneGroup {
5973 match pane_group {
5974 Member::Axis(PaneAxis {
5975 axis,
5976 members,
5977 flexes,
5978 bounding_boxes: _,
5979 }) => SerializedPaneGroup::Group {
5980 axis: SerializedAxis(*axis),
5981 children: members
5982 .iter()
5983 .map(|member| build_serialized_pane_group(member, window, cx))
5984 .collect::<Vec<_>>(),
5985 flexes: Some(flexes.lock().clone()),
5986 },
5987 Member::Pane(pane_handle) => {
5988 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
5989 }
5990 }
5991 }
5992
5993 fn build_serialized_docks(
5994 this: &Workspace,
5995 window: &mut Window,
5996 cx: &mut App,
5997 ) -> DockStructure {
5998 let left_dock = this.left_dock.read(cx);
5999 let left_visible = left_dock.is_open();
6000 let left_active_panel = left_dock
6001 .active_panel()
6002 .map(|panel| panel.persistent_name().to_string());
6003 let left_dock_zoom = left_dock
6004 .active_panel()
6005 .map(|panel| panel.is_zoomed(window, cx))
6006 .unwrap_or(false);
6007
6008 let right_dock = this.right_dock.read(cx);
6009 let right_visible = right_dock.is_open();
6010 let right_active_panel = right_dock
6011 .active_panel()
6012 .map(|panel| panel.persistent_name().to_string());
6013 let right_dock_zoom = right_dock
6014 .active_panel()
6015 .map(|panel| panel.is_zoomed(window, cx))
6016 .unwrap_or(false);
6017
6018 let bottom_dock = this.bottom_dock.read(cx);
6019 let bottom_visible = bottom_dock.is_open();
6020 let bottom_active_panel = bottom_dock
6021 .active_panel()
6022 .map(|panel| panel.persistent_name().to_string());
6023 let bottom_dock_zoom = bottom_dock
6024 .active_panel()
6025 .map(|panel| panel.is_zoomed(window, cx))
6026 .unwrap_or(false);
6027
6028 DockStructure {
6029 left: DockData {
6030 visible: left_visible,
6031 active_panel: left_active_panel,
6032 zoom: left_dock_zoom,
6033 },
6034 right: DockData {
6035 visible: right_visible,
6036 active_panel: right_active_panel,
6037 zoom: right_dock_zoom,
6038 },
6039 bottom: DockData {
6040 visible: bottom_visible,
6041 active_panel: bottom_active_panel,
6042 zoom: bottom_dock_zoom,
6043 },
6044 }
6045 }
6046
6047 match self.workspace_location(cx) {
6048 WorkspaceLocation::Location(location, paths) => {
6049 let breakpoints = self.project.update(cx, |project, cx| {
6050 project
6051 .breakpoint_store()
6052 .read(cx)
6053 .all_source_breakpoints(cx)
6054 });
6055 let user_toolchains = self
6056 .project
6057 .read(cx)
6058 .user_toolchains(cx)
6059 .unwrap_or_default();
6060
6061 let center_group = build_serialized_pane_group(&self.center.root, window, cx);
6062 let docks = build_serialized_docks(self, window, cx);
6063 let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
6064
6065 let serialized_workspace = SerializedWorkspace {
6066 id: database_id,
6067 location,
6068 paths,
6069 center_group,
6070 window_bounds,
6071 display: Default::default(),
6072 docks,
6073 centered_layout: self.centered_layout,
6074 session_id: self.session_id.clone(),
6075 breakpoints,
6076 window_id: Some(window.window_handle().window_id().as_u64()),
6077 user_toolchains,
6078 };
6079
6080 window.spawn(cx, async move |_| {
6081 persistence::DB.save_workspace(serialized_workspace).await;
6082 })
6083 }
6084 WorkspaceLocation::DetachFromSession => {
6085 let window_bounds = SerializedWindowBounds(window.window_bounds());
6086 let display = window.display(cx).and_then(|d| d.uuid().ok());
6087 // Save dock state for empty local workspaces
6088 let docks = build_serialized_docks(self, window, cx);
6089 window.spawn(cx, async move |_| {
6090 persistence::DB
6091 .set_window_open_status(
6092 database_id,
6093 window_bounds,
6094 display.unwrap_or_default(),
6095 )
6096 .await
6097 .log_err();
6098 persistence::DB
6099 .set_session_id(database_id, None)
6100 .await
6101 .log_err();
6102 persistence::write_default_dock_state(docks).await.log_err();
6103 })
6104 }
6105 WorkspaceLocation::None => {
6106 // Save dock state for empty non-local workspaces
6107 let docks = build_serialized_docks(self, window, cx);
6108 window.spawn(cx, async move |_| {
6109 persistence::write_default_dock_state(docks).await.log_err();
6110 })
6111 }
6112 }
6113 }
6114
6115 fn has_any_items_open(&self, cx: &App) -> bool {
6116 self.panes.iter().any(|pane| pane.read(cx).items_len() > 0)
6117 }
6118
6119 fn workspace_location(&self, cx: &App) -> WorkspaceLocation {
6120 let paths = PathList::new(&self.root_paths(cx));
6121 if let Some(connection) = self.project.read(cx).remote_connection_options(cx) {
6122 WorkspaceLocation::Location(SerializedWorkspaceLocation::Remote(connection), paths)
6123 } else if self.project.read(cx).is_local() {
6124 if !paths.is_empty() || self.has_any_items_open(cx) {
6125 WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths)
6126 } else {
6127 WorkspaceLocation::DetachFromSession
6128 }
6129 } else {
6130 WorkspaceLocation::None
6131 }
6132 }
6133
6134 fn update_history(&self, cx: &mut App) {
6135 let Some(id) = self.database_id() else {
6136 return;
6137 };
6138 if !self.project.read(cx).is_local() {
6139 return;
6140 }
6141 if let Some(manager) = HistoryManager::global(cx) {
6142 let paths = PathList::new(&self.root_paths(cx));
6143 manager.update(cx, |this, cx| {
6144 this.update_history(id, HistoryManagerEntry::new(id, &paths), cx);
6145 });
6146 }
6147 }
6148
6149 async fn serialize_items(
6150 this: &WeakEntity<Self>,
6151 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
6152 cx: &mut AsyncWindowContext,
6153 ) -> Result<()> {
6154 const CHUNK_SIZE: usize = 200;
6155
6156 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
6157
6158 while let Some(items_received) = serializable_items.next().await {
6159 let unique_items =
6160 items_received
6161 .into_iter()
6162 .fold(HashMap::default(), |mut acc, item| {
6163 acc.entry(item.item_id()).or_insert(item);
6164 acc
6165 });
6166
6167 // We use into_iter() here so that the references to the items are moved into
6168 // the tasks and not kept alive while we're sleeping.
6169 for (_, item) in unique_items.into_iter() {
6170 if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
6171 item.serialize(workspace, false, window, cx)
6172 }) {
6173 cx.background_spawn(async move { task.await.log_err() })
6174 .detach();
6175 }
6176 }
6177
6178 cx.background_executor()
6179 .timer(SERIALIZATION_THROTTLE_TIME)
6180 .await;
6181 }
6182
6183 Ok(())
6184 }
6185
6186 pub(crate) fn enqueue_item_serialization(
6187 &mut self,
6188 item: Box<dyn SerializableItemHandle>,
6189 ) -> Result<()> {
6190 self.serializable_items_tx
6191 .unbounded_send(item)
6192 .map_err(|err| anyhow!("failed to send serializable item over channel: {err}"))
6193 }
6194
6195 pub(crate) fn load_workspace(
6196 serialized_workspace: SerializedWorkspace,
6197 paths_to_open: Vec<Option<ProjectPath>>,
6198 window: &mut Window,
6199 cx: &mut Context<Workspace>,
6200 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
6201 cx.spawn_in(window, async move |workspace, cx| {
6202 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
6203
6204 let mut center_group = None;
6205 let mut center_items = None;
6206
6207 // Traverse the splits tree and add to things
6208 if let Some((group, active_pane, items)) = serialized_workspace
6209 .center_group
6210 .deserialize(&project, serialized_workspace.id, workspace.clone(), cx)
6211 .await
6212 {
6213 center_items = Some(items);
6214 center_group = Some((group, active_pane))
6215 }
6216
6217 let mut items_by_project_path = HashMap::default();
6218 let mut item_ids_by_kind = HashMap::default();
6219 let mut all_deserialized_items = Vec::default();
6220 cx.update(|_, cx| {
6221 for item in center_items.unwrap_or_default().into_iter().flatten() {
6222 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
6223 item_ids_by_kind
6224 .entry(serializable_item_handle.serialized_item_kind())
6225 .or_insert(Vec::new())
6226 .push(item.item_id().as_u64() as ItemId);
6227 }
6228
6229 if let Some(project_path) = item.project_path(cx) {
6230 items_by_project_path.insert(project_path, item.clone());
6231 }
6232 all_deserialized_items.push(item);
6233 }
6234 })?;
6235
6236 let opened_items = paths_to_open
6237 .into_iter()
6238 .map(|path_to_open| {
6239 path_to_open
6240 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
6241 })
6242 .collect::<Vec<_>>();
6243
6244 // Remove old panes from workspace panes list
6245 workspace.update_in(cx, |workspace, window, cx| {
6246 if let Some((center_group, active_pane)) = center_group {
6247 workspace.remove_panes(workspace.center.root.clone(), window, cx);
6248
6249 // Swap workspace center group
6250 workspace.center = PaneGroup::with_root(center_group);
6251 workspace.center.set_is_center(true);
6252 workspace.center.mark_positions(cx);
6253
6254 if let Some(active_pane) = active_pane {
6255 workspace.set_active_pane(&active_pane, window, cx);
6256 cx.focus_self(window);
6257 } else {
6258 workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
6259 }
6260 }
6261
6262 let docks = serialized_workspace.docks;
6263
6264 for (dock, serialized_dock) in [
6265 (&mut workspace.right_dock, docks.right),
6266 (&mut workspace.left_dock, docks.left),
6267 (&mut workspace.bottom_dock, docks.bottom),
6268 ]
6269 .iter_mut()
6270 {
6271 dock.update(cx, |dock, cx| {
6272 dock.serialized_dock = Some(serialized_dock.clone());
6273 dock.restore_state(window, cx);
6274 });
6275 }
6276
6277 cx.notify();
6278 })?;
6279
6280 let _ = project
6281 .update(cx, |project, cx| {
6282 project
6283 .breakpoint_store()
6284 .update(cx, |breakpoint_store, cx| {
6285 breakpoint_store
6286 .with_serialized_breakpoints(serialized_workspace.breakpoints, cx)
6287 })
6288 })
6289 .await;
6290
6291 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
6292 // after loading the items, we might have different items and in order to avoid
6293 // the database filling up, we delete items that haven't been loaded now.
6294 //
6295 // The items that have been loaded, have been saved after they've been added to the workspace.
6296 let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
6297 item_ids_by_kind
6298 .into_iter()
6299 .map(|(item_kind, loaded_items)| {
6300 SerializableItemRegistry::cleanup(
6301 item_kind,
6302 serialized_workspace.id,
6303 loaded_items,
6304 window,
6305 cx,
6306 )
6307 .log_err()
6308 })
6309 .collect::<Vec<_>>()
6310 })?;
6311
6312 futures::future::join_all(clean_up_tasks).await;
6313
6314 workspace
6315 .update_in(cx, |workspace, window, cx| {
6316 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
6317 workspace.serialize_workspace_internal(window, cx).detach();
6318
6319 // Ensure that we mark the window as edited if we did load dirty items
6320 workspace.update_window_edited(window, cx);
6321 })
6322 .ok();
6323
6324 Ok(opened_items)
6325 })
6326 }
6327
6328 fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
6329 self.add_workspace_actions_listeners(div, window, cx)
6330 .on_action(cx.listener(
6331 |_workspace, action_sequence: &settings::ActionSequence, window, cx| {
6332 for action in &action_sequence.0 {
6333 window.dispatch_action(action.boxed_clone(), cx);
6334 }
6335 },
6336 ))
6337 .on_action(cx.listener(Self::close_inactive_items_and_panes))
6338 .on_action(cx.listener(Self::close_all_items_and_panes))
6339 .on_action(cx.listener(Self::close_item_in_all_panes))
6340 .on_action(cx.listener(Self::save_all))
6341 .on_action(cx.listener(Self::send_keystrokes))
6342 .on_action(cx.listener(Self::add_folder_to_project))
6343 .on_action(cx.listener(Self::follow_next_collaborator))
6344 .on_action(cx.listener(Self::close_window))
6345 .on_action(cx.listener(Self::activate_pane_at_index))
6346 .on_action(cx.listener(Self::move_item_to_pane_at_index))
6347 .on_action(cx.listener(Self::move_focused_panel_to_next_position))
6348 .on_action(cx.listener(Self::toggle_edit_predictions_all_files))
6349 .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
6350 let pane = workspace.active_pane().clone();
6351 workspace.unfollow_in_pane(&pane, window, cx);
6352 }))
6353 .on_action(cx.listener(|workspace, action: &Save, window, cx| {
6354 workspace
6355 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
6356 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6357 }))
6358 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
6359 workspace
6360 .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
6361 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6362 }))
6363 .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
6364 workspace
6365 .save_active_item(SaveIntent::SaveAs, window, cx)
6366 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6367 }))
6368 .on_action(
6369 cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
6370 workspace.activate_previous_pane(window, cx)
6371 }),
6372 )
6373 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
6374 workspace.activate_next_pane(window, cx)
6375 }))
6376 .on_action(
6377 cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
6378 workspace.activate_next_window(cx)
6379 }),
6380 )
6381 .on_action(
6382 cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
6383 workspace.activate_previous_window(cx)
6384 }),
6385 )
6386 .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
6387 workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
6388 }))
6389 .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
6390 workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
6391 }))
6392 .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
6393 workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
6394 }))
6395 .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
6396 workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
6397 }))
6398 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
6399 workspace.activate_next_pane(window, cx)
6400 }))
6401 .on_action(cx.listener(
6402 |workspace, action: &MoveItemToPaneInDirection, window, cx| {
6403 workspace.move_item_to_pane_in_direction(action, window, cx)
6404 },
6405 ))
6406 .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
6407 workspace.swap_pane_in_direction(SplitDirection::Left, cx)
6408 }))
6409 .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
6410 workspace.swap_pane_in_direction(SplitDirection::Right, cx)
6411 }))
6412 .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
6413 workspace.swap_pane_in_direction(SplitDirection::Up, cx)
6414 }))
6415 .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
6416 workspace.swap_pane_in_direction(SplitDirection::Down, cx)
6417 }))
6418 .on_action(cx.listener(|workspace, _: &SwapPaneAdjacent, window, cx| {
6419 const DIRECTION_PRIORITY: [SplitDirection; 4] = [
6420 SplitDirection::Down,
6421 SplitDirection::Up,
6422 SplitDirection::Right,
6423 SplitDirection::Left,
6424 ];
6425 for dir in DIRECTION_PRIORITY {
6426 if workspace.find_pane_in_direction(dir, cx).is_some() {
6427 workspace.swap_pane_in_direction(dir, cx);
6428 workspace.activate_pane_in_direction(dir.opposite(), window, cx);
6429 break;
6430 }
6431 }
6432 }))
6433 .on_action(cx.listener(|workspace, _: &MovePaneLeft, _, cx| {
6434 workspace.move_pane_to_border(SplitDirection::Left, cx)
6435 }))
6436 .on_action(cx.listener(|workspace, _: &MovePaneRight, _, cx| {
6437 workspace.move_pane_to_border(SplitDirection::Right, cx)
6438 }))
6439 .on_action(cx.listener(|workspace, _: &MovePaneUp, _, cx| {
6440 workspace.move_pane_to_border(SplitDirection::Up, cx)
6441 }))
6442 .on_action(cx.listener(|workspace, _: &MovePaneDown, _, cx| {
6443 workspace.move_pane_to_border(SplitDirection::Down, cx)
6444 }))
6445 .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
6446 this.toggle_dock(DockPosition::Left, window, cx);
6447 }))
6448 .on_action(cx.listener(
6449 |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
6450 workspace.toggle_dock(DockPosition::Right, window, cx);
6451 },
6452 ))
6453 .on_action(cx.listener(
6454 |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
6455 workspace.toggle_dock(DockPosition::Bottom, window, cx);
6456 },
6457 ))
6458 .on_action(cx.listener(
6459 |workspace: &mut Workspace, _: &CloseActiveDock, window, cx| {
6460 if !workspace.close_active_dock(window, cx) {
6461 cx.propagate();
6462 }
6463 },
6464 ))
6465 .on_action(
6466 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
6467 workspace.close_all_docks(window, cx);
6468 }),
6469 )
6470 .on_action(cx.listener(Self::toggle_all_docks))
6471 .on_action(cx.listener(
6472 |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
6473 workspace.clear_all_notifications(cx);
6474 },
6475 ))
6476 .on_action(cx.listener(
6477 |workspace: &mut Workspace, _: &ClearNavigationHistory, window, cx| {
6478 workspace.clear_navigation_history(window, cx);
6479 },
6480 ))
6481 .on_action(cx.listener(
6482 |workspace: &mut Workspace, _: &SuppressNotification, _, cx| {
6483 if let Some((notification_id, _)) = workspace.notifications.pop() {
6484 workspace.suppress_notification(¬ification_id, cx);
6485 }
6486 },
6487 ))
6488 .on_action(cx.listener(
6489 |workspace: &mut Workspace, _: &ToggleWorktreeSecurity, window, cx| {
6490 workspace.show_worktree_trust_security_modal(true, window, cx);
6491 },
6492 ))
6493 .on_action(
6494 cx.listener(|_: &mut Workspace, _: &ClearTrustedWorktrees, _, cx| {
6495 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
6496 trusted_worktrees.update(cx, |trusted_worktrees, _| {
6497 trusted_worktrees.clear_trusted_paths()
6498 });
6499 let clear_task = persistence::DB.clear_trusted_worktrees();
6500 cx.spawn(async move |_, cx| {
6501 if clear_task.await.log_err().is_some() {
6502 cx.update(|cx| reload(cx));
6503 }
6504 })
6505 .detach();
6506 }
6507 }),
6508 )
6509 .on_action(cx.listener(
6510 |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
6511 workspace.reopen_closed_item(window, cx).detach();
6512 },
6513 ))
6514 .on_action(cx.listener(
6515 |workspace: &mut Workspace, _: &ResetActiveDockSize, window, cx| {
6516 for dock in workspace.all_docks() {
6517 if dock.focus_handle(cx).contains_focused(window, cx) {
6518 let Some(panel) = dock.read(cx).active_panel() else {
6519 return;
6520 };
6521
6522 // Set to `None`, then the size will fall back to the default.
6523 panel.clone().set_size(None, window, cx);
6524
6525 return;
6526 }
6527 }
6528 },
6529 ))
6530 .on_action(cx.listener(
6531 |workspace: &mut Workspace, _: &ResetOpenDocksSize, window, cx| {
6532 for dock in workspace.all_docks() {
6533 if let Some(panel) = dock.read(cx).visible_panel() {
6534 // Set to `None`, then the size will fall back to the default.
6535 panel.clone().set_size(None, window, cx);
6536 }
6537 }
6538 },
6539 ))
6540 .on_action(cx.listener(
6541 |workspace: &mut Workspace, act: &IncreaseActiveDockSize, window, cx| {
6542 adjust_active_dock_size_by_px(
6543 px_with_ui_font_fallback(act.px, cx),
6544 workspace,
6545 window,
6546 cx,
6547 );
6548 },
6549 ))
6550 .on_action(cx.listener(
6551 |workspace: &mut Workspace, act: &DecreaseActiveDockSize, window, cx| {
6552 adjust_active_dock_size_by_px(
6553 px_with_ui_font_fallback(act.px, cx) * -1.,
6554 workspace,
6555 window,
6556 cx,
6557 );
6558 },
6559 ))
6560 .on_action(cx.listener(
6561 |workspace: &mut Workspace, act: &IncreaseOpenDocksSize, window, cx| {
6562 adjust_open_docks_size_by_px(
6563 px_with_ui_font_fallback(act.px, cx),
6564 workspace,
6565 window,
6566 cx,
6567 );
6568 },
6569 ))
6570 .on_action(cx.listener(
6571 |workspace: &mut Workspace, act: &DecreaseOpenDocksSize, window, cx| {
6572 adjust_open_docks_size_by_px(
6573 px_with_ui_font_fallback(act.px, cx) * -1.,
6574 workspace,
6575 window,
6576 cx,
6577 );
6578 },
6579 ))
6580 .on_action(cx.listener(Workspace::toggle_centered_layout))
6581 .on_action(cx.listener(
6582 |workspace: &mut Workspace, _action: &pane::ActivateNextItem, window, cx| {
6583 if let Some(active_dock) = workspace.active_dock(window, cx) {
6584 let dock = active_dock.read(cx);
6585 if let Some(active_panel) = dock.active_panel() {
6586 if active_panel.pane(cx).is_none() {
6587 let mut recent_pane: Option<Entity<Pane>> = None;
6588 let mut recent_timestamp = 0;
6589 for pane_handle in workspace.panes() {
6590 let pane = pane_handle.read(cx);
6591 for entry in pane.activation_history() {
6592 if entry.timestamp > recent_timestamp {
6593 recent_timestamp = entry.timestamp;
6594 recent_pane = Some(pane_handle.clone());
6595 }
6596 }
6597 }
6598
6599 if let Some(pane) = recent_pane {
6600 pane.update(cx, |pane, cx| {
6601 let current_index = pane.active_item_index();
6602 let items_len = pane.items_len();
6603 if items_len > 0 {
6604 let next_index = if current_index + 1 < items_len {
6605 current_index + 1
6606 } else {
6607 0
6608 };
6609 pane.activate_item(
6610 next_index, false, false, window, cx,
6611 );
6612 }
6613 });
6614 return;
6615 }
6616 }
6617 }
6618 }
6619 cx.propagate();
6620 },
6621 ))
6622 .on_action(cx.listener(
6623 |workspace: &mut Workspace, _action: &pane::ActivatePreviousItem, window, cx| {
6624 if let Some(active_dock) = workspace.active_dock(window, cx) {
6625 let dock = active_dock.read(cx);
6626 if let Some(active_panel) = dock.active_panel() {
6627 if active_panel.pane(cx).is_none() {
6628 let mut recent_pane: Option<Entity<Pane>> = None;
6629 let mut recent_timestamp = 0;
6630 for pane_handle in workspace.panes() {
6631 let pane = pane_handle.read(cx);
6632 for entry in pane.activation_history() {
6633 if entry.timestamp > recent_timestamp {
6634 recent_timestamp = entry.timestamp;
6635 recent_pane = Some(pane_handle.clone());
6636 }
6637 }
6638 }
6639
6640 if let Some(pane) = recent_pane {
6641 pane.update(cx, |pane, cx| {
6642 let current_index = pane.active_item_index();
6643 let items_len = pane.items_len();
6644 if items_len > 0 {
6645 let prev_index = if current_index > 0 {
6646 current_index - 1
6647 } else {
6648 items_len.saturating_sub(1)
6649 };
6650 pane.activate_item(
6651 prev_index, false, false, window, cx,
6652 );
6653 }
6654 });
6655 return;
6656 }
6657 }
6658 }
6659 }
6660 cx.propagate();
6661 },
6662 ))
6663 .on_action(cx.listener(
6664 |workspace: &mut Workspace, action: &pane::CloseActiveItem, window, cx| {
6665 if let Some(active_dock) = workspace.active_dock(window, cx) {
6666 let dock = active_dock.read(cx);
6667 if let Some(active_panel) = dock.active_panel() {
6668 if active_panel.pane(cx).is_none() {
6669 let active_pane = workspace.active_pane().clone();
6670 active_pane.update(cx, |pane, cx| {
6671 pane.close_active_item(action, window, cx)
6672 .detach_and_log_err(cx);
6673 });
6674 return;
6675 }
6676 }
6677 }
6678 cx.propagate();
6679 },
6680 ))
6681 .on_action(
6682 cx.listener(|workspace, _: &ToggleReadOnlyFile, window, cx| {
6683 let pane = workspace.active_pane().clone();
6684 if let Some(item) = pane.read(cx).active_item() {
6685 item.toggle_read_only(window, cx);
6686 }
6687 }),
6688 )
6689 .on_action(cx.listener(Workspace::cancel))
6690 }
6691
6692 #[cfg(any(test, feature = "test-support"))]
6693 pub fn set_random_database_id(&mut self) {
6694 self.database_id = Some(WorkspaceId(Uuid::new_v4().as_u64_pair().0 as i64));
6695 }
6696
6697 #[cfg(any(test, feature = "test-support"))]
6698 pub(crate) fn test_new(
6699 project: Entity<Project>,
6700 window: &mut Window,
6701 cx: &mut Context<Self>,
6702 ) -> Self {
6703 use node_runtime::NodeRuntime;
6704 use session::Session;
6705
6706 let client = project.read(cx).client();
6707 let user_store = project.read(cx).user_store();
6708 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
6709 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
6710 window.activate_window();
6711 let app_state = Arc::new(AppState {
6712 languages: project.read(cx).languages().clone(),
6713 workspace_store,
6714 client,
6715 user_store,
6716 fs: project.read(cx).fs().clone(),
6717 build_window_options: |_, _| Default::default(),
6718 node_runtime: NodeRuntime::unavailable(),
6719 session,
6720 });
6721 let workspace = Self::new(Default::default(), project, app_state, window, cx);
6722 workspace
6723 .active_pane
6724 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
6725 workspace
6726 }
6727
6728 pub fn register_action<A: Action>(
6729 &mut self,
6730 callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
6731 ) -> &mut Self {
6732 let callback = Arc::new(callback);
6733
6734 self.workspace_actions.push(Box::new(move |div, _, _, cx| {
6735 let callback = callback.clone();
6736 div.on_action(cx.listener(move |workspace, event, window, cx| {
6737 (callback)(workspace, event, window, cx)
6738 }))
6739 }));
6740 self
6741 }
6742 pub fn register_action_renderer(
6743 &mut self,
6744 callback: impl Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div + 'static,
6745 ) -> &mut Self {
6746 self.workspace_actions.push(Box::new(callback));
6747 self
6748 }
6749
6750 fn add_workspace_actions_listeners(
6751 &self,
6752 mut div: Div,
6753 window: &mut Window,
6754 cx: &mut Context<Self>,
6755 ) -> Div {
6756 for action in self.workspace_actions.iter() {
6757 div = (action)(div, self, window, cx)
6758 }
6759 div
6760 }
6761
6762 pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
6763 self.modal_layer.read(cx).has_active_modal()
6764 }
6765
6766 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
6767 self.modal_layer.read(cx).active_modal()
6768 }
6769
6770 /// Toggles a modal of type `V`. If a modal of the same type is currently active,
6771 /// it will be hidden. If a different modal is active, it will be replaced with the new one.
6772 /// If no modal is active, the new modal will be shown.
6773 ///
6774 /// If closing the current modal fails (e.g., due to `on_before_dismiss` returning
6775 /// `DismissDecision::Dismiss(false)` or `DismissDecision::Pending`), the new modal
6776 /// will not be shown.
6777 pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
6778 where
6779 B: FnOnce(&mut Window, &mut Context<V>) -> V,
6780 {
6781 self.modal_layer.update(cx, |modal_layer, cx| {
6782 modal_layer.toggle_modal(window, cx, build)
6783 })
6784 }
6785
6786 pub fn hide_modal(&mut self, window: &mut Window, cx: &mut App) -> bool {
6787 self.modal_layer
6788 .update(cx, |modal_layer, cx| modal_layer.hide_modal(window, cx))
6789 }
6790
6791 pub fn toggle_status_toast<V: ToastView>(&mut self, entity: Entity<V>, cx: &mut App) {
6792 self.toast_layer
6793 .update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity))
6794 }
6795
6796 pub fn toggle_centered_layout(
6797 &mut self,
6798 _: &ToggleCenteredLayout,
6799 _: &mut Window,
6800 cx: &mut Context<Self>,
6801 ) {
6802 self.centered_layout = !self.centered_layout;
6803 if let Some(database_id) = self.database_id() {
6804 cx.background_spawn(DB.set_centered_layout(database_id, self.centered_layout))
6805 .detach_and_log_err(cx);
6806 }
6807 cx.notify();
6808 }
6809
6810 fn adjust_padding(padding: Option<f32>) -> f32 {
6811 padding
6812 .unwrap_or(CenteredPaddingSettings::default().0)
6813 .clamp(
6814 CenteredPaddingSettings::MIN_PADDING,
6815 CenteredPaddingSettings::MAX_PADDING,
6816 )
6817 }
6818
6819 fn render_dock(
6820 &self,
6821 position: DockPosition,
6822 dock: &Entity<Dock>,
6823 window: &mut Window,
6824 cx: &mut App,
6825 ) -> Option<Div> {
6826 if self.zoomed_position == Some(position) {
6827 return None;
6828 }
6829
6830 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
6831 let pane = panel.pane(cx)?;
6832 let follower_states = &self.follower_states;
6833 leader_border_for_pane(follower_states, &pane, window, cx)
6834 });
6835
6836 Some(
6837 div()
6838 .flex()
6839 .flex_none()
6840 .overflow_hidden()
6841 .child(dock.clone())
6842 .children(leader_border),
6843 )
6844 }
6845
6846 pub fn for_window(window: &Window, cx: &App) -> Option<Entity<Workspace>> {
6847 window
6848 .root::<MultiWorkspace>()
6849 .flatten()
6850 .map(|multi_workspace| multi_workspace.read(cx).workspace().clone())
6851 }
6852
6853 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
6854 self.zoomed.as_ref()
6855 }
6856
6857 pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
6858 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
6859 return;
6860 };
6861 let windows = cx.windows();
6862 let next_window =
6863 SystemWindowTabController::get_next_tab_group_window(cx, current_window_id).or_else(
6864 || {
6865 windows
6866 .iter()
6867 .cycle()
6868 .skip_while(|window| window.window_id() != current_window_id)
6869 .nth(1)
6870 },
6871 );
6872
6873 if let Some(window) = next_window {
6874 window
6875 .update(cx, |_, window, _| window.activate_window())
6876 .ok();
6877 }
6878 }
6879
6880 pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
6881 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
6882 return;
6883 };
6884 let windows = cx.windows();
6885 let prev_window =
6886 SystemWindowTabController::get_prev_tab_group_window(cx, current_window_id).or_else(
6887 || {
6888 windows
6889 .iter()
6890 .rev()
6891 .cycle()
6892 .skip_while(|window| window.window_id() != current_window_id)
6893 .nth(1)
6894 },
6895 );
6896
6897 if let Some(window) = prev_window {
6898 window
6899 .update(cx, |_, window, _| window.activate_window())
6900 .ok();
6901 }
6902 }
6903
6904 pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
6905 if cx.stop_active_drag(window) {
6906 } else if let Some((notification_id, _)) = self.notifications.pop() {
6907 dismiss_app_notification(¬ification_id, cx);
6908 } else {
6909 cx.propagate();
6910 }
6911 }
6912
6913 fn adjust_dock_size_by_px(
6914 &mut self,
6915 panel_size: Pixels,
6916 dock_pos: DockPosition,
6917 px: Pixels,
6918 window: &mut Window,
6919 cx: &mut Context<Self>,
6920 ) {
6921 match dock_pos {
6922 DockPosition::Left => self.resize_left_dock(panel_size + px, window, cx),
6923 DockPosition::Right => self.resize_right_dock(panel_size + px, window, cx),
6924 DockPosition::Bottom => self.resize_bottom_dock(panel_size + px, window, cx),
6925 }
6926 }
6927
6928 fn resize_left_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
6929 let size = new_size.min(self.bounds.right() - RESIZE_HANDLE_SIZE);
6930
6931 self.left_dock.update(cx, |left_dock, cx| {
6932 if WorkspaceSettings::get_global(cx)
6933 .resize_all_panels_in_dock
6934 .contains(&DockPosition::Left)
6935 {
6936 left_dock.resize_all_panels(Some(size), window, cx);
6937 } else {
6938 left_dock.resize_active_panel(Some(size), window, cx);
6939 }
6940 });
6941 }
6942
6943 fn resize_right_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
6944 let mut size = new_size.max(self.bounds.left() - RESIZE_HANDLE_SIZE);
6945 self.left_dock.read_with(cx, |left_dock, cx| {
6946 let left_dock_size = left_dock
6947 .active_panel_size(window, cx)
6948 .unwrap_or(Pixels::ZERO);
6949 if left_dock_size + size > self.bounds.right() {
6950 size = self.bounds.right() - left_dock_size
6951 }
6952 });
6953 self.right_dock.update(cx, |right_dock, cx| {
6954 if WorkspaceSettings::get_global(cx)
6955 .resize_all_panels_in_dock
6956 .contains(&DockPosition::Right)
6957 {
6958 right_dock.resize_all_panels(Some(size), window, cx);
6959 } else {
6960 right_dock.resize_active_panel(Some(size), window, cx);
6961 }
6962 });
6963 }
6964
6965 fn resize_bottom_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
6966 let size = new_size.min(self.bounds.bottom() - RESIZE_HANDLE_SIZE - self.bounds.top());
6967 self.bottom_dock.update(cx, |bottom_dock, cx| {
6968 if WorkspaceSettings::get_global(cx)
6969 .resize_all_panels_in_dock
6970 .contains(&DockPosition::Bottom)
6971 {
6972 bottom_dock.resize_all_panels(Some(size), window, cx);
6973 } else {
6974 bottom_dock.resize_active_panel(Some(size), window, cx);
6975 }
6976 });
6977 }
6978
6979 fn toggle_edit_predictions_all_files(
6980 &mut self,
6981 _: &ToggleEditPrediction,
6982 _window: &mut Window,
6983 cx: &mut Context<Self>,
6984 ) {
6985 let fs = self.project().read(cx).fs().clone();
6986 let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx);
6987 update_settings_file(fs, cx, move |file, _| {
6988 file.project.all_languages.defaults.show_edit_predictions = Some(!show_edit_predictions)
6989 });
6990 }
6991
6992 pub fn show_worktree_trust_security_modal(
6993 &mut self,
6994 toggle: bool,
6995 window: &mut Window,
6996 cx: &mut Context<Self>,
6997 ) {
6998 if let Some(security_modal) = self.active_modal::<SecurityModal>(cx) {
6999 if toggle {
7000 security_modal.update(cx, |security_modal, cx| {
7001 security_modal.dismiss(cx);
7002 })
7003 } else {
7004 security_modal.update(cx, |security_modal, cx| {
7005 security_modal.refresh_restricted_paths(cx);
7006 });
7007 }
7008 } else {
7009 let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
7010 .map(|trusted_worktrees| {
7011 trusted_worktrees
7012 .read(cx)
7013 .has_restricted_worktrees(&self.project().read(cx).worktree_store(), cx)
7014 })
7015 .unwrap_or(false);
7016 if has_restricted_worktrees {
7017 let project = self.project().read(cx);
7018 let remote_host = project
7019 .remote_connection_options(cx)
7020 .map(RemoteHostLocation::from);
7021 let worktree_store = project.worktree_store().downgrade();
7022 self.toggle_modal(window, cx, |_, cx| {
7023 SecurityModal::new(worktree_store, remote_host, cx)
7024 });
7025 }
7026 }
7027 }
7028}
7029
7030fn leader_border_for_pane(
7031 follower_states: &HashMap<CollaboratorId, FollowerState>,
7032 pane: &Entity<Pane>,
7033 _: &Window,
7034 cx: &App,
7035) -> Option<Div> {
7036 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
7037 if state.pane() == pane {
7038 Some((*leader_id, state))
7039 } else {
7040 None
7041 }
7042 })?;
7043
7044 let mut leader_color = match leader_id {
7045 CollaboratorId::PeerId(leader_peer_id) => {
7046 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
7047 let leader = room.remote_participant_for_peer_id(leader_peer_id)?;
7048
7049 cx.theme()
7050 .players()
7051 .color_for_participant(leader.participant_index.0)
7052 .cursor
7053 }
7054 CollaboratorId::Agent => cx.theme().players().agent().cursor,
7055 };
7056 leader_color.fade_out(0.3);
7057 Some(
7058 div()
7059 .absolute()
7060 .size_full()
7061 .left_0()
7062 .top_0()
7063 .border_2()
7064 .border_color(leader_color),
7065 )
7066}
7067
7068fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
7069 ZED_WINDOW_POSITION
7070 .zip(*ZED_WINDOW_SIZE)
7071 .map(|(position, size)| Bounds {
7072 origin: position,
7073 size,
7074 })
7075}
7076
7077fn open_items(
7078 serialized_workspace: Option<SerializedWorkspace>,
7079 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
7080 window: &mut Window,
7081 cx: &mut Context<Workspace>,
7082) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
7083 let restored_items = serialized_workspace.map(|serialized_workspace| {
7084 Workspace::load_workspace(
7085 serialized_workspace,
7086 project_paths_to_open
7087 .iter()
7088 .map(|(_, project_path)| project_path)
7089 .cloned()
7090 .collect(),
7091 window,
7092 cx,
7093 )
7094 });
7095
7096 cx.spawn_in(window, async move |workspace, cx| {
7097 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
7098
7099 if let Some(restored_items) = restored_items {
7100 let restored_items = restored_items.await?;
7101
7102 let restored_project_paths = restored_items
7103 .iter()
7104 .filter_map(|item| {
7105 cx.update(|_, cx| item.as_ref()?.project_path(cx))
7106 .ok()
7107 .flatten()
7108 })
7109 .collect::<HashSet<_>>();
7110
7111 for restored_item in restored_items {
7112 opened_items.push(restored_item.map(Ok));
7113 }
7114
7115 project_paths_to_open
7116 .iter_mut()
7117 .for_each(|(_, project_path)| {
7118 if let Some(project_path_to_open) = project_path
7119 && restored_project_paths.contains(project_path_to_open)
7120 {
7121 *project_path = None;
7122 }
7123 });
7124 } else {
7125 for _ in 0..project_paths_to_open.len() {
7126 opened_items.push(None);
7127 }
7128 }
7129 assert!(opened_items.len() == project_paths_to_open.len());
7130
7131 let tasks =
7132 project_paths_to_open
7133 .into_iter()
7134 .enumerate()
7135 .map(|(ix, (abs_path, project_path))| {
7136 let workspace = workspace.clone();
7137 cx.spawn(async move |cx| {
7138 let file_project_path = project_path?;
7139 let abs_path_task = workspace.update(cx, |workspace, cx| {
7140 workspace.project().update(cx, |project, cx| {
7141 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
7142 })
7143 });
7144
7145 // We only want to open file paths here. If one of the items
7146 // here is a directory, it was already opened further above
7147 // with a `find_or_create_worktree`.
7148 if let Ok(task) = abs_path_task
7149 && task.await.is_none_or(|p| p.is_file())
7150 {
7151 return Some((
7152 ix,
7153 workspace
7154 .update_in(cx, |workspace, window, cx| {
7155 workspace.open_path(
7156 file_project_path,
7157 None,
7158 true,
7159 window,
7160 cx,
7161 )
7162 })
7163 .log_err()?
7164 .await,
7165 ));
7166 }
7167 None
7168 })
7169 });
7170
7171 let tasks = tasks.collect::<Vec<_>>();
7172
7173 let tasks = futures::future::join_all(tasks);
7174 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
7175 opened_items[ix] = Some(path_open_result);
7176 }
7177
7178 Ok(opened_items)
7179 })
7180}
7181
7182enum ActivateInDirectionTarget {
7183 Pane(Entity<Pane>),
7184 Dock(Entity<Dock>),
7185}
7186
7187fn notify_if_database_failed(window: WindowHandle<MultiWorkspace>, cx: &mut AsyncApp) {
7188 window
7189 .update(cx, |multi_workspace, _, cx| {
7190 let workspace = multi_workspace.workspace().clone();
7191 workspace.update(cx, |workspace, cx| {
7192 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
7193 struct DatabaseFailedNotification;
7194
7195 workspace.show_notification(
7196 NotificationId::unique::<DatabaseFailedNotification>(),
7197 cx,
7198 |cx| {
7199 cx.new(|cx| {
7200 MessageNotification::new("Failed to load the database file.", cx)
7201 .primary_message("File an Issue")
7202 .primary_icon(IconName::Plus)
7203 .primary_on_click(|window, cx| {
7204 window.dispatch_action(Box::new(FileBugReport), cx)
7205 })
7206 })
7207 },
7208 );
7209 }
7210 });
7211 })
7212 .log_err();
7213}
7214
7215fn px_with_ui_font_fallback(val: u32, cx: &Context<Workspace>) -> Pixels {
7216 if val == 0 {
7217 ThemeSettings::get_global(cx).ui_font_size(cx)
7218 } else {
7219 px(val as f32)
7220 }
7221}
7222
7223fn adjust_active_dock_size_by_px(
7224 px: Pixels,
7225 workspace: &mut Workspace,
7226 window: &mut Window,
7227 cx: &mut Context<Workspace>,
7228) {
7229 let Some(active_dock) = workspace
7230 .all_docks()
7231 .into_iter()
7232 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx))
7233 else {
7234 return;
7235 };
7236 let dock = active_dock.read(cx);
7237 let Some(panel_size) = dock.active_panel_size(window, cx) else {
7238 return;
7239 };
7240 let dock_pos = dock.position();
7241 workspace.adjust_dock_size_by_px(panel_size, dock_pos, px, window, cx);
7242}
7243
7244fn adjust_open_docks_size_by_px(
7245 px: Pixels,
7246 workspace: &mut Workspace,
7247 window: &mut Window,
7248 cx: &mut Context<Workspace>,
7249) {
7250 let docks = workspace
7251 .all_docks()
7252 .into_iter()
7253 .filter_map(|dock| {
7254 if dock.read(cx).is_open() {
7255 let dock = dock.read(cx);
7256 let panel_size = dock.active_panel_size(window, cx)?;
7257 let dock_pos = dock.position();
7258 Some((panel_size, dock_pos, px))
7259 } else {
7260 None
7261 }
7262 })
7263 .collect::<Vec<_>>();
7264
7265 docks
7266 .into_iter()
7267 .for_each(|(panel_size, dock_pos, offset)| {
7268 workspace.adjust_dock_size_by_px(panel_size, dock_pos, offset, window, cx);
7269 });
7270}
7271
7272impl Focusable for Workspace {
7273 fn focus_handle(&self, cx: &App) -> FocusHandle {
7274 self.active_pane.focus_handle(cx)
7275 }
7276}
7277
7278#[derive(Clone)]
7279struct DraggedDock(DockPosition);
7280
7281impl Render for DraggedDock {
7282 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
7283 gpui::Empty
7284 }
7285}
7286
7287impl Render for Workspace {
7288 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
7289 static FIRST_PAINT: AtomicBool = AtomicBool::new(true);
7290 if FIRST_PAINT.swap(false, std::sync::atomic::Ordering::Relaxed) {
7291 log::info!("Rendered first frame");
7292 }
7293 let mut context = KeyContext::new_with_defaults();
7294 context.add("Workspace");
7295 context.set("keyboard_layout", cx.keyboard_layout().name().to_string());
7296 if let Some(status) = self
7297 .debugger_provider
7298 .as_ref()
7299 .and_then(|provider| provider.active_thread_state(cx))
7300 {
7301 match status {
7302 ThreadStatus::Running | ThreadStatus::Stepping => {
7303 context.add("debugger_running");
7304 }
7305 ThreadStatus::Stopped => context.add("debugger_stopped"),
7306 ThreadStatus::Exited | ThreadStatus::Ended => {}
7307 }
7308 }
7309
7310 if self.left_dock.read(cx).is_open() {
7311 if let Some(active_panel) = self.left_dock.read(cx).active_panel() {
7312 context.set("left_dock", active_panel.panel_key());
7313 }
7314 }
7315
7316 if self.right_dock.read(cx).is_open() {
7317 if let Some(active_panel) = self.right_dock.read(cx).active_panel() {
7318 context.set("right_dock", active_panel.panel_key());
7319 }
7320 }
7321
7322 if self.bottom_dock.read(cx).is_open() {
7323 if let Some(active_panel) = self.bottom_dock.read(cx).active_panel() {
7324 context.set("bottom_dock", active_panel.panel_key());
7325 }
7326 }
7327
7328 let centered_layout = self.centered_layout
7329 && self.center.panes().len() == 1
7330 && self.active_item(cx).is_some();
7331 let render_padding = |size| {
7332 (size > 0.0).then(|| {
7333 div()
7334 .h_full()
7335 .w(relative(size))
7336 .bg(cx.theme().colors().editor_background)
7337 .border_color(cx.theme().colors().pane_group_border)
7338 })
7339 };
7340 let paddings = if centered_layout {
7341 let settings = WorkspaceSettings::get_global(cx).centered_layout;
7342 (
7343 render_padding(Self::adjust_padding(
7344 settings.left_padding.map(|padding| padding.0),
7345 )),
7346 render_padding(Self::adjust_padding(
7347 settings.right_padding.map(|padding| padding.0),
7348 )),
7349 )
7350 } else {
7351 (None, None)
7352 };
7353 let ui_font = theme::setup_ui_font(window, cx);
7354
7355 let theme = cx.theme().clone();
7356 let colors = theme.colors();
7357 let notification_entities = self
7358 .notifications
7359 .iter()
7360 .map(|(_, notification)| notification.entity_id())
7361 .collect::<Vec<_>>();
7362 let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout;
7363
7364 self.actions(div(), window, cx)
7365 .key_context(context)
7366 .relative()
7367 .size_full()
7368 .flex()
7369 .flex_col()
7370 .font(ui_font)
7371 .gap_0()
7372 .justify_start()
7373 .items_start()
7374 .text_color(colors.text)
7375 .overflow_hidden()
7376 .children(self.titlebar_item.clone())
7377 .on_modifiers_changed(move |_, _, cx| {
7378 for &id in ¬ification_entities {
7379 cx.notify(id);
7380 }
7381 })
7382 .child(
7383 div()
7384 .size_full()
7385 .relative()
7386 .flex_1()
7387 .flex()
7388 .flex_col()
7389 .child(
7390 div()
7391 .id("workspace")
7392 .bg(colors.background)
7393 .relative()
7394 .flex_1()
7395 .w_full()
7396 .flex()
7397 .flex_col()
7398 .overflow_hidden()
7399 .border_t_1()
7400 .border_b_1()
7401 .border_color(colors.border)
7402 .child({
7403 let this = cx.entity();
7404 canvas(
7405 move |bounds, window, cx| {
7406 this.update(cx, |this, cx| {
7407 let bounds_changed = this.bounds != bounds;
7408 this.bounds = bounds;
7409
7410 if bounds_changed {
7411 this.left_dock.update(cx, |dock, cx| {
7412 dock.clamp_panel_size(
7413 bounds.size.width,
7414 window,
7415 cx,
7416 )
7417 });
7418
7419 this.right_dock.update(cx, |dock, cx| {
7420 dock.clamp_panel_size(
7421 bounds.size.width,
7422 window,
7423 cx,
7424 )
7425 });
7426
7427 this.bottom_dock.update(cx, |dock, cx| {
7428 dock.clamp_panel_size(
7429 bounds.size.height,
7430 window,
7431 cx,
7432 )
7433 });
7434 }
7435 })
7436 },
7437 |_, _, _, _| {},
7438 )
7439 .absolute()
7440 .size_full()
7441 })
7442 .when(self.zoomed.is_none(), |this| {
7443 this.on_drag_move(cx.listener(
7444 move |workspace,
7445 e: &DragMoveEvent<DraggedDock>,
7446 window,
7447 cx| {
7448 if workspace.previous_dock_drag_coordinates
7449 != Some(e.event.position)
7450 {
7451 workspace.previous_dock_drag_coordinates =
7452 Some(e.event.position);
7453 match e.drag(cx).0 {
7454 DockPosition::Left => {
7455 workspace.resize_left_dock(
7456 e.event.position.x
7457 - workspace.bounds.left(),
7458 window,
7459 cx,
7460 );
7461 }
7462 DockPosition::Right => {
7463 workspace.resize_right_dock(
7464 workspace.bounds.right()
7465 - e.event.position.x,
7466 window,
7467 cx,
7468 );
7469 }
7470 DockPosition::Bottom => {
7471 workspace.resize_bottom_dock(
7472 workspace.bounds.bottom()
7473 - e.event.position.y,
7474 window,
7475 cx,
7476 );
7477 }
7478 };
7479 workspace.serialize_workspace(window, cx);
7480 }
7481 },
7482 ))
7483
7484 })
7485 .child({
7486 match bottom_dock_layout {
7487 BottomDockLayout::Full => div()
7488 .flex()
7489 .flex_col()
7490 .h_full()
7491 .child(
7492 div()
7493 .flex()
7494 .flex_row()
7495 .flex_1()
7496 .overflow_hidden()
7497 .children(self.render_dock(
7498 DockPosition::Left,
7499 &self.left_dock,
7500 window,
7501 cx,
7502 ))
7503
7504 .child(
7505 div()
7506 .flex()
7507 .flex_col()
7508 .flex_1()
7509 .overflow_hidden()
7510 .child(
7511 h_flex()
7512 .flex_1()
7513 .when_some(
7514 paddings.0,
7515 |this, p| {
7516 this.child(
7517 p.border_r_1(),
7518 )
7519 },
7520 )
7521 .child(self.center.render(
7522 self.zoomed.as_ref(),
7523 &PaneRenderContext {
7524 follower_states:
7525 &self.follower_states,
7526 active_call: self.active_call(),
7527 active_pane: &self.active_pane,
7528 app_state: &self.app_state,
7529 project: &self.project,
7530 workspace: &self.weak_self,
7531 },
7532 window,
7533 cx,
7534 ))
7535 .when_some(
7536 paddings.1,
7537 |this, p| {
7538 this.child(
7539 p.border_l_1(),
7540 )
7541 },
7542 ),
7543 ),
7544 )
7545
7546 .children(self.render_dock(
7547 DockPosition::Right,
7548 &self.right_dock,
7549 window,
7550 cx,
7551 )),
7552 )
7553 .child(div().w_full().children(self.render_dock(
7554 DockPosition::Bottom,
7555 &self.bottom_dock,
7556 window,
7557 cx
7558 ))),
7559
7560 BottomDockLayout::LeftAligned => div()
7561 .flex()
7562 .flex_row()
7563 .h_full()
7564 .child(
7565 div()
7566 .flex()
7567 .flex_col()
7568 .flex_1()
7569 .h_full()
7570 .child(
7571 div()
7572 .flex()
7573 .flex_row()
7574 .flex_1()
7575 .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx))
7576
7577 .child(
7578 div()
7579 .flex()
7580 .flex_col()
7581 .flex_1()
7582 .overflow_hidden()
7583 .child(
7584 h_flex()
7585 .flex_1()
7586 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
7587 .child(self.center.render(
7588 self.zoomed.as_ref(),
7589 &PaneRenderContext {
7590 follower_states:
7591 &self.follower_states,
7592 active_call: self.active_call(),
7593 active_pane: &self.active_pane,
7594 app_state: &self.app_state,
7595 project: &self.project,
7596 workspace: &self.weak_self,
7597 },
7598 window,
7599 cx,
7600 ))
7601 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
7602 )
7603 )
7604
7605 )
7606 .child(
7607 div()
7608 .w_full()
7609 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
7610 ),
7611 )
7612 .children(self.render_dock(
7613 DockPosition::Right,
7614 &self.right_dock,
7615 window,
7616 cx,
7617 )),
7618
7619 BottomDockLayout::RightAligned => div()
7620 .flex()
7621 .flex_row()
7622 .h_full()
7623 .children(self.render_dock(
7624 DockPosition::Left,
7625 &self.left_dock,
7626 window,
7627 cx,
7628 ))
7629
7630 .child(
7631 div()
7632 .flex()
7633 .flex_col()
7634 .flex_1()
7635 .h_full()
7636 .child(
7637 div()
7638 .flex()
7639 .flex_row()
7640 .flex_1()
7641 .child(
7642 div()
7643 .flex()
7644 .flex_col()
7645 .flex_1()
7646 .overflow_hidden()
7647 .child(
7648 h_flex()
7649 .flex_1()
7650 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
7651 .child(self.center.render(
7652 self.zoomed.as_ref(),
7653 &PaneRenderContext {
7654 follower_states:
7655 &self.follower_states,
7656 active_call: self.active_call(),
7657 active_pane: &self.active_pane,
7658 app_state: &self.app_state,
7659 project: &self.project,
7660 workspace: &self.weak_self,
7661 },
7662 window,
7663 cx,
7664 ))
7665 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
7666 )
7667 )
7668
7669 .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx))
7670 )
7671 .child(
7672 div()
7673 .w_full()
7674 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
7675 ),
7676 ),
7677
7678 BottomDockLayout::Contained => div()
7679 .flex()
7680 .flex_row()
7681 .h_full()
7682 .children(self.render_dock(
7683 DockPosition::Left,
7684 &self.left_dock,
7685 window,
7686 cx,
7687 ))
7688
7689 .child(
7690 div()
7691 .flex()
7692 .flex_col()
7693 .flex_1()
7694 .overflow_hidden()
7695 .child(
7696 h_flex()
7697 .flex_1()
7698 .when_some(paddings.0, |this, p| {
7699 this.child(p.border_r_1())
7700 })
7701 .child(self.center.render(
7702 self.zoomed.as_ref(),
7703 &PaneRenderContext {
7704 follower_states:
7705 &self.follower_states,
7706 active_call: self.active_call(),
7707 active_pane: &self.active_pane,
7708 app_state: &self.app_state,
7709 project: &self.project,
7710 workspace: &self.weak_self,
7711 },
7712 window,
7713 cx,
7714 ))
7715 .when_some(paddings.1, |this, p| {
7716 this.child(p.border_l_1())
7717 }),
7718 )
7719 .children(self.render_dock(
7720 DockPosition::Bottom,
7721 &self.bottom_dock,
7722 window,
7723 cx,
7724 )),
7725 )
7726
7727 .children(self.render_dock(
7728 DockPosition::Right,
7729 &self.right_dock,
7730 window,
7731 cx,
7732 )),
7733 }
7734 })
7735 .children(self.zoomed.as_ref().and_then(|view| {
7736 let zoomed_view = view.upgrade()?;
7737 let div = div()
7738 .occlude()
7739 .absolute()
7740 .overflow_hidden()
7741 .border_color(colors.border)
7742 .bg(colors.background)
7743 .child(zoomed_view)
7744 .inset_0()
7745 .shadow_lg();
7746
7747 if !WorkspaceSettings::get_global(cx).zoomed_padding {
7748 return Some(div);
7749 }
7750
7751 Some(match self.zoomed_position {
7752 Some(DockPosition::Left) => div.right_2().border_r_1(),
7753 Some(DockPosition::Right) => div.left_2().border_l_1(),
7754 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
7755 None => {
7756 div.top_2().bottom_2().left_2().right_2().border_1()
7757 }
7758 })
7759 }))
7760 .children(self.render_notifications(window, cx)),
7761 )
7762 .when(self.status_bar_visible(cx), |parent| {
7763 parent.child(self.status_bar.clone())
7764 })
7765 .child(self.modal_layer.clone())
7766 .child(self.toast_layer.clone()),
7767 )
7768 }
7769}
7770
7771impl WorkspaceStore {
7772 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
7773 Self {
7774 workspaces: Default::default(),
7775 _subscriptions: vec![
7776 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
7777 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
7778 ],
7779 client,
7780 }
7781 }
7782
7783 pub fn update_followers(
7784 &self,
7785 project_id: Option<u64>,
7786 update: proto::update_followers::Variant,
7787 cx: &App,
7788 ) -> Option<()> {
7789 let active_call = ActiveCall::try_global(cx)?;
7790 let room_id = active_call.read(cx).room()?.read(cx).id();
7791 self.client
7792 .send(proto::UpdateFollowers {
7793 room_id,
7794 project_id,
7795 variant: Some(update),
7796 })
7797 .log_err()
7798 }
7799
7800 pub async fn handle_follow(
7801 this: Entity<Self>,
7802 envelope: TypedEnvelope<proto::Follow>,
7803 mut cx: AsyncApp,
7804 ) -> Result<proto::FollowResponse> {
7805 this.update(&mut cx, |this, cx| {
7806 let follower = Follower {
7807 project_id: envelope.payload.project_id,
7808 peer_id: envelope.original_sender_id()?,
7809 };
7810
7811 let mut response = proto::FollowResponse::default();
7812
7813 this.workspaces.retain(|(window_handle, weak_workspace)| {
7814 let Some(workspace) = weak_workspace.upgrade() else {
7815 return false;
7816 };
7817 window_handle
7818 .update(cx, |_, window, cx| {
7819 workspace.update(cx, |workspace, cx| {
7820 let handler_response =
7821 workspace.handle_follow(follower.project_id, window, cx);
7822 if let Some(active_view) = handler_response.active_view
7823 && workspace.project.read(cx).remote_id() == follower.project_id
7824 {
7825 response.active_view = Some(active_view)
7826 }
7827 });
7828 })
7829 .is_ok()
7830 });
7831
7832 Ok(response)
7833 })
7834 }
7835
7836 async fn handle_update_followers(
7837 this: Entity<Self>,
7838 envelope: TypedEnvelope<proto::UpdateFollowers>,
7839 mut cx: AsyncApp,
7840 ) -> Result<()> {
7841 let leader_id = envelope.original_sender_id()?;
7842 let update = envelope.payload;
7843
7844 this.update(&mut cx, |this, cx| {
7845 this.workspaces.retain(|(window_handle, weak_workspace)| {
7846 let Some(workspace) = weak_workspace.upgrade() else {
7847 return false;
7848 };
7849 window_handle
7850 .update(cx, |_, window, cx| {
7851 workspace.update(cx, |workspace, cx| {
7852 let project_id = workspace.project.read(cx).remote_id();
7853 if update.project_id != project_id && update.project_id.is_some() {
7854 return;
7855 }
7856 workspace.handle_update_followers(
7857 leader_id,
7858 update.clone(),
7859 window,
7860 cx,
7861 );
7862 });
7863 })
7864 .is_ok()
7865 });
7866 Ok(())
7867 })
7868 }
7869
7870 pub fn workspaces(&self) -> impl Iterator<Item = &WeakEntity<Workspace>> {
7871 self.workspaces.iter().map(|(_, weak)| weak)
7872 }
7873
7874 pub fn workspaces_with_windows(
7875 &self,
7876 ) -> impl Iterator<Item = (gpui::AnyWindowHandle, &WeakEntity<Workspace>)> {
7877 self.workspaces.iter().map(|(window, weak)| (*window, weak))
7878 }
7879}
7880
7881impl ViewId {
7882 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
7883 Ok(Self {
7884 creator: message
7885 .creator
7886 .map(CollaboratorId::PeerId)
7887 .context("creator is missing")?,
7888 id: message.id,
7889 })
7890 }
7891
7892 pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
7893 if let CollaboratorId::PeerId(peer_id) = self.creator {
7894 Some(proto::ViewId {
7895 creator: Some(peer_id),
7896 id: self.id,
7897 })
7898 } else {
7899 None
7900 }
7901 }
7902}
7903
7904impl FollowerState {
7905 fn pane(&self) -> &Entity<Pane> {
7906 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
7907 }
7908}
7909
7910pub trait WorkspaceHandle {
7911 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
7912}
7913
7914impl WorkspaceHandle for Entity<Workspace> {
7915 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
7916 self.read(cx)
7917 .worktrees(cx)
7918 .flat_map(|worktree| {
7919 let worktree_id = worktree.read(cx).id();
7920 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
7921 worktree_id,
7922 path: f.path.clone(),
7923 })
7924 })
7925 .collect::<Vec<_>>()
7926 }
7927}
7928
7929pub async fn last_opened_workspace_location(
7930 fs: &dyn fs::Fs,
7931) -> Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)> {
7932 DB.last_workspace(fs)
7933 .await
7934 .log_err()
7935 .flatten()
7936 .map(|(id, location, paths, _timestamp)| (id, location, paths))
7937}
7938
7939pub async fn last_session_workspace_locations(
7940 last_session_id: &str,
7941 last_session_window_stack: Option<Vec<WindowId>>,
7942 fs: &dyn fs::Fs,
7943) -> Option<Vec<SessionWorkspace>> {
7944 DB.last_session_workspace_locations(last_session_id, last_session_window_stack, fs)
7945 .await
7946 .log_err()
7947}
7948
7949pub struct MultiWorkspaceRestoreResult {
7950 pub window_handle: WindowHandle<MultiWorkspace>,
7951 pub errors: Vec<anyhow::Error>,
7952}
7953
7954pub async fn restore_multiworkspace(
7955 multi_workspace: SerializedMultiWorkspace,
7956 app_state: Arc<AppState>,
7957 cx: &mut AsyncApp,
7958) -> anyhow::Result<MultiWorkspaceRestoreResult> {
7959 let SerializedMultiWorkspace { workspaces, state } = multi_workspace;
7960 let mut group_iter = workspaces.into_iter();
7961 let first = group_iter
7962 .next()
7963 .context("window group must not be empty")?;
7964
7965 let window_handle = if first.paths.is_empty() {
7966 cx.update(|cx| open_workspace_by_id(first.workspace_id, app_state.clone(), None, cx))
7967 .await?
7968 } else {
7969 let (window, _items) = cx
7970 .update(|cx| {
7971 Workspace::new_local(
7972 first.paths.paths().to_vec(),
7973 app_state.clone(),
7974 None,
7975 None,
7976 None,
7977 cx,
7978 )
7979 })
7980 .await?;
7981 window
7982 };
7983
7984 let mut errors = Vec::new();
7985
7986 for session_workspace in group_iter {
7987 let error = if session_workspace.paths.is_empty() {
7988 cx.update(|cx| {
7989 open_workspace_by_id(
7990 session_workspace.workspace_id,
7991 app_state.clone(),
7992 Some(window_handle),
7993 cx,
7994 )
7995 })
7996 .await
7997 .err()
7998 } else {
7999 cx.update(|cx| {
8000 Workspace::new_local(
8001 session_workspace.paths.paths().to_vec(),
8002 app_state.clone(),
8003 Some(window_handle),
8004 None,
8005 None,
8006 cx,
8007 )
8008 })
8009 .await
8010 .err()
8011 };
8012
8013 if let Some(error) = error {
8014 errors.push(error);
8015 }
8016 }
8017
8018 if let Some(target_id) = state.active_workspace_id {
8019 window_handle
8020 .update(cx, |multi_workspace, window, cx| {
8021 let target_index = multi_workspace
8022 .workspaces()
8023 .iter()
8024 .position(|ws| ws.read(cx).database_id() == Some(target_id));
8025 if let Some(index) = target_index {
8026 multi_workspace.activate_index(index, window, cx);
8027 } else if !multi_workspace.workspaces().is_empty() {
8028 multi_workspace.activate_index(0, window, cx);
8029 }
8030 })
8031 .ok();
8032 } else {
8033 window_handle
8034 .update(cx, |multi_workspace, window, cx| {
8035 if !multi_workspace.workspaces().is_empty() {
8036 multi_workspace.activate_index(0, window, cx);
8037 }
8038 })
8039 .ok();
8040 }
8041
8042 if state.sidebar_open {
8043 window_handle
8044 .update(cx, |multi_workspace, _, cx| {
8045 multi_workspace.open_sidebar(cx);
8046 })
8047 .ok();
8048 }
8049
8050 window_handle
8051 .update(cx, |_, window, _cx| {
8052 window.activate_window();
8053 })
8054 .ok();
8055
8056 Ok(MultiWorkspaceRestoreResult {
8057 window_handle,
8058 errors,
8059 })
8060}
8061
8062actions!(
8063 collab,
8064 [
8065 /// Opens the channel notes for the current call.
8066 ///
8067 /// Use `collab_panel::OpenSelectedChannelNotes` to open the channel notes for the selected
8068 /// channel in the collab panel.
8069 ///
8070 /// If you want to open a specific channel, use `zed::OpenZedUrl` with a channel notes URL -
8071 /// can be copied via "Copy link to section" in the context menu of the channel notes
8072 /// buffer. These URLs look like `https://zed.dev/channel/channel-name-CHANNEL_ID/notes`.
8073 OpenChannelNotes,
8074 /// Mutes your microphone.
8075 Mute,
8076 /// Deafens yourself (mute both microphone and speakers).
8077 Deafen,
8078 /// Leaves the current call.
8079 LeaveCall,
8080 /// Shares the current project with collaborators.
8081 ShareProject,
8082 /// Shares your screen with collaborators.
8083 ScreenShare,
8084 /// Copies the current room name and session id for debugging purposes.
8085 CopyRoomId,
8086 ]
8087);
8088actions!(
8089 zed,
8090 [
8091 /// Opens the Zed log file.
8092 OpenLog,
8093 /// Reveals the Zed log file in the system file manager.
8094 RevealLogInFileManager
8095 ]
8096);
8097
8098async fn join_channel_internal(
8099 channel_id: ChannelId,
8100 app_state: &Arc<AppState>,
8101 requesting_window: Option<WindowHandle<MultiWorkspace>>,
8102 requesting_workspace: Option<WeakEntity<Workspace>>,
8103 active_call: &Entity<ActiveCall>,
8104 cx: &mut AsyncApp,
8105) -> Result<bool> {
8106 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
8107 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
8108 return (false, None);
8109 };
8110
8111 let already_in_channel = room.channel_id() == Some(channel_id);
8112 let should_prompt = room.is_sharing_project()
8113 && !room.remote_participants().is_empty()
8114 && !already_in_channel;
8115 let open_room = if already_in_channel {
8116 active_call.room().cloned()
8117 } else {
8118 None
8119 };
8120 (should_prompt, open_room)
8121 });
8122
8123 if let Some(room) = open_room {
8124 let task = room.update(cx, |room, cx| {
8125 if let Some((project, host)) = room.most_active_project(cx) {
8126 return Some(join_in_room_project(project, host, app_state.clone(), cx));
8127 }
8128
8129 None
8130 });
8131 if let Some(task) = task {
8132 task.await?;
8133 }
8134 return anyhow::Ok(true);
8135 }
8136
8137 if should_prompt {
8138 if let Some(multi_workspace) = requesting_window {
8139 let answer = multi_workspace
8140 .update(cx, |_, window, cx| {
8141 window.prompt(
8142 PromptLevel::Warning,
8143 "Do you want to switch channels?",
8144 Some("Leaving this call will unshare your current project."),
8145 &["Yes, Join Channel", "Cancel"],
8146 cx,
8147 )
8148 })?
8149 .await;
8150
8151 if answer == Ok(1) {
8152 return Ok(false);
8153 }
8154 } else {
8155 return Ok(false); // unreachable!() hopefully
8156 }
8157 }
8158
8159 let client = cx.update(|cx| active_call.read(cx).client());
8160
8161 let mut client_status = client.status();
8162
8163 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
8164 'outer: loop {
8165 let Some(status) = client_status.recv().await else {
8166 anyhow::bail!("error connecting");
8167 };
8168
8169 match status {
8170 Status::Connecting
8171 | Status::Authenticating
8172 | Status::Authenticated
8173 | Status::Reconnecting
8174 | Status::Reauthenticating
8175 | Status::Reauthenticated => continue,
8176 Status::Connected { .. } => break 'outer,
8177 Status::SignedOut | Status::AuthenticationError => {
8178 return Err(ErrorCode::SignedOut.into());
8179 }
8180 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
8181 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
8182 return Err(ErrorCode::Disconnected.into());
8183 }
8184 }
8185 }
8186
8187 let room = active_call
8188 .update(cx, |active_call, cx| {
8189 active_call.join_channel(channel_id, cx)
8190 })
8191 .await?;
8192
8193 let Some(room) = room else {
8194 return anyhow::Ok(true);
8195 };
8196
8197 room.update(cx, |room, _| room.room_update_completed())
8198 .await;
8199
8200 let task = room.update(cx, |room, cx| {
8201 if let Some((project, host)) = room.most_active_project(cx) {
8202 return Some(join_in_room_project(project, host, app_state.clone(), cx));
8203 }
8204
8205 // If you are the first to join a channel, see if you should share your project.
8206 if room.remote_participants().is_empty()
8207 && !room.local_participant_is_guest()
8208 && let Some(workspace) = requesting_workspace.as_ref().and_then(|w| w.upgrade())
8209 {
8210 let project = workspace.update(cx, |workspace, cx| {
8211 let project = workspace.project.read(cx);
8212
8213 if !CallSettings::get_global(cx).share_on_join {
8214 return None;
8215 }
8216
8217 if (project.is_local() || project.is_via_remote_server())
8218 && project.visible_worktrees(cx).any(|tree| {
8219 tree.read(cx)
8220 .root_entry()
8221 .is_some_and(|entry| entry.is_dir())
8222 })
8223 {
8224 Some(workspace.project.clone())
8225 } else {
8226 None
8227 }
8228 });
8229 if let Some(project) = project {
8230 return Some(cx.spawn(async move |room, cx| {
8231 room.update(cx, |room, cx| room.share_project(project, cx))?
8232 .await?;
8233 Ok(())
8234 }));
8235 }
8236 }
8237
8238 None
8239 });
8240 if let Some(task) = task {
8241 task.await?;
8242 return anyhow::Ok(true);
8243 }
8244 anyhow::Ok(false)
8245}
8246
8247pub fn join_channel(
8248 channel_id: ChannelId,
8249 app_state: Arc<AppState>,
8250 requesting_window: Option<WindowHandle<MultiWorkspace>>,
8251 requesting_workspace: Option<WeakEntity<Workspace>>,
8252 cx: &mut App,
8253) -> Task<Result<()>> {
8254 let active_call = ActiveCall::global(cx);
8255 cx.spawn(async move |cx| {
8256 let result = join_channel_internal(
8257 channel_id,
8258 &app_state,
8259 requesting_window,
8260 requesting_workspace,
8261 &active_call,
8262 cx,
8263 )
8264 .await;
8265
8266 // join channel succeeded, and opened a window
8267 if matches!(result, Ok(true)) {
8268 return anyhow::Ok(());
8269 }
8270
8271 // find an existing workspace to focus and show call controls
8272 let mut active_window = requesting_window.or_else(|| activate_any_workspace_window(cx));
8273 if active_window.is_none() {
8274 // no open workspaces, make one to show the error in (blergh)
8275 let (window_handle, _) = cx
8276 .update(|cx| {
8277 Workspace::new_local(
8278 vec![],
8279 app_state.clone(),
8280 requesting_window,
8281 None,
8282 None,
8283 cx,
8284 )
8285 })
8286 .await?;
8287
8288 window_handle
8289 .update(cx, |_, window, _cx| {
8290 window.activate_window();
8291 })
8292 .ok();
8293
8294 if result.is_ok() {
8295 cx.update(|cx| {
8296 cx.dispatch_action(&OpenChannelNotes);
8297 });
8298 }
8299
8300 active_window = Some(window_handle);
8301 }
8302
8303 if let Err(err) = result {
8304 log::error!("failed to join channel: {}", err);
8305 if let Some(active_window) = active_window {
8306 active_window
8307 .update(cx, |_, window, cx| {
8308 let detail: SharedString = match err.error_code() {
8309 ErrorCode::SignedOut => "Please sign in to continue.".into(),
8310 ErrorCode::UpgradeRequired => concat!(
8311 "Your are running an unsupported version of Zed. ",
8312 "Please update to continue."
8313 )
8314 .into(),
8315 ErrorCode::NoSuchChannel => concat!(
8316 "No matching channel was found. ",
8317 "Please check the link and try again."
8318 )
8319 .into(),
8320 ErrorCode::Forbidden => concat!(
8321 "This channel is private, and you do not have access. ",
8322 "Please ask someone to add you and try again."
8323 )
8324 .into(),
8325 ErrorCode::Disconnected => {
8326 "Please check your internet connection and try again.".into()
8327 }
8328 _ => format!("{}\n\nPlease try again.", err).into(),
8329 };
8330 window.prompt(
8331 PromptLevel::Critical,
8332 "Failed to join channel",
8333 Some(&detail),
8334 &["Ok"],
8335 cx,
8336 )
8337 })?
8338 .await
8339 .ok();
8340 }
8341 }
8342
8343 // return ok, we showed the error to the user.
8344 anyhow::Ok(())
8345 })
8346}
8347
8348pub async fn get_any_active_multi_workspace(
8349 app_state: Arc<AppState>,
8350 mut cx: AsyncApp,
8351) -> anyhow::Result<WindowHandle<MultiWorkspace>> {
8352 // find an existing workspace to focus and show call controls
8353 let active_window = activate_any_workspace_window(&mut cx);
8354 if active_window.is_none() {
8355 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, None, cx))
8356 .await?;
8357 }
8358 activate_any_workspace_window(&mut cx).context("could not open zed")
8359}
8360
8361fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<MultiWorkspace>> {
8362 cx.update(|cx| {
8363 if let Some(workspace_window) = cx
8364 .active_window()
8365 .and_then(|window| window.downcast::<MultiWorkspace>())
8366 {
8367 return Some(workspace_window);
8368 }
8369
8370 for window in cx.windows() {
8371 if let Some(workspace_window) = window.downcast::<MultiWorkspace>() {
8372 workspace_window
8373 .update(cx, |_, window, _| window.activate_window())
8374 .ok();
8375 return Some(workspace_window);
8376 }
8377 }
8378 None
8379 })
8380}
8381
8382pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<MultiWorkspace>> {
8383 workspace_windows_for_location(&SerializedWorkspaceLocation::Local, cx)
8384}
8385
8386pub fn workspace_windows_for_location(
8387 serialized_location: &SerializedWorkspaceLocation,
8388 cx: &App,
8389) -> Vec<WindowHandle<MultiWorkspace>> {
8390 cx.windows()
8391 .into_iter()
8392 .filter_map(|window| window.downcast::<MultiWorkspace>())
8393 .filter(|multi_workspace| {
8394 let same_host = |left: &RemoteConnectionOptions, right: &RemoteConnectionOptions| match (left, right) {
8395 (RemoteConnectionOptions::Ssh(a), RemoteConnectionOptions::Ssh(b)) => {
8396 (&a.host, &a.username, &a.port) == (&b.host, &b.username, &b.port)
8397 }
8398 (RemoteConnectionOptions::Wsl(a), RemoteConnectionOptions::Wsl(b)) => {
8399 // The WSL username is not consistently populated in the workspace location, so ignore it for now.
8400 a.distro_name == b.distro_name
8401 }
8402 (RemoteConnectionOptions::Docker(a), RemoteConnectionOptions::Docker(b)) => {
8403 a.container_id == b.container_id
8404 }
8405 #[cfg(any(test, feature = "test-support"))]
8406 (RemoteConnectionOptions::Mock(a), RemoteConnectionOptions::Mock(b)) => {
8407 a.id == b.id
8408 }
8409 _ => false,
8410 };
8411
8412 multi_workspace.read(cx).is_ok_and(|multi_workspace| {
8413 multi_workspace.workspaces().iter().any(|workspace| {
8414 match workspace.read(cx).workspace_location(cx) {
8415 WorkspaceLocation::Location(location, _) => {
8416 match (&location, serialized_location) {
8417 (
8418 SerializedWorkspaceLocation::Local,
8419 SerializedWorkspaceLocation::Local,
8420 ) => true,
8421 (
8422 SerializedWorkspaceLocation::Remote(a),
8423 SerializedWorkspaceLocation::Remote(b),
8424 ) => same_host(a, b),
8425 _ => false,
8426 }
8427 }
8428 _ => false,
8429 }
8430 })
8431 })
8432 })
8433 .collect()
8434}
8435
8436pub async fn find_existing_workspace(
8437 abs_paths: &[PathBuf],
8438 open_options: &OpenOptions,
8439 location: &SerializedWorkspaceLocation,
8440 cx: &mut AsyncApp,
8441) -> (
8442 Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)>,
8443 OpenVisible,
8444) {
8445 let mut existing: Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)> = None;
8446 let mut open_visible = OpenVisible::All;
8447 let mut best_match = None;
8448
8449 if open_options.open_new_workspace != Some(true) {
8450 cx.update(|cx| {
8451 for window in workspace_windows_for_location(location, cx) {
8452 if let Ok(multi_workspace) = window.read(cx) {
8453 for workspace in multi_workspace.workspaces() {
8454 let project = workspace.read(cx).project.read(cx);
8455 let m = project.visibility_for_paths(
8456 abs_paths,
8457 open_options.open_new_workspace == None,
8458 cx,
8459 );
8460 if m > best_match {
8461 existing = Some((window, workspace.clone()));
8462 best_match = m;
8463 } else if best_match.is_none()
8464 && open_options.open_new_workspace == Some(false)
8465 {
8466 existing = Some((window, workspace.clone()))
8467 }
8468 }
8469 }
8470 }
8471 });
8472
8473 let all_paths_are_files = existing
8474 .as_ref()
8475 .and_then(|(_, target_workspace)| {
8476 cx.update(|cx| {
8477 let workspace = target_workspace.read(cx);
8478 let project = workspace.project.read(cx);
8479 let path_style = workspace.path_style(cx);
8480 Some(!abs_paths.iter().any(|path| {
8481 let path = util::paths::SanitizedPath::new(path);
8482 project.worktrees(cx).any(|worktree| {
8483 let worktree = worktree.read(cx);
8484 let abs_path = worktree.abs_path();
8485 path_style
8486 .strip_prefix(path.as_ref(), abs_path.as_ref())
8487 .and_then(|rel| worktree.entry_for_path(&rel))
8488 .is_some_and(|e| e.is_dir())
8489 })
8490 }))
8491 })
8492 })
8493 .unwrap_or(false);
8494
8495 if open_options.open_new_workspace.is_none()
8496 && existing.is_some()
8497 && open_options.wait
8498 && all_paths_are_files
8499 {
8500 cx.update(|cx| {
8501 let windows = workspace_windows_for_location(location, cx);
8502 let window = cx
8503 .active_window()
8504 .and_then(|window| window.downcast::<MultiWorkspace>())
8505 .filter(|window| windows.contains(window))
8506 .or_else(|| windows.into_iter().next());
8507 if let Some(window) = window {
8508 if let Ok(multi_workspace) = window.read(cx) {
8509 let active_workspace = multi_workspace.workspace().clone();
8510 existing = Some((window, active_workspace));
8511 open_visible = OpenVisible::None;
8512 }
8513 }
8514 });
8515 }
8516 }
8517 (existing, open_visible)
8518}
8519
8520#[derive(Default, Clone)]
8521pub struct OpenOptions {
8522 pub visible: Option<OpenVisible>,
8523 pub focus: Option<bool>,
8524 pub open_new_workspace: Option<bool>,
8525 pub wait: bool,
8526 pub replace_window: Option<WindowHandle<MultiWorkspace>>,
8527 pub env: Option<HashMap<String, String>>,
8528}
8529
8530/// Opens a workspace by its database ID, used for restoring empty workspaces with unsaved content.
8531pub fn open_workspace_by_id(
8532 workspace_id: WorkspaceId,
8533 app_state: Arc<AppState>,
8534 requesting_window: Option<WindowHandle<MultiWorkspace>>,
8535 cx: &mut App,
8536) -> Task<anyhow::Result<WindowHandle<MultiWorkspace>>> {
8537 let project_handle = Project::local(
8538 app_state.client.clone(),
8539 app_state.node_runtime.clone(),
8540 app_state.user_store.clone(),
8541 app_state.languages.clone(),
8542 app_state.fs.clone(),
8543 None,
8544 project::LocalProjectFlags {
8545 init_worktree_trust: true,
8546 ..project::LocalProjectFlags::default()
8547 },
8548 cx,
8549 );
8550
8551 cx.spawn(async move |cx| {
8552 let serialized_workspace = persistence::DB
8553 .workspace_for_id(workspace_id)
8554 .with_context(|| format!("Workspace {workspace_id:?} not found"))?;
8555
8556 let centered_layout = serialized_workspace.centered_layout;
8557
8558 let (window, workspace) = if let Some(window) = requesting_window {
8559 let workspace = window.update(cx, |multi_workspace, window, cx| {
8560 let workspace = cx.new(|cx| {
8561 let mut workspace = Workspace::new(
8562 Some(workspace_id),
8563 project_handle.clone(),
8564 app_state.clone(),
8565 window,
8566 cx,
8567 );
8568 workspace.centered_layout = centered_layout;
8569 workspace
8570 });
8571 multi_workspace.add_workspace(workspace.clone(), cx);
8572 workspace
8573 })?;
8574 (window, workspace)
8575 } else {
8576 let window_bounds_override = window_bounds_env_override();
8577
8578 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
8579 (Some(WindowBounds::Windowed(bounds)), None)
8580 } else if let Some(display) = serialized_workspace.display
8581 && let Some(bounds) = serialized_workspace.window_bounds.as_ref()
8582 {
8583 (Some(bounds.0), Some(display))
8584 } else if let Some((display, bounds)) = persistence::read_default_window_bounds() {
8585 (Some(bounds), Some(display))
8586 } else {
8587 (None, None)
8588 };
8589
8590 let options = cx.update(|cx| {
8591 let mut options = (app_state.build_window_options)(display, cx);
8592 options.window_bounds = window_bounds;
8593 options
8594 });
8595
8596 let window = cx.open_window(options, {
8597 let app_state = app_state.clone();
8598 let project_handle = project_handle.clone();
8599 move |window, cx| {
8600 let workspace = cx.new(|cx| {
8601 let mut workspace = Workspace::new(
8602 Some(workspace_id),
8603 project_handle,
8604 app_state,
8605 window,
8606 cx,
8607 );
8608 workspace.centered_layout = centered_layout;
8609 workspace
8610 });
8611 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
8612 }
8613 })?;
8614
8615 let workspace = window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
8616 multi_workspace.workspace().clone()
8617 })?;
8618
8619 (window, workspace)
8620 };
8621
8622 notify_if_database_failed(window, cx);
8623
8624 // Restore items from the serialized workspace
8625 window
8626 .update(cx, |_, window, cx| {
8627 workspace.update(cx, |_workspace, cx| {
8628 open_items(Some(serialized_workspace), vec![], window, cx)
8629 })
8630 })?
8631 .await?;
8632
8633 window.update(cx, |_, window, cx| {
8634 workspace.update(cx, |workspace, cx| {
8635 workspace.serialize_workspace(window, cx);
8636 });
8637 })?;
8638
8639 Ok(window)
8640 })
8641}
8642
8643#[allow(clippy::type_complexity)]
8644pub fn open_paths(
8645 abs_paths: &[PathBuf],
8646 app_state: Arc<AppState>,
8647 open_options: OpenOptions,
8648 cx: &mut App,
8649) -> Task<
8650 anyhow::Result<(
8651 WindowHandle<MultiWorkspace>,
8652 Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
8653 )>,
8654> {
8655 let abs_paths = abs_paths.to_vec();
8656 #[cfg(target_os = "windows")]
8657 let wsl_path = abs_paths
8658 .iter()
8659 .find_map(|p| util::paths::WslPath::from_path(p));
8660
8661 cx.spawn(async move |cx| {
8662 let (mut existing, mut open_visible) = find_existing_workspace(
8663 &abs_paths,
8664 &open_options,
8665 &SerializedWorkspaceLocation::Local,
8666 cx,
8667 )
8668 .await;
8669
8670 // Fallback: if no workspace contains the paths and all paths are files,
8671 // prefer an existing local workspace window (active window first).
8672 if open_options.open_new_workspace.is_none() && existing.is_none() {
8673 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
8674 let all_metadatas = futures::future::join_all(all_paths)
8675 .await
8676 .into_iter()
8677 .filter_map(|result| result.ok().flatten())
8678 .collect::<Vec<_>>();
8679
8680 if all_metadatas.iter().all(|file| !file.is_dir) {
8681 cx.update(|cx| {
8682 let windows = workspace_windows_for_location(
8683 &SerializedWorkspaceLocation::Local,
8684 cx,
8685 );
8686 let window = cx
8687 .active_window()
8688 .and_then(|window| window.downcast::<MultiWorkspace>())
8689 .filter(|window| windows.contains(window))
8690 .or_else(|| windows.into_iter().next());
8691 if let Some(window) = window {
8692 if let Ok(multi_workspace) = window.read(cx) {
8693 let active_workspace = multi_workspace.workspace().clone();
8694 existing = Some((window, active_workspace));
8695 open_visible = OpenVisible::None;
8696 }
8697 }
8698 });
8699 }
8700 }
8701
8702 let result = if let Some((existing, target_workspace)) = existing {
8703 let open_task = existing
8704 .update(cx, |multi_workspace, window, cx| {
8705 window.activate_window();
8706 multi_workspace.activate(target_workspace.clone(), cx);
8707 target_workspace.update(cx, |workspace, cx| {
8708 workspace.open_paths(
8709 abs_paths,
8710 OpenOptions {
8711 visible: Some(open_visible),
8712 ..Default::default()
8713 },
8714 None,
8715 window,
8716 cx,
8717 )
8718 })
8719 })?
8720 .await;
8721
8722 _ = existing.update(cx, |multi_workspace, _, cx| {
8723 let workspace = multi_workspace.workspace().clone();
8724 workspace.update(cx, |workspace, cx| {
8725 for item in open_task.iter().flatten() {
8726 if let Err(e) = item {
8727 workspace.show_error(&e, cx);
8728 }
8729 }
8730 });
8731 });
8732
8733 Ok((existing, open_task))
8734 } else {
8735 let result = cx
8736 .update(move |cx| {
8737 Workspace::new_local(
8738 abs_paths,
8739 app_state.clone(),
8740 open_options.replace_window,
8741 open_options.env,
8742 None,
8743 cx,
8744 )
8745 })
8746 .await;
8747
8748 if let Ok((ref window_handle, _)) = result {
8749 window_handle
8750 .update(cx, |_, window, _cx| {
8751 window.activate_window();
8752 })
8753 .log_err();
8754 }
8755
8756 result
8757 };
8758
8759 #[cfg(target_os = "windows")]
8760 if let Some(util::paths::WslPath{distro, path}) = wsl_path
8761 && let Ok((multi_workspace_window, _)) = &result
8762 {
8763 multi_workspace_window
8764 .update(cx, move |multi_workspace, _window, cx| {
8765 struct OpenInWsl;
8766 let workspace = multi_workspace.workspace().clone();
8767 workspace.update(cx, |workspace, cx| {
8768 workspace.show_notification(NotificationId::unique::<OpenInWsl>(), cx, move |cx| {
8769 let display_path = util::markdown::MarkdownInlineCode(&path.to_string_lossy());
8770 let msg = format!("{display_path} is inside a WSL filesystem, some features may not work unless you open it with WSL remote");
8771 cx.new(move |cx| {
8772 MessageNotification::new(msg, cx)
8773 .primary_message("Open in WSL")
8774 .primary_icon(IconName::FolderOpen)
8775 .primary_on_click(move |window, cx| {
8776 window.dispatch_action(Box::new(remote::OpenWslPath {
8777 distro: remote::WslConnectionOptions {
8778 distro_name: distro.clone(),
8779 user: None,
8780 },
8781 paths: vec![path.clone().into()],
8782 }), cx)
8783 })
8784 })
8785 });
8786 });
8787 })
8788 .unwrap();
8789 };
8790 result
8791 })
8792}
8793
8794pub fn open_new(
8795 open_options: OpenOptions,
8796 app_state: Arc<AppState>,
8797 cx: &mut App,
8798 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
8799) -> Task<anyhow::Result<()>> {
8800 let task = Workspace::new_local(
8801 Vec::new(),
8802 app_state,
8803 open_options.replace_window,
8804 open_options.env,
8805 Some(Box::new(init)),
8806 cx,
8807 );
8808 cx.spawn(async move |cx| {
8809 let (window, _opened_paths) = task.await?;
8810 window
8811 .update(cx, |_, window, _cx| {
8812 window.activate_window();
8813 })
8814 .ok();
8815 Ok(())
8816 })
8817}
8818
8819pub fn create_and_open_local_file(
8820 path: &'static Path,
8821 window: &mut Window,
8822 cx: &mut Context<Workspace>,
8823 default_content: impl 'static + Send + FnOnce() -> Rope,
8824) -> Task<Result<Box<dyn ItemHandle>>> {
8825 cx.spawn_in(window, async move |workspace, cx| {
8826 let fs = workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
8827 if !fs.is_file(path).await {
8828 fs.create_file(path, Default::default()).await?;
8829 fs.save(path, &default_content(), Default::default())
8830 .await?;
8831 }
8832
8833 workspace
8834 .update_in(cx, |workspace, window, cx| {
8835 workspace.with_local_or_wsl_workspace(window, cx, |workspace, window, cx| {
8836 let path = workspace
8837 .project
8838 .read_with(cx, |project, cx| project.try_windows_path_to_wsl(path, cx));
8839 cx.spawn_in(window, async move |workspace, cx| {
8840 let path = path.await?;
8841 let mut items = workspace
8842 .update_in(cx, |workspace, window, cx| {
8843 workspace.open_paths(
8844 vec![path.to_path_buf()],
8845 OpenOptions {
8846 visible: Some(OpenVisible::None),
8847 ..Default::default()
8848 },
8849 None,
8850 window,
8851 cx,
8852 )
8853 })?
8854 .await;
8855 let item = items.pop().flatten();
8856 item.with_context(|| format!("path {path:?} is not a file"))?
8857 })
8858 })
8859 })?
8860 .await?
8861 .await
8862 })
8863}
8864
8865pub fn open_remote_project_with_new_connection(
8866 window: WindowHandle<MultiWorkspace>,
8867 remote_connection: Arc<dyn RemoteConnection>,
8868 cancel_rx: oneshot::Receiver<()>,
8869 delegate: Arc<dyn RemoteClientDelegate>,
8870 app_state: Arc<AppState>,
8871 paths: Vec<PathBuf>,
8872 cx: &mut App,
8873) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
8874 cx.spawn(async move |cx| {
8875 let (workspace_id, serialized_workspace) =
8876 deserialize_remote_project(remote_connection.connection_options(), paths.clone(), cx)
8877 .await?;
8878
8879 let session = match cx
8880 .update(|cx| {
8881 remote::RemoteClient::new(
8882 ConnectionIdentifier::Workspace(workspace_id.0),
8883 remote_connection,
8884 cancel_rx,
8885 delegate,
8886 cx,
8887 )
8888 })
8889 .await?
8890 {
8891 Some(result) => result,
8892 None => return Ok(Vec::new()),
8893 };
8894
8895 let project = cx.update(|cx| {
8896 project::Project::remote(
8897 session,
8898 app_state.client.clone(),
8899 app_state.node_runtime.clone(),
8900 app_state.user_store.clone(),
8901 app_state.languages.clone(),
8902 app_state.fs.clone(),
8903 true,
8904 cx,
8905 )
8906 });
8907
8908 open_remote_project_inner(
8909 project,
8910 paths,
8911 workspace_id,
8912 serialized_workspace,
8913 app_state,
8914 window,
8915 cx,
8916 )
8917 .await
8918 })
8919}
8920
8921pub fn open_remote_project_with_existing_connection(
8922 connection_options: RemoteConnectionOptions,
8923 project: Entity<Project>,
8924 paths: Vec<PathBuf>,
8925 app_state: Arc<AppState>,
8926 window: WindowHandle<MultiWorkspace>,
8927 cx: &mut AsyncApp,
8928) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
8929 cx.spawn(async move |cx| {
8930 let (workspace_id, serialized_workspace) =
8931 deserialize_remote_project(connection_options.clone(), paths.clone(), cx).await?;
8932
8933 open_remote_project_inner(
8934 project,
8935 paths,
8936 workspace_id,
8937 serialized_workspace,
8938 app_state,
8939 window,
8940 cx,
8941 )
8942 .await
8943 })
8944}
8945
8946async fn open_remote_project_inner(
8947 project: Entity<Project>,
8948 paths: Vec<PathBuf>,
8949 workspace_id: WorkspaceId,
8950 serialized_workspace: Option<SerializedWorkspace>,
8951 app_state: Arc<AppState>,
8952 window: WindowHandle<MultiWorkspace>,
8953 cx: &mut AsyncApp,
8954) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
8955 let toolchains = DB.toolchains(workspace_id).await?;
8956 for (toolchain, worktree_path, path) in toolchains {
8957 project
8958 .update(cx, |this, cx| {
8959 let Some(worktree_id) =
8960 this.find_worktree(&worktree_path, cx)
8961 .and_then(|(worktree, rel_path)| {
8962 if rel_path.is_empty() {
8963 Some(worktree.read(cx).id())
8964 } else {
8965 None
8966 }
8967 })
8968 else {
8969 return Task::ready(None);
8970 };
8971
8972 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
8973 })
8974 .await;
8975 }
8976 let mut project_paths_to_open = vec![];
8977 let mut project_path_errors = vec![];
8978
8979 for path in paths {
8980 let result = cx
8981 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))
8982 .await;
8983 match result {
8984 Ok((_, project_path)) => {
8985 project_paths_to_open.push((path.clone(), Some(project_path)));
8986 }
8987 Err(error) => {
8988 project_path_errors.push(error);
8989 }
8990 };
8991 }
8992
8993 if project_paths_to_open.is_empty() {
8994 return Err(project_path_errors.pop().context("no paths given")?);
8995 }
8996
8997 let workspace = window.update(cx, |multi_workspace, window, cx| {
8998 telemetry::event!("SSH Project Opened");
8999
9000 let new_workspace = cx.new(|cx| {
9001 let mut workspace =
9002 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
9003 workspace.update_history(cx);
9004
9005 if let Some(ref serialized) = serialized_workspace {
9006 workspace.centered_layout = serialized.centered_layout;
9007 }
9008
9009 workspace
9010 });
9011
9012 multi_workspace.activate(new_workspace.clone(), cx);
9013 new_workspace
9014 })?;
9015
9016 let items = window
9017 .update(cx, |_, window, cx| {
9018 window.activate_window();
9019 workspace.update(cx, |_workspace, cx| {
9020 open_items(serialized_workspace, project_paths_to_open, window, cx)
9021 })
9022 })?
9023 .await?;
9024
9025 workspace.update(cx, |workspace, cx| {
9026 for error in project_path_errors {
9027 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
9028 if let Some(path) = error.error_tag("path") {
9029 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
9030 }
9031 } else {
9032 workspace.show_error(&error, cx)
9033 }
9034 }
9035 });
9036
9037 Ok(items.into_iter().map(|item| item?.ok()).collect())
9038}
9039
9040fn deserialize_remote_project(
9041 connection_options: RemoteConnectionOptions,
9042 paths: Vec<PathBuf>,
9043 cx: &AsyncApp,
9044) -> Task<Result<(WorkspaceId, Option<SerializedWorkspace>)>> {
9045 cx.background_spawn(async move {
9046 let remote_connection_id = persistence::DB
9047 .get_or_create_remote_connection(connection_options)
9048 .await?;
9049
9050 let serialized_workspace =
9051 persistence::DB.remote_workspace_for_roots(&paths, remote_connection_id);
9052
9053 let workspace_id = if let Some(workspace_id) =
9054 serialized_workspace.as_ref().map(|workspace| workspace.id)
9055 {
9056 workspace_id
9057 } else {
9058 persistence::DB.next_id().await?
9059 };
9060
9061 Ok((workspace_id, serialized_workspace))
9062 })
9063}
9064
9065pub fn join_in_room_project(
9066 project_id: u64,
9067 follow_user_id: u64,
9068 app_state: Arc<AppState>,
9069 cx: &mut App,
9070) -> Task<Result<()>> {
9071 let windows = cx.windows();
9072 cx.spawn(async move |cx| {
9073 let existing_window_and_workspace: Option<(
9074 WindowHandle<MultiWorkspace>,
9075 Entity<Workspace>,
9076 )> = windows.into_iter().find_map(|window_handle| {
9077 window_handle
9078 .downcast::<MultiWorkspace>()
9079 .and_then(|window_handle| {
9080 window_handle
9081 .update(cx, |multi_workspace, _window, cx| {
9082 for workspace in multi_workspace.workspaces() {
9083 if workspace.read(cx).project().read(cx).remote_id()
9084 == Some(project_id)
9085 {
9086 return Some((window_handle, workspace.clone()));
9087 }
9088 }
9089 None
9090 })
9091 .unwrap_or(None)
9092 })
9093 });
9094
9095 let multi_workspace_window = if let Some((existing_window, target_workspace)) =
9096 existing_window_and_workspace
9097 {
9098 existing_window
9099 .update(cx, |multi_workspace, _, cx| {
9100 multi_workspace.activate(target_workspace, cx);
9101 })
9102 .ok();
9103 existing_window
9104 } else {
9105 let active_call = cx.update(|cx| ActiveCall::global(cx));
9106 let room = active_call
9107 .read_with(cx, |call, _| call.room().cloned())
9108 .context("not in a call")?;
9109 let project = room
9110 .update(cx, |room, cx| {
9111 room.join_project(
9112 project_id,
9113 app_state.languages.clone(),
9114 app_state.fs.clone(),
9115 cx,
9116 )
9117 })
9118 .await?;
9119
9120 let window_bounds_override = window_bounds_env_override();
9121 cx.update(|cx| {
9122 let mut options = (app_state.build_window_options)(None, cx);
9123 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
9124 cx.open_window(options, |window, cx| {
9125 let workspace = cx.new(|cx| {
9126 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
9127 });
9128 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
9129 })
9130 })?
9131 };
9132
9133 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
9134 cx.activate(true);
9135 window.activate_window();
9136
9137 // We set the active workspace above, so this is the correct workspace.
9138 let workspace = multi_workspace.workspace().clone();
9139 workspace.update(cx, |workspace, cx| {
9140 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
9141 let follow_peer_id = room
9142 .read(cx)
9143 .remote_participants()
9144 .iter()
9145 .find(|(_, participant)| participant.user.id == follow_user_id)
9146 .map(|(_, p)| p.peer_id)
9147 .or_else(|| {
9148 // If we couldn't follow the given user, follow the host instead.
9149 let collaborator = workspace
9150 .project()
9151 .read(cx)
9152 .collaborators()
9153 .values()
9154 .find(|collaborator| collaborator.is_host)?;
9155 Some(collaborator.peer_id)
9156 });
9157
9158 if let Some(follow_peer_id) = follow_peer_id {
9159 workspace.follow(follow_peer_id, window, cx);
9160 }
9161 }
9162 });
9163 })?;
9164
9165 anyhow::Ok(())
9166 })
9167}
9168
9169pub fn reload(cx: &mut App) {
9170 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
9171 let mut workspace_windows = cx
9172 .windows()
9173 .into_iter()
9174 .filter_map(|window| window.downcast::<MultiWorkspace>())
9175 .collect::<Vec<_>>();
9176
9177 // If multiple windows have unsaved changes, and need a save prompt,
9178 // prompt in the active window before switching to a different window.
9179 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
9180
9181 let mut prompt = None;
9182 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
9183 prompt = window
9184 .update(cx, |_, window, cx| {
9185 window.prompt(
9186 PromptLevel::Info,
9187 "Are you sure you want to restart?",
9188 None,
9189 &["Restart", "Cancel"],
9190 cx,
9191 )
9192 })
9193 .ok();
9194 }
9195
9196 cx.spawn(async move |cx| {
9197 if let Some(prompt) = prompt {
9198 let answer = prompt.await?;
9199 if answer != 0 {
9200 return anyhow::Ok(());
9201 }
9202 }
9203
9204 // If the user cancels any save prompt, then keep the app open.
9205 for window in workspace_windows {
9206 if let Ok(should_close) = window.update(cx, |multi_workspace, window, cx| {
9207 let workspace = multi_workspace.workspace().clone();
9208 workspace.update(cx, |workspace, cx| {
9209 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
9210 })
9211 }) && !should_close.await?
9212 {
9213 return anyhow::Ok(());
9214 }
9215 }
9216 cx.update(|cx| cx.restart());
9217 anyhow::Ok(())
9218 })
9219 .detach_and_log_err(cx);
9220}
9221
9222fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
9223 let mut parts = value.split(',');
9224 let x: usize = parts.next()?.parse().ok()?;
9225 let y: usize = parts.next()?.parse().ok()?;
9226 Some(point(px(x as f32), px(y as f32)))
9227}
9228
9229fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
9230 let mut parts = value.split(',');
9231 let width: usize = parts.next()?.parse().ok()?;
9232 let height: usize = parts.next()?.parse().ok()?;
9233 Some(size(px(width as f32), px(height as f32)))
9234}
9235
9236/// Add client-side decorations (rounded corners, shadows, resize handling) when
9237/// appropriate.
9238///
9239/// The `border_radius_tiling` parameter allows overriding which corners get
9240/// rounded, independently of the actual window tiling state. This is used
9241/// specifically for the workspace switcher sidebar: when the sidebar is open,
9242/// we want square corners on the left (so the sidebar appears flush with the
9243/// window edge) but we still need the shadow padding for proper visual
9244/// appearance. Unlike actual window tiling, this only affects border radius -
9245/// not padding or shadows.
9246pub fn client_side_decorations(
9247 element: impl IntoElement,
9248 window: &mut Window,
9249 cx: &mut App,
9250 border_radius_tiling: Tiling,
9251) -> Stateful<Div> {
9252 const BORDER_SIZE: Pixels = px(1.0);
9253 let decorations = window.window_decorations();
9254 let tiling = match decorations {
9255 Decorations::Server => Tiling::default(),
9256 Decorations::Client { tiling } => tiling,
9257 };
9258
9259 match decorations {
9260 Decorations::Client { .. } => window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW),
9261 Decorations::Server => window.set_client_inset(px(0.0)),
9262 }
9263
9264 struct GlobalResizeEdge(ResizeEdge);
9265 impl Global for GlobalResizeEdge {}
9266
9267 div()
9268 .id("window-backdrop")
9269 .bg(transparent_black())
9270 .map(|div| match decorations {
9271 Decorations::Server => div,
9272 Decorations::Client { .. } => div
9273 .when(
9274 !(tiling.top
9275 || tiling.right
9276 || border_radius_tiling.top
9277 || border_radius_tiling.right),
9278 |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9279 )
9280 .when(
9281 !(tiling.top
9282 || tiling.left
9283 || border_radius_tiling.top
9284 || border_radius_tiling.left),
9285 |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9286 )
9287 .when(
9288 !(tiling.bottom
9289 || tiling.right
9290 || border_radius_tiling.bottom
9291 || border_radius_tiling.right),
9292 |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9293 )
9294 .when(
9295 !(tiling.bottom
9296 || tiling.left
9297 || border_radius_tiling.bottom
9298 || border_radius_tiling.left),
9299 |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9300 )
9301 .when(!tiling.top, |div| {
9302 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
9303 })
9304 .when(!tiling.bottom, |div| {
9305 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
9306 })
9307 .when(!tiling.left, |div| {
9308 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
9309 })
9310 .when(!tiling.right, |div| {
9311 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
9312 })
9313 .on_mouse_move(move |e, window, cx| {
9314 let size = window.window_bounds().get_bounds().size;
9315 let pos = e.position;
9316
9317 let new_edge =
9318 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
9319
9320 let edge = cx.try_global::<GlobalResizeEdge>();
9321 if new_edge != edge.map(|edge| edge.0) {
9322 window
9323 .window_handle()
9324 .update(cx, |workspace, _, cx| {
9325 cx.notify(workspace.entity_id());
9326 })
9327 .ok();
9328 }
9329 })
9330 .on_mouse_down(MouseButton::Left, move |e, window, _| {
9331 let size = window.window_bounds().get_bounds().size;
9332 let pos = e.position;
9333
9334 let edge = match resize_edge(
9335 pos,
9336 theme::CLIENT_SIDE_DECORATION_SHADOW,
9337 size,
9338 tiling,
9339 ) {
9340 Some(value) => value,
9341 None => return,
9342 };
9343
9344 window.start_window_resize(edge);
9345 }),
9346 })
9347 .size_full()
9348 .child(
9349 div()
9350 .cursor(CursorStyle::Arrow)
9351 .map(|div| match decorations {
9352 Decorations::Server => div,
9353 Decorations::Client { .. } => div
9354 .border_color(cx.theme().colors().border)
9355 .when(
9356 !(tiling.top
9357 || tiling.right
9358 || border_radius_tiling.top
9359 || border_radius_tiling.right),
9360 |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9361 )
9362 .when(
9363 !(tiling.top
9364 || tiling.left
9365 || border_radius_tiling.top
9366 || border_radius_tiling.left),
9367 |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9368 )
9369 .when(
9370 !(tiling.bottom
9371 || tiling.right
9372 || border_radius_tiling.bottom
9373 || border_radius_tiling.right),
9374 |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9375 )
9376 .when(
9377 !(tiling.bottom
9378 || tiling.left
9379 || border_radius_tiling.bottom
9380 || border_radius_tiling.left),
9381 |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9382 )
9383 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
9384 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
9385 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
9386 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
9387 .when(!tiling.is_tiled(), |div| {
9388 div.shadow(vec![gpui::BoxShadow {
9389 color: Hsla {
9390 h: 0.,
9391 s: 0.,
9392 l: 0.,
9393 a: 0.4,
9394 },
9395 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
9396 spread_radius: px(0.),
9397 offset: point(px(0.0), px(0.0)),
9398 }])
9399 }),
9400 })
9401 .on_mouse_move(|_e, _, cx| {
9402 cx.stop_propagation();
9403 })
9404 .size_full()
9405 .child(element),
9406 )
9407 .map(|div| match decorations {
9408 Decorations::Server => div,
9409 Decorations::Client { tiling, .. } => div.child(
9410 canvas(
9411 |_bounds, window, _| {
9412 window.insert_hitbox(
9413 Bounds::new(
9414 point(px(0.0), px(0.0)),
9415 window.window_bounds().get_bounds().size,
9416 ),
9417 HitboxBehavior::Normal,
9418 )
9419 },
9420 move |_bounds, hitbox, window, cx| {
9421 let mouse = window.mouse_position();
9422 let size = window.window_bounds().get_bounds().size;
9423 let Some(edge) =
9424 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
9425 else {
9426 return;
9427 };
9428 cx.set_global(GlobalResizeEdge(edge));
9429 window.set_cursor_style(
9430 match edge {
9431 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
9432 ResizeEdge::Left | ResizeEdge::Right => {
9433 CursorStyle::ResizeLeftRight
9434 }
9435 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
9436 CursorStyle::ResizeUpLeftDownRight
9437 }
9438 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
9439 CursorStyle::ResizeUpRightDownLeft
9440 }
9441 },
9442 &hitbox,
9443 );
9444 },
9445 )
9446 .size_full()
9447 .absolute(),
9448 ),
9449 })
9450}
9451
9452fn resize_edge(
9453 pos: Point<Pixels>,
9454 shadow_size: Pixels,
9455 window_size: Size<Pixels>,
9456 tiling: Tiling,
9457) -> Option<ResizeEdge> {
9458 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
9459 if bounds.contains(&pos) {
9460 return None;
9461 }
9462
9463 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
9464 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
9465 if !tiling.top && top_left_bounds.contains(&pos) {
9466 return Some(ResizeEdge::TopLeft);
9467 }
9468
9469 let top_right_bounds = Bounds::new(
9470 Point::new(window_size.width - corner_size.width, px(0.)),
9471 corner_size,
9472 );
9473 if !tiling.top && top_right_bounds.contains(&pos) {
9474 return Some(ResizeEdge::TopRight);
9475 }
9476
9477 let bottom_left_bounds = Bounds::new(
9478 Point::new(px(0.), window_size.height - corner_size.height),
9479 corner_size,
9480 );
9481 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
9482 return Some(ResizeEdge::BottomLeft);
9483 }
9484
9485 let bottom_right_bounds = Bounds::new(
9486 Point::new(
9487 window_size.width - corner_size.width,
9488 window_size.height - corner_size.height,
9489 ),
9490 corner_size,
9491 );
9492 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
9493 return Some(ResizeEdge::BottomRight);
9494 }
9495
9496 if !tiling.top && pos.y < shadow_size {
9497 Some(ResizeEdge::Top)
9498 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
9499 Some(ResizeEdge::Bottom)
9500 } else if !tiling.left && pos.x < shadow_size {
9501 Some(ResizeEdge::Left)
9502 } else if !tiling.right && pos.x > window_size.width - shadow_size {
9503 Some(ResizeEdge::Right)
9504 } else {
9505 None
9506 }
9507}
9508
9509fn join_pane_into_active(
9510 active_pane: &Entity<Pane>,
9511 pane: &Entity<Pane>,
9512 window: &mut Window,
9513 cx: &mut App,
9514) {
9515 if pane == active_pane {
9516 } else if pane.read(cx).items_len() == 0 {
9517 pane.update(cx, |_, cx| {
9518 cx.emit(pane::Event::Remove {
9519 focus_on_pane: None,
9520 });
9521 })
9522 } else {
9523 move_all_items(pane, active_pane, window, cx);
9524 }
9525}
9526
9527fn move_all_items(
9528 from_pane: &Entity<Pane>,
9529 to_pane: &Entity<Pane>,
9530 window: &mut Window,
9531 cx: &mut App,
9532) {
9533 let destination_is_different = from_pane != to_pane;
9534 let mut moved_items = 0;
9535 for (item_ix, item_handle) in from_pane
9536 .read(cx)
9537 .items()
9538 .enumerate()
9539 .map(|(ix, item)| (ix, item.clone()))
9540 .collect::<Vec<_>>()
9541 {
9542 let ix = item_ix - moved_items;
9543 if destination_is_different {
9544 // Close item from previous pane
9545 from_pane.update(cx, |source, cx| {
9546 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
9547 });
9548 moved_items += 1;
9549 }
9550
9551 // This automatically removes duplicate items in the pane
9552 to_pane.update(cx, |destination, cx| {
9553 destination.add_item(item_handle, true, true, None, window, cx);
9554 window.focus(&destination.focus_handle(cx), cx)
9555 });
9556 }
9557}
9558
9559pub fn move_item(
9560 source: &Entity<Pane>,
9561 destination: &Entity<Pane>,
9562 item_id_to_move: EntityId,
9563 destination_index: usize,
9564 activate: bool,
9565 window: &mut Window,
9566 cx: &mut App,
9567) {
9568 let Some((item_ix, item_handle)) = source
9569 .read(cx)
9570 .items()
9571 .enumerate()
9572 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
9573 .map(|(ix, item)| (ix, item.clone()))
9574 else {
9575 // Tab was closed during drag
9576 return;
9577 };
9578
9579 if source != destination {
9580 // Close item from previous pane
9581 source.update(cx, |source, cx| {
9582 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
9583 });
9584 }
9585
9586 // This automatically removes duplicate items in the pane
9587 destination.update(cx, |destination, cx| {
9588 destination.add_item_inner(
9589 item_handle,
9590 activate,
9591 activate,
9592 activate,
9593 Some(destination_index),
9594 window,
9595 cx,
9596 );
9597 if activate {
9598 window.focus(&destination.focus_handle(cx), cx)
9599 }
9600 });
9601}
9602
9603pub fn move_active_item(
9604 source: &Entity<Pane>,
9605 destination: &Entity<Pane>,
9606 focus_destination: bool,
9607 close_if_empty: bool,
9608 window: &mut Window,
9609 cx: &mut App,
9610) {
9611 if source == destination {
9612 return;
9613 }
9614 let Some(active_item) = source.read(cx).active_item() else {
9615 return;
9616 };
9617 source.update(cx, |source_pane, cx| {
9618 let item_id = active_item.item_id();
9619 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
9620 destination.update(cx, |target_pane, cx| {
9621 target_pane.add_item(
9622 active_item,
9623 focus_destination,
9624 focus_destination,
9625 Some(target_pane.items_len()),
9626 window,
9627 cx,
9628 );
9629 });
9630 });
9631}
9632
9633pub fn clone_active_item(
9634 workspace_id: Option<WorkspaceId>,
9635 source: &Entity<Pane>,
9636 destination: &Entity<Pane>,
9637 focus_destination: bool,
9638 window: &mut Window,
9639 cx: &mut App,
9640) {
9641 if source == destination {
9642 return;
9643 }
9644 let Some(active_item) = source.read(cx).active_item() else {
9645 return;
9646 };
9647 if !active_item.can_split(cx) {
9648 return;
9649 }
9650 let destination = destination.downgrade();
9651 let task = active_item.clone_on_split(workspace_id, window, cx);
9652 window
9653 .spawn(cx, async move |cx| {
9654 let Some(clone) = task.await else {
9655 return;
9656 };
9657 destination
9658 .update_in(cx, |target_pane, window, cx| {
9659 target_pane.add_item(
9660 clone,
9661 focus_destination,
9662 focus_destination,
9663 Some(target_pane.items_len()),
9664 window,
9665 cx,
9666 );
9667 })
9668 .log_err();
9669 })
9670 .detach();
9671}
9672
9673#[derive(Debug)]
9674pub struct WorkspacePosition {
9675 pub window_bounds: Option<WindowBounds>,
9676 pub display: Option<Uuid>,
9677 pub centered_layout: bool,
9678}
9679
9680pub fn remote_workspace_position_from_db(
9681 connection_options: RemoteConnectionOptions,
9682 paths_to_open: &[PathBuf],
9683 cx: &App,
9684) -> Task<Result<WorkspacePosition>> {
9685 let paths = paths_to_open.to_vec();
9686
9687 cx.background_spawn(async move {
9688 let remote_connection_id = persistence::DB
9689 .get_or_create_remote_connection(connection_options)
9690 .await
9691 .context("fetching serialized ssh project")?;
9692 let serialized_workspace =
9693 persistence::DB.remote_workspace_for_roots(&paths, remote_connection_id);
9694
9695 let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
9696 (Some(WindowBounds::Windowed(bounds)), None)
9697 } else {
9698 let restorable_bounds = serialized_workspace
9699 .as_ref()
9700 .and_then(|workspace| {
9701 Some((workspace.display?, workspace.window_bounds.map(|b| b.0)?))
9702 })
9703 .or_else(|| persistence::read_default_window_bounds());
9704
9705 if let Some((serialized_display, serialized_bounds)) = restorable_bounds {
9706 (Some(serialized_bounds), Some(serialized_display))
9707 } else {
9708 (None, None)
9709 }
9710 };
9711
9712 let centered_layout = serialized_workspace
9713 .as_ref()
9714 .map(|w| w.centered_layout)
9715 .unwrap_or(false);
9716
9717 Ok(WorkspacePosition {
9718 window_bounds,
9719 display,
9720 centered_layout,
9721 })
9722 })
9723}
9724
9725pub fn with_active_or_new_workspace(
9726 cx: &mut App,
9727 f: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + 'static,
9728) {
9729 match cx
9730 .active_window()
9731 .and_then(|w| w.downcast::<MultiWorkspace>())
9732 {
9733 Some(multi_workspace) => {
9734 cx.defer(move |cx| {
9735 multi_workspace
9736 .update(cx, |multi_workspace, window, cx| {
9737 let workspace = multi_workspace.workspace().clone();
9738 workspace.update(cx, |workspace, cx| f(workspace, window, cx));
9739 })
9740 .log_err();
9741 });
9742 }
9743 None => {
9744 let app_state = AppState::global(cx);
9745 if let Some(app_state) = app_state.upgrade() {
9746 open_new(
9747 OpenOptions::default(),
9748 app_state,
9749 cx,
9750 move |workspace, window, cx| f(workspace, window, cx),
9751 )
9752 .detach_and_log_err(cx);
9753 }
9754 }
9755 }
9756}
9757
9758#[cfg(test)]
9759mod tests {
9760 use std::{cell::RefCell, rc::Rc};
9761
9762 use super::*;
9763 use crate::{
9764 dock::{PanelEvent, test::TestPanel},
9765 item::{
9766 ItemBufferKind, ItemEvent,
9767 test::{TestItem, TestProjectItem},
9768 },
9769 };
9770 use fs::FakeFs;
9771 use gpui::{
9772 DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
9773 UpdateGlobal, VisualTestContext, px,
9774 };
9775 use project::{Project, ProjectEntryId};
9776 use serde_json::json;
9777 use settings::SettingsStore;
9778 use util::rel_path::rel_path;
9779
9780 #[gpui::test]
9781 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
9782 init_test(cx);
9783
9784 let fs = FakeFs::new(cx.executor());
9785 let project = Project::test(fs, [], cx).await;
9786 let (workspace, cx) =
9787 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9788
9789 // Adding an item with no ambiguity renders the tab without detail.
9790 let item1 = cx.new(|cx| {
9791 let mut item = TestItem::new(cx);
9792 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
9793 item
9794 });
9795 workspace.update_in(cx, |workspace, window, cx| {
9796 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
9797 });
9798 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
9799
9800 // Adding an item that creates ambiguity increases the level of detail on
9801 // both tabs.
9802 let item2 = cx.new_window_entity(|_window, cx| {
9803 let mut item = TestItem::new(cx);
9804 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
9805 item
9806 });
9807 workspace.update_in(cx, |workspace, window, cx| {
9808 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
9809 });
9810 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
9811 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
9812
9813 // Adding an item that creates ambiguity increases the level of detail only
9814 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
9815 // we stop at the highest detail available.
9816 let item3 = cx.new(|cx| {
9817 let mut item = TestItem::new(cx);
9818 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
9819 item
9820 });
9821 workspace.update_in(cx, |workspace, window, cx| {
9822 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
9823 });
9824 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
9825 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
9826 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
9827 }
9828
9829 #[gpui::test]
9830 async fn test_tracking_active_path(cx: &mut TestAppContext) {
9831 init_test(cx);
9832
9833 let fs = FakeFs::new(cx.executor());
9834 fs.insert_tree(
9835 "/root1",
9836 json!({
9837 "one.txt": "",
9838 "two.txt": "",
9839 }),
9840 )
9841 .await;
9842 fs.insert_tree(
9843 "/root2",
9844 json!({
9845 "three.txt": "",
9846 }),
9847 )
9848 .await;
9849
9850 let project = Project::test(fs, ["root1".as_ref()], cx).await;
9851 let (workspace, cx) =
9852 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9853 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9854 let worktree_id = project.update(cx, |project, cx| {
9855 project.worktrees(cx).next().unwrap().read(cx).id()
9856 });
9857
9858 let item1 = cx.new(|cx| {
9859 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
9860 });
9861 let item2 = cx.new(|cx| {
9862 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
9863 });
9864
9865 // Add an item to an empty pane
9866 workspace.update_in(cx, |workspace, window, cx| {
9867 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
9868 });
9869 project.update(cx, |project, cx| {
9870 assert_eq!(
9871 project.active_entry(),
9872 project
9873 .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
9874 .map(|e| e.id)
9875 );
9876 });
9877 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
9878
9879 // Add a second item to a non-empty pane
9880 workspace.update_in(cx, |workspace, window, cx| {
9881 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
9882 });
9883 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
9884 project.update(cx, |project, cx| {
9885 assert_eq!(
9886 project.active_entry(),
9887 project
9888 .entry_for_path(&(worktree_id, rel_path("two.txt")).into(), cx)
9889 .map(|e| e.id)
9890 );
9891 });
9892
9893 // Close the active item
9894 pane.update_in(cx, |pane, window, cx| {
9895 pane.close_active_item(&Default::default(), window, cx)
9896 })
9897 .await
9898 .unwrap();
9899 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
9900 project.update(cx, |project, cx| {
9901 assert_eq!(
9902 project.active_entry(),
9903 project
9904 .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
9905 .map(|e| e.id)
9906 );
9907 });
9908
9909 // Add a project folder
9910 project
9911 .update(cx, |project, cx| {
9912 project.find_or_create_worktree("root2", true, cx)
9913 })
9914 .await
9915 .unwrap();
9916 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
9917
9918 // Remove a project folder
9919 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
9920 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
9921 }
9922
9923 #[gpui::test]
9924 async fn test_close_window(cx: &mut TestAppContext) {
9925 init_test(cx);
9926
9927 let fs = FakeFs::new(cx.executor());
9928 fs.insert_tree("/root", json!({ "one": "" })).await;
9929
9930 let project = Project::test(fs, ["root".as_ref()], cx).await;
9931 let (workspace, cx) =
9932 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9933
9934 // When there are no dirty items, there's nothing to do.
9935 let item1 = cx.new(TestItem::new);
9936 workspace.update_in(cx, |w, window, cx| {
9937 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
9938 });
9939 let task = workspace.update_in(cx, |w, window, cx| {
9940 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
9941 });
9942 assert!(task.await.unwrap());
9943
9944 // When there are dirty untitled items, prompt to save each one. If the user
9945 // cancels any prompt, then abort.
9946 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
9947 let item3 = cx.new(|cx| {
9948 TestItem::new(cx)
9949 .with_dirty(true)
9950 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
9951 });
9952 workspace.update_in(cx, |w, window, cx| {
9953 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
9954 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
9955 });
9956 let task = workspace.update_in(cx, |w, window, cx| {
9957 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
9958 });
9959 cx.executor().run_until_parked();
9960 cx.simulate_prompt_answer("Cancel"); // cancel save all
9961 cx.executor().run_until_parked();
9962 assert!(!cx.has_pending_prompt());
9963 assert!(!task.await.unwrap());
9964 }
9965
9966 #[gpui::test]
9967 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
9968 init_test(cx);
9969
9970 // Register TestItem as a serializable item
9971 cx.update(|cx| {
9972 register_serializable_item::<TestItem>(cx);
9973 });
9974
9975 let fs = FakeFs::new(cx.executor());
9976 fs.insert_tree("/root", json!({ "one": "" })).await;
9977
9978 let project = Project::test(fs, ["root".as_ref()], cx).await;
9979 let (workspace, cx) =
9980 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9981
9982 // When there are dirty untitled items, but they can serialize, then there is no prompt.
9983 let item1 = cx.new(|cx| {
9984 TestItem::new(cx)
9985 .with_dirty(true)
9986 .with_serialize(|| Some(Task::ready(Ok(()))))
9987 });
9988 let item2 = cx.new(|cx| {
9989 TestItem::new(cx)
9990 .with_dirty(true)
9991 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
9992 .with_serialize(|| Some(Task::ready(Ok(()))))
9993 });
9994 workspace.update_in(cx, |w, window, cx| {
9995 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
9996 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
9997 });
9998 let task = workspace.update_in(cx, |w, window, cx| {
9999 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
10000 });
10001 assert!(task.await.unwrap());
10002 }
10003
10004 #[gpui::test]
10005 async fn test_close_pane_items(cx: &mut TestAppContext) {
10006 init_test(cx);
10007
10008 let fs = FakeFs::new(cx.executor());
10009
10010 let project = Project::test(fs, None, cx).await;
10011 let (workspace, cx) =
10012 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10013
10014 let item1 = cx.new(|cx| {
10015 TestItem::new(cx)
10016 .with_dirty(true)
10017 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
10018 });
10019 let item2 = cx.new(|cx| {
10020 TestItem::new(cx)
10021 .with_dirty(true)
10022 .with_conflict(true)
10023 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
10024 });
10025 let item3 = cx.new(|cx| {
10026 TestItem::new(cx)
10027 .with_dirty(true)
10028 .with_conflict(true)
10029 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
10030 });
10031 let item4 = cx.new(|cx| {
10032 TestItem::new(cx).with_dirty(true).with_project_items(&[{
10033 let project_item = TestProjectItem::new_untitled(cx);
10034 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
10035 project_item
10036 }])
10037 });
10038 let pane = workspace.update_in(cx, |workspace, window, cx| {
10039 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
10040 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10041 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
10042 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
10043 workspace.active_pane().clone()
10044 });
10045
10046 let close_items = pane.update_in(cx, |pane, window, cx| {
10047 pane.activate_item(1, true, true, window, cx);
10048 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
10049 let item1_id = item1.item_id();
10050 let item3_id = item3.item_id();
10051 let item4_id = item4.item_id();
10052 pane.close_items(window, cx, SaveIntent::Close, &move |id| {
10053 [item1_id, item3_id, item4_id].contains(&id)
10054 })
10055 });
10056 cx.executor().run_until_parked();
10057
10058 assert!(cx.has_pending_prompt());
10059 cx.simulate_prompt_answer("Save all");
10060
10061 cx.executor().run_until_parked();
10062
10063 // Item 1 is saved. There's a prompt to save item 3.
10064 pane.update(cx, |pane, cx| {
10065 assert_eq!(item1.read(cx).save_count, 1);
10066 assert_eq!(item1.read(cx).save_as_count, 0);
10067 assert_eq!(item1.read(cx).reload_count, 0);
10068 assert_eq!(pane.items_len(), 3);
10069 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
10070 });
10071 assert!(cx.has_pending_prompt());
10072
10073 // Cancel saving item 3.
10074 cx.simulate_prompt_answer("Discard");
10075 cx.executor().run_until_parked();
10076
10077 // Item 3 is reloaded. There's a prompt to save item 4.
10078 pane.update(cx, |pane, cx| {
10079 assert_eq!(item3.read(cx).save_count, 0);
10080 assert_eq!(item3.read(cx).save_as_count, 0);
10081 assert_eq!(item3.read(cx).reload_count, 1);
10082 assert_eq!(pane.items_len(), 2);
10083 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
10084 });
10085
10086 // There's a prompt for a path for item 4.
10087 cx.simulate_new_path_selection(|_| Some(Default::default()));
10088 close_items.await.unwrap();
10089
10090 // The requested items are closed.
10091 pane.update(cx, |pane, cx| {
10092 assert_eq!(item4.read(cx).save_count, 0);
10093 assert_eq!(item4.read(cx).save_as_count, 1);
10094 assert_eq!(item4.read(cx).reload_count, 0);
10095 assert_eq!(pane.items_len(), 1);
10096 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
10097 });
10098 }
10099
10100 #[gpui::test]
10101 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
10102 init_test(cx);
10103
10104 let fs = FakeFs::new(cx.executor());
10105 let project = Project::test(fs, [], cx).await;
10106 let (workspace, cx) =
10107 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10108
10109 // Create several workspace items with single project entries, and two
10110 // workspace items with multiple project entries.
10111 let single_entry_items = (0..=4)
10112 .map(|project_entry_id| {
10113 cx.new(|cx| {
10114 TestItem::new(cx)
10115 .with_dirty(true)
10116 .with_project_items(&[dirty_project_item(
10117 project_entry_id,
10118 &format!("{project_entry_id}.txt"),
10119 cx,
10120 )])
10121 })
10122 })
10123 .collect::<Vec<_>>();
10124 let item_2_3 = cx.new(|cx| {
10125 TestItem::new(cx)
10126 .with_dirty(true)
10127 .with_buffer_kind(ItemBufferKind::Multibuffer)
10128 .with_project_items(&[
10129 single_entry_items[2].read(cx).project_items[0].clone(),
10130 single_entry_items[3].read(cx).project_items[0].clone(),
10131 ])
10132 });
10133 let item_3_4 = cx.new(|cx| {
10134 TestItem::new(cx)
10135 .with_dirty(true)
10136 .with_buffer_kind(ItemBufferKind::Multibuffer)
10137 .with_project_items(&[
10138 single_entry_items[3].read(cx).project_items[0].clone(),
10139 single_entry_items[4].read(cx).project_items[0].clone(),
10140 ])
10141 });
10142
10143 // Create two panes that contain the following project entries:
10144 // left pane:
10145 // multi-entry items: (2, 3)
10146 // single-entry items: 0, 2, 3, 4
10147 // right pane:
10148 // single-entry items: 4, 1
10149 // multi-entry items: (3, 4)
10150 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
10151 let left_pane = workspace.active_pane().clone();
10152 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
10153 workspace.add_item_to_active_pane(
10154 single_entry_items[0].boxed_clone(),
10155 None,
10156 true,
10157 window,
10158 cx,
10159 );
10160 workspace.add_item_to_active_pane(
10161 single_entry_items[2].boxed_clone(),
10162 None,
10163 true,
10164 window,
10165 cx,
10166 );
10167 workspace.add_item_to_active_pane(
10168 single_entry_items[3].boxed_clone(),
10169 None,
10170 true,
10171 window,
10172 cx,
10173 );
10174 workspace.add_item_to_active_pane(
10175 single_entry_items[4].boxed_clone(),
10176 None,
10177 true,
10178 window,
10179 cx,
10180 );
10181
10182 let right_pane =
10183 workspace.split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx);
10184
10185 let boxed_clone = single_entry_items[1].boxed_clone();
10186 let right_pane = window.spawn(cx, async move |cx| {
10187 right_pane.await.inspect(|right_pane| {
10188 right_pane
10189 .update_in(cx, |pane, window, cx| {
10190 pane.add_item(boxed_clone, true, true, None, window, cx);
10191 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
10192 })
10193 .unwrap();
10194 })
10195 });
10196
10197 (left_pane, right_pane)
10198 });
10199 let right_pane = right_pane.await.unwrap();
10200 cx.focus(&right_pane);
10201
10202 let close = right_pane.update_in(cx, |pane, window, cx| {
10203 pane.close_all_items(&CloseAllItems::default(), window, cx)
10204 .unwrap()
10205 });
10206 cx.executor().run_until_parked();
10207
10208 let msg = cx.pending_prompt().unwrap().0;
10209 assert!(msg.contains("1.txt"));
10210 assert!(!msg.contains("2.txt"));
10211 assert!(!msg.contains("3.txt"));
10212 assert!(!msg.contains("4.txt"));
10213
10214 // With best-effort close, cancelling item 1 keeps it open but items 4
10215 // and (3,4) still close since their entries exist in left pane.
10216 cx.simulate_prompt_answer("Cancel");
10217 close.await;
10218
10219 right_pane.read_with(cx, |pane, _| {
10220 assert_eq!(pane.items_len(), 1);
10221 });
10222
10223 // Remove item 3 from left pane, making (2,3) the only item with entry 3.
10224 left_pane
10225 .update_in(cx, |left_pane, window, cx| {
10226 left_pane.close_item_by_id(
10227 single_entry_items[3].entity_id(),
10228 SaveIntent::Skip,
10229 window,
10230 cx,
10231 )
10232 })
10233 .await
10234 .unwrap();
10235
10236 let close = left_pane.update_in(cx, |pane, window, cx| {
10237 pane.close_all_items(&CloseAllItems::default(), window, cx)
10238 .unwrap()
10239 });
10240 cx.executor().run_until_parked();
10241
10242 let details = cx.pending_prompt().unwrap().1;
10243 assert!(details.contains("0.txt"));
10244 assert!(details.contains("3.txt"));
10245 assert!(details.contains("4.txt"));
10246 // Ideally 2.txt wouldn't appear since entry 2 still exists in item 2.
10247 // But we can only save whole items, so saving (2,3) for entry 3 includes 2.
10248 // assert!(!details.contains("2.txt"));
10249
10250 cx.simulate_prompt_answer("Save all");
10251 cx.executor().run_until_parked();
10252 close.await;
10253
10254 left_pane.read_with(cx, |pane, _| {
10255 assert_eq!(pane.items_len(), 0);
10256 });
10257 }
10258
10259 #[gpui::test]
10260 async fn test_autosave(cx: &mut gpui::TestAppContext) {
10261 init_test(cx);
10262
10263 let fs = FakeFs::new(cx.executor());
10264 let project = Project::test(fs, [], cx).await;
10265 let (workspace, cx) =
10266 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10267 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10268
10269 let item = cx.new(|cx| {
10270 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10271 });
10272 let item_id = item.entity_id();
10273 workspace.update_in(cx, |workspace, window, cx| {
10274 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
10275 });
10276
10277 // Autosave on window change.
10278 item.update(cx, |item, cx| {
10279 SettingsStore::update_global(cx, |settings, cx| {
10280 settings.update_user_settings(cx, |settings| {
10281 settings.workspace.autosave = Some(AutosaveSetting::OnWindowChange);
10282 })
10283 });
10284 item.is_dirty = true;
10285 });
10286
10287 // Deactivating the window saves the file.
10288 cx.deactivate_window();
10289 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
10290
10291 // Re-activating the window doesn't save the file.
10292 cx.update(|window, _| window.activate_window());
10293 cx.executor().run_until_parked();
10294 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
10295
10296 // Autosave on focus change.
10297 item.update_in(cx, |item, window, cx| {
10298 cx.focus_self(window);
10299 SettingsStore::update_global(cx, |settings, cx| {
10300 settings.update_user_settings(cx, |settings| {
10301 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
10302 })
10303 });
10304 item.is_dirty = true;
10305 });
10306 // Blurring the item saves the file.
10307 item.update_in(cx, |_, window, _| window.blur());
10308 cx.executor().run_until_parked();
10309 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
10310
10311 // Deactivating the window still saves the file.
10312 item.update_in(cx, |item, window, cx| {
10313 cx.focus_self(window);
10314 item.is_dirty = true;
10315 });
10316 cx.deactivate_window();
10317 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
10318
10319 // Autosave after delay.
10320 item.update(cx, |item, cx| {
10321 SettingsStore::update_global(cx, |settings, cx| {
10322 settings.update_user_settings(cx, |settings| {
10323 settings.workspace.autosave = Some(AutosaveSetting::AfterDelay {
10324 milliseconds: 500.into(),
10325 });
10326 })
10327 });
10328 item.is_dirty = true;
10329 cx.emit(ItemEvent::Edit);
10330 });
10331
10332 // Delay hasn't fully expired, so the file is still dirty and unsaved.
10333 cx.executor().advance_clock(Duration::from_millis(250));
10334 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
10335
10336 // After delay expires, the file is saved.
10337 cx.executor().advance_clock(Duration::from_millis(250));
10338 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
10339
10340 // Autosave after delay, should save earlier than delay if tab is closed
10341 item.update(cx, |item, cx| {
10342 item.is_dirty = true;
10343 cx.emit(ItemEvent::Edit);
10344 });
10345 cx.executor().advance_clock(Duration::from_millis(250));
10346 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
10347
10348 // // Ensure auto save with delay saves the item on close, even if the timer hasn't yet run out.
10349 pane.update_in(cx, |pane, window, cx| {
10350 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
10351 })
10352 .await
10353 .unwrap();
10354 assert!(!cx.has_pending_prompt());
10355 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
10356
10357 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
10358 workspace.update_in(cx, |workspace, window, cx| {
10359 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
10360 });
10361 item.update_in(cx, |item, _window, cx| {
10362 item.is_dirty = true;
10363 for project_item in &mut item.project_items {
10364 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
10365 }
10366 });
10367 cx.run_until_parked();
10368 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
10369
10370 // Autosave on focus change, ensuring closing the tab counts as such.
10371 item.update(cx, |item, cx| {
10372 SettingsStore::update_global(cx, |settings, cx| {
10373 settings.update_user_settings(cx, |settings| {
10374 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
10375 })
10376 });
10377 item.is_dirty = true;
10378 for project_item in &mut item.project_items {
10379 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
10380 }
10381 });
10382
10383 pane.update_in(cx, |pane, window, cx| {
10384 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
10385 })
10386 .await
10387 .unwrap();
10388 assert!(!cx.has_pending_prompt());
10389 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
10390
10391 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
10392 workspace.update_in(cx, |workspace, window, cx| {
10393 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
10394 });
10395 item.update_in(cx, |item, window, cx| {
10396 item.project_items[0].update(cx, |item, _| {
10397 item.entry_id = None;
10398 });
10399 item.is_dirty = true;
10400 window.blur();
10401 });
10402 cx.run_until_parked();
10403 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
10404
10405 // Ensure autosave is prevented for deleted files also when closing the buffer.
10406 let _close_items = pane.update_in(cx, |pane, window, cx| {
10407 pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
10408 });
10409 cx.run_until_parked();
10410 assert!(cx.has_pending_prompt());
10411 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
10412 }
10413
10414 #[gpui::test]
10415 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
10416 init_test(cx);
10417
10418 let fs = FakeFs::new(cx.executor());
10419
10420 let project = Project::test(fs, [], cx).await;
10421 let (workspace, cx) =
10422 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10423
10424 let item = cx.new(|cx| {
10425 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10426 });
10427 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10428 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
10429 let toolbar_notify_count = Rc::new(RefCell::new(0));
10430
10431 workspace.update_in(cx, |workspace, window, cx| {
10432 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
10433 let toolbar_notification_count = toolbar_notify_count.clone();
10434 cx.observe_in(&toolbar, window, move |_, _, _, _| {
10435 *toolbar_notification_count.borrow_mut() += 1
10436 })
10437 .detach();
10438 });
10439
10440 pane.read_with(cx, |pane, _| {
10441 assert!(!pane.can_navigate_backward());
10442 assert!(!pane.can_navigate_forward());
10443 });
10444
10445 item.update_in(cx, |item, _, cx| {
10446 item.set_state("one".to_string(), cx);
10447 });
10448
10449 // Toolbar must be notified to re-render the navigation buttons
10450 assert_eq!(*toolbar_notify_count.borrow(), 1);
10451
10452 pane.read_with(cx, |pane, _| {
10453 assert!(pane.can_navigate_backward());
10454 assert!(!pane.can_navigate_forward());
10455 });
10456
10457 workspace
10458 .update_in(cx, |workspace, window, cx| {
10459 workspace.go_back(pane.downgrade(), window, cx)
10460 })
10461 .await
10462 .unwrap();
10463
10464 assert_eq!(*toolbar_notify_count.borrow(), 2);
10465 pane.read_with(cx, |pane, _| {
10466 assert!(!pane.can_navigate_backward());
10467 assert!(pane.can_navigate_forward());
10468 });
10469 }
10470
10471 #[gpui::test]
10472 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
10473 init_test(cx);
10474 let fs = FakeFs::new(cx.executor());
10475
10476 let project = Project::test(fs, [], cx).await;
10477 let (workspace, cx) =
10478 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10479
10480 let panel = workspace.update_in(cx, |workspace, window, cx| {
10481 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
10482 workspace.add_panel(panel.clone(), window, cx);
10483
10484 workspace
10485 .right_dock()
10486 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
10487
10488 panel
10489 });
10490
10491 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10492 pane.update_in(cx, |pane, window, cx| {
10493 let item = cx.new(TestItem::new);
10494 pane.add_item(Box::new(item), true, true, None, window, cx);
10495 });
10496
10497 // Transfer focus from center to panel
10498 workspace.update_in(cx, |workspace, window, cx| {
10499 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10500 });
10501
10502 workspace.update_in(cx, |workspace, window, cx| {
10503 assert!(workspace.right_dock().read(cx).is_open());
10504 assert!(!panel.is_zoomed(window, cx));
10505 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10506 });
10507
10508 // Transfer focus from panel to center
10509 workspace.update_in(cx, |workspace, window, cx| {
10510 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10511 });
10512
10513 workspace.update_in(cx, |workspace, window, cx| {
10514 assert!(workspace.right_dock().read(cx).is_open());
10515 assert!(!panel.is_zoomed(window, cx));
10516 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10517 });
10518
10519 // Close the dock
10520 workspace.update_in(cx, |workspace, window, cx| {
10521 workspace.toggle_dock(DockPosition::Right, window, cx);
10522 });
10523
10524 workspace.update_in(cx, |workspace, window, cx| {
10525 assert!(!workspace.right_dock().read(cx).is_open());
10526 assert!(!panel.is_zoomed(window, cx));
10527 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10528 });
10529
10530 // Open the dock
10531 workspace.update_in(cx, |workspace, window, cx| {
10532 workspace.toggle_dock(DockPosition::Right, window, cx);
10533 });
10534
10535 workspace.update_in(cx, |workspace, window, cx| {
10536 assert!(workspace.right_dock().read(cx).is_open());
10537 assert!(!panel.is_zoomed(window, cx));
10538 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10539 });
10540
10541 // Focus and zoom panel
10542 panel.update_in(cx, |panel, window, cx| {
10543 cx.focus_self(window);
10544 panel.set_zoomed(true, window, cx)
10545 });
10546
10547 workspace.update_in(cx, |workspace, window, cx| {
10548 assert!(workspace.right_dock().read(cx).is_open());
10549 assert!(panel.is_zoomed(window, cx));
10550 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10551 });
10552
10553 // Transfer focus to the center closes the dock
10554 workspace.update_in(cx, |workspace, window, cx| {
10555 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10556 });
10557
10558 workspace.update_in(cx, |workspace, window, cx| {
10559 assert!(!workspace.right_dock().read(cx).is_open());
10560 assert!(panel.is_zoomed(window, cx));
10561 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10562 });
10563
10564 // Transferring focus back to the panel keeps it zoomed
10565 workspace.update_in(cx, |workspace, window, cx| {
10566 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10567 });
10568
10569 workspace.update_in(cx, |workspace, window, cx| {
10570 assert!(workspace.right_dock().read(cx).is_open());
10571 assert!(panel.is_zoomed(window, cx));
10572 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10573 });
10574
10575 // Close the dock while it is zoomed
10576 workspace.update_in(cx, |workspace, window, cx| {
10577 workspace.toggle_dock(DockPosition::Right, window, cx)
10578 });
10579
10580 workspace.update_in(cx, |workspace, window, cx| {
10581 assert!(!workspace.right_dock().read(cx).is_open());
10582 assert!(panel.is_zoomed(window, cx));
10583 assert!(workspace.zoomed.is_none());
10584 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10585 });
10586
10587 // Opening the dock, when it's zoomed, retains focus
10588 workspace.update_in(cx, |workspace, window, cx| {
10589 workspace.toggle_dock(DockPosition::Right, window, cx)
10590 });
10591
10592 workspace.update_in(cx, |workspace, window, cx| {
10593 assert!(workspace.right_dock().read(cx).is_open());
10594 assert!(panel.is_zoomed(window, cx));
10595 assert!(workspace.zoomed.is_some());
10596 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10597 });
10598
10599 // Unzoom and close the panel, zoom the active pane.
10600 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
10601 workspace.update_in(cx, |workspace, window, cx| {
10602 workspace.toggle_dock(DockPosition::Right, window, cx)
10603 });
10604 pane.update_in(cx, |pane, window, cx| {
10605 pane.toggle_zoom(&Default::default(), window, cx)
10606 });
10607
10608 // Opening a dock unzooms the pane.
10609 workspace.update_in(cx, |workspace, window, cx| {
10610 workspace.toggle_dock(DockPosition::Right, window, cx)
10611 });
10612 workspace.update_in(cx, |workspace, window, cx| {
10613 let pane = pane.read(cx);
10614 assert!(!pane.is_zoomed());
10615 assert!(!pane.focus_handle(cx).is_focused(window));
10616 assert!(workspace.right_dock().read(cx).is_open());
10617 assert!(workspace.zoomed.is_none());
10618 });
10619 }
10620
10621 #[gpui::test]
10622 async fn test_close_panel_on_toggle(cx: &mut gpui::TestAppContext) {
10623 init_test(cx);
10624 let fs = FakeFs::new(cx.executor());
10625
10626 let project = Project::test(fs, [], cx).await;
10627 let (workspace, cx) =
10628 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10629
10630 let panel = workspace.update_in(cx, |workspace, window, cx| {
10631 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
10632 workspace.add_panel(panel.clone(), window, cx);
10633 panel
10634 });
10635
10636 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10637 pane.update_in(cx, |pane, window, cx| {
10638 let item = cx.new(TestItem::new);
10639 pane.add_item(Box::new(item), true, true, None, window, cx);
10640 });
10641
10642 // Enable close_panel_on_toggle
10643 cx.update_global(|store: &mut SettingsStore, cx| {
10644 store.update_user_settings(cx, |settings| {
10645 settings.workspace.close_panel_on_toggle = Some(true);
10646 });
10647 });
10648
10649 // Panel starts closed. Toggling should open and focus it.
10650 workspace.update_in(cx, |workspace, window, cx| {
10651 assert!(!workspace.right_dock().read(cx).is_open());
10652 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10653 });
10654
10655 workspace.update_in(cx, |workspace, window, cx| {
10656 assert!(
10657 workspace.right_dock().read(cx).is_open(),
10658 "Dock should be open after toggling from center"
10659 );
10660 assert!(
10661 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
10662 "Panel should be focused after toggling from center"
10663 );
10664 });
10665
10666 // Panel is open and focused. Toggling should close the panel and
10667 // return focus to the center.
10668 workspace.update_in(cx, |workspace, window, cx| {
10669 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10670 });
10671
10672 workspace.update_in(cx, |workspace, window, cx| {
10673 assert!(
10674 !workspace.right_dock().read(cx).is_open(),
10675 "Dock should be closed after toggling from focused panel"
10676 );
10677 assert!(
10678 !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
10679 "Panel should not be focused after toggling from focused panel"
10680 );
10681 });
10682
10683 // Open the dock and focus something else so the panel is open but not
10684 // focused. Toggling should focus the panel (not close it).
10685 workspace.update_in(cx, |workspace, window, cx| {
10686 workspace
10687 .right_dock()
10688 .update(cx, |dock, cx| dock.set_open(true, window, cx));
10689 window.focus(&pane.read(cx).focus_handle(cx), cx);
10690 });
10691
10692 workspace.update_in(cx, |workspace, window, cx| {
10693 assert!(workspace.right_dock().read(cx).is_open());
10694 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10695 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10696 });
10697
10698 workspace.update_in(cx, |workspace, window, cx| {
10699 assert!(
10700 workspace.right_dock().read(cx).is_open(),
10701 "Dock should remain open when toggling focuses an open-but-unfocused panel"
10702 );
10703 assert!(
10704 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
10705 "Panel should be focused after toggling an open-but-unfocused panel"
10706 );
10707 });
10708
10709 // Now disable the setting and verify the original behavior: toggling
10710 // from a focused panel moves focus to center but leaves the dock open.
10711 cx.update_global(|store: &mut SettingsStore, cx| {
10712 store.update_user_settings(cx, |settings| {
10713 settings.workspace.close_panel_on_toggle = Some(false);
10714 });
10715 });
10716
10717 workspace.update_in(cx, |workspace, window, cx| {
10718 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10719 });
10720
10721 workspace.update_in(cx, |workspace, window, cx| {
10722 assert!(
10723 workspace.right_dock().read(cx).is_open(),
10724 "Dock should remain open when setting is disabled"
10725 );
10726 assert!(
10727 !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
10728 "Panel should not be focused after toggling with setting disabled"
10729 );
10730 });
10731 }
10732
10733 #[gpui::test]
10734 async fn test_pane_zoom_in_out(cx: &mut TestAppContext) {
10735 init_test(cx);
10736 let fs = FakeFs::new(cx.executor());
10737
10738 let project = Project::test(fs, [], cx).await;
10739 let (workspace, cx) =
10740 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10741
10742 let pane = workspace.update_in(cx, |workspace, _window, _cx| {
10743 workspace.active_pane().clone()
10744 });
10745
10746 // Add an item to the pane so it can be zoomed
10747 workspace.update_in(cx, |workspace, window, cx| {
10748 let item = cx.new(TestItem::new);
10749 workspace.add_item(pane.clone(), Box::new(item), None, true, true, window, cx);
10750 });
10751
10752 // Initially not zoomed
10753 workspace.update_in(cx, |workspace, _window, cx| {
10754 assert!(!pane.read(cx).is_zoomed(), "Pane starts unzoomed");
10755 assert!(
10756 workspace.zoomed.is_none(),
10757 "Workspace should track no zoomed pane"
10758 );
10759 assert!(pane.read(cx).items_len() > 0, "Pane should have items");
10760 });
10761
10762 // Zoom In
10763 pane.update_in(cx, |pane, window, cx| {
10764 pane.zoom_in(&crate::ZoomIn, window, cx);
10765 });
10766
10767 workspace.update_in(cx, |workspace, window, cx| {
10768 assert!(
10769 pane.read(cx).is_zoomed(),
10770 "Pane should be zoomed after ZoomIn"
10771 );
10772 assert!(
10773 workspace.zoomed.is_some(),
10774 "Workspace should track the zoomed pane"
10775 );
10776 assert!(
10777 pane.read(cx).focus_handle(cx).contains_focused(window, cx),
10778 "ZoomIn should focus the pane"
10779 );
10780 });
10781
10782 // Zoom In again is a no-op
10783 pane.update_in(cx, |pane, window, cx| {
10784 pane.zoom_in(&crate::ZoomIn, window, cx);
10785 });
10786
10787 workspace.update_in(cx, |workspace, window, cx| {
10788 assert!(pane.read(cx).is_zoomed(), "Second ZoomIn keeps pane zoomed");
10789 assert!(
10790 workspace.zoomed.is_some(),
10791 "Workspace still tracks zoomed pane"
10792 );
10793 assert!(
10794 pane.read(cx).focus_handle(cx).contains_focused(window, cx),
10795 "Pane remains focused after repeated ZoomIn"
10796 );
10797 });
10798
10799 // Zoom Out
10800 pane.update_in(cx, |pane, window, cx| {
10801 pane.zoom_out(&crate::ZoomOut, window, cx);
10802 });
10803
10804 workspace.update_in(cx, |workspace, _window, cx| {
10805 assert!(
10806 !pane.read(cx).is_zoomed(),
10807 "Pane should unzoom after ZoomOut"
10808 );
10809 assert!(
10810 workspace.zoomed.is_none(),
10811 "Workspace clears zoom tracking after ZoomOut"
10812 );
10813 });
10814
10815 // Zoom Out again is a no-op
10816 pane.update_in(cx, |pane, window, cx| {
10817 pane.zoom_out(&crate::ZoomOut, window, cx);
10818 });
10819
10820 workspace.update_in(cx, |workspace, _window, cx| {
10821 assert!(
10822 !pane.read(cx).is_zoomed(),
10823 "Second ZoomOut keeps pane unzoomed"
10824 );
10825 assert!(
10826 workspace.zoomed.is_none(),
10827 "Workspace remains without zoomed pane"
10828 );
10829 });
10830 }
10831
10832 #[gpui::test]
10833 async fn test_toggle_all_docks(cx: &mut gpui::TestAppContext) {
10834 init_test(cx);
10835 let fs = FakeFs::new(cx.executor());
10836
10837 let project = Project::test(fs, [], cx).await;
10838 let (workspace, cx) =
10839 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10840 workspace.update_in(cx, |workspace, window, cx| {
10841 // Open two docks
10842 let left_dock = workspace.dock_at_position(DockPosition::Left);
10843 let right_dock = workspace.dock_at_position(DockPosition::Right);
10844
10845 left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
10846 right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
10847
10848 assert!(left_dock.read(cx).is_open());
10849 assert!(right_dock.read(cx).is_open());
10850 });
10851
10852 workspace.update_in(cx, |workspace, window, cx| {
10853 // Toggle all docks - should close both
10854 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
10855
10856 let left_dock = workspace.dock_at_position(DockPosition::Left);
10857 let right_dock = workspace.dock_at_position(DockPosition::Right);
10858 assert!(!left_dock.read(cx).is_open());
10859 assert!(!right_dock.read(cx).is_open());
10860 });
10861
10862 workspace.update_in(cx, |workspace, window, cx| {
10863 // Toggle again - should reopen both
10864 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
10865
10866 let left_dock = workspace.dock_at_position(DockPosition::Left);
10867 let right_dock = workspace.dock_at_position(DockPosition::Right);
10868 assert!(left_dock.read(cx).is_open());
10869 assert!(right_dock.read(cx).is_open());
10870 });
10871 }
10872
10873 #[gpui::test]
10874 async fn test_toggle_all_with_manual_close(cx: &mut gpui::TestAppContext) {
10875 init_test(cx);
10876 let fs = FakeFs::new(cx.executor());
10877
10878 let project = Project::test(fs, [], cx).await;
10879 let (workspace, cx) =
10880 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10881 workspace.update_in(cx, |workspace, window, cx| {
10882 // Open two docks
10883 let left_dock = workspace.dock_at_position(DockPosition::Left);
10884 let right_dock = workspace.dock_at_position(DockPosition::Right);
10885
10886 left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
10887 right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
10888
10889 assert!(left_dock.read(cx).is_open());
10890 assert!(right_dock.read(cx).is_open());
10891 });
10892
10893 workspace.update_in(cx, |workspace, window, cx| {
10894 // Close them manually
10895 workspace.toggle_dock(DockPosition::Left, window, cx);
10896 workspace.toggle_dock(DockPosition::Right, window, cx);
10897
10898 let left_dock = workspace.dock_at_position(DockPosition::Left);
10899 let right_dock = workspace.dock_at_position(DockPosition::Right);
10900 assert!(!left_dock.read(cx).is_open());
10901 assert!(!right_dock.read(cx).is_open());
10902 });
10903
10904 workspace.update_in(cx, |workspace, window, cx| {
10905 // Toggle all docks - only last closed (right dock) should reopen
10906 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
10907
10908 let left_dock = workspace.dock_at_position(DockPosition::Left);
10909 let right_dock = workspace.dock_at_position(DockPosition::Right);
10910 assert!(!left_dock.read(cx).is_open());
10911 assert!(right_dock.read(cx).is_open());
10912 });
10913 }
10914
10915 #[gpui::test]
10916 async fn test_toggle_all_docks_after_dock_move(cx: &mut gpui::TestAppContext) {
10917 init_test(cx);
10918 let fs = FakeFs::new(cx.executor());
10919 let project = Project::test(fs, [], cx).await;
10920 let (workspace, cx) =
10921 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10922
10923 // Open two docks (left and right) with one panel each
10924 let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| {
10925 let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
10926 workspace.add_panel(left_panel.clone(), window, cx);
10927
10928 let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
10929 workspace.add_panel(right_panel.clone(), window, cx);
10930
10931 workspace.toggle_dock(DockPosition::Left, window, cx);
10932 workspace.toggle_dock(DockPosition::Right, window, cx);
10933
10934 // Verify initial state
10935 assert!(
10936 workspace.left_dock().read(cx).is_open(),
10937 "Left dock should be open"
10938 );
10939 assert_eq!(
10940 workspace
10941 .left_dock()
10942 .read(cx)
10943 .visible_panel()
10944 .unwrap()
10945 .panel_id(),
10946 left_panel.panel_id(),
10947 "Left panel should be visible in left dock"
10948 );
10949 assert!(
10950 workspace.right_dock().read(cx).is_open(),
10951 "Right dock should be open"
10952 );
10953 assert_eq!(
10954 workspace
10955 .right_dock()
10956 .read(cx)
10957 .visible_panel()
10958 .unwrap()
10959 .panel_id(),
10960 right_panel.panel_id(),
10961 "Right panel should be visible in right dock"
10962 );
10963 assert!(
10964 !workspace.bottom_dock().read(cx).is_open(),
10965 "Bottom dock should be closed"
10966 );
10967
10968 (left_panel, right_panel)
10969 });
10970
10971 // Focus the left panel and move it to the next position (bottom dock)
10972 workspace.update_in(cx, |workspace, window, cx| {
10973 workspace.toggle_panel_focus::<TestPanel>(window, cx); // Focus left panel
10974 assert!(
10975 left_panel.read(cx).focus_handle(cx).is_focused(window),
10976 "Left panel should be focused"
10977 );
10978 });
10979
10980 cx.dispatch_action(MoveFocusedPanelToNextPosition);
10981
10982 // Verify the left panel has moved to the bottom dock, and the bottom dock is now open
10983 workspace.update(cx, |workspace, cx| {
10984 assert!(
10985 !workspace.left_dock().read(cx).is_open(),
10986 "Left dock should be closed"
10987 );
10988 assert!(
10989 workspace.bottom_dock().read(cx).is_open(),
10990 "Bottom dock should now be open"
10991 );
10992 assert_eq!(
10993 left_panel.read(cx).position,
10994 DockPosition::Bottom,
10995 "Left panel should now be in the bottom dock"
10996 );
10997 assert_eq!(
10998 workspace
10999 .bottom_dock()
11000 .read(cx)
11001 .visible_panel()
11002 .unwrap()
11003 .panel_id(),
11004 left_panel.panel_id(),
11005 "Left panel should be the visible panel in the bottom dock"
11006 );
11007 });
11008
11009 // Toggle all docks off
11010 workspace.update_in(cx, |workspace, window, cx| {
11011 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11012 assert!(
11013 !workspace.left_dock().read(cx).is_open(),
11014 "Left dock should be closed"
11015 );
11016 assert!(
11017 !workspace.right_dock().read(cx).is_open(),
11018 "Right dock should be closed"
11019 );
11020 assert!(
11021 !workspace.bottom_dock().read(cx).is_open(),
11022 "Bottom dock should be closed"
11023 );
11024 });
11025
11026 // Toggle all docks back on and verify positions are restored
11027 workspace.update_in(cx, |workspace, window, cx| {
11028 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11029 assert!(
11030 !workspace.left_dock().read(cx).is_open(),
11031 "Left dock should remain closed"
11032 );
11033 assert!(
11034 workspace.right_dock().read(cx).is_open(),
11035 "Right dock should remain open"
11036 );
11037 assert!(
11038 workspace.bottom_dock().read(cx).is_open(),
11039 "Bottom dock should remain open"
11040 );
11041 assert_eq!(
11042 left_panel.read(cx).position,
11043 DockPosition::Bottom,
11044 "Left panel should remain in the bottom dock"
11045 );
11046 assert_eq!(
11047 right_panel.read(cx).position,
11048 DockPosition::Right,
11049 "Right panel should remain in the right dock"
11050 );
11051 assert_eq!(
11052 workspace
11053 .bottom_dock()
11054 .read(cx)
11055 .visible_panel()
11056 .unwrap()
11057 .panel_id(),
11058 left_panel.panel_id(),
11059 "Left panel should be the visible panel in the right dock"
11060 );
11061 });
11062 }
11063
11064 #[gpui::test]
11065 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
11066 init_test(cx);
11067
11068 let fs = FakeFs::new(cx.executor());
11069
11070 let project = Project::test(fs, None, cx).await;
11071 let (workspace, cx) =
11072 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11073
11074 // Let's arrange the panes like this:
11075 //
11076 // +-----------------------+
11077 // | top |
11078 // +------+--------+-------+
11079 // | left | center | right |
11080 // +------+--------+-------+
11081 // | bottom |
11082 // +-----------------------+
11083
11084 let top_item = cx.new(|cx| {
11085 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
11086 });
11087 let bottom_item = cx.new(|cx| {
11088 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
11089 });
11090 let left_item = cx.new(|cx| {
11091 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
11092 });
11093 let right_item = cx.new(|cx| {
11094 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
11095 });
11096 let center_item = cx.new(|cx| {
11097 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
11098 });
11099
11100 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11101 let top_pane_id = workspace.active_pane().entity_id();
11102 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
11103 workspace.split_pane(
11104 workspace.active_pane().clone(),
11105 SplitDirection::Down,
11106 window,
11107 cx,
11108 );
11109 top_pane_id
11110 });
11111 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11112 let bottom_pane_id = workspace.active_pane().entity_id();
11113 workspace.add_item_to_active_pane(
11114 Box::new(bottom_item.clone()),
11115 None,
11116 false,
11117 window,
11118 cx,
11119 );
11120 workspace.split_pane(
11121 workspace.active_pane().clone(),
11122 SplitDirection::Up,
11123 window,
11124 cx,
11125 );
11126 bottom_pane_id
11127 });
11128 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11129 let left_pane_id = workspace.active_pane().entity_id();
11130 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
11131 workspace.split_pane(
11132 workspace.active_pane().clone(),
11133 SplitDirection::Right,
11134 window,
11135 cx,
11136 );
11137 left_pane_id
11138 });
11139 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11140 let right_pane_id = workspace.active_pane().entity_id();
11141 workspace.add_item_to_active_pane(
11142 Box::new(right_item.clone()),
11143 None,
11144 false,
11145 window,
11146 cx,
11147 );
11148 workspace.split_pane(
11149 workspace.active_pane().clone(),
11150 SplitDirection::Left,
11151 window,
11152 cx,
11153 );
11154 right_pane_id
11155 });
11156 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11157 let center_pane_id = workspace.active_pane().entity_id();
11158 workspace.add_item_to_active_pane(
11159 Box::new(center_item.clone()),
11160 None,
11161 false,
11162 window,
11163 cx,
11164 );
11165 center_pane_id
11166 });
11167 cx.executor().run_until_parked();
11168
11169 workspace.update_in(cx, |workspace, window, cx| {
11170 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
11171
11172 // Join into next from center pane into right
11173 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
11174 });
11175
11176 workspace.update_in(cx, |workspace, window, cx| {
11177 let active_pane = workspace.active_pane();
11178 assert_eq!(right_pane_id, active_pane.entity_id());
11179 assert_eq!(2, active_pane.read(cx).items_len());
11180 let item_ids_in_pane =
11181 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
11182 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
11183 assert!(item_ids_in_pane.contains(&right_item.item_id()));
11184
11185 // Join into next from right pane into bottom
11186 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
11187 });
11188
11189 workspace.update_in(cx, |workspace, window, cx| {
11190 let active_pane = workspace.active_pane();
11191 assert_eq!(bottom_pane_id, active_pane.entity_id());
11192 assert_eq!(3, active_pane.read(cx).items_len());
11193 let item_ids_in_pane =
11194 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
11195 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
11196 assert!(item_ids_in_pane.contains(&right_item.item_id()));
11197 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
11198
11199 // Join into next from bottom pane into left
11200 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
11201 });
11202
11203 workspace.update_in(cx, |workspace, window, cx| {
11204 let active_pane = workspace.active_pane();
11205 assert_eq!(left_pane_id, active_pane.entity_id());
11206 assert_eq!(4, active_pane.read(cx).items_len());
11207 let item_ids_in_pane =
11208 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
11209 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
11210 assert!(item_ids_in_pane.contains(&right_item.item_id()));
11211 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
11212 assert!(item_ids_in_pane.contains(&left_item.item_id()));
11213
11214 // Join into next from left pane into top
11215 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
11216 });
11217
11218 workspace.update_in(cx, |workspace, window, cx| {
11219 let active_pane = workspace.active_pane();
11220 assert_eq!(top_pane_id, active_pane.entity_id());
11221 assert_eq!(5, active_pane.read(cx).items_len());
11222 let item_ids_in_pane =
11223 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
11224 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
11225 assert!(item_ids_in_pane.contains(&right_item.item_id()));
11226 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
11227 assert!(item_ids_in_pane.contains(&left_item.item_id()));
11228 assert!(item_ids_in_pane.contains(&top_item.item_id()));
11229
11230 // Single pane left: no-op
11231 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
11232 });
11233
11234 workspace.update(cx, |workspace, _cx| {
11235 let active_pane = workspace.active_pane();
11236 assert_eq!(top_pane_id, active_pane.entity_id());
11237 });
11238 }
11239
11240 fn add_an_item_to_active_pane(
11241 cx: &mut VisualTestContext,
11242 workspace: &Entity<Workspace>,
11243 item_id: u64,
11244 ) -> Entity<TestItem> {
11245 let item = cx.new(|cx| {
11246 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
11247 item_id,
11248 "item{item_id}.txt",
11249 cx,
11250 )])
11251 });
11252 workspace.update_in(cx, |workspace, window, cx| {
11253 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
11254 });
11255 item
11256 }
11257
11258 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
11259 workspace.update_in(cx, |workspace, window, cx| {
11260 workspace.split_pane(
11261 workspace.active_pane().clone(),
11262 SplitDirection::Right,
11263 window,
11264 cx,
11265 )
11266 })
11267 }
11268
11269 #[gpui::test]
11270 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
11271 init_test(cx);
11272 let fs = FakeFs::new(cx.executor());
11273 let project = Project::test(fs, None, cx).await;
11274 let (workspace, cx) =
11275 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11276
11277 add_an_item_to_active_pane(cx, &workspace, 1);
11278 split_pane(cx, &workspace);
11279 add_an_item_to_active_pane(cx, &workspace, 2);
11280 split_pane(cx, &workspace); // empty pane
11281 split_pane(cx, &workspace);
11282 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
11283
11284 cx.executor().run_until_parked();
11285
11286 workspace.update(cx, |workspace, cx| {
11287 let num_panes = workspace.panes().len();
11288 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
11289 let active_item = workspace
11290 .active_pane()
11291 .read(cx)
11292 .active_item()
11293 .expect("item is in focus");
11294
11295 assert_eq!(num_panes, 4);
11296 assert_eq!(num_items_in_current_pane, 1);
11297 assert_eq!(active_item.item_id(), last_item.item_id());
11298 });
11299
11300 workspace.update_in(cx, |workspace, window, cx| {
11301 workspace.join_all_panes(window, cx);
11302 });
11303
11304 workspace.update(cx, |workspace, cx| {
11305 let num_panes = workspace.panes().len();
11306 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
11307 let active_item = workspace
11308 .active_pane()
11309 .read(cx)
11310 .active_item()
11311 .expect("item is in focus");
11312
11313 assert_eq!(num_panes, 1);
11314 assert_eq!(num_items_in_current_pane, 3);
11315 assert_eq!(active_item.item_id(), last_item.item_id());
11316 });
11317 }
11318 struct TestModal(FocusHandle);
11319
11320 impl TestModal {
11321 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
11322 Self(cx.focus_handle())
11323 }
11324 }
11325
11326 impl EventEmitter<DismissEvent> for TestModal {}
11327
11328 impl Focusable for TestModal {
11329 fn focus_handle(&self, _cx: &App) -> FocusHandle {
11330 self.0.clone()
11331 }
11332 }
11333
11334 impl ModalView for TestModal {}
11335
11336 impl Render for TestModal {
11337 fn render(
11338 &mut self,
11339 _window: &mut Window,
11340 _cx: &mut Context<TestModal>,
11341 ) -> impl IntoElement {
11342 div().track_focus(&self.0)
11343 }
11344 }
11345
11346 #[gpui::test]
11347 async fn test_panels(cx: &mut gpui::TestAppContext) {
11348 init_test(cx);
11349 let fs = FakeFs::new(cx.executor());
11350
11351 let project = Project::test(fs, [], cx).await;
11352 let (workspace, cx) =
11353 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11354
11355 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
11356 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
11357 workspace.add_panel(panel_1.clone(), window, cx);
11358 workspace.toggle_dock(DockPosition::Left, window, cx);
11359 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
11360 workspace.add_panel(panel_2.clone(), window, cx);
11361 workspace.toggle_dock(DockPosition::Right, window, cx);
11362
11363 let left_dock = workspace.left_dock();
11364 assert_eq!(
11365 left_dock.read(cx).visible_panel().unwrap().panel_id(),
11366 panel_1.panel_id()
11367 );
11368 assert_eq!(
11369 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
11370 panel_1.size(window, cx)
11371 );
11372
11373 left_dock.update(cx, |left_dock, cx| {
11374 left_dock.resize_active_panel(Some(px(1337.)), window, cx)
11375 });
11376 assert_eq!(
11377 workspace
11378 .right_dock()
11379 .read(cx)
11380 .visible_panel()
11381 .unwrap()
11382 .panel_id(),
11383 panel_2.panel_id(),
11384 );
11385
11386 (panel_1, panel_2)
11387 });
11388
11389 // Move panel_1 to the right
11390 panel_1.update_in(cx, |panel_1, window, cx| {
11391 panel_1.set_position(DockPosition::Right, window, cx)
11392 });
11393
11394 workspace.update_in(cx, |workspace, window, cx| {
11395 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
11396 // Since it was the only panel on the left, the left dock should now be closed.
11397 assert!(!workspace.left_dock().read(cx).is_open());
11398 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
11399 let right_dock = workspace.right_dock();
11400 assert_eq!(
11401 right_dock.read(cx).visible_panel().unwrap().panel_id(),
11402 panel_1.panel_id()
11403 );
11404 assert_eq!(
11405 right_dock.read(cx).active_panel_size(window, cx).unwrap(),
11406 px(1337.)
11407 );
11408
11409 // Now we move panel_2 to the left
11410 panel_2.set_position(DockPosition::Left, window, cx);
11411 });
11412
11413 workspace.update(cx, |workspace, cx| {
11414 // Since panel_2 was not visible on the right, we don't open the left dock.
11415 assert!(!workspace.left_dock().read(cx).is_open());
11416 // And the right dock is unaffected in its displaying of panel_1
11417 assert!(workspace.right_dock().read(cx).is_open());
11418 assert_eq!(
11419 workspace
11420 .right_dock()
11421 .read(cx)
11422 .visible_panel()
11423 .unwrap()
11424 .panel_id(),
11425 panel_1.panel_id(),
11426 );
11427 });
11428
11429 // Move panel_1 back to the left
11430 panel_1.update_in(cx, |panel_1, window, cx| {
11431 panel_1.set_position(DockPosition::Left, window, cx)
11432 });
11433
11434 workspace.update_in(cx, |workspace, window, cx| {
11435 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
11436 let left_dock = workspace.left_dock();
11437 assert!(left_dock.read(cx).is_open());
11438 assert_eq!(
11439 left_dock.read(cx).visible_panel().unwrap().panel_id(),
11440 panel_1.panel_id()
11441 );
11442 assert_eq!(
11443 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
11444 px(1337.)
11445 );
11446 // And the right dock should be closed as it no longer has any panels.
11447 assert!(!workspace.right_dock().read(cx).is_open());
11448
11449 // Now we move panel_1 to the bottom
11450 panel_1.set_position(DockPosition::Bottom, window, cx);
11451 });
11452
11453 workspace.update_in(cx, |workspace, window, cx| {
11454 // Since panel_1 was visible on the left, we close the left dock.
11455 assert!(!workspace.left_dock().read(cx).is_open());
11456 // The bottom dock is sized based on the panel's default size,
11457 // since the panel orientation changed from vertical to horizontal.
11458 let bottom_dock = workspace.bottom_dock();
11459 assert_eq!(
11460 bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
11461 panel_1.size(window, cx),
11462 );
11463 // Close bottom dock and move panel_1 back to the left.
11464 bottom_dock.update(cx, |bottom_dock, cx| {
11465 bottom_dock.set_open(false, window, cx)
11466 });
11467 panel_1.set_position(DockPosition::Left, window, cx);
11468 });
11469
11470 // Emit activated event on panel 1
11471 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
11472
11473 // Now the left dock is open and panel_1 is active and focused.
11474 workspace.update_in(cx, |workspace, window, cx| {
11475 let left_dock = workspace.left_dock();
11476 assert!(left_dock.read(cx).is_open());
11477 assert_eq!(
11478 left_dock.read(cx).visible_panel().unwrap().panel_id(),
11479 panel_1.panel_id(),
11480 );
11481 assert!(panel_1.focus_handle(cx).is_focused(window));
11482 });
11483
11484 // Emit closed event on panel 2, which is not active
11485 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
11486
11487 // Wo don't close the left dock, because panel_2 wasn't the active panel
11488 workspace.update(cx, |workspace, cx| {
11489 let left_dock = workspace.left_dock();
11490 assert!(left_dock.read(cx).is_open());
11491 assert_eq!(
11492 left_dock.read(cx).visible_panel().unwrap().panel_id(),
11493 panel_1.panel_id(),
11494 );
11495 });
11496
11497 // Emitting a ZoomIn event shows the panel as zoomed.
11498 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
11499 workspace.read_with(cx, |workspace, _| {
11500 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
11501 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
11502 });
11503
11504 // Move panel to another dock while it is zoomed
11505 panel_1.update_in(cx, |panel, window, cx| {
11506 panel.set_position(DockPosition::Right, window, cx)
11507 });
11508 workspace.read_with(cx, |workspace, _| {
11509 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
11510
11511 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
11512 });
11513
11514 // This is a helper for getting a:
11515 // - valid focus on an element,
11516 // - that isn't a part of the panes and panels system of the Workspace,
11517 // - and doesn't trigger the 'on_focus_lost' API.
11518 let focus_other_view = {
11519 let workspace = workspace.clone();
11520 move |cx: &mut VisualTestContext| {
11521 workspace.update_in(cx, |workspace, window, cx| {
11522 if workspace.active_modal::<TestModal>(cx).is_some() {
11523 workspace.toggle_modal(window, cx, TestModal::new);
11524 workspace.toggle_modal(window, cx, TestModal::new);
11525 } else {
11526 workspace.toggle_modal(window, cx, TestModal::new);
11527 }
11528 })
11529 }
11530 };
11531
11532 // If focus is transferred to another view that's not a panel or another pane, we still show
11533 // the panel as zoomed.
11534 focus_other_view(cx);
11535 workspace.read_with(cx, |workspace, _| {
11536 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
11537 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
11538 });
11539
11540 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
11541 workspace.update_in(cx, |_workspace, window, cx| {
11542 cx.focus_self(window);
11543 });
11544 workspace.read_with(cx, |workspace, _| {
11545 assert_eq!(workspace.zoomed, None);
11546 assert_eq!(workspace.zoomed_position, None);
11547 });
11548
11549 // If focus is transferred again to another view that's not a panel or a pane, we won't
11550 // show the panel as zoomed because it wasn't zoomed before.
11551 focus_other_view(cx);
11552 workspace.read_with(cx, |workspace, _| {
11553 assert_eq!(workspace.zoomed, None);
11554 assert_eq!(workspace.zoomed_position, None);
11555 });
11556
11557 // When the panel is activated, it is zoomed again.
11558 cx.dispatch_action(ToggleRightDock);
11559 workspace.read_with(cx, |workspace, _| {
11560 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
11561 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
11562 });
11563
11564 // Emitting a ZoomOut event unzooms the panel.
11565 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
11566 workspace.read_with(cx, |workspace, _| {
11567 assert_eq!(workspace.zoomed, None);
11568 assert_eq!(workspace.zoomed_position, None);
11569 });
11570
11571 // Emit closed event on panel 1, which is active
11572 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
11573
11574 // Now the left dock is closed, because panel_1 was the active panel
11575 workspace.update(cx, |workspace, cx| {
11576 let right_dock = workspace.right_dock();
11577 assert!(!right_dock.read(cx).is_open());
11578 });
11579 }
11580
11581 #[gpui::test]
11582 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
11583 init_test(cx);
11584
11585 let fs = FakeFs::new(cx.background_executor.clone());
11586 let project = Project::test(fs, [], cx).await;
11587 let (workspace, cx) =
11588 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11589 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11590
11591 let dirty_regular_buffer = cx.new(|cx| {
11592 TestItem::new(cx)
11593 .with_dirty(true)
11594 .with_label("1.txt")
11595 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
11596 });
11597 let dirty_regular_buffer_2 = cx.new(|cx| {
11598 TestItem::new(cx)
11599 .with_dirty(true)
11600 .with_label("2.txt")
11601 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
11602 });
11603 let dirty_multi_buffer_with_both = cx.new(|cx| {
11604 TestItem::new(cx)
11605 .with_dirty(true)
11606 .with_buffer_kind(ItemBufferKind::Multibuffer)
11607 .with_label("Fake Project Search")
11608 .with_project_items(&[
11609 dirty_regular_buffer.read(cx).project_items[0].clone(),
11610 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
11611 ])
11612 });
11613 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
11614 workspace.update_in(cx, |workspace, window, cx| {
11615 workspace.add_item(
11616 pane.clone(),
11617 Box::new(dirty_regular_buffer.clone()),
11618 None,
11619 false,
11620 false,
11621 window,
11622 cx,
11623 );
11624 workspace.add_item(
11625 pane.clone(),
11626 Box::new(dirty_regular_buffer_2.clone()),
11627 None,
11628 false,
11629 false,
11630 window,
11631 cx,
11632 );
11633 workspace.add_item(
11634 pane.clone(),
11635 Box::new(dirty_multi_buffer_with_both.clone()),
11636 None,
11637 false,
11638 false,
11639 window,
11640 cx,
11641 );
11642 });
11643
11644 pane.update_in(cx, |pane, window, cx| {
11645 pane.activate_item(2, true, true, window, cx);
11646 assert_eq!(
11647 pane.active_item().unwrap().item_id(),
11648 multi_buffer_with_both_files_id,
11649 "Should select the multi buffer in the pane"
11650 );
11651 });
11652 let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
11653 pane.close_other_items(
11654 &CloseOtherItems {
11655 save_intent: Some(SaveIntent::Save),
11656 close_pinned: true,
11657 },
11658 None,
11659 window,
11660 cx,
11661 )
11662 });
11663 cx.background_executor.run_until_parked();
11664 assert!(!cx.has_pending_prompt());
11665 close_all_but_multi_buffer_task
11666 .await
11667 .expect("Closing all buffers but the multi buffer failed");
11668 pane.update(cx, |pane, cx| {
11669 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
11670 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
11671 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
11672 assert_eq!(pane.items_len(), 1);
11673 assert_eq!(
11674 pane.active_item().unwrap().item_id(),
11675 multi_buffer_with_both_files_id,
11676 "Should have only the multi buffer left in the pane"
11677 );
11678 assert!(
11679 dirty_multi_buffer_with_both.read(cx).is_dirty,
11680 "The multi buffer containing the unsaved buffer should still be dirty"
11681 );
11682 });
11683
11684 dirty_regular_buffer.update(cx, |buffer, cx| {
11685 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
11686 });
11687
11688 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
11689 pane.close_active_item(
11690 &CloseActiveItem {
11691 save_intent: Some(SaveIntent::Close),
11692 close_pinned: false,
11693 },
11694 window,
11695 cx,
11696 )
11697 });
11698 cx.background_executor.run_until_parked();
11699 assert!(
11700 cx.has_pending_prompt(),
11701 "Dirty multi buffer should prompt a save dialog"
11702 );
11703 cx.simulate_prompt_answer("Save");
11704 cx.background_executor.run_until_parked();
11705 close_multi_buffer_task
11706 .await
11707 .expect("Closing the multi buffer failed");
11708 pane.update(cx, |pane, cx| {
11709 assert_eq!(
11710 dirty_multi_buffer_with_both.read(cx).save_count,
11711 1,
11712 "Multi buffer item should get be saved"
11713 );
11714 // Test impl does not save inner items, so we do not assert them
11715 assert_eq!(
11716 pane.items_len(),
11717 0,
11718 "No more items should be left in the pane"
11719 );
11720 assert!(pane.active_item().is_none());
11721 });
11722 }
11723
11724 #[gpui::test]
11725 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
11726 cx: &mut TestAppContext,
11727 ) {
11728 init_test(cx);
11729
11730 let fs = FakeFs::new(cx.background_executor.clone());
11731 let project = Project::test(fs, [], cx).await;
11732 let (workspace, cx) =
11733 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11734 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11735
11736 let dirty_regular_buffer = cx.new(|cx| {
11737 TestItem::new(cx)
11738 .with_dirty(true)
11739 .with_label("1.txt")
11740 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
11741 });
11742 let dirty_regular_buffer_2 = cx.new(|cx| {
11743 TestItem::new(cx)
11744 .with_dirty(true)
11745 .with_label("2.txt")
11746 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
11747 });
11748 let clear_regular_buffer = cx.new(|cx| {
11749 TestItem::new(cx)
11750 .with_label("3.txt")
11751 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
11752 });
11753
11754 let dirty_multi_buffer_with_both = cx.new(|cx| {
11755 TestItem::new(cx)
11756 .with_dirty(true)
11757 .with_buffer_kind(ItemBufferKind::Multibuffer)
11758 .with_label("Fake Project Search")
11759 .with_project_items(&[
11760 dirty_regular_buffer.read(cx).project_items[0].clone(),
11761 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
11762 clear_regular_buffer.read(cx).project_items[0].clone(),
11763 ])
11764 });
11765 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
11766 workspace.update_in(cx, |workspace, window, cx| {
11767 workspace.add_item(
11768 pane.clone(),
11769 Box::new(dirty_regular_buffer.clone()),
11770 None,
11771 false,
11772 false,
11773 window,
11774 cx,
11775 );
11776 workspace.add_item(
11777 pane.clone(),
11778 Box::new(dirty_multi_buffer_with_both.clone()),
11779 None,
11780 false,
11781 false,
11782 window,
11783 cx,
11784 );
11785 });
11786
11787 pane.update_in(cx, |pane, window, cx| {
11788 pane.activate_item(1, true, true, window, cx);
11789 assert_eq!(
11790 pane.active_item().unwrap().item_id(),
11791 multi_buffer_with_both_files_id,
11792 "Should select the multi buffer in the pane"
11793 );
11794 });
11795 let _close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
11796 pane.close_active_item(
11797 &CloseActiveItem {
11798 save_intent: None,
11799 close_pinned: false,
11800 },
11801 window,
11802 cx,
11803 )
11804 });
11805 cx.background_executor.run_until_parked();
11806 assert!(
11807 cx.has_pending_prompt(),
11808 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
11809 );
11810 }
11811
11812 /// Tests that when `close_on_file_delete` is enabled, files are automatically
11813 /// closed when they are deleted from disk.
11814 #[gpui::test]
11815 async fn test_close_on_disk_deletion_enabled(cx: &mut TestAppContext) {
11816 init_test(cx);
11817
11818 // Enable the close_on_disk_deletion setting
11819 cx.update_global(|store: &mut SettingsStore, cx| {
11820 store.update_user_settings(cx, |settings| {
11821 settings.workspace.close_on_file_delete = Some(true);
11822 });
11823 });
11824
11825 let fs = FakeFs::new(cx.background_executor.clone());
11826 let project = Project::test(fs, [], cx).await;
11827 let (workspace, cx) =
11828 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11829 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11830
11831 // Create a test item that simulates a file
11832 let item = cx.new(|cx| {
11833 TestItem::new(cx)
11834 .with_label("test.txt")
11835 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
11836 });
11837
11838 // Add item to workspace
11839 workspace.update_in(cx, |workspace, window, cx| {
11840 workspace.add_item(
11841 pane.clone(),
11842 Box::new(item.clone()),
11843 None,
11844 false,
11845 false,
11846 window,
11847 cx,
11848 );
11849 });
11850
11851 // Verify the item is in the pane
11852 pane.read_with(cx, |pane, _| {
11853 assert_eq!(pane.items().count(), 1);
11854 });
11855
11856 // Simulate file deletion by setting the item's deleted state
11857 item.update(cx, |item, _| {
11858 item.set_has_deleted_file(true);
11859 });
11860
11861 // Emit UpdateTab event to trigger the close behavior
11862 cx.run_until_parked();
11863 item.update(cx, |_, cx| {
11864 cx.emit(ItemEvent::UpdateTab);
11865 });
11866
11867 // Allow the close operation to complete
11868 cx.run_until_parked();
11869
11870 // Verify the item was automatically closed
11871 pane.read_with(cx, |pane, _| {
11872 assert_eq!(
11873 pane.items().count(),
11874 0,
11875 "Item should be automatically closed when file is deleted"
11876 );
11877 });
11878 }
11879
11880 /// Tests that when `close_on_file_delete` is disabled (default), files remain
11881 /// open with a strikethrough when they are deleted from disk.
11882 #[gpui::test]
11883 async fn test_close_on_disk_deletion_disabled(cx: &mut TestAppContext) {
11884 init_test(cx);
11885
11886 // Ensure close_on_disk_deletion is disabled (default)
11887 cx.update_global(|store: &mut SettingsStore, cx| {
11888 store.update_user_settings(cx, |settings| {
11889 settings.workspace.close_on_file_delete = Some(false);
11890 });
11891 });
11892
11893 let fs = FakeFs::new(cx.background_executor.clone());
11894 let project = Project::test(fs, [], cx).await;
11895 let (workspace, cx) =
11896 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11897 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11898
11899 // Create a test item that simulates a file
11900 let item = cx.new(|cx| {
11901 TestItem::new(cx)
11902 .with_label("test.txt")
11903 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
11904 });
11905
11906 // Add item to workspace
11907 workspace.update_in(cx, |workspace, window, cx| {
11908 workspace.add_item(
11909 pane.clone(),
11910 Box::new(item.clone()),
11911 None,
11912 false,
11913 false,
11914 window,
11915 cx,
11916 );
11917 });
11918
11919 // Verify the item is in the pane
11920 pane.read_with(cx, |pane, _| {
11921 assert_eq!(pane.items().count(), 1);
11922 });
11923
11924 // Simulate file deletion
11925 item.update(cx, |item, _| {
11926 item.set_has_deleted_file(true);
11927 });
11928
11929 // Emit UpdateTab event
11930 cx.run_until_parked();
11931 item.update(cx, |_, cx| {
11932 cx.emit(ItemEvent::UpdateTab);
11933 });
11934
11935 // Allow any potential close operation to complete
11936 cx.run_until_parked();
11937
11938 // Verify the item remains open (with strikethrough)
11939 pane.read_with(cx, |pane, _| {
11940 assert_eq!(
11941 pane.items().count(),
11942 1,
11943 "Item should remain open when close_on_disk_deletion is disabled"
11944 );
11945 });
11946
11947 // Verify the item shows as deleted
11948 item.read_with(cx, |item, _| {
11949 assert!(
11950 item.has_deleted_file,
11951 "Item should be marked as having deleted file"
11952 );
11953 });
11954 }
11955
11956 /// Tests that dirty files are not automatically closed when deleted from disk,
11957 /// even when `close_on_file_delete` is enabled. This ensures users don't lose
11958 /// unsaved changes without being prompted.
11959 #[gpui::test]
11960 async fn test_close_on_disk_deletion_with_dirty_file(cx: &mut TestAppContext) {
11961 init_test(cx);
11962
11963 // Enable the close_on_file_delete setting
11964 cx.update_global(|store: &mut SettingsStore, cx| {
11965 store.update_user_settings(cx, |settings| {
11966 settings.workspace.close_on_file_delete = Some(true);
11967 });
11968 });
11969
11970 let fs = FakeFs::new(cx.background_executor.clone());
11971 let project = Project::test(fs, [], cx).await;
11972 let (workspace, cx) =
11973 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11974 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11975
11976 // Create a dirty test item
11977 let item = cx.new(|cx| {
11978 TestItem::new(cx)
11979 .with_dirty(true)
11980 .with_label("test.txt")
11981 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
11982 });
11983
11984 // Add item to workspace
11985 workspace.update_in(cx, |workspace, window, cx| {
11986 workspace.add_item(
11987 pane.clone(),
11988 Box::new(item.clone()),
11989 None,
11990 false,
11991 false,
11992 window,
11993 cx,
11994 );
11995 });
11996
11997 // Simulate file deletion
11998 item.update(cx, |item, _| {
11999 item.set_has_deleted_file(true);
12000 });
12001
12002 // Emit UpdateTab event to trigger the close behavior
12003 cx.run_until_parked();
12004 item.update(cx, |_, cx| {
12005 cx.emit(ItemEvent::UpdateTab);
12006 });
12007
12008 // Allow any potential close operation to complete
12009 cx.run_until_parked();
12010
12011 // Verify the item remains open (dirty files are not auto-closed)
12012 pane.read_with(cx, |pane, _| {
12013 assert_eq!(
12014 pane.items().count(),
12015 1,
12016 "Dirty items should not be automatically closed even when file is deleted"
12017 );
12018 });
12019
12020 // Verify the item is marked as deleted and still dirty
12021 item.read_with(cx, |item, _| {
12022 assert!(
12023 item.has_deleted_file,
12024 "Item should be marked as having deleted file"
12025 );
12026 assert!(item.is_dirty, "Item should still be dirty");
12027 });
12028 }
12029
12030 /// Tests that navigation history is cleaned up when files are auto-closed
12031 /// due to deletion from disk.
12032 #[gpui::test]
12033 async fn test_close_on_disk_deletion_cleans_navigation_history(cx: &mut TestAppContext) {
12034 init_test(cx);
12035
12036 // Enable the close_on_file_delete setting
12037 cx.update_global(|store: &mut SettingsStore, cx| {
12038 store.update_user_settings(cx, |settings| {
12039 settings.workspace.close_on_file_delete = Some(true);
12040 });
12041 });
12042
12043 let fs = FakeFs::new(cx.background_executor.clone());
12044 let project = Project::test(fs, [], cx).await;
12045 let (workspace, cx) =
12046 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12047 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12048
12049 // Create test items
12050 let item1 = cx.new(|cx| {
12051 TestItem::new(cx)
12052 .with_label("test1.txt")
12053 .with_project_items(&[TestProjectItem::new(1, "test1.txt", cx)])
12054 });
12055 let item1_id = item1.item_id();
12056
12057 let item2 = cx.new(|cx| {
12058 TestItem::new(cx)
12059 .with_label("test2.txt")
12060 .with_project_items(&[TestProjectItem::new(2, "test2.txt", cx)])
12061 });
12062
12063 // Add items to workspace
12064 workspace.update_in(cx, |workspace, window, cx| {
12065 workspace.add_item(
12066 pane.clone(),
12067 Box::new(item1.clone()),
12068 None,
12069 false,
12070 false,
12071 window,
12072 cx,
12073 );
12074 workspace.add_item(
12075 pane.clone(),
12076 Box::new(item2.clone()),
12077 None,
12078 false,
12079 false,
12080 window,
12081 cx,
12082 );
12083 });
12084
12085 // Activate item1 to ensure it gets navigation entries
12086 pane.update_in(cx, |pane, window, cx| {
12087 pane.activate_item(0, true, true, window, cx);
12088 });
12089
12090 // Switch to item2 and back to create navigation history
12091 pane.update_in(cx, |pane, window, cx| {
12092 pane.activate_item(1, true, true, window, cx);
12093 });
12094 cx.run_until_parked();
12095
12096 pane.update_in(cx, |pane, window, cx| {
12097 pane.activate_item(0, true, true, window, cx);
12098 });
12099 cx.run_until_parked();
12100
12101 // Simulate file deletion for item1
12102 item1.update(cx, |item, _| {
12103 item.set_has_deleted_file(true);
12104 });
12105
12106 // Emit UpdateTab event to trigger the close behavior
12107 item1.update(cx, |_, cx| {
12108 cx.emit(ItemEvent::UpdateTab);
12109 });
12110 cx.run_until_parked();
12111
12112 // Verify item1 was closed
12113 pane.read_with(cx, |pane, _| {
12114 assert_eq!(
12115 pane.items().count(),
12116 1,
12117 "Should have 1 item remaining after auto-close"
12118 );
12119 });
12120
12121 // Check navigation history after close
12122 let has_item = pane.read_with(cx, |pane, cx| {
12123 let mut has_item = false;
12124 pane.nav_history().for_each_entry(cx, &mut |entry, _| {
12125 if entry.item.id() == item1_id {
12126 has_item = true;
12127 }
12128 });
12129 has_item
12130 });
12131
12132 assert!(
12133 !has_item,
12134 "Navigation history should not contain closed item entries"
12135 );
12136 }
12137
12138 #[gpui::test]
12139 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
12140 cx: &mut TestAppContext,
12141 ) {
12142 init_test(cx);
12143
12144 let fs = FakeFs::new(cx.background_executor.clone());
12145 let project = Project::test(fs, [], cx).await;
12146 let (workspace, cx) =
12147 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12148 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12149
12150 let dirty_regular_buffer = cx.new(|cx| {
12151 TestItem::new(cx)
12152 .with_dirty(true)
12153 .with_label("1.txt")
12154 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
12155 });
12156 let dirty_regular_buffer_2 = cx.new(|cx| {
12157 TestItem::new(cx)
12158 .with_dirty(true)
12159 .with_label("2.txt")
12160 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
12161 });
12162 let clear_regular_buffer = cx.new(|cx| {
12163 TestItem::new(cx)
12164 .with_label("3.txt")
12165 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
12166 });
12167
12168 let dirty_multi_buffer = cx.new(|cx| {
12169 TestItem::new(cx)
12170 .with_dirty(true)
12171 .with_buffer_kind(ItemBufferKind::Multibuffer)
12172 .with_label("Fake Project Search")
12173 .with_project_items(&[
12174 dirty_regular_buffer.read(cx).project_items[0].clone(),
12175 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
12176 clear_regular_buffer.read(cx).project_items[0].clone(),
12177 ])
12178 });
12179 workspace.update_in(cx, |workspace, window, cx| {
12180 workspace.add_item(
12181 pane.clone(),
12182 Box::new(dirty_regular_buffer.clone()),
12183 None,
12184 false,
12185 false,
12186 window,
12187 cx,
12188 );
12189 workspace.add_item(
12190 pane.clone(),
12191 Box::new(dirty_regular_buffer_2.clone()),
12192 None,
12193 false,
12194 false,
12195 window,
12196 cx,
12197 );
12198 workspace.add_item(
12199 pane.clone(),
12200 Box::new(dirty_multi_buffer.clone()),
12201 None,
12202 false,
12203 false,
12204 window,
12205 cx,
12206 );
12207 });
12208
12209 pane.update_in(cx, |pane, window, cx| {
12210 pane.activate_item(2, true, true, window, cx);
12211 assert_eq!(
12212 pane.active_item().unwrap().item_id(),
12213 dirty_multi_buffer.item_id(),
12214 "Should select the multi buffer in the pane"
12215 );
12216 });
12217 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
12218 pane.close_active_item(
12219 &CloseActiveItem {
12220 save_intent: None,
12221 close_pinned: false,
12222 },
12223 window,
12224 cx,
12225 )
12226 });
12227 cx.background_executor.run_until_parked();
12228 assert!(
12229 !cx.has_pending_prompt(),
12230 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
12231 );
12232 close_multi_buffer_task
12233 .await
12234 .expect("Closing multi buffer failed");
12235 pane.update(cx, |pane, cx| {
12236 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
12237 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
12238 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
12239 assert_eq!(
12240 pane.items()
12241 .map(|item| item.item_id())
12242 .sorted()
12243 .collect::<Vec<_>>(),
12244 vec![
12245 dirty_regular_buffer.item_id(),
12246 dirty_regular_buffer_2.item_id(),
12247 ],
12248 "Should have no multi buffer left in the pane"
12249 );
12250 assert!(dirty_regular_buffer.read(cx).is_dirty);
12251 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
12252 });
12253 }
12254
12255 #[gpui::test]
12256 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
12257 init_test(cx);
12258 let fs = FakeFs::new(cx.executor());
12259 let project = Project::test(fs, [], cx).await;
12260 let (workspace, cx) =
12261 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12262
12263 // Add a new panel to the right dock, opening the dock and setting the
12264 // focus to the new panel.
12265 let panel = workspace.update_in(cx, |workspace, window, cx| {
12266 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
12267 workspace.add_panel(panel.clone(), window, cx);
12268
12269 workspace
12270 .right_dock()
12271 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
12272
12273 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12274
12275 panel
12276 });
12277
12278 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
12279 // panel to the next valid position which, in this case, is the left
12280 // dock.
12281 cx.dispatch_action(MoveFocusedPanelToNextPosition);
12282 workspace.update(cx, |workspace, cx| {
12283 assert!(workspace.left_dock().read(cx).is_open());
12284 assert_eq!(panel.read(cx).position, DockPosition::Left);
12285 });
12286
12287 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
12288 // panel to the next valid position which, in this case, is the bottom
12289 // dock.
12290 cx.dispatch_action(MoveFocusedPanelToNextPosition);
12291 workspace.update(cx, |workspace, cx| {
12292 assert!(workspace.bottom_dock().read(cx).is_open());
12293 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
12294 });
12295
12296 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
12297 // around moving the panel to its initial position, the right dock.
12298 cx.dispatch_action(MoveFocusedPanelToNextPosition);
12299 workspace.update(cx, |workspace, cx| {
12300 assert!(workspace.right_dock().read(cx).is_open());
12301 assert_eq!(panel.read(cx).position, DockPosition::Right);
12302 });
12303
12304 // Remove focus from the panel, ensuring that, if the panel is not
12305 // focused, the `MoveFocusedPanelToNextPosition` action does not update
12306 // the panel's position, so the panel is still in the right dock.
12307 workspace.update_in(cx, |workspace, window, cx| {
12308 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12309 });
12310
12311 cx.dispatch_action(MoveFocusedPanelToNextPosition);
12312 workspace.update(cx, |workspace, cx| {
12313 assert!(workspace.right_dock().read(cx).is_open());
12314 assert_eq!(panel.read(cx).position, DockPosition::Right);
12315 });
12316 }
12317
12318 #[gpui::test]
12319 async fn test_moving_items_create_panes(cx: &mut TestAppContext) {
12320 init_test(cx);
12321
12322 let fs = FakeFs::new(cx.executor());
12323 let project = Project::test(fs, [], cx).await;
12324 let (workspace, cx) =
12325 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
12326
12327 let item_1 = cx.new(|cx| {
12328 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
12329 });
12330 workspace.update_in(cx, |workspace, window, cx| {
12331 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
12332 workspace.move_item_to_pane_in_direction(
12333 &MoveItemToPaneInDirection {
12334 direction: SplitDirection::Right,
12335 focus: true,
12336 clone: false,
12337 },
12338 window,
12339 cx,
12340 );
12341 workspace.move_item_to_pane_at_index(
12342 &MoveItemToPane {
12343 destination: 3,
12344 focus: true,
12345 clone: false,
12346 },
12347 window,
12348 cx,
12349 );
12350
12351 assert_eq!(workspace.panes.len(), 1, "No new panes were created");
12352 assert_eq!(
12353 pane_items_paths(&workspace.active_pane, cx),
12354 vec!["first.txt".to_string()],
12355 "Single item was not moved anywhere"
12356 );
12357 });
12358
12359 let item_2 = cx.new(|cx| {
12360 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "second.txt", cx)])
12361 });
12362 workspace.update_in(cx, |workspace, window, cx| {
12363 workspace.add_item_to_active_pane(Box::new(item_2), None, true, window, cx);
12364 assert_eq!(
12365 pane_items_paths(&workspace.panes[0], cx),
12366 vec!["first.txt".to_string(), "second.txt".to_string()],
12367 );
12368 workspace.move_item_to_pane_in_direction(
12369 &MoveItemToPaneInDirection {
12370 direction: SplitDirection::Right,
12371 focus: true,
12372 clone: false,
12373 },
12374 window,
12375 cx,
12376 );
12377
12378 assert_eq!(workspace.panes.len(), 2, "A new pane should be created");
12379 assert_eq!(
12380 pane_items_paths(&workspace.panes[0], cx),
12381 vec!["first.txt".to_string()],
12382 "After moving, one item should be left in the original pane"
12383 );
12384 assert_eq!(
12385 pane_items_paths(&workspace.panes[1], cx),
12386 vec!["second.txt".to_string()],
12387 "New item should have been moved to the new pane"
12388 );
12389 });
12390
12391 let item_3 = cx.new(|cx| {
12392 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "third.txt", cx)])
12393 });
12394 workspace.update_in(cx, |workspace, window, cx| {
12395 let original_pane = workspace.panes[0].clone();
12396 workspace.set_active_pane(&original_pane, window, cx);
12397 workspace.add_item_to_active_pane(Box::new(item_3), None, true, window, cx);
12398 assert_eq!(workspace.panes.len(), 2, "No new panes were created");
12399 assert_eq!(
12400 pane_items_paths(&workspace.active_pane, cx),
12401 vec!["first.txt".to_string(), "third.txt".to_string()],
12402 "New pane should be ready to move one item out"
12403 );
12404
12405 workspace.move_item_to_pane_at_index(
12406 &MoveItemToPane {
12407 destination: 3,
12408 focus: true,
12409 clone: false,
12410 },
12411 window,
12412 cx,
12413 );
12414 assert_eq!(workspace.panes.len(), 3, "A new pane should be created");
12415 assert_eq!(
12416 pane_items_paths(&workspace.active_pane, cx),
12417 vec!["first.txt".to_string()],
12418 "After moving, one item should be left in the original pane"
12419 );
12420 assert_eq!(
12421 pane_items_paths(&workspace.panes[1], cx),
12422 vec!["second.txt".to_string()],
12423 "Previously created pane should be unchanged"
12424 );
12425 assert_eq!(
12426 pane_items_paths(&workspace.panes[2], cx),
12427 vec!["third.txt".to_string()],
12428 "New item should have been moved to the new pane"
12429 );
12430 });
12431 }
12432
12433 #[gpui::test]
12434 async fn test_moving_items_can_clone_panes(cx: &mut TestAppContext) {
12435 init_test(cx);
12436
12437 let fs = FakeFs::new(cx.executor());
12438 let project = Project::test(fs, [], cx).await;
12439 let (workspace, cx) =
12440 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
12441
12442 let item_1 = cx.new(|cx| {
12443 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
12444 });
12445 workspace.update_in(cx, |workspace, window, cx| {
12446 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
12447 workspace.move_item_to_pane_in_direction(
12448 &MoveItemToPaneInDirection {
12449 direction: SplitDirection::Right,
12450 focus: true,
12451 clone: true,
12452 },
12453 window,
12454 cx,
12455 );
12456 });
12457 cx.run_until_parked();
12458 workspace.update_in(cx, |workspace, window, cx| {
12459 workspace.move_item_to_pane_at_index(
12460 &MoveItemToPane {
12461 destination: 3,
12462 focus: true,
12463 clone: true,
12464 },
12465 window,
12466 cx,
12467 );
12468 });
12469 cx.run_until_parked();
12470
12471 workspace.update(cx, |workspace, cx| {
12472 assert_eq!(workspace.panes.len(), 3, "Two new panes were created");
12473 for pane in workspace.panes() {
12474 assert_eq!(
12475 pane_items_paths(pane, cx),
12476 vec!["first.txt".to_string()],
12477 "Single item exists in all panes"
12478 );
12479 }
12480 });
12481
12482 // verify that the active pane has been updated after waiting for the
12483 // pane focus event to fire and resolve
12484 workspace.read_with(cx, |workspace, _app| {
12485 assert_eq!(
12486 workspace.active_pane(),
12487 &workspace.panes[2],
12488 "The third pane should be the active one: {:?}",
12489 workspace.panes
12490 );
12491 })
12492 }
12493
12494 #[gpui::test]
12495 async fn test_close_item_in_all_panes(cx: &mut TestAppContext) {
12496 init_test(cx);
12497
12498 let fs = FakeFs::new(cx.executor());
12499 fs.insert_tree("/root", json!({ "test.txt": "" })).await;
12500
12501 let project = Project::test(fs, ["root".as_ref()], cx).await;
12502 let (workspace, cx) =
12503 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
12504
12505 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12506 // Add item to pane A with project path
12507 let item_a = cx.new(|cx| {
12508 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
12509 });
12510 workspace.update_in(cx, |workspace, window, cx| {
12511 workspace.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
12512 });
12513
12514 // Split to create pane B
12515 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
12516 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
12517 });
12518
12519 // Add item with SAME project path to pane B, and pin it
12520 let item_b = cx.new(|cx| {
12521 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
12522 });
12523 pane_b.update_in(cx, |pane, window, cx| {
12524 pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
12525 pane.set_pinned_count(1);
12526 });
12527
12528 assert_eq!(pane_a.read_with(cx, |pane, _| pane.items_len()), 1);
12529 assert_eq!(pane_b.read_with(cx, |pane, _| pane.items_len()), 1);
12530
12531 // close_pinned: false should only close the unpinned copy
12532 workspace.update_in(cx, |workspace, window, cx| {
12533 workspace.close_item_in_all_panes(
12534 &CloseItemInAllPanes {
12535 save_intent: Some(SaveIntent::Close),
12536 close_pinned: false,
12537 },
12538 window,
12539 cx,
12540 )
12541 });
12542 cx.executor().run_until_parked();
12543
12544 let item_count_a = pane_a.read_with(cx, |pane, _| pane.items_len());
12545 let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
12546 assert_eq!(item_count_a, 0, "Unpinned item in pane A should be closed");
12547 assert_eq!(item_count_b, 1, "Pinned item in pane B should remain");
12548
12549 // Split again, seeing as closing the previous item also closed its
12550 // pane, so only pane remains, which does not allow us to properly test
12551 // that both items close when `close_pinned: true`.
12552 let pane_c = workspace.update_in(cx, |workspace, window, cx| {
12553 workspace.split_pane(pane_b.clone(), SplitDirection::Right, window, cx)
12554 });
12555
12556 // Add an item with the same project path to pane C so that
12557 // close_item_in_all_panes can determine what to close across all panes
12558 // (it reads the active item from the active pane, and split_pane
12559 // creates an empty pane).
12560 let item_c = cx.new(|cx| {
12561 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
12562 });
12563 pane_c.update_in(cx, |pane, window, cx| {
12564 pane.add_item(Box::new(item_c.clone()), true, true, None, window, cx);
12565 });
12566
12567 // close_pinned: true should close the pinned copy too
12568 workspace.update_in(cx, |workspace, window, cx| {
12569 let panes_count = workspace.panes().len();
12570 assert_eq!(panes_count, 2, "Workspace should have two panes (B and C)");
12571
12572 workspace.close_item_in_all_panes(
12573 &CloseItemInAllPanes {
12574 save_intent: Some(SaveIntent::Close),
12575 close_pinned: true,
12576 },
12577 window,
12578 cx,
12579 )
12580 });
12581 cx.executor().run_until_parked();
12582
12583 let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
12584 let item_count_c = pane_c.read_with(cx, |pane, _| pane.items_len());
12585 assert_eq!(item_count_b, 0, "Pinned item in pane B should be closed");
12586 assert_eq!(item_count_c, 0, "Unpinned item in pane C should be closed");
12587 }
12588
12589 mod register_project_item_tests {
12590
12591 use super::*;
12592
12593 // View
12594 struct TestPngItemView {
12595 focus_handle: FocusHandle,
12596 }
12597 // Model
12598 struct TestPngItem {}
12599
12600 impl project::ProjectItem for TestPngItem {
12601 fn try_open(
12602 _project: &Entity<Project>,
12603 path: &ProjectPath,
12604 cx: &mut App,
12605 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
12606 if path.path.extension().unwrap() == "png" {
12607 Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestPngItem {}))))
12608 } else {
12609 None
12610 }
12611 }
12612
12613 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
12614 None
12615 }
12616
12617 fn project_path(&self, _: &App) -> Option<ProjectPath> {
12618 None
12619 }
12620
12621 fn is_dirty(&self) -> bool {
12622 false
12623 }
12624 }
12625
12626 impl Item for TestPngItemView {
12627 type Event = ();
12628 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
12629 "".into()
12630 }
12631 }
12632 impl EventEmitter<()> for TestPngItemView {}
12633 impl Focusable for TestPngItemView {
12634 fn focus_handle(&self, _cx: &App) -> FocusHandle {
12635 self.focus_handle.clone()
12636 }
12637 }
12638
12639 impl Render for TestPngItemView {
12640 fn render(
12641 &mut self,
12642 _window: &mut Window,
12643 _cx: &mut Context<Self>,
12644 ) -> impl IntoElement {
12645 Empty
12646 }
12647 }
12648
12649 impl ProjectItem for TestPngItemView {
12650 type Item = TestPngItem;
12651
12652 fn for_project_item(
12653 _project: Entity<Project>,
12654 _pane: Option<&Pane>,
12655 _item: Entity<Self::Item>,
12656 _: &mut Window,
12657 cx: &mut Context<Self>,
12658 ) -> Self
12659 where
12660 Self: Sized,
12661 {
12662 Self {
12663 focus_handle: cx.focus_handle(),
12664 }
12665 }
12666 }
12667
12668 // View
12669 struct TestIpynbItemView {
12670 focus_handle: FocusHandle,
12671 }
12672 // Model
12673 struct TestIpynbItem {}
12674
12675 impl project::ProjectItem for TestIpynbItem {
12676 fn try_open(
12677 _project: &Entity<Project>,
12678 path: &ProjectPath,
12679 cx: &mut App,
12680 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
12681 if path.path.extension().unwrap() == "ipynb" {
12682 Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestIpynbItem {}))))
12683 } else {
12684 None
12685 }
12686 }
12687
12688 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
12689 None
12690 }
12691
12692 fn project_path(&self, _: &App) -> Option<ProjectPath> {
12693 None
12694 }
12695
12696 fn is_dirty(&self) -> bool {
12697 false
12698 }
12699 }
12700
12701 impl Item for TestIpynbItemView {
12702 type Event = ();
12703 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
12704 "".into()
12705 }
12706 }
12707 impl EventEmitter<()> for TestIpynbItemView {}
12708 impl Focusable for TestIpynbItemView {
12709 fn focus_handle(&self, _cx: &App) -> FocusHandle {
12710 self.focus_handle.clone()
12711 }
12712 }
12713
12714 impl Render for TestIpynbItemView {
12715 fn render(
12716 &mut self,
12717 _window: &mut Window,
12718 _cx: &mut Context<Self>,
12719 ) -> impl IntoElement {
12720 Empty
12721 }
12722 }
12723
12724 impl ProjectItem for TestIpynbItemView {
12725 type Item = TestIpynbItem;
12726
12727 fn for_project_item(
12728 _project: Entity<Project>,
12729 _pane: Option<&Pane>,
12730 _item: Entity<Self::Item>,
12731 _: &mut Window,
12732 cx: &mut Context<Self>,
12733 ) -> Self
12734 where
12735 Self: Sized,
12736 {
12737 Self {
12738 focus_handle: cx.focus_handle(),
12739 }
12740 }
12741 }
12742
12743 struct TestAlternatePngItemView {
12744 focus_handle: FocusHandle,
12745 }
12746
12747 impl Item for TestAlternatePngItemView {
12748 type Event = ();
12749 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
12750 "".into()
12751 }
12752 }
12753
12754 impl EventEmitter<()> for TestAlternatePngItemView {}
12755 impl Focusable for TestAlternatePngItemView {
12756 fn focus_handle(&self, _cx: &App) -> FocusHandle {
12757 self.focus_handle.clone()
12758 }
12759 }
12760
12761 impl Render for TestAlternatePngItemView {
12762 fn render(
12763 &mut self,
12764 _window: &mut Window,
12765 _cx: &mut Context<Self>,
12766 ) -> impl IntoElement {
12767 Empty
12768 }
12769 }
12770
12771 impl ProjectItem for TestAlternatePngItemView {
12772 type Item = TestPngItem;
12773
12774 fn for_project_item(
12775 _project: Entity<Project>,
12776 _pane: Option<&Pane>,
12777 _item: Entity<Self::Item>,
12778 _: &mut Window,
12779 cx: &mut Context<Self>,
12780 ) -> Self
12781 where
12782 Self: Sized,
12783 {
12784 Self {
12785 focus_handle: cx.focus_handle(),
12786 }
12787 }
12788 }
12789
12790 #[gpui::test]
12791 async fn test_register_project_item(cx: &mut TestAppContext) {
12792 init_test(cx);
12793
12794 cx.update(|cx| {
12795 register_project_item::<TestPngItemView>(cx);
12796 register_project_item::<TestIpynbItemView>(cx);
12797 });
12798
12799 let fs = FakeFs::new(cx.executor());
12800 fs.insert_tree(
12801 "/root1",
12802 json!({
12803 "one.png": "BINARYDATAHERE",
12804 "two.ipynb": "{ totally a notebook }",
12805 "three.txt": "editing text, sure why not?"
12806 }),
12807 )
12808 .await;
12809
12810 let project = Project::test(fs, ["root1".as_ref()], cx).await;
12811 let (workspace, cx) =
12812 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
12813
12814 let worktree_id = project.update(cx, |project, cx| {
12815 project.worktrees(cx).next().unwrap().read(cx).id()
12816 });
12817
12818 let handle = workspace
12819 .update_in(cx, |workspace, window, cx| {
12820 let project_path = (worktree_id, rel_path("one.png"));
12821 workspace.open_path(project_path, None, true, window, cx)
12822 })
12823 .await
12824 .unwrap();
12825
12826 // Now we can check if the handle we got back errored or not
12827 assert_eq!(
12828 handle.to_any_view().entity_type(),
12829 TypeId::of::<TestPngItemView>()
12830 );
12831
12832 let handle = workspace
12833 .update_in(cx, |workspace, window, cx| {
12834 let project_path = (worktree_id, rel_path("two.ipynb"));
12835 workspace.open_path(project_path, None, true, window, cx)
12836 })
12837 .await
12838 .unwrap();
12839
12840 assert_eq!(
12841 handle.to_any_view().entity_type(),
12842 TypeId::of::<TestIpynbItemView>()
12843 );
12844
12845 let handle = workspace
12846 .update_in(cx, |workspace, window, cx| {
12847 let project_path = (worktree_id, rel_path("three.txt"));
12848 workspace.open_path(project_path, None, true, window, cx)
12849 })
12850 .await;
12851 assert!(handle.is_err());
12852 }
12853
12854 #[gpui::test]
12855 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
12856 init_test(cx);
12857
12858 cx.update(|cx| {
12859 register_project_item::<TestPngItemView>(cx);
12860 register_project_item::<TestAlternatePngItemView>(cx);
12861 });
12862
12863 let fs = FakeFs::new(cx.executor());
12864 fs.insert_tree(
12865 "/root1",
12866 json!({
12867 "one.png": "BINARYDATAHERE",
12868 "two.ipynb": "{ totally a notebook }",
12869 "three.txt": "editing text, sure why not?"
12870 }),
12871 )
12872 .await;
12873 let project = Project::test(fs, ["root1".as_ref()], cx).await;
12874 let (workspace, cx) =
12875 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
12876 let worktree_id = project.update(cx, |project, cx| {
12877 project.worktrees(cx).next().unwrap().read(cx).id()
12878 });
12879
12880 let handle = workspace
12881 .update_in(cx, |workspace, window, cx| {
12882 let project_path = (worktree_id, rel_path("one.png"));
12883 workspace.open_path(project_path, None, true, window, cx)
12884 })
12885 .await
12886 .unwrap();
12887
12888 // This _must_ be the second item registered
12889 assert_eq!(
12890 handle.to_any_view().entity_type(),
12891 TypeId::of::<TestAlternatePngItemView>()
12892 );
12893
12894 let handle = workspace
12895 .update_in(cx, |workspace, window, cx| {
12896 let project_path = (worktree_id, rel_path("three.txt"));
12897 workspace.open_path(project_path, None, true, window, cx)
12898 })
12899 .await;
12900 assert!(handle.is_err());
12901 }
12902 }
12903
12904 #[gpui::test]
12905 async fn test_status_bar_visibility(cx: &mut TestAppContext) {
12906 init_test(cx);
12907
12908 let fs = FakeFs::new(cx.executor());
12909 let project = Project::test(fs, [], cx).await;
12910 let (workspace, _cx) =
12911 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
12912
12913 // Test with status bar shown (default)
12914 workspace.read_with(cx, |workspace, cx| {
12915 let visible = workspace.status_bar_visible(cx);
12916 assert!(visible, "Status bar should be visible by default");
12917 });
12918
12919 // Test with status bar hidden
12920 cx.update_global(|store: &mut SettingsStore, cx| {
12921 store.update_user_settings(cx, |settings| {
12922 settings.status_bar.get_or_insert_default().show = Some(false);
12923 });
12924 });
12925
12926 workspace.read_with(cx, |workspace, cx| {
12927 let visible = workspace.status_bar_visible(cx);
12928 assert!(!visible, "Status bar should be hidden when show is false");
12929 });
12930
12931 // Test with status bar shown explicitly
12932 cx.update_global(|store: &mut SettingsStore, cx| {
12933 store.update_user_settings(cx, |settings| {
12934 settings.status_bar.get_or_insert_default().show = Some(true);
12935 });
12936 });
12937
12938 workspace.read_with(cx, |workspace, cx| {
12939 let visible = workspace.status_bar_visible(cx);
12940 assert!(visible, "Status bar should be visible when show is true");
12941 });
12942 }
12943
12944 #[gpui::test]
12945 async fn test_pane_close_active_item(cx: &mut TestAppContext) {
12946 init_test(cx);
12947
12948 let fs = FakeFs::new(cx.executor());
12949 let project = Project::test(fs, [], cx).await;
12950 let (workspace, cx) =
12951 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12952 let panel = workspace.update_in(cx, |workspace, window, cx| {
12953 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
12954 workspace.add_panel(panel.clone(), window, cx);
12955
12956 workspace
12957 .right_dock()
12958 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
12959
12960 panel
12961 });
12962
12963 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12964 let item_a = cx.new(TestItem::new);
12965 let item_b = cx.new(TestItem::new);
12966 let item_a_id = item_a.entity_id();
12967 let item_b_id = item_b.entity_id();
12968
12969 pane.update_in(cx, |pane, window, cx| {
12970 pane.add_item(Box::new(item_a.clone()), true, true, None, window, cx);
12971 pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
12972 });
12973
12974 pane.read_with(cx, |pane, _| {
12975 assert_eq!(pane.items_len(), 2);
12976 assert_eq!(pane.active_item().unwrap().item_id(), item_b_id);
12977 });
12978
12979 workspace.update_in(cx, |workspace, window, cx| {
12980 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12981 });
12982
12983 workspace.update_in(cx, |_, window, cx| {
12984 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12985 });
12986
12987 // Assert that the `pane::CloseActiveItem` action is handled at the
12988 // workspace level when one of the dock panels is focused and, in that
12989 // case, the center pane's active item is closed but the focus is not
12990 // moved.
12991 cx.dispatch_action(pane::CloseActiveItem::default());
12992 cx.run_until_parked();
12993
12994 pane.read_with(cx, |pane, _| {
12995 assert_eq!(pane.items_len(), 1);
12996 assert_eq!(pane.active_item().unwrap().item_id(), item_a_id);
12997 });
12998
12999 workspace.update_in(cx, |workspace, window, cx| {
13000 assert!(workspace.right_dock().read(cx).is_open());
13001 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
13002 });
13003 }
13004
13005 #[gpui::test]
13006 async fn test_panel_zoom_preserved_across_workspace_switch(cx: &mut TestAppContext) {
13007 init_test(cx);
13008 let fs = FakeFs::new(cx.executor());
13009
13010 let project_a = Project::test(fs.clone(), [], cx).await;
13011 let project_b = Project::test(fs, [], cx).await;
13012
13013 let multi_workspace_handle =
13014 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
13015
13016 let workspace_a = multi_workspace_handle
13017 .read_with(cx, |mw, _| mw.workspace().clone())
13018 .unwrap();
13019
13020 let _workspace_b = multi_workspace_handle
13021 .update(cx, |mw, window, cx| {
13022 mw.test_add_workspace(project_b, window, cx)
13023 })
13024 .unwrap();
13025
13026 // Switch to workspace A
13027 multi_workspace_handle
13028 .update(cx, |mw, window, cx| {
13029 mw.activate_index(0, window, cx);
13030 })
13031 .unwrap();
13032
13033 let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
13034
13035 // Add a panel to workspace A's right dock and open the dock
13036 let panel = workspace_a.update_in(cx, |workspace, window, cx| {
13037 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
13038 workspace.add_panel(panel.clone(), window, cx);
13039 workspace
13040 .right_dock()
13041 .update(cx, |dock, cx| dock.set_open(true, window, cx));
13042 panel
13043 });
13044
13045 // Focus the panel through the workspace (matching existing test pattern)
13046 workspace_a.update_in(cx, |workspace, window, cx| {
13047 workspace.toggle_panel_focus::<TestPanel>(window, cx);
13048 });
13049
13050 // Zoom the panel
13051 panel.update_in(cx, |panel, window, cx| {
13052 panel.set_zoomed(true, window, cx);
13053 });
13054
13055 // Verify the panel is zoomed and the dock is open
13056 workspace_a.update_in(cx, |workspace, window, cx| {
13057 assert!(
13058 workspace.right_dock().read(cx).is_open(),
13059 "dock should be open before switch"
13060 );
13061 assert!(
13062 panel.is_zoomed(window, cx),
13063 "panel should be zoomed before switch"
13064 );
13065 assert!(
13066 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
13067 "panel should be focused before switch"
13068 );
13069 });
13070
13071 // Switch to workspace B
13072 multi_workspace_handle
13073 .update(cx, |mw, window, cx| {
13074 mw.activate_index(1, window, cx);
13075 })
13076 .unwrap();
13077 cx.run_until_parked();
13078
13079 // Switch back to workspace A
13080 multi_workspace_handle
13081 .update(cx, |mw, window, cx| {
13082 mw.activate_index(0, window, cx);
13083 })
13084 .unwrap();
13085 cx.run_until_parked();
13086
13087 // Verify the panel is still zoomed and the dock is still open
13088 workspace_a.update_in(cx, |workspace, window, cx| {
13089 assert!(
13090 workspace.right_dock().read(cx).is_open(),
13091 "dock should still be open after switching back"
13092 );
13093 assert!(
13094 panel.is_zoomed(window, cx),
13095 "panel should still be zoomed after switching back"
13096 );
13097 });
13098 }
13099
13100 fn pane_items_paths(pane: &Entity<Pane>, cx: &App) -> Vec<String> {
13101 pane.read(cx)
13102 .items()
13103 .flat_map(|item| {
13104 item.project_paths(cx)
13105 .into_iter()
13106 .map(|path| path.path.display(PathStyle::local()).into_owned())
13107 })
13108 .collect()
13109 }
13110
13111 pub fn init_test(cx: &mut TestAppContext) {
13112 cx.update(|cx| {
13113 let settings_store = SettingsStore::test(cx);
13114 cx.set_global(settings_store);
13115 theme::init(theme::LoadThemes::JustBase, cx);
13116 });
13117 }
13118
13119 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
13120 let item = TestProjectItem::new(id, path, cx);
13121 item.update(cx, |item, _| {
13122 item.is_dirty = true;
13123 });
13124 item
13125 }
13126
13127 #[gpui::test]
13128 async fn test_zoomed_panel_without_pane_preserved_on_center_focus(
13129 cx: &mut gpui::TestAppContext,
13130 ) {
13131 init_test(cx);
13132 let fs = FakeFs::new(cx.executor());
13133
13134 let project = Project::test(fs, [], cx).await;
13135 let (workspace, cx) =
13136 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13137
13138 let panel = workspace.update_in(cx, |workspace, window, cx| {
13139 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
13140 workspace.add_panel(panel.clone(), window, cx);
13141 workspace
13142 .right_dock()
13143 .update(cx, |dock, cx| dock.set_open(true, window, cx));
13144 panel
13145 });
13146
13147 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13148 pane.update_in(cx, |pane, window, cx| {
13149 let item = cx.new(TestItem::new);
13150 pane.add_item(Box::new(item), true, true, None, window, cx);
13151 });
13152
13153 // Transfer focus to the panel, then zoom it. Using toggle_panel_focus
13154 // mirrors the real-world flow and avoids side effects from directly
13155 // focusing the panel while the center pane is active.
13156 workspace.update_in(cx, |workspace, window, cx| {
13157 workspace.toggle_panel_focus::<TestPanel>(window, cx);
13158 });
13159
13160 panel.update_in(cx, |panel, window, cx| {
13161 panel.set_zoomed(true, window, cx);
13162 });
13163
13164 workspace.update_in(cx, |workspace, window, cx| {
13165 assert!(workspace.right_dock().read(cx).is_open());
13166 assert!(panel.is_zoomed(window, cx));
13167 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
13168 });
13169
13170 // Simulate a spurious pane::Event::Focus on the center pane while the
13171 // panel still has focus. This mirrors what happens during macOS window
13172 // activation: the center pane fires a focus event even though actual
13173 // focus remains on the dock panel.
13174 pane.update_in(cx, |_, _, cx| {
13175 cx.emit(pane::Event::Focus);
13176 });
13177
13178 // The dock must remain open because the panel had focus at the time the
13179 // event was processed. Before the fix, dock_to_preserve was None for
13180 // panels that don't implement pane(), causing the dock to close.
13181 workspace.update_in(cx, |workspace, window, cx| {
13182 assert!(
13183 workspace.right_dock().read(cx).is_open(),
13184 "Dock should stay open when its zoomed panel (without pane()) still has focus"
13185 );
13186 assert!(panel.is_zoomed(window, cx));
13187 });
13188 }
13189}