debugger beta: Autoscroll to recently saved debug scenario when saving a scenario (#31528)
I added a test to this too as one of my first steps of improving `NewSessionModal`'s test coverage. Release Notes: - debugger beta: Select saved debug config when opening debug.json from `NewSessionModal`
This commit is contained in:
parent
94c006236e
commit
86b75759d1
3 changed files with 319 additions and 137 deletions
|
@ -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);
|
||||
|
|
|
@ -270,7 +270,11 @@ pub fn task_contexts(
|
|||
.read(cx)
|
||||
.worktree_for_id(*worktree_id, cx)
|
||||
.map_or(false, |worktree| is_visible_directory(&worktree, cx))
|
||||
});
|
||||
})
|
||||
.or(workspace
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.map(|tree| tree.read(cx).id()));
|
||||
|
||||
let active_editor = active_item.and_then(|item| item.act_as::<Editor>(cx));
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue