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<anyhow::Result<Box<dyn ItemHandle>>>>,
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 anyhow::bail!("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<anyhow::Result<Box<dyn ItemHandle>>>>> {
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<anyhow::Result<Box<dyn ItemHandle>>> {
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<anyhow::Result<Box<dyn ItemHandle>>> {
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<anyhow::Result<Box<dyn ItemHandle>>> {
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<anyhow::Result<Box<dyn ItemHandle>>> {
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.context("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 .context("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 anyhow::Ok(())
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.context("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.variant.context("missing update view variant")?;
4332 let id = update_view.id.context("missing update view id")?;
4333 let mut tasks = Vec::new();
4334 this.update_in(cx, |this, window, cx| {
4335 let project = this.project.clone();
4336 if let Some(state) = this.follower_states.get(&leader_id.into()) {
4337 let view_id = ViewId::from_proto(id.clone())?;
4338 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
4339 tasks.push(item.view.apply_update_proto(
4340 &project,
4341 variant.clone(),
4342 window,
4343 cx,
4344 ));
4345 }
4346 }
4347 anyhow::Ok(())
4348 })??;
4349 try_join_all(tasks).await.log_err();
4350 }
4351 }
4352 this.update_in(cx, |this, window, cx| {
4353 this.leader_updated(leader_id, window, cx)
4354 })?;
4355 Ok(())
4356 }
4357
4358 async fn add_view_from_leader(
4359 this: WeakEntity<Self>,
4360 leader_id: PeerId,
4361 view: &proto::View,
4362 cx: &mut AsyncWindowContext,
4363 ) -> Result<()> {
4364 let this = this.upgrade().context("workspace dropped")?;
4365
4366 let Some(id) = view.id.clone() else {
4367 anyhow::bail!("no id for view");
4368 };
4369 let id = ViewId::from_proto(id)?;
4370 let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
4371
4372 let pane = this.update(cx, |this, _cx| {
4373 let state = this
4374 .follower_states
4375 .get(&leader_id.into())
4376 .context("stopped following")?;
4377 anyhow::Ok(state.pane().clone())
4378 })??;
4379 let existing_item = pane.update_in(cx, |pane, window, cx| {
4380 let client = this.read(cx).client().clone();
4381 pane.items().find_map(|item| {
4382 let item = item.to_followable_item_handle(cx)?;
4383 if item.remote_id(&client, window, cx) == Some(id) {
4384 Some(item)
4385 } else {
4386 None
4387 }
4388 })
4389 })?;
4390 let item = if let Some(existing_item) = existing_item {
4391 existing_item
4392 } else {
4393 let variant = view.variant.clone();
4394 anyhow::ensure!(variant.is_some(), "missing view variant");
4395
4396 let task = cx.update(|window, cx| {
4397 FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx)
4398 })?;
4399
4400 let Some(task) = task else {
4401 anyhow::bail!(
4402 "failed to construct view from leader (maybe from a different version of zed?)"
4403 );
4404 };
4405
4406 let mut new_item = task.await?;
4407 pane.update_in(cx, |pane, window, cx| {
4408 let mut item_to_remove = None;
4409 for (ix, item) in pane.items().enumerate() {
4410 if let Some(item) = item.to_followable_item_handle(cx) {
4411 match new_item.dedup(item.as_ref(), window, cx) {
4412 Some(item::Dedup::KeepExisting) => {
4413 new_item =
4414 item.boxed_clone().to_followable_item_handle(cx).unwrap();
4415 break;
4416 }
4417 Some(item::Dedup::ReplaceExisting) => {
4418 item_to_remove = Some((ix, item.item_id()));
4419 break;
4420 }
4421 None => {}
4422 }
4423 }
4424 }
4425
4426 if let Some((ix, id)) = item_to_remove {
4427 pane.remove_item(id, false, false, window, cx);
4428 pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx);
4429 }
4430 })?;
4431
4432 new_item
4433 };
4434
4435 this.update_in(cx, |this, window, cx| {
4436 let state = this.follower_states.get_mut(&leader_id.into())?;
4437 item.set_leader_id(Some(leader_id.into()), window, cx);
4438 state.items_by_leader_view_id.insert(
4439 id,
4440 FollowerView {
4441 view: item,
4442 location: panel_id,
4443 },
4444 );
4445
4446 Some(())
4447 })?;
4448
4449 Ok(())
4450 }
4451
4452 fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4453 let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else {
4454 return;
4455 };
4456
4457 if let Some(agent_location) = self.project.read(cx).agent_location() {
4458 let buffer_entity_id = agent_location.buffer.entity_id();
4459 let view_id = ViewId {
4460 creator: CollaboratorId::Agent,
4461 id: buffer_entity_id.as_u64(),
4462 };
4463 follower_state.active_view_id = Some(view_id);
4464
4465 let item = match follower_state.items_by_leader_view_id.entry(view_id) {
4466 hash_map::Entry::Occupied(entry) => Some(entry.into_mut()),
4467 hash_map::Entry::Vacant(entry) => {
4468 let existing_view =
4469 follower_state
4470 .center_pane
4471 .read(cx)
4472 .items()
4473 .find_map(|item| {
4474 let item = item.to_followable_item_handle(cx)?;
4475 if item.is_singleton(cx)
4476 && item.project_item_model_ids(cx).as_slice()
4477 == [buffer_entity_id]
4478 {
4479 Some(item)
4480 } else {
4481 None
4482 }
4483 });
4484 let view = existing_view.or_else(|| {
4485 agent_location.buffer.upgrade().and_then(|buffer| {
4486 cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| {
4487 registry.build_item(buffer, self.project.clone(), None, window, cx)
4488 })?
4489 .to_followable_item_handle(cx)
4490 })
4491 });
4492
4493 if let Some(view) = view {
4494 Some(entry.insert(FollowerView {
4495 view,
4496 location: None,
4497 }))
4498 } else {
4499 None
4500 }
4501 }
4502 };
4503
4504 if let Some(item) = item {
4505 item.view
4506 .set_leader_id(Some(CollaboratorId::Agent), window, cx);
4507 item.view
4508 .update_agent_location(agent_location.position, window, cx);
4509 }
4510 } else {
4511 follower_state.active_view_id = None;
4512 }
4513
4514 self.leader_updated(CollaboratorId::Agent, window, cx);
4515 }
4516
4517 pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
4518 let mut is_project_item = true;
4519 let mut update = proto::UpdateActiveView::default();
4520 if window.is_window_active() {
4521 let (active_item, panel_id) = self.active_item_for_followers(window, cx);
4522
4523 if let Some(item) = active_item {
4524 if item.item_focus_handle(cx).contains_focused(window, cx) {
4525 let leader_id = self
4526 .pane_for(&*item)
4527 .and_then(|pane| self.leader_for_pane(&pane));
4528 let leader_peer_id = match leader_id {
4529 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
4530 Some(CollaboratorId::Agent) | None => None,
4531 };
4532
4533 if let Some(item) = item.to_followable_item_handle(cx) {
4534 let id = item
4535 .remote_id(&self.app_state.client, window, cx)
4536 .map(|id| id.to_proto());
4537
4538 if let Some(id) = id.clone() {
4539 if let Some(variant) = item.to_state_proto(window, cx) {
4540 let view = Some(proto::View {
4541 id: id.clone(),
4542 leader_id: leader_peer_id,
4543 variant: Some(variant),
4544 panel_id: panel_id.map(|id| id as i32),
4545 });
4546
4547 is_project_item = item.is_project_item(window, cx);
4548 update = proto::UpdateActiveView {
4549 view,
4550 // TODO: Remove after version 0.145.x stabilizes.
4551 id: id.clone(),
4552 leader_id: leader_peer_id,
4553 };
4554 }
4555 };
4556 }
4557 }
4558 }
4559 }
4560
4561 let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
4562 if active_view_id != self.last_active_view_id.as_ref() {
4563 self.last_active_view_id = active_view_id.cloned();
4564 self.update_followers(
4565 is_project_item,
4566 proto::update_followers::Variant::UpdateActiveView(update),
4567 window,
4568 cx,
4569 );
4570 }
4571 }
4572
4573 fn active_item_for_followers(
4574 &self,
4575 window: &mut Window,
4576 cx: &mut App,
4577 ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
4578 let mut active_item = None;
4579 let mut panel_id = None;
4580 for dock in self.all_docks() {
4581 if dock.focus_handle(cx).contains_focused(window, cx) {
4582 if let Some(panel) = dock.read(cx).active_panel() {
4583 if let Some(pane) = panel.pane(cx) {
4584 if let Some(item) = pane.read(cx).active_item() {
4585 active_item = Some(item);
4586 panel_id = panel.remote_id();
4587 break;
4588 }
4589 }
4590 }
4591 }
4592 }
4593
4594 if active_item.is_none() {
4595 active_item = self.active_pane().read(cx).active_item();
4596 }
4597 (active_item, panel_id)
4598 }
4599
4600 fn update_followers(
4601 &self,
4602 project_only: bool,
4603 update: proto::update_followers::Variant,
4604 _: &mut Window,
4605 cx: &mut App,
4606 ) -> Option<()> {
4607 // If this update only applies to for followers in the current project,
4608 // then skip it unless this project is shared. If it applies to all
4609 // followers, regardless of project, then set `project_id` to none,
4610 // indicating that it goes to all followers.
4611 let project_id = if project_only {
4612 Some(self.project.read(cx).remote_id()?)
4613 } else {
4614 None
4615 };
4616 self.app_state().workspace_store.update(cx, |store, cx| {
4617 store.update_followers(project_id, update, cx)
4618 })
4619 }
4620
4621 pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<CollaboratorId> {
4622 self.follower_states.iter().find_map(|(leader_id, state)| {
4623 if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
4624 Some(*leader_id)
4625 } else {
4626 None
4627 }
4628 })
4629 }
4630
4631 fn leader_updated(
4632 &mut self,
4633 leader_id: impl Into<CollaboratorId>,
4634 window: &mut Window,
4635 cx: &mut Context<Self>,
4636 ) -> Option<Box<dyn ItemHandle>> {
4637 cx.notify();
4638
4639 let leader_id = leader_id.into();
4640 let (panel_id, item) = match leader_id {
4641 CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?,
4642 CollaboratorId::Agent => (None, self.active_item_for_agent()?),
4643 };
4644
4645 let state = self.follower_states.get(&leader_id)?;
4646 let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
4647 let pane;
4648 if let Some(panel_id) = panel_id {
4649 pane = self
4650 .activate_panel_for_proto_id(panel_id, window, cx)?
4651 .pane(cx)?;
4652 let state = self.follower_states.get_mut(&leader_id)?;
4653 state.dock_pane = Some(pane.clone());
4654 } else {
4655 pane = state.center_pane.clone();
4656 let state = self.follower_states.get_mut(&leader_id)?;
4657 if let Some(dock_pane) = state.dock_pane.take() {
4658 transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx);
4659 }
4660 }
4661
4662 pane.update(cx, |pane, cx| {
4663 let focus_active_item = pane.has_focus(window, cx) || transfer_focus;
4664 if let Some(index) = pane.index_for_item(item.as_ref()) {
4665 pane.activate_item(index, false, false, window, cx);
4666 } else {
4667 pane.add_item(item.boxed_clone(), false, false, None, window, cx)
4668 }
4669
4670 if focus_active_item {
4671 pane.focus_active_item(window, cx)
4672 }
4673 });
4674
4675 Some(item)
4676 }
4677
4678 fn active_item_for_agent(&self) -> Option<Box<dyn ItemHandle>> {
4679 let state = self.follower_states.get(&CollaboratorId::Agent)?;
4680 let active_view_id = state.active_view_id?;
4681 Some(
4682 state
4683 .items_by_leader_view_id
4684 .get(&active_view_id)?
4685 .view
4686 .boxed_clone(),
4687 )
4688 }
4689
4690 fn active_item_for_peer(
4691 &self,
4692 peer_id: PeerId,
4693 window: &mut Window,
4694 cx: &mut Context<Self>,
4695 ) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
4696 let call = self.active_call()?;
4697 let room = call.read(cx).room()?.read(cx);
4698 let participant = room.remote_participant_for_peer_id(peer_id)?;
4699 let leader_in_this_app;
4700 let leader_in_this_project;
4701 match participant.location {
4702 call::ParticipantLocation::SharedProject { project_id } => {
4703 leader_in_this_app = true;
4704 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
4705 }
4706 call::ParticipantLocation::UnsharedProject => {
4707 leader_in_this_app = true;
4708 leader_in_this_project = false;
4709 }
4710 call::ParticipantLocation::External => {
4711 leader_in_this_app = false;
4712 leader_in_this_project = false;
4713 }
4714 };
4715 let state = self.follower_states.get(&peer_id.into())?;
4716 let mut item_to_activate = None;
4717 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
4718 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
4719 if leader_in_this_project || !item.view.is_project_item(window, cx) {
4720 item_to_activate = Some((item.location, item.view.boxed_clone()));
4721 }
4722 }
4723 } else if let Some(shared_screen) =
4724 self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx)
4725 {
4726 item_to_activate = Some((None, Box::new(shared_screen)));
4727 }
4728 item_to_activate
4729 }
4730
4731 fn shared_screen_for_peer(
4732 &self,
4733 peer_id: PeerId,
4734 pane: &Entity<Pane>,
4735 window: &mut Window,
4736 cx: &mut App,
4737 ) -> Option<Entity<SharedScreen>> {
4738 let call = self.active_call()?;
4739 let room = call.read(cx).room()?.clone();
4740 let participant = room.read(cx).remote_participant_for_peer_id(peer_id)?;
4741 let track = participant.video_tracks.values().next()?.clone();
4742 let user = participant.user.clone();
4743
4744 for item in pane.read(cx).items_of_type::<SharedScreen>() {
4745 if item.read(cx).peer_id == peer_id {
4746 return Some(item);
4747 }
4748 }
4749
4750 Some(cx.new(|cx| SharedScreen::new(track, peer_id, user.clone(), room.clone(), window, cx)))
4751 }
4752
4753 pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4754 if window.is_window_active() {
4755 self.update_active_view_for_followers(window, cx);
4756
4757 if let Some(database_id) = self.database_id {
4758 cx.background_spawn(persistence::DB.update_timestamp(database_id))
4759 .detach();
4760 }
4761 } else {
4762 for pane in &self.panes {
4763 pane.update(cx, |pane, cx| {
4764 if let Some(item) = pane.active_item() {
4765 item.workspace_deactivated(window, cx);
4766 }
4767 for item in pane.items() {
4768 if matches!(
4769 item.workspace_settings(cx).autosave,
4770 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
4771 ) {
4772 Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
4773 .detach_and_log_err(cx);
4774 }
4775 }
4776 });
4777 }
4778 }
4779 }
4780
4781 pub fn active_call(&self) -> Option<&Entity<ActiveCall>> {
4782 self.active_call.as_ref().map(|(call, _)| call)
4783 }
4784
4785 fn on_active_call_event(
4786 &mut self,
4787 _: &Entity<ActiveCall>,
4788 event: &call::room::Event,
4789 window: &mut Window,
4790 cx: &mut Context<Self>,
4791 ) {
4792 match event {
4793 call::room::Event::ParticipantLocationChanged { participant_id }
4794 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4795 self.leader_updated(participant_id, window, cx);
4796 }
4797 _ => {}
4798 }
4799 }
4800
4801 pub fn database_id(&self) -> Option<WorkspaceId> {
4802 self.database_id
4803 }
4804
4805 pub fn session_id(&self) -> Option<String> {
4806 self.session_id.clone()
4807 }
4808
4809 fn local_paths(&self, cx: &App) -> Option<Vec<Arc<Path>>> {
4810 let project = self.project().read(cx);
4811
4812 if project.is_local() {
4813 Some(
4814 project
4815 .visible_worktrees(cx)
4816 .map(|worktree| worktree.read(cx).abs_path())
4817 .collect::<Vec<_>>(),
4818 )
4819 } else {
4820 None
4821 }
4822 }
4823
4824 fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
4825 match member {
4826 Member::Axis(PaneAxis { members, .. }) => {
4827 for child in members.iter() {
4828 self.remove_panes(child.clone(), window, cx)
4829 }
4830 }
4831 Member::Pane(pane) => {
4832 self.force_remove_pane(&pane, &None, window, cx);
4833 }
4834 }
4835 }
4836
4837 fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
4838 self.session_id.take();
4839 self.serialize_workspace_internal(window, cx)
4840 }
4841
4842 fn force_remove_pane(
4843 &mut self,
4844 pane: &Entity<Pane>,
4845 focus_on: &Option<Entity<Pane>>,
4846 window: &mut Window,
4847 cx: &mut Context<Workspace>,
4848 ) {
4849 self.panes.retain(|p| p != pane);
4850 if let Some(focus_on) = focus_on {
4851 focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4852 } else {
4853 if self.active_pane() == pane {
4854 self.panes
4855 .last()
4856 .unwrap()
4857 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4858 }
4859 }
4860 if self.last_active_center_pane == Some(pane.downgrade()) {
4861 self.last_active_center_pane = None;
4862 }
4863 cx.notify();
4864 }
4865
4866 fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4867 if self._schedule_serialize.is_none() {
4868 self._schedule_serialize = Some(cx.spawn_in(window, async move |this, cx| {
4869 cx.background_executor()
4870 .timer(Duration::from_millis(100))
4871 .await;
4872 this.update_in(cx, |this, window, cx| {
4873 this.serialize_workspace_internal(window, cx).detach();
4874 this._schedule_serialize.take();
4875 })
4876 .log_err();
4877 }));
4878 }
4879 }
4880
4881 fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
4882 let Some(database_id) = self.database_id() else {
4883 return Task::ready(());
4884 };
4885
4886 fn serialize_pane_handle(
4887 pane_handle: &Entity<Pane>,
4888 window: &mut Window,
4889 cx: &mut App,
4890 ) -> SerializedPane {
4891 let (items, active, pinned_count) = {
4892 let pane = pane_handle.read(cx);
4893 let active_item_id = pane.active_item().map(|item| item.item_id());
4894 (
4895 pane.items()
4896 .filter_map(|handle| {
4897 let handle = handle.to_serializable_item_handle(cx)?;
4898
4899 Some(SerializedItem {
4900 kind: Arc::from(handle.serialized_item_kind()),
4901 item_id: handle.item_id().as_u64(),
4902 active: Some(handle.item_id()) == active_item_id,
4903 preview: pane.is_active_preview_item(handle.item_id()),
4904 })
4905 })
4906 .collect::<Vec<_>>(),
4907 pane.has_focus(window, cx),
4908 pane.pinned_count(),
4909 )
4910 };
4911
4912 SerializedPane::new(items, active, pinned_count)
4913 }
4914
4915 fn build_serialized_pane_group(
4916 pane_group: &Member,
4917 window: &mut Window,
4918 cx: &mut App,
4919 ) -> SerializedPaneGroup {
4920 match pane_group {
4921 Member::Axis(PaneAxis {
4922 axis,
4923 members,
4924 flexes,
4925 bounding_boxes: _,
4926 }) => SerializedPaneGroup::Group {
4927 axis: SerializedAxis(*axis),
4928 children: members
4929 .iter()
4930 .map(|member| build_serialized_pane_group(member, window, cx))
4931 .collect::<Vec<_>>(),
4932 flexes: Some(flexes.lock().clone()),
4933 },
4934 Member::Pane(pane_handle) => {
4935 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
4936 }
4937 }
4938 }
4939
4940 fn build_serialized_docks(
4941 this: &Workspace,
4942 window: &mut Window,
4943 cx: &mut App,
4944 ) -> DockStructure {
4945 let left_dock = this.left_dock.read(cx);
4946 let left_visible = left_dock.is_open();
4947 let left_active_panel = left_dock
4948 .active_panel()
4949 .map(|panel| panel.persistent_name().to_string());
4950 let left_dock_zoom = left_dock
4951 .active_panel()
4952 .map(|panel| panel.is_zoomed(window, cx))
4953 .unwrap_or(false);
4954
4955 let right_dock = this.right_dock.read(cx);
4956 let right_visible = right_dock.is_open();
4957 let right_active_panel = right_dock
4958 .active_panel()
4959 .map(|panel| panel.persistent_name().to_string());
4960 let right_dock_zoom = right_dock
4961 .active_panel()
4962 .map(|panel| panel.is_zoomed(window, cx))
4963 .unwrap_or(false);
4964
4965 let bottom_dock = this.bottom_dock.read(cx);
4966 let bottom_visible = bottom_dock.is_open();
4967 let bottom_active_panel = bottom_dock
4968 .active_panel()
4969 .map(|panel| panel.persistent_name().to_string());
4970 let bottom_dock_zoom = bottom_dock
4971 .active_panel()
4972 .map(|panel| panel.is_zoomed(window, cx))
4973 .unwrap_or(false);
4974
4975 DockStructure {
4976 left: DockData {
4977 visible: left_visible,
4978 active_panel: left_active_panel,
4979 zoom: left_dock_zoom,
4980 },
4981 right: DockData {
4982 visible: right_visible,
4983 active_panel: right_active_panel,
4984 zoom: right_dock_zoom,
4985 },
4986 bottom: DockData {
4987 visible: bottom_visible,
4988 active_panel: bottom_active_panel,
4989 zoom: bottom_dock_zoom,
4990 },
4991 }
4992 }
4993
4994 if let Some(location) = self.serialize_workspace_location(cx) {
4995 let breakpoints = self.project.update(cx, |project, cx| {
4996 project
4997 .breakpoint_store()
4998 .read(cx)
4999 .all_source_breakpoints(cx)
5000 });
5001
5002 let center_group = build_serialized_pane_group(&self.center.root, window, cx);
5003 let docks = build_serialized_docks(self, window, cx);
5004 let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
5005 let serialized_workspace = SerializedWorkspace {
5006 id: database_id,
5007 location,
5008 center_group,
5009 window_bounds,
5010 display: Default::default(),
5011 docks,
5012 centered_layout: self.centered_layout,
5013 session_id: self.session_id.clone(),
5014 breakpoints,
5015 window_id: Some(window.window_handle().window_id().as_u64()),
5016 };
5017
5018 return window.spawn(cx, async move |_| {
5019 persistence::DB.save_workspace(serialized_workspace).await;
5020 });
5021 }
5022 Task::ready(())
5023 }
5024
5025 fn serialize_workspace_location(&self, cx: &App) -> Option<SerializedWorkspaceLocation> {
5026 if let Some(ssh_project) = &self.serialized_ssh_project {
5027 Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
5028 } else if let Some(local_paths) = self.local_paths(cx) {
5029 if !local_paths.is_empty() {
5030 Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
5031 } else {
5032 None
5033 }
5034 } else {
5035 None
5036 }
5037 }
5038
5039 fn update_history(&self, cx: &mut App) {
5040 let Some(id) = self.database_id() else {
5041 return;
5042 };
5043 let Some(location) = self.serialize_workspace_location(cx) else {
5044 return;
5045 };
5046 if let Some(manager) = HistoryManager::global(cx) {
5047 manager.update(cx, |this, cx| {
5048 this.update_history(id, HistoryManagerEntry::new(id, &location), cx);
5049 });
5050 }
5051 }
5052
5053 async fn serialize_items(
5054 this: &WeakEntity<Self>,
5055 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
5056 cx: &mut AsyncWindowContext,
5057 ) -> Result<()> {
5058 const CHUNK_SIZE: usize = 200;
5059
5060 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
5061
5062 while let Some(items_received) = serializable_items.next().await {
5063 let unique_items =
5064 items_received
5065 .into_iter()
5066 .fold(HashMap::default(), |mut acc, item| {
5067 acc.entry(item.item_id()).or_insert(item);
5068 acc
5069 });
5070
5071 // We use into_iter() here so that the references to the items are moved into
5072 // the tasks and not kept alive while we're sleeping.
5073 for (_, item) in unique_items.into_iter() {
5074 if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
5075 item.serialize(workspace, false, window, cx)
5076 }) {
5077 cx.background_spawn(async move { task.await.log_err() })
5078 .detach();
5079 }
5080 }
5081
5082 cx.background_executor()
5083 .timer(SERIALIZATION_THROTTLE_TIME)
5084 .await;
5085 }
5086
5087 Ok(())
5088 }
5089
5090 pub(crate) fn enqueue_item_serialization(
5091 &mut self,
5092 item: Box<dyn SerializableItemHandle>,
5093 ) -> Result<()> {
5094 self.serializable_items_tx
5095 .unbounded_send(item)
5096 .map_err(|err| anyhow!("failed to send serializable item over channel: {err}"))
5097 }
5098
5099 pub(crate) fn load_workspace(
5100 serialized_workspace: SerializedWorkspace,
5101 paths_to_open: Vec<Option<ProjectPath>>,
5102 window: &mut Window,
5103 cx: &mut Context<Workspace>,
5104 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
5105 cx.spawn_in(window, async move |workspace, cx| {
5106 let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
5107
5108 let mut center_group = None;
5109 let mut center_items = None;
5110
5111 // Traverse the splits tree and add to things
5112 if let Some((group, active_pane, items)) = serialized_workspace
5113 .center_group
5114 .deserialize(&project, serialized_workspace.id, workspace.clone(), cx)
5115 .await
5116 {
5117 center_items = Some(items);
5118 center_group = Some((group, active_pane))
5119 }
5120
5121 let mut items_by_project_path = HashMap::default();
5122 let mut item_ids_by_kind = HashMap::default();
5123 let mut all_deserialized_items = Vec::default();
5124 cx.update(|_, cx| {
5125 for item in center_items.unwrap_or_default().into_iter().flatten() {
5126 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
5127 item_ids_by_kind
5128 .entry(serializable_item_handle.serialized_item_kind())
5129 .or_insert(Vec::new())
5130 .push(item.item_id().as_u64() as ItemId);
5131 }
5132
5133 if let Some(project_path) = item.project_path(cx) {
5134 items_by_project_path.insert(project_path, item.clone());
5135 }
5136 all_deserialized_items.push(item);
5137 }
5138 })?;
5139
5140 let opened_items = paths_to_open
5141 .into_iter()
5142 .map(|path_to_open| {
5143 path_to_open
5144 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
5145 })
5146 .collect::<Vec<_>>();
5147
5148 // Remove old panes from workspace panes list
5149 workspace.update_in(cx, |workspace, window, cx| {
5150 if let Some((center_group, active_pane)) = center_group {
5151 workspace.remove_panes(workspace.center.root.clone(), window, cx);
5152
5153 // Swap workspace center group
5154 workspace.center = PaneGroup::with_root(center_group);
5155 if let Some(active_pane) = active_pane {
5156 workspace.set_active_pane(&active_pane, window, cx);
5157 cx.focus_self(window);
5158 } else {
5159 workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
5160 }
5161 }
5162
5163 let docks = serialized_workspace.docks;
5164
5165 for (dock, serialized_dock) in [
5166 (&mut workspace.right_dock, docks.right),
5167 (&mut workspace.left_dock, docks.left),
5168 (&mut workspace.bottom_dock, docks.bottom),
5169 ]
5170 .iter_mut()
5171 {
5172 dock.update(cx, |dock, cx| {
5173 dock.serialized_dock = Some(serialized_dock.clone());
5174 dock.restore_state(window, cx);
5175 });
5176 }
5177
5178 cx.notify();
5179 })?;
5180
5181 let _ = project
5182 .update(cx, |project, cx| {
5183 project
5184 .breakpoint_store()
5185 .update(cx, |breakpoint_store, cx| {
5186 breakpoint_store
5187 .with_serialized_breakpoints(serialized_workspace.breakpoints, cx)
5188 })
5189 })?
5190 .await;
5191
5192 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
5193 // after loading the items, we might have different items and in order to avoid
5194 // the database filling up, we delete items that haven't been loaded now.
5195 //
5196 // The items that have been loaded, have been saved after they've been added to the workspace.
5197 let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
5198 item_ids_by_kind
5199 .into_iter()
5200 .map(|(item_kind, loaded_items)| {
5201 SerializableItemRegistry::cleanup(
5202 item_kind,
5203 serialized_workspace.id,
5204 loaded_items,
5205 window,
5206 cx,
5207 )
5208 .log_err()
5209 })
5210 .collect::<Vec<_>>()
5211 })?;
5212
5213 futures::future::join_all(clean_up_tasks).await;
5214
5215 workspace
5216 .update_in(cx, |workspace, window, cx| {
5217 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
5218 workspace.serialize_workspace_internal(window, cx).detach();
5219
5220 // Ensure that we mark the window as edited if we did load dirty items
5221 workspace.update_window_edited(window, cx);
5222 })
5223 .ok();
5224
5225 Ok(opened_items)
5226 })
5227 }
5228
5229 fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
5230 self.add_workspace_actions_listeners(div, window, cx)
5231 .on_action(cx.listener(Self::close_inactive_items_and_panes))
5232 .on_action(cx.listener(Self::close_all_items_and_panes))
5233 .on_action(cx.listener(Self::save_all))
5234 .on_action(cx.listener(Self::send_keystrokes))
5235 .on_action(cx.listener(Self::add_folder_to_project))
5236 .on_action(cx.listener(Self::follow_next_collaborator))
5237 .on_action(cx.listener(Self::close_window))
5238 .on_action(cx.listener(Self::activate_pane_at_index))
5239 .on_action(cx.listener(Self::move_item_to_pane_at_index))
5240 .on_action(cx.listener(Self::move_focused_panel_to_next_position))
5241 .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
5242 let pane = workspace.active_pane().clone();
5243 workspace.unfollow_in_pane(&pane, window, cx);
5244 }))
5245 .on_action(cx.listener(|workspace, action: &Save, window, cx| {
5246 workspace
5247 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
5248 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
5249 }))
5250 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
5251 workspace
5252 .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
5253 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
5254 }))
5255 .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
5256 workspace
5257 .save_active_item(SaveIntent::SaveAs, window, cx)
5258 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
5259 }))
5260 .on_action(
5261 cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
5262 workspace.activate_previous_pane(window, cx)
5263 }),
5264 )
5265 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
5266 workspace.activate_next_pane(window, cx)
5267 }))
5268 .on_action(
5269 cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
5270 workspace.activate_next_window(cx)
5271 }),
5272 )
5273 .on_action(
5274 cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
5275 workspace.activate_previous_window(cx)
5276 }),
5277 )
5278 .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
5279 workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
5280 }))
5281 .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
5282 workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
5283 }))
5284 .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
5285 workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
5286 }))
5287 .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
5288 workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
5289 }))
5290 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
5291 workspace.activate_next_pane(window, cx)
5292 }))
5293 .on_action(cx.listener(
5294 |workspace, action: &MoveItemToPaneInDirection, window, cx| {
5295 workspace.move_item_to_pane_in_direction(action, window, cx)
5296 },
5297 ))
5298 .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
5299 workspace.swap_pane_in_direction(SplitDirection::Left, cx)
5300 }))
5301 .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
5302 workspace.swap_pane_in_direction(SplitDirection::Right, cx)
5303 }))
5304 .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
5305 workspace.swap_pane_in_direction(SplitDirection::Up, cx)
5306 }))
5307 .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
5308 workspace.swap_pane_in_direction(SplitDirection::Down, cx)
5309 }))
5310 .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
5311 this.toggle_dock(DockPosition::Left, window, cx);
5312 }))
5313 .on_action(cx.listener(
5314 |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
5315 workspace.toggle_dock(DockPosition::Right, window, cx);
5316 },
5317 ))
5318 .on_action(cx.listener(
5319 |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
5320 workspace.toggle_dock(DockPosition::Bottom, window, cx);
5321 },
5322 ))
5323 .on_action(cx.listener(
5324 |workspace: &mut Workspace, _: &CloseActiveDock, window, cx| {
5325 workspace.close_active_dock(window, cx);
5326 },
5327 ))
5328 .on_action(
5329 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
5330 workspace.close_all_docks(window, cx);
5331 }),
5332 )
5333 .on_action(cx.listener(
5334 |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
5335 workspace.clear_all_notifications(cx);
5336 },
5337 ))
5338 .on_action(cx.listener(
5339 |workspace: &mut Workspace, _: &SuppressNotification, _, cx| {
5340 if let Some((notification_id, _)) = workspace.notifications.pop() {
5341 workspace.suppress_notification(¬ification_id, cx);
5342 }
5343 },
5344 ))
5345 .on_action(cx.listener(
5346 |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
5347 workspace.reopen_closed_item(window, cx).detach();
5348 },
5349 ))
5350 .on_action(cx.listener(Workspace::toggle_centered_layout))
5351 .on_action(cx.listener(Workspace::cancel))
5352 }
5353
5354 #[cfg(any(test, feature = "test-support"))]
5355 pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
5356 use node_runtime::NodeRuntime;
5357 use session::Session;
5358
5359 let client = project.read(cx).client();
5360 let user_store = project.read(cx).user_store();
5361
5362 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
5363 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
5364 window.activate_window();
5365 let app_state = Arc::new(AppState {
5366 languages: project.read(cx).languages().clone(),
5367 workspace_store,
5368 client,
5369 user_store,
5370 fs: project.read(cx).fs().clone(),
5371 build_window_options: |_, _| Default::default(),
5372 node_runtime: NodeRuntime::unavailable(),
5373 session,
5374 });
5375 let workspace = Self::new(Default::default(), project, app_state, window, cx);
5376 workspace
5377 .active_pane
5378 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
5379 workspace
5380 }
5381
5382 pub fn register_action<A: Action>(
5383 &mut self,
5384 callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
5385 ) -> &mut Self {
5386 let callback = Arc::new(callback);
5387
5388 self.workspace_actions.push(Box::new(move |div, _, cx| {
5389 let callback = callback.clone();
5390 div.on_action(cx.listener(move |workspace, event, window, cx| {
5391 (callback)(workspace, event, window, cx)
5392 }))
5393 }));
5394 self
5395 }
5396
5397 fn add_workspace_actions_listeners(
5398 &self,
5399 mut div: Div,
5400 window: &mut Window,
5401 cx: &mut Context<Self>,
5402 ) -> Div {
5403 for action in self.workspace_actions.iter() {
5404 div = (action)(div, window, cx)
5405 }
5406 div
5407 }
5408
5409 pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
5410 self.modal_layer.read(cx).has_active_modal()
5411 }
5412
5413 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
5414 self.modal_layer.read(cx).active_modal()
5415 }
5416
5417 pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
5418 where
5419 B: FnOnce(&mut Window, &mut Context<V>) -> V,
5420 {
5421 self.modal_layer.update(cx, |modal_layer, cx| {
5422 modal_layer.toggle_modal(window, cx, build)
5423 })
5424 }
5425
5426 pub fn toggle_status_toast<V: ToastView>(&mut self, entity: Entity<V>, cx: &mut App) {
5427 self.toast_layer
5428 .update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity))
5429 }
5430
5431 pub fn toggle_centered_layout(
5432 &mut self,
5433 _: &ToggleCenteredLayout,
5434 _: &mut Window,
5435 cx: &mut Context<Self>,
5436 ) {
5437 self.centered_layout = !self.centered_layout;
5438 if let Some(database_id) = self.database_id() {
5439 cx.background_spawn(DB.set_centered_layout(database_id, self.centered_layout))
5440 .detach_and_log_err(cx);
5441 }
5442 cx.notify();
5443 }
5444
5445 fn adjust_padding(padding: Option<f32>) -> f32 {
5446 padding
5447 .unwrap_or(Self::DEFAULT_PADDING)
5448 .clamp(0.0, Self::MAX_PADDING)
5449 }
5450
5451 fn render_dock(
5452 &self,
5453 position: DockPosition,
5454 dock: &Entity<Dock>,
5455 window: &mut Window,
5456 cx: &mut App,
5457 ) -> Option<Div> {
5458 if self.zoomed_position == Some(position) {
5459 return None;
5460 }
5461
5462 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
5463 let pane = panel.pane(cx)?;
5464 let follower_states = &self.follower_states;
5465 leader_border_for_pane(follower_states, &pane, window, cx)
5466 });
5467
5468 Some(
5469 div()
5470 .flex()
5471 .flex_none()
5472 .overflow_hidden()
5473 .child(dock.clone())
5474 .children(leader_border),
5475 )
5476 }
5477
5478 pub fn for_window(window: &mut Window, _: &mut App) -> Option<Entity<Workspace>> {
5479 window.root().flatten()
5480 }
5481
5482 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
5483 self.zoomed.as_ref()
5484 }
5485
5486 pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
5487 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5488 return;
5489 };
5490 let windows = cx.windows();
5491 let Some(next_window) = windows
5492 .iter()
5493 .cycle()
5494 .skip_while(|window| window.window_id() != current_window_id)
5495 .nth(1)
5496 else {
5497 return;
5498 };
5499 next_window
5500 .update(cx, |_, window, _| window.activate_window())
5501 .ok();
5502 }
5503
5504 pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
5505 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5506 return;
5507 };
5508 let windows = cx.windows();
5509 let Some(prev_window) = windows
5510 .iter()
5511 .rev()
5512 .cycle()
5513 .skip_while(|window| window.window_id() != current_window_id)
5514 .nth(1)
5515 else {
5516 return;
5517 };
5518 prev_window
5519 .update(cx, |_, window, _| window.activate_window())
5520 .ok();
5521 }
5522
5523 pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
5524 if cx.stop_active_drag(window) {
5525 return;
5526 } else if let Some((notification_id, _)) = self.notifications.pop() {
5527 dismiss_app_notification(¬ification_id, cx);
5528 } else {
5529 cx.emit(Event::ClearActivityIndicator);
5530 cx.propagate();
5531 }
5532 }
5533}
5534
5535fn leader_border_for_pane(
5536 follower_states: &HashMap<CollaboratorId, FollowerState>,
5537 pane: &Entity<Pane>,
5538 _: &Window,
5539 cx: &App,
5540) -> Option<Div> {
5541 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
5542 if state.pane() == pane {
5543 Some((*leader_id, state))
5544 } else {
5545 None
5546 }
5547 })?;
5548
5549 let mut leader_color = match leader_id {
5550 CollaboratorId::PeerId(leader_peer_id) => {
5551 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
5552 let leader = room.remote_participant_for_peer_id(leader_peer_id)?;
5553
5554 cx.theme()
5555 .players()
5556 .color_for_participant(leader.participant_index.0)
5557 .cursor
5558 }
5559 CollaboratorId::Agent => cx.theme().players().agent().cursor,
5560 };
5561 leader_color.fade_out(0.3);
5562 Some(
5563 div()
5564 .absolute()
5565 .size_full()
5566 .left_0()
5567 .top_0()
5568 .border_2()
5569 .border_color(leader_color),
5570 )
5571}
5572
5573fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
5574 ZED_WINDOW_POSITION
5575 .zip(*ZED_WINDOW_SIZE)
5576 .map(|(position, size)| Bounds {
5577 origin: position,
5578 size,
5579 })
5580}
5581
5582fn open_items(
5583 serialized_workspace: Option<SerializedWorkspace>,
5584 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
5585 window: &mut Window,
5586 cx: &mut Context<Workspace>,
5587) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
5588 let restored_items = serialized_workspace.map(|serialized_workspace| {
5589 Workspace::load_workspace(
5590 serialized_workspace,
5591 project_paths_to_open
5592 .iter()
5593 .map(|(_, project_path)| project_path)
5594 .cloned()
5595 .collect(),
5596 window,
5597 cx,
5598 )
5599 });
5600
5601 cx.spawn_in(window, async move |workspace, cx| {
5602 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
5603
5604 if let Some(restored_items) = restored_items {
5605 let restored_items = restored_items.await?;
5606
5607 let restored_project_paths = restored_items
5608 .iter()
5609 .filter_map(|item| {
5610 cx.update(|_, cx| item.as_ref()?.project_path(cx))
5611 .ok()
5612 .flatten()
5613 })
5614 .collect::<HashSet<_>>();
5615
5616 for restored_item in restored_items {
5617 opened_items.push(restored_item.map(Ok));
5618 }
5619
5620 project_paths_to_open
5621 .iter_mut()
5622 .for_each(|(_, project_path)| {
5623 if let Some(project_path_to_open) = project_path {
5624 if restored_project_paths.contains(project_path_to_open) {
5625 *project_path = None;
5626 }
5627 }
5628 });
5629 } else {
5630 for _ in 0..project_paths_to_open.len() {
5631 opened_items.push(None);
5632 }
5633 }
5634 assert!(opened_items.len() == project_paths_to_open.len());
5635
5636 let tasks =
5637 project_paths_to_open
5638 .into_iter()
5639 .enumerate()
5640 .map(|(ix, (abs_path, project_path))| {
5641 let workspace = workspace.clone();
5642 cx.spawn(async move |cx| {
5643 let file_project_path = project_path?;
5644 let abs_path_task = workspace.update(cx, |workspace, cx| {
5645 workspace.project().update(cx, |project, cx| {
5646 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
5647 })
5648 });
5649
5650 // We only want to open file paths here. If one of the items
5651 // here is a directory, it was already opened further above
5652 // with a `find_or_create_worktree`.
5653 if let Ok(task) = abs_path_task {
5654 if task.await.map_or(true, |p| p.is_file()) {
5655 return Some((
5656 ix,
5657 workspace
5658 .update_in(cx, |workspace, window, cx| {
5659 workspace.open_path(
5660 file_project_path,
5661 None,
5662 true,
5663 window,
5664 cx,
5665 )
5666 })
5667 .log_err()?
5668 .await,
5669 ));
5670 }
5671 }
5672 None
5673 })
5674 });
5675
5676 let tasks = tasks.collect::<Vec<_>>();
5677
5678 let tasks = futures::future::join_all(tasks);
5679 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
5680 opened_items[ix] = Some(path_open_result);
5681 }
5682
5683 Ok(opened_items)
5684 })
5685}
5686
5687enum ActivateInDirectionTarget {
5688 Pane(Entity<Pane>),
5689 Dock(Entity<Dock>),
5690}
5691
5692fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
5693 workspace
5694 .update(cx, |workspace, _, cx| {
5695 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
5696 struct DatabaseFailedNotification;
5697
5698 workspace.show_notification(
5699 NotificationId::unique::<DatabaseFailedNotification>(),
5700 cx,
5701 |cx| {
5702 cx.new(|cx| {
5703 MessageNotification::new("Failed to load the database file.", cx)
5704 .primary_message("File an Issue")
5705 .primary_icon(IconName::Plus)
5706 .primary_on_click(|window, cx| {
5707 window.dispatch_action(Box::new(FileBugReport), cx)
5708 })
5709 })
5710 },
5711 );
5712 }
5713 })
5714 .log_err();
5715}
5716
5717impl Focusable for Workspace {
5718 fn focus_handle(&self, cx: &App) -> FocusHandle {
5719 self.active_pane.focus_handle(cx)
5720 }
5721}
5722
5723#[derive(Clone)]
5724struct DraggedDock(DockPosition);
5725
5726impl Render for DraggedDock {
5727 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5728 gpui::Empty
5729 }
5730}
5731
5732impl Render for Workspace {
5733 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5734 let mut context = KeyContext::new_with_defaults();
5735 context.add("Workspace");
5736 context.set("keyboard_layout", cx.keyboard_layout().name().to_string());
5737 let centered_layout = self.centered_layout
5738 && self.center.panes().len() == 1
5739 && self.active_item(cx).is_some();
5740 let render_padding = |size| {
5741 (size > 0.0).then(|| {
5742 div()
5743 .h_full()
5744 .w(relative(size))
5745 .bg(cx.theme().colors().editor_background)
5746 .border_color(cx.theme().colors().pane_group_border)
5747 })
5748 };
5749 let paddings = if centered_layout {
5750 let settings = WorkspaceSettings::get_global(cx).centered_layout;
5751 (
5752 render_padding(Self::adjust_padding(settings.left_padding)),
5753 render_padding(Self::adjust_padding(settings.right_padding)),
5754 )
5755 } else {
5756 (None, None)
5757 };
5758 let ui_font = theme::setup_ui_font(window, cx);
5759
5760 let theme = cx.theme().clone();
5761 let colors = theme.colors();
5762 let notification_entities = self
5763 .notifications
5764 .iter()
5765 .map(|(_, notification)| notification.entity_id())
5766 .collect::<Vec<_>>();
5767
5768 client_side_decorations(
5769 self.actions(div(), window, cx)
5770 .key_context(context)
5771 .relative()
5772 .size_full()
5773 .flex()
5774 .flex_col()
5775 .font(ui_font)
5776 .gap_0()
5777 .justify_start()
5778 .items_start()
5779 .text_color(colors.text)
5780 .overflow_hidden()
5781 .children(self.titlebar_item.clone())
5782 .on_modifiers_changed(move |_, _, cx| {
5783 for &id in ¬ification_entities {
5784 cx.notify(id);
5785 }
5786 })
5787 .child(
5788 div()
5789 .size_full()
5790 .relative()
5791 .flex_1()
5792 .flex()
5793 .flex_col()
5794 .child(
5795 div()
5796 .id("workspace")
5797 .bg(colors.background)
5798 .relative()
5799 .flex_1()
5800 .w_full()
5801 .flex()
5802 .flex_col()
5803 .overflow_hidden()
5804 .border_t_1()
5805 .border_b_1()
5806 .border_color(colors.border)
5807 .child({
5808 let this = cx.entity().clone();
5809 canvas(
5810 move |bounds, window, cx| {
5811 this.update(cx, |this, cx| {
5812 let bounds_changed = this.bounds != bounds;
5813 this.bounds = bounds;
5814
5815 if bounds_changed {
5816 this.left_dock.update(cx, |dock, cx| {
5817 dock.clamp_panel_size(
5818 bounds.size.width,
5819 window,
5820 cx,
5821 )
5822 });
5823
5824 this.right_dock.update(cx, |dock, cx| {
5825 dock.clamp_panel_size(
5826 bounds.size.width,
5827 window,
5828 cx,
5829 )
5830 });
5831
5832 this.bottom_dock.update(cx, |dock, cx| {
5833 dock.clamp_panel_size(
5834 bounds.size.height,
5835 window,
5836 cx,
5837 )
5838 });
5839 }
5840 })
5841 },
5842 |_, _, _, _| {},
5843 )
5844 .absolute()
5845 .size_full()
5846 })
5847 .when(self.zoomed.is_none(), |this| {
5848 this.on_drag_move(cx.listener(
5849 move |workspace,
5850 e: &DragMoveEvent<DraggedDock>,
5851 window,
5852 cx| {
5853 if workspace.previous_dock_drag_coordinates
5854 != Some(e.event.position)
5855 {
5856 workspace.previous_dock_drag_coordinates =
5857 Some(e.event.position);
5858 match e.drag(cx).0 {
5859 DockPosition::Left => {
5860 resize_left_dock(
5861 e.event.position.x
5862 - workspace.bounds.left(),
5863 workspace,
5864 window,
5865 cx,
5866 );
5867 }
5868 DockPosition::Right => {
5869 resize_right_dock(
5870 workspace.bounds.right()
5871 - e.event.position.x,
5872 workspace,
5873 window,
5874 cx,
5875 );
5876 }
5877 DockPosition::Bottom => {
5878 resize_bottom_dock(
5879 workspace.bounds.bottom()
5880 - e.event.position.y,
5881 workspace,
5882 window,
5883 cx,
5884 );
5885 }
5886 };
5887 workspace.serialize_workspace(window, cx);
5888 }
5889 },
5890 ))
5891 })
5892 .child({
5893 match self.bottom_dock_layout {
5894 BottomDockLayout::Full => div()
5895 .flex()
5896 .flex_col()
5897 .h_full()
5898 .child(
5899 div()
5900 .flex()
5901 .flex_row()
5902 .flex_1()
5903 .overflow_hidden()
5904 .children(self.render_dock(
5905 DockPosition::Left,
5906 &self.left_dock,
5907 window,
5908 cx,
5909 ))
5910 .child(
5911 div()
5912 .flex()
5913 .flex_col()
5914 .flex_1()
5915 .overflow_hidden()
5916 .child(
5917 h_flex()
5918 .flex_1()
5919 .when_some(
5920 paddings.0,
5921 |this, p| {
5922 this.child(
5923 p.border_r_1(),
5924 )
5925 },
5926 )
5927 .child(self.center.render(
5928 self.zoomed.as_ref(),
5929 &PaneRenderContext {
5930 follower_states:
5931 &self.follower_states,
5932 active_call: self.active_call(),
5933 active_pane: &self.active_pane,
5934 app_state: &self.app_state,
5935 project: &self.project,
5936 workspace: &self.weak_self,
5937 },
5938 window,
5939 cx,
5940 ))
5941 .when_some(
5942 paddings.1,
5943 |this, p| {
5944 this.child(
5945 p.border_l_1(),
5946 )
5947 },
5948 ),
5949 ),
5950 )
5951 .children(self.render_dock(
5952 DockPosition::Right,
5953 &self.right_dock,
5954 window,
5955 cx,
5956 )),
5957 )
5958 .child(div().w_full().children(self.render_dock(
5959 DockPosition::Bottom,
5960 &self.bottom_dock,
5961 window,
5962 cx
5963 ))),
5964
5965 BottomDockLayout::LeftAligned => div()
5966 .flex()
5967 .flex_row()
5968 .h_full()
5969 .child(
5970 div()
5971 .flex()
5972 .flex_col()
5973 .flex_1()
5974 .h_full()
5975 .child(
5976 div()
5977 .flex()
5978 .flex_row()
5979 .flex_1()
5980 .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx))
5981 .child(
5982 div()
5983 .flex()
5984 .flex_col()
5985 .flex_1()
5986 .overflow_hidden()
5987 .child(
5988 h_flex()
5989 .flex_1()
5990 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
5991 .child(self.center.render(
5992 self.zoomed.as_ref(),
5993 &PaneRenderContext {
5994 follower_states:
5995 &self.follower_states,
5996 active_call: self.active_call(),
5997 active_pane: &self.active_pane,
5998 app_state: &self.app_state,
5999 project: &self.project,
6000 workspace: &self.weak_self,
6001 },
6002 window,
6003 cx,
6004 ))
6005 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
6006 )
6007 )
6008 )
6009 .child(
6010 div()
6011 .w_full()
6012 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
6013 ),
6014 )
6015 .children(self.render_dock(
6016 DockPosition::Right,
6017 &self.right_dock,
6018 window,
6019 cx,
6020 )),
6021
6022 BottomDockLayout::RightAligned => div()
6023 .flex()
6024 .flex_row()
6025 .h_full()
6026 .children(self.render_dock(
6027 DockPosition::Left,
6028 &self.left_dock,
6029 window,
6030 cx,
6031 ))
6032 .child(
6033 div()
6034 .flex()
6035 .flex_col()
6036 .flex_1()
6037 .h_full()
6038 .child(
6039 div()
6040 .flex()
6041 .flex_row()
6042 .flex_1()
6043 .child(
6044 div()
6045 .flex()
6046 .flex_col()
6047 .flex_1()
6048 .overflow_hidden()
6049 .child(
6050 h_flex()
6051 .flex_1()
6052 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
6053 .child(self.center.render(
6054 self.zoomed.as_ref(),
6055 &PaneRenderContext {
6056 follower_states:
6057 &self.follower_states,
6058 active_call: self.active_call(),
6059 active_pane: &self.active_pane,
6060 app_state: &self.app_state,
6061 project: &self.project,
6062 workspace: &self.weak_self,
6063 },
6064 window,
6065 cx,
6066 ))
6067 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
6068 )
6069 )
6070 .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx))
6071 )
6072 .child(
6073 div()
6074 .w_full()
6075 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
6076 ),
6077 ),
6078
6079 BottomDockLayout::Contained => div()
6080 .flex()
6081 .flex_row()
6082 .h_full()
6083 .children(self.render_dock(
6084 DockPosition::Left,
6085 &self.left_dock,
6086 window,
6087 cx,
6088 ))
6089 .child(
6090 div()
6091 .flex()
6092 .flex_col()
6093 .flex_1()
6094 .overflow_hidden()
6095 .child(
6096 h_flex()
6097 .flex_1()
6098 .when_some(paddings.0, |this, p| {
6099 this.child(p.border_r_1())
6100 })
6101 .child(self.center.render(
6102 self.zoomed.as_ref(),
6103 &PaneRenderContext {
6104 follower_states:
6105 &self.follower_states,
6106 active_call: self.active_call(),
6107 active_pane: &self.active_pane,
6108 app_state: &self.app_state,
6109 project: &self.project,
6110 workspace: &self.weak_self,
6111 },
6112 window,
6113 cx,
6114 ))
6115 .when_some(paddings.1, |this, p| {
6116 this.child(p.border_l_1())
6117 }),
6118 )
6119 .children(self.render_dock(
6120 DockPosition::Bottom,
6121 &self.bottom_dock,
6122 window,
6123 cx,
6124 )),
6125 )
6126 .children(self.render_dock(
6127 DockPosition::Right,
6128 &self.right_dock,
6129 window,
6130 cx,
6131 )),
6132 }
6133 })
6134 .children(self.zoomed.as_ref().and_then(|view| {
6135 let zoomed_view = view.upgrade()?;
6136 let div = div()
6137 .occlude()
6138 .absolute()
6139 .overflow_hidden()
6140 .border_color(colors.border)
6141 .bg(colors.background)
6142 .child(zoomed_view)
6143 .inset_0()
6144 .shadow_lg();
6145
6146 Some(match self.zoomed_position {
6147 Some(DockPosition::Left) => div.right_2().border_r_1(),
6148 Some(DockPosition::Right) => div.left_2().border_l_1(),
6149 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
6150 None => {
6151 div.top_2().bottom_2().left_2().right_2().border_1()
6152 }
6153 })
6154 }))
6155 .children(self.render_notifications(window, cx)),
6156 )
6157 .child(self.status_bar.clone())
6158 .child(self.modal_layer.clone())
6159 .child(self.toast_layer.clone()),
6160 ),
6161 window,
6162 cx,
6163 )
6164 }
6165}
6166
6167fn resize_bottom_dock(
6168 new_size: Pixels,
6169 workspace: &mut Workspace,
6170 window: &mut Window,
6171 cx: &mut App,
6172) {
6173 let size =
6174 new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE - workspace.bounds.top());
6175 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
6176 bottom_dock.resize_active_panel(Some(size), window, cx);
6177 });
6178}
6179
6180fn resize_right_dock(
6181 new_size: Pixels,
6182 workspace: &mut Workspace,
6183 window: &mut Window,
6184 cx: &mut App,
6185) {
6186 let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
6187 workspace.right_dock.update(cx, |right_dock, cx| {
6188 right_dock.resize_active_panel(Some(size), window, cx);
6189 });
6190}
6191
6192fn resize_left_dock(
6193 new_size: Pixels,
6194 workspace: &mut Workspace,
6195 window: &mut Window,
6196 cx: &mut App,
6197) {
6198 let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
6199
6200 workspace.left_dock.update(cx, |left_dock, cx| {
6201 left_dock.resize_active_panel(Some(size), window, cx);
6202 });
6203}
6204
6205impl WorkspaceStore {
6206 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
6207 Self {
6208 workspaces: Default::default(),
6209 _subscriptions: vec![
6210 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
6211 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
6212 ],
6213 client,
6214 }
6215 }
6216
6217 pub fn update_followers(
6218 &self,
6219 project_id: Option<u64>,
6220 update: proto::update_followers::Variant,
6221 cx: &App,
6222 ) -> Option<()> {
6223 let active_call = ActiveCall::try_global(cx)?;
6224 let room_id = active_call.read(cx).room()?.read(cx).id();
6225 self.client
6226 .send(proto::UpdateFollowers {
6227 room_id,
6228 project_id,
6229 variant: Some(update),
6230 })
6231 .log_err()
6232 }
6233
6234 pub async fn handle_follow(
6235 this: Entity<Self>,
6236 envelope: TypedEnvelope<proto::Follow>,
6237 mut cx: AsyncApp,
6238 ) -> Result<proto::FollowResponse> {
6239 this.update(&mut cx, |this, cx| {
6240 let follower = Follower {
6241 project_id: envelope.payload.project_id,
6242 peer_id: envelope.original_sender_id()?,
6243 };
6244
6245 let mut response = proto::FollowResponse::default();
6246 this.workspaces.retain(|workspace| {
6247 workspace
6248 .update(cx, |workspace, window, cx| {
6249 let handler_response =
6250 workspace.handle_follow(follower.project_id, window, cx);
6251 if let Some(active_view) = handler_response.active_view.clone() {
6252 if workspace.project.read(cx).remote_id() == follower.project_id {
6253 response.active_view = Some(active_view)
6254 }
6255 }
6256 })
6257 .is_ok()
6258 });
6259
6260 Ok(response)
6261 })?
6262 }
6263
6264 async fn handle_update_followers(
6265 this: Entity<Self>,
6266 envelope: TypedEnvelope<proto::UpdateFollowers>,
6267 mut cx: AsyncApp,
6268 ) -> Result<()> {
6269 let leader_id = envelope.original_sender_id()?;
6270 let update = envelope.payload;
6271
6272 this.update(&mut cx, |this, cx| {
6273 this.workspaces.retain(|workspace| {
6274 workspace
6275 .update(cx, |workspace, window, cx| {
6276 let project_id = workspace.project.read(cx).remote_id();
6277 if update.project_id != project_id && update.project_id.is_some() {
6278 return;
6279 }
6280 workspace.handle_update_followers(leader_id, update.clone(), window, cx);
6281 })
6282 .is_ok()
6283 });
6284 Ok(())
6285 })?
6286 }
6287}
6288
6289impl ViewId {
6290 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
6291 Ok(Self {
6292 creator: message
6293 .creator
6294 .map(CollaboratorId::PeerId)
6295 .context("creator is missing")?,
6296 id: message.id,
6297 })
6298 }
6299
6300 pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
6301 if let CollaboratorId::PeerId(peer_id) = self.creator {
6302 Some(proto::ViewId {
6303 creator: Some(peer_id),
6304 id: self.id,
6305 })
6306 } else {
6307 None
6308 }
6309 }
6310}
6311
6312impl FollowerState {
6313 fn pane(&self) -> &Entity<Pane> {
6314 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
6315 }
6316}
6317
6318pub trait WorkspaceHandle {
6319 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
6320}
6321
6322impl WorkspaceHandle for Entity<Workspace> {
6323 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
6324 self.read(cx)
6325 .worktrees(cx)
6326 .flat_map(|worktree| {
6327 let worktree_id = worktree.read(cx).id();
6328 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
6329 worktree_id,
6330 path: f.path.clone(),
6331 })
6332 })
6333 .collect::<Vec<_>>()
6334 }
6335}
6336
6337impl std::fmt::Debug for OpenPaths {
6338 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6339 f.debug_struct("OpenPaths")
6340 .field("paths", &self.paths)
6341 .finish()
6342 }
6343}
6344
6345pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
6346 DB.last_workspace().await.log_err().flatten()
6347}
6348
6349pub fn last_session_workspace_locations(
6350 last_session_id: &str,
6351 last_session_window_stack: Option<Vec<WindowId>>,
6352) -> Option<Vec<SerializedWorkspaceLocation>> {
6353 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
6354 .log_err()
6355}
6356
6357actions!(
6358 collab,
6359 [
6360 OpenChannelNotes,
6361 Mute,
6362 Deafen,
6363 LeaveCall,
6364 ShareProject,
6365 ScreenShare
6366 ]
6367);
6368actions!(zed, [OpenLog]);
6369
6370async fn join_channel_internal(
6371 channel_id: ChannelId,
6372 app_state: &Arc<AppState>,
6373 requesting_window: Option<WindowHandle<Workspace>>,
6374 active_call: &Entity<ActiveCall>,
6375 cx: &mut AsyncApp,
6376) -> Result<bool> {
6377 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
6378 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
6379 return (false, None);
6380 };
6381
6382 let already_in_channel = room.channel_id() == Some(channel_id);
6383 let should_prompt = room.is_sharing_project()
6384 && !room.remote_participants().is_empty()
6385 && !already_in_channel;
6386 let open_room = if already_in_channel {
6387 active_call.room().cloned()
6388 } else {
6389 None
6390 };
6391 (should_prompt, open_room)
6392 })?;
6393
6394 if let Some(room) = open_room {
6395 let task = room.update(cx, |room, cx| {
6396 if let Some((project, host)) = room.most_active_project(cx) {
6397 return Some(join_in_room_project(project, host, app_state.clone(), cx));
6398 }
6399
6400 None
6401 })?;
6402 if let Some(task) = task {
6403 task.await?;
6404 }
6405 return anyhow::Ok(true);
6406 }
6407
6408 if should_prompt {
6409 if let Some(workspace) = requesting_window {
6410 let answer = workspace
6411 .update(cx, |_, window, cx| {
6412 window.prompt(
6413 PromptLevel::Warning,
6414 "Do you want to switch channels?",
6415 Some("Leaving this call will unshare your current project."),
6416 &["Yes, Join Channel", "Cancel"],
6417 cx,
6418 )
6419 })?
6420 .await;
6421
6422 if answer == Ok(1) {
6423 return Ok(false);
6424 }
6425 } else {
6426 return Ok(false); // unreachable!() hopefully
6427 }
6428 }
6429
6430 let client = cx.update(|cx| active_call.read(cx).client())?;
6431
6432 let mut client_status = client.status();
6433
6434 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
6435 'outer: loop {
6436 let Some(status) = client_status.recv().await else {
6437 anyhow::bail!("error connecting");
6438 };
6439
6440 match status {
6441 Status::Connecting
6442 | Status::Authenticating
6443 | Status::Reconnecting
6444 | Status::Reauthenticating => continue,
6445 Status::Connected { .. } => break 'outer,
6446 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
6447 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
6448 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
6449 return Err(ErrorCode::Disconnected.into());
6450 }
6451 }
6452 }
6453
6454 let room = active_call
6455 .update(cx, |active_call, cx| {
6456 active_call.join_channel(channel_id, cx)
6457 })?
6458 .await?;
6459
6460 let Some(room) = room else {
6461 return anyhow::Ok(true);
6462 };
6463
6464 room.update(cx, |room, _| room.room_update_completed())?
6465 .await;
6466
6467 let task = room.update(cx, |room, cx| {
6468 if let Some((project, host)) = room.most_active_project(cx) {
6469 return Some(join_in_room_project(project, host, app_state.clone(), cx));
6470 }
6471
6472 // If you are the first to join a channel, see if you should share your project.
6473 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
6474 if let Some(workspace) = requesting_window {
6475 let project = workspace.update(cx, |workspace, _, cx| {
6476 let project = workspace.project.read(cx);
6477
6478 if !CallSettings::get_global(cx).share_on_join {
6479 return None;
6480 }
6481
6482 if (project.is_local() || project.is_via_ssh())
6483 && project.visible_worktrees(cx).any(|tree| {
6484 tree.read(cx)
6485 .root_entry()
6486 .map_or(false, |entry| entry.is_dir())
6487 })
6488 {
6489 Some(workspace.project.clone())
6490 } else {
6491 None
6492 }
6493 });
6494 if let Ok(Some(project)) = project {
6495 return Some(cx.spawn(async move |room, cx| {
6496 room.update(cx, |room, cx| room.share_project(project, cx))?
6497 .await?;
6498 Ok(())
6499 }));
6500 }
6501 }
6502 }
6503
6504 None
6505 })?;
6506 if let Some(task) = task {
6507 task.await?;
6508 return anyhow::Ok(true);
6509 }
6510 anyhow::Ok(false)
6511}
6512
6513pub fn join_channel(
6514 channel_id: ChannelId,
6515 app_state: Arc<AppState>,
6516 requesting_window: Option<WindowHandle<Workspace>>,
6517 cx: &mut App,
6518) -> Task<Result<()>> {
6519 let active_call = ActiveCall::global(cx);
6520 cx.spawn(async move |cx| {
6521 let result = join_channel_internal(
6522 channel_id,
6523 &app_state,
6524 requesting_window,
6525 &active_call,
6526 cx,
6527 )
6528 .await;
6529
6530 // join channel succeeded, and opened a window
6531 if matches!(result, Ok(true)) {
6532 return anyhow::Ok(());
6533 }
6534
6535 // find an existing workspace to focus and show call controls
6536 let mut active_window =
6537 requesting_window.or_else(|| activate_any_workspace_window( cx));
6538 if active_window.is_none() {
6539 // no open workspaces, make one to show the error in (blergh)
6540 let (window_handle, _) = cx
6541 .update(|cx| {
6542 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
6543 })?
6544 .await?;
6545
6546 if result.is_ok() {
6547 cx.update(|cx| {
6548 cx.dispatch_action(&OpenChannelNotes);
6549 }).log_err();
6550 }
6551
6552 active_window = Some(window_handle);
6553 }
6554
6555 if let Err(err) = result {
6556 log::error!("failed to join channel: {}", err);
6557 if let Some(active_window) = active_window {
6558 active_window
6559 .update(cx, |_, window, cx| {
6560 let detail: SharedString = match err.error_code() {
6561 ErrorCode::SignedOut => {
6562 "Please sign in to continue.".into()
6563 }
6564 ErrorCode::UpgradeRequired => {
6565 "Your are running an unsupported version of Zed. Please update to continue.".into()
6566 }
6567 ErrorCode::NoSuchChannel => {
6568 "No matching channel was found. Please check the link and try again.".into()
6569 }
6570 ErrorCode::Forbidden => {
6571 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
6572 }
6573 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
6574 _ => format!("{}\n\nPlease try again.", err).into(),
6575 };
6576 window.prompt(
6577 PromptLevel::Critical,
6578 "Failed to join channel",
6579 Some(&detail),
6580 &["Ok"],
6581 cx)
6582 })?
6583 .await
6584 .ok();
6585 }
6586 }
6587
6588 // return ok, we showed the error to the user.
6589 anyhow::Ok(())
6590 })
6591}
6592
6593pub async fn get_any_active_workspace(
6594 app_state: Arc<AppState>,
6595 mut cx: AsyncApp,
6596) -> anyhow::Result<WindowHandle<Workspace>> {
6597 // find an existing workspace to focus and show call controls
6598 let active_window = activate_any_workspace_window(&mut cx);
6599 if active_window.is_none() {
6600 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
6601 .await?;
6602 }
6603 activate_any_workspace_window(&mut cx).context("could not open zed")
6604}
6605
6606fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
6607 cx.update(|cx| {
6608 if let Some(workspace_window) = cx
6609 .active_window()
6610 .and_then(|window| window.downcast::<Workspace>())
6611 {
6612 return Some(workspace_window);
6613 }
6614
6615 for window in cx.windows() {
6616 if let Some(workspace_window) = window.downcast::<Workspace>() {
6617 workspace_window
6618 .update(cx, |_, window, _| window.activate_window())
6619 .ok();
6620 return Some(workspace_window);
6621 }
6622 }
6623 None
6624 })
6625 .ok()
6626 .flatten()
6627}
6628
6629pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
6630 cx.windows()
6631 .into_iter()
6632 .filter_map(|window| window.downcast::<Workspace>())
6633 .filter(|workspace| {
6634 workspace
6635 .read(cx)
6636 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
6637 })
6638 .collect()
6639}
6640
6641#[derive(Default)]
6642pub struct OpenOptions {
6643 pub visible: Option<OpenVisible>,
6644 pub focus: Option<bool>,
6645 pub open_new_workspace: Option<bool>,
6646 pub replace_window: Option<WindowHandle<Workspace>>,
6647 pub env: Option<HashMap<String, String>>,
6648}
6649
6650#[allow(clippy::type_complexity)]
6651pub fn open_paths(
6652 abs_paths: &[PathBuf],
6653 app_state: Arc<AppState>,
6654 open_options: OpenOptions,
6655 cx: &mut App,
6656) -> Task<
6657 anyhow::Result<(
6658 WindowHandle<Workspace>,
6659 Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
6660 )>,
6661> {
6662 let abs_paths = abs_paths.to_vec();
6663 let mut existing = None;
6664 let mut best_match = None;
6665 let mut open_visible = OpenVisible::All;
6666
6667 cx.spawn(async move |cx| {
6668 if open_options.open_new_workspace != Some(true) {
6669 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
6670 let all_metadatas = futures::future::join_all(all_paths)
6671 .await
6672 .into_iter()
6673 .filter_map(|result| result.ok().flatten())
6674 .collect::<Vec<_>>();
6675
6676 cx.update(|cx| {
6677 for window in local_workspace_windows(&cx) {
6678 if let Ok(workspace) = window.read(&cx) {
6679 let m = workspace.project.read(&cx).visibility_for_paths(
6680 &abs_paths,
6681 &all_metadatas,
6682 open_options.open_new_workspace == None,
6683 cx,
6684 );
6685 if m > best_match {
6686 existing = Some(window);
6687 best_match = m;
6688 } else if best_match.is_none()
6689 && open_options.open_new_workspace == Some(false)
6690 {
6691 existing = Some(window)
6692 }
6693 }
6694 }
6695 })?;
6696
6697 if open_options.open_new_workspace.is_none() && existing.is_none() {
6698 if all_metadatas.iter().all(|file| !file.is_dir) {
6699 cx.update(|cx| {
6700 if let Some(window) = cx
6701 .active_window()
6702 .and_then(|window| window.downcast::<Workspace>())
6703 {
6704 if let Ok(workspace) = window.read(cx) {
6705 let project = workspace.project().read(cx);
6706 if project.is_local() && !project.is_via_collab() {
6707 existing = Some(window);
6708 open_visible = OpenVisible::None;
6709 return;
6710 }
6711 }
6712 }
6713 for window in local_workspace_windows(cx) {
6714 if let Ok(workspace) = window.read(cx) {
6715 let project = workspace.project().read(cx);
6716 if project.is_via_collab() {
6717 continue;
6718 }
6719 existing = Some(window);
6720 open_visible = OpenVisible::None;
6721 break;
6722 }
6723 }
6724 })?;
6725 }
6726 }
6727 }
6728
6729 if let Some(existing) = existing {
6730 let open_task = existing
6731 .update(cx, |workspace, window, cx| {
6732 window.activate_window();
6733 workspace.open_paths(
6734 abs_paths,
6735 OpenOptions {
6736 visible: Some(open_visible),
6737 ..Default::default()
6738 },
6739 None,
6740 window,
6741 cx,
6742 )
6743 })?
6744 .await;
6745
6746 _ = existing.update(cx, |workspace, _, cx| {
6747 for item in open_task.iter().flatten() {
6748 if let Err(e) = item {
6749 workspace.show_error(&e, cx);
6750 }
6751 }
6752 });
6753
6754 Ok((existing, open_task))
6755 } else {
6756 cx.update(move |cx| {
6757 Workspace::new_local(
6758 abs_paths,
6759 app_state.clone(),
6760 open_options.replace_window,
6761 open_options.env,
6762 cx,
6763 )
6764 })?
6765 .await
6766 }
6767 })
6768}
6769
6770pub fn open_new(
6771 open_options: OpenOptions,
6772 app_state: Arc<AppState>,
6773 cx: &mut App,
6774 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6775) -> Task<anyhow::Result<()>> {
6776 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6777 cx.spawn(async move |cx| {
6778 let (workspace, opened_paths) = task.await?;
6779 workspace.update(cx, |workspace, window, cx| {
6780 if opened_paths.is_empty() {
6781 init(workspace, window, cx)
6782 }
6783 })?;
6784 Ok(())
6785 })
6786}
6787
6788pub fn create_and_open_local_file(
6789 path: &'static Path,
6790 window: &mut Window,
6791 cx: &mut Context<Workspace>,
6792 default_content: impl 'static + Send + FnOnce() -> Rope,
6793) -> Task<Result<Box<dyn ItemHandle>>> {
6794 cx.spawn_in(window, async move |workspace, cx| {
6795 let fs = workspace.update(cx, |workspace, _| workspace.app_state().fs.clone())?;
6796 if !fs.is_file(path).await {
6797 fs.create_file(path, Default::default()).await?;
6798 fs.save(path, &default_content(), Default::default())
6799 .await?;
6800 }
6801
6802 let mut items = workspace
6803 .update_in(cx, |workspace, window, cx| {
6804 workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6805 workspace.open_paths(
6806 vec![path.to_path_buf()],
6807 OpenOptions {
6808 visible: Some(OpenVisible::None),
6809 ..Default::default()
6810 },
6811 None,
6812 window,
6813 cx,
6814 )
6815 })
6816 })?
6817 .await?
6818 .await;
6819
6820 let item = items.pop().flatten();
6821 item.with_context(|| format!("path {path:?} is not a file"))?
6822 })
6823}
6824
6825pub fn open_ssh_project_with_new_connection(
6826 window: WindowHandle<Workspace>,
6827 connection_options: SshConnectionOptions,
6828 cancel_rx: oneshot::Receiver<()>,
6829 delegate: Arc<dyn SshClientDelegate>,
6830 app_state: Arc<AppState>,
6831 paths: Vec<PathBuf>,
6832 cx: &mut App,
6833) -> Task<Result<()>> {
6834 cx.spawn(async move |cx| {
6835 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6836 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6837
6838 let session = match cx
6839 .update(|cx| {
6840 remote::SshRemoteClient::new(
6841 ConnectionIdentifier::Workspace(workspace_id.0),
6842 connection_options,
6843 cancel_rx,
6844 delegate,
6845 cx,
6846 )
6847 })?
6848 .await?
6849 {
6850 Some(result) => result,
6851 None => return Ok(()),
6852 };
6853
6854 let project = cx.update(|cx| {
6855 project::Project::ssh(
6856 session,
6857 app_state.client.clone(),
6858 app_state.node_runtime.clone(),
6859 app_state.user_store.clone(),
6860 app_state.languages.clone(),
6861 app_state.fs.clone(),
6862 cx,
6863 )
6864 })?;
6865
6866 open_ssh_project_inner(
6867 project,
6868 paths,
6869 serialized_ssh_project,
6870 workspace_id,
6871 serialized_workspace,
6872 app_state,
6873 window,
6874 cx,
6875 )
6876 .await
6877 })
6878}
6879
6880pub fn open_ssh_project_with_existing_connection(
6881 connection_options: SshConnectionOptions,
6882 project: Entity<Project>,
6883 paths: Vec<PathBuf>,
6884 app_state: Arc<AppState>,
6885 window: WindowHandle<Workspace>,
6886 cx: &mut AsyncApp,
6887) -> Task<Result<()>> {
6888 cx.spawn(async move |cx| {
6889 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6890 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6891
6892 open_ssh_project_inner(
6893 project,
6894 paths,
6895 serialized_ssh_project,
6896 workspace_id,
6897 serialized_workspace,
6898 app_state,
6899 window,
6900 cx,
6901 )
6902 .await
6903 })
6904}
6905
6906async fn open_ssh_project_inner(
6907 project: Entity<Project>,
6908 paths: Vec<PathBuf>,
6909 serialized_ssh_project: SerializedSshProject,
6910 workspace_id: WorkspaceId,
6911 serialized_workspace: Option<SerializedWorkspace>,
6912 app_state: Arc<AppState>,
6913 window: WindowHandle<Workspace>,
6914 cx: &mut AsyncApp,
6915) -> Result<()> {
6916 let toolchains = DB.toolchains(workspace_id).await?;
6917 for (toolchain, worktree_id, path) in toolchains {
6918 project
6919 .update(cx, |this, cx| {
6920 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
6921 })?
6922 .await;
6923 }
6924 let mut project_paths_to_open = vec![];
6925 let mut project_path_errors = vec![];
6926
6927 for path in paths {
6928 let result = cx
6929 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6930 .await;
6931 match result {
6932 Ok((_, project_path)) => {
6933 project_paths_to_open.push((path.clone(), Some(project_path)));
6934 }
6935 Err(error) => {
6936 project_path_errors.push(error);
6937 }
6938 };
6939 }
6940
6941 if project_paths_to_open.is_empty() {
6942 return Err(project_path_errors.pop().context("no paths given")?);
6943 }
6944
6945 cx.update_window(window.into(), |_, window, cx| {
6946 window.replace_root(cx, |window, cx| {
6947 telemetry::event!("SSH Project Opened");
6948
6949 let mut workspace =
6950 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6951 workspace.set_serialized_ssh_project(serialized_ssh_project);
6952 workspace.update_history(cx);
6953 workspace
6954 });
6955 })?;
6956
6957 window
6958 .update(cx, |_, window, cx| {
6959 window.activate_window();
6960 open_items(serialized_workspace, project_paths_to_open, window, cx)
6961 })?
6962 .await?;
6963
6964 window.update(cx, |workspace, _, cx| {
6965 for error in project_path_errors {
6966 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6967 if let Some(path) = error.error_tag("path") {
6968 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6969 }
6970 } else {
6971 workspace.show_error(&error, cx)
6972 }
6973 }
6974 })?;
6975
6976 Ok(())
6977}
6978
6979fn serialize_ssh_project(
6980 connection_options: SshConnectionOptions,
6981 paths: Vec<PathBuf>,
6982 cx: &AsyncApp,
6983) -> Task<
6984 Result<(
6985 SerializedSshProject,
6986 WorkspaceId,
6987 Option<SerializedWorkspace>,
6988 )>,
6989> {
6990 cx.background_spawn(async move {
6991 let serialized_ssh_project = persistence::DB
6992 .get_or_create_ssh_project(
6993 connection_options.host.clone(),
6994 connection_options.port,
6995 paths
6996 .iter()
6997 .map(|path| path.to_string_lossy().to_string())
6998 .collect::<Vec<_>>(),
6999 connection_options.username.clone(),
7000 )
7001 .await?;
7002
7003 let serialized_workspace =
7004 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
7005
7006 let workspace_id = if let Some(workspace_id) =
7007 serialized_workspace.as_ref().map(|workspace| workspace.id)
7008 {
7009 workspace_id
7010 } else {
7011 persistence::DB.next_id().await?
7012 };
7013
7014 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
7015 })
7016}
7017
7018pub fn join_in_room_project(
7019 project_id: u64,
7020 follow_user_id: u64,
7021 app_state: Arc<AppState>,
7022 cx: &mut App,
7023) -> Task<Result<()>> {
7024 let windows = cx.windows();
7025 cx.spawn(async move |cx| {
7026 let existing_workspace = windows.into_iter().find_map(|window_handle| {
7027 window_handle
7028 .downcast::<Workspace>()
7029 .and_then(|window_handle| {
7030 window_handle
7031 .update(cx, |workspace, _window, cx| {
7032 if workspace.project().read(cx).remote_id() == Some(project_id) {
7033 Some(window_handle)
7034 } else {
7035 None
7036 }
7037 })
7038 .unwrap_or(None)
7039 })
7040 });
7041
7042 let workspace = if let Some(existing_workspace) = existing_workspace {
7043 existing_workspace
7044 } else {
7045 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
7046 let room = active_call
7047 .read_with(cx, |call, _| call.room().cloned())?
7048 .context("not in a call")?;
7049 let project = room
7050 .update(cx, |room, cx| {
7051 room.join_project(
7052 project_id,
7053 app_state.languages.clone(),
7054 app_state.fs.clone(),
7055 cx,
7056 )
7057 })?
7058 .await?;
7059
7060 let window_bounds_override = window_bounds_env_override();
7061 cx.update(|cx| {
7062 let mut options = (app_state.build_window_options)(None, cx);
7063 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
7064 cx.open_window(options, |window, cx| {
7065 cx.new(|cx| {
7066 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
7067 })
7068 })
7069 })??
7070 };
7071
7072 workspace.update(cx, |workspace, window, cx| {
7073 cx.activate(true);
7074 window.activate_window();
7075
7076 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
7077 let follow_peer_id = room
7078 .read(cx)
7079 .remote_participants()
7080 .iter()
7081 .find(|(_, participant)| participant.user.id == follow_user_id)
7082 .map(|(_, p)| p.peer_id)
7083 .or_else(|| {
7084 // If we couldn't follow the given user, follow the host instead.
7085 let collaborator = workspace
7086 .project()
7087 .read(cx)
7088 .collaborators()
7089 .values()
7090 .find(|collaborator| collaborator.is_host)?;
7091 Some(collaborator.peer_id)
7092 });
7093
7094 if let Some(follow_peer_id) = follow_peer_id {
7095 workspace.follow(follow_peer_id, window, cx);
7096 }
7097 }
7098 })?;
7099
7100 anyhow::Ok(())
7101 })
7102}
7103
7104pub fn reload(reload: &Reload, cx: &mut App) {
7105 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
7106 let mut workspace_windows = cx
7107 .windows()
7108 .into_iter()
7109 .filter_map(|window| window.downcast::<Workspace>())
7110 .collect::<Vec<_>>();
7111
7112 // If multiple windows have unsaved changes, and need a save prompt,
7113 // prompt in the active window before switching to a different window.
7114 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
7115
7116 let mut prompt = None;
7117 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
7118 prompt = window
7119 .update(cx, |_, window, cx| {
7120 window.prompt(
7121 PromptLevel::Info,
7122 "Are you sure you want to restart?",
7123 None,
7124 &["Restart", "Cancel"],
7125 cx,
7126 )
7127 })
7128 .ok();
7129 }
7130
7131 let binary_path = reload.binary_path.clone();
7132 cx.spawn(async move |cx| {
7133 if let Some(prompt) = prompt {
7134 let answer = prompt.await?;
7135 if answer != 0 {
7136 return Ok(());
7137 }
7138 }
7139
7140 // If the user cancels any save prompt, then keep the app open.
7141 for window in workspace_windows {
7142 if let Ok(should_close) = window.update(cx, |workspace, window, cx| {
7143 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
7144 }) {
7145 if !should_close.await? {
7146 return Ok(());
7147 }
7148 }
7149 }
7150
7151 cx.update(|cx| cx.restart(binary_path))
7152 })
7153 .detach_and_log_err(cx);
7154}
7155
7156fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
7157 let mut parts = value.split(',');
7158 let x: usize = parts.next()?.parse().ok()?;
7159 let y: usize = parts.next()?.parse().ok()?;
7160 Some(point(px(x as f32), px(y as f32)))
7161}
7162
7163fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
7164 let mut parts = value.split(',');
7165 let width: usize = parts.next()?.parse().ok()?;
7166 let height: usize = parts.next()?.parse().ok()?;
7167 Some(size(px(width as f32), px(height as f32)))
7168}
7169
7170pub fn client_side_decorations(
7171 element: impl IntoElement,
7172 window: &mut Window,
7173 cx: &mut App,
7174) -> Stateful<Div> {
7175 const BORDER_SIZE: Pixels = px(1.0);
7176 let decorations = window.window_decorations();
7177
7178 if matches!(decorations, Decorations::Client { .. }) {
7179 window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
7180 }
7181
7182 struct GlobalResizeEdge(ResizeEdge);
7183 impl Global for GlobalResizeEdge {}
7184
7185 div()
7186 .id("window-backdrop")
7187 .bg(transparent_black())
7188 .map(|div| match decorations {
7189 Decorations::Server => div,
7190 Decorations::Client { tiling, .. } => div
7191 .when(!(tiling.top || tiling.right), |div| {
7192 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7193 })
7194 .when(!(tiling.top || tiling.left), |div| {
7195 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7196 })
7197 .when(!(tiling.bottom || tiling.right), |div| {
7198 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7199 })
7200 .when(!(tiling.bottom || tiling.left), |div| {
7201 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7202 })
7203 .when(!tiling.top, |div| {
7204 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
7205 })
7206 .when(!tiling.bottom, |div| {
7207 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
7208 })
7209 .when(!tiling.left, |div| {
7210 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
7211 })
7212 .when(!tiling.right, |div| {
7213 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
7214 })
7215 .on_mouse_move(move |e, window, cx| {
7216 let size = window.window_bounds().get_bounds().size;
7217 let pos = e.position;
7218
7219 let new_edge =
7220 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
7221
7222 let edge = cx.try_global::<GlobalResizeEdge>();
7223 if new_edge != edge.map(|edge| edge.0) {
7224 window
7225 .window_handle()
7226 .update(cx, |workspace, _, cx| {
7227 cx.notify(workspace.entity_id());
7228 })
7229 .ok();
7230 }
7231 })
7232 .on_mouse_down(MouseButton::Left, move |e, window, _| {
7233 let size = window.window_bounds().get_bounds().size;
7234 let pos = e.position;
7235
7236 let edge = match resize_edge(
7237 pos,
7238 theme::CLIENT_SIDE_DECORATION_SHADOW,
7239 size,
7240 tiling,
7241 ) {
7242 Some(value) => value,
7243 None => return,
7244 };
7245
7246 window.start_window_resize(edge);
7247 }),
7248 })
7249 .size_full()
7250 .child(
7251 div()
7252 .cursor(CursorStyle::Arrow)
7253 .map(|div| match decorations {
7254 Decorations::Server => div,
7255 Decorations::Client { tiling } => div
7256 .border_color(cx.theme().colors().border)
7257 .when(!(tiling.top || tiling.right), |div| {
7258 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7259 })
7260 .when(!(tiling.top || tiling.left), |div| {
7261 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7262 })
7263 .when(!(tiling.bottom || tiling.right), |div| {
7264 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7265 })
7266 .when(!(tiling.bottom || tiling.left), |div| {
7267 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7268 })
7269 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
7270 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
7271 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
7272 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
7273 .when(!tiling.is_tiled(), |div| {
7274 div.shadow(smallvec::smallvec![gpui::BoxShadow {
7275 color: Hsla {
7276 h: 0.,
7277 s: 0.,
7278 l: 0.,
7279 a: 0.4,
7280 },
7281 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
7282 spread_radius: px(0.),
7283 offset: point(px(0.0), px(0.0)),
7284 }])
7285 }),
7286 })
7287 .on_mouse_move(|_e, _, cx| {
7288 cx.stop_propagation();
7289 })
7290 .size_full()
7291 .child(element),
7292 )
7293 .map(|div| match decorations {
7294 Decorations::Server => div,
7295 Decorations::Client { tiling, .. } => div.child(
7296 canvas(
7297 |_bounds, window, _| {
7298 window.insert_hitbox(
7299 Bounds::new(
7300 point(px(0.0), px(0.0)),
7301 window.window_bounds().get_bounds().size,
7302 ),
7303 false,
7304 )
7305 },
7306 move |_bounds, hitbox, window, cx| {
7307 let mouse = window.mouse_position();
7308 let size = window.window_bounds().get_bounds().size;
7309 let Some(edge) =
7310 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
7311 else {
7312 return;
7313 };
7314 cx.set_global(GlobalResizeEdge(edge));
7315 window.set_cursor_style(
7316 match edge {
7317 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
7318 ResizeEdge::Left | ResizeEdge::Right => {
7319 CursorStyle::ResizeLeftRight
7320 }
7321 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
7322 CursorStyle::ResizeUpLeftDownRight
7323 }
7324 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
7325 CursorStyle::ResizeUpRightDownLeft
7326 }
7327 },
7328 Some(&hitbox),
7329 );
7330 },
7331 )
7332 .size_full()
7333 .absolute(),
7334 ),
7335 })
7336}
7337
7338fn resize_edge(
7339 pos: Point<Pixels>,
7340 shadow_size: Pixels,
7341 window_size: Size<Pixels>,
7342 tiling: Tiling,
7343) -> Option<ResizeEdge> {
7344 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
7345 if bounds.contains(&pos) {
7346 return None;
7347 }
7348
7349 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
7350 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
7351 if !tiling.top && top_left_bounds.contains(&pos) {
7352 return Some(ResizeEdge::TopLeft);
7353 }
7354
7355 let top_right_bounds = Bounds::new(
7356 Point::new(window_size.width - corner_size.width, px(0.)),
7357 corner_size,
7358 );
7359 if !tiling.top && top_right_bounds.contains(&pos) {
7360 return Some(ResizeEdge::TopRight);
7361 }
7362
7363 let bottom_left_bounds = Bounds::new(
7364 Point::new(px(0.), window_size.height - corner_size.height),
7365 corner_size,
7366 );
7367 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
7368 return Some(ResizeEdge::BottomLeft);
7369 }
7370
7371 let bottom_right_bounds = Bounds::new(
7372 Point::new(
7373 window_size.width - corner_size.width,
7374 window_size.height - corner_size.height,
7375 ),
7376 corner_size,
7377 );
7378 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
7379 return Some(ResizeEdge::BottomRight);
7380 }
7381
7382 if !tiling.top && pos.y < shadow_size {
7383 Some(ResizeEdge::Top)
7384 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
7385 Some(ResizeEdge::Bottom)
7386 } else if !tiling.left && pos.x < shadow_size {
7387 Some(ResizeEdge::Left)
7388 } else if !tiling.right && pos.x > window_size.width - shadow_size {
7389 Some(ResizeEdge::Right)
7390 } else {
7391 None
7392 }
7393}
7394
7395fn join_pane_into_active(
7396 active_pane: &Entity<Pane>,
7397 pane: &Entity<Pane>,
7398 window: &mut Window,
7399 cx: &mut App,
7400) {
7401 if pane == active_pane {
7402 return;
7403 } else if pane.read(cx).items_len() == 0 {
7404 pane.update(cx, |_, cx| {
7405 cx.emit(pane::Event::Remove {
7406 focus_on_pane: None,
7407 });
7408 })
7409 } else {
7410 move_all_items(pane, active_pane, window, cx);
7411 }
7412}
7413
7414fn move_all_items(
7415 from_pane: &Entity<Pane>,
7416 to_pane: &Entity<Pane>,
7417 window: &mut Window,
7418 cx: &mut App,
7419) {
7420 let destination_is_different = from_pane != to_pane;
7421 let mut moved_items = 0;
7422 for (item_ix, item_handle) in from_pane
7423 .read(cx)
7424 .items()
7425 .enumerate()
7426 .map(|(ix, item)| (ix, item.clone()))
7427 .collect::<Vec<_>>()
7428 {
7429 let ix = item_ix - moved_items;
7430 if destination_is_different {
7431 // Close item from previous pane
7432 from_pane.update(cx, |source, cx| {
7433 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
7434 });
7435 moved_items += 1;
7436 }
7437
7438 // This automatically removes duplicate items in the pane
7439 to_pane.update(cx, |destination, cx| {
7440 destination.add_item(item_handle, true, true, None, window, cx);
7441 window.focus(&destination.focus_handle(cx))
7442 });
7443 }
7444}
7445
7446pub fn move_item(
7447 source: &Entity<Pane>,
7448 destination: &Entity<Pane>,
7449 item_id_to_move: EntityId,
7450 destination_index: usize,
7451 window: &mut Window,
7452 cx: &mut App,
7453) {
7454 let Some((item_ix, item_handle)) = source
7455 .read(cx)
7456 .items()
7457 .enumerate()
7458 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
7459 .map(|(ix, item)| (ix, item.clone()))
7460 else {
7461 // Tab was closed during drag
7462 return;
7463 };
7464
7465 if source != destination {
7466 // Close item from previous pane
7467 source.update(cx, |source, cx| {
7468 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
7469 });
7470 }
7471
7472 // This automatically removes duplicate items in the pane
7473 destination.update(cx, |destination, cx| {
7474 destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
7475 window.focus(&destination.focus_handle(cx))
7476 });
7477}
7478
7479pub fn move_active_item(
7480 source: &Entity<Pane>,
7481 destination: &Entity<Pane>,
7482 focus_destination: bool,
7483 close_if_empty: bool,
7484 window: &mut Window,
7485 cx: &mut App,
7486) {
7487 if source == destination {
7488 return;
7489 }
7490 let Some(active_item) = source.read(cx).active_item() else {
7491 return;
7492 };
7493 source.update(cx, |source_pane, cx| {
7494 let item_id = active_item.item_id();
7495 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
7496 destination.update(cx, |target_pane, cx| {
7497 target_pane.add_item(
7498 active_item,
7499 focus_destination,
7500 focus_destination,
7501 Some(target_pane.items_len()),
7502 window,
7503 cx,
7504 );
7505 });
7506 });
7507}
7508
7509#[derive(Debug)]
7510pub struct WorkspacePosition {
7511 pub window_bounds: Option<WindowBounds>,
7512 pub display: Option<Uuid>,
7513 pub centered_layout: bool,
7514}
7515
7516pub fn ssh_workspace_position_from_db(
7517 host: String,
7518 port: Option<u16>,
7519 user: Option<String>,
7520 paths_to_open: &[PathBuf],
7521 cx: &App,
7522) -> Task<Result<WorkspacePosition>> {
7523 let paths = paths_to_open
7524 .iter()
7525 .map(|path| path.to_string_lossy().to_string())
7526 .collect::<Vec<_>>();
7527
7528 cx.background_spawn(async move {
7529 let serialized_ssh_project = persistence::DB
7530 .get_or_create_ssh_project(host, port, paths, user)
7531 .await
7532 .context("fetching serialized ssh project")?;
7533 let serialized_workspace =
7534 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
7535
7536 let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
7537 (Some(WindowBounds::Windowed(bounds)), None)
7538 } else {
7539 let restorable_bounds = serialized_workspace
7540 .as_ref()
7541 .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
7542 .or_else(|| {
7543 let (display, window_bounds) = DB.last_window().log_err()?;
7544 Some((display?, window_bounds?))
7545 });
7546
7547 if let Some((serialized_display, serialized_status)) = restorable_bounds {
7548 (Some(serialized_status.0), Some(serialized_display))
7549 } else {
7550 (None, None)
7551 }
7552 };
7553
7554 let centered_layout = serialized_workspace
7555 .as_ref()
7556 .map(|w| w.centered_layout)
7557 .unwrap_or(false);
7558
7559 Ok(WorkspacePosition {
7560 window_bounds,
7561 display,
7562 centered_layout,
7563 })
7564 })
7565}
7566
7567#[cfg(test)]
7568mod tests {
7569 use std::{cell::RefCell, rc::Rc};
7570
7571 use super::*;
7572 use crate::{
7573 dock::{PanelEvent, test::TestPanel},
7574 item::{
7575 ItemEvent,
7576 test::{TestItem, TestProjectItem},
7577 },
7578 };
7579 use fs::FakeFs;
7580 use gpui::{
7581 DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
7582 UpdateGlobal, VisualTestContext, px,
7583 };
7584 use project::{Project, ProjectEntryId};
7585 use serde_json::json;
7586 use settings::SettingsStore;
7587
7588 #[gpui::test]
7589 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
7590 init_test(cx);
7591
7592 let fs = FakeFs::new(cx.executor());
7593 let project = Project::test(fs, [], cx).await;
7594 let (workspace, cx) =
7595 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7596
7597 // Adding an item with no ambiguity renders the tab without detail.
7598 let item1 = cx.new(|cx| {
7599 let mut item = TestItem::new(cx);
7600 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
7601 item
7602 });
7603 workspace.update_in(cx, |workspace, window, cx| {
7604 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7605 });
7606 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
7607
7608 // Adding an item that creates ambiguity increases the level of detail on
7609 // both tabs.
7610 let item2 = cx.new_window_entity(|_window, cx| {
7611 let mut item = TestItem::new(cx);
7612 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7613 item
7614 });
7615 workspace.update_in(cx, |workspace, window, cx| {
7616 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7617 });
7618 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7619 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7620
7621 // Adding an item that creates ambiguity increases the level of detail only
7622 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
7623 // we stop at the highest detail available.
7624 let item3 = cx.new(|cx| {
7625 let mut item = TestItem::new(cx);
7626 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7627 item
7628 });
7629 workspace.update_in(cx, |workspace, window, cx| {
7630 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7631 });
7632 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7633 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7634 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7635 }
7636
7637 #[gpui::test]
7638 async fn test_tracking_active_path(cx: &mut TestAppContext) {
7639 init_test(cx);
7640
7641 let fs = FakeFs::new(cx.executor());
7642 fs.insert_tree(
7643 "/root1",
7644 json!({
7645 "one.txt": "",
7646 "two.txt": "",
7647 }),
7648 )
7649 .await;
7650 fs.insert_tree(
7651 "/root2",
7652 json!({
7653 "three.txt": "",
7654 }),
7655 )
7656 .await;
7657
7658 let project = Project::test(fs, ["root1".as_ref()], cx).await;
7659 let (workspace, cx) =
7660 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7661 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7662 let worktree_id = project.update(cx, |project, cx| {
7663 project.worktrees(cx).next().unwrap().read(cx).id()
7664 });
7665
7666 let item1 = cx.new(|cx| {
7667 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
7668 });
7669 let item2 = cx.new(|cx| {
7670 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
7671 });
7672
7673 // Add an item to an empty pane
7674 workspace.update_in(cx, |workspace, window, cx| {
7675 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
7676 });
7677 project.update(cx, |project, cx| {
7678 assert_eq!(
7679 project.active_entry(),
7680 project
7681 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7682 .map(|e| e.id)
7683 );
7684 });
7685 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7686
7687 // Add a second item to a non-empty pane
7688 workspace.update_in(cx, |workspace, window, cx| {
7689 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
7690 });
7691 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
7692 project.update(cx, |project, cx| {
7693 assert_eq!(
7694 project.active_entry(),
7695 project
7696 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
7697 .map(|e| e.id)
7698 );
7699 });
7700
7701 // Close the active item
7702 pane.update_in(cx, |pane, window, cx| {
7703 pane.close_active_item(&Default::default(), window, cx)
7704 .unwrap()
7705 })
7706 .await
7707 .unwrap();
7708 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7709 project.update(cx, |project, cx| {
7710 assert_eq!(
7711 project.active_entry(),
7712 project
7713 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7714 .map(|e| e.id)
7715 );
7716 });
7717
7718 // Add a project folder
7719 project
7720 .update(cx, |project, cx| {
7721 project.find_or_create_worktree("root2", true, cx)
7722 })
7723 .await
7724 .unwrap();
7725 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
7726
7727 // Remove a project folder
7728 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
7729 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
7730 }
7731
7732 #[gpui::test]
7733 async fn test_close_window(cx: &mut TestAppContext) {
7734 init_test(cx);
7735
7736 let fs = FakeFs::new(cx.executor());
7737 fs.insert_tree("/root", json!({ "one": "" })).await;
7738
7739 let project = Project::test(fs, ["root".as_ref()], cx).await;
7740 let (workspace, cx) =
7741 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7742
7743 // When there are no dirty items, there's nothing to do.
7744 let item1 = cx.new(TestItem::new);
7745 workspace.update_in(cx, |w, window, cx| {
7746 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
7747 });
7748 let task = workspace.update_in(cx, |w, window, cx| {
7749 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7750 });
7751 assert!(task.await.unwrap());
7752
7753 // When there are dirty untitled items, prompt to save each one. If the user
7754 // cancels any prompt, then abort.
7755 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
7756 let item3 = cx.new(|cx| {
7757 TestItem::new(cx)
7758 .with_dirty(true)
7759 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7760 });
7761 workspace.update_in(cx, |w, window, cx| {
7762 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7763 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7764 });
7765 let task = workspace.update_in(cx, |w, window, cx| {
7766 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7767 });
7768 cx.executor().run_until_parked();
7769 cx.simulate_prompt_answer("Cancel"); // cancel save all
7770 cx.executor().run_until_parked();
7771 assert!(!cx.has_pending_prompt());
7772 assert!(!task.await.unwrap());
7773 }
7774
7775 #[gpui::test]
7776 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
7777 init_test(cx);
7778
7779 // Register TestItem as a serializable item
7780 cx.update(|cx| {
7781 register_serializable_item::<TestItem>(cx);
7782 });
7783
7784 let fs = FakeFs::new(cx.executor());
7785 fs.insert_tree("/root", json!({ "one": "" })).await;
7786
7787 let project = Project::test(fs, ["root".as_ref()], cx).await;
7788 let (workspace, cx) =
7789 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7790
7791 // When there are dirty untitled items, but they can serialize, then there is no prompt.
7792 let item1 = cx.new(|cx| {
7793 TestItem::new(cx)
7794 .with_dirty(true)
7795 .with_serialize(|| Some(Task::ready(Ok(()))))
7796 });
7797 let item2 = cx.new(|cx| {
7798 TestItem::new(cx)
7799 .with_dirty(true)
7800 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7801 .with_serialize(|| Some(Task::ready(Ok(()))))
7802 });
7803 workspace.update_in(cx, |w, window, cx| {
7804 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7805 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7806 });
7807 let task = workspace.update_in(cx, |w, window, cx| {
7808 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7809 });
7810 assert!(task.await.unwrap());
7811 }
7812
7813 #[gpui::test]
7814 async fn test_close_pane_items(cx: &mut TestAppContext) {
7815 init_test(cx);
7816
7817 let fs = FakeFs::new(cx.executor());
7818
7819 let project = Project::test(fs, None, cx).await;
7820 let (workspace, cx) =
7821 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7822
7823 let item1 = cx.new(|cx| {
7824 TestItem::new(cx)
7825 .with_dirty(true)
7826 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7827 });
7828 let item2 = cx.new(|cx| {
7829 TestItem::new(cx)
7830 .with_dirty(true)
7831 .with_conflict(true)
7832 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7833 });
7834 let item3 = cx.new(|cx| {
7835 TestItem::new(cx)
7836 .with_dirty(true)
7837 .with_conflict(true)
7838 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7839 });
7840 let item4 = cx.new(|cx| {
7841 TestItem::new(cx).with_dirty(true).with_project_items(&[{
7842 let project_item = TestProjectItem::new_untitled(cx);
7843 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7844 project_item
7845 }])
7846 });
7847 let pane = workspace.update_in(cx, |workspace, window, cx| {
7848 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7849 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7850 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7851 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7852 workspace.active_pane().clone()
7853 });
7854
7855 let close_items = pane.update_in(cx, |pane, window, cx| {
7856 pane.activate_item(1, true, true, window, cx);
7857 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7858 let item1_id = item1.item_id();
7859 let item3_id = item3.item_id();
7860 let item4_id = item4.item_id();
7861 pane.close_items(window, cx, SaveIntent::Close, move |id| {
7862 [item1_id, item3_id, item4_id].contains(&id)
7863 })
7864 });
7865 cx.executor().run_until_parked();
7866
7867 assert!(cx.has_pending_prompt());
7868 cx.simulate_prompt_answer("Save all");
7869
7870 cx.executor().run_until_parked();
7871
7872 // Item 1 is saved. There's a prompt to save item 3.
7873 pane.update(cx, |pane, cx| {
7874 assert_eq!(item1.read(cx).save_count, 1);
7875 assert_eq!(item1.read(cx).save_as_count, 0);
7876 assert_eq!(item1.read(cx).reload_count, 0);
7877 assert_eq!(pane.items_len(), 3);
7878 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7879 });
7880 assert!(cx.has_pending_prompt());
7881
7882 // Cancel saving item 3.
7883 cx.simulate_prompt_answer("Discard");
7884 cx.executor().run_until_parked();
7885
7886 // Item 3 is reloaded. There's a prompt to save item 4.
7887 pane.update(cx, |pane, cx| {
7888 assert_eq!(item3.read(cx).save_count, 0);
7889 assert_eq!(item3.read(cx).save_as_count, 0);
7890 assert_eq!(item3.read(cx).reload_count, 1);
7891 assert_eq!(pane.items_len(), 2);
7892 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7893 });
7894
7895 // There's a prompt for a path for item 4.
7896 cx.simulate_new_path_selection(|_| Some(Default::default()));
7897 close_items.await.unwrap();
7898
7899 // The requested items are closed.
7900 pane.update(cx, |pane, cx| {
7901 assert_eq!(item4.read(cx).save_count, 0);
7902 assert_eq!(item4.read(cx).save_as_count, 1);
7903 assert_eq!(item4.read(cx).reload_count, 0);
7904 assert_eq!(pane.items_len(), 1);
7905 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7906 });
7907 }
7908
7909 #[gpui::test]
7910 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7911 init_test(cx);
7912
7913 let fs = FakeFs::new(cx.executor());
7914 let project = Project::test(fs, [], cx).await;
7915 let (workspace, cx) =
7916 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7917
7918 // Create several workspace items with single project entries, and two
7919 // workspace items with multiple project entries.
7920 let single_entry_items = (0..=4)
7921 .map(|project_entry_id| {
7922 cx.new(|cx| {
7923 TestItem::new(cx)
7924 .with_dirty(true)
7925 .with_project_items(&[dirty_project_item(
7926 project_entry_id,
7927 &format!("{project_entry_id}.txt"),
7928 cx,
7929 )])
7930 })
7931 })
7932 .collect::<Vec<_>>();
7933 let item_2_3 = cx.new(|cx| {
7934 TestItem::new(cx)
7935 .with_dirty(true)
7936 .with_singleton(false)
7937 .with_project_items(&[
7938 single_entry_items[2].read(cx).project_items[0].clone(),
7939 single_entry_items[3].read(cx).project_items[0].clone(),
7940 ])
7941 });
7942 let item_3_4 = cx.new(|cx| {
7943 TestItem::new(cx)
7944 .with_dirty(true)
7945 .with_singleton(false)
7946 .with_project_items(&[
7947 single_entry_items[3].read(cx).project_items[0].clone(),
7948 single_entry_items[4].read(cx).project_items[0].clone(),
7949 ])
7950 });
7951
7952 // Create two panes that contain the following project entries:
7953 // left pane:
7954 // multi-entry items: (2, 3)
7955 // single-entry items: 0, 2, 3, 4
7956 // right pane:
7957 // single-entry items: 4, 1
7958 // multi-entry items: (3, 4)
7959 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
7960 let left_pane = workspace.active_pane().clone();
7961 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7962 workspace.add_item_to_active_pane(
7963 single_entry_items[0].boxed_clone(),
7964 None,
7965 true,
7966 window,
7967 cx,
7968 );
7969 workspace.add_item_to_active_pane(
7970 single_entry_items[2].boxed_clone(),
7971 None,
7972 true,
7973 window,
7974 cx,
7975 );
7976 workspace.add_item_to_active_pane(
7977 single_entry_items[3].boxed_clone(),
7978 None,
7979 true,
7980 window,
7981 cx,
7982 );
7983 workspace.add_item_to_active_pane(
7984 single_entry_items[4].boxed_clone(),
7985 None,
7986 true,
7987 window,
7988 cx,
7989 );
7990
7991 let right_pane = workspace
7992 .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7993 .unwrap();
7994
7995 right_pane.update(cx, |pane, cx| {
7996 pane.add_item(
7997 single_entry_items[1].boxed_clone(),
7998 true,
7999 true,
8000 None,
8001 window,
8002 cx,
8003 );
8004 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
8005 });
8006
8007 (left_pane, right_pane)
8008 });
8009
8010 cx.focus(&right_pane);
8011
8012 let mut close = right_pane.update_in(cx, |pane, window, cx| {
8013 pane.close_all_items(&CloseAllItems::default(), window, cx)
8014 .unwrap()
8015 });
8016 cx.executor().run_until_parked();
8017
8018 let msg = cx.pending_prompt().unwrap().0;
8019 assert!(msg.contains("1.txt"));
8020 assert!(!msg.contains("2.txt"));
8021 assert!(!msg.contains("3.txt"));
8022 assert!(!msg.contains("4.txt"));
8023
8024 cx.simulate_prompt_answer("Cancel");
8025 close.await.unwrap();
8026
8027 left_pane
8028 .update_in(cx, |left_pane, window, cx| {
8029 left_pane.close_item_by_id(
8030 single_entry_items[3].entity_id(),
8031 SaveIntent::Skip,
8032 window,
8033 cx,
8034 )
8035 })
8036 .await
8037 .unwrap();
8038
8039 close = right_pane.update_in(cx, |pane, window, cx| {
8040 pane.close_all_items(&CloseAllItems::default(), window, cx)
8041 .unwrap()
8042 });
8043 cx.executor().run_until_parked();
8044
8045 let details = cx.pending_prompt().unwrap().1;
8046 assert!(details.contains("1.txt"));
8047 assert!(!details.contains("2.txt"));
8048 assert!(details.contains("3.txt"));
8049 // ideally this assertion could be made, but today we can only
8050 // save whole items not project items, so the orphaned item 3 causes
8051 // 4 to be saved too.
8052 // assert!(!details.contains("4.txt"));
8053
8054 cx.simulate_prompt_answer("Save all");
8055
8056 cx.executor().run_until_parked();
8057 close.await.unwrap();
8058 right_pane.update(cx, |pane, _| {
8059 assert_eq!(pane.items_len(), 0);
8060 });
8061 }
8062
8063 #[gpui::test]
8064 async fn test_autosave(cx: &mut gpui::TestAppContext) {
8065 init_test(cx);
8066
8067 let fs = FakeFs::new(cx.executor());
8068 let project = Project::test(fs, [], cx).await;
8069 let (workspace, cx) =
8070 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8071 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8072
8073 let item = cx.new(|cx| {
8074 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
8075 });
8076 let item_id = item.entity_id();
8077 workspace.update_in(cx, |workspace, window, cx| {
8078 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8079 });
8080
8081 // Autosave on window change.
8082 item.update(cx, |item, cx| {
8083 SettingsStore::update_global(cx, |settings, cx| {
8084 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8085 settings.autosave = Some(AutosaveSetting::OnWindowChange);
8086 })
8087 });
8088 item.is_dirty = true;
8089 });
8090
8091 // Deactivating the window saves the file.
8092 cx.deactivate_window();
8093 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
8094
8095 // Re-activating the window doesn't save the file.
8096 cx.update(|window, _| window.activate_window());
8097 cx.executor().run_until_parked();
8098 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
8099
8100 // Autosave on focus change.
8101 item.update_in(cx, |item, window, cx| {
8102 cx.focus_self(window);
8103 SettingsStore::update_global(cx, |settings, cx| {
8104 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8105 settings.autosave = Some(AutosaveSetting::OnFocusChange);
8106 })
8107 });
8108 item.is_dirty = true;
8109 });
8110
8111 // Blurring the item saves the file.
8112 item.update_in(cx, |_, window, _| window.blur());
8113 cx.executor().run_until_parked();
8114 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
8115
8116 // Deactivating the window still saves the file.
8117 item.update_in(cx, |item, window, cx| {
8118 cx.focus_self(window);
8119 item.is_dirty = true;
8120 });
8121 cx.deactivate_window();
8122 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
8123
8124 // Autosave after delay.
8125 item.update(cx, |item, cx| {
8126 SettingsStore::update_global(cx, |settings, cx| {
8127 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8128 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
8129 })
8130 });
8131 item.is_dirty = true;
8132 cx.emit(ItemEvent::Edit);
8133 });
8134
8135 // Delay hasn't fully expired, so the file is still dirty and unsaved.
8136 cx.executor().advance_clock(Duration::from_millis(250));
8137 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
8138
8139 // After delay expires, the file is saved.
8140 cx.executor().advance_clock(Duration::from_millis(250));
8141 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
8142
8143 // Autosave on focus change, ensuring closing the tab counts as such.
8144 item.update(cx, |item, cx| {
8145 SettingsStore::update_global(cx, |settings, cx| {
8146 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8147 settings.autosave = Some(AutosaveSetting::OnFocusChange);
8148 })
8149 });
8150 item.is_dirty = true;
8151 for project_item in &mut item.project_items {
8152 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
8153 }
8154 });
8155
8156 pane.update_in(cx, |pane, window, cx| {
8157 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
8158 })
8159 .await
8160 .unwrap();
8161 assert!(!cx.has_pending_prompt());
8162 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
8163
8164 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
8165 workspace.update_in(cx, |workspace, window, cx| {
8166 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8167 });
8168 item.update_in(cx, |item, window, cx| {
8169 item.project_items[0].update(cx, |item, _| {
8170 item.entry_id = None;
8171 });
8172 item.is_dirty = true;
8173 window.blur();
8174 });
8175 cx.run_until_parked();
8176 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
8177
8178 // Ensure autosave is prevented for deleted files also when closing the buffer.
8179 let _close_items = pane.update_in(cx, |pane, window, cx| {
8180 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
8181 });
8182 cx.run_until_parked();
8183 assert!(cx.has_pending_prompt());
8184 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
8185 }
8186
8187 #[gpui::test]
8188 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
8189 init_test(cx);
8190
8191 let fs = FakeFs::new(cx.executor());
8192
8193 let project = Project::test(fs, [], cx).await;
8194 let (workspace, cx) =
8195 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8196
8197 let item = cx.new(|cx| {
8198 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
8199 });
8200 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8201 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
8202 let toolbar_notify_count = Rc::new(RefCell::new(0));
8203
8204 workspace.update_in(cx, |workspace, window, cx| {
8205 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8206 let toolbar_notification_count = toolbar_notify_count.clone();
8207 cx.observe_in(&toolbar, window, move |_, _, _, _| {
8208 *toolbar_notification_count.borrow_mut() += 1
8209 })
8210 .detach();
8211 });
8212
8213 pane.update(cx, |pane, _| {
8214 assert!(!pane.can_navigate_backward());
8215 assert!(!pane.can_navigate_forward());
8216 });
8217
8218 item.update_in(cx, |item, _, cx| {
8219 item.set_state("one".to_string(), cx);
8220 });
8221
8222 // Toolbar must be notified to re-render the navigation buttons
8223 assert_eq!(*toolbar_notify_count.borrow(), 1);
8224
8225 pane.update(cx, |pane, _| {
8226 assert!(pane.can_navigate_backward());
8227 assert!(!pane.can_navigate_forward());
8228 });
8229
8230 workspace
8231 .update_in(cx, |workspace, window, cx| {
8232 workspace.go_back(pane.downgrade(), window, cx)
8233 })
8234 .await
8235 .unwrap();
8236
8237 assert_eq!(*toolbar_notify_count.borrow(), 2);
8238 pane.update(cx, |pane, _| {
8239 assert!(!pane.can_navigate_backward());
8240 assert!(pane.can_navigate_forward());
8241 });
8242 }
8243
8244 #[gpui::test]
8245 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
8246 init_test(cx);
8247 let fs = FakeFs::new(cx.executor());
8248
8249 let project = Project::test(fs, [], cx).await;
8250 let (workspace, cx) =
8251 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8252
8253 let panel = workspace.update_in(cx, |workspace, window, cx| {
8254 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8255 workspace.add_panel(panel.clone(), window, cx);
8256
8257 workspace
8258 .right_dock()
8259 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8260
8261 panel
8262 });
8263
8264 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8265 pane.update_in(cx, |pane, window, cx| {
8266 let item = cx.new(TestItem::new);
8267 pane.add_item(Box::new(item), true, true, None, window, cx);
8268 });
8269
8270 // Transfer focus from center to panel
8271 workspace.update_in(cx, |workspace, window, cx| {
8272 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8273 });
8274
8275 workspace.update_in(cx, |workspace, window, cx| {
8276 assert!(workspace.right_dock().read(cx).is_open());
8277 assert!(!panel.is_zoomed(window, cx));
8278 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8279 });
8280
8281 // Transfer focus from panel to center
8282 workspace.update_in(cx, |workspace, window, cx| {
8283 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8284 });
8285
8286 workspace.update_in(cx, |workspace, window, cx| {
8287 assert!(workspace.right_dock().read(cx).is_open());
8288 assert!(!panel.is_zoomed(window, cx));
8289 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8290 });
8291
8292 // Close the dock
8293 workspace.update_in(cx, |workspace, window, cx| {
8294 workspace.toggle_dock(DockPosition::Right, window, cx);
8295 });
8296
8297 workspace.update_in(cx, |workspace, window, cx| {
8298 assert!(!workspace.right_dock().read(cx).is_open());
8299 assert!(!panel.is_zoomed(window, cx));
8300 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8301 });
8302
8303 // Open the dock
8304 workspace.update_in(cx, |workspace, window, cx| {
8305 workspace.toggle_dock(DockPosition::Right, window, cx);
8306 });
8307
8308 workspace.update_in(cx, |workspace, window, cx| {
8309 assert!(workspace.right_dock().read(cx).is_open());
8310 assert!(!panel.is_zoomed(window, cx));
8311 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8312 });
8313
8314 // Focus and zoom panel
8315 panel.update_in(cx, |panel, window, cx| {
8316 cx.focus_self(window);
8317 panel.set_zoomed(true, window, cx)
8318 });
8319
8320 workspace.update_in(cx, |workspace, window, cx| {
8321 assert!(workspace.right_dock().read(cx).is_open());
8322 assert!(panel.is_zoomed(window, cx));
8323 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8324 });
8325
8326 // Transfer focus to the center closes the dock
8327 workspace.update_in(cx, |workspace, window, cx| {
8328 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8329 });
8330
8331 workspace.update_in(cx, |workspace, window, cx| {
8332 assert!(!workspace.right_dock().read(cx).is_open());
8333 assert!(panel.is_zoomed(window, cx));
8334 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8335 });
8336
8337 // Transferring focus back to the panel keeps it zoomed
8338 workspace.update_in(cx, |workspace, window, cx| {
8339 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8340 });
8341
8342 workspace.update_in(cx, |workspace, window, cx| {
8343 assert!(workspace.right_dock().read(cx).is_open());
8344 assert!(panel.is_zoomed(window, cx));
8345 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8346 });
8347
8348 // Close the dock while it is zoomed
8349 workspace.update_in(cx, |workspace, window, cx| {
8350 workspace.toggle_dock(DockPosition::Right, window, cx)
8351 });
8352
8353 workspace.update_in(cx, |workspace, window, cx| {
8354 assert!(!workspace.right_dock().read(cx).is_open());
8355 assert!(panel.is_zoomed(window, cx));
8356 assert!(workspace.zoomed.is_none());
8357 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8358 });
8359
8360 // Opening the dock, when it's zoomed, retains focus
8361 workspace.update_in(cx, |workspace, window, cx| {
8362 workspace.toggle_dock(DockPosition::Right, window, cx)
8363 });
8364
8365 workspace.update_in(cx, |workspace, window, cx| {
8366 assert!(workspace.right_dock().read(cx).is_open());
8367 assert!(panel.is_zoomed(window, cx));
8368 assert!(workspace.zoomed.is_some());
8369 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8370 });
8371
8372 // Unzoom and close the panel, zoom the active pane.
8373 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
8374 workspace.update_in(cx, |workspace, window, cx| {
8375 workspace.toggle_dock(DockPosition::Right, window, cx)
8376 });
8377 pane.update_in(cx, |pane, window, cx| {
8378 pane.toggle_zoom(&Default::default(), window, cx)
8379 });
8380
8381 // Opening a dock unzooms the pane.
8382 workspace.update_in(cx, |workspace, window, cx| {
8383 workspace.toggle_dock(DockPosition::Right, window, cx)
8384 });
8385 workspace.update_in(cx, |workspace, window, cx| {
8386 let pane = pane.read(cx);
8387 assert!(!pane.is_zoomed());
8388 assert!(!pane.focus_handle(cx).is_focused(window));
8389 assert!(workspace.right_dock().read(cx).is_open());
8390 assert!(workspace.zoomed.is_none());
8391 });
8392 }
8393
8394 #[gpui::test]
8395 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
8396 init_test(cx);
8397
8398 let fs = FakeFs::new(cx.executor());
8399
8400 let project = Project::test(fs, None, cx).await;
8401 let (workspace, cx) =
8402 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8403
8404 // Let's arrange the panes like this:
8405 //
8406 // +-----------------------+
8407 // | top |
8408 // +------+--------+-------+
8409 // | left | center | right |
8410 // +------+--------+-------+
8411 // | bottom |
8412 // +-----------------------+
8413
8414 let top_item = cx.new(|cx| {
8415 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
8416 });
8417 let bottom_item = cx.new(|cx| {
8418 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
8419 });
8420 let left_item = cx.new(|cx| {
8421 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
8422 });
8423 let right_item = cx.new(|cx| {
8424 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
8425 });
8426 let center_item = cx.new(|cx| {
8427 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
8428 });
8429
8430 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8431 let top_pane_id = workspace.active_pane().entity_id();
8432 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
8433 workspace.split_pane(
8434 workspace.active_pane().clone(),
8435 SplitDirection::Down,
8436 window,
8437 cx,
8438 );
8439 top_pane_id
8440 });
8441 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8442 let bottom_pane_id = workspace.active_pane().entity_id();
8443 workspace.add_item_to_active_pane(
8444 Box::new(bottom_item.clone()),
8445 None,
8446 false,
8447 window,
8448 cx,
8449 );
8450 workspace.split_pane(
8451 workspace.active_pane().clone(),
8452 SplitDirection::Up,
8453 window,
8454 cx,
8455 );
8456 bottom_pane_id
8457 });
8458 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8459 let left_pane_id = workspace.active_pane().entity_id();
8460 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
8461 workspace.split_pane(
8462 workspace.active_pane().clone(),
8463 SplitDirection::Right,
8464 window,
8465 cx,
8466 );
8467 left_pane_id
8468 });
8469 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8470 let right_pane_id = workspace.active_pane().entity_id();
8471 workspace.add_item_to_active_pane(
8472 Box::new(right_item.clone()),
8473 None,
8474 false,
8475 window,
8476 cx,
8477 );
8478 workspace.split_pane(
8479 workspace.active_pane().clone(),
8480 SplitDirection::Left,
8481 window,
8482 cx,
8483 );
8484 right_pane_id
8485 });
8486 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8487 let center_pane_id = workspace.active_pane().entity_id();
8488 workspace.add_item_to_active_pane(
8489 Box::new(center_item.clone()),
8490 None,
8491 false,
8492 window,
8493 cx,
8494 );
8495 center_pane_id
8496 });
8497 cx.executor().run_until_parked();
8498
8499 workspace.update_in(cx, |workspace, window, cx| {
8500 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
8501
8502 // Join into next from center pane into right
8503 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8504 });
8505
8506 workspace.update_in(cx, |workspace, window, cx| {
8507 let active_pane = workspace.active_pane();
8508 assert_eq!(right_pane_id, active_pane.entity_id());
8509 assert_eq!(2, active_pane.read(cx).items_len());
8510 let item_ids_in_pane =
8511 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8512 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8513 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8514
8515 // Join into next from right pane into bottom
8516 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8517 });
8518
8519 workspace.update_in(cx, |workspace, window, cx| {
8520 let active_pane = workspace.active_pane();
8521 assert_eq!(bottom_pane_id, active_pane.entity_id());
8522 assert_eq!(3, active_pane.read(cx).items_len());
8523 let item_ids_in_pane =
8524 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8525 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8526 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8527 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8528
8529 // Join into next from bottom pane into left
8530 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8531 });
8532
8533 workspace.update_in(cx, |workspace, window, cx| {
8534 let active_pane = workspace.active_pane();
8535 assert_eq!(left_pane_id, active_pane.entity_id());
8536 assert_eq!(4, active_pane.read(cx).items_len());
8537 let item_ids_in_pane =
8538 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8539 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8540 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8541 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8542 assert!(item_ids_in_pane.contains(&left_item.item_id()));
8543
8544 // Join into next from left pane into top
8545 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8546 });
8547
8548 workspace.update_in(cx, |workspace, window, cx| {
8549 let active_pane = workspace.active_pane();
8550 assert_eq!(top_pane_id, active_pane.entity_id());
8551 assert_eq!(5, active_pane.read(cx).items_len());
8552 let item_ids_in_pane =
8553 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8554 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8555 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8556 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8557 assert!(item_ids_in_pane.contains(&left_item.item_id()));
8558 assert!(item_ids_in_pane.contains(&top_item.item_id()));
8559
8560 // Single pane left: no-op
8561 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
8562 });
8563
8564 workspace.update(cx, |workspace, _cx| {
8565 let active_pane = workspace.active_pane();
8566 assert_eq!(top_pane_id, active_pane.entity_id());
8567 });
8568 }
8569
8570 fn add_an_item_to_active_pane(
8571 cx: &mut VisualTestContext,
8572 workspace: &Entity<Workspace>,
8573 item_id: u64,
8574 ) -> Entity<TestItem> {
8575 let item = cx.new(|cx| {
8576 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
8577 item_id,
8578 "item{item_id}.txt",
8579 cx,
8580 )])
8581 });
8582 workspace.update_in(cx, |workspace, window, cx| {
8583 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
8584 });
8585 return item;
8586 }
8587
8588 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
8589 return workspace.update_in(cx, |workspace, window, cx| {
8590 let new_pane = workspace.split_pane(
8591 workspace.active_pane().clone(),
8592 SplitDirection::Right,
8593 window,
8594 cx,
8595 );
8596 new_pane
8597 });
8598 }
8599
8600 #[gpui::test]
8601 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
8602 init_test(cx);
8603 let fs = FakeFs::new(cx.executor());
8604 let project = Project::test(fs, None, cx).await;
8605 let (workspace, cx) =
8606 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8607
8608 add_an_item_to_active_pane(cx, &workspace, 1);
8609 split_pane(cx, &workspace);
8610 add_an_item_to_active_pane(cx, &workspace, 2);
8611 split_pane(cx, &workspace); // empty pane
8612 split_pane(cx, &workspace);
8613 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
8614
8615 cx.executor().run_until_parked();
8616
8617 workspace.update(cx, |workspace, cx| {
8618 let num_panes = workspace.panes().len();
8619 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8620 let active_item = workspace
8621 .active_pane()
8622 .read(cx)
8623 .active_item()
8624 .expect("item is in focus");
8625
8626 assert_eq!(num_panes, 4);
8627 assert_eq!(num_items_in_current_pane, 1);
8628 assert_eq!(active_item.item_id(), last_item.item_id());
8629 });
8630
8631 workspace.update_in(cx, |workspace, window, cx| {
8632 workspace.join_all_panes(window, cx);
8633 });
8634
8635 workspace.update(cx, |workspace, cx| {
8636 let num_panes = workspace.panes().len();
8637 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8638 let active_item = workspace
8639 .active_pane()
8640 .read(cx)
8641 .active_item()
8642 .expect("item is in focus");
8643
8644 assert_eq!(num_panes, 1);
8645 assert_eq!(num_items_in_current_pane, 3);
8646 assert_eq!(active_item.item_id(), last_item.item_id());
8647 });
8648 }
8649 struct TestModal(FocusHandle);
8650
8651 impl TestModal {
8652 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
8653 Self(cx.focus_handle())
8654 }
8655 }
8656
8657 impl EventEmitter<DismissEvent> for TestModal {}
8658
8659 impl Focusable for TestModal {
8660 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8661 self.0.clone()
8662 }
8663 }
8664
8665 impl ModalView for TestModal {}
8666
8667 impl Render for TestModal {
8668 fn render(
8669 &mut self,
8670 _window: &mut Window,
8671 _cx: &mut Context<TestModal>,
8672 ) -> impl IntoElement {
8673 div().track_focus(&self.0)
8674 }
8675 }
8676
8677 #[gpui::test]
8678 async fn test_panels(cx: &mut gpui::TestAppContext) {
8679 init_test(cx);
8680 let fs = FakeFs::new(cx.executor());
8681
8682 let project = Project::test(fs, [], cx).await;
8683 let (workspace, cx) =
8684 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8685
8686 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
8687 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
8688 workspace.add_panel(panel_1.clone(), window, cx);
8689 workspace.toggle_dock(DockPosition::Left, window, cx);
8690 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8691 workspace.add_panel(panel_2.clone(), window, cx);
8692 workspace.toggle_dock(DockPosition::Right, window, cx);
8693
8694 let left_dock = workspace.left_dock();
8695 assert_eq!(
8696 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8697 panel_1.panel_id()
8698 );
8699 assert_eq!(
8700 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8701 panel_1.size(window, cx)
8702 );
8703
8704 left_dock.update(cx, |left_dock, cx| {
8705 left_dock.resize_active_panel(Some(px(1337.)), window, cx)
8706 });
8707 assert_eq!(
8708 workspace
8709 .right_dock()
8710 .read(cx)
8711 .visible_panel()
8712 .unwrap()
8713 .panel_id(),
8714 panel_2.panel_id(),
8715 );
8716
8717 (panel_1, panel_2)
8718 });
8719
8720 // Move panel_1 to the right
8721 panel_1.update_in(cx, |panel_1, window, cx| {
8722 panel_1.set_position(DockPosition::Right, window, cx)
8723 });
8724
8725 workspace.update_in(cx, |workspace, window, cx| {
8726 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
8727 // Since it was the only panel on the left, the left dock should now be closed.
8728 assert!(!workspace.left_dock().read(cx).is_open());
8729 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
8730 let right_dock = workspace.right_dock();
8731 assert_eq!(
8732 right_dock.read(cx).visible_panel().unwrap().panel_id(),
8733 panel_1.panel_id()
8734 );
8735 assert_eq!(
8736 right_dock.read(cx).active_panel_size(window, cx).unwrap(),
8737 px(1337.)
8738 );
8739
8740 // Now we move panel_2 to the left
8741 panel_2.set_position(DockPosition::Left, window, cx);
8742 });
8743
8744 workspace.update(cx, |workspace, cx| {
8745 // Since panel_2 was not visible on the right, we don't open the left dock.
8746 assert!(!workspace.left_dock().read(cx).is_open());
8747 // And the right dock is unaffected in its displaying of panel_1
8748 assert!(workspace.right_dock().read(cx).is_open());
8749 assert_eq!(
8750 workspace
8751 .right_dock()
8752 .read(cx)
8753 .visible_panel()
8754 .unwrap()
8755 .panel_id(),
8756 panel_1.panel_id(),
8757 );
8758 });
8759
8760 // Move panel_1 back to the left
8761 panel_1.update_in(cx, |panel_1, window, cx| {
8762 panel_1.set_position(DockPosition::Left, window, cx)
8763 });
8764
8765 workspace.update_in(cx, |workspace, window, cx| {
8766 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
8767 let left_dock = workspace.left_dock();
8768 assert!(left_dock.read(cx).is_open());
8769 assert_eq!(
8770 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8771 panel_1.panel_id()
8772 );
8773 assert_eq!(
8774 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8775 px(1337.)
8776 );
8777 // And the right dock should be closed as it no longer has any panels.
8778 assert!(!workspace.right_dock().read(cx).is_open());
8779
8780 // Now we move panel_1 to the bottom
8781 panel_1.set_position(DockPosition::Bottom, window, cx);
8782 });
8783
8784 workspace.update_in(cx, |workspace, window, cx| {
8785 // Since panel_1 was visible on the left, we close the left dock.
8786 assert!(!workspace.left_dock().read(cx).is_open());
8787 // The bottom dock is sized based on the panel's default size,
8788 // since the panel orientation changed from vertical to horizontal.
8789 let bottom_dock = workspace.bottom_dock();
8790 assert_eq!(
8791 bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
8792 panel_1.size(window, cx),
8793 );
8794 // Close bottom dock and move panel_1 back to the left.
8795 bottom_dock.update(cx, |bottom_dock, cx| {
8796 bottom_dock.set_open(false, window, cx)
8797 });
8798 panel_1.set_position(DockPosition::Left, window, cx);
8799 });
8800
8801 // Emit activated event on panel 1
8802 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
8803
8804 // Now the left dock is open and panel_1 is active and focused.
8805 workspace.update_in(cx, |workspace, window, cx| {
8806 let left_dock = workspace.left_dock();
8807 assert!(left_dock.read(cx).is_open());
8808 assert_eq!(
8809 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8810 panel_1.panel_id(),
8811 );
8812 assert!(panel_1.focus_handle(cx).is_focused(window));
8813 });
8814
8815 // Emit closed event on panel 2, which is not active
8816 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8817
8818 // Wo don't close the left dock, because panel_2 wasn't the active panel
8819 workspace.update(cx, |workspace, cx| {
8820 let left_dock = workspace.left_dock();
8821 assert!(left_dock.read(cx).is_open());
8822 assert_eq!(
8823 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8824 panel_1.panel_id(),
8825 );
8826 });
8827
8828 // Emitting a ZoomIn event shows the panel as zoomed.
8829 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
8830 workspace.update(cx, |workspace, _| {
8831 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8832 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
8833 });
8834
8835 // Move panel to another dock while it is zoomed
8836 panel_1.update_in(cx, |panel, window, cx| {
8837 panel.set_position(DockPosition::Right, window, cx)
8838 });
8839 workspace.update(cx, |workspace, _| {
8840 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8841
8842 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8843 });
8844
8845 // This is a helper for getting a:
8846 // - valid focus on an element,
8847 // - that isn't a part of the panes and panels system of the Workspace,
8848 // - and doesn't trigger the 'on_focus_lost' API.
8849 let focus_other_view = {
8850 let workspace = workspace.clone();
8851 move |cx: &mut VisualTestContext| {
8852 workspace.update_in(cx, |workspace, window, cx| {
8853 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
8854 workspace.toggle_modal(window, cx, TestModal::new);
8855 workspace.toggle_modal(window, cx, TestModal::new);
8856 } else {
8857 workspace.toggle_modal(window, cx, TestModal::new);
8858 }
8859 })
8860 }
8861 };
8862
8863 // If focus is transferred to another view that's not a panel or another pane, we still show
8864 // the panel as zoomed.
8865 focus_other_view(cx);
8866 workspace.update(cx, |workspace, _| {
8867 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8868 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8869 });
8870
8871 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8872 workspace.update_in(cx, |_workspace, window, cx| {
8873 cx.focus_self(window);
8874 });
8875 workspace.update(cx, |workspace, _| {
8876 assert_eq!(workspace.zoomed, None);
8877 assert_eq!(workspace.zoomed_position, None);
8878 });
8879
8880 // If focus is transferred again to another view that's not a panel or a pane, we won't
8881 // show the panel as zoomed because it wasn't zoomed before.
8882 focus_other_view(cx);
8883 workspace.update(cx, |workspace, _| {
8884 assert_eq!(workspace.zoomed, None);
8885 assert_eq!(workspace.zoomed_position, None);
8886 });
8887
8888 // When the panel is activated, it is zoomed again.
8889 cx.dispatch_action(ToggleRightDock);
8890 workspace.update(cx, |workspace, _| {
8891 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8892 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8893 });
8894
8895 // Emitting a ZoomOut event unzooms the panel.
8896 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8897 workspace.update(cx, |workspace, _| {
8898 assert_eq!(workspace.zoomed, None);
8899 assert_eq!(workspace.zoomed_position, None);
8900 });
8901
8902 // Emit closed event on panel 1, which is active
8903 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8904
8905 // Now the left dock is closed, because panel_1 was the active panel
8906 workspace.update(cx, |workspace, cx| {
8907 let right_dock = workspace.right_dock();
8908 assert!(!right_dock.read(cx).is_open());
8909 });
8910 }
8911
8912 #[gpui::test]
8913 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8914 init_test(cx);
8915
8916 let fs = FakeFs::new(cx.background_executor.clone());
8917 let project = Project::test(fs, [], cx).await;
8918 let (workspace, cx) =
8919 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8920 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8921
8922 let dirty_regular_buffer = cx.new(|cx| {
8923 TestItem::new(cx)
8924 .with_dirty(true)
8925 .with_label("1.txt")
8926 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8927 });
8928 let dirty_regular_buffer_2 = cx.new(|cx| {
8929 TestItem::new(cx)
8930 .with_dirty(true)
8931 .with_label("2.txt")
8932 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8933 });
8934 let dirty_multi_buffer_with_both = cx.new(|cx| {
8935 TestItem::new(cx)
8936 .with_dirty(true)
8937 .with_singleton(false)
8938 .with_label("Fake Project Search")
8939 .with_project_items(&[
8940 dirty_regular_buffer.read(cx).project_items[0].clone(),
8941 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8942 ])
8943 });
8944 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8945 workspace.update_in(cx, |workspace, window, cx| {
8946 workspace.add_item(
8947 pane.clone(),
8948 Box::new(dirty_regular_buffer.clone()),
8949 None,
8950 false,
8951 false,
8952 window,
8953 cx,
8954 );
8955 workspace.add_item(
8956 pane.clone(),
8957 Box::new(dirty_regular_buffer_2.clone()),
8958 None,
8959 false,
8960 false,
8961 window,
8962 cx,
8963 );
8964 workspace.add_item(
8965 pane.clone(),
8966 Box::new(dirty_multi_buffer_with_both.clone()),
8967 None,
8968 false,
8969 false,
8970 window,
8971 cx,
8972 );
8973 });
8974
8975 pane.update_in(cx, |pane, window, cx| {
8976 pane.activate_item(2, true, true, window, cx);
8977 assert_eq!(
8978 pane.active_item().unwrap().item_id(),
8979 multi_buffer_with_both_files_id,
8980 "Should select the multi buffer in the pane"
8981 );
8982 });
8983 let close_all_but_multi_buffer_task = pane
8984 .update_in(cx, |pane, window, cx| {
8985 pane.close_inactive_items(
8986 &CloseInactiveItems {
8987 save_intent: Some(SaveIntent::Save),
8988 close_pinned: true,
8989 },
8990 window,
8991 cx,
8992 )
8993 })
8994 .expect("should have inactive files to close");
8995 cx.background_executor.run_until_parked();
8996 assert!(!cx.has_pending_prompt());
8997 close_all_but_multi_buffer_task
8998 .await
8999 .expect("Closing all buffers but the multi buffer failed");
9000 pane.update(cx, |pane, cx| {
9001 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
9002 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
9003 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
9004 assert_eq!(pane.items_len(), 1);
9005 assert_eq!(
9006 pane.active_item().unwrap().item_id(),
9007 multi_buffer_with_both_files_id,
9008 "Should have only the multi buffer left in the pane"
9009 );
9010 assert!(
9011 dirty_multi_buffer_with_both.read(cx).is_dirty,
9012 "The multi buffer containing the unsaved buffer should still be dirty"
9013 );
9014 });
9015
9016 dirty_regular_buffer.update(cx, |buffer, cx| {
9017 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
9018 });
9019
9020 let close_multi_buffer_task = pane
9021 .update_in(cx, |pane, window, cx| {
9022 pane.close_active_item(
9023 &CloseActiveItem {
9024 save_intent: Some(SaveIntent::Close),
9025 close_pinned: false,
9026 },
9027 window,
9028 cx,
9029 )
9030 })
9031 .expect("should have the multi buffer to close");
9032 cx.background_executor.run_until_parked();
9033 assert!(
9034 cx.has_pending_prompt(),
9035 "Dirty multi buffer should prompt a save dialog"
9036 );
9037 cx.simulate_prompt_answer("Save");
9038 cx.background_executor.run_until_parked();
9039 close_multi_buffer_task
9040 .await
9041 .expect("Closing the multi buffer failed");
9042 pane.update(cx, |pane, cx| {
9043 assert_eq!(
9044 dirty_multi_buffer_with_both.read(cx).save_count,
9045 1,
9046 "Multi buffer item should get be saved"
9047 );
9048 // Test impl does not save inner items, so we do not assert them
9049 assert_eq!(
9050 pane.items_len(),
9051 0,
9052 "No more items should be left in the pane"
9053 );
9054 assert!(pane.active_item().is_none());
9055 });
9056 }
9057
9058 #[gpui::test]
9059 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
9060 cx: &mut TestAppContext,
9061 ) {
9062 init_test(cx);
9063
9064 let fs = FakeFs::new(cx.background_executor.clone());
9065 let project = Project::test(fs, [], cx).await;
9066 let (workspace, cx) =
9067 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9068 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
9069
9070 let dirty_regular_buffer = cx.new(|cx| {
9071 TestItem::new(cx)
9072 .with_dirty(true)
9073 .with_label("1.txt")
9074 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
9075 });
9076 let dirty_regular_buffer_2 = cx.new(|cx| {
9077 TestItem::new(cx)
9078 .with_dirty(true)
9079 .with_label("2.txt")
9080 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9081 });
9082 let clear_regular_buffer = cx.new(|cx| {
9083 TestItem::new(cx)
9084 .with_label("3.txt")
9085 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
9086 });
9087
9088 let dirty_multi_buffer_with_both = cx.new(|cx| {
9089 TestItem::new(cx)
9090 .with_dirty(true)
9091 .with_singleton(false)
9092 .with_label("Fake Project Search")
9093 .with_project_items(&[
9094 dirty_regular_buffer.read(cx).project_items[0].clone(),
9095 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
9096 clear_regular_buffer.read(cx).project_items[0].clone(),
9097 ])
9098 });
9099 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
9100 workspace.update_in(cx, |workspace, window, cx| {
9101 workspace.add_item(
9102 pane.clone(),
9103 Box::new(dirty_regular_buffer.clone()),
9104 None,
9105 false,
9106 false,
9107 window,
9108 cx,
9109 );
9110 workspace.add_item(
9111 pane.clone(),
9112 Box::new(dirty_multi_buffer_with_both.clone()),
9113 None,
9114 false,
9115 false,
9116 window,
9117 cx,
9118 );
9119 });
9120
9121 pane.update_in(cx, |pane, window, cx| {
9122 pane.activate_item(1, true, true, window, cx);
9123 assert_eq!(
9124 pane.active_item().unwrap().item_id(),
9125 multi_buffer_with_both_files_id,
9126 "Should select the multi buffer in the pane"
9127 );
9128 });
9129 let _close_multi_buffer_task = pane
9130 .update_in(cx, |pane, window, cx| {
9131 pane.close_active_item(
9132 &CloseActiveItem {
9133 save_intent: None,
9134 close_pinned: false,
9135 },
9136 window,
9137 cx,
9138 )
9139 })
9140 .expect("should have active multi buffer to close");
9141 cx.background_executor.run_until_parked();
9142 assert!(
9143 cx.has_pending_prompt(),
9144 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
9145 );
9146 }
9147
9148 #[gpui::test]
9149 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
9150 cx: &mut TestAppContext,
9151 ) {
9152 init_test(cx);
9153
9154 let fs = FakeFs::new(cx.background_executor.clone());
9155 let project = Project::test(fs, [], cx).await;
9156 let (workspace, cx) =
9157 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9158 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
9159
9160 let dirty_regular_buffer = cx.new(|cx| {
9161 TestItem::new(cx)
9162 .with_dirty(true)
9163 .with_label("1.txt")
9164 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
9165 });
9166 let dirty_regular_buffer_2 = cx.new(|cx| {
9167 TestItem::new(cx)
9168 .with_dirty(true)
9169 .with_label("2.txt")
9170 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9171 });
9172 let clear_regular_buffer = cx.new(|cx| {
9173 TestItem::new(cx)
9174 .with_label("3.txt")
9175 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
9176 });
9177
9178 let dirty_multi_buffer = cx.new(|cx| {
9179 TestItem::new(cx)
9180 .with_dirty(true)
9181 .with_singleton(false)
9182 .with_label("Fake Project Search")
9183 .with_project_items(&[
9184 dirty_regular_buffer.read(cx).project_items[0].clone(),
9185 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
9186 clear_regular_buffer.read(cx).project_items[0].clone(),
9187 ])
9188 });
9189 workspace.update_in(cx, |workspace, window, cx| {
9190 workspace.add_item(
9191 pane.clone(),
9192 Box::new(dirty_regular_buffer.clone()),
9193 None,
9194 false,
9195 false,
9196 window,
9197 cx,
9198 );
9199 workspace.add_item(
9200 pane.clone(),
9201 Box::new(dirty_regular_buffer_2.clone()),
9202 None,
9203 false,
9204 false,
9205 window,
9206 cx,
9207 );
9208 workspace.add_item(
9209 pane.clone(),
9210 Box::new(dirty_multi_buffer.clone()),
9211 None,
9212 false,
9213 false,
9214 window,
9215 cx,
9216 );
9217 });
9218
9219 pane.update_in(cx, |pane, window, cx| {
9220 pane.activate_item(2, true, true, window, cx);
9221 assert_eq!(
9222 pane.active_item().unwrap().item_id(),
9223 dirty_multi_buffer.item_id(),
9224 "Should select the multi buffer in the pane"
9225 );
9226 });
9227 let close_multi_buffer_task = pane
9228 .update_in(cx, |pane, window, cx| {
9229 pane.close_active_item(
9230 &CloseActiveItem {
9231 save_intent: None,
9232 close_pinned: false,
9233 },
9234 window,
9235 cx,
9236 )
9237 })
9238 .expect("should have active multi buffer to close");
9239 cx.background_executor.run_until_parked();
9240 assert!(
9241 !cx.has_pending_prompt(),
9242 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
9243 );
9244 close_multi_buffer_task
9245 .await
9246 .expect("Closing multi buffer failed");
9247 pane.update(cx, |pane, cx| {
9248 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
9249 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
9250 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
9251 assert_eq!(
9252 pane.items()
9253 .map(|item| item.item_id())
9254 .sorted()
9255 .collect::<Vec<_>>(),
9256 vec![
9257 dirty_regular_buffer.item_id(),
9258 dirty_regular_buffer_2.item_id(),
9259 ],
9260 "Should have no multi buffer left in the pane"
9261 );
9262 assert!(dirty_regular_buffer.read(cx).is_dirty);
9263 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
9264 });
9265 }
9266
9267 #[gpui::test]
9268 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
9269 init_test(cx);
9270 let fs = FakeFs::new(cx.executor());
9271 let project = Project::test(fs, [], cx).await;
9272 let (workspace, cx) =
9273 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9274
9275 // Add a new panel to the right dock, opening the dock and setting the
9276 // focus to the new panel.
9277 let panel = workspace.update_in(cx, |workspace, window, cx| {
9278 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
9279 workspace.add_panel(panel.clone(), window, cx);
9280
9281 workspace
9282 .right_dock()
9283 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
9284
9285 workspace.toggle_panel_focus::<TestPanel>(window, cx);
9286
9287 panel
9288 });
9289
9290 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
9291 // panel to the next valid position which, in this case, is the left
9292 // dock.
9293 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9294 workspace.update(cx, |workspace, cx| {
9295 assert!(workspace.left_dock().read(cx).is_open());
9296 assert_eq!(panel.read(cx).position, DockPosition::Left);
9297 });
9298
9299 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
9300 // panel to the next valid position which, in this case, is the bottom
9301 // dock.
9302 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9303 workspace.update(cx, |workspace, cx| {
9304 assert!(workspace.bottom_dock().read(cx).is_open());
9305 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
9306 });
9307
9308 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
9309 // around moving the panel to its initial position, the right dock.
9310 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9311 workspace.update(cx, |workspace, cx| {
9312 assert!(workspace.right_dock().read(cx).is_open());
9313 assert_eq!(panel.read(cx).position, DockPosition::Right);
9314 });
9315
9316 // Remove focus from the panel, ensuring that, if the panel is not
9317 // focused, the `MoveFocusedPanelToNextPosition` action does not update
9318 // the panel's position, so the panel is still in the right dock.
9319 workspace.update_in(cx, |workspace, window, cx| {
9320 workspace.toggle_panel_focus::<TestPanel>(window, cx);
9321 });
9322
9323 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9324 workspace.update(cx, |workspace, cx| {
9325 assert!(workspace.right_dock().read(cx).is_open());
9326 assert_eq!(panel.read(cx).position, DockPosition::Right);
9327 });
9328 }
9329
9330 mod register_project_item_tests {
9331
9332 use super::*;
9333
9334 // View
9335 struct TestPngItemView {
9336 focus_handle: FocusHandle,
9337 }
9338 // Model
9339 struct TestPngItem {}
9340
9341 impl project::ProjectItem for TestPngItem {
9342 fn try_open(
9343 _project: &Entity<Project>,
9344 path: &ProjectPath,
9345 cx: &mut App,
9346 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
9347 if path.path.extension().unwrap() == "png" {
9348 Some(cx.spawn(async move |cx| cx.new(|_| TestPngItem {})))
9349 } else {
9350 None
9351 }
9352 }
9353
9354 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
9355 None
9356 }
9357
9358 fn project_path(&self, _: &App) -> Option<ProjectPath> {
9359 None
9360 }
9361
9362 fn is_dirty(&self) -> bool {
9363 false
9364 }
9365 }
9366
9367 impl Item for TestPngItemView {
9368 type Event = ();
9369 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
9370 "".into()
9371 }
9372 }
9373 impl EventEmitter<()> for TestPngItemView {}
9374 impl Focusable for TestPngItemView {
9375 fn focus_handle(&self, _cx: &App) -> FocusHandle {
9376 self.focus_handle.clone()
9377 }
9378 }
9379
9380 impl Render for TestPngItemView {
9381 fn render(
9382 &mut self,
9383 _window: &mut Window,
9384 _cx: &mut Context<Self>,
9385 ) -> impl IntoElement {
9386 Empty
9387 }
9388 }
9389
9390 impl ProjectItem for TestPngItemView {
9391 type Item = TestPngItem;
9392
9393 fn for_project_item(
9394 _project: Entity<Project>,
9395 _pane: Option<&Pane>,
9396 _item: Entity<Self::Item>,
9397 _: &mut Window,
9398 cx: &mut Context<Self>,
9399 ) -> Self
9400 where
9401 Self: Sized,
9402 {
9403 Self {
9404 focus_handle: cx.focus_handle(),
9405 }
9406 }
9407 }
9408
9409 // View
9410 struct TestIpynbItemView {
9411 focus_handle: FocusHandle,
9412 }
9413 // Model
9414 struct TestIpynbItem {}
9415
9416 impl project::ProjectItem for TestIpynbItem {
9417 fn try_open(
9418 _project: &Entity<Project>,
9419 path: &ProjectPath,
9420 cx: &mut App,
9421 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
9422 if path.path.extension().unwrap() == "ipynb" {
9423 Some(cx.spawn(async move |cx| cx.new(|_| TestIpynbItem {})))
9424 } else {
9425 None
9426 }
9427 }
9428
9429 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
9430 None
9431 }
9432
9433 fn project_path(&self, _: &App) -> Option<ProjectPath> {
9434 None
9435 }
9436
9437 fn is_dirty(&self) -> bool {
9438 false
9439 }
9440 }
9441
9442 impl Item for TestIpynbItemView {
9443 type Event = ();
9444 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
9445 "".into()
9446 }
9447 }
9448 impl EventEmitter<()> for TestIpynbItemView {}
9449 impl Focusable for TestIpynbItemView {
9450 fn focus_handle(&self, _cx: &App) -> FocusHandle {
9451 self.focus_handle.clone()
9452 }
9453 }
9454
9455 impl Render for TestIpynbItemView {
9456 fn render(
9457 &mut self,
9458 _window: &mut Window,
9459 _cx: &mut Context<Self>,
9460 ) -> impl IntoElement {
9461 Empty
9462 }
9463 }
9464
9465 impl ProjectItem for TestIpynbItemView {
9466 type Item = TestIpynbItem;
9467
9468 fn for_project_item(
9469 _project: Entity<Project>,
9470 _pane: Option<&Pane>,
9471 _item: Entity<Self::Item>,
9472 _: &mut Window,
9473 cx: &mut Context<Self>,
9474 ) -> Self
9475 where
9476 Self: Sized,
9477 {
9478 Self {
9479 focus_handle: cx.focus_handle(),
9480 }
9481 }
9482 }
9483
9484 struct TestAlternatePngItemView {
9485 focus_handle: FocusHandle,
9486 }
9487
9488 impl Item for TestAlternatePngItemView {
9489 type Event = ();
9490 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
9491 "".into()
9492 }
9493 }
9494
9495 impl EventEmitter<()> for TestAlternatePngItemView {}
9496 impl Focusable for TestAlternatePngItemView {
9497 fn focus_handle(&self, _cx: &App) -> FocusHandle {
9498 self.focus_handle.clone()
9499 }
9500 }
9501
9502 impl Render for TestAlternatePngItemView {
9503 fn render(
9504 &mut self,
9505 _window: &mut Window,
9506 _cx: &mut Context<Self>,
9507 ) -> impl IntoElement {
9508 Empty
9509 }
9510 }
9511
9512 impl ProjectItem for TestAlternatePngItemView {
9513 type Item = TestPngItem;
9514
9515 fn for_project_item(
9516 _project: Entity<Project>,
9517 _pane: Option<&Pane>,
9518 _item: Entity<Self::Item>,
9519 _: &mut Window,
9520 cx: &mut Context<Self>,
9521 ) -> Self
9522 where
9523 Self: Sized,
9524 {
9525 Self {
9526 focus_handle: cx.focus_handle(),
9527 }
9528 }
9529 }
9530
9531 #[gpui::test]
9532 async fn test_register_project_item(cx: &mut TestAppContext) {
9533 init_test(cx);
9534
9535 cx.update(|cx| {
9536 register_project_item::<TestPngItemView>(cx);
9537 register_project_item::<TestIpynbItemView>(cx);
9538 });
9539
9540 let fs = FakeFs::new(cx.executor());
9541 fs.insert_tree(
9542 "/root1",
9543 json!({
9544 "one.png": "BINARYDATAHERE",
9545 "two.ipynb": "{ totally a notebook }",
9546 "three.txt": "editing text, sure why not?"
9547 }),
9548 )
9549 .await;
9550
9551 let project = Project::test(fs, ["root1".as_ref()], cx).await;
9552 let (workspace, cx) =
9553 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9554
9555 let worktree_id = project.update(cx, |project, cx| {
9556 project.worktrees(cx).next().unwrap().read(cx).id()
9557 });
9558
9559 let handle = workspace
9560 .update_in(cx, |workspace, window, cx| {
9561 let project_path = (worktree_id, "one.png");
9562 workspace.open_path(project_path, None, true, window, cx)
9563 })
9564 .await
9565 .unwrap();
9566
9567 // Now we can check if the handle we got back errored or not
9568 assert_eq!(
9569 handle.to_any().entity_type(),
9570 TypeId::of::<TestPngItemView>()
9571 );
9572
9573 let handle = workspace
9574 .update_in(cx, |workspace, window, cx| {
9575 let project_path = (worktree_id, "two.ipynb");
9576 workspace.open_path(project_path, None, true, window, cx)
9577 })
9578 .await
9579 .unwrap();
9580
9581 assert_eq!(
9582 handle.to_any().entity_type(),
9583 TypeId::of::<TestIpynbItemView>()
9584 );
9585
9586 let handle = workspace
9587 .update_in(cx, |workspace, window, cx| {
9588 let project_path = (worktree_id, "three.txt");
9589 workspace.open_path(project_path, None, true, window, cx)
9590 })
9591 .await;
9592 assert!(handle.is_err());
9593 }
9594
9595 #[gpui::test]
9596 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
9597 init_test(cx);
9598
9599 cx.update(|cx| {
9600 register_project_item::<TestPngItemView>(cx);
9601 register_project_item::<TestAlternatePngItemView>(cx);
9602 });
9603
9604 let fs = FakeFs::new(cx.executor());
9605 fs.insert_tree(
9606 "/root1",
9607 json!({
9608 "one.png": "BINARYDATAHERE",
9609 "two.ipynb": "{ totally a notebook }",
9610 "three.txt": "editing text, sure why not?"
9611 }),
9612 )
9613 .await;
9614 let project = Project::test(fs, ["root1".as_ref()], cx).await;
9615 let (workspace, cx) =
9616 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9617 let worktree_id = project.update(cx, |project, cx| {
9618 project.worktrees(cx).next().unwrap().read(cx).id()
9619 });
9620
9621 let handle = workspace
9622 .update_in(cx, |workspace, window, cx| {
9623 let project_path = (worktree_id, "one.png");
9624 workspace.open_path(project_path, None, true, window, cx)
9625 })
9626 .await
9627 .unwrap();
9628
9629 // This _must_ be the second item registered
9630 assert_eq!(
9631 handle.to_any().entity_type(),
9632 TypeId::of::<TestAlternatePngItemView>()
9633 );
9634
9635 let handle = workspace
9636 .update_in(cx, |workspace, window, cx| {
9637 let project_path = (worktree_id, "three.txt");
9638 workspace.open_path(project_path, None, true, window, cx)
9639 })
9640 .await;
9641 assert!(handle.is_err());
9642 }
9643 }
9644
9645 pub fn init_test(cx: &mut TestAppContext) {
9646 cx.update(|cx| {
9647 let settings_store = SettingsStore::test(cx);
9648 cx.set_global(settings_store);
9649 theme::init(theme::LoadThemes::JustBase, cx);
9650 language::init(cx);
9651 crate::init_settings(cx);
9652 Project::init_settings(cx);
9653 });
9654 }
9655
9656 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
9657 let item = TestProjectItem::new(id, path, cx);
9658 item.update(cx, |item, _| {
9659 item.is_dirty = true;
9660 });
9661 item
9662 }
9663}