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 =
5883 new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE - workspace.bounds.top());
5884 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
5885 bottom_dock.resize_active_panel(Some(size), window, cx);
5886 });
5887}
5888
5889fn resize_right_dock(
5890 new_size: Pixels,
5891 workspace: &mut Workspace,
5892 window: &mut Window,
5893 cx: &mut App,
5894) {
5895 let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
5896 workspace.right_dock.update(cx, |right_dock, cx| {
5897 right_dock.resize_active_panel(Some(size), window, cx);
5898 });
5899}
5900
5901fn resize_left_dock(
5902 new_size: Pixels,
5903 workspace: &mut Workspace,
5904 window: &mut Window,
5905 cx: &mut App,
5906) {
5907 let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5908
5909 workspace.left_dock.update(cx, |left_dock, cx| {
5910 left_dock.resize_active_panel(Some(size), window, cx);
5911 });
5912}
5913
5914impl WorkspaceStore {
5915 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
5916 Self {
5917 workspaces: Default::default(),
5918 _subscriptions: vec![
5919 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
5920 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
5921 ],
5922 client,
5923 }
5924 }
5925
5926 pub fn update_followers(
5927 &self,
5928 project_id: Option<u64>,
5929 update: proto::update_followers::Variant,
5930 cx: &App,
5931 ) -> Option<()> {
5932 let active_call = ActiveCall::try_global(cx)?;
5933 let room_id = active_call.read(cx).room()?.read(cx).id();
5934 self.client
5935 .send(proto::UpdateFollowers {
5936 room_id,
5937 project_id,
5938 variant: Some(update),
5939 })
5940 .log_err()
5941 }
5942
5943 pub async fn handle_follow(
5944 this: Entity<Self>,
5945 envelope: TypedEnvelope<proto::Follow>,
5946 mut cx: AsyncApp,
5947 ) -> Result<proto::FollowResponse> {
5948 this.update(&mut cx, |this, cx| {
5949 let follower = Follower {
5950 project_id: envelope.payload.project_id,
5951 peer_id: envelope.original_sender_id()?,
5952 };
5953
5954 let mut response = proto::FollowResponse::default();
5955 this.workspaces.retain(|workspace| {
5956 workspace
5957 .update(cx, |workspace, window, cx| {
5958 let handler_response =
5959 workspace.handle_follow(follower.project_id, window, cx);
5960 if let Some(active_view) = handler_response.active_view.clone() {
5961 if workspace.project.read(cx).remote_id() == follower.project_id {
5962 response.active_view = Some(active_view)
5963 }
5964 }
5965 })
5966 .is_ok()
5967 });
5968
5969 Ok(response)
5970 })?
5971 }
5972
5973 async fn handle_update_followers(
5974 this: Entity<Self>,
5975 envelope: TypedEnvelope<proto::UpdateFollowers>,
5976 mut cx: AsyncApp,
5977 ) -> Result<()> {
5978 let leader_id = envelope.original_sender_id()?;
5979 let update = envelope.payload;
5980
5981 this.update(&mut cx, |this, cx| {
5982 this.workspaces.retain(|workspace| {
5983 workspace
5984 .update(cx, |workspace, window, cx| {
5985 let project_id = workspace.project.read(cx).remote_id();
5986 if update.project_id != project_id && update.project_id.is_some() {
5987 return;
5988 }
5989 workspace.handle_update_followers(leader_id, update.clone(), window, cx);
5990 })
5991 .is_ok()
5992 });
5993 Ok(())
5994 })?
5995 }
5996}
5997
5998impl ViewId {
5999 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
6000 Ok(Self {
6001 creator: message
6002 .creator
6003 .ok_or_else(|| anyhow!("creator is missing"))?,
6004 id: message.id,
6005 })
6006 }
6007
6008 pub(crate) fn to_proto(self) -> proto::ViewId {
6009 proto::ViewId {
6010 creator: Some(self.creator),
6011 id: self.id,
6012 }
6013 }
6014}
6015
6016impl FollowerState {
6017 fn pane(&self) -> &Entity<Pane> {
6018 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
6019 }
6020}
6021
6022pub trait WorkspaceHandle {
6023 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
6024}
6025
6026impl WorkspaceHandle for Entity<Workspace> {
6027 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
6028 self.read(cx)
6029 .worktrees(cx)
6030 .flat_map(|worktree| {
6031 let worktree_id = worktree.read(cx).id();
6032 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
6033 worktree_id,
6034 path: f.path.clone(),
6035 })
6036 })
6037 .collect::<Vec<_>>()
6038 }
6039}
6040
6041impl std::fmt::Debug for OpenPaths {
6042 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6043 f.debug_struct("OpenPaths")
6044 .field("paths", &self.paths)
6045 .finish()
6046 }
6047}
6048
6049pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
6050 DB.last_workspace().await.log_err().flatten()
6051}
6052
6053pub fn last_session_workspace_locations(
6054 last_session_id: &str,
6055 last_session_window_stack: Option<Vec<WindowId>>,
6056) -> Option<Vec<SerializedWorkspaceLocation>> {
6057 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
6058 .log_err()
6059}
6060
6061actions!(
6062 collab,
6063 [
6064 OpenChannelNotes,
6065 Mute,
6066 Deafen,
6067 LeaveCall,
6068 ShareProject,
6069 ScreenShare
6070 ]
6071);
6072actions!(zed, [OpenLog]);
6073
6074async fn join_channel_internal(
6075 channel_id: ChannelId,
6076 app_state: &Arc<AppState>,
6077 requesting_window: Option<WindowHandle<Workspace>>,
6078 active_call: &Entity<ActiveCall>,
6079 cx: &mut AsyncApp,
6080) -> Result<bool> {
6081 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
6082 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
6083 return (false, None);
6084 };
6085
6086 let already_in_channel = room.channel_id() == Some(channel_id);
6087 let should_prompt = room.is_sharing_project()
6088 && !room.remote_participants().is_empty()
6089 && !already_in_channel;
6090 let open_room = if already_in_channel {
6091 active_call.room().cloned()
6092 } else {
6093 None
6094 };
6095 (should_prompt, open_room)
6096 })?;
6097
6098 if let Some(room) = open_room {
6099 let task = room.update(cx, |room, cx| {
6100 if let Some((project, host)) = room.most_active_project(cx) {
6101 return Some(join_in_room_project(project, host, app_state.clone(), cx));
6102 }
6103
6104 None
6105 })?;
6106 if let Some(task) = task {
6107 task.await?;
6108 }
6109 return anyhow::Ok(true);
6110 }
6111
6112 if should_prompt {
6113 if let Some(workspace) = requesting_window {
6114 let answer = workspace
6115 .update(cx, |_, window, cx| {
6116 window.prompt(
6117 PromptLevel::Warning,
6118 "Do you want to switch channels?",
6119 Some("Leaving this call will unshare your current project."),
6120 &["Yes, Join Channel", "Cancel"],
6121 cx,
6122 )
6123 })?
6124 .await;
6125
6126 if answer == Ok(1) {
6127 return Ok(false);
6128 }
6129 } else {
6130 return Ok(false); // unreachable!() hopefully
6131 }
6132 }
6133
6134 let client = cx.update(|cx| active_call.read(cx).client())?;
6135
6136 let mut client_status = client.status();
6137
6138 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
6139 'outer: loop {
6140 let Some(status) = client_status.recv().await else {
6141 return Err(anyhow!("error connecting"));
6142 };
6143
6144 match status {
6145 Status::Connecting
6146 | Status::Authenticating
6147 | Status::Reconnecting
6148 | Status::Reauthenticating => continue,
6149 Status::Connected { .. } => break 'outer,
6150 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
6151 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
6152 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
6153 return Err(ErrorCode::Disconnected.into());
6154 }
6155 }
6156 }
6157
6158 let room = active_call
6159 .update(cx, |active_call, cx| {
6160 active_call.join_channel(channel_id, cx)
6161 })?
6162 .await?;
6163
6164 let Some(room) = room else {
6165 return anyhow::Ok(true);
6166 };
6167
6168 room.update(cx, |room, _| room.room_update_completed())?
6169 .await;
6170
6171 let task = room.update(cx, |room, cx| {
6172 if let Some((project, host)) = room.most_active_project(cx) {
6173 return Some(join_in_room_project(project, host, app_state.clone(), cx));
6174 }
6175
6176 // If you are the first to join a channel, see if you should share your project.
6177 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
6178 if let Some(workspace) = requesting_window {
6179 let project = workspace.update(cx, |workspace, _, cx| {
6180 let project = workspace.project.read(cx);
6181
6182 if !CallSettings::get_global(cx).share_on_join {
6183 return None;
6184 }
6185
6186 if (project.is_local() || project.is_via_ssh())
6187 && project.visible_worktrees(cx).any(|tree| {
6188 tree.read(cx)
6189 .root_entry()
6190 .map_or(false, |entry| entry.is_dir())
6191 })
6192 {
6193 Some(workspace.project.clone())
6194 } else {
6195 None
6196 }
6197 });
6198 if let Ok(Some(project)) = project {
6199 return Some(cx.spawn(async move |room, cx| {
6200 room.update(cx, |room, cx| room.share_project(project, cx))?
6201 .await?;
6202 Ok(())
6203 }));
6204 }
6205 }
6206 }
6207
6208 None
6209 })?;
6210 if let Some(task) = task {
6211 task.await?;
6212 return anyhow::Ok(true);
6213 }
6214 anyhow::Ok(false)
6215}
6216
6217pub fn join_channel(
6218 channel_id: ChannelId,
6219 app_state: Arc<AppState>,
6220 requesting_window: Option<WindowHandle<Workspace>>,
6221 cx: &mut App,
6222) -> Task<Result<()>> {
6223 let active_call = ActiveCall::global(cx);
6224 cx.spawn(async move |cx| {
6225 let result = join_channel_internal(
6226 channel_id,
6227 &app_state,
6228 requesting_window,
6229 &active_call,
6230 cx,
6231 )
6232 .await;
6233
6234 // join channel succeeded, and opened a window
6235 if matches!(result, Ok(true)) {
6236 return anyhow::Ok(());
6237 }
6238
6239 // find an existing workspace to focus and show call controls
6240 let mut active_window =
6241 requesting_window.or_else(|| activate_any_workspace_window( cx));
6242 if active_window.is_none() {
6243 // no open workspaces, make one to show the error in (blergh)
6244 let (window_handle, _) = cx
6245 .update(|cx| {
6246 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
6247 })?
6248 .await?;
6249
6250 if result.is_ok() {
6251 cx.update(|cx| {
6252 cx.dispatch_action(&OpenChannelNotes);
6253 }).log_err();
6254 }
6255
6256 active_window = Some(window_handle);
6257 }
6258
6259 if let Err(err) = result {
6260 log::error!("failed to join channel: {}", err);
6261 if let Some(active_window) = active_window {
6262 active_window
6263 .update(cx, |_, window, cx| {
6264 let detail: SharedString = match err.error_code() {
6265 ErrorCode::SignedOut => {
6266 "Please sign in to continue.".into()
6267 }
6268 ErrorCode::UpgradeRequired => {
6269 "Your are running an unsupported version of Zed. Please update to continue.".into()
6270 }
6271 ErrorCode::NoSuchChannel => {
6272 "No matching channel was found. Please check the link and try again.".into()
6273 }
6274 ErrorCode::Forbidden => {
6275 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
6276 }
6277 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
6278 _ => format!("{}\n\nPlease try again.", err).into(),
6279 };
6280 window.prompt(
6281 PromptLevel::Critical,
6282 "Failed to join channel",
6283 Some(&detail),
6284 &["Ok"],
6285 cx)
6286 })?
6287 .await
6288 .ok();
6289 }
6290 }
6291
6292 // return ok, we showed the error to the user.
6293 anyhow::Ok(())
6294 })
6295}
6296
6297pub async fn get_any_active_workspace(
6298 app_state: Arc<AppState>,
6299 mut cx: AsyncApp,
6300) -> anyhow::Result<WindowHandle<Workspace>> {
6301 // find an existing workspace to focus and show call controls
6302 let active_window = activate_any_workspace_window(&mut cx);
6303 if active_window.is_none() {
6304 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
6305 .await?;
6306 }
6307 activate_any_workspace_window(&mut cx).context("could not open zed")
6308}
6309
6310fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
6311 cx.update(|cx| {
6312 if let Some(workspace_window) = cx
6313 .active_window()
6314 .and_then(|window| window.downcast::<Workspace>())
6315 {
6316 return Some(workspace_window);
6317 }
6318
6319 for window in cx.windows() {
6320 if let Some(workspace_window) = window.downcast::<Workspace>() {
6321 workspace_window
6322 .update(cx, |_, window, _| window.activate_window())
6323 .ok();
6324 return Some(workspace_window);
6325 }
6326 }
6327 None
6328 })
6329 .ok()
6330 .flatten()
6331}
6332
6333pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
6334 cx.windows()
6335 .into_iter()
6336 .filter_map(|window| window.downcast::<Workspace>())
6337 .filter(|workspace| {
6338 workspace
6339 .read(cx)
6340 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
6341 })
6342 .collect()
6343}
6344
6345#[derive(Default)]
6346pub struct OpenOptions {
6347 pub visible: Option<OpenVisible>,
6348 pub focus: Option<bool>,
6349 pub open_new_workspace: Option<bool>,
6350 pub replace_window: Option<WindowHandle<Workspace>>,
6351 pub env: Option<HashMap<String, String>>,
6352}
6353
6354#[allow(clippy::type_complexity)]
6355pub fn open_paths(
6356 abs_paths: &[PathBuf],
6357 app_state: Arc<AppState>,
6358 open_options: OpenOptions,
6359 cx: &mut App,
6360) -> Task<
6361 anyhow::Result<(
6362 WindowHandle<Workspace>,
6363 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
6364 )>,
6365> {
6366 let abs_paths = abs_paths.to_vec();
6367 let mut existing = None;
6368 let mut best_match = None;
6369 let mut open_visible = OpenVisible::All;
6370
6371 cx.spawn(async move |cx| {
6372 if open_options.open_new_workspace != Some(true) {
6373 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
6374 let all_metadatas = futures::future::join_all(all_paths)
6375 .await
6376 .into_iter()
6377 .filter_map(|result| result.ok().flatten())
6378 .collect::<Vec<_>>();
6379
6380 cx.update(|cx| {
6381 for window in local_workspace_windows(&cx) {
6382 if let Ok(workspace) = window.read(&cx) {
6383 let m = workspace.project.read(&cx).visibility_for_paths(
6384 &abs_paths,
6385 &all_metadatas,
6386 open_options.open_new_workspace == None,
6387 cx,
6388 );
6389 if m > best_match {
6390 existing = Some(window);
6391 best_match = m;
6392 } else if best_match.is_none()
6393 && open_options.open_new_workspace == Some(false)
6394 {
6395 existing = Some(window)
6396 }
6397 }
6398 }
6399 })?;
6400
6401 if open_options.open_new_workspace.is_none() && existing.is_none() {
6402 if all_metadatas.iter().all(|file| !file.is_dir) {
6403 cx.update(|cx| {
6404 if let Some(window) = cx
6405 .active_window()
6406 .and_then(|window| window.downcast::<Workspace>())
6407 {
6408 if let Ok(workspace) = window.read(cx) {
6409 let project = workspace.project().read(cx);
6410 if project.is_local() && !project.is_via_collab() {
6411 existing = Some(window);
6412 open_visible = OpenVisible::None;
6413 return;
6414 }
6415 }
6416 }
6417 for window in local_workspace_windows(cx) {
6418 if let Ok(workspace) = window.read(cx) {
6419 let project = workspace.project().read(cx);
6420 if project.is_via_collab() {
6421 continue;
6422 }
6423 existing = Some(window);
6424 open_visible = OpenVisible::None;
6425 break;
6426 }
6427 }
6428 })?;
6429 }
6430 }
6431 }
6432
6433 if let Some(existing) = existing {
6434 let open_task = existing
6435 .update(cx, |workspace, window, cx| {
6436 window.activate_window();
6437 workspace.open_paths(
6438 abs_paths,
6439 OpenOptions {
6440 visible: Some(open_visible),
6441 ..Default::default()
6442 },
6443 None,
6444 window,
6445 cx,
6446 )
6447 })?
6448 .await;
6449
6450 _ = existing.update(cx, |workspace, _, cx| {
6451 for item in open_task.iter().flatten() {
6452 if let Err(e) = item {
6453 workspace.show_error(&e, cx);
6454 }
6455 }
6456 });
6457
6458 Ok((existing, open_task))
6459 } else {
6460 cx.update(move |cx| {
6461 Workspace::new_local(
6462 abs_paths,
6463 app_state.clone(),
6464 open_options.replace_window,
6465 open_options.env,
6466 cx,
6467 )
6468 })?
6469 .await
6470 }
6471 })
6472}
6473
6474pub fn open_new(
6475 open_options: OpenOptions,
6476 app_state: Arc<AppState>,
6477 cx: &mut App,
6478 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6479) -> Task<anyhow::Result<()>> {
6480 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6481 cx.spawn(async move |cx| {
6482 let (workspace, opened_paths) = task.await?;
6483 workspace.update(cx, |workspace, window, cx| {
6484 if opened_paths.is_empty() {
6485 init(workspace, window, cx)
6486 }
6487 })?;
6488 Ok(())
6489 })
6490}
6491
6492pub fn create_and_open_local_file(
6493 path: &'static Path,
6494 window: &mut Window,
6495 cx: &mut Context<Workspace>,
6496 default_content: impl 'static + Send + FnOnce() -> Rope,
6497) -> Task<Result<Box<dyn ItemHandle>>> {
6498 cx.spawn_in(window, async move |workspace, cx| {
6499 let fs = workspace.update(cx, |workspace, _| workspace.app_state().fs.clone())?;
6500 if !fs.is_file(path).await {
6501 fs.create_file(path, Default::default()).await?;
6502 fs.save(path, &default_content(), Default::default())
6503 .await?;
6504 }
6505
6506 let mut items = workspace
6507 .update_in(cx, |workspace, window, cx| {
6508 workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6509 workspace.open_paths(
6510 vec![path.to_path_buf()],
6511 OpenOptions {
6512 visible: Some(OpenVisible::None),
6513 ..Default::default()
6514 },
6515 None,
6516 window,
6517 cx,
6518 )
6519 })
6520 })?
6521 .await?
6522 .await;
6523
6524 let item = items.pop().flatten();
6525 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
6526 })
6527}
6528
6529pub fn open_ssh_project_with_new_connection(
6530 window: WindowHandle<Workspace>,
6531 connection_options: SshConnectionOptions,
6532 cancel_rx: oneshot::Receiver<()>,
6533 delegate: Arc<dyn SshClientDelegate>,
6534 app_state: Arc<AppState>,
6535 paths: Vec<PathBuf>,
6536 cx: &mut App,
6537) -> Task<Result<()>> {
6538 cx.spawn(async move |cx| {
6539 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6540 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6541
6542 let session = match cx
6543 .update(|cx| {
6544 remote::SshRemoteClient::new(
6545 ConnectionIdentifier::Workspace(workspace_id.0),
6546 connection_options,
6547 cancel_rx,
6548 delegate,
6549 cx,
6550 )
6551 })?
6552 .await?
6553 {
6554 Some(result) => result,
6555 None => return Ok(()),
6556 };
6557
6558 let project = cx.update(|cx| {
6559 project::Project::ssh(
6560 session,
6561 app_state.client.clone(),
6562 app_state.node_runtime.clone(),
6563 app_state.user_store.clone(),
6564 app_state.languages.clone(),
6565 app_state.fs.clone(),
6566 cx,
6567 )
6568 })?;
6569
6570 open_ssh_project_inner(
6571 project,
6572 paths,
6573 serialized_ssh_project,
6574 workspace_id,
6575 serialized_workspace,
6576 app_state,
6577 window,
6578 cx,
6579 )
6580 .await
6581 })
6582}
6583
6584pub fn open_ssh_project_with_existing_connection(
6585 connection_options: SshConnectionOptions,
6586 project: Entity<Project>,
6587 paths: Vec<PathBuf>,
6588 app_state: Arc<AppState>,
6589 window: WindowHandle<Workspace>,
6590 cx: &mut AsyncApp,
6591) -> Task<Result<()>> {
6592 cx.spawn(async move |cx| {
6593 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6594 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6595
6596 open_ssh_project_inner(
6597 project,
6598 paths,
6599 serialized_ssh_project,
6600 workspace_id,
6601 serialized_workspace,
6602 app_state,
6603 window,
6604 cx,
6605 )
6606 .await
6607 })
6608}
6609
6610async fn open_ssh_project_inner(
6611 project: Entity<Project>,
6612 paths: Vec<PathBuf>,
6613 serialized_ssh_project: SerializedSshProject,
6614 workspace_id: WorkspaceId,
6615 serialized_workspace: Option<SerializedWorkspace>,
6616 app_state: Arc<AppState>,
6617 window: WindowHandle<Workspace>,
6618 cx: &mut AsyncApp,
6619) -> Result<()> {
6620 let toolchains = DB.toolchains(workspace_id).await?;
6621 for (toolchain, worktree_id, path) in toolchains {
6622 project
6623 .update(cx, |this, cx| {
6624 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
6625 })?
6626 .await;
6627 }
6628 let mut project_paths_to_open = vec![];
6629 let mut project_path_errors = vec![];
6630
6631 for path in paths {
6632 let result = cx
6633 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6634 .await;
6635 match result {
6636 Ok((_, project_path)) => {
6637 project_paths_to_open.push((path.clone(), Some(project_path)));
6638 }
6639 Err(error) => {
6640 project_path_errors.push(error);
6641 }
6642 };
6643 }
6644
6645 if project_paths_to_open.is_empty() {
6646 return Err(project_path_errors
6647 .pop()
6648 .unwrap_or_else(|| anyhow!("no paths given")));
6649 }
6650
6651 cx.update_window(window.into(), |_, window, cx| {
6652 window.replace_root(cx, |window, cx| {
6653 telemetry::event!("SSH Project Opened");
6654
6655 let mut workspace =
6656 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6657 workspace.set_serialized_ssh_project(serialized_ssh_project);
6658 workspace.update_history(cx);
6659 workspace
6660 });
6661 })?;
6662
6663 window
6664 .update(cx, |_, window, cx| {
6665 window.activate_window();
6666 open_items(serialized_workspace, project_paths_to_open, window, cx)
6667 })?
6668 .await?;
6669
6670 window.update(cx, |workspace, _, cx| {
6671 for error in project_path_errors {
6672 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6673 if let Some(path) = error.error_tag("path") {
6674 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6675 }
6676 } else {
6677 workspace.show_error(&error, cx)
6678 }
6679 }
6680 })?;
6681
6682 Ok(())
6683}
6684
6685fn serialize_ssh_project(
6686 connection_options: SshConnectionOptions,
6687 paths: Vec<PathBuf>,
6688 cx: &AsyncApp,
6689) -> Task<
6690 Result<(
6691 SerializedSshProject,
6692 WorkspaceId,
6693 Option<SerializedWorkspace>,
6694 )>,
6695> {
6696 cx.background_spawn(async move {
6697 let serialized_ssh_project = persistence::DB
6698 .get_or_create_ssh_project(
6699 connection_options.host.clone(),
6700 connection_options.port,
6701 paths
6702 .iter()
6703 .map(|path| path.to_string_lossy().to_string())
6704 .collect::<Vec<_>>(),
6705 connection_options.username.clone(),
6706 )
6707 .await?;
6708
6709 let serialized_workspace =
6710 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
6711
6712 let workspace_id = if let Some(workspace_id) =
6713 serialized_workspace.as_ref().map(|workspace| workspace.id)
6714 {
6715 workspace_id
6716 } else {
6717 persistence::DB.next_id().await?
6718 };
6719
6720 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
6721 })
6722}
6723
6724pub fn join_in_room_project(
6725 project_id: u64,
6726 follow_user_id: u64,
6727 app_state: Arc<AppState>,
6728 cx: &mut App,
6729) -> Task<Result<()>> {
6730 let windows = cx.windows();
6731 cx.spawn(async move |cx| {
6732 let existing_workspace = windows.into_iter().find_map(|window_handle| {
6733 window_handle
6734 .downcast::<Workspace>()
6735 .and_then(|window_handle| {
6736 window_handle
6737 .update(cx, |workspace, _window, cx| {
6738 if workspace.project().read(cx).remote_id() == Some(project_id) {
6739 Some(window_handle)
6740 } else {
6741 None
6742 }
6743 })
6744 .unwrap_or(None)
6745 })
6746 });
6747
6748 let workspace = if let Some(existing_workspace) = existing_workspace {
6749 existing_workspace
6750 } else {
6751 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
6752 let room = active_call
6753 .read_with(cx, |call, _| call.room().cloned())?
6754 .ok_or_else(|| anyhow!("not in a call"))?;
6755 let project = room
6756 .update(cx, |room, cx| {
6757 room.join_project(
6758 project_id,
6759 app_state.languages.clone(),
6760 app_state.fs.clone(),
6761 cx,
6762 )
6763 })?
6764 .await?;
6765
6766 let window_bounds_override = window_bounds_env_override();
6767 cx.update(|cx| {
6768 let mut options = (app_state.build_window_options)(None, cx);
6769 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
6770 cx.open_window(options, |window, cx| {
6771 cx.new(|cx| {
6772 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
6773 })
6774 })
6775 })??
6776 };
6777
6778 workspace.update(cx, |workspace, window, cx| {
6779 cx.activate(true);
6780 window.activate_window();
6781
6782 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
6783 let follow_peer_id = room
6784 .read(cx)
6785 .remote_participants()
6786 .iter()
6787 .find(|(_, participant)| participant.user.id == follow_user_id)
6788 .map(|(_, p)| p.peer_id)
6789 .or_else(|| {
6790 // If we couldn't follow the given user, follow the host instead.
6791 let collaborator = workspace
6792 .project()
6793 .read(cx)
6794 .collaborators()
6795 .values()
6796 .find(|collaborator| collaborator.is_host)?;
6797 Some(collaborator.peer_id)
6798 });
6799
6800 if let Some(follow_peer_id) = follow_peer_id {
6801 workspace.follow(follow_peer_id, window, cx);
6802 }
6803 }
6804 })?;
6805
6806 anyhow::Ok(())
6807 })
6808}
6809
6810pub fn reload(reload: &Reload, cx: &mut App) {
6811 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
6812 let mut workspace_windows = cx
6813 .windows()
6814 .into_iter()
6815 .filter_map(|window| window.downcast::<Workspace>())
6816 .collect::<Vec<_>>();
6817
6818 // If multiple windows have unsaved changes, and need a save prompt,
6819 // prompt in the active window before switching to a different window.
6820 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
6821
6822 let mut prompt = None;
6823 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
6824 prompt = window
6825 .update(cx, |_, window, cx| {
6826 window.prompt(
6827 PromptLevel::Info,
6828 "Are you sure you want to restart?",
6829 None,
6830 &["Restart", "Cancel"],
6831 cx,
6832 )
6833 })
6834 .ok();
6835 }
6836
6837 let binary_path = reload.binary_path.clone();
6838 cx.spawn(async move |cx| {
6839 if let Some(prompt) = prompt {
6840 let answer = prompt.await?;
6841 if answer != 0 {
6842 return Ok(());
6843 }
6844 }
6845
6846 // If the user cancels any save prompt, then keep the app open.
6847 for window in workspace_windows {
6848 if let Ok(should_close) = window.update(cx, |workspace, window, cx| {
6849 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
6850 }) {
6851 if !should_close.await? {
6852 return Ok(());
6853 }
6854 }
6855 }
6856
6857 cx.update(|cx| cx.restart(binary_path))
6858 })
6859 .detach_and_log_err(cx);
6860}
6861
6862fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
6863 let mut parts = value.split(',');
6864 let x: usize = parts.next()?.parse().ok()?;
6865 let y: usize = parts.next()?.parse().ok()?;
6866 Some(point(px(x as f32), px(y as f32)))
6867}
6868
6869fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
6870 let mut parts = value.split(',');
6871 let width: usize = parts.next()?.parse().ok()?;
6872 let height: usize = parts.next()?.parse().ok()?;
6873 Some(size(px(width as f32), px(height as f32)))
6874}
6875
6876pub fn client_side_decorations(
6877 element: impl IntoElement,
6878 window: &mut Window,
6879 cx: &mut App,
6880) -> Stateful<Div> {
6881 const BORDER_SIZE: Pixels = px(1.0);
6882 let decorations = window.window_decorations();
6883
6884 if matches!(decorations, Decorations::Client { .. }) {
6885 window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6886 }
6887
6888 struct GlobalResizeEdge(ResizeEdge);
6889 impl Global for GlobalResizeEdge {}
6890
6891 div()
6892 .id("window-backdrop")
6893 .bg(transparent_black())
6894 .map(|div| match decorations {
6895 Decorations::Server => div,
6896 Decorations::Client { tiling, .. } => div
6897 .when(!(tiling.top || tiling.right), |div| {
6898 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6899 })
6900 .when(!(tiling.top || tiling.left), |div| {
6901 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6902 })
6903 .when(!(tiling.bottom || tiling.right), |div| {
6904 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6905 })
6906 .when(!(tiling.bottom || tiling.left), |div| {
6907 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6908 })
6909 .when(!tiling.top, |div| {
6910 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6911 })
6912 .when(!tiling.bottom, |div| {
6913 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6914 })
6915 .when(!tiling.left, |div| {
6916 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6917 })
6918 .when(!tiling.right, |div| {
6919 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6920 })
6921 .on_mouse_move(move |e, window, cx| {
6922 let size = window.window_bounds().get_bounds().size;
6923 let pos = e.position;
6924
6925 let new_edge =
6926 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6927
6928 let edge = cx.try_global::<GlobalResizeEdge>();
6929 if new_edge != edge.map(|edge| edge.0) {
6930 window
6931 .window_handle()
6932 .update(cx, |workspace, _, cx| {
6933 cx.notify(workspace.entity_id());
6934 })
6935 .ok();
6936 }
6937 })
6938 .on_mouse_down(MouseButton::Left, move |e, window, _| {
6939 let size = window.window_bounds().get_bounds().size;
6940 let pos = e.position;
6941
6942 let edge = match resize_edge(
6943 pos,
6944 theme::CLIENT_SIDE_DECORATION_SHADOW,
6945 size,
6946 tiling,
6947 ) {
6948 Some(value) => value,
6949 None => return,
6950 };
6951
6952 window.start_window_resize(edge);
6953 }),
6954 })
6955 .size_full()
6956 .child(
6957 div()
6958 .cursor(CursorStyle::Arrow)
6959 .map(|div| match decorations {
6960 Decorations::Server => div,
6961 Decorations::Client { tiling } => div
6962 .border_color(cx.theme().colors().border)
6963 .when(!(tiling.top || tiling.right), |div| {
6964 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6965 })
6966 .when(!(tiling.top || tiling.left), |div| {
6967 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6968 })
6969 .when(!(tiling.bottom || tiling.right), |div| {
6970 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6971 })
6972 .when(!(tiling.bottom || tiling.left), |div| {
6973 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6974 })
6975 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6976 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6977 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6978 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6979 .when(!tiling.is_tiled(), |div| {
6980 div.shadow(smallvec::smallvec![gpui::BoxShadow {
6981 color: Hsla {
6982 h: 0.,
6983 s: 0.,
6984 l: 0.,
6985 a: 0.4,
6986 },
6987 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6988 spread_radius: px(0.),
6989 offset: point(px(0.0), px(0.0)),
6990 }])
6991 }),
6992 })
6993 .on_mouse_move(|_e, _, cx| {
6994 cx.stop_propagation();
6995 })
6996 .size_full()
6997 .child(element),
6998 )
6999 .map(|div| match decorations {
7000 Decorations::Server => div,
7001 Decorations::Client { tiling, .. } => div.child(
7002 canvas(
7003 |_bounds, window, _| {
7004 window.insert_hitbox(
7005 Bounds::new(
7006 point(px(0.0), px(0.0)),
7007 window.window_bounds().get_bounds().size,
7008 ),
7009 false,
7010 )
7011 },
7012 move |_bounds, hitbox, window, cx| {
7013 let mouse = window.mouse_position();
7014 let size = window.window_bounds().get_bounds().size;
7015 let Some(edge) =
7016 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
7017 else {
7018 return;
7019 };
7020 cx.set_global(GlobalResizeEdge(edge));
7021 window.set_cursor_style(
7022 match edge {
7023 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
7024 ResizeEdge::Left | ResizeEdge::Right => {
7025 CursorStyle::ResizeLeftRight
7026 }
7027 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
7028 CursorStyle::ResizeUpLeftDownRight
7029 }
7030 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
7031 CursorStyle::ResizeUpRightDownLeft
7032 }
7033 },
7034 Some(&hitbox),
7035 );
7036 },
7037 )
7038 .size_full()
7039 .absolute(),
7040 ),
7041 })
7042}
7043
7044fn resize_edge(
7045 pos: Point<Pixels>,
7046 shadow_size: Pixels,
7047 window_size: Size<Pixels>,
7048 tiling: Tiling,
7049) -> Option<ResizeEdge> {
7050 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
7051 if bounds.contains(&pos) {
7052 return None;
7053 }
7054
7055 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
7056 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
7057 if !tiling.top && top_left_bounds.contains(&pos) {
7058 return Some(ResizeEdge::TopLeft);
7059 }
7060
7061 let top_right_bounds = Bounds::new(
7062 Point::new(window_size.width - corner_size.width, px(0.)),
7063 corner_size,
7064 );
7065 if !tiling.top && top_right_bounds.contains(&pos) {
7066 return Some(ResizeEdge::TopRight);
7067 }
7068
7069 let bottom_left_bounds = Bounds::new(
7070 Point::new(px(0.), window_size.height - corner_size.height),
7071 corner_size,
7072 );
7073 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
7074 return Some(ResizeEdge::BottomLeft);
7075 }
7076
7077 let bottom_right_bounds = Bounds::new(
7078 Point::new(
7079 window_size.width - corner_size.width,
7080 window_size.height - corner_size.height,
7081 ),
7082 corner_size,
7083 );
7084 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
7085 return Some(ResizeEdge::BottomRight);
7086 }
7087
7088 if !tiling.top && pos.y < shadow_size {
7089 Some(ResizeEdge::Top)
7090 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
7091 Some(ResizeEdge::Bottom)
7092 } else if !tiling.left && pos.x < shadow_size {
7093 Some(ResizeEdge::Left)
7094 } else if !tiling.right && pos.x > window_size.width - shadow_size {
7095 Some(ResizeEdge::Right)
7096 } else {
7097 None
7098 }
7099}
7100
7101fn join_pane_into_active(
7102 active_pane: &Entity<Pane>,
7103 pane: &Entity<Pane>,
7104 window: &mut Window,
7105 cx: &mut App,
7106) {
7107 if pane == active_pane {
7108 return;
7109 } else if pane.read(cx).items_len() == 0 {
7110 pane.update(cx, |_, cx| {
7111 cx.emit(pane::Event::Remove {
7112 focus_on_pane: None,
7113 });
7114 })
7115 } else {
7116 move_all_items(pane, active_pane, window, cx);
7117 }
7118}
7119
7120fn move_all_items(
7121 from_pane: &Entity<Pane>,
7122 to_pane: &Entity<Pane>,
7123 window: &mut Window,
7124 cx: &mut App,
7125) {
7126 let destination_is_different = from_pane != to_pane;
7127 let mut moved_items = 0;
7128 for (item_ix, item_handle) in from_pane
7129 .read(cx)
7130 .items()
7131 .enumerate()
7132 .map(|(ix, item)| (ix, item.clone()))
7133 .collect::<Vec<_>>()
7134 {
7135 let ix = item_ix - moved_items;
7136 if destination_is_different {
7137 // Close item from previous pane
7138 from_pane.update(cx, |source, cx| {
7139 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
7140 });
7141 moved_items += 1;
7142 }
7143
7144 // This automatically removes duplicate items in the pane
7145 to_pane.update(cx, |destination, cx| {
7146 destination.add_item(item_handle, true, true, None, window, cx);
7147 window.focus(&destination.focus_handle(cx))
7148 });
7149 }
7150}
7151
7152pub fn move_item(
7153 source: &Entity<Pane>,
7154 destination: &Entity<Pane>,
7155 item_id_to_move: EntityId,
7156 destination_index: usize,
7157 window: &mut Window,
7158 cx: &mut App,
7159) {
7160 let Some((item_ix, item_handle)) = source
7161 .read(cx)
7162 .items()
7163 .enumerate()
7164 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
7165 .map(|(ix, item)| (ix, item.clone()))
7166 else {
7167 // Tab was closed during drag
7168 return;
7169 };
7170
7171 if source != destination {
7172 // Close item from previous pane
7173 source.update(cx, |source, cx| {
7174 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
7175 });
7176 }
7177
7178 // This automatically removes duplicate items in the pane
7179 destination.update(cx, |destination, cx| {
7180 destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
7181 window.focus(&destination.focus_handle(cx))
7182 });
7183}
7184
7185pub fn move_active_item(
7186 source: &Entity<Pane>,
7187 destination: &Entity<Pane>,
7188 focus_destination: bool,
7189 close_if_empty: bool,
7190 window: &mut Window,
7191 cx: &mut App,
7192) {
7193 if source == destination {
7194 return;
7195 }
7196 let Some(active_item) = source.read(cx).active_item() else {
7197 return;
7198 };
7199 source.update(cx, |source_pane, cx| {
7200 let item_id = active_item.item_id();
7201 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
7202 destination.update(cx, |target_pane, cx| {
7203 target_pane.add_item(
7204 active_item,
7205 focus_destination,
7206 focus_destination,
7207 Some(target_pane.items_len()),
7208 window,
7209 cx,
7210 );
7211 });
7212 });
7213}
7214
7215#[cfg(test)]
7216mod tests {
7217 use std::{cell::RefCell, rc::Rc};
7218
7219 use super::*;
7220 use crate::{
7221 dock::{PanelEvent, test::TestPanel},
7222 item::{
7223 ItemEvent,
7224 test::{TestItem, TestProjectItem},
7225 },
7226 };
7227 use fs::FakeFs;
7228 use gpui::{
7229 DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
7230 UpdateGlobal, VisualTestContext, px,
7231 };
7232 use project::{Project, ProjectEntryId};
7233 use serde_json::json;
7234 use settings::SettingsStore;
7235
7236 #[gpui::test]
7237 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
7238 init_test(cx);
7239
7240 let fs = FakeFs::new(cx.executor());
7241 let project = Project::test(fs, [], cx).await;
7242 let (workspace, cx) =
7243 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7244
7245 // Adding an item with no ambiguity renders the tab without detail.
7246 let item1 = cx.new(|cx| {
7247 let mut item = TestItem::new(cx);
7248 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
7249 item
7250 });
7251 workspace.update_in(cx, |workspace, window, cx| {
7252 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7253 });
7254 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
7255
7256 // Adding an item that creates ambiguity increases the level of detail on
7257 // both tabs.
7258 let item2 = cx.new_window_entity(|_window, cx| {
7259 let mut item = TestItem::new(cx);
7260 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7261 item
7262 });
7263 workspace.update_in(cx, |workspace, window, cx| {
7264 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7265 });
7266 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7267 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7268
7269 // Adding an item that creates ambiguity increases the level of detail only
7270 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
7271 // we stop at the highest detail available.
7272 let item3 = cx.new(|cx| {
7273 let mut item = TestItem::new(cx);
7274 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7275 item
7276 });
7277 workspace.update_in(cx, |workspace, window, cx| {
7278 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7279 });
7280 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7281 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7282 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7283 }
7284
7285 #[gpui::test]
7286 async fn test_tracking_active_path(cx: &mut TestAppContext) {
7287 init_test(cx);
7288
7289 let fs = FakeFs::new(cx.executor());
7290 fs.insert_tree(
7291 "/root1",
7292 json!({
7293 "one.txt": "",
7294 "two.txt": "",
7295 }),
7296 )
7297 .await;
7298 fs.insert_tree(
7299 "/root2",
7300 json!({
7301 "three.txt": "",
7302 }),
7303 )
7304 .await;
7305
7306 let project = Project::test(fs, ["root1".as_ref()], cx).await;
7307 let (workspace, cx) =
7308 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7309 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7310 let worktree_id = project.update(cx, |project, cx| {
7311 project.worktrees(cx).next().unwrap().read(cx).id()
7312 });
7313
7314 let item1 = cx.new(|cx| {
7315 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
7316 });
7317 let item2 = cx.new(|cx| {
7318 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
7319 });
7320
7321 // Add an item to an empty pane
7322 workspace.update_in(cx, |workspace, window, cx| {
7323 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
7324 });
7325 project.update(cx, |project, cx| {
7326 assert_eq!(
7327 project.active_entry(),
7328 project
7329 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7330 .map(|e| e.id)
7331 );
7332 });
7333 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7334
7335 // Add a second item to a non-empty pane
7336 workspace.update_in(cx, |workspace, window, cx| {
7337 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
7338 });
7339 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
7340 project.update(cx, |project, cx| {
7341 assert_eq!(
7342 project.active_entry(),
7343 project
7344 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
7345 .map(|e| e.id)
7346 );
7347 });
7348
7349 // Close the active item
7350 pane.update_in(cx, |pane, window, cx| {
7351 pane.close_active_item(&Default::default(), window, cx)
7352 .unwrap()
7353 })
7354 .await
7355 .unwrap();
7356 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7357 project.update(cx, |project, cx| {
7358 assert_eq!(
7359 project.active_entry(),
7360 project
7361 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7362 .map(|e| e.id)
7363 );
7364 });
7365
7366 // Add a project folder
7367 project
7368 .update(cx, |project, cx| {
7369 project.find_or_create_worktree("root2", true, cx)
7370 })
7371 .await
7372 .unwrap();
7373 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
7374
7375 // Remove a project folder
7376 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
7377 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
7378 }
7379
7380 #[gpui::test]
7381 async fn test_close_window(cx: &mut TestAppContext) {
7382 init_test(cx);
7383
7384 let fs = FakeFs::new(cx.executor());
7385 fs.insert_tree("/root", json!({ "one": "" })).await;
7386
7387 let project = Project::test(fs, ["root".as_ref()], cx).await;
7388 let (workspace, cx) =
7389 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7390
7391 // When there are no dirty items, there's nothing to do.
7392 let item1 = cx.new(TestItem::new);
7393 workspace.update_in(cx, |w, window, cx| {
7394 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
7395 });
7396 let task = workspace.update_in(cx, |w, window, cx| {
7397 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7398 });
7399 assert!(task.await.unwrap());
7400
7401 // When there are dirty untitled items, prompt to save each one. If the user
7402 // cancels any prompt, then abort.
7403 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
7404 let item3 = cx.new(|cx| {
7405 TestItem::new(cx)
7406 .with_dirty(true)
7407 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7408 });
7409 workspace.update_in(cx, |w, window, cx| {
7410 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7411 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7412 });
7413 let task = workspace.update_in(cx, |w, window, cx| {
7414 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7415 });
7416 cx.executor().run_until_parked();
7417 cx.simulate_prompt_answer("Cancel"); // cancel save all
7418 cx.executor().run_until_parked();
7419 assert!(!cx.has_pending_prompt());
7420 assert!(!task.await.unwrap());
7421 }
7422
7423 #[gpui::test]
7424 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
7425 init_test(cx);
7426
7427 // Register TestItem as a serializable item
7428 cx.update(|cx| {
7429 register_serializable_item::<TestItem>(cx);
7430 });
7431
7432 let fs = FakeFs::new(cx.executor());
7433 fs.insert_tree("/root", json!({ "one": "" })).await;
7434
7435 let project = Project::test(fs, ["root".as_ref()], cx).await;
7436 let (workspace, cx) =
7437 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7438
7439 // When there are dirty untitled items, but they can serialize, then there is no prompt.
7440 let item1 = cx.new(|cx| {
7441 TestItem::new(cx)
7442 .with_dirty(true)
7443 .with_serialize(|| Some(Task::ready(Ok(()))))
7444 });
7445 let item2 = cx.new(|cx| {
7446 TestItem::new(cx)
7447 .with_dirty(true)
7448 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7449 .with_serialize(|| Some(Task::ready(Ok(()))))
7450 });
7451 workspace.update_in(cx, |w, window, cx| {
7452 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7453 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7454 });
7455 let task = workspace.update_in(cx, |w, window, cx| {
7456 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7457 });
7458 assert!(task.await.unwrap());
7459 }
7460
7461 #[gpui::test]
7462 async fn test_close_pane_items(cx: &mut TestAppContext) {
7463 init_test(cx);
7464
7465 let fs = FakeFs::new(cx.executor());
7466
7467 let project = Project::test(fs, None, cx).await;
7468 let (workspace, cx) =
7469 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7470
7471 let item1 = cx.new(|cx| {
7472 TestItem::new(cx)
7473 .with_dirty(true)
7474 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7475 });
7476 let item2 = cx.new(|cx| {
7477 TestItem::new(cx)
7478 .with_dirty(true)
7479 .with_conflict(true)
7480 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7481 });
7482 let item3 = cx.new(|cx| {
7483 TestItem::new(cx)
7484 .with_dirty(true)
7485 .with_conflict(true)
7486 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7487 });
7488 let item4 = cx.new(|cx| {
7489 TestItem::new(cx).with_dirty(true).with_project_items(&[{
7490 let project_item = TestProjectItem::new_untitled(cx);
7491 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7492 project_item
7493 }])
7494 });
7495 let pane = workspace.update_in(cx, |workspace, window, cx| {
7496 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7497 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7498 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7499 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7500 workspace.active_pane().clone()
7501 });
7502
7503 let close_items = pane.update_in(cx, |pane, window, cx| {
7504 pane.activate_item(1, true, true, window, cx);
7505 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7506 let item1_id = item1.item_id();
7507 let item3_id = item3.item_id();
7508 let item4_id = item4.item_id();
7509 pane.close_items(window, cx, SaveIntent::Close, move |id| {
7510 [item1_id, item3_id, item4_id].contains(&id)
7511 })
7512 });
7513 cx.executor().run_until_parked();
7514
7515 assert!(cx.has_pending_prompt());
7516 cx.simulate_prompt_answer("Save all");
7517
7518 cx.executor().run_until_parked();
7519
7520 // Item 1 is saved. There's a prompt to save item 3.
7521 pane.update(cx, |pane, cx| {
7522 assert_eq!(item1.read(cx).save_count, 1);
7523 assert_eq!(item1.read(cx).save_as_count, 0);
7524 assert_eq!(item1.read(cx).reload_count, 0);
7525 assert_eq!(pane.items_len(), 3);
7526 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7527 });
7528 assert!(cx.has_pending_prompt());
7529
7530 // Cancel saving item 3.
7531 cx.simulate_prompt_answer("Discard");
7532 cx.executor().run_until_parked();
7533
7534 // Item 3 is reloaded. There's a prompt to save item 4.
7535 pane.update(cx, |pane, cx| {
7536 assert_eq!(item3.read(cx).save_count, 0);
7537 assert_eq!(item3.read(cx).save_as_count, 0);
7538 assert_eq!(item3.read(cx).reload_count, 1);
7539 assert_eq!(pane.items_len(), 2);
7540 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7541 });
7542
7543 // There's a prompt for a path for item 4.
7544 cx.simulate_new_path_selection(|_| Some(Default::default()));
7545 close_items.await.unwrap();
7546
7547 // The requested items are closed.
7548 pane.update(cx, |pane, cx| {
7549 assert_eq!(item4.read(cx).save_count, 0);
7550 assert_eq!(item4.read(cx).save_as_count, 1);
7551 assert_eq!(item4.read(cx).reload_count, 0);
7552 assert_eq!(pane.items_len(), 1);
7553 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7554 });
7555 }
7556
7557 #[gpui::test]
7558 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7559 init_test(cx);
7560
7561 let fs = FakeFs::new(cx.executor());
7562 let project = Project::test(fs, [], cx).await;
7563 let (workspace, cx) =
7564 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7565
7566 // Create several workspace items with single project entries, and two
7567 // workspace items with multiple project entries.
7568 let single_entry_items = (0..=4)
7569 .map(|project_entry_id| {
7570 cx.new(|cx| {
7571 TestItem::new(cx)
7572 .with_dirty(true)
7573 .with_project_items(&[dirty_project_item(
7574 project_entry_id,
7575 &format!("{project_entry_id}.txt"),
7576 cx,
7577 )])
7578 })
7579 })
7580 .collect::<Vec<_>>();
7581 let item_2_3 = cx.new(|cx| {
7582 TestItem::new(cx)
7583 .with_dirty(true)
7584 .with_singleton(false)
7585 .with_project_items(&[
7586 single_entry_items[2].read(cx).project_items[0].clone(),
7587 single_entry_items[3].read(cx).project_items[0].clone(),
7588 ])
7589 });
7590 let item_3_4 = cx.new(|cx| {
7591 TestItem::new(cx)
7592 .with_dirty(true)
7593 .with_singleton(false)
7594 .with_project_items(&[
7595 single_entry_items[3].read(cx).project_items[0].clone(),
7596 single_entry_items[4].read(cx).project_items[0].clone(),
7597 ])
7598 });
7599
7600 // Create two panes that contain the following project entries:
7601 // left pane:
7602 // multi-entry items: (2, 3)
7603 // single-entry items: 0, 2, 3, 4
7604 // right pane:
7605 // single-entry items: 4, 1
7606 // multi-entry items: (3, 4)
7607 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
7608 let left_pane = workspace.active_pane().clone();
7609 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7610 workspace.add_item_to_active_pane(
7611 single_entry_items[0].boxed_clone(),
7612 None,
7613 true,
7614 window,
7615 cx,
7616 );
7617 workspace.add_item_to_active_pane(
7618 single_entry_items[2].boxed_clone(),
7619 None,
7620 true,
7621 window,
7622 cx,
7623 );
7624 workspace.add_item_to_active_pane(
7625 single_entry_items[3].boxed_clone(),
7626 None,
7627 true,
7628 window,
7629 cx,
7630 );
7631 workspace.add_item_to_active_pane(
7632 single_entry_items[4].boxed_clone(),
7633 None,
7634 true,
7635 window,
7636 cx,
7637 );
7638
7639 let right_pane = workspace
7640 .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7641 .unwrap();
7642
7643 right_pane.update(cx, |pane, cx| {
7644 pane.add_item(
7645 single_entry_items[1].boxed_clone(),
7646 true,
7647 true,
7648 None,
7649 window,
7650 cx,
7651 );
7652 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
7653 });
7654
7655 (left_pane, right_pane)
7656 });
7657
7658 cx.focus(&right_pane);
7659
7660 let mut close = right_pane.update_in(cx, |pane, window, cx| {
7661 pane.close_all_items(&CloseAllItems::default(), window, cx)
7662 .unwrap()
7663 });
7664 cx.executor().run_until_parked();
7665
7666 let msg = cx.pending_prompt().unwrap().0;
7667 assert!(msg.contains("1.txt"));
7668 assert!(!msg.contains("2.txt"));
7669 assert!(!msg.contains("3.txt"));
7670 assert!(!msg.contains("4.txt"));
7671
7672 cx.simulate_prompt_answer("Cancel");
7673 close.await.unwrap();
7674
7675 left_pane
7676 .update_in(cx, |left_pane, window, cx| {
7677 left_pane.close_item_by_id(
7678 single_entry_items[3].entity_id(),
7679 SaveIntent::Skip,
7680 window,
7681 cx,
7682 )
7683 })
7684 .await
7685 .unwrap();
7686
7687 close = right_pane.update_in(cx, |pane, window, cx| {
7688 pane.close_all_items(&CloseAllItems::default(), window, cx)
7689 .unwrap()
7690 });
7691 cx.executor().run_until_parked();
7692
7693 let details = cx.pending_prompt().unwrap().1;
7694 assert!(details.contains("1.txt"));
7695 assert!(!details.contains("2.txt"));
7696 assert!(details.contains("3.txt"));
7697 // ideally this assertion could be made, but today we can only
7698 // save whole items not project items, so the orphaned item 3 causes
7699 // 4 to be saved too.
7700 // assert!(!details.contains("4.txt"));
7701
7702 cx.simulate_prompt_answer("Save all");
7703
7704 cx.executor().run_until_parked();
7705 close.await.unwrap();
7706 right_pane.update(cx, |pane, _| {
7707 assert_eq!(pane.items_len(), 0);
7708 });
7709 }
7710
7711 #[gpui::test]
7712 async fn test_autosave(cx: &mut gpui::TestAppContext) {
7713 init_test(cx);
7714
7715 let fs = FakeFs::new(cx.executor());
7716 let project = Project::test(fs, [], cx).await;
7717 let (workspace, cx) =
7718 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7719 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7720
7721 let item = cx.new(|cx| {
7722 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7723 });
7724 let item_id = item.entity_id();
7725 workspace.update_in(cx, |workspace, window, cx| {
7726 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7727 });
7728
7729 // Autosave on window change.
7730 item.update(cx, |item, cx| {
7731 SettingsStore::update_global(cx, |settings, cx| {
7732 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7733 settings.autosave = Some(AutosaveSetting::OnWindowChange);
7734 })
7735 });
7736 item.is_dirty = true;
7737 });
7738
7739 // Deactivating the window saves the file.
7740 cx.deactivate_window();
7741 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7742
7743 // Re-activating the window doesn't save the file.
7744 cx.update(|window, _| window.activate_window());
7745 cx.executor().run_until_parked();
7746 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7747
7748 // Autosave on focus change.
7749 item.update_in(cx, |item, window, cx| {
7750 cx.focus_self(window);
7751 SettingsStore::update_global(cx, |settings, cx| {
7752 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7753 settings.autosave = Some(AutosaveSetting::OnFocusChange);
7754 })
7755 });
7756 item.is_dirty = true;
7757 });
7758
7759 // Blurring the item saves the file.
7760 item.update_in(cx, |_, window, _| window.blur());
7761 cx.executor().run_until_parked();
7762 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
7763
7764 // Deactivating the window still saves the file.
7765 item.update_in(cx, |item, window, cx| {
7766 cx.focus_self(window);
7767 item.is_dirty = true;
7768 });
7769 cx.deactivate_window();
7770 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7771
7772 // Autosave after delay.
7773 item.update(cx, |item, cx| {
7774 SettingsStore::update_global(cx, |settings, cx| {
7775 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7776 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
7777 })
7778 });
7779 item.is_dirty = true;
7780 cx.emit(ItemEvent::Edit);
7781 });
7782
7783 // Delay hasn't fully expired, so the file is still dirty and unsaved.
7784 cx.executor().advance_clock(Duration::from_millis(250));
7785 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7786
7787 // After delay expires, the file is saved.
7788 cx.executor().advance_clock(Duration::from_millis(250));
7789 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
7790
7791 // Autosave on focus change, ensuring closing the tab counts as such.
7792 item.update(cx, |item, cx| {
7793 SettingsStore::update_global(cx, |settings, cx| {
7794 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7795 settings.autosave = Some(AutosaveSetting::OnFocusChange);
7796 })
7797 });
7798 item.is_dirty = true;
7799 for project_item in &mut item.project_items {
7800 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7801 }
7802 });
7803
7804 pane.update_in(cx, |pane, window, cx| {
7805 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7806 })
7807 .await
7808 .unwrap();
7809 assert!(!cx.has_pending_prompt());
7810 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7811
7812 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
7813 workspace.update_in(cx, |workspace, window, cx| {
7814 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7815 });
7816 item.update_in(cx, |item, window, cx| {
7817 item.project_items[0].update(cx, |item, _| {
7818 item.entry_id = None;
7819 });
7820 item.is_dirty = true;
7821 window.blur();
7822 });
7823 cx.run_until_parked();
7824 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7825
7826 // Ensure autosave is prevented for deleted files also when closing the buffer.
7827 let _close_items = pane.update_in(cx, |pane, window, cx| {
7828 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7829 });
7830 cx.run_until_parked();
7831 assert!(cx.has_pending_prompt());
7832 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7833 }
7834
7835 #[gpui::test]
7836 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
7837 init_test(cx);
7838
7839 let fs = FakeFs::new(cx.executor());
7840
7841 let project = Project::test(fs, [], cx).await;
7842 let (workspace, cx) =
7843 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7844
7845 let item = cx.new(|cx| {
7846 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7847 });
7848 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7849 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
7850 let toolbar_notify_count = Rc::new(RefCell::new(0));
7851
7852 workspace.update_in(cx, |workspace, window, cx| {
7853 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7854 let toolbar_notification_count = toolbar_notify_count.clone();
7855 cx.observe_in(&toolbar, window, move |_, _, _, _| {
7856 *toolbar_notification_count.borrow_mut() += 1
7857 })
7858 .detach();
7859 });
7860
7861 pane.update(cx, |pane, _| {
7862 assert!(!pane.can_navigate_backward());
7863 assert!(!pane.can_navigate_forward());
7864 });
7865
7866 item.update_in(cx, |item, _, cx| {
7867 item.set_state("one".to_string(), cx);
7868 });
7869
7870 // Toolbar must be notified to re-render the navigation buttons
7871 assert_eq!(*toolbar_notify_count.borrow(), 1);
7872
7873 pane.update(cx, |pane, _| {
7874 assert!(pane.can_navigate_backward());
7875 assert!(!pane.can_navigate_forward());
7876 });
7877
7878 workspace
7879 .update_in(cx, |workspace, window, cx| {
7880 workspace.go_back(pane.downgrade(), window, cx)
7881 })
7882 .await
7883 .unwrap();
7884
7885 assert_eq!(*toolbar_notify_count.borrow(), 2);
7886 pane.update(cx, |pane, _| {
7887 assert!(!pane.can_navigate_backward());
7888 assert!(pane.can_navigate_forward());
7889 });
7890 }
7891
7892 #[gpui::test]
7893 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
7894 init_test(cx);
7895 let fs = FakeFs::new(cx.executor());
7896
7897 let project = Project::test(fs, [], cx).await;
7898 let (workspace, cx) =
7899 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7900
7901 let panel = workspace.update_in(cx, |workspace, window, cx| {
7902 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7903 workspace.add_panel(panel.clone(), window, cx);
7904
7905 workspace
7906 .right_dock()
7907 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
7908
7909 panel
7910 });
7911
7912 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7913 pane.update_in(cx, |pane, window, cx| {
7914 let item = cx.new(TestItem::new);
7915 pane.add_item(Box::new(item), true, true, None, window, cx);
7916 });
7917
7918 // Transfer focus from center to panel
7919 workspace.update_in(cx, |workspace, window, cx| {
7920 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7921 });
7922
7923 workspace.update_in(cx, |workspace, window, cx| {
7924 assert!(workspace.right_dock().read(cx).is_open());
7925 assert!(!panel.is_zoomed(window, cx));
7926 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7927 });
7928
7929 // Transfer focus from panel to center
7930 workspace.update_in(cx, |workspace, window, cx| {
7931 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7932 });
7933
7934 workspace.update_in(cx, |workspace, window, cx| {
7935 assert!(workspace.right_dock().read(cx).is_open());
7936 assert!(!panel.is_zoomed(window, cx));
7937 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7938 });
7939
7940 // Close the dock
7941 workspace.update_in(cx, |workspace, window, cx| {
7942 workspace.toggle_dock(DockPosition::Right, window, cx);
7943 });
7944
7945 workspace.update_in(cx, |workspace, window, cx| {
7946 assert!(!workspace.right_dock().read(cx).is_open());
7947 assert!(!panel.is_zoomed(window, cx));
7948 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7949 });
7950
7951 // Open the dock
7952 workspace.update_in(cx, |workspace, window, cx| {
7953 workspace.toggle_dock(DockPosition::Right, window, cx);
7954 });
7955
7956 workspace.update_in(cx, |workspace, window, cx| {
7957 assert!(workspace.right_dock().read(cx).is_open());
7958 assert!(!panel.is_zoomed(window, cx));
7959 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7960 });
7961
7962 // Focus and zoom panel
7963 panel.update_in(cx, |panel, window, cx| {
7964 cx.focus_self(window);
7965 panel.set_zoomed(true, window, cx)
7966 });
7967
7968 workspace.update_in(cx, |workspace, window, cx| {
7969 assert!(workspace.right_dock().read(cx).is_open());
7970 assert!(panel.is_zoomed(window, cx));
7971 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7972 });
7973
7974 // Transfer focus to the center closes the dock
7975 workspace.update_in(cx, |workspace, window, cx| {
7976 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7977 });
7978
7979 workspace.update_in(cx, |workspace, window, cx| {
7980 assert!(!workspace.right_dock().read(cx).is_open());
7981 assert!(panel.is_zoomed(window, cx));
7982 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7983 });
7984
7985 // Transferring focus back to the panel keeps it zoomed
7986 workspace.update_in(cx, |workspace, window, cx| {
7987 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7988 });
7989
7990 workspace.update_in(cx, |workspace, window, cx| {
7991 assert!(workspace.right_dock().read(cx).is_open());
7992 assert!(panel.is_zoomed(window, cx));
7993 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7994 });
7995
7996 // Close the dock while it is zoomed
7997 workspace.update_in(cx, |workspace, window, cx| {
7998 workspace.toggle_dock(DockPosition::Right, window, cx)
7999 });
8000
8001 workspace.update_in(cx, |workspace, window, cx| {
8002 assert!(!workspace.right_dock().read(cx).is_open());
8003 assert!(panel.is_zoomed(window, cx));
8004 assert!(workspace.zoomed.is_none());
8005 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8006 });
8007
8008 // Opening the dock, when it's zoomed, retains focus
8009 workspace.update_in(cx, |workspace, window, cx| {
8010 workspace.toggle_dock(DockPosition::Right, window, cx)
8011 });
8012
8013 workspace.update_in(cx, |workspace, window, cx| {
8014 assert!(workspace.right_dock().read(cx).is_open());
8015 assert!(panel.is_zoomed(window, cx));
8016 assert!(workspace.zoomed.is_some());
8017 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8018 });
8019
8020 // Unzoom and close the panel, zoom the active pane.
8021 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
8022 workspace.update_in(cx, |workspace, window, cx| {
8023 workspace.toggle_dock(DockPosition::Right, window, cx)
8024 });
8025 pane.update_in(cx, |pane, window, cx| {
8026 pane.toggle_zoom(&Default::default(), window, cx)
8027 });
8028
8029 // Opening a dock unzooms the pane.
8030 workspace.update_in(cx, |workspace, window, cx| {
8031 workspace.toggle_dock(DockPosition::Right, window, cx)
8032 });
8033 workspace.update_in(cx, |workspace, window, cx| {
8034 let pane = pane.read(cx);
8035 assert!(!pane.is_zoomed());
8036 assert!(!pane.focus_handle(cx).is_focused(window));
8037 assert!(workspace.right_dock().read(cx).is_open());
8038 assert!(workspace.zoomed.is_none());
8039 });
8040 }
8041
8042 #[gpui::test]
8043 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
8044 init_test(cx);
8045
8046 let fs = FakeFs::new(cx.executor());
8047
8048 let project = Project::test(fs, None, cx).await;
8049 let (workspace, cx) =
8050 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8051
8052 // Let's arrange the panes like this:
8053 //
8054 // +-----------------------+
8055 // | top |
8056 // +------+--------+-------+
8057 // | left | center | right |
8058 // +------+--------+-------+
8059 // | bottom |
8060 // +-----------------------+
8061
8062 let top_item = cx.new(|cx| {
8063 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
8064 });
8065 let bottom_item = cx.new(|cx| {
8066 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
8067 });
8068 let left_item = cx.new(|cx| {
8069 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
8070 });
8071 let right_item = cx.new(|cx| {
8072 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
8073 });
8074 let center_item = cx.new(|cx| {
8075 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
8076 });
8077
8078 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8079 let top_pane_id = workspace.active_pane().entity_id();
8080 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
8081 workspace.split_pane(
8082 workspace.active_pane().clone(),
8083 SplitDirection::Down,
8084 window,
8085 cx,
8086 );
8087 top_pane_id
8088 });
8089 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8090 let bottom_pane_id = workspace.active_pane().entity_id();
8091 workspace.add_item_to_active_pane(
8092 Box::new(bottom_item.clone()),
8093 None,
8094 false,
8095 window,
8096 cx,
8097 );
8098 workspace.split_pane(
8099 workspace.active_pane().clone(),
8100 SplitDirection::Up,
8101 window,
8102 cx,
8103 );
8104 bottom_pane_id
8105 });
8106 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8107 let left_pane_id = workspace.active_pane().entity_id();
8108 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
8109 workspace.split_pane(
8110 workspace.active_pane().clone(),
8111 SplitDirection::Right,
8112 window,
8113 cx,
8114 );
8115 left_pane_id
8116 });
8117 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8118 let right_pane_id = workspace.active_pane().entity_id();
8119 workspace.add_item_to_active_pane(
8120 Box::new(right_item.clone()),
8121 None,
8122 false,
8123 window,
8124 cx,
8125 );
8126 workspace.split_pane(
8127 workspace.active_pane().clone(),
8128 SplitDirection::Left,
8129 window,
8130 cx,
8131 );
8132 right_pane_id
8133 });
8134 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8135 let center_pane_id = workspace.active_pane().entity_id();
8136 workspace.add_item_to_active_pane(
8137 Box::new(center_item.clone()),
8138 None,
8139 false,
8140 window,
8141 cx,
8142 );
8143 center_pane_id
8144 });
8145 cx.executor().run_until_parked();
8146
8147 workspace.update_in(cx, |workspace, window, cx| {
8148 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
8149
8150 // Join into next from center pane into right
8151 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8152 });
8153
8154 workspace.update_in(cx, |workspace, window, cx| {
8155 let active_pane = workspace.active_pane();
8156 assert_eq!(right_pane_id, active_pane.entity_id());
8157 assert_eq!(2, active_pane.read(cx).items_len());
8158 let item_ids_in_pane =
8159 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8160 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8161 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8162
8163 // Join into next from right pane into bottom
8164 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8165 });
8166
8167 workspace.update_in(cx, |workspace, window, cx| {
8168 let active_pane = workspace.active_pane();
8169 assert_eq!(bottom_pane_id, active_pane.entity_id());
8170 assert_eq!(3, active_pane.read(cx).items_len());
8171 let item_ids_in_pane =
8172 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8173 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8174 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8175 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8176
8177 // Join into next from bottom pane into left
8178 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8179 });
8180
8181 workspace.update_in(cx, |workspace, window, cx| {
8182 let active_pane = workspace.active_pane();
8183 assert_eq!(left_pane_id, active_pane.entity_id());
8184 assert_eq!(4, active_pane.read(cx).items_len());
8185 let item_ids_in_pane =
8186 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8187 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8188 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8189 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8190 assert!(item_ids_in_pane.contains(&left_item.item_id()));
8191
8192 // Join into next from left pane into top
8193 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8194 });
8195
8196 workspace.update_in(cx, |workspace, window, cx| {
8197 let active_pane = workspace.active_pane();
8198 assert_eq!(top_pane_id, active_pane.entity_id());
8199 assert_eq!(5, active_pane.read(cx).items_len());
8200 let item_ids_in_pane =
8201 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8202 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8203 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8204 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8205 assert!(item_ids_in_pane.contains(&left_item.item_id()));
8206 assert!(item_ids_in_pane.contains(&top_item.item_id()));
8207
8208 // Single pane left: no-op
8209 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
8210 });
8211
8212 workspace.update(cx, |workspace, _cx| {
8213 let active_pane = workspace.active_pane();
8214 assert_eq!(top_pane_id, active_pane.entity_id());
8215 });
8216 }
8217
8218 fn add_an_item_to_active_pane(
8219 cx: &mut VisualTestContext,
8220 workspace: &Entity<Workspace>,
8221 item_id: u64,
8222 ) -> Entity<TestItem> {
8223 let item = cx.new(|cx| {
8224 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
8225 item_id,
8226 "item{item_id}.txt",
8227 cx,
8228 )])
8229 });
8230 workspace.update_in(cx, |workspace, window, cx| {
8231 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
8232 });
8233 return item;
8234 }
8235
8236 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
8237 return workspace.update_in(cx, |workspace, window, cx| {
8238 let new_pane = workspace.split_pane(
8239 workspace.active_pane().clone(),
8240 SplitDirection::Right,
8241 window,
8242 cx,
8243 );
8244 new_pane
8245 });
8246 }
8247
8248 #[gpui::test]
8249 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
8250 init_test(cx);
8251 let fs = FakeFs::new(cx.executor());
8252 let project = Project::test(fs, None, cx).await;
8253 let (workspace, cx) =
8254 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8255
8256 add_an_item_to_active_pane(cx, &workspace, 1);
8257 split_pane(cx, &workspace);
8258 add_an_item_to_active_pane(cx, &workspace, 2);
8259 split_pane(cx, &workspace); // empty pane
8260 split_pane(cx, &workspace);
8261 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
8262
8263 cx.executor().run_until_parked();
8264
8265 workspace.update(cx, |workspace, cx| {
8266 let num_panes = workspace.panes().len();
8267 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8268 let active_item = workspace
8269 .active_pane()
8270 .read(cx)
8271 .active_item()
8272 .expect("item is in focus");
8273
8274 assert_eq!(num_panes, 4);
8275 assert_eq!(num_items_in_current_pane, 1);
8276 assert_eq!(active_item.item_id(), last_item.item_id());
8277 });
8278
8279 workspace.update_in(cx, |workspace, window, cx| {
8280 workspace.join_all_panes(window, cx);
8281 });
8282
8283 workspace.update(cx, |workspace, cx| {
8284 let num_panes = workspace.panes().len();
8285 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8286 let active_item = workspace
8287 .active_pane()
8288 .read(cx)
8289 .active_item()
8290 .expect("item is in focus");
8291
8292 assert_eq!(num_panes, 1);
8293 assert_eq!(num_items_in_current_pane, 3);
8294 assert_eq!(active_item.item_id(), last_item.item_id());
8295 });
8296 }
8297 struct TestModal(FocusHandle);
8298
8299 impl TestModal {
8300 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
8301 Self(cx.focus_handle())
8302 }
8303 }
8304
8305 impl EventEmitter<DismissEvent> for TestModal {}
8306
8307 impl Focusable for TestModal {
8308 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8309 self.0.clone()
8310 }
8311 }
8312
8313 impl ModalView for TestModal {}
8314
8315 impl Render for TestModal {
8316 fn render(
8317 &mut self,
8318 _window: &mut Window,
8319 _cx: &mut Context<TestModal>,
8320 ) -> impl IntoElement {
8321 div().track_focus(&self.0)
8322 }
8323 }
8324
8325 #[gpui::test]
8326 async fn test_panels(cx: &mut gpui::TestAppContext) {
8327 init_test(cx);
8328 let fs = FakeFs::new(cx.executor());
8329
8330 let project = Project::test(fs, [], cx).await;
8331 let (workspace, cx) =
8332 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8333
8334 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
8335 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
8336 workspace.add_panel(panel_1.clone(), window, cx);
8337 workspace.toggle_dock(DockPosition::Left, window, cx);
8338 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8339 workspace.add_panel(panel_2.clone(), window, cx);
8340 workspace.toggle_dock(DockPosition::Right, window, cx);
8341
8342 let left_dock = workspace.left_dock();
8343 assert_eq!(
8344 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8345 panel_1.panel_id()
8346 );
8347 assert_eq!(
8348 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8349 panel_1.size(window, cx)
8350 );
8351
8352 left_dock.update(cx, |left_dock, cx| {
8353 left_dock.resize_active_panel(Some(px(1337.)), window, cx)
8354 });
8355 assert_eq!(
8356 workspace
8357 .right_dock()
8358 .read(cx)
8359 .visible_panel()
8360 .unwrap()
8361 .panel_id(),
8362 panel_2.panel_id(),
8363 );
8364
8365 (panel_1, panel_2)
8366 });
8367
8368 // Move panel_1 to the right
8369 panel_1.update_in(cx, |panel_1, window, cx| {
8370 panel_1.set_position(DockPosition::Right, window, cx)
8371 });
8372
8373 workspace.update_in(cx, |workspace, window, cx| {
8374 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
8375 // Since it was the only panel on the left, the left dock should now be closed.
8376 assert!(!workspace.left_dock().read(cx).is_open());
8377 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
8378 let right_dock = workspace.right_dock();
8379 assert_eq!(
8380 right_dock.read(cx).visible_panel().unwrap().panel_id(),
8381 panel_1.panel_id()
8382 );
8383 assert_eq!(
8384 right_dock.read(cx).active_panel_size(window, cx).unwrap(),
8385 px(1337.)
8386 );
8387
8388 // Now we move panel_2 to the left
8389 panel_2.set_position(DockPosition::Left, window, cx);
8390 });
8391
8392 workspace.update(cx, |workspace, cx| {
8393 // Since panel_2 was not visible on the right, we don't open the left dock.
8394 assert!(!workspace.left_dock().read(cx).is_open());
8395 // And the right dock is unaffected in its displaying of panel_1
8396 assert!(workspace.right_dock().read(cx).is_open());
8397 assert_eq!(
8398 workspace
8399 .right_dock()
8400 .read(cx)
8401 .visible_panel()
8402 .unwrap()
8403 .panel_id(),
8404 panel_1.panel_id(),
8405 );
8406 });
8407
8408 // Move panel_1 back to the left
8409 panel_1.update_in(cx, |panel_1, window, cx| {
8410 panel_1.set_position(DockPosition::Left, window, cx)
8411 });
8412
8413 workspace.update_in(cx, |workspace, window, cx| {
8414 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
8415 let left_dock = workspace.left_dock();
8416 assert!(left_dock.read(cx).is_open());
8417 assert_eq!(
8418 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8419 panel_1.panel_id()
8420 );
8421 assert_eq!(
8422 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8423 px(1337.)
8424 );
8425 // And the right dock should be closed as it no longer has any panels.
8426 assert!(!workspace.right_dock().read(cx).is_open());
8427
8428 // Now we move panel_1 to the bottom
8429 panel_1.set_position(DockPosition::Bottom, window, cx);
8430 });
8431
8432 workspace.update_in(cx, |workspace, window, cx| {
8433 // Since panel_1 was visible on the left, we close the left dock.
8434 assert!(!workspace.left_dock().read(cx).is_open());
8435 // The bottom dock is sized based on the panel's default size,
8436 // since the panel orientation changed from vertical to horizontal.
8437 let bottom_dock = workspace.bottom_dock();
8438 assert_eq!(
8439 bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
8440 panel_1.size(window, cx),
8441 );
8442 // Close bottom dock and move panel_1 back to the left.
8443 bottom_dock.update(cx, |bottom_dock, cx| {
8444 bottom_dock.set_open(false, window, cx)
8445 });
8446 panel_1.set_position(DockPosition::Left, window, cx);
8447 });
8448
8449 // Emit activated event on panel 1
8450 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
8451
8452 // Now the left dock is open and panel_1 is active and focused.
8453 workspace.update_in(cx, |workspace, window, cx| {
8454 let left_dock = workspace.left_dock();
8455 assert!(left_dock.read(cx).is_open());
8456 assert_eq!(
8457 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8458 panel_1.panel_id(),
8459 );
8460 assert!(panel_1.focus_handle(cx).is_focused(window));
8461 });
8462
8463 // Emit closed event on panel 2, which is not active
8464 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8465
8466 // Wo don't close the left dock, because panel_2 wasn't the active panel
8467 workspace.update(cx, |workspace, cx| {
8468 let left_dock = workspace.left_dock();
8469 assert!(left_dock.read(cx).is_open());
8470 assert_eq!(
8471 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8472 panel_1.panel_id(),
8473 );
8474 });
8475
8476 // Emitting a ZoomIn event shows the panel as zoomed.
8477 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
8478 workspace.update(cx, |workspace, _| {
8479 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8480 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
8481 });
8482
8483 // Move panel to another dock while it is zoomed
8484 panel_1.update_in(cx, |panel, window, cx| {
8485 panel.set_position(DockPosition::Right, window, cx)
8486 });
8487 workspace.update(cx, |workspace, _| {
8488 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8489
8490 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8491 });
8492
8493 // This is a helper for getting a:
8494 // - valid focus on an element,
8495 // - that isn't a part of the panes and panels system of the Workspace,
8496 // - and doesn't trigger the 'on_focus_lost' API.
8497 let focus_other_view = {
8498 let workspace = workspace.clone();
8499 move |cx: &mut VisualTestContext| {
8500 workspace.update_in(cx, |workspace, window, cx| {
8501 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
8502 workspace.toggle_modal(window, cx, TestModal::new);
8503 workspace.toggle_modal(window, cx, TestModal::new);
8504 } else {
8505 workspace.toggle_modal(window, cx, TestModal::new);
8506 }
8507 })
8508 }
8509 };
8510
8511 // If focus is transferred to another view that's not a panel or another pane, we still show
8512 // the panel as zoomed.
8513 focus_other_view(cx);
8514 workspace.update(cx, |workspace, _| {
8515 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8516 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8517 });
8518
8519 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8520 workspace.update_in(cx, |_workspace, window, cx| {
8521 cx.focus_self(window);
8522 });
8523 workspace.update(cx, |workspace, _| {
8524 assert_eq!(workspace.zoomed, None);
8525 assert_eq!(workspace.zoomed_position, None);
8526 });
8527
8528 // If focus is transferred again to another view that's not a panel or a pane, we won't
8529 // show the panel as zoomed because it wasn't zoomed before.
8530 focus_other_view(cx);
8531 workspace.update(cx, |workspace, _| {
8532 assert_eq!(workspace.zoomed, None);
8533 assert_eq!(workspace.zoomed_position, None);
8534 });
8535
8536 // When the panel is activated, it is zoomed again.
8537 cx.dispatch_action(ToggleRightDock);
8538 workspace.update(cx, |workspace, _| {
8539 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8540 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8541 });
8542
8543 // Emitting a ZoomOut event unzooms the panel.
8544 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8545 workspace.update(cx, |workspace, _| {
8546 assert_eq!(workspace.zoomed, None);
8547 assert_eq!(workspace.zoomed_position, None);
8548 });
8549
8550 // Emit closed event on panel 1, which is active
8551 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8552
8553 // Now the left dock is closed, because panel_1 was the active panel
8554 workspace.update(cx, |workspace, cx| {
8555 let right_dock = workspace.right_dock();
8556 assert!(!right_dock.read(cx).is_open());
8557 });
8558 }
8559
8560 #[gpui::test]
8561 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8562 init_test(cx);
8563
8564 let fs = FakeFs::new(cx.background_executor.clone());
8565 let project = Project::test(fs, [], cx).await;
8566 let (workspace, cx) =
8567 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8568 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8569
8570 let dirty_regular_buffer = cx.new(|cx| {
8571 TestItem::new(cx)
8572 .with_dirty(true)
8573 .with_label("1.txt")
8574 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8575 });
8576 let dirty_regular_buffer_2 = cx.new(|cx| {
8577 TestItem::new(cx)
8578 .with_dirty(true)
8579 .with_label("2.txt")
8580 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8581 });
8582 let dirty_multi_buffer_with_both = cx.new(|cx| {
8583 TestItem::new(cx)
8584 .with_dirty(true)
8585 .with_singleton(false)
8586 .with_label("Fake Project Search")
8587 .with_project_items(&[
8588 dirty_regular_buffer.read(cx).project_items[0].clone(),
8589 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8590 ])
8591 });
8592 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8593 workspace.update_in(cx, |workspace, window, cx| {
8594 workspace.add_item(
8595 pane.clone(),
8596 Box::new(dirty_regular_buffer.clone()),
8597 None,
8598 false,
8599 false,
8600 window,
8601 cx,
8602 );
8603 workspace.add_item(
8604 pane.clone(),
8605 Box::new(dirty_regular_buffer_2.clone()),
8606 None,
8607 false,
8608 false,
8609 window,
8610 cx,
8611 );
8612 workspace.add_item(
8613 pane.clone(),
8614 Box::new(dirty_multi_buffer_with_both.clone()),
8615 None,
8616 false,
8617 false,
8618 window,
8619 cx,
8620 );
8621 });
8622
8623 pane.update_in(cx, |pane, window, cx| {
8624 pane.activate_item(2, true, true, window, cx);
8625 assert_eq!(
8626 pane.active_item().unwrap().item_id(),
8627 multi_buffer_with_both_files_id,
8628 "Should select the multi buffer in the pane"
8629 );
8630 });
8631 let close_all_but_multi_buffer_task = pane
8632 .update_in(cx, |pane, window, cx| {
8633 pane.close_inactive_items(
8634 &CloseInactiveItems {
8635 save_intent: Some(SaveIntent::Save),
8636 close_pinned: true,
8637 },
8638 window,
8639 cx,
8640 )
8641 })
8642 .expect("should have inactive files to close");
8643 cx.background_executor.run_until_parked();
8644 assert!(!cx.has_pending_prompt());
8645 close_all_but_multi_buffer_task
8646 .await
8647 .expect("Closing all buffers but the multi buffer failed");
8648 pane.update(cx, |pane, cx| {
8649 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
8650 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
8651 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
8652 assert_eq!(pane.items_len(), 1);
8653 assert_eq!(
8654 pane.active_item().unwrap().item_id(),
8655 multi_buffer_with_both_files_id,
8656 "Should have only the multi buffer left in the pane"
8657 );
8658 assert!(
8659 dirty_multi_buffer_with_both.read(cx).is_dirty,
8660 "The multi buffer containing the unsaved buffer should still be dirty"
8661 );
8662 });
8663
8664 dirty_regular_buffer.update(cx, |buffer, cx| {
8665 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
8666 });
8667
8668 let close_multi_buffer_task = pane
8669 .update_in(cx, |pane, window, cx| {
8670 pane.close_active_item(
8671 &CloseActiveItem {
8672 save_intent: Some(SaveIntent::Close),
8673 close_pinned: false,
8674 },
8675 window,
8676 cx,
8677 )
8678 })
8679 .expect("should have the multi buffer to close");
8680 cx.background_executor.run_until_parked();
8681 assert!(
8682 cx.has_pending_prompt(),
8683 "Dirty multi buffer should prompt a save dialog"
8684 );
8685 cx.simulate_prompt_answer("Save");
8686 cx.background_executor.run_until_parked();
8687 close_multi_buffer_task
8688 .await
8689 .expect("Closing the multi buffer failed");
8690 pane.update(cx, |pane, cx| {
8691 assert_eq!(
8692 dirty_multi_buffer_with_both.read(cx).save_count,
8693 1,
8694 "Multi buffer item should get be saved"
8695 );
8696 // Test impl does not save inner items, so we do not assert them
8697 assert_eq!(
8698 pane.items_len(),
8699 0,
8700 "No more items should be left in the pane"
8701 );
8702 assert!(pane.active_item().is_none());
8703 });
8704 }
8705
8706 #[gpui::test]
8707 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
8708 cx: &mut TestAppContext,
8709 ) {
8710 init_test(cx);
8711
8712 let fs = FakeFs::new(cx.background_executor.clone());
8713 let project = Project::test(fs, [], cx).await;
8714 let (workspace, cx) =
8715 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8716 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8717
8718 let dirty_regular_buffer = cx.new(|cx| {
8719 TestItem::new(cx)
8720 .with_dirty(true)
8721 .with_label("1.txt")
8722 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8723 });
8724 let dirty_regular_buffer_2 = cx.new(|cx| {
8725 TestItem::new(cx)
8726 .with_dirty(true)
8727 .with_label("2.txt")
8728 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8729 });
8730 let clear_regular_buffer = cx.new(|cx| {
8731 TestItem::new(cx)
8732 .with_label("3.txt")
8733 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8734 });
8735
8736 let dirty_multi_buffer_with_both = cx.new(|cx| {
8737 TestItem::new(cx)
8738 .with_dirty(true)
8739 .with_singleton(false)
8740 .with_label("Fake Project Search")
8741 .with_project_items(&[
8742 dirty_regular_buffer.read(cx).project_items[0].clone(),
8743 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8744 clear_regular_buffer.read(cx).project_items[0].clone(),
8745 ])
8746 });
8747 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8748 workspace.update_in(cx, |workspace, window, cx| {
8749 workspace.add_item(
8750 pane.clone(),
8751 Box::new(dirty_regular_buffer.clone()),
8752 None,
8753 false,
8754 false,
8755 window,
8756 cx,
8757 );
8758 workspace.add_item(
8759 pane.clone(),
8760 Box::new(dirty_multi_buffer_with_both.clone()),
8761 None,
8762 false,
8763 false,
8764 window,
8765 cx,
8766 );
8767 });
8768
8769 pane.update_in(cx, |pane, window, cx| {
8770 pane.activate_item(1, true, true, window, cx);
8771 assert_eq!(
8772 pane.active_item().unwrap().item_id(),
8773 multi_buffer_with_both_files_id,
8774 "Should select the multi buffer in the pane"
8775 );
8776 });
8777 let _close_multi_buffer_task = pane
8778 .update_in(cx, |pane, window, cx| {
8779 pane.close_active_item(
8780 &CloseActiveItem {
8781 save_intent: None,
8782 close_pinned: false,
8783 },
8784 window,
8785 cx,
8786 )
8787 })
8788 .expect("should have active multi buffer to close");
8789 cx.background_executor.run_until_parked();
8790 assert!(
8791 cx.has_pending_prompt(),
8792 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
8793 );
8794 }
8795
8796 #[gpui::test]
8797 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
8798 cx: &mut TestAppContext,
8799 ) {
8800 init_test(cx);
8801
8802 let fs = FakeFs::new(cx.background_executor.clone());
8803 let project = Project::test(fs, [], cx).await;
8804 let (workspace, cx) =
8805 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8806 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8807
8808 let dirty_regular_buffer = cx.new(|cx| {
8809 TestItem::new(cx)
8810 .with_dirty(true)
8811 .with_label("1.txt")
8812 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8813 });
8814 let dirty_regular_buffer_2 = cx.new(|cx| {
8815 TestItem::new(cx)
8816 .with_dirty(true)
8817 .with_label("2.txt")
8818 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8819 });
8820 let clear_regular_buffer = cx.new(|cx| {
8821 TestItem::new(cx)
8822 .with_label("3.txt")
8823 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8824 });
8825
8826 let dirty_multi_buffer = cx.new(|cx| {
8827 TestItem::new(cx)
8828 .with_dirty(true)
8829 .with_singleton(false)
8830 .with_label("Fake Project Search")
8831 .with_project_items(&[
8832 dirty_regular_buffer.read(cx).project_items[0].clone(),
8833 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8834 clear_regular_buffer.read(cx).project_items[0].clone(),
8835 ])
8836 });
8837 workspace.update_in(cx, |workspace, window, cx| {
8838 workspace.add_item(
8839 pane.clone(),
8840 Box::new(dirty_regular_buffer.clone()),
8841 None,
8842 false,
8843 false,
8844 window,
8845 cx,
8846 );
8847 workspace.add_item(
8848 pane.clone(),
8849 Box::new(dirty_regular_buffer_2.clone()),
8850 None,
8851 false,
8852 false,
8853 window,
8854 cx,
8855 );
8856 workspace.add_item(
8857 pane.clone(),
8858 Box::new(dirty_multi_buffer.clone()),
8859 None,
8860 false,
8861 false,
8862 window,
8863 cx,
8864 );
8865 });
8866
8867 pane.update_in(cx, |pane, window, cx| {
8868 pane.activate_item(2, true, true, window, cx);
8869 assert_eq!(
8870 pane.active_item().unwrap().item_id(),
8871 dirty_multi_buffer.item_id(),
8872 "Should select the multi buffer in the pane"
8873 );
8874 });
8875 let close_multi_buffer_task = pane
8876 .update_in(cx, |pane, window, cx| {
8877 pane.close_active_item(
8878 &CloseActiveItem {
8879 save_intent: None,
8880 close_pinned: false,
8881 },
8882 window,
8883 cx,
8884 )
8885 })
8886 .expect("should have active multi buffer to close");
8887 cx.background_executor.run_until_parked();
8888 assert!(
8889 !cx.has_pending_prompt(),
8890 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
8891 );
8892 close_multi_buffer_task
8893 .await
8894 .expect("Closing multi buffer failed");
8895 pane.update(cx, |pane, cx| {
8896 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8897 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
8898 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8899 assert_eq!(
8900 pane.items()
8901 .map(|item| item.item_id())
8902 .sorted()
8903 .collect::<Vec<_>>(),
8904 vec![
8905 dirty_regular_buffer.item_id(),
8906 dirty_regular_buffer_2.item_id(),
8907 ],
8908 "Should have no multi buffer left in the pane"
8909 );
8910 assert!(dirty_regular_buffer.read(cx).is_dirty);
8911 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
8912 });
8913 }
8914
8915 #[gpui::test]
8916 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
8917 init_test(cx);
8918 let fs = FakeFs::new(cx.executor());
8919 let project = Project::test(fs, [], cx).await;
8920 let (workspace, cx) =
8921 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8922
8923 // Add a new panel to the right dock, opening the dock and setting the
8924 // focus to the new panel.
8925 let panel = workspace.update_in(cx, |workspace, window, cx| {
8926 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8927 workspace.add_panel(panel.clone(), window, cx);
8928
8929 workspace
8930 .right_dock()
8931 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8932
8933 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8934
8935 panel
8936 });
8937
8938 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8939 // panel to the next valid position which, in this case, is the left
8940 // dock.
8941 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8942 workspace.update(cx, |workspace, cx| {
8943 assert!(workspace.left_dock().read(cx).is_open());
8944 assert_eq!(panel.read(cx).position, DockPosition::Left);
8945 });
8946
8947 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8948 // panel to the next valid position which, in this case, is the bottom
8949 // dock.
8950 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8951 workspace.update(cx, |workspace, cx| {
8952 assert!(workspace.bottom_dock().read(cx).is_open());
8953 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
8954 });
8955
8956 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
8957 // around moving the panel to its initial position, the right dock.
8958 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8959 workspace.update(cx, |workspace, cx| {
8960 assert!(workspace.right_dock().read(cx).is_open());
8961 assert_eq!(panel.read(cx).position, DockPosition::Right);
8962 });
8963
8964 // Remove focus from the panel, ensuring that, if the panel is not
8965 // focused, the `MoveFocusedPanelToNextPosition` action does not update
8966 // the panel's position, so the panel is still in the right dock.
8967 workspace.update_in(cx, |workspace, window, cx| {
8968 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8969 });
8970
8971 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8972 workspace.update(cx, |workspace, cx| {
8973 assert!(workspace.right_dock().read(cx).is_open());
8974 assert_eq!(panel.read(cx).position, DockPosition::Right);
8975 });
8976 }
8977
8978 mod register_project_item_tests {
8979
8980 use super::*;
8981
8982 // View
8983 struct TestPngItemView {
8984 focus_handle: FocusHandle,
8985 }
8986 // Model
8987 struct TestPngItem {}
8988
8989 impl project::ProjectItem for TestPngItem {
8990 fn try_open(
8991 _project: &Entity<Project>,
8992 path: &ProjectPath,
8993 cx: &mut App,
8994 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8995 if path.path.extension().unwrap() == "png" {
8996 Some(cx.spawn(async move |cx| cx.new(|_| TestPngItem {})))
8997 } else {
8998 None
8999 }
9000 }
9001
9002 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
9003 None
9004 }
9005
9006 fn project_path(&self, _: &App) -> Option<ProjectPath> {
9007 None
9008 }
9009
9010 fn is_dirty(&self) -> bool {
9011 false
9012 }
9013 }
9014
9015 impl Item for TestPngItemView {
9016 type Event = ();
9017 }
9018 impl EventEmitter<()> for TestPngItemView {}
9019 impl Focusable for TestPngItemView {
9020 fn focus_handle(&self, _cx: &App) -> FocusHandle {
9021 self.focus_handle.clone()
9022 }
9023 }
9024
9025 impl Render for TestPngItemView {
9026 fn render(
9027 &mut self,
9028 _window: &mut Window,
9029 _cx: &mut Context<Self>,
9030 ) -> impl IntoElement {
9031 Empty
9032 }
9033 }
9034
9035 impl ProjectItem for TestPngItemView {
9036 type Item = TestPngItem;
9037
9038 fn for_project_item(
9039 _project: Entity<Project>,
9040 _pane: &Pane,
9041 _item: Entity<Self::Item>,
9042 _: &mut Window,
9043 cx: &mut Context<Self>,
9044 ) -> Self
9045 where
9046 Self: Sized,
9047 {
9048 Self {
9049 focus_handle: cx.focus_handle(),
9050 }
9051 }
9052 }
9053
9054 // View
9055 struct TestIpynbItemView {
9056 focus_handle: FocusHandle,
9057 }
9058 // Model
9059 struct TestIpynbItem {}
9060
9061 impl project::ProjectItem for TestIpynbItem {
9062 fn try_open(
9063 _project: &Entity<Project>,
9064 path: &ProjectPath,
9065 cx: &mut App,
9066 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
9067 if path.path.extension().unwrap() == "ipynb" {
9068 Some(cx.spawn(async move |cx| cx.new(|_| TestIpynbItem {})))
9069 } else {
9070 None
9071 }
9072 }
9073
9074 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
9075 None
9076 }
9077
9078 fn project_path(&self, _: &App) -> Option<ProjectPath> {
9079 None
9080 }
9081
9082 fn is_dirty(&self) -> bool {
9083 false
9084 }
9085 }
9086
9087 impl Item for TestIpynbItemView {
9088 type Event = ();
9089 }
9090 impl EventEmitter<()> for TestIpynbItemView {}
9091 impl Focusable for TestIpynbItemView {
9092 fn focus_handle(&self, _cx: &App) -> FocusHandle {
9093 self.focus_handle.clone()
9094 }
9095 }
9096
9097 impl Render for TestIpynbItemView {
9098 fn render(
9099 &mut self,
9100 _window: &mut Window,
9101 _cx: &mut Context<Self>,
9102 ) -> impl IntoElement {
9103 Empty
9104 }
9105 }
9106
9107 impl ProjectItem for TestIpynbItemView {
9108 type Item = TestIpynbItem;
9109
9110 fn for_project_item(
9111 _project: Entity<Project>,
9112 _pane: &Pane,
9113 _item: Entity<Self::Item>,
9114 _: &mut Window,
9115 cx: &mut Context<Self>,
9116 ) -> Self
9117 where
9118 Self: Sized,
9119 {
9120 Self {
9121 focus_handle: cx.focus_handle(),
9122 }
9123 }
9124 }
9125
9126 struct TestAlternatePngItemView {
9127 focus_handle: FocusHandle,
9128 }
9129
9130 impl Item for TestAlternatePngItemView {
9131 type Event = ();
9132 }
9133
9134 impl EventEmitter<()> for TestAlternatePngItemView {}
9135 impl Focusable for TestAlternatePngItemView {
9136 fn focus_handle(&self, _cx: &App) -> FocusHandle {
9137 self.focus_handle.clone()
9138 }
9139 }
9140
9141 impl Render for TestAlternatePngItemView {
9142 fn render(
9143 &mut self,
9144 _window: &mut Window,
9145 _cx: &mut Context<Self>,
9146 ) -> impl IntoElement {
9147 Empty
9148 }
9149 }
9150
9151 impl ProjectItem for TestAlternatePngItemView {
9152 type Item = TestPngItem;
9153
9154 fn for_project_item(
9155 _project: Entity<Project>,
9156 _pane: &Pane,
9157 _item: Entity<Self::Item>,
9158 _: &mut Window,
9159 cx: &mut Context<Self>,
9160 ) -> Self
9161 where
9162 Self: Sized,
9163 {
9164 Self {
9165 focus_handle: cx.focus_handle(),
9166 }
9167 }
9168 }
9169
9170 #[gpui::test]
9171 async fn test_register_project_item(cx: &mut TestAppContext) {
9172 init_test(cx);
9173
9174 cx.update(|cx| {
9175 register_project_item::<TestPngItemView>(cx);
9176 register_project_item::<TestIpynbItemView>(cx);
9177 });
9178
9179 let fs = FakeFs::new(cx.executor());
9180 fs.insert_tree(
9181 "/root1",
9182 json!({
9183 "one.png": "BINARYDATAHERE",
9184 "two.ipynb": "{ totally a notebook }",
9185 "three.txt": "editing text, sure why not?"
9186 }),
9187 )
9188 .await;
9189
9190 let project = Project::test(fs, ["root1".as_ref()], cx).await;
9191 let (workspace, cx) =
9192 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9193
9194 let worktree_id = project.update(cx, |project, cx| {
9195 project.worktrees(cx).next().unwrap().read(cx).id()
9196 });
9197
9198 let handle = workspace
9199 .update_in(cx, |workspace, window, cx| {
9200 let project_path = (worktree_id, "one.png");
9201 workspace.open_path(project_path, None, true, window, cx)
9202 })
9203 .await
9204 .unwrap();
9205
9206 // Now we can check if the handle we got back errored or not
9207 assert_eq!(
9208 handle.to_any().entity_type(),
9209 TypeId::of::<TestPngItemView>()
9210 );
9211
9212 let handle = workspace
9213 .update_in(cx, |workspace, window, cx| {
9214 let project_path = (worktree_id, "two.ipynb");
9215 workspace.open_path(project_path, None, true, window, cx)
9216 })
9217 .await
9218 .unwrap();
9219
9220 assert_eq!(
9221 handle.to_any().entity_type(),
9222 TypeId::of::<TestIpynbItemView>()
9223 );
9224
9225 let handle = workspace
9226 .update_in(cx, |workspace, window, cx| {
9227 let project_path = (worktree_id, "three.txt");
9228 workspace.open_path(project_path, None, true, window, cx)
9229 })
9230 .await;
9231 assert!(handle.is_err());
9232 }
9233
9234 #[gpui::test]
9235 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
9236 init_test(cx);
9237
9238 cx.update(|cx| {
9239 register_project_item::<TestPngItemView>(cx);
9240 register_project_item::<TestAlternatePngItemView>(cx);
9241 });
9242
9243 let fs = FakeFs::new(cx.executor());
9244 fs.insert_tree(
9245 "/root1",
9246 json!({
9247 "one.png": "BINARYDATAHERE",
9248 "two.ipynb": "{ totally a notebook }",
9249 "three.txt": "editing text, sure why not?"
9250 }),
9251 )
9252 .await;
9253 let project = Project::test(fs, ["root1".as_ref()], cx).await;
9254 let (workspace, cx) =
9255 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9256 let worktree_id = project.update(cx, |project, cx| {
9257 project.worktrees(cx).next().unwrap().read(cx).id()
9258 });
9259
9260 let handle = workspace
9261 .update_in(cx, |workspace, window, cx| {
9262 let project_path = (worktree_id, "one.png");
9263 workspace.open_path(project_path, None, true, window, cx)
9264 })
9265 .await
9266 .unwrap();
9267
9268 // This _must_ be the second item registered
9269 assert_eq!(
9270 handle.to_any().entity_type(),
9271 TypeId::of::<TestAlternatePngItemView>()
9272 );
9273
9274 let handle = workspace
9275 .update_in(cx, |workspace, window, cx| {
9276 let project_path = (worktree_id, "three.txt");
9277 workspace.open_path(project_path, None, true, window, cx)
9278 })
9279 .await;
9280 assert!(handle.is_err());
9281 }
9282 }
9283
9284 pub fn init_test(cx: &mut TestAppContext) {
9285 cx.update(|cx| {
9286 let settings_store = SettingsStore::test(cx);
9287 cx.set_global(settings_store);
9288 theme::init(theme::LoadThemes::JustBase, cx);
9289 language::init(cx);
9290 crate::init_settings(cx);
9291 Project::init_settings(cx);
9292 });
9293 }
9294
9295 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
9296 let item = TestProjectItem::new(id, path, cx);
9297 item.update(cx, |item, _| {
9298 item.is_dirty = true;
9299 });
9300 item
9301 }
9302}