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