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