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