diff --git a/.zed/debug.json b/.zed/debug.json
new file mode 100644
index 00000000000000..b7646ee3bd7d28
--- /dev/null
+++ b/.zed/debug.json
@@ -0,0 +1,19 @@
+[
+ {
+ "label": "Debug Zed with LLDB",
+ "adapter": "lldb",
+ "program": "$ZED_WORKTREE_ROOT/target/debug/zed",
+ "request": "launch",
+ "cwd": "$ZED_WORKTREE_ROOT"
+ },
+ {
+ "label": "Debug Zed with GDB",
+ "adapter": "gdb",
+ "program": "$ZED_WORKTREE_ROOT/target/debug/zed",
+ "request": "launch",
+ "cwd": "$ZED_WORKTREE_ROOT",
+ "initialize_args": {
+ "stopAtBeginningOfMainSubprogram": true
+ }
+ }
+]
diff --git a/Cargo.lock b/Cargo.lock
index f7412a5fb88f38..b68d89267133b9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13,7 +13,6 @@ dependencies = [
"futures 0.3.31",
"gpui",
"language",
- "lsp",
"project",
"smallvec",
"ui",
@@ -2772,9 +2771,12 @@ dependencies = [
"clock",
"collab_ui",
"collections",
+ "command_palette_hooks",
"context_server",
"ctor",
+ "dap",
"dashmap 6.1.0",
+ "debugger_ui",
"derive_more",
"editor",
"env_logger 0.11.6",
@@ -3690,6 +3692,67 @@ dependencies = [
"syn 2.0.90",
]
+[[package]]
+name = "dap"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-compression",
+ "async-pipe",
+ "async-tar",
+ "async-trait",
+ "client",
+ "collections",
+ "dap-types",
+ "env_logger 0.11.6",
+ "fs",
+ "futures 0.3.31",
+ "gpui",
+ "http_client",
+ "language",
+ "log",
+ "node_runtime",
+ "parking_lot",
+ "paths",
+ "schemars",
+ "serde",
+ "serde_json",
+ "settings",
+ "smallvec",
+ "smol",
+ "sysinfo",
+ "task",
+ "util",
+]
+
+[[package]]
+name = "dap-types"
+version = "0.0.1"
+source = "git+https://github.com/zed-industries/dap-types?rev=bf5632dc19f806e8a435c9f04a4bfe7322badea2#bf5632dc19f806e8a435c9f04a4bfe7322badea2"
+dependencies = [
+ "schemars",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "dap_adapters"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "dap",
+ "gpui",
+ "language",
+ "paths",
+ "regex",
+ "serde",
+ "serde_json",
+ "sysinfo",
+ "task",
+ "util",
+]
+
[[package]]
name = "dashmap"
version = "5.5.3"
@@ -3763,6 +3826,58 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "debugger_tools"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "dap",
+ "editor",
+ "futures 0.3.31",
+ "gpui",
+ "project",
+ "serde_json",
+ "settings",
+ "smol",
+ "util",
+ "workspace",
+]
+
+[[package]]
+name = "debugger_ui"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client",
+ "collections",
+ "command_palette_hooks",
+ "dap",
+ "editor",
+ "env_logger 0.11.6",
+ "fuzzy",
+ "gpui",
+ "language",
+ "log",
+ "menu",
+ "picker",
+ "pretty_assertions",
+ "project",
+ "rpc",
+ "serde",
+ "serde_json",
+ "settings",
+ "sum_tree",
+ "sysinfo",
+ "task",
+ "tasks_ui",
+ "terminal_view",
+ "theme",
+ "ui",
+ "unindent",
+ "util",
+ "workspace",
+]
+
[[package]]
name = "deepseek"
version = "0.1.0"
@@ -4063,6 +4178,7 @@ dependencies = [
"log",
"lsp",
"markdown",
+ "menu",
"multi_buffer",
"ordered-float 2.10.1",
"parking_lot",
@@ -10115,6 +10231,8 @@ dependencies = [
"client",
"clock",
"collections",
+ "dap",
+ "dap_adapters",
"env_logger 0.11.6",
"fancy-regex 0.14.0",
"fs",
@@ -10126,6 +10244,7 @@ dependencies = [
"gpui",
"http_client",
"image",
+ "indexmap",
"itertools 0.14.0",
"language",
"log",
@@ -10774,6 +10893,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"auto_update",
+ "dap",
"editor",
"extension_host",
"file_finder",
@@ -13293,6 +13413,7 @@ dependencies = [
"parking_lot",
"schemars",
"serde",
+ "serde_json",
"serde_json_lenient",
"sha2",
"shellexpand 2.1.2",
@@ -16697,6 +16818,8 @@ dependencies = [
"component_preview",
"copilot",
"db",
+ "debugger_tools",
+ "debugger_ui",
"diagnostics",
"editor",
"env_logger 0.11.6",
diff --git a/Cargo.toml b/Cargo.toml
index 448f653a519ef1..3949f1a08cc086 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -33,6 +33,10 @@ members = [
"crates/context_server_settings",
"crates/copilot",
"crates/credentials_provider",
+ "crates/dap",
+ "crates/dap_adapters",
+ "crates/debugger_tools",
+ "crates/debugger_ui",
"crates/db",
"crates/deepseek",
"crates/diagnostics",
@@ -235,7 +239,11 @@ context_server = { path = "crates/context_server" }
context_server_settings = { path = "crates/context_server_settings" }
copilot = { path = "crates/copilot" }
credentials_provider = { path = "crates/credentials_provider" }
+dap = { path = "crates/dap" }
+dap_adapters = { path = "crates/dap_adapters" }
db = { path = "crates/db" }
+debugger_ui = { path = "crates/debugger_ui" }
+debugger_tools = { path = "crates/debugger_tools" }
deepseek = { path = "crates/deepseek" }
diagnostics = { path = "crates/diagnostics" }
buffer_diff = { path = "crates/buffer_diff" }
diff --git a/assets/icons/debug.svg b/assets/icons/debug.svg
new file mode 100644
index 00000000000000..8cea0c460402fb
--- /dev/null
+++ b/assets/icons/debug.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/debug_breakpoint.svg b/assets/icons/debug_breakpoint.svg
new file mode 100644
index 00000000000000..f6a7b35658eeff
--- /dev/null
+++ b/assets/icons/debug_breakpoint.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/debug_continue.svg b/assets/icons/debug_continue.svg
new file mode 100644
index 00000000000000..e2a99c38d032fc
--- /dev/null
+++ b/assets/icons/debug_continue.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/debug_disconnect.svg b/assets/icons/debug_disconnect.svg
new file mode 100644
index 00000000000000..0eb253715288fc
--- /dev/null
+++ b/assets/icons/debug_disconnect.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/debug_ignore_breakpoints.svg b/assets/icons/debug_ignore_breakpoints.svg
new file mode 100644
index 00000000000000..ba7074e083c700
--- /dev/null
+++ b/assets/icons/debug_ignore_breakpoints.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/debug_log_breakpoint.svg b/assets/icons/debug_log_breakpoint.svg
new file mode 100644
index 00000000000000..a878ce3e04189d
--- /dev/null
+++ b/assets/icons/debug_log_breakpoint.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/debug_pause.svg b/assets/icons/debug_pause.svg
new file mode 100644
index 00000000000000..bea531bc5a755b
--- /dev/null
+++ b/assets/icons/debug_pause.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/debug_restart.svg b/assets/icons/debug_restart.svg
new file mode 100644
index 00000000000000..4eff13b94b698d
--- /dev/null
+++ b/assets/icons/debug_restart.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/debug_step_back.svg b/assets/icons/debug_step_back.svg
new file mode 100644
index 00000000000000..bc7c9b8444cda2
--- /dev/null
+++ b/assets/icons/debug_step_back.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/debug_step_into.svg b/assets/icons/debug_step_into.svg
new file mode 100644
index 00000000000000..69e5cff3f176ca
--- /dev/null
+++ b/assets/icons/debug_step_into.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/debug_step_out.svg b/assets/icons/debug_step_out.svg
new file mode 100644
index 00000000000000..680e13e65e0414
--- /dev/null
+++ b/assets/icons/debug_step_out.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/debug_step_over.svg b/assets/icons/debug_step_over.svg
new file mode 100644
index 00000000000000..005b901da3c49b
--- /dev/null
+++ b/assets/icons/debug_step_over.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/debug_stop.svg b/assets/icons/debug_stop.svg
new file mode 100644
index 00000000000000..fef651c5864a15
--- /dev/null
+++ b/assets/icons/debug_stop.svg
@@ -0,0 +1 @@
+
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index 889fa2e33e09c4..3157fc450f9158 100644
--- a/assets/keymaps/default-macos.json
+++ b/assets/keymaps/default-macos.json
@@ -724,6 +724,14 @@
"space": "project_panel::Open"
}
},
+ {
+ "context": "VariableList",
+ "use_key_equivalents": true,
+ "bindings": {
+ "left": "variable_list::CollapseSelectedEntry",
+ "right": "variable_list::ExpandSelectedEntry"
+ }
+ },
{
"context": "GitPanel && ChangesList",
"use_key_equivalents": true,
diff --git a/assets/settings/default.json b/assets/settings/default.json
index d8c60e89848ec5..8b006f471966d6 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -1377,6 +1377,12 @@
// }
// ]
"ssh_connections": [],
+
// Configures context servers for use in the Assistant.
- "context_servers": {}
+ "context_servers": {},
+ "debugger": {
+ "stepping_granularity": "line",
+ "save_breakpoints": true,
+ "button": true
+ }
}
diff --git a/assets/settings/initial_debug_tasks.json b/assets/settings/initial_debug_tasks.json
new file mode 100644
index 00000000000000..e77d7c872767d8
--- /dev/null
+++ b/assets/settings/initial_debug_tasks.json
@@ -0,0 +1,32 @@
+[
+ {
+ "label": "Debug active PHP file",
+ "adapter": "php",
+ "program": "$ZED_FILE",
+ "request": "launch",
+ "cwd": "$ZED_WORKTREE_ROOT"
+ },
+ {
+ "label": "Debug active Python file",
+ "adapter": "python",
+ "program": "$ZED_FILE",
+ "request": "launch",
+ "cwd": "$ZED_WORKTREE_ROOT"
+ },
+ {
+ "label": "Debug active JavaScript file",
+ "adapter": "javascript",
+ "program": "$ZED_FILE",
+ "request": "launch",
+ "cwd": "$ZED_WORKTREE_ROOT"
+ },
+ {
+ "label": "JavaScript debug terminal",
+ "adapter": "javascript",
+ "request": "launch",
+ "cwd": "$ZED_WORKTREE_ROOT",
+ "initialize_args": {
+ "console": "integratedTerminal"
+ }
+ }
+]
diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml
index e28c30ca886720..a17846690a3922 100644
--- a/crates/activity_indicator/Cargo.toml
+++ b/crates/activity_indicator/Cargo.toml
@@ -20,7 +20,6 @@ extension_host.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
-lsp.workspace = true
project.workspace = true
smallvec.workspace = true
ui.workspace = true
diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs
index 7a86e5019642b4..834e83f9887055 100644
--- a/crates/activity_indicator/src/activity_indicator.rs
+++ b/crates/activity_indicator/src/activity_indicator.rs
@@ -7,8 +7,7 @@ use gpui::{
EventEmitter, InteractiveElement as _, ParentElement as _, Render, SharedString,
StatefulInteractiveElement, Styled, Transformation, Window,
};
-use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId};
-use lsp::LanguageServerName;
+use language::{BinaryStatus, LanguageRegistry, LanguageServerId};
use project::{EnvironmentErrorMessage, LanguageServerProgress, Project, WorktreeId};
use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
@@ -20,21 +19,21 @@ actions!(activity_indicator, [ShowErrorMessage]);
pub enum Event {
ShowError {
- lsp_name: LanguageServerName,
+ server_name: SharedString,
error: String,
},
}
pub struct ActivityIndicator {
- statuses: Vec,
+ statuses: Vec,
project: Entity,
auto_updater: Option>,
context_menu_handle: PopoverMenuHandle,
}
-struct LspStatus {
- name: LanguageServerName,
- status: LanguageServerBinaryStatus,
+struct ServerStatus {
+ name: SharedString,
+ status: BinaryStatus,
}
struct PendingWork<'a> {
@@ -65,7 +64,20 @@ impl ActivityIndicator {
while let Some((name, status)) = status_events.next().await {
this.update(&mut cx, |this: &mut ActivityIndicator, cx| {
this.statuses.retain(|s| s.name != name);
- this.statuses.push(LspStatus { name, status });
+ this.statuses.push(ServerStatus { name, status });
+ cx.notify();
+ })?;
+ }
+ anyhow::Ok(())
+ })
+ .detach();
+
+ let mut status_events = languages.dap_server_binary_statuses();
+ cx.spawn(|this, mut cx| async move {
+ while let Some((name, status)) = status_events.next().await {
+ this.update(&mut cx, |this, cx| {
+ this.statuses.retain(|s| s.name != name);
+ this.statuses.push(ServerStatus { name, status });
cx.notify();
})?;
}
@@ -88,18 +100,18 @@ impl ActivityIndicator {
});
cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event {
- Event::ShowError { lsp_name, error } => {
+ Event::ShowError { server_name, error } => {
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
let project = project.clone();
let error = error.clone();
- let lsp_name = lsp_name.clone();
+ let server_name = server_name.clone();
cx.spawn_in(window, |workspace, mut cx| async move {
let buffer = create_buffer.await?;
buffer.update(&mut cx, |buffer, cx| {
buffer.edit(
[(
0..0,
- format!("Language server error: {}\n\n{}", lsp_name, error),
+ format!("Language server error: {}\n\n{}", server_name, error),
)],
None,
cx,
@@ -129,9 +141,9 @@ impl ActivityIndicator {
fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context) {
self.statuses.retain(|status| {
- if let LanguageServerBinaryStatus::Failed { error } = &status.status {
+ if let BinaryStatus::Failed { error } = &status.status {
cx.emit(Event::ShowError {
- lsp_name: status.name.clone(),
+ server_name: status.name.clone(),
error: error.clone(),
});
false
@@ -260,12 +272,10 @@ impl ActivityIndicator {
let mut failed = SmallVec::<[_; 3]>::new();
for status in &self.statuses {
match status.status {
- LanguageServerBinaryStatus::CheckingForUpdate => {
- checking_for_update.push(status.name.clone())
- }
- LanguageServerBinaryStatus::Downloading => downloading.push(status.name.clone()),
- LanguageServerBinaryStatus::Failed { .. } => failed.push(status.name.clone()),
- LanguageServerBinaryStatus::None => {}
+ BinaryStatus::CheckingForUpdate => checking_for_update.push(status.name.clone()),
+ BinaryStatus::Downloading => downloading.push(status.name.clone()),
+ BinaryStatus::Failed { .. } => failed.push(status.name.clone()),
+ BinaryStatus::None => {}
}
}
@@ -278,7 +288,7 @@ impl ActivityIndicator {
),
message: format!(
"Downloading {}...",
- downloading.iter().map(|name| name.0.as_ref()).fold(
+ downloading.iter().map(|name| name.as_ref()).fold(
String::new(),
|mut acc, s| {
if !acc.is_empty() {
@@ -306,7 +316,7 @@ impl ActivityIndicator {
),
message: format!(
"Checking for updates to {}...",
- checking_for_update.iter().map(|name| name.0.as_ref()).fold(
+ checking_for_update.iter().map(|name| name.as_ref()).fold(
String::new(),
|mut acc, s| {
if !acc.is_empty() {
@@ -336,7 +346,7 @@ impl ActivityIndicator {
"Failed to run {}. Click to show error.",
failed
.iter()
- .map(|name| name.0.as_ref())
+ .map(|name| name.as_ref())
.fold(String::new(), |mut acc, s| {
if !acc.is_empty() {
acc.push_str(", ");
diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml
index 1af098416b7667..6cb75524c8a22b 100644
--- a/crates/collab/Cargo.toml
+++ b/crates/collab/Cargo.toml
@@ -89,8 +89,11 @@ channel.workspace = true
client = { workspace = true, features = ["test-support"] }
collab_ui = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
+command_palette_hooks.workspace = true
context_server.workspace = true
ctor.workspace = true
+dap = { workspace = true, features = ["test-support"] }
+debugger_ui = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
extension.workspace = true
diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql
index 30d36cfe8c1070..750a21818c3e28 100644
--- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql
+++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql
@@ -469,3 +469,14 @@ CREATE TABLE IF NOT EXISTS processed_stripe_events (
);
CREATE INDEX "ix_processed_stripe_events_on_stripe_event_created_timestamp" ON processed_stripe_events (stripe_event_created_timestamp);
+
+CREATE TABLE IF NOT EXISTS "breakpoints" (
+ "id" INTEGER PRIMARY KEY AUTOINCREMENT,
+ "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
+ "position" INTEGER NOT NULL,
+ "log_message" TEXT NULL,
+ "worktree_id" BIGINT NOT NULL,
+ "path" TEXT NOT NULL,
+ "kind" VARCHAR NOT NULL
+);
+CREATE INDEX "index_breakpoints_on_project_id" ON "breakpoints" ("project_id");
diff --git a/crates/collab/migrations/20241121185750_add_breakpoints.sql b/crates/collab/migrations/20241121185750_add_breakpoints.sql
new file mode 100644
index 00000000000000..4b3071457392f4
--- /dev/null
+++ b/crates/collab/migrations/20241121185750_add_breakpoints.sql
@@ -0,0 +1,11 @@
+CREATE TABLE IF NOT EXISTS "breakpoints" (
+ "id" SERIAL PRIMARY KEY,
+ "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
+ "position" INTEGER NOT NULL,
+ "log_message" TEXT NULL,
+ "worktree_id" BIGINT NOT NULL,
+ "path" TEXT NOT NULL,
+ "kind" VARCHAR NOT NULL
+);
+
+CREATE INDEX "index_breakpoints_on_project_id" ON "breakpoints" ("project_id");
diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs
index 908e488af6dd52..a1796617d12a45 100644
--- a/crates/collab/src/db.rs
+++ b/crates/collab/src/db.rs
@@ -659,6 +659,7 @@ pub struct RejoinedProject {
pub collaborators: Vec,
pub worktrees: Vec,
pub language_servers: Vec,
+ pub breakpoints: HashMap>,
}
impl RejoinedProject {
@@ -681,6 +682,17 @@ impl RejoinedProject {
.map(|collaborator| collaborator.to_proto())
.collect(),
language_servers: self.language_servers.clone(),
+ breakpoints: self
+ .breakpoints
+ .iter()
+ .map(
+ |(project_path, breakpoints)| proto::SynchronizeBreakpoints {
+ project_id: self.id.to_proto(),
+ breakpoints: breakpoints.iter().cloned().collect(),
+ project_path: Some(project_path.clone()),
+ },
+ )
+ .collect(),
}
}
}
@@ -727,6 +739,7 @@ pub struct Project {
pub collaborators: Vec,
pub worktrees: BTreeMap,
pub language_servers: Vec,
+ pub breakpoints: HashMap>,
}
pub struct ProjectCollaborator {
diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs
index 698b1c5693337e..36f3dad9e2fb77 100644
--- a/crates/collab/src/db/ids.rs
+++ b/crates/collab/src/db/ids.rs
@@ -94,6 +94,9 @@ id_type!(RoomParticipantId);
id_type!(ServerId);
id_type!(SignupId);
id_type!(UserId);
+id_type!(DebugClientId);
+id_type!(SessionId);
+id_type!(ThreadId);
/// ChannelRole gives you permissions for both channels and calls.
#[derive(
diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs
index 1cff5b53b09c12..53c51172677cfe 100644
--- a/crates/collab/src/db/queries/projects.rs
+++ b/crates/collab/src/db/queries/projects.rs
@@ -1,5 +1,5 @@
use anyhow::Context as _;
-
+use collections::{HashMap, HashSet};
use util::ResultExt;
use super::*;
@@ -571,6 +571,60 @@ impl Database {
.await
}
+ pub async fn update_breakpoints(
+ &self,
+ connection_id: ConnectionId,
+ update: &proto::SynchronizeBreakpoints,
+ ) -> Result>> {
+ let project_id = ProjectId::from_proto(update.project_id);
+ self.project_transaction(project_id, |tx| async move {
+ let project_path = update
+ .project_path
+ .as_ref()
+ .ok_or_else(|| anyhow!("invalid project path"))?;
+
+ // Ensure the update comes from the host.
+ let project = project::Entity::find_by_id(project_id)
+ .one(&*tx)
+ .await?
+ .ok_or_else(|| anyhow!("no such project"))?;
+
+ // remove all existing breakpoints
+ breakpoints::Entity::delete_many()
+ .filter(breakpoints::Column::ProjectId.eq(project.id))
+ .exec(&*tx)
+ .await?;
+
+ if !update.breakpoints.is_empty() {
+ breakpoints::Entity::insert_many(update.breakpoints.iter().map(|breakpoint| {
+ breakpoints::ActiveModel {
+ id: ActiveValue::NotSet,
+ project_id: ActiveValue::Set(project_id),
+ worktree_id: ActiveValue::Set(project_path.worktree_id as i64),
+ path: ActiveValue::Set(project_path.path.clone()),
+ kind: match proto::BreakpointKind::from_i32(breakpoint.kind) {
+ Some(proto::BreakpointKind::Log) => {
+ ActiveValue::Set(breakpoints::BreakpointKind::Log)
+ }
+ Some(proto::BreakpointKind::Standard) => {
+ ActiveValue::Set(breakpoints::BreakpointKind::Standard)
+ }
+ None => ActiveValue::Set(breakpoints::BreakpointKind::Standard),
+ },
+ log_message: ActiveValue::Set(breakpoint.message.clone()),
+ position: ActiveValue::Set(breakpoint.cached_position as i32),
+ }
+ }))
+ .exec_without_returning(&*tx)
+ .await?;
+ }
+
+ self.internal_project_connection_ids(project_id, connection_id, true, &tx)
+ .await
+ })
+ .await
+ }
+
/// Updates the worktree settings for the given connection.
pub async fn update_worktree_settings(
&self,
@@ -852,6 +906,33 @@ impl Database {
}
}
+ let mut breakpoints: HashMap> =
+ HashMap::default();
+
+ let db_breakpoints = project.find_related(breakpoints::Entity).all(tx).await?;
+
+ for breakpoint in db_breakpoints.iter() {
+ let project_path = proto::ProjectPath {
+ worktree_id: breakpoint.worktree_id as u64,
+ path: breakpoint.path.clone(),
+ };
+
+ breakpoints
+ .entry(project_path)
+ .or_default()
+ .insert(proto::Breakpoint {
+ position: None,
+ cached_position: breakpoint.position as u32,
+ kind: match breakpoint.kind {
+ breakpoints::BreakpointKind::Standard => {
+ proto::BreakpointKind::Standard.into()
+ }
+ breakpoints::BreakpointKind::Log => proto::BreakpointKind::Log.into(),
+ },
+ message: breakpoint.log_message.clone(),
+ });
+ }
+
// Populate language servers.
let language_servers = project
.find_related(language_server::Entity)
@@ -879,6 +960,7 @@ impl Database {
worktree_id: None,
})
.collect(),
+ breakpoints,
};
Ok((project, replica_id as ReplicaId))
}
@@ -1106,39 +1188,50 @@ impl Database {
exclude_dev_server: bool,
) -> Result>> {
self.project_transaction(project_id, |tx| async move {
- let project = project::Entity::find_by_id(project_id)
- .one(&*tx)
- .await?
- .ok_or_else(|| anyhow!("no such project"))?;
+ self.internal_project_connection_ids(project_id, connection_id, exclude_dev_server, &tx)
+ .await
+ })
+ .await
+ }
- let mut collaborators = project_collaborator::Entity::find()
- .filter(project_collaborator::Column::ProjectId.eq(project_id))
- .stream(&*tx)
- .await?;
+ async fn internal_project_connection_ids(
+ &self,
+ project_id: ProjectId,
+ connection_id: ConnectionId,
+ exclude_dev_server: bool,
+ tx: &DatabaseTransaction,
+ ) -> Result> {
+ let project = project::Entity::find_by_id(project_id)
+ .one(tx)
+ .await?
+ .ok_or_else(|| anyhow!("no such project"))?;
- let mut connection_ids = HashSet::default();
- if let Some(host_connection) = project.host_connection().log_err() {
- if !exclude_dev_server {
- connection_ids.insert(host_connection);
- }
- }
+ let mut collaborators = project_collaborator::Entity::find()
+ .filter(project_collaborator::Column::ProjectId.eq(project_id))
+ .stream(tx)
+ .await?;
- while let Some(collaborator) = collaborators.next().await {
- let collaborator = collaborator?;
- connection_ids.insert(collaborator.connection());
+ let mut connection_ids = HashSet::default();
+ if let Some(host_connection) = project.host_connection().log_err() {
+ if !exclude_dev_server {
+ connection_ids.insert(host_connection);
}
+ }
- if connection_ids.contains(&connection_id)
- || Some(connection_id) == project.host_connection().ok()
- {
- Ok(connection_ids)
- } else {
- Err(anyhow!(
- "can only send project updates to a project you're in"
- ))?
- }
- })
- .await
+ while let Some(collaborator) = collaborators.next().await {
+ let collaborator = collaborator?;
+ connection_ids.insert(collaborator.connection());
+ }
+
+ if connection_ids.contains(&connection_id)
+ || Some(connection_id) == project.host_connection().ok()
+ {
+ Ok(connection_ids)
+ } else {
+ Err(anyhow!(
+ "can only send project updates to a project you're in"
+ ))?
+ }
}
async fn project_guest_connection_ids(
diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs
index 3f65cc4258e6c1..9aba63aff4a89b 100644
--- a/crates/collab/src/db/queries/rooms.rs
+++ b/crates/collab/src/db/queries/rooms.rs
@@ -765,6 +765,33 @@ impl Database {
worktrees.push(worktree);
}
+ let mut breakpoints: HashMap> =
+ HashMap::default();
+
+ let db_breakpoints = project.find_related(breakpoints::Entity).all(tx).await?;
+
+ for breakpoint in db_breakpoints.iter() {
+ let project_path = proto::ProjectPath {
+ worktree_id: breakpoint.worktree_id as u64,
+ path: breakpoint.path.clone(),
+ };
+
+ breakpoints
+ .entry(project_path)
+ .or_default()
+ .insert(proto::Breakpoint {
+ position: None,
+ cached_position: breakpoint.position as u32,
+ kind: match breakpoint.kind {
+ breakpoints::BreakpointKind::Standard => {
+ proto::BreakpointKind::Standard.into()
+ }
+ breakpoints::BreakpointKind::Log => proto::BreakpointKind::Log.into(),
+ },
+ message: breakpoint.log_message.clone(),
+ });
+ }
+
let language_servers = project
.find_related(language_server::Entity)
.all(tx)
@@ -834,6 +861,7 @@ impl Database {
collaborators,
worktrees,
language_servers,
+ breakpoints,
}))
}
diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs
index 8a4ec29998ac86..fd4553c52b41c4 100644
--- a/crates/collab/src/db/tables.rs
+++ b/crates/collab/src/db/tables.rs
@@ -2,6 +2,7 @@ pub mod access_token;
pub mod billing_customer;
pub mod billing_preference;
pub mod billing_subscription;
+pub mod breakpoints;
pub mod buffer;
pub mod buffer_operation;
pub mod buffer_snapshot;
diff --git a/crates/collab/src/db/tables/breakpoints.rs b/crates/collab/src/db/tables/breakpoints.rs
new file mode 100644
index 00000000000000..c00bfef9d6d477
--- /dev/null
+++ b/crates/collab/src/db/tables/breakpoints.rs
@@ -0,0 +1,47 @@
+use crate::db::ProjectId;
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "breakpoints")]
+pub struct Model {
+ #[sea_orm(primary_key)]
+ pub id: i32,
+ #[sea_orm(primary_key)]
+ pub project_id: ProjectId,
+ pub worktree_id: i64,
+ pub path: String,
+ pub kind: BreakpointKind,
+ pub log_message: Option,
+ pub position: i32,
+}
+
+#[derive(
+ Copy, Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Default, Hash, serde::Serialize,
+)]
+#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
+#[serde(rename_all = "snake_case")]
+pub enum BreakpointKind {
+ #[default]
+ #[sea_orm(string_value = "standard")]
+ Standard,
+ #[sea_orm(string_value = "log")]
+ Log,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+ #[sea_orm(
+ belongs_to = "super::project::Entity",
+ from = "Column::ProjectId",
+ to = "super::project::Column::Id"
+ )]
+ Project,
+}
+
+impl Related for Entity {
+ fn to() -> RelationDef {
+ Relation::Project.def()
+ }
+}
+
+impl ActiveModelBehavior for ActiveModel {}
diff --git a/crates/collab/src/db/tables/project.rs b/crates/collab/src/db/tables/project.rs
index 10e3da50e1dd09..09d20bce625435 100644
--- a/crates/collab/src/db/tables/project.rs
+++ b/crates/collab/src/db/tables/project.rs
@@ -49,6 +49,8 @@ pub enum Relation {
Collaborators,
#[sea_orm(has_many = "super::language_server::Entity")]
LanguageServers,
+ #[sea_orm(has_many = "super::breakpoints::Entity")]
+ Breakpoints,
}
impl Related for Entity {
@@ -81,4 +83,10 @@ impl Related for Entity {
}
}
+impl Related for Entity {
+ fn to() -> RelationDef {
+ Relation::Breakpoints.def()
+ }
+}
+
impl ActiveModelBehavior for ActiveModel {}
diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs
index 189a5e5471f9f8..19ce05f1eb8e59 100644
--- a/crates/collab/src/rpc.rs
+++ b/crates/collab/src/rpc.rs
@@ -423,7 +423,40 @@ impl Server {
app_state.config.openai_api_key.clone(),
)
}
- });
+ })
+ .add_message_handler(update_breakpoints)
+ .add_message_handler(broadcast_project_message_from_host::)
+ .add_message_handler(
+ broadcast_project_message_from_host::,
+ )
+ .add_message_handler(broadcast_project_message_from_host::)
+ .add_message_handler(
+ broadcast_project_message_from_host::,
+ )
+ .add_message_handler(broadcast_project_message_from_host::)
+ .add_request_handler(forward_mutating_project_request::)
+ .add_request_handler(forward_mutating_project_request::)
+ .add_request_handler(forward_mutating_project_request::)
+ .add_request_handler(forward_mutating_project_request::)
+ .add_request_handler(forward_mutating_project_request::)
+ .add_request_handler(forward_mutating_project_request::)
+ .add_request_handler(forward_mutating_project_request::)
+ .add_request_handler(
+ forward_mutating_project_request::,
+ )
+ .add_request_handler(forward_mutating_project_request::)
+ .add_request_handler(forward_mutating_project_request::)
+ .add_message_handler(broadcast_project_message_from_host::)
+ .add_request_handler(forward_mutating_project_request::)
+ .add_message_handler(
+ broadcast_project_message_from_host::,
+ )
+ .add_message_handler(
+ broadcast_project_message_from_host::,
+ )
+ .add_message_handler(
+ broadcast_project_message_from_host::,
+ );
Arc::new(server)
}
@@ -1863,6 +1896,18 @@ fn join_project_internal(
.trace_err();
}
+ let breakpoints = project
+ .breakpoints
+ .iter()
+ .map(
+ |(project_path, breakpoint_set)| proto::SynchronizeBreakpoints {
+ project_id: project.id.0 as u64,
+ breakpoints: breakpoint_set.iter().map(|bp| bp.clone()).collect(),
+ project_path: Some(project_path.clone()),
+ },
+ )
+ .collect();
+
// First, we send the metadata associated with each worktree.
response.send(proto::JoinProjectResponse {
project_id: project.id.0 as u64,
@@ -1871,6 +1916,7 @@ fn join_project_internal(
collaborators: collaborators.clone(),
language_servers: project.language_servers.clone(),
role: project.role.into(),
+ breakpoints,
})?;
for (worktree_id, worktree) in mem::take(&mut project.worktrees) {
@@ -2057,7 +2103,7 @@ async fn update_worktree_settings(
Ok(())
}
-/// Notify other participants that a language server has started.
+/// Notify other participants that a language server has started.
async fn start_language_server(
request: proto::StartLanguageServer,
session: Session,
@@ -2103,6 +2149,29 @@ async fn update_language_server(
Ok(())
}
+/// Notify other participants that breakpoints have changed.
+async fn update_breakpoints(
+ request: proto::SynchronizeBreakpoints,
+ session: Session,
+) -> Result<()> {
+ let guest_connection_ids = session
+ .db()
+ .await
+ .update_breakpoints(session.connection_id, &request)
+ .await?;
+
+ broadcast(
+ Some(session.connection_id),
+ guest_connection_ids.iter().copied(),
+ |connection_id| {
+ session
+ .peer
+ .forward_send(session.connection_id, connection_id, request.clone())
+ },
+ );
+ Ok(())
+}
+
/// forward a project request to the host. These requests should be read only
/// as guests are allowed to send them.
async fn forward_read_only_project_request(
diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs
index 20fa289389aaf7..b9f0f79a9e23bc 100644
--- a/crates/collab/src/tests.rs
+++ b/crates/collab/src/tests.rs
@@ -11,6 +11,7 @@ mod channel_buffer_tests;
mod channel_guest_tests;
mod channel_message_tests;
mod channel_tests;
+mod debug_panel_tests;
mod editor_tests;
mod following_tests;
mod integration_tests;
diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs
new file mode 100644
index 00000000000000..a6d286d343d54b
--- /dev/null
+++ b/crates/collab/src/tests/debug_panel_tests.rs
@@ -0,0 +1,2444 @@
+use call::ActiveCall;
+use dap::requests::{Disconnect, Initialize, Launch, StackTrace};
+use dap::{
+ requests::{RestartFrame, SetBreakpoints},
+ SourceBreakpoint, StackFrame,
+};
+use debugger_ui::debugger_panel::DebugPanel;
+use debugger_ui::session::DebugSession;
+use editor::Editor;
+use gpui::{Entity, TestAppContext, VisualTestContext};
+use project::{Project, ProjectPath, WorktreeId};
+use serde_json::json;
+use std::sync::Arc;
+use std::{
+ path::Path,
+ sync::atomic::{AtomicBool, Ordering},
+};
+use unindent::Unindent as _;
+use workspace::{dock::Panel, Workspace};
+
+use super::{TestClient, TestServer};
+
+pub fn init_test(cx: &mut gpui::TestAppContext) {
+ if std::env::var("RUST_LOG").is_ok() {
+ env_logger::try_init().ok();
+ }
+
+ cx.update(|cx| {
+ theme::init(theme::LoadThemes::JustBase, cx);
+ command_palette_hooks::init(cx);
+ language::init(cx);
+ workspace::init_settings(cx);
+ project::Project::init_settings(cx);
+ debugger_ui::init(cx);
+ editor::init(cx);
+ });
+}
+
+async fn add_debugger_panel(workspace: &Entity, cx: &mut VisualTestContext) {
+ let debugger_panel = workspace
+ .update_in(cx, |_workspace, window, cx| {
+ cx.spawn_in(window, DebugPanel::load)
+ })
+ .await
+ .unwrap();
+
+ workspace.update_in(cx, |workspace, window, cx| {
+ workspace.add_panel(debugger_panel, window, cx);
+ });
+}
+
+pub fn active_debug_panel_item(
+ workspace: Entity,
+ cx: &mut VisualTestContext,
+) -> Entity {
+ workspace.update_in(cx, |workspace, _window, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+ debug_panel
+ .update(cx, |this, cx| this.active_debug_panel_item(cx))
+ .unwrap()
+ })
+}
+
+struct ZedInstance<'a> {
+ client: TestClient,
+ project: Option>,
+ active_call: Entity,
+ cx: &'a mut TestAppContext,
+}
+
+impl<'a> ZedInstance<'a> {
+ fn new(client: TestClient, cx: &'a mut TestAppContext) -> Self {
+ ZedInstance {
+ project: None,
+ client,
+ active_call: cx.read(ActiveCall::global),
+ cx,
+ }
+ }
+
+ async fn host_project(
+ &mut self,
+ project_files: Option,
+ ) -> (u64, WorktreeId) {
+ let (project, worktree_id) = self.client.build_local_project("/project", self.cx).await;
+ self.active_call
+ .update(self.cx, |call, cx| call.set_location(Some(&project), cx))
+ .await
+ .unwrap();
+
+ if let Some(tree) = project_files {
+ self.client.fs().insert_tree("/project", tree).await;
+ }
+
+ self.project = Some(project.clone());
+
+ let project_id = self
+ .active_call
+ .update(self.cx, |call, cx| call.share_project(project, cx))
+ .await
+ .unwrap();
+
+ (project_id, worktree_id)
+ }
+
+ async fn join_project(&mut self, project_id: u64) {
+ let remote_project = self.client.join_remote_project(project_id, self.cx).await;
+ self.project = Some(remote_project);
+
+ self.active_call
+ .update(self.cx, |call, cx| {
+ call.set_location(self.project.as_ref(), cx)
+ })
+ .await
+ .unwrap();
+ }
+
+ async fn expand(
+ &'a mut self,
+ ) -> (
+ &'a TestClient,
+ Entity,
+ Entity,
+ &'a mut VisualTestContext,
+ ) {
+ let (workspace, cx) = self.client.build_workspace(
+ self.project
+ .as_ref()
+ .expect("Project should be hosted or built before expanding"),
+ self.cx,
+ );
+ add_debugger_panel(&workspace, cx).await;
+ (&self.client, workspace, self.project.clone().unwrap(), cx)
+ }
+}
+
+async fn setup_three_member_test<'a, 'b, 'c>(
+ server: &mut TestServer,
+ host_cx: &'a mut TestAppContext,
+ first_remote_cx: &'b mut TestAppContext,
+ second_remote_cx: &'c mut TestAppContext,
+) -> (ZedInstance<'a>, ZedInstance<'b>, ZedInstance<'c>) {
+ let host_client = server.create_client(host_cx, "user_host").await;
+ let first_remote_client = server.create_client(first_remote_cx, "user_remote_1").await;
+ let second_remote_client = server
+ .create_client(second_remote_cx, "user_remote_2")
+ .await;
+
+ init_test(host_cx);
+ init_test(first_remote_cx);
+ init_test(second_remote_cx);
+
+ server
+ .create_room(&mut [
+ (&host_client, host_cx),
+ (&first_remote_client, first_remote_cx),
+ (&second_remote_client, second_remote_cx),
+ ])
+ .await;
+
+ let host_zed = ZedInstance::new(host_client, host_cx);
+ let first_remote_zed = ZedInstance::new(first_remote_client, first_remote_cx);
+ let second_remote_zed = ZedInstance::new(second_remote_client, second_remote_cx);
+
+ (host_zed, first_remote_zed, second_remote_zed)
+}
+
+async fn setup_two_member_test<'a, 'b>(
+ server: &mut TestServer,
+ host_cx: &'a mut TestAppContext,
+ remote_cx: &'b mut TestAppContext,
+) -> (ZedInstance<'a>, ZedInstance<'b>) {
+ let host_client = server.create_client(host_cx, "user_host").await;
+ let remote_client = server.create_client(remote_cx, "user_remote").await;
+
+ init_test(host_cx);
+ init_test(remote_cx);
+
+ server
+ .create_room(&mut [(&host_client, host_cx), (&remote_client, remote_cx)])
+ .await;
+
+ let host_zed = ZedInstance::new(host_client, host_cx);
+ let remote_zed = ZedInstance::new(remote_client, remote_cx);
+
+ (host_zed, remote_zed)
+}
+
+#[gpui::test]
+async fn test_debug_panel_item_opens_on_remote(
+ host_cx: &mut TestAppContext,
+ remote_cx: &mut TestAppContext,
+) {
+ let executor = host_cx.executor();
+ let mut server = TestServer::start(executor).await;
+
+ let (mut host_zed, mut remote_zed) =
+ setup_two_member_test(&mut server, host_cx, remote_cx).await;
+
+ let (host_project_id, _) = host_zed.host_project(None).await;
+ remote_zed.join_project(host_project_id).await;
+
+ let (_client_host, _host_workspace, host_project, host_cx) = host_zed.expand().await;
+ let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await;
+
+ remote_cx.run_until_parked();
+
+ let task = host_project.update(host_cx, |project, cx| {
+ project.start_debug_session(dap::test_config(), cx)
+ });
+
+ let (session, client) = task.await.unwrap();
+
+ client
+ .on_request::(move |_, _| {
+ Ok(dap::Capabilities {
+ supports_step_back: Some(false),
+ ..Default::default()
+ })
+ })
+ .await;
+
+ client.on_request::(move |_, _| Ok(())).await;
+
+ client
+ .on_request::(move |_, _| {
+ Ok(dap::StackTraceResponse {
+ stack_frames: Vec::default(),
+ total_frames: None,
+ })
+ })
+ .await;
+
+ client.on_request::(move |_, _| Ok(())).await;
+
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ remote_workspace.update(remote_cx, |workspace, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+ let active_debug_panel_item = debug_panel
+ .update(cx, |this, cx| this.active_debug_panel_item(cx))
+ .unwrap();
+
+ assert_eq!(
+ 1,
+ debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
+ );
+ assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id());
+ assert_eq!(1, active_debug_panel_item.read(cx).thread_id().0);
+ });
+
+ let shutdown_client = host_project.update(host_cx, |project, cx| {
+ project.dap_store().update(cx, |dap_store, cx| {
+ dap_store.shutdown_session(&session.read(cx).id(), cx)
+ })
+ });
+
+ shutdown_client.await.unwrap();
+}
+
+#[gpui::test]
+async fn test_active_debug_panel_item_set_on_join_project(
+ host_cx: &mut TestAppContext,
+ remote_cx: &mut TestAppContext,
+) {
+ let executor = host_cx.executor();
+ let mut server = TestServer::start(executor).await;
+
+ let (mut host_zed, mut remote_zed) =
+ setup_two_member_test(&mut server, host_cx, remote_cx).await;
+
+ let (host_project_id, _) = host_zed.host_project(None).await;
+
+ let (_client_host, _host_workspace, host_project, host_cx) = host_zed.expand().await;
+
+ host_cx.run_until_parked();
+
+ let task = host_project.update(host_cx, |project, cx| {
+ project.start_debug_session(dap::test_config(), cx)
+ });
+
+ let (session, client) = task.await.unwrap();
+
+ client
+ .on_request::(move |_, _| {
+ Ok(dap::Capabilities {
+ supports_step_back: Some(false),
+ ..Default::default()
+ })
+ })
+ .await;
+
+ client.on_request::(move |_, _| Ok(())).await;
+
+ client
+ .on_request::(move |_, _| {
+ Ok(dap::StackTraceResponse {
+ stack_frames: Vec::default(),
+ total_frames: None,
+ })
+ })
+ .await;
+
+ client.on_request::(move |_, _| Ok(())).await;
+
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ // Give host_client time to send a debug panel item to collab server
+ host_cx.run_until_parked();
+
+ remote_zed.join_project(host_project_id).await;
+ let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await;
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ remote_workspace.update(remote_cx, |workspace, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+ let active_debug_panel_item = debug_panel
+ .update(cx, |this, cx| this.active_debug_panel_item(cx))
+ .unwrap();
+
+ assert_eq!(
+ 1,
+ debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
+ );
+ assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id());
+ assert_eq!(1, active_debug_panel_item.read(cx).thread_id().0);
+ });
+
+ let shutdown_client = host_project.update(host_cx, |project, cx| {
+ project.dap_store().update(cx, |dap_store, cx| {
+ dap_store.shutdown_session(&session.read(cx).id(), cx)
+ })
+ });
+
+ shutdown_client.await.unwrap();
+
+ remote_cx.run_until_parked();
+
+ // assert we don't have a debug panel item anymore because the client shutdown
+ remote_workspace.update(remote_cx, |workspace, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+
+ debug_panel.update(cx, |this, cx| {
+ assert!(this.active_debug_panel_item(cx).is_none());
+ assert_eq!(0, this.pane().unwrap().read(cx).items_len());
+ });
+ });
+}
+
+#[gpui::test]
+async fn test_debug_panel_remote_button_presses(
+ host_cx: &mut TestAppContext,
+ remote_cx: &mut TestAppContext,
+) {
+ let executor = host_cx.executor();
+ let mut server = TestServer::start(executor).await;
+
+ let (mut host_zed, mut remote_zed) =
+ setup_two_member_test(&mut server, host_cx, remote_cx).await;
+
+ let (host_project_id, _) = host_zed.host_project(None).await;
+ remote_zed.join_project(host_project_id).await;
+
+ let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await;
+ let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await;
+
+ let task = host_project.update(host_cx, |project, cx| {
+ project.start_debug_session(dap::test_config(), cx)
+ });
+
+ let (_, client) = task.await.unwrap();
+
+ client
+ .on_request::(move |_, _| {
+ Ok(dap::Capabilities {
+ supports_step_back: Some(true),
+ ..Default::default()
+ })
+ })
+ .await;
+
+ client.on_request::(move |_, _| Ok(())).await;
+
+ client
+ .on_request::(move |_, _| {
+ Ok(dap::StackTraceResponse {
+ stack_frames: Vec::default(),
+ total_frames: None,
+ })
+ })
+ .await;
+
+ client.on_request::(move |_, _| Ok(())).await;
+
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ client
+ .on_request::(move |_, _| {
+ Ok(dap::ContinueResponse {
+ all_threads_continued: Some(true),
+ })
+ })
+ .await;
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ let remote_debug_item = remote_workspace.update(remote_cx, |workspace, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+ let active_debug_panel_item = debug_panel
+ .update(cx, |this, cx| this.active_debug_panel_item(cx))
+ .unwrap();
+
+ assert_eq!(
+ 1,
+ debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
+ );
+ assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id());
+ assert_eq!(1, active_debug_panel_item.read(cx).thread_id().0);
+ active_debug_panel_item
+ });
+
+ let local_debug_item = host_workspace.update(host_cx, |workspace, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+ let active_debug_panel_item = debug_panel
+ .update(cx, |this, cx| this.active_debug_panel_item(cx))
+ .unwrap();
+
+ assert_eq!(
+ 1,
+ debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
+ );
+ assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id());
+ assert_eq!(1, active_debug_panel_item.read(cx).thread_id().0);
+ active_debug_panel_item
+ });
+
+ remote_debug_item.update(remote_cx, |this, cx| {
+ this.continue_thread(cx);
+ });
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ local_debug_item.update(host_cx, |debug_panel_item, cx| {
+ assert_eq!(
+ debugger_ui::debugger_panel::ThreadStatus::Running,
+ debug_panel_item.thread_state().read(cx).status,
+ );
+ });
+
+ remote_debug_item.update(remote_cx, |debug_panel_item, cx| {
+ assert_eq!(
+ debugger_ui::debugger_panel::ThreadStatus::Running,
+ debug_panel_item.thread_state().read(cx).status,
+ );
+ });
+
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ client
+ .on_request::(move |_, _| {
+ Ok(dap::StackTraceResponse {
+ stack_frames: Vec::default(),
+ total_frames: None,
+ })
+ })
+ .await;
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ local_debug_item.update(host_cx, |debug_panel_item, cx| {
+ assert_eq!(
+ debugger_ui::debugger_panel::ThreadStatus::Stopped,
+ debug_panel_item.thread_state().read(cx).status,
+ );
+ });
+
+ remote_debug_item.update(remote_cx, |debug_panel_item, cx| {
+ assert_eq!(
+ debugger_ui::debugger_panel::ThreadStatus::Stopped,
+ debug_panel_item.thread_state().read(cx).status,
+ );
+ });
+
+ client
+ .on_request::(move |_, _| {
+ Ok(dap::ContinueResponse {
+ all_threads_continued: Some(true),
+ })
+ })
+ .await;
+
+ local_debug_item.update(host_cx, |this, cx| {
+ this.continue_thread(cx);
+ });
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ local_debug_item.update(host_cx, |debug_panel_item, cx| {
+ assert_eq!(
+ debugger_ui::debugger_panel::ThreadStatus::Running,
+ debug_panel_item.thread_state().read(cx).status,
+ );
+ });
+
+ remote_debug_item.update(remote_cx, |debug_panel_item, cx| {
+ assert_eq!(
+ debugger_ui::debugger_panel::ThreadStatus::Running,
+ debug_panel_item.thread_state().read(cx).status,
+ );
+ });
+
+ client
+ .on_request::(move |_, _| Ok(()))
+ .await;
+
+ client
+ .on_request::(move |_, _| {
+ Ok(dap::StackTraceResponse {
+ stack_frames: Vec::default(),
+ total_frames: None,
+ })
+ })
+ .await;
+
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ remote_debug_item.update(remote_cx, |this, cx| {
+ this.pause_thread(cx);
+ });
+
+ remote_cx.run_until_parked();
+ host_cx.run_until_parked();
+
+ client
+ .on_request::(move |_, _| Ok(()))
+ .await;
+
+ remote_debug_item.update(remote_cx, |this, cx| {
+ this.step_out(cx);
+ });
+
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ remote_cx.run_until_parked();
+ host_cx.run_until_parked();
+
+ client
+ .on_request::(move |_, _| Ok(()))
+ .await;
+
+ remote_debug_item.update(remote_cx, |this, cx| {
+ this.step_over(cx);
+ });
+
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ remote_cx.run_until_parked();
+ host_cx.run_until_parked();
+
+ client
+ .on_request::(move |_, _| Ok(()))
+ .await;
+
+ remote_debug_item.update(remote_cx, |this, cx| {
+ this.step_in(cx);
+ });
+
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ remote_cx.run_until_parked();
+ host_cx.run_until_parked();
+
+ client
+ .on_request::(move |_, _| Ok(()))
+ .await;
+
+ remote_debug_item.update(remote_cx, |this, cx| {
+ this.step_back(cx);
+ });
+
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ remote_cx.run_until_parked();
+ host_cx.run_until_parked();
+
+ remote_debug_item.update(remote_cx, |this, cx| {
+ this.stop_thread(cx);
+ });
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ // assert we don't have a debug panel item anymore because the client shutdown
+ remote_workspace.update(remote_cx, |workspace, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+
+ debug_panel.update(cx, |this, cx| {
+ assert!(this.active_debug_panel_item(cx).is_none());
+ assert_eq!(0, this.pane().unwrap().read(cx).items_len());
+ });
+ });
+}
+
+#[gpui::test]
+async fn test_restart_stack_frame(host_cx: &mut TestAppContext, remote_cx: &mut TestAppContext) {
+ let executor = host_cx.executor();
+ let mut server = TestServer::start(executor).await;
+
+ let (mut host_zed, mut remote_zed) =
+ setup_two_member_test(&mut server, host_cx, remote_cx).await;
+
+ let (host_project_id, _) = host_zed.host_project(None).await;
+ remote_zed.join_project(host_project_id).await;
+
+ let (_client_host, _host_workspace, host_project, host_cx) = host_zed.expand().await;
+ let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await;
+
+ let called_restart_frame = Arc::new(AtomicBool::new(false));
+
+ let task = host_project.update(host_cx, |project, cx| {
+ project.start_debug_session(dap::test_config(), cx)
+ });
+
+ let (session, client) = task.await.unwrap();
+
+ client
+ .on_request::(move |_, _| {
+ Ok(dap::Capabilities {
+ supports_restart_frame: Some(true),
+ ..Default::default()
+ })
+ })
+ .await;
+
+ client.on_request::(move |_, _| Ok(())).await;
+
+ let stack_frames = vec![StackFrame {
+ id: 1,
+ name: "Stack Frame 1".into(),
+ source: Some(dap::Source {
+ name: Some("test.js".into()),
+ path: Some("/project/src/test.js".into()),
+ source_reference: None,
+ presentation_hint: None,
+ origin: None,
+ sources: None,
+ adapter_data: None,
+ checksums: None,
+ }),
+ line: 3,
+ column: 1,
+ end_line: None,
+ end_column: None,
+ can_restart: None,
+ instruction_pointer_reference: None,
+ module_id: None,
+ presentation_hint: None,
+ }];
+
+ client
+ .on_request::({
+ let stack_frames = Arc::new(stack_frames.clone());
+ move |_, args| {
+ assert_eq!(1, args.thread_id);
+
+ Ok(dap::StackTraceResponse {
+ stack_frames: (*stack_frames).clone(),
+ total_frames: None,
+ })
+ }
+ })
+ .await;
+
+ client
+ .on_request::({
+ let called_restart_frame = called_restart_frame.clone();
+ move |_, args| {
+ assert_eq!(1, args.frame_id);
+
+ called_restart_frame.store(true, Ordering::SeqCst);
+
+ Ok(())
+ }
+ })
+ .await;
+
+ client.on_request::(move |_, _| Ok(())).await;
+
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ // try to restart stack frame 1 from the guest side
+ remote_workspace.update(remote_cx, |workspace, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+ let active_debug_panel_item = debug_panel
+ .update(cx, |this, cx| this.active_debug_panel_item(cx))
+ .unwrap();
+
+ active_debug_panel_item.update(cx, |debug_panel_item, cx| {
+ debug_panel_item
+ .stack_frame_list()
+ .update(cx, |stack_frame_list, cx| {
+ stack_frame_list.restart_stack_frame(1, cx);
+ });
+ });
+ });
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ assert!(
+ called_restart_frame.load(std::sync::atomic::Ordering::SeqCst),
+ "Restart stack frame was not called"
+ );
+
+ let shutdown_client = host_project.update(host_cx, |project, cx| {
+ project.dap_store().update(cx, |dap_store, cx| {
+ dap_store.shutdown_session(&session.read(cx).id(), cx)
+ })
+ });
+
+ shutdown_client.await.unwrap();
+}
+
+#[gpui::test]
+async fn test_updated_breakpoints_send_to_dap(
+ host_cx: &mut TestAppContext,
+ remote_cx: &mut TestAppContext,
+) {
+ let executor = host_cx.executor();
+ let mut server = TestServer::start(executor).await;
+
+ let (mut host_zed, mut remote_zed) =
+ setup_two_member_test(&mut server, host_cx, remote_cx).await;
+
+ let (host_project_id, worktree_id) = host_zed
+ .host_project(Some(json!({"test.txt": "one\ntwo\nthree\nfour\nfive"})))
+ .await;
+
+ remote_zed.join_project(host_project_id).await;
+
+ let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await;
+ let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await;
+
+ let project_path = ProjectPath {
+ worktree_id,
+ path: Arc::from(Path::new(&"test.txt")),
+ };
+
+ let task = host_project.update(host_cx, |project, cx| {
+ project.start_debug_session(dap::test_config(), cx)
+ });
+
+ let (session, client) = task.await.unwrap();
+
+ client
+ .on_request::(move |_, _| {
+ Ok(dap::Capabilities {
+ supports_restart_frame: Some(true),
+ ..Default::default()
+ })
+ })
+ .await;
+
+ client.on_request::(move |_, _| Ok(())).await;
+ client
+ .on_request::(move |_, _| {
+ Ok(dap::StackTraceResponse {
+ stack_frames: Vec::default(),
+ total_frames: None,
+ })
+ })
+ .await;
+
+ let called_set_breakpoints = Arc::new(AtomicBool::new(false));
+ client
+ .on_request::({
+ let called_set_breakpoints = called_set_breakpoints.clone();
+ move |_, args| {
+ assert_eq!("/project/test.txt", args.source.path.unwrap());
+ assert_eq!(
+ vec![SourceBreakpoint {
+ line: 3,
+ column: None,
+ condition: None,
+ hit_condition: None,
+ log_message: None,
+ mode: None
+ }],
+ args.breakpoints.unwrap()
+ );
+ assert!(!args.source_modified.unwrap());
+
+ called_set_breakpoints.store(true, Ordering::SeqCst);
+
+ Ok(dap::SetBreakpointsResponse {
+ breakpoints: Vec::default(),
+ })
+ }
+ })
+ .await;
+
+ client.on_request::(move |_, _| Ok(())).await;
+
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ // Client B opens an editor.
+ let editor_b = remote_workspace
+ .update_in(remote_cx, |workspace, window, cx| {
+ workspace.open_path(project_path.clone(), None, true, window, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::()
+ .unwrap();
+
+ editor_b.update_in(remote_cx, |editor, window, cx| {
+ editor.move_down(&editor::actions::MoveDown, window, cx);
+ editor.move_down(&editor::actions::MoveDown, window, cx);
+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
+ });
+
+ // Client A opens an editor.
+ let editor_a = host_workspace
+ .update_in(host_cx, |workspace, window, cx| {
+ workspace.open_path(project_path.clone(), None, true, window, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::()
+ .unwrap();
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ let called_set_breakpoints = Arc::new(AtomicBool::new(false));
+ client
+ .on_request::({
+ let called_set_breakpoints = called_set_breakpoints.clone();
+ move |_, args| {
+ assert_eq!("/project/test.txt", args.source.path.unwrap());
+ assert!(args.breakpoints.unwrap().is_empty());
+ assert!(!args.source_modified.unwrap());
+
+ called_set_breakpoints.store(true, Ordering::SeqCst);
+
+ Ok(dap::SetBreakpointsResponse {
+ breakpoints: Vec::default(),
+ })
+ }
+ })
+ .await;
+
+ // remove the breakpoint that client B added
+ editor_a.update_in(host_cx, |editor, window, cx| {
+ editor.move_down(&editor::actions::MoveDown, window, cx);
+ editor.move_down(&editor::actions::MoveDown, window, cx);
+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
+ });
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ assert!(
+ called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
+ "SetBreakpoint request must be called"
+ );
+
+ let called_set_breakpoints = Arc::new(AtomicBool::new(false));
+ client
+ .on_request::({
+ let called_set_breakpoints = called_set_breakpoints.clone();
+ move |_, args| {
+ assert_eq!("/project/test.txt", args.source.path.unwrap());
+ let mut breakpoints = args.breakpoints.unwrap();
+ breakpoints.sort_by_key(|b| b.line);
+ assert_eq!(
+ vec![
+ SourceBreakpoint {
+ line: 2,
+ column: None,
+ condition: None,
+ hit_condition: None,
+ log_message: None,
+ mode: None
+ },
+ SourceBreakpoint {
+ line: 3,
+ column: None,
+ condition: None,
+ hit_condition: None,
+ log_message: None,
+ mode: None
+ }
+ ],
+ breakpoints
+ );
+ assert!(!args.source_modified.unwrap());
+
+ called_set_breakpoints.store(true, Ordering::SeqCst);
+
+ Ok(dap::SetBreakpointsResponse {
+ breakpoints: Vec::default(),
+ })
+ }
+ })
+ .await;
+
+ // Add our own breakpoint now
+ editor_a.update_in(host_cx, |editor, window, cx| {
+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
+ editor.move_up(&editor::actions::MoveUp, window, cx);
+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
+ });
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ assert!(
+ called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
+ "SetBreakpoint request must be called"
+ );
+
+ let shutdown_client = host_project.update(host_cx, |project, cx| {
+ project.dap_store().update(cx, |dap_store, cx| {
+ dap_store.shutdown_session(&session.read(cx).id(), cx)
+ })
+ });
+
+ shutdown_client.await.unwrap();
+}
+
+#[gpui::test]
+async fn test_module_list(
+ host_cx: &mut TestAppContext,
+ remote_cx: &mut TestAppContext,
+ late_join_cx: &mut TestAppContext,
+) {
+ let executor = host_cx.executor();
+ let mut server = TestServer::start(executor).await;
+
+ let (mut host_zed, mut remote_zed, mut late_join_zed) =
+ setup_three_member_test(&mut server, host_cx, remote_cx, late_join_cx).await;
+
+ let (host_project_id, _worktree_id) = host_zed.host_project(None).await;
+
+ remote_zed.join_project(host_project_id).await;
+
+ let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await;
+ let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await;
+
+ let task = host_project.update(host_cx, |project, cx| {
+ project.start_debug_session(dap::test_config(), cx)
+ });
+
+ let (session, client) = task.await.unwrap();
+
+ let called_initialize = Arc::new(AtomicBool::new(false));
+
+ client
+ .on_request::({
+ let called_initialize = called_initialize.clone();
+ move |_, _| {
+ called_initialize.store(true, Ordering::SeqCst);
+ Ok(dap::Capabilities {
+ supports_restart_frame: Some(true),
+ supports_modules_request: Some(true),
+ ..Default::default()
+ })
+ }
+ })
+ .await;
+
+ client.on_request::(move |_, _| Ok(())).await;
+ client
+ .on_request::(move |_, _| {
+ Ok(dap::StackTraceResponse {
+ stack_frames: Vec::default(),
+ total_frames: None,
+ })
+ })
+ .await;
+
+ let called_modules = Arc::new(AtomicBool::new(false));
+ let modules = vec![
+ dap::Module {
+ id: dap::ModuleId::Number(1),
+ name: "First Module".into(),
+ address_range: None,
+ date_time_stamp: None,
+ path: None,
+ symbol_file_path: None,
+ symbol_status: None,
+ version: None,
+ is_optimized: None,
+ is_user_code: None,
+ },
+ dap::Module {
+ id: dap::ModuleId::Number(2),
+ name: "Second Module".into(),
+ address_range: None,
+ date_time_stamp: None,
+ path: None,
+ symbol_file_path: None,
+ symbol_status: None,
+ version: None,
+ is_optimized: None,
+ is_user_code: None,
+ },
+ ];
+
+ client
+ .on_request::({
+ let called_modules = called_modules.clone();
+ let modules = modules.clone();
+ move |_, _| unsafe {
+ static mut REQUEST_COUNT: i32 = 1;
+ assert_eq!(
+ 1, REQUEST_COUNT,
+ "This request should only be called once from the host"
+ );
+ REQUEST_COUNT += 1;
+ called_modules.store(true, Ordering::SeqCst);
+
+ Ok(dap::ModulesResponse {
+ modules: modules.clone(),
+ total_modules: Some(2u64),
+ })
+ }
+ })
+ .await;
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ assert!(
+ called_initialize.load(std::sync::atomic::Ordering::SeqCst),
+ "Request Initialize must be called"
+ );
+
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ assert!(
+ called_modules.load(std::sync::atomic::Ordering::SeqCst),
+ "Request Modules must be called"
+ );
+
+ host_workspace.update(host_cx, |workspace, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+ let debug_panel_item = debug_panel
+ .update(cx, |this, cx| this.active_debug_panel_item(cx))
+ .unwrap();
+
+ debug_panel_item.update(cx, |item, cx| {
+ assert_eq!(
+ true,
+ item.capabilities(cx).supports_modules_request.unwrap(),
+ "Local supports modules request should be true"
+ );
+
+ let local_module_list = item.module_list().update(cx, |list, cx| list.modules(cx));
+
+ assert_eq!(
+ 2usize,
+ local_module_list.len(),
+ "Local module list should have two items in it"
+ );
+ assert_eq!(
+ modules.clone(),
+ local_module_list,
+ "Local module list should match module list from response"
+ );
+ })
+ });
+
+ remote_workspace.update(remote_cx, |workspace, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+ let debug_panel_item = debug_panel
+ .update(cx, |this, cx| this.active_debug_panel_item(cx))
+ .unwrap();
+
+ debug_panel_item.update(cx, |item, cx| {
+ assert_eq!(
+ true,
+ item.capabilities(cx).supports_modules_request.unwrap(),
+ "Remote capabilities supports modules request should be true"
+ );
+ let remote_module_list = item.module_list().update(cx, |list, cx| list.modules(cx));
+
+ assert_eq!(
+ 2usize,
+ remote_module_list.len(),
+ "Remote module list should have two items in it"
+ );
+ assert_eq!(
+ modules.clone(),
+ remote_module_list,
+ "Remote module list should match module list from response"
+ );
+ })
+ });
+
+ late_join_zed.join_project(host_project_id).await;
+ let (_late_join_client, late_join_workspace, _late_join_project, late_join_cx) =
+ late_join_zed.expand().await;
+
+ late_join_workspace.update(late_join_cx, |workspace, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+ let debug_panel_item = debug_panel
+ .update(cx, |this, cx| this.active_debug_panel_item(cx))
+ .unwrap();
+
+ debug_panel_item.update(cx, |item, cx| {
+ assert_eq!(
+ true,
+ item.capabilities(cx).supports_modules_request.unwrap(),
+ "Remote (mid session join) capabilities supports modules request should be true"
+ );
+ let remote_module_list = item.module_list().update(cx, |list, cx| list.modules(cx));
+
+ assert_eq!(
+ 2usize,
+ remote_module_list.len(),
+ "Remote (mid session join) module list should have two items in it"
+ );
+ assert_eq!(
+ modules.clone(),
+ remote_module_list,
+ "Remote (mid session join) module list should match module list from response"
+ );
+ })
+ });
+
+ client.on_request::(move |_, _| Ok(())).await;
+
+ let shutdown_client = host_project.update(host_cx, |project, cx| {
+ project.dap_store().update(cx, |dap_store, cx| {
+ dap_store.shutdown_session(&session.read(cx).id(), cx)
+ })
+ });
+
+ shutdown_client.await.unwrap();
+}
+
+// #[gpui::test]
+// async fn test_variable_list(
+// host_cx: &mut TestAppContext,
+// remote_cx: &mut TestAppContext,
+// late_join_cx: &mut TestAppContext,
+// ) {
+// let executor = host_cx.executor();
+// let mut server = TestServer::start(executor).await;
+
+// let (mut host_zed, mut remote_zed, mut late_join_zed) =
+// setup_three_member_test(&mut server, host_cx, remote_cx, late_join_cx).await;
+
+// let (host_project_id, _worktree_id) = host_zed
+// .host_project(Some(json!({"test.txt": "one\ntwo\nthree\nfour\nfive"})))
+// .await;
+
+// remote_zed.join_project(host_project_id).await;
+
+// let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await;
+// let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await;
+
+// let task = host_project.update(host_cx, |project, cx| {
+// project.start_debug_session(
+// dap::DebugAdapterConfig {
+// label: "test config".into(),
+// kind: dap::DebugAdapterKind::Fake,
+// request: dap::DebugRequestType::Launch,
+// program: None,
+// cwd: None,
+// initialize_args: None,
+// },
+// cx,
+// )
+// });
+
+// let (session, client) = task.await.unwrap();
+
+// client
+// .on_request::(move |_, _| {
+// Ok(dap::Capabilities {
+// supports_step_back: Some(true),
+// ..Default::default()
+// })
+// })
+// .await;
+
+// client.on_request::(move |_, _| Ok(())).await;
+
+// let stack_frames = vec![dap::StackFrame {
+// id: 1,
+// name: "Stack Frame 1".into(),
+// source: Some(dap::Source {
+// name: Some("test.js".into()),
+// path: Some("/project/src/test.js".into()),
+// source_reference: None,
+// presentation_hint: None,
+// origin: None,
+// sources: None,
+// adapter_data: None,
+// checksums: None,
+// }),
+// line: 1,
+// column: 1,
+// end_line: None,
+// end_column: None,
+// can_restart: None,
+// instruction_pointer_reference: None,
+// module_id: None,
+// presentation_hint: None,
+// }];
+
+// let scopes = vec![Scope {
+// name: "Scope 1".into(),
+// presentation_hint: None,
+// variables_reference: 1,
+// named_variables: None,
+// indexed_variables: None,
+// expensive: false,
+// source: None,
+// line: None,
+// column: None,
+// end_line: None,
+// end_column: None,
+// }];
+
+// let variable_1 = Variable {
+// name: "variable 1".into(),
+// value: "1".into(),
+// type_: None,
+// presentation_hint: None,
+// evaluate_name: None,
+// variables_reference: 2,
+// named_variables: None,
+// indexed_variables: None,
+// memory_reference: None,
+// };
+
+// let variable_2 = Variable {
+// name: "variable 2".into(),
+// value: "2".into(),
+// type_: None,
+// presentation_hint: None,
+// evaluate_name: None,
+// variables_reference: 3,
+// named_variables: None,
+// indexed_variables: None,
+// memory_reference: None,
+// };
+
+// let variable_3 = Variable {
+// name: "variable 3".into(),
+// value: "hello world".into(),
+// type_: None,
+// presentation_hint: None,
+// evaluate_name: None,
+// variables_reference: 4,
+// named_variables: None,
+// indexed_variables: None,
+// memory_reference: None,
+// };
+
+// let variable_4 = Variable {
+// name: "variable 4".into(),
+// value: "hello world this is the final variable".into(),
+// type_: None,
+// presentation_hint: None,
+// evaluate_name: None,
+// variables_reference: 0,
+// named_variables: None,
+// indexed_variables: None,
+// memory_reference: None,
+// };
+
+// client
+// .on_request::({
+// let stack_frames = std::sync::Arc::new(stack_frames.clone());
+// move |_, args| {
+// assert_eq!(1, args.thread_id);
+
+// Ok(dap::StackTraceResponse {
+// stack_frames: (*stack_frames).clone(),
+// total_frames: None,
+// })
+// }
+// })
+// .await;
+
+// client
+// .on_request::({
+// let scopes = Arc::new(scopes.clone());
+// move |_, args| {
+// assert_eq!(1, args.frame_id);
+
+// Ok(dap::ScopesResponse {
+// scopes: (*scopes).clone(),
+// })
+// }
+// })
+// .await;
+
+// let first_variable_request = vec![variable_1.clone(), variable_2.clone()];
+
+// client
+// .on_request::({
+// move |_, args| {
+// assert_eq!(1, args.variables_reference);
+
+// Ok(dap::VariablesResponse {
+// variables: first_variable_request.clone(),
+// })
+// }
+// })
+// .await;
+
+// client
+// .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+// reason: dap::StoppedEventReason::Pause,
+// description: None,
+// thread_id: Some(1),
+// preserve_focus_hint: None,
+// text: None,
+// all_threads_stopped: None,
+// hit_breakpoint_ids: None,
+// }))
+// .await;
+
+// host_cx.run_until_parked();
+// remote_cx.run_until_parked();
+
+// let local_debug_item = host_workspace.update(host_cx, |workspace, cx| {
+// let debug_panel = workspace.panel::(cx).unwrap();
+// let active_debug_panel_item = debug_panel
+// .update(cx, |this, cx| this.active_debug_panel_item(cx))
+// .unwrap();
+
+// assert_eq!(
+// 1,
+// debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
+// );
+// assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id());
+// assert_eq!(1, active_debug_panel_item.read(cx).thread_id());
+// active_debug_panel_item
+// });
+
+// let remote_debug_item = remote_workspace.update(remote_cx, |workspace, cx| {
+// let debug_panel = workspace.panel::(cx).unwrap();
+// let active_debug_panel_item = debug_panel
+// .update(cx, |this, cx| this.active_debug_panel_item(cx))
+// .unwrap();
+
+// assert_eq!(
+// 1,
+// debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
+// );
+// assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id());
+// assert_eq!(1, active_debug_panel_item.read(cx).thread_id());
+// active_debug_panel_item
+// });
+
+// let first_visual_entries = vec!["v Scope 1", " > variable 1", " > variable 2"];
+// let first_variable_containers = vec![
+// VariableContainer {
+// container_reference: scopes[0].variables_reference,
+// variable: variable_1.clone(),
+// depth: 1,
+// },
+// VariableContainer {
+// container_reference: scopes[0].variables_reference,
+// variable: variable_2.clone(),
+// depth: 1,
+// },
+// ];
+
+// local_debug_item
+// .update(host_cx, |this, _| this.variable_list().clone())
+// .update(host_cx, |variable_list, cx| {
+// assert_eq!(1, variable_list.scopes().len());
+// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone());
+// assert_eq!(&first_variable_containers, &variable_list.variables());
+
+// variable_list.assert_visual_entries(first_visual_entries.clone(), cx);
+// });
+
+// client
+// .on_request::({
+// let variables = Arc::new(vec![variable_3.clone()]);
+// move |_, args| {
+// assert_eq!(2, args.variables_reference);
+
+// Ok(dap::VariablesResponse {
+// variables: (*variables).clone(),
+// })
+// }
+// })
+// .await;
+
+// remote_debug_item
+// .update(remote_cx, |this, _| this.variable_list().clone())
+// .update(remote_cx, |variable_list, cx| {
+// assert_eq!(1, variable_list.scopes().len());
+// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone());
+// assert_eq!(&first_variable_containers, &variable_list.variables());
+
+// variable_list.assert_visual_entries(first_visual_entries.clone(), cx);
+
+// variable_list.toggle_variable(&scopes[0], &variable_1, 1, cx);
+// });
+
+// host_cx.run_until_parked();
+// remote_cx.run_until_parked();
+
+// let second_req_variable_list = vec![
+// VariableContainer {
+// container_reference: scopes[0].variables_reference,
+// variable: variable_1.clone(),
+// depth: 1,
+// },
+// VariableContainer {
+// container_reference: variable_1.variables_reference,
+// variable: variable_3.clone(),
+// depth: 2,
+// },
+// VariableContainer {
+// container_reference: scopes[0].variables_reference,
+// variable: variable_2.clone(),
+// depth: 1,
+// },
+// ];
+
+// remote_debug_item
+// .update(remote_cx, |this, _| this.variable_list().clone())
+// .update(remote_cx, |variable_list, cx| {
+// assert_eq!(1, variable_list.scopes().len());
+// assert_eq!(3, variable_list.variables().len());
+// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone());
+// assert_eq!(&second_req_variable_list, &variable_list.variables());
+
+// variable_list.assert_visual_entries(
+// vec![
+// "v Scope 1",
+// " v variable 1",
+// " > variable 3",
+// " > variable 2",
+// ],
+// cx,
+// );
+// });
+
+// client
+// .on_request::({
+// let variables = Arc::new(vec![variable_4.clone()]);
+// move |_, args| {
+// assert_eq!(3, args.variables_reference);
+
+// Ok(dap::VariablesResponse {
+// variables: (*variables).clone(),
+// })
+// }
+// })
+// .await;
+
+// local_debug_item
+// .update(host_cx, |this, _| this.variable_list().clone())
+// .update(host_cx, |variable_list, cx| {
+// assert_eq!(1, variable_list.scopes().len());
+// assert_eq!(3, variable_list.variables().len());
+// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone());
+// assert_eq!(&second_req_variable_list, &variable_list.variables());
+
+// variable_list.assert_visual_entries(first_visual_entries.clone(), cx);
+
+// variable_list.toggle_variable(&scopes[0], &variable_2.clone(), 1, cx);
+// });
+
+// host_cx.run_until_parked();
+// remote_cx.run_until_parked();
+
+// let final_variable_containers: Vec = vec![
+// VariableContainer {
+// container_reference: scopes[0].variables_reference,
+// variable: variable_1.clone(),
+// depth: 1,
+// },
+// VariableContainer {
+// container_reference: variable_1.variables_reference,
+// variable: variable_3.clone(),
+// depth: 2,
+// },
+// VariableContainer {
+// container_reference: scopes[0].variables_reference,
+// variable: variable_2.clone(),
+// depth: 1,
+// },
+// VariableContainer {
+// container_reference: variable_2.variables_reference,
+// variable: variable_4.clone(),
+// depth: 2,
+// },
+// ];
+
+// remote_debug_item
+// .update(remote_cx, |this, _| this.variable_list().clone())
+// .update(remote_cx, |variable_list, cx| {
+// assert_eq!(1, variable_list.scopes().len());
+// assert_eq!(4, variable_list.variables().len());
+// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone());
+// assert_eq!(&final_variable_containers, &variable_list.variables());
+
+// variable_list.assert_visual_entries(
+// vec![
+// "v Scope 1",
+// " v variable 1",
+// " > variable 3",
+// " > variable 2",
+// ],
+// cx,
+// );
+// });
+
+// local_debug_item
+// .update(host_cx, |this, _| this.variable_list().clone())
+// .update(host_cx, |variable_list, cx| {
+// assert_eq!(1, variable_list.scopes().len());
+// assert_eq!(4, variable_list.variables().len());
+// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone());
+// assert_eq!(&final_variable_containers, &variable_list.variables());
+
+// variable_list.assert_visual_entries(
+// vec![
+// "v Scope 1",
+// " > variable 1",
+// " v variable 2",
+// " > variable 4",
+// ],
+// cx,
+// );
+// });
+
+// late_join_zed.join_project(host_project_id).await;
+// let (_late_join_client, late_join_workspace, _late_join_project, late_join_cx) =
+// late_join_zed.expand().await;
+
+// late_join_cx.run_until_parked();
+
+// let last_join_remote_item = late_join_workspace.update(late_join_cx, |workspace, cx| {
+// let debug_panel = workspace.panel::(cx).unwrap();
+// let active_debug_panel_item = debug_panel
+// .update(cx, |this, cx| this.active_debug_panel_item(cx))
+// .unwrap();
+
+// assert_eq!(
+// 1,
+// debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
+// );
+// assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id());
+// assert_eq!(1, active_debug_panel_item.read(cx).thread_id());
+// active_debug_panel_item
+// });
+
+// last_join_remote_item
+// .update(late_join_cx, |this, _| this.variable_list().clone())
+// .update(late_join_cx, |variable_list, cx| {
+// assert_eq!(1, variable_list.scopes().len());
+// assert_eq!(4, variable_list.variables().len());
+// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone());
+// assert_eq!(final_variable_containers, variable_list.variables());
+
+// variable_list.assert_visual_entries(first_visual_entries, cx);
+// });
+
+// client.on_request::(move |_, _| Ok(())).await;
+
+// let shutdown_client = host_project.update(host_cx, |project, cx| {
+// project.dap_store().update(cx, |dap_store, cx| {
+// dap_store.shutdown_session(&session.read(cx).id(), cx)
+// })
+// });
+
+// shutdown_client.await.unwrap();
+// }
+
+#[gpui::test]
+async fn test_ignore_breakpoints(
+ host_cx: &mut TestAppContext,
+ remote_cx: &mut TestAppContext,
+ cx_c: &mut TestAppContext,
+) {
+ let executor = host_cx.executor();
+ let mut server = TestServer::start(executor).await;
+
+ let (mut host_zed, mut remote_zed, mut late_join_zed) =
+ setup_three_member_test(&mut server, host_cx, remote_cx, cx_c).await;
+
+ let (host_project_id, worktree_id) = host_zed
+ .host_project(Some(json!({"test.txt": "one\ntwo\nthree\nfour\nfive"})))
+ .await;
+
+ remote_zed.join_project(host_project_id).await;
+
+ let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await;
+ let (_client_remote, remote_workspace, remote_project, remote_cx) = remote_zed.expand().await;
+
+ let project_path = ProjectPath {
+ worktree_id,
+ path: Arc::from(Path::new(&"test.txt")),
+ };
+
+ let local_editor = host_workspace
+ .update_in(host_cx, |workspace, window, cx| {
+ workspace.open_path(project_path.clone(), None, true, window, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::()
+ .unwrap();
+
+ local_editor.update_in(host_cx, |editor, window, cx| {
+ editor.move_down(&editor::actions::MoveDown, window, cx);
+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); // Line 2
+ editor.move_down(&editor::actions::MoveDown, window, cx);
+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
+ // Line 3
+ });
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ let task = host_project.update(host_cx, |project, cx| {
+ project.start_debug_session(dap::test_config(), cx)
+ });
+
+ let (session, client) = task.await.unwrap();
+ let client_id = client.id();
+
+ client
+ .on_request::(move |_, _| {
+ Ok(dap::Capabilities {
+ supports_configuration_done_request: Some(true),
+ ..Default::default()
+ })
+ })
+ .await;
+
+ let called_set_breakpoints = Arc::new(AtomicBool::new(false));
+ client
+ .on_request::({
+ let called_set_breakpoints = called_set_breakpoints.clone();
+ move |_, args| {
+ assert_eq!("/project/test.txt", args.source.path.unwrap());
+
+ let mut actual_breakpoints = args.breakpoints.unwrap();
+ actual_breakpoints.sort_by_key(|b| b.line);
+
+ let expected_breakpoints = vec![
+ SourceBreakpoint {
+ line: 2,
+ column: None,
+ condition: None,
+ hit_condition: None,
+ log_message: None,
+ mode: None,
+ },
+ SourceBreakpoint {
+ line: 3,
+ column: None,
+ condition: None,
+ hit_condition: None,
+ log_message: None,
+ mode: None,
+ },
+ ];
+
+ assert_eq!(actual_breakpoints, expected_breakpoints);
+
+ called_set_breakpoints.store(true, Ordering::SeqCst);
+
+ Ok(dap::SetBreakpointsResponse {
+ breakpoints: Vec::default(),
+ })
+ }
+ })
+ .await;
+
+ client.on_request::(move |_, _| Ok(())).await;
+ client
+ .on_request::(move |_, _| {
+ Ok(dap::StackTraceResponse {
+ stack_frames: Vec::default(),
+ total_frames: None,
+ })
+ })
+ .await;
+
+ client.on_request::(move |_, _| Ok(())).await;
+
+ client
+ .fake_event(dap::messages::Events::Initialized(Some(
+ dap::Capabilities {
+ supports_configuration_done_request: Some(true),
+ ..Default::default()
+ },
+ )))
+ .await;
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ assert!(
+ called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
+ "SetBreakpoint request must be called when starting debug session"
+ );
+
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ let remote_debug_item = remote_workspace.update(remote_cx, |workspace, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+ let active_debug_panel_item = debug_panel
+ .update(cx, |this, cx| this.active_debug_panel_item(cx))
+ .unwrap();
+
+ assert_eq!(
+ 1,
+ debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
+ );
+
+ let session_id = debug_panel.update(cx, |this, cx| {
+ this.dap_store()
+ .read(cx)
+ .session_by_client_id(client.id())
+ .unwrap()
+ .read(cx)
+ .id()
+ });
+
+ let breakpoints_ignored = active_debug_panel_item.read(cx).are_breakpoints_ignored(cx);
+
+ assert_eq!(
+ session_id,
+ active_debug_panel_item.read(cx).session().read(cx).id()
+ );
+ assert_eq!(false, breakpoints_ignored);
+ assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id());
+ assert_eq!(1, active_debug_panel_item.read(cx).thread_id().0);
+ active_debug_panel_item
+ });
+
+ called_set_breakpoints.store(false, Ordering::SeqCst);
+
+ client
+ .on_request::({
+ let called_set_breakpoints = called_set_breakpoints.clone();
+ move |_, args| {
+ assert_eq!("/project/test.txt", args.source.path.unwrap());
+ assert_eq!(args.breakpoints, Some(vec![]));
+
+ called_set_breakpoints.store(true, Ordering::SeqCst);
+
+ Ok(dap::SetBreakpointsResponse {
+ breakpoints: Vec::default(),
+ })
+ }
+ })
+ .await;
+
+ let local_debug_item = host_workspace.update(host_cx, |workspace, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+ let active_debug_panel_item = debug_panel
+ .update(cx, |this, cx| this.active_debug_panel_item(cx))
+ .unwrap();
+
+ assert_eq!(
+ 1,
+ debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
+ );
+
+ assert_eq!(
+ false,
+ active_debug_panel_item.read(cx).are_breakpoints_ignored(cx)
+ );
+ assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id());
+ assert_eq!(1, active_debug_panel_item.read(cx).thread_id().0);
+
+ active_debug_panel_item
+ });
+
+ local_debug_item.update(host_cx, |item, cx| {
+ item.toggle_ignore_breakpoints(cx); // Set to true
+ assert_eq!(true, item.are_breakpoints_ignored(cx));
+ });
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ assert!(
+ called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
+ "SetBreakpoint request must be called to ignore breakpoints"
+ );
+
+ client
+ .on_request::({
+ let called_set_breakpoints = called_set_breakpoints.clone();
+ move |_, _args| {
+ called_set_breakpoints.store(true, Ordering::SeqCst);
+
+ Ok(dap::SetBreakpointsResponse {
+ breakpoints: Vec::default(),
+ })
+ }
+ })
+ .await;
+
+ let remote_editor = remote_workspace
+ .update_in(remote_cx, |workspace, window, cx| {
+ workspace.open_path(project_path.clone(), None, true, window, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::()
+ .unwrap();
+
+ called_set_breakpoints.store(false, std::sync::atomic::Ordering::SeqCst);
+
+ remote_editor.update_in(remote_cx, |editor, window, cx| {
+ // Line 1
+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
+ });
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ assert!(
+ called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
+ "SetBreakpoint request be called whenever breakpoints are toggled but with not breakpoints"
+ );
+
+ remote_debug_item.update(remote_cx, |debug_panel, cx| {
+ let breakpoints_ignored = debug_panel.are_breakpoints_ignored(cx);
+
+ assert_eq!(true, breakpoints_ignored);
+ assert_eq!(client.id(), debug_panel.client_id());
+ assert_eq!(1, debug_panel.thread_id().0);
+ });
+
+ client
+ .on_request::({
+ let called_set_breakpoints = called_set_breakpoints.clone();
+ move |_, args| {
+ assert_eq!("/project/test.txt", args.source.path.unwrap());
+
+ let mut actual_breakpoints = args.breakpoints.unwrap();
+ actual_breakpoints.sort_by_key(|b| b.line);
+
+ let expected_breakpoints = vec![
+ SourceBreakpoint {
+ line: 1,
+ column: None,
+ condition: None,
+ hit_condition: None,
+ log_message: None,
+ mode: None,
+ },
+ SourceBreakpoint {
+ line: 2,
+ column: None,
+ condition: None,
+ hit_condition: None,
+ log_message: None,
+ mode: None,
+ },
+ SourceBreakpoint {
+ line: 3,
+ column: None,
+ condition: None,
+ hit_condition: None,
+ log_message: None,
+ mode: None,
+ },
+ ];
+
+ assert_eq!(actual_breakpoints, expected_breakpoints);
+
+ called_set_breakpoints.store(true, Ordering::SeqCst);
+
+ Ok(dap::SetBreakpointsResponse {
+ breakpoints: Vec::default(),
+ })
+ }
+ })
+ .await;
+
+ late_join_zed.join_project(host_project_id).await;
+ let (_late_join_client, late_join_workspace, late_join_project, late_join_cx) =
+ late_join_zed.expand().await;
+
+ late_join_cx.run_until_parked();
+
+ let last_join_remote_item = late_join_workspace.update(late_join_cx, |workspace, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+ let active_debug_panel_item = debug_panel
+ .update(cx, |this, cx| this.active_debug_panel_item(cx))
+ .unwrap();
+
+ let breakpoints_ignored = active_debug_panel_item.read(cx).are_breakpoints_ignored(cx);
+
+ assert_eq!(true, breakpoints_ignored);
+
+ assert_eq!(
+ 1,
+ debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
+ );
+ assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id());
+ assert_eq!(1, active_debug_panel_item.read(cx).thread_id().0);
+ active_debug_panel_item
+ });
+
+ remote_debug_item.update(remote_cx, |item, cx| {
+ item.toggle_ignore_breakpoints(cx);
+ });
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+ late_join_cx.run_until_parked();
+
+ assert!(
+ called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
+ "SetBreakpoint request should be called to update breakpoints"
+ );
+
+ client
+ .on_request::({
+ let called_set_breakpoints = called_set_breakpoints.clone();
+ move |_, args| {
+ assert_eq!("/project/test.txt", args.source.path.unwrap());
+ assert_eq!(args.breakpoints, Some(vec![]));
+
+ called_set_breakpoints.store(true, Ordering::SeqCst);
+
+ Ok(dap::SetBreakpointsResponse {
+ breakpoints: Vec::default(),
+ })
+ }
+ })
+ .await;
+
+ local_debug_item.update(host_cx, |debug_panel_item, cx| {
+ assert_eq!(
+ false,
+ debug_panel_item.are_breakpoints_ignored(cx),
+ "Remote client set this to false"
+ );
+ });
+
+ remote_debug_item.update(remote_cx, |debug_panel_item, cx| {
+ assert_eq!(
+ false,
+ debug_panel_item.are_breakpoints_ignored(cx),
+ "Remote client set this to false"
+ );
+ });
+
+ last_join_remote_item.update(late_join_cx, |debug_panel_item, cx| {
+ assert_eq!(
+ false,
+ debug_panel_item.are_breakpoints_ignored(cx),
+ "Remote client set this to false"
+ );
+ });
+
+ let shutdown_client = host_project.update(host_cx, |project, cx| {
+ project.dap_store().update(cx, |dap_store, cx| {
+ dap_store.shutdown_session(&session.read(cx).id(), cx)
+ })
+ });
+
+ shutdown_client.await.unwrap();
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ remote_project.update(remote_cx, |project, cx| {
+ project.dap_store().update(cx, |dap_store, _cx| {
+ let sessions = dap_store.sessions().collect::>();
+
+ assert_eq!(
+ None,
+ dap_store.session_by_client_id(&client_id),
+ "No client_id to session mapping should exist after shutdown"
+ );
+ assert_eq!(
+ 0,
+ sessions.len(),
+ "No sessions should be left after shutdown"
+ );
+ })
+ });
+
+ late_join_project.update(late_join_cx, |project, cx| {
+ project.dap_store().update(cx, |dap_store, _cx| {
+ let sessions = dap_store.sessions().collect::>();
+
+ assert_eq!(
+ None,
+ dap_store.session_by_client_id(&client_id),
+ "No client_id to session mapping should exist after shutdown"
+ );
+ assert_eq!(
+ 0,
+ sessions.len(),
+ "No sessions should be left after shutdown"
+ );
+ })
+ });
+}
+
+#[gpui::test]
+async fn test_debug_panel_console(host_cx: &mut TestAppContext, remote_cx: &mut TestAppContext) {
+ let executor = host_cx.executor();
+ let mut server = TestServer::start(executor).await;
+
+ let (mut host_zed, mut remote_zed) =
+ setup_two_member_test(&mut server, host_cx, remote_cx).await;
+
+ let (host_project_id, _) = host_zed.host_project(None).await;
+ remote_zed.join_project(host_project_id).await;
+
+ let (_client_host, _host_workspace, host_project, host_cx) = host_zed.expand().await;
+ let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await;
+
+ remote_cx.run_until_parked();
+
+ let task = host_project.update(host_cx, |project, cx| {
+ project.start_debug_session(dap::test_config(), cx)
+ });
+
+ let (session, client) = task.await.unwrap();
+
+ client
+ .on_request::(move |_, _| {
+ Ok(dap::Capabilities {
+ supports_step_back: Some(false),
+ ..Default::default()
+ })
+ })
+ .await;
+
+ client.on_request::(move |_, _| Ok(())).await;
+
+ client
+ .on_request::(move |_, _| {
+ Ok(dap::StackTraceResponse {
+ stack_frames: Vec::default(),
+ total_frames: None,
+ })
+ })
+ .await;
+
+ client.on_request::(move |_, _| Ok(())).await;
+
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ client
+ .fake_event(dap::messages::Events::Output(dap::OutputEvent {
+ category: None,
+ output: "First line".to_string(),
+ data: None,
+ variables_reference: None,
+ source: None,
+ line: None,
+ column: None,
+ group: None,
+ }))
+ .await;
+
+ client
+ .fake_event(dap::messages::Events::Output(dap::OutputEvent {
+ category: Some(dap::OutputEventCategory::Stdout),
+ output: "First group".to_string(),
+ data: None,
+ variables_reference: None,
+ source: None,
+ line: None,
+ column: None,
+ group: Some(dap::OutputEventGroup::Start),
+ }))
+ .await;
+
+ client
+ .fake_event(dap::messages::Events::Output(dap::OutputEvent {
+ category: Some(dap::OutputEventCategory::Stdout),
+ output: "First item in group 1".to_string(),
+ data: None,
+ variables_reference: None,
+ source: None,
+ line: None,
+ column: None,
+ group: None,
+ }))
+ .await;
+
+ client
+ .fake_event(dap::messages::Events::Output(dap::OutputEvent {
+ category: Some(dap::OutputEventCategory::Stdout),
+ output: "Second item in group 1".to_string(),
+ data: None,
+ variables_reference: None,
+ source: None,
+ line: None,
+ column: None,
+ group: None,
+ }))
+ .await;
+
+ client
+ .fake_event(dap::messages::Events::Output(dap::OutputEvent {
+ category: Some(dap::OutputEventCategory::Stdout),
+ output: "Second group".to_string(),
+ data: None,
+ variables_reference: None,
+ source: None,
+ line: None,
+ column: None,
+ group: Some(dap::OutputEventGroup::Start),
+ }))
+ .await;
+
+ client
+ .fake_event(dap::messages::Events::Output(dap::OutputEvent {
+ category: Some(dap::OutputEventCategory::Stdout),
+ output: "First item in group 2".to_string(),
+ data: None,
+ variables_reference: None,
+ source: None,
+ line: None,
+ column: None,
+ group: None,
+ }))
+ .await;
+
+ client
+ .fake_event(dap::messages::Events::Output(dap::OutputEvent {
+ category: Some(dap::OutputEventCategory::Stdout),
+ output: "Second item in group 2".to_string(),
+ data: None,
+ variables_reference: None,
+ source: None,
+ line: None,
+ column: None,
+ group: None,
+ }))
+ .await;
+
+ client
+ .fake_event(dap::messages::Events::Output(dap::OutputEvent {
+ category: Some(dap::OutputEventCategory::Stdout),
+ output: "End group 2".to_string(),
+ data: None,
+ variables_reference: None,
+ source: None,
+ line: None,
+ column: None,
+ group: Some(dap::OutputEventGroup::End),
+ }))
+ .await;
+
+ client
+ .fake_event(dap::messages::Events::Output(dap::OutputEvent {
+ category: Some(dap::OutputEventCategory::Stdout),
+ output: "Third group".to_string(),
+ data: None,
+ variables_reference: None,
+ source: None,
+ line: None,
+ column: None,
+ group: Some(dap::OutputEventGroup::StartCollapsed),
+ }))
+ .await;
+
+ client
+ .fake_event(dap::messages::Events::Output(dap::OutputEvent {
+ category: Some(dap::OutputEventCategory::Stdout),
+ output: "First item in group 3".to_string(),
+ data: None,
+ variables_reference: None,
+ source: None,
+ line: None,
+ column: None,
+ group: None,
+ }))
+ .await;
+
+ client
+ .fake_event(dap::messages::Events::Output(dap::OutputEvent {
+ category: Some(dap::OutputEventCategory::Stdout),
+ output: "Second item in group 3".to_string(),
+ data: None,
+ variables_reference: None,
+ source: None,
+ line: None,
+ column: None,
+ group: None,
+ }))
+ .await;
+
+ client
+ .fake_event(dap::messages::Events::Output(dap::OutputEvent {
+ category: Some(dap::OutputEventCategory::Stdout),
+ output: "End group 3".to_string(),
+ data: None,
+ variables_reference: None,
+ source: None,
+ line: None,
+ column: None,
+ group: Some(dap::OutputEventGroup::End),
+ }))
+ .await;
+
+ client
+ .fake_event(dap::messages::Events::Output(dap::OutputEvent {
+ category: Some(dap::OutputEventCategory::Stdout),
+ output: "Third item in group 1".to_string(),
+ data: None,
+ variables_reference: None,
+ source: None,
+ line: None,
+ column: None,
+ group: None,
+ }))
+ .await;
+
+ client
+ .fake_event(dap::messages::Events::Output(dap::OutputEvent {
+ category: Some(dap::OutputEventCategory::Stdout),
+ output: "Second item".to_string(),
+ data: None,
+ variables_reference: None,
+ source: None,
+ line: None,
+ column: None,
+ group: Some(dap::OutputEventGroup::End),
+ }))
+ .await;
+
+ host_cx.run_until_parked();
+ remote_cx.run_until_parked();
+
+ active_debug_panel_item(remote_workspace, remote_cx).update(
+ remote_cx,
+ |debug_panel_item, cx| {
+ debug_panel_item.console().update(cx, |console, cx| {
+ console.editor().update(cx, |editor, cx| {
+ pretty_assertions::assert_eq!(
+ "
+ ()
+ .unwrap();
+
+ // Client B opens same editor as A.
+ let editor_b = workspace_b
+ .update_in(cx_b, |workspace, window, cx| {
+ workspace.open_path(project_path.clone(), None, true, window, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::()
+ .unwrap();
+
+ cx_a.run_until_parked();
+ cx_b.run_until_parked();
+
+ // Client A adds breakpoint on line (1)
+ editor_a.update_in(cx_a, |editor, window, cx| {
+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
+ });
+
+ cx_a.run_until_parked();
+ cx_b.run_until_parked();
+
+ let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
+ editor
+ .breakpoint_store()
+ .clone()
+ .unwrap()
+ .read(cx)
+ .breakpoints()
+ .clone()
+ });
+ let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
+ editor
+ .breakpoint_store()
+ .clone()
+ .unwrap()
+ .read(cx)
+ .breakpoints()
+ .clone()
+ });
+
+ assert_eq!(1, breakpoints_a.len());
+ assert_eq!(1, breakpoints_a.get(&project_path).unwrap().len());
+ assert_eq!(breakpoints_a, breakpoints_b);
+
+ // Client B adds breakpoint on line(2)
+ editor_b.update_in(cx_b, |editor, window, cx| {
+ editor.move_down(&editor::actions::MoveDown, window, cx);
+ editor.move_down(&editor::actions::MoveDown, window, cx);
+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
+ });
+
+ cx_a.run_until_parked();
+ cx_b.run_until_parked();
+
+ let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
+ editor
+ .breakpoint_store()
+ .clone()
+ .unwrap()
+ .read(cx)
+ .breakpoints()
+ .clone()
+ });
+ let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
+ editor
+ .breakpoint_store()
+ .clone()
+ .unwrap()
+ .read(cx)
+ .breakpoints()
+ .clone()
+ });
+
+ assert_eq!(1, breakpoints_a.len());
+ assert_eq!(breakpoints_a, breakpoints_b);
+ assert_eq!(2, breakpoints_a.get(&project_path).unwrap().len());
+
+ // Client A removes last added breakpoint from client B
+ editor_a.update_in(cx_a, |editor, window, cx| {
+ editor.move_down(&editor::actions::MoveDown, window, cx);
+ editor.move_down(&editor::actions::MoveDown, window, cx);
+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
+ });
+
+ cx_a.run_until_parked();
+ cx_b.run_until_parked();
+
+ let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
+ editor
+ .breakpoint_store()
+ .clone()
+ .unwrap()
+ .read(cx)
+ .breakpoints()
+ .clone()
+ });
+ let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
+ editor
+ .breakpoint_store()
+ .clone()
+ .unwrap()
+ .read(cx)
+ .breakpoints()
+ .clone()
+ });
+
+ assert_eq!(1, breakpoints_a.len());
+ assert_eq!(breakpoints_a, breakpoints_b);
+ assert_eq!(1, breakpoints_a.get(&project_path).unwrap().len());
+
+ // Client B removes first added breakpoint by client A
+ editor_b.update_in(cx_b, |editor, window, cx| {
+ editor.move_up(&editor::actions::MoveUp, window, cx);
+ editor.move_up(&editor::actions::MoveUp, window, cx);
+ editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
+ });
+
+ cx_a.run_until_parked();
+ cx_b.run_until_parked();
+
+ let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
+ editor
+ .breakpoint_store()
+ .clone()
+ .unwrap()
+ .read(cx)
+ .breakpoints()
+ .clone()
+ });
+ let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
+ editor
+ .breakpoint_store()
+ .clone()
+ .unwrap()
+ .read(cx)
+ .breakpoints()
+ .clone()
+ });
+
+ assert_eq!(0, breakpoints_a.len());
+ assert_eq!(breakpoints_a, breakpoints_b);
+}
+
#[track_caller]
fn tab_undo_assert(
cx_a: &mut EditorTestContext,
diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml
new file mode 100644
index 00000000000000..f2f1a7f613e8e1
--- /dev/null
+++ b/crates/dap/Cargo.toml
@@ -0,0 +1,54 @@
+[package]
+name = "dap"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[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 = { git = "https://github.com/zed-industries/dap-types", rev = "bf5632dc19f806e8a435c9f04a4bfe7322badea2" }
+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
+schemars.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+settings.workspace = true
+smallvec.workspace = true
+smol.workspace = true
+sysinfo.workspace = true
+task.workspace = true
+util.workspace = true
+
+[dev-dependencies]
+async-pipe.workspace = true
+env_logger.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"] }
diff --git a/crates/dap/LICENSE-GPL b/crates/dap/LICENSE-GPL
new file mode 120000
index 00000000000000..89e542f750cd38
--- /dev/null
+++ b/crates/dap/LICENSE-GPL
@@ -0,0 +1 @@
+../../LICENSE-GPL
\ No newline at end of file
diff --git a/crates/dap/docs/breakpoints.md b/crates/dap/docs/breakpoints.md
new file mode 100644
index 00000000000000..8b819b089bf8c4
--- /dev/null
+++ b/crates/dap/docs/breakpoints.md
@@ -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.
diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs
new file mode 100644
index 00000000000000..40b076b2919a73
--- /dev/null
+++ b/crates/dap/src/adapters.rs
@@ -0,0 +1,399 @@
+use ::fs::Fs;
+use anyhow::{anyhow, Context as _, Ok, Result};
+use async_compression::futures::bufread::GzipDecoder;
+use async_tar::Archive;
+use async_trait::async_trait;
+use futures::io::BufReader;
+use gpui::{AsyncApp, SharedString};
+pub use http_client::{github::latest_github_release, HttpClient};
+use language::LanguageToolchainStore;
+use node_runtime::NodeRuntime;
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use settings::WorktreeId;
+use smol::{self, fs::File, lock::Mutex};
+use std::{
+ collections::{HashMap, HashSet},
+ ffi::{OsStr, OsString},
+ fmt::Debug,
+ net::Ipv4Addr,
+ ops::Deref,
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+use sysinfo::{Pid, Process};
+use task::DebugAdapterConfig;
+use util::ResultExt;
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum DapStatus {
+ None,
+ CheckingForUpdate,
+ Downloading,
+ Failed { error: String },
+}
+
+#[async_trait(?Send)]
+pub trait DapDelegate {
+ fn worktree_id(&self) -> WorktreeId;
+ fn http_client(&self) -> Arc;
+ fn node_runtime(&self) -> NodeRuntime;
+ fn toolchain_store(&self) -> Arc;
+ fn fs(&self) -> Arc;
+ fn updated_adapters(&self) -> Arc>>;
+ fn update_status(&self, dap_name: DebugAdapterName, status: DapStatus);
+ fn which(&self, command: &OsStr) -> Option;
+ async fn shell_env(&self) -> collections::HashMap;
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
+pub struct DebugAdapterName(pub Arc);
+
+impl Deref for DebugAdapterName {
+ type Target = str;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl AsRef for DebugAdapterName {
+ fn as_ref(&self) -> &str {
+ &self.0
+ }
+}
+
+impl AsRef for DebugAdapterName {
+ fn as_ref(&self) -> &Path {
+ Path::new(&*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 for SharedString {
+ fn from(name: DebugAdapterName) -> Self {
+ SharedString::from(name.0)
+ }
+}
+
+impl<'a> From<&'a str> for DebugAdapterName {
+ fn from(str: &'a str) -> DebugAdapterName {
+ DebugAdapterName(str.to_string().into())
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct TcpArguments {
+ pub host: Ipv4Addr,
+ pub port: Option,
+ pub timeout: Option,
+}
+#[derive(Debug, Clone)]
+pub struct DebugAdapterBinary {
+ pub command: String,
+ pub arguments: Option>,
+ pub envs: Option>,
+ pub cwd: Option,
+ pub connection: Option,
+ #[cfg(any(test, feature = "test-support"))]
+ // todo(debugger) Find a way to remove this. It's a hack for FakeTransport
+ pub is_fake: bool,
+}
+
+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 {
+ let adapter_path = paths::debug_adapters_dir().join(&adapter_name);
+ 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,
+ );
+
+ let mut response = delegate
+ .http_client()
+ .get(&github_version.url, Default::default(), true)
+ .await
+ .context("Error downloading release")?;
+ if !response.status().is_success() {
+ Err(anyhow!(
+ "download failed with status {}",
+ response.status().to_string()
+ ))?;
+ }
+
+ 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?;
+
+ // we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
+ util::command::new_smol_command("unzip")
+ .arg(&zip_path)
+ .arg("-d")
+ .arg(&version_path)
+ .output()
+ .await?;
+
+ 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)
+}
+
+pub async fn fetch_latest_adapter_version_from_github(
+ github_repo: GithubRepo,
+ delegate: &dyn DapDelegate,
+) -> Result {
+ let release = latest_github_release(
+ &format!("{}/{}", github_repo.repo_owner, github_repo.repo_name),
+ false,
+ false,
+ delegate.http_client(),
+ )
+ .await?;
+
+ Ok(AdapterVersion {
+ tag_name: release.tag_name,
+ url: release.zipball_url,
+ })
+}
+
+#[async_trait(?Send)]
+pub trait DebugAdapter: 'static + Send + Sync {
+ fn name(&self) -> DebugAdapterName;
+
+ async fn get_binary(
+ &self,
+ delegate: &dyn DapDelegate,
+ config: &DebugAdapterConfig,
+ user_installed_path: Option,
+ cx: &mut AsyncApp,
+ ) -> Result {
+ if delegate
+ .updated_adapters()
+ .lock()
+ .await
+ .contains(&self.name())
+ {
+ log::info!("Using cached debug adapter binary {}", self.name());
+
+ if let Some(binary) = self
+ .get_installed_binary(delegate, &config, user_installed_path.clone(), cx)
+ .await
+ .log_err()
+ {
+ return Ok(binary);
+ }
+
+ log::info!(
+ "Cached binary {} is corrupt falling back to install",
+ self.name()
+ );
+ }
+
+ log::info!("Getting latest version of debug adapter {}", self.name());
+ delegate.update_status(self.name(), DapStatus::CheckingForUpdate);
+ if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
+ log::info!(
+ "Installiing latest version of debug adapter {}",
+ self.name()
+ );
+ delegate.update_status(self.name(), DapStatus::Downloading);
+ self.install_binary(version, delegate).await?;
+
+ delegate
+ .updated_adapters()
+ .lock_arc()
+ .await
+ .insert(self.name());
+ }
+
+ self.get_installed_binary(delegate, &config, user_installed_path, cx)
+ .await
+ }
+
+ async fn fetch_latest_adapter_version(
+ &self,
+ delegate: &dyn DapDelegate,
+ ) -> Result;
+
+ /// Installs the binary for the debug adapter.
+ /// This method is called when the adapter binary is not found or needs to be updated.
+ /// It should download and install the necessary files for the debug adapter to function.
+ async fn install_binary(
+ &self,
+ version: AdapterVersion,
+ delegate: &dyn DapDelegate,
+ ) -> Result<()>;
+
+ async fn get_installed_binary(
+ &self,
+ delegate: &dyn DapDelegate,
+ config: &DebugAdapterConfig,
+ user_installed_path: Option,
+ cx: &mut AsyncApp,
+ ) -> Result;
+
+ /// Should return base configuration to make the debug adapter work
+ fn request_args(&self, config: &DebugAdapterConfig) -> Value;
+
+ /// Filters out the processes that the adapter can attach to for debugging
+ fn attach_processes<'a>(
+ &self,
+ _: &'a HashMap,
+ ) -> Option> {
+ None
+ }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+pub struct FakeAdapter {}
+
+#[cfg(any(test, feature = "test-support"))]
+impl FakeAdapter {
+ 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())
+ }
+
+ async fn get_binary(
+ &self,
+ _: &dyn DapDelegate,
+ _: &DebugAdapterConfig,
+ _: Option,
+ _: &mut AsyncApp,
+ ) -> Result {
+ Ok(DebugAdapterBinary {
+ command: "command".into(),
+ arguments: None,
+ connection: Some(TcpArguments {
+ host: Ipv4Addr::LOCALHOST,
+ port: None,
+ timeout: None,
+ }),
+ is_fake: true,
+ envs: None,
+ cwd: None,
+ })
+ }
+
+ async fn fetch_latest_adapter_version(
+ &self,
+ _delegate: &dyn DapDelegate,
+ ) -> Result {
+ unimplemented!("fetch latest adapter version");
+ }
+
+ async fn install_binary(
+ &self,
+ _version: AdapterVersion,
+ _delegate: &dyn DapDelegate,
+ ) -> Result<()> {
+ unimplemented!("install binary");
+ }
+
+ async fn get_installed_binary(
+ &self,
+ _: &dyn DapDelegate,
+ _: &DebugAdapterConfig,
+ _: Option,
+ _: &mut AsyncApp,
+ ) -> Result {
+ unimplemented!("get installed binary");
+ }
+
+ fn request_args(&self, config: &DebugAdapterConfig) -> Value {
+ use serde_json::json;
+ use task::DebugRequestType;
+
+ json!({
+ "request": match config.request {
+ DebugRequestType::Launch => "launch",
+ DebugRequestType::Attach(_) => "attach",
+ },
+ "process_id": if let DebugRequestType::Attach(attach_config) = &config.request {
+ attach_config.process_id
+ } else {
+ None
+ },
+ })
+ }
+
+ fn attach_processes<'a>(
+ &self,
+ processes: &'a HashMap,
+ ) -> Option> {
+ Some(
+ processes
+ .iter()
+ .filter(|(pid, _)| pid.as_u32() == std::process::id())
+ .collect::>(),
+ )
+ }
+}
diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs
new file mode 100644
index 00000000000000..5aad4935e90a0c
--- /dev/null
+++ b/crates/dap/src/client.rs
@@ -0,0 +1,474 @@
+use crate::{
+ adapters::DebugAdapterBinary,
+ transport::{IoKind, LogKind, TransportDelegate},
+};
+use anyhow::{anyhow, Result};
+use dap_types::{
+ messages::{Message, Response},
+ requests::Request,
+};
+use futures::{channel::oneshot, select, FutureExt as _};
+use gpui::{App, AsyncApp, BackgroundExecutor};
+use smol::channel::{Receiver, Sender};
+use std::{
+ hash::Hash,
+ sync::atomic::{AtomicU64, Ordering},
+ time::Duration,
+};
+
+#[cfg(any(test, feature = "test-support"))]
+const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(2);
+
+#[cfg(not(any(test, feature = "test-support")))]
+const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(12);
+
+#[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,
+ executor: BackgroundExecutor,
+ transport_delegate: TransportDelegate,
+}
+
+pub type DapMessageHandler = Box;
+
+impl DebugAdapterClient {
+ pub async fn start(
+ id: SessionId,
+ binary: DebugAdapterBinary,
+ message_handler: DapMessageHandler,
+ cx: AsyncApp,
+ ) -> Result {
+ let ((server_rx, server_tx), transport_delegate) =
+ TransportDelegate::start(&binary, cx.clone()).await?;
+ let this = Self {
+ id,
+ binary,
+ transport_delegate,
+ sequence_count: AtomicU64::new(1),
+ executor: cx.background_executor().clone(),
+ };
+ log::info!("Successfully connected to debug adapter");
+
+ let client_id = this.id;
+
+ // start handling events/reverse requests
+ cx.update(|cx| {
+ cx.spawn({
+ let server_tx = server_tx.clone();
+ |mut cx| async move {
+ Self::handle_receive_messages(
+ client_id,
+ server_rx,
+ server_tx,
+ message_handler,
+ &mut cx,
+ )
+ .await
+ }
+ })
+ .detach_and_log_err(cx);
+
+ this
+ })
+ }
+
+ async fn handle_receive_messages(
+ client_id: SessionId,
+ server_rx: Receiver,
+ client_tx: Sender,
+ mut message_handler: DapMessageHandler,
+ cx: &mut AsyncApp,
+ ) -> Result<()> {
+ let result = loop {
+ let message = match server_rx.recv().await {
+ Ok(message) => message,
+ Err(e) => break Err(e.into()),
+ };
+
+ if let Err(e) = match message {
+ Message::Event(ev) => {
+ log::debug!("Client {} received event `{}`", client_id.0, &ev);
+
+ cx.update(|cx| message_handler(Message::Event(ev), cx))
+ }
+ Message::Request(req) => cx.update(|cx| message_handler(Message::Request(req), cx)),
+ Message::Response(response) => {
+ log::debug!("Received response after request timeout: {:#?}", response);
+
+ Ok(())
+ }
+ } {
+ break Err(e);
+ }
+
+ smol::future::yield_now().await;
+ };
+
+ drop(client_tx);
+
+ log::debug!("Handle receive messages dropped");
+
+ result
+ }
+
+ /// 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(&self, arguments: R::Arguments) -> Result {
+ let serialized_arguments = serde_json::to_value(arguments)?;
+
+ let (callback_tx, callback_rx) = oneshot::channel::>();
+
+ 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
+ .add_pending_request(sequence_id, callback_tx)
+ .await;
+
+ log::debug!(
+ "Client {} send `{}` request with sequence_id: {}",
+ self.id.0,
+ R::COMMAND.to_string(),
+ sequence_id
+ );
+
+ self.send_message(Message::Request(request)).await?;
+
+ let mut timeout = self.executor.timer(DAP_REQUEST_TIMEOUT).fuse();
+ let command = R::COMMAND.to_string();
+
+ select! {
+ response = callback_rx.fuse() => {
+ log::debug!(
+ "Client {} received response for: `{}` sequence_id: {}",
+ self.id.0,
+ command,
+ sequence_id
+ );
+
+ let response = response??;
+ match response.success {
+ true => Ok(serde_json::from_value(response.body.unwrap_or_default())?),
+ false => Err(anyhow!("Request failed")),
+ }
+ }
+
+ _ = timeout => {
+ self.transport_delegate.cancel_pending_request(&sequence_id).await;
+ log::error!("Cancelled DAP request for {command:?} id {sequence_id} which took over {DAP_REQUEST_TIMEOUT:?}");
+ anyhow::bail!("DAP request timeout");
+ }
+ }
+ }
+
+ 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 async fn shutdown(&self) -> Result<()> {
+ self.transport_delegate.shutdown().await
+ }
+
+ pub fn has_adapter_logs(&self) -> bool {
+ self.transport_delegate.has_adapter_logs()
+ }
+
+ pub fn add_log_handler(&self, f: F, kind: LogKind)
+ where
+ F: 'static + Send + FnMut(IoKind, &str),
+ {
+ self.transport_delegate.add_log_handler(f, kind);
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub async fn on_request(&self, handler: F)
+ where
+ F: 'static
+ + Send
+ + FnMut(u64, R::Arguments) -> Result,
+ {
+ let transport = self.transport_delegate.transport();
+
+ transport.on_request::(handler).await;
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub async fn fake_reverse_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(&self, handler: F)
+ where
+ F: 'static + Send + Fn(Response),
+ {
+ let transport = self.transport_delegate.transport();
+
+ transport.on_response::(handler).await;
+ }
+
+ #[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, debugger_settings::DebuggerSettings};
+ use dap_types::{
+ messages::Events,
+ requests::{Initialize, Request, RunInTerminal},
+ Capabilities, InitializeRequestArguments, InitializeRequestArgumentsPathFormat,
+ RunInTerminalRequestArguments,
+ };
+ use gpui::TestAppContext;
+ use serde_json::json;
+ use settings::{Settings, SettingsStore};
+ use std::sync::atomic::{AtomicBool, Ordering};
+
+ pub fn init_test(cx: &mut gpui::TestAppContext) {
+ if std::env::var("RUST_LOG").is_ok() {
+ env_logger::try_init().ok();
+ }
+
+ cx.update(|cx| {
+ let settings = SettingsStore::test(cx);
+ cx.set_global(settings);
+ DebuggerSettings::register(cx);
+ });
+ }
+
+ #[gpui::test]
+ pub async fn test_initialize_client(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let mut client = DebugAdapterClient::start(
+ crate::client::SessionId(1),
+ DebugAdapterBinary {
+ command: "command".into(),
+ arguments: Default::default(),
+ envs: Default::default(),
+ connection: Some(crate::adapters::TcpArguments {
+ host: Ipv4Addr::LOCALHOST,
+ port: None,
+ timeout: None,
+ }),
+ is_fake: true,
+ cwd: None,
+ },
+ |_, _| panic!("Did not expect to hit this code path"),
+ cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ client
+ .on_request::(move |_, _| {
+ Ok(dap_types::Capabilities {
+ supports_configuration_done_request: Some(true),
+ ..Default::default()
+ })
+ })
+ .await;
+
+ cx.run_until_parked();
+
+ let response = client
+ .request::(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),
+ })
+ .await
+ .unwrap();
+
+ cx.run_until_parked();
+
+ assert_eq!(
+ dap_types::Capabilities {
+ supports_configuration_done_request: Some(true),
+ ..Default::default()
+ },
+ response
+ );
+
+ client.shutdown().await.unwrap();
+ }
+
+ #[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: "command".into(),
+ arguments: Default::default(),
+ envs: Default::default(),
+ connection: Some(TcpArguments {
+ host: Ipv4Addr::LOCALHOST,
+ port: None,
+ timeout: None,
+ }),
+ is_fake: true,
+ cwd: None,
+ },
+ {
+ 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
+ );
+ }
+ },
+ 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"
+ );
+
+ client.shutdown().await.unwrap();
+ }
+
+ #[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: "command".into(),
+ arguments: Default::default(),
+ envs: Default::default(),
+ connection: Some(TcpArguments {
+ host: Ipv4Addr::LOCALHOST,
+ port: None,
+ timeout: None,
+ }),
+ is_fake: true,
+ cwd: None,
+ },
+ {
+ 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
+ );
+ }
+ },
+ cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ cx.run_until_parked();
+
+ client
+ .fake_reverse_request::(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"
+ );
+
+ client.shutdown().await.unwrap();
+ }
+}
diff --git a/crates/dap/src/debugger_settings.rs b/crates/dap/src/debugger_settings.rs
new file mode 100644
index 00000000000000..5b83927dfe8ebe
--- /dev/null
+++ b/crates/dap/src/debugger_settings.rs
@@ -0,0 +1,59 @@
+use dap_types::SteppingGranularity;
+use gpui::{App, Global};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::{Settings, SettingsSources};
+
+#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy)]
+#[serde(default)]
+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,
+}
+
+impl Default for DebuggerSettings {
+ fn default() -> Self {
+ Self {
+ button: true,
+ save_breakpoints: true,
+ stepping_granularity: SteppingGranularity::Line,
+ timeout: 2000,
+ log_dap_communications: true,
+ format_dap_log_messages: true,
+ }
+ }
+}
+
+impl Settings for DebuggerSettings {
+ const KEY: Option<&'static str> = Some("debugger");
+
+ type FileContent = Self;
+
+ fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result {
+ sources.json_merge()
+ }
+}
+
+impl Global for DebuggerSettings {}
diff --git a/crates/dap/src/lib.rs b/crates/dap/src/lib.rs
new file mode 100644
index 00000000000000..d83c0931cb0efc
--- /dev/null
+++ b/crates/dap/src/lib.rs
@@ -0,0 +1,24 @@
+pub mod adapters;
+pub mod client;
+pub mod debugger_settings;
+pub mod proto_conversions;
+pub mod transport;
+
+pub use dap_types::*;
+pub use task::{DebugAdapterConfig, DebugAdapterKind, DebugRequestType};
+
+#[cfg(any(test, feature = "test-support"))]
+pub use adapters::FakeAdapter;
+
+#[cfg(any(test, feature = "test-support"))]
+pub fn test_config() -> DebugAdapterConfig {
+ DebugAdapterConfig {
+ label: "test config".into(),
+ kind: DebugAdapterKind::Fake,
+ request: DebugRequestType::Launch,
+ program: None,
+ supports_attach: false,
+ cwd: None,
+ initialize_args: None,
+ }
+}
diff --git a/crates/dap/src/proto_conversions.rs b/crates/dap/src/proto_conversions.rs
new file mode 100644
index 00000000000000..41af22cc7dd672
--- /dev/null
+++ b/crates/dap/src/proto_conversions.rs
@@ -0,0 +1,638 @@
+use anyhow::{anyhow, Result};
+use client::proto::{
+ self, DapChecksum, DapChecksumAlgorithm, DapEvaluateContext, DapModule, DapScope,
+ DapScopePresentationHint, DapSource, DapSourcePresentationHint, DapStackFrame, DapVariable,
+ SetDebugClientCapabilities,
+};
+use dap_types::{
+ Capabilities, 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 ProtoConversion for Vec
+where
+ T: ProtoConversion