debugger: Improve saving scenarios through new session modal (#30566)

Anthony Eid created

- A loading icon is displayed while a scenario is being saved
- Saving a scenario doesn't take you to debug.json unless a user clicks
on the arrow icons that shows up after a successful save
- An error icon where show when a scenario fails to save
- Fixed a bug where scenario's failed to save when there was no .zed
directory in the user's worktree


Release Notes:

- N/A

Change summary

crates/debugger_ui/src/debugger_panel.rs    |  28 +-
crates/debugger_ui/src/new_session_modal.rs | 204 ++++++++++++++++++----
2 files changed, 177 insertions(+), 55 deletions(-)

Detailed changes

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -22,7 +22,7 @@ use gpui::{
 
 use language::Buffer;
 use project::debugger::session::{Session, SessionStateEvent};
-use project::{Fs, WorktreeId};
+use project::{Fs, ProjectPath, WorktreeId};
 use project::{Project, debugger::session::ThreadStatus};
 use rpc::proto::{self};
 use settings::Settings;
@@ -997,7 +997,7 @@ impl DebugPanel {
         worktree_id: WorktreeId,
         window: &mut Window,
         cx: &mut App,
-    ) -> Task<Result<()>> {
+    ) -> Task<Result<ProjectPath>> {
         self.workspace
             .update(cx, |workspace, cx| {
                 let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
@@ -1006,14 +1006,20 @@ impl DebugPanel {
 
                 let serialized_scenario = serde_json::to_value(scenario);
 
-                path.push(paths::local_debug_file_relative_path());
-
                 cx.spawn_in(window, async move |workspace, cx| {
                     let serialized_scenario = serialized_scenario?;
-                    let path = path.as_path();
                     let fs =
                         workspace.update(cx, |workspace, _| workspace.app_state().fs.clone())?;
 
+                    path.push(paths::local_settings_folder_relative_path());
+                    if !fs.is_dir(path.as_path()).await {
+                        fs.create_dir(path.as_path()).await?;
+                    }
+                    path.pop();
+
+                    path.push(paths::local_debug_file_relative_path());
+                    let path = path.as_path();
+
                     if !fs.is_file(path).await {
                         let content =
                             serde_json::to_string_pretty(&serde_json::Value::Array(vec![
@@ -1034,21 +1040,19 @@ impl DebugPanel {
                         .await?;
                     }
 
-                    workspace.update_in(cx, |workspace, window, cx| {
+                    workspace.update(cx, |workspace, cx| {
                         if let Some(project_path) = workspace
                             .project()
                             .read(cx)
                             .project_path_for_absolute_path(&path, cx)
                         {
-                            workspace.open_path(project_path, None, true, window, cx)
+                            Ok(project_path)
                         } else {
-                            Task::ready(Err(anyhow!(
+                            Err(anyhow!(
                                 "Couldn't get project path for .zed/debug.json in active worktree"
-                            )))
+                            ))
                         }
-                    })?.await?;
-
-                    anyhow::Ok(())
+                    })?
                 })
             })
             .unwrap_or_else(|err| Task::ready(Err(err)))

crates/debugger_ui/src/new_session_modal.rs 🔗

@@ -4,9 +4,11 @@ use std::{
     ops::Not,
     path::{Path, PathBuf},
     sync::Arc,
+    time::Duration,
     usize,
 };
 
+use anyhow::Result;
 use dap::{
     DapRegistry, DebugRequest,
     adapters::{DebugAdapterName, DebugTaskDefinition},
@@ -14,26 +16,32 @@ use dap::{
 use editor::{Editor, EditorElement, EditorStyle};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render,
-    Subscription, TextStyle, WeakEntity,
+    Animation, AnimationExt as _, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle,
+    Focusable, Render, Subscription, TextStyle, Transformation, WeakEntity, percentage,
 };
 use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
-use project::{TaskContexts, TaskSourceKind, task_store::TaskStore};
+use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
 use settings::Settings;
 use task::{DebugScenario, LaunchRequest};
 use theme::ThemeSettings;
 use ui::{
     ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
-    ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, InteractiveElement,
-    IntoElement, Label, LabelCommon as _, ListItem, ListItemSpacing, ParentElement, RenderOnce,
-    SharedString, Styled, StyledExt, ToggleButton, ToggleState, Toggleable, Window, div, h_flex,
-    relative, rems, v_flex,
+    ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconButton, IconName, IconSize,
+    InteractiveElement, IntoElement, Label, LabelCommon as _, ListItem, ListItemSpacing,
+    ParentElement, RenderOnce, SharedString, Styled, StyledExt, ToggleButton, ToggleState,
+    Toggleable, Window, div, h_flex, relative, rems, v_flex,
 };
 use util::ResultExt;
 use workspace::{ModalView, Workspace, pane};
 
 use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
 
+enum SaveScenarioState {
+    Saving,
+    Saved(ProjectPath),
+    Failed(SharedString),
+}
+
 pub(super) struct NewSessionModal {
     workspace: WeakEntity<Workspace>,
     debug_panel: WeakEntity<DebugPanel>,
@@ -43,6 +51,7 @@ pub(super) struct NewSessionModal {
     custom_mode: Entity<CustomMode>,
     debugger: Option<DebugAdapterName>,
     task_contexts: Arc<TaskContexts>,
+    save_scenario_state: Option<SaveScenarioState>,
     _subscriptions: [Subscription; 2],
 }
 
@@ -126,6 +135,7 @@ impl NewSessionModal {
                         debug_panel: debug_panel.downgrade(),
                         workspace: workspace_handle,
                         task_contexts,
+                        save_scenario_state: None,
                         _subscriptions,
                     }
                 });
@@ -220,7 +230,7 @@ impl NewSessionModal {
                 cx.emit(DismissEvent);
             })
             .ok();
-            anyhow::Result::<_, anyhow::Error>::Ok(())
+            Result::<_, anyhow::Error>::Ok(())
         })
         .detach_and_log_err(cx);
     }
@@ -380,6 +390,8 @@ impl Render for NewSessionModal {
         window: &mut ui::Window,
         cx: &mut ui::Context<Self>,
     ) -> impl ui::IntoElement {
+        let this = cx.weak_entity().clone();
+
         v_flex()
             .size_full()
             .w(rems(34.))
@@ -485,42 +497,148 @@ impl Render for NewSessionModal {
                                 }
                             }),
                         ),
-                        NewSessionMode::Custom => div().child(
-                            Button::new("new-session-modal-back", "Save to .zed/debug.json...")
-                                .on_click(cx.listener(|this, _, window, cx| {
-                                    let Some(save_scenario_task) = this
-                                        .debugger
-                                        .as_ref()
-                                        .and_then(|debugger| this.debug_scenario(&debugger, cx))
-                                        .zip(this.task_contexts.worktree())
-                                        .and_then(|(scenario, worktree_id)| {
-                                            this.debug_panel
-                                                .update(cx, |panel, cx| {
-                                                    panel.save_scenario(
-                                                        &scenario,
-                                                        worktree_id,
-                                                        window,
-                                                        cx,
-                                                    )
-                                                })
-                                                .ok()
+                        NewSessionMode::Custom => h_flex()
+                            .child(
+                                Button::new("new-session-modal-back", "Save to .zed/debug.json...")
+                                    .on_click(cx.listener(|this, _, window, cx| {
+                                        let Some(save_scenario) = this
+                                            .debugger
+                                            .as_ref()
+                                            .and_then(|debugger| this.debug_scenario(&debugger, cx))
+                                            .zip(this.task_contexts.worktree())
+                                            .and_then(|(scenario, worktree_id)| {
+                                                this.debug_panel
+                                                    .update(cx, |panel, cx| {
+                                                        panel.save_scenario(
+                                                            &scenario,
+                                                            worktree_id,
+                                                            window,
+                                                            cx,
+                                                        )
+                                                    })
+                                                    .ok()
+                                            })
+                                        else {
+                                            return;
+                                        };
+
+                                        this.save_scenario_state = Some(SaveScenarioState::Saving);
+
+                                        cx.spawn(async move |this, cx| {
+                                            let res = save_scenario.await;
+
+                                            this.update(cx, |this, _| match res {
+                                                Ok(saved_file) => {
+                                                    this.save_scenario_state =
+                                                        Some(SaveScenarioState::Saved(saved_file))
+                                                }
+                                                Err(error) => {
+                                                    this.save_scenario_state =
+                                                        Some(SaveScenarioState::Failed(
+                                                            error.to_string().into(),
+                                                        ))
+                                                }
+                                            })
+                                            .ok();
+
+                                            cx.background_executor()
+                                                .timer(Duration::from_secs(2))
+                                                .await;
+                                            this.update(cx, |this, _| {
+                                                this.save_scenario_state.take()
+                                            })
+                                            .ok();
                                         })
-                                    else {
-                                        return;
-                                    };
-
-                                    cx.spawn(async move |this, cx| {
-                                        if save_scenario_task.await.is_ok() {
-                                            this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
-                                        }
-                                    })
-                                    .detach();
-                                }))
-                                .disabled(
-                                    self.debugger.is_none()
-                                        || self.custom_mode.read(cx).program.read(cx).is_empty(cx),
-                                ),
-                        ),
+                                        .detach();
+                                    }))
+                                    .disabled(
+                                        self.debugger.is_none()
+                                            || self
+                                                .custom_mode
+                                                .read(cx)
+                                                .program
+                                                .read(cx)
+                                                .is_empty(cx)
+                                            || self.save_scenario_state.is_some(),
+                                    ),
+                            )
+                            .when_some(self.save_scenario_state.as_ref(), {
+                                let this_entity = this.clone();
+
+                                move |this, save_state| match save_state {
+                                    SaveScenarioState::Saved(saved_path) => this.child(
+                                        IconButton::new(
+                                            "new-session-modal-go-to-file",
+                                            IconName::ArrowUpRight,
+                                        )
+                                        .icon_size(IconSize::Small)
+                                        .icon_color(Color::Muted)
+                                        .on_click({
+                                            let this_entity = this_entity.clone();
+                                            let saved_path = saved_path.clone();
+                                            move |_, window, cx| {
+                                                window
+                                                    .spawn(cx, {
+                                                        let this_entity = this_entity.clone();
+                                                        let saved_path = saved_path.clone();
+
+                                                        async move |cx| {
+                                                            this_entity
+                                                                .update_in(
+                                                                    cx,
+                                                                    |this, window, cx| {
+                                                                        this.workspace.update(
+                                                                            cx,
+                                                                            |workspace, cx| {
+                                                                                workspace.open_path(
+                                                                                    saved_path
+                                                                                        .clone(),
+                                                                                    None,
+                                                                                    true,
+                                                                                    window,
+                                                                                    cx,
+                                                                                )
+                                                                            },
+                                                                        )
+                                                                    },
+                                                                )??
+                                                                .await?;
+
+                                                            this_entity
+                                                                .update(cx, |_, cx| {
+                                                                    cx.emit(DismissEvent)
+                                                                })
+                                                                .ok();
+
+                                                            anyhow::Ok(())
+                                                        }
+                                                    })
+                                                    .detach();
+                                            }
+                                        }),
+                                    ),
+                                    SaveScenarioState::Saving => this.child(
+                                        Icon::new(IconName::Spinner)
+                                            .size(IconSize::Small)
+                                            .color(Color::Muted)
+                                            .with_animation(
+                                                "Spinner",
+                                                Animation::new(Duration::from_secs(3)).repeat(),
+                                                |icon, delta| {
+                                                    icon.transform(Transformation::rotate(
+                                                        percentage(delta),
+                                                    ))
+                                                },
+                                            ),
+                                    ),
+                                    SaveScenarioState::Failed(error_msg) => this.child(
+                                        IconButton::new("Failed Scenario Saved", IconName::X)
+                                            .icon_size(IconSize::Small)
+                                            .icon_color(Color::Error)
+                                            .tooltip(ui::Tooltip::text(error_msg.clone())),
+                                    ),
+                                }
+                            }),
                     })
                     .child(
                         Button::new("debugger-spawn", "Start")