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