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