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