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