debugger: Use DAP schema to configure daps (#30833)

This PR allows DAPs to define their own schema so users can see
completion items when editing their debug.json files.

Users facing this aren’t the biggest chance, but behind the scenes, this
affected a lot of code because we manually translated common fields from
Zed's config format to be adapter-specific. Now we store the raw JSON
from a user's configuration file and just send that.

I'm ignoring the Protobuf CICD error because the DebugTaskDefinition
message is not yet user facing and we need to deprecate some fields in
it.

Release Notes:

- debugger beta: Show completion items when editing debug.json
- debugger beta: Breaking change, debug.json schema now relays on what
DAP you have selected instead of always having the same based values.

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
Anthony Eid 2025-05-22 05:48:26 -04:00 committed by GitHub
parent 0d7f4842f3
commit 1c9b818342
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 2357 additions and 740 deletions

View file

@ -2,13 +2,13 @@
{ {
"label": "Debug Zed (CodeLLDB)", "label": "Debug Zed (CodeLLDB)",
"adapter": "CodeLLDB", "adapter": "CodeLLDB",
"program": "target/debug/zed", "program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch" "request": "launch"
}, },
{ {
"label": "Debug Zed (GDB)", "label": "Debug Zed (GDB)",
"adapter": "GDB", "adapter": "GDB",
"program": "target/debug/zed", "program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch", "request": "launch",
"initialize_args": { "initialize_args": {
"stopAtBeginningOfMainSubprogram": true "stopAtBeginningOfMainSubprogram": true

5
Cargo.lock generated
View file

@ -3002,6 +3002,7 @@ dependencies = [
"context_server", "context_server",
"ctor", "ctor",
"dap", "dap",
"dap_adapters",
"dashmap 6.1.0", "dashmap 6.1.0",
"debugger_ui", "debugger_ui",
"derive_more", "derive_more",
@ -4180,6 +4181,8 @@ dependencies = [
"dap", "dap",
"extension", "extension",
"gpui", "gpui",
"serde_json",
"task",
"workspace-hack", "workspace-hack",
] ]
@ -8891,6 +8894,7 @@ dependencies = [
"async-tar", "async-tar",
"async-trait", "async-trait",
"collections", "collections",
"dap",
"futures 0.3.31", "futures 0.3.31",
"gpui", "gpui",
"http_client", "http_client",
@ -17063,6 +17067,7 @@ dependencies = [
"rust-embed", "rust-embed",
"serde", "serde",
"serde_json", "serde_json",
"serde_json_lenient",
"smol", "smol",
"take-until", "take-until",
"tempfile", "tempfile",

View file

@ -92,6 +92,7 @@ command_palette_hooks.workspace = true
context_server.workspace = true context_server.workspace = true
ctor.workspace = true ctor.workspace = true
dap = { workspace = true, features = ["test-support"] } dap = { workspace = true, features = ["test-support"] }
dap_adapters = { workspace = true, features = ["test-support"] }
debugger_ui = { workspace = true, features = ["test-support"] } debugger_ui = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true env_logger.workspace = true

View file

@ -592,9 +592,11 @@ async fn test_remote_server_debugger(
if std::env::var("RUST_LOG").is_ok() { if std::env::var("RUST_LOG").is_ok() {
env_logger::try_init().ok(); env_logger::try_init().ok();
} }
dap_adapters::init(cx);
}); });
server_cx.update(|cx| { server_cx.update(|cx| {
release_channel::init(SemanticVersion::default(), cx); release_channel::init(SemanticVersion::default(), cx);
dap_adapters::init(cx);
}); });
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx); let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor()); let remote_fs = FakeFs::new(server_cx.executor());

View file

@ -1,5 +1,5 @@
use ::fs::Fs; use ::fs::Fs;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result, anyhow};
use async_compression::futures::bufread::GzipDecoder; use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive; use async_tar::Archive;
use async_trait::async_trait; use async_trait::async_trait;
@ -22,7 +22,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
}; };
use task::{AttachRequest, DebugRequest, DebugScenario, LaunchRequest, TcpArgumentsTemplate}; use task::{DebugScenario, TcpArgumentsTemplate, ZedDebugConfig};
use util::archive::extract_zip; use util::archive::extract_zip;
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -131,13 +131,12 @@ impl TcpArguments {
derive(serde::Deserialize, serde::Serialize) derive(serde::Deserialize, serde::Serialize)
)] )]
pub struct DebugTaskDefinition { pub struct DebugTaskDefinition {
/// The name of this debug task
pub label: SharedString, pub label: SharedString,
/// The debug adapter to use
pub adapter: DebugAdapterName, pub adapter: DebugAdapterName,
pub request: DebugRequest, /// The configuration to send to the debug adapter
/// Additional initialization arguments to be sent on DAP initialization pub config: serde_json::Value,
pub initialize_args: Option<serde_json::Value>,
/// Whether to tell the debug adapter to stop on entry
pub stop_on_entry: Option<bool>,
/// Optional TCP connection information /// Optional TCP connection information
/// ///
/// If provided, this will be used to connect to the debug adapter instead of /// If provided, this will be used to connect to the debug adapter instead of
@ -147,86 +146,34 @@ pub struct DebugTaskDefinition {
} }
impl DebugTaskDefinition { impl DebugTaskDefinition {
pub fn cwd(&self) -> Option<&Path> {
if let DebugRequest::Launch(config) = &self.request {
config.cwd.as_ref().map(Path::new)
} else {
None
}
}
pub fn to_scenario(&self) -> DebugScenario { pub fn to_scenario(&self) -> DebugScenario {
DebugScenario { DebugScenario {
label: self.label.clone(), label: self.label.clone(),
adapter: self.adapter.clone().into(), adapter: self.adapter.clone().into(),
build: None, build: None,
request: Some(self.request.clone()),
stop_on_entry: self.stop_on_entry,
tcp_connection: self.tcp_connection.clone(), tcp_connection: self.tcp_connection.clone(),
initialize_args: self.initialize_args.clone(), config: self.config.clone(),
} }
} }
pub fn to_proto(&self) -> proto::DebugTaskDefinition { pub fn to_proto(&self) -> proto::DebugTaskDefinition {
proto::DebugTaskDefinition { proto::DebugTaskDefinition {
adapter: self.adapter.to_string(), label: self.label.clone().into(),
request: Some(match &self.request { config: self.config.to_string(),
DebugRequest::Launch(config) => { tcp_connection: self.tcp_connection.clone().map(|v| v.to_proto()),
proto::debug_task_definition::Request::DebugLaunchRequest( adapter: self.adapter.clone().0.into(),
proto::DebugLaunchRequest {
program: config.program.clone(),
cwd: config.cwd.as_ref().map(|c| c.to_string_lossy().to_string()),
args: config.args.clone(),
env: config
.env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
},
)
}
DebugRequest::Attach(attach_request) => {
proto::debug_task_definition::Request::DebugAttachRequest(
proto::DebugAttachRequest {
process_id: attach_request.process_id.unwrap_or_default(),
},
)
}
}),
label: self.label.to_string(),
initialize_args: self.initialize_args.as_ref().map(|v| v.to_string()),
tcp_connection: self.tcp_connection.as_ref().map(|t| t.to_proto()),
stop_on_entry: self.stop_on_entry,
} }
} }
pub fn from_proto(proto: proto::DebugTaskDefinition) -> Result<Self> { pub fn from_proto(proto: proto::DebugTaskDefinition) -> Result<Self> {
let request = proto.request.context("request is required")?;
Ok(Self { Ok(Self {
label: proto.label.into(), label: proto.label.into(),
initialize_args: proto.initialize_args.map(|v| v.into()), config: serde_json::from_str(&proto.config)?,
tcp_connection: proto tcp_connection: proto
.tcp_connection .tcp_connection
.map(TcpArgumentsTemplate::from_proto) .map(TcpArgumentsTemplate::from_proto)
.transpose()?, .transpose()?,
stop_on_entry: proto.stop_on_entry,
adapter: DebugAdapterName(proto.adapter.into()), adapter: DebugAdapterName(proto.adapter.into()),
request: match request {
proto::debug_task_definition::Request::DebugAttachRequest(config) => {
DebugRequest::Attach(AttachRequest {
process_id: Some(config.process_id),
})
}
proto::debug_task_definition::Request::DebugLaunchRequest(config) => {
DebugRequest::Launch(LaunchRequest {
program: config.program,
cwd: config.cwd.map(|cwd| cwd.into()),
args: config.args,
env: Default::default(),
})
}
},
}) })
} }
} }
@ -407,6 +354,8 @@ pub async fn fetch_latest_adapter_version_from_github(
pub trait DebugAdapter: 'static + Send + Sync { pub trait DebugAdapter: 'static + Send + Sync {
fn name(&self) -> DebugAdapterName; fn name(&self) -> DebugAdapterName;
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario>;
async fn get_binary( async fn get_binary(
&self, &self,
delegate: &Arc<dyn DapDelegate>, delegate: &Arc<dyn DapDelegate>,
@ -419,6 +368,25 @@ pub trait DebugAdapter: 'static + Send + Sync {
fn adapter_language_name(&self) -> Option<LanguageName> { fn adapter_language_name(&self) -> Option<LanguageName> {
None None
} }
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config.as_object().context("Config isn't an object")?;
let request_variant = map["request"]
.as_str()
.ok_or_else(|| anyhow!("request is not valid"))?;
match request_variant {
"launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
"attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
_ => Err(anyhow!("request must be either 'launch' or 'attach'")),
}
}
fn dap_schema(&self) -> serde_json::Value;
} }
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
@ -432,29 +400,29 @@ impl FakeAdapter {
Self {} Self {}
} }
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments { fn request_args(
&self,
task_definition: &DebugTaskDefinition,
) -> StartDebuggingRequestArguments {
use serde_json::json; use serde_json::json;
use task::DebugRequest;
let obj = task_definition.config.as_object().unwrap();
let request_variant = obj["request"].as_str().unwrap();
let value = json!({ let value = json!({
"request": match config.request { "request": request_variant,
DebugRequest::Launch(_) => "launch", "process_id": obj.get("process_id"),
DebugRequest::Attach(_) => "attach", "raw_request": serde_json::to_value(task_definition).unwrap()
},
"process_id": if let DebugRequest::Attach(attach_config) = &config.request {
attach_config.process_id
} else {
None
},
"raw_request": serde_json::to_value(config).unwrap()
}); });
let request = match config.request {
DebugRequest::Launch(_) => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
DebugRequest::Attach(_) => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
};
StartDebuggingRequestArguments { StartDebuggingRequestArguments {
configuration: value, configuration: value,
request, request: match request_variant {
"launch" => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
"attach" => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
_ => unreachable!("Wrong fake adapter input for request field"),
},
} }
} }
} }
@ -466,6 +434,41 @@ impl DebugAdapter for FakeAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into()) DebugAdapterName(Self::ADAPTER_NAME.into())
} }
fn dap_schema(&self) -> serde_json::Value {
serde_json::Value::Null
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let request = config.as_object().unwrap()["request"].as_str().unwrap();
let request = match request {
"launch" => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
"attach" => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
_ => unreachable!("Wrong fake adapter input for request field"),
};
Ok(request)
}
fn adapter_language_name(&self) -> Option<LanguageName> {
None
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let config = serde_json::to_value(zed_scenario.request).unwrap();
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
build: None,
config,
tcp_connection: None,
})
}
async fn get_binary( async fn get_binary(
&self, &self,
_: &Arc<dyn DapDelegate>, _: &Arc<dyn DapDelegate>,
@ -479,7 +482,7 @@ impl DebugAdapter for FakeAdapter {
connection: None, connection: None,
envs: HashMap::default(), envs: HashMap::default(),
cwd: None, cwd: None,
request_args: self.request_args(config), request_args: self.request_args(&config),
}) })
} }
} }

View file

