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: 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 // 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 _ => task_type,
98 }
99 .to_owned()
100 .into()
101}
102
103#[cfg(test)]
104mod tests {
105 use serde_json::json;
106
107 use crate::{DebugScenario, DebugTaskFile, TcpArgumentsTemplate};
108
109 use super::VsCodeDebugTaskFile;
110
111 #[test]
112 fn test_parsing_vscode_launch_json() {
113 let raw = r#"
114 {
115 "version": "0.2.0",
116 "configurations": [
117 {
118 "name": "Debug my JS app",
119 "request": "launch",
120 "type": "node",
121 "program": "${workspaceFolder}/xyz.js",
122 "showDevDebugOutput": false,
123 "stopOnEntry": true,
124 "args": ["--foo", "${workspaceFolder}/thing"],
125 "cwd": "${workspaceFolder}/${env:FOO}/sub",
126 "env": {
127 "X": "Y"
128 },
129 "port": 17
130 },
131 ]
132 }
133 "#;
134 let parsed: VsCodeDebugTaskFile =
135 serde_json_lenient::from_str(&raw).expect("deserializing launch.json");
136 let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates");
137 pretty_assertions::assert_eq!(
138 zed,
139 DebugTaskFile(vec![DebugScenario {
140 label: "Debug my JS app".into(),
141 adapter: "JavaScript".into(),
142 config: json!({
143 "showDevDebugOutput": false,
144 }),
145 tcp_connection: Some(TcpArgumentsTemplate {
146 port: Some(17),
147 host: None,
148 timeout: None,
149 }),
150 build: None
151 }])
152 );
153 }
154}