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