@ -4,7 +4,9 @@ use collections::FxHashMap;
use gpui::{App, Global, SharedString}; use gpui::{App, Global, SharedString};
use language::LanguageName; use language::LanguageName;
use parking_lot::RwLock; use parking_lot::RwLock;
use task::{DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate}; use task::{
AdapterSchema, AdapterSchemas, DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate,
};
use crate::{ use crate::{
adapters::{DebugAdapter, DebugAdapterName}, adapters::{DebugAdapter, DebugAdapterName},
@ -41,14 +43,7 @@ impl Global for DapRegistry {}
impl DapRegistry { impl DapRegistry {
pub fn global(cx: &mut App) -> &mut Self { pub fn global(cx: &mut App) -> &mut Self {
let ret = cx.default_global::<Self>(); cx.default_global::<Self>()
#[cfg(any(test, feature = "test-support"))]
if ret.adapter(crate::FakeAdapter::ADAPTER_NAME).is_none() {
ret.add_adapter(Arc::new(crate::FakeAdapter::new()));
}
ret
} }
pub fn add_adapter(&self, adapter: Arc<dyn DebugAdapter>) { pub fn add_adapter(&self, adapter: Arc<dyn DebugAdapter>) {
@ -69,6 +64,19 @@ impl DapRegistry {
); );
} }
pub fn adapters_schema(&self) -> task::AdapterSchemas {
let mut schemas = AdapterSchemas(vec![]);
for (name, adapter) in self.0.read().adapters.iter() {
schemas.0.push(AdapterSchema {
adapter: name.clone().into(),
schema: adapter.dap_schema(),
});
}
schemas
}
pub fn add_inline_value_provider( pub fn add_inline_value_provider(
&self, &self,
language: String, language: String,

View file

@ -1,11 +1,15 @@
use std::{collections::HashMap, path::PathBuf, sync::OnceLock}; use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result, anyhow};
use async_trait::async_trait; use async_trait::async_trait;
use dap::adapters::{DebugTaskDefinition, latest_github_release}; use dap::{
StartDebuggingRequestArgumentsRequest,
adapters::{DebugTaskDefinition, latest_github_release},
};
use futures::StreamExt; use futures::StreamExt;
use gpui::AsyncApp; use gpui::AsyncApp;
use task::DebugRequest; use serde_json::Value;
use task::{DebugRequest, DebugScenario, ZedDebugConfig};
use util::fs::remove_matching; use util::fs::remove_matching;
use crate::*; use crate::*;
@ -18,45 +22,27 @@ pub(crate) struct CodeLldbDebugAdapter {
impl CodeLldbDebugAdapter { impl CodeLldbDebugAdapter {
const ADAPTER_NAME: &'static str = "CodeLLDB"; const ADAPTER_NAME: &'static str = "CodeLLDB";
fn request_args(&self, config: &DebugTaskDefinition) -> dap::StartDebuggingRequestArguments { fn request_args(
let mut configuration = json!({ &self,
"request": match config.request { task_definition: &DebugTaskDefinition,
DebugRequest::Launch(_) => "launch", ) -> Result<dap::StartDebuggingRequestArguments> {
DebugRequest::Attach(_) => "attach",
},
});
let map = configuration.as_object_mut().unwrap();
// CodeLLDB uses `name` for a terminal label. // CodeLLDB uses `name` for a terminal label.
map.insert( let mut configuration = task_definition.config.clone();
"name".into(),
Value::String(String::from(config.label.as_ref())),
);
let request = config.request.to_dap();
match &config.request {
DebugRequest::Attach(attach) => {
map.insert("pid".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() { configuration
map.insert("args".into(), launch.args.clone().into()); .as_object_mut()
} .context("CodeLLDB is not a valid json object")?
if !launch.env.is_empty() { .insert(
map.insert("env".into(), launch.env_json()); "name".into(),
} Value::String(String::from(task_definition.label.as_ref())),
if let Some(stop_on_entry) = config.stop_on_entry { );
map.insert("stopOnEntry".into(), stop_on_entry.into());
} let request = self.validate_config(&configuration)?;
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into()); Ok(dap::StartDebuggingRequestArguments {
}
}
}
dap::StartDebuggingRequestArguments {
request, request,
configuration, configuration,
} })
} }
async fn fetch_latest_adapter_version( async fn fetch_latest_adapter_version(
@ -103,6 +89,286 @@ impl DebugAdapter for CodeLldbDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into()) DebugAdapterName(Self::ADAPTER_NAME.into())
} }
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config
.as_object()
.ok_or_else(|| anyhow!("Config isn't an object"))?;
let request_variant = map
.get("request")
.and_then(|r| r.as_str())
.ok_or_else(|| anyhow!("request field is required and must be a string"))?;
match request_variant {
"launch" => {
// For launch, verify that one of the required configs exists
if !(map.contains_key("program")
|| map.contains_key("targetCreateCommands")
|| map.contains_key("cargo"))
{
return Err(anyhow!(
"launch request requires either 'program', 'targetCreateCommands', or 'cargo' field"
));
}
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
"attach" => {
// For attach, verify that either pid or program exists
if !(map.contains_key("pid") || map.contains_key("program")) {
return Err(anyhow!(
"attach request requires either 'pid' or 'program' field"
));
}
Ok(StartDebuggingRequestArgumentsRequest::Attach)
}
_ => Err(anyhow!(
"request must be either 'launch' or 'attach', got '{}'",
request_variant
)),
}
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut configuration = json!({
"request": match zed_scenario.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
});
let map = configuration.as_object_mut().unwrap();
// CodeLLDB uses `name` for a terminal label.
map.insert(
"name".into(),
Value::String(String::from(zed_scenario.label.as_ref())),
);
match &zed_scenario.request {
DebugRequest::Attach(attach) => {
map.insert("pid".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
map.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
config: configuration,
build: None,
tcp_connection: None,
})
}
fn dap_schema(&self) -> serde_json::Value {
json!({
"properties": {
"request": {
"type": "string",
"enum": ["attach", "launch"],
"description": "Debug adapter request type"
},
"program": {
"type": "string",
"description": "Path to the program to debug or attach to"
},
"args": {
"type": ["array", "string"],
"description": "Program arguments"
},
"cwd": {
"type": "string",
"description": "Program working directory"
},
"env": {
"type": "object",
"description": "Additional environment variables",
"patternProperties": {
".*": {
"type": "string"
}
}
},
"envFile": {
"type": "string",
"description": "File to read the environment variables from"
},
"stdio": {
"type": ["null", "string", "array", "object"],
"description": "Destination for stdio streams: null = send to debugger console or a terminal, \"<path>\" = attach to a file/tty/fifo"
},
"terminal": {
"type": "string",
"enum": ["integrated", "console"],
"description": "Terminal type to use",
"default": "integrated"
},
"console": {
"type": "string",
"enum": ["integratedTerminal", "internalConsole"],
"description": "Terminal type to use (compatibility alias of 'terminal')"
},
"stopOnEntry": {
"type": "boolean",
"description": "Automatically stop debuggee after launch",
"default": false
},
"initCommands": {
"type": "array",
"description": "Initialization commands executed upon debugger startup",
"items": {
"type": "string"
}
},
"targetCreateCommands": {
"type": "array",
"description": "Commands that create the debug target",
"items": {
"type": "string"
}
},
"preRunCommands": {
"type": "array",
"description": "Commands executed just before the program is launched",
"items": {
"type": "string"
}
},
"processCreateCommands": {
"type": "array",
"description": "Commands that create the debuggee process",
"items": {
"type": "string"
}
},
"postRunCommands": {
"type": "array",
"description": "Commands executed just after the program has been launched",
"items": {
"type": "string"
}
},
"preTerminateCommands": {
"type": "array",
"description": "Commands executed just before the debuggee is terminated or disconnected from",
"items": {
"type": "string"
}
},
"exitCommands": {
"type": "array",
"description": "Commands executed at the end of debugging session",
"items": {
"type": "string"
}
},
"expressions": {
"type": "string",
"enum": ["simple", "python", "native"],
"description": "The default evaluator type used for expressions"
},
"sourceMap": {
"type": "object",
"description": "Source path remapping between the build machine and the local machine",
"patternProperties": {
".*": {
"type": ["string", "null"]
}
}
},
"relativePathBase": {
"type": "string",
"description": "Base directory used for resolution of relative source paths. Defaults to the workspace folder"
},
"sourceLanguages": {
"type": "array",
"description": "A list of source languages to enable language-specific features for",
"items": {
"type": "string"
}
},
"reverseDebugging": {
"type": "boolean",
"description": "Enable reverse debugging",
"default": false
},
"breakpointMode": {
"type": "string",
"enum": ["path", "file"],
"description": "Specifies how source breakpoints should be set"
},
"pid": {
"type": ["integer", "string"],
"description": "Process id to attach to"
},
"waitFor": {
"type": "boolean",
"description": "Wait for the process to launch (MacOS only)",
"default": false
}
},
"required": ["request"],
"allOf": [
{
"if": {
"properties": {
"request": {
"enum": ["launch"]
}
}
},
"then": {
"oneOf": [
{
"required": ["program"]
},
{
"required": ["targetCreateCommands"]
},
{
"required": ["cargo"]
}
]
}
},
{
"if": {
"properties": {
"request": {
"enum": ["attach"]
}
}
},
"then": {
"oneOf": [
{
"required": ["pid"]
},
{
"required": ["program"]
}
]
}
}
]
})
}
async fn get_binary( async fn get_binary(
&self, &self,
delegate: &Arc<dyn DapDelegate>, delegate: &Arc<dyn DapDelegate>,
@ -175,7 +441,7 @@ impl DebugAdapter for CodeLldbDebugAdapter {
"--settings".into(), "--settings".into(),
json!({"sourceLanguages": ["cpp", "rust"]}).to_string(), json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
], ],
request_args: self.request_args(config), request_args: self.request_args(&config)?,
envs: HashMap::default(), envs: HashMap::default(),
connection: None, connection: None,
}) })

View file

@ -12,7 +12,7 @@ use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use codelldb::CodeLldbDebugAdapter; use codelldb::CodeLldbDebugAdapter;
use dap::{ use dap::{
DapRegistry, DebugRequest, DapRegistry,
adapters::{ adapters::{
self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
GithubRepo, GithubRepo,
@ -27,7 +27,8 @@ use javascript::JsDebugAdapter;
use php::PhpDebugAdapter; use php::PhpDebugAdapter;
use python::PythonDebugAdapter; use python::PythonDebugAdapter;
use ruby::RubyDebugAdapter; use ruby::RubyDebugAdapter;
use serde_json::{Value, json}; use serde_json::json;
use task::{DebugScenario, ZedDebugConfig};
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
cx.update_default_global(|registry: &mut DapRegistry, _cx| { cx.update_default_global(|registry: &mut DapRegistry, _cx| {
@ -39,21 +40,13 @@ pub fn init(cx: &mut App) {
registry.add_adapter(Arc::from(GoDebugAdapter)); registry.add_adapter(Arc::from(GoDebugAdapter));
registry.add_adapter(Arc::from(GdbDebugAdapter)); registry.add_adapter(Arc::from(GdbDebugAdapter));
#[cfg(any(test, feature = "test-support"))]
{
registry.add_adapter(Arc::from(dap::FakeAdapter {}));
}
registry.add_inline_value_provider("Rust".to_string(), Arc::from(RustInlineValueProvider)); registry.add_inline_value_provider("Rust".to_string(), Arc::from(RustInlineValueProvider));
registry registry
.add_inline_value_provider("Python".to_string(), Arc::from(PythonInlineValueProvider)); .add_inline_value_provider("Python".to_string(), Arc::from(PythonInlineValueProvider));
}) })
} }
trait ToDap {
fn to_dap(&self) -> dap::StartDebuggingRequestArgumentsRequest;
}
impl ToDap for DebugRequest {
fn to_dap(&self) -> dap::StartDebuggingRequestArgumentsRequest {
match self {
Self::Launch(_) => dap::StartDebuggingRequestArgumentsRequest::Launch,
Self::Attach(_) => dap::StartDebuggingRequestArgumentsRequest::Attach,
}
}
}

View file

@ -4,7 +4,7 @@ use anyhow::{Context as _, Result, bail};
use async_trait::async_trait; use async_trait::async_trait;
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::AsyncApp; use gpui::AsyncApp;
use task::DebugRequest; use task::{DebugScenario, ZedDebugConfig};
use crate::*; use crate::*;
@ -13,48 +13,6 @@ pub(crate) struct GdbDebugAdapter;
impl GdbDebugAdapter { impl GdbDebugAdapter {
const ADAPTER_NAME: &'static str = "GDB"; const ADAPTER_NAME: &'static str = "GDB";
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
let mut args = json!({
"request": match config.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
});
let map = args.as_object_mut().unwrap();
match &config.request {
DebugRequest::Attach(attach) => {
map.insert("pid".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
map.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = config.stop_on_entry {
map.insert(
"stopAtBeginningOfMainSubprogram".into(),
stop_on_entry.into(),
);
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
StartDebuggingRequestArguments {
configuration: args,
request: config.request.to_dap(),
}
}
} }
#[async_trait(?Send)] #[async_trait(?Send)]
@ -63,6 +21,137 @@ impl DebugAdapter for GdbDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into()) DebugAdapterName(Self::ADAPTER_NAME.into())
} }
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut obj = serde_json::Map::default();
match &zed_scenario.request {
dap::DebugRequest::Attach(attach) => {
obj.insert("pid".into(), attach.process_id.into());
}
dap::DebugRequest::Launch(launch) => {
obj.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
obj.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
obj.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
obj.insert(
"stopAtBeginningOfMainSubprogram".into(),
stop_on_entry.into(),
);
}
if let Some(cwd) = launch.cwd.as_ref() {
obj.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
build: None,
config: serde_json::Value::Object(obj),
tcp_connection: None,
})
}
fn dap_schema(&self) -> serde_json::Value {
json!({
"oneOf": [
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "Request to launch a new process"
}
}
},
{
"type": "object",
"properties": {
"program": {
"type": "string",
"description": "The program to debug. This corresponds to the GDB 'file' command."
},
"args": {
"type": "array",
"items": {
"type": "string"
},
"description": "Command line arguments passed to the program. These strings are provided as command-line arguments to the inferior.",
"default": []
},
"cwd": {
"type": "string",
"description": "Working directory for the debugged program. GDB will change its working directory to this directory."
},
"env": {
"type": "object",
"description": "Environment variables for the debugged program. Each key is the name of an environment variable; each value is the value of that variable."
},
"stopAtBeginningOfMainSubprogram": {
"type": "boolean",
"description": "When true, GDB will set a temporary breakpoint at the program's main procedure, like the 'start' command.",
"default": false
},
"stopOnEntry": {
"type": "boolean",
"description": "When true, GDB will set a temporary breakpoint at the program's first instruction, like the 'starti' command.",
"default": false
}
},
"required": ["program"]
}
]
},
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["attach"],
"description": "Request to attach to an existing process"
}
}
},
{
"type": "object",
"properties": {
"pid": {
"type": "number",
"description": "The process ID to which GDB should attach."
},
"program": {
"type": "string",
"description": "The program to debug (optional). This corresponds to the GDB 'file' command. In many cases, GDB can determine which program is running automatically."
},
"target": {
"type": "string",
"description": "The target to which GDB should connect. This is passed to the 'target remote' command."
}
},
"required": ["pid"]
}
]
}
]
})
}
async fn get_binary( async fn get_binary(
&self, &self,
delegate: &Arc<dyn DapDelegate>, delegate: &Arc<dyn DapDelegate>,
@ -86,13 +175,18 @@ impl DebugAdapter for GdbDebugAdapter {
let gdb_path = user_setting_path.unwrap_or(gdb_path?); let gdb_path = user_setting_path.unwrap_or(gdb_path?);
let request_args = StartDebuggingRequestArguments {
request: self.validate_config(&config.config)?,
configuration: config.config.clone(),
};
Ok(DebugAdapterBinary { Ok(DebugAdapterBinary {
command: gdb_path, command: gdb_path,
arguments: vec!["-i=dap".into()], arguments: vec!["-i=dap".into()],
envs: HashMap::default(), envs: HashMap::default(),
cwd: None, cwd: None,
connection: None, connection: None,
request_args: self.request_args(config), request_args,
}) })
} }
} }

View file

