Simplify debug launcher UI (#31928)

This PR updates the name of the `NewSessionModal` to `NewProcessModal`
(to reflect it's new purpose), changes the tabs in the modal to read
`Run | Debug | Attach | Launch` and changes the associated types in code
to match the tabs. In addition, this PR adds a few labels to the text
fields in the `Launch` tab, and adds a link to open the associated
settings file. In both debug.json files, added links to the zed.dev
debugger docs.

Release Notes:

- Debugger Beta: Improve the new process modal
This commit is contained in:
Mikayla Maki 2025-06-02 14:24:08 -07:00 committed by GitHub
parent f1aab1120d
commit b7ec437b13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 497 additions and 490 deletions

View file

@ -1,3 +1,7 @@
// Some example tasks for common languages.
//
// For more documentation on how to configure debug tasks,
// see: https://zed.dev/docs/debugger
[
{
"label": "Debug active PHP file",

View file

@ -0,0 +1,5 @@
// Project-local debug tasks
//
// For more documentation on how to configure debug tasks,
// see: https://zed.dev/docs/debugger
[]

View file

@ -50,6 +50,7 @@ project.workspace = true
rpc.workspace = true
serde.workspace = true
serde_json.workspace = true
# serde_json_lenient.workspace = true
settings.workspace = true
shlex.workspace = true
sysinfo.workspace = true

View file

@ -7,7 +7,7 @@ use crate::{
ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
};
use anyhow::{Context as _, Result, anyhow};
use anyhow::Result;
use command_palette_hooks::CommandPaletteFilter;
use dap::StartDebuggingRequestArguments;
use dap::adapters::DebugAdapterName;
@ -24,7 +24,7 @@ use gpui::{
use language::Buffer;
use project::debugger::session::{Session, SessionStateEvent};
use project::{Fs, ProjectPath, WorktreeId};
use project::{Fs, WorktreeId};
use project::{Project, debugger::session::ThreadStatus};
use rpc::proto::{self};
use settings::Settings;
@ -942,68 +942,69 @@ impl DebugPanel {
cx.notify();
}
pub(crate) fn save_scenario(
&self,
scenario: &DebugScenario,
worktree_id: WorktreeId,
window: &mut Window,
cx: &mut App,
) -> Task<Result<ProjectPath>> {
self.workspace
.update(cx, |workspace, cx| {
let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
return Task::ready(Err(anyhow!("Couldn't get worktree path")));
};
// TODO: restore once we have proper comment preserving file edits
// pub(crate) fn save_scenario(
// &self,
// scenario: &DebugScenario,
// worktree_id: WorktreeId,
// window: &mut Window,
// cx: &mut App,
// ) -> Task<Result<ProjectPath>> {
// self.workspace
// .update(cx, |workspace, cx| {
// let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
// return Task::ready(Err(anyhow!("Couldn't get worktree path")));
// };
let serialized_scenario = serde_json::to_value(scenario);
// let serialized_scenario = serde_json::to_value(scenario);
cx.spawn_in(window, async move |workspace, cx| {
let serialized_scenario = serialized_scenario?;
let fs =
workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
// cx.spawn_in(window, async move |workspace, cx| {
// let serialized_scenario = serialized_scenario?;
// let fs =
// workspace.read_with(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_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();
// 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![
serialized_scenario,
]))?;
// if !fs.is_file(path).await {
// fs.create_file(path, Default::default()).await?;
// fs.write(
// path,
// initial_local_debug_tasks_content().to_string().as_bytes(),
// )
// .await?;
// }
fs.create_file(path, Default::default()).await?;
fs.save(path, &content.into(), Default::default()).await?;
} else {
let content = fs.load(path).await?;
let mut values = serde_json::from_str::<Vec<serde_json::Value>>(&content)?;
values.push(serialized_scenario);
fs.save(
path,
&serde_json::to_string_pretty(&values).map(Into::into)?,
Default::default(),
)
.await?;
}
// let content = fs.load(path).await?;
// let mut values =
// serde_json_lenient::from_str::<Vec<serde_json::Value>>(&content)?;
// values.push(serialized_scenario);
// fs.save(
// path,
// &serde_json_lenient::to_string_pretty(&values).map(Into::into)?,
// Default::default(),
// )
// .await?;
workspace.update(cx, |workspace, cx| {
workspace
.project()
.read(cx)
.project_path_for_absolute_path(&path, cx)
.context(
"Couldn't get project path for .zed/debug.json in active worktree",
)
})?
})
})
.unwrap_or_else(|err| Task::ready(Err(err)))
}
// workspace.update(cx, |workspace, cx| {
// workspace
// .project()
// .read(cx)
// .project_path_for_absolute_path(&path, cx)
// .context(
// "Couldn't get project path for .zed/debug.json in active worktree",
// )
// })?
// })
// })
// .unwrap_or_else(|err| Task::ready(Err(err)))
// }
pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.thread_picker_menu_handle.toggle(window, cx);

