1use collections::HashMap;
2use gpui::SharedString;
3use serde::Deserialize;
4use util::ResultExt as _;
5
6use crate::{
7 DebugScenario, DebugTaskFile, EnvVariableReplacer, TcpArgumentsTemplate, VariableName,
8};
9
10#[derive(Clone, Debug, Deserialize, PartialEq)]
11#[serde(rename_all = "camelCase")]
12enum Request {
13 Launch,
14 Attach,
15}
16
17// TODO support preLaunchTask linkage with other tasks
18#[derive(Clone, Debug, Deserialize, PartialEq)]
19#[serde(rename_all = "camelCase")]
20struct VsCodeDebugTaskDefinition {
21 r#type: String,
22 name: String,
23 request: Request,
24
25 #[serde(default)]
26 program: Option<String>,
27 #[serde(default)]
28 args: Vec<String>,
29 #[serde(default)]
30 env: HashMap<String, Option<String>>,
31 // TODO envFile?
32 #[serde(default)]
33 cwd: Option<String>,
34 #[serde(default)]
35 port: Option<u16>,
36 #[serde(default)]
37 stop_on_entry: Option<bool>,
38 #[serde(flatten)]
39 other_attributes: serde_json::Value,
40}
41
42impl VsCodeDebugTaskDefinition {
43 fn try_to_zed(self, replacer: &EnvVariableReplacer) -> anyhow::Result<DebugScenario> {
44 let label = replacer.replace(&self.name).into();
45 // TODO based on grep.app results it seems that vscode supports whitespace-splitting this field (ugh)
46 let definition = DebugScenario {
47 label,
48 build: None,
49 adapter: task_type_to_adapter_name(&self.r#type),
50 // TODO host?
51 tcp_connection: self.port.map(|port| TcpArgumentsTemplate {
52 port: Some(port),
53 host: None,
54 timeout: None,
55 }),
56 config: replacer.replace_value(self.other_attributes),
57 };
58 Ok(definition)
59 }
60}
61
62#[derive(Clone, Debug, Deserialize, PartialEq)]
63#[serde(rename_all = "camelCase")]
64pub struct VsCodeDebugTaskFile {
65 version: String,
66 configurations: Vec<VsCodeDebugTaskDefinition>,
67}
68
69impl TryFrom<VsCodeDebugTaskFile> for DebugTaskFile {
70 type Error = anyhow::Error;
71
72 fn try_from(file: VsCodeDebugTaskFile) -> Result<Self, Self::Error> {
73 let replacer = EnvVariableReplacer::new(HashMap::from_iter([
74 (
75 "workspaceFolder".to_owned(),
76 VariableName::WorktreeRoot.to_string(),
77 ),
78 ("file".to_owned(), VariableName::Filename.to_string()), // TODO other interesting variables?
79 ]));
80 let templates = file
81 .configurations
82 .into_iter()
83 .filter_map(|config| config.try_to_zed(&replacer).log_err())
84 .collect::<Vec<_>>();
85 Ok(DebugTaskFile(templates))
86 }
87}
88
89// todo(debugger) figure out how to make JsDebugAdapter::ADAPTER_NAME et al available here
90fn task_type_to_adapter_name(task_type: &str) -> SharedString {
91 match task_type {
92 "node" => "JavaScript",
93 "go" => "Delve",
94 "php" => "PHP",
95 "cppdbg" | "lldb" => "CodeLLDB",
96 "debugpy" => "Debugpy",
97 "rdbg" => "Ruby",
98 _ => task_type,
99 }
100 .to_owned()
101 .into()
102}
103
104#[cfg(test)]
105mod tests {
106 use serde_json::json;
107
108 use crate::{DebugScenario, DebugTaskFile, TcpArgumentsTemplate};
109
110 use super::VsCodeDebugTaskFile;
111
112 #[test]
113 fn test_parsing_vscode_launch_json() {
114 let raw = r#"
115 {
116 "version": "0.2.0",
117 "configurations": [
118 {
119 "name": "Debug my JS app",
120 "request": "launch",
121 "type": "node",
122 "program": "${workspaceFolder}/xyz.js",
123 "showDevDebugOutput": false,
124 "stopOnEntry": true,
125 "args": ["--foo", "${workspaceFolder}/thing"],
126 "cwd": "${workspaceFolder}/${env:FOO}/sub",
127 "env": {
128 "X": "Y"
129 },
130 "port": 17
131 },
132 ]
133 }
134 "#;
135 let parsed: VsCodeDebugTaskFile =
136 serde_json_lenient::from_str(&raw).expect("deserializing launch.json");
137 let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates");
138 pretty_assertions::assert_eq!(
139 zed,
140 DebugTaskFile(vec![DebugScenario {
141 label: "Debug my JS app".into(),
142 adapter: "JavaScript".into(),
143 config: json!({
144 "showDevDebugOutput": false,
145 }),
146 tcp_connection: Some(TcpArgumentsTemplate {
147 port: Some(17),
148 host: None,
149 timeout: None,
150 }),
151 build: None
152 }])
153 );
154 }
155}