logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

63
crates/dap/Cargo.toml Normal file
View File

@@ -0,0 +1,63 @@
[package]
name = "dap"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/dap.rs"
doctest = false
[features]
test-support = [
"gpui/test-support",
"util/test-support",
"task/test-support",
"async-pipe",
"settings/test-support",
]
[dependencies]
anyhow.workspace = true
async-compression.workspace = true
async-pipe = { workspace = true, optional = true }
async-tar.workspace = true
async-trait.workspace = true
client.workspace = true
collections.workspace = true
dap-types.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true
language.workspace = true
log.workspace = true
node_runtime.workspace = true
parking_lot.workspace = true
paths.workspace = true
proto.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smallvec.workspace = true
smol.workspace = true
task.workspace = true
telemetry.workspace = true
util.workspace = true
[target.'cfg(not(windows))'.dependencies]
libc.workspace = true
[dev-dependencies]
async-pipe.workspace = true
gpui = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
task = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
zlog.workspace = true

1
crates/dap/LICENSE-GPL Symbolic link
View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,9 @@
# Overview
The active `Project` is responsible for maintain opened and closed breakpoints
as well as serializing breakpoints to save. At a high level project serializes
the positions of breakpoints that don't belong to any active buffers and handles
converting breakpoints from serializing to active whenever a buffer is opened/closed.
`Project` also handles sending all relevant breakpoint information to debug adapter's
during debugging or when starting a debugger.

483
crates/dap/src/adapters.rs Normal file
View File

