diff --git a/.gitignore b/.gitignore
index d19c5a102aac8..e9eb4c3ef124a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,6 +29,7 @@ DerivedData/
.vscode
.wrangler
.flatpak-builder
+.zed/debug.json
# Don't commit any secrets to the repo.
.env.secret.toml
diff --git a/Cargo.lock b/Cargo.lock
index db14b8c7ae431..4f2d0c5c9f269 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3340,6 +3340,56 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
+[[package]]
+name = "dap"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "dap-types",
+ "fs",
+ "futures 0.3.30",
+ "gpui",
+ "http_client",
+ "log",
+ "node_runtime",
+ "parking_lot",
+ "schemars",
+ "serde",
+ "serde_json",
+ "settings",
+ "smol",
+ "task",
+]
+
+[[package]]
+name = "dap-types"
+version = "0.0.1"
+source = "git+https://github.com/zed-industries/dap-types?rev=b95818130022bfc72bbcd639bdd0c0358c7549fc#b95818130022bfc72bbcd639bdd0c0358c7549fc"
+dependencies = [
+ "schemars",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "dap_adapters"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "dap",
+ "fs",
+ "futures 0.3.30",
+ "gpui",
+ "http_client",
+ "paths",
+ "serde",
+ "serde_json",
+ "smol",
+ "task",
+]
+
[[package]]
name = "dashmap"
version = "5.5.3"
@@ -3413,6 +3463,30 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "debugger_ui"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "dap",
+ "editor",
+ "futures 0.3.30",
+ "fuzzy",
+ "gpui",
+ "language",
+ "menu",
+ "parking_lot",
+ "project",
+ "serde",
+ "serde_json",
+ "settings",
+ "task",
+ "tasks_ui",
+ "theme",
+ "ui",
+ "workspace",
+]
+
[[package]]
name = "deflate64"
version = "0.1.9"
@@ -3689,6 +3763,7 @@ dependencies = [
"log",
"lsp",
"markdown",
+ "menu",
"multi_buffer",
"ordered-float 2.10.1",
"parking_lot",
@@ -8429,6 +8504,8 @@ dependencies = [
"client",
"clock",
"collections",
+ "dap",
+ "dap_adapters",
"dev_server_projects",
"env_logger",
"fs",
@@ -8966,6 +9043,7 @@ dependencies = [
"anyhow",
"auto_update",
"client",
+ "dap",
"dev_server_projects",
"editor",
"futures 0.3.30",
@@ -10716,6 +10794,7 @@ dependencies = [
"indoc",
"libsqlite3-sys",
"parking_lot",
+ "project",
"smol",
"sqlformat",
"thread_local",
@@ -11471,6 +11550,7 @@ dependencies = [
"parking_lot",
"schemars",
"serde",
+ "serde_json",
"serde_json_lenient",
"sha2",
"shellexpand 2.1.2",
@@ -14586,6 +14666,7 @@ dependencies = [
"command_palette_hooks",
"copilot",
"db",
+ "debugger_ui",
"dev_server_projects",
"diagnostics",
"editor",
diff --git a/Cargo.toml b/Cargo.toml
index 47cd3f915f69a..530605f1f223f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,6 +22,9 @@ members = [
"crates/command_palette_hooks",
"crates/context_servers",
"crates/copilot",
+ "crates/dap",
+ "crates/dap_adapters",
+ "crates/debugger_ui",
"crates/db",
"crates/dev_server_projects",
"crates/diagnostics",
@@ -203,7 +206,10 @@ command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
context_servers = { path = "crates/context_servers" }
copilot = { path = "crates/copilot" }
+dap = { path = "crates/dap" }
+dap_adapters = { path = "crates/dap_adapters" }
db = { path = "crates/db" }
+debugger_ui = { path = "crates/debugger_ui" }
dev_server_projects = { path = "crates/dev_server_projects" }
diagnostics = { path = "crates/diagnostics" }
editor = { path = "crates/editor" }
diff --git a/assets/icons/debug.svg b/assets/icons/debug.svg
new file mode 100644
index 0000000000000..8cea0c460402f
--- /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 0000000000000..f6a7b35658eef
--- /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 0000000000000..e2a99c38d032f
--- /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 0000000000000..0eb253715288f
--- /dev/null
+++ b/assets/icons/debug_disconnect.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 0000000000000..a878ce3e04189
--- /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 0000000000000..bea531bc5a755
--- /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 0000000000000..4eff13b94b698
--- /dev/null
+++ b/assets/icons/debug_restart.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 0000000000000..69e5cff3f176c
--- /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 0000000000000..680e13e65e041
--- /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 0000000000000..005b901da3c49
--- /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 0000000000000..fef651c5864a1
--- /dev/null
+++ b/assets/icons/debug_stop.svg
@@ -0,0 +1 @@
+
diff --git a/assets/settings/default.json b/assets/settings/default.json
index d299f05766b5c..f71c15f3b4887 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -1109,6 +1109,13 @@
// }
// ]
"ssh_connections": null,
+
+ "debugger": {
+ "stepping_granularity": "line",
+ "save_breakpoints": true,
+ "button": true
+ },
+
// Configures the Context Server Protocol binaries
//
// Examples:
diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs
index 943cbbb29ff01..e3ad58117f364 100644
--- a/crates/assistant/src/assistant_panel.rs
+++ b/crates/assistant/src/assistant_panel.rs
@@ -350,7 +350,7 @@ impl AssistantPanel {
workspace.project().clone(),
Default::default(),
None,
- NewContext.boxed_clone(),
+ Some(NewContext.boxed_clone()),
cx,
);
diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml
new file mode 100644
index 0000000000000..33971d417bfd1
--- /dev/null
+++ b/crates/dap/Cargo.toml
@@ -0,0 +1,27 @@
+[package]
+name = "dap"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[dependencies]
+anyhow.workspace = true
+async-trait.workspace = true
+dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b95818130022bfc72bbcd639bdd0c0358c7549fc" }
+fs.workspace = true
+futures.workspace = true
+gpui.workspace = true
+http_client.workspace = true
+log.workspace = true
+node_runtime.workspace = true
+parking_lot.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+settings.workspace = true
+smol.workspace = true
+task.workspace = true
diff --git a/crates/dap/LICENSE-GPL b/crates/dap/LICENSE-GPL
new file mode 120000
index 0000000000000..e0f9dbd5d63fe
--- /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 0000000000000..8b819b089bf8c
--- /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 0000000000000..236297eba0151
--- /dev/null
+++ b/crates/dap/src/adapters.rs
@@ -0,0 +1,204 @@
+use crate::client::TransportParams;
+use ::fs::Fs;
+use anyhow::{anyhow, Context, Result};
+use async_trait::async_trait;
+use futures::AsyncReadExt;
+use gpui::AsyncAppContext;
+use http_client::HttpClient;
+use node_runtime::NodeRuntime;
+use serde_json::Value;
+use smol::{
+ self,
+ io::BufReader,
+ net::{TcpListener, TcpStream},
+ process,
+};
+use std::{
+ collections::HashMap,
+ ffi::OsString,
+ fmt::Debug,
+ net::{Ipv4Addr, SocketAddrV4},
+ path::Path,
+ process::Stdio,
+ sync::Arc,
+ time::Duration,
+};
+
+use task::{DebugAdapterConfig, TCPHost};
+
+/// Get an open port to use with the tcp client when not supplied by debug config
+async fn get_open_port(host: Ipv4Addr) -> Option {
+ Some(
+ TcpListener::bind(SocketAddrV4::new(host, 0))
+ .await
+ .ok()?
+ .local_addr()
+ .ok()?
+ .port(),
+ )
+}
+
+pub trait DapDelegate {
+ fn http_client(&self) -> Option>;
+ fn node_runtime(&self) -> Option;
+ fn fs(&self) -> Arc;
+}
+
+/// TCP clients don't have an error communication stream with an adapter
+/// # Parameters
+/// - `host`: The ip/port that that the client will connect too
+/// - `adapter_binary`: The debug adapter binary to start
+/// - `cx`: The context that the new client belongs too
+pub async fn create_tcp_client(
+ host: TCPHost,
+ adapter_binary: &DebugAdapterBinary,
+ cx: &mut AsyncAppContext,
+) -> Result {
+ let host_address = host.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1));
+
+ let mut port = host.port;
+ if port.is_none() {
+ port = get_open_port(host_address).await;
+ }
+
+ let mut command = process::Command::new(&adapter_binary.command);
+
+ if let Some(args) = &adapter_binary.arguments {
+ command.args(args);
+ }
+
+ if let Some(envs) = &adapter_binary.envs {
+ command.envs(envs);
+ }
+
+ command
+ .stdin(Stdio::null())
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .kill_on_drop(true);
+
+ let process = command
+ .spawn()
+ .with_context(|| "failed to start debug adapter.")?;
+
+ if let Some(delay) = host.delay {
+ // some debug adapters need some time to start the TCP server
+ // so we have to wait few milliseconds before we can connect to it
+ cx.background_executor()
+ .timer(Duration::from_millis(delay))
+ .await;
+ }
+
+ let address = SocketAddrV4::new(
+ host_address,
+ port.ok_or(anyhow!("Port is required to connect to TCP server"))?,
+ );
+
+ let (rx, tx) = TcpStream::connect(address).await?.split();
+ log::info!("Debug adapter has connected to tcp server");
+
+ Ok(TransportParams::new(
+ Box::new(BufReader::new(rx)),
+ Box::new(tx),
+ None,
+ Some(process),
+ ))
+}
+
+/// Creates a debug client that connects to an adapter through std input/output
+///
+/// # Parameters
+/// - `adapter_binary`: The debug adapter binary to start
+pub fn create_stdio_client(adapter_binary: &DebugAdapterBinary) -> Result {
+ let mut command = process::Command::new(&adapter_binary.command);
+
+ if let Some(args) = &adapter_binary.arguments {
+ command.args(args);
+ }
+
+ if let Some(envs) = &adapter_binary.envs {
+ command.envs(envs);
+ }
+
+ command
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .kill_on_drop(true);
+
+ let mut process = command
+ .spawn()
+ .with_context(|| "failed to spawn command.")?;
+
+ let stdin = process
+ .stdin
+ .take()
+ .ok_or_else(|| anyhow!("Failed to open stdin"))?;
+ let stdout = process
+ .stdout
+ .take()
+ .ok_or_else(|| anyhow!("Failed to open stdout"))?;
+ let stderr = process
+ .stderr
+ .take()
+ .ok_or_else(|| anyhow!("Failed to open stderr"))?;
+
+ log::info!("Debug adapter has connected to stdio adapter");
+
+ Ok(TransportParams::new(
+ Box::new(BufReader::new(stdout)),
+ Box::new(stdin),
+ Some(Box::new(BufReader::new(stderr))),
+ Some(process),
+ ))
+}
+
+pub struct DebugAdapterName(pub Arc);
+
+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)
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct DebugAdapterBinary {
+ pub command: String,
+ pub arguments: Option>,
+ pub envs: Option>,
+}
+
+#[async_trait(?Send)]
+pub trait DebugAdapter: 'static + Send + Sync {
+ fn id(&self) -> String {
+ "".to_string()
+ }
+
+ fn name(&self) -> DebugAdapterName;
+
+ async fn connect(
+ &self,
+ adapter_binary: &DebugAdapterBinary,
+ cx: &mut AsyncAppContext,
+ ) -> anyhow::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, delegate: &dyn DapDelegate) -> Result<()>;
+
+ async fn fetch_binary(
+ &self,
+ delegate: &dyn DapDelegate,
+ config: &DebugAdapterConfig,
+ ) -> Result;
+
+ /// Should return base configuration to make the debug adapter work
+ fn request_args(&self, config: &DebugAdapterConfig) -> Value;
+}
diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs
new file mode 100644
index 0000000000000..ddf987c02dcfb
--- /dev/null
+++ b/crates/dap/src/client.rs
@@ -0,0 +1,233 @@
+use crate::transport::Transport;
+use anyhow::{anyhow, Result};
+use dap_types::{
+ messages::{Message, Response},
+ requests::Request,
+};
+use futures::{AsyncBufRead, AsyncWrite};
+use gpui::{AppContext, AsyncAppContext};
+use parking_lot::Mutex;
+use serde_json::Value;
+use smol::{
+ channel::{bounded, Receiver, Sender},
+ process::Child,
+};
+use std::{
+ hash::Hash,
+ sync::{
+ atomic::{AtomicU64, Ordering},
+ Arc,
+ },
+};
+use task::{DebugAdapterConfig, DebugRequestType};
+
+#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum ThreadStatus {
+ #[default]
+ Running,
+ Stopped,
+ Exited,
+ Ended,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[repr(transparent)]
+pub struct DebugAdapterClientId(pub usize);
+
+pub struct DebugAdapterClient {
+ id: DebugAdapterClientId,
+ adapter_id: String,
+ request_args: Value,
+ transport: Arc,
+ _process: Arc>>,
+ sequence_count: AtomicU64,
+ config: DebugAdapterConfig,
+}
+
+pub struct TransportParams {
+ rx: Box,
+ tx: Box,
+ err: Option>,
+ process: Option,
+}
+
+impl TransportParams {
+ pub fn new(
+ rx: Box,
+ tx: Box,
+ err: Option>,
+ process: Option,
+ ) -> Self {
+ TransportParams {
+ rx,
+ tx,
+ err,
+ process,
+ }
+ }
+}
+
+impl DebugAdapterClient {
+ pub async fn new(
+ id: DebugAdapterClientId,
+ adapter_id: String,
+ request_args: Value,
+ config: DebugAdapterConfig,
+ transport_params: TransportParams,
+ event_handler: F,
+ cx: &mut AsyncAppContext,
+ ) -> Result>
+ where
+ F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone,
+ {
+ let transport = Self::handle_transport(
+ transport_params.rx,
+ transport_params.tx,
+ transport_params.err,
+ event_handler,
+ cx,
+ );
+ Ok(Arc::new(Self {
+ id,
+ adapter_id,
+ request_args,
+ config,
+ transport,
+ sequence_count: AtomicU64::new(1),
+ _process: Arc::new(Mutex::new(transport_params.process)),
+ }))
+ }
+
+ pub fn handle_transport(
+ rx: Box,
+ tx: Box,
+ err: Option>,
+ event_handler: F,
+ cx: &mut AsyncAppContext,
+ ) -> Arc
+ where
+ F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone,
+ {
+ let transport = Transport::start(rx, tx, err, cx);
+
+ let server_rx = transport.server_rx.clone();
+ let server_tr = transport.server_tx.clone();
+ cx.spawn(|mut cx| async move {
+ Self::handle_recv(server_rx, server_tr, event_handler, &mut cx).await
+ })
+ .detach();
+
+ transport
+ }
+
+ async fn handle_recv(
+ server_rx: Receiver,
+ client_tx: Sender,
+ mut event_handler: F,
+ cx: &mut AsyncAppContext,
+ ) -> Result<()>
+ where
+ F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone,
+ {
+ while let Ok(payload) = server_rx.recv().await {
+ match payload {
+ Message::Event(ev) => cx.update(|cx| event_handler(Message::Event(ev), cx))?,
+ Message::Response(_) => unreachable!(),
+ Message::Request(req) => {
+ cx.update(|cx| event_handler(Message::Request(req), cx))?
+ }
+ };
+ }
+
+ drop(client_tx);
+
+ anyhow::Ok(())
+ }
+
+ /// 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) = bounded::>(1);
+
+ 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
+ .current_requests
+ .lock()
+ .await
+ .insert(sequence_id, callback_tx);
+ }
+
+ self.transport
+ .server_tx
+ .send(Message::Request(request))
+ .await?;
+
+ let response = callback_rx.recv().await??;
+
+ match response.success {
+ true => Ok(serde_json::from_value(response.body.unwrap_or_default())?),
+ false => Err(anyhow!("Request failed")),
+ }
+ }
+
+ pub fn id(&self) -> DebugAdapterClientId {
+ self.id
+ }
+
+ pub fn config(&self) -> DebugAdapterConfig {
+ self.config.clone()
+ }
+
+ pub fn adapter_id(&self) -> String {
+ self.adapter_id.clone()
+ }
+
+ pub fn request_args(&self) -> Value {
+ self.request_args.clone()
+ }
+
+ pub fn request_type(&self) -> DebugRequestType {
+ self.config.request.clone()
+ }
+
+ /// 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.server_tx.close();
+ self.transport.server_rx.close();
+
+ let mut adapter = self._process.lock().take();
+
+ async move {
+ let mut current_requests = self.transport.current_requests.lock().await;
+ let mut pending_requests = self.transport.pending_requests.lock().await;
+
+ current_requests.clear();
+ pending_requests.clear();
+
+ if let Some(mut adapter) = adapter.take() {
+ adapter.kill()?;
+ }
+
+ drop(current_requests);
+ drop(pending_requests);
+ drop(adapter);
+
+ anyhow::Ok(())
+ }
+ .await
+ }
+}
diff --git a/crates/dap/src/debugger_settings.rs b/crates/dap/src/debugger_settings.rs
new file mode 100644
index 0000000000000..0d5a744a84936
--- /dev/null
+++ b/crates/dap/src/debugger_settings.rs
@@ -0,0 +1,47 @@
+use dap_types::SteppingGranularity;
+use gpui::{AppContext, 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,
+}
+
+impl Default for DebuggerSettings {
+ fn default() -> Self {
+ Self {
+ button: true,
+ save_breakpoints: true,
+ stepping_granularity: SteppingGranularity::Line,
+ }
+ }
+}
+
+impl Settings for DebuggerSettings {
+ const KEY: Option<&'static str> = Some("debugger");
+
+ type FileContent = Self;
+
+ fn load(
+ sources: SettingsSources,
+ _: &mut AppContext,
+ ) -> 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 0000000000000..df62861da7bf1
--- /dev/null
+++ b/crates/dap/src/lib.rs
@@ -0,0 +1,5 @@
+pub mod adapters;
+pub mod client;
+pub mod transport;
+pub use dap_types::*;
+pub mod debugger_settings;
diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs
new file mode 100644
index 0000000000000..5a7dbbd4ff313
--- /dev/null
+++ b/crates/dap/src/transport.rs
@@ -0,0 +1,226 @@
+use anyhow::{anyhow, Context, Result};
+use dap_types::{
+ messages::{Message, Response},
+ ErrorResponse,
+};
+use futures::{AsyncBufRead, AsyncWrite};
+use gpui::AsyncAppContext;
+use smol::{
+ channel::{unbounded, Receiver, Sender},
+ io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt},
+ lock::Mutex,
+};
+use std::{collections::HashMap, sync::Arc};
+
+#[derive(Debug)]
+pub struct Transport {
+ pub server_tx: Sender,
+ pub server_rx: Receiver,
+ pub current_requests: Arc>>>>,
+ pub pending_requests: Arc>>>>,
+}
+
+impl Transport {
+ pub fn start(
+ server_stdout: Box,
+ server_stdin: Box,
+ server_stderr: Option>,
+ cx: &mut AsyncAppContext,
+ ) -> Arc {
+ let (client_tx, server_rx) = unbounded::();
+ let (server_tx, client_rx) = unbounded::();
+
+ let current_requests = Arc::new(Mutex::new(HashMap::default()));
+ let pending_requests = Arc::new(Mutex::new(HashMap::default()));
+
+ cx.background_executor()
+ .spawn(Self::receive(
+ pending_requests.clone(),
+ server_stdout,
+ client_tx,
+ ))
+ .detach();
+
+ if let Some(stderr) = server_stderr {
+ cx.background_executor().spawn(Self::err(stderr)).detach();
+ }
+
+ cx.background_executor()
+ .spawn(Self::send(
+ current_requests.clone(),
+ pending_requests.clone(),
+ server_stdin,
+ client_rx,
+ ))
+ .detach();
+
+ Arc::new(Self {
+ server_rx,
+ server_tx,
+ current_requests,
+ pending_requests,
+ })
+ }
+
+ async fn recv_server_message(
+ reader: &mut Box,
+ buffer: &mut String,
+ ) -> Result {
+ let mut content_length = None;
+ loop {
+ buffer.truncate(0);
+
+ if reader
+ .read_line(buffer)
+ .await
+ .with_context(|| "reading a message from server")?
+ == 0
+ {
+ return Err(anyhow!("debugger reader stream closed"));
+ };
+
+ if buffer == "\r\n" {
+ break;
+ }
+
+ let parts = buffer.trim().split_once(": ");
+
+ match parts {
+ Some(("Content-Length", value)) => {
+ content_length = Some(value.parse().context("invalid content length")?);
+ }
+ _ => {}
+ }
+ }
+
+ let content_length = content_length.context("missing content length")?;
+
+ let mut content = vec![0; content_length];
+ reader
+ .read_exact(&mut content)
+ .await
+ .with_context(|| "reading after a loop")?;
+
+ let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?;
+ Ok(serde_json::from_str::(msg)?)
+ }
+
+ async fn recv_server_error(
+ err: &mut (impl AsyncBufRead + Unpin + Send),
+ buffer: &mut String,
+ ) -> Result<()> {
+ buffer.truncate(0);
+ if err.read_line(buffer).await? == 0 {
+ return Err(anyhow!("debugger error stream closed"));
+ };
+
+ Ok(())
+ }
+
+ async fn send_payload_to_server(
+ current_requests: &Mutex>>>,
+ pending_requests: &Mutex>>>,
+ server_stdin: &mut Box,
+ mut payload: Message,
+ ) -> Result<()> {
+ if let Message::Request(request) = &mut payload {
+ if let Some(sender) = current_requests.lock().await.remove(&request.seq) {
+ pending_requests.lock().await.insert(request.seq, sender);
+ }
+ }
+ Self::send_string_to_server(server_stdin, serde_json::to_string(&payload)?).await
+ }
+
+ async fn send_string_to_server(
+ server_stdin: &mut Box,
+ request: String,
+ ) -> Result<()> {
+ server_stdin
+ .write_all(format!("Content-Length: {}\r\n\r\n{}", request.len(), request).as_bytes())
+ .await?;
+
+ server_stdin.flush().await?;
+ Ok(())
+ }
+
+ fn process_response(response: Response) -> Result {
+ if response.success {
+ Ok(response)
+ } else {
+ if let Some(body) = response.body {
+ if let Ok(error) = serde_json::from_value::(body) {
+ if let Some(message) = error.error {
+ return Err(anyhow!(message.format));
+ };
+ };
+ }
+
+ Err(anyhow!("Received error response from adapter"))
+ }
+ }
+
+ async fn process_server_message(
+ pending_requests: &Arc>>>>,
+ client_tx: &Sender,
+ message: Message,
+ ) -> Result<()> {
+ match message {
+ Message::Response(res) => {
+ if let Some(tx) = pending_requests.lock().await.remove(&res.request_seq) {
+ tx.send(Self::process_response(res)).await?;
+ } else {
+ client_tx.send(Message::Response(res)).await?;
+ };
+ }
+ Message::Request(_) => {
+ client_tx.send(message).await?;
+ }
+ Message::Event(_) => {
+ client_tx.send(message).await?;
+ }
+ }
+ Ok(())
+ }
+
+ async fn receive(
+ pending_requests: Arc>>>>,
+ mut server_stdout: Box,
+ client_tx: Sender,
+ ) -> Result<()> {
+ let mut recv_buffer = String::new();
+
+ while let Ok(msg) = Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await {
+ Self::process_server_message(&pending_requests, &client_tx, msg)
+ .await
+ .context("Process server message failed in transport::receive")?;
+ }
+
+ Ok(())
+ }
+
+ async fn send(
+ current_requests: Arc>>>>,
+ pending_requests: Arc>>>>,
+ mut server_stdin: Box,
+ client_rx: Receiver,
+ ) -> Result<()> {
+ while let Ok(payload) = client_rx.recv().await {
+ Self::send_payload_to_server(
+ ¤t_requests,
+ &pending_requests,
+ &mut server_stdin,
+ payload,
+ )
+ .await?;
+ }
+
+ Ok(())
+ }
+
+ async fn err(mut server_stderr: Box) -> Result<()> {
+ let mut recv_buffer = String::new();
+ loop {
+ Self::recv_server_error(&mut server_stderr, &mut recv_buffer).await?;
+ }
+ }
+}
diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml
new file mode 100644
index 0000000000000..d6515c69b8a41
--- /dev/null
+++ b/crates/dap_adapters/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "dap_adapters"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "GPL-3.0-or-later"
+
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/dap_adapters.rs"
+doctest = false
+
+[dependencies]
+anyhow.workspace = true
+async-trait.workspace = true
+dap.workspace = true
+fs.workspace = true
+futures.workspace = true
+gpui.workspace = true
+http_client.workspace = true
+task.workspace = true
+smol.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+paths.workspace = true
diff --git a/crates/dap_adapters/LICENSE-GPL b/crates/dap_adapters/LICENSE-GPL
new file mode 120000
index 0000000000000..e0f9dbd5d63fe
--- /dev/null
+++ b/crates/dap_adapters/LICENSE-GPL
@@ -0,0 +1 @@
+LICENSE-GPL
\ No newline at end of file
diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs
new file mode 100644
index 0000000000000..a33e1802839b5
--- /dev/null
+++ b/crates/dap_adapters/src/custom.rs
@@ -0,0 +1,63 @@
+use std::ffi::OsString;
+
+use serde_json::Value;
+use task::DebugAdapterConfig;
+
+use crate::*;
+
+#[derive(Debug, Eq, PartialEq, Clone)]
+pub(crate) struct CustomDebugAdapter {
+ custom_args: CustomArgs,
+}
+
+impl CustomDebugAdapter {
+ const ADAPTER_NAME: &'static str = "custom_dap";
+
+ pub(crate) fn new(custom_args: CustomArgs) -> Self {
+ CustomDebugAdapter { custom_args }
+ }
+}
+
+#[async_trait(?Send)]
+impl DebugAdapter for CustomDebugAdapter {
+ fn name(&self) -> DebugAdapterName {
+ DebugAdapterName(Self::ADAPTER_NAME.into())
+ }
+
+ async fn connect(
+ &self,
+ adapter_binary: &DebugAdapterBinary,
+ cx: &mut AsyncAppContext,
+ ) -> Result {
+ match &self.custom_args.connection {
+ DebugConnectionType::STDIO => create_stdio_client(adapter_binary),
+ DebugConnectionType::TCP(tcp_host) => {
+ create_tcp_client(tcp_host.clone(), adapter_binary, cx).await
+ }
+ }
+ }
+
+ async fn install_binary(&self, _: &dyn DapDelegate) -> Result<()> {
+ Ok(())
+ }
+
+ async fn fetch_binary(
+ &self,
+ _: &dyn DapDelegate,
+ _: &DebugAdapterConfig,
+ ) -> Result {
+ Ok(DebugAdapterBinary {
+ command: self.custom_args.command.clone(),
+ arguments: self
+ .custom_args
+ .args
+ .clone()
+ .map(|args| args.iter().map(OsString::from).collect()),
+ envs: self.custom_args.envs.clone(),
+ })
+ }
+
+ fn request_args(&self, config: &DebugAdapterConfig) -> Value {
+ json!({"program": config.program})
+ }
+}
diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs
new file mode 100644
index 0000000000000..fac4f3c77c737
--- /dev/null
+++ b/crates/dap_adapters/src/dap_adapters.rs
@@ -0,0 +1,42 @@
+mod custom;
+mod javascript;
+mod lldb;
+mod php;
+mod python;
+
+use custom::CustomDebugAdapter;
+use javascript::JsDebugAdapter;
+use lldb::LldbDebugAdapter;
+use php::PhpDebugAdapter;
+use python::PythonDebugAdapter;
+
+use anyhow::{anyhow, bail, Context, Result};
+use async_trait::async_trait;
+use dap::{
+ adapters::{
+ create_stdio_client, create_tcp_client, DapDelegate, DebugAdapter, DebugAdapterBinary,
+ DebugAdapterName,
+ },
+ client::TransportParams,
+};
+use gpui::AsyncAppContext;
+use http_client::github::latest_github_release;
+use serde_json::{json, Value};
+use smol::{
+ fs::{self, File},
+ process,
+};
+use std::{fmt::Debug, process::Stdio};
+use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost};
+
+pub fn build_adapter(adapter_config: &DebugAdapterConfig) -> Result> {
+ match &adapter_config.kind {
+ DebugAdapterKind::Custom(start_args) => {
+ Ok(Box::new(CustomDebugAdapter::new(start_args.clone())))
+ }
+ DebugAdapterKind::Python => Ok(Box::new(PythonDebugAdapter::new())),
+ DebugAdapterKind::PHP => Ok(Box::new(PhpDebugAdapter::new())),
+ DebugAdapterKind::Javascript => Ok(Box::new(JsDebugAdapter::new())),
+ DebugAdapterKind::Lldb => Ok(Box::new(LldbDebugAdapter::new())),
+ }
+}
diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs
new file mode 100644
index 0000000000000..288c2c0508024
--- /dev/null
+++ b/crates/dap_adapters/src/javascript.rs
@@ -0,0 +1,169 @@
+use crate::*;
+
+#[derive(Debug, Eq, PartialEq, Clone)]
+pub(crate) struct JsDebugAdapter {}
+
+impl JsDebugAdapter {
+ const ADAPTER_NAME: &'static str = "vscode-js-debug";
+ const ADAPTER_PATH: &'static str = "src/dapDebugServer.js";
+
+ pub(crate) fn new() -> Self {
+ JsDebugAdapter {}
+ }
+}
+
+#[async_trait(?Send)]
+impl DebugAdapter for JsDebugAdapter {
+ fn name(&self) -> DebugAdapterName {
+ DebugAdapterName(Self::ADAPTER_NAME.into())
+ }
+
+ async fn connect(
+ &self,
+ adapter_binary: &DebugAdapterBinary,
+ cx: &mut AsyncAppContext,
+ ) -> Result {
+ let host = TCPHost {
+ port: Some(8133),
+ host: None,
+ delay: Some(1000),
+ };
+
+ create_tcp_client(host, adapter_binary, cx).await
+ }
+
+ async fn fetch_binary(
+ &self,
+ delegate: &dyn DapDelegate,
+ _: &DebugAdapterConfig,
+ ) -> Result {
+ let node_runtime = delegate
+ .node_runtime()
+ .ok_or(anyhow!("Couldn't get npm runtime"))?;
+
+ let adapter_path = paths::debug_adapters_dir().join(self.name());
+
+ Ok(DebugAdapterBinary {
+ command: node_runtime
+ .binary_path()
+ .await?
+ .to_string_lossy()
+ .into_owned(),
+ arguments: Some(vec![
+ adapter_path.join(Self::ADAPTER_PATH).into(),
+ "8133".into(),
+ ]),
+ envs: None,
+ })
+ }
+
+ async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> {
+ let adapter_path = paths::debug_adapters_dir().join(self.name());
+ let fs = delegate.fs();
+
+ if fs.is_dir(adapter_path.as_path()).await {
+ return Ok(());
+ }
+
+ if let Some(http_client) = delegate.http_client() {
+ if !adapter_path.exists() {
+ fs.create_dir(&adapter_path.as_path()).await?;
+ }
+
+ let release = latest_github_release(
+ "microsoft/vscode-js-debug",
+ false,
+ false,
+ http_client.clone(),
+ )
+ .await?;
+
+ let asset_name = format!("{}-{}", self.name(), release.tag_name);
+ let zip_path = adapter_path.join(asset_name);
+
+ if fs::metadata(&zip_path).await.is_err() {
+ let mut response = http_client
+ .get(&release.zipball_url, Default::default(), true)
+ .await
+ .context("Error downloading release")?;
+
+ let mut file = File::create(&zip_path).await?;
+ futures::io::copy(response.body_mut(), &mut file).await?;
+
+ let _unzip_status = process::Command::new("unzip")
+ .current_dir(&adapter_path)
+ .arg(&zip_path)
+ .output()
+ .await?
+ .status;
+
+ let mut ls = process::Command::new("ls")
+ .current_dir(&adapter_path)
+ .stdout(Stdio::piped())
+ .spawn()?;
+
+ let std = ls
+ .stdout
+ .take()
+ .ok_or(anyhow!("Failed to list directories"))?
+ .into_stdio()
+ .await?;
+
+ let file_name = String::from_utf8(
+ process::Command::new("grep")
+ .arg("microsoft-vscode-js-debug")
+ .stdin(std)
+ .output()
+ .await?
+ .stdout,
+ )?;
+
+ let file_name = file_name.trim_end();
+
+ process::Command::new("sh")
+ .current_dir(&adapter_path)
+ .arg("-c")
+ .arg(format!("mv {file_name}/* ."))
+ .output()
+ .await?;
+
+ process::Command::new("rm")
+ .current_dir(&adapter_path)
+ .arg("-rf")
+ .arg(file_name)
+ .arg(zip_path)
+ .output()
+ .await?;
+
+ let _ = delegate
+ .node_runtime()
+ .ok_or(anyhow!("Couldn't get npm runtime"))?
+ .run_npm_subcommand(&adapter_path, "install", &[])
+ .await
+ .ok();
+
+ let _ = delegate
+ .node_runtime()
+ .ok_or(anyhow!("Couldn't get npm runtime"))?
+ .run_npm_subcommand(&adapter_path, "run", &["compile"])
+ .await
+ .ok();
+
+ return Ok(());
+ }
+ }
+
+ bail!("Install or fetch not implemented for Javascript debug adapter (yet)");
+ }
+
+ fn request_args(&self, config: &DebugAdapterConfig) -> Value {
+ json!({
+ "program": config.program,
+ "type": "pwa-node",
+ "skipFiles": [
+ "/**",
+ "**/node_modules/**"
+ ]
+ })
+ }
+}
diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs
new file mode 100644
index 0000000000000..56cda6750a275
--- /dev/null
+++ b/crates/dap_adapters/src/lldb.rs
@@ -0,0 +1,47 @@
+use anyhow::Result;
+use async_trait::async_trait;
+use task::DebugAdapterConfig;
+
+use crate::*;
+
+#[derive(Debug, Eq, PartialEq, Clone)]
+pub(crate) struct LldbDebugAdapter {}
+
+impl LldbDebugAdapter {
+ const ADAPTER_NAME: &'static str = "lldb";
+
+ pub(crate) fn new() -> Self {
+ LldbDebugAdapter {}
+ }
+}
+
+#[async_trait(?Send)]
+impl DebugAdapter for LldbDebugAdapter {
+ fn name(&self) -> DebugAdapterName {
+ DebugAdapterName(Self::ADAPTER_NAME.into())
+ }
+
+ async fn connect(
+ &self,
+ adapter_binary: &DebugAdapterBinary,
+ _: &mut AsyncAppContext,
+ ) -> Result {
+ create_stdio_client(adapter_binary)
+ }
+
+ async fn install_binary(&self, _: &dyn DapDelegate) -> Result<()> {
+ bail!("Install or fetch not implemented for lldb debug adapter (yet)")
+ }
+
+ async fn fetch_binary(
+ &self,
+ _: &dyn DapDelegate,
+ _: &DebugAdapterConfig,
+ ) -> Result {
+ bail!("Install or fetch not implemented for lldb debug adapter (yet)")
+ }
+
+ fn request_args(&self, config: &DebugAdapterConfig) -> Value {
+ json!({"program": config.program})
+ }
+}
diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs
new file mode 100644
index 0000000000000..62ecb16506bb8
--- /dev/null
+++ b/crates/dap_adapters/src/php.rs
@@ -0,0 +1,158 @@
+use crate::*;
+
+#[derive(Debug, Eq, PartialEq, Clone)]
+pub(crate) struct PhpDebugAdapter {}
+
+impl PhpDebugAdapter {
+ const ADAPTER_NAME: &'static str = "vscode-php-debug";
+ const ADAPTER_PATH: &'static str = "out/phpDebug.js";
+
+ pub(crate) fn new() -> Self {
+ PhpDebugAdapter {}
+ }
+}
+
+#[async_trait(?Send)]
+impl DebugAdapter for PhpDebugAdapter {
+ fn name(&self) -> DebugAdapterName {
+ DebugAdapterName(Self::ADAPTER_NAME.into())
+ }
+
+ async fn connect(
+ &self,
+ adapter_binary: &DebugAdapterBinary,
+ cx: &mut AsyncAppContext,
+ ) -> Result {
+ let host = TCPHost {
+ port: Some(8132),
+ host: None,
+ delay: Some(1000),
+ };
+
+ create_tcp_client(host, adapter_binary, cx).await
+ }
+
+ async fn fetch_binary(
+ &self,
+ delegate: &dyn DapDelegate,
+ _: &DebugAdapterConfig,
+ ) -> Result {
+ let node_runtime = delegate
+ .node_runtime()
+ .ok_or(anyhow!("Couldn't get npm runtime"))?;
+
+ let adapter_path = paths::debug_adapters_dir().join(self.name());
+
+ Ok(DebugAdapterBinary {
+ command: node_runtime
+ .binary_path()
+ .await?
+ .to_string_lossy()
+ .into_owned(),
+ arguments: Some(vec![
+ adapter_path.join(Self::ADAPTER_PATH).into(),
+ "--server=8132".into(),
+ ]),
+ envs: None,
+ })
+ }
+
+ async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> {
+ let adapter_path = paths::debug_adapters_dir().join(self.name());
+ let fs = delegate.fs();
+
+ if fs.is_dir(adapter_path.as_path()).await {
+ return Ok(());
+ }
+
+ if let Some(http_client) = delegate.http_client() {
+ if !adapter_path.exists() {
+ fs.create_dir(&adapter_path.as_path()).await?;
+ }
+
+ let release =
+ latest_github_release("xdebug/vscode-php-debug", false, false, http_client.clone())
+ .await?;
+
+ let asset_name = format!("{}-{}", self.name(), release.tag_name);
+ let zip_path = adapter_path.join(asset_name);
+
+ if fs::metadata(&zip_path).await.is_err() {
+ let mut response = http_client
+ .get(&release.zipball_url, Default::default(), true)
+ .await
+ .context("Error downloading release")?;
+
+ let mut file = File::create(&zip_path).await?;
+ futures::io::copy(response.body_mut(), &mut file).await?;
+
+ let _unzip_status = process::Command::new("unzip")
+ .current_dir(&adapter_path)
+ .arg(&zip_path)
+ .output()
+ .await?
+ .status;
+
+ let mut ls = process::Command::new("ls")
+ .current_dir(&adapter_path)
+ .stdout(Stdio::piped())
+ .spawn()?;
+
+ let std = ls
+ .stdout
+ .take()
+ .ok_or(anyhow!("Failed to list directories"))?
+ .into_stdio()
+ .await?;
+
+ let file_name = String::from_utf8(
+ process::Command::new("grep")
+ .arg("xdebug-vscode-php-debug")
+ .stdin(std)
+ .output()
+ .await?
+ .stdout,
+ )?;
+
+ let file_name = file_name.trim_end();
+
+ process::Command::new("sh")
+ .current_dir(&adapter_path)
+ .arg("-c")
+ .arg(format!("mv {file_name}/* ."))
+ .output()
+ .await?;
+
+ process::Command::new("rm")
+ .current_dir(&adapter_path)
+ .arg("-rf")
+ .arg(file_name)
+ .arg(zip_path)
+ .output()
+ .await?;
+
+ let _ = delegate
+ .node_runtime()
+ .ok_or(anyhow!("Couldn't get npm runtime"))?
+ .run_npm_subcommand(&adapter_path, "install", &[])
+ .await
+ .is_ok();
+
+ let _ = delegate
+ .node_runtime()
+ .ok_or(anyhow!("Couldn't get npm runtime"))?
+ .run_npm_subcommand(&adapter_path, "run", &["build"])
+ .await
+ .is_ok();
+
+ return Ok(());
+ }
+ }
+
+ bail!("Install or fetch not implemented for PHP debug adapter (yet)");
+ }
+
+ fn request_args(&self, config: &DebugAdapterConfig) -> Value {
+ json!({"program": config.program})
+ }
+}
diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs
new file mode 100644
index 0000000000000..12b16280f0de0
--- /dev/null
+++ b/crates/dap_adapters/src/python.rs
@@ -0,0 +1,128 @@
+use crate::*;
+
+#[derive(Debug, Eq, PartialEq, Clone)]
+pub(crate) struct PythonDebugAdapter {}
+
+impl PythonDebugAdapter {
+ const ADAPTER_NAME: &'static str = "debugpy";
+ const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
+
+ pub(crate) fn new() -> Self {
+ PythonDebugAdapter {}
+ }
+}
+
+#[async_trait(?Send)]
+impl DebugAdapter for PythonDebugAdapter {
+ fn name(&self) -> DebugAdapterName {
+ DebugAdapterName(Self::ADAPTER_NAME.into())
+ }
+
+ async fn connect(
+ &self,
+ adapter_binary: &DebugAdapterBinary,
+ _: &mut AsyncAppContext,
+ ) -> Result {
+ create_stdio_client(adapter_binary)
+ }
+
+ async fn fetch_binary(
+ &self,
+ _: &dyn DapDelegate,
+ _: &DebugAdapterConfig,
+ ) -> Result {
+ let adapter_path = paths::debug_adapters_dir().join(self.name());
+
+ Ok(DebugAdapterBinary {
+ command: "python3".to_string(),
+ arguments: Some(vec![adapter_path.join(Self::ADAPTER_PATH).into()]),
+ envs: None,
+ })
+ }
+
+ async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> {
+ let adapter_path = paths::debug_adapters_dir().join(self.name());
+ let fs = delegate.fs();
+
+ if fs.is_dir(adapter_path.as_path()).await {
+ return Ok(());
+ }
+
+ if let Some(http_client) = delegate.http_client() {
+ let debugpy_dir = paths::debug_adapters_dir().join("debugpy");
+
+ if !debugpy_dir.exists() {
+ fs.create_dir(&debugpy_dir.as_path()).await?;
+ }
+
+ let release =
+ latest_github_release("microsoft/debugpy", false, false, http_client.clone())
+ .await?;
+ let asset_name = format!("{}.zip", release.tag_name);
+
+ let zip_path = debugpy_dir.join(asset_name);
+
+ if fs::metadata(&zip_path).await.is_err() {
+ let mut response = http_client
+ .get(&release.zipball_url, Default::default(), true)
+ .await
+ .context("Error downloading release")?;
+
+ let mut file = File::create(&zip_path).await?;
+ futures::io::copy(response.body_mut(), &mut file).await?;
+
+ let _unzip_status = process::Command::new("unzip")
+ .current_dir(&debugpy_dir)
+ .arg(&zip_path)
+ .output()
+ .await?
+ .status;
+
+ let mut ls = process::Command::new("ls")
+ .current_dir(&debugpy_dir)
+ .stdout(Stdio::piped())
+ .spawn()?;
+
+ let std = ls
+ .stdout
+ .take()
+ .ok_or(anyhow!("Failed to list directories"))?
+ .into_stdio()
+ .await?;
+
+ let file_name = String::from_utf8(
+ process::Command::new("grep")
+ .arg("microsoft-debugpy")
+ .stdin(std)
+ .output()
+ .await?
+ .stdout,
+ )?;
+
+ let file_name = file_name.trim_end();
+ process::Command::new("sh")
+ .current_dir(&debugpy_dir)
+ .arg("-c")
+ .arg(format!("mv {file_name}/* ."))
+ .output()
+ .await?;
+
+ process::Command::new("rm")
+ .current_dir(&debugpy_dir)
+ .arg("-rf")
+ .arg(file_name)
+ .arg(zip_path)
+ .output()
+ .await?;
+
+ return Ok(());
+ }
+ }
+
+ bail!("Install or fetch not implemented for Python debug adapter (yet)");
+ }
+
+ fn request_args(&self, config: &DebugAdapterConfig) -> Value {
+ json!({"program": config.program, "subProcess": true})
+ }
+}
diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml
new file mode 100644
index 0000000000000..1e720b53cda99
--- /dev/null
+++ b/crates/debugger_ui/Cargo.toml
@@ -0,0 +1,35 @@
+[package]
+name = "debugger_ui"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[dependencies]
+anyhow.workspace = true
+dap.workspace = true
+editor.workspace = true
+futures.workspace = true
+fuzzy.workspace = true
+gpui.workspace = true
+language.workspace = true
+menu.workspace = true
+parking_lot.workspace = true
+project.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+settings.workspace = true
+task.workspace = true
+tasks_ui.workspace = true
+theme.workspace = true
+ui.workspace = true
+workspace.workspace = true
+
+[dev-dependencies]
+editor = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+project = { workspace = true, features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }
diff --git a/crates/debugger_ui/LICENSE-GPL b/crates/debugger_ui/LICENSE-GPL
new file mode 120000
index 0000000000000..e0f9dbd5d63fe
--- /dev/null
+++ b/crates/debugger_ui/LICENSE-GPL
@@ -0,0 +1 @@
+LICENSE-GPL
\ No newline at end of file
diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs
new file mode 100644
index 0000000000000..00b2c37015629
--- /dev/null
+++ b/crates/debugger_ui/src/console.rs
@@ -0,0 +1,383 @@
+use crate::{
+ stack_frame_list::{StackFrameList, StackFrameListEvent},
+ variable_list::VariableList,
+};
+use dap::client::DebugAdapterClientId;
+use editor::{CompletionProvider, Editor, EditorElement, EditorStyle};
+use fuzzy::StringMatchCandidate;
+use gpui::{Model, Render, Subscription, Task, TextStyle, View, ViewContext, WeakView};
+use language::{Buffer, CodeLabel, LanguageServerId, ToOffsetUtf16};
+use menu::Confirm;
+use parking_lot::RwLock;
+use project::{dap_store::DapStore, Completion};
+use settings::Settings;
+use std::{collections::HashMap, sync::Arc};
+use theme::ThemeSettings;
+use ui::prelude::*;
+
+pub struct Console {
+ console: View,
+ query_bar: View,
+ dap_store: Model,
+ client_id: DebugAdapterClientId,
+ _subscriptions: Vec,
+ variable_list: View,
+ stack_frame_list: View,
+}
+
+impl Console {
+ pub fn new(
+ stack_frame_list: &View,
+ client_id: &DebugAdapterClientId,
+ variable_list: View,
+ dap_store: Model,
+ cx: &mut ViewContext,
+ ) -> Self {
+ let console = cx.new_view(|cx| {
+ let mut editor = Editor::multi_line(cx);
+ editor.move_to_end(&editor::actions::MoveToEnd, cx);
+ editor.set_read_only(true);
+ editor.set_show_gutter(false, cx);
+ editor.set_use_autoclose(false);
+ editor.set_show_wrap_guides(false, cx);
+ editor.set_show_indent_guides(false, cx);
+ editor.set_show_inline_completions(Some(false), cx);
+ editor
+ });
+
+ let this = cx.view().downgrade();
+ let query_bar = cx.new_view(|cx| {
+ let mut editor = Editor::single_line(cx);
+ editor.set_placeholder_text("Evaluate an expression", cx);
+ editor.set_use_autoclose(false);
+ editor.set_show_gutter(false, cx);
+ editor.set_show_wrap_guides(false, cx);
+ editor.set_show_indent_guides(false, cx);
+ editor.set_completion_provider(Some(Box::new(ConsoleQueryBarCompletionProvider(this))));
+
+ editor
+ });
+
+ let _subscriptions =
+ vec![cx.subscribe(stack_frame_list, Self::handle_stack_frame_list_events)];
+
+ Self {
+ console,
+ dap_store,
+ query_bar,
+ variable_list,
+ _subscriptions,
+ client_id: *client_id,
+ stack_frame_list: stack_frame_list.clone(),
+ }
+ }
+
+ fn handle_stack_frame_list_events(
+ &mut self,
+ _: View,
+ event: &StackFrameListEvent,
+ cx: &mut ViewContext,
+ ) {
+ match event {
+ StackFrameListEvent::ChangedStackFrame => cx.notify(),
+ StackFrameListEvent::StackFramesUpdated => {
+ // TODO debugger: check if we need to do something here
+ }
+ }
+ }
+
+ pub fn add_message(&mut self, message: &str, cx: &mut ViewContext) {
+ self.console.update(cx, |console, cx| {
+ console.set_read_only(false);
+ console.move_to_end(&editor::actions::MoveToEnd, cx);
+ console.insert(format!("{}\n", message.trim_end()).as_str(), cx);
+ console.set_read_only(true);
+ });
+ }
+
+ fn evaluate(&mut self, _: &Confirm, cx: &mut ViewContext) {
+ let expression = self.query_bar.update(cx, |editor, cx| {
+ let expression = editor.text(cx);
+
+ editor.clear(cx);
+
+ expression
+ });
+
+ let evaluate_task = self.dap_store.update(cx, |store, cx| {
+ store.evaluate(
+ &self.client_id,
+ self.stack_frame_list.read(cx).current_stack_frame_id(),
+ expression,
+ dap::EvaluateArgumentsContext::Variables,
+ cx,
+ )
+ });
+
+ cx.spawn(|this, mut cx| async move {
+ let response = evaluate_task.await?;
+
+ this.update(&mut cx, |console, cx| {
+ console.add_message(&response.result, cx);
+
+ console.variable_list.update(cx, |variable_list, cx| {
+ variable_list
+ .refetch_existing_variables(cx)
+ .detach_and_log_err(cx);
+ })
+ })
+ })
+ .detach_and_log_err(cx);
+ }
+
+ fn render_console(&self, cx: &ViewContext) -> impl IntoElement {
+ let settings = ThemeSettings::get_global(cx);
+ let text_style = TextStyle {
+ color: if self.console.read(cx).read_only(cx) {
+ cx.theme().colors().text_disabled
+ } else {
+ cx.theme().colors().text
+ },
+ font_family: settings.buffer_font.family.clone(),
+ font_features: settings.buffer_font.features.clone(),
+ font_size: settings.buffer_font_size.into(),
+ font_weight: settings.buffer_font.weight,
+ line_height: relative(settings.buffer_line_height.value()),
+ ..Default::default()
+ };
+
+ EditorElement::new(
+ &self.console,
+ EditorStyle {
+ background: cx.theme().colors().editor_background,
+ local_player: cx.theme().players().local(),
+ text: text_style,
+ ..Default::default()
+ },
+ )
+ }
+
+ fn render_query_bar(&self, cx: &ViewContext) -> impl IntoElement {
+ let settings = ThemeSettings::get_global(cx);
+ let text_style = TextStyle {
+ color: if self.console.read(cx).read_only(cx) {
+ cx.theme().colors().text_disabled
+ } else {
+ cx.theme().colors().text
+ },
+ font_family: settings.ui_font.family.clone(),
+ font_features: settings.ui_font.features.clone(),
+ font_fallbacks: settings.ui_font.fallbacks.clone(),
+ font_size: TextSize::Editor.rems(cx).into(),
+ font_weight: settings.ui_font.weight,
+ line_height: relative(1.3),
+ ..Default::default()
+ };
+
+ EditorElement::new(
+ &self.query_bar,
+ EditorStyle {
+ background: cx.theme().colors().editor_background,
+ local_player: cx.theme().players().local(),
+ text: text_style,
+ ..Default::default()
+ },
+ )
+ }
+}
+
+impl Render for Console {
+ fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement {
+ v_flex()
+ .key_context("DebugConsole")
+ .on_action(cx.listener(Self::evaluate))
+ .size_full()
+ .child(self.render_console(cx))
+ .child(
+ div()
+ .child(self.render_query_bar(cx))
+ .pt(Spacing::XSmall.rems(cx)),
+ )
+ .border_2()
+ }
+}
+
+struct ConsoleQueryBarCompletionProvider(WeakView);
+
+impl CompletionProvider for ConsoleQueryBarCompletionProvider {
+ fn completions(
+ &self,
+ buffer: &Model,
+ buffer_position: language::Anchor,
+ _trigger: editor::CompletionContext,
+ cx: &mut ViewContext,
+ ) -> gpui::Task>> {
+ let Some(console) = self.0.upgrade() else {
+ return Task::ready(Ok(Vec::new()));
+ };
+
+ let support_completions = console.update(cx, |this, cx| {
+ this.dap_store
+ .read(cx)
+ .capabilities_by_id(&this.client_id)
+ .supports_completions_request
+ .unwrap_or_default()
+ });
+
+ if support_completions {
+ self.client_completions(&console, buffer, buffer_position, cx)
+ } else {
+ self.variable_list_completions(&console, buffer, buffer_position, cx)
+ }
+ }
+
+ fn resolve_completions(
+ &self,
+ _buffer: Model,
+ _completion_indices: Vec,
+ _completions: Arc>>,
+ _cx: &mut ViewContext,
+ ) -> gpui::Task> {
+ Task::ready(Ok(false))
+ }
+
+ fn apply_additional_edits_for_completion(
+ &self,
+ _buffer: Model,
+ _completion: project::Completion,
+ _push_to_history: bool,
+ _cx: &mut ViewContext,
+ ) -> gpui::Task>> {
+ Task::ready(Ok(None))
+ }
+
+ fn is_completion_trigger(
+ &self,
+ _buffer: &Model,
+ _position: language::Anchor,
+ _text: &str,
+ _trigger_in_words: bool,
+ _cx: &mut ViewContext,
+ ) -> bool {
+ true
+ }
+}
+
+impl ConsoleQueryBarCompletionProvider {
+ fn variable_list_completions(
+ &self,
+ console: &View,
+ buffer: &Model,
+ buffer_position: language::Anchor,
+ cx: &mut ViewContext,
+ ) -> gpui::Task>> {
+ let (variables, string_matches) = console.update(cx, |console, cx| {
+ let mut variables = HashMap::new();
+ let mut string_matches = Vec::new();
+
+ for variable in console.variable_list.update(cx, |v, cx| v.variables(cx)) {
+ if let Some(evaluate_name) = &variable.variable.evaluate_name {
+ variables.insert(evaluate_name.clone(), variable.variable.value.clone());
+ string_matches.push(StringMatchCandidate {
+ id: 0,
+ string: evaluate_name.clone(),
+ char_bag: evaluate_name.chars().collect(),
+ });
+ }
+
+ variables.insert(
+ variable.variable.name.clone(),
+ variable.variable.value.clone(),
+ );
+
+ string_matches.push(StringMatchCandidate {
+ id: 0,
+ string: variable.variable.name.clone(),
+ char_bag: variable.variable.name.chars().collect(),
+ });
+ }
+
+ (variables, string_matches)
+ });
+
+ let query = buffer.read(cx).text();
+ let start_position = buffer.read(cx).anchor_before(0);
+
+ cx.spawn(|_, cx| async move {
+ let matches = fuzzy::match_strings(
+ &string_matches,
+ &query,
+ true,
+ 10,
+ &Default::default(),
+ cx.background_executor().clone(),
+ )
+ .await;
+
+ Ok(matches
+ .iter()
+ .filter_map(|string_match| {
+ let variable_value = variables.get(&string_match.string)?;
+
+ Some(project::Completion {
+ old_range: start_position..buffer_position,
+ new_text: string_match.string.clone(),
+ label: CodeLabel {
+ filter_range: 0..string_match.string.len(),
+ text: format!("{} {}", string_match.string.clone(), variable_value),
+ runs: Vec::new(),
+ },
+ server_id: LanguageServerId(0), // TODO debugger: read from client
+ documentation: None,
+ lsp_completion: Default::default(),
+ confirm: None,
+ })
+ })
+ .collect())
+ })
+ }
+
+ fn client_completions(
+ &self,
+ console: &View,
+ buffer: &Model,
+ buffer_position: language::Anchor,
+ cx: &mut ViewContext,
+ ) -> gpui::Task>> {
+ let text = buffer.read(cx).text();
+ let start_position = buffer.read(cx).anchor_before(0);
+ let snapshot = buffer.read(cx).snapshot();
+
+ let completion_task = console.update(cx, |console, cx| {
+ console.dap_store.update(cx, |store, cx| {
+ store.completions(
+ &console.client_id,
+ console.stack_frame_list.read(cx).current_stack_frame_id(),
+ text,
+ buffer_position.to_offset_utf16(&snapshot).0 as u64,
+ cx,
+ )
+ })
+ });
+
+ cx.background_executor().spawn(async move {
+ Ok(completion_task
+ .await?
+ .iter()
+ .map(|completion| project::Completion {
+ old_range: start_position..buffer_position,
+ new_text: completion.text.clone().unwrap_or(completion.label.clone()),
+ label: CodeLabel {
+ filter_range: 0..completion.label.len(),
+ text: completion.label.clone(),
+ runs: Vec::new(),
+ },
+ server_id: LanguageServerId(0), // TODO debugger: read from client
+ documentation: None,
+ lsp_completion: Default::default(),
+ confirm: None,
+ })
+ .collect())
+ })
+ }
+}
diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs
new file mode 100644
index 0000000000000..aa70e43fe5c4e
--- /dev/null
+++ b/crates/debugger_ui/src/debugger_panel.rs
@@ -0,0 +1,663 @@
+use crate::debugger_panel_item::DebugPanelItem;
+use anyhow::Result;
+use dap::client::DebugAdapterClient;
+use dap::client::{DebugAdapterClientId, ThreadStatus};
+use dap::debugger_settings::DebuggerSettings;
+use dap::messages::{Events, Message};
+use dap::requests::{Request, StartDebugging};
+use dap::{
+ Capabilities, CapabilitiesEvent, ContinuedEvent, ExitedEvent, LoadedSourceEvent, ModuleEvent,
+ OutputEvent, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason,
+};
+use gpui::{
+ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView,
+ FontWeight, Model, Subscription, Task, View, ViewContext, WeakView,
+};
+use project::dap_store::DapStore;
+use serde_json::Value;
+use settings::Settings;
+use std::collections::BTreeMap;
+use std::sync::Arc;
+use std::u64;
+use ui::prelude::*;
+use workspace::{
+ dock::{DockPosition, Panel, PanelEvent},
+ Workspace,
+};
+use workspace::{pane, Pane, Start};
+
+pub enum DebugPanelEvent {
+ Exited(DebugAdapterClientId),
+ Terminated(DebugAdapterClientId),
+ Stopped {
+ client_id: DebugAdapterClientId,
+ event: StoppedEvent,
+ go_to_stack_frame: bool,
+ },
+ Thread((DebugAdapterClientId, ThreadEvent)),
+ Continued((DebugAdapterClientId, ContinuedEvent)),
+ Output((DebugAdapterClientId, OutputEvent)),
+ Module((DebugAdapterClientId, ModuleEvent)),
+ LoadedSource((DebugAdapterClientId, LoadedSourceEvent)),
+ ClientStopped(DebugAdapterClientId),
+ CapabilitiesChanged(DebugAdapterClientId),
+}
+
+actions!(debug_panel, [ToggleFocus]);
+
+#[derive(Debug, Default, Clone)]
+pub struct ThreadState {
+ pub status: ThreadStatus,
+ // we update this value only once we stopped,
+ // we will use this to indicated if we should show a warning when debugger thread was exited
+ pub stopped: bool,
+}
+
+pub struct DebugPanel {
+ size: Pixels,
+ pane: View,
+ focus_handle: FocusHandle,
+ dap_store: Model,
+ workspace: WeakView,
+ show_did_not_stop_warning: bool,
+ _subscriptions: Vec,
+ thread_states: BTreeMap<(DebugAdapterClientId, u64), Model>,
+}
+
+impl DebugPanel {
+ pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> View {
+ cx.new_view(|cx| {
+ let pane = cx.new_view(|cx| {
+ let mut pane = Pane::new(
+ workspace.weak_handle(),
+ workspace.project().clone(),
+ Default::default(),
+ None,
+ None,
+ cx,
+ );
+ pane.set_can_split(false, cx);
+ pane.set_can_navigate(true, cx);
+ pane.display_nav_history_buttons(None);
+ pane.set_should_display_tab_bar(|_| true);
+ pane.set_close_pane_if_empty(false, cx);
+
+ pane
+ });
+
+ let project = workspace.project().clone();
+
+ let _subscriptions = vec![
+ cx.observe(&pane, |_, _, cx| cx.notify()),
+ cx.subscribe(&pane, Self::handle_pane_event),
+ cx.subscribe(&project, {
+ move |this: &mut Self, _, event, cx| match event {
+ project::Event::DebugClientEvent { message, client_id } => {
+ let Some(client) = this.debug_client_by_id(client_id, cx) else {
+ return cx.emit(DebugPanelEvent::ClientStopped(*client_id));
+ };
+
+ match message {
+ Message::Event(event) => {
+ this.handle_debug_client_events(client_id, event, cx);
+ }
+ Message::Request(request) => {
+ if StartDebugging::COMMAND == request.command {
+ Self::handle_start_debugging_request(
+ this,
+ client,
+ request.arguments.clone(),
+ cx,
+ );
+ }
+ }
+ _ => unreachable!(),
+ }
+ }
+ project::Event::DebugClientStopped(client_id) => {
+ cx.emit(DebugPanelEvent::ClientStopped(*client_id));
+
+ this.thread_states
+ .retain(|&(client_id_, _), _| client_id_ != *client_id);
+
+ cx.notify();
+ }
+ _ => {}
+ }
+ }),
+ ];
+
+ Self {
+ pane,
+ size: px(300.),
+ _subscriptions,
+ dap_store: project.read(cx).dap_store(),
+ focus_handle: cx.focus_handle(),
+ show_did_not_stop_warning: false,
+ thread_states: Default::default(),
+ workspace: workspace.weak_handle(),
+ }
+ })
+ }
+
+ pub fn load(
+ workspace: WeakView,
+ cx: AsyncWindowContext,
+ ) -> Task>> {
+ cx.spawn(|mut cx| async move {
+ workspace.update(&mut cx, |workspace, cx| DebugPanel::new(workspace, cx))
+ })
+ }
+
+ pub fn active_debug_panel_item(
+ &self,
+ cx: &mut ViewContext,
+ ) -> Option> {
+ self.pane
+ .read(cx)
+ .active_item()
+ .and_then(|panel| panel.downcast::())
+ }
+
+ fn debug_client_by_id(
+ &self,
+ client_id: &DebugAdapterClientId,
+ cx: &mut ViewContext,
+ ) -> Option> {
+ self.workspace
+ .update(cx, |this, cx| {
+ this.project()
+ .read(cx)
+ .dap_store()
+ .read(cx)
+ .client_by_id(client_id)
+ })
+ .ok()
+ .flatten()
+ }
+
+ fn handle_pane_event(
+ &mut self,
+ _: View,
+ event: &pane::Event,
+ cx: &mut ViewContext,
+ ) {
+ match event {
+ pane::Event::RemovedItem { item } => {
+ let thread_panel = item.downcast::().unwrap();
+
+ let thread_id = thread_panel.read(cx).thread_id();
+ let client_id = thread_panel.read(cx).client_id();
+
+ self.thread_states.remove(&(client_id, thread_id));
+
+ cx.notify();
+
+ self.dap_store.update(cx, |store, cx| {
+ store
+ .terminate_threads(&client_id, Some(vec![thread_id; 1]), cx)
+ .detach()
+ });
+ }
+ pane::Event::Remove { .. } => cx.emit(PanelEvent::Close),
+ pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
+ pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
+ pane::Event::AddItem { item } => {
+ self.workspace
+ .update(cx, |workspace, cx| {
+ item.added_to_pane(workspace, self.pane.clone(), cx)
+ })
+ .ok();
+ }
+ pane::Event::ActivateItem { local } => {
+ if !local {
+ return;
+ }
+
+ if let Some(active_item) = self.pane.read(cx).active_item() {
+ if let Some(debug_item) = active_item.downcast::() {
+ debug_item.update(cx, |panel, cx| {
+ panel.go_to_current_stack_frame(cx);
+ });
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+
+ fn handle_start_debugging_request(
+ this: &mut Self,
+ client: Arc,
+ request_args: Option,
+ cx: &mut ViewContext,
+ ) {
+ let start_args = if let Some(args) = request_args {
+ serde_json::from_value(args.clone()).ok()
+ } else {
+ None
+ };
+
+ this.dap_store.update(cx, |store, cx| {
+ store.start_client(client.config(), start_args, cx);
+ });
+ }
+
+ fn handle_debug_client_events(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ event: &Events,
+ cx: &mut ViewContext,
+ ) {
+ match event {
+ Events::Initialized(event) => self.handle_initialized_event(&client_id, event, cx),
+ Events::Stopped(event) => self.handle_stopped_event(&client_id, event, cx),
+ Events::Continued(event) => self.handle_continued_event(&client_id, event, cx),
+ Events::Exited(event) => self.handle_exited_event(&client_id, event, cx),
+ Events::Terminated(event) => self.handle_terminated_event(&client_id, event, cx),
+ Events::Thread(event) => self.handle_thread_event(&client_id, event, cx),
+ Events::Output(event) => self.handle_output_event(&client_id, event, cx),
+ Events::Breakpoint(_) => {}
+ Events::Module(event) => self.handle_module_event(&client_id, event, cx),
+ Events::LoadedSource(event) => self.handle_loaded_source_event(&client_id, event, cx),
+ Events::Capabilities(event) => {
+ self.handle_capabilities_changed_event(client_id, event, cx);
+ }
+ Events::Memory(_) => {}
+ Events::Process(_) => {}
+ Events::ProgressEnd(_) => {}
+ Events::ProgressStart(_) => {}
+ Events::ProgressUpdate(_) => {}
+ Events::Invalidated(_) => {}
+ Events::Other(_) => {}
+ }
+ }
+
+ fn handle_initialized_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ capabilities: &Option,
+ cx: &mut ViewContext,
+ ) {
+ if let Some(capabilities) = capabilities {
+ self.dap_store.update(cx, |store, cx| {
+ store.merge_capabilities_for_client(&client_id, capabilities, cx);
+ });
+
+ cx.emit(DebugPanelEvent::CapabilitiesChanged(*client_id));
+ }
+
+ let send_breakpoints_task = self.workspace.update(cx, |workspace, cx| {
+ workspace
+ .project()
+ .update(cx, |project, cx| project.send_breakpoints(&client_id, cx))
+ });
+
+ let configuration_done_task = self
+ .dap_store
+ .update(cx, |store, cx| store.configuration_done(&client_id, cx));
+
+ cx.background_executor()
+ .spawn(async move {
+ send_breakpoints_task?.await;
+
+ configuration_done_task.await
+ })
+ .detach_and_log_err(cx);
+ }
+
+ fn handle_continued_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ event: &ContinuedEvent,
+ cx: &mut ViewContext,
+ ) {
+ cx.emit(DebugPanelEvent::Continued((*client_id, event.clone())));
+ }
+
+ fn handle_stopped_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ event: &StoppedEvent,
+ cx: &mut ViewContext,
+ ) {
+ let Some(thread_id) = event.thread_id else {
+ return;
+ };
+
+ let Some(client_kind) = self
+ .dap_store
+ .read(cx)
+ .client_by_id(client_id)
+ .map(|c| c.config().kind)
+ else {
+ return; // this can never happen
+ };
+
+ let client_id = *client_id;
+
+ cx.spawn({
+ let event = event.clone();
+ |this, mut cx| async move {
+ let workspace = this.update(&mut cx, |this, cx| {
+ let thread_state = this
+ .thread_states
+ .entry((client_id, thread_id))
+ .or_insert(cx.new_model(|_| ThreadState::default()))
+ .clone();
+
+ thread_state.update(cx, |thread_state, cx| {
+ thread_state.stopped = true;
+ thread_state.status = ThreadStatus::Stopped;
+
+ cx.notify();
+ });
+
+ let existing_item = this
+ .pane
+ .read(cx)
+ .items()
+ .filter_map(|item| item.downcast::())
+ .any(|item| {
+ let item = item.read(cx);
+
+ item.client_id() == client_id && item.thread_id() == thread_id
+ });
+
+ if !existing_item {
+ let debug_panel = cx.view().clone();
+ this.pane.update(cx, |pane, cx| {
+ let tab = cx.new_view(|cx| {
+ DebugPanelItem::new(
+ debug_panel,
+ this.workspace.clone(),
+ this.dap_store.clone(),
+ thread_state.clone(),
+ &client_id,
+ &client_kind,
+ thread_id,
+ cx,
+ )
+ });
+
+ pane.add_item(Box::new(tab), true, true, None, cx);
+ });
+ }
+
+ let go_to_stack_frame = if let Some(item) = this.pane.read(cx).active_item() {
+ item.downcast::().map_or(false, |pane| {
+ let pane = pane.read(cx);
+ pane.thread_id() == thread_id && pane.client_id() == client_id
+ })
+ } else {
+ true
+ };
+
+ cx.emit(DebugPanelEvent::Stopped {
+ client_id,
+ event,
+ go_to_stack_frame,
+ });
+
+ cx.notify();
+
+ this.workspace.clone()
+ })?;
+
+ cx.update(|cx| {
+ workspace.update(cx, |workspace, cx| {
+ workspace.focus_panel::(cx);
+ })
+ })
+ }
+ })
+ .detach_and_log_err(cx);
+ }
+
+ fn handle_thread_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ event: &ThreadEvent,
+ cx: &mut ViewContext,
+ ) {
+ let thread_id = event.thread_id;
+
+ if let Some(thread_state) = self.thread_states.get(&(*client_id, thread_id)) {
+ if !thread_state.read(cx).stopped && event.reason == ThreadEventReason::Exited {
+ self.show_did_not_stop_warning = true;
+ cx.notify();
+ };
+ }
+
+ if event.reason == ThreadEventReason::Started {
+ self.thread_states.insert(
+ (*client_id, thread_id),
+ cx.new_model(|_| ThreadState::default()),
+ );
+ }
+
+ cx.emit(DebugPanelEvent::Thread((*client_id, event.clone())));
+ }
+
+ fn handle_exited_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ _: &ExitedEvent,
+ cx: &mut ViewContext,
+ ) {
+ cx.emit(DebugPanelEvent::Exited(*client_id));
+ }
+
+ fn handle_terminated_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ event: &Option,
+ cx: &mut ViewContext,
+ ) {
+ let restart_args = event.clone().and_then(|e| e.restart);
+
+ for (_, thread_state) in self
+ .thread_states
+ .range_mut(&(*client_id, u64::MIN)..&(*client_id, u64::MAX))
+ {
+ thread_state.update(cx, |thread_state, cx| {
+ thread_state.status = ThreadStatus::Ended;
+
+ cx.notify();
+ });
+ }
+
+ self.dap_store.update(cx, |store, cx| {
+ if restart_args.is_some() {
+ store
+ .restart(&client_id, restart_args, cx)
+ .detach_and_log_err(cx);
+ } else {
+ store.shutdown_client(&client_id, cx).detach_and_log_err(cx);
+ }
+ });
+
+ cx.emit(DebugPanelEvent::Terminated(*client_id));
+ }
+
+ fn handle_output_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ event: &OutputEvent,
+ cx: &mut ViewContext,
+ ) {
+ cx.emit(DebugPanelEvent::Output((*client_id, event.clone())));
+ }
+
+ fn handle_module_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ event: &ModuleEvent,
+ cx: &mut ViewContext,
+ ) {
+ cx.emit(DebugPanelEvent::Module((*client_id, event.clone())));
+ }
+
+ fn handle_loaded_source_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ event: &LoadedSourceEvent,
+ cx: &mut ViewContext,
+ ) {
+ cx.emit(DebugPanelEvent::LoadedSource((*client_id, event.clone())));
+ }
+
+ fn handle_capabilities_changed_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ event: &CapabilitiesEvent,
+ cx: &mut ViewContext,
+ ) {
+ self.dap_store.update(cx, |store, cx| {
+ store.merge_capabilities_for_client(client_id, &event.capabilities, cx);
+ });
+
+ cx.emit(DebugPanelEvent::CapabilitiesChanged(*client_id));
+ }
+
+ fn render_did_not_stop_warning(&self, cx: &mut ViewContext) -> impl IntoElement {
+ const TITLE: &'static str = "Debug session exited without hitting any breakpoints";
+ const DESCRIPTION: &'static str =
+ "Try adding a breakpoint, or define the correct path mapping for your debugger.";
+
+ div()
+ .absolute()
+ .right_3()
+ .bottom_12()
+ .max_w_96()
+ .py_2()
+ .px_3()
+ .elevation_2(cx)
+ .occlude()
+ .child(
+ v_flex()
+ .gap_0p5()
+ .child(
+ h_flex()
+ .gap_1p5()
+ .items_center()
+ .child(Icon::new(IconName::Warning).color(Color::Conflict))
+ .child(Label::new(TITLE).weight(FontWeight::MEDIUM)),
+ )
+ .child(
+ Label::new(DESCRIPTION)
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .child(
+ h_flex().justify_end().mt_1().child(
+ Button::new("dismiss", "Dismiss")
+ .color(Color::Muted)
+ .on_click(cx.listener(|this, _, cx| {
+ this.show_did_not_stop_warning = false;
+ cx.notify();
+ })),
+ ),
+ ),
+ )
+ }
+}
+
+impl EventEmitter for DebugPanel {}
+impl EventEmitter for DebugPanel {}
+impl EventEmitter for DebugPanel {}
+
+impl FocusableView for DebugPanel {
+ fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl Panel for DebugPanel {
+ fn pane(&self) -> Option> {
+ Some(self.pane.clone())
+ }
+
+ fn persistent_name() -> &'static str {
+ "DebugPanel"
+ }
+
+ fn position(&self, _cx: &WindowContext) -> DockPosition {
+ DockPosition::Bottom
+ }
+
+ fn position_is_valid(&self, position: DockPosition) -> bool {
+ position == DockPosition::Bottom
+ }
+
+ fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext) {}
+
+ fn size(&self, _cx: &WindowContext) -> Pixels {
+ self.size
+ }
+
+ fn set_size(&mut self, size: Option, _cx: &mut ViewContext) {
+ self.size = size.unwrap();
+ }
+
+ fn icon(&self, _cx: &WindowContext) -> Option {
+ Some(IconName::Debug)
+ }
+
+ fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str> {
+ if DebuggerSettings::get_global(cx).button {
+ Some("Debug Panel")
+ } else {
+ None
+ }
+ }
+
+ fn toggle_action(&self) -> Box {
+ Box::new(ToggleFocus)
+ }
+}
+
+impl Render for DebugPanel {
+ fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement {
+ v_flex()
+ .key_context("DebugPanel")
+ .track_focus(&self.focus_handle)
+ .size_full()
+ .when(self.show_did_not_stop_warning, |this| {
+ this.child(self.render_did_not_stop_warning(cx))
+ })
+ .map(|this| {
+ if self.pane.read(cx).items_len() == 0 {
+ this.child(
+ h_flex().size_full().items_center().justify_center().child(
+ v_flex()
+ .gap_2()
+ .rounded_md()
+ .max_w_64()
+ .items_start()
+ .child(
+ Label::new("You can create a debug task by creating a new task and setting the `type` key to `debug`")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .child(
+ h_flex().w_full().justify_end().child(
+ Button::new(
+ "start-debugger",
+ "Choose a debugger",
+ )
+ .label_size(LabelSize::Small)
+ .on_click(move |_, cx| {
+ cx.dispatch_action(Start.boxed_clone());
+ })
+ ),
+ ),
+ ),
+ )
+ } else {
+ this.child(self.pane.clone())
+ }
+ })
+ .into_any()
+ }
+}
diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs
new file mode 100644
index 0000000000000..c203817fa2fad
--- /dev/null
+++ b/crates/debugger_ui/src/debugger_panel_item.rs
@@ -0,0 +1,713 @@
+use crate::console::Console;
+use crate::debugger_panel::{DebugPanel, DebugPanelEvent, ThreadState};
+use crate::loaded_source_list::LoadedSourceList;
+use crate::module_list::ModuleList;
+use crate::stack_frame_list::{StackFrameList, StackFrameListEvent};
+use crate::variable_list::VariableList;
+
+use dap::client::{DebugAdapterClientId, ThreadStatus};
+use dap::debugger_settings::DebuggerSettings;
+use dap::{
+ Capabilities, ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, OutputEventCategory,
+ StoppedEvent, ThreadEvent,
+};
+use editor::Editor;
+use gpui::{
+ AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, Model, Subscription, View,
+ WeakView,
+};
+use project::dap_store::DapStore;
+use settings::Settings;
+use task::DebugAdapterKind;
+use ui::WindowContext;
+use ui::{prelude::*, Tooltip};
+use workspace::item::{Item, ItemEvent};
+use workspace::Workspace;
+
+#[derive(Debug)]
+pub enum DebugPanelItemEvent {
+ Close,
+ Stopped { go_to_stack_frame: bool },
+}
+
+#[derive(Clone, PartialEq, Eq)]
+enum ThreadItem {
+ Console,
+ LoadedSource,
+ Modules,
+ Output,
+ Variables,
+}
+
+pub struct DebugPanelItem {
+ thread_id: u64,
+ console: View,
+ focus_handle: FocusHandle,
+ dap_store: Model,
+ output_editor: View,
+ module_list: View,
+ client_kind: DebugAdapterKind,
+ active_thread_item: ThreadItem,
+ workspace: WeakView,
+ client_id: DebugAdapterClientId,
+ thread_state: Model,
+ variable_list: View,
+ _subscriptions: Vec,
+ stack_frame_list: View,
+ loaded_source_list: View,
+}
+
+impl DebugPanelItem {
+ #[allow(clippy::too_many_arguments)]
+ pub fn new(
+ debug_panel: View,
+ workspace: WeakView,
+ dap_store: Model,
+ thread_state: Model,
+ client_id: &DebugAdapterClientId,
+ client_kind: &DebugAdapterKind,
+ thread_id: u64,
+ cx: &mut ViewContext,
+ ) -> Self {
+ let focus_handle = cx.focus_handle();
+
+ let this = cx.view().clone();
+ let stack_frame_list = cx.new_view(|cx| {
+ StackFrameList::new(&workspace, &this, &dap_store, client_id, thread_id, cx)
+ });
+
+ let variable_list = cx
+ .new_view(|cx| VariableList::new(&stack_frame_list, dap_store.clone(), &client_id, cx));
+
+ let module_list = cx.new_view(|cx| ModuleList::new(dap_store.clone(), &client_id, cx));
+
+ let loaded_source_list =
+ cx.new_view(|cx| LoadedSourceList::new(&this, dap_store.clone(), &client_id, cx));
+
+ let console = cx.new_view(|cx| {
+ Console::new(
+ &stack_frame_list,
+ client_id,
+ variable_list.clone(),
+ dap_store.clone(),
+ cx,
+ )
+ });
+
+ let _subscriptions = vec![
+ cx.subscribe(&debug_panel, {
+ move |this: &mut Self, _, event: &DebugPanelEvent, cx| {
+ match event {
+ DebugPanelEvent::Stopped {
+ client_id,
+ event,
+ go_to_stack_frame,
+ } => this.handle_stopped_event(client_id, event, *go_to_stack_frame, cx),
+ DebugPanelEvent::Thread((client_id, event)) => {
+ this.handle_thread_event(client_id, event, cx)
+ }
+ DebugPanelEvent::Output((client_id, event)) => {
+ this.handle_output_event(client_id, event, cx)
+ }
+ DebugPanelEvent::Module((client_id, event)) => {
+ this.handle_module_event(client_id, event, cx)
+ }
+ DebugPanelEvent::LoadedSource((client_id, event)) => {
+ this.handle_loaded_source_event(client_id, event, cx)
+ }
+ DebugPanelEvent::ClientStopped(client_id) => {
+ this.handle_client_stopped_event(client_id, cx)
+ }
+ DebugPanelEvent::Continued((client_id, event)) => {
+ this.handle_thread_continued_event(client_id, event, cx);
+ }
+ DebugPanelEvent::Exited(client_id)
+ | DebugPanelEvent::Terminated(client_id) => {
+ this.handle_client_exited_and_terminated_event(client_id, cx);
+ }
+ DebugPanelEvent::CapabilitiesChanged(client_id) => {
+ this.handle_capabilities_changed_event(client_id, cx);
+ }
+ };
+ }
+ }),
+ cx.subscribe(
+ &stack_frame_list,
+ move |this: &mut Self, _, event: &StackFrameListEvent, cx| match event {
+ StackFrameListEvent::ChangedStackFrame => this.clear_highlights(cx),
+ _ => {}
+ },
+ ),
+ ];
+
+ let output_editor = cx.new_view(|cx| {
+ let mut editor = Editor::multi_line(cx);
+ editor.set_placeholder_text("Debug adapter and script output", cx);
+ editor.set_read_only(true);
+ editor.set_show_inline_completions(Some(false), cx);
+ editor.set_searchable(false);
+ editor.set_auto_replace_emoji_shortcode(false);
+ editor.set_show_indent_guides(false, cx);
+ editor.set_autoindent(false);
+ editor.set_show_gutter(false, cx);
+ editor.set_show_line_numbers(false, cx);
+ editor
+ });
+
+ Self {
+ console,
+ thread_id,
+ dap_store,
+ workspace,
+ module_list,
+ thread_state,
+ focus_handle,
+ output_editor,
+ variable_list,
+ _subscriptions,
+ stack_frame_list,
+ loaded_source_list,
+ client_id: *client_id,
+ client_kind: client_kind.clone(),
+ active_thread_item: ThreadItem::Variables,
+ }
+ }
+
+ pub fn update_thread_state_status(&mut self, status: ThreadStatus, cx: &mut ViewContext) {
+ self.thread_state.update(cx, |thread_state, cx| {
+ thread_state.status = status;
+
+ cx.notify();
+ });
+
+ if status == ThreadStatus::Exited
+ || status == ThreadStatus::Ended
+ || status == ThreadStatus::Stopped
+ {
+ self.clear_highlights(cx);
+ }
+ }
+
+ fn should_skip_event(&self, client_id: &DebugAdapterClientId, thread_id: u64) -> bool {
+ thread_id != self.thread_id || *client_id != self.client_id
+ }
+
+ fn handle_thread_continued_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ event: &ContinuedEvent,
+ cx: &mut ViewContext,
+ ) {
+ if self.should_skip_event(client_id, event.thread_id) {
+ return;
+ }
+
+ self.update_thread_state_status(ThreadStatus::Running, cx);
+ }
+
+ fn handle_stopped_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ event: &StoppedEvent,
+ go_to_stack_frame: bool,
+ cx: &mut ViewContext,
+ ) {
+ if self.should_skip_event(client_id, event.thread_id.unwrap_or(self.thread_id)) {
+ return;
+ }
+
+ cx.emit(DebugPanelItemEvent::Stopped { go_to_stack_frame });
+ }
+
+ fn handle_thread_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ event: &ThreadEvent,
+ cx: &mut ViewContext,
+ ) {
+ if self.should_skip_event(client_id, event.thread_id) {
+ return;
+ }
+
+ self.update_thread_state_status(ThreadStatus::Running, cx);
+ }
+
+ fn handle_output_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ event: &OutputEvent,
+ cx: &mut ViewContext,
+ ) {
+ if self.should_skip_event(client_id, self.thread_id) {
+ return;
+ }
+
+ // The default value of an event category is console
+ // so we assume that is the output type if it doesn't exist
+ let output_category = event
+ .category
+ .as_ref()
+ .unwrap_or(&OutputEventCategory::Console);
+
+ match output_category {
+ OutputEventCategory::Console => {
+ self.console.update(cx, |console, cx| {
+ console.add_message(&event.output, cx);
+ });
+ }
+ _ => {
+ self.output_editor.update(cx, |editor, cx| {
+ editor.set_read_only(false);
+ editor.move_to_end(&editor::actions::MoveToEnd, cx);
+ editor.insert(format!("{}\n", &event.output.trim_end()).as_str(), cx);
+ editor.set_read_only(true);
+
+ cx.notify();
+ });
+ }
+ }
+ }
+
+ fn handle_module_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ event: &ModuleEvent,
+ cx: &mut ViewContext,
+ ) {
+ if self.should_skip_event(client_id, self.thread_id) {
+ return;
+ }
+
+ self.module_list.update(cx, |variable_list, cx| {
+ variable_list.on_module_event(event, cx);
+ });
+ }
+
+ fn handle_loaded_source_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ event: &LoadedSourceEvent,
+ cx: &mut ViewContext,
+ ) {
+ if self.should_skip_event(client_id, self.thread_id) {
+ return;
+ }
+
+ self.loaded_source_list
+ .update(cx, |loaded_source_list, cx| {
+ loaded_source_list.on_loaded_source_event(event, cx);
+ });
+ }
+
+ fn handle_client_stopped_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ cx: &mut ViewContext,
+ ) {
+ if self.should_skip_event(client_id, self.thread_id) {
+ return;
+ }
+
+ self.update_thread_state_status(ThreadStatus::Stopped, cx);
+
+ cx.emit(DebugPanelItemEvent::Close);
+ }
+
+ fn handle_client_exited_and_terminated_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ cx: &mut ViewContext,
+ ) {
+ if Self::should_skip_event(self, client_id, self.thread_id) {
+ return;
+ }
+
+ self.update_thread_state_status(ThreadStatus::Exited, cx);
+
+ cx.emit(DebugPanelItemEvent::Close);
+ }
+
+ fn handle_capabilities_changed_event(
+ &mut self,
+ client_id: &DebugAdapterClientId,
+ cx: &mut ViewContext,
+ ) {
+ if Self::should_skip_event(self, client_id, self.thread_id) {
+ return;
+ }
+
+ cx.notify();
+ }
+
+ pub fn client_id(&self) -> DebugAdapterClientId {
+ self.client_id
+ }
+
+ pub fn thread_id(&self) -> u64 {
+ self.thread_id
+ }
+
+ pub fn capabilities(&self, cx: &mut ViewContext) -> Capabilities {
+ self.dap_store
+ .read_with(cx, |store, _| store.capabilities_by_id(&self.client_id))
+ }
+
+ fn clear_highlights(&self, cx: &mut ViewContext) {
+ self.workspace
+ .update(cx, |workspace, cx| {
+ let editor_views = workspace
+ .items_of_type::(cx)
+ .collect::>>();
+
+ for editor_view in editor_views {
+ editor_view.update(cx, |editor, _| {
+ editor.clear_row_highlights::();
+ });
+ }
+ })
+ .ok();
+ }
+
+ pub fn go_to_current_stack_frame(&self, cx: &mut ViewContext) {
+ self.stack_frame_list.update(cx, |stack_frame_list, cx| {
+ stack_frame_list
+ .go_to_stack_frame(cx)
+ .detach_and_log_err(cx);
+ });
+ }
+
+ fn render_entry_button(
+ &self,
+ label: &SharedString,
+ thread_item: ThreadItem,
+ cx: &mut ViewContext,
+ ) -> AnyElement {
+ div()
+ .id(label.clone())
+ .px_2()
+ .py_1()
+ .cursor_pointer()
+ .border_b_2()
+ .when(self.active_thread_item == thread_item, |this| {
+ this.border_color(cx.theme().colors().border)
+ })
+ .child(Button::new(label.clone(), label.clone()))
+ .on_click(cx.listener(move |this, _, cx| {
+ this.active_thread_item = thread_item.clone();
+
+ cx.notify();
+ }))
+ .into_any_element()
+ }
+
+ pub fn continue_thread(&mut self, cx: &mut ViewContext) {
+ self.update_thread_state_status(ThreadStatus::Running, cx);
+
+ self.dap_store.update(cx, |store, cx| {
+ store
+ .continue_thread(&self.client_id, self.thread_id, cx)
+ .detach_and_log_err(cx);
+ });
+ }
+
+ pub fn step_over(&mut self, cx: &mut ViewContext) {
+ self.update_thread_state_status(ThreadStatus::Running, cx);
+
+ let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
+
+ self.dap_store.update(cx, |store, cx| {
+ store
+ .step_over(&self.client_id, self.thread_id, granularity, cx)
+ .detach_and_log_err(cx);
+ });
+ }
+
+ pub fn step_in(&mut self, cx: &mut ViewContext) {
+ self.update_thread_state_status(ThreadStatus::Running, cx);
+
+ let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
+
+ self.dap_store.update(cx, |store, cx| {
+ store
+ .step_in(&self.client_id, self.thread_id, granularity, cx)
+ .detach_and_log_err(cx);
+ });
+ }
+
+ pub fn step_out(&mut self, cx: &mut ViewContext) {
+ self.update_thread_state_status(ThreadStatus::Running, cx);
+
+ let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
+
+ self.dap_store.update(cx, |store, cx| {
+ store
+ .step_out(&self.client_id, self.thread_id, granularity, cx)
+ .detach_and_log_err(cx);
+ });
+ }
+
+ pub fn restart_client(&self, cx: &mut ViewContext) {
+ self.dap_store.update(cx, |store, cx| {
+ store
+ .restart(&self.client_id, None, cx)
+ .detach_and_log_err(cx);
+ });
+ }
+
+ pub fn pause_thread(&self, cx: &mut ViewContext) {
+ self.dap_store.update(cx, |store, cx| {
+ store
+ .pause_thread(&self.client_id, self.thread_id, cx)
+ .detach_and_log_err(cx)
+ });
+ }
+
+ pub fn stop_thread(&self, cx: &mut ViewContext) {
+ self.dap_store.update(cx, |store, cx| {
+ store
+ .terminate_threads(&self.client_id, Some(vec![self.thread_id; 1]), cx)
+ .detach_and_log_err(cx)
+ });
+ }
+
+ pub fn disconnect_client(&self, cx: &mut ViewContext) {
+ self.dap_store.update(cx, |store, cx| {
+ store
+ .disconnect_client(&self.client_id, cx)
+ .detach_and_log_err(cx);
+ });
+ }
+}
+
+impl EventEmitter for DebugPanelItem {}
+
+impl FocusableView for DebugPanelItem {
+ fn focus_handle(&self, _: &AppContext) -> FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl Item for DebugPanelItem {
+ type Event = DebugPanelItemEvent;
+
+ fn tab_content(
+ &self,
+ params: workspace::item::TabContentParams,
+ _: &WindowContext,
+ ) -> AnyElement {
+ Label::new(format!(
+ "{:?} - Thread {}",
+ self.client_kind, self.thread_id
+ ))
+ .color(if params.selected {
+ Color::Default
+ } else {
+ Color::Muted
+ })
+ .into_any_element()
+ }
+
+ fn tab_tooltip_text(&self, cx: &AppContext) -> Option {
+ Some(SharedString::from(format!(
+ "{:?} Thread {} - {:?}",
+ self.client_kind,
+ self.thread_id,
+ self.thread_state.read(cx).status,
+ )))
+ }
+
+ fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
+ match event {
+ DebugPanelItemEvent::Close => f(ItemEvent::CloseItem),
+ DebugPanelItemEvent::Stopped { .. } => {}
+ }
+ }
+}
+
+impl Render for DebugPanelItem {
+ fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement {
+ let thread_status = self.thread_state.read(cx).status;
+ let active_thread_item = &self.active_thread_item;
+
+ let capabilities = self.capabilities(cx);
+
+ h_flex()
+ .key_context("DebugPanelItem")
+ .track_focus(&self.focus_handle)
+ .size_full()
+ .items_start()
+ .child(
+ v_flex()
+ .size_full()
+ .items_start()
+ .child(
+ h_flex()
+ .p_1()
+ .border_b_1()
+ .w_full()
+ .border_color(cx.theme().colors().border_variant)
+ .gap_2()
+ .map(|this| {
+ if thread_status == ThreadStatus::Running {
+ this.child(
+ IconButton::new("debug-pause", IconName::DebugPause)
+ .icon_size(IconSize::Small)
+ .on_click(cx.listener(|this, _, cx| {
+ this.pause_thread(cx);
+ }))
+ .tooltip(move |cx| Tooltip::text("Pause program", cx)),
+ )
+ } else {
+ this.child(
+ IconButton::new("debug-continue", IconName::DebugContinue)
+ .icon_size(IconSize::Small)
+ .on_click(
+ cx.listener(|this, _, cx| this.continue_thread(cx)),
+ )
+ .disabled(thread_status != ThreadStatus::Stopped)
+ .tooltip(move |cx| {
+ Tooltip::text("Continue program", cx)
+ }),
+ )
+ }
+ })
+ .child(
+ IconButton::new("debug-step-over", IconName::DebugStepOver)
+ .icon_size(IconSize::Small)
+ .on_click(cx.listener(|this, _, cx| {
+ this.step_over(cx);
+ }))
+ .disabled(thread_status != ThreadStatus::Stopped)
+ .tooltip(move |cx| Tooltip::text("Step over", cx)),
+ )
+ .child(
+ IconButton::new("debug-step-in", IconName::DebugStepInto)
+ .icon_size(IconSize::Small)
+ .on_click(cx.listener(|this, _, cx| {
+ this.step_in(cx);
+ }))
+ .disabled(thread_status != ThreadStatus::Stopped)
+ .tooltip(move |cx| Tooltip::text("Step in", cx)),
+ )
+ .child(
+ IconButton::new("debug-step-out", IconName::DebugStepOut)
+ .icon_size(IconSize::Small)
+ .on_click(cx.listener(|this, _, cx| {
+ this.step_out(cx);
+ }))
+ .disabled(thread_status != ThreadStatus::Stopped)
+ .tooltip(move |cx| Tooltip::text("Step out", cx)),
+ )
+ .child(
+ IconButton::new("debug-restart", IconName::DebugRestart)
+ .icon_size(IconSize::Small)
+ .on_click(cx.listener(|this, _, cx| {
+ this.restart_client(cx);
+ }))
+ .disabled(
+ !capabilities.supports_restart_request.unwrap_or_default(),
+ )
+ .tooltip(move |cx| Tooltip::text("Restart", cx)),
+ )
+ .child(
+ IconButton::new("debug-stop", IconName::DebugStop)
+ .icon_size(IconSize::Small)
+ .on_click(cx.listener(|this, _, cx| {
+ this.stop_thread(cx);
+ }))
+ .disabled(
+ thread_status != ThreadStatus::Stopped
+ && thread_status != ThreadStatus::Running,
+ )
+ .tooltip(move |cx| Tooltip::text("Stop", cx)),
+ )
+ .child(
+ IconButton::new("debug-disconnect", IconName::DebugDisconnect)
+ .icon_size(IconSize::Small)
+ .on_click(cx.listener(|this, _, cx| {
+ this.disconnect_client(cx);
+ }))
+ .disabled(
+ thread_status == ThreadStatus::Exited
+ || thread_status == ThreadStatus::Ended,
+ )
+ .tooltip(move |cx| Tooltip::text("Disconnect", cx)),
+ ),
+ )
+ .child(
+ h_flex()
+ .size_full()
+ .items_start()
+ .p_1()
+ .gap_4()
+ .child(self.stack_frame_list.clone()),
+ ),
+ )
+ .child(
+ v_flex()
+ .border_l_1()
+ .border_color(cx.theme().colors().border_variant)
+ .size_full()
+ .items_start()
+ .child(
+ h_flex()
+ .border_b_1()
+ .w_full()
+ .border_color(cx.theme().colors().border_variant)
+ .child(self.render_entry_button(
+ &SharedString::from("Variables"),
+ ThreadItem::Variables,
+ cx,
+ ))
+ .when(
+ capabilities.supports_modules_request.unwrap_or_default(),
+ |this| {
+ this.child(self.render_entry_button(
+ &SharedString::from("Modules"),
+ ThreadItem::Modules,
+ cx,
+ ))
+ },
+ )
+ .when(
+ capabilities
+ .supports_loaded_sources_request
+ .unwrap_or_default(),
+ |this| {
+ this.child(self.render_entry_button(
+ &SharedString::from("Loaded Sources"),
+ ThreadItem::LoadedSource,
+ cx,
+ ))
+ },
+ )
+ .child(self.render_entry_button(
+ &SharedString::from("Console"),
+ ThreadItem::Console,
+ cx,
+ ))
+ .child(self.render_entry_button(
+ &SharedString::from("Output"),
+ ThreadItem::Output,
+ cx,
+ )),
+ )
+ .when(*active_thread_item == ThreadItem::Variables, |this| {
+ this.size_full().child(self.variable_list.clone())
+ })
+ .when(*active_thread_item == ThreadItem::Modules, |this| {
+ this.size_full().child(self.module_list.clone())
+ })
+ .when(*active_thread_item == ThreadItem::LoadedSource, |this| {
+ this.size_full().child(self.loaded_source_list.clone())
+ })
+ .when(*active_thread_item == ThreadItem::Output, |this| {
+ this.child(self.output_editor.clone())
+ })
+ .when(*active_thread_item == ThreadItem::Console, |this| {
+ this.child(self.console.clone())
+ }),
+ )
+ .into_any()
+ }
+}
diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs
new file mode 100644
index 0000000000000..d7e3e59b4f8b0
--- /dev/null
+++ b/crates/debugger_ui/src/lib.rs
@@ -0,0 +1,118 @@
+use dap::debugger_settings::DebuggerSettings;
+use debugger_panel::{DebugPanel, ToggleFocus};
+use gpui::AppContext;
+use settings::Settings;
+use ui::ViewContext;
+use workspace::{
+ Continue, Pause, Restart, Start, StepInto, StepOut, StepOver, Stop, StopDebugAdapters,
+ Workspace,
+};
+
+mod console;
+pub mod debugger_panel;
+mod debugger_panel_item;
+mod loaded_source_list;
+mod module_list;
+mod stack_frame_list;
+mod variable_list;
+
+pub fn init(cx: &mut AppContext) {
+ DebuggerSettings::register(cx);
+
+ cx.observe_new_views(
+ |workspace: &mut Workspace, _cx: &mut ViewContext| {
+ workspace
+ .register_action(|workspace, _: &ToggleFocus, cx| {
+ workspace.toggle_panel_focus::(cx);
+ })
+ .register_action(|workspace: &mut Workspace, _: &Start, cx| {
+ tasks_ui::toggle_modal(workspace, task::TaskModal::DebugModal, cx).detach();
+ })
+ .register_action(|workspace: &mut Workspace, _: &StopDebugAdapters, cx| {
+ workspace.project().update(cx, |project, cx| {
+ project.dap_store().update(cx, |store, cx| {
+ store.shutdown_clients(cx).detach();
+ })
+ })
+ })
+ .register_action(|workspace: &mut Workspace, _: &Stop, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+
+ debug_panel.update(cx, |panel, cx| {
+ let Some(active_item) = panel.active_debug_panel_item(cx) else {
+ return;
+ };
+
+ active_item.update(cx, |item, cx| item.stop_thread(cx))
+ });
+ })
+ .register_action(|workspace: &mut Workspace, _: &Continue, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+
+ debug_panel.update(cx, |panel, cx| {
+ let Some(active_item) = panel.active_debug_panel_item(cx) else {
+ return;
+ };
+
+ active_item.update(cx, |item, cx| item.continue_thread(cx))
+ });
+ })
+ .register_action(|workspace: &mut Workspace, _: &StepInto, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+
+ debug_panel.update(cx, |panel, cx| {
+ let Some(active_item) = panel.active_debug_panel_item(cx) else {
+ return;
+ };
+
+ active_item.update(cx, |item, cx| item.step_in(cx))
+ });
+ })
+ .register_action(|workspace: &mut Workspace, _: &StepOut, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+
+ debug_panel.update(cx, |panel, cx| {
+ let Some(active_item) = panel.active_debug_panel_item(cx) else {
+ return;
+ };
+
+ active_item.update(cx, |item, cx| item.step_out(cx))
+ });
+ })
+ .register_action(|workspace: &mut Workspace, _: &StepOver, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+
+ debug_panel.update(cx, |panel, cx| {
+ let Some(active_item) = panel.active_debug_panel_item(cx) else {
+ return;
+ };
+
+ active_item.update(cx, |item, cx| item.step_over(cx))
+ });
+ })
+ .register_action(|workspace: &mut Workspace, _: &Restart, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+
+ debug_panel.update(cx, |panel, cx| {
+ let Some(active_item) = panel.active_debug_panel_item(cx) else {
+ return;
+ };
+
+ active_item.update(cx, |item, cx| item.restart_client(cx))
+ });
+ })
+ .register_action(|workspace: &mut Workspace, _: &Pause, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+
+ debug_panel.update(cx, |panel, cx| {
+ let Some(active_item) = panel.active_debug_panel_item(cx) else {
+ return;
+ };
+
+ active_item.update(cx, |item, cx| item.pause_thread(cx))
+ });
+ });
+ },
+ )
+ .detach();
+}
diff --git a/crates/debugger_ui/src/loaded_source_list.rs b/crates/debugger_ui/src/loaded_source_list.rs
new file mode 100644
index 0000000000000..761a33be05e5d
--- /dev/null
+++ b/crates/debugger_ui/src/loaded_source_list.rs
@@ -0,0 +1,155 @@
+use anyhow::Result;
+use dap::{client::DebugAdapterClientId, LoadedSourceEvent, Source};
+use gpui::{
+ list, AnyElement, FocusHandle, FocusableView, ListState, Model, Subscription, Task, View,
+};
+use project::dap_store::DapStore;
+use ui::prelude::*;
+
+use crate::debugger_panel_item::{self, DebugPanelItem, DebugPanelItemEvent};
+
+pub struct LoadedSourceList {
+ list: ListState,
+ sources: Vec