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 #[cfg(target_os = "windows")]
4427 fn shared_screen_for_peer(
4428 &self,
4429 _peer_id: PeerId,
4430 _pane: &Entity<Pane>,
4431 _window: &mut Window,
4432 _cx: &mut App,
4433 ) -> Option<Entity<SharedScreen>> {
4434 None
4435 }
4436
4437 #[cfg(not(target_os = "windows"))]
4438 fn shared_screen_for_peer(
4439 &self,
4440 peer_id: PeerId,
4441 pane: &Entity<Pane>,
4442 window: &mut Window,
4443 cx: &mut App,
4444 ) -> Option<Entity<SharedScreen>> {
4445 let call = self.active_call()?;
4446 let room = call.read(cx).room()?.clone();
4447 let participant = room.read(cx).remote_participant_for_peer_id(peer_id)?;
4448 let track = participant.video_tracks.values().next()?.clone();
4449 let user = participant.user.clone();
4450
4451 for item in pane.read(cx).items_of_type::<SharedScreen>() {
4452 if item.read(cx).peer_id == peer_id {
4453 return Some(item);
4454 }
4455 }
4456
4457 Some(cx.new(|cx| SharedScreen::new(track, peer_id, user.clone(), room.clone(), window, cx)))
4458 }
4459
4460 pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4461 if window.is_window_active() {
4462 self.update_active_view_for_followers(window, cx);
4463
4464 if let Some(database_id) = self.database_id {
4465 cx.background_spawn(persistence::DB.update_timestamp(database_id))
4466 .detach();
4467 }
4468 } else {
4469 for pane in &self.panes {
4470 pane.update(cx, |pane, cx| {
4471 if let Some(item) = pane.active_item() {
4472 item.workspace_deactivated(window, cx);
4473 }
4474 for item in pane.items() {
4475 if matches!(
4476 item.workspace_settings(cx).autosave,
4477 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
4478 ) {
4479 Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
4480 .detach_and_log_err(cx);
4481 }
4482 }
4483 });
4484 }
4485 }
4486 }
4487
4488 pub fn active_call(&self) -> Option<&Entity<ActiveCall>> {
4489 self.active_call.as_ref().map(|(call, _)| call)
4490 }
4491
4492 fn on_active_call_event(
4493 &mut self,
4494 _: &Entity<ActiveCall>,
4495 event: &call::room::Event,
4496 window: &mut Window,
4497 cx: &mut Context<Self>,
4498 ) {
4499 match event {
4500 call::room::Event::ParticipantLocationChanged { participant_id }
4501 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4502 self.leader_updated(*participant_id, window, cx);
4503 }
4504 _ => {}
4505 }
4506 }
4507
4508 pub fn database_id(&self) -> Option<WorkspaceId> {
4509 self.database_id
4510 }
4511
4512 pub fn session_id(&self) -> Option<String> {
4513 self.session_id.clone()
4514 }
4515
4516 fn local_paths(&self, cx: &App) -> Option<Vec<Arc<Path>>> {
4517 let project = self.project().read(cx);
4518
4519 if project.is_local() {
4520 Some(
4521 project
4522 .visible_worktrees(cx)
4523 .map(|worktree| worktree.read(cx).abs_path())
4524 .collect::<Vec<_>>(),
4525 )
4526 } else {
4527 None
4528 }
4529 }
4530
4531 fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
4532 match member {
4533 Member::Axis(PaneAxis { members, .. }) => {
4534 for child in members.iter() {
4535 self.remove_panes(child.clone(), window, cx)
4536 }
4537 }
4538 Member::Pane(pane) => {
4539 self.force_remove_pane(&pane, &None, window, cx);
4540 }
4541 }
4542 }
4543
4544 fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
4545 self.session_id.take();
4546 self.serialize_workspace_internal(window, cx)
4547 }
4548
4549 fn force_remove_pane(
4550 &mut self,
4551 pane: &Entity<Pane>,
4552 focus_on: &Option<Entity<Pane>>,
4553 window: &mut Window,
4554 cx: &mut Context<Workspace>,
4555 ) {
4556 self.panes.retain(|p| p != pane);
4557 if let Some(focus_on) = focus_on {
4558 focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4559 } else {
4560 if self.active_pane() == pane {
4561 self.panes
4562 .last()
4563 .unwrap()
4564 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4565 }
4566 }
4567 if self.last_active_center_pane == Some(pane.downgrade()) {
4568 self.last_active_center_pane = None;
4569 }
4570 cx.notify();
4571 }
4572
4573 fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4574 if self._schedule_serialize.is_none() {
4575 self._schedule_serialize = Some(cx.spawn_in(window, async move |this, cx| {
4576 cx.background_executor()
4577 .timer(Duration::from_millis(100))
4578 .await;
4579 this.update_in(cx, |this, window, cx| {
4580 this.serialize_workspace_internal(window, cx).detach();
4581 this._schedule_serialize.take();
4582 })
4583 .log_err();
4584 }));
4585 }
4586 }
4587
4588 fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
4589 let Some(database_id) = self.database_id() else {
4590 return Task::ready(());
4591 };
4592
4593 fn serialize_pane_handle(
4594 pane_handle: &Entity<Pane>,
4595 window: &mut Window,
4596 cx: &mut App,
4597 ) -> SerializedPane {
4598 let (items, active, pinned_count) = {
4599 let pane = pane_handle.read(cx);
4600 let active_item_id = pane.active_item().map(|item| item.item_id());
4601 (
4602 pane.items()
4603 .filter_map(|handle| {
4604 let handle = handle.to_serializable_item_handle(cx)?;
4605
4606 Some(SerializedItem {
4607 kind: Arc::from(handle.serialized_item_kind()),
4608 item_id: handle.item_id().as_u64(),
4609 active: Some(handle.item_id()) == active_item_id,
4610 preview: pane.is_active_preview_item(handle.item_id()),
4611 })
4612 })
4613 .collect::<Vec<_>>(),
4614 pane.has_focus(window, cx),
4615 pane.pinned_count(),
4616 )
4617 };
4618
4619 SerializedPane::new(items, active, pinned_count)
4620 }
4621
4622 fn build_serialized_pane_group(
4623 pane_group: &Member,
4624 window: &mut Window,
4625 cx: &mut App,
4626 ) -> SerializedPaneGroup {
4627 match pane_group {
4628 Member::Axis(PaneAxis {
4629 axis,
4630 members,
4631 flexes,
4632 bounding_boxes: _,
4633 }) => SerializedPaneGroup::Group {
4634 axis: SerializedAxis(*axis),
4635 children: members
4636 .iter()
4637 .map(|member| build_serialized_pane_group(member, window, cx))
4638 .collect::<Vec<_>>(),
4639 flexes: Some(flexes.lock().clone()),
4640 },
4641 Member::Pane(pane_handle) => {
4642 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
4643 }
4644 }
4645 }
4646
4647 fn build_serialized_docks(
4648 this: &Workspace,
4649 window: &mut Window,
4650 cx: &mut App,
4651 ) -> DockStructure {
4652 let left_dock = this.left_dock.read(cx);
4653 let left_visible = left_dock.is_open();
4654 let left_active_panel = left_dock
4655 .active_panel()
4656 .map(|panel| panel.persistent_name().to_string());
4657 let left_dock_zoom = left_dock
4658 .active_panel()
4659 .map(|panel| panel.is_zoomed(window, cx))
4660 .unwrap_or(false);
4661
4662 let right_dock = this.right_dock.read(cx);
4663 let right_visible = right_dock.is_open();
4664 let right_active_panel = right_dock
4665 .active_panel()
4666 .map(|panel| panel.persistent_name().to_string());
4667 let right_dock_zoom = right_dock
4668 .active_panel()
4669 .map(|panel| panel.is_zoomed(window, cx))
4670 .unwrap_or(false);
4671
4672 let bottom_dock = this.bottom_dock.read(cx);
4673 let bottom_visible = bottom_dock.is_open();
4674 let bottom_active_panel = bottom_dock
4675 .active_panel()
4676 .map(|panel| panel.persistent_name().to_string());
4677 let bottom_dock_zoom = bottom_dock
4678 .active_panel()
4679 .map(|panel| panel.is_zoomed(window, cx))
4680 .unwrap_or(false);
4681
4682 DockStructure {
4683 left: DockData {
4684 visible: left_visible,
4685 active_panel: left_active_panel,
4686 zoom: left_dock_zoom,
4687 },
4688 right: DockData {
4689 visible: right_visible,
4690 active_panel: right_active_panel,
4691 zoom: right_dock_zoom,
4692 },
4693 bottom: DockData {
4694 visible: bottom_visible,
4695 active_panel: bottom_active_panel,
4696 zoom: bottom_dock_zoom,
4697 },
4698 }
4699 }
4700
4701 let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4702 Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4703 } else if let Some(local_paths) = self.local_paths(cx) {
4704 if !local_paths.is_empty() {
4705 Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4706 } else {
4707 None
4708 }
4709 } else {
4710 None
4711 };
4712
4713 if let Some(location) = location {
4714 let breakpoints = self.project.update(cx, |project, cx| {
4715 project.breakpoint_store().read(cx).all_breakpoints(cx)
4716 });
4717
4718 let center_group = build_serialized_pane_group(&self.center.root, window, cx);
4719 let docks = build_serialized_docks(self, window, cx);
4720 let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
4721 let serialized_workspace = SerializedWorkspace {
4722 id: database_id,
4723 location,
4724 center_group,
4725 window_bounds,
4726 display: Default::default(),
4727 docks,
4728 centered_layout: self.centered_layout,
4729 session_id: self.session_id.clone(),
4730 breakpoints,
4731 window_id: Some(window.window_handle().window_id().as_u64()),
4732 };
4733 return window.spawn(cx, async move |_| {
4734 persistence::DB.save_workspace(serialized_workspace).await
4735 });
4736 }
4737 Task::ready(())
4738 }
4739
4740 async fn serialize_items(
4741 this: &WeakEntity<Self>,
4742 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4743 cx: &mut AsyncWindowContext,
4744 ) -> Result<()> {
4745 const CHUNK_SIZE: usize = 200;
4746
4747 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4748
4749 while let Some(items_received) = serializable_items.next().await {
4750 let unique_items =
4751 items_received
4752 .into_iter()
4753 .fold(HashMap::default(), |mut acc, item| {
4754 acc.entry(item.item_id()).or_insert(item);
4755 acc
4756 });
4757
4758 // We use into_iter() here so that the references to the items are moved into
4759 // the tasks and not kept alive while we're sleeping.
4760 for (_, item) in unique_items.into_iter() {
4761 if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
4762 item.serialize(workspace, false, window, cx)
4763 }) {
4764 cx.background_spawn(async move { task.await.log_err() })
4765 .detach();
4766 }
4767 }
4768
4769 cx.background_executor()
4770 .timer(SERIALIZATION_THROTTLE_TIME)
4771 .await;
4772 }
4773
4774 Ok(())
4775 }
4776
4777 pub(crate) fn enqueue_item_serialization(
4778 &mut self,
4779 item: Box<dyn SerializableItemHandle>,
4780 ) -> Result<()> {
4781 self.serializable_items_tx
4782 .unbounded_send(item)
4783 .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4784 }
4785
4786 pub(crate) fn load_workspace(
4787 serialized_workspace: SerializedWorkspace,
4788 paths_to_open: Vec<Option<ProjectPath>>,
4789 window: &mut Window,
4790 cx: &mut Context<Workspace>,
4791 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4792 cx.spawn_in(window, async move |workspace, cx| {
4793 let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
4794
4795 let mut center_group = None;
4796 let mut center_items = None;
4797
4798 // Traverse the splits tree and add to things
4799 if let Some((group, active_pane, items)) = serialized_workspace
4800 .center_group
4801 .deserialize(&project, serialized_workspace.id, workspace.clone(), cx)
4802 .await
4803 {
4804 center_items = Some(items);
4805 center_group = Some((group, active_pane))
4806 }
4807
4808 let mut items_by_project_path = HashMap::default();
4809 let mut item_ids_by_kind = HashMap::default();
4810 let mut all_deserialized_items = Vec::default();
4811 cx.update(|_, cx| {
4812 for item in center_items.unwrap_or_default().into_iter().flatten() {
4813 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4814 item_ids_by_kind
4815 .entry(serializable_item_handle.serialized_item_kind())
4816 .or_insert(Vec::new())
4817 .push(item.item_id().as_u64() as ItemId);
4818 }
4819
4820 if let Some(project_path) = item.project_path(cx) {
4821 items_by_project_path.insert(project_path, item.clone());
4822 }
4823 all_deserialized_items.push(item);
4824 }
4825 })?;
4826
4827 let opened_items = paths_to_open
4828 .into_iter()
4829 .map(|path_to_open| {
4830 path_to_open
4831 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4832 })
4833 .collect::<Vec<_>>();
4834
4835 // Remove old panes from workspace panes list
4836 workspace.update_in(cx, |workspace, window, cx| {
4837 if let Some((center_group, active_pane)) = center_group {
4838 workspace.remove_panes(workspace.center.root.clone(), window, cx);
4839
4840 // Swap workspace center group
4841 workspace.center = PaneGroup::with_root(center_group);
4842 if let Some(active_pane) = active_pane {
4843 workspace.set_active_pane(&active_pane, window, cx);
4844 cx.focus_self(window);
4845 } else {
4846 workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
4847 }
4848 }
4849
4850 let docks = serialized_workspace.docks;
4851
4852 for (dock, serialized_dock) in [
4853 (&mut workspace.right_dock, docks.right),
4854 (&mut workspace.left_dock, docks.left),
4855 (&mut workspace.bottom_dock, docks.bottom),
4856 ]
4857 .iter_mut()
4858 {
4859 dock.update(cx, |dock, cx| {
4860 dock.serialized_dock = Some(serialized_dock.clone());
4861 dock.restore_state(window, cx);
4862 });
4863 }
4864
4865 cx.notify();
4866 })?;
4867
4868 let _ = project
4869 .update(cx, |project, cx| {
4870 project
4871 .breakpoint_store()
4872 .update(cx, |breakpoint_store, cx| {
4873 breakpoint_store
4874 .with_serialized_breakpoints(serialized_workspace.breakpoints, cx)
4875 })
4876 })?
4877 .await;
4878
4879 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4880 // after loading the items, we might have different items and in order to avoid
4881 // the database filling up, we delete items that haven't been loaded now.
4882 //
4883 // The items that have been loaded, have been saved after they've been added to the workspace.
4884 let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
4885 item_ids_by_kind
4886 .into_iter()
4887 .map(|(item_kind, loaded_items)| {
4888 SerializableItemRegistry::cleanup(
4889 item_kind,
4890 serialized_workspace.id,
4891 loaded_items,
4892 window,
4893 cx,
4894 )
4895 .log_err()
4896 })
4897 .collect::<Vec<_>>()
4898 })?;
4899
4900 futures::future::join_all(clean_up_tasks).await;
4901
4902 workspace
4903 .update_in(cx, |workspace, window, cx| {
4904 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4905 workspace.serialize_workspace_internal(window, cx).detach();
4906
4907 // Ensure that we mark the window as edited if we did load dirty items
4908 workspace.update_window_edited(window, cx);
4909 })
4910 .ok();
4911
4912 Ok(opened_items)
4913 })
4914 }
4915
4916 fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
4917 self.add_workspace_actions_listeners(div, window, cx)
4918 .on_action(cx.listener(Self::close_inactive_items_and_panes))
4919 .on_action(cx.listener(Self::close_all_items_and_panes))
4920 .on_action(cx.listener(Self::save_all))
4921 .on_action(cx.listener(Self::send_keystrokes))
4922 .on_action(cx.listener(Self::add_folder_to_project))
4923 .on_action(cx.listener(Self::follow_next_collaborator))
4924 .on_action(cx.listener(Self::close_window))
4925 .on_action(cx.listener(Self::activate_pane_at_index))
4926 .on_action(cx.listener(Self::move_item_to_pane_at_index))
4927 .on_action(cx.listener(Self::move_focused_panel_to_next_position))
4928 .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
4929 let pane = workspace.active_pane().clone();
4930 workspace.unfollow_in_pane(&pane, window, cx);
4931 }))
4932 .on_action(cx.listener(|workspace, action: &Save, window, cx| {
4933 workspace
4934 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
4935 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4936 }))
4937 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
4938 workspace
4939 .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
4940 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4941 }))
4942 .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
4943 workspace
4944 .save_active_item(SaveIntent::SaveAs, window, cx)
4945 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4946 }))
4947 .on_action(
4948 cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
4949 workspace.activate_previous_pane(window, cx)
4950 }),
4951 )
4952 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4953 workspace.activate_next_pane(window, cx)
4954 }))
4955 .on_action(
4956 cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
4957 workspace.activate_next_window(cx)
4958 }),
4959 )
4960 .on_action(
4961 cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
4962 workspace.activate_previous_window(cx)
4963 }),
4964 )
4965 .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
4966 workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
4967 }))
4968 .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
4969 workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
4970 }))
4971 .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
4972 workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
4973 }))
4974 .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
4975 workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
4976 }))
4977 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4978 workspace.activate_next_pane(window, cx)
4979 }))
4980 .on_action(cx.listener(
4981 |workspace, action: &MoveItemToPaneInDirection, window, cx| {
4982 workspace.move_item_to_pane_in_direction(action, window, cx)
4983 },
4984 ))
4985 .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
4986 workspace.swap_pane_in_direction(SplitDirection::Left, cx)
4987 }))
4988 .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
4989 workspace.swap_pane_in_direction(SplitDirection::Right, cx)
4990 }))
4991 .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
4992 workspace.swap_pane_in_direction(SplitDirection::Up, cx)
4993 }))
4994 .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
4995 workspace.swap_pane_in_direction(SplitDirection::Down, cx)
4996 }))
4997 .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
4998 this.toggle_dock(DockPosition::Left, window, cx);
4999 }))
5000 .on_action(cx.listener(
5001 |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
5002 workspace.toggle_dock(DockPosition::Right, window, cx);
5003 },
5004 ))
5005 .on_action(cx.listener(
5006 |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
5007 workspace.toggle_dock(DockPosition::Bottom, window, cx);
5008 },
5009 ))
5010 .on_action(
5011 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
5012 workspace.close_all_docks(window, cx);
5013 }),
5014 )
5015 .on_action(cx.listener(
5016 |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
5017 workspace.clear_all_notifications(cx);
5018 },
5019 ))
5020 .on_action(cx.listener(
5021 |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
5022 workspace.reopen_closed_item(window, cx).detach();
5023 },
5024 ))
5025 .on_action(cx.listener(Workspace::toggle_centered_layout))
5026 }
5027
5028 #[cfg(any(test, feature = "test-support"))]
5029 pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
5030 use node_runtime::NodeRuntime;
5031 use session::Session;
5032
5033 let client = project.read(cx).client();
5034 let user_store = project.read(cx).user_store();
5035
5036 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
5037 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
5038 window.activate_window();
5039 let app_state = Arc::new(AppState {
5040 languages: project.read(cx).languages().clone(),
5041 debug_adapters: project.read(cx).debug_adapters().clone(),
5042 workspace_store,
5043 client,
5044 user_store,
5045 fs: project.read(cx).fs().clone(),
5046 build_window_options: |_, _| Default::default(),
5047 node_runtime: NodeRuntime::unavailable(),
5048 session,
5049 });
5050 let workspace = Self::new(Default::default(), project, app_state, window, cx);
5051 workspace
5052 .active_pane
5053 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
5054 workspace
5055 }
5056
5057 pub fn register_action<A: Action>(
5058 &mut self,
5059 callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
5060 ) -> &mut Self {
5061 let callback = Arc::new(callback);
5062
5063 self.workspace_actions.push(Box::new(move |div, _, cx| {
5064 let callback = callback.clone();
5065 div.on_action(cx.listener(move |workspace, event, window, cx| {
5066 (callback.clone())(workspace, event, window, cx)
5067 }))
5068 }));
5069 self
5070 }
5071
5072 fn add_workspace_actions_listeners(
5073 &self,
5074 mut div: Div,
5075 window: &mut Window,
5076 cx: &mut Context<Self>,
5077 ) -> Div {
5078 for action in self.workspace_actions.iter() {
5079 div = (action)(div, window, cx)
5080 }
5081 div
5082 }
5083
5084 pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
5085 self.modal_layer.read(cx).has_active_modal()
5086 }
5087
5088 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
5089 self.modal_layer.read(cx).active_modal()
5090 }
5091
5092 pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
5093 where
5094 B: FnOnce(&mut Window, &mut Context<V>) -> V,
5095 {
5096 self.modal_layer.update(cx, |modal_layer, cx| {
5097 modal_layer.toggle_modal(window, cx, build)
5098 })
5099 }
5100
5101 pub fn toggle_status_toast<V: ToastView>(&mut self, entity: Entity<V>, cx: &mut App) {
5102 self.toast_layer
5103 .update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity))
5104 }
5105
5106 pub fn toggle_centered_layout(
5107 &mut self,
5108 _: &ToggleCenteredLayout,
5109 _: &mut Window,
5110 cx: &mut Context<Self>,
5111 ) {
5112 self.centered_layout = !self.centered_layout;
5113 if let Some(database_id) = self.database_id() {
5114 cx.background_spawn(DB.set_centered_layout(database_id, self.centered_layout))
5115 .detach_and_log_err(cx);
5116 }
5117 cx.notify();
5118 }
5119
5120 fn adjust_padding(padding: Option<f32>) -> f32 {
5121 padding
5122 .unwrap_or(Self::DEFAULT_PADDING)
5123 .clamp(0.0, Self::MAX_PADDING)
5124 }
5125
5126 fn render_dock(
5127 &self,
5128 position: DockPosition,
5129 dock: &Entity<Dock>,
5130 window: &mut Window,
5131 cx: &mut App,
5132 ) -> Option<Div> {
5133 if self.zoomed_position == Some(position) {
5134 return None;
5135 }
5136
5137 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
5138 let pane = panel.pane(cx)?;
5139 let follower_states = &self.follower_states;
5140 leader_border_for_pane(follower_states, &pane, window, cx)
5141 });
5142
5143 Some(
5144 div()
5145 .flex()
5146 .flex_none()
5147 .overflow_hidden()
5148 .child(dock.clone())
5149 .children(leader_border),
5150 )
5151 }
5152
5153 pub fn for_window(window: &mut Window, _: &mut App) -> Option<Entity<Workspace>> {
5154 window.root().flatten()
5155 }
5156
5157 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
5158 self.zoomed.as_ref()
5159 }
5160
5161 pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
5162 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5163 return;
5164 };
5165 let windows = cx.windows();
5166 let Some(next_window) = windows
5167 .iter()
5168 .cycle()
5169 .skip_while(|window| window.window_id() != current_window_id)
5170 .nth(1)
5171 else {
5172 return;
5173 };
5174 next_window
5175 .update(cx, |_, window, _| window.activate_window())
5176 .ok();
5177 }
5178
5179 pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
5180 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5181 return;
5182 };
5183 let windows = cx.windows();
5184 let Some(prev_window) = windows
5185 .iter()
5186 .rev()
5187 .cycle()
5188 .skip_while(|window| window.window_id() != current_window_id)
5189 .nth(1)
5190 else {
5191 return;
5192 };
5193 prev_window
5194 .update(cx, |_, window, _| window.activate_window())
5195 .ok();
5196 }
5197
5198 pub fn debug_task_ready(&mut self, task_id: &TaskId, cx: &mut App) {
5199 if let Some(debug_config) = self.debug_task_queue.remove(task_id) {
5200 self.project.update(cx, |project, cx| {
5201 project
5202 .start_debug_session(debug_config, cx)
5203 .detach_and_log_err(cx);
5204 })
5205 }
5206 }
5207}
5208
5209fn leader_border_for_pane(
5210 follower_states: &HashMap<PeerId, FollowerState>,
5211 pane: &Entity<Pane>,
5212 _: &Window,
5213 cx: &App,
5214) -> Option<Div> {
5215 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
5216 if state.pane() == pane {
5217 Some((*leader_id, state))
5218 } else {
5219 None
5220 }
5221 })?;
5222
5223 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
5224 let leader = room.remote_participant_for_peer_id(leader_id)?;
5225
5226 let mut leader_color = cx
5227 .theme()
5228 .players()
5229 .color_for_participant(leader.participant_index.0)
5230 .cursor;
5231 leader_color.fade_out(0.3);
5232 Some(
5233 div()
5234 .absolute()
5235 .size_full()
5236 .left_0()
5237 .top_0()
5238 .border_2()
5239 .border_color(leader_color),
5240 )
5241}
5242
5243fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
5244 ZED_WINDOW_POSITION
5245 .zip(*ZED_WINDOW_SIZE)
5246 .map(|(position, size)| Bounds {
5247 origin: position,
5248 size,
5249 })
5250}
5251
5252fn open_items(
5253 serialized_workspace: Option<SerializedWorkspace>,
5254 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
5255 window: &mut Window,
5256 cx: &mut Context<Workspace>,
5257) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
5258 let restored_items = serialized_workspace.map(|serialized_workspace| {
5259 Workspace::load_workspace(
5260 serialized_workspace,
5261 project_paths_to_open
5262 .iter()
5263 .map(|(_, project_path)| project_path)
5264 .cloned()
5265 .collect(),
5266 window,
5267 cx,
5268 )
5269 });
5270
5271 cx.spawn_in(window, async move |workspace, cx| {
5272 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
5273
5274 if let Some(restored_items) = restored_items {
5275 let restored_items = restored_items.await?;
5276
5277 let restored_project_paths = restored_items
5278 .iter()
5279 .filter_map(|item| {
5280 cx.update(|_, cx| item.as_ref()?.project_path(cx))
5281 .ok()
5282 .flatten()
5283 })
5284 .collect::<HashSet<_>>();
5285
5286 for restored_item in restored_items {
5287 opened_items.push(restored_item.map(Ok));
5288 }
5289
5290 project_paths_to_open
5291 .iter_mut()
5292 .for_each(|(_, project_path)| {
5293 if let Some(project_path_to_open) = project_path {
5294 if restored_project_paths.contains(project_path_to_open) {
5295 *project_path = None;
5296 }
5297 }
5298 });
5299 } else {
5300 for _ in 0..project_paths_to_open.len() {
5301 opened_items.push(None);
5302 }
5303 }
5304 assert!(opened_items.len() == project_paths_to_open.len());
5305
5306 let tasks =
5307 project_paths_to_open
5308 .into_iter()
5309 .enumerate()
5310 .map(|(ix, (abs_path, project_path))| {
5311 let workspace = workspace.clone();
5312 cx.spawn(async move |cx| {
5313 let file_project_path = project_path?;
5314 let abs_path_task = workspace.update(cx, |workspace, cx| {
5315 workspace.project().update(cx, |project, cx| {
5316 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
5317 })
5318 });
5319
5320 // We only want to open file paths here. If one of the items
5321 // here is a directory, it was already opened further above
5322 // with a `find_or_create_worktree`.
5323 if let Ok(task) = abs_path_task {
5324 if task.await.map_or(true, |p| p.is_file()) {
5325 return Some((
5326 ix,
5327 workspace
5328 .update_in(cx, |workspace, window, cx| {
5329 workspace.open_path(
5330 file_project_path,
5331 None,
5332 true,
5333 window,
5334 cx,
5335 )
5336 })
5337 .log_err()?
5338 .await,
5339 ));
5340 }
5341 }
5342 None
5343 })
5344 });
5345
5346 let tasks = tasks.collect::<Vec<_>>();
5347
5348 let tasks = futures::future::join_all(tasks);
5349 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
5350 opened_items[ix] = Some(path_open_result);
5351 }
5352
5353 Ok(opened_items)
5354 })
5355}
5356
5357enum ActivateInDirectionTarget {
5358 Pane(Entity<Pane>),
5359 Dock(Entity<Dock>),
5360}
5361
5362fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
5363 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";
5364
5365 workspace
5366 .update(cx, |workspace, _, cx| {
5367 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
5368 struct DatabaseFailedNotification;
5369
5370 workspace.show_notification(
5371 NotificationId::unique::<DatabaseFailedNotification>(),
5372 cx,
5373 |cx| {
5374 cx.new(|cx| {
5375 MessageNotification::new("Failed to load the database file.", cx)
5376 .primary_message("File an Issue")
5377 .primary_icon(IconName::Plus)
5378 .primary_on_click(|_window, cx| cx.open_url(REPORT_ISSUE_URL))
5379 })
5380 },
5381 );
5382 }
5383 })
5384 .log_err();
5385}
5386
5387impl Focusable for Workspace {
5388 fn focus_handle(&self, cx: &App) -> FocusHandle {
5389 self.active_pane.focus_handle(cx)
5390 }
5391}
5392
5393#[derive(Clone)]
5394struct DraggedDock(DockPosition);
5395
5396impl Render for DraggedDock {
5397 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5398 gpui::Empty
5399 }
5400}
5401
5402impl Render for Workspace {
5403 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5404 let mut context = KeyContext::new_with_defaults();
5405 context.add("Workspace");
5406 context.set("keyboard_layout", cx.keyboard_layout().clone());
5407 let centered_layout = self.centered_layout
5408 && self.center.panes().len() == 1
5409 && self.active_item(cx).is_some();
5410 let render_padding = |size| {
5411 (size > 0.0).then(|| {
5412 div()
5413 .h_full()
5414 .w(relative(size))
5415 .bg(cx.theme().colors().editor_background)
5416 .border_color(cx.theme().colors().pane_group_border)
5417 })
5418 };
5419 let paddings = if centered_layout {
5420 let settings = WorkspaceSettings::get_global(cx).centered_layout;
5421 (
5422 render_padding(Self::adjust_padding(settings.left_padding)),
5423 render_padding(Self::adjust_padding(settings.right_padding)),
5424 )
5425 } else {
5426 (None, None)
5427 };
5428 let ui_font = theme::setup_ui_font(window, cx);
5429
5430 let theme = cx.theme().clone();
5431 let colors = theme.colors();
5432
5433 client_side_decorations(
5434 self.actions(div(), window, cx)
5435 .key_context(context)
5436 .relative()
5437 .size_full()
5438 .flex()
5439 .flex_col()
5440 .font(ui_font)
5441 .gap_0()
5442 .justify_start()
5443 .items_start()
5444 .text_color(colors.text)
5445 .overflow_hidden()
5446 .children(self.titlebar_item.clone())
5447 .child(
5448 div()
5449 .size_full()
5450 .relative()
5451 .flex_1()
5452 .flex()
5453 .flex_col()
5454 .child(
5455 div()
5456 .id("workspace")
5457 .bg(colors.background)
5458 .relative()
5459 .flex_1()
5460 .w_full()
5461 .flex()
5462 .flex_col()
5463 .overflow_hidden()
5464 .border_t_1()
5465 .border_b_1()
5466 .border_color(colors.border)
5467 .child({
5468 let this = cx.entity().clone();
5469 canvas(
5470 move |bounds, window, cx| {
5471 this.update(cx, |this, cx| {
5472 let bounds_changed = this.bounds != bounds;
5473 this.bounds = bounds;
5474
5475 if bounds_changed {
5476 this.left_dock.update(cx, |dock, cx| {
5477 dock.clamp_panel_size(
5478 bounds.size.width,
5479 window,
5480 cx,
5481 )
5482 });
5483
5484 this.right_dock.update(cx, |dock, cx| {
5485 dock.clamp_panel_size(
5486 bounds.size.width,
5487 window,
5488 cx,
5489 )
5490 });
5491
5492 this.bottom_dock.update(cx, |dock, cx| {
5493 dock.clamp_panel_size(
5494 bounds.size.height,
5495 window,
5496 cx,
5497 )
5498 });
5499 }
5500 })
5501 },
5502 |_, _, _, _| {},
5503 )
5504 .absolute()
5505 .size_full()
5506 })
5507 .when(self.zoomed.is_none(), |this| {
5508 this.on_drag_move(cx.listener(
5509 move |workspace,
5510 e: &DragMoveEvent<DraggedDock>,
5511 window,
5512 cx| {
5513 if workspace.previous_dock_drag_coordinates
5514 != Some(e.event.position)
5515 {
5516 workspace.previous_dock_drag_coordinates =
5517 Some(e.event.position);
5518 match e.drag(cx).0 {
5519 DockPosition::Left => {
5520 resize_left_dock(
5521 e.event.position.x
5522 - workspace.bounds.left(),
5523 workspace,
5524 window,
5525 cx,
5526 );
5527 }
5528 DockPosition::Right => {
5529 resize_right_dock(
5530 workspace.bounds.right()
5531 - e.event.position.x,
5532 workspace,
5533 window,
5534 cx,
5535 );
5536 }
5537 DockPosition::Bottom => {
5538 resize_bottom_dock(
5539 workspace.bounds.bottom()
5540 - e.event.position.y,
5541 workspace,
5542 window,
5543 cx,
5544 );
5545 }
5546 };
5547 workspace.serialize_workspace(window, cx);
5548 }
5549 },
5550 ))
5551 })
5552 .child(
5553 div()
5554 .flex()
5555 .flex_row()
5556 .h_full()
5557 // Left Dock
5558 .children(self.render_dock(
5559 DockPosition::Left,
5560 &self.left_dock,
5561 window,
5562 cx,
5563 ))
5564 // Panes
5565 .child(
5566 div()
5567 .flex()
5568 .flex_col()
5569 .flex_1()
5570 .overflow_hidden()
5571 .child(
5572 h_flex()
5573 .flex_1()
5574 .when_some(paddings.0, |this, p| {
5575 this.child(p.border_r_1())
5576 })
5577 .child(self.center.render(
5578 &self.project,
5579 &self.follower_states,
5580 self.active_call(),
5581 &self.active_pane,
5582 self.zoomed.as_ref(),
5583 &self.app_state,
5584 window,
5585 cx,
5586 ))
5587 .when_some(paddings.1, |this, p| {
5588 this.child(p.border_l_1())
5589 }),
5590 )
5591 .children(self.render_dock(
5592 DockPosition::Bottom,
5593 &self.bottom_dock,
5594 window,
5595 cx,
5596 )),
5597 )
5598 // Right Dock
5599 .children(self.render_dock(
5600 DockPosition::Right,
5601 &self.right_dock,
5602 window,
5603 cx,
5604 )),
5605 )
5606 .children(self.zoomed.as_ref().and_then(|view| {
5607 let zoomed_view = view.upgrade()?;
5608 let div = div()
5609 .occlude()
5610 .absolute()
5611 .overflow_hidden()
5612 .border_color(colors.border)
5613 .bg(colors.background)
5614 .child(zoomed_view)
5615 .inset_0()
5616 .shadow_lg();
5617
5618 Some(match self.zoomed_position {
5619 Some(DockPosition::Left) => div.right_2().border_r_1(),
5620 Some(DockPosition::Right) => div.left_2().border_l_1(),
5621 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
5622 None => {
5623 div.top_2().bottom_2().left_2().right_2().border_1()
5624 }
5625 })
5626 }))
5627 .children(self.render_notifications(window, cx)),
5628 )
5629 .child(self.status_bar.clone())
5630 .child(self.modal_layer.clone())
5631 .child(self.toast_layer.clone()),
5632 ),
5633 window,
5634 cx,
5635 )
5636 }
5637}
5638
5639fn resize_bottom_dock(
5640 new_size: Pixels,
5641 workspace: &mut Workspace,
5642 window: &mut Window,
5643 cx: &mut App,
5644) {
5645 let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
5646 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
5647 bottom_dock.resize_active_panel(Some(size), window, cx);
5648 });
5649}
5650
5651fn resize_right_dock(
5652 new_size: Pixels,
5653 workspace: &mut Workspace,
5654 window: &mut Window,
5655 cx: &mut App,
5656) {
5657 let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
5658 workspace.right_dock.update(cx, |right_dock, cx| {
5659 right_dock.resize_active_panel(Some(size), window, cx);
5660 });
5661}
5662
5663fn resize_left_dock(
5664 new_size: Pixels,
5665 workspace: &mut Workspace,
5666 window: &mut Window,
5667 cx: &mut App,
5668) {
5669 let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5670
5671 workspace.left_dock.update(cx, |left_dock, cx| {
5672 left_dock.resize_active_panel(Some(size), window, cx);
5673 });
5674}
5675
5676impl WorkspaceStore {
5677 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
5678 Self {
5679 workspaces: Default::default(),
5680 _subscriptions: vec![
5681 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
5682 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
5683 ],
5684 client,
5685 }
5686 }
5687
5688 pub fn update_followers(
5689 &self,
5690 project_id: Option<u64>,
5691 update: proto::update_followers::Variant,
5692 cx: &App,
5693 ) -> Option<()> {
5694 let active_call = ActiveCall::try_global(cx)?;
5695 let room_id = active_call.read(cx).room()?.read(cx).id();
5696 self.client
5697 .send(proto::UpdateFollowers {
5698 room_id,
5699 project_id,
5700 variant: Some(update),
5701 })
5702 .log_err()
5703 }
5704
5705 pub async fn handle_follow(
5706 this: Entity<Self>,
5707 envelope: TypedEnvelope<proto::Follow>,
5708 mut cx: AsyncApp,
5709 ) -> Result<proto::FollowResponse> {
5710 this.update(&mut cx, |this, cx| {
5711 let follower = Follower {
5712 project_id: envelope.payload.project_id,
5713 peer_id: envelope.original_sender_id()?,
5714 };
5715
5716 let mut response = proto::FollowResponse::default();
5717 this.workspaces.retain(|workspace| {
5718 workspace
5719 .update(cx, |workspace, window, cx| {
5720 let handler_response =
5721 workspace.handle_follow(follower.project_id, window, cx);
5722 if let Some(active_view) = handler_response.active_view.clone() {
5723 if workspace.project.read(cx).remote_id() == follower.project_id {
5724 response.active_view = Some(active_view)
5725 }
5726 }
5727 })
5728 .is_ok()
5729 });
5730
5731 Ok(response)
5732 })?
5733 }
5734
5735 async fn handle_update_followers(
5736 this: Entity<Self>,
5737 envelope: TypedEnvelope<proto::UpdateFollowers>,
5738 mut cx: AsyncApp,
5739 ) -> Result<()> {
5740 let leader_id = envelope.original_sender_id()?;
5741 let update = envelope.payload;
5742
5743 this.update(&mut cx, |this, cx| {
5744 this.workspaces.retain(|workspace| {
5745 workspace
5746 .update(cx, |workspace, window, cx| {
5747 let project_id = workspace.project.read(cx).remote_id();
5748 if update.project_id != project_id && update.project_id.is_some() {
5749 return;
5750 }
5751 workspace.handle_update_followers(leader_id, update.clone(), window, cx);
5752 })
5753 .is_ok()
5754 });
5755 Ok(())
5756 })?
5757 }
5758}
5759
5760impl ViewId {
5761 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5762 Ok(Self {
5763 creator: message
5764 .creator
5765 .ok_or_else(|| anyhow!("creator is missing"))?,
5766 id: message.id,
5767 })
5768 }
5769
5770 pub(crate) fn to_proto(self) -> proto::ViewId {
5771 proto::ViewId {
5772 creator: Some(self.creator),
5773 id: self.id,
5774 }
5775 }
5776}
5777
5778impl FollowerState {
5779 fn pane(&self) -> &Entity<Pane> {
5780 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5781 }
5782}
5783
5784pub trait WorkspaceHandle {
5785 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
5786}
5787
5788impl WorkspaceHandle for Entity<Workspace> {
5789 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
5790 self.read(cx)
5791 .worktrees(cx)
5792 .flat_map(|worktree| {
5793 let worktree_id = worktree.read(cx).id();
5794 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5795 worktree_id,
5796 path: f.path.clone(),
5797 })
5798 })
5799 .collect::<Vec<_>>()
5800 }
5801}
5802
5803impl std::fmt::Debug for OpenPaths {
5804 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5805 f.debug_struct("OpenPaths")
5806 .field("paths", &self.paths)
5807 .finish()
5808 }
5809}
5810
5811pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5812 DB.last_workspace().await.log_err().flatten()
5813}
5814
5815pub fn last_session_workspace_locations(
5816 last_session_id: &str,
5817 last_session_window_stack: Option<Vec<WindowId>>,
5818) -> Option<Vec<SerializedWorkspaceLocation>> {
5819 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5820 .log_err()
5821}
5822
5823actions!(
5824 collab,
5825 [
5826 OpenChannelNotes,
5827 Mute,
5828 Deafen,
5829 LeaveCall,
5830 ShareProject,
5831 ScreenShare
5832 ]
5833);
5834actions!(zed, [OpenLog]);
5835
5836async fn join_channel_internal(
5837 channel_id: ChannelId,
5838 app_state: &Arc<AppState>,
5839 requesting_window: Option<WindowHandle<Workspace>>,
5840 active_call: &Entity<ActiveCall>,
5841 cx: &mut AsyncApp,
5842) -> Result<bool> {
5843 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5844 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5845 return (false, None);
5846 };
5847
5848 let already_in_channel = room.channel_id() == Some(channel_id);
5849 let should_prompt = room.is_sharing_project()
5850 && !room.remote_participants().is_empty()
5851 && !already_in_channel;
5852 let open_room = if already_in_channel {
5853 active_call.room().cloned()
5854 } else {
5855 None
5856 };
5857 (should_prompt, open_room)
5858 })?;
5859
5860 if let Some(room) = open_room {
5861 let task = room.update(cx, |room, cx| {
5862 if let Some((project, host)) = room.most_active_project(cx) {
5863 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5864 }
5865
5866 None
5867 })?;
5868 if let Some(task) = task {
5869 task.await?;
5870 }
5871 return anyhow::Ok(true);
5872 }
5873
5874 if should_prompt {
5875 if let Some(workspace) = requesting_window {
5876 let answer = workspace
5877 .update(cx, |_, window, cx| {
5878 window.prompt(
5879 PromptLevel::Warning,
5880 "Do you want to switch channels?",
5881 Some("Leaving this call will unshare your current project."),
5882 &["Yes, Join Channel", "Cancel"],
5883 cx,
5884 )
5885 })?
5886 .await;
5887
5888 if answer == Ok(1) {
5889 return Ok(false);
5890 }
5891 } else {
5892 return Ok(false); // unreachable!() hopefully
5893 }
5894 }
5895
5896 let client = cx.update(|cx| active_call.read(cx).client())?;
5897
5898 let mut client_status = client.status();
5899
5900 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5901 'outer: loop {
5902 let Some(status) = client_status.recv().await else {
5903 return Err(anyhow!("error connecting"));
5904 };
5905
5906 match status {
5907 Status::Connecting
5908 | Status::Authenticating
5909 | Status::Reconnecting
5910 | Status::Reauthenticating => continue,
5911 Status::Connected { .. } => break 'outer,
5912 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5913 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5914 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5915 return Err(ErrorCode::Disconnected.into());
5916 }
5917 }
5918 }
5919
5920 let room = active_call
5921 .update(cx, |active_call, cx| {
5922 active_call.join_channel(channel_id, cx)
5923 })?
5924 .await?;
5925
5926 let Some(room) = room else {
5927 return anyhow::Ok(true);
5928 };
5929
5930 room.update(cx, |room, _| room.room_update_completed())?
5931 .await;
5932
5933 let task = room.update(cx, |room, cx| {
5934 if let Some((project, host)) = room.most_active_project(cx) {
5935 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5936 }
5937
5938 // If you are the first to join a channel, see if you should share your project.
5939 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5940 if let Some(workspace) = requesting_window {
5941 let project = workspace.update(cx, |workspace, _, cx| {
5942 let project = workspace.project.read(cx);
5943
5944 if !CallSettings::get_global(cx).share_on_join {
5945 return None;
5946 }
5947
5948 if (project.is_local() || project.is_via_ssh())
5949 && project.visible_worktrees(cx).any(|tree| {
5950 tree.read(cx)
5951 .root_entry()
5952 .map_or(false, |entry| entry.is_dir())
5953 })
5954 {
5955 Some(workspace.project.clone())
5956 } else {
5957 None
5958 }
5959 });
5960 if let Ok(Some(project)) = project {
5961 return Some(cx.spawn(async move |room, cx| {
5962 room.update(cx, |room, cx| room.share_project(project, cx))?
5963 .await?;
5964 Ok(())
5965 }));
5966 }
5967 }
5968 }
5969
5970 None
5971 })?;
5972 if let Some(task) = task {
5973 task.await?;
5974 return anyhow::Ok(true);
5975 }
5976 anyhow::Ok(false)
5977}
5978
5979pub fn join_channel(
5980 channel_id: ChannelId,
5981 app_state: Arc<AppState>,
5982 requesting_window: Option<WindowHandle<Workspace>>,
5983 cx: &mut App,
5984) -> Task<Result<()>> {
5985 let active_call = ActiveCall::global(cx);
5986 cx.spawn(async move |cx| {
5987 let result = join_channel_internal(
5988 channel_id,
5989 &app_state,
5990 requesting_window,
5991 &active_call,
5992 cx,
5993 )
5994 .await;
5995
5996 // join channel succeeded, and opened a window
5997 if matches!(result, Ok(true)) {
5998 return anyhow::Ok(());
5999 }
6000
6001 // find an existing workspace to focus and show call controls
6002 let mut active_window =
6003 requesting_window.or_else(|| activate_any_workspace_window( cx));
6004 if active_window.is_none() {
6005 // no open workspaces, make one to show the error in (blergh)
6006 let (window_handle, _) = cx
6007 .update(|cx| {
6008 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
6009 })?
6010 .await?;
6011
6012 if result.is_ok() {
6013 cx.update(|cx| {
6014 cx.dispatch_action(&OpenChannelNotes);
6015 }).log_err();
6016 }
6017
6018 active_window = Some(window_handle);
6019 }
6020
6021 if let Err(err) = result {
6022 log::error!("failed to join channel: {}", err);
6023 if let Some(active_window) = active_window {
6024 active_window
6025 .update(cx, |_, window, cx| {
6026 let detail: SharedString = match err.error_code() {
6027 ErrorCode::SignedOut => {
6028 "Please sign in to continue.".into()
6029 }
6030 ErrorCode::UpgradeRequired => {
6031 "Your are running an unsupported version of Zed. Please update to continue.".into()
6032 }
6033 ErrorCode::NoSuchChannel => {
6034 "No matching channel was found. Please check the link and try again.".into()
6035 }
6036 ErrorCode::Forbidden => {
6037 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
6038 }
6039 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
6040 _ => format!("{}\n\nPlease try again.", err).into(),
6041 };
6042 window.prompt(
6043 PromptLevel::Critical,
6044 "Failed to join channel",
6045 Some(&detail),
6046 &["Ok"],
6047 cx)
6048 })?
6049 .await
6050 .ok();
6051 }
6052 }
6053
6054 // return ok, we showed the error to the user.
6055 anyhow::Ok(())
6056 })
6057}
6058
6059pub async fn get_any_active_workspace(
6060 app_state: Arc<AppState>,
6061 mut cx: AsyncApp,
6062) -> anyhow::Result<WindowHandle<Workspace>> {
6063 // find an existing workspace to focus and show call controls
6064 let active_window = activate_any_workspace_window(&mut cx);
6065 if active_window.is_none() {
6066 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
6067 .await?;
6068 }
6069 activate_any_workspace_window(&mut cx).context("could not open zed")
6070}
6071
6072fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
6073 cx.update(|cx| {
6074 if let Some(workspace_window) = cx
6075 .active_window()
6076 .and_then(|window| window.downcast::<Workspace>())
6077 {
6078 return Some(workspace_window);
6079 }
6080
6081 for window in cx.windows() {
6082 if let Some(workspace_window) = window.downcast::<Workspace>() {
6083 workspace_window
6084 .update(cx, |_, window, _| window.activate_window())
6085 .ok();
6086 return Some(workspace_window);
6087 }
6088 }
6089 None
6090 })
6091 .ok()
6092 .flatten()
6093}
6094
6095pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
6096 cx.windows()
6097 .into_iter()
6098 .filter_map(|window| window.downcast::<Workspace>())
6099 .filter(|workspace| {
6100 workspace
6101 .read(cx)
6102 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
6103 })
6104 .collect()
6105}
6106
6107#[derive(Default)]
6108pub struct OpenOptions {
6109 pub visible: Option<OpenVisible>,
6110 pub focus: Option<bool>,
6111 pub open_new_workspace: Option<bool>,
6112 pub replace_window: Option<WindowHandle<Workspace>>,
6113 pub env: Option<HashMap<String, String>>,
6114}
6115
6116#[allow(clippy::type_complexity)]
6117pub fn open_paths(
6118 abs_paths: &[PathBuf],
6119 app_state: Arc<AppState>,
6120 open_options: OpenOptions,
6121 cx: &mut App,
6122) -> Task<
6123 anyhow::Result<(
6124 WindowHandle<Workspace>,
6125 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
6126 )>,
6127> {
6128 let abs_paths = abs_paths.to_vec();
6129 let mut existing = None;
6130 let mut best_match = None;
6131 let mut open_visible = OpenVisible::All;
6132
6133 cx.spawn(async move |cx| {
6134 if open_options.open_new_workspace != Some(true) {
6135 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
6136 let all_metadatas = futures::future::join_all(all_paths)
6137 .await
6138 .into_iter()
6139 .filter_map(|result| result.ok().flatten())
6140 .collect::<Vec<_>>();
6141
6142 cx.update(|cx| {
6143 for window in local_workspace_windows(&cx) {
6144 if let Ok(workspace) = window.read(&cx) {
6145 let m = workspace.project.read(&cx).visibility_for_paths(
6146 &abs_paths,
6147 &all_metadatas,
6148 open_options.open_new_workspace == None,
6149 cx,
6150 );
6151 if m > best_match {
6152 existing = Some(window);
6153 best_match = m;
6154 } else if best_match.is_none()
6155 && open_options.open_new_workspace == Some(false)
6156 {
6157 existing = Some(window)
6158 }
6159 }
6160 }
6161 })?;
6162
6163 if open_options.open_new_workspace.is_none() && existing.is_none() {
6164 if all_metadatas.iter().all(|file| !file.is_dir) {
6165 cx.update(|cx| {
6166 if let Some(window) = cx
6167 .active_window()
6168 .and_then(|window| window.downcast::<Workspace>())
6169 {
6170 if let Ok(workspace) = window.read(cx) {
6171 let project = workspace.project().read(cx);
6172 if project.is_local() && !project.is_via_collab() {
6173 existing = Some(window);
6174 open_visible = OpenVisible::None;
6175 return;
6176 }
6177 }
6178 }
6179 for window in local_workspace_windows(cx) {
6180 if let Ok(workspace) = window.read(cx) {
6181 let project = workspace.project().read(cx);
6182 if project.is_via_collab() {
6183 continue;
6184 }
6185 existing = Some(window);
6186 open_visible = OpenVisible::None;
6187 break;
6188 }
6189 }
6190 })?;
6191 }
6192 }
6193 }
6194
6195 if let Some(existing) = existing {
6196 let open_task = existing
6197 .update(cx, |workspace, window, cx| {
6198 window.activate_window();
6199 workspace.open_paths(
6200 abs_paths,
6201 OpenOptions {
6202 visible: Some(open_visible),
6203 ..Default::default()
6204 },
6205 None,
6206 window,
6207 cx,
6208 )
6209 })?
6210 .await;
6211
6212 _ = existing.update(cx, |workspace, _, cx| {
6213 for item in open_task.iter().flatten() {
6214 if let Err(e) = item {
6215 workspace.show_error(&e, cx);
6216 }
6217 }
6218 });
6219
6220 Ok((existing, open_task))
6221 } else {
6222 cx.update(move |cx| {
6223 Workspace::new_local(
6224 abs_paths,
6225 app_state.clone(),
6226 open_options.replace_window,
6227 open_options.env,
6228 cx,
6229 )
6230 })?
6231 .await
6232 }
6233 })
6234}
6235
6236pub fn open_new(
6237 open_options: OpenOptions,
6238 app_state: Arc<AppState>,
6239 cx: &mut App,
6240 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6241) -> Task<anyhow::Result<()>> {
6242 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6243 cx.spawn(async move |cx| {
6244 let (workspace, opened_paths) = task.await?;
6245 workspace.update(cx, |workspace, window, cx| {
6246 if opened_paths.is_empty() {
6247 init(workspace, window, cx)
6248 }
6249 })?;
6250 Ok(())
6251 })
6252}
6253
6254pub fn create_and_open_local_file(
6255 path: &'static Path,
6256 window: &mut Window,
6257 cx: &mut Context<Workspace>,
6258 default_content: impl 'static + Send + FnOnce() -> Rope,
6259) -> Task<Result<Box<dyn ItemHandle>>> {
6260 cx.spawn_in(window, async move |workspace, cx| {
6261 let fs = workspace.update(cx, |workspace, _| workspace.app_state().fs.clone())?;
6262 if !fs.is_file(path).await {
6263 fs.create_file(path, Default::default()).await?;
6264 fs.save(path, &default_content(), Default::default())
6265 .await?;
6266 }
6267
6268 let mut items = workspace
6269 .update_in(cx, |workspace, window, cx| {
6270 workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6271 workspace.open_paths(
6272 vec![path.to_path_buf()],
6273 OpenOptions {
6274 visible: Some(OpenVisible::None),
6275 ..Default::default()
6276 },
6277 None,
6278 window,
6279 cx,
6280 )
6281 })
6282 })?
6283 .await?
6284 .await;
6285
6286 let item = items.pop().flatten();
6287 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
6288 })
6289}
6290
6291pub fn open_ssh_project_with_new_connection(
6292 window: WindowHandle<Workspace>,
6293 connection_options: SshConnectionOptions,
6294 cancel_rx: oneshot::Receiver<()>,
6295 delegate: Arc<dyn SshClientDelegate>,
6296 app_state: Arc<AppState>,
6297 paths: Vec<PathBuf>,
6298 cx: &mut App,
6299) -> Task<Result<()>> {
6300 cx.spawn(async move |cx| {
6301 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6302 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6303
6304 let session = match cx
6305 .update(|cx| {
6306 remote::SshRemoteClient::new(
6307 ConnectionIdentifier::Workspace(workspace_id.0),
6308 connection_options,
6309 cancel_rx,
6310 delegate,
6311 cx,
6312 )
6313 })?
6314 .await?
6315 {
6316 Some(result) => result,
6317 None => return Ok(()),
6318 };
6319
6320 let project = cx.update(|cx| {
6321 project::Project::ssh(
6322 session,
6323 app_state.client.clone(),
6324 app_state.node_runtime.clone(),
6325 app_state.user_store.clone(),
6326 app_state.languages.clone(),
6327 app_state.fs.clone(),
6328 cx,
6329 )
6330 })?;
6331
6332 open_ssh_project_inner(
6333 project,
6334 paths,
6335 serialized_ssh_project,
6336 workspace_id,
6337 serialized_workspace,
6338 app_state,
6339 window,
6340 cx,
6341 )
6342 .await
6343 })
6344}
6345
6346pub fn open_ssh_project_with_existing_connection(
6347 connection_options: SshConnectionOptions,
6348 project: Entity<Project>,
6349 paths: Vec<PathBuf>,
6350 app_state: Arc<AppState>,
6351 window: WindowHandle<Workspace>,
6352 cx: &mut AsyncApp,
6353) -> Task<Result<()>> {
6354 cx.spawn(async move |cx| {
6355 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6356 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6357
6358 open_ssh_project_inner(
6359 project,
6360 paths,
6361 serialized_ssh_project,
6362 workspace_id,
6363 serialized_workspace,
6364 app_state,
6365 window,
6366 cx,
6367 )
6368 .await
6369 })
6370}
6371
6372async fn open_ssh_project_inner(
6373 project: Entity<Project>,
6374 paths: Vec<PathBuf>,
6375 serialized_ssh_project: SerializedSshProject,
6376 workspace_id: WorkspaceId,
6377 serialized_workspace: Option<SerializedWorkspace>,
6378 app_state: Arc<AppState>,
6379 window: WindowHandle<Workspace>,
6380 cx: &mut AsyncApp,
6381) -> Result<()> {
6382 let toolchains = DB.toolchains(workspace_id).await?;
6383 for (toolchain, worktree_id, path) in toolchains {
6384 project
6385 .update(cx, |this, cx| {
6386 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
6387 })?
6388 .await;
6389 }
6390 let mut project_paths_to_open = vec![];
6391 let mut project_path_errors = vec![];
6392
6393 for path in paths {
6394 let result = cx
6395 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6396 .await;
6397 match result {
6398 Ok((_, project_path)) => {
6399 project_paths_to_open.push((path.clone(), Some(project_path)));
6400 }
6401 Err(error) => {
6402 project_path_errors.push(error);
6403 }
6404 };
6405 }
6406
6407 if project_paths_to_open.is_empty() {
6408 return Err(project_path_errors
6409 .pop()
6410 .unwrap_or_else(|| anyhow!("no paths given")));
6411 }
6412
6413 cx.update_window(window.into(), |_, window, cx| {
6414 window.replace_root(cx, |window, cx| {
6415 telemetry::event!("SSH Project Opened");
6416
6417 let mut workspace =
6418 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6419 workspace.set_serialized_ssh_project(serialized_ssh_project);
6420 workspace
6421 });
6422 })?;
6423
6424 window
6425 .update(cx, |_, window, cx| {
6426 window.activate_window();
6427 open_items(serialized_workspace, project_paths_to_open, window, cx)
6428 })?
6429 .await?;
6430
6431 window.update(cx, |workspace, _, cx| {
6432 for error in project_path_errors {
6433 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6434 if let Some(path) = error.error_tag("path") {
6435 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6436 }
6437 } else {
6438 workspace.show_error(&error, cx)
6439 }
6440 }
6441 })?;
6442
6443 Ok(())
6444}
6445
6446fn serialize_ssh_project(
6447 connection_options: SshConnectionOptions,
6448 paths: Vec<PathBuf>,
6449 cx: &AsyncApp,
6450) -> Task<
6451 Result<(
6452 SerializedSshProject,
6453 WorkspaceId,
6454 Option<SerializedWorkspace>,
6455 )>,
6456> {
6457 cx.background_spawn(async move {
6458 let serialized_ssh_project = persistence::DB
6459 .get_or_create_ssh_project(
6460 connection_options.host.clone(),
6461 connection_options.port,
6462 paths
6463 .iter()
6464 .map(|path| path.to_string_lossy().to_string())
6465 .collect::<Vec<_>>(),
6466 connection_options.username.clone(),
6467 )
6468 .await?;
6469
6470 let serialized_workspace =
6471 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
6472
6473 let workspace_id = if let Some(workspace_id) =
6474 serialized_workspace.as_ref().map(|workspace| workspace.id)
6475 {
6476 workspace_id
6477 } else {
6478 persistence::DB.next_id().await?
6479 };
6480
6481 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
6482 })
6483}
6484
6485pub fn join_in_room_project(
6486 project_id: u64,
6487 follow_user_id: u64,
6488 app_state: Arc<AppState>,
6489 cx: &mut App,
6490) -> Task<Result<()>> {
6491 let windows = cx.windows();
6492 cx.spawn(async move |cx| {
6493 let existing_workspace = windows.into_iter().find_map(|window_handle| {
6494 window_handle
6495 .downcast::<Workspace>()
6496 .and_then(|window_handle| {
6497 window_handle
6498 .update(cx, |workspace, _window, cx| {
6499 if workspace.project().read(cx).remote_id() == Some(project_id) {
6500 Some(window_handle)
6501 } else {
6502 None
6503 }
6504 })
6505 .unwrap_or(None)
6506 })
6507 });
6508
6509 let workspace = if let Some(existing_workspace) = existing_workspace {
6510 existing_workspace
6511 } else {
6512 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
6513 let room = active_call
6514 .read_with(cx, |call, _| call.room().cloned())?
6515 .ok_or_else(|| anyhow!("not in a call"))?;
6516 let project = room
6517 .update(cx, |room, cx| {
6518 room.join_project(
6519 project_id,
6520 app_state.languages.clone(),
6521 app_state.fs.clone(),
6522 cx,
6523 )
6524 })?
6525 .await?;
6526
6527 let window_bounds_override = window_bounds_env_override();
6528 cx.update(|cx| {
6529 let mut options = (app_state.build_window_options)(None, cx);
6530 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
6531 cx.open_window(options, |window, cx| {
6532 cx.new(|cx| {
6533 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
6534 })
6535 })
6536 })??
6537 };
6538
6539 workspace.update(cx, |workspace, window, cx| {
6540 cx.activate(true);
6541 window.activate_window();
6542
6543 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
6544 let follow_peer_id = room
6545 .read(cx)
6546 .remote_participants()
6547 .iter()
6548 .find(|(_, participant)| participant.user.id == follow_user_id)
6549 .map(|(_, p)| p.peer_id)
6550 .or_else(|| {
6551 // If we couldn't follow the given user, follow the host instead.
6552 let collaborator = workspace
6553 .project()
6554 .read(cx)
6555 .collaborators()
6556 .values()
6557 .find(|collaborator| collaborator.is_host)?;
6558 Some(collaborator.peer_id)
6559 });
6560
6561 if let Some(follow_peer_id) = follow_peer_id {
6562 workspace.follow(follow_peer_id, window, cx);
6563 }
6564 }
6565 })?;
6566
6567 anyhow::Ok(())
6568 })
6569}
6570
6571pub fn reload(reload: &Reload, cx: &mut App) {
6572 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
6573 let mut workspace_windows = cx
6574 .windows()
6575 .into_iter()
6576 .filter_map(|window| window.downcast::<Workspace>())
6577 .collect::<Vec<_>>();
6578
6579 // If multiple windows have unsaved changes, and need a save prompt,
6580 // prompt in the active window before switching to a different window.
6581 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
6582
6583 let mut prompt = None;
6584 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
6585 prompt = window
6586 .update(cx, |_, window, cx| {
6587 window.prompt(
6588 PromptLevel::Info,
6589 "Are you sure you want to restart?",
6590 None,
6591 &["Restart", "Cancel"],
6592 cx,
6593 )
6594 })
6595 .ok();
6596 }
6597
6598 let binary_path = reload.binary_path.clone();
6599 cx.spawn(async move |cx| {
6600 if let Some(prompt) = prompt {
6601 let answer = prompt.await?;
6602 if answer != 0 {
6603 return Ok(());
6604 }
6605 }
6606
6607 // If the user cancels any save prompt, then keep the app open.
6608 for window in workspace_windows {
6609 if let Ok(should_close) = window.update(cx, |workspace, window, cx| {
6610 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
6611 }) {
6612 if !should_close.await? {
6613 return Ok(());
6614 }
6615 }
6616 }
6617
6618 cx.update(|cx| cx.restart(binary_path))
6619 })
6620 .detach_and_log_err(cx);
6621}
6622
6623fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
6624 let mut parts = value.split(',');
6625 let x: usize = parts.next()?.parse().ok()?;
6626 let y: usize = parts.next()?.parse().ok()?;
6627 Some(point(px(x as f32), px(y as f32)))
6628}
6629
6630fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
6631 let mut parts = value.split(',');
6632 let width: usize = parts.next()?.parse().ok()?;
6633 let height: usize = parts.next()?.parse().ok()?;
6634 Some(size(px(width as f32), px(height as f32)))
6635}
6636
6637pub fn client_side_decorations(
6638 element: impl IntoElement,
6639 window: &mut Window,
6640 cx: &mut App,
6641) -> Stateful<Div> {
6642 const BORDER_SIZE: Pixels = px(1.0);
6643 let decorations = window.window_decorations();
6644
6645 if matches!(decorations, Decorations::Client { .. }) {
6646 window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6647 }
6648
6649 struct GlobalResizeEdge(ResizeEdge);
6650 impl Global for GlobalResizeEdge {}
6651
6652 div()
6653 .id("window-backdrop")
6654 .bg(transparent_black())
6655 .map(|div| match decorations {
6656 Decorations::Server => div,
6657 Decorations::Client { tiling, .. } => div
6658 .when(!(tiling.top || tiling.right), |div| {
6659 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6660 })
6661 .when(!(tiling.top || tiling.left), |div| {
6662 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6663 })
6664 .when(!(tiling.bottom || tiling.right), |div| {
6665 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6666 })
6667 .when(!(tiling.bottom || tiling.left), |div| {
6668 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6669 })
6670 .when(!tiling.top, |div| {
6671 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6672 })
6673 .when(!tiling.bottom, |div| {
6674 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6675 })
6676 .when(!tiling.left, |div| {
6677 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6678 })
6679 .when(!tiling.right, |div| {
6680 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6681 })
6682 .on_mouse_move(move |e, window, cx| {
6683 let size = window.window_bounds().get_bounds().size;
6684 let pos = e.position;
6685
6686 let new_edge =
6687 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6688
6689 let edge = cx.try_global::<GlobalResizeEdge>();
6690 if new_edge != edge.map(|edge| edge.0) {
6691 window
6692 .window_handle()
6693 .update(cx, |workspace, _, cx| {
6694 cx.notify(workspace.entity_id());
6695 })
6696 .ok();
6697 }
6698 })
6699 .on_mouse_down(MouseButton::Left, move |e, window, _| {
6700 let size = window.window_bounds().get_bounds().size;
6701 let pos = e.position;
6702
6703 let edge = match resize_edge(
6704 pos,
6705 theme::CLIENT_SIDE_DECORATION_SHADOW,
6706 size,
6707 tiling,
6708 ) {
6709 Some(value) => value,
6710 None => return,
6711 };
6712
6713 window.start_window_resize(edge);
6714 }),
6715 })
6716 .size_full()
6717 .child(
6718 div()
6719 .cursor(CursorStyle::Arrow)
6720 .map(|div| match decorations {
6721 Decorations::Server => div,
6722 Decorations::Client { tiling } => div
6723 .border_color(cx.theme().colors().border)
6724 .when(!(tiling.top || tiling.right), |div| {
6725 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6726 })
6727 .when(!(tiling.top || tiling.left), |div| {
6728 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6729 })
6730 .when(!(tiling.bottom || tiling.right), |div| {
6731 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6732 })
6733 .when(!(tiling.bottom || tiling.left), |div| {
6734 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6735 })
6736 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6737 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6738 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6739 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6740 .when(!tiling.is_tiled(), |div| {
6741 div.shadow(smallvec::smallvec![gpui::BoxShadow {
6742 color: Hsla {
6743 h: 0.,
6744 s: 0.,
6745 l: 0.,
6746 a: 0.4,
6747 },
6748 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6749 spread_radius: px(0.),
6750 offset: point(px(0.0), px(0.0)),
6751 }])
6752 }),
6753 })
6754 .on_mouse_move(|_e, _, cx| {
6755 cx.stop_propagation();
6756 })
6757 .size_full()
6758 .child(element),
6759 )
6760 .map(|div| match decorations {
6761 Decorations::Server => div,
6762 Decorations::Client { tiling, .. } => div.child(
6763 canvas(
6764 |_bounds, window, _| {
6765 window.insert_hitbox(
6766 Bounds::new(
6767 point(px(0.0), px(0.0)),
6768 window.window_bounds().get_bounds().size,
6769 ),
6770 false,
6771 )
6772 },
6773 move |_bounds, hitbox, window, cx| {
6774 let mouse = window.mouse_position();
6775 let size = window.window_bounds().get_bounds().size;
6776 let Some(edge) =
6777 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6778 else {
6779 return;
6780 };
6781 cx.set_global(GlobalResizeEdge(edge));
6782 window.set_cursor_style(
6783 match edge {
6784 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6785 ResizeEdge::Left | ResizeEdge::Right => {
6786 CursorStyle::ResizeLeftRight
6787 }
6788 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6789 CursorStyle::ResizeUpLeftDownRight
6790 }
6791 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6792 CursorStyle::ResizeUpRightDownLeft
6793 }
6794 },
6795 Some(&hitbox),
6796 );
6797 },
6798 )
6799 .size_full()
6800 .absolute(),
6801 ),
6802 })
6803}
6804
6805fn resize_edge(
6806 pos: Point<Pixels>,
6807 shadow_size: Pixels,
6808 window_size: Size<Pixels>,
6809 tiling: Tiling,
6810) -> Option<ResizeEdge> {
6811 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6812 if bounds.contains(&pos) {
6813 return None;
6814 }
6815
6816 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6817 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6818 if !tiling.top && top_left_bounds.contains(&pos) {
6819 return Some(ResizeEdge::TopLeft);
6820 }
6821
6822 let top_right_bounds = Bounds::new(
6823 Point::new(window_size.width - corner_size.width, px(0.)),
6824 corner_size,
6825 );
6826 if !tiling.top && top_right_bounds.contains(&pos) {
6827 return Some(ResizeEdge::TopRight);
6828 }
6829
6830 let bottom_left_bounds = Bounds::new(
6831 Point::new(px(0.), window_size.height - corner_size.height),
6832 corner_size,
6833 );
6834 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6835 return Some(ResizeEdge::BottomLeft);
6836 }
6837
6838 let bottom_right_bounds = Bounds::new(
6839 Point::new(
6840 window_size.width - corner_size.width,
6841 window_size.height - corner_size.height,
6842 ),
6843 corner_size,
6844 );
6845 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6846 return Some(ResizeEdge::BottomRight);
6847 }
6848
6849 if !tiling.top && pos.y < shadow_size {
6850 Some(ResizeEdge::Top)
6851 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6852 Some(ResizeEdge::Bottom)
6853 } else if !tiling.left && pos.x < shadow_size {
6854 Some(ResizeEdge::Left)
6855 } else if !tiling.right && pos.x > window_size.width - shadow_size {
6856 Some(ResizeEdge::Right)
6857 } else {
6858 None
6859 }
6860}
6861
6862fn join_pane_into_active(
6863 active_pane: &Entity<Pane>,
6864 pane: &Entity<Pane>,
6865 window: &mut Window,
6866 cx: &mut App,
6867) {
6868 if pane == active_pane {
6869 return;
6870 } else if pane.read(cx).items_len() == 0 {
6871 pane.update(cx, |_, cx| {
6872 cx.emit(pane::Event::Remove {
6873 focus_on_pane: None,
6874 });
6875 })
6876 } else {
6877 move_all_items(pane, active_pane, window, cx);
6878 }
6879}
6880
6881fn move_all_items(
6882 from_pane: &Entity<Pane>,
6883 to_pane: &Entity<Pane>,
6884 window: &mut Window,
6885 cx: &mut App,
6886) {
6887 let destination_is_different = from_pane != to_pane;
6888 let mut moved_items = 0;
6889 for (item_ix, item_handle) in from_pane
6890 .read(cx)
6891 .items()
6892 .enumerate()
6893 .map(|(ix, item)| (ix, item.clone()))
6894 .collect::<Vec<_>>()
6895 {
6896 let ix = item_ix - moved_items;
6897 if destination_is_different {
6898 // Close item from previous pane
6899 from_pane.update(cx, |source, cx| {
6900 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
6901 });
6902 moved_items += 1;
6903 }
6904
6905 // This automatically removes duplicate items in the pane
6906 to_pane.update(cx, |destination, cx| {
6907 destination.add_item(item_handle, true, true, None, window, cx);
6908 window.focus(&destination.focus_handle(cx))
6909 });
6910 }
6911}
6912
6913pub fn move_item(
6914 source: &Entity<Pane>,
6915 destination: &Entity<Pane>,
6916 item_id_to_move: EntityId,
6917 destination_index: usize,
6918 window: &mut Window,
6919 cx: &mut App,
6920) {
6921 let Some((item_ix, item_handle)) = source
6922 .read(cx)
6923 .items()
6924 .enumerate()
6925 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6926 .map(|(ix, item)| (ix, item.clone()))
6927 else {
6928 // Tab was closed during drag
6929 return;
6930 };
6931
6932 if source != destination {
6933 // Close item from previous pane
6934 source.update(cx, |source, cx| {
6935 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
6936 });
6937 }
6938
6939 // This automatically removes duplicate items in the pane
6940 destination.update(cx, |destination, cx| {
6941 destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
6942 window.focus(&destination.focus_handle(cx))
6943 });
6944}
6945
6946pub fn move_active_item(
6947 source: &Entity<Pane>,
6948 destination: &Entity<Pane>,
6949 focus_destination: bool,
6950 close_if_empty: bool,
6951 window: &mut Window,
6952 cx: &mut App,
6953) {
6954 if source == destination {
6955 return;
6956 }
6957 let Some(active_item) = source.read(cx).active_item() else {
6958 return;
6959 };
6960 source.update(cx, |source_pane, cx| {
6961 let item_id = active_item.item_id();
6962 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
6963 destination.update(cx, |target_pane, cx| {
6964 target_pane.add_item(
6965 active_item,
6966 focus_destination,
6967 focus_destination,
6968 Some(target_pane.items_len()),
6969 window,
6970 cx,
6971 );
6972 });
6973 });
6974}
6975
6976#[cfg(test)]
6977mod tests {
6978 use std::{cell::RefCell, rc::Rc};
6979
6980 use super::*;
6981 use crate::{
6982 dock::{PanelEvent, test::TestPanel},
6983 item::{
6984 ItemEvent,
6985 test::{TestItem, TestProjectItem},
6986 },
6987 };
6988 use fs::FakeFs;
6989 use gpui::{
6990 DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
6991 UpdateGlobal, VisualTestContext, px,
6992 };
6993 use project::{Project, ProjectEntryId};
6994 use serde_json::json;
6995 use settings::SettingsStore;
6996
6997 #[gpui::test]
6998 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6999 init_test(cx);
7000
7001 let fs = FakeFs::new(cx.executor());
7002 let project = Project::test(fs, [], cx).await;
7003 let (workspace, cx) =
7004 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7005
7006 // Adding an item with no ambiguity renders the tab without detail.
7007 let item1 = cx.new(|cx| {
7008 let mut item = TestItem::new(cx);
7009 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
7010 item
7011 });
7012 workspace.update_in(cx, |workspace, window, cx| {
7013 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7014 });
7015 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
7016
7017 // Adding an item that creates ambiguity increases the level of detail on
7018 // both tabs.
7019 let item2 = cx.new_window_entity(|_window, cx| {
7020 let mut item = TestItem::new(cx);
7021 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7022 item
7023 });
7024 workspace.update_in(cx, |workspace, window, cx| {
7025 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7026 });
7027 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7028 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7029
7030 // Adding an item that creates ambiguity increases the level of detail only
7031 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
7032 // we stop at the highest detail available.
7033 let item3 = cx.new(|cx| {
7034 let mut item = TestItem::new(cx);
7035 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7036 item
7037 });
7038 workspace.update_in(cx, |workspace, window, cx| {
7039 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7040 });
7041 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7042 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7043 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7044 }
7045
7046 #[gpui::test]
7047 async fn test_tracking_active_path(cx: &mut TestAppContext) {
7048 init_test(cx);
7049
7050 let fs = FakeFs::new(cx.executor());
7051 fs.insert_tree(
7052 "/root1",
7053 json!({
7054 "one.txt": "",
7055 "two.txt": "",
7056 }),
7057 )
7058 .await;
7059 fs.insert_tree(
7060 "/root2",
7061 json!({
7062 "three.txt": "",
7063 }),
7064 )
7065 .await;
7066
7067 let project = Project::test(fs, ["root1".as_ref()], cx).await;
7068 let (workspace, cx) =
7069 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7070 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7071 let worktree_id = project.update(cx, |project, cx| {
7072 project.worktrees(cx).next().unwrap().read(cx).id()
7073 });
7074
7075 let item1 = cx.new(|cx| {
7076 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
7077 });
7078 let item2 = cx.new(|cx| {
7079 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
7080 });
7081
7082 // Add an item to an empty pane
7083 workspace.update_in(cx, |workspace, window, cx| {
7084 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
7085 });
7086 project.update(cx, |project, cx| {
7087 assert_eq!(
7088 project.active_entry(),
7089 project
7090 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7091 .map(|e| e.id)
7092 );
7093 });
7094 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7095
7096 // Add a second item to a non-empty pane
7097 workspace.update_in(cx, |workspace, window, cx| {
7098 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
7099 });
7100 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
7101 project.update(cx, |project, cx| {
7102 assert_eq!(
7103 project.active_entry(),
7104 project
7105 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
7106 .map(|e| e.id)
7107 );
7108 });
7109
7110 // Close the active item
7111 pane.update_in(cx, |pane, window, cx| {
7112 pane.close_active_item(&Default::default(), window, cx)
7113 .unwrap()
7114 })
7115 .await
7116 .unwrap();
7117 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7118 project.update(cx, |project, cx| {
7119 assert_eq!(
7120 project.active_entry(),
7121 project
7122 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7123 .map(|e| e.id)
7124 );
7125 });
7126
7127 // Add a project folder
7128 project
7129 .update(cx, |project, cx| {
7130 project.find_or_create_worktree("root2", true, cx)
7131 })
7132 .await
7133 .unwrap();
7134 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
7135
7136 // Remove a project folder
7137 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
7138 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
7139 }
7140
7141 #[gpui::test]
7142 async fn test_close_window(cx: &mut TestAppContext) {
7143 init_test(cx);
7144
7145 let fs = FakeFs::new(cx.executor());
7146 fs.insert_tree("/root", json!({ "one": "" })).await;
7147
7148 let project = Project::test(fs, ["root".as_ref()], cx).await;
7149 let (workspace, cx) =
7150 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7151
7152 // When there are no dirty items, there's nothing to do.
7153 let item1 = cx.new(TestItem::new);
7154 workspace.update_in(cx, |w, window, cx| {
7155 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
7156 });
7157 let task = workspace.update_in(cx, |w, window, cx| {
7158 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7159 });
7160 assert!(task.await.unwrap());
7161
7162 // When there are dirty untitled items, prompt to save each one. If the user
7163 // cancels any prompt, then abort.
7164 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
7165 let item3 = cx.new(|cx| {
7166 TestItem::new(cx)
7167 .with_dirty(true)
7168 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7169 });
7170 workspace.update_in(cx, |w, window, cx| {
7171 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7172 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7173 });
7174 let task = workspace.update_in(cx, |w, window, cx| {
7175 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7176 });
7177 cx.executor().run_until_parked();
7178 cx.simulate_prompt_answer("Cancel"); // cancel save all
7179 cx.executor().run_until_parked();
7180 assert!(!cx.has_pending_prompt());
7181 assert!(!task.await.unwrap());
7182 }
7183
7184 #[gpui::test]
7185 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
7186 init_test(cx);
7187
7188 // Register TestItem as a serializable item
7189 cx.update(|cx| {
7190 register_serializable_item::<TestItem>(cx);
7191 });
7192
7193 let fs = FakeFs::new(cx.executor());
7194 fs.insert_tree("/root", json!({ "one": "" })).await;
7195
7196 let project = Project::test(fs, ["root".as_ref()], cx).await;
7197 let (workspace, cx) =
7198 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7199
7200 // When there are dirty untitled items, but they can serialize, then there is no prompt.
7201 let item1 = cx.new(|cx| {
7202 TestItem::new(cx)
7203 .with_dirty(true)
7204 .with_serialize(|| Some(Task::ready(Ok(()))))
7205 });
7206 let item2 = cx.new(|cx| {
7207 TestItem::new(cx)
7208 .with_dirty(true)
7209 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7210 .with_serialize(|| Some(Task::ready(Ok(()))))
7211 });
7212 workspace.update_in(cx, |w, window, cx| {
7213 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7214 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7215 });
7216 let task = workspace.update_in(cx, |w, window, cx| {
7217 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7218 });
7219 assert!(task.await.unwrap());
7220 }
7221
7222 #[gpui::test]
7223 async fn test_close_pane_items(cx: &mut TestAppContext) {
7224 init_test(cx);
7225
7226 let fs = FakeFs::new(cx.executor());
7227
7228 let project = Project::test(fs, None, cx).await;
7229 let (workspace, cx) =
7230 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7231
7232 let item1 = cx.new(|cx| {
7233 TestItem::new(cx)
7234 .with_dirty(true)
7235 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7236 });
7237 let item2 = cx.new(|cx| {
7238 TestItem::new(cx)
7239 .with_dirty(true)
7240 .with_conflict(true)
7241 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7242 });
7243 let item3 = cx.new(|cx| {
7244 TestItem::new(cx)
7245 .with_dirty(true)
7246 .with_conflict(true)
7247 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7248 });
7249 let item4 = cx.new(|cx| {
7250 TestItem::new(cx).with_dirty(true).with_project_items(&[{
7251 let project_item = TestProjectItem::new_untitled(cx);
7252 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7253 project_item
7254 }])
7255 });
7256 let pane = workspace.update_in(cx, |workspace, window, cx| {
7257 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7258 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7259 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7260 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7261 workspace.active_pane().clone()
7262 });
7263
7264 let close_items = pane.update_in(cx, |pane, window, cx| {
7265 pane.activate_item(1, true, true, window, cx);
7266 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7267 let item1_id = item1.item_id();
7268 let item3_id = item3.item_id();
7269 let item4_id = item4.item_id();
7270 pane.close_items(window, cx, SaveIntent::Close, move |id| {
7271 [item1_id, item3_id, item4_id].contains(&id)
7272 })
7273 });
7274 cx.executor().run_until_parked();
7275
7276 assert!(cx.has_pending_prompt());
7277 cx.simulate_prompt_answer("Save all");
7278
7279 cx.executor().run_until_parked();
7280
7281 // Item 1 is saved. There's a prompt to save item 3.
7282 pane.update(cx, |pane, cx| {
7283 assert_eq!(item1.read(cx).save_count, 1);
7284 assert_eq!(item1.read(cx).save_as_count, 0);
7285 assert_eq!(item1.read(cx).reload_count, 0);
7286 assert_eq!(pane.items_len(), 3);
7287 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7288 });
7289 assert!(cx.has_pending_prompt());
7290
7291 // Cancel saving item 3.
7292 cx.simulate_prompt_answer("Discard");
7293 cx.executor().run_until_parked();
7294
7295 // Item 3 is reloaded. There's a prompt to save item 4.
7296 pane.update(cx, |pane, cx| {
7297 assert_eq!(item3.read(cx).save_count, 0);
7298 assert_eq!(item3.read(cx).save_as_count, 0);
7299 assert_eq!(item3.read(cx).reload_count, 1);
7300 assert_eq!(pane.items_len(), 2);
7301 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7302 });
7303
7304 // There's a prompt for a path for item 4.
7305 cx.simulate_new_path_selection(|_| Some(Default::default()));
7306 close_items.await.unwrap();
7307
7308 // The requested items are closed.
7309 pane.update(cx, |pane, cx| {
7310 assert_eq!(item4.read(cx).save_count, 0);
7311 assert_eq!(item4.read(cx).save_as_count, 1);
7312 assert_eq!(item4.read(cx).reload_count, 0);
7313 assert_eq!(pane.items_len(), 1);
7314 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7315 });
7316 }
7317
7318 #[gpui::test]
7319 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7320 init_test(cx);
7321
7322 let fs = FakeFs::new(cx.executor());
7323 let project = Project::test(fs, [], cx).await;
7324 let (workspace, cx) =
7325 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7326
7327 // Create several workspace items with single project entries, and two
7328 // workspace items with multiple project entries.
7329 let single_entry_items = (0..=4)
7330 .map(|project_entry_id| {
7331 cx.new(|cx| {
7332 TestItem::new(cx)
7333 .with_dirty(true)
7334 .with_project_items(&[dirty_project_item(
7335 project_entry_id,
7336 &format!("{project_entry_id}.txt"),
7337 cx,
7338 )])
7339 })
7340 })
7341 .collect::<Vec<_>>();
7342 let item_2_3 = cx.new(|cx| {
7343 TestItem::new(cx)
7344 .with_dirty(true)
7345 .with_singleton(false)
7346 .with_project_items(&[
7347 single_entry_items[2].read(cx).project_items[0].clone(),
7348 single_entry_items[3].read(cx).project_items[0].clone(),
7349 ])
7350 });
7351 let item_3_4 = cx.new(|cx| {
7352 TestItem::new(cx)
7353 .with_dirty(true)
7354 .with_singleton(false)
7355 .with_project_items(&[
7356 single_entry_items[3].read(cx).project_items[0].clone(),
7357 single_entry_items[4].read(cx).project_items[0].clone(),
7358 ])
7359 });
7360
7361 // Create two panes that contain the following project entries:
7362 // left pane:
7363 // multi-entry items: (2, 3)
7364 // single-entry items: 0, 2, 3, 4
7365 // right pane:
7366 // single-entry items: 4, 1
7367 // multi-entry items: (3, 4)
7368 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
7369 let left_pane = workspace.active_pane().clone();
7370 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7371 workspace.add_item_to_active_pane(
7372 single_entry_items[0].boxed_clone(),
7373 None,
7374 true,
7375 window,
7376 cx,
7377 );
7378 workspace.add_item_to_active_pane(
7379 single_entry_items[2].boxed_clone(),
7380 None,
7381 true,
7382 window,
7383 cx,
7384 );
7385 workspace.add_item_to_active_pane(
7386 single_entry_items[3].boxed_clone(),
7387 None,
7388 true,
7389 window,
7390 cx,
7391 );
7392 workspace.add_item_to_active_pane(
7393 single_entry_items[4].boxed_clone(),
7394 None,
7395 true,
7396 window,
7397 cx,
7398 );
7399
7400 let right_pane = workspace
7401 .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7402 .unwrap();
7403
7404 right_pane.update(cx, |pane, cx| {
7405 pane.add_item(
7406 single_entry_items[1].boxed_clone(),
7407 true,
7408 true,
7409 None,
7410 window,
7411 cx,
7412 );
7413 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
7414 });
7415
7416 (left_pane, right_pane)
7417 });
7418
7419 cx.focus(&right_pane);
7420
7421 let mut close = right_pane.update_in(cx, |pane, window, cx| {
7422 pane.close_all_items(&CloseAllItems::default(), window, cx)
7423 .unwrap()
7424 });
7425 cx.executor().run_until_parked();
7426
7427 let msg = cx.pending_prompt().unwrap().0;
7428 assert!(msg.contains("1.txt"));
7429 assert!(!msg.contains("2.txt"));
7430 assert!(!msg.contains("3.txt"));
7431 assert!(!msg.contains("4.txt"));
7432
7433 cx.simulate_prompt_answer("Cancel");
7434 close.await.unwrap();
7435
7436 left_pane
7437 .update_in(cx, |left_pane, window, cx| {
7438 left_pane.close_item_by_id(
7439 single_entry_items[3].entity_id(),
7440 SaveIntent::Skip,
7441 window,
7442 cx,
7443 )
7444 })
7445 .await
7446 .unwrap();
7447
7448 close = right_pane.update_in(cx, |pane, window, cx| {
7449 pane.close_all_items(&CloseAllItems::default(), window, cx)
7450 .unwrap()
7451 });
7452 cx.executor().run_until_parked();
7453
7454 let details = cx.pending_prompt().unwrap().1;
7455 assert!(details.contains("1.txt"));
7456 assert!(!details.contains("2.txt"));
7457 assert!(details.contains("3.txt"));
7458 // ideally this assertion could be made, but today we can only
7459 // save whole items not project items, so the orphaned item 3 causes
7460 // 4 to be saved too.
7461 // assert!(!details.contains("4.txt"));
7462
7463 cx.simulate_prompt_answer("Save all");
7464
7465 cx.executor().run_until_parked();
7466 close.await.unwrap();
7467 right_pane.update(cx, |pane, _| {
7468 assert_eq!(pane.items_len(), 0);
7469 });
7470 }
7471
7472 #[gpui::test]
7473 async fn test_autosave(cx: &mut gpui::TestAppContext) {
7474 init_test(cx);
7475
7476 let fs = FakeFs::new(cx.executor());
7477 let project = Project::test(fs, [], cx).await;
7478 let (workspace, cx) =
7479 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7480 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7481
7482 let item = cx.new(|cx| {
7483 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7484 });
7485 let item_id = item.entity_id();
7486 workspace.update_in(cx, |workspace, window, cx| {
7487 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7488 });
7489
7490 // Autosave on window change.
7491 item.update(cx, |item, cx| {
7492 SettingsStore::update_global(cx, |settings, cx| {
7493 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7494 settings.autosave = Some(AutosaveSetting::OnWindowChange);
7495 })
7496 });
7497 item.is_dirty = true;
7498 });
7499
7500 // Deactivating the window saves the file.
7501 cx.deactivate_window();
7502 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7503
7504 // Re-activating the window doesn't save the file.
7505 cx.update(|window, _| window.activate_window());
7506 cx.executor().run_until_parked();
7507 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7508
7509 // Autosave on focus change.
7510 item.update_in(cx, |item, window, cx| {
7511 cx.focus_self(window);
7512 SettingsStore::update_global(cx, |settings, cx| {
7513 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7514 settings.autosave = Some(AutosaveSetting::OnFocusChange);
7515 })
7516 });
7517 item.is_dirty = true;
7518 });
7519
7520 // Blurring the item saves the file.
7521 item.update_in(cx, |_, window, _| window.blur());
7522 cx.executor().run_until_parked();
7523 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
7524
7525 // Deactivating the window still saves the file.
7526 item.update_in(cx, |item, window, cx| {
7527 cx.focus_self(window);
7528 item.is_dirty = true;
7529 });
7530 cx.deactivate_window();
7531 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7532
7533 // Autosave after delay.
7534 item.update(cx, |item, cx| {
7535 SettingsStore::update_global(cx, |settings, cx| {
7536 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7537 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
7538 })
7539 });
7540 item.is_dirty = true;
7541 cx.emit(ItemEvent::Edit);
7542 });
7543
7544 // Delay hasn't fully expired, so the file is still dirty and unsaved.
7545 cx.executor().advance_clock(Duration::from_millis(250));
7546 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7547
7548 // After delay expires, the file is saved.
7549 cx.executor().advance_clock(Duration::from_millis(250));
7550 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
7551
7552 // Autosave on focus change, ensuring closing the tab counts as such.
7553 item.update(cx, |item, cx| {
7554 SettingsStore::update_global(cx, |settings, cx| {
7555 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7556 settings.autosave = Some(AutosaveSetting::OnFocusChange);
7557 })
7558 });
7559 item.is_dirty = true;
7560 for project_item in &mut item.project_items {
7561 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7562 }
7563 });
7564
7565 pane.update_in(cx, |pane, window, cx| {
7566 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7567 })
7568 .await
7569 .unwrap();
7570 assert!(!cx.has_pending_prompt());
7571 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7572
7573 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
7574 workspace.update_in(cx, |workspace, window, cx| {
7575 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7576 });
7577 item.update_in(cx, |item, window, cx| {
7578 item.project_items[0].update(cx, |item, _| {
7579 item.entry_id = None;
7580 });
7581 item.is_dirty = true;
7582 window.blur();
7583 });
7584 cx.run_until_parked();
7585 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7586
7587 // Ensure autosave is prevented for deleted files also when closing the buffer.
7588 let _close_items = pane.update_in(cx, |pane, window, cx| {
7589 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7590 });
7591 cx.run_until_parked();
7592 assert!(cx.has_pending_prompt());
7593 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7594 }
7595
7596 #[gpui::test]
7597 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
7598 init_test(cx);
7599
7600 let fs = FakeFs::new(cx.executor());
7601
7602 let project = Project::test(fs, [], cx).await;
7603 let (workspace, cx) =
7604 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7605
7606 let item = cx.new(|cx| {
7607 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7608 });
7609 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7610 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
7611 let toolbar_notify_count = Rc::new(RefCell::new(0));
7612
7613 workspace.update_in(cx, |workspace, window, cx| {
7614 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7615 let toolbar_notification_count = toolbar_notify_count.clone();
7616 cx.observe_in(&toolbar, window, move |_, _, _, _| {
7617 *toolbar_notification_count.borrow_mut() += 1
7618 })
7619 .detach();
7620 });
7621
7622 pane.update(cx, |pane, _| {
7623 assert!(!pane.can_navigate_backward());
7624 assert!(!pane.can_navigate_forward());
7625 });
7626
7627 item.update_in(cx, |item, _, cx| {
7628 item.set_state("one".to_string(), cx);
7629 });
7630
7631 // Toolbar must be notified to re-render the navigation buttons
7632 assert_eq!(*toolbar_notify_count.borrow(), 1);
7633
7634 pane.update(cx, |pane, _| {
7635 assert!(pane.can_navigate_backward());
7636 assert!(!pane.can_navigate_forward());
7637 });
7638
7639 workspace
7640 .update_in(cx, |workspace, window, cx| {
7641 workspace.go_back(pane.downgrade(), window, cx)
7642 })
7643 .await
7644 .unwrap();
7645
7646 assert_eq!(*toolbar_notify_count.borrow(), 2);
7647 pane.update(cx, |pane, _| {
7648 assert!(!pane.can_navigate_backward());
7649 assert!(pane.can_navigate_forward());
7650 });
7651 }
7652
7653 #[gpui::test]
7654 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
7655 init_test(cx);
7656 let fs = FakeFs::new(cx.executor());
7657
7658 let project = Project::test(fs, [], cx).await;
7659 let (workspace, cx) =
7660 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7661
7662 let panel = workspace.update_in(cx, |workspace, window, cx| {
7663 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7664 workspace.add_panel(panel.clone(), window, cx);
7665
7666 workspace
7667 .right_dock()
7668 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
7669
7670 panel
7671 });
7672
7673 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7674 pane.update_in(cx, |pane, window, cx| {
7675 let item = cx.new(TestItem::new);
7676 pane.add_item(Box::new(item), true, true, None, window, cx);
7677 });
7678
7679 // Transfer focus from center to panel
7680 workspace.update_in(cx, |workspace, window, cx| {
7681 workspace.toggle_panel_focus::<TestPanel>(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 // Transfer focus from panel to center
7691 workspace.update_in(cx, |workspace, window, cx| {
7692 workspace.toggle_panel_focus::<TestPanel>(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 // Close the dock
7702 workspace.update_in(cx, |workspace, window, cx| {
7703 workspace.toggle_dock(DockPosition::Right, window, cx);
7704 });
7705
7706 workspace.update_in(cx, |workspace, window, cx| {
7707 assert!(!workspace.right_dock().read(cx).is_open());
7708 assert!(!panel.is_zoomed(window, cx));
7709 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7710 });
7711
7712 // Open the dock
7713 workspace.update_in(cx, |workspace, window, cx| {
7714 workspace.toggle_dock(DockPosition::Right, window, cx);
7715 });
7716
7717 workspace.update_in(cx, |workspace, window, cx| {
7718 assert!(workspace.right_dock().read(cx).is_open());
7719 assert!(!panel.is_zoomed(window, cx));
7720 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7721 });
7722
7723 // Focus and zoom panel
7724 panel.update_in(cx, |panel, window, cx| {
7725 cx.focus_self(window);
7726 panel.set_zoomed(true, 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 // Transfer focus to the center closes the dock
7736 workspace.update_in(cx, |workspace, window, cx| {
7737 workspace.toggle_panel_focus::<TestPanel>(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!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7744 });
7745
7746 // Transferring focus back to the panel keeps it zoomed
7747 workspace.update_in(cx, |workspace, window, cx| {
7748 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7749 });
7750
7751 workspace.update_in(cx, |workspace, window, cx| {
7752 assert!(workspace.right_dock().read(cx).is_open());
7753 assert!(panel.is_zoomed(window, cx));
7754 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7755 });
7756
7757 // Close the dock while it is zoomed
7758 workspace.update_in(cx, |workspace, window, cx| {
7759 workspace.toggle_dock(DockPosition::Right, window, cx)
7760 });
7761
7762 workspace.update_in(cx, |workspace, window, cx| {
7763 assert!(!workspace.right_dock().read(cx).is_open());
7764 assert!(panel.is_zoomed(window, cx));
7765 assert!(workspace.zoomed.is_none());
7766 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7767 });
7768
7769 // Opening the dock, when it's zoomed, retains focus
7770 workspace.update_in(cx, |workspace, window, cx| {
7771 workspace.toggle_dock(DockPosition::Right, window, cx)
7772 });
7773
7774 workspace.update_in(cx, |workspace, window, cx| {
7775 assert!(workspace.right_dock().read(cx).is_open());
7776 assert!(panel.is_zoomed(window, cx));
7777 assert!(workspace.zoomed.is_some());
7778 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7779 });
7780
7781 // Unzoom and close the panel, zoom the active pane.
7782 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
7783 workspace.update_in(cx, |workspace, window, cx| {
7784 workspace.toggle_dock(DockPosition::Right, window, cx)
7785 });
7786 pane.update_in(cx, |pane, window, cx| {
7787 pane.toggle_zoom(&Default::default(), window, cx)
7788 });
7789
7790 // Opening a dock unzooms the pane.
7791 workspace.update_in(cx, |workspace, window, cx| {
7792 workspace.toggle_dock(DockPosition::Right, window, cx)
7793 });
7794 workspace.update_in(cx, |workspace, window, cx| {
7795 let pane = pane.read(cx);
7796 assert!(!pane.is_zoomed());
7797 assert!(!pane.focus_handle(cx).is_focused(window));
7798 assert!(workspace.right_dock().read(cx).is_open());
7799 assert!(workspace.zoomed.is_none());
7800 });
7801 }
7802
7803 #[gpui::test]
7804 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7805 init_test(cx);
7806
7807 let fs = FakeFs::new(cx.executor());
7808
7809 let project = Project::test(fs, None, cx).await;
7810 let (workspace, cx) =
7811 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7812
7813 // Let's arrange the panes like this:
7814 //
7815 // +-----------------------+
7816 // | top |
7817 // +------+--------+-------+
7818 // | left | center | right |
7819 // +------+--------+-------+
7820 // | bottom |
7821 // +-----------------------+
7822
7823 let top_item = cx.new(|cx| {
7824 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7825 });
7826 let bottom_item = cx.new(|cx| {
7827 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7828 });
7829 let left_item = cx.new(|cx| {
7830 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7831 });
7832 let right_item = cx.new(|cx| {
7833 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7834 });
7835 let center_item = cx.new(|cx| {
7836 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7837 });
7838
7839 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7840 let top_pane_id = workspace.active_pane().entity_id();
7841 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
7842 workspace.split_pane(
7843 workspace.active_pane().clone(),
7844 SplitDirection::Down,
7845 window,
7846 cx,
7847 );
7848 top_pane_id
7849 });
7850 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7851 let bottom_pane_id = workspace.active_pane().entity_id();
7852 workspace.add_item_to_active_pane(
7853 Box::new(bottom_item.clone()),
7854 None,
7855 false,
7856 window,
7857 cx,
7858 );
7859 workspace.split_pane(
7860 workspace.active_pane().clone(),
7861 SplitDirection::Up,
7862 window,
7863 cx,
7864 );
7865 bottom_pane_id
7866 });
7867 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7868 let left_pane_id = workspace.active_pane().entity_id();
7869 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
7870 workspace.split_pane(
7871 workspace.active_pane().clone(),
7872 SplitDirection::Right,
7873 window,
7874 cx,
7875 );
7876 left_pane_id
7877 });
7878 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7879 let right_pane_id = workspace.active_pane().entity_id();
7880 workspace.add_item_to_active_pane(
7881 Box::new(right_item.clone()),
7882 None,
7883 false,
7884 window,
7885 cx,
7886 );
7887 workspace.split_pane(
7888 workspace.active_pane().clone(),
7889 SplitDirection::Left,
7890 window,
7891 cx,
7892 );
7893 right_pane_id
7894 });
7895 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7896 let center_pane_id = workspace.active_pane().entity_id();
7897 workspace.add_item_to_active_pane(
7898 Box::new(center_item.clone()),
7899 None,
7900 false,
7901 window,
7902 cx,
7903 );
7904 center_pane_id
7905 });
7906 cx.executor().run_until_parked();
7907
7908 workspace.update_in(cx, |workspace, window, cx| {
7909 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7910
7911 // Join into next from center pane into right
7912 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7913 });
7914
7915 workspace.update_in(cx, |workspace, window, cx| {
7916 let active_pane = workspace.active_pane();
7917 assert_eq!(right_pane_id, active_pane.entity_id());
7918 assert_eq!(2, active_pane.read(cx).items_len());
7919 let item_ids_in_pane =
7920 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7921 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7922 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7923
7924 // Join into next from right pane into bottom
7925 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7926 });
7927
7928 workspace.update_in(cx, |workspace, window, cx| {
7929 let active_pane = workspace.active_pane();
7930 assert_eq!(bottom_pane_id, active_pane.entity_id());
7931 assert_eq!(3, active_pane.read(cx).items_len());
7932 let item_ids_in_pane =
7933 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7934 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7935 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7936 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7937
7938 // Join into next from bottom pane into left
7939 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7940 });
7941
7942 workspace.update_in(cx, |workspace, window, cx| {
7943 let active_pane = workspace.active_pane();
7944 assert_eq!(left_pane_id, active_pane.entity_id());
7945 assert_eq!(4, active_pane.read(cx).items_len());
7946 let item_ids_in_pane =
7947 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7948 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7949 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7950 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7951 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7952
7953 // Join into next from left pane into top
7954 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7955 });
7956
7957 workspace.update_in(cx, |workspace, window, cx| {
7958 let active_pane = workspace.active_pane();
7959 assert_eq!(top_pane_id, active_pane.entity_id());
7960 assert_eq!(5, active_pane.read(cx).items_len());
7961 let item_ids_in_pane =
7962 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7963 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7964 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7965 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7966 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7967 assert!(item_ids_in_pane.contains(&top_item.item_id()));
7968
7969 // Single pane left: no-op
7970 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
7971 });
7972
7973 workspace.update(cx, |workspace, _cx| {
7974 let active_pane = workspace.active_pane();
7975 assert_eq!(top_pane_id, active_pane.entity_id());
7976 });
7977 }
7978
7979 fn add_an_item_to_active_pane(
7980 cx: &mut VisualTestContext,
7981 workspace: &Entity<Workspace>,
7982 item_id: u64,
7983 ) -> Entity<TestItem> {
7984 let item = cx.new(|cx| {
7985 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7986 item_id,
7987 "item{item_id}.txt",
7988 cx,
7989 )])
7990 });
7991 workspace.update_in(cx, |workspace, window, cx| {
7992 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
7993 });
7994 return item;
7995 }
7996
7997 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
7998 return workspace.update_in(cx, |workspace, window, cx| {
7999 let new_pane = workspace.split_pane(
8000 workspace.active_pane().clone(),
8001 SplitDirection::Right,
8002 window,
8003 cx,
8004 );
8005 new_pane
8006 });
8007 }
8008
8009 #[gpui::test]
8010 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
8011 init_test(cx);
8012 let fs = FakeFs::new(cx.executor());
8013 let project = Project::test(fs, None, cx).await;
8014 let (workspace, cx) =
8015 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8016
8017 add_an_item_to_active_pane(cx, &workspace, 1);
8018 split_pane(cx, &workspace);
8019 add_an_item_to_active_pane(cx, &workspace, 2);
8020 split_pane(cx, &workspace); // empty pane
8021 split_pane(cx, &workspace);
8022 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
8023
8024 cx.executor().run_until_parked();
8025
8026 workspace.update(cx, |workspace, cx| {
8027 let num_panes = workspace.panes().len();
8028 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8029 let active_item = workspace
8030 .active_pane()
8031 .read(cx)
8032 .active_item()
8033 .expect("item is in focus");
8034
8035 assert_eq!(num_panes, 4);
8036 assert_eq!(num_items_in_current_pane, 1);
8037 assert_eq!(active_item.item_id(), last_item.item_id());
8038 });
8039
8040 workspace.update_in(cx, |workspace, window, cx| {
8041 workspace.join_all_panes(window, cx);
8042 });
8043
8044 workspace.update(cx, |workspace, cx| {
8045 let num_panes = workspace.panes().len();
8046 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8047 let active_item = workspace
8048 .active_pane()
8049 .read(cx)
8050 .active_item()
8051 .expect("item is in focus");
8052
8053 assert_eq!(num_panes, 1);
8054 assert_eq!(num_items_in_current_pane, 3);
8055 assert_eq!(active_item.item_id(), last_item.item_id());
8056 });
8057 }
8058 struct TestModal(FocusHandle);
8059
8060 impl TestModal {
8061 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
8062 Self(cx.focus_handle())
8063 }
8064 }
8065
8066 impl EventEmitter<DismissEvent> for TestModal {}
8067
8068 impl Focusable for TestModal {
8069 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8070 self.0.clone()
8071 }
8072 }
8073
8074 impl ModalView for TestModal {}
8075
8076 impl Render for TestModal {
8077 fn render(
8078 &mut self,
8079 _window: &mut Window,
8080 _cx: &mut Context<TestModal>,
8081 ) -> impl IntoElement {
8082 div().track_focus(&self.0)
8083 }
8084 }
8085
8086 #[gpui::test]
8087 async fn test_panels(cx: &mut gpui::TestAppContext) {
8088 init_test(cx);
8089 let fs = FakeFs::new(cx.executor());
8090
8091 let project = Project::test(fs, [], cx).await;
8092 let (workspace, cx) =
8093 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8094
8095 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
8096 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
8097 workspace.add_panel(panel_1.clone(), window, cx);
8098 workspace.toggle_dock(DockPosition::Left, window, cx);
8099 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8100 workspace.add_panel(panel_2.clone(), window, cx);
8101 workspace.toggle_dock(DockPosition::Right, window, cx);
8102
8103 let left_dock = workspace.left_dock();
8104 assert_eq!(
8105 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8106 panel_1.panel_id()
8107 );
8108 assert_eq!(
8109 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8110 panel_1.size(window, cx)
8111 );
8112
8113 left_dock.update(cx, |left_dock, cx| {
8114 left_dock.resize_active_panel(Some(px(1337.)), window, cx)
8115 });
8116 assert_eq!(
8117 workspace
8118 .right_dock()
8119 .read(cx)
8120 .visible_panel()
8121 .unwrap()
8122 .panel_id(),
8123 panel_2.panel_id(),
8124 );
8125
8126 (panel_1, panel_2)
8127 });
8128
8129 // Move panel_1 to the right
8130 panel_1.update_in(cx, |panel_1, window, cx| {
8131 panel_1.set_position(DockPosition::Right, window, cx)
8132 });
8133
8134 workspace.update_in(cx, |workspace, window, cx| {
8135 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
8136 // Since it was the only panel on the left, the left dock should now be closed.
8137 assert!(!workspace.left_dock().read(cx).is_open());
8138 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
8139 let right_dock = workspace.right_dock();
8140 assert_eq!(
8141 right_dock.read(cx).visible_panel().unwrap().panel_id(),
8142 panel_1.panel_id()
8143 );
8144 assert_eq!(
8145 right_dock.read(cx).active_panel_size(window, cx).unwrap(),
8146 px(1337.)
8147 );
8148
8149 // Now we move panel_2 to the left
8150 panel_2.set_position(DockPosition::Left, window, cx);
8151 });
8152
8153 workspace.update(cx, |workspace, cx| {
8154 // Since panel_2 was not visible on the right, we don't open the left dock.
8155 assert!(!workspace.left_dock().read(cx).is_open());
8156 // And the right dock is unaffected in its displaying of panel_1
8157 assert!(workspace.right_dock().read(cx).is_open());
8158 assert_eq!(
8159 workspace
8160 .right_dock()
8161 .read(cx)
8162 .visible_panel()
8163 .unwrap()
8164 .panel_id(),
8165 panel_1.panel_id(),
8166 );
8167 });
8168
8169 // Move panel_1 back to the left
8170 panel_1.update_in(cx, |panel_1, window, cx| {
8171 panel_1.set_position(DockPosition::Left, window, cx)
8172 });
8173
8174 workspace.update_in(cx, |workspace, window, cx| {
8175 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
8176 let left_dock = workspace.left_dock();
8177 assert!(left_dock.read(cx).is_open());
8178 assert_eq!(
8179 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8180 panel_1.panel_id()
8181 );
8182 assert_eq!(
8183 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8184 px(1337.)
8185 );
8186 // And the right dock should be closed as it no longer has any panels.
8187 assert!(!workspace.right_dock().read(cx).is_open());
8188
8189 // Now we move panel_1 to the bottom
8190 panel_1.set_position(DockPosition::Bottom, window, cx);
8191 });
8192
8193 workspace.update_in(cx, |workspace, window, cx| {
8194 // Since panel_1 was visible on the left, we close the left dock.
8195 assert!(!workspace.left_dock().read(cx).is_open());
8196 // The bottom dock is sized based on the panel's default size,
8197 // since the panel orientation changed from vertical to horizontal.
8198 let bottom_dock = workspace.bottom_dock();
8199 assert_eq!(
8200 bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
8201 panel_1.size(window, cx),
8202 );
8203 // Close bottom dock and move panel_1 back to the left.
8204 bottom_dock.update(cx, |bottom_dock, cx| {
8205 bottom_dock.set_open(false, window, cx)
8206 });
8207 panel_1.set_position(DockPosition::Left, window, cx);
8208 });
8209
8210 // Emit activated event on panel 1
8211 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
8212
8213 // Now the left dock is open and panel_1 is active and focused.
8214 workspace.update_in(cx, |workspace, window, cx| {
8215 let left_dock = workspace.left_dock();
8216 assert!(left_dock.read(cx).is_open());
8217 assert_eq!(
8218 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8219 panel_1.panel_id(),
8220 );
8221 assert!(panel_1.focus_handle(cx).is_focused(window));
8222 });
8223
8224 // Emit closed event on panel 2, which is not active
8225 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8226
8227 // Wo don't close the left dock, because panel_2 wasn't the active panel
8228 workspace.update(cx, |workspace, cx| {
8229 let left_dock = workspace.left_dock();
8230 assert!(left_dock.read(cx).is_open());
8231 assert_eq!(
8232 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8233 panel_1.panel_id(),
8234 );
8235 });
8236
8237 // Emitting a ZoomIn event shows the panel as zoomed.
8238 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
8239 workspace.update(cx, |workspace, _| {
8240 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8241 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
8242 });
8243
8244 // Move panel to another dock while it is zoomed
8245 panel_1.update_in(cx, |panel, window, cx| {
8246 panel.set_position(DockPosition::Right, window, cx)
8247 });
8248 workspace.update(cx, |workspace, _| {
8249 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8250
8251 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8252 });
8253
8254 // This is a helper for getting a:
8255 // - valid focus on an element,
8256 // - that isn't a part of the panes and panels system of the Workspace,
8257 // - and doesn't trigger the 'on_focus_lost' API.
8258 let focus_other_view = {
8259 let workspace = workspace.clone();
8260 move |cx: &mut VisualTestContext| {
8261 workspace.update_in(cx, |workspace, window, cx| {
8262 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
8263 workspace.toggle_modal(window, cx, TestModal::new);
8264 workspace.toggle_modal(window, cx, TestModal::new);
8265 } else {
8266 workspace.toggle_modal(window, cx, TestModal::new);
8267 }
8268 })
8269 }
8270 };
8271
8272 // If focus is transferred to another view that's not a panel or another pane, we still show
8273 // the panel as zoomed.
8274 focus_other_view(cx);
8275 workspace.update(cx, |workspace, _| {
8276 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8277 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8278 });
8279
8280 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8281 workspace.update_in(cx, |_workspace, window, cx| {
8282 cx.focus_self(window);
8283 });
8284 workspace.update(cx, |workspace, _| {
8285 assert_eq!(workspace.zoomed, None);
8286 assert_eq!(workspace.zoomed_position, None);
8287 });
8288
8289 // If focus is transferred again to another view that's not a panel or a pane, we won't
8290 // show the panel as zoomed because it wasn't zoomed before.
8291 focus_other_view(cx);
8292 workspace.update(cx, |workspace, _| {
8293 assert_eq!(workspace.zoomed, None);
8294 assert_eq!(workspace.zoomed_position, None);
8295 });
8296
8297 // When the panel is activated, it is zoomed again.
8298 cx.dispatch_action(ToggleRightDock);
8299 workspace.update(cx, |workspace, _| {
8300 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8301 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8302 });
8303
8304 // Emitting a ZoomOut event unzooms the panel.
8305 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8306 workspace.update(cx, |workspace, _| {
8307 assert_eq!(workspace.zoomed, None);
8308 assert_eq!(workspace.zoomed_position, None);
8309 });
8310
8311 // Emit closed event on panel 1, which is active
8312 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8313
8314 // Now the left dock is closed, because panel_1 was the active panel
8315 workspace.update(cx, |workspace, cx| {
8316 let right_dock = workspace.right_dock();
8317 assert!(!right_dock.read(cx).is_open());
8318 });
8319 }
8320
8321 #[gpui::test]
8322 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8323 init_test(cx);
8324
8325 let fs = FakeFs::new(cx.background_executor.clone());
8326 let project = Project::test(fs, [], cx).await;
8327 let (workspace, cx) =
8328 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8329 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8330
8331 let dirty_regular_buffer = cx.new(|cx| {
8332 TestItem::new(cx)
8333 .with_dirty(true)
8334 .with_label("1.txt")
8335 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8336 });
8337 let dirty_regular_buffer_2 = cx.new(|cx| {
8338 TestItem::new(cx)
8339 .with_dirty(true)
8340 .with_label("2.txt")
8341 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8342 });
8343 let dirty_multi_buffer_with_both = cx.new(|cx| {
8344 TestItem::new(cx)
8345 .with_dirty(true)
8346 .with_singleton(false)
8347 .with_label("Fake Project Search")
8348 .with_project_items(&[
8349 dirty_regular_buffer.read(cx).project_items[0].clone(),
8350 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8351 ])
8352 });
8353 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8354 workspace.update_in(cx, |workspace, window, cx| {
8355 workspace.add_item(
8356 pane.clone(),
8357 Box::new(dirty_regular_buffer.clone()),
8358 None,
8359 false,
8360 false,
8361 window,
8362 cx,
8363 );
8364 workspace.add_item(
8365 pane.clone(),
8366 Box::new(dirty_regular_buffer_2.clone()),
8367 None,
8368 false,
8369 false,
8370 window,
8371 cx,
8372 );
8373 workspace.add_item(
8374 pane.clone(),
8375 Box::new(dirty_multi_buffer_with_both.clone()),
8376 None,
8377 false,
8378 false,
8379 window,
8380 cx,
8381 );
8382 });
8383
8384 pane.update_in(cx, |pane, window, cx| {
8385 pane.activate_item(2, true, true, window, cx);
8386 assert_eq!(
8387 pane.active_item().unwrap().item_id(),
8388 multi_buffer_with_both_files_id,
8389 "Should select the multi buffer in the pane"
8390 );
8391 });
8392 let close_all_but_multi_buffer_task = pane
8393 .update_in(cx, |pane, window, cx| {
8394 pane.close_inactive_items(
8395 &CloseInactiveItems {
8396 save_intent: Some(SaveIntent::Save),
8397 close_pinned: true,
8398 },
8399 window,
8400 cx,
8401 )
8402 })
8403 .expect("should have inactive files to close");
8404 cx.background_executor.run_until_parked();
8405 assert!(!cx.has_pending_prompt());
8406 close_all_but_multi_buffer_task
8407 .await
8408 .expect("Closing all buffers but the multi buffer failed");
8409 pane.update(cx, |pane, cx| {
8410 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
8411 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
8412 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
8413 assert_eq!(pane.items_len(), 1);
8414 assert_eq!(
8415 pane.active_item().unwrap().item_id(),
8416 multi_buffer_with_both_files_id,
8417 "Should have only the multi buffer left in the pane"
8418 );
8419 assert!(
8420 dirty_multi_buffer_with_both.read(cx).is_dirty,
8421 "The multi buffer containing the unsaved buffer should still be dirty"
8422 );
8423 });
8424
8425 dirty_regular_buffer.update(cx, |buffer, cx| {
8426 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
8427 });
8428
8429 let close_multi_buffer_task = pane
8430 .update_in(cx, |pane, window, cx| {
8431 pane.close_active_item(
8432 &CloseActiveItem {
8433 save_intent: Some(SaveIntent::Close),
8434 close_pinned: false,
8435 },
8436 window,
8437 cx,
8438 )
8439 })
8440 .expect("should have the multi buffer to close");
8441 cx.background_executor.run_until_parked();
8442 assert!(
8443 cx.has_pending_prompt(),
8444 "Dirty multi buffer should prompt a save dialog"
8445 );
8446 cx.simulate_prompt_answer("Save");
8447 cx.background_executor.run_until_parked();
8448 close_multi_buffer_task
8449 .await
8450 .expect("Closing the multi buffer failed");
8451 pane.update(cx, |pane, cx| {
8452 assert_eq!(
8453 dirty_multi_buffer_with_both.read(cx).save_count,
8454 1,
8455 "Multi buffer item should get be saved"
8456 );
8457 // Test impl does not save inner items, so we do not assert them
8458 assert_eq!(
8459 pane.items_len(),
8460 0,
8461 "No more items should be left in the pane"
8462 );
8463 assert!(pane.active_item().is_none());
8464 });
8465 }
8466
8467 #[gpui::test]
8468 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
8469 cx: &mut TestAppContext,
8470 ) {
8471 init_test(cx);
8472
8473 let fs = FakeFs::new(cx.background_executor.clone());
8474 let project = Project::test(fs, [], cx).await;
8475 let (workspace, cx) =
8476 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8477 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8478
8479 let dirty_regular_buffer = cx.new(|cx| {
8480 TestItem::new(cx)
8481 .with_dirty(true)
8482 .with_label("1.txt")
8483 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8484 });
8485 let dirty_regular_buffer_2 = cx.new(|cx| {
8486 TestItem::new(cx)
8487 .with_dirty(true)
8488 .with_label("2.txt")
8489 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8490 });
8491 let clear_regular_buffer = cx.new(|cx| {
8492 TestItem::new(cx)
8493 .with_label("3.txt")
8494 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8495 });
8496
8497 let dirty_multi_buffer_with_both = cx.new(|cx| {
8498 TestItem::new(cx)
8499 .with_dirty(true)
8500 .with_singleton(false)
8501 .with_label("Fake Project Search")
8502 .with_project_items(&[
8503 dirty_regular_buffer.read(cx).project_items[0].clone(),
8504 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8505 clear_regular_buffer.read(cx).project_items[0].clone(),
8506 ])
8507 });
8508 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8509 workspace.update_in(cx, |workspace, window, cx| {
8510 workspace.add_item(
8511 pane.clone(),
8512 Box::new(dirty_regular_buffer.clone()),
8513 None,
8514 false,
8515 false,
8516 window,
8517 cx,
8518 );
8519 workspace.add_item(
8520 pane.clone(),
8521 Box::new(dirty_multi_buffer_with_both.clone()),
8522 None,
8523 false,
8524 false,
8525 window,
8526 cx,
8527 );
8528 });
8529
8530 pane.update_in(cx, |pane, window, cx| {
8531 pane.activate_item(1, true, true, window, cx);
8532 assert_eq!(
8533 pane.active_item().unwrap().item_id(),
8534 multi_buffer_with_both_files_id,
8535 "Should select the multi buffer in the pane"
8536 );
8537 });
8538 let _close_multi_buffer_task = pane
8539 .update_in(cx, |pane, window, cx| {
8540 pane.close_active_item(
8541 &CloseActiveItem {
8542 save_intent: None,
8543 close_pinned: false,
8544 },
8545 window,
8546 cx,
8547 )
8548 })
8549 .expect("should have active multi buffer to close");
8550 cx.background_executor.run_until_parked();
8551 assert!(
8552 cx.has_pending_prompt(),
8553 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
8554 );
8555 }
8556
8557 #[gpui::test]
8558 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
8559 cx: &mut TestAppContext,
8560 ) {
8561 init_test(cx);
8562
8563 let fs = FakeFs::new(cx.background_executor.clone());
8564 let project = Project::test(fs, [], cx).await;
8565 let (workspace, cx) =
8566 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8567 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8568
8569 let dirty_regular_buffer = cx.new(|cx| {
8570 TestItem::new(cx)
8571 .with_dirty(true)
8572 .with_label("1.txt")
8573 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8574 });
8575 let dirty_regular_buffer_2 = cx.new(|cx| {
8576 TestItem::new(cx)
8577 .with_dirty(true)
8578 .with_label("2.txt")
8579 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8580 });
8581 let clear_regular_buffer = cx.new(|cx| {
8582 TestItem::new(cx)
8583 .with_label("3.txt")
8584 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8585 });
8586
8587 let dirty_multi_buffer = cx.new(|cx| {
8588 TestItem::new(cx)
8589 .with_dirty(true)
8590 .with_singleton(false)
8591 .with_label("Fake Project Search")
8592 .with_project_items(&[
8593 dirty_regular_buffer.read(cx).project_items[0].clone(),
8594 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8595 clear_regular_buffer.read(cx).project_items[0].clone(),
8596 ])
8597 });
8598 workspace.update_in(cx, |workspace, window, cx| {
8599 workspace.add_item(
8600 pane.clone(),
8601 Box::new(dirty_regular_buffer.clone()),
8602 None,
8603 false,
8604 false,
8605 window,
8606 cx,
8607 );
8608 workspace.add_item(
8609 pane.clone(),
8610 Box::new(dirty_regular_buffer_2.clone()),
8611 None,
8612 false,
8613 false,
8614 window,
8615 cx,
8616 );
8617 workspace.add_item(
8618 pane.clone(),
8619 Box::new(dirty_multi_buffer.clone()),
8620 None,
8621 false,
8622 false,
8623 window,
8624 cx,
8625 );
8626 });
8627
8628 pane.update_in(cx, |pane, window, cx| {
8629 pane.activate_item(2, true, true, window, cx);
8630 assert_eq!(
8631 pane.active_item().unwrap().item_id(),
8632 dirty_multi_buffer.item_id(),
8633 "Should select the multi buffer in the pane"
8634 );
8635 });
8636 let close_multi_buffer_task = pane
8637 .update_in(cx, |pane, window, cx| {
8638 pane.close_active_item(
8639 &CloseActiveItem {
8640 save_intent: None,
8641 close_pinned: false,
8642 },
8643 window,
8644 cx,
8645 )
8646 })
8647 .expect("should have active multi buffer to close");
8648 cx.background_executor.run_until_parked();
8649 assert!(
8650 !cx.has_pending_prompt(),
8651 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
8652 );
8653 close_multi_buffer_task
8654 .await
8655 .expect("Closing multi buffer failed");
8656 pane.update(cx, |pane, cx| {
8657 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8658 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
8659 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8660 assert_eq!(
8661 pane.items()
8662 .map(|item| item.item_id())
8663 .sorted()
8664 .collect::<Vec<_>>(),
8665 vec![
8666 dirty_regular_buffer.item_id(),
8667 dirty_regular_buffer_2.item_id(),
8668 ],
8669 "Should have no multi buffer left in the pane"
8670 );
8671 assert!(dirty_regular_buffer.read(cx).is_dirty);
8672 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
8673 });
8674 }
8675
8676 #[gpui::test]
8677 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
8678 init_test(cx);
8679 let fs = FakeFs::new(cx.executor());
8680 let project = Project::test(fs, [], cx).await;
8681 let (workspace, cx) =
8682 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8683
8684 // Add a new panel to the right dock, opening the dock and setting the
8685 // focus to the new panel.
8686 let panel = workspace.update_in(cx, |workspace, window, cx| {
8687 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8688 workspace.add_panel(panel.clone(), window, cx);
8689
8690 workspace
8691 .right_dock()
8692 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8693
8694 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8695
8696 panel
8697 });
8698
8699 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8700 // panel to the next valid position which, in this case, is the left
8701 // dock.
8702 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8703 workspace.update(cx, |workspace, cx| {
8704 assert!(workspace.left_dock().read(cx).is_open());
8705 assert_eq!(panel.read(cx).position, DockPosition::Left);
8706 });
8707
8708 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8709 // panel to the next valid position which, in this case, is the bottom
8710 // dock.
8711 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8712 workspace.update(cx, |workspace, cx| {
8713 assert!(workspace.bottom_dock().read(cx).is_open());
8714 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
8715 });
8716
8717 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
8718 // around moving the panel to its initial position, the right dock.
8719 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8720 workspace.update(cx, |workspace, cx| {
8721 assert!(workspace.right_dock().read(cx).is_open());
8722 assert_eq!(panel.read(cx).position, DockPosition::Right);
8723 });
8724
8725 // Remove focus from the panel, ensuring that, if the panel is not
8726 // focused, the `MoveFocusedPanelToNextPosition` action does not update
8727 // the panel's position, so the panel is still in the right dock.
8728 workspace.update_in(cx, |workspace, window, cx| {
8729 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8730 });
8731
8732 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8733 workspace.update(cx, |workspace, cx| {
8734 assert!(workspace.right_dock().read(cx).is_open());
8735 assert_eq!(panel.read(cx).position, DockPosition::Right);
8736 });
8737 }
8738
8739 mod register_project_item_tests {
8740
8741 use super::*;
8742
8743 // View
8744 struct TestPngItemView {
8745 focus_handle: FocusHandle,
8746 }
8747 // Model
8748 struct TestPngItem {}
8749
8750 impl project::ProjectItem for TestPngItem {
8751 fn try_open(
8752 _project: &Entity<Project>,
8753 path: &ProjectPath,
8754 cx: &mut App,
8755 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8756 if path.path.extension().unwrap() == "png" {
8757 Some(cx.spawn(async move |cx| cx.new(|_| TestPngItem {})))
8758 } else {
8759 None
8760 }
8761 }
8762
8763 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8764 None
8765 }
8766
8767 fn project_path(&self, _: &App) -> Option<ProjectPath> {
8768 None
8769 }
8770
8771 fn is_dirty(&self) -> bool {
8772 false
8773 }
8774 }
8775
8776 impl Item for TestPngItemView {
8777 type Event = ();
8778 }
8779 impl EventEmitter<()> for TestPngItemView {}
8780 impl Focusable for TestPngItemView {
8781 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8782 self.focus_handle.clone()
8783 }
8784 }
8785
8786 impl Render for TestPngItemView {
8787 fn render(
8788 &mut self,
8789 _window: &mut Window,
8790 _cx: &mut Context<Self>,
8791 ) -> impl IntoElement {
8792 Empty
8793 }
8794 }
8795
8796 impl ProjectItem for TestPngItemView {
8797 type Item = TestPngItem;
8798
8799 fn for_project_item(
8800 _project: Entity<Project>,
8801 _pane: &Pane,
8802 _item: Entity<Self::Item>,
8803 _: &mut Window,
8804 cx: &mut Context<Self>,
8805 ) -> Self
8806 where
8807 Self: Sized,
8808 {
8809 Self {
8810 focus_handle: cx.focus_handle(),
8811 }
8812 }
8813 }
8814
8815 // View
8816 struct TestIpynbItemView {
8817 focus_handle: FocusHandle,
8818 }
8819 // Model
8820 struct TestIpynbItem {}
8821
8822 impl project::ProjectItem for TestIpynbItem {
8823 fn try_open(
8824 _project: &Entity<Project>,
8825 path: &ProjectPath,
8826 cx: &mut App,
8827 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8828 if path.path.extension().unwrap() == "ipynb" {
8829 Some(cx.spawn(async move |cx| cx.new(|_| TestIpynbItem {})))
8830 } else {
8831 None
8832 }
8833 }
8834
8835 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8836 None
8837 }
8838
8839 fn project_path(&self, _: &App) -> Option<ProjectPath> {
8840 None
8841 }
8842
8843 fn is_dirty(&self) -> bool {
8844 false
8845 }
8846 }
8847
8848 impl Item for TestIpynbItemView {
8849 type Event = ();
8850 }
8851 impl EventEmitter<()> for TestIpynbItemView {}
8852 impl Focusable for TestIpynbItemView {
8853 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8854 self.focus_handle.clone()
8855 }
8856 }
8857
8858 impl Render for TestIpynbItemView {
8859 fn render(
8860 &mut self,
8861 _window: &mut Window,
8862 _cx: &mut Context<Self>,
8863 ) -> impl IntoElement {
8864 Empty
8865 }
8866 }
8867
8868 impl ProjectItem for TestIpynbItemView {
8869 type Item = TestIpynbItem;
8870
8871 fn for_project_item(
8872 _project: Entity<Project>,
8873 _pane: &Pane,
8874 _item: Entity<Self::Item>,
8875 _: &mut Window,
8876 cx: &mut Context<Self>,
8877 ) -> Self
8878 where
8879 Self: Sized,
8880 {
8881 Self {
8882 focus_handle: cx.focus_handle(),
8883 }
8884 }
8885 }
8886
8887 struct TestAlternatePngItemView {
8888 focus_handle: FocusHandle,
8889 }
8890
8891 impl Item for TestAlternatePngItemView {
8892 type Event = ();
8893 }
8894
8895 impl EventEmitter<()> for TestAlternatePngItemView {}
8896 impl Focusable for TestAlternatePngItemView {
8897 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8898 self.focus_handle.clone()
8899 }
8900 }
8901
8902 impl Render for TestAlternatePngItemView {
8903 fn render(
8904 &mut self,
8905 _window: &mut Window,
8906 _cx: &mut Context<Self>,
8907 ) -> impl IntoElement {
8908 Empty
8909 }
8910 }
8911
8912 impl ProjectItem for TestAlternatePngItemView {
8913 type Item = TestPngItem;
8914
8915 fn for_project_item(
8916 _project: Entity<Project>,
8917 _pane: &Pane,
8918 _item: Entity<Self::Item>,
8919 _: &mut Window,
8920 cx: &mut Context<Self>,
8921 ) -> Self
8922 where
8923 Self: Sized,
8924 {
8925 Self {
8926 focus_handle: cx.focus_handle(),
8927 }
8928 }
8929 }
8930
8931 #[gpui::test]
8932 async fn test_register_project_item(cx: &mut TestAppContext) {
8933 init_test(cx);
8934
8935 cx.update(|cx| {
8936 register_project_item::<TestPngItemView>(cx);
8937 register_project_item::<TestIpynbItemView>(cx);
8938 });
8939
8940 let fs = FakeFs::new(cx.executor());
8941 fs.insert_tree(
8942 "/root1",
8943 json!({
8944 "one.png": "BINARYDATAHERE",
8945 "two.ipynb": "{ totally a notebook }",
8946 "three.txt": "editing text, sure why not?"
8947 }),
8948 )
8949 .await;
8950
8951 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8952 let (workspace, cx) =
8953 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8954
8955 let worktree_id = project.update(cx, |project, cx| {
8956 project.worktrees(cx).next().unwrap().read(cx).id()
8957 });
8958
8959 let handle = workspace
8960 .update_in(cx, |workspace, window, cx| {
8961 let project_path = (worktree_id, "one.png");
8962 workspace.open_path(project_path, None, true, window, cx)
8963 })
8964 .await
8965 .unwrap();
8966
8967 // Now we can check if the handle we got back errored or not
8968 assert_eq!(
8969 handle.to_any().entity_type(),
8970 TypeId::of::<TestPngItemView>()
8971 );
8972
8973 let handle = workspace
8974 .update_in(cx, |workspace, window, cx| {
8975 let project_path = (worktree_id, "two.ipynb");
8976 workspace.open_path(project_path, None, true, window, cx)
8977 })
8978 .await
8979 .unwrap();
8980
8981 assert_eq!(
8982 handle.to_any().entity_type(),
8983 TypeId::of::<TestIpynbItemView>()
8984 );
8985
8986 let handle = workspace
8987 .update_in(cx, |workspace, window, cx| {
8988 let project_path = (worktree_id, "three.txt");
8989 workspace.open_path(project_path, None, true, window, cx)
8990 })
8991 .await;
8992 assert!(handle.is_err());
8993 }
8994
8995 #[gpui::test]
8996 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8997 init_test(cx);
8998
8999 cx.update(|cx| {
9000 register_project_item::<TestPngItemView>(cx);
9001 register_project_item::<TestAlternatePngItemView>(cx);
9002 });
9003
9004 let fs = FakeFs::new(cx.executor());
9005 fs.insert_tree(
9006 "/root1",
9007 json!({
9008 "one.png": "BINARYDATAHERE",
9009 "two.ipynb": "{ totally a notebook }",
9010 "three.txt": "editing text, sure why not?"
9011 }),
9012 )
9013 .await;
9014 let project = Project::test(fs, ["root1".as_ref()], cx).await;
9015 let (workspace, cx) =
9016 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9017 let worktree_id = project.update(cx, |project, cx| {
9018 project.worktrees(cx).next().unwrap().read(cx).id()
9019 });
9020
9021 let handle = workspace
9022 .update_in(cx, |workspace, window, cx| {
9023 let project_path = (worktree_id, "one.png");
9024 workspace.open_path(project_path, None, true, window, cx)
9025 })
9026 .await
9027 .unwrap();
9028
9029 // This _must_ be the second item registered
9030 assert_eq!(
9031 handle.to_any().entity_type(),
9032 TypeId::of::<TestAlternatePngItemView>()
9033 );
9034
9035 let handle = workspace
9036 .update_in(cx, |workspace, window, cx| {
9037 let project_path = (worktree_id, "three.txt");
9038 workspace.open_path(project_path, None, true, window, cx)
9039 })
9040 .await;
9041 assert!(handle.is_err());
9042 }
9043 }
9044
9045 pub fn init_test(cx: &mut TestAppContext) {
9046 cx.update(|cx| {
9047 let settings_store = SettingsStore::test(cx);
9048 cx.set_global(settings_store);
9049 theme::init(theme::LoadThemes::JustBase, cx);
9050 language::init(cx);
9051 crate::init_settings(cx);
9052 Project::init_settings(cx);
9053 });
9054 }
9055
9056 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
9057 let item = TestProjectItem::new(id, path, cx);
9058 item.update(cx, |item, _| {
9059 item.is_dirty = true;
9060 });
9061 item
9062 }
9063}