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