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