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