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