@@ -1,5 +1,5 @@
use collections::FxHashMap;
-use language::LanguageRegistry;
+use language::{LanguageRegistry, Point, Selection};
use std::{
borrow::Cow,
ops::Not,
@@ -12,7 +12,7 @@ use std::{
use dap::{
DapRegistry, DebugRequest, TelemetrySpawnLocation, adapters::DebugAdapterName, send_telemetry,
};
-use editor::{Editor, EditorElement, EditorStyle};
+use editor::{Anchor, Editor, EditorElement, EditorStyle, scroll::Autoscroll};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
Animation, AnimationExt as _, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle,
@@ -37,7 +37,7 @@ use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
enum SaveScenarioState {
Saving,
- Saved(ProjectPath),
+ Saved((ProjectPath, SharedString)),
Failed(SharedString),
}
@@ -284,6 +284,177 @@ impl NewSessionModal {
self.launch_picker.read(cx).delegate.task_contexts.clone()
}
+ fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ let Some((save_scenario, scenario_label)) = self
+ .debugger
+ .as_ref()
+ .and_then(|debugger| self.debug_scenario(&debugger, cx))
+ .zip(self.task_contexts(cx).and_then(|tcx| tcx.worktree()))
+ .and_then(|(scenario, worktree_id)| {
+ self.debug_panel
+ .update(cx, |panel, cx| {
+ panel.save_scenario(&scenario, worktree_id, window, cx)
+ })
+ .ok()
+ .zip(Some(scenario.label.clone()))
+ })
+ else {
+ return;
+ };
+
+ self.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, scenario_label)))
+ }
+ Err(error) => {
+ this.save_scenario_state =
+ Some(SaveScenarioState::Failed(error.to_string().into()))
+ }
+ })
+ .ok();
+
+ cx.background_executor().timer(Duration::from_secs(3)).await;
+ this.update(cx, |this, _| this.save_scenario_state.take())
+ .ok();
+ })
+ .detach();
+ }
+
+ fn render_save_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
+ let this_entity = cx.weak_entity().clone();
+
+ div().when_some(self.save_scenario_state.as_ref(), {
+ let this_entity = this_entity.clone();
+
+ move |this, save_state| match save_state {
+ SaveScenarioState::Saved((saved_path, scenario_label)) => 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();
+ let scenario_label = scenario_label.clone();
+ move |_, window, cx| {
+ window
+ .spawn(cx, {
+ let this_entity = this_entity.clone();
+ let saved_path = saved_path.clone();
+ let scenario_label = scenario_label.clone();
+
+ async move |cx| {
+ let editor = 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?;
+
+ cx.update(|window, cx| {
+ if let Some(editor) = editor.act_as::<Editor>(cx) {
+ editor.update(cx, |editor, cx| {
+ let row = editor
+ .text(cx)
+ .lines()
+ .enumerate()
+ .find_map(|(row, text)| {
+ if text.contains(
+ scenario_label.as_ref(),
+ ) {
+ Some(row)
+ } else {
+ None
+ }
+ })?;
+
+ let buffer = editor.buffer().read(cx);
+ let excerpt_id =
+ *buffer.excerpt_ids().first()?;
+
+ let snapshot = buffer
+ .as_singleton()?
+ .read(cx)
+ .snapshot();
+
+ let anchor = snapshot.anchor_before(
+ Point::new(row as u32, 0),
+ );
+
+ let anchor = Anchor {
+ buffer_id: anchor.buffer_id,
+ excerpt_id,
+ text_anchor: anchor,
+ diff_base_anchor: None,
+ };
+
+ editor.change_selections(
+ Some(Autoscroll::center()),
+ window,
+ cx,
+ |selections| {
+ let id =
+ selections.new_selection_id();
+ selections.select_anchors(
+ vec![Selection {
+ id,
+ start: anchor,
+ end: anchor,
+ reversed: false,
+ goal: language::SelectionGoal::None
+ }],
+ );
+ },
+ );
+
+ Some(())
+ });
+ }
+ })?;
+
+ 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())),
+ ),
+ }
+ })
+ }
+
fn adapter_drop_down_menu(
&mut self,
window: &mut Window,
@@ -355,7 +526,7 @@ impl NewSessionModal {
static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
#[derive(Clone)]
-enum NewSessionMode {
+pub(crate) enum NewSessionMode {
Custom,
Attach,
Launch,
@@ -423,8 +594,6 @@ 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.))
@@ -534,58 +703,7 @@ impl Render for NewSessionModal {
.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(cx)
- .and_then(|tcx| tcx.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();
- })
- .detach();
+ this.save_debug_scenario(window, cx);
}))
.disabled(
self.debugger.is_none()
@@ -598,83 +716,7 @@ impl Render for NewSessionModal {
|| 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(self.render_save_state(cx)),
})
.child(
Button::new("debugger-spawn", "Start")
@@ -1162,6 +1204,42 @@ pub(crate) fn resolve_path(path: &mut String) {
};
}
+#[cfg(test)]
+impl NewSessionModal {
+ pub(crate) fn set_custom(
+ &mut self,
+ program: impl AsRef<str>,
+ cwd: impl AsRef<str>,
+ stop_on_entry: bool,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.mode = NewSessionMode::Custom;
+ self.debugger = Some(dap::adapters::DebugAdapterName("fake-adapter".into()));
+
+ self.custom_mode.update(cx, |custom, cx| {
+ custom.program.update(cx, |editor, cx| {
+ editor.clear(window, cx);
+ editor.set_text(program.as_ref(), window, cx);
+ });
+
+ custom.cwd.update(cx, |editor, cx| {
+ editor.clear(window, cx);
+ editor.set_text(cwd.as_ref(), window, cx);
+ });
+
+ custom.stop_on_entry = match stop_on_entry {
+ true => ToggleState::Selected,
+ _ => ToggleState::Unselected,
+ }
+ })
+ }
+
+ pub(crate) fn save_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.save_debug_scenario(window, cx);
+ }
+}
+
#[cfg(test)]
mod tests {
use paths::home_dir;
@@ -1,6 +1,6 @@
use dap::DapRegistry;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
-use project::{FakeFs, Project};
+use project::{FakeFs, Fs, Project};
use serde_json::json;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
@@ -151,6 +151,106 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths(
}
}
+#[gpui::test]
+async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(executor.clone());
+ fs.insert_tree(
+ path!("/project"),
+ json!({
+ "main.rs": "fn main() {}"
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
+ let workspace = init_test_workspace(&project, cx).await;
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+
+ workspace
+ .update(cx, |workspace, window, cx| {
+ crate::new_session_modal::NewSessionModal::show(workspace, window, cx);
+ })
+ .unwrap();
+
+ cx.run_until_parked();
+
+ let modal = workspace
+ .update(cx, |workspace, _, cx| {
+ workspace.active_modal::<crate::new_session_modal::NewSessionModal>(cx)
+ })
+ .unwrap()
+ .expect("Modal should be active");
+
+ modal.update_in(cx, |modal, window, cx| {
+ modal.set_custom("/project/main", "/project", false, window, cx);
+ modal.save_scenario(window, cx);
+ });
+
+ cx.executor().run_until_parked();
+
+ let debug_json_content = fs
+ .load(path!("/project/.zed/debug.json").as_ref())
+ .await
+ .expect("debug.json should exist");
+
+ let expected_content = vec![
+ "[",
+ " {",
+ r#" "adapter": "fake-adapter","#,
+ r#" "label": "main (fake-adapter)","#,
+ r#" "request": "launch","#,
+ r#" "program": "/project/main","#,
+ r#" "cwd": "/project","#,
+ r#" "args": [],"#,
+ r#" "env": {}"#,
+ " }",
+ "]",
+ ];
+
+ let actual_lines: Vec<&str> = debug_json_content.lines().collect();
+ pretty_assertions::assert_eq!(expected_content, actual_lines);
+
+ modal.update_in(cx, |modal, window, cx| {
+ modal.set_custom("/project/other", "/project", true, window, cx);
+ modal.save_scenario(window, cx);
+ });
+
+ cx.executor().run_until_parked();
+
+ let debug_json_content = fs
+ .load(path!("/project/.zed/debug.json").as_ref())
+ .await
+ .expect("debug.json should exist after second save");
+
+ let expected_content = vec![
+ "[",
+ " {",
+ r#" "adapter": "fake-adapter","#,
+ r#" "label": "main (fake-adapter)","#,
+ r#" "request": "launch","#,
+ r#" "program": "/project/main","#,
+ r#" "cwd": "/project","#,
+ r#" "args": [],"#,
+ r#" "env": {}"#,
+ " },",
+ " {",
+ r#" "adapter": "fake-adapter","#,
+ r#" "label": "other (fake-adapter)","#,
+ r#" "request": "launch","#,
+ r#" "program": "/project/other","#,
+ r#" "cwd": "/project","#,
+ r#" "args": [],"#,
+ r#" "env": {}"#,
+ " }",
+ "]",
+ ];
+
+ let actual_lines: Vec<&str> = debug_json_content.lines().collect();
+ pretty_assertions::assert_eq!(expected_content, actual_lines);
+}
+
#[gpui::test]
async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
init_test(cx);