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