pub mod client;
use std::collections::{BTreeMap, HashMap};
use std::path::PathBuf;
use std::sync::mpsc::{Receiver, Sender};
pub use client::DapClient;
#[derive(Debug, Clone)]
pub struct AdapterConfig {
pub cmd: String,
pub args: Vec<String>,
pub launch: serde_json::Value,
}
impl AdapterConfig {
pub fn from_toml(v: &toml::Value) -> Result<Self, String> {
let table = v.as_table().ok_or("expected a table")?;
let cmd = table
.get("cmd")
.and_then(|c| c.as_str())
.ok_or("missing `cmd`")?
.to_string();
let args = table
.get("args")
.and_then(|a| a.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let launch_toml = table
.get("launch")
.cloned()
.unwrap_or(toml::Value::Table(toml::value::Table::new()));
let launch = serde_json::to_value(launch_toml).map_err(|e| format!("launch json: {e}"))?;
Ok(AdapterConfig { cmd, args, launch })
}
}
pub fn substitute_vars(
v: &mut serde_json::Value,
workspace: &std::path::Path,
file: Option<&std::path::Path>,
) {
match v {
serde_json::Value::String(s) => {
let mut out = s.clone();
if let Some(f) = file {
out = out.replace("${file}", &f.to_string_lossy());
out = out.replace(
"${fileBasename}",
&f.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default(),
);
out = out.replace(
"${fileDirname}",
&f.parent()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default(),
);
}
out = out.replace("${workspaceFolder}", &workspace.to_string_lossy());
out = out.replace("${workspace}", &workspace.to_string_lossy());
*s = out;
}
serde_json::Value::Array(arr) => {
for child in arr {
substitute_vars(child, workspace, file);
}
}
serde_json::Value::Object(obj) => {
for (_, child) in obj.iter_mut() {
substitute_vars(child, workspace, file);
}
}
_ => {}
}
}
#[derive(Debug)]
pub enum DapEvent {
Initialized,
Running,
Output { category: String, text: String },
Stopped {
reason: String,
thread_id: i64,
description: Option<String>,
},
Continued,
StackTrace {
thread_id: i64,
frames: Vec<StackFrame>,
},
Scopes { frame_id: i64, scopes: Vec<Scope> },
Variables {
variables_ref: i64,
variables: Vec<Variable>,
},
Evaluate {
expression: String,
value: String,
ty: Option<String>,
err: Option<String>,
variables_ref: i64,
},
Threads(Vec<ThreadInfo>),
SetVariableDone {
parent_ref: i64,
name: String,
value: String,
ty: Option<String>,
variables_ref: i64,
},
InitializeCaps {
exception_filters: Vec<ExceptionFilter>,
},
Exited { exit_code: i64 },
Terminated,
Failed(String),
}
#[derive(Debug, Clone)]
pub struct StackFrame {
pub id: i64,
pub name: String,
pub source: Option<PathBuf>,
pub line: u32,
pub column: u32,
}
#[derive(Debug, Clone)]
pub struct Scope {
pub name: String,
pub variables_reference: i64,
pub expensive: bool,
}
#[derive(Debug, Clone)]
pub struct ThreadInfo {
pub id: i64,
pub name: String,
}
#[derive(Debug, Clone)]
pub struct ExceptionFilter {
pub filter: String,
pub label: String,
pub default: bool,
}
#[derive(Debug, Clone)]
pub struct Variable {
pub name: String,
pub value: String,
pub ty: Option<String>,
pub variables_reference: i64,
}
pub struct DapManager {
pub client: DapClient,
pub rx: Receiver<DapEvent>,
pub initialized: bool,
pub running: bool,
pub stopped_at: Option<(i64, Option<PathBuf>, u32, String)>,
pub stack_frames: Vec<StackFrame>,
pub scopes: Vec<Scope>,
pub variables: HashMap<i64, Vec<Variable>>,
pub expanded_vars: std::collections::HashSet<i64>,
pub expanded_paths: std::collections::HashSet<Vec<String>>,
pub threads: Vec<ThreadInfo>,
pub exception_filters: Vec<ExceptionFilter>,
pub enabled_exception_filters: std::collections::HashSet<String>,
}
impl DapManager {
pub fn new(client: DapClient, rx: Receiver<DapEvent>) -> Self {
DapManager {
client,
rx,
initialized: false,
running: false,
stopped_at: None,
stack_frames: Vec::new(),
scopes: Vec::new(),
variables: HashMap::new(),
expanded_vars: std::collections::HashSet::new(),
expanded_paths: std::collections::HashSet::new(),
threads: Vec::new(),
exception_filters: Vec::new(),
enabled_exception_filters: std::collections::HashSet::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct VarRow {
pub depth: usize,
pub is_scope: bool,
pub label: String,
pub name: String,
pub value: String,
pub var_ref: i64,
pub expanded: bool,
pub expandable: bool,
pub parent_ref: i64,
}
impl DapManager {
pub fn variable_rows(&self) -> Vec<VarRow> {
let mut out: Vec<VarRow> = Vec::new();
for scope in &self.scopes {
let r = scope.variables_reference;
let expandable = r > 0;
let expanded = expandable && self.expanded_vars.contains(&r);
out.push(VarRow {
depth: 0,
is_scope: true,
label: scope.name.clone(),
name: scope.name.clone(),
value: if scope.expensive {
"(expensive)".to_string()
} else {
String::new()
},
var_ref: r,
expanded,
expandable,
parent_ref: 0,
});
if expanded && let Some(vars) = self.variables.get(&r) {
for v in vars {
self.walk_var(v, 1, r, &mut out);
}
}
}
out
}
fn walk_var(&self, v: &Variable, depth: usize, parent_ref: i64, out: &mut Vec<VarRow>) {
let expandable = v.variables_reference > 0;
let expanded = expandable && self.expanded_vars.contains(&v.variables_reference);
let label = match &v.ty {
Some(ty) if !ty.is_empty() => format!("{}: {}", v.name, ty),
_ => v.name.clone(),
};
out.push(VarRow {
depth,
is_scope: false,
label,
name: v.name.clone(),
value: v.value.clone(),
var_ref: v.variables_reference,
expanded,
expandable,
parent_ref,
});
if expanded && let Some(children) = self.variables.get(&v.variables_reference) {
for child in children {
self.walk_var(child, depth + 1, v.variables_reference, out);
}
}
}
pub fn path_for_var_ref(&self, var_ref: i64) -> Option<Vec<String>> {
path_for_var_ref_in(&self.variable_rows(), var_ref)
}
pub fn var_ref_for_path(&self, path: &[String]) -> Option<i64> {
var_ref_for_path_in(&self.variable_rows(), path)
}
}
pub fn path_for_var_ref_in(rows: &[VarRow], var_ref: i64) -> Option<Vec<String>> {
use std::collections::HashMap;
let by_ref: HashMap<i64, &VarRow> = rows.iter().map(|r| (r.var_ref, r)).collect();
let mut current = by_ref.get(&var_ref).copied()?;
let mut path = vec![current.name.clone()];
while current.parent_ref != 0 {
let Some(parent) = by_ref.get(¤t.parent_ref).copied() else {
break;
};
path.insert(0, parent.name.clone());
current = parent;
}
Some(path)
}
pub fn var_ref_for_path_in(rows: &[VarRow], path: &[String]) -> Option<i64> {
let mut current_parent: i64 = 0;
let mut chosen_ref: Option<i64> = None;
for (target_depth, segment) in path.iter().enumerate() {
chosen_ref = rows.iter().find_map(|r| {
if r.depth == target_depth && r.parent_ref == current_parent && r.name == *segment {
Some(r.var_ref)
} else {
None
}
});
current_parent = chosen_ref?;
}
chosen_ref
}
#[allow(dead_code)]
pub fn parse_adapters(table: &BTreeMap<String, toml::Value>) -> BTreeMap<String, AdapterConfig> {
let mut out = BTreeMap::new();
for (name, v) in table {
match AdapterConfig::from_toml(v) {
Ok(cfg) => {
out.insert(name.clone(), cfg);
}
Err(e) => {
eprintln!("dap: skipping [dap.{name}]: {e}");
}
}
}
out
}
#[allow(dead_code)]
pub type DapEventTx = Sender<DapEvent>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn substitute_vars_replaces_file_and_workspace() {
let mut v = serde_json::json!({
"program": "${file}",
"cwd": "${workspace}",
"args": ["--ws", "${workspaceFolder}"],
});
let ws = std::path::Path::new("/repo");
let f = std::path::Path::new("/repo/src/main.py");
substitute_vars(&mut v, ws, Some(f));
assert_eq!(v["program"], "/repo/src/main.py");
assert_eq!(v["cwd"], "/repo");
assert_eq!(v["args"][1], "/repo");
}
#[test]
fn variable_rows_flattens_scopes_and_expanded_composites() {
use std::sync::mpsc;
let (_tx, rx) = mpsc::channel::<DapEvent>();
struct Mgr {
scopes: Vec<Scope>,
variables: HashMap<i64, Vec<Variable>>,
expanded_vars: std::collections::HashSet<i64>,
}
impl Mgr {
fn variable_rows(&self) -> Vec<VarRow> {
let real = FakeDap {
scopes: self.scopes.clone(),
variables: self.variables.clone(),
expanded_vars: self.expanded_vars.clone(),
};
real.variable_rows()
}
}
struct FakeDap {
scopes: Vec<Scope>,
variables: HashMap<i64, Vec<Variable>>,
expanded_vars: std::collections::HashSet<i64>,
}
impl FakeDap {
fn variable_rows(&self) -> Vec<VarRow> {
let mut out: Vec<VarRow> = Vec::new();
for scope in &self.scopes {
let r = scope.variables_reference;
let expandable = r > 0;
let expanded = expandable && self.expanded_vars.contains(&r);
out.push(VarRow {
depth: 0,
is_scope: true,
label: scope.name.clone(),
name: scope.name.clone(),
value: String::new(),
var_ref: r,
expanded,
expandable,
parent_ref: 0,
});
if expanded && let Some(vars) = self.variables.get(&r) {
for v in vars {
self.walk(v, 1, r, &mut out);
}
}
}
out
}
fn walk(&self, v: &Variable, depth: usize, parent_ref: i64, out: &mut Vec<VarRow>) {
let expandable = v.variables_reference > 0;
let expanded = expandable && self.expanded_vars.contains(&v.variables_reference);
out.push(VarRow {
depth,
is_scope: false,
label: v.name.clone(),
name: v.name.clone(),
value: v.value.clone(),
var_ref: v.variables_reference,
expanded,
expandable,
parent_ref,
});
if expanded && let Some(children) = self.variables.get(&v.variables_reference) {
for child in children {
self.walk(child, depth + 1, v.variables_reference, out);
}
}
}
}
let _ = rx; let scope = Scope {
name: "Locals".to_string(),
variables_reference: 1,
expensive: false,
};
let composite = Variable {
name: "list".to_string(),
value: "Vec<i32>".to_string(),
ty: None,
variables_reference: 2,
};
let leaf = Variable {
name: "count".to_string(),
value: "42".to_string(),
ty: None,
variables_reference: 0,
};
let child = Variable {
name: "[0]".to_string(),
value: "7".to_string(),
ty: None,
variables_reference: 0,
};
let mut variables = HashMap::new();
variables.insert(1, vec![composite.clone(), leaf.clone()]);
variables.insert(2, vec![child.clone()]);
let mut expanded = std::collections::HashSet::new();
expanded.insert(1); expanded.insert(2); let mgr = Mgr {
scopes: vec![scope],
variables,
expanded_vars: expanded,
};
let rows = mgr.variable_rows();
assert_eq!(rows.len(), 4);
assert!(rows[0].is_scope);
assert_eq!(rows[0].label, "Locals");
assert_eq!(rows[0].parent_ref, 0); assert_eq!(rows[1].depth, 1);
assert_eq!(rows[1].label, "list");
assert!(rows[1].expanded);
assert_eq!(rows[1].parent_ref, 1); assert_eq!(rows[2].depth, 2);
assert_eq!(rows[2].label, "[0]");
assert_eq!(rows[2].parent_ref, 2); assert_eq!(rows[3].depth, 1);
assert_eq!(rows[3].label, "count");
assert!(!rows[3].expandable);
assert_eq!(rows[3].parent_ref, 1); }
fn mk_row(depth: usize, name: &str, var_ref: i64, parent_ref: i64) -> VarRow {
VarRow {
depth,
is_scope: depth == 0,
label: name.to_string(),
name: name.to_string(),
value: String::new(),
var_ref,
expanded: false,
expandable: var_ref > 0,
parent_ref,
}
}
#[test]
fn path_for_var_ref_walks_back_to_scope() {
let rows = vec![
mk_row(0, "Locals", 1, 0),
mk_row(1, "self", 2, 1),
mk_row(2, "data", 3, 2),
];
let p = path_for_var_ref_in(&rows, 3).unwrap();
assert_eq!(p, vec!["Locals", "self", "data"]);
let p2 = path_for_var_ref_in(&rows, 2).unwrap();
assert_eq!(p2, vec!["Locals", "self"]);
let p3 = path_for_var_ref_in(&rows, 1).unwrap();
assert_eq!(p3, vec!["Locals"]);
}
#[test]
fn path_for_var_ref_returns_none_when_ref_missing() {
let rows = vec![mk_row(0, "Locals", 1, 0)];
assert!(path_for_var_ref_in(&rows, 999).is_none());
}
#[test]
fn var_ref_for_path_resolves_top_down() {
let rows = vec![
mk_row(0, "Locals", 10, 0),
mk_row(1, "self", 20, 10),
mk_row(2, "data", 30, 20),
];
assert_eq!(
var_ref_for_path_in(&rows, &["Locals".to_string()]),
Some(10)
);
assert_eq!(
var_ref_for_path_in(&rows, &["Locals".to_string(), "self".to_string()]),
Some(20)
);
assert_eq!(
var_ref_for_path_in(
&rows,
&["Locals".to_string(), "self".to_string(), "data".to_string()]
),
Some(30)
);
}
#[test]
fn var_ref_for_path_returns_none_when_segment_missing() {
let rows = vec![mk_row(0, "Locals", 1, 0), mk_row(1, "self", 2, 1)];
assert_eq!(
var_ref_for_path_in(&rows, &["Locals".to_string(), "nope".to_string()]),
None
);
}
#[test]
fn path_round_trips_through_ref_renumbering() {
let old = vec![
mk_row(0, "Locals", 1, 0),
mk_row(1, "self", 2, 1),
mk_row(2, "data", 3, 2),
];
let path = path_for_var_ref_in(&old, 3).unwrap();
let new = vec![
mk_row(0, "Locals", 100, 0),
mk_row(1, "self", 200, 100),
mk_row(2, "data", 300, 200),
];
let new_ref = var_ref_for_path_in(&new, &path).unwrap();
assert_eq!(new_ref, 300);
}
#[test]
fn adapter_from_toml_parses_basic_shape() {
let raw: toml::Value = toml::from_str(
r#"
cmd = "python"
args = ["-m", "debugpy.adapter"]
[launch]
request = "launch"
program = "${file}"
"#,
)
.unwrap();
let cfg = AdapterConfig::from_toml(&raw).unwrap();
assert_eq!(cfg.cmd, "python");
assert_eq!(cfg.args, vec!["-m", "debugpy.adapter"]);
assert_eq!(cfg.launch["request"], "launch");
}
}