1use collections::HashMap;
  2use serde::Deserialize;
  3use util::ResultExt as _;
  4
  5use crate::{
  6    DebugScenario, DebugTaskFile, EnvVariableReplacer, TcpArgumentsTemplate, VariableName,
  7};
  8
  9// TODO support preLaunchTask linkage with other tasks
 10#[derive(Clone, Debug, Deserialize, PartialEq)]
 11#[serde(rename_all = "camelCase")]
 12struct VsCodeDebugTaskDefinition {
 13    r#type: String,
 14    name: String,
 15    #[serde(default)]
 16    port: Option<u16>,
 17    #[serde(flatten)]
 18    other_attributes: serde_json::Value,
 19}
 20
 21impl VsCodeDebugTaskDefinition {
 22    fn try_to_zed(mut self, replacer: &EnvVariableReplacer) -> anyhow::Result<DebugScenario> {
 23        let label = replacer.replace(&self.name);
 24        let mut config = replacer.replace_value(self.other_attributes);
 25        let adapter = task_type_to_adapter_name(&self.r#type);
 26        if let Some(config) = config.as_object_mut()
 27            && adapter == "JavaScript"
 28        {
 29            config.insert("type".to_owned(), self.r#type.clone().into());
 30            if let Some(port) = self.port.take() {
 31                config.insert("port".to_owned(), port.into());
 32            }
 33        }
 34        let definition = DebugScenario {
 35            label: label.into(),
 36            build: None,
 37            adapter: adapter.into(),
 38            tcp_connection: self.port.map(|port| TcpArgumentsTemplate {
 39                port: Some(port),
 40                host: None,
 41                timeout: None,
 42            }),
 43            config,
 44        };
 45        Ok(definition)
 46    }
 47}
 48
 49#[derive(Clone, Debug, Deserialize, PartialEq)]
 50#[serde(rename_all = "camelCase")]
 51pub struct VsCodeDebugTaskFile {
 52    #[serde(default)]
 53    version: Option<String>,
 54    configurations: Vec<VsCodeDebugTaskDefinition>,
 55}
 56
 57impl TryFrom<VsCodeDebugTaskFile> for DebugTaskFile {
 58    type Error = anyhow::Error;
 59
 60    fn try_from(file: VsCodeDebugTaskFile) -> Result<Self, Self::Error> {
 61        let replacer = EnvVariableReplacer::new(HashMap::from_iter([
 62            (
 63                "workspaceFolder".to_owned(),
 64                VariableName::WorktreeRoot.to_string(),
 65            ),
 66            (
 67                "relativeFile".to_owned(),
 68                VariableName::RelativeFile.to_string(),
 69            ),
 70            ("file".to_owned(), VariableName::File.to_string()),
 71        ]));
 72        let templates = file
 73            .configurations
 74            .into_iter()
 75            .filter_map(|config| config.try_to_zed(&replacer).log_err())
 76            .collect::<Vec<_>>();
 77        Ok(DebugTaskFile(templates))
 78    }
 79}
 80
 81fn task_type_to_adapter_name(task_type: &str) -> String {
 82    match task_type {
 83        "pwa-node" | "node" | "node-terminal" | "chrome" | "pwa-chrome" | "edge" | "pwa-edge"
 84        | "msedge" | "pwa-msedge" => "JavaScript",
 85        "go" => "Delve",
 86        "php" => "Xdebug",
 87        "cppdbg" | "lldb" => "CodeLLDB",
 88        "debugpy" => "Debugpy",
 89        "rdbg" => "rdbg",
 90        _ => task_type,
 91    }
 92    .to_owned()
 93}
 94
 95#[cfg(test)]
 96mod tests {
 97    use serde_json::json;
 98
 99    use crate::{DebugScenario, DebugTaskFile};
100
101    use super::VsCodeDebugTaskFile;
102
103    #[test]
104    fn test_parsing_vscode_launch_json() {
105        let raw = r#"
106            {
107                "version": "0.2.0",
108                "configurations": [
109                    {
110                        "name": "Debug my JS app",
111                        "request": "launch",
112                        "type": "node",
113                        "program": "${workspaceFolder}/xyz.js",
114                        "showDevDebugOutput": false,
115                        "stopOnEntry": true,
116                        "args": ["--foo", "${workspaceFolder}/thing"],
117                        "cwd": "${workspaceFolder}/${env:FOO}/sub",
118                        "env": {
119                            "X": "Y"
120                        },
121                        "port": 17
122                    },
123                ]
124            }
125        "#;
126        let parsed: VsCodeDebugTaskFile =
127            serde_json_lenient::from_str(raw).expect("deserializing launch.json");
128        let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates");
129        pretty_assertions::assert_eq!(
130            zed,
131            DebugTaskFile(vec![DebugScenario {
132                label: "Debug my JS app".into(),
133                adapter: "JavaScript".into(),
134                config: json!({
135                    "request": "launch",
136                    "program": "${ZED_WORKTREE_ROOT}/xyz.js",
137                    "showDevDebugOutput": false,
138                    "stopOnEntry": true,
139                    "args": [
140                        "--foo",
141                        "${ZED_WORKTREE_ROOT}/thing",
142                    ],
143                    "cwd": "${ZED_WORKTREE_ROOT}/${FOO}/sub",
144                    "env": {
145                        "X": "Y",
146                    },
147                    "type": "node",
148                    "port": 17,
149                }),
150                tcp_connection: None,
151                build: None
152            }])
153        );
154    }
155}