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