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