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