View file

@ -3,7 +3,7 @@ use debugger_panel::{DebugPanel, ToggleFocus};
use editor::Editor;
use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};
use gpui::{App, EntityInputHandler, actions};
use new_session_modal::{NewSessionModal, NewSessionMode};
use new_process_modal::{NewProcessModal, NewProcessMode};
use project::debugger::{self, breakpoint_store::SourceBreakpoint};
use session::DebugSession;
use settings::Settings;
@ -15,7 +15,7 @@ use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
pub mod attach_modal;
pub mod debugger_panel;
mod dropdown_menus;
mod new_session_modal;
mod new_process_modal;
mod persistence;
pub(crate) mod session;
mod stack_trace_view;
@ -210,7 +210,7 @@ pub fn init(cx: &mut App) {
},
)
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
NewSessionModal::show(workspace, window, NewSessionMode::Launch, None, cx);
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
})
.register_action(
|workspace: &mut Workspace, _: &RerunLastSession, window, cx| {
@ -352,7 +352,7 @@ fn spawn_task_or_modal(
.detach_and_log_err(cx)
}
Spawn::ViaModal { reveal_target } => {
NewSessionModal::show(workspace, window, NewSessionMode::Task, *reveal_target, cx);
NewProcessModal::show(workspace, window, NewProcessMode::Task, *reveal_target, cx);
}
}
}

View file

