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

@ -1,5 +1,5 @@
use ::fs::Fs;
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, anyhow};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
@ -22,7 +22,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use task::{AttachRequest, DebugRequest, DebugScenario, LaunchRequest, TcpArgumentsTemplate};
use task::{DebugScenario, TcpArgumentsTemplate, ZedDebugConfig};
use util::archive::extract_zip;
#[derive(Clone, Debug, PartialEq, Eq)]
@ -131,13 +131,12 @@ impl TcpArguments {
derive(serde::Deserialize, serde::Serialize)
)]
pub struct DebugTaskDefinition {
/// The name of this debug task
pub label: SharedString,
/// The debug adapter to use
pub adapter: DebugAdapterName,
pub request: DebugRequest,
/// Additional initialization arguments to be sent on DAP initialization
pub initialize_args: Option<serde_json::Value>,
/// Whether to tell the debug adapter to stop on entry
pub stop_on_entry: Option<bool>,
/// The configuration to send to the debug adapter
pub config: serde_json::Value,
/// Optional TCP connection information
///
/// If provided, this will be used to connect to the debug adapter instead of
@ -147,86 +146,34 @@ pub struct 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 {
DebugScenario {
label: self.label.clone(),
adapter: self.adapter.clone().into(),
build: None,
request: Some(self.request.clone()),
stop_on_entry: self.stop_on_entry,
tcp_connection: self.tcp_connection.clone(),
initialize_args: self.initialize_args.clone(),
config: self.config.clone(),
}
}
pub fn to_proto(&self) -> proto::DebugTaskDefinition {
proto::DebugTaskDefinition {
adapter: self.adapter.to_string(),
request: Some(match &self.request {
DebugRequest::Launch(config) => {
proto::debug_task_definition::Request::DebugLaunchRequest(
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,
label: self.label.clone().into(),
config: self.config.to_string(),
tcp_connection: self.tcp_connection.clone().map(|v| v.to_proto()),
adapter: self.adapter.clone().0.into(),
}
}
pub fn from_proto(proto: proto::DebugTaskDefinition) -> Result<Self> {
let request = proto.request.context("request is required")?;
Ok(Self {
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
.map(TcpArgumentsTemplate::from_proto)
.transpose()?,
stop_on_entry: proto.stop_on_entry,
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 {
fn name(&self) -> DebugAdapterName;
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario>;
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
@ -419,6 +368,25 @@ pub trait DebugAdapter: 'static + Send + Sync {
fn adapter_language_name(&self) -> Option<LanguageName> {
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"))]
@ -432,29 +400,29 @@ impl FakeAdapter {
Self {}
}
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
fn request_args(
&self,
task_definition: &DebugTaskDefinition,
) -> StartDebuggingRequestArguments {
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!({
"request": match config.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
"process_id": if let DebugRequest::Attach(attach_config) = &config.request {
attach_config.process_id
} else {
None
},
"raw_request": serde_json::to_value(config).unwrap()
"request": request_variant,
"process_id": obj.get("process_id"),
"raw_request": serde_json::to_value(task_definition).unwrap()
});
let request = match config.request {
DebugRequest::Launch(_) => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
DebugRequest::Attach(_) => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
};
StartDebuggingRequestArguments {
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())
}
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(
&self,
_: &Arc<dyn DapDelegate>,
@ -479,7 +482,7 @@ impl DebugAdapter for FakeAdapter {
connection: None,
envs: HashMap::default(),
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 language::LanguageName;
use parking_lot::RwLock;
use task::{DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate};
use task::{
AdapterSchema, AdapterSchemas, DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate,
};
use crate::{
adapters::{DebugAdapter, DebugAdapterName},
@ -41,14 +43,7 @@ impl Global for DapRegistry {}
impl DapRegistry {
pub fn global(cx: &mut App) -> &mut Self {
let ret = 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
cx.default_global::<Self>()
}
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(
&self,
language: String,