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