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