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 active_call: Option<(Entity<ActiveCall>, Vec<Subscription>)>,
830 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
831 database_id: Option<WorkspaceId>,
832 app_state: Arc<AppState>,
833 dispatching_keystrokes: Rc<RefCell<(HashSet<String>, Vec<Keystroke>)>>,
834 _subscriptions: Vec<Subscription>,
835 _apply_leader_updates: Task<Result<()>>,
836 _observe_current_user: Task<Result<()>>,
837 _schedule_serialize: Option<Task<()>>,
838 pane_history_timestamp: Arc<AtomicUsize>,
839 bounds: Bounds<Pixels>,
840 centered_layout: bool,
841 bounds_save_task_queued: Option<Task<()>>,
842 on_prompt_for_new_path: Option<PromptForNewPath>,
843 on_prompt_for_open_path: Option<PromptForOpenPath>,
844 serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
845 serialized_ssh_project: Option<SerializedSshProject>,
846 _items_serializer: Task<Result<()>>,
847 session_id: Option<String>,
848}
849
850impl EventEmitter<Event> for Workspace {}
851
852#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
853pub struct ViewId {
854 pub creator: PeerId,
855 pub id: u64,
856}
857
858pub struct FollowerState {
859 center_pane: Entity<Pane>,
860 dock_pane: Option<Entity<Pane>>,
861 active_view_id: Option<ViewId>,
862 items_by_leader_view_id: HashMap<ViewId, FollowerView>,
863}
864
865struct FollowerView {
866 view: Box<dyn FollowableItemHandle>,
867 location: Option<proto::PanelId>,
868}
869
870impl Workspace {
871 const DEFAULT_PADDING: f32 = 0.2;
872 const MAX_PADDING: f32 = 0.4;
873
874 pub fn new(
875 workspace_id: Option<WorkspaceId>,
876 project: Entity<Project>,
877 app_state: Arc<AppState>,
878 window: &mut Window,
879 cx: &mut Context<Self>,
880 ) -> Self {
881 cx.observe_in(&project, window, |_, _, _, cx| cx.notify())
882 .detach();
883 cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
884 match event {
885 project::Event::RemoteIdChanged(_) => {
886 this.update_window_title(window, cx);
887 }
888
889 project::Event::CollaboratorLeft(peer_id) => {
890 this.collaborator_left(*peer_id, window, cx);
891 }
892
893 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => {
894 this.update_window_title(window, cx);
895 this.serialize_workspace(window, cx);
896 }
897
898 project::Event::DisconnectedFromHost => {
899 this.update_window_edited(window, cx);
900 let leaders_to_unfollow =
901 this.follower_states.keys().copied().collect::<Vec<_>>();
902 for leader_id in leaders_to_unfollow {
903 this.unfollow(leader_id, window, cx);
904 }
905 }
906
907 project::Event::DisconnectedFromSshRemote => {
908 this.update_window_edited(window, cx);
909 }
910
911 project::Event::Closed => {
912 window.remove_window();
913 }
914
915 project::Event::DeletedEntry(_, entry_id) => {
916 for pane in this.panes.iter() {
917 pane.update(cx, |pane, cx| {
918 pane.handle_deleted_project_item(*entry_id, window, cx)
919 });
920 }
921 }
922
923 project::Event::Toast {
924 notification_id,
925 message,
926 } => this.show_notification(
927 NotificationId::named(notification_id.clone()),
928 cx,
929 |cx| cx.new(|cx| MessageNotification::new(message.clone(), cx)),
930 ),
931
932 project::Event::HideToast { notification_id } => {
933 this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
934 }
935
936 project::Event::LanguageServerPrompt(request) => {
937 struct LanguageServerPrompt;
938
939 let mut hasher = DefaultHasher::new();
940 request.lsp_name.as_str().hash(&mut hasher);
941 let id = hasher.finish();
942
943 this.show_notification(
944 NotificationId::composite::<LanguageServerPrompt>(id as usize),
945 cx,
946 |cx| {
947 cx.new(|cx| {
948 notifications::LanguageServerPrompt::new(request.clone(), cx)
949 })
950 },
951 );
952 }
953
954 _ => {}
955 }
956 cx.notify()
957 })
958 .detach();
959
960 cx.on_focus_lost(window, |this, window, cx| {
961 let focus_handle = this.focus_handle(cx);
962 window.focus(&focus_handle);
963 })
964 .detach();
965
966 let weak_handle = cx.entity().downgrade();
967 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
968
969 let center_pane = cx.new(|cx| {
970 let mut center_pane = Pane::new(
971 weak_handle.clone(),
972 project.clone(),
973 pane_history_timestamp.clone(),
974 None,
975 NewFile.boxed_clone(),
976 window,
977 cx,
978 );
979 center_pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
980 center_pane
981 });
982 cx.subscribe_in(¢er_pane, window, Self::handle_pane_event)
983 .detach();
984
985 window.focus(¢er_pane.focus_handle(cx));
986
987 cx.emit(Event::PaneAdded(center_pane.clone()));
988
989 let window_handle = window.window_handle().downcast::<Workspace>().unwrap();
990 app_state.workspace_store.update(cx, |store, _| {
991 store.workspaces.insert(window_handle);
992 });
993
994 let mut current_user = app_state.user_store.read(cx).watch_current_user();
995 let mut connection_status = app_state.client.status();
996 let _observe_current_user = cx.spawn_in(window, |this, mut cx| async move {
997 current_user.next().await;
998 connection_status.next().await;
999 let mut stream =
1000 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
1001
1002 while stream.recv().await.is_some() {
1003 this.update(&mut cx, |_, cx| cx.notify())?;
1004 }
1005 anyhow::Ok(())
1006 });
1007
1008 // All leader updates are enqueued and then processed in a single task, so
1009 // that each asynchronous operation can be run in order.
1010 let (leader_updates_tx, mut leader_updates_rx) =
1011 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
1012 let _apply_leader_updates = cx.spawn_in(window, |this, mut cx| async move {
1013 while let Some((leader_id, update)) = leader_updates_rx.next().await {
1014 Self::process_leader_update(&this, leader_id, update, &mut cx)
1015 .await
1016 .log_err();
1017 }
1018
1019 Ok(())
1020 });
1021
1022 cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
1023
1024 let left_dock = Dock::new(DockPosition::Left, window, cx);
1025 let bottom_dock = Dock::new(DockPosition::Bottom, window, cx);
1026 let right_dock = Dock::new(DockPosition::Right, window, cx);
1027 let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx));
1028 let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx));
1029 let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx));
1030 let status_bar = cx.new(|cx| {
1031 let mut status_bar = StatusBar::new(¢er_pane.clone(), window, cx);
1032 status_bar.add_left_item(left_dock_buttons, window, cx);
1033 status_bar.add_right_item(right_dock_buttons, window, cx);
1034 status_bar.add_right_item(bottom_dock_buttons, window, cx);
1035 status_bar
1036 });
1037
1038 let modal_layer = cx.new(|_| ModalLayer::new());
1039 let toast_layer = cx.new(|_| ToastLayer::new());
1040
1041 let session_id = app_state.session.read(cx).id().to_owned();
1042
1043 let mut active_call = None;
1044 if let Some(call) = ActiveCall::try_global(cx) {
1045 let call = call.clone();
1046 let subscriptions = vec![cx.subscribe_in(&call, window, Self::on_active_call_event)];
1047 active_call = Some((call, subscriptions));
1048 }
1049
1050 let (serializable_items_tx, serializable_items_rx) =
1051 mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
1052 let _items_serializer = cx.spawn_in(window, |this, mut cx| async move {
1053 Self::serialize_items(&this, serializable_items_rx, &mut cx).await
1054 });
1055
1056 let subscriptions = vec![
1057 cx.observe_window_activation(window, Self::on_window_activation_changed),
1058 cx.observe_window_bounds(window, move |this, window, cx| {
1059 if this.bounds_save_task_queued.is_some() {
1060 return;
1061 }
1062 this.bounds_save_task_queued =
1063 Some(cx.spawn_in(window, |this, mut cx| async move {
1064 cx.background_executor()
1065 .timer(Duration::from_millis(100))
1066 .await;
1067 this.update_in(&mut cx, |this, window, cx| {
1068 if let Some(display) = window.display(cx) {
1069 if let Ok(display_uuid) = display.uuid() {
1070 let window_bounds = window.inner_window_bounds();
1071 if let Some(database_id) = workspace_id {
1072 cx.background_executor()
1073 .spawn(DB.set_window_open_status(
1074 database_id,
1075 SerializedWindowBounds(window_bounds),
1076 display_uuid,
1077 ))
1078 .detach_and_log_err(cx);
1079 }
1080 }
1081 }
1082 this.bounds_save_task_queued.take();
1083 })
1084 .ok();
1085 }));
1086 cx.notify();
1087 }),
1088 cx.observe_window_appearance(window, |_, window, cx| {
1089 let window_appearance = window.appearance();
1090
1091 *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
1092
1093 ThemeSettings::reload_current_theme(cx);
1094 ThemeSettings::reload_current_icon_theme(cx);
1095 }),
1096 cx.on_release(move |this, cx| {
1097 this.app_state.workspace_store.update(cx, move |store, _| {
1098 store.workspaces.remove(&window_handle.clone());
1099 })
1100 }),
1101 ];
1102
1103 cx.defer_in(window, |this, window, cx| {
1104 this.update_window_title(window, cx);
1105 this.show_initial_notifications(cx);
1106 });
1107 Workspace {
1108 weak_self: weak_handle.clone(),
1109 zoomed: None,
1110 zoomed_position: None,
1111 previous_dock_drag_coordinates: None,
1112 center: PaneGroup::new(center_pane.clone()),
1113 panes: vec![center_pane.clone()],
1114 panes_by_item: Default::default(),
1115 active_pane: center_pane.clone(),
1116 last_active_center_pane: Some(center_pane.downgrade()),
1117 last_active_view_id: None,
1118 status_bar,
1119 modal_layer,
1120 toast_layer,
1121 titlebar_item: None,
1122 notifications: Default::default(),
1123 left_dock,
1124 bottom_dock,
1125 right_dock,
1126 project: project.clone(),
1127 follower_states: Default::default(),
1128 last_leaders_by_pane: Default::default(),
1129 dispatching_keystrokes: Default::default(),
1130 window_edited: false,
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 self.update_window_edited(window, cx);
3412 serialize_workspace = false;
3413 }
3414 pane::Event::RemoveItem { .. } => {}
3415 pane::Event::RemovedItem { item_id } => {
3416 cx.emit(Event::ActiveItemChanged);
3417 self.update_window_edited(window, cx);
3418 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
3419 if entry.get().entity_id() == pane.entity_id() {
3420 entry.remove();
3421 }
3422 }
3423 }
3424 pane::Event::Focus => {
3425 cx.on_next_frame(window, |_, window, _| {
3426 window.invalidate_character_coordinates();
3427 });
3428 self.handle_pane_focused(pane.clone(), window, cx);
3429 }
3430 pane::Event::ZoomIn => {
3431 if *pane == self.active_pane {
3432 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
3433 if pane.read(cx).has_focus(window, cx) {
3434 self.zoomed = Some(pane.downgrade().into());
3435 self.zoomed_position = None;
3436 cx.emit(Event::ZoomChanged);
3437 }
3438 cx.notify();
3439 }
3440 }
3441 pane::Event::ZoomOut => {
3442 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3443 if self.zoomed_position.is_none() {
3444 self.zoomed = None;
3445 cx.emit(Event::ZoomChanged);
3446 }
3447 cx.notify();
3448 }
3449 }
3450
3451 if serialize_workspace {
3452 self.serialize_workspace(window, cx);
3453 }
3454 }
3455
3456 pub fn unfollow_in_pane(
3457 &mut self,
3458 pane: &Entity<Pane>,
3459 window: &mut Window,
3460 cx: &mut Context<Workspace>,
3461 ) -> Option<PeerId> {
3462 let leader_id = self.leader_for_pane(pane)?;
3463 self.unfollow(leader_id, window, cx);
3464 Some(leader_id)
3465 }
3466
3467 pub fn split_pane(
3468 &mut self,
3469 pane_to_split: Entity<Pane>,
3470 split_direction: SplitDirection,
3471 window: &mut Window,
3472 cx: &mut Context<Self>,
3473 ) -> Entity<Pane> {
3474 let new_pane = self.add_pane(window, cx);
3475 self.center
3476 .split(&pane_to_split, &new_pane, split_direction)
3477 .unwrap();
3478 cx.notify();
3479 new_pane
3480 }
3481
3482 pub fn split_and_clone(
3483 &mut self,
3484 pane: Entity<Pane>,
3485 direction: SplitDirection,
3486 window: &mut Window,
3487 cx: &mut Context<Self>,
3488 ) -> Option<Entity<Pane>> {
3489 let item = pane.read(cx).active_item()?;
3490 let maybe_pane_handle =
3491 if let Some(clone) = item.clone_on_split(self.database_id(), window, cx) {
3492 let new_pane = self.add_pane(window, cx);
3493 new_pane.update(cx, |pane, cx| {
3494 pane.add_item(clone, true, true, None, window, cx)
3495 });
3496 self.center.split(&pane, &new_pane, direction).unwrap();
3497 Some(new_pane)
3498 } else {
3499 None
3500 };
3501 cx.notify();
3502 maybe_pane_handle
3503 }
3504
3505 pub fn split_pane_with_item(
3506 &mut self,
3507 pane_to_split: WeakEntity<Pane>,
3508 split_direction: SplitDirection,
3509 from: WeakEntity<Pane>,
3510 item_id_to_move: EntityId,
3511 window: &mut Window,
3512 cx: &mut Context<Self>,
3513 ) {
3514 let Some(pane_to_split) = pane_to_split.upgrade() else {
3515 return;
3516 };
3517 let Some(from) = from.upgrade() else {
3518 return;
3519 };
3520
3521 let new_pane = self.add_pane(window, cx);
3522 move_item(&from, &new_pane, item_id_to_move, 0, window, cx);
3523 self.center
3524 .split(&pane_to_split, &new_pane, split_direction)
3525 .unwrap();
3526 cx.notify();
3527 }
3528
3529 pub fn split_pane_with_project_entry(
3530 &mut self,
3531 pane_to_split: WeakEntity<Pane>,
3532 split_direction: SplitDirection,
3533 project_entry: ProjectEntryId,
3534 window: &mut Window,
3535 cx: &mut Context<Self>,
3536 ) -> Option<Task<Result<()>>> {
3537 let pane_to_split = pane_to_split.upgrade()?;
3538 let new_pane = self.add_pane(window, cx);
3539 self.center
3540 .split(&pane_to_split, &new_pane, split_direction)
3541 .unwrap();
3542
3543 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
3544 let task = self.open_path(path, Some(new_pane.downgrade()), true, window, cx);
3545 Some(cx.foreground_executor().spawn(async move {
3546 task.await?;
3547 Ok(())
3548 }))
3549 }
3550
3551 pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3552 let active_item = self.active_pane.read(cx).active_item();
3553 for pane in &self.panes {
3554 join_pane_into_active(&self.active_pane, pane, window, cx);
3555 }
3556 if let Some(active_item) = active_item {
3557 self.activate_item(active_item.as_ref(), true, true, window, cx);
3558 }
3559 cx.notify();
3560 }
3561
3562 pub fn join_pane_into_next(
3563 &mut self,
3564 pane: Entity<Pane>,
3565 window: &mut Window,
3566 cx: &mut Context<Self>,
3567 ) {
3568 let next_pane = self
3569 .find_pane_in_direction(SplitDirection::Right, cx)
3570 .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
3571 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3572 .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
3573 let Some(next_pane) = next_pane else {
3574 return;
3575 };
3576 move_all_items(&pane, &next_pane, window, cx);
3577 cx.notify();
3578 }
3579
3580 fn remove_pane(
3581 &mut self,
3582 pane: Entity<Pane>,
3583 focus_on: Option<Entity<Pane>>,
3584 window: &mut Window,
3585 cx: &mut Context<Self>,
3586 ) {
3587 if self.center.remove(&pane).unwrap() {
3588 self.force_remove_pane(&pane, &focus_on, window, cx);
3589 self.unfollow_in_pane(&pane, window, cx);
3590 self.last_leaders_by_pane.remove(&pane.downgrade());
3591 for removed_item in pane.read(cx).items() {
3592 self.panes_by_item.remove(&removed_item.item_id());
3593 }
3594
3595 cx.notify();
3596 } else {
3597 self.active_item_path_changed(window, cx);
3598 }
3599 cx.emit(Event::PaneRemoved);
3600 }
3601
3602 pub fn panes(&self) -> &[Entity<Pane>] {
3603 &self.panes
3604 }
3605
3606 pub fn active_pane(&self) -> &Entity<Pane> {
3607 &self.active_pane
3608 }
3609
3610 pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> {
3611 for dock in self.all_docks() {
3612 if dock.focus_handle(cx).contains_focused(window, cx) {
3613 if let Some(pane) = dock
3614 .read(cx)
3615 .active_panel()
3616 .and_then(|panel| panel.pane(cx))
3617 {
3618 return pane;
3619 }
3620 }
3621 }
3622 self.active_pane().clone()
3623 }
3624
3625 pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
3626 self.find_pane_in_direction(SplitDirection::Right, cx)
3627 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3628 .unwrap_or_else(|| {
3629 self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx)
3630 })
3631 .clone()
3632 }
3633
3634 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> {
3635 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
3636 weak_pane.upgrade()
3637 }
3638
3639 fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
3640 self.follower_states.retain(|leader_id, state| {
3641 if *leader_id == peer_id {
3642 for item in state.items_by_leader_view_id.values() {
3643 item.view.set_leader_peer_id(None, window, cx);
3644 }
3645 false
3646 } else {
3647 true
3648 }
3649 });
3650 cx.notify();
3651 }
3652
3653 pub fn start_following(
3654 &mut self,
3655 leader_id: PeerId,
3656 window: &mut Window,
3657 cx: &mut Context<Self>,
3658 ) -> Option<Task<Result<()>>> {
3659 let pane = self.active_pane().clone();
3660
3661 self.last_leaders_by_pane
3662 .insert(pane.downgrade(), leader_id);
3663 self.unfollow(leader_id, window, cx);
3664 self.unfollow_in_pane(&pane, window, cx);
3665 self.follower_states.insert(
3666 leader_id,
3667 FollowerState {
3668 center_pane: pane.clone(),
3669 dock_pane: None,
3670 active_view_id: None,
3671 items_by_leader_view_id: Default::default(),
3672 },
3673 );
3674 cx.notify();
3675
3676 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3677 let project_id = self.project.read(cx).remote_id();
3678 let request = self.app_state.client.request(proto::Follow {
3679 room_id,
3680 project_id,
3681 leader_id: Some(leader_id),
3682 });
3683
3684 Some(cx.spawn_in(window, |this, mut cx| async move {
3685 let response = request.await?;
3686 this.update(&mut cx, |this, _| {
3687 let state = this
3688 .follower_states
3689 .get_mut(&leader_id)
3690 .ok_or_else(|| anyhow!("following interrupted"))?;
3691 state.active_view_id = response
3692 .active_view
3693 .as_ref()
3694 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3695 Ok::<_, anyhow::Error>(())
3696 })??;
3697 if let Some(view) = response.active_view {
3698 Self::add_view_from_leader(this.clone(), leader_id, &view, &mut cx).await?;
3699 }
3700 this.update_in(&mut cx, |this, window, cx| {
3701 this.leader_updated(leader_id, window, cx)
3702 })?;
3703 Ok(())
3704 }))
3705 }
3706
3707 pub fn follow_next_collaborator(
3708 &mut self,
3709 _: &FollowNextCollaborator,
3710 window: &mut Window,
3711 cx: &mut Context<Self>,
3712 ) {
3713 let collaborators = self.project.read(cx).collaborators();
3714 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
3715 let mut collaborators = collaborators.keys().copied();
3716 for peer_id in collaborators.by_ref() {
3717 if peer_id == leader_id {
3718 break;
3719 }
3720 }
3721 collaborators.next()
3722 } else if let Some(last_leader_id) =
3723 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
3724 {
3725 if collaborators.contains_key(last_leader_id) {
3726 Some(*last_leader_id)
3727 } else {
3728 None
3729 }
3730 } else {
3731 None
3732 };
3733
3734 let pane = self.active_pane.clone();
3735 let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
3736 else {
3737 return;
3738 };
3739 if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
3740 return;
3741 }
3742 if let Some(task) = self.start_following(leader_id, window, cx) {
3743 task.detach_and_log_err(cx)
3744 }
3745 }
3746
3747 pub fn follow(&mut self, leader_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
3748 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
3749 return;
3750 };
3751 let room = room.read(cx);
3752 let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
3753 return;
3754 };
3755
3756 let project = self.project.read(cx);
3757
3758 let other_project_id = match remote_participant.location {
3759 call::ParticipantLocation::External => None,
3760 call::ParticipantLocation::UnsharedProject => None,
3761 call::ParticipantLocation::SharedProject { project_id } => {
3762 if Some(project_id) == project.remote_id() {
3763 None
3764 } else {
3765 Some(project_id)
3766 }
3767 }
3768 };
3769
3770 // if they are active in another project, follow there.
3771 if let Some(project_id) = other_project_id {
3772 let app_state = self.app_state.clone();
3773 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
3774 .detach_and_log_err(cx);
3775 }
3776
3777 // if you're already following, find the right pane and focus it.
3778 if let Some(follower_state) = self.follower_states.get(&leader_id) {
3779 window.focus(&follower_state.pane().focus_handle(cx));
3780
3781 return;
3782 }
3783
3784 // Otherwise, follow.
3785 if let Some(task) = self.start_following(leader_id, window, cx) {
3786 task.detach_and_log_err(cx)
3787 }
3788 }
3789
3790 pub fn unfollow(
3791 &mut self,
3792 leader_id: PeerId,
3793 window: &mut Window,
3794 cx: &mut Context<Self>,
3795 ) -> Option<()> {
3796 cx.notify();
3797 let state = self.follower_states.remove(&leader_id)?;
3798 for (_, item) in state.items_by_leader_view_id {
3799 item.view.set_leader_peer_id(None, window, cx);
3800 }
3801
3802 let project_id = self.project.read(cx).remote_id();
3803 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3804 self.app_state
3805 .client
3806 .send(proto::Unfollow {
3807 room_id,
3808 project_id,
3809 leader_id: Some(leader_id),
3810 })
3811 .log_err();
3812
3813 Some(())
3814 }
3815
3816 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
3817 self.follower_states.contains_key(&peer_id)
3818 }
3819
3820 fn active_item_path_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3821 cx.emit(Event::ActiveItemChanged);
3822 let active_entry = self.active_project_path(cx);
3823 self.project
3824 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
3825
3826 self.update_window_title(window, cx);
3827 }
3828
3829 fn update_window_title(&mut self, window: &mut Window, cx: &mut App) {
3830 let project = self.project().read(cx);
3831 let mut title = String::new();
3832
3833 for (i, name) in project.worktree_root_names(cx).enumerate() {
3834 if i > 0 {
3835 title.push_str(", ");
3836 }
3837 title.push_str(name);
3838 }
3839
3840 if title.is_empty() {
3841 title = "empty project".to_string();
3842 }
3843
3844 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
3845 let filename = path
3846 .path
3847 .file_name()
3848 .map(|s| s.to_string_lossy())
3849 .or_else(|| {
3850 Some(Cow::Borrowed(
3851 project
3852 .worktree_for_id(path.worktree_id, cx)?
3853 .read(cx)
3854 .root_name(),
3855 ))
3856 });
3857
3858 if let Some(filename) = filename {
3859 title.push_str(" — ");
3860 title.push_str(filename.as_ref());
3861 }
3862 }
3863
3864 if project.is_via_collab() {
3865 title.push_str(" ↙");
3866 } else if project.is_shared() {
3867 title.push_str(" ↗");
3868 }
3869
3870 window.set_window_title(&title);
3871 }
3872
3873 fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
3874 let is_edited = !self.project.read(cx).is_disconnected(cx)
3875 && self
3876 .items(cx)
3877 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
3878 if is_edited != self.window_edited {
3879 self.window_edited = is_edited;
3880 window.set_window_edited(self.window_edited)
3881 }
3882 }
3883
3884 fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
3885 if self.notifications.is_empty() {
3886 None
3887 } else {
3888 Some(
3889 div()
3890 .absolute()
3891 .right_3()
3892 .bottom_3()
3893 .w_112()
3894 .h_full()
3895 .flex()
3896 .flex_col()
3897 .justify_end()
3898 .gap_2()
3899 .children(
3900 self.notifications
3901 .iter()
3902 .map(|(_, notification)| notification.clone().into_any()),
3903 ),
3904 )
3905 }
3906 }
3907
3908 // RPC handlers
3909
3910 fn active_view_for_follower(
3911 &self,
3912 follower_project_id: Option<u64>,
3913 window: &mut Window,
3914 cx: &mut Context<Self>,
3915 ) -> Option<proto::View> {
3916 let (item, panel_id) = self.active_item_for_followers(window, cx);
3917 let item = item?;
3918 let leader_id = self
3919 .pane_for(&*item)
3920 .and_then(|pane| self.leader_for_pane(&pane));
3921
3922 let item_handle = item.to_followable_item_handle(cx)?;
3923 let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
3924 let variant = item_handle.to_state_proto(window, cx)?;
3925
3926 if item_handle.is_project_item(window, cx)
3927 && (follower_project_id.is_none()
3928 || follower_project_id != self.project.read(cx).remote_id())
3929 {
3930 return None;
3931 }
3932
3933 Some(proto::View {
3934 id: Some(id.to_proto()),
3935 leader_id,
3936 variant: Some(variant),
3937 panel_id: panel_id.map(|id| id as i32),
3938 })
3939 }
3940
3941 fn handle_follow(
3942 &mut self,
3943 follower_project_id: Option<u64>,
3944 window: &mut Window,
3945 cx: &mut Context<Self>,
3946 ) -> proto::FollowResponse {
3947 let active_view = self.active_view_for_follower(follower_project_id, window, cx);
3948
3949 cx.notify();
3950 proto::FollowResponse {
3951 // TODO: Remove after version 0.145.x stabilizes.
3952 active_view_id: active_view.as_ref().and_then(|view| view.id.clone()),
3953 views: active_view.iter().cloned().collect(),
3954 active_view,
3955 }
3956 }
3957
3958 fn handle_update_followers(
3959 &mut self,
3960 leader_id: PeerId,
3961 message: proto::UpdateFollowers,
3962 _window: &mut Window,
3963 _cx: &mut Context<Self>,
3964 ) {
3965 self.leader_updates_tx
3966 .unbounded_send((leader_id, message))
3967 .ok();
3968 }
3969
3970 async fn process_leader_update(
3971 this: &WeakEntity<Self>,
3972 leader_id: PeerId,
3973 update: proto::UpdateFollowers,
3974 cx: &mut AsyncWindowContext,
3975 ) -> Result<()> {
3976 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
3977 proto::update_followers::Variant::CreateView(view) => {
3978 let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
3979 let should_add_view = this.update(cx, |this, _| {
3980 if let Some(state) = this.follower_states.get_mut(&leader_id) {
3981 anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
3982 } else {
3983 anyhow::Ok(false)
3984 }
3985 })??;
3986
3987 if should_add_view {
3988 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3989 }
3990 }
3991 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
3992 let should_add_view = this.update(cx, |this, _| {
3993 if let Some(state) = this.follower_states.get_mut(&leader_id) {
3994 state.active_view_id = update_active_view
3995 .view
3996 .as_ref()
3997 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3998
3999 if state.active_view_id.is_some_and(|view_id| {
4000 !state.items_by_leader_view_id.contains_key(&view_id)
4001 }) {
4002 anyhow::Ok(true)
4003 } else {
4004 anyhow::Ok(false)
4005 }
4006 } else {
4007 anyhow::Ok(false)
4008 }
4009 })??;
4010
4011 if should_add_view {
4012 if let Some(view) = update_active_view.view {
4013 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
4014 }
4015 }
4016 }
4017 proto::update_followers::Variant::UpdateView(update_view) => {
4018 let variant = update_view
4019 .variant
4020 .ok_or_else(|| anyhow!("missing update view variant"))?;
4021 let id = update_view
4022 .id
4023 .ok_or_else(|| anyhow!("missing update view id"))?;
4024 let mut tasks = Vec::new();
4025 this.update_in(cx, |this, window, cx| {
4026 let project = this.project.clone();
4027 if let Some(state) = this.follower_states.get(&leader_id) {
4028 let view_id = ViewId::from_proto(id.clone())?;
4029 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
4030 tasks.push(item.view.apply_update_proto(
4031 &project,
4032 variant.clone(),
4033 window,
4034 cx,
4035 ));
4036 }
4037 }
4038 anyhow::Ok(())
4039 })??;
4040 try_join_all(tasks).await.log_err();
4041 }
4042 }
4043 this.update_in(cx, |this, window, cx| {
4044 this.leader_updated(leader_id, window, cx)
4045 })?;
4046 Ok(())
4047 }
4048
4049 async fn add_view_from_leader(
4050 this: WeakEntity<Self>,
4051 leader_id: PeerId,
4052 view: &proto::View,
4053 cx: &mut AsyncWindowContext,
4054 ) -> Result<()> {
4055 let this = this.upgrade().context("workspace dropped")?;
4056
4057 let Some(id) = view.id.clone() else {
4058 return Err(anyhow!("no id for view"));
4059 };
4060 let id = ViewId::from_proto(id)?;
4061 let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
4062
4063 let pane = this.update(cx, |this, _cx| {
4064 let state = this
4065 .follower_states
4066 .get(&leader_id)
4067 .context("stopped following")?;
4068 anyhow::Ok(state.pane().clone())
4069 })??;
4070 let existing_item = pane.update_in(cx, |pane, window, cx| {
4071 let client = this.read(cx).client().clone();
4072 pane.items().find_map(|item| {
4073 let item = item.to_followable_item_handle(cx)?;
4074 if item.remote_id(&client, window, cx) == Some(id) {
4075 Some(item)
4076 } else {
4077 None
4078 }
4079 })
4080 })?;
4081 let item = if let Some(existing_item) = existing_item {
4082 existing_item
4083 } else {
4084 let variant = view.variant.clone();
4085 if variant.is_none() {
4086 Err(anyhow!("missing view variant"))?;
4087 }
4088
4089 let task = cx.update(|window, cx| {
4090 FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx)
4091 })?;
4092
4093 let Some(task) = task else {
4094 return Err(anyhow!(
4095 "failed to construct view from leader (maybe from a different version of zed?)"
4096 ));
4097 };
4098
4099 let mut new_item = task.await?;
4100 pane.update_in(cx, |pane, window, cx| {
4101 let mut item_to_remove = None;
4102 for (ix, item) in pane.items().enumerate() {
4103 if let Some(item) = item.to_followable_item_handle(cx) {
4104 match new_item.dedup(item.as_ref(), window, cx) {
4105 Some(item::Dedup::KeepExisting) => {
4106 new_item =
4107 item.boxed_clone().to_followable_item_handle(cx).unwrap();
4108 break;
4109 }
4110 Some(item::Dedup::ReplaceExisting) => {
4111 item_to_remove = Some((ix, item.item_id()));
4112 break;
4113 }
4114 None => {}
4115 }
4116 }
4117 }
4118
4119 if let Some((ix, id)) = item_to_remove {
4120 pane.remove_item(id, false, false, window, cx);
4121 pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx);
4122 }
4123 })?;
4124
4125 new_item
4126 };
4127
4128 this.update_in(cx, |this, window, cx| {
4129 let state = this.follower_states.get_mut(&leader_id)?;
4130 item.set_leader_peer_id(Some(leader_id), window, cx);
4131 state.items_by_leader_view_id.insert(
4132 id,
4133 FollowerView {
4134 view: item,
4135 location: panel_id,
4136 },
4137 );
4138
4139 Some(())
4140 })?;
4141
4142 Ok(())
4143 }
4144
4145 pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
4146 let mut is_project_item = true;
4147 let mut update = proto::UpdateActiveView::default();
4148 if window.is_window_active() {
4149 let (active_item, panel_id) = self.active_item_for_followers(window, cx);
4150
4151 if let Some(item) = active_item {
4152 if item.item_focus_handle(cx).contains_focused(window, cx) {
4153 let leader_id = self
4154 .pane_for(&*item)
4155 .and_then(|pane| self.leader_for_pane(&pane));
4156
4157 if let Some(item) = item.to_followable_item_handle(cx) {
4158 let id = item
4159 .remote_id(&self.app_state.client, window, cx)
4160 .map(|id| id.to_proto());
4161
4162 if let Some(id) = id.clone() {
4163 if let Some(variant) = item.to_state_proto(window, cx) {
4164 let view = Some(proto::View {
4165 id: Some(id.clone()),
4166 leader_id,
4167 variant: Some(variant),
4168 panel_id: panel_id.map(|id| id as i32),
4169 });
4170
4171 is_project_item = item.is_project_item(window, cx);
4172 update = proto::UpdateActiveView {
4173 view,
4174 // TODO: Remove after version 0.145.x stabilizes.
4175 id: Some(id.clone()),
4176 leader_id,
4177 };
4178 }
4179 };
4180 }
4181 }
4182 }
4183 }
4184
4185 let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
4186 if active_view_id != self.last_active_view_id.as_ref() {
4187 self.last_active_view_id = active_view_id.cloned();
4188 self.update_followers(
4189 is_project_item,
4190 proto::update_followers::Variant::UpdateActiveView(update),
4191 window,
4192 cx,
4193 );
4194 }
4195 }
4196
4197 fn active_item_for_followers(
4198 &self,
4199 window: &mut Window,
4200 cx: &mut App,
4201 ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
4202 let mut active_item = None;
4203 let mut panel_id = None;
4204 for dock in self.all_docks() {
4205 if dock.focus_handle(cx).contains_focused(window, cx) {
4206 if let Some(panel) = dock.read(cx).active_panel() {
4207 if let Some(pane) = panel.pane(cx) {
4208 if let Some(item) = pane.read(cx).active_item() {
4209 active_item = Some(item);
4210 panel_id = panel.remote_id();
4211 break;
4212 }
4213 }
4214 }
4215 }
4216 }
4217
4218 if active_item.is_none() {
4219 active_item = self.active_pane().read(cx).active_item();
4220 }
4221 (active_item, panel_id)
4222 }
4223
4224 fn update_followers(
4225 &self,
4226 project_only: bool,
4227 update: proto::update_followers::Variant,
4228 _: &mut Window,
4229 cx: &mut App,
4230 ) -> Option<()> {
4231 // If this update only applies to for followers in the current project,
4232 // then skip it unless this project is shared. If it applies to all
4233 // followers, regardless of project, then set `project_id` to none,
4234 // indicating that it goes to all followers.
4235 let project_id = if project_only {
4236 Some(self.project.read(cx).remote_id()?)
4237 } else {
4238 None
4239 };
4240 self.app_state().workspace_store.update(cx, |store, cx| {
4241 store.update_followers(project_id, update, cx)
4242 })
4243 }
4244
4245 pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<PeerId> {
4246 self.follower_states.iter().find_map(|(leader_id, state)| {
4247 if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
4248 Some(*leader_id)
4249 } else {
4250 None
4251 }
4252 })
4253 }
4254
4255 fn leader_updated(
4256 &mut self,
4257 leader_id: PeerId,
4258 window: &mut Window,
4259 cx: &mut Context<Self>,
4260 ) -> Option<()> {
4261 cx.notify();
4262
4263 let call = self.active_call()?;
4264 let room = call.read(cx).room()?.read(cx);
4265 let participant = room.remote_participant_for_peer_id(leader_id)?;
4266
4267 let leader_in_this_app;
4268 let leader_in_this_project;
4269 match participant.location {
4270 call::ParticipantLocation::SharedProject { project_id } => {
4271 leader_in_this_app = true;
4272 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
4273 }
4274 call::ParticipantLocation::UnsharedProject => {
4275 leader_in_this_app = true;
4276 leader_in_this_project = false;
4277 }
4278 call::ParticipantLocation::External => {
4279 leader_in_this_app = false;
4280 leader_in_this_project = false;
4281 }
4282 };
4283
4284 let state = self.follower_states.get(&leader_id)?;
4285 let mut item_to_activate = None;
4286 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
4287 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
4288 if leader_in_this_project || !item.view.is_project_item(window, cx) {
4289 item_to_activate = Some((item.location, item.view.boxed_clone()));
4290 }
4291 }
4292 } else if let Some(shared_screen) =
4293 self.shared_screen_for_peer(leader_id, &state.center_pane, window, cx)
4294 {
4295 item_to_activate = Some((None, Box::new(shared_screen)));
4296 }
4297
4298 let (panel_id, item) = item_to_activate?;
4299
4300 let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
4301 let pane;
4302 if let Some(panel_id) = panel_id {
4303 pane = self
4304 .activate_panel_for_proto_id(panel_id, window, cx)?
4305 .pane(cx)?;
4306 let state = self.follower_states.get_mut(&leader_id)?;
4307 state.dock_pane = Some(pane.clone());
4308 } else {
4309 pane = state.center_pane.clone();
4310 let state = self.follower_states.get_mut(&leader_id)?;
4311 if let Some(dock_pane) = state.dock_pane.take() {
4312 transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx);
4313 }
4314 }
4315
4316 pane.update(cx, |pane, cx| {
4317 let focus_active_item = pane.has_focus(window, cx) || transfer_focus;
4318 if let Some(index) = pane.index_for_item(item.as_ref()) {
4319 pane.activate_item(index, false, false, window, cx);
4320 } else {
4321 pane.add_item(item.boxed_clone(), false, false, None, window, cx)
4322 }
4323
4324 if focus_active_item {
4325 pane.focus_active_item(window, cx)
4326 }
4327 });
4328
4329 None
4330 }
4331
4332 #[cfg(target_os = "windows")]
4333 fn shared_screen_for_peer(
4334 &self,
4335 _peer_id: PeerId,
4336 _pane: &Entity<Pane>,
4337 _window: &mut Window,
4338 _cx: &mut App,
4339 ) -> Option<Entity<SharedScreen>> {
4340 None
4341 }
4342
4343 #[cfg(not(target_os = "windows"))]
4344 fn shared_screen_for_peer(
4345 &self,
4346 peer_id: PeerId,
4347 pane: &Entity<Pane>,
4348 window: &mut Window,
4349 cx: &mut App,
4350 ) -> Option<Entity<SharedScreen>> {
4351 let call = self.active_call()?;
4352 let room = call.read(cx).room()?.read(cx);
4353 let participant = room.remote_participant_for_peer_id(peer_id)?;
4354 let track = participant.video_tracks.values().next()?.clone();
4355 let user = participant.user.clone();
4356
4357 for item in pane.read(cx).items_of_type::<SharedScreen>() {
4358 if item.read(cx).peer_id == peer_id {
4359 return Some(item);
4360 }
4361 }
4362
4363 Some(cx.new(|cx| SharedScreen::new(track, peer_id, user.clone(), window, cx)))
4364 }
4365
4366 pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4367 if window.is_window_active() {
4368 self.update_active_view_for_followers(window, cx);
4369
4370 if let Some(database_id) = self.database_id {
4371 cx.background_spawn(persistence::DB.update_timestamp(database_id))
4372 .detach();
4373 }
4374 } else {
4375 for pane in &self.panes {
4376 pane.update(cx, |pane, cx| {
4377 if let Some(item) = pane.active_item() {
4378 item.workspace_deactivated(window, cx);
4379 }
4380 for item in pane.items() {
4381 if matches!(
4382 item.workspace_settings(cx).autosave,
4383 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
4384 ) {
4385 Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
4386 .detach_and_log_err(cx);
4387 }
4388 }
4389 });
4390 }
4391 }
4392 }
4393
4394 pub fn active_call(&self) -> Option<&Entity<ActiveCall>> {
4395 self.active_call.as_ref().map(|(call, _)| call)
4396 }
4397
4398 fn on_active_call_event(
4399 &mut self,
4400 _: &Entity<ActiveCall>,
4401 event: &call::room::Event,
4402 window: &mut Window,
4403 cx: &mut Context<Self>,
4404 ) {
4405 match event {
4406 call::room::Event::ParticipantLocationChanged { participant_id }
4407 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4408 self.leader_updated(*participant_id, window, cx);
4409 }
4410 _ => {}
4411 }
4412 }
4413
4414 pub fn database_id(&self) -> Option<WorkspaceId> {
4415 self.database_id
4416 }
4417
4418 pub fn session_id(&self) -> Option<String> {
4419 self.session_id.clone()
4420 }
4421
4422 fn local_paths(&self, cx: &App) -> Option<Vec<Arc<Path>>> {
4423 let project = self.project().read(cx);
4424
4425 if project.is_local() {
4426 Some(
4427 project
4428 .visible_worktrees(cx)
4429 .map(|worktree| worktree.read(cx).abs_path())
4430 .collect::<Vec<_>>(),
4431 )
4432 } else {
4433 None
4434 }
4435 }
4436
4437 fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
4438 match member {
4439 Member::Axis(PaneAxis { members, .. }) => {
4440 for child in members.iter() {
4441 self.remove_panes(child.clone(), window, cx)
4442 }
4443 }
4444 Member::Pane(pane) => {
4445 self.force_remove_pane(&pane, &None, window, cx);
4446 }
4447 }
4448 }
4449
4450 fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
4451 self.session_id.take();
4452 self.serialize_workspace_internal(window, cx)
4453 }
4454
4455 fn force_remove_pane(
4456 &mut self,
4457 pane: &Entity<Pane>,
4458 focus_on: &Option<Entity<Pane>>,
4459 window: &mut Window,
4460 cx: &mut Context<Workspace>,
4461 ) {
4462 self.panes.retain(|p| p != pane);
4463 if let Some(focus_on) = focus_on {
4464 focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4465 } else {
4466 if self.active_pane() == pane {
4467 self.panes
4468 .last()
4469 .unwrap()
4470 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4471 }
4472 }
4473 if self.last_active_center_pane == Some(pane.downgrade()) {
4474 self.last_active_center_pane = None;
4475 }
4476 cx.notify();
4477 }
4478
4479 fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4480 if self._schedule_serialize.is_none() {
4481 self._schedule_serialize = Some(cx.spawn_in(window, |this, mut cx| async move {
4482 cx.background_executor()
4483 .timer(Duration::from_millis(100))
4484 .await;
4485 this.update_in(&mut cx, |this, window, cx| {
4486 this.serialize_workspace_internal(window, cx).detach();
4487 this._schedule_serialize.take();
4488 })
4489 .log_err();
4490 }));
4491 }
4492 }
4493
4494 fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
4495 let Some(database_id) = self.database_id() else {
4496 return Task::ready(());
4497 };
4498
4499 fn serialize_pane_handle(
4500 pane_handle: &Entity<Pane>,
4501 window: &mut Window,
4502 cx: &mut App,
4503 ) -> SerializedPane {
4504 let (items, active, pinned_count) = {
4505 let pane = pane_handle.read(cx);
4506 let active_item_id = pane.active_item().map(|item| item.item_id());
4507 (
4508 pane.items()
4509 .filter_map(|handle| {
4510 let handle = handle.to_serializable_item_handle(cx)?;
4511
4512 Some(SerializedItem {
4513 kind: Arc::from(handle.serialized_item_kind()),
4514 item_id: handle.item_id().as_u64(),
4515 active: Some(handle.item_id()) == active_item_id,
4516 preview: pane.is_active_preview_item(handle.item_id()),
4517 })
4518 })
4519 .collect::<Vec<_>>(),
4520 pane.has_focus(window, cx),
4521 pane.pinned_count(),
4522 )
4523 };
4524
4525 SerializedPane::new(items, active, pinned_count)
4526 }
4527
4528 fn build_serialized_pane_group(
4529 pane_group: &Member,
4530 window: &mut Window,
4531 cx: &mut App,
4532 ) -> SerializedPaneGroup {
4533 match pane_group {
4534 Member::Axis(PaneAxis {
4535 axis,
4536 members,
4537 flexes,
4538 bounding_boxes: _,
4539 }) => SerializedPaneGroup::Group {
4540 axis: SerializedAxis(*axis),
4541 children: members
4542 .iter()
4543 .map(|member| build_serialized_pane_group(member, window, cx))
4544 .collect::<Vec<_>>(),
4545 flexes: Some(flexes.lock().clone()),
4546 },
4547 Member::Pane(pane_handle) => {
4548 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
4549 }
4550 }
4551 }
4552
4553 fn build_serialized_docks(
4554 this: &Workspace,
4555 window: &mut Window,
4556 cx: &mut App,
4557 ) -> DockStructure {
4558 let left_dock = this.left_dock.read(cx);
4559 let left_visible = left_dock.is_open();
4560 let left_active_panel = left_dock
4561 .active_panel()
4562 .map(|panel| panel.persistent_name().to_string());
4563 let left_dock_zoom = left_dock
4564 .active_panel()
4565 .map(|panel| panel.is_zoomed(window, cx))
4566 .unwrap_or(false);
4567
4568 let right_dock = this.right_dock.read(cx);
4569 let right_visible = right_dock.is_open();
4570 let right_active_panel = right_dock
4571 .active_panel()
4572 .map(|panel| panel.persistent_name().to_string());
4573 let right_dock_zoom = right_dock
4574 .active_panel()
4575 .map(|panel| panel.is_zoomed(window, cx))
4576 .unwrap_or(false);
4577
4578 let bottom_dock = this.bottom_dock.read(cx);
4579 let bottom_visible = bottom_dock.is_open();
4580 let bottom_active_panel = bottom_dock
4581 .active_panel()
4582 .map(|panel| panel.persistent_name().to_string());
4583 let bottom_dock_zoom = bottom_dock
4584 .active_panel()
4585 .map(|panel| panel.is_zoomed(window, cx))
4586 .unwrap_or(false);
4587
4588 DockStructure {
4589 left: DockData {
4590 visible: left_visible,
4591 active_panel: left_active_panel,
4592 zoom: left_dock_zoom,
4593 },
4594 right: DockData {
4595 visible: right_visible,
4596 active_panel: right_active_panel,
4597 zoom: right_dock_zoom,
4598 },
4599 bottom: DockData {
4600 visible: bottom_visible,
4601 active_panel: bottom_active_panel,
4602 zoom: bottom_dock_zoom,
4603 },
4604 }
4605 }
4606
4607 let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4608 Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4609 } else if let Some(local_paths) = self.local_paths(cx) {
4610 if !local_paths.is_empty() {
4611 Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4612 } else {
4613 None
4614 }
4615 } else {
4616 None
4617 };
4618
4619 if let Some(location) = location {
4620 let center_group = build_serialized_pane_group(&self.center.root, window, cx);
4621 let docks = build_serialized_docks(self, window, cx);
4622 let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
4623 let serialized_workspace = SerializedWorkspace {
4624 id: database_id,
4625 location,
4626 center_group,
4627 window_bounds,
4628 display: Default::default(),
4629 docks,
4630 centered_layout: self.centered_layout,
4631 session_id: self.session_id.clone(),
4632 window_id: Some(window.window_handle().window_id().as_u64()),
4633 };
4634 return window.spawn(cx, |_| persistence::DB.save_workspace(serialized_workspace));
4635 }
4636 Task::ready(())
4637 }
4638
4639 async fn serialize_items(
4640 this: &WeakEntity<Self>,
4641 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4642 cx: &mut AsyncWindowContext,
4643 ) -> Result<()> {
4644 const CHUNK_SIZE: usize = 200;
4645
4646 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4647
4648 while let Some(items_received) = serializable_items.next().await {
4649 let unique_items =
4650 items_received
4651 .into_iter()
4652 .fold(HashMap::default(), |mut acc, item| {
4653 acc.entry(item.item_id()).or_insert(item);
4654 acc
4655 });
4656
4657 // We use into_iter() here so that the references to the items are moved into
4658 // the tasks and not kept alive while we're sleeping.
4659 for (_, item) in unique_items.into_iter() {
4660 if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
4661 item.serialize(workspace, false, window, cx)
4662 }) {
4663 cx.background_spawn(async move { task.await.log_err() })
4664 .detach();
4665 }
4666 }
4667
4668 cx.background_executor()
4669 .timer(SERIALIZATION_THROTTLE_TIME)
4670 .await;
4671 }
4672
4673 Ok(())
4674 }
4675
4676 pub(crate) fn enqueue_item_serialization(
4677 &mut self,
4678 item: Box<dyn SerializableItemHandle>,
4679 ) -> Result<()> {
4680 self.serializable_items_tx
4681 .unbounded_send(item)
4682 .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4683 }
4684
4685 pub(crate) fn load_workspace(
4686 serialized_workspace: SerializedWorkspace,
4687 paths_to_open: Vec<Option<ProjectPath>>,
4688 window: &mut Window,
4689 cx: &mut Context<Workspace>,
4690 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4691 cx.spawn_in(window, |workspace, mut cx| async move {
4692 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4693
4694 let mut center_group = None;
4695 let mut center_items = None;
4696
4697 // Traverse the splits tree and add to things
4698 if let Some((group, active_pane, items)) = serialized_workspace
4699 .center_group
4700 .deserialize(
4701 &project,
4702 serialized_workspace.id,
4703 workspace.clone(),
4704 &mut cx,
4705 )
4706 .await
4707 {
4708 center_items = Some(items);
4709 center_group = Some((group, active_pane))
4710 }
4711
4712 let mut items_by_project_path = HashMap::default();
4713 let mut item_ids_by_kind = HashMap::default();
4714 let mut all_deserialized_items = Vec::default();
4715 cx.update(|_, cx| {
4716 for item in center_items.unwrap_or_default().into_iter().flatten() {
4717 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4718 item_ids_by_kind
4719 .entry(serializable_item_handle.serialized_item_kind())
4720 .or_insert(Vec::new())
4721 .push(item.item_id().as_u64() as ItemId);
4722 }
4723
4724 if let Some(project_path) = item.project_path(cx) {
4725 items_by_project_path.insert(project_path, item.clone());
4726 }
4727 all_deserialized_items.push(item);
4728 }
4729 })?;
4730
4731 let opened_items = paths_to_open
4732 .into_iter()
4733 .map(|path_to_open| {
4734 path_to_open
4735 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4736 })
4737 .collect::<Vec<_>>();
4738
4739 // Remove old panes from workspace panes list
4740 workspace.update_in(&mut cx, |workspace, window, cx| {
4741 if let Some((center_group, active_pane)) = center_group {
4742 workspace.remove_panes(workspace.center.root.clone(), window, cx);
4743
4744 // Swap workspace center group
4745 workspace.center = PaneGroup::with_root(center_group);
4746 if let Some(active_pane) = active_pane {
4747 workspace.set_active_pane(&active_pane, window, cx);
4748 cx.focus_self(window);
4749 } else {
4750 workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
4751 }
4752 }
4753
4754 let docks = serialized_workspace.docks;
4755
4756 for (dock, serialized_dock) in [
4757 (&mut workspace.right_dock, docks.right),
4758 (&mut workspace.left_dock, docks.left),
4759 (&mut workspace.bottom_dock, docks.bottom),
4760 ]
4761 .iter_mut()
4762 {
4763 dock.update(cx, |dock, cx| {
4764 dock.serialized_dock = Some(serialized_dock.clone());
4765 dock.restore_state(window, cx);
4766 });
4767 }
4768
4769 cx.notify();
4770 })?;
4771
4772 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4773 // after loading the items, we might have different items and in order to avoid
4774 // the database filling up, we delete items that haven't been loaded now.
4775 //
4776 // The items that have been loaded, have been saved after they've been added to the workspace.
4777 let clean_up_tasks = workspace.update_in(&mut cx, |_, window, cx| {
4778 item_ids_by_kind
4779 .into_iter()
4780 .map(|(item_kind, loaded_items)| {
4781 SerializableItemRegistry::cleanup(
4782 item_kind,
4783 serialized_workspace.id,
4784 loaded_items,
4785 window,
4786 cx,
4787 )
4788 .log_err()
4789 })
4790 .collect::<Vec<_>>()
4791 })?;
4792
4793 futures::future::join_all(clean_up_tasks).await;
4794
4795 workspace
4796 .update_in(&mut cx, |workspace, window, cx| {
4797 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4798 workspace.serialize_workspace_internal(window, cx).detach();
4799
4800 // Ensure that we mark the window as edited if we did load dirty items
4801 workspace.update_window_edited(window, cx);
4802 })
4803 .ok();
4804
4805 Ok(opened_items)
4806 })
4807 }
4808
4809 fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
4810 self.add_workspace_actions_listeners(div, window, cx)
4811 .on_action(cx.listener(Self::close_inactive_items_and_panes))
4812 .on_action(cx.listener(Self::close_all_items_and_panes))
4813 .on_action(cx.listener(Self::save_all))
4814 .on_action(cx.listener(Self::send_keystrokes))
4815 .on_action(cx.listener(Self::add_folder_to_project))
4816 .on_action(cx.listener(Self::follow_next_collaborator))
4817 .on_action(cx.listener(Self::close_window))
4818 .on_action(cx.listener(Self::activate_pane_at_index))
4819 .on_action(cx.listener(Self::move_item_to_pane_at_index))
4820 .on_action(cx.listener(Self::move_focused_panel_to_next_position))
4821 .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
4822 let pane = workspace.active_pane().clone();
4823 workspace.unfollow_in_pane(&pane, window, cx);
4824 }))
4825 .on_action(cx.listener(|workspace, action: &Save, window, cx| {
4826 workspace
4827 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
4828 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4829 }))
4830 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
4831 workspace
4832 .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
4833 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4834 }))
4835 .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
4836 workspace
4837 .save_active_item(SaveIntent::SaveAs, window, cx)
4838 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4839 }))
4840 .on_action(
4841 cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
4842 workspace.activate_previous_pane(window, cx)
4843 }),
4844 )
4845 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4846 workspace.activate_next_pane(window, cx)
4847 }))
4848 .on_action(
4849 cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
4850 workspace.activate_next_window(cx)
4851 }),
4852 )
4853 .on_action(
4854 cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
4855 workspace.activate_previous_window(cx)
4856 }),
4857 )
4858 .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
4859 workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
4860 }))
4861 .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
4862 workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
4863 }))
4864 .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
4865 workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
4866 }))
4867 .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
4868 workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
4869 }))
4870 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4871 workspace.activate_next_pane(window, cx)
4872 }))
4873 .on_action(cx.listener(
4874 |workspace, action: &MoveItemToPaneInDirection, window, cx| {
4875 workspace.move_item_to_pane_in_direction(action, window, cx)
4876 },
4877 ))
4878 .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
4879 workspace.swap_pane_in_direction(SplitDirection::Left, cx)
4880 }))
4881 .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
4882 workspace.swap_pane_in_direction(SplitDirection::Right, cx)
4883 }))
4884 .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
4885 workspace.swap_pane_in_direction(SplitDirection::Up, cx)
4886 }))
4887 .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
4888 workspace.swap_pane_in_direction(SplitDirection::Down, cx)
4889 }))
4890 .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
4891 this.toggle_dock(DockPosition::Left, window, cx);
4892 }))
4893 .on_action(cx.listener(
4894 |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
4895 workspace.toggle_dock(DockPosition::Right, window, cx);
4896 },
4897 ))
4898 .on_action(cx.listener(
4899 |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
4900 workspace.toggle_dock(DockPosition::Bottom, window, cx);
4901 },
4902 ))
4903 .on_action(
4904 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
4905 workspace.close_all_docks(window, cx);
4906 }),
4907 )
4908 .on_action(cx.listener(
4909 |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
4910 workspace.clear_all_notifications(cx);
4911 },
4912 ))
4913 .on_action(cx.listener(
4914 |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
4915 workspace.reopen_closed_item(window, cx).detach();
4916 },
4917 ))
4918 .on_action(cx.listener(Workspace::toggle_centered_layout))
4919 }
4920
4921 #[cfg(any(test, feature = "test-support"))]
4922 pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
4923 use node_runtime::NodeRuntime;
4924 use session::Session;
4925
4926 let client = project.read(cx).client();
4927 let user_store = project.read(cx).user_store();
4928
4929 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
4930 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
4931 window.activate_window();
4932 let app_state = Arc::new(AppState {
4933 languages: project.read(cx).languages().clone(),
4934 workspace_store,
4935 client,
4936 user_store,
4937 fs: project.read(cx).fs().clone(),
4938 build_window_options: |_, _| Default::default(),
4939 node_runtime: NodeRuntime::unavailable(),
4940 session,
4941 });
4942 let workspace = Self::new(Default::default(), project, app_state, window, cx);
4943 workspace
4944 .active_pane
4945 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4946 workspace
4947 }
4948
4949 pub fn register_action<A: Action>(
4950 &mut self,
4951 callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
4952 ) -> &mut Self {
4953 let callback = Arc::new(callback);
4954
4955 self.workspace_actions.push(Box::new(move |div, _, cx| {
4956 let callback = callback.clone();
4957 div.on_action(cx.listener(move |workspace, event, window, cx| {
4958 (callback.clone())(workspace, event, window, cx)
4959 }))
4960 }));
4961 self
4962 }
4963
4964 fn add_workspace_actions_listeners(
4965 &self,
4966 mut div: Div,
4967 window: &mut Window,
4968 cx: &mut Context<Self>,
4969 ) -> Div {
4970 for action in self.workspace_actions.iter() {
4971 div = (action)(div, window, cx)
4972 }
4973 div
4974 }
4975
4976 pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
4977 self.modal_layer.read(cx).has_active_modal()
4978 }
4979
4980 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
4981 self.modal_layer.read(cx).active_modal()
4982 }
4983
4984 pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
4985 where
4986 B: FnOnce(&mut Window, &mut Context<V>) -> V,
4987 {
4988 self.modal_layer.update(cx, |modal_layer, cx| {
4989 modal_layer.toggle_modal(window, cx, build)
4990 })
4991 }
4992
4993 pub fn toggle_status_toast<V: ToastView>(
4994 &mut self,
4995 window: &mut Window,
4996 cx: &mut App,
4997 entity: Entity<V>,
4998 ) {
4999 self.toast_layer.update(cx, |toast_layer, cx| {
5000 toast_layer.toggle_toast(window, cx, entity)
5001 })
5002 }
5003
5004 pub fn toggle_centered_layout(
5005 &mut self,
5006 _: &ToggleCenteredLayout,
5007 _: &mut Window,
5008 cx: &mut Context<Self>,
5009 ) {
5010 self.centered_layout = !self.centered_layout;
5011 if let Some(database_id) = self.database_id() {
5012 cx.background_spawn(DB.set_centered_layout(database_id, self.centered_layout))
5013 .detach_and_log_err(cx);
5014 }
5015 cx.notify();
5016 }
5017
5018 fn adjust_padding(padding: Option<f32>) -> f32 {
5019 padding
5020 .unwrap_or(Self::DEFAULT_PADDING)
5021 .clamp(0.0, Self::MAX_PADDING)
5022 }
5023
5024 fn render_dock(
5025 &self,
5026 position: DockPosition,
5027 dock: &Entity<Dock>,
5028 window: &mut Window,
5029 cx: &mut App,
5030 ) -> Option<Div> {
5031 if self.zoomed_position == Some(position) {
5032 return None;
5033 }
5034
5035 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
5036 let pane = panel.pane(cx)?;
5037 let follower_states = &self.follower_states;
5038 leader_border_for_pane(follower_states, &pane, window, cx)
5039 });
5040
5041 Some(
5042 div()
5043 .flex()
5044 .flex_none()
5045 .overflow_hidden()
5046 .child(dock.clone())
5047 .children(leader_border),
5048 )
5049 }
5050
5051 pub fn for_window(window: &mut Window, _: &mut App) -> Option<Entity<Workspace>> {
5052 window.root().flatten()
5053 }
5054
5055 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
5056 self.zoomed.as_ref()
5057 }
5058
5059 pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
5060 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5061 return;
5062 };
5063 let windows = cx.windows();
5064 let Some(next_window) = windows
5065 .iter()
5066 .cycle()
5067 .skip_while(|window| window.window_id() != current_window_id)
5068 .nth(1)
5069 else {
5070 return;
5071 };
5072 next_window
5073 .update(cx, |_, window, _| window.activate_window())
5074 .ok();
5075 }
5076
5077 pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
5078 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5079 return;
5080 };
5081 let windows = cx.windows();
5082 let Some(prev_window) = windows
5083 .iter()
5084 .rev()
5085 .cycle()
5086 .skip_while(|window| window.window_id() != current_window_id)
5087 .nth(1)
5088 else {
5089 return;
5090 };
5091 prev_window
5092 .update(cx, |_, window, _| window.activate_window())
5093 .ok();
5094 }
5095}
5096
5097fn leader_border_for_pane(
5098 follower_states: &HashMap<PeerId, FollowerState>,
5099 pane: &Entity<Pane>,
5100 _: &Window,
5101 cx: &App,
5102) -> Option<Div> {
5103 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
5104 if state.pane() == pane {
5105 Some((*leader_id, state))
5106 } else {
5107 None
5108 }
5109 })?;
5110
5111 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
5112 let leader = room.remote_participant_for_peer_id(leader_id)?;
5113
5114 let mut leader_color = cx
5115 .theme()
5116 .players()
5117 .color_for_participant(leader.participant_index.0)
5118 .cursor;
5119 leader_color.fade_out(0.3);
5120 Some(
5121 div()
5122 .absolute()
5123 .size_full()
5124 .left_0()
5125 .top_0()
5126 .border_2()
5127 .border_color(leader_color),
5128 )
5129}
5130
5131fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
5132 ZED_WINDOW_POSITION
5133 .zip(*ZED_WINDOW_SIZE)
5134 .map(|(position, size)| Bounds {
5135 origin: position,
5136 size,
5137 })
5138}
5139
5140fn open_items(
5141 serialized_workspace: Option<SerializedWorkspace>,
5142 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
5143 window: &mut Window,
5144 cx: &mut Context<Workspace>,
5145) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
5146 let restored_items = serialized_workspace.map(|serialized_workspace| {
5147 Workspace::load_workspace(
5148 serialized_workspace,
5149 project_paths_to_open
5150 .iter()
5151 .map(|(_, project_path)| project_path)
5152 .cloned()
5153 .collect(),
5154 window,
5155 cx,
5156 )
5157 });
5158
5159 cx.spawn_in(window, |workspace, mut cx| async move {
5160 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
5161
5162 if let Some(restored_items) = restored_items {
5163 let restored_items = restored_items.await?;
5164
5165 let restored_project_paths = restored_items
5166 .iter()
5167 .filter_map(|item| {
5168 cx.update(|_, cx| item.as_ref()?.project_path(cx))
5169 .ok()
5170 .flatten()
5171 })
5172 .collect::<HashSet<_>>();
5173
5174 for restored_item in restored_items {
5175 opened_items.push(restored_item.map(Ok));
5176 }
5177
5178 project_paths_to_open
5179 .iter_mut()
5180 .for_each(|(_, project_path)| {
5181 if let Some(project_path_to_open) = project_path {
5182 if restored_project_paths.contains(project_path_to_open) {
5183 *project_path = None;
5184 }
5185 }
5186 });
5187 } else {
5188 for _ in 0..project_paths_to_open.len() {
5189 opened_items.push(None);
5190 }
5191 }
5192 assert!(opened_items.len() == project_paths_to_open.len());
5193
5194 let tasks =
5195 project_paths_to_open
5196 .into_iter()
5197 .enumerate()
5198 .map(|(ix, (abs_path, project_path))| {
5199 let workspace = workspace.clone();
5200 cx.spawn(|mut cx| async move {
5201 let file_project_path = project_path?;
5202 let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
5203 workspace.project().update(cx, |project, cx| {
5204 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
5205 })
5206 });
5207
5208 // We only want to open file paths here. If one of the items
5209 // here is a directory, it was already opened further above
5210 // with a `find_or_create_worktree`.
5211 if let Ok(task) = abs_path_task {
5212 if task.await.map_or(true, |p| p.is_file()) {
5213 return Some((
5214 ix,
5215 workspace
5216 .update_in(&mut cx, |workspace, window, cx| {
5217 workspace.open_path(
5218 file_project_path,
5219 None,
5220 true,
5221 window,
5222 cx,
5223 )
5224 })
5225 .log_err()?
5226 .await,
5227 ));
5228 }
5229 }
5230 None
5231 })
5232 });
5233
5234 let tasks = tasks.collect::<Vec<_>>();
5235
5236 let tasks = futures::future::join_all(tasks);
5237 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
5238 opened_items[ix] = Some(path_open_result);
5239 }
5240
5241 Ok(opened_items)
5242 })
5243}
5244
5245enum ActivateInDirectionTarget {
5246 Pane(Entity<Pane>),
5247 Dock(Entity<Dock>),
5248}
5249
5250fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
5251 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";
5252
5253 workspace
5254 .update(cx, |workspace, _, cx| {
5255 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
5256 struct DatabaseFailedNotification;
5257
5258 workspace.show_notification(
5259 NotificationId::unique::<DatabaseFailedNotification>(),
5260 cx,
5261 |cx| {
5262 cx.new(|cx| {
5263 MessageNotification::new("Failed to load the database file.", cx)
5264 .primary_message("File an Issue")
5265 .primary_icon(IconName::Plus)
5266 .primary_on_click(|_window, cx| cx.open_url(REPORT_ISSUE_URL))
5267 })
5268 },
5269 );
5270 }
5271 })
5272 .log_err();
5273}
5274
5275impl Focusable for Workspace {
5276 fn focus_handle(&self, cx: &App) -> FocusHandle {
5277 self.active_pane.focus_handle(cx)
5278 }
5279}
5280
5281#[derive(Clone)]
5282struct DraggedDock(DockPosition);
5283
5284impl Render for DraggedDock {
5285 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5286 gpui::Empty
5287 }
5288}
5289
5290impl Render for Workspace {
5291 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5292 let mut context = KeyContext::new_with_defaults();
5293 context.add("Workspace");
5294 context.set("keyboard_layout", cx.keyboard_layout().clone());
5295 let centered_layout = self.centered_layout
5296 && self.center.panes().len() == 1
5297 && self.active_item(cx).is_some();
5298 let render_padding = |size| {
5299 (size > 0.0).then(|| {
5300 div()
5301 .h_full()
5302 .w(relative(size))
5303 .bg(cx.theme().colors().editor_background)
5304 .border_color(cx.theme().colors().pane_group_border)
5305 })
5306 };
5307 let paddings = if centered_layout {
5308 let settings = WorkspaceSettings::get_global(cx).centered_layout;
5309 (
5310 render_padding(Self::adjust_padding(settings.left_padding)),
5311 render_padding(Self::adjust_padding(settings.right_padding)),
5312 )
5313 } else {
5314 (None, None)
5315 };
5316 let ui_font = theme::setup_ui_font(window, cx);
5317
5318 let theme = cx.theme().clone();
5319 let colors = theme.colors();
5320
5321 client_side_decorations(
5322 self.actions(div(), window, cx)
5323 .key_context(context)
5324 .relative()
5325 .size_full()
5326 .flex()
5327 .flex_col()
5328 .font(ui_font)
5329 .gap_0()
5330 .justify_start()
5331 .items_start()
5332 .text_color(colors.text)
5333 .overflow_hidden()
5334 .children(self.titlebar_item.clone())
5335 .child(
5336 div()
5337 .size_full()
5338 .relative()
5339 .flex_1()
5340 .flex()
5341 .flex_col()
5342 .child(
5343 div()
5344 .id("workspace")
5345 .bg(colors.background)
5346 .relative()
5347 .flex_1()
5348 .w_full()
5349 .flex()
5350 .flex_col()
5351 .overflow_hidden()
5352 .border_t_1()
5353 .border_b_1()
5354 .border_color(colors.border)
5355 .child({
5356 let this = cx.entity().clone();
5357 canvas(
5358 move |bounds, window, cx| {
5359 this.update(cx, |this, cx| {
5360 let bounds_changed = this.bounds != bounds;
5361 this.bounds = bounds;
5362
5363 if bounds_changed {
5364 this.left_dock.update(cx, |dock, cx| {
5365 dock.clamp_panel_size(
5366 bounds.size.width,
5367 window,
5368 cx,
5369 )
5370 });
5371
5372 this.right_dock.update(cx, |dock, cx| {
5373 dock.clamp_panel_size(
5374 bounds.size.width,
5375 window,
5376 cx,
5377 )
5378 });
5379
5380 this.bottom_dock.update(cx, |dock, cx| {
5381 dock.clamp_panel_size(
5382 bounds.size.height,
5383 window,
5384 cx,
5385 )
5386 });
5387 }
5388 })
5389 },
5390 |_, _, _, _| {},
5391 )
5392 .absolute()
5393 .size_full()
5394 })
5395 .when(self.zoomed.is_none(), |this| {
5396 this.on_drag_move(cx.listener(
5397 move |workspace,
5398 e: &DragMoveEvent<DraggedDock>,
5399 window,
5400 cx| {
5401 if workspace.previous_dock_drag_coordinates
5402 != Some(e.event.position)
5403 {
5404 workspace.previous_dock_drag_coordinates =
5405 Some(e.event.position);
5406 match e.drag(cx).0 {
5407 DockPosition::Left => {
5408 resize_left_dock(
5409 e.event.position.x
5410 - workspace.bounds.left(),
5411 workspace,
5412 window,
5413 cx,
5414 );
5415 }
5416 DockPosition::Right => {
5417 resize_right_dock(
5418 workspace.bounds.right()
5419 - e.event.position.x,
5420 workspace,
5421 window,
5422 cx,
5423 );
5424 }
5425 DockPosition::Bottom => {
5426 resize_bottom_dock(
5427 workspace.bounds.bottom()
5428 - e.event.position.y,
5429 workspace,
5430 window,
5431 cx,
5432 );
5433 }
5434 };
5435 workspace.serialize_workspace(window, cx);
5436 }
5437 },
5438 ))
5439 })
5440 .child(
5441 div()
5442 .flex()
5443 .flex_row()
5444 .h_full()
5445 // Left Dock
5446 .children(self.render_dock(
5447 DockPosition::Left,
5448 &self.left_dock,
5449 window,
5450 cx,
5451 ))
5452 // Panes
5453 .child(
5454 div()
5455 .flex()
5456 .flex_col()
5457 .flex_1()
5458 .overflow_hidden()
5459 .child(
5460 h_flex()
5461 .flex_1()
5462 .when_some(paddings.0, |this, p| {
5463 this.child(p.border_r_1())
5464 })
5465 .child(self.center.render(
5466 &self.project,
5467 &self.follower_states,
5468 self.active_call(),
5469 &self.active_pane,
5470 self.zoomed.as_ref(),
5471 &self.app_state,
5472 window,
5473 cx,
5474 ))
5475 .when_some(paddings.1, |this, p| {
5476 this.child(p.border_l_1())
5477 }),
5478 )
5479 .children(self.render_dock(
5480 DockPosition::Bottom,
5481 &self.bottom_dock,
5482 window,
5483 cx,
5484 )),
5485 )
5486 // Right Dock
5487 .children(self.render_dock(
5488 DockPosition::Right,
5489 &self.right_dock,
5490 window,
5491 cx,
5492 )),
5493 )
5494 .children(self.zoomed.as_ref().and_then(|view| {
5495 let zoomed_view = view.upgrade()?;
5496 let div = div()
5497 .occlude()
5498 .absolute()
5499 .overflow_hidden()
5500 .border_color(colors.border)
5501 .bg(colors.background)
5502 .child(zoomed_view)
5503 .inset_0()
5504 .shadow_lg();
5505
5506 Some(match self.zoomed_position {
5507 Some(DockPosition::Left) => div.right_2().border_r_1(),
5508 Some(DockPosition::Right) => div.left_2().border_l_1(),
5509 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
5510 None => {
5511 div.top_2().bottom_2().left_2().right_2().border_1()
5512 }
5513 })
5514 }))
5515 .children(self.render_notifications(window, cx)),
5516 )
5517 .child(self.status_bar.clone())
5518 .child(deferred(self.modal_layer.clone()))
5519 .child(self.toast_layer.clone()),
5520 ),
5521 window,
5522 cx,
5523 )
5524 }
5525}
5526
5527fn resize_bottom_dock(
5528 new_size: Pixels,
5529 workspace: &mut Workspace,
5530 window: &mut Window,
5531 cx: &mut App,
5532) {
5533 let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
5534 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
5535 bottom_dock.resize_active_panel(Some(size), window, cx);
5536 });
5537}
5538
5539fn resize_right_dock(
5540 new_size: Pixels,
5541 workspace: &mut Workspace,
5542 window: &mut Window,
5543 cx: &mut App,
5544) {
5545 let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
5546 workspace.right_dock.update(cx, |right_dock, cx| {
5547 right_dock.resize_active_panel(Some(size), window, cx);
5548 });
5549}
5550
5551fn resize_left_dock(
5552 new_size: Pixels,
5553 workspace: &mut Workspace,
5554 window: &mut Window,
5555 cx: &mut App,
5556) {
5557 let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5558
5559 workspace.left_dock.update(cx, |left_dock, cx| {
5560 left_dock.resize_active_panel(Some(size), window, cx);
5561 });
5562}
5563
5564impl WorkspaceStore {
5565 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
5566 Self {
5567 workspaces: Default::default(),
5568 _subscriptions: vec![
5569 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
5570 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
5571 ],
5572 client,
5573 }
5574 }
5575
5576 pub fn update_followers(
5577 &self,
5578 project_id: Option<u64>,
5579 update: proto::update_followers::Variant,
5580 cx: &App,
5581 ) -> Option<()> {
5582 let active_call = ActiveCall::try_global(cx)?;
5583 let room_id = active_call.read(cx).room()?.read(cx).id();
5584 self.client
5585 .send(proto::UpdateFollowers {
5586 room_id,
5587 project_id,
5588 variant: Some(update),
5589 })
5590 .log_err()
5591 }
5592
5593 pub async fn handle_follow(
5594 this: Entity<Self>,
5595 envelope: TypedEnvelope<proto::Follow>,
5596 mut cx: AsyncApp,
5597 ) -> Result<proto::FollowResponse> {
5598 this.update(&mut cx, |this, cx| {
5599 let follower = Follower {
5600 project_id: envelope.payload.project_id,
5601 peer_id: envelope.original_sender_id()?,
5602 };
5603
5604 let mut response = proto::FollowResponse::default();
5605 this.workspaces.retain(|workspace| {
5606 workspace
5607 .update(cx, |workspace, window, cx| {
5608 let handler_response =
5609 workspace.handle_follow(follower.project_id, window, cx);
5610 if let Some(active_view) = handler_response.active_view.clone() {
5611 if workspace.project.read(cx).remote_id() == follower.project_id {
5612 response.active_view = Some(active_view)
5613 }
5614 }
5615 })
5616 .is_ok()
5617 });
5618
5619 Ok(response)
5620 })?
5621 }
5622
5623 async fn handle_update_followers(
5624 this: Entity<Self>,
5625 envelope: TypedEnvelope<proto::UpdateFollowers>,
5626 mut cx: AsyncApp,
5627 ) -> Result<()> {
5628 let leader_id = envelope.original_sender_id()?;
5629 let update = envelope.payload;
5630
5631 this.update(&mut cx, |this, cx| {
5632 this.workspaces.retain(|workspace| {
5633 workspace
5634 .update(cx, |workspace, window, cx| {
5635 let project_id = workspace.project.read(cx).remote_id();
5636 if update.project_id != project_id && update.project_id.is_some() {
5637 return;
5638 }
5639 workspace.handle_update_followers(leader_id, update.clone(), window, cx);
5640 })
5641 .is_ok()
5642 });
5643 Ok(())
5644 })?
5645 }
5646}
5647
5648impl ViewId {
5649 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5650 Ok(Self {
5651 creator: message
5652 .creator
5653 .ok_or_else(|| anyhow!("creator is missing"))?,
5654 id: message.id,
5655 })
5656 }
5657
5658 pub(crate) fn to_proto(self) -> proto::ViewId {
5659 proto::ViewId {
5660 creator: Some(self.creator),
5661 id: self.id,
5662 }
5663 }
5664}
5665
5666impl FollowerState {
5667 fn pane(&self) -> &Entity<Pane> {
5668 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5669 }
5670}
5671
5672pub trait WorkspaceHandle {
5673 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
5674}
5675
5676impl WorkspaceHandle for Entity<Workspace> {
5677 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
5678 self.read(cx)
5679 .worktrees(cx)
5680 .flat_map(|worktree| {
5681 let worktree_id = worktree.read(cx).id();
5682 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5683 worktree_id,
5684 path: f.path.clone(),
5685 })
5686 })
5687 .collect::<Vec<_>>()
5688 }
5689}
5690
5691impl std::fmt::Debug for OpenPaths {
5692 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5693 f.debug_struct("OpenPaths")
5694 .field("paths", &self.paths)
5695 .finish()
5696 }
5697}
5698
5699pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5700 DB.last_workspace().await.log_err().flatten()
5701}
5702
5703pub fn last_session_workspace_locations(
5704 last_session_id: &str,
5705 last_session_window_stack: Option<Vec<WindowId>>,
5706) -> Option<Vec<SerializedWorkspaceLocation>> {
5707 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5708 .log_err()
5709}
5710
5711actions!(collab, [OpenChannelNotes]);
5712actions!(zed, [OpenLog]);
5713
5714async fn join_channel_internal(
5715 channel_id: ChannelId,
5716 app_state: &Arc<AppState>,
5717 requesting_window: Option<WindowHandle<Workspace>>,
5718 active_call: &Entity<ActiveCall>,
5719 cx: &mut AsyncApp,
5720) -> Result<bool> {
5721 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5722 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5723 return (false, None);
5724 };
5725
5726 let already_in_channel = room.channel_id() == Some(channel_id);
5727 let should_prompt = room.is_sharing_project()
5728 && !room.remote_participants().is_empty()
5729 && !already_in_channel;
5730 let open_room = if already_in_channel {
5731 active_call.room().cloned()
5732 } else {
5733 None
5734 };
5735 (should_prompt, open_room)
5736 })?;
5737
5738 if let Some(room) = open_room {
5739 let task = room.update(cx, |room, cx| {
5740 if let Some((project, host)) = room.most_active_project(cx) {
5741 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5742 }
5743
5744 None
5745 })?;
5746 if let Some(task) = task {
5747 task.await?;
5748 }
5749 return anyhow::Ok(true);
5750 }
5751
5752 if should_prompt {
5753 if let Some(workspace) = requesting_window {
5754 let answer = workspace
5755 .update(cx, |_, window, cx| {
5756 window.prompt(
5757 PromptLevel::Warning,
5758 "Do you want to switch channels?",
5759 Some("Leaving this call will unshare your current project."),
5760 &["Yes, Join Channel", "Cancel"],
5761 cx,
5762 )
5763 })?
5764 .await;
5765
5766 if answer == Ok(1) {
5767 return Ok(false);
5768 }
5769 } else {
5770 return Ok(false); // unreachable!() hopefully
5771 }
5772 }
5773
5774 let client = cx.update(|cx| active_call.read(cx).client())?;
5775
5776 let mut client_status = client.status();
5777
5778 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5779 'outer: loop {
5780 let Some(status) = client_status.recv().await else {
5781 return Err(anyhow!("error connecting"));
5782 };
5783
5784 match status {
5785 Status::Connecting
5786 | Status::Authenticating
5787 | Status::Reconnecting
5788 | Status::Reauthenticating => continue,
5789 Status::Connected { .. } => break 'outer,
5790 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5791 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5792 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5793 return Err(ErrorCode::Disconnected.into());
5794 }
5795 }
5796 }
5797
5798 let room = active_call
5799 .update(cx, |active_call, cx| {
5800 active_call.join_channel(channel_id, cx)
5801 })?
5802 .await?;
5803
5804 let Some(room) = room else {
5805 return anyhow::Ok(true);
5806 };
5807
5808 room.update(cx, |room, _| room.room_update_completed())?
5809 .await;
5810
5811 let task = room.update(cx, |room, cx| {
5812 if let Some((project, host)) = room.most_active_project(cx) {
5813 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5814 }
5815
5816 // If you are the first to join a channel, see if you should share your project.
5817 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5818 if let Some(workspace) = requesting_window {
5819 let project = workspace.update(cx, |workspace, _, cx| {
5820 let project = workspace.project.read(cx);
5821
5822 if !CallSettings::get_global(cx).share_on_join {
5823 return None;
5824 }
5825
5826 if (project.is_local() || project.is_via_ssh())
5827 && project.visible_worktrees(cx).any(|tree| {
5828 tree.read(cx)
5829 .root_entry()
5830 .map_or(false, |entry| entry.is_dir())
5831 })
5832 {
5833 Some(workspace.project.clone())
5834 } else {
5835 None
5836 }
5837 });
5838 if let Ok(Some(project)) = project {
5839 return Some(cx.spawn(|room, mut cx| async move {
5840 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5841 .await?;
5842 Ok(())
5843 }));
5844 }
5845 }
5846 }
5847
5848 None
5849 })?;
5850 if let Some(task) = task {
5851 task.await?;
5852 return anyhow::Ok(true);
5853 }
5854 anyhow::Ok(false)
5855}
5856
5857pub fn join_channel(
5858 channel_id: ChannelId,
5859 app_state: Arc<AppState>,
5860 requesting_window: Option<WindowHandle<Workspace>>,
5861 cx: &mut App,
5862) -> Task<Result<()>> {
5863 let active_call = ActiveCall::global(cx);
5864 cx.spawn(|mut cx| async move {
5865 let result = join_channel_internal(
5866 channel_id,
5867 &app_state,
5868 requesting_window,
5869 &active_call,
5870 &mut cx,
5871 )
5872 .await;
5873
5874 // join channel succeeded, and opened a window
5875 if matches!(result, Ok(true)) {
5876 return anyhow::Ok(());
5877 }
5878
5879 // find an existing workspace to focus and show call controls
5880 let mut active_window =
5881 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5882 if active_window.is_none() {
5883 // no open workspaces, make one to show the error in (blergh)
5884 let (window_handle, _) = cx
5885 .update(|cx| {
5886 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5887 })?
5888 .await?;
5889
5890 if result.is_ok() {
5891 cx.update(|cx| {
5892 cx.dispatch_action(&OpenChannelNotes);
5893 }).log_err();
5894 }
5895
5896 active_window = Some(window_handle);
5897 }
5898
5899 if let Err(err) = result {
5900 log::error!("failed to join channel: {}", err);
5901 if let Some(active_window) = active_window {
5902 active_window
5903 .update(&mut cx, |_, window, cx| {
5904 let detail: SharedString = match err.error_code() {
5905 ErrorCode::SignedOut => {
5906 "Please sign in to continue.".into()
5907 }
5908 ErrorCode::UpgradeRequired => {
5909 "Your are running an unsupported version of Zed. Please update to continue.".into()
5910 }
5911 ErrorCode::NoSuchChannel => {
5912 "No matching channel was found. Please check the link and try again.".into()
5913 }
5914 ErrorCode::Forbidden => {
5915 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5916 }
5917 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5918 _ => format!("{}\n\nPlease try again.", err).into(),
5919 };
5920 window.prompt(
5921 PromptLevel::Critical,
5922 "Failed to join channel",
5923 Some(&detail),
5924 &["Ok"],
5925 cx)
5926 })?
5927 .await
5928 .ok();
5929 }
5930 }
5931
5932 // return ok, we showed the error to the user.
5933 anyhow::Ok(())
5934 })
5935}
5936
5937pub async fn get_any_active_workspace(
5938 app_state: Arc<AppState>,
5939 mut cx: AsyncApp,
5940) -> anyhow::Result<WindowHandle<Workspace>> {
5941 // find an existing workspace to focus and show call controls
5942 let active_window = activate_any_workspace_window(&mut cx);
5943 if active_window.is_none() {
5944 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5945 .await?;
5946 }
5947 activate_any_workspace_window(&mut cx).context("could not open zed")
5948}
5949
5950fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
5951 cx.update(|cx| {
5952 if let Some(workspace_window) = cx
5953 .active_window()
5954 .and_then(|window| window.downcast::<Workspace>())
5955 {
5956 return Some(workspace_window);
5957 }
5958
5959 for window in cx.windows() {
5960 if let Some(workspace_window) = window.downcast::<Workspace>() {
5961 workspace_window
5962 .update(cx, |_, window, _| window.activate_window())
5963 .ok();
5964 return Some(workspace_window);
5965 }
5966 }
5967 None
5968 })
5969 .ok()
5970 .flatten()
5971}
5972
5973pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
5974 cx.windows()
5975 .into_iter()
5976 .filter_map(|window| window.downcast::<Workspace>())
5977 .filter(|workspace| {
5978 workspace
5979 .read(cx)
5980 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5981 })
5982 .collect()
5983}
5984
5985#[derive(Default)]
5986pub struct OpenOptions {
5987 pub visible: Option<OpenVisible>,
5988 pub focus: Option<bool>,
5989 pub open_new_workspace: Option<bool>,
5990 pub replace_window: Option<WindowHandle<Workspace>>,
5991 pub env: Option<HashMap<String, String>>,
5992}
5993
5994#[allow(clippy::type_complexity)]
5995pub fn open_paths(
5996 abs_paths: &[PathBuf],
5997 app_state: Arc<AppState>,
5998 open_options: OpenOptions,
5999 cx: &mut App,
6000) -> Task<
6001 anyhow::Result<(
6002 WindowHandle<Workspace>,
6003 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
6004 )>,
6005> {
6006 let abs_paths = abs_paths.to_vec();
6007 let mut existing = None;
6008 let mut best_match = None;
6009 let mut open_visible = OpenVisible::All;
6010
6011 cx.spawn(move |mut cx| async move {
6012 if open_options.open_new_workspace != Some(true) {
6013 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
6014 let all_metadatas = futures::future::join_all(all_paths)
6015 .await
6016 .into_iter()
6017 .filter_map(|result| result.ok().flatten())
6018 .collect::<Vec<_>>();
6019
6020 cx.update(|cx| {
6021 for window in local_workspace_windows(&cx) {
6022 if let Ok(workspace) = window.read(&cx) {
6023 let m = workspace.project.read(&cx).visibility_for_paths(
6024 &abs_paths,
6025 &all_metadatas,
6026 open_options.open_new_workspace == None,
6027 cx,
6028 );
6029 if m > best_match {
6030 existing = Some(window);
6031 best_match = m;
6032 } else if best_match.is_none()
6033 && open_options.open_new_workspace == Some(false)
6034 {
6035 existing = Some(window)
6036 }
6037 }
6038 }
6039 })?;
6040
6041 if open_options.open_new_workspace.is_none() && existing.is_none() {
6042 if all_metadatas.iter().all(|file| !file.is_dir) {
6043 cx.update(|cx| {
6044 if let Some(window) = cx
6045 .active_window()
6046 .and_then(|window| window.downcast::<Workspace>())
6047 {
6048 if let Ok(workspace) = window.read(cx) {
6049 let project = workspace.project().read(cx);
6050 if project.is_local() && !project.is_via_collab() {
6051 existing = Some(window);
6052 open_visible = OpenVisible::None;
6053 return;
6054 }
6055 }
6056 }
6057 for window in local_workspace_windows(cx) {
6058 if let Ok(workspace) = window.read(cx) {
6059 let project = workspace.project().read(cx);
6060 if project.is_via_collab() {
6061 continue;
6062 }
6063 existing = Some(window);
6064 open_visible = OpenVisible::None;
6065 break;
6066 }
6067 }
6068 })?;
6069 }
6070 }
6071 }
6072
6073 if let Some(existing) = existing {
6074 let open_task = existing
6075 .update(&mut cx, |workspace, window, cx| {
6076 window.activate_window();
6077 workspace.open_paths(
6078 abs_paths,
6079 OpenOptions {
6080 visible: Some(open_visible),
6081 ..Default::default()
6082 },
6083 None,
6084 window,
6085 cx,
6086 )
6087 })?
6088 .await;
6089
6090 _ = existing.update(&mut cx, |workspace, _, cx| {
6091 for item in open_task.iter().flatten() {
6092 if let Err(e) = item {
6093 workspace.show_error(&e, cx);
6094 }
6095 }
6096 });
6097
6098 Ok((existing, open_task))
6099 } else {
6100 cx.update(move |cx| {
6101 Workspace::new_local(
6102 abs_paths,
6103 app_state.clone(),
6104 open_options.replace_window,
6105 open_options.env,
6106 cx,
6107 )
6108 })?
6109 .await
6110 }
6111 })
6112}
6113
6114pub fn open_new(
6115 open_options: OpenOptions,
6116 app_state: Arc<AppState>,
6117 cx: &mut App,
6118 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6119) -> Task<anyhow::Result<()>> {
6120 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6121 cx.spawn(|mut cx| async move {
6122 let (workspace, opened_paths) = task.await?;
6123 workspace.update(&mut cx, |workspace, window, cx| {
6124 if opened_paths.is_empty() {
6125 init(workspace, window, cx)
6126 }
6127 })?;
6128 Ok(())
6129 })
6130}
6131
6132pub fn create_and_open_local_file(
6133 path: &'static Path,
6134 window: &mut Window,
6135 cx: &mut Context<Workspace>,
6136 default_content: impl 'static + Send + FnOnce() -> Rope,
6137) -> Task<Result<Box<dyn ItemHandle>>> {
6138 cx.spawn_in(window, |workspace, mut cx| async move {
6139 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
6140 if !fs.is_file(path).await {
6141 fs.create_file(path, Default::default()).await?;
6142 fs.save(path, &default_content(), Default::default())
6143 .await?;
6144 }
6145
6146 let mut items = workspace
6147 .update_in(&mut cx, |workspace, window, cx| {
6148 workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6149 workspace.open_paths(
6150 vec![path.to_path_buf()],
6151 OpenOptions {
6152 visible: Some(OpenVisible::None),
6153 ..Default::default()
6154 },
6155 None,
6156 window,
6157 cx,
6158 )
6159 })
6160 })?
6161 .await?
6162 .await;
6163
6164 let item = items.pop().flatten();
6165 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
6166 })
6167}
6168
6169pub fn open_ssh_project(
6170 window: WindowHandle<Workspace>,
6171 connection_options: SshConnectionOptions,
6172 cancel_rx: oneshot::Receiver<()>,
6173 delegate: Arc<dyn SshClientDelegate>,
6174 app_state: Arc<AppState>,
6175 paths: Vec<PathBuf>,
6176 cx: &mut App,
6177) -> Task<Result<()>> {
6178 cx.spawn(|mut cx| async move {
6179 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6180 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6181
6182 let session = match cx
6183 .update(|cx| {
6184 remote::SshRemoteClient::new(
6185 ConnectionIdentifier::Workspace(workspace_id.0),
6186 connection_options,
6187 cancel_rx,
6188 delegate,
6189 cx,
6190 )
6191 })?
6192 .await?
6193 {
6194 Some(result) => result,
6195 None => return Ok(()),
6196 };
6197
6198 let project = cx.update(|cx| {
6199 project::Project::ssh(
6200 session,
6201 app_state.client.clone(),
6202 app_state.node_runtime.clone(),
6203 app_state.user_store.clone(),
6204 app_state.languages.clone(),
6205 app_state.fs.clone(),
6206 cx,
6207 )
6208 })?;
6209
6210 let toolchains = DB.toolchains(workspace_id).await?;
6211 for (toolchain, worktree_id) in toolchains {
6212 project
6213 .update(&mut cx, |this, cx| {
6214 this.activate_toolchain(worktree_id, toolchain, cx)
6215 })?
6216 .await;
6217 }
6218 let mut project_paths_to_open = vec![];
6219 let mut project_path_errors = vec![];
6220
6221 for path in paths {
6222 let result = cx
6223 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6224 .await;
6225 match result {
6226 Ok((_, project_path)) => {
6227 project_paths_to_open.push((path.clone(), Some(project_path)));
6228 }
6229 Err(error) => {
6230 project_path_errors.push(error);
6231 }
6232 };
6233 }
6234
6235 if project_paths_to_open.is_empty() {
6236 return Err(project_path_errors
6237 .pop()
6238 .unwrap_or_else(|| anyhow!("no paths given")));
6239 }
6240
6241 cx.update_window(window.into(), |_, window, cx| {
6242 window.replace_root(cx, |window, cx| {
6243 telemetry::event!("SSH Project Opened");
6244
6245 let mut workspace =
6246 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6247 workspace.set_serialized_ssh_project(serialized_ssh_project);
6248 workspace
6249 });
6250 })?;
6251
6252 window
6253 .update(&mut cx, |_, window, cx| {
6254 window.activate_window();
6255
6256 open_items(serialized_workspace, project_paths_to_open, window, cx)
6257 })?
6258 .await?;
6259
6260 window.update(&mut cx, |workspace, _, cx| {
6261 for error in project_path_errors {
6262 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6263 if let Some(path) = error.error_tag("path") {
6264 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6265 }
6266 } else {
6267 workspace.show_error(&error, cx)
6268 }
6269 }
6270 })
6271 })
6272}
6273
6274fn serialize_ssh_project(
6275 connection_options: SshConnectionOptions,
6276 paths: Vec<PathBuf>,
6277 cx: &AsyncApp,
6278) -> Task<
6279 Result<(
6280 SerializedSshProject,
6281 WorkspaceId,
6282 Option<SerializedWorkspace>,
6283 )>,
6284> {
6285 cx.background_spawn(async move {
6286 let serialized_ssh_project = persistence::DB
6287 .get_or_create_ssh_project(
6288 connection_options.host.clone(),
6289 connection_options.port,
6290 paths
6291 .iter()
6292 .map(|path| path.to_string_lossy().to_string())
6293 .collect::<Vec<_>>(),
6294 connection_options.username.clone(),
6295 )
6296 .await?;
6297
6298 let serialized_workspace =
6299 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
6300
6301 let workspace_id = if let Some(workspace_id) =
6302 serialized_workspace.as_ref().map(|workspace| workspace.id)
6303 {
6304 workspace_id
6305 } else {
6306 persistence::DB.next_id().await?
6307 };
6308
6309 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
6310 })
6311}
6312
6313pub fn join_in_room_project(
6314 project_id: u64,
6315 follow_user_id: u64,
6316 app_state: Arc<AppState>,
6317 cx: &mut App,
6318) -> Task<Result<()>> {
6319 let windows = cx.windows();
6320 cx.spawn(|mut cx| async move {
6321 let existing_workspace = windows.into_iter().find_map(|window_handle| {
6322 window_handle
6323 .downcast::<Workspace>()
6324 .and_then(|window_handle| {
6325 window_handle
6326 .update(&mut cx, |workspace, _window, cx| {
6327 if workspace.project().read(cx).remote_id() == Some(project_id) {
6328 Some(window_handle)
6329 } else {
6330 None
6331 }
6332 })
6333 .unwrap_or(None)
6334 })
6335 });
6336
6337 let workspace = if let Some(existing_workspace) = existing_workspace {
6338 existing_workspace
6339 } else {
6340 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
6341 let room = active_call
6342 .read_with(&cx, |call, _| call.room().cloned())?
6343 .ok_or_else(|| anyhow!("not in a call"))?;
6344 let project = room
6345 .update(&mut cx, |room, cx| {
6346 room.join_project(
6347 project_id,
6348 app_state.languages.clone(),
6349 app_state.fs.clone(),
6350 cx,
6351 )
6352 })?
6353 .await?;
6354
6355 let window_bounds_override = window_bounds_env_override();
6356 cx.update(|cx| {
6357 let mut options = (app_state.build_window_options)(None, cx);
6358 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
6359 cx.open_window(options, |window, cx| {
6360 cx.new(|cx| {
6361 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
6362 })
6363 })
6364 })??
6365 };
6366
6367 workspace.update(&mut cx, |workspace, window, cx| {
6368 cx.activate(true);
6369 window.activate_window();
6370
6371 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
6372 let follow_peer_id = room
6373 .read(cx)
6374 .remote_participants()
6375 .iter()
6376 .find(|(_, participant)| participant.user.id == follow_user_id)
6377 .map(|(_, p)| p.peer_id)
6378 .or_else(|| {
6379 // If we couldn't follow the given user, follow the host instead.
6380 let collaborator = workspace
6381 .project()
6382 .read(cx)
6383 .collaborators()
6384 .values()
6385 .find(|collaborator| collaborator.is_host)?;
6386 Some(collaborator.peer_id)
6387 });
6388
6389 if let Some(follow_peer_id) = follow_peer_id {
6390 workspace.follow(follow_peer_id, window, cx);
6391 }
6392 }
6393 })?;
6394
6395 anyhow::Ok(())
6396 })
6397}
6398
6399pub fn reload(reload: &Reload, cx: &mut App) {
6400 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
6401 let mut workspace_windows = cx
6402 .windows()
6403 .into_iter()
6404 .filter_map(|window| window.downcast::<Workspace>())
6405 .collect::<Vec<_>>();
6406
6407 // If multiple windows have unsaved changes, and need a save prompt,
6408 // prompt in the active window before switching to a different window.
6409 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
6410
6411 let mut prompt = None;
6412 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
6413 prompt = window
6414 .update(cx, |_, window, cx| {
6415 window.prompt(
6416 PromptLevel::Info,
6417 "Are you sure you want to restart?",
6418 None,
6419 &["Restart", "Cancel"],
6420 cx,
6421 )
6422 })
6423 .ok();
6424 }
6425
6426 let binary_path = reload.binary_path.clone();
6427 cx.spawn(|mut cx| async move {
6428 if let Some(prompt) = prompt {
6429 let answer = prompt.await?;
6430 if answer != 0 {
6431 return Ok(());
6432 }
6433 }
6434
6435 // If the user cancels any save prompt, then keep the app open.
6436 for window in workspace_windows {
6437 if let Ok(should_close) = window.update(&mut cx, |workspace, window, cx| {
6438 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
6439 }) {
6440 if !should_close.await? {
6441 return Ok(());
6442 }
6443 }
6444 }
6445
6446 cx.update(|cx| cx.restart(binary_path))
6447 })
6448 .detach_and_log_err(cx);
6449}
6450
6451fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
6452 let mut parts = value.split(',');
6453 let x: usize = parts.next()?.parse().ok()?;
6454 let y: usize = parts.next()?.parse().ok()?;
6455 Some(point(px(x as f32), px(y as f32)))
6456}
6457
6458fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
6459 let mut parts = value.split(',');
6460 let width: usize = parts.next()?.parse().ok()?;
6461 let height: usize = parts.next()?.parse().ok()?;
6462 Some(size(px(width as f32), px(height as f32)))
6463}
6464
6465pub fn client_side_decorations(
6466 element: impl IntoElement,
6467 window: &mut Window,
6468 cx: &mut App,
6469) -> Stateful<Div> {
6470 const BORDER_SIZE: Pixels = px(1.0);
6471 let decorations = window.window_decorations();
6472
6473 if matches!(decorations, Decorations::Client { .. }) {
6474 window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6475 }
6476
6477 struct GlobalResizeEdge(ResizeEdge);
6478 impl Global for GlobalResizeEdge {}
6479
6480 div()
6481 .id("window-backdrop")
6482 .bg(transparent_black())
6483 .map(|div| match decorations {
6484 Decorations::Server => div,
6485 Decorations::Client { tiling, .. } => div
6486 .when(!(tiling.top || tiling.right), |div| {
6487 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6488 })
6489 .when(!(tiling.top || tiling.left), |div| {
6490 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6491 })
6492 .when(!(tiling.bottom || tiling.right), |div| {
6493 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6494 })
6495 .when(!(tiling.bottom || tiling.left), |div| {
6496 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6497 })
6498 .when(!tiling.top, |div| {
6499 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6500 })
6501 .when(!tiling.bottom, |div| {
6502 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6503 })
6504 .when(!tiling.left, |div| {
6505 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6506 })
6507 .when(!tiling.right, |div| {
6508 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6509 })
6510 .on_mouse_move(move |e, window, cx| {
6511 let size = window.window_bounds().get_bounds().size;
6512 let pos = e.position;
6513
6514 let new_edge =
6515 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6516
6517 let edge = cx.try_global::<GlobalResizeEdge>();
6518 if new_edge != edge.map(|edge| edge.0) {
6519 window
6520 .window_handle()
6521 .update(cx, |workspace, _, cx| {
6522 cx.notify(workspace.entity_id());
6523 })
6524 .ok();
6525 }
6526 })
6527 .on_mouse_down(MouseButton::Left, move |e, window, _| {
6528 let size = window.window_bounds().get_bounds().size;
6529 let pos = e.position;
6530
6531 let edge = match resize_edge(
6532 pos,
6533 theme::CLIENT_SIDE_DECORATION_SHADOW,
6534 size,
6535 tiling,
6536 ) {
6537 Some(value) => value,
6538 None => return,
6539 };
6540
6541 window.start_window_resize(edge);
6542 }),
6543 })
6544 .size_full()
6545 .child(
6546 div()
6547 .cursor(CursorStyle::Arrow)
6548 .map(|div| match decorations {
6549 Decorations::Server => div,
6550 Decorations::Client { tiling } => div
6551 .border_color(cx.theme().colors().border)
6552 .when(!(tiling.top || tiling.right), |div| {
6553 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6554 })
6555 .when(!(tiling.top || tiling.left), |div| {
6556 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6557 })
6558 .when(!(tiling.bottom || tiling.right), |div| {
6559 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6560 })
6561 .when(!(tiling.bottom || tiling.left), |div| {
6562 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6563 })
6564 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6565 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6566 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6567 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6568 .when(!tiling.is_tiled(), |div| {
6569 div.shadow(smallvec::smallvec![gpui::BoxShadow {
6570 color: Hsla {
6571 h: 0.,
6572 s: 0.,
6573 l: 0.,
6574 a: 0.4,
6575 },
6576 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6577 spread_radius: px(0.),
6578 offset: point(px(0.0), px(0.0)),
6579 }])
6580 }),
6581 })
6582 .on_mouse_move(|_e, _, cx| {
6583 cx.stop_propagation();
6584 })
6585 .size_full()
6586 .child(element),
6587 )
6588 .map(|div| match decorations {
6589 Decorations::Server => div,
6590 Decorations::Client { tiling, .. } => div.child(
6591 canvas(
6592 |_bounds, window, _| {
6593 window.insert_hitbox(
6594 Bounds::new(
6595 point(px(0.0), px(0.0)),
6596 window.window_bounds().get_bounds().size,
6597 ),
6598 false,
6599 )
6600 },
6601 move |_bounds, hitbox, window, cx| {
6602 let mouse = window.mouse_position();
6603 let size = window.window_bounds().get_bounds().size;
6604 let Some(edge) =
6605 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6606 else {
6607 return;
6608 };
6609 cx.set_global(GlobalResizeEdge(edge));
6610 window.set_cursor_style(
6611 match edge {
6612 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6613 ResizeEdge::Left | ResizeEdge::Right => {
6614 CursorStyle::ResizeLeftRight
6615 }
6616 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6617 CursorStyle::ResizeUpLeftDownRight
6618 }
6619 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6620 CursorStyle::ResizeUpRightDownLeft
6621 }
6622 },
6623 &hitbox,
6624 );
6625 },
6626 )
6627 .size_full()
6628 .absolute(),
6629 ),
6630 })
6631}
6632
6633fn resize_edge(
6634 pos: Point<Pixels>,
6635 shadow_size: Pixels,
6636 window_size: Size<Pixels>,
6637 tiling: Tiling,
6638) -> Option<ResizeEdge> {
6639 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6640 if bounds.contains(&pos) {
6641 return None;
6642 }
6643
6644 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6645 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6646 if !tiling.top && top_left_bounds.contains(&pos) {
6647 return Some(ResizeEdge::TopLeft);
6648 }
6649
6650 let top_right_bounds = Bounds::new(
6651 Point::new(window_size.width - corner_size.width, px(0.)),
6652 corner_size,
6653 );
6654 if !tiling.top && top_right_bounds.contains(&pos) {
6655 return Some(ResizeEdge::TopRight);
6656 }
6657
6658 let bottom_left_bounds = Bounds::new(
6659 Point::new(px(0.), window_size.height - corner_size.height),
6660 corner_size,
6661 );
6662 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6663 return Some(ResizeEdge::BottomLeft);
6664 }
6665
6666 let bottom_right_bounds = Bounds::new(
6667 Point::new(
6668 window_size.width - corner_size.width,
6669 window_size.height - corner_size.height,
6670 ),
6671 corner_size,
6672 );
6673 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6674 return Some(ResizeEdge::BottomRight);
6675 }
6676
6677 if !tiling.top && pos.y < shadow_size {
6678 Some(ResizeEdge::Top)
6679 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6680 Some(ResizeEdge::Bottom)
6681 } else if !tiling.left && pos.x < shadow_size {
6682 Some(ResizeEdge::Left)
6683 } else if !tiling.right && pos.x > window_size.width - shadow_size {
6684 Some(ResizeEdge::Right)
6685 } else {
6686 None
6687 }
6688}
6689
6690fn join_pane_into_active(
6691 active_pane: &Entity<Pane>,
6692 pane: &Entity<Pane>,
6693 window: &mut Window,
6694 cx: &mut App,
6695) {
6696 if pane == active_pane {
6697 return;
6698 } else if pane.read(cx).items_len() == 0 {
6699 pane.update(cx, |_, cx| {
6700 cx.emit(pane::Event::Remove {
6701 focus_on_pane: None,
6702 });
6703 })
6704 } else {
6705 move_all_items(pane, active_pane, window, cx);
6706 }
6707}
6708
6709fn move_all_items(
6710 from_pane: &Entity<Pane>,
6711 to_pane: &Entity<Pane>,
6712 window: &mut Window,
6713 cx: &mut App,
6714) {
6715 let destination_is_different = from_pane != to_pane;
6716 let mut moved_items = 0;
6717 for (item_ix, item_handle) in from_pane
6718 .read(cx)
6719 .items()
6720 .enumerate()
6721 .map(|(ix, item)| (ix, item.clone()))
6722 .collect::<Vec<_>>()
6723 {
6724 let ix = item_ix - moved_items;
6725 if destination_is_different {
6726 // Close item from previous pane
6727 from_pane.update(cx, |source, cx| {
6728 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
6729 });
6730 moved_items += 1;
6731 }
6732
6733 // This automatically removes duplicate items in the pane
6734 to_pane.update(cx, |destination, cx| {
6735 destination.add_item(item_handle, true, true, None, window, cx);
6736 window.focus(&destination.focus_handle(cx))
6737 });
6738 }
6739}
6740
6741pub fn move_item(
6742 source: &Entity<Pane>,
6743 destination: &Entity<Pane>,
6744 item_id_to_move: EntityId,
6745 destination_index: usize,
6746 window: &mut Window,
6747 cx: &mut App,
6748) {
6749 let Some((item_ix, item_handle)) = source
6750 .read(cx)
6751 .items()
6752 .enumerate()
6753 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6754 .map(|(ix, item)| (ix, item.clone()))
6755 else {
6756 // Tab was closed during drag
6757 return;
6758 };
6759
6760 if source != destination {
6761 // Close item from previous pane
6762 source.update(cx, |source, cx| {
6763 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
6764 });
6765 }
6766
6767 // This automatically removes duplicate items in the pane
6768 destination.update(cx, |destination, cx| {
6769 destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
6770 window.focus(&destination.focus_handle(cx))
6771 });
6772}
6773
6774pub fn move_active_item(
6775 source: &Entity<Pane>,
6776 destination: &Entity<Pane>,
6777 focus_destination: bool,
6778 close_if_empty: bool,
6779 window: &mut Window,
6780 cx: &mut App,
6781) {
6782 if source == destination {
6783 return;
6784 }
6785 let Some(active_item) = source.read(cx).active_item() else {
6786 return;
6787 };
6788 source.update(cx, |source_pane, cx| {
6789 let item_id = active_item.item_id();
6790 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
6791 destination.update(cx, |target_pane, cx| {
6792 target_pane.add_item(
6793 active_item,
6794 focus_destination,
6795 focus_destination,
6796 Some(target_pane.items_len()),
6797 window,
6798 cx,
6799 );
6800 });
6801 });
6802}
6803
6804#[cfg(test)]
6805mod tests {
6806 use std::{cell::RefCell, rc::Rc};
6807
6808 use super::*;
6809 use crate::{
6810 dock::{test::TestPanel, PanelEvent},
6811 item::{
6812 test::{TestItem, TestProjectItem},
6813 ItemEvent,
6814 },
6815 };
6816 use fs::FakeFs;
6817 use gpui::{
6818 px, DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
6819 UpdateGlobal, VisualTestContext,
6820 };
6821 use project::{Project, ProjectEntryId};
6822 use serde_json::json;
6823 use settings::SettingsStore;
6824
6825 #[gpui::test]
6826 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6827 init_test(cx);
6828
6829 let fs = FakeFs::new(cx.executor());
6830 let project = Project::test(fs, [], cx).await;
6831 let (workspace, cx) =
6832 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6833
6834 // Adding an item with no ambiguity renders the tab without detail.
6835 let item1 = cx.new(|cx| {
6836 let mut item = TestItem::new(cx);
6837 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6838 item
6839 });
6840 workspace.update_in(cx, |workspace, window, cx| {
6841 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6842 });
6843 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6844
6845 // Adding an item that creates ambiguity increases the level of detail on
6846 // both tabs.
6847 let item2 = cx.new_window_entity(|_window, cx| {
6848 let mut item = TestItem::new(cx);
6849 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6850 item
6851 });
6852 workspace.update_in(cx, |workspace, window, cx| {
6853 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6854 });
6855 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6856 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6857
6858 // Adding an item that creates ambiguity increases the level of detail only
6859 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6860 // we stop at the highest detail available.
6861 let item3 = cx.new(|cx| {
6862 let mut item = TestItem::new(cx);
6863 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6864 item
6865 });
6866 workspace.update_in(cx, |workspace, window, cx| {
6867 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6868 });
6869 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6870 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6871 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6872 }
6873
6874 #[gpui::test]
6875 async fn test_tracking_active_path(cx: &mut TestAppContext) {
6876 init_test(cx);
6877
6878 let fs = FakeFs::new(cx.executor());
6879 fs.insert_tree(
6880 "/root1",
6881 json!({
6882 "one.txt": "",
6883 "two.txt": "",
6884 }),
6885 )
6886 .await;
6887 fs.insert_tree(
6888 "/root2",
6889 json!({
6890 "three.txt": "",
6891 }),
6892 )
6893 .await;
6894
6895 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6896 let (workspace, cx) =
6897 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6898 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6899 let worktree_id = project.update(cx, |project, cx| {
6900 project.worktrees(cx).next().unwrap().read(cx).id()
6901 });
6902
6903 let item1 = cx.new(|cx| {
6904 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6905 });
6906 let item2 = cx.new(|cx| {
6907 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6908 });
6909
6910 // Add an item to an empty pane
6911 workspace.update_in(cx, |workspace, window, cx| {
6912 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
6913 });
6914 project.update(cx, |project, cx| {
6915 assert_eq!(
6916 project.active_entry(),
6917 project
6918 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6919 .map(|e| e.id)
6920 );
6921 });
6922 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6923
6924 // Add a second item to a non-empty pane
6925 workspace.update_in(cx, |workspace, window, cx| {
6926 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
6927 });
6928 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6929 project.update(cx, |project, cx| {
6930 assert_eq!(
6931 project.active_entry(),
6932 project
6933 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6934 .map(|e| e.id)
6935 );
6936 });
6937
6938 // Close the active item
6939 pane.update_in(cx, |pane, window, cx| {
6940 pane.close_active_item(&Default::default(), window, cx)
6941 .unwrap()
6942 })
6943 .await
6944 .unwrap();
6945 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6946 project.update(cx, |project, cx| {
6947 assert_eq!(
6948 project.active_entry(),
6949 project
6950 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6951 .map(|e| e.id)
6952 );
6953 });
6954
6955 // Add a project folder
6956 project
6957 .update(cx, |project, cx| {
6958 project.find_or_create_worktree("root2", true, cx)
6959 })
6960 .await
6961 .unwrap();
6962 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6963
6964 // Remove a project folder
6965 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6966 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6967 }
6968
6969 #[gpui::test]
6970 async fn test_close_window(cx: &mut TestAppContext) {
6971 init_test(cx);
6972
6973 let fs = FakeFs::new(cx.executor());
6974 fs.insert_tree("/root", json!({ "one": "" })).await;
6975
6976 let project = Project::test(fs, ["root".as_ref()], cx).await;
6977 let (workspace, cx) =
6978 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6979
6980 // When there are no dirty items, there's nothing to do.
6981 let item1 = cx.new(TestItem::new);
6982 workspace.update_in(cx, |w, window, cx| {
6983 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
6984 });
6985 let task = workspace.update_in(cx, |w, window, cx| {
6986 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6987 });
6988 assert!(task.await.unwrap());
6989
6990 // When there are dirty untitled items, prompt to save each one. If the user
6991 // cancels any prompt, then abort.
6992 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
6993 let item3 = cx.new(|cx| {
6994 TestItem::new(cx)
6995 .with_dirty(true)
6996 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6997 });
6998 workspace.update_in(cx, |w, window, cx| {
6999 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7000 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7001 });
7002 let task = workspace.update_in(cx, |w, window, cx| {
7003 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7004 });
7005 cx.executor().run_until_parked();
7006 cx.simulate_prompt_answer("Cancel"); // cancel save all
7007 cx.executor().run_until_parked();
7008 assert!(!cx.has_pending_prompt());
7009 assert!(!task.await.unwrap());
7010 }
7011
7012 #[gpui::test]
7013 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
7014 init_test(cx);
7015
7016 // Register TestItem as a serializable item
7017 cx.update(|cx| {
7018 register_serializable_item::<TestItem>(cx);
7019 });
7020
7021 let fs = FakeFs::new(cx.executor());
7022 fs.insert_tree("/root", json!({ "one": "" })).await;
7023
7024 let project = Project::test(fs, ["root".as_ref()], cx).await;
7025 let (workspace, cx) =
7026 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7027
7028 // When there are dirty untitled items, but they can serialize, then there is no prompt.
7029 let item1 = cx.new(|cx| {
7030 TestItem::new(cx)
7031 .with_dirty(true)
7032 .with_serialize(|| Some(Task::ready(Ok(()))))
7033 });
7034 let item2 = cx.new(|cx| {
7035 TestItem::new(cx)
7036 .with_dirty(true)
7037 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7038 .with_serialize(|| Some(Task::ready(Ok(()))))
7039 });
7040 workspace.update_in(cx, |w, window, cx| {
7041 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7042 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7043 });
7044 let task = workspace.update_in(cx, |w, window, cx| {
7045 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7046 });
7047 assert!(task.await.unwrap());
7048 }
7049
7050 #[gpui::test]
7051 async fn test_close_pane_items(cx: &mut TestAppContext) {
7052 init_test(cx);
7053
7054 let fs = FakeFs::new(cx.executor());
7055
7056 let project = Project::test(fs, None, cx).await;
7057 let (workspace, cx) =
7058 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7059
7060 let item1 = cx.new(|cx| {
7061 TestItem::new(cx)
7062 .with_dirty(true)
7063 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7064 });
7065 let item2 = cx.new(|cx| {
7066 TestItem::new(cx)
7067 .with_dirty(true)
7068 .with_conflict(true)
7069 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7070 });
7071 let item3 = cx.new(|cx| {
7072 TestItem::new(cx)
7073 .with_dirty(true)
7074 .with_conflict(true)
7075 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7076 });
7077 let item4 = cx.new(|cx| {
7078 TestItem::new(cx).with_dirty(true).with_project_items(&[{
7079 let project_item = TestProjectItem::new_untitled(cx);
7080 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7081 project_item
7082 }])
7083 });
7084 let pane = workspace.update_in(cx, |workspace, window, cx| {
7085 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7086 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7087 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7088 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7089 workspace.active_pane().clone()
7090 });
7091
7092 let close_items = pane.update_in(cx, |pane, window, cx| {
7093 pane.activate_item(1, true, true, window, cx);
7094 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7095 let item1_id = item1.item_id();
7096 let item3_id = item3.item_id();
7097 let item4_id = item4.item_id();
7098 pane.close_items(window, cx, SaveIntent::Close, move |id| {
7099 [item1_id, item3_id, item4_id].contains(&id)
7100 })
7101 });
7102 cx.executor().run_until_parked();
7103
7104 assert!(cx.has_pending_prompt());
7105 cx.simulate_prompt_answer("Save all");
7106
7107 cx.executor().run_until_parked();
7108
7109 // Item 1 is saved. There's a prompt to save item 3.
7110 pane.update(cx, |pane, cx| {
7111 assert_eq!(item1.read(cx).save_count, 1);
7112 assert_eq!(item1.read(cx).save_as_count, 0);
7113 assert_eq!(item1.read(cx).reload_count, 0);
7114 assert_eq!(pane.items_len(), 3);
7115 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7116 });
7117 assert!(cx.has_pending_prompt());
7118
7119 // Cancel saving item 3.
7120 cx.simulate_prompt_answer("Discard");
7121 cx.executor().run_until_parked();
7122
7123 // Item 3 is reloaded. There's a prompt to save item 4.
7124 pane.update(cx, |pane, cx| {
7125 assert_eq!(item3.read(cx).save_count, 0);
7126 assert_eq!(item3.read(cx).save_as_count, 0);
7127 assert_eq!(item3.read(cx).reload_count, 1);
7128 assert_eq!(pane.items_len(), 2);
7129 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7130 });
7131
7132 // There's a prompt for a path for item 4.
7133 cx.simulate_new_path_selection(|_| Some(Default::default()));
7134 close_items.await.unwrap();
7135
7136 // The requested items are closed.
7137 pane.update(cx, |pane, cx| {
7138 assert_eq!(item4.read(cx).save_count, 0);
7139 assert_eq!(item4.read(cx).save_as_count, 1);
7140 assert_eq!(item4.read(cx).reload_count, 0);
7141 assert_eq!(pane.items_len(), 1);
7142 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7143 });
7144 }
7145
7146 #[gpui::test]
7147 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7148 init_test(cx);
7149
7150 let fs = FakeFs::new(cx.executor());
7151 let project = Project::test(fs, [], cx).await;
7152 let (workspace, cx) =
7153 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7154
7155 // Create several workspace items with single project entries, and two
7156 // workspace items with multiple project entries.
7157 let single_entry_items = (0..=4)
7158 .map(|project_entry_id| {
7159 cx.new(|cx| {
7160 TestItem::new(cx)
7161 .with_dirty(true)
7162 .with_project_items(&[dirty_project_item(
7163 project_entry_id,
7164 &format!("{project_entry_id}.txt"),
7165 cx,
7166 )])
7167 })
7168 })
7169 .collect::<Vec<_>>();
7170 let item_2_3 = cx.new(|cx| {
7171 TestItem::new(cx)
7172 .with_dirty(true)
7173 .with_singleton(false)
7174 .with_project_items(&[
7175 single_entry_items[2].read(cx).project_items[0].clone(),
7176 single_entry_items[3].read(cx).project_items[0].clone(),
7177 ])
7178 });
7179 let item_3_4 = cx.new(|cx| {
7180 TestItem::new(cx)
7181 .with_dirty(true)
7182 .with_singleton(false)
7183 .with_project_items(&[
7184 single_entry_items[3].read(cx).project_items[0].clone(),
7185 single_entry_items[4].read(cx).project_items[0].clone(),
7186 ])
7187 });
7188
7189 // Create two panes that contain the following project entries:
7190 // left pane:
7191 // multi-entry items: (2, 3)
7192 // single-entry items: 0, 2, 3, 4
7193 // right pane:
7194 // single-entry items: 4, 1
7195 // multi-entry items: (3, 4)
7196 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
7197 let left_pane = workspace.active_pane().clone();
7198 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7199 workspace.add_item_to_active_pane(
7200 single_entry_items[0].boxed_clone(),
7201 None,
7202 true,
7203 window,
7204 cx,
7205 );
7206 workspace.add_item_to_active_pane(
7207 single_entry_items[2].boxed_clone(),
7208 None,
7209 true,
7210 window,
7211 cx,
7212 );
7213 workspace.add_item_to_active_pane(
7214 single_entry_items[3].boxed_clone(),
7215 None,
7216 true,
7217 window,
7218 cx,
7219 );
7220 workspace.add_item_to_active_pane(
7221 single_entry_items[4].boxed_clone(),
7222 None,
7223 true,
7224 window,
7225 cx,
7226 );
7227
7228 let right_pane = workspace
7229 .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7230 .unwrap();
7231
7232 right_pane.update(cx, |pane, cx| {
7233 pane.add_item(
7234 single_entry_items[1].boxed_clone(),
7235 true,
7236 true,
7237 None,
7238 window,
7239 cx,
7240 );
7241 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
7242 });
7243
7244 (left_pane, right_pane)
7245 });
7246
7247 cx.focus(&right_pane);
7248
7249 let mut close = right_pane.update_in(cx, |pane, window, cx| {
7250 pane.close_all_items(&CloseAllItems::default(), window, cx)
7251 .unwrap()
7252 });
7253 cx.executor().run_until_parked();
7254
7255 let msg = cx.pending_prompt().unwrap().0;
7256 assert!(msg.contains("1.txt"));
7257 assert!(!msg.contains("2.txt"));
7258 assert!(!msg.contains("3.txt"));
7259 assert!(!msg.contains("4.txt"));
7260
7261 cx.simulate_prompt_answer("Cancel");
7262 close.await.unwrap();
7263
7264 left_pane
7265 .update_in(cx, |left_pane, window, cx| {
7266 left_pane.close_item_by_id(
7267 single_entry_items[3].entity_id(),
7268 SaveIntent::Skip,
7269 window,
7270 cx,
7271 )
7272 })
7273 .await
7274 .unwrap();
7275
7276 close = right_pane.update_in(cx, |pane, window, cx| {
7277 pane.close_all_items(&CloseAllItems::default(), window, cx)
7278 .unwrap()
7279 });
7280 cx.executor().run_until_parked();
7281
7282 let details = cx.pending_prompt().unwrap().1;
7283 assert!(details.contains("1.txt"));
7284 assert!(!details.contains("2.txt"));
7285 assert!(details.contains("3.txt"));
7286 // ideally this assertion could be made, but today we can only
7287 // save whole items not project items, so the orphaned item 3 causes
7288 // 4 to be saved too.
7289 // assert!(!details.contains("4.txt"));
7290
7291 cx.simulate_prompt_answer("Save all");
7292
7293 cx.executor().run_until_parked();
7294 close.await.unwrap();
7295 right_pane.update(cx, |pane, _| {
7296 assert_eq!(pane.items_len(), 0);
7297 });
7298 }
7299
7300 #[gpui::test]
7301 async fn test_autosave(cx: &mut gpui::TestAppContext) {
7302 init_test(cx);
7303
7304 let fs = FakeFs::new(cx.executor());
7305 let project = Project::test(fs, [], cx).await;
7306 let (workspace, cx) =
7307 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7308 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7309
7310 let item = cx.new(|cx| {
7311 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7312 });
7313 let item_id = item.entity_id();
7314 workspace.update_in(cx, |workspace, window, cx| {
7315 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7316 });
7317
7318 // Autosave on window change.
7319 item.update(cx, |item, cx| {
7320 SettingsStore::update_global(cx, |settings, cx| {
7321 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7322 settings.autosave = Some(AutosaveSetting::OnWindowChange);
7323 })
7324 });
7325 item.is_dirty = true;
7326 });
7327
7328 // Deactivating the window saves the file.
7329 cx.deactivate_window();
7330 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7331
7332 // Re-activating the window doesn't save the file.
7333 cx.update(|window, _| window.activate_window());
7334 cx.executor().run_until_parked();
7335 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7336
7337 // Autosave on focus change.
7338 item.update_in(cx, |item, window, cx| {
7339 cx.focus_self(window);
7340 SettingsStore::update_global(cx, |settings, cx| {
7341 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7342 settings.autosave = Some(AutosaveSetting::OnFocusChange);
7343 })
7344 });
7345 item.is_dirty = true;
7346 });
7347
7348 // Blurring the item saves the file.
7349 item.update_in(cx, |_, window, _| window.blur());
7350 cx.executor().run_until_parked();
7351 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
7352
7353 // Deactivating the window still saves the file.
7354 item.update_in(cx, |item, window, cx| {
7355 cx.focus_self(window);
7356 item.is_dirty = true;
7357 });
7358 cx.deactivate_window();
7359 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7360
7361 // Autosave after delay.
7362 item.update(cx, |item, cx| {
7363 SettingsStore::update_global(cx, |settings, cx| {
7364 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7365 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
7366 })
7367 });
7368 item.is_dirty = true;
7369 cx.emit(ItemEvent::Edit);
7370 });
7371
7372 // Delay hasn't fully expired, so the file is still dirty and unsaved.
7373 cx.executor().advance_clock(Duration::from_millis(250));
7374 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7375
7376 // After delay expires, the file is saved.
7377 cx.executor().advance_clock(Duration::from_millis(250));
7378 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
7379
7380 // Autosave on focus change, ensuring closing the tab counts as such.
7381 item.update(cx, |item, cx| {
7382 SettingsStore::update_global(cx, |settings, cx| {
7383 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7384 settings.autosave = Some(AutosaveSetting::OnFocusChange);
7385 })
7386 });
7387 item.is_dirty = true;
7388 for project_item in &mut item.project_items {
7389 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7390 }
7391 });
7392
7393 pane.update_in(cx, |pane, window, cx| {
7394 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7395 })
7396 .await
7397 .unwrap();
7398 assert!(!cx.has_pending_prompt());
7399 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7400
7401 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
7402 workspace.update_in(cx, |workspace, window, cx| {
7403 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7404 });
7405 item.update_in(cx, |item, window, cx| {
7406 item.project_items[0].update(cx, |item, _| {
7407 item.entry_id = None;
7408 });
7409 item.is_dirty = true;
7410 window.blur();
7411 });
7412 cx.run_until_parked();
7413 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7414
7415 // Ensure autosave is prevented for deleted files also when closing the buffer.
7416 let _close_items = pane.update_in(cx, |pane, window, cx| {
7417 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7418 });
7419 cx.run_until_parked();
7420 assert!(cx.has_pending_prompt());
7421 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7422 }
7423
7424 #[gpui::test]
7425 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
7426 init_test(cx);
7427
7428 let fs = FakeFs::new(cx.executor());
7429
7430 let project = Project::test(fs, [], cx).await;
7431 let (workspace, cx) =
7432 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7433
7434 let item = cx.new(|cx| {
7435 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7436 });
7437 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7438 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
7439 let toolbar_notify_count = Rc::new(RefCell::new(0));
7440
7441 workspace.update_in(cx, |workspace, window, cx| {
7442 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7443 let toolbar_notification_count = toolbar_notify_count.clone();
7444 cx.observe_in(&toolbar, window, move |_, _, _, _| {
7445 *toolbar_notification_count.borrow_mut() += 1
7446 })
7447 .detach();
7448 });
7449
7450 pane.update(cx, |pane, _| {
7451 assert!(!pane.can_navigate_backward());
7452 assert!(!pane.can_navigate_forward());
7453 });
7454
7455 item.update_in(cx, |item, _, cx| {
7456 item.set_state("one".to_string(), cx);
7457 });
7458
7459 // Toolbar must be notified to re-render the navigation buttons
7460 assert_eq!(*toolbar_notify_count.borrow(), 1);
7461
7462 pane.update(cx, |pane, _| {
7463 assert!(pane.can_navigate_backward());
7464 assert!(!pane.can_navigate_forward());
7465 });
7466
7467 workspace
7468 .update_in(cx, |workspace, window, cx| {
7469 workspace.go_back(pane.downgrade(), window, cx)
7470 })
7471 .await
7472 .unwrap();
7473
7474 assert_eq!(*toolbar_notify_count.borrow(), 2);
7475 pane.update(cx, |pane, _| {
7476 assert!(!pane.can_navigate_backward());
7477 assert!(pane.can_navigate_forward());
7478 });
7479 }
7480
7481 #[gpui::test]
7482 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
7483 init_test(cx);
7484 let fs = FakeFs::new(cx.executor());
7485
7486 let project = Project::test(fs, [], cx).await;
7487 let (workspace, cx) =
7488 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7489
7490 let panel = workspace.update_in(cx, |workspace, window, cx| {
7491 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7492 workspace.add_panel(panel.clone(), window, cx);
7493
7494 workspace
7495 .right_dock()
7496 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
7497
7498 panel
7499 });
7500
7501 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7502 pane.update_in(cx, |pane, window, cx| {
7503 let item = cx.new(TestItem::new);
7504 pane.add_item(Box::new(item), true, true, None, window, cx);
7505 });
7506
7507 // Transfer focus from center to panel
7508 workspace.update_in(cx, |workspace, window, cx| {
7509 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7510 });
7511
7512 workspace.update_in(cx, |workspace, window, cx| {
7513 assert!(workspace.right_dock().read(cx).is_open());
7514 assert!(!panel.is_zoomed(window, cx));
7515 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7516 });
7517
7518 // Transfer focus from panel to center
7519 workspace.update_in(cx, |workspace, window, cx| {
7520 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7521 });
7522
7523 workspace.update_in(cx, |workspace, window, cx| {
7524 assert!(workspace.right_dock().read(cx).is_open());
7525 assert!(!panel.is_zoomed(window, cx));
7526 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7527 });
7528
7529 // Close the dock
7530 workspace.update_in(cx, |workspace, window, cx| {
7531 workspace.toggle_dock(DockPosition::Right, window, cx);
7532 });
7533
7534 workspace.update_in(cx, |workspace, window, cx| {
7535 assert!(!workspace.right_dock().read(cx).is_open());
7536 assert!(!panel.is_zoomed(window, cx));
7537 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7538 });
7539
7540 // Open the dock
7541 workspace.update_in(cx, |workspace, window, cx| {
7542 workspace.toggle_dock(DockPosition::Right, window, cx);
7543 });
7544
7545 workspace.update_in(cx, |workspace, window, cx| {
7546 assert!(workspace.right_dock().read(cx).is_open());
7547 assert!(!panel.is_zoomed(window, cx));
7548 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7549 });
7550
7551 // Focus and zoom panel
7552 panel.update_in(cx, |panel, window, cx| {
7553 cx.focus_self(window);
7554 panel.set_zoomed(true, window, cx)
7555 });
7556
7557 workspace.update_in(cx, |workspace, window, cx| {
7558 assert!(workspace.right_dock().read(cx).is_open());
7559 assert!(panel.is_zoomed(window, cx));
7560 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7561 });
7562
7563 // Transfer focus to the center closes the dock
7564 workspace.update_in(cx, |workspace, window, cx| {
7565 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7566 });
7567
7568 workspace.update_in(cx, |workspace, window, cx| {
7569 assert!(!workspace.right_dock().read(cx).is_open());
7570 assert!(panel.is_zoomed(window, cx));
7571 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7572 });
7573
7574 // Transferring focus back to the panel keeps it zoomed
7575 workspace.update_in(cx, |workspace, window, cx| {
7576 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7577 });
7578
7579 workspace.update_in(cx, |workspace, window, cx| {
7580 assert!(workspace.right_dock().read(cx).is_open());
7581 assert!(panel.is_zoomed(window, cx));
7582 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7583 });
7584
7585 // Close the dock while it is zoomed
7586 workspace.update_in(cx, |workspace, window, cx| {
7587 workspace.toggle_dock(DockPosition::Right, window, cx)
7588 });
7589
7590 workspace.update_in(cx, |workspace, window, cx| {
7591 assert!(!workspace.right_dock().read(cx).is_open());
7592 assert!(panel.is_zoomed(window, cx));
7593 assert!(workspace.zoomed.is_none());
7594 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7595 });
7596
7597 // Opening the dock, when it's zoomed, retains focus
7598 workspace.update_in(cx, |workspace, window, cx| {
7599 workspace.toggle_dock(DockPosition::Right, window, cx)
7600 });
7601
7602 workspace.update_in(cx, |workspace, window, cx| {
7603 assert!(workspace.right_dock().read(cx).is_open());
7604 assert!(panel.is_zoomed(window, cx));
7605 assert!(workspace.zoomed.is_some());
7606 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7607 });
7608
7609 // Unzoom and close the panel, zoom the active pane.
7610 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
7611 workspace.update_in(cx, |workspace, window, cx| {
7612 workspace.toggle_dock(DockPosition::Right, window, cx)
7613 });
7614 pane.update_in(cx, |pane, window, cx| {
7615 pane.toggle_zoom(&Default::default(), window, cx)
7616 });
7617
7618 // Opening a dock unzooms the pane.
7619 workspace.update_in(cx, |workspace, window, cx| {
7620 workspace.toggle_dock(DockPosition::Right, window, cx)
7621 });
7622 workspace.update_in(cx, |workspace, window, cx| {
7623 let pane = pane.read(cx);
7624 assert!(!pane.is_zoomed());
7625 assert!(!pane.focus_handle(cx).is_focused(window));
7626 assert!(workspace.right_dock().read(cx).is_open());
7627 assert!(workspace.zoomed.is_none());
7628 });
7629 }
7630
7631 #[gpui::test]
7632 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7633 init_test(cx);
7634
7635 let fs = FakeFs::new(cx.executor());
7636
7637 let project = Project::test(fs, None, cx).await;
7638 let (workspace, cx) =
7639 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7640
7641 // Let's arrange the panes like this:
7642 //
7643 // +-----------------------+
7644 // | top |
7645 // +------+--------+-------+
7646 // | left | center | right |
7647 // +------+--------+-------+
7648 // | bottom |
7649 // +-----------------------+
7650
7651 let top_item = cx.new(|cx| {
7652 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7653 });
7654 let bottom_item = cx.new(|cx| {
7655 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7656 });
7657 let left_item = cx.new(|cx| {
7658 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7659 });
7660 let right_item = cx.new(|cx| {
7661 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7662 });
7663 let center_item = cx.new(|cx| {
7664 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7665 });
7666
7667 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7668 let top_pane_id = workspace.active_pane().entity_id();
7669 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
7670 workspace.split_pane(
7671 workspace.active_pane().clone(),
7672 SplitDirection::Down,
7673 window,
7674 cx,
7675 );
7676 top_pane_id
7677 });
7678 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7679 let bottom_pane_id = workspace.active_pane().entity_id();
7680 workspace.add_item_to_active_pane(
7681 Box::new(bottom_item.clone()),
7682 None,
7683 false,
7684 window,
7685 cx,
7686 );
7687 workspace.split_pane(
7688 workspace.active_pane().clone(),
7689 SplitDirection::Up,
7690 window,
7691 cx,
7692 );
7693 bottom_pane_id
7694 });
7695 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7696 let left_pane_id = workspace.active_pane().entity_id();
7697 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
7698 workspace.split_pane(
7699 workspace.active_pane().clone(),
7700 SplitDirection::Right,
7701 window,
7702 cx,
7703 );
7704 left_pane_id
7705 });
7706 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7707 let right_pane_id = workspace.active_pane().entity_id();
7708 workspace.add_item_to_active_pane(
7709 Box::new(right_item.clone()),
7710 None,
7711 false,
7712 window,
7713 cx,
7714 );
7715 workspace.split_pane(
7716 workspace.active_pane().clone(),
7717 SplitDirection::Left,
7718 window,
7719 cx,
7720 );
7721 right_pane_id
7722 });
7723 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7724 let center_pane_id = workspace.active_pane().entity_id();
7725 workspace.add_item_to_active_pane(
7726 Box::new(center_item.clone()),
7727 None,
7728 false,
7729 window,
7730 cx,
7731 );
7732 center_pane_id
7733 });
7734 cx.executor().run_until_parked();
7735
7736 workspace.update_in(cx, |workspace, window, cx| {
7737 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7738
7739 // Join into next from center pane into right
7740 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7741 });
7742
7743 workspace.update_in(cx, |workspace, window, cx| {
7744 let active_pane = workspace.active_pane();
7745 assert_eq!(right_pane_id, active_pane.entity_id());
7746 assert_eq!(2, active_pane.read(cx).items_len());
7747 let item_ids_in_pane =
7748 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7749 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7750 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7751
7752 // Join into next from right pane into bottom
7753 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7754 });
7755
7756 workspace.update_in(cx, |workspace, window, cx| {
7757 let active_pane = workspace.active_pane();
7758 assert_eq!(bottom_pane_id, active_pane.entity_id());
7759 assert_eq!(3, active_pane.read(cx).items_len());
7760 let item_ids_in_pane =
7761 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7762 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7763 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7764 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7765
7766 // Join into next from bottom pane into left
7767 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7768 });
7769
7770 workspace.update_in(cx, |workspace, window, cx| {
7771 let active_pane = workspace.active_pane();
7772 assert_eq!(left_pane_id, active_pane.entity_id());
7773 assert_eq!(4, active_pane.read(cx).items_len());
7774 let item_ids_in_pane =
7775 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7776 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7777 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7778 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7779 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7780
7781 // Join into next from left pane into top
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!(top_pane_id, active_pane.entity_id());
7788 assert_eq!(5, 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 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7795 assert!(item_ids_in_pane.contains(&top_item.item_id()));
7796
7797 // Single pane left: no-op
7798 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
7799 });
7800
7801 workspace.update(cx, |workspace, _cx| {
7802 let active_pane = workspace.active_pane();
7803 assert_eq!(top_pane_id, active_pane.entity_id());
7804 });
7805 }
7806
7807 fn add_an_item_to_active_pane(
7808 cx: &mut VisualTestContext,
7809 workspace: &Entity<Workspace>,
7810 item_id: u64,
7811 ) -> Entity<TestItem> {
7812 let item = cx.new(|cx| {
7813 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7814 item_id,
7815 "item{item_id}.txt",
7816 cx,
7817 )])
7818 });
7819 workspace.update_in(cx, |workspace, window, cx| {
7820 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
7821 });
7822 return item;
7823 }
7824
7825 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
7826 return workspace.update_in(cx, |workspace, window, cx| {
7827 let new_pane = workspace.split_pane(
7828 workspace.active_pane().clone(),
7829 SplitDirection::Right,
7830 window,
7831 cx,
7832 );
7833 new_pane
7834 });
7835 }
7836
7837 #[gpui::test]
7838 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7839 init_test(cx);
7840 let fs = FakeFs::new(cx.executor());
7841 let project = Project::test(fs, None, cx).await;
7842 let (workspace, cx) =
7843 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7844
7845 add_an_item_to_active_pane(cx, &workspace, 1);
7846 split_pane(cx, &workspace);
7847 add_an_item_to_active_pane(cx, &workspace, 2);
7848 split_pane(cx, &workspace); // empty pane
7849 split_pane(cx, &workspace);
7850 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7851
7852 cx.executor().run_until_parked();
7853
7854 workspace.update(cx, |workspace, cx| {
7855 let num_panes = workspace.panes().len();
7856 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7857 let active_item = workspace
7858 .active_pane()
7859 .read(cx)
7860 .active_item()
7861 .expect("item is in focus");
7862
7863 assert_eq!(num_panes, 4);
7864 assert_eq!(num_items_in_current_pane, 1);
7865 assert_eq!(active_item.item_id(), last_item.item_id());
7866 });
7867
7868 workspace.update_in(cx, |workspace, window, cx| {
7869 workspace.join_all_panes(window, cx);
7870 });
7871
7872 workspace.update(cx, |workspace, cx| {
7873 let num_panes = workspace.panes().len();
7874 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7875 let active_item = workspace
7876 .active_pane()
7877 .read(cx)
7878 .active_item()
7879 .expect("item is in focus");
7880
7881 assert_eq!(num_panes, 1);
7882 assert_eq!(num_items_in_current_pane, 3);
7883 assert_eq!(active_item.item_id(), last_item.item_id());
7884 });
7885 }
7886 struct TestModal(FocusHandle);
7887
7888 impl TestModal {
7889 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
7890 Self(cx.focus_handle())
7891 }
7892 }
7893
7894 impl EventEmitter<DismissEvent> for TestModal {}
7895
7896 impl Focusable for TestModal {
7897 fn focus_handle(&self, _cx: &App) -> FocusHandle {
7898 self.0.clone()
7899 }
7900 }
7901
7902 impl ModalView for TestModal {}
7903
7904 impl Render for TestModal {
7905 fn render(
7906 &mut self,
7907 _window: &mut Window,
7908 _cx: &mut Context<TestModal>,
7909 ) -> impl IntoElement {
7910 div().track_focus(&self.0)
7911 }
7912 }
7913
7914 #[gpui::test]
7915 async fn test_panels(cx: &mut gpui::TestAppContext) {
7916 init_test(cx);
7917 let fs = FakeFs::new(cx.executor());
7918
7919 let project = Project::test(fs, [], cx).await;
7920 let (workspace, cx) =
7921 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7922
7923 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
7924 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
7925 workspace.add_panel(panel_1.clone(), window, cx);
7926 workspace.toggle_dock(DockPosition::Left, window, cx);
7927 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7928 workspace.add_panel(panel_2.clone(), window, cx);
7929 workspace.toggle_dock(DockPosition::Right, window, cx);
7930
7931 let left_dock = workspace.left_dock();
7932 assert_eq!(
7933 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7934 panel_1.panel_id()
7935 );
7936 assert_eq!(
7937 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7938 panel_1.size(window, cx)
7939 );
7940
7941 left_dock.update(cx, |left_dock, cx| {
7942 left_dock.resize_active_panel(Some(px(1337.)), window, cx)
7943 });
7944 assert_eq!(
7945 workspace
7946 .right_dock()
7947 .read(cx)
7948 .visible_panel()
7949 .unwrap()
7950 .panel_id(),
7951 panel_2.panel_id(),
7952 );
7953
7954 (panel_1, panel_2)
7955 });
7956
7957 // Move panel_1 to the right
7958 panel_1.update_in(cx, |panel_1, window, cx| {
7959 panel_1.set_position(DockPosition::Right, window, cx)
7960 });
7961
7962 workspace.update_in(cx, |workspace, window, cx| {
7963 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7964 // Since it was the only panel on the left, the left dock should now be closed.
7965 assert!(!workspace.left_dock().read(cx).is_open());
7966 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7967 let right_dock = workspace.right_dock();
7968 assert_eq!(
7969 right_dock.read(cx).visible_panel().unwrap().panel_id(),
7970 panel_1.panel_id()
7971 );
7972 assert_eq!(
7973 right_dock.read(cx).active_panel_size(window, cx).unwrap(),
7974 px(1337.)
7975 );
7976
7977 // Now we move panel_2 to the left
7978 panel_2.set_position(DockPosition::Left, window, cx);
7979 });
7980
7981 workspace.update(cx, |workspace, cx| {
7982 // Since panel_2 was not visible on the right, we don't open the left dock.
7983 assert!(!workspace.left_dock().read(cx).is_open());
7984 // And the right dock is unaffected in its displaying of panel_1
7985 assert!(workspace.right_dock().read(cx).is_open());
7986 assert_eq!(
7987 workspace
7988 .right_dock()
7989 .read(cx)
7990 .visible_panel()
7991 .unwrap()
7992 .panel_id(),
7993 panel_1.panel_id(),
7994 );
7995 });
7996
7997 // Move panel_1 back to the left
7998 panel_1.update_in(cx, |panel_1, window, cx| {
7999 panel_1.set_position(DockPosition::Left, window, cx)
8000 });
8001
8002 workspace.update_in(cx, |workspace, window, cx| {
8003 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
8004 let left_dock = workspace.left_dock();
8005 assert!(left_dock.read(cx).is_open());
8006 assert_eq!(
8007 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8008 panel_1.panel_id()
8009 );
8010 assert_eq!(
8011 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8012 px(1337.)
8013 );
8014 // And the right dock should be closed as it no longer has any panels.
8015 assert!(!workspace.right_dock().read(cx).is_open());
8016
8017 // Now we move panel_1 to the bottom
8018 panel_1.set_position(DockPosition::Bottom, window, cx);
8019 });
8020
8021 workspace.update_in(cx, |workspace, window, cx| {
8022 // Since panel_1 was visible on the left, we close the left dock.
8023 assert!(!workspace.left_dock().read(cx).is_open());
8024 // The bottom dock is sized based on the panel's default size,
8025 // since the panel orientation changed from vertical to horizontal.
8026 let bottom_dock = workspace.bottom_dock();
8027 assert_eq!(
8028 bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
8029 panel_1.size(window, cx),
8030 );
8031 // Close bottom dock and move panel_1 back to the left.
8032 bottom_dock.update(cx, |bottom_dock, cx| {
8033 bottom_dock.set_open(false, window, cx)
8034 });
8035 panel_1.set_position(DockPosition::Left, window, cx);
8036 });
8037
8038 // Emit activated event on panel 1
8039 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
8040
8041 // Now the left dock is open and panel_1 is active and focused.
8042 workspace.update_in(cx, |workspace, window, cx| {
8043 let left_dock = workspace.left_dock();
8044 assert!(left_dock.read(cx).is_open());
8045 assert_eq!(
8046 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8047 panel_1.panel_id(),
8048 );
8049 assert!(panel_1.focus_handle(cx).is_focused(window));
8050 });
8051
8052 // Emit closed event on panel 2, which is not active
8053 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8054
8055 // Wo don't close the left dock, because panel_2 wasn't the active panel
8056 workspace.update(cx, |workspace, cx| {
8057 let left_dock = workspace.left_dock();
8058 assert!(left_dock.read(cx).is_open());
8059 assert_eq!(
8060 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8061 panel_1.panel_id(),
8062 );
8063 });
8064
8065 // Emitting a ZoomIn event shows the panel as zoomed.
8066 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
8067 workspace.update(cx, |workspace, _| {
8068 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8069 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
8070 });
8071
8072 // Move panel to another dock while it is zoomed
8073 panel_1.update_in(cx, |panel, window, cx| {
8074 panel.set_position(DockPosition::Right, window, cx)
8075 });
8076 workspace.update(cx, |workspace, _| {
8077 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8078
8079 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8080 });
8081
8082 // This is a helper for getting a:
8083 // - valid focus on an element,
8084 // - that isn't a part of the panes and panels system of the Workspace,
8085 // - and doesn't trigger the 'on_focus_lost' API.
8086 let focus_other_view = {
8087 let workspace = workspace.clone();
8088 move |cx: &mut VisualTestContext| {
8089 workspace.update_in(cx, |workspace, window, cx| {
8090 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
8091 workspace.toggle_modal(window, cx, TestModal::new);
8092 workspace.toggle_modal(window, cx, TestModal::new);
8093 } else {
8094 workspace.toggle_modal(window, cx, TestModal::new);
8095 }
8096 })
8097 }
8098 };
8099
8100 // If focus is transferred to another view that's not a panel or another pane, we still show
8101 // the panel as zoomed.
8102 focus_other_view(cx);
8103 workspace.update(cx, |workspace, _| {
8104 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8105 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8106 });
8107
8108 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8109 workspace.update_in(cx, |_workspace, window, cx| {
8110 cx.focus_self(window);
8111 });
8112 workspace.update(cx, |workspace, _| {
8113 assert_eq!(workspace.zoomed, None);
8114 assert_eq!(workspace.zoomed_position, None);
8115 });
8116
8117 // If focus is transferred again to another view that's not a panel or a pane, we won't
8118 // show the panel as zoomed because it wasn't zoomed before.
8119 focus_other_view(cx);
8120 workspace.update(cx, |workspace, _| {
8121 assert_eq!(workspace.zoomed, None);
8122 assert_eq!(workspace.zoomed_position, None);
8123 });
8124
8125 // When the panel is activated, it is zoomed again.
8126 cx.dispatch_action(ToggleRightDock);
8127 workspace.update(cx, |workspace, _| {
8128 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8129 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8130 });
8131
8132 // Emitting a ZoomOut event unzooms the panel.
8133 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8134 workspace.update(cx, |workspace, _| {
8135 assert_eq!(workspace.zoomed, None);
8136 assert_eq!(workspace.zoomed_position, None);
8137 });
8138
8139 // Emit closed event on panel 1, which is active
8140 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8141
8142 // Now the left dock is closed, because panel_1 was the active panel
8143 workspace.update(cx, |workspace, cx| {
8144 let right_dock = workspace.right_dock();
8145 assert!(!right_dock.read(cx).is_open());
8146 });
8147 }
8148
8149 #[gpui::test]
8150 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8151 init_test(cx);
8152
8153 let fs = FakeFs::new(cx.background_executor.clone());
8154 let project = Project::test(fs, [], cx).await;
8155 let (workspace, cx) =
8156 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8157 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8158
8159 let dirty_regular_buffer = cx.new(|cx| {
8160 TestItem::new(cx)
8161 .with_dirty(true)
8162 .with_label("1.txt")
8163 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8164 });
8165 let dirty_regular_buffer_2 = cx.new(|cx| {
8166 TestItem::new(cx)
8167 .with_dirty(true)
8168 .with_label("2.txt")
8169 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8170 });
8171 let dirty_multi_buffer_with_both = cx.new(|cx| {
8172 TestItem::new(cx)
8173 .with_dirty(true)
8174 .with_singleton(false)
8175 .with_label("Fake Project Search")
8176 .with_project_items(&[
8177 dirty_regular_buffer.read(cx).project_items[0].clone(),
8178 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8179 ])
8180 });
8181 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8182 workspace.update_in(cx, |workspace, window, cx| {
8183 workspace.add_item(
8184 pane.clone(),
8185 Box::new(dirty_regular_buffer.clone()),
8186 None,
8187 false,
8188 false,
8189 window,
8190 cx,
8191 );
8192 workspace.add_item(
8193 pane.clone(),
8194 Box::new(dirty_regular_buffer_2.clone()),
8195 None,
8196 false,
8197 false,
8198 window,
8199 cx,
8200 );
8201 workspace.add_item(
8202 pane.clone(),
8203 Box::new(dirty_multi_buffer_with_both.clone()),
8204 None,
8205 false,
8206 false,
8207 window,
8208 cx,
8209 );
8210 });
8211
8212 pane.update_in(cx, |pane, window, cx| {
8213 pane.activate_item(2, true, true, window, cx);
8214 assert_eq!(
8215 pane.active_item().unwrap().item_id(),
8216 multi_buffer_with_both_files_id,
8217 "Should select the multi buffer in the pane"
8218 );
8219 });
8220 let close_all_but_multi_buffer_task = pane
8221 .update_in(cx, |pane, window, cx| {
8222 pane.close_inactive_items(
8223 &CloseInactiveItems {
8224 save_intent: Some(SaveIntent::Save),
8225 close_pinned: true,
8226 },
8227 window,
8228 cx,
8229 )
8230 })
8231 .expect("should have inactive files to close");
8232 cx.background_executor.run_until_parked();
8233 assert!(!cx.has_pending_prompt());
8234 close_all_but_multi_buffer_task
8235 .await
8236 .expect("Closing all buffers but the multi buffer failed");
8237 pane.update(cx, |pane, cx| {
8238 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
8239 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
8240 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
8241 assert_eq!(pane.items_len(), 1);
8242 assert_eq!(
8243 pane.active_item().unwrap().item_id(),
8244 multi_buffer_with_both_files_id,
8245 "Should have only the multi buffer left in the pane"
8246 );
8247 assert!(
8248 dirty_multi_buffer_with_both.read(cx).is_dirty,
8249 "The multi buffer containing the unsaved buffer should still be dirty"
8250 );
8251 });
8252
8253 dirty_regular_buffer.update(cx, |buffer, cx| {
8254 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
8255 });
8256
8257 let close_multi_buffer_task = pane
8258 .update_in(cx, |pane, window, cx| {
8259 pane.close_active_item(
8260 &CloseActiveItem {
8261 save_intent: Some(SaveIntent::Close),
8262 close_pinned: false,
8263 },
8264 window,
8265 cx,
8266 )
8267 })
8268 .expect("should have the multi buffer to close");
8269 cx.background_executor.run_until_parked();
8270 assert!(
8271 cx.has_pending_prompt(),
8272 "Dirty multi buffer should prompt a save dialog"
8273 );
8274 cx.simulate_prompt_answer("Save");
8275 cx.background_executor.run_until_parked();
8276 close_multi_buffer_task
8277 .await
8278 .expect("Closing the multi buffer failed");
8279 pane.update(cx, |pane, cx| {
8280 assert_eq!(
8281 dirty_multi_buffer_with_both.read(cx).save_count,
8282 1,
8283 "Multi buffer item should get be saved"
8284 );
8285 // Test impl does not save inner items, so we do not assert them
8286 assert_eq!(
8287 pane.items_len(),
8288 0,
8289 "No more items should be left in the pane"
8290 );
8291 assert!(pane.active_item().is_none());
8292 });
8293 }
8294
8295 #[gpui::test]
8296 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
8297 cx: &mut TestAppContext,
8298 ) {
8299 init_test(cx);
8300
8301 let fs = FakeFs::new(cx.background_executor.clone());
8302 let project = Project::test(fs, [], cx).await;
8303 let (workspace, cx) =
8304 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8305 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8306
8307 let dirty_regular_buffer = cx.new(|cx| {
8308 TestItem::new(cx)
8309 .with_dirty(true)
8310 .with_label("1.txt")
8311 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8312 });
8313 let dirty_regular_buffer_2 = cx.new(|cx| {
8314 TestItem::new(cx)
8315 .with_dirty(true)
8316 .with_label("2.txt")
8317 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8318 });
8319 let clear_regular_buffer = cx.new(|cx| {
8320 TestItem::new(cx)
8321 .with_label("3.txt")
8322 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8323 });
8324
8325 let dirty_multi_buffer_with_both = cx.new(|cx| {
8326 TestItem::new(cx)
8327 .with_dirty(true)
8328 .with_singleton(false)
8329 .with_label("Fake Project Search")
8330 .with_project_items(&[
8331 dirty_regular_buffer.read(cx).project_items[0].clone(),
8332 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8333 clear_regular_buffer.read(cx).project_items[0].clone(),
8334 ])
8335 });
8336 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8337 workspace.update_in(cx, |workspace, window, cx| {
8338 workspace.add_item(
8339 pane.clone(),
8340 Box::new(dirty_regular_buffer.clone()),
8341 None,
8342 false,
8343 false,
8344 window,
8345 cx,
8346 );
8347 workspace.add_item(
8348 pane.clone(),
8349 Box::new(dirty_multi_buffer_with_both.clone()),
8350 None,
8351 false,
8352 false,
8353 window,
8354 cx,
8355 );
8356 });
8357
8358 pane.update_in(cx, |pane, window, cx| {
8359 pane.activate_item(1, true, true, window, cx);
8360 assert_eq!(
8361 pane.active_item().unwrap().item_id(),
8362 multi_buffer_with_both_files_id,
8363 "Should select the multi buffer in the pane"
8364 );
8365 });
8366 let _close_multi_buffer_task = pane
8367 .update_in(cx, |pane, window, cx| {
8368 pane.close_active_item(
8369 &CloseActiveItem {
8370 save_intent: None,
8371 close_pinned: false,
8372 },
8373 window,
8374 cx,
8375 )
8376 })
8377 .expect("should have active multi buffer to close");
8378 cx.background_executor.run_until_parked();
8379 assert!(
8380 cx.has_pending_prompt(),
8381 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
8382 );
8383 }
8384
8385 #[gpui::test]
8386 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
8387 cx: &mut TestAppContext,
8388 ) {
8389 init_test(cx);
8390
8391 let fs = FakeFs::new(cx.background_executor.clone());
8392 let project = Project::test(fs, [], cx).await;
8393 let (workspace, cx) =
8394 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8395 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8396
8397 let dirty_regular_buffer = cx.new(|cx| {
8398 TestItem::new(cx)
8399 .with_dirty(true)
8400 .with_label("1.txt")
8401 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8402 });
8403 let dirty_regular_buffer_2 = cx.new(|cx| {
8404 TestItem::new(cx)
8405 .with_dirty(true)
8406 .with_label("2.txt")
8407 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8408 });
8409 let clear_regular_buffer = cx.new(|cx| {
8410 TestItem::new(cx)
8411 .with_label("3.txt")
8412 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8413 });
8414
8415 let dirty_multi_buffer = cx.new(|cx| {
8416 TestItem::new(cx)
8417 .with_dirty(true)
8418 .with_singleton(false)
8419 .with_label("Fake Project Search")
8420 .with_project_items(&[
8421 dirty_regular_buffer.read(cx).project_items[0].clone(),
8422 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8423 clear_regular_buffer.read(cx).project_items[0].clone(),
8424 ])
8425 });
8426 workspace.update_in(cx, |workspace, window, cx| {
8427 workspace.add_item(
8428 pane.clone(),
8429 Box::new(dirty_regular_buffer.clone()),
8430 None,
8431 false,
8432 false,
8433 window,
8434 cx,
8435 );
8436 workspace.add_item(
8437 pane.clone(),
8438 Box::new(dirty_regular_buffer_2.clone()),
8439 None,
8440 false,
8441 false,
8442 window,
8443 cx,
8444 );
8445 workspace.add_item(
8446 pane.clone(),
8447 Box::new(dirty_multi_buffer.clone()),
8448 None,
8449 false,
8450 false,
8451 window,
8452 cx,
8453 );
8454 });
8455
8456 pane.update_in(cx, |pane, window, cx| {
8457 pane.activate_item(2, true, true, window, cx);
8458 assert_eq!(
8459 pane.active_item().unwrap().item_id(),
8460 dirty_multi_buffer.item_id(),
8461 "Should select the multi buffer in the pane"
8462 );
8463 });
8464 let close_multi_buffer_task = pane
8465 .update_in(cx, |pane, window, cx| {
8466 pane.close_active_item(
8467 &CloseActiveItem {
8468 save_intent: None,
8469 close_pinned: false,
8470 },
8471 window,
8472 cx,
8473 )
8474 })
8475 .expect("should have active multi buffer to close");
8476 cx.background_executor.run_until_parked();
8477 assert!(
8478 !cx.has_pending_prompt(),
8479 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
8480 );
8481 close_multi_buffer_task
8482 .await
8483 .expect("Closing multi buffer failed");
8484 pane.update(cx, |pane, cx| {
8485 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8486 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
8487 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8488 assert_eq!(
8489 pane.items()
8490 .map(|item| item.item_id())
8491 .sorted()
8492 .collect::<Vec<_>>(),
8493 vec![
8494 dirty_regular_buffer.item_id(),
8495 dirty_regular_buffer_2.item_id(),
8496 ],
8497 "Should have no multi buffer left in the pane"
8498 );
8499 assert!(dirty_regular_buffer.read(cx).is_dirty);
8500 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
8501 });
8502 }
8503
8504 #[gpui::test]
8505 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
8506 init_test(cx);
8507 let fs = FakeFs::new(cx.executor());
8508 let project = Project::test(fs, [], cx).await;
8509 let (workspace, cx) =
8510 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8511
8512 // Add a new panel to the right dock, opening the dock and setting the
8513 // focus to the new panel.
8514 let panel = workspace.update_in(cx, |workspace, window, cx| {
8515 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8516 workspace.add_panel(panel.clone(), window, cx);
8517
8518 workspace
8519 .right_dock()
8520 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8521
8522 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8523
8524 panel
8525 });
8526
8527 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8528 // panel to the next valid position which, in this case, is the left
8529 // dock.
8530 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8531 workspace.update(cx, |workspace, cx| {
8532 assert!(workspace.left_dock().read(cx).is_open());
8533 assert_eq!(panel.read(cx).position, DockPosition::Left);
8534 });
8535
8536 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8537 // panel to the next valid position which, in this case, is the bottom
8538 // dock.
8539 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8540 workspace.update(cx, |workspace, cx| {
8541 assert!(workspace.bottom_dock().read(cx).is_open());
8542 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
8543 });
8544
8545 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
8546 // around moving the panel to its initial position, the right dock.
8547 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8548 workspace.update(cx, |workspace, cx| {
8549 assert!(workspace.right_dock().read(cx).is_open());
8550 assert_eq!(panel.read(cx).position, DockPosition::Right);
8551 });
8552
8553 // Remove focus from the panel, ensuring that, if the panel is not
8554 // focused, the `MoveFocusedPanelToNextPosition` action does not update
8555 // the panel's position, so the panel is still in the right dock.
8556 workspace.update_in(cx, |workspace, window, cx| {
8557 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8558 });
8559
8560 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8561 workspace.update(cx, |workspace, cx| {
8562 assert!(workspace.right_dock().read(cx).is_open());
8563 assert_eq!(panel.read(cx).position, DockPosition::Right);
8564 });
8565 }
8566
8567 mod register_project_item_tests {
8568
8569 use super::*;
8570
8571 // View
8572 struct TestPngItemView {
8573 focus_handle: FocusHandle,
8574 }
8575 // Model
8576 struct TestPngItem {}
8577
8578 impl project::ProjectItem for TestPngItem {
8579 fn try_open(
8580 _project: &Entity<Project>,
8581 path: &ProjectPath,
8582 cx: &mut App,
8583 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8584 if path.path.extension().unwrap() == "png" {
8585 Some(cx.spawn(|mut cx| async move { cx.new(|_| TestPngItem {}) }))
8586 } else {
8587 None
8588 }
8589 }
8590
8591 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8592 None
8593 }
8594
8595 fn project_path(&self, _: &App) -> Option<ProjectPath> {
8596 None
8597 }
8598
8599 fn is_dirty(&self) -> bool {
8600 false
8601 }
8602 }
8603
8604 impl Item for TestPngItemView {
8605 type Event = ();
8606 }
8607 impl EventEmitter<()> for TestPngItemView {}
8608 impl Focusable for TestPngItemView {
8609 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8610 self.focus_handle.clone()
8611 }
8612 }
8613
8614 impl Render for TestPngItemView {
8615 fn render(
8616 &mut self,
8617 _window: &mut Window,
8618 _cx: &mut Context<Self>,
8619 ) -> impl IntoElement {
8620 Empty
8621 }
8622 }
8623
8624 impl ProjectItem for TestPngItemView {
8625 type Item = TestPngItem;
8626
8627 fn for_project_item(
8628 _project: Entity<Project>,
8629 _item: Entity<Self::Item>,
8630 _: &mut Window,
8631 cx: &mut Context<Self>,
8632 ) -> Self
8633 where
8634 Self: Sized,
8635 {
8636 Self {
8637 focus_handle: cx.focus_handle(),
8638 }
8639 }
8640 }
8641
8642 // View
8643 struct TestIpynbItemView {
8644 focus_handle: FocusHandle,
8645 }
8646 // Model
8647 struct TestIpynbItem {}
8648
8649 impl project::ProjectItem for TestIpynbItem {
8650 fn try_open(
8651 _project: &Entity<Project>,
8652 path: &ProjectPath,
8653 cx: &mut App,
8654 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8655 if path.path.extension().unwrap() == "ipynb" {
8656 Some(cx.spawn(|mut cx| async move { cx.new(|_| TestIpynbItem {}) }))
8657 } else {
8658 None
8659 }
8660 }
8661
8662 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8663 None
8664 }
8665
8666 fn project_path(&self, _: &App) -> Option<ProjectPath> {
8667 None
8668 }
8669
8670 fn is_dirty(&self) -> bool {
8671 false
8672 }
8673 }
8674
8675 impl Item for TestIpynbItemView {
8676 type Event = ();
8677 }
8678 impl EventEmitter<()> for TestIpynbItemView {}
8679 impl Focusable for TestIpynbItemView {
8680 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8681 self.focus_handle.clone()
8682 }
8683 }
8684
8685 impl Render for TestIpynbItemView {
8686 fn render(
8687 &mut self,
8688 _window: &mut Window,
8689 _cx: &mut Context<Self>,
8690 ) -> impl IntoElement {
8691 Empty
8692 }
8693 }
8694
8695 impl ProjectItem for TestIpynbItemView {
8696 type Item = TestIpynbItem;
8697
8698 fn for_project_item(
8699 _project: Entity<Project>,
8700 _item: Entity<Self::Item>,
8701 _: &mut Window,
8702 cx: &mut Context<Self>,
8703 ) -> Self
8704 where
8705 Self: Sized,
8706 {
8707 Self {
8708 focus_handle: cx.focus_handle(),
8709 }
8710 }
8711 }
8712
8713 struct TestAlternatePngItemView {
8714 focus_handle: FocusHandle,
8715 }
8716
8717 impl Item for TestAlternatePngItemView {
8718 type Event = ();
8719 }
8720
8721 impl EventEmitter<()> for TestAlternatePngItemView {}
8722 impl Focusable for TestAlternatePngItemView {
8723 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8724 self.focus_handle.clone()
8725 }
8726 }
8727
8728 impl Render for TestAlternatePngItemView {
8729 fn render(
8730 &mut self,
8731 _window: &mut Window,
8732 _cx: &mut Context<Self>,
8733 ) -> impl IntoElement {
8734 Empty
8735 }
8736 }
8737
8738 impl ProjectItem for TestAlternatePngItemView {
8739 type Item = TestPngItem;
8740
8741 fn for_project_item(
8742 _project: Entity<Project>,
8743 _item: Entity<Self::Item>,
8744 _: &mut Window,
8745 cx: &mut Context<Self>,
8746 ) -> Self
8747 where
8748 Self: Sized,
8749 {
8750 Self {
8751 focus_handle: cx.focus_handle(),
8752 }
8753 }
8754 }
8755
8756 #[gpui::test]
8757 async fn test_register_project_item(cx: &mut TestAppContext) {
8758 init_test(cx);
8759
8760 cx.update(|cx| {
8761 register_project_item::<TestPngItemView>(cx);
8762 register_project_item::<TestIpynbItemView>(cx);
8763 });
8764
8765 let fs = FakeFs::new(cx.executor());
8766 fs.insert_tree(
8767 "/root1",
8768 json!({
8769 "one.png": "BINARYDATAHERE",
8770 "two.ipynb": "{ totally a notebook }",
8771 "three.txt": "editing text, sure why not?"
8772 }),
8773 )
8774 .await;
8775
8776 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8777 let (workspace, cx) =
8778 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8779
8780 let worktree_id = project.update(cx, |project, cx| {
8781 project.worktrees(cx).next().unwrap().read(cx).id()
8782 });
8783
8784 let handle = workspace
8785 .update_in(cx, |workspace, window, cx| {
8786 let project_path = (worktree_id, "one.png");
8787 workspace.open_path(project_path, None, true, window, cx)
8788 })
8789 .await
8790 .unwrap();
8791
8792 // Now we can check if the handle we got back errored or not
8793 assert_eq!(
8794 handle.to_any().entity_type(),
8795 TypeId::of::<TestPngItemView>()
8796 );
8797
8798 let handle = workspace
8799 .update_in(cx, |workspace, window, cx| {
8800 let project_path = (worktree_id, "two.ipynb");
8801 workspace.open_path(project_path, None, true, window, cx)
8802 })
8803 .await
8804 .unwrap();
8805
8806 assert_eq!(
8807 handle.to_any().entity_type(),
8808 TypeId::of::<TestIpynbItemView>()
8809 );
8810
8811 let handle = workspace
8812 .update_in(cx, |workspace, window, cx| {
8813 let project_path = (worktree_id, "three.txt");
8814 workspace.open_path(project_path, None, true, window, cx)
8815 })
8816 .await;
8817 assert!(handle.is_err());
8818 }
8819
8820 #[gpui::test]
8821 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8822 init_test(cx);
8823
8824 cx.update(|cx| {
8825 register_project_item::<TestPngItemView>(cx);
8826 register_project_item::<TestAlternatePngItemView>(cx);
8827 });
8828
8829 let fs = FakeFs::new(cx.executor());
8830 fs.insert_tree(
8831 "/root1",
8832 json!({
8833 "one.png": "BINARYDATAHERE",
8834 "two.ipynb": "{ totally a notebook }",
8835 "three.txt": "editing text, sure why not?"
8836 }),
8837 )
8838 .await;
8839 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8840 let (workspace, cx) =
8841 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8842 let worktree_id = project.update(cx, |project, cx| {
8843 project.worktrees(cx).next().unwrap().read(cx).id()
8844 });
8845
8846 let handle = workspace
8847 .update_in(cx, |workspace, window, cx| {
8848 let project_path = (worktree_id, "one.png");
8849 workspace.open_path(project_path, None, true, window, cx)
8850 })
8851 .await
8852 .unwrap();
8853
8854 // This _must_ be the second item registered
8855 assert_eq!(
8856 handle.to_any().entity_type(),
8857 TypeId::of::<TestAlternatePngItemView>()
8858 );
8859
8860 let handle = workspace
8861 .update_in(cx, |workspace, window, cx| {
8862 let project_path = (worktree_id, "three.txt");
8863 workspace.open_path(project_path, None, true, window, cx)
8864 })
8865 .await;
8866 assert!(handle.is_err());
8867 }
8868 }
8869
8870 pub fn init_test(cx: &mut TestAppContext) {
8871 cx.update(|cx| {
8872 let settings_store = SettingsStore::test(cx);
8873 cx.set_global(settings_store);
8874 theme::init(theme::LoadThemes::JustBase, cx);
8875 language::init(cx);
8876 crate::init_settings(cx);
8877 Project::init_settings(cx);
8878 });
8879 }
8880
8881 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
8882 let item = TestProjectItem::new(id, path, cx);
8883 item.update(cx, |item, _| {
8884 item.is_dirty = true;
8885 });
8886 item
8887 }
8888}