Implement basic support for VS Code debug configurations (#29160)
- [x] Basic implementation - [x] Match common VSC debug extension names to Zed debug adapters - [ ] ~~`preLaunchTask` support~~ descoped for this PR Release Notes: - N/A
This commit is contained in:
parent
36d02de784
commit
207fb04969
7 changed files with 269 additions and 46 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -14228,6 +14228,7 @@ dependencies = [
|
||||||
"gpui",
|
"gpui",
|
||||||
"hex",
|
"hex",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
"pretty_assertions",
|
||||||
"proto",
|
"proto",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
@ -401,7 +401,7 @@ pub fn task_file_name() -> &'static str {
|
||||||
"tasks.json"
|
"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 {
|
pub fn local_debug_file_relative_path() -> &'static Path {
|
||||||
Path::new(".zed/debug.json")
|
Path::new(".zed/debug.json")
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,8 @@ use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Task}
|
||||||
use lsp::LanguageServerName;
|
use lsp::LanguageServerName;
|
||||||
use paths::{
|
use paths::{
|
||||||
EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path,
|
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::{
|
use rpc::{
|
||||||
AnyProtoClient, TypedEnvelope,
|
AnyProtoClient, TypedEnvelope,
|
||||||
|
@ -24,7 +25,7 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use task::{TaskTemplates, VsCodeTaskFile};
|
use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
|
||||||
use util::{ResultExt, serde::default_true};
|
use util::{ResultExt, serde::default_true};
|
||||||
use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
|
use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
|
||||||
|
|
||||||
|
@ -573,6 +574,18 @@ impl SettingsObserver {
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
(settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug))
|
(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) {
|
} else if path.ends_with(EDITORCONFIG_NAME) {
|
||||||
let Some(settings_dir) = path.parent().map(Arc::from) else {
|
let Some(settings_dir) = path.parent().map(Arc::from) else {
|
||||||
continue;
|
continue;
|
||||||
|
@ -618,6 +631,23 @@ impl SettingsObserver {
|
||||||
"serializing Zed tasks into JSON, file {abs_path:?}"
|
"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 {
|
} else {
|
||||||
Ok(content)
|
Ok(content)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,3 +34,4 @@ workspace-hack.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
|
pretty_assertions.workspace = true
|
||||||
|
|
|
@ -4,6 +4,7 @@ mod debug_format;
|
||||||
mod serde_helpers;
|
mod serde_helpers;
|
||||||
pub mod static_source;
|
pub mod static_source;
|
||||||
mod task_template;
|
mod task_template;
|
||||||
|
mod vscode_debug_format;
|
||||||
mod vscode_format;
|
mod vscode_format;
|
||||||
|
|
||||||
use collections::{HashMap, HashSet, hash_map};
|
use collections::{HashMap, HashSet, hash_map};
|
||||||
|
@ -22,6 +23,7 @@ pub use task_template::{
|
||||||
DebugArgs, DebugArgsRequest, HideStrategy, RevealStrategy, TaskModal, TaskTemplate,
|
DebugArgs, DebugArgsRequest, HideStrategy, RevealStrategy, TaskModal, TaskTemplate,
|
||||||
TaskTemplates, TaskType,
|
TaskTemplates, TaskType,
|
||||||
};
|
};
|
||||||
|
pub use vscode_debug_format::VsCodeDebugTaskFile;
|
||||||
pub use vscode_format::VsCodeTaskFile;
|
pub use vscode_format::VsCodeTaskFile;
|
||||||
pub use zed_actions::RevealTarget;
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
184
crates/task/src/vscode_debug_format.rs
Normal file
184
crates/task/src/vscode_debug_format.rs
Normal file
|
@ -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 serde::Deserialize;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
use crate::{TaskTemplate, TaskTemplates, VariableName};
|
use crate::{EnvVariableReplacer, TaskTemplate, TaskTemplates, VariableName};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[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 {
|
impl VsCodeTaskDefinition {
|
||||||
fn into_zed_format(self, replacer: &EnvVariableReplacer) -> anyhow::Result<TaskTemplate> {
|
fn into_zed_format(self, replacer: &EnvVariableReplacer) -> anyhow::Result<TaskTemplate> {
|
||||||
if self.other_attributes.contains_key("dependsOn") {
|
if self.other_attributes.contains_key("dependsOn") {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue