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