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