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 91538b01848f2e..5b458740b0cc66 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13,7 +13,6 @@ dependencies = [
"futures 0.3.31",
"gpui",
"language",
- "lsp",
"project",
"smallvec",
"ui",
@@ -2644,6 +2643,12 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "circular-buffer"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dacb91f972298e70fc507a2ffcaf1545807f1a36da586fb846646030adc542f"
+
[[package]]
name = "clang-sys"
version = "1.8.1"
@@ -2893,9 +2898,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.7",
@@ -3839,6 +3847,66 @@ dependencies = [
"syn 2.0.100",
]
+[[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.7",
+ "fs",
+ "futures 0.3.31",
+ "gpui",
+ "http_client",
+ "language",
+ "log",
+ "node_runtime",
+ "parking_lot",
+ "paths",
+ "schemars",
+ "serde",
+ "serde_json",
+ "settings",
+ "smallvec",
+ "smol",
+ "task",
+ "util",
+]
+
+[[package]]
+name = "dap-types"
+version = "0.0.1"
+source = "git+https://github.com/zed-industries/dap-types?rev=bfd4af0#bfd4af084bbaa5f344e6925370d7642e41d0b5b8"
+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"
@@ -3912,6 +3980,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.7",
+ "feature_flags",
+ "futures 0.3.31",
+ "fuzzy",
+ "gpui",
+ "language",
+ "log",
+ "menu",
+ "picker",
+ "pretty_assertions",
+ "project",
+ "rpc",
+ "serde",
+ "serde_json",
+ "settings",
+ "sysinfo",
+ "task",
+ "terminal_view",
+ "theme",
+ "ui",
+ "unindent",
+ "util",
+ "workspace",
+]
+
[[package]]
name = "deepseek"
version = "0.1.0"
@@ -4212,6 +4332,7 @@ dependencies = [
"db",
"emojis",
"env_logger 0.11.7",
+ "feature_flags",
"file_icons",
"fs",
"futures 0.3.31",
@@ -4228,6 +4349,7 @@ dependencies = [
"log",
"lsp",
"markdown",
+ "menu",
"multi_buffer",
"ordered-float 2.10.1",
"parking_lot",
@@ -10422,9 +10544,12 @@ dependencies = [
"askpass",
"async-trait",
"buffer_diff",
+ "circular-buffer",
"client",
"clock",
"collections",
+ "dap",
+ "dap_adapters",
"env_logger 0.11.7",
"extension",
"fancy-regex 0.14.0",
@@ -10437,6 +10562,7 @@ dependencies = [
"gpui",
"http_client",
"image",
+ "indexmap",
"itertools 0.14.0",
"language",
"log",
@@ -11100,6 +11226,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"auto_update",
+ "dap",
"editor",
"extension_host",
"file_finder",
@@ -13657,12 +13784,14 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
+ "dap-types",
"futures 0.3.31",
"gpui",
"hex",
"parking_lot",
"schemars",
"serde",
+ "serde_json",
"serde_json_lenient",
"sha2",
"shellexpand 2.1.2",
@@ -13675,7 +13804,9 @@ name = "tasks_ui"
version = "0.1.0"
dependencies = [
"anyhow",
+ "debugger_ui",
"editor",
+ "feature_flags",
"file_icons",
"fuzzy",
"gpui",
@@ -17218,6 +17349,8 @@ dependencies = [
"component_preview",
"copilot",
"db",
+ "debugger_tools",
+ "debugger_ui",
"diagnostics",
"editor",
"env_logger 0.11.7",
diff --git a/Cargo.toml b/Cargo.toml
index f9a6835c5acf7b..22e26ff796a93a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -37,6 +37,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",
@@ -236,7 +240,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" }
@@ -402,6 +410,7 @@ bytes = "1.0"
cargo_metadata = "0.19"
cargo_toml = "0.21"
chrono = { version = "0.4", features = ["serde"] }
+circular-buffer = "1.0"
clap = { version = "4.4", features = ["derive"] }
cocoa = "0.26"
cocoa-foundation = "0.2.0"
@@ -410,6 +419,7 @@ core-foundation = "0.9.3"
core-foundation-sys = "0.8.6"
ctor = "0.4.0"
dashmap = "6.0"
+dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "bfd4af0" }
derive_more = "0.99.17"
dirs = "4.0"
ec4rs = "1.1"
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-linux.json b/assets/keymaps/default-linux.json
index eb66cf6ed4b7b8..674c196f0ead10 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -30,6 +30,13 @@
"ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],
"ctrl-,": "zed::OpenSettings",
"ctrl-q": "zed::Quit",
+ "f4": "debugger::Start",
+ "f5": "debugger::Continue",
+ "shift-f5": "debugger::Stop",
+ "f6": "debugger::Pause",
+ "f7": "debugger::StepOver",
+ "cmd-f11": "debugger::StepInto",
+ "shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions",
"ctrl-shift-i": "edit_prediction::ToggleMenu"
@@ -124,7 +131,9 @@
"alt-g b": "editor::ToggleGitBlame",
"menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu",
- "ctrl-shift-e": "editor::ToggleEditPrediction"
+ "ctrl-shift-e": "editor::ToggleEditPrediction",
+ "f9": "editor::ToggleBreakpoint",
+ "shift-f9": "editor::EditLogBreakpoint"
}
},
{
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index 4252351a79e493..4c626ead6d1990 100644
--- a/assets/keymaps/default-macos.json
+++ b/assets/keymaps/default-macos.json
@@ -14,6 +14,13 @@
{
"use_key_equivalents": true,
"bindings": {
+ "f4": "debugger::Start",
+ "f5": "debugger::Continue",
+ "shift-f5": "debugger::Stop",
+ "f6": "debugger::Pause",
+ "f7": "debugger::StepOver",
+ "f11": "debugger::StepInto",
+ "shift-f11": "debugger::StepOut",
"home": "menu::SelectFirst",
"shift-pageup": "menu::SelectFirst",
"pageup": "menu::SelectFirst",
@@ -148,6 +155,8 @@
"cmd-\"": "editor::ExpandAllDiffHunks",
"cmd-alt-g b": "editor::ToggleGitBlame",
"cmd-i": "editor::ShowSignatureHelp",
+ "f9": "editor::ToggleBreakpoint",
+ "shift-f9": "editor::EditLogBreakpoint",
"ctrl-f12": "editor::GoToDeclaration",
"alt-ctrl-f12": "editor::GoToDeclarationSplit",
"ctrl-cmd-e": "editor::ToggleEditPrediction"
@@ -756,6 +765,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/keymaps/linux/jetbrains.json b/assets/keymaps/linux/jetbrains.json
index 89d01dfb5db9fb..43937701cf13a6 100644
--- a/assets/keymaps/linux/jetbrains.json
+++ b/assets/keymaps/linux/jetbrains.json
@@ -3,7 +3,14 @@
"bindings": {
"ctrl-alt-s": "zed::OpenSettings",
"ctrl-{": "pane::ActivatePreviousItem",
- "ctrl-}": "pane::ActivateNextItem"
+ "ctrl-}": "pane::ActivateNextItem",
+ "ctrl-f2": "debugger::Stop",
+ "f6": "debugger::Pause",
+ "f7": "debugger::StepInto",
+ "f8": "debugger::StepOver",
+ "shift-f8": "debugger::StepOut",
+ "f9": "debugger::Continue",
+ "alt-shift-f9": "debugger::Start"
}
},
{
@@ -49,7 +56,9 @@
"ctrl-home": "editor::MoveToBeginning",
"ctrl-end": "editor::MoveToEnd",
"ctrl-shift-home": "editor::SelectToBeginning",
- "ctrl-shift-end": "editor::SelectToEnd"
+ "ctrl-shift-end": "editor::SelectToEnd",
+ "ctrl-f8": "editor::ToggleBreakpoint",
+ "ctrl-shift-f8": "editor::EditLogBreakpoint"
}
},
{
diff --git a/assets/keymaps/macos/jetbrains.json b/assets/keymaps/macos/jetbrains.json
index 4adacb945c4ef6..355e86090813b7 100644
--- a/assets/keymaps/macos/jetbrains.json
+++ b/assets/keymaps/macos/jetbrains.json
@@ -2,7 +2,14 @@
{
"bindings": {
"cmd-{": "pane::ActivatePreviousItem",
- "cmd-}": "pane::ActivateNextItem"
+ "cmd-}": "pane::ActivateNextItem",
+ "ctrl-f2": "debugger::Stop",
+ "f6": "debugger::Pause",
+ "f7": "debugger::StepInto",
+ "f8": "debugger::StepOver",
+ "shift-f8": "debugger::StepOut",
+ "f9": "debugger::Continue",
+ "alt-shift-f9": "debugger::Start"
}
},
{
@@ -46,7 +53,9 @@
"cmd-home": "editor::MoveToBeginning",
"cmd-end": "editor::MoveToEnd",
"cmd-shift-home": "editor::SelectToBeginning",
- "cmd-shift-end": "editor::SelectToEnd"
+ "cmd-shift-end": "editor::SelectToEnd",
+ "ctrl-f8": "editor::ToggleBreakpoint",
+ "ctrl-shift-f8": "editor::EditLogBreakpoint"
}
},
{
diff --git a/assets/settings/default.json b/assets/settings/default.json
index 800c2761230bae..70d016fb7960d9 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -324,6 +324,8 @@
"code_actions": true,
// Whether to show runnables buttons in the gutter.
"runnables": true,
+ // Whether to show breakpoints in the gutter.
+ "breakpoints": true,
// Whether to show fold buttons in the gutter.
"folds": true
},
@@ -1453,6 +1455,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 105bcff8db096b..5fd1f276316332 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, LspStoreEvent, Project,
ProjectEnvironmentEvent, WorktreeId,
@@ -23,21 +22,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> {
@@ -68,7 +67,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();
})?;
}
@@ -106,18 +118,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,
@@ -147,9 +159,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
@@ -278,12 +290,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 => {}
}
}
@@ -296,7 +306,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() {
@@ -324,7 +334,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() {
@@ -354,7 +364,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/assistant_context_editor/src/context_editor.rs b/crates/assistant_context_editor/src/context_editor.rs
index 72b4467015f1aa..4f74da00a8bfbb 100644
--- a/crates/assistant_context_editor/src/context_editor.rs
+++ b/crates/assistant_context_editor/src/context_editor.rs
@@ -229,6 +229,7 @@ impl ContextEditor {
editor.set_show_git_diff_gutter(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_runnables(false, cx);
+ editor.set_show_breakpoints(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_completion_provider(Some(Box::new(completion_provider)));
diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml
index 520b7b19ff8004..c3299a8ffd677a 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/queries/projects.rs b/crates/collab/src/db/queries/projects.rs
index 1cff5b53b09c12..2970c9be0ffb6d 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::HashSet;
use util::ResultExt;
use super::*;
@@ -1106,39 +1106,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/rpc.rs b/crates/collab/src/rpc.rs
index 1d6c4bb01f7a12..a22f5e5646a641 100644
--- a/crates/collab/src/rpc.rs
+++ b/crates/collab/src/rpc.rs
@@ -404,6 +404,8 @@ impl Server {
.add_request_handler(forward_read_only_project_request::)
.add_request_handler(forward_read_only_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_request_handler(forward_mutating_project_request::)
.add_request_handler(forward_mutating_project_request::)
@@ -2064,7 +2066,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,
diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs
index d8acfb5819f6b8..5ec4937168283e 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 git_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..a8e9b745ac0171
--- /dev/null
+++ b/crates/collab/src/tests/debug_panel_tests.rs
@@ -0,0 +1,2454 @@
+use call::ActiveCall;
+use dap::requests::{Initialize, Launch, StackTrace};
+use dap::DebugRequestType;
+use dap::{requests::SetBreakpoints, SourceBreakpoint};
+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 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_session(
+ 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_session(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(DebugRequestType::Launch, None, None), cx)
+ });
+
+ let session = task.await.unwrap();
+ let client = session.read_with(host_cx, |project, _| project.adapter_client().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
+ .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_session = debug_panel
+ .update(cx, |this, cx| this.active_session(cx))
+ .unwrap();
+
+ assert_eq!(
+ 1,
+ debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
+ );
+ // assert_eq!(client.id(), active_session.read(cx).());
+ // assert_eq!(1, active_session.read(cx).thread_id().0);
+ // todo(debugger) check selected thread id
+ });
+
+ 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).session_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(DebugRequestType::Launch, None, None), cx)
+ });
+
+ let session = task.await.unwrap();
+ let client = session.read_with(host_cx, |project, _| project.adapter_client().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
+ .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_session = debug_panel
+ .update(cx, |this, cx| this.active_session(cx))
+ .unwrap();
+
+ assert_eq!(
+ 1,
+ debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
+ );
+ // assert_eq!(cl, active_session.read(cx).client_id());
+ // assert_eq!(1, active_session.read(cx).thread_id().0);
+ // todo(debugger)
+ });
+
+ 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).session_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_session(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,
+) {
+ unimplemented!("Collab is still being refactored");
+ // 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(None), cx)
+ // });
+
+ // let session = task.await.unwrap();
+ // let client = session.read_with(host_cx, |project, _| project.adapter_client().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
+ // .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_session = debug_panel
+ // .update(cx, |this, cx| this.active_session(cx))
+ // .unwrap();
+
+ // assert_eq!(
+ // 1,
+ // debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
+ // );
+ // // assert_eq!(client.id(), active_session.read(cx).client_id());
+ // // assert_eq!(1, active_session.read(cx).thread_id().0);
+ // // todo(debugger)
+ // active_session
+ // });
+
+ // let local_debug_item = host_workspace.update(host_cx, |workspace, cx| {
+ // let debug_panel = workspace.panel::(cx).unwrap();
+ // let active_session = debug_panel
+ // .update(cx, |this, cx| this.active_session(cx))
+ // .unwrap();
+
+ // assert_eq!(
+ // 1,
+ // debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
+ // );
+ // // assert_eq!(client.id(), active_session.read(cx).client_id());
+ // // assert_eq!(1, active_session.read(cx).thread_id().0);
+ // // todo(debugger)
+ // active_session
+ // });
+
+ // 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_session(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) {
+ unimplemented!("Collab is still being refactored");
+ // 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(None), cx)
+ // });
+
+ // let session = task.await.unwrap();
+ // let client = session.read(cx).adapter_client().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
+ // .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_session = debug_panel
+ // .update(cx, |this, cx| this.active_session(cx))
+ // .unwrap();
+
+ // active_session.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).session_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(DebugRequestType::Launch, None, None), cx)
+ });
+
+ let session = task.await.unwrap();
+ let client = session.read_with(host_cx, |project, _| project.adapter_client().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());
+ // todo(debugger): Implement source_modified handling
+
+ called_set_breakpoints.store(true, Ordering::SeqCst);
+
+ Ok(dap::SetBreakpointsResponse {
+ breakpoints: Vec::default(),
+ })
+ }
+ })
+ .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());
+ // todo(debugger) Implement source modified support
+
+ 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());
+ // todo(debugger) Implement source modified support
+
+ 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).session_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,
+) {
+ unimplemented!("Collab is still being refactored");
+ // 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(None), cx)
+ // });
+
+ // let session = task.await.unwrap();
+ // let client = session.read_with(host_cx, |project, _| project.adapter_client().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_session(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_session(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_session(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"
+ // );
+ // })
+ // });
+
+ // 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);
+// });
+
+// 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,
+) {
+ unimplemented!("Collab is still being refactored");
+ // 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(None), cx)
+ // });
+
+ // let session = task.await.unwrap();
+ // let client = session.read_with(host_cx, |project, _| project.adapter_client().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
+ // .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_session = debug_panel
+ // .update(cx, |this, cx| this.active_session(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_session.read(cx).are_breakpoints_ignored(cx);
+
+ // assert_eq!(session_id, active_session.read(cx).session().read(cx).id());
+ // assert_eq!(false, breakpoints_ignored);
+ // assert_eq!(client.id(), active_session.read(cx).client_id());
+ // assert_eq!(1, active_session.read(cx).thread_id().0);
+ // active_session
+ // });
+
+ // 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_session = debug_panel
+ // .update(cx, |this, cx| this.active_session(cx))
+ // .unwrap();
+
+ // assert_eq!(
+ // 1,
+ // debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
+ // );
+
+ // assert_eq!(false, active_session.read(cx).are_breakpoints_ignored(cx));
+ // assert_eq!(client.id(), active_session.read(cx).client_id());
+ // assert_eq!(1, active_session.read(cx).thread_id().0);
+
+ // active_session
+ // });
+
+ // 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_session = debug_panel
+ // .update(cx, |this, cx| this.active_session(cx))
+ // .unwrap();
+
+ // let breakpoints_ignored = active_session.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_session.read(cx).client_id());
+ // assert_eq!(1, active_session.read(cx).thread_id().0);
+ // active_session
+ // });
+
+ // 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) {
+ unimplemented!("Collab is still being refactored");
+ // 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(None), cx)
+ // });
+
+ // let session = task.await.unwrap();
+ // let client = session.read_with(host_cx, |project, _| project.adapter_client().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
+ // .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,
+ // location_reference: 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),
+ // location_reference: None,
+ // }))
+ // .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,
+ // location_reference: 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,
+ // location_reference: 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),
+ // location_reference: None,
+ // }))
+ // .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,
+ // location_reference: 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,
+ // location_reference: 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),
+ // location_reference: None,
+ // }))
+ // .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),
+ // location_reference: None,
+ // }))
+ // .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,
+ // location_reference: 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,
+ // location_reference: 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),
+ // location_reference: None,
+ // }))
+ // .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,
+ // location_reference: 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),
+ // location_reference: None,
+ // }))
+ // .await;
+
+ // host_cx.run_until_parked();
+ // remote_cx.run_until_parked();
+
+ // active_session(remote_workspace, remote_cx).update(remote_cx, |session_item, cx| {
+ // session_item
+ // .mode()
+ // .as_running()
+ // .unwrap()
+ // .read(cx)
+ // .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)
+ .all_breakpoints(cx)
+ .clone()
+ });
+ let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
+ editor
+ .breakpoint_store()
+ .clone()
+ .unwrap()
+ .read(cx)
+ .all_breakpoints(cx)
+ .clone()
+ });
+
+ assert_eq!(1, breakpoints_a.len());
+ assert_eq!(1, breakpoints_a.get(&abs_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)
+ .all_breakpoints(cx)
+ .clone()
+ });
+ let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
+ editor
+ .breakpoint_store()
+ .clone()
+ .unwrap()
+ .read(cx)
+ .all_breakpoints(cx)
+ .clone()
+ });
+
+ assert_eq!(1, breakpoints_a.len());
+ assert_eq!(breakpoints_a, breakpoints_b);
+ assert_eq!(2, breakpoints_a.get(&abs_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)
+ .all_breakpoints(cx)
+ .clone()
+ });
+ let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
+ editor
+ .breakpoint_store()
+ .clone()
+ .unwrap()
+ .read(cx)
+ .all_breakpoints(cx)
+ .clone()
+ });
+
+ assert_eq!(1, breakpoints_a.len());
+ assert_eq!(breakpoints_a, breakpoints_b);
+ assert_eq!(1, breakpoints_a.get(&abs_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)
+ .all_breakpoints(cx)
+ .clone()
+ });
+ let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
+ editor
+ .breakpoint_store()
+ .clone()
+ .unwrap()
+ .read(cx)
+ .all_breakpoints(cx)
+ .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..7c7bab6110a9fa
--- /dev/null
+++ b/crates/dap/Cargo.toml
@@ -0,0 +1,53 @@
+[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.workspace = true
+fs.workspace = true
+futures.workspace = true
+gpui.workspace = true
+http_client.workspace = true
+language.workspace = true
+log.workspace = true
+node_runtime.workspace = true
+parking_lot.workspace = true
+paths.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+settings.workspace = true
+smallvec.workspace = true
+smol.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..a1053b2ff8cec2
--- /dev/null
+++ b/crates/dap/src/adapters.rs
@@ -0,0 +1,370 @@
+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 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: u16,
+ pub timeout: Option,
+}
+#[derive(Debug, Clone)]
+pub struct DebugAdapterBinary {
+ pub command: String,
+ pub arguments: Option>,
+ pub envs: Option>,
+ pub cwd: Option,
+ pub connection: Option,
+}
+
+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;
+}
+
+#[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: None,
+ 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
+ },
+ })
+ }
+}
diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs
new file mode 100644
index 00000000000000..a4dc394d0b27ea
--- /dev/null
+++ b/crates/dap/src/client.rs
@@ -0,0 +1,490 @@
+use crate::{
+ adapters::{DebugAdapterBinary, DebugAdapterName},
+ 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::{AppContext, 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,
+ name: DebugAdapterName,
+ sequence_count: AtomicU64,
+ binary: DebugAdapterBinary,
+ executor: BackgroundExecutor,
+ transport_delegate: TransportDelegate,
+}
+
+pub type DapMessageHandler = Box;
+
+impl DebugAdapterClient {
+ pub async fn start(
+ id: SessionId,
+ name: DebugAdapterName,
+ 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,
+ name,
+ 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.background_spawn(Self::handle_receive_messages(
+ client_id,
+ server_rx,
+ server_tx.clone(),
+ message_handler,
+ ))
+ .detach();
+
+ Ok(this)
+ }
+
+ pub async fn reconnect(
+ &self,
+ session_id: SessionId,
+ binary: DebugAdapterBinary,
+ message_handler: DapMessageHandler,
+ cx: AsyncApp,
+ ) -> Result {
+ let binary = match self.transport_delegate.transport() {
+ crate::transport::Transport::Tcp(tcp_transport) => DebugAdapterBinary {
+ command: binary.command,
+ arguments: binary.arguments,
+ envs: binary.envs,
+ cwd: binary.cwd,
+ connection: Some(crate::adapters::TcpArguments {
+ host: tcp_transport.host,
+ port: tcp_transport.port,
+ timeout: Some(tcp_transport.timeout),
+ }),
+ },
+ _ => self.binary.clone(),
+ };
+
+ Self::start(session_id, self.name(), binary, message_handler, cx).await
+ }
+
+ async fn handle_receive_messages(
+ client_id: SessionId,
+ server_rx: Receiver,
+ client_tx: Sender,
+ mut message_handler: DapMessageHandler,
+ ) -> Result<()> {
+ let result = loop {
+ let message = match server_rx.recv().await {
+ Ok(message) => message,
+ Err(e) => break Err(e.into()),
+ };
+
+ match message {
+ Message::Event(ev) => {
+ log::debug!("Client {} received event `{}`", client_id.0, &ev);
+
+ message_handler(Message::Event(ev))
+ }
+ Message::Request(req) => {
+ log::debug!(
+ "Client {} received reverse request `{}`",
+ client_id.0,
+ &req.command
+ );
+
+ message_handler(Message::Request(req))
+ }
+ Message::Response(response) => {
+ log::debug!("Received response after request timeout: {:#?}", response);
+ }
+ }
+
+ 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: {}", response.message.unwrap_or_default())),
+ }
+ }
+
+ _ = 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 name(&self) -> DebugAdapterName {
+ self.name.clone()
+ }
+ 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().as_fake();
+ 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().as_fake();
+ 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},
+ Arc,
+ };
+
+ 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 client = DebugAdapterClient::start(
+ crate::client::SessionId(1),
+ DebugAdapterName("adapter".into()),
+ DebugAdapterBinary {
+ command: "command".into(),
+ arguments: Default::default(),
+ envs: Default::default(),
+ connection: None,
+ cwd: None,
+ },
+ Box::new(|_| 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),
+ supports_ansistyling: Some(false),
+ })
+ .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),
+ DebugAdapterName("adapter".into()),
+ DebugAdapterBinary {
+ command: "command".into(),
+ arguments: Default::default(),
+ envs: Default::default(),
+ connection: None,
+ cwd: None,
+ },
+ Box::new({
+ let called_event_handler = called_event_handler.clone();
+ move |event| {
+ called_event_handler.store(true, Ordering::SeqCst);
+
+ assert_eq!(
+ Message::Event(Box::new(Events::Initialized(
+ Some(Capabilities::default())
+ ))),
+ event
+ );
+ }
+ }),
+ 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),
+ DebugAdapterName(Arc::from("test-adapter")),
+ DebugAdapterBinary {
+ command: "command".into(),
+ arguments: Default::default(),
+ envs: Default::default(),
+ connection: None,
+ cwd: None,
+ },
+ Box::new({
+ let called_event_handler = called_event_handler.clone();
+ move |event| {
+ called_event_handler.store(true, Ordering::SeqCst);
+
+ assert_eq!(
+ Message::Request(dap_types::messages::Request {
+ seq: 1,
+ command: RunInTerminal::COMMAND.into(),
+ arguments: Some(json!({
+ "cwd": "/project/path/src",
+ "args": ["node", "test.js"],
+ }))
+ }),
+ event
+ );
+ }
+ }),
+ 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..f8e54aa51c5573
--- /dev/null
+++ b/crates/dap/src/lib.rs
@@ -0,0 +1,38 @@
+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};
+
+pub type ScopeId = u64;
+pub type VariableReference = u64;
+pub type StackFrameId = u64;
+
+#[cfg(any(test, feature = "test-support"))]
+pub use adapters::FakeAdapter;
+
+#[cfg(any(test, feature = "test-support"))]
+pub fn test_config(
+ request: DebugRequestType,
+ fail: Option,
+ caps: Option,
+) -> DebugAdapterConfig {
+ DebugAdapterConfig {
+ label: "test config".into(),
+ kind: DebugAdapterKind::Fake((
+ fail.unwrap_or_default(),
+ caps.unwrap_or(Capabilities {
+ supports_step_back: Some(false),
+ ..Default::default()
+ }),
+ )),
+ request,
+ 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..097593b11cc4d7
--- /dev/null
+++ b/crates/dap/src/proto_conversions.rs
@@ -0,0 +1,591 @@
+use anyhow::{anyhow, Result};
+use client::proto::{
+ self, DapChecksum, DapChecksumAlgorithm, DapEvaluateContext, DapModule, DapScope,
+ DapScopePresentationHint, DapSource, DapSourcePresentationHint, DapStackFrame, DapVariable,
+};
+use dap_types::{OutputEventCategory, OutputEventGroup, ScopePresentationHint, Source};
+
+pub trait ProtoConversion {
+ type ProtoType;
+ type Output;
+
+ fn to_proto(&self) -> Self::ProtoType;
+ fn from_proto(payload: Self::ProtoType) -> Self::Output;
+}
+
+impl ProtoConversion for Vec
+where
+ T: ProtoConversion