use anyhow::{Context as _, Result}; use collections::FxHashMap; use gpui::SharedString; use schemars::{JsonSchema, r#gen::SchemaSettings}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::{net::Ipv4Addr, path::Path}; use crate::TaskTemplate; /// 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, 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, } impl<'de> Deserialize<'de> for AttachRequest { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { #[derive(Deserialize)] struct Helper { process_id: Option, } 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, /// Arguments to pass to a debuggee #[serde(default)] pub args: Vec, #[serde(default)] pub env: FxHashMap, } 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::>(), ) } } /// 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 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 { 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 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) } } #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(untagged)] pub enum BuildTaskDefinition { ByName(SharedString), Template { #[serde(flatten)] task_template: TaskTemplate, #[serde(skip)] locator_name: Option, }, } /// This struct represent a user created debug task #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[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, #[serde(flatten)] pub request: Option, /// Additional initialization arguments to be sent on DAP initialization #[serde(default, skip_serializing_if = "Option::is_none")] 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. #[serde(default, skip_serializing_if = "Option::is_none")] pub tcp_connection: Option, /// Whether to tell the debug adapter to stop on entry #[serde(default, skip_serializing_if = "Option::is_none")] pub stop_on_entry: Option, } 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. #[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, 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() }) ); } #[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!(deserialized.request, None); } #[test] fn test_launch_scenario_deserialization() { let json = r#"{ "label": "Launch program", "adapter": "CodeLLDB", "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"), } } #[test] fn test_attach_scenario_deserialization() { let json = r#"{ "label": "Attach to process", "adapter": "CodeLLDB", "process_id": 1234 }"#; 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"), } } }