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