1pub mod dock;
2pub mod history_manager;
3pub mod invalid_item_view;
4pub mod item;
5mod modal_layer;
6pub mod notifications;
7pub mod pane;
8pub mod pane_group;
9mod path_list;
10mod persistence;
11pub mod searchable;
12mod security_modal;
13pub mod shared_screen;
14mod status_bar;
15pub mod tasks;
16mod theme_preview;
17mod toast_layer;
18mod toolbar;
19pub mod utility_pane;
20pub mod welcome;
21mod workspace_settings;
22
23pub use crate::notifications::NotificationFrame;
24pub use dock::Panel;
25pub use path_list::PathList;
26pub use toast_layer::{ToastAction, ToastLayer, ToastView};
27
28use anyhow::{Context as _, Result, anyhow};
29use call::{ActiveCall, call_settings::CallSettings};
30use client::{
31 ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore,
32 proto::{self, ErrorCode, PanelId, PeerId},
33};
34use collections::{HashMap, HashSet, hash_map};
35use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
36use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
37use futures::{
38 Future, FutureExt, StreamExt,
39 channel::{
40 mpsc::{self, UnboundedReceiver, UnboundedSender},
41 oneshot,
42 },
43 future::{Shared, try_join_all},
44};
45use gpui::{
46 Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context,
47 CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
48 Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView, MouseButton,
49 PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription,
50 SystemWindowTabController, Task, Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId,
51 WindowOptions, actions, canvas, point, relative, size, transparent_black,
52};
53pub use history_manager::*;
54pub use item::{
55 FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
56 ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
57};
58use itertools::Itertools;
59use language::{Buffer, LanguageRegistry, Rope, language_settings::all_language_settings};
60pub use modal_layer::*;
61use node_runtime::NodeRuntime;
62use notifications::{
63 DetachAndPromptErr, Notifications, dismiss_app_notification,
64 simple_message_notification::MessageNotification,
65};
66pub use pane::*;
67pub use pane_group::{
68 ActivePaneDecorator, HANDLE_HITBOX_SIZE, Member, PaneAxis, PaneGroup, PaneRenderContext,
69 SplitDirection,
70};
71use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace};
72pub use persistence::{
73 DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items,
74 model::{ItemId, SerializedWorkspaceLocation},
75};
76use postage::stream::Stream;
77use project::{
78 DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
79 WorktreeSettings,
80 debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
81 project_settings::ProjectSettings,
82 toolchain_store::ToolchainStoreEvent,
83 trusted_worktrees::{RemoteHostLocation, TrustedWorktrees, TrustedWorktreesEvent},
84};
85use remote::{
86 RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions,
87 remote_client::ConnectionIdentifier,
88};
89use schemars::JsonSchema;
90use serde::Deserialize;
91use session::AppSession;
92use settings::{
93 CenteredPaddingSettings, Settings, SettingsLocation, SettingsStore, update_settings_file,
94};
95use shared_screen::SharedScreen;
96use sqlez::{
97 bindable::{Bind, Column, StaticColumnCount},
98 statement::Statement,
99};
100use status_bar::StatusBar;
101pub use status_bar::StatusItemView;
102use std::{
103 any::TypeId,
104 borrow::Cow,
105 cell::RefCell,
106 cmp,
107 collections::VecDeque,
108 env,
109 hash::Hash,
110 path::{Path, PathBuf},
111 process::ExitStatus,
112 rc::Rc,
113 sync::{
114 Arc, LazyLock, Weak,
115 atomic::{AtomicBool, AtomicUsize},
116 },
117 time::Duration,
118};
119use task::{DebugScenario, SharedTaskContext, SpawnInTerminal};
120use theme::{ActiveTheme, GlobalTheme, SystemAppearance, ThemeSettings};
121pub use toolbar::{
122 PaneSearchBarCallbacks, Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
123};
124pub use ui;
125use ui::{Window, prelude::*};
126use util::{
127 ResultExt, TryFutureExt,
128 paths::{PathStyle, SanitizedPath},
129 rel_path::RelPath,
130 serde::default_true,
131};
132use uuid::Uuid;
133pub use workspace_settings::{
134 AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, StatusBarSettings, TabBarSettings,
135 WorkspaceSettings,
136};
137use zed_actions::{Spawn, feedback::FileBugReport};
138
139use crate::{
140 item::ItemBufferKind,
141 notifications::NotificationId,
142 utility_pane::{UTILITY_PANE_MIN_WIDTH, utility_slot_for_dock_position},
143};
144use crate::{
145 persistence::{
146 SerializedAxis,
147 model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
148 },
149 security_modal::SecurityModal,
150 utility_pane::{DraggedUtilityPane, UtilityPaneFrame, UtilityPaneSlot, UtilityPaneState},
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 /// Opens the project switcher dropdown (only visible when multiple folders are open).
219 SwitchProject,
220 /// Clears all notifications.
221 ClearAllNotifications,
222 /// Clears all navigation history, including forward/backward navigation, recently opened files, and recently closed tabs. **This action is irreversible**.
223 ClearNavigationHistory,
224 /// Closes the active dock.
225 CloseActiveDock,
226 /// Closes all docks.
227 CloseAllDocks,
228 /// Toggles all docks.
229 ToggleAllDocks,
230 /// Closes the current window.
231 CloseWindow,
232 /// Closes the current project.
233 CloseProject,
234 /// Opens the feedback dialog.
235 Feedback,
236 /// Follows the next collaborator in the session.
237 FollowNextCollaborator,
238 /// Moves the focused panel to the next position.
239 MoveFocusedPanelToNextPosition,
240 /// Creates a new file.
241 NewFile,
242 /// Creates a new file in a vertical split.
243 NewFileSplitVertical,
244 /// Creates a new file in a horizontal split.
245 NewFileSplitHorizontal,
246 /// Opens a new search.
247 NewSearch,
248 /// Opens a new window.
249 NewWindow,
250 /// Opens a file or directory.
251 Open,
252 /// Opens multiple files.
253 OpenFiles,
254 /// Opens the current location in terminal.
255 OpenInTerminal,
256 /// Opens the component preview.
257 OpenComponentPreview,
258 /// Reloads the active item.
259 ReloadActiveItem,
260 /// Resets the active dock to its default size.
261 ResetActiveDockSize,
262 /// Resets all open docks to their default sizes.
263 ResetOpenDocksSize,
264 /// Reloads the application
265 Reload,
266 /// Saves the current file with a new name.
267 SaveAs,
268 /// Saves without formatting.
269 SaveWithoutFormat,
270 /// Shuts down all debug adapters.
271 ShutdownDebugAdapters,
272 /// Suppresses the current notification.
273 SuppressNotification,
274 /// Toggles the bottom dock.
275 ToggleBottomDock,
276 /// Toggles centered layout mode.
277 ToggleCenteredLayout,
278 /// Toggles edit prediction feature globally for all files.
279 ToggleEditPrediction,
280 /// Toggles the left dock.
281 ToggleLeftDock,
282 /// Toggles the right dock.
283 ToggleRightDock,
284 /// Toggles zoom on the active pane.
285 ToggleZoom,
286 /// Toggles read-only mode for the active item (if supported by that item).
287 ToggleReadOnlyFile,
288 /// Zooms in on the active pane.
289 ZoomIn,
290 /// Zooms out of the active pane.
291 ZoomOut,
292 /// If any worktrees are in restricted mode, shows a modal with possible actions.
293 /// If the modal is shown already, closes it without trusting any worktree.
294 ToggleWorktreeSecurity,
295 /// Clears all trusted worktrees, placing them in restricted mode on next open.
296 /// Requires restart to take effect on already opened projects.
297 ClearTrustedWorktrees,
298 /// Stops following a collaborator.
299 Unfollow,
300 /// Restores the banner.
301 RestoreBanner,
302 /// Toggles expansion of the selected item.
303 ToggleExpandItem,
304 ]
305);
306
307/// Activates a specific pane by its index.
308#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
309#[action(namespace = workspace)]
310pub struct ActivatePane(pub usize);
311
312/// Moves an item to a specific pane by index.
313#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
314#[action(namespace = workspace)]
315#[serde(deny_unknown_fields)]
316pub struct MoveItemToPane {
317 #[serde(default = "default_1")]
318 pub destination: usize,
319 #[serde(default = "default_true")]
320 pub focus: bool,
321 #[serde(default)]
322 pub clone: bool,
323}
324
325fn default_1() -> usize {
326 1
327}
328
329/// Moves an item to a pane in the specified direction.
330#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
331#[action(namespace = workspace)]
332#[serde(deny_unknown_fields)]
333pub struct MoveItemToPaneInDirection {
334 #[serde(default = "default_right")]
335 pub direction: SplitDirection,
336 #[serde(default = "default_true")]
337 pub focus: bool,
338 #[serde(default)]
339 pub clone: bool,
340}
341
342/// Creates a new file in a split of the desired direction.
343#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
344#[action(namespace = workspace)]
345#[serde(deny_unknown_fields)]
346pub struct NewFileSplit(pub SplitDirection);
347
348fn default_right() -> SplitDirection {
349 SplitDirection::Right
350}
351
352/// Saves all open files in the workspace.
353#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
354#[action(namespace = workspace)]
355#[serde(deny_unknown_fields)]
356pub struct SaveAll {
357 #[serde(default)]
358 pub save_intent: Option<SaveIntent>,
359}
360
361/// Saves the current file with the specified options.
362#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
363#[action(namespace = workspace)]
364#[serde(deny_unknown_fields)]
365pub struct Save {
366 #[serde(default)]
367 pub save_intent: Option<SaveIntent>,
368}
369
370/// Closes all items and panes in the workspace.
371#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
372#[action(namespace = workspace)]
373#[serde(deny_unknown_fields)]
374pub struct CloseAllItemsAndPanes {
375 #[serde(default)]
376 pub save_intent: Option<SaveIntent>,
377}
378
379/// Closes all inactive tabs and panes in the workspace.
380#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
381#[action(namespace = workspace)]
382#[serde(deny_unknown_fields)]
383pub struct CloseInactiveTabsAndPanes {
384 #[serde(default)]
385 pub save_intent: Option<SaveIntent>,
386}
387
388/// Sends a sequence of keystrokes to the active element.
389#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
390#[action(namespace = workspace)]
391pub struct SendKeystrokes(pub String);
392
393actions!(
394 project_symbols,
395 [
396 /// Toggles the project symbols search.
397 #[action(name = "Toggle")]
398 ToggleProjectSymbols
399 ]
400);
401
402/// Toggles the file finder interface.
403#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
404#[action(namespace = file_finder, name = "Toggle")]
405#[serde(deny_unknown_fields)]
406pub struct ToggleFileFinder {
407 #[serde(default)]
408 pub separate_history: bool,
409}
410
411/// Opens a new terminal in the center.
412#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
413#[action(namespace = workspace)]
414#[serde(deny_unknown_fields)]
415pub struct NewCenterTerminal {
416 /// If true, creates a local terminal even in remote projects.
417 #[serde(default)]
418 pub local: bool,
419}
420
421/// Opens a new terminal.
422#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
423#[action(namespace = workspace)]
424#[serde(deny_unknown_fields)]
425pub struct NewTerminal {
426 /// If true, creates a local terminal even in remote projects.
427 #[serde(default)]
428 pub local: bool,
429}
430
431/// Increases size of a currently focused dock by a given amount of pixels.
432#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
433#[action(namespace = workspace)]
434#[serde(deny_unknown_fields)]
435pub struct IncreaseActiveDockSize {
436 /// For 0px parameter, uses UI font size value.
437 #[serde(default)]
438 pub px: u32,
439}
440
441/// Decreases size of a currently focused dock by a given amount of pixels.
442#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
443#[action(namespace = workspace)]
444#[serde(deny_unknown_fields)]
445pub struct DecreaseActiveDockSize {
446 /// For 0px parameter, uses UI font size value.
447 #[serde(default)]
448 pub px: u32,
449}
450
451/// Increases size of all currently visible docks uniformly, by a given amount of pixels.
452#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
453#[action(namespace = workspace)]
454#[serde(deny_unknown_fields)]
455pub struct IncreaseOpenDocksSize {
456 /// For 0px parameter, uses UI font size value.
457 #[serde(default)]
458 pub px: u32,
459}
460
461/// Decreases size of all currently visible docks uniformly, by a given amount of pixels.
462#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
463#[action(namespace = workspace)]
464#[serde(deny_unknown_fields)]
465pub struct DecreaseOpenDocksSize {
466 /// For 0px parameter, uses UI font size value.
467 #[serde(default)]
468 pub px: u32,
469}
470
471actions!(
472 workspace,
473 [
474 /// Activates the pane to the left.
475 ActivatePaneLeft,
476 /// Activates the pane to the right.
477 ActivatePaneRight,
478 /// Activates the pane above.
479 ActivatePaneUp,
480 /// Activates the pane below.
481 ActivatePaneDown,
482 /// Swaps the current pane with the one to the left.
483 SwapPaneLeft,
484 /// Swaps the current pane with the one to the right.
485 SwapPaneRight,
486 /// Swaps the current pane with the one above.
487 SwapPaneUp,
488 /// Swaps the current pane with the one below.
489 SwapPaneDown,
490 // Swaps the current pane with the first available adjacent pane (searching in order: below, above, right, left) and activates that pane.
491 SwapPaneAdjacent,
492 /// Move the current pane to be at the far left.
493 MovePaneLeft,
494 /// Move the current pane to be at the far right.
495 MovePaneRight,
496 /// Move the current pane to be at the very top.
497 MovePaneUp,
498 /// Move the current pane to be at the very bottom.
499 MovePaneDown,
500 ]
501);
502
503#[derive(PartialEq, Eq, Debug)]
504pub enum CloseIntent {
505 /// Quit the program entirely.
506 Quit,
507 /// Close a window.
508 CloseWindow,
509 /// Replace the workspace in an existing window.
510 ReplaceWindow,
511}
512
513#[derive(Clone)]
514pub struct Toast {
515 id: NotificationId,
516 msg: Cow<'static, str>,
517 autohide: bool,
518 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut Window, &mut App)>)>,
519}
520
521impl Toast {
522 pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
523 Toast {
524 id,
525 msg: msg.into(),
526 on_click: None,
527 autohide: false,
528 }
529 }
530
531 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
532 where
533 M: Into<Cow<'static, str>>,
534 F: Fn(&mut Window, &mut App) + 'static,
535 {
536 self.on_click = Some((message.into(), Arc::new(on_click)));
537 self
538 }
539
540 pub fn autohide(mut self) -> Self {
541 self.autohide = true;
542 self
543 }
544}
545
546impl PartialEq for Toast {
547 fn eq(&self, other: &Self) -> bool {
548 self.id == other.id
549 && self.msg == other.msg
550 && self.on_click.is_some() == other.on_click.is_some()
551 }
552}
553
554/// Opens a new terminal with the specified working directory.
555#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema, Action)]
556#[action(namespace = workspace)]
557#[serde(deny_unknown_fields)]
558pub struct OpenTerminal {
559 pub working_directory: PathBuf,
560 /// If true, creates a local terminal even in remote projects.
561 #[serde(default)]
562 pub local: bool,
563}
564
565#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
566pub struct WorkspaceId(i64);
567
568impl StaticColumnCount for WorkspaceId {}
569impl Bind for WorkspaceId {
570 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
571 self.0.bind(statement, start_index)
572 }
573}
574impl Column for WorkspaceId {
575 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
576 i64::column(statement, start_index)
577 .map(|(i, next_index)| (Self(i), next_index))
578 .with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
579 }
580}
581impl From<WorkspaceId> for i64 {
582 fn from(val: WorkspaceId) -> Self {
583 val.0
584 }
585}
586
587fn prompt_and_open_paths(app_state: Arc<AppState>, options: PathPromptOptions, cx: &mut App) {
588 let paths = cx.prompt_for_paths(options);
589 cx.spawn(
590 async move |cx| match paths.await.anyhow().and_then(|res| res) {
591 Ok(Some(paths)) => {
592 cx.update(|cx| {
593 open_paths(&paths, app_state, OpenOptions::default(), cx).detach_and_log_err(cx)
594 });
595 }
596 Ok(None) => {}
597 Err(err) => {
598 util::log_err(&err);
599 cx.update(|cx| {
600 if let Some(workspace_window) = cx
601 .active_window()
602 .and_then(|window| window.downcast::<Workspace>())
603 {
604 workspace_window
605 .update(cx, |workspace, _, cx| {
606 workspace.show_portal_error(err.to_string(), cx);
607 })
608 .ok();
609 }
610 });
611 }
612 },
613 )
614 .detach();
615}
616
617pub fn init(app_state: Arc<AppState>, cx: &mut App) {
618 component::init();
619 theme_preview::init(cx);
620 toast_layer::init(cx);
621 history_manager::init(cx);
622
623 cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx))
624 .on_action(|_: &Reload, cx| reload(cx))
625 .on_action({
626 let app_state = Arc::downgrade(&app_state);
627 move |_: &Open, cx: &mut App| {
628 if let Some(app_state) = app_state.upgrade() {
629 prompt_and_open_paths(
630 app_state,
631 PathPromptOptions {
632 files: true,
633 directories: true,
634 multiple: true,
635 prompt: None,
636 },
637 cx,
638 );
639 }
640 }
641 })
642 .on_action({
643 let app_state = Arc::downgrade(&app_state);
644 move |_: &OpenFiles, cx: &mut App| {
645 let directories = cx.can_select_mixed_files_and_dirs();
646 if let Some(app_state) = app_state.upgrade() {
647 prompt_and_open_paths(
648 app_state,
649 PathPromptOptions {
650 files: true,
651 directories,
652 multiple: true,
653 prompt: None,
654 },
655 cx,
656 );
657 }
658 }
659 });
660}
661
662type BuildProjectItemFn =
663 fn(AnyEntity, Entity<Project>, Option<&Pane>, &mut Window, &mut App) -> Box<dyn ItemHandle>;
664
665type BuildProjectItemForPathFn =
666 fn(
667 &Entity<Project>,
668 &ProjectPath,
669 &mut Window,
670 &mut App,
671 ) -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
672
673#[derive(Clone, Default)]
674struct ProjectItemRegistry {
675 build_project_item_fns_by_type: HashMap<TypeId, BuildProjectItemFn>,
676 build_project_item_for_path_fns: Vec<BuildProjectItemForPathFn>,
677}
678
679impl ProjectItemRegistry {
680 fn register<T: ProjectItem>(&mut self) {
681 self.build_project_item_fns_by_type.insert(
682 TypeId::of::<T::Item>(),
683 |item, project, pane, window, cx| {
684 let item = item.downcast().unwrap();
685 Box::new(cx.new(|cx| T::for_project_item(project, pane, item, window, cx)))
686 as Box<dyn ItemHandle>
687 },
688 );
689 self.build_project_item_for_path_fns
690 .push(|project, project_path, window, cx| {
691 let project_path = project_path.clone();
692 let is_file = project
693 .read(cx)
694 .entry_for_path(&project_path, cx)
695 .is_some_and(|entry| entry.is_file());
696 let entry_abs_path = project.read(cx).absolute_path(&project_path, cx);
697 let is_local = project.read(cx).is_local();
698 let project_item =
699 <T::Item as project::ProjectItem>::try_open(project, &project_path, cx)?;
700 let project = project.clone();
701 Some(window.spawn(cx, async move |cx| {
702 match project_item.await.with_context(|| {
703 format!(
704 "opening project path {:?}",
705 entry_abs_path.as_deref().unwrap_or(&project_path.path.as_std_path())
706 )
707 }) {
708 Ok(project_item) => {
709 let project_item = project_item;
710 let project_entry_id: Option<ProjectEntryId> =
711 project_item.read_with(cx, project::ProjectItem::entry_id);
712 let build_workspace_item = Box::new(
713 |pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
714 Box::new(cx.new(|cx| {
715 T::for_project_item(
716 project,
717 Some(pane),
718 project_item,
719 window,
720 cx,
721 )
722 })) as Box<dyn ItemHandle>
723 },
724 ) as Box<_>;
725 Ok((project_entry_id, build_workspace_item))
726 }
727 Err(e) => {
728 log::warn!("Failed to open a project item: {e:#}");
729 if e.error_code() == ErrorCode::Internal {
730 if let Some(abs_path) =
731 entry_abs_path.as_deref().filter(|_| is_file)
732 {
733 if let Some(broken_project_item_view) =
734 cx.update(|window, cx| {
735 T::for_broken_project_item(
736 abs_path, is_local, &e, window, cx,
737 )
738 })?
739 {
740 let build_workspace_item = Box::new(
741 move |_: &mut Pane, _: &mut Window, cx: &mut Context<Pane>| {
742 cx.new(|_| broken_project_item_view).boxed_clone()
743 },
744 )
745 as Box<_>;
746 return Ok((None, build_workspace_item));
747 }
748 }
749 }
750 Err(e)
751 }
752 }
753 }))
754 });
755 }
756
757 fn open_path(
758 &self,
759 project: &Entity<Project>,
760 path: &ProjectPath,
761 window: &mut Window,
762 cx: &mut App,
763 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
764 let Some(open_project_item) = self
765 .build_project_item_for_path_fns
766 .iter()
767 .rev()
768 .find_map(|open_project_item| open_project_item(project, path, window, cx))
769 else {
770 return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
771 };
772 open_project_item
773 }
774
775 fn build_item<T: project::ProjectItem>(
776 &self,
777 item: Entity<T>,
778 project: Entity<Project>,
779 pane: Option<&Pane>,
780 window: &mut Window,
781 cx: &mut App,
782 ) -> Option<Box<dyn ItemHandle>> {
783 let build = self
784 .build_project_item_fns_by_type
785 .get(&TypeId::of::<T>())?;
786 Some(build(item.into_any(), project, pane, window, cx))
787 }
788}
789
790type WorkspaceItemBuilder =
791 Box<dyn FnOnce(&mut Pane, &mut Window, &mut Context<Pane>) -> Box<dyn ItemHandle>>;
792
793impl Global for ProjectItemRegistry {}
794
795/// Registers a [ProjectItem] for the app. When opening a file, all the registered
796/// items will get a chance to open the file, starting from the project item that
797/// was added last.
798pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
799 cx.default_global::<ProjectItemRegistry>().register::<I>();
800}
801
802#[derive(Default)]
803pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
804
805struct FollowableViewDescriptor {
806 from_state_proto: fn(
807 Entity<Workspace>,
808 ViewId,
809 &mut Option<proto::view::Variant>,
810 &mut Window,
811 &mut App,
812 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
813 to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
814}
815
816impl Global for FollowableViewRegistry {}
817
818impl FollowableViewRegistry {
819 pub fn register<I: FollowableItem>(cx: &mut App) {
820 cx.default_global::<Self>().0.insert(
821 TypeId::of::<I>(),
822 FollowableViewDescriptor {
823 from_state_proto: |workspace, id, state, window, cx| {
824 I::from_state_proto(workspace, id, state, window, cx).map(|task| {
825 cx.foreground_executor()
826 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
827 })
828 },
829 to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
830 },
831 );
832 }
833
834 pub fn from_state_proto(
835 workspace: Entity<Workspace>,
836 view_id: ViewId,
837 mut state: Option<proto::view::Variant>,
838 window: &mut Window,
839 cx: &mut App,
840 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
841 cx.update_default_global(|this: &mut Self, cx| {
842 this.0.values().find_map(|descriptor| {
843 (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, window, cx)
844 })
845 })
846 }
847
848 pub fn to_followable_view(
849 view: impl Into<AnyView>,
850 cx: &App,
851 ) -> Option<Box<dyn FollowableItemHandle>> {
852 let this = cx.try_global::<Self>()?;
853 let view = view.into();
854 let descriptor = this.0.get(&view.entity_type())?;
855 Some((descriptor.to_followable_view)(&view))
856 }
857}
858
859#[derive(Copy, Clone)]
860struct SerializableItemDescriptor {
861 deserialize: fn(
862 Entity<Project>,
863 WeakEntity<Workspace>,
864 WorkspaceId,
865 ItemId,
866 &mut Window,
867 &mut Context<Pane>,
868 ) -> Task<Result<Box<dyn ItemHandle>>>,
869 cleanup: fn(WorkspaceId, Vec<ItemId>, &mut Window, &mut App) -> Task<Result<()>>,
870 view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
871}
872
873#[derive(Default)]
874struct SerializableItemRegistry {
875 descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
876 descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
877}
878
879impl Global for SerializableItemRegistry {}
880
881impl SerializableItemRegistry {
882 fn deserialize(
883 item_kind: &str,
884 project: Entity<Project>,
885 workspace: WeakEntity<Workspace>,
886 workspace_id: WorkspaceId,
887 item_item: ItemId,
888 window: &mut Window,
889 cx: &mut Context<Pane>,
890 ) -> Task<Result<Box<dyn ItemHandle>>> {
891 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
892 return Task::ready(Err(anyhow!(
893 "cannot deserialize {}, descriptor not found",
894 item_kind
895 )));
896 };
897
898 (descriptor.deserialize)(project, workspace, workspace_id, item_item, window, cx)
899 }
900
901 fn cleanup(
902 item_kind: &str,
903 workspace_id: WorkspaceId,
904 loaded_items: Vec<ItemId>,
905 window: &mut Window,
906 cx: &mut App,
907 ) -> Task<Result<()>> {
908 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
909 return Task::ready(Err(anyhow!(
910 "cannot cleanup {}, descriptor not found",
911 item_kind
912 )));
913 };
914
915 (descriptor.cleanup)(workspace_id, loaded_items, window, cx)
916 }
917
918 fn view_to_serializable_item_handle(
919 view: AnyView,
920 cx: &App,
921 ) -> Option<Box<dyn SerializableItemHandle>> {
922 let this = cx.try_global::<Self>()?;
923 let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
924 Some((descriptor.view_to_serializable_item)(view))
925 }
926
927 fn descriptor(item_kind: &str, cx: &App) -> Option<SerializableItemDescriptor> {
928 let this = cx.try_global::<Self>()?;
929 this.descriptors_by_kind.get(item_kind).copied()
930 }
931}
932
933pub fn register_serializable_item<I: SerializableItem>(cx: &mut App) {
934 let serialized_item_kind = I::serialized_item_kind();
935
936 let registry = cx.default_global::<SerializableItemRegistry>();
937 let descriptor = SerializableItemDescriptor {
938 deserialize: |project, workspace, workspace_id, item_id, window, cx| {
939 let task = I::deserialize(project, workspace, workspace_id, item_id, window, cx);
940 cx.foreground_executor()
941 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
942 },
943 cleanup: |workspace_id, loaded_items, window, cx| {
944 I::cleanup(workspace_id, loaded_items, window, cx)
945 },
946 view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
947 };
948 registry
949 .descriptors_by_kind
950 .insert(Arc::from(serialized_item_kind), descriptor);
951 registry
952 .descriptors_by_type
953 .insert(TypeId::of::<I>(), descriptor);
954}
955
956pub struct AppState {
957 pub languages: Arc<LanguageRegistry>,
958 pub client: Arc<Client>,
959 pub user_store: Entity<UserStore>,
960 pub workspace_store: Entity<WorkspaceStore>,
961 pub fs: Arc<dyn fs::Fs>,
962 pub build_window_options: fn(Option<Uuid>, &mut App) -> WindowOptions,
963 pub node_runtime: NodeRuntime,
964 pub session: Entity<AppSession>,
965}
966
967struct GlobalAppState(Weak<AppState>);
968
969impl Global for GlobalAppState {}
970
971pub struct WorkspaceStore {
972 workspaces: HashSet<WindowHandle<Workspace>>,
973 client: Arc<Client>,
974 _subscriptions: Vec<client::Subscription>,
975}
976
977#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
978pub enum CollaboratorId {
979 PeerId(PeerId),
980 Agent,
981}
982
983impl From<PeerId> for CollaboratorId {
984 fn from(peer_id: PeerId) -> Self {
985 CollaboratorId::PeerId(peer_id)
986 }
987}
988
989impl From<&PeerId> for CollaboratorId {
990 fn from(peer_id: &PeerId) -> Self {
991 CollaboratorId::PeerId(*peer_id)
992 }
993}
994
995#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
996struct Follower {
997 project_id: Option<u64>,
998 peer_id: PeerId,
999}
1000
1001impl AppState {
1002 #[track_caller]
1003 pub fn global(cx: &App) -> Weak<Self> {
1004 cx.global::<GlobalAppState>().0.clone()
1005 }
1006 pub fn try_global(cx: &App) -> Option<Weak<Self>> {
1007 cx.try_global::<GlobalAppState>()
1008 .map(|state| state.0.clone())
1009 }
1010 pub fn set_global(state: Weak<AppState>, cx: &mut App) {
1011 cx.set_global(GlobalAppState(state));
1012 }
1013
1014 #[cfg(any(test, feature = "test-support"))]
1015 pub fn test(cx: &mut App) -> Arc<Self> {
1016 use fs::Fs;
1017 use node_runtime::NodeRuntime;
1018 use session::Session;
1019 use settings::SettingsStore;
1020
1021 if !cx.has_global::<SettingsStore>() {
1022 let settings_store = SettingsStore::test(cx);
1023 cx.set_global(settings_store);
1024 }
1025
1026 let fs = fs::FakeFs::new(cx.background_executor().clone());
1027 <dyn Fs>::set_global(fs.clone(), cx);
1028 let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
1029 let clock = Arc::new(clock::FakeSystemClock::new());
1030 let http_client = http_client::FakeHttpClient::with_404_response();
1031 let client = Client::new(clock, http_client, cx);
1032 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
1033 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1034 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
1035
1036 theme::init(theme::LoadThemes::JustBase, cx);
1037 client::init(&client, cx);
1038
1039 Arc::new(Self {
1040 client,
1041 fs,
1042 languages,
1043 user_store,
1044 workspace_store,
1045 node_runtime: NodeRuntime::unavailable(),
1046 build_window_options: |_, _| Default::default(),
1047 session,
1048 })
1049 }
1050}
1051
1052struct DelayedDebouncedEditAction {
1053 task: Option<Task<()>>,
1054 cancel_channel: Option<oneshot::Sender<()>>,
1055}
1056
1057impl DelayedDebouncedEditAction {
1058 fn new() -> DelayedDebouncedEditAction {
1059 DelayedDebouncedEditAction {
1060 task: None,
1061 cancel_channel: None,
1062 }
1063 }
1064
1065 fn fire_new<F>(
1066 &mut self,
1067 delay: Duration,
1068 window: &mut Window,
1069 cx: &mut Context<Workspace>,
1070 func: F,
1071 ) where
1072 F: 'static
1073 + Send
1074 + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> Task<Result<()>>,
1075 {
1076 if let Some(channel) = self.cancel_channel.take() {
1077 _ = channel.send(());
1078 }
1079
1080 let (sender, mut receiver) = oneshot::channel::<()>();
1081 self.cancel_channel = Some(sender);
1082
1083 let previous_task = self.task.take();
1084 self.task = Some(cx.spawn_in(window, async move |workspace, cx| {
1085 let mut timer = cx.background_executor().timer(delay).fuse();
1086 if let Some(previous_task) = previous_task {
1087 previous_task.await;
1088 }
1089
1090 futures::select_biased! {
1091 _ = receiver => return,
1092 _ = timer => {}
1093 }
1094
1095 if let Some(result) = workspace
1096 .update_in(cx, |workspace, window, cx| (func)(workspace, window, cx))
1097 .log_err()
1098 {
1099 result.await.log_err();
1100 }
1101 }));
1102 }
1103}
1104
1105pub enum Event {
1106 PaneAdded(Entity<Pane>),
1107 PaneRemoved,
1108 ItemAdded {
1109 item: Box<dyn ItemHandle>,
1110 },
1111 ActiveItemChanged,
1112 ItemRemoved {
1113 item_id: EntityId,
1114 },
1115 UserSavedItem {
1116 pane: WeakEntity<Pane>,
1117 item: Box<dyn WeakItemHandle>,
1118 save_intent: SaveIntent,
1119 },
1120 ContactRequestedJoin(u64),
1121 WorkspaceCreated(WeakEntity<Workspace>),
1122 OpenBundledFile {
1123 text: Cow<'static, str>,
1124 title: &'static str,
1125 language: &'static str,
1126 },
1127 ZoomChanged,
1128 ModalOpened,
1129}
1130
1131#[derive(Debug, Clone)]
1132pub enum OpenVisible {
1133 All,
1134 None,
1135 OnlyFiles,
1136 OnlyDirectories,
1137}
1138
1139enum WorkspaceLocation {
1140 // Valid local paths or SSH project to serialize
1141 Location(SerializedWorkspaceLocation, PathList),
1142 // No valid location found hence clear session id
1143 DetachFromSession,
1144 // No valid location found to serialize
1145 None,
1146}
1147
1148type PromptForNewPath = Box<
1149 dyn Fn(
1150 &mut Workspace,
1151 DirectoryLister,
1152 Option<String>,
1153 &mut Window,
1154 &mut Context<Workspace>,
1155 ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
1156>;
1157
1158type PromptForOpenPath = Box<
1159 dyn Fn(
1160 &mut Workspace,
1161 DirectoryLister,
1162 &mut Window,
1163 &mut Context<Workspace>,
1164 ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
1165>;
1166
1167#[derive(Default)]
1168struct DispatchingKeystrokes {
1169 dispatched: HashSet<Vec<Keystroke>>,
1170 queue: VecDeque<Keystroke>,
1171 task: Option<Shared<Task<()>>>,
1172}
1173
1174/// Collects everything project-related for a certain window opened.
1175/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
1176///
1177/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
1178/// The `Workspace` owns everybody's state and serves as a default, "global context",
1179/// that can be used to register a global action to be triggered from any place in the window.
1180pub struct Workspace {
1181 weak_self: WeakEntity<Self>,
1182 workspace_actions: Vec<Box<dyn Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div>>,
1183 zoomed: Option<AnyWeakView>,
1184 previous_dock_drag_coordinates: Option<Point<Pixels>>,
1185 zoomed_position: Option<DockPosition>,
1186 center: PaneGroup,
1187 left_dock: Entity<Dock>,
1188 bottom_dock: Entity<Dock>,
1189 right_dock: Entity<Dock>,
1190 panes: Vec<Entity<Pane>>,
1191 active_worktree_override: Option<WorktreeId>,
1192 panes_by_item: HashMap<EntityId, WeakEntity<Pane>>,
1193 active_pane: Entity<Pane>,
1194 last_active_center_pane: Option<WeakEntity<Pane>>,
1195 last_active_view_id: Option<proto::ViewId>,
1196 status_bar: Entity<StatusBar>,
1197 modal_layer: Entity<ModalLayer>,
1198 toast_layer: Entity<ToastLayer>,
1199 titlebar_item: Option<AnyView>,
1200 notifications: Notifications,
1201 suppressed_notifications: HashSet<NotificationId>,
1202 project: Entity<Project>,
1203 follower_states: HashMap<CollaboratorId, FollowerState>,
1204 last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
1205 window_edited: bool,
1206 last_window_title: Option<String>,
1207 dirty_items: HashMap<EntityId, Subscription>,
1208 active_call: Option<(Entity<ActiveCall>, Vec<Subscription>)>,
1209 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
1210 database_id: Option<WorkspaceId>,
1211 app_state: Arc<AppState>,
1212 dispatching_keystrokes: Rc<RefCell<DispatchingKeystrokes>>,
1213 _subscriptions: Vec<Subscription>,
1214 _apply_leader_updates: Task<Result<()>>,
1215 _observe_current_user: Task<Result<()>>,
1216 _schedule_serialize_workspace: Option<Task<()>>,
1217 _schedule_serialize_ssh_paths: Option<Task<()>>,
1218 pane_history_timestamp: Arc<AtomicUsize>,
1219 bounds: Bounds<Pixels>,
1220 pub centered_layout: bool,
1221 bounds_save_task_queued: Option<Task<()>>,
1222 on_prompt_for_new_path: Option<PromptForNewPath>,
1223 on_prompt_for_open_path: Option<PromptForOpenPath>,
1224 terminal_provider: Option<Box<dyn TerminalProvider>>,
1225 debugger_provider: Option<Arc<dyn DebuggerProvider>>,
1226 serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
1227 _items_serializer: Task<Result<()>>,
1228 session_id: Option<String>,
1229 scheduled_tasks: Vec<Task<()>>,
1230 last_open_dock_positions: Vec<DockPosition>,
1231 removing: bool,
1232 utility_panes: UtilityPaneState,
1233}
1234
1235impl EventEmitter<Event> for Workspace {}
1236
1237#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
1238pub struct ViewId {
1239 pub creator: CollaboratorId,
1240 pub id: u64,
1241}
1242
1243pub struct FollowerState {
1244 center_pane: Entity<Pane>,
1245 dock_pane: Option<Entity<Pane>>,
1246 active_view_id: Option<ViewId>,
1247 items_by_leader_view_id: HashMap<ViewId, FollowerView>,
1248}
1249
1250struct FollowerView {
1251 view: Box<dyn FollowableItemHandle>,
1252 location: Option<proto::PanelId>,
1253}
1254
1255impl Workspace {
1256 pub fn new(
1257 workspace_id: Option<WorkspaceId>,
1258 project: Entity<Project>,
1259 app_state: Arc<AppState>,
1260 window: &mut Window,
1261 cx: &mut Context<Self>,
1262 ) -> Self {
1263 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1264 cx.subscribe(&trusted_worktrees, |_, worktrees_store, e, cx| {
1265 if let TrustedWorktreesEvent::Trusted(..) = e {
1266 // Do not persist auto trusted worktrees
1267 if !ProjectSettings::get_global(cx).session.trust_all_worktrees {
1268 worktrees_store.update(cx, |worktrees_store, cx| {
1269 worktrees_store.schedule_serialization(
1270 cx,
1271 |new_trusted_worktrees, cx| {
1272 let timeout =
1273 cx.background_executor().timer(SERIALIZATION_THROTTLE_TIME);
1274 cx.background_spawn(async move {
1275 timeout.await;
1276 persistence::DB
1277 .save_trusted_worktrees(new_trusted_worktrees)
1278 .await
1279 .log_err();
1280 })
1281 },
1282 )
1283 });
1284 }
1285 }
1286 })
1287 .detach();
1288
1289 cx.observe_global::<SettingsStore>(|_, cx| {
1290 if ProjectSettings::get_global(cx).session.trust_all_worktrees {
1291 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1292 trusted_worktrees.update(cx, |trusted_worktrees, cx| {
1293 trusted_worktrees.auto_trust_all(cx);
1294 })
1295 }
1296 }
1297 })
1298 .detach();
1299 }
1300
1301 cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
1302 match event {
1303 project::Event::RemoteIdChanged(_) => {
1304 this.update_window_title(window, cx);
1305 }
1306
1307 project::Event::CollaboratorLeft(peer_id) => {
1308 this.collaborator_left(*peer_id, window, cx);
1309 }
1310
1311 &project::Event::WorktreeRemoved(id) | &project::Event::WorktreeAdded(id) => {
1312 this.update_window_title(window, cx);
1313 if this
1314 .project()
1315 .read(cx)
1316 .worktree_for_id(id, cx)
1317 .is_some_and(|wt| wt.read(cx).is_visible())
1318 {
1319 this.serialize_workspace(window, cx);
1320 this.update_history(cx);
1321 }
1322 }
1323 project::Event::WorktreeUpdatedEntries(..) => {
1324 this.update_window_title(window, cx);
1325 this.serialize_workspace(window, cx);
1326 }
1327
1328 project::Event::DisconnectedFromHost => {
1329 this.update_window_edited(window, cx);
1330 let leaders_to_unfollow =
1331 this.follower_states.keys().copied().collect::<Vec<_>>();
1332 for leader_id in leaders_to_unfollow {
1333 this.unfollow(leader_id, window, cx);
1334 }
1335 }
1336
1337 project::Event::DisconnectedFromRemote {
1338 server_not_running: _,
1339 } => {
1340 this.update_window_edited(window, cx);
1341 }
1342
1343 project::Event::Closed => {
1344 window.remove_window();
1345 }
1346
1347 project::Event::DeletedEntry(_, entry_id) => {
1348 for pane in this.panes.iter() {
1349 pane.update(cx, |pane, cx| {
1350 pane.handle_deleted_project_item(*entry_id, window, cx)
1351 });
1352 }
1353 }
1354
1355 project::Event::Toast {
1356 notification_id,
1357 message,
1358 link,
1359 } => this.show_notification(
1360 NotificationId::named(notification_id.clone()),
1361 cx,
1362 |cx| {
1363 let mut notification = MessageNotification::new(message.clone(), cx);
1364 if let Some(link) = link {
1365 notification = notification
1366 .more_info_message(link.label)
1367 .more_info_url(link.url);
1368 }
1369
1370 cx.new(|_| notification)
1371 },
1372 ),
1373
1374 project::Event::HideToast { notification_id } => {
1375 this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
1376 }
1377
1378 project::Event::LanguageServerPrompt(request) => {
1379 struct LanguageServerPrompt;
1380
1381 this.show_notification(
1382 NotificationId::composite::<LanguageServerPrompt>(request.id),
1383 cx,
1384 |cx| {
1385 cx.new(|cx| {
1386 notifications::LanguageServerPrompt::new(request.clone(), cx)
1387 })
1388 },
1389 );
1390 }
1391
1392 project::Event::AgentLocationChanged => {
1393 this.handle_agent_location_changed(window, cx)
1394 }
1395
1396 _ => {}
1397 }
1398 cx.notify()
1399 })
1400 .detach();
1401
1402 cx.subscribe_in(
1403 &project.read(cx).breakpoint_store(),
1404 window,
1405 |workspace, _, event, window, cx| match event {
1406 BreakpointStoreEvent::BreakpointsUpdated(_, _)
1407 | BreakpointStoreEvent::BreakpointsCleared(_) => {
1408 workspace.serialize_workspace(window, cx);
1409 }
1410 BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {}
1411 },
1412 )
1413 .detach();
1414 if let Some(toolchain_store) = project.read(cx).toolchain_store() {
1415 cx.subscribe_in(
1416 &toolchain_store,
1417 window,
1418 |workspace, _, event, window, cx| match event {
1419 ToolchainStoreEvent::CustomToolchainsModified => {
1420 workspace.serialize_workspace(window, cx);
1421 }
1422 _ => {}
1423 },
1424 )
1425 .detach();
1426 }
1427
1428 cx.on_focus_lost(window, |this, window, cx| {
1429 let focus_handle = this.focus_handle(cx);
1430 window.focus(&focus_handle, cx);
1431 })
1432 .detach();
1433
1434 let weak_handle = cx.entity().downgrade();
1435 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
1436
1437 let center_pane = cx.new(|cx| {
1438 let mut center_pane = Pane::new(
1439 weak_handle.clone(),
1440 project.clone(),
1441 pane_history_timestamp.clone(),
1442 None,
1443 NewFile.boxed_clone(),
1444 true,
1445 window,
1446 cx,
1447 );
1448 center_pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
1449 center_pane.set_should_display_welcome_page(true);
1450 center_pane
1451 });
1452 cx.subscribe_in(¢er_pane, window, Self::handle_pane_event)
1453 .detach();
1454
1455 window.focus(¢er_pane.focus_handle(cx), cx);
1456
1457 cx.emit(Event::PaneAdded(center_pane.clone()));
1458
1459 let window_handle = window.window_handle().downcast::<Workspace>().unwrap();
1460 app_state.workspace_store.update(cx, |store, _| {
1461 store.workspaces.insert(window_handle);
1462 });
1463
1464 let mut current_user = app_state.user_store.read(cx).watch_current_user();
1465 let mut connection_status = app_state.client.status();
1466 let _observe_current_user = cx.spawn_in(window, async move |this, cx| {
1467 current_user.next().await;
1468 connection_status.next().await;
1469 let mut stream =
1470 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
1471
1472 while stream.recv().await.is_some() {
1473 this.update(cx, |_, cx| cx.notify())?;
1474 }
1475 anyhow::Ok(())
1476 });
1477
1478 // All leader updates are enqueued and then processed in a single task, so
1479 // that each asynchronous operation can be run in order.
1480 let (leader_updates_tx, mut leader_updates_rx) =
1481 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
1482 let _apply_leader_updates = cx.spawn_in(window, async move |this, cx| {
1483 while let Some((leader_id, update)) = leader_updates_rx.next().await {
1484 Self::process_leader_update(&this, leader_id, update, cx)
1485 .await
1486 .log_err();
1487 }
1488
1489 Ok(())
1490 });
1491
1492 cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
1493 let modal_layer = cx.new(|_| ModalLayer::new());
1494 let toast_layer = cx.new(|_| ToastLayer::new());
1495 cx.subscribe(
1496 &modal_layer,
1497 |_, _, _: &modal_layer::ModalOpenedEvent, cx| {
1498 cx.emit(Event::ModalOpened);
1499 },
1500 )
1501 .detach();
1502
1503 let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx);
1504 let bottom_dock = Dock::new(DockPosition::Bottom, modal_layer.clone(), window, cx);
1505 let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), window, cx);
1506 let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx));
1507 let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx));
1508 let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx));
1509 let status_bar = cx.new(|cx| {
1510 let mut status_bar = StatusBar::new(¢er_pane.clone(), window, cx);
1511 status_bar.add_left_item(left_dock_buttons, window, cx);
1512 status_bar.add_right_item(right_dock_buttons, window, cx);
1513 status_bar.add_right_item(bottom_dock_buttons, window, cx);
1514 status_bar
1515 });
1516
1517 let session_id = app_state.session.read(cx).id().to_owned();
1518
1519 let mut active_call = None;
1520 if let Some(call) = ActiveCall::try_global(cx) {
1521 let subscriptions = vec![cx.subscribe_in(&call, window, Self::on_active_call_event)];
1522 active_call = Some((call, subscriptions));
1523 }
1524
1525 let (serializable_items_tx, serializable_items_rx) =
1526 mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
1527 let _items_serializer = cx.spawn_in(window, async move |this, cx| {
1528 Self::serialize_items(&this, serializable_items_rx, cx).await
1529 });
1530
1531 let subscriptions = vec![
1532 cx.observe_window_activation(window, Self::on_window_activation_changed),
1533 cx.observe_window_bounds(window, move |this, window, cx| {
1534 if this.bounds_save_task_queued.is_some() {
1535 return;
1536 }
1537 this.bounds_save_task_queued = Some(cx.spawn_in(window, async move |this, cx| {
1538 cx.background_executor()
1539 .timer(Duration::from_millis(100))
1540 .await;
1541 this.update_in(cx, |this, window, cx| {
1542 if let Some(display) = window.display(cx)
1543 && let Ok(display_uuid) = display.uuid()
1544 {
1545 let window_bounds = window.inner_window_bounds();
1546 let has_paths = !this.root_paths(cx).is_empty();
1547 if !has_paths {
1548 cx.background_executor()
1549 .spawn(persistence::write_default_window_bounds(
1550 window_bounds,
1551 display_uuid,
1552 ))
1553 .detach_and_log_err(cx);
1554 }
1555 if let Some(database_id) = workspace_id {
1556 cx.background_executor()
1557 .spawn(DB.set_window_open_status(
1558 database_id,
1559 SerializedWindowBounds(window_bounds),
1560 display_uuid,
1561 ))
1562 .detach_and_log_err(cx);
1563 } else {
1564 cx.background_executor()
1565 .spawn(persistence::write_default_window_bounds(
1566 window_bounds,
1567 display_uuid,
1568 ))
1569 .detach_and_log_err(cx);
1570 }
1571 }
1572 this.bounds_save_task_queued.take();
1573 })
1574 .ok();
1575 }));
1576 cx.notify();
1577 }),
1578 cx.observe_window_appearance(window, |_, window, cx| {
1579 let window_appearance = window.appearance();
1580
1581 *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
1582
1583 GlobalTheme::reload_theme(cx);
1584 GlobalTheme::reload_icon_theme(cx);
1585 }),
1586 cx.on_release(move |this, cx| {
1587 this.app_state.workspace_store.update(cx, move |store, _| {
1588 store.workspaces.remove(&window_handle);
1589 })
1590 }),
1591 ];
1592
1593 cx.defer_in(window, move |this, window, cx| {
1594 this.update_window_title(window, cx);
1595 this.show_initial_notifications(cx);
1596 });
1597
1598 let mut center = PaneGroup::new(center_pane.clone());
1599 center.set_is_center(true);
1600 center.mark_positions(cx);
1601
1602 Workspace {
1603 weak_self: weak_handle.clone(),
1604 zoomed: None,
1605 zoomed_position: None,
1606 previous_dock_drag_coordinates: None,
1607 center,
1608 panes: vec![center_pane.clone()],
1609 panes_by_item: Default::default(),
1610 active_pane: center_pane.clone(),
1611 last_active_center_pane: Some(center_pane.downgrade()),
1612 last_active_view_id: None,
1613 status_bar,
1614 modal_layer,
1615 toast_layer,
1616 titlebar_item: None,
1617 active_worktree_override: None,
1618 notifications: Notifications::default(),
1619 suppressed_notifications: HashSet::default(),
1620 left_dock,
1621 bottom_dock,
1622 right_dock,
1623 project: project.clone(),
1624 follower_states: Default::default(),
1625 last_leaders_by_pane: Default::default(),
1626 dispatching_keystrokes: Default::default(),
1627 window_edited: false,
1628 last_window_title: None,
1629 dirty_items: Default::default(),
1630 active_call,
1631 database_id: workspace_id,
1632 app_state,
1633 _observe_current_user,
1634 _apply_leader_updates,
1635 _schedule_serialize_workspace: None,
1636 _schedule_serialize_ssh_paths: None,
1637 leader_updates_tx,
1638 _subscriptions: subscriptions,
1639 pane_history_timestamp,
1640 workspace_actions: Default::default(),
1641 // This data will be incorrect, but it will be overwritten by the time it needs to be used.
1642 bounds: Default::default(),
1643 centered_layout: false,
1644 bounds_save_task_queued: None,
1645 on_prompt_for_new_path: None,
1646 on_prompt_for_open_path: None,
1647 terminal_provider: None,
1648 debugger_provider: None,
1649 serializable_items_tx,
1650 _items_serializer,
1651 session_id: Some(session_id),
1652
1653 scheduled_tasks: Vec::new(),
1654 last_open_dock_positions: Vec::new(),
1655 removing: false,
1656 utility_panes: UtilityPaneState::default(),
1657 }
1658 }
1659
1660 pub fn new_local(
1661 abs_paths: Vec<PathBuf>,
1662 app_state: Arc<AppState>,
1663 requesting_window: Option<WindowHandle<Workspace>>,
1664 env: Option<HashMap<String, String>>,
1665 init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
1666 cx: &mut App,
1667 ) -> Task<
1668 anyhow::Result<(
1669 WindowHandle<Workspace>,
1670 Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
1671 )>,
1672 > {
1673 let project_handle = Project::local(
1674 app_state.client.clone(),
1675 app_state.node_runtime.clone(),
1676 app_state.user_store.clone(),
1677 app_state.languages.clone(),
1678 app_state.fs.clone(),
1679 env,
1680 Default::default(),
1681 cx,
1682 );
1683
1684 cx.spawn(async move |cx| {
1685 let mut paths_to_open = Vec::with_capacity(abs_paths.len());
1686 for path in abs_paths.into_iter() {
1687 if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() {
1688 paths_to_open.push(canonical)
1689 } else {
1690 paths_to_open.push(path)
1691 }
1692 }
1693
1694 let serialized_workspace =
1695 persistence::DB.workspace_for_roots(paths_to_open.as_slice());
1696
1697 if let Some(paths) = serialized_workspace.as_ref().map(|ws| &ws.paths) {
1698 paths_to_open = paths.ordered_paths().cloned().collect();
1699 if !paths.is_lexicographically_ordered() {
1700 project_handle.update(cx, |project, cx| {
1701 project.set_worktrees_reordered(true, cx);
1702 });
1703 }
1704 }
1705
1706 // Get project paths for all of the abs_paths
1707 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
1708 Vec::with_capacity(paths_to_open.len());
1709
1710 for path in paths_to_open.into_iter() {
1711 if let Some((_, project_entry)) = cx
1712 .update(|cx| {
1713 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
1714 })
1715 .await
1716 .log_err()
1717 {
1718 project_paths.push((path, Some(project_entry)));
1719 } else {
1720 project_paths.push((path, None));
1721 }
1722 }
1723
1724 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
1725 serialized_workspace.id
1726 } else {
1727 DB.next_id().await.unwrap_or_else(|_| Default::default())
1728 };
1729
1730 let toolchains = DB.toolchains(workspace_id).await?;
1731
1732 for (toolchain, worktree_path, path) in toolchains {
1733 let toolchain_path = PathBuf::from(toolchain.path.clone().to_string());
1734 let Some(worktree_id) = project_handle.read_with(cx, |this, cx| {
1735 this.find_worktree(&worktree_path, cx)
1736 .and_then(|(worktree, rel_path)| {
1737 if rel_path.is_empty() {
1738 Some(worktree.read(cx).id())
1739 } else {
1740 None
1741 }
1742 })
1743 }) else {
1744 // We did not find a worktree with a given path, but that's whatever.
1745 continue;
1746 };
1747 if !app_state.fs.is_file(toolchain_path.as_path()).await {
1748 continue;
1749 }
1750
1751 project_handle
1752 .update(cx, |this, cx| {
1753 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
1754 })
1755 .await;
1756 }
1757 if let Some(workspace) = serialized_workspace.as_ref() {
1758 project_handle.update(cx, |this, cx| {
1759 for (scope, toolchains) in &workspace.user_toolchains {
1760 for toolchain in toolchains {
1761 this.add_toolchain(toolchain.clone(), scope.clone(), cx);
1762 }
1763 }
1764 });
1765 }
1766
1767 let window = if let Some(window) = requesting_window {
1768 let centered_layout = serialized_workspace
1769 .as_ref()
1770 .map(|w| w.centered_layout)
1771 .unwrap_or(false);
1772
1773 cx.update_window(window.into(), |_, window, cx| {
1774 window.replace_root(cx, |window, cx| {
1775 let mut workspace = Workspace::new(
1776 Some(workspace_id),
1777 project_handle.clone(),
1778 app_state.clone(),
1779 window,
1780 cx,
1781 );
1782
1783 workspace.centered_layout = centered_layout;
1784
1785 // Call init callback to add items before window renders
1786 if let Some(init) = init {
1787 init(&mut workspace, window, cx);
1788 }
1789
1790 workspace
1791 });
1792 })?;
1793 window
1794 } else {
1795 let window_bounds_override = window_bounds_env_override();
1796
1797 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
1798 (Some(WindowBounds::Windowed(bounds)), None)
1799 } else if let Some(workspace) = serialized_workspace.as_ref()
1800 && let Some(display) = workspace.display
1801 && let Some(bounds) = workspace.window_bounds.as_ref()
1802 {
1803 // Reopening an existing workspace - restore its saved bounds
1804 (Some(bounds.0), Some(display))
1805 } else if let Some((display, bounds)) = persistence::read_default_window_bounds() {
1806 // New or empty workspace - use the last known window bounds
1807 (Some(bounds), Some(display))
1808 } else {
1809 // New window - let GPUI's default_bounds() handle cascading
1810 (None, None)
1811 };
1812
1813 // Use the serialized workspace to construct the new window
1814 let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx));
1815 options.window_bounds = window_bounds;
1816 let centered_layout = serialized_workspace
1817 .as_ref()
1818 .map(|w| w.centered_layout)
1819 .unwrap_or(false);
1820 cx.open_window(options, {
1821 let app_state = app_state.clone();
1822 let project_handle = project_handle.clone();
1823 move |window, cx| {
1824 cx.new(|cx| {
1825 let mut workspace = Workspace::new(
1826 Some(workspace_id),
1827 project_handle,
1828 app_state,
1829 window,
1830 cx,
1831 );
1832 workspace.centered_layout = centered_layout;
1833
1834 // Call init callback to add items before window renders
1835 if let Some(init) = init {
1836 init(&mut workspace, window, cx);
1837 }
1838
1839 workspace
1840 })
1841 }
1842 })?
1843 };
1844
1845 notify_if_database_failed(window, cx);
1846 // Check if this is an empty workspace (no paths to open)
1847 // An empty workspace is one where project_paths is empty
1848 let is_empty_workspace = project_paths.is_empty();
1849 // Check if serialized workspace has paths before it's moved
1850 let serialized_workspace_has_paths = serialized_workspace
1851 .as_ref()
1852 .map(|ws| !ws.paths.is_empty())
1853 .unwrap_or(false);
1854
1855 let opened_items = window
1856 .update(cx, |_workspace, window, cx| {
1857 open_items(serialized_workspace, project_paths, window, cx)
1858 })?
1859 .await
1860 .unwrap_or_default();
1861
1862 // Restore default dock state for empty workspaces
1863 // Only restore if:
1864 // 1. This is an empty workspace (no paths), AND
1865 // 2. The serialized workspace either doesn't exist or has no paths
1866 if is_empty_workspace && !serialized_workspace_has_paths {
1867 if let Some(default_docks) = persistence::read_default_dock_state() {
1868 window
1869 .update(cx, |workspace, window, cx| {
1870 for (dock, serialized_dock) in [
1871 (&mut workspace.right_dock, default_docks.right),
1872 (&mut workspace.left_dock, default_docks.left),
1873 (&mut workspace.bottom_dock, default_docks.bottom),
1874 ]
1875 .iter_mut()
1876 {
1877 dock.update(cx, |dock, cx| {
1878 dock.serialized_dock = Some(serialized_dock.clone());
1879 dock.restore_state(window, cx);
1880 });
1881 }
1882 cx.notify();
1883 })
1884 .log_err();
1885 }
1886 }
1887
1888 window
1889 .update(cx, |workspace, window, cx| {
1890 window.activate_window();
1891 workspace.update_history(cx);
1892 })
1893 .log_err();
1894 Ok((window, opened_items))
1895 })
1896 }
1897
1898 pub fn weak_handle(&self) -> WeakEntity<Self> {
1899 self.weak_self.clone()
1900 }
1901
1902 pub fn left_dock(&self) -> &Entity<Dock> {
1903 &self.left_dock
1904 }
1905
1906 pub fn bottom_dock(&self) -> &Entity<Dock> {
1907 &self.bottom_dock
1908 }
1909
1910 pub fn set_bottom_dock_layout(
1911 &mut self,
1912 layout: BottomDockLayout,
1913 window: &mut Window,
1914 cx: &mut Context<Self>,
1915 ) {
1916 let fs = self.project().read(cx).fs();
1917 settings::update_settings_file(fs.clone(), cx, move |content, _cx| {
1918 content.workspace.bottom_dock_layout = Some(layout);
1919 });
1920
1921 cx.notify();
1922 self.serialize_workspace(window, cx);
1923 }
1924
1925 pub fn right_dock(&self) -> &Entity<Dock> {
1926 &self.right_dock
1927 }
1928
1929 pub fn all_docks(&self) -> [&Entity<Dock>; 3] {
1930 [&self.left_dock, &self.bottom_dock, &self.right_dock]
1931 }
1932
1933 pub fn dock_at_position(&self, position: DockPosition) -> &Entity<Dock> {
1934 match position {
1935 DockPosition::Left => &self.left_dock,
1936 DockPosition::Bottom => &self.bottom_dock,
1937 DockPosition::Right => &self.right_dock,
1938 }
1939 }
1940
1941 pub fn is_edited(&self) -> bool {
1942 self.window_edited
1943 }
1944
1945 pub fn add_panel<T: Panel>(
1946 &mut self,
1947 panel: Entity<T>,
1948 window: &mut Window,
1949 cx: &mut Context<Self>,
1950 ) {
1951 let focus_handle = panel.panel_focus_handle(cx);
1952 cx.on_focus_in(&focus_handle, window, Self::handle_panel_focused)
1953 .detach();
1954
1955 let dock_position = panel.position(window, cx);
1956 let dock = self.dock_at_position(dock_position);
1957
1958 dock.update(cx, |dock, cx| {
1959 dock.add_panel(panel, self.weak_self.clone(), window, cx)
1960 });
1961 }
1962
1963 pub fn remove_panel<T: Panel>(
1964 &mut self,
1965 panel: &Entity<T>,
1966 window: &mut Window,
1967 cx: &mut Context<Self>,
1968 ) {
1969 let mut found_in_dock = None;
1970 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1971 let found = dock.update(cx, |dock, cx| dock.remove_panel(panel, window, cx));
1972
1973 if found {
1974 found_in_dock = Some(dock.clone());
1975 }
1976 }
1977 if let Some(found_in_dock) = found_in_dock {
1978 let position = found_in_dock.read(cx).position();
1979 let slot = utility_slot_for_dock_position(position);
1980 self.clear_utility_pane_if_provider(slot, Entity::entity_id(panel), cx);
1981 }
1982 }
1983
1984 pub fn status_bar(&self) -> &Entity<StatusBar> {
1985 &self.status_bar
1986 }
1987
1988 pub fn status_bar_visible(&self, cx: &App) -> bool {
1989 StatusBarSettings::get_global(cx).show
1990 }
1991
1992 pub fn app_state(&self) -> &Arc<AppState> {
1993 &self.app_state
1994 }
1995
1996 pub fn user_store(&self) -> &Entity<UserStore> {
1997 &self.app_state.user_store
1998 }
1999
2000 pub fn project(&self) -> &Entity<Project> {
2001 &self.project
2002 }
2003
2004 pub fn path_style(&self, cx: &App) -> PathStyle {
2005 self.project.read(cx).path_style(cx)
2006 }
2007
2008 pub fn recently_activated_items(&self, cx: &App) -> HashMap<EntityId, usize> {
2009 let mut history: HashMap<EntityId, usize> = HashMap::default();
2010
2011 for pane_handle in &self.panes {
2012 let pane = pane_handle.read(cx);
2013
2014 for entry in pane.activation_history() {
2015 history.insert(
2016 entry.entity_id,
2017 history
2018 .get(&entry.entity_id)
2019 .cloned()
2020 .unwrap_or(0)
2021 .max(entry.timestamp),
2022 );
2023 }
2024 }
2025
2026 history
2027 }
2028
2029 pub fn recent_active_item_by_type<T: 'static>(&self, cx: &App) -> Option<Entity<T>> {
2030 let mut recent_item: Option<Entity<T>> = None;
2031 let mut recent_timestamp = 0;
2032 for pane_handle in &self.panes {
2033 let pane = pane_handle.read(cx);
2034 let item_map: HashMap<EntityId, &Box<dyn ItemHandle>> =
2035 pane.items().map(|item| (item.item_id(), item)).collect();
2036 for entry in pane.activation_history() {
2037 if entry.timestamp > recent_timestamp
2038 && let Some(&item) = item_map.get(&entry.entity_id)
2039 && let Some(typed_item) = item.act_as::<T>(cx)
2040 {
2041 recent_timestamp = entry.timestamp;
2042 recent_item = Some(typed_item);
2043 }
2044 }
2045 }
2046 recent_item
2047 }
2048
2049 pub fn recent_navigation_history_iter(
2050 &self,
2051 cx: &App,
2052 ) -> impl Iterator<Item = (ProjectPath, Option<PathBuf>)> + use<> {
2053 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
2054 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
2055
2056 for pane in &self.panes {
2057 let pane = pane.read(cx);
2058
2059 pane.nav_history()
2060 .for_each_entry(cx, |entry, (project_path, fs_path)| {
2061 if let Some(fs_path) = &fs_path {
2062 abs_paths_opened
2063 .entry(fs_path.clone())
2064 .or_default()
2065 .insert(project_path.clone());
2066 }
2067 let timestamp = entry.timestamp;
2068 match history.entry(project_path) {
2069 hash_map::Entry::Occupied(mut entry) => {
2070 let (_, old_timestamp) = entry.get();
2071 if ×tamp > old_timestamp {
2072 entry.insert((fs_path, timestamp));
2073 }
2074 }
2075 hash_map::Entry::Vacant(entry) => {
2076 entry.insert((fs_path, timestamp));
2077 }
2078 }
2079 });
2080
2081 if let Some(item) = pane.active_item()
2082 && let Some(project_path) = item.project_path(cx)
2083 {
2084 let fs_path = self.project.read(cx).absolute_path(&project_path, cx);
2085
2086 if let Some(fs_path) = &fs_path {
2087 abs_paths_opened
2088 .entry(fs_path.clone())
2089 .or_default()
2090 .insert(project_path.clone());
2091 }
2092
2093 history.insert(project_path, (fs_path, std::usize::MAX));
2094 }
2095 }
2096
2097 history
2098 .into_iter()
2099 .sorted_by_key(|(_, (_, order))| *order)
2100 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
2101 .rev()
2102 .filter(move |(history_path, abs_path)| {
2103 let latest_project_path_opened = abs_path
2104 .as_ref()
2105 .and_then(|abs_path| abs_paths_opened.get(abs_path))
2106 .and_then(|project_paths| {
2107 project_paths
2108 .iter()
2109 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
2110 });
2111
2112 latest_project_path_opened.is_none_or(|path| path == history_path)
2113 })
2114 }
2115
2116 pub fn recent_navigation_history(
2117 &self,
2118 limit: Option<usize>,
2119 cx: &App,
2120 ) -> Vec<(ProjectPath, Option<PathBuf>)> {
2121 self.recent_navigation_history_iter(cx)
2122 .take(limit.unwrap_or(usize::MAX))
2123 .collect()
2124 }
2125
2126 pub fn clear_navigation_history(&mut self, _window: &mut Window, cx: &mut Context<Workspace>) {
2127 for pane in &self.panes {
2128 pane.update(cx, |pane, cx| pane.nav_history_mut().clear(cx));
2129 }
2130 }
2131
2132 fn navigate_history(
2133 &mut self,
2134 pane: WeakEntity<Pane>,
2135 mode: NavigationMode,
2136 window: &mut Window,
2137 cx: &mut Context<Workspace>,
2138 ) -> Task<Result<()>> {
2139 self.navigate_history_impl(pane, mode, window, |history, cx| history.pop(mode, cx), cx)
2140 }
2141
2142 fn navigate_tag_history(
2143 &mut self,
2144 pane: WeakEntity<Pane>,
2145 mode: TagNavigationMode,
2146 window: &mut Window,
2147 cx: &mut Context<Workspace>,
2148 ) -> Task<Result<()>> {
2149 self.navigate_history_impl(
2150 pane,
2151 NavigationMode::Normal,
2152 window,
2153 |history, _cx| history.pop_tag(mode),
2154 cx,
2155 )
2156 }
2157
2158 fn navigate_history_impl(
2159 &mut self,
2160 pane: WeakEntity<Pane>,
2161 mode: NavigationMode,
2162 window: &mut Window,
2163 mut cb: impl FnMut(&mut NavHistory, &mut App) -> Option<NavigationEntry>,
2164 cx: &mut Context<Workspace>,
2165 ) -> Task<Result<()>> {
2166 let to_load = if let Some(pane) = pane.upgrade() {
2167 pane.update(cx, |pane, cx| {
2168 window.focus(&pane.focus_handle(cx), cx);
2169 loop {
2170 // Retrieve the weak item handle from the history.
2171 let entry = cb(pane.nav_history_mut(), cx)?;
2172
2173 // If the item is still present in this pane, then activate it.
2174 if let Some(index) = entry
2175 .item
2176 .upgrade()
2177 .and_then(|v| pane.index_for_item(v.as_ref()))
2178 {
2179 let prev_active_item_index = pane.active_item_index();
2180 pane.nav_history_mut().set_mode(mode);
2181 pane.activate_item(index, true, true, window, cx);
2182 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2183
2184 let mut navigated = prev_active_item_index != pane.active_item_index();
2185 if let Some(data) = entry.data {
2186 navigated |= pane.active_item()?.navigate(data, window, cx);
2187 }
2188
2189 if navigated {
2190 break None;
2191 }
2192 } else {
2193 // If the item is no longer present in this pane, then retrieve its
2194 // path info in order to reopen it.
2195 break pane
2196 .nav_history()
2197 .path_for_item(entry.item.id())
2198 .map(|(project_path, abs_path)| (project_path, abs_path, entry));
2199 }
2200 }
2201 })
2202 } else {
2203 None
2204 };
2205
2206 if let Some((project_path, abs_path, entry)) = to_load {
2207 // If the item was no longer present, then load it again from its previous path, first try the local path
2208 let open_by_project_path = self.load_path(project_path.clone(), window, cx);
2209
2210 cx.spawn_in(window, async move |workspace, cx| {
2211 let open_by_project_path = open_by_project_path.await;
2212 let mut navigated = false;
2213 match open_by_project_path
2214 .with_context(|| format!("Navigating to {project_path:?}"))
2215 {
2216 Ok((project_entry_id, build_item)) => {
2217 let prev_active_item_id = pane.update(cx, |pane, _| {
2218 pane.nav_history_mut().set_mode(mode);
2219 pane.active_item().map(|p| p.item_id())
2220 })?;
2221
2222 pane.update_in(cx, |pane, window, cx| {
2223 let item = pane.open_item(
2224 project_entry_id,
2225 project_path,
2226 true,
2227 entry.is_preview,
2228 true,
2229 None,
2230 window, cx,
2231 build_item,
2232 );
2233 navigated |= Some(item.item_id()) != prev_active_item_id;
2234 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2235 if let Some(data) = entry.data {
2236 navigated |= item.navigate(data, window, cx);
2237 }
2238 })?;
2239 }
2240 Err(open_by_project_path_e) => {
2241 // Fall back to opening by abs path, in case an external file was opened and closed,
2242 // and its worktree is now dropped
2243 if let Some(abs_path) = abs_path {
2244 let prev_active_item_id = pane.update(cx, |pane, _| {
2245 pane.nav_history_mut().set_mode(mode);
2246 pane.active_item().map(|p| p.item_id())
2247 })?;
2248 let open_by_abs_path = workspace.update_in(cx, |workspace, window, cx| {
2249 workspace.open_abs_path(abs_path.clone(), OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
2250 })?;
2251 match open_by_abs_path
2252 .await
2253 .with_context(|| format!("Navigating to {abs_path:?}"))
2254 {
2255 Ok(item) => {
2256 pane.update_in(cx, |pane, window, cx| {
2257 navigated |= Some(item.item_id()) != prev_active_item_id;
2258 pane.nav_history_mut().set_mode(NavigationMode::Normal);
2259 if let Some(data) = entry.data {
2260 navigated |= item.navigate(data, window, cx);
2261 }
2262 })?;
2263 }
2264 Err(open_by_abs_path_e) => {
2265 log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
2266 }
2267 }
2268 }
2269 }
2270 }
2271
2272 if !navigated {
2273 workspace
2274 .update_in(cx, |workspace, window, cx| {
2275 Self::navigate_history(workspace, pane, mode, window, cx)
2276 })?
2277 .await?;
2278 }
2279
2280 Ok(())
2281 })
2282 } else {
2283 Task::ready(Ok(()))
2284 }
2285 }
2286
2287 pub fn go_back(
2288 &mut self,
2289 pane: WeakEntity<Pane>,
2290 window: &mut Window,
2291 cx: &mut Context<Workspace>,
2292 ) -> Task<Result<()>> {
2293 self.navigate_history(pane, NavigationMode::GoingBack, window, cx)
2294 }
2295
2296 pub fn go_forward(
2297 &mut self,
2298 pane: WeakEntity<Pane>,
2299 window: &mut Window,
2300 cx: &mut Context<Workspace>,
2301 ) -> Task<Result<()>> {
2302 self.navigate_history(pane, NavigationMode::GoingForward, window, cx)
2303 }
2304
2305 pub fn reopen_closed_item(
2306 &mut self,
2307 window: &mut Window,
2308 cx: &mut Context<Workspace>,
2309 ) -> Task<Result<()>> {
2310 self.navigate_history(
2311 self.active_pane().downgrade(),
2312 NavigationMode::ReopeningClosedItem,
2313 window,
2314 cx,
2315 )
2316 }
2317
2318 pub fn client(&self) -> &Arc<Client> {
2319 &self.app_state.client
2320 }
2321
2322 pub fn set_titlebar_item(&mut self, item: AnyView, _: &mut Window, cx: &mut Context<Self>) {
2323 self.titlebar_item = Some(item);
2324 cx.notify();
2325 }
2326
2327 pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
2328 self.on_prompt_for_new_path = Some(prompt)
2329 }
2330
2331 pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) {
2332 self.on_prompt_for_open_path = Some(prompt)
2333 }
2334
2335 pub fn set_terminal_provider(&mut self, provider: impl TerminalProvider + 'static) {
2336 self.terminal_provider = Some(Box::new(provider));
2337 }
2338
2339 pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) {
2340 self.debugger_provider = Some(Arc::new(provider));
2341 }
2342
2343 pub fn debugger_provider(&self) -> Option<Arc<dyn DebuggerProvider>> {
2344 self.debugger_provider.clone()
2345 }
2346
2347 pub fn prompt_for_open_path(
2348 &mut self,
2349 path_prompt_options: PathPromptOptions,
2350 lister: DirectoryLister,
2351 window: &mut Window,
2352 cx: &mut Context<Self>,
2353 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
2354 if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
2355 let prompt = self.on_prompt_for_open_path.take().unwrap();
2356 let rx = prompt(self, lister, window, cx);
2357 self.on_prompt_for_open_path = Some(prompt);
2358 rx
2359 } else {
2360 let (tx, rx) = oneshot::channel();
2361 let abs_path = cx.prompt_for_paths(path_prompt_options);
2362
2363 cx.spawn_in(window, async move |workspace, cx| {
2364 let Ok(result) = abs_path.await else {
2365 return Ok(());
2366 };
2367
2368 match result {
2369 Ok(result) => {
2370 tx.send(result).ok();
2371 }
2372 Err(err) => {
2373 let rx = workspace.update_in(cx, |workspace, window, cx| {
2374 workspace.show_portal_error(err.to_string(), cx);
2375 let prompt = workspace.on_prompt_for_open_path.take().unwrap();
2376 let rx = prompt(workspace, lister, window, cx);
2377 workspace.on_prompt_for_open_path = Some(prompt);
2378 rx
2379 })?;
2380 if let Ok(path) = rx.await {
2381 tx.send(path).ok();
2382 }
2383 }
2384 };
2385 anyhow::Ok(())
2386 })
2387 .detach();
2388
2389 rx
2390 }
2391 }
2392
2393 pub fn prompt_for_new_path(
2394 &mut self,
2395 lister: DirectoryLister,
2396 suggested_name: Option<String>,
2397 window: &mut Window,
2398 cx: &mut Context<Self>,
2399 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
2400 if self.project.read(cx).is_via_collab()
2401 || self.project.read(cx).is_via_remote_server()
2402 || !WorkspaceSettings::get_global(cx).use_system_path_prompts
2403 {
2404 let prompt = self.on_prompt_for_new_path.take().unwrap();
2405 let rx = prompt(self, lister, suggested_name, window, cx);
2406 self.on_prompt_for_new_path = Some(prompt);
2407 return rx;
2408 }
2409
2410 let (tx, rx) = oneshot::channel();
2411 cx.spawn_in(window, async move |workspace, cx| {
2412 let abs_path = workspace.update(cx, |workspace, cx| {
2413 let relative_to = workspace
2414 .most_recent_active_path(cx)
2415 .and_then(|p| p.parent().map(|p| p.to_path_buf()))
2416 .or_else(|| {
2417 let project = workspace.project.read(cx);
2418 project.visible_worktrees(cx).find_map(|worktree| {
2419 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
2420 })
2421 })
2422 .or_else(std::env::home_dir)
2423 .unwrap_or_else(|| PathBuf::from(""));
2424 cx.prompt_for_new_path(&relative_to, suggested_name.as_deref())
2425 })?;
2426 let abs_path = match abs_path.await? {
2427 Ok(path) => path,
2428 Err(err) => {
2429 let rx = workspace.update_in(cx, |workspace, window, cx| {
2430 workspace.show_portal_error(err.to_string(), cx);
2431
2432 let prompt = workspace.on_prompt_for_new_path.take().unwrap();
2433 let rx = prompt(workspace, lister, suggested_name, window, cx);
2434 workspace.on_prompt_for_new_path = Some(prompt);
2435 rx
2436 })?;
2437 if let Ok(path) = rx.await {
2438 tx.send(path).ok();
2439 }
2440 return anyhow::Ok(());
2441 }
2442 };
2443
2444 tx.send(abs_path.map(|path| vec![path])).ok();
2445 anyhow::Ok(())
2446 })
2447 .detach();
2448
2449 rx
2450 }
2451
2452 pub fn titlebar_item(&self) -> Option<AnyView> {
2453 self.titlebar_item.clone()
2454 }
2455
2456 /// Returns the worktree override set by the user (e.g., via the project dropdown).
2457 /// When set, git-related operations should use this worktree instead of deriving
2458 /// the active worktree from the focused file.
2459 pub fn active_worktree_override(&self) -> Option<WorktreeId> {
2460 self.active_worktree_override
2461 }
2462
2463 pub fn set_active_worktree_override(
2464 &mut self,
2465 worktree_id: Option<WorktreeId>,
2466 cx: &mut Context<Self>,
2467 ) {
2468 self.active_worktree_override = worktree_id;
2469 cx.notify();
2470 }
2471
2472 pub fn clear_active_worktree_override(&mut self, cx: &mut Context<Self>) {
2473 self.active_worktree_override = None;
2474 cx.notify();
2475 }
2476
2477 /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
2478 ///
2479 /// If the given workspace has a local project, then it will be passed
2480 /// to the callback. Otherwise, a new empty window will be created.
2481 pub fn with_local_workspace<T, F>(
2482 &mut self,
2483 window: &mut Window,
2484 cx: &mut Context<Self>,
2485 callback: F,
2486 ) -> Task<Result<T>>
2487 where
2488 T: 'static,
2489 F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
2490 {
2491 if self.project.read(cx).is_local() {
2492 Task::ready(Ok(callback(self, window, cx)))
2493 } else {
2494 let env = self.project.read(cx).cli_environment(cx);
2495 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, None, cx);
2496 cx.spawn_in(window, async move |_vh, cx| {
2497 let (workspace, _) = task.await?;
2498 workspace.update(cx, callback)
2499 })
2500 }
2501 }
2502
2503 /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
2504 ///
2505 /// If the given workspace has a local project, then it will be passed
2506 /// to the callback. Otherwise, a new empty window will be created.
2507 pub fn with_local_or_wsl_workspace<T, F>(
2508 &mut self,
2509 window: &mut Window,
2510 cx: &mut Context<Self>,
2511 callback: F,
2512 ) -> Task<Result<T>>
2513 where
2514 T: 'static,
2515 F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
2516 {
2517 let project = self.project.read(cx);
2518 if project.is_local() || project.is_via_wsl_with_host_interop(cx) {
2519 Task::ready(Ok(callback(self, window, cx)))
2520 } else {
2521 let env = self.project.read(cx).cli_environment(cx);
2522 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, None, cx);
2523 cx.spawn_in(window, async move |_vh, cx| {
2524 let (workspace, _) = task.await?;
2525 workspace.update(cx, callback)
2526 })
2527 }
2528 }
2529
2530 pub fn worktrees<'a>(&self, cx: &'a App) -> impl 'a + Iterator<Item = Entity<Worktree>> {
2531 self.project.read(cx).worktrees(cx)
2532 }
2533
2534 pub fn visible_worktrees<'a>(
2535 &self,
2536 cx: &'a App,
2537 ) -> impl 'a + Iterator<Item = Entity<Worktree>> {
2538 self.project.read(cx).visible_worktrees(cx)
2539 }
2540
2541 #[cfg(any(test, feature = "test-support"))]
2542 pub fn worktree_scans_complete(&self, cx: &App) -> impl Future<Output = ()> + 'static + use<> {
2543 let futures = self
2544 .worktrees(cx)
2545 .filter_map(|worktree| worktree.read(cx).as_local())
2546 .map(|worktree| worktree.scan_complete())
2547 .collect::<Vec<_>>();
2548 async move {
2549 for future in futures {
2550 future.await;
2551 }
2552 }
2553 }
2554
2555 pub fn close_global(cx: &mut App) {
2556 cx.defer(|cx| {
2557 cx.windows().iter().find(|window| {
2558 window
2559 .update(cx, |_, window, _| {
2560 if window.is_window_active() {
2561 //This can only get called when the window's project connection has been lost
2562 //so we don't need to prompt the user for anything and instead just close the window
2563 window.remove_window();
2564 true
2565 } else {
2566 false
2567 }
2568 })
2569 .unwrap_or(false)
2570 });
2571 });
2572 }
2573
2574 pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
2575 let prepare = self.prepare_to_close(CloseIntent::CloseWindow, window, cx);
2576 cx.spawn_in(window, async move |_, cx| {
2577 if prepare.await? {
2578 cx.update(|window, _cx| window.remove_window())?;
2579 }
2580 anyhow::Ok(())
2581 })
2582 .detach_and_log_err(cx)
2583 }
2584
2585 pub fn move_focused_panel_to_next_position(
2586 &mut self,
2587 _: &MoveFocusedPanelToNextPosition,
2588 window: &mut Window,
2589 cx: &mut Context<Self>,
2590 ) {
2591 let docks = self.all_docks();
2592 let active_dock = docks
2593 .into_iter()
2594 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
2595
2596 if let Some(dock) = active_dock {
2597 dock.update(cx, |dock, cx| {
2598 let active_panel = dock
2599 .active_panel()
2600 .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx));
2601
2602 if let Some(panel) = active_panel {
2603 panel.move_to_next_position(window, cx);
2604 }
2605 })
2606 }
2607 }
2608
2609 pub fn prepare_to_close(
2610 &mut self,
2611 close_intent: CloseIntent,
2612 window: &mut Window,
2613 cx: &mut Context<Self>,
2614 ) -> Task<Result<bool>> {
2615 let active_call = self.active_call().cloned();
2616
2617 cx.spawn_in(window, async move |this, cx| {
2618 this.update(cx, |this, _| {
2619 if close_intent == CloseIntent::CloseWindow {
2620 this.removing = true;
2621 }
2622 })?;
2623
2624 let workspace_count = cx.update(|_window, cx| {
2625 cx.windows()
2626 .iter()
2627 .filter(|window| window.downcast::<Workspace>().is_some())
2628 .count()
2629 })?;
2630
2631 #[cfg(target_os = "macos")]
2632 let save_last_workspace = false;
2633
2634 // On Linux and Windows, closing the last window should restore the last workspace.
2635 #[cfg(not(target_os = "macos"))]
2636 let save_last_workspace = {
2637 let remaining_workspaces = cx.update(|_window, cx| {
2638 cx.windows()
2639 .iter()
2640 .filter_map(|window| window.downcast::<Workspace>())
2641 .filter_map(|workspace| {
2642 workspace
2643 .update(cx, |workspace, _, _| workspace.removing)
2644 .ok()
2645 })
2646 .filter(|removing| !removing)
2647 .count()
2648 })?;
2649
2650 close_intent != CloseIntent::ReplaceWindow && remaining_workspaces == 0
2651 };
2652
2653 if let Some(active_call) = active_call
2654 && workspace_count == 1
2655 && active_call.read_with(cx, |call, _| call.room().is_some())
2656 {
2657 if close_intent == CloseIntent::CloseWindow {
2658 let answer = cx.update(|window, cx| {
2659 window.prompt(
2660 PromptLevel::Warning,
2661 "Do you want to leave the current call?",
2662 None,
2663 &["Close window and hang up", "Cancel"],
2664 cx,
2665 )
2666 })?;
2667
2668 if answer.await.log_err() == Some(1) {
2669 return anyhow::Ok(false);
2670 } else {
2671 active_call
2672 .update(cx, |call, cx| call.hang_up(cx))
2673 .await
2674 .log_err();
2675 }
2676 }
2677 if close_intent == CloseIntent::ReplaceWindow {
2678 _ = active_call.update(cx, |this, cx| {
2679 let workspace = cx
2680 .windows()
2681 .iter()
2682 .filter_map(|window| window.downcast::<Workspace>())
2683 .next()
2684 .unwrap();
2685 let project = workspace.read(cx)?.project.clone();
2686 if project.read(cx).is_shared() {
2687 this.unshare_project(project, cx)?;
2688 }
2689 Ok::<_, anyhow::Error>(())
2690 })?;
2691 }
2692 }
2693
2694 let save_result = this
2695 .update_in(cx, |this, window, cx| {
2696 this.save_all_internal(SaveIntent::Close, window, cx)
2697 })?
2698 .await;
2699
2700 // If we're not quitting, but closing, we remove the workspace from
2701 // the current session.
2702 if close_intent != CloseIntent::Quit
2703 && !save_last_workspace
2704 && save_result.as_ref().is_ok_and(|&res| res)
2705 {
2706 this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx))?
2707 .await;
2708 }
2709
2710 save_result
2711 })
2712 }
2713
2714 fn save_all(&mut self, action: &SaveAll, window: &mut Window, cx: &mut Context<Self>) {
2715 self.save_all_internal(
2716 action.save_intent.unwrap_or(SaveIntent::SaveAll),
2717 window,
2718 cx,
2719 )
2720 .detach_and_log_err(cx);
2721 }
2722
2723 fn send_keystrokes(
2724 &mut self,
2725 action: &SendKeystrokes,
2726 window: &mut Window,
2727 cx: &mut Context<Self>,
2728 ) {
2729 let keystrokes: Vec<Keystroke> = action
2730 .0
2731 .split(' ')
2732 .flat_map(|k| Keystroke::parse(k).log_err())
2733 .map(|k| {
2734 cx.keyboard_mapper()
2735 .map_key_equivalent(k, false)
2736 .inner()
2737 .clone()
2738 })
2739 .collect();
2740 let _ = self.send_keystrokes_impl(keystrokes, window, cx);
2741 }
2742
2743 pub fn send_keystrokes_impl(
2744 &mut self,
2745 keystrokes: Vec<Keystroke>,
2746 window: &mut Window,
2747 cx: &mut Context<Self>,
2748 ) -> Shared<Task<()>> {
2749 let mut state = self.dispatching_keystrokes.borrow_mut();
2750 if !state.dispatched.insert(keystrokes.clone()) {
2751 cx.propagate();
2752 return state.task.clone().unwrap();
2753 }
2754
2755 state.queue.extend(keystrokes);
2756
2757 let keystrokes = self.dispatching_keystrokes.clone();
2758 if state.task.is_none() {
2759 state.task = Some(
2760 window
2761 .spawn(cx, async move |cx| {
2762 // limit to 100 keystrokes to avoid infinite recursion.
2763 for _ in 0..100 {
2764 let mut state = keystrokes.borrow_mut();
2765 let Some(keystroke) = state.queue.pop_front() else {
2766 state.dispatched.clear();
2767 state.task.take();
2768 return;
2769 };
2770 drop(state);
2771 cx.update(|window, cx| {
2772 let focused = window.focused(cx);
2773 window.dispatch_keystroke(keystroke.clone(), cx);
2774 if window.focused(cx) != focused {
2775 // dispatch_keystroke may cause the focus to change.
2776 // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
2777 // And we need that to happen before the next keystroke to keep vim mode happy...
2778 // (Note that the tests always do this implicitly, so you must manually test with something like:
2779 // "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
2780 // )
2781 window.draw(cx).clear();
2782 }
2783 })
2784 .ok();
2785 }
2786
2787 *keystrokes.borrow_mut() = Default::default();
2788 log::error!("over 100 keystrokes passed to send_keystrokes");
2789 })
2790 .shared(),
2791 );
2792 }
2793 state.task.clone().unwrap()
2794 }
2795
2796 fn save_all_internal(
2797 &mut self,
2798 mut save_intent: SaveIntent,
2799 window: &mut Window,
2800 cx: &mut Context<Self>,
2801 ) -> Task<Result<bool>> {
2802 if self.project.read(cx).is_disconnected(cx) {
2803 return Task::ready(Ok(true));
2804 }
2805 let dirty_items = self
2806 .panes
2807 .iter()
2808 .flat_map(|pane| {
2809 pane.read(cx).items().filter_map(|item| {
2810 if item.is_dirty(cx) {
2811 item.tab_content_text(0, cx);
2812 Some((pane.downgrade(), item.boxed_clone()))
2813 } else {
2814 None
2815 }
2816 })
2817 })
2818 .collect::<Vec<_>>();
2819
2820 let project = self.project.clone();
2821 cx.spawn_in(window, async move |workspace, cx| {
2822 let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() {
2823 let (serialize_tasks, remaining_dirty_items) =
2824 workspace.update_in(cx, |workspace, window, cx| {
2825 let mut remaining_dirty_items = Vec::new();
2826 let mut serialize_tasks = Vec::new();
2827 for (pane, item) in dirty_items {
2828 if let Some(task) = item
2829 .to_serializable_item_handle(cx)
2830 .and_then(|handle| handle.serialize(workspace, true, window, cx))
2831 {
2832 serialize_tasks.push(task);
2833 } else {
2834 remaining_dirty_items.push((pane, item));
2835 }
2836 }
2837 (serialize_tasks, remaining_dirty_items)
2838 })?;
2839
2840 futures::future::try_join_all(serialize_tasks).await?;
2841
2842 if remaining_dirty_items.len() > 1 {
2843 let answer = workspace.update_in(cx, |_, window, cx| {
2844 let detail = Pane::file_names_for_prompt(
2845 &mut remaining_dirty_items.iter().map(|(_, handle)| handle),
2846 cx,
2847 );
2848 window.prompt(
2849 PromptLevel::Warning,
2850 "Do you want to save all changes in the following files?",
2851 Some(&detail),
2852 &["Save all", "Discard all", "Cancel"],
2853 cx,
2854 )
2855 })?;
2856 match answer.await.log_err() {
2857 Some(0) => save_intent = SaveIntent::SaveAll,
2858 Some(1) => save_intent = SaveIntent::Skip,
2859 Some(2) => return Ok(false),
2860 _ => {}
2861 }
2862 }
2863
2864 remaining_dirty_items
2865 } else {
2866 dirty_items
2867 };
2868
2869 for (pane, item) in dirty_items {
2870 let (singleton, project_entry_ids) = cx.update(|_, cx| {
2871 (
2872 item.buffer_kind(cx) == ItemBufferKind::Singleton,
2873 item.project_entry_ids(cx),
2874 )
2875 })?;
2876 if (singleton || !project_entry_ids.is_empty())
2877 && !Pane::save_item(project.clone(), &pane, &*item, save_intent, cx).await?
2878 {
2879 return Ok(false);
2880 }
2881 }
2882 Ok(true)
2883 })
2884 }
2885
2886 pub fn open_workspace_for_paths(
2887 &mut self,
2888 replace_current_window: bool,
2889 paths: Vec<PathBuf>,
2890 window: &mut Window,
2891 cx: &mut Context<Self>,
2892 ) -> Task<Result<()>> {
2893 let window_handle = window.window_handle().downcast::<Self>();
2894 let is_remote = self.project.read(cx).is_via_collab();
2895 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
2896 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
2897
2898 let window_to_replace = if replace_current_window {
2899 window_handle
2900 } else if is_remote || has_worktree || has_dirty_items {
2901 None
2902 } else {
2903 window_handle
2904 };
2905 let app_state = self.app_state.clone();
2906
2907 cx.spawn(async move |_, cx| {
2908 cx.update(|cx| {
2909 open_paths(
2910 &paths,
2911 app_state,
2912 OpenOptions {
2913 replace_window: window_to_replace,
2914 ..Default::default()
2915 },
2916 cx,
2917 )
2918 })
2919 .await?;
2920 Ok(())
2921 })
2922 }
2923
2924 #[allow(clippy::type_complexity)]
2925 pub fn open_paths(
2926 &mut self,
2927 mut abs_paths: Vec<PathBuf>,
2928 options: OpenOptions,
2929 pane: Option<WeakEntity<Pane>>,
2930 window: &mut Window,
2931 cx: &mut Context<Self>,
2932 ) -> Task<Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>> {
2933 let fs = self.app_state.fs.clone();
2934
2935 let caller_ordered_abs_paths = abs_paths.clone();
2936
2937 // Sort the paths to ensure we add worktrees for parents before their children.
2938 abs_paths.sort_unstable();
2939 cx.spawn_in(window, async move |this, cx| {
2940 let mut tasks = Vec::with_capacity(abs_paths.len());
2941
2942 for abs_path in &abs_paths {
2943 let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
2944 OpenVisible::All => Some(true),
2945 OpenVisible::None => Some(false),
2946 OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
2947 Some(Some(metadata)) => Some(!metadata.is_dir),
2948 Some(None) => Some(true),
2949 None => None,
2950 },
2951 OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
2952 Some(Some(metadata)) => Some(metadata.is_dir),
2953 Some(None) => Some(false),
2954 None => None,
2955 },
2956 };
2957 let project_path = match visible {
2958 Some(visible) => match this
2959 .update(cx, |this, cx| {
2960 Workspace::project_path_for_path(
2961 this.project.clone(),
2962 abs_path,
2963 visible,
2964 cx,
2965 )
2966 })
2967 .log_err()
2968 {
2969 Some(project_path) => project_path.await.log_err(),
2970 None => None,
2971 },
2972 None => None,
2973 };
2974
2975 let this = this.clone();
2976 let abs_path: Arc<Path> = SanitizedPath::new(&abs_path).as_path().into();
2977 let fs = fs.clone();
2978 let pane = pane.clone();
2979 let task = cx.spawn(async move |cx| {
2980 let (_worktree, project_path) = project_path?;
2981 if fs.is_dir(&abs_path).await {
2982 // Opening a directory should not race to update the active entry.
2983 // We'll select/reveal a deterministic final entry after all paths finish opening.
2984 None
2985 } else {
2986 Some(
2987 this.update_in(cx, |this, window, cx| {
2988 this.open_path(
2989 project_path,
2990 pane,
2991 options.focus.unwrap_or(true),
2992 window,
2993 cx,
2994 )
2995 })
2996 .ok()?
2997 .await,
2998 )
2999 }
3000 });
3001 tasks.push(task);
3002 }
3003
3004 let results = futures::future::join_all(tasks).await;
3005
3006 // Determine the winner using the fake/abstract FS metadata, not `Path::is_dir`.
3007 let mut winner: Option<(PathBuf, bool)> = None;
3008 for abs_path in caller_ordered_abs_paths.into_iter().rev() {
3009 if let Some(Some(metadata)) = fs.metadata(&abs_path).await.log_err() {
3010 if !metadata.is_dir {
3011 winner = Some((abs_path, false));
3012 break;
3013 }
3014 if winner.is_none() {
3015 winner = Some((abs_path, true));
3016 }
3017 } else if winner.is_none() {
3018 winner = Some((abs_path, false));
3019 }
3020 }
3021
3022 // Compute the winner entry id on the foreground thread and emit once, after all
3023 // paths finish opening. This avoids races between concurrently-opening paths
3024 // (directories in particular) and makes the resulting project panel selection
3025 // deterministic.
3026 if let Some((winner_abs_path, winner_is_dir)) = winner {
3027 'emit_winner: {
3028 let winner_abs_path: Arc<Path> =
3029 SanitizedPath::new(&winner_abs_path).as_path().into();
3030
3031 let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
3032 OpenVisible::All => true,
3033 OpenVisible::None => false,
3034 OpenVisible::OnlyFiles => !winner_is_dir,
3035 OpenVisible::OnlyDirectories => winner_is_dir,
3036 };
3037
3038 let Some(worktree_task) = this
3039 .update(cx, |workspace, cx| {
3040 workspace.project.update(cx, |project, cx| {
3041 project.find_or_create_worktree(
3042 winner_abs_path.as_ref(),
3043 visible,
3044 cx,
3045 )
3046 })
3047 })
3048 .ok()
3049 else {
3050 break 'emit_winner;
3051 };
3052
3053 let Ok((worktree, _)) = worktree_task.await else {
3054 break 'emit_winner;
3055 };
3056
3057 let Ok(Some(entry_id)) = this.update(cx, |_, cx| {
3058 let worktree = worktree.read(cx);
3059 let worktree_abs_path = worktree.abs_path();
3060 let entry = if winner_abs_path.as_ref() == worktree_abs_path.as_ref() {
3061 worktree.root_entry()
3062 } else {
3063 winner_abs_path
3064 .strip_prefix(worktree_abs_path.as_ref())
3065 .ok()
3066 .and_then(|relative_path| {
3067 let relative_path =
3068 RelPath::new(relative_path, PathStyle::local())
3069 .log_err()?;
3070 worktree.entry_for_path(&relative_path)
3071 })
3072 }?;
3073 Some(entry.id)
3074 }) else {
3075 break 'emit_winner;
3076 };
3077
3078 this.update(cx, |workspace, cx| {
3079 workspace.project.update(cx, |_, cx| {
3080 cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
3081 });
3082 })
3083 .ok();
3084 }
3085 }
3086
3087 results
3088 })
3089 }
3090
3091 pub fn open_resolved_path(
3092 &mut self,
3093 path: ResolvedPath,
3094 window: &mut Window,
3095 cx: &mut Context<Self>,
3096 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3097 match path {
3098 ResolvedPath::ProjectPath { project_path, .. } => {
3099 self.open_path(project_path, None, true, window, cx)
3100 }
3101 ResolvedPath::AbsPath { path, .. } => self.open_abs_path(
3102 PathBuf::from(path),
3103 OpenOptions {
3104 visible: Some(OpenVisible::None),
3105 ..Default::default()
3106 },
3107 window,
3108 cx,
3109 ),
3110 }
3111 }
3112
3113 pub fn absolute_path_of_worktree(
3114 &self,
3115 worktree_id: WorktreeId,
3116 cx: &mut Context<Self>,
3117 ) -> Option<PathBuf> {
3118 self.project
3119 .read(cx)
3120 .worktree_for_id(worktree_id, cx)
3121 // TODO: use `abs_path` or `root_dir`
3122 .map(|wt| wt.read(cx).abs_path().as_ref().to_path_buf())
3123 }
3124
3125 fn add_folder_to_project(
3126 &mut self,
3127 _: &AddFolderToProject,
3128 window: &mut Window,
3129 cx: &mut Context<Self>,
3130 ) {
3131 let project = self.project.read(cx);
3132 if project.is_via_collab() {
3133 self.show_error(
3134 &anyhow!("You cannot add folders to someone else's project"),
3135 cx,
3136 );
3137 return;
3138 }
3139 let paths = self.prompt_for_open_path(
3140 PathPromptOptions {
3141 files: false,
3142 directories: true,
3143 multiple: true,
3144 prompt: None,
3145 },
3146 DirectoryLister::Project(self.project.clone()),
3147 window,
3148 cx,
3149 );
3150 cx.spawn_in(window, async move |this, cx| {
3151 if let Some(paths) = paths.await.log_err().flatten() {
3152 let results = this
3153 .update_in(cx, |this, window, cx| {
3154 this.open_paths(
3155 paths,
3156 OpenOptions {
3157 visible: Some(OpenVisible::All),
3158 ..Default::default()
3159 },
3160 None,
3161 window,
3162 cx,
3163 )
3164 })?
3165 .await;
3166 for result in results.into_iter().flatten() {
3167 result.log_err();
3168 }
3169 }
3170 anyhow::Ok(())
3171 })
3172 .detach_and_log_err(cx);
3173 }
3174
3175 pub fn project_path_for_path(
3176 project: Entity<Project>,
3177 abs_path: &Path,
3178 visible: bool,
3179 cx: &mut App,
3180 ) -> Task<Result<(Entity<Worktree>, ProjectPath)>> {
3181 let entry = project.update(cx, |project, cx| {
3182 project.find_or_create_worktree(abs_path, visible, cx)
3183 });
3184 cx.spawn(async move |cx| {
3185 let (worktree, path) = entry.await?;
3186 let worktree_id = worktree.read_with(cx, |t, _| t.id());
3187 Ok((worktree, ProjectPath { worktree_id, path }))
3188 })
3189 }
3190
3191 pub fn items<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator<Item = &'a Box<dyn ItemHandle>> {
3192 self.panes.iter().flat_map(|pane| pane.read(cx).items())
3193 }
3194
3195 pub fn item_of_type<T: Item>(&self, cx: &App) -> Option<Entity<T>> {
3196 self.items_of_type(cx).max_by_key(|item| item.item_id())
3197 }
3198
3199 pub fn items_of_type<'a, T: Item>(
3200 &'a self,
3201 cx: &'a App,
3202 ) -> impl 'a + Iterator<Item = Entity<T>> {
3203 self.panes
3204 .iter()
3205 .flat_map(|pane| pane.read(cx).items_of_type())
3206 }
3207
3208 pub fn active_item(&self, cx: &App) -> Option<Box<dyn ItemHandle>> {
3209 self.active_pane().read(cx).active_item()
3210 }
3211
3212 pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
3213 let item = self.active_item(cx)?;
3214 item.to_any_view().downcast::<I>().ok()
3215 }
3216
3217 fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
3218 self.active_item(cx).and_then(|item| item.project_path(cx))
3219 }
3220
3221 pub fn most_recent_active_path(&self, cx: &App) -> Option<PathBuf> {
3222 self.recent_navigation_history_iter(cx)
3223 .filter_map(|(path, abs_path)| {
3224 let worktree = self
3225 .project
3226 .read(cx)
3227 .worktree_for_id(path.worktree_id, cx)?;
3228 if worktree.read(cx).is_visible() {
3229 abs_path
3230 } else {
3231 None
3232 }
3233 })
3234 .next()
3235 }
3236
3237 pub fn save_active_item(
3238 &mut self,
3239 save_intent: SaveIntent,
3240 window: &mut Window,
3241 cx: &mut App,
3242 ) -> Task<Result<()>> {
3243 let project = self.project.clone();
3244 let pane = self.active_pane();
3245 let item = pane.read(cx).active_item();
3246 let pane = pane.downgrade();
3247
3248 window.spawn(cx, async move |cx| {
3249 if let Some(item) = item {
3250 Pane::save_item(project, &pane, item.as_ref(), save_intent, cx)
3251 .await
3252 .map(|_| ())
3253 } else {
3254 Ok(())
3255 }
3256 })
3257 }
3258
3259 pub fn close_inactive_items_and_panes(
3260 &mut self,
3261 action: &CloseInactiveTabsAndPanes,
3262 window: &mut Window,
3263 cx: &mut Context<Self>,
3264 ) {
3265 if let Some(task) = self.close_all_internal(
3266 true,
3267 action.save_intent.unwrap_or(SaveIntent::Close),
3268 window,
3269 cx,
3270 ) {
3271 task.detach_and_log_err(cx)
3272 }
3273 }
3274
3275 pub fn close_all_items_and_panes(
3276 &mut self,
3277 action: &CloseAllItemsAndPanes,
3278 window: &mut Window,
3279 cx: &mut Context<Self>,
3280 ) {
3281 if let Some(task) = self.close_all_internal(
3282 false,
3283 action.save_intent.unwrap_or(SaveIntent::Close),
3284 window,
3285 cx,
3286 ) {
3287 task.detach_and_log_err(cx)
3288 }
3289 }
3290
3291 fn close_all_internal(
3292 &mut self,
3293 retain_active_pane: bool,
3294 save_intent: SaveIntent,
3295 window: &mut Window,
3296 cx: &mut Context<Self>,
3297 ) -> Option<Task<Result<()>>> {
3298 let current_pane = self.active_pane();
3299
3300 let mut tasks = Vec::new();
3301
3302 if retain_active_pane {
3303 let current_pane_close = current_pane.update(cx, |pane, cx| {
3304 pane.close_other_items(
3305 &CloseOtherItems {
3306 save_intent: None,
3307 close_pinned: false,
3308 },
3309 None,
3310 window,
3311 cx,
3312 )
3313 });
3314
3315 tasks.push(current_pane_close);
3316 }
3317
3318 for pane in self.panes() {
3319 if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
3320 continue;
3321 }
3322
3323 let close_pane_items = pane.update(cx, |pane: &mut Pane, cx| {
3324 pane.close_all_items(
3325 &CloseAllItems {
3326 save_intent: Some(save_intent),
3327 close_pinned: false,
3328 },
3329 window,
3330 cx,
3331 )
3332 });
3333
3334 tasks.push(close_pane_items)
3335 }
3336
3337 if tasks.is_empty() {
3338 None
3339 } else {
3340 Some(cx.spawn_in(window, async move |_, _| {
3341 for task in tasks {
3342 task.await?
3343 }
3344 Ok(())
3345 }))
3346 }
3347 }
3348
3349 pub fn is_dock_at_position_open(&self, position: DockPosition, cx: &mut Context<Self>) -> bool {
3350 self.dock_at_position(position).read(cx).is_open()
3351 }
3352
3353 pub fn toggle_dock(
3354 &mut self,
3355 dock_side: DockPosition,
3356 window: &mut Window,
3357 cx: &mut Context<Self>,
3358 ) {
3359 let mut focus_center = false;
3360 let mut reveal_dock = false;
3361
3362 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
3363 let was_visible = self.is_dock_at_position_open(dock_side, cx) && !other_is_zoomed;
3364
3365 if let Some(panel) = self.dock_at_position(dock_side).read(cx).active_panel() {
3366 telemetry::event!(
3367 "Panel Button Clicked",
3368 name = panel.persistent_name(),
3369 toggle_state = !was_visible
3370 );
3371 }
3372 if was_visible {
3373 self.save_open_dock_positions(cx);
3374 }
3375
3376 let dock = self.dock_at_position(dock_side);
3377 dock.update(cx, |dock, cx| {
3378 dock.set_open(!was_visible, window, cx);
3379
3380 if dock.active_panel().is_none() {
3381 let Some(panel_ix) = dock
3382 .first_enabled_panel_idx(cx)
3383 .log_with_level(log::Level::Info)
3384 else {
3385 return;
3386 };
3387 dock.activate_panel(panel_ix, window, cx);
3388 }
3389
3390 if let Some(active_panel) = dock.active_panel() {
3391 if was_visible {
3392 if active_panel
3393 .panel_focus_handle(cx)
3394 .contains_focused(window, cx)
3395 {
3396 focus_center = true;
3397 }
3398 } else {
3399 let focus_handle = &active_panel.panel_focus_handle(cx);
3400 window.focus(focus_handle, cx);
3401 reveal_dock = true;
3402 }
3403 }
3404 });
3405
3406 if reveal_dock {
3407 self.dismiss_zoomed_items_to_reveal(Some(dock_side), window, cx);
3408 }
3409
3410 if focus_center {
3411 self.active_pane
3412 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
3413 }
3414
3415 cx.notify();
3416 self.serialize_workspace(window, cx);
3417 }
3418
3419 fn active_dock(&self, window: &Window, cx: &Context<Self>) -> Option<&Entity<Dock>> {
3420 self.all_docks().into_iter().find(|&dock| {
3421 dock.read(cx).is_open() && dock.focus_handle(cx).contains_focused(window, cx)
3422 })
3423 }
3424
3425 fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
3426 if let Some(dock) = self.active_dock(window, cx).cloned() {
3427 self.save_open_dock_positions(cx);
3428 dock.update(cx, |dock, cx| {
3429 dock.set_open(false, window, cx);
3430 });
3431 return true;
3432 }
3433 false
3434 }
3435
3436 pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3437 self.save_open_dock_positions(cx);
3438 for dock in self.all_docks() {
3439 dock.update(cx, |dock, cx| {
3440 dock.set_open(false, window, cx);
3441 });
3442 }
3443
3444 cx.focus_self(window);
3445 cx.notify();
3446 self.serialize_workspace(window, cx);
3447 }
3448
3449 fn get_open_dock_positions(&self, cx: &Context<Self>) -> Vec<DockPosition> {
3450 self.all_docks()
3451 .into_iter()
3452 .filter_map(|dock| {
3453 let dock_ref = dock.read(cx);
3454 if dock_ref.is_open() {
3455 Some(dock_ref.position())
3456 } else {
3457 None
3458 }
3459 })
3460 .collect()
3461 }
3462
3463 /// Saves the positions of currently open docks.
3464 ///
3465 /// Updates `last_open_dock_positions` with positions of all currently open
3466 /// docks, to later be restored by the 'Toggle All Docks' action.
3467 fn save_open_dock_positions(&mut self, cx: &mut Context<Self>) {
3468 let open_dock_positions = self.get_open_dock_positions(cx);
3469 if !open_dock_positions.is_empty() {
3470 self.last_open_dock_positions = open_dock_positions;
3471 }
3472 }
3473
3474 /// Toggles all docks between open and closed states.
3475 ///
3476 /// If any docks are open, closes all and remembers their positions. If all
3477 /// docks are closed, restores the last remembered dock configuration.
3478 fn toggle_all_docks(
3479 &mut self,
3480 _: &ToggleAllDocks,
3481 window: &mut Window,
3482 cx: &mut Context<Self>,
3483 ) {
3484 let open_dock_positions = self.get_open_dock_positions(cx);
3485
3486 if !open_dock_positions.is_empty() {
3487 self.close_all_docks(window, cx);
3488 } else if !self.last_open_dock_positions.is_empty() {
3489 self.restore_last_open_docks(window, cx);
3490 }
3491 }
3492
3493 /// Reopens docks from the most recently remembered configuration.
3494 ///
3495 /// Opens all docks whose positions are stored in `last_open_dock_positions`
3496 /// and clears the stored positions.
3497 fn restore_last_open_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3498 let positions_to_open = std::mem::take(&mut self.last_open_dock_positions);
3499
3500 for position in positions_to_open {
3501 let dock = self.dock_at_position(position);
3502 dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
3503 }
3504
3505 cx.focus_self(window);
3506 cx.notify();
3507 self.serialize_workspace(window, cx);
3508 }
3509
3510 /// Transfer focus to the panel of the given type.
3511 pub fn focus_panel<T: Panel>(
3512 &mut self,
3513 window: &mut Window,
3514 cx: &mut Context<Self>,
3515 ) -> Option<Entity<T>> {
3516 let panel = self.focus_or_unfocus_panel::<T>(window, cx, |_, _, _| true)?;
3517 panel.to_any().downcast().ok()
3518 }
3519
3520 /// Focus the panel of the given type if it isn't already focused. If it is
3521 /// already focused, then transfer focus back to the workspace center.
3522 pub fn toggle_panel_focus<T: Panel>(
3523 &mut self,
3524 window: &mut Window,
3525 cx: &mut Context<Self>,
3526 ) -> bool {
3527 let mut did_focus_panel = false;
3528 self.focus_or_unfocus_panel::<T>(window, cx, |panel, window, cx| {
3529 did_focus_panel = !panel.panel_focus_handle(cx).contains_focused(window, cx);
3530 did_focus_panel
3531 });
3532
3533 telemetry::event!(
3534 "Panel Button Clicked",
3535 name = T::persistent_name(),
3536 toggle_state = did_focus_panel
3537 );
3538
3539 did_focus_panel
3540 }
3541
3542 pub fn activate_panel_for_proto_id(
3543 &mut self,
3544 panel_id: PanelId,
3545 window: &mut Window,
3546 cx: &mut Context<Self>,
3547 ) -> Option<Arc<dyn PanelHandle>> {
3548 let mut panel = None;
3549 for dock in self.all_docks() {
3550 if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
3551 panel = dock.update(cx, |dock, cx| {
3552 dock.activate_panel(panel_index, window, cx);
3553 dock.set_open(true, window, cx);
3554 dock.active_panel().cloned()
3555 });
3556 break;
3557 }
3558 }
3559
3560 if panel.is_some() {
3561 cx.notify();
3562 self.serialize_workspace(window, cx);
3563 }
3564
3565 panel
3566 }
3567
3568 /// Focus or unfocus the given panel type, depending on the given callback.
3569 fn focus_or_unfocus_panel<T: Panel>(
3570 &mut self,
3571 window: &mut Window,
3572 cx: &mut Context<Self>,
3573 mut should_focus: impl FnMut(&dyn PanelHandle, &mut Window, &mut Context<Dock>) -> bool,
3574 ) -> Option<Arc<dyn PanelHandle>> {
3575 let mut result_panel = None;
3576 let mut serialize = false;
3577 for dock in self.all_docks() {
3578 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
3579 let mut focus_center = false;
3580 let panel = dock.update(cx, |dock, cx| {
3581 dock.activate_panel(panel_index, window, cx);
3582
3583 let panel = dock.active_panel().cloned();
3584 if let Some(panel) = panel.as_ref() {
3585 if should_focus(&**panel, window, cx) {
3586 dock.set_open(true, window, cx);
3587 panel.panel_focus_handle(cx).focus(window, cx);
3588 } else {
3589 focus_center = true;
3590 }
3591 }
3592 panel
3593 });
3594
3595 if focus_center {
3596 self.active_pane
3597 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
3598 }
3599
3600 result_panel = panel;
3601 serialize = true;
3602 break;
3603 }
3604 }
3605
3606 if serialize {
3607 self.serialize_workspace(window, cx);
3608 }
3609
3610 cx.notify();
3611 result_panel
3612 }
3613
3614 /// Open the panel of the given type
3615 pub fn open_panel<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3616 for dock in self.all_docks() {
3617 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
3618 dock.update(cx, |dock, cx| {
3619 dock.activate_panel(panel_index, window, cx);
3620 dock.set_open(true, window, cx);
3621 });
3622 }
3623 }
3624 }
3625
3626 pub fn close_panel<T: Panel>(&self, window: &mut Window, cx: &mut Context<Self>) {
3627 for dock in self.all_docks().iter() {
3628 dock.update(cx, |dock, cx| {
3629 if dock.panel::<T>().is_some() {
3630 dock.set_open(false, window, cx)
3631 }
3632 })
3633 }
3634 }
3635
3636 pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
3637 self.all_docks()
3638 .iter()
3639 .find_map(|dock| dock.read(cx).panel::<T>())
3640 }
3641
3642 fn dismiss_zoomed_items_to_reveal(
3643 &mut self,
3644 dock_to_reveal: Option<DockPosition>,
3645 window: &mut Window,
3646 cx: &mut Context<Self>,
3647 ) {
3648 // If a center pane is zoomed, unzoom it.
3649 for pane in &self.panes {
3650 if pane != &self.active_pane || dock_to_reveal.is_some() {
3651 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3652 }
3653 }
3654
3655 // If another dock is zoomed, hide it.
3656 let mut focus_center = false;
3657 for dock in self.all_docks() {
3658 dock.update(cx, |dock, cx| {
3659 if Some(dock.position()) != dock_to_reveal
3660 && let Some(panel) = dock.active_panel()
3661 && panel.is_zoomed(window, cx)
3662 {
3663 focus_center |= panel.panel_focus_handle(cx).contains_focused(window, cx);
3664 dock.set_open(false, window, cx);
3665 }
3666 });
3667 }
3668
3669 if focus_center {
3670 self.active_pane
3671 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
3672 }
3673
3674 if self.zoomed_position != dock_to_reveal {
3675 self.zoomed = None;
3676 self.zoomed_position = None;
3677 cx.emit(Event::ZoomChanged);
3678 }
3679
3680 cx.notify();
3681 }
3682
3683 fn add_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
3684 let pane = cx.new(|cx| {
3685 let mut pane = Pane::new(
3686 self.weak_handle(),
3687 self.project.clone(),
3688 self.pane_history_timestamp.clone(),
3689 None,
3690 NewFile.boxed_clone(),
3691 true,
3692 window,
3693 cx,
3694 );
3695 pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
3696 pane
3697 });
3698 cx.subscribe_in(&pane, window, Self::handle_pane_event)
3699 .detach();
3700 self.panes.push(pane.clone());
3701
3702 window.focus(&pane.focus_handle(cx), cx);
3703
3704 cx.emit(Event::PaneAdded(pane.clone()));
3705 pane
3706 }
3707
3708 pub fn add_item_to_center(
3709 &mut self,
3710 item: Box<dyn ItemHandle>,
3711 window: &mut Window,
3712 cx: &mut Context<Self>,
3713 ) -> bool {
3714 if let Some(center_pane) = self.last_active_center_pane.clone() {
3715 if let Some(center_pane) = center_pane.upgrade() {
3716 center_pane.update(cx, |pane, cx| {
3717 pane.add_item(item, true, true, None, window, cx)
3718 });
3719 true
3720 } else {
3721 false
3722 }
3723 } else {
3724 false
3725 }
3726 }
3727
3728 pub fn add_item_to_active_pane(
3729 &mut self,
3730 item: Box<dyn ItemHandle>,
3731 destination_index: Option<usize>,
3732 focus_item: bool,
3733 window: &mut Window,
3734 cx: &mut App,
3735 ) {
3736 self.add_item(
3737 self.active_pane.clone(),
3738 item,
3739 destination_index,
3740 false,
3741 focus_item,
3742 window,
3743 cx,
3744 )
3745 }
3746
3747 pub fn add_item(
3748 &mut self,
3749 pane: Entity<Pane>,
3750 item: Box<dyn ItemHandle>,
3751 destination_index: Option<usize>,
3752 activate_pane: bool,
3753 focus_item: bool,
3754 window: &mut Window,
3755 cx: &mut App,
3756 ) {
3757 pane.update(cx, |pane, cx| {
3758 pane.add_item(
3759 item,
3760 activate_pane,
3761 focus_item,
3762 destination_index,
3763 window,
3764 cx,
3765 )
3766 });
3767 }
3768
3769 pub fn split_item(
3770 &mut self,
3771 split_direction: SplitDirection,
3772 item: Box<dyn ItemHandle>,
3773 window: &mut Window,
3774 cx: &mut Context<Self>,
3775 ) {
3776 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, window, cx);
3777 self.add_item(new_pane, item, None, true, true, window, cx);
3778 }
3779
3780 pub fn open_abs_path(
3781 &mut self,
3782 abs_path: PathBuf,
3783 options: OpenOptions,
3784 window: &mut Window,
3785 cx: &mut Context<Self>,
3786 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3787 cx.spawn_in(window, async move |workspace, cx| {
3788 let open_paths_task_result = workspace
3789 .update_in(cx, |workspace, window, cx| {
3790 workspace.open_paths(vec![abs_path.clone()], options, None, window, cx)
3791 })
3792 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
3793 .await;
3794 anyhow::ensure!(
3795 open_paths_task_result.len() == 1,
3796 "open abs path {abs_path:?} task returned incorrect number of results"
3797 );
3798 match open_paths_task_result
3799 .into_iter()
3800 .next()
3801 .expect("ensured single task result")
3802 {
3803 Some(open_result) => {
3804 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
3805 }
3806 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
3807 }
3808 })
3809 }
3810
3811 pub fn split_abs_path(
3812 &mut self,
3813 abs_path: PathBuf,
3814 visible: bool,
3815 window: &mut Window,
3816 cx: &mut Context<Self>,
3817 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3818 let project_path_task =
3819 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
3820 cx.spawn_in(window, async move |this, cx| {
3821 let (_, path) = project_path_task.await?;
3822 this.update_in(cx, |this, window, cx| this.split_path(path, window, cx))?
3823 .await
3824 })
3825 }
3826
3827 pub fn open_path(
3828 &mut self,
3829 path: impl Into<ProjectPath>,
3830 pane: Option<WeakEntity<Pane>>,
3831 focus_item: bool,
3832 window: &mut Window,
3833 cx: &mut App,
3834 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3835 self.open_path_preview(path, pane, focus_item, false, true, window, cx)
3836 }
3837
3838 pub fn open_path_preview(
3839 &mut self,
3840 path: impl Into<ProjectPath>,
3841 pane: Option<WeakEntity<Pane>>,
3842 focus_item: bool,
3843 allow_preview: bool,
3844 activate: bool,
3845 window: &mut Window,
3846 cx: &mut App,
3847 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3848 let pane = pane.unwrap_or_else(|| {
3849 self.last_active_center_pane.clone().unwrap_or_else(|| {
3850 self.panes
3851 .first()
3852 .expect("There must be an active pane")
3853 .downgrade()
3854 })
3855 });
3856
3857 let project_path = path.into();
3858 let task = self.load_path(project_path.clone(), window, cx);
3859 window.spawn(cx, async move |cx| {
3860 let (project_entry_id, build_item) = task.await?;
3861
3862 pane.update_in(cx, |pane, window, cx| {
3863 pane.open_item(
3864 project_entry_id,
3865 project_path,
3866 focus_item,
3867 allow_preview,
3868 activate,
3869 None,
3870 window,
3871 cx,
3872 build_item,
3873 )
3874 })
3875 })
3876 }
3877
3878 pub fn split_path(
3879 &mut self,
3880 path: impl Into<ProjectPath>,
3881 window: &mut Window,
3882 cx: &mut Context<Self>,
3883 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3884 self.split_path_preview(path, false, None, window, cx)
3885 }
3886
3887 pub fn split_path_preview(
3888 &mut self,
3889 path: impl Into<ProjectPath>,
3890 allow_preview: bool,
3891 split_direction: Option<SplitDirection>,
3892 window: &mut Window,
3893 cx: &mut Context<Self>,
3894 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3895 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
3896 self.panes
3897 .first()
3898 .expect("There must be an active pane")
3899 .downgrade()
3900 });
3901
3902 if let Member::Pane(center_pane) = &self.center.root
3903 && center_pane.read(cx).items_len() == 0
3904 {
3905 return self.open_path(path, Some(pane), true, window, cx);
3906 }
3907
3908 let project_path = path.into();
3909 let task = self.load_path(project_path.clone(), window, cx);
3910 cx.spawn_in(window, async move |this, cx| {
3911 let (project_entry_id, build_item) = task.await?;
3912 this.update_in(cx, move |this, window, cx| -> Option<_> {
3913 let pane = pane.upgrade()?;
3914 let new_pane = this.split_pane(
3915 pane,
3916 split_direction.unwrap_or(SplitDirection::Right),
3917 window,
3918 cx,
3919 );
3920 new_pane.update(cx, |new_pane, cx| {
3921 Some(new_pane.open_item(
3922 project_entry_id,
3923 project_path,
3924 true,
3925 allow_preview,
3926 true,
3927 None,
3928 window,
3929 cx,
3930 build_item,
3931 ))
3932 })
3933 })
3934 .map(|option| option.context("pane was dropped"))?
3935 })
3936 }
3937
3938 fn load_path(
3939 &mut self,
3940 path: ProjectPath,
3941 window: &mut Window,
3942 cx: &mut App,
3943 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
3944 let registry = cx.default_global::<ProjectItemRegistry>().clone();
3945 registry.open_path(self.project(), &path, window, cx)
3946 }
3947
3948 pub fn find_project_item<T>(
3949 &self,
3950 pane: &Entity<Pane>,
3951 project_item: &Entity<T::Item>,
3952 cx: &App,
3953 ) -> Option<Entity<T>>
3954 where
3955 T: ProjectItem,
3956 {
3957 use project::ProjectItem as _;
3958 let project_item = project_item.read(cx);
3959 let entry_id = project_item.entry_id(cx);
3960 let project_path = project_item.project_path(cx);
3961
3962 let mut item = None;
3963 if let Some(entry_id) = entry_id {
3964 item = pane.read(cx).item_for_entry(entry_id, cx);
3965 }
3966 if item.is_none()
3967 && let Some(project_path) = project_path
3968 {
3969 item = pane.read(cx).item_for_path(project_path, cx);
3970 }
3971
3972 item.and_then(|item| item.downcast::<T>())
3973 }
3974
3975 pub fn is_project_item_open<T>(
3976 &self,
3977 pane: &Entity<Pane>,
3978 project_item: &Entity<T::Item>,
3979 cx: &App,
3980 ) -> bool
3981 where
3982 T: ProjectItem,
3983 {
3984 self.find_project_item::<T>(pane, project_item, cx)
3985 .is_some()
3986 }
3987
3988 pub fn open_project_item<T>(
3989 &mut self,
3990 pane: Entity<Pane>,
3991 project_item: Entity<T::Item>,
3992 activate_pane: bool,
3993 focus_item: bool,
3994 keep_old_preview: bool,
3995 allow_new_preview: bool,
3996 window: &mut Window,
3997 cx: &mut Context<Self>,
3998 ) -> Entity<T>
3999 where
4000 T: ProjectItem,
4001 {
4002 let old_item_id = pane.read(cx).active_item().map(|item| item.item_id());
4003
4004 if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
4005 if !keep_old_preview
4006 && let Some(old_id) = old_item_id
4007 && old_id != item.item_id()
4008 {
4009 // switching to a different item, so unpreview old active item
4010 pane.update(cx, |pane, _| {
4011 pane.unpreview_item_if_preview(old_id);
4012 });
4013 }
4014
4015 self.activate_item(&item, activate_pane, focus_item, window, cx);
4016 if !allow_new_preview {
4017 pane.update(cx, |pane, _| {
4018 pane.unpreview_item_if_preview(item.item_id());
4019 });
4020 }
4021 return item;
4022 }
4023
4024 let item = pane.update(cx, |pane, cx| {
4025 cx.new(|cx| {
4026 T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx)
4027 })
4028 });
4029 let mut destination_index = None;
4030 pane.update(cx, |pane, cx| {
4031 if !keep_old_preview && let Some(old_id) = old_item_id {
4032 pane.unpreview_item_if_preview(old_id);
4033 }
4034 if allow_new_preview {
4035 destination_index = pane.replace_preview_item_id(item.item_id(), window, cx);
4036 }
4037 });
4038
4039 self.add_item(
4040 pane,
4041 Box::new(item.clone()),
4042 destination_index,
4043 activate_pane,
4044 focus_item,
4045 window,
4046 cx,
4047 );
4048 item
4049 }
4050
4051 pub fn open_shared_screen(
4052 &mut self,
4053 peer_id: PeerId,
4054 window: &mut Window,
4055 cx: &mut Context<Self>,
4056 ) {
4057 if let Some(shared_screen) =
4058 self.shared_screen_for_peer(peer_id, &self.active_pane, window, cx)
4059 {
4060 self.active_pane.update(cx, |pane, cx| {
4061 pane.add_item(Box::new(shared_screen), false, true, None, window, cx)
4062 });
4063 }
4064 }
4065
4066 pub fn activate_item(
4067 &mut self,
4068 item: &dyn ItemHandle,
4069 activate_pane: bool,
4070 focus_item: bool,
4071 window: &mut Window,
4072 cx: &mut App,
4073 ) -> bool {
4074 let result = self.panes.iter().find_map(|pane| {
4075 pane.read(cx)
4076 .index_for_item(item)
4077 .map(|ix| (pane.clone(), ix))
4078 });
4079 if let Some((pane, ix)) = result {
4080 pane.update(cx, |pane, cx| {
4081 pane.activate_item(ix, activate_pane, focus_item, window, cx)
4082 });
4083 true
4084 } else {
4085 false
4086 }
4087 }
4088
4089 fn activate_pane_at_index(
4090 &mut self,
4091 action: &ActivatePane,
4092 window: &mut Window,
4093 cx: &mut Context<Self>,
4094 ) {
4095 let panes = self.center.panes();
4096 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
4097 window.focus(&pane.focus_handle(cx), cx);
4098 } else {
4099 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx)
4100 .detach();
4101 }
4102 }
4103
4104 fn move_item_to_pane_at_index(
4105 &mut self,
4106 action: &MoveItemToPane,
4107 window: &mut Window,
4108 cx: &mut Context<Self>,
4109 ) {
4110 let panes = self.center.panes();
4111 let destination = match panes.get(action.destination) {
4112 Some(&destination) => destination.clone(),
4113 None => {
4114 if !action.clone && self.active_pane.read(cx).items_len() < 2 {
4115 return;
4116 }
4117 let direction = SplitDirection::Right;
4118 let split_off_pane = self
4119 .find_pane_in_direction(direction, cx)
4120 .unwrap_or_else(|| self.active_pane.clone());
4121 let new_pane = self.add_pane(window, cx);
4122 if self
4123 .center
4124 .split(&split_off_pane, &new_pane, direction, cx)
4125 .log_err()
4126 .is_none()
4127 {
4128 return;
4129 };
4130 new_pane
4131 }
4132 };
4133
4134 if action.clone {
4135 if self
4136 .active_pane
4137 .read(cx)
4138 .active_item()
4139 .is_some_and(|item| item.can_split(cx))
4140 {
4141 clone_active_item(
4142 self.database_id(),
4143 &self.active_pane,
4144 &destination,
4145 action.focus,
4146 window,
4147 cx,
4148 );
4149 return;
4150 }
4151 }
4152 move_active_item(
4153 &self.active_pane,
4154 &destination,
4155 action.focus,
4156 true,
4157 window,
4158 cx,
4159 )
4160 }
4161
4162 pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) {
4163 let panes = self.center.panes();
4164 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
4165 let next_ix = (ix + 1) % panes.len();
4166 let next_pane = panes[next_ix].clone();
4167 window.focus(&next_pane.focus_handle(cx), cx);
4168 }
4169 }
4170
4171 pub fn activate_previous_pane(&mut self, window: &mut Window, cx: &mut App) {
4172 let panes = self.center.panes();
4173 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
4174 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
4175 let prev_pane = panes[prev_ix].clone();
4176 window.focus(&prev_pane.focus_handle(cx), cx);
4177 }
4178 }
4179
4180 pub fn activate_pane_in_direction(
4181 &mut self,
4182 direction: SplitDirection,
4183 window: &mut Window,
4184 cx: &mut App,
4185 ) {
4186 use ActivateInDirectionTarget as Target;
4187 enum Origin {
4188 LeftDock,
4189 RightDock,
4190 BottomDock,
4191 Center,
4192 }
4193
4194 let origin: Origin = [
4195 (&self.left_dock, Origin::LeftDock),
4196 (&self.right_dock, Origin::RightDock),
4197 (&self.bottom_dock, Origin::BottomDock),
4198 ]
4199 .into_iter()
4200 .find_map(|(dock, origin)| {
4201 if dock.focus_handle(cx).contains_focused(window, cx) && dock.read(cx).is_open() {
4202 Some(origin)
4203 } else {
4204 None
4205 }
4206 })
4207 .unwrap_or(Origin::Center);
4208
4209 let get_last_active_pane = || {
4210 let pane = self
4211 .last_active_center_pane
4212 .clone()
4213 .unwrap_or_else(|| {
4214 self.panes
4215 .first()
4216 .expect("There must be an active pane")
4217 .downgrade()
4218 })
4219 .upgrade()?;
4220 (pane.read(cx).items_len() != 0).then_some(pane)
4221 };
4222
4223 let try_dock =
4224 |dock: &Entity<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
4225
4226 let target = match (origin, direction) {
4227 // We're in the center, so we first try to go to a different pane,
4228 // otherwise try to go to a dock.
4229 (Origin::Center, direction) => {
4230 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
4231 Some(Target::Pane(pane))
4232 } else {
4233 match direction {
4234 SplitDirection::Up => None,
4235 SplitDirection::Down => try_dock(&self.bottom_dock),
4236 SplitDirection::Left => try_dock(&self.left_dock),
4237 SplitDirection::Right => try_dock(&self.right_dock),
4238 }
4239 }
4240 }
4241
4242 (Origin::LeftDock, SplitDirection::Right) => {
4243 if let Some(last_active_pane) = get_last_active_pane() {
4244 Some(Target::Pane(last_active_pane))
4245 } else {
4246 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
4247 }
4248 }
4249
4250 (Origin::LeftDock, SplitDirection::Down)
4251 | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
4252
4253 (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
4254 (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
4255 (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
4256
4257 (Origin::RightDock, SplitDirection::Left) => {
4258 if let Some(last_active_pane) = get_last_active_pane() {
4259 Some(Target::Pane(last_active_pane))
4260 } else {
4261 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
4262 }
4263 }
4264
4265 _ => None,
4266 };
4267
4268 match target {
4269 Some(ActivateInDirectionTarget::Pane(pane)) => {
4270 let pane = pane.read(cx);
4271 if let Some(item) = pane.active_item() {
4272 item.item_focus_handle(cx).focus(window, cx);
4273 } else {
4274 log::error!(
4275 "Could not find a focus target when in switching focus in {direction} direction for a pane",
4276 );
4277 }
4278 }
4279 Some(ActivateInDirectionTarget::Dock(dock)) => {
4280 // Defer this to avoid a panic when the dock's active panel is already on the stack.
4281 window.defer(cx, move |window, cx| {
4282 let dock = dock.read(cx);
4283 if let Some(panel) = dock.active_panel() {
4284 panel.panel_focus_handle(cx).focus(window, cx);
4285 } else {
4286 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
4287 }
4288 })
4289 }
4290 None => {}
4291 }
4292 }
4293
4294 pub fn move_item_to_pane_in_direction(
4295 &mut self,
4296 action: &MoveItemToPaneInDirection,
4297 window: &mut Window,
4298 cx: &mut Context<Self>,
4299 ) {
4300 let destination = match self.find_pane_in_direction(action.direction, cx) {
4301 Some(destination) => destination,
4302 None => {
4303 if !action.clone && self.active_pane.read(cx).items_len() < 2 {
4304 return;
4305 }
4306 let new_pane = self.add_pane(window, cx);
4307 if self
4308 .center
4309 .split(&self.active_pane, &new_pane, action.direction, cx)
4310 .log_err()
4311 .is_none()
4312 {
4313 return;
4314 };
4315 new_pane
4316 }
4317 };
4318
4319 if action.clone {
4320 if self
4321 .active_pane
4322 .read(cx)
4323 .active_item()
4324 .is_some_and(|item| item.can_split(cx))
4325 {
4326 clone_active_item(
4327 self.database_id(),
4328 &self.active_pane,
4329 &destination,
4330 action.focus,
4331 window,
4332 cx,
4333 );
4334 return;
4335 }
4336 }
4337 move_active_item(
4338 &self.active_pane,
4339 &destination,
4340 action.focus,
4341 true,
4342 window,
4343 cx,
4344 );
4345 }
4346
4347 pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
4348 self.center.bounding_box_for_pane(pane)
4349 }
4350
4351 pub fn find_pane_in_direction(
4352 &mut self,
4353 direction: SplitDirection,
4354 cx: &App,
4355 ) -> Option<Entity<Pane>> {
4356 self.center
4357 .find_pane_in_direction(&self.active_pane, direction, cx)
4358 .cloned()
4359 }
4360
4361 pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
4362 if let Some(to) = self.find_pane_in_direction(direction, cx) {
4363 self.center.swap(&self.active_pane, &to, cx);
4364 cx.notify();
4365 }
4366 }
4367
4368 pub fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
4369 if self
4370 .center
4371 .move_to_border(&self.active_pane, direction, cx)
4372 .unwrap()
4373 {
4374 cx.notify();
4375 }
4376 }
4377
4378 pub fn resize_pane(
4379 &mut self,
4380 axis: gpui::Axis,
4381 amount: Pixels,
4382 window: &mut Window,
4383 cx: &mut Context<Self>,
4384 ) {
4385 let docks = self.all_docks();
4386 let active_dock = docks
4387 .into_iter()
4388 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
4389
4390 if let Some(dock) = active_dock {
4391 let Some(panel_size) = dock.read(cx).active_panel_size(window, cx) else {
4392 return;
4393 };
4394 match dock.read(cx).position() {
4395 DockPosition::Left => self.resize_left_dock(panel_size + amount, window, cx),
4396 DockPosition::Bottom => self.resize_bottom_dock(panel_size + amount, window, cx),
4397 DockPosition::Right => self.resize_right_dock(panel_size + amount, window, cx),
4398 }
4399 } else {
4400 self.center
4401 .resize(&self.active_pane, axis, amount, &self.bounds, cx);
4402 }
4403 cx.notify();
4404 }
4405
4406 pub fn reset_pane_sizes(&mut self, cx: &mut Context<Self>) {
4407 self.center.reset_pane_sizes(cx);
4408 cx.notify();
4409 }
4410
4411 fn handle_pane_focused(
4412 &mut self,
4413 pane: Entity<Pane>,
4414 window: &mut Window,
4415 cx: &mut Context<Self>,
4416 ) {
4417 // This is explicitly hoisted out of the following check for pane identity as
4418 // terminal panel panes are not registered as a center panes.
4419 self.status_bar.update(cx, |status_bar, cx| {
4420 status_bar.set_active_pane(&pane, window, cx);
4421 });
4422 if self.active_pane != pane {
4423 self.set_active_pane(&pane, window, cx);
4424 }
4425
4426 if self.last_active_center_pane.is_none() {
4427 self.last_active_center_pane = Some(pane.downgrade());
4428 }
4429
4430 // If this pane is in a dock, preserve that dock when dismissing zoomed items.
4431 // This prevents the dock from closing when focus events fire during window activation.
4432 let dock_to_preserve = self.all_docks().iter().find_map(|dock| {
4433 let dock_read = dock.read(cx);
4434 if let Some(panel) = dock_read.active_panel()
4435 && let Some(dock_pane) = panel.pane(cx)
4436 && dock_pane == pane
4437 {
4438 Some(dock_read.position())
4439 } else {
4440 None
4441 }
4442 });
4443
4444 self.dismiss_zoomed_items_to_reveal(dock_to_preserve, window, cx);
4445 if pane.read(cx).is_zoomed() {
4446 self.zoomed = Some(pane.downgrade().into());
4447 } else {
4448 self.zoomed = None;
4449 }
4450 self.zoomed_position = None;
4451 cx.emit(Event::ZoomChanged);
4452 self.update_active_view_for_followers(window, cx);
4453 pane.update(cx, |pane, _| {
4454 pane.track_alternate_file_items();
4455 });
4456
4457 cx.notify();
4458 }
4459
4460 fn set_active_pane(
4461 &mut self,
4462 pane: &Entity<Pane>,
4463 window: &mut Window,
4464 cx: &mut Context<Self>,
4465 ) {
4466 self.active_pane = pane.clone();
4467 self.active_item_path_changed(true, window, cx);
4468 self.last_active_center_pane = Some(pane.downgrade());
4469 }
4470
4471 fn handle_panel_focused(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4472 self.update_active_view_for_followers(window, cx);
4473 }
4474
4475 fn handle_pane_event(
4476 &mut self,
4477 pane: &Entity<Pane>,
4478 event: &pane::Event,
4479 window: &mut Window,
4480 cx: &mut Context<Self>,
4481 ) {
4482 let mut serialize_workspace = true;
4483 match event {
4484 pane::Event::AddItem { item } => {
4485 item.added_to_pane(self, pane.clone(), window, cx);
4486 cx.emit(Event::ItemAdded {
4487 item: item.boxed_clone(),
4488 });
4489 }
4490 pane::Event::Split { direction, mode } => {
4491 match mode {
4492 SplitMode::ClonePane => {
4493 self.split_and_clone(pane.clone(), *direction, window, cx)
4494 .detach();
4495 }
4496 SplitMode::EmptyPane => {
4497 self.split_pane(pane.clone(), *direction, window, cx);
4498 }
4499 SplitMode::MovePane => {
4500 self.split_and_move(pane.clone(), *direction, window, cx);
4501 }
4502 };
4503 }
4504 pane::Event::JoinIntoNext => {
4505 self.join_pane_into_next(pane.clone(), window, cx);
4506 }
4507 pane::Event::JoinAll => {
4508 self.join_all_panes(window, cx);
4509 }
4510 pane::Event::Remove { focus_on_pane } => {
4511 self.remove_pane(pane.clone(), focus_on_pane.clone(), window, cx);
4512 }
4513 pane::Event::ActivateItem {
4514 local,
4515 focus_changed,
4516 } => {
4517 window.invalidate_character_coordinates();
4518
4519 pane.update(cx, |pane, _| {
4520 pane.track_alternate_file_items();
4521 });
4522 if *local {
4523 self.unfollow_in_pane(pane, window, cx);
4524 }
4525 serialize_workspace = *focus_changed || pane != self.active_pane();
4526 if pane == self.active_pane() {
4527 self.active_item_path_changed(*focus_changed, window, cx);
4528 self.update_active_view_for_followers(window, cx);
4529 } else if *local {
4530 self.set_active_pane(pane, window, cx);
4531 }
4532 }
4533 pane::Event::UserSavedItem { item, save_intent } => {
4534 cx.emit(Event::UserSavedItem {
4535 pane: pane.downgrade(),
4536 item: item.boxed_clone(),
4537 save_intent: *save_intent,
4538 });
4539 serialize_workspace = false;
4540 }
4541 pane::Event::ChangeItemTitle => {
4542 if *pane == self.active_pane {
4543 self.active_item_path_changed(false, window, cx);
4544 }
4545 serialize_workspace = false;
4546 }
4547 pane::Event::RemovedItem { item } => {
4548 cx.emit(Event::ActiveItemChanged);
4549 self.update_window_edited(window, cx);
4550 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id())
4551 && entry.get().entity_id() == pane.entity_id()
4552 {
4553 entry.remove();
4554 }
4555 cx.emit(Event::ItemRemoved {
4556 item_id: item.item_id(),
4557 });
4558 }
4559 pane::Event::Focus => {
4560 window.invalidate_character_coordinates();
4561 self.handle_pane_focused(pane.clone(), window, cx);
4562 }
4563 pane::Event::ZoomIn => {
4564 if *pane == self.active_pane {
4565 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
4566 if pane.read(cx).has_focus(window, cx) {
4567 self.zoomed = Some(pane.downgrade().into());
4568 self.zoomed_position = None;
4569 cx.emit(Event::ZoomChanged);
4570 }
4571 cx.notify();
4572 }
4573 }
4574 pane::Event::ZoomOut => {
4575 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
4576 if self.zoomed_position.is_none() {
4577 self.zoomed = None;
4578 cx.emit(Event::ZoomChanged);
4579 }
4580 cx.notify();
4581 }
4582 pane::Event::ItemPinned | pane::Event::ItemUnpinned => {}
4583 }
4584
4585 if serialize_workspace {
4586 self.serialize_workspace(window, cx);
4587 }
4588 }
4589
4590 pub fn unfollow_in_pane(
4591 &mut self,
4592 pane: &Entity<Pane>,
4593 window: &mut Window,
4594 cx: &mut Context<Workspace>,
4595 ) -> Option<CollaboratorId> {
4596 let leader_id = self.leader_for_pane(pane)?;
4597 self.unfollow(leader_id, window, cx);
4598 Some(leader_id)
4599 }
4600
4601 pub fn split_pane(
4602 &mut self,
4603 pane_to_split: Entity<Pane>,
4604 split_direction: SplitDirection,
4605 window: &mut Window,
4606 cx: &mut Context<Self>,
4607 ) -> Entity<Pane> {
4608 let new_pane = self.add_pane(window, cx);
4609 self.center
4610 .split(&pane_to_split, &new_pane, split_direction, cx)
4611 .unwrap();
4612 cx.notify();
4613 new_pane
4614 }
4615
4616 pub fn split_and_move(
4617 &mut self,
4618 pane: Entity<Pane>,
4619 direction: SplitDirection,
4620 window: &mut Window,
4621 cx: &mut Context<Self>,
4622 ) {
4623 let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx)) else {
4624 return;
4625 };
4626 let new_pane = self.add_pane(window, cx);
4627 new_pane.update(cx, |pane, cx| {
4628 pane.add_item(item, true, true, None, window, cx)
4629 });
4630 self.center.split(&pane, &new_pane, direction, cx).unwrap();
4631 cx.notify();
4632 }
4633
4634 pub fn split_and_clone(
4635 &mut self,
4636 pane: Entity<Pane>,
4637 direction: SplitDirection,
4638 window: &mut Window,
4639 cx: &mut Context<Self>,
4640 ) -> Task<Option<Entity<Pane>>> {
4641 let Some(item) = pane.read(cx).active_item() else {
4642 return Task::ready(None);
4643 };
4644 if !item.can_split(cx) {
4645 return Task::ready(None);
4646 }
4647 let task = item.clone_on_split(self.database_id(), window, cx);
4648 cx.spawn_in(window, async move |this, cx| {
4649 if let Some(clone) = task.await {
4650 this.update_in(cx, |this, window, cx| {
4651 let new_pane = this.add_pane(window, cx);
4652 let nav_history = pane.read(cx).fork_nav_history();
4653 new_pane.update(cx, |pane, cx| {
4654 pane.set_nav_history(nav_history, cx);
4655 pane.add_item(clone, true, true, None, window, cx)
4656 });
4657 this.center.split(&pane, &new_pane, direction, cx).unwrap();
4658 cx.notify();
4659 new_pane
4660 })
4661 .ok()
4662 } else {
4663 None
4664 }
4665 })
4666 }
4667
4668 pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4669 let active_item = self.active_pane.read(cx).active_item();
4670 for pane in &self.panes {
4671 join_pane_into_active(&self.active_pane, pane, window, cx);
4672 }
4673 if let Some(active_item) = active_item {
4674 self.activate_item(active_item.as_ref(), true, true, window, cx);
4675 }
4676 cx.notify();
4677 }
4678
4679 pub fn join_pane_into_next(
4680 &mut self,
4681 pane: Entity<Pane>,
4682 window: &mut Window,
4683 cx: &mut Context<Self>,
4684 ) {
4685 let next_pane = self
4686 .find_pane_in_direction(SplitDirection::Right, cx)
4687 .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
4688 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
4689 .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
4690 let Some(next_pane) = next_pane else {
4691 return;
4692 };
4693 move_all_items(&pane, &next_pane, window, cx);
4694 cx.notify();
4695 }
4696
4697 fn remove_pane(
4698 &mut self,
4699 pane: Entity<Pane>,
4700 focus_on: Option<Entity<Pane>>,
4701 window: &mut Window,
4702 cx: &mut Context<Self>,
4703 ) {
4704 if self.center.remove(&pane, cx).unwrap() {
4705 self.force_remove_pane(&pane, &focus_on, window, cx);
4706 self.unfollow_in_pane(&pane, window, cx);
4707 self.last_leaders_by_pane.remove(&pane.downgrade());
4708 for removed_item in pane.read(cx).items() {
4709 self.panes_by_item.remove(&removed_item.item_id());
4710 }
4711
4712 cx.notify();
4713 } else {
4714 self.active_item_path_changed(true, window, cx);
4715 }
4716 cx.emit(Event::PaneRemoved);
4717 }
4718
4719 pub fn panes_mut(&mut self) -> &mut [Entity<Pane>] {
4720 &mut self.panes
4721 }
4722
4723 pub fn panes(&self) -> &[Entity<Pane>] {
4724 &self.panes
4725 }
4726
4727 pub fn active_pane(&self) -> &Entity<Pane> {
4728 &self.active_pane
4729 }
4730
4731 pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> {
4732 for dock in self.all_docks() {
4733 if dock.focus_handle(cx).contains_focused(window, cx)
4734 && let Some(pane) = dock
4735 .read(cx)
4736 .active_panel()
4737 .and_then(|panel| panel.pane(cx))
4738 {
4739 return pane;
4740 }
4741 }
4742 self.active_pane().clone()
4743 }
4744
4745 pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
4746 self.find_pane_in_direction(SplitDirection::Right, cx)
4747 .unwrap_or_else(|| {
4748 self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx)
4749 })
4750 }
4751
4752 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> {
4753 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
4754 weak_pane.upgrade()
4755 }
4756
4757 fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
4758 self.follower_states.retain(|leader_id, state| {
4759 if *leader_id == CollaboratorId::PeerId(peer_id) {
4760 for item in state.items_by_leader_view_id.values() {
4761 item.view.set_leader_id(None, window, cx);
4762 }
4763 false
4764 } else {
4765 true
4766 }
4767 });
4768 cx.notify();
4769 }
4770
4771 pub fn start_following(
4772 &mut self,
4773 leader_id: impl Into<CollaboratorId>,
4774 window: &mut Window,
4775 cx: &mut Context<Self>,
4776 ) -> Option<Task<Result<()>>> {
4777 let leader_id = leader_id.into();
4778 let pane = self.active_pane().clone();
4779
4780 self.last_leaders_by_pane
4781 .insert(pane.downgrade(), leader_id);
4782 self.unfollow(leader_id, window, cx);
4783 self.unfollow_in_pane(&pane, window, cx);
4784 self.follower_states.insert(
4785 leader_id,
4786 FollowerState {
4787 center_pane: pane.clone(),
4788 dock_pane: None,
4789 active_view_id: None,
4790 items_by_leader_view_id: Default::default(),
4791 },
4792 );
4793 cx.notify();
4794
4795 match leader_id {
4796 CollaboratorId::PeerId(leader_peer_id) => {
4797 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
4798 let project_id = self.project.read(cx).remote_id();
4799 let request = self.app_state.client.request(proto::Follow {
4800 room_id,
4801 project_id,
4802 leader_id: Some(leader_peer_id),
4803 });
4804
4805 Some(cx.spawn_in(window, async move |this, cx| {
4806 let response = request.await?;
4807 this.update(cx, |this, _| {
4808 let state = this
4809 .follower_states
4810 .get_mut(&leader_id)
4811 .context("following interrupted")?;
4812 state.active_view_id = response
4813 .active_view
4814 .as_ref()
4815 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
4816 anyhow::Ok(())
4817 })??;
4818 if let Some(view) = response.active_view {
4819 Self::add_view_from_leader(this.clone(), leader_peer_id, &view, cx).await?;
4820 }
4821 this.update_in(cx, |this, window, cx| {
4822 this.leader_updated(leader_id, window, cx)
4823 })?;
4824 Ok(())
4825 }))
4826 }
4827 CollaboratorId::Agent => {
4828 self.leader_updated(leader_id, window, cx)?;
4829 Some(Task::ready(Ok(())))
4830 }
4831 }
4832 }
4833
4834 pub fn follow_next_collaborator(
4835 &mut self,
4836 _: &FollowNextCollaborator,
4837 window: &mut Window,
4838 cx: &mut Context<Self>,
4839 ) {
4840 let collaborators = self.project.read(cx).collaborators();
4841 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
4842 let mut collaborators = collaborators.keys().copied();
4843 for peer_id in collaborators.by_ref() {
4844 if CollaboratorId::PeerId(peer_id) == leader_id {
4845 break;
4846 }
4847 }
4848 collaborators.next().map(CollaboratorId::PeerId)
4849 } else if let Some(last_leader_id) =
4850 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
4851 {
4852 match last_leader_id {
4853 CollaboratorId::PeerId(peer_id) => {
4854 if collaborators.contains_key(peer_id) {
4855 Some(*last_leader_id)
4856 } else {
4857 None
4858 }
4859 }
4860 CollaboratorId::Agent => Some(CollaboratorId::Agent),
4861 }
4862 } else {
4863 None
4864 };
4865
4866 let pane = self.active_pane.clone();
4867 let Some(leader_id) = next_leader_id.or_else(|| {
4868 Some(CollaboratorId::PeerId(
4869 collaborators.keys().copied().next()?,
4870 ))
4871 }) else {
4872 return;
4873 };
4874 if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
4875 return;
4876 }
4877 if let Some(task) = self.start_following(leader_id, window, cx) {
4878 task.detach_and_log_err(cx)
4879 }
4880 }
4881
4882 pub fn follow(
4883 &mut self,
4884 leader_id: impl Into<CollaboratorId>,
4885 window: &mut Window,
4886 cx: &mut Context<Self>,
4887 ) {
4888 let leader_id = leader_id.into();
4889
4890 if let CollaboratorId::PeerId(peer_id) = leader_id {
4891 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
4892 return;
4893 };
4894 let room = room.read(cx);
4895 let Some(remote_participant) = room.remote_participant_for_peer_id(peer_id) else {
4896 return;
4897 };
4898
4899 let project = self.project.read(cx);
4900
4901 let other_project_id = match remote_participant.location {
4902 call::ParticipantLocation::External => None,
4903 call::ParticipantLocation::UnsharedProject => None,
4904 call::ParticipantLocation::SharedProject { project_id } => {
4905 if Some(project_id) == project.remote_id() {
4906 None
4907 } else {
4908 Some(project_id)
4909 }
4910 }
4911 };
4912
4913 // if they are active in another project, follow there.
4914 if let Some(project_id) = other_project_id {
4915 let app_state = self.app_state.clone();
4916 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
4917 .detach_and_log_err(cx);
4918 }
4919 }
4920
4921 // if you're already following, find the right pane and focus it.
4922 if let Some(follower_state) = self.follower_states.get(&leader_id) {
4923 window.focus(&follower_state.pane().focus_handle(cx), cx);
4924
4925 return;
4926 }
4927
4928 // Otherwise, follow.
4929 if let Some(task) = self.start_following(leader_id, window, cx) {
4930 task.detach_and_log_err(cx)
4931 }
4932 }
4933
4934 pub fn unfollow(
4935 &mut self,
4936 leader_id: impl Into<CollaboratorId>,
4937 window: &mut Window,
4938 cx: &mut Context<Self>,
4939 ) -> Option<()> {
4940 cx.notify();
4941
4942 let leader_id = leader_id.into();
4943 let state = self.follower_states.remove(&leader_id)?;
4944 for (_, item) in state.items_by_leader_view_id {
4945 item.view.set_leader_id(None, window, cx);
4946 }
4947
4948 if let CollaboratorId::PeerId(leader_peer_id) = leader_id {
4949 let project_id = self.project.read(cx).remote_id();
4950 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
4951 self.app_state
4952 .client
4953 .send(proto::Unfollow {
4954 room_id,
4955 project_id,
4956 leader_id: Some(leader_peer_id),
4957 })
4958 .log_err();
4959 }
4960
4961 Some(())
4962 }
4963
4964 pub fn is_being_followed(&self, id: impl Into<CollaboratorId>) -> bool {
4965 self.follower_states.contains_key(&id.into())
4966 }
4967
4968 fn active_item_path_changed(
4969 &mut self,
4970 focus_changed: bool,
4971 window: &mut Window,
4972 cx: &mut Context<Self>,
4973 ) {
4974 cx.emit(Event::ActiveItemChanged);
4975 let active_entry = self.active_project_path(cx);
4976 self.project.update(cx, |project, cx| {
4977 project.set_active_path(active_entry.clone(), cx)
4978 });
4979
4980 if focus_changed && let Some(project_path) = &active_entry {
4981 let git_store_entity = self.project.read(cx).git_store().clone();
4982 git_store_entity.update(cx, |git_store, cx| {
4983 git_store.set_active_repo_for_path(project_path, cx);
4984 });
4985 }
4986
4987 self.update_window_title(window, cx);
4988 }
4989
4990 fn update_window_title(&mut self, window: &mut Window, cx: &mut App) {
4991 let project = self.project().read(cx);
4992 let mut title = String::new();
4993
4994 for (i, worktree) in project.visible_worktrees(cx).enumerate() {
4995 let name = {
4996 let settings_location = SettingsLocation {
4997 worktree_id: worktree.read(cx).id(),
4998 path: RelPath::empty(),
4999 };
5000
5001 let settings = WorktreeSettings::get(Some(settings_location), cx);
5002 match &settings.project_name {
5003 Some(name) => name.as_str(),
5004 None => worktree.read(cx).root_name_str(),
5005 }
5006 };
5007 if i > 0 {
5008 title.push_str(", ");
5009 }
5010 title.push_str(name);
5011 }
5012
5013 if title.is_empty() {
5014 title = "empty project".to_string();
5015 }
5016
5017 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
5018 let filename = path.path.file_name().or_else(|| {
5019 Some(
5020 project
5021 .worktree_for_id(path.worktree_id, cx)?
5022 .read(cx)
5023 .root_name_str(),
5024 )
5025 });
5026
5027 if let Some(filename) = filename {
5028 title.push_str(" — ");
5029 title.push_str(filename.as_ref());
5030 }
5031 }
5032
5033 if project.is_via_collab() {
5034 title.push_str(" ↙");
5035 } else if project.is_shared() {
5036 title.push_str(" ↗");
5037 }
5038
5039 if let Some(last_title) = self.last_window_title.as_ref()
5040 && &title == last_title
5041 {
5042 return;
5043 }
5044 window.set_window_title(&title);
5045 SystemWindowTabController::update_tab_title(
5046 cx,
5047 window.window_handle().window_id(),
5048 SharedString::from(&title),
5049 );
5050 self.last_window_title = Some(title);
5051 }
5052
5053 fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
5054 let is_edited = !self.project.read(cx).is_disconnected(cx) && !self.dirty_items.is_empty();
5055 if is_edited != self.window_edited {
5056 self.window_edited = is_edited;
5057 window.set_window_edited(self.window_edited)
5058 }
5059 }
5060
5061 fn update_item_dirty_state(
5062 &mut self,
5063 item: &dyn ItemHandle,
5064 window: &mut Window,
5065 cx: &mut App,
5066 ) {
5067 let is_dirty = item.is_dirty(cx);
5068 let item_id = item.item_id();
5069 let was_dirty = self.dirty_items.contains_key(&item_id);
5070 if is_dirty == was_dirty {
5071 return;
5072 }
5073 if was_dirty {
5074 self.dirty_items.remove(&item_id);
5075 self.update_window_edited(window, cx);
5076 return;
5077 }
5078 if let Some(window_handle) = window.window_handle().downcast::<Self>() {
5079 let s = item.on_release(
5080 cx,
5081 Box::new(move |cx| {
5082 window_handle
5083 .update(cx, |this, window, cx| {
5084 this.dirty_items.remove(&item_id);
5085 this.update_window_edited(window, cx)
5086 })
5087 .ok();
5088 }),
5089 );
5090 self.dirty_items.insert(item_id, s);
5091 self.update_window_edited(window, cx);
5092 }
5093 }
5094
5095 fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
5096 if self.notifications.is_empty() {
5097 None
5098 } else {
5099 Some(
5100 div()
5101 .absolute()
5102 .right_3()
5103 .bottom_3()
5104 .w_112()
5105 .h_full()
5106 .flex()
5107 .flex_col()
5108 .justify_end()
5109 .gap_2()
5110 .children(
5111 self.notifications
5112 .iter()
5113 .map(|(_, notification)| notification.clone().into_any()),
5114 ),
5115 )
5116 }
5117 }
5118
5119 // RPC handlers
5120
5121 fn active_view_for_follower(
5122 &self,
5123 follower_project_id: Option<u64>,
5124 window: &mut Window,
5125 cx: &mut Context<Self>,
5126 ) -> Option<proto::View> {
5127 let (item, panel_id) = self.active_item_for_followers(window, cx);
5128 let item = item?;
5129 let leader_id = self
5130 .pane_for(&*item)
5131 .and_then(|pane| self.leader_for_pane(&pane));
5132 let leader_peer_id = match leader_id {
5133 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
5134 Some(CollaboratorId::Agent) | None => None,
5135 };
5136
5137 let item_handle = item.to_followable_item_handle(cx)?;
5138 let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
5139 let variant = item_handle.to_state_proto(window, cx)?;
5140
5141 if item_handle.is_project_item(window, cx)
5142 && (follower_project_id.is_none()
5143 || follower_project_id != self.project.read(cx).remote_id())
5144 {
5145 return None;
5146 }
5147
5148 Some(proto::View {
5149 id: id.to_proto(),
5150 leader_id: leader_peer_id,
5151 variant: Some(variant),
5152 panel_id: panel_id.map(|id| id as i32),
5153 })
5154 }
5155
5156 fn handle_follow(
5157 &mut self,
5158 follower_project_id: Option<u64>,
5159 window: &mut Window,
5160 cx: &mut Context<Self>,
5161 ) -> proto::FollowResponse {
5162 let active_view = self.active_view_for_follower(follower_project_id, window, cx);
5163
5164 cx.notify();
5165 proto::FollowResponse {
5166 views: active_view.iter().cloned().collect(),
5167 active_view,
5168 }
5169 }
5170
5171 fn handle_update_followers(
5172 &mut self,
5173 leader_id: PeerId,
5174 message: proto::UpdateFollowers,
5175 _window: &mut Window,
5176 _cx: &mut Context<Self>,
5177 ) {
5178 self.leader_updates_tx
5179 .unbounded_send((leader_id, message))
5180 .ok();
5181 }
5182
5183 async fn process_leader_update(
5184 this: &WeakEntity<Self>,
5185 leader_id: PeerId,
5186 update: proto::UpdateFollowers,
5187 cx: &mut AsyncWindowContext,
5188 ) -> Result<()> {
5189 match update.variant.context("invalid update")? {
5190 proto::update_followers::Variant::CreateView(view) => {
5191 let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
5192 let should_add_view = this.update(cx, |this, _| {
5193 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
5194 anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
5195 } else {
5196 anyhow::Ok(false)
5197 }
5198 })??;
5199
5200 if should_add_view {
5201 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
5202 }
5203 }
5204 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
5205 let should_add_view = this.update(cx, |this, _| {
5206 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
5207 state.active_view_id = update_active_view
5208 .view
5209 .as_ref()
5210 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
5211
5212 if state.active_view_id.is_some_and(|view_id| {
5213 !state.items_by_leader_view_id.contains_key(&view_id)
5214 }) {
5215 anyhow::Ok(true)
5216 } else {
5217 anyhow::Ok(false)
5218 }
5219 } else {
5220 anyhow::Ok(false)
5221 }
5222 })??;
5223
5224 if should_add_view && let Some(view) = update_active_view.view {
5225 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
5226 }
5227 }
5228 proto::update_followers::Variant::UpdateView(update_view) => {
5229 let variant = update_view.variant.context("missing update view variant")?;
5230 let id = update_view.id.context("missing update view id")?;
5231 let mut tasks = Vec::new();
5232 this.update_in(cx, |this, window, cx| {
5233 let project = this.project.clone();
5234 if let Some(state) = this.follower_states.get(&leader_id.into()) {
5235 let view_id = ViewId::from_proto(id.clone())?;
5236 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
5237 tasks.push(item.view.apply_update_proto(
5238 &project,
5239 variant.clone(),
5240 window,
5241 cx,
5242 ));
5243 }
5244 }
5245 anyhow::Ok(())
5246 })??;
5247 try_join_all(tasks).await.log_err();
5248 }
5249 }
5250 this.update_in(cx, |this, window, cx| {
5251 this.leader_updated(leader_id, window, cx)
5252 })?;
5253 Ok(())
5254 }
5255
5256 async fn add_view_from_leader(
5257 this: WeakEntity<Self>,
5258 leader_id: PeerId,
5259 view: &proto::View,
5260 cx: &mut AsyncWindowContext,
5261 ) -> Result<()> {
5262 let this = this.upgrade().context("workspace dropped")?;
5263
5264 let Some(id) = view.id.clone() else {
5265 anyhow::bail!("no id for view");
5266 };
5267 let id = ViewId::from_proto(id)?;
5268 let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
5269
5270 let pane = this.update(cx, |this, _cx| {
5271 let state = this
5272 .follower_states
5273 .get(&leader_id.into())
5274 .context("stopped following")?;
5275 anyhow::Ok(state.pane().clone())
5276 })?;
5277 let existing_item = pane.update_in(cx, |pane, window, cx| {
5278 let client = this.read(cx).client().clone();
5279 pane.items().find_map(|item| {
5280 let item = item.to_followable_item_handle(cx)?;
5281 if item.remote_id(&client, window, cx) == Some(id) {
5282 Some(item)
5283 } else {
5284 None
5285 }
5286 })
5287 })?;
5288 let item = if let Some(existing_item) = existing_item {
5289 existing_item
5290 } else {
5291 let variant = view.variant.clone();
5292 anyhow::ensure!(variant.is_some(), "missing view variant");
5293
5294 let task = cx.update(|window, cx| {
5295 FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx)
5296 })?;
5297
5298 let Some(task) = task else {
5299 anyhow::bail!(
5300 "failed to construct view from leader (maybe from a different version of zed?)"
5301 );
5302 };
5303
5304 let mut new_item = task.await?;
5305 pane.update_in(cx, |pane, window, cx| {
5306 let mut item_to_remove = None;
5307 for (ix, item) in pane.items().enumerate() {
5308 if let Some(item) = item.to_followable_item_handle(cx) {
5309 match new_item.dedup(item.as_ref(), window, cx) {
5310 Some(item::Dedup::KeepExisting) => {
5311 new_item =
5312 item.boxed_clone().to_followable_item_handle(cx).unwrap();
5313 break;
5314 }
5315 Some(item::Dedup::ReplaceExisting) => {
5316 item_to_remove = Some((ix, item.item_id()));
5317 break;
5318 }
5319 None => {}
5320 }
5321 }
5322 }
5323
5324 if let Some((ix, id)) = item_to_remove {
5325 pane.remove_item(id, false, false, window, cx);
5326 pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx);
5327 }
5328 })?;
5329
5330 new_item
5331 };
5332
5333 this.update_in(cx, |this, window, cx| {
5334 let state = this.follower_states.get_mut(&leader_id.into())?;
5335 item.set_leader_id(Some(leader_id.into()), window, cx);
5336 state.items_by_leader_view_id.insert(
5337 id,
5338 FollowerView {
5339 view: item,
5340 location: panel_id,
5341 },
5342 );
5343
5344 Some(())
5345 })
5346 .context("no follower state")?;
5347
5348 Ok(())
5349 }
5350
5351 fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5352 let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else {
5353 return;
5354 };
5355
5356 if let Some(agent_location) = self.project.read(cx).agent_location() {
5357 let buffer_entity_id = agent_location.buffer.entity_id();
5358 let view_id = ViewId {
5359 creator: CollaboratorId::Agent,
5360 id: buffer_entity_id.as_u64(),
5361 };
5362 follower_state.active_view_id = Some(view_id);
5363
5364 let item = match follower_state.items_by_leader_view_id.entry(view_id) {
5365 hash_map::Entry::Occupied(entry) => Some(entry.into_mut()),
5366 hash_map::Entry::Vacant(entry) => {
5367 let existing_view =
5368 follower_state
5369 .center_pane
5370 .read(cx)
5371 .items()
5372 .find_map(|item| {
5373 let item = item.to_followable_item_handle(cx)?;
5374 if item.buffer_kind(cx) == ItemBufferKind::Singleton
5375 && item.project_item_model_ids(cx).as_slice()
5376 == [buffer_entity_id]
5377 {
5378 Some(item)
5379 } else {
5380 None
5381 }
5382 });
5383 let view = existing_view.or_else(|| {
5384 agent_location.buffer.upgrade().and_then(|buffer| {
5385 cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| {
5386 registry.build_item(buffer, self.project.clone(), None, window, cx)
5387 })?
5388 .to_followable_item_handle(cx)
5389 })
5390 });
5391
5392 view.map(|view| {
5393 entry.insert(FollowerView {
5394 view,
5395 location: None,
5396 })
5397 })
5398 }
5399 };
5400
5401 if let Some(item) = item {
5402 item.view
5403 .set_leader_id(Some(CollaboratorId::Agent), window, cx);
5404 item.view
5405 .update_agent_location(agent_location.position, window, cx);
5406 }
5407 } else {
5408 follower_state.active_view_id = None;
5409 }
5410
5411 self.leader_updated(CollaboratorId::Agent, window, cx);
5412 }
5413
5414 pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
5415 let mut is_project_item = true;
5416 let mut update = proto::UpdateActiveView::default();
5417 if window.is_window_active() {
5418 let (active_item, panel_id) = self.active_item_for_followers(window, cx);
5419
5420 if let Some(item) = active_item
5421 && item.item_focus_handle(cx).contains_focused(window, cx)
5422 {
5423 let leader_id = self
5424 .pane_for(&*item)
5425 .and_then(|pane| self.leader_for_pane(&pane));
5426 let leader_peer_id = match leader_id {
5427 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
5428 Some(CollaboratorId::Agent) | None => None,
5429 };
5430
5431 if let Some(item) = item.to_followable_item_handle(cx) {
5432 let id = item
5433 .remote_id(&self.app_state.client, window, cx)
5434 .map(|id| id.to_proto());
5435
5436 if let Some(id) = id
5437 && let Some(variant) = item.to_state_proto(window, cx)
5438 {
5439 let view = Some(proto::View {
5440 id,
5441 leader_id: leader_peer_id,
5442 variant: Some(variant),
5443 panel_id: panel_id.map(|id| id as i32),
5444 });
5445
5446 is_project_item = item.is_project_item(window, cx);
5447 update = proto::UpdateActiveView { view };
5448 };
5449 }
5450 }
5451 }
5452
5453 let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
5454 if active_view_id != self.last_active_view_id.as_ref() {
5455 self.last_active_view_id = active_view_id.cloned();
5456 self.update_followers(
5457 is_project_item,
5458 proto::update_followers::Variant::UpdateActiveView(update),
5459 window,
5460 cx,
5461 );
5462 }
5463 }
5464
5465 fn active_item_for_followers(
5466 &self,
5467 window: &mut Window,
5468 cx: &mut App,
5469 ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
5470 let mut active_item = None;
5471 let mut panel_id = None;
5472 for dock in self.all_docks() {
5473 if dock.focus_handle(cx).contains_focused(window, cx)
5474 && let Some(panel) = dock.read(cx).active_panel()
5475 && let Some(pane) = panel.pane(cx)
5476 && let Some(item) = pane.read(cx).active_item()
5477 {
5478 active_item = Some(item);
5479 panel_id = panel.remote_id();
5480 break;
5481 }
5482 }
5483
5484 if active_item.is_none() {
5485 active_item = self.active_pane().read(cx).active_item();
5486 }
5487 (active_item, panel_id)
5488 }
5489
5490 fn update_followers(
5491 &self,
5492 project_only: bool,
5493 update: proto::update_followers::Variant,
5494 _: &mut Window,
5495 cx: &mut App,
5496 ) -> Option<()> {
5497 // If this update only applies to for followers in the current project,
5498 // then skip it unless this project is shared. If it applies to all
5499 // followers, regardless of project, then set `project_id` to none,
5500 // indicating that it goes to all followers.
5501 let project_id = if project_only {
5502 Some(self.project.read(cx).remote_id()?)
5503 } else {
5504 None
5505 };
5506 self.app_state().workspace_store.update(cx, |store, cx| {
5507 store.update_followers(project_id, update, cx)
5508 })
5509 }
5510
5511 pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<CollaboratorId> {
5512 self.follower_states.iter().find_map(|(leader_id, state)| {
5513 if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
5514 Some(*leader_id)
5515 } else {
5516 None
5517 }
5518 })
5519 }
5520
5521 fn leader_updated(
5522 &mut self,
5523 leader_id: impl Into<CollaboratorId>,
5524 window: &mut Window,
5525 cx: &mut Context<Self>,
5526 ) -> Option<Box<dyn ItemHandle>> {
5527 cx.notify();
5528
5529 let leader_id = leader_id.into();
5530 let (panel_id, item) = match leader_id {
5531 CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?,
5532 CollaboratorId::Agent => (None, self.active_item_for_agent()?),
5533 };
5534
5535 let state = self.follower_states.get(&leader_id)?;
5536 let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
5537 let pane;
5538 if let Some(panel_id) = panel_id {
5539 pane = self
5540 .activate_panel_for_proto_id(panel_id, window, cx)?
5541 .pane(cx)?;
5542 let state = self.follower_states.get_mut(&leader_id)?;
5543 state.dock_pane = Some(pane.clone());
5544 } else {
5545 pane = state.center_pane.clone();
5546 let state = self.follower_states.get_mut(&leader_id)?;
5547 if let Some(dock_pane) = state.dock_pane.take() {
5548 transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx);
5549 }
5550 }
5551
5552 pane.update(cx, |pane, cx| {
5553 let focus_active_item = pane.has_focus(window, cx) || transfer_focus;
5554 if let Some(index) = pane.index_for_item(item.as_ref()) {
5555 pane.activate_item(index, false, false, window, cx);
5556 } else {
5557 pane.add_item(item.boxed_clone(), false, false, None, window, cx)
5558 }
5559
5560 if focus_active_item {
5561 pane.focus_active_item(window, cx)
5562 }
5563 });
5564
5565 Some(item)
5566 }
5567
5568 fn active_item_for_agent(&self) -> Option<Box<dyn ItemHandle>> {
5569 let state = self.follower_states.get(&CollaboratorId::Agent)?;
5570 let active_view_id = state.active_view_id?;
5571 Some(
5572 state
5573 .items_by_leader_view_id
5574 .get(&active_view_id)?
5575 .view
5576 .boxed_clone(),
5577 )
5578 }
5579
5580 fn active_item_for_peer(
5581 &self,
5582 peer_id: PeerId,
5583 window: &mut Window,
5584 cx: &mut Context<Self>,
5585 ) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
5586 let call = self.active_call()?;
5587 let room = call.read(cx).room()?.read(cx);
5588 let participant = room.remote_participant_for_peer_id(peer_id)?;
5589 let leader_in_this_app;
5590 let leader_in_this_project;
5591 match participant.location {
5592 call::ParticipantLocation::SharedProject { project_id } => {
5593 leader_in_this_app = true;
5594 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
5595 }
5596 call::ParticipantLocation::UnsharedProject => {
5597 leader_in_this_app = true;
5598 leader_in_this_project = false;
5599 }
5600 call::ParticipantLocation::External => {
5601 leader_in_this_app = false;
5602 leader_in_this_project = false;
5603 }
5604 };
5605 let state = self.follower_states.get(&peer_id.into())?;
5606 let mut item_to_activate = None;
5607 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
5608 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id)
5609 && (leader_in_this_project || !item.view.is_project_item(window, cx))
5610 {
5611 item_to_activate = Some((item.location, item.view.boxed_clone()));
5612 }
5613 } else if let Some(shared_screen) =
5614 self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx)
5615 {
5616 item_to_activate = Some((None, Box::new(shared_screen)));
5617 }
5618 item_to_activate
5619 }
5620
5621 fn shared_screen_for_peer(
5622 &self,
5623 peer_id: PeerId,
5624 pane: &Entity<Pane>,
5625 window: &mut Window,
5626 cx: &mut App,
5627 ) -> Option<Entity<SharedScreen>> {
5628 let call = self.active_call()?;
5629 let room = call.read(cx).room()?.clone();
5630 let participant = room.read(cx).remote_participant_for_peer_id(peer_id)?;
5631 let track = participant.video_tracks.values().next()?.clone();
5632 let user = participant.user.clone();
5633
5634 for item in pane.read(cx).items_of_type::<SharedScreen>() {
5635 if item.read(cx).peer_id == peer_id {
5636 return Some(item);
5637 }
5638 }
5639
5640 Some(cx.new(|cx| SharedScreen::new(track, peer_id, user.clone(), room.clone(), window, cx)))
5641 }
5642
5643 pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5644 if window.is_window_active() {
5645 self.update_active_view_for_followers(window, cx);
5646
5647 if let Some(database_id) = self.database_id {
5648 cx.background_spawn(persistence::DB.update_timestamp(database_id))
5649 .detach();
5650 }
5651 } else {
5652 for pane in &self.panes {
5653 pane.update(cx, |pane, cx| {
5654 if let Some(item) = pane.active_item() {
5655 item.workspace_deactivated(window, cx);
5656 }
5657 for item in pane.items() {
5658 if matches!(
5659 item.workspace_settings(cx).autosave,
5660 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
5661 ) {
5662 Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
5663 .detach_and_log_err(cx);
5664 }
5665 }
5666 });
5667 }
5668 }
5669 }
5670
5671 pub fn active_call(&self) -> Option<&Entity<ActiveCall>> {
5672 self.active_call.as_ref().map(|(call, _)| call)
5673 }
5674
5675 fn on_active_call_event(
5676 &mut self,
5677 _: &Entity<ActiveCall>,
5678 event: &call::room::Event,
5679 window: &mut Window,
5680 cx: &mut Context<Self>,
5681 ) {
5682 match event {
5683 call::room::Event::ParticipantLocationChanged { participant_id }
5684 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
5685 self.leader_updated(participant_id, window, cx);
5686 }
5687 _ => {}
5688 }
5689 }
5690
5691 pub fn database_id(&self) -> Option<WorkspaceId> {
5692 self.database_id
5693 }
5694
5695 pub fn session_id(&self) -> Option<String> {
5696 self.session_id.clone()
5697 }
5698
5699 pub fn root_paths(&self, cx: &App) -> Vec<Arc<Path>> {
5700 let project = self.project().read(cx);
5701 project
5702 .visible_worktrees(cx)
5703 .map(|worktree| worktree.read(cx).abs_path())
5704 .collect::<Vec<_>>()
5705 }
5706
5707 fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
5708 match member {
5709 Member::Axis(PaneAxis { members, .. }) => {
5710 for child in members.iter() {
5711 self.remove_panes(child.clone(), window, cx)
5712 }
5713 }
5714 Member::Pane(pane) => {
5715 self.force_remove_pane(&pane, &None, window, cx);
5716 }
5717 }
5718 }
5719
5720 fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
5721 self.session_id.take();
5722 self.serialize_workspace_internal(window, cx)
5723 }
5724
5725 fn force_remove_pane(
5726 &mut self,
5727 pane: &Entity<Pane>,
5728 focus_on: &Option<Entity<Pane>>,
5729 window: &mut Window,
5730 cx: &mut Context<Workspace>,
5731 ) {
5732 self.panes.retain(|p| p != pane);
5733 if let Some(focus_on) = focus_on {
5734 focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
5735 } else if self.active_pane() == pane {
5736 self.panes
5737 .last()
5738 .unwrap()
5739 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
5740 }
5741 if self.last_active_center_pane == Some(pane.downgrade()) {
5742 self.last_active_center_pane = None;
5743 }
5744 cx.notify();
5745 }
5746
5747 fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5748 if self._schedule_serialize_workspace.is_none() {
5749 self._schedule_serialize_workspace =
5750 Some(cx.spawn_in(window, async move |this, cx| {
5751 cx.background_executor()
5752 .timer(SERIALIZATION_THROTTLE_TIME)
5753 .await;
5754 this.update_in(cx, |this, window, cx| {
5755 this.serialize_workspace_internal(window, cx).detach();
5756 this._schedule_serialize_workspace.take();
5757 })
5758 .log_err();
5759 }));
5760 }
5761 }
5762
5763 fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
5764 let Some(database_id) = self.database_id() else {
5765 return Task::ready(());
5766 };
5767
5768 fn serialize_pane_handle(
5769 pane_handle: &Entity<Pane>,
5770 window: &mut Window,
5771 cx: &mut App,
5772 ) -> SerializedPane {
5773 let (items, active, pinned_count) = {
5774 let pane = pane_handle.read(cx);
5775 let active_item_id = pane.active_item().map(|item| item.item_id());
5776 (
5777 pane.items()
5778 .filter_map(|handle| {
5779 let handle = handle.to_serializable_item_handle(cx)?;
5780
5781 Some(SerializedItem {
5782 kind: Arc::from(handle.serialized_item_kind()),
5783 item_id: handle.item_id().as_u64(),
5784 active: Some(handle.item_id()) == active_item_id,
5785 preview: pane.is_active_preview_item(handle.item_id()),
5786 })
5787 })
5788 .collect::<Vec<_>>(),
5789 pane.has_focus(window, cx),
5790 pane.pinned_count(),
5791 )
5792 };
5793
5794 SerializedPane::new(items, active, pinned_count)
5795 }
5796
5797 fn build_serialized_pane_group(
5798 pane_group: &Member,
5799 window: &mut Window,
5800 cx: &mut App,
5801 ) -> SerializedPaneGroup {
5802 match pane_group {
5803 Member::Axis(PaneAxis {
5804 axis,
5805 members,
5806 flexes,
5807 bounding_boxes: _,
5808 }) => SerializedPaneGroup::Group {
5809 axis: SerializedAxis(*axis),
5810 children: members
5811 .iter()
5812 .map(|member| build_serialized_pane_group(member, window, cx))
5813 .collect::<Vec<_>>(),
5814 flexes: Some(flexes.lock().clone()),
5815 },
5816 Member::Pane(pane_handle) => {
5817 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
5818 }
5819 }
5820 }
5821
5822 fn build_serialized_docks(
5823 this: &Workspace,
5824 window: &mut Window,
5825 cx: &mut App,
5826 ) -> DockStructure {
5827 let left_dock = this.left_dock.read(cx);
5828 let left_visible = left_dock.is_open();
5829 let left_active_panel = left_dock
5830 .active_panel()
5831 .map(|panel| panel.persistent_name().to_string());
5832 let left_dock_zoom = left_dock
5833 .active_panel()
5834 .map(|panel| panel.is_zoomed(window, cx))
5835 .unwrap_or(false);
5836
5837 let right_dock = this.right_dock.read(cx);
5838 let right_visible = right_dock.is_open();
5839 let right_active_panel = right_dock
5840 .active_panel()
5841 .map(|panel| panel.persistent_name().to_string());
5842 let right_dock_zoom = right_dock
5843 .active_panel()
5844 .map(|panel| panel.is_zoomed(window, cx))
5845 .unwrap_or(false);
5846
5847 let bottom_dock = this.bottom_dock.read(cx);
5848 let bottom_visible = bottom_dock.is_open();
5849 let bottom_active_panel = bottom_dock
5850 .active_panel()
5851 .map(|panel| panel.persistent_name().to_string());
5852 let bottom_dock_zoom = bottom_dock
5853 .active_panel()
5854 .map(|panel| panel.is_zoomed(window, cx))
5855 .unwrap_or(false);
5856
5857 DockStructure {
5858 left: DockData {
5859 visible: left_visible,
5860 active_panel: left_active_panel,
5861 zoom: left_dock_zoom,
5862 },
5863 right: DockData {
5864 visible: right_visible,
5865 active_panel: right_active_panel,
5866 zoom: right_dock_zoom,
5867 },
5868 bottom: DockData {
5869 visible: bottom_visible,
5870 active_panel: bottom_active_panel,
5871 zoom: bottom_dock_zoom,
5872 },
5873 }
5874 }
5875
5876 match self.workspace_location(cx) {
5877 WorkspaceLocation::Location(location, paths) => {
5878 let breakpoints = self.project.update(cx, |project, cx| {
5879 project
5880 .breakpoint_store()
5881 .read(cx)
5882 .all_source_breakpoints(cx)
5883 });
5884 let user_toolchains = self
5885 .project
5886 .read(cx)
5887 .user_toolchains(cx)
5888 .unwrap_or_default();
5889
5890 let center_group = build_serialized_pane_group(&self.center.root, window, cx);
5891 let docks = build_serialized_docks(self, window, cx);
5892 let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
5893
5894 let serialized_workspace = SerializedWorkspace {
5895 id: database_id,
5896 location,
5897 paths,
5898 center_group,
5899 window_bounds,
5900 display: Default::default(),
5901 docks,
5902 centered_layout: self.centered_layout,
5903 session_id: self.session_id.clone(),
5904 breakpoints,
5905 window_id: Some(window.window_handle().window_id().as_u64()),
5906 user_toolchains,
5907 };
5908
5909 window.spawn(cx, async move |_| {
5910 persistence::DB.save_workspace(serialized_workspace).await;
5911 })
5912 }
5913 WorkspaceLocation::DetachFromSession => {
5914 let window_bounds = SerializedWindowBounds(window.window_bounds());
5915 let display = window.display(cx).and_then(|d| d.uuid().ok());
5916 // Save dock state for empty local workspaces
5917 let docks = build_serialized_docks(self, window, cx);
5918 window.spawn(cx, async move |_| {
5919 persistence::DB
5920 .set_window_open_status(
5921 database_id,
5922 window_bounds,
5923 display.unwrap_or_default(),
5924 )
5925 .await
5926 .log_err();
5927 persistence::DB
5928 .set_session_id(database_id, None)
5929 .await
5930 .log_err();
5931 persistence::write_default_dock_state(docks).await.log_err();
5932 })
5933 }
5934 WorkspaceLocation::None => {
5935 // Save dock state for empty non-local workspaces
5936 let docks = build_serialized_docks(self, window, cx);
5937 window.spawn(cx, async move |_| {
5938 persistence::write_default_dock_state(docks).await.log_err();
5939 })
5940 }
5941 }
5942 }
5943
5944 fn has_any_items_open(&self, cx: &App) -> bool {
5945 self.panes.iter().any(|pane| pane.read(cx).items_len() > 0)
5946 }
5947
5948 fn workspace_location(&self, cx: &App) -> WorkspaceLocation {
5949 let paths = PathList::new(&self.root_paths(cx));
5950 if let Some(connection) = self.project.read(cx).remote_connection_options(cx) {
5951 WorkspaceLocation::Location(SerializedWorkspaceLocation::Remote(connection), paths)
5952 } else if self.project.read(cx).is_local() {
5953 if !paths.is_empty() || self.has_any_items_open(cx) {
5954 WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths)
5955 } else {
5956 WorkspaceLocation::DetachFromSession
5957 }
5958 } else {
5959 WorkspaceLocation::None
5960 }
5961 }
5962
5963 pub fn serialized_workspace_location(&self, cx: &App) -> Option<SerializedWorkspaceLocation> {
5964 if let Some(connection) = self.project.read(cx).remote_connection_options(cx) {
5965 Some(SerializedWorkspaceLocation::Remote(connection))
5966 } else if self.project.read(cx).is_local() && self.has_any_items_open(cx) {
5967 Some(SerializedWorkspaceLocation::Local)
5968 } else {
5969 None
5970 }
5971 }
5972
5973 fn update_history(&self, cx: &mut App) {
5974 let Some(id) = self.database_id() else {
5975 return;
5976 };
5977 if !self.project.read(cx).is_local() {
5978 return;
5979 }
5980 if let Some(manager) = HistoryManager::global(cx) {
5981 let paths = PathList::new(&self.root_paths(cx));
5982 manager.update(cx, |this, cx| {
5983 this.update_history(id, HistoryManagerEntry::new(id, &paths), cx);
5984 });
5985 }
5986 }
5987
5988 async fn serialize_items(
5989 this: &WeakEntity<Self>,
5990 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
5991 cx: &mut AsyncWindowContext,
5992 ) -> Result<()> {
5993 const CHUNK_SIZE: usize = 200;
5994
5995 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
5996
5997 while let Some(items_received) = serializable_items.next().await {
5998 let unique_items =
5999 items_received
6000 .into_iter()
6001 .fold(HashMap::default(), |mut acc, item| {
6002 acc.entry(item.item_id()).or_insert(item);
6003 acc
6004 });
6005
6006 // We use into_iter() here so that the references to the items are moved into
6007 // the tasks and not kept alive while we're sleeping.
6008 for (_, item) in unique_items.into_iter() {
6009 if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
6010 item.serialize(workspace, false, window, cx)
6011 }) {
6012 cx.background_spawn(async move { task.await.log_err() })
6013 .detach();
6014 }
6015 }
6016
6017 cx.background_executor()
6018 .timer(SERIALIZATION_THROTTLE_TIME)
6019 .await;
6020 }
6021
6022 Ok(())
6023 }
6024
6025 pub(crate) fn enqueue_item_serialization(
6026 &mut self,
6027 item: Box<dyn SerializableItemHandle>,
6028 ) -> Result<()> {
6029 self.serializable_items_tx
6030 .unbounded_send(item)
6031 .map_err(|err| anyhow!("failed to send serializable item over channel: {err}"))
6032 }
6033
6034 pub(crate) fn load_workspace(
6035 serialized_workspace: SerializedWorkspace,
6036 paths_to_open: Vec<Option<ProjectPath>>,
6037 window: &mut Window,
6038 cx: &mut Context<Workspace>,
6039 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
6040 cx.spawn_in(window, async move |workspace, cx| {
6041 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
6042
6043 let mut center_group = None;
6044 let mut center_items = None;
6045
6046 // Traverse the splits tree and add to things
6047 if let Some((group, active_pane, items)) = serialized_workspace
6048 .center_group
6049 .deserialize(&project, serialized_workspace.id, workspace.clone(), cx)
6050 .await
6051 {
6052 center_items = Some(items);
6053 center_group = Some((group, active_pane))
6054 }
6055
6056 let mut items_by_project_path = HashMap::default();
6057 let mut item_ids_by_kind = HashMap::default();
6058 let mut all_deserialized_items = Vec::default();
6059 cx.update(|_, cx| {
6060 for item in center_items.unwrap_or_default().into_iter().flatten() {
6061 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
6062 item_ids_by_kind
6063 .entry(serializable_item_handle.serialized_item_kind())
6064 .or_insert(Vec::new())
6065 .push(item.item_id().as_u64() as ItemId);
6066 }
6067
6068 if let Some(project_path) = item.project_path(cx) {
6069 items_by_project_path.insert(project_path, item.clone());
6070 }
6071 all_deserialized_items.push(item);
6072 }
6073 })?;
6074
6075 let opened_items = paths_to_open
6076 .into_iter()
6077 .map(|path_to_open| {
6078 path_to_open
6079 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
6080 })
6081 .collect::<Vec<_>>();
6082
6083 // Remove old panes from workspace panes list
6084 workspace.update_in(cx, |workspace, window, cx| {
6085 if let Some((center_group, active_pane)) = center_group {
6086 workspace.remove_panes(workspace.center.root.clone(), window, cx);
6087
6088 // Swap workspace center group
6089 workspace.center = PaneGroup::with_root(center_group);
6090 workspace.center.set_is_center(true);
6091 workspace.center.mark_positions(cx);
6092
6093 if let Some(active_pane) = active_pane {
6094 workspace.set_active_pane(&active_pane, window, cx);
6095 cx.focus_self(window);
6096 } else {
6097 workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
6098 }
6099 }
6100
6101 let docks = serialized_workspace.docks;
6102
6103 for (dock, serialized_dock) in [
6104 (&mut workspace.right_dock, docks.right),
6105 (&mut workspace.left_dock, docks.left),
6106 (&mut workspace.bottom_dock, docks.bottom),
6107 ]
6108 .iter_mut()
6109 {
6110 dock.update(cx, |dock, cx| {
6111 dock.serialized_dock = Some(serialized_dock.clone());
6112 dock.restore_state(window, cx);
6113 });
6114 }
6115
6116 cx.notify();
6117 })?;
6118
6119 let _ = project
6120 .update(cx, |project, cx| {
6121 project
6122 .breakpoint_store()
6123 .update(cx, |breakpoint_store, cx| {
6124 breakpoint_store
6125 .with_serialized_breakpoints(serialized_workspace.breakpoints, cx)
6126 })
6127 })
6128 .await;
6129
6130 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
6131 // after loading the items, we might have different items and in order to avoid
6132 // the database filling up, we delete items that haven't been loaded now.
6133 //
6134 // The items that have been loaded, have been saved after they've been added to the workspace.
6135 let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
6136 item_ids_by_kind
6137 .into_iter()
6138 .map(|(item_kind, loaded_items)| {
6139 SerializableItemRegistry::cleanup(
6140 item_kind,
6141 serialized_workspace.id,
6142 loaded_items,
6143 window,
6144 cx,
6145 )
6146 .log_err()
6147 })
6148 .collect::<Vec<_>>()
6149 })?;
6150
6151 futures::future::join_all(clean_up_tasks).await;
6152
6153 workspace
6154 .update_in(cx, |workspace, window, cx| {
6155 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
6156 workspace.serialize_workspace_internal(window, cx).detach();
6157
6158 // Ensure that we mark the window as edited if we did load dirty items
6159 workspace.update_window_edited(window, cx);
6160 })
6161 .ok();
6162
6163 Ok(opened_items)
6164 })
6165 }
6166
6167 fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
6168 self.add_workspace_actions_listeners(div, window, cx)
6169 .on_action(cx.listener(
6170 |_workspace, action_sequence: &settings::ActionSequence, window, cx| {
6171 for action in &action_sequence.0 {
6172 window.dispatch_action(action.boxed_clone(), cx);
6173 }
6174 },
6175 ))
6176 .on_action(cx.listener(Self::close_inactive_items_and_panes))
6177 .on_action(cx.listener(Self::close_all_items_and_panes))
6178 .on_action(cx.listener(Self::save_all))
6179 .on_action(cx.listener(Self::send_keystrokes))
6180 .on_action(cx.listener(Self::add_folder_to_project))
6181 .on_action(cx.listener(Self::follow_next_collaborator))
6182 .on_action(cx.listener(Self::close_window))
6183 .on_action(cx.listener(Self::activate_pane_at_index))
6184 .on_action(cx.listener(Self::move_item_to_pane_at_index))
6185 .on_action(cx.listener(Self::move_focused_panel_to_next_position))
6186 .on_action(cx.listener(Self::toggle_edit_predictions_all_files))
6187 .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
6188 let pane = workspace.active_pane().clone();
6189 workspace.unfollow_in_pane(&pane, window, cx);
6190 }))
6191 .on_action(cx.listener(|workspace, action: &Save, window, cx| {
6192 workspace
6193 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
6194 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6195 }))
6196 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
6197 workspace
6198 .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
6199 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6200 }))
6201 .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
6202 workspace
6203 .save_active_item(SaveIntent::SaveAs, window, cx)
6204 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
6205 }))
6206 .on_action(
6207 cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
6208 workspace.activate_previous_pane(window, cx)
6209 }),
6210 )
6211 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
6212 workspace.activate_next_pane(window, cx)
6213 }))
6214 .on_action(
6215 cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
6216 workspace.activate_next_window(cx)
6217 }),
6218 )
6219 .on_action(
6220 cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
6221 workspace.activate_previous_window(cx)
6222 }),
6223 )
6224 .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
6225 workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
6226 }))
6227 .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
6228 workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
6229 }))
6230 .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
6231 workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
6232 }))
6233 .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
6234 workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
6235 }))
6236 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
6237 workspace.activate_next_pane(window, cx)
6238 }))
6239 .on_action(cx.listener(
6240 |workspace, action: &MoveItemToPaneInDirection, window, cx| {
6241 workspace.move_item_to_pane_in_direction(action, window, cx)
6242 },
6243 ))
6244 .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
6245 workspace.swap_pane_in_direction(SplitDirection::Left, cx)
6246 }))
6247 .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
6248 workspace.swap_pane_in_direction(SplitDirection::Right, cx)
6249 }))
6250 .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
6251 workspace.swap_pane_in_direction(SplitDirection::Up, cx)
6252 }))
6253 .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
6254 workspace.swap_pane_in_direction(SplitDirection::Down, cx)
6255 }))
6256 .on_action(cx.listener(|workspace, _: &SwapPaneAdjacent, window, cx| {
6257 const DIRECTION_PRIORITY: [SplitDirection; 4] = [
6258 SplitDirection::Down,
6259 SplitDirection::Up,
6260 SplitDirection::Right,
6261 SplitDirection::Left,
6262 ];
6263 for dir in DIRECTION_PRIORITY {
6264 if workspace.find_pane_in_direction(dir, cx).is_some() {
6265 workspace.swap_pane_in_direction(dir, cx);
6266 workspace.activate_pane_in_direction(dir.opposite(), window, cx);
6267 break;
6268 }
6269 }
6270 }))
6271 .on_action(cx.listener(|workspace, _: &MovePaneLeft, _, cx| {
6272 workspace.move_pane_to_border(SplitDirection::Left, cx)
6273 }))
6274 .on_action(cx.listener(|workspace, _: &MovePaneRight, _, cx| {
6275 workspace.move_pane_to_border(SplitDirection::Right, cx)
6276 }))
6277 .on_action(cx.listener(|workspace, _: &MovePaneUp, _, cx| {
6278 workspace.move_pane_to_border(SplitDirection::Up, cx)
6279 }))
6280 .on_action(cx.listener(|workspace, _: &MovePaneDown, _, cx| {
6281 workspace.move_pane_to_border(SplitDirection::Down, cx)
6282 }))
6283 .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
6284 this.toggle_dock(DockPosition::Left, window, cx);
6285 }))
6286 .on_action(cx.listener(
6287 |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
6288 workspace.toggle_dock(DockPosition::Right, window, cx);
6289 },
6290 ))
6291 .on_action(cx.listener(
6292 |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
6293 workspace.toggle_dock(DockPosition::Bottom, window, cx);
6294 },
6295 ))
6296 .on_action(cx.listener(
6297 |workspace: &mut Workspace, _: &CloseActiveDock, window, cx| {
6298 if !workspace.close_active_dock(window, cx) {
6299 cx.propagate();
6300 }
6301 },
6302 ))
6303 .on_action(
6304 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
6305 workspace.close_all_docks(window, cx);
6306 }),
6307 )
6308 .on_action(cx.listener(Self::toggle_all_docks))
6309 .on_action(cx.listener(
6310 |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
6311 workspace.clear_all_notifications(cx);
6312 },
6313 ))
6314 .on_action(cx.listener(
6315 |workspace: &mut Workspace, _: &ClearNavigationHistory, window, cx| {
6316 workspace.clear_navigation_history(window, cx);
6317 },
6318 ))
6319 .on_action(cx.listener(
6320 |workspace: &mut Workspace, _: &SuppressNotification, _, cx| {
6321 if let Some((notification_id, _)) = workspace.notifications.pop() {
6322 workspace.suppress_notification(¬ification_id, cx);
6323 }
6324 },
6325 ))
6326 .on_action(cx.listener(
6327 |workspace: &mut Workspace, _: &ToggleWorktreeSecurity, window, cx| {
6328 workspace.show_worktree_trust_security_modal(true, window, cx);
6329 },
6330 ))
6331 .on_action(
6332 cx.listener(|_: &mut Workspace, _: &ClearTrustedWorktrees, _, cx| {
6333 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
6334 trusted_worktrees.update(cx, |trusted_worktrees, _| {
6335 trusted_worktrees.clear_trusted_paths()
6336 });
6337 let clear_task = persistence::DB.clear_trusted_worktrees();
6338 cx.spawn(async move |_, cx| {
6339 if clear_task.await.log_err().is_some() {
6340 cx.update(|cx| reload(cx));
6341 }
6342 })
6343 .detach();
6344 }
6345 }),
6346 )
6347 .on_action(cx.listener(
6348 |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
6349 workspace.reopen_closed_item(window, cx).detach();
6350 },
6351 ))
6352 .on_action(cx.listener(
6353 |workspace: &mut Workspace, _: &ResetActiveDockSize, window, cx| {
6354 for dock in workspace.all_docks() {
6355 if dock.focus_handle(cx).contains_focused(window, cx) {
6356 let Some(panel) = dock.read(cx).active_panel() else {
6357 return;
6358 };
6359
6360 // Set to `None`, then the size will fall back to the default.
6361 panel.clone().set_size(None, window, cx);
6362
6363 return;
6364 }
6365 }
6366 },
6367 ))
6368 .on_action(cx.listener(
6369 |workspace: &mut Workspace, _: &ResetOpenDocksSize, window, cx| {
6370 for dock in workspace.all_docks() {
6371 if let Some(panel) = dock.read(cx).visible_panel() {
6372 // Set to `None`, then the size will fall back to the default.
6373 panel.clone().set_size(None, window, cx);
6374 }
6375 }
6376 },
6377 ))
6378 .on_action(cx.listener(
6379 |workspace: &mut Workspace, act: &IncreaseActiveDockSize, window, cx| {
6380 adjust_active_dock_size_by_px(
6381 px_with_ui_font_fallback(act.px, cx),
6382 workspace,
6383 window,
6384 cx,
6385 );
6386 },
6387 ))
6388 .on_action(cx.listener(
6389 |workspace: &mut Workspace, act: &DecreaseActiveDockSize, window, cx| {
6390 adjust_active_dock_size_by_px(
6391 px_with_ui_font_fallback(act.px, cx) * -1.,
6392 workspace,
6393 window,
6394 cx,
6395 );
6396 },
6397 ))
6398 .on_action(cx.listener(
6399 |workspace: &mut Workspace, act: &IncreaseOpenDocksSize, window, cx| {
6400 adjust_open_docks_size_by_px(
6401 px_with_ui_font_fallback(act.px, cx),
6402 workspace,
6403 window,
6404 cx,
6405 );
6406 },
6407 ))
6408 .on_action(cx.listener(
6409 |workspace: &mut Workspace, act: &DecreaseOpenDocksSize, window, cx| {
6410 adjust_open_docks_size_by_px(
6411 px_with_ui_font_fallback(act.px, cx) * -1.,
6412 workspace,
6413 window,
6414 cx,
6415 );
6416 },
6417 ))
6418 .on_action(cx.listener(Workspace::toggle_centered_layout))
6419 .on_action(cx.listener(
6420 |workspace: &mut Workspace, _action: &pane::ActivateNextItem, window, cx| {
6421 if let Some(active_dock) = workspace.active_dock(window, cx) {
6422 let dock = active_dock.read(cx);
6423 if let Some(active_panel) = dock.active_panel() {
6424 if active_panel.pane(cx).is_none() {
6425 let mut recent_pane: Option<Entity<Pane>> = None;
6426 let mut recent_timestamp = 0;
6427 for pane_handle in workspace.panes() {
6428 let pane = pane_handle.read(cx);
6429 for entry in pane.activation_history() {
6430 if entry.timestamp > recent_timestamp {
6431 recent_timestamp = entry.timestamp;
6432 recent_pane = Some(pane_handle.clone());
6433 }
6434 }
6435 }
6436
6437 if let Some(pane) = recent_pane {
6438 pane.update(cx, |pane, cx| {
6439 let current_index = pane.active_item_index();
6440 let items_len = pane.items_len();
6441 if items_len > 0 {
6442 let next_index = if current_index + 1 < items_len {
6443 current_index + 1
6444 } else {
6445 0
6446 };
6447 pane.activate_item(
6448 next_index, false, false, window, cx,
6449 );
6450 }
6451 });
6452 return;
6453 }
6454 }
6455 }
6456 }
6457 cx.propagate();
6458 },
6459 ))
6460 .on_action(cx.listener(
6461 |workspace: &mut Workspace, _action: &pane::ActivatePreviousItem, window, cx| {
6462 if let Some(active_dock) = workspace.active_dock(window, cx) {
6463 let dock = active_dock.read(cx);
6464 if let Some(active_panel) = dock.active_panel() {
6465 if active_panel.pane(cx).is_none() {
6466 let mut recent_pane: Option<Entity<Pane>> = None;
6467 let mut recent_timestamp = 0;
6468 for pane_handle in workspace.panes() {
6469 let pane = pane_handle.read(cx);
6470 for entry in pane.activation_history() {
6471 if entry.timestamp > recent_timestamp {
6472 recent_timestamp = entry.timestamp;
6473 recent_pane = Some(pane_handle.clone());
6474 }
6475 }
6476 }
6477
6478 if let Some(pane) = recent_pane {
6479 pane.update(cx, |pane, cx| {
6480 let current_index = pane.active_item_index();
6481 let items_len = pane.items_len();
6482 if items_len > 0 {
6483 let prev_index = if current_index > 0 {
6484 current_index - 1
6485 } else {
6486 items_len.saturating_sub(1)
6487 };
6488 pane.activate_item(
6489 prev_index, false, false, window, cx,
6490 );
6491 }
6492 });
6493 return;
6494 }
6495 }
6496 }
6497 }
6498 cx.propagate();
6499 },
6500 ))
6501 .on_action(cx.listener(
6502 |workspace: &mut Workspace, action: &pane::CloseActiveItem, window, cx| {
6503 if let Some(active_dock) = workspace.active_dock(window, cx) {
6504 let dock = active_dock.read(cx);
6505 if let Some(active_panel) = dock.active_panel() {
6506 if active_panel.pane(cx).is_none() {
6507 let active_pane = workspace.active_pane().clone();
6508 active_pane.update(cx, |pane, cx| {
6509 pane.close_active_item(action, window, cx)
6510 .detach_and_log_err(cx);
6511 });
6512 return;
6513 }
6514 }
6515 }
6516 cx.propagate();
6517 },
6518 ))
6519 .on_action(
6520 cx.listener(|workspace, _: &ToggleReadOnlyFile, window, cx| {
6521 let pane = workspace.active_pane().clone();
6522 if let Some(item) = pane.read(cx).active_item() {
6523 item.toggle_read_only(window, cx);
6524 }
6525 }),
6526 )
6527 .on_action(cx.listener(Workspace::cancel))
6528 }
6529
6530 #[cfg(any(test, feature = "test-support"))]
6531 pub fn set_random_database_id(&mut self) {
6532 self.database_id = Some(WorkspaceId(Uuid::new_v4().as_u64_pair().0 as i64));
6533 }
6534
6535 #[cfg(any(test, feature = "test-support"))]
6536 pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
6537 use node_runtime::NodeRuntime;
6538 use session::Session;
6539
6540 let client = project.read(cx).client();
6541 let user_store = project.read(cx).user_store();
6542 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
6543 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
6544 window.activate_window();
6545 let app_state = Arc::new(AppState {
6546 languages: project.read(cx).languages().clone(),
6547 workspace_store,
6548 client,
6549 user_store,
6550 fs: project.read(cx).fs().clone(),
6551 build_window_options: |_, _| Default::default(),
6552 node_runtime: NodeRuntime::unavailable(),
6553 session,
6554 });
6555 let workspace = Self::new(Default::default(), project, app_state, window, cx);
6556 workspace
6557 .active_pane
6558 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
6559 workspace
6560 }
6561
6562 pub fn register_action<A: Action>(
6563 &mut self,
6564 callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
6565 ) -> &mut Self {
6566 let callback = Arc::new(callback);
6567
6568 self.workspace_actions.push(Box::new(move |div, _, _, cx| {
6569 let callback = callback.clone();
6570 div.on_action(cx.listener(move |workspace, event, window, cx| {
6571 (callback)(workspace, event, window, cx)
6572 }))
6573 }));
6574 self
6575 }
6576 pub fn register_action_renderer(
6577 &mut self,
6578 callback: impl Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div + 'static,
6579 ) -> &mut Self {
6580 self.workspace_actions.push(Box::new(callback));
6581 self
6582 }
6583
6584 fn add_workspace_actions_listeners(
6585 &self,
6586 mut div: Div,
6587 window: &mut Window,
6588 cx: &mut Context<Self>,
6589 ) -> Div {
6590 for action in self.workspace_actions.iter() {
6591 div = (action)(div, self, window, cx)
6592 }
6593 div
6594 }
6595
6596 pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
6597 self.modal_layer.read(cx).has_active_modal()
6598 }
6599
6600 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
6601 self.modal_layer.read(cx).active_modal()
6602 }
6603
6604 /// Toggles a modal of type `V`. If a modal of the same type is currently active,
6605 /// it will be hidden. If a different modal is active, it will be replaced with the new one.
6606 /// If no modal is active, the new modal will be shown.
6607 ///
6608 /// If closing the current modal fails (e.g., due to `on_before_dismiss` returning
6609 /// `DismissDecision::Dismiss(false)` or `DismissDecision::Pending`), the new modal
6610 /// will not be shown.
6611 pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
6612 where
6613 B: FnOnce(&mut Window, &mut Context<V>) -> V,
6614 {
6615 self.modal_layer.update(cx, |modal_layer, cx| {
6616 modal_layer.toggle_modal(window, cx, build)
6617 })
6618 }
6619
6620 pub fn hide_modal(&mut self, window: &mut Window, cx: &mut App) -> bool {
6621 self.modal_layer
6622 .update(cx, |modal_layer, cx| modal_layer.hide_modal(window, cx))
6623 }
6624
6625 pub fn toggle_status_toast<V: ToastView>(&mut self, entity: Entity<V>, cx: &mut App) {
6626 self.toast_layer
6627 .update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity))
6628 }
6629
6630 pub fn toggle_centered_layout(
6631 &mut self,
6632 _: &ToggleCenteredLayout,
6633 _: &mut Window,
6634 cx: &mut Context<Self>,
6635 ) {
6636 self.centered_layout = !self.centered_layout;
6637 if let Some(database_id) = self.database_id() {
6638 cx.background_spawn(DB.set_centered_layout(database_id, self.centered_layout))
6639 .detach_and_log_err(cx);
6640 }
6641 cx.notify();
6642 }
6643
6644 fn adjust_padding(padding: Option<f32>) -> f32 {
6645 padding
6646 .unwrap_or(CenteredPaddingSettings::default().0)
6647 .clamp(
6648 CenteredPaddingSettings::MIN_PADDING,
6649 CenteredPaddingSettings::MAX_PADDING,
6650 )
6651 }
6652
6653 fn render_dock(
6654 &self,
6655 position: DockPosition,
6656 dock: &Entity<Dock>,
6657 window: &mut Window,
6658 cx: &mut App,
6659 ) -> Option<Div> {
6660 if self.zoomed_position == Some(position) {
6661 return None;
6662 }
6663
6664 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
6665 let pane = panel.pane(cx)?;
6666 let follower_states = &self.follower_states;
6667 leader_border_for_pane(follower_states, &pane, window, cx)
6668 });
6669
6670 Some(
6671 div()
6672 .flex()
6673 .flex_none()
6674 .overflow_hidden()
6675 .child(dock.clone())
6676 .children(leader_border),
6677 )
6678 }
6679
6680 pub fn for_window(window: &mut Window, _: &mut App) -> Option<Entity<Workspace>> {
6681 window.root().flatten()
6682 }
6683
6684 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
6685 self.zoomed.as_ref()
6686 }
6687
6688 pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
6689 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
6690 return;
6691 };
6692 let windows = cx.windows();
6693 let next_window =
6694 SystemWindowTabController::get_next_tab_group_window(cx, current_window_id).or_else(
6695 || {
6696 windows
6697 .iter()
6698 .cycle()
6699 .skip_while(|window| window.window_id() != current_window_id)
6700 .nth(1)
6701 },
6702 );
6703
6704 if let Some(window) = next_window {
6705 window
6706 .update(cx, |_, window, _| window.activate_window())
6707 .ok();
6708 }
6709 }
6710
6711 pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
6712 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
6713 return;
6714 };
6715 let windows = cx.windows();
6716 let prev_window =
6717 SystemWindowTabController::get_prev_tab_group_window(cx, current_window_id).or_else(
6718 || {
6719 windows
6720 .iter()
6721 .rev()
6722 .cycle()
6723 .skip_while(|window| window.window_id() != current_window_id)
6724 .nth(1)
6725 },
6726 );
6727
6728 if let Some(window) = prev_window {
6729 window
6730 .update(cx, |_, window, _| window.activate_window())
6731 .ok();
6732 }
6733 }
6734
6735 pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
6736 if cx.stop_active_drag(window) {
6737 } else if let Some((notification_id, _)) = self.notifications.pop() {
6738 dismiss_app_notification(¬ification_id, cx);
6739 } else {
6740 cx.propagate();
6741 }
6742 }
6743
6744 fn adjust_dock_size_by_px(
6745 &mut self,
6746 panel_size: Pixels,
6747 dock_pos: DockPosition,
6748 px: Pixels,
6749 window: &mut Window,
6750 cx: &mut Context<Self>,
6751 ) {
6752 match dock_pos {
6753 DockPosition::Left => self.resize_left_dock(panel_size + px, window, cx),
6754 DockPosition::Right => self.resize_right_dock(panel_size + px, window, cx),
6755 DockPosition::Bottom => self.resize_bottom_dock(panel_size + px, window, cx),
6756 }
6757 }
6758
6759 fn resize_left_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
6760 let size = new_size.min(self.bounds.right() - RESIZE_HANDLE_SIZE);
6761
6762 self.left_dock.update(cx, |left_dock, cx| {
6763 if WorkspaceSettings::get_global(cx)
6764 .resize_all_panels_in_dock
6765 .contains(&DockPosition::Left)
6766 {
6767 left_dock.resize_all_panels(Some(size), window, cx);
6768 } else {
6769 left_dock.resize_active_panel(Some(size), window, cx);
6770 }
6771 });
6772 self.clamp_utility_pane_widths(window, cx);
6773 }
6774
6775 fn resize_right_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
6776 let mut size = new_size.max(self.bounds.left() - RESIZE_HANDLE_SIZE);
6777 self.left_dock.read_with(cx, |left_dock, cx| {
6778 let left_dock_size = left_dock
6779 .active_panel_size(window, cx)
6780 .unwrap_or(Pixels::ZERO);
6781 if left_dock_size + size > self.bounds.right() {
6782 size = self.bounds.right() - left_dock_size
6783 }
6784 });
6785 self.right_dock.update(cx, |right_dock, cx| {
6786 if WorkspaceSettings::get_global(cx)
6787 .resize_all_panels_in_dock
6788 .contains(&DockPosition::Right)
6789 {
6790 right_dock.resize_all_panels(Some(size), window, cx);
6791 } else {
6792 right_dock.resize_active_panel(Some(size), window, cx);
6793 }
6794 });
6795 self.clamp_utility_pane_widths(window, cx);
6796 }
6797
6798 fn resize_bottom_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
6799 let size = new_size.min(self.bounds.bottom() - RESIZE_HANDLE_SIZE - self.bounds.top());
6800 self.bottom_dock.update(cx, |bottom_dock, cx| {
6801 if WorkspaceSettings::get_global(cx)
6802 .resize_all_panels_in_dock
6803 .contains(&DockPosition::Bottom)
6804 {
6805 bottom_dock.resize_all_panels(Some(size), window, cx);
6806 } else {
6807 bottom_dock.resize_active_panel(Some(size), window, cx);
6808 }
6809 });
6810 self.clamp_utility_pane_widths(window, cx);
6811 }
6812
6813 fn max_utility_pane_width(&self, window: &Window, cx: &App) -> Pixels {
6814 let left_dock_width = self
6815 .left_dock
6816 .read(cx)
6817 .active_panel_size(window, cx)
6818 .unwrap_or(px(0.0));
6819 let right_dock_width = self
6820 .right_dock
6821 .read(cx)
6822 .active_panel_size(window, cx)
6823 .unwrap_or(px(0.0));
6824 let center_pane_width = self.bounds.size.width - left_dock_width - right_dock_width;
6825 center_pane_width - px(10.0)
6826 }
6827
6828 fn clamp_utility_pane_widths(&mut self, window: &mut Window, cx: &mut App) {
6829 let max_width = self.max_utility_pane_width(window, cx);
6830
6831 // Clamp left slot utility pane if it exists
6832 if let Some(handle) = self.utility_pane(UtilityPaneSlot::Left) {
6833 let current_width = handle.width(cx);
6834 if current_width > max_width {
6835 handle.set_width(Some(max_width.max(UTILITY_PANE_MIN_WIDTH)), cx);
6836 }
6837 }
6838
6839 // Clamp right slot utility pane if it exists
6840 if let Some(handle) = self.utility_pane(UtilityPaneSlot::Right) {
6841 let current_width = handle.width(cx);
6842 if current_width > max_width {
6843 handle.set_width(Some(max_width.max(UTILITY_PANE_MIN_WIDTH)), cx);
6844 }
6845 }
6846 }
6847
6848 fn toggle_edit_predictions_all_files(
6849 &mut self,
6850 _: &ToggleEditPrediction,
6851 _window: &mut Window,
6852 cx: &mut Context<Self>,
6853 ) {
6854 let fs = self.project().read(cx).fs().clone();
6855 let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx);
6856 update_settings_file(fs, cx, move |file, _| {
6857 file.project.all_languages.defaults.show_edit_predictions = Some(!show_edit_predictions)
6858 });
6859 }
6860
6861 pub fn show_worktree_trust_security_modal(
6862 &mut self,
6863 toggle: bool,
6864 window: &mut Window,
6865 cx: &mut Context<Self>,
6866 ) {
6867 if let Some(security_modal) = self.active_modal::<SecurityModal>(cx) {
6868 if toggle {
6869 security_modal.update(cx, |security_modal, cx| {
6870 security_modal.dismiss(cx);
6871 })
6872 } else {
6873 security_modal.update(cx, |security_modal, cx| {
6874 security_modal.refresh_restricted_paths(cx);
6875 });
6876 }
6877 } else {
6878 let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
6879 .map(|trusted_worktrees| {
6880 trusted_worktrees
6881 .read(cx)
6882 .has_restricted_worktrees(&self.project().read(cx).worktree_store(), cx)
6883 })
6884 .unwrap_or(false);
6885 if has_restricted_worktrees {
6886 let project = self.project().read(cx);
6887 let remote_host = project
6888 .remote_connection_options(cx)
6889 .map(RemoteHostLocation::from);
6890 let worktree_store = project.worktree_store().downgrade();
6891 self.toggle_modal(window, cx, |_, cx| {
6892 SecurityModal::new(worktree_store, remote_host, cx)
6893 });
6894 }
6895 }
6896 }
6897}
6898
6899fn leader_border_for_pane(
6900 follower_states: &HashMap<CollaboratorId, FollowerState>,
6901 pane: &Entity<Pane>,
6902 _: &Window,
6903 cx: &App,
6904) -> Option<Div> {
6905 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
6906 if state.pane() == pane {
6907 Some((*leader_id, state))
6908 } else {
6909 None
6910 }
6911 })?;
6912
6913 let mut leader_color = match leader_id {
6914 CollaboratorId::PeerId(leader_peer_id) => {
6915 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
6916 let leader = room.remote_participant_for_peer_id(leader_peer_id)?;
6917
6918 cx.theme()
6919 .players()
6920 .color_for_participant(leader.participant_index.0)
6921 .cursor
6922 }
6923 CollaboratorId::Agent => cx.theme().players().agent().cursor,
6924 };
6925 leader_color.fade_out(0.3);
6926 Some(
6927 div()
6928 .absolute()
6929 .size_full()
6930 .left_0()
6931 .top_0()
6932 .border_2()
6933 .border_color(leader_color),
6934 )
6935}
6936
6937fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
6938 ZED_WINDOW_POSITION
6939 .zip(*ZED_WINDOW_SIZE)
6940 .map(|(position, size)| Bounds {
6941 origin: position,
6942 size,
6943 })
6944}
6945
6946fn open_items(
6947 serialized_workspace: Option<SerializedWorkspace>,
6948 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
6949 window: &mut Window,
6950 cx: &mut Context<Workspace>,
6951) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
6952 let restored_items = serialized_workspace.map(|serialized_workspace| {
6953 Workspace::load_workspace(
6954 serialized_workspace,
6955 project_paths_to_open
6956 .iter()
6957 .map(|(_, project_path)| project_path)
6958 .cloned()
6959 .collect(),
6960 window,
6961 cx,
6962 )
6963 });
6964
6965 cx.spawn_in(window, async move |workspace, cx| {
6966 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
6967
6968 if let Some(restored_items) = restored_items {
6969 let restored_items = restored_items.await?;
6970
6971 let restored_project_paths = restored_items
6972 .iter()
6973 .filter_map(|item| {
6974 cx.update(|_, cx| item.as_ref()?.project_path(cx))
6975 .ok()
6976 .flatten()
6977 })
6978 .collect::<HashSet<_>>();
6979
6980 for restored_item in restored_items {
6981 opened_items.push(restored_item.map(Ok));
6982 }
6983
6984 project_paths_to_open
6985 .iter_mut()
6986 .for_each(|(_, project_path)| {
6987 if let Some(project_path_to_open) = project_path
6988 && restored_project_paths.contains(project_path_to_open)
6989 {
6990 *project_path = None;
6991 }
6992 });
6993 } else {
6994 for _ in 0..project_paths_to_open.len() {
6995 opened_items.push(None);
6996 }
6997 }
6998 assert!(opened_items.len() == project_paths_to_open.len());
6999
7000 let tasks =
7001 project_paths_to_open
7002 .into_iter()
7003 .enumerate()
7004 .map(|(ix, (abs_path, project_path))| {
7005 let workspace = workspace.clone();
7006 cx.spawn(async move |cx| {
7007 let file_project_path = project_path?;
7008 let abs_path_task = workspace.update(cx, |workspace, cx| {
7009 workspace.project().update(cx, |project, cx| {
7010 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
7011 })
7012 });
7013
7014 // We only want to open file paths here. If one of the items
7015 // here is a directory, it was already opened further above
7016 // with a `find_or_create_worktree`.
7017 if let Ok(task) = abs_path_task
7018 && task.await.is_none_or(|p| p.is_file())
7019 {
7020 return Some((
7021 ix,
7022 workspace
7023 .update_in(cx, |workspace, window, cx| {
7024 workspace.open_path(
7025 file_project_path,
7026 None,
7027 true,
7028 window,
7029 cx,
7030 )
7031 })
7032 .log_err()?
7033 .await,
7034 ));
7035 }
7036 None
7037 })
7038 });
7039
7040 let tasks = tasks.collect::<Vec<_>>();
7041
7042 let tasks = futures::future::join_all(tasks);
7043 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
7044 opened_items[ix] = Some(path_open_result);
7045 }
7046
7047 Ok(opened_items)
7048 })
7049}
7050
7051enum ActivateInDirectionTarget {
7052 Pane(Entity<Pane>),
7053 Dock(Entity<Dock>),
7054}
7055
7056fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
7057 workspace
7058 .update(cx, |workspace, _, cx| {
7059 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
7060 struct DatabaseFailedNotification;
7061
7062 workspace.show_notification(
7063 NotificationId::unique::<DatabaseFailedNotification>(),
7064 cx,
7065 |cx| {
7066 cx.new(|cx| {
7067 MessageNotification::new("Failed to load the database file.", cx)
7068 .primary_message("File an Issue")
7069 .primary_icon(IconName::Plus)
7070 .primary_on_click(|window, cx| {
7071 window.dispatch_action(Box::new(FileBugReport), cx)
7072 })
7073 })
7074 },
7075 );
7076 }
7077 })
7078 .log_err();
7079}
7080
7081fn px_with_ui_font_fallback(val: u32, cx: &Context<Workspace>) -> Pixels {
7082 if val == 0 {
7083 ThemeSettings::get_global(cx).ui_font_size(cx)
7084 } else {
7085 px(val as f32)
7086 }
7087}
7088
7089fn adjust_active_dock_size_by_px(
7090 px: Pixels,
7091 workspace: &mut Workspace,
7092 window: &mut Window,
7093 cx: &mut Context<Workspace>,
7094) {
7095 let Some(active_dock) = workspace
7096 .all_docks()
7097 .into_iter()
7098 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx))
7099 else {
7100 return;
7101 };
7102 let dock = active_dock.read(cx);
7103 let Some(panel_size) = dock.active_panel_size(window, cx) else {
7104 return;
7105 };
7106 let dock_pos = dock.position();
7107 workspace.adjust_dock_size_by_px(panel_size, dock_pos, px, window, cx);
7108}
7109
7110fn adjust_open_docks_size_by_px(
7111 px: Pixels,
7112 workspace: &mut Workspace,
7113 window: &mut Window,
7114 cx: &mut Context<Workspace>,
7115) {
7116 let docks = workspace
7117 .all_docks()
7118 .into_iter()
7119 .filter_map(|dock| {
7120 if dock.read(cx).is_open() {
7121 let dock = dock.read(cx);
7122 let panel_size = dock.active_panel_size(window, cx)?;
7123 let dock_pos = dock.position();
7124 Some((panel_size, dock_pos, px))
7125 } else {
7126 None
7127 }
7128 })
7129 .collect::<Vec<_>>();
7130
7131 docks
7132 .into_iter()
7133 .for_each(|(panel_size, dock_pos, offset)| {
7134 workspace.adjust_dock_size_by_px(panel_size, dock_pos, offset, window, cx);
7135 });
7136}
7137
7138impl Focusable for Workspace {
7139 fn focus_handle(&self, cx: &App) -> FocusHandle {
7140 self.active_pane.focus_handle(cx)
7141 }
7142}
7143
7144#[derive(Clone)]
7145struct DraggedDock(DockPosition);
7146
7147impl Render for DraggedDock {
7148 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
7149 gpui::Empty
7150 }
7151}
7152
7153impl Render for Workspace {
7154 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
7155 static FIRST_PAINT: AtomicBool = AtomicBool::new(true);
7156 if FIRST_PAINT.swap(false, std::sync::atomic::Ordering::Relaxed) {
7157 log::info!("Rendered first frame");
7158 }
7159 let mut context = KeyContext::new_with_defaults();
7160 context.add("Workspace");
7161 context.set("keyboard_layout", cx.keyboard_layout().name().to_string());
7162 if let Some(status) = self
7163 .debugger_provider
7164 .as_ref()
7165 .and_then(|provider| provider.active_thread_state(cx))
7166 {
7167 match status {
7168 ThreadStatus::Running | ThreadStatus::Stepping => {
7169 context.add("debugger_running");
7170 }
7171 ThreadStatus::Stopped => context.add("debugger_stopped"),
7172 ThreadStatus::Exited | ThreadStatus::Ended => {}
7173 }
7174 }
7175
7176 if self.left_dock.read(cx).is_open() {
7177 if let Some(active_panel) = self.left_dock.read(cx).active_panel() {
7178 context.set("left_dock", active_panel.panel_key());
7179 }
7180 }
7181
7182 if self.right_dock.read(cx).is_open() {
7183 if let Some(active_panel) = self.right_dock.read(cx).active_panel() {
7184 context.set("right_dock", active_panel.panel_key());
7185 }
7186 }
7187
7188 if self.bottom_dock.read(cx).is_open() {
7189 if let Some(active_panel) = self.bottom_dock.read(cx).active_panel() {
7190 context.set("bottom_dock", active_panel.panel_key());
7191 }
7192 }
7193
7194 let centered_layout = self.centered_layout
7195 && self.center.panes().len() == 1
7196 && self.active_item(cx).is_some();
7197 let render_padding = |size| {
7198 (size > 0.0).then(|| {
7199 div()
7200 .h_full()
7201 .w(relative(size))
7202 .bg(cx.theme().colors().editor_background)
7203 .border_color(cx.theme().colors().pane_group_border)
7204 })
7205 };
7206 let paddings = if centered_layout {
7207 let settings = WorkspaceSettings::get_global(cx).centered_layout;
7208 (
7209 render_padding(Self::adjust_padding(
7210 settings.left_padding.map(|padding| padding.0),
7211 )),
7212 render_padding(Self::adjust_padding(
7213 settings.right_padding.map(|padding| padding.0),
7214 )),
7215 )
7216 } else {
7217 (None, None)
7218 };
7219 let ui_font = theme::setup_ui_font(window, cx);
7220
7221 let theme = cx.theme().clone();
7222 let colors = theme.colors();
7223 let notification_entities = self
7224 .notifications
7225 .iter()
7226 .map(|(_, notification)| notification.entity_id())
7227 .collect::<Vec<_>>();
7228 let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout;
7229
7230 client_side_decorations(
7231 self.actions(div(), window, cx)
7232 .key_context(context)
7233 .relative()
7234 .size_full()
7235 .flex()
7236 .flex_col()
7237 .font(ui_font)
7238 .gap_0()
7239 .justify_start()
7240 .items_start()
7241 .text_color(colors.text)
7242 .overflow_hidden()
7243 .children(self.titlebar_item.clone())
7244 .on_modifiers_changed(move |_, _, cx| {
7245 for &id in ¬ification_entities {
7246 cx.notify(id);
7247 }
7248 })
7249 .child(
7250 div()
7251 .size_full()
7252 .relative()
7253 .flex_1()
7254 .flex()
7255 .flex_col()
7256 .child(
7257 div()
7258 .id("workspace")
7259 .bg(colors.background)
7260 .relative()
7261 .flex_1()
7262 .w_full()
7263 .flex()
7264 .flex_col()
7265 .overflow_hidden()
7266 .border_t_1()
7267 .border_b_1()
7268 .border_color(colors.border)
7269 .child({
7270 let this = cx.entity();
7271 canvas(
7272 move |bounds, window, cx| {
7273 this.update(cx, |this, cx| {
7274 let bounds_changed = this.bounds != bounds;
7275 this.bounds = bounds;
7276
7277 if bounds_changed {
7278 this.left_dock.update(cx, |dock, cx| {
7279 dock.clamp_panel_size(
7280 bounds.size.width,
7281 window,
7282 cx,
7283 )
7284 });
7285
7286 this.right_dock.update(cx, |dock, cx| {
7287 dock.clamp_panel_size(
7288 bounds.size.width,
7289 window,
7290 cx,
7291 )
7292 });
7293
7294 this.bottom_dock.update(cx, |dock, cx| {
7295 dock.clamp_panel_size(
7296 bounds.size.height,
7297 window,
7298 cx,
7299 )
7300 });
7301 }
7302 })
7303 },
7304 |_, _, _, _| {},
7305 )
7306 .absolute()
7307 .size_full()
7308 })
7309 .when(self.zoomed.is_none(), |this| {
7310 this.on_drag_move(cx.listener(
7311 move |workspace,
7312 e: &DragMoveEvent<DraggedDock>,
7313 window,
7314 cx| {
7315 if workspace.previous_dock_drag_coordinates
7316 != Some(e.event.position)
7317 {
7318 workspace.previous_dock_drag_coordinates =
7319 Some(e.event.position);
7320 match e.drag(cx).0 {
7321 DockPosition::Left => {
7322 workspace.resize_left_dock(
7323 e.event.position.x
7324 - workspace.bounds.left(),
7325 window,
7326 cx,
7327 );
7328 }
7329 DockPosition::Right => {
7330 workspace.resize_right_dock(
7331 workspace.bounds.right()
7332 - e.event.position.x,
7333 window,
7334 cx,
7335 );
7336 }
7337 DockPosition::Bottom => {
7338 workspace.resize_bottom_dock(
7339 workspace.bounds.bottom()
7340 - e.event.position.y,
7341 window,
7342 cx,
7343 );
7344 }
7345 };
7346 workspace.serialize_workspace(window, cx);
7347 }
7348 },
7349 ))
7350 .on_drag_move(cx.listener(
7351 move |workspace,
7352 e: &DragMoveEvent<DraggedUtilityPane>,
7353 window,
7354 cx| {
7355 let slot = e.drag(cx).0;
7356 match slot {
7357 UtilityPaneSlot::Left => {
7358 let left_dock_width = workspace.left_dock.read(cx)
7359 .active_panel_size(window, cx)
7360 .unwrap_or(gpui::px(0.0));
7361 let new_width = e.event.position.x
7362 - workspace.bounds.left()
7363 - left_dock_width;
7364 workspace.resize_utility_pane(slot, new_width, window, cx);
7365 }
7366 UtilityPaneSlot::Right => {
7367 let right_dock_width = workspace.right_dock.read(cx)
7368 .active_panel_size(window, cx)
7369 .unwrap_or(gpui::px(0.0));
7370 let new_width = workspace.bounds.right()
7371 - e.event.position.x
7372 - right_dock_width;
7373 workspace.resize_utility_pane(slot, new_width, window, cx);
7374 }
7375 }
7376 },
7377 ))
7378 })
7379 .child({
7380 match bottom_dock_layout {
7381 BottomDockLayout::Full => div()
7382 .flex()
7383 .flex_col()
7384 .h_full()
7385 .child(
7386 div()
7387 .flex()
7388 .flex_row()
7389 .flex_1()
7390 .overflow_hidden()
7391 .children(self.render_dock(
7392 DockPosition::Left,
7393 &self.left_dock,
7394 window,
7395 cx,
7396 ))
7397 .when(cx.has_flag::<AgentV2FeatureFlag>(), |this| {
7398 this.when_some(self.utility_pane(UtilityPaneSlot::Left), |this, pane| {
7399 this.when(pane.expanded(cx), |this| {
7400 this.child(
7401 UtilityPaneFrame::new(UtilityPaneSlot::Left, pane.box_clone(), cx)
7402 )
7403 })
7404 })
7405 })
7406 .child(
7407 div()
7408 .flex()
7409 .flex_col()
7410 .flex_1()
7411 .overflow_hidden()
7412 .child(
7413 h_flex()
7414 .flex_1()
7415 .when_some(
7416 paddings.0,
7417 |this, p| {
7418 this.child(
7419 p.border_r_1(),
7420 )
7421 },
7422 )
7423 .child(self.center.render(
7424 self.zoomed.as_ref(),
7425 &PaneRenderContext {
7426 follower_states:
7427 &self.follower_states,
7428 active_call: self.active_call(),
7429 active_pane: &self.active_pane,
7430 app_state: &self.app_state,
7431 project: &self.project,
7432 workspace: &self.weak_self,
7433 },
7434 window,
7435 cx,
7436 ))
7437 .when_some(
7438 paddings.1,
7439 |this, p| {
7440 this.child(
7441 p.border_l_1(),
7442 )
7443 },
7444 ),
7445 ),
7446 )
7447 .when(cx.has_flag::<AgentV2FeatureFlag>(), |this| {
7448 this.when_some(self.utility_pane(UtilityPaneSlot::Right), |this, pane| {
7449 this.when(pane.expanded(cx), |this| {
7450 this.child(
7451 UtilityPaneFrame::new(UtilityPaneSlot::Right, pane.box_clone(), cx)
7452 )
7453 })
7454 })
7455 })
7456 .children(self.render_dock(
7457 DockPosition::Right,
7458 &self.right_dock,
7459 window,
7460 cx,
7461 )),
7462 )
7463 .child(div().w_full().children(self.render_dock(
7464 DockPosition::Bottom,
7465 &self.bottom_dock,
7466 window,
7467 cx
7468 ))),
7469
7470 BottomDockLayout::LeftAligned => div()
7471 .flex()
7472 .flex_row()
7473 .h_full()
7474 .child(
7475 div()
7476 .flex()
7477 .flex_col()
7478 .flex_1()
7479 .h_full()
7480 .child(
7481 div()
7482 .flex()
7483 .flex_row()
7484 .flex_1()
7485 .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx))
7486 .when(cx.has_flag::<AgentV2FeatureFlag>(), |this| {
7487 this.when_some(self.utility_pane(UtilityPaneSlot::Left), |this, pane| {
7488 this.when(pane.expanded(cx), |this| {
7489 this.child(
7490 UtilityPaneFrame::new(UtilityPaneSlot::Left, pane.box_clone(), cx)
7491 )
7492 })
7493 })
7494 })
7495 .child(
7496 div()
7497 .flex()
7498 .flex_col()
7499 .flex_1()
7500 .overflow_hidden()
7501 .child(
7502 h_flex()
7503 .flex_1()
7504 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
7505 .child(self.center.render(
7506 self.zoomed.as_ref(),
7507 &PaneRenderContext {
7508 follower_states:
7509 &self.follower_states,
7510 active_call: self.active_call(),
7511 active_pane: &self.active_pane,
7512 app_state: &self.app_state,
7513 project: &self.project,
7514 workspace: &self.weak_self,
7515 },
7516 window,
7517 cx,
7518 ))
7519 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
7520 )
7521 )
7522 .when_some(self.utility_pane(UtilityPaneSlot::Right), |this, pane| {
7523 this.when(pane.expanded(cx), |this| {
7524 this.child(
7525 UtilityPaneFrame::new(UtilityPaneSlot::Right, pane.box_clone(), cx)
7526 )
7527 })
7528 })
7529 )
7530 .child(
7531 div()
7532 .w_full()
7533 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
7534 ),
7535 )
7536 .children(self.render_dock(
7537 DockPosition::Right,
7538 &self.right_dock,
7539 window,
7540 cx,
7541 )),
7542
7543 BottomDockLayout::RightAligned => div()
7544 .flex()
7545 .flex_row()
7546 .h_full()
7547 .children(self.render_dock(
7548 DockPosition::Left,
7549 &self.left_dock,
7550 window,
7551 cx,
7552 ))
7553 .when(cx.has_flag::<AgentV2FeatureFlag>(), |this| {
7554 this.when_some(self.utility_pane(UtilityPaneSlot::Left), |this, pane| {
7555 this.when(pane.expanded(cx), |this| {
7556 this.child(
7557 UtilityPaneFrame::new(UtilityPaneSlot::Left, pane.box_clone(), cx)
7558 )
7559 })
7560 })
7561 })
7562 .child(
7563 div()
7564 .flex()
7565 .flex_col()
7566 .flex_1()
7567 .h_full()
7568 .child(
7569 div()
7570 .flex()
7571 .flex_row()
7572 .flex_1()
7573 .child(
7574 div()
7575 .flex()
7576 .flex_col()
7577 .flex_1()
7578 .overflow_hidden()
7579 .child(
7580 h_flex()
7581 .flex_1()
7582 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
7583 .child(self.center.render(
7584 self.zoomed.as_ref(),
7585 &PaneRenderContext {
7586 follower_states:
7587 &self.follower_states,
7588 active_call: self.active_call(),
7589 active_pane: &self.active_pane,
7590 app_state: &self.app_state,
7591 project: &self.project,
7592 workspace: &self.weak_self,
7593 },
7594 window,
7595 cx,
7596 ))
7597 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
7598 )
7599 )
7600 .when(cx.has_flag::<AgentV2FeatureFlag>(), |this| {
7601 this.when_some(self.utility_pane(UtilityPaneSlot::Right), |this, pane| {
7602 this.when(pane.expanded(cx), |this| {
7603 this.child(
7604 UtilityPaneFrame::new(UtilityPaneSlot::Right, pane.box_clone(), cx)
7605 )
7606 })
7607 })
7608 })
7609 .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx))
7610 )
7611 .child(
7612 div()
7613 .w_full()
7614 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
7615 ),
7616 ),
7617
7618 BottomDockLayout::Contained => div()
7619 .flex()
7620 .flex_row()
7621 .h_full()
7622 .children(self.render_dock(
7623 DockPosition::Left,
7624 &self.left_dock,
7625 window,
7626 cx,
7627 ))
7628 .when_some(self.utility_pane(UtilityPaneSlot::Left), |this, pane| {
7629 this.when(pane.expanded(cx), |this| {
7630 this.child(
7631 UtilityPaneFrame::new(UtilityPaneSlot::Left, pane.box_clone(), cx)
7632 )
7633 })
7634 })
7635 .child(
7636 div()
7637 .flex()
7638 .flex_col()
7639 .flex_1()
7640 .overflow_hidden()
7641 .child(
7642 h_flex()
7643 .flex_1()
7644 .when_some(paddings.0, |this, p| {
7645 this.child(p.border_r_1())
7646 })
7647 .child(self.center.render(
7648 self.zoomed.as_ref(),
7649 &PaneRenderContext {
7650 follower_states:
7651 &self.follower_states,
7652 active_call: self.active_call(),
7653 active_pane: &self.active_pane,
7654 app_state: &self.app_state,
7655 project: &self.project,
7656 workspace: &self.weak_self,
7657 },
7658 window,
7659 cx,
7660 ))
7661 .when_some(paddings.1, |this, p| {
7662 this.child(p.border_l_1())
7663 }),
7664 )
7665 .children(self.render_dock(
7666 DockPosition::Bottom,
7667 &self.bottom_dock,
7668 window,
7669 cx,
7670 )),
7671 )
7672 .when(cx.has_flag::<AgentV2FeatureFlag>(), |this| {
7673 this.when_some(self.utility_pane(UtilityPaneSlot::Right), |this, pane| {
7674 this.when(pane.expanded(cx), |this| {
7675 this.child(
7676 UtilityPaneFrame::new(UtilityPaneSlot::Right, pane.box_clone(), cx)
7677 )
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 window,
7723 cx,
7724 )
7725 }
7726}
7727
7728impl WorkspaceStore {
7729 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
7730 Self {
7731 workspaces: Default::default(),
7732 _subscriptions: vec![
7733 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
7734 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
7735 ],
7736 client,
7737 }
7738 }
7739
7740 pub fn update_followers(
7741 &self,
7742 project_id: Option<u64>,
7743 update: proto::update_followers::Variant,
7744 cx: &App,
7745 ) -> Option<()> {
7746 let active_call = ActiveCall::try_global(cx)?;
7747 let room_id = active_call.read(cx).room()?.read(cx).id();
7748 self.client
7749 .send(proto::UpdateFollowers {
7750 room_id,
7751 project_id,
7752 variant: Some(update),
7753 })
7754 .log_err()
7755 }
7756
7757 pub async fn handle_follow(
7758 this: Entity<Self>,
7759 envelope: TypedEnvelope<proto::Follow>,
7760 mut cx: AsyncApp,
7761 ) -> Result<proto::FollowResponse> {
7762 this.update(&mut cx, |this, cx| {
7763 let follower = Follower {
7764 project_id: envelope.payload.project_id,
7765 peer_id: envelope.original_sender_id()?,
7766 };
7767
7768 let mut response = proto::FollowResponse::default();
7769 this.workspaces.retain(|workspace| {
7770 workspace
7771 .update(cx, |workspace, window, cx| {
7772 let handler_response =
7773 workspace.handle_follow(follower.project_id, window, cx);
7774 if let Some(active_view) = handler_response.active_view
7775 && workspace.project.read(cx).remote_id() == follower.project_id
7776 {
7777 response.active_view = Some(active_view)
7778 }
7779 })
7780 .is_ok()
7781 });
7782
7783 Ok(response)
7784 })
7785 }
7786
7787 async fn handle_update_followers(
7788 this: Entity<Self>,
7789 envelope: TypedEnvelope<proto::UpdateFollowers>,
7790 mut cx: AsyncApp,
7791 ) -> Result<()> {
7792 let leader_id = envelope.original_sender_id()?;
7793 let update = envelope.payload;
7794
7795 this.update(&mut cx, |this, cx| {
7796 this.workspaces.retain(|workspace| {
7797 workspace
7798 .update(cx, |workspace, window, cx| {
7799 let project_id = workspace.project.read(cx).remote_id();
7800 if update.project_id != project_id && update.project_id.is_some() {
7801 return;
7802 }
7803 workspace.handle_update_followers(leader_id, update.clone(), window, cx);
7804 })
7805 .is_ok()
7806 });
7807 Ok(())
7808 })
7809 }
7810
7811 pub fn workspaces(&self) -> &HashSet<WindowHandle<Workspace>> {
7812 &self.workspaces
7813 }
7814}
7815
7816impl ViewId {
7817 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
7818 Ok(Self {
7819 creator: message
7820 .creator
7821 .map(CollaboratorId::PeerId)
7822 .context("creator is missing")?,
7823 id: message.id,
7824 })
7825 }
7826
7827 pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
7828 if let CollaboratorId::PeerId(peer_id) = self.creator {
7829 Some(proto::ViewId {
7830 creator: Some(peer_id),
7831 id: self.id,
7832 })
7833 } else {
7834 None
7835 }
7836 }
7837}
7838
7839impl FollowerState {
7840 fn pane(&self) -> &Entity<Pane> {
7841 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
7842 }
7843}
7844
7845pub trait WorkspaceHandle {
7846 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
7847}
7848
7849impl WorkspaceHandle for Entity<Workspace> {
7850 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
7851 self.read(cx)
7852 .worktrees(cx)
7853 .flat_map(|worktree| {
7854 let worktree_id = worktree.read(cx).id();
7855 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
7856 worktree_id,
7857 path: f.path.clone(),
7858 })
7859 })
7860 .collect::<Vec<_>>()
7861 }
7862}
7863
7864pub async fn last_opened_workspace_location()
7865-> Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)> {
7866 DB.last_workspace().await.log_err().flatten()
7867}
7868
7869pub fn last_session_workspace_locations(
7870 last_session_id: &str,
7871 last_session_window_stack: Option<Vec<WindowId>>,
7872) -> Option<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
7873 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
7874 .log_err()
7875}
7876
7877actions!(
7878 collab,
7879 [
7880 /// Opens the channel notes for the current call.
7881 ///
7882 /// Use `collab_panel::OpenSelectedChannelNotes` to open the channel notes for the selected
7883 /// channel in the collab panel.
7884 ///
7885 /// If you want to open a specific channel, use `zed::OpenZedUrl` with a channel notes URL -
7886 /// can be copied via "Copy link to section" in the context menu of the channel notes
7887 /// buffer. These URLs look like `https://zed.dev/channel/channel-name-CHANNEL_ID/notes`.
7888 OpenChannelNotes,
7889 /// Mutes your microphone.
7890 Mute,
7891 /// Deafens yourself (mute both microphone and speakers).
7892 Deafen,
7893 /// Leaves the current call.
7894 LeaveCall,
7895 /// Shares the current project with collaborators.
7896 ShareProject,
7897 /// Shares your screen with collaborators.
7898 ScreenShare,
7899 /// Copies the current room name and session id for debugging purposes.
7900 CopyRoomId,
7901 ]
7902);
7903actions!(
7904 zed,
7905 [
7906 /// Opens the Zed log file.
7907 OpenLog,
7908 /// Reveals the Zed log file in the system file manager.
7909 RevealLogInFileManager
7910 ]
7911);
7912
7913async fn join_channel_internal(
7914 channel_id: ChannelId,
7915 app_state: &Arc<AppState>,
7916 requesting_window: Option<WindowHandle<Workspace>>,
7917 active_call: &Entity<ActiveCall>,
7918 cx: &mut AsyncApp,
7919) -> Result<bool> {
7920 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
7921 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
7922 return (false, None);
7923 };
7924
7925 let already_in_channel = room.channel_id() == Some(channel_id);
7926 let should_prompt = room.is_sharing_project()
7927 && !room.remote_participants().is_empty()
7928 && !already_in_channel;
7929 let open_room = if already_in_channel {
7930 active_call.room().cloned()
7931 } else {
7932 None
7933 };
7934 (should_prompt, open_room)
7935 });
7936
7937 if let Some(room) = open_room {
7938 let task = room.update(cx, |room, cx| {
7939 if let Some((project, host)) = room.most_active_project(cx) {
7940 return Some(join_in_room_project(project, host, app_state.clone(), cx));
7941 }
7942
7943 None
7944 });
7945 if let Some(task) = task {
7946 task.await?;
7947 }
7948 return anyhow::Ok(true);
7949 }
7950
7951 if should_prompt {
7952 if let Some(workspace) = requesting_window {
7953 let answer = workspace
7954 .update(cx, |_, window, cx| {
7955 window.prompt(
7956 PromptLevel::Warning,
7957 "Do you want to switch channels?",
7958 Some("Leaving this call will unshare your current project."),
7959 &["Yes, Join Channel", "Cancel"],
7960 cx,
7961 )
7962 })?
7963 .await;
7964
7965 if answer == Ok(1) {
7966 return Ok(false);
7967 }
7968 } else {
7969 return Ok(false); // unreachable!() hopefully
7970 }
7971 }
7972
7973 let client = cx.update(|cx| active_call.read(cx).client());
7974
7975 let mut client_status = client.status();
7976
7977 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
7978 'outer: loop {
7979 let Some(status) = client_status.recv().await else {
7980 anyhow::bail!("error connecting");
7981 };
7982
7983 match status {
7984 Status::Connecting
7985 | Status::Authenticating
7986 | Status::Authenticated
7987 | Status::Reconnecting
7988 | Status::Reauthenticating
7989 | Status::Reauthenticated => continue,
7990 Status::Connected { .. } => break 'outer,
7991 Status::SignedOut | Status::AuthenticationError => {
7992 return Err(ErrorCode::SignedOut.into());
7993 }
7994 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
7995 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
7996 return Err(ErrorCode::Disconnected.into());
7997 }
7998 }
7999 }
8000
8001 let room = active_call
8002 .update(cx, |active_call, cx| {
8003 active_call.join_channel(channel_id, cx)
8004 })
8005 .await?;
8006
8007 let Some(room) = room else {
8008 return anyhow::Ok(true);
8009 };
8010
8011 room.update(cx, |room, _| room.room_update_completed())
8012 .await;
8013
8014 let task = room.update(cx, |room, cx| {
8015 if let Some((project, host)) = room.most_active_project(cx) {
8016 return Some(join_in_room_project(project, host, app_state.clone(), cx));
8017 }
8018
8019 // If you are the first to join a channel, see if you should share your project.
8020 if room.remote_participants().is_empty()
8021 && !room.local_participant_is_guest()
8022 && let Some(workspace) = requesting_window
8023 {
8024 let project = workspace.update(cx, |workspace, _, cx| {
8025 let project = workspace.project.read(cx);
8026
8027 if !CallSettings::get_global(cx).share_on_join {
8028 return None;
8029 }
8030
8031 if (project.is_local() || project.is_via_remote_server())
8032 && project.visible_worktrees(cx).any(|tree| {
8033 tree.read(cx)
8034 .root_entry()
8035 .is_some_and(|entry| entry.is_dir())
8036 })
8037 {
8038 Some(workspace.project.clone())
8039 } else {
8040 None
8041 }
8042 });
8043 if let Ok(Some(project)) = project {
8044 return Some(cx.spawn(async move |room, cx| {
8045 room.update(cx, |room, cx| room.share_project(project, cx))?
8046 .await?;
8047 Ok(())
8048 }));
8049 }
8050 }
8051
8052 None
8053 });
8054 if let Some(task) = task {
8055 task.await?;
8056 return anyhow::Ok(true);
8057 }
8058 anyhow::Ok(false)
8059}
8060
8061pub fn join_channel(
8062 channel_id: ChannelId,
8063 app_state: Arc<AppState>,
8064 requesting_window: Option<WindowHandle<Workspace>>,
8065 cx: &mut App,
8066) -> Task<Result<()>> {
8067 let active_call = ActiveCall::global(cx);
8068 cx.spawn(async move |cx| {
8069 let result =
8070 join_channel_internal(channel_id, &app_state, requesting_window, &active_call, cx)
8071 .await;
8072
8073 // join channel succeeded, and opened a window
8074 if matches!(result, Ok(true)) {
8075 return anyhow::Ok(());
8076 }
8077
8078 // find an existing workspace to focus and show call controls
8079 let mut active_window = requesting_window.or_else(|| activate_any_workspace_window(cx));
8080 if active_window.is_none() {
8081 // no open workspaces, make one to show the error in (blergh)
8082 let (window_handle, _) = cx
8083 .update(|cx| {
8084 Workspace::new_local(
8085 vec![],
8086 app_state.clone(),
8087 requesting_window,
8088 None,
8089 None,
8090 cx,
8091 )
8092 })
8093 .await?;
8094
8095 if result.is_ok() {
8096 cx.update(|cx| {
8097 cx.dispatch_action(&OpenChannelNotes);
8098 });
8099 }
8100
8101 active_window = Some(window_handle);
8102 }
8103
8104 if let Err(err) = result {
8105 log::error!("failed to join channel: {}", err);
8106 if let Some(active_window) = active_window {
8107 active_window
8108 .update(cx, |_, window, cx| {
8109 let detail: SharedString = match err.error_code() {
8110 ErrorCode::SignedOut => "Please sign in to continue.".into(),
8111 ErrorCode::UpgradeRequired => concat!(
8112 "Your are running an unsupported version of Zed. ",
8113 "Please update to continue."
8114 )
8115 .into(),
8116 ErrorCode::NoSuchChannel => concat!(
8117 "No matching channel was found. ",
8118 "Please check the link and try again."
8119 )
8120 .into(),
8121 ErrorCode::Forbidden => concat!(
8122 "This channel is private, and you do not have access. ",
8123 "Please ask someone to add you and try again."
8124 )
8125 .into(),
8126 ErrorCode::Disconnected => {
8127 "Please check your internet connection and try again.".into()
8128 }
8129 _ => format!("{}\n\nPlease try again.", err).into(),
8130 };
8131 window.prompt(
8132 PromptLevel::Critical,
8133 "Failed to join channel",
8134 Some(&detail),
8135 &["Ok"],
8136 cx,
8137 )
8138 })?
8139 .await
8140 .ok();
8141 }
8142 }
8143
8144 // return ok, we showed the error to the user.
8145 anyhow::Ok(())
8146 })
8147}
8148
8149pub async fn get_any_active_workspace(
8150 app_state: Arc<AppState>,
8151 mut cx: AsyncApp,
8152) -> anyhow::Result<WindowHandle<Workspace>> {
8153 // find an existing workspace to focus and show call controls
8154 let active_window = activate_any_workspace_window(&mut cx);
8155 if active_window.is_none() {
8156 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, None, cx))
8157 .await?;
8158 }
8159 activate_any_workspace_window(&mut cx).context("could not open zed")
8160}
8161
8162fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
8163 cx.update(|cx| {
8164 if let Some(workspace_window) = cx
8165 .active_window()
8166 .and_then(|window| window.downcast::<Workspace>())
8167 {
8168 return Some(workspace_window);
8169 }
8170
8171 for window in cx.windows() {
8172 if let Some(workspace_window) = window.downcast::<Workspace>() {
8173 workspace_window
8174 .update(cx, |_, window, _| window.activate_window())
8175 .ok();
8176 return Some(workspace_window);
8177 }
8178 }
8179 None
8180 })
8181}
8182
8183pub fn workspace_windows_for_location(
8184 serialized_location: &SerializedWorkspaceLocation,
8185 cx: &App,
8186) -> Vec<WindowHandle<Workspace>> {
8187 cx.windows()
8188 .into_iter()
8189 .filter_map(|window| window.downcast::<Workspace>())
8190 .filter(|workspace| {
8191 let same_host = |left: &RemoteConnectionOptions, right: &RemoteConnectionOptions| match (left, right) {
8192 (RemoteConnectionOptions::Ssh(a), RemoteConnectionOptions::Ssh(b)) => {
8193 (&a.host, &a.username, &a.port) == (&b.host, &b.username, &b.port)
8194 }
8195 (RemoteConnectionOptions::Wsl(a), RemoteConnectionOptions::Wsl(b)) => {
8196 // The WSL username is not consistently populated in the workspace location, so ignore it for now.
8197 a.distro_name == b.distro_name
8198 }
8199 (RemoteConnectionOptions::Docker(a), RemoteConnectionOptions::Docker(b)) => {
8200 a.container_id == b.container_id
8201 }
8202 #[cfg(any(test, feature = "test-support"))]
8203 (RemoteConnectionOptions::Mock(a), RemoteConnectionOptions::Mock(b)) => {
8204 a.id == b.id
8205 }
8206 _ => false,
8207 };
8208
8209 workspace
8210 .read(cx)
8211 .is_ok_and(|workspace| match workspace.workspace_location(cx) {
8212 WorkspaceLocation::Location(location, _) => {
8213 match (&location, serialized_location) {
8214 (
8215 SerializedWorkspaceLocation::Local,
8216 SerializedWorkspaceLocation::Local,
8217 ) => true,
8218 (
8219 SerializedWorkspaceLocation::Remote(a),
8220 SerializedWorkspaceLocation::Remote(b),
8221 ) => same_host(a, b),
8222 _ => false,
8223 }
8224 }
8225 _ => false,
8226 })
8227 })
8228 .collect()
8229}
8230
8231#[derive(Default, Clone)]
8232pub struct OpenOptions {
8233 pub visible: Option<OpenVisible>,
8234 pub focus: Option<bool>,
8235 pub open_new_workspace: Option<bool>,
8236 pub wait: bool,
8237 pub replace_window: Option<WindowHandle<Workspace>>,
8238 pub env: Option<HashMap<String, String>>,
8239}
8240
8241/// Opens a workspace by its database ID, used for restoring empty workspaces with unsaved content.
8242pub fn open_workspace_by_id(
8243 workspace_id: WorkspaceId,
8244 app_state: Arc<AppState>,
8245 cx: &mut App,
8246) -> Task<anyhow::Result<WindowHandle<Workspace>>> {
8247 let project_handle = Project::local(
8248 app_state.client.clone(),
8249 app_state.node_runtime.clone(),
8250 app_state.user_store.clone(),
8251 app_state.languages.clone(),
8252 app_state.fs.clone(),
8253 None,
8254 project::LocalProjectFlags {
8255 init_worktree_trust: true,
8256 ..project::LocalProjectFlags::default()
8257 },
8258 cx,
8259 );
8260
8261 cx.spawn(async move |cx| {
8262 let serialized_workspace = persistence::DB
8263 .workspace_for_id(workspace_id)
8264 .with_context(|| format!("Workspace {workspace_id:?} not found"))?;
8265
8266 let window_bounds_override = window_bounds_env_override();
8267
8268 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
8269 (Some(WindowBounds::Windowed(bounds)), None)
8270 } else if let Some(display) = serialized_workspace.display
8271 && let Some(bounds) = serialized_workspace.window_bounds.as_ref()
8272 {
8273 (Some(bounds.0), Some(display))
8274 } else if let Some((display, bounds)) = persistence::read_default_window_bounds() {
8275 (Some(bounds), Some(display))
8276 } else {
8277 (None, None)
8278 };
8279
8280 let options = cx.update(|cx| {
8281 let mut options = (app_state.build_window_options)(display, cx);
8282 options.window_bounds = window_bounds;
8283 options
8284 });
8285 let centered_layout = serialized_workspace.centered_layout;
8286
8287 let window = cx.open_window(options, {
8288 let app_state = app_state.clone();
8289 let project_handle = project_handle.clone();
8290 move |window, cx| {
8291 cx.new(|cx| {
8292 let mut workspace =
8293 Workspace::new(Some(workspace_id), project_handle, app_state, window, cx);
8294 workspace.centered_layout = centered_layout;
8295 workspace
8296 })
8297 }
8298 })?;
8299
8300 notify_if_database_failed(window, cx);
8301
8302 // Restore items from the serialized workspace
8303 window
8304 .update(cx, |_workspace, window, cx| {
8305 open_items(Some(serialized_workspace), vec![], window, cx)
8306 })?
8307 .await?;
8308
8309 window.update(cx, |workspace, window, cx| {
8310 window.activate_window();
8311 workspace.serialize_workspace(window, cx);
8312 })?;
8313
8314 Ok(window)
8315 })
8316}
8317
8318pub async fn find_existing_workspace(
8319 abs_paths: &[PathBuf],
8320 open_options: &OpenOptions,
8321 location: &SerializedWorkspaceLocation,
8322 cx: &mut AsyncApp,
8323) -> (Option<WindowHandle<Workspace>>, OpenVisible) {
8324 let mut existing = None;
8325 let mut open_visible = OpenVisible::All;
8326 let mut best_match = None;
8327
8328 if open_options.open_new_workspace != Some(true) {
8329 cx.update(|cx| {
8330 for window in workspace_windows_for_location(location, cx) {
8331 if let Ok(workspace) = window.read(cx) {
8332 let project = workspace.project.read(cx);
8333 let m = project.visibility_for_paths(
8334 abs_paths,
8335 open_options.open_new_workspace == None,
8336 cx,
8337 );
8338 if m > best_match {
8339 existing = Some(window);
8340 best_match = m;
8341 } else if best_match.is_none() && open_options.open_new_workspace == Some(false)
8342 {
8343 existing = Some(window)
8344 }
8345 }
8346 }
8347 });
8348
8349 let all_paths_are_files = existing
8350 .and_then(|workspace| {
8351 cx.update(|cx| {
8352 workspace
8353 .read(cx)
8354 .map(|workspace| {
8355 let project = workspace.project.read(cx);
8356 let path_style = workspace.path_style(cx);
8357 !abs_paths.iter().any(|path| {
8358 let path = util::paths::SanitizedPath::new(path);
8359 project.worktrees(cx).any(|worktree| {
8360 let worktree = worktree.read(cx);
8361 let abs_path = worktree.abs_path();
8362 path_style
8363 .strip_prefix(path.as_ref(), abs_path.as_ref())
8364 .and_then(|rel| worktree.entry_for_path(&rel))
8365 .is_some_and(|e| e.is_dir())
8366 })
8367 })
8368 })
8369 .ok()
8370 })
8371 })
8372 .unwrap_or(false);
8373
8374 if open_options.open_new_workspace.is_none()
8375 && existing.is_some()
8376 && open_options.wait
8377 && all_paths_are_files
8378 {
8379 cx.update(|cx| {
8380 let windows = workspace_windows_for_location(location, cx);
8381 let window = cx
8382 .active_window()
8383 .and_then(|window| window.downcast::<Workspace>())
8384 .filter(|window| windows.contains(window))
8385 .or_else(|| windows.into_iter().next());
8386 if let Some(window) = window {
8387 existing = Some(window);
8388 open_visible = OpenVisible::None;
8389 }
8390 });
8391 }
8392 }
8393 return (existing, open_visible);
8394}
8395
8396#[allow(clippy::type_complexity)]
8397pub fn open_paths(
8398 abs_paths: &[PathBuf],
8399 app_state: Arc<AppState>,
8400 open_options: OpenOptions,
8401 cx: &mut App,
8402) -> Task<
8403 anyhow::Result<(
8404 WindowHandle<Workspace>,
8405 Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
8406 )>,
8407> {
8408 let abs_paths = abs_paths.to_vec();
8409 #[cfg(target_os = "windows")]
8410 let wsl_path = abs_paths
8411 .iter()
8412 .find_map(|p| util::paths::WslPath::from_path(p));
8413
8414 cx.spawn(async move |cx| {
8415 let (mut existing, mut open_visible) = find_existing_workspace(&abs_paths, &open_options, &SerializedWorkspaceLocation::Local, cx).await;
8416
8417 // Fallback: if no workspace contains the paths and all paths are files,
8418 // prefer an existing local workspace window (active window first).
8419 if open_options.open_new_workspace.is_none() && existing.is_none() {
8420 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
8421 let all_metadatas = futures::future::join_all(all_paths)
8422 .await
8423 .into_iter()
8424 .filter_map(|result| result.ok().flatten())
8425 .collect::<Vec<_>>();
8426
8427 if all_metadatas.iter().all(|file| !file.is_dir) {
8428 cx.update(|cx| {
8429 let windows = workspace_windows_for_location(&SerializedWorkspaceLocation::Local, cx);
8430 let window = cx
8431 .active_window()
8432 .and_then(|window| window.downcast::<Workspace>())
8433 .filter(|window| windows.contains(window))
8434 .or_else(|| windows.into_iter().next());
8435 if let Some(window) = window {
8436 existing = Some(window);
8437 open_visible = OpenVisible::None;
8438 }
8439 });
8440 }
8441 }
8442
8443 let result = if let Some(existing) = existing {
8444 let open_task = existing
8445 .update(cx, |workspace, window, cx| {
8446 window.activate_window();
8447 workspace.open_paths(
8448 abs_paths,
8449 OpenOptions {
8450 visible: Some(open_visible),
8451 ..Default::default()
8452 },
8453 None,
8454 window,
8455 cx,
8456 )
8457 })?
8458 .await;
8459
8460 _ = existing.update(cx, |workspace, _, cx| {
8461 for item in open_task.iter().flatten() {
8462 if let Err(e) = item {
8463 workspace.show_error(&e, cx);
8464 }
8465 }
8466 });
8467
8468 Ok((existing, open_task))
8469 } else {
8470 cx.update(move |cx| {
8471 Workspace::new_local(
8472 abs_paths,
8473 app_state.clone(),
8474 open_options.replace_window,
8475 open_options.env,
8476 None,
8477 cx,
8478 )
8479 })
8480 .await
8481 };
8482
8483 #[cfg(target_os = "windows")]
8484 if let Some(util::paths::WslPath{distro, path}) = wsl_path
8485 && let Ok((workspace, _)) = &result
8486 {
8487 workspace
8488 .update(cx, move |workspace, _window, cx| {
8489 struct OpenInWsl;
8490 workspace.show_notification(NotificationId::unique::<OpenInWsl>(), cx, move |cx| {
8491 let display_path = util::markdown::MarkdownInlineCode(&path.to_string_lossy());
8492 let msg = format!("{display_path} is inside a WSL filesystem, some features may not work unless you open it with WSL remote");
8493 cx.new(move |cx| {
8494 MessageNotification::new(msg, cx)
8495 .primary_message("Open in WSL")
8496 .primary_icon(IconName::FolderOpen)
8497 .primary_on_click(move |window, cx| {
8498 window.dispatch_action(Box::new(remote::OpenWslPath {
8499 distro: remote::WslConnectionOptions {
8500 distro_name: distro.clone(),
8501 user: None,
8502 },
8503 paths: vec![path.clone().into()],
8504 }), cx)
8505 })
8506 })
8507 });
8508 })
8509 .unwrap();
8510 };
8511 result
8512 })
8513}
8514
8515pub fn open_new(
8516 open_options: OpenOptions,
8517 app_state: Arc<AppState>,
8518 cx: &mut App,
8519 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
8520) -> Task<anyhow::Result<()>> {
8521 let task = Workspace::new_local(
8522 Vec::new(),
8523 app_state,
8524 open_options.replace_window,
8525 open_options.env,
8526 Some(Box::new(init)),
8527 cx,
8528 );
8529 cx.spawn(async move |_cx| {
8530 let (_workspace, _opened_paths) = task.await?;
8531 // Init callback is called synchronously during workspace creation
8532 Ok(())
8533 })
8534}
8535
8536pub fn create_and_open_local_file(
8537 path: &'static Path,
8538 window: &mut Window,
8539 cx: &mut Context<Workspace>,
8540 default_content: impl 'static + Send + FnOnce() -> Rope,
8541) -> Task<Result<Box<dyn ItemHandle>>> {
8542 cx.spawn_in(window, async move |workspace, cx| {
8543 let fs = workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
8544 if !fs.is_file(path).await {
8545 fs.create_file(path, Default::default()).await?;
8546 fs.save(path, &default_content(), Default::default())
8547 .await?;
8548 }
8549
8550 workspace
8551 .update_in(cx, |workspace, window, cx| {
8552 workspace.with_local_or_wsl_workspace(window, cx, |workspace, window, cx| {
8553 let path = workspace
8554 .project
8555 .read_with(cx, |project, cx| project.try_windows_path_to_wsl(path, cx));
8556 cx.spawn_in(window, async move |workspace, cx| {
8557 let path = path.await?;
8558 let mut items = workspace
8559 .update_in(cx, |workspace, window, cx| {
8560 workspace.open_paths(
8561 vec![path.to_path_buf()],
8562 OpenOptions {
8563 visible: Some(OpenVisible::None),
8564 ..Default::default()
8565 },
8566 None,
8567 window,
8568 cx,
8569 )
8570 })?
8571 .await;
8572 let item = items.pop().flatten();
8573 item.with_context(|| format!("path {path:?} is not a file"))?
8574 })
8575 })
8576 })?
8577 .await?
8578 .await
8579 })
8580}
8581
8582pub fn open_remote_project_with_new_connection(
8583 window: WindowHandle<Workspace>,
8584 remote_connection: Arc<dyn RemoteConnection>,
8585 cancel_rx: oneshot::Receiver<()>,
8586 delegate: Arc<dyn RemoteClientDelegate>,
8587 app_state: Arc<AppState>,
8588 paths: Vec<PathBuf>,
8589 cx: &mut App,
8590) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
8591 cx.spawn(async move |cx| {
8592 let (workspace_id, serialized_workspace) =
8593 deserialize_remote_project(remote_connection.connection_options(), paths.clone(), cx)
8594 .await?;
8595
8596 let session = match cx
8597 .update(|cx| {
8598 remote::RemoteClient::new(
8599 ConnectionIdentifier::Workspace(workspace_id.0),
8600 remote_connection,
8601 cancel_rx,
8602 delegate,
8603 cx,
8604 )
8605 })
8606 .await?
8607 {
8608 Some(result) => result,
8609 None => return Ok(Vec::new()),
8610 };
8611
8612 let project = cx.update(|cx| {
8613 project::Project::remote(
8614 session,
8615 app_state.client.clone(),
8616 app_state.node_runtime.clone(),
8617 app_state.user_store.clone(),
8618 app_state.languages.clone(),
8619 app_state.fs.clone(),
8620 true,
8621 cx,
8622 )
8623 });
8624
8625 open_remote_project_inner(
8626 project,
8627 paths,
8628 workspace_id,
8629 serialized_workspace,
8630 app_state,
8631 window,
8632 cx,
8633 )
8634 .await
8635 })
8636}
8637
8638pub fn open_remote_project_with_existing_connection(
8639 connection_options: RemoteConnectionOptions,
8640 project: Entity<Project>,
8641 paths: Vec<PathBuf>,
8642 app_state: Arc<AppState>,
8643 window: WindowHandle<Workspace>,
8644 cx: &mut AsyncApp,
8645) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
8646 cx.spawn(async move |cx| {
8647 let (workspace_id, serialized_workspace) =
8648 deserialize_remote_project(connection_options.clone(), paths.clone(), cx).await?;
8649
8650 open_remote_project_inner(
8651 project,
8652 paths,
8653 workspace_id,
8654 serialized_workspace,
8655 app_state,
8656 window,
8657 cx,
8658 )
8659 .await
8660 })
8661}
8662
8663async fn open_remote_project_inner(
8664 project: Entity<Project>,
8665 paths: Vec<PathBuf>,
8666 workspace_id: WorkspaceId,
8667 serialized_workspace: Option<SerializedWorkspace>,
8668 app_state: Arc<AppState>,
8669 window: WindowHandle<Workspace>,
8670 cx: &mut AsyncApp,
8671) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
8672 let toolchains = DB.toolchains(workspace_id).await?;
8673 for (toolchain, worktree_path, path) in toolchains {
8674 project
8675 .update(cx, |this, cx| {
8676 let Some(worktree_id) =
8677 this.find_worktree(&worktree_path, cx)
8678 .and_then(|(worktree, rel_path)| {
8679 if rel_path.is_empty() {
8680 Some(worktree.read(cx).id())
8681 } else {
8682 None
8683 }
8684 })
8685 else {
8686 return Task::ready(None);
8687 };
8688
8689 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
8690 })
8691 .await;
8692 }
8693 let mut project_paths_to_open = vec![];
8694 let mut project_path_errors = vec![];
8695
8696 for path in paths {
8697 let result = cx
8698 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))
8699 .await;
8700 match result {
8701 Ok((_, project_path)) => {
8702 project_paths_to_open.push((path.clone(), Some(project_path)));
8703 }
8704 Err(error) => {
8705 project_path_errors.push(error);
8706 }
8707 };
8708 }
8709
8710 if project_paths_to_open.is_empty() {
8711 return Err(project_path_errors.pop().context("no paths given")?);
8712 }
8713
8714 if let Some(detach_session_task) = window
8715 .update(cx, |_workspace, window, cx| {
8716 cx.spawn_in(window, async move |this, cx| {
8717 this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx))
8718 })
8719 })
8720 .ok()
8721 {
8722 detach_session_task.await.ok();
8723 }
8724
8725 cx.update_window(window.into(), |_, window, cx| {
8726 window.replace_root(cx, |window, cx| {
8727 telemetry::event!("SSH Project Opened");
8728
8729 let mut workspace =
8730 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
8731 workspace.update_history(cx);
8732
8733 if let Some(ref serialized) = serialized_workspace {
8734 workspace.centered_layout = serialized.centered_layout;
8735 }
8736
8737 workspace
8738 });
8739 })?;
8740
8741 let items = window
8742 .update(cx, |_, window, cx| {
8743 window.activate_window();
8744 open_items(serialized_workspace, project_paths_to_open, window, cx)
8745 })?
8746 .await?;
8747
8748 window.update(cx, |workspace, _, cx| {
8749 for error in project_path_errors {
8750 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
8751 if let Some(path) = error.error_tag("path") {
8752 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
8753 }
8754 } else {
8755 workspace.show_error(&error, cx)
8756 }
8757 }
8758 })?;
8759
8760 Ok(items.into_iter().map(|item| item?.ok()).collect())
8761}
8762
8763fn deserialize_remote_project(
8764 connection_options: RemoteConnectionOptions,
8765 paths: Vec<PathBuf>,
8766 cx: &AsyncApp,
8767) -> Task<Result<(WorkspaceId, Option<SerializedWorkspace>)>> {
8768 cx.background_spawn(async move {
8769 let remote_connection_id = persistence::DB
8770 .get_or_create_remote_connection(connection_options)
8771 .await?;
8772
8773 let serialized_workspace =
8774 persistence::DB.remote_workspace_for_roots(&paths, remote_connection_id);
8775
8776 let workspace_id = if let Some(workspace_id) =
8777 serialized_workspace.as_ref().map(|workspace| workspace.id)
8778 {
8779 workspace_id
8780 } else {
8781 persistence::DB.next_id().await?
8782 };
8783
8784 Ok((workspace_id, serialized_workspace))
8785 })
8786}
8787
8788pub fn join_in_room_project(
8789 project_id: u64,
8790 follow_user_id: u64,
8791 app_state: Arc<AppState>,
8792 cx: &mut App,
8793) -> Task<Result<()>> {
8794 let windows = cx.windows();
8795 cx.spawn(async move |cx| {
8796 let existing_workspace = windows.into_iter().find_map(|window_handle| {
8797 window_handle
8798 .downcast::<Workspace>()
8799 .and_then(|window_handle| {
8800 window_handle
8801 .update(cx, |workspace, _window, cx| {
8802 if workspace.project().read(cx).remote_id() == Some(project_id) {
8803 Some(window_handle)
8804 } else {
8805 None
8806 }
8807 })
8808 .unwrap_or(None)
8809 })
8810 });
8811
8812 let workspace = if let Some(existing_workspace) = existing_workspace {
8813 existing_workspace
8814 } else {
8815 let active_call = cx.update(|cx| ActiveCall::global(cx));
8816 let room = active_call
8817 .read_with(cx, |call, _| call.room().cloned())
8818 .context("not in a call")?;
8819 let project = room
8820 .update(cx, |room, cx| {
8821 room.join_project(
8822 project_id,
8823 app_state.languages.clone(),
8824 app_state.fs.clone(),
8825 cx,
8826 )
8827 })
8828 .await?;
8829
8830 let window_bounds_override = window_bounds_env_override();
8831 cx.update(|cx| {
8832 let mut options = (app_state.build_window_options)(None, cx);
8833 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
8834 cx.open_window(options, |window, cx| {
8835 cx.new(|cx| {
8836 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
8837 })
8838 })
8839 })?
8840 };
8841
8842 workspace.update(cx, |workspace, window, cx| {
8843 cx.activate(true);
8844 window.activate_window();
8845
8846 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
8847 let follow_peer_id = room
8848 .read(cx)
8849 .remote_participants()
8850 .iter()
8851 .find(|(_, participant)| participant.user.id == follow_user_id)
8852 .map(|(_, p)| p.peer_id)
8853 .or_else(|| {
8854 // If we couldn't follow the given user, follow the host instead.
8855 let collaborator = workspace
8856 .project()
8857 .read(cx)
8858 .collaborators()
8859 .values()
8860 .find(|collaborator| collaborator.is_host)?;
8861 Some(collaborator.peer_id)
8862 });
8863
8864 if let Some(follow_peer_id) = follow_peer_id {
8865 workspace.follow(follow_peer_id, window, cx);
8866 }
8867 }
8868 })?;
8869
8870 anyhow::Ok(())
8871 })
8872}
8873
8874pub fn reload(cx: &mut App) {
8875 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
8876 let mut workspace_windows = cx
8877 .windows()
8878 .into_iter()
8879 .filter_map(|window| window.downcast::<Workspace>())
8880 .collect::<Vec<_>>();
8881
8882 // If multiple windows have unsaved changes, and need a save prompt,
8883 // prompt in the active window before switching to a different window.
8884 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
8885
8886 let mut prompt = None;
8887 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
8888 prompt = window
8889 .update(cx, |_, window, cx| {
8890 window.prompt(
8891 PromptLevel::Info,
8892 "Are you sure you want to restart?",
8893 None,
8894 &["Restart", "Cancel"],
8895 cx,
8896 )
8897 })
8898 .ok();
8899 }
8900
8901 cx.spawn(async move |cx| {
8902 if let Some(prompt) = prompt {
8903 let answer = prompt.await?;
8904 if answer != 0 {
8905 return anyhow::Ok(());
8906 }
8907 }
8908
8909 // If the user cancels any save prompt, then keep the app open.
8910 for window in workspace_windows {
8911 if let Ok(should_close) = window.update(cx, |workspace, window, cx| {
8912 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
8913 }) && !should_close.await?
8914 {
8915 return anyhow::Ok(());
8916 }
8917 }
8918 cx.update(|cx| cx.restart());
8919 anyhow::Ok(())
8920 })
8921 .detach_and_log_err(cx);
8922}
8923
8924fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
8925 let mut parts = value.split(',');
8926 let x: usize = parts.next()?.parse().ok()?;
8927 let y: usize = parts.next()?.parse().ok()?;
8928 Some(point(px(x as f32), px(y as f32)))
8929}
8930
8931fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
8932 let mut parts = value.split(',');
8933 let width: usize = parts.next()?.parse().ok()?;
8934 let height: usize = parts.next()?.parse().ok()?;
8935 Some(size(px(width as f32), px(height as f32)))
8936}
8937
8938/// Add client-side decorations (rounded corners, shadows, resize handling) when appropriate.
8939pub fn client_side_decorations(
8940 element: impl IntoElement,
8941 window: &mut Window,
8942 cx: &mut App,
8943) -> Stateful<Div> {
8944 const BORDER_SIZE: Pixels = px(1.0);
8945 let decorations = window.window_decorations();
8946
8947 match decorations {
8948 Decorations::Client { .. } => window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW),
8949 Decorations::Server => window.set_client_inset(px(0.0)),
8950 }
8951
8952 struct GlobalResizeEdge(ResizeEdge);
8953 impl Global for GlobalResizeEdge {}
8954
8955 div()
8956 .id("window-backdrop")
8957 .bg(transparent_black())
8958 .map(|div| match decorations {
8959 Decorations::Server => div,
8960 Decorations::Client { tiling, .. } => div
8961 .when(!(tiling.top || tiling.right), |div| {
8962 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
8963 })
8964 .when(!(tiling.top || tiling.left), |div| {
8965 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
8966 })
8967 .when(!(tiling.bottom || tiling.right), |div| {
8968 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
8969 })
8970 .when(!(tiling.bottom || tiling.left), |div| {
8971 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
8972 })
8973 .when(!tiling.top, |div| {
8974 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
8975 })
8976 .when(!tiling.bottom, |div| {
8977 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
8978 })
8979 .when(!tiling.left, |div| {
8980 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
8981 })
8982 .when(!tiling.right, |div| {
8983 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
8984 })
8985 .on_mouse_move(move |e, window, cx| {
8986 let size = window.window_bounds().get_bounds().size;
8987 let pos = e.position;
8988
8989 let new_edge =
8990 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
8991
8992 let edge = cx.try_global::<GlobalResizeEdge>();
8993 if new_edge != edge.map(|edge| edge.0) {
8994 window
8995 .window_handle()
8996 .update(cx, |workspace, _, cx| {
8997 cx.notify(workspace.entity_id());
8998 })
8999 .ok();
9000 }
9001 })
9002 .on_mouse_down(MouseButton::Left, move |e, window, _| {
9003 let size = window.window_bounds().get_bounds().size;
9004 let pos = e.position;
9005
9006 let edge = match resize_edge(
9007 pos,
9008 theme::CLIENT_SIDE_DECORATION_SHADOW,
9009 size,
9010 tiling,
9011 ) {
9012 Some(value) => value,
9013 None => return,
9014 };
9015
9016 window.start_window_resize(edge);
9017 }),
9018 })
9019 .size_full()
9020 .child(
9021 div()
9022 .cursor(CursorStyle::Arrow)
9023 .map(|div| match decorations {
9024 Decorations::Server => div,
9025 Decorations::Client { tiling } => div
9026 .border_color(cx.theme().colors().border)
9027 .when(!(tiling.top || tiling.right), |div| {
9028 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
9029 })
9030 .when(!(tiling.top || tiling.left), |div| {
9031 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
9032 })
9033 .when(!(tiling.bottom || tiling.right), |div| {
9034 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
9035 })
9036 .when(!(tiling.bottom || tiling.left), |div| {
9037 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
9038 })
9039 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
9040 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
9041 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
9042 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
9043 .when(!tiling.is_tiled(), |div| {
9044 div.shadow(vec![gpui::BoxShadow {
9045 color: Hsla {
9046 h: 0.,
9047 s: 0.,
9048 l: 0.,
9049 a: 0.4,
9050 },
9051 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
9052 spread_radius: px(0.),
9053 offset: point(px(0.0), px(0.0)),
9054 }])
9055 }),
9056 })
9057 .on_mouse_move(|_e, _, cx| {
9058 cx.stop_propagation();
9059 })
9060 .size_full()
9061 .child(element),
9062 )
9063 .map(|div| match decorations {
9064 Decorations::Server => div,
9065 Decorations::Client { tiling, .. } => div.child(
9066 canvas(
9067 |_bounds, window, _| {
9068 window.insert_hitbox(
9069 Bounds::new(
9070 point(px(0.0), px(0.0)),
9071 window.window_bounds().get_bounds().size,
9072 ),
9073 HitboxBehavior::Normal,
9074 )
9075 },
9076 move |_bounds, hitbox, window, cx| {
9077 let mouse = window.mouse_position();
9078 let size = window.window_bounds().get_bounds().size;
9079 let Some(edge) =
9080 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
9081 else {
9082 return;
9083 };
9084 cx.set_global(GlobalResizeEdge(edge));
9085 window.set_cursor_style(
9086 match edge {
9087 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
9088 ResizeEdge::Left | ResizeEdge::Right => {
9089 CursorStyle::ResizeLeftRight
9090 }
9091 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
9092 CursorStyle::ResizeUpLeftDownRight
9093 }
9094 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
9095 CursorStyle::ResizeUpRightDownLeft
9096 }
9097 },
9098 &hitbox,
9099 );
9100 },
9101 )
9102 .size_full()
9103 .absolute(),
9104 ),
9105 })
9106}
9107
9108fn resize_edge(
9109 pos: Point<Pixels>,
9110 shadow_size: Pixels,
9111 window_size: Size<Pixels>,
9112 tiling: Tiling,
9113) -> Option<ResizeEdge> {
9114 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
9115 if bounds.contains(&pos) {
9116 return None;
9117 }
9118
9119 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
9120 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
9121 if !tiling.top && top_left_bounds.contains(&pos) {
9122 return Some(ResizeEdge::TopLeft);
9123 }
9124
9125 let top_right_bounds = Bounds::new(
9126 Point::new(window_size.width - corner_size.width, px(0.)),
9127 corner_size,
9128 );
9129 if !tiling.top && top_right_bounds.contains(&pos) {
9130 return Some(ResizeEdge::TopRight);
9131 }
9132
9133 let bottom_left_bounds = Bounds::new(
9134 Point::new(px(0.), window_size.height - corner_size.height),
9135 corner_size,
9136 );
9137 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
9138 return Some(ResizeEdge::BottomLeft);
9139 }
9140
9141 let bottom_right_bounds = Bounds::new(
9142 Point::new(
9143 window_size.width - corner_size.width,
9144 window_size.height - corner_size.height,
9145 ),
9146 corner_size,
9147 );
9148 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
9149 return Some(ResizeEdge::BottomRight);
9150 }
9151
9152 if !tiling.top && pos.y < shadow_size {
9153 Some(ResizeEdge::Top)
9154 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
9155 Some(ResizeEdge::Bottom)
9156 } else if !tiling.left && pos.x < shadow_size {
9157 Some(ResizeEdge::Left)
9158 } else if !tiling.right && pos.x > window_size.width - shadow_size {
9159 Some(ResizeEdge::Right)
9160 } else {
9161 None
9162 }
9163}
9164
9165fn join_pane_into_active(
9166 active_pane: &Entity<Pane>,
9167 pane: &Entity<Pane>,
9168 window: &mut Window,
9169 cx: &mut App,
9170) {
9171 if pane == active_pane {
9172 } else if pane.read(cx).items_len() == 0 {
9173 pane.update(cx, |_, cx| {
9174 cx.emit(pane::Event::Remove {
9175 focus_on_pane: None,
9176 });
9177 })
9178 } else {
9179 move_all_items(pane, active_pane, window, cx);
9180 }
9181}
9182
9183fn move_all_items(
9184 from_pane: &Entity<Pane>,
9185 to_pane: &Entity<Pane>,
9186 window: &mut Window,
9187 cx: &mut App,
9188) {
9189 let destination_is_different = from_pane != to_pane;
9190 let mut moved_items = 0;
9191 for (item_ix, item_handle) in from_pane
9192 .read(cx)
9193 .items()
9194 .enumerate()
9195 .map(|(ix, item)| (ix, item.clone()))
9196 .collect::<Vec<_>>()
9197 {
9198 let ix = item_ix - moved_items;
9199 if destination_is_different {
9200 // Close item from previous pane
9201 from_pane.update(cx, |source, cx| {
9202 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
9203 });
9204 moved_items += 1;
9205 }
9206
9207 // This automatically removes duplicate items in the pane
9208 to_pane.update(cx, |destination, cx| {
9209 destination.add_item(item_handle, true, true, None, window, cx);
9210 window.focus(&destination.focus_handle(cx), cx)
9211 });
9212 }
9213}
9214
9215pub fn move_item(
9216 source: &Entity<Pane>,
9217 destination: &Entity<Pane>,
9218 item_id_to_move: EntityId,
9219 destination_index: usize,
9220 activate: bool,
9221 window: &mut Window,
9222 cx: &mut App,
9223) {
9224 let Some((item_ix, item_handle)) = source
9225 .read(cx)
9226 .items()
9227 .enumerate()
9228 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
9229 .map(|(ix, item)| (ix, item.clone()))
9230 else {
9231 // Tab was closed during drag
9232 return;
9233 };
9234
9235 if source != destination {
9236 // Close item from previous pane
9237 source.update(cx, |source, cx| {
9238 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
9239 });
9240 }
9241
9242 // This automatically removes duplicate items in the pane
9243 destination.update(cx, |destination, cx| {
9244 destination.add_item_inner(
9245 item_handle,
9246 activate,
9247 activate,
9248 activate,
9249 Some(destination_index),
9250 window,
9251 cx,
9252 );
9253 if activate {
9254 window.focus(&destination.focus_handle(cx), cx)
9255 }
9256 });
9257}
9258
9259pub fn move_active_item(
9260 source: &Entity<Pane>,
9261 destination: &Entity<Pane>,
9262 focus_destination: bool,
9263 close_if_empty: bool,
9264 window: &mut Window,
9265 cx: &mut App,
9266) {
9267 if source == destination {
9268 return;
9269 }
9270 let Some(active_item) = source.read(cx).active_item() else {
9271 return;
9272 };
9273 source.update(cx, |source_pane, cx| {
9274 let item_id = active_item.item_id();
9275 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
9276 destination.update(cx, |target_pane, cx| {
9277 target_pane.add_item(
9278 active_item,
9279 focus_destination,
9280 focus_destination,
9281 Some(target_pane.items_len()),
9282 window,
9283 cx,
9284 );
9285 });
9286 });
9287}
9288
9289pub fn clone_active_item(
9290 workspace_id: Option<WorkspaceId>,
9291 source: &Entity<Pane>,
9292 destination: &Entity<Pane>,
9293 focus_destination: bool,
9294 window: &mut Window,
9295 cx: &mut App,
9296) {
9297 if source == destination {
9298 return;
9299 }
9300 let Some(active_item) = source.read(cx).active_item() else {
9301 return;
9302 };
9303 if !active_item.can_split(cx) {
9304 return;
9305 }
9306 let destination = destination.downgrade();
9307 let task = active_item.clone_on_split(workspace_id, window, cx);
9308 window
9309 .spawn(cx, async move |cx| {
9310 let Some(clone) = task.await else {
9311 return;
9312 };
9313 destination
9314 .update_in(cx, |target_pane, window, cx| {
9315 target_pane.add_item(
9316 clone,
9317 focus_destination,
9318 focus_destination,
9319 Some(target_pane.items_len()),
9320 window,
9321 cx,
9322 );
9323 })
9324 .log_err();
9325 })
9326 .detach();
9327}
9328
9329#[derive(Debug)]
9330pub struct WorkspacePosition {
9331 pub window_bounds: Option<WindowBounds>,
9332 pub display: Option<Uuid>,
9333 pub centered_layout: bool,
9334}
9335
9336pub fn remote_workspace_position_from_db(
9337 connection_options: RemoteConnectionOptions,
9338 paths_to_open: &[PathBuf],
9339 cx: &App,
9340) -> Task<Result<WorkspacePosition>> {
9341 let paths = paths_to_open.to_vec();
9342
9343 cx.background_spawn(async move {
9344 let remote_connection_id = persistence::DB
9345 .get_or_create_remote_connection(connection_options)
9346 .await
9347 .context("fetching serialized ssh project")?;
9348 let serialized_workspace =
9349 persistence::DB.remote_workspace_for_roots(&paths, remote_connection_id);
9350
9351 let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
9352 (Some(WindowBounds::Windowed(bounds)), None)
9353 } else {
9354 let restorable_bounds = serialized_workspace
9355 .as_ref()
9356 .and_then(|workspace| {
9357 Some((workspace.display?, workspace.window_bounds.map(|b| b.0)?))
9358 })
9359 .or_else(|| persistence::read_default_window_bounds());
9360
9361 if let Some((serialized_display, serialized_bounds)) = restorable_bounds {
9362 (Some(serialized_bounds), Some(serialized_display))
9363 } else {
9364 (None, None)
9365 }
9366 };
9367
9368 let centered_layout = serialized_workspace
9369 .as_ref()
9370 .map(|w| w.centered_layout)
9371 .unwrap_or(false);
9372
9373 Ok(WorkspacePosition {
9374 window_bounds,
9375 display,
9376 centered_layout,
9377 })
9378 })
9379}
9380
9381pub fn with_active_or_new_workspace(
9382 cx: &mut App,
9383 f: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + 'static,
9384) {
9385 match cx.active_window().and_then(|w| w.downcast::<Workspace>()) {
9386 Some(workspace) => {
9387 cx.defer(move |cx| {
9388 workspace
9389 .update(cx, |workspace, window, cx| f(workspace, window, cx))
9390 .log_err();
9391 });
9392 }
9393 None => {
9394 let app_state = AppState::global(cx);
9395 if let Some(app_state) = app_state.upgrade() {
9396 open_new(
9397 OpenOptions::default(),
9398 app_state,
9399 cx,
9400 move |workspace, window, cx| f(workspace, window, cx),
9401 )
9402 .detach_and_log_err(cx);
9403 }
9404 }
9405 }
9406}
9407
9408#[cfg(test)]
9409mod tests {
9410 use std::{cell::RefCell, rc::Rc};
9411
9412 use super::*;
9413 use crate::{
9414 dock::{PanelEvent, test::TestPanel},
9415 item::{
9416 ItemBufferKind, ItemEvent,
9417 test::{TestItem, TestProjectItem},
9418 },
9419 };
9420 use fs::FakeFs;
9421 use gpui::{
9422 DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
9423 UpdateGlobal, VisualTestContext, px,
9424 };
9425 use project::{Project, ProjectEntryId};
9426 use serde_json::json;
9427 use settings::SettingsStore;
9428 use util::rel_path::rel_path;
9429
9430 #[gpui::test]
9431 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
9432 init_test(cx);
9433
9434 let fs = FakeFs::new(cx.executor());
9435 let project = Project::test(fs, [], cx).await;
9436 let (workspace, cx) =
9437 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9438
9439 // Adding an item with no ambiguity renders the tab without detail.
9440 let item1 = cx.new(|cx| {
9441 let mut item = TestItem::new(cx);
9442 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
9443 item
9444 });
9445 workspace.update_in(cx, |workspace, window, cx| {
9446 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
9447 });
9448 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
9449
9450 // Adding an item that creates ambiguity increases the level of detail on
9451 // both tabs.
9452 let item2 = cx.new_window_entity(|_window, cx| {
9453 let mut item = TestItem::new(cx);
9454 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
9455 item
9456 });
9457 workspace.update_in(cx, |workspace, window, cx| {
9458 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
9459 });
9460 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
9461 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
9462
9463 // Adding an item that creates ambiguity increases the level of detail only
9464 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
9465 // we stop at the highest detail available.
9466 let item3 = cx.new(|cx| {
9467 let mut item = TestItem::new(cx);
9468 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
9469 item
9470 });
9471 workspace.update_in(cx, |workspace, window, cx| {
9472 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
9473 });
9474 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
9475 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
9476 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
9477 }
9478
9479 #[gpui::test]
9480 async fn test_tracking_active_path(cx: &mut TestAppContext) {
9481 init_test(cx);
9482
9483 let fs = FakeFs::new(cx.executor());
9484 fs.insert_tree(
9485 "/root1",
9486 json!({
9487 "one.txt": "",
9488 "two.txt": "",
9489 }),
9490 )
9491 .await;
9492 fs.insert_tree(
9493 "/root2",
9494 json!({
9495 "three.txt": "",
9496 }),
9497 )
9498 .await;
9499
9500 let project = Project::test(fs, ["root1".as_ref()], cx).await;
9501 let (workspace, cx) =
9502 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9503 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9504 let worktree_id = project.update(cx, |project, cx| {
9505 project.worktrees(cx).next().unwrap().read(cx).id()
9506 });
9507
9508 let item1 = cx.new(|cx| {
9509 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
9510 });
9511 let item2 = cx.new(|cx| {
9512 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
9513 });
9514
9515 // Add an item to an empty pane
9516 workspace.update_in(cx, |workspace, window, cx| {
9517 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
9518 });
9519 project.update(cx, |project, cx| {
9520 assert_eq!(
9521 project.active_entry(),
9522 project
9523 .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
9524 .map(|e| e.id)
9525 );
9526 });
9527 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
9528
9529 // Add a second item to a non-empty pane
9530 workspace.update_in(cx, |workspace, window, cx| {
9531 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
9532 });
9533 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
9534 project.update(cx, |project, cx| {
9535 assert_eq!(
9536 project.active_entry(),
9537 project
9538 .entry_for_path(&(worktree_id, rel_path("two.txt")).into(), cx)
9539 .map(|e| e.id)
9540 );
9541 });
9542
9543 // Close the active item
9544 pane.update_in(cx, |pane, window, cx| {
9545 pane.close_active_item(&Default::default(), window, cx)
9546 })
9547 .await
9548 .unwrap();
9549 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
9550 project.update(cx, |project, cx| {
9551 assert_eq!(
9552 project.active_entry(),
9553 project
9554 .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
9555 .map(|e| e.id)
9556 );
9557 });
9558
9559 // Add a project folder
9560 project
9561 .update(cx, |project, cx| {
9562 project.find_or_create_worktree("root2", true, cx)
9563 })
9564 .await
9565 .unwrap();
9566 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
9567
9568 // Remove a project folder
9569 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
9570 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
9571 }
9572
9573 #[gpui::test]
9574 async fn test_close_window(cx: &mut TestAppContext) {
9575 init_test(cx);
9576
9577 let fs = FakeFs::new(cx.executor());
9578 fs.insert_tree("/root", json!({ "one": "" })).await;
9579
9580 let project = Project::test(fs, ["root".as_ref()], cx).await;
9581 let (workspace, cx) =
9582 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9583
9584 // When there are no dirty items, there's nothing to do.
9585 let item1 = cx.new(TestItem::new);
9586 workspace.update_in(cx, |w, window, cx| {
9587 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
9588 });
9589 let task = workspace.update_in(cx, |w, window, cx| {
9590 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
9591 });
9592 assert!(task.await.unwrap());
9593
9594 // When there are dirty untitled items, prompt to save each one. If the user
9595 // cancels any prompt, then abort.
9596 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
9597 let item3 = cx.new(|cx| {
9598 TestItem::new(cx)
9599 .with_dirty(true)
9600 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
9601 });
9602 workspace.update_in(cx, |w, window, cx| {
9603 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
9604 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
9605 });
9606 let task = workspace.update_in(cx, |w, window, cx| {
9607 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
9608 });
9609 cx.executor().run_until_parked();
9610 cx.simulate_prompt_answer("Cancel"); // cancel save all
9611 cx.executor().run_until_parked();
9612 assert!(!cx.has_pending_prompt());
9613 assert!(!task.await.unwrap());
9614 }
9615
9616 #[gpui::test]
9617 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
9618 init_test(cx);
9619
9620 // Register TestItem as a serializable item
9621 cx.update(|cx| {
9622 register_serializable_item::<TestItem>(cx);
9623 });
9624
9625 let fs = FakeFs::new(cx.executor());
9626 fs.insert_tree("/root", json!({ "one": "" })).await;
9627
9628 let project = Project::test(fs, ["root".as_ref()], cx).await;
9629 let (workspace, cx) =
9630 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9631
9632 // When there are dirty untitled items, but they can serialize, then there is no prompt.
9633 let item1 = cx.new(|cx| {
9634 TestItem::new(cx)
9635 .with_dirty(true)
9636 .with_serialize(|| Some(Task::ready(Ok(()))))
9637 });
9638 let item2 = cx.new(|cx| {
9639 TestItem::new(cx)
9640 .with_dirty(true)
9641 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
9642 .with_serialize(|| Some(Task::ready(Ok(()))))
9643 });
9644 workspace.update_in(cx, |w, window, cx| {
9645 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
9646 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
9647 });
9648 let task = workspace.update_in(cx, |w, window, cx| {
9649 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
9650 });
9651 assert!(task.await.unwrap());
9652 }
9653
9654 #[gpui::test]
9655 async fn test_close_pane_items(cx: &mut TestAppContext) {
9656 init_test(cx);
9657
9658 let fs = FakeFs::new(cx.executor());
9659
9660 let project = Project::test(fs, None, cx).await;
9661 let (workspace, cx) =
9662 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9663
9664 let item1 = cx.new(|cx| {
9665 TestItem::new(cx)
9666 .with_dirty(true)
9667 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
9668 });
9669 let item2 = cx.new(|cx| {
9670 TestItem::new(cx)
9671 .with_dirty(true)
9672 .with_conflict(true)
9673 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9674 });
9675 let item3 = cx.new(|cx| {
9676 TestItem::new(cx)
9677 .with_dirty(true)
9678 .with_conflict(true)
9679 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
9680 });
9681 let item4 = cx.new(|cx| {
9682 TestItem::new(cx).with_dirty(true).with_project_items(&[{
9683 let project_item = TestProjectItem::new_untitled(cx);
9684 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
9685 project_item
9686 }])
9687 });
9688 let pane = workspace.update_in(cx, |workspace, window, cx| {
9689 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
9690 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
9691 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
9692 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
9693 workspace.active_pane().clone()
9694 });
9695
9696 let close_items = pane.update_in(cx, |pane, window, cx| {
9697 pane.activate_item(1, true, true, window, cx);
9698 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
9699 let item1_id = item1.item_id();
9700 let item3_id = item3.item_id();
9701 let item4_id = item4.item_id();
9702 pane.close_items(window, cx, SaveIntent::Close, move |id| {
9703 [item1_id, item3_id, item4_id].contains(&id)
9704 })
9705 });
9706 cx.executor().run_until_parked();
9707
9708 assert!(cx.has_pending_prompt());
9709 cx.simulate_prompt_answer("Save all");
9710
9711 cx.executor().run_until_parked();
9712
9713 // Item 1 is saved. There's a prompt to save item 3.
9714 pane.update(cx, |pane, cx| {
9715 assert_eq!(item1.read(cx).save_count, 1);
9716 assert_eq!(item1.read(cx).save_as_count, 0);
9717 assert_eq!(item1.read(cx).reload_count, 0);
9718 assert_eq!(pane.items_len(), 3);
9719 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
9720 });
9721 assert!(cx.has_pending_prompt());
9722
9723 // Cancel saving item 3.
9724 cx.simulate_prompt_answer("Discard");
9725 cx.executor().run_until_parked();
9726
9727 // Item 3 is reloaded. There's a prompt to save item 4.
9728 pane.update(cx, |pane, cx| {
9729 assert_eq!(item3.read(cx).save_count, 0);
9730 assert_eq!(item3.read(cx).save_as_count, 0);
9731 assert_eq!(item3.read(cx).reload_count, 1);
9732 assert_eq!(pane.items_len(), 2);
9733 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
9734 });
9735
9736 // There's a prompt for a path for item 4.
9737 cx.simulate_new_path_selection(|_| Some(Default::default()));
9738 close_items.await.unwrap();
9739
9740 // The requested items are closed.
9741 pane.update(cx, |pane, cx| {
9742 assert_eq!(item4.read(cx).save_count, 0);
9743 assert_eq!(item4.read(cx).save_as_count, 1);
9744 assert_eq!(item4.read(cx).reload_count, 0);
9745 assert_eq!(pane.items_len(), 1);
9746 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
9747 });
9748 }
9749
9750 #[gpui::test]
9751 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
9752 init_test(cx);
9753
9754 let fs = FakeFs::new(cx.executor());
9755 let project = Project::test(fs, [], cx).await;
9756 let (workspace, cx) =
9757 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9758
9759 // Create several workspace items with single project entries, and two
9760 // workspace items with multiple project entries.
9761 let single_entry_items = (0..=4)
9762 .map(|project_entry_id| {
9763 cx.new(|cx| {
9764 TestItem::new(cx)
9765 .with_dirty(true)
9766 .with_project_items(&[dirty_project_item(
9767 project_entry_id,
9768 &format!("{project_entry_id}.txt"),
9769 cx,
9770 )])
9771 })
9772 })
9773 .collect::<Vec<_>>();
9774 let item_2_3 = cx.new(|cx| {
9775 TestItem::new(cx)
9776 .with_dirty(true)
9777 .with_buffer_kind(ItemBufferKind::Multibuffer)
9778 .with_project_items(&[
9779 single_entry_items[2].read(cx).project_items[0].clone(),
9780 single_entry_items[3].read(cx).project_items[0].clone(),
9781 ])
9782 });
9783 let item_3_4 = cx.new(|cx| {
9784 TestItem::new(cx)
9785 .with_dirty(true)
9786 .with_buffer_kind(ItemBufferKind::Multibuffer)
9787 .with_project_items(&[
9788 single_entry_items[3].read(cx).project_items[0].clone(),
9789 single_entry_items[4].read(cx).project_items[0].clone(),
9790 ])
9791 });
9792
9793 // Create two panes that contain the following project entries:
9794 // left pane:
9795 // multi-entry items: (2, 3)
9796 // single-entry items: 0, 2, 3, 4
9797 // right pane:
9798 // single-entry items: 4, 1
9799 // multi-entry items: (3, 4)
9800 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
9801 let left_pane = workspace.active_pane().clone();
9802 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
9803 workspace.add_item_to_active_pane(
9804 single_entry_items[0].boxed_clone(),
9805 None,
9806 true,
9807 window,
9808 cx,
9809 );
9810 workspace.add_item_to_active_pane(
9811 single_entry_items[2].boxed_clone(),
9812 None,
9813 true,
9814 window,
9815 cx,
9816 );
9817 workspace.add_item_to_active_pane(
9818 single_entry_items[3].boxed_clone(),
9819 None,
9820 true,
9821 window,
9822 cx,
9823 );
9824 workspace.add_item_to_active_pane(
9825 single_entry_items[4].boxed_clone(),
9826 None,
9827 true,
9828 window,
9829 cx,
9830 );
9831
9832 let right_pane =
9833 workspace.split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx);
9834
9835 let boxed_clone = single_entry_items[1].boxed_clone();
9836 let right_pane = window.spawn(cx, async move |cx| {
9837 right_pane.await.inspect(|right_pane| {
9838 right_pane
9839 .update_in(cx, |pane, window, cx| {
9840 pane.add_item(boxed_clone, true, true, None, window, cx);
9841 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
9842 })
9843 .unwrap();
9844 })
9845 });
9846
9847 (left_pane, right_pane)
9848 });
9849 let right_pane = right_pane.await.unwrap();
9850 cx.focus(&right_pane);
9851
9852 let close = right_pane.update_in(cx, |pane, window, cx| {
9853 pane.close_all_items(&CloseAllItems::default(), window, cx)
9854 .unwrap()
9855 });
9856 cx.executor().run_until_parked();
9857
9858 let msg = cx.pending_prompt().unwrap().0;
9859 assert!(msg.contains("1.txt"));
9860 assert!(!msg.contains("2.txt"));
9861 assert!(!msg.contains("3.txt"));
9862 assert!(!msg.contains("4.txt"));
9863
9864 // With best-effort close, cancelling item 1 keeps it open but items 4
9865 // and (3,4) still close since their entries exist in left pane.
9866 cx.simulate_prompt_answer("Cancel");
9867 close.await;
9868
9869 right_pane.read_with(cx, |pane, _| {
9870 assert_eq!(pane.items_len(), 1);
9871 });
9872
9873 // Remove item 3 from left pane, making (2,3) the only item with entry 3.
9874 left_pane
9875 .update_in(cx, |left_pane, window, cx| {
9876 left_pane.close_item_by_id(
9877 single_entry_items[3].entity_id(),
9878 SaveIntent::Skip,
9879 window,
9880 cx,
9881 )
9882 })
9883 .await
9884 .unwrap();
9885
9886 let close = left_pane.update_in(cx, |pane, window, cx| {
9887 pane.close_all_items(&CloseAllItems::default(), window, cx)
9888 .unwrap()
9889 });
9890 cx.executor().run_until_parked();
9891
9892 let details = cx.pending_prompt().unwrap().1;
9893 assert!(details.contains("0.txt"));
9894 assert!(details.contains("3.txt"));
9895 assert!(details.contains("4.txt"));
9896 // Ideally 2.txt wouldn't appear since entry 2 still exists in item 2.
9897 // But we can only save whole items, so saving (2,3) for entry 3 includes 2.
9898 // assert!(!details.contains("2.txt"));
9899
9900 cx.simulate_prompt_answer("Save all");
9901 cx.executor().run_until_parked();
9902 close.await;
9903
9904 left_pane.read_with(cx, |pane, _| {
9905 assert_eq!(pane.items_len(), 0);
9906 });
9907 }
9908
9909 #[gpui::test]
9910 async fn test_autosave(cx: &mut gpui::TestAppContext) {
9911 init_test(cx);
9912
9913 let fs = FakeFs::new(cx.executor());
9914 let project = Project::test(fs, [], cx).await;
9915 let (workspace, cx) =
9916 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9917 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9918
9919 let item = cx.new(|cx| {
9920 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
9921 });
9922 let item_id = item.entity_id();
9923 workspace.update_in(cx, |workspace, window, cx| {
9924 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
9925 });
9926
9927 // Autosave on window change.
9928 item.update(cx, |item, cx| {
9929 SettingsStore::update_global(cx, |settings, cx| {
9930 settings.update_user_settings(cx, |settings| {
9931 settings.workspace.autosave = Some(AutosaveSetting::OnWindowChange);
9932 })
9933 });
9934 item.is_dirty = true;
9935 });
9936
9937 // Deactivating the window saves the file.
9938 cx.deactivate_window();
9939 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
9940
9941 // Re-activating the window doesn't save the file.
9942 cx.update(|window, _| window.activate_window());
9943 cx.executor().run_until_parked();
9944 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
9945
9946 // Autosave on focus change.
9947 item.update_in(cx, |item, window, cx| {
9948 cx.focus_self(window);
9949 SettingsStore::update_global(cx, |settings, cx| {
9950 settings.update_user_settings(cx, |settings| {
9951 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
9952 })
9953 });
9954 item.is_dirty = true;
9955 });
9956 // Blurring the item saves the file.
9957 item.update_in(cx, |_, window, _| window.blur());
9958 cx.executor().run_until_parked();
9959 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
9960
9961 // Deactivating the window still saves the file.
9962 item.update_in(cx, |item, window, cx| {
9963 cx.focus_self(window);
9964 item.is_dirty = true;
9965 });
9966 cx.deactivate_window();
9967 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
9968
9969 // Autosave after delay.
9970 item.update(cx, |item, cx| {
9971 SettingsStore::update_global(cx, |settings, cx| {
9972 settings.update_user_settings(cx, |settings| {
9973 settings.workspace.autosave = Some(AutosaveSetting::AfterDelay {
9974 milliseconds: 500.into(),
9975 });
9976 })
9977 });
9978 item.is_dirty = true;
9979 cx.emit(ItemEvent::Edit);
9980 });
9981
9982 // Delay hasn't fully expired, so the file is still dirty and unsaved.
9983 cx.executor().advance_clock(Duration::from_millis(250));
9984 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
9985
9986 // After delay expires, the file is saved.
9987 cx.executor().advance_clock(Duration::from_millis(250));
9988 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
9989
9990 // Autosave after delay, should save earlier than delay if tab is closed
9991 item.update(cx, |item, cx| {
9992 item.is_dirty = true;
9993 cx.emit(ItemEvent::Edit);
9994 });
9995 cx.executor().advance_clock(Duration::from_millis(250));
9996 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
9997
9998 // // Ensure auto save with delay saves the item on close, even if the timer hasn't yet run out.
9999 pane.update_in(cx, |pane, window, cx| {
10000 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
10001 })
10002 .await
10003 .unwrap();
10004 assert!(!cx.has_pending_prompt());
10005 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
10006
10007 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
10008 workspace.update_in(cx, |workspace, window, cx| {
10009 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
10010 });
10011 item.update_in(cx, |item, _window, cx| {
10012 item.is_dirty = true;
10013 for project_item in &mut item.project_items {
10014 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
10015 }
10016 });
10017 cx.run_until_parked();
10018 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
10019
10020 // Autosave on focus change, ensuring closing the tab counts as such.
10021 item.update(cx, |item, cx| {
10022 SettingsStore::update_global(cx, |settings, cx| {
10023 settings.update_user_settings(cx, |settings| {
10024 settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
10025 })
10026 });
10027 item.is_dirty = true;
10028 for project_item in &mut item.project_items {
10029 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
10030 }
10031 });
10032
10033 pane.update_in(cx, |pane, window, cx| {
10034 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
10035 })
10036 .await
10037 .unwrap();
10038 assert!(!cx.has_pending_prompt());
10039 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
10040
10041 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
10042 workspace.update_in(cx, |workspace, window, cx| {
10043 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
10044 });
10045 item.update_in(cx, |item, window, cx| {
10046 item.project_items[0].update(cx, |item, _| {
10047 item.entry_id = None;
10048 });
10049 item.is_dirty = true;
10050 window.blur();
10051 });
10052 cx.run_until_parked();
10053 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
10054
10055 // Ensure autosave is prevented for deleted files also when closing the buffer.
10056 let _close_items = pane.update_in(cx, |pane, window, cx| {
10057 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
10058 });
10059 cx.run_until_parked();
10060 assert!(cx.has_pending_prompt());
10061 item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
10062 }
10063
10064 #[gpui::test]
10065 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
10066 init_test(cx);
10067
10068 let fs = FakeFs::new(cx.executor());
10069
10070 let project = Project::test(fs, [], cx).await;
10071 let (workspace, cx) =
10072 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10073
10074 let item = cx.new(|cx| {
10075 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10076 });
10077 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10078 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
10079 let toolbar_notify_count = Rc::new(RefCell::new(0));
10080
10081 workspace.update_in(cx, |workspace, window, cx| {
10082 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
10083 let toolbar_notification_count = toolbar_notify_count.clone();
10084 cx.observe_in(&toolbar, window, move |_, _, _, _| {
10085 *toolbar_notification_count.borrow_mut() += 1
10086 })
10087 .detach();
10088 });
10089
10090 pane.read_with(cx, |pane, _| {
10091 assert!(!pane.can_navigate_backward());
10092 assert!(!pane.can_navigate_forward());
10093 });
10094
10095 item.update_in(cx, |item, _, cx| {
10096 item.set_state("one".to_string(), cx);
10097 });
10098
10099 // Toolbar must be notified to re-render the navigation buttons
10100 assert_eq!(*toolbar_notify_count.borrow(), 1);
10101
10102 pane.read_with(cx, |pane, _| {
10103 assert!(pane.can_navigate_backward());
10104 assert!(!pane.can_navigate_forward());
10105 });
10106
10107 workspace
10108 .update_in(cx, |workspace, window, cx| {
10109 workspace.go_back(pane.downgrade(), window, cx)
10110 })
10111 .await
10112 .unwrap();
10113
10114 assert_eq!(*toolbar_notify_count.borrow(), 2);
10115 pane.read_with(cx, |pane, _| {
10116 assert!(!pane.can_navigate_backward());
10117 assert!(pane.can_navigate_forward());
10118 });
10119 }
10120
10121 #[gpui::test]
10122 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
10123 init_test(cx);
10124 let fs = FakeFs::new(cx.executor());
10125
10126 let project = Project::test(fs, [], cx).await;
10127 let (workspace, cx) =
10128 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10129
10130 let panel = workspace.update_in(cx, |workspace, window, cx| {
10131 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
10132 workspace.add_panel(panel.clone(), window, cx);
10133
10134 workspace
10135 .right_dock()
10136 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
10137
10138 panel
10139 });
10140
10141 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10142 pane.update_in(cx, |pane, window, cx| {
10143 let item = cx.new(TestItem::new);
10144 pane.add_item(Box::new(item), true, true, None, window, cx);
10145 });
10146
10147 // Transfer focus from center to panel
10148 workspace.update_in(cx, |workspace, window, cx| {
10149 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10150 });
10151
10152 workspace.update_in(cx, |workspace, window, cx| {
10153 assert!(workspace.right_dock().read(cx).is_open());
10154 assert!(!panel.is_zoomed(window, cx));
10155 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10156 });
10157
10158 // Transfer focus from panel to center
10159 workspace.update_in(cx, |workspace, window, cx| {
10160 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10161 });
10162
10163 workspace.update_in(cx, |workspace, window, cx| {
10164 assert!(workspace.right_dock().read(cx).is_open());
10165 assert!(!panel.is_zoomed(window, cx));
10166 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10167 });
10168
10169 // Close the dock
10170 workspace.update_in(cx, |workspace, window, cx| {
10171 workspace.toggle_dock(DockPosition::Right, window, cx);
10172 });
10173
10174 workspace.update_in(cx, |workspace, window, cx| {
10175 assert!(!workspace.right_dock().read(cx).is_open());
10176 assert!(!panel.is_zoomed(window, cx));
10177 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10178 });
10179
10180 // Open the dock
10181 workspace.update_in(cx, |workspace, window, cx| {
10182 workspace.toggle_dock(DockPosition::Right, window, cx);
10183 });
10184
10185 workspace.update_in(cx, |workspace, window, cx| {
10186 assert!(workspace.right_dock().read(cx).is_open());
10187 assert!(!panel.is_zoomed(window, cx));
10188 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10189 });
10190
10191 // Focus and zoom panel
10192 panel.update_in(cx, |panel, window, cx| {
10193 cx.focus_self(window);
10194 panel.set_zoomed(true, window, cx)
10195 });
10196
10197 workspace.update_in(cx, |workspace, window, cx| {
10198 assert!(workspace.right_dock().read(cx).is_open());
10199 assert!(panel.is_zoomed(window, cx));
10200 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10201 });
10202
10203 // Transfer focus to the center closes the dock
10204 workspace.update_in(cx, |workspace, window, cx| {
10205 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10206 });
10207
10208 workspace.update_in(cx, |workspace, window, cx| {
10209 assert!(!workspace.right_dock().read(cx).is_open());
10210 assert!(panel.is_zoomed(window, cx));
10211 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10212 });
10213
10214 // Transferring focus back to the panel keeps it zoomed
10215 workspace.update_in(cx, |workspace, window, cx| {
10216 workspace.toggle_panel_focus::<TestPanel>(window, cx);
10217 });
10218
10219 workspace.update_in(cx, |workspace, window, cx| {
10220 assert!(workspace.right_dock().read(cx).is_open());
10221 assert!(panel.is_zoomed(window, cx));
10222 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10223 });
10224
10225 // Close the dock while it is zoomed
10226 workspace.update_in(cx, |workspace, window, cx| {
10227 workspace.toggle_dock(DockPosition::Right, window, cx)
10228 });
10229
10230 workspace.update_in(cx, |workspace, window, cx| {
10231 assert!(!workspace.right_dock().read(cx).is_open());
10232 assert!(panel.is_zoomed(window, cx));
10233 assert!(workspace.zoomed.is_none());
10234 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10235 });
10236
10237 // Opening the dock, when it's zoomed, retains focus
10238 workspace.update_in(cx, |workspace, window, cx| {
10239 workspace.toggle_dock(DockPosition::Right, window, cx)
10240 });
10241
10242 workspace.update_in(cx, |workspace, window, cx| {
10243 assert!(workspace.right_dock().read(cx).is_open());
10244 assert!(panel.is_zoomed(window, cx));
10245 assert!(workspace.zoomed.is_some());
10246 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
10247 });
10248
10249 // Unzoom and close the panel, zoom the active pane.
10250 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
10251 workspace.update_in(cx, |workspace, window, cx| {
10252 workspace.toggle_dock(DockPosition::Right, window, cx)
10253 });
10254 pane.update_in(cx, |pane, window, cx| {
10255 pane.toggle_zoom(&Default::default(), window, cx)
10256 });
10257
10258 // Opening a dock unzooms the pane.
10259 workspace.update_in(cx, |workspace, window, cx| {
10260 workspace.toggle_dock(DockPosition::Right, window, cx)
10261 });
10262 workspace.update_in(cx, |workspace, window, cx| {
10263 let pane = pane.read(cx);
10264 assert!(!pane.is_zoomed());
10265 assert!(!pane.focus_handle(cx).is_focused(window));
10266 assert!(workspace.right_dock().read(cx).is_open());
10267 assert!(workspace.zoomed.is_none());
10268 });
10269 }
10270
10271 #[gpui::test]
10272 async fn test_pane_zoom_in_out(cx: &mut TestAppContext) {
10273 init_test(cx);
10274 let fs = FakeFs::new(cx.executor());
10275
10276 let project = Project::test(fs, [], cx).await;
10277 let (workspace, cx) =
10278 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10279
10280 let pane = workspace.update_in(cx, |workspace, _window, _cx| {
10281 workspace.active_pane().clone()
10282 });
10283
10284 // Add an item to the pane so it can be zoomed
10285 workspace.update_in(cx, |workspace, window, cx| {
10286 let item = cx.new(TestItem::new);
10287 workspace.add_item(pane.clone(), Box::new(item), None, true, true, window, cx);
10288 });
10289
10290 // Initially not zoomed
10291 workspace.update_in(cx, |workspace, _window, cx| {
10292 assert!(!pane.read(cx).is_zoomed(), "Pane starts unzoomed");
10293 assert!(
10294 workspace.zoomed.is_none(),
10295 "Workspace should track no zoomed pane"
10296 );
10297 assert!(pane.read(cx).items_len() > 0, "Pane should have items");
10298 });
10299
10300 // Zoom In
10301 pane.update_in(cx, |pane, window, cx| {
10302 pane.zoom_in(&crate::ZoomIn, window, cx);
10303 });
10304
10305 workspace.update_in(cx, |workspace, window, cx| {
10306 assert!(
10307 pane.read(cx).is_zoomed(),
10308 "Pane should be zoomed after ZoomIn"
10309 );
10310 assert!(
10311 workspace.zoomed.is_some(),
10312 "Workspace should track the zoomed pane"
10313 );
10314 assert!(
10315 pane.read(cx).focus_handle(cx).contains_focused(window, cx),
10316 "ZoomIn should focus the pane"
10317 );
10318 });
10319
10320 // Zoom In again is a no-op
10321 pane.update_in(cx, |pane, window, cx| {
10322 pane.zoom_in(&crate::ZoomIn, window, cx);
10323 });
10324
10325 workspace.update_in(cx, |workspace, window, cx| {
10326 assert!(pane.read(cx).is_zoomed(), "Second ZoomIn keeps pane zoomed");
10327 assert!(
10328 workspace.zoomed.is_some(),
10329 "Workspace still tracks zoomed pane"
10330 );
10331 assert!(
10332 pane.read(cx).focus_handle(cx).contains_focused(window, cx),
10333 "Pane remains focused after repeated ZoomIn"
10334 );
10335 });
10336
10337 // Zoom Out
10338 pane.update_in(cx, |pane, window, cx| {
10339 pane.zoom_out(&crate::ZoomOut, window, cx);
10340 });
10341
10342 workspace.update_in(cx, |workspace, _window, cx| {
10343 assert!(
10344 !pane.read(cx).is_zoomed(),
10345 "Pane should unzoom after ZoomOut"
10346 );
10347 assert!(
10348 workspace.zoomed.is_none(),
10349 "Workspace clears zoom tracking after ZoomOut"
10350 );
10351 });
10352
10353 // Zoom Out again is a no-op
10354 pane.update_in(cx, |pane, window, cx| {
10355 pane.zoom_out(&crate::ZoomOut, window, cx);
10356 });
10357
10358 workspace.update_in(cx, |workspace, _window, cx| {
10359 assert!(
10360 !pane.read(cx).is_zoomed(),
10361 "Second ZoomOut keeps pane unzoomed"
10362 );
10363 assert!(
10364 workspace.zoomed.is_none(),
10365 "Workspace remains without zoomed pane"
10366 );
10367 });
10368 }
10369
10370 #[gpui::test]
10371 async fn test_toggle_all_docks(cx: &mut gpui::TestAppContext) {
10372 init_test(cx);
10373 let fs = FakeFs::new(cx.executor());
10374
10375 let project = Project::test(fs, [], cx).await;
10376 let (workspace, cx) =
10377 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10378 workspace.update_in(cx, |workspace, window, cx| {
10379 // Open two docks
10380 let left_dock = workspace.dock_at_position(DockPosition::Left);
10381 let right_dock = workspace.dock_at_position(DockPosition::Right);
10382
10383 left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
10384 right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
10385
10386 assert!(left_dock.read(cx).is_open());
10387 assert!(right_dock.read(cx).is_open());
10388 });
10389
10390 workspace.update_in(cx, |workspace, window, cx| {
10391 // Toggle all docks - should close both
10392 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
10393
10394 let left_dock = workspace.dock_at_position(DockPosition::Left);
10395 let right_dock = workspace.dock_at_position(DockPosition::Right);
10396 assert!(!left_dock.read(cx).is_open());
10397 assert!(!right_dock.read(cx).is_open());
10398 });
10399
10400 workspace.update_in(cx, |workspace, window, cx| {
10401 // Toggle again - should reopen both
10402 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
10403
10404 let left_dock = workspace.dock_at_position(DockPosition::Left);
10405 let right_dock = workspace.dock_at_position(DockPosition::Right);
10406 assert!(left_dock.read(cx).is_open());
10407 assert!(right_dock.read(cx).is_open());
10408 });
10409 }
10410
10411 #[gpui::test]
10412 async fn test_toggle_all_with_manual_close(cx: &mut gpui::TestAppContext) {
10413 init_test(cx);
10414 let fs = FakeFs::new(cx.executor());
10415
10416 let project = Project::test(fs, [], cx).await;
10417 let (workspace, cx) =
10418 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10419 workspace.update_in(cx, |workspace, window, cx| {
10420 // Open two docks
10421 let left_dock = workspace.dock_at_position(DockPosition::Left);
10422 let right_dock = workspace.dock_at_position(DockPosition::Right);
10423
10424 left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
10425 right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
10426
10427 assert!(left_dock.read(cx).is_open());
10428 assert!(right_dock.read(cx).is_open());
10429 });
10430
10431 workspace.update_in(cx, |workspace, window, cx| {
10432 // Close them manually
10433 workspace.toggle_dock(DockPosition::Left, window, cx);
10434 workspace.toggle_dock(DockPosition::Right, window, cx);
10435
10436 let left_dock = workspace.dock_at_position(DockPosition::Left);
10437 let right_dock = workspace.dock_at_position(DockPosition::Right);
10438 assert!(!left_dock.read(cx).is_open());
10439 assert!(!right_dock.read(cx).is_open());
10440 });
10441
10442 workspace.update_in(cx, |workspace, window, cx| {
10443 // Toggle all docks - only last closed (right dock) should reopen
10444 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
10445
10446 let left_dock = workspace.dock_at_position(DockPosition::Left);
10447 let right_dock = workspace.dock_at_position(DockPosition::Right);
10448 assert!(!left_dock.read(cx).is_open());
10449 assert!(right_dock.read(cx).is_open());
10450 });
10451 }
10452
10453 #[gpui::test]
10454 async fn test_toggle_all_docks_after_dock_move(cx: &mut gpui::TestAppContext) {
10455 init_test(cx);
10456 let fs = FakeFs::new(cx.executor());
10457 let project = Project::test(fs, [], cx).await;
10458 let (workspace, cx) =
10459 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10460
10461 // Open two docks (left and right) with one panel each
10462 let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| {
10463 let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
10464 workspace.add_panel(left_panel.clone(), window, cx);
10465
10466 let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
10467 workspace.add_panel(right_panel.clone(), window, cx);
10468
10469 workspace.toggle_dock(DockPosition::Left, window, cx);
10470 workspace.toggle_dock(DockPosition::Right, window, cx);
10471
10472 // Verify initial state
10473 assert!(
10474 workspace.left_dock().read(cx).is_open(),
10475 "Left dock should be open"
10476 );
10477 assert_eq!(
10478 workspace
10479 .left_dock()
10480 .read(cx)
10481 .visible_panel()
10482 .unwrap()
10483 .panel_id(),
10484 left_panel.panel_id(),
10485 "Left panel should be visible in left dock"
10486 );
10487 assert!(
10488 workspace.right_dock().read(cx).is_open(),
10489 "Right dock should be open"
10490 );
10491 assert_eq!(
10492 workspace
10493 .right_dock()
10494 .read(cx)
10495 .visible_panel()
10496 .unwrap()
10497 .panel_id(),
10498 right_panel.panel_id(),
10499 "Right panel should be visible in right dock"
10500 );
10501 assert!(
10502 !workspace.bottom_dock().read(cx).is_open(),
10503 "Bottom dock should be closed"
10504 );
10505
10506 (left_panel, right_panel)
10507 });
10508
10509 // Focus the left panel and move it to the next position (bottom dock)
10510 workspace.update_in(cx, |workspace, window, cx| {
10511 workspace.toggle_panel_focus::<TestPanel>(window, cx); // Focus left panel
10512 assert!(
10513 left_panel.read(cx).focus_handle(cx).is_focused(window),
10514 "Left panel should be focused"
10515 );
10516 });
10517
10518 cx.dispatch_action(MoveFocusedPanelToNextPosition);
10519
10520 // Verify the left panel has moved to the bottom dock, and the bottom dock is now open
10521 workspace.update(cx, |workspace, cx| {
10522 assert!(
10523 !workspace.left_dock().read(cx).is_open(),
10524 "Left dock should be closed"
10525 );
10526 assert!(
10527 workspace.bottom_dock().read(cx).is_open(),
10528 "Bottom dock should now be open"
10529 );
10530 assert_eq!(
10531 left_panel.read(cx).position,
10532 DockPosition::Bottom,
10533 "Left panel should now be in the bottom dock"
10534 );
10535 assert_eq!(
10536 workspace
10537 .bottom_dock()
10538 .read(cx)
10539 .visible_panel()
10540 .unwrap()
10541 .panel_id(),
10542 left_panel.panel_id(),
10543 "Left panel should be the visible panel in the bottom dock"
10544 );
10545 });
10546
10547 // Toggle all docks off
10548 workspace.update_in(cx, |workspace, window, cx| {
10549 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
10550 assert!(
10551 !workspace.left_dock().read(cx).is_open(),
10552 "Left dock should be closed"
10553 );
10554 assert!(
10555 !workspace.right_dock().read(cx).is_open(),
10556 "Right dock should be closed"
10557 );
10558 assert!(
10559 !workspace.bottom_dock().read(cx).is_open(),
10560 "Bottom dock should be closed"
10561 );
10562 });
10563
10564 // Toggle all docks back on and verify positions are restored
10565 workspace.update_in(cx, |workspace, window, cx| {
10566 workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
10567 assert!(
10568 !workspace.left_dock().read(cx).is_open(),
10569 "Left dock should remain closed"
10570 );
10571 assert!(
10572 workspace.right_dock().read(cx).is_open(),
10573 "Right dock should remain open"
10574 );
10575 assert!(
10576 workspace.bottom_dock().read(cx).is_open(),
10577 "Bottom dock should remain open"
10578 );
10579 assert_eq!(
10580 left_panel.read(cx).position,
10581 DockPosition::Bottom,
10582 "Left panel should remain in the bottom dock"
10583 );
10584 assert_eq!(
10585 right_panel.read(cx).position,
10586 DockPosition::Right,
10587 "Right panel should remain in the right dock"
10588 );
10589 assert_eq!(
10590 workspace
10591 .bottom_dock()
10592 .read(cx)
10593 .visible_panel()
10594 .unwrap()
10595 .panel_id(),
10596 left_panel.panel_id(),
10597 "Left panel should be the visible panel in the right dock"
10598 );
10599 });
10600 }
10601
10602 #[gpui::test]
10603 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
10604 init_test(cx);
10605
10606 let fs = FakeFs::new(cx.executor());
10607
10608 let project = Project::test(fs, None, cx).await;
10609 let (workspace, cx) =
10610 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10611
10612 // Let's arrange the panes like this:
10613 //
10614 // +-----------------------+
10615 // | top |
10616 // +------+--------+-------+
10617 // | left | center | right |
10618 // +------+--------+-------+
10619 // | bottom |
10620 // +-----------------------+
10621
10622 let top_item = cx.new(|cx| {
10623 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
10624 });
10625 let bottom_item = cx.new(|cx| {
10626 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
10627 });
10628 let left_item = cx.new(|cx| {
10629 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
10630 });
10631 let right_item = cx.new(|cx| {
10632 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
10633 });
10634 let center_item = cx.new(|cx| {
10635 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
10636 });
10637
10638 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
10639 let top_pane_id = workspace.active_pane().entity_id();
10640 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
10641 workspace.split_pane(
10642 workspace.active_pane().clone(),
10643 SplitDirection::Down,
10644 window,
10645 cx,
10646 );
10647 top_pane_id
10648 });
10649 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
10650 let bottom_pane_id = workspace.active_pane().entity_id();
10651 workspace.add_item_to_active_pane(
10652 Box::new(bottom_item.clone()),
10653 None,
10654 false,
10655 window,
10656 cx,
10657 );
10658 workspace.split_pane(
10659 workspace.active_pane().clone(),
10660 SplitDirection::Up,
10661 window,
10662 cx,
10663 );
10664 bottom_pane_id
10665 });
10666 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
10667 let left_pane_id = workspace.active_pane().entity_id();
10668 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
10669 workspace.split_pane(
10670 workspace.active_pane().clone(),
10671 SplitDirection::Right,
10672 window,
10673 cx,
10674 );
10675 left_pane_id
10676 });
10677 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
10678 let right_pane_id = workspace.active_pane().entity_id();
10679 workspace.add_item_to_active_pane(
10680 Box::new(right_item.clone()),
10681 None,
10682 false,
10683 window,
10684 cx,
10685 );
10686 workspace.split_pane(
10687 workspace.active_pane().clone(),
10688 SplitDirection::Left,
10689 window,
10690 cx,
10691 );
10692 right_pane_id
10693 });
10694 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
10695 let center_pane_id = workspace.active_pane().entity_id();
10696 workspace.add_item_to_active_pane(
10697 Box::new(center_item.clone()),
10698 None,
10699 false,
10700 window,
10701 cx,
10702 );
10703 center_pane_id
10704 });
10705 cx.executor().run_until_parked();
10706
10707 workspace.update_in(cx, |workspace, window, cx| {
10708 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
10709
10710 // Join into next from center pane into right
10711 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
10712 });
10713
10714 workspace.update_in(cx, |workspace, window, cx| {
10715 let active_pane = workspace.active_pane();
10716 assert_eq!(right_pane_id, active_pane.entity_id());
10717 assert_eq!(2, active_pane.read(cx).items_len());
10718 let item_ids_in_pane =
10719 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
10720 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
10721 assert!(item_ids_in_pane.contains(&right_item.item_id()));
10722
10723 // Join into next from right pane into bottom
10724 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
10725 });
10726
10727 workspace.update_in(cx, |workspace, window, cx| {
10728 let active_pane = workspace.active_pane();
10729 assert_eq!(bottom_pane_id, active_pane.entity_id());
10730 assert_eq!(3, active_pane.read(cx).items_len());
10731 let item_ids_in_pane =
10732 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
10733 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
10734 assert!(item_ids_in_pane.contains(&right_item.item_id()));
10735 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
10736
10737 // Join into next from bottom pane into left
10738 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
10739 });
10740
10741 workspace.update_in(cx, |workspace, window, cx| {
10742 let active_pane = workspace.active_pane();
10743 assert_eq!(left_pane_id, active_pane.entity_id());
10744 assert_eq!(4, active_pane.read(cx).items_len());
10745 let item_ids_in_pane =
10746 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
10747 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
10748 assert!(item_ids_in_pane.contains(&right_item.item_id()));
10749 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
10750 assert!(item_ids_in_pane.contains(&left_item.item_id()));
10751
10752 // Join into next from left pane into top
10753 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
10754 });
10755
10756 workspace.update_in(cx, |workspace, window, cx| {
10757 let active_pane = workspace.active_pane();
10758 assert_eq!(top_pane_id, active_pane.entity_id());
10759 assert_eq!(5, active_pane.read(cx).items_len());
10760 let item_ids_in_pane =
10761 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
10762 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
10763 assert!(item_ids_in_pane.contains(&right_item.item_id()));
10764 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
10765 assert!(item_ids_in_pane.contains(&left_item.item_id()));
10766 assert!(item_ids_in_pane.contains(&top_item.item_id()));
10767
10768 // Single pane left: no-op
10769 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
10770 });
10771
10772 workspace.update(cx, |workspace, _cx| {
10773 let active_pane = workspace.active_pane();
10774 assert_eq!(top_pane_id, active_pane.entity_id());
10775 });
10776 }
10777
10778 fn add_an_item_to_active_pane(
10779 cx: &mut VisualTestContext,
10780 workspace: &Entity<Workspace>,
10781 item_id: u64,
10782 ) -> Entity<TestItem> {
10783 let item = cx.new(|cx| {
10784 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
10785 item_id,
10786 "item{item_id}.txt",
10787 cx,
10788 )])
10789 });
10790 workspace.update_in(cx, |workspace, window, cx| {
10791 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
10792 });
10793 item
10794 }
10795
10796 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
10797 workspace.update_in(cx, |workspace, window, cx| {
10798 workspace.split_pane(
10799 workspace.active_pane().clone(),
10800 SplitDirection::Right,
10801 window,
10802 cx,
10803 )
10804 })
10805 }
10806
10807 #[gpui::test]
10808 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
10809 init_test(cx);
10810 let fs = FakeFs::new(cx.executor());
10811 let project = Project::test(fs, None, cx).await;
10812 let (workspace, cx) =
10813 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10814
10815 add_an_item_to_active_pane(cx, &workspace, 1);
10816 split_pane(cx, &workspace);
10817 add_an_item_to_active_pane(cx, &workspace, 2);
10818 split_pane(cx, &workspace); // empty pane
10819 split_pane(cx, &workspace);
10820 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
10821
10822 cx.executor().run_until_parked();
10823
10824 workspace.update(cx, |workspace, cx| {
10825 let num_panes = workspace.panes().len();
10826 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
10827 let active_item = workspace
10828 .active_pane()
10829 .read(cx)
10830 .active_item()
10831 .expect("item is in focus");
10832
10833 assert_eq!(num_panes, 4);
10834 assert_eq!(num_items_in_current_pane, 1);
10835 assert_eq!(active_item.item_id(), last_item.item_id());
10836 });
10837
10838 workspace.update_in(cx, |workspace, window, cx| {
10839 workspace.join_all_panes(window, cx);
10840 });
10841
10842 workspace.update(cx, |workspace, cx| {
10843 let num_panes = workspace.panes().len();
10844 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
10845 let active_item = workspace
10846 .active_pane()
10847 .read(cx)
10848 .active_item()
10849 .expect("item is in focus");
10850
10851 assert_eq!(num_panes, 1);
10852 assert_eq!(num_items_in_current_pane, 3);
10853 assert_eq!(active_item.item_id(), last_item.item_id());
10854 });
10855 }
10856 struct TestModal(FocusHandle);
10857
10858 impl TestModal {
10859 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
10860 Self(cx.focus_handle())
10861 }
10862 }
10863
10864 impl EventEmitter<DismissEvent> for TestModal {}
10865
10866 impl Focusable for TestModal {
10867 fn focus_handle(&self, _cx: &App) -> FocusHandle {
10868 self.0.clone()
10869 }
10870 }
10871
10872 impl ModalView for TestModal {}
10873
10874 impl Render for TestModal {
10875 fn render(
10876 &mut self,
10877 _window: &mut Window,
10878 _cx: &mut Context<TestModal>,
10879 ) -> impl IntoElement {
10880 div().track_focus(&self.0)
10881 }
10882 }
10883
10884 #[gpui::test]
10885 async fn test_panels(cx: &mut gpui::TestAppContext) {
10886 init_test(cx);
10887 let fs = FakeFs::new(cx.executor());
10888
10889 let project = Project::test(fs, [], cx).await;
10890 let (workspace, cx) =
10891 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10892
10893 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
10894 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
10895 workspace.add_panel(panel_1.clone(), window, cx);
10896 workspace.toggle_dock(DockPosition::Left, window, cx);
10897 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
10898 workspace.add_panel(panel_2.clone(), window, cx);
10899 workspace.toggle_dock(DockPosition::Right, window, cx);
10900
10901 let left_dock = workspace.left_dock();
10902 assert_eq!(
10903 left_dock.read(cx).visible_panel().unwrap().panel_id(),
10904 panel_1.panel_id()
10905 );
10906 assert_eq!(
10907 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
10908 panel_1.size(window, cx)
10909 );
10910
10911 left_dock.update(cx, |left_dock, cx| {
10912 left_dock.resize_active_panel(Some(px(1337.)), window, cx)
10913 });
10914 assert_eq!(
10915 workspace
10916 .right_dock()
10917 .read(cx)
10918 .visible_panel()
10919 .unwrap()
10920 .panel_id(),
10921 panel_2.panel_id(),
10922 );
10923
10924 (panel_1, panel_2)
10925 });
10926
10927 // Move panel_1 to the right
10928 panel_1.update_in(cx, |panel_1, window, cx| {
10929 panel_1.set_position(DockPosition::Right, window, cx)
10930 });
10931
10932 workspace.update_in(cx, |workspace, window, cx| {
10933 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
10934 // Since it was the only panel on the left, the left dock should now be closed.
10935 assert!(!workspace.left_dock().read(cx).is_open());
10936 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
10937 let right_dock = workspace.right_dock();
10938 assert_eq!(
10939 right_dock.read(cx).visible_panel().unwrap().panel_id(),
10940 panel_1.panel_id()
10941 );
10942 assert_eq!(
10943 right_dock.read(cx).active_panel_size(window, cx).unwrap(),
10944 px(1337.)
10945 );
10946
10947 // Now we move panel_2 to the left
10948 panel_2.set_position(DockPosition::Left, window, cx);
10949 });
10950
10951 workspace.update(cx, |workspace, cx| {
10952 // Since panel_2 was not visible on the right, we don't open the left dock.
10953 assert!(!workspace.left_dock().read(cx).is_open());
10954 // And the right dock is unaffected in its displaying of panel_1
10955 assert!(workspace.right_dock().read(cx).is_open());
10956 assert_eq!(
10957 workspace
10958 .right_dock()
10959 .read(cx)
10960 .visible_panel()
10961 .unwrap()
10962 .panel_id(),
10963 panel_1.panel_id(),
10964 );
10965 });
10966
10967 // Move panel_1 back to the left
10968 panel_1.update_in(cx, |panel_1, window, cx| {
10969 panel_1.set_position(DockPosition::Left, window, cx)
10970 });
10971
10972 workspace.update_in(cx, |workspace, window, cx| {
10973 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
10974 let left_dock = workspace.left_dock();
10975 assert!(left_dock.read(cx).is_open());
10976 assert_eq!(
10977 left_dock.read(cx).visible_panel().unwrap().panel_id(),
10978 panel_1.panel_id()
10979 );
10980 assert_eq!(
10981 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
10982 px(1337.)
10983 );
10984 // And the right dock should be closed as it no longer has any panels.
10985 assert!(!workspace.right_dock().read(cx).is_open());
10986
10987 // Now we move panel_1 to the bottom
10988 panel_1.set_position(DockPosition::Bottom, window, cx);
10989 });
10990
10991 workspace.update_in(cx, |workspace, window, cx| {
10992 // Since panel_1 was visible on the left, we close the left dock.
10993 assert!(!workspace.left_dock().read(cx).is_open());
10994 // The bottom dock is sized based on the panel's default size,
10995 // since the panel orientation changed from vertical to horizontal.
10996 let bottom_dock = workspace.bottom_dock();
10997 assert_eq!(
10998 bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
10999 panel_1.size(window, cx),
11000 );
11001 // Close bottom dock and move panel_1 back to the left.
11002 bottom_dock.update(cx, |bottom_dock, cx| {
11003 bottom_dock.set_open(false, window, cx)
11004 });
11005 panel_1.set_position(DockPosition::Left, window, cx);
11006 });
11007
11008 // Emit activated event on panel 1
11009 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
11010
11011 // Now the left dock is open and panel_1 is active and focused.
11012 workspace.update_in(cx, |workspace, window, cx| {
11013 let left_dock = workspace.left_dock();
11014 assert!(left_dock.read(cx).is_open());
11015 assert_eq!(
11016 left_dock.read(cx).visible_panel().unwrap().panel_id(),
11017 panel_1.panel_id(),
11018 );
11019 assert!(panel_1.focus_handle(cx).is_focused(window));
11020 });
11021
11022 // Emit closed event on panel 2, which is not active
11023 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
11024
11025 // Wo don't close the left dock, because panel_2 wasn't the active panel
11026 workspace.update(cx, |workspace, cx| {
11027 let left_dock = workspace.left_dock();
11028 assert!(left_dock.read(cx).is_open());
11029 assert_eq!(
11030 left_dock.read(cx).visible_panel().unwrap().panel_id(),
11031 panel_1.panel_id(),
11032 );
11033 });
11034
11035 // Emitting a ZoomIn event shows the panel as zoomed.
11036 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
11037 workspace.read_with(cx, |workspace, _| {
11038 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
11039 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
11040 });
11041
11042 // Move panel to another dock while it is zoomed
11043 panel_1.update_in(cx, |panel, window, cx| {
11044 panel.set_position(DockPosition::Right, window, cx)
11045 });
11046 workspace.read_with(cx, |workspace, _| {
11047 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
11048
11049 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
11050 });
11051
11052 // This is a helper for getting a:
11053 // - valid focus on an element,
11054 // - that isn't a part of the panes and panels system of the Workspace,
11055 // - and doesn't trigger the 'on_focus_lost' API.
11056 let focus_other_view = {
11057 let workspace = workspace.clone();
11058 move |cx: &mut VisualTestContext| {
11059 workspace.update_in(cx, |workspace, window, cx| {
11060 if workspace.active_modal::<TestModal>(cx).is_some() {
11061 workspace.toggle_modal(window, cx, TestModal::new);
11062 workspace.toggle_modal(window, cx, TestModal::new);
11063 } else {
11064 workspace.toggle_modal(window, cx, TestModal::new);
11065 }
11066 })
11067 }
11068 };
11069
11070 // If focus is transferred to another view that's not a panel or another pane, we still show
11071 // the panel as zoomed.
11072 focus_other_view(cx);
11073 workspace.read_with(cx, |workspace, _| {
11074 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
11075 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
11076 });
11077
11078 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
11079 workspace.update_in(cx, |_workspace, window, cx| {
11080 cx.focus_self(window);
11081 });
11082 workspace.read_with(cx, |workspace, _| {
11083 assert_eq!(workspace.zoomed, None);
11084 assert_eq!(workspace.zoomed_position, None);
11085 });
11086
11087 // If focus is transferred again to another view that's not a panel or a pane, we won't
11088 // show the panel as zoomed because it wasn't zoomed before.
11089 focus_other_view(cx);
11090 workspace.read_with(cx, |workspace, _| {
11091 assert_eq!(workspace.zoomed, None);
11092 assert_eq!(workspace.zoomed_position, None);
11093 });
11094
11095 // When the panel is activated, it is zoomed again.
11096 cx.dispatch_action(ToggleRightDock);
11097 workspace.read_with(cx, |workspace, _| {
11098 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
11099 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
11100 });
11101
11102 // Emitting a ZoomOut event unzooms the panel.
11103 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
11104 workspace.read_with(cx, |workspace, _| {
11105 assert_eq!(workspace.zoomed, None);
11106 assert_eq!(workspace.zoomed_position, None);
11107 });
11108
11109 // Emit closed event on panel 1, which is active
11110 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
11111
11112 // Now the left dock is closed, because panel_1 was the active panel
11113 workspace.update(cx, |workspace, cx| {
11114 let right_dock = workspace.right_dock();
11115 assert!(!right_dock.read(cx).is_open());
11116 });
11117 }
11118
11119 #[gpui::test]
11120 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
11121 init_test(cx);
11122
11123 let fs = FakeFs::new(cx.background_executor.clone());
11124 let project = Project::test(fs, [], cx).await;
11125 let (workspace, cx) =
11126 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11127 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11128
11129 let dirty_regular_buffer = cx.new(|cx| {
11130 TestItem::new(cx)
11131 .with_dirty(true)
11132 .with_label("1.txt")
11133 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
11134 });
11135 let dirty_regular_buffer_2 = cx.new(|cx| {
11136 TestItem::new(cx)
11137 .with_dirty(true)
11138 .with_label("2.txt")
11139 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
11140 });
11141 let dirty_multi_buffer_with_both = cx.new(|cx| {
11142 TestItem::new(cx)
11143 .with_dirty(true)
11144 .with_buffer_kind(ItemBufferKind::Multibuffer)
11145 .with_label("Fake Project Search")
11146 .with_project_items(&[
11147 dirty_regular_buffer.read(cx).project_items[0].clone(),
11148 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
11149 ])
11150 });
11151 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
11152 workspace.update_in(cx, |workspace, window, cx| {
11153 workspace.add_item(
11154 pane.clone(),
11155 Box::new(dirty_regular_buffer.clone()),
11156 None,
11157 false,
11158 false,
11159 window,
11160 cx,
11161 );
11162 workspace.add_item(
11163 pane.clone(),
11164 Box::new(dirty_regular_buffer_2.clone()),
11165 None,
11166 false,
11167 false,
11168 window,
11169 cx,
11170 );
11171 workspace.add_item(
11172 pane.clone(),
11173 Box::new(dirty_multi_buffer_with_both.clone()),
11174 None,
11175 false,
11176 false,
11177 window,
11178 cx,
11179 );
11180 });
11181
11182 pane.update_in(cx, |pane, window, cx| {
11183 pane.activate_item(2, true, true, window, cx);
11184 assert_eq!(
11185 pane.active_item().unwrap().item_id(),
11186 multi_buffer_with_both_files_id,
11187 "Should select the multi buffer in the pane"
11188 );
11189 });
11190 let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
11191 pane.close_other_items(
11192 &CloseOtherItems {
11193 save_intent: Some(SaveIntent::Save),
11194 close_pinned: true,
11195 },
11196 None,
11197 window,
11198 cx,
11199 )
11200 });
11201 cx.background_executor.run_until_parked();
11202 assert!(!cx.has_pending_prompt());
11203 close_all_but_multi_buffer_task
11204 .await
11205 .expect("Closing all buffers but the multi buffer failed");
11206 pane.update(cx, |pane, cx| {
11207 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
11208 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
11209 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
11210 assert_eq!(pane.items_len(), 1);
11211 assert_eq!(
11212 pane.active_item().unwrap().item_id(),
11213 multi_buffer_with_both_files_id,
11214 "Should have only the multi buffer left in the pane"
11215 );
11216 assert!(
11217 dirty_multi_buffer_with_both.read(cx).is_dirty,
11218 "The multi buffer containing the unsaved buffer should still be dirty"
11219 );
11220 });
11221
11222 dirty_regular_buffer.update(cx, |buffer, cx| {
11223 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
11224 });
11225
11226 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
11227 pane.close_active_item(
11228 &CloseActiveItem {
11229 save_intent: Some(SaveIntent::Close),
11230 close_pinned: false,
11231 },
11232 window,
11233 cx,
11234 )
11235 });
11236 cx.background_executor.run_until_parked();
11237 assert!(
11238 cx.has_pending_prompt(),
11239 "Dirty multi buffer should prompt a save dialog"
11240 );
11241 cx.simulate_prompt_answer("Save");
11242 cx.background_executor.run_until_parked();
11243 close_multi_buffer_task
11244 .await
11245 .expect("Closing the multi buffer failed");
11246 pane.update(cx, |pane, cx| {
11247 assert_eq!(
11248 dirty_multi_buffer_with_both.read(cx).save_count,
11249 1,
11250 "Multi buffer item should get be saved"
11251 );
11252 // Test impl does not save inner items, so we do not assert them
11253 assert_eq!(
11254 pane.items_len(),
11255 0,
11256 "No more items should be left in the pane"
11257 );
11258 assert!(pane.active_item().is_none());
11259 });
11260 }
11261
11262 #[gpui::test]
11263 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
11264 cx: &mut TestAppContext,
11265 ) {
11266 init_test(cx);
11267
11268 let fs = FakeFs::new(cx.background_executor.clone());
11269 let project = Project::test(fs, [], cx).await;
11270 let (workspace, cx) =
11271 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11272 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11273
11274 let dirty_regular_buffer = cx.new(|cx| {
11275 TestItem::new(cx)
11276 .with_dirty(true)
11277 .with_label("1.txt")
11278 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
11279 });
11280 let dirty_regular_buffer_2 = cx.new(|cx| {
11281 TestItem::new(cx)
11282 .with_dirty(true)
11283 .with_label("2.txt")
11284 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
11285 });
11286 let clear_regular_buffer = cx.new(|cx| {
11287 TestItem::new(cx)
11288 .with_label("3.txt")
11289 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
11290 });
11291
11292 let dirty_multi_buffer_with_both = cx.new(|cx| {
11293 TestItem::new(cx)
11294 .with_dirty(true)
11295 .with_buffer_kind(ItemBufferKind::Multibuffer)
11296 .with_label("Fake Project Search")
11297 .with_project_items(&[
11298 dirty_regular_buffer.read(cx).project_items[0].clone(),
11299 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
11300 clear_regular_buffer.read(cx).project_items[0].clone(),
11301 ])
11302 });
11303 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
11304 workspace.update_in(cx, |workspace, window, cx| {
11305 workspace.add_item(
11306 pane.clone(),
11307 Box::new(dirty_regular_buffer.clone()),
11308 None,
11309 false,
11310 false,
11311 window,
11312 cx,
11313 );
11314 workspace.add_item(
11315 pane.clone(),
11316 Box::new(dirty_multi_buffer_with_both.clone()),
11317 None,
11318 false,
11319 false,
11320 window,
11321 cx,
11322 );
11323 });
11324
11325 pane.update_in(cx, |pane, window, cx| {
11326 pane.activate_item(1, true, true, window, cx);
11327 assert_eq!(
11328 pane.active_item().unwrap().item_id(),
11329 multi_buffer_with_both_files_id,
11330 "Should select the multi buffer in the pane"
11331 );
11332 });
11333 let _close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
11334 pane.close_active_item(
11335 &CloseActiveItem {
11336 save_intent: None,
11337 close_pinned: false,
11338 },
11339 window,
11340 cx,
11341 )
11342 });
11343 cx.background_executor.run_until_parked();
11344 assert!(
11345 cx.has_pending_prompt(),
11346 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
11347 );
11348 }
11349
11350 /// Tests that when `close_on_file_delete` is enabled, files are automatically
11351 /// closed when they are deleted from disk.
11352 #[gpui::test]
11353 async fn test_close_on_disk_deletion_enabled(cx: &mut TestAppContext) {
11354 init_test(cx);
11355
11356 // Enable the close_on_disk_deletion setting
11357 cx.update_global(|store: &mut SettingsStore, cx| {
11358 store.update_user_settings(cx, |settings| {
11359 settings.workspace.close_on_file_delete = Some(true);
11360 });
11361 });
11362
11363 let fs = FakeFs::new(cx.background_executor.clone());
11364 let project = Project::test(fs, [], cx).await;
11365 let (workspace, cx) =
11366 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11367 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11368
11369 // Create a test item that simulates a file
11370 let item = cx.new(|cx| {
11371 TestItem::new(cx)
11372 .with_label("test.txt")
11373 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
11374 });
11375
11376 // Add item to workspace
11377 workspace.update_in(cx, |workspace, window, cx| {
11378 workspace.add_item(
11379 pane.clone(),
11380 Box::new(item.clone()),
11381 None,
11382 false,
11383 false,
11384 window,
11385 cx,
11386 );
11387 });
11388
11389 // Verify the item is in the pane
11390 pane.read_with(cx, |pane, _| {
11391 assert_eq!(pane.items().count(), 1);
11392 });
11393
11394 // Simulate file deletion by setting the item's deleted state
11395 item.update(cx, |item, _| {
11396 item.set_has_deleted_file(true);
11397 });
11398
11399 // Emit UpdateTab event to trigger the close behavior
11400 cx.run_until_parked();
11401 item.update(cx, |_, cx| {
11402 cx.emit(ItemEvent::UpdateTab);
11403 });
11404
11405 // Allow the close operation to complete
11406 cx.run_until_parked();
11407
11408 // Verify the item was automatically closed
11409 pane.read_with(cx, |pane, _| {
11410 assert_eq!(
11411 pane.items().count(),
11412 0,
11413 "Item should be automatically closed when file is deleted"
11414 );
11415 });
11416 }
11417
11418 /// Tests that when `close_on_file_delete` is disabled (default), files remain
11419 /// open with a strikethrough when they are deleted from disk.
11420 #[gpui::test]
11421 async fn test_close_on_disk_deletion_disabled(cx: &mut TestAppContext) {
11422 init_test(cx);
11423
11424 // Ensure close_on_disk_deletion is disabled (default)
11425 cx.update_global(|store: &mut SettingsStore, cx| {
11426 store.update_user_settings(cx, |settings| {
11427 settings.workspace.close_on_file_delete = Some(false);
11428 });
11429 });
11430
11431 let fs = FakeFs::new(cx.background_executor.clone());
11432 let project = Project::test(fs, [], cx).await;
11433 let (workspace, cx) =
11434 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11435 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11436
11437 // Create a test item that simulates a file
11438 let item = cx.new(|cx| {
11439 TestItem::new(cx)
11440 .with_label("test.txt")
11441 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
11442 });
11443
11444 // Add item to workspace
11445 workspace.update_in(cx, |workspace, window, cx| {
11446 workspace.add_item(
11447 pane.clone(),
11448 Box::new(item.clone()),
11449 None,
11450 false,
11451 false,
11452 window,
11453 cx,
11454 );
11455 });
11456
11457 // Verify the item is in the pane
11458 pane.read_with(cx, |pane, _| {
11459 assert_eq!(pane.items().count(), 1);
11460 });
11461
11462 // Simulate file deletion
11463 item.update(cx, |item, _| {
11464 item.set_has_deleted_file(true);
11465 });
11466
11467 // Emit UpdateTab event
11468 cx.run_until_parked();
11469 item.update(cx, |_, cx| {
11470 cx.emit(ItemEvent::UpdateTab);
11471 });
11472
11473 // Allow any potential close operation to complete
11474 cx.run_until_parked();
11475
11476 // Verify the item remains open (with strikethrough)
11477 pane.read_with(cx, |pane, _| {
11478 assert_eq!(
11479 pane.items().count(),
11480 1,
11481 "Item should remain open when close_on_disk_deletion is disabled"
11482 );
11483 });
11484
11485 // Verify the item shows as deleted
11486 item.read_with(cx, |item, _| {
11487 assert!(
11488 item.has_deleted_file,
11489 "Item should be marked as having deleted file"
11490 );
11491 });
11492 }
11493
11494 /// Tests that dirty files are not automatically closed when deleted from disk,
11495 /// even when `close_on_file_delete` is enabled. This ensures users don't lose
11496 /// unsaved changes without being prompted.
11497 #[gpui::test]
11498 async fn test_close_on_disk_deletion_with_dirty_file(cx: &mut TestAppContext) {
11499 init_test(cx);
11500
11501 // Enable the close_on_file_delete setting
11502 cx.update_global(|store: &mut SettingsStore, cx| {
11503 store.update_user_settings(cx, |settings| {
11504 settings.workspace.close_on_file_delete = Some(true);
11505 });
11506 });
11507
11508 let fs = FakeFs::new(cx.background_executor.clone());
11509 let project = Project::test(fs, [], cx).await;
11510 let (workspace, cx) =
11511 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11512 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11513
11514 // Create a dirty test item
11515 let item = cx.new(|cx| {
11516 TestItem::new(cx)
11517 .with_dirty(true)
11518 .with_label("test.txt")
11519 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
11520 });
11521
11522 // Add item to workspace
11523 workspace.update_in(cx, |workspace, window, cx| {
11524 workspace.add_item(
11525 pane.clone(),
11526 Box::new(item.clone()),
11527 None,
11528 false,
11529 false,
11530 window,
11531 cx,
11532 );
11533 });
11534
11535 // Simulate file deletion
11536 item.update(cx, |item, _| {
11537 item.set_has_deleted_file(true);
11538 });
11539
11540 // Emit UpdateTab event to trigger the close behavior
11541 cx.run_until_parked();
11542 item.update(cx, |_, cx| {
11543 cx.emit(ItemEvent::UpdateTab);
11544 });
11545
11546 // Allow any potential close operation to complete
11547 cx.run_until_parked();
11548
11549 // Verify the item remains open (dirty files are not auto-closed)
11550 pane.read_with(cx, |pane, _| {
11551 assert_eq!(
11552 pane.items().count(),
11553 1,
11554 "Dirty items should not be automatically closed even when file is deleted"
11555 );
11556 });
11557
11558 // Verify the item is marked as deleted and still dirty
11559 item.read_with(cx, |item, _| {
11560 assert!(
11561 item.has_deleted_file,
11562 "Item should be marked as having deleted file"
11563 );
11564 assert!(item.is_dirty, "Item should still be dirty");
11565 });
11566 }
11567
11568 /// Tests that navigation history is cleaned up when files are auto-closed
11569 /// due to deletion from disk.
11570 #[gpui::test]
11571 async fn test_close_on_disk_deletion_cleans_navigation_history(cx: &mut TestAppContext) {
11572 init_test(cx);
11573
11574 // Enable the close_on_file_delete setting
11575 cx.update_global(|store: &mut SettingsStore, cx| {
11576 store.update_user_settings(cx, |settings| {
11577 settings.workspace.close_on_file_delete = Some(true);
11578 });
11579 });
11580
11581 let fs = FakeFs::new(cx.background_executor.clone());
11582 let project = Project::test(fs, [], cx).await;
11583 let (workspace, cx) =
11584 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11585 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11586
11587 // Create test items
11588 let item1 = cx.new(|cx| {
11589 TestItem::new(cx)
11590 .with_label("test1.txt")
11591 .with_project_items(&[TestProjectItem::new(1, "test1.txt", cx)])
11592 });
11593 let item1_id = item1.item_id();
11594
11595 let item2 = cx.new(|cx| {
11596 TestItem::new(cx)
11597 .with_label("test2.txt")
11598 .with_project_items(&[TestProjectItem::new(2, "test2.txt", cx)])
11599 });
11600
11601 // Add items to workspace
11602 workspace.update_in(cx, |workspace, window, cx| {
11603 workspace.add_item(
11604 pane.clone(),
11605 Box::new(item1.clone()),
11606 None,
11607 false,
11608 false,
11609 window,
11610 cx,
11611 );
11612 workspace.add_item(
11613 pane.clone(),
11614 Box::new(item2.clone()),
11615 None,
11616 false,
11617 false,
11618 window,
11619 cx,
11620 );
11621 });
11622
11623 // Activate item1 to ensure it gets navigation entries
11624 pane.update_in(cx, |pane, window, cx| {
11625 pane.activate_item(0, true, true, window, cx);
11626 });
11627
11628 // Switch to item2 and back to create navigation history
11629 pane.update_in(cx, |pane, window, cx| {
11630 pane.activate_item(1, true, true, window, cx);
11631 });
11632 cx.run_until_parked();
11633
11634 pane.update_in(cx, |pane, window, cx| {
11635 pane.activate_item(0, true, true, window, cx);
11636 });
11637 cx.run_until_parked();
11638
11639 // Simulate file deletion for item1
11640 item1.update(cx, |item, _| {
11641 item.set_has_deleted_file(true);
11642 });
11643
11644 // Emit UpdateTab event to trigger the close behavior
11645 item1.update(cx, |_, cx| {
11646 cx.emit(ItemEvent::UpdateTab);
11647 });
11648 cx.run_until_parked();
11649
11650 // Verify item1 was closed
11651 pane.read_with(cx, |pane, _| {
11652 assert_eq!(
11653 pane.items().count(),
11654 1,
11655 "Should have 1 item remaining after auto-close"
11656 );
11657 });
11658
11659 // Check navigation history after close
11660 let has_item = pane.read_with(cx, |pane, cx| {
11661 let mut has_item = false;
11662 pane.nav_history().for_each_entry(cx, |entry, _| {
11663 if entry.item.id() == item1_id {
11664 has_item = true;
11665 }
11666 });
11667 has_item
11668 });
11669
11670 assert!(
11671 !has_item,
11672 "Navigation history should not contain closed item entries"
11673 );
11674 }
11675
11676 #[gpui::test]
11677 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
11678 cx: &mut TestAppContext,
11679 ) {
11680 init_test(cx);
11681
11682 let fs = FakeFs::new(cx.background_executor.clone());
11683 let project = Project::test(fs, [], cx).await;
11684 let (workspace, cx) =
11685 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11686 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11687
11688 let dirty_regular_buffer = cx.new(|cx| {
11689 TestItem::new(cx)
11690 .with_dirty(true)
11691 .with_label("1.txt")
11692 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
11693 });
11694 let dirty_regular_buffer_2 = cx.new(|cx| {
11695 TestItem::new(cx)
11696 .with_dirty(true)
11697 .with_label("2.txt")
11698 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
11699 });
11700 let clear_regular_buffer = cx.new(|cx| {
11701 TestItem::new(cx)
11702 .with_label("3.txt")
11703 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
11704 });
11705
11706 let dirty_multi_buffer = cx.new(|cx| {
11707 TestItem::new(cx)
11708 .with_dirty(true)
11709 .with_buffer_kind(ItemBufferKind::Multibuffer)
11710 .with_label("Fake Project Search")
11711 .with_project_items(&[
11712 dirty_regular_buffer.read(cx).project_items[0].clone(),
11713 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
11714 clear_regular_buffer.read(cx).project_items[0].clone(),
11715 ])
11716 });
11717 workspace.update_in(cx, |workspace, window, cx| {
11718 workspace.add_item(
11719 pane.clone(),
11720 Box::new(dirty_regular_buffer.clone()),
11721 None,
11722 false,
11723 false,
11724 window,
11725 cx,
11726 );
11727 workspace.add_item(
11728 pane.clone(),
11729 Box::new(dirty_regular_buffer_2.clone()),
11730 None,
11731 false,
11732 false,
11733 window,
11734 cx,
11735 );
11736 workspace.add_item(
11737 pane.clone(),
11738 Box::new(dirty_multi_buffer.clone()),
11739 None,
11740 false,
11741 false,
11742 window,
11743 cx,
11744 );
11745 });
11746
11747 pane.update_in(cx, |pane, window, cx| {
11748 pane.activate_item(2, true, true, window, cx);
11749 assert_eq!(
11750 pane.active_item().unwrap().item_id(),
11751 dirty_multi_buffer.item_id(),
11752 "Should select the multi buffer in the pane"
11753 );
11754 });
11755 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
11756 pane.close_active_item(
11757 &CloseActiveItem {
11758 save_intent: None,
11759 close_pinned: false,
11760 },
11761 window,
11762 cx,
11763 )
11764 });
11765 cx.background_executor.run_until_parked();
11766 assert!(
11767 !cx.has_pending_prompt(),
11768 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
11769 );
11770 close_multi_buffer_task
11771 .await
11772 .expect("Closing multi buffer failed");
11773 pane.update(cx, |pane, cx| {
11774 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
11775 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
11776 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
11777 assert_eq!(
11778 pane.items()
11779 .map(|item| item.item_id())
11780 .sorted()
11781 .collect::<Vec<_>>(),
11782 vec![
11783 dirty_regular_buffer.item_id(),
11784 dirty_regular_buffer_2.item_id(),
11785 ],
11786 "Should have no multi buffer left in the pane"
11787 );
11788 assert!(dirty_regular_buffer.read(cx).is_dirty);
11789 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
11790 });
11791 }
11792
11793 #[gpui::test]
11794 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
11795 init_test(cx);
11796 let fs = FakeFs::new(cx.executor());
11797 let project = Project::test(fs, [], cx).await;
11798 let (workspace, cx) =
11799 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11800
11801 // Add a new panel to the right dock, opening the dock and setting the
11802 // focus to the new panel.
11803 let panel = workspace.update_in(cx, |workspace, window, cx| {
11804 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
11805 workspace.add_panel(panel.clone(), window, cx);
11806
11807 workspace
11808 .right_dock()
11809 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
11810
11811 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11812
11813 panel
11814 });
11815
11816 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
11817 // panel to the next valid position which, in this case, is the left
11818 // dock.
11819 cx.dispatch_action(MoveFocusedPanelToNextPosition);
11820 workspace.update(cx, |workspace, cx| {
11821 assert!(workspace.left_dock().read(cx).is_open());
11822 assert_eq!(panel.read(cx).position, DockPosition::Left);
11823 });
11824
11825 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
11826 // panel to the next valid position which, in this case, is the bottom
11827 // dock.
11828 cx.dispatch_action(MoveFocusedPanelToNextPosition);
11829 workspace.update(cx, |workspace, cx| {
11830 assert!(workspace.bottom_dock().read(cx).is_open());
11831 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
11832 });
11833
11834 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
11835 // around moving the panel to its initial position, the right dock.
11836 cx.dispatch_action(MoveFocusedPanelToNextPosition);
11837 workspace.update(cx, |workspace, cx| {
11838 assert!(workspace.right_dock().read(cx).is_open());
11839 assert_eq!(panel.read(cx).position, DockPosition::Right);
11840 });
11841
11842 // Remove focus from the panel, ensuring that, if the panel is not
11843 // focused, the `MoveFocusedPanelToNextPosition` action does not update
11844 // the panel's position, so the panel is still in the right dock.
11845 workspace.update_in(cx, |workspace, window, cx| {
11846 workspace.toggle_panel_focus::<TestPanel>(window, cx);
11847 });
11848
11849 cx.dispatch_action(MoveFocusedPanelToNextPosition);
11850 workspace.update(cx, |workspace, cx| {
11851 assert!(workspace.right_dock().read(cx).is_open());
11852 assert_eq!(panel.read(cx).position, DockPosition::Right);
11853 });
11854 }
11855
11856 #[gpui::test]
11857 async fn test_moving_items_create_panes(cx: &mut TestAppContext) {
11858 init_test(cx);
11859
11860 let fs = FakeFs::new(cx.executor());
11861 let project = Project::test(fs, [], cx).await;
11862 let (workspace, cx) =
11863 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
11864
11865 let item_1 = cx.new(|cx| {
11866 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
11867 });
11868 workspace.update_in(cx, |workspace, window, cx| {
11869 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
11870 workspace.move_item_to_pane_in_direction(
11871 &MoveItemToPaneInDirection {
11872 direction: SplitDirection::Right,
11873 focus: true,
11874 clone: false,
11875 },
11876 window,
11877 cx,
11878 );
11879 workspace.move_item_to_pane_at_index(
11880 &MoveItemToPane {
11881 destination: 3,
11882 focus: true,
11883 clone: false,
11884 },
11885 window,
11886 cx,
11887 );
11888
11889 assert_eq!(workspace.panes.len(), 1, "No new panes were created");
11890 assert_eq!(
11891 pane_items_paths(&workspace.active_pane, cx),
11892 vec!["first.txt".to_string()],
11893 "Single item was not moved anywhere"
11894 );
11895 });
11896
11897 let item_2 = cx.new(|cx| {
11898 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "second.txt", cx)])
11899 });
11900 workspace.update_in(cx, |workspace, window, cx| {
11901 workspace.add_item_to_active_pane(Box::new(item_2), None, true, window, cx);
11902 assert_eq!(
11903 pane_items_paths(&workspace.panes[0], cx),
11904 vec!["first.txt".to_string(), "second.txt".to_string()],
11905 );
11906 workspace.move_item_to_pane_in_direction(
11907 &MoveItemToPaneInDirection {
11908 direction: SplitDirection::Right,
11909 focus: true,
11910 clone: false,
11911 },
11912 window,
11913 cx,
11914 );
11915
11916 assert_eq!(workspace.panes.len(), 2, "A new pane should be created");
11917 assert_eq!(
11918 pane_items_paths(&workspace.panes[0], cx),
11919 vec!["first.txt".to_string()],
11920 "After moving, one item should be left in the original pane"
11921 );
11922 assert_eq!(
11923 pane_items_paths(&workspace.panes[1], cx),
11924 vec!["second.txt".to_string()],
11925 "New item should have been moved to the new pane"
11926 );
11927 });
11928
11929 let item_3 = cx.new(|cx| {
11930 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "third.txt", cx)])
11931 });
11932 workspace.update_in(cx, |workspace, window, cx| {
11933 let original_pane = workspace.panes[0].clone();
11934 workspace.set_active_pane(&original_pane, window, cx);
11935 workspace.add_item_to_active_pane(Box::new(item_3), None, true, window, cx);
11936 assert_eq!(workspace.panes.len(), 2, "No new panes were created");
11937 assert_eq!(
11938 pane_items_paths(&workspace.active_pane, cx),
11939 vec!["first.txt".to_string(), "third.txt".to_string()],
11940 "New pane should be ready to move one item out"
11941 );
11942
11943 workspace.move_item_to_pane_at_index(
11944 &MoveItemToPane {
11945 destination: 3,
11946 focus: true,
11947 clone: false,
11948 },
11949 window,
11950 cx,
11951 );
11952 assert_eq!(workspace.panes.len(), 3, "A new pane should be created");
11953 assert_eq!(
11954 pane_items_paths(&workspace.active_pane, cx),
11955 vec!["first.txt".to_string()],
11956 "After moving, one item should be left in the original pane"
11957 );
11958 assert_eq!(
11959 pane_items_paths(&workspace.panes[1], cx),
11960 vec!["second.txt".to_string()],
11961 "Previously created pane should be unchanged"
11962 );
11963 assert_eq!(
11964 pane_items_paths(&workspace.panes[2], cx),
11965 vec!["third.txt".to_string()],
11966 "New item should have been moved to the new pane"
11967 );
11968 });
11969 }
11970
11971 #[gpui::test]
11972 async fn test_moving_items_can_clone_panes(cx: &mut TestAppContext) {
11973 init_test(cx);
11974
11975 let fs = FakeFs::new(cx.executor());
11976 let project = Project::test(fs, [], cx).await;
11977 let (workspace, cx) =
11978 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
11979
11980 let item_1 = cx.new(|cx| {
11981 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
11982 });
11983 workspace.update_in(cx, |workspace, window, cx| {
11984 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
11985 workspace.move_item_to_pane_in_direction(
11986 &MoveItemToPaneInDirection {
11987 direction: SplitDirection::Right,
11988 focus: true,
11989 clone: true,
11990 },
11991 window,
11992 cx,
11993 );
11994 });
11995 cx.run_until_parked();
11996 workspace.update_in(cx, |workspace, window, cx| {
11997 workspace.move_item_to_pane_at_index(
11998 &MoveItemToPane {
11999 destination: 3,
12000 focus: true,
12001 clone: true,
12002 },
12003 window,
12004 cx,
12005 );
12006 });
12007 cx.run_until_parked();
12008
12009 workspace.update(cx, |workspace, cx| {
12010 assert_eq!(workspace.panes.len(), 3, "Two new panes were created");
12011 for pane in workspace.panes() {
12012 assert_eq!(
12013 pane_items_paths(pane, cx),
12014 vec!["first.txt".to_string()],
12015 "Single item exists in all panes"
12016 );
12017 }
12018 });
12019
12020 // verify that the active pane has been updated after waiting for the
12021 // pane focus event to fire and resolve
12022 workspace.read_with(cx, |workspace, _app| {
12023 assert_eq!(
12024 workspace.active_pane(),
12025 &workspace.panes[2],
12026 "The third pane should be the active one: {:?}",
12027 workspace.panes
12028 );
12029 })
12030 }
12031
12032 mod register_project_item_tests {
12033
12034 use super::*;
12035
12036 // View
12037 struct TestPngItemView {
12038 focus_handle: FocusHandle,
12039 }
12040 // Model
12041 struct TestPngItem {}
12042
12043 impl project::ProjectItem for TestPngItem {
12044 fn try_open(
12045 _project: &Entity<Project>,
12046 path: &ProjectPath,
12047 cx: &mut App,
12048 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
12049 if path.path.extension().unwrap() == "png" {
12050 Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestPngItem {}))))
12051 } else {
12052 None
12053 }
12054 }
12055
12056 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
12057 None
12058 }
12059
12060 fn project_path(&self, _: &App) -> Option<ProjectPath> {
12061 None
12062 }
12063
12064 fn is_dirty(&self) -> bool {
12065 false
12066 }
12067 }
12068
12069 impl Item for TestPngItemView {
12070 type Event = ();
12071 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
12072 "".into()
12073 }
12074 }
12075 impl EventEmitter<()> for TestPngItemView {}
12076 impl Focusable for TestPngItemView {
12077 fn focus_handle(&self, _cx: &App) -> FocusHandle {
12078 self.focus_handle.clone()
12079 }
12080 }
12081
12082 impl Render for TestPngItemView {
12083 fn render(
12084 &mut self,
12085 _window: &mut Window,
12086 _cx: &mut Context<Self>,
12087 ) -> impl IntoElement {
12088 Empty
12089 }
12090 }
12091
12092 impl ProjectItem for TestPngItemView {
12093 type Item = TestPngItem;
12094
12095 fn for_project_item(
12096 _project: Entity<Project>,
12097 _pane: Option<&Pane>,
12098 _item: Entity<Self::Item>,
12099 _: &mut Window,
12100 cx: &mut Context<Self>,
12101 ) -> Self
12102 where
12103 Self: Sized,
12104 {
12105 Self {
12106 focus_handle: cx.focus_handle(),
12107 }
12108 }
12109 }
12110
12111 // View
12112 struct TestIpynbItemView {
12113 focus_handle: FocusHandle,
12114 }
12115 // Model
12116 struct TestIpynbItem {}
12117
12118 impl project::ProjectItem for TestIpynbItem {
12119 fn try_open(
12120 _project: &Entity<Project>,
12121 path: &ProjectPath,
12122 cx: &mut App,
12123 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
12124 if path.path.extension().unwrap() == "ipynb" {
12125 Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestIpynbItem {}))))
12126 } else {
12127 None
12128 }
12129 }
12130
12131 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
12132 None
12133 }
12134
12135 fn project_path(&self, _: &App) -> Option<ProjectPath> {
12136 None
12137 }
12138
12139 fn is_dirty(&self) -> bool {
12140 false
12141 }
12142 }
12143
12144 impl Item for TestIpynbItemView {
12145 type Event = ();
12146 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
12147 "".into()
12148 }
12149 }
12150 impl EventEmitter<()> for TestIpynbItemView {}
12151 impl Focusable for TestIpynbItemView {
12152 fn focus_handle(&self, _cx: &App) -> FocusHandle {
12153 self.focus_handle.clone()
12154 }
12155 }
12156
12157 impl Render for TestIpynbItemView {
12158 fn render(
12159 &mut self,
12160 _window: &mut Window,
12161 _cx: &mut Context<Self>,
12162 ) -> impl IntoElement {
12163 Empty
12164 }
12165 }
12166
12167 impl ProjectItem for TestIpynbItemView {
12168 type Item = TestIpynbItem;
12169
12170 fn for_project_item(
12171 _project: Entity<Project>,
12172 _pane: Option<&Pane>,
12173 _item: Entity<Self::Item>,
12174 _: &mut Window,
12175 cx: &mut Context<Self>,
12176 ) -> Self
12177 where
12178 Self: Sized,
12179 {
12180 Self {
12181 focus_handle: cx.focus_handle(),
12182 }
12183 }
12184 }
12185
12186 struct TestAlternatePngItemView {
12187 focus_handle: FocusHandle,
12188 }
12189
12190 impl Item for TestAlternatePngItemView {
12191 type Event = ();
12192 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
12193 "".into()
12194 }
12195 }
12196
12197 impl EventEmitter<()> for TestAlternatePngItemView {}
12198 impl Focusable for TestAlternatePngItemView {
12199 fn focus_handle(&self, _cx: &App) -> FocusHandle {
12200 self.focus_handle.clone()
12201 }
12202 }
12203
12204 impl Render for TestAlternatePngItemView {
12205 fn render(
12206 &mut self,
12207 _window: &mut Window,
12208 _cx: &mut Context<Self>,
12209 ) -> impl IntoElement {
12210 Empty
12211 }
12212 }
12213
12214 impl ProjectItem for TestAlternatePngItemView {
12215 type Item = TestPngItem;
12216
12217 fn for_project_item(
12218 _project: Entity<Project>,
12219 _pane: Option<&Pane>,
12220 _item: Entity<Self::Item>,
12221 _: &mut Window,
12222 cx: &mut Context<Self>,
12223 ) -> Self
12224 where
12225 Self: Sized,
12226 {
12227 Self {
12228 focus_handle: cx.focus_handle(),
12229 }
12230 }
12231 }
12232
12233 #[gpui::test]
12234 async fn test_register_project_item(cx: &mut TestAppContext) {
12235 init_test(cx);
12236
12237 cx.update(|cx| {
12238 register_project_item::<TestPngItemView>(cx);
12239 register_project_item::<TestIpynbItemView>(cx);
12240 });
12241
12242 let fs = FakeFs::new(cx.executor());
12243 fs.insert_tree(
12244 "/root1",
12245 json!({
12246 "one.png": "BINARYDATAHERE",
12247 "two.ipynb": "{ totally a notebook }",
12248 "three.txt": "editing text, sure why not?"
12249 }),
12250 )
12251 .await;
12252
12253 let project = Project::test(fs, ["root1".as_ref()], cx).await;
12254 let (workspace, cx) =
12255 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
12256
12257 let worktree_id = project.update(cx, |project, cx| {
12258 project.worktrees(cx).next().unwrap().read(cx).id()
12259 });
12260
12261 let handle = workspace
12262 .update_in(cx, |workspace, window, cx| {
12263 let project_path = (worktree_id, rel_path("one.png"));
12264 workspace.open_path(project_path, None, true, window, cx)
12265 })
12266 .await
12267 .unwrap();
12268
12269 // Now we can check if the handle we got back errored or not
12270 assert_eq!(
12271 handle.to_any_view().entity_type(),
12272 TypeId::of::<TestPngItemView>()
12273 );
12274
12275 let handle = workspace
12276 .update_in(cx, |workspace, window, cx| {
12277 let project_path = (worktree_id, rel_path("two.ipynb"));
12278 workspace.open_path(project_path, None, true, window, cx)
12279 })
12280 .await
12281 .unwrap();
12282
12283 assert_eq!(
12284 handle.to_any_view().entity_type(),
12285 TypeId::of::<TestIpynbItemView>()
12286 );
12287
12288 let handle = workspace
12289 .update_in(cx, |workspace, window, cx| {
12290 let project_path = (worktree_id, rel_path("three.txt"));
12291 workspace.open_path(project_path, None, true, window, cx)
12292 })
12293 .await;
12294 assert!(handle.is_err());
12295 }
12296
12297 #[gpui::test]
12298 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
12299 init_test(cx);
12300
12301 cx.update(|cx| {
12302 register_project_item::<TestPngItemView>(cx);
12303 register_project_item::<TestAlternatePngItemView>(cx);
12304 });
12305
12306 let fs = FakeFs::new(cx.executor());
12307 fs.insert_tree(
12308 "/root1",
12309 json!({
12310 "one.png": "BINARYDATAHERE",
12311 "two.ipynb": "{ totally a notebook }",
12312 "three.txt": "editing text, sure why not?"
12313 }),
12314 )
12315 .await;
12316 let project = Project::test(fs, ["root1".as_ref()], cx).await;
12317 let (workspace, cx) =
12318 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
12319 let worktree_id = project.update(cx, |project, cx| {
12320 project.worktrees(cx).next().unwrap().read(cx).id()
12321 });
12322
12323 let handle = workspace
12324 .update_in(cx, |workspace, window, cx| {
12325 let project_path = (worktree_id, rel_path("one.png"));
12326 workspace.open_path(project_path, None, true, window, cx)
12327 })
12328 .await
12329 .unwrap();
12330
12331 // This _must_ be the second item registered
12332 assert_eq!(
12333 handle.to_any_view().entity_type(),
12334 TypeId::of::<TestAlternatePngItemView>()
12335 );
12336
12337 let handle = workspace
12338 .update_in(cx, |workspace, window, cx| {
12339 let project_path = (worktree_id, rel_path("three.txt"));
12340 workspace.open_path(project_path, None, true, window, cx)
12341 })
12342 .await;
12343 assert!(handle.is_err());
12344 }
12345 }
12346
12347 #[gpui::test]
12348 async fn test_status_bar_visibility(cx: &mut TestAppContext) {
12349 init_test(cx);
12350
12351 let fs = FakeFs::new(cx.executor());
12352 let project = Project::test(fs, [], cx).await;
12353 let (workspace, _cx) =
12354 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
12355
12356 // Test with status bar shown (default)
12357 workspace.read_with(cx, |workspace, cx| {
12358 let visible = workspace.status_bar_visible(cx);
12359 assert!(visible, "Status bar should be visible by default");
12360 });
12361
12362 // Test with status bar hidden
12363 cx.update_global(|store: &mut SettingsStore, cx| {
12364 store.update_user_settings(cx, |settings| {
12365 settings.status_bar.get_or_insert_default().show = Some(false);
12366 });
12367 });
12368
12369 workspace.read_with(cx, |workspace, cx| {
12370 let visible = workspace.status_bar_visible(cx);
12371 assert!(!visible, "Status bar should be hidden when show is false");
12372 });
12373
12374 // Test with status bar shown explicitly
12375 cx.update_global(|store: &mut SettingsStore, cx| {
12376 store.update_user_settings(cx, |settings| {
12377 settings.status_bar.get_or_insert_default().show = Some(true);
12378 });
12379 });
12380
12381 workspace.read_with(cx, |workspace, cx| {
12382 let visible = workspace.status_bar_visible(cx);
12383 assert!(visible, "Status bar should be visible when show is true");
12384 });
12385 }
12386
12387 #[gpui::test]
12388 async fn test_pane_close_active_item(cx: &mut TestAppContext) {
12389 init_test(cx);
12390
12391 let fs = FakeFs::new(cx.executor());
12392 let project = Project::test(fs, [], cx).await;
12393 let (workspace, cx) =
12394 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12395 let panel = workspace.update_in(cx, |workspace, window, cx| {
12396 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
12397 workspace.add_panel(panel.clone(), window, cx);
12398
12399 workspace
12400 .right_dock()
12401 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
12402
12403 panel
12404 });
12405
12406 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12407 let item_a = cx.new(TestItem::new);
12408 let item_b = cx.new(TestItem::new);
12409 let item_a_id = item_a.entity_id();
12410 let item_b_id = item_b.entity_id();
12411
12412 pane.update_in(cx, |pane, window, cx| {
12413 pane.add_item(Box::new(item_a.clone()), true, true, None, window, cx);
12414 pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
12415 });
12416
12417 pane.read_with(cx, |pane, _| {
12418 assert_eq!(pane.items_len(), 2);
12419 assert_eq!(pane.active_item().unwrap().item_id(), item_b_id);
12420 });
12421
12422 workspace.update_in(cx, |workspace, window, cx| {
12423 workspace.toggle_panel_focus::<TestPanel>(window, cx);
12424 });
12425
12426 workspace.update_in(cx, |_, window, cx| {
12427 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12428 });
12429
12430 // Assert that the `pane::CloseActiveItem` action is handled at the
12431 // workspace level when one of the dock panels is focused and, in that
12432 // case, the center pane's active item is closed but the focus is not
12433 // moved.
12434 cx.dispatch_action(pane::CloseActiveItem::default());
12435 cx.run_until_parked();
12436
12437 pane.read_with(cx, |pane, _| {
12438 assert_eq!(pane.items_len(), 1);
12439 assert_eq!(pane.active_item().unwrap().item_id(), item_a_id);
12440 });
12441
12442 workspace.update_in(cx, |workspace, window, cx| {
12443 assert!(workspace.right_dock().read(cx).is_open());
12444 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12445 });
12446 }
12447
12448 fn pane_items_paths(pane: &Entity<Pane>, cx: &App) -> Vec<String> {
12449 pane.read(cx)
12450 .items()
12451 .flat_map(|item| {
12452 item.project_paths(cx)
12453 .into_iter()
12454 .map(|path| path.path.display(PathStyle::local()).into_owned())
12455 })
12456 .collect()
12457 }
12458
12459 pub fn init_test(cx: &mut TestAppContext) {
12460 cx.update(|cx| {
12461 let settings_store = SettingsStore::test(cx);
12462 cx.set_global(settings_store);
12463 theme::init(theme::LoadThemes::JustBase, cx);
12464 });
12465 }
12466
12467 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
12468 let item = TestProjectItem::new(id, path, cx);
12469 item.update(cx, |item, _| {
12470 item.is_dirty = true;
12471 });
12472 item
12473 }
12474}