Cargo.lock 🔗
@@ -14228,6 +14228,7 @@ dependencies = [
"gpui",
"hex",
"parking_lot",
+ "pretty_assertions",
"proto",
"schemars",
"serde",
Cole Miller created
- [x] Basic implementation
- [x] Match common VSC debug extension names to Zed debug adapters
- [ ] ~~`preLaunchTask` support~~ descoped for this PR
Release Notes:
- N/A
Cargo.lock | 1
crates/paths/src/paths.rs | 2
crates/project/src/project_settings.rs | 34 ++++
crates/task/Cargo.toml | 1
crates/task/src/lib.rs | 49 +++++++
crates/task/src/vscode_debug_format.rs | 184 ++++++++++++++++++++++++++++
crates/task/src/vscode_format.rs | 44 ------
7 files changed, 269 insertions(+), 46 deletions(-)
@@ -14228,6 +14228,7 @@ dependencies = [
"gpui",
"hex",
"parking_lot",
+ "pretty_assertions",
"proto",
"schemars",
"serde",
@@ -401,7 +401,7 @@ pub fn task_file_name() -> &'static str {
"tasks.json"
}
-/// Returns the relative path to a `launch.json` file within a project.
+/// Returns the relative path to a `debug.json` file within a project.
pub fn local_debug_file_relative_path() -> &'static Path {
Path::new(".zed/debug.json")
}
@@ -7,7 +7,8 @@ use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Task}
use lsp::LanguageServerName;
use paths::{
EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path,
- local_tasks_file_relative_path, local_vscode_tasks_file_relative_path,
+ local_tasks_file_relative_path, local_vscode_launch_file_relative_path,
+ local_vscode_tasks_file_relative_path,
};
use rpc::{
AnyProtoClient, TypedEnvelope,
@@ -24,7 +25,7 @@ use std::{
sync::Arc,
time::Duration,
};
-use task::{TaskTemplates, VsCodeTaskFile};
+use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
use util::{ResultExt, serde::default_true};
use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
@@ -573,6 +574,18 @@ impl SettingsObserver {
.unwrap(),
);
(settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug))
+ } else if path.ends_with(local_vscode_launch_file_relative_path()) {
+ let settings_dir = Arc::<Path>::from(
+ path.ancestors()
+ .nth(
+ local_vscode_tasks_file_relative_path()
+ .components()
+ .count()
+ .saturating_sub(1),
+ )
+ .unwrap(),
+ );
+ (settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug))
} else if path.ends_with(EDITORCONFIG_NAME) {
let Some(settings_dir) = path.parent().map(Arc::from) else {
continue;
@@ -618,6 +631,23 @@ impl SettingsObserver {
"serializing Zed tasks into JSON, file {abs_path:?}"
)
})
+ } else if abs_path.ends_with(local_vscode_launch_file_relative_path()) {
+ let vscode_tasks =
+ parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
+ .with_context(|| {
+ format!("parsing VSCode debug tasks, file {abs_path:?}")
+ })?;
+ let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
+ .with_context(|| {
+ format!(
+ "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
+ )
+ })?;
+ serde_json::to_string(&zed_tasks).with_context(|| {
+ format!(
+ "serializing Zed tasks into JSON, file {abs_path:?}"
+ )
+ })
} else {
Ok(content)
}
@@ -34,3 +34,4 @@ workspace-hack.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
+pretty_assertions.workspace = true
@@ -4,6 +4,7 @@ mod debug_format;
mod serde_helpers;
pub mod static_source;
mod task_template;
+mod vscode_debug_format;
mod vscode_format;
use collections::{HashMap, HashSet, hash_map};
@@ -22,6 +23,7 @@ pub use task_template::{
DebugArgs, DebugArgsRequest, HideStrategy, RevealStrategy, TaskModal, TaskTemplate,
TaskTemplates, TaskType,
};
+pub use vscode_debug_format::VsCodeDebugTaskFile;
pub use vscode_format::VsCodeTaskFile;
pub use zed_actions::RevealTarget;
@@ -522,3 +524,50 @@ impl ShellBuilder {
}
}
}
+
+type VsCodeEnvVariable = String;
+type ZedEnvVariable = String;
+
+struct EnvVariableReplacer {
+ variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>,
+}
+
+impl EnvVariableReplacer {
+ fn new(variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>) -> Self {
+ Self { variables }
+ }
+ // Replaces occurrences of VsCode-specific environment variables with Zed equivalents.
+ fn replace(&self, input: &str) -> String {
+ shellexpand::env_with_context_no_errors(&input, |var: &str| {
+ // Colons denote a default value in case the variable is not set. We want to preserve that default, as otherwise shellexpand will substitute it for us.
+ let colon_position = var.find(':').unwrap_or(var.len());
+ let (left, right) = var.split_at(colon_position);
+ if left == "env" && !right.is_empty() {
+ let variable_name = &right[1..];
+ return Some(format!("${{{variable_name}}}"));
+ }
+ let (variable_name, default) = (left, right);
+ let append_previous_default = |ret: &mut String| {
+ if !default.is_empty() {
+ ret.push_str(default);
+ }
+ };
+ if let Some(substitution) = self.variables.get(variable_name) {
+ // Got a VSCode->Zed hit, perform a substitution
+ let mut name = format!("${{{substitution}");
+ append_previous_default(&mut name);
+ name.push('}');
+ return Some(name);
+ }
+ // This is an unknown variable.
+ // We should not error out, as they may come from user environment (e.g. $PATH). That means that the variable substitution might not be perfect.
+ // If there's a default, we need to return the string verbatim as otherwise shellexpand will apply that default for us.
+ if !default.is_empty() {
+ return Some(format!("${{{var}}}"));
+ }
+ // Else we can just return None and that variable will be left as is.
+ None
+ })
+ .into_owned()
+ }
+}
@@ -0,0 +1,184 @@
+use std::path::PathBuf;
+
+use anyhow::anyhow;
+use collections::HashMap;
+use serde::Deserialize;
+use util::ResultExt as _;
+
+use crate::{
+ AttachRequest, DebugRequest, DebugTaskDefinition, DebugTaskFile, DebugTaskTemplate,
+ EnvVariableReplacer, LaunchRequest, TcpArgumentsTemplate, VariableName,
+};
+
+#[derive(Clone, Debug, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+enum Request {
+ Launch,
+ Attach,
+}
+
+// TODO support preLaunchTask linkage with other tasks
+#[derive(Clone, Debug, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+struct VsCodeDebugTaskDefinition {
+ r#type: String,
+ name: String,
+ request: Request,
+
+ #[serde(default)]
+ program: Option<String>,
+ #[serde(default)]
+ args: Vec<String>,
+ #[serde(default)]
+ env: HashMap<String, Option<String>>,
+ // TODO envFile?
+ #[serde(default)]
+ cwd: Option<String>,
+ #[serde(default)]
+ port: Option<u16>,
+ #[serde(default)]
+ stop_on_entry: Option<bool>,
+ #[serde(flatten)]
+ other_attributes: HashMap<String, serde_json_lenient::Value>,
+}
+
+impl VsCodeDebugTaskDefinition {
+ fn try_to_zed(self, replacer: &EnvVariableReplacer) -> anyhow::Result<DebugTaskTemplate> {
+ let label = replacer.replace(&self.name);
+ // TODO based on grep.app results it seems that vscode supports whitespace-splitting this field (ugh)
+ let definition = DebugTaskDefinition {
+ label,
+ request: match self.request {
+ Request::Launch => {
+ let cwd = self.cwd.map(|cwd| PathBuf::from(replacer.replace(&cwd)));
+ let program = self.program.ok_or_else(|| {
+ anyhow!("vscode debug launch configuration does not define a program")
+ })?;
+ let program = replacer.replace(&program);
+ let args = self
+ .args
+ .into_iter()
+ .map(|arg| replacer.replace(&arg))
+ .collect();
+ DebugRequest::Launch(LaunchRequest { program, cwd, args })
+ }
+ Request::Attach => DebugRequest::Attach(AttachRequest { process_id: None }),
+ },
+ adapter: task_type_to_adapter_name(self.r#type),
+ // TODO host?
+ tcp_connection: self.port.map(|port| TcpArgumentsTemplate {
+ port: Some(port),
+ host: None,
+ timeout: None,
+ }),
+ stop_on_entry: self.stop_on_entry,
+ // TODO
+ initialize_args: None,
+ };
+ let template = DebugTaskTemplate {
+ locator: None,
+ definition,
+ };
+ Ok(template)
+ }
+}
+
+/// blah
+#[derive(Clone, Debug, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct VsCodeDebugTaskFile {
+ version: String,
+ configurations: Vec<VsCodeDebugTaskDefinition>,
+}
+
+impl TryFrom<VsCodeDebugTaskFile> for DebugTaskFile {
+ type Error = anyhow::Error;
+
+ fn try_from(file: VsCodeDebugTaskFile) -> Result<Self, Self::Error> {
+ let replacer = EnvVariableReplacer::new(HashMap::from_iter([
+ (
+ "workspaceFolder".to_owned(),
+ VariableName::WorktreeRoot.to_string(),
+ ),
+ // TODO other interesting variables?
+ ]));
+ let templates = file
+ .configurations
+ .into_iter()
+ .filter_map(|config| config.try_to_zed(&replacer).log_err())
+ .collect::<Vec<_>>();
+ Ok(DebugTaskFile(templates))
+ }
+}
+
+// TODO figure out how to make JsDebugAdapter::ADAPTER_NAME et al available here
+fn task_type_to_adapter_name(task_type: String) -> String {
+ match task_type.as_str() {
+ "node" => "JavaScript".to_owned(),
+ "go" => "Delve".to_owned(),
+ "php" => "PHP".to_owned(),
+ "cppdbg" | "lldb" => "CodeLLDB".to_owned(),
+ "debugpy" => "Debugpy".to_owned(),
+ _ => task_type,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::{
+ DebugRequest, DebugTaskDefinition, DebugTaskFile, DebugTaskTemplate, LaunchRequest,
+ TcpArgumentsTemplate,
+ };
+
+ use super::VsCodeDebugTaskFile;
+
+ #[test]
+ fn test_parsing_vscode_launch_json() {
+ let raw = r#"
+ {
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Debug my JS app",
+ "request": "launch",
+ "type": "node",
+ "program": "${workspaceFolder}/xyz.js",
+ "showDevDebugOutput": false,
+ "stopOnEntry": true,
+ "args": ["--foo", "${workspaceFolder}/thing"],
+ "cwd": "${workspaceFolder}/${env:FOO}/sub",
+ "env": {
+ "X": "Y"
+ },
+ "port": 17
+ },
+ ]
+ }
+ "#;
+ let parsed: VsCodeDebugTaskFile =
+ serde_json_lenient::from_str(&raw).expect("deserializing launch.json");
+ let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates");
+ pretty_assertions::assert_eq!(
+ zed,
+ DebugTaskFile(vec![DebugTaskTemplate {
+ locator: None,
+ definition: DebugTaskDefinition {
+ label: "Debug my JS app".into(),
+ adapter: "JavaScript".into(),
+ stop_on_entry: Some(true),
+ initialize_args: None,
+ tcp_connection: Some(TcpArgumentsTemplate {
+ port: Some(17),
+ host: None,
+ timeout: None,
+ }),
+ request: DebugRequest::Launch(LaunchRequest {
+ program: "${ZED_WORKTREE_ROOT}/xyz.js".into(),
+ args: vec!["--foo".into(), "${ZED_WORKTREE_ROOT}/thing".into()],
+ cwd: Some("${ZED_WORKTREE_ROOT}/${FOO}/sub".into()),
+ }),
+ }
+ }])
+ );
+ }
+}
@@ -3,7 +3,7 @@ use collections::HashMap;
use serde::Deserialize;
use util::ResultExt;
-use crate::{TaskTemplate, TaskTemplates, VariableName};
+use crate::{EnvVariableReplacer, TaskTemplate, TaskTemplates, VariableName};
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
@@ -41,48 +41,6 @@ enum Command {
},
}
-type VsCodeEnvVariable = String;
-type ZedEnvVariable = String;
-
-struct EnvVariableReplacer {
- variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>,
-}
-
-impl EnvVariableReplacer {
- fn new(variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>) -> Self {
- Self { variables }
- }
- // Replaces occurrences of VsCode-specific environment variables with Zed equivalents.
- fn replace(&self, input: &str) -> String {
- shellexpand::env_with_context_no_errors(&input, |var: &str| {
- // Colons denote a default value in case the variable is not set. We want to preserve that default, as otherwise shellexpand will substitute it for us.
- let colon_position = var.find(':').unwrap_or(var.len());
- let (variable_name, default) = var.split_at(colon_position);
- let append_previous_default = |ret: &mut String| {
- if !default.is_empty() {
- ret.push_str(default);
- }
- };
- if let Some(substitution) = self.variables.get(variable_name) {
- // Got a VSCode->Zed hit, perform a substitution
- let mut name = format!("${{{substitution}");
- append_previous_default(&mut name);
- name.push('}');
- return Some(name);
- }
- // This is an unknown variable.
- // We should not error out, as they may come from user environment (e.g. $PATH). That means that the variable substitution might not be perfect.
- // If there's a default, we need to return the string verbatim as otherwise shellexpand will apply that default for us.
- if !default.is_empty() {
- return Some(format!("${{{var}}}"));
- }
- // Else we can just return None and that variable will be left as is.
- None
- })
- .into_owned()
- }
-}
-
impl VsCodeTaskDefinition {
fn into_zed_format(self, replacer: &EnvVariableReplacer) -> anyhow::Result<TaskTemplate> {
if self.other_attributes.contains_key("dependsOn") {