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