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