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