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