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