@ -1,5 +1,9 @@
use anyhow::Context as _; use anyhow::{Context as _, anyhow};
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; use dap::{
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::DebugTaskDefinition,
};
use gpui::{AsyncApp, SharedString}; use gpui::{AsyncApp, SharedString};
use language::LanguageName; use language::LanguageName;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf}; use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
@ -11,8 +15,291 @@ pub(crate) struct GoDebugAdapter;
impl GoDebugAdapter { impl GoDebugAdapter {
const ADAPTER_NAME: &'static str = "Delve"; const ADAPTER_NAME: &'static str = "Delve";
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments { }
let mut args = match &config.request {
#[async_trait(?Send)]
impl DebugAdapter for GoDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("Go").into())
}
fn dap_schema(&self) -> serde_json::Value {
// Create common properties shared between launch and attach
let common_properties = json!({
"debugAdapter": {
"enum": ["legacy", "dlv-dap"],
"description": "Select which debug adapter to use with this configuration.",
"default": "dlv-dap"
},
"stopOnEntry": {
"type": "boolean",
"description": "Automatically stop program after launch or attach.",
"default": false
},
"showLog": {
"type": "boolean",
"description": "Show log output from the delve debugger. Maps to dlv's `--log` flag.",
"default": false
},
"cwd": {
"type": "string",
"description": "Workspace relative or absolute path to the working directory of the program being debugged.",
"default": "${ZED_WORKTREE_ROOT}"
},
"dlvFlags": {
"type": "array",
"description": "Extra flags for `dlv`. See `dlv help` for the full list of supported flags.",
"items": {
"type": "string"
},
"default": []
},
"port": {
"type": "number",
"description": "Debug server port. For remote configurations, this is where to connect.",
"default": 2345
},
"host": {
"type": "string",
"description": "Debug server host. For remote configurations, this is where to connect.",
"default": "127.0.0.1"
},
"substitutePath": {
"type": "array",
"items": {
"type": "object",
"properties": {
"from": {
"type": "string",
"description": "The absolute local path to be replaced."
},
"to": {
"type": "string",
"description": "The absolute remote path to replace with."
}
}
},
"description": "Mappings from local to remote paths for debugging.",
"default": []
},
"trace": {
"type": "string",
"enum": ["verbose", "trace", "log", "info", "warn", "error"],
"default": "error",
"description": "Debug logging level."
},
"backend": {
"type": "string",
"enum": ["default", "native", "lldb", "rr"],
"description": "Backend used by delve. Maps to `dlv`'s `--backend` flag."
},
"logOutput": {
"type": "string",
"enum": ["debugger", "gdbwire", "lldbout", "debuglineerr", "rpc", "dap"],
"description": "Components that should produce debug output.",
"default": "debugger"
},
"logDest": {
"type": "string",
"description": "Log destination for delve."
},
"stackTraceDepth": {
"type": "number",
"description": "Maximum depth of stack traces.",
"default": 50
},
"showGlobalVariables": {
"type": "boolean",
"default": false,
"description": "Show global package variables in variables pane."
},
"showRegisters": {
"type": "boolean",
"default": false,
"description": "Show register variables in variables pane."
},
"hideSystemGoroutines": {
"type": "boolean",
"default": false,
"description": "Hide system goroutines from call stack view."
},
"console": {
"default": "internalConsole",
"description": "Where to launch the debugger.",
"enum": ["internalConsole", "integratedTerminal"]
},
"asRoot": {
"default": false,
"description": "Debug with elevated permissions (on Unix).",
"type": "boolean"
}
});
// Create launch-specific properties
let launch_properties = json!({
"program": {
"type": "string",
"description": "Path to the program folder or file to debug.",
"default": "${ZED_WORKTREE_ROOT}"
},
"args": {
"type": ["array", "string"],
"description": "Command line arguments for the program.",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"description": "Environment variables for the debugged program.",
"default": {}
},
"envFile": {
"type": ["string", "array"],
"items": {
"type": "string"
},
"description": "Path(s) to files with environment variables.",
"default": ""
},
"buildFlags": {
"type": ["string", "array"],
"items": {
"type": "string"
},
"description": "Flags for the Go compiler.",
"default": []
},
"output": {
"type": "string",
"description": "Output path for the binary.",
"default": "debug"
},
"mode": {
"enum": [ "debug", "test", "exec", "replay", "core"],
"description": "Debug mode for launch configuration.",
},
"traceDirPath": {
"type": "string",
"description": "Directory for record trace (for 'replay' mode).",
"default": ""
},
"coreFilePath": {
"type": "string",
"description": "Path to core dump file (for 'core' mode).",
"default": ""
}
});
// Create attach-specific properties
let attach_properties = json!({
"processId": {
"anyOf": [
{
"enum": ["${command:pickProcess}", "${command:pickGoProcess}"],
"description": "Use process picker to select a process."
},
{
"type": "string",
"description": "Process name to attach to."
},
{
"type": "number",
"description": "Process ID to attach to."
}
],
"default": 0
},
"mode": {
"enum": ["local", "remote"],
"description": "Local or remote debugging.",
"default": "local"
},
"remotePath": {
"type": "string",
"description": "Path to source on remote machine.",
"markdownDeprecationMessage": "Use `substitutePath` instead.",
"default": ""
}
});
// Create the final schema
json!({
"oneOf": [
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "Request to launch a new process"
}
}
},
{
"type": "object",
"properties": common_properties
},
{
"type": "object",
"required": ["program", "mode"],
"properties": launch_properties
}
]
},
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["attach"],
"description": "Request to attach to an existing process"
}
}
},
{
"type": "object",
"properties": common_properties
},
{
"type": "object",
"required": ["processId", "mode"],
"properties": attach_properties
}
]
}
]
})
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config.as_object().context("Config isn't an object")?;
let request_variant = map["request"].as_str().context("request is not valid")?;
match request_variant {
"launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
"attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
_ => Err(anyhow!("request must be either 'launch' or 'attach'")),
}
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut args = match &zed_scenario.request {
dap::DebugRequest::Attach(attach_config) => { dap::DebugRequest::Attach(attach_config) => {
json!({ json!({
"processId": attach_config.process_id, "processId": attach_config.process_id,
@ -28,31 +315,23 @@ impl GoDebugAdapter {
let map = args.as_object_mut().unwrap(); let map = args.as_object_mut().unwrap();
if let Some(stop_on_entry) = config.stop_on_entry { if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into()); map.insert("stopOnEntry".into(), stop_on_entry.into());
} }
StartDebuggingRequestArguments { Ok(DebugScenario {
configuration: args, adapter: zed_scenario.adapter,
request: config.request.to_dap(), label: zed_scenario.label,
} build: None,
} config: args,
} tcp_connection: None,
})
#[async_trait(?Send)]
impl DebugAdapter for GoDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("Go").into())
} }
async fn get_binary( async fn get_binary(
&self, &self,
delegate: &Arc<dyn DapDelegate>, delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition, task_definition: &DebugTaskDefinition,
_user_installed_path: Option<PathBuf>, _user_installed_path: Option<PathBuf>,
_cx: &mut AsyncApp, _cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> { ) -> Result<DebugAdapterBinary> {
@ -62,7 +341,7 @@ impl DebugAdapter for GoDebugAdapter {
.and_then(|p| p.to_str().map(|p| p.to_string())) .and_then(|p| p.to_str().map(|p| p.to_string()))
.context("Dlv not found in path")?; .context("Dlv not found in path")?;
let tcp_connection = config.tcp_connection.clone().unwrap_or_default(); let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary { Ok(DebugAdapterBinary {
@ -75,7 +354,10 @@ impl DebugAdapter for GoDebugAdapter {
port, port,
timeout, timeout,
}), }),
request_args: self.request_args(config), request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
request: self.validate_config(&task_definition.config)?,
},
}) })
} }
} }

View file

@ -1,6 +1,9 @@
use adapters::latest_github_release; use adapters::latest_github_release;
use anyhow::Context as _; use anyhow::{Context as _, anyhow};
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; use dap::{
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::DebugTaskDefinition,
};
use gpui::AsyncApp; use gpui::AsyncApp;
use std::{collections::HashMap, path::PathBuf, sync::OnceLock}; use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use task::DebugRequest; use task::DebugRequest;
@ -18,43 +21,6 @@ impl JsDebugAdapter {
const ADAPTER_NPM_NAME: &'static str = "vscode-js-debug"; const ADAPTER_NPM_NAME: &'static str = "vscode-js-debug";
const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js"; const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js";
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
let mut args = json!({
"type": "pwa-node",
"request": match config.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
});
let map = args.as_object_mut().unwrap();
match &config.request {
DebugRequest::Attach(attach) => {
map.insert("processId".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
map.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = config.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
StartDebuggingRequestArguments {
configuration: args,
request: config.request.to_dap(),
}
}
async fn fetch_latest_adapter_version( async fn fetch_latest_adapter_version(
&self, &self,
delegate: &Arc<dyn DapDelegate>, delegate: &Arc<dyn DapDelegate>,
@ -84,7 +50,7 @@ impl JsDebugAdapter {
async fn get_installed_binary( async fn get_installed_binary(
&self, &self,
delegate: &Arc<dyn DapDelegate>, delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition, task_definition: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>, user_installed_path: Option<PathBuf>,
_: &mut AsyncApp, _: &mut AsyncApp,
) -> Result<DebugAdapterBinary> { ) -> Result<DebugAdapterBinary> {
@ -102,7 +68,7 @@ impl JsDebugAdapter {
.context("Couldn't find JavaScript dap directory")? .context("Couldn't find JavaScript dap directory")?
}; };
let tcp_connection = config.tcp_connection.clone().unwrap_or_default(); let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary { Ok(DebugAdapterBinary {
@ -127,7 +93,10 @@ impl JsDebugAdapter {
port, port,
timeout, timeout,
}), }),
request_args: self.request_args(config), request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
request: self.validate_config(&task_definition.config)?,
},
}) })
} }
} }
@ -138,6 +107,322 @@ impl DebugAdapter for JsDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into()) DebugAdapterName(Self::ADAPTER_NAME.into())
} }
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<dap::StartDebuggingRequestArgumentsRequest> {
match config.get("request") {
Some(val) if val == "launch" => {
if config.get("program").is_none() {
return Err(anyhow!("program is required"));
}
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
Some(val) if val == "attach" => {
if !config.get("processId").is_some_and(|val| val.is_u64()) {
return Err(anyhow!("processId must be a number"));
}
Ok(StartDebuggingRequestArgumentsRequest::Attach)
}
_ => Err(anyhow!("missing or invalid request field in config")),
}
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut args = json!({
"type": "pwa-node",
"request": match zed_scenario.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
});
let map = args.as_object_mut().unwrap();
match &zed_scenario.request {
DebugRequest::Attach(attach) => {
map.insert("processId".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
map.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
};
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
build: None,
config: args,
tcp_connection: None,
})
}
fn dap_schema(&self) -> serde_json::Value {
json!({
"oneOf": [
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "Request to launch a new process"
}
}
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "edge", "pwa-edge"],
"description": "The type of debug session",
"default": "pwa-node"
},
"program": {
"type": "string",
"description": "Path to the program or file to debug"
},
"cwd": {
"type": "string",
"description": "Absolute path to the working directory of the program being debugged"
},
"args": {
"type": ["array", "string"],
"description": "Command line arguments passed to the program",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"description": "Environment variables passed to the program",
"default": {}
},
"envFile": {
"type": ["string", "array"],
"description": "Path to a file containing environment variable definitions",
"items": {
"type": "string"
}
},
"stopOnEntry": {
"type": "boolean",
"description": "Automatically stop program after launch",
"default": false
},
"runtimeExecutable": {
"type": ["string", "null"],
"description": "Runtime to use, an absolute path or the name of a runtime available on PATH",
"default": "node"
},
"runtimeArgs": {
"type": ["array", "null"],
"description": "Arguments passed to the runtime executable",
"items": {
"type": "string"
},
"default": []
},
"outFiles": {
"type": "array",
"description": "Glob patterns for locating generated JavaScript files",
"items": {
"type": "string"
},
"default": ["${ZED_WORKTREE_ROOT}/**/*.js", "!**/node_modules/**"]
},
"sourceMaps": {
"type": "boolean",
"description": "Use JavaScript source maps if they exist",
"default": true
},
"sourceMapPathOverrides": {
"type": "object",
"description": "Rewrites the locations of source files from what the sourcemap says to their locations on disk",
"default": {}
},
"restart": {
"type": ["boolean", "object"],
"description": "Restart session after Node.js has terminated",
"default": false
},
"trace": {
"type": ["boolean", "object"],
"description": "Enables logging of the Debug Adapter",
"default": false
},
"console": {
"type": "string",
"enum": ["internalConsole", "integratedTerminal"],
"description": "Where to launch the debug target",
"default": "internalConsole"
},
// Browser-specific
"url": {
"type": ["string", "null"],
"description": "Will navigate to this URL and attach to it (browser debugging)"
},
"webRoot": {
"type": "string",
"description": "Workspace absolute path to the webserver root",
"default": "${ZED_WORKTREE_ROOT}"
},
"userDataDir": {
"type": ["string", "boolean"],
"description": "Path to a custom Chrome user profile (browser debugging)",
"default": true
},
"skipFiles": {
"type": "array",
"description": "An array of glob patterns for files to skip when debugging",
"items": {
"type": "string"
},
"default": ["<node_internals>/**"]
},
"timeout": {
"type": "number",
"description": "Retry for this number of milliseconds to connect to the debug adapter",
"default": 10000
},
"resolveSourceMapLocations": {
"type": ["array", "null"],
"description": "A list of minimatch patterns for source map resolution",
"items": {
"type": "string"
}
}
},
"required": ["program"]
}
]
},
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["attach"],
"description": "Request to attach to an existing process"
}
}
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "edge", "pwa-edge"],
"description": "The type of debug session",
"default": "pwa-node"
},
"processId": {
"type": ["string", "number"],
"description": "ID of process to attach to (Node.js debugging)"
},
"port": {
"type": "number",
"description": "Debug port to attach to",
"default": 9229
},
"address": {
"type": "string",
"description": "TCP/IP address of the process to be debugged",
"default": "localhost"
},
"restart": {
"type": ["boolean", "object"],
"description": "Restart session after Node.js has terminated",
"default": false
},
"sourceMaps": {
"type": "boolean",
"description": "Use JavaScript source maps if they exist",
"default": true
},
"sourceMapPathOverrides": {
"type": "object",
"description": "Rewrites the locations of source files from what the sourcemap says to their locations on disk",
"default": {}
},
"outFiles": {
"type": "array",
"description": "Glob patterns for locating generated JavaScript files",
"items": {
"type": "string"
},
"default": ["${ZED_WORKTREE_ROOT}/**/*.js", "!**/node_modules/**"]
},
"url": {
"type": "string",
"description": "Will search for a page with this URL and attach to it (browser debugging)"
},
"webRoot": {
"type": "string",
"description": "Workspace absolute path to the webserver root",
"default": "${ZED_WORKTREE_ROOT}"
},
"skipFiles": {
"type": "array",
"description": "An array of glob patterns for files to skip when debugging",
"items": {
"type": "string"
},
"default": ["<node_internals>/**"]
},
"timeout": {
"type": "number",
"description": "Retry for this number of milliseconds to connect to the debug adapter",
"default": 10000
},
"resolveSourceMapLocations": {
"type": ["array", "null"],
"description": "A list of minimatch patterns for source map resolution",
"items": {
"type": "string"
}
},
"remoteRoot": {
"type": ["string", "null"],
"description": "Path to the remote directory containing the program"
},
"localRoot": {
"type": ["string", "null"],
"description": "Path to the local directory containing the program"
}
},
"oneOf": [
{ "required": ["processId"] },
{ "required": ["port"] }
]
}
]
}
]
})
}
async fn get_binary( async fn get_binary(
&self, &self,
delegate: &Arc<dyn DapDelegate>, delegate: &Arc<dyn DapDelegate>,

View file

@ -1,5 +1,7 @@
use adapters::latest_github_release; use adapters::latest_github_release;
use anyhow::Context as _; use anyhow::Context as _;
use anyhow::bail;
use dap::StartDebuggingRequestArguments;
use dap::adapters::{DebugTaskDefinition, TcpArguments}; use dap::adapters::{DebugTaskDefinition, TcpArguments};
use gpui::{AsyncApp, SharedString}; use gpui::{AsyncApp, SharedString};
use language::LanguageName; use language::LanguageName;
@ -18,27 +20,6 @@ impl PhpDebugAdapter {
const ADAPTER_PACKAGE_NAME: &'static str = "vscode-php-debug"; const ADAPTER_PACKAGE_NAME: &'static str = "vscode-php-debug";
const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js"; const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js";
fn request_args(
&self,
config: &DebugTaskDefinition,
) -> Result<dap::StartDebuggingRequestArguments> {
match &config.request {
dap::DebugRequest::Attach(_) => {
anyhow::bail!("php adapter does not support attaching")
}
dap::DebugRequest::Launch(launch_config) => Ok(dap::StartDebuggingRequestArguments {
configuration: json!({
"program": launch_config.program,
"cwd": launch_config.cwd,
"args": launch_config.args,
"env": launch_config.env_json(),
"stopOnEntry": config.stop_on_entry.unwrap_or_default(),
}),
request: config.request.to_dap(),
}),
}
}
async fn fetch_latest_adapter_version( async fn fetch_latest_adapter_version(
&self, &self,
delegate: &Arc<dyn DapDelegate>, delegate: &Arc<dyn DapDelegate>,
@ -68,7 +49,7 @@ impl PhpDebugAdapter {
async fn get_installed_binary( async fn get_installed_binary(
&self, &self,
delegate: &Arc<dyn DapDelegate>, delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition, task_definition: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>, user_installed_path: Option<PathBuf>,
_: &mut AsyncApp, _: &mut AsyncApp,
) -> Result<DebugAdapterBinary> { ) -> Result<DebugAdapterBinary> {
@ -86,7 +67,7 @@ impl PhpDebugAdapter {
.context("Couldn't find PHP dap directory")? .context("Couldn't find PHP dap directory")?
}; };
let tcp_connection = config.tcp_connection.clone().unwrap_or_default(); let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary { Ok(DebugAdapterBinary {
@ -110,13 +91,202 @@ impl PhpDebugAdapter {
}), }),
cwd: None, cwd: None,
envs: HashMap::default(), envs: HashMap::default(),
request_args: self.request_args(config)?, request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
request: dap::StartDebuggingRequestArgumentsRequest::Launch,
},
}) })
} }
} }
#[async_trait(?Send)] #[async_trait(?Send)]
impl DebugAdapter for PhpDebugAdapter { impl DebugAdapter for PhpDebugAdapter {
fn dap_schema(&self) -> serde_json::Value {
json!({
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "The request type for the PHP debug adapter, always \"launch\"",
"default": "launch"
},
"hostname": {
"type": "string",
"description": "The address to bind to when listening for Xdebug (default: all IPv6 connections if available, else all IPv4 connections) or Unix Domain socket (prefix with unix://) or Windows Pipe (\\\\?\\pipe\\name) - cannot be combined with port"
},
"port": {
"type": "integer",
"description": "The port on which to listen for Xdebug (default: 9003). If port is set to 0 a random port is chosen by the system and a placeholder ${port} is replaced with the chosen port in env and runtimeArgs.",
"default": 9003
},
"program": {
"type": "string",
"description": "The PHP script to debug (typically a path to a file)",
"default": "${file}"
},
"cwd": {
"type": "string",
"description": "Working directory for the debugged program"
},
"args": {
"type": "array",
"items": {
"type": "string"
},
"description": "Command line arguments to pass to the program"
},
"env": {
"type": "object",
"description": "Environment variables to pass to the program",
"additionalProperties": {
"type": "string"
}
},
"stopOnEntry": {
"type": "boolean",
"description": "Whether to break at the beginning of the script",
"default": false
},
"pathMappings": {
"type": "array",
"description": "A list of server paths mapping to the local source paths on your machine for remote host debugging",
"items": {
"type": "object",
"properties": {
"serverPath": {
"type": "string",
"description": "Path on the server"
},
"localPath": {
"type": "string",
"description": "Corresponding path on the local machine"
}
},
"required": ["serverPath", "localPath"]
}
},
"log": {
"type": "boolean",
"description": "Whether to log all communication between editor and the adapter to the debug console",
"default": false
},
"ignore": {
"type": "array",
"description": "An array of glob patterns that errors should be ignored from (for example **/vendor/**/*.php)",
"items": {
"type": "string"
}
},
"ignoreExceptions": {
"type": "array",
"description": "An array of exception class names that should be ignored (for example BaseException, \\NS1\\Exception, \\*\\Exception or \\**\\Exception*)",
"items": {
"type": "string"
}
},
"skipFiles": {
"type": "array",
"description": "An array of glob patterns to skip when debugging. Star patterns and negations are allowed.",
"items": {
"type": "string"
}
},
"skipEntryPaths": {
"type": "array",
"description": "An array of glob patterns to immediately detach from and ignore for debugging if the entry script matches",
"items": {
"type": "string"
}
},
"maxConnections": {
"type": "integer",
"description": "Accept only this number of parallel debugging sessions. Additional connections will be dropped.",
"default": 1
},
"proxy": {
"type": "object",
"description": "DBGp Proxy settings",
"properties": {
"enable": {
"type": "boolean",
"description": "To enable proxy registration",
"default": false
},
"host": {
"type": "string",
"description": "The address of the proxy. Supports host name, IP address, or Unix domain socket.",
"default": "127.0.0.1"
},
"port": {
"type": "integer",
"description": "The port where the adapter will register with the proxy",
"default": 9001
},
"key": {
"type": "string",
"description": "A unique key that allows the proxy to match requests to your editor",
"default": "vsc"
},
"timeout": {
"type": "integer",
"description": "The number of milliseconds to wait before giving up on the connection to proxy",
"default": 3000
},
"allowMultipleSessions": {
"type": "boolean",
"description": "If the proxy should forward multiple sessions/connections at the same time or not",
"default": true
}
}
},
"xdebugSettings": {
"type": "object",
"description": "Allows you to override Xdebug's remote debugging settings to fine tune Xdebug to your needs",
"properties": {
"max_children": {
"type": "integer",
"description": "Max number of array or object children to initially retrieve"
},
"max_data": {
"type": "integer",
"description": "Max amount of variable data to initially retrieve"
},
"max_depth": {
"type": "integer",
"description": "Maximum depth that the debugger engine may return when sending arrays, hashes or object structures to the IDE"
},
"show_hidden": {
"type": "integer",
"description": "Whether to show detailed internal information on properties (e.g. private members of classes). Zero means hidden members are not shown.",
"enum": [0, 1]
},
"breakpoint_include_return_value": {
"type": "boolean",
"description": "Determines whether to enable an additional \"return from function\" debugging step, allowing inspection of the return value when a function call returns"
}
}
},
"xdebugCloudToken": {
"type": "string",
"description": "Instead of listening locally, open a connection and register with Xdebug Cloud and accept debugging sessions on that connection"
},
"stream": {
"type": "object",
"description": "Allows to influence DBGp streams. Xdebug only supports stdout",
"properties": {
"stdout": {
"type": "integer",
"description": "Redirect stdout stream: 0 (disable), 1 (copy), 2 (redirect)",
"enum": [0, 1, 2],
"default": 0
}
}
}
},
"required": ["request", "program"]
})
}
fn name(&self) -> DebugAdapterName { fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into()) DebugAdapterName(Self::ADAPTER_NAME.into())
} }
@ -125,10 +295,33 @@ impl DebugAdapter for PhpDebugAdapter {
Some(SharedString::new_static("PHP").into()) Some(SharedString::new_static("PHP").into())
} }
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let obj = match &zed_scenario.request {
dap::DebugRequest::Attach(_) => {
bail!("Php adapter doesn't support attaching")
}
dap::DebugRequest::Launch(launch_config) => json!({
"program": launch_config.program,
"cwd": launch_config.cwd,
"args": launch_config.args,
"env": launch_config.env_json(),
"stopOnEntry": zed_scenario.stop_on_entry.unwrap_or_default(),
}),
};
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
build: None,
config: obj,
tcp_connection: None,
})
}
async fn get_binary( async fn get_binary(
&self, &self,
delegate: &Arc<dyn DapDelegate>, delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition, task_definition: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>, user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp, cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> { ) -> Result<DebugAdapterBinary> {
@ -145,7 +338,7 @@ impl DebugAdapter for PhpDebugAdapter {
} }
} }
self.get_installed_binary(delegate, &config, user_installed_path, cx) self.get_installed_binary(delegate, &task_definition, user_installed_path, cx)
.await .await
} }
} }

View file

@ -1,6 +1,9 @@
use crate::*; use crate::*;
use anyhow::Context as _; use anyhow::{Context as _, anyhow};
use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; use dap::{
DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::DebugTaskDefinition,
};
use gpui::{AsyncApp, SharedString}; use gpui::{AsyncApp, SharedString};
use language::LanguageName; use language::LanguageName;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::OnceLock}; use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::OnceLock};
@ -17,39 +20,16 @@ impl PythonDebugAdapter {
const ADAPTER_PATH: &'static str = "src/debugpy/adapter"; const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
const LANGUAGE_NAME: &'static str = "Python"; const LANGUAGE_NAME: &'static str = "Python";
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments { fn request_args(
let mut args = json!({ &self,
"request": match config.request { task_definition: &DebugTaskDefinition,
DebugRequest::Launch(_) => "launch", ) -> Result<StartDebuggingRequestArguments> {
DebugRequest::Attach(_) => "attach", let request = self.validate_config(&task_definition.config)?;
},
"subProcess": true,
"redirectOutput": true,
});
let map = args.as_object_mut().unwrap();
match &config.request {
DebugRequest::Attach(attach) => {
map.insert("processId".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
map.insert("args".into(), launch.args.clone().into());
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = config.stop_on_entry { Ok(StartDebuggingRequestArguments {
map.insert("stopOnEntry".into(), stop_on_entry.into()); configuration: task_definition.config.clone(),
} request,
if let Some(cwd) = launch.cwd.as_ref() { })
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
StartDebuggingRequestArguments {
configuration: args,
request: config.request.to_dap(),
}
} }
async fn fetch_latest_adapter_version( async fn fetch_latest_adapter_version(
&self, &self,
@ -160,7 +140,7 @@ impl PythonDebugAdapter {
}), }),
cwd: None, cwd: None,
envs: HashMap::default(), envs: HashMap::default(),
request_args: self.request_args(config), request_args: self.request_args(config)?,
}) })
} }
} }
@ -175,6 +155,394 @@ impl DebugAdapter for PythonDebugAdapter {
Some(SharedString::new_static("Python").into()) Some(SharedString::new_static("Python").into())
} }
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut args = json!({
"request": match zed_scenario.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
"subProcess": true,
"redirectOutput": true,
});
let map = args.as_object_mut().unwrap();
match &zed_scenario.request {
DebugRequest::Attach(attach) => {
map.insert("processId".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
map.insert("args".into(), launch.args.clone().into());
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
config: args,
build: None,
tcp_connection: None,
})
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config.as_object().context("Config isn't an object")?;
let request_variant = map["request"].as_str().context("request is not valid")?;
match request_variant {
"launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
"attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
_ => Err(anyhow!("request must be either 'launch' or 'attach'")),
}
}
fn dap_schema(&self) -> serde_json::Value {
json!({
"properties": {
"request": {
"type": "string",
"enum": ["attach", "launch"],
"description": "Debug adapter request type"
},
"autoReload": {
"default": {},
"description": "Configures automatic reload of code on edit.",
"properties": {
"enable": {
"default": false,
"description": "Automatically reload code on edit.",
"type": "boolean"
},
"exclude": {
"default": [
"**/.git/**",
"**/.metadata/**",
"**/__pycache__/**",
"**/node_modules/**",
"**/site-packages/**"
],
"description": "Glob patterns of paths to exclude from auto reload.",
"items": {
"type": "string"
},
"type": "array"
},
"include": {
"default": [
"**/*.py",
"**/*.pyw"
],
"description": "Glob patterns of paths to include in auto reload.",
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"debugAdapterPath": {
"description": "Path (fully qualified) to the python debug adapter executable.",
"type": "string"
},
"django": {
"default": false,
"description": "Django debugging.",
"type": "boolean"
},
"jinja": {
"default": null,
"description": "Jinja template debugging (e.g. Flask).",
"enum": [
false,
null,
true
]
},
"justMyCode": {
"default": true,
"description": "If true, show and debug only user-written code. If false, show and debug all code, including library calls.",
"type": "boolean"
},
"logToFile": {
"default": false,
"description": "Enable logging of debugger events to a log file. This file can be found in the debugpy extension install folder.",
"type": "boolean"
},
"pathMappings": {
"default": [],
"items": {
"label": "Path mapping",
"properties": {
"localRoot": {
"default": "${ZED_WORKTREE_ROOT}",
"label": "Local source root.",
"type": "string"
},
"remoteRoot": {
"default": "",
"label": "Remote source root.",
"type": "string"
}
},
"required": [
"localRoot",
"remoteRoot"
],
"type": "object"
},
"label": "Path mappings.",
"type": "array"
},
"redirectOutput": {
"default": true,
"description": "Redirect output.",
"type": "boolean"
},
"showReturnValue": {
"default": true,
"description": "Show return value of functions when stepping.",
"type": "boolean"
},
"subProcess": {
"default": false,
"description": "Whether to enable Sub Process debugging",
"type": "boolean"
},
"consoleName": {
"default": "Python Debug Console",
"description": "Display name of the debug console or terminal",
"type": "string"
},
"clientOS": {
"default": null,
"description": "OS that VS code is using.",
"enum": [
"windows",
null,
"unix"
]
}
},
"required": ["request"],
"allOf": [
{
"if": {
"properties": {
"request": {
"enum": ["attach"]
}
}
},
"then": {
"properties": {
"connect": {
"label": "Attach by connecting to debugpy over a socket.",
"properties": {
"host": {
"default": "127.0.0.1",
"description": "Hostname or IP address to connect to.",
"type": "string"
},
"port": {
"description": "Port to connect to.",
"type": [
"number",
"string"
]
}
},
"required": [
"port"
],
"type": "object"
},
"listen": {
"label": "Attach by listening for incoming socket connection from debugpy",
"properties": {
"host": {
"default": "127.0.0.1",
"description": "Hostname or IP address of the interface to listen on.",
"type": "string"
},
"port": {
"description": "Port to listen on.",
"type": [
"number",
"string"
]
}
},
"required": [
"port"
],
"type": "object"
},
"processId": {
"anyOf": [
{
"default": "${command:pickProcess}",
"description": "Use process picker to select a process to attach, or Process ID as integer.",
"enum": [
"${command:pickProcess}"
]
},
{
"description": "ID of the local process to attach to.",
"type": "integer"
}
]
}
}
}
},
{
"if": {
"properties": {
"request": {
"enum": ["launch"]
}
}
},
"then": {
"properties": {
"args": {
"default": [],
"description": "Command line arguments passed to the program. For string type arguments, it will pass through the shell as is, and therefore all shell variable expansions will apply. But for the array type, the values will be shell-escaped.",
"items": {
"type": "string"
},
"anyOf": [
{
"default": "${command:pickArgs}",
"enum": [
"${command:pickArgs}"
]
},
{
"type": [
"array",
"string"
]
}
]
},
"console": {
"default": "integratedTerminal",
"description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.",
"enum": [
"externalTerminal",
"integratedTerminal",
"internalConsole"
]
},
"cwd": {
"default": "${ZED_WORKTREE_ROOT}",
"description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).",
"type": "string"
},
"autoStartBrowser": {
"default": false,
"description": "Open external browser to launch the application",
"type": "boolean"
},
"env": {
"additionalProperties": {
"type": "string"
},
"default": {},
"description": "Environment variables defined as a key value pair. Property ends up being the Environment Variable and the value of the property ends up being the value of the Env Variable.",
"type": "object"
},
"envFile": {
"default": "${ZED_WORKTREE_ROOT}/.env",
"description": "Absolute path to a file containing environment variable definitions.",
"type": "string"
},
"gevent": {
"default": false,
"description": "Enable debugging of gevent monkey-patched code.",
"type": "boolean"
},
"module": {
"default": "",
"description": "Name of the module to be debugged.",
"type": "string"
},
"program": {
"default": "${ZED_FILE}",
"description": "Absolute path to the program.",
"type": "string"
},
"purpose": {
"default": [],
"description": "Tells extension to use this configuration for test debugging, or when using debug-in-terminal command.",
"items": {
"enum": [
"debug-test",
"debug-in-terminal"
],
"enumDescriptions": [
"Use this configuration while debugging tests using test view or test debug commands.",
"Use this configuration while debugging a file using debug in terminal button in the editor."
]
},
"type": "array"
},
"pyramid": {
"default": false,
"description": "Whether debugging Pyramid applications.",
"type": "boolean"
},
"python": {
"default": "${command:python.interpreterPath}",
"description": "Absolute path to the Python interpreter executable; overrides workspace configuration if set.",
"type": "string"
},
"pythonArgs": {
"default": [],
"description": "Command-line arguments passed to the Python interpreter. To pass arguments to the debug target, use \"args\".",
"items": {
"type": "string"
},
"type": "array"
},
"stopOnEntry": {
"default": false,
"description": "Automatically stop after launch.",
"type": "boolean"
},
"sudo": {
"default": false,
"description": "Running debug program under elevated permissions (on Unix).",
"type": "boolean"
},
"guiEventLoop": {
"default": "matplotlib",
"description": "The GUI event loop that's going to run. Possible values: \"matplotlib\", \"wx\", \"qt\", \"none\", or a custom function that'll be imported and run.",
"type": "string"
}
}
}
}
]
})
}
async fn get_binary( async fn get_binary(
&self, &self,
delegate: &Arc<dyn DapDelegate>, delegate: &Arc<dyn DapDelegate>,

View file

@ -3,16 +3,17 @@ use async_trait::async_trait;
use dap::{ use dap::{
DebugRequest, StartDebuggingRequestArguments, DebugRequest, StartDebuggingRequestArguments,
adapters::{ adapters::{
self, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
}, },
}; };
use gpui::{AsyncApp, SharedString}; use gpui::{AsyncApp, SharedString};
use language::LanguageName; use language::LanguageName;
use std::{path::PathBuf, sync::Arc}; use serde_json::json;
use std::path::PathBuf;
use std::sync::Arc;
use task::{DebugScenario, ZedDebugConfig};
use util::command::new_smol_command; use util::command::new_smol_command;
use crate::ToDap;
#[derive(Default)] #[derive(Default)]
pub(crate) struct RubyDebugAdapter; pub(crate) struct RubyDebugAdapter;
@ -30,6 +31,187 @@ impl DebugAdapter for RubyDebugAdapter {
Some(SharedString::new_static("Ruby").into()) Some(SharedString::new_static("Ruby").into())
} }
fn dap_schema(&self) -> serde_json::Value {
json!({
"oneOf": [
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "Request to launch a new process"
}
}
},
{
"type": "object",
"required": ["script"],
"properties": {
"command": {
"type": "string",
"description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
"default": "ruby"
},
"script": {
"type": "string",
"description": "Absolute path to a Ruby file."
},
"cwd": {
"type": "string",
"description": "Directory to execute the program in",
"default": "${ZED_WORKTREE_ROOT}"
},
"args": {
"type": "array",
"description": "Command line arguments passed to the program",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"description": "Additional environment variables to pass to the debugging (and debugged) process",
"default": {}
},
"showProtocolLog": {
"type": "boolean",
"description": "Show a log of DAP requests, events, and responses",
"default": false
},
"useBundler": {
"type": "boolean",
"description": "Execute Ruby programs with `bundle exec` instead of directly",
"default": false
},
"bundlePath": {
"type": "string",
"description": "Location of the bundle executable"
},
"rdbgPath": {
"type": "string",
"description": "Location of the rdbg executable"
},
"askParameters": {
"type": "boolean",
"description": "Ask parameters at first."
},
"debugPort": {
"type": "string",
"description": "UNIX domain socket name or TPC/IP host:port"
},
"waitLaunchTime": {
"type": "number",
"description": "Wait time before connection in milliseconds"
},
"localfs": {
"type": "boolean",
"description": "true if the VSCode and debugger run on a same machine",
"default": false
},
"useTerminal": {
"type": "boolean",
"description": "Create a new terminal and then execute commands there",
"default": false
}
}
}
]
},
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["attach"],
"description": "Request to attach to an existing process"
}
}
},
{
"type": "object",
"properties": {
"rdbgPath": {
"type": "string",
"description": "Location of the rdbg executable"
},
"debugPort": {
"type": "string",
"description": "UNIX domain socket name or TPC/IP host:port"
},
"showProtocolLog": {
"type": "boolean",
"description": "Show a log of DAP requests, events, and responses",
"default": false
},
"localfs": {
"type": "boolean",
"description": "true if the VSCode and debugger run on a same machine",
"default": false
},
"localfsMap": {
"type": "string",
"description": "Specify pairs of remote root path and local root path like `/remote_dir:/local_dir`. You can specify multiple pairs like `/rem1:/loc1,/rem2:/loc2` by concatenating with `,`."
},
"env": {
"type": "object",
"description": "Additional environment variables to pass to the rdbg process",
"default": {}
}
}
}
]
}
]
})
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut config = serde_json::Map::new();
match &zed_scenario.request {
DebugRequest::Launch(launch) => {
config.insert("request".to_string(), json!("launch"));
config.insert("script".to_string(), json!(launch.program));
config.insert("command".to_string(), json!("ruby"));
if !launch.args.is_empty() {
config.insert("args".to_string(), json!(launch.args));
}
if !launch.env.is_empty() {
config.insert("env".to_string(), json!(launch.env));
}
if let Some(cwd) = &launch.cwd {
config.insert("cwd".to_string(), json!(cwd));
}
// Ruby stops on entry so there's no need to handle that case
}
DebugRequest::Attach(attach) => {
config.insert("request".to_string(), json!("attach"));
config.insert("processId".to_string(), json!(attach.process_id));
}
}
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
config: serde_json::Value::Object(config),
tcp_connection: None,
build: None,
})
}
async fn get_binary( async fn get_binary(
&self, &self,
delegate: &Arc<dyn DapDelegate>, delegate: &Arc<dyn DapDelegate>,
@ -66,34 +248,25 @@ impl DebugAdapter for RubyDebugAdapter {
let tcp_connection = definition.tcp_connection.clone().unwrap_or_default(); let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let DebugRequest::Launch(launch) = definition.request.clone() else { let arguments = vec![
anyhow::bail!("rdbg does not yet support attaching");
};
let mut arguments = vec![
"--open".to_string(), "--open".to_string(),
format!("--port={}", port), format!("--port={}", port),
format!("--host={}", host), format!("--host={}", host),
]; ];
if delegate.which(launch.program.as_ref()).await.is_some() {
arguments.push("--command".to_string())
}
arguments.push(launch.program);
arguments.extend(launch.args);
Ok(DebugAdapterBinary { Ok(DebugAdapterBinary {
command: rdbg_path.to_string_lossy().to_string(), command: rdbg_path.to_string_lossy().to_string(),
arguments, arguments,
connection: Some(adapters::TcpArguments { connection: Some(dap::adapters::TcpArguments {
host, host,
port, port,
timeout, timeout,
}), }),
cwd: launch.cwd, cwd: None,
envs: launch.env.into_iter().collect(), envs: std::collections::HashMap::default(),
request_args: StartDebuggingRequestArguments { request_args: StartDebuggingRequestArguments {
configuration: serde_json::Value::Object(Default::default()), request: self.validate_config(&definition.config)?,
request: definition.request.to_dap(), configuration: definition.config.clone(),
}, },
}) })
} }

View file

@ -11,6 +11,8 @@ async-trait.workspace = true
dap.workspace = true dap.workspace = true
extension.workspace = true extension.workspace = true
gpui.workspace = true gpui.workspace = true
serde_json.workspace = true
task.workspace = true
workspace-hack = { version = "0.1", path = "../../tooling/workspace-hack" } workspace-hack = { version = "0.1", path = "../../tooling/workspace-hack" }
[lints] [lints]

View file

@ -7,6 +7,7 @@ use dap::adapters::{
}; };
use extension::{Extension, WorktreeDelegate}; use extension::{Extension, WorktreeDelegate};
use gpui::AsyncApp; use gpui::AsyncApp;
use task::{DebugScenario, ZedDebugConfig};
pub(crate) struct ExtensionDapAdapter { pub(crate) struct ExtensionDapAdapter {
extension: Arc<dyn Extension>, extension: Arc<dyn Extension>,
@ -60,6 +61,10 @@ impl DebugAdapter for ExtensionDapAdapter {
self.debug_adapter_name.as_ref().into() self.debug_adapter_name.as_ref().into()
} }
fn dap_schema(&self) -> serde_json::Value {
serde_json::Value::Null
}
async fn get_binary( async fn get_binary(
&self, &self,
delegate: &Arc<dyn DapDelegate>, delegate: &Arc<dyn DapDelegate>,
@ -76,4 +81,8 @@ impl DebugAdapter for ExtensionDapAdapter {
) )
.await .await
} }
fn config_from_zed_format(&self, _zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
Err(anyhow::anyhow!("DAP extensions are not implemented yet"))
}
} }

View file

