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