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