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