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