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