@ -1,11 +1,10 @@
use collections::FxHashMap;
use language::{LanguageRegistry, Point, Selection};
use language::LanguageRegistry;
use paths::local_debug_file_relative_path;
use std::{
borrow::Cow,
ops::Not,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
usize,
};
use tasks_ui::{TaskOverrides, TasksModal};
@ -13,45 +12,47 @@ use tasks_ui::{TaskOverrides, TasksModal};
use dap::{
DapRegistry, DebugRequest, TelemetrySpawnLocation, adapters::DebugAdapterName, send_telemetry,
};
use editor::{Anchor, Editor, EditorElement, EditorStyle, scroll::Autoscroll};
use editor::{Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
Animation, AnimationExt as _, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle,
Focusable, KeyContext, Render, Subscription, TextStyle, Transformation, WeakEntity, percentage,
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle,
InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, Subscription,
TextStyle, UnderlineStyle, WeakEntity,
};
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
use settings::Settings;
use task::{DebugScenario, LaunchRequest, RevealTarget, ZedDebugConfig};
use settings::{Settings, initial_local_debug_tasks_content};
use task::{DebugScenario, RevealTarget, ZedDebugConfig};
use theme::ThemeSettings;
use ui::{
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconButton, IconName, IconSize,
ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize,
IconWithIndicator, Indicator, InteractiveElement, IntoElement, Label, LabelCommon as _,
ListItem, ListItemSpacing, ParentElement, RenderOnce, SharedString, Styled, StyledExt,
ToggleButton, ToggleState, Toggleable, Window, div, h_flex, relative, rems, v_flex,
StyledTypography, ToggleButton, ToggleState, Toggleable, Window, div, h_flex, px, 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, SharedString)),
Failed(SharedString),
}
// enum SaveScenarioState {
// Saving,
// Saved((ProjectPath, SharedString)),
// Failed(SharedString),
// }
pub(super) struct NewSessionModal {
pub(super) struct NewProcessModal {
workspace: WeakEntity<Workspace>,
debug_panel: WeakEntity<DebugPanel>,
mode: NewSessionMode,
launch_picker: Entity<Picker<DebugScenarioDelegate>>,
mode: NewProcessMode,
debug_picker: Entity<Picker<DebugDelegate>>,
attach_mode: Entity<AttachMode>,
configure_mode: Entity<ConfigureMode>,
launch_mode: Entity<LaunchMode>,
task_mode: TaskMode,
debugger: Option<DebugAdapterName>,
save_scenario_state: Option<SaveScenarioState>,
// save_scenario_state: Option<SaveScenarioState>,
_subscriptions: [Subscription; 3],
}
@ -73,11 +74,11 @@ fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString {
}
}
impl NewSessionModal {
impl NewProcessModal {
pub(super) fn show(
workspace: &mut Workspace,
window: &mut Window,
mode: NewSessionMode,
mode: NewProcessMode,
reveal_target: Option<RevealTarget>,
cx: &mut Context<Workspace>,
) {
@ -101,12 +102,12 @@ impl NewSessionModal {
let launch_picker = cx.new(|cx| {
let mut delegate =
DebugScenarioDelegate::new(debug_panel.downgrade(), task_store.clone());
DebugDelegate::new(debug_panel.downgrade(), task_store.clone());
delegate.task_contexts_loaded(task_contexts.clone(), languages, window, cx);
Picker::uniform_list(delegate, window, cx).modal(false)
});
let configure_mode = ConfigureMode::new(None, window, cx);
let configure_mode = LaunchMode::new(window, cx);
if let Some(active_cwd) = task_contexts
.active_context()
.and_then(|context| context.cwd.clone())
@ -148,15 +149,15 @@ impl NewSessionModal {
];
Self {
launch_picker,
debug_picker: launch_picker,
attach_mode,
configure_mode,
launch_mode: configure_mode,
task_mode,
debugger: None,
mode,
debug_panel: debug_panel.downgrade(),
workspace: workspace_handle,
save_scenario_state: None,
// save_scenario_state: None,
_subscriptions,
}
});
@ -170,49 +171,49 @@ impl NewSessionModal {
fn render_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
let dap_menu = self.adapter_drop_down_menu(window, cx);
match self.mode {
NewSessionMode::Task => self
NewProcessMode::Task => self
.task_mode
.task_modal
.read(cx)
.picker
.clone()
.into_any_element(),
NewSessionMode::Attach => self.attach_mode.update(cx, |this, cx| {
NewProcessMode::Attach => self.attach_mode.update(cx, |this, cx| {
this.clone().render(window, cx).into_any_element()
}),
NewSessionMode::Configure => self.configure_mode.update(cx, |this, cx| {
NewProcessMode::Launch => self.launch_mode.update(cx, |this, cx| {
this.clone().render(dap_menu, window, cx).into_any_element()
}),
NewSessionMode::Launch => v_flex()
NewProcessMode::Debug => v_flex()
.w(rems(34.))
.child(self.launch_picker.clone())
.child(self.debug_picker.clone())
.into_any_element(),
}
}
fn mode_focus_handle(&self, cx: &App) -> FocusHandle {
match self.mode {
NewSessionMode::Task => self.task_mode.task_modal.focus_handle(cx),
NewSessionMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx),
NewSessionMode::Configure => self.configure_mode.read(cx).program.focus_handle(cx),
NewSessionMode::Launch => self.launch_picker.focus_handle(cx),
NewProcessMode::Task => self.task_mode.task_modal.focus_handle(cx),
NewProcessMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx),
NewProcessMode::Launch => self.launch_mode.read(cx).program.focus_handle(cx),
NewProcessMode::Debug => self.debug_picker.focus_handle(cx),
}
}
fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> {
let request = match self.mode {
NewSessionMode::Configure => Some(DebugRequest::Launch(
self.configure_mode.read(cx).debug_request(cx),
NewProcessMode::Launch => Some(DebugRequest::Launch(
self.launch_mode.read(cx).debug_request(cx),
)),
NewSessionMode::Attach => Some(DebugRequest::Attach(
NewProcessMode::Attach => Some(DebugRequest::Attach(
self.attach_mode.read(cx).debug_request(),
)),
_ => None,
}?;
let label = suggested_label(&request, debugger);
let stop_on_entry = if let NewSessionMode::Configure = &self.mode {
Some(self.configure_mode.read(cx).stop_on_entry.selected())
let stop_on_entry = if let NewProcessMode::Launch = &self.mode {
Some(self.launch_mode.read(cx).stop_on_entry.selected())
} else {
None
};
@ -229,18 +230,29 @@ impl NewSessionModal {
.and_then(|adapter| adapter.config_from_zed_format(session_scenario).ok())
}
fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) {
let Some(debugger) = self.debugger.as_ref() else {
fn start_new_session(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.debugger.as_ref().is_none() {
return;
};
}
if let NewSessionMode::Launch = &self.mode {
self.launch_picker.update(cx, |picker, cx| {
if let NewProcessMode::Debug = &self.mode {
self.debug_picker.update(cx, |picker, cx| {
picker.delegate.confirm(false, window, cx);
});
return;
}
// TODO: Restore once we have proper, comment preserving edits
// if let NewProcessMode::Launch = &self.mode {
// if self.launch_mode.read(cx).save_to_debug_json.selected() {
// self.save_debug_scenario(window, cx);
// }
// }
let Some(debugger) = self.debugger.as_ref() else {
return;
};
let Some(config) = self.debug_scenario(debugger, cx) else {
log::error!("debug config not found in mode: {}", self.mode);
return;
@ -289,179 +301,50 @@ impl NewSessionModal {
}
fn task_contexts(&self, cx: &App) -> Option<Arc<TaskContexts>> {
self.launch_picker.read(cx).delegate.task_contexts.clone()
self.debug_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;
};
// 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);
// self.save_scenario_state = Some(SaveScenarioState::Saving);
cx.spawn(async move |this, cx| {
let res = save_scenario.await;
// 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();
// 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())),
),
}
})
}
// cx.background_executor().timer(Duration::from_secs(3)).await;
// this.update(cx, |this, _| this.save_scenario_state.take())
// .ok();
// })
// .detach();
// }
fn adapter_drop_down_menu(
&mut self,
@ -513,7 +396,7 @@ impl NewSessionModal {
weak.update(cx, |this, cx| {
this.debugger = Some(name.clone());
cx.notify();
if let NewSessionMode::Attach = &this.mode {
if let NewProcessMode::Attach = &this.mode {
Self::update_attach_picker(&this.attach_mode, &name, window, cx);
}
})
@ -529,32 +412,96 @@ impl NewSessionModal {
}),
)
}
fn open_debug_json(&self, window: &mut Window, cx: &mut Context<NewProcessModal>) {
let this = cx.entity();
window
.spawn(cx, async move |cx| {
let worktree_id = this.update(cx, |this, cx| {
let tcx = this.task_contexts(cx);
tcx?.worktree()
})?;
let Some(worktree_id) = worktree_id else {
let _ = cx.prompt(
PromptLevel::Critical,
"Cannot open debug.json",
Some("You must have at least one project open"),
&[PromptButton::ok("Ok")],
);
return Ok(());
};
let editor = this
.update_in(cx, |this, window, cx| {
this.workspace.update(cx, |workspace, cx| {
workspace.open_path(
ProjectPath {
worktree_id,
path: local_debug_file_relative_path().into(),
},
None,
true,
window,
cx,
)
})
})??
.await?;
cx.update(|_window, cx| {
if let Some(editor) = editor.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| {
editor.buffer().update(cx, |buffer, cx| {
if let Some(singleton) = buffer.as_singleton() {
singleton.update(cx, |buffer, cx| {
if buffer.is_empty() {
buffer.edit(
[(0..0, initial_local_debug_tasks_content())],
None,
cx,
);
}
})
}
})
});
}
})
.ok();
this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
anyhow::Ok(())
})
.detach();
}
}
static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
#[derive(Clone)]
pub(crate) enum NewSessionMode {
pub(crate) enum NewProcessMode {
Task,
Configure,
Attach,
Launch,
Attach,
Debug,
}
impl std::fmt::Display for NewSessionMode {
impl std::fmt::Display for NewProcessMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mode = match self {
NewSessionMode::Task => "Run",
NewSessionMode::Launch => "Debug",
NewSessionMode::Attach => "Attach",
NewSessionMode::Configure => "Configure Debugger",
NewProcessMode::Task => "Run",
NewProcessMode::Debug => "Debug",
NewProcessMode::Attach => "Attach",
NewProcessMode::Launch => "Launch",
};
write!(f, "{}", mode)
}
}
impl Focusable for NewSessionMode {
impl Focusable for NewProcessMode {
fn focus_handle(&self, cx: &App) -> FocusHandle {
cx.focus_handle()
}
@ -598,7 +545,7 @@ fn render_editor(editor: &Entity<Editor>, window: &mut Window, cx: &App) -> impl
.bg(theme.colors().editor_background)
}
impl Render for NewSessionModal {
impl Render for NewProcessModal {
fn render(
&mut self,
window: &mut ui::Window,
@ -620,10 +567,10 @@ impl Render for NewSessionModal {
}))
.on_action(cx.listener(|this, _: &pane::ActivateNextItem, window, cx| {
this.mode = match this.mode {
NewSessionMode::Task => NewSessionMode::Launch,
NewSessionMode::Launch => NewSessionMode::Attach,
NewSessionMode::Attach => NewSessionMode::Configure,
NewSessionMode::Configure => NewSessionMode::Task,
NewProcessMode::Task => NewProcessMode::Debug,
NewProcessMode::Debug => NewProcessMode::Attach,
NewProcessMode::Attach => NewProcessMode::Launch,
NewProcessMode::Launch => NewProcessMode::Task,
};
this.mode_focus_handle(cx).focus(window);
@ -631,10 +578,10 @@ impl Render for NewSessionModal {
.on_action(
cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| {
this.mode = match this.mode {
NewSessionMode::Task => NewSessionMode::Configure,
NewSessionMode::Launch => NewSessionMode::Task,
NewSessionMode::Attach => NewSessionMode::Launch,
NewSessionMode::Configure => NewSessionMode::Attach,
NewProcessMode::Task => NewProcessMode::Launch,
NewProcessMode::Debug => NewProcessMode::Task,
NewProcessMode::Attach => NewProcessMode::Debug,
NewProcessMode::Launch => NewProcessMode::Attach,
};
this.mode_focus_handle(cx).focus(window);
@ -652,13 +599,13 @@ impl Render for NewSessionModal {
.child(
ToggleButton::new(
"debugger-session-ui-tasks-button",
NewSessionMode::Task.to_string(),
NewProcessMode::Task.to_string(),
)
.size(ButtonSize::Default)
.toggle_state(matches!(self.mode, NewSessionMode::Task))
.toggle_state(matches!(self.mode, NewProcessMode::Task))
.style(ui::ButtonStyle::Subtle)
.on_click(cx.listener(|this, _, window, cx| {
this.mode = NewSessionMode::Task;
this.mode = NewProcessMode::Task;
this.mode_focus_handle(cx).focus(window);
cx.notify();
}))
@ -667,13 +614,13 @@ impl Render for NewSessionModal {
.child(
ToggleButton::new(
"debugger-session-ui-launch-button",
NewSessionMode::Launch.to_string(),
NewProcessMode::Debug.to_string(),
)
.size(ButtonSize::Default)
.style(ui::ButtonStyle::Subtle)
.toggle_state(matches!(self.mode, NewSessionMode::Launch))
.toggle_state(matches!(self.mode, NewProcessMode::Debug))
.on_click(cx.listener(|this, _, window, cx| {
this.mode = NewSessionMode::Launch;
this.mode = NewProcessMode::Debug;
this.mode_focus_handle(cx).focus(window);
cx.notify();
}))
@ -682,13 +629,13 @@ impl Render for NewSessionModal {
.child(
ToggleButton::new(
"debugger-session-ui-attach-button",
NewSessionMode::Attach.to_string(),
NewProcessMode::Attach.to_string(),
)
.size(ButtonSize::Default)
.toggle_state(matches!(self.mode, NewSessionMode::Attach))
.toggle_state(matches!(self.mode, NewProcessMode::Attach))
.style(ui::ButtonStyle::Subtle)
.on_click(cx.listener(|this, _, window, cx| {
this.mode = NewSessionMode::Attach;
this.mode = NewProcessMode::Attach;
if let Some(debugger) = this.debugger.as_ref() {
Self::update_attach_picker(
@ -706,13 +653,13 @@ impl Render for NewSessionModal {
.child(
ToggleButton::new(
"debugger-session-ui-custom-button",
NewSessionMode::Configure.to_string(),
NewProcessMode::Launch.to_string(),
)
.size(ButtonSize::Default)
.toggle_state(matches!(self.mode, NewSessionMode::Configure))
.toggle_state(matches!(self.mode, NewProcessMode::Launch))
.style(ui::ButtonStyle::Subtle)
.on_click(cx.listener(|this, _, window, cx| {
this.mode = NewSessionMode::Configure;
this.mode = NewProcessMode::Launch;
this.mode_focus_handle(cx).focus(window);
cx.notify();
}))
@ -733,30 +680,42 @@ impl Render for NewSessionModal {
.border_t_1()
.w_full();
match self.mode {
NewSessionMode::Configure => el.child(
NewProcessMode::Launch => el.child(
container
.child(
h_flex()
.text_ui_sm(cx)
.text_color(Color::Muted.color(cx))
.child(
Button::new(
"new-session-modal-back",
"Save to .zed/debug.json...",
InteractiveText::new(
"open-debug-json",
StyledText::new(
"Open .zed/debug.json for advanced configuration",
)
.with_highlights([(
5..20,
HighlightStyle {
underline: Some(UnderlineStyle {
thickness: px(1.0),
color: None,
wavy: false,
}),
..Default::default()
},
)]),
)
.on_click(cx.listener(|this, _, window, cx| {
this.save_debug_scenario(window, cx);
}))
.disabled(
self.debugger.is_none()
|| self
.configure_mode
.read(cx)
.program
.read(cx)
.is_empty(cx)
|| self.save_scenario_state.is_some(),
.on_click(
vec![5..20],
{
let this = cx.entity();
move |_, window, cx| {
this.update(cx, |this, cx| {
this.open_debug_json(window, cx);
})
}
},
),
)
.child(self.render_save_state(cx)),
),
)
.child(
Button::new("debugger-spawn", "Start")
@ -766,7 +725,7 @@ impl Render for NewSessionModal {
.disabled(
self.debugger.is_none()
|| self
.configure_mode
.launch_mode
.read(cx)
.program
.read(cx)
@ -774,7 +733,7 @@ impl Render for NewSessionModal {
),
),
),
NewSessionMode::Attach => el.child(
NewProcessMode::Attach => el.child(
container
.child(div().child(self.adapter_drop_down_menu(window, cx)))
.child(
@ -797,21 +756,21 @@ impl Render for NewSessionModal {
),
),
),
NewSessionMode::Launch => el,
NewSessionMode::Task => el,
NewProcessMode::Debug => el,
NewProcessMode::Task => el,
}
})
}
}
impl EventEmitter<DismissEvent> for NewSessionModal {}
impl Focusable for NewSessionModal {
impl EventEmitter<DismissEvent> for NewProcessModal {}
impl Focusable for NewProcessModal {
fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
self.mode_focus_handle(cx)
}
}
impl ModalView for NewSessionModal {}
impl ModalView for NewProcessModal {}
impl RenderOnce for AttachMode {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
@ -823,44 +782,30 @@ impl RenderOnce for AttachMode {
}
#[derive(Clone)]
pub(super) struct ConfigureMode {
pub(super) struct LaunchMode {
program: Entity<Editor>,
cwd: Entity<Editor>,
stop_on_entry: ToggleState,
// save_to_debug_json: ToggleState,
}
impl ConfigureMode {
pub(super) fn new(
past_launch_config: Option<LaunchRequest>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
let (past_program, past_cwd) = past_launch_config
.map(|config| (Some(config.program), config.cwd))
.unwrap_or_else(|| (None, None));
impl LaunchMode {
pub(super) fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
let program = cx.new(|cx| Editor::single_line(window, cx));
program.update(cx, |this, cx| {
this.set_placeholder_text(
"ALPHA=\"Windows\" BETA=\"Wen\" your_program --arg1 --arg2=arg3",
cx,
);
if let Some(past_program) = past_program {
this.set_text(past_program, window, cx);
};
this.set_placeholder_text("ENV=Zed ~/bin/debugger --launch", cx);
});
let cwd = cx.new(|cx| Editor::single_line(window, cx));
cwd.update(cx, |this, cx| {
this.set_placeholder_text("Working Directory", cx);
if let Some(past_cwd) = past_cwd {
this.set_text(past_cwd.to_string_lossy(), window, cx);
};
this.set_placeholder_text("Ex: $ZED_WORKTREE_ROOT", cx);
});
cx.new(|_| Self {
program,
cwd,
stop_on_entry: ToggleState::Unselected,
// save_to_debug_json: ToggleState::Unselected,
})
}
@ -873,11 +818,17 @@ impl ConfigureMode {
}
pub(super) fn debug_request(&self, cx: &App) -> task::LaunchRequest {
let path = self.cwd.read(cx).text(cx);
let cwd_text = self.cwd.read(cx).text(cx);
let cwd = if cwd_text.is_empty() {
None
} else {
Some(PathBuf::from(cwd_text))
};
if cfg!(windows) {
return task::LaunchRequest {
program: self.program.read(cx).text(cx),
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
cwd,
args: Default::default(),
env: Default::default(),
};
@ -902,7 +853,7 @@ impl ConfigureMode {
task::LaunchRequest {
program,
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
cwd,
args,
env,
}
@ -929,7 +880,17 @@ impl ConfigureMode {
.gap(ui::DynamicSpacing::Base08.rems(cx))
.child(adapter_menu),
)
.child(
Label::new("Debugger Program")
.size(ui::LabelSize::Small)
.color(Color::Muted),
)
.child(render_editor(&self.program, window, cx))
.child(
Label::new("Working Directory")
.size(ui::LabelSize::Small)
.color(Color::Muted),
)
.child(render_editor(&self.cwd, window, cx))
.child(
CheckboxWithLabel::new(
@ -950,6 +911,27 @@ impl ConfigureMode {
)
.checkbox_position(ui::IconPosition::End),
)
// TODO: restore once we have proper, comment preserving
// file edits.
// .child(
// CheckboxWithLabel::new(
// "debugger-save-to-debug-json",
// Label::new("Save to debug.json")
// .size(ui::LabelSize::Small)
// .color(Color::Muted),
// self.save_to_debug_json,
// {
// let this = cx.weak_entity();
// move |state, _, cx| {
// this.update(cx, |this, _| {
// this.save_to_debug_json = *state;
// })
// .ok();
// }
// },
// )
// .checkbox_position(ui::IconPosition::End),
// )
}
}
@ -964,7 +946,7 @@ impl AttachMode {
debugger: Option<DebugAdapterName>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
cx: &mut Context<NewProcessModal>,
) -> Entity<Self> {
let definition = ZedDebugConfig {
adapter: debugger.unwrap_or(DebugAdapterName("".into())).0,
@ -994,7 +976,7 @@ pub(super) struct TaskMode {
pub(super) task_modal: Entity<TasksModal>,
}
pub(super) struct DebugScenarioDelegate {
pub(super) struct DebugDelegate {
task_store: Entity<TaskStore>,
candidates: Vec<(Option<TaskSourceKind>, DebugScenario)>,
selected_index: usize,
@ -1006,7 +988,7 @@ pub(super) struct DebugScenarioDelegate {
last_used_candidate_index: Option<usize>,
}
impl DebugScenarioDelegate {
impl DebugDelegate {
pub(super) fn new(debug_panel: WeakEntity<DebugPanel>, task_store: Entity<TaskStore>) -> Self {
Self {
task_store,
@ -1085,7 +1067,7 @@ impl DebugScenarioDelegate {
}
}
impl PickerDelegate for DebugScenarioDelegate {
impl PickerDelegate for DebugDelegate {
type ListItem = ui::ListItem;
fn match_count(&self) -> usize {
@ -1270,37 +1252,38 @@ pub(crate) fn resolve_path(path: &mut String) {
}
#[cfg(test)]
impl NewSessionModal {
pub(crate) fn set_configure(
&mut self,
program: impl AsRef<str>,
cwd: impl AsRef<str>,
stop_on_entry: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.mode = NewSessionMode::Configure;
self.debugger = Some(dap::adapters::DebugAdapterName("fake-adapter".into()));
impl NewProcessModal {
// #[cfg(test)]
// pub(crate) fn set_configure(
// &mut self,
// program: impl AsRef<str>,
// cwd: impl AsRef<str>,
// stop_on_entry: bool,
// window: &mut Window,
// cx: &mut Context<Self>,
// ) {
// self.mode = NewProcessMode::Launch;
// self.debugger = Some(dap::adapters::DebugAdapterName("fake-adapter".into()));
self.configure_mode.update(cx, |configure, cx| {
configure.program.update(cx, |editor, cx| {
editor.clear(window, cx);
editor.set_text(program.as_ref(), window, cx);
});
// self.launch_mode.update(cx, |configure, cx| {
// configure.program.update(cx, |editor, cx| {
// editor.clear(window, cx);
// editor.set_text(program.as_ref(), window, cx);
// });
configure.cwd.update(cx, |editor, cx| {
editor.clear(window, cx);
editor.set_text(cwd.as_ref(), window, cx);
});
// configure.cwd.update(cx, |editor, cx| {
// editor.clear(window, cx);
// editor.set_text(cwd.as_ref(), window, cx);
// });
configure.stop_on_entry = match stop_on_entry {
true => ToggleState::Selected,
_ => ToggleState::Unselected,
}
})
}
// configure.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);
}
// pub(crate) fn save_scenario(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
// self.save_debug_scenario(window, cx);
// }
}

View file

@ -8,7 +8,7 @@ pub mod variable_list;
use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
use crate::{
new_session_modal::resolve_path,
new_process_modal::resolve_path,
persistence::{self, DebuggerPaneItem, SerializedLayout},
};
@ -566,7 +566,7 @@ impl RunningState {
}
}
pub(crate) fn relativlize_paths(
pub(crate) fn relativize_paths(
key: Option<&str>,
config: &mut serde_json::Value,
context: &TaskContext,
@ -574,12 +574,12 @@ impl RunningState {
match config {
serde_json::Value::Object(obj) => {
obj.iter_mut()
.for_each(|(key, value)| Self::relativlize_paths(Some(key), value, context));
.for_each(|(key, value)| Self::relativize_paths(Some(key), value, context));
}
serde_json::Value::Array(array) => {
array
.iter_mut()
.for_each(|value| Self::relativlize_paths(None, value, context));
.for_each(|value| Self::relativize_paths(None, value, context));
}
serde_json::Value::String(s) if key == Some("program") || key == Some("cwd") => {
// Some built-in zed tasks wrap their arguments in quotes as they might contain spaces.
@ -806,7 +806,7 @@ impl RunningState {
mut config,
tcp_connection,
} = scenario;
Self::relativlize_paths(None, &mut config, &task_context);
Self::relativize_paths(None, &mut config, &task_context);
Self::substitute_variables_in_config(&mut config, &task_context);
let request_type = dap_registry

View file

@ -25,7 +25,7 @@ mod inline_values;
#[cfg(test)]
mod module_list;
#[cfg(test)]
mod new_session_modal;
mod new_process_modal;
#[cfg(test)]
mod persistence;
#[cfg(test)]

View file

@ -1,13 +1,13 @@
use dap::DapRegistry;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Fs, Project};
use project::{FakeFs, Project};
use serde_json::json;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig};
use util::path;
use crate::new_session_modal::NewSessionMode;
// use crate::new_process_modal::NewProcessMode;
use crate::tests::{init_test, init_test_workspace};
#[gpui::test]
@ -152,111 +152,111 @@ 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);
// #[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 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);
// 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,
NewSessionMode::Launch,
None,
cx,
);
})
.unwrap();
// workspace
// .update(cx, |workspace, window, cx| {
// crate::new_process_modal::NewProcessModal::show(
// workspace,
// window,
// NewProcessMode::Debug,
// None,
// cx,
// );
// })
// .unwrap();
cx.run_until_parked();
// 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");
// let modal = workspace
// .update(cx, |workspace, _, cx| {
// workspace.active_modal::<crate::new_process_modal::NewProcessModal>(cx)
// })
// .unwrap()
// .expect("Modal should be active");
modal.update_in(cx, |modal, window, cx| {
modal.set_configure("/project/main", "/project", false, window, cx);
modal.save_scenario(window, cx);
});
// modal.update_in(cx, |modal, window, cx| {
// modal.set_configure("/project/main", "/project", false, window, cx);
// modal.save_scenario(window, cx);
// });
cx.executor().run_until_parked();
// 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 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 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);
// 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_configure("/project/other", "/project", true, window, cx);
modal.save_scenario(window, cx);
});
// modal.update_in(cx, |modal, window, cx| {
// modal.set_configure("/project/other", "/project", true, window, cx);
// modal.save_scenario(window, cx);
// });
cx.executor().run_until_parked();
// 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 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 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);
}
// 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) {

View file

@ -408,6 +408,7 @@ pub fn task_file_name() -> &'static str {
}
/// Returns the relative path to a `debug.json` file within a project.
/// .zed/debug.json
pub fn local_debug_file_relative_path() -> &'static Path {
Path::new(".zed/debug.json")
}

View file

@ -115,3 +115,7 @@ pub fn initial_tasks_content() -> Cow<'static, str> {
pub fn initial_debug_tasks_content() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/initial_debug_tasks.json")
}
pub fn initial_local_debug_tasks_content() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/initial_local_debug_tasks.json")
}

View file

@ -50,8 +50,8 @@ use rope::Rope;
use search::project_search::ProjectSearchBar;
use settings::{
DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeymapFile, KeymapFileLoadResult, Settings,
SettingsStore, VIM_KEYMAP_PATH, initial_debug_tasks_content, initial_project_settings_content,
initial_tasks_content, update_settings_file,
SettingsStore, VIM_KEYMAP_PATH, initial_local_debug_tasks_content,
initial_project_settings_content, initial_tasks_content, update_settings_file,
};
use std::path::PathBuf;
use std::sync::atomic::{self, AtomicBool};
@ -740,6 +740,14 @@ fn register_actions(
cx,
);
})
.register_action(move |_: &mut Workspace, _: &OpenDebugTasks, window, cx| {
open_settings_file(
paths::debug_scenarios_file(),
|| settings::initial_debug_tasks_content().as_ref().into(),
window,
cx,
);
})
.register_action(open_project_settings_file)
.register_action(open_project_tasks_file)
.register_action(open_project_debug_tasks_file)
@ -1508,7 +1516,7 @@ fn open_project_debug_tasks_file(
open_local_file(
workspace,
local_debug_file_relative_path(),
initial_debug_tasks_content(),
initial_local_debug_tasks_content(),
window,
cx,
)