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