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