@@ -0,0 +1,483 @@
use anyhow::{Context as _, Result, anyhow};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use collections::HashMap;
pub use dap_types::{StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest};
use fs::Fs;
use futures::io::BufReader;
use gpui::{AsyncApp, SharedString};
pub use http_client::{HttpClient, github::latest_github_release};
use language::{LanguageName, LanguageToolchainStore};
use node_runtime::NodeRuntime;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::WorktreeId;
use smol::fs::File;
use std::{
borrow::Borrow,
ffi::OsStr,
fmt::Debug,
net::IpAddr,
ops::Deref,
path::{Path, PathBuf},
sync::Arc,
};
use task::{DebugScenario, TcpArgumentsTemplate, ZedDebugConfig};
use util::{archive::extract_zip, rel_path::RelPath};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DapStatus {
None,
CheckingForUpdate,
Downloading,
Failed { error: String },
}
#[async_trait]
pub trait DapDelegate: Send + Sync + 'static {
fn worktree_id(&self) -> WorktreeId;
fn worktree_root_path(&self) -> &Path;
fn http_client(&self) -> Arc<dyn HttpClient>;
fn node_runtime(&self) -> NodeRuntime;
fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore>;
fn fs(&self) -> Arc<dyn Fs>;
fn output_to_console(&self, msg: String);
async fn which(&self, command: &OsStr) -> Option<PathBuf>;
async fn read_text_file(&self, path: &RelPath) -> Result<String>;
async fn shell_env(&self) -> collections::HashMap<String, String>;
fn is_headless(&self) -> bool;
}
#[derive(
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, JsonSchema,
)]
#[serde(transparent)]
pub struct DebugAdapterName(pub SharedString);
impl Deref for DebugAdapterName {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<str> for DebugAdapterName {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Borrow<str> for DebugAdapterName {
fn borrow(&self) -> &str {
&self.0
}
}
impl Borrow<SharedString> for DebugAdapterName {
fn borrow(&self) -> &SharedString {
&self.0
}
}
impl std::fmt::Display for DebugAdapterName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
impl From<DebugAdapterName> for SharedString {
fn from(name: DebugAdapterName) -> Self {
name.0
}
}
impl From<SharedString> for DebugAdapterName {
fn from(name: SharedString) -> Self {
DebugAdapterName(name)
}
}
impl<'a> From<&'a str> for DebugAdapterName {
fn from(str: &'a str) -> DebugAdapterName {
DebugAdapterName(str.to_string().into())
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct TcpArguments {
pub host: IpAddr,
pub port: u16,
pub timeout: Option<u64>,
}
impl TcpArguments {
pub fn from_proto(proto: proto::TcpHost) -> anyhow::Result<Self> {
let host = TcpArgumentsTemplate::from_proto(proto)?;
Ok(TcpArguments {
host: host.host.context("missing host")?,
port: host.port.context("missing port")?,
timeout: host.timeout,
})
}
pub fn to_proto(&self) -> proto::TcpHost {
TcpArgumentsTemplate {
host: Some(self.host),
port: Some(self.port),
timeout: self.timeout,
}
.to_proto()
}
}
/// Represents a debuggable binary/process (what process is going to be debugged and with what arguments).
///
/// We start off with a [DebugScenario], a user-facing type that additionally defines how a debug target is built; once
/// an optional build step is completed, we turn it's result into a DebugTaskDefinition by running a locator (or using a user-provided task) and resolving task variables.
/// Finally, a [DebugTaskDefinition] has to be turned into a concrete debugger invocation ([DebugAdapterBinary]).
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
any(feature = "test-support", test),
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,
/// 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
/// spawning a new debug adapter process. This is useful for connecting to a debug adapter
/// that is already running or is started by another process.
pub tcp_connection: Option<TcpArgumentsTemplate>,
}
impl DebugTaskDefinition {
pub fn to_scenario(&self) -> DebugScenario {
DebugScenario {
label: self.label.clone(),
adapter: self.adapter.clone().into(),
build: None,
tcp_connection: self.tcp_connection.clone(),
config: self.config.clone(),
}
}
pub fn to_proto(&self) -> proto::DebugTaskDefinition {
proto::DebugTaskDefinition {
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> {
Ok(Self {
label: proto.label.into(),
config: serde_json::from_str(&proto.config)?,
tcp_connection: proto
.tcp_connection
.map(TcpArgumentsTemplate::from_proto)
.transpose()?,
adapter: DebugAdapterName(proto.adapter.into()),
})
}
}
/// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct DebugAdapterBinary {
pub command: Option<String>,
pub arguments: Vec<String>,
pub envs: HashMap<String, String>,
pub cwd: Option<PathBuf>,
pub connection: Option<TcpArguments>,
pub request_args: StartDebuggingRequestArguments,
}
impl DebugAdapterBinary {
pub fn from_proto(binary: proto::DebugAdapterBinary) -> anyhow::Result<Self> {
let request = match binary.launch_type() {
proto::debug_adapter_binary::LaunchType::Launch => {
StartDebuggingRequestArgumentsRequest::Launch
}
proto::debug_adapter_binary::LaunchType::Attach => {
StartDebuggingRequestArgumentsRequest::Attach
}
};
Ok(DebugAdapterBinary {
command: binary.command,
arguments: binary.arguments,
envs: binary.envs.into_iter().collect(),
connection: binary
.connection
.map(TcpArguments::from_proto)
.transpose()?,
request_args: StartDebuggingRequestArguments {
configuration: serde_json::from_str(&binary.configuration)?,
request,
},
cwd: binary.cwd.map(|cwd| cwd.into()),
})
}
pub fn to_proto(&self) -> proto::DebugAdapterBinary {
proto::DebugAdapterBinary {
command: self.command.clone(),
arguments: self.arguments.clone(),
envs: self
.envs
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
cwd: self
.cwd
.as_ref()
.map(|cwd| cwd.to_string_lossy().into_owned()),
connection: self.connection.as_ref().map(|c| c.to_proto()),
launch_type: match self.request_args.request {
StartDebuggingRequestArgumentsRequest::Launch => {
proto::debug_adapter_binary::LaunchType::Launch.into()
}
StartDebuggingRequestArgumentsRequest::Attach => {
proto::debug_adapter_binary::LaunchType::Attach.into()
}
},
configuration: self.request_args.configuration.to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct AdapterVersion {
pub tag_name: String,
pub url: String,
}
pub enum DownloadedFileType {
Vsix,
GzipTar,
Zip,
}
pub struct GithubRepo {
pub repo_name: String,
pub repo_owner: String,
}
pub async fn download_adapter_from_github(
adapter_name: DebugAdapterName,
github_version: AdapterVersion,
file_type: DownloadedFileType,
delegate: &dyn DapDelegate,
) -> Result<PathBuf> {
let adapter_path = paths::debug_adapters_dir().join(&adapter_name.as_ref());
let version_path = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name));
let fs = delegate.fs();
if version_path.exists() {
return Ok(version_path);
}
if !adapter_path.exists() {
fs.create_dir(adapter_path.as_path())
.await
.context("Failed creating adapter path")?;
}
log::debug!(
"Downloading adapter {} from {}",
adapter_name,
&github_version.url,
);
delegate.output_to_console(format!("Downloading from {}...", github_version.url));
let mut response = delegate
.http_client()
.get(&github_version.url, Default::default(), true)
.await
.context("Error downloading release")?;
anyhow::ensure!(
response.status().is_success(),
"download failed with status {}",
response.status()
);
delegate.output_to_console("Download complete".to_owned());
match file_type {
DownloadedFileType::GzipTar => {
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
let archive = Archive::new(decompressed_bytes);
archive.unpack(&version_path).await?;
}
DownloadedFileType::Zip | DownloadedFileType::Vsix => {
let zip_path = version_path.with_extension("zip");
let mut file = File::create(&zip_path).await?;
futures::io::copy(response.body_mut(), &mut file).await?;
let file = File::open(&zip_path).await?;
extract_zip(&version_path, file)
.await
// we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
.inspect_err(|e| log::warn!("ZIP extraction error: {}. Ignoring...", e))
.ok();
util::fs::remove_matching(&adapter_path, |entry| {
entry
.file_name()
.is_some_and(|file| file.to_string_lossy().ends_with(".zip"))
})
.await;
}
}
// remove older versions
util::fs::remove_matching(&adapter_path, |entry| {
entry.to_string_lossy() != version_path.to_string_lossy()
})
.await;
Ok(version_path)
}
#[async_trait(?Send)]
pub trait DebugAdapter: 'static + Send + Sync {
fn name(&self) -> DebugAdapterName;
async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario>;
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
user_args: Option<Vec<String>>,
user_env: Option<HashMap<String, String>>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary>;
/// Returns the language name of an adapter if it only supports one language
fn adapter_language_name(&self) -> Option<LanguageName> {
None
}
/// Extracts the kind (attach/launch) of debug configuration from the given JSON config.
/// This method should only return error when the kind cannot be determined for a given configuration;
/// in particular, it *should not* validate whether the request as a whole is valid, because that's best left to the debug adapter itself to decide.
async fn request_kind(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
match config.get("request") {
Some(val) if val == "launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
Some(val) if val == "attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
_ => Err(anyhow!(
"missing or invalid `request` field in config. Expected 'launch' or 'attach'"
)),
}
}
fn dap_schema(&self) -> serde_json::Value;
fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> {
None
}
fn compact_child_session(&self) -> bool {
false
}
fn prefer_thread_name(&self) -> bool {
false
}
}
#[cfg(any(test, feature = "test-support"))]
pub struct FakeAdapter {}
#[cfg(any(test, feature = "test-support"))]
impl FakeAdapter {
pub const ADAPTER_NAME: &'static str = "fake-adapter";
pub fn new() -> Self {
Self {}
}
}
#[cfg(any(test, feature = "test-support"))]
#[async_trait(?Send)]
impl DebugAdapter for FakeAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn dap_schema(&self) -> serde_json::Value {
serde_json::Value::Null
}
async fn request_kind(
&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
}
async 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>,
task_definition: &DebugTaskDefinition,
_: Option<PathBuf>,
_: Option<Vec<String>>,
_: Option<HashMap<String, String>>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
let connection = task_definition
.tcp_connection
.as_ref()
.map(|connection| TcpArguments {
host: connection.host(),
port: connection.port.unwrap_or(17),
timeout: connection.timeout,
});
Ok(DebugAdapterBinary {
command: Some("command".into()),
arguments: vec![],
connection,
envs: HashMap::default(),
cwd: None,
request_args: StartDebuggingRequestArguments {
request: self.request_kind(&task_definition.config).await?,
configuration: task_definition.config.clone(),
},
})
}
}

465
crates/dap/src/client.rs Normal file
View File

@@ -0,0 +1,465 @@
use crate::{
adapters::DebugAdapterBinary,
transport::{IoKind, LogKind, TransportDelegate},
};
use anyhow::Result;
use dap_types::{
messages::{Message, Response},
requests::Request,
};
use futures::channel::oneshot;
use gpui::AsyncApp;
use std::{
hash::Hash,
sync::atomic::{AtomicU64, Ordering},
};
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct SessionId(pub u32);
impl SessionId {
pub fn from_proto(client_id: u64) -> Self {
Self(client_id as u32)
}
pub fn to_proto(self) -> u64 {
self.0 as u64
}
}
/// Represents a connection to the debug adapter process, either via stdout/stdin or a socket.
pub struct DebugAdapterClient {
id: SessionId,
sequence_count: AtomicU64,
binary: DebugAdapterBinary,
transport_delegate: TransportDelegate,
}
pub type DapMessageHandler = Box<dyn FnMut(Message) + 'static + Send + Sync>;
impl DebugAdapterClient {
pub async fn start(
id: SessionId,
binary: DebugAdapterBinary,
message_handler: DapMessageHandler,
cx: &mut AsyncApp,
) -> Result<Self> {
let transport_delegate = TransportDelegate::start(&binary, cx).await?;
let this = Self {
id,
binary,
transport_delegate,
sequence_count: AtomicU64::new(1),
};
this.connect(message_handler, cx).await?;
Ok(this)
}
pub fn should_reconnect_for_ssh(&self) -> bool {
self.transport_delegate.tcp_arguments().is_some()
&& (self.binary.command.as_deref() == Some("ssh")
|| (cfg!(feature = "test-support")
&& self.binary.command.as_deref() == Some("mock")))
}
pub async fn connect(
&self,
message_handler: DapMessageHandler,
cx: &mut AsyncApp,
) -> Result<()> {
self.transport_delegate.connect(message_handler, cx).await
}
pub async fn create_child_connection(
&self,
session_id: SessionId,
binary: DebugAdapterBinary,
message_handler: DapMessageHandler,
cx: &mut AsyncApp,
) -> Result<Self> {
let binary = if let Some(connection) = self.transport_delegate.tcp_arguments() {
DebugAdapterBinary {
command: None,
arguments: Default::default(),
envs: Default::default(),
cwd: Default::default(),
connection: Some(connection),
request_args: binary.request_args,
}
} else {
self.binary.clone()
};
Self::start(session_id, binary, message_handler, cx).await
}
/// Send a request to an adapter and get a response back
/// Note: This function will block until a response is sent back from the adapter
pub async fn request<R: Request>(&self, arguments: R::Arguments) -> Result<R::Response> {
let serialized_arguments = serde_json::to_value(arguments)?;
let (callback_tx, callback_rx) = oneshot::channel::<Result<Response>>();
let sequence_id = self.next_sequence_id();
let request = crate::messages::Request {
seq: sequence_id,
command: R::COMMAND.to_string(),
arguments: Some(serialized_arguments),
};
self.transport_delegate
.pending_requests
.lock()
.insert(sequence_id, callback_tx)?;
log::debug!(
"Client {} send `{}` request with sequence_id: {}",
self.id.0,
R::COMMAND,
sequence_id
);
log::debug!(" request: {request:?}");
self.send_message(Message::Request(request)).await?;
let command = R::COMMAND.to_string();
let response = callback_rx.await??;
log::debug!(
"Client {} received response for: `{}` sequence_id: {}",
self.id.0,
command,
sequence_id
);
log::debug!(" response: {response:?}");
match response.success {
true => {
if let Some(json) = response.body {
Ok(serde_json::from_value(json)?)
// Note: dap types configure themselves to return `None` when an empty object is received,
// which then fails here...
} else if let Ok(result) =
serde_json::from_value(serde_json::Value::Object(Default::default()))
{
Ok(result)
} else {
Ok(serde_json::from_value(Default::default())?)
}
}
false => anyhow::bail!("Request failed: {}", response.message.unwrap_or_default()),
}
}
pub async fn send_message(&self, message: Message) -> Result<()> {
self.transport_delegate.send_message(message).await
}
pub fn id(&self) -> SessionId {
self.id
}
pub fn binary(&self) -> &DebugAdapterBinary {
&self.binary
}
/// Get the next sequence id to be used in a request
pub fn next_sequence_id(&self) -> u64 {
self.sequence_count.fetch_add(1, Ordering::Relaxed)
}
pub fn kill(&self) {
log::debug!("Killing DAP process");
self.transport_delegate.transport.lock().kill();
self.transport_delegate.pending_requests.lock().shutdown();
}
pub fn has_adapter_logs(&self) -> bool {
self.transport_delegate.has_adapter_logs()
}
pub fn add_log_handler<F>(&self, f: F, kind: LogKind)
where
F: 'static + Send + FnMut(IoKind, Option<&str>, &str),
{
self.transport_delegate.add_log_handler(f, kind);
}
#[cfg(any(test, feature = "test-support"))]
pub fn on_request<R: dap_types::requests::Request, F>(&self, mut handler: F)
where
F: 'static
+ Send
+ FnMut(u64, R::Arguments) -> Result<R::Response, dap_types::ErrorResponse>,
{
use crate::transport::RequestHandling;
self.transport_delegate
.transport
.lock()
.as_fake()
.on_request::<R, _>(move |seq, request| {
RequestHandling::Respond(handler(seq, request))
});
}
#[cfg(any(test, feature = "test-support"))]
pub fn on_request_ext<R: dap_types::requests::Request, F>(&self, handler: F)
where
F: 'static
+ Send
+ FnMut(
u64,
R::Arguments,
) -> crate::transport::RequestHandling<
Result<R::Response, dap_types::ErrorResponse>,
>,
{
self.transport_delegate
.transport
.lock()
.as_fake()
.on_request::<R, F>(handler);
}
#[cfg(any(test, feature = "test-support"))]
pub async fn fake_reverse_request<R: dap_types::requests::Request>(&self, args: R::Arguments) {
self.send_message(Message::Request(dap_types::messages::Request {
seq: self.sequence_count.load(Ordering::Relaxed),
command: R::COMMAND.into(),
arguments: serde_json::to_value(args).ok(),
}))
.await
.unwrap();
}
#[cfg(any(test, feature = "test-support"))]
pub async fn on_response<R: dap_types::requests::Request, F>(&self, handler: F)
where
F: 'static + Send + Fn(Response),
{
self.transport_delegate
.transport
.lock()
.as_fake()
.on_response::<R, F>(handler);
}
#[cfg(any(test, feature = "test-support"))]
pub async fn fake_event(&self, event: dap_types::messages::Events) {
self.send_message(Message::Event(Box::new(event)))
.await
.unwrap();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::DebugAdapterClient;
use dap_types::{
Capabilities, InitializeRequestArguments, InitializeRequestArgumentsPathFormat,
RunInTerminalRequestArguments, StartDebuggingRequestArguments,
messages::Events,
requests::{Initialize, Request, RunInTerminal},
};
use gpui::TestAppContext;
use serde_json::json;
use settings::SettingsStore;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
pub fn init_test(cx: &mut gpui::TestAppContext) {
zlog::init_test();
cx.update(|cx| {
let settings = SettingsStore::test(cx);
cx.set_global(settings);
});
}
#[gpui::test]
pub async fn test_initialize_client(cx: &mut TestAppContext) {
#![expect(clippy::result_large_err)]
init_test(cx);
let client = DebugAdapterClient::start(
crate::client::SessionId(1),
DebugAdapterBinary {
command: Some("command".into()),
arguments: Default::default(),
envs: Default::default(),
connection: None,
cwd: None,
request_args: StartDebuggingRequestArguments {
configuration: serde_json::Value::Null,
request: dap_types::StartDebuggingRequestArgumentsRequest::Launch,
},
},
Box::new(|_| {}),
&mut cx.to_async(),
)
.await
.unwrap();
client.on_request::<Initialize, _>(move |_, _| {
Ok(dap_types::Capabilities {
supports_configuration_done_request: Some(true),
..Default::default()
})
});
cx.run_until_parked();
let response = client
.request::<Initialize>(InitializeRequestArguments {
client_id: Some("zed".to_owned()),
client_name: Some("Zed".to_owned()),
adapter_id: "fake-adapter".to_owned(),
locale: Some("en-US".to_owned()),
path_format: Some(InitializeRequestArgumentsPathFormat::Path),
supports_variable_type: Some(true),
supports_variable_paging: Some(false),
supports_run_in_terminal_request: Some(true),
supports_memory_references: Some(true),
supports_progress_reporting: Some(false),
supports_invalidated_event: Some(false),
lines_start_at1: Some(true),
columns_start_at1: Some(true),
supports_memory_event: Some(false),
supports_args_can_be_interpreted_by_shell: Some(false),
supports_start_debugging_request: Some(true),
supports_ansistyling: Some(false),
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
dap_types::Capabilities {
supports_configuration_done_request: Some(true),
..Default::default()
},
response
);
}
#[gpui::test]
pub async fn test_calls_event_handler(cx: &mut TestAppContext) {
init_test(cx);
let called_event_handler = Arc::new(AtomicBool::new(false));
let client = DebugAdapterClient::start(
crate::client::SessionId(1),
DebugAdapterBinary {
command: Some("command".into()),
arguments: Default::default(),
envs: Default::default(),
connection: None,
cwd: None,
request_args: StartDebuggingRequestArguments {
configuration: serde_json::Value::Null,
request: dap_types::StartDebuggingRequestArgumentsRequest::Launch,
},
},
Box::new({
let called_event_handler = called_event_handler.clone();
move |event| {
called_event_handler.store(true, Ordering::SeqCst);
assert_eq!(
Message::Event(Box::new(Events::Initialized(
Some(Capabilities::default())
))),
event
);
}
}),
&mut cx.to_async(),
)
.await
.unwrap();
cx.run_until_parked();
client
.fake_event(Events::Initialized(Some(Capabilities::default())))
.await;
cx.run_until_parked();
assert!(
called_event_handler.load(std::sync::atomic::Ordering::SeqCst),
"Event handler was not called"
);
}
#[gpui::test]
pub async fn test_calls_event_handler_for_reverse_request(cx: &mut TestAppContext) {
init_test(cx);
let called_event_handler = Arc::new(AtomicBool::new(false));
let client = DebugAdapterClient::start(
crate::client::SessionId(1),
DebugAdapterBinary {
command: Some("command".into()),
arguments: Default::default(),
envs: Default::default(),
connection: None,
cwd: None,
request_args: dap_types::StartDebuggingRequestArguments {
configuration: serde_json::Value::Null,
request: dap_types::StartDebuggingRequestArgumentsRequest::Launch,
},
},
Box::new({
let called_event_handler = called_event_handler.clone();
move |event| {
called_event_handler.store(true, Ordering::SeqCst);
assert_eq!(
Message::Request(dap_types::messages::Request {
seq: 1,
command: RunInTerminal::COMMAND.into(),
arguments: Some(json!({
"cwd": "/project/path/src",
"args": ["node", "test.js"],
}))
}),
event
);
}
}),
&mut cx.to_async(),
)
.await
.unwrap();
cx.run_until_parked();
client
.fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
kind: None,
title: None,
cwd: "/project/path/src".into(),
args: vec!["node".into(), "test.js".into()],
env: None,
args_can_be_interpreted_by_shell: None,
})
.await;
cx.run_until_parked();
assert!(
called_event_handler.load(std::sync::atomic::Ordering::SeqCst),
"Event handler was not called"
);
}
}

76
crates/dap/src/dap.rs Normal file
View File

@@ -0,0 +1,76 @@
pub mod adapters;
pub mod client;
pub mod debugger_settings;
pub mod inline_value;
pub mod proto_conversions;
mod registry;
pub mod transport;
use std::net::IpAddr;
pub use dap_types::*;
use debugger_settings::DebuggerSettings;
use gpui::App;
pub use registry::{DapLocator, DapRegistry};
use serde::Serialize;
use settings::Settings;
pub use task::DebugRequest;
pub type ScopeId = u64;
pub type VariableReference = u64;
pub type StackFrameId = u64;
#[cfg(any(test, feature = "test-support"))]
pub use adapters::FakeAdapter;
use task::{DebugScenario, TcpArgumentsTemplate};
pub async fn configure_tcp_connection(
tcp_connection: TcpArgumentsTemplate,
) -> anyhow::Result<(IpAddr, u16, Option<u64>)> {
let host = tcp_connection.host();
let timeout = tcp_connection.timeout;
let port = if let Some(port) = tcp_connection.port {
port
} else {
transport::TcpTransport::port(&tcp_connection).await?
};
Ok((host, port, timeout))
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TelemetrySpawnLocation {
Gutter,
ScenarioList,
Custom,
}
pub fn send_telemetry(scenario: &DebugScenario, location: TelemetrySpawnLocation, cx: &App) {
let Some(adapter) = cx.global::<DapRegistry>().adapter(&scenario.adapter) else {
return;
};
let dock = DebuggerSettings::get_global(cx).dock;
let config = scenario.config.clone();
let with_build_task = scenario.build.is_some();
let adapter_name = scenario.adapter.clone();
cx.spawn(async move |_| {
let kind = adapter
.request_kind(&config)
.await
.ok()
.map(serde_json::to_value)
.and_then(Result::ok);
telemetry::event!(
"Debugger Session Started",
spawn_location = location,
with_build_task = with_build_task,
kind = kind,
adapter = adapter_name,
dock_position = dock,
);
})
.detach();
}

View File

@@ -0,0 +1,61 @@
use dap_types::SteppingGranularity;
use settings::{RegisterSetting, Settings, SettingsContent};
#[derive(Debug, RegisterSetting)]
pub struct DebuggerSettings {
/// Determines the stepping granularity.
///
/// Default: line
pub stepping_granularity: SteppingGranularity,
/// Whether the breakpoints should be reused across Zed sessions.
///
/// Default: true
pub save_breakpoints: bool,
/// Whether to show the debug button in the status bar.
///
/// Default: true
pub button: bool,
/// Time in milliseconds until timeout error when connecting to a TCP debug adapter
///
/// Default: 2000ms
pub timeout: u64,
/// Whether to log messages between active debug adapters and Zed
///
/// Default: true
pub log_dap_communications: bool,
/// Whether to format dap messages in when adding them to debug adapter logger
///
/// Default: true
pub format_dap_log_messages: bool,
/// The dock position of the debug panel
///
/// Default: Bottom
pub dock: settings::DockPosition,
}
impl Settings for DebuggerSettings {
fn from_settings(content: &SettingsContent) -> Self {
let content = content.debugger.clone().unwrap();
Self {
stepping_granularity: dap_granularity_from_settings(
content.stepping_granularity.unwrap(),
),
save_breakpoints: content.save_breakpoints.unwrap(),
button: content.button.unwrap(),
timeout: content.timeout.unwrap(),
log_dap_communications: content.log_dap_communications.unwrap(),
format_dap_log_messages: content.format_dap_log_messages.unwrap(),
dock: content.dock.unwrap(),
}
}
}
fn dap_granularity_from_settings(
granularity: settings::SteppingGranularity,
) -> dap_types::SteppingGranularity {
match granularity {
settings::SteppingGranularity::Instruction => dap_types::SteppingGranularity::Instruction,
settings::SteppingGranularity::Line => dap_types::SteppingGranularity::Line,
settings::SteppingGranularity::Statement => dap_types::SteppingGranularity::Statement,
}
}

View File

@@ -0,0 +1,20 @@
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VariableLookupKind {
Variable,
Expression,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VariableScope {
Local,
Global,
}
#[derive(Debug, Clone)]
pub struct InlineValueLocation {
pub variable_name: String,
pub scope: VariableScope,
pub lookup: VariableLookupKind,
pub row: usize,
pub column: usize,
}

View File

@@ -0,0 +1,578 @@
use anyhow::{Context as _, Result};
use client::proto::{
self, DapChecksum, DapChecksumAlgorithm, DapEvaluateContext, DapModule, DapScope,
DapScopePresentationHint, DapSource, DapSourcePresentationHint, DapStackFrame, DapVariable,
};
use dap_types::{OutputEventCategory, OutputEventGroup, ScopePresentationHint, Source};
pub trait ProtoConversion {
type ProtoType;
type Output;
fn to_proto(self) -> Self::ProtoType;
fn from_proto(payload: Self::ProtoType) -> Self::Output;
}
impl<T> ProtoConversion for Vec<T>
where
T: ProtoConversion<Output = T>,
{
type ProtoType = Vec<T::ProtoType>;
type Output = Self;
fn to_proto(self) -> Self::ProtoType {
self.into_iter().map(|item| item.to_proto()).collect()
}
fn from_proto(payload: Self::ProtoType) -> Self {
payload
.into_iter()
.map(|item| T::from_proto(item))
.collect()
}
}
impl ProtoConversion for dap_types::Scope {
type ProtoType = DapScope;
type Output = Self;
fn to_proto(self) -> Self::ProtoType {
Self::ProtoType {
name: self.name,
presentation_hint: self.presentation_hint.map(|hint| hint.to_proto().into()),
variables_reference: self.variables_reference,
named_variables: self.named_variables,
indexed_variables: self.indexed_variables,
expensive: self.expensive,
source: self.source.map(Source::to_proto),
line: self.line,
end_line: self.end_line,
column: self.column,
end_column: self.end_column,
}
}
fn from_proto(payload: Self::ProtoType) -> Self {
let presentation_hint = payload
.presentation_hint
.and_then(DapScopePresentationHint::from_i32);
Self {
name: payload.name,
presentation_hint: presentation_hint.map(ScopePresentationHint::from_proto),
variables_reference: payload.variables_reference,
named_variables: payload.named_variables,
indexed_variables: payload.indexed_variables,
expensive: payload.expensive,
source: payload.source.map(dap_types::Source::from_proto),
line: payload.line,
end_line: payload.end_line,
column: payload.column,
end_column: payload.end_column,
}
}
}
impl ProtoConversion for dap_types::Variable {
type ProtoType = DapVariable;
type Output = Self;
fn to_proto(self) -> Self::ProtoType {
Self::ProtoType {
name: self.name,
value: self.value,
r#type: self.type_,
evaluate_name: self.evaluate_name,
variables_reference: self.variables_reference,
named_variables: self.named_variables,
indexed_variables: self.indexed_variables,
memory_reference: self.memory_reference,
}
}
fn from_proto(payload: Self::ProtoType) -> Self {
Self {
name: payload.name,
value: payload.value,
type_: payload.r#type,
evaluate_name: payload.evaluate_name,
presentation_hint: None, // TODO Debugger Collab Add this
variables_reference: payload.variables_reference,
named_variables: payload.named_variables,
indexed_variables: payload.indexed_variables,
memory_reference: payload.memory_reference,
declaration_location_reference: None, // TODO
value_location_reference: None, // TODO
}
}
}
impl ProtoConversion for dap_types::ScopePresentationHint {
type ProtoType = DapScopePresentationHint;
type Output = Self;
fn to_proto(self) -> Self::ProtoType {
match self {
Self::Locals => Self::ProtoType::Locals,
Self::Arguments => Self::ProtoType::Arguments,
Self::Registers => Self::ProtoType::Registers,
Self::ReturnValue => Self::ProtoType::ReturnValue,
Self::Unknown => Self::ProtoType::ScopeUnknown,
_ => unreachable!(),
}
}
fn from_proto(payload: Self::ProtoType) -> Self {
match payload {
Self::ProtoType::Locals => Self::Locals,
Self::ProtoType::Arguments => Self::Arguments,
Self::ProtoType::Registers => Self::Registers,
Self::ProtoType::ReturnValue => Self::ReturnValue,
Self::ProtoType::ScopeUnknown => Self::Unknown,
}
}
}
impl ProtoConversion for dap_types::SourcePresentationHint {
type ProtoType = DapSourcePresentationHint;
type Output = Self;
fn to_proto(self) -> Self::ProtoType {
match self {
Self::Normal => Self::ProtoType::SourceNormal,
Self::Emphasize => Self::ProtoType::Emphasize,
Self::Deemphasize => Self::ProtoType::Deemphasize,
Self::Unknown => Self::ProtoType::SourceUnknown,
}
}
fn from_proto(payload: Self::ProtoType) -> Self {
match payload {
Self::ProtoType::SourceNormal => Self::Normal,
Self::ProtoType::Emphasize => Self::Emphasize,
Self::ProtoType::Deemphasize => Self::Deemphasize,
Self::ProtoType::SourceUnknown => Self::Unknown,
}
}
}
impl ProtoConversion for dap_types::Checksum {
type ProtoType = DapChecksum;
type Output = Self;
fn to_proto(self) -> Self::ProtoType {
DapChecksum {
algorithm: self.algorithm.to_proto().into(),
checksum: self.checksum,
}
}
fn from_proto(payload: Self::ProtoType) -> Self {
Self {
algorithm: dap_types::ChecksumAlgorithm::from_proto(payload.algorithm()),
checksum: payload.checksum,
}
}
}
impl ProtoConversion for dap_types::ChecksumAlgorithm {
type ProtoType = DapChecksumAlgorithm;
type Output = Self;
fn to_proto(self) -> Self::ProtoType {
match self {
Self::Md5 => DapChecksumAlgorithm::Md5,
Self::Sha1 => DapChecksumAlgorithm::Sha1,
Self::Sha256 => DapChecksumAlgorithm::Sha256,
Self::Timestamp => DapChecksumAlgorithm::Timestamp,
}
}
fn from_proto(payload: Self::ProtoType) -> Self {
match payload {
Self::ProtoType::Md5 => Self::Md5,
Self::ProtoType::Sha1 => Self::Sha1,
Self::ProtoType::Sha256 => Self::Sha256,
Self::ProtoType::Timestamp => Self::Timestamp,
Self::ProtoType::ChecksumAlgorithmUnspecified => unreachable!(),
}
}
}
impl ProtoConversion for dap_types::Source {
type ProtoType = DapSource;
type Output = Self;
fn to_proto(self) -> Self::ProtoType {
Self::ProtoType {
name: self.name,
path: self.path,
source_reference: self.source_reference,
presentation_hint: self.presentation_hint.map(|hint| hint.to_proto().into()),
origin: self.origin,
sources: self.sources.map(|src| src.to_proto()).unwrap_or_default(),
adapter_data: Default::default(), // TODO Debugger Collab
checksums: self.checksums.map(|c| c.to_proto()).unwrap_or_default(),
}
}
fn from_proto(payload: Self::ProtoType) -> Self {
Self {
name: payload.name,
path: payload.path,
source_reference: payload.source_reference,
presentation_hint: payload
.presentation_hint
.and_then(DapSourcePresentationHint::from_i32)
.map(dap_types::SourcePresentationHint::from_proto),
origin: payload.origin,
sources: Some(Vec::<dap_types::Source>::from_proto(payload.sources)),
checksums: Some(Vec::<dap_types::Checksum>::from_proto(payload.checksums)),
adapter_data: None, // TODO Debugger Collab
}
}
}
impl ProtoConversion for dap_types::StackFrame {
type ProtoType = DapStackFrame;
type Output = Self;
fn to_proto(self) -> Self::ProtoType {
Self::ProtoType {
id: self.id,
name: self.name.clone(),
source: self.source.map(|src| src.to_proto()),
line: self.line,
column: self.column,
end_line: self.end_line,
end_column: self.end_column,
can_restart: self.can_restart,
instruction_pointer_reference: self.instruction_pointer_reference,
module_id: None, // TODO Debugger Collab
presentation_hint: None, // TODO Debugger Collab
}
}
fn from_proto(payload: Self::ProtoType) -> Self {
Self {
id: payload.id,
name: payload.name,
source: payload.source.map(dap_types::Source::from_proto),
line: payload.line,
column: payload.column,
end_line: payload.end_line,
end_column: payload.end_column,
can_restart: payload.can_restart,
instruction_pointer_reference: payload.instruction_pointer_reference,
module_id: None, // TODO Debugger Collab
presentation_hint: None, // TODO Debugger Collab
}
}
}
impl ProtoConversion for dap_types::ModuleId {
type ProtoType = proto::dap_module_id::Id;
type Output = Self;
fn to_proto(self) -> Self::ProtoType {
match self {
Self::Number(num) => Self::ProtoType::Number(num),
Self::String(string) => Self::ProtoType::String(string),
}
}
fn from_proto(payload: Self::ProtoType) -> Self::Output {
match payload {
Self::ProtoType::Number(num) => Self::Number(num),
Self::ProtoType::String(string) => Self::String(string),
}
}
}
impl ProtoConversion for dap_types::Module {
type ProtoType = DapModule;
type Output = Result<Self>;
fn to_proto(self) -> Self::ProtoType {
DapModule {
id: Some(proto::DapModuleId {
id: Some(self.id.to_proto()),
}),
name: self.name,
path: self.path,
is_optimized: self.is_optimized,
is_user_code: self.is_user_code,
version: self.version,
symbol_status: self.symbol_status,
symbol_file_path: self.symbol_file_path,
date_time_stamp: self.date_time_stamp,
address_range: self.address_range,
}
}
fn from_proto(payload: Self::ProtoType) -> Result<Self> {
let id = match payload
.id
.context("All DapModule proto messages must have an id")?
.id
.context("All DapModuleID proto messages must have an id")?
{
proto::dap_module_id::Id::String(string) => dap_types::ModuleId::String(string),
proto::dap_module_id::Id::Number(num) => dap_types::ModuleId::Number(num),
};
Ok(Self {
id,
name: payload.name,
path: payload.path,
is_optimized: payload.is_optimized,
is_user_code: payload.is_user_code,
version: payload.version,
symbol_status: payload.symbol_status,
symbol_file_path: payload.symbol_file_path,
date_time_stamp: payload.date_time_stamp,
address_range: payload.address_range,
})
}
}
impl ProtoConversion for dap_types::SteppingGranularity {
type ProtoType = proto::SteppingGranularity;
type Output = Self;
fn to_proto(self) -> Self::ProtoType {
match self {
Self::Statement => Self::ProtoType::Statement,
Self::Line => Self::ProtoType::Line,
Self::Instruction => Self::ProtoType::Instruction,
}
}
fn from_proto(payload: Self::ProtoType) -> Self {
match payload {
Self::ProtoType::Line => Self::Line,
Self::ProtoType::Instruction => Self::Instruction,
Self::ProtoType::Statement => Self::Statement,
}
}
}
impl ProtoConversion for dap_types::OutputEventCategory {
type ProtoType = proto::DapOutputCategory;
type Output = Self;
fn to_proto(self) -> Self::ProtoType {
match self {
Self::Console => Self::ProtoType::ConsoleOutput,
Self::Important => Self::ProtoType::Important,
Self::Stdout => Self::ProtoType::Stdout,
Self::Stderr => Self::ProtoType::Stderr,
_ => Self::ProtoType::Unknown,
}
}
fn from_proto(payload: Self::ProtoType) -> Self {
match payload {
Self::ProtoType::ConsoleOutput => Self::Console,
Self::ProtoType::Important => Self::Important,
Self::ProtoType::Stdout => Self::Stdout,
Self::ProtoType::Stderr => Self::Stderr,
Self::ProtoType::Unknown => Self::Unknown,
}
}
}
impl ProtoConversion for dap_types::OutputEvent {
type ProtoType = proto::DapOutputEvent;
type Output = Self;
fn to_proto(self) -> Self::ProtoType {
Self::ProtoType {
category: self.category.map(|category| category.to_proto().into()),
output: self.output.clone(),
variables_reference: self.variables_reference,
source: self.source.map(|source| source.to_proto()),
line: self.line.map(|line| line as u32),
column: self.column.map(|column| column as u32),
group: self.group.map(|group| group.to_proto().into()),
}
}
fn from_proto(payload: Self::ProtoType) -> Self {
Self {
category: payload
.category
.and_then(proto::DapOutputCategory::from_i32)
.map(OutputEventCategory::from_proto),
output: payload.output,
variables_reference: payload.variables_reference,
source: payload.source.map(Source::from_proto),
line: payload.line.map(|line| line as u64),
column: payload.column.map(|column| column as u64),
group: payload
.group
.and_then(proto::DapOutputEventGroup::from_i32)
.map(OutputEventGroup::from_proto),
data: None,
location_reference: None,
}
}
}
impl ProtoConversion for dap_types::OutputEventGroup {
type ProtoType = proto::DapOutputEventGroup;
type Output = Self;
fn to_proto(self) -> Self::ProtoType {
match self {
Self::Start => Self::ProtoType::Start,
Self::StartCollapsed => Self::ProtoType::StartCollapsed,
Self::End => Self::ProtoType::End,
}
}
fn from_proto(payload: Self::ProtoType) -> Self {
match payload {
Self::ProtoType::Start => Self::Start,
Self::ProtoType::StartCollapsed => Self::StartCollapsed,
Self::ProtoType::End => Self::End,
}
}
}
impl ProtoConversion for dap_types::CompletionItem {
type ProtoType = proto::DapCompletionItem;
type Output = Self;
fn to_proto(self) -> Self::ProtoType {
Self::ProtoType {
label: self.label.clone(),
text: self.text.clone(),
detail: self.detail.clone(),
typ: self
.type_
.map(ProtoConversion::to_proto)
.map(|typ| typ.into()),
start: self.start,
length: self.length,
selection_start: self.selection_start,
selection_length: self.selection_length,
sort_text: self.sort_text,
}
}
fn from_proto(payload: Self::ProtoType) -> Self {
let typ = payload.typ(); // todo(debugger): This might be a potential issue/bug because it defaults to a type when it's None
Self {
label: payload.label,
detail: payload.detail,
sort_text: payload.sort_text,
text: payload.text.clone(),
type_: Some(dap_types::CompletionItemType::from_proto(typ)),
start: payload.start,
length: payload.length,
selection_start: payload.selection_start,
selection_length: payload.selection_length,
}
}
}
impl ProtoConversion for dap_types::EvaluateArgumentsContext {
type ProtoType = DapEvaluateContext;
type Output = Self;
fn to_proto(self) -> Self::ProtoType {
match self {
Self::Variables => Self::ProtoType::EvaluateVariables,
Self::Watch => Self::ProtoType::Watch,
Self::Hover => Self::ProtoType::Hover,
Self::Repl => Self::ProtoType::Repl,
Self::Clipboard => Self::ProtoType::Clipboard,
Self::Unknown => Self::ProtoType::EvaluateUnknown,
_ => Self::ProtoType::EvaluateUnknown,
}
}
fn from_proto(payload: Self::ProtoType) -> Self {
match payload {
Self::ProtoType::EvaluateVariables => Self::Variables,
Self::ProtoType::Watch => Self::Watch,
Self::ProtoType::Hover => Self::Hover,
Self::ProtoType::Repl => Self::Repl,
Self::ProtoType::Clipboard => Self::Clipboard,
Self::ProtoType::EvaluateUnknown => Self::Unknown,
}
}
}
impl ProtoConversion for dap_types::CompletionItemType {
type ProtoType = proto::DapCompletionItemType;
type Output = Self;
fn to_proto(self) -> Self::ProtoType {
match self {
Self::Class => Self::ProtoType::Class,
Self::Color => Self::ProtoType::Color,
Self::Constructor => Self::ProtoType::Constructor,
Self::Customcolor => Self::ProtoType::Customcolor,
Self::Enum => Self::ProtoType::Enum,
Self::Field => Self::ProtoType::Field,
Self::File => Self::ProtoType::CompletionItemFile,
Self::Function => Self::ProtoType::Function,
Self::Interface => Self::ProtoType::Interface,
Self::Keyword => Self::ProtoType::Keyword,
Self::Method => Self::ProtoType::Method,
Self::Module => Self::ProtoType::Module,
Self::Property => Self::ProtoType::Property,
Self::Reference => Self::ProtoType::Reference,
Self::Snippet => Self::ProtoType::Snippet,
Self::Text => Self::ProtoType::Text,
Self::Unit => Self::ProtoType::Unit,
Self::Value => Self::ProtoType::Value,
Self::Variable => Self::ProtoType::Variable,
}
}
fn from_proto(payload: Self::ProtoType) -> Self {
match payload {
Self::ProtoType::Class => Self::Class,
Self::ProtoType::Color => Self::Color,
Self::ProtoType::CompletionItemFile => Self::File,
Self::ProtoType::Constructor => Self::Constructor,
Self::ProtoType::Customcolor => Self::Customcolor,
Self::ProtoType::Enum => Self::Enum,
Self::ProtoType::Field => Self::Field,
Self::ProtoType::Function => Self::Function,
Self::ProtoType::Interface => Self::Interface,
Self::ProtoType::Keyword => Self::Keyword,
Self::ProtoType::Method => Self::Method,
Self::ProtoType::Module => Self::Module,
Self::ProtoType::Property => Self::Property,
Self::ProtoType::Reference => Self::Reference,
Self::ProtoType::Snippet => Self::Snippet,
Self::ProtoType::Text => Self::Text,
Self::ProtoType::Unit => Self::Unit,
Self::ProtoType::Value => Self::Value,
Self::ProtoType::Variable => Self::Variable,
}
}
}
impl ProtoConversion for dap_types::Thread {
type ProtoType = proto::DapThread;
type Output = Self;
fn to_proto(self) -> Self::ProtoType {
proto::DapThread {
id: self.id,
name: self.name,
}
}
fn from_proto(payload: Self::ProtoType) -> Self {
Self {
id: payload.id,
name: payload.name,
}
}
}

View File

@@ -0,0 +1,97 @@
use anyhow::Result;
use async_trait::async_trait;
use collections::FxHashMap;
use gpui::{App, BackgroundExecutor, Global, SharedString};
use language::LanguageName;
use parking_lot::RwLock;
use task::{
AdapterSchema, AdapterSchemas, DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate,
};
use crate::adapters::{DebugAdapter, DebugAdapterName};
use std::{collections::BTreeMap, sync::Arc};
/// Given a user build configuration, locator creates a fill-in debug target ([DebugScenario]) on behalf of the user.
#[async_trait]
pub trait DapLocator: Send + Sync {
fn name(&self) -> SharedString;
/// Determines whether this locator can generate debug target for given task.
async fn create_scenario(
&self,
build_config: &TaskTemplate,
resolved_label: &str,
adapter: &DebugAdapterName,
) -> Option<DebugScenario>;
async fn run(
&self,
build_config: SpawnInTerminal,
executor: BackgroundExecutor,
) -> Result<DebugRequest>;
}
#[derive(Default)]
struct DapRegistryState {
adapters: BTreeMap<DebugAdapterName, Arc<dyn DebugAdapter>>,
locators: FxHashMap<SharedString, Arc<dyn DapLocator>>,
}
#[derive(Clone, Default)]
/// Stores available debug adapters.
pub struct DapRegistry(Arc<RwLock<DapRegistryState>>);
impl Global for DapRegistry {}
impl DapRegistry {
pub fn global(cx: &mut App) -> &mut Self {
cx.default_global::<Self>()
}
pub fn add_adapter(&self, adapter: Arc<dyn DebugAdapter>) {
let name = adapter.name();
let _previous_value = self.0.write().adapters.insert(name, adapter);
}
pub fn add_locator(&self, locator: Arc<dyn DapLocator>) {
self.0.write().locators.insert(locator.name(), locator);
}
pub fn remove_adapter(&self, name: &str) {
self.0.write().adapters.remove(name);
}
pub fn remove_locator(&self, locator: &str) {
self.0.write().locators.remove(locator);
}
pub fn adapter_language(&self, adapter_name: &str) -> Option<LanguageName> {
self.adapter(adapter_name)
.and_then(|adapter| adapter.adapter_language_name())
}
pub fn adapters_schema(&self) -> task::AdapterSchemas {
let mut schemas = vec![];
let adapters = &self.0.read().adapters;
for (name, adapter) in adapters.into_iter() {
schemas.push(AdapterSchema {
adapter: name.clone().into(),
schema: adapter.dap_schema(),
});
}
AdapterSchemas(schemas)
}
pub fn locators(&self) -> FxHashMap<SharedString, Arc<dyn DapLocator>> {
self.0.read().locators.clone()
}
pub fn adapter(&self, name: &str) -> Option<Arc<dyn DebugAdapter>> {
self.0.read().adapters.get(name).cloned()
}
pub fn enumerate_adapters<B: FromIterator<DebugAdapterName>>(&self) -> B {
self.0.read().adapters.keys().cloned().collect()
}
}

1020
crates/dap/src/transport.rs Normal file

File diff suppressed because it is too large Load Diff