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