use anyhow::Result; use schemars::{JsonSchema, r#gen::SchemaSettings}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::{net::Ipv4Addr, path::Path}; use crate::{TaskTemplate, TaskType, task_template::DebugArgs}; /// 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, /// The host that the debug adapter is listening too /// /// Default: 127.0.0.1 pub host: Option, /// The max amount of time in milliseconds to connect to a tcp DAP before returning an error /// /// Default: 2000ms pub timeout: Option, } impl TcpArgumentsTemplate { /// Get the host or fallback to the default host pub fn host(&self) -> Ipv4Addr { self.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)) } pub fn from_proto(proto: proto::TcpHost) -> Result { 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, Deserialize, 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, } /// 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 pub cwd: Option, /// Arguments to pass to a debuggee #[serde(default)] pub args: Vec, } /// 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)] pub enum DebugRequest { /// Call the `launch` request on the debug adapter Launch(LaunchRequest), /// Call the `attach` request on the debug adapter Attach(AttachRequest), } impl From for DebugRequest { fn from(launch_config: LaunchRequest) -> Self { DebugRequest::Launch(launch_config) } } impl From for DebugRequest { fn from(attach_config: AttachRequest) -> Self { DebugRequest::Attach(attach_config) } } impl TryFrom for DebugTaskTemplate { type Error = (); fn try_from(value: TaskTemplate) -> Result { let TaskType::Debug(debug_args) = value.task_type else { return Err(()); }; let request = match debug_args.request { crate::DebugArgsRequest::Launch => DebugRequest::Launch(LaunchRequest { program: value.command, cwd: value.cwd.map(PathBuf::from), args: value.args, }), crate::DebugArgsRequest::Attach(attach_config) => DebugRequest::Attach(attach_config), }; Ok(DebugTaskTemplate { locator: debug_args.locator, definition: DebugTaskDefinition { adapter: debug_args.adapter, request, label: value.label, initialize_args: debug_args.initialize_args, tcp_connection: debug_args.tcp_connection, stop_on_entry: debug_args.stop_on_entry, }, }) } } impl DebugTaskTemplate { /// Translate from debug definition to a task template pub fn to_zed_format(self) -> TaskTemplate { let (command, cwd, request) = match self.definition.request { DebugRequest::Launch(launch_config) => ( launch_config.program, launch_config .cwd .map(|cwd| cwd.to_string_lossy().to_string()), crate::task_template::DebugArgsRequest::Launch, ), DebugRequest::Attach(attach_config) => ( "".to_owned(), None, crate::task_template::DebugArgsRequest::Attach(attach_config), ), }; let task_type = TaskType::Debug(DebugArgs { adapter: self.definition.adapter, request, initialize_args: self.definition.initialize_args, locator: self.locator, tcp_connection: self.definition.tcp_connection, stop_on_entry: self.definition.stop_on_entry, }); let label = self.definition.label.clone(); TaskTemplate { label, command, args: vec![], task_type, cwd, ..Default::default() } } } #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] pub struct DebugTaskTemplate { pub locator: Option, #[serde(flatten)] pub definition: DebugTaskDefinition, } /// This struct represent a user created debug task #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] pub struct DebugTaskDefinition { /// The adapter to run pub adapter: String, /// The type of request that should be called on the debug adapter #[serde(flatten)] pub request: DebugRequest, /// Name of the debug task pub label: String, /// Additional initialization arguments to be sent on DAP initialization pub initialize_args: Option, /// 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. pub tcp_connection: Option, /// Whether to tell the debug adapter to stop on entry pub stop_on_entry: Option, } impl DebugTaskDefinition { pub fn cwd(&self) -> Option<&Path> { if let DebugRequest::Launch(config) = &self.request { config.cwd.as_deref() } else { None } } pub fn to_proto(&self) -> proto::DebugTaskDefinition { proto::DebugTaskDefinition { adapter: self.adapter.clone(), 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(), }, ) } DebugRequest::Attach(attach_request) => { proto::debug_task_definition::Request::DebugAttachRequest( proto::DebugAttachRequest { process_id: attach_request.process_id.unwrap_or_default(), }, ) } }), label: self.label.clone(), 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 { let request = proto .request .ok_or_else(|| anyhow::anyhow!("request is required"))?; Ok(Self { label: proto.label, initialize_args: proto.initialize_args.map(|v| v.into()), tcp_connection: proto .tcp_connection .map(TcpArgumentsTemplate::from_proto) .transpose()?, stop_on_entry: proto.stop_on_entry, adapter: proto.adapter.clone(), 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, }) } }, }) } } /// 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); 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::(); serde_json_lenient::to_value(schema).unwrap() } } #[cfg(test)] mod tests { use crate::{DebugRequest, 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() }) ); } }