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