1pub mod dock;
2pub mod history_manager;
3pub mod item;
4mod modal_layer;
5pub mod notifications;
6pub mod pane;
7pub mod pane_group;
8mod persistence;
9pub mod searchable;
10pub mod shared_screen;
11mod status_bar;
12pub mod tasks;
13mod theme_preview;
14mod toast_layer;
15mod toolbar;
16mod workspace_settings;
17
18pub use toast_layer::{ToastAction, ToastLayer, ToastView};
19
20use anyhow::{Context as _, Result, anyhow};
21use call::{ActiveCall, call_settings::CallSettings};
22use client::{
23 ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore,
24 proto::{self, ErrorCode, PanelId, PeerId},
25};
26use collections::{HashMap, HashSet, hash_map};
27pub use dock::Panel;
28use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
29use futures::{
30 Future, FutureExt, StreamExt,
31 channel::{
32 mpsc::{self, UnboundedReceiver, UnboundedSender},
33 oneshot,
34 },
35 future::try_join_all,
36};
37use gpui::{
38 Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context,
39 CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
40 Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView, MouseButton,
41 PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, Task,
42 Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId, WindowOptions, actions, canvas,
43 point, relative, size, transparent_black,
44};
45pub use history_manager::*;
46pub use item::{
47 FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
48 ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
49};
50use itertools::Itertools;
51use language::{Buffer, LanguageRegistry, Rope};
52pub use modal_layer::*;
53use node_runtime::NodeRuntime;
54use notifications::{
55 DetachAndPromptErr, Notifications, dismiss_app_notification,
56 simple_message_notification::MessageNotification,
57};
58pub use pane::*;
59pub use pane_group::*;
60use persistence::{
61 DB, SerializedWindowBounds,
62 model::{SerializedSshProject, SerializedWorkspace},
63};
64pub use persistence::{
65 DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items,
66 model::{ItemId, LocalPaths, SerializedWorkspaceLocation},
67};
68use postage::stream::Stream;
69use project::{
70 DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
71 debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
72};
73use remote::{SshClientDelegate, SshConnectionOptions, ssh_session::ConnectionIdentifier};
74use schemars::JsonSchema;
75use serde::Deserialize;
76use session::AppSession;
77use settings::Settings;
78use shared_screen::SharedScreen;
79use sqlez::{
80 bindable::{Bind, Column, StaticColumnCount},
81 statement::Statement,
82};
83use status_bar::StatusBar;
84pub use status_bar::StatusItemView;
85use std::{
86 any::TypeId,
87 borrow::Cow,
88 cell::RefCell,
89 cmp,
90 collections::hash_map::DefaultHasher,
91 env,
92 hash::{Hash, Hasher},
93 path::{Path, PathBuf},
94 process::ExitStatus,
95 rc::Rc,
96 sync::{Arc, LazyLock, Weak, atomic::AtomicUsize},
97 time::Duration,
98};
99use task::{DebugScenario, SpawnInTerminal, TaskContext};
100use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
101pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
102pub use ui;
103use ui::{Window, prelude::*};
104use util::{ResultExt, TryFutureExt, paths::SanitizedPath, serde::default_true};
105use uuid::Uuid;
106pub use workspace_settings::{
107 AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
108};
109use zed_actions::{Spawn, feedback::FileBugReport};
110
111use crate::notifications::NotificationId;
112use crate::persistence::{
113 SerializedAxis,
114 model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
115};
116
117pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200);
118
119static ZED_WINDOW_SIZE: LazyLock<Option<Size<Pixels>>> = LazyLock::new(|| {
120 env::var("ZED_WINDOW_SIZE")
121 .ok()
122 .as_deref()
123 .and_then(parse_pixel_size_env_var)
124});
125
126static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
127 env::var("ZED_WINDOW_POSITION")
128 .ok()
129 .as_deref()
130 .and_then(parse_pixel_position_env_var)
131});
132
133pub trait TerminalProvider {
134 fn spawn(
135 &self,
136 task: SpawnInTerminal,
137 window: &mut Window,
138 cx: &mut App,
139 ) -> Task<Option<Result<ExitStatus>>>;
140}
141
142pub trait DebuggerProvider {
143 // `active_buffer` is used to resolve build task's name against language-specific tasks.
144 fn start_session(
145 &self,
146 definition: DebugScenario,
147 task_context: TaskContext,
148 active_buffer: Option<Entity<Buffer>>,
149 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, Action)]
217#[action(namespace = workspace)]
218pub struct ActivatePane(pub usize);
219
220#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
221#[action(namespace = workspace)]
222#[serde(deny_unknown_fields)]
223pub struct MoveItemToPane {
224 pub destination: usize,
225 #[serde(default = "default_true")]
226 pub focus: bool,
227 #[serde(default)]
228 pub clone: bool,
229}
230
231#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
232#[action(namespace = workspace)]
233#[serde(deny_unknown_fields)]
234pub struct MoveItemToPaneInDirection {
235 pub direction: SplitDirection,
236 #[serde(default = "default_true")]
237 pub focus: bool,
238 #[serde(default)]
239 pub clone: bool,
240}
241
242#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
243#[action(namespace = workspace)]
244#[serde(deny_unknown_fields)]
245pub struct SaveAll {
246 pub save_intent: Option<SaveIntent>,
247}
248
249#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
250#[action(namespace = workspace)]
251#[serde(deny_unknown_fields)]
252pub struct Save {
253 pub save_intent: Option<SaveIntent>,
254}
255
256#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
257#[action(namespace = workspace)]
258#[serde(deny_unknown_fields)]
259pub struct CloseAllItemsAndPanes {
260 pub save_intent: Option<SaveIntent>,
261}
262
263#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
264#[action(namespace = workspace)]
265#[serde(deny_unknown_fields)]
266pub struct CloseInactiveTabsAndPanes {
267 pub save_intent: Option<SaveIntent>,
268}
269
270#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
271#[action(namespace = workspace)]
272pub struct SendKeystrokes(pub String);
273
274#[derive(Clone, Deserialize, PartialEq, Default, JsonSchema, Action)]
275#[action(namespace = workspace)]
276#[serde(deny_unknown_fields)]
277pub struct Reload {
278 pub binary_path: Option<PathBuf>,
279}
280
281actions!(
282 project_symbols,
283 [
284 #[action(name = "Toggle")]
285 ToggleProjectSymbols
286 ]
287);
288
289#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
290#[action(namespace = file_finder, name = "Toggle")]
291#[serde(deny_unknown_fields)]
292pub struct ToggleFileFinder {
293 #[serde(default)]
294 pub separate_history: bool,
295}
296
297actions!(
298 workspace,
299 [
300 ActivatePaneLeft,
301 ActivatePaneRight,
302 ActivatePaneUp,
303 ActivatePaneDown,
304 SwapPaneLeft,
305 SwapPaneRight,
306 SwapPaneUp,
307 SwapPaneDown,
308 ]
309);
310
311#[derive(PartialEq, Eq, Debug)]
312pub enum CloseIntent {
313 /// Quit the program entirely.
314 Quit,
315 /// Close a window.
316 CloseWindow,
317 /// Replace the workspace in an existing window.
318 ReplaceWindow,
319}
320
321#[derive(Clone)]
322pub struct Toast {
323 id: NotificationId,
324 msg: Cow<'static, str>,
325 autohide: bool,
326 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut Window, &mut App)>)>,
327}
328
329impl Toast {
330 pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
331 Toast {
332 id,
333 msg: msg.into(),
334 on_click: None,
335 autohide: false,
336 }
337 }
338
339 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
340 where
341 M: Into<Cow<'static, str>>,
342 F: Fn(&mut Window, &mut App) + 'static,
343 {
344 self.on_click = Some((message.into(), Arc::new(on_click)));
345 self
346 }
347
348 pub fn autohide(mut self) -> Self {
349 self.autohide = true;
350 self
351 }
352}
353
354impl PartialEq for Toast {
355 fn eq(&self, other: &Self) -> bool {
356 self.id == other.id
357 && self.msg == other.msg
358 && self.on_click.is_some() == other.on_click.is_some()
359 }
360}
361
362#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema, Action)]
363#[action(namespace = workspace)]
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).clear();
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.propagate();
5622 }
5623 }
5624}
5625
5626fn leader_border_for_pane(
5627 follower_states: &HashMap<CollaboratorId, FollowerState>,
5628 pane: &Entity<Pane>,
5629 _: &Window,
5630 cx: &App,
5631) -> Option<Div> {
5632 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
5633 if state.pane() == pane {
5634 Some((*leader_id, state))
5635 } else {
5636 None
5637 }
5638 })?;
5639
5640 let mut leader_color = match leader_id {
5641 CollaboratorId::PeerId(leader_peer_id) => {
5642 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
5643 let leader = room.remote_participant_for_peer_id(leader_peer_id)?;
5644
5645 cx.theme()
5646 .players()
5647 .color_for_participant(leader.participant_index.0)
5648 .cursor
5649 }
5650 CollaboratorId::Agent => cx.theme().players().agent().cursor,
5651 };
5652 leader_color.fade_out(0.3);
5653 Some(
5654 div()
5655 .absolute()
5656 .size_full()
5657 .left_0()
5658 .top_0()
5659 .border_2()
5660 .border_color(leader_color),
5661 )
5662}
5663
5664fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
5665 ZED_WINDOW_POSITION
5666 .zip(*ZED_WINDOW_SIZE)
5667 .map(|(position, size)| Bounds {
5668 origin: position,
5669 size,
5670 })
5671}
5672
5673fn open_items(
5674 serialized_workspace: Option<SerializedWorkspace>,
5675 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
5676 window: &mut Window,
5677 cx: &mut Context<Workspace>,
5678) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
5679 let restored_items = serialized_workspace.map(|serialized_workspace| {
5680 Workspace::load_workspace(
5681 serialized_workspace,
5682 project_paths_to_open
5683 .iter()
5684 .map(|(_, project_path)| project_path)
5685 .cloned()
5686 .collect(),
5687 window,
5688 cx,
5689 )
5690 });
5691
5692 cx.spawn_in(window, async move |workspace, cx| {
5693 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
5694
5695 if let Some(restored_items) = restored_items {
5696 let restored_items = restored_items.await?;
5697
5698 let restored_project_paths = restored_items
5699 .iter()
5700 .filter_map(|item| {
5701 cx.update(|_, cx| item.as_ref()?.project_path(cx))
5702 .ok()
5703 .flatten()
5704 })
5705 .collect::<HashSet<_>>();
5706
5707 for restored_item in restored_items {
5708 opened_items.push(restored_item.map(Ok));
5709 }
5710
5711 project_paths_to_open
5712 .iter_mut()
5713 .for_each(|(_, project_path)| {
5714 if let Some(project_path_to_open) = project_path {
5715 if restored_project_paths.contains(project_path_to_open) {
5716 *project_path = None;
5717 }
5718 }
5719 });
5720 } else {
5721 for _ in 0..project_paths_to_open.len() {
5722 opened_items.push(None);
5723 }
5724 }
5725 assert!(opened_items.len() == project_paths_to_open.len());
5726
5727 let tasks =
5728 project_paths_to_open
5729 .into_iter()
5730 .enumerate()
5731 .map(|(ix, (abs_path, project_path))| {
5732 let workspace = workspace.clone();
5733 cx.spawn(async move |cx| {
5734 let file_project_path = project_path?;
5735 let abs_path_task = workspace.update(cx, |workspace, cx| {
5736 workspace.project().update(cx, |project, cx| {
5737 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
5738 })
5739 });
5740
5741 // We only want to open file paths here. If one of the items
5742 // here is a directory, it was already opened further above
5743 // with a `find_or_create_worktree`.
5744 if let Ok(task) = abs_path_task {
5745 if task.await.map_or(true, |p| p.is_file()) {
5746 return Some((
5747 ix,
5748 workspace
5749 .update_in(cx, |workspace, window, cx| {
5750 workspace.open_path(
5751 file_project_path,
5752 None,
5753 true,
5754 window,
5755 cx,
5756 )
5757 })
5758 .log_err()?
5759 .await,
5760 ));
5761 }
5762 }
5763 None
5764 })
5765 });
5766
5767 let tasks = tasks.collect::<Vec<_>>();
5768
5769 let tasks = futures::future::join_all(tasks);
5770 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
5771 opened_items[ix] = Some(path_open_result);
5772 }
5773
5774 Ok(opened_items)
5775 })
5776}
5777
5778enum ActivateInDirectionTarget {
5779 Pane(Entity<Pane>),
5780 Dock(Entity<Dock>),
5781}
5782
5783fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
5784 workspace
5785 .update(cx, |workspace, _, cx| {
5786 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
5787 struct DatabaseFailedNotification;
5788
5789 workspace.show_notification(
5790 NotificationId::unique::<DatabaseFailedNotification>(),
5791 cx,
5792 |cx| {
5793 cx.new(|cx| {
5794 MessageNotification::new("Failed to load the database file.", cx)
5795 .primary_message("File an Issue")
5796 .primary_icon(IconName::Plus)
5797 .primary_on_click(|window, cx| {
5798 window.dispatch_action(Box::new(FileBugReport), cx)
5799 })
5800 })
5801 },
5802 );
5803 }
5804 })
5805 .log_err();
5806}
5807
5808impl Focusable for Workspace {
5809 fn focus_handle(&self, cx: &App) -> FocusHandle {
5810 self.active_pane.focus_handle(cx)
5811 }
5812}
5813
5814#[derive(Clone)]
5815struct DraggedDock(DockPosition);
5816
5817impl Render for DraggedDock {
5818 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5819 gpui::Empty
5820 }
5821}
5822
5823impl Render for Workspace {
5824 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5825 let mut context = KeyContext::new_with_defaults();
5826 context.add("Workspace");
5827 context.set("keyboard_layout", cx.keyboard_layout().name().to_string());
5828 if let Some(status) = self
5829 .debugger_provider
5830 .as_ref()
5831 .and_then(|provider| provider.active_thread_state(cx))
5832 {
5833 match status {
5834 ThreadStatus::Running | ThreadStatus::Stepping => {
5835 context.add("debugger_running");
5836 }
5837 ThreadStatus::Stopped => context.add("debugger_stopped"),
5838 ThreadStatus::Exited | ThreadStatus::Ended => {}
5839 }
5840 }
5841
5842 let centered_layout = self.centered_layout
5843 && self.center.panes().len() == 1
5844 && self.active_item(cx).is_some();
5845 let render_padding = |size| {
5846 (size > 0.0).then(|| {
5847 div()
5848 .h_full()
5849 .w(relative(size))
5850 .bg(cx.theme().colors().editor_background)
5851 .border_color(cx.theme().colors().pane_group_border)
5852 })
5853 };
5854 let paddings = if centered_layout {
5855 let settings = WorkspaceSettings::get_global(cx).centered_layout;
5856 (
5857 render_padding(Self::adjust_padding(settings.left_padding)),
5858 render_padding(Self::adjust_padding(settings.right_padding)),
5859 )
5860 } else {
5861 (None, None)
5862 };
5863 let ui_font = theme::setup_ui_font(window, cx);
5864
5865 let theme = cx.theme().clone();
5866 let colors = theme.colors();
5867 let notification_entities = self
5868 .notifications
5869 .iter()
5870 .map(|(_, notification)| notification.entity_id())
5871 .collect::<Vec<_>>();
5872
5873 client_side_decorations(
5874 self.actions(div(), window, cx)
5875 .key_context(context)
5876 .relative()
5877 .size_full()
5878 .flex()
5879 .flex_col()
5880 .font(ui_font)
5881 .gap_0()
5882 .justify_start()
5883 .items_start()
5884 .text_color(colors.text)
5885 .overflow_hidden()
5886 .children(self.titlebar_item.clone())
5887 .on_modifiers_changed(move |_, _, cx| {
5888 for &id in ¬ification_entities {
5889 cx.notify(id);
5890 }
5891 })
5892 .child(
5893 div()
5894 .size_full()
5895 .relative()
5896 .flex_1()
5897 .flex()
5898 .flex_col()
5899 .child(
5900 div()
5901 .id("workspace")
5902 .bg(colors.background)
5903 .relative()
5904 .flex_1()
5905 .w_full()
5906 .flex()
5907 .flex_col()
5908 .overflow_hidden()
5909 .border_t_1()
5910 .border_b_1()
5911 .border_color(colors.border)
5912 .child({
5913 let this = cx.entity().clone();
5914 canvas(
5915 move |bounds, window, cx| {
5916 this.update(cx, |this, cx| {
5917 let bounds_changed = this.bounds != bounds;
5918 this.bounds = bounds;
5919
5920 if bounds_changed {
5921 this.left_dock.update(cx, |dock, cx| {
5922 dock.clamp_panel_size(
5923 bounds.size.width,
5924 window,
5925 cx,
5926 )
5927 });
5928
5929 this.right_dock.update(cx, |dock, cx| {
5930 dock.clamp_panel_size(
5931 bounds.size.width,
5932 window,
5933 cx,
5934 )
5935 });
5936
5937 this.bottom_dock.update(cx, |dock, cx| {
5938 dock.clamp_panel_size(
5939 bounds.size.height,
5940 window,
5941 cx,
5942 )
5943 });
5944 }
5945 })
5946 },
5947 |_, _, _, _| {},
5948 )
5949 .absolute()
5950 .size_full()
5951 })
5952 .when(self.zoomed.is_none(), |this| {
5953 this.on_drag_move(cx.listener(
5954 move |workspace,
5955 e: &DragMoveEvent<DraggedDock>,
5956 window,
5957 cx| {
5958 if workspace.previous_dock_drag_coordinates
5959 != Some(e.event.position)
5960 {
5961 workspace.previous_dock_drag_coordinates =
5962 Some(e.event.position);
5963 match e.drag(cx).0 {
5964 DockPosition::Left => {
5965 resize_left_dock(
5966 e.event.position.x
5967 - workspace.bounds.left(),
5968 workspace,
5969 window,
5970 cx,
5971 );
5972 }
5973 DockPosition::Right => {
5974 resize_right_dock(
5975 workspace.bounds.right()
5976 - e.event.position.x,
5977 workspace,
5978 window,
5979 cx,
5980 );
5981 }
5982 DockPosition::Bottom => {
5983 resize_bottom_dock(
5984 workspace.bounds.bottom()
5985 - e.event.position.y,
5986 workspace,
5987 window,
5988 cx,
5989 );
5990 }
5991 };
5992 workspace.serialize_workspace(window, cx);
5993 }
5994 },
5995 ))
5996 })
5997 .child({
5998 match self.bottom_dock_layout {
5999 BottomDockLayout::Full => div()
6000 .flex()
6001 .flex_col()
6002 .h_full()
6003 .child(
6004 div()
6005 .flex()
6006 .flex_row()
6007 .flex_1()
6008 .overflow_hidden()
6009 .children(self.render_dock(
6010 DockPosition::Left,
6011 &self.left_dock,
6012 window,
6013 cx,
6014 ))
6015 .child(
6016 div()
6017 .flex()
6018 .flex_col()
6019 .flex_1()
6020 .overflow_hidden()
6021 .child(
6022 h_flex()
6023 .flex_1()
6024 .when_some(
6025 paddings.0,
6026 |this, p| {
6027 this.child(
6028 p.border_r_1(),
6029 )
6030 },
6031 )
6032 .child(self.center.render(
6033 self.zoomed.as_ref(),
6034 &PaneRenderContext {
6035 follower_states:
6036 &self.follower_states,
6037 active_call: self.active_call(),
6038 active_pane: &self.active_pane,
6039 app_state: &self.app_state,
6040 project: &self.project,
6041 workspace: &self.weak_self,
6042 },
6043 window,
6044 cx,
6045 ))
6046 .when_some(
6047 paddings.1,
6048 |this, p| {
6049 this.child(
6050 p.border_l_1(),
6051 )
6052 },
6053 ),
6054 ),
6055 )
6056 .children(self.render_dock(
6057 DockPosition::Right,
6058 &self.right_dock,
6059 window,
6060 cx,
6061 )),
6062 )
6063 .child(div().w_full().children(self.render_dock(
6064 DockPosition::Bottom,
6065 &self.bottom_dock,
6066 window,
6067 cx
6068 ))),
6069
6070 BottomDockLayout::LeftAligned => div()
6071 .flex()
6072 .flex_row()
6073 .h_full()
6074 .child(
6075 div()
6076 .flex()
6077 .flex_col()
6078 .flex_1()
6079 .h_full()
6080 .child(
6081 div()
6082 .flex()
6083 .flex_row()
6084 .flex_1()
6085 .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx))
6086 .child(
6087 div()
6088 .flex()
6089 .flex_col()
6090 .flex_1()
6091 .overflow_hidden()
6092 .child(
6093 h_flex()
6094 .flex_1()
6095 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
6096 .child(self.center.render(
6097 self.zoomed.as_ref(),
6098 &PaneRenderContext {
6099 follower_states:
6100 &self.follower_states,
6101 active_call: self.active_call(),
6102 active_pane: &self.active_pane,
6103 app_state: &self.app_state,
6104 project: &self.project,
6105 workspace: &self.weak_self,
6106 },
6107 window,
6108 cx,
6109 ))
6110 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
6111 )
6112 )
6113 )
6114 .child(
6115 div()
6116 .w_full()
6117 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
6118 ),
6119 )
6120 .children(self.render_dock(
6121 DockPosition::Right,
6122 &self.right_dock,
6123 window,
6124 cx,
6125 )),
6126
6127 BottomDockLayout::RightAligned => div()
6128 .flex()
6129 .flex_row()
6130 .h_full()
6131 .children(self.render_dock(
6132 DockPosition::Left,
6133 &self.left_dock,
6134 window,
6135 cx,
6136 ))
6137 .child(
6138 div()
6139 .flex()
6140 .flex_col()
6141 .flex_1()
6142 .h_full()
6143 .child(
6144 div()
6145 .flex()
6146 .flex_row()
6147 .flex_1()
6148 .child(
6149 div()
6150 .flex()
6151 .flex_col()
6152 .flex_1()
6153 .overflow_hidden()
6154 .child(
6155 h_flex()
6156 .flex_1()
6157 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
6158 .child(self.center.render(
6159 self.zoomed.as_ref(),
6160 &PaneRenderContext {
6161 follower_states:
6162 &self.follower_states,
6163 active_call: self.active_call(),
6164 active_pane: &self.active_pane,
6165 app_state: &self.app_state,
6166 project: &self.project,
6167 workspace: &self.weak_self,
6168 },
6169 window,
6170 cx,
6171 ))
6172 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
6173 )
6174 )
6175 .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx))
6176 )
6177 .child(
6178 div()
6179 .w_full()
6180 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
6181 ),
6182 ),
6183
6184 BottomDockLayout::Contained => div()
6185 .flex()
6186 .flex_row()
6187 .h_full()
6188 .children(self.render_dock(
6189 DockPosition::Left,
6190 &self.left_dock,
6191 window,
6192 cx,
6193 ))
6194 .child(
6195 div()
6196 .flex()
6197 .flex_col()
6198 .flex_1()
6199 .overflow_hidden()
6200 .child(
6201 h_flex()
6202 .flex_1()
6203 .when_some(paddings.0, |this, p| {
6204 this.child(p.border_r_1())
6205 })
6206 .child(self.center.render(
6207 self.zoomed.as_ref(),
6208 &PaneRenderContext {
6209 follower_states:
6210 &self.follower_states,
6211 active_call: self.active_call(),
6212 active_pane: &self.active_pane,
6213 app_state: &self.app_state,
6214 project: &self.project,
6215 workspace: &self.weak_self,
6216 },
6217 window,
6218 cx,
6219 ))
6220 .when_some(paddings.1, |this, p| {
6221 this.child(p.border_l_1())
6222 }),
6223 )
6224 .children(self.render_dock(
6225 DockPosition::Bottom,
6226 &self.bottom_dock,
6227 window,
6228 cx,
6229 )),
6230 )
6231 .children(self.render_dock(
6232 DockPosition::Right,
6233 &self.right_dock,
6234 window,
6235 cx,
6236 )),
6237 }
6238 })
6239 .children(self.zoomed.as_ref().and_then(|view| {
6240 let zoomed_view = view.upgrade()?;
6241 let div = div()
6242 .occlude()
6243 .absolute()
6244 .overflow_hidden()
6245 .border_color(colors.border)
6246 .bg(colors.background)
6247 .child(zoomed_view)
6248 .inset_0()
6249 .shadow_lg();
6250
6251 Some(match self.zoomed_position {
6252 Some(DockPosition::Left) => div.right_2().border_r_1(),
6253 Some(DockPosition::Right) => div.left_2().border_l_1(),
6254 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
6255 None => {
6256 div.top_2().bottom_2().left_2().right_2().border_1()
6257 }
6258 })
6259 }))
6260 .children(self.render_notifications(window, cx)),
6261 )
6262 .child(self.status_bar.clone())
6263 .child(self.modal_layer.clone())
6264 .child(self.toast_layer.clone()),
6265 ),
6266 window,
6267 cx,
6268 )
6269 }
6270}
6271
6272fn resize_bottom_dock(
6273 new_size: Pixels,
6274 workspace: &mut Workspace,
6275 window: &mut Window,
6276 cx: &mut App,
6277) {
6278 let size =
6279 new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE - workspace.bounds.top());
6280 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
6281 if WorkspaceSettings::get_global(cx)
6282 .resize_all_panels_in_dock
6283 .contains(&DockPosition::Bottom)
6284 {
6285 bottom_dock.resize_all_panels(Some(size), window, cx);
6286 } else {
6287 bottom_dock.resize_active_panel(Some(size), window, cx);
6288 }
6289 });
6290}
6291
6292fn resize_right_dock(
6293 new_size: Pixels,
6294 workspace: &mut Workspace,
6295 window: &mut Window,
6296 cx: &mut App,
6297) {
6298 let mut size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
6299 workspace.left_dock.read_with(cx, |left_dock, cx| {
6300 let left_dock_size = left_dock
6301 .active_panel_size(window, cx)
6302 .unwrap_or(Pixels(0.0));
6303 if left_dock_size + size > workspace.bounds.right() {
6304 size = workspace.bounds.right() - left_dock_size
6305 }
6306 });
6307 workspace.right_dock.update(cx, |right_dock, cx| {
6308 if WorkspaceSettings::get_global(cx)
6309 .resize_all_panels_in_dock
6310 .contains(&DockPosition::Right)
6311 {
6312 right_dock.resize_all_panels(Some(size), window, cx);
6313 } else {
6314 right_dock.resize_active_panel(Some(size), window, cx);
6315 }
6316 });
6317}
6318
6319fn resize_left_dock(
6320 new_size: Pixels,
6321 workspace: &mut Workspace,
6322 window: &mut Window,
6323 cx: &mut App,
6324) {
6325 let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
6326
6327 workspace.left_dock.update(cx, |left_dock, cx| {
6328 if WorkspaceSettings::get_global(cx)
6329 .resize_all_panels_in_dock
6330 .contains(&DockPosition::Left)
6331 {
6332 left_dock.resize_all_panels(Some(size), window, cx);
6333 } else {
6334 left_dock.resize_active_panel(Some(size), window, cx);
6335 }
6336 });
6337}
6338
6339impl WorkspaceStore {
6340 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
6341 Self {
6342 workspaces: Default::default(),
6343 _subscriptions: vec![
6344 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
6345 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
6346 ],
6347 client,
6348 }
6349 }
6350
6351 pub fn update_followers(
6352 &self,
6353 project_id: Option<u64>,
6354 update: proto::update_followers::Variant,
6355 cx: &App,
6356 ) -> Option<()> {
6357 let active_call = ActiveCall::try_global(cx)?;
6358 let room_id = active_call.read(cx).room()?.read(cx).id();
6359 self.client
6360 .send(proto::UpdateFollowers {
6361 room_id,
6362 project_id,
6363 variant: Some(update),
6364 })
6365 .log_err()
6366 }
6367
6368 pub async fn handle_follow(
6369 this: Entity<Self>,
6370 envelope: TypedEnvelope<proto::Follow>,
6371 mut cx: AsyncApp,
6372 ) -> Result<proto::FollowResponse> {
6373 this.update(&mut cx, |this, cx| {
6374 let follower = Follower {
6375 project_id: envelope.payload.project_id,
6376 peer_id: envelope.original_sender_id()?,
6377 };
6378
6379 let mut response = proto::FollowResponse::default();
6380 this.workspaces.retain(|workspace| {
6381 workspace
6382 .update(cx, |workspace, window, cx| {
6383 let handler_response =
6384 workspace.handle_follow(follower.project_id, window, cx);
6385 if let Some(active_view) = handler_response.active_view.clone() {
6386 if workspace.project.read(cx).remote_id() == follower.project_id {
6387 response.active_view = Some(active_view)
6388 }
6389 }
6390 })
6391 .is_ok()
6392 });
6393
6394 Ok(response)
6395 })?
6396 }
6397
6398 async fn handle_update_followers(
6399 this: Entity<Self>,
6400 envelope: TypedEnvelope<proto::UpdateFollowers>,
6401 mut cx: AsyncApp,
6402 ) -> Result<()> {
6403 let leader_id = envelope.original_sender_id()?;
6404 let update = envelope.payload;
6405
6406 this.update(&mut cx, |this, cx| {
6407 this.workspaces.retain(|workspace| {
6408 workspace
6409 .update(cx, |workspace, window, cx| {
6410 let project_id = workspace.project.read(cx).remote_id();
6411 if update.project_id != project_id && update.project_id.is_some() {
6412 return;
6413 }
6414 workspace.handle_update_followers(leader_id, update.clone(), window, cx);
6415 })
6416 .is_ok()
6417 });
6418 Ok(())
6419 })?
6420 }
6421}
6422
6423impl ViewId {
6424 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
6425 Ok(Self {
6426 creator: message
6427 .creator
6428 .map(CollaboratorId::PeerId)
6429 .context("creator is missing")?,
6430 id: message.id,
6431 })
6432 }
6433
6434 pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
6435 if let CollaboratorId::PeerId(peer_id) = self.creator {
6436 Some(proto::ViewId {
6437 creator: Some(peer_id),
6438 id: self.id,
6439 })
6440 } else {
6441 None
6442 }
6443 }
6444}
6445
6446impl FollowerState {
6447 fn pane(&self) -> &Entity<Pane> {
6448 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
6449 }
6450}
6451
6452pub trait WorkspaceHandle {
6453 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
6454}
6455
6456impl WorkspaceHandle for Entity<Workspace> {
6457 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
6458 self.read(cx)
6459 .worktrees(cx)
6460 .flat_map(|worktree| {
6461 let worktree_id = worktree.read(cx).id();
6462 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
6463 worktree_id,
6464 path: f.path.clone(),
6465 })
6466 })
6467 .collect::<Vec<_>>()
6468 }
6469}
6470
6471impl std::fmt::Debug for OpenPaths {
6472 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6473 f.debug_struct("OpenPaths")
6474 .field("paths", &self.paths)
6475 .finish()
6476 }
6477}
6478
6479pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
6480 DB.last_workspace().await.log_err().flatten()
6481}
6482
6483pub fn last_session_workspace_locations(
6484 last_session_id: &str,
6485 last_session_window_stack: Option<Vec<WindowId>>,
6486) -> Option<Vec<SerializedWorkspaceLocation>> {
6487 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
6488 .log_err()
6489}
6490
6491actions!(
6492 collab,
6493 [
6494 /// Opens the channel notes for the current call.
6495 ///
6496 /// If you want to open a specific channel, use `zed::OpenZedUrl` with a channel notes URL -
6497 /// can be copied via "Copy link to section" in the context menu of the channel notes
6498 /// buffer. These URLs look like `https://zed.dev/channel/channel-name-CHANNEL_ID/notes`.
6499 OpenChannelNotes,
6500 Mute,
6501 Deafen,
6502 LeaveCall,
6503 ShareProject,
6504 ScreenShare
6505 ]
6506);
6507actions!(zed, [OpenLog]);
6508
6509async fn join_channel_internal(
6510 channel_id: ChannelId,
6511 app_state: &Arc<AppState>,
6512 requesting_window: Option<WindowHandle<Workspace>>,
6513 active_call: &Entity<ActiveCall>,
6514 cx: &mut AsyncApp,
6515) -> Result<bool> {
6516 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
6517 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
6518 return (false, None);
6519 };
6520
6521 let already_in_channel = room.channel_id() == Some(channel_id);
6522 let should_prompt = room.is_sharing_project()
6523 && !room.remote_participants().is_empty()
6524 && !already_in_channel;
6525 let open_room = if already_in_channel {
6526 active_call.room().cloned()
6527 } else {
6528 None
6529 };
6530 (should_prompt, open_room)
6531 })?;
6532
6533 if let Some(room) = open_room {
6534 let task = room.update(cx, |room, cx| {
6535 if let Some((project, host)) = room.most_active_project(cx) {
6536 return Some(join_in_room_project(project, host, app_state.clone(), cx));
6537 }
6538
6539 None
6540 })?;
6541 if let Some(task) = task {
6542 task.await?;
6543 }
6544 return anyhow::Ok(true);
6545 }
6546
6547 if should_prompt {
6548 if let Some(workspace) = requesting_window {
6549 let answer = workspace
6550 .update(cx, |_, window, cx| {
6551 window.prompt(
6552 PromptLevel::Warning,
6553 "Do you want to switch channels?",
6554 Some("Leaving this call will unshare your current project."),
6555 &["Yes, Join Channel", "Cancel"],
6556 cx,
6557 )
6558 })?
6559 .await;
6560
6561 if answer == Ok(1) {
6562 return Ok(false);
6563 }
6564 } else {
6565 return Ok(false); // unreachable!() hopefully
6566 }
6567 }
6568
6569 let client = cx.update(|cx| active_call.read(cx).client())?;
6570
6571 let mut client_status = client.status();
6572
6573 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
6574 'outer: loop {
6575 let Some(status) = client_status.recv().await else {
6576 anyhow::bail!("error connecting");
6577 };
6578
6579 match status {
6580 Status::Connecting
6581 | Status::Authenticating
6582 | Status::Reconnecting
6583 | Status::Reauthenticating => continue,
6584 Status::Connected { .. } => break 'outer,
6585 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
6586 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
6587 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
6588 return Err(ErrorCode::Disconnected.into());
6589 }
6590 }
6591 }
6592
6593 let room = active_call
6594 .update(cx, |active_call, cx| {
6595 active_call.join_channel(channel_id, cx)
6596 })?
6597 .await?;
6598
6599 let Some(room) = room else {
6600 return anyhow::Ok(true);
6601 };
6602
6603 room.update(cx, |room, _| room.room_update_completed())?
6604 .await;
6605
6606 let task = room.update(cx, |room, cx| {
6607 if let Some((project, host)) = room.most_active_project(cx) {
6608 return Some(join_in_room_project(project, host, app_state.clone(), cx));
6609 }
6610
6611 // If you are the first to join a channel, see if you should share your project.
6612 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
6613 if let Some(workspace) = requesting_window {
6614 let project = workspace.update(cx, |workspace, _, cx| {
6615 let project = workspace.project.read(cx);
6616
6617 if !CallSettings::get_global(cx).share_on_join {
6618 return None;
6619 }
6620
6621 if (project.is_local() || project.is_via_ssh())
6622 && project.visible_worktrees(cx).any(|tree| {
6623 tree.read(cx)
6624 .root_entry()
6625 .map_or(false, |entry| entry.is_dir())
6626 })
6627 {
6628 Some(workspace.project.clone())
6629 } else {
6630 None
6631 }
6632 });
6633 if let Ok(Some(project)) = project {
6634 return Some(cx.spawn(async move |room, cx| {
6635 room.update(cx, |room, cx| room.share_project(project, cx))?
6636 .await?;
6637 Ok(())
6638 }));
6639 }
6640 }
6641 }
6642
6643 None
6644 })?;
6645 if let Some(task) = task {
6646 task.await?;
6647 return anyhow::Ok(true);
6648 }
6649 anyhow::Ok(false)
6650}
6651
6652pub fn join_channel(
6653 channel_id: ChannelId,
6654 app_state: Arc<AppState>,
6655 requesting_window: Option<WindowHandle<Workspace>>,
6656 cx: &mut App,
6657) -> Task<Result<()>> {
6658 let active_call = ActiveCall::global(cx);
6659 cx.spawn(async move |cx| {
6660 let result = join_channel_internal(
6661 channel_id,
6662 &app_state,
6663 requesting_window,
6664 &active_call,
6665 cx,
6666 )
6667 .await;
6668
6669 // join channel succeeded, and opened a window
6670 if matches!(result, Ok(true)) {
6671 return anyhow::Ok(());
6672 }
6673
6674 // find an existing workspace to focus and show call controls
6675 let mut active_window =
6676 requesting_window.or_else(|| activate_any_workspace_window( cx));
6677 if active_window.is_none() {
6678 // no open workspaces, make one to show the error in (blergh)
6679 let (window_handle, _) = cx
6680 .update(|cx| {
6681 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
6682 })?
6683 .await?;
6684
6685 if result.is_ok() {
6686 cx.update(|cx| {
6687 cx.dispatch_action(&OpenChannelNotes);
6688 }).log_err();
6689 }
6690
6691 active_window = Some(window_handle);
6692 }
6693
6694 if let Err(err) = result {
6695 log::error!("failed to join channel: {}", err);
6696 if let Some(active_window) = active_window {
6697 active_window
6698 .update(cx, |_, window, cx| {
6699 let detail: SharedString = match err.error_code() {
6700 ErrorCode::SignedOut => {
6701 "Please sign in to continue.".into()
6702 }
6703 ErrorCode::UpgradeRequired => {
6704 "Your are running an unsupported version of Zed. Please update to continue.".into()
6705 }
6706 ErrorCode::NoSuchChannel => {
6707 "No matching channel was found. Please check the link and try again.".into()
6708 }
6709 ErrorCode::Forbidden => {
6710 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
6711 }
6712 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
6713 _ => format!("{}\n\nPlease try again.", err).into(),
6714 };
6715 window.prompt(
6716 PromptLevel::Critical,
6717 "Failed to join channel",
6718 Some(&detail),
6719 &["Ok"],
6720 cx)
6721 })?
6722 .await
6723 .ok();
6724 }
6725 }
6726
6727 // return ok, we showed the error to the user.
6728 anyhow::Ok(())
6729 })
6730}
6731
6732pub async fn get_any_active_workspace(
6733 app_state: Arc<AppState>,
6734 mut cx: AsyncApp,
6735) -> anyhow::Result<WindowHandle<Workspace>> {
6736 // find an existing workspace to focus and show call controls
6737 let active_window = activate_any_workspace_window(&mut cx);
6738 if active_window.is_none() {
6739 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
6740 .await?;
6741 }
6742 activate_any_workspace_window(&mut cx).context("could not open zed")
6743}
6744
6745fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
6746 cx.update(|cx| {
6747 if let Some(workspace_window) = cx
6748 .active_window()
6749 .and_then(|window| window.downcast::<Workspace>())
6750 {
6751 return Some(workspace_window);
6752 }
6753
6754 for window in cx.windows() {
6755 if let Some(workspace_window) = window.downcast::<Workspace>() {
6756 workspace_window
6757 .update(cx, |_, window, _| window.activate_window())
6758 .ok();
6759 return Some(workspace_window);
6760 }
6761 }
6762 None
6763 })
6764 .ok()
6765 .flatten()
6766}
6767
6768pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
6769 cx.windows()
6770 .into_iter()
6771 .filter_map(|window| window.downcast::<Workspace>())
6772 .filter(|workspace| {
6773 workspace
6774 .read(cx)
6775 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
6776 })
6777 .collect()
6778}
6779
6780#[derive(Default)]
6781pub struct OpenOptions {
6782 pub visible: Option<OpenVisible>,
6783 pub focus: Option<bool>,
6784 pub open_new_workspace: Option<bool>,
6785 pub replace_window: Option<WindowHandle<Workspace>>,
6786 pub env: Option<HashMap<String, String>>,
6787}
6788
6789#[allow(clippy::type_complexity)]
6790pub fn open_paths(
6791 abs_paths: &[PathBuf],
6792 app_state: Arc<AppState>,
6793 open_options: OpenOptions,
6794 cx: &mut App,
6795) -> Task<
6796 anyhow::Result<(
6797 WindowHandle<Workspace>,
6798 Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
6799 )>,
6800> {
6801 let abs_paths = abs_paths.to_vec();
6802 let mut existing = None;
6803 let mut best_match = None;
6804 let mut open_visible = OpenVisible::All;
6805
6806 cx.spawn(async move |cx| {
6807 if open_options.open_new_workspace != Some(true) {
6808 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
6809 let all_metadatas = futures::future::join_all(all_paths)
6810 .await
6811 .into_iter()
6812 .filter_map(|result| result.ok().flatten())
6813 .collect::<Vec<_>>();
6814
6815 cx.update(|cx| {
6816 for window in local_workspace_windows(&cx) {
6817 if let Ok(workspace) = window.read(&cx) {
6818 let m = workspace.project.read(&cx).visibility_for_paths(
6819 &abs_paths,
6820 &all_metadatas,
6821 open_options.open_new_workspace == None,
6822 cx,
6823 );
6824 if m > best_match {
6825 existing = Some(window);
6826 best_match = m;
6827 } else if best_match.is_none()
6828 && open_options.open_new_workspace == Some(false)
6829 {
6830 existing = Some(window)
6831 }
6832 }
6833 }
6834 })?;
6835
6836 if open_options.open_new_workspace.is_none() && existing.is_none() {
6837 if all_metadatas.iter().all(|file| !file.is_dir) {
6838 cx.update(|cx| {
6839 if let Some(window) = cx
6840 .active_window()
6841 .and_then(|window| window.downcast::<Workspace>())
6842 {
6843 if let Ok(workspace) = window.read(cx) {
6844 let project = workspace.project().read(cx);
6845 if project.is_local() && !project.is_via_collab() {
6846 existing = Some(window);
6847 open_visible = OpenVisible::None;
6848 return;
6849 }
6850 }
6851 }
6852 for window in local_workspace_windows(cx) {
6853 if let Ok(workspace) = window.read(cx) {
6854 let project = workspace.project().read(cx);
6855 if project.is_via_collab() {
6856 continue;
6857 }
6858 existing = Some(window);
6859 open_visible = OpenVisible::None;
6860 break;
6861 }
6862 }
6863 })?;
6864 }
6865 }
6866 }
6867
6868 if let Some(existing) = existing {
6869 let open_task = existing
6870 .update(cx, |workspace, window, cx| {
6871 window.activate_window();
6872 workspace.open_paths(
6873 abs_paths,
6874 OpenOptions {
6875 visible: Some(open_visible),
6876 ..Default::default()
6877 },
6878 None,
6879 window,
6880 cx,
6881 )
6882 })?
6883 .await;
6884
6885 _ = existing.update(cx, |workspace, _, cx| {
6886 for item in open_task.iter().flatten() {
6887 if let Err(e) = item {
6888 workspace.show_error(&e, cx);
6889 }
6890 }
6891 });
6892
6893 Ok((existing, open_task))
6894 } else {
6895 cx.update(move |cx| {
6896 Workspace::new_local(
6897 abs_paths,
6898 app_state.clone(),
6899 open_options.replace_window,
6900 open_options.env,
6901 cx,
6902 )
6903 })?
6904 .await
6905 }
6906 })
6907}
6908
6909pub fn open_new(
6910 open_options: OpenOptions,
6911 app_state: Arc<AppState>,
6912 cx: &mut App,
6913 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6914) -> Task<anyhow::Result<()>> {
6915 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6916 cx.spawn(async move |cx| {
6917 let (workspace, opened_paths) = task.await?;
6918 workspace.update(cx, |workspace, window, cx| {
6919 if opened_paths.is_empty() {
6920 init(workspace, window, cx)
6921 }
6922 })?;
6923 Ok(())
6924 })
6925}
6926
6927pub fn create_and_open_local_file(
6928 path: &'static Path,
6929 window: &mut Window,
6930 cx: &mut Context<Workspace>,
6931 default_content: impl 'static + Send + FnOnce() -> Rope,
6932) -> Task<Result<Box<dyn ItemHandle>>> {
6933 cx.spawn_in(window, async move |workspace, cx| {
6934 let fs = workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
6935 if !fs.is_file(path).await {
6936 fs.create_file(path, Default::default()).await?;
6937 fs.save(path, &default_content(), Default::default())
6938 .await?;
6939 }
6940
6941 let mut items = workspace
6942 .update_in(cx, |workspace, window, cx| {
6943 workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6944 workspace.open_paths(
6945 vec![path.to_path_buf()],
6946 OpenOptions {
6947 visible: Some(OpenVisible::None),
6948 ..Default::default()
6949 },
6950 None,
6951 window,
6952 cx,
6953 )
6954 })
6955 })?
6956 .await?
6957 .await;
6958
6959 let item = items.pop().flatten();
6960 item.with_context(|| format!("path {path:?} is not a file"))?
6961 })
6962}
6963
6964pub fn open_ssh_project_with_new_connection(
6965 window: WindowHandle<Workspace>,
6966 connection_options: SshConnectionOptions,
6967 cancel_rx: oneshot::Receiver<()>,
6968 delegate: Arc<dyn SshClientDelegate>,
6969 app_state: Arc<AppState>,
6970 paths: Vec<PathBuf>,
6971 cx: &mut App,
6972) -> Task<Result<()>> {
6973 cx.spawn(async move |cx| {
6974 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6975 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6976
6977 let session = match cx
6978 .update(|cx| {
6979 remote::SshRemoteClient::new(
6980 ConnectionIdentifier::Workspace(workspace_id.0),
6981 connection_options,
6982 cancel_rx,
6983 delegate,
6984 cx,
6985 )
6986 })?
6987 .await?
6988 {
6989 Some(result) => result,
6990 None => return Ok(()),
6991 };
6992
6993 let project = cx.update(|cx| {
6994 project::Project::ssh(
6995 session,
6996 app_state.client.clone(),
6997 app_state.node_runtime.clone(),
6998 app_state.user_store.clone(),
6999 app_state.languages.clone(),
7000 app_state.fs.clone(),
7001 cx,
7002 )
7003 })?;
7004
7005 open_ssh_project_inner(
7006 project,
7007 paths,
7008 serialized_ssh_project,
7009 workspace_id,
7010 serialized_workspace,
7011 app_state,
7012 window,
7013 cx,
7014 )
7015 .await
7016 })
7017}
7018
7019pub fn open_ssh_project_with_existing_connection(
7020 connection_options: SshConnectionOptions,
7021 project: Entity<Project>,
7022 paths: Vec<PathBuf>,
7023 app_state: Arc<AppState>,
7024 window: WindowHandle<Workspace>,
7025 cx: &mut AsyncApp,
7026) -> Task<Result<()>> {
7027 cx.spawn(async move |cx| {
7028 let (serialized_ssh_project, workspace_id, serialized_workspace) =
7029 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
7030
7031 open_ssh_project_inner(
7032 project,
7033 paths,
7034 serialized_ssh_project,
7035 workspace_id,
7036 serialized_workspace,
7037 app_state,
7038 window,
7039 cx,
7040 )
7041 .await
7042 })
7043}
7044
7045async fn open_ssh_project_inner(
7046 project: Entity<Project>,
7047 paths: Vec<PathBuf>,
7048 serialized_ssh_project: SerializedSshProject,
7049 workspace_id: WorkspaceId,
7050 serialized_workspace: Option<SerializedWorkspace>,
7051 app_state: Arc<AppState>,
7052 window: WindowHandle<Workspace>,
7053 cx: &mut AsyncApp,
7054) -> Result<()> {
7055 let toolchains = DB.toolchains(workspace_id).await?;
7056 for (toolchain, worktree_id, path) in toolchains {
7057 project
7058 .update(cx, |this, cx| {
7059 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
7060 })?
7061 .await;
7062 }
7063 let mut project_paths_to_open = vec![];
7064 let mut project_path_errors = vec![];
7065
7066 for path in paths {
7067 let result = cx
7068 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
7069 .await;
7070 match result {
7071 Ok((_, project_path)) => {
7072 project_paths_to_open.push((path.clone(), Some(project_path)));
7073 }
7074 Err(error) => {
7075 project_path_errors.push(error);
7076 }
7077 };
7078 }
7079
7080 if project_paths_to_open.is_empty() {
7081 return Err(project_path_errors.pop().context("no paths given")?);
7082 }
7083
7084 cx.update_window(window.into(), |_, window, cx| {
7085 window.replace_root(cx, |window, cx| {
7086 telemetry::event!("SSH Project Opened");
7087
7088 let mut workspace =
7089 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
7090 workspace.set_serialized_ssh_project(serialized_ssh_project);
7091 workspace.update_history(cx);
7092
7093 if let Some(ref serialized) = serialized_workspace {
7094 workspace.centered_layout = serialized.centered_layout;
7095 }
7096
7097 workspace
7098 });
7099 })?;
7100
7101 window
7102 .update(cx, |_, window, cx| {
7103 window.activate_window();
7104 open_items(serialized_workspace, project_paths_to_open, window, cx)
7105 })?
7106 .await?;
7107
7108 window.update(cx, |workspace, _, cx| {
7109 for error in project_path_errors {
7110 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
7111 if let Some(path) = error.error_tag("path") {
7112 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
7113 }
7114 } else {
7115 workspace.show_error(&error, cx)
7116 }
7117 }
7118 })?;
7119
7120 Ok(())
7121}
7122
7123fn serialize_ssh_project(
7124 connection_options: SshConnectionOptions,
7125 paths: Vec<PathBuf>,
7126 cx: &AsyncApp,
7127) -> Task<
7128 Result<(
7129 SerializedSshProject,
7130 WorkspaceId,
7131 Option<SerializedWorkspace>,
7132 )>,
7133> {
7134 cx.background_spawn(async move {
7135 let serialized_ssh_project = persistence::DB
7136 .get_or_create_ssh_project(
7137 connection_options.host.clone(),
7138 connection_options.port,
7139 paths
7140 .iter()
7141 .map(|path| path.to_string_lossy().to_string())
7142 .collect::<Vec<_>>(),
7143 connection_options.username.clone(),
7144 )
7145 .await?;
7146
7147 let serialized_workspace =
7148 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
7149
7150 let workspace_id = if let Some(workspace_id) =
7151 serialized_workspace.as_ref().map(|workspace| workspace.id)
7152 {
7153 workspace_id
7154 } else {
7155 persistence::DB.next_id().await?
7156 };
7157
7158 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
7159 })
7160}
7161
7162pub fn join_in_room_project(
7163 project_id: u64,
7164 follow_user_id: u64,
7165 app_state: Arc<AppState>,
7166 cx: &mut App,
7167) -> Task<Result<()>> {
7168 let windows = cx.windows();
7169 cx.spawn(async move |cx| {
7170 let existing_workspace = windows.into_iter().find_map(|window_handle| {
7171 window_handle
7172 .downcast::<Workspace>()
7173 .and_then(|window_handle| {
7174 window_handle
7175 .update(cx, |workspace, _window, cx| {
7176 if workspace.project().read(cx).remote_id() == Some(project_id) {
7177 Some(window_handle)
7178 } else {
7179 None
7180 }
7181 })
7182 .unwrap_or(None)
7183 })
7184 });
7185
7186 let workspace = if let Some(existing_workspace) = existing_workspace {
7187 existing_workspace
7188 } else {
7189 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
7190 let room = active_call
7191 .read_with(cx, |call, _| call.room().cloned())?
7192 .context("not in a call")?;
7193 let project = room
7194 .update(cx, |room, cx| {
7195 room.join_project(
7196 project_id,
7197 app_state.languages.clone(),
7198 app_state.fs.clone(),
7199 cx,
7200 )
7201 })?
7202 .await?;
7203
7204 let window_bounds_override = window_bounds_env_override();
7205 cx.update(|cx| {
7206 let mut options = (app_state.build_window_options)(None, cx);
7207 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
7208 cx.open_window(options, |window, cx| {
7209 cx.new(|cx| {
7210 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
7211 })
7212 })
7213 })??
7214 };
7215
7216 workspace.update(cx, |workspace, window, cx| {
7217 cx.activate(true);
7218 window.activate_window();
7219
7220 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
7221 let follow_peer_id = room
7222 .read(cx)
7223 .remote_participants()
7224 .iter()
7225 .find(|(_, participant)| participant.user.id == follow_user_id)
7226 .map(|(_, p)| p.peer_id)
7227 .or_else(|| {
7228 // If we couldn't follow the given user, follow the host instead.
7229 let collaborator = workspace
7230 .project()
7231 .read(cx)
7232 .collaborators()
7233 .values()
7234 .find(|collaborator| collaborator.is_host)?;
7235 Some(collaborator.peer_id)
7236 });
7237
7238 if let Some(follow_peer_id) = follow_peer_id {
7239 workspace.follow(follow_peer_id, window, cx);
7240 }
7241 }
7242 })?;
7243
7244 anyhow::Ok(())
7245 })
7246}
7247
7248pub fn reload(reload: &Reload, cx: &mut App) {
7249 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
7250 let mut workspace_windows = cx
7251 .windows()
7252 .into_iter()
7253 .filter_map(|window| window.downcast::<Workspace>())
7254 .collect::<Vec<_>>();
7255
7256 // If multiple windows have unsaved changes, and need a save prompt,
7257 // prompt in the active window before switching to a different window.
7258 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
7259
7260 let mut prompt = None;
7261 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
7262 prompt = window
7263 .update(cx, |_, window, cx| {
7264 window.prompt(
7265 PromptLevel::Info,
7266 "Are you sure you want to restart?",
7267 None,
7268 &["Restart", "Cancel"],
7269 cx,
7270 )
7271 })
7272 .ok();
7273 }
7274
7275 let binary_path = reload.binary_path.clone();
7276 cx.spawn(async move |cx| {
7277 if let Some(prompt) = prompt {
7278 let answer = prompt.await?;
7279 if answer != 0 {
7280 return Ok(());
7281 }
7282 }
7283
7284 // If the user cancels any save prompt, then keep the app open.
7285 for window in workspace_windows {
7286 if let Ok(should_close) = window.update(cx, |workspace, window, cx| {
7287 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
7288 }) {
7289 if !should_close.await? {
7290 return Ok(());
7291 }
7292 }
7293 }
7294
7295 cx.update(|cx| cx.restart(binary_path))
7296 })
7297 .detach_and_log_err(cx);
7298}
7299
7300fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
7301 let mut parts = value.split(',');
7302 let x: usize = parts.next()?.parse().ok()?;
7303 let y: usize = parts.next()?.parse().ok()?;
7304 Some(point(px(x as f32), px(y as f32)))
7305}
7306
7307fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
7308 let mut parts = value.split(',');
7309 let width: usize = parts.next()?.parse().ok()?;
7310 let height: usize = parts.next()?.parse().ok()?;
7311 Some(size(px(width as f32), px(height as f32)))
7312}
7313
7314pub fn client_side_decorations(
7315 element: impl IntoElement,
7316 window: &mut Window,
7317 cx: &mut App,
7318) -> Stateful<Div> {
7319 const BORDER_SIZE: Pixels = px(1.0);
7320 let decorations = window.window_decorations();
7321
7322 if matches!(decorations, Decorations::Client { .. }) {
7323 window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
7324 }
7325
7326 struct GlobalResizeEdge(ResizeEdge);
7327 impl Global for GlobalResizeEdge {}
7328
7329 div()
7330 .id("window-backdrop")
7331 .bg(transparent_black())
7332 .map(|div| match decorations {
7333 Decorations::Server => div,
7334 Decorations::Client { tiling, .. } => div
7335 .when(!(tiling.top || tiling.right), |div| {
7336 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7337 })
7338 .when(!(tiling.top || tiling.left), |div| {
7339 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7340 })
7341 .when(!(tiling.bottom || tiling.right), |div| {
7342 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7343 })
7344 .when(!(tiling.bottom || tiling.left), |div| {
7345 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7346 })
7347 .when(!tiling.top, |div| {
7348 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
7349 })
7350 .when(!tiling.bottom, |div| {
7351 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
7352 })
7353 .when(!tiling.left, |div| {
7354 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
7355 })
7356 .when(!tiling.right, |div| {
7357 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
7358 })
7359 .on_mouse_move(move |e, window, cx| {
7360 let size = window.window_bounds().get_bounds().size;
7361 let pos = e.position;
7362
7363 let new_edge =
7364 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
7365
7366 let edge = cx.try_global::<GlobalResizeEdge>();
7367 if new_edge != edge.map(|edge| edge.0) {
7368 window
7369 .window_handle()
7370 .update(cx, |workspace, _, cx| {
7371 cx.notify(workspace.entity_id());
7372 })
7373 .ok();
7374 }
7375 })
7376 .on_mouse_down(MouseButton::Left, move |e, window, _| {
7377 let size = window.window_bounds().get_bounds().size;
7378 let pos = e.position;
7379
7380 let edge = match resize_edge(
7381 pos,
7382 theme::CLIENT_SIDE_DECORATION_SHADOW,
7383 size,
7384 tiling,
7385 ) {
7386 Some(value) => value,
7387 None => return,
7388 };
7389
7390 window.start_window_resize(edge);
7391 }),
7392 })
7393 .size_full()
7394 .child(
7395 div()
7396 .cursor(CursorStyle::Arrow)
7397 .map(|div| match decorations {
7398 Decorations::Server => div,
7399 Decorations::Client { tiling } => div
7400 .border_color(cx.theme().colors().border)
7401 .when(!(tiling.top || tiling.right), |div| {
7402 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7403 })
7404 .when(!(tiling.top || tiling.left), |div| {
7405 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7406 })
7407 .when(!(tiling.bottom || tiling.right), |div| {
7408 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7409 })
7410 .when(!(tiling.bottom || tiling.left), |div| {
7411 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7412 })
7413 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
7414 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
7415 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
7416 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
7417 .when(!tiling.is_tiled(), |div| {
7418 div.shadow(vec![gpui::BoxShadow {
7419 color: Hsla {
7420 h: 0.,
7421 s: 0.,
7422 l: 0.,
7423 a: 0.4,
7424 },
7425 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
7426 spread_radius: px(0.),
7427 offset: point(px(0.0), px(0.0)),
7428 }])
7429 }),
7430 })
7431 .on_mouse_move(|_e, _, cx| {
7432 cx.stop_propagation();
7433 })
7434 .size_full()
7435 .child(element),
7436 )
7437 .map(|div| match decorations {
7438 Decorations::Server => div,
7439 Decorations::Client { tiling, .. } => div.child(
7440 canvas(
7441 |_bounds, window, _| {
7442 window.insert_hitbox(
7443 Bounds::new(
7444 point(px(0.0), px(0.0)),
7445 window.window_bounds().get_bounds().size,
7446 ),
7447 HitboxBehavior::Normal,
7448 )
7449 },
7450 move |_bounds, hitbox, window, cx| {
7451 let mouse = window.mouse_position();
7452 let size = window.window_bounds().get_bounds().size;
7453 let Some(edge) =
7454 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
7455 else {
7456 return;
7457 };
7458 cx.set_global(GlobalResizeEdge(edge));
7459 window.set_cursor_style(
7460 match edge {
7461 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
7462 ResizeEdge::Left | ResizeEdge::Right => {
7463 CursorStyle::ResizeLeftRight
7464 }
7465 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
7466 CursorStyle::ResizeUpLeftDownRight
7467 }
7468 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
7469 CursorStyle::ResizeUpRightDownLeft
7470 }
7471 },
7472 &hitbox,
7473 );
7474 },
7475 )
7476 .size_full()
7477 .absolute(),
7478 ),
7479 })
7480}
7481
7482fn resize_edge(
7483 pos: Point<Pixels>,
7484 shadow_size: Pixels,
7485 window_size: Size<Pixels>,
7486 tiling: Tiling,
7487) -> Option<ResizeEdge> {
7488 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
7489 if bounds.contains(&pos) {
7490 return None;
7491 }
7492
7493 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
7494 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
7495 if !tiling.top && top_left_bounds.contains(&pos) {
7496 return Some(ResizeEdge::TopLeft);
7497 }
7498
7499 let top_right_bounds = Bounds::new(
7500 Point::new(window_size.width - corner_size.width, px(0.)),
7501 corner_size,
7502 );
7503 if !tiling.top && top_right_bounds.contains(&pos) {
7504 return Some(ResizeEdge::TopRight);
7505 }
7506
7507 let bottom_left_bounds = Bounds::new(
7508 Point::new(px(0.), window_size.height - corner_size.height),
7509 corner_size,
7510 );
7511 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
7512 return Some(ResizeEdge::BottomLeft);
7513 }
7514
7515 let bottom_right_bounds = Bounds::new(
7516 Point::new(
7517 window_size.width - corner_size.width,
7518 window_size.height - corner_size.height,
7519 ),
7520 corner_size,
7521 );
7522 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
7523 return Some(ResizeEdge::BottomRight);
7524 }
7525
7526 if !tiling.top && pos.y < shadow_size {
7527 Some(ResizeEdge::Top)
7528 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
7529 Some(ResizeEdge::Bottom)
7530 } else if !tiling.left && pos.x < shadow_size {
7531 Some(ResizeEdge::Left)
7532 } else if !tiling.right && pos.x > window_size.width - shadow_size {
7533 Some(ResizeEdge::Right)
7534 } else {
7535 None
7536 }
7537}
7538
7539fn join_pane_into_active(
7540 active_pane: &Entity<Pane>,
7541 pane: &Entity<Pane>,
7542 window: &mut Window,
7543 cx: &mut App,
7544) {
7545 if pane == active_pane {
7546 return;
7547 } else if pane.read(cx).items_len() == 0 {
7548 pane.update(cx, |_, cx| {
7549 cx.emit(pane::Event::Remove {
7550 focus_on_pane: None,
7551 });
7552 })
7553 } else {
7554 move_all_items(pane, active_pane, window, cx);
7555 }
7556}
7557
7558fn move_all_items(
7559 from_pane: &Entity<Pane>,
7560 to_pane: &Entity<Pane>,
7561 window: &mut Window,
7562 cx: &mut App,
7563) {
7564 let destination_is_different = from_pane != to_pane;
7565 let mut moved_items = 0;
7566 for (item_ix, item_handle) in from_pane
7567 .read(cx)
7568 .items()
7569 .enumerate()
7570 .map(|(ix, item)| (ix, item.clone()))
7571 .collect::<Vec<_>>()
7572 {
7573 let ix = item_ix - moved_items;
7574 if destination_is_different {
7575 // Close item from previous pane
7576 from_pane.update(cx, |source, cx| {
7577 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
7578 });
7579 moved_items += 1;
7580 }
7581
7582 // This automatically removes duplicate items in the pane
7583 to_pane.update(cx, |destination, cx| {
7584 destination.add_item(item_handle, true, true, None, window, cx);
7585 window.focus(&destination.focus_handle(cx))
7586 });
7587 }
7588}
7589
7590pub fn move_item(
7591 source: &Entity<Pane>,
7592 destination: &Entity<Pane>,
7593 item_id_to_move: EntityId,
7594 destination_index: usize,
7595 activate: bool,
7596 window: &mut Window,
7597 cx: &mut App,
7598) {
7599 let Some((item_ix, item_handle)) = source
7600 .read(cx)
7601 .items()
7602 .enumerate()
7603 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
7604 .map(|(ix, item)| (ix, item.clone()))
7605 else {
7606 // Tab was closed during drag
7607 return;
7608 };
7609
7610 if source != destination {
7611 // Close item from previous pane
7612 source.update(cx, |source, cx| {
7613 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
7614 });
7615 }
7616
7617 // This automatically removes duplicate items in the pane
7618 destination.update(cx, |destination, cx| {
7619 destination.add_item_inner(
7620 item_handle,
7621 activate,
7622 activate,
7623 activate,
7624 Some(destination_index),
7625 window,
7626 cx,
7627 );
7628 if activate {
7629 window.focus(&destination.focus_handle(cx))
7630 }
7631 });
7632}
7633
7634pub fn move_active_item(
7635 source: &Entity<Pane>,
7636 destination: &Entity<Pane>,
7637 focus_destination: bool,
7638 close_if_empty: bool,
7639 window: &mut Window,
7640 cx: &mut App,
7641) {
7642 if source == destination {
7643 return;
7644 }
7645 let Some(active_item) = source.read(cx).active_item() else {
7646 return;
7647 };
7648 source.update(cx, |source_pane, cx| {
7649 let item_id = active_item.item_id();
7650 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
7651 destination.update(cx, |target_pane, cx| {
7652 target_pane.add_item(
7653 active_item,
7654 focus_destination,
7655 focus_destination,
7656 Some(target_pane.items_len()),
7657 window,
7658 cx,
7659 );
7660 });
7661 });
7662}
7663
7664pub fn clone_active_item(
7665 workspace_id: Option<WorkspaceId>,
7666 source: &Entity<Pane>,
7667 destination: &Entity<Pane>,
7668 focus_destination: bool,
7669 window: &mut Window,
7670 cx: &mut App,
7671) {
7672 if source == destination {
7673 return;
7674 }
7675 let Some(active_item) = source.read(cx).active_item() else {
7676 return;
7677 };
7678 destination.update(cx, |target_pane, cx| {
7679 let Some(clone) = active_item.clone_on_split(workspace_id, window, cx) else {
7680 return;
7681 };
7682 target_pane.add_item(
7683 clone,
7684 focus_destination,
7685 focus_destination,
7686 Some(target_pane.items_len()),
7687 window,
7688 cx,
7689 );
7690 });
7691}
7692
7693#[derive(Debug)]
7694pub struct WorkspacePosition {
7695 pub window_bounds: Option<WindowBounds>,
7696 pub display: Option<Uuid>,
7697 pub centered_layout: bool,
7698}
7699
7700pub fn ssh_workspace_position_from_db(
7701 host: String,
7702 port: Option<u16>,
7703 user: Option<String>,
7704 paths_to_open: &[PathBuf],
7705 cx: &App,
7706) -> Task<Result<WorkspacePosition>> {
7707 let paths = paths_to_open
7708 .iter()
7709 .map(|path| path.to_string_lossy().to_string())
7710 .collect::<Vec<_>>();
7711
7712 cx.background_spawn(async move {
7713 let serialized_ssh_project = persistence::DB
7714 .get_or_create_ssh_project(host, port, paths, user)
7715 .await
7716 .context("fetching serialized ssh project")?;
7717 let serialized_workspace =
7718 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
7719
7720 let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
7721 (Some(WindowBounds::Windowed(bounds)), None)
7722 } else {
7723 let restorable_bounds = serialized_workspace
7724 .as_ref()
7725 .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
7726 .or_else(|| {
7727 let (display, window_bounds) = DB.last_window().log_err()?;
7728 Some((display?, window_bounds?))
7729 });
7730
7731 if let Some((serialized_display, serialized_status)) = restorable_bounds {
7732 (Some(serialized_status.0), Some(serialized_display))
7733 } else {
7734 (None, None)
7735 }
7736 };
7737
7738 let centered_layout = serialized_workspace
7739 .as_ref()
7740 .map(|w| w.centered_layout)
7741 .unwrap_or(false);
7742
7743 Ok(WorkspacePosition {
7744 window_bounds,
7745 display,
7746 centered_layout,
7747 })
7748 })
7749}
7750
7751pub fn with_active_or_new_workspace(
7752 cx: &mut App,
7753 f: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + 'static,
7754) {
7755 match cx.active_window().and_then(|w| w.downcast::<Workspace>()) {
7756 Some(workspace) => {
7757 cx.defer(move |cx| {
7758 workspace
7759 .update(cx, |workspace, window, cx| f(workspace, window, cx))
7760 .log_err();
7761 });
7762 }
7763 None => {
7764 let app_state = AppState::global(cx);
7765 if let Some(app_state) = app_state.upgrade() {
7766 open_new(
7767 OpenOptions::default(),
7768 app_state,
7769 cx,
7770 move |workspace, window, cx| f(workspace, window, cx),
7771 )
7772 .detach_and_log_err(cx);
7773 }
7774 }
7775 }
7776}
7777
7778#[cfg(test)]
7779mod tests {
7780 use std::{cell::RefCell, rc::Rc};
7781
7782 use super::*;
7783 use crate::{
7784 dock::{PanelEvent, test::TestPanel},
7785 item::{
7786 ItemEvent,
7787 test::{TestItem, TestProjectItem},
7788 },
7789 };
7790 use fs::FakeFs;
7791 use gpui::{
7792 DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
7793 UpdateGlobal, VisualTestContext, px,
7794 };
7795 use project::{Project, ProjectEntryId};
7796 use serde_json::json;
7797 use settings::SettingsStore;
7798
7799 #[gpui::test]
7800 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
7801 init_test(cx);
7802
7803 let fs = FakeFs::new(cx.executor());
7804 let project = Project::test(fs, [], cx).await;
7805 let (workspace, cx) =
7806 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7807
7808 // Adding an item with no ambiguity renders the tab without detail.
7809 let item1 = cx.new(|cx| {
7810 let mut item = TestItem::new(cx);
7811 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
7812 item
7813 });
7814 workspace.update_in(cx, |workspace, window, cx| {
7815 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7816 });
7817 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
7818
7819 // Adding an item that creates ambiguity increases the level of detail on
7820 // both tabs.
7821 let item2 = cx.new_window_entity(|_window, cx| {
7822 let mut item = TestItem::new(cx);
7823 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7824 item
7825 });
7826 workspace.update_in(cx, |workspace, window, cx| {
7827 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7828 });
7829 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7830 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7831
7832 // Adding an item that creates ambiguity increases the level of detail only
7833 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
7834 // we stop at the highest detail available.
7835 let item3 = cx.new(|cx| {
7836 let mut item = TestItem::new(cx);
7837 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7838 item
7839 });
7840 workspace.update_in(cx, |workspace, window, cx| {
7841 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7842 });
7843 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7844 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7845 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7846 }
7847
7848 #[gpui::test]
7849 async fn test_tracking_active_path(cx: &mut TestAppContext) {
7850 init_test(cx);
7851
7852 let fs = FakeFs::new(cx.executor());
7853 fs.insert_tree(
7854 "/root1",
7855 json!({
7856 "one.txt": "",
7857 "two.txt": "",
7858 }),
7859 )
7860 .await;
7861 fs.insert_tree(
7862 "/root2",
7863 json!({
7864 "three.txt": "",
7865 }),
7866 )
7867 .await;
7868
7869 let project = Project::test(fs, ["root1".as_ref()], cx).await;
7870 let (workspace, cx) =
7871 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7872 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
7873 let worktree_id = project.update(cx, |project, cx| {
7874 project.worktrees(cx).next().unwrap().read(cx).id()
7875 });
7876
7877 let item1 = cx.new(|cx| {
7878 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
7879 });
7880 let item2 = cx.new(|cx| {
7881 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
7882 });
7883
7884 // Add an item to an empty pane
7885 workspace.update_in(cx, |workspace, window, cx| {
7886 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
7887 });
7888 project.update(cx, |project, cx| {
7889 assert_eq!(
7890 project.active_entry(),
7891 project
7892 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7893 .map(|e| e.id)
7894 );
7895 });
7896 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7897
7898 // Add a second item to a non-empty pane
7899 workspace.update_in(cx, |workspace, window, cx| {
7900 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
7901 });
7902 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
7903 project.update(cx, |project, cx| {
7904 assert_eq!(
7905 project.active_entry(),
7906 project
7907 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
7908 .map(|e| e.id)
7909 );
7910 });
7911
7912 // Close the active item
7913 pane.update_in(cx, |pane, window, cx| {
7914 pane.close_active_item(&Default::default(), window, cx)
7915 })
7916 .await
7917 .unwrap();
7918 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7919 project.update(cx, |project, cx| {
7920 assert_eq!(
7921 project.active_entry(),
7922 project
7923 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7924 .map(|e| e.id)
7925 );
7926 });
7927
7928 // Add a project folder
7929 project
7930 .update(cx, |project, cx| {
7931 project.find_or_create_worktree("root2", true, cx)
7932 })
7933 .await
7934 .unwrap();
7935 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
7936
7937 // Remove a project folder
7938 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
7939 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
7940 }
7941
7942 #[gpui::test]
7943 async fn test_close_window(cx: &mut TestAppContext) {
7944 init_test(cx);
7945
7946 let fs = FakeFs::new(cx.executor());
7947 fs.insert_tree("/root", json!({ "one": "" })).await;
7948
7949 let project = Project::test(fs, ["root".as_ref()], cx).await;
7950 let (workspace, cx) =
7951 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7952
7953 // When there are no dirty items, there's nothing to do.
7954 let item1 = cx.new(TestItem::new);
7955 workspace.update_in(cx, |w, window, cx| {
7956 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
7957 });
7958 let task = workspace.update_in(cx, |w, window, cx| {
7959 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7960 });
7961 assert!(task.await.unwrap());
7962
7963 // When there are dirty untitled items, prompt to save each one. If the user
7964 // cancels any prompt, then abort.
7965 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
7966 let item3 = cx.new(|cx| {
7967 TestItem::new(cx)
7968 .with_dirty(true)
7969 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7970 });
7971 workspace.update_in(cx, |w, window, cx| {
7972 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7973 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7974 });
7975 let task = workspace.update_in(cx, |w, window, cx| {
7976 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7977 });
7978 cx.executor().run_until_parked();
7979 cx.simulate_prompt_answer("Cancel"); // cancel save all
7980 cx.executor().run_until_parked();
7981 assert!(!cx.has_pending_prompt());
7982 assert!(!task.await.unwrap());
7983 }
7984
7985 #[gpui::test]
7986 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
7987 init_test(cx);
7988
7989 // Register TestItem as a serializable item
7990 cx.update(|cx| {
7991 register_serializable_item::<TestItem>(cx);
7992 });
7993
7994 let fs = FakeFs::new(cx.executor());
7995 fs.insert_tree("/root", json!({ "one": "" })).await;
7996
7997 let project = Project::test(fs, ["root".as_ref()], cx).await;
7998 let (workspace, cx) =
7999 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8000
8001 // When there are dirty untitled items, but they can serialize, then there is no prompt.
8002 let item1 = cx.new(|cx| {
8003 TestItem::new(cx)
8004 .with_dirty(true)
8005 .with_serialize(|| Some(Task::ready(Ok(()))))
8006 });
8007 let item2 = cx.new(|cx| {
8008 TestItem::new(cx)
8009 .with_dirty(true)
8010 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
8011 .with_serialize(|| Some(Task::ready(Ok(()))))
8012 });
8013 workspace.update_in(cx, |w, window, cx| {
8014 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
8015 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
8016 });
8017 let task = workspace.update_in(cx, |w, window, cx| {
8018 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
8019 });
8020 assert!(task.await.unwrap());
8021 }
8022
8023 #[gpui::test]
8024 async fn test_close_pane_items(cx: &mut TestAppContext) {
8025 init_test(cx);
8026
8027 let fs = FakeFs::new(cx.executor());
8028
8029 let project = Project::test(fs, None, cx).await;
8030 let (workspace, cx) =
8031 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8032
8033 let item1 = cx.new(|cx| {
8034 TestItem::new(cx)
8035 .with_dirty(true)
8036 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8037 });
8038 let item2 = cx.new(|cx| {
8039 TestItem::new(cx)
8040 .with_dirty(true)
8041 .with_conflict(true)
8042 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8043 });
8044 let item3 = cx.new(|cx| {
8045 TestItem::new(cx)
8046 .with_dirty(true)
8047 .with_conflict(true)
8048 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
8049 });
8050 let item4 = cx.new(|cx| {
8051 TestItem::new(cx).with_dirty(true).with_project_items(&[{
8052 let project_item = TestProjectItem::new_untitled(cx);
8053 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
8054 project_item
8055 }])
8056 });
8057 let pane = workspace.update_in(cx, |workspace, window, cx| {
8058 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
8059 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
8060 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
8061 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
8062 workspace.active_pane().clone()
8063 });
8064
8065 let close_items = pane.update_in(cx, |pane, window, cx| {
8066 pane.activate_item(1, true, true, window, cx);
8067 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
8068 let item1_id = item1.item_id();
8069 let item3_id = item3.item_id();
8070 let item4_id = item4.item_id();
8071 pane.close_items(window, cx, SaveIntent::Close, move |id| {
8072 [item1_id, item3_id, item4_id].contains(&id)
8073 })
8074 });
8075 cx.executor().run_until_parked();
8076
8077 assert!(cx.has_pending_prompt());
8078 cx.simulate_prompt_answer("Save all");
8079
8080 cx.executor().run_until_parked();
8081
8082 // Item 1 is saved. There's a prompt to save item 3.
8083 pane.update(cx, |pane, cx| {
8084 assert_eq!(item1.read(cx).save_count, 1);
8085 assert_eq!(item1.read(cx).save_as_count, 0);
8086 assert_eq!(item1.read(cx).reload_count, 0);
8087 assert_eq!(pane.items_len(), 3);
8088 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
8089 });
8090 assert!(cx.has_pending_prompt());
8091
8092 // Cancel saving item 3.
8093 cx.simulate_prompt_answer("Discard");
8094 cx.executor().run_until_parked();
8095
8096 // Item 3 is reloaded. There's a prompt to save item 4.
8097 pane.update(cx, |pane, cx| {
8098 assert_eq!(item3.read(cx).save_count, 0);
8099 assert_eq!(item3.read(cx).save_as_count, 0);
8100 assert_eq!(item3.read(cx).reload_count, 1);
8101 assert_eq!(pane.items_len(), 2);
8102 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
8103 });
8104
8105 // There's a prompt for a path for item 4.
8106 cx.simulate_new_path_selection(|_| Some(Default::default()));
8107 close_items.await.unwrap();
8108
8109 // The requested items are closed.
8110 pane.update(cx, |pane, cx| {
8111 assert_eq!(item4.read(cx).save_count, 0);
8112 assert_eq!(item4.read(cx).save_as_count, 1);
8113 assert_eq!(item4.read(cx).reload_count, 0);
8114 assert_eq!(pane.items_len(), 1);
8115 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
8116 });
8117 }
8118
8119 #[gpui::test]
8120 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
8121 init_test(cx);
8122
8123 let fs = FakeFs::new(cx.executor());
8124 let project = Project::test(fs, [], cx).await;
8125 let (workspace, cx) =
8126 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8127
8128 // Create several workspace items with single project entries, and two
8129 // workspace items with multiple project entries.
8130 let single_entry_items = (0..=4)
8131 .map(|project_entry_id| {
8132 cx.new(|cx| {
8133 TestItem::new(cx)
8134 .with_dirty(true)
8135 .with_project_items(&[dirty_project_item(
8136 project_entry_id,
8137 &format!("{project_entry_id}.txt"),
8138 cx,
8139 )])
8140 })
8141 })
8142 .collect::<Vec<_>>();
8143 let item_2_3 = cx.new(|cx| {
8144 TestItem::new(cx)
8145 .with_dirty(true)
8146 .with_singleton(false)
8147 .with_project_items(&[
8148 single_entry_items[2].read(cx).project_items[0].clone(),
8149 single_entry_items[3].read(cx).project_items[0].clone(),
8150 ])
8151 });
8152 let item_3_4 = cx.new(|cx| {
8153 TestItem::new(cx)
8154 .with_dirty(true)
8155 .with_singleton(false)
8156 .with_project_items(&[
8157 single_entry_items[3].read(cx).project_items[0].clone(),
8158 single_entry_items[4].read(cx).project_items[0].clone(),
8159 ])
8160 });
8161
8162 // Create two panes that contain the following project entries:
8163 // left pane:
8164 // multi-entry items: (2, 3)
8165 // single-entry items: 0, 2, 3, 4
8166 // right pane:
8167 // single-entry items: 4, 1
8168 // multi-entry items: (3, 4)
8169 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
8170 let left_pane = workspace.active_pane().clone();
8171 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
8172 workspace.add_item_to_active_pane(
8173 single_entry_items[0].boxed_clone(),
8174 None,
8175 true,
8176 window,
8177 cx,
8178 );
8179 workspace.add_item_to_active_pane(
8180 single_entry_items[2].boxed_clone(),
8181 None,
8182 true,
8183 window,
8184 cx,
8185 );
8186 workspace.add_item_to_active_pane(
8187 single_entry_items[3].boxed_clone(),
8188 None,
8189 true,
8190 window,
8191 cx,
8192 );
8193 workspace.add_item_to_active_pane(
8194 single_entry_items[4].boxed_clone(),
8195 None,
8196 true,
8197 window,
8198 cx,
8199 );
8200
8201 let right_pane = workspace
8202 .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
8203 .unwrap();
8204
8205 right_pane.update(cx, |pane, cx| {
8206 pane.add_item(
8207 single_entry_items[1].boxed_clone(),
8208 true,
8209 true,
8210 None,
8211 window,
8212 cx,
8213 );
8214 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
8215 });
8216
8217 (left_pane, right_pane)
8218 });
8219
8220 cx.focus(&right_pane);
8221
8222 let mut close = right_pane.update_in(cx, |pane, window, cx| {
8223 pane.close_all_items(&CloseAllItems::default(), window, cx)
8224 .unwrap()
8225 });
8226 cx.executor().run_until_parked();
8227
8228 let msg = cx.pending_prompt().unwrap().0;
8229 assert!(msg.contains("1.txt"));
8230 assert!(!msg.contains("2.txt"));
8231 assert!(!msg.contains("3.txt"));
8232 assert!(!msg.contains("4.txt"));
8233
8234 cx.simulate_prompt_answer("Cancel");
8235 close.await;
8236
8237 left_pane
8238 .update_in(cx, |left_pane, window, cx| {
8239 left_pane.close_item_by_id(
8240 single_entry_items[3].entity_id(),
8241 SaveIntent::Skip,
8242 window,
8243 cx,
8244 )
8245 })
8246 .await
8247 .unwrap();
8248
8249 close = right_pane.update_in(cx, |pane, window, cx| {
8250 pane.close_all_items(&CloseAllItems::default(), window, cx)
8251 .unwrap()
8252 });
8253 cx.executor().run_until_parked();
8254
8255 let details = cx.pending_prompt().unwrap().1;
8256 assert!(details.contains("1.txt"));
8257 assert!(!details.contains("2.txt"));
8258 assert!(details.contains("3.txt"));
8259 // ideally this assertion could be made, but today we can only
8260 // save whole items not project items, so the orphaned item 3 causes
8261 // 4 to be saved too.
8262 // assert!(!details.contains("4.txt"));
8263
8264 cx.simulate_prompt_answer("Save all");
8265
8266 cx.executor().run_until_parked();
8267 close.await;
8268 right_pane.read_with(cx, |pane, _| {
8269 assert_eq!(pane.items_len(), 0);
8270 });
8271 }
8272
8273 #[gpui::test]
8274 async fn test_autosave(cx: &mut gpui::TestAppContext) {
8275 init_test(cx);
8276
8277 let fs = FakeFs::new(cx.executor());
8278 let project = Project::test(fs, [], cx).await;
8279 let (workspace, cx) =
8280 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8281 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8282
8283 let item = cx.new(|cx| {
8284 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
8285 });
8286 let item_id = item.entity_id();
8287 workspace.update_in(cx, |workspace, window, cx| {
8288 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8289 });
8290
8291 // Autosave on window change.
8292 item.update(cx, |item, cx| {
8293 SettingsStore::update_global(cx, |settings, cx| {
8294 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8295 settings.autosave = Some(AutosaveSetting::OnWindowChange);
8296 })
8297 });
8298 item.is_dirty = true;
8299 });
8300
8301 // Deactivating the window saves the file.
8302 cx.deactivate_window();
8303 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
8304
8305 // Re-activating the window doesn't save the file.
8306 cx.update(|window, _| window.activate_window());
8307 cx.executor().run_until_parked();
8308 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
8309
8310 // Autosave on focus change.
8311 item.update_in(cx, |item, window, cx| {
8312 cx.focus_self(window);
8313 SettingsStore::update_global(cx, |settings, cx| {
8314 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8315 settings.autosave = Some(AutosaveSetting::OnFocusChange);
8316 })
8317 });
8318 item.is_dirty = true;
8319 });
8320
8321 // Blurring the item saves the file.
8322 item.update_in(cx, |_, window, _| window.blur());
8323 cx.executor().run_until_parked();
8324 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
8325
8326 // Deactivating the window still saves the file.
8327 item.update_in(cx, |item, window, cx| {
8328 cx.focus_self(window);
8329 item.is_dirty = true;
8330 });
8331 cx.deactivate_window();
8332 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
8333
8334 // Autosave after delay.
8335 item.update(cx, |item, cx| {
8336 SettingsStore::update_global(cx, |settings, cx| {
8337 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8338 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
8339 })
8340 });
8341 item.is_dirty = true;
8342 cx.emit(ItemEvent::Edit);
8343 });
8344
8345 // Delay hasn't fully expired, so the file is still dirty and unsaved.
8346 cx.executor().advance_clock(Duration::from_millis(250));
8347 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
8348
8349 // After delay expires, the file is saved.
8350 cx.executor().advance_clock(Duration::from_millis(250));
8351 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
8352
8353 // Autosave on focus change, ensuring closing the tab counts as such.
8354 item.update(cx, |item, cx| {
8355 SettingsStore::update_global(cx, |settings, cx| {
8356 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8357 settings.autosave = Some(AutosaveSetting::OnFocusChange);
8358 })
8359 });
8360 item.is_dirty = true;
8361 for project_item in &mut item.project_items {
8362 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
8363 }
8364 });
8365
8366 pane.update_in(cx, |pane, window, cx| {
8367 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
8368 })
8369 .await
8370 .unwrap();
8371 assert!(!cx.has_pending_prompt());
8372 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
8373
8374 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
8375 workspace.update_in(cx, |workspace, window, cx| {
8376 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8377 });
8378 item.update_in(cx, |item, window, cx| {
8379 item.project_items[0].update(cx, |item, _| {
8380 item.entry_id = None;
8381 });
8382 item.is_dirty = true;
8383 window.blur();
8384 });
8385 cx.run_until_parked();
8386 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
8387
8388 // Ensure autosave is prevented for deleted files also when closing the buffer.
8389 let _close_items = pane.update_in(cx, |pane, window, cx| {
8390 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
8391 });
8392 cx.run_until_parked();
8393 assert!(cx.has_pending_prompt());
8394 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
8395 }
8396
8397 #[gpui::test]
8398 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
8399 init_test(cx);
8400
8401 let fs = FakeFs::new(cx.executor());
8402
8403 let project = Project::test(fs, [], cx).await;
8404 let (workspace, cx) =
8405 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8406
8407 let item = cx.new(|cx| {
8408 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
8409 });
8410 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8411 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
8412 let toolbar_notify_count = Rc::new(RefCell::new(0));
8413
8414 workspace.update_in(cx, |workspace, window, cx| {
8415 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8416 let toolbar_notification_count = toolbar_notify_count.clone();
8417 cx.observe_in(&toolbar, window, move |_, _, _, _| {
8418 *toolbar_notification_count.borrow_mut() += 1
8419 })
8420 .detach();
8421 });
8422
8423 pane.read_with(cx, |pane, _| {
8424 assert!(!pane.can_navigate_backward());
8425 assert!(!pane.can_navigate_forward());
8426 });
8427
8428 item.update_in(cx, |item, _, cx| {
8429 item.set_state("one".to_string(), cx);
8430 });
8431
8432 // Toolbar must be notified to re-render the navigation buttons
8433 assert_eq!(*toolbar_notify_count.borrow(), 1);
8434
8435 pane.read_with(cx, |pane, _| {
8436 assert!(pane.can_navigate_backward());
8437 assert!(!pane.can_navigate_forward());
8438 });
8439
8440 workspace
8441 .update_in(cx, |workspace, window, cx| {
8442 workspace.go_back(pane.downgrade(), window, cx)
8443 })
8444 .await
8445 .unwrap();
8446
8447 assert_eq!(*toolbar_notify_count.borrow(), 2);
8448 pane.read_with(cx, |pane, _| {
8449 assert!(!pane.can_navigate_backward());
8450 assert!(pane.can_navigate_forward());
8451 });
8452 }
8453
8454 #[gpui::test]
8455 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
8456 init_test(cx);
8457 let fs = FakeFs::new(cx.executor());
8458
8459 let project = Project::test(fs, [], cx).await;
8460 let (workspace, cx) =
8461 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8462
8463 let panel = workspace.update_in(cx, |workspace, window, cx| {
8464 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8465 workspace.add_panel(panel.clone(), window, cx);
8466
8467 workspace
8468 .right_dock()
8469 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8470
8471 panel
8472 });
8473
8474 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8475 pane.update_in(cx, |pane, window, cx| {
8476 let item = cx.new(TestItem::new);
8477 pane.add_item(Box::new(item), true, true, None, window, cx);
8478 });
8479
8480 // Transfer focus from center to panel
8481 workspace.update_in(cx, |workspace, window, cx| {
8482 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8483 });
8484
8485 workspace.update_in(cx, |workspace, window, cx| {
8486 assert!(workspace.right_dock().read(cx).is_open());
8487 assert!(!panel.is_zoomed(window, cx));
8488 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8489 });
8490
8491 // Transfer focus from panel to center
8492 workspace.update_in(cx, |workspace, window, cx| {
8493 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8494 });
8495
8496 workspace.update_in(cx, |workspace, window, cx| {
8497 assert!(workspace.right_dock().read(cx).is_open());
8498 assert!(!panel.is_zoomed(window, cx));
8499 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8500 });
8501
8502 // Close the dock
8503 workspace.update_in(cx, |workspace, window, cx| {
8504 workspace.toggle_dock(DockPosition::Right, window, cx);
8505 });
8506
8507 workspace.update_in(cx, |workspace, window, cx| {
8508 assert!(!workspace.right_dock().read(cx).is_open());
8509 assert!(!panel.is_zoomed(window, cx));
8510 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8511 });
8512
8513 // Open the dock
8514 workspace.update_in(cx, |workspace, window, cx| {
8515 workspace.toggle_dock(DockPosition::Right, window, cx);
8516 });
8517
8518 workspace.update_in(cx, |workspace, window, cx| {
8519 assert!(workspace.right_dock().read(cx).is_open());
8520 assert!(!panel.is_zoomed(window, cx));
8521 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8522 });
8523
8524 // Focus and zoom panel
8525 panel.update_in(cx, |panel, window, cx| {
8526 cx.focus_self(window);
8527 panel.set_zoomed(true, window, cx)
8528 });
8529
8530 workspace.update_in(cx, |workspace, window, cx| {
8531 assert!(workspace.right_dock().read(cx).is_open());
8532 assert!(panel.is_zoomed(window, cx));
8533 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8534 });
8535
8536 // Transfer focus to the center closes the dock
8537 workspace.update_in(cx, |workspace, window, cx| {
8538 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8539 });
8540
8541 workspace.update_in(cx, |workspace, window, cx| {
8542 assert!(!workspace.right_dock().read(cx).is_open());
8543 assert!(panel.is_zoomed(window, cx));
8544 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8545 });
8546
8547 // Transferring focus back to the panel keeps it zoomed
8548 workspace.update_in(cx, |workspace, window, cx| {
8549 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8550 });
8551
8552 workspace.update_in(cx, |workspace, window, cx| {
8553 assert!(workspace.right_dock().read(cx).is_open());
8554 assert!(panel.is_zoomed(window, cx));
8555 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8556 });
8557
8558 // Close the dock while it is zoomed
8559 workspace.update_in(cx, |workspace, window, cx| {
8560 workspace.toggle_dock(DockPosition::Right, window, cx)
8561 });
8562
8563 workspace.update_in(cx, |workspace, window, cx| {
8564 assert!(!workspace.right_dock().read(cx).is_open());
8565 assert!(panel.is_zoomed(window, cx));
8566 assert!(workspace.zoomed.is_none());
8567 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8568 });
8569
8570 // Opening the dock, when it's zoomed, retains focus
8571 workspace.update_in(cx, |workspace, window, cx| {
8572 workspace.toggle_dock(DockPosition::Right, window, cx)
8573 });
8574
8575 workspace.update_in(cx, |workspace, window, cx| {
8576 assert!(workspace.right_dock().read(cx).is_open());
8577 assert!(panel.is_zoomed(window, cx));
8578 assert!(workspace.zoomed.is_some());
8579 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8580 });
8581
8582 // Unzoom and close the panel, zoom the active pane.
8583 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
8584 workspace.update_in(cx, |workspace, window, cx| {
8585 workspace.toggle_dock(DockPosition::Right, window, cx)
8586 });
8587 pane.update_in(cx, |pane, window, cx| {
8588 pane.toggle_zoom(&Default::default(), window, cx)
8589 });
8590
8591 // Opening a dock unzooms the pane.
8592 workspace.update_in(cx, |workspace, window, cx| {
8593 workspace.toggle_dock(DockPosition::Right, window, cx)
8594 });
8595 workspace.update_in(cx, |workspace, window, cx| {
8596 let pane = pane.read(cx);
8597 assert!(!pane.is_zoomed());
8598 assert!(!pane.focus_handle(cx).is_focused(window));
8599 assert!(workspace.right_dock().read(cx).is_open());
8600 assert!(workspace.zoomed.is_none());
8601 });
8602 }
8603
8604 #[gpui::test]
8605 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
8606 init_test(cx);
8607
8608 let fs = FakeFs::new(cx.executor());
8609
8610 let project = Project::test(fs, None, cx).await;
8611 let (workspace, cx) =
8612 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8613
8614 // Let's arrange the panes like this:
8615 //
8616 // +-----------------------+
8617 // | top |
8618 // +------+--------+-------+
8619 // | left | center | right |
8620 // +------+--------+-------+
8621 // | bottom |
8622 // +-----------------------+
8623
8624 let top_item = cx.new(|cx| {
8625 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
8626 });
8627 let bottom_item = cx.new(|cx| {
8628 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
8629 });
8630 let left_item = cx.new(|cx| {
8631 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
8632 });
8633 let right_item = cx.new(|cx| {
8634 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
8635 });
8636 let center_item = cx.new(|cx| {
8637 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
8638 });
8639
8640 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8641 let top_pane_id = workspace.active_pane().entity_id();
8642 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
8643 workspace.split_pane(
8644 workspace.active_pane().clone(),
8645 SplitDirection::Down,
8646 window,
8647 cx,
8648 );
8649 top_pane_id
8650 });
8651 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8652 let bottom_pane_id = workspace.active_pane().entity_id();
8653 workspace.add_item_to_active_pane(
8654 Box::new(bottom_item.clone()),
8655 None,
8656 false,
8657 window,
8658 cx,
8659 );
8660 workspace.split_pane(
8661 workspace.active_pane().clone(),
8662 SplitDirection::Up,
8663 window,
8664 cx,
8665 );
8666 bottom_pane_id
8667 });
8668 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8669 let left_pane_id = workspace.active_pane().entity_id();
8670 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
8671 workspace.split_pane(
8672 workspace.active_pane().clone(),
8673 SplitDirection::Right,
8674 window,
8675 cx,
8676 );
8677 left_pane_id
8678 });
8679 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8680 let right_pane_id = workspace.active_pane().entity_id();
8681 workspace.add_item_to_active_pane(
8682 Box::new(right_item.clone()),
8683 None,
8684 false,
8685 window,
8686 cx,
8687 );
8688 workspace.split_pane(
8689 workspace.active_pane().clone(),
8690 SplitDirection::Left,
8691 window,
8692 cx,
8693 );
8694 right_pane_id
8695 });
8696 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8697 let center_pane_id = workspace.active_pane().entity_id();
8698 workspace.add_item_to_active_pane(
8699 Box::new(center_item.clone()),
8700 None,
8701 false,
8702 window,
8703 cx,
8704 );
8705 center_pane_id
8706 });
8707 cx.executor().run_until_parked();
8708
8709 workspace.update_in(cx, |workspace, window, cx| {
8710 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
8711
8712 // Join into next from center pane into right
8713 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8714 });
8715
8716 workspace.update_in(cx, |workspace, window, cx| {
8717 let active_pane = workspace.active_pane();
8718 assert_eq!(right_pane_id, active_pane.entity_id());
8719 assert_eq!(2, active_pane.read(cx).items_len());
8720 let item_ids_in_pane =
8721 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8722 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8723 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8724
8725 // Join into next from right pane into bottom
8726 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8727 });
8728
8729 workspace.update_in(cx, |workspace, window, cx| {
8730 let active_pane = workspace.active_pane();
8731 assert_eq!(bottom_pane_id, active_pane.entity_id());
8732 assert_eq!(3, active_pane.read(cx).items_len());
8733 let item_ids_in_pane =
8734 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8735 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8736 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8737 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8738
8739 // Join into next from bottom pane into left
8740 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8741 });
8742
8743 workspace.update_in(cx, |workspace, window, cx| {
8744 let active_pane = workspace.active_pane();
8745 assert_eq!(left_pane_id, active_pane.entity_id());
8746 assert_eq!(4, active_pane.read(cx).items_len());
8747 let item_ids_in_pane =
8748 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8749 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8750 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8751 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8752 assert!(item_ids_in_pane.contains(&left_item.item_id()));
8753
8754 // Join into next from left pane into top
8755 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8756 });
8757
8758 workspace.update_in(cx, |workspace, window, cx| {
8759 let active_pane = workspace.active_pane();
8760 assert_eq!(top_pane_id, active_pane.entity_id());
8761 assert_eq!(5, active_pane.read(cx).items_len());
8762 let item_ids_in_pane =
8763 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8764 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8765 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8766 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8767 assert!(item_ids_in_pane.contains(&left_item.item_id()));
8768 assert!(item_ids_in_pane.contains(&top_item.item_id()));
8769
8770 // Single pane left: no-op
8771 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
8772 });
8773
8774 workspace.update(cx, |workspace, _cx| {
8775 let active_pane = workspace.active_pane();
8776 assert_eq!(top_pane_id, active_pane.entity_id());
8777 });
8778 }
8779
8780 fn add_an_item_to_active_pane(
8781 cx: &mut VisualTestContext,
8782 workspace: &Entity<Workspace>,
8783 item_id: u64,
8784 ) -> Entity<TestItem> {
8785 let item = cx.new(|cx| {
8786 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
8787 item_id,
8788 "item{item_id}.txt",
8789 cx,
8790 )])
8791 });
8792 workspace.update_in(cx, |workspace, window, cx| {
8793 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
8794 });
8795 return item;
8796 }
8797
8798 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
8799 return workspace.update_in(cx, |workspace, window, cx| {
8800 let new_pane = workspace.split_pane(
8801 workspace.active_pane().clone(),
8802 SplitDirection::Right,
8803 window,
8804 cx,
8805 );
8806 new_pane
8807 });
8808 }
8809
8810 #[gpui::test]
8811 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
8812 init_test(cx);
8813 let fs = FakeFs::new(cx.executor());
8814 let project = Project::test(fs, None, cx).await;
8815 let (workspace, cx) =
8816 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8817
8818 add_an_item_to_active_pane(cx, &workspace, 1);
8819 split_pane(cx, &workspace);
8820 add_an_item_to_active_pane(cx, &workspace, 2);
8821 split_pane(cx, &workspace); // empty pane
8822 split_pane(cx, &workspace);
8823 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
8824
8825 cx.executor().run_until_parked();
8826
8827 workspace.update(cx, |workspace, cx| {
8828 let num_panes = workspace.panes().len();
8829 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8830 let active_item = workspace
8831 .active_pane()
8832 .read(cx)
8833 .active_item()
8834 .expect("item is in focus");
8835
8836 assert_eq!(num_panes, 4);
8837 assert_eq!(num_items_in_current_pane, 1);
8838 assert_eq!(active_item.item_id(), last_item.item_id());
8839 });
8840
8841 workspace.update_in(cx, |workspace, window, cx| {
8842 workspace.join_all_panes(window, cx);
8843 });
8844
8845 workspace.update(cx, |workspace, cx| {
8846 let num_panes = workspace.panes().len();
8847 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8848 let active_item = workspace
8849 .active_pane()
8850 .read(cx)
8851 .active_item()
8852 .expect("item is in focus");
8853
8854 assert_eq!(num_panes, 1);
8855 assert_eq!(num_items_in_current_pane, 3);
8856 assert_eq!(active_item.item_id(), last_item.item_id());
8857 });
8858 }
8859 struct TestModal(FocusHandle);
8860
8861 impl TestModal {
8862 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
8863 Self(cx.focus_handle())
8864 }
8865 }
8866
8867 impl EventEmitter<DismissEvent> for TestModal {}
8868
8869 impl Focusable for TestModal {
8870 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8871 self.0.clone()
8872 }
8873 }
8874
8875 impl ModalView for TestModal {}
8876
8877 impl Render for TestModal {
8878 fn render(
8879 &mut self,
8880 _window: &mut Window,
8881 _cx: &mut Context<TestModal>,
8882 ) -> impl IntoElement {
8883 div().track_focus(&self.0)
8884 }
8885 }
8886
8887 #[gpui::test]
8888 async fn test_panels(cx: &mut gpui::TestAppContext) {
8889 init_test(cx);
8890 let fs = FakeFs::new(cx.executor());
8891
8892 let project = Project::test(fs, [], cx).await;
8893 let (workspace, cx) =
8894 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8895
8896 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
8897 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
8898 workspace.add_panel(panel_1.clone(), window, cx);
8899 workspace.toggle_dock(DockPosition::Left, window, cx);
8900 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8901 workspace.add_panel(panel_2.clone(), window, cx);
8902 workspace.toggle_dock(DockPosition::Right, window, cx);
8903
8904 let left_dock = workspace.left_dock();
8905 assert_eq!(
8906 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8907 panel_1.panel_id()
8908 );
8909 assert_eq!(
8910 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8911 panel_1.size(window, cx)
8912 );
8913
8914 left_dock.update(cx, |left_dock, cx| {
8915 left_dock.resize_active_panel(Some(px(1337.)), window, cx)
8916 });
8917 assert_eq!(
8918 workspace
8919 .right_dock()
8920 .read(cx)
8921 .visible_panel()
8922 .unwrap()
8923 .panel_id(),
8924 panel_2.panel_id(),
8925 );
8926
8927 (panel_1, panel_2)
8928 });
8929
8930 // Move panel_1 to the right
8931 panel_1.update_in(cx, |panel_1, window, cx| {
8932 panel_1.set_position(DockPosition::Right, window, cx)
8933 });
8934
8935 workspace.update_in(cx, |workspace, window, cx| {
8936 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
8937 // Since it was the only panel on the left, the left dock should now be closed.
8938 assert!(!workspace.left_dock().read(cx).is_open());
8939 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
8940 let right_dock = workspace.right_dock();
8941 assert_eq!(
8942 right_dock.read(cx).visible_panel().unwrap().panel_id(),
8943 panel_1.panel_id()
8944 );
8945 assert_eq!(
8946 right_dock.read(cx).active_panel_size(window, cx).unwrap(),
8947 px(1337.)
8948 );
8949
8950 // Now we move panel_2 to the left
8951 panel_2.set_position(DockPosition::Left, window, cx);
8952 });
8953
8954 workspace.update(cx, |workspace, cx| {
8955 // Since panel_2 was not visible on the right, we don't open the left dock.
8956 assert!(!workspace.left_dock().read(cx).is_open());
8957 // And the right dock is unaffected in its displaying of panel_1
8958 assert!(workspace.right_dock().read(cx).is_open());
8959 assert_eq!(
8960 workspace
8961 .right_dock()
8962 .read(cx)
8963 .visible_panel()
8964 .unwrap()
8965 .panel_id(),
8966 panel_1.panel_id(),
8967 );
8968 });
8969
8970 // Move panel_1 back to the left
8971 panel_1.update_in(cx, |panel_1, window, cx| {
8972 panel_1.set_position(DockPosition::Left, window, cx)
8973 });
8974
8975 workspace.update_in(cx, |workspace, window, cx| {
8976 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
8977 let left_dock = workspace.left_dock();
8978 assert!(left_dock.read(cx).is_open());
8979 assert_eq!(
8980 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8981 panel_1.panel_id()
8982 );
8983 assert_eq!(
8984 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8985 px(1337.)
8986 );
8987 // And the right dock should be closed as it no longer has any panels.
8988 assert!(!workspace.right_dock().read(cx).is_open());
8989
8990 // Now we move panel_1 to the bottom
8991 panel_1.set_position(DockPosition::Bottom, window, cx);
8992 });
8993
8994 workspace.update_in(cx, |workspace, window, cx| {
8995 // Since panel_1 was visible on the left, we close the left dock.
8996 assert!(!workspace.left_dock().read(cx).is_open());
8997 // The bottom dock is sized based on the panel's default size,
8998 // since the panel orientation changed from vertical to horizontal.
8999 let bottom_dock = workspace.bottom_dock();
9000 assert_eq!(
9001 bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
9002 panel_1.size(window, cx),
9003 );
9004 // Close bottom dock and move panel_1 back to the left.
9005 bottom_dock.update(cx, |bottom_dock, cx| {
9006 bottom_dock.set_open(false, window, cx)
9007 });
9008 panel_1.set_position(DockPosition::Left, window, cx);
9009 });
9010
9011 // Emit activated event on panel 1
9012 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
9013
9014 // Now the left dock is open and panel_1 is active and focused.
9015 workspace.update_in(cx, |workspace, window, cx| {
9016 let left_dock = workspace.left_dock();
9017 assert!(left_dock.read(cx).is_open());
9018 assert_eq!(
9019 left_dock.read(cx).visible_panel().unwrap().panel_id(),
9020 panel_1.panel_id(),
9021 );
9022 assert!(panel_1.focus_handle(cx).is_focused(window));
9023 });
9024
9025 // Emit closed event on panel 2, which is not active
9026 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
9027
9028 // Wo don't close the left dock, because panel_2 wasn't the active panel
9029 workspace.update(cx, |workspace, cx| {
9030 let left_dock = workspace.left_dock();
9031 assert!(left_dock.read(cx).is_open());
9032 assert_eq!(
9033 left_dock.read(cx).visible_panel().unwrap().panel_id(),
9034 panel_1.panel_id(),
9035 );
9036 });
9037
9038 // Emitting a ZoomIn event shows the panel as zoomed.
9039 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
9040 workspace.read_with(cx, |workspace, _| {
9041 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
9042 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
9043 });
9044
9045 // Move panel to another dock while it is zoomed
9046 panel_1.update_in(cx, |panel, window, cx| {
9047 panel.set_position(DockPosition::Right, window, cx)
9048 });
9049 workspace.read_with(cx, |workspace, _| {
9050 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
9051
9052 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
9053 });
9054
9055 // This is a helper for getting a:
9056 // - valid focus on an element,
9057 // - that isn't a part of the panes and panels system of the Workspace,
9058 // - and doesn't trigger the 'on_focus_lost' API.
9059 let focus_other_view = {
9060 let workspace = workspace.clone();
9061 move |cx: &mut VisualTestContext| {
9062 workspace.update_in(cx, |workspace, window, cx| {
9063 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
9064 workspace.toggle_modal(window, cx, TestModal::new);
9065 workspace.toggle_modal(window, cx, TestModal::new);
9066 } else {
9067 workspace.toggle_modal(window, cx, TestModal::new);
9068 }
9069 })
9070 }
9071 };
9072
9073 // If focus is transferred to another view that's not a panel or another pane, we still show
9074 // the panel as zoomed.
9075 focus_other_view(cx);
9076 workspace.read_with(cx, |workspace, _| {
9077 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
9078 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
9079 });
9080
9081 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
9082 workspace.update_in(cx, |_workspace, window, cx| {
9083 cx.focus_self(window);
9084 });
9085 workspace.read_with(cx, |workspace, _| {
9086 assert_eq!(workspace.zoomed, None);
9087 assert_eq!(workspace.zoomed_position, None);
9088 });
9089
9090 // If focus is transferred again to another view that's not a panel or a pane, we won't
9091 // show the panel as zoomed because it wasn't zoomed before.
9092 focus_other_view(cx);
9093 workspace.read_with(cx, |workspace, _| {
9094 assert_eq!(workspace.zoomed, None);
9095 assert_eq!(workspace.zoomed_position, None);
9096 });
9097
9098 // When the panel is activated, it is zoomed again.
9099 cx.dispatch_action(ToggleRightDock);
9100 workspace.read_with(cx, |workspace, _| {
9101 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
9102 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
9103 });
9104
9105 // Emitting a ZoomOut event unzooms the panel.
9106 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
9107 workspace.read_with(cx, |workspace, _| {
9108 assert_eq!(workspace.zoomed, None);
9109 assert_eq!(workspace.zoomed_position, None);
9110 });
9111
9112 // Emit closed event on panel 1, which is active
9113 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
9114
9115 // Now the left dock is closed, because panel_1 was the active panel
9116 workspace.update(cx, |workspace, cx| {
9117 let right_dock = workspace.right_dock();
9118 assert!(!right_dock.read(cx).is_open());
9119 });
9120 }
9121
9122 #[gpui::test]
9123 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
9124 init_test(cx);
9125
9126 let fs = FakeFs::new(cx.background_executor.clone());
9127 let project = Project::test(fs, [], cx).await;
9128 let (workspace, cx) =
9129 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9130 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9131
9132 let dirty_regular_buffer = cx.new(|cx| {
9133 TestItem::new(cx)
9134 .with_dirty(true)
9135 .with_label("1.txt")
9136 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
9137 });
9138 let dirty_regular_buffer_2 = cx.new(|cx| {
9139 TestItem::new(cx)
9140 .with_dirty(true)
9141 .with_label("2.txt")
9142 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9143 });
9144 let dirty_multi_buffer_with_both = cx.new(|cx| {
9145 TestItem::new(cx)
9146 .with_dirty(true)
9147 .with_singleton(false)
9148 .with_label("Fake Project Search")
9149 .with_project_items(&[
9150 dirty_regular_buffer.read(cx).project_items[0].clone(),
9151 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
9152 ])
9153 });
9154 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
9155 workspace.update_in(cx, |workspace, window, cx| {
9156 workspace.add_item(
9157 pane.clone(),
9158 Box::new(dirty_regular_buffer.clone()),
9159 None,
9160 false,
9161 false,
9162 window,
9163 cx,
9164 );
9165 workspace.add_item(
9166 pane.clone(),
9167 Box::new(dirty_regular_buffer_2.clone()),
9168 None,
9169 false,
9170 false,
9171 window,
9172 cx,
9173 );
9174 workspace.add_item(
9175 pane.clone(),
9176 Box::new(dirty_multi_buffer_with_both.clone()),
9177 None,
9178 false,
9179 false,
9180 window,
9181 cx,
9182 );
9183 });
9184
9185 pane.update_in(cx, |pane, window, cx| {
9186 pane.activate_item(2, true, true, window, cx);
9187 assert_eq!(
9188 pane.active_item().unwrap().item_id(),
9189 multi_buffer_with_both_files_id,
9190 "Should select the multi buffer in the pane"
9191 );
9192 });
9193 let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
9194 pane.close_inactive_items(
9195 &CloseInactiveItems {
9196 save_intent: Some(SaveIntent::Save),
9197 close_pinned: true,
9198 },
9199 window,
9200 cx,
9201 )
9202 });
9203 cx.background_executor.run_until_parked();
9204 assert!(!cx.has_pending_prompt());
9205 close_all_but_multi_buffer_task
9206 .await
9207 .expect("Closing all buffers but the multi buffer failed");
9208 pane.update(cx, |pane, cx| {
9209 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
9210 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
9211 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
9212 assert_eq!(pane.items_len(), 1);
9213 assert_eq!(
9214 pane.active_item().unwrap().item_id(),
9215 multi_buffer_with_both_files_id,
9216 "Should have only the multi buffer left in the pane"
9217 );
9218 assert!(
9219 dirty_multi_buffer_with_both.read(cx).is_dirty,
9220 "The multi buffer containing the unsaved buffer should still be dirty"
9221 );
9222 });
9223
9224 dirty_regular_buffer.update(cx, |buffer, cx| {
9225 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
9226 });
9227
9228 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
9229 pane.close_active_item(
9230 &CloseActiveItem {
9231 save_intent: Some(SaveIntent::Close),
9232 close_pinned: false,
9233 },
9234 window,
9235 cx,
9236 )
9237 });
9238 cx.background_executor.run_until_parked();
9239 assert!(
9240 cx.has_pending_prompt(),
9241 "Dirty multi buffer should prompt a save dialog"
9242 );
9243 cx.simulate_prompt_answer("Save");
9244 cx.background_executor.run_until_parked();
9245 close_multi_buffer_task
9246 .await
9247 .expect("Closing the multi buffer failed");
9248 pane.update(cx, |pane, cx| {
9249 assert_eq!(
9250 dirty_multi_buffer_with_both.read(cx).save_count,
9251 1,
9252 "Multi buffer item should get be saved"
9253 );
9254 // Test impl does not save inner items, so we do not assert them
9255 assert_eq!(
9256 pane.items_len(),
9257 0,
9258 "No more items should be left in the pane"
9259 );
9260 assert!(pane.active_item().is_none());
9261 });
9262 }
9263
9264 #[gpui::test]
9265 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
9266 cx: &mut TestAppContext,
9267 ) {
9268 init_test(cx);
9269
9270 let fs = FakeFs::new(cx.background_executor.clone());
9271 let project = Project::test(fs, [], cx).await;
9272 let (workspace, cx) =
9273 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9274 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9275
9276 let dirty_regular_buffer = cx.new(|cx| {
9277 TestItem::new(cx)
9278 .with_dirty(true)
9279 .with_label("1.txt")
9280 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
9281 });
9282 let dirty_regular_buffer_2 = cx.new(|cx| {
9283 TestItem::new(cx)
9284 .with_dirty(true)
9285 .with_label("2.txt")
9286 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9287 });
9288 let clear_regular_buffer = cx.new(|cx| {
9289 TestItem::new(cx)
9290 .with_label("3.txt")
9291 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
9292 });
9293
9294 let dirty_multi_buffer_with_both = cx.new(|cx| {
9295 TestItem::new(cx)
9296 .with_dirty(true)
9297 .with_singleton(false)
9298 .with_label("Fake Project Search")
9299 .with_project_items(&[
9300 dirty_regular_buffer.read(cx).project_items[0].clone(),
9301 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
9302 clear_regular_buffer.read(cx).project_items[0].clone(),
9303 ])
9304 });
9305 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
9306 workspace.update_in(cx, |workspace, window, cx| {
9307 workspace.add_item(
9308 pane.clone(),
9309 Box::new(dirty_regular_buffer.clone()),
9310 None,
9311 false,
9312 false,
9313 window,
9314 cx,
9315 );
9316 workspace.add_item(
9317 pane.clone(),
9318 Box::new(dirty_multi_buffer_with_both.clone()),
9319 None,
9320 false,
9321 false,
9322 window,
9323 cx,
9324 );
9325 });
9326
9327 pane.update_in(cx, |pane, window, cx| {
9328 pane.activate_item(1, true, true, window, cx);
9329 assert_eq!(
9330 pane.active_item().unwrap().item_id(),
9331 multi_buffer_with_both_files_id,
9332 "Should select the multi buffer in the pane"
9333 );
9334 });
9335 let _close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
9336 pane.close_active_item(
9337 &CloseActiveItem {
9338 save_intent: None,
9339 close_pinned: false,
9340 },
9341 window,
9342 cx,
9343 )
9344 });
9345 cx.background_executor.run_until_parked();
9346 assert!(
9347 cx.has_pending_prompt(),
9348 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
9349 );
9350 }
9351
9352 /// Tests that when `close_on_file_delete` is enabled, files are automatically
9353 /// closed when they are deleted from disk.
9354 #[gpui::test]
9355 async fn test_close_on_disk_deletion_enabled(cx: &mut TestAppContext) {
9356 init_test(cx);
9357
9358 // Enable the close_on_disk_deletion setting
9359 cx.update_global(|store: &mut SettingsStore, cx| {
9360 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
9361 settings.close_on_file_delete = Some(true);
9362 });
9363 });
9364
9365 let fs = FakeFs::new(cx.background_executor.clone());
9366 let project = Project::test(fs, [], cx).await;
9367 let (workspace, cx) =
9368 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9369 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9370
9371 // Create a test item that simulates a file
9372 let item = cx.new(|cx| {
9373 TestItem::new(cx)
9374 .with_label("test.txt")
9375 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
9376 });
9377
9378 // Add item to workspace
9379 workspace.update_in(cx, |workspace, window, cx| {
9380 workspace.add_item(
9381 pane.clone(),
9382 Box::new(item.clone()),
9383 None,
9384 false,
9385 false,
9386 window,
9387 cx,
9388 );
9389 });
9390
9391 // Verify the item is in the pane
9392 pane.read_with(cx, |pane, _| {
9393 assert_eq!(pane.items().count(), 1);
9394 });
9395
9396 // Simulate file deletion by setting the item's deleted state
9397 item.update(cx, |item, _| {
9398 item.set_has_deleted_file(true);
9399 });
9400
9401 // Emit UpdateTab event to trigger the close behavior
9402 cx.run_until_parked();
9403 item.update(cx, |_, cx| {
9404 cx.emit(ItemEvent::UpdateTab);
9405 });
9406
9407 // Allow the close operation to complete
9408 cx.run_until_parked();
9409
9410 // Verify the item was automatically closed
9411 pane.read_with(cx, |pane, _| {
9412 assert_eq!(
9413 pane.items().count(),
9414 0,
9415 "Item should be automatically closed when file is deleted"
9416 );
9417 });
9418 }
9419
9420 /// Tests that when `close_on_file_delete` is disabled (default), files remain
9421 /// open with a strikethrough when they are deleted from disk.
9422 #[gpui::test]
9423 async fn test_close_on_disk_deletion_disabled(cx: &mut TestAppContext) {
9424 init_test(cx);
9425
9426 // Ensure close_on_disk_deletion is disabled (default)
9427 cx.update_global(|store: &mut SettingsStore, cx| {
9428 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
9429 settings.close_on_file_delete = Some(false);
9430 });
9431 });
9432
9433 let fs = FakeFs::new(cx.background_executor.clone());
9434 let project = Project::test(fs, [], cx).await;
9435 let (workspace, cx) =
9436 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9437 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9438
9439 // Create a test item that simulates a file
9440 let item = cx.new(|cx| {
9441 TestItem::new(cx)
9442 .with_label("test.txt")
9443 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
9444 });
9445
9446 // Add item to workspace
9447 workspace.update_in(cx, |workspace, window, cx| {
9448 workspace.add_item(
9449 pane.clone(),
9450 Box::new(item.clone()),
9451 None,
9452 false,
9453 false,
9454 window,
9455 cx,
9456 );
9457 });
9458
9459 // Verify the item is in the pane
9460 pane.read_with(cx, |pane, _| {
9461 assert_eq!(pane.items().count(), 1);
9462 });
9463
9464 // Simulate file deletion
9465 item.update(cx, |item, _| {
9466 item.set_has_deleted_file(true);
9467 });
9468
9469 // Emit UpdateTab event
9470 cx.run_until_parked();
9471 item.update(cx, |_, cx| {
9472 cx.emit(ItemEvent::UpdateTab);
9473 });
9474
9475 // Allow any potential close operation to complete
9476 cx.run_until_parked();
9477
9478 // Verify the item remains open (with strikethrough)
9479 pane.read_with(cx, |pane, _| {
9480 assert_eq!(
9481 pane.items().count(),
9482 1,
9483 "Item should remain open when close_on_disk_deletion is disabled"
9484 );
9485 });
9486
9487 // Verify the item shows as deleted
9488 item.read_with(cx, |item, _| {
9489 assert!(
9490 item.has_deleted_file,
9491 "Item should be marked as having deleted file"
9492 );
9493 });
9494 }
9495
9496 /// Tests that dirty files are not automatically closed when deleted from disk,
9497 /// even when `close_on_file_delete` is enabled. This ensures users don't lose
9498 /// unsaved changes without being prompted.
9499 #[gpui::test]
9500 async fn test_close_on_disk_deletion_with_dirty_file(cx: &mut TestAppContext) {
9501 init_test(cx);
9502
9503 // Enable the close_on_file_delete setting
9504 cx.update_global(|store: &mut SettingsStore, cx| {
9505 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
9506 settings.close_on_file_delete = Some(true);
9507 });
9508 });
9509
9510 let fs = FakeFs::new(cx.background_executor.clone());
9511 let project = Project::test(fs, [], cx).await;
9512 let (workspace, cx) =
9513 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9514 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9515
9516 // Create a dirty test item
9517 let item = cx.new(|cx| {
9518 TestItem::new(cx)
9519 .with_dirty(true)
9520 .with_label("test.txt")
9521 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
9522 });
9523
9524 // Add item to workspace
9525 workspace.update_in(cx, |workspace, window, cx| {
9526 workspace.add_item(
9527 pane.clone(),
9528 Box::new(item.clone()),
9529 None,
9530 false,
9531 false,
9532 window,
9533 cx,
9534 );
9535 });
9536
9537 // Simulate file deletion
9538 item.update(cx, |item, _| {
9539 item.set_has_deleted_file(true);
9540 });
9541
9542 // Emit UpdateTab event to trigger the close behavior
9543 cx.run_until_parked();
9544 item.update(cx, |_, cx| {
9545 cx.emit(ItemEvent::UpdateTab);
9546 });
9547
9548 // Allow any potential close operation to complete
9549 cx.run_until_parked();
9550
9551 // Verify the item remains open (dirty files are not auto-closed)
9552 pane.read_with(cx, |pane, _| {
9553 assert_eq!(
9554 pane.items().count(),
9555 1,
9556 "Dirty items should not be automatically closed even when file is deleted"
9557 );
9558 });
9559
9560 // Verify the item is marked as deleted and still dirty
9561 item.read_with(cx, |item, _| {
9562 assert!(
9563 item.has_deleted_file,
9564 "Item should be marked as having deleted file"
9565 );
9566 assert!(item.is_dirty, "Item should still be dirty");
9567 });
9568 }
9569
9570 /// Tests that navigation history is cleaned up when files are auto-closed
9571 /// due to deletion from disk.
9572 #[gpui::test]
9573 async fn test_close_on_disk_deletion_cleans_navigation_history(cx: &mut TestAppContext) {
9574 init_test(cx);
9575
9576 // Enable the close_on_file_delete setting
9577 cx.update_global(|store: &mut SettingsStore, cx| {
9578 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
9579 settings.close_on_file_delete = Some(true);
9580 });
9581 });
9582
9583 let fs = FakeFs::new(cx.background_executor.clone());
9584 let project = Project::test(fs, [], cx).await;
9585 let (workspace, cx) =
9586 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9587 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9588
9589 // Create test items
9590 let item1 = cx.new(|cx| {
9591 TestItem::new(cx)
9592 .with_label("test1.txt")
9593 .with_project_items(&[TestProjectItem::new(1, "test1.txt", cx)])
9594 });
9595 let item1_id = item1.item_id();
9596
9597 let item2 = cx.new(|cx| {
9598 TestItem::new(cx)
9599 .with_label("test2.txt")
9600 .with_project_items(&[TestProjectItem::new(2, "test2.txt", cx)])
9601 });
9602
9603 // Add items to workspace
9604 workspace.update_in(cx, |workspace, window, cx| {
9605 workspace.add_item(
9606 pane.clone(),
9607 Box::new(item1.clone()),
9608 None,
9609 false,
9610 false,
9611 window,
9612 cx,
9613 );
9614 workspace.add_item(
9615 pane.clone(),
9616 Box::new(item2.clone()),
9617 None,
9618 false,
9619 false,
9620 window,
9621 cx,
9622 );
9623 });
9624
9625 // Activate item1 to ensure it gets navigation entries
9626 pane.update_in(cx, |pane, window, cx| {
9627 pane.activate_item(0, true, true, window, cx);
9628 });
9629
9630 // Switch to item2 and back to create navigation history
9631 pane.update_in(cx, |pane, window, cx| {
9632 pane.activate_item(1, true, true, window, cx);
9633 });
9634 cx.run_until_parked();
9635
9636 pane.update_in(cx, |pane, window, cx| {
9637 pane.activate_item(0, true, true, window, cx);
9638 });
9639 cx.run_until_parked();
9640
9641 // Simulate file deletion for item1
9642 item1.update(cx, |item, _| {
9643 item.set_has_deleted_file(true);
9644 });
9645
9646 // Emit UpdateTab event to trigger the close behavior
9647 item1.update(cx, |_, cx| {
9648 cx.emit(ItemEvent::UpdateTab);
9649 });
9650 cx.run_until_parked();
9651
9652 // Verify item1 was closed
9653 pane.read_with(cx, |pane, _| {
9654 assert_eq!(
9655 pane.items().count(),
9656 1,
9657 "Should have 1 item remaining after auto-close"
9658 );
9659 });
9660
9661 // Check navigation history after close
9662 let has_item = pane.read_with(cx, |pane, cx| {
9663 let mut has_item = false;
9664 pane.nav_history().for_each_entry(cx, |entry, _| {
9665 if entry.item.id() == item1_id {
9666 has_item = true;
9667 }
9668 });
9669 has_item
9670 });
9671
9672 assert!(
9673 !has_item,
9674 "Navigation history should not contain closed item entries"
9675 );
9676 }
9677
9678 #[gpui::test]
9679 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
9680 cx: &mut TestAppContext,
9681 ) {
9682 init_test(cx);
9683
9684 let fs = FakeFs::new(cx.background_executor.clone());
9685 let project = Project::test(fs, [], cx).await;
9686 let (workspace, cx) =
9687 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9688 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9689
9690 let dirty_regular_buffer = cx.new(|cx| {
9691 TestItem::new(cx)
9692 .with_dirty(true)
9693 .with_label("1.txt")
9694 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
9695 });
9696 let dirty_regular_buffer_2 = cx.new(|cx| {
9697 TestItem::new(cx)
9698 .with_dirty(true)
9699 .with_label("2.txt")
9700 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9701 });
9702 let clear_regular_buffer = cx.new(|cx| {
9703 TestItem::new(cx)
9704 .with_label("3.txt")
9705 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
9706 });
9707
9708 let dirty_multi_buffer = cx.new(|cx| {
9709 TestItem::new(cx)
9710 .with_dirty(true)
9711 .with_singleton(false)
9712 .with_label("Fake Project Search")
9713 .with_project_items(&[
9714 dirty_regular_buffer.read(cx).project_items[0].clone(),
9715 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
9716 clear_regular_buffer.read(cx).project_items[0].clone(),
9717 ])
9718 });
9719 workspace.update_in(cx, |workspace, window, cx| {
9720 workspace.add_item(
9721 pane.clone(),
9722 Box::new(dirty_regular_buffer.clone()),
9723 None,
9724 false,
9725 false,
9726 window,
9727 cx,
9728 );
9729 workspace.add_item(
9730 pane.clone(),
9731 Box::new(dirty_regular_buffer_2.clone()),
9732 None,
9733 false,
9734 false,
9735 window,
9736 cx,
9737 );
9738 workspace.add_item(
9739 pane.clone(),
9740 Box::new(dirty_multi_buffer.clone()),
9741 None,
9742 false,
9743 false,
9744 window,
9745 cx,
9746 );
9747 });
9748
9749 pane.update_in(cx, |pane, window, cx| {
9750 pane.activate_item(2, true, true, window, cx);
9751 assert_eq!(
9752 pane.active_item().unwrap().item_id(),
9753 dirty_multi_buffer.item_id(),
9754 "Should select the multi buffer in the pane"
9755 );
9756 });
9757 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
9758 pane.close_active_item(
9759 &CloseActiveItem {
9760 save_intent: None,
9761 close_pinned: false,
9762 },
9763 window,
9764 cx,
9765 )
9766 });
9767 cx.background_executor.run_until_parked();
9768 assert!(
9769 !cx.has_pending_prompt(),
9770 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
9771 );
9772 close_multi_buffer_task
9773 .await
9774 .expect("Closing multi buffer failed");
9775 pane.update(cx, |pane, cx| {
9776 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
9777 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
9778 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
9779 assert_eq!(
9780 pane.items()
9781 .map(|item| item.item_id())
9782 .sorted()
9783 .collect::<Vec<_>>(),
9784 vec![
9785 dirty_regular_buffer.item_id(),
9786 dirty_regular_buffer_2.item_id(),
9787 ],
9788 "Should have no multi buffer left in the pane"
9789 );
9790 assert!(dirty_regular_buffer.read(cx).is_dirty);
9791 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
9792 });
9793 }
9794
9795 #[gpui::test]
9796 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
9797 init_test(cx);
9798 let fs = FakeFs::new(cx.executor());
9799 let project = Project::test(fs, [], cx).await;
9800 let (workspace, cx) =
9801 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9802
9803 // Add a new panel to the right dock, opening the dock and setting the
9804 // focus to the new panel.
9805 let panel = workspace.update_in(cx, |workspace, window, cx| {
9806 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
9807 workspace.add_panel(panel.clone(), window, cx);
9808
9809 workspace
9810 .right_dock()
9811 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
9812
9813 workspace.toggle_panel_focus::<TestPanel>(window, cx);
9814
9815 panel
9816 });
9817
9818 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
9819 // panel to the next valid position which, in this case, is the left
9820 // dock.
9821 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9822 workspace.update(cx, |workspace, cx| {
9823 assert!(workspace.left_dock().read(cx).is_open());
9824 assert_eq!(panel.read(cx).position, DockPosition::Left);
9825 });
9826
9827 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
9828 // panel to the next valid position which, in this case, is the bottom
9829 // dock.
9830 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9831 workspace.update(cx, |workspace, cx| {
9832 assert!(workspace.bottom_dock().read(cx).is_open());
9833 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
9834 });
9835
9836 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
9837 // around moving the panel to its initial position, the right dock.
9838 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9839 workspace.update(cx, |workspace, cx| {
9840 assert!(workspace.right_dock().read(cx).is_open());
9841 assert_eq!(panel.read(cx).position, DockPosition::Right);
9842 });
9843
9844 // Remove focus from the panel, ensuring that, if the panel is not
9845 // focused, the `MoveFocusedPanelToNextPosition` action does not update
9846 // the panel's position, so the panel is still in the right dock.
9847 workspace.update_in(cx, |workspace, window, cx| {
9848 workspace.toggle_panel_focus::<TestPanel>(window, cx);
9849 });
9850
9851 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9852 workspace.update(cx, |workspace, cx| {
9853 assert!(workspace.right_dock().read(cx).is_open());
9854 assert_eq!(panel.read(cx).position, DockPosition::Right);
9855 });
9856 }
9857
9858 #[gpui::test]
9859 async fn test_moving_items_create_panes(cx: &mut TestAppContext) {
9860 init_test(cx);
9861
9862 let fs = FakeFs::new(cx.executor());
9863 let project = Project::test(fs, [], cx).await;
9864 let (workspace, cx) =
9865 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9866
9867 let item_1 = cx.new(|cx| {
9868 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
9869 });
9870 workspace.update_in(cx, |workspace, window, cx| {
9871 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
9872 workspace.move_item_to_pane_in_direction(
9873 &MoveItemToPaneInDirection {
9874 direction: SplitDirection::Right,
9875 focus: true,
9876 clone: false,
9877 },
9878 window,
9879 cx,
9880 );
9881 workspace.move_item_to_pane_at_index(
9882 &MoveItemToPane {
9883 destination: 3,
9884 focus: true,
9885 clone: false,
9886 },
9887 window,
9888 cx,
9889 );
9890
9891 assert_eq!(workspace.panes.len(), 1, "No new panes were created");
9892 assert_eq!(
9893 pane_items_paths(&workspace.active_pane, cx),
9894 vec!["first.txt".to_string()],
9895 "Single item was not moved anywhere"
9896 );
9897 });
9898
9899 let item_2 = cx.new(|cx| {
9900 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "second.txt", cx)])
9901 });
9902 workspace.update_in(cx, |workspace, window, cx| {
9903 workspace.add_item_to_active_pane(Box::new(item_2), None, true, window, cx);
9904 assert_eq!(
9905 pane_items_paths(&workspace.panes[0], cx),
9906 vec!["first.txt".to_string(), "second.txt".to_string()],
9907 );
9908 workspace.move_item_to_pane_in_direction(
9909 &MoveItemToPaneInDirection {
9910 direction: SplitDirection::Right,
9911 focus: true,
9912 clone: false,
9913 },
9914 window,
9915 cx,
9916 );
9917
9918 assert_eq!(workspace.panes.len(), 2, "A new pane should be created");
9919 assert_eq!(
9920 pane_items_paths(&workspace.panes[0], cx),
9921 vec!["first.txt".to_string()],
9922 "After moving, one item should be left in the original pane"
9923 );
9924 assert_eq!(
9925 pane_items_paths(&workspace.panes[1], cx),
9926 vec!["second.txt".to_string()],
9927 "New item should have been moved to the new pane"
9928 );
9929 });
9930
9931 let item_3 = cx.new(|cx| {
9932 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "third.txt", cx)])
9933 });
9934 workspace.update_in(cx, |workspace, window, cx| {
9935 let original_pane = workspace.panes[0].clone();
9936 workspace.set_active_pane(&original_pane, window, cx);
9937 workspace.add_item_to_active_pane(Box::new(item_3), None, true, window, cx);
9938 assert_eq!(workspace.panes.len(), 2, "No new panes were created");
9939 assert_eq!(
9940 pane_items_paths(&workspace.active_pane, cx),
9941 vec!["first.txt".to_string(), "third.txt".to_string()],
9942 "New pane should be ready to move one item out"
9943 );
9944
9945 workspace.move_item_to_pane_at_index(
9946 &MoveItemToPane {
9947 destination: 3,
9948 focus: true,
9949 clone: false,
9950 },
9951 window,
9952 cx,
9953 );
9954 assert_eq!(workspace.panes.len(), 3, "A new pane should be created");
9955 assert_eq!(
9956 pane_items_paths(&workspace.active_pane, cx),
9957 vec!["first.txt".to_string()],
9958 "After moving, one item should be left in the original pane"
9959 );
9960 assert_eq!(
9961 pane_items_paths(&workspace.panes[1], cx),
9962 vec!["second.txt".to_string()],
9963 "Previously created pane should be unchanged"
9964 );
9965 assert_eq!(
9966 pane_items_paths(&workspace.panes[2], cx),
9967 vec!["third.txt".to_string()],
9968 "New item should have been moved to the new pane"
9969 );
9970 });
9971 }
9972
9973 #[gpui::test]
9974 async fn test_moving_items_can_clone_panes(cx: &mut TestAppContext) {
9975 init_test(cx);
9976
9977 let fs = FakeFs::new(cx.executor());
9978 let project = Project::test(fs, [], cx).await;
9979 let (workspace, cx) =
9980 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9981
9982 let item_1 = cx.new(|cx| {
9983 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
9984 });
9985 workspace.update_in(cx, |workspace, window, cx| {
9986 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
9987 workspace.move_item_to_pane_in_direction(
9988 &MoveItemToPaneInDirection {
9989 direction: SplitDirection::Right,
9990 focus: true,
9991 clone: true,
9992 },
9993 window,
9994 cx,
9995 );
9996 workspace.move_item_to_pane_at_index(
9997 &MoveItemToPane {
9998 destination: 3,
9999 focus: true,
10000 clone: true,
10001 },
10002 window,
10003 cx,
10004 );
10005
10006 assert_eq!(workspace.panes.len(), 3, "Two new panes were created");
10007 for pane in workspace.panes() {
10008 assert_eq!(
10009 pane_items_paths(pane, cx),
10010 vec!["first.txt".to_string()],
10011 "Single item exists in all panes"
10012 );
10013 }
10014 });
10015
10016 // verify that the active pane has been updated after waiting for the
10017 // pane focus event to fire and resolve
10018 workspace.read_with(cx, |workspace, _app| {
10019 assert_eq!(
10020 workspace.active_pane(),
10021 &workspace.panes[2],
10022 "The third pane should be the active one: {:?}",
10023 workspace.panes
10024 );
10025 })
10026 }
10027
10028 mod register_project_item_tests {
10029
10030 use super::*;
10031
10032 // View
10033 struct TestPngItemView {
10034 focus_handle: FocusHandle,
10035 }
10036 // Model
10037 struct TestPngItem {}
10038
10039 impl project::ProjectItem for TestPngItem {
10040 fn try_open(
10041 _project: &Entity<Project>,
10042 path: &ProjectPath,
10043 cx: &mut App,
10044 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
10045 if path.path.extension().unwrap() == "png" {
10046 Some(cx.spawn(async move |cx| cx.new(|_| TestPngItem {})))
10047 } else {
10048 None
10049 }
10050 }
10051
10052 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
10053 None
10054 }
10055
10056 fn project_path(&self, _: &App) -> Option<ProjectPath> {
10057 None
10058 }
10059
10060 fn is_dirty(&self) -> bool {
10061 false
10062 }
10063 }
10064
10065 impl Item for TestPngItemView {
10066 type Event = ();
10067 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
10068 "".into()
10069 }
10070 }
10071 impl EventEmitter<()> for TestPngItemView {}
10072 impl Focusable for TestPngItemView {
10073 fn focus_handle(&self, _cx: &App) -> FocusHandle {
10074 self.focus_handle.clone()
10075 }
10076 }
10077
10078 impl Render for TestPngItemView {
10079 fn render(
10080 &mut self,
10081 _window: &mut Window,
10082 _cx: &mut Context<Self>,
10083 ) -> impl IntoElement {
10084 Empty
10085 }
10086 }
10087
10088 impl ProjectItem for TestPngItemView {
10089 type Item = TestPngItem;
10090
10091 fn for_project_item(
10092 _project: Entity<Project>,
10093 _pane: Option<&Pane>,
10094 _item: Entity<Self::Item>,
10095 _: &mut Window,
10096 cx: &mut Context<Self>,
10097 ) -> Self
10098 where
10099 Self: Sized,
10100 {
10101 Self {
10102 focus_handle: cx.focus_handle(),
10103 }
10104 }
10105 }
10106
10107 // View
10108 struct TestIpynbItemView {
10109 focus_handle: FocusHandle,
10110 }
10111 // Model
10112 struct TestIpynbItem {}
10113
10114 impl project::ProjectItem for TestIpynbItem {
10115 fn try_open(
10116 _project: &Entity<Project>,
10117 path: &ProjectPath,
10118 cx: &mut App,
10119 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
10120 if path.path.extension().unwrap() == "ipynb" {
10121 Some(cx.spawn(async move |cx| cx.new(|_| TestIpynbItem {})))
10122 } else {
10123 None
10124 }
10125 }
10126
10127 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
10128 None
10129 }
10130
10131 fn project_path(&self, _: &App) -> Option<ProjectPath> {
10132 None
10133 }
10134
10135 fn is_dirty(&self) -> bool {
10136 false
10137 }
10138 }
10139
10140 impl Item for TestIpynbItemView {
10141 type Event = ();
10142 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
10143 "".into()
10144 }
10145 }
10146 impl EventEmitter<()> for TestIpynbItemView {}
10147 impl Focusable for TestIpynbItemView {
10148 fn focus_handle(&self, _cx: &App) -> FocusHandle {
10149 self.focus_handle.clone()
10150 }
10151 }
10152
10153 impl Render for TestIpynbItemView {
10154 fn render(
10155 &mut self,
10156 _window: &mut Window,
10157 _cx: &mut Context<Self>,
10158 ) -> impl IntoElement {
10159 Empty
10160 }
10161 }
10162
10163 impl ProjectItem for TestIpynbItemView {
10164 type Item = TestIpynbItem;
10165
10166 fn for_project_item(
10167 _project: Entity<Project>,
10168 _pane: Option<&Pane>,
10169 _item: Entity<Self::Item>,
10170 _: &mut Window,
10171 cx: &mut Context<Self>,
10172 ) -> Self
10173 where
10174 Self: Sized,
10175 {
10176 Self {
10177 focus_handle: cx.focus_handle(),
10178 }
10179 }
10180 }
10181
10182 struct TestAlternatePngItemView {
10183 focus_handle: FocusHandle,
10184 }
10185
10186 impl Item for TestAlternatePngItemView {
10187 type Event = ();
10188 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
10189 "".into()
10190 }
10191 }
10192
10193 impl EventEmitter<()> for TestAlternatePngItemView {}
10194 impl Focusable for TestAlternatePngItemView {
10195 fn focus_handle(&self, _cx: &App) -> FocusHandle {
10196 self.focus_handle.clone()
10197 }
10198 }
10199
10200 impl Render for TestAlternatePngItemView {
10201 fn render(
10202 &mut self,
10203 _window: &mut Window,
10204 _cx: &mut Context<Self>,
10205 ) -> impl IntoElement {
10206 Empty
10207 }
10208 }
10209
10210 impl ProjectItem for TestAlternatePngItemView {
10211 type Item = TestPngItem;
10212
10213 fn for_project_item(
10214 _project: Entity<Project>,
10215 _pane: Option<&Pane>,
10216 _item: Entity<Self::Item>,
10217 _: &mut Window,
10218 cx: &mut Context<Self>,
10219 ) -> Self
10220 where
10221 Self: Sized,
10222 {
10223 Self {
10224 focus_handle: cx.focus_handle(),
10225 }
10226 }
10227 }
10228
10229 #[gpui::test]
10230 async fn test_register_project_item(cx: &mut TestAppContext) {
10231 init_test(cx);
10232
10233 cx.update(|cx| {
10234 register_project_item::<TestPngItemView>(cx);
10235 register_project_item::<TestIpynbItemView>(cx);
10236 });
10237
10238 let fs = FakeFs::new(cx.executor());
10239 fs.insert_tree(
10240 "/root1",
10241 json!({
10242 "one.png": "BINARYDATAHERE",
10243 "two.ipynb": "{ totally a notebook }",
10244 "three.txt": "editing text, sure why not?"
10245 }),
10246 )
10247 .await;
10248
10249 let project = Project::test(fs, ["root1".as_ref()], cx).await;
10250 let (workspace, cx) =
10251 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10252
10253 let worktree_id = project.update(cx, |project, cx| {
10254 project.worktrees(cx).next().unwrap().read(cx).id()
10255 });
10256
10257 let handle = workspace
10258 .update_in(cx, |workspace, window, cx| {
10259 let project_path = (worktree_id, "one.png");
10260 workspace.open_path(project_path, None, true, window, cx)
10261 })
10262 .await
10263 .unwrap();
10264
10265 // Now we can check if the handle we got back errored or not
10266 assert_eq!(
10267 handle.to_any().entity_type(),
10268 TypeId::of::<TestPngItemView>()
10269 );
10270
10271 let handle = workspace
10272 .update_in(cx, |workspace, window, cx| {
10273 let project_path = (worktree_id, "two.ipynb");
10274 workspace.open_path(project_path, None, true, window, cx)
10275 })
10276 .await
10277 .unwrap();
10278
10279 assert_eq!(
10280 handle.to_any().entity_type(),
10281 TypeId::of::<TestIpynbItemView>()
10282 );
10283
10284 let handle = workspace
10285 .update_in(cx, |workspace, window, cx| {
10286 let project_path = (worktree_id, "three.txt");
10287 workspace.open_path(project_path, None, true, window, cx)
10288 })
10289 .await;
10290 assert!(handle.is_err());
10291 }
10292
10293 #[gpui::test]
10294 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
10295 init_test(cx);
10296
10297 cx.update(|cx| {
10298 register_project_item::<TestPngItemView>(cx);
10299 register_project_item::<TestAlternatePngItemView>(cx);
10300 });
10301
10302 let fs = FakeFs::new(cx.executor());
10303 fs.insert_tree(
10304 "/root1",
10305 json!({
10306 "one.png": "BINARYDATAHERE",
10307 "two.ipynb": "{ totally a notebook }",
10308 "three.txt": "editing text, sure why not?"
10309 }),
10310 )
10311 .await;
10312 let project = Project::test(fs, ["root1".as_ref()], cx).await;
10313 let (workspace, cx) =
10314 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10315 let worktree_id = project.update(cx, |project, cx| {
10316 project.worktrees(cx).next().unwrap().read(cx).id()
10317 });
10318
10319 let handle = workspace
10320 .update_in(cx, |workspace, window, cx| {
10321 let project_path = (worktree_id, "one.png");
10322 workspace.open_path(project_path, None, true, window, cx)
10323 })
10324 .await
10325 .unwrap();
10326
10327 // This _must_ be the second item registered
10328 assert_eq!(
10329 handle.to_any().entity_type(),
10330 TypeId::of::<TestAlternatePngItemView>()
10331 );
10332
10333 let handle = workspace
10334 .update_in(cx, |workspace, window, cx| {
10335 let project_path = (worktree_id, "three.txt");
10336 workspace.open_path(project_path, None, true, window, cx)
10337 })
10338 .await;
10339 assert!(handle.is_err());
10340 }
10341 }
10342
10343 fn pane_items_paths(pane: &Entity<Pane>, cx: &App) -> Vec<String> {
10344 pane.read(cx)
10345 .items()
10346 .flat_map(|item| {
10347 item.project_paths(cx)
10348 .into_iter()
10349 .map(|path| path.path.to_string_lossy().to_string())
10350 })
10351 .collect()
10352 }
10353
10354 pub fn init_test(cx: &mut TestAppContext) {
10355 cx.update(|cx| {
10356 let settings_store = SettingsStore::test(cx);
10357 cx.set_global(settings_store);
10358 theme::init(theme::LoadThemes::JustBase, cx);
10359 language::init(cx);
10360 crate::init_settings(cx);
10361 Project::init_settings(cx);
10362 });
10363 }
10364
10365 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
10366 let item = TestProjectItem::new(id, path, cx);
10367 item.update(cx, |item, _| {
10368 item.is_dirty = true;
10369 });
10370 item
10371 }
10372}