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