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