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 for window in local_workspace_windows(cx) {
5948 if let Ok(workspace) = window.read(cx) {
5949 let project = workspace.project().read(cx);
5950 if project.is_via_collab() {
5951 continue;
5952 }
5953 existing = Some(window);
5954 open_visible = OpenVisible::None;
5955 break;
5956 }
5957 }
5958 })?;
5959 }
5960 }
5961
5962 if let Some(existing) = existing {
5963 let open_task = existing
5964 .update(&mut cx, |workspace, window, cx| {
5965 window.activate_window();
5966 workspace.open_paths(abs_paths, open_visible, None, window, cx)
5967 })?
5968 .await;
5969
5970 _ = existing.update(&mut cx, |workspace, _, cx| {
5971 for item in open_task.iter().flatten() {
5972 if let Err(e) = item {
5973 workspace.show_error(&e, cx);
5974 }
5975 }
5976 });
5977
5978 Ok((existing, open_task))
5979 } else {
5980 cx.update(move |cx| {
5981 Workspace::new_local(
5982 abs_paths,
5983 app_state.clone(),
5984 open_options.replace_window,
5985 open_options.env,
5986 cx,
5987 )
5988 })?
5989 .await
5990 }
5991 })
5992}
5993
5994pub fn open_new(
5995 open_options: OpenOptions,
5996 app_state: Arc<AppState>,
5997 cx: &mut App,
5998 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
5999) -> Task<anyhow::Result<()>> {
6000 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6001 cx.spawn(|mut cx| async move {
6002 let (workspace, opened_paths) = task.await?;
6003 workspace.update(&mut cx, |workspace, window, cx| {
6004 if opened_paths.is_empty() {
6005 init(workspace, window, cx)
6006 }
6007 })?;
6008 Ok(())
6009 })
6010}
6011
6012pub fn create_and_open_local_file(
6013 path: &'static Path,
6014 window: &mut Window,
6015 cx: &mut Context<Workspace>,
6016 default_content: impl 'static + Send + FnOnce() -> Rope,
6017) -> Task<Result<Box<dyn ItemHandle>>> {
6018 cx.spawn_in(window, |workspace, mut cx| async move {
6019 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
6020 if !fs.is_file(path).await {
6021 fs.create_file(path, Default::default()).await?;
6022 fs.save(path, &default_content(), Default::default())
6023 .await?;
6024 }
6025
6026 let mut items = workspace
6027 .update_in(&mut cx, |workspace, window, cx| {
6028 workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6029 workspace.open_paths(
6030 vec![path.to_path_buf()],
6031 OpenVisible::None,
6032 None,
6033 window,
6034 cx,
6035 )
6036 })
6037 })?
6038 .await?
6039 .await;
6040
6041 let item = items.pop().flatten();
6042 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
6043 })
6044}
6045
6046pub fn open_ssh_project(
6047 window: WindowHandle<Workspace>,
6048 connection_options: SshConnectionOptions,
6049 cancel_rx: oneshot::Receiver<()>,
6050 delegate: Arc<dyn SshClientDelegate>,
6051 app_state: Arc<AppState>,
6052 paths: Vec<PathBuf>,
6053 cx: &mut App,
6054) -> Task<Result<()>> {
6055 cx.spawn(|mut cx| async move {
6056 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6057 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6058
6059 let session = match cx
6060 .update(|cx| {
6061 remote::SshRemoteClient::new(
6062 ConnectionIdentifier::Workspace(workspace_id.0),
6063 connection_options,
6064 cancel_rx,
6065 delegate,
6066 cx,
6067 )
6068 })?
6069 .await?
6070 {
6071 Some(result) => result,
6072 None => return Ok(()),
6073 };
6074
6075 let project = cx.update(|cx| {
6076 project::Project::ssh(
6077 session,
6078 app_state.client.clone(),
6079 app_state.node_runtime.clone(),
6080 app_state.user_store.clone(),
6081 app_state.languages.clone(),
6082 app_state.fs.clone(),
6083 cx,
6084 )
6085 })?;
6086
6087 let toolchains = DB.toolchains(workspace_id).await?;
6088 for (toolchain, worktree_id) in toolchains {
6089 project
6090 .update(&mut cx, |this, cx| {
6091 this.activate_toolchain(worktree_id, toolchain, cx)
6092 })?
6093 .await;
6094 }
6095 let mut project_paths_to_open = vec![];
6096 let mut project_path_errors = vec![];
6097
6098 for path in paths {
6099 let result = cx
6100 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6101 .await;
6102 match result {
6103 Ok((_, project_path)) => {
6104 project_paths_to_open.push((path.clone(), Some(project_path)));
6105 }
6106 Err(error) => {
6107 project_path_errors.push(error);
6108 }
6109 };
6110 }
6111
6112 if project_paths_to_open.is_empty() {
6113 return Err(project_path_errors
6114 .pop()
6115 .unwrap_or_else(|| anyhow!("no paths given")));
6116 }
6117
6118 cx.update_window(window.into(), |_, window, cx| {
6119 window.replace_root(cx, |window, cx| {
6120 let mut workspace =
6121 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6122
6123 workspace
6124 .client()
6125 .telemetry()
6126 .report_app_event("open ssh project".to_string());
6127
6128 workspace.set_serialized_ssh_project(serialized_ssh_project);
6129 workspace
6130 });
6131 })?;
6132
6133 window
6134 .update(&mut cx, |_, window, cx| {
6135 window.activate_window();
6136
6137 open_items(serialized_workspace, project_paths_to_open, window, cx)
6138 })?
6139 .await?;
6140
6141 window.update(&mut cx, |workspace, _, cx| {
6142 for error in project_path_errors {
6143 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6144 if let Some(path) = error.error_tag("path") {
6145 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6146 }
6147 } else {
6148 workspace.show_error(&error, cx)
6149 }
6150 }
6151 })
6152 })
6153}
6154
6155fn serialize_ssh_project(
6156 connection_options: SshConnectionOptions,
6157 paths: Vec<PathBuf>,
6158 cx: &AsyncApp,
6159) -> Task<
6160 Result<(
6161 SerializedSshProject,
6162 WorkspaceId,
6163 Option<SerializedWorkspace>,
6164 )>,
6165> {
6166 cx.background_executor().spawn(async move {
6167 let serialized_ssh_project = persistence::DB
6168 .get_or_create_ssh_project(
6169 connection_options.host.clone(),
6170 connection_options.port,
6171 paths
6172 .iter()
6173 .map(|path| path.to_string_lossy().to_string())
6174 .collect::<Vec<_>>(),
6175 connection_options.username.clone(),
6176 )
6177 .await?;
6178
6179 let serialized_workspace =
6180 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
6181
6182 let workspace_id = if let Some(workspace_id) =
6183 serialized_workspace.as_ref().map(|workspace| workspace.id)
6184 {
6185 workspace_id
6186 } else {
6187 persistence::DB.next_id().await?
6188 };
6189
6190 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
6191 })
6192}
6193
6194pub fn join_in_room_project(
6195 project_id: u64,
6196 follow_user_id: u64,
6197 app_state: Arc<AppState>,
6198 cx: &mut App,
6199) -> Task<Result<()>> {
6200 let windows = cx.windows();
6201 cx.spawn(|mut cx| async move {
6202 let existing_workspace = windows.into_iter().find_map(|window_handle| {
6203 window_handle
6204 .downcast::<Workspace>()
6205 .and_then(|window_handle| {
6206 window_handle
6207 .update(&mut cx, |workspace, _window, cx| {
6208 if workspace.project().read(cx).remote_id() == Some(project_id) {
6209 Some(window_handle)
6210 } else {
6211 None
6212 }
6213 })
6214 .unwrap_or(None)
6215 })
6216 });
6217
6218 let workspace = if let Some(existing_workspace) = existing_workspace {
6219 existing_workspace
6220 } else {
6221 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
6222 let room = active_call
6223 .read_with(&cx, |call, _| call.room().cloned())?
6224 .ok_or_else(|| anyhow!("not in a call"))?;
6225 let project = room
6226 .update(&mut cx, |room, cx| {
6227 room.join_project(
6228 project_id,
6229 app_state.languages.clone(),
6230 app_state.fs.clone(),
6231 cx,
6232 )
6233 })?
6234 .await?;
6235
6236 let window_bounds_override = window_bounds_env_override();
6237 cx.update(|cx| {
6238 let mut options = (app_state.build_window_options)(None, cx);
6239 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
6240 cx.open_window(options, |window, cx| {
6241 cx.new(|cx| {
6242 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
6243 })
6244 })
6245 })??
6246 };
6247
6248 workspace.update(&mut cx, |workspace, window, cx| {
6249 cx.activate(true);
6250 window.activate_window();
6251
6252 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
6253 let follow_peer_id = room
6254 .read(cx)
6255 .remote_participants()
6256 .iter()
6257 .find(|(_, participant)| participant.user.id == follow_user_id)
6258 .map(|(_, p)| p.peer_id)
6259 .or_else(|| {
6260 // If we couldn't follow the given user, follow the host instead.
6261 let collaborator = workspace
6262 .project()
6263 .read(cx)
6264 .collaborators()
6265 .values()
6266 .find(|collaborator| collaborator.is_host)?;
6267 Some(collaborator.peer_id)
6268 });
6269
6270 if let Some(follow_peer_id) = follow_peer_id {
6271 workspace.follow(follow_peer_id, window, cx);
6272 }
6273 }
6274 })?;
6275
6276 anyhow::Ok(())
6277 })
6278}
6279
6280pub fn reload(reload: &Reload, cx: &mut App) {
6281 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
6282 let mut workspace_windows = cx
6283 .windows()
6284 .into_iter()
6285 .filter_map(|window| window.downcast::<Workspace>())
6286 .collect::<Vec<_>>();
6287
6288 // If multiple windows have unsaved changes, and need a save prompt,
6289 // prompt in the active window before switching to a different window.
6290 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
6291
6292 let mut prompt = None;
6293 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
6294 prompt = window
6295 .update(cx, |_, window, cx| {
6296 window.prompt(
6297 PromptLevel::Info,
6298 "Are you sure you want to restart?",
6299 None,
6300 &["Restart", "Cancel"],
6301 cx,
6302 )
6303 })
6304 .ok();
6305 }
6306
6307 let binary_path = reload.binary_path.clone();
6308 cx.spawn(|mut cx| async move {
6309 if let Some(prompt) = prompt {
6310 let answer = prompt.await?;
6311 if answer != 0 {
6312 return Ok(());
6313 }
6314 }
6315
6316 // If the user cancels any save prompt, then keep the app open.
6317 for window in workspace_windows {
6318 if let Ok(should_close) = window.update(&mut cx, |workspace, window, cx| {
6319 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
6320 }) {
6321 if !should_close.await? {
6322 return Ok(());
6323 }
6324 }
6325 }
6326
6327 cx.update(|cx| cx.restart(binary_path))
6328 })
6329 .detach_and_log_err(cx);
6330}
6331
6332fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
6333 let mut parts = value.split(',');
6334 let x: usize = parts.next()?.parse().ok()?;
6335 let y: usize = parts.next()?.parse().ok()?;
6336 Some(point(px(x as f32), px(y as f32)))
6337}
6338
6339fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
6340 let mut parts = value.split(',');
6341 let width: usize = parts.next()?.parse().ok()?;
6342 let height: usize = parts.next()?.parse().ok()?;
6343 Some(size(px(width as f32), px(height as f32)))
6344}
6345
6346pub fn client_side_decorations(
6347 element: impl IntoElement,
6348 window: &mut Window,
6349 cx: &mut App,
6350) -> Stateful<Div> {
6351 const BORDER_SIZE: Pixels = px(1.0);
6352 let decorations = window.window_decorations();
6353
6354 if matches!(decorations, Decorations::Client { .. }) {
6355 window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6356 }
6357
6358 struct GlobalResizeEdge(ResizeEdge);
6359 impl Global for GlobalResizeEdge {}
6360
6361 div()
6362 .id("window-backdrop")
6363 .bg(transparent_black())
6364 .map(|div| match decorations {
6365 Decorations::Server => div,
6366 Decorations::Client { tiling, .. } => div
6367 .when(!(tiling.top || tiling.right), |div| {
6368 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6369 })
6370 .when(!(tiling.top || tiling.left), |div| {
6371 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6372 })
6373 .when(!(tiling.bottom || tiling.right), |div| {
6374 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6375 })
6376 .when(!(tiling.bottom || tiling.left), |div| {
6377 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6378 })
6379 .when(!tiling.top, |div| {
6380 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6381 })
6382 .when(!tiling.bottom, |div| {
6383 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6384 })
6385 .when(!tiling.left, |div| {
6386 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6387 })
6388 .when(!tiling.right, |div| {
6389 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6390 })
6391 .on_mouse_move(move |e, window, cx| {
6392 let size = window.window_bounds().get_bounds().size;
6393 let pos = e.position;
6394
6395 let new_edge =
6396 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6397
6398 let edge = cx.try_global::<GlobalResizeEdge>();
6399 if new_edge != edge.map(|edge| edge.0) {
6400 window
6401 .window_handle()
6402 .update(cx, |workspace, _, cx| {
6403 cx.notify(workspace.entity_id());
6404 })
6405 .ok();
6406 }
6407 })
6408 .on_mouse_down(MouseButton::Left, move |e, window, _| {
6409 let size = window.window_bounds().get_bounds().size;
6410 let pos = e.position;
6411
6412 let edge = match resize_edge(
6413 pos,
6414 theme::CLIENT_SIDE_DECORATION_SHADOW,
6415 size,
6416 tiling,
6417 ) {
6418 Some(value) => value,
6419 None => return,
6420 };
6421
6422 window.start_window_resize(edge);
6423 }),
6424 })
6425 .size_full()
6426 .child(
6427 div()
6428 .cursor(CursorStyle::Arrow)
6429 .map(|div| match decorations {
6430 Decorations::Server => div,
6431 Decorations::Client { tiling } => div
6432 .border_color(cx.theme().colors().border)
6433 .when(!(tiling.top || tiling.right), |div| {
6434 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6435 })
6436 .when(!(tiling.top || tiling.left), |div| {
6437 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6438 })
6439 .when(!(tiling.bottom || tiling.right), |div| {
6440 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6441 })
6442 .when(!(tiling.bottom || tiling.left), |div| {
6443 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6444 })
6445 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6446 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6447 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6448 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6449 .when(!tiling.is_tiled(), |div| {
6450 div.shadow(smallvec::smallvec![gpui::BoxShadow {
6451 color: Hsla {
6452 h: 0.,
6453 s: 0.,
6454 l: 0.,
6455 a: 0.4,
6456 },
6457 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6458 spread_radius: px(0.),
6459 offset: point(px(0.0), px(0.0)),
6460 }])
6461 }),
6462 })
6463 .on_mouse_move(|_e, _, cx| {
6464 cx.stop_propagation();
6465 })
6466 .size_full()
6467 .child(element),
6468 )
6469 .map(|div| match decorations {
6470 Decorations::Server => div,
6471 Decorations::Client { tiling, .. } => div.child(
6472 canvas(
6473 |_bounds, window, _| {
6474 window.insert_hitbox(
6475 Bounds::new(
6476 point(px(0.0), px(0.0)),
6477 window.window_bounds().get_bounds().size,
6478 ),
6479 false,
6480 )
6481 },
6482 move |_bounds, hitbox, window, cx| {
6483 let mouse = window.mouse_position();
6484 let size = window.window_bounds().get_bounds().size;
6485 let Some(edge) =
6486 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6487 else {
6488 return;
6489 };
6490 cx.set_global(GlobalResizeEdge(edge));
6491 window.set_cursor_style(
6492 match edge {
6493 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6494 ResizeEdge::Left | ResizeEdge::Right => {
6495 CursorStyle::ResizeLeftRight
6496 }
6497 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6498 CursorStyle::ResizeUpLeftDownRight
6499 }
6500 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6501 CursorStyle::ResizeUpRightDownLeft
6502 }
6503 },
6504 &hitbox,
6505 );
6506 },
6507 )
6508 .size_full()
6509 .absolute(),
6510 ),
6511 })
6512}
6513
6514fn resize_edge(
6515 pos: Point<Pixels>,
6516 shadow_size: Pixels,
6517 window_size: Size<Pixels>,
6518 tiling: Tiling,
6519) -> Option<ResizeEdge> {
6520 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6521 if bounds.contains(&pos) {
6522 return None;
6523 }
6524
6525 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6526 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6527 if !tiling.top && top_left_bounds.contains(&pos) {
6528 return Some(ResizeEdge::TopLeft);
6529 }
6530
6531 let top_right_bounds = Bounds::new(
6532 Point::new(window_size.width - corner_size.width, px(0.)),
6533 corner_size,
6534 );
6535 if !tiling.top && top_right_bounds.contains(&pos) {
6536 return Some(ResizeEdge::TopRight);
6537 }
6538
6539 let bottom_left_bounds = Bounds::new(
6540 Point::new(px(0.), window_size.height - corner_size.height),
6541 corner_size,
6542 );
6543 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6544 return Some(ResizeEdge::BottomLeft);
6545 }
6546
6547 let bottom_right_bounds = Bounds::new(
6548 Point::new(
6549 window_size.width - corner_size.width,
6550 window_size.height - corner_size.height,
6551 ),
6552 corner_size,
6553 );
6554 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6555 return Some(ResizeEdge::BottomRight);
6556 }
6557
6558 if !tiling.top && pos.y < shadow_size {
6559 Some(ResizeEdge::Top)
6560 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6561 Some(ResizeEdge::Bottom)
6562 } else if !tiling.left && pos.x < shadow_size {
6563 Some(ResizeEdge::Left)
6564 } else if !tiling.right && pos.x > window_size.width - shadow_size {
6565 Some(ResizeEdge::Right)
6566 } else {
6567 None
6568 }
6569}
6570
6571fn join_pane_into_active(
6572 active_pane: &Entity<Pane>,
6573 pane: &Entity<Pane>,
6574 window: &mut Window,
6575 cx: &mut App,
6576) {
6577 if pane == active_pane {
6578 return;
6579 } else if pane.read(cx).items_len() == 0 {
6580 pane.update(cx, |_, cx| {
6581 cx.emit(pane::Event::Remove {
6582 focus_on_pane: None,
6583 });
6584 })
6585 } else {
6586 move_all_items(pane, active_pane, window, cx);
6587 }
6588}
6589
6590fn move_all_items(
6591 from_pane: &Entity<Pane>,
6592 to_pane: &Entity<Pane>,
6593 window: &mut Window,
6594 cx: &mut App,
6595) {
6596 let destination_is_different = from_pane != to_pane;
6597 let mut moved_items = 0;
6598 for (item_ix, item_handle) in from_pane
6599 .read(cx)
6600 .items()
6601 .enumerate()
6602 .map(|(ix, item)| (ix, item.clone()))
6603 .collect::<Vec<_>>()
6604 {
6605 let ix = item_ix - moved_items;
6606 if destination_is_different {
6607 // Close item from previous pane
6608 from_pane.update(cx, |source, cx| {
6609 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
6610 });
6611 moved_items += 1;
6612 }
6613
6614 // This automatically removes duplicate items in the pane
6615 to_pane.update(cx, |destination, cx| {
6616 destination.add_item(item_handle, true, true, None, window, cx);
6617 window.focus(&destination.focus_handle(cx))
6618 });
6619 }
6620}
6621
6622pub fn move_item(
6623 source: &Entity<Pane>,
6624 destination: &Entity<Pane>,
6625 item_id_to_move: EntityId,
6626 destination_index: usize,
6627 window: &mut Window,
6628 cx: &mut App,
6629) {
6630 let Some((item_ix, item_handle)) = source
6631 .read(cx)
6632 .items()
6633 .enumerate()
6634 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6635 .map(|(ix, item)| (ix, item.clone()))
6636 else {
6637 // Tab was closed during drag
6638 return;
6639 };
6640
6641 if source != destination {
6642 // Close item from previous pane
6643 source.update(cx, |source, cx| {
6644 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
6645 });
6646 }
6647
6648 // This automatically removes duplicate items in the pane
6649 destination.update(cx, |destination, cx| {
6650 destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
6651 window.focus(&destination.focus_handle(cx))
6652 });
6653}
6654
6655pub fn move_active_item(
6656 source: &Entity<Pane>,
6657 destination: &Entity<Pane>,
6658 focus_destination: bool,
6659 close_if_empty: bool,
6660 window: &mut Window,
6661 cx: &mut App,
6662) {
6663 if source == destination {
6664 return;
6665 }
6666 let Some(active_item) = source.read(cx).active_item() else {
6667 return;
6668 };
6669 source.update(cx, |source_pane, cx| {
6670 let item_id = active_item.item_id();
6671 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
6672 destination.update(cx, |target_pane, cx| {
6673 target_pane.add_item(
6674 active_item,
6675 focus_destination,
6676 focus_destination,
6677 Some(target_pane.items_len()),
6678 window,
6679 cx,
6680 );
6681 });
6682 });
6683}
6684
6685#[cfg(test)]
6686mod tests {
6687 use std::{cell::RefCell, rc::Rc};
6688
6689 use super::*;
6690 use crate::{
6691 dock::{test::TestPanel, PanelEvent},
6692 item::{
6693 test::{TestItem, TestProjectItem},
6694 ItemEvent,
6695 },
6696 };
6697 use fs::FakeFs;
6698 use gpui::{
6699 px, DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
6700 UpdateGlobal, VisualTestContext,
6701 };
6702 use project::{Project, ProjectEntryId};
6703 use serde_json::json;
6704 use settings::SettingsStore;
6705
6706 #[gpui::test]
6707 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6708 init_test(cx);
6709
6710 let fs = FakeFs::new(cx.executor());
6711 let project = Project::test(fs, [], cx).await;
6712 let (workspace, cx) =
6713 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6714
6715 // Adding an item with no ambiguity renders the tab without detail.
6716 let item1 = cx.new(|cx| {
6717 let mut item = TestItem::new(cx);
6718 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6719 item
6720 });
6721 workspace.update_in(cx, |workspace, window, cx| {
6722 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6723 });
6724 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6725
6726 // Adding an item that creates ambiguity increases the level of detail on
6727 // both tabs.
6728 let item2 = cx.new_window_entity(|_window, cx| {
6729 let mut item = TestItem::new(cx);
6730 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6731 item
6732 });
6733 workspace.update_in(cx, |workspace, window, cx| {
6734 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6735 });
6736 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6737 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6738
6739 // Adding an item that creates ambiguity increases the level of detail only
6740 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6741 // we stop at the highest detail available.
6742 let item3 = cx.new(|cx| {
6743 let mut item = TestItem::new(cx);
6744 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6745 item
6746 });
6747 workspace.update_in(cx, |workspace, window, cx| {
6748 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6749 });
6750 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6751 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6752 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6753 }
6754
6755 #[gpui::test]
6756 async fn test_tracking_active_path(cx: &mut TestAppContext) {
6757 init_test(cx);
6758
6759 let fs = FakeFs::new(cx.executor());
6760 fs.insert_tree(
6761 "/root1",
6762 json!({
6763 "one.txt": "",
6764 "two.txt": "",
6765 }),
6766 )
6767 .await;
6768 fs.insert_tree(
6769 "/root2",
6770 json!({
6771 "three.txt": "",
6772 }),
6773 )
6774 .await;
6775
6776 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6777 let (workspace, cx) =
6778 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6779 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6780 let worktree_id = project.update(cx, |project, cx| {
6781 project.worktrees(cx).next().unwrap().read(cx).id()
6782 });
6783
6784 let item1 = cx.new(|cx| {
6785 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6786 });
6787 let item2 = cx.new(|cx| {
6788 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6789 });
6790
6791 // Add an item to an empty pane
6792 workspace.update_in(cx, |workspace, window, cx| {
6793 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
6794 });
6795 project.update(cx, |project, cx| {
6796 assert_eq!(
6797 project.active_entry(),
6798 project
6799 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6800 .map(|e| e.id)
6801 );
6802 });
6803 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6804
6805 // Add a second item to a non-empty pane
6806 workspace.update_in(cx, |workspace, window, cx| {
6807 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
6808 });
6809 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6810 project.update(cx, |project, cx| {
6811 assert_eq!(
6812 project.active_entry(),
6813 project
6814 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6815 .map(|e| e.id)
6816 );
6817 });
6818
6819 // Close the active item
6820 pane.update_in(cx, |pane, window, cx| {
6821 pane.close_active_item(&Default::default(), window, cx)
6822 .unwrap()
6823 })
6824 .await
6825 .unwrap();
6826 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6827 project.update(cx, |project, cx| {
6828 assert_eq!(
6829 project.active_entry(),
6830 project
6831 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6832 .map(|e| e.id)
6833 );
6834 });
6835
6836 // Add a project folder
6837 project
6838 .update(cx, |project, cx| {
6839 project.find_or_create_worktree("root2", true, cx)
6840 })
6841 .await
6842 .unwrap();
6843 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6844
6845 // Remove a project folder
6846 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6847 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6848 }
6849
6850 #[gpui::test]
6851 async fn test_close_window(cx: &mut TestAppContext) {
6852 init_test(cx);
6853
6854 let fs = FakeFs::new(cx.executor());
6855 fs.insert_tree("/root", json!({ "one": "" })).await;
6856
6857 let project = Project::test(fs, ["root".as_ref()], cx).await;
6858 let (workspace, cx) =
6859 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6860
6861 // When there are no dirty items, there's nothing to do.
6862 let item1 = cx.new(TestItem::new);
6863 workspace.update_in(cx, |w, window, cx| {
6864 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
6865 });
6866 let task = workspace.update_in(cx, |w, window, cx| {
6867 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6868 });
6869 assert!(task.await.unwrap());
6870
6871 // When there are dirty untitled items, prompt to save each one. If the user
6872 // cancels any prompt, then abort.
6873 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
6874 let item3 = cx.new(|cx| {
6875 TestItem::new(cx)
6876 .with_dirty(true)
6877 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6878 });
6879 workspace.update_in(cx, |w, window, cx| {
6880 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6881 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6882 });
6883 let task = workspace.update_in(cx, |w, window, cx| {
6884 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6885 });
6886 cx.executor().run_until_parked();
6887 cx.simulate_prompt_answer(2); // cancel save all
6888 cx.executor().run_until_parked();
6889 cx.simulate_prompt_answer(2); // cancel save all
6890 cx.executor().run_until_parked();
6891 assert!(!cx.has_pending_prompt());
6892 assert!(!task.await.unwrap());
6893 }
6894
6895 #[gpui::test]
6896 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6897 init_test(cx);
6898
6899 // Register TestItem as a serializable item
6900 cx.update(|cx| {
6901 register_serializable_item::<TestItem>(cx);
6902 });
6903
6904 let fs = FakeFs::new(cx.executor());
6905 fs.insert_tree("/root", json!({ "one": "" })).await;
6906
6907 let project = Project::test(fs, ["root".as_ref()], cx).await;
6908 let (workspace, cx) =
6909 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6910
6911 // When there are dirty untitled items, but they can serialize, then there is no prompt.
6912 let item1 = cx.new(|cx| {
6913 TestItem::new(cx)
6914 .with_dirty(true)
6915 .with_serialize(|| Some(Task::ready(Ok(()))))
6916 });
6917 let item2 = cx.new(|cx| {
6918 TestItem::new(cx)
6919 .with_dirty(true)
6920 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6921 .with_serialize(|| Some(Task::ready(Ok(()))))
6922 });
6923 workspace.update_in(cx, |w, window, cx| {
6924 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6925 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6926 });
6927 let task = workspace.update_in(cx, |w, window, cx| {
6928 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6929 });
6930 assert!(task.await.unwrap());
6931 }
6932
6933 #[gpui::test]
6934 async fn test_close_pane_items(cx: &mut TestAppContext) {
6935 init_test(cx);
6936
6937 let fs = FakeFs::new(cx.executor());
6938
6939 let project = Project::test(fs, None, cx).await;
6940 let (workspace, cx) =
6941 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6942
6943 let item1 = cx.new(|cx| {
6944 TestItem::new(cx)
6945 .with_dirty(true)
6946 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
6947 });
6948 let item2 = cx.new(|cx| {
6949 TestItem::new(cx)
6950 .with_dirty(true)
6951 .with_conflict(true)
6952 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
6953 });
6954 let item3 = cx.new(|cx| {
6955 TestItem::new(cx)
6956 .with_dirty(true)
6957 .with_conflict(true)
6958 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
6959 });
6960 let item4 = cx.new(|cx| {
6961 TestItem::new(cx).with_dirty(true).with_project_items(&[{
6962 let project_item = TestProjectItem::new_untitled(cx);
6963 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
6964 project_item
6965 }])
6966 });
6967 let pane = workspace.update_in(cx, |workspace, window, cx| {
6968 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6969 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6970 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6971 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
6972 workspace.active_pane().clone()
6973 });
6974
6975 let close_items = pane.update_in(cx, |pane, window, cx| {
6976 pane.activate_item(1, true, true, window, cx);
6977 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6978 let item1_id = item1.item_id();
6979 let item3_id = item3.item_id();
6980 let item4_id = item4.item_id();
6981 pane.close_items(window, cx, SaveIntent::Close, move |id| {
6982 [item1_id, item3_id, item4_id].contains(&id)
6983 })
6984 });
6985 cx.executor().run_until_parked();
6986
6987 assert!(cx.has_pending_prompt());
6988 // Ignore "Save all" prompt
6989 cx.simulate_prompt_answer(2);
6990 cx.executor().run_until_parked();
6991 // There's a prompt to save item 1.
6992 pane.update(cx, |pane, _| {
6993 assert_eq!(pane.items_len(), 4);
6994 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
6995 });
6996 // Confirm saving item 1.
6997 cx.simulate_prompt_answer(0);
6998 cx.executor().run_until_parked();
6999
7000 // Item 1 is saved. There's a prompt to save item 3.
7001 pane.update(cx, |pane, cx| {
7002 assert_eq!(item1.read(cx).save_count, 1);
7003 assert_eq!(item1.read(cx).save_as_count, 0);
7004 assert_eq!(item1.read(cx).reload_count, 0);
7005 assert_eq!(pane.items_len(), 3);
7006 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7007 });
7008 assert!(cx.has_pending_prompt());
7009
7010 // Cancel saving item 3.
7011 cx.simulate_prompt_answer(1);
7012 cx.executor().run_until_parked();
7013
7014 // Item 3 is reloaded. There's a prompt to save item 4.
7015 pane.update(cx, |pane, cx| {
7016 assert_eq!(item3.read(cx).save_count, 0);
7017 assert_eq!(item3.read(cx).save_as_count, 0);
7018 assert_eq!(item3.read(cx).reload_count, 1);
7019 assert_eq!(pane.items_len(), 2);
7020 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7021 });
7022 assert!(cx.has_pending_prompt());
7023
7024 // Confirm saving item 4.
7025 cx.simulate_prompt_answer(0);
7026 cx.executor().run_until_parked();
7027
7028 // There's a prompt for a path for item 4.
7029 cx.simulate_new_path_selection(|_| Some(Default::default()));
7030 close_items.await.unwrap();
7031
7032 // The requested items are closed.
7033 pane.update(cx, |pane, cx| {
7034 assert_eq!(item4.read(cx).save_count, 0);
7035 assert_eq!(item4.read(cx).save_as_count, 1);
7036 assert_eq!(item4.read(cx).reload_count, 0);
7037 assert_eq!(pane.items_len(), 1);
7038 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7039 });
7040 }
7041
7042 #[gpui::test]
7043 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7044 init_test(cx);
7045
7046 let fs = FakeFs::new(cx.executor());
7047 let project = Project::test(fs, [], cx).await;
7048 let (workspace, cx) =
7049 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7050
7051 // Create several workspace items with single project entries, and two
7052 // workspace items with multiple project entries.
7053 let single_entry_items = (0..=4)
7054 .map(|project_entry_id| {
7055 cx.new(|cx| {
7056 TestItem::new(cx)
7057 .with_dirty(true)
7058 .with_project_items(&[dirty_project_item(
7059 project_entry_id,
7060 &format!("{project_entry_id}.txt"),
7061 cx,
7062 )])
7063 })
7064 })
7065 .collect::<Vec<_>>();
7066 let item_2_3 = cx.new(|cx| {
7067 TestItem::new(cx)
7068 .with_dirty(true)
7069 .with_singleton(false)
7070 .with_project_items(&[
7071 single_entry_items[2].read(cx).project_items[0].clone(),
7072 single_entry_items[3].read(cx).project_items[0].clone(),
7073 ])
7074 });
7075 let item_3_4 = cx.new(|cx| {
7076 TestItem::new(cx)
7077 .with_dirty(true)
7078 .with_singleton(false)
7079 .with_project_items(&[
7080 single_entry_items[3].read(cx).project_items[0].clone(),
7081 single_entry_items[4].read(cx).project_items[0].clone(),
7082 ])
7083 });
7084
7085 // Create two panes that contain the following project entries:
7086 // left pane:
7087 // multi-entry items: (2, 3)
7088 // single-entry items: 0, 1, 2, 3, 4
7089 // right pane:
7090 // single-entry items: 1
7091 // multi-entry items: (3, 4)
7092 let left_pane = workspace.update_in(cx, |workspace, window, cx| {
7093 let left_pane = workspace.active_pane().clone();
7094 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7095 for item in single_entry_items {
7096 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
7097 }
7098 left_pane.update(cx, |pane, cx| {
7099 pane.activate_item(2, true, true, window, cx);
7100 });
7101
7102 let right_pane = workspace
7103 .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7104 .unwrap();
7105
7106 right_pane.update(cx, |pane, cx| {
7107 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
7108 });
7109
7110 left_pane
7111 });
7112
7113 cx.focus(&left_pane);
7114
7115 // When closing all of the items in the left pane, we should be prompted twice:
7116 // once for project entry 0, and once for project entry 2. Project entries 1,
7117 // 3, and 4 are all still open in the other paten. After those two
7118 // prompts, the task should complete.
7119
7120 let close = left_pane.update_in(cx, |pane, window, cx| {
7121 pane.close_all_items(&CloseAllItems::default(), window, cx)
7122 .unwrap()
7123 });
7124 cx.executor().run_until_parked();
7125
7126 // Discard "Save all" prompt
7127 cx.simulate_prompt_answer(2);
7128
7129 cx.executor().run_until_parked();
7130 left_pane.update(cx, |pane, cx| {
7131 assert_eq!(
7132 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
7133 &[ProjectEntryId::from_proto(0)]
7134 );
7135 });
7136 cx.simulate_prompt_answer(0);
7137
7138 cx.executor().run_until_parked();
7139 left_pane.update(cx, |pane, cx| {
7140 assert_eq!(
7141 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
7142 &[ProjectEntryId::from_proto(2)]
7143 );
7144 });
7145 cx.simulate_prompt_answer(0);
7146
7147 cx.executor().run_until_parked();
7148 close.await.unwrap();
7149 left_pane.update(cx, |pane, _| {
7150 assert_eq!(pane.items_len(), 0);
7151 });
7152 }
7153
7154 #[gpui::test]
7155 async fn test_autosave(cx: &mut gpui::TestAppContext) {
7156 init_test(cx);
7157
7158 let fs = FakeFs::new(cx.executor());
7159 let project = Project::test(fs, [], cx).await;
7160 let (workspace, cx) =
7161 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7162 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7163
7164 let item = cx.new(|cx| {
7165 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7166 });
7167 let item_id = item.entity_id();
7168 workspace.update_in(cx, |workspace, window, cx| {
7169 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7170 });
7171
7172 // Autosave on window change.
7173 item.update(cx, |item, cx| {
7174 SettingsStore::update_global(cx, |settings, cx| {
7175 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7176 settings.autosave = Some(AutosaveSetting::OnWindowChange);
7177 })
7178 });
7179 item.is_dirty = true;
7180 });
7181
7182 // Deactivating the window saves the file.
7183 cx.deactivate_window();
7184 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7185
7186 // Re-activating the window doesn't save the file.
7187 cx.update(|window, _| window.activate_window());
7188 cx.executor().run_until_parked();
7189 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7190
7191 // Autosave on focus change.
7192 item.update_in(cx, |item, window, cx| {
7193 cx.focus_self(window);
7194 SettingsStore::update_global(cx, |settings, cx| {
7195 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7196 settings.autosave = Some(AutosaveSetting::OnFocusChange);
7197 })
7198 });
7199 item.is_dirty = true;
7200 });
7201
7202 // Blurring the item saves the file.
7203 item.update_in(cx, |_, window, _| window.blur());
7204 cx.executor().run_until_parked();
7205 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
7206
7207 // Deactivating the window still saves the file.
7208 item.update_in(cx, |item, window, cx| {
7209 cx.focus_self(window);
7210 item.is_dirty = true;
7211 });
7212 cx.deactivate_window();
7213 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7214
7215 // Autosave after delay.
7216 item.update(cx, |item, cx| {
7217 SettingsStore::update_global(cx, |settings, cx| {
7218 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7219 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
7220 })
7221 });
7222 item.is_dirty = true;
7223 cx.emit(ItemEvent::Edit);
7224 });
7225
7226 // Delay hasn't fully expired, so the file is still dirty and unsaved.
7227 cx.executor().advance_clock(Duration::from_millis(250));
7228 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7229
7230 // After delay expires, the file is saved.
7231 cx.executor().advance_clock(Duration::from_millis(250));
7232 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
7233
7234 // Autosave on focus change, ensuring closing the tab counts as such.
7235 item.update(cx, |item, cx| {
7236 SettingsStore::update_global(cx, |settings, cx| {
7237 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7238 settings.autosave = Some(AutosaveSetting::OnFocusChange);
7239 })
7240 });
7241 item.is_dirty = true;
7242 for project_item in &mut item.project_items {
7243 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7244 }
7245 });
7246
7247 pane.update_in(cx, |pane, window, cx| {
7248 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7249 })
7250 .await
7251 .unwrap();
7252 assert!(!cx.has_pending_prompt());
7253 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7254
7255 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
7256 workspace.update_in(cx, |workspace, window, cx| {
7257 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7258 });
7259 item.update_in(cx, |item, window, cx| {
7260 item.project_items[0].update(cx, |item, _| {
7261 item.entry_id = None;
7262 });
7263 item.is_dirty = true;
7264 window.blur();
7265 });
7266 cx.run_until_parked();
7267 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7268
7269 // Ensure autosave is prevented for deleted files also when closing the buffer.
7270 let _close_items = pane.update_in(cx, |pane, window, cx| {
7271 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7272 });
7273 cx.run_until_parked();
7274 assert!(cx.has_pending_prompt());
7275 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7276 }
7277
7278 #[gpui::test]
7279 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
7280 init_test(cx);
7281
7282 let fs = FakeFs::new(cx.executor());
7283
7284 let project = Project::test(fs, [], cx).await;
7285 let (workspace, cx) =
7286 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7287
7288 let item = cx.new(|cx| {
7289 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7290 });
7291 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7292 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
7293 let toolbar_notify_count = Rc::new(RefCell::new(0));
7294
7295 workspace.update_in(cx, |workspace, window, cx| {
7296 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7297 let toolbar_notification_count = toolbar_notify_count.clone();
7298 cx.observe_in(&toolbar, window, move |_, _, _, _| {
7299 *toolbar_notification_count.borrow_mut() += 1
7300 })
7301 .detach();
7302 });
7303
7304 pane.update(cx, |pane, _| {
7305 assert!(!pane.can_navigate_backward());
7306 assert!(!pane.can_navigate_forward());
7307 });
7308
7309 item.update_in(cx, |item, _, cx| {
7310 item.set_state("one".to_string(), cx);
7311 });
7312
7313 // Toolbar must be notified to re-render the navigation buttons
7314 assert_eq!(*toolbar_notify_count.borrow(), 1);
7315
7316 pane.update(cx, |pane, _| {
7317 assert!(pane.can_navigate_backward());
7318 assert!(!pane.can_navigate_forward());
7319 });
7320
7321 workspace
7322 .update_in(cx, |workspace, window, cx| {
7323 workspace.go_back(pane.downgrade(), window, cx)
7324 })
7325 .await
7326 .unwrap();
7327
7328 assert_eq!(*toolbar_notify_count.borrow(), 2);
7329 pane.update(cx, |pane, _| {
7330 assert!(!pane.can_navigate_backward());
7331 assert!(pane.can_navigate_forward());
7332 });
7333 }
7334
7335 #[gpui::test]
7336 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
7337 init_test(cx);
7338 let fs = FakeFs::new(cx.executor());
7339
7340 let project = Project::test(fs, [], cx).await;
7341 let (workspace, cx) =
7342 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7343
7344 let panel = workspace.update_in(cx, |workspace, window, cx| {
7345 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7346 workspace.add_panel(panel.clone(), window, cx);
7347
7348 workspace
7349 .right_dock()
7350 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
7351
7352 panel
7353 });
7354
7355 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7356 pane.update_in(cx, |pane, window, cx| {
7357 let item = cx.new(TestItem::new);
7358 pane.add_item(Box::new(item), true, true, None, window, cx);
7359 });
7360
7361 // Transfer focus from center to panel
7362 workspace.update_in(cx, |workspace, window, cx| {
7363 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7364 });
7365
7366 workspace.update_in(cx, |workspace, window, cx| {
7367 assert!(workspace.right_dock().read(cx).is_open());
7368 assert!(!panel.is_zoomed(window, cx));
7369 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7370 });
7371
7372 // Transfer focus from panel to center
7373 workspace.update_in(cx, |workspace, window, cx| {
7374 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7375 });
7376
7377 workspace.update_in(cx, |workspace, window, cx| {
7378 assert!(workspace.right_dock().read(cx).is_open());
7379 assert!(!panel.is_zoomed(window, cx));
7380 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7381 });
7382
7383 // Close the dock
7384 workspace.update_in(cx, |workspace, window, cx| {
7385 workspace.toggle_dock(DockPosition::Right, window, cx);
7386 });
7387
7388 workspace.update_in(cx, |workspace, window, cx| {
7389 assert!(!workspace.right_dock().read(cx).is_open());
7390 assert!(!panel.is_zoomed(window, cx));
7391 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7392 });
7393
7394 // Open the dock
7395 workspace.update_in(cx, |workspace, window, cx| {
7396 workspace.toggle_dock(DockPosition::Right, window, cx);
7397 });
7398
7399 workspace.update_in(cx, |workspace, window, cx| {
7400 assert!(workspace.right_dock().read(cx).is_open());
7401 assert!(!panel.is_zoomed(window, cx));
7402 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7403 });
7404
7405 // Focus and zoom panel
7406 panel.update_in(cx, |panel, window, cx| {
7407 cx.focus_self(window);
7408 panel.set_zoomed(true, window, cx)
7409 });
7410
7411 workspace.update_in(cx, |workspace, window, cx| {
7412 assert!(workspace.right_dock().read(cx).is_open());
7413 assert!(panel.is_zoomed(window, cx));
7414 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7415 });
7416
7417 // Transfer focus to the center closes the dock
7418 workspace.update_in(cx, |workspace, window, cx| {
7419 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7420 });
7421
7422 workspace.update_in(cx, |workspace, window, cx| {
7423 assert!(!workspace.right_dock().read(cx).is_open());
7424 assert!(panel.is_zoomed(window, cx));
7425 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7426 });
7427
7428 // Transferring focus back to the panel keeps it zoomed
7429 workspace.update_in(cx, |workspace, window, cx| {
7430 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7431 });
7432
7433 workspace.update_in(cx, |workspace, window, cx| {
7434 assert!(workspace.right_dock().read(cx).is_open());
7435 assert!(panel.is_zoomed(window, cx));
7436 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7437 });
7438
7439 // Close the dock while it is zoomed
7440 workspace.update_in(cx, |workspace, window, cx| {
7441 workspace.toggle_dock(DockPosition::Right, window, cx)
7442 });
7443
7444 workspace.update_in(cx, |workspace, window, cx| {
7445 assert!(!workspace.right_dock().read(cx).is_open());
7446 assert!(panel.is_zoomed(window, cx));
7447 assert!(workspace.zoomed.is_none());
7448 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7449 });
7450
7451 // Opening the dock, when it's zoomed, retains focus
7452 workspace.update_in(cx, |workspace, window, cx| {
7453 workspace.toggle_dock(DockPosition::Right, window, cx)
7454 });
7455
7456 workspace.update_in(cx, |workspace, window, cx| {
7457 assert!(workspace.right_dock().read(cx).is_open());
7458 assert!(panel.is_zoomed(window, cx));
7459 assert!(workspace.zoomed.is_some());
7460 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7461 });
7462
7463 // Unzoom and close the panel, zoom the active pane.
7464 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
7465 workspace.update_in(cx, |workspace, window, cx| {
7466 workspace.toggle_dock(DockPosition::Right, window, cx)
7467 });
7468 pane.update_in(cx, |pane, window, cx| {
7469 pane.toggle_zoom(&Default::default(), window, cx)
7470 });
7471
7472 // Opening a dock unzooms the pane.
7473 workspace.update_in(cx, |workspace, window, cx| {
7474 workspace.toggle_dock(DockPosition::Right, window, cx)
7475 });
7476 workspace.update_in(cx, |workspace, window, cx| {
7477 let pane = pane.read(cx);
7478 assert!(!pane.is_zoomed());
7479 assert!(!pane.focus_handle(cx).is_focused(window));
7480 assert!(workspace.right_dock().read(cx).is_open());
7481 assert!(workspace.zoomed.is_none());
7482 });
7483 }
7484
7485 #[gpui::test]
7486 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7487 init_test(cx);
7488
7489 let fs = FakeFs::new(cx.executor());
7490
7491 let project = Project::test(fs, None, cx).await;
7492 let (workspace, cx) =
7493 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7494
7495 // Let's arrange the panes like this:
7496 //
7497 // +-----------------------+
7498 // | top |
7499 // +------+--------+-------+
7500 // | left | center | right |
7501 // +------+--------+-------+
7502 // | bottom |
7503 // +-----------------------+
7504
7505 let top_item = cx.new(|cx| {
7506 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7507 });
7508 let bottom_item = cx.new(|cx| {
7509 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7510 });
7511 let left_item = cx.new(|cx| {
7512 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7513 });
7514 let right_item = cx.new(|cx| {
7515 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7516 });
7517 let center_item = cx.new(|cx| {
7518 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7519 });
7520
7521 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7522 let top_pane_id = workspace.active_pane().entity_id();
7523 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
7524 workspace.split_pane(
7525 workspace.active_pane().clone(),
7526 SplitDirection::Down,
7527 window,
7528 cx,
7529 );
7530 top_pane_id
7531 });
7532 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7533 let bottom_pane_id = workspace.active_pane().entity_id();
7534 workspace.add_item_to_active_pane(
7535 Box::new(bottom_item.clone()),
7536 None,
7537 false,
7538 window,
7539 cx,
7540 );
7541 workspace.split_pane(
7542 workspace.active_pane().clone(),
7543 SplitDirection::Up,
7544 window,
7545 cx,
7546 );
7547 bottom_pane_id
7548 });
7549 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7550 let left_pane_id = workspace.active_pane().entity_id();
7551 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
7552 workspace.split_pane(
7553 workspace.active_pane().clone(),
7554 SplitDirection::Right,
7555 window,
7556 cx,
7557 );
7558 left_pane_id
7559 });
7560 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7561 let right_pane_id = workspace.active_pane().entity_id();
7562 workspace.add_item_to_active_pane(
7563 Box::new(right_item.clone()),
7564 None,
7565 false,
7566 window,
7567 cx,
7568 );
7569 workspace.split_pane(
7570 workspace.active_pane().clone(),
7571 SplitDirection::Left,
7572 window,
7573 cx,
7574 );
7575 right_pane_id
7576 });
7577 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7578 let center_pane_id = workspace.active_pane().entity_id();
7579 workspace.add_item_to_active_pane(
7580 Box::new(center_item.clone()),
7581 None,
7582 false,
7583 window,
7584 cx,
7585 );
7586 center_pane_id
7587 });
7588 cx.executor().run_until_parked();
7589
7590 workspace.update_in(cx, |workspace, window, cx| {
7591 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7592
7593 // Join into next from center pane into right
7594 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7595 });
7596
7597 workspace.update_in(cx, |workspace, window, cx| {
7598 let active_pane = workspace.active_pane();
7599 assert_eq!(right_pane_id, active_pane.entity_id());
7600 assert_eq!(2, active_pane.read(cx).items_len());
7601 let item_ids_in_pane =
7602 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7603 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7604 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7605
7606 // Join into next from right pane into bottom
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!(bottom_pane_id, active_pane.entity_id());
7613 assert_eq!(3, 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 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7619
7620 // Join into next from bottom pane into left
7621 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7622 });
7623
7624 workspace.update_in(cx, |workspace, window, cx| {
7625 let active_pane = workspace.active_pane();
7626 assert_eq!(left_pane_id, active_pane.entity_id());
7627 assert_eq!(4, active_pane.read(cx).items_len());
7628 let item_ids_in_pane =
7629 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7630 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7631 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7632 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7633 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7634
7635 // Join into next from left pane into top
7636 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7637 });
7638
7639 workspace.update_in(cx, |workspace, window, cx| {
7640 let active_pane = workspace.active_pane();
7641 assert_eq!(top_pane_id, active_pane.entity_id());
7642 assert_eq!(5, active_pane.read(cx).items_len());
7643 let item_ids_in_pane =
7644 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7645 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7646 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7647 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7648 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7649 assert!(item_ids_in_pane.contains(&top_item.item_id()));
7650
7651 // Single pane left: no-op
7652 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
7653 });
7654
7655 workspace.update(cx, |workspace, _cx| {
7656 let active_pane = workspace.active_pane();
7657 assert_eq!(top_pane_id, active_pane.entity_id());
7658 });
7659 }
7660
7661 fn add_an_item_to_active_pane(
7662 cx: &mut VisualTestContext,
7663 workspace: &Entity<Workspace>,
7664 item_id: u64,
7665 ) -> Entity<TestItem> {
7666 let item = cx.new(|cx| {
7667 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7668 item_id,
7669 "item{item_id}.txt",
7670 cx,
7671 )])
7672 });
7673 workspace.update_in(cx, |workspace, window, cx| {
7674 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
7675 });
7676 return item;
7677 }
7678
7679 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
7680 return workspace.update_in(cx, |workspace, window, cx| {
7681 let new_pane = workspace.split_pane(
7682 workspace.active_pane().clone(),
7683 SplitDirection::Right,
7684 window,
7685 cx,
7686 );
7687 new_pane
7688 });
7689 }
7690
7691 #[gpui::test]
7692 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7693 init_test(cx);
7694 let fs = FakeFs::new(cx.executor());
7695 let project = Project::test(fs, None, cx).await;
7696 let (workspace, cx) =
7697 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7698
7699 add_an_item_to_active_pane(cx, &workspace, 1);
7700 split_pane(cx, &workspace);
7701 add_an_item_to_active_pane(cx, &workspace, 2);
7702 split_pane(cx, &workspace); // empty pane
7703 split_pane(cx, &workspace);
7704 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7705
7706 cx.executor().run_until_parked();
7707
7708 workspace.update(cx, |workspace, cx| {
7709 let num_panes = workspace.panes().len();
7710 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7711 let active_item = workspace
7712 .active_pane()
7713 .read(cx)
7714 .active_item()
7715 .expect("item is in focus");
7716
7717 assert_eq!(num_panes, 4);
7718 assert_eq!(num_items_in_current_pane, 1);
7719 assert_eq!(active_item.item_id(), last_item.item_id());
7720 });
7721
7722 workspace.update_in(cx, |workspace, window, cx| {
7723 workspace.join_all_panes(window, cx);
7724 });
7725
7726 workspace.update(cx, |workspace, cx| {
7727 let num_panes = workspace.panes().len();
7728 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7729 let active_item = workspace
7730 .active_pane()
7731 .read(cx)
7732 .active_item()
7733 .expect("item is in focus");
7734
7735 assert_eq!(num_panes, 1);
7736 assert_eq!(num_items_in_current_pane, 3);
7737 assert_eq!(active_item.item_id(), last_item.item_id());
7738 });
7739 }
7740 struct TestModal(FocusHandle);
7741
7742 impl TestModal {
7743 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
7744 Self(cx.focus_handle())
7745 }
7746 }
7747
7748 impl EventEmitter<DismissEvent> for TestModal {}
7749
7750 impl Focusable for TestModal {
7751 fn focus_handle(&self, _cx: &App) -> FocusHandle {
7752 self.0.clone()
7753 }
7754 }
7755
7756 impl ModalView for TestModal {}
7757
7758 impl Render for TestModal {
7759 fn render(
7760 &mut self,
7761 _window: &mut Window,
7762 _cx: &mut Context<TestModal>,
7763 ) -> impl IntoElement {
7764 div().track_focus(&self.0)
7765 }
7766 }
7767
7768 #[gpui::test]
7769 async fn test_panels(cx: &mut gpui::TestAppContext) {
7770 init_test(cx);
7771 let fs = FakeFs::new(cx.executor());
7772
7773 let project = Project::test(fs, [], cx).await;
7774 let (workspace, cx) =
7775 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7776
7777 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
7778 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
7779 workspace.add_panel(panel_1.clone(), window, cx);
7780 workspace.toggle_dock(DockPosition::Left, window, cx);
7781 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7782 workspace.add_panel(panel_2.clone(), window, cx);
7783 workspace.toggle_dock(DockPosition::Right, window, cx);
7784
7785 let left_dock = workspace.left_dock();
7786 assert_eq!(
7787 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7788 panel_1.panel_id()
7789 );
7790 assert_eq!(
7791 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7792 panel_1.size(window, cx)
7793 );
7794
7795 left_dock.update(cx, |left_dock, cx| {
7796 left_dock.resize_active_panel(Some(px(1337.)), window, cx)
7797 });
7798 assert_eq!(
7799 workspace
7800 .right_dock()
7801 .read(cx)
7802 .visible_panel()
7803 .unwrap()
7804 .panel_id(),
7805 panel_2.panel_id(),
7806 );
7807
7808 (panel_1, panel_2)
7809 });
7810
7811 // Move panel_1 to the right
7812 panel_1.update_in(cx, |panel_1, window, cx| {
7813 panel_1.set_position(DockPosition::Right, window, cx)
7814 });
7815
7816 workspace.update_in(cx, |workspace, window, cx| {
7817 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7818 // Since it was the only panel on the left, the left dock should now be closed.
7819 assert!(!workspace.left_dock().read(cx).is_open());
7820 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7821 let right_dock = workspace.right_dock();
7822 assert_eq!(
7823 right_dock.read(cx).visible_panel().unwrap().panel_id(),
7824 panel_1.panel_id()
7825 );
7826 assert_eq!(
7827 right_dock.read(cx).active_panel_size(window, cx).unwrap(),
7828 px(1337.)
7829 );
7830
7831 // Now we move panel_2 to the left
7832 panel_2.set_position(DockPosition::Left, window, cx);
7833 });
7834
7835 workspace.update(cx, |workspace, cx| {
7836 // Since panel_2 was not visible on the right, we don't open the left dock.
7837 assert!(!workspace.left_dock().read(cx).is_open());
7838 // And the right dock is unaffected in its displaying of panel_1
7839 assert!(workspace.right_dock().read(cx).is_open());
7840 assert_eq!(
7841 workspace
7842 .right_dock()
7843 .read(cx)
7844 .visible_panel()
7845 .unwrap()
7846 .panel_id(),
7847 panel_1.panel_id(),
7848 );
7849 });
7850
7851 // Move panel_1 back to the left
7852 panel_1.update_in(cx, |panel_1, window, cx| {
7853 panel_1.set_position(DockPosition::Left, window, cx)
7854 });
7855
7856 workspace.update_in(cx, |workspace, window, cx| {
7857 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7858 let left_dock = workspace.left_dock();
7859 assert!(left_dock.read(cx).is_open());
7860 assert_eq!(
7861 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7862 panel_1.panel_id()
7863 );
7864 assert_eq!(
7865 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7866 px(1337.)
7867 );
7868 // And the right dock should be closed as it no longer has any panels.
7869 assert!(!workspace.right_dock().read(cx).is_open());
7870
7871 // Now we move panel_1 to the bottom
7872 panel_1.set_position(DockPosition::Bottom, window, cx);
7873 });
7874
7875 workspace.update_in(cx, |workspace, window, cx| {
7876 // Since panel_1 was visible on the left, we close the left dock.
7877 assert!(!workspace.left_dock().read(cx).is_open());
7878 // The bottom dock is sized based on the panel's default size,
7879 // since the panel orientation changed from vertical to horizontal.
7880 let bottom_dock = workspace.bottom_dock();
7881 assert_eq!(
7882 bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
7883 panel_1.size(window, cx),
7884 );
7885 // Close bottom dock and move panel_1 back to the left.
7886 bottom_dock.update(cx, |bottom_dock, cx| {
7887 bottom_dock.set_open(false, window, cx)
7888 });
7889 panel_1.set_position(DockPosition::Left, window, cx);
7890 });
7891
7892 // Emit activated event on panel 1
7893 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7894
7895 // Now the left dock is open and panel_1 is active and focused.
7896 workspace.update_in(cx, |workspace, window, cx| {
7897 let left_dock = workspace.left_dock();
7898 assert!(left_dock.read(cx).is_open());
7899 assert_eq!(
7900 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7901 panel_1.panel_id(),
7902 );
7903 assert!(panel_1.focus_handle(cx).is_focused(window));
7904 });
7905
7906 // Emit closed event on panel 2, which is not active
7907 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7908
7909 // Wo don't close the left dock, because panel_2 wasn't the active panel
7910 workspace.update(cx, |workspace, cx| {
7911 let left_dock = workspace.left_dock();
7912 assert!(left_dock.read(cx).is_open());
7913 assert_eq!(
7914 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7915 panel_1.panel_id(),
7916 );
7917 });
7918
7919 // Emitting a ZoomIn event shows the panel as zoomed.
7920 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7921 workspace.update(cx, |workspace, _| {
7922 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7923 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7924 });
7925
7926 // Move panel to another dock while it is zoomed
7927 panel_1.update_in(cx, |panel, window, cx| {
7928 panel.set_position(DockPosition::Right, window, cx)
7929 });
7930 workspace.update(cx, |workspace, _| {
7931 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7932
7933 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7934 });
7935
7936 // This is a helper for getting a:
7937 // - valid focus on an element,
7938 // - that isn't a part of the panes and panels system of the Workspace,
7939 // - and doesn't trigger the 'on_focus_lost' API.
7940 let focus_other_view = {
7941 let workspace = workspace.clone();
7942 move |cx: &mut VisualTestContext| {
7943 workspace.update_in(cx, |workspace, window, cx| {
7944 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
7945 workspace.toggle_modal(window, cx, TestModal::new);
7946 workspace.toggle_modal(window, cx, TestModal::new);
7947 } else {
7948 workspace.toggle_modal(window, cx, TestModal::new);
7949 }
7950 })
7951 }
7952 };
7953
7954 // If focus is transferred to another view that's not a panel or another pane, we still show
7955 // the panel as zoomed.
7956 focus_other_view(cx);
7957 workspace.update(cx, |workspace, _| {
7958 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7959 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7960 });
7961
7962 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
7963 workspace.update_in(cx, |_workspace, window, cx| {
7964 cx.focus_self(window);
7965 });
7966 workspace.update(cx, |workspace, _| {
7967 assert_eq!(workspace.zoomed, None);
7968 assert_eq!(workspace.zoomed_position, None);
7969 });
7970
7971 // If focus is transferred again to another view that's not a panel or a pane, we won't
7972 // show the panel as zoomed because it wasn't zoomed before.
7973 focus_other_view(cx);
7974 workspace.update(cx, |workspace, _| {
7975 assert_eq!(workspace.zoomed, None);
7976 assert_eq!(workspace.zoomed_position, None);
7977 });
7978
7979 // When the panel is activated, it is zoomed again.
7980 cx.dispatch_action(ToggleRightDock);
7981 workspace.update(cx, |workspace, _| {
7982 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7983 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7984 });
7985
7986 // Emitting a ZoomOut event unzooms the panel.
7987 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
7988 workspace.update(cx, |workspace, _| {
7989 assert_eq!(workspace.zoomed, None);
7990 assert_eq!(workspace.zoomed_position, None);
7991 });
7992
7993 // Emit closed event on panel 1, which is active
7994 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7995
7996 // Now the left dock is closed, because panel_1 was the active panel
7997 workspace.update(cx, |workspace, cx| {
7998 let right_dock = workspace.right_dock();
7999 assert!(!right_dock.read(cx).is_open());
8000 });
8001 }
8002
8003 #[gpui::test]
8004 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8005 init_test(cx);
8006
8007 let fs = FakeFs::new(cx.background_executor.clone());
8008 let project = Project::test(fs, [], cx).await;
8009 let (workspace, cx) =
8010 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8011 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8012
8013 let dirty_regular_buffer = cx.new(|cx| {
8014 TestItem::new(cx)
8015 .with_dirty(true)
8016 .with_label("1.txt")
8017 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8018 });
8019 let dirty_regular_buffer_2 = cx.new(|cx| {
8020 TestItem::new(cx)
8021 .with_dirty(true)
8022 .with_label("2.txt")
8023 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8024 });
8025 let dirty_multi_buffer_with_both = cx.new(|cx| {
8026 TestItem::new(cx)
8027 .with_dirty(true)
8028 .with_singleton(false)
8029 .with_label("Fake Project Search")
8030 .with_project_items(&[
8031 dirty_regular_buffer.read(cx).project_items[0].clone(),
8032 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8033 ])
8034 });
8035 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8036 workspace.update_in(cx, |workspace, window, cx| {
8037 workspace.add_item(
8038 pane.clone(),
8039 Box::new(dirty_regular_buffer.clone()),
8040 None,
8041 false,
8042 false,
8043 window,
8044 cx,
8045 );
8046 workspace.add_item(
8047 pane.clone(),
8048 Box::new(dirty_regular_buffer_2.clone()),
8049 None,
8050 false,
8051 false,
8052 window,
8053 cx,
8054 );
8055 workspace.add_item(
8056 pane.clone(),
8057 Box::new(dirty_multi_buffer_with_both.clone()),
8058 None,
8059 false,
8060 false,
8061 window,
8062 cx,
8063 );
8064 });
8065
8066 pane.update_in(cx, |pane, window, cx| {
8067 pane.activate_item(2, true, true, window, cx);
8068 assert_eq!(
8069 pane.active_item().unwrap().item_id(),
8070 multi_buffer_with_both_files_id,
8071 "Should select the multi buffer in the pane"
8072 );
8073 });
8074 let close_all_but_multi_buffer_task = pane
8075 .update_in(cx, |pane, window, cx| {
8076 pane.close_inactive_items(
8077 &CloseInactiveItems {
8078 save_intent: Some(SaveIntent::Save),
8079 close_pinned: true,
8080 },
8081 window,
8082 cx,
8083 )
8084 })
8085 .expect("should have inactive files to close");
8086 cx.background_executor.run_until_parked();
8087 assert!(
8088 !cx.has_pending_prompt(),
8089 "Multi buffer still has the unsaved buffer inside, so no save prompt should be shown"
8090 );
8091 close_all_but_multi_buffer_task
8092 .await
8093 .expect("Closing all buffers but the multi buffer failed");
8094 pane.update(cx, |pane, cx| {
8095 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8096 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
8097 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8098 assert_eq!(pane.items_len(), 1);
8099 assert_eq!(
8100 pane.active_item().unwrap().item_id(),
8101 multi_buffer_with_both_files_id,
8102 "Should have only the multi buffer left in the pane"
8103 );
8104 assert!(
8105 dirty_multi_buffer_with_both.read(cx).is_dirty,
8106 "The multi buffer containing the unsaved buffer should still be dirty"
8107 );
8108 });
8109
8110 let close_multi_buffer_task = pane
8111 .update_in(cx, |pane, window, cx| {
8112 pane.close_active_item(
8113 &CloseActiveItem {
8114 save_intent: Some(SaveIntent::Close),
8115 },
8116 window,
8117 cx,
8118 )
8119 })
8120 .expect("should have the multi buffer to close");
8121 cx.background_executor.run_until_parked();
8122 assert!(
8123 cx.has_pending_prompt(),
8124 "Dirty multi buffer should prompt a save dialog"
8125 );
8126 cx.simulate_prompt_answer(0);
8127 cx.background_executor.run_until_parked();
8128 close_multi_buffer_task
8129 .await
8130 .expect("Closing the multi buffer failed");
8131 pane.update(cx, |pane, cx| {
8132 assert_eq!(
8133 dirty_multi_buffer_with_both.read(cx).save_count,
8134 1,
8135 "Multi buffer item should get be saved"
8136 );
8137 // Test impl does not save inner items, so we do not assert them
8138 assert_eq!(
8139 pane.items_len(),
8140 0,
8141 "No more items should be left in the pane"
8142 );
8143 assert!(pane.active_item().is_none());
8144 });
8145 }
8146
8147 #[gpui::test]
8148 async fn test_no_save_prompt_when_dirty_singleton_buffer_closed_with_a_multi_buffer_containing_it_present_in_the_pane(
8149 cx: &mut TestAppContext,
8150 ) {
8151 init_test(cx);
8152
8153 let fs = FakeFs::new(cx.background_executor.clone());
8154 let project = Project::test(fs, [], cx).await;
8155 let (workspace, cx) =
8156 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8157 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8158
8159 let dirty_regular_buffer = cx.new(|cx| {
8160 TestItem::new(cx)
8161 .with_dirty(true)
8162 .with_label("1.txt")
8163 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8164 });
8165 let dirty_regular_buffer_2 = cx.new(|cx| {
8166 TestItem::new(cx)
8167 .with_dirty(true)
8168 .with_label("2.txt")
8169 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8170 });
8171 let clear_regular_buffer = cx.new(|cx| {
8172 TestItem::new(cx)
8173 .with_label("3.txt")
8174 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8175 });
8176
8177 let dirty_multi_buffer_with_both = cx.new(|cx| {
8178 TestItem::new(cx)
8179 .with_dirty(true)
8180 .with_singleton(false)
8181 .with_label("Fake Project Search")
8182 .with_project_items(&[
8183 dirty_regular_buffer.read(cx).project_items[0].clone(),
8184 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8185 clear_regular_buffer.read(cx).project_items[0].clone(),
8186 ])
8187 });
8188 workspace.update_in(cx, |workspace, window, cx| {
8189 workspace.add_item(
8190 pane.clone(),
8191 Box::new(dirty_regular_buffer.clone()),
8192 None,
8193 false,
8194 false,
8195 window,
8196 cx,
8197 );
8198 workspace.add_item(
8199 pane.clone(),
8200 Box::new(dirty_multi_buffer_with_both.clone()),
8201 None,
8202 false,
8203 false,
8204 window,
8205 cx,
8206 );
8207 });
8208
8209 pane.update_in(cx, |pane, window, cx| {
8210 pane.activate_item(0, true, true, window, cx);
8211 assert_eq!(
8212 pane.active_item().unwrap().item_id(),
8213 dirty_regular_buffer.item_id(),
8214 "Should select the dirty singleton buffer in the pane"
8215 );
8216 });
8217 let close_singleton_buffer_task = pane
8218 .update_in(cx, |pane, window, cx| {
8219 pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
8220 })
8221 .expect("should have active singleton buffer to close");
8222 cx.background_executor.run_until_parked();
8223 assert!(
8224 !cx.has_pending_prompt(),
8225 "Multi buffer is still in the pane and has the unsaved buffer inside, so no save prompt should be shown"
8226 );
8227
8228 close_singleton_buffer_task
8229 .await
8230 .expect("Should not fail closing the singleton buffer");
8231 pane.update(cx, |pane, cx| {
8232 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8233 assert_eq!(
8234 dirty_multi_buffer_with_both.read(cx).save_count,
8235 0,
8236 "Multi buffer itself should not be saved"
8237 );
8238 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8239 assert_eq!(
8240 pane.items_len(),
8241 1,
8242 "A dirty multi buffer should be present in the pane"
8243 );
8244 assert_eq!(
8245 pane.active_item().unwrap().item_id(),
8246 dirty_multi_buffer_with_both.item_id(),
8247 "Should activate the only remaining item in the pane"
8248 );
8249 });
8250 }
8251
8252 #[gpui::test]
8253 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
8254 cx: &mut TestAppContext,
8255 ) {
8256 init_test(cx);
8257
8258 let fs = FakeFs::new(cx.background_executor.clone());
8259 let project = Project::test(fs, [], cx).await;
8260 let (workspace, cx) =
8261 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8262 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8263
8264 let dirty_regular_buffer = cx.new(|cx| {
8265 TestItem::new(cx)
8266 .with_dirty(true)
8267 .with_label("1.txt")
8268 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8269 });
8270 let dirty_regular_buffer_2 = cx.new(|cx| {
8271 TestItem::new(cx)
8272 .with_dirty(true)
8273 .with_label("2.txt")
8274 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8275 });
8276 let clear_regular_buffer = cx.new(|cx| {
8277 TestItem::new(cx)
8278 .with_label("3.txt")
8279 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8280 });
8281
8282 let dirty_multi_buffer_with_both = cx.new(|cx| {
8283 TestItem::new(cx)
8284 .with_dirty(true)
8285 .with_singleton(false)
8286 .with_label("Fake Project Search")
8287 .with_project_items(&[
8288 dirty_regular_buffer.read(cx).project_items[0].clone(),
8289 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8290 clear_regular_buffer.read(cx).project_items[0].clone(),
8291 ])
8292 });
8293 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8294 workspace.update_in(cx, |workspace, window, cx| {
8295 workspace.add_item(
8296 pane.clone(),
8297 Box::new(dirty_regular_buffer.clone()),
8298 None,
8299 false,
8300 false,
8301 window,
8302 cx,
8303 );
8304 workspace.add_item(
8305 pane.clone(),
8306 Box::new(dirty_multi_buffer_with_both.clone()),
8307 None,
8308 false,
8309 false,
8310 window,
8311 cx,
8312 );
8313 });
8314
8315 pane.update_in(cx, |pane, window, cx| {
8316 pane.activate_item(1, true, true, window, cx);
8317 assert_eq!(
8318 pane.active_item().unwrap().item_id(),
8319 multi_buffer_with_both_files_id,
8320 "Should select the multi buffer in the pane"
8321 );
8322 });
8323 let _close_multi_buffer_task = pane
8324 .update_in(cx, |pane, window, cx| {
8325 pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
8326 })
8327 .expect("should have active multi buffer to close");
8328 cx.background_executor.run_until_parked();
8329 assert!(
8330 cx.has_pending_prompt(),
8331 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
8332 );
8333 }
8334
8335 #[gpui::test]
8336 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
8337 cx: &mut TestAppContext,
8338 ) {
8339 init_test(cx);
8340
8341 let fs = FakeFs::new(cx.background_executor.clone());
8342 let project = Project::test(fs, [], cx).await;
8343 let (workspace, cx) =
8344 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8345 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8346
8347 let dirty_regular_buffer = cx.new(|cx| {
8348 TestItem::new(cx)
8349 .with_dirty(true)
8350 .with_label("1.txt")
8351 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8352 });
8353 let dirty_regular_buffer_2 = cx.new(|cx| {
8354 TestItem::new(cx)
8355 .with_dirty(true)
8356 .with_label("2.txt")
8357 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8358 });
8359 let clear_regular_buffer = cx.new(|cx| {
8360 TestItem::new(cx)
8361 .with_label("3.txt")
8362 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8363 });
8364
8365 let dirty_multi_buffer = cx.new(|cx| {
8366 TestItem::new(cx)
8367 .with_dirty(true)
8368 .with_singleton(false)
8369 .with_label("Fake Project Search")
8370 .with_project_items(&[
8371 dirty_regular_buffer.read(cx).project_items[0].clone(),
8372 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8373 clear_regular_buffer.read(cx).project_items[0].clone(),
8374 ])
8375 });
8376 workspace.update_in(cx, |workspace, window, cx| {
8377 workspace.add_item(
8378 pane.clone(),
8379 Box::new(dirty_regular_buffer.clone()),
8380 None,
8381 false,
8382 false,
8383 window,
8384 cx,
8385 );
8386 workspace.add_item(
8387 pane.clone(),
8388 Box::new(dirty_regular_buffer_2.clone()),
8389 None,
8390 false,
8391 false,
8392 window,
8393 cx,
8394 );
8395 workspace.add_item(
8396 pane.clone(),
8397 Box::new(dirty_multi_buffer.clone()),
8398 None,
8399 false,
8400 false,
8401 window,
8402 cx,
8403 );
8404 });
8405
8406 pane.update_in(cx, |pane, window, cx| {
8407 pane.activate_item(2, true, true, window, cx);
8408 assert_eq!(
8409 pane.active_item().unwrap().item_id(),
8410 dirty_multi_buffer.item_id(),
8411 "Should select the multi buffer in the pane"
8412 );
8413 });
8414 let close_multi_buffer_task = pane
8415 .update_in(cx, |pane, window, cx| {
8416 pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
8417 })
8418 .expect("should have active multi buffer to close");
8419 cx.background_executor.run_until_parked();
8420 assert!(
8421 !cx.has_pending_prompt(),
8422 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
8423 );
8424 close_multi_buffer_task
8425 .await
8426 .expect("Closing multi buffer failed");
8427 pane.update(cx, |pane, cx| {
8428 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8429 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
8430 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8431 assert_eq!(
8432 pane.items()
8433 .map(|item| item.item_id())
8434 .sorted()
8435 .collect::<Vec<_>>(),
8436 vec![
8437 dirty_regular_buffer.item_id(),
8438 dirty_regular_buffer_2.item_id(),
8439 ],
8440 "Should have no multi buffer left in the pane"
8441 );
8442 assert!(dirty_regular_buffer.read(cx).is_dirty);
8443 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
8444 });
8445 }
8446
8447 #[gpui::test]
8448 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
8449 init_test(cx);
8450 let fs = FakeFs::new(cx.executor());
8451 let project = Project::test(fs, [], cx).await;
8452 let (workspace, cx) =
8453 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8454
8455 // Add a new panel to the right dock, opening the dock and setting the
8456 // focus to the new panel.
8457 let panel = workspace.update_in(cx, |workspace, window, cx| {
8458 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8459 workspace.add_panel(panel.clone(), window, cx);
8460
8461 workspace
8462 .right_dock()
8463 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8464
8465 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8466
8467 panel
8468 });
8469
8470 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8471 // panel to the next valid position which, in this case, is the left
8472 // dock.
8473 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8474 workspace.update(cx, |workspace, cx| {
8475 assert!(workspace.left_dock().read(cx).is_open());
8476 assert_eq!(panel.read(cx).position, DockPosition::Left);
8477 });
8478
8479 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8480 // panel to the next valid position which, in this case, is the bottom
8481 // dock.
8482 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8483 workspace.update(cx, |workspace, cx| {
8484 assert!(workspace.bottom_dock().read(cx).is_open());
8485 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
8486 });
8487
8488 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
8489 // around moving the panel to its initial position, the right dock.
8490 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8491 workspace.update(cx, |workspace, cx| {
8492 assert!(workspace.right_dock().read(cx).is_open());
8493 assert_eq!(panel.read(cx).position, DockPosition::Right);
8494 });
8495
8496 // Remove focus from the panel, ensuring that, if the panel is not
8497 // focused, the `MoveFocusedPanelToNextPosition` action does not update
8498 // the panel's position, so the panel is still in the right dock.
8499 workspace.update_in(cx, |workspace, window, cx| {
8500 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8501 });
8502
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
8510 mod register_project_item_tests {
8511
8512 use super::*;
8513
8514 // View
8515 struct TestPngItemView {
8516 focus_handle: FocusHandle,
8517 }
8518 // Model
8519 struct TestPngItem {}
8520
8521 impl project::ProjectItem for TestPngItem {
8522 fn try_open(
8523 _project: &Entity<Project>,
8524 path: &ProjectPath,
8525 cx: &mut App,
8526 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8527 if path.path.extension().unwrap() == "png" {
8528 Some(cx.spawn(|mut cx| async move { cx.new(|_| TestPngItem {}) }))
8529 } else {
8530 None
8531 }
8532 }
8533
8534 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8535 None
8536 }
8537
8538 fn project_path(&self, _: &App) -> Option<ProjectPath> {
8539 None
8540 }
8541
8542 fn is_dirty(&self) -> bool {
8543 false
8544 }
8545 }
8546
8547 impl Item for TestPngItemView {
8548 type Event = ();
8549 }
8550 impl EventEmitter<()> for TestPngItemView {}
8551 impl Focusable for TestPngItemView {
8552 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8553 self.focus_handle.clone()
8554 }
8555 }
8556
8557 impl Render for TestPngItemView {
8558 fn render(
8559 &mut self,
8560 _window: &mut Window,
8561 _cx: &mut Context<Self>,
8562 ) -> impl IntoElement {
8563 Empty
8564 }
8565 }
8566
8567 impl ProjectItem for TestPngItemView {
8568 type Item = TestPngItem;
8569
8570 fn for_project_item(
8571 _project: Entity<Project>,
8572 _item: Entity<Self::Item>,
8573 _: &mut Window,
8574 cx: &mut Context<Self>,
8575 ) -> Self
8576 where
8577 Self: Sized,
8578 {
8579 Self {
8580 focus_handle: cx.focus_handle(),
8581 }
8582 }
8583 }
8584
8585 // View
8586 struct TestIpynbItemView {
8587 focus_handle: FocusHandle,
8588 }
8589 // Model
8590 struct TestIpynbItem {}
8591
8592 impl project::ProjectItem for TestIpynbItem {
8593 fn try_open(
8594 _project: &Entity<Project>,
8595 path: &ProjectPath,
8596 cx: &mut App,
8597 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8598 if path.path.extension().unwrap() == "ipynb" {
8599 Some(cx.spawn(|mut cx| async move { cx.new(|_| TestIpynbItem {}) }))
8600 } else {
8601 None
8602 }
8603 }
8604
8605 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8606 None
8607 }
8608
8609 fn project_path(&self, _: &App) -> Option<ProjectPath> {
8610 None
8611 }
8612
8613 fn is_dirty(&self) -> bool {
8614 false
8615 }
8616 }
8617
8618 impl Item for TestIpynbItemView {
8619 type Event = ();
8620 }
8621 impl EventEmitter<()> for TestIpynbItemView {}
8622 impl Focusable for TestIpynbItemView {
8623 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8624 self.focus_handle.clone()
8625 }
8626 }
8627
8628 impl Render for TestIpynbItemView {
8629 fn render(
8630 &mut self,
8631 _window: &mut Window,
8632 _cx: &mut Context<Self>,
8633 ) -> impl IntoElement {
8634 Empty
8635 }
8636 }
8637
8638 impl ProjectItem for TestIpynbItemView {
8639 type Item = TestIpynbItem;
8640
8641 fn for_project_item(
8642 _project: Entity<Project>,
8643 _item: Entity<Self::Item>,
8644 _: &mut Window,
8645 cx: &mut Context<Self>,
8646 ) -> Self
8647 where
8648 Self: Sized,
8649 {
8650 Self {
8651 focus_handle: cx.focus_handle(),
8652 }
8653 }
8654 }
8655
8656 struct TestAlternatePngItemView {
8657 focus_handle: FocusHandle,
8658 }
8659
8660 impl Item for TestAlternatePngItemView {
8661 type Event = ();
8662 }
8663
8664 impl EventEmitter<()> for TestAlternatePngItemView {}
8665 impl Focusable for TestAlternatePngItemView {
8666 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8667 self.focus_handle.clone()
8668 }
8669 }
8670
8671 impl Render for TestAlternatePngItemView {
8672 fn render(
8673 &mut self,
8674 _window: &mut Window,
8675 _cx: &mut Context<Self>,
8676 ) -> impl IntoElement {
8677 Empty
8678 }
8679 }
8680
8681 impl ProjectItem for TestAlternatePngItemView {
8682 type Item = TestPngItem;
8683
8684 fn for_project_item(
8685 _project: Entity<Project>,
8686 _item: Entity<Self::Item>,
8687 _: &mut Window,
8688 cx: &mut Context<Self>,
8689 ) -> Self
8690 where
8691 Self: Sized,
8692 {
8693 Self {
8694 focus_handle: cx.focus_handle(),
8695 }
8696 }
8697 }
8698
8699 #[gpui::test]
8700 async fn test_register_project_item(cx: &mut TestAppContext) {
8701 init_test(cx);
8702
8703 cx.update(|cx| {
8704 register_project_item::<TestPngItemView>(cx);
8705 register_project_item::<TestIpynbItemView>(cx);
8706 });
8707
8708 let fs = FakeFs::new(cx.executor());
8709 fs.insert_tree(
8710 "/root1",
8711 json!({
8712 "one.png": "BINARYDATAHERE",
8713 "two.ipynb": "{ totally a notebook }",
8714 "three.txt": "editing text, sure why not?"
8715 }),
8716 )
8717 .await;
8718
8719 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8720 let (workspace, cx) =
8721 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8722
8723 let worktree_id = project.update(cx, |project, cx| {
8724 project.worktrees(cx).next().unwrap().read(cx).id()
8725 });
8726
8727 let handle = workspace
8728 .update_in(cx, |workspace, window, cx| {
8729 let project_path = (worktree_id, "one.png");
8730 workspace.open_path(project_path, None, true, window, cx)
8731 })
8732 .await
8733 .unwrap();
8734
8735 // Now we can check if the handle we got back errored or not
8736 assert_eq!(
8737 handle.to_any().entity_type(),
8738 TypeId::of::<TestPngItemView>()
8739 );
8740
8741 let handle = workspace
8742 .update_in(cx, |workspace, window, cx| {
8743 let project_path = (worktree_id, "two.ipynb");
8744 workspace.open_path(project_path, None, true, window, cx)
8745 })
8746 .await
8747 .unwrap();
8748
8749 assert_eq!(
8750 handle.to_any().entity_type(),
8751 TypeId::of::<TestIpynbItemView>()
8752 );
8753
8754 let handle = workspace
8755 .update_in(cx, |workspace, window, cx| {
8756 let project_path = (worktree_id, "three.txt");
8757 workspace.open_path(project_path, None, true, window, cx)
8758 })
8759 .await;
8760 assert!(handle.is_err());
8761 }
8762
8763 #[gpui::test]
8764 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8765 init_test(cx);
8766
8767 cx.update(|cx| {
8768 register_project_item::<TestPngItemView>(cx);
8769 register_project_item::<TestAlternatePngItemView>(cx);
8770 });
8771
8772 let fs = FakeFs::new(cx.executor());
8773 fs.insert_tree(
8774 "/root1",
8775 json!({
8776 "one.png": "BINARYDATAHERE",
8777 "two.ipynb": "{ totally a notebook }",
8778 "three.txt": "editing text, sure why not?"
8779 }),
8780 )
8781 .await;
8782 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8783 let (workspace, cx) =
8784 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8785 let worktree_id = project.update(cx, |project, cx| {
8786 project.worktrees(cx).next().unwrap().read(cx).id()
8787 });
8788
8789 let handle = workspace
8790 .update_in(cx, |workspace, window, cx| {
8791 let project_path = (worktree_id, "one.png");
8792 workspace.open_path(project_path, None, true, window, cx)
8793 })
8794 .await
8795 .unwrap();
8796
8797 // This _must_ be the second item registered
8798 assert_eq!(
8799 handle.to_any().entity_type(),
8800 TypeId::of::<TestAlternatePngItemView>()
8801 );
8802
8803 let handle = workspace
8804 .update_in(cx, |workspace, window, cx| {
8805 let project_path = (worktree_id, "three.txt");
8806 workspace.open_path(project_path, None, true, window, cx)
8807 })
8808 .await;
8809 assert!(handle.is_err());
8810 }
8811 }
8812
8813 pub fn init_test(cx: &mut TestAppContext) {
8814 cx.update(|cx| {
8815 let settings_store = SettingsStore::test(cx);
8816 cx.set_global(settings_store);
8817 theme::init(theme::LoadThemes::JustBase, cx);
8818 language::init(cx);
8819 crate::init_settings(cx);
8820 Project::init_settings(cx);
8821 });
8822 }
8823
8824 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
8825 let item = TestProjectItem::new(id, path, cx);
8826 item.update(cx, |item, _| {
8827 item.is_dirty = true;
8828 });
8829 item
8830 }
8831}