Something went wrong. Try again.
fork of gpui
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535use anyhow::{Context as _, Result};use collections::FxHashMap;use gpui::SharedString;use log as _;use schemars::JsonSchema;use serde::{Deserialize, Serialize};use std::net::IpAddr;use std::path::PathBuf;use util::{debug_panic, schemars::add_new_subschema};
use crate::{TaskTemplate, adapter_schema::AdapterSchemas};
/// Represents the host information of the debug adapter#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]pub struct TcpArgumentsTemplate { /// The port that the debug adapter is listening on /// /// Default: We will try to find an open port pub port: Option<u16>, /// The host that the debug adapter is listening too /// /// Default: 127.0.0.1 pub host: Option<IpAddr>, /// The max amount of time in milliseconds to connect to a tcp DAP before returning an error /// /// Default: 2000ms pub timeout: Option<u64>,}
impl TcpArgumentsTemplate { /// Get the host or fallback to the default host pub fn host(&self) -> IpAddr { self.host .unwrap_or(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)) }
pub fn from_proto(proto: proto::TcpHost) -> Result<Self> { Ok(Self { port: proto.port.map(|p| p.try_into()).transpose()?, host: proto.host.map(|h| h.parse()).transpose()?, timeout: proto.timeout, }) }
pub fn to_proto(&self) -> proto::TcpHost { proto::TcpHost { port: self.port.map(|p| p.into()), host: self.host.map(|h| h.to_string()), timeout: self.timeout, } }}
/// Represents the attach request information of the debug adapter#[derive(Default, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]pub struct AttachRequest { /// The processId to attach to, if left empty we will show a process picker pub process_id: Option<u32>,}
impl<'de> Deserialize<'de> for AttachRequest { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, { #[derive(Deserialize)] struct Helper { process_id: Option<u32>, }
let helper = Helper::deserialize(deserializer)?;
// Skip creating an AttachRequest if process_id is None if helper.process_id.is_none() { return Err(serde::de::Error::custom("process_id is required")); }
Ok(AttachRequest { process_id: helper.process_id, }) }}
/// Represents the launch request information of the debug adapter#[derive(Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, Clone, Debug)]pub struct LaunchRequest { /// The program that you trying to debug pub program: String, /// The current working directory of your project #[serde(default)] pub cwd: Option<PathBuf>, /// Arguments to pass to a debuggee #[serde(default)] pub args: Vec<String>, #[serde(default)] pub env: FxHashMap<String, String>,}
impl LaunchRequest { pub fn env_json(&self) -> serde_json::Value { serde_json::Value::Object( self.env .iter() .map(|(k, v)| (k.clone(), v.to_owned().into())) .collect::<serde_json::Map<String, serde_json::Value>>(), ) }}
/// 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", tag = "request")]pub enum DebugRequest { /// Call the `launch` request on the debug adapter Launch(LaunchRequest), /// Call the `attach` request on the debug adapter Attach(AttachRequest),}
impl DebugRequest { pub fn to_proto(&self) -> proto::DebugRequest { match self { DebugRequest::Launch(launch_request) => proto::DebugRequest { request: Some(proto::debug_request::Request::DebugLaunchRequest( proto::DebugLaunchRequest { program: launch_request.program.clone(), cwd: launch_request .cwd .as_ref() .map(|cwd| cwd.to_string_lossy().into_owned()), args: launch_request.args.clone(), env: launch_request .env .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(), }, )), }, DebugRequest::Attach(attach_request) => proto::DebugRequest { request: Some(proto::debug_request::Request::DebugAttachRequest( proto::DebugAttachRequest { process_id: attach_request .process_id .expect("The process ID to be already filled out."), }, )), }, } }
pub fn from_proto(val: proto::DebugRequest) -> Result<DebugRequest> { let request = val.request.context("Missing debug request")?; match request { proto::debug_request::Request::DebugLaunchRequest(proto::DebugLaunchRequest { program, cwd, args, env, }) => Ok(DebugRequest::Launch(LaunchRequest { program, cwd: cwd.map(From::from), args, env: env.into_iter().collect(), })),
proto::debug_request::Request::DebugAttachRequest(proto::DebugAttachRequest { process_id, }) => Ok(DebugRequest::Attach(AttachRequest { process_id: Some(process_id), })), } }}
impl From<LaunchRequest> for DebugRequest { fn from(launch_config: LaunchRequest) -> Self { DebugRequest::Launch(launch_config) }}
impl From<AttachRequest> for DebugRequest { fn from(attach_config: AttachRequest) -> Self { DebugRequest::Attach(attach_config) }}
#[derive(Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]#[serde(untagged)]pub enum BuildTaskDefinition { ByName(SharedString), Template { #[serde(flatten)] task_template: TaskTemplate, #[serde(skip)] locator_name: Option<SharedString>, },}
impl<'de> Deserialize<'de> for BuildTaskDefinition { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, { #[derive(Deserialize)] struct TemplateHelper { #[serde(default)] label: Option<String>, #[serde(flatten)] rest: serde_json::Value, }
let value = serde_json::Value::deserialize(deserializer)?;
if let Ok(name) = serde_json::from_value::<SharedString>(value.clone()) { return Ok(BuildTaskDefinition::ByName(name)); }
let helper: TemplateHelper = serde_json::from_value(value).map_err(serde::de::Error::custom)?;
let mut template_value = helper.rest; if let serde_json::Value::Object(ref mut map) = template_value { map.insert( "label".to_string(), serde_json::to_value(helper.label.unwrap_or_else(|| "debug-build".to_owned())) .map_err(serde::de::Error::custom)?, ); }
let task_template: TaskTemplate = serde_json::from_value(template_value).map_err(serde::de::Error::custom)?;
Ok(BuildTaskDefinition::Template { task_template, locator_name: None, }) }}
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]pub enum Request { Launch, Attach,}
/// This struct represent a user created debug task from the new process 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, Clone, Debug, JsonSchema)]#[serde(rename_all = "snake_case")]pub struct DebugScenario { pub adapter: SharedString, /// Name of the debug task pub label: SharedString, /// A task to run prior to spawning the debuggee. #[serde(default, skip_serializing_if = "Option::is_none")] pub build: Option<BuildTaskDefinition>, /// 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 /// spawning a new process. This is useful for connecting to a debug adapter /// that is already running or is started by another process. #[serde(default, skip_serializing_if = "Option::is_none")] pub tcp_connection: Option<TcpArgumentsTemplate>,}
/// A group of Debug Tasks defined in a JSON file.#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]#[serde(transparent)]pub struct DebugTaskFile(pub Vec<DebugScenario>);
impl DebugTaskFile { pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json::Value { let mut generator = schemars::generate::SchemaSettings::draft2019_09().into_generator();
let mut build_task_value = BuildTaskDefinition::json_schema(&mut generator).to_value();
if let Some(template_object) = build_task_value .get_mut("anyOf") .and_then(|array| array.as_array_mut()) .and_then(|array| array.get_mut(1)) { if let Some(properties) = template_object .get_mut("properties") .and_then(|value| value.as_object_mut()) && properties.remove("label").is_none() { debug_panic!( "Generated TaskTemplate json schema did not have expected 'label' field. \ Schema of 2nd alternative is: {template_object:?}" ); }
if let Some(arr) = template_object .get_mut("required") .and_then(|array| array.as_array_mut()) { arr.retain(|v| v.as_str() != Some("label")); } } else { debug_panic!( "Generated TaskTemplate json schema did not match expectations. \ Schema is: {build_task_value:?}" ); }
let adapter_conditions = schemas .0 .iter() .map(|adapter_schema| { let adapter_name = adapter_schema.adapter.to_string(); add_new_subschema( &mut generator, &format!("{adapter_name}DebugSettings"), serde_json::json!({ "if": { "properties": { "adapter": { "const": adapter_name } } }, "then": adapter_schema.schema }), ) }) .collect::<Vec<_>>();
let build_task_definition_ref = add_new_subschema( &mut generator, BuildTaskDefinition::schema_name().as_ref(), build_task_value, );
let meta_schema = generator .settings() .meta_schema .as_ref() .expect("meta_schema should be present in schemars settings") .to_string();
serde_json::json!({ "$schema": meta_schema, "title": "Debug Configurations", "description": "Configuration for debug scenarios", "allowTrailingCommas": true, "type": "array", "items": { "type": "object", "required": ["adapter", "label"], // TODO: Uncommenting this will cause json-language-server to provide warnings for // unrecognized properties. It should be enabled if/when there's an adapter JSON // schema that's comprehensive. In order to not get warnings for the other schemas, // `additionalProperties` or `unevaluatedProperties` (to handle "allOf" etc style // schema combinations) could be set to `true` for that schema. // // "unevaluatedProperties": false, "properties": { "adapter": { "type": "string", "description": "The name of the debug adapter" }, "label": { "type": "string", "description": "The name of the debug configuration" }, "build": build_task_definition_ref, "tcp_connection": { "type": "object", "description": "Optional TCP connection information for connecting to an already running debug adapter", "properties": { "port": { "type": "integer", "description": "The port that the debug adapter is listening on (default: auto-find open port)" }, "host": { "type": "string", "description": "The host that the debug adapter is listening to, as an IPv4 or IPv6 address (default: 127.0.0.1)" }, "timeout": { "type": "integer", "description": "The max amount of time in milliseconds to connect to a tcp DAP before returning an error (default: 2000ms)" } } } }, "allOf": adapter_conditions }, "$defs": generator.take_definitions(true), }) }}
#[cfg(test)]mod tests { use crate::DebugScenario; use serde_json::json;
#[test] fn test_just_build_args() { let json = r#"{ "label": "Build & debug rust", "adapter": "CodeLLDB", "build": { "command": "rust", "args": ["build"] } }"#;
let deserialized: DebugScenario = serde_json::from_str(json).unwrap(); assert!(deserialized.build.is_some()); match deserialized.build.as_ref().unwrap() { crate::BuildTaskDefinition::Template { task_template, .. } => { assert_eq!("debug-build", task_template.label); assert_eq!("rust", task_template.command); assert_eq!(vec!["build"], task_template.args); } _ => panic!("Expected Template variant"), } assert_eq!(json!({}), deserialized.config); assert_eq!("CodeLLDB", deserialized.adapter.as_ref()); assert_eq!("Build & debug rust", deserialized.label.as_ref()); }
#[test] fn test_empty_scenario_has_none_request() { let json = r#"{ "label": "Build & debug rust", "build": "rust", "adapter": "CodeLLDB" }"#;
let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
assert_eq!(json!({}), deserialized.config); assert_eq!("CodeLLDB", deserialized.adapter.as_ref()); assert_eq!("Build & debug rust", deserialized.label.as_ref()); }
#[test] fn test_launch_scenario_deserialization() { 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();
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] fn test_attach_scenario_deserialization() { let json = r#"{ "label": "Attach to process", "adapter": "CodeLLDB", "process_id": 1234, "request": "attach" }"#;
let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
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()); }
#[test] fn test_build_task_definition_without_label() { use crate::BuildTaskDefinition;
let json = r#""my_build_task""#; let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap(); match deserialized { BuildTaskDefinition::ByName(name) => assert_eq!("my_build_task", name.as_ref()), _ => panic!("Expected ByName variant"), }
let json = r#"{ "command": "cargo", "args": ["build", "--release"] }"#; let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap(); match deserialized { BuildTaskDefinition::Template { task_template, .. } => { assert_eq!("debug-build", task_template.label); assert_eq!("cargo", task_template.command); assert_eq!(vec!["build", "--release"], task_template.args); } _ => panic!("Expected Template variant"), }
let json = r#"{ "label": "Build Release", "command": "cargo", "args": ["build", "--release"] }"#; let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap(); match deserialized { BuildTaskDefinition::Template { task_template, .. } => { assert_eq!("Build Release", task_template.label); assert_eq!("cargo", task_template.command); assert_eq!(vec!["build", "--release"], task_template.args); } _ => panic!("Expected Template variant"), } }}