1use serde_json::{Value, json};
10
11use crate::debug::{DebugServerKind, DebugTargetKind, is_server_supported_for_target};
12
13fn server_label(server: DebugServerKind) -> &'static str {
14 match server {
15 DebugServerKind::Jlink => "J-Link",
16 DebugServerKind::Openocd => "OpenOCD",
17 DebugServerKind::Pyocd => "pyOCD",
18 DebugServerKind::Gdbserver => "gdbserver",
19 DebugServerKind::None => "local",
20 }
21}
22
23pub fn create_launch_draft(
27 target: DebugTargetKind,
28 server: DebugServerKind,
29) -> Result<Value, String> {
30 if !is_server_supported_for_target(target, server) {
31 return Err(format!(
32 "Unsupported debug backend '{}' for target '{}'.",
33 server.as_str(),
34 target.as_str()
35 ));
36 }
37
38 let draft = match target {
39 DebugTargetKind::ZephyrMcu => {
40 let name = format!("ALP: Zephyr Debug ({})", server_label(server));
41 match server {
42 DebugServerKind::Openocd => json!({
43 "name": name,
44 "type": "cortex-debug",
45 "request": "launch",
46 "cwd": "${workspaceFolder}",
47 "executable": "${workspaceFolder}/build/app/zephyr/zephyr.elf",
48 "runToEntryPoint": "main",
49 "preLaunchTask": "alp: build active target",
50 "servertype": "openocd",
51 "configFiles": ["<resolved-openocd-board-cfg>"],
52 }),
53 DebugServerKind::Pyocd => json!({
54 "name": name,
55 "type": "cortex-debug",
56 "request": "launch",
57 "cwd": "${workspaceFolder}",
58 "executable": "${workspaceFolder}/build/app/zephyr/zephyr.elf",
59 "runToEntryPoint": "main",
60 "preLaunchTask": "alp: build active target",
61 "servertype": "pyocd",
62 "targetId": "<resolved-target-id>",
63 }),
64 _ => json!({
65 "name": name,
66 "type": "cortex-debug",
67 "request": "launch",
68 "cwd": "${workspaceFolder}",
69 "executable": "${workspaceFolder}/build/app/zephyr/zephyr.elf",
70 "runToEntryPoint": "main",
71 "preLaunchTask": "alp: build active target",
72 "servertype": "jlink",
73 "device": "<resolved-device>",
74 "interface": "swd",
75 }),
76 }
77 }
78 DebugTargetKind::BaremetalMcu => json!({
79 "name": format!("ALP: Baremetal Debug ({})", server_label(server)),
80 "type": "cortex-debug",
81 "request": "launch",
82 "servertype": server.as_str(),
83 "cwd": "${workspaceFolder}",
84 "executable": "${workspaceFolder}/build/baremetal/app.elf",
85 "device": "<resolved-device>",
86 "interface": "swd",
87 "svdFile": "<resolved-svd>",
88 "preLaunchTask": "alp: build baremetal target",
89 }),
90 DebugTargetKind::YoctoUserspace => json!({
91 "name": "ALP: Yocto Remote Debug",
92 "type": "cppdbg",
93 "request": "launch",
94 "program": "${workspaceFolder}/build/yocto/app",
95 "cwd": "${workspaceFolder}",
96 "MIMode": "gdb",
97 "miDebuggerServerAddress": "<host>:<port>",
98 "miDebuggerPath": "<resolved-gdb>",
99 "setupCommands": [{ "text": "-enable-pretty-printing" }],
100 "preLaunchTask": "alp: deploy and start gdbserver",
101 }),
102 DebugTargetKind::NativeHost => json!({
103 "name": "ALP: Native Sim Debug",
104 "type": "codelldb",
105 "request": "launch",
106 "program": "${workspaceFolder}/build/native_sim/zephyr/zephyr.exe",
107 "cwd": "${workspaceFolder}",
108 "preLaunchTask": "alp: build native_sim target",
109 }),
110 };
111 Ok(draft)
112}
113
114pub fn launch_preview_notes() -> Vec<String> {
116 vec![
117 "This is a draft launch configuration generated by the extension.".to_string(),
118 "Placeholder fields such as <resolved-device> still need project-specific resolution."
119 .to_string(),
120 "The long-term target is to resolve these values from the shared debug model.".to_string(),
121 ]
122}
123
124pub fn launch_preview_document(draft: Value) -> Value {
126 json!({
127 "version": "0.2.0",
128 "configurations": [draft],
129 })
130}
131
132#[derive(Debug)]
135pub struct LaunchJsonWritePlan {
136 pub content: String,
138 pub replaced: bool,
140}
141
142pub fn create_launch_json_write_plan(
145 existing_content: Option<&str>,
146 draft: &Value,
147) -> Result<LaunchJsonWritePlan, String> {
148 let mut document = parse_launch_json_or_default(existing_content)?;
149 let next_name = configuration_name(draft)?;
150
151 let configs = document
152 .get_mut("configurations")
153 .and_then(Value::as_array_mut)
154 .expect("configurations is always an array");
155
156 let existing_index = configs
157 .iter()
158 .position(|c| c.get("name").and_then(Value::as_str) == Some(next_name));
159
160 let replaced = match existing_index {
161 Some(index) => {
162 configs[index] = draft.clone();
163 true
164 }
165 None => {
166 configs.push(draft.clone());
167 false
168 }
169 };
170
171 let content = format!(
172 "{}\n",
173 serde_json::to_string_pretty(&document).expect("launch document is serializable")
174 );
175 Ok(LaunchJsonWritePlan { content, replaced })
176}
177
178fn parse_launch_json_or_default(content: Option<&str>) -> Result<Value, String> {
179 let trimmed = content.map(str::trim).unwrap_or("");
180 if trimmed.is_empty() {
181 return Ok(json!({ "version": "0.2.0", "configurations": [] }));
182 }
183
184 let parsed: Value = serde_json::from_str(trimmed)
185 .map_err(|_| "Alp: .vscode/launch.json is not valid JSON.".to_string())?;
186 let Value::Object(mut candidate) = parsed else {
187 return Err("Alp: .vscode/launch.json must be a JSON object.".to_string());
188 };
189
190 let version = match candidate.get("version").and_then(Value::as_str) {
191 Some(v) if !v.trim().is_empty() => Value::String(v.to_string()),
192 _ => Value::String("0.2.0".to_string()),
193 };
194 let configurations = match candidate.get("configurations") {
195 Some(Value::Array(entries)) => {
196 Value::Array(entries.iter().filter(|e| e.is_object()).cloned().collect())
197 }
198 _ => Value::Array(Vec::new()),
199 };
200
201 candidate.insert("version".to_string(), version);
203 candidate.insert("configurations".to_string(), configurations);
204 Ok(Value::Object(candidate))
205}
206
207fn configuration_name(configuration: &Value) -> Result<&str, String> {
208 match configuration.get("name").and_then(Value::as_str) {
209 Some(name) if !name.trim().is_empty() => Ok(name),
210 _ => Err("Alp: debug launch draft is missing a valid name.".to_string()),
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn unsupported_server_errors() {
220 let err =
221 create_launch_draft(DebugTargetKind::NativeHost, DebugServerKind::Jlink).unwrap_err();
222 assert!(err.contains("Unsupported debug backend 'jlink' for target 'native-host'"));
223 }
224
225 #[test]
226 fn zephyr_jlink_draft_key_order_preserved() {
227 let draft =
228 create_launch_draft(DebugTargetKind::ZephyrMcu, DebugServerKind::Jlink).unwrap();
229 let json = serde_json::to_string(&draft).unwrap();
230 assert!(json.starts_with(
232 "{\"name\":\"ALP: Zephyr Debug (J-Link)\",\"type\":\"cortex-debug\",\"request\":\"launch\""
233 ));
234 assert!(json.ends_with(
235 "\"servertype\":\"jlink\",\"device\":\"<resolved-device>\",\"interface\":\"swd\"}"
236 ));
237 }
238
239 #[test]
240 fn write_plan_appends_then_replaces_by_name() {
241 let draft =
242 create_launch_draft(DebugTargetKind::NativeHost, DebugServerKind::None).unwrap();
243 let plan = create_launch_json_write_plan(None, &draft).unwrap();
244 assert!(!plan.replaced);
245 assert!(plan.content.ends_with("\n"));
246 assert!(plan.content.contains("\"version\": \"0.2.0\""));
247
248 let plan2 = create_launch_json_write_plan(Some(&plan.content), &draft).unwrap();
250 assert!(plan2.replaced);
251 let doc: Value = serde_json::from_str(&plan2.content).unwrap();
252 assert_eq!(doc["configurations"].as_array().unwrap().len(), 1);
253 }
254
255 #[test]
256 fn preserves_unknown_top_level_keys() {
257 let existing = "{\"version\":\"0.1.0\",\"inputs\":[],\"configurations\":[]}";
258 let draft =
259 create_launch_draft(DebugTargetKind::NativeHost, DebugServerKind::None).unwrap();
260 let plan = create_launch_json_write_plan(Some(existing), &draft).unwrap();
261 assert!(plan.content.contains("\"inputs\""));
263 assert!(plan.content.contains("\"version\": \"0.1.0\""));
264 }
265
266 #[test]
267 fn invalid_json_errors() {
268 let err =
269 create_launch_json_write_plan(Some("{not json"), &json!({"name": "x"})).unwrap_err();
270 assert!(err.contains("not valid JSON"));
271 }
272}