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