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