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