javascript.rs

  1use adapters::latest_github_release;
  2use anyhow::Context as _;
  3use collections::HashMap;
  4use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
  5use gpui::AsyncApp;
  6use serde_json::Value;
  7use std::{
  8    borrow::Cow,
  9    path::PathBuf,
 10    sync::{LazyLock, OnceLock},
 11};
 12use task::DebugRequest;
 13use util::{ResultExt, maybe};
 14
 15use crate::*;
 16
 17#[derive(Debug, Default)]
 18pub struct JsDebugAdapter {
 19    checked: OnceLock<()>,
 20}
 21
 22impl JsDebugAdapter {
 23    pub const ADAPTER_NAME: &'static str = "JavaScript";
 24    const ADAPTER_NPM_NAME: &'static str = "vscode-js-debug";
 25    const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js";
 26
 27    async fn fetch_latest_adapter_version(
 28        &self,
 29        delegate: &Arc<dyn DapDelegate>,
 30    ) -> Result<AdapterVersion> {
 31        let release = latest_github_release(
 32            &format!("microsoft/{}", Self::ADAPTER_NPM_NAME),
 33            true,
 34            false,
 35            delegate.http_client(),
 36        )
 37        .await?;
 38
 39        let asset_name = format!("js-debug-dap-{}.tar.gz", release.tag_name);
 40
 41        Ok(AdapterVersion {
 42            tag_name: release.tag_name,
 43            url: release
 44                .assets
 45                .iter()
 46                .find(|asset| asset.name == asset_name)
 47                .with_context(|| format!("no asset found matching {asset_name:?}"))?
 48                .browser_download_url
 49                .clone(),
 50        })
 51    }
 52
 53    async fn get_installed_binary(
 54        &self,
 55        delegate: &Arc<dyn DapDelegate>,
 56        task_definition: &DebugTaskDefinition,
 57        user_installed_path: Option<PathBuf>,
 58        user_args: Option<Vec<String>>,
 59        _: &mut AsyncApp,
 60    ) -> Result<DebugAdapterBinary> {
 61        let adapter_path = if let Some(user_installed_path) = user_installed_path {
 62            user_installed_path
 63        } else {
 64            let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
 65
 66            let file_name_prefix = format!("{}_", self.name());
 67
 68            util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
 69                file_name.starts_with(&file_name_prefix)
 70            })
 71            .await
 72            .context("Couldn't find JavaScript dap directory")?
 73        };
 74
 75        let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
 76        let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
 77
 78        let mut envs = HashMap::default();
 79
 80        let mut configuration = task_definition.config.clone();
 81        if let Some(configuration) = configuration.as_object_mut() {
 82            maybe!({
 83                configuration
 84                    .get("type")
 85                    .filter(|value| value == &"node-terminal")?;
 86                let command = configuration.get("command")?.as_str()?.to_owned();
 87                let mut args = shlex::split(&command)?.into_iter();
 88                let program = args.next()?;
 89                configuration.insert("runtimeExecutable".to_owned(), program.into());
 90                configuration.insert(
 91                    "runtimeArgs".to_owned(),
 92                    args.map(Value::from).collect::<Vec<_>>().into(),
 93                );
 94                configuration.insert("console".to_owned(), "externalTerminal".into());
 95                Some(())
 96            });
 97
 98            configuration.entry("type").and_modify(normalize_task_type);
 99
100            if let Some(program) = configuration
101                .get("program")
102                .cloned()
103                .and_then(|value| value.as_str().map(str::to_owned))
104            {
105                match program.as_str() {
106                    "npm" | "pnpm" | "yarn" | "bun"
107                        if !configuration.contains_key("runtimeExecutable")
108                            && !configuration.contains_key("runtimeArgs") =>
109                    {
110                        configuration.remove("program");
111                        configuration.insert("runtimeExecutable".to_owned(), program.into());
112                        if let Some(args) = configuration.remove("args") {
113                            configuration.insert("runtimeArgs".to_owned(), args);
114                        }
115                    }
116                    _ => {}
117                }
118            }
119
120            if let Some(env) = configuration.get("env").cloned() {
121                if let Ok(env) = serde_json::from_value(env) {
122                    envs = env;
123                }
124            }
125
126            configuration
127                .entry("cwd")
128                .or_insert(delegate.worktree_root_path().to_string_lossy().into());
129
130            configuration
131                .entry("console")
132                .or_insert("externalTerminal".into());
133
134            configuration.entry("sourceMaps").or_insert(true.into());
135            configuration
136                .entry("pauseForSourceMap")
137                .or_insert(true.into());
138            configuration
139                .entry("sourceMapRenames")
140                .or_insert(true.into());
141        }
142
143        let arguments = if let Some(mut args) = user_args {
144            args.insert(
145                0,
146                adapter_path
147                    .join(Self::ADAPTER_PATH)
148                    .to_string_lossy()
149                    .to_string(),
150            );
151            args
152        } else {
153            vec![
154                adapter_path
155                    .join(Self::ADAPTER_PATH)
156                    .to_string_lossy()
157                    .to_string(),
158                port.to_string(),
159                host.to_string(),
160            ]
161        };
162
163        Ok(DebugAdapterBinary {
164            command: Some(
165                delegate
166                    .node_runtime()
167                    .binary_path()
168                    .await?
169                    .to_string_lossy()
170                    .into_owned(),
171            ),
172            arguments,
173            cwd: Some(delegate.worktree_root_path().to_path_buf()),
174            envs,
175            connection: Some(adapters::TcpArguments {
176                host,
177                port,
178                timeout,
179            }),
180            request_args: StartDebuggingRequestArguments {
181                configuration,
182                request: self.request_kind(&task_definition.config).await?,
183            },
184        })
185    }
186}
187
188#[async_trait(?Send)]
189impl DebugAdapter for JsDebugAdapter {
190    fn name(&self) -> DebugAdapterName {
191        DebugAdapterName(Self::ADAPTER_NAME.into())
192    }
193
194    async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
195        let mut args = json!({
196            "type": "pwa-node",
197            "request": match zed_scenario.request {
198                DebugRequest::Launch(_) => "launch",
199                DebugRequest::Attach(_) => "attach",
200            },
201        });
202
203        let map = args.as_object_mut().unwrap();
204        match &zed_scenario.request {
205            DebugRequest::Attach(attach) => {
206                map.insert("processId".into(), attach.process_id.into());
207            }
208            DebugRequest::Launch(launch) => {
209                if launch.program.starts_with("http://") {
210                    map.insert("url".into(), launch.program.clone().into());
211                } else {
212                    map.insert("program".into(), launch.program.clone().into());
213                }
214
215                if !launch.args.is_empty() {
216                    map.insert("args".into(), launch.args.clone().into());
217                }
218                if !launch.env.is_empty() {
219                    map.insert("env".into(), launch.env_json());
220                }
221
222                if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
223                    map.insert("stopOnEntry".into(), stop_on_entry.into());
224                }
225                if let Some(cwd) = launch.cwd.as_ref() {
226                    map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
227                }
228            }
229        };
230
231        Ok(DebugScenario {
232            adapter: zed_scenario.adapter,
233            label: zed_scenario.label,
234            build: None,
235            config: args,
236            tcp_connection: None,
237        })
238    }
239
240    fn dap_schema(&self) -> Cow<'static, serde_json::Value> {
241        static SCHEMA: LazyLock<serde_json::Value> = LazyLock::new(|| {
242            const RAW_SCHEMA: &str = include_str!("../schemas/JavaScript.json");
243            serde_json::from_str(RAW_SCHEMA).unwrap()
244        });
245        Cow::Borrowed(&*SCHEMA)
246    }
247
248    async fn get_binary(
249        &self,
250        delegate: &Arc<dyn DapDelegate>,
251        config: &DebugTaskDefinition,
252        user_installed_path: Option<PathBuf>,
253        user_args: Option<Vec<String>>,
254        cx: &mut AsyncApp,
255    ) -> Result<DebugAdapterBinary> {
256        if self.checked.set(()).is_ok() {
257            delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
258            if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
259                adapters::download_adapter_from_github(
260                    Self::ADAPTER_NAME,
261                    version,
262                    adapters::DownloadedFileType::GzipTar,
263                    paths::debug_adapters_dir(),
264                    delegate.as_ref(),
265                )
266                .await?;
267            } else {
268                delegate.output_to_console(format!("{} debug adapter is up to date", self.name()));
269            }
270        }
271
272        self.get_installed_binary(delegate, &config, user_installed_path, user_args, cx)
273            .await
274    }
275
276    fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
277        let label = args
278            .configuration
279            .get("name")?
280            .as_str()
281            .filter(|name| !name.is_empty())?;
282        Some(label.to_owned())
283    }
284}
285
286#[cfg(feature = "update-schemas")]
287impl JsDebugAdapter {
288    pub fn get_schema(
289        temp_dir: &tempfile::TempDir,
290        delegate: UpdateSchemasDapDelegate,
291    ) -> anyhow::Result<serde_json::Value> {
292        let (package_json, package_nls_json) = get_vsix_package_json(
293            temp_dir,
294            &format!("microsoft/{}", Self::ADAPTER_NPM_NAME),
295            |release| {
296                let version = release
297                    .tag_name
298                    .strip_prefix("v")
299                    .context("parse version")?;
300                let asset_name = format!("ms-vscode.js-debug.{version}.vsix");
301                Ok(asset_name)
302            },
303            delegate,
304        )?;
305
306        let package_json = parse_package_json(package_json, package_nls_json)?;
307
308        let types = package_json
309            .contributes
310            .debuggers
311            .iter()
312            .map(|debugger| debugger.r#type.clone())
313            .collect::<Vec<_>>();
314        let mut conjuncts = package_json
315            .contributes
316            .debuggers
317            .into_iter()
318            .flat_map(|debugger| {
319                let r#type = debugger.r#type;
320                let configuration_attributes = debugger.configuration_attributes;
321                configuration_attributes
322                    .launch
323                    .map(|schema| ("launch", schema))
324                    .into_iter()
325                    .chain(
326                        configuration_attributes
327                            .attach
328                            .map(|schema| ("attach", schema)),
329                    )
330                    .map(|(request, schema)| {
331                        json!({
332                            "if": {
333                                "properties": {
334                                    "type": {
335                                        "const": r#type
336                                    },
337                                    "request": {
338                                        "const": request
339                                    }
340                                },
341                                "required": ["type", "request"]
342                            },
343                            "then": schema
344                        })
345                    })
346                    .collect::<Vec<_>>()
347            })
348            .collect::<Vec<_>>();
349        conjuncts.push(json!({
350            "properties": {
351                "type": {
352                    "enum": types
353                }
354            },
355            "required": ["type"]
356        }));
357        let schema = json!({
358            "allOf": conjuncts
359        });
360        Ok(schema)
361    }
362}
363
364fn normalize_task_type(task_type: &mut Value) {
365    let Some(task_type_str) = task_type.as_str() else {
366        return;
367    };
368
369    let new_name = match task_type_str {
370        "node" | "pwa-node" | "node-terminal" => "pwa-node",
371        "chrome" | "pwa-chrome" => "pwa-chrome",
372        "edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
373        _ => task_type_str,
374    }
375    .to_owned();
376
377    *task_type = Value::String(new_name);
378}