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 let paths = cx.prompt_for_paths(options);
616 cx.spawn(
617 async move |cx| match paths.await.anyhow().and_then(|res| res) {
618 Ok(Some(paths)) => {
619 cx.update(|cx| {
620 open_paths(&paths, app_state, OpenOptions::default(), cx).detach_and_log_err(cx)
621 });
622 }
623 Ok(None) => {}
624 Err(err) => {
625 util::log_err(&err);
626 cx.update(|cx| {
627 if let Some(workspace_window) = cx
628 .active_window()
629 .and_then(|window| window.downcast::<MultiWorkspace>())
630 {
631 workspace_window
632 .update(cx, |multi_workspace, _, cx| {
633 let workspace = multi_workspace.workspace().clone();
634 workspace.update(cx, |workspace, cx| {
635 workspace.show_portal_error(err.to_string(), cx);
636 });
637 })
638 .ok();
639 }
640 });
641 }
642 },
643 )
644 .detach();
645}
646
647pub fn init(app_state: Arc<AppState>, cx: &mut App) {
648 component::init();
649 theme_preview::init(cx);
650 toast_layer::init(cx);
651 history_manager::init(app_state.fs.clone(), cx);
652
653 cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx))
654 .on_action(|_: &Reload, cx| reload(cx))
655 .on_action({
656 let app_state = Arc::downgrade(&app_state);
657 move |_: &Open, cx: &mut App| {
658 if let Some(app_state) = app_state.upgrade() {
659 prompt_and_open_paths(
660 app_state,
661 PathPromptOptions {
662 files: true,
663 directories: true,
664 multiple: true,
665 prompt: None,
666 },
667 cx,
668 );
669 }
670 }
671 })
672 .on_action({
673 let app_state = Arc::downgrade(&app_state);
674 move |_: &OpenFiles, cx: &mut App| {
675 let directories = cx.can_select_mixed_files_and_dirs();
676 if let Some(app_state) = app_state.upgrade() {
677 prompt_and_open_paths(
678 app_state,
679 PathPromptOptions {
680 files: true,
681 directories,
682 multiple: true,
683 prompt: None,
684 },
685 cx,
686 );
687 }
688 }
689 });
690}
691
692type BuildProjectItemFn =
693 fn(AnyEntity, Entity<Project>, Option<&Pane>, &mut Window, &mut App) -> Box<dyn ItemHandle>;
694
695type BuildProjectItemForPathFn =
696 fn(
697 &Entity<Project>,
698 &ProjectPath,
699 &mut Window,
700 &mut App,
701 ) -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
702
703#[derive(Clone, Default)]
704struct ProjectItemRegistry {
705 build_project_item_fns_by_type: HashMap<TypeId, BuildProjectItemFn>,
706 build_project_item_for_path_fns: Vec<BuildProjectItemForPathFn>,
707}
708
709impl ProjectItemRegistry {
710 fn register<T: ProjectItem>(&mut self) {
711 self.build_project_item_fns_by_type.insert(
712 TypeId::of::<T::Item>(),
713 |item, project, pane, window, cx| {
714 let item = item.downcast().unwrap();
715 Box::new(cx.new(|cx| T::for_project_item(project, pane, item, window, cx)))
716 as Box<dyn ItemHandle>
717 },
718 );
719 self.build_project_item_for_path_fns
720 .push(|project, project_path, window, cx| {
721 let project_path = project_path.clone();
722 let is_file = project
723 .read(cx)
724 .entry_for_path(&project_path, cx)
725 .is_some_and(|entry| entry.is_file());
726 let entry_abs_path = project.read(cx).absolute_path(&project_path, cx);
727 let is_local = project.read(cx).is_local();
728 let project_item =
729 <T::Item as project::ProjectItem>::try_open(project, &project_path, cx)?;
730 let project = project.clone();
731 Some(window.spawn(cx, async move |cx| {
732 match project_item.await.with_context(|| {
733 format!(
734 "opening project path {:?}",
735 entry_abs_path.as_deref().unwrap_or(&project_path.path.as_std_path())
736 )
737 }) {
738 Ok(project_item) => {
739 let project_item = project_item;
740 let project_entry_id: Option<ProjectEntryId> =
741 project_item.read_with(cx, project::ProjectItem::entry_id);
742 let build_workspace_item = Box::new(
743 |pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
744 Box::new(cx.new(|cx| {
745 T::for_project_item(
746 project,
747 Some(pane),
748 project_item,
749 window,
750 cx,
751 )
752 })) as Box<dyn ItemHandle>
753 },
754 ) as Box<_>;
755 Ok((project_entry_id, build_workspace_item))
756 }
757 Err(e) => {
758 log::warn!("Failed to open a project item: {e:#}");
759 if e.error_code() == ErrorCode::Internal {
760 if let Some(abs_path) =
761 entry_abs_path.as_deref().filter(|_| is_file)
762 {
763 if let Some(broken_project_item_view) =
764 cx.update(|window, cx| {
765 T::for_broken_project_item(
766 abs_path, is_local, &e, window, cx,
767 )
768 })?
769 {
770 let build_workspace_item = Box::new(
771 move |_: &mut Pane, _: &mut Window, cx: &mut Context<Pane>| {
772 cx.new(|_| broken_project_item_view).boxed_clone()
773 },
774 )
775 as Box<_>;
776 return Ok((None, build_workspace_item));
777 }
778 }
779 }
780 Err(e)
781 }
782 }
783 }))
784 });
785 }
786
787 fn open_path(
788 &self,
789 project: &Entity<Project>,
790 path: &ProjectPath,
791 window: &mut Window,
792 cx: &mut App,
793 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
794 let Some(open_project_item) = self
795 .build_project_item_for_path_fns
796 .iter()
797 .rev()
798 .find_map(|open_project_item| open_project_item(project, path, window, cx))
799 else {
800 return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
801 };
802 open_project_item
803 }
804
805 fn build_item<T: project::ProjectItem>(
806 &self,
807 item: Entity<T>,
808 project: Entity<Project>,
809 pane: Option<&Pane>,
810 window: &mut Window,
811 cx: &mut App,
812 ) -> Option<Box<dyn ItemHandle>> {
813 let build = self
814 .build_project_item_fns_by_type
815 .get(&TypeId::of::<T>())?;
816 Some(build(item.into_any(), project, pane, window, cx))
817 }
818}
819
820type WorkspaceItemBuilder =
821 Box<dyn FnOnce(&mut Pane, &mut Window, &mut Context<Pane>) -> Box<dyn ItemHandle>>;
822
823impl Global for ProjectItemRegistry {}
824
825/// Registers a [ProjectItem] for the app. When opening a file, all the registered
826/// items will get a chance to open the file, starting from the project item that
827/// was added last.
828pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
829 cx.default_global::<ProjectItemRegistry>().register::<I>();
830}
831
832#[derive(Default)]
833pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
834
835struct FollowableViewDescriptor {
836 from_state_proto: fn(
837 Entity<Workspace>,
838 ViewId,
839 &mut Option<proto::view::Variant>,
840 &mut Window,
841 &mut App,
842 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
843 to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
844}
845
846impl Global for FollowableViewRegistry {}
847
848impl FollowableViewRegistry {
849 pub fn register<I: FollowableItem>(cx: &mut App) {
850 cx.default_global::<Self>().0.insert(
851 TypeId::of::<I>(),
852 FollowableViewDescriptor {
853 from_state_proto: |workspace, id, state, window, cx| {
854 I::from_state_proto(workspace, id, state, window, cx).map(|task| {
855 cx.foreground_executor()
856 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
857 })
858 },
859 to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
860 },
861 );
862 }
863
864 pub fn from_state_proto(
865 workspace: Entity<Workspace>,
866 view_id: ViewId,
867 mut state: Option<proto::view::Variant>,
868 window: &mut Window,
869 cx: &mut App,
870 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
871 cx.update_default_global(|this: &mut Self, cx| {
872 this.0.values().find_map(|descriptor| {
873 (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, window, cx)
874 })
875 })
876 }
877
878 pub fn to_followable_view(
879 view: impl Into<AnyView>,
880 cx: &App,
881 ) -> Option<Box<dyn FollowableItemHandle>> {
882 let this = cx.try_global::<Self>()?;
883 let view = view.into();
884 let descriptor = this.0.get(&view.entity_type())?;
885 Some((descriptor.to_followable_view)(&view))
886 }
887}
888
889#[derive(Copy, Clone)]
890struct SerializableItemDescriptor {
891 deserialize: fn(
892 Entity<Project>,
893 WeakEntity<Workspace>,
894 WorkspaceId,
895 ItemId,
896 &mut Window,
897 &mut Context<Pane>,
898 ) -> Task<Result<Box<dyn ItemHandle>>>,
899 cleanup: fn(WorkspaceId, Vec<ItemId>, &mut Window, &mut App) -> Task<Result<()>>,
900 view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
901}
902
903#[derive(Default)]
904struct SerializableItemRegistry {
905 descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
906 descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
907}
908
909impl Global for SerializableItemRegistry {}
910
911impl SerializableItemRegistry {
912 fn deserialize(
913 item_kind: &str,
914 project: Entity<Project>,
915 workspace: WeakEntity<Workspace>,
916 workspace_id: WorkspaceId,
917 item_item: ItemId,
918 window: &mut Window,
919 cx: &mut Context<Pane>,
920 ) -> Task<Result<Box<dyn ItemHandle>>> {
921 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
922 return Task::ready(Err(anyhow!(
923 "cannot deserialize {}, descriptor not found",
924 item_kind
925 )));
926 };
927
928 (descriptor.deserialize)(project, workspace, workspace_id, item_item, window, cx)
929 }
930
931 fn cleanup(
932 item_kind: &str,
933 workspace_id: WorkspaceId,
934 loaded_items: Vec<ItemId>,
935 window: &mut Window,
936 cx: &mut App,
937 ) -> Task<Result<()>> {
938 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
939 return Task::ready(Err(anyhow!(
940 "cannot cleanup {}, descriptor not found",
941 item_kind
942 )));
943 };
944
945 (descriptor.cleanup)(workspace_id, loaded_items, window, cx)
946 }
947
948 fn view_to_serializable_item_handle(
949 view: AnyView,
950 cx: &App,
951 ) -> Option<Box<dyn SerializableItemHandle>> {
952 let this = cx.try_global::<Self>()?;
953 let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
954 Some((descriptor.view_to_serializable_item)(view))
955 }
956
957 fn descriptor(item_kind: &str, cx: &App) -> Option<SerializableItemDescriptor> {
958 let this = cx.try_global::<Self>()?;
959 this.descriptors_by_kind.get(item_kind).copied()
960 }
961}
962
963pub fn register_serializable_item<I: SerializableItem>(cx: &mut App) {
964 let serialized_item_kind = I::serialized_item_kind();
965
966 let registry = cx.default_global::<SerializableItemRegistry>();
967 let descriptor = SerializableItemDescriptor {
968 deserialize: |project, workspace, workspace_id, item_id, window, cx| {
969 let task = I::deserialize(project, workspace, workspace_id, item_id, window, cx);
970 cx.foreground_executor()
971 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
972 },
973 cleanup: |workspace_id, loaded_items, window, cx| {
974 I::cleanup(workspace_id, loaded_items, window, cx)
975 },
976 view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
977 };
978 registry
979 .descriptors_by_kind
980 .insert(Arc::from(serialized_item_kind), descriptor);
981 registry
982 .descriptors_by_type
983 .insert(TypeId::of::<I>(), descriptor);
984}
985
986pub struct AppState {
987 pub languages: Arc<LanguageRegistry>,
988 pub client: Arc<Client>,
989 pub user_store: Entity<UserStore>,
990 pub workspace_store: Entity<WorkspaceStore>,
991 pub fs: Arc<dyn fs::Fs>,
992 pub build_window_options: fn(Option<Uuid>, &mut App) -> WindowOptions,
993 pub node_runtime: NodeRuntime,
994 pub session: Entity<AppSession>,
995}
996
997struct GlobalAppState(Weak<AppState>);
998
999impl Global for GlobalAppState {}
1000
1001pub struct WorkspaceStore {
1002 workspaces: HashSet<(gpui::AnyWindowHandle, WeakEntity<Workspace>)>,
1003 client: Arc<Client>,
1004 _subscriptions: Vec<client::Subscription>,
1005}
1006
1007#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
1008pub enum CollaboratorId {
1009 PeerId(PeerId),
1010 Agent,
1011}
1012
1013impl From<PeerId> for CollaboratorId {
1014 fn from(peer_id: PeerId) -> Self {
1015 CollaboratorId::PeerId(peer_id)
1016 }
1017}
1018
1019impl From<&PeerId> for CollaboratorId {
1020 fn from(peer_id: &PeerId) -> Self {
1021 CollaboratorId::PeerId(*peer_id)
1022 }
1023}
1024
1025#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
1026struct Follower {
1027 project_id: Option<u64>,
1028 peer_id: PeerId,
1029}
1030
1031impl AppState {
1032 #[track_caller]
1033 pub fn global(cx: &App) -> Weak<Self> {
1034 cx.global::<GlobalAppState>().0.clone()
1035 }
1036 pub fn try_global(cx: &App) -> Option<Weak<Self>> {
1037 cx.try_global::<GlobalAppState>()
1038 .map(|state| state.0.clone())
1039 }
1040 pub fn set_global(state: Weak<AppState>, cx: &mut App) {
1041 cx.set_global(GlobalAppState(state));
1042 }
1043
1044 #[cfg(any(test, feature = "test-support"))]
1045 pub fn test(cx: &mut App) -> Arc<Self> {
1046 use fs::Fs;
1047 use node_runtime::NodeRuntime;
1048 use session::Session;
1049 use settings::SettingsStore;
1050
1051 if !cx.has_global::<SettingsStore>() {
1052 let settings_store = SettingsStore::test(cx);
1053 cx.set_global(settings_store);
1054 }
1055
1056 let fs = fs::FakeFs::new(cx.background_executor().clone());
1057 <dyn Fs>::set_global(fs.clone(), cx);
1058 let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
1059 let clock = Arc::new(clock::FakeSystemClock::new());
1060 let http_client = http_client::FakeHttpClient::with_404_response();
1061 let client = Client::new(clock, http_client, cx);
1062 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
1063 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1064 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
1065
1066 theme::init(theme::LoadThemes::JustBase, cx);
1067 client::init(&client, cx);
1068
1069 Arc::new(Self {
1070 client,
1071 fs,
1072 languages,
1073 user_store,
1074 workspace_store,
1075 node_runtime: NodeRuntime::unavailable(),
1076 build_window_options: |_, _| Default::default(),
1077 session,
1078 })
1079 }
1080}
1081
1082struct DelayedDebouncedEditAction {
1083 task: Option<Task<()>>,
1084 cancel_channel: Option<oneshot::Sender<()>>,
1085}
1086
1087impl DelayedDebouncedEditAction {
1088 fn new() -> DelayedDebouncedEditAction {
1089 DelayedDebouncedEditAction {
1090 task: None,
1091 cancel_channel: None,
1092 }
1093 }
1094
1095 fn fire_new<F>(
1096 &mut self,
1097 delay: Duration,
1098 window: &mut Window,
1099 cx: &mut Context<Workspace>,
1100 func: F,
1101 ) where
1102 F: 'static
1103 + Send
1104 + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> Task<Result<()>>,
1105 {
1106 if let Some(channel) = self.cancel_channel.take() {
1107 _ = channel.send(());
1108 }
1109
1110 let (sender, mut receiver) = oneshot::channel::<()>();
1111 self.cancel_channel = Some(sender);
1112
1113 let previous_task = self.task.take();
1114 self.task = Some(cx.spawn_in(window, async move |workspace, cx| {
1115 let mut timer = cx.background_executor().timer(delay).fuse();
1116 if let Some(previous_task) = previous_task {
1117 previous_task.await;
1118 }
1119
1120 futures::select_biased! {
1121 _ = receiver => return,
1122 _ = timer => {}
1123 }
1124
1125 if let Some(result) = workspace
1126 .update_in(cx, |workspace, window, cx| (func)(workspace, window, cx))
1127 .log_err()
1128 {
1129 result.await.log_err();
1130 }
1131 }));
1132 }
1133}
1134
1135pub enum Event {
1136 PaneAdded(Entity<Pane>),
1137 PaneRemoved,
1138 ItemAdded {
1139 item: Box<dyn ItemHandle>,
1140 },
1141 ActiveItemChanged,
1142 ItemRemoved {
1143 item_id: EntityId,
1144 },
1145 UserSavedItem {
1146 pane: WeakEntity<Pane>,
1147 item: Box<dyn WeakItemHandle>,
1148 save_intent: SaveIntent,
1149 },
1150 ContactRequestedJoin(u64),
1151 WorkspaceCreated(WeakEntity<Workspace>),
1152 OpenBundledFile {
1153 text: Cow<'static, str>,
1154 title: &'static str,
1155 language: &'static str,
1156 },
1157 ZoomChanged,
1158 ModalOpened,
1159}
1160
1161#[derive(Debug, Clone)]
1162pub enum OpenVisible {
1163 All,
1164 None,
1165 OnlyFiles,
1166 OnlyDirectories,
1167}
1168
1169enum WorkspaceLocation {
1170 // Valid local paths or SSH project to serialize
1171 Location(SerializedWorkspaceLocation, PathList),
1172 // No valid location found hence clear session id
1173 DetachFromSession,
1174 // No valid location found to serialize
1175 None,
1176}
1177
1178type PromptForNewPath = Box<
1179 dyn Fn(
1180 &mut Workspace,
1181 DirectoryLister,
1182 Option<String>,
1183 &mut Window,
1184 &mut Context<Workspace>,
1185 ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
1186>;
1187
1188type PromptForOpenPath = Box<
1189 dyn Fn(
1190 &mut Workspace,
1191 DirectoryLister,
1192 &mut Window,
1193 &mut Context<Workspace>,
1194 ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
1195>;
1196
1197#[derive(Default)]
1198struct DispatchingKeystrokes {
1199 dispatched: HashSet<Vec<Keystroke>>,
1200 queue: VecDeque<Keystroke>,
1201 task: Option<Shared<Task<()>>>,
1202}
1203
1204/// Collects everything project-related for a certain window opened.
1205/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
1206///
1207/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
1208/// The `Workspace` owns everybody's state and serves as a default, "global context",
1209/// that can be used to register a global action to be triggered from any place in the window.
1210pub struct Workspace {
1211 weak_self: WeakEntity<Self>,
1212 workspace_actions: Vec<Box<dyn Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div>>,
1213 zoomed: Option<AnyWeakView>,
1214 previous_dock_drag_coordinates: Option<Point<Pixels>>,
1215 zoomed_position: Option<DockPosition>,
1216 center: PaneGroup,
1217 left_dock: Entity<Dock>,
1218 bottom_dock: Entity<Dock>,
1219 right_dock: Entity<Dock>,
1220 panes: Vec<Entity<Pane>>,
1221 active_worktree_override: Option<WorktreeId>,
1222 panes_by_item: HashMap<EntityId, WeakEntity<Pane>>,
1223 active_pane: Entity<Pane>,
1224 last_active_center_pane: Option<WeakEntity<Pane>>,
1225 last_active_view_id: Option<proto::ViewId>,
1226 status_bar: Entity<StatusBar>,
1227 modal_layer: Entity<ModalLayer>,
1228 toast_layer: Entity<ToastLayer>,
1229 titlebar_item: Option<AnyView>,
1230 notifications: Notifications,
1231 suppressed_notifications: HashSet<NotificationId>,
1232 project: Entity<Project>,
1233 follower_states: HashMap<CollaboratorId, FollowerState>,
1234 last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
1235 window_edited: bool,
1236 last_window_title: Option<String>,
1237 dirty_items: HashMap<EntityId, Subscription>,
1238 active_call: Option<(Entity<ActiveCall>, Vec<Subscription>)>,
1239 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
1240 database_id: Option<WorkspaceId>,
1241 app_state: Arc<AppState>,
1242 dispatching_keystrokes: Rc<RefCell<DispatchingKeystrokes>>,
1243 _subscriptions: Vec<Subscription>,
1244 _apply_leader_updates: Task<Result<()>>,
1245 _observe_current_user: Task<Result<()>>,
1246 _schedule_serialize_workspace: Option<Task<()>>,
1247 _schedule_serialize_ssh_paths: Option<Task<()>>,
1248 pane_history_timestamp: Arc<AtomicUsize>,
1249 bounds: Bounds<Pixels>,
1250 pub centered_layout: bool,
1251 bounds_save_task_queued: Option<Task<()>>,
1252 on_prompt_for_new_path: Option<PromptForNewPath>,
1253 on_prompt_for_open_path: Option<PromptForOpenPath>,
1254 terminal_provider: Option<Box<dyn TerminalProvider>>,
1255 debugger_provider: Option<Arc<dyn DebuggerProvider>>,
1256 serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
1257 _items_serializer: Task<Result<()>>,
1258 session_id: Option<String>,
1259 scheduled_tasks: Vec<Task<()>>,
1260 last_open_dock_positions: Vec<DockPosition>,
1261 removing: bool,
1262}
1263
1264impl EventEmitter<Event> for Workspace {}
1265
1266#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
1267pub struct ViewId {
1268 pub creator: CollaboratorId,
1269 pub id: u64,
1270}
1271
1272pub struct FollowerState {
1273 center_pane: Entity<Pane>,
1274 dock_pane: Option<Entity<Pane>>,
1275 active_view_id: Option<ViewId>,
1276 items_by_leader_view_id: HashMap<ViewId, FollowerView>,
1277}
1278
1279struct FollowerView {
1280 view: Box<dyn FollowableItemHandle>,
1281 location: Option<proto::PanelId>,
1282}
1283
1284impl Workspace {
1285 pub fn new(
1286 workspace_id: Option<WorkspaceId>,
1287 project: Entity<Project>,
1288 app_state: Arc<AppState>,
1289 window: &mut Window,
1290 cx: &mut Context<Self>,
1291 ) -> Self {
1292 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1293 cx.subscribe(&trusted_worktrees, |_, worktrees_store, e, cx| {
1294 if let TrustedWorktreesEvent::Trusted(..) = e {
1295 // Do not persist auto trusted worktrees
1296 if !ProjectSettings::get_global(cx).session.trust_all_worktrees {
1297 worktrees_store.update(cx, |worktrees_store, cx| {
1298 worktrees_store.schedule_serialization(
1299 cx,
1300 |new_trusted_worktrees, cx| {
1301 let timeout =
1302 cx.background_executor().timer(SERIALIZATION_THROTTLE_TIME);
1303 cx.background_spawn(async move {
1304 timeout.await;
1305 persistence::DB
1306 .save_trusted_worktrees(new_trusted_worktrees)
1307 .await
1308 .log_err();
1309 })
1310 },
1311 )
1312 });
1313 }
1314 }
1315 })
1316 .detach();
1317
1318 cx.observe_global::<SettingsStore>(|_, cx| {
1319 if ProjectSettings::get_global(cx).session.trust_all_worktrees {
1320 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1321 trusted_worktrees.update(cx, |trusted_worktrees, cx| {
1322 trusted_worktrees.auto_trust_all(cx);
1323 })
1324 }
1325 }
1326 })
1327 .detach();
1328 }
1329
1330 cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
1331 match event {
1332 project::Event::RemoteIdChanged(_) => {
1333 this.update_window_title(window, cx);
1334 }
1335
1336 project::Event::CollaboratorLeft(peer_id) => {
1337 this.collaborator_left(*peer_id, window, cx);
1338 }
1339
1340 &project::Event::WorktreeRemoved(id) | &project::Event::WorktreeAdded(id) => {
1341 this.update_window_title(window, cx);
1342 if this
1343 .project()
1344 .read(cx)
1345 .worktree_for_id(id, cx)
1346 .is_some_and(|wt| wt.read(cx).is_visible())
1347 {
1348 this.serialize_workspace(window, cx);
1349 this.update_history(cx);
1350 }
1351 }
1352 project::Event::WorktreeUpdatedEntries(..) => {
1353 this.update_window_title(window, cx);
1354 this.serialize_workspace(window, cx);
1355 }
1356
1357 project::Event::DisconnectedFromHost => {
1358 this.update_window_edited(window, cx);
1359 let leaders_to_unfollow =
1360 this.follower_states.keys().copied().collect::<Vec<_>>();
1361 for leader_id in leaders_to_unfollow {
1362 this.unfollow(leader_id, window, cx);
1363 }
1364 }
1365
1366 project::Event::DisconnectedFromRemote {
1367 server_not_running: _,
1368 } => {
1369 this.update_window_edited(window, cx);
1370 }
1371
1372 project::Event::Closed => {
1373 window.remove_window();
1374 }
1375
1376 project::Event::DeletedEntry(_, entry_id) => {
1377 for pane in this.panes.iter() {
1378 pane.update(cx, |pane, cx| {
1379 pane.handle_deleted_project_item(*entry_id, window, cx)
1380 });
1381 }
1382 }
1383
1384 project::Event::Toast {
1385 notification_id,
1386 message,
1387 link,
1388 } => this.show_notification(
1389 NotificationId::named(notification_id.clone()),
1390 cx,
1391 |cx| {
1392 let mut notification = MessageNotification::new(message.clone(), cx);
1393 if let Some(link) = link {
1394 notification = notification
1395 .more_info_message(link.label)
1396 .more_info_url(link.url);
1397 }
1398
1399 cx.new(|_| notification)
1400 },
1401 ),
1402
1403 project::Event::HideToast { notification_id } => {
1404 this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
1405 }
1406
1407 project::Event::LanguageServerPrompt(request) => {
1408 struct LanguageServerPrompt;
1409
1410 this.show_notification(
1411 NotificationId::composite::<LanguageServerPrompt>(request.id),
1412 cx,
1413 |cx| {
1414 cx.new(|cx| {
1415 notifications::LanguageServerPrompt::new(request.clone(), cx)
1416 })
1417 },
1418 );
1419 }
1420
1421 project::Event::AgentLocationChanged => {
1422 this.handle_agent_location_changed(window, cx)
1423 }
1424
1425 _ => {}
1426 }
1427 cx.notify()
1428 })
1429 .detach();
1430
1431 cx.subscribe_in(
1432 &project.read(cx).breakpoint_store(),
1433 window,
1434 |workspace, _, event, window, cx| match event {
1435 BreakpointStoreEvent::BreakpointsUpdated(_, _)
1436 | BreakpointStoreEvent::BreakpointsCleared(_) => {
1437 workspace.serialize_workspace(window, cx);
1438 }
1439 BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {}
1440 },
1441 )
1442 .detach();
1443 if let Some(toolchain_store) = project.read(cx).toolchain_store() {
1444 cx.subscribe_in(
1445 &toolchain_store,
1446 window,
1447 |workspace, _, event, window, cx| match event {
1448 ToolchainStoreEvent::CustomToolchainsModified => {
1449 workspace.serialize_workspace(window, cx);
1450 }
1451 _ => {}
1452 },
1453 )
1454 .detach();
1455 }
1456
1457 cx.on_focus_lost(window, |this, window, cx| {
1458 let focus_handle = this.focus_handle(cx);
1459 window.focus(&focus_handle, cx);
1460 })
1461 .detach();
1462
1463 let weak_handle = cx.entity().downgrade();
1464 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
1465
1466 let center_pane = cx.new(|cx| {
1467 let mut center_pane = Pane::new(
1468 weak_handle.clone(),
1469 project.clone(),
1470 pane_history_timestamp.clone(),
1471 None,
1472 NewFile.boxed_clone(),
1473 true,
1474 window,
1475 cx,
1476 );
1477 center_pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
1478 center_pane.set_should_display_welcome_page(true);
1479 center_pane
1480 });
1481 cx.subscribe_in(¢er_pane, window, Self::handle_pane_event)
1482 .detach();
1483
1484 window.focus(¢er_pane.focus_handle(cx), cx);
1485
1486 cx.emit(Event::PaneAdded(center_pane.clone()));
1487
1488 let any_window_handle = window.window_handle();
1489 app_state.workspace_store.update(cx, |store, _| {
1490 store
1491 .workspaces
1492 .insert((any_window_handle, weak_handle.clone()));
1493 });
1494
1495 let mut current_user = app_state.user_store.read(cx).watch_current_user();
1496 let mut connection_status = app_state.client.status();
1497 let _observe_current_user = cx.spawn_in(window, async move |this, cx| {
1498 current_user.next().await;
1499 connection_status.next().await;
1500 let mut stream =
1501 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
1502
1503 while stream.recv().await.is_some() {
1504 this.update(cx, |_, cx| cx.notify())?;
1505 }
1506 anyhow::Ok(())
1507 });
1508
1509 // All leader updates are enqueued and then processed in a single task, so
1510 // that each asynchronous operation can be run in order.
1511 let (leader_updates_tx, mut leader_updates_rx) =
1512 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
1513 let _apply_leader_updates = cx.spawn_in(window, async move |this, cx| {
1514 while let Some((leader_id, update)) = leader_updates_rx.next().await {
1515 Self::process_leader_update(&this, leader_id, update, cx)
1516 .await
1517 .log_err();
1518 }
1519
1520 Ok(())
1521 });
1522
1523 cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
1524 let modal_layer = cx.new(|_| ModalLayer::new());
1525 let toast_layer = cx.new(|_| ToastLayer::new());
1526 cx.subscribe(
1527 &modal_layer,
1528 |_, _, _: &modal_layer::ModalOpenedEvent, cx| {
1529 cx.emit(Event::ModalOpened);
1530 },
1531 )
1532 .detach();
1533
1534 let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx);
1535 let bottom_dock = Dock::new(DockPosition::Bottom, modal_layer.clone(), window, cx);
1536 let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), window, cx);
1537 let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx));
1538 let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx));
1539 let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx));
1540 let status_bar = cx.new(|cx| {
1541 let mut status_bar = StatusBar::new(¢er_pane.clone(), window, cx);
1542 status_bar.add_left_item(left_dock_buttons, window, cx);
1543 status_bar.add_right_item(right_dock_buttons, window, cx);
1544 status_bar.add_right_item(bottom_dock_buttons, window, cx);
1545 status_bar
1546 });
1547
1548 let session_id = app_state.session.read(cx).id().to_owned();
1549
1550 let mut active_call = None;
1551 if let Some(call) = ActiveCall::try_global(cx) {
1552 let subscriptions = vec![cx.subscribe_in(&call, window, Self::on_active_call_event)];
1553 active_call = Some((call, subscriptions));
1554 }
1555
1556 let (serializable_items_tx, serializable_items_rx) =
1557 mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
1558 let _items_serializer = cx.spawn_in(window, async move |this, cx| {
1559 Self::serialize_items(&this, serializable_items_rx, cx).await
1560 });
1561
1562 let subscriptions = vec![
1563 cx.observe_window_activation(window, Self::on_window_activation_changed),
1564 cx.observe_window_bounds(window, move |this, window, cx| {
1565 if this.bounds_save_task_queued.is_some() {
1566 return;
1567 }
1568 this.bounds_save_task_queued = Some(cx.spawn_in(window, async move |this, cx| {
1569 cx.background_executor()
1570 .timer(Duration::from_millis(100))
1571 .await;
1572 this.update_in(cx, |this, window, cx| {
1573 if let Some(display) = window.display(cx)
1574 && let Ok(display_uuid) = display.uuid()
1575 {
1576 let window_bounds = window.inner_window_bounds();
1577 let has_paths = !this.root_paths(cx).is_empty();
1578 if !has_paths {
1579 cx.background_executor()
1580 .spawn(persistence::write_default_window_bounds(
1581 window_bounds,
1582 display_uuid,
1583 ))
1584 .detach_and_log_err(cx);
1585 }
1586 if let Some(database_id) = workspace_id {
1587 cx.background_executor()
1588 .spawn(DB.set_window_open_status(
1589 database_id,
1590 SerializedWindowBounds(window_bounds),
1591 display_uuid,
1592 ))
1593 .detach_and_log_err(cx);
1594 } else {
1595 cx.background_executor()
1596 .spawn(persistence::write_default_window_bounds(
1597 window_bounds,
1598 display_uuid,
1599 ))
1600 .detach_and_log_err(cx);
1601 }
1602 }
1603 this.bounds_save_task_queued.take();
1604 })
1605 .ok();
1606 }));
1607 cx.notify();
1608 }),
1609 cx.observe_window_appearance(window, |_, window, cx| {
1610 let window_appearance = window.appearance();
1611
1612 *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
1613
1614 GlobalTheme::reload_theme(cx);
1615 GlobalTheme::reload_icon_theme(cx);
1616 }),
1617 cx.on_release({
1618 let weak_handle = weak_handle.clone();
1619 move |this, cx| {
1620 this.app_state.workspace_store.update(cx, move |store, _| {
1621 store.workspaces.retain(|(_, weak)| weak != &weak_handle);
1622 })
1623 }
1624 }),
1625 ];
1626
1627 cx.defer_in(window, move |this, window, cx| {
1628 this.update_window_title(window, cx);
1629 this.show_initial_notifications(cx);
1630 });
1631
1632 let mut center = PaneGroup::new(center_pane.clone());
1633 center.set_is_center(true);
1634 center.mark_positions(cx);
1635
1636 Workspace {
1637 weak_self: weak_handle.clone(),
1638 zoomed: None,
1639 zoomed_position: None,
1640 previous_dock_drag_coordinates: None,
1641 center,
1642 panes: vec![center_pane.clone()],
1643 panes_by_item: Default::default(),
1644 active_pane: center_pane.clone(),
1645 last_active_center_pane: Some(center_pane.downgrade()),
1646 last_active_view_id: None,
1647 status_bar,
1648 modal_layer,
1649 toast_layer,
1650 titlebar_item: None,
1651 active_worktree_override: None,
1652 notifications: Notifications::default(),
1653 suppressed_notifications: HashSet::default(),
1654 left_dock,
1655 bottom_dock,
1656 right_dock,
1657 project: project.clone(),
1658 follower_states: Default::default(),
1659 last_leaders_by_pane: Default::default(),
1660 dispatching_keystrokes: Default::default(),
1661 window_edited: false,
1662 last_window_title: None,
1663 dirty_items: Default::default(),
1664 active_call,
1665 database_id: workspace_id,
1666 app_state,
1667 _observe_current_user,
1668 _apply_leader_updates,
1669 _schedule_serialize_workspace: None,
1670 _schedule_serialize_ssh_paths: None,
1671 leader_updates_tx,
1672 _subscriptions: subscriptions,
1673 pane_history_timestamp,
1674 workspace_actions: Default::default(),
1675 // This data will be incorrect, but it will be overwritten by the time it needs to be used.
1676 bounds: Default::default(),
1677 centered_layout: false,
1678 bounds_save_task_queued: None,
1679 on_prompt_for_new_path: None,
1680 on_prompt_for_open_path: None,
1681 terminal_provider: None,
1682 debugger_provider: None,
1683 serializable_items_tx,
1684 _items_serializer,
1685 session_id: Some(session_id),
1686
1687 scheduled_tasks: Vec::new(),
1688 last_open_dock_positions: Vec::new(),
1689 removing: false,
1690 }
1691 }
1692
1693 pub fn new_local(
1694 abs_paths: Vec<PathBuf>,
1695 app_state: Arc<AppState>,
1696 requesting_window: Option<WindowHandle<MultiWorkspace>>,
1697 env: Option<HashMap<String, String>>,
1698 init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
1699 cx: &mut App,
1700 ) -> Task<
1701 anyhow::Result<(
1702 WindowHandle<MultiWorkspace>,
1703 Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
1704 )>,
1705 > {
1706 let project_handle = Project::local(
1707 app_state.client.clone(),
1708 app_state.node_runtime.clone(),
1709 app_state.user_store.clone(),
1710 app_state.languages.clone(),
1711 app_state.fs.clone(),
1712 env,
1713 Default::default(),
1714 cx,
1715 );
1716
1717 cx.spawn(async move |cx| {
1718 let mut paths_to_open = Vec::with_capacity(abs_paths.len());
1719 for path in abs_paths.into_iter() {
1720 if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() {
1721 paths_to_open.push(canonical)
1722 } else {
1723 paths_to_open.push(path)
1724 }
1725 }
1726
1727 let serialized_workspace =
1728 persistence::DB.workspace_for_roots(paths_to_open.as_slice());
1729
1730 if let Some(paths) = serialized_workspace.as_ref().map(|ws| &ws.paths) {
1731 paths_to_open = paths.ordered_paths().cloned().collect();
1732 if !paths.is_lexicographically_ordered() {
1733 project_handle.update(cx, |project, cx| {
1734 project.set_worktrees_reordered(true, cx);
1735 });
1736 }
1737 }
1738
1739 // Get project paths for all of the abs_paths
1740 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
1741 Vec::with_capacity(paths_to_open.len());
1742
1743 for path in paths_to_open.into_iter() {
1744 if let Some((_, project_entry)) = cx
1745 .update(|cx| {
1746 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
1747 })
1748 .await
1749 .log_err()
1750 {
1751 project_paths.push((path, Some(project_entry)));
1752 } else {
1753 project_paths.push((path, None));
1754 }
1755 }
1756
1757 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
1758 serialized_workspace.id
1759 } else {
1760 DB.next_id().await.unwrap_or_else(|_| Default::default())
1761 };
1762
1763 let toolchains = DB.toolchains(workspace_id).await?;
1764
1765 for (toolchain, worktree_path, path) in toolchains {
1766 let toolchain_path = PathBuf::from(toolchain.path.clone().to_string());
1767 let Some(worktree_id) = project_handle.read_with(cx, |this, cx| {
1768 this.find_worktree(&worktree_path, cx)
1769 .and_then(|(worktree, rel_path)| {
1770 if rel_path.is_empty() {
1771 Some(worktree.read(cx).id())
1772 } else {
1773 None
1774 }
1775 })
1776 }) else {
1777 // We did not find a worktree with a given path, but that's whatever.
1778 continue;
1779 };
1780 if !app_state.fs.is_file(toolchain_path.as_path()).await {
1781 continue;
1782 }
1783
1784 project_handle
1785 .update(cx, |this, cx| {
1786 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
1787 })
1788 .await;
1789 }
1790 if let Some(workspace) = serialized_workspace.as_ref() {
1791 project_handle.update(cx, |this, cx| {
1792 for (scope, toolchains) in &workspace.user_toolchains {
1793 for toolchain in toolchains {
1794 this.add_toolchain(toolchain.clone(), scope.clone(), cx);
1795 }
1796 }
1797 });
1798 }
1799
1800 let (window, workspace): (WindowHandle<MultiWorkspace>, Entity<Workspace>) =
1801 if let Some(window) = requesting_window {
1802 let centered_layout = serialized_workspace
1803 .as_ref()
1804 .map(|w| w.centered_layout)
1805 .unwrap_or(false);
1806
1807 let workspace = window.update(cx, |multi_workspace, window, cx| {
1808 let workspace = cx.new(|cx| {
1809 let mut workspace = Workspace::new(
1810 Some(workspace_id),
1811 project_handle.clone(),
1812 app_state.clone(),
1813 window,
1814 cx,
1815 );
1816
1817 workspace.centered_layout = centered_layout;
1818
1819 // Call init callback to add items before window renders
1820 if let Some(init) = init {
1821 init(&mut workspace, window, cx);
1822 }
1823
1824 workspace
1825 });
1826 multi_workspace.activate(workspace.clone(), cx);
1827 workspace
1828 })?;
1829 (window, workspace)
1830 } else {
1831 let window_bounds_override = window_bounds_env_override();
1832
1833 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
1834 (Some(WindowBounds::Windowed(bounds)), None)
1835 } else if let Some(workspace) = serialized_workspace.as_ref()
1836 && let Some(display) = workspace.display
1837 && let Some(bounds) = workspace.window_bounds.as_ref()
1838 {
1839 // Reopening an existing workspace - restore its saved bounds
1840 (Some(bounds.0), Some(display))
1841 } else if let Some((display, bounds)) =
1842 persistence::read_default_window_bounds()
1843 {
1844 // New or empty workspace - use the last known window bounds
1845 (Some(bounds), Some(display))
1846 } else {
1847 // New window - let GPUI's default_bounds() handle cascading
1848 (None, None)
1849 };
1850
1851 // Use the serialized workspace to construct the new window
1852 let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx));
1853 options.window_bounds = window_bounds;
1854 let centered_layout = serialized_workspace
1855 .as_ref()
1856 .map(|w| w.centered_layout)
1857 .unwrap_or(false);
1858 let window = cx.open_window(options, {
1859 let app_state = app_state.clone();
1860 let project_handle = project_handle.clone();
1861 move |window, cx| {
1862 let workspace = cx.new(|cx| {
1863 let mut workspace = Workspace::new(
1864 Some(workspace_id),
1865 project_handle,
1866 app_state,
1867 window,
1868 cx,
1869 );
1870 workspace.centered_layout = centered_layout;
1871
1872 // Call init callback to add items before window renders
1873 if let Some(init) = init {
1874 init(&mut workspace, window, cx);
1875 }
1876
1877 workspace
1878 });
1879 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
1880 }
1881 })?;
1882 let workspace =
1883 window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
1884 multi_workspace.workspace().clone()
1885 })?;
1886 (window, workspace)
1887 };
1888
1889 notify_if_database_failed(window, cx);
1890 // Check if this is an empty workspace (no paths to open)
1891 // An empty workspace is one where project_paths is empty
1892 let is_empty_workspace = project_paths.is_empty();
1893 // Check if serialized workspace has paths before it's moved
1894 let serialized_workspace_has_paths = serialized_workspace
1895 .as_ref()
1896 .map(|ws| !ws.paths.is_empty())
1897 .unwrap_or(false);
1898
1899 let opened_items = window
1900 .update(cx, |_, window, cx| {
1901 workspace.update(cx, |_workspace: &mut Workspace, cx| {
1902 open_items(serialized_workspace, project_paths, window, cx)
1903 })
1904 })?
1905 .await
1906 .unwrap_or_default();
1907
1908 // Restore default dock state for empty workspaces
1909 // Only restore if:
1910 // 1. This is an empty workspace (no paths), AND
1911 // 2. The serialized workspace either doesn't exist or has no paths
1912 if is_empty_workspace && !serialized_workspace_has_paths {
1913 if let Some(default_docks) = persistence::read_default_dock_state() {
1914 window
1915 .update(cx, |_, window, cx| {
1916 workspace.update(cx, |workspace, cx| {
1917 for (dock, serialized_dock) in [
1918 (&workspace.right_dock, &default_docks.right),
1919 (&workspace.left_dock, &default_docks.left),
1920 (&workspace.bottom_dock, &default_docks.bottom),
1921 ] {
1922 dock.update(cx, |dock, cx| {
1923 dock.serialized_dock = Some(serialized_dock.clone());
1924 dock.restore_state(window, cx);
1925 });
1926 }
1927 cx.notify();
1928 });
1929 })
1930 .log_err();
1931 }
1932 }
1933
1934 window
1935 .update(cx, |_, _window, cx| {
1936 workspace.update(cx, |this: &mut Workspace, cx| {
1937 this.update_history(cx);
1938 });
1939 })
1940 .log_err();
1941 Ok((window, opened_items))
1942 })
1943 }
1944
1945 pub fn weak_handle(&self) -> WeakEntity<Self> {
1946 self.weak_self.clone()
1947 }
1948
1949 pub fn left_dock(&self) -> &Entity<Dock> {
1950 &self.left_dock
1951 }
1952
1953 pub fn bottom_dock(&self) -> &Entity<Dock> {
1954 &self.bottom_dock
1955 }
1956
1957 pub fn set_bottom_dock_layout(
1958 &mut self,
1959 layout: BottomDockLayout,
1960 window: &mut Window,
1961 cx: &mut Context<Self>,
1962 ) {
1963 let fs = self.project().read(cx).fs();
1964 settings::update_settings_file(fs.clone(), cx, move |content, _cx| {
1965 content.workspace.bottom_dock_layout = Some(layout);
1966 });
1967
1968 cx.notify();
1969 self.serialize_workspace(window, cx);
1970 }
1971
1972 pub fn right_dock(&self) -> &Entity<Dock> {
1973 &self.right_dock
1974 }
1975
1976 pub fn all_docks(&self) -> [&Entity<Dock>; 3] {
1977 [&self.left_dock, &self.bottom_dock, &self.right_dock]
1978 }
1979
1980 pub fn dock_at_position(&self, position: DockPosition) -> &Entity<Dock> {
1981 match position {
1982 DockPosition::Left => &self.left_dock,
1983 DockPosition::Bottom => &self.bottom_dock,
1984 DockPosition::Right => &self.right_dock,
1985 }
1986 }
1987
1988 pub fn is_edited(&self) -> bool {
1989 self.window_edited
1990 }
1991
1992 pub fn add_panel<T: Panel>(
1993 &mut self,
1994 panel: Entity<T>,
1995 window: &mut Window,
1996 cx: &mut Context<Self>,
1997 ) {
1998 let focus_handle = panel.panel_focus_handle(cx);
1999 cx.on_focus_in(&focus_handle, window, Self::handle_panel_focused)
2000 .detach();
2001
2002 let dock_position = panel.position(window, cx);
2003 let dock = self.dock_at_position(dock_position);
2004
2005 dock.update(cx, |dock, cx| {
2006 dock.add_panel(panel, self.weak_self.clone(), window, cx)
2007 });
2008 }
2009
2010 pub fn remove_panel<T: Panel>(
2011 &mut self,
2012 panel: &Entity<T>,
2013 window: &mut Window,
2014 cx: &mut Context<Self>,
2015 ) {
2016 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2017 dock.update(cx, |dock, cx| dock.remove_panel(panel, window, cx));
2018 }
2019 }
2020
2021 pub fn status_bar(&self) -> &Entity<StatusBar> {
2022 &self.status_bar
2023 }
2024
2025 pub fn set_workspace_sidebar_open(&self, open: bool, cx: &mut App) {
2026 self.status_bar.update(cx, |status_bar, cx| {
2027 status_bar.set_workspace_sidebar_open(open, cx);
2028 });
2029 }
2030
2031 pub fn status_bar_visible(&self, cx: &App) -> bool {
2032 StatusBarSettings::get_global(cx).show
2033 }
2034
2035 pub fn app_state(&self) -> &Arc<AppState> {
2036 &self.app_state
2037 }
2038
2039 pub fn user_store(&self) -> &Entity<UserStore> {
2040 &self.app_state.user_store
2041 }
2042
2043 pub fn project(&self) -> &Entity<Project> {
2044 &self.project
2045 }
2046
2047 pub fn path_style(&self, cx: &App) -> PathStyle {
2048 self.project.read(cx).path_style(cx)
2049 }
2050
2051 pub fn recently_activated_items(&self, cx: &App) -> HashMap<EntityId, usize> {
2052 let mut history: HashMap<EntityId, usize> = HashMap::default();
2053
2054 for pane_handle in &self.panes {
2055 let pane = pane_handle.read(cx);
2056
2057 for entry in pane.activation_history() {
2058 history.insert(
2059 entry.entity_id,
2060 history
2061 .get(&entry.entity_id)
2062 .cloned()
2063 .unwrap_or(0)
2064 .max(entry.timestamp),
2065 );
2066 }
2067 }
2068
2069 history
2070 }
2071
2072 pub fn recent_active_item_by_type<T: 'static>(&self, cx: &App) -> Option<Entity<T>> {
2073 let mut recent_item: Option<Entity<T>> = None;
2074 let mut recent_timestamp = 0;
2075 for pane_handle in &self.panes {
2076 let pane = pane_handle.read(cx);
2077 let item_map: HashMap<EntityId, &Box<dyn ItemHandle>> =
2078 pane.items().map(|item| (item.item_id(), item)).collect();
2079 for entry in pane.activation_history() {
2080 if entry.timestamp > recent_timestamp
2081 && let Some(&item) = item_map.get(&entry.entity_id)
2082 && let Some(typed_item) = item.act_as::<T>(cx)
2083 {
2084 recent_timestamp = entry.timestamp;
2085 recent_item = Some(typed_item);
2086 }
2087 }
2088 }
2089 recent_item
2090 }
2091
2092 pub fn recent_navigation_history_iter(
2093 &self,
2094 cx: &App,
2095 ) -> impl Iterator<Item = (ProjectPath, Option<PathBuf>)> + use<> {
2096 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
2097 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
2098
2099 for pane in &self.panes {
2100 let pane = pane.read(cx);
2101
2102 pane.nav_history()
2103 .for_each_entry(cx, |entry, (project_path, fs_path)| {
2104 if let Some(fs_path) = &fs_path {
2105 abs_paths_opened
2106 .entry(fs_path.clone())
2107 .or_default()
2108 .insert(project_path.clone());
2109 }
2110 let timestamp = entry.timestamp;
2111 match history.entry(project_path) {
2112 hash_map::Entry::Occupied(mut entry) => {
2113 let (_, old_timestamp) = entry.get();
2114 if ×tamp > old_timestamp {
2115 entry.insert((fs_path, timestamp));
2116 }
2117 }
2118 hash_map::Entry::Vacant(entry) => {
2119 entry.insert((fs_path, timestamp));
2120 }
2121 }
2122 });
2123
2124 if let Some(item) = pane.active_item()
2125 && let Some(project_path) = item.project_path(cx)
2126 {
2127 let fs_path = self.project.read(cx).absolute_path(&project_path, cx);
2128
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
2136 history.insert(project_path, (fs_path, std::usize::MAX));
2137 }
2138 }
2139
2140 history
2141 .into_iter()
2142 .sorted_by_key(|(_, (_, order))| *order)
2143 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
2144 .rev()
2145 .filter(move |(history_path, abs_path)| {
2146 let latest_project_path_opened = abs_path
2147 .as_ref()
2148 .and_then(|abs_path| abs_paths_opened.get(abs_path))
2149 .and_then(|project_paths| {
2150 project_paths
2151 .iter()
2152 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
2153 });
2154
2155 latest_project_path_opened.is_none_or(|path| path == history_path)
2156 })
2157 }
2158
2159 pub fn recent_navigation_history(
2160 &self,
2161 limit: Option<usize>,
2162 cx: &App,
2163 ) -> Vec<(ProjectPath, Option<PathBuf>)> {
2164 self.recent_navigation_history_iter(cx)
2165 .take(limit.unwrap_or(usize::MAX))
2166 .collect()
2167 }
2168
2169 pub fn clear_navigation_history(&mut self, _window: &mut Window, cx: &mut Context<Workspace>) {
2170 for pane in &self.panes {
2171 pane.update(cx, |pane, cx| pane.nav_history_mut().clear(cx));
2172 }
2173 }
2174
2175 fn navigate_history(
2176 &mut self,
2177 pane: WeakEntity<Pane>,
2178 mode: NavigationMode,
2179 window: &mut Window,
2180 cx: &mut Context<Workspace>,
2181 ) -> Task<Result<()>> {
2182 self.navigate_history_impl(pane, mode, window, |history, cx| history.pop(mode, cx), cx)
2183 }
2184
2185 fn navigate_tag_history(
2186 &mut self,
2187 pane: WeakEntity<Pane>,
2188 mode: TagNavigationMode,
2189 window: &mut Window,
2190 cx: &mut Context<Workspace>,
2191 ) -> Task<Result<()>> {
2192 self.navigate_history_impl(
2193 pane,
2194 NavigationMode::Normal,
2195 window,
2196 |history, _cx| history.pop_tag(mode),
2197 cx,
2198 )
2199 }
2200
2201 fn navigate_history_impl(
2202 &mut self,
2203 pane: WeakEntity<Pane>,
2204 mode: NavigationMode,
2205 window: &mut Window,
2206 mut cb: impl FnMut(&mut NavHistory, &mut App) -> Option<NavigationEntry>,
2207 cx: &mut Context<Workspace>,
2208 ) -> Task<Result<()>> {
2209 let to_load = if let Some(pane) = pane.upgrade() {
2210 pane.update(cx, |pane, cx| {
2211 window.focus(&pane.focus_handle(cx), cx);
2212 loop {
2213 // Retrieve the weak item handle from the history.
2214 let entry = cb(pane.nav_history_mut(), cx)?;
2215
2216 // If the item is still present in this pane, then activate it.
2217 if let Some(index) = entry
2218 .item
2219 .upgrade()
2220 .and_then(|v| pane.index_for_item(v.as_ref()))
2221 {
2222 let prev_active_item_index = pane.active_item_index();
2223 pane.nav_history_mut().set_mode(mode);
2224 pane.activate_item(index, true, true, window, cx);
2225 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2226
2227 let mut navigated = prev_active_item_index != pane.active_item_index();
2228 if let Some(data) = entry.data {
2229 navigated |= pane.active_item()?.navigate(data, window, cx);
2230 }
2231
2232 if navigated {
2233 break None;
2234 }
2235 } else {
2236 // If the item is no longer present in this pane, then retrieve its
2237 // path info in order to reopen it.
2238 break pane
2239 .nav_history()
2240 .path_for_item(entry.item.id())
2241 .map(|(project_path, abs_path)| (project_path, abs_path, entry));
2242 }
2243 }
2244 })
2245 } else {
2246 None
2247 };
2248
2249 if let Some((project_path, abs_path, entry)) = to_load {
2250 // If the item was no longer present, then load it again from its previous path, first try the local path
2251 let open_by_project_path = self.load_path(project_path.clone(), window, cx);
2252
2253 cx.spawn_in(window, async move |workspace, cx| {
2254 let open_by_project_path = open_by_project_path.await;
2255 let mut navigated = false;
2256 match open_by_project_path
2257 .with_context(|| format!("Navigating to {project_path:?}"))
2258 {
2259 Ok((project_entry_id, build_item)) => {
2260 let prev_active_item_id = pane.update(cx, |pane, _| {
2261 pane.nav_history_mut().set_mode(mode);
2262 pane.active_item().map(|p| p.item_id())
2263 })?;
2264
2265 pane.update_in(cx, |pane, window, cx| {
2266 let item = pane.open_item(
2267 project_entry_id,
2268 project_path,
2269 true,
2270 entry.is_preview,
2271 true,
2272 None,
2273 window, cx,
2274 build_item,
2275 );
2276 navigated |= Some(item.item_id()) != prev_active_item_id;
2277 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2278 if let Some(data) = entry.data {
2279 navigated |= item.navigate(data, window, cx);
2280 }
2281 })?;
2282 }
2283 Err(open_by_project_path_e) => {
2284 // Fall back to opening by abs path, in case an external file was opened and closed,
2285 // and its worktree is now dropped
2286 if let Some(abs_path) = abs_path {
2287 let prev_active_item_id = pane.update(cx, |pane, _| {
2288 pane.nav_history_mut().set_mode(mode);
2289 pane.active_item().map(|p| p.item_id())
2290 })?;
2291 let open_by_abs_path = workspace.update_in(cx, |workspace, window, cx| {
2292 workspace.open_abs_path(abs_path.clone(), OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
2293 })?;
2294 match open_by_abs_path
2295 .await
2296 .with_context(|| format!("Navigating to {abs_path:?}"))
2297 {
2298 Ok(item) => {
2299 pane.update_in(cx, |pane, window, cx| {
2300 navigated |= Some(item.item_id()) != prev_active_item_id;
2301 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2302 if let Some(data) = entry.data {
2303 navigated |= item.navigate(data, window, cx);
2304 }
2305 })?;
2306 }
2307 Err(open_by_abs_path_e) => {
2308 log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
2309 }
2310 }
2311 }
2312 }
2313 }
2314
2315 if !navigated {
2316 workspace
2317 .update_in(cx, |workspace, window, cx| {
2318 Self::navigate_history(workspace, pane, mode, window, cx)
2319 })?
2320 .await?;
2321 }
2322
2323 Ok(())
2324 })
2325 } else {
2326 Task::ready(Ok(()))
2327 }
2328 }
2329
2330 pub fn go_back(
2331 &mut self,
2332 pane: WeakEntity<Pane>,
2333 window: &mut Window,
2334 cx: &mut Context<Workspace>,
2335 ) -> Task<Result<()>> {
2336 self.navigate_history(pane, NavigationMode::GoingBack, window, cx)
2337 }
2338
2339 pub fn go_forward(
2340 &mut self,
2341 pane: WeakEntity<Pane>,
2342 window: &mut Window,
2343 cx: &mut Context<Workspace>,
2344 ) -> Task<Result<()>> {
2345 self.navigate_history(pane, NavigationMode::GoingForward, window, cx)
2346 }
2347
2348 pub fn reopen_closed_item(
2349 &mut self,
2350 window: &mut Window,
2351 cx: &mut Context<Workspace>,
2352 ) -> Task<Result<()>> {
2353 self.navigate_history(
2354 self.active_pane().downgrade(),
2355 NavigationMode::ReopeningClosedItem,
2356 window,
2357 cx,
2358 )
2359 }
2360
2361 pub fn client(&self) -> &Arc<Client> {
2362 &self.app_state.client
2363 }
2364
2365 pub fn set_titlebar_item(&mut self, item: AnyView, _: &mut Window, cx: &mut Context<Self>) {
2366 self.titlebar_item = Some(item);
2367 cx.notify();
2368 }
2369
2370 pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
2371 self.on_prompt_for_new_path = Some(prompt)
2372 }
2373
2374 pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) {
2375 self.on_prompt_for_open_path = Some(prompt)
2376 }
2377
2378 pub fn set_terminal_provider(&mut self, provider: impl TerminalProvider + 'static) {
2379 self.terminal_provider = Some(Box::new(provider));
2380 }
2381
2382 pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) {
2383 self.debugger_provider = Some(Arc::new(provider));
2384 }
2385
2386 pub fn debugger_provider(&self) -> Option<Arc<dyn DebuggerProvider>> {
2387 self.debugger_provider.clone()
2388 }
2389
2390 pub fn prompt_for_open_path(
2391 &mut self,
2392 path_prompt_options: PathPromptOptions,
2393 lister: DirectoryLister,
2394 window: &mut Window,
2395 cx: &mut Context<Self>,
2396 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
2397 if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
2398 let prompt = self.on_prompt_for_open_path.take().unwrap();
2399 let rx = prompt(self, lister, window, cx);
2400 self.on_prompt_for_open_path = Some(prompt);
2401 rx
2402 } else {
2403 let (tx, rx) = oneshot::channel();
2404 let abs_path = cx.prompt_for_paths(path_prompt_options);
2405
2406 cx.spawn_in(window, async move |workspace, cx| {
2407 let Ok(result) = abs_path.await else {
2408 return Ok(());
2409 };
2410
2411 match result {
2412 Ok(result) => {
2413 tx.send(result).ok();
2414 }
2415 Err(err) => {
2416 let rx = workspace.update_in(cx, |workspace, window, cx| {
2417 workspace.show_portal_error(err.to_string(), cx);
2418 let prompt = workspace.on_prompt_for_open_path.take().unwrap();
2419 let rx = prompt(workspace, lister, window, cx);
2420 workspace.on_prompt_for_open_path = Some(prompt);
2421 rx
2422 })?;
2423 if let Ok(path) = rx.await {
2424 tx.send(path).ok();
2425 }
2426 }
2427 };
2428 anyhow::Ok(())
2429 })
2430 .detach();
2431
2432 rx
2433 }
2434 }
2435
2436 pub fn prompt_for_new_path(
2437 &mut self,
2438 lister: DirectoryLister,
2439 suggested_name: Option<String>,
2440 window: &mut Window,
2441 cx: &mut Context<Self>,
2442 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
2443 if self.project.read(cx).is_via_collab()
2444 || self.project.read(cx).is_via_remote_server()
2445 || !WorkspaceSettings::get_global(cx).use_system_path_prompts
2446 {
2447 let prompt = self.on_prompt_for_new_path.take().unwrap();
2448 let rx = prompt(self, lister, suggested_name, window, cx);
2449 self.on_prompt_for_new_path = Some(prompt);
2450 return rx;
2451 }
2452
2453 let (tx, rx) = oneshot::channel();
2454 cx.spawn_in(window, async move |workspace, cx| {
2455 let abs_path = workspace.update(cx, |workspace, cx| {
2456 let relative_to = workspace
2457 .most_recent_active_path(cx)
2458 .and_then(|p| p.parent().map(|p| p.to_path_buf()))
2459 .or_else(|| {
2460 let project = workspace.project.read(cx);
2461 project.visible_worktrees(cx).find_map(|worktree| {
2462 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
2463 })
2464 })
2465 .or_else(std::env::home_dir)
2466 .unwrap_or_else(|| PathBuf::from(""));
2467 cx.prompt_for_new_path(&relative_to, suggested_name.as_deref())
2468 })?;
2469 let abs_path = match abs_path.await? {
2470 Ok(path) => path,
2471 Err(err) => {
2472 let rx = workspace.update_in(cx, |workspace, window, cx| {
2473 workspace.show_portal_error(err.to_string(), cx);
2474
2475 let prompt = workspace.on_prompt_for_new_path.take().unwrap();
2476 let rx = prompt(workspace, lister, suggested_name, window, cx);
2477 workspace.on_prompt_for_new_path = Some(prompt);
2478 rx
2479 })?;
2480 if let Ok(path) = rx.await {
2481 tx.send(path).ok();
2482 }
2483 return anyhow::Ok(());
2484 }
2485 };
2486
2487 tx.send(abs_path.map(|path| vec![path])).ok();
2488 anyhow::Ok(())
2489 })
2490 .detach();
2491
2492 rx
2493 }
2494
2495 pub fn titlebar_item(&self) -> Option<AnyView> {
2496 self.titlebar_item.clone()
2497 }
2498
2499 /// Returns the worktree override set by the user (e.g., via the project dropdown).
2500 /// When set, git-related operations should use this worktree instead of deriving
2501 /// the active worktree from the focused file.
2502 pub fn active_worktree_override(&self) -> Option<WorktreeId> {
2503 self.active_worktree_override
2504 }
2505
2506 pub fn set_active_worktree_override(
2507 &mut self,
2508 worktree_id: Option<WorktreeId>,
2509 cx: &mut Context<Self>,
2510 ) {
2511 self.active_worktree_override = worktree_id;
2512 cx.notify();
2513 }
2514
2515 pub fn clear_active_worktree_override(&mut self, cx: &mut Context<Self>) {
2516 self.active_worktree_override = None;
2517 cx.notify();
2518 }
2519
2520 /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
2521 ///
2522 /// If the given workspace has a local project, then it will be passed
2523 /// to the callback. Otherwise, a new empty window will be created.
2524 pub fn with_local_workspace<T, F>(
2525 &mut self,
2526 window: &mut Window,
2527 cx: &mut Context<Self>,
2528 callback: F,
2529 ) -> Task<Result<T>>
2530 where
2531 T: 'static,
2532 F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
2533 {
2534 if self.project.read(cx).is_local() {
2535 Task::ready(Ok(callback(self, window, cx)))
2536 } else {
2537 let env = self.project.read(cx).cli_environment(cx);
2538 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, None, cx);
2539 cx.spawn_in(window, async move |_vh, cx| {
2540 let (multi_workspace_window, _) = task.await?;
2541 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
2542 let workspace = multi_workspace.workspace().clone();
2543 workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
2544 })
2545 })
2546 }
2547 }
2548
2549 /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
2550 ///
2551 /// If the given workspace has a local project, then it will be passed
2552 /// to the callback. Otherwise, a new empty window will be created.
2553 pub fn with_local_or_wsl_workspace<T, F>(
2554 &mut self,
2555 window: &mut Window,
2556 cx: &mut Context<Self>,
2557 callback: F,
2558 ) -> Task<Result<T>>
2559 where
2560 T: 'static,
2561 F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
2562 {
2563 let project = self.project.read(cx);
2564 if project.is_local() || project.is_via_wsl_with_host_interop(cx) {
2565 Task::ready(Ok(callback(self, window, cx)))
2566 } else {
2567 let env = self.project.read(cx).cli_environment(cx);
2568 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, None, cx);
2569 cx.spawn_in(window, async move |_vh, cx| {
2570 let (multi_workspace_window, _) = task.await?;
2571 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
2572 let workspace = multi_workspace.workspace().clone();
2573 workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
2574 })
2575 })
2576 }
2577 }
2578
2579 pub fn worktrees<'a>(&self, cx: &'a App) -> impl 'a + Iterator<Item = Entity<Worktree>> {
2580 self.project.read(cx).worktrees(cx)
2581 }
2582
2583 pub fn visible_worktrees<'a>(
2584 &self,
2585 cx: &'a App,
2586 ) -> impl 'a + Iterator<Item = Entity<Worktree>> {
2587 self.project.read(cx).visible_worktrees(cx)
2588 }
2589
2590 #[cfg(any(test, feature = "test-support"))]
2591 pub fn worktree_scans_complete(&self, cx: &App) -> impl Future<Output = ()> + 'static + use<> {
2592 let futures = self
2593 .worktrees(cx)
2594 .filter_map(|worktree| worktree.read(cx).as_local())
2595 .map(|worktree| worktree.scan_complete())
2596 .collect::<Vec<_>>();
2597 async move {
2598 for future in futures {
2599 future.await;
2600 }
2601 }
2602 }
2603
2604 pub fn close_global(cx: &mut App) {
2605 cx.defer(|cx| {
2606 cx.windows().iter().find(|window| {
2607 window
2608 .update(cx, |_, window, _| {
2609 if window.is_window_active() {
2610 //This can only get called when the window's project connection has been lost
2611 //so we don't need to prompt the user for anything and instead just close the window
2612 window.remove_window();
2613 true
2614 } else {
2615 false
2616 }
2617 })
2618 .unwrap_or(false)
2619 });
2620 });
2621 }
2622
2623 pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
2624 let prepare = self.prepare_to_close(CloseIntent::CloseWindow, window, cx);
2625 cx.spawn_in(window, async move |_, cx| {
2626 if prepare.await? {
2627 cx.update(|window, _cx| window.remove_window())?;
2628 }
2629 anyhow::Ok(())
2630 })
2631 .detach_and_log_err(cx)
2632 }
2633
2634 pub fn move_focused_panel_to_next_position(
2635 &mut self,
2636 _: &MoveFocusedPanelToNextPosition,
2637 window: &mut Window,
2638 cx: &mut Context<Self>,
2639 ) {
2640 let docks = self.all_docks();
2641 let active_dock = docks
2642 .into_iter()
2643 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
2644
2645 if let Some(dock) = active_dock {
2646 dock.update(cx, |dock, cx| {
2647 let active_panel = dock
2648 .active_panel()
2649 .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx));
2650
2651 if let Some(panel) = active_panel {
2652 panel.move_to_next_position(window, cx);
2653 }
2654 })
2655 }
2656 }
2657
2658 pub fn prepare_to_close(
2659 &mut self,
2660 close_intent: CloseIntent,
2661 window: &mut Window,
2662 cx: &mut Context<Self>,
2663 ) -> Task<Result<bool>> {
2664 let active_call = self.active_call().cloned();
2665
2666 cx.spawn_in(window, async move |this, cx| {
2667 this.update(cx, |this, _| {
2668 if close_intent == CloseIntent::CloseWindow {
2669 this.removing = true;
2670 }
2671 })?;
2672
2673 let workspace_count = cx.update(|_window, cx| {
2674 cx.windows()
2675 .iter()
2676 .filter(|window| window.downcast::<MultiWorkspace>().is_some())
2677 .count()
2678 })?;
2679
2680 #[cfg(target_os = "macos")]
2681 let save_last_workspace = false;
2682
2683 // On Linux and Windows, closing the last window should restore the last workspace.
2684 #[cfg(not(target_os = "macos"))]
2685 let save_last_workspace = {
2686 let remaining_workspaces = cx.update(|_window, cx| {
2687 cx.windows()
2688 .iter()
2689 .filter_map(|window| window.downcast::<MultiWorkspace>())
2690 .filter_map(|multi_workspace| {
2691 multi_workspace
2692 .update(cx, |multi_workspace, _, cx| {
2693 multi_workspace.workspace().read(cx).removing
2694 })
2695 .ok()
2696 })
2697 .filter(|removing| !removing)
2698 .count()
2699 })?;
2700
2701 close_intent != CloseIntent::ReplaceWindow && remaining_workspaces == 0
2702 };
2703
2704 if let Some(active_call) = active_call
2705 && workspace_count == 1
2706 && active_call.read_with(cx, |call, _| call.room().is_some())
2707 {
2708 if close_intent == CloseIntent::CloseWindow {
2709 let answer = cx.update(|window, cx| {
2710 window.prompt(
2711 PromptLevel::Warning,
2712 "Do you want to leave the current call?",
2713 None,
2714 &["Close window and hang up", "Cancel"],
2715 cx,
2716 )
2717 })?;
2718
2719 if answer.await.log_err() == Some(1) {
2720 return anyhow::Ok(false);
2721 } else {
2722 active_call
2723 .update(cx, |call, cx| call.hang_up(cx))
2724 .await
2725 .log_err();
2726 }
2727 }
2728 if close_intent == CloseIntent::ReplaceWindow {
2729 _ = active_call.update(cx, |this, cx| {
2730 let multi_workspace = cx
2731 .windows()
2732 .iter()
2733 .filter_map(|window| window.downcast::<MultiWorkspace>())
2734 .next()
2735 .unwrap();
2736 let project = multi_workspace
2737 .read(cx)?
2738 .workspace()
2739 .read(cx)
2740 .project
2741 .clone();
2742 if project.read(cx).is_shared() {
2743 this.unshare_project(project, cx)?;
2744 }
2745 Ok::<_, anyhow::Error>(())
2746 })?;
2747 }
2748 }
2749
2750 let save_result = this
2751 .update_in(cx, |this, window, cx| {
2752 this.save_all_internal(SaveIntent::Close, window, cx)
2753 })?
2754 .await;
2755
2756 // If we're not quitting, but closing, we remove the workspace from
2757 // the current session.
2758 if close_intent != CloseIntent::Quit
2759 && !save_last_workspace
2760 && save_result.as_ref().is_ok_and(|&res| res)
2761 {
2762 this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx))?
2763 .await;
2764 }
2765
2766 save_result
2767 })
2768 }
2769
2770 fn save_all(&mut self, action: &SaveAll, window: &mut Window, cx: &mut Context<Self>) {
2771 self.save_all_internal(
2772 action.save_intent.unwrap_or(SaveIntent::SaveAll),
2773 window,
2774 cx,
2775 )
2776 .detach_and_log_err(cx);
2777 }
2778
2779 fn send_keystrokes(
2780 &mut self,
2781 action: &SendKeystrokes,
2782 window: &mut Window,
2783 cx: &mut Context<Self>,
2784 ) {
2785 let keystrokes: Vec<Keystroke> = action
2786 .0
2787 .split(' ')
2788 .flat_map(|k| Keystroke::parse(k).log_err())
2789 .map(|k| {
2790 cx.keyboard_mapper()
2791 .map_key_equivalent(k, false)
2792 .inner()
2793 .clone()
2794 })
2795 .collect();
2796 let _ = self.send_keystrokes_impl(keystrokes, window, cx);
2797 }
2798
2799 pub fn send_keystrokes_impl(
2800 &mut self,
2801 keystrokes: Vec<Keystroke>,
2802 window: &mut Window,
2803 cx: &mut Context<Self>,
2804 ) -> Shared<Task<()>> {
2805 let mut state = self.dispatching_keystrokes.borrow_mut();
2806 if !state.dispatched.insert(keystrokes.clone()) {
2807 cx.propagate();
2808 return state.task.clone().unwrap();
2809 }
2810
2811 state.queue.extend(keystrokes);
2812
2813 let keystrokes = self.dispatching_keystrokes.clone();
2814 if state.task.is_none() {
2815 state.task = Some(
2816 window
2817 .spawn(cx, async move |cx| {
2818 // limit to 100 keystrokes to avoid infinite recursion.
2819 for _ in 0..100 {
2820 let mut state = keystrokes.borrow_mut();
2821 let Some(keystroke) = state.queue.pop_front() else {
2822 state.dispatched.clear();
2823 state.task.take();
2824 return;
2825 };
2826 drop(state);
2827 cx.update(|window, cx| {
2828 let focused = window.focused(cx);
2829 window.dispatch_keystroke(keystroke.clone(), cx);
2830 if window.focused(cx) != focused {
2831 // dispatch_keystroke may cause the focus to change.
2832 // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
2833 // And we need that to happen before the next keystroke to keep vim mode happy...
2834 // (Note that the tests always do this implicitly, so you must manually test with something like:
2835 // "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
2836 // )
2837 window.draw(cx).clear();
2838 }
2839 })
2840 .ok();
2841 }
2842
2843 *keystrokes.borrow_mut() = Default::default();
2844 log::error!("over 100 keystrokes passed to send_keystrokes");
2845 })
2846 .shared(),
2847 );
2848 }
2849 state.task.clone().unwrap()
2850 }
2851
2852 fn save_all_internal(
2853 &mut self,
2854 mut save_intent: SaveIntent,
2855 window: &mut Window,
2856 cx: &mut Context<Self>,
2857 ) -> Task<Result<bool>> {
2858 if self.project.read(cx).is_disconnected(cx) {
2859 return Task::ready(Ok(true));
2860 }
2861 let dirty_items = self
2862 .panes
2863 .iter()
2864 .flat_map(|pane| {
2865 pane.read(cx).items().filter_map(|item| {
2866 if item.is_dirty(cx) {
2867 item.tab_content_text(0, cx);
2868 Some((pane.downgrade(), item.boxed_clone()))
2869 } else {
2870 None
2871 }
2872 })
2873 })
2874 .collect::<Vec<_>>();
2875
2876 let project = self.project.clone();
2877 cx.spawn_in(window, async move |workspace, cx| {
2878 let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() {
2879 let (serialize_tasks, remaining_dirty_items) =
2880 workspace.update_in(cx, |workspace, window, cx| {
2881 let mut remaining_dirty_items = Vec::new();
2882 let mut serialize_tasks = Vec::new();
2883 for (pane, item) in dirty_items {
2884 if let Some(task) = item
2885 .to_serializable_item_handle(cx)
2886 .and_then(|handle| handle.serialize(workspace, true, window, cx))
2887 {
2888 serialize_tasks.push(task);
2889 } else {
2890 remaining_dirty_items.push((pane, item));
2891 }
2892 }
2893 (serialize_tasks, remaining_dirty_items)
2894 })?;
2895
2896 futures::future::try_join_all(serialize_tasks).await?;
2897
2898 if remaining_dirty_items.len() > 1 {
2899 let answer = workspace.update_in(cx, |_, window, cx| {
2900 let detail = Pane::file_names_for_prompt(
2901 &mut remaining_dirty_items.iter().map(|(_, handle)| handle),
2902 cx,
2903 );
2904 window.prompt(
2905 PromptLevel::Warning,
2906 "Do you want to save all changes in the following files?",
2907 Some(&detail),
2908 &["Save all", "Discard all", "Cancel"],
2909 cx,
2910 )
2911 })?;
2912 match answer.await.log_err() {
2913 Some(0) => save_intent = SaveIntent::SaveAll,
2914 Some(1) => save_intent = SaveIntent::Skip,
2915 Some(2) => return Ok(false),
2916 _ => {}
2917 }
2918 }
2919
2920 remaining_dirty_items
2921 } else {
2922 dirty_items
2923 };
2924
2925 for (pane, item) in dirty_items {
2926 let (singleton, project_entry_ids) = cx.update(|_, cx| {
2927 (
2928 item.buffer_kind(cx) == ItemBufferKind::Singleton,
2929 item.project_entry_ids(cx),
2930 )
2931 })?;
2932 if (singleton || !project_entry_ids.is_empty())
2933 && !Pane::save_item(project.clone(), &pane, &*item, save_intent, cx).await?
2934 {
2935 return Ok(false);
2936 }
2937 }
2938 Ok(true)
2939 })
2940 }
2941
2942 pub fn open_workspace_for_paths(
2943 &mut self,
2944 replace_current_window: bool,
2945 paths: Vec<PathBuf>,
2946 window: &mut Window,
2947 cx: &mut Context<Self>,
2948 ) -> Task<Result<()>> {
2949 let window_handle = window.window_handle().downcast::<MultiWorkspace>();
2950 let is_remote = self.project.read(cx).is_via_collab();
2951 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
2952 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
2953
2954 let window_to_replace = if replace_current_window {
2955 window_handle
2956 } else if is_remote || has_worktree || has_dirty_items {
2957 None
2958 } else {
2959 window_handle
2960 };
2961 let app_state = self.app_state.clone();
2962
2963 cx.spawn(async move |_, cx| {
2964 cx.update(|cx| {
2965 open_paths(
2966 &paths,
2967 app_state,
2968 OpenOptions {
2969 replace_window: window_to_replace,
2970 ..Default::default()
2971 },
2972 cx,
2973 )
2974 })
2975 .await?;
2976 Ok(())
2977 })
2978 }
2979
2980 #[allow(clippy::type_complexity)]
2981 pub fn open_paths(
2982 &mut self,
2983 mut abs_paths: Vec<PathBuf>,
2984 options: OpenOptions,
2985 pane: Option<WeakEntity<Pane>>,
2986 window: &mut Window,
2987 cx: &mut Context<Self>,
2988 ) -> Task<Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>> {
2989 let fs = self.app_state.fs.clone();
2990
2991 let caller_ordered_abs_paths = abs_paths.clone();
2992
2993 // Sort the paths to ensure we add worktrees for parents before their children.
2994 abs_paths.sort_unstable();
2995 cx.spawn_in(window, async move |this, cx| {
2996 let mut tasks = Vec::with_capacity(abs_paths.len());
2997
2998 for abs_path in &abs_paths {
2999 let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
3000 OpenVisible::All => Some(true),
3001 OpenVisible::None => Some(false),
3002 OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
3003 Some(Some(metadata)) => Some(!metadata.is_dir),
3004 Some(None) => Some(true),
3005 None => None,
3006 },
3007 OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
3008 Some(Some(metadata)) => Some(metadata.is_dir),
3009 Some(None) => Some(false),
3010 None => None,
3011 },
3012 };
3013 let project_path = match visible {
3014 Some(visible) => match this
3015 .update(cx, |this, cx| {
3016 Workspace::project_path_for_path(
3017 this.project.clone(),
3018 abs_path,
3019 visible,
3020 cx,
3021 )
3022 })
3023 .log_err()
3024 {
3025 Some(project_path) => project_path.await.log_err(),
3026 None => None,
3027 },
3028 None => None,
3029 };
3030
3031 let this = this.clone();
3032 let abs_path: Arc<Path> = SanitizedPath::new(&abs_path).as_path().into();
3033 let fs = fs.clone();
3034 let pane = pane.clone();
3035 let task = cx.spawn(async move |cx| {
3036 let (_worktree, project_path) = project_path?;
3037 if fs.is_dir(&abs_path).await {
3038 // Opening a directory should not race to update the active entry.
3039 // We'll select/reveal a deterministic final entry after all paths finish opening.
3040 None
3041 } else {
3042 Some(
3043 this.update_in(cx, |this, window, cx| {
3044 this.open_path(
3045 project_path,
3046 pane,
3047 options.focus.unwrap_or(true),
3048 window,
3049 cx,
3050 )
3051 })
3052 .ok()?
3053 .await,
3054 )
3055 }
3056 });
3057 tasks.push(task);
3058 }
3059
3060 let results = futures::future::join_all(tasks).await;
3061
3062 // Determine the winner using the fake/abstract FS metadata, not `Path::is_dir`.
3063 let mut winner: Option<(PathBuf, bool)> = None;
3064 for abs_path in caller_ordered_abs_paths.into_iter().rev() {
3065 if let Some(Some(metadata)) = fs.metadata(&abs_path).await.log_err() {
3066 if !metadata.is_dir {
3067 winner = Some((abs_path, false));
3068 break;
3069 }
3070 if winner.is_none() {
3071 winner = Some((abs_path, true));
3072 }
3073 } else if winner.is_none() {
3074 winner = Some((abs_path, false));
3075 }
3076 }
3077
3078 // Compute the winner entry id on the foreground thread and emit once, after all
3079 // paths finish opening. This avoids races between concurrently-opening paths
3080 // (directories in particular) and makes the resulting project panel selection
3081 // deterministic.
3082 if let Some((winner_abs_path, winner_is_dir)) = winner {
3083 'emit_winner: {
3084 let winner_abs_path: Arc<Path> =
3085 SanitizedPath::new(&winner_abs_path).as_path().into();
3086
3087 let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
3088 OpenVisible::All => true,
3089 OpenVisible::None => false,
3090 OpenVisible::OnlyFiles => !winner_is_dir,
3091 OpenVisible::OnlyDirectories => winner_is_dir,
3092 };
3093
3094 let Some(worktree_task) = this
3095 .update(cx, |workspace, cx| {
3096 workspace.project.update(cx, |project, cx| {
3097 project.find_or_create_worktree(
3098 winner_abs_path.as_ref(),
3099 visible,
3100 cx,
3101 )
3102 })
3103 })
3104 .ok()
3105 else {
3106 break 'emit_winner;
3107 };
3108
3109 let Ok((worktree, _)) = worktree_task.await else {
3110 break 'emit_winner;
3111 };
3112
3113 let Ok(Some(entry_id)) = this.update(cx, |_, cx| {
3114 let worktree = worktree.read(cx);
3115 let worktree_abs_path = worktree.abs_path();
3116 let entry = if winner_abs_path.as_ref() == worktree_abs_path.as_ref() {
3117 worktree.root_entry()
3118 } else {
3119 winner_abs_path
3120 .strip_prefix(worktree_abs_path.as_ref())
3121 .ok()
3122 .and_then(|relative_path| {
3123 let relative_path =
3124 RelPath::new(relative_path, PathStyle::local())
3125 .log_err()?;
3126 worktree.entry_for_path(&relative_path)
3127 })
3128 }?;
3129 Some(entry.id)
3130 }) else {
3131 break 'emit_winner;
3132 };
3133
3134 this.update(cx, |workspace, cx| {
3135 workspace.project.update(cx, |_, cx| {
3136 cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
3137 });
3138 })
3139 .ok();
3140 }
3141 }
3142
3143 results
3144 })
3145 }
3146
3147 pub fn open_resolved_path(
3148 &mut self,
3149 path: ResolvedPath,
3150 window: &mut Window,
3151 cx: &mut Context<Self>,
3152 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3153 match path {
3154 ResolvedPath::ProjectPath { project_path, .. } => {
3155 self.open_path(project_path, None, true, window, cx)
3156 }
3157 ResolvedPath::AbsPath { path, .. } => self.open_abs_path(
3158 PathBuf::from(path),
3159 OpenOptions {
3160 visible: Some(OpenVisible::None),
3161 ..Default::default()
3162 },
3163 window,
3164 cx,
3165 ),
3166 }
3167 }
3168
3169 pub fn absolute_path_of_worktree(
3170 &self,
3171 worktree_id: WorktreeId,
3172 cx: &mut Context<Self>,
3173 ) -> Option<PathBuf> {
3174 self.project
3175 .read(cx)
3176 .worktree_for_id(worktree_id, cx)
3177 // TODO: use `abs_path` or `root_dir`
3178 .map(|wt| wt.read(cx).abs_path().as_ref().to_path_buf())
3179 }
3180
3181 fn add_folder_to_project(
3182 &mut self,
3183 _: &AddFolderToProject,
3184 window: &mut Window,
3185 cx: &mut Context<Self>,
3186 ) {
3187 let project = self.project.read(cx);
3188 if project.is_via_collab() {
3189 self.show_error(
3190 &anyhow!("You cannot add folders to someone else's project"),
3191 cx,
3192 );
3193 return;
3194 }
3195 let paths = self.prompt_for_open_path(
3196 PathPromptOptions {
3197 files: false,
3198 directories: true,
3199 multiple: true,
3200 prompt: None,
3201 },
3202 DirectoryLister::Project(self.project.clone()),
3203 window,
3204 cx,
3205 );
3206 cx.spawn_in(window, async move |this, cx| {
3207 if let Some(paths) = paths.await.log_err().flatten() {
3208 let results = this
3209 .update_in(cx, |this, window, cx| {
3210 this.open_paths(
3211 paths,
3212 OpenOptions {
3213 visible: Some(OpenVisible::All),
3214 ..Default::default()
3215 },
3216 None,
3217 window,
3218 cx,
3219 )
3220 })?
3221 .await;
3222 for result in results.into_iter().flatten() {
3223 result.log_err();
3224 }
3225 }
3226 anyhow::Ok(())
3227 })
3228 .detach_and_log_err(cx);
3229 }
3230
3231 pub fn project_path_for_path(
3232 project: Entity<Project>,
3233 abs_path: &Path,
3234 visible: bool,
3235 cx: &mut App,
3236 ) -> Task<Result<(Entity<Worktree>, ProjectPath)>> {
3237 let entry = project.update(cx, |project, cx| {
3238 project.find_or_create_worktree(abs_path, visible, cx)
3239 });
3240 cx.spawn(async move |cx| {
3241 let (worktree, path) = entry.await?;
3242 let worktree_id = worktree.read_with(cx, |t, _| t.id());
3243 Ok((worktree, ProjectPath { worktree_id, path }))
3244 })
3245 }
3246
3247 pub fn items<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator<Item = &'a Box<dyn ItemHandle>> {
3248 self.panes.iter().flat_map(|pane| pane.read(cx).items())
3249 }
3250
3251 pub fn item_of_type<T: Item>(&self, cx: &App) -> Option<Entity<T>> {
3252 self.items_of_type(cx).max_by_key(|item| item.item_id())
3253 }
3254
3255 pub fn items_of_type<'a, T: Item>(
3256 &'a self,
3257 cx: &'a App,
3258 ) -> impl 'a + Iterator<Item = Entity<T>> {
3259 self.panes
3260 .iter()
3261 .flat_map(|pane| pane.read(cx).items_of_type())
3262 }
3263
3264 pub fn active_item(&self, cx: &App) -> Option<Box<dyn ItemHandle>> {
3265 self.active_pane().read(cx).active_item()
3266 }
3267
3268 pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
3269 let item = self.active_item(cx)?;
3270 item.to_any_view().downcast::<I>().ok()
3271 }
3272
3273 fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
3274 self.active_item(cx).and_then(|item| item.project_path(cx))
3275 }
3276
3277 pub fn most_recent_active_path(&self, cx: &App) -> Option<PathBuf> {
3278 self.recent_navigation_history_iter(cx)
3279 .filter_map(|(path, abs_path)| {
3280 let worktree = self
3281 .project
3282 .read(cx)
3283 .worktree_for_id(path.worktree_id, cx)?;
3284 if worktree.read(cx).is_visible() {
3285 abs_path
3286 } else {
3287 None
3288 }
3289 })
3290 .next()
3291 }
3292
3293 pub fn save_active_item(
3294 &mut self,
3295 save_intent: SaveIntent,
3296 window: &mut Window,
3297 cx: &mut App,
3298 ) -> Task<Result<()>> {
3299 let project = self.project.clone();
3300 let pane = self.active_pane();
3301 let item = pane.read(cx).active_item();
3302 let pane = pane.downgrade();
3303
3304 window.spawn(cx, async move |cx| {
3305 if let Some(item) = item {
3306 Pane::save_item(project, &pane, item.as_ref(), save_intent, cx)
3307 .await
3308 .map(|_| ())
3309 } else {
3310 Ok(())
3311 }
3312 })
3313 }
3314
3315 pub fn close_inactive_items_and_panes(
3316 &mut self,
3317 action: &CloseInactiveTabsAndPanes,
3318 window: &mut Window,
3319 cx: &mut Context<Self>,
3320 ) {
3321 if let Some(task) = self.close_all_internal(
3322 true,
3323 action.save_intent.unwrap_or(SaveIntent::Close),
3324 window,
3325 cx,
3326 ) {
3327 task.detach_and_log_err(cx)
3328 }
3329 }
3330
3331 pub fn close_all_items_and_panes(
3332 &mut self,
3333 action: &CloseAllItemsAndPanes,
3334 window: &mut Window,
3335 cx: &mut Context<Self>,
3336 ) {
3337 if let Some(task) = self.close_all_internal(
3338 false,
3339 action.save_intent.unwrap_or(SaveIntent::Close),
3340 window,
3341 cx,
3342 ) {
3343 task.detach_and_log_err(cx)
3344 }
3345 }
3346
3347 /// Closes the active item across all panes.
3348 pub fn close_item_in_all_panes(
3349 &mut self,
3350 action: &CloseItemInAllPanes,
3351 window: &mut Window,
3352 cx: &mut Context<Self>,
3353 ) {
3354 let Some(active_item) = self.active_pane().read(cx).active_item() else {
3355 return;
3356 };
3357
3358 let save_intent = action.save_intent.unwrap_or(SaveIntent::Close);
3359 let close_pinned = action.close_pinned;
3360
3361 if let Some(project_path) = active_item.project_path(cx) {
3362 self.close_items_with_project_path(
3363 &project_path,
3364 save_intent,
3365 close_pinned,
3366 window,
3367 cx,
3368 );
3369 } else if close_pinned || !self.active_pane().read(cx).is_active_item_pinned() {
3370 let item_id = active_item.item_id();
3371 self.active_pane().update(cx, |pane, cx| {
3372 pane.close_item_by_id(item_id, save_intent, window, cx)
3373 .detach_and_log_err(cx);
3374 });
3375 }
3376 }
3377
3378 /// Closes all items with the given project path across all panes.
3379 pub fn close_items_with_project_path(
3380 &mut self,
3381 project_path: &ProjectPath,
3382 save_intent: SaveIntent,
3383 close_pinned: bool,
3384 window: &mut Window,
3385 cx: &mut Context<Self>,
3386 ) {
3387 let panes = self.panes().to_vec();
3388 for pane in panes {
3389 pane.update(cx, |pane, cx| {
3390 pane.close_items_for_project_path(
3391 project_path,
3392 save_intent,
3393 close_pinned,
3394 window,
3395 cx,
3396 )
3397 .detach_and_log_err(cx);
3398 });
3399 }
3400 }
3401
3402 fn close_all_internal(
3403 &mut self,
3404 retain_active_pane: bool,
3405 save_intent: SaveIntent,
3406 window: &mut Window,
3407 cx: &mut Context<Self>,
3408 ) -> Option<Task<Result<()>>> {
3409 let current_pane = self.active_pane();
3410
3411 let mut tasks = Vec::new();
3412
3413 if retain_active_pane {
3414 let current_pane_close = current_pane.update(cx, |pane, cx| {
3415 pane.close_other_items(
3416 &CloseOtherItems {
3417 save_intent: None,
3418 close_pinned: false,
3419 },
3420 None,
3421 window,
3422 cx,
3423 )
3424 });
3425
3426 tasks.push(current_pane_close);
3427 }
3428
3429 for pane in self.panes() {
3430 if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
3431 continue;
3432 }
3433
3434 let close_pane_items = pane.update(cx, |pane: &mut Pane, cx| {
3435 pane.close_all_items(
3436 &CloseAllItems {
3437 save_intent: Some(save_intent),
3438 close_pinned: false,
3439 },
3440 window,
3441 cx,
3442 )
3443 });
3444
3445 tasks.push(close_pane_items)
3446 }
3447
3448 if tasks.is_empty() {
3449 None
3450 } else {
3451 Some(cx.spawn_in(window, async move |_, _| {
3452 for task in tasks {
3453 task.await?
3454 }
3455 Ok(())
3456 }))
3457 }
3458 }
3459
3460 pub fn is_dock_at_position_open(&self, position: DockPosition, cx: &mut Context<Self>) -> bool {
3461 self.dock_at_position(position).read(cx).is_open()
3462 }
3463
3464 pub fn toggle_dock(
3465 &mut self,
3466 dock_side: DockPosition,
3467 window: &mut Window,
3468 cx: &mut Context<Self>,
3469 ) {
3470 let mut focus_center = false;
3471 let mut reveal_dock = false;
3472
3473 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
3474 let was_visible = self.is_dock_at_position_open(dock_side, cx) && !other_is_zoomed;
3475
3476 if let Some(panel) = self.dock_at_position(dock_side).read(cx).active_panel() {
3477 telemetry::event!(
3478 "Panel Button Clicked",
3479 name = panel.persistent_name(),
3480 toggle_state = !was_visible
3481 );
3482 }
3483 if was_visible {
3484 self.save_open_dock_positions(cx);
3485 }
3486
3487 let dock = self.dock_at_position(dock_side);
3488 dock.update(cx, |dock, cx| {
3489 dock.set_open(!was_visible, window, cx);
3490
3491 if dock.active_panel().is_none() {
3492 let Some(panel_ix) = dock
3493 .first_enabled_panel_idx(cx)
3494 .log_with_level(log::Level::Info)
3495 else {
3496 return;
3497 };
3498 dock.activate_panel(panel_ix, window, cx);
3499 }
3500
3501 if let Some(active_panel) = dock.active_panel() {
3502 if was_visible {
3503 if active_panel
3504 .panel_focus_handle(cx)
3505 .contains_focused(window, cx)
3506 {
3507 focus_center = true;
3508 }
3509 } else {
3510 let focus_handle = &active_panel.panel_focus_handle(cx);
3511 window.focus(focus_handle, cx);
3512 reveal_dock = true;
3513 }
3514 }
3515 });
3516
3517 if reveal_dock {
3518 self.dismiss_zoomed_items_to_reveal(Some(dock_side), window, cx);
3519 }
3520
3521 if focus_center {
3522 self.active_pane
3523 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
3524 }
3525
3526 cx.notify();
3527 self.serialize_workspace(window, cx);
3528 }
3529
3530 fn active_dock(&self, window: &Window, cx: &Context<Self>) -> Option<&Entity<Dock>> {
3531 self.all_docks().into_iter().find(|&dock| {
3532 dock.read(cx).is_open() && dock.focus_handle(cx).contains_focused(window, cx)
3533 })
3534 }
3535
3536 fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
3537 if let Some(dock) = self.active_dock(window, cx).cloned() {
3538 self.save_open_dock_positions(cx);
3539 dock.update(cx, |dock, cx| {
3540 dock.set_open(false, window, cx);
3541 });
3542 return true;
3543 }
3544 false
3545 }
3546
3547 pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3548 self.save_open_dock_positions(cx);
3549 for dock in self.all_docks() {
3550 dock.update(cx, |dock, cx| {
3551 dock.set_open(false, window, cx);
3552 });
3553 }
3554
3555 cx.focus_self(window);
3556 cx.notify();
3557 self.serialize_workspace(window, cx);
3558 }
3559
3560 fn get_open_dock_positions(&self, cx: &Context<Self>) -> Vec<DockPosition> {
3561 self.all_docks()
3562 .into_iter()
3563 .filter_map(|dock| {
3564 let dock_ref = dock.read(cx);
3565 if dock_ref.is_open() {
3566 Some(dock_ref.position())
3567 } else {
3568 None
3569 }
3570 })
3571 .collect()
3572 }
3573
3574 /// Saves the positions of currently open docks.
3575 ///
3576 /// Updates `last_open_dock_positions` with positions of all currently open
3577 /// docks, to later be restored by the 'Toggle All Docks' action.
3578 fn save_open_dock_positions(&mut self, cx: &mut Context<Self>) {
3579 let open_dock_positions = self.get_open_dock_positions(cx);
3580 if !open_dock_positions.is_empty() {
3581 self.last_open_dock_positions = open_dock_positions;
3582 }
3583 }
3584
3585 /// Toggles all docks between open and closed states.
3586 ///
3587 /// If any docks are open, closes all and remembers their positions. If all
3588 /// docks are closed, restores the last remembered dock configuration.
3589 fn toggle_all_docks(
3590 &mut self,
3591 _: &ToggleAllDocks,
3592 window: &mut Window,
3593 cx: &mut Context<Self>,
3594 ) {
3595 let open_dock_positions = self.get_open_dock_positions(cx);
3596
3597 if !open_dock_positions.is_empty() {
3598 self.close_all_docks(window, cx);
3599 } else if !self.last_open_dock_positions.is_empty() {
3600 self.restore_last_open_docks(window, cx);
3601 }
3602 }
3603
3604 /// Reopens docks from the most recently remembered configuration.
3605 ///
3606 /// Opens all docks whose positions are stored in `last_open_dock_positions`
3607 /// and clears the stored positions.
3608 fn restore_last_open_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3609 let positions_to_open = std::mem::take(&mut self.last_open_dock_positions);
3610
3611 for position in positions_to_open {
3612 let dock = self.dock_at_position(position);
3613 dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
3614 }
3615
3616 cx.focus_self(window);
3617 cx.notify();
3618 self.serialize_workspace(window, cx);
3619 }
3620
3621 /// Transfer focus to the panel of the given type.
3622 pub fn focus_panel<T: Panel>(
3623 &mut self,
3624 window: &mut Window,
3625 cx: &mut Context<Self>,
3626 ) -> Option<Entity<T>> {
3627 let panel = self.focus_or_unfocus_panel::<T>(window, cx, |_, _, _| true)?;
3628 panel.to_any().downcast().ok()
3629 }
3630
3631 /// Focus the panel of the given type if it isn't already focused. If it is
3632 /// already focused, then transfer focus back to the workspace center.
3633 /// When the `close_panel_on_toggle` setting is enabled, also closes the
3634 /// panel when transferring focus back to the center.
3635 pub fn toggle_panel_focus<T: Panel>(
3636 &mut self,
3637 window: &mut Window,
3638 cx: &mut Context<Self>,
3639 ) -> bool {
3640 let mut did_focus_panel = false;
3641 self.focus_or_unfocus_panel::<T>(window, cx, |panel, window, cx| {
3642 did_focus_panel = !panel.panel_focus_handle(cx).contains_focused(window, cx);
3643 did_focus_panel
3644 });
3645
3646 if !did_focus_panel && WorkspaceSettings::get_global(cx).close_panel_on_toggle {
3647 self.close_panel::<T>(window, cx);
3648 }
3649
3650 telemetry::event!(
3651 "Panel Button Clicked",
3652 name = T::persistent_name(),
3653 toggle_state = did_focus_panel
3654 );
3655
3656 did_focus_panel
3657 }
3658
3659 pub fn activate_panel_for_proto_id(
3660 &mut self,
3661 panel_id: PanelId,
3662 window: &mut Window,
3663 cx: &mut Context<Self>,
3664 ) -> Option<Arc<dyn PanelHandle>> {
3665 let mut panel = None;
3666 for dock in self.all_docks() {
3667 if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
3668 panel = dock.update(cx, |dock, cx| {
3669 dock.activate_panel(panel_index, window, cx);
3670 dock.set_open(true, window, cx);
3671 dock.active_panel().cloned()
3672 });
3673 break;
3674 }
3675 }
3676
3677 if panel.is_some() {
3678 cx.notify();
3679 self.serialize_workspace(window, cx);
3680 }
3681
3682 panel
3683 }
3684
3685 /// Focus or unfocus the given panel type, depending on the given callback.
3686 fn focus_or_unfocus_panel<T: Panel>(
3687 &mut self,
3688 window: &mut Window,
3689 cx: &mut Context<Self>,
3690 mut should_focus: impl FnMut(&dyn PanelHandle, &mut Window, &mut Context<Dock>) -> bool,
3691 ) -> Option<Arc<dyn PanelHandle>> {
3692 let mut result_panel = None;
3693 let mut serialize = false;
3694 for dock in self.all_docks() {
3695 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
3696 let mut focus_center = false;
3697 let panel = dock.update(cx, |dock, cx| {
3698 dock.activate_panel(panel_index, window, cx);
3699
3700 let panel = dock.active_panel().cloned();
3701 if let Some(panel) = panel.as_ref() {
3702 if should_focus(&**panel, window, cx) {
3703 dock.set_open(true, window, cx);
3704 panel.panel_focus_handle(cx).focus(window, cx);
3705 } else {
3706 focus_center = true;
3707 }
3708 }
3709 panel
3710 });
3711
3712 if focus_center {
3713 self.active_pane
3714 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
3715 }
3716
3717 result_panel = panel;
3718 serialize = true;
3719 break;
3720 }
3721 }
3722
3723 if serialize {
3724 self.serialize_workspace(window, cx);
3725 }
3726
3727 cx.notify();
3728 result_panel
3729 }
3730
3731 /// Open the panel of the given type
3732 pub fn open_panel<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3733 for dock in self.all_docks() {
3734 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
3735 dock.update(cx, |dock, cx| {
3736 dock.activate_panel(panel_index, window, cx);
3737 dock.set_open(true, window, cx);
3738 });
3739 }
3740 }
3741 }
3742
3743 pub fn close_panel<T: Panel>(&self, window: &mut Window, cx: &mut Context<Self>) {
3744 for dock in self.all_docks().iter() {
3745 dock.update(cx, |dock, cx| {
3746 if dock.panel::<T>().is_some() {
3747 dock.set_open(false, window, cx)
3748 }
3749 })
3750 }
3751 }
3752
3753 pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
3754 self.all_docks()
3755 .iter()
3756 .find_map(|dock| dock.read(cx).panel::<T>())
3757 }
3758
3759 fn dismiss_zoomed_items_to_reveal(
3760 &mut self,
3761 dock_to_reveal: Option<DockPosition>,
3762 window: &mut Window,
3763 cx: &mut Context<Self>,
3764 ) {
3765 // If a center pane is zoomed, unzoom it.
3766 for pane in &self.panes {
3767 if pane != &self.active_pane || dock_to_reveal.is_some() {
3768 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3769 }
3770 }
3771
3772 // If another dock is zoomed, hide it.
3773 let mut focus_center = false;
3774 for dock in self.all_docks() {
3775 dock.update(cx, |dock, cx| {
3776 if Some(dock.position()) != dock_to_reveal
3777 && let Some(panel) = dock.active_panel()
3778 && panel.is_zoomed(window, cx)
3779 {
3780 focus_center |= panel.panel_focus_handle(cx).contains_focused(window, cx);
3781 dock.set_open(false, window, cx);
3782 }
3783 });
3784 }
3785
3786 if focus_center {
3787 self.active_pane
3788 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
3789 }
3790
3791 if self.zoomed_position != dock_to_reveal {
3792 self.zoomed = None;
3793 self.zoomed_position = None;
3794 cx.emit(Event::ZoomChanged);
3795 }
3796
3797 cx.notify();
3798 }
3799
3800 fn add_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
3801 let pane = cx.new(|cx| {
3802 let mut pane = Pane::new(
3803 self.weak_handle(),
3804 self.project.clone(),
3805 self.pane_history_timestamp.clone(),
3806 None,
3807 NewFile.boxed_clone(),
3808 true,
3809 window,
3810 cx,
3811 );
3812 pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
3813 pane
3814 });
3815 cx.subscribe_in(&pane, window, Self::handle_pane_event)
3816 .detach();
3817 self.panes.push(pane.clone());
3818
3819 window.focus(&pane.focus_handle(cx), cx);
3820
3821 cx.emit(Event::PaneAdded(pane.clone()));
3822 pane
3823 }
3824
3825 pub fn add_item_to_center(
3826 &mut self,
3827 item: Box<dyn ItemHandle>,
3828 window: &mut Window,
3829 cx: &mut Context<Self>,
3830 ) -> bool {
3831 if let Some(center_pane) = self.last_active_center_pane.clone() {
3832 if let Some(center_pane) = center_pane.upgrade() {
3833 center_pane.update(cx, |pane, cx| {
3834 pane.add_item(item, true, true, None, window, cx)
3835 });
3836 true
3837 } else {
3838 false
3839 }
3840 } else {
3841 false
3842 }
3843 }
3844
3845 pub fn add_item_to_active_pane(
3846 &mut self,
3847 item: Box<dyn ItemHandle>,
3848 destination_index: Option<usize>,
3849 focus_item: bool,
3850 window: &mut Window,
3851 cx: &mut App,
3852 ) {
3853 self.add_item(
3854 self.active_pane.clone(),
3855 item,
3856 destination_index,
3857 false,
3858 focus_item,
3859 window,
3860 cx,
3861 )
3862 }
3863
3864 pub fn add_item(
3865 &mut self,
3866 pane: Entity<Pane>,
3867 item: Box<dyn ItemHandle>,
3868 destination_index: Option<usize>,
3869 activate_pane: bool,
3870 focus_item: bool,
3871 window: &mut Window,
3872 cx: &mut App,
3873 ) {
3874 pane.update(cx, |pane, cx| {
3875 pane.add_item(
3876 item,
3877 activate_pane,
3878 focus_item,
3879 destination_index,
3880 window,
3881 cx,
3882 )
3883 });
3884 }
3885
3886 pub fn split_item(
3887 &mut self,
3888 split_direction: SplitDirection,
3889 item: Box<dyn ItemHandle>,
3890 window: &mut Window,
3891 cx: &mut Context<Self>,
3892 ) {
3893 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, window, cx);
3894 self.add_item(new_pane, item, None, true, true, window, cx);
3895 }
3896
3897 pub fn open_abs_path(
3898 &mut self,
3899 abs_path: PathBuf,
3900 options: OpenOptions,
3901 window: &mut Window,
3902 cx: &mut Context<Self>,
3903 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3904 cx.spawn_in(window, async move |workspace, cx| {
3905 let open_paths_task_result = workspace
3906 .update_in(cx, |workspace, window, cx| {
3907 workspace.open_paths(vec![abs_path.clone()], options, None, window, cx)
3908 })
3909 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
3910 .await;
3911 anyhow::ensure!(
3912 open_paths_task_result.len() == 1,
3913 "open abs path {abs_path:?} task returned incorrect number of results"
3914 );
3915 match open_paths_task_result
3916 .into_iter()
3917 .next()
3918 .expect("ensured single task result")
3919 {
3920 Some(open_result) => {
3921 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
3922 }
3923 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
3924 }
3925 })
3926 }
3927
3928 pub fn split_abs_path(
3929 &mut self,
3930 abs_path: PathBuf,
3931 visible: bool,
3932 window: &mut Window,
3933 cx: &mut Context<Self>,
3934 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3935 let project_path_task =
3936 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
3937 cx.spawn_in(window, async move |this, cx| {
3938 let (_, path) = project_path_task.await?;
3939 this.update_in(cx, |this, window, cx| this.split_path(path, window, cx))?
3940 .await
3941 })
3942 }
3943
3944 pub fn open_path(
3945 &mut self,
3946 path: impl Into<ProjectPath>,
3947 pane: Option<WeakEntity<Pane>>,
3948 focus_item: bool,
3949 window: &mut Window,
3950 cx: &mut App,
3951 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3952 self.open_path_preview(path, pane, focus_item, false, true, window, cx)
3953 }
3954
3955 pub fn open_path_preview(
3956 &mut self,
3957 path: impl Into<ProjectPath>,
3958 pane: Option<WeakEntity<Pane>>,
3959 focus_item: bool,
3960 allow_preview: bool,
3961 activate: bool,
3962 window: &mut Window,
3963 cx: &mut App,
3964 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3965 let pane = pane.unwrap_or_else(|| {
3966 self.last_active_center_pane.clone().unwrap_or_else(|| {
3967 self.panes
3968 .first()
3969 .expect("There must be an active pane")
3970 .downgrade()
3971 })
3972 });
3973
3974 let project_path = path.into();
3975 let task = self.load_path(project_path.clone(), window, cx);
3976 window.spawn(cx, async move |cx| {
3977 let (project_entry_id, build_item) = task.await?;
3978
3979 pane.update_in(cx, |pane, window, cx| {
3980 pane.open_item(
3981 project_entry_id,
3982 project_path,
3983 focus_item,
3984 allow_preview,
3985 activate,
3986 None,
3987 window,
3988 cx,
3989 build_item,
3990 )
3991 })
3992 })
3993 }
3994
3995 pub fn split_path(
3996 &mut self,
3997 path: impl Into<ProjectPath>,
3998 window: &mut Window,
3999 cx: &mut Context<Self>,
4000 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4001 self.split_path_preview(path, false, None, window, cx)
4002 }
4003
4004 pub fn split_path_preview(
4005 &mut self,
4006 path: impl Into<ProjectPath>,
4007 allow_preview: bool,
4008 split_direction: Option<SplitDirection>,
4009 window: &mut Window,
4010 cx: &mut Context<Self>,
4011 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
4012 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
4013 self.panes
4014 .first()
4015 .expect("There must be an active pane")
4016 .downgrade()
4017 });
4018
4019 if let Member::Pane(center_pane) = &self.center.root
4020 && center_pane.read(cx).items_len() == 0
4021 {
4022 return self.open_path(path, Some(pane), true, window, cx);
4023 }
4024
4025 let project_path = path.into();
4026 let task = self.load_path(project_path.clone(), window, cx);
4027 cx.spawn_in(window, async move |this, cx| {
4028 let (project_entry_id, build_item) = task.await?;
4029 this.update_in(cx, move |this, window, cx| -> Option<_> {
4030 let pane = pane.upgrade()?;
4031 let new_pane = this.split_pane(
4032 pane,
4033 split_direction.unwrap_or(SplitDirection::Right),
4034 window,
4035 cx,
4036 );
4037 new_pane.update(cx, |new_pane, cx| {
4038 Some(new_pane.open_item(
4039 project_entry_id,
4040 project_path,
4041 true,
4042 allow_preview,
4043 true,
4044 None,
4045 window,
4046 cx,
4047 build_item,
4048 ))
4049 })
4050 })
4051 .map(|option| option.context("pane was dropped"))?
4052 })
4053 }
4054
4055 fn load_path(
4056 &mut self,
4057 path: ProjectPath,
4058 window: &mut Window,
4059 cx: &mut App,
4060 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
4061 let registry = cx.default_global::<ProjectItemRegistry>().clone();
4062 registry.open_path(self.project(), &path, window, cx)
4063 }
4064
4065 pub fn find_project_item<T>(
4066 &self,
4067 pane: &Entity<Pane>,
4068 project_item: &Entity<T::Item>,
4069 cx: &App,
4070 ) -> Option<Entity<T>>
4071 where
4072 T: ProjectItem,
4073 {
4074 use project::ProjectItem as _;
4075 let project_item = project_item.read(cx);
4076 let entry_id = project_item.entry_id(cx);
4077 let project_path = project_item.project_path(cx);
4078
4079 let mut item = None;
4080 if let Some(entry_id) = entry_id {
4081 item = pane.read(cx).item_for_entry(entry_id, cx);
4082 }
4083 if item.is_none()
4084 && let Some(project_path) = project_path
4085 {
4086 item = pane.read(cx).item_for_path(project_path, cx);
4087 }
4088
4089 item.and_then(|item| item.downcast::<T>())
4090 }
4091
4092 pub fn is_project_item_open<T>(
4093 &self,
4094 pane: &Entity<Pane>,
4095 project_item: &Entity<T::Item>,
4096 cx: &App,
4097 ) -> bool
4098 where
4099 T: ProjectItem,
4100 {
4101 self.find_project_item::<T>(pane, project_item, cx)
4102 .is_some()
4103 }
4104
4105 pub fn open_project_item<T>(
4106 &mut self,
4107 pane: Entity<Pane>,
4108 project_item: Entity<T::Item>,
4109 activate_pane: bool,
4110 focus_item: bool,
4111 keep_old_preview: bool,
4112 allow_new_preview: bool,
4113 window: &mut Window,
4114 cx: &mut Context<Self>,
4115 ) -> Entity<T>
4116 where
4117 T: ProjectItem,
4118 {
4119 let old_item_id = pane.read(cx).active_item().map(|item| item.item_id());
4120
4121 if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
4122 if !keep_old_preview
4123 && let Some(old_id) = old_item_id
4124 && old_id != item.item_id()
4125 {
4126 // switching to a different item, so unpreview old active item
4127 pane.update(cx, |pane, _| {
4128 pane.unpreview_item_if_preview(old_id);
4129 });
4130 }
4131
4132 self.activate_item(&item, activate_pane, focus_item, window, cx);
4133 if !allow_new_preview {
4134 pane.update(cx, |pane, _| {
4135 pane.unpreview_item_if_preview(item.item_id());
4136 });
4137 }
4138 return item;
4139 }
4140
4141 let item = pane.update(cx, |pane, cx| {
4142 cx.new(|cx| {
4143 T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx)
4144 })
4145 });
4146 let mut destination_index = None;
4147 pane.update(cx, |pane, cx| {
4148 if !keep_old_preview && let Some(old_id) = old_item_id {
4149 pane.unpreview_item_if_preview(old_id);
4150 }
4151 if allow_new_preview {
4152 destination_index = pane.replace_preview_item_id(item.item_id(), window, cx);
4153 }
4154 });
4155
4156 self.add_item(
4157 pane,
4158 Box::new(item.clone()),
4159 destination_index,
4160 activate_pane,
4161 focus_item,
4162 window,
4163 cx,
4164 );
4165 item
4166 }
4167
4168 pub fn open_shared_screen(
4169 &mut self,
4170 peer_id: PeerId,
4171 window: &mut Window,
4172 cx: &mut Context<Self>,
4173 ) {
4174 if let Some(shared_screen) =
4175 self.shared_screen_for_peer(peer_id, &self.active_pane, window, cx)
4176 {
4177 self.active_pane.update(cx, |pane, cx| {
4178 pane.add_item(Box::new(shared_screen), false, true, None, window, cx)
4179 });
4180 }
4181 }
4182
4183 pub fn activate_item(
4184 &mut self,
4185 item: &dyn ItemHandle,
4186 activate_pane: bool,
4187 focus_item: bool,
4188 window: &mut Window,
4189 cx: &mut App,
4190 ) -> bool {
4191 let result = self.panes.iter().find_map(|pane| {
4192 pane.read(cx)
4193 .index_for_item(item)
4194 .map(|ix| (pane.clone(), ix))
4195 });
4196 if let Some((pane, ix)) = result {
4197 pane.update(cx, |pane, cx| {
4198 pane.activate_item(ix, activate_pane, focus_item, window, cx)
4199 });
4200 true
4201 } else {
4202 false
4203 }
4204 }
4205
4206 fn activate_pane_at_index(
4207 &mut self,
4208 action: &ActivatePane,
4209 window: &mut Window,
4210 cx: &mut Context<Self>,
4211 ) {
4212 let panes = self.center.panes();
4213 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
4214 window.focus(&pane.focus_handle(cx), cx);
4215 } else {
4216 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx)
4217 .detach();
4218 }
4219 }
4220
4221 fn move_item_to_pane_at_index(
4222 &mut self,
4223 action: &MoveItemToPane,
4224 window: &mut Window,
4225 cx: &mut Context<Self>,
4226 ) {
4227 let panes = self.center.panes();
4228 let destination = match panes.get(action.destination) {
4229 Some(&destination) => destination.clone(),
4230 None => {
4231 if !action.clone && self.active_pane.read(cx).items_len() < 2 {
4232 return;
4233 }
4234 let direction = SplitDirection::Right;
4235 let split_off_pane = self
4236 .find_pane_in_direction(direction, cx)
4237 .unwrap_or_else(|| self.active_pane.clone());
4238 let new_pane = self.add_pane(window, cx);
4239 if self
4240 .center
4241 .split(&split_off_pane, &new_pane, direction, cx)
4242 .log_err()
4243 .is_none()
4244 {
4245 return;
4246 };
4247 new_pane
4248 }
4249 };
4250
4251 if action.clone {
4252 if self
4253 .active_pane
4254 .read(cx)
4255 .active_item()
4256 .is_some_and(|item| item.can_split(cx))
4257 {
4258 clone_active_item(
4259 self.database_id(),
4260 &self.active_pane,
4261 &destination,
4262 action.focus,
4263 window,
4264 cx,
4265 );
4266 return;
4267 }
4268 }
4269 move_active_item(
4270 &self.active_pane,
4271 &destination,
4272 action.focus,
4273 true,
4274 window,
4275 cx,
4276 )
4277 }
4278
4279 pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) {
4280 let panes = self.center.panes();
4281 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
4282 let next_ix = (ix + 1) % panes.len();
4283 let next_pane = panes[next_ix].clone();
4284 window.focus(&next_pane.focus_handle(cx), cx);
4285 }
4286 }
4287
4288 pub fn activate_previous_pane(&mut self, window: &mut Window, cx: &mut App) {
4289 let panes = self.center.panes();
4290 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
4291 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
4292 let prev_pane = panes[prev_ix].clone();
4293 window.focus(&prev_pane.focus_handle(cx), cx);
4294 }
4295 }
4296
4297 pub fn activate_pane_in_direction(
4298 &mut self,
4299 direction: SplitDirection,
4300 window: &mut Window,
4301 cx: &mut App,
4302 ) {
4303 use ActivateInDirectionTarget as Target;
4304 enum Origin {
4305 LeftDock,
4306 RightDock,
4307 BottomDock,
4308 Center,
4309 }
4310
4311 let origin: Origin = [
4312 (&self.left_dock, Origin::LeftDock),
4313 (&self.right_dock, Origin::RightDock),
4314 (&self.bottom_dock, Origin::BottomDock),
4315 ]
4316 .into_iter()
4317 .find_map(|(dock, origin)| {
4318 if dock.focus_handle(cx).contains_focused(window, cx) && dock.read(cx).is_open() {
4319 Some(origin)
4320 } else {
4321 None
4322 }
4323 })
4324 .unwrap_or(Origin::Center);
4325
4326 let get_last_active_pane = || {
4327 let pane = self
4328 .last_active_center_pane
4329 .clone()
4330 .unwrap_or_else(|| {
4331 self.panes
4332 .first()
4333 .expect("There must be an active pane")
4334 .downgrade()
4335 })
4336 .upgrade()?;
4337 (pane.read(cx).items_len() != 0).then_some(pane)
4338 };
4339
4340 let try_dock =
4341 |dock: &Entity<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
4342
4343 let target = match (origin, direction) {
4344 // We're in the center, so we first try to go to a different pane,
4345 // otherwise try to go to a dock.
4346 (Origin::Center, direction) => {
4347 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
4348 Some(Target::Pane(pane))
4349 } else {
4350 match direction {
4351 SplitDirection::Up => None,
4352 SplitDirection::Down => try_dock(&self.bottom_dock),
4353 SplitDirection::Left => try_dock(&self.left_dock),
4354 SplitDirection::Right => try_dock(&self.right_dock),
4355 }
4356 }
4357 }
4358
4359 (Origin::LeftDock, SplitDirection::Right) => {
4360 if let Some(last_active_pane) = get_last_active_pane() {
4361 Some(Target::Pane(last_active_pane))
4362 } else {
4363 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
4364 }
4365 }
4366
4367 (Origin::LeftDock, SplitDirection::Down)
4368 | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
4369
4370 (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
4371 (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
4372 (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
4373
4374 (Origin::RightDock, SplitDirection::Left) => {
4375 if let Some(last_active_pane) = get_last_active_pane() {
4376 Some(Target::Pane(last_active_pane))
4377 } else {
4378 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
4379 }
4380 }
4381
4382 _ => None,
4383 };
4384
4385 match target {
4386 Some(ActivateInDirectionTarget::Pane(pane)) => {
4387 let pane = pane.read(cx);
4388 if let Some(item) = pane.active_item() {
4389 item.item_focus_handle(cx).focus(window, cx);
4390 } else {
4391 log::error!(
4392 "Could not find a focus target when in switching focus in {direction} direction for a pane",
4393 );
4394 }
4395 }
4396 Some(ActivateInDirectionTarget::Dock(dock)) => {
4397 // Defer this to avoid a panic when the dock's active panel is already on the stack.
4398 window.defer(cx, move |window, cx| {
4399 let dock = dock.read(cx);
4400 if let Some(panel) = dock.active_panel() {
4401 panel.panel_focus_handle(cx).focus(window, cx);
4402 } else {
4403 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
4404 }
4405 })
4406 }
4407 None => {}
4408 }
4409 }
4410
4411 pub fn move_item_to_pane_in_direction(
4412 &mut self,
4413 action: &MoveItemToPaneInDirection,
4414 window: &mut Window,
4415 cx: &mut Context<Self>,
4416 ) {
4417 let destination = match self.find_pane_in_direction(action.direction, cx) {
4418 Some(destination) => destination,
4419 None => {
4420 if !action.clone && self.active_pane.read(cx).items_len() < 2 {
4421 return;
4422 }
4423 let new_pane = self.add_pane(window, cx);
4424 if self
4425 .center
4426 .split(&self.active_pane, &new_pane, action.direction, cx)
4427 .log_err()
4428 .is_none()
4429 {
4430 return;
4431 };
4432 new_pane
4433 }
4434 };
4435
4436 if action.clone {
4437 if self
4438 .active_pane
4439 .read(cx)
4440 .active_item()
4441 .is_some_and(|item| item.can_split(cx))
4442 {
4443 clone_active_item(
4444 self.database_id(),
4445 &self.active_pane,
4446 &destination,
4447 action.focus,
4448 window,
4449 cx,
4450 );
4451 return;
4452 }
4453 }
4454 move_active_item(
4455 &self.active_pane,
4456 &destination,
4457 action.focus,
4458 true,
4459 window,
4460 cx,
4461 );
4462 }
4463
4464 pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
4465 self.center.bounding_box_for_pane(pane)
4466 }
4467
4468 pub fn find_pane_in_direction(
4469 &mut self,
4470 direction: SplitDirection,
4471 cx: &App,
4472 ) -> Option<Entity<Pane>> {
4473 self.center
4474 .find_pane_in_direction(&self.active_pane, direction, cx)
4475 .cloned()
4476 }
4477
4478 pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
4479 if let Some(to) = self.find_pane_in_direction(direction, cx) {
4480 self.center.swap(&self.active_pane, &to, cx);
4481 cx.notify();
4482 }
4483 }
4484
4485 pub fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
4486 if self
4487 .center
4488 .move_to_border(&self.active_pane, direction, cx)
4489 .unwrap()
4490 {
4491 cx.notify();
4492 }
4493 }
4494
4495 pub fn resize_pane(
4496 &mut self,
4497 axis: gpui::Axis,
4498 amount: Pixels,
4499 window: &mut Window,
4500 cx: &mut Context<Self>,
4501 ) {
4502 let docks = self.all_docks();
4503 let active_dock = docks
4504 .into_iter()
4505 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
4506
4507 if let Some(dock) = active_dock {
4508 let Some(panel_size) = dock.read(cx).active_panel_size(window, cx) else {
4509 return;
4510 };
4511 match dock.read(cx).position() {
4512 DockPosition::Left => self.resize_left_dock(panel_size + amount, window, cx),
4513 DockPosition::Bottom => self.resize_bottom_dock(panel_size + amount, window, cx),
4514 DockPosition::Right => self.resize_right_dock(panel_size + amount, window, cx),
4515 }
4516 } else {
4517 self.center
4518 .resize(&self.active_pane, axis, amount, &self.bounds, cx);
4519 }
4520 cx.notify();
4521 }
4522
4523 pub fn reset_pane_sizes(&mut self, cx: &mut Context<Self>) {
4524 self.center.reset_pane_sizes(cx);
4525 cx.notify();
4526 }
4527
4528 fn handle_pane_focused(
4529 &mut self,
4530 pane: Entity<Pane>,
4531 window: &mut Window,
4532 cx: &mut Context<Self>,
4533 ) {
4534 // This is explicitly hoisted out of the following check for pane identity as
4535 // terminal panel panes are not registered as a center panes.
4536 self.status_bar.update(cx, |status_bar, cx| {
4537 status_bar.set_active_pane(&pane, window, cx);
4538 });
4539 if self.active_pane != pane {
4540 self.set_active_pane(&pane, window, cx);
4541 }
4542
4543 if self.last_active_center_pane.is_none() {
4544 self.last_active_center_pane = Some(pane.downgrade());
4545 }
4546
4547 // If this pane is in a dock, preserve that dock when dismissing zoomed items.
4548 // This prevents the dock from closing when focus events fire during window activation.
4549 // We also preserve any dock whose active panel itself has focus — this covers
4550 // panels like AgentPanel that don't implement `pane()` but can still be zoomed.
4551 let dock_to_preserve = self.all_docks().iter().find_map(|dock| {
4552 let dock_read = dock.read(cx);
4553 if let Some(panel) = dock_read.active_panel() {
4554 if panel.pane(cx).is_some_and(|dock_pane| dock_pane == pane)
4555 || panel.panel_focus_handle(cx).contains_focused(window, cx)
4556 {
4557 return Some(dock_read.position());
4558 }
4559 }
4560 None
4561 });
4562
4563 self.dismiss_zoomed_items_to_reveal(dock_to_preserve, window, cx);
4564 if pane.read(cx).is_zoomed() {
4565 self.zoomed = Some(pane.downgrade().into());
4566 } else {
4567 self.zoomed = None;
4568 }
4569 self.zoomed_position = None;
4570 cx.emit(Event::ZoomChanged);
4571 self.update_active_view_for_followers(window, cx);
4572 pane.update(cx, |pane, _| {
4573 pane.track_alternate_file_items();
4574 });
4575
4576 cx.notify();
4577 }
4578
4579 fn set_active_pane(
4580 &mut self,
4581 pane: &Entity<Pane>,
4582 window: &mut Window,
4583 cx: &mut Context<Self>,
4584 ) {
4585 self.active_pane = pane.clone();
4586 self.active_item_path_changed(true, window, cx);
4587 self.last_active_center_pane = Some(pane.downgrade());
4588 }
4589
4590 fn handle_panel_focused(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4591 self.update_active_view_for_followers(window, cx);
4592 }
4593
4594 fn handle_pane_event(
4595 &mut self,
4596 pane: &Entity<Pane>,
4597 event: &pane::Event,
4598 window: &mut Window,
4599 cx: &mut Context<Self>,
4600 ) {
4601 let mut serialize_workspace = true;
4602 match event {
4603 pane::Event::AddItem { item } => {
4604 item.added_to_pane(self, pane.clone(), window, cx);
4605 cx.emit(Event::ItemAdded {
4606 item: item.boxed_clone(),
4607 });
4608 }
4609 pane::Event::Split { direction, mode } => {
4610 match mode {
4611 SplitMode::ClonePane => {
4612 self.split_and_clone(pane.clone(), *direction, window, cx)
4613 .detach();
4614 }
4615 SplitMode::EmptyPane => {
4616 self.split_pane(pane.clone(), *direction, window, cx);
4617 }
4618 SplitMode::MovePane => {
4619 self.split_and_move(pane.clone(), *direction, window, cx);
4620 }
4621 };
4622 }
4623 pane::Event::JoinIntoNext => {
4624 self.join_pane_into_next(pane.clone(), window, cx);
4625 }
4626 pane::Event::JoinAll => {
4627 self.join_all_panes(window, cx);
4628 }
4629 pane::Event::Remove { focus_on_pane } => {
4630 self.remove_pane(pane.clone(), focus_on_pane.clone(), window, cx);
4631 }
4632 pane::Event::ActivateItem {
4633 local,
4634 focus_changed,
4635 } => {
4636 window.invalidate_character_coordinates();
4637
4638 pane.update(cx, |pane, _| {
4639 pane.track_alternate_file_items();
4640 });
4641 if *local {
4642 self.unfollow_in_pane(pane, window, cx);
4643 }
4644 serialize_workspace = *focus_changed || pane != self.active_pane();
4645 if pane == self.active_pane() {
4646 self.active_item_path_changed(*focus_changed, window, cx);
4647 self.update_active_view_for_followers(window, cx);
4648 } else if *local {
4649 self.set_active_pane(pane, window, cx);
4650 }
4651 }
4652 pane::Event::UserSavedItem { item, save_intent } => {
4653 cx.emit(Event::UserSavedItem {
4654 pane: pane.downgrade(),
4655 item: item.boxed_clone(),
4656 save_intent: *save_intent,
4657 });
4658 serialize_workspace = false;
4659 }
4660 pane::Event::ChangeItemTitle => {
4661 if *pane == self.active_pane {
4662 self.active_item_path_changed(false, window, cx);
4663 }
4664 serialize_workspace = false;
4665 }
4666 pane::Event::RemovedItem { item } => {
4667 cx.emit(Event::ActiveItemChanged);
4668 self.update_window_edited(window, cx);
4669 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id())
4670 && entry.get().entity_id() == pane.entity_id()
4671 {
4672 entry.remove();
4673 }
4674 cx.emit(Event::ItemRemoved {
4675 item_id: item.item_id(),
4676 });
4677 }
4678 pane::Event::Focus => {
4679 window.invalidate_character_coordinates();
4680 self.handle_pane_focused(pane.clone(), window, cx);
4681 }
4682 pane::Event::ZoomIn => {
4683 if *pane == self.active_pane {
4684 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
4685 if pane.read(cx).has_focus(window, cx) {
4686 self.zoomed = Some(pane.downgrade().into());
4687 self.zoomed_position = None;
4688 cx.emit(Event::ZoomChanged);
4689 }
4690 cx.notify();
4691 }
4692 }
4693 pane::Event::ZoomOut => {
4694 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
4695 if self.zoomed_position.is_none() {
4696 self.zoomed = None;
4697 cx.emit(Event::ZoomChanged);
4698 }
4699 cx.notify();
4700 }
4701 pane::Event::ItemPinned | pane::Event::ItemUnpinned => {}
4702 }
4703
4704 if serialize_workspace {
4705 self.serialize_workspace(window, cx);
4706 }
4707 }
4708
4709 pub fn unfollow_in_pane(
4710 &mut self,
4711 pane: &Entity<Pane>,
4712 window: &mut Window,
4713 cx: &mut Context<Workspace>,
4714 ) -> Option<CollaboratorId> {
4715 let leader_id = self.leader_for_pane(pane)?;
4716 self.unfollow(leader_id, window, cx);
4717 Some(leader_id)
4718 }
4719
4720 pub fn split_pane(
4721 &mut self,
4722 pane_to_split: Entity<Pane>,
4723 split_direction: SplitDirection,
4724 window: &mut Window,
4725 cx: &mut Context<Self>,
4726 ) -> Entity<Pane> {
4727 let new_pane = self.add_pane(window, cx);
4728 self.center
4729 .split(&pane_to_split, &new_pane, split_direction, cx)
4730 .unwrap();
4731 cx.notify();
4732 new_pane
4733 }
4734
4735 pub fn split_and_move(
4736 &mut self,
4737 pane: Entity<Pane>,
4738 direction: SplitDirection,
4739 window: &mut Window,
4740 cx: &mut Context<Self>,
4741 ) {
4742 let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx)) else {
4743 return;
4744 };
4745 let new_pane = self.add_pane(window, cx);
4746 new_pane.update(cx, |pane, cx| {
4747 pane.add_item(item, true, true, None, window, cx)
4748 });
4749 self.center.split(&pane, &new_pane, direction, cx).unwrap();
4750 cx.notify();
4751 }
4752
4753 pub fn split_and_clone(
4754 &mut self,
4755 pane: Entity<Pane>,
4756 direction: SplitDirection,
4757 window: &mut Window,
4758 cx: &mut Context<Self>,
4759 ) -> Task<Option<Entity<Pane>>> {
4760 let Some(item) = pane.read(cx).active_item() else {
4761 return Task::ready(None);
4762 };
4763 if !item.can_split(cx) {
4764 return Task::ready(None);
4765 }
4766 let task = item.clone_on_split(self.database_id(), window, cx);
4767 cx.spawn_in(window, async move |this, cx| {
4768 if let Some(clone) = task.await {
4769 this.update_in(cx, |this, window, cx| {
4770 let new_pane = this.add_pane(window, cx);
4771 let nav_history = pane.read(cx).fork_nav_history();
4772 new_pane.update(cx, |pane, cx| {
4773 pane.set_nav_history(nav_history, cx);
4774 pane.add_item(clone, true, true, None, window, cx)
4775 });
4776 this.center.split(&pane, &new_pane, direction, cx).unwrap();
4777 cx.notify();
4778 new_pane
4779 })
4780 .ok()
4781 } else {
4782 None
4783 }
4784 })
4785 }
4786
4787 pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4788 let active_item = self.active_pane.read(cx).active_item();
4789 for pane in &self.panes {
4790 join_pane_into_active(&self.active_pane, pane, window, cx);
4791 }
4792 if let Some(active_item) = active_item {
4793 self.activate_item(active_item.as_ref(), true, true, window, cx);
4794 }
4795 cx.notify();
4796 }
4797
4798 pub fn join_pane_into_next(
4799 &mut self,
4800 pane: Entity<Pane>,
4801 window: &mut Window,
4802 cx: &mut Context<Self>,
4803 ) {
4804 let next_pane = self
4805 .find_pane_in_direction(SplitDirection::Right, cx)
4806 .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
4807 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
4808 .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
4809 let Some(next_pane) = next_pane else {
4810 return;
4811 };
4812 move_all_items(&pane, &next_pane, window, cx);
4813 cx.notify();
4814 }
4815
4816 fn remove_pane(
4817 &mut self,
4818 pane: Entity<Pane>,
4819 focus_on: Option<Entity<Pane>>,
4820 window: &mut Window,
4821 cx: &mut Context<Self>,
4822 ) {
4823 if self.center.remove(&pane, cx).unwrap() {
4824 self.force_remove_pane(&pane, &focus_on, window, cx);
4825 self.unfollow_in_pane(&pane, window, cx);
4826 self.last_leaders_by_pane.remove(&pane.downgrade());
4827 for removed_item in pane.read(cx).items() {
4828 self.panes_by_item.remove(&removed_item.item_id());
4829 }
4830
4831 cx.notify();
4832 } else {
4833 self.active_item_path_changed(true, window, cx);
4834 }
4835 cx.emit(Event::PaneRemoved);
4836 }
4837
4838 pub fn panes_mut(&mut self) -> &mut [Entity<Pane>] {
4839 &mut self.panes
4840 }
4841
4842 pub fn panes(&self) -> &[Entity<Pane>] {
4843 &self.panes
4844 }
4845
4846 pub fn active_pane(&self) -> &Entity<Pane> {
4847 &self.active_pane
4848 }
4849
4850 pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> {
4851 for dock in self.all_docks() {
4852 if dock.focus_handle(cx).contains_focused(window, cx)
4853 && let Some(pane) = dock
4854 .read(cx)
4855 .active_panel()
4856 .and_then(|panel| panel.pane(cx))
4857 {
4858 return pane;
4859 }
4860 }
4861 self.active_pane().clone()
4862 }
4863
4864 pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
4865 self.find_pane_in_direction(SplitDirection::Right, cx)
4866 .unwrap_or_else(|| {
4867 self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx)
4868 })
4869 }
4870
4871 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> {
4872 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
4873 weak_pane.upgrade()
4874 }
4875
4876 fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
4877 self.follower_states.retain(|leader_id, state| {
4878 if *leader_id == CollaboratorId::PeerId(peer_id) {
4879 for item in state.items_by_leader_view_id.values() {
4880 item.view.set_leader_id(None, window, cx);
4881 }
4882 false
4883 } else {
4884 true
4885 }
4886 });
4887 cx.notify();
4888 }
4889
4890 pub fn start_following(
4891 &mut self,
4892 leader_id: impl Into<CollaboratorId>,
4893 window: &mut Window,
4894 cx: &mut Context<Self>,
4895 ) -> Option<Task<Result<()>>> {
4896 let leader_id = leader_id.into();
4897 let pane = self.active_pane().clone();
4898
4899 self.last_leaders_by_pane
4900 .insert(pane.downgrade(), leader_id);
4901 self.unfollow(leader_id, window, cx);
4902 self.unfollow_in_pane(&pane, window, cx);
4903 self.follower_states.insert(
4904 leader_id,
4905 FollowerState {
4906 center_pane: pane.clone(),
4907 dock_pane: None,
4908 active_view_id: None,
4909 items_by_leader_view_id: Default::default(),
4910 },
4911 );
4912 cx.notify();
4913
4914 match leader_id {
4915 CollaboratorId::PeerId(leader_peer_id) => {
4916 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
4917 let project_id = self.project.read(cx).remote_id();
4918 let request = self.app_state.client.request(proto::Follow {
4919 room_id,
4920 project_id,
4921 leader_id: Some(leader_peer_id),
4922 });
4923
4924 Some(cx.spawn_in(window, async move |this, cx| {
4925 let response = request.await?;
4926 this.update(cx, |this, _| {
4927 let state = this
4928 .follower_states
4929 .get_mut(&leader_id)
4930 .context("following interrupted")?;
4931 state.active_view_id = response
4932 .active_view
4933 .as_ref()
4934 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
4935 anyhow::Ok(())
4936 })??;
4937 if let Some(view) = response.active_view {
4938 Self::add_view_from_leader(this.clone(), leader_peer_id, &view, cx).await?;
4939 }
4940 this.update_in(cx, |this, window, cx| {
4941 this.leader_updated(leader_id, window, cx)
4942 })?;
4943 Ok(())
4944 }))
4945 }
4946 CollaboratorId::Agent => {
4947 self.leader_updated(leader_id, window, cx)?;
4948 Some(Task::ready(Ok(())))
4949 }
4950 }
4951 }
4952
4953 pub fn follow_next_collaborator(
4954 &mut self,
4955 _: &FollowNextCollaborator,
4956 window: &mut Window,
4957 cx: &mut Context<Self>,
4958 ) {
4959 let collaborators = self.project.read(cx).collaborators();
4960 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
4961 let mut collaborators = collaborators.keys().copied();
4962 for peer_id in collaborators.by_ref() {
4963 if CollaboratorId::PeerId(peer_id) == leader_id {
4964 break;
4965 }
4966 }
4967 collaborators.next().map(CollaboratorId::PeerId)
4968 } else if let Some(last_leader_id) =
4969 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
4970 {
4971 match last_leader_id {
4972 CollaboratorId::PeerId(peer_id) => {
4973 if collaborators.contains_key(peer_id) {
4974 Some(*last_leader_id)
4975 } else {
4976 None
4977 }
4978 }
4979 CollaboratorId::Agent => Some(CollaboratorId::Agent),
4980 }
4981 } else {
4982 None
4983 };
4984
4985 let pane = self.active_pane.clone();
4986 let Some(leader_id) = next_leader_id.or_else(|| {
4987 Some(CollaboratorId::PeerId(
4988 collaborators.keys().copied().next()?,
4989 ))
4990 }) else {
4991 return;
4992 };
4993 if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
4994 return;
4995 }
4996 if let Some(task) = self.start_following(leader_id, window, cx) {
4997 task.detach_and_log_err(cx)
4998 }
4999 }
5000
5001 pub fn follow(
5002 &mut self,
5003 leader_id: impl Into<CollaboratorId>,
5004 window: &mut Window,
5005 cx: &mut Context<Self>,
5006 ) {
5007 let leader_id = leader_id.into();
5008
5009 if let CollaboratorId::PeerId(peer_id) = leader_id {
5010 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
5011 return;
5012 };
5013 let room = room.read(cx);
5014 let Some(remote_participant) = room.remote_participant_for_peer_id(peer_id) else {
5015 return;
5016 };
5017
5018 let project = self.project.read(cx);
5019
5020 let other_project_id = match remote_participant.location {
5021 call::ParticipantLocation::External => None,
5022 call::ParticipantLocation::UnsharedProject => None,
5023 call::ParticipantLocation::SharedProject { project_id } => {
5024 if Some(project_id) == project.remote_id() {
5025 None
5026 } else {
5027 Some(project_id)
5028 }
5029 }
5030 };
5031
5032 // if they are active in another project, follow there.
5033 if let Some(project_id) = other_project_id {
5034 let app_state = self.app_state.clone();
5035 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
5036 .detach_and_log_err(cx);
5037 }
5038 }
5039
5040 // if you're already following, find the right pane and focus it.
5041 if let Some(follower_state) = self.follower_states.get(&leader_id) {
5042 window.focus(&follower_state.pane().focus_handle(cx), cx);
5043
5044 return;
5045 }
5046
5047 // Otherwise, follow.
5048 if let Some(task) = self.start_following(leader_id, window, cx) {
5049 task.detach_and_log_err(cx)
5050 }
5051 }
5052
5053 pub fn unfollow(
5054 &mut self,
5055 leader_id: impl Into<CollaboratorId>,
5056 window: &mut Window,
5057 cx: &mut Context<Self>,
5058 ) -> Option<()> {
5059 cx.notify();
5060
5061 let leader_id = leader_id.into();
5062 let state = self.follower_states.remove(&leader_id)?;
5063 for (_, item) in state.items_by_leader_view_id {
5064 item.view.set_leader_id(None, window, cx);
5065 }
5066
5067 if let CollaboratorId::PeerId(leader_peer_id) = leader_id {
5068 let project_id = self.project.read(cx).remote_id();
5069 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
5070 self.app_state
5071 .client
5072 .send(proto::Unfollow {
5073 room_id,
5074 project_id,
5075 leader_id: Some(leader_peer_id),
5076 })
5077 .log_err();
5078 }
5079
5080 Some(())
5081 }
5082
5083 pub fn is_being_followed(&self, id: impl Into<CollaboratorId>) -> bool {
5084 self.follower_states.contains_key(&id.into())
5085 }
5086
5087 fn active_item_path_changed(
5088 &mut self,
5089 focus_changed: bool,
5090 window: &mut Window,
5091 cx: &mut Context<Self>,
5092 ) {
5093 cx.emit(Event::ActiveItemChanged);
5094 let active_entry = self.active_project_path(cx);
5095 self.project.update(cx, |project, cx| {
5096 project.set_active_path(active_entry.clone(), cx)
5097 });
5098
5099 if focus_changed && let Some(project_path) = &active_entry {
5100 let git_store_entity = self.project.read(cx).git_store().clone();
5101 git_store_entity.update(cx, |git_store, cx| {
5102 git_store.set_active_repo_for_path(project_path, cx);
5103 });
5104 }
5105
5106 self.update_window_title(window, cx);
5107 }
5108
5109 fn update_window_title(&mut self, window: &mut Window, cx: &mut App) {
5110 let project = self.project().read(cx);
5111 let mut title = String::new();
5112
5113 for (i, worktree) in project.visible_worktrees(cx).enumerate() {
5114 let name = {
5115 let settings_location = SettingsLocation {
5116 worktree_id: worktree.read(cx).id(),
5117 path: RelPath::empty(),
5118 };
5119
5120 let settings = WorktreeSettings::get(Some(settings_location), cx);
5121 match &settings.project_name {
5122 Some(name) => name.as_str(),
5123 None => worktree.read(cx).root_name_str(),
5124 }
5125 };
5126 if i > 0 {
5127 title.push_str(", ");
5128 }
5129 title.push_str(name);
5130 }
5131
5132 if title.is_empty() {
5133 title = "empty project".to_string();
5134 }
5135
5136 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
5137 let filename = path.path.file_name().or_else(|| {
5138 Some(
5139 project
5140 .worktree_for_id(path.worktree_id, cx)?
5141 .read(cx)
5142 .root_name_str(),
5143 )
5144 });
5145
5146 if let Some(filename) = filename {
5147 title.push_str(" — ");
5148 title.push_str(filename.as_ref());
5149 }
5150 }
5151
5152 if project.is_via_collab() {
5153 title.push_str(" ↙");
5154 } else if project.is_shared() {
5155 title.push_str(" ↗");
5156 }
5157
5158 if let Some(last_title) = self.last_window_title.as_ref()
5159 && &title == last_title
5160 {
5161 return;
5162 }
5163 window.set_window_title(&title);
5164 SystemWindowTabController::update_tab_title(
5165 cx,
5166 window.window_handle().window_id(),
5167 SharedString::from(&title),
5168 );
5169 self.last_window_title = Some(title);
5170 }
5171
5172 fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
5173 let is_edited = !self.project.read(cx).is_disconnected(cx) && !self.dirty_items.is_empty();
5174 if is_edited != self.window_edited {
5175 self.window_edited = is_edited;
5176 window.set_window_edited(self.window_edited)
5177 }
5178 }
5179
5180 fn update_item_dirty_state(
5181 &mut self,
5182 item: &dyn ItemHandle,
5183 window: &mut Window,
5184 cx: &mut App,
5185 ) {
5186 let is_dirty = item.is_dirty(cx);
5187 let item_id = item.item_id();
5188 let was_dirty = self.dirty_items.contains_key(&item_id);
5189 if is_dirty == was_dirty {
5190 return;
5191 }
5192 if was_dirty {
5193 self.dirty_items.remove(&item_id);
5194 self.update_window_edited(window, cx);
5195 return;
5196 }
5197
5198 let workspace = self.weak_handle();
5199 let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
5200 return;
5201 };
5202 let on_release_callback = Box::new(move |cx: &mut App| {
5203 window_handle
5204 .update(cx, |_, window, cx| {
5205 workspace
5206 .update(cx, |workspace, cx| {
5207 workspace.dirty_items.remove(&item_id);
5208 workspace.update_window_edited(window, cx)
5209 })
5210 .ok();
5211 })
5212 .ok();
5213 });
5214
5215 let s = item.on_release(cx, on_release_callback);
5216 self.dirty_items.insert(item_id, s);
5217 self.update_window_edited(window, cx);
5218 }
5219
5220 fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
5221 if self.notifications.is_empty() {
5222 None
5223 } else {
5224 Some(
5225 div()
5226 .absolute()
5227 .right_3()
5228 .bottom_3()
5229 .w_112()
5230 .h_full()
5231 .flex()
5232 .flex_col()
5233 .justify_end()
5234 .gap_2()
5235 .children(
5236 self.notifications
5237 .iter()
5238 .map(|(_, notification)| notification.clone().into_any()),
5239 ),
5240 )
5241 }
5242 }
5243
5244 // RPC handlers
5245
5246 fn active_view_for_follower(
5247 &self,
5248 follower_project_id: Option<u64>,
5249 window: &mut Window,
5250 cx: &mut Context<Self>,
5251 ) -> Option<proto::View> {
5252 let (item, panel_id) = self.active_item_for_followers(window, cx);
5253 let item = item?;
5254 let leader_id = self
5255 .pane_for(&*item)
5256 .and_then(|pane| self.leader_for_pane(&pane));
5257 let leader_peer_id = match leader_id {
5258 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
5259 Some(CollaboratorId::Agent) | None => None,
5260 };
5261
5262 let item_handle = item.to_followable_item_handle(cx)?;
5263 let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
5264 let variant = item_handle.to_state_proto(window, cx)?;
5265
5266 if item_handle.is_project_item(window, cx)
5267 && (follower_project_id.is_none()
5268 || follower_project_id != self.project.read(cx).remote_id())
5269 {
5270 return None;
5271 }
5272
5273 Some(proto::View {
5274 id: id.to_proto(),
5275 leader_id: leader_peer_id,
5276 variant: Some(variant),
5277 panel_id: panel_id.map(|id| id as i32),
5278 })
5279 }
5280
5281 fn handle_follow(
5282 &mut self,
5283 follower_project_id: Option<u64>,
5284 window: &mut Window,
5285 cx: &mut Context<Self>,
5286 ) -> proto::FollowResponse {
5287 let active_view = self.active_view_for_follower(follower_project_id, window, cx);
5288
5289 cx.notify();
5290 proto::FollowResponse {
5291 views: active_view.iter().cloned().collect(),
5292 active_view,
5293 }
5294 }
5295
5296 fn handle_update_followers(
5297 &mut self,
5298 leader_id: PeerId,
5299 message: proto::UpdateFollowers,
5300 _window: &mut Window,
5301 _cx: &mut Context<Self>,
5302 ) {
5303 self.leader_updates_tx
5304 .unbounded_send((leader_id, message))
5305 .ok();
5306 }
5307
5308 async fn process_leader_update(
5309 this: &WeakEntity<Self>,
5310 leader_id: PeerId,
5311 update: proto::UpdateFollowers,
5312 cx: &mut AsyncWindowContext,
5313 ) -> Result<()> {
5314 match update.variant.context("invalid update")? {
5315 proto::update_followers::Variant::CreateView(view) => {
5316 let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
5317 let should_add_view = this.update(cx, |this, _| {
5318 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
5319 anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
5320 } else {
5321 anyhow::Ok(false)
5322 }
5323 })??;
5324
5325 if should_add_view {
5326 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
5327 }
5328 }
5329 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
5330 let should_add_view = this.update(cx, |this, _| {
5331 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
5332 state.active_view_id = update_active_view
5333 .view
5334 .as_ref()
5335 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
5336
5337 if state.active_view_id.is_some_and(|view_id| {
5338 !state.items_by_leader_view_id.contains_key(&view_id)
5339 }) {
5340 anyhow::Ok(true)
5341 } else {
5342 anyhow::Ok(false)
5343 }
5344 } else {
5345 anyhow::Ok(false)
5346 }
5347 })??;
5348
5349 if should_add_view && let Some(view) = update_active_view.view {
5350 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
5351 }
5352 }
5353 proto::update_followers::Variant::UpdateView(update_view) => {
5354 let variant = update_view.variant.context("missing update view variant")?;
5355 let id = update_view.id.context("missing update view id")?;
5356 let mut tasks = Vec::new();
5357 this.update_in(cx, |this, window, cx| {
5358 let project = this.project.clone();
5359 if let Some(state) = this.follower_states.get(&leader_id.into()) {
5360 let view_id = ViewId::from_proto(id.clone())?;
5361 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
5362 tasks.push(item.view.apply_update_proto(
5363 &project,
5364 variant.clone(),
5365 window,
5366 cx,
5367 ));
5368 }
5369 }
5370 anyhow::Ok(())
5371 })??;
5372 try_join_all(tasks).await.log_err();
5373 }
5374 }
5375 this.update_in(cx, |this, window, cx| {
5376 this.leader_updated(leader_id, window, cx)
5377 })?;
5378 Ok(())
5379 }
5380
5381 async fn add_view_from_leader(
5382 this: WeakEntity<Self>,
5383 leader_id: PeerId,
5384 view: &proto::View,
5385 cx: &mut AsyncWindowContext,
5386 ) -> Result<()> {
5387 let this = this.upgrade().context("workspace dropped")?;
5388
5389 let Some(id) = view.id.clone() else {
5390 anyhow::bail!("no id for view");
5391 };
5392 let id = ViewId::from_proto(id)?;
5393 let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
5394
5395 let pane = this.update(cx, |this, _cx| {
5396 let state = this
5397 .follower_states
5398 .get(&leader_id.into())
5399 .context("stopped following")?;
5400 anyhow::Ok(state.pane().clone())
5401 })?;
5402 let existing_item = pane.update_in(cx, |pane, window, cx| {
5403 let client = this.read(cx).client().clone();
5404 pane.items().find_map(|item| {
5405 let item = item.to_followable_item_handle(cx)?;
5406 if item.remote_id(&client, window, cx) == Some(id) {
5407 Some(item)
5408 } else {
5409 None
5410 }
5411 })
5412 })?;
5413 let item = if let Some(existing_item) = existing_item {
5414 existing_item
5415 } else {
5416 let variant = view.variant.clone();
5417 anyhow::ensure!(variant.is_some(), "missing view variant");
5418
5419 let task = cx.update(|window, cx| {
5420 FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx)
5421 })?;
5422
5423 let Some(task) = task else {
5424 anyhow::bail!(
5425 "failed to construct view from leader (maybe from a different version of zed?)"
5426 );
5427 };
5428
5429 let mut new_item = task.await?;
5430 pane.update_in(cx, |pane, window, cx| {
5431 let mut item_to_remove = None;
5432 for (ix, item) in pane.items().enumerate() {
5433 if let Some(item) = item.to_followable_item_handle(cx) {
5434 match new_item.dedup(item.as_ref(), window, cx) {
5435 Some(item::Dedup::KeepExisting) => {
5436 new_item =
5437 item.boxed_clone().to_followable_item_handle(cx).unwrap();
5438 break;
5439 }
5440 Some(item::Dedup::ReplaceExisting) => {
5441 item_to_remove = Some((ix, item.item_id()));
5442 break;
5443 }
5444 None => {}
5445 }
5446 }
5447 }
5448
5449 if let Some((ix, id)) = item_to_remove {
5450 pane.remove_item(id, false, false, window, cx);
5451 pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx);
5452 }
5453 })?;
5454
5455 new_item
5456 };
5457
5458 this.update_in(cx, |this, window, cx| {
5459 let state = this.follower_states.get_mut(&leader_id.into())?;
5460 item.set_leader_id(Some(leader_id.into()), window, cx);
5461 state.items_by_leader_view_id.insert(
5462 id,
5463 FollowerView {
5464 view: item,
5465 location: panel_id,
5466 },
5467 );
5468
5469 Some(())
5470 })
5471 .context("no follower state")?;
5472
5473 Ok(())
5474 }
5475
5476 fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5477 let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else {
5478 return;
5479 };
5480
5481 if let Some(agent_location) = self.project.read(cx).agent_location() {
5482 let buffer_entity_id = agent_location.buffer.entity_id();
5483 let view_id = ViewId {
5484 creator: CollaboratorId::Agent,
5485 id: buffer_entity_id.as_u64(),
5486 };
5487 follower_state.active_view_id = Some(view_id);
5488
5489 let item = match follower_state.items_by_leader_view_id.entry(view_id) {
5490 hash_map::Entry::Occupied(entry) => Some(entry.into_mut()),
5491 hash_map::Entry::Vacant(entry) => {
5492 let existing_view =
5493 follower_state
5494 .center_pane
5495 .read(cx)
5496 .items()
5497 .find_map(|item| {
5498 let item = item.to_followable_item_handle(cx)?;
5499 if item.buffer_kind(cx) == ItemBufferKind::Singleton
5500 && item.project_item_model_ids(cx).as_slice()
5501 == [buffer_entity_id]
5502 {
5503 Some(item)
5504 } else {
5505 None
5506 }
5507 });
5508 let view = existing_view.or_else(|| {
5509 agent_location.buffer.upgrade().and_then(|buffer| {
5510 cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| {
5511 registry.build_item(buffer, self.project.clone(), None, window, cx)
5512 })?
5513 .to_followable_item_handle(cx)
5514 })
5515 });
5516
5517 view.map(|view| {
5518 entry.insert(FollowerView {
5519 view,
5520 location: None,
5521 })
5522 })
5523 }
5524 };
5525
5526 if let Some(item) = item {
5527 item.view
5528 .set_leader_id(Some(CollaboratorId::Agent), window, cx);
5529 item.view
5530 .update_agent_location(agent_location.position, window, cx);
5531 }
5532 } else {
5533 follower_state.active_view_id = None;
5534 }
5535
5536 self.leader_updated(CollaboratorId::Agent, window, cx);
5537 }
5538
5539 pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
5540 let mut is_project_item = true;
5541 let mut update = proto::UpdateActiveView::default();
5542 if window.is_window_active() {
5543 let (active_item, panel_id) = self.active_item_for_followers(window, cx);
5544
5545 if let Some(item) = active_item
5546 && item.item_focus_handle(cx).contains_focused(window, cx)
5547 {
5548 let leader_id = self
5549 .pane_for(&*item)
5550 .and_then(|pane| self.leader_for_pane(&pane));
5551 let leader_peer_id = match leader_id {
5552 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
5553 Some(CollaboratorId::Agent) | None => None,
5554 };
5555
5556 if let Some(item) = item.to_followable_item_handle(cx) {
5557 let id = item
5558 .remote_id(&self.app_state.client, window, cx)
5559 .map(|id| id.to_proto());
5560
5561 if let Some(id) = id
5562 && let Some(variant) = item.to_state_proto(window, cx)
5563 {
5564 let view = Some(proto::View {
5565 id,
5566 leader_id: leader_peer_id,
5567 variant: Some(variant),
5568 panel_id: panel_id.map(|id| id as i32),
5569 });
5570
5571 is_project_item = item.is_project_item(window, cx);
5572 update = proto::UpdateActiveView { view };
5573 };
5574 }
5575 }
5576 }
5577
5578 let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
5579 if active_view_id != self.last_active_view_id.as_ref() {
5580 self.last_active_view_id = active_view_id.cloned();
5581 self.update_followers(
5582 is_project_item,
5583 proto::update_followers::Variant::UpdateActiveView(update),
5584 window,
5585 cx,
5586 );
5587 }
5588 }
5589
5590 fn active_item_for_followers(
5591 &self,
5592 window: &mut Window,
5593 cx: &mut App,
5594 ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
5595 let mut active_item = None;
5596 let mut panel_id = None;
5597 for dock in self.all_docks() {
5598 if dock.focus_handle(cx).contains_focused(window, cx)
5599 && let Some(panel) = dock.read(cx).active_panel()
5600 && let Some(pane) = panel.pane(cx)
5601 && let Some(item) = pane.read(cx).active_item()
5602 {
5603 active_item = Some(item);
5604 panel_id = panel.remote_id();
5605 break;
5606 }
5607 }
5608
5609 if active_item.is_none() {
5610 active_item = self.active_pane().read(cx).active_item();
5611 }
5612 (active_item, panel_id)
5613 }
5614
5615 fn update_followers(
5616 &self,
5617 project_only: bool,
5618 update: proto::update_followers::Variant,
5619 _: &mut Window,
5620 cx: &mut App,
5621 ) -> Option<()> {
5622 // If this update only applies to for followers in the current project,
5623 // then skip it unless this project is shared. If it applies to all
5624 // followers, regardless of project, then set `project_id` to none,
5625 // indicating that it goes to all followers.
5626 let project_id = if project_only {
5627 Some(self.project.read(cx).remote_id()?)
5628 } else {
5629 None
5630 };
5631 self.app_state().workspace_store.update(cx, |store, cx| {
5632 store.update_followers(project_id, update, cx)
5633 })
5634 }
5635
5636 pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<CollaboratorId> {
5637 self.follower_states.iter().find_map(|(leader_id, state)| {
5638 if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
5639 Some(*leader_id)
5640 } else {
5641 None
5642 }
5643 })
5644 }
5645
5646 fn leader_updated(
5647 &mut self,
5648 leader_id: impl Into<CollaboratorId>,
5649 window: &mut Window,
5650 cx: &mut Context<Self>,
5651 ) -> Option<Box<dyn ItemHandle>> {
5652 cx.notify();
5653
5654 let leader_id = leader_id.into();
5655 let (panel_id, item) = match leader_id {
5656 CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?,
5657 CollaboratorId::Agent => (None, self.active_item_for_agent()?),
5658 };
5659
5660 let state = self.follower_states.get(&leader_id)?;
5661 let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
5662 let pane;
5663 if let Some(panel_id) = panel_id {
5664 pane = self
5665 .activate_panel_for_proto_id(panel_id, window, cx)?
5666 .pane(cx)?;
5667 let state = self.follower_states.get_mut(&leader_id)?;
5668 state.dock_pane = Some(pane.clone());
5669 } else {
5670 pane = state.center_pane.clone();
5671 let state = self.follower_states.get_mut(&leader_id)?;
5672 if let Some(dock_pane) = state.dock_pane.take() {
5673 transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx);
5674 }
5675 }
5676
5677 pane.update(cx, |pane, cx| {
5678 let focus_active_item = pane.has_focus(window, cx) || transfer_focus;
5679 if let Some(index) = pane.index_for_item(item.as_ref()) {
5680 pane.activate_item(index, false, false, window, cx);
5681 } else {
5682 pane.add_item(item.boxed_clone(), false, false, None, window, cx)
5683 }
5684
5685 if focus_active_item {
5686 pane.focus_active_item(window, cx)
5687 }
5688 });
5689
5690 Some(item)
5691 }
5692
5693 fn active_item_for_agent(&self) -> Option<Box<dyn ItemHandle>> {
5694 let state = self.follower_states.get(&CollaboratorId::Agent)?;
5695 let active_view_id = state.active_view_id?;
5696 Some(
5697 state
5698 .items_by_leader_view_id
5699 .get(&active_view_id)?
5700 .view
5701 .boxed_clone(),
5702 )
5703 }
5704
5705 fn active_item_for_peer(
5706 &self,
5707 peer_id: PeerId,
5708 window: &mut Window,
5709 cx: &mut Context<Self>,
5710 ) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
5711 let call = self.active_call()?;
5712 let room = call.read(cx).room()?.read(cx);
5713 let participant = room.remote_participant_for_peer_id(peer_id)?;
5714 let leader_in_this_app;
5715 let leader_in_this_project;
5716 match participant.location {
5717 call::ParticipantLocation::SharedProject { project_id } => {
5718 leader_in_this_app = true;
5719 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
5720 }
5721 call::ParticipantLocation::UnsharedProject => {
5722 leader_in_this_app = true;
5723 leader_in_this_project = false;
5724 }
5725 call::ParticipantLocation::External => {
5726 leader_in_this_app = false;
5727 leader_in_this_project = false;
5728 }
5729 };
5730 let state = self.follower_states.get(&peer_id.into())?;
5731 let mut item_to_activate = None;
5732 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
5733 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id)
5734 && (leader_in_this_project || !item.view.is_project_item(window, cx))
5735 {
5736 item_to_activate = Some((item.location, item.view.boxed_clone()));
5737 }
5738 } else if let Some(shared_screen) =
5739 self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx)
5740 {
5741 item_to_activate = Some((None, Box::new(shared_screen)));
5742 }
5743 item_to_activate
5744 }
5745
5746 fn shared_screen_for_peer(
5747 &self,
5748 peer_id: PeerId,
5749 pane: &Entity<Pane>,
5750 window: &mut Window,
5751 cx: &mut App,
5752 ) -> Option<Entity<SharedScreen>> {
5753 let call = self.active_call()?;
5754 let room = call.read(cx).room()?.clone();
5755 let participant = room.read(cx).remote_participant_for_peer_id(peer_id)?;
5756 let track = participant.video_tracks.values().next()?.clone();
5757 let user = participant.user.clone();
5758
5759 for item in pane.read(cx).items_of_type::<SharedScreen>() {
5760 if item.read(cx).peer_id == peer_id {
5761 return Some(item);
5762 }
5763 }
5764
5765 Some(cx.new(|cx| SharedScreen::new(track, peer_id, user.clone(), room.clone(), window, cx)))
5766 }
5767
5768 pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5769 if window.is_window_active() {
5770 self.update_active_view_for_followers(window, cx);
5771
5772 if let Some(database_id) = self.database_id {
5773 cx.background_spawn(persistence::DB.update_timestamp(database_id))
5774 .detach();
5775 }
5776 } else {
5777 for pane in &self.panes {
5778 pane.update(cx, |pane, cx| {
5779 if let Some(item) = pane.active_item() {
5780 item.workspace_deactivated(window, cx);
5781 }
5782 for item in pane.items() {
5783 if matches!(
5784 item.workspace_settings(cx).autosave,
5785 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
5786 ) {
5787 Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
5788 .detach_and_log_err(cx);
5789 }
5790 }
5791 });
5792 }
5793 }
5794 }
5795
5796 pub fn active_call(&self) -> Option<&Entity<ActiveCall>> {
5797 self.active_call.as_ref().map(|(call, _)| call)
5798 }
5799
5800 fn on_active_call_event(
5801 &mut self,
5802 _: &Entity<ActiveCall>,
5803 event: &call::room::Event,
5804 window: &mut Window,
5805 cx: &mut Context<Self>,
5806 ) {
5807 match event {
5808 call::room::Event::ParticipantLocationChanged { participant_id }
5809 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
5810 self.leader_updated(participant_id, window, cx);
5811 }
5812 _ => {}
5813 }
5814 }
5815
5816 pub fn database_id(&self) -> Option<WorkspaceId> {
5817 self.database_id
5818 }
5819
5820 pub fn session_id(&self) -> Option<String> {
5821 self.session_id.clone()
5822 }
5823
5824 pub fn root_paths(&self, cx: &App) -> Vec<Arc<Path>> {
5825 let project = self.project().read(cx);
5826 project
5827 .visible_worktrees(cx)
5828 .map(|worktree| worktree.read(cx).abs_path())
5829 .collect::<Vec<_>>()
5830 }
5831
5832 fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
5833 match member {
5834 Member::Axis(PaneAxis { members, .. }) => {
5835 for child in members.iter() {
5836 self.remove_panes(child.clone(), window, cx)
5837 }
5838 }
5839 Member::Pane(pane) => {
5840 self.force_remove_pane(&pane, &None, window, cx);
5841 }
5842 }
5843 }
5844
5845 fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
5846 self.session_id.take();
5847 self.serialize_workspace_internal(window, cx)
5848 }
5849
5850 fn force_remove_pane(
5851 &mut self,
5852 pane: &Entity<Pane>,
5853 focus_on: &Option<Entity<Pane>>,
5854 window: &mut Window,
5855 cx: &mut Context<Workspace>,
5856 ) {
5857 self.panes.retain(|p| p != pane);
5858 if let Some(focus_on) = focus_on {
5859 focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
5860 } else if self.active_pane() == pane {
5861 self.panes
5862 .last()
5863 .unwrap()
5864 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
5865 }
5866 if self.last_active_center_pane == Some(pane.downgrade()) {
5867 self.last_active_center_pane = None;
5868 }
5869 cx.notify();
5870 }
5871
5872 fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5873 if self._schedule_serialize_workspace.is_none() {
5874 self._schedule_serialize_workspace =
5875 Some(cx.spawn_in(window, async move |this, cx| {
5876 cx.background_executor()
5877 .timer(SERIALIZATION_THROTTLE_TIME)
5878 .await;
5879 this.update_in(cx, |this, window, cx| {
5880 this.serialize_workspace_internal(window, cx).detach();
5881 this._schedule_serialize_workspace.take();
5882 })
5883 .log_err();
5884 }));
5885 }
5886 }
5887
5888 fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
5889 let Some(database_id) = self.database_id() else {
5890 return Task::ready(());
5891 };
5892
5893 fn serialize_pane_handle(
5894 pane_handle: &Entity<Pane>,
5895 window: &mut Window,
5896 cx: &mut App,
5897 ) -> SerializedPane {
5898 let (items, active, pinned_count) = {
5899 let pane = pane_handle.read(cx);
5900 let active_item_id = pane.active_item().map(|item| item.item_id());
5901 (
5902 pane.items()
5903 .filter_map(|handle| {
5904 let handle = handle.to_serializable_item_handle(cx)?;
5905
5906 Some(SerializedItem {
5907 kind: Arc::from(handle.serialized_item_kind()),
5908 item_id: handle.item_id().as_u64(),
5909 active: Some(handle.item_id()) == active_item_id,
5910 preview: pane.is_active_preview_item(handle.item_id()),
5911 })
5912 })
5913 .collect::<Vec<_>>(),
5914 pane.has_focus(window, cx),
5915 pane.pinned_count(),
5916 )
5917 };
5918
5919 SerializedPane::new(items, active, pinned_count)
5920 }
5921
5922 fn build_serialized_pane_group(
5923 pane_group: &Member,
5924 window: &mut Window,
5925 cx: &mut App,
5926 ) -> SerializedPaneGroup {
5927 match pane_group {
5928 Member::Axis(PaneAxis {
5929 axis,
5930 members,
5931 flexes,
5932 bounding_boxes: _,
5933 }) => SerializedPaneGroup::Group {
5934 axis: SerializedAxis(*axis),
5935 children: members
5936 .iter()
5937 .map(|member| build_serialized_pane_group(member, window, cx))
5938 .collect::<Vec<_>>(),
5939 flexes: Some(flexes.lock().clone()),
5940 },
5941 Member::Pane(pane_handle) => {
5942 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
5943 }
5944 }
5945 }
5946
5947 fn build_serialized_docks(
5948 this: &Workspace,
5949 window: &mut Window,
5950 cx: &mut App,
5951 ) -> DockStructure {
5952 let left_dock = this.left_dock.read(cx);
5953 let left_visible = left_dock.is_open();
5954 let left_active_panel = left_dock
5955 .active_panel()
5956 .map(|panel| panel.persistent_name().to_string());
5957 let left_dock_zoom = left_dock
5958 .active_panel()
5959 .map(|panel| panel.is_zoomed(window, cx))
5960 .unwrap_or(false);
5961
5962 let right_dock = this.right_dock.read(cx);
5963 let right_visible = right_dock.is_open();
5964 let right_active_panel = right_dock
5965 .active_panel()
5966 .map(|panel| panel.persistent_name().to_string());
5967 let right_dock_zoom = right_dock
5968 .active_panel()
5969 .map(|panel| panel.is_zoomed(window, cx))
5970 .unwrap_or(false);
5971
5972 let bottom_dock = this.bottom_dock.read(cx);
5973 let bottom_visible = bottom_dock.is_open();
5974 let bottom_active_panel = bottom_dock
5975 .active_panel()
5976 .map(|panel| panel.persistent_name().to_string());
5977 let bottom_dock_zoom = bottom_dock
5978 .active_panel()
5979 .map(|panel| panel.is_zoomed(window, cx))
5980 .unwrap_or(false);
5981
5982 DockStructure {
5983 left: DockData {
5984 visible: left_visible,
5985 active_panel: left_active_panel,
5986 zoom: left_dock_zoom,
5987 },
5988 right: DockData {
5989 visible: right_visible,
5990 active_panel: right_active_panel,
5991 zoom: right_dock_zoom,
5992 },
5993 bottom: DockData {
5994 visible: bottom_visible,
5995 active_panel: bottom_active_panel,
5996 zoom: bottom_dock_zoom,
5997 },
5998 }
5999 }
6000
6001 match self.workspace_location(cx) {
6002 WorkspaceLocation::Location(location, paths) => {
6003 let breakpoints = self.project.update(cx, |project, cx| {
6004 project
6005 .breakpoint_store()
6006 .read(cx)
6007 .all_source_breakpoints(cx)
6008 });
6009 let user_toolchains = self
6010 .project
6011 .read(cx)
6012 .user_toolchains(cx)
6013 .unwrap_or_default();
6014
6015 let center_group = build_serialized_pane_group(&self.center.root, window, cx);
6016 let docks = build_serialized_docks(self, window, cx);
6017 let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
6018
6019 let serialized_workspace = SerializedWorkspace {
6020 id: database_id,
6021 location,
6022 paths,
6023 center_group,
6024 window_bounds,
6025 display: Default::default(),
6026 docks,
6027 centered_layout: self.centered_layout,
6028 session_id: self.session_id.clone(),
6029 breakpoints,
6030 window_id: Some(window.window_handle().window_id().as_u64()),
6031 user_toolchains,
6032 };
6033
6034 window.spawn(cx, async move |_| {
6035 persistence::DB.save_workspace(serialized_workspace).await;
6036 })
6037 }
6038 WorkspaceLocation::DetachFromSession => {
6039 let window_bounds = SerializedWindowBounds(window.window_bounds());
6040 let display = window.display(cx).and_then(|d| d.uuid().ok());
6041 // Save dock state for empty local workspaces
6042 let docks = build_serialized_docks(self, window, cx);
6043 window.spawn(cx, async move |_| {
6044 persistence::DB
6045 .set_window_open_status(
6046 database_id,
6047 window_bounds,
6048 display.unwrap_or_default(),
6049 )
6050 .await
6051 .log_err();
6052 persistence::DB
6053 .set_session_id(database_id, None)
6054 .await
6055 .log_err();
6056 persistence::write_default_dock_state(docks).await.log_err();
6057 })
6058 }
6059 WorkspaceLocation::None => {
6060 // Save dock state for empty non-local workspaces
6061 let docks = build_serialized_docks(self, window, cx);
6062 window.spawn(cx, async move |_| {
6063 persistence::write_default_dock_state(docks).await.log_err();
6064 })
6065 }
6066 }
6067 }
6068
6069 fn has_any_items_open(&self, cx: &App) -> bool {
6070 self.panes.iter().any(|pane| pane.read(cx).items_len() > 0)
6071 }
6072
6073 fn workspace_location(&self, cx: &App) -> WorkspaceLocation {
6074 let paths = PathList::new(&self.root_paths(cx));
6075 if let Some(connection) = self.project.read(cx).remote_connection_options(cx) {
6076 WorkspaceLocation::Location(SerializedWorkspaceLocation::Remote(connection), paths)
6077 } else if self.project.read(cx).is_local() {
6078 if !paths.is_empty() || self.has_any_items_open(cx) {
6079 WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths)
6080 } else {
6081 WorkspaceLocation::DetachFromSession
6082 }
6083 } else {
6084 WorkspaceLocation::None
6085 }
6086 }
6087
6088 fn update_history(&self, cx: &mut App) {
6089 let Some(id) = self.database_id() else {
6090 return;
6091 };
6092 if !self.project.read(cx).is_local() {
6093 return;
6094 }
6095 if let Some(manager) = HistoryManager::global(cx) {
6096 let paths = PathList::new(&self.root_paths(cx));
6097 manager.update(cx, |this, cx| {
6098 this.update_history(id, HistoryManagerEntry::new(id, &paths), cx);
6099 });
6100 }
6101 }
6102
6103 async fn serialize_items(
6104 this: &WeakEntity<Self>,
6105 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
6106 cx: &mut AsyncWindowContext,
6107 ) -> Result<()> {
6108 const CHUNK_SIZE: usize = 200;
6109
6110 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
6111
6112 while let Some(items_received) = serializable_items.next().await {
6113 let unique_items =
6114 items_received
6115 .into_iter()
6116 .fold(HashMap::default(), |mut acc, item| {
6117 acc.entry(item.item_id()).or_insert(item);
6118 acc
6119 });
6120
6121 // We use into_iter() here so that the references to the items are moved into
6122 // the tasks and not kept alive while we're sleeping.
6123 for (_, item) in unique_items.into_iter() {
6124 if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
6125 item.serialize(workspace, false, window, cx)
6126 }) {
6127 cx.background_spawn(async move { task.await.log_err() })
6128 .detach();
6129 }
6130 }
6131
6132 cx.background_executor()
6133 .timer(SERIALIZATION_THROTTLE_TIME)
6134 .await;
6135 }
6136
6137 Ok(())
6138 }
6139
6140 pub(crate) fn enqueue_item_serialization(
6141 &mut self,
6142 item: Box<dyn SerializableItemHandle>,
6143 ) -> Result<()> {
6144 self.serializable_items_tx
6145 .unbounded_send(item)
6146 .map_err(|err| anyhow!("failed to send serializable item over channel: {err}"))
6147 }
6148
6149 pub(crate) fn load_workspace(
6150 serialized_workspace: SerializedWorkspace,
6151 paths_to_open: Vec<Option<ProjectPath>>,
6152 window: &mut Window,
6153 cx: &mut Context<Workspace>,
6154 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
6155 cx.spawn_in(window, async move |workspace, cx| {
6156 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
6157
6158 let mut center_group = None;
6159 let mut center_items = None;
6160
6161 // Traverse the splits tree and add to things
6162 if let Some((group, active_pane, items)) = serialized_workspace
6163 .center_group
6164 .deserialize(&project, serialized_workspace.id, workspace.clone(), cx)
6165 .await
6166 {
6167 center_items = Some(items);
6168 center_group = Some((group, active_pane))
6169 }
6170
6171 let mut items_by_project_path = HashMap::default();
6172 let mut item_ids_by_kind = HashMap::default();
6173 let mut all_deserialized_items = Vec::default();
6174 cx.update(|_, cx| {
6175 for item in center_items.unwrap_or_default().into_iter().flatten() {
6176 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
6177 item_ids_by_kind
6178 .entry(serializable_item_handle.serialized_item_kind())
6179 .or_insert(Vec::new())
6180 .push(item.item_id().as_u64() as ItemId);
6181 }
6182
6183 if let Some(project_path) = item.project_path(cx) {
6184 items_by_project_path.insert(project_path, item.clone());
6185 }
6186 all_deserialized_items.push(item);
6187 }
6188 })?;
6189
6190 let opened_items = paths_to_open
6191 .into_iter()
6192 .map(|path_to_open| {
6193 path_to_open
6194 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
6195 })
6196 .collect::<Vec<_>>();
6197
6198 // Remove old panes from workspace panes list
6199 workspace.update_in(cx, |workspace, window, cx| {
6200 if let Some((center_group, active_pane)) = center_group {
6201 workspace.remove_panes(workspace.center.root.clone(), window, cx);
6202
6203 // Swap workspace center group
6204 workspace.center = PaneGroup::with_root(center_group);
6205 workspace.center.set_is_center(true);
6206 workspace.center.mark_positions(cx);
6207
6208 if let Some(active_pane) = active_pane {
6209 workspace.set_active_pane(&active_pane, window, cx);
6210 cx.focus_self(window);
6211 } else {
6212 workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
6213 }
6214 }
6215
6216 let docks = serialized_workspace.docks;
6217
6218 for (dock, serialized_dock) in [
6219 (&mut workspace.right_dock, docks.right),
6220 (&mut workspace.left_dock, docks.left),
6221 (&mut workspace.bottom_dock, docks.bottom),
6222 ]
6223 .iter_mut()
6224 {
6225 dock.update(cx, |dock, cx| {
6226 dock.serialized_dock = Some(serialized_dock.clone());
6227 dock.restore_state(window, cx);
6228 });
6229 }
6230
6231 cx.notify();
6232 })?;
6233
6234 let _ = project
6235 .update(cx, |project, cx| {
6236 project
6237 .breakpoint_store()
6238 .update(cx, |breakpoint_store, cx| {
6239 breakpoint_store
6240 .with_serialized_breakpoints(serialized_workspace.breakpoints, cx)
6241 })
6242 })
6243 .await;
6244
6245 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
6246 // after loading the items, we might have different items and in order to avoid
6247 // the database filling up, we delete items that haven't been loaded now.
6248 //
6249 // The items that have been loaded, have been saved after they've been added to the workspace.
6250 let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
6251 item_ids_by_kind
6252 .into_iter()
6253 .map(|(item_kind, loaded_items)| {
6254 SerializableItemRegistry::cleanup(
6255 item_kind,
6256 serialized_workspace.id,
6257 loaded_items,
6258 window,
6259 cx,
6260 )
6261 .log_err()
6262 })
6263 .collect::<Vec<_>>()
6264 })?;
6265
6266 futures::future::join_all(clean_up_tasks).await;
6267
6268 workspace
6269 .update_in(cx, |workspace, window, cx| {
6270 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
6271 workspace.serialize_workspace_internal(window, cx).detach();
6272
6273 // Ensure that we mark the window as edited if we did load dirty items
6274 workspace.update_window_edited(window, cx);
6275 })
6276 .ok();
6277
6278 Ok(opened_items)
6279 })
6280 }
6281
6282 fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
6283 self.add_workspace_actions_listeners(div, window, cx)
6284 .on_action(cx.listener(
6285 |_workspace, action_sequence: &settings::ActionSequence, window, cx| {
6286 for action in &action_sequence.0 {
6287 window.dispatch_action(action.boxed_clone(), cx);
6288 }
6289 },
6290 ))
6291 .on_action(cx.listener(Self::close_inactive_items_and_panes))
6292 .on_action(cx.listener(Self::close_all_items_and_panes))
6293 .on_action(cx.listener(Self::close_item_in_all_panes))
6294 .on_action(cx.listener(Self::save_all))
6295 .on_action(cx.listener(Self::send_keystrokes))
6296 .on_action(cx.listener(Self::add_folder_to_project))
6297 .on_action(cx.listener(Self::follow_next_collaborator))
6298 .on_action(cx.listener(Self::close_window))
6299 .on_action(cx.listener(Self::activate_pane_at_index))
6300 .on_action(cx.listener(Self::move_item_to_pane_at_index))
6301 .on_action(cx.listener(Self::move_focused_panel_to_next_position))
6302 .on_action(cx.listener(Self::toggle_edit_predictions_all_files))
6303 .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
6304 let pane = workspace.active_pane().clone();
6305 workspace.unfollow_in_pane(&pane, window, cx);
6306 }))
6307 .on_action(cx.listener(|workspace, action: &Save, window, cx| {
6308 workspace
6309 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
6310 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6311 }))
6312 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
6313 workspace
6314 .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
6315 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6316 }))
6317 .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
6318 workspace
6319 .save_active_item(SaveIntent::SaveAs, window, cx)
6320 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6321 }))
6322 .on_action(
6323 cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
6324 workspace.activate_previous_pane(window, cx)
6325 }),
6326 )
6327 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
6328 workspace.activate_next_pane(window, cx)
6329 }))
6330 .on_action(
6331 cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
6332 workspace.activate_next_window(cx)
6333 }),
6334 )
6335 .on_action(
6336 cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
6337 workspace.activate_previous_window(cx)
6338 }),
6339 )
6340 .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
6341 workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
6342 }))
6343 .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
6344 workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
6345 }))
6346 .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
6347 workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
6348 }))
6349 .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
6350 workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
6351 }))
6352 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
6353 workspace.activate_next_pane(window, cx)
6354 }))
6355 .on_action(cx.listener(
6356 |workspace, action: &MoveItemToPaneInDirection, window, cx| {
6357 workspace.move_item_to_pane_in_direction(action, window, cx)
6358 },
6359 ))
6360 .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
6361 workspace.swap_pane_in_direction(SplitDirection::Left, cx)
6362 }))
6363 .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
6364 workspace.swap_pane_in_direction(SplitDirection::Right, cx)
6365 }))
6366 .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
6367 workspace.swap_pane_in_direction(SplitDirection::Up, cx)
6368 }))
6369 .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
6370 workspace.swap_pane_in_direction(SplitDirection::Down, cx)
6371 }))
6372 .on_action(cx.listener(|workspace, _: &SwapPaneAdjacent, window, cx| {
6373 const DIRECTION_PRIORITY: [SplitDirection; 4] = [
6374 SplitDirection::Down,
6375 SplitDirection::Up,
6376 SplitDirection::Right,
6377 SplitDirection::Left,
6378 ];
6379 for dir in DIRECTION_PRIORITY {
6380 if workspace.find_pane_in_direction(dir, cx).is_some() {
6381 workspace.swap_pane_in_direction(dir, cx);
6382 workspace.activate_pane_in_direction(dir.opposite(), window, cx);
6383 break;
6384 }
6385 }
6386 }))
6387 .on_action(cx.listener(|workspace, _: &MovePaneLeft, _, cx| {
6388 workspace.move_pane_to_border(SplitDirection::Left, cx)
6389 }))
6390 .on_action(cx.listener(|workspace, _: &MovePaneRight, _, cx| {
6391 workspace.move_pane_to_border(SplitDirection::Right, cx)
6392 }))
6393 .on_action(cx.listener(|workspace, _: &MovePaneUp, _, cx| {
6394 workspace.move_pane_to_border(SplitDirection::Up, cx)
6395 }))
6396 .on_action(cx.listener(|workspace, _: &MovePaneDown, _, cx| {
6397 workspace.move_pane_to_border(SplitDirection::Down, cx)
6398 }))
6399 .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
6400 this.toggle_dock(DockPosition::Left, window, cx);
6401 }))
6402 .on_action(cx.listener(
6403 |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
6404 workspace.toggle_dock(DockPosition::Right, window, cx);
6405 },
6406 ))
6407 .on_action(cx.listener(
6408 |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
6409 workspace.toggle_dock(DockPosition::Bottom, window, cx);
6410 },
6411 ))
6412 .on_action(cx.listener(
6413 |workspace: &mut Workspace, _: &CloseActiveDock, window, cx| {
6414 if !workspace.close_active_dock(window, cx) {
6415 cx.propagate();
6416 }
6417 },
6418 ))
6419 .on_action(
6420 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
6421 workspace.close_all_docks(window, cx);
6422 }),
6423 )
6424 .on_action(cx.listener(Self::toggle_all_docks))
6425 .on_action(cx.listener(
6426 |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
6427 workspace.clear_all_notifications(cx);
6428 },
6429 ))
6430 .on_action(cx.listener(
6431 |workspace: &mut Workspace, _: &ClearNavigationHistory, window, cx| {
6432 workspace.clear_navigation_history(window, cx);
6433 },
6434 ))
6435 .on_action(cx.listener(
6436 |workspace: &mut Workspace, _: &SuppressNotification, _, cx| {
6437 if let Some((notification_id, _)) = workspace.notifications.pop() {
6438 workspace.suppress_notification(¬ification_id, cx);
6439 }
6440 },
6441 ))
6442 .on_action(cx.listener(
6443 |workspace: &mut Workspace, _: &ToggleWorktreeSecurity, window, cx| {
6444 workspace.show_worktree_trust_security_modal(true, window, cx);
6445 },
6446 ))
6447 .on_action(
6448 cx.listener(|_: &mut Workspace, _: &ClearTrustedWorktrees, _, cx| {
6449 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
6450 trusted_worktrees.update(cx, |trusted_worktrees, _| {
6451 trusted_worktrees.clear_trusted_paths()
6452 });
6453 let clear_task = persistence::DB.clear_trusted_worktrees();
6454 cx.spawn(async move |_, cx| {
6455 if clear_task.await.log_err().is_some() {
6456 cx.update(|cx| reload(cx));
6457 }
6458 })
6459 .detach();
6460 }
6461 }),
6462 )
6463 .on_action(cx.listener(
6464 |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
6465 workspace.reopen_closed_item(window, cx).detach();
6466 },
6467 ))
6468 .on_action(cx.listener(
6469 |workspace: &mut Workspace, _: &ResetActiveDockSize, window, cx| {
6470 for dock in workspace.all_docks() {
6471 if dock.focus_handle(cx).contains_focused(window, cx) {
6472 let Some(panel) = dock.read(cx).active_panel() else {
6473 return;
6474 };
6475
6476 // Set to `None`, then the size will fall back to the default.
6477 panel.clone().set_size(None, window, cx);
6478
6479 return;
6480 }
6481 }
6482 },
6483 ))
6484 .on_action(cx.listener(
6485 |workspace: &mut Workspace, _: &ResetOpenDocksSize, window, cx| {
6486 for dock in workspace.all_docks() {
6487 if let Some(panel) = dock.read(cx).visible_panel() {
6488 // Set to `None`, then the size will fall back to the default.
6489 panel.clone().set_size(None, window, cx);
6490 }
6491 }
6492 },
6493 ))
6494 .on_action(cx.listener(
6495 |workspace: &mut Workspace, act: &IncreaseActiveDockSize, window, cx| {
6496 adjust_active_dock_size_by_px(
6497 px_with_ui_font_fallback(act.px, cx),
6498 workspace,
6499 window,
6500 cx,
6501 );
6502 },
6503 ))
6504 .on_action(cx.listener(
6505 |workspace: &mut Workspace, act: &DecreaseActiveDockSize, window, cx| {
6506 adjust_active_dock_size_by_px(
6507 px_with_ui_font_fallback(act.px, cx) * -1.,
6508 workspace,
6509 window,
6510 cx,
6511 );
6512 },
6513 ))
6514 .on_action(cx.listener(
6515 |workspace: &mut Workspace, act: &IncreaseOpenDocksSize, window, cx| {
6516 adjust_open_docks_size_by_px(
6517 px_with_ui_font_fallback(act.px, cx),
6518 workspace,
6519 window,
6520 cx,
6521 );
6522 },
6523 ))
6524 .on_action(cx.listener(
6525 |workspace: &mut Workspace, act: &DecreaseOpenDocksSize, window, cx| {
6526 adjust_open_docks_size_by_px(
6527 px_with_ui_font_fallback(act.px, cx) * -1.,
6528 workspace,
6529 window,
6530 cx,
6531 );
6532 },
6533 ))
6534 .on_action(cx.listener(Workspace::toggle_centered_layout))
6535 .on_action(cx.listener(
6536 |workspace: &mut Workspace, _action: &pane::ActivateNextItem, window, cx| {
6537 if let Some(active_dock) = workspace.active_dock(window, cx) {
6538 let dock = active_dock.read(cx);
6539 if let Some(active_panel) = dock.active_panel() {
6540 if active_panel.pane(cx).is_none() {
6541 let mut recent_pane: Option<Entity<Pane>> = None;
6542 let mut recent_timestamp = 0;
6543 for pane_handle in workspace.panes() {
6544 let pane = pane_handle.read(cx);
6545 for entry in pane.activation_history() {
6546 if entry.timestamp > recent_timestamp {
6547 recent_timestamp = entry.timestamp;
6548 recent_pane = Some(pane_handle.clone());
6549 }
6550 }
6551 }
6552
6553 if let Some(pane) = recent_pane {
6554 pane.update(cx, |pane, cx| {
6555 let current_index = pane.active_item_index();
6556 let items_len = pane.items_len();
6557 if items_len > 0 {
6558 let next_index = if current_index + 1 < items_len {
6559 current_index + 1
6560 } else {
6561 0
6562 };
6563 pane.activate_item(
6564 next_index, false, false, window, cx,
6565 );
6566 }
6567 });
6568 return;
6569 }
6570 }
6571 }
6572 }
6573 cx.propagate();
6574 },
6575 ))
6576 .on_action(cx.listener(
6577 |workspace: &mut Workspace, _action: &pane::ActivatePreviousItem, window, cx| {
6578 if let Some(active_dock) = workspace.active_dock(window, cx) {
6579 let dock = active_dock.read(cx);
6580 if let Some(active_panel) = dock.active_panel() {
6581 if active_panel.pane(cx).is_none() {
6582 let mut recent_pane: Option<Entity<Pane>> = None;
6583 let mut recent_timestamp = 0;
6584 for pane_handle in workspace.panes() {
6585 let pane = pane_handle.read(cx);
6586 for entry in pane.activation_history() {
6587 if entry.timestamp > recent_timestamp {
6588 recent_timestamp = entry.timestamp;
6589 recent_pane = Some(pane_handle.clone());
6590 }
6591 }
6592 }
6593
6594 if let Some(pane) = recent_pane {
6595 pane.update(cx, |pane, cx| {
6596 let current_index = pane.active_item_index();
6597 let items_len = pane.items_len();
6598 if items_len > 0 {
6599 let prev_index = if current_index > 0 {
6600 current_index - 1
6601 } else {
6602 items_len.saturating_sub(1)
6603 };
6604 pane.activate_item(
6605 prev_index, false, false, window, cx,
6606 );
6607 }
6608 });
6609 return;
6610 }
6611 }
6612 }
6613 }
6614 cx.propagate();
6615 },
6616 ))
6617 .on_action(cx.listener(
6618 |workspace: &mut Workspace, action: &pane::CloseActiveItem, window, cx| {
6619 if let Some(active_dock) = workspace.active_dock(window, cx) {
6620 let dock = active_dock.read(cx);
6621 if let Some(active_panel) = dock.active_panel() {
6622 if active_panel.pane(cx).is_none() {
6623 let active_pane = workspace.active_pane().clone();
6624 active_pane.update(cx, |pane, cx| {
6625 pane.close_active_item(action, window, cx)
6626 .detach_and_log_err(cx);
6627 });
6628 return;
6629 }
6630 }
6631 }
6632 cx.propagate();
6633 },
6634 ))
6635 .on_action(
6636 cx.listener(|workspace, _: &ToggleReadOnlyFile, window, cx| {
6637 let pane = workspace.active_pane().clone();
6638 if let Some(item) = pane.read(cx).active_item() {
6639 item.toggle_read_only(window, cx);
6640 }
6641 }),
6642 )
6643 .on_action(cx.listener(Workspace::cancel))
6644 }
6645
6646 #[cfg(any(test, feature = "test-support"))]
6647 pub fn set_random_database_id(&mut self) {
6648 self.database_id = Some(WorkspaceId(Uuid::new_v4().as_u64_pair().0 as i64));
6649 }
6650
6651 #[cfg(any(test, feature = "test-support"))]
6652 pub(crate) fn test_new(
6653 project: Entity<Project>,
6654 window: &mut Window,
6655 cx: &mut Context<Self>,
6656 ) -> Self {
6657 use node_runtime::NodeRuntime;
6658 use session::Session;
6659
6660 let client = project.read(cx).client();
6661 let user_store = project.read(cx).user_store();
6662 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
6663 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
6664 window.activate_window();
6665 let app_state = Arc::new(AppState {
6666 languages: project.read(cx).languages().clone(),
6667 workspace_store,
6668 client,
6669 user_store,
6670 fs: project.read(cx).fs().clone(),
6671 build_window_options: |_, _| Default::default(),
6672 node_runtime: NodeRuntime::unavailable(),
6673 session,
6674 });
6675 let workspace = Self::new(Default::default(), project, app_state, window, cx);
6676 workspace
6677 .active_pane
6678 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
6679 workspace
6680 }
6681
6682 pub fn register_action<A: Action>(
6683 &mut self,
6684 callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
6685 ) -> &mut Self {
6686 let callback = Arc::new(callback);
6687
6688 self.workspace_actions.push(Box::new(move |div, _, _, cx| {
6689 let callback = callback.clone();
6690 div.on_action(cx.listener(move |workspace, event, window, cx| {
6691 (callback)(workspace, event, window, cx)
6692 }))
6693 }));
6694 self
6695 }
6696 pub fn register_action_renderer(
6697 &mut self,
6698 callback: impl Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div + 'static,
6699 ) -> &mut Self {
6700 self.workspace_actions.push(Box::new(callback));
6701 self
6702 }
6703
6704 fn add_workspace_actions_listeners(
6705 &self,
6706 mut div: Div,
6707 window: &mut Window,
6708 cx: &mut Context<Self>,
6709 ) -> Div {
6710 for action in self.workspace_actions.iter() {
6711 div = (action)(div, self, window, cx)
6712 }
6713 div
6714 }
6715
6716 pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
6717 self.modal_layer.read(cx).has_active_modal()
6718 }
6719
6720 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
6721 self.modal_layer.read(cx).active_modal()
6722 }
6723
6724 /// Toggles a modal of type `V`. If a modal of the same type is currently active,
6725 /// it will be hidden. If a different modal is active, it will be replaced with the new one.
6726 /// If no modal is active, the new modal will be shown.
6727 ///
6728 /// If closing the current modal fails (e.g., due to `on_before_dismiss` returning
6729 /// `DismissDecision::Dismiss(false)` or `DismissDecision::Pending`), the new modal
6730 /// will not be shown.
6731 pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
6732 where
6733 B: FnOnce(&mut Window, &mut Context<V>) -> V,
6734 {
6735 self.modal_layer.update(cx, |modal_layer, cx| {
6736 modal_layer.toggle_modal(window, cx, build)
6737 })
6738 }
6739
6740 pub fn hide_modal(&mut self, window: &mut Window, cx: &mut App) -> bool {
6741 self.modal_layer
6742 .update(cx, |modal_layer, cx| modal_layer.hide_modal(window, cx))
6743 }
6744
6745 pub fn toggle_status_toast<V: ToastView>(&mut self, entity: Entity<V>, cx: &mut App) {
6746 self.toast_layer
6747 .update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity))
6748 }
6749
6750 pub fn toggle_centered_layout(
6751 &mut self,
6752 _: &ToggleCenteredLayout,
6753 _: &mut Window,
6754 cx: &mut Context<Self>,
6755 ) {
6756 self.centered_layout = !self.centered_layout;
6757 if let Some(database_id) = self.database_id() {
6758 cx.background_spawn(DB.set_centered_layout(database_id, self.centered_layout))
6759 .detach_and_log_err(cx);
6760 }
6761 cx.notify();
6762 }
6763
6764 fn adjust_padding(padding: Option<f32>) -> f32 {
6765 padding
6766 .unwrap_or(CenteredPaddingSettings::default().0)
6767 .clamp(
6768 CenteredPaddingSettings::MIN_PADDING,
6769 CenteredPaddingSettings::MAX_PADDING,
6770 )
6771 }
6772
6773 fn render_dock(
6774 &self,
6775 position: DockPosition,
6776 dock: &Entity<Dock>,
6777 window: &mut Window,
6778 cx: &mut App,
6779 ) -> Option<Div> {
6780 if self.zoomed_position == Some(position) {
6781 return None;
6782 }
6783
6784 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
6785 let pane = panel.pane(cx)?;
6786 let follower_states = &self.follower_states;
6787 leader_border_for_pane(follower_states, &pane, window, cx)
6788 });
6789
6790 Some(
6791 div()
6792 .flex()
6793 .flex_none()
6794 .overflow_hidden()
6795 .child(dock.clone())
6796 .children(leader_border),
6797 )
6798 }
6799
6800 pub fn for_window(window: &Window, cx: &App) -> Option<Entity<Workspace>> {
6801 window
6802 .root::<MultiWorkspace>()
6803 .flatten()
6804 .map(|multi_workspace| multi_workspace.read(cx).workspace().clone())
6805 }
6806
6807 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
6808 self.zoomed.as_ref()
6809 }
6810
6811 pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
6812 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
6813 return;
6814 };
6815 let windows = cx.windows();
6816 let next_window =
6817 SystemWindowTabController::get_next_tab_group_window(cx, current_window_id).or_else(
6818 || {
6819 windows
6820 .iter()
6821 .cycle()
6822 .skip_while(|window| window.window_id() != current_window_id)
6823 .nth(1)
6824 },
6825 );
6826
6827 if let Some(window) = next_window {
6828 window
6829 .update(cx, |_, window, _| window.activate_window())
6830 .ok();
6831 }
6832 }
6833
6834 pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
6835 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
6836 return;
6837 };
6838 let windows = cx.windows();
6839 let prev_window =
6840 SystemWindowTabController::get_prev_tab_group_window(cx, current_window_id).or_else(
6841 || {
6842 windows
6843 .iter()
6844 .rev()
6845 .cycle()
6846 .skip_while(|window| window.window_id() != current_window_id)
6847 .nth(1)
6848 },
6849 );
6850
6851 if let Some(window) = prev_window {
6852 window
6853 .update(cx, |_, window, _| window.activate_window())
6854 .ok();
6855 }
6856 }
6857
6858 pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
6859 if cx.stop_active_drag(window) {
6860 } else if let Some((notification_id, _)) = self.notifications.pop() {
6861 dismiss_app_notification(¬ification_id, cx);
6862 } else {
6863 cx.propagate();
6864 }
6865 }
6866
6867 fn adjust_dock_size_by_px(
6868 &mut self,
6869 panel_size: Pixels,
6870 dock_pos: DockPosition,
6871 px: Pixels,
6872 window: &mut Window,
6873 cx: &mut Context<Self>,
6874 ) {
6875 match dock_pos {
6876 DockPosition::Left => self.resize_left_dock(panel_size + px, window, cx),
6877 DockPosition::Right => self.resize_right_dock(panel_size + px, window, cx),
6878 DockPosition::Bottom => self.resize_bottom_dock(panel_size + px, window, cx),
6879 }
6880 }
6881
6882 fn resize_left_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
6883 let size = new_size.min(self.bounds.right() - RESIZE_HANDLE_SIZE);
6884
6885 self.left_dock.update(cx, |left_dock, cx| {
6886 if WorkspaceSettings::get_global(cx)
6887 .resize_all_panels_in_dock
6888 .contains(&DockPosition::Left)
6889 {
6890 left_dock.resize_all_panels(Some(size), window, cx);
6891 } else {
6892 left_dock.resize_active_panel(Some(size), window, cx);
6893 }
6894 });
6895 }
6896
6897 fn resize_right_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
6898 let mut size = new_size.max(self.bounds.left() - RESIZE_HANDLE_SIZE);
6899 self.left_dock.read_with(cx, |left_dock, cx| {
6900 let left_dock_size = left_dock
6901 .active_panel_size(window, cx)
6902 .unwrap_or(Pixels::ZERO);
6903 if left_dock_size + size > self.bounds.right() {
6904 size = self.bounds.right() - left_dock_size
6905 }
6906 });
6907 self.right_dock.update(cx, |right_dock, cx| {
6908 if WorkspaceSettings::get_global(cx)
6909 .resize_all_panels_in_dock
6910 .contains(&DockPosition::Right)
6911 {
6912 right_dock.resize_all_panels(Some(size), window, cx);
6913 } else {
6914 right_dock.resize_active_panel(Some(size), window, cx);
6915 }
6916 });
6917 }
6918
6919 fn resize_bottom_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
6920 let size = new_size.min(self.bounds.bottom() - RESIZE_HANDLE_SIZE - self.bounds.top());
6921 self.bottom_dock.update(cx, |bottom_dock, cx| {
6922 if WorkspaceSettings::get_global(cx)
6923 .resize_all_panels_in_dock
6924 .contains(&DockPosition::Bottom)
6925 {
6926 bottom_dock.resize_all_panels(Some(size), window, cx);
6927 } else {
6928 bottom_dock.resize_active_panel(Some(size), window, cx);
6929 }
6930 });
6931 }
6932
6933 fn toggle_edit_predictions_all_files(
6934 &mut self,
6935 _: &ToggleEditPrediction,
6936 _window: &mut Window,
6937 cx: &mut Context<Self>,
6938 ) {
6939 let fs = self.project().read(cx).fs().clone();
6940 let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx);
6941 update_settings_file(fs, cx, move |file, _| {
6942 file.project.all_languages.defaults.show_edit_predictions = Some(!show_edit_predictions)
6943 });
6944 }
6945
6946 pub fn show_worktree_trust_security_modal(
6947 &mut self,
6948 toggle: bool,
6949 window: &mut Window,
6950 cx: &mut Context<Self>,
6951 ) {
6952 if let Some(security_modal) = self.active_modal::<SecurityModal>(cx) {
6953 if toggle {
6954 security_modal.update(cx, |security_modal, cx| {
6955 security_modal.dismiss(cx);
6956 })
6957 } else {
6958 security_modal.update(cx, |security_modal, cx| {
6959 security_modal.refresh_restricted_paths(cx);
6960 });
6961 }
6962 } else {
6963 let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
6964 .map(|trusted_worktrees| {
6965 trusted_worktrees
6966 .read(cx)
6967 .has_restricted_worktrees(&self.project().read(cx).worktree_store(), cx)
6968 })
6969 .unwrap_or(false);
6970 if has_restricted_worktrees {
6971 let project = self.project().read(cx);
6972 let remote_host = project
6973 .remote_connection_options(cx)
6974 .map(RemoteHostLocation::from);
6975 let worktree_store = project.worktree_store().downgrade();
6976 self.toggle_modal(window, cx, |_, cx| {
6977 SecurityModal::new(worktree_store, remote_host, cx)
6978 });
6979 }
6980 }
6981 }
6982}
6983
6984fn leader_border_for_pane(
6985 follower_states: &HashMap<CollaboratorId, FollowerState>,
6986 pane: &Entity<Pane>,
6987 _: &Window,
6988 cx: &App,
6989) -> Option<Div> {
6990 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
6991 if state.pane() == pane {
6992 Some((*leader_id, state))
6993 } else {
6994 None
6995 }
6996 })?;
6997
6998 let mut leader_color = match leader_id {
6999 CollaboratorId::PeerId(leader_peer_id) => {
7000 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
7001 let leader = room.remote_participant_for_peer_id(leader_peer_id)?;
7002
7003 cx.theme()
7004 .players()
7005 .color_for_participant(leader.participant_index.0)
7006 .cursor
7007 }
7008 CollaboratorId::Agent => cx.theme().players().agent().cursor,
7009 };
7010 leader_color.fade_out(0.3);
7011 Some(
7012 div()
7013 .absolute()
7014 .size_full()
7015 .left_0()
7016 .top_0()
7017 .border_2()
7018 .border_color(leader_color),
7019 )
7020}
7021
7022fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
7023 ZED_WINDOW_POSITION
7024 .zip(*ZED_WINDOW_SIZE)
7025 .map(|(position, size)| Bounds {
7026 origin: position,
7027 size,
7028 })
7029}
7030
7031fn open_items(
7032 serialized_workspace: Option<SerializedWorkspace>,
7033 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
7034 window: &mut Window,
7035 cx: &mut Context<Workspace>,
7036) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
7037 let restored_items = serialized_workspace.map(|serialized_workspace| {
7038 Workspace::load_workspace(
7039 serialized_workspace,
7040 project_paths_to_open
7041 .iter()
7042 .map(|(_, project_path)| project_path)
7043 .cloned()
7044 .collect(),
7045 window,
7046 cx,
7047 )
7048 });
7049
7050 cx.spawn_in(window, async move |workspace, cx| {
7051 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
7052
7053 if let Some(restored_items) = restored_items {
7054 let restored_items = restored_items.await?;
7055
7056 let restored_project_paths = restored_items
7057 .iter()
7058 .filter_map(|item| {
7059 cx.update(|_, cx| item.as_ref()?.project_path(cx))
7060 .ok()
7061 .flatten()
7062 })
7063 .collect::<HashSet<_>>();
7064
7065 for restored_item in restored_items {
7066 opened_items.push(restored_item.map(Ok));
7067 }
7068
7069 project_paths_to_open
7070 .iter_mut()
7071 .for_each(|(_, project_path)| {
7072 if let Some(project_path_to_open) = project_path
7073 && restored_project_paths.contains(project_path_to_open)
7074 {
7075 *project_path = None;
7076 }
7077 });
7078 } else {
7079 for _ in 0..project_paths_to_open.len() {
7080 opened_items.push(None);
7081 }
7082 }
7083 assert!(opened_items.len() == project_paths_to_open.len());
7084
7085 let tasks =
7086 project_paths_to_open
7087 .into_iter()
7088 .enumerate()
7089 .map(|(ix, (abs_path, project_path))| {
7090 let workspace = workspace.clone();
7091 cx.spawn(async move |cx| {
7092 let file_project_path = project_path?;
7093 let abs_path_task = workspace.update(cx, |workspace, cx| {
7094 workspace.project().update(cx, |project, cx| {
7095 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
7096 })
7097 });
7098
7099 // We only want to open file paths here. If one of the items
7100 // here is a directory, it was already opened further above
7101 // with a `find_or_create_worktree`.
7102 if let Ok(task) = abs_path_task
7103 && task.await.is_none_or(|p| p.is_file())
7104 {
7105 return Some((
7106 ix,
7107 workspace
7108 .update_in(cx, |workspace, window, cx| {
7109 workspace.open_path(
7110 file_project_path,
7111 None,
7112 true,
7113 window,
7114 cx,
7115 )
7116 })
7117 .log_err()?
7118 .await,
7119 ));
7120 }
7121 None
7122 })
7123 });
7124
7125 let tasks = tasks.collect::<Vec<_>>();
7126
7127 let tasks = futures::future::join_all(tasks);
7128 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
7129 opened_items[ix] = Some(path_open_result);
7130 }
7131
7132 Ok(opened_items)
7133 })
7134}
7135
7136enum ActivateInDirectionTarget {
7137 Pane(Entity<Pane>),
7138 Dock(Entity<Dock>),
7139}
7140
7141fn notify_if_database_failed(window: WindowHandle<MultiWorkspace>, cx: &mut AsyncApp) {
7142 window
7143 .update(cx, |multi_workspace, _, cx| {
7144 let workspace = multi_workspace.workspace().clone();
7145 workspace.update(cx, |workspace, cx| {
7146 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
7147 struct DatabaseFailedNotification;
7148
7149 workspace.show_notification(
7150 NotificationId::unique::<DatabaseFailedNotification>(),
7151 cx,
7152 |cx| {
7153 cx.new(|cx| {
7154 MessageNotification::new("Failed to load the database file.", cx)
7155 .primary_message("File an Issue")
7156 .primary_icon(IconName::Plus)
7157 .primary_on_click(|window, cx| {
7158 window.dispatch_action(Box::new(FileBugReport), cx)
7159 })
7160 })
7161 },
7162 );
7163 }
7164 });
7165 })
7166 .log_err();
7167}
7168
7169fn px_with_ui_font_fallback(val: u32, cx: &Context<Workspace>) -> Pixels {
7170 if val == 0 {
7171 ThemeSettings::get_global(cx).ui_font_size(cx)
7172 } else {
7173 px(val as f32)
7174 }
7175}
7176
7177fn adjust_active_dock_size_by_px(
7178 px: Pixels,
7179 workspace: &mut Workspace,
7180 window: &mut Window,
7181 cx: &mut Context<Workspace>,
7182) {
7183 let Some(active_dock) = workspace
7184 .all_docks()
7185 .into_iter()
7186 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx))
7187 else {
7188 return;
7189 };
7190 let dock = active_dock.read(cx);
7191 let Some(panel_size) = dock.active_panel_size(window, cx) else {
7192 return;
7193 };
7194 let dock_pos = dock.position();
7195 workspace.adjust_dock_size_by_px(panel_size, dock_pos, px, window, cx);
7196}
7197
7198fn adjust_open_docks_size_by_px(
7199 px: Pixels,
7200 workspace: &mut Workspace,
7201 window: &mut Window,
7202 cx: &mut Context<Workspace>,
7203) {
7204 let docks = workspace
7205 .all_docks()
7206 .into_iter()
7207 .filter_map(|dock| {
7208 if dock.read(cx).is_open() {
7209 let dock = dock.read(cx);
7210 let panel_size = dock.active_panel_size(window, cx)?;
7211 let dock_pos = dock.position();
7212 Some((panel_size, dock_pos, px))
7213 } else {
7214 None
7215 }
7216 })
7217 .collect::<Vec<_>>();
7218
7219 docks
7220 .into_iter()
7221 .for_each(|(panel_size, dock_pos, offset)| {
7222 workspace.adjust_dock_size_by_px(panel_size, dock_pos, offset, window, cx);
7223 });
7224}
7225
7226impl Focusable for Workspace {
7227 fn focus_handle(&self, cx: &App) -> FocusHandle {
7228 self.active_pane.focus_handle(cx)
7229 }
7230}
7231
7232#[derive(Clone)]
7233struct DraggedDock(DockPosition);
7234
7235impl Render for DraggedDock {
7236 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
7237 gpui::Empty
7238 }
7239}
7240
7241impl Render for Workspace {
7242 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
7243 static FIRST_PAINT: AtomicBool = AtomicBool::new(true);
7244 if FIRST_PAINT.swap(false, std::sync::atomic::Ordering::Relaxed) {
7245 log::info!("Rendered first frame");
7246 }
7247 let mut context = KeyContext::new_with_defaults();
7248 context.add("Workspace");
7249 context.set("keyboard_layout", cx.keyboard_layout().name().to_string());
7250 if let Some(status) = self
7251 .debugger_provider
7252 .as_ref()
7253 .and_then(|provider| provider.active_thread_state(cx))
7254 {
7255 match status {
7256 ThreadStatus::Running | ThreadStatus::Stepping => {
7257 context.add("debugger_running");
7258 }
7259 ThreadStatus::Stopped => context.add("debugger_stopped"),
7260 ThreadStatus::Exited | ThreadStatus::Ended => {}
7261 }
7262 }
7263
7264 if self.left_dock.read(cx).is_open() {
7265 if let Some(active_panel) = self.left_dock.read(cx).active_panel() {
7266 context.set("left_dock", active_panel.panel_key());
7267 }
7268 }
7269
7270 if self.right_dock.read(cx).is_open() {
7271 if let Some(active_panel) = self.right_dock.read(cx).active_panel() {
7272 context.set("right_dock", active_panel.panel_key());
7273 }
7274 }
7275
7276 if self.bottom_dock.read(cx).is_open() {
7277 if let Some(active_panel) = self.bottom_dock.read(cx).active_panel() {
7278 context.set("bottom_dock", active_panel.panel_key());
7279 }
7280 }
7281
7282 let centered_layout = self.centered_layout
7283 && self.center.panes().len() == 1
7284 && self.active_item(cx).is_some();
7285 let render_padding = |size| {
7286 (size > 0.0).then(|| {
7287 div()
7288 .h_full()
7289 .w(relative(size))
7290 .bg(cx.theme().colors().editor_background)
7291 .border_color(cx.theme().colors().pane_group_border)
7292 })
7293 };
7294 let paddings = if centered_layout {
7295 let settings = WorkspaceSettings::get_global(cx).centered_layout;
7296 (
7297 render_padding(Self::adjust_padding(
7298 settings.left_padding.map(|padding| padding.0),
7299 )),
7300 render_padding(Self::adjust_padding(
7301 settings.right_padding.map(|padding| padding.0),
7302 )),
7303 )
7304 } else {
7305 (None, None)
7306 };
7307 let ui_font = theme::setup_ui_font(window, cx);
7308
7309 let theme = cx.theme().clone();
7310 let colors = theme.colors();
7311 let notification_entities = self
7312 .notifications
7313 .iter()
7314 .map(|(_, notification)| notification.entity_id())
7315 .collect::<Vec<_>>();
7316 let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout;
7317
7318 self.actions(div(), window, cx)
7319 .key_context(context)
7320 .relative()
7321 .size_full()
7322 .flex()
7323 .flex_col()
7324 .font(ui_font)
7325 .gap_0()
7326 .justify_start()
7327 .items_start()
7328 .text_color(colors.text)
7329 .overflow_hidden()
7330 .children(self.titlebar_item.clone())
7331 .on_modifiers_changed(move |_, _, cx| {
7332 for &id in ¬ification_entities {
7333 cx.notify(id);
7334 }
7335 })
7336 .child(
7337 div()
7338 .size_full()
7339 .relative()
7340 .flex_1()
7341 .flex()
7342 .flex_col()
7343 .child(
7344 div()
7345 .id("workspace")
7346 .bg(colors.background)
7347 .relative()
7348 .flex_1()
7349 .w_full()
7350 .flex()
7351 .flex_col()
7352 .overflow_hidden()
7353 .border_t_1()
7354 .border_b_1()
7355 .border_color(colors.border)
7356 .child({
7357 let this = cx.entity();
7358 canvas(
7359 move |bounds, window, cx| {
7360 this.update(cx, |this, cx| {
7361 let bounds_changed = this.bounds != bounds;
7362 this.bounds = bounds;
7363
7364 if bounds_changed {
7365 this.left_dock.update(cx, |dock, cx| {
7366 dock.clamp_panel_size(
7367 bounds.size.width,
7368 window,
7369 cx,
7370 )
7371 });
7372
7373 this.right_dock.update(cx, |dock, cx| {
7374 dock.clamp_panel_size(
7375 bounds.size.width,
7376 window,
7377 cx,
7378 )
7379 });
7380
7381 this.bottom_dock.update(cx, |dock, cx| {
7382 dock.clamp_panel_size(
7383 bounds.size.height,
7384 window,
7385 cx,
7386 )
7387 });
7388 }
7389 })
7390 },
7391 |_, _, _, _| {},
7392 )
7393 .absolute()
7394 .size_full()
7395 })
7396 .when(self.zoomed.is_none(), |this| {
7397 this.on_drag_move(cx.listener(
7398 move |workspace,
7399 e: &DragMoveEvent<DraggedDock>,
7400 window,
7401 cx| {
7402 if workspace.previous_dock_drag_coordinates
7403 != Some(e.event.position)
7404 {
7405 workspace.previous_dock_drag_coordinates =
7406 Some(e.event.position);
7407 match e.drag(cx).0 {
7408 DockPosition::Left => {
7409 workspace.resize_left_dock(
7410 e.event.position.x
7411 - workspace.bounds.left(),
7412 window,
7413 cx,
7414 );
7415 }
7416 DockPosition::Right => {
7417 workspace.resize_right_dock(
7418 workspace.bounds.right()
7419 - e.event.position.x,
7420 window,
7421 cx,
7422 );
7423 }
7424 DockPosition::Bottom => {
7425 workspace.resize_bottom_dock(
7426 workspace.bounds.bottom()
7427 - e.event.position.y,
7428 window,
7429 cx,
7430 );
7431 }
7432 };
7433 workspace.serialize_workspace(window, cx);
7434 }
7435 },
7436 ))
7437
7438 })
7439 .child({
7440 match bottom_dock_layout {
7441 BottomDockLayout::Full => div()
7442 .flex()
7443 .flex_col()
7444 .h_full()
7445 .child(
7446 div()
7447 .flex()
7448 .flex_row()
7449 .flex_1()
7450 .overflow_hidden()
7451 .children(self.render_dock(
7452 DockPosition::Left,
7453 &self.left_dock,
7454 window,
7455 cx,
7456 ))
7457
7458 .child(
7459 div()
7460 .flex()
7461 .flex_col()
7462 .flex_1()
7463 .overflow_hidden()
7464 .child(
7465 h_flex()
7466 .flex_1()
7467 .when_some(
7468 paddings.0,
7469 |this, p| {
7470 this.child(
7471 p.border_r_1(),
7472 )
7473 },
7474 )
7475 .child(self.center.render(
7476 self.zoomed.as_ref(),
7477 &PaneRenderContext {
7478 follower_states:
7479 &self.follower_states,
7480 active_call: self.active_call(),
7481 active_pane: &self.active_pane,
7482 app_state: &self.app_state,
7483 project: &self.project,
7484 workspace: &self.weak_self,
7485 },
7486 window,
7487 cx,
7488 ))
7489 .when_some(
7490 paddings.1,
7491 |this, p| {
7492 this.child(
7493 p.border_l_1(),
7494 )
7495 },
7496 ),
7497 ),
7498 )
7499
7500 .children(self.render_dock(
7501 DockPosition::Right,
7502 &self.right_dock,
7503 window,
7504 cx,
7505 )),
7506 )
7507 .child(div().w_full().children(self.render_dock(
7508 DockPosition::Bottom,
7509 &self.bottom_dock,
7510 window,
7511 cx
7512 ))),
7513
7514 BottomDockLayout::LeftAligned => div()
7515 .flex()
7516 .flex_row()
7517 .h_full()
7518 .child(
7519 div()
7520 .flex()
7521 .flex_col()
7522 .flex_1()
7523 .h_full()
7524 .child(
7525 div()
7526 .flex()
7527 .flex_row()
7528 .flex_1()
7529 .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx))
7530
7531 .child(
7532 div()
7533 .flex()
7534 .flex_col()
7535 .flex_1()
7536 .overflow_hidden()
7537 .child(
7538 h_flex()
7539 .flex_1()
7540 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
7541 .child(self.center.render(
7542 self.zoomed.as_ref(),
7543 &PaneRenderContext {
7544 follower_states:
7545 &self.follower_states,
7546 active_call: self.active_call(),
7547 active_pane: &self.active_pane,
7548 app_state: &self.app_state,
7549 project: &self.project,
7550 workspace: &self.weak_self,
7551 },
7552 window,
7553 cx,
7554 ))
7555 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
7556 )
7557 )
7558
7559 )
7560 .child(
7561 div()
7562 .w_full()
7563 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
7564 ),
7565 )
7566 .children(self.render_dock(
7567 DockPosition::Right,
7568 &self.right_dock,
7569 window,
7570 cx,
7571 )),
7572
7573 BottomDockLayout::RightAligned => div()
7574 .flex()
7575 .flex_row()
7576 .h_full()
7577 .children(self.render_dock(
7578 DockPosition::Left,
7579 &self.left_dock,
7580 window,
7581 cx,
7582 ))
7583
7584 .child(
7585 div()
7586 .flex()
7587 .flex_col()
7588 .flex_1()
7589 .h_full()
7590 .child(
7591 div()
7592 .flex()
7593 .flex_row()
7594 .flex_1()
7595 .child(
7596 div()
7597 .flex()
7598 .flex_col()
7599 .flex_1()
7600 .overflow_hidden()
7601 .child(
7602 h_flex()
7603 .flex_1()
7604 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
7605 .child(self.center.render(
7606 self.zoomed.as_ref(),
7607 &PaneRenderContext {
7608 follower_states:
7609 &self.follower_states,
7610 active_call: self.active_call(),
7611 active_pane: &self.active_pane,
7612 app_state: &self.app_state,
7613 project: &self.project,
7614 workspace: &self.weak_self,
7615 },
7616 window,
7617 cx,
7618 ))
7619 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
7620 )
7621 )
7622
7623 .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx))
7624 )
7625 .child(
7626 div()
7627 .w_full()
7628 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
7629 ),
7630 ),
7631
7632 BottomDockLayout::Contained => div()
7633 .flex()
7634 .flex_row()
7635 .h_full()
7636 .children(self.render_dock(
7637 DockPosition::Left,
7638 &self.left_dock,
7639 window,
7640 cx,
7641 ))
7642
7643 .child(
7644 div()
7645 .flex()
7646 .flex_col()
7647 .flex_1()
7648 .overflow_hidden()
7649 .child(
7650 h_flex()
7651 .flex_1()
7652 .when_some(paddings.0, |this, p| {
7653 this.child(p.border_r_1())
7654 })
7655 .child(self.center.render(
7656 self.zoomed.as_ref(),
7657 &PaneRenderContext {
7658 follower_states:
7659 &self.follower_states,
7660 active_call: self.active_call(),
7661 active_pane: &self.active_pane,
7662 app_state: &self.app_state,
7663 project: &self.project,
7664 workspace: &self.weak_self,
7665 },
7666 window,
7667 cx,
7668 ))
7669 .when_some(paddings.1, |this, p| {
7670 this.child(p.border_l_1())
7671 }),
7672 )
7673 .children(self.render_dock(
7674 DockPosition::Bottom,
7675 &self.bottom_dock,
7676 window,
7677 cx,
7678 )),
7679 )
7680
7681 .children(self.render_dock(
7682 DockPosition::Right,
7683 &self.right_dock,
7684 window,
7685 cx,
7686 )),
7687 }
7688 })
7689 .children(self.zoomed.as_ref().and_then(|view| {
7690 let zoomed_view = view.upgrade()?;
7691 let div = div()
7692 .occlude()
7693 .absolute()
7694 .overflow_hidden()
7695 .border_color(colors.border)
7696 .bg(colors.background)
7697 .child(zoomed_view)
7698 .inset_0()
7699 .shadow_lg();
7700
7701 if !WorkspaceSettings::get_global(cx).zoomed_padding {
7702 return Some(div);
7703 }
7704
7705 Some(match self.zoomed_position {
7706 Some(DockPosition::Left) => div.right_2().border_r_1(),
7707 Some(DockPosition::Right) => div.left_2().border_l_1(),
7708 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
7709 None => {
7710 div.top_2().bottom_2().left_2().right_2().border_1()
7711 }
7712 })
7713 }))
7714 .children(self.render_notifications(window, cx)),
7715 )
7716 .when(self.status_bar_visible(cx), |parent| {
7717 parent.child(self.status_bar.clone())
7718 })
7719 .child(self.modal_layer.clone())
7720 .child(self.toast_layer.clone()),
7721 )
7722 }
7723}
7724
7725impl WorkspaceStore {
7726 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
7727 Self {
7728 workspaces: Default::default(),
7729 _subscriptions: vec![
7730 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
7731 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
7732 ],
7733 client,
7734 }
7735 }
7736
7737 pub fn update_followers(
7738 &self,
7739 project_id: Option<u64>,
7740 update: proto::update_followers::Variant,
7741 cx: &App,
7742 ) -> Option<()> {
7743 let active_call = ActiveCall::try_global(cx)?;
7744 let room_id = active_call.read(cx).room()?.read(cx).id();
7745 self.client
7746 .send(proto::UpdateFollowers {
7747 room_id,
7748 project_id,
7749 variant: Some(update),
7750 })
7751 .log_err()
7752 }
7753
7754 pub async fn handle_follow(
7755 this: Entity<Self>,
7756 envelope: TypedEnvelope<proto::Follow>,
7757 mut cx: AsyncApp,
7758 ) -> Result<proto::FollowResponse> {
7759 this.update(&mut cx, |this, cx| {
7760 let follower = Follower {
7761 project_id: envelope.payload.project_id,
7762 peer_id: envelope.original_sender_id()?,
7763 };
7764
7765 let mut response = proto::FollowResponse::default();
7766
7767 this.workspaces.retain(|(window_handle, weak_workspace)| {
7768 let Some(workspace) = weak_workspace.upgrade() else {
7769 return false;
7770 };
7771 window_handle
7772 .update(cx, |_, window, cx| {
7773 workspace.update(cx, |workspace, cx| {
7774 let handler_response =
7775 workspace.handle_follow(follower.project_id, window, cx);
7776 if let Some(active_view) = handler_response.active_view
7777 && workspace.project.read(cx).remote_id() == follower.project_id
7778 {
7779 response.active_view = Some(active_view)
7780 }
7781 });
7782 })
7783 .is_ok()
7784 });
7785
7786 Ok(response)
7787 })
7788 }
7789
7790 async fn handle_update_followers(
7791 this: Entity<Self>,
7792 envelope: TypedEnvelope<proto::UpdateFollowers>,
7793 mut cx: AsyncApp,
7794 ) -> Result<()> {
7795 let leader_id = envelope.original_sender_id()?;
7796 let update = envelope.payload;
7797
7798 this.update(&mut cx, |this, cx| {
7799 this.workspaces.retain(|(window_handle, weak_workspace)| {
7800 let Some(workspace) = weak_workspace.upgrade() else {
7801 return false;
7802 };
7803 window_handle
7804 .update(cx, |_, window, cx| {
7805 workspace.update(cx, |workspace, cx| {
7806 let project_id = workspace.project.read(cx).remote_id();
7807 if update.project_id != project_id && update.project_id.is_some() {
7808 return;
7809 }
7810 workspace.handle_update_followers(
7811 leader_id,
7812 update.clone(),
7813 window,
7814 cx,
7815 );
7816 });
7817 })
7818 .is_ok()
7819 });
7820 Ok(())
7821 })
7822 }
7823
7824 pub fn workspaces(&self) -> impl Iterator<Item = &WeakEntity<Workspace>> {
7825 self.workspaces.iter().map(|(_, weak)| weak)
7826 }
7827
7828 pub fn workspaces_with_windows(
7829 &self,
7830 ) -> impl Iterator<Item = (gpui::AnyWindowHandle, &WeakEntity<Workspace>)> {
7831 self.workspaces.iter().map(|(window, weak)| (*window, weak))
7832 }
7833}
7834
7835impl ViewId {
7836 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
7837 Ok(Self {
7838 creator: message
7839 .creator
7840 .map(CollaboratorId::PeerId)
7841 .context("creator is missing")?,
7842 id: message.id,
7843 })
7844 }
7845
7846 pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
7847 if let CollaboratorId::PeerId(peer_id) = self.creator {
7848 Some(proto::ViewId {
7849 creator: Some(peer_id),
7850 id: self.id,
7851 })
7852 } else {
7853 None
7854 }
7855 }
7856}
7857
7858impl FollowerState {
7859 fn pane(&self) -> &Entity<Pane> {
7860 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
7861 }
7862}
7863
7864pub trait WorkspaceHandle {
7865 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
7866}
7867
7868impl WorkspaceHandle for Entity<Workspace> {
7869 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
7870 self.read(cx)
7871 .worktrees(cx)
7872 .flat_map(|worktree| {
7873 let worktree_id = worktree.read(cx).id();
7874 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
7875 worktree_id,
7876 path: f.path.clone(),
7877 })
7878 })
7879 .collect::<Vec<_>>()
7880 }
7881}
7882
7883pub async fn last_opened_workspace_location(
7884 fs: &dyn fs::Fs,
7885) -> Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)> {
7886 DB.last_workspace(fs)
7887 .await
7888 .log_err()
7889 .flatten()
7890 .map(|(id, location, paths, _timestamp)| (id, location, paths))
7891}
7892
7893pub async fn last_session_workspace_locations(
7894 last_session_id: &str,
7895 last_session_window_stack: Option<Vec<WindowId>>,
7896 fs: &dyn fs::Fs,
7897) -> Option<Vec<SessionWorkspace>> {
7898 DB.last_session_workspace_locations(last_session_id, last_session_window_stack, fs)
7899 .await
7900 .log_err()
7901}
7902
7903pub async fn restore_multiworkspace(
7904 multi_workspace: SerializedMultiWorkspace,
7905 app_state: Arc<AppState>,
7906 cx: &mut AsyncApp,
7907) -> anyhow::Result<WindowHandle<MultiWorkspace>> {
7908 let SerializedMultiWorkspace { workspaces, state } = multi_workspace;
7909 let mut group_iter = workspaces.into_iter();
7910 let first = group_iter
7911 .next()
7912 .context("window group must not be empty")?;
7913
7914 let window_handle = if first.paths.is_empty() {
7915 cx.update(|cx| open_workspace_by_id(first.workspace_id, app_state.clone(), None, cx))
7916 .await?
7917 } else {
7918 let (window, _items) = cx
7919 .update(|cx| {
7920 Workspace::new_local(
7921 first.paths.paths().to_vec(),
7922 app_state.clone(),
7923 None,
7924 None,
7925 None,
7926 cx,
7927 )
7928 })
7929 .await?;
7930 window
7931 };
7932
7933 for session_workspace in group_iter {
7934 if session_workspace.paths.is_empty() {
7935 cx.update(|cx| {
7936 open_workspace_by_id(
7937 session_workspace.workspace_id,
7938 app_state.clone(),
7939 Some(window_handle),
7940 cx,
7941 )
7942 })
7943 .await?;
7944 } else {
7945 cx.update(|cx| {
7946 Workspace::new_local(
7947 session_workspace.paths.paths().to_vec(),
7948 app_state.clone(),
7949 Some(window_handle),
7950 None,
7951 None,
7952 cx,
7953 )
7954 })
7955 .await?;
7956 }
7957 }
7958
7959 if let Some(target_id) = state.active_workspace_id {
7960 window_handle
7961 .update(cx, |multi_workspace, window, cx| {
7962 let target_index = multi_workspace
7963 .workspaces()
7964 .iter()
7965 .position(|ws| ws.read(cx).database_id() == Some(target_id));
7966 if let Some(index) = target_index {
7967 multi_workspace.activate_index(index, window, cx);
7968 } else if !multi_workspace.workspaces().is_empty() {
7969 multi_workspace.activate_index(0, window, cx);
7970 }
7971 })
7972 .ok();
7973 } else {
7974 window_handle
7975 .update(cx, |multi_workspace, window, cx| {
7976 if !multi_workspace.workspaces().is_empty() {
7977 multi_workspace.activate_index(0, window, cx);
7978 }
7979 })
7980 .ok();
7981 }
7982
7983 if state.sidebar_open {
7984 window_handle
7985 .update(cx, |multi_workspace, _, cx| {
7986 multi_workspace.open_sidebar(cx);
7987 })
7988 .ok();
7989 }
7990
7991 window_handle
7992 .update(cx, |_, window, _cx| {
7993 window.activate_window();
7994 })
7995 .ok();
7996
7997 Ok(window_handle)
7998}
7999
8000actions!(
8001 collab,
8002 [
8003 /// Opens the channel notes for the current call.
8004 ///
8005 /// Use `collab_panel::OpenSelectedChannelNotes` to open the channel notes for the selected
8006 /// channel in the collab panel.
8007 ///
8008 /// If you want to open a specific channel, use `zed::OpenZedUrl` with a channel notes URL -
8009 /// can be copied via "Copy link to section" in the context menu of the channel notes
8010 /// buffer. These URLs look like `https://zed.dev/channel/channel-name-CHANNEL_ID/notes`.
8011 OpenChannelNotes,
8012 /// Mutes your microphone.
8013 Mute,
8014 /// Deafens yourself (mute both microphone and speakers).
8015 Deafen,
8016 /// Leaves the current call.
8017 LeaveCall,
8018 /// Shares the current project with collaborators.
8019 ShareProject,
8020 /// Shares your screen with collaborators.
8021 ScreenShare,
8022 /// Copies the current room name and session id for debugging purposes.
8023 CopyRoomId,
8024 ]
8025);
8026actions!(
8027 zed,
8028 [
8029 /// Opens the Zed log file.
8030 OpenLog,
8031 /// Reveals the Zed log file in the system file manager.
8032 RevealLogInFileManager
8033 ]
8034);
8035
8036async fn join_channel_internal(
8037 channel_id: ChannelId,
8038 app_state: &Arc<AppState>,
8039 requesting_window: Option<WindowHandle<MultiWorkspace>>,
8040 requesting_workspace: Option<WeakEntity<Workspace>>,
8041 active_call: &Entity<ActiveCall>,
8042 cx: &mut AsyncApp,
8043) -> Result<bool> {
8044 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
8045 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
8046 return (false, None);
8047 };
8048
8049 let already_in_channel = room.channel_id() == Some(channel_id);
8050 let should_prompt = room.is_sharing_project()
8051 && !room.remote_participants().is_empty()
8052 && !already_in_channel;
8053 let open_room = if already_in_channel {
8054 active_call.room().cloned()
8055 } else {
8056 None
8057 };
8058 (should_prompt, open_room)
8059 });
8060
8061 if let Some(room) = open_room {
8062 let task = room.update(cx, |room, cx| {
8063 if let Some((project, host)) = room.most_active_project(cx) {
8064 return Some(join_in_room_project(project, host, app_state.clone(), cx));
8065 }
8066
8067 None
8068 });
8069 if let Some(task) = task {
8070 task.await?;
8071 }
8072 return anyhow::Ok(true);
8073 }
8074
8075 if should_prompt {
8076 if let Some(multi_workspace) = requesting_window {
8077 let answer = multi_workspace
8078 .update(cx, |_, window, cx| {
8079 window.prompt(
8080 PromptLevel::Warning,
8081 "Do you want to switch channels?",
8082 Some("Leaving this call will unshare your current project."),
8083 &["Yes, Join Channel", "Cancel"],
8084 cx,
8085 )
8086 })?
8087 .await;
8088
8089 if answer == Ok(1) {
8090 return Ok(false);
8091 }
8092 } else {
8093 return Ok(false); // unreachable!() hopefully
8094 }
8095 }
8096
8097 let client = cx.update(|cx| active_call.read(cx).client());
8098
8099 let mut client_status = client.status();
8100
8101 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
8102 'outer: loop {
8103 let Some(status) = client_status.recv().await else {
8104 anyhow::bail!("error connecting");
8105 };
8106
8107 match status {
8108 Status::Connecting
8109 | Status::Authenticating
8110 | Status::Authenticated
8111 | Status::Reconnecting
8112 | Status::Reauthenticating
8113 | Status::Reauthenticated => continue,
8114 Status::Connected { .. } => break 'outer,
8115 Status::SignedOut | Status::AuthenticationError => {
8116 return Err(ErrorCode::SignedOut.into());
8117 }
8118 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
8119 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
8120 return Err(ErrorCode::Disconnected.into());
8121 }
8122 }
8123 }
8124
8125 let room = active_call
8126 .update(cx, |active_call, cx| {
8127 active_call.join_channel(channel_id, cx)
8128 })
8129 .await?;
8130
8131 let Some(room) = room else {
8132 return anyhow::Ok(true);
8133 };
8134
8135 room.update(cx, |room, _| room.room_update_completed())
8136 .await;
8137
8138 let task = room.update(cx, |room, cx| {
8139 if let Some((project, host)) = room.most_active_project(cx) {
8140 return Some(join_in_room_project(project, host, app_state.clone(), cx));
8141 }
8142
8143 // If you are the first to join a channel, see if you should share your project.
8144 if room.remote_participants().is_empty()
8145 && !room.local_participant_is_guest()
8146 && let Some(workspace) = requesting_workspace.as_ref().and_then(|w| w.upgrade())
8147 {
8148 let project = workspace.update(cx, |workspace, cx| {
8149 let project = workspace.project.read(cx);
8150
8151 if !CallSettings::get_global(cx).share_on_join {
8152 return None;
8153 }
8154
8155 if (project.is_local() || project.is_via_remote_server())
8156 && project.visible_worktrees(cx).any(|tree| {
8157 tree.read(cx)
8158 .root_entry()
8159 .is_some_and(|entry| entry.is_dir())
8160 })
8161 {
8162 Some(workspace.project.clone())
8163 } else {
8164 None
8165 }
8166 });
8167 if let Some(project) = project {
8168 return Some(cx.spawn(async move |room, cx| {
8169 room.update(cx, |room, cx| room.share_project(project, cx))?
8170 .await?;
8171 Ok(())
8172 }));
8173 }
8174 }
8175
8176 None
8177 });
8178 if let Some(task) = task {
8179 task.await?;
8180 return anyhow::Ok(true);
8181 }
8182 anyhow::Ok(false)
8183}
8184
8185pub fn join_channel(
8186 channel_id: ChannelId,
8187 app_state: Arc<AppState>,
8188 requesting_window: Option<WindowHandle<MultiWorkspace>>,
8189 requesting_workspace: Option<WeakEntity<Workspace>>,
8190 cx: &mut App,
8191) -> Task<Result<()>> {
8192 let active_call = ActiveCall::global(cx);
8193 cx.spawn(async move |cx| {
8194 let result = join_channel_internal(
8195 channel_id,
8196 &app_state,
8197 requesting_window,
8198 requesting_workspace,
8199 &active_call,
8200 cx,
8201 )
8202 .await;
8203
8204 // join channel succeeded, and opened a window
8205 if matches!(result, Ok(true)) {
8206 return anyhow::Ok(());
8207 }
8208
8209 // find an existing workspace to focus and show call controls
8210 let mut active_window = requesting_window.or_else(|| activate_any_workspace_window(cx));
8211 if active_window.is_none() {
8212 // no open workspaces, make one to show the error in (blergh)
8213 let (window_handle, _) = cx
8214 .update(|cx| {
8215 Workspace::new_local(
8216 vec![],
8217 app_state.clone(),
8218 requesting_window,
8219 None,
8220 None,
8221 cx,
8222 )
8223 })
8224 .await?;
8225
8226 window_handle
8227 .update(cx, |_, window, _cx| {
8228 window.activate_window();
8229 })
8230 .ok();
8231
8232 if result.is_ok() {
8233 cx.update(|cx| {
8234 cx.dispatch_action(&OpenChannelNotes);
8235 });
8236 }
8237
8238 active_window = Some(window_handle);
8239 }
8240
8241 if let Err(err) = result {
8242 log::error!("failed to join channel: {}", err);
8243 if let Some(active_window) = active_window {
8244 active_window
8245 .update(cx, |_, window, cx| {
8246 let detail: SharedString = match err.error_code() {
8247 ErrorCode::SignedOut => "Please sign in to continue.".into(),
8248 ErrorCode::UpgradeRequired => concat!(
8249 "Your are running an unsupported version of Zed. ",
8250 "Please update to continue."
8251 )
8252 .into(),
8253 ErrorCode::NoSuchChannel => concat!(
8254 "No matching channel was found. ",
8255 "Please check the link and try again."
8256 )
8257 .into(),
8258 ErrorCode::Forbidden => concat!(
8259 "This channel is private, and you do not have access. ",
8260 "Please ask someone to add you and try again."
8261 )
8262 .into(),
8263 ErrorCode::Disconnected => {
8264 "Please check your internet connection and try again.".into()
8265 }
8266 _ => format!("{}\n\nPlease try again.", err).into(),
8267 };
8268 window.prompt(
8269 PromptLevel::Critical,
8270 "Failed to join channel",
8271 Some(&detail),
8272 &["Ok"],
8273 cx,
8274 )
8275 })?
8276 .await
8277 .ok();
8278 }
8279 }
8280
8281 // return ok, we showed the error to the user.
8282 anyhow::Ok(())
8283 })
8284}
8285
8286pub async fn get_any_active_multi_workspace(
8287 app_state: Arc<AppState>,
8288 mut cx: AsyncApp,
8289) -> anyhow::Result<WindowHandle<MultiWorkspace>> {
8290 // find an existing workspace to focus and show call controls
8291 let active_window = activate_any_workspace_window(&mut cx);
8292 if active_window.is_none() {
8293 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, None, cx))
8294 .await?;
8295 }
8296 activate_any_workspace_window(&mut cx).context("could not open zed")
8297}
8298
8299fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<MultiWorkspace>> {
8300 cx.update(|cx| {
8301 if let Some(workspace_window) = cx
8302 .active_window()
8303 .and_then(|window| window.downcast::<MultiWorkspace>())
8304 {
8305 return Some(workspace_window);
8306 }
8307
8308 for window in cx.windows() {
8309 if let Some(workspace_window) = window.downcast::<MultiWorkspace>() {
8310 workspace_window
8311 .update(cx, |_, window, _| window.activate_window())
8312 .ok();
8313 return Some(workspace_window);
8314 }
8315 }
8316 None
8317 })
8318}
8319
8320pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<MultiWorkspace>> {
8321 workspace_windows_for_location(&SerializedWorkspaceLocation::Local, cx)
8322}
8323
8324pub fn workspace_windows_for_location(
8325 serialized_location: &SerializedWorkspaceLocation,
8326 cx: &App,
8327) -> Vec<WindowHandle<MultiWorkspace>> {
8328 cx.windows()
8329 .into_iter()
8330 .filter_map(|window| window.downcast::<MultiWorkspace>())
8331 .filter(|multi_workspace| {
8332 let same_host = |left: &RemoteConnectionOptions, right: &RemoteConnectionOptions| match (left, right) {
8333 (RemoteConnectionOptions::Ssh(a), RemoteConnectionOptions::Ssh(b)) => {
8334 (&a.host, &a.username, &a.port) == (&b.host, &b.username, &b.port)
8335 }
8336 (RemoteConnectionOptions::Wsl(a), RemoteConnectionOptions::Wsl(b)) => {
8337 // The WSL username is not consistently populated in the workspace location, so ignore it for now.
8338 a.distro_name == b.distro_name
8339 }
8340 (RemoteConnectionOptions::Docker(a), RemoteConnectionOptions::Docker(b)) => {
8341 a.container_id == b.container_id
8342 }
8343 #[cfg(any(test, feature = "test-support"))]
8344 (RemoteConnectionOptions::Mock(a), RemoteConnectionOptions::Mock(b)) => {
8345 a.id == b.id
8346 }
8347 _ => false,
8348 };
8349
8350 multi_workspace.read(cx).is_ok_and(|multi_workspace| {
8351 multi_workspace.workspaces().iter().any(|workspace| {
8352 match workspace.read(cx).workspace_location(cx) {
8353 WorkspaceLocation::Location(location, _) => {
8354 match (&location, serialized_location) {
8355 (
8356 SerializedWorkspaceLocation::Local,
8357 SerializedWorkspaceLocation::Local,
8358 ) => true,
8359 (
8360 SerializedWorkspaceLocation::Remote(a),
8361 SerializedWorkspaceLocation::Remote(b),
8362 ) => same_host(a, b),
8363 _ => false,
8364 }
8365 }
8366 _ => false,
8367 }
8368 })
8369 })
8370 })
8371 .collect()
8372}
8373
8374pub async fn find_existing_workspace(
8375 abs_paths: &[PathBuf],
8376 open_options: &OpenOptions,
8377 location: &SerializedWorkspaceLocation,
8378 cx: &mut AsyncApp,
8379) -> (
8380 Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)>,
8381 OpenVisible,
8382) {
8383 let mut existing: Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)> = None;
8384 let mut open_visible = OpenVisible::All;
8385 let mut best_match = None;
8386
8387 if open_options.open_new_workspace != Some(true) {
8388 cx.update(|cx| {
8389 for window in workspace_windows_for_location(location, cx) {
8390 if let Ok(multi_workspace) = window.read(cx) {
8391 for workspace in multi_workspace.workspaces() {
8392 let project = workspace.read(cx).project.read(cx);
8393 let m = project.visibility_for_paths(
8394 abs_paths,
8395 open_options.open_new_workspace == None,
8396 cx,
8397 );
8398 if m > best_match {
8399 existing = Some((window, workspace.clone()));
8400 best_match = m;
8401 } else if best_match.is_none()
8402 && open_options.open_new_workspace == Some(false)
8403 {
8404 existing = Some((window, workspace.clone()))
8405 }
8406 }
8407 }
8408 }
8409 });
8410
8411 let all_paths_are_files = existing
8412 .as_ref()
8413 .and_then(|(_, target_workspace)| {
8414 cx.update(|cx| {
8415 let workspace = target_workspace.read(cx);
8416 let project = workspace.project.read(cx);
8417 let path_style = workspace.path_style(cx);
8418 Some(!abs_paths.iter().any(|path| {
8419 let path = util::paths::SanitizedPath::new(path);
8420 project.worktrees(cx).any(|worktree| {
8421 let worktree = worktree.read(cx);
8422 let abs_path = worktree.abs_path();
8423 path_style
8424 .strip_prefix(path.as_ref(), abs_path.as_ref())
8425 .and_then(|rel| worktree.entry_for_path(&rel))
8426 .is_some_and(|e| e.is_dir())
8427 })
8428 }))
8429 })
8430 })
8431 .unwrap_or(false);
8432
8433 if open_options.open_new_workspace.is_none()
8434 && existing.is_some()
8435 && open_options.wait
8436 && all_paths_are_files
8437 {
8438 cx.update(|cx| {
8439 let windows = workspace_windows_for_location(location, cx);
8440 let window = cx
8441 .active_window()
8442 .and_then(|window| window.downcast::<MultiWorkspace>())
8443 .filter(|window| windows.contains(window))
8444 .or_else(|| windows.into_iter().next());
8445 if let Some(window) = window {
8446 if let Ok(multi_workspace) = window.read(cx) {
8447 let active_workspace = multi_workspace.workspace().clone();
8448 existing = Some((window, active_workspace));
8449 open_visible = OpenVisible::None;
8450 }
8451 }
8452 });
8453 }
8454 }
8455 (existing, open_visible)
8456}
8457
8458#[derive(Default, Clone)]
8459pub struct OpenOptions {
8460 pub visible: Option<OpenVisible>,
8461 pub focus: Option<bool>,
8462 pub open_new_workspace: Option<bool>,
8463 pub wait: bool,
8464 pub replace_window: Option<WindowHandle<MultiWorkspace>>,
8465 pub env: Option<HashMap<String, String>>,
8466}
8467
8468/// Opens a workspace by its database ID, used for restoring empty workspaces with unsaved content.
8469pub fn open_workspace_by_id(
8470 workspace_id: WorkspaceId,
8471 app_state: Arc<AppState>,
8472 requesting_window: Option<WindowHandle<MultiWorkspace>>,
8473 cx: &mut App,
8474) -> Task<anyhow::Result<WindowHandle<MultiWorkspace>>> {
8475 let project_handle = Project::local(
8476 app_state.client.clone(),
8477 app_state.node_runtime.clone(),
8478 app_state.user_store.clone(),
8479 app_state.languages.clone(),
8480 app_state.fs.clone(),
8481 None,
8482 project::LocalProjectFlags {
8483 init_worktree_trust: true,
8484 ..project::LocalProjectFlags::default()
8485 },
8486 cx,
8487 );
8488
8489 cx.spawn(async move |cx| {
8490 let serialized_workspace = persistence::DB
8491 .workspace_for_id(workspace_id)
8492 .with_context(|| format!("Workspace {workspace_id:?} not found"))?;
8493
8494 let centered_layout = serialized_workspace.centered_layout;
8495
8496 let (window, workspace) = if let Some(window) = requesting_window {
8497 let workspace = window.update(cx, |multi_workspace, window, cx| {
8498 let workspace = cx.new(|cx| {
8499 let mut workspace = Workspace::new(
8500 Some(workspace_id),
8501 project_handle.clone(),
8502 app_state.clone(),
8503 window,
8504 cx,
8505 );
8506 workspace.centered_layout = centered_layout;
8507 workspace
8508 });
8509 multi_workspace.add_workspace(workspace.clone(), cx);
8510 workspace
8511 })?;
8512 (window, workspace)
8513 } else {
8514 let window_bounds_override = window_bounds_env_override();
8515
8516 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
8517 (Some(WindowBounds::Windowed(bounds)), None)
8518 } else if let Some(display) = serialized_workspace.display
8519 && let Some(bounds) = serialized_workspace.window_bounds.as_ref()
8520 {
8521 (Some(bounds.0), Some(display))
8522 } else if let Some((display, bounds)) = persistence::read_default_window_bounds() {
8523 (Some(bounds), Some(display))
8524 } else {
8525 (None, None)
8526 };
8527
8528 let options = cx.update(|cx| {
8529 let mut options = (app_state.build_window_options)(display, cx);
8530 options.window_bounds = window_bounds;
8531 options
8532 });
8533
8534 let window = cx.open_window(options, {
8535 let app_state = app_state.clone();
8536 let project_handle = project_handle.clone();
8537 move |window, cx| {
8538 let workspace = cx.new(|cx| {
8539 let mut workspace = Workspace::new(
8540 Some(workspace_id),
8541 project_handle,
8542 app_state,
8543 window,
8544 cx,
8545 );
8546 workspace.centered_layout = centered_layout;
8547 workspace
8548 });
8549 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
8550 }
8551 })?;
8552
8553 let workspace = window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
8554 multi_workspace.workspace().clone()
8555 })?;
8556
8557 (window, workspace)
8558 };
8559
8560 notify_if_database_failed(window, cx);
8561
8562 // Restore items from the serialized workspace
8563 window
8564 .update(cx, |_, window, cx| {
8565 workspace.update(cx, |_workspace, cx| {
8566 open_items(Some(serialized_workspace), vec![], window, cx)
8567 })
8568 })?
8569 .await?;
8570
8571 window.update(cx, |_, window, cx| {
8572 workspace.update(cx, |workspace, cx| {
8573 workspace.serialize_workspace(window, cx);
8574 });
8575 })?;
8576
8577 Ok(window)
8578 })
8579}
8580
8581#[allow(clippy::type_complexity)]
8582pub fn open_paths(
8583 abs_paths: &[PathBuf],
8584 app_state: Arc<AppState>,
8585 open_options: OpenOptions,
8586 cx: &mut App,
8587) -> Task<
8588 anyhow::Result<(
8589 WindowHandle<MultiWorkspace>,
8590 Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
8591 )>,
8592> {
8593 let abs_paths = abs_paths.to_vec();
8594 #[cfg(target_os = "windows")]
8595 let wsl_path = abs_paths
8596 .iter()
8597 .find_map(|p| util::paths::WslPath::from_path(p));
8598
8599 cx.spawn(async move |cx| {
8600 let (mut existing, mut open_visible) = find_existing_workspace(
8601 &abs_paths,
8602 &open_options,
8603 &SerializedWorkspaceLocation::Local,
8604 cx,
8605 )
8606 .await;
8607
8608 // Fallback: if no workspace contains the paths and all paths are files,
8609 // prefer an existing local workspace window (active window first).
8610 if open_options.open_new_workspace.is_none() && existing.is_none() {
8611 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
8612 let all_metadatas = futures::future::join_all(all_paths)
8613 .await
8614 .into_iter()
8615 .filter_map(|result| result.ok().flatten())
8616 .collect::<Vec<_>>();
8617
8618 if all_metadatas.iter().all(|file| !file.is_dir) {
8619 cx.update(|cx| {
8620 let windows = workspace_windows_for_location(
8621 &SerializedWorkspaceLocation::Local,
8622 cx,
8623 );
8624 let window = cx
8625 .active_window()
8626 .and_then(|window| window.downcast::<MultiWorkspace>())
8627 .filter(|window| windows.contains(window))
8628 .or_else(|| windows.into_iter().next());
8629 if let Some(window) = window {
8630 if let Ok(multi_workspace) = window.read(cx) {
8631 let active_workspace = multi_workspace.workspace().clone();
8632 existing = Some((window, active_workspace));
8633 open_visible = OpenVisible::None;
8634 }
8635 }
8636 });
8637 }
8638 }
8639
8640 let result = if let Some((existing, target_workspace)) = existing {
8641 let open_task = existing
8642 .update(cx, |multi_workspace, window, cx| {
8643 window.activate_window();
8644 multi_workspace.activate(target_workspace.clone(), cx);
8645 target_workspace.update(cx, |workspace, cx| {
8646 workspace.open_paths(
8647 abs_paths,
8648 OpenOptions {
8649 visible: Some(open_visible),
8650 ..Default::default()
8651 },
8652 None,
8653 window,
8654 cx,
8655 )
8656 })
8657 })?
8658 .await;
8659
8660 _ = existing.update(cx, |multi_workspace, _, cx| {
8661 let workspace = multi_workspace.workspace().clone();
8662 workspace.update(cx, |workspace, cx| {
8663 for item in open_task.iter().flatten() {
8664 if let Err(e) = item {
8665 workspace.show_error(&e, cx);
8666 }
8667 }
8668 });
8669 });
8670
8671 Ok((existing, open_task))
8672 } else {
8673 let result = cx
8674 .update(move |cx| {
8675 Workspace::new_local(
8676 abs_paths,
8677 app_state.clone(),
8678 open_options.replace_window,
8679 open_options.env,
8680 None,
8681 cx,
8682 )
8683 })
8684 .await;
8685
8686 if let Ok((ref window_handle, _)) = result {
8687 window_handle
8688 .update(cx, |_, window, _cx| {
8689 window.activate_window();
8690 })
8691 .log_err();
8692 }
8693
8694 result
8695 };
8696
8697 #[cfg(target_os = "windows")]
8698 if let Some(util::paths::WslPath{distro, path}) = wsl_path
8699 && let Ok((multi_workspace_window, _)) = &result
8700 {
8701 multi_workspace_window
8702 .update(cx, move |multi_workspace, _window, cx| {
8703 struct OpenInWsl;
8704 let workspace = multi_workspace.workspace().clone();
8705 workspace.update(cx, |workspace, cx| {
8706 workspace.show_notification(NotificationId::unique::<OpenInWsl>(), cx, move |cx| {
8707 let display_path = util::markdown::MarkdownInlineCode(&path.to_string_lossy());
8708 let msg = format!("{display_path} is inside a WSL filesystem, some features may not work unless you open it with WSL remote");
8709 cx.new(move |cx| {
8710 MessageNotification::new(msg, cx)
8711 .primary_message("Open in WSL")
8712 .primary_icon(IconName::FolderOpen)
8713 .primary_on_click(move |window, cx| {
8714 window.dispatch_action(Box::new(remote::OpenWslPath {
8715 distro: remote::WslConnectionOptions {
8716 distro_name: distro.clone(),
8717 user: None,
8718 },
8719 paths: vec![path.clone().into()],
8720 }), cx)
8721 })
8722 })
8723 });
8724 });
8725 })
8726 .unwrap();
8727 };
8728 result
8729 })
8730}
8731
8732pub fn open_new(
8733 open_options: OpenOptions,
8734 app_state: Arc<AppState>,
8735 cx: &mut App,
8736 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
8737) -> Task<anyhow::Result<()>> {
8738 let task = Workspace::new_local(
8739 Vec::new(),
8740 app_state,
8741 open_options.replace_window,
8742 open_options.env,
8743 Some(Box::new(init)),
8744 cx,
8745 );
8746 cx.spawn(async move |cx| {
8747 let (window, _opened_paths) = task.await?;
8748 window
8749 .update(cx, |_, window, _cx| {
8750 window.activate_window();
8751 })
8752 .ok();
8753 Ok(())
8754 })
8755}
8756
8757pub fn create_and_open_local_file(
8758 path: &'static Path,
8759 window: &mut Window,
8760 cx: &mut Context<Workspace>,
8761 default_content: impl 'static + Send + FnOnce() -> Rope,
8762) -> Task<Result<Box<dyn ItemHandle>>> {
8763 cx.spawn_in(window, async move |workspace, cx| {
8764 let fs = workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
8765 if !fs.is_file(path).await {
8766 fs.create_file(path, Default::default()).await?;
8767 fs.save(path, &default_content(), Default::default())
8768 .await?;
8769 }
8770
8771 workspace
8772 .update_in(cx, |workspace, window, cx| {
8773 workspace.with_local_or_wsl_workspace(window, cx, |workspace, window, cx| {
8774 let path = workspace
8775 .project
8776 .read_with(cx, |project, cx| project.try_windows_path_to_wsl(path, cx));
8777 cx.spawn_in(window, async move |workspace, cx| {
8778 let path = path.await?;
8779 let mut items = workspace
8780 .update_in(cx, |workspace, window, cx| {
8781 workspace.open_paths(
8782 vec![path.to_path_buf()],
8783 OpenOptions {
8784 visible: Some(OpenVisible::None),
8785 ..Default::default()
8786 },
8787 None,
8788 window,
8789 cx,
8790 )
8791 })?
8792 .await;
8793 let item = items.pop().flatten();
8794 item.with_context(|| format!("path {path:?} is not a file"))?
8795 })
8796 })
8797 })?
8798 .await?
8799 .await
8800 })
8801}
8802
8803pub fn open_remote_project_with_new_connection(
8804 window: WindowHandle<MultiWorkspace>,
8805 remote_connection: Arc<dyn RemoteConnection>,
8806 cancel_rx: oneshot::Receiver<()>,
8807 delegate: Arc<dyn RemoteClientDelegate>,
8808 app_state: Arc<AppState>,
8809 paths: Vec<PathBuf>,
8810 cx: &mut App,
8811) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
8812 cx.spawn(async move |cx| {
8813 let (workspace_id, serialized_workspace) =
8814 deserialize_remote_project(remote_connection.connection_options(), paths.clone(), cx)
8815 .await?;
8816
8817 let session = match cx
8818 .update(|cx| {
8819 remote::RemoteClient::new(
8820 ConnectionIdentifier::Workspace(workspace_id.0),
8821 remote_connection,
8822 cancel_rx,
8823 delegate,
8824 cx,
8825 )
8826 })
8827 .await?
8828 {
8829 Some(result) => result,
8830 None => return Ok(Vec::new()),
8831 };
8832
8833 let project = cx.update(|cx| {
8834 project::Project::remote(
8835 session,
8836 app_state.client.clone(),
8837 app_state.node_runtime.clone(),
8838 app_state.user_store.clone(),
8839 app_state.languages.clone(),
8840 app_state.fs.clone(),
8841 true,
8842 cx,
8843 )
8844 });
8845
8846 open_remote_project_inner(
8847 project,
8848 paths,
8849 workspace_id,
8850 serialized_workspace,
8851 app_state,
8852 window,
8853 cx,
8854 )
8855 .await
8856 })
8857}
8858
8859pub fn open_remote_project_with_existing_connection(
8860 connection_options: RemoteConnectionOptions,
8861 project: Entity<Project>,
8862 paths: Vec<PathBuf>,
8863 app_state: Arc<AppState>,
8864 window: WindowHandle<MultiWorkspace>,
8865 cx: &mut AsyncApp,
8866) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
8867 cx.spawn(async move |cx| {
8868 let (workspace_id, serialized_workspace) =
8869 deserialize_remote_project(connection_options.clone(), paths.clone(), cx).await?;
8870
8871 open_remote_project_inner(
8872 project,
8873 paths,
8874 workspace_id,
8875 serialized_workspace,
8876 app_state,
8877 window,
8878 cx,
8879 )
8880 .await
8881 })
8882}
8883
8884async fn open_remote_project_inner(
8885 project: Entity<Project>,
8886 paths: Vec<PathBuf>,
8887 workspace_id: WorkspaceId,
8888 serialized_workspace: Option<SerializedWorkspace>,
8889 app_state: Arc<AppState>,
8890 window: WindowHandle<MultiWorkspace>,
8891 cx: &mut AsyncApp,
8892) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
8893 let toolchains = DB.toolchains(workspace_id).await?;
8894 for (toolchain, worktree_path, path) in toolchains {
8895 project
8896 .update(cx, |this, cx| {
8897 let Some(worktree_id) =
8898 this.find_worktree(&worktree_path, cx)
8899 .and_then(|(worktree, rel_path)| {
8900 if rel_path.is_empty() {
8901 Some(worktree.read(cx).id())
8902 } else {
8903 None
8904 }
8905 })
8906 else {
8907 return Task::ready(None);
8908 };
8909
8910 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
8911 })
8912 .await;
8913 }
8914 let mut project_paths_to_open = vec![];
8915 let mut project_path_errors = vec![];
8916
8917 for path in paths {
8918 let result = cx
8919 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))
8920 .await;
8921 match result {
8922 Ok((_, project_path)) => {
8923 project_paths_to_open.push((path.clone(), Some(project_path)));
8924 }
8925 Err(error) => {
8926 project_path_errors.push(error);
8927 }
8928 };
8929 }
8930
8931 if project_paths_to_open.is_empty() {
8932 return Err(project_path_errors.pop().context("no paths given")?);
8933 }
8934
8935 let workspace = window.update(cx, |multi_workspace, window, cx| {
8936 telemetry::event!("SSH Project Opened");
8937
8938 let new_workspace = cx.new(|cx| {
8939 let mut workspace =
8940 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
8941 workspace.update_history(cx);
8942
8943 if let Some(ref serialized) = serialized_workspace {
8944 workspace.centered_layout = serialized.centered_layout;
8945 }
8946
8947 workspace
8948 });
8949
8950 multi_workspace.activate(new_workspace.clone(), cx);
8951 new_workspace
8952 })?;
8953
8954 let items = window
8955 .update(cx, |_, window, cx| {
8956 window.activate_window();
8957 workspace.update(cx, |_workspace, cx| {
8958 open_items(serialized_workspace, project_paths_to_open, window, cx)
8959 })
8960 })?
8961 .await?;
8962
8963 workspace.update(cx, |workspace, cx| {
8964 for error in project_path_errors {
8965 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
8966 if let Some(path) = error.error_tag("path") {
8967 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
8968 }
8969 } else {
8970 workspace.show_error(&error, cx)
8971 }
8972 }
8973 });
8974
8975 Ok(items.into_iter().map(|item| item?.ok()).collect())
8976}
8977
8978fn deserialize_remote_project(
8979 connection_options: RemoteConnectionOptions,
8980 paths: Vec<PathBuf>,
8981 cx: &AsyncApp,
8982) -> Task<Result<(WorkspaceId, Option<SerializedWorkspace>)>> {
8983 cx.background_spawn(async move {
8984 let remote_connection_id = persistence::DB
8985 .get_or_create_remote_connection(connection_options)
8986 .await?;
8987
8988 let serialized_workspace =
8989 persistence::DB.remote_workspace_for_roots(&paths, remote_connection_id);
8990
8991 let workspace_id = if let Some(workspace_id) =
8992 serialized_workspace.as_ref().map(|workspace| workspace.id)
8993 {
8994 workspace_id
8995 } else {
8996 persistence::DB.next_id().await?
8997 };
8998
8999 Ok((workspace_id, serialized_workspace))
9000 })
9001}
9002
9003pub fn join_in_room_project(
9004 project_id: u64,
9005 follow_user_id: u64,
9006 app_state: Arc<AppState>,
9007 cx: &mut App,
9008) -> Task<Result<()>> {
9009 let windows = cx.windows();
9010 cx.spawn(async move |cx| {
9011 let existing_window_and_workspace: Option<(
9012 WindowHandle<MultiWorkspace>,
9013 Entity<Workspace>,
9014 )> = windows.into_iter().find_map(|window_handle| {
9015 window_handle
9016 .downcast::<MultiWorkspace>()
9017 .and_then(|window_handle| {
9018 window_handle
9019 .update(cx, |multi_workspace, _window, cx| {
9020 for workspace in multi_workspace.workspaces() {
9021 if workspace.read(cx).project().read(cx).remote_id()
9022 == Some(project_id)
9023 {
9024 return Some((window_handle, workspace.clone()));
9025 }
9026 }
9027 None
9028 })
9029 .unwrap_or(None)
9030 })
9031 });
9032
9033 let multi_workspace_window = if let Some((existing_window, target_workspace)) =
9034 existing_window_and_workspace
9035 {
9036 existing_window
9037 .update(cx, |multi_workspace, _, cx| {
9038 multi_workspace.activate(target_workspace, cx);
9039 })
9040 .ok();
9041 existing_window
9042 } else {
9043 let active_call = cx.update(|cx| ActiveCall::global(cx));
9044 let room = active_call
9045 .read_with(cx, |call, _| call.room().cloned())
9046 .context("not in a call")?;
9047 let project = room
9048 .update(cx, |room, cx| {
9049 room.join_project(
9050 project_id,
9051 app_state.languages.clone(),
9052 app_state.fs.clone(),
9053 cx,
9054 )
9055 })
9056 .await?;
9057
9058 let window_bounds_override = window_bounds_env_override();
9059 cx.update(|cx| {
9060 let mut options = (app_state.build_window_options)(None, cx);
9061 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
9062 cx.open_window(options, |window, cx| {
9063 let workspace = cx.new(|cx| {
9064 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
9065 });
9066 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
9067 })
9068 })?
9069 };
9070
9071 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
9072 cx.activate(true);
9073 window.activate_window();
9074
9075 // We set the active workspace above, so this is the correct workspace.
9076 let workspace = multi_workspace.workspace().clone();
9077 workspace.update(cx, |workspace, cx| {
9078 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
9079 let follow_peer_id = room
9080 .read(cx)
9081 .remote_participants()
9082 .iter()
9083 .find(|(_, participant)| participant.user.id == follow_user_id)
9084 .map(|(_, p)| p.peer_id)
9085 .or_else(|| {
9086 // If we couldn't follow the given user, follow the host instead.
9087 let collaborator = workspace
9088 .project()
9089 .read(cx)
9090 .collaborators()
9091 .values()
9092 .find(|collaborator| collaborator.is_host)?;
9093 Some(collaborator.peer_id)
9094 });
9095
9096 if let Some(follow_peer_id) = follow_peer_id {
9097 workspace.follow(follow_peer_id, window, cx);
9098 }
9099 }
9100 });
9101 })?;
9102
9103 anyhow::Ok(())
9104 })
9105}
9106
9107pub fn reload(cx: &mut App) {
9108 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
9109 let mut workspace_windows = cx
9110 .windows()
9111 .into_iter()
9112 .filter_map(|window| window.downcast::<MultiWorkspace>())
9113 .collect::<Vec<_>>();
9114
9115 // If multiple windows have unsaved changes, and need a save prompt,
9116 // prompt in the active window before switching to a different window.
9117 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
9118
9119 let mut prompt = None;
9120 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
9121 prompt = window
9122 .update(cx, |_, window, cx| {
9123 window.prompt(
9124 PromptLevel::Info,
9125 "Are you sure you want to restart?",
9126 None,
9127 &["Restart", "Cancel"],
9128 cx,
9129 )
9130 })
9131 .ok();
9132 }
9133
9134 cx.spawn(async move |cx| {
9135 if let Some(prompt) = prompt {
9136 let answer = prompt.await?;
9137 if answer != 0 {
9138 return anyhow::Ok(());
9139 }
9140 }
9141
9142 // If the user cancels any save prompt, then keep the app open.
9143 for window in workspace_windows {
9144 if let Ok(should_close) = window.update(cx, |multi_workspace, window, cx| {
9145 let workspace = multi_workspace.workspace().clone();
9146 workspace.update(cx, |workspace, cx| {
9147 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
9148 })
9149 }) && !should_close.await?
9150 {
9151 return anyhow::Ok(());
9152 }
9153 }
9154 cx.update(|cx| cx.restart());
9155 anyhow::Ok(())
9156 })
9157 .detach_and_log_err(cx);
9158}
9159
9160fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
9161 let mut parts = value.split(',');
9162 let x: usize = parts.next()?.parse().ok()?;
9163 let y: usize = parts.next()?.parse().ok()?;
9164 Some(point(px(x as f32), px(y as f32)))
9165}
9166
9167fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
9168 let mut parts = value.split(',');
9169 let width: usize = parts.next()?.parse().ok()?;
9170 let height: usize = parts.next()?.parse().ok()?;
9171 Some(size(px(width as f32), px(height as f32)))
9172}
9173
9174/// Add client-side decorations (rounded corners, shadows, resize handling) when
9175/// appropriate.
9176///
9177/// The `border_radius_tiling` parameter allows overriding which corners get
9178/// rounded, independently of the actual window tiling state. This is used
9179/// specifically for the workspace switcher sidebar: when the sidebar is open,
9180/// we want square corners on the left (so the sidebar appears flush with the
9181/// window edge) but we still need the shadow padding for proper visual
9182/// appearance. Unlike actual window tiling, this only affects border radius -
9183/// not padding or shadows.
9184pub fn client_side_decorations(
9185 element: impl IntoElement,
9186 window: &mut Window,
9187 cx: &mut App,
9188 border_radius_tiling: Tiling,
9189) -> Stateful<Div> {
9190 const BORDER_SIZE: Pixels = px(1.0);
9191 let decorations = window.window_decorations();
9192 let tiling = match decorations {
9193 Decorations::Server => Tiling::default(),
9194 Decorations::Client { tiling } => tiling,
9195 };
9196
9197 match decorations {
9198 Decorations::Client { .. } => window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW),
9199 Decorations::Server => window.set_client_inset(px(0.0)),
9200 }
9201
9202 struct GlobalResizeEdge(ResizeEdge);
9203 impl Global for GlobalResizeEdge {}
9204
9205 div()
9206 .id("window-backdrop")
9207 .bg(transparent_black())
9208 .map(|div| match decorations {
9209 Decorations::Server => div,
9210 Decorations::Client { .. } => div
9211 .when(
9212 !(tiling.top
9213 || tiling.right
9214 || border_radius_tiling.top
9215 || border_radius_tiling.right),
9216 |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9217 )
9218 .when(
9219 !(tiling.top
9220 || tiling.left
9221 || border_radius_tiling.top
9222 || border_radius_tiling.left),
9223 |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9224 )
9225 .when(
9226 !(tiling.bottom
9227 || tiling.right
9228 || border_radius_tiling.bottom
9229 || border_radius_tiling.right),
9230 |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9231 )
9232 .when(
9233 !(tiling.bottom
9234 || tiling.left
9235 || border_radius_tiling.bottom
9236 || border_radius_tiling.left),
9237 |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9238 )
9239 .when(!tiling.top, |div| {
9240 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
9241 })
9242 .when(!tiling.bottom, |div| {
9243 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
9244 })
9245 .when(!tiling.left, |div| {
9246 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
9247 })
9248 .when(!tiling.right, |div| {
9249 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
9250 })
9251 .on_mouse_move(move |e, window, cx| {
9252 let size = window.window_bounds().get_bounds().size;
9253 let pos = e.position;
9254
9255 let new_edge =
9256 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
9257
9258 let edge = cx.try_global::<GlobalResizeEdge>();
9259 if new_edge != edge.map(|edge| edge.0) {
9260 window
9261 .window_handle()
9262 .update(cx, |workspace, _, cx| {
9263 cx.notify(workspace.entity_id());
9264 })
9265 .ok();
9266 }
9267 })
9268 .on_mouse_down(MouseButton::Left, move |e, window, _| {
9269 let size = window.window_bounds().get_bounds().size;
9270 let pos = e.position;
9271
9272 let edge = match resize_edge(
9273 pos,
9274 theme::CLIENT_SIDE_DECORATION_SHADOW,
9275 size,
9276 tiling,
9277 ) {
9278 Some(value) => value,
9279 None => return,
9280 };
9281
9282 window.start_window_resize(edge);
9283 }),
9284 })
9285 .size_full()
9286 .child(
9287 div()
9288 .cursor(CursorStyle::Arrow)
9289 .map(|div| match decorations {
9290 Decorations::Server => div,
9291 Decorations::Client { .. } => div
9292 .border_color(cx.theme().colors().border)
9293 .when(
9294 !(tiling.top
9295 || tiling.right
9296 || border_radius_tiling.top
9297 || border_radius_tiling.right),
9298 |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9299 )
9300 .when(
9301 !(tiling.top
9302 || tiling.left
9303 || border_radius_tiling.top
9304 || border_radius_tiling.left),
9305 |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9306 )
9307 .when(
9308 !(tiling.bottom
9309 || tiling.right
9310 || border_radius_tiling.bottom
9311 || border_radius_tiling.right),
9312 |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9313 )
9314 .when(
9315 !(tiling.bottom
9316 || tiling.left
9317 || border_radius_tiling.bottom
9318 || border_radius_tiling.left),
9319 |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
9320 )
9321 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
9322 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
9323 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
9324 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
9325 .when(!tiling.is_tiled(), |div| {
9326 div.shadow(vec![gpui::BoxShadow {
9327 color: Hsla {
9328 h: 0.,
9329 s: 0.,
9330 l: 0.,
9331 a: 0.4,
9332 },
9333 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
9334 spread_radius: px(0.),
9335 offset: point(px(0.0), px(0.0)),
9336 }])
9337 }),
9338 })
9339 .on_mouse_move(|_e, _, cx| {
9340 cx.stop_propagation();
9341 })
9342 .size_full()
9343 .child(element),
9344 )
9345 .map(|div| match decorations {
9346 Decorations::Server => div,
9347 Decorations::Client { tiling, .. } => div.child(
9348 canvas(
9349 |_bounds, window, _| {
9350 window.insert_hitbox(
9351 Bounds::new(
9352 point(px(0.0), px(0.0)),
9353 window.window_bounds().get_bounds().size,
9354 ),
9355 HitboxBehavior::Normal,
9356 )
9357 },
9358 move |_bounds, hitbox, window, cx| {
9359 let mouse = window.mouse_position();
9360 let size = window.window_bounds().get_bounds().size;
9361 let Some(edge) =
9362 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
9363 else {
9364 return;
9365 };
9366 cx.set_global(GlobalResizeEdge(edge));
9367 window.set_cursor_style(
9368 match edge {
9369 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
9370 ResizeEdge::Left | ResizeEdge::Right => {
9371 CursorStyle::ResizeLeftRight
9372 }
9373 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
9374 CursorStyle::ResizeUpLeftDownRight
9375 }
9376 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
9377 CursorStyle::ResizeUpRightDownLeft
9378 }
9379 },
9380 &hitbox,
9381 );
9382 },
9383 )
9384 .size_full()
9385 .absolute(),
9386 ),
9387 })
9388}
9389
9390fn resize_edge(
9391 pos: Point<Pixels>,
9392 shadow_size: Pixels,
9393 window_size: Size<Pixels>,
9394 tiling: Tiling,
9395) -> Option<ResizeEdge> {
9396 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
9397 if bounds.contains(&pos) {
9398 return None;
9399 }
9400
9401 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
9402 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
9403 if !tiling.top && top_left_bounds.contains(&pos) {
9404 return Some(ResizeEdge::TopLeft);
9405 }
9406
9407 let top_right_bounds = Bounds::new(
9408 Point::new(window_size.width - corner_size.width, px(0.)),
9409 corner_size,
9410 );
9411 if !tiling.top && top_right_bounds.contains(&pos) {
9412 return Some(ResizeEdge::TopRight);
9413 }
9414
9415 let bottom_left_bounds = Bounds::new(
9416 Point::new(px(0.), window_size.height - corner_size.height),
9417 corner_size,
9418 );
9419 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
9420 return Some(ResizeEdge::BottomLeft);
9421 }
9422
9423 let bottom_right_bounds = Bounds::new(
9424 Point::new(
9425 window_size.width - corner_size.width,
9426 window_size.height - corner_size.height,
9427 ),
9428 corner_size,
9429 );
9430 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
9431 return Some(ResizeEdge::BottomRight);
9432 }
9433
9434 if !tiling.top && pos.y < shadow_size {
9435 Some(ResizeEdge::Top)
9436 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
9437 Some(ResizeEdge::Bottom)
9438 } else if !tiling.left && pos.x < shadow_size {
9439 Some(ResizeEdge::Left)
9440 } else if !tiling.right && pos.x > window_size.width - shadow_size {
9441 Some(ResizeEdge::Right)
9442 } else {
9443 None
9444 }
9445}
9446
9447fn join_pane_into_active(
9448 active_pane: &Entity<Pane>,
9449 pane: &Entity<Pane>,
9450 window: &mut Window,
9451 cx: &mut App,
9452) {
9453 if pane == active_pane {
9454 } else if pane.read(cx).items_len() == 0 {
9455 pane.update(cx, |_, cx| {
9456 cx.emit(pane::Event::Remove {
9457 focus_on_pane: None,
9458 });
9459 })
9460 } else {
9461 move_all_items(pane, active_pane, window, cx);
9462 }
9463}
9464
9465fn move_all_items(
9466 from_pane: &Entity<Pane>,
9467 to_pane: &Entity<Pane>,
9468 window: &mut Window,
9469 cx: &mut App,
9470) {
9471 let destination_is_different = from_pane != to_pane;
9472 let mut moved_items = 0;
9473 for (item_ix, item_handle) in from_pane
9474 .read(cx)
9475 .items()
9476 .enumerate()
9477 .map(|(ix, item)| (ix, item.clone()))
9478 .collect::<Vec<_>>()
9479 {
9480 let ix = item_ix - moved_items;
9481 if destination_is_different {
9482 // Close item from previous pane
9483 from_pane.update(cx, |source, cx| {
9484 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
9485 });
9486 moved_items += 1;
9487 }
9488
9489 // This automatically removes duplicate items in the pane
9490 to_pane.update(cx, |destination, cx| {
9491 destination.add_item(item_handle, true, true, None, window, cx);
9492 window.focus(&destination.focus_handle(cx), cx)
9493 });
9494 }
9495}
9496
9497pub fn move_item(
9498 source: &Entity<Pane>,
9499 destination: &Entity<Pane>,
9500 item_id_to_move: EntityId,
9501 destination_index: usize,
9502 activate: bool,
9503 window: &mut Window,
9504 cx: &mut App,
9505) {
9506 let Some((item_ix, item_handle)) = source
9507 .read(cx)
9508 .items()
9509 .enumerate()
9510 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
9511 .map(|(ix, item)| (ix, item.clone()))
9512 else {
9513 // Tab was closed during drag
9514 return;
9515 };
9516
9517 if source != destination {
9518 // Close item from previous pane
9519 source.update(cx, |source, cx| {
9520 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
9521 });
9522 }
9523
9524 // This automatically removes duplicate items in the pane
9525 destination.update(cx, |destination, cx| {
9526 destination.add_item_inner(
9527 item_handle,
9528 activate,
9529 activate,
9530 activate,
9531 Some(destination_index),
9532 window,
9533 cx,
9534 );
9535 if activate {
9536 window.focus(&destination.focus_handle(cx), cx)
9537 }
9538 });
9539}
9540
9541pub fn move_active_item(
9542 source: &Entity<Pane>,
9543 destination: &Entity<Pane>,
9544 focus_destination: bool,
9545 close_if_empty: bool,
9546 window: &mut Window,
9547 cx: &mut App,
9548) {
9549 if source == destination {
9550 return;
9551 }
9552 let Some(active_item) = source.read(cx).active_item() else {
9553 return;
9554 };
9555 source.update(cx, |source_pane, cx| {
9556 let item_id = active_item.item_id();
9557 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
9558 destination.update(cx, |target_pane, cx| {
9559 target_pane.add_item(
9560 active_item,
9561 focus_destination,
9562 focus_destination,
9563 Some(target_pane.items_len()),
9564 window,
9565 cx,
9566 );
9567 });
9568 });
9569}
9570
9571pub fn clone_active_item(
9572 workspace_id: Option<WorkspaceId>,
9573 source: &Entity<Pane>,
9574 destination: &Entity<Pane>,
9575 focus_destination: bool,
9576 window: &mut Window,
9577 cx: &mut App,
9578) {
9579 if source == destination {
9580 return;
9581 }
9582 let Some(active_item) = source.read(cx).active_item() else {
9583 return;
9584 };
9585 if !active_item.can_split(cx) {
9586 return;
9587 }
9588 let destination = destination.downgrade();
9589 let task = active_item.clone_on_split(workspace_id, window, cx);
9590 window
9591 .spawn(cx, async move |cx| {
9592 let Some(clone) = task.await else {
9593 return;
9594 };
9595 destination
9596 .update_in(cx, |target_pane, window, cx| {
9597 target_pane.add_item(
9598 clone,
9599 focus_destination,
9600 focus_destination,
9601 Some(target_pane.items_len()),
9602 window,
9603 cx,
9604 );
9605 })
9606 .log_err();
9607 })
9608 .detach();
9609}
9610
9611#[derive(Debug)]
9612pub struct WorkspacePosition {
9613 pub window_bounds: Option<WindowBounds>,
9614 pub display: Option<Uuid>,
9615 pub centered_layout: bool,
9616}
9617
9618pub fn remote_workspace_position_from_db(
9619 connection_options: RemoteConnectionOptions,
9620 paths_to_open: &[PathBuf],
9621 cx: &App,
9622) -> Task<Result<WorkspacePosition>> {
9623 let paths = paths_to_open.to_vec();
9624
9625 cx.background_spawn(async move {
9626 let remote_connection_id = persistence::DB
9627 .get_or_create_remote_connection(connection_options)
9628 .await
9629 .context("fetching serialized ssh project")?;
9630 let serialized_workspace =
9631 persistence::DB.remote_workspace_for_roots(&paths, remote_connection_id);
9632
9633 let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
9634 (Some(WindowBounds::Windowed(bounds)), None)
9635 } else {
9636 let restorable_bounds = serialized_workspace
9637 .as_ref()
9638 .and_then(|workspace| {
9639 Some((workspace.display?, workspace.window_bounds.map(|b| b.0)?))
9640 })
9641 .or_else(|| persistence::read_default_window_bounds());
9642
9643 if let Some((serialized_display, serialized_bounds)) = restorable_bounds {
9644 (Some(serialized_bounds), Some(serialized_display))
9645 } else {
9646 (None, None)
9647 }
9648 };
9649
9650 let centered_layout = serialized_workspace
9651 .as_ref()
9652 .map(|w| w.centered_layout)
9653 .unwrap_or(false);
9654
9655 Ok(WorkspacePosition {
9656 window_bounds,
9657 display,
9658 centered_layout,
9659 })
9660 })
9661}
9662
9663pub fn with_active_or_new_workspace(
9664 cx: &mut App,
9665 f: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + 'static,
9666) {
9667 match cx
9668 .active_window()
9669 .and_then(|w| w.downcast::<MultiWorkspace>())
9670 {
9671 Some(multi_workspace) => {
9672 cx.defer(move |cx| {
9673 multi_workspace
9674 .update(cx, |multi_workspace, window, cx| {
9675 let workspace = multi_workspace.workspace().clone();
9676 workspace.update(cx, |workspace, cx| f(workspace, window, cx));
9677 })
9678 .log_err();
9679 });
9680 }
9681 None => {
9682 let app_state = AppState::global(cx);
9683 if let Some(app_state) = app_state.upgrade() {
9684 open_new(
9685 OpenOptions::default(),
9686 app_state,
9687 cx,
9688 move |workspace, window, cx| f(workspace, window, cx),
9689 )
9690 .detach_and_log_err(cx);
9691 }
9692 }
9693 }
9694}
9695
9696#[cfg(test)]
9697mod tests {
9698 use std::{cell::RefCell, rc::Rc};
9699
9700 use super::*;
9701 use crate::{
9702 dock::{PanelEvent, test::TestPanel},
9703 item::{
9704 ItemBufferKind, ItemEvent,
9705 test::{TestItem, TestProjectItem},
9706 },
9707 };
9708 use fs::FakeFs;
9709 use gpui::{
9710 DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
9711 UpdateGlobal, VisualTestContext, px,
9712 };
9713 use project::{Project, ProjectEntryId};
9714 use serde_json::json;
9715 use settings::SettingsStore;
9716 use util::rel_path::rel_path;
9717
9718 #[gpui::test]
9719 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
9720 init_test(cx);
9721
9722 let fs = FakeFs::new(cx.executor());
9723 let project = Project::test(fs, [], cx).await;
9724 let (workspace, cx) =
9725 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9726
9727 // Adding an item with no ambiguity renders the tab without detail.
9728 let item1 = cx.new(|cx| {
9729 let mut item = TestItem::new(cx);
9730 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
9731 item
9732 });
9733 workspace.update_in(cx, |workspace, window, cx| {
9734 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
9735 });
9736 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
9737
9738 // Adding an item that creates ambiguity increases the level of detail on
9739 // both tabs.
9740 let item2 = cx.new_window_entity(|_window, cx| {
9741 let mut item = TestItem::new(cx);
9742 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
9743 item
9744 });
9745 workspace.update_in(cx, |workspace, window, cx| {
9746 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
9747 });
9748 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
9749 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
9750
9751 // Adding an item that creates ambiguity increases the level of detail only
9752 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
9753 // we stop at the highest detail available.
9754 let item3 = cx.new(|cx| {
9755 let mut item = TestItem::new(cx);
9756 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
9757 item
9758 });
9759 workspace.update_in(cx, |workspace, window, cx| {
9760 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
9761 });
9762 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
9763 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
9764 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
9765 }
9766
9767 #[gpui::test]
9768 async fn test_tracking_active_path(cx: &mut TestAppContext) {
9769 init_test(cx);
9770
9771 let fs = FakeFs::new(cx.executor());
9772 fs.insert_tree(
9773 "/root1",
9774 json!({
9775 "one.txt": "",
9776 "two.txt": "",
9777 }),
9778 )
9779 .await;
9780 fs.insert_tree(
9781 "/root2",
9782 json!({
9783 "three.txt": "",
9784 }),
9785 )
9786 .await;
9787
9788 let project = Project::test(fs, ["root1".as_ref()], cx).await;
9789 let (workspace, cx) =
9790 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9791 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9792 let worktree_id = project.update(cx, |project, cx| {
9793 project.worktrees(cx).next().unwrap().read(cx).id()
9794 });
9795
9796 let item1 = cx.new(|cx| {
9797 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
9798 });
9799 let item2 = cx.new(|cx| {
9800 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
9801 });
9802
9803 // Add an item to an empty pane
9804 workspace.update_in(cx, |workspace, window, cx| {
9805 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
9806 });
9807 project.update(cx, |project, cx| {
9808 assert_eq!(
9809 project.active_entry(),
9810 project
9811 .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
9812 .map(|e| e.id)
9813 );
9814 });
9815 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
9816
9817 // Add a second item to a non-empty pane
9818 workspace.update_in(cx, |workspace, window, cx| {
9819 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
9820 });
9821 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
9822 project.update(cx, |project, cx| {
9823 assert_eq!(
9824 project.active_entry(),
9825 project
9826 .entry_for_path(&(worktree_id, rel_path("two.txt")).into(), cx)
9827 .map(|e| e.id)
9828 );
9829 });
9830
9831 // Close the active item
9832 pane.update_in(cx, |pane, window, cx| {
9833 pane.close_active_item(&Default::default(), window, cx)
9834 })
9835 .await
9836 .unwrap();
9837 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
9838 project.update(cx, |project, cx| {
9839 assert_eq!(
9840 project.active_entry(),
9841 project
9842 .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
9843 .map(|e| e.id)
9844 );
9845 });
9846
9847 // Add a project folder
9848 project
9849 .update(cx, |project, cx| {
9850 project.find_or_create_worktree("root2", true, cx)
9851 })
9852 .await
9853 .unwrap();
9854 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
9855
9856 // Remove a project folder
9857 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
9858 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
9859 }
9860
9861 #[gpui::test]
9862 async fn test_close_window(cx: &mut TestAppContext) {
9863 init_test(cx);
9864
9865 let fs = FakeFs::new(cx.executor());
9866 fs.insert_tree("/root", json!({ "one": "" })).await;
9867
9868 let project = Project::test(fs, ["root".as_ref()], cx).await;
9869 let (workspace, cx) =
9870 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9871
9872 // When there are no dirty items, there's nothing to do.
9873 let item1 = cx.new(TestItem::new);
9874 workspace.update_in(cx, |w, window, cx| {
9875 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
9876 });
9877 let task = workspace.update_in(cx, |w, window, cx| {
9878 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
9879 });
9880 assert!(task.await.unwrap());
9881
9882 // When there are dirty untitled items, prompt to save each one. If the user
9883 // cancels any prompt, then abort.
9884 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
9885 let item3 = cx.new(|cx| {
9886 TestItem::new(cx)
9887 .with_dirty(true)
9888 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
9889 });
9890 workspace.update_in(cx, |w, window, cx| {
9891 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
9892 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
9893 });
9894 let task = workspace.update_in(cx, |w, window, cx| {
9895 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
9896 });
9897 cx.executor().run_until_parked();
9898 cx.simulate_prompt_answer("Cancel"); // cancel save all
9899 cx.executor().run_until_parked();
9900 assert!(!cx.has_pending_prompt());
9901 assert!(!task.await.unwrap());
9902 }
9903
9904 #[gpui::test]
9905 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
9906 init_test(cx);
9907
9908 // Register TestItem as a serializable item
9909 cx.update(|cx| {
9910 register_serializable_item::<TestItem>(cx);
9911 });
9912
9913 let fs = FakeFs::new(cx.executor());
9914 fs.insert_tree("/root", json!({ "one": "" })).await;
9915
9916 let project = Project::test(fs, ["root".as_ref()], cx).await;
9917 let (workspace, cx) =
9918 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9919
9920 // When there are dirty untitled items, but they can serialize, then there is no prompt.
9921 let item1 = cx.new(|cx| {
9922 TestItem::new(cx)
9923 .with_dirty(true)
9924 .with_serialize(|| Some(Task::ready(Ok(()))))
9925 });
9926 let item2 = cx.new(|cx| {
9927 TestItem::new(cx)
9928 .with_dirty(true)
9929 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
9930 .with_serialize(|| Some(Task::ready(Ok(()))))
9931 });
9932 workspace.update_in(cx, |w, window, cx| {
9933 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
9934 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
9935 });
9936 let task = workspace.update_in(cx, |w, window, cx| {
9937 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
9938 });
9939 assert!(task.await.unwrap());
9940 }
9941
9942 #[gpui::test]
9943 async fn test_close_pane_items(cx: &mut TestAppContext) {
9944 init_test(cx);
9945
9946 let fs = FakeFs::new(cx.executor());
9947
9948 let project = Project::test(fs, None, cx).await;
9949 let (workspace, cx) =
9950 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9951
9952 let item1 = cx.new(|cx| {
9953 TestItem::new(cx)
9954 .with_dirty(true)
9955 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
9956 });
9957 let item2 = cx.new(|cx| {
9958 TestItem::new(cx)
9959 .with_dirty(true)
9960 .with_conflict(true)
9961 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9962 });
9963 let item3 = cx.new(|cx| {
9964 TestItem::new(cx)
9965 .with_dirty(true)
9966 .with_conflict(true)
9967 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
9968 });
9969 let item4 = cx.new(|cx| {
9970 TestItem::new(cx).with_dirty(true).with_project_items(&[{
9971 let project_item = TestProjectItem::new_untitled(cx);
9972 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
9973 project_item
9974 }])
9975 });
9976 let pane = workspace.update_in(cx, |workspace, window, cx| {
9977 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
9978 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
9979 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
9980 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
9981 workspace.active_pane().clone()
9982 });
9983
9984 let close_items = pane.update_in(cx, |pane, window, cx| {
9985 pane.activate_item(1, true, true, window, cx);
9986 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
9987 let item1_id = item1.item_id();
9988 let item3_id = item3.item_id();
9989 let item4_id = item4.item_id();
9990 pane.close_items(window, cx, SaveIntent::Close, move |id| {
9991 [item1_id, item3_id, item4_id].contains(&id)
9992 })
9993 });
9994 cx.executor().run_until_parked();
9995
9996 assert!(cx.has_pending_prompt());
9997 cx.simulate_prompt_answer("Save all");
9998
9999 cx.executor().run_until_parked();
10000
10001 // Item 1 is saved. There's a prompt to save item 3.
10002 pane.update(cx, |pane, cx| {
10003 assert_eq!(item1.read(cx).save_count, 1);
10004 assert_eq!(item1.read(cx).save_as_count, 0);
10005 assert_eq!(item1.read(cx).reload_count, 0);
10006 assert_eq!(pane.items_len(), 3);
10007 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
10008 });
10009 assert!(cx.has_pending_prompt());
10010
10011 // Cancel saving item 3.
10012 cx.simulate_prompt_answer("Discard");
10013 cx.executor().run_until_parked();
10014
10015 // Item 3 is reloaded. There's a prompt to save item 4.
10016 pane.update(cx, |pane, cx| {
10017 assert_eq!(item3.read(cx).save_count, 0);
10018 assert_eq!(item3.read(cx).save_as_count, 0);
10019 assert_eq!(item3.read(cx).reload_count, 1);
10020 assert_eq!(pane.items_len(), 2);
10021 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
10022 });
10023
10024 // There's a prompt for a path for item 4.
10025 cx.simulate_new_path_selection(|_| Some(Default::default()));
10026 close_items.await.unwrap();
10027
10028 // The requested items are closed.
10029 pane.update(cx, |pane, cx| {
10030 assert_eq!(item4.read(cx).save_count, 0);
10031 assert_eq!(item4.read(cx).save_as_count, 1);
10032 assert_eq!(item4.read(cx).reload_count, 0);
10033 assert_eq!(pane.items_len(), 1);
10034 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
10035 });
10036 }
10037
10038 #[gpui::test]
10039 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
10040 init_test(cx);
10041
10042 let fs = FakeFs::new(cx.executor());
10043 let project = Project::test(fs, [], cx).await;
10044 let (workspace, cx) =
10045 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10046
10047 // Create several workspace items with single project entries, and two
10048 // workspace items with multiple project entries.
10049 let single_entry_items = (0..=4)
10050 .map(|project_entry_id| {
10051 cx.new(|cx| {
10052 TestItem::new(cx)
10053 .with_dirty(true)
10054 .with_project_items(&[dirty_project_item(
10055 project_entry_id,
10056 &format!("{project_entry_id}.txt"),
10057 cx,
10058 )])
10059 })
10060 })
10061 .collect::<Vec<_>>();
10062 let item_2_3 = cx.new(|cx| {
10063 TestItem::new(cx)
10064 .with_dirty(true)
10065 .with_buffer_kind(ItemBufferKind::Multibuffer)
10066 .with_project_items(&[
10067 single_entry_items[2].read(cx).project_items[0].clone(),
10068 single_entry_items[3].read(cx).project_items[0].clone(),
10069 ])
10070 });
10071 let item_3_4 = cx.new(|cx| {
10072 TestItem::new(cx)
10073 .with_dirty(true)
10074 .with_buffer_kind(ItemBufferKind::Multibuffer)
10075 .with_project_items(&[
10076 single_entry_items[3].read(cx).project_items[0].clone(),
10077 single_entry_items[4].read(cx).project_items[0].clone(),
10078 ])
10079 });
10080
10081 // Create two panes that contain the following project entries:
10082 // left pane:
10083 // multi-entry items: (2, 3)
10084 // single-entry items: 0, 2, 3, 4
10085 // right pane:
10086 // single-entry items: 4, 1
10087 // multi-entry items: (3, 4)
10088 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
10089 let left_pane = workspace.active_pane().clone();
10090 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
10091 workspace.add_item_to_active_pane(
10092 single_entry_items[0].boxed_clone(),
10093 None,
10094 true,
10095 window,
10096 cx,
10097 );
10098 workspace.add_item_to_active_pane(
10099 single_entry_items[2].boxed_clone(),
10100 None,
10101 true,
10102 window,
10103 cx,
10104 );
10105 workspace.add_item_to_active_pane(
10106 single_entry_items[3].boxed_clone(),
10107 None,
10108 true,
10109 window,
10110 cx,
10111 );
10112 workspace.add_item_to_active_pane(
10113 single_entry_items[4].boxed_clone(),
10114 None,
10115 true,
10116 window,
10117 cx,
10118 );
10119
10120 let right_pane =
10121 workspace.split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx);
10122
10123 let boxed_clone = single_entry_items[1].boxed_clone();
10124 let right_pane = window.spawn(cx, async move |cx| {
10125 right_pane.await.inspect(|right_pane| {
10126 right_pane
10127 .update_in(cx, |pane, window, cx| {
10128 pane.add_item(boxed_clone, true, true, None, window, cx);
10129 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
10130 })
10131 .unwrap();
10132 })
10133 });
10134
10135 (left_pane, right_pane)
10136 });
10137 let right_pane = right_pane.await.unwrap();
10138 cx.focus(&right_pane);
10139
10140 let close = right_pane.update_in(cx, |pane, window, cx| {
10141 pane.close_all_items(&CloseAllItems::default(), window, cx)
10142 .unwrap()
10143 });
10144 cx.executor().run_until_parked();
10145
10146 let msg = cx.pending_prompt().unwrap().0;
10147 assert!(msg.contains("1.txt"));
10148 assert!(!msg.contains("2.txt"));
10149 assert!(!msg.contains("3.txt"));
10150 assert!(!msg.contains("4.txt"));
10151
10152 // With best-effort close, cancelling item 1 keeps it open but items 4
10153 // and (3,4) still close since their entries exist in left pane.
10154 cx.simulate_prompt_answer("Cancel");
10155 close.await;
10156
10157 right_pane.read_with(cx, |pane, _| {
10158 assert_eq!(pane.items_len(), 1);
10159 });
10160
10161 // Remove item 3 from left pane, making (2,3) the only item with entry 3.
10162 left_pane
10163 .update_in(cx, |left_pane, window, cx| {
10164 left_pane.close_item_by_id(
10165 single_entry_items[3].entity_id(),
10166 SaveIntent::Skip,
10167 window,
10168 cx,
10169 )
10170 })
10171 .await
10172 .unwrap();
10173
10174 let close = left_pane.update_in(cx, |pane, window, cx| {
10175 pane.close_all_items(&CloseAllItems::default(), window, cx)
10176 .unwrap()
10177 });
10178 cx.executor().run_until_parked();
10179
10180 let details = cx.pending_prompt().unwrap().1;
10181 assert!(details.contains("0.txt"));
10182 assert!(details.contains("3.txt"));
10183 assert!(details.contains("4.txt"));
10184 // Ideally 2.txt wouldn't appear since entry 2 still exists in item 2.
10185 // But we can only save whole items, so saving (2,3) for entry 3 includes 2.
10186 // assert!(!details.contains("2.txt"));
10187
10188 cx.simulate_prompt_answer("Save all");
10189 cx.executor().run_until_parked();
10190 close.await;
10191
10192 left_pane.read_with(cx, |pane, _| {
10193 assert_eq!(pane.items_len(), 0);
10194 });
10195 }
10196
10197 #[gpui::test]
10198 async fn test_autosave(cx: &mut gpui::TestAppContext) {
10199 init_test(cx);
10200
10201 let fs = FakeFs::new(cx.executor());
10202 let project = Project::test(fs, [], cx).await;
10203 let (workspace, cx) =
10204 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10205 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10206
10207 let item = cx.new(|cx| {
10208 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10209 });
10210 let item_id = item.entity_id();
10211 workspace.update_in(cx, |workspace, window, cx| {
10212 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
10213 });
10214
10215 // Autosave on window change.
10216 item.update(cx, |item, cx| {
10217 SettingsStore::update_global(cx, |settings, cx| {
10218 settings.update_user_settings(cx, |settings| {
10219 settings.workspace.autosave = Some(AutosaveSetting::OnWindowChange);
10220 })
10221 });
10222 item.is_dirty = true;
10223 });
10224
10225 // Deactivating the window saves the file.
10226 cx.deactivate_window();
10227 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
10228
10229 // Re-activating the window doesn't save the file.
10230 cx.update(|window, _| window.activate_window());
10231 cx.executor().run_until_parked();
10232 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
10233
10234 // Autosave on focus change.
10235 item.update_in(cx, |item, window, cx| {
10236 cx.focus_self(window);
10237 SettingsStore::update_global(cx, |settings, cx| {
10238 settings.update_user_settings(cx, |settings| {
10239 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
10240 })
10241 });
10242 item.is_dirty = true;
10243 });
10244 // Blurring the item saves the file.
10245 item.update_in(cx, |_, window, _| window.blur());
10246 cx.executor().run_until_parked();
10247 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
10248
10249 // Deactivating the window still saves the file.
10250 item.update_in(cx, |item, window, cx| {
10251 cx.focus_self(window);
10252 item.is_dirty = true;
10253 });
10254 cx.deactivate_window();
10255 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
10256
10257 // Autosave after delay.
10258 item.update(cx, |item, cx| {
10259 SettingsStore::update_global(cx, |settings, cx| {
10260 settings.update_user_settings(cx, |settings| {
10261 settings.workspace.autosave = Some(AutosaveSetting::AfterDelay {
10262 milliseconds: 500.into(),
10263 });
10264 })
10265 });
10266 item.is_dirty = true;
10267 cx.emit(ItemEvent::Edit);
10268 });
10269
10270 // Delay hasn't fully expired, so the file is still dirty and unsaved.
10271 cx.executor().advance_clock(Duration::from_millis(250));
10272 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
10273
10274 // After delay expires, the file is saved.
10275 cx.executor().advance_clock(Duration::from_millis(250));
10276 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
10277
10278 // Autosave after delay, should save earlier than delay if tab is closed
10279 item.update(cx, |item, cx| {
10280 item.is_dirty = true;
10281 cx.emit(ItemEvent::Edit);
10282 });
10283 cx.executor().advance_clock(Duration::from_millis(250));
10284 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
10285
10286 // // Ensure auto save with delay saves the item on close, even if the timer hasn't yet run out.
10287 pane.update_in(cx, |pane, window, cx| {
10288 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
10289 })
10290 .await
10291 .unwrap();
10292 assert!(!cx.has_pending_prompt());
10293 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
10294
10295 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
10296 workspace.update_in(cx, |workspace, window, cx| {
10297 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
10298 });
10299 item.update_in(cx, |item, _window, cx| {
10300 item.is_dirty = true;
10301 for project_item in &mut item.project_items {
10302 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
10303 }
10304 });
10305 cx.run_until_parked();
10306 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
10307
10308 // Autosave on focus change, ensuring closing the tab counts as such.
10309 item.update(cx, |item, cx| {
10310 SettingsStore::update_global(cx, |settings, cx| {
10311 settings.update_user_settings(cx, |settings| {
10312 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
10313 })
10314 });
10315 item.is_dirty = true;
10316 for project_item in &mut item.project_items {
10317 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
10318 }
10319 });
10320
10321 pane.update_in(cx, |pane, window, cx| {
10322 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
10323 })
10324 .await
10325 .unwrap();
10326 assert!(!cx.has_pending_prompt());
10327 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
10328
10329 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
10330 workspace.update_in(cx, |workspace, window, cx| {
10331 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
10332 });
10333 item.update_in(cx, |item, window, cx| {
10334 item.project_items[0].update(cx, |item, _| {
10335 item.entry_id = None;
10336 });
10337 item.is_dirty = true;
10338 window.blur();
10339 });
10340 cx.run_until_parked();
10341 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
10342
10343 // Ensure autosave is prevented for deleted files also when closing the buffer.
10344 let _close_items = pane.update_in(cx, |pane, window, cx| {
10345 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
10346 });
10347 cx.run_until_parked();
10348 assert!(cx.has_pending_prompt());
10349 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
10350 }
10351
10352 #[gpui::test]
10353 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
10354 init_test(cx);
10355
10356 let fs = FakeFs::new(cx.executor());
10357
10358 let project = Project::test(fs, [], cx).await;
10359 let (workspace, cx) =
10360 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10361
10362 let item = cx.new(|cx| {
10363 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10364 });
10365 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10366 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
10367 let toolbar_notify_count = Rc::new(RefCell::new(0));
10368
10369 workspace.update_in(cx, |workspace, window, cx| {
10370 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
10371 let toolbar_notification_count = toolbar_notify_count.clone();
10372 cx.observe_in(&toolbar, window, move |_, _, _, _| {
10373 *toolbar_notification_count.borrow_mut() += 1
10374 })
10375 .detach();
10376 });
10377
10378 pane.read_with(cx, |pane, _| {
10379 assert!(!pane.can_navigate_backward());
10380 assert!(!pane.can_navigate_forward());
10381 });
10382
10383 item.update_in(cx, |item, _, cx| {
10384 item.set_state("one".to_string(), cx);
10385 });
10386
10387 // Toolbar must be notified to re-render the navigation buttons
10388 assert_eq!(*toolbar_notify_count.borrow(), 1);
10389
10390 pane.read_with(cx, |pane, _| {
10391 assert!(pane.can_navigate_backward());
10392 assert!(!pane.can_navigate_forward());
10393 });
10394
10395 workspace
10396 .update_in(cx, |workspace, window, cx| {
10397 workspace.go_back(pane.downgrade(), window, cx)
10398 })
10399 .await
10400 .unwrap();
10401
10402 assert_eq!(*toolbar_notify_count.borrow(), 2);
10403 pane.read_with(cx, |pane, _| {
10404 assert!(!pane.can_navigate_backward());
10405 assert!(pane.can_navigate_forward());
10406 });
10407 }
10408
10409 #[gpui::test]
10410 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
10411 init_test(cx);
10412 let fs = FakeFs::new(cx.executor());
10413
10414 let project = Project::test(fs, [], cx).await;
10415 let (workspace, cx) =
10416 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10417
10418 let panel = workspace.update_in(cx, |workspace, window, cx| {
10419 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
10420 workspace.add_panel(panel.clone(), window, cx);
10421
10422 workspace
10423 .right_dock()
10424 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
10425
10426 panel
10427 });
10428
10429 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10430 pane.update_in(cx, |pane, window, cx| {
10431 let item = cx.new(TestItem::new);
10432 pane.add_item(Box::new(item), true, true, None, window, cx);
10433 });
10434
10435 // Transfer focus from center to panel
10436 workspace.update_in(cx, |workspace, window, cx| {
10437 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10438 });
10439
10440 workspace.update_in(cx, |workspace, window, cx| {
10441 assert!(workspace.right_dock().read(cx).is_open());
10442 assert!(!panel.is_zoomed(window, cx));
10443 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10444 });
10445
10446 // Transfer focus from panel to center
10447 workspace.update_in(cx, |workspace, window, cx| {
10448 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10449 });
10450
10451 workspace.update_in(cx, |workspace, window, cx| {
10452 assert!(workspace.right_dock().read(cx).is_open());
10453 assert!(!panel.is_zoomed(window, cx));
10454 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10455 });
10456
10457 // Close the dock
10458 workspace.update_in(cx, |workspace, window, cx| {
10459 workspace.toggle_dock(DockPosition::Right, window, cx);
10460 });
10461
10462 workspace.update_in(cx, |workspace, window, cx| {
10463 assert!(!workspace.right_dock().read(cx).is_open());
10464 assert!(!panel.is_zoomed(window, cx));
10465 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10466 });
10467
10468 // Open the dock
10469 workspace.update_in(cx, |workspace, window, cx| {
10470 workspace.toggle_dock(DockPosition::Right, window, cx);
10471 });
10472
10473 workspace.update_in(cx, |workspace, window, cx| {
10474 assert!(workspace.right_dock().read(cx).is_open());
10475 assert!(!panel.is_zoomed(window, cx));
10476 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10477 });
10478
10479 // Focus and zoom panel
10480 panel.update_in(cx, |panel, window, cx| {
10481 cx.focus_self(window);
10482 panel.set_zoomed(true, window, cx)
10483 });
10484
10485 workspace.update_in(cx, |workspace, window, cx| {
10486 assert!(workspace.right_dock().read(cx).is_open());
10487 assert!(panel.is_zoomed(window, cx));
10488 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10489 });
10490
10491 // Transfer focus to the center closes the dock
10492 workspace.update_in(cx, |workspace, window, cx| {
10493 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10494 });
10495
10496 workspace.update_in(cx, |workspace, window, cx| {
10497 assert!(!workspace.right_dock().read(cx).is_open());
10498 assert!(panel.is_zoomed(window, cx));
10499 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10500 });
10501
10502 // Transferring focus back to the panel keeps it zoomed
10503 workspace.update_in(cx, |workspace, window, cx| {
10504 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10505 });
10506
10507 workspace.update_in(cx, |workspace, window, cx| {
10508 assert!(workspace.right_dock().read(cx).is_open());
10509 assert!(panel.is_zoomed(window, cx));
10510 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10511 });
10512
10513 // Close the dock while it is zoomed
10514 workspace.update_in(cx, |workspace, window, cx| {
10515 workspace.toggle_dock(DockPosition::Right, window, cx)
10516 });
10517
10518 workspace.update_in(cx, |workspace, window, cx| {
10519 assert!(!workspace.right_dock().read(cx).is_open());
10520 assert!(panel.is_zoomed(window, cx));
10521 assert!(workspace.zoomed.is_none());
10522 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10523 });
10524
10525 // Opening the dock, when it's zoomed, retains focus
10526 workspace.update_in(cx, |workspace, window, cx| {
10527 workspace.toggle_dock(DockPosition::Right, window, cx)
10528 });
10529
10530 workspace.update_in(cx, |workspace, window, cx| {
10531 assert!(workspace.right_dock().read(cx).is_open());
10532 assert!(panel.is_zoomed(window, cx));
10533 assert!(workspace.zoomed.is_some());
10534 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10535 });
10536
10537 // Unzoom and close the panel, zoom the active pane.
10538 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
10539 workspace.update_in(cx, |workspace, window, cx| {
10540 workspace.toggle_dock(DockPosition::Right, window, cx)
10541 });
10542 pane.update_in(cx, |pane, window, cx| {
10543 pane.toggle_zoom(&Default::default(), window, cx)
10544 });
10545
10546 // Opening a dock unzooms the pane.
10547 workspace.update_in(cx, |workspace, window, cx| {
10548 workspace.toggle_dock(DockPosition::Right, window, cx)
10549 });
10550 workspace.update_in(cx, |workspace, window, cx| {
10551 let pane = pane.read(cx);
10552 assert!(!pane.is_zoomed());
10553 assert!(!pane.focus_handle(cx).is_focused(window));
10554 assert!(workspace.right_dock().read(cx).is_open());
10555 assert!(workspace.zoomed.is_none());
10556 });
10557 }
10558
10559 #[gpui::test]
10560 async fn test_close_panel_on_toggle(cx: &mut gpui::TestAppContext) {
10561 init_test(cx);
10562 let fs = FakeFs::new(cx.executor());
10563
10564 let project = Project::test(fs, [], cx).await;
10565 let (workspace, cx) =
10566 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10567
10568 let panel = workspace.update_in(cx, |workspace, window, cx| {
10569 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
10570 workspace.add_panel(panel.clone(), window, cx);
10571 panel
10572 });
10573
10574 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10575 pane.update_in(cx, |pane, window, cx| {
10576 let item = cx.new(TestItem::new);
10577 pane.add_item(Box::new(item), true, true, None, window, cx);
10578 });
10579
10580 // Enable close_panel_on_toggle
10581 cx.update_global(|store: &mut SettingsStore, cx| {
10582 store.update_user_settings(cx, |settings| {
10583 settings.workspace.close_panel_on_toggle = Some(true);
10584 });
10585 });
10586
10587 // Panel starts closed. Toggling should open and focus it.
10588 workspace.update_in(cx, |workspace, window, cx| {
10589 assert!(!workspace.right_dock().read(cx).is_open());
10590 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10591 });
10592
10593 workspace.update_in(cx, |workspace, window, cx| {
10594 assert!(
10595 workspace.right_dock().read(cx).is_open(),
10596 "Dock should be open after toggling from center"
10597 );
10598 assert!(
10599 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
10600 "Panel should be focused after toggling from center"
10601 );
10602 });
10603
10604 // Panel is open and focused. Toggling should close the panel and
10605 // return focus to the center.
10606 workspace.update_in(cx, |workspace, window, cx| {
10607 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10608 });
10609
10610 workspace.update_in(cx, |workspace, window, cx| {
10611 assert!(
10612 !workspace.right_dock().read(cx).is_open(),
10613 "Dock should be closed after toggling from focused panel"
10614 );
10615 assert!(
10616 !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
10617 "Panel should not be focused after toggling from focused panel"
10618 );
10619 });
10620
10621 // Open the dock and focus something else so the panel is open but not
10622 // focused. Toggling should focus the panel (not close it).
10623 workspace.update_in(cx, |workspace, window, cx| {
10624 workspace
10625 .right_dock()
10626 .update(cx, |dock, cx| dock.set_open(true, window, cx));
10627 window.focus(&pane.read(cx).focus_handle(cx), cx);
10628 });
10629
10630 workspace.update_in(cx, |workspace, window, cx| {
10631 assert!(workspace.right_dock().read(cx).is_open());
10632 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10633 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10634 });
10635
10636 workspace.update_in(cx, |workspace, window, cx| {
10637 assert!(
10638 workspace.right_dock().read(cx).is_open(),
10639 "Dock should remain open when toggling focuses an open-but-unfocused panel"
10640 );
10641 assert!(
10642 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
10643 "Panel should be focused after toggling an open-but-unfocused panel"
10644 );
10645 });
10646
10647 // Now disable the setting and verify the original behavior: toggling
10648 // from a focused panel moves focus to center but leaves the dock open.
10649 cx.update_global(|store: &mut SettingsStore, cx| {
10650 store.update_user_settings(cx, |settings| {
10651 settings.workspace.close_panel_on_toggle = Some(false);
10652 });
10653 });
10654
10655 workspace.update_in(cx, |workspace, window, cx| {
10656 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10657 });
10658
10659 workspace.update_in(cx, |workspace, window, cx| {
10660 assert!(
10661 workspace.right_dock().read(cx).is_open(),
10662 "Dock should remain open when setting is disabled"
10663 );
10664 assert!(
10665 !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
10666 "Panel should not be focused after toggling with setting disabled"
10667 );
10668 });
10669 }
10670
10671 #[gpui::test]
10672 async fn test_pane_zoom_in_out(cx: &mut TestAppContext) {
10673 init_test(cx);
10674 let fs = FakeFs::new(cx.executor());
10675
10676 let project = Project::test(fs, [], cx).await;
10677 let (workspace, cx) =
10678 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10679
10680 let pane = workspace.update_in(cx, |workspace, _window, _cx| {
10681 workspace.active_pane().clone()
10682 });
10683
10684 // Add an item to the pane so it can be zoomed
10685 workspace.update_in(cx, |workspace, window, cx| {
10686 let item = cx.new(TestItem::new);
10687 workspace.add_item(pane.clone(), Box::new(item), None, true, true, window, cx);
10688 });
10689
10690 // Initially not zoomed
10691 workspace.update_in(cx, |workspace, _window, cx| {
10692 assert!(!pane.read(cx).is_zoomed(), "Pane starts unzoomed");
10693 assert!(
10694 workspace.zoomed.is_none(),
10695 "Workspace should track no zoomed pane"
10696 );
10697 assert!(pane.read(cx).items_len() > 0, "Pane should have items");
10698 });
10699
10700 // Zoom In
10701 pane.update_in(cx, |pane, window, cx| {
10702 pane.zoom_in(&crate::ZoomIn, window, cx);
10703 });
10704
10705 workspace.update_in(cx, |workspace, window, cx| {
10706 assert!(
10707 pane.read(cx).is_zoomed(),
10708 "Pane should be zoomed after ZoomIn"
10709 );
10710 assert!(
10711 workspace.zoomed.is_some(),
10712 "Workspace should track the zoomed pane"
10713 );
10714 assert!(
10715 pane.read(cx).focus_handle(cx).contains_focused(window, cx),
10716 "ZoomIn should focus the pane"
10717 );
10718 });
10719
10720 // Zoom In again is a no-op
10721 pane.update_in(cx, |pane, window, cx| {
10722 pane.zoom_in(&crate::ZoomIn, window, cx);
10723 });
10724
10725 workspace.update_in(cx, |workspace, window, cx| {
10726 assert!(pane.read(cx).is_zoomed(), "Second ZoomIn keeps pane zoomed");
10727 assert!(
10728 workspace.zoomed.is_some(),
10729 "Workspace still tracks zoomed pane"
10730 );
10731 assert!(
10732 pane.read(cx).focus_handle(cx).contains_focused(window, cx),
10733 "Pane remains focused after repeated ZoomIn"
10734 );
10735 });
10736
10737 // Zoom Out
10738 pane.update_in(cx, |pane, window, cx| {
10739 pane.zoom_out(&crate::ZoomOut, window, cx);
10740 });
10741
10742 workspace.update_in(cx, |workspace, _window, cx| {
10743 assert!(
10744 !pane.read(cx).is_zoomed(),
10745 "Pane should unzoom after ZoomOut"
10746 );
10747 assert!(
10748 workspace.zoomed.is_none(),
10749 "Workspace clears zoom tracking after ZoomOut"
10750 );
10751 });
10752
10753 // Zoom Out again is a no-op
10754 pane.update_in(cx, |pane, window, cx| {
10755 pane.zoom_out(&crate::ZoomOut, window, cx);
10756 });
10757
10758 workspace.update_in(cx, |workspace, _window, cx| {
10759 assert!(
10760 !pane.read(cx).is_zoomed(),
10761 "Second ZoomOut keeps pane unzoomed"
10762 );
10763 assert!(
10764 workspace.zoomed.is_none(),
10765 "Workspace remains without zoomed pane"
10766 );
10767 });
10768 }
10769
10770 #[gpui::test]
10771 async fn test_toggle_all_docks(cx: &mut gpui::TestAppContext) {
10772 init_test(cx);
10773 let fs = FakeFs::new(cx.executor());
10774
10775 let project = Project::test(fs, [], cx).await;
10776 let (workspace, cx) =
10777 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10778 workspace.update_in(cx, |workspace, window, cx| {
10779 // Open two docks
10780 let left_dock = workspace.dock_at_position(DockPosition::Left);
10781 let right_dock = workspace.dock_at_position(DockPosition::Right);
10782
10783 left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
10784 right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
10785
10786 assert!(left_dock.read(cx).is_open());
10787 assert!(right_dock.read(cx).is_open());
10788 });
10789
10790 workspace.update_in(cx, |workspace, window, cx| {
10791 // Toggle all docks - should close both
10792 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
10793
10794 let left_dock = workspace.dock_at_position(DockPosition::Left);
10795 let right_dock = workspace.dock_at_position(DockPosition::Right);
10796 assert!(!left_dock.read(cx).is_open());
10797 assert!(!right_dock.read(cx).is_open());
10798 });
10799
10800 workspace.update_in(cx, |workspace, window, cx| {
10801 // Toggle again - should reopen both
10802 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
10803
10804 let left_dock = workspace.dock_at_position(DockPosition::Left);
10805 let right_dock = workspace.dock_at_position(DockPosition::Right);
10806 assert!(left_dock.read(cx).is_open());
10807 assert!(right_dock.read(cx).is_open());
10808 });
10809 }
10810
10811 #[gpui::test]
10812 async fn test_toggle_all_with_manual_close(cx: &mut gpui::TestAppContext) {
10813 init_test(cx);
10814 let fs = FakeFs::new(cx.executor());
10815
10816 let project = Project::test(fs, [], cx).await;
10817 let (workspace, cx) =
10818 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10819 workspace.update_in(cx, |workspace, window, cx| {
10820 // Open two docks
10821 let left_dock = workspace.dock_at_position(DockPosition::Left);
10822 let right_dock = workspace.dock_at_position(DockPosition::Right);
10823
10824 left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
10825 right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
10826
10827 assert!(left_dock.read(cx).is_open());
10828 assert!(right_dock.read(cx).is_open());
10829 });
10830
10831 workspace.update_in(cx, |workspace, window, cx| {
10832 // Close them manually
10833 workspace.toggle_dock(DockPosition::Left, window, cx);
10834 workspace.toggle_dock(DockPosition::Right, window, cx);
10835
10836 let left_dock = workspace.dock_at_position(DockPosition::Left);
10837 let right_dock = workspace.dock_at_position(DockPosition::Right);
10838 assert!(!left_dock.read(cx).is_open());
10839 assert!(!right_dock.read(cx).is_open());
10840 });
10841
10842 workspace.update_in(cx, |workspace, window, cx| {
10843 // Toggle all docks - only last closed (right dock) should reopen
10844 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
10845
10846 let left_dock = workspace.dock_at_position(DockPosition::Left);
10847 let right_dock = workspace.dock_at_position(DockPosition::Right);
10848 assert!(!left_dock.read(cx).is_open());
10849 assert!(right_dock.read(cx).is_open());
10850 });
10851 }
10852
10853 #[gpui::test]
10854 async fn test_toggle_all_docks_after_dock_move(cx: &mut gpui::TestAppContext) {
10855 init_test(cx);
10856 let fs = FakeFs::new(cx.executor());
10857 let project = Project::test(fs, [], cx).await;
10858 let (workspace, cx) =
10859 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10860
10861 // Open two docks (left and right) with one panel each
10862 let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| {
10863 let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
10864 workspace.add_panel(left_panel.clone(), window, cx);
10865
10866 let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
10867 workspace.add_panel(right_panel.clone(), window, cx);
10868
10869 workspace.toggle_dock(DockPosition::Left, window, cx);
10870 workspace.toggle_dock(DockPosition::Right, window, cx);
10871
10872 // Verify initial state
10873 assert!(
10874 workspace.left_dock().read(cx).is_open(),
10875 "Left dock should be open"
10876 );
10877 assert_eq!(
10878 workspace
10879 .left_dock()
10880 .read(cx)
10881 .visible_panel()
10882 .unwrap()
10883 .panel_id(),
10884 left_panel.panel_id(),
10885 "Left panel should be visible in left dock"
10886 );
10887 assert!(
10888 workspace.right_dock().read(cx).is_open(),
10889 "Right dock should be open"
10890 );
10891 assert_eq!(
10892 workspace
10893 .right_dock()
10894 .read(cx)
10895 .visible_panel()
10896 .unwrap()
10897 .panel_id(),
10898 right_panel.panel_id(),
10899 "Right panel should be visible in right dock"
10900 );
10901 assert!(
10902 !workspace.bottom_dock().read(cx).is_open(),
10903 "Bottom dock should be closed"
10904 );
10905
10906 (left_panel, right_panel)
10907 });
10908
10909 // Focus the left panel and move it to the next position (bottom dock)
10910 workspace.update_in(cx, |workspace, window, cx| {
10911 workspace.toggle_panel_focus::<TestPanel>(window, cx); // Focus left panel
10912 assert!(
10913 left_panel.read(cx).focus_handle(cx).is_focused(window),
10914 "Left panel should be focused"
10915 );
10916 });
10917
10918 cx.dispatch_action(MoveFocusedPanelToNextPosition);
10919
10920 // Verify the left panel has moved to the bottom dock, and the bottom dock is now open
10921 workspace.update(cx, |workspace, cx| {
10922 assert!(
10923 !workspace.left_dock().read(cx).is_open(),
10924 "Left dock should be closed"
10925 );
10926 assert!(
10927 workspace.bottom_dock().read(cx).is_open(),
10928 "Bottom dock should now be open"
10929 );
10930 assert_eq!(
10931 left_panel.read(cx).position,
10932 DockPosition::Bottom,
10933 "Left panel should now be in the bottom dock"
10934 );
10935 assert_eq!(
10936 workspace
10937 .bottom_dock()
10938 .read(cx)
10939 .visible_panel()
10940 .unwrap()
10941 .panel_id(),
10942 left_panel.panel_id(),
10943 "Left panel should be the visible panel in the bottom dock"
10944 );
10945 });
10946
10947 // Toggle all docks off
10948 workspace.update_in(cx, |workspace, window, cx| {
10949 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
10950 assert!(
10951 !workspace.left_dock().read(cx).is_open(),
10952 "Left dock should be closed"
10953 );
10954 assert!(
10955 !workspace.right_dock().read(cx).is_open(),
10956 "Right dock should be closed"
10957 );
10958 assert!(
10959 !workspace.bottom_dock().read(cx).is_open(),
10960 "Bottom dock should be closed"
10961 );
10962 });
10963
10964 // Toggle all docks back on and verify positions are restored
10965 workspace.update_in(cx, |workspace, window, cx| {
10966 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
10967 assert!(
10968 !workspace.left_dock().read(cx).is_open(),
10969 "Left dock should remain closed"
10970 );
10971 assert!(
10972 workspace.right_dock().read(cx).is_open(),
10973 "Right dock should remain open"
10974 );
10975 assert!(
10976 workspace.bottom_dock().read(cx).is_open(),
10977 "Bottom dock should remain open"
10978 );
10979 assert_eq!(
10980 left_panel.read(cx).position,
10981 DockPosition::Bottom,
10982 "Left panel should remain in the bottom dock"
10983 );
10984 assert_eq!(
10985 right_panel.read(cx).position,
10986 DockPosition::Right,
10987 "Right panel should remain in the right dock"
10988 );
10989 assert_eq!(
10990 workspace
10991 .bottom_dock()
10992 .read(cx)
10993 .visible_panel()
10994 .unwrap()
10995 .panel_id(),
10996 left_panel.panel_id(),
10997 "Left panel should be the visible panel in the right dock"
10998 );
10999 });
11000 }
11001
11002 #[gpui::test]
11003 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
11004 init_test(cx);
11005
11006 let fs = FakeFs::new(cx.executor());
11007
11008 let project = Project::test(fs, None, cx).await;
11009 let (workspace, cx) =
11010 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11011
11012 // Let's arrange the panes like this:
11013 //
11014 // +-----------------------+
11015 // | top |
11016 // +------+--------+-------+
11017 // | left | center | right |
11018 // +------+--------+-------+
11019 // | bottom |
11020 // +-----------------------+
11021
11022 let top_item = cx.new(|cx| {
11023 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
11024 });
11025 let bottom_item = cx.new(|cx| {
11026 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
11027 });
11028 let left_item = cx.new(|cx| {
11029 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
11030 });
11031 let right_item = cx.new(|cx| {
11032 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
11033 });
11034 let center_item = cx.new(|cx| {
11035 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
11036 });
11037
11038 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11039 let top_pane_id = workspace.active_pane().entity_id();
11040 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
11041 workspace.split_pane(
11042 workspace.active_pane().clone(),
11043 SplitDirection::Down,
11044 window,
11045 cx,
11046 );
11047 top_pane_id
11048 });
11049 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11050 let bottom_pane_id = workspace.active_pane().entity_id();
11051 workspace.add_item_to_active_pane(
11052 Box::new(bottom_item.clone()),
11053 None,
11054 false,
11055 window,
11056 cx,
11057 );
11058 workspace.split_pane(
11059 workspace.active_pane().clone(),
11060 SplitDirection::Up,
11061 window,
11062 cx,
11063 );
11064 bottom_pane_id
11065 });
11066 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11067 let left_pane_id = workspace.active_pane().entity_id();
11068 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
11069 workspace.split_pane(
11070 workspace.active_pane().clone(),
11071 SplitDirection::Right,
11072 window,
11073 cx,
11074 );
11075 left_pane_id
11076 });
11077 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11078 let right_pane_id = workspace.active_pane().entity_id();
11079 workspace.add_item_to_active_pane(
11080 Box::new(right_item.clone()),
11081 None,
11082 false,
11083 window,
11084 cx,
11085 );
11086 workspace.split_pane(
11087 workspace.active_pane().clone(),
11088 SplitDirection::Left,
11089 window,
11090 cx,
11091 );
11092 right_pane_id
11093 });
11094 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11095 let center_pane_id = workspace.active_pane().entity_id();
11096 workspace.add_item_to_active_pane(
11097 Box::new(center_item.clone()),
11098 None,
11099 false,
11100 window,
11101 cx,
11102 );
11103 center_pane_id
11104 });
11105 cx.executor().run_until_parked();
11106
11107 workspace.update_in(cx, |workspace, window, cx| {
11108 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
11109
11110 // Join into next from center pane into right
11111 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
11112 });
11113
11114 workspace.update_in(cx, |workspace, window, cx| {
11115 let active_pane = workspace.active_pane();
11116 assert_eq!(right_pane_id, active_pane.entity_id());
11117 assert_eq!(2, active_pane.read(cx).items_len());
11118 let item_ids_in_pane =
11119 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
11120 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
11121 assert!(item_ids_in_pane.contains(&right_item.item_id()));
11122
11123 // Join into next from right pane into bottom
11124 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
11125 });
11126
11127 workspace.update_in(cx, |workspace, window, cx| {
11128 let active_pane = workspace.active_pane();
11129 assert_eq!(bottom_pane_id, active_pane.entity_id());
11130 assert_eq!(3, active_pane.read(cx).items_len());
11131 let item_ids_in_pane =
11132 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
11133 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
11134 assert!(item_ids_in_pane.contains(&right_item.item_id()));
11135 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
11136
11137 // Join into next from bottom pane into left
11138 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
11139 });
11140
11141 workspace.update_in(cx, |workspace, window, cx| {
11142 let active_pane = workspace.active_pane();
11143 assert_eq!(left_pane_id, active_pane.entity_id());
11144 assert_eq!(4, active_pane.read(cx).items_len());
11145 let item_ids_in_pane =
11146 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
11147 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
11148 assert!(item_ids_in_pane.contains(&right_item.item_id()));
11149 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
11150 assert!(item_ids_in_pane.contains(&left_item.item_id()));
11151
11152 // Join into next from left pane into top
11153 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
11154 });
11155
11156 workspace.update_in(cx, |workspace, window, cx| {
11157 let active_pane = workspace.active_pane();
11158 assert_eq!(top_pane_id, active_pane.entity_id());
11159 assert_eq!(5, active_pane.read(cx).items_len());
11160 let item_ids_in_pane =
11161 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
11162 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
11163 assert!(item_ids_in_pane.contains(&right_item.item_id()));
11164 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
11165 assert!(item_ids_in_pane.contains(&left_item.item_id()));
11166 assert!(item_ids_in_pane.contains(&top_item.item_id()));
11167
11168 // Single pane left: no-op
11169 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
11170 });
11171
11172 workspace.update(cx, |workspace, _cx| {
11173 let active_pane = workspace.active_pane();
11174 assert_eq!(top_pane_id, active_pane.entity_id());
11175 });
11176 }
11177
11178 fn add_an_item_to_active_pane(
11179 cx: &mut VisualTestContext,
11180 workspace: &Entity<Workspace>,
11181 item_id: u64,
11182 ) -> Entity<TestItem> {
11183 let item = cx.new(|cx| {
11184 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
11185 item_id,
11186 "item{item_id}.txt",
11187 cx,
11188 )])
11189 });
11190 workspace.update_in(cx, |workspace, window, cx| {
11191 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
11192 });
11193 item
11194 }
11195
11196 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
11197 workspace.update_in(cx, |workspace, window, cx| {
11198 workspace.split_pane(
11199 workspace.active_pane().clone(),
11200 SplitDirection::Right,
11201 window,
11202 cx,
11203 )
11204 })
11205 }
11206
11207 #[gpui::test]
11208 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
11209 init_test(cx);
11210 let fs = FakeFs::new(cx.executor());
11211 let project = Project::test(fs, None, cx).await;
11212 let (workspace, cx) =
11213 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11214
11215 add_an_item_to_active_pane(cx, &workspace, 1);
11216 split_pane(cx, &workspace);
11217 add_an_item_to_active_pane(cx, &workspace, 2);
11218 split_pane(cx, &workspace); // empty pane
11219 split_pane(cx, &workspace);
11220 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
11221
11222 cx.executor().run_until_parked();
11223
11224 workspace.update(cx, |workspace, cx| {
11225 let num_panes = workspace.panes().len();
11226 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
11227 let active_item = workspace
11228 .active_pane()
11229 .read(cx)
11230 .active_item()
11231 .expect("item is in focus");
11232
11233 assert_eq!(num_panes, 4);
11234 assert_eq!(num_items_in_current_pane, 1);
11235 assert_eq!(active_item.item_id(), last_item.item_id());
11236 });
11237
11238 workspace.update_in(cx, |workspace, window, cx| {
11239 workspace.join_all_panes(window, cx);
11240 });
11241
11242 workspace.update(cx, |workspace, cx| {
11243 let num_panes = workspace.panes().len();
11244 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
11245 let active_item = workspace
11246 .active_pane()
11247 .read(cx)
11248 .active_item()
11249 .expect("item is in focus");
11250
11251 assert_eq!(num_panes, 1);
11252 assert_eq!(num_items_in_current_pane, 3);
11253 assert_eq!(active_item.item_id(), last_item.item_id());
11254 });
11255 }
11256 struct TestModal(FocusHandle);
11257
11258 impl TestModal {
11259 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
11260 Self(cx.focus_handle())
11261 }
11262 }
11263
11264 impl EventEmitter<DismissEvent> for TestModal {}
11265
11266 impl Focusable for TestModal {
11267 fn focus_handle(&self, _cx: &App) -> FocusHandle {
11268 self.0.clone()
11269 }
11270 }
11271
11272 impl ModalView for TestModal {}
11273
11274 impl Render for TestModal {
11275 fn render(
11276 &mut self,
11277 _window: &mut Window,
11278 _cx: &mut Context<TestModal>,
11279 ) -> impl IntoElement {
11280 div().track_focus(&self.0)
11281 }
11282 }
11283
11284 #[gpui::test]
11285 async fn test_panels(cx: &mut gpui::TestAppContext) {
11286 init_test(cx);
11287 let fs = FakeFs::new(cx.executor());
11288
11289 let project = Project::test(fs, [], cx).await;
11290 let (workspace, cx) =
11291 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11292
11293 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
11294 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
11295 workspace.add_panel(panel_1.clone(), window, cx);
11296 workspace.toggle_dock(DockPosition::Left, window, cx);
11297 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
11298 workspace.add_panel(panel_2.clone(), window, cx);
11299 workspace.toggle_dock(DockPosition::Right, window, cx);
11300
11301 let left_dock = workspace.left_dock();
11302 assert_eq!(
11303 left_dock.read(cx).visible_panel().unwrap().panel_id(),
11304 panel_1.panel_id()
11305 );
11306 assert_eq!(
11307 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
11308 panel_1.size(window, cx)
11309 );
11310
11311 left_dock.update(cx, |left_dock, cx| {
11312 left_dock.resize_active_panel(Some(px(1337.)), window, cx)
11313 });
11314 assert_eq!(
11315 workspace
11316 .right_dock()
11317 .read(cx)
11318 .visible_panel()
11319 .unwrap()
11320 .panel_id(),
11321 panel_2.panel_id(),
11322 );
11323
11324 (panel_1, panel_2)
11325 });
11326
11327 // Move panel_1 to the right
11328 panel_1.update_in(cx, |panel_1, window, cx| {
11329 panel_1.set_position(DockPosition::Right, window, cx)
11330 });
11331
11332 workspace.update_in(cx, |workspace, window, cx| {
11333 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
11334 // Since it was the only panel on the left, the left dock should now be closed.
11335 assert!(!workspace.left_dock().read(cx).is_open());
11336 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
11337 let right_dock = workspace.right_dock();
11338 assert_eq!(
11339 right_dock.read(cx).visible_panel().unwrap().panel_id(),
11340 panel_1.panel_id()
11341 );
11342 assert_eq!(
11343 right_dock.read(cx).active_panel_size(window, cx).unwrap(),
11344 px(1337.)
11345 );
11346
11347 // Now we move panel_2 to the left
11348 panel_2.set_position(DockPosition::Left, window, cx);
11349 });
11350
11351 workspace.update(cx, |workspace, cx| {
11352 // Since panel_2 was not visible on the right, we don't open the left dock.
11353 assert!(!workspace.left_dock().read(cx).is_open());
11354 // And the right dock is unaffected in its displaying of panel_1
11355 assert!(workspace.right_dock().read(cx).is_open());
11356 assert_eq!(
11357 workspace
11358 .right_dock()
11359 .read(cx)
11360 .visible_panel()
11361 .unwrap()
11362 .panel_id(),
11363 panel_1.panel_id(),
11364 );
11365 });
11366
11367 // Move panel_1 back to the left
11368 panel_1.update_in(cx, |panel_1, window, cx| {
11369 panel_1.set_position(DockPosition::Left, window, cx)
11370 });
11371
11372 workspace.update_in(cx, |workspace, window, cx| {
11373 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
11374 let left_dock = workspace.left_dock();
11375 assert!(left_dock.read(cx).is_open());
11376 assert_eq!(
11377 left_dock.read(cx).visible_panel().unwrap().panel_id(),
11378 panel_1.panel_id()
11379 );
11380 assert_eq!(
11381 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
11382 px(1337.)
11383 );
11384 // And the right dock should be closed as it no longer has any panels.
11385 assert!(!workspace.right_dock().read(cx).is_open());
11386
11387 // Now we move panel_1 to the bottom
11388 panel_1.set_position(DockPosition::Bottom, window, cx);
11389 });
11390
11391 workspace.update_in(cx, |workspace, window, cx| {
11392 // Since panel_1 was visible on the left, we close the left dock.
11393 assert!(!workspace.left_dock().read(cx).is_open());
11394 // The bottom dock is sized based on the panel's default size,
11395 // since the panel orientation changed from vertical to horizontal.
11396 let bottom_dock = workspace.bottom_dock();
11397 assert_eq!(
11398 bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
11399 panel_1.size(window, cx),
11400 );
11401 // Close bottom dock and move panel_1 back to the left.
11402 bottom_dock.update(cx, |bottom_dock, cx| {
11403 bottom_dock.set_open(false, window, cx)
11404 });
11405 panel_1.set_position(DockPosition::Left, window, cx);
11406 });
11407
11408 // Emit activated event on panel 1
11409 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
11410
11411 // Now the left dock is open and panel_1 is active and focused.
11412 workspace.update_in(cx, |workspace, window, cx| {
11413 let left_dock = workspace.left_dock();
11414 assert!(left_dock.read(cx).is_open());
11415 assert_eq!(
11416 left_dock.read(cx).visible_panel().unwrap().panel_id(),
11417 panel_1.panel_id(),
11418 );
11419 assert!(panel_1.focus_handle(cx).is_focused(window));
11420 });
11421
11422 // Emit closed event on panel 2, which is not active
11423 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
11424
11425 // Wo don't close the left dock, because panel_2 wasn't the active panel
11426 workspace.update(cx, |workspace, cx| {
11427 let left_dock = workspace.left_dock();
11428 assert!(left_dock.read(cx).is_open());
11429 assert_eq!(
11430 left_dock.read(cx).visible_panel().unwrap().panel_id(),
11431 panel_1.panel_id(),
11432 );
11433 });
11434
11435 // Emitting a ZoomIn event shows the panel as zoomed.
11436 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
11437 workspace.read_with(cx, |workspace, _| {
11438 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
11439 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
11440 });
11441
11442 // Move panel to another dock while it is zoomed
11443 panel_1.update_in(cx, |panel, window, cx| {
11444 panel.set_position(DockPosition::Right, window, cx)
11445 });
11446 workspace.read_with(cx, |workspace, _| {
11447 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
11448
11449 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
11450 });
11451
11452 // This is a helper for getting a:
11453 // - valid focus on an element,
11454 // - that isn't a part of the panes and panels system of the Workspace,
11455 // - and doesn't trigger the 'on_focus_lost' API.
11456 let focus_other_view = {
11457 let workspace = workspace.clone();
11458 move |cx: &mut VisualTestContext| {
11459 workspace.update_in(cx, |workspace, window, cx| {
11460 if workspace.active_modal::<TestModal>(cx).is_some() {
11461 workspace.toggle_modal(window, cx, TestModal::new);
11462 workspace.toggle_modal(window, cx, TestModal::new);
11463 } else {
11464 workspace.toggle_modal(window, cx, TestModal::new);
11465 }
11466 })
11467 }
11468 };
11469
11470 // If focus is transferred to another view that's not a panel or another pane, we still show
11471 // the panel as zoomed.
11472 focus_other_view(cx);
11473 workspace.read_with(cx, |workspace, _| {
11474 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
11475 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
11476 });
11477
11478 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
11479 workspace.update_in(cx, |_workspace, window, cx| {
11480 cx.focus_self(window);
11481 });
11482 workspace.read_with(cx, |workspace, _| {
11483 assert_eq!(workspace.zoomed, None);
11484 assert_eq!(workspace.zoomed_position, None);
11485 });
11486
11487 // If focus is transferred again to another view that's not a panel or a pane, we won't
11488 // show the panel as zoomed because it wasn't zoomed before.
11489 focus_other_view(cx);
11490 workspace.read_with(cx, |workspace, _| {
11491 assert_eq!(workspace.zoomed, None);
11492 assert_eq!(workspace.zoomed_position, None);
11493 });
11494
11495 // When the panel is activated, it is zoomed again.
11496 cx.dispatch_action(ToggleRightDock);
11497 workspace.read_with(cx, |workspace, _| {
11498 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
11499 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
11500 });
11501
11502 // Emitting a ZoomOut event unzooms the panel.
11503 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
11504 workspace.read_with(cx, |workspace, _| {
11505 assert_eq!(workspace.zoomed, None);
11506 assert_eq!(workspace.zoomed_position, None);
11507 });
11508
11509 // Emit closed event on panel 1, which is active
11510 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
11511
11512 // Now the left dock is closed, because panel_1 was the active panel
11513 workspace.update(cx, |workspace, cx| {
11514 let right_dock = workspace.right_dock();
11515 assert!(!right_dock.read(cx).is_open());
11516 });
11517 }
11518
11519 #[gpui::test]
11520 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
11521 init_test(cx);
11522
11523 let fs = FakeFs::new(cx.background_executor.clone());
11524 let project = Project::test(fs, [], cx).await;
11525 let (workspace, cx) =
11526 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11527 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11528
11529 let dirty_regular_buffer = cx.new(|cx| {
11530 TestItem::new(cx)
11531 .with_dirty(true)
11532 .with_label("1.txt")
11533 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
11534 });
11535 let dirty_regular_buffer_2 = cx.new(|cx| {
11536 TestItem::new(cx)
11537 .with_dirty(true)
11538 .with_label("2.txt")
11539 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
11540 });
11541 let dirty_multi_buffer_with_both = cx.new(|cx| {
11542 TestItem::new(cx)
11543 .with_dirty(true)
11544 .with_buffer_kind(ItemBufferKind::Multibuffer)
11545 .with_label("Fake Project Search")
11546 .with_project_items(&[
11547 dirty_regular_buffer.read(cx).project_items[0].clone(),
11548 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
11549 ])
11550 });
11551 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
11552 workspace.update_in(cx, |workspace, window, cx| {
11553 workspace.add_item(
11554 pane.clone(),
11555 Box::new(dirty_regular_buffer.clone()),
11556 None,
11557 false,
11558 false,
11559 window,
11560 cx,
11561 );
11562 workspace.add_item(
11563 pane.clone(),
11564 Box::new(dirty_regular_buffer_2.clone()),
11565 None,
11566 false,
11567 false,
11568 window,
11569 cx,
11570 );
11571 workspace.add_item(
11572 pane.clone(),
11573 Box::new(dirty_multi_buffer_with_both.clone()),
11574 None,
11575 false,
11576 false,
11577 window,
11578 cx,
11579 );
11580 });
11581
11582 pane.update_in(cx, |pane, window, cx| {
11583 pane.activate_item(2, true, true, window, cx);
11584 assert_eq!(
11585 pane.active_item().unwrap().item_id(),
11586 multi_buffer_with_both_files_id,
11587 "Should select the multi buffer in the pane"
11588 );
11589 });
11590 let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
11591 pane.close_other_items(
11592 &CloseOtherItems {
11593 save_intent: Some(SaveIntent::Save),
11594 close_pinned: true,
11595 },
11596 None,
11597 window,
11598 cx,
11599 )
11600 });
11601 cx.background_executor.run_until_parked();
11602 assert!(!cx.has_pending_prompt());
11603 close_all_but_multi_buffer_task
11604 .await
11605 .expect("Closing all buffers but the multi buffer failed");
11606 pane.update(cx, |pane, cx| {
11607 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
11608 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
11609 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
11610 assert_eq!(pane.items_len(), 1);
11611 assert_eq!(
11612 pane.active_item().unwrap().item_id(),
11613 multi_buffer_with_both_files_id,
11614 "Should have only the multi buffer left in the pane"
11615 );
11616 assert!(
11617 dirty_multi_buffer_with_both.read(cx).is_dirty,
11618 "The multi buffer containing the unsaved buffer should still be dirty"
11619 );
11620 });
11621
11622 dirty_regular_buffer.update(cx, |buffer, cx| {
11623 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
11624 });
11625
11626 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
11627 pane.close_active_item(
11628 &CloseActiveItem {
11629 save_intent: Some(SaveIntent::Close),
11630 close_pinned: false,
11631 },
11632 window,
11633 cx,
11634 )
11635 });
11636 cx.background_executor.run_until_parked();
11637 assert!(
11638 cx.has_pending_prompt(),
11639 "Dirty multi buffer should prompt a save dialog"
11640 );
11641 cx.simulate_prompt_answer("Save");
11642 cx.background_executor.run_until_parked();
11643 close_multi_buffer_task
11644 .await
11645 .expect("Closing the multi buffer failed");
11646 pane.update(cx, |pane, cx| {
11647 assert_eq!(
11648 dirty_multi_buffer_with_both.read(cx).save_count,
11649 1,
11650 "Multi buffer item should get be saved"
11651 );
11652 // Test impl does not save inner items, so we do not assert them
11653 assert_eq!(
11654 pane.items_len(),
11655 0,
11656 "No more items should be left in the pane"
11657 );
11658 assert!(pane.active_item().is_none());
11659 });
11660 }
11661
11662 #[gpui::test]
11663 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
11664 cx: &mut TestAppContext,
11665 ) {
11666 init_test(cx);
11667
11668 let fs = FakeFs::new(cx.background_executor.clone());
11669 let project = Project::test(fs, [], cx).await;
11670 let (workspace, cx) =
11671 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11672 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11673
11674 let dirty_regular_buffer = cx.new(|cx| {
11675 TestItem::new(cx)
11676 .with_dirty(true)
11677 .with_label("1.txt")
11678 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
11679 });
11680 let dirty_regular_buffer_2 = cx.new(|cx| {
11681 TestItem::new(cx)
11682 .with_dirty(true)
11683 .with_label("2.txt")
11684 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
11685 });
11686 let clear_regular_buffer = cx.new(|cx| {
11687 TestItem::new(cx)
11688 .with_label("3.txt")
11689 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
11690 });
11691
11692 let dirty_multi_buffer_with_both = cx.new(|cx| {
11693 TestItem::new(cx)
11694 .with_dirty(true)
11695 .with_buffer_kind(ItemBufferKind::Multibuffer)
11696 .with_label("Fake Project Search")
11697 .with_project_items(&[
11698 dirty_regular_buffer.read(cx).project_items[0].clone(),
11699 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
11700 clear_regular_buffer.read(cx).project_items[0].clone(),
11701 ])
11702 });
11703 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
11704 workspace.update_in(cx, |workspace, window, cx| {
11705 workspace.add_item(
11706 pane.clone(),
11707 Box::new(dirty_regular_buffer.clone()),
11708 None,
11709 false,
11710 false,
11711 window,
11712 cx,
11713 );
11714 workspace.add_item(
11715 pane.clone(),
11716 Box::new(dirty_multi_buffer_with_both.clone()),
11717 None,
11718 false,
11719 false,
11720 window,
11721 cx,
11722 );
11723 });
11724
11725 pane.update_in(cx, |pane, window, cx| {
11726 pane.activate_item(1, true, true, window, cx);
11727 assert_eq!(
11728 pane.active_item().unwrap().item_id(),
11729 multi_buffer_with_both_files_id,
11730 "Should select the multi buffer in the pane"
11731 );
11732 });
11733 let _close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
11734 pane.close_active_item(
11735 &CloseActiveItem {
11736 save_intent: None,
11737 close_pinned: false,
11738 },
11739 window,
11740 cx,
11741 )
11742 });
11743 cx.background_executor.run_until_parked();
11744 assert!(
11745 cx.has_pending_prompt(),
11746 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
11747 );
11748 }
11749
11750 /// Tests that when `close_on_file_delete` is enabled, files are automatically
11751 /// closed when they are deleted from disk.
11752 #[gpui::test]
11753 async fn test_close_on_disk_deletion_enabled(cx: &mut TestAppContext) {
11754 init_test(cx);
11755
11756 // Enable the close_on_disk_deletion setting
11757 cx.update_global(|store: &mut SettingsStore, cx| {
11758 store.update_user_settings(cx, |settings| {
11759 settings.workspace.close_on_file_delete = Some(true);
11760 });
11761 });
11762
11763 let fs = FakeFs::new(cx.background_executor.clone());
11764 let project = Project::test(fs, [], cx).await;
11765 let (workspace, cx) =
11766 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11767 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11768
11769 // Create a test item that simulates a file
11770 let item = cx.new(|cx| {
11771 TestItem::new(cx)
11772 .with_label("test.txt")
11773 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
11774 });
11775
11776 // Add item to workspace
11777 workspace.update_in(cx, |workspace, window, cx| {
11778 workspace.add_item(
11779 pane.clone(),
11780 Box::new(item.clone()),
11781 None,
11782 false,
11783 false,
11784 window,
11785 cx,
11786 );
11787 });
11788
11789 // Verify the item is in the pane
11790 pane.read_with(cx, |pane, _| {
11791 assert_eq!(pane.items().count(), 1);
11792 });
11793
11794 // Simulate file deletion by setting the item's deleted state
11795 item.update(cx, |item, _| {
11796 item.set_has_deleted_file(true);
11797 });
11798
11799 // Emit UpdateTab event to trigger the close behavior
11800 cx.run_until_parked();
11801 item.update(cx, |_, cx| {
11802 cx.emit(ItemEvent::UpdateTab);
11803 });
11804
11805 // Allow the close operation to complete
11806 cx.run_until_parked();
11807
11808 // Verify the item was automatically closed
11809 pane.read_with(cx, |pane, _| {
11810 assert_eq!(
11811 pane.items().count(),
11812 0,
11813 "Item should be automatically closed when file is deleted"
11814 );
11815 });
11816 }
11817
11818 /// Tests that when `close_on_file_delete` is disabled (default), files remain
11819 /// open with a strikethrough when they are deleted from disk.
11820 #[gpui::test]
11821 async fn test_close_on_disk_deletion_disabled(cx: &mut TestAppContext) {
11822 init_test(cx);
11823
11824 // Ensure close_on_disk_deletion is disabled (default)
11825 cx.update_global(|store: &mut SettingsStore, cx| {
11826 store.update_user_settings(cx, |settings| {
11827 settings.workspace.close_on_file_delete = Some(false);
11828 });
11829 });
11830
11831 let fs = FakeFs::new(cx.background_executor.clone());
11832 let project = Project::test(fs, [], cx).await;
11833 let (workspace, cx) =
11834 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11835 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11836
11837 // Create a test item that simulates a file
11838 let item = cx.new(|cx| {
11839 TestItem::new(cx)
11840 .with_label("test.txt")
11841 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
11842 });
11843
11844 // Add item to workspace
11845 workspace.update_in(cx, |workspace, window, cx| {
11846 workspace.add_item(
11847 pane.clone(),
11848 Box::new(item.clone()),
11849 None,
11850 false,
11851 false,
11852 window,
11853 cx,
11854 );
11855 });
11856
11857 // Verify the item is in the pane
11858 pane.read_with(cx, |pane, _| {
11859 assert_eq!(pane.items().count(), 1);
11860 });
11861
11862 // Simulate file deletion
11863 item.update(cx, |item, _| {
11864 item.set_has_deleted_file(true);
11865 });
11866
11867 // Emit UpdateTab event
11868 cx.run_until_parked();
11869 item.update(cx, |_, cx| {
11870 cx.emit(ItemEvent::UpdateTab);
11871 });
11872
11873 // Allow any potential close operation to complete
11874 cx.run_until_parked();
11875
11876 // Verify the item remains open (with strikethrough)
11877 pane.read_with(cx, |pane, _| {
11878 assert_eq!(
11879 pane.items().count(),
11880 1,
11881 "Item should remain open when close_on_disk_deletion is disabled"
11882 );
11883 });
11884
11885 // Verify the item shows as deleted
11886 item.read_with(cx, |item, _| {
11887 assert!(
11888 item.has_deleted_file,
11889 "Item should be marked as having deleted file"
11890 );
11891 });
11892 }
11893
11894 /// Tests that dirty files are not automatically closed when deleted from disk,
11895 /// even when `close_on_file_delete` is enabled. This ensures users don't lose
11896 /// unsaved changes without being prompted.
11897 #[gpui::test]
11898 async fn test_close_on_disk_deletion_with_dirty_file(cx: &mut TestAppContext) {
11899 init_test(cx);
11900
11901 // Enable the close_on_file_delete setting
11902 cx.update_global(|store: &mut SettingsStore, cx| {
11903 store.update_user_settings(cx, |settings| {
11904 settings.workspace.close_on_file_delete = Some(true);
11905 });
11906 });
11907
11908 let fs = FakeFs::new(cx.background_executor.clone());
11909 let project = Project::test(fs, [], cx).await;
11910 let (workspace, cx) =
11911 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11912 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11913
11914 // Create a dirty test item
11915 let item = cx.new(|cx| {
11916 TestItem::new(cx)
11917 .with_dirty(true)
11918 .with_label("test.txt")
11919 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
11920 });
11921
11922 // Add item to workspace
11923 workspace.update_in(cx, |workspace, window, cx| {
11924 workspace.add_item(
11925 pane.clone(),
11926 Box::new(item.clone()),
11927 None,
11928 false,
11929 false,
11930 window,
11931 cx,
11932 );
11933 });
11934
11935 // Simulate file deletion
11936 item.update(cx, |item, _| {
11937 item.set_has_deleted_file(true);
11938 });
11939
11940 // Emit UpdateTab event to trigger the close behavior
11941 cx.run_until_parked();
11942 item.update(cx, |_, cx| {
11943 cx.emit(ItemEvent::UpdateTab);
11944 });
11945
11946 // Allow any potential close operation to complete
11947 cx.run_until_parked();
11948
11949 // Verify the item remains open (dirty files are not auto-closed)
11950 pane.read_with(cx, |pane, _| {
11951 assert_eq!(
11952 pane.items().count(),
11953 1,
11954 "Dirty items should not be automatically closed even when file is deleted"
11955 );
11956 });
11957
11958 // Verify the item is marked as deleted and still dirty
11959 item.read_with(cx, |item, _| {
11960 assert!(
11961 item.has_deleted_file,
11962 "Item should be marked as having deleted file"
11963 );
11964 assert!(item.is_dirty, "Item should still be dirty");
11965 });
11966 }
11967
11968 /// Tests that navigation history is cleaned up when files are auto-closed
11969 /// due to deletion from disk.
11970 #[gpui::test]
11971 async fn test_close_on_disk_deletion_cleans_navigation_history(cx: &mut TestAppContext) {
11972 init_test(cx);
11973
11974 // Enable the close_on_file_delete setting
11975 cx.update_global(|store: &mut SettingsStore, cx| {
11976 store.update_user_settings(cx, |settings| {
11977 settings.workspace.close_on_file_delete = Some(true);
11978 });
11979 });
11980
11981 let fs = FakeFs::new(cx.background_executor.clone());
11982 let project = Project::test(fs, [], cx).await;
11983 let (workspace, cx) =
11984 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11985 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11986
11987 // Create test items
11988 let item1 = cx.new(|cx| {
11989 TestItem::new(cx)
11990 .with_label("test1.txt")
11991 .with_project_items(&[TestProjectItem::new(1, "test1.txt", cx)])
11992 });
11993 let item1_id = item1.item_id();
11994
11995 let item2 = cx.new(|cx| {
11996 TestItem::new(cx)
11997 .with_label("test2.txt")
11998 .with_project_items(&[TestProjectItem::new(2, "test2.txt", cx)])
11999 });
12000
12001 // Add items to workspace
12002 workspace.update_in(cx, |workspace, window, cx| {
12003 workspace.add_item(
12004 pane.clone(),
12005 Box::new(item1.clone()),
12006 None,
12007 false,
12008 false,
12009 window,
12010 cx,
12011 );
12012 workspace.add_item(
12013 pane.clone(),
12014 Box::new(item2.clone()),
12015 None,
12016 false,
12017 false,
12018 window,
12019 cx,
12020 );
12021 });
12022
12023 // Activate item1 to ensure it gets navigation entries
12024 pane.update_in(cx, |pane, window, cx| {
12025 pane.activate_item(0, true, true, window, cx);
12026 });
12027
12028 // Switch to item2 and back to create navigation history
12029 pane.update_in(cx, |pane, window, cx| {
12030 pane.activate_item(1, true, true, window, cx);
12031 });
12032 cx.run_until_parked();
12033
12034 pane.update_in(cx, |pane, window, cx| {
12035 pane.activate_item(0, true, true, window, cx);
12036 });
12037 cx.run_until_parked();
12038
12039 // Simulate file deletion for item1
12040 item1.update(cx, |item, _| {
12041 item.set_has_deleted_file(true);
12042 });
12043
12044 // Emit UpdateTab event to trigger the close behavior
12045 item1.update(cx, |_, cx| {
12046 cx.emit(ItemEvent::UpdateTab);
12047 });
12048 cx.run_until_parked();
12049
12050 // Verify item1 was closed
12051 pane.read_with(cx, |pane, _| {
12052 assert_eq!(
12053 pane.items().count(),
12054 1,
12055 "Should have 1 item remaining after auto-close"
12056 );
12057 });
12058
12059 // Check navigation history after close
12060 let has_item = pane.read_with(cx, |pane, cx| {
12061 let mut has_item = false;
12062 pane.nav_history().for_each_entry(cx, |entry, _| {
12063 if entry.item.id() == item1_id {
12064 has_item = true;
12065 }
12066 });
12067 has_item
12068 });
12069
12070 assert!(
12071 !has_item,
12072 "Navigation history should not contain closed item entries"
12073 );
12074 }
12075
12076 #[gpui::test]
12077 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
12078 cx: &mut TestAppContext,
12079 ) {
12080 init_test(cx);
12081
12082 let fs = FakeFs::new(cx.background_executor.clone());
12083 let project = Project::test(fs, [], cx).await;
12084 let (workspace, cx) =
12085 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12086 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12087
12088 let dirty_regular_buffer = cx.new(|cx| {
12089 TestItem::new(cx)
12090 .with_dirty(true)
12091 .with_label("1.txt")
12092 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
12093 });
12094 let dirty_regular_buffer_2 = cx.new(|cx| {
12095 TestItem::new(cx)
12096 .with_dirty(true)
12097 .with_label("2.txt")
12098 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
12099 });
12100 let clear_regular_buffer = cx.new(|cx| {
12101 TestItem::new(cx)
12102 .with_label("3.txt")
12103 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
12104 });
12105
12106 let dirty_multi_buffer = cx.new(|cx| {
12107 TestItem::new(cx)
12108 .with_dirty(true)
12109 .with_buffer_kind(ItemBufferKind::Multibuffer)
12110 .with_label("Fake Project Search")
12111 .with_project_items(&[
12112 dirty_regular_buffer.read(cx).project_items[0].clone(),
12113 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
12114 clear_regular_buffer.read(cx).project_items[0].clone(),
12115 ])
12116 });
12117 workspace.update_in(cx, |workspace, window, cx| {
12118 workspace.add_item(
12119 pane.clone(),
12120 Box::new(dirty_regular_buffer.clone()),
12121 None,
12122 false,
12123 false,
12124 window,
12125 cx,
12126 );
12127 workspace.add_item(
12128 pane.clone(),
12129 Box::new(dirty_regular_buffer_2.clone()),
12130 None,
12131 false,
12132 false,
12133 window,
12134 cx,
12135 );
12136 workspace.add_item(
12137 pane.clone(),
12138 Box::new(dirty_multi_buffer.clone()),
12139 None,
12140 false,
12141 false,
12142 window,
12143 cx,
12144 );
12145 });
12146
12147 pane.update_in(cx, |pane, window, cx| {
12148 pane.activate_item(2, true, true, window, cx);
12149 assert_eq!(
12150 pane.active_item().unwrap().item_id(),
12151 dirty_multi_buffer.item_id(),
12152 "Should select the multi buffer in the pane"
12153 );
12154 });
12155 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
12156 pane.close_active_item(
12157 &CloseActiveItem {
12158 save_intent: None,
12159 close_pinned: false,
12160 },
12161 window,
12162 cx,
12163 )
12164 });
12165 cx.background_executor.run_until_parked();
12166 assert!(
12167 !cx.has_pending_prompt(),
12168 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
12169 );
12170 close_multi_buffer_task
12171 .await
12172 .expect("Closing multi buffer failed");
12173 pane.update(cx, |pane, cx| {
12174 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
12175 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
12176 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
12177 assert_eq!(
12178 pane.items()
12179 .map(|item| item.item_id())
12180 .sorted()
12181 .collect::<Vec<_>>(),
12182 vec![
12183 dirty_regular_buffer.item_id(),
12184 dirty_regular_buffer_2.item_id(),
12185 ],
12186 "Should have no multi buffer left in the pane"
12187 );
12188 assert!(dirty_regular_buffer.read(cx).is_dirty);
12189 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
12190 });
12191 }
12192
12193 #[gpui::test]
12194 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
12195 init_test(cx);
12196 let fs = FakeFs::new(cx.executor());
12197 let project = Project::test(fs, [], cx).await;
12198 let (workspace, cx) =
12199 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12200
12201 // Add a new panel to the right dock, opening the dock and setting the
12202 // focus to the new panel.
12203 let panel = workspace.update_in(cx, |workspace, window, cx| {
12204 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
12205 workspace.add_panel(panel.clone(), window, cx);
12206
12207 workspace
12208 .right_dock()
12209 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
12210
12211 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12212
12213 panel
12214 });
12215
12216 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
12217 // panel to the next valid position which, in this case, is the left
12218 // dock.
12219 cx.dispatch_action(MoveFocusedPanelToNextPosition);
12220 workspace.update(cx, |workspace, cx| {
12221 assert!(workspace.left_dock().read(cx).is_open());
12222 assert_eq!(panel.read(cx).position, DockPosition::Left);
12223 });
12224
12225 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
12226 // panel to the next valid position which, in this case, is the bottom
12227 // dock.
12228 cx.dispatch_action(MoveFocusedPanelToNextPosition);
12229 workspace.update(cx, |workspace, cx| {
12230 assert!(workspace.bottom_dock().read(cx).is_open());
12231 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
12232 });
12233
12234 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
12235 // around moving the panel to its initial position, the right dock.
12236 cx.dispatch_action(MoveFocusedPanelToNextPosition);
12237 workspace.update(cx, |workspace, cx| {
12238 assert!(workspace.right_dock().read(cx).is_open());
12239 assert_eq!(panel.read(cx).position, DockPosition::Right);
12240 });
12241
12242 // Remove focus from the panel, ensuring that, if the panel is not
12243 // focused, the `MoveFocusedPanelToNextPosition` action does not update
12244 // the panel's position, so the panel is still in the right dock.
12245 workspace.update_in(cx, |workspace, window, cx| {
12246 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12247 });
12248
12249 cx.dispatch_action(MoveFocusedPanelToNextPosition);
12250 workspace.update(cx, |workspace, cx| {
12251 assert!(workspace.right_dock().read(cx).is_open());
12252 assert_eq!(panel.read(cx).position, DockPosition::Right);
12253 });
12254 }
12255
12256 #[gpui::test]
12257 async fn test_moving_items_create_panes(cx: &mut TestAppContext) {
12258 init_test(cx);
12259
12260 let fs = FakeFs::new(cx.executor());
12261 let project = Project::test(fs, [], cx).await;
12262 let (workspace, cx) =
12263 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
12264
12265 let item_1 = cx.new(|cx| {
12266 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
12267 });
12268 workspace.update_in(cx, |workspace, window, cx| {
12269 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
12270 workspace.move_item_to_pane_in_direction(
12271 &MoveItemToPaneInDirection {
12272 direction: SplitDirection::Right,
12273 focus: true,
12274 clone: false,
12275 },
12276 window,
12277 cx,
12278 );
12279 workspace.move_item_to_pane_at_index(
12280 &MoveItemToPane {
12281 destination: 3,
12282 focus: true,
12283 clone: false,
12284 },
12285 window,
12286 cx,
12287 );
12288
12289 assert_eq!(workspace.panes.len(), 1, "No new panes were created");
12290 assert_eq!(
12291 pane_items_paths(&workspace.active_pane, cx),
12292 vec!["first.txt".to_string()],
12293 "Single item was not moved anywhere"
12294 );
12295 });
12296
12297 let item_2 = cx.new(|cx| {
12298 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "second.txt", cx)])
12299 });
12300 workspace.update_in(cx, |workspace, window, cx| {
12301 workspace.add_item_to_active_pane(Box::new(item_2), None, true, window, cx);
12302 assert_eq!(
12303 pane_items_paths(&workspace.panes[0], cx),
12304 vec!["first.txt".to_string(), "second.txt".to_string()],
12305 );
12306 workspace.move_item_to_pane_in_direction(
12307 &MoveItemToPaneInDirection {
12308 direction: SplitDirection::Right,
12309 focus: true,
12310 clone: false,
12311 },
12312 window,
12313 cx,
12314 );
12315
12316 assert_eq!(workspace.panes.len(), 2, "A new pane should be created");
12317 assert_eq!(
12318 pane_items_paths(&workspace.panes[0], cx),
12319 vec!["first.txt".to_string()],
12320 "After moving, one item should be left in the original pane"
12321 );
12322 assert_eq!(
12323 pane_items_paths(&workspace.panes[1], cx),
12324 vec!["second.txt".to_string()],
12325 "New item should have been moved to the new pane"
12326 );
12327 });
12328
12329 let item_3 = cx.new(|cx| {
12330 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "third.txt", cx)])
12331 });
12332 workspace.update_in(cx, |workspace, window, cx| {
12333 let original_pane = workspace.panes[0].clone();
12334 workspace.set_active_pane(&original_pane, window, cx);
12335 workspace.add_item_to_active_pane(Box::new(item_3), None, true, window, cx);
12336 assert_eq!(workspace.panes.len(), 2, "No new panes were created");
12337 assert_eq!(
12338 pane_items_paths(&workspace.active_pane, cx),
12339 vec!["first.txt".to_string(), "third.txt".to_string()],
12340 "New pane should be ready to move one item out"
12341 );
12342
12343 workspace.move_item_to_pane_at_index(
12344 &MoveItemToPane {
12345 destination: 3,
12346 focus: true,
12347 clone: false,
12348 },
12349 window,
12350 cx,
12351 );
12352 assert_eq!(workspace.panes.len(), 3, "A new pane should be created");
12353 assert_eq!(
12354 pane_items_paths(&workspace.active_pane, cx),
12355 vec!["first.txt".to_string()],
12356 "After moving, one item should be left in the original pane"
12357 );
12358 assert_eq!(
12359 pane_items_paths(&workspace.panes[1], cx),
12360 vec!["second.txt".to_string()],
12361 "Previously created pane should be unchanged"
12362 );
12363 assert_eq!(
12364 pane_items_paths(&workspace.panes[2], cx),
12365 vec!["third.txt".to_string()],
12366 "New item should have been moved to the new pane"
12367 );
12368 });
12369 }
12370
12371 #[gpui::test]
12372 async fn test_moving_items_can_clone_panes(cx: &mut TestAppContext) {
12373 init_test(cx);
12374
12375 let fs = FakeFs::new(cx.executor());
12376 let project = Project::test(fs, [], cx).await;
12377 let (workspace, cx) =
12378 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
12379
12380 let item_1 = cx.new(|cx| {
12381 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
12382 });
12383 workspace.update_in(cx, |workspace, window, cx| {
12384 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
12385 workspace.move_item_to_pane_in_direction(
12386 &MoveItemToPaneInDirection {
12387 direction: SplitDirection::Right,
12388 focus: true,
12389 clone: true,
12390 },
12391 window,
12392 cx,
12393 );
12394 });
12395 cx.run_until_parked();
12396 workspace.update_in(cx, |workspace, window, cx| {
12397 workspace.move_item_to_pane_at_index(
12398 &MoveItemToPane {
12399 destination: 3,
12400 focus: true,
12401 clone: true,
12402 },
12403 window,
12404 cx,
12405 );
12406 });
12407 cx.run_until_parked();
12408
12409 workspace.update(cx, |workspace, cx| {
12410 assert_eq!(workspace.panes.len(), 3, "Two new panes were created");
12411 for pane in workspace.panes() {
12412 assert_eq!(
12413 pane_items_paths(pane, cx),
12414 vec!["first.txt".to_string()],
12415 "Single item exists in all panes"
12416 );
12417 }
12418 });
12419
12420 // verify that the active pane has been updated after waiting for the
12421 // pane focus event to fire and resolve
12422 workspace.read_with(cx, |workspace, _app| {
12423 assert_eq!(
12424 workspace.active_pane(),
12425 &workspace.panes[2],
12426 "The third pane should be the active one: {:?}",
12427 workspace.panes
12428 );
12429 })
12430 }
12431
12432 #[gpui::test]
12433 async fn test_close_item_in_all_panes(cx: &mut TestAppContext) {
12434 init_test(cx);
12435
12436 let fs = FakeFs::new(cx.executor());
12437 fs.insert_tree("/root", json!({ "test.txt": "" })).await;
12438
12439 let project = Project::test(fs, ["root".as_ref()], cx).await;
12440 let (workspace, cx) =
12441 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
12442
12443 let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12444 // Add item to pane A with project path
12445 let item_a = cx.new(|cx| {
12446 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
12447 });
12448 workspace.update_in(cx, |workspace, window, cx| {
12449 workspace.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
12450 });
12451
12452 // Split to create pane B
12453 let pane_b = workspace.update_in(cx, |workspace, window, cx| {
12454 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
12455 });
12456
12457 // Add item with SAME project path to pane B, and pin it
12458 let item_b = cx.new(|cx| {
12459 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
12460 });
12461 pane_b.update_in(cx, |pane, window, cx| {
12462 pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
12463 pane.set_pinned_count(1);
12464 });
12465
12466 assert_eq!(pane_a.read_with(cx, |pane, _| pane.items_len()), 1);
12467 assert_eq!(pane_b.read_with(cx, |pane, _| pane.items_len()), 1);
12468
12469 // close_pinned: false should only close the unpinned copy
12470 workspace.update_in(cx, |workspace, window, cx| {
12471 workspace.close_item_in_all_panes(
12472 &CloseItemInAllPanes {
12473 save_intent: Some(SaveIntent::Close),
12474 close_pinned: false,
12475 },
12476 window,
12477 cx,
12478 )
12479 });
12480 cx.executor().run_until_parked();
12481
12482 let item_count_a = pane_a.read_with(cx, |pane, _| pane.items_len());
12483 let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
12484 assert_eq!(item_count_a, 0, "Unpinned item in pane A should be closed");
12485 assert_eq!(item_count_b, 1, "Pinned item in pane B should remain");
12486
12487 // Split again, seeing as closing the previous item also closed its
12488 // pane, so only pane remains, which does not allow us to properly test
12489 // that both items close when `close_pinned: true`.
12490 let pane_c = workspace.update_in(cx, |workspace, window, cx| {
12491 workspace.split_pane(pane_b.clone(), SplitDirection::Right, window, cx)
12492 });
12493
12494 // Add an item with the same project path to pane C so that
12495 // close_item_in_all_panes can determine what to close across all panes
12496 // (it reads the active item from the active pane, and split_pane
12497 // creates an empty pane).
12498 let item_c = cx.new(|cx| {
12499 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
12500 });
12501 pane_c.update_in(cx, |pane, window, cx| {
12502 pane.add_item(Box::new(item_c.clone()), true, true, None, window, cx);
12503 });
12504
12505 // close_pinned: true should close the pinned copy too
12506 workspace.update_in(cx, |workspace, window, cx| {
12507 let panes_count = workspace.panes().len();
12508 assert_eq!(panes_count, 2, "Workspace should have two panes (B and C)");
12509
12510 workspace.close_item_in_all_panes(
12511 &CloseItemInAllPanes {
12512 save_intent: Some(SaveIntent::Close),
12513 close_pinned: true,
12514 },
12515 window,
12516 cx,
12517 )
12518 });
12519 cx.executor().run_until_parked();
12520
12521 let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
12522 let item_count_c = pane_c.read_with(cx, |pane, _| pane.items_len());
12523 assert_eq!(item_count_b, 0, "Pinned item in pane B should be closed");
12524 assert_eq!(item_count_c, 0, "Unpinned item in pane C should be closed");
12525 }
12526
12527 mod register_project_item_tests {
12528
12529 use super::*;
12530
12531 // View
12532 struct TestPngItemView {
12533 focus_handle: FocusHandle,
12534 }
12535 // Model
12536 struct TestPngItem {}
12537
12538 impl project::ProjectItem for TestPngItem {
12539 fn try_open(
12540 _project: &Entity<Project>,
12541 path: &ProjectPath,
12542 cx: &mut App,
12543 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
12544 if path.path.extension().unwrap() == "png" {
12545 Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestPngItem {}))))
12546 } else {
12547 None
12548 }
12549 }
12550
12551 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
12552 None
12553 }
12554
12555 fn project_path(&self, _: &App) -> Option<ProjectPath> {
12556 None
12557 }
12558
12559 fn is_dirty(&self) -> bool {
12560 false
12561 }
12562 }
12563
12564 impl Item for TestPngItemView {
12565 type Event = ();
12566 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
12567 "".into()
12568 }
12569 }
12570 impl EventEmitter<()> for TestPngItemView {}
12571 impl Focusable for TestPngItemView {
12572 fn focus_handle(&self, _cx: &App) -> FocusHandle {
12573 self.focus_handle.clone()
12574 }
12575 }
12576
12577 impl Render for TestPngItemView {
12578 fn render(
12579 &mut self,
12580 _window: &mut Window,
12581 _cx: &mut Context<Self>,
12582 ) -> impl IntoElement {
12583 Empty
12584 }
12585 }
12586
12587 impl ProjectItem for TestPngItemView {
12588 type Item = TestPngItem;
12589
12590 fn for_project_item(
12591 _project: Entity<Project>,
12592 _pane: Option<&Pane>,
12593 _item: Entity<Self::Item>,
12594 _: &mut Window,
12595 cx: &mut Context<Self>,
12596 ) -> Self
12597 where
12598 Self: Sized,
12599 {
12600 Self {
12601 focus_handle: cx.focus_handle(),
12602 }
12603 }
12604 }
12605
12606 // View
12607 struct TestIpynbItemView {
12608 focus_handle: FocusHandle,
12609 }
12610 // Model
12611 struct TestIpynbItem {}
12612
12613 impl project::ProjectItem for TestIpynbItem {
12614 fn try_open(
12615 _project: &Entity<Project>,
12616 path: &ProjectPath,
12617 cx: &mut App,
12618 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
12619 if path.path.extension().unwrap() == "ipynb" {
12620 Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestIpynbItem {}))))
12621 } else {
12622 None
12623 }
12624 }
12625
12626 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
12627 None
12628 }
12629
12630 fn project_path(&self, _: &App) -> Option<ProjectPath> {
12631 None
12632 }
12633
12634 fn is_dirty(&self) -> bool {
12635 false
12636 }
12637 }
12638
12639 impl Item for TestIpynbItemView {
12640 type Event = ();
12641 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
12642 "".into()
12643 }
12644 }
12645 impl EventEmitter<()> for TestIpynbItemView {}
12646 impl Focusable for TestIpynbItemView {
12647 fn focus_handle(&self, _cx: &App) -> FocusHandle {
12648 self.focus_handle.clone()
12649 }
12650 }
12651
12652 impl Render for TestIpynbItemView {
12653 fn render(
12654 &mut self,
12655 _window: &mut Window,
12656 _cx: &mut Context<Self>,
12657 ) -> impl IntoElement {
12658 Empty
12659 }
12660 }
12661
12662 impl ProjectItem for TestIpynbItemView {
12663 type Item = TestIpynbItem;
12664
12665 fn for_project_item(
12666 _project: Entity<Project>,
12667 _pane: Option<&Pane>,
12668 _item: Entity<Self::Item>,
12669 _: &mut Window,
12670 cx: &mut Context<Self>,
12671 ) -> Self
12672 where
12673 Self: Sized,
12674 {
12675 Self {
12676 focus_handle: cx.focus_handle(),
12677 }
12678 }
12679 }
12680
12681 struct TestAlternatePngItemView {
12682 focus_handle: FocusHandle,
12683 }
12684
12685 impl Item for TestAlternatePngItemView {
12686 type Event = ();
12687 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
12688 "".into()
12689 }
12690 }
12691
12692 impl EventEmitter<()> for TestAlternatePngItemView {}
12693 impl Focusable for TestAlternatePngItemView {
12694 fn focus_handle(&self, _cx: &App) -> FocusHandle {
12695 self.focus_handle.clone()
12696 }
12697 }
12698
12699 impl Render for TestAlternatePngItemView {
12700 fn render(
12701 &mut self,
12702 _window: &mut Window,
12703 _cx: &mut Context<Self>,
12704 ) -> impl IntoElement {
12705 Empty
12706 }
12707 }
12708
12709 impl ProjectItem for TestAlternatePngItemView {
12710 type Item = TestPngItem;
12711
12712 fn for_project_item(
12713 _project: Entity<Project>,
12714 _pane: Option<&Pane>,
12715 _item: Entity<Self::Item>,
12716 _: &mut Window,
12717 cx: &mut Context<Self>,
12718 ) -> Self
12719 where
12720 Self: Sized,
12721 {
12722 Self {
12723 focus_handle: cx.focus_handle(),
12724 }
12725 }
12726 }
12727
12728 #[gpui::test]
12729 async fn test_register_project_item(cx: &mut TestAppContext) {
12730 init_test(cx);
12731
12732 cx.update(|cx| {
12733 register_project_item::<TestPngItemView>(cx);
12734 register_project_item::<TestIpynbItemView>(cx);
12735 });
12736
12737 let fs = FakeFs::new(cx.executor());
12738 fs.insert_tree(
12739 "/root1",
12740 json!({
12741 "one.png": "BINARYDATAHERE",
12742 "two.ipynb": "{ totally a notebook }",
12743 "three.txt": "editing text, sure why not?"
12744 }),
12745 )
12746 .await;
12747
12748 let project = Project::test(fs, ["root1".as_ref()], cx).await;
12749 let (workspace, cx) =
12750 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
12751
12752 let worktree_id = project.update(cx, |project, cx| {
12753 project.worktrees(cx).next().unwrap().read(cx).id()
12754 });
12755
12756 let handle = workspace
12757 .update_in(cx, |workspace, window, cx| {
12758 let project_path = (worktree_id, rel_path("one.png"));
12759 workspace.open_path(project_path, None, true, window, cx)
12760 })
12761 .await
12762 .unwrap();
12763
12764 // Now we can check if the handle we got back errored or not
12765 assert_eq!(
12766 handle.to_any_view().entity_type(),
12767 TypeId::of::<TestPngItemView>()
12768 );
12769
12770 let handle = workspace
12771 .update_in(cx, |workspace, window, cx| {
12772 let project_path = (worktree_id, rel_path("two.ipynb"));
12773 workspace.open_path(project_path, None, true, window, cx)
12774 })
12775 .await
12776 .unwrap();
12777
12778 assert_eq!(
12779 handle.to_any_view().entity_type(),
12780 TypeId::of::<TestIpynbItemView>()
12781 );
12782
12783 let handle = workspace
12784 .update_in(cx, |workspace, window, cx| {
12785 let project_path = (worktree_id, rel_path("three.txt"));
12786 workspace.open_path(project_path, None, true, window, cx)
12787 })
12788 .await;
12789 assert!(handle.is_err());
12790 }
12791
12792 #[gpui::test]
12793 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
12794 init_test(cx);
12795
12796 cx.update(|cx| {
12797 register_project_item::<TestPngItemView>(cx);
12798 register_project_item::<TestAlternatePngItemView>(cx);
12799 });
12800
12801 let fs = FakeFs::new(cx.executor());
12802 fs.insert_tree(
12803 "/root1",
12804 json!({
12805 "one.png": "BINARYDATAHERE",
12806 "two.ipynb": "{ totally a notebook }",
12807 "three.txt": "editing text, sure why not?"
12808 }),
12809 )
12810 .await;
12811 let project = Project::test(fs, ["root1".as_ref()], cx).await;
12812 let (workspace, cx) =
12813 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
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 // This _must_ be the second item registered
12827 assert_eq!(
12828 handle.to_any_view().entity_type(),
12829 TypeId::of::<TestAlternatePngItemView>()
12830 );
12831
12832 let handle = workspace
12833 .update_in(cx, |workspace, window, cx| {
12834 let project_path = (worktree_id, rel_path("three.txt"));
12835 workspace.open_path(project_path, None, true, window, cx)
12836 })
12837 .await;
12838 assert!(handle.is_err());
12839 }
12840 }
12841
12842 #[gpui::test]
12843 async fn test_status_bar_visibility(cx: &mut TestAppContext) {
12844 init_test(cx);
12845
12846 let fs = FakeFs::new(cx.executor());
12847 let project = Project::test(fs, [], cx).await;
12848 let (workspace, _cx) =
12849 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
12850
12851 // Test with status bar shown (default)
12852 workspace.read_with(cx, |workspace, cx| {
12853 let visible = workspace.status_bar_visible(cx);
12854 assert!(visible, "Status bar should be visible by default");
12855 });
12856
12857 // Test with status bar hidden
12858 cx.update_global(|store: &mut SettingsStore, cx| {
12859 store.update_user_settings(cx, |settings| {
12860 settings.status_bar.get_or_insert_default().show = Some(false);
12861 });
12862 });
12863
12864 workspace.read_with(cx, |workspace, cx| {
12865 let visible = workspace.status_bar_visible(cx);
12866 assert!(!visible, "Status bar should be hidden when show is false");
12867 });
12868
12869 // Test with status bar shown explicitly
12870 cx.update_global(|store: &mut SettingsStore, cx| {
12871 store.update_user_settings(cx, |settings| {
12872 settings.status_bar.get_or_insert_default().show = Some(true);
12873 });
12874 });
12875
12876 workspace.read_with(cx, |workspace, cx| {
12877 let visible = workspace.status_bar_visible(cx);
12878 assert!(visible, "Status bar should be visible when show is true");
12879 });
12880 }
12881
12882 #[gpui::test]
12883 async fn test_pane_close_active_item(cx: &mut TestAppContext) {
12884 init_test(cx);
12885
12886 let fs = FakeFs::new(cx.executor());
12887 let project = Project::test(fs, [], cx).await;
12888 let (workspace, cx) =
12889 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12890 let panel = workspace.update_in(cx, |workspace, window, cx| {
12891 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
12892 workspace.add_panel(panel.clone(), window, cx);
12893
12894 workspace
12895 .right_dock()
12896 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
12897
12898 panel
12899 });
12900
12901 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12902 let item_a = cx.new(TestItem::new);
12903 let item_b = cx.new(TestItem::new);
12904 let item_a_id = item_a.entity_id();
12905 let item_b_id = item_b.entity_id();
12906
12907 pane.update_in(cx, |pane, window, cx| {
12908 pane.add_item(Box::new(item_a.clone()), true, true, None, window, cx);
12909 pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
12910 });
12911
12912 pane.read_with(cx, |pane, _| {
12913 assert_eq!(pane.items_len(), 2);
12914 assert_eq!(pane.active_item().unwrap().item_id(), item_b_id);
12915 });
12916
12917 workspace.update_in(cx, |workspace, window, cx| {
12918 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12919 });
12920
12921 workspace.update_in(cx, |_, window, cx| {
12922 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12923 });
12924
12925 // Assert that the `pane::CloseActiveItem` action is handled at the
12926 // workspace level when one of the dock panels is focused and, in that
12927 // case, the center pane's active item is closed but the focus is not
12928 // moved.
12929 cx.dispatch_action(pane::CloseActiveItem::default());
12930 cx.run_until_parked();
12931
12932 pane.read_with(cx, |pane, _| {
12933 assert_eq!(pane.items_len(), 1);
12934 assert_eq!(pane.active_item().unwrap().item_id(), item_a_id);
12935 });
12936
12937 workspace.update_in(cx, |workspace, window, cx| {
12938 assert!(workspace.right_dock().read(cx).is_open());
12939 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12940 });
12941 }
12942
12943 #[gpui::test]
12944 async fn test_panel_zoom_preserved_across_workspace_switch(cx: &mut TestAppContext) {
12945 init_test(cx);
12946 let fs = FakeFs::new(cx.executor());
12947
12948 let project_a = Project::test(fs.clone(), [], cx).await;
12949 let project_b = Project::test(fs, [], cx).await;
12950
12951 let multi_workspace_handle =
12952 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
12953
12954 let workspace_a = multi_workspace_handle
12955 .read_with(cx, |mw, _| mw.workspace().clone())
12956 .unwrap();
12957
12958 let _workspace_b = multi_workspace_handle
12959 .update(cx, |mw, window, cx| {
12960 mw.test_add_workspace(project_b, window, cx)
12961 })
12962 .unwrap();
12963
12964 // Switch to workspace A
12965 multi_workspace_handle
12966 .update(cx, |mw, window, cx| {
12967 mw.activate_index(0, window, cx);
12968 })
12969 .unwrap();
12970
12971 let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
12972
12973 // Add a panel to workspace A's right dock and open the dock
12974 let panel = workspace_a.update_in(cx, |workspace, window, cx| {
12975 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
12976 workspace.add_panel(panel.clone(), window, cx);
12977 workspace
12978 .right_dock()
12979 .update(cx, |dock, cx| dock.set_open(true, window, cx));
12980 panel
12981 });
12982
12983 // Focus the panel through the workspace (matching existing test pattern)
12984 workspace_a.update_in(cx, |workspace, window, cx| {
12985 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12986 });
12987
12988 // Zoom the panel
12989 panel.update_in(cx, |panel, window, cx| {
12990 panel.set_zoomed(true, window, cx);
12991 });
12992
12993 // Verify the panel is zoomed and the dock is open
12994 workspace_a.update_in(cx, |workspace, window, cx| {
12995 assert!(
12996 workspace.right_dock().read(cx).is_open(),
12997 "dock should be open before switch"
12998 );
12999 assert!(
13000 panel.is_zoomed(window, cx),
13001 "panel should be zoomed before switch"
13002 );
13003 assert!(
13004 panel.read(cx).focus_handle(cx).contains_focused(window, cx),
13005 "panel should be focused before switch"
13006 );
13007 });
13008
13009 // Switch to workspace B
13010 multi_workspace_handle
13011 .update(cx, |mw, window, cx| {
13012 mw.activate_index(1, window, cx);
13013 })
13014 .unwrap();
13015 cx.run_until_parked();
13016
13017 // Switch back to workspace A
13018 multi_workspace_handle
13019 .update(cx, |mw, window, cx| {
13020 mw.activate_index(0, window, cx);
13021 })
13022 .unwrap();
13023 cx.run_until_parked();
13024
13025 // Verify the panel is still zoomed and the dock is still open
13026 workspace_a.update_in(cx, |workspace, window, cx| {
13027 assert!(
13028 workspace.right_dock().read(cx).is_open(),
13029 "dock should still be open after switching back"
13030 );
13031 assert!(
13032 panel.is_zoomed(window, cx),
13033 "panel should still be zoomed after switching back"
13034 );
13035 });
13036 }
13037
13038 fn pane_items_paths(pane: &Entity<Pane>, cx: &App) -> Vec<String> {
13039 pane.read(cx)
13040 .items()
13041 .flat_map(|item| {
13042 item.project_paths(cx)
13043 .into_iter()
13044 .map(|path| path.path.display(PathStyle::local()).into_owned())
13045 })
13046 .collect()
13047 }
13048
13049 pub fn init_test(cx: &mut TestAppContext) {
13050 cx.update(|cx| {
13051 let settings_store = SettingsStore::test(cx);
13052 cx.set_global(settings_store);
13053 theme::init(theme::LoadThemes::JustBase, cx);
13054 });
13055 }
13056
13057 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
13058 let item = TestProjectItem::new(id, path, cx);
13059 item.update(cx, |item, _| {
13060 item.is_dirty = true;
13061 });
13062 item
13063 }
13064
13065 #[gpui::test]
13066 async fn test_zoomed_panel_without_pane_preserved_on_center_focus(
13067 cx: &mut gpui::TestAppContext,
13068 ) {
13069 init_test(cx);
13070 let fs = FakeFs::new(cx.executor());
13071
13072 let project = Project::test(fs, [], cx).await;
13073 let (workspace, cx) =
13074 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13075
13076 let panel = workspace.update_in(cx, |workspace, window, cx| {
13077 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
13078 workspace.add_panel(panel.clone(), window, cx);
13079 workspace
13080 .right_dock()
13081 .update(cx, |dock, cx| dock.set_open(true, window, cx));
13082 panel
13083 });
13084
13085 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13086 pane.update_in(cx, |pane, window, cx| {
13087 let item = cx.new(TestItem::new);
13088 pane.add_item(Box::new(item), true, true, None, window, cx);
13089 });
13090
13091 // Transfer focus to the panel, then zoom it. Using toggle_panel_focus
13092 // mirrors the real-world flow and avoids side effects from directly
13093 // focusing the panel while the center pane is active.
13094 workspace.update_in(cx, |workspace, window, cx| {
13095 workspace.toggle_panel_focus::<TestPanel>(window, cx);
13096 });
13097
13098 panel.update_in(cx, |panel, window, cx| {
13099 panel.set_zoomed(true, window, cx);
13100 });
13101
13102 workspace.update_in(cx, |workspace, window, cx| {
13103 assert!(workspace.right_dock().read(cx).is_open());
13104 assert!(panel.is_zoomed(window, cx));
13105 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
13106 });
13107
13108 // Simulate a spurious pane::Event::Focus on the center pane while the
13109 // panel still has focus. This mirrors what happens during macOS window
13110 // activation: the center pane fires a focus event even though actual
13111 // focus remains on the dock panel.
13112 pane.update_in(cx, |_, _, cx| {
13113 cx.emit(pane::Event::Focus);
13114 });
13115
13116 // The dock must remain open because the panel had focus at the time the
13117 // event was processed. Before the fix, dock_to_preserve was None for
13118 // panels that don't implement pane(), causing the dock to close.
13119 workspace.update_in(cx, |workspace, window, cx| {
13120 assert!(
13121 workspace.right_dock().read(cx).is_open(),
13122 "Dock should stay open when its zoomed panel (without pane()) still has focus"
13123 );
13124 assert!(panel.is_zoomed(window, cx));
13125 });
13126 }
13127}