vscode_debug_format.rs

  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#[derive(Clone, Debug, Deserialize, PartialEq)]
 87#[serde(rename_all = "camelCase")]
 88pub struct VsCodeDebugTaskFile {
 89    version: String,
 90    configurations: Vec<VsCodeDebugTaskDefinition>,
 91}
 92
 93impl TryFrom<VsCodeDebugTaskFile> for DebugTaskFile {
 94    type Error = anyhow::Error;
 95
 96    fn try_from(file: VsCodeDebugTaskFile) -> Result<Self, Self::Error> {
 97        let replacer = EnvVariableReplacer::new(HashMap::from_iter([
 98            (
 99                "workspaceFolder".to_owned(),
100                VariableName::WorktreeRoot.to_string(),
101            ),
102            // TODO other interesting variables?
103        ]));
104        let templates = file
105            .configurations
106            .into_iter()
107            .filter_map(|config| config.try_to_zed(&replacer).log_err())
108            .collect::<Vec<_>>();
109        Ok(DebugTaskFile(templates))
110    }
111}
112
113// TODO figure out how to make JsDebugAdapter::ADAPTER_NAME et al available here
114fn task_type_to_adapter_name(task_type: String) -> String {
115    match task_type.as_str() {
116        "node" => "JavaScript".to_owned(),
117        "go" => "Delve".to_owned(),
118        "php" => "PHP".to_owned(),
119        "cppdbg" | "lldb" => "CodeLLDB".to_owned(),
120        "debugpy" => "Debugpy".to_owned(),
121        _ => task_type,
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use crate::{
128        DebugRequest, DebugTaskDefinition, DebugTaskFile, DebugTaskTemplate, LaunchRequest,
129        TcpArgumentsTemplate,
130    };
131
132    use super::VsCodeDebugTaskFile;
133
134    #[test]
135    fn test_parsing_vscode_launch_json() {
136        let raw = r#"
137            {
138                "version": "0.2.0",
139                "configurations": [
140                    {
141                        "name": "Debug my JS app",
142                        "request": "launch",
143                        "type": "node",
144                        "program": "${workspaceFolder}/xyz.js",
145                        "showDevDebugOutput": false,
146                        "stopOnEntry": true,
147                        "args": ["--foo", "${workspaceFolder}/thing"],
148                        "cwd": "${workspaceFolder}/${env:FOO}/sub",
149                        "env": {
150                            "X": "Y"
151                        },
152                        "port": 17
153                    },
154                ]
155            }
156        "#;
157        let parsed: VsCodeDebugTaskFile =
158            serde_json_lenient::from_str(&raw).expect("deserializing launch.json");
159        let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates");
160        pretty_assertions::assert_eq!(
161            zed,
162            DebugTaskFile(vec![DebugTaskTemplate {
163                locator: None,
164                definition: DebugTaskDefinition {
165                    label: "Debug my JS app".into(),
166                    adapter: "JavaScript".into(),
167                    stop_on_entry: Some(true),
168                    initialize_args: None,
169                    tcp_connection: Some(TcpArgumentsTemplate {
170                        port: Some(17),
171                        host: None,
172                        timeout: None,
173                    }),
174                    request: DebugRequest::Launch(LaunchRequest {
175                        program: "${ZED_WORKTREE_ROOT}/xyz.js".into(),
176                        args: vec!["--foo".into(), "${ZED_WORKTREE_ROOT}/thing".into()],
177                        cwd: Some("${ZED_WORKTREE_ROOT}/${FOO}/sub".into()),
178                    }),
179                }
180            }])
181        );
182    }
183}