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