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