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