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

@ -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 collections::FxHashMap;
use gpui::SharedString;
use schemars::{JsonSchema, r#gen::SchemaSettings};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::net::Ipv4Addr;
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
#[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
#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
#[serde(rename_all = "lowercase", untagged)]
#[serde(rename_all = "lowercase", tag = "request")]
pub enum DebugRequest {
/// Call the `launch` request on the debug adapter
Launch(LaunchRequest),
@ -193,8 +193,30 @@ pub enum BuildTaskDefinition {
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
#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct DebugScenario {
pub adapter: SharedString,
@ -203,11 +225,9 @@ pub struct DebugScenario {
/// A task to run prior to spawning the debuggee.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build: Option<BuildTaskDefinition>,
#[serde(flatten)]
pub request: Option<DebugRequest>,
/// Additional initialization arguments to be sent on DAP initialization
#[serde(default, skip_serializing_if = "Option::is_none")]
pub initialize_args: Option<serde_json::Value>,
/// The main arguments to be sent to the debug adapter
#[serde(default, flatten)]
pub config: serde_json::Value,
/// Optional TCP connection information
///
/// 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.
#[serde(default, skip_serializing_if = "Option::is_none")]
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.
@ -237,32 +244,15 @@ pub struct DebugTaskFile(pub Vec<DebugScenario>);
impl DebugTaskFile {
/// Generates JSON schema of Tasks JSON template format.
pub fn generate_json_schema() -> serde_json_lenient::Value {
let schema = SchemaSettings::draft07()
.with(|settings| settings.option_add_null_type = false)
.into_generator()
.into_root_schema_for::<Self>();
serde_json_lenient::to_value(schema).unwrap()
pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json_lenient::Value {
schemas.generate_json_schema().unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use crate::{DebugRequest, DebugScenario, LaunchRequest};
#[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()
})
);
}
use crate::DebugScenario;
use serde_json::json;
#[test]
fn test_empty_scenario_has_none_request() {
@ -273,7 +263,10 @@ mod tests {
}"#;
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]
@ -281,18 +274,19 @@ mod tests {
let json = r#"{
"label": "Launch program",
"adapter": "CodeLLDB",
"request": "launch",
"program": "target/debug/myapp",
"args": ["--test"]
}"#;
let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
match deserialized.request {
Some(DebugRequest::Launch(launch)) => {
assert_eq!(launch.program, "target/debug/myapp");
assert_eq!(launch.args, vec!["--test"]);
}
_ => panic!("Expected Launch request"),
}
assert_eq!(
json!({ "request": "launch", "program": "target/debug/myapp", "args": ["--test"] }),
deserialized.config
);
assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
assert_eq!("Launch program", deserialized.label.as_ref());
}
#[test]
@ -300,15 +294,17 @@ mod tests {
let json = r#"{
"label": "Attach to process",
"adapter": "CodeLLDB",
"process_id": 1234
"process_id": 1234,
"request": "attach"
}"#;
let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
match deserialized.request {
Some(DebugRequest::Attach(attach)) => {
assert_eq!(attach.process_id, Some(1234));
}
_ => panic!("Expected Attach request"),
}
assert_eq!(
json!({ "request": "attach", "process_id": 1234 }),
deserialized.config
);
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.
mod adapter_schema;
mod debug_format;
mod serde_helpers;
pub mod static_source;
@ -15,14 +16,14 @@ use std::borrow::Cow;
use std::path::PathBuf;
use std::str::FromStr;
pub use adapter_schema::{AdapterSchema, AdapterSchemas};
pub use debug_format::{
AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest,
TcpArgumentsTemplate,
Request, TcpArgumentsTemplate, ZedDebugConfig,
};
pub use task_template::{
DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates,
substitute_all_template_variables_in_str, substitute_variables_in_map,
substitute_variables_in_str,
substitute_variables_in_map, substitute_variables_in_str,
};
pub use vscode_debug_format::VsCodeDebugTaskFile;
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,
)
}
pub fn substitute_all_template_variables_in_str<A: AsRef<str>>(
fn substitute_all_template_variables_in_str<A: AsRef<str>>(
template_str: &str,
task_variables: &HashMap<String, A>,
variable_names: &HashMap<String, VariableName>,

View file

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