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