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