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