2022-03-22 04:53:44 +01:00
|
|
|
use super::{Context, Editor};
|
2021-09-04 21:57:58 +02:00
|
|
|
use crate::{
|
2021-12-03 05:27:00 +01:00
|
|
|
compositor::{self, Compositor},
|
|
|
|
job::{Callback, Jobs},
|
2022-02-15 02:33:55 +01:00
|
|
|
ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, Text},
|
2021-09-04 21:57:58 +02:00
|
|
|
};
|
2022-07-02 13:21:27 +02:00
|
|
|
use dap::{StackFrame, Thread, ThreadStates};
|
|
|
|
use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
|
2022-03-22 04:53:44 +01:00
|
|
|
use helix_dap::{self as dap, Client};
|
2021-08-29 15:43:00 +02:00
|
|
|
use helix_lsp::block_on;
|
2021-11-30 09:52:39 +01:00
|
|
|
use helix_view::editor::Breakpoint;
|
2021-08-29 15:43:00 +02:00
|
|
|
|
|
|
|
use serde_json::{to_value, Value};
|
|
|
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
2022-12-25 06:54:09 +01:00
|
|
|
use tui::{text::Spans, widgets::Row};
|
2021-08-29 15:43:00 +02:00
|
|
|
|
|
|
|
use std::collections::HashMap;
|
2021-12-03 05:27:00 +01:00
|
|
|
use std::future::Future;
|
2021-11-30 09:52:39 +01:00
|
|
|
use std::path::PathBuf;
|
2021-08-29 15:43:00 +02:00
|
|
|
|
2021-12-05 06:52:56 +01:00
|
|
|
use anyhow::{anyhow, bail};
|
2021-12-03 05:27:00 +01:00
|
|
|
|
2022-03-22 04:53:44 +01:00
|
|
|
use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select_thread_id};
|
2021-08-30 03:58:15 +02:00
|
|
|
|
2022-07-02 13:21:27 +02:00
|
|
|
impl ui::menu::Item for StackFrame {
|
|
|
|
type Data = ();
|
|
|
|
|
2022-12-25 06:54:09 +01:00
|
|
|
fn format(&self, _data: &Self::Data) -> Row {
|
2022-07-02 13:21:27 +02:00
|
|
|
self.name.as_str().into() // TODO: include thread_states in the label
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ui::menu::Item for DebugTemplate {
|
|
|
|
type Data = ();
|
|
|
|
|
2022-12-25 06:54:09 +01:00
|
|
|
fn format(&self, _data: &Self::Data) -> Row {
|
2022-07-02 13:21:27 +02:00
|
|
|
self.name.as_str().into()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ui::menu::Item for Thread {
|
|
|
|
type Data = ThreadStates;
|
|
|
|
|
2022-12-25 06:54:09 +01:00
|
|
|
fn format(&self, thread_states: &Self::Data) -> Row {
|
2022-07-02 13:21:27 +02:00
|
|
|
format!(
|
|
|
|
"{} ({})",
|
|
|
|
self.name,
|
|
|
|
thread_states
|
|
|
|
.get(&self.id)
|
|
|
|
.map(|state| state.as_str())
|
|
|
|
.unwrap_or("unknown")
|
|
|
|
)
|
|
|
|
.into()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-05 06:52:56 +01:00
|
|
|
fn thread_picker(
|
|
|
|
cx: &mut Context,
|
|
|
|
callback_fn: impl Fn(&mut Editor, &dap::Thread) + Send + 'static,
|
|
|
|
) {
|
2021-12-07 16:59:11 +01:00
|
|
|
let debugger = debugger!(cx.editor);
|
2021-09-03 04:40:49 +02:00
|
|
|
|
2021-12-05 06:52:56 +01:00
|
|
|
let future = debugger.threads();
|
|
|
|
dap_callback(
|
|
|
|
cx.jobs,
|
|
|
|
future,
|
2022-03-23 08:55:58 +01:00
|
|
|
move |editor, compositor, response: dap::requests::ThreadsResponse| {
|
2021-12-05 06:52:56 +01:00
|
|
|
let threads = response.threads;
|
|
|
|
if threads.len() == 1 {
|
|
|
|
callback_fn(editor, &threads[0]);
|
|
|
|
return;
|
|
|
|
}
|
2021-12-08 02:21:58 +01:00
|
|
|
let debugger = debugger!(editor);
|
2021-12-05 06:52:56 +01:00
|
|
|
|
|
|
|
let thread_states = debugger.thread_states.clone();
|
|
|
|
let picker = FilePicker::new(
|
|
|
|
threads,
|
2022-07-02 13:21:27 +02:00
|
|
|
thread_states,
|
2021-12-05 06:52:56 +01:00
|
|
|
move |cx, thread, _action| callback_fn(cx.editor, thread),
|
|
|
|
move |editor, thread| {
|
|
|
|
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
|
|
|
|
let frame = frames.get(0)?;
|
|
|
|
let path = frame.source.as_ref()?.path.clone()?;
|
|
|
|
let pos = Some((
|
|
|
|
frame.line.saturating_sub(1),
|
|
|
|
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
|
|
|
|
));
|
2022-11-21 02:58:35 +01:00
|
|
|
Some((path.into(), pos))
|
2021-12-05 06:52:56 +01:00
|
|
|
},
|
|
|
|
);
|
|
|
|
compositor.push(Box::new(picker));
|
2021-09-05 12:39:27 +02:00
|
|
|
},
|
2021-09-03 04:40:49 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-12-02 02:21:28 +01:00
|
|
|
fn get_breakpoint_at_current_line(editor: &mut Editor) -> Option<(usize, Breakpoint)> {
|
|
|
|
let (view, doc) = current!(editor);
|
|
|
|
let text = doc.text().slice(..);
|
|
|
|
|
2021-12-02 02:24:17 +01:00
|
|
|
let line = doc.selection(view.id).primary().cursor_line(text);
|
2021-12-02 02:31:19 +01:00
|
|
|
let path = doc.path()?;
|
2021-12-02 02:21:28 +01:00
|
|
|
editor.breakpoints.get(path).and_then(|breakpoints| {
|
|
|
|
let i = breakpoints.iter().position(|b| b.line == line);
|
|
|
|
i.map(|i| (i, breakpoints[i].clone()))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-09-03 04:40:49 +02:00
|
|
|
// -- DAP
|
|
|
|
|
2021-12-03 05:27:00 +01:00
|
|
|
fn dap_callback<T, F>(
|
|
|
|
jobs: &mut Jobs,
|
|
|
|
call: impl Future<Output = helix_dap::Result<serde_json::Value>> + 'static + Send,
|
|
|
|
callback: F,
|
|
|
|
) where
|
|
|
|
T: for<'de> serde::Deserialize<'de> + Send + 'static,
|
|
|
|
F: FnOnce(&mut Editor, &mut Compositor, T) + Send + 'static,
|
|
|
|
{
|
|
|
|
let callback = Box::pin(async move {
|
|
|
|
let json = call.await?;
|
|
|
|
let response = serde_json::from_value(json)?;
|
2022-07-12 05:38:26 +02:00
|
|
|
let call: Callback = Callback::EditorCompositor(Box::new(
|
|
|
|
move |editor: &mut Editor, compositor: &mut Compositor| {
|
|
|
|
callback(editor, compositor, response)
|
|
|
|
},
|
|
|
|
));
|
2021-12-03 05:27:00 +01:00
|
|
|
Ok(call)
|
|
|
|
});
|
2022-07-12 05:38:26 +02:00
|
|
|
|
2021-12-03 05:27:00 +01:00
|
|
|
jobs.callback(callback);
|
|
|
|
}
|
|
|
|
|
2021-08-29 15:43:00 +02:00
|
|
|
pub fn dap_start_impl(
|
2021-12-03 05:27:00 +01:00
|
|
|
cx: &mut compositor::Context,
|
2021-08-29 15:43:00 +02:00
|
|
|
name: Option<&str>,
|
|
|
|
socket: Option<std::net::SocketAddr>,
|
2022-02-13 10:31:51 +01:00
|
|
|
params: Option<Vec<std::borrow::Cow<str>>>,
|
2021-12-03 05:27:00 +01:00
|
|
|
) -> Result<(), anyhow::Error> {
|
|
|
|
let doc = doc!(cx.editor);
|
2021-08-29 15:43:00 +02:00
|
|
|
|
2021-12-06 01:32:11 +01:00
|
|
|
let config = doc
|
2021-11-07 13:26:03 +01:00
|
|
|
.language_config()
|
2021-11-07 10:37:42 +01:00
|
|
|
.and_then(|config| config.debugger.as_ref())
|
2022-03-08 02:33:40 +01:00
|
|
|
.ok_or_else(|| anyhow!("No debug adapter available for language"))?;
|
2021-08-29 15:43:00 +02:00
|
|
|
|
|
|
|
let result = match socket {
|
|
|
|
Some(socket) => block_on(Client::tcp(socket, 0)),
|
|
|
|
None => block_on(Client::process(
|
2021-11-07 10:37:42 +01:00
|
|
|
&config.transport,
|
|
|
|
&config.command,
|
|
|
|
config.args.iter().map(|arg| arg.as_str()).collect(),
|
|
|
|
config.port_arg.as_deref(),
|
2021-08-29 15:43:00 +02:00
|
|
|
0,
|
|
|
|
)),
|
|
|
|
};
|
|
|
|
|
|
|
|
let (mut debugger, events) = match result {
|
|
|
|
Ok(r) => r,
|
2021-12-03 05:27:00 +01:00
|
|
|
Err(e) => bail!("Failed to start debug session: {}", e),
|
2021-08-29 15:43:00 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
let request = debugger.initialize(config.name.clone());
|
|
|
|
if let Err(e) = block_on(request) {
|
2021-12-03 05:27:00 +01:00
|
|
|
bail!("Failed to initialize debug adapter: {}", e);
|
2021-08-29 15:43:00 +02:00
|
|
|
}
|
|
|
|
|
2021-11-07 10:37:42 +01:00
|
|
|
debugger.quirks = config.quirks.clone();
|
2021-09-26 20:36:06 +02:00
|
|
|
|
2021-11-07 10:57:44 +01:00
|
|
|
// TODO: avoid refetching all of this... pass a config in
|
2021-11-07 13:47:44 +01:00
|
|
|
let template = match name {
|
2021-08-29 15:43:00 +02:00
|
|
|
Some(name) => config.templates.iter().find(|t| t.name == name),
|
|
|
|
None => config.templates.get(0),
|
2021-12-06 01:32:11 +01:00
|
|
|
}
|
2022-03-08 02:33:40 +01:00
|
|
|
.ok_or_else(|| anyhow!("No debug config with given name"))?;
|
2021-08-29 15:43:00 +02:00
|
|
|
|
2021-11-07 13:47:44 +01:00
|
|
|
let mut args: HashMap<&str, Value> = HashMap::new();
|
2021-08-29 15:43:00 +02:00
|
|
|
|
|
|
|
if let Some(params) = params {
|
2021-11-07 13:47:44 +01:00
|
|
|
for (k, t) in &template.args {
|
|
|
|
let mut value = t.clone();
|
2021-08-29 15:43:00 +02:00
|
|
|
for (i, x) in params.iter().enumerate() {
|
2021-09-26 20:36:06 +02:00
|
|
|
let mut param = x.to_string();
|
2021-11-07 13:47:44 +01:00
|
|
|
if let Some(DebugConfigCompletion::Advanced(cfg)) = template.completion.get(i) {
|
2021-11-07 13:55:57 +01:00
|
|
|
if matches!(cfg.completion.as_deref(), Some("filename" | "directory")) {
|
2022-02-13 10:31:51 +01:00
|
|
|
param = std::fs::canonicalize(x.as_ref())
|
2021-09-26 20:54:36 +02:00
|
|
|
.ok()
|
2021-09-26 20:36:06 +02:00
|
|
|
.and_then(|pb| pb.into_os_string().into_string().ok())
|
2021-09-26 20:54:36 +02:00
|
|
|
.unwrap_or_else(|| x.to_string());
|
2021-09-26 20:36:06 +02:00
|
|
|
}
|
|
|
|
}
|
2021-08-29 15:43:00 +02:00
|
|
|
// For param #0 replace {0} in args
|
2021-11-07 13:47:44 +01:00
|
|
|
let pattern = format!("{{{}}}", i);
|
2021-10-24 16:24:18 +02:00
|
|
|
value = match value {
|
2021-12-03 03:59:44 +01:00
|
|
|
// TODO: just use toml::Value -> json::Value
|
2021-10-24 16:24:18 +02:00
|
|
|
DebugArgumentValue::String(v) => {
|
2021-11-07 13:47:44 +01:00
|
|
|
DebugArgumentValue::String(v.replace(&pattern, ¶m))
|
2021-10-24 16:24:18 +02:00
|
|
|
}
|
|
|
|
DebugArgumentValue::Array(arr) => DebugArgumentValue::Array(
|
2021-11-07 13:47:44 +01:00
|
|
|
arr.iter().map(|v| v.replace(&pattern, ¶m)).collect(),
|
2021-10-24 16:24:18 +02:00
|
|
|
),
|
2021-12-03 03:59:44 +01:00
|
|
|
DebugArgumentValue::Boolean(_) => value,
|
2021-10-24 16:24:18 +02:00
|
|
|
};
|
2021-08-29 15:43:00 +02:00
|
|
|
}
|
|
|
|
|
2021-12-03 03:59:44 +01:00
|
|
|
match value {
|
|
|
|
DebugArgumentValue::String(string) => {
|
|
|
|
if let Ok(integer) = string.parse::<usize>() {
|
|
|
|
args.insert(k, to_value(integer).unwrap());
|
|
|
|
} else {
|
|
|
|
args.insert(k, to_value(string).unwrap());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
DebugArgumentValue::Array(arr) => {
|
|
|
|
args.insert(k, to_value(arr).unwrap());
|
|
|
|
}
|
|
|
|
DebugArgumentValue::Boolean(bool) => {
|
|
|
|
args.insert(k, to_value(bool).unwrap());
|
2021-10-24 16:24:18 +02:00
|
|
|
}
|
2021-08-29 15:43:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-31 13:59:15 +02:00
|
|
|
args.insert("cwd", to_value(std::env::current_dir().unwrap())?);
|
|
|
|
|
2021-08-29 15:43:00 +02:00
|
|
|
let args = to_value(args).unwrap();
|
|
|
|
|
2021-12-03 05:27:00 +01:00
|
|
|
let callback = |_editor: &mut Editor, _compositor: &mut Compositor, _response: Value| {
|
|
|
|
// if let Err(e) = result {
|
|
|
|
// editor.set_error(format!("Failed {} target: {}", template.request, e));
|
|
|
|
// }
|
|
|
|
};
|
2021-12-03 03:59:44 +01:00
|
|
|
|
2021-12-03 05:27:00 +01:00
|
|
|
match &template.request[..] {
|
|
|
|
"launch" => {
|
|
|
|
let call = debugger.launch(args);
|
|
|
|
dap_callback(cx.jobs, call, callback);
|
|
|
|
}
|
|
|
|
"attach" => {
|
|
|
|
let call = debugger.attach(args);
|
|
|
|
dap_callback(cx.jobs, call, callback);
|
2021-08-29 15:43:00 +02:00
|
|
|
}
|
2021-12-03 05:27:00 +01:00
|
|
|
request => bail!("Unsupported request '{}'", request),
|
2021-08-29 15:43:00 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
// TODO: either await "initialized" or buffer commands until event is received
|
2021-12-03 05:27:00 +01:00
|
|
|
cx.editor.debugger = Some(debugger);
|
2021-08-29 15:43:00 +02:00
|
|
|
let stream = UnboundedReceiverStream::new(events);
|
2021-12-03 05:27:00 +01:00
|
|
|
cx.editor.debugger_events.push(stream);
|
|
|
|
Ok(())
|
2021-08-29 15:43:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn dap_launch(cx: &mut Context) {
|
|
|
|
if cx.editor.debugger.is_some() {
|
2022-02-15 08:45:28 +01:00
|
|
|
cx.editor.set_error("Debugger is already running");
|
2021-08-29 15:43:00 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-11-07 13:26:03 +01:00
|
|
|
let doc = doc!(cx.editor);
|
2021-08-29 15:43:00 +02:00
|
|
|
|
2021-11-07 13:26:03 +01:00
|
|
|
let config = match doc
|
|
|
|
.language_config()
|
2021-11-07 10:37:42 +01:00
|
|
|
.and_then(|config| config.debugger.as_ref())
|
|
|
|
{
|
2021-08-29 15:43:00 +02:00
|
|
|
Some(c) => c,
|
|
|
|
None => {
|
2021-11-07 13:37:00 +01:00
|
|
|
cx.editor
|
2022-02-15 08:45:28 +01:00
|
|
|
.set_error("No debug adapter available for language");
|
2021-08-29 15:43:00 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-11-07 13:26:03 +01:00
|
|
|
let templates = config.templates.clone();
|
|
|
|
|
2022-02-15 02:33:55 +01:00
|
|
|
cx.push_layer(Box::new(overlayed(Picker::new(
|
2021-11-07 13:26:03 +01:00
|
|
|
templates,
|
2022-07-02 13:21:27 +02:00
|
|
|
(),
|
2021-11-07 10:03:04 +01:00
|
|
|
|cx, template, _action| {
|
2021-10-17 08:14:16 +02:00
|
|
|
let completions = template.completion.clone();
|
2021-11-07 10:12:30 +01:00
|
|
|
let name = template.name.clone();
|
|
|
|
let callback = Box::pin(async move {
|
2022-09-03 05:38:38 +02:00
|
|
|
let call: Callback =
|
|
|
|
Callback::EditorCompositor(Box::new(move |_editor, compositor| {
|
|
|
|
let prompt = debug_parameter_prompt(completions, name, Vec::new());
|
|
|
|
compositor.push(Box::new(prompt));
|
|
|
|
}));
|
2021-11-07 10:12:30 +01:00
|
|
|
Ok(call)
|
|
|
|
});
|
|
|
|
cx.jobs.callback(callback);
|
2021-10-17 08:14:16 +02:00
|
|
|
},
|
2022-02-15 03:37:33 +01:00
|
|
|
))));
|
2021-08-29 15:43:00 +02:00
|
|
|
}
|
|
|
|
|
2023-03-06 10:19:53 +01:00
|
|
|
pub fn dap_restart(cx: &mut Context) {
|
|
|
|
let debugger = match &cx.editor.debugger {
|
|
|
|
Some(debugger) => debugger,
|
|
|
|
None => {
|
|
|
|
cx.editor.set_error("Debugger is not running");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
if !debugger
|
|
|
|
.capabilities()
|
|
|
|
.supports_restart_request
|
|
|
|
.unwrap_or(false)
|
|
|
|
{
|
|
|
|
cx.editor
|
|
|
|
.set_error("Debugger does not support session restarts");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if debugger.starting_request_args().is_none() {
|
|
|
|
cx.editor
|
|
|
|
.set_error("No arguments found with which to restart the sessions");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
dap_callback(
|
|
|
|
cx.jobs,
|
|
|
|
debugger.restart(),
|
|
|
|
|editor, _compositor, _resp: ()| editor.set_status("Debugging session restarted"),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-11-07 10:12:30 +01:00
|
|
|
fn debug_parameter_prompt(
|
|
|
|
completions: Vec<DebugConfigCompletion>,
|
|
|
|
config_name: String,
|
|
|
|
mut params: Vec<String>,
|
|
|
|
) -> Prompt {
|
2021-11-07 13:55:57 +01:00
|
|
|
let completion = completions.get(params.len()).unwrap();
|
2021-11-07 10:12:30 +01:00
|
|
|
let field_type = if let DebugConfigCompletion::Advanced(cfg) = completion {
|
2021-11-07 13:55:57 +01:00
|
|
|
cfg.completion.as_deref().unwrap_or("")
|
2021-11-07 10:12:30 +01:00
|
|
|
} else {
|
2021-11-07 13:55:57 +01:00
|
|
|
""
|
2021-11-07 10:12:30 +01:00
|
|
|
};
|
|
|
|
let name = match completion {
|
2021-11-07 13:55:57 +01:00
|
|
|
DebugConfigCompletion::Advanced(cfg) => cfg.name.as_deref().unwrap_or(field_type),
|
|
|
|
DebugConfigCompletion::Named(name) => name.as_str(),
|
2021-11-07 10:12:30 +01:00
|
|
|
};
|
|
|
|
let default_val = match completion {
|
2021-11-07 13:55:57 +01:00
|
|
|
DebugConfigCompletion::Advanced(cfg) => cfg.default.as_deref().unwrap_or(""),
|
|
|
|
_ => "",
|
|
|
|
}
|
|
|
|
.to_owned();
|
2021-11-07 10:12:30 +01:00
|
|
|
|
2021-11-07 13:55:57 +01:00
|
|
|
let completer = match field_type {
|
2021-11-07 10:12:30 +01:00
|
|
|
"filename" => ui::completers::filename,
|
|
|
|
"directory" => ui::completers::directory,
|
2021-11-20 22:03:39 +01:00
|
|
|
_ => ui::completers::none,
|
2021-11-07 10:12:30 +01:00
|
|
|
};
|
2021-11-20 22:03:39 +01:00
|
|
|
|
2021-11-07 10:12:30 +01:00
|
|
|
Prompt::new(
|
|
|
|
format!("{}: ", name).into(),
|
|
|
|
None,
|
|
|
|
completer,
|
2021-11-22 03:22:08 +01:00
|
|
|
move |cx, input: &str, event: PromptEvent| {
|
2021-11-07 10:12:30 +01:00
|
|
|
if event != PromptEvent::Validate {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut value = input.to_owned();
|
|
|
|
if value.is_empty() {
|
|
|
|
value = default_val.clone();
|
|
|
|
}
|
|
|
|
params.push(value);
|
|
|
|
|
|
|
|
if params.len() < completions.len() {
|
|
|
|
let completions = completions.clone();
|
|
|
|
let config_name = config_name.clone();
|
|
|
|
let params = params.clone();
|
|
|
|
let callback = Box::pin(async move {
|
2022-09-03 05:38:38 +02:00
|
|
|
let call: Callback =
|
|
|
|
Callback::EditorCompositor(Box::new(move |_editor, compositor| {
|
|
|
|
let prompt = debug_parameter_prompt(completions, config_name, params);
|
|
|
|
compositor.push(Box::new(prompt));
|
|
|
|
}));
|
2021-11-07 10:12:30 +01:00
|
|
|
Ok(call)
|
|
|
|
});
|
|
|
|
cx.jobs.callback(callback);
|
2022-02-17 04:22:48 +01:00
|
|
|
} else if let Err(err) = dap_start_impl(
|
2021-12-05 06:55:35 +01:00
|
|
|
cx,
|
|
|
|
Some(&config_name),
|
|
|
|
None,
|
2022-02-13 10:31:51 +01:00
|
|
|
Some(params.iter().map(|x| x.into()).collect()),
|
2021-12-05 06:55:35 +01:00
|
|
|
) {
|
2022-02-17 04:22:48 +01:00
|
|
|
cx.editor.set_error(err.to_string());
|
2021-11-07 10:12:30 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-08-29 15:43:00 +02:00
|
|
|
pub fn dap_toggle_breakpoint(cx: &mut Context) {
|
|
|
|
let (view, doc) = current!(cx.editor);
|
|
|
|
let path = match doc.path() {
|
2021-11-22 08:30:35 +01:00
|
|
|
Some(path) => path.clone(),
|
2021-08-29 15:43:00 +02:00
|
|
|
None => {
|
|
|
|
cx.editor
|
2022-02-15 08:45:28 +01:00
|
|
|
.set_error("Can't set breakpoint: document has no path");
|
2021-08-29 15:43:00 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
2021-11-22 08:30:35 +01:00
|
|
|
let text = doc.text().slice(..);
|
2021-12-02 02:24:17 +01:00
|
|
|
let line = doc.selection(view.id).primary().cursor_line(text);
|
2021-11-22 08:30:35 +01:00
|
|
|
dap_toggle_breakpoint_impl(cx, path, line);
|
|
|
|
}
|
|
|
|
|
2021-11-30 09:52:39 +01:00
|
|
|
pub fn dap_toggle_breakpoint_impl(cx: &mut Context, path: PathBuf, line: usize) {
|
2021-08-29 15:43:00 +02:00
|
|
|
// TODO: need to map breakpoints over edits and update them?
|
|
|
|
// we shouldn't really allow editing while debug is running though
|
|
|
|
|
2021-09-03 06:02:09 +02:00
|
|
|
let breakpoints = cx.editor.breakpoints.entry(path.clone()).or_default();
|
2021-11-30 09:52:39 +01:00
|
|
|
// TODO: always keep breakpoints sorted and use binary search to determine insertion point
|
|
|
|
if let Some(pos) = breakpoints
|
|
|
|
.iter()
|
|
|
|
.position(|breakpoint| breakpoint.line == line)
|
|
|
|
{
|
2021-09-03 04:10:30 +02:00
|
|
|
breakpoints.remove(pos);
|
|
|
|
} else {
|
2021-11-30 09:52:39 +01:00
|
|
|
breakpoints.push(Breakpoint {
|
|
|
|
line,
|
|
|
|
..Default::default()
|
|
|
|
});
|
2021-09-03 04:10:30 +02:00
|
|
|
}
|
2021-08-29 15:43:00 +02:00
|
|
|
|
2021-12-07 16:59:11 +01:00
|
|
|
let debugger = debugger!(cx.editor);
|
2021-11-30 09:52:39 +01:00
|
|
|
|
|
|
|
if let Err(e) = breakpoints_changed(debugger, path, breakpoints) {
|
|
|
|
cx.editor
|
|
|
|
.set_error(format!("Failed to set breakpoints: {}", e));
|
|
|
|
}
|
2021-08-29 15:43:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn dap_continue(cx: &mut Context) {
|
2021-12-07 16:59:11 +01:00
|
|
|
let debugger = debugger!(cx.editor);
|
2021-08-29 15:43:00 +02:00
|
|
|
|
2021-09-03 04:10:30 +02:00
|
|
|
if let Some(thread_id) = debugger.thread_id {
|
|
|
|
let request = debugger.continue_thread(thread_id);
|
2022-02-15 08:30:23 +01:00
|
|
|
|
|
|
|
dap_callback(
|
|
|
|
cx.jobs,
|
|
|
|
request,
|
|
|
|
|editor, _compositor, _response: dap::requests::ContinueResponse| {
|
|
|
|
debugger!(editor).resume_application();
|
|
|
|
},
|
|
|
|
);
|
2021-09-03 04:10:30 +02:00
|
|
|
} else {
|
|
|
|
cx.editor
|
2022-02-15 08:45:28 +01:00
|
|
|
.set_error("Currently active thread is not stopped. Switch the thread.");
|
2021-08-29 15:43:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn dap_pause(cx: &mut Context) {
|
2021-09-03 04:40:49 +02:00
|
|
|
thread_picker(cx, |editor, thread| {
|
2021-12-08 02:21:58 +01:00
|
|
|
let debugger = debugger!(editor);
|
2021-09-03 04:40:49 +02:00
|
|
|
let request = debugger.pause(thread.id);
|
|
|
|
// NOTE: we don't need to set active thread id here because DAP will emit a "stopped" event
|
|
|
|
if let Err(e) = block_on(request) {
|
2021-11-07 13:20:58 +01:00
|
|
|
editor.set_error(format!("Failed to pause: {}", e));
|
2021-09-03 04:40:49 +02:00
|
|
|
}
|
|
|
|
})
|
2021-08-29 15:43:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn dap_step_in(cx: &mut Context) {
|
2021-12-07 16:59:11 +01:00
|
|
|
let debugger = debugger!(cx.editor);
|
2021-08-29 15:43:00 +02:00
|
|
|
|
2021-09-03 04:10:30 +02:00
|
|
|
if let Some(thread_id) = debugger.thread_id {
|
|
|
|
let request = debugger.step_in(thread_id);
|
2022-02-15 08:30:23 +01:00
|
|
|
|
|
|
|
dap_callback(cx.jobs, request, |editor, _compositor, _response: ()| {
|
|
|
|
debugger!(editor).resume_application();
|
|
|
|
});
|
2021-09-03 04:10:30 +02:00
|
|
|
} else {
|
|
|
|
cx.editor
|
2022-02-15 08:45:28 +01:00
|
|
|
.set_error("Currently active thread is not stopped. Switch the thread.");
|
2021-08-29 15:43:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn dap_step_out(cx: &mut Context) {
|
2021-12-07 16:59:11 +01:00
|
|
|
let debugger = debugger!(cx.editor);
|
2021-08-29 15:43:00 +02:00
|
|
|
|
2021-09-03 04:10:30 +02:00
|
|
|
if let Some(thread_id) = debugger.thread_id {
|
|
|
|
let request = debugger.step_out(thread_id);
|
2022-02-15 08:30:23 +01:00
|
|
|
dap_callback(cx.jobs, request, |editor, _compositor, _response: ()| {
|
|
|
|
debugger!(editor).resume_application();
|
|
|
|
});
|
2021-09-03 04:10:30 +02:00
|
|
|
} else {
|
|
|
|
cx.editor
|
2022-02-15 08:45:28 +01:00
|
|
|
.set_error("Currently active thread is not stopped. Switch the thread.");
|
2021-08-29 15:43:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn dap_next(cx: &mut Context) {
|
2021-12-07 16:59:11 +01:00
|
|
|
let debugger = debugger!(cx.editor);
|
2021-08-29 15:43:00 +02:00
|
|
|
|
2021-09-03 04:10:30 +02:00
|
|
|
if let Some(thread_id) = debugger.thread_id {
|
|
|
|
let request = debugger.next(thread_id);
|
2022-02-15 08:30:23 +01:00
|
|
|
dap_callback(cx.jobs, request, |editor, _compositor, _response: ()| {
|
|
|
|
debugger!(editor).resume_application();
|
|
|
|
});
|
2021-09-03 04:10:30 +02:00
|
|
|
} else {
|
|
|
|
cx.editor
|
2022-02-15 08:45:28 +01:00
|
|
|
.set_error("Currently active thread is not stopped. Switch the thread.");
|
2021-08-29 15:43:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn dap_variables(cx: &mut Context) {
|
2021-12-07 16:59:11 +01:00
|
|
|
let debugger = debugger!(cx.editor);
|
2021-09-03 04:10:30 +02:00
|
|
|
|
2021-09-03 04:30:25 +02:00
|
|
|
if debugger.thread_id.is_none() {
|
2021-09-03 04:10:30 +02:00
|
|
|
cx.editor
|
2023-02-20 05:00:44 +01:00
|
|
|
.set_status("Cannot access variables while target is running.");
|
2021-09-03 04:10:30 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
let (frame, thread_id) = match (debugger.active_frame, debugger.thread_id) {
|
|
|
|
(Some(frame), Some(thread_id)) => (frame, thread_id),
|
|
|
|
_ => {
|
2021-08-29 15:43:00 +02:00
|
|
|
cx.editor
|
2023-02-20 05:00:44 +01:00
|
|
|
.set_status("Cannot find current stack frame to access variables.");
|
2021-08-29 15:43:00 +02:00
|
|
|
return;
|
|
|
|
}
|
2021-09-03 04:10:30 +02:00
|
|
|
};
|
|
|
|
|
2023-02-20 05:00:44 +01:00
|
|
|
let thread_frame = match debugger.stack_frames.get(&thread_id) {
|
|
|
|
Some(thread_frame) => thread_frame,
|
|
|
|
None => {
|
|
|
|
cx.editor
|
|
|
|
.set_error("Failed to get stack frame for thread: {thread_id}");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
let stack_frame = match thread_frame.get(frame) {
|
|
|
|
Some(stack_frame) => stack_frame,
|
|
|
|
None => {
|
|
|
|
cx.editor
|
|
|
|
.set_error("Failed to get stack frame for thread {thread_id} and frame {frame}.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let frame_id = stack_frame.id;
|
2021-09-03 04:10:30 +02:00
|
|
|
let scopes = match block_on(debugger.scopes(frame_id)) {
|
|
|
|
Ok(s) => s,
|
|
|
|
Err(e) => {
|
2021-11-07 13:20:58 +01:00
|
|
|
cx.editor.set_error(format!("Failed to get scopes: {}", e));
|
2021-09-03 04:10:30 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
2021-12-08 06:50:20 +01:00
|
|
|
|
2021-12-13 16:41:51 +01:00
|
|
|
// TODO: allow expanding variables into sub-fields
|
|
|
|
let mut variables = Vec::new();
|
2021-12-09 03:28:53 +01:00
|
|
|
|
|
|
|
let theme = &cx.editor.theme;
|
|
|
|
let scope_style = theme.get("ui.linenr.selected");
|
|
|
|
let type_style = theme.get("ui.text");
|
|
|
|
let text_style = theme.get("ui.text.focus");
|
|
|
|
|
2021-09-03 04:10:30 +02:00
|
|
|
for scope in scopes.iter() {
|
2021-12-09 03:28:53 +01:00
|
|
|
// use helix_view::graphics::Style;
|
2022-07-02 13:21:27 +02:00
|
|
|
use tui::text::Span;
|
2021-09-03 04:10:30 +02:00
|
|
|
let response = block_on(debugger.variables(scope.variables_reference));
|
|
|
|
|
2021-12-09 03:28:53 +01:00
|
|
|
variables.push(Spans::from(Span::styled(
|
|
|
|
format!("▸ {}", scope.name),
|
|
|
|
scope_style,
|
|
|
|
)));
|
|
|
|
|
2021-09-03 04:10:30 +02:00
|
|
|
if let Ok(vars) = response {
|
|
|
|
variables.reserve(vars.len());
|
|
|
|
for var in vars {
|
2021-12-09 03:28:53 +01:00
|
|
|
let mut spans = Vec::with_capacity(5);
|
|
|
|
|
|
|
|
spans.push(Span::styled(var.name.to_owned(), text_style));
|
|
|
|
if let Some(ty) = var.ty {
|
|
|
|
spans.push(Span::raw(": "));
|
|
|
|
spans.push(Span::styled(ty.to_owned(), type_style));
|
|
|
|
}
|
|
|
|
spans.push(Span::raw(" = "));
|
|
|
|
spans.push(Span::styled(var.value.to_owned(), text_style));
|
|
|
|
variables.push(Spans::from(spans));
|
2021-08-29 15:43:00 +02:00
|
|
|
}
|
|
|
|
}
|
2021-09-03 04:10:30 +02:00
|
|
|
}
|
2021-08-29 15:43:00 +02:00
|
|
|
|
2021-12-09 03:28:53 +01:00
|
|
|
let contents = Text::from(tui::text::Text::from(variables));
|
2022-02-13 10:31:51 +01:00
|
|
|
let popup = Popup::new("dap-variables", contents);
|
2021-11-07 09:55:01 +01:00
|
|
|
cx.push_layer(Box::new(popup));
|
2021-08-29 15:43:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn dap_terminate(cx: &mut Context) {
|
2021-12-07 16:59:11 +01:00
|
|
|
let debugger = debugger!(cx.editor);
|
2021-09-03 04:10:30 +02:00
|
|
|
|
feat(dap): send Disconnect if Terminated event received (#5532)
Send a `Disconnect` DAP request if the `Terminated` event is received.
According to the specification, if the debugging session was started by
as `launch`, the debuggee should be terminated alongside the session. If
instead the session was started as `attach`, it should not be disposed of.
This default behaviour can be overriden if the `supportTerminateDebuggee`
capability is supported by the adapter, through the `Disconnect` request
`terminateDebuggee` argument, as described in
[the specification][discon-spec].
This also implies saving the starting command for a debug sessions, in
order to decide which behaviour should be used, as well as validating the
capabilities of the adapter, in order to decide what the disconnect should
do.
An additional change made is handling of the `Exited` event, showing a
message if the exit code is different than `0`, for the user to be aware
off the termination failure.
[discon-spec]: https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Disconnect
Closes: #4674
Signed-off-by: Filip Dutescu <filip.dutescu@gmail.com>
2023-02-20 05:00:00 +01:00
|
|
|
let request = debugger.disconnect(None);
|
2022-02-15 08:30:23 +01:00
|
|
|
dap_callback(cx.jobs, request, |editor, _compositor, _response: ()| {
|
|
|
|
// editor.set_error(format!("Failed to disconnect: {}", e));
|
|
|
|
editor.debugger = None;
|
|
|
|
});
|
2021-08-29 15:43:00 +02:00
|
|
|
}
|
2021-08-29 16:28:31 +02:00
|
|
|
|
2021-09-26 09:24:58 +02:00
|
|
|
pub fn dap_enable_exceptions(cx: &mut Context) {
|
2021-12-07 16:59:11 +01:00
|
|
|
let debugger = debugger!(cx.editor);
|
2021-09-26 09:24:58 +02:00
|
|
|
|
|
|
|
let filters = match &debugger.capabilities().exception_breakpoint_filters {
|
2021-11-21 16:02:58 +01:00
|
|
|
Some(filters) => filters.iter().map(|f| f.filter.clone()).collect(),
|
2021-09-26 09:24:58 +02:00
|
|
|
None => return,
|
|
|
|
};
|
|
|
|
|
2022-02-15 08:30:23 +01:00
|
|
|
let request = debugger.set_exception_breakpoints(filters);
|
|
|
|
|
|
|
|
dap_callback(
|
|
|
|
cx.jobs,
|
|
|
|
request,
|
|
|
|
|_editor, _compositor, _response: dap::requests::SetExceptionBreakpointsResponse| {
|
|
|
|
// editor.set_error(format!("Failed to set up exception breakpoints: {}", e));
|
|
|
|
},
|
|
|
|
)
|
2021-09-26 09:24:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn dap_disable_exceptions(cx: &mut Context) {
|
2021-12-07 16:59:11 +01:00
|
|
|
let debugger = debugger!(cx.editor);
|
2021-09-26 09:24:58 +02:00
|
|
|
|
2022-02-15 08:30:23 +01:00
|
|
|
let request = debugger.set_exception_breakpoints(Vec::new());
|
|
|
|
|
|
|
|
dap_callback(
|
|
|
|
cx.jobs,
|
|
|
|
request,
|
|
|
|
|_editor, _compositor, _response: dap::requests::SetExceptionBreakpointsResponse| {
|
|
|
|
// editor.set_error(format!("Failed to set up exception breakpoints: {}", e));
|
|
|
|
},
|
|
|
|
)
|
2021-09-26 09:24:58 +02:00
|
|
|
}
|
|
|
|
|
2021-11-22 03:09:09 +01:00
|
|
|
// TODO: both edit condition and edit log need to be stable: we might get new breakpoints from the debugger which can change offsets
|
2021-09-04 21:57:58 +02:00
|
|
|
pub fn dap_edit_condition(cx: &mut Context) {
|
2021-12-02 02:21:28 +01:00
|
|
|
if let Some((pos, breakpoint)) = get_breakpoint_at_current_line(cx.editor) {
|
2021-11-22 03:22:08 +01:00
|
|
|
let path = match doc!(cx.editor).path() {
|
|
|
|
Some(path) => path.clone(),
|
|
|
|
None => return,
|
|
|
|
};
|
2021-09-05 07:14:17 +02:00
|
|
|
let callback = Box::pin(async move {
|
2022-07-12 05:38:26 +02:00
|
|
|
let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| {
|
2022-03-23 08:55:58 +01:00
|
|
|
let mut prompt = Prompt::new(
|
|
|
|
"condition:".into(),
|
|
|
|
None,
|
|
|
|
ui::completers::none,
|
|
|
|
move |cx, input: &str, event: PromptEvent| {
|
|
|
|
if event != PromptEvent::Validate {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let breakpoints = &mut cx.editor.breakpoints.get_mut(&path).unwrap();
|
|
|
|
breakpoints[pos].condition = match input {
|
|
|
|
"" => None,
|
|
|
|
input => Some(input.to_owned()),
|
|
|
|
};
|
|
|
|
|
|
|
|
let debugger = debugger!(cx.editor);
|
|
|
|
|
|
|
|
if let Err(e) = breakpoints_changed(debugger, path.clone(), breakpoints) {
|
|
|
|
cx.editor
|
|
|
|
.set_error(format!("Failed to set breakpoints: {}", e));
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
if let Some(condition) = breakpoint.condition {
|
2022-08-31 18:26:21 +02:00
|
|
|
prompt.insert_str(&condition, editor)
|
2022-03-23 08:55:58 +01:00
|
|
|
}
|
|
|
|
compositor.push(Box::new(prompt));
|
2022-07-12 05:38:26 +02:00
|
|
|
}));
|
2021-09-05 07:50:03 +02:00
|
|
|
Ok(call)
|
|
|
|
});
|
|
|
|
cx.jobs.callback(callback);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn dap_edit_log(cx: &mut Context) {
|
2021-12-02 02:21:28 +01:00
|
|
|
if let Some((pos, breakpoint)) = get_breakpoint_at_current_line(cx.editor) {
|
2021-11-22 03:22:08 +01:00
|
|
|
let path = match doc!(cx.editor).path() {
|
|
|
|
Some(path) => path.clone(),
|
|
|
|
None => return,
|
|
|
|
};
|
2021-09-05 07:50:03 +02:00
|
|
|
let callback = Box::pin(async move {
|
2022-07-12 05:38:26 +02:00
|
|
|
let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| {
|
2022-03-23 08:55:58 +01:00
|
|
|
let mut prompt = Prompt::new(
|
|
|
|
"log-message:".into(),
|
|
|
|
None,
|
|
|
|
ui::completers::none,
|
|
|
|
move |cx, input: &str, event: PromptEvent| {
|
|
|
|
if event != PromptEvent::Validate {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let breakpoints = &mut cx.editor.breakpoints.get_mut(&path).unwrap();
|
|
|
|
breakpoints[pos].log_message = match input {
|
|
|
|
"" => None,
|
|
|
|
input => Some(input.to_owned()),
|
|
|
|
};
|
|
|
|
|
|
|
|
let debugger = debugger!(cx.editor);
|
|
|
|
if let Err(e) = breakpoints_changed(debugger, path.clone(), breakpoints) {
|
|
|
|
cx.editor
|
|
|
|
.set_error(format!("Failed to set breakpoints: {}", e));
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
if let Some(log_message) = breakpoint.log_message {
|
2022-08-31 18:26:21 +02:00
|
|
|
prompt.insert_str(&log_message, editor);
|
2022-03-23 08:55:58 +01:00
|
|
|
}
|
|
|
|
compositor.push(Box::new(prompt));
|
2022-07-12 05:38:26 +02:00
|
|
|
}));
|
2021-09-05 07:14:17 +02:00
|
|
|
Ok(call)
|
|
|
|
});
|
|
|
|
cx.jobs.callback(callback);
|
2021-09-04 21:57:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-29 16:28:31 +02:00
|
|
|
pub fn dap_switch_thread(cx: &mut Context) {
|
2021-09-03 04:40:49 +02:00
|
|
|
thread_picker(cx, |editor, thread| {
|
|
|
|
block_on(select_thread_id(editor, thread.id, true));
|
|
|
|
})
|
2021-08-29 16:28:31 +02:00
|
|
|
}
|
2021-09-03 10:25:11 +02:00
|
|
|
pub fn dap_switch_stack_frame(cx: &mut Context) {
|
2021-12-07 16:59:11 +01:00
|
|
|
let debugger = debugger!(cx.editor);
|
2021-09-03 10:25:11 +02:00
|
|
|
|
|
|
|
let thread_id = match debugger.thread_id {
|
|
|
|
Some(thread_id) => thread_id,
|
|
|
|
None => {
|
2022-02-15 08:45:28 +01:00
|
|
|
cx.editor.set_error("No thread is currently active");
|
2021-09-03 10:25:11 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let frames = debugger.stack_frames[&thread_id].clone();
|
|
|
|
|
|
|
|
let picker = FilePicker::new(
|
|
|
|
frames,
|
2022-07-02 13:21:27 +02:00
|
|
|
(),
|
2021-11-07 10:03:04 +01:00
|
|
|
move |cx, frame, _action| {
|
2021-12-08 02:21:58 +01:00
|
|
|
let debugger = debugger!(cx.editor);
|
2021-09-03 10:25:11 +02:00
|
|
|
// TODO: this should be simpler to find
|
|
|
|
let pos = debugger.stack_frames[&thread_id]
|
|
|
|
.iter()
|
|
|
|
.position(|f| f.id == frame.id);
|
|
|
|
debugger.active_frame = pos;
|
2021-09-04 09:24:00 +02:00
|
|
|
|
|
|
|
let frame = debugger.stack_frames[&thread_id]
|
|
|
|
.get(pos.unwrap_or(0))
|
|
|
|
.cloned();
|
|
|
|
if let Some(frame) = &frame {
|
2021-11-07 10:03:04 +01:00
|
|
|
jump_to_stack_frame(cx.editor, frame);
|
2021-09-04 09:24:00 +02:00
|
|
|
}
|
2021-09-03 10:25:11 +02:00
|
|
|
},
|
|
|
|
move |_editor, frame| {
|
|
|
|
frame
|
|
|
|
.source
|
|
|
|
.as_ref()
|
|
|
|
.and_then(|source| source.path.clone())
|
|
|
|
.map(|path| {
|
|
|
|
(
|
2022-11-21 02:58:35 +01:00
|
|
|
path.into(),
|
2021-09-03 10:25:11 +02:00
|
|
|
Some((
|
|
|
|
frame.line.saturating_sub(1),
|
|
|
|
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
|
|
|
|
)),
|
|
|
|
)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
);
|
|
|
|
cx.push_layer(Box::new(picker))
|
|
|
|
}
|