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 if self.active_pane() == pane {
4446 self.panes
4447 .last()
4448 .unwrap()
4449 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4450 }
4451 }
4452 if self.last_active_center_pane == Some(pane.downgrade()) {
4453 self.last_active_center_pane = None;
4454 }
4455 cx.notify();
4456 }
4457
4458 fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4459 if self._schedule_serialize.is_none() {
4460 self._schedule_serialize = Some(cx.spawn_in(window, |this, mut cx| async move {
4461 cx.background_executor()
4462 .timer(Duration::from_millis(100))
4463 .await;
4464 this.update_in(&mut cx, |this, window, cx| {
4465 this.serialize_workspace_internal(window, cx).detach();
4466 this._schedule_serialize.take();
4467 })
4468 .log_err();
4469 }));
4470 }
4471 }
4472
4473 fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
4474 let Some(database_id) = self.database_id() else {
4475 return Task::ready(());
4476 };
4477
4478 fn serialize_pane_handle(
4479 pane_handle: &Entity<Pane>,
4480 window: &mut Window,
4481 cx: &mut App,
4482 ) -> SerializedPane {
4483 let (items, active, pinned_count) = {
4484 let pane = pane_handle.read(cx);
4485 let active_item_id = pane.active_item().map(|item| item.item_id());
4486 (
4487 pane.items()
4488 .filter_map(|handle| {
4489 let handle = handle.to_serializable_item_handle(cx)?;
4490
4491 Some(SerializedItem {
4492 kind: Arc::from(handle.serialized_item_kind()),
4493 item_id: handle.item_id().as_u64(),
4494 active: Some(handle.item_id()) == active_item_id,
4495 preview: pane.is_active_preview_item(handle.item_id()),
4496 })
4497 })
4498 .collect::<Vec<_>>(),
4499 pane.has_focus(window, cx),
4500 pane.pinned_count(),
4501 )
4502 };
4503
4504 SerializedPane::new(items, active, pinned_count)
4505 }
4506
4507 fn build_serialized_pane_group(
4508 pane_group: &Member,
4509 window: &mut Window,
4510 cx: &mut App,
4511 ) -> SerializedPaneGroup {
4512 match pane_group {
4513 Member::Axis(PaneAxis {
4514 axis,
4515 members,
4516 flexes,
4517 bounding_boxes: _,
4518 }) => SerializedPaneGroup::Group {
4519 axis: SerializedAxis(*axis),
4520 children: members
4521 .iter()
4522 .map(|member| build_serialized_pane_group(member, window, cx))
4523 .collect::<Vec<_>>(),
4524 flexes: Some(flexes.lock().clone()),
4525 },
4526 Member::Pane(pane_handle) => {
4527 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
4528 }
4529 }
4530 }
4531
4532 fn build_serialized_docks(
4533 this: &Workspace,
4534 window: &mut Window,
4535 cx: &mut App,
4536 ) -> DockStructure {
4537 let left_dock = this.left_dock.read(cx);
4538 let left_visible = left_dock.is_open();
4539 let left_active_panel = left_dock
4540 .active_panel()
4541 .map(|panel| panel.persistent_name().to_string());
4542 let left_dock_zoom = left_dock
4543 .active_panel()
4544 .map(|panel| panel.is_zoomed(window, cx))
4545 .unwrap_or(false);
4546
4547 let right_dock = this.right_dock.read(cx);
4548 let right_visible = right_dock.is_open();
4549 let right_active_panel = right_dock
4550 .active_panel()
4551 .map(|panel| panel.persistent_name().to_string());
4552 let right_dock_zoom = right_dock
4553 .active_panel()
4554 .map(|panel| panel.is_zoomed(window, cx))
4555 .unwrap_or(false);
4556
4557 let bottom_dock = this.bottom_dock.read(cx);
4558 let bottom_visible = bottom_dock.is_open();
4559 let bottom_active_panel = bottom_dock
4560 .active_panel()
4561 .map(|panel| panel.persistent_name().to_string());
4562 let bottom_dock_zoom = bottom_dock
4563 .active_panel()
4564 .map(|panel| panel.is_zoomed(window, cx))
4565 .unwrap_or(false);
4566
4567 DockStructure {
4568 left: DockData {
4569 visible: left_visible,
4570 active_panel: left_active_panel,
4571 zoom: left_dock_zoom,
4572 },
4573 right: DockData {
4574 visible: right_visible,
4575 active_panel: right_active_panel,
4576 zoom: right_dock_zoom,
4577 },
4578 bottom: DockData {
4579 visible: bottom_visible,
4580 active_panel: bottom_active_panel,
4581 zoom: bottom_dock_zoom,
4582 },
4583 }
4584 }
4585
4586 let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4587 Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4588 } else if let Some(local_paths) = self.local_paths(cx) {
4589 if !local_paths.is_empty() {
4590 Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4591 } else {
4592 None
4593 }
4594 } else {
4595 None
4596 };
4597
4598 if let Some(location) = location {
4599 let center_group = build_serialized_pane_group(&self.center.root, window, cx);
4600 let docks = build_serialized_docks(self, window, cx);
4601 let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
4602 let serialized_workspace = SerializedWorkspace {
4603 id: database_id,
4604 location,
4605 center_group,
4606 window_bounds,
4607 display: Default::default(),
4608 docks,
4609 centered_layout: self.centered_layout,
4610 session_id: self.session_id.clone(),
4611 window_id: Some(window.window_handle().window_id().as_u64()),
4612 };
4613 return window.spawn(cx, |_| persistence::DB.save_workspace(serialized_workspace));
4614 }
4615 Task::ready(())
4616 }
4617
4618 async fn serialize_items(
4619 this: &WeakEntity<Self>,
4620 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4621 cx: &mut AsyncWindowContext,
4622 ) -> Result<()> {
4623 const CHUNK_SIZE: usize = 200;
4624
4625 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4626
4627 while let Some(items_received) = serializable_items.next().await {
4628 let unique_items =
4629 items_received
4630 .into_iter()
4631 .fold(HashMap::default(), |mut acc, item| {
4632 acc.entry(item.item_id()).or_insert(item);
4633 acc
4634 });
4635
4636 // We use into_iter() here so that the references to the items are moved into
4637 // the tasks and not kept alive while we're sleeping.
4638 for (_, item) in unique_items.into_iter() {
4639 if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
4640 item.serialize(workspace, false, window, cx)
4641 }) {
4642 cx.background_executor()
4643 .spawn(async move { task.await.log_err() })
4644 .detach();
4645 }
4646 }
4647
4648 cx.background_executor()
4649 .timer(SERIALIZATION_THROTTLE_TIME)
4650 .await;
4651 }
4652
4653 Ok(())
4654 }
4655
4656 pub(crate) fn enqueue_item_serialization(
4657 &mut self,
4658 item: Box<dyn SerializableItemHandle>,
4659 ) -> Result<()> {
4660 self.serializable_items_tx
4661 .unbounded_send(item)
4662 .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4663 }
4664
4665 pub(crate) fn load_workspace(
4666 serialized_workspace: SerializedWorkspace,
4667 paths_to_open: Vec<Option<ProjectPath>>,
4668 window: &mut Window,
4669 cx: &mut Context<Workspace>,
4670 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4671 cx.spawn_in(window, |workspace, mut cx| async move {
4672 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4673
4674 let mut center_group = None;
4675 let mut center_items = None;
4676
4677 // Traverse the splits tree and add to things
4678 if let Some((group, active_pane, items)) = serialized_workspace
4679 .center_group
4680 .deserialize(
4681 &project,
4682 serialized_workspace.id,
4683 workspace.clone(),
4684 &mut cx,
4685 )
4686 .await
4687 {
4688 center_items = Some(items);
4689 center_group = Some((group, active_pane))
4690 }
4691
4692 let mut items_by_project_path = HashMap::default();
4693 let mut item_ids_by_kind = HashMap::default();
4694 let mut all_deserialized_items = Vec::default();
4695 cx.update(|_, cx| {
4696 for item in center_items.unwrap_or_default().into_iter().flatten() {
4697 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4698 item_ids_by_kind
4699 .entry(serializable_item_handle.serialized_item_kind())
4700 .or_insert(Vec::new())
4701 .push(item.item_id().as_u64() as ItemId);
4702 }
4703
4704 if let Some(project_path) = item.project_path(cx) {
4705 items_by_project_path.insert(project_path, item.clone());
4706 }
4707 all_deserialized_items.push(item);
4708 }
4709 })?;
4710
4711 let opened_items = paths_to_open
4712 .into_iter()
4713 .map(|path_to_open| {
4714 path_to_open
4715 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4716 })
4717 .collect::<Vec<_>>();
4718
4719 // Remove old panes from workspace panes list
4720 workspace.update_in(&mut cx, |workspace, window, cx| {
4721 if let Some((center_group, active_pane)) = center_group {
4722 workspace.remove_panes(workspace.center.root.clone(), window, cx);
4723
4724 // Swap workspace center group
4725 workspace.center = PaneGroup::with_root(center_group);
4726 if let Some(active_pane) = active_pane {
4727 workspace.set_active_pane(&active_pane, window, cx);
4728 cx.focus_self(window);
4729 } else {
4730 workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
4731 }
4732 }
4733
4734 let docks = serialized_workspace.docks;
4735
4736 for (dock, serialized_dock) in [
4737 (&mut workspace.right_dock, docks.right),
4738 (&mut workspace.left_dock, docks.left),
4739 (&mut workspace.bottom_dock, docks.bottom),
4740 ]
4741 .iter_mut()
4742 {
4743 dock.update(cx, |dock, cx| {
4744 dock.serialized_dock = Some(serialized_dock.clone());
4745 dock.restore_state(window, cx);
4746 });
4747 }
4748
4749 cx.notify();
4750 })?;
4751
4752 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4753 // after loading the items, we might have different items and in order to avoid
4754 // the database filling up, we delete items that haven't been loaded now.
4755 //
4756 // The items that have been loaded, have been saved after they've been added to the workspace.
4757 let clean_up_tasks = workspace.update_in(&mut cx, |_, window, cx| {
4758 item_ids_by_kind
4759 .into_iter()
4760 .map(|(item_kind, loaded_items)| {
4761 SerializableItemRegistry::cleanup(
4762 item_kind,
4763 serialized_workspace.id,
4764 loaded_items,
4765 window,
4766 cx,
4767 )
4768 .log_err()
4769 })
4770 .collect::<Vec<_>>()
4771 })?;
4772
4773 futures::future::join_all(clean_up_tasks).await;
4774
4775 workspace
4776 .update_in(&mut cx, |workspace, window, cx| {
4777 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4778 workspace.serialize_workspace_internal(window, cx).detach();
4779
4780 // Ensure that we mark the window as edited if we did load dirty items
4781 workspace.update_window_edited(window, cx);
4782 })
4783 .ok();
4784
4785 Ok(opened_items)
4786 })
4787 }
4788
4789 fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
4790 self.add_workspace_actions_listeners(div, window, cx)
4791 .on_action(cx.listener(Self::close_inactive_items_and_panes))
4792 .on_action(cx.listener(Self::close_all_items_and_panes))
4793 .on_action(cx.listener(Self::save_all))
4794 .on_action(cx.listener(Self::send_keystrokes))
4795 .on_action(cx.listener(Self::add_folder_to_project))
4796 .on_action(cx.listener(Self::follow_next_collaborator))
4797 .on_action(cx.listener(Self::close_window))
4798 .on_action(cx.listener(Self::activate_pane_at_index))
4799 .on_action(cx.listener(Self::move_item_to_pane_at_index))
4800 .on_action(cx.listener(Self::move_focused_panel_to_next_position))
4801 .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
4802 let pane = workspace.active_pane().clone();
4803 workspace.unfollow_in_pane(&pane, window, cx);
4804 }))
4805 .on_action(cx.listener(|workspace, action: &Save, window, cx| {
4806 workspace
4807 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
4808 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4809 }))
4810 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
4811 workspace
4812 .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
4813 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4814 }))
4815 .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
4816 workspace
4817 .save_active_item(SaveIntent::SaveAs, window, cx)
4818 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4819 }))
4820 .on_action(
4821 cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
4822 workspace.activate_previous_pane(window, cx)
4823 }),
4824 )
4825 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4826 workspace.activate_next_pane(window, cx)
4827 }))
4828 .on_action(
4829 cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
4830 workspace.activate_next_window(cx)
4831 }),
4832 )
4833 .on_action(
4834 cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
4835 workspace.activate_previous_window(cx)
4836 }),
4837 )
4838 .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
4839 workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
4840 }))
4841 .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
4842 workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
4843 }))
4844 .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
4845 workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
4846 }))
4847 .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
4848 workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
4849 }))
4850 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4851 workspace.activate_next_pane(window, cx)
4852 }))
4853 .on_action(cx.listener(
4854 |workspace, action: &MoveItemToPaneInDirection, window, cx| {
4855 workspace.move_item_to_pane_in_direction(action, window, cx)
4856 },
4857 ))
4858 .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
4859 workspace.swap_pane_in_direction(SplitDirection::Left, cx)
4860 }))
4861 .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
4862 workspace.swap_pane_in_direction(SplitDirection::Right, cx)
4863 }))
4864 .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
4865 workspace.swap_pane_in_direction(SplitDirection::Up, cx)
4866 }))
4867 .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
4868 workspace.swap_pane_in_direction(SplitDirection::Down, cx)
4869 }))
4870 .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
4871 this.toggle_dock(DockPosition::Left, window, cx);
4872 }))
4873 .on_action(cx.listener(
4874 |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
4875 workspace.toggle_dock(DockPosition::Right, window, cx);
4876 },
4877 ))
4878 .on_action(cx.listener(
4879 |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
4880 workspace.toggle_dock(DockPosition::Bottom, window, cx);
4881 },
4882 ))
4883 .on_action(
4884 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
4885 workspace.close_all_docks(window, cx);
4886 }),
4887 )
4888 .on_action(cx.listener(
4889 |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
4890 workspace.clear_all_notifications(cx);
4891 },
4892 ))
4893 .on_action(cx.listener(
4894 |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
4895 workspace.reopen_closed_item(window, cx).detach();
4896 },
4897 ))
4898 .on_action(cx.listener(Workspace::toggle_centered_layout))
4899 }
4900
4901 #[cfg(any(test, feature = "test-support"))]
4902 pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
4903 use node_runtime::NodeRuntime;
4904 use session::Session;
4905
4906 let client = project.read(cx).client();
4907 let user_store = project.read(cx).user_store();
4908
4909 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
4910 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
4911 window.activate_window();
4912 let app_state = Arc::new(AppState {
4913 languages: project.read(cx).languages().clone(),
4914 workspace_store,
4915 client,
4916 user_store,
4917 fs: project.read(cx).fs().clone(),
4918 build_window_options: |_, _| Default::default(),
4919 node_runtime: NodeRuntime::unavailable(),
4920 session,
4921 });
4922 let workspace = Self::new(Default::default(), project, app_state, window, cx);
4923 workspace
4924 .active_pane
4925 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4926 workspace
4927 }
4928
4929 pub fn register_action<A: Action>(
4930 &mut self,
4931 callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
4932 ) -> &mut Self {
4933 let callback = Arc::new(callback);
4934
4935 self.workspace_actions.push(Box::new(move |div, _, cx| {
4936 let callback = callback.clone();
4937 div.on_action(cx.listener(move |workspace, event, window, cx| {
4938 (callback.clone())(workspace, event, window, cx)
4939 }))
4940 }));
4941 self
4942 }
4943
4944 fn add_workspace_actions_listeners(
4945 &self,
4946 mut div: Div,
4947 window: &mut Window,
4948 cx: &mut Context<Self>,
4949 ) -> Div {
4950 for action in self.workspace_actions.iter() {
4951 div = (action)(div, window, cx)
4952 }
4953 div
4954 }
4955
4956 pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
4957 self.modal_layer.read(cx).has_active_modal()
4958 }
4959
4960 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
4961 self.modal_layer.read(cx).active_modal()
4962 }
4963
4964 pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
4965 where
4966 B: FnOnce(&mut Window, &mut Context<V>) -> V,
4967 {
4968 self.modal_layer.update(cx, |modal_layer, cx| {
4969 modal_layer.toggle_modal(window, cx, build)
4970 })
4971 }
4972
4973 pub fn toggle_centered_layout(
4974 &mut self,
4975 _: &ToggleCenteredLayout,
4976 _: &mut Window,
4977 cx: &mut Context<Self>,
4978 ) {
4979 self.centered_layout = !self.centered_layout;
4980 if let Some(database_id) = self.database_id() {
4981 cx.background_executor()
4982 .spawn(DB.set_centered_layout(database_id, self.centered_layout))
4983 .detach_and_log_err(cx);
4984 }
4985 cx.notify();
4986 }
4987
4988 fn adjust_padding(padding: Option<f32>) -> f32 {
4989 padding
4990 .unwrap_or(Self::DEFAULT_PADDING)
4991 .clamp(0.0, Self::MAX_PADDING)
4992 }
4993
4994 fn render_dock(
4995 &self,
4996 position: DockPosition,
4997 dock: &Entity<Dock>,
4998 window: &mut Window,
4999 cx: &mut App,
5000 ) -> Option<Div> {
5001 if self.zoomed_position == Some(position) {
5002 return None;
5003 }
5004
5005 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
5006 let pane = panel.pane(cx)?;
5007 let follower_states = &self.follower_states;
5008 leader_border_for_pane(follower_states, &pane, window, cx)
5009 });
5010
5011 Some(
5012 div()
5013 .flex()
5014 .flex_none()
5015 .overflow_hidden()
5016 .child(dock.clone())
5017 .children(leader_border),
5018 )
5019 }
5020
5021 pub fn for_window(window: &mut Window, _: &mut App) -> Option<Entity<Workspace>> {
5022 window.root().flatten()
5023 }
5024
5025 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
5026 self.zoomed.as_ref()
5027 }
5028
5029 pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
5030 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5031 return;
5032 };
5033 let windows = cx.windows();
5034 let Some(next_window) = windows
5035 .iter()
5036 .cycle()
5037 .skip_while(|window| window.window_id() != current_window_id)
5038 .nth(1)
5039 else {
5040 return;
5041 };
5042 next_window
5043 .update(cx, |_, window, _| window.activate_window())
5044 .ok();
5045 }
5046
5047 pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
5048 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5049 return;
5050 };
5051 let windows = cx.windows();
5052 let Some(prev_window) = windows
5053 .iter()
5054 .rev()
5055 .cycle()
5056 .skip_while(|window| window.window_id() != current_window_id)
5057 .nth(1)
5058 else {
5059 return;
5060 };
5061 prev_window
5062 .update(cx, |_, window, _| window.activate_window())
5063 .ok();
5064 }
5065}
5066
5067fn leader_border_for_pane(
5068 follower_states: &HashMap<PeerId, FollowerState>,
5069 pane: &Entity<Pane>,
5070 _: &Window,
5071 cx: &App,
5072) -> Option<Div> {
5073 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
5074 if state.pane() == pane {
5075 Some((*leader_id, state))
5076 } else {
5077 None
5078 }
5079 })?;
5080
5081 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
5082 let leader = room.remote_participant_for_peer_id(leader_id)?;
5083
5084 let mut leader_color = cx
5085 .theme()
5086 .players()
5087 .color_for_participant(leader.participant_index.0)
5088 .cursor;
5089 leader_color.fade_out(0.3);
5090 Some(
5091 div()
5092 .absolute()
5093 .size_full()
5094 .left_0()
5095 .top_0()
5096 .border_2()
5097 .border_color(leader_color),
5098 )
5099}
5100
5101fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
5102 ZED_WINDOW_POSITION
5103 .zip(*ZED_WINDOW_SIZE)
5104 .map(|(position, size)| Bounds {
5105 origin: position,
5106 size,
5107 })
5108}
5109
5110fn open_items(
5111 serialized_workspace: Option<SerializedWorkspace>,
5112 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
5113 window: &mut Window,
5114 cx: &mut Context<Workspace>,
5115) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
5116 let restored_items = serialized_workspace.map(|serialized_workspace| {
5117 Workspace::load_workspace(
5118 serialized_workspace,
5119 project_paths_to_open
5120 .iter()
5121 .map(|(_, project_path)| project_path)
5122 .cloned()
5123 .collect(),
5124 window,
5125 cx,
5126 )
5127 });
5128
5129 cx.spawn_in(window, |workspace, mut cx| async move {
5130 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
5131
5132 if let Some(restored_items) = restored_items {
5133 let restored_items = restored_items.await?;
5134
5135 let restored_project_paths = restored_items
5136 .iter()
5137 .filter_map(|item| {
5138 cx.update(|_, cx| item.as_ref()?.project_path(cx))
5139 .ok()
5140 .flatten()
5141 })
5142 .collect::<HashSet<_>>();
5143
5144 for restored_item in restored_items {
5145 opened_items.push(restored_item.map(Ok));
5146 }
5147
5148 project_paths_to_open
5149 .iter_mut()
5150 .for_each(|(_, project_path)| {
5151 if let Some(project_path_to_open) = project_path {
5152 if restored_project_paths.contains(project_path_to_open) {
5153 *project_path = None;
5154 }
5155 }
5156 });
5157 } else {
5158 for _ in 0..project_paths_to_open.len() {
5159 opened_items.push(None);
5160 }
5161 }
5162 assert!(opened_items.len() == project_paths_to_open.len());
5163
5164 let tasks =
5165 project_paths_to_open
5166 .into_iter()
5167 .enumerate()
5168 .map(|(ix, (abs_path, project_path))| {
5169 let workspace = workspace.clone();
5170 cx.spawn(|mut cx| async move {
5171 let file_project_path = project_path?;
5172 let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
5173 workspace.project().update(cx, |project, cx| {
5174 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
5175 })
5176 });
5177
5178 // We only want to open file paths here. If one of the items
5179 // here is a directory, it was already opened further above
5180 // with a `find_or_create_worktree`.
5181 if let Ok(task) = abs_path_task {
5182 if task.await.map_or(true, |p| p.is_file()) {
5183 return Some((
5184 ix,
5185 workspace
5186 .update_in(&mut cx, |workspace, window, cx| {
5187 workspace.open_path(
5188 file_project_path,
5189 None,
5190 true,
5191 window,
5192 cx,
5193 )
5194 })
5195 .log_err()?
5196 .await,
5197 ));
5198 }
5199 }
5200 None
5201 })
5202 });
5203
5204 let tasks = tasks.collect::<Vec<_>>();
5205
5206 let tasks = futures::future::join_all(tasks);
5207 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
5208 opened_items[ix] = Some(path_open_result);
5209 }
5210
5211 Ok(opened_items)
5212 })
5213}
5214
5215enum ActivateInDirectionTarget {
5216 Pane(Entity<Pane>),
5217 Dock(Entity<Dock>),
5218}
5219
5220fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
5221 const REPORT_ISSUE_URL: &str = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=admin+read%2Ctriage%2Cbug&projects=&template=1_bug_report.yml";
5222
5223 workspace
5224 .update(cx, |workspace, _, cx| {
5225 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
5226 struct DatabaseFailedNotification;
5227
5228 workspace.show_notification(
5229 NotificationId::unique::<DatabaseFailedNotification>(),
5230 cx,
5231 |cx| {
5232 cx.new(|_| {
5233 MessageNotification::new("Failed to load the database file.")
5234 .primary_message("File an Issue")
5235 .primary_icon(IconName::Plus)
5236 .primary_on_click(|_window, cx| cx.open_url(REPORT_ISSUE_URL))
5237 })
5238 },
5239 );
5240 }
5241 })
5242 .log_err();
5243}
5244
5245impl Focusable for Workspace {
5246 fn focus_handle(&self, cx: &App) -> FocusHandle {
5247 self.active_pane.focus_handle(cx)
5248 }
5249}
5250
5251#[derive(Clone)]
5252struct DraggedDock(DockPosition);
5253
5254impl Render for DraggedDock {
5255 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5256 gpui::Empty
5257 }
5258}
5259
5260impl Render for Workspace {
5261 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5262 let mut context = KeyContext::new_with_defaults();
5263 context.add("Workspace");
5264 context.set("keyboard_layout", cx.keyboard_layout().clone());
5265 let centered_layout = self.centered_layout
5266 && self.center.panes().len() == 1
5267 && self.active_item(cx).is_some();
5268 let render_padding = |size| {
5269 (size > 0.0).then(|| {
5270 div()
5271 .h_full()
5272 .w(relative(size))
5273 .bg(cx.theme().colors().editor_background)
5274 .border_color(cx.theme().colors().pane_group_border)
5275 })
5276 };
5277 let paddings = if centered_layout {
5278 let settings = WorkspaceSettings::get_global(cx).centered_layout;
5279 (
5280 render_padding(Self::adjust_padding(settings.left_padding)),
5281 render_padding(Self::adjust_padding(settings.right_padding)),
5282 )
5283 } else {
5284 (None, None)
5285 };
5286 let ui_font = theme::setup_ui_font(window, cx);
5287
5288 let theme = cx.theme().clone();
5289 let colors = theme.colors();
5290
5291 client_side_decorations(
5292 self.actions(div(), window, cx)
5293 .key_context(context)
5294 .relative()
5295 .size_full()
5296 .flex()
5297 .flex_col()
5298 .font(ui_font)
5299 .gap_0()
5300 .justify_start()
5301 .items_start()
5302 .text_color(colors.text)
5303 .overflow_hidden()
5304 .children(self.titlebar_item.clone())
5305 .child(
5306 div()
5307 .size_full()
5308 .relative()
5309 .flex_1()
5310 .flex()
5311 .flex_col()
5312 .child(
5313 div()
5314 .id("workspace")
5315 .bg(colors.background)
5316 .relative()
5317 .flex_1()
5318 .w_full()
5319 .flex()
5320 .flex_col()
5321 .overflow_hidden()
5322 .border_t_1()
5323 .border_b_1()
5324 .border_color(colors.border)
5325 .child({
5326 let this = cx.entity().clone();
5327 canvas(
5328 move |bounds, window, cx| {
5329 this.update(cx, |this, cx| {
5330 let bounds_changed = this.bounds != bounds;
5331 this.bounds = bounds;
5332
5333 if bounds_changed {
5334 this.left_dock.update(cx, |dock, cx| {
5335 dock.clamp_panel_size(
5336 bounds.size.width,
5337 window,
5338 cx,
5339 )
5340 });
5341
5342 this.right_dock.update(cx, |dock, cx| {
5343 dock.clamp_panel_size(
5344 bounds.size.width,
5345 window,
5346 cx,
5347 )
5348 });
5349
5350 this.bottom_dock.update(cx, |dock, cx| {
5351 dock.clamp_panel_size(
5352 bounds.size.height,
5353 window,
5354 cx,
5355 )
5356 });
5357 }
5358 })
5359 },
5360 |_, _, _, _| {},
5361 )
5362 .absolute()
5363 .size_full()
5364 })
5365 .when(self.zoomed.is_none(), |this| {
5366 this.on_drag_move(cx.listener(
5367 move |workspace,
5368 e: &DragMoveEvent<DraggedDock>,
5369 window,
5370 cx| {
5371 if workspace.previous_dock_drag_coordinates
5372 != Some(e.event.position)
5373 {
5374 workspace.previous_dock_drag_coordinates =
5375 Some(e.event.position);
5376 match e.drag(cx).0 {
5377 DockPosition::Left => {
5378 resize_left_dock(
5379 e.event.position.x
5380 - workspace.bounds.left(),
5381 workspace,
5382 window,
5383 cx,
5384 );
5385 }
5386 DockPosition::Right => {
5387 resize_right_dock(
5388 workspace.bounds.right()
5389 - e.event.position.x,
5390 workspace,
5391 window,
5392 cx,
5393 );
5394 }
5395 DockPosition::Bottom => {
5396 resize_bottom_dock(
5397 workspace.bounds.bottom()
5398 - e.event.position.y,
5399 workspace,
5400 window,
5401 cx,
5402 );
5403 }
5404 };
5405 workspace.serialize_workspace(window, cx);
5406 }
5407 },
5408 ))
5409 })
5410 .child(
5411 div()
5412 .flex()
5413 .flex_row()
5414 .h_full()
5415 // Left Dock
5416 .children(self.render_dock(
5417 DockPosition::Left,
5418 &self.left_dock,
5419 window,
5420 cx,
5421 ))
5422 // Panes
5423 .child(
5424 div()
5425 .flex()
5426 .flex_col()
5427 .flex_1()
5428 .overflow_hidden()
5429 .child(
5430 h_flex()
5431 .flex_1()
5432 .when_some(paddings.0, |this, p| {
5433 this.child(p.border_r_1())
5434 })
5435 .child(self.center.render(
5436 &self.project,
5437 &self.follower_states,
5438 self.active_call(),
5439 &self.active_pane,
5440 self.zoomed.as_ref(),
5441 &self.app_state,
5442 window,
5443 cx,
5444 ))
5445 .when_some(paddings.1, |this, p| {
5446 this.child(p.border_l_1())
5447 }),
5448 )
5449 .children(self.render_dock(
5450 DockPosition::Bottom,
5451 &self.bottom_dock,
5452 window,
5453 cx,
5454 )),
5455 )
5456 // Right Dock
5457 .children(self.render_dock(
5458 DockPosition::Right,
5459 &self.right_dock,
5460 window,
5461 cx,
5462 )),
5463 )
5464 .children(self.zoomed.as_ref().and_then(|view| {
5465 let zoomed_view = view.upgrade()?;
5466 let div = div()
5467 .occlude()
5468 .absolute()
5469 .overflow_hidden()
5470 .border_color(colors.border)
5471 .bg(colors.background)
5472 .child(zoomed_view)
5473 .inset_0()
5474 .shadow_lg();
5475
5476 Some(match self.zoomed_position {
5477 Some(DockPosition::Left) => div.right_2().border_r_1(),
5478 Some(DockPosition::Right) => div.left_2().border_l_1(),
5479 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
5480 None => {
5481 div.top_2().bottom_2().left_2().right_2().border_1()
5482 }
5483 })
5484 }))
5485 .children(self.render_notifications(window, cx)),
5486 )
5487 .child(self.status_bar.clone())
5488 .child(self.modal_layer.clone()),
5489 ),
5490 window,
5491 cx,
5492 )
5493 }
5494}
5495
5496fn resize_bottom_dock(
5497 new_size: Pixels,
5498 workspace: &mut Workspace,
5499 window: &mut Window,
5500 cx: &mut App,
5501) {
5502 let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
5503 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
5504 bottom_dock.resize_active_panel(Some(size), window, cx);
5505 });
5506}
5507
5508fn resize_right_dock(
5509 new_size: Pixels,
5510 workspace: &mut Workspace,
5511 window: &mut Window,
5512 cx: &mut App,
5513) {
5514 let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
5515 workspace.right_dock.update(cx, |right_dock, cx| {
5516 right_dock.resize_active_panel(Some(size), window, cx);
5517 });
5518}
5519
5520fn resize_left_dock(
5521 new_size: Pixels,
5522 workspace: &mut Workspace,
5523 window: &mut Window,
5524 cx: &mut App,
5525) {
5526 let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5527
5528 workspace.left_dock.update(cx, |left_dock, cx| {
5529 left_dock.resize_active_panel(Some(size), window, cx);
5530 });
5531}
5532
5533impl WorkspaceStore {
5534 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
5535 Self {
5536 workspaces: Default::default(),
5537 _subscriptions: vec![
5538 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
5539 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
5540 ],
5541 client,
5542 }
5543 }
5544
5545 pub fn update_followers(
5546 &self,
5547 project_id: Option<u64>,
5548 update: proto::update_followers::Variant,
5549 cx: &App,
5550 ) -> Option<()> {
5551 let active_call = ActiveCall::try_global(cx)?;
5552 let room_id = active_call.read(cx).room()?.read(cx).id();
5553 self.client
5554 .send(proto::UpdateFollowers {
5555 room_id,
5556 project_id,
5557 variant: Some(update),
5558 })
5559 .log_err()
5560 }
5561
5562 pub async fn handle_follow(
5563 this: Entity<Self>,
5564 envelope: TypedEnvelope<proto::Follow>,
5565 mut cx: AsyncApp,
5566 ) -> Result<proto::FollowResponse> {
5567 this.update(&mut cx, |this, cx| {
5568 let follower = Follower {
5569 project_id: envelope.payload.project_id,
5570 peer_id: envelope.original_sender_id()?,
5571 };
5572
5573 let mut response = proto::FollowResponse::default();
5574 this.workspaces.retain(|workspace| {
5575 workspace
5576 .update(cx, |workspace, window, cx| {
5577 let handler_response =
5578 workspace.handle_follow(follower.project_id, window, cx);
5579 if let Some(active_view) = handler_response.active_view.clone() {
5580 if workspace.project.read(cx).remote_id() == follower.project_id {
5581 response.active_view = Some(active_view)
5582 }
5583 }
5584 })
5585 .is_ok()
5586 });
5587
5588 Ok(response)
5589 })?
5590 }
5591
5592 async fn handle_update_followers(
5593 this: Entity<Self>,
5594 envelope: TypedEnvelope<proto::UpdateFollowers>,
5595 mut cx: AsyncApp,
5596 ) -> Result<()> {
5597 let leader_id = envelope.original_sender_id()?;
5598 let update = envelope.payload;
5599
5600 this.update(&mut cx, |this, cx| {
5601 this.workspaces.retain(|workspace| {
5602 workspace
5603 .update(cx, |workspace, window, cx| {
5604 let project_id = workspace.project.read(cx).remote_id();
5605 if update.project_id != project_id && update.project_id.is_some() {
5606 return;
5607 }
5608 workspace.handle_update_followers(leader_id, update.clone(), window, cx);
5609 })
5610 .is_ok()
5611 });
5612 Ok(())
5613 })?
5614 }
5615}
5616
5617impl ViewId {
5618 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5619 Ok(Self {
5620 creator: message
5621 .creator
5622 .ok_or_else(|| anyhow!("creator is missing"))?,
5623 id: message.id,
5624 })
5625 }
5626
5627 pub(crate) fn to_proto(self) -> proto::ViewId {
5628 proto::ViewId {
5629 creator: Some(self.creator),
5630 id: self.id,
5631 }
5632 }
5633}
5634
5635impl FollowerState {
5636 fn pane(&self) -> &Entity<Pane> {
5637 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5638 }
5639}
5640
5641pub trait WorkspaceHandle {
5642 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
5643}
5644
5645impl WorkspaceHandle for Entity<Workspace> {
5646 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
5647 self.read(cx)
5648 .worktrees(cx)
5649 .flat_map(|worktree| {
5650 let worktree_id = worktree.read(cx).id();
5651 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5652 worktree_id,
5653 path: f.path.clone(),
5654 })
5655 })
5656 .collect::<Vec<_>>()
5657 }
5658}
5659
5660impl std::fmt::Debug for OpenPaths {
5661 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5662 f.debug_struct("OpenPaths")
5663 .field("paths", &self.paths)
5664 .finish()
5665 }
5666}
5667
5668pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5669 DB.last_workspace().await.log_err().flatten()
5670}
5671
5672pub fn last_session_workspace_locations(
5673 last_session_id: &str,
5674 last_session_window_stack: Option<Vec<WindowId>>,
5675) -> Option<Vec<SerializedWorkspaceLocation>> {
5676 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5677 .log_err()
5678}
5679
5680actions!(collab, [OpenChannelNotes]);
5681actions!(zed, [OpenLog]);
5682
5683async fn join_channel_internal(
5684 channel_id: ChannelId,
5685 app_state: &Arc<AppState>,
5686 requesting_window: Option<WindowHandle<Workspace>>,
5687 active_call: &Entity<ActiveCall>,
5688 cx: &mut AsyncApp,
5689) -> Result<bool> {
5690 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5691 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5692 return (false, None);
5693 };
5694
5695 let already_in_channel = room.channel_id() == Some(channel_id);
5696 let should_prompt = room.is_sharing_project()
5697 && !room.remote_participants().is_empty()
5698 && !already_in_channel;
5699 let open_room = if already_in_channel {
5700 active_call.room().cloned()
5701 } else {
5702 None
5703 };
5704 (should_prompt, open_room)
5705 })?;
5706
5707 if let Some(room) = open_room {
5708 let task = room.update(cx, |room, cx| {
5709 if let Some((project, host)) = room.most_active_project(cx) {
5710 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5711 }
5712
5713 None
5714 })?;
5715 if let Some(task) = task {
5716 task.await?;
5717 }
5718 return anyhow::Ok(true);
5719 }
5720
5721 if should_prompt {
5722 if let Some(workspace) = requesting_window {
5723 let answer = workspace
5724 .update(cx, |_, window, cx| {
5725 window.prompt(
5726 PromptLevel::Warning,
5727 "Do you want to switch channels?",
5728 Some("Leaving this call will unshare your current project."),
5729 &["Yes, Join Channel", "Cancel"],
5730 cx,
5731 )
5732 })?
5733 .await;
5734
5735 if answer == Ok(1) {
5736 return Ok(false);
5737 }
5738 } else {
5739 return Ok(false); // unreachable!() hopefully
5740 }
5741 }
5742
5743 let client = cx.update(|cx| active_call.read(cx).client())?;
5744
5745 let mut client_status = client.status();
5746
5747 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5748 'outer: loop {
5749 let Some(status) = client_status.recv().await else {
5750 return Err(anyhow!("error connecting"));
5751 };
5752
5753 match status {
5754 Status::Connecting
5755 | Status::Authenticating
5756 | Status::Reconnecting
5757 | Status::Reauthenticating => continue,
5758 Status::Connected { .. } => break 'outer,
5759 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5760 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5761 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5762 return Err(ErrorCode::Disconnected.into());
5763 }
5764 }
5765 }
5766
5767 let room = active_call
5768 .update(cx, |active_call, cx| {
5769 active_call.join_channel(channel_id, cx)
5770 })?
5771 .await?;
5772
5773 let Some(room) = room else {
5774 return anyhow::Ok(true);
5775 };
5776
5777 room.update(cx, |room, _| room.room_update_completed())?
5778 .await;
5779
5780 let task = room.update(cx, |room, cx| {
5781 if let Some((project, host)) = room.most_active_project(cx) {
5782 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5783 }
5784
5785 // If you are the first to join a channel, see if you should share your project.
5786 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5787 if let Some(workspace) = requesting_window {
5788 let project = workspace.update(cx, |workspace, _, cx| {
5789 let project = workspace.project.read(cx);
5790
5791 if !CallSettings::get_global(cx).share_on_join {
5792 return None;
5793 }
5794
5795 if (project.is_local() || project.is_via_ssh())
5796 && project.visible_worktrees(cx).any(|tree| {
5797 tree.read(cx)
5798 .root_entry()
5799 .map_or(false, |entry| entry.is_dir())
5800 })
5801 {
5802 Some(workspace.project.clone())
5803 } else {
5804 None
5805 }
5806 });
5807 if let Ok(Some(project)) = project {
5808 return Some(cx.spawn(|room, mut cx| async move {
5809 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5810 .await?;
5811 Ok(())
5812 }));
5813 }
5814 }
5815 }
5816
5817 None
5818 })?;
5819 if let Some(task) = task {
5820 task.await?;
5821 return anyhow::Ok(true);
5822 }
5823 anyhow::Ok(false)
5824}
5825
5826pub fn join_channel(
5827 channel_id: ChannelId,
5828 app_state: Arc<AppState>,
5829 requesting_window: Option<WindowHandle<Workspace>>,
5830 cx: &mut App,
5831) -> Task<Result<()>> {
5832 let active_call = ActiveCall::global(cx);
5833 cx.spawn(|mut cx| async move {
5834 let result = join_channel_internal(
5835 channel_id,
5836 &app_state,
5837 requesting_window,
5838 &active_call,
5839 &mut cx,
5840 )
5841 .await;
5842
5843 // join channel succeeded, and opened a window
5844 if matches!(result, Ok(true)) {
5845 return anyhow::Ok(());
5846 }
5847
5848 // find an existing workspace to focus and show call controls
5849 let mut active_window =
5850 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5851 if active_window.is_none() {
5852 // no open workspaces, make one to show the error in (blergh)
5853 let (window_handle, _) = cx
5854 .update(|cx| {
5855 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5856 })?
5857 .await?;
5858
5859 if result.is_ok() {
5860 cx.update(|cx| {
5861 cx.dispatch_action(&OpenChannelNotes);
5862 }).log_err();
5863 }
5864
5865 active_window = Some(window_handle);
5866 }
5867
5868 if let Err(err) = result {
5869 log::error!("failed to join channel: {}", err);
5870 if let Some(active_window) = active_window {
5871 active_window
5872 .update(&mut cx, |_, window, cx| {
5873 let detail: SharedString = match err.error_code() {
5874 ErrorCode::SignedOut => {
5875 "Please sign in to continue.".into()
5876 }
5877 ErrorCode::UpgradeRequired => {
5878 "Your are running an unsupported version of Zed. Please update to continue.".into()
5879 }
5880 ErrorCode::NoSuchChannel => {
5881 "No matching channel was found. Please check the link and try again.".into()
5882 }
5883 ErrorCode::Forbidden => {
5884 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5885 }
5886 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5887 _ => format!("{}\n\nPlease try again.", err).into(),
5888 };
5889 window.prompt(
5890 PromptLevel::Critical,
5891 "Failed to join channel",
5892 Some(&detail),
5893 &["Ok"],
5894 cx)
5895 })?
5896 .await
5897 .ok();
5898 }
5899 }
5900
5901 // return ok, we showed the error to the user.
5902 anyhow::Ok(())
5903 })
5904}
5905
5906pub async fn get_any_active_workspace(
5907 app_state: Arc<AppState>,
5908 mut cx: AsyncApp,
5909) -> anyhow::Result<WindowHandle<Workspace>> {
5910 // find an existing workspace to focus and show call controls
5911 let active_window = activate_any_workspace_window(&mut cx);
5912 if active_window.is_none() {
5913 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5914 .await?;
5915 }
5916 activate_any_workspace_window(&mut cx).context("could not open zed")
5917}
5918
5919fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
5920 cx.update(|cx| {
5921 if let Some(workspace_window) = cx
5922 .active_window()
5923 .and_then(|window| window.downcast::<Workspace>())
5924 {
5925 return Some(workspace_window);
5926 }
5927
5928 for window in cx.windows() {
5929 if let Some(workspace_window) = window.downcast::<Workspace>() {
5930 workspace_window
5931 .update(cx, |_, window, _| window.activate_window())
5932 .ok();
5933 return Some(workspace_window);
5934 }
5935 }
5936 None
5937 })
5938 .ok()
5939 .flatten()
5940}
5941
5942pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
5943 cx.windows()
5944 .into_iter()
5945 .filter_map(|window| window.downcast::<Workspace>())
5946 .filter(|workspace| {
5947 workspace
5948 .read(cx)
5949 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5950 })
5951 .collect()
5952}
5953
5954#[derive(Default)]
5955pub struct OpenOptions {
5956 pub open_new_workspace: Option<bool>,
5957 pub replace_window: Option<WindowHandle<Workspace>>,
5958 pub env: Option<HashMap<String, String>>,
5959}
5960
5961#[allow(clippy::type_complexity)]
5962pub fn open_paths(
5963 abs_paths: &[PathBuf],
5964 app_state: Arc<AppState>,
5965 open_options: OpenOptions,
5966 cx: &mut App,
5967) -> Task<
5968 anyhow::Result<(
5969 WindowHandle<Workspace>,
5970 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5971 )>,
5972> {
5973 let abs_paths = abs_paths.to_vec();
5974 let mut existing = None;
5975 let mut best_match = None;
5976 let mut open_visible = OpenVisible::All;
5977
5978 if open_options.open_new_workspace != Some(true) {
5979 for window in local_workspace_windows(cx) {
5980 if let Ok(workspace) = window.read(cx) {
5981 let m = workspace
5982 .project
5983 .read(cx)
5984 .visibility_for_paths(&abs_paths, cx);
5985 if m > best_match {
5986 existing = Some(window);
5987 best_match = m;
5988 } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5989 existing = Some(window)
5990 }
5991 }
5992 }
5993 }
5994
5995 cx.spawn(move |mut cx| async move {
5996 if open_options.open_new_workspace.is_none() && existing.is_none() {
5997 let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5998 if futures::future::join_all(all_files)
5999 .await
6000 .into_iter()
6001 .filter_map(|result| result.ok().flatten())
6002 .all(|file| !file.is_dir)
6003 {
6004 cx.update(|cx| {
6005 if let Some(window) = cx
6006 .active_window()
6007 .and_then(|window| window.downcast::<Workspace>())
6008 {
6009 if let Ok(workspace) = window.read(cx) {
6010 let project = workspace.project().read(cx);
6011 if project.is_local() && !project.is_via_collab() {
6012 existing = Some(window);
6013 open_visible = OpenVisible::None;
6014 return;
6015 }
6016 }
6017 }
6018 for window in local_workspace_windows(cx) {
6019 if let Ok(workspace) = window.read(cx) {
6020 let project = workspace.project().read(cx);
6021 if project.is_via_collab() {
6022 continue;
6023 }
6024 existing = Some(window);
6025 open_visible = OpenVisible::None;
6026 break;
6027 }
6028 }
6029 })?;
6030 }
6031 }
6032
6033 if let Some(existing) = existing {
6034 let open_task = existing
6035 .update(&mut cx, |workspace, window, cx| {
6036 window.activate_window();
6037 workspace.open_paths(abs_paths, open_visible, None, window, cx)
6038 })?
6039 .await;
6040
6041 _ = existing.update(&mut cx, |workspace, _, cx| {
6042 for item in open_task.iter().flatten() {
6043 if let Err(e) = item {
6044 workspace.show_error(&e, cx);
6045 }
6046 }
6047 });
6048
6049 Ok((existing, open_task))
6050 } else {
6051 cx.update(move |cx| {
6052 Workspace::new_local(
6053 abs_paths,
6054 app_state.clone(),
6055 open_options.replace_window,
6056 open_options.env,
6057 cx,
6058 )
6059 })?
6060 .await
6061 }
6062 })
6063}
6064
6065pub fn open_new(
6066 open_options: OpenOptions,
6067 app_state: Arc<AppState>,
6068 cx: &mut App,
6069 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6070) -> Task<anyhow::Result<()>> {
6071 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6072 cx.spawn(|mut cx| async move {
6073 let (workspace, opened_paths) = task.await?;
6074 workspace.update(&mut cx, |workspace, window, cx| {
6075 if opened_paths.is_empty() {
6076 init(workspace, window, cx)
6077 }
6078 })?;
6079 Ok(())
6080 })
6081}
6082
6083pub fn create_and_open_local_file(
6084 path: &'static Path,
6085 window: &mut Window,
6086 cx: &mut Context<Workspace>,
6087 default_content: impl 'static + Send + FnOnce() -> Rope,
6088) -> Task<Result<Box<dyn ItemHandle>>> {
6089 cx.spawn_in(window, |workspace, mut cx| async move {
6090 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
6091 if !fs.is_file(path).await {
6092 fs.create_file(path, Default::default()).await?;
6093 fs.save(path, &default_content(), Default::default())
6094 .await?;
6095 }
6096
6097 let mut items = workspace
6098 .update_in(&mut cx, |workspace, window, cx| {
6099 workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6100 workspace.open_paths(
6101 vec![path.to_path_buf()],
6102 OpenVisible::None,
6103 None,
6104 window,
6105 cx,
6106 )
6107 })
6108 })?
6109 .await?
6110 .await;
6111
6112 let item = items.pop().flatten();
6113 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
6114 })
6115}
6116
6117pub fn open_ssh_project(
6118 window: WindowHandle<Workspace>,
6119 connection_options: SshConnectionOptions,
6120 cancel_rx: oneshot::Receiver<()>,
6121 delegate: Arc<dyn SshClientDelegate>,
6122 app_state: Arc<AppState>,
6123 paths: Vec<PathBuf>,
6124 cx: &mut App,
6125) -> Task<Result<()>> {
6126 cx.spawn(|mut cx| async move {
6127 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6128 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6129
6130 let session = match cx
6131 .update(|cx| {
6132 remote::SshRemoteClient::new(
6133 ConnectionIdentifier::Workspace(workspace_id.0),
6134 connection_options,
6135 cancel_rx,
6136 delegate,
6137 cx,
6138 )
6139 })?
6140 .await?
6141 {
6142 Some(result) => result,
6143 None => return Ok(()),
6144 };
6145
6146 let project = cx.update(|cx| {
6147 project::Project::ssh(
6148 session,
6149 app_state.client.clone(),
6150 app_state.node_runtime.clone(),
6151 app_state.user_store.clone(),
6152 app_state.languages.clone(),
6153 app_state.fs.clone(),
6154 cx,
6155 )
6156 })?;
6157
6158 let toolchains = DB.toolchains(workspace_id).await?;
6159 for (toolchain, worktree_id) in toolchains {
6160 project
6161 .update(&mut cx, |this, cx| {
6162 this.activate_toolchain(worktree_id, toolchain, cx)
6163 })?
6164 .await;
6165 }
6166 let mut project_paths_to_open = vec![];
6167 let mut project_path_errors = vec![];
6168
6169 for path in paths {
6170 let result = cx
6171 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6172 .await;
6173 match result {
6174 Ok((_, project_path)) => {
6175 project_paths_to_open.push((path.clone(), Some(project_path)));
6176 }
6177 Err(error) => {
6178 project_path_errors.push(error);
6179 }
6180 };
6181 }
6182
6183 if project_paths_to_open.is_empty() {
6184 return Err(project_path_errors
6185 .pop()
6186 .unwrap_or_else(|| anyhow!("no paths given")));
6187 }
6188
6189 cx.update_window(window.into(), |_, window, cx| {
6190 window.replace_root(cx, |window, cx| {
6191 telemetry::event!("SSH Project Opened");
6192
6193 let mut workspace =
6194 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6195 workspace.set_serialized_ssh_project(serialized_ssh_project);
6196 workspace
6197 });
6198 })?;
6199
6200 window
6201 .update(&mut cx, |_, window, cx| {
6202 window.activate_window();
6203
6204 open_items(serialized_workspace, project_paths_to_open, window, cx)
6205 })?
6206 .await?;
6207
6208 window.update(&mut cx, |workspace, _, cx| {
6209 for error in project_path_errors {
6210 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6211 if let Some(path) = error.error_tag("path") {
6212 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6213 }
6214 } else {
6215 workspace.show_error(&error, cx)
6216 }
6217 }
6218 })
6219 })
6220}
6221
6222fn serialize_ssh_project(
6223 connection_options: SshConnectionOptions,
6224 paths: Vec<PathBuf>,
6225 cx: &AsyncApp,
6226) -> Task<
6227 Result<(
6228 SerializedSshProject,
6229 WorkspaceId,
6230 Option<SerializedWorkspace>,
6231 )>,
6232> {
6233 cx.background_executor().spawn(async move {
6234 let serialized_ssh_project = persistence::DB
6235 .get_or_create_ssh_project(
6236 connection_options.host.clone(),
6237 connection_options.port,
6238 paths
6239 .iter()
6240 .map(|path| path.to_string_lossy().to_string())
6241 .collect::<Vec<_>>(),
6242 connection_options.username.clone(),
6243 )
6244 .await?;
6245
6246 let serialized_workspace =
6247 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
6248
6249 let workspace_id = if let Some(workspace_id) =
6250 serialized_workspace.as_ref().map(|workspace| workspace.id)
6251 {
6252 workspace_id
6253 } else {
6254 persistence::DB.next_id().await?
6255 };
6256
6257 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
6258 })
6259}
6260
6261pub fn join_in_room_project(
6262 project_id: u64,
6263 follow_user_id: u64,
6264 app_state: Arc<AppState>,
6265 cx: &mut App,
6266) -> Task<Result<()>> {
6267 let windows = cx.windows();
6268 cx.spawn(|mut cx| async move {
6269 let existing_workspace = windows.into_iter().find_map(|window_handle| {
6270 window_handle
6271 .downcast::<Workspace>()
6272 .and_then(|window_handle| {
6273 window_handle
6274 .update(&mut cx, |workspace, _window, cx| {
6275 if workspace.project().read(cx).remote_id() == Some(project_id) {
6276 Some(window_handle)
6277 } else {
6278 None
6279 }
6280 })
6281 .unwrap_or(None)
6282 })
6283 });
6284
6285 let workspace = if let Some(existing_workspace) = existing_workspace {
6286 existing_workspace
6287 } else {
6288 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
6289 let room = active_call
6290 .read_with(&cx, |call, _| call.room().cloned())?
6291 .ok_or_else(|| anyhow!("not in a call"))?;
6292 let project = room
6293 .update(&mut cx, |room, cx| {
6294 room.join_project(
6295 project_id,
6296 app_state.languages.clone(),
6297 app_state.fs.clone(),
6298 cx,
6299 )
6300 })?
6301 .await?;
6302
6303 let window_bounds_override = window_bounds_env_override();
6304 cx.update(|cx| {
6305 let mut options = (app_state.build_window_options)(None, cx);
6306 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
6307 cx.open_window(options, |window, cx| {
6308 cx.new(|cx| {
6309 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
6310 })
6311 })
6312 })??
6313 };
6314
6315 workspace.update(&mut cx, |workspace, window, cx| {
6316 cx.activate(true);
6317 window.activate_window();
6318
6319 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
6320 let follow_peer_id = room
6321 .read(cx)
6322 .remote_participants()
6323 .iter()
6324 .find(|(_, participant)| participant.user.id == follow_user_id)
6325 .map(|(_, p)| p.peer_id)
6326 .or_else(|| {
6327 // If we couldn't follow the given user, follow the host instead.
6328 let collaborator = workspace
6329 .project()
6330 .read(cx)
6331 .collaborators()
6332 .values()
6333 .find(|collaborator| collaborator.is_host)?;
6334 Some(collaborator.peer_id)
6335 });
6336
6337 if let Some(follow_peer_id) = follow_peer_id {
6338 workspace.follow(follow_peer_id, window, cx);
6339 }
6340 }
6341 })?;
6342
6343 anyhow::Ok(())
6344 })
6345}
6346
6347pub fn reload(reload: &Reload, cx: &mut App) {
6348 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
6349 let mut workspace_windows = cx
6350 .windows()
6351 .into_iter()
6352 .filter_map(|window| window.downcast::<Workspace>())
6353 .collect::<Vec<_>>();
6354
6355 // If multiple windows have unsaved changes, and need a save prompt,
6356 // prompt in the active window before switching to a different window.
6357 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
6358
6359 let mut prompt = None;
6360 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
6361 prompt = window
6362 .update(cx, |_, window, cx| {
6363 window.prompt(
6364 PromptLevel::Info,
6365 "Are you sure you want to restart?",
6366 None,
6367 &["Restart", "Cancel"],
6368 cx,
6369 )
6370 })
6371 .ok();
6372 }
6373
6374 let binary_path = reload.binary_path.clone();
6375 cx.spawn(|mut cx| async move {
6376 if let Some(prompt) = prompt {
6377 let answer = prompt.await?;
6378 if answer != 0 {
6379 return Ok(());
6380 }
6381 }
6382
6383 // If the user cancels any save prompt, then keep the app open.
6384 for window in workspace_windows {
6385 if let Ok(should_close) = window.update(&mut cx, |workspace, window, cx| {
6386 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
6387 }) {
6388 if !should_close.await? {
6389 return Ok(());
6390 }
6391 }
6392 }
6393
6394 cx.update(|cx| cx.restart(binary_path))
6395 })
6396 .detach_and_log_err(cx);
6397}
6398
6399fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
6400 let mut parts = value.split(',');
6401 let x: usize = parts.next()?.parse().ok()?;
6402 let y: usize = parts.next()?.parse().ok()?;
6403 Some(point(px(x as f32), px(y as f32)))
6404}
6405
6406fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
6407 let mut parts = value.split(',');
6408 let width: usize = parts.next()?.parse().ok()?;
6409 let height: usize = parts.next()?.parse().ok()?;
6410 Some(size(px(width as f32), px(height as f32)))
6411}
6412
6413pub fn client_side_decorations(
6414 element: impl IntoElement,
6415 window: &mut Window,
6416 cx: &mut App,
6417) -> Stateful<Div> {
6418 const BORDER_SIZE: Pixels = px(1.0);
6419 let decorations = window.window_decorations();
6420
6421 if matches!(decorations, Decorations::Client { .. }) {
6422 window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6423 }
6424
6425 struct GlobalResizeEdge(ResizeEdge);
6426 impl Global for GlobalResizeEdge {}
6427
6428 div()
6429 .id("window-backdrop")
6430 .bg(transparent_black())
6431 .map(|div| match decorations {
6432 Decorations::Server => div,
6433 Decorations::Client { tiling, .. } => div
6434 .when(!(tiling.top || tiling.right), |div| {
6435 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6436 })
6437 .when(!(tiling.top || tiling.left), |div| {
6438 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6439 })
6440 .when(!(tiling.bottom || tiling.right), |div| {
6441 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6442 })
6443 .when(!(tiling.bottom || tiling.left), |div| {
6444 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6445 })
6446 .when(!tiling.top, |div| {
6447 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6448 })
6449 .when(!tiling.bottom, |div| {
6450 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6451 })
6452 .when(!tiling.left, |div| {
6453 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6454 })
6455 .when(!tiling.right, |div| {
6456 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6457 })
6458 .on_mouse_move(move |e, window, cx| {
6459 let size = window.window_bounds().get_bounds().size;
6460 let pos = e.position;
6461
6462 let new_edge =
6463 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6464
6465 let edge = cx.try_global::<GlobalResizeEdge>();
6466 if new_edge != edge.map(|edge| edge.0) {
6467 window
6468 .window_handle()
6469 .update(cx, |workspace, _, cx| {
6470 cx.notify(workspace.entity_id());
6471 })
6472 .ok();
6473 }
6474 })
6475 .on_mouse_down(MouseButton::Left, move |e, window, _| {
6476 let size = window.window_bounds().get_bounds().size;
6477 let pos = e.position;
6478
6479 let edge = match resize_edge(
6480 pos,
6481 theme::CLIENT_SIDE_DECORATION_SHADOW,
6482 size,
6483 tiling,
6484 ) {
6485 Some(value) => value,
6486 None => return,
6487 };
6488
6489 window.start_window_resize(edge);
6490 }),
6491 })
6492 .size_full()
6493 .child(
6494 div()
6495 .cursor(CursorStyle::Arrow)
6496 .map(|div| match decorations {
6497 Decorations::Server => div,
6498 Decorations::Client { tiling } => div
6499 .border_color(cx.theme().colors().border)
6500 .when(!(tiling.top || tiling.right), |div| {
6501 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6502 })
6503 .when(!(tiling.top || tiling.left), |div| {
6504 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6505 })
6506 .when(!(tiling.bottom || tiling.right), |div| {
6507 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6508 })
6509 .when(!(tiling.bottom || tiling.left), |div| {
6510 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6511 })
6512 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6513 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6514 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6515 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6516 .when(!tiling.is_tiled(), |div| {
6517 div.shadow(smallvec::smallvec![gpui::BoxShadow {
6518 color: Hsla {
6519 h: 0.,
6520 s: 0.,
6521 l: 0.,
6522 a: 0.4,
6523 },
6524 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6525 spread_radius: px(0.),
6526 offset: point(px(0.0), px(0.0)),
6527 }])
6528 }),
6529 })
6530 .on_mouse_move(|_e, _, cx| {
6531 cx.stop_propagation();
6532 })
6533 .size_full()
6534 .child(element),
6535 )
6536 .map(|div| match decorations {
6537 Decorations::Server => div,
6538 Decorations::Client { tiling, .. } => div.child(
6539 canvas(
6540 |_bounds, window, _| {
6541 window.insert_hitbox(
6542 Bounds::new(
6543 point(px(0.0), px(0.0)),
6544 window.window_bounds().get_bounds().size,
6545 ),
6546 false,
6547 )
6548 },
6549 move |_bounds, hitbox, window, cx| {
6550 let mouse = window.mouse_position();
6551 let size = window.window_bounds().get_bounds().size;
6552 let Some(edge) =
6553 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6554 else {
6555 return;
6556 };
6557 cx.set_global(GlobalResizeEdge(edge));
6558 window.set_cursor_style(
6559 match edge {
6560 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6561 ResizeEdge::Left | ResizeEdge::Right => {
6562 CursorStyle::ResizeLeftRight
6563 }
6564 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6565 CursorStyle::ResizeUpLeftDownRight
6566 }
6567 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6568 CursorStyle::ResizeUpRightDownLeft
6569 }
6570 },
6571 &hitbox,
6572 );
6573 },
6574 )
6575 .size_full()
6576 .absolute(),
6577 ),
6578 })
6579}
6580
6581fn resize_edge(
6582 pos: Point<Pixels>,
6583 shadow_size: Pixels,
6584 window_size: Size<Pixels>,
6585 tiling: Tiling,
6586) -> Option<ResizeEdge> {
6587 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6588 if bounds.contains(&pos) {
6589 return None;
6590 }
6591
6592 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6593 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6594 if !tiling.top && top_left_bounds.contains(&pos) {
6595 return Some(ResizeEdge::TopLeft);
6596 }
6597
6598 let top_right_bounds = Bounds::new(
6599 Point::new(window_size.width - corner_size.width, px(0.)),
6600 corner_size,
6601 );
6602 if !tiling.top && top_right_bounds.contains(&pos) {
6603 return Some(ResizeEdge::TopRight);
6604 }
6605
6606 let bottom_left_bounds = Bounds::new(
6607 Point::new(px(0.), window_size.height - corner_size.height),
6608 corner_size,
6609 );
6610 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6611 return Some(ResizeEdge::BottomLeft);
6612 }
6613
6614 let bottom_right_bounds = Bounds::new(
6615 Point::new(
6616 window_size.width - corner_size.width,
6617 window_size.height - corner_size.height,
6618 ),
6619 corner_size,
6620 );
6621 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6622 return Some(ResizeEdge::BottomRight);
6623 }
6624
6625 if !tiling.top && pos.y < shadow_size {
6626 Some(ResizeEdge::Top)
6627 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6628 Some(ResizeEdge::Bottom)
6629 } else if !tiling.left && pos.x < shadow_size {
6630 Some(ResizeEdge::Left)
6631 } else if !tiling.right && pos.x > window_size.width - shadow_size {
6632 Some(ResizeEdge::Right)
6633 } else {
6634 None
6635 }
6636}
6637
6638fn join_pane_into_active(
6639 active_pane: &Entity<Pane>,
6640 pane: &Entity<Pane>,
6641 window: &mut Window,
6642 cx: &mut App,
6643) {
6644 if pane == active_pane {
6645 return;
6646 } else if pane.read(cx).items_len() == 0 {
6647 pane.update(cx, |_, cx| {
6648 cx.emit(pane::Event::Remove {
6649 focus_on_pane: None,
6650 });
6651 })
6652 } else {
6653 move_all_items(pane, active_pane, window, cx);
6654 }
6655}
6656
6657fn move_all_items(
6658 from_pane: &Entity<Pane>,
6659 to_pane: &Entity<Pane>,
6660 window: &mut Window,
6661 cx: &mut App,
6662) {
6663 let destination_is_different = from_pane != to_pane;
6664 let mut moved_items = 0;
6665 for (item_ix, item_handle) in from_pane
6666 .read(cx)
6667 .items()
6668 .enumerate()
6669 .map(|(ix, item)| (ix, item.clone()))
6670 .collect::<Vec<_>>()
6671 {
6672 let ix = item_ix - moved_items;
6673 if destination_is_different {
6674 // Close item from previous pane
6675 from_pane.update(cx, |source, cx| {
6676 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
6677 });
6678 moved_items += 1;
6679 }
6680
6681 // This automatically removes duplicate items in the pane
6682 to_pane.update(cx, |destination, cx| {
6683 destination.add_item(item_handle, true, true, None, window, cx);
6684 window.focus(&destination.focus_handle(cx))
6685 });
6686 }
6687}
6688
6689pub fn move_item(
6690 source: &Entity<Pane>,
6691 destination: &Entity<Pane>,
6692 item_id_to_move: EntityId,
6693 destination_index: usize,
6694 window: &mut Window,
6695 cx: &mut App,
6696) {
6697 let Some((item_ix, item_handle)) = source
6698 .read(cx)
6699 .items()
6700 .enumerate()
6701 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6702 .map(|(ix, item)| (ix, item.clone()))
6703 else {
6704 // Tab was closed during drag
6705 return;
6706 };
6707
6708 if source != destination {
6709 // Close item from previous pane
6710 source.update(cx, |source, cx| {
6711 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
6712 });
6713 }
6714
6715 // This automatically removes duplicate items in the pane
6716 destination.update(cx, |destination, cx| {
6717 destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
6718 window.focus(&destination.focus_handle(cx))
6719 });
6720}
6721
6722pub fn move_active_item(
6723 source: &Entity<Pane>,
6724 destination: &Entity<Pane>,
6725 focus_destination: bool,
6726 close_if_empty: bool,
6727 window: &mut Window,
6728 cx: &mut App,
6729) {
6730 if source == destination {
6731 return;
6732 }
6733 let Some(active_item) = source.read(cx).active_item() else {
6734 return;
6735 };
6736 source.update(cx, |source_pane, cx| {
6737 let item_id = active_item.item_id();
6738 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
6739 destination.update(cx, |target_pane, cx| {
6740 target_pane.add_item(
6741 active_item,
6742 focus_destination,
6743 focus_destination,
6744 Some(target_pane.items_len()),
6745 window,
6746 cx,
6747 );
6748 });
6749 });
6750}
6751
6752#[cfg(test)]
6753mod tests {
6754 use std::{cell::RefCell, rc::Rc};
6755
6756 use super::*;
6757 use crate::{
6758 dock::{test::TestPanel, PanelEvent},
6759 item::{
6760 test::{TestItem, TestProjectItem},
6761 ItemEvent,
6762 },
6763 };
6764 use fs::FakeFs;
6765 use gpui::{
6766 px, DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
6767 UpdateGlobal, VisualTestContext,
6768 };
6769 use project::{Project, ProjectEntryId};
6770 use serde_json::json;
6771 use settings::SettingsStore;
6772
6773 #[gpui::test]
6774 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6775 init_test(cx);
6776
6777 let fs = FakeFs::new(cx.executor());
6778 let project = Project::test(fs, [], cx).await;
6779 let (workspace, cx) =
6780 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6781
6782 // Adding an item with no ambiguity renders the tab without detail.
6783 let item1 = cx.new(|cx| {
6784 let mut item = TestItem::new(cx);
6785 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6786 item
6787 });
6788 workspace.update_in(cx, |workspace, window, cx| {
6789 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6790 });
6791 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6792
6793 // Adding an item that creates ambiguity increases the level of detail on
6794 // both tabs.
6795 let item2 = cx.new_window_entity(|_window, cx| {
6796 let mut item = TestItem::new(cx);
6797 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6798 item
6799 });
6800 workspace.update_in(cx, |workspace, window, cx| {
6801 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6802 });
6803 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6804 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6805
6806 // Adding an item that creates ambiguity increases the level of detail only
6807 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6808 // we stop at the highest detail available.
6809 let item3 = cx.new(|cx| {
6810 let mut item = TestItem::new(cx);
6811 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6812 item
6813 });
6814 workspace.update_in(cx, |workspace, window, cx| {
6815 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6816 });
6817 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6818 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6819 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6820 }
6821
6822 #[gpui::test]
6823 async fn test_tracking_active_path(cx: &mut TestAppContext) {
6824 init_test(cx);
6825
6826 let fs = FakeFs::new(cx.executor());
6827 fs.insert_tree(
6828 "/root1",
6829 json!({
6830 "one.txt": "",
6831 "two.txt": "",
6832 }),
6833 )
6834 .await;
6835 fs.insert_tree(
6836 "/root2",
6837 json!({
6838 "three.txt": "",
6839 }),
6840 )
6841 .await;
6842
6843 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6844 let (workspace, cx) =
6845 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6846 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6847 let worktree_id = project.update(cx, |project, cx| {
6848 project.worktrees(cx).next().unwrap().read(cx).id()
6849 });
6850
6851 let item1 = cx.new(|cx| {
6852 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6853 });
6854 let item2 = cx.new(|cx| {
6855 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6856 });
6857
6858 // Add an item to an empty pane
6859 workspace.update_in(cx, |workspace, window, cx| {
6860 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
6861 });
6862 project.update(cx, |project, cx| {
6863 assert_eq!(
6864 project.active_entry(),
6865 project
6866 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6867 .map(|e| e.id)
6868 );
6869 });
6870 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6871
6872 // Add a second item to a non-empty pane
6873 workspace.update_in(cx, |workspace, window, cx| {
6874 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
6875 });
6876 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6877 project.update(cx, |project, cx| {
6878 assert_eq!(
6879 project.active_entry(),
6880 project
6881 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6882 .map(|e| e.id)
6883 );
6884 });
6885
6886 // Close the active item
6887 pane.update_in(cx, |pane, window, cx| {
6888 pane.close_active_item(&Default::default(), window, cx)
6889 .unwrap()
6890 })
6891 .await
6892 .unwrap();
6893 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6894 project.update(cx, |project, cx| {
6895 assert_eq!(
6896 project.active_entry(),
6897 project
6898 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6899 .map(|e| e.id)
6900 );
6901 });
6902
6903 // Add a project folder
6904 project
6905 .update(cx, |project, cx| {
6906 project.find_or_create_worktree("root2", true, cx)
6907 })
6908 .await
6909 .unwrap();
6910 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6911
6912 // Remove a project folder
6913 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6914 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6915 }
6916
6917 #[gpui::test]
6918 async fn test_close_window(cx: &mut TestAppContext) {
6919 init_test(cx);
6920
6921 let fs = FakeFs::new(cx.executor());
6922 fs.insert_tree("/root", json!({ "one": "" })).await;
6923
6924 let project = Project::test(fs, ["root".as_ref()], cx).await;
6925 let (workspace, cx) =
6926 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6927
6928 // When there are no dirty items, there's nothing to do.
6929 let item1 = cx.new(TestItem::new);
6930 workspace.update_in(cx, |w, window, cx| {
6931 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
6932 });
6933 let task = workspace.update_in(cx, |w, window, cx| {
6934 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6935 });
6936 assert!(task.await.unwrap());
6937
6938 // When there are dirty untitled items, prompt to save each one. If the user
6939 // cancels any prompt, then abort.
6940 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
6941 let item3 = cx.new(|cx| {
6942 TestItem::new(cx)
6943 .with_dirty(true)
6944 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6945 });
6946 workspace.update_in(cx, |w, window, cx| {
6947 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6948 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6949 });
6950 let task = workspace.update_in(cx, |w, window, cx| {
6951 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6952 });
6953 cx.executor().run_until_parked();
6954 cx.simulate_prompt_answer(2); // cancel save all
6955 cx.executor().run_until_parked();
6956 cx.simulate_prompt_answer(2); // cancel save all
6957 cx.executor().run_until_parked();
6958 assert!(!cx.has_pending_prompt());
6959 assert!(!task.await.unwrap());
6960 }
6961
6962 #[gpui::test]
6963 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6964 init_test(cx);
6965
6966 // Register TestItem as a serializable item
6967 cx.update(|cx| {
6968 register_serializable_item::<TestItem>(cx);
6969 });
6970
6971 let fs = FakeFs::new(cx.executor());
6972 fs.insert_tree("/root", json!({ "one": "" })).await;
6973
6974 let project = Project::test(fs, ["root".as_ref()], cx).await;
6975 let (workspace, cx) =
6976 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6977
6978 // When there are dirty untitled items, but they can serialize, then there is no prompt.
6979 let item1 = cx.new(|cx| {
6980 TestItem::new(cx)
6981 .with_dirty(true)
6982 .with_serialize(|| Some(Task::ready(Ok(()))))
6983 });
6984 let item2 = cx.new(|cx| {
6985 TestItem::new(cx)
6986 .with_dirty(true)
6987 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6988 .with_serialize(|| Some(Task::ready(Ok(()))))
6989 });
6990 workspace.update_in(cx, |w, window, cx| {
6991 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6992 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6993 });
6994 let task = workspace.update_in(cx, |w, window, cx| {
6995 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6996 });
6997 assert!(task.await.unwrap());
6998 }
6999
7000 #[gpui::test]
7001 async fn test_close_pane_items(cx: &mut TestAppContext) {
7002 init_test(cx);
7003
7004 let fs = FakeFs::new(cx.executor());
7005
7006 let project = Project::test(fs, None, cx).await;
7007 let (workspace, cx) =
7008 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7009
7010 let item1 = cx.new(|cx| {
7011 TestItem::new(cx)
7012 .with_dirty(true)
7013 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7014 });
7015 let item2 = cx.new(|cx| {
7016 TestItem::new(cx)
7017 .with_dirty(true)
7018 .with_conflict(true)
7019 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7020 });
7021 let item3 = cx.new(|cx| {
7022 TestItem::new(cx)
7023 .with_dirty(true)
7024 .with_conflict(true)
7025 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7026 });
7027 let item4 = cx.new(|cx| {
7028 TestItem::new(cx).with_dirty(true).with_project_items(&[{
7029 let project_item = TestProjectItem::new_untitled(cx);
7030 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7031 project_item
7032 }])
7033 });
7034 let pane = workspace.update_in(cx, |workspace, window, cx| {
7035 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7036 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7037 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7038 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7039 workspace.active_pane().clone()
7040 });
7041
7042 let close_items = pane.update_in(cx, |pane, window, cx| {
7043 pane.activate_item(1, true, true, window, cx);
7044 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7045 let item1_id = item1.item_id();
7046 let item3_id = item3.item_id();
7047 let item4_id = item4.item_id();
7048 pane.close_items(window, cx, SaveIntent::Close, move |id| {
7049 [item1_id, item3_id, item4_id].contains(&id)
7050 })
7051 });
7052 cx.executor().run_until_parked();
7053
7054 assert!(cx.has_pending_prompt());
7055 // Ignore "Save all" prompt
7056 cx.simulate_prompt_answer(2);
7057 cx.executor().run_until_parked();
7058 // There's a prompt to save item 1.
7059 pane.update(cx, |pane, _| {
7060 assert_eq!(pane.items_len(), 4);
7061 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
7062 });
7063 // Confirm saving item 1.
7064 cx.simulate_prompt_answer(0);
7065 cx.executor().run_until_parked();
7066
7067 // Item 1 is saved. There's a prompt to save item 3.
7068 pane.update(cx, |pane, cx| {
7069 assert_eq!(item1.read(cx).save_count, 1);
7070 assert_eq!(item1.read(cx).save_as_count, 0);
7071 assert_eq!(item1.read(cx).reload_count, 0);
7072 assert_eq!(pane.items_len(), 3);
7073 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7074 });
7075 assert!(cx.has_pending_prompt());
7076
7077 // Cancel saving item 3.
7078 cx.simulate_prompt_answer(1);
7079 cx.executor().run_until_parked();
7080
7081 // Item 3 is reloaded. There's a prompt to save item 4.
7082 pane.update(cx, |pane, cx| {
7083 assert_eq!(item3.read(cx).save_count, 0);
7084 assert_eq!(item3.read(cx).save_as_count, 0);
7085 assert_eq!(item3.read(cx).reload_count, 1);
7086 assert_eq!(pane.items_len(), 2);
7087 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7088 });
7089 assert!(cx.has_pending_prompt());
7090
7091 // Confirm saving item 4.
7092 cx.simulate_prompt_answer(0);
7093 cx.executor().run_until_parked();
7094
7095 // There's a prompt for a path for item 4.
7096 cx.simulate_new_path_selection(|_| Some(Default::default()));
7097 close_items.await.unwrap();
7098
7099 // The requested items are closed.
7100 pane.update(cx, |pane, cx| {
7101 assert_eq!(item4.read(cx).save_count, 0);
7102 assert_eq!(item4.read(cx).save_as_count, 1);
7103 assert_eq!(item4.read(cx).reload_count, 0);
7104 assert_eq!(pane.items_len(), 1);
7105 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7106 });
7107 }
7108
7109 #[gpui::test]
7110 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7111 init_test(cx);
7112
7113 let fs = FakeFs::new(cx.executor());
7114 let project = Project::test(fs, [], cx).await;
7115 let (workspace, cx) =
7116 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7117
7118 // Create several workspace items with single project entries, and two
7119 // workspace items with multiple project entries.
7120 let single_entry_items = (0..=4)
7121 .map(|project_entry_id| {
7122 cx.new(|cx| {
7123 TestItem::new(cx)
7124 .with_dirty(true)
7125 .with_project_items(&[dirty_project_item(
7126 project_entry_id,
7127 &format!("{project_entry_id}.txt"),
7128 cx,
7129 )])
7130 })
7131 })
7132 .collect::<Vec<_>>();
7133 let item_2_3 = cx.new(|cx| {
7134 TestItem::new(cx)
7135 .with_dirty(true)
7136 .with_singleton(false)
7137 .with_project_items(&[
7138 single_entry_items[2].read(cx).project_items[0].clone(),
7139 single_entry_items[3].read(cx).project_items[0].clone(),
7140 ])
7141 });
7142 let item_3_4 = cx.new(|cx| {
7143 TestItem::new(cx)
7144 .with_dirty(true)
7145 .with_singleton(false)
7146 .with_project_items(&[
7147 single_entry_items[3].read(cx).project_items[0].clone(),
7148 single_entry_items[4].read(cx).project_items[0].clone(),
7149 ])
7150 });
7151
7152 // Create two panes that contain the following project entries:
7153 // left pane:
7154 // multi-entry items: (2, 3)
7155 // single-entry items: 0, 1, 2, 3, 4
7156 // right pane:
7157 // single-entry items: 1
7158 // multi-entry items: (3, 4)
7159 let left_pane = workspace.update_in(cx, |workspace, window, cx| {
7160 let left_pane = workspace.active_pane().clone();
7161 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7162 for item in single_entry_items {
7163 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
7164 }
7165 left_pane.update(cx, |pane, cx| {
7166 pane.activate_item(2, true, true, window, cx);
7167 });
7168
7169 let right_pane = workspace
7170 .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7171 .unwrap();
7172
7173 right_pane.update(cx, |pane, cx| {
7174 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
7175 });
7176
7177 left_pane
7178 });
7179
7180 cx.focus(&left_pane);
7181
7182 // When closing all of the items in the left pane, we should be prompted twice:
7183 // once for project entry 0, and once for project entry 2. Project entries 1,
7184 // 3, and 4 are all still open in the other paten. After those two
7185 // prompts, the task should complete.
7186
7187 let close = left_pane.update_in(cx, |pane, window, cx| {
7188 pane.close_all_items(&CloseAllItems::default(), window, cx)
7189 .unwrap()
7190 });
7191 cx.executor().run_until_parked();
7192
7193 // Discard "Save all" prompt
7194 cx.simulate_prompt_answer(2);
7195
7196 cx.executor().run_until_parked();
7197 left_pane.update(cx, |pane, cx| {
7198 assert_eq!(
7199 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
7200 &[ProjectEntryId::from_proto(0)]
7201 );
7202 });
7203 cx.simulate_prompt_answer(0);
7204
7205 cx.executor().run_until_parked();
7206 left_pane.update(cx, |pane, cx| {
7207 assert_eq!(
7208 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
7209 &[ProjectEntryId::from_proto(2)]
7210 );
7211 });
7212 cx.simulate_prompt_answer(0);
7213
7214 cx.executor().run_until_parked();
7215 close.await.unwrap();
7216 left_pane.update(cx, |pane, _| {
7217 assert_eq!(pane.items_len(), 0);
7218 });
7219 }
7220
7221 #[gpui::test]
7222 async fn test_autosave(cx: &mut gpui::TestAppContext) {
7223 init_test(cx);
7224
7225 let fs = FakeFs::new(cx.executor());
7226 let project = Project::test(fs, [], cx).await;
7227 let (workspace, cx) =
7228 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7229 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7230
7231 let item = cx.new(|cx| {
7232 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7233 });
7234 let item_id = item.entity_id();
7235 workspace.update_in(cx, |workspace, window, cx| {
7236 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7237 });
7238
7239 // Autosave on window change.
7240 item.update(cx, |item, cx| {
7241 SettingsStore::update_global(cx, |settings, cx| {
7242 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7243 settings.autosave = Some(AutosaveSetting::OnWindowChange);
7244 })
7245 });
7246 item.is_dirty = true;
7247 });
7248
7249 // Deactivating the window saves the file.
7250 cx.deactivate_window();
7251 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7252
7253 // Re-activating the window doesn't save the file.
7254 cx.update(|window, _| window.activate_window());
7255 cx.executor().run_until_parked();
7256 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7257
7258 // Autosave on focus change.
7259 item.update_in(cx, |item, window, cx| {
7260 cx.focus_self(window);
7261 SettingsStore::update_global(cx, |settings, cx| {
7262 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7263 settings.autosave = Some(AutosaveSetting::OnFocusChange);
7264 })
7265 });
7266 item.is_dirty = true;
7267 });
7268
7269 // Blurring the item saves the file.
7270 item.update_in(cx, |_, window, _| window.blur());
7271 cx.executor().run_until_parked();
7272 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
7273
7274 // Deactivating the window still saves the file.
7275 item.update_in(cx, |item, window, cx| {
7276 cx.focus_self(window);
7277 item.is_dirty = true;
7278 });
7279 cx.deactivate_window();
7280 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7281
7282 // Autosave after delay.
7283 item.update(cx, |item, cx| {
7284 SettingsStore::update_global(cx, |settings, cx| {
7285 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7286 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
7287 })
7288 });
7289 item.is_dirty = true;
7290 cx.emit(ItemEvent::Edit);
7291 });
7292
7293 // Delay hasn't fully expired, so the file is still dirty and unsaved.
7294 cx.executor().advance_clock(Duration::from_millis(250));
7295 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7296
7297 // After delay expires, the file is saved.
7298 cx.executor().advance_clock(Duration::from_millis(250));
7299 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
7300
7301 // Autosave on focus change, ensuring closing the tab counts as such.
7302 item.update(cx, |item, cx| {
7303 SettingsStore::update_global(cx, |settings, cx| {
7304 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7305 settings.autosave = Some(AutosaveSetting::OnFocusChange);
7306 })
7307 });
7308 item.is_dirty = true;
7309 for project_item in &mut item.project_items {
7310 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7311 }
7312 });
7313
7314 pane.update_in(cx, |pane, window, cx| {
7315 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7316 })
7317 .await
7318 .unwrap();
7319 assert!(!cx.has_pending_prompt());
7320 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7321
7322 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
7323 workspace.update_in(cx, |workspace, window, cx| {
7324 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7325 });
7326 item.update_in(cx, |item, window, cx| {
7327 item.project_items[0].update(cx, |item, _| {
7328 item.entry_id = None;
7329 });
7330 item.is_dirty = true;
7331 window.blur();
7332 });
7333 cx.run_until_parked();
7334 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7335
7336 // Ensure autosave is prevented for deleted files also when closing the buffer.
7337 let _close_items = pane.update_in(cx, |pane, window, cx| {
7338 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7339 });
7340 cx.run_until_parked();
7341 assert!(cx.has_pending_prompt());
7342 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7343 }
7344
7345 #[gpui::test]
7346 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
7347 init_test(cx);
7348
7349 let fs = FakeFs::new(cx.executor());
7350
7351 let project = Project::test(fs, [], cx).await;
7352 let (workspace, cx) =
7353 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7354
7355 let item = cx.new(|cx| {
7356 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7357 });
7358 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7359 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
7360 let toolbar_notify_count = Rc::new(RefCell::new(0));
7361
7362 workspace.update_in(cx, |workspace, window, cx| {
7363 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7364 let toolbar_notification_count = toolbar_notify_count.clone();
7365 cx.observe_in(&toolbar, window, move |_, _, _, _| {
7366 *toolbar_notification_count.borrow_mut() += 1
7367 })
7368 .detach();
7369 });
7370
7371 pane.update(cx, |pane, _| {
7372 assert!(!pane.can_navigate_backward());
7373 assert!(!pane.can_navigate_forward());
7374 });
7375
7376 item.update_in(cx, |item, _, cx| {
7377 item.set_state("one".to_string(), cx);
7378 });
7379
7380 // Toolbar must be notified to re-render the navigation buttons
7381 assert_eq!(*toolbar_notify_count.borrow(), 1);
7382
7383 pane.update(cx, |pane, _| {
7384 assert!(pane.can_navigate_backward());
7385 assert!(!pane.can_navigate_forward());
7386 });
7387
7388 workspace
7389 .update_in(cx, |workspace, window, cx| {
7390 workspace.go_back(pane.downgrade(), window, cx)
7391 })
7392 .await
7393 .unwrap();
7394
7395 assert_eq!(*toolbar_notify_count.borrow(), 2);
7396 pane.update(cx, |pane, _| {
7397 assert!(!pane.can_navigate_backward());
7398 assert!(pane.can_navigate_forward());
7399 });
7400 }
7401
7402 #[gpui::test]
7403 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
7404 init_test(cx);
7405 let fs = FakeFs::new(cx.executor());
7406
7407 let project = Project::test(fs, [], cx).await;
7408 let (workspace, cx) =
7409 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7410
7411 let panel = workspace.update_in(cx, |workspace, window, cx| {
7412 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7413 workspace.add_panel(panel.clone(), window, cx);
7414
7415 workspace
7416 .right_dock()
7417 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
7418
7419 panel
7420 });
7421
7422 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7423 pane.update_in(cx, |pane, window, cx| {
7424 let item = cx.new(TestItem::new);
7425 pane.add_item(Box::new(item), true, true, None, window, cx);
7426 });
7427
7428 // Transfer focus from center to panel
7429 workspace.update_in(cx, |workspace, window, cx| {
7430 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7431 });
7432
7433 workspace.update_in(cx, |workspace, window, cx| {
7434 assert!(workspace.right_dock().read(cx).is_open());
7435 assert!(!panel.is_zoomed(window, cx));
7436 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7437 });
7438
7439 // Transfer focus from panel to center
7440 workspace.update_in(cx, |workspace, window, cx| {
7441 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7442 });
7443
7444 workspace.update_in(cx, |workspace, window, cx| {
7445 assert!(workspace.right_dock().read(cx).is_open());
7446 assert!(!panel.is_zoomed(window, cx));
7447 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7448 });
7449
7450 // Close the dock
7451 workspace.update_in(cx, |workspace, window, cx| {
7452 workspace.toggle_dock(DockPosition::Right, window, cx);
7453 });
7454
7455 workspace.update_in(cx, |workspace, window, cx| {
7456 assert!(!workspace.right_dock().read(cx).is_open());
7457 assert!(!panel.is_zoomed(window, cx));
7458 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7459 });
7460
7461 // Open the dock
7462 workspace.update_in(cx, |workspace, window, cx| {
7463 workspace.toggle_dock(DockPosition::Right, window, cx);
7464 });
7465
7466 workspace.update_in(cx, |workspace, window, cx| {
7467 assert!(workspace.right_dock().read(cx).is_open());
7468 assert!(!panel.is_zoomed(window, cx));
7469 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7470 });
7471
7472 // Focus and zoom panel
7473 panel.update_in(cx, |panel, window, cx| {
7474 cx.focus_self(window);
7475 panel.set_zoomed(true, window, cx)
7476 });
7477
7478 workspace.update_in(cx, |workspace, window, cx| {
7479 assert!(workspace.right_dock().read(cx).is_open());
7480 assert!(panel.is_zoomed(window, cx));
7481 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7482 });
7483
7484 // Transfer focus to the center closes the dock
7485 workspace.update_in(cx, |workspace, window, cx| {
7486 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7487 });
7488
7489 workspace.update_in(cx, |workspace, window, cx| {
7490 assert!(!workspace.right_dock().read(cx).is_open());
7491 assert!(panel.is_zoomed(window, cx));
7492 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7493 });
7494
7495 // Transferring focus back to the panel keeps it zoomed
7496 workspace.update_in(cx, |workspace, window, cx| {
7497 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7498 });
7499
7500 workspace.update_in(cx, |workspace, window, cx| {
7501 assert!(workspace.right_dock().read(cx).is_open());
7502 assert!(panel.is_zoomed(window, cx));
7503 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7504 });
7505
7506 // Close the dock while it is zoomed
7507 workspace.update_in(cx, |workspace, window, cx| {
7508 workspace.toggle_dock(DockPosition::Right, window, cx)
7509 });
7510
7511 workspace.update_in(cx, |workspace, window, cx| {
7512 assert!(!workspace.right_dock().read(cx).is_open());
7513 assert!(panel.is_zoomed(window, cx));
7514 assert!(workspace.zoomed.is_none());
7515 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7516 });
7517
7518 // Opening the dock, when it's zoomed, retains focus
7519 workspace.update_in(cx, |workspace, window, cx| {
7520 workspace.toggle_dock(DockPosition::Right, window, cx)
7521 });
7522
7523 workspace.update_in(cx, |workspace, window, cx| {
7524 assert!(workspace.right_dock().read(cx).is_open());
7525 assert!(panel.is_zoomed(window, cx));
7526 assert!(workspace.zoomed.is_some());
7527 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7528 });
7529
7530 // Unzoom and close the panel, zoom the active pane.
7531 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
7532 workspace.update_in(cx, |workspace, window, cx| {
7533 workspace.toggle_dock(DockPosition::Right, window, cx)
7534 });
7535 pane.update_in(cx, |pane, window, cx| {
7536 pane.toggle_zoom(&Default::default(), window, cx)
7537 });
7538
7539 // Opening a dock unzooms the pane.
7540 workspace.update_in(cx, |workspace, window, cx| {
7541 workspace.toggle_dock(DockPosition::Right, window, cx)
7542 });
7543 workspace.update_in(cx, |workspace, window, cx| {
7544 let pane = pane.read(cx);
7545 assert!(!pane.is_zoomed());
7546 assert!(!pane.focus_handle(cx).is_focused(window));
7547 assert!(workspace.right_dock().read(cx).is_open());
7548 assert!(workspace.zoomed.is_none());
7549 });
7550 }
7551
7552 #[gpui::test]
7553 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7554 init_test(cx);
7555
7556 let fs = FakeFs::new(cx.executor());
7557
7558 let project = Project::test(fs, None, cx).await;
7559 let (workspace, cx) =
7560 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7561
7562 // Let's arrange the panes like this:
7563 //
7564 // +-----------------------+
7565 // | top |
7566 // +------+--------+-------+
7567 // | left | center | right |
7568 // +------+--------+-------+
7569 // | bottom |
7570 // +-----------------------+
7571
7572 let top_item = cx.new(|cx| {
7573 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7574 });
7575 let bottom_item = cx.new(|cx| {
7576 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7577 });
7578 let left_item = cx.new(|cx| {
7579 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7580 });
7581 let right_item = cx.new(|cx| {
7582 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7583 });
7584 let center_item = cx.new(|cx| {
7585 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7586 });
7587
7588 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7589 let top_pane_id = workspace.active_pane().entity_id();
7590 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
7591 workspace.split_pane(
7592 workspace.active_pane().clone(),
7593 SplitDirection::Down,
7594 window,
7595 cx,
7596 );
7597 top_pane_id
7598 });
7599 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7600 let bottom_pane_id = workspace.active_pane().entity_id();
7601 workspace.add_item_to_active_pane(
7602 Box::new(bottom_item.clone()),
7603 None,
7604 false,
7605 window,
7606 cx,
7607 );
7608 workspace.split_pane(
7609 workspace.active_pane().clone(),
7610 SplitDirection::Up,
7611 window,
7612 cx,
7613 );
7614 bottom_pane_id
7615 });
7616 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7617 let left_pane_id = workspace.active_pane().entity_id();
7618 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
7619 workspace.split_pane(
7620 workspace.active_pane().clone(),
7621 SplitDirection::Right,
7622 window,
7623 cx,
7624 );
7625 left_pane_id
7626 });
7627 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7628 let right_pane_id = workspace.active_pane().entity_id();
7629 workspace.add_item_to_active_pane(
7630 Box::new(right_item.clone()),
7631 None,
7632 false,
7633 window,
7634 cx,
7635 );
7636 workspace.split_pane(
7637 workspace.active_pane().clone(),
7638 SplitDirection::Left,
7639 window,
7640 cx,
7641 );
7642 right_pane_id
7643 });
7644 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7645 let center_pane_id = workspace.active_pane().entity_id();
7646 workspace.add_item_to_active_pane(
7647 Box::new(center_item.clone()),
7648 None,
7649 false,
7650 window,
7651 cx,
7652 );
7653 center_pane_id
7654 });
7655 cx.executor().run_until_parked();
7656
7657 workspace.update_in(cx, |workspace, window, cx| {
7658 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7659
7660 // Join into next from center pane into right
7661 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7662 });
7663
7664 workspace.update_in(cx, |workspace, window, cx| {
7665 let active_pane = workspace.active_pane();
7666 assert_eq!(right_pane_id, active_pane.entity_id());
7667 assert_eq!(2, active_pane.read(cx).items_len());
7668 let item_ids_in_pane =
7669 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7670 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7671 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7672
7673 // Join into next from right pane into bottom
7674 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7675 });
7676
7677 workspace.update_in(cx, |workspace, window, cx| {
7678 let active_pane = workspace.active_pane();
7679 assert_eq!(bottom_pane_id, active_pane.entity_id());
7680 assert_eq!(3, active_pane.read(cx).items_len());
7681 let item_ids_in_pane =
7682 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7683 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7684 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7685 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7686
7687 // Join into next from bottom pane into left
7688 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7689 });
7690
7691 workspace.update_in(cx, |workspace, window, cx| {
7692 let active_pane = workspace.active_pane();
7693 assert_eq!(left_pane_id, active_pane.entity_id());
7694 assert_eq!(4, active_pane.read(cx).items_len());
7695 let item_ids_in_pane =
7696 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7697 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7698 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7699 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7700 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7701
7702 // Join into next from left pane into top
7703 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7704 });
7705
7706 workspace.update_in(cx, |workspace, window, cx| {
7707 let active_pane = workspace.active_pane();
7708 assert_eq!(top_pane_id, active_pane.entity_id());
7709 assert_eq!(5, active_pane.read(cx).items_len());
7710 let item_ids_in_pane =
7711 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7712 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7713 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7714 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7715 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7716 assert!(item_ids_in_pane.contains(&top_item.item_id()));
7717
7718 // Single pane left: no-op
7719 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
7720 });
7721
7722 workspace.update(cx, |workspace, _cx| {
7723 let active_pane = workspace.active_pane();
7724 assert_eq!(top_pane_id, active_pane.entity_id());
7725 });
7726 }
7727
7728 fn add_an_item_to_active_pane(
7729 cx: &mut VisualTestContext,
7730 workspace: &Entity<Workspace>,
7731 item_id: u64,
7732 ) -> Entity<TestItem> {
7733 let item = cx.new(|cx| {
7734 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7735 item_id,
7736 "item{item_id}.txt",
7737 cx,
7738 )])
7739 });
7740 workspace.update_in(cx, |workspace, window, cx| {
7741 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
7742 });
7743 return item;
7744 }
7745
7746 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
7747 return workspace.update_in(cx, |workspace, window, cx| {
7748 let new_pane = workspace.split_pane(
7749 workspace.active_pane().clone(),
7750 SplitDirection::Right,
7751 window,
7752 cx,
7753 );
7754 new_pane
7755 });
7756 }
7757
7758 #[gpui::test]
7759 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7760 init_test(cx);
7761 let fs = FakeFs::new(cx.executor());
7762 let project = Project::test(fs, None, cx).await;
7763 let (workspace, cx) =
7764 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7765
7766 add_an_item_to_active_pane(cx, &workspace, 1);
7767 split_pane(cx, &workspace);
7768 add_an_item_to_active_pane(cx, &workspace, 2);
7769 split_pane(cx, &workspace); // empty pane
7770 split_pane(cx, &workspace);
7771 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7772
7773 cx.executor().run_until_parked();
7774
7775 workspace.update(cx, |workspace, cx| {
7776 let num_panes = workspace.panes().len();
7777 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7778 let active_item = workspace
7779 .active_pane()
7780 .read(cx)
7781 .active_item()
7782 .expect("item is in focus");
7783
7784 assert_eq!(num_panes, 4);
7785 assert_eq!(num_items_in_current_pane, 1);
7786 assert_eq!(active_item.item_id(), last_item.item_id());
7787 });
7788
7789 workspace.update_in(cx, |workspace, window, cx| {
7790 workspace.join_all_panes(window, cx);
7791 });
7792
7793 workspace.update(cx, |workspace, cx| {
7794 let num_panes = workspace.panes().len();
7795 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7796 let active_item = workspace
7797 .active_pane()
7798 .read(cx)
7799 .active_item()
7800 .expect("item is in focus");
7801
7802 assert_eq!(num_panes, 1);
7803 assert_eq!(num_items_in_current_pane, 3);
7804 assert_eq!(active_item.item_id(), last_item.item_id());
7805 });
7806 }
7807 struct TestModal(FocusHandle);
7808
7809 impl TestModal {
7810 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
7811 Self(cx.focus_handle())
7812 }
7813 }
7814
7815 impl EventEmitter<DismissEvent> for TestModal {}
7816
7817 impl Focusable for TestModal {
7818 fn focus_handle(&self, _cx: &App) -> FocusHandle {
7819 self.0.clone()
7820 }
7821 }
7822
7823 impl ModalView for TestModal {}
7824
7825 impl Render for TestModal {
7826 fn render(
7827 &mut self,
7828 _window: &mut Window,
7829 _cx: &mut Context<TestModal>,
7830 ) -> impl IntoElement {
7831 div().track_focus(&self.0)
7832 }
7833 }
7834
7835 #[gpui::test]
7836 async fn test_panels(cx: &mut gpui::TestAppContext) {
7837 init_test(cx);
7838 let fs = FakeFs::new(cx.executor());
7839
7840 let project = Project::test(fs, [], cx).await;
7841 let (workspace, cx) =
7842 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7843
7844 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
7845 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
7846 workspace.add_panel(panel_1.clone(), window, cx);
7847 workspace.toggle_dock(DockPosition::Left, window, cx);
7848 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7849 workspace.add_panel(panel_2.clone(), window, cx);
7850 workspace.toggle_dock(DockPosition::Right, window, cx);
7851
7852 let left_dock = workspace.left_dock();
7853 assert_eq!(
7854 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7855 panel_1.panel_id()
7856 );
7857 assert_eq!(
7858 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7859 panel_1.size(window, cx)
7860 );
7861
7862 left_dock.update(cx, |left_dock, cx| {
7863 left_dock.resize_active_panel(Some(px(1337.)), window, cx)
7864 });
7865 assert_eq!(
7866 workspace
7867 .right_dock()
7868 .read(cx)
7869 .visible_panel()
7870 .unwrap()
7871 .panel_id(),
7872 panel_2.panel_id(),
7873 );
7874
7875 (panel_1, panel_2)
7876 });
7877
7878 // Move panel_1 to the right
7879 panel_1.update_in(cx, |panel_1, window, cx| {
7880 panel_1.set_position(DockPosition::Right, window, cx)
7881 });
7882
7883 workspace.update_in(cx, |workspace, window, cx| {
7884 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7885 // Since it was the only panel on the left, the left dock should now be closed.
7886 assert!(!workspace.left_dock().read(cx).is_open());
7887 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7888 let right_dock = workspace.right_dock();
7889 assert_eq!(
7890 right_dock.read(cx).visible_panel().unwrap().panel_id(),
7891 panel_1.panel_id()
7892 );
7893 assert_eq!(
7894 right_dock.read(cx).active_panel_size(window, cx).unwrap(),
7895 px(1337.)
7896 );
7897
7898 // Now we move panel_2 to the left
7899 panel_2.set_position(DockPosition::Left, window, cx);
7900 });
7901
7902 workspace.update(cx, |workspace, cx| {
7903 // Since panel_2 was not visible on the right, we don't open the left dock.
7904 assert!(!workspace.left_dock().read(cx).is_open());
7905 // And the right dock is unaffected in its displaying of panel_1
7906 assert!(workspace.right_dock().read(cx).is_open());
7907 assert_eq!(
7908 workspace
7909 .right_dock()
7910 .read(cx)
7911 .visible_panel()
7912 .unwrap()
7913 .panel_id(),
7914 panel_1.panel_id(),
7915 );
7916 });
7917
7918 // Move panel_1 back to the left
7919 panel_1.update_in(cx, |panel_1, window, cx| {
7920 panel_1.set_position(DockPosition::Left, window, cx)
7921 });
7922
7923 workspace.update_in(cx, |workspace, window, cx| {
7924 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7925 let left_dock = workspace.left_dock();
7926 assert!(left_dock.read(cx).is_open());
7927 assert_eq!(
7928 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7929 panel_1.panel_id()
7930 );
7931 assert_eq!(
7932 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7933 px(1337.)
7934 );
7935 // And the right dock should be closed as it no longer has any panels.
7936 assert!(!workspace.right_dock().read(cx).is_open());
7937
7938 // Now we move panel_1 to the bottom
7939 panel_1.set_position(DockPosition::Bottom, window, cx);
7940 });
7941
7942 workspace.update_in(cx, |workspace, window, cx| {
7943 // Since panel_1 was visible on the left, we close the left dock.
7944 assert!(!workspace.left_dock().read(cx).is_open());
7945 // The bottom dock is sized based on the panel's default size,
7946 // since the panel orientation changed from vertical to horizontal.
7947 let bottom_dock = workspace.bottom_dock();
7948 assert_eq!(
7949 bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
7950 panel_1.size(window, cx),
7951 );
7952 // Close bottom dock and move panel_1 back to the left.
7953 bottom_dock.update(cx, |bottom_dock, cx| {
7954 bottom_dock.set_open(false, window, cx)
7955 });
7956 panel_1.set_position(DockPosition::Left, window, cx);
7957 });
7958
7959 // Emit activated event on panel 1
7960 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7961
7962 // Now the left dock is open and panel_1 is active and focused.
7963 workspace.update_in(cx, |workspace, window, cx| {
7964 let left_dock = workspace.left_dock();
7965 assert!(left_dock.read(cx).is_open());
7966 assert_eq!(
7967 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7968 panel_1.panel_id(),
7969 );
7970 assert!(panel_1.focus_handle(cx).is_focused(window));
7971 });
7972
7973 // Emit closed event on panel 2, which is not active
7974 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7975
7976 // Wo don't close the left dock, because panel_2 wasn't the active panel
7977 workspace.update(cx, |workspace, cx| {
7978 let left_dock = workspace.left_dock();
7979 assert!(left_dock.read(cx).is_open());
7980 assert_eq!(
7981 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7982 panel_1.panel_id(),
7983 );
7984 });
7985
7986 // Emitting a ZoomIn event shows the panel as zoomed.
7987 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7988 workspace.update(cx, |workspace, _| {
7989 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7990 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7991 });
7992
7993 // Move panel to another dock while it is zoomed
7994 panel_1.update_in(cx, |panel, window, cx| {
7995 panel.set_position(DockPosition::Right, window, cx)
7996 });
7997 workspace.update(cx, |workspace, _| {
7998 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7999
8000 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8001 });
8002
8003 // This is a helper for getting a:
8004 // - valid focus on an element,
8005 // - that isn't a part of the panes and panels system of the Workspace,
8006 // - and doesn't trigger the 'on_focus_lost' API.
8007 let focus_other_view = {
8008 let workspace = workspace.clone();
8009 move |cx: &mut VisualTestContext| {
8010 workspace.update_in(cx, |workspace, window, cx| {
8011 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
8012 workspace.toggle_modal(window, cx, TestModal::new);
8013 workspace.toggle_modal(window, cx, TestModal::new);
8014 } else {
8015 workspace.toggle_modal(window, cx, TestModal::new);
8016 }
8017 })
8018 }
8019 };
8020
8021 // If focus is transferred to another view that's not a panel or another pane, we still show
8022 // the panel as zoomed.
8023 focus_other_view(cx);
8024 workspace.update(cx, |workspace, _| {
8025 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8026 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8027 });
8028
8029 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8030 workspace.update_in(cx, |_workspace, window, cx| {
8031 cx.focus_self(window);
8032 });
8033 workspace.update(cx, |workspace, _| {
8034 assert_eq!(workspace.zoomed, None);
8035 assert_eq!(workspace.zoomed_position, None);
8036 });
8037
8038 // If focus is transferred again to another view that's not a panel or a pane, we won't
8039 // show the panel as zoomed because it wasn't zoomed before.
8040 focus_other_view(cx);
8041 workspace.update(cx, |workspace, _| {
8042 assert_eq!(workspace.zoomed, None);
8043 assert_eq!(workspace.zoomed_position, None);
8044 });
8045
8046 // When the panel is activated, it is zoomed again.
8047 cx.dispatch_action(ToggleRightDock);
8048 workspace.update(cx, |workspace, _| {
8049 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8050 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8051 });
8052
8053 // Emitting a ZoomOut event unzooms the panel.
8054 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8055 workspace.update(cx, |workspace, _| {
8056 assert_eq!(workspace.zoomed, None);
8057 assert_eq!(workspace.zoomed_position, None);
8058 });
8059
8060 // Emit closed event on panel 1, which is active
8061 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8062
8063 // Now the left dock is closed, because panel_1 was the active panel
8064 workspace.update(cx, |workspace, cx| {
8065 let right_dock = workspace.right_dock();
8066 assert!(!right_dock.read(cx).is_open());
8067 });
8068 }
8069
8070 #[gpui::test]
8071 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8072 init_test(cx);
8073
8074 let fs = FakeFs::new(cx.background_executor.clone());
8075 let project = Project::test(fs, [], cx).await;
8076 let (workspace, cx) =
8077 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8078 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8079
8080 let dirty_regular_buffer = cx.new(|cx| {
8081 TestItem::new(cx)
8082 .with_dirty(true)
8083 .with_label("1.txt")
8084 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8085 });
8086 let dirty_regular_buffer_2 = cx.new(|cx| {
8087 TestItem::new(cx)
8088 .with_dirty(true)
8089 .with_label("2.txt")
8090 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8091 });
8092 let dirty_multi_buffer_with_both = cx.new(|cx| {
8093 TestItem::new(cx)
8094 .with_dirty(true)
8095 .with_singleton(false)
8096 .with_label("Fake Project Search")
8097 .with_project_items(&[
8098 dirty_regular_buffer.read(cx).project_items[0].clone(),
8099 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8100 ])
8101 });
8102 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8103 workspace.update_in(cx, |workspace, window, cx| {
8104 workspace.add_item(
8105 pane.clone(),
8106 Box::new(dirty_regular_buffer.clone()),
8107 None,
8108 false,
8109 false,
8110 window,
8111 cx,
8112 );
8113 workspace.add_item(
8114 pane.clone(),
8115 Box::new(dirty_regular_buffer_2.clone()),
8116 None,
8117 false,
8118 false,
8119 window,
8120 cx,
8121 );
8122 workspace.add_item(
8123 pane.clone(),
8124 Box::new(dirty_multi_buffer_with_both.clone()),
8125 None,
8126 false,
8127 false,
8128 window,
8129 cx,
8130 );
8131 });
8132
8133 pane.update_in(cx, |pane, window, cx| {
8134 pane.activate_item(2, true, true, window, cx);
8135 assert_eq!(
8136 pane.active_item().unwrap().item_id(),
8137 multi_buffer_with_both_files_id,
8138 "Should select the multi buffer in the pane"
8139 );
8140 });
8141 let close_all_but_multi_buffer_task = pane
8142 .update_in(cx, |pane, window, cx| {
8143 pane.close_inactive_items(
8144 &CloseInactiveItems {
8145 save_intent: Some(SaveIntent::Save),
8146 close_pinned: true,
8147 },
8148 window,
8149 cx,
8150 )
8151 })
8152 .expect("should have inactive files to close");
8153 cx.background_executor.run_until_parked();
8154 assert!(
8155 !cx.has_pending_prompt(),
8156 "Multi buffer still has the unsaved buffer inside, so no save prompt should be shown"
8157 );
8158 close_all_but_multi_buffer_task
8159 .await
8160 .expect("Closing all buffers but the multi buffer failed");
8161 pane.update(cx, |pane, cx| {
8162 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8163 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
8164 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8165 assert_eq!(pane.items_len(), 1);
8166 assert_eq!(
8167 pane.active_item().unwrap().item_id(),
8168 multi_buffer_with_both_files_id,
8169 "Should have only the multi buffer left in the pane"
8170 );
8171 assert!(
8172 dirty_multi_buffer_with_both.read(cx).is_dirty,
8173 "The multi buffer containing the unsaved buffer should still be dirty"
8174 );
8175 });
8176
8177 let close_multi_buffer_task = pane
8178 .update_in(cx, |pane, window, cx| {
8179 pane.close_active_item(
8180 &CloseActiveItem {
8181 save_intent: Some(SaveIntent::Close),
8182 close_pinned: false,
8183 },
8184 window,
8185 cx,
8186 )
8187 })
8188 .expect("should have the multi buffer to close");
8189 cx.background_executor.run_until_parked();
8190 assert!(
8191 cx.has_pending_prompt(),
8192 "Dirty multi buffer should prompt a save dialog"
8193 );
8194 cx.simulate_prompt_answer(0);
8195 cx.background_executor.run_until_parked();
8196 close_multi_buffer_task
8197 .await
8198 .expect("Closing the multi buffer failed");
8199 pane.update(cx, |pane, cx| {
8200 assert_eq!(
8201 dirty_multi_buffer_with_both.read(cx).save_count,
8202 1,
8203 "Multi buffer item should get be saved"
8204 );
8205 // Test impl does not save inner items, so we do not assert them
8206 assert_eq!(
8207 pane.items_len(),
8208 0,
8209 "No more items should be left in the pane"
8210 );
8211 assert!(pane.active_item().is_none());
8212 });
8213 }
8214
8215 #[gpui::test]
8216 async fn test_no_save_prompt_when_dirty_singleton_buffer_closed_with_a_multi_buffer_containing_it_present_in_the_pane(
8217 cx: &mut TestAppContext,
8218 ) {
8219 init_test(cx);
8220
8221 let fs = FakeFs::new(cx.background_executor.clone());
8222 let project = Project::test(fs, [], cx).await;
8223 let (workspace, cx) =
8224 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8225 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8226
8227 let dirty_regular_buffer = cx.new(|cx| {
8228 TestItem::new(cx)
8229 .with_dirty(true)
8230 .with_label("1.txt")
8231 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8232 });
8233 let dirty_regular_buffer_2 = cx.new(|cx| {
8234 TestItem::new(cx)
8235 .with_dirty(true)
8236 .with_label("2.txt")
8237 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8238 });
8239 let clear_regular_buffer = cx.new(|cx| {
8240 TestItem::new(cx)
8241 .with_label("3.txt")
8242 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8243 });
8244
8245 let dirty_multi_buffer_with_both = cx.new(|cx| {
8246 TestItem::new(cx)
8247 .with_dirty(true)
8248 .with_singleton(false)
8249 .with_label("Fake Project Search")
8250 .with_project_items(&[
8251 dirty_regular_buffer.read(cx).project_items[0].clone(),
8252 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8253 clear_regular_buffer.read(cx).project_items[0].clone(),
8254 ])
8255 });
8256 workspace.update_in(cx, |workspace, window, cx| {
8257 workspace.add_item(
8258 pane.clone(),
8259 Box::new(dirty_regular_buffer.clone()),
8260 None,
8261 false,
8262 false,
8263 window,
8264 cx,
8265 );
8266 workspace.add_item(
8267 pane.clone(),
8268 Box::new(dirty_multi_buffer_with_both.clone()),
8269 None,
8270 false,
8271 false,
8272 window,
8273 cx,
8274 );
8275 });
8276
8277 pane.update_in(cx, |pane, window, cx| {
8278 pane.activate_item(0, true, true, window, cx);
8279 assert_eq!(
8280 pane.active_item().unwrap().item_id(),
8281 dirty_regular_buffer.item_id(),
8282 "Should select the dirty singleton buffer in the pane"
8283 );
8284 });
8285 let close_singleton_buffer_task = pane
8286 .update_in(cx, |pane, window, cx| {
8287 pane.close_active_item(
8288 &CloseActiveItem {
8289 save_intent: None,
8290 close_pinned: false,
8291 },
8292 window,
8293 cx,
8294 )
8295 })
8296 .expect("should have active singleton buffer to close");
8297 cx.background_executor.run_until_parked();
8298 assert!(
8299 !cx.has_pending_prompt(),
8300 "Multi buffer is still in the pane and has the unsaved buffer inside, so no save prompt should be shown"
8301 );
8302
8303 close_singleton_buffer_task
8304 .await
8305 .expect("Should not fail closing the singleton buffer");
8306 pane.update(cx, |pane, cx| {
8307 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8308 assert_eq!(
8309 dirty_multi_buffer_with_both.read(cx).save_count,
8310 0,
8311 "Multi buffer itself should not be saved"
8312 );
8313 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8314 assert_eq!(
8315 pane.items_len(),
8316 1,
8317 "A dirty multi buffer should be present in the pane"
8318 );
8319 assert_eq!(
8320 pane.active_item().unwrap().item_id(),
8321 dirty_multi_buffer_with_both.item_id(),
8322 "Should activate the only remaining item in the pane"
8323 );
8324 });
8325 }
8326
8327 #[gpui::test]
8328 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
8329 cx: &mut TestAppContext,
8330 ) {
8331 init_test(cx);
8332
8333 let fs = FakeFs::new(cx.background_executor.clone());
8334 let project = Project::test(fs, [], cx).await;
8335 let (workspace, cx) =
8336 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8337 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8338
8339 let dirty_regular_buffer = cx.new(|cx| {
8340 TestItem::new(cx)
8341 .with_dirty(true)
8342 .with_label("1.txt")
8343 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8344 });
8345 let dirty_regular_buffer_2 = cx.new(|cx| {
8346 TestItem::new(cx)
8347 .with_dirty(true)
8348 .with_label("2.txt")
8349 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8350 });
8351 let clear_regular_buffer = cx.new(|cx| {
8352 TestItem::new(cx)
8353 .with_label("3.txt")
8354 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8355 });
8356
8357 let dirty_multi_buffer_with_both = cx.new(|cx| {
8358 TestItem::new(cx)
8359 .with_dirty(true)
8360 .with_singleton(false)
8361 .with_label("Fake Project Search")
8362 .with_project_items(&[
8363 dirty_regular_buffer.read(cx).project_items[0].clone(),
8364 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8365 clear_regular_buffer.read(cx).project_items[0].clone(),
8366 ])
8367 });
8368 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8369 workspace.update_in(cx, |workspace, window, cx| {
8370 workspace.add_item(
8371 pane.clone(),
8372 Box::new(dirty_regular_buffer.clone()),
8373 None,
8374 false,
8375 false,
8376 window,
8377 cx,
8378 );
8379 workspace.add_item(
8380 pane.clone(),
8381 Box::new(dirty_multi_buffer_with_both.clone()),
8382 None,
8383 false,
8384 false,
8385 window,
8386 cx,
8387 );
8388 });
8389
8390 pane.update_in(cx, |pane, window, cx| {
8391 pane.activate_item(1, true, true, window, cx);
8392 assert_eq!(
8393 pane.active_item().unwrap().item_id(),
8394 multi_buffer_with_both_files_id,
8395 "Should select the multi buffer in the pane"
8396 );
8397 });
8398 let _close_multi_buffer_task = pane
8399 .update_in(cx, |pane, window, cx| {
8400 pane.close_active_item(
8401 &CloseActiveItem {
8402 save_intent: None,
8403 close_pinned: false,
8404 },
8405 window,
8406 cx,
8407 )
8408 })
8409 .expect("should have active multi buffer to close");
8410 cx.background_executor.run_until_parked();
8411 assert!(
8412 cx.has_pending_prompt(),
8413 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
8414 );
8415 }
8416
8417 #[gpui::test]
8418 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
8419 cx: &mut TestAppContext,
8420 ) {
8421 init_test(cx);
8422
8423 let fs = FakeFs::new(cx.background_executor.clone());
8424 let project = Project::test(fs, [], cx).await;
8425 let (workspace, cx) =
8426 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8427 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8428
8429 let dirty_regular_buffer = cx.new(|cx| {
8430 TestItem::new(cx)
8431 .with_dirty(true)
8432 .with_label("1.txt")
8433 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8434 });
8435 let dirty_regular_buffer_2 = cx.new(|cx| {
8436 TestItem::new(cx)
8437 .with_dirty(true)
8438 .with_label("2.txt")
8439 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8440 });
8441 let clear_regular_buffer = cx.new(|cx| {
8442 TestItem::new(cx)
8443 .with_label("3.txt")
8444 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8445 });
8446
8447 let dirty_multi_buffer = cx.new(|cx| {
8448 TestItem::new(cx)
8449 .with_dirty(true)
8450 .with_singleton(false)
8451 .with_label("Fake Project Search")
8452 .with_project_items(&[
8453 dirty_regular_buffer.read(cx).project_items[0].clone(),
8454 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8455 clear_regular_buffer.read(cx).project_items[0].clone(),
8456 ])
8457 });
8458 workspace.update_in(cx, |workspace, window, cx| {
8459 workspace.add_item(
8460 pane.clone(),
8461 Box::new(dirty_regular_buffer.clone()),
8462 None,
8463 false,
8464 false,
8465 window,
8466 cx,
8467 );
8468 workspace.add_item(
8469 pane.clone(),
8470 Box::new(dirty_regular_buffer_2.clone()),
8471 None,
8472 false,
8473 false,
8474 window,
8475 cx,
8476 );
8477 workspace.add_item(
8478 pane.clone(),
8479 Box::new(dirty_multi_buffer.clone()),
8480 None,
8481 false,
8482 false,
8483 window,
8484 cx,
8485 );
8486 });
8487
8488 pane.update_in(cx, |pane, window, cx| {
8489 pane.activate_item(2, true, true, window, cx);
8490 assert_eq!(
8491 pane.active_item().unwrap().item_id(),
8492 dirty_multi_buffer.item_id(),
8493 "Should select the multi buffer in the pane"
8494 );
8495 });
8496 let close_multi_buffer_task = pane
8497 .update_in(cx, |pane, window, cx| {
8498 pane.close_active_item(
8499 &CloseActiveItem {
8500 save_intent: None,
8501 close_pinned: false,
8502 },
8503 window,
8504 cx,
8505 )
8506 })
8507 .expect("should have active multi buffer to close");
8508 cx.background_executor.run_until_parked();
8509 assert!(
8510 !cx.has_pending_prompt(),
8511 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
8512 );
8513 close_multi_buffer_task
8514 .await
8515 .expect("Closing multi buffer failed");
8516 pane.update(cx, |pane, cx| {
8517 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8518 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
8519 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8520 assert_eq!(
8521 pane.items()
8522 .map(|item| item.item_id())
8523 .sorted()
8524 .collect::<Vec<_>>(),
8525 vec![
8526 dirty_regular_buffer.item_id(),
8527 dirty_regular_buffer_2.item_id(),
8528 ],
8529 "Should have no multi buffer left in the pane"
8530 );
8531 assert!(dirty_regular_buffer.read(cx).is_dirty);
8532 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
8533 });
8534 }
8535
8536 #[gpui::test]
8537 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
8538 init_test(cx);
8539 let fs = FakeFs::new(cx.executor());
8540 let project = Project::test(fs, [], cx).await;
8541 let (workspace, cx) =
8542 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8543
8544 // Add a new panel to the right dock, opening the dock and setting the
8545 // focus to the new panel.
8546 let panel = workspace.update_in(cx, |workspace, window, cx| {
8547 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8548 workspace.add_panel(panel.clone(), window, cx);
8549
8550 workspace
8551 .right_dock()
8552 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8553
8554 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8555
8556 panel
8557 });
8558
8559 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8560 // panel to the next valid position which, in this case, is the left
8561 // dock.
8562 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8563 workspace.update(cx, |workspace, cx| {
8564 assert!(workspace.left_dock().read(cx).is_open());
8565 assert_eq!(panel.read(cx).position, DockPosition::Left);
8566 });
8567
8568 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8569 // panel to the next valid position which, in this case, is the bottom
8570 // dock.
8571 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8572 workspace.update(cx, |workspace, cx| {
8573 assert!(workspace.bottom_dock().read(cx).is_open());
8574 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
8575 });
8576
8577 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
8578 // around moving the panel to its initial position, the right dock.
8579 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8580 workspace.update(cx, |workspace, cx| {
8581 assert!(workspace.right_dock().read(cx).is_open());
8582 assert_eq!(panel.read(cx).position, DockPosition::Right);
8583 });
8584
8585 // Remove focus from the panel, ensuring that, if the panel is not
8586 // focused, the `MoveFocusedPanelToNextPosition` action does not update
8587 // the panel's position, so the panel is still in the right dock.
8588 workspace.update_in(cx, |workspace, window, cx| {
8589 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8590 });
8591
8592 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8593 workspace.update(cx, |workspace, cx| {
8594 assert!(workspace.right_dock().read(cx).is_open());
8595 assert_eq!(panel.read(cx).position, DockPosition::Right);
8596 });
8597 }
8598
8599 mod register_project_item_tests {
8600
8601 use super::*;
8602
8603 // View
8604 struct TestPngItemView {
8605 focus_handle: FocusHandle,
8606 }
8607 // Model
8608 struct TestPngItem {}
8609
8610 impl project::ProjectItem for TestPngItem {
8611 fn try_open(
8612 _project: &Entity<Project>,
8613 path: &ProjectPath,
8614 cx: &mut App,
8615 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8616 if path.path.extension().unwrap() == "png" {
8617 Some(cx.spawn(|mut cx| async move { cx.new(|_| TestPngItem {}) }))
8618 } else {
8619 None
8620 }
8621 }
8622
8623 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8624 None
8625 }
8626
8627 fn project_path(&self, _: &App) -> Option<ProjectPath> {
8628 None
8629 }
8630
8631 fn is_dirty(&self) -> bool {
8632 false
8633 }
8634 }
8635
8636 impl Item for TestPngItemView {
8637 type Event = ();
8638 }
8639 impl EventEmitter<()> for TestPngItemView {}
8640 impl Focusable for TestPngItemView {
8641 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8642 self.focus_handle.clone()
8643 }
8644 }
8645
8646 impl Render for TestPngItemView {
8647 fn render(
8648 &mut self,
8649 _window: &mut Window,
8650 _cx: &mut Context<Self>,
8651 ) -> impl IntoElement {
8652 Empty
8653 }
8654 }
8655
8656 impl ProjectItem for TestPngItemView {
8657 type Item = TestPngItem;
8658
8659 fn for_project_item(
8660 _project: Entity<Project>,
8661 _item: Entity<Self::Item>,
8662 _: &mut Window,
8663 cx: &mut Context<Self>,
8664 ) -> Self
8665 where
8666 Self: Sized,
8667 {
8668 Self {
8669 focus_handle: cx.focus_handle(),
8670 }
8671 }
8672 }
8673
8674 // View
8675 struct TestIpynbItemView {
8676 focus_handle: FocusHandle,
8677 }
8678 // Model
8679 struct TestIpynbItem {}
8680
8681 impl project::ProjectItem for TestIpynbItem {
8682 fn try_open(
8683 _project: &Entity<Project>,
8684 path: &ProjectPath,
8685 cx: &mut App,
8686 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8687 if path.path.extension().unwrap() == "ipynb" {
8688 Some(cx.spawn(|mut cx| async move { cx.new(|_| TestIpynbItem {}) }))
8689 } else {
8690 None
8691 }
8692 }
8693
8694 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8695 None
8696 }
8697
8698 fn project_path(&self, _: &App) -> Option<ProjectPath> {
8699 None
8700 }
8701
8702 fn is_dirty(&self) -> bool {
8703 false
8704 }
8705 }
8706
8707 impl Item for TestIpynbItemView {
8708 type Event = ();
8709 }
8710 impl EventEmitter<()> for TestIpynbItemView {}
8711 impl Focusable for TestIpynbItemView {
8712 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8713 self.focus_handle.clone()
8714 }
8715 }
8716
8717 impl Render for TestIpynbItemView {
8718 fn render(
8719 &mut self,
8720 _window: &mut Window,
8721 _cx: &mut Context<Self>,
8722 ) -> impl IntoElement {
8723 Empty
8724 }
8725 }
8726
8727 impl ProjectItem for TestIpynbItemView {
8728 type Item = TestIpynbItem;
8729
8730 fn for_project_item(
8731 _project: Entity<Project>,
8732 _item: Entity<Self::Item>,
8733 _: &mut Window,
8734 cx: &mut Context<Self>,
8735 ) -> Self
8736 where
8737 Self: Sized,
8738 {
8739 Self {
8740 focus_handle: cx.focus_handle(),
8741 }
8742 }
8743 }
8744
8745 struct TestAlternatePngItemView {
8746 focus_handle: FocusHandle,
8747 }
8748
8749 impl Item for TestAlternatePngItemView {
8750 type Event = ();
8751 }
8752
8753 impl EventEmitter<()> for TestAlternatePngItemView {}
8754 impl Focusable for TestAlternatePngItemView {
8755 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8756 self.focus_handle.clone()
8757 }
8758 }
8759
8760 impl Render for TestAlternatePngItemView {
8761 fn render(
8762 &mut self,
8763 _window: &mut Window,
8764 _cx: &mut Context<Self>,
8765 ) -> impl IntoElement {
8766 Empty
8767 }
8768 }
8769
8770 impl ProjectItem for TestAlternatePngItemView {
8771 type Item = TestPngItem;
8772
8773 fn for_project_item(
8774 _project: Entity<Project>,
8775 _item: Entity<Self::Item>,
8776 _: &mut Window,
8777 cx: &mut Context<Self>,
8778 ) -> Self
8779 where
8780 Self: Sized,
8781 {
8782 Self {
8783 focus_handle: cx.focus_handle(),
8784 }
8785 }
8786 }
8787
8788 #[gpui::test]
8789 async fn test_register_project_item(cx: &mut TestAppContext) {
8790 init_test(cx);
8791
8792 cx.update(|cx| {
8793 register_project_item::<TestPngItemView>(cx);
8794 register_project_item::<TestIpynbItemView>(cx);
8795 });
8796
8797 let fs = FakeFs::new(cx.executor());
8798 fs.insert_tree(
8799 "/root1",
8800 json!({
8801 "one.png": "BINARYDATAHERE",
8802 "two.ipynb": "{ totally a notebook }",
8803 "three.txt": "editing text, sure why not?"
8804 }),
8805 )
8806 .await;
8807
8808 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8809 let (workspace, cx) =
8810 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8811
8812 let worktree_id = project.update(cx, |project, cx| {
8813 project.worktrees(cx).next().unwrap().read(cx).id()
8814 });
8815
8816 let handle = workspace
8817 .update_in(cx, |workspace, window, cx| {
8818 let project_path = (worktree_id, "one.png");
8819 workspace.open_path(project_path, None, true, window, cx)
8820 })
8821 .await
8822 .unwrap();
8823
8824 // Now we can check if the handle we got back errored or not
8825 assert_eq!(
8826 handle.to_any().entity_type(),
8827 TypeId::of::<TestPngItemView>()
8828 );
8829
8830 let handle = workspace
8831 .update_in(cx, |workspace, window, cx| {
8832 let project_path = (worktree_id, "two.ipynb");
8833 workspace.open_path(project_path, None, true, window, cx)
8834 })
8835 .await
8836 .unwrap();
8837
8838 assert_eq!(
8839 handle.to_any().entity_type(),
8840 TypeId::of::<TestIpynbItemView>()
8841 );
8842
8843 let handle = workspace
8844 .update_in(cx, |workspace, window, cx| {
8845 let project_path = (worktree_id, "three.txt");
8846 workspace.open_path(project_path, None, true, window, cx)
8847 })
8848 .await;
8849 assert!(handle.is_err());
8850 }
8851
8852 #[gpui::test]
8853 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8854 init_test(cx);
8855
8856 cx.update(|cx| {
8857 register_project_item::<TestPngItemView>(cx);
8858 register_project_item::<TestAlternatePngItemView>(cx);
8859 });
8860
8861 let fs = FakeFs::new(cx.executor());
8862 fs.insert_tree(
8863 "/root1",
8864 json!({
8865 "one.png": "BINARYDATAHERE",
8866 "two.ipynb": "{ totally a notebook }",
8867 "three.txt": "editing text, sure why not?"
8868 }),
8869 )
8870 .await;
8871 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8872 let (workspace, cx) =
8873 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8874 let worktree_id = project.update(cx, |project, cx| {
8875 project.worktrees(cx).next().unwrap().read(cx).id()
8876 });
8877
8878 let handle = workspace
8879 .update_in(cx, |workspace, window, cx| {
8880 let project_path = (worktree_id, "one.png");
8881 workspace.open_path(project_path, None, true, window, cx)
8882 })
8883 .await
8884 .unwrap();
8885
8886 // This _must_ be the second item registered
8887 assert_eq!(
8888 handle.to_any().entity_type(),
8889 TypeId::of::<TestAlternatePngItemView>()
8890 );
8891
8892 let handle = workspace
8893 .update_in(cx, |workspace, window, cx| {
8894 let project_path = (worktree_id, "three.txt");
8895 workspace.open_path(project_path, None, true, window, cx)
8896 })
8897 .await;
8898 assert!(handle.is_err());
8899 }
8900 }
8901
8902 pub fn init_test(cx: &mut TestAppContext) {
8903 cx.update(|cx| {
8904 let settings_store = SettingsStore::test(cx);
8905 cx.set_global(settings_store);
8906 theme::init(theme::LoadThemes::JustBase, cx);
8907 language::init(cx);
8908 crate::init_settings(cx);
8909 Project::init_settings(cx);
8910 });
8911 }
8912
8913 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
8914 let item = TestProjectItem::new(id, path, cx);
8915 item.update(cx, |item, _| {
8916 item.is_dirty = true;
8917 });
8918 item
8919 }
8920}