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