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