@ -1,15 +1,15 @@
use dap::DebugRequest; use dap::{DapRegistry, DebugRequest};
use dap::adapters::DebugTaskDefinition;
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render}; use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render};
use gpui::{Subscription, WeakEntity}; use gpui::{Subscription, WeakEntity};
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use task::ZedDebugConfig;
use util::debug_panic;
use std::sync::Arc; use std::sync::Arc;
use sysinfo::System; use sysinfo::System;
use ui::{Context, Tooltip, prelude::*}; use ui::{Context, Tooltip, prelude::*};
use ui::{ListItem, ListItemSpacing}; use ui::{ListItem, ListItemSpacing};
use util::debug_panic;
use workspace::{ModalView, Workspace}; use workspace::{ModalView, Workspace};
use crate::debugger_panel::DebugPanel; use crate::debugger_panel::DebugPanel;
@ -25,7 +25,7 @@ pub(crate) struct AttachModalDelegate {
selected_index: usize, selected_index: usize,
matches: Vec<StringMatch>, matches: Vec<StringMatch>,
placeholder_text: Arc<str>, placeholder_text: Arc<str>,
pub(crate) definition: DebugTaskDefinition, pub(crate) definition: ZedDebugConfig,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
candidates: Arc<[Candidate]>, candidates: Arc<[Candidate]>,
} }
@ -33,7 +33,7 @@ pub(crate) struct AttachModalDelegate {
impl AttachModalDelegate { impl AttachModalDelegate {
fn new( fn new(
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
definition: DebugTaskDefinition, definition: ZedDebugConfig,
candidates: Arc<[Candidate]>, candidates: Arc<[Candidate]>,
) -> Self { ) -> Self {
Self { Self {
@ -54,7 +54,7 @@ pub struct AttachModal {
impl AttachModal { impl AttachModal {
pub fn new( pub fn new(
definition: DebugTaskDefinition, definition: ZedDebugConfig,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
modal: bool, modal: bool,
window: &mut Window, window: &mut Window,
@ -83,7 +83,7 @@ impl AttachModal {
pub(super) fn with_processes( pub(super) fn with_processes(
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
definition: DebugTaskDefinition, definition: ZedDebugConfig,
processes: Arc<[Candidate]>, processes: Arc<[Candidate]>,
modal: bool, modal: bool,
window: &mut Window, window: &mut Window,
@ -228,7 +228,13 @@ impl PickerDelegate for AttachModalDelegate {
} }
} }
let scenario = self.definition.to_scenario(); let Some(scenario) = cx.read_global::<DapRegistry, _>(|registry, _| {
registry
.adapter(&self.definition.adapter)
.and_then(|adapter| adapter.config_from_zed_format(self.definition.clone()).ok())
}) else {
return;
};
let panel = self let panel = self
.workspace .workspace

View file

@ -82,7 +82,7 @@ impl DebugPanel {
let thread_picker_menu_handle = PopoverMenuHandle::default(); let thread_picker_menu_handle = PopoverMenuHandle::default();
let session_picker_menu_handle = PopoverMenuHandle::default(); let session_picker_menu_handle = PopoverMenuHandle::default();
let debug_panel = Self { Self {
size: px(300.), size: px(300.),
sessions: vec![], sessions: vec![],
active_session: None, active_session: None,
@ -93,9 +93,7 @@ impl DebugPanel {
fs: workspace.app_state().fs.clone(), fs: workspace.app_state().fs.clone(),
thread_picker_menu_handle, thread_picker_menu_handle,
session_picker_menu_handle, session_picker_menu_handle,
}; }
debug_panel
}) })
} }
@ -301,6 +299,7 @@ impl DebugPanel {
cx.spawn(async move |_, cx| { cx.spawn(async move |_, cx| {
if let Err(error) = task.await { if let Err(error) = task.await {
log::error!("{error}");
session session
.update(cx, |session, cx| { .update(cx, |session, cx| {
session session

View file

@ -9,10 +9,7 @@ use std::{
usize, usize,
}; };
use dap::{ use dap::{DapRegistry, DebugRequest, adapters::DebugAdapterName};
DapRegistry, DebugRequest,
adapters::{DebugAdapterName, DebugTaskDefinition},
};
use editor::{Editor, EditorElement, EditorStyle}; use editor::{Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
@ -22,7 +19,7 @@ use gpui::{
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore}; use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
use settings::Settings; use settings::Settings;
use task::{DebugScenario, LaunchRequest}; use task::{DebugScenario, LaunchRequest, ZedDebugConfig};
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{ use ui::{
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context, ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
@ -210,15 +207,16 @@ impl NewSessionModal {
None None
}; };
Some(DebugScenario { let session_scenario = ZedDebugConfig {
adapter: debugger.to_owned().into(), adapter: debugger.to_owned().into(),
label, label,
request: Some(request), request: request,
initialize_args: None,
tcp_connection: None,
stop_on_entry, stop_on_entry,
build: None, };
})
cx.global::<DapRegistry>()
.adapter(&session_scenario.adapter)
.and_then(|adapter| adapter.config_from_zed_format(session_scenario).ok())
} }
fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) { fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) {
@ -264,12 +262,12 @@ impl NewSessionModal {
cx: &mut App, cx: &mut App,
) { ) {
attach.update(cx, |this, cx| { attach.update(cx, |this, cx| {
if adapter != &this.definition.adapter { if adapter.0 != this.definition.adapter {
this.definition.adapter = adapter.clone(); this.definition.adapter = adapter.0.clone();
this.attach_picker.update(cx, |this, cx| { this.attach_picker.update(cx, |this, cx| {
this.picker.update(cx, |this, cx| { this.picker.update(cx, |this, cx| {
this.delegate.definition.adapter = adapter.clone(); this.delegate.definition.adapter = adapter.0.clone();
this.focus(window, cx); this.focus(window, cx);
}) })
}); });
@ -862,7 +860,7 @@ impl CustomMode {
#[derive(Clone)] #[derive(Clone)]
pub(super) struct AttachMode { pub(super) struct AttachMode {
pub(super) definition: DebugTaskDefinition, pub(super) definition: ZedDebugConfig,
pub(super) attach_picker: Entity<AttachModal>, pub(super) attach_picker: Entity<AttachModal>,
} }
@ -873,12 +871,10 @@ impl AttachMode {
window: &mut Window, window: &mut Window,
cx: &mut Context<NewSessionModal>, cx: &mut Context<NewSessionModal>,
) -> Entity<Self> { ) -> Entity<Self> {
let definition = DebugTaskDefinition { let definition = ZedDebugConfig {
adapter: debugger.unwrap_or(DebugAdapterName("".into())), adapter: debugger.unwrap_or(DebugAdapterName("".into())).0,
label: "Attach New Session Setup".into(), label: "Attach New Session Setup".into(),
request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }), request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
initialize_args: None,
tcp_connection: None,
stop_on_entry: Some(false), stop_on_entry: Some(false),
}; };
let attach_picker = cx.new(|cx| { let attach_picker = cx.new(|cx| {
@ -938,27 +934,14 @@ impl DebugScenarioDelegate {
}); });
let language = language.or_else(|| { let language = language.or_else(|| {
scenario scenario.label.split_whitespace().find_map(|word| {
.request language_names
.as_ref() .iter()
.and_then(|request| match request { .find(|name| name.eq_ignore_ascii_case(word))
DebugRequest::Launch(launch) => launch .map(|name| TaskSourceKind::Language {
.program name: name.to_owned().into(),
.rsplit_once(".")
.and_then(|split| languages.language_name_for_extension(split.1))
.map(|name| TaskSourceKind::Language { name: name.into() }),
_ => None,
})
.or_else(|| {
scenario.label.split_whitespace().find_map(|word| {
language_names
.iter()
.find(|name| name.eq_ignore_ascii_case(word))
.map(|name| TaskSourceKind::Language {
name: name.to_owned().into(),
})
}) })
}) })
}); });
(language, scenario) (language, scenario)
@ -1092,7 +1075,7 @@ impl PickerDelegate for DebugScenarioDelegate {
.get(self.selected_index()) .get(self.selected_index())
.and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned()); .and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned());
let Some((_, mut debug_scenario)) = debug_scenario else { let Some((_, debug_scenario)) = debug_scenario else {
return; return;
}; };
@ -1107,19 +1090,6 @@ impl PickerDelegate for DebugScenarioDelegate {
}) })
.unwrap_or_default(); .unwrap_or_default();
if let Some(launch_config) =
debug_scenario
.request
.as_mut()
.and_then(|request| match request {
DebugRequest::Launch(launch) => Some(launch),
_ => None,
})
{
let (program, _) = resolve_paths(launch_config.program.clone(), String::new());
launch_config.program = program;
}
self.debug_panel self.debug_panel
.update(cx, |panel, cx| { .update(cx, |panel, cx| {
panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx); panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx);

View file

@ -15,7 +15,7 @@ use breakpoint_list::BreakpointList;
use collections::{HashMap, IndexMap}; use collections::{HashMap, IndexMap};
use console::Console; use console::Console;
use dap::{ use dap::{
Capabilities, RunInTerminalRequestArguments, Thread, Capabilities, DapRegistry, RunInTerminalRequestArguments, Thread,
adapters::{DebugAdapterName, DebugTaskDefinition}, adapters::{DebugAdapterName, DebugTaskDefinition},
client::SessionId, client::SessionId,
debugger_settings::DebuggerSettings, debugger_settings::DebuggerSettings,
@ -38,8 +38,8 @@ use serde_json::Value;
use settings::Settings; use settings::Settings;
use stack_frame_list::StackFrameList; use stack_frame_list::StackFrameList;
use task::{ use task::{
BuildTaskDefinition, DebugScenario, LaunchRequest, ShellBuilder, SpawnInTerminal, TaskContext, BuildTaskDefinition, DebugScenario, ShellBuilder, SpawnInTerminal, TaskContext, ZedDebugConfig,
substitute_variables_in_map, substitute_variables_in_str, substitute_variables_in_str,
}; };
use terminal_view::TerminalView; use terminal_view::TerminalView;
use ui::{ use ui::{
@ -519,6 +519,30 @@ impl Focusable for DebugTerminal {
} }
impl RunningState { impl RunningState {
// todo(debugger) move this to util and make it so you pass a closure to it that converts a string
pub(crate) fn substitute_variables_in_config(
config: &mut serde_json::Value,
context: &TaskContext,
) {
match config {
serde_json::Value::Object(obj) => {
obj.values_mut()
.for_each(|value| Self::substitute_variables_in_config(value, context));
}
serde_json::Value::Array(array) => {
array
.iter_mut()
.for_each(|value| Self::substitute_variables_in_config(value, context));
}
serde_json::Value::String(s) => {
if let Some(substituted) = substitute_variables_in_str(&s, context) {
*s = substituted;
}
}
_ => {}
}
}
pub(crate) fn new( pub(crate) fn new(
session: Entity<Session>, session: Entity<Session>,
project: Entity<Project>, project: Entity<Project>,
@ -704,6 +728,7 @@ impl RunningState {
}; };
let project = workspace.read(cx).project().clone(); let project = workspace.read(cx).project().clone();
let dap_store = project.read(cx).dap_store().downgrade(); let dap_store = project.read(cx).dap_store().downgrade();
let dap_registry = cx.global::<DapRegistry>().clone();
let task_store = project.read(cx).task_store().downgrade(); let task_store = project.read(cx).task_store().downgrade();
let weak_project = project.downgrade(); let weak_project = project.downgrade();
let weak_workspace = workspace.downgrade(); let weak_workspace = workspace.downgrade();
@ -713,11 +738,18 @@ impl RunningState {
adapter, adapter,
label, label,
build, build,
request, mut config,
initialize_args,
tcp_connection, tcp_connection,
stop_on_entry,
} = scenario; } = scenario;
Self::substitute_variables_in_config(&mut config, &task_context);
let request_type = dap_registry
.adapter(&adapter)
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
.and_then(|adapter| adapter.validate_config(&config));
let config_is_valid = request_type.is_ok();
let build_output = if let Some(build) = build { let build_output = if let Some(build) = build {
let (task, locator_name) = match build { let (task, locator_name) = match build {
BuildTaskDefinition::Template { BuildTaskDefinition::Template {
@ -746,9 +778,9 @@ impl RunningState {
}; };
let locator_name = if let Some(locator_name) = locator_name { let locator_name = if let Some(locator_name) = locator_name {
debug_assert!(request.is_none()); debug_assert!(!config_is_valid);
Some(locator_name) Some(locator_name)
} else if request.is_none() { } else if !config_is_valid {
dap_store dap_store
.update(cx, |this, cx| { .update(cx, |this, cx| {
this.debug_scenario_for_build_task( this.debug_scenario_for_build_task(
@ -825,63 +857,43 @@ impl RunningState {
} else { } else {
None None
}; };
let request = if let Some(request) = request {
request if config_is_valid {
// Ok(DebugTaskDefinition {
// label,
// adapter: DebugAdapterName(adapter),
// config,
// tcp_connection,
// })
} else if let Some((task, locator_name)) = build_output { } else if let Some((task, locator_name)) = build_output {
let locator_name = let locator_name =
locator_name.context("Could not find a valid locator for a build task")?; locator_name.context("Could not find a valid locator for a build task")?;
dap_store let request = dap_store
.update(cx, |this, cx| { .update(cx, |this, cx| {
this.run_debug_locator(&locator_name, task, cx) this.run_debug_locator(&locator_name, task, cx)
})? })?
.await? .await?;
let zed_config = ZedDebugConfig {
label: label.clone(),
adapter: adapter.clone(),
request,
stop_on_entry: None,
};
let scenario = dap_registry
.adapter(&adapter)
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
.map(|adapter| adapter.config_from_zed_format(zed_config))??;
config = scenario.config;
} else { } else {
anyhow::bail!("No request or build provided"); anyhow::bail!("No request or build provided");
}; };
let request = match request {
dap::DebugRequest::Launch(launch_request) => {
let cwd = match launch_request.cwd.as_deref().and_then(|path| path.to_str()) {
Some(cwd) => {
let substituted_cwd = substitute_variables_in_str(&cwd, &task_context)
.context("substituting variables in cwd")?;
Some(PathBuf::from(substituted_cwd))
}
None => None,
};
let env = substitute_variables_in_map(
&launch_request.env.into_iter().collect(),
&task_context,
)
.context("substituting variables in env")?
.into_iter()
.collect();
let new_launch_request = LaunchRequest {
program: substitute_variables_in_str(
&launch_request.program,
&task_context,
)
.context("substituting variables in program")?,
args: launch_request
.args
.into_iter()
.map(|arg| substitute_variables_in_str(&arg, &task_context))
.collect::<Option<Vec<_>>>()
.context("substituting variables in args")?,
cwd,
env,
};
dap::DebugRequest::Launch(new_launch_request)
}
request @ dap::DebugRequest::Attach(_) => request, // todo(debugger): We should check that process_id is valid and if not show the modal
};
Ok(DebugTaskDefinition { Ok(DebugTaskDefinition {
label, label,
adapter: DebugAdapterName(adapter), adapter: DebugAdapterName(adapter),
request, config,
initialize_args,
stop_on_entry,
tcp_connection, tcp_connection,
}) })
}) })

View file

@ -2,7 +2,7 @@ use std::sync::Arc;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use dap::adapters::DebugTaskDefinition; use dap::adapters::DebugTaskDefinition;
use dap::{DebugRequest, client::DebugAdapterClient}; use dap::client::DebugAdapterClient;
use gpui::{Entity, TestAppContext, WindowHandle}; use gpui::{Entity, TestAppContext, WindowHandle};
use project::{Project, debugger::session::Session}; use project::{Project, debugger::session::Session};
use settings::SettingsStore; use settings::SettingsStore;
@ -136,16 +136,18 @@ pub fn start_debug_session<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
cx: &mut gpui::TestAppContext, cx: &mut gpui::TestAppContext,
configure: T, configure: T,
) -> Result<Entity<Session>> { ) -> Result<Entity<Session>> {
use serde_json::json;
start_debug_session_with( start_debug_session_with(
workspace, workspace,
cx, cx,
DebugTaskDefinition { DebugTaskDefinition {
adapter: "fake-adapter".into(), adapter: "fake-adapter".into(),
request: DebugRequest::Launch(Default::default()),
label: "test".into(), label: "test".into(),
initialize_args: None, config: json!({
"request": "launch"
}),
tcp_connection: None, tcp_connection: None,
stop_on_entry: None,
}, },
configure, configure,
) )

View file

@ -5,7 +5,7 @@ use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use menu::Confirm; use menu::Confirm;
use project::{FakeFs, Project}; use project::{FakeFs, Project};
use serde_json::json; use serde_json::json;
use task::{AttachRequest, TcpArgumentsTemplate}; use task::AttachRequest;
use tests::{init_test, init_test_workspace}; use tests::{init_test, init_test_workspace};
use util::path; use util::path;
@ -32,13 +32,12 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
cx, cx,
DebugTaskDefinition { DebugTaskDefinition {
adapter: "fake-adapter".into(), adapter: "fake-adapter".into(),
request: dap::DebugRequest::Attach(AttachRequest {
process_id: Some(10),
}),
label: "label".into(), label: "label".into(),
initialize_args: None, config: json!({
"request": "attach",
"process_id": 10,
}),
tcp_connection: None, tcp_connection: None,
stop_on_entry: None,
}, },
|client| { |client| {
client.on_request::<dap::requests::Attach, _>(move |_, args| { client.on_request::<dap::requests::Attach, _>(move |_, args| {
@ -107,13 +106,10 @@ async fn test_show_attach_modal_and_select_process(
workspace.toggle_modal(window, cx, |window, cx| { workspace.toggle_modal(window, cx, |window, cx| {
AttachModal::with_processes( AttachModal::with_processes(
workspace_handle, workspace_handle,
DebugTaskDefinition { task::ZedDebugConfig {
adapter: FakeAdapter::ADAPTER_NAME.into(), adapter: FakeAdapter::ADAPTER_NAME.into(),
request: dap::DebugRequest::Attach(AttachRequest::default()), request: dap::DebugRequest::Attach(AttachRequest::default()),
label: "attach example".into(), label: "attach example".into(),
initialize_args: None,
tcp_connection: Some(TcpArgumentsTemplate::default()),
stop_on_entry: None, stop_on_entry: None,
}, },
vec![ vec![

View file

@ -24,14 +24,12 @@ use project::{
}; };
use serde_json::json; use serde_json::json;
use std::{ use std::{
collections::HashMap,
path::Path, path::Path,
sync::{ sync::{
Arc, Arc,
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
}, },
}; };
use task::LaunchRequest;
use terminal_view::terminal_panel::TerminalPanel; use terminal_view::terminal_panel::TerminalPanel;
use tests::{active_debug_session_panel, init_test, init_test_workspace}; use tests::{active_debug_session_panel, init_test, init_test_workspace};
use util::path; use util::path;
@ -1388,16 +1386,15 @@ async fn test_we_send_arguments_from_user_config(
let cx = &mut VisualTestContext::from_window(*workspace, cx); let cx = &mut VisualTestContext::from_window(*workspace, cx);
let debug_definition = DebugTaskDefinition { let debug_definition = DebugTaskDefinition {
adapter: "fake-adapter".into(), adapter: "fake-adapter".into(),
request: dap::DebugRequest::Launch(LaunchRequest { config: json!({
program: "main.rs".to_owned(), "request": "launch",
args: vec!["arg1".to_owned(), "arg2".to_owned()], "program": "main.rs".to_owned(),
cwd: Some(path!("/Random_path").into()), "args": vec!["arg1".to_owned(), "arg2".to_owned()],
env: HashMap::from_iter(vec![("KEY".to_owned(), "VALUE".to_owned())]), "cwd": path!("/Random_path"),
"env": json!({ "KEY": "VALUE" }),
}), }),
label: "test".into(), label: "test".into(),
initialize_args: None,
tcp_connection: None, tcp_connection: None,
stop_on_entry: None,
}; };
let launch_handler_called = Arc::new(AtomicBool::new(false)); let launch_handler_called = Arc::new(AtomicBool::new(false));

View file

@ -143,6 +143,8 @@ pub trait Extension: Send + Sync + 'static {
user_installed_path: Option<PathBuf>, user_installed_path: Option<PathBuf>,
worktree: Arc<dyn WorktreeDelegate>, worktree: Arc<dyn WorktreeDelegate>,
) -> Result<DebugAdapterBinary>; ) -> Result<DebugAdapterBinary>;
async fn dap_schema(&self) -> Result<serde_json::Value>;
} }
pub fn parse_wasm_extension_version( pub fn parse_wasm_extension_version(

View file

@ -20,7 +20,7 @@ pub use wit::{
make_file_executable, make_file_executable,
zed::extension::context_server::ContextServerConfiguration, zed::extension::context_server::ContextServerConfiguration,
zed::extension::dap::{ zed::extension::dap::{
DebugAdapterBinary, DebugRequest, DebugTaskDefinition, StartDebuggingRequestArguments, DebugAdapterBinary, DebugTaskDefinition, StartDebuggingRequestArguments,
StartDebuggingRequestArgumentsRequest, TcpArguments, TcpArgumentsTemplate, StartDebuggingRequestArgumentsRequest, TcpArguments, TcpArgumentsTemplate,
resolve_tcp_template, resolve_tcp_template,
}, },
@ -203,6 +203,10 @@ pub trait Extension: Send + Sync {
) -> Result<DebugAdapterBinary, String> { ) -> Result<DebugAdapterBinary, String> {
Err("`get_dap_binary` not implemented".to_string()) Err("`get_dap_binary` not implemented".to_string())
} }
fn dap_schema(&mut self) -> Result<serde_json::Value, String> {
Err("`dap_schema` not implemented".to_string())
}
} }
/// Registers the provided type as a Zed extension. /// Registers the provided type as a Zed extension.
@ -396,6 +400,10 @@ impl wit::Guest for Component {
) -> Result<wit::DebugAdapterBinary, String> { ) -> Result<wit::DebugAdapterBinary, String> {
extension().get_dap_binary(adapter_name, config, user_installed_path, worktree) extension().get_dap_binary(adapter_name, config, user_installed_path, worktree)
} }
fn dap_schema() -> Result<String, String> {
extension().dap_schema().map(|schema| schema.to_string())
}
} }
/// The ID of a language server. /// The ID of a language server.

View file

@ -35,9 +35,7 @@ interface dap {
record debug-task-definition { record debug-task-definition {
label: string, label: string,
adapter: string, adapter: string,
request: debug-request, config: string,
initialize-args: option<string>,
stop-on-entry: option<bool>,
tcp-connection: option<tcp-arguments-template>, tcp-connection: option<tcp-arguments-template>,
} }

View file

@ -134,6 +134,7 @@ world extension {
export labels-for-completions: func(language-server-id: string, completions: list<completion>) -> result<list<option<code-label>>, string>; export labels-for-completions: func(language-server-id: string, completions: list<completion>) -> result<list<option<code-label>>, string>;
export labels-for-symbols: func(language-server-id: string, symbols: list<symbol>) -> result<list<option<code-label>>, string>; export labels-for-symbols: func(language-server-id: string, symbols: list<symbol>) -> result<list<option<code-label>>, string>;
/// Returns the completions that should be shown when completing the provided slash command with the given query. /// Returns the completions that should be shown when completing the provided slash command with the given query.
export complete-slash-command-argument: func(command: slash-command, args: list<string>) -> result<list<slash-command-argument-completion>, string>; export complete-slash-command-argument: func(command: slash-command, args: list<string>) -> result<list<slash-command-argument-completion>, string>;
@ -158,4 +159,6 @@ world extension {
/// Returns a configured debug adapter binary for a given debug task. /// Returns a configured debug adapter binary for a given debug task.
export get-dap-binary: func(adapter-name: string, config: debug-task-definition, user-installed-path: option<string>, worktree: borrow<worktree>) -> result<debug-adapter-binary, string>; export get-dap-binary: func(adapter-name: string, config: debug-task-definition, user-installed-path: option<string>, worktree: borrow<worktree>) -> result<debug-adapter-binary, string>;
/// Get a debug adapter's configuration schema
export dap-schema: func() -> result<string, string>;
} }

View file

@ -398,6 +398,20 @@ impl extension::Extension for WasmExtension {
}) })
.await .await
} }
async fn dap_schema(&self) -> Result<serde_json::Value> {
self.call(|extension, store| {
async move {
extension
.call_dap_schema(store)
.await
.and_then(|schema| serde_json::to_value(schema).map_err(|err| err.to_string()))
.map_err(|err| anyhow!(err.to_string()))
}
.boxed()
})
.await
}
} }
pub struct WasmState { pub struct WasmState {
@ -710,100 +724,3 @@ impl CacheStore for IncrementalCompilationCache {
true true
} }
} }
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use extension::{
ExtensionCapability, ExtensionLibraryKind, LanguageServerManifestEntry, LibManifestEntry,
SchemaVersion,
extension_builder::{CompileExtensionOptions, ExtensionBuilder},
};
use gpui::TestAppContext;
use reqwest_client::ReqwestClient;
use super::*;
#[gpui::test]
fn test_cache_size_for_test_extension(cx: &TestAppContext) {
let cache_store = cache_store();
let engine = wasm_engine();
let wasm_bytes = wasm_bytes(cx, &mut manifest());
Component::new(&engine, wasm_bytes).unwrap();
cache_store.cache.run_pending_tasks();
let size: usize = cache_store
.cache
.iter()
.map(|(k, v)| k.len() + v.len())
.sum();
// If this assertion fails, it means extensions got larger and we may want to
// reconsider our cache size.
assert!(size < 512 * 1024);
}
fn wasm_bytes(cx: &TestAppContext, manifest: &mut ExtensionManifest) -> Vec<u8> {
let extension_builder = extension_builder();
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("extensions/test-extension");
cx.executor()
.block(extension_builder.compile_extension(
&path,
manifest,
CompileExtensionOptions { release: true },
))
.unwrap();
std::fs::read(path.join("extension.wasm")).unwrap()
}
fn extension_builder() -> ExtensionBuilder {
let user_agent = format!(
"Zed Extension CLI/{} ({}; {})",
env!("CARGO_PKG_VERSION"),
std::env::consts::OS,
std::env::consts::ARCH
);
let http_client = Arc::new(ReqwestClient::user_agent(&user_agent).unwrap());
// Local dir so that we don't have to download it on every run
let build_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("benches/.build");
ExtensionBuilder::new(http_client, build_dir)
}
fn manifest() -> ExtensionManifest {
ExtensionManifest {
id: "test-extension".into(),
name: "Test Extension".into(),
version: "0.1.0".into(),
schema_version: SchemaVersion(1),
description: Some("An extension for use in tests.".into()),
authors: Vec::new(),
repository: None,
themes: Default::default(),
icon_themes: Vec::new(),
lib: LibManifestEntry {
kind: Some(ExtensionLibraryKind::Rust),
version: Some(SemanticVersion::new(0, 1, 0)),
},
languages: Vec::new(),
grammars: BTreeMap::default(),
language_servers: [("gleam".into(), LanguageServerManifestEntry::default())]
.into_iter()
.collect(),
context_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: vec![ExtensionCapability::ProcessExec {
command: "echo".into(),
args: vec!["hello!".into()],
}],
debug_adapters: Vec::new(),
}
}
}

View file

@ -922,6 +922,20 @@ impl Extension {
_ => anyhow::bail!("`get_dap_binary` not available prior to v0.6.0"), _ => anyhow::bail!("`get_dap_binary` not available prior to v0.6.0"),
} }
} }
pub async fn call_dap_schema(&self, store: &mut Store<WasmState>) -> Result<String, String> {
match self {
Extension::V0_6_0(ext) => {
let schema = ext
.call_dap_schema(store)
.await
.map_err(|err| err.to_string())?;
schema
}
_ => Err("`get_dap_binary` not available prior to v0.6.0".to_string()),
}
}
} }
trait ToWasmtimeResult<T> { trait ToWasmtimeResult<T> {

View file

@ -1,7 +1,7 @@
use crate::wasm_host::wit::since_v0_6_0::{ use crate::wasm_host::wit::since_v0_6_0::{
dap::{ dap::{
AttachRequest, DebugRequest, LaunchRequest, StartDebuggingRequestArguments, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, TcpArguments,
StartDebuggingRequestArgumentsRequest, TcpArguments, TcpArgumentsTemplate, TcpArgumentsTemplate,
}, },
slash_command::SlashCommandOutputSection, slash_command::SlashCommandOutputSection,
}; };
@ -79,17 +79,6 @@ impl From<Command> for extension::Command {
} }
} }
impl From<extension::LaunchRequest> for LaunchRequest {
fn from(value: extension::LaunchRequest) -> Self {
Self {
program: value.program,
cwd: value.cwd.map(|path| path.to_string_lossy().into_owned()),
envs: value.env.into_iter().collect(),
args: value.args,
}
}
}
impl From<StartDebuggingRequestArgumentsRequest> impl From<StartDebuggingRequestArgumentsRequest>
for extension::StartDebuggingRequestArgumentsRequest for extension::StartDebuggingRequestArgumentsRequest
{ {
@ -129,32 +118,14 @@ impl From<extension::TcpArgumentsTemplate> for TcpArgumentsTemplate {
} }
} }
} }
impl From<extension::AttachRequest> for AttachRequest {
fn from(value: extension::AttachRequest) -> Self {
Self {
process_id: value.process_id,
}
}
}
impl From<extension::DebugRequest> for DebugRequest {
fn from(value: extension::DebugRequest) -> Self {
match value {
extension::DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()),
extension::DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()),
}
}
}
impl TryFrom<extension::DebugTaskDefinition> for DebugTaskDefinition { impl TryFrom<extension::DebugTaskDefinition> for DebugTaskDefinition {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(value: extension::DebugTaskDefinition) -> Result<Self, Self::Error> { fn try_from(value: extension::DebugTaskDefinition) -> Result<Self, Self::Error> {
let initialize_args = value.initialize_args.map(|s| s.to_string());
Ok(Self { Ok(Self {
label: value.label.to_string(), label: value.label.to_string(),
adapter: value.adapter.to_string(), adapter: value.adapter.to_string(),
request: value.request.into(), config: value.config.to_string(),
initialize_args,
stop_on_entry: value.stop_on_entry,
tcp_connection: value.tcp_connection.map(Into::into), tcp_connection: value.tcp_connection.map(Into::into),
}) })
} }

View file

@ -39,6 +39,7 @@ async-compression.workspace = true
async-tar.workspace = true async-tar.workspace = true
async-trait.workspace = true async-trait.workspace = true
collections.workspace = true collections.workspace = true
dap.workspace = true
futures.workspace = true futures.workspace = true
gpui.workspace = true gpui.workspace = true
http_client.workspace = true http_client.workspace = true

View file

@ -3,6 +3,7 @@ use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive; use async_tar::Archive;
use async_trait::async_trait; use async_trait::async_trait;
use collections::HashMap; use collections::HashMap;
use dap::DapRegistry;
use futures::StreamExt; use futures::StreamExt;
use gpui::{App, AsyncApp}; use gpui::{App, AsyncApp};
use http_client::github::{GitHubLspBinaryVersion, latest_github_release}; use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
@ -85,8 +86,10 @@ impl JsonLspAdapter {
}, },
cx, cx,
); );
let adapter_schemas = cx.global::<DapRegistry>().adapters_schema();
let tasks_schema = task::TaskTemplates::generate_json_schema(); let tasks_schema = task::TaskTemplates::generate_json_schema();
let debug_schema = task::DebugTaskFile::generate_json_schema(); let debug_schema = task::DebugTaskFile::generate_json_schema(&adapter_schemas);
let snippets_schema = snippet_provider::format::VsSnippetsFile::generate_json_schema(); let snippets_schema = snippet_provider::format::VsSnippetsFile::generate_json_schema();
let tsconfig_schema = serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap(); let tsconfig_schema = serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap();
let package_json_schema = serde_json::Value::from_str(PACKAGE_JSON_SCHEMA).unwrap(); let package_json_schema = serde_json::Value::from_str(PACKAGE_JSON_SCHEMA).unwrap();

View file

@ -195,10 +195,7 @@ impl DapStore {
.and_then(|s| s.binary.as_ref().map(PathBuf::from)); .and_then(|s| s.binary.as_ref().map(PathBuf::from));
let delegate = self.delegate(&worktree, console, cx); let delegate = self.delegate(&worktree, console, cx);
let cwd: Arc<Path> = definition let cwd: Arc<Path> = worktree.read(cx).abs_path().as_ref().into();
.cwd()
.unwrap_or(worktree.read(cx).abs_path().as_ref())
.into();
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let mut binary = adapter let mut binary = adapter
@ -416,9 +413,7 @@ impl DapStore {
})? })?
.await?; .await?;
if let Some(args) = definition.initialize_args { merge_json_value_into(definition.config, &mut binary.request_args.configuration);
merge_json_value_into(args, &mut binary.request_args.configuration);
}
session session
.update(cx, |session, cx| { .update(cx, |session, cx| {

View file

@ -82,10 +82,8 @@ impl DapLocator for CargoLocator {
task_template, task_template,
locator_name: Some(self.name()), locator_name: Some(self.name()),
}), }),
request: None, config: serde_json::Value::Null,
initialize_args: None,
tcp_connection: None, tcp_connection: None,
stop_on_entry: None,
}) })
} }

View file

@ -864,7 +864,7 @@ impl Session {
pub fn binary(&self) -> &DebugAdapterBinary { pub fn binary(&self) -> &DebugAdapterBinary {
let Mode::Running(local_mode) = &self.mode else { let Mode::Running(local_mode) = &self.mode else {
panic!("Session is not local"); panic!("Session is not running");
}; };
&local_mode.binary &local_mode.binary
} }

View file

@ -461,13 +461,8 @@ message DapModule {
message DebugTaskDefinition { message DebugTaskDefinition {
string adapter = 1; string adapter = 1;
string label = 2; string label = 2;
oneof request { string config = 3;
DebugLaunchRequest debug_launch_request = 3; optional TcpHost tcp_connection = 4;
DebugAttachRequest debug_attach_request = 4;
}
optional string initialize_args = 5;
optional TcpHost tcp_connection = 6;
optional bool stop_on_entry = 7;
} }
message TcpHost { message TcpHost {

View file

@ -0,0 +1,62 @@
use anyhow::Result;
use gpui::SharedString;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
/// Represents a schema for a specific adapter
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct AdapterSchema {
/// The adapter name identifier
pub adapter: SharedString,
/// The JSON schema for this adapter's configuration
pub schema: serde_json::Value,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct AdapterSchemas(pub Vec<AdapterSchema>);
impl AdapterSchemas {
pub fn generate_json_schema(&self) -> Result<serde_json_lenient::Value> {
let adapter_conditions = self
.0
.iter()
.map(|adapter_schema| {
let adapter_name = adapter_schema.adapter.to_string();
json!({
"if": {
"properties": {
"adapter": { "const": adapter_name }
}
},
"then": adapter_schema.schema
})
})
.collect::<Vec<_>>();
let schema = serde_json_lenient::json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Debug Adapter Configurations",
"description": "Configuration for debug adapters. Schema changes based on the selected adapter.",
"type": "array",
"items": {
"type": "object",
"required": ["adapter", "label"],
"properties": {
"adapter": {
"type": "string",
"description": "The name of the debug adapter"
},
"label": {
"type": "string",
"description": "The name of the debug configuration"
},
},
"allOf": adapter_conditions
}
});
Ok(serde_json_lenient::to_value(schema)?)
}
}

View file

@ -1,12 +1,12 @@
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use collections::FxHashMap; use collections::FxHashMap;
use gpui::SharedString; use gpui::SharedString;
use schemars::{JsonSchema, r#gen::SchemaSettings}; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::net::Ipv4Addr;
use std::path::PathBuf; use std::path::PathBuf;
use std::{net::Ipv4Addr, path::Path};
use crate::TaskTemplate; use crate::{TaskTemplate, adapter_schema::AdapterSchemas};
/// Represents the host information of the debug adapter /// Represents the host information of the debug adapter
#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
@ -106,7 +106,7 @@ impl LaunchRequest {
/// Represents the type that will determine which request to call on the debug adapter /// Represents the type that will determine which request to call on the debug adapter
#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
#[serde(rename_all = "lowercase", untagged)] #[serde(rename_all = "lowercase", tag = "request")]
pub enum DebugRequest { pub enum DebugRequest {
/// Call the `launch` request on the debug adapter /// Call the `launch` request on the debug adapter
Launch(LaunchRequest), Launch(LaunchRequest),
@ -193,8 +193,30 @@ pub enum BuildTaskDefinition {
locator_name: Option<SharedString>, locator_name: Option<SharedString>,
}, },
} }
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
pub enum Request {
Launch,
Attach,
}
/// This struct represent a user created debug task from the new session modal
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct ZedDebugConfig {
/// Name of the debug task
pub label: SharedString,
/// The debug adapter to use
pub adapter: SharedString,
#[serde(flatten)]
pub request: DebugRequest,
/// Whether to tell the debug adapter to stop on entry
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stop_on_entry: Option<bool>,
}
/// This struct represent a user created debug task /// This struct represent a user created debug task
#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct DebugScenario { pub struct DebugScenario {
pub adapter: SharedString, pub adapter: SharedString,
@ -203,11 +225,9 @@ pub struct DebugScenario {
/// A task to run prior to spawning the debuggee. /// A task to run prior to spawning the debuggee.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub build: Option<BuildTaskDefinition>, pub build: Option<BuildTaskDefinition>,
#[serde(flatten)] /// The main arguments to be sent to the debug adapter
pub request: Option<DebugRequest>, #[serde(default, flatten)]
/// Additional initialization arguments to be sent on DAP initialization pub config: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub initialize_args: Option<serde_json::Value>,
/// Optional TCP connection information /// Optional TCP connection information
/// ///
/// If provided, this will be used to connect to the debug adapter instead of /// If provided, this will be used to connect to the debug adapter instead of
@ -215,19 +235,6 @@ pub struct DebugScenario {
/// that is already running or is started by another process. /// that is already running or is started by another process.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub tcp_connection: Option<TcpArgumentsTemplate>, pub tcp_connection: Option<TcpArgumentsTemplate>,
/// Whether to tell the debug adapter to stop on entry
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stop_on_entry: Option<bool>,
}
impl DebugScenario {
pub fn cwd(&self) -> Option<&Path> {
if let Some(DebugRequest::Launch(config)) = &self.request {
config.cwd.as_ref().map(Path::new)
} else {
None
}
}
} }
/// A group of Debug Tasks defined in a JSON file. /// A group of Debug Tasks defined in a JSON file.
@ -237,32 +244,15 @@ pub struct DebugTaskFile(pub Vec<DebugScenario>);
impl DebugTaskFile { impl DebugTaskFile {
/// Generates JSON schema of Tasks JSON template format. /// Generates JSON schema of Tasks JSON template format.
pub fn generate_json_schema() -> serde_json_lenient::Value { pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json_lenient::Value {
let schema = SchemaSettings::draft07() schemas.generate_json_schema().unwrap_or_default()
.with(|settings| settings.option_add_null_type = false)
.into_generator()
.into_root_schema_for::<Self>();
serde_json_lenient::to_value(schema).unwrap()
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{DebugRequest, DebugScenario, LaunchRequest}; use crate::DebugScenario;
use serde_json::json;
#[test]
fn test_can_deserialize_non_attach_task() {
let deserialized: DebugRequest =
serde_json::from_str(r#"{"program": "cafebabe"}"#).unwrap();
assert_eq!(
deserialized,
DebugRequest::Launch(LaunchRequest {
program: "cafebabe".to_owned(),
..Default::default()
})
);
}
#[test] #[test]
fn test_empty_scenario_has_none_request() { fn test_empty_scenario_has_none_request() {
@ -273,7 +263,10 @@ mod tests {
}"#; }"#;
let deserialized: DebugScenario = serde_json::from_str(json).unwrap(); let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
assert_eq!(deserialized.request, None);
assert_eq!(json!({}), deserialized.config);
assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
assert_eq!("Build & debug rust", deserialized.label.as_ref());
} }
#[test] #[test]
@ -281,18 +274,19 @@ mod tests {
let json = r#"{ let json = r#"{
"label": "Launch program", "label": "Launch program",
"adapter": "CodeLLDB", "adapter": "CodeLLDB",
"request": "launch",
"program": "target/debug/myapp", "program": "target/debug/myapp",
"args": ["--test"] "args": ["--test"]
}"#; }"#;
let deserialized: DebugScenario = serde_json::from_str(json).unwrap(); let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
match deserialized.request {
Some(DebugRequest::Launch(launch)) => { assert_eq!(
assert_eq!(launch.program, "target/debug/myapp"); json!({ "request": "launch", "program": "target/debug/myapp", "args": ["--test"] }),
assert_eq!(launch.args, vec!["--test"]); deserialized.config
} );
_ => panic!("Expected Launch request"), assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
} assert_eq!("Launch program", deserialized.label.as_ref());
} }
#[test] #[test]
@ -300,15 +294,17 @@ mod tests {
let json = r#"{ let json = r#"{
"label": "Attach to process", "label": "Attach to process",
"adapter": "CodeLLDB", "adapter": "CodeLLDB",
"process_id": 1234 "process_id": 1234,
"request": "attach"
}"#; }"#;
let deserialized: DebugScenario = serde_json::from_str(json).unwrap(); let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
match deserialized.request {
Some(DebugRequest::Attach(attach)) => { assert_eq!(
assert_eq!(attach.process_id, Some(1234)); json!({ "request": "attach", "process_id": 1234 }),
} deserialized.config
_ => panic!("Expected Attach request"), );
} assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
assert_eq!("Attach to process", deserialized.label.as_ref());
} }
} }

View file

@ -1,5 +1,6 @@
//! Baseline interface of Tasks in Zed: all tasks in Zed are intended to use those for implementing their own logic. //! Baseline interface of Tasks in Zed: all tasks in Zed are intended to use those for implementing their own logic.
mod adapter_schema;
mod debug_format; mod debug_format;
mod serde_helpers; mod serde_helpers;
pub mod static_source; pub mod static_source;
@ -15,14 +16,14 @@ use std::borrow::Cow;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
pub use adapter_schema::{AdapterSchema, AdapterSchemas};
pub use debug_format::{ pub use debug_format::{
AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest,
TcpArgumentsTemplate, Request, TcpArgumentsTemplate, ZedDebugConfig,
}; };
pub use task_template::{ pub use task_template::{
DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates, DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates,
substitute_all_template_variables_in_str, substitute_variables_in_map, substitute_variables_in_map, substitute_variables_in_str,
substitute_variables_in_str,
}; };
pub use vscode_debug_format::VsCodeDebugTaskFile; pub use vscode_debug_format::VsCodeDebugTaskFile;
pub use vscode_format::VsCodeTaskFile; pub use vscode_format::VsCodeTaskFile;

View file

@ -315,7 +315,7 @@ pub fn substitute_variables_in_str(template_str: &str, context: &TaskContext) ->
&mut substituted_variables, &mut substituted_variables,
) )
} }
pub fn substitute_all_template_variables_in_str<A: AsRef<str>>( fn substitute_all_template_variables_in_str<A: AsRef<str>>(
template_str: &str, template_str: &str,
task_variables: &HashMap<String, A>, task_variables: &HashMap<String, A>,
variable_names: &HashMap<String, VariableName>, variable_names: &HashMap<String, VariableName>,

View file

@ -1,14 +1,10 @@
use std::path::PathBuf;
use anyhow::Context as _;
use collections::HashMap; use collections::HashMap;
use gpui::SharedString; use gpui::SharedString;
use serde::Deserialize; use serde::Deserialize;
use util::ResultExt as _; use util::ResultExt as _;
use crate::{ use crate::{
AttachRequest, DebugRequest, DebugScenario, DebugTaskFile, EnvVariableReplacer, LaunchRequest, DebugScenario, DebugTaskFile, EnvVariableReplacer, TcpArgumentsTemplate, VariableName,
TcpArgumentsTemplate, VariableName,
}; };
#[derive(Clone, Debug, Deserialize, PartialEq)] #[derive(Clone, Debug, Deserialize, PartialEq)]
@ -40,7 +36,7 @@ struct VsCodeDebugTaskDefinition {
#[serde(default)] #[serde(default)]
stop_on_entry: Option<bool>, stop_on_entry: Option<bool>,
#[serde(flatten)] #[serde(flatten)]
other_attributes: HashMap<String, serde_json_lenient::Value>, other_attributes: serde_json::Value,
} }
impl VsCodeDebugTaskDefinition { impl VsCodeDebugTaskDefinition {
@ -50,33 +46,6 @@ impl VsCodeDebugTaskDefinition {
let definition = DebugScenario { let definition = DebugScenario {
label, label,
build: None, build: None,
request: match self.request {
Request::Launch => {
let cwd = self.cwd.map(|cwd| PathBuf::from(replacer.replace(&cwd)));
let program = self
.program
.context("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();
let env = self
.env
.into_iter()
.filter_map(|(k, v)| v.map(|v| (k, v)))
.collect();
DebugRequest::Launch(LaunchRequest {
program,
cwd,
args,
env,
})
.into()
}
Request::Attach => DebugRequest::Attach(AttachRequest { process_id: None }).into(),
},
adapter: task_type_to_adapter_name(&self.r#type), adapter: task_type_to_adapter_name(&self.r#type),
// TODO host? // TODO host?
tcp_connection: self.port.map(|port| TcpArgumentsTemplate { tcp_connection: self.port.map(|port| TcpArgumentsTemplate {
@ -84,9 +53,7 @@ impl VsCodeDebugTaskDefinition {
host: None, host: None,
timeout: None, timeout: None,
}), }),
stop_on_entry: self.stop_on_entry, config: self.other_attributes,
// TODO
initialize_args: None,
}; };
Ok(definition) Ok(definition)
} }
@ -135,10 +102,9 @@ fn task_type_to_adapter_name(task_type: &str) -> SharedString {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use serde_json::json;
use collections::FxHashMap; use crate::{DebugScenario, DebugTaskFile, TcpArgumentsTemplate};
use crate::{DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, TcpArgumentsTemplate};
use super::VsCodeDebugTaskFile; use super::VsCodeDebugTaskFile;
@ -173,19 +139,14 @@ mod tests {
DebugTaskFile(vec![DebugScenario { DebugTaskFile(vec![DebugScenario {
label: "Debug my JS app".into(), label: "Debug my JS app".into(),
adapter: "JavaScript".into(), adapter: "JavaScript".into(),
stop_on_entry: Some(true), config: json!({
initialize_args: None, "showDevDebugOutput": false,
}),
tcp_connection: Some(TcpArgumentsTemplate { tcp_connection: Some(TcpArgumentsTemplate {
port: Some(17), port: Some(17),
host: None, host: None,
timeout: None, timeout: None,
}), }),
request: Some(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()),
env: FxHashMap::from_iter([("X".into(), "Y".into())])
})),
build: None build: None
}]) }])
); );

View file

@ -32,6 +32,7 @@ regex.workspace = true
rust-embed.workspace = true rust-embed.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
serde_json_lenient.workspace = true
smol.workspace = true smol.workspace = true
take-until.workspace = true take-until.workspace = true
tempfile = { workspace = true, optional = true } tempfile = { workspace = true, optional = true }

View file

@ -401,6 +401,31 @@ pub fn parse_env_output(env: &str, mut f: impl FnMut(String, String)) {
} }
} }
pub fn merge_json_lenient_value_into(
source: serde_json_lenient::Value,
target: &mut serde_json_lenient::Value,
) {
match (source, target) {
(serde_json_lenient::Value::Object(source), serde_json_lenient::Value::Object(target)) => {
for (key, value) in source {
if let Some(target) = target.get_mut(&key) {
merge_json_lenient_value_into(value, target);
} else {
target.insert(key, value);
}
}
}
(serde_json_lenient::Value::Array(source), serde_json_lenient::Value::Array(target)) => {
for value in source {
target.push(value);
}
}
(source, target) => *target = source,
}
}
pub fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) { pub fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) {
use serde_json::Value; use serde_json::Value;