vscode_debug_format.rs

  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}