Debugger implementation (#13433)

###  DISCLAIMER

> As of 6th March 2025, debugger is still in development. We plan to
merge it behind a staff-only feature flag for staff use only, followed
by non-public release and then finally a public one (akin to how Git
panel release was handled). This is done to ensure the best experience
when it gets released.

### END OF DISCLAIMER 

**The current state of the debugger implementation:**


https://github.com/user-attachments/assets/c4deff07-80dd-4dc6-ad2e-0c252a478fe9


https://github.com/user-attachments/assets/e1ed2345-b750-4bb6-9c97-50961b76904f

----

All the todo's are in the following channel, so it's easier to work on
this together:
https://zed.dev/channel/zed-debugger-11370

If you are on Linux, you can use the following command to join the
channel:
```cli
zed https://zed.dev/channel/zed-debugger-11370 
```

## Current Features

- Collab
  - Breakpoints
    - Sync when you (re)join a project
    - Sync when you add/remove a breakpoint
  - Sync active debug line
  - Stack frames
    - Click on stack frame
      - View variables that belong to the stack frame
      - Visit the source file
    - Restart stack frame (if adapter supports this)
  - Variables
  - Loaded sources
  - Modules
  - Controls
    - Continue
    - Step back
      - Stepping granularity (configurable)
    - Step into
      - Stepping granularity (configurable)
    - Step over
      - Stepping granularity (configurable)
    - Step out
      - Stepping granularity (configurable)
  - Debug console
- Breakpoints
  - Log breakpoints
  - line breakpoints
  - Persistent between zed sessions (configurable)
  - Multi buffer support
  - Toggle disable/enable all breakpoints
- Stack frames
  - Click on stack frame
    - View variables that belong to the stack frame
    - Visit the source file
    - Show collapsed stack frames
  - Restart stack frame (if adapter supports this)
- Loaded sources
  - View all used loaded sources if supported by adapter.
- Modules
  - View all used modules (if adapter supports this)
- Variables
  - Copy value
  - Copy name
  - Copy memory reference
  - Set value (if adapter supports this)
  - keyboard navigation
- Debug Console
  - See logs
  - View output that was sent from debug adapter
    - Output grouping
  - Evaluate code
    - Updates the variable list
    - Auto completion
- If not supported by adapter, we will show auto-completion for existing
variables
- Debug Terminal
- Run custom commands and change env values right inside your Zed
terminal
- Attach to process (if adapter supports this)
  - Process picker
- Controls
  - Continue
  - Step back
    - Stepping granularity (configurable)
  - Step into
    - Stepping granularity (configurable)
  - Step over
    - Stepping granularity (configurable)
  - Step out
    - Stepping granularity (configurable)
  - Disconnect
  - Restart
  - Stop
- Warning when a debug session exited without hitting any breakpoint
- Debug view to see Adapter/RPC log messages
- Testing
  - Fake debug adapter
    - Fake requests & events

---

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com>
Co-authored-by: Piotr <piotr@zed.dev>
This commit is contained in:
Remco Smits 2025-03-18 17:55:25 +01:00 committed by GitHub
parent ed4e654fdf
commit 41a60ffecf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
156 changed files with 25840 additions and 451 deletions

View file

@ -2,6 +2,7 @@ pub mod buffer_store;
mod color_extractor;
pub mod connection_manager;
pub mod debounced_delay;
pub mod debugger;
pub mod git;
pub mod image_store;
pub mod lsp_command;
@ -28,14 +29,23 @@ pub mod search_history;
mod yarn;
use crate::git::GitStore;
use anyhow::{anyhow, Context as _, Result};
use buffer_store::{BufferStore, BufferStoreEvent};
use client::{
proto, Client, Collaborator, PendingEntitySubscription, ProjectId, TypedEnvelope, UserStore,
};
use clock::ReplicaId;
use dap::{client::DebugAdapterClient, DebugAdapterConfig};
use collections::{BTreeSet, HashMap, HashSet};
use debounced_delay::DebouncedDelay;
use debugger::{
breakpoint_store::BreakpointStore,
dap_store::{DapStore, DapStoreEvent},
session::Session,
};
pub use environment::ProjectEnvironment;
use futures::{
channel::mpsc::{self, UnboundedReceiver},
@ -47,8 +57,8 @@ use image_store::{ImageItemEvent, ImageStoreEvent};
use ::git::{blame::Blame, repository::GitRepository, status::FileStatus};
use gpui::{
AnyEntity, App, AppContext as _, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter,
Hsla, SharedString, Task, WeakEntity, Window,
AnyEntity, App, AppContext, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Hsla,
SharedString, Task, WeakEntity, Window,
};
use itertools::Itertools;
use language::{
@ -86,11 +96,13 @@ use std::{
sync::Arc,
time::Duration,
};
use task_store::TaskStore;
use terminals::Terminals;
use text::{Anchor, BufferId};
use toolchain_store::EmptyToolchainStore;
use util::{
maybe,
paths::{compare_paths, SanitizedPath},
ResultExt as _,
};
@ -149,6 +161,8 @@ pub struct Project {
active_entry: Option<ProjectEntryId>,
buffer_ordered_messages_tx: mpsc::UnboundedSender<BufferOrderedMessage>,
languages: Arc<LanguageRegistry>,
dap_store: Entity<DapStore>,
breakpoint_store: Entity<BreakpointStore>,
client: Arc<client::Client>,
join_project_response_message_id: u32,
task_store: Entity<TaskStore>,
@ -286,6 +300,11 @@ pub enum Event {
ExpandedAllForEntry(WorktreeId, ProjectEntryId),
}
pub enum DebugAdapterClientState {
Starting(Task<Option<Arc<DebugAdapterClient>>>),
Running(Arc<DebugAdapterClient>),
}
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub struct ProjectPath {
pub worktree_id: WorktreeId,
@ -669,6 +688,7 @@ enum EntitySubscription {
WorktreeStore(PendingEntitySubscription<WorktreeStore>),
LspStore(PendingEntitySubscription<LspStore>),
SettingsObserver(PendingEntitySubscription<SettingsObserver>),
DapStore(PendingEntitySubscription<DapStore>),
}
#[derive(Debug, Clone)]
@ -775,6 +795,8 @@ impl Project {
SettingsObserver::init(&client);
TaskStore::init(Some(&client));
ToolchainStore::init(&client);
DapStore::init(&client);
BreakpointStore::init(&client);
}
pub fn local(
@ -795,10 +817,38 @@ impl Project {
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
.detach();
let environment = ProjectEnvironment::new(&worktree_store, env, cx);
let toolchain_store = cx.new(|cx| {
ToolchainStore::local(
languages.clone(),
worktree_store.clone(),
environment.clone(),
cx,
)
});
let buffer_store = cx.new(|cx| BufferStore::local(worktree_store.clone(), cx));
cx.subscribe(&buffer_store, Self::on_buffer_store_event)
.detach();
let breakpoint_store =
cx.new(|_| BreakpointStore::local(worktree_store.clone(), buffer_store.clone()));
let dap_store = cx.new(|cx| {
DapStore::new_local(
client.http_client(),
node.clone(),
fs.clone(),
languages.clone(),
environment.clone(),
toolchain_store.read(cx).as_language_toolchain_store(),
breakpoint_store.clone(),
worktree_store.clone(),
cx,
)
});
cx.subscribe(&dap_store, Self::on_dap_store_event).detach();
let image_store = cx.new(|cx| ImageStore::local(worktree_store.clone(), cx));
cx.subscribe(&image_store, Self::on_image_store_event)
.detach();
@ -813,15 +863,6 @@ impl Project {
)
});
let environment = ProjectEnvironment::new(&worktree_store, env, cx);
let toolchain_store = cx.new(|cx| {
ToolchainStore::local(
languages.clone(),
worktree_store.clone(),
environment.clone(),
cx,
)
});
let task_store = cx.new(|cx| {
TaskStore::local(
fs.clone(),
@ -891,6 +932,8 @@ impl Project {
settings_observer,
fs,
ssh_client: None,
breakpoint_store,
dap_store,
buffers_needing_diff: Default::default(),
git_diff_debouncer: DebouncedDelay::new(),
terminals: Terminals {
@ -986,6 +1029,17 @@ impl Project {
});
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
let breakpoint_store =
cx.new(|_| BreakpointStore::remote(SSH_PROJECT_ID, client.clone().into()));
let dap_store = cx.new(|_| {
DapStore::new_remote(
SSH_PROJECT_ID,
client.clone().into(),
breakpoint_store.clone(),
)
});
let git_store = cx.new(|cx| {
GitStore::ssh(
&worktree_store,
@ -1005,6 +1059,8 @@ impl Project {
buffer_store,
image_store,
lsp_store,
breakpoint_store,
dap_store,
join_project_response_message_id: 0,
client_state: ProjectClientState::Local,
git_store,
@ -1056,6 +1112,7 @@ impl Project {
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store);
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store);
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.lsp_store);
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.dap_store);
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer);
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.git_store);
@ -1071,6 +1128,7 @@ impl Project {
SettingsObserver::init(&ssh_proto);
TaskStore::init(Some(&ssh_proto));
ToolchainStore::init(&ssh_proto);
DapStore::init(&ssh_proto);
GitStore::init(&ssh_proto);
this
@ -1116,6 +1174,7 @@ impl Project {
EntitySubscription::SettingsObserver(
client.subscribe_to_entity::<SettingsObserver>(remote_id)?,
),
EntitySubscription::DapStore(client.subscribe_to_entity::<DapStore>(remote_id)?),
];
let response = client
.request_envelope(proto::JoinProject {
@ -1137,7 +1196,7 @@ impl Project {
async fn from_join_project_response(
response: TypedEnvelope<proto::JoinProjectResponse>,
subscriptions: [EntitySubscription; 6],
subscriptions: [EntitySubscription; 7],
client: Arc<Client>,
run_tasks: bool,
user_store: Entity<UserStore>,
@ -1158,6 +1217,15 @@ impl Project {
ImageStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx)
})?;
let environment = cx.update(|cx| ProjectEnvironment::new(&worktree_store, None, cx))?;
let breakpoint_store =
cx.new(|_| BreakpointStore::remote(remote_id, client.clone().into()))?;
let dap_store = cx.new(|_cx| {
DapStore::new_remote(remote_id, client.clone().into(), breakpoint_store.clone())
})?;
let lsp_store = cx.new(|cx| {
let mut lsp_store = LspStore::new_remote(
buffer_store.clone(),
@ -1229,6 +1297,8 @@ impl Project {
cx.subscribe(&settings_observer, Self::on_settings_observer_event)
.detach();
cx.subscribe(&dap_store, Self::on_dap_store_event).detach();
let mut this = Self {
buffer_ordered_messages_tx: tx,
buffer_store: buffer_store.clone(),
@ -1254,6 +1324,8 @@ impl Project {
remote_id,
replica_id,
},
breakpoint_store,
dap_store: dap_store.clone(),
git_store: git_store.clone(),
buffers_needing_diff: Default::default(),
git_diff_debouncer: DebouncedDelay::new(),
@ -1264,7 +1336,7 @@ impl Project {
search_history: Self::new_search_history(),
search_included_history: Self::new_search_history(),
search_excluded_history: Self::new_search_history(),
environment: ProjectEnvironment::new(&worktree_store, None, cx),
environment,
remotely_created_models: Arc::new(Mutex::new(RemotelyCreatedModels::default())),
toolchain_store: None,
};
@ -1296,6 +1368,9 @@ impl Project {
EntitySubscription::LspStore(subscription) => {
subscription.set_entity(&lsp_store, &mut cx)
}
EntitySubscription::DapStore(subscription) => {
subscription.set_entity(&dap_store, &mut cx)
}
})
.collect::<Vec<_>>();
@ -1353,6 +1428,30 @@ impl Project {
}
}
pub fn start_debug_session(
&mut self,
config: DebugAdapterConfig,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Session>>> {
let worktree = maybe!({
if let Some(cwd) = &config.cwd {
Some(self.find_worktree(cwd.as_path(), cx)?.0)
} else {
self.worktrees(cx).next()
}
});
let Some(worktree) = &worktree else {
return Task::ready(Err(anyhow!("Failed to find a worktree")));
};
self.dap_store
.update(cx, |dap_store, cx| {
dap_store.new_session(config, worktree, None, cx)
})
.1
}
#[cfg(any(test, feature = "test-support"))]
pub async fn example(
root_paths: impl IntoIterator<Item = &Path>,
@ -1434,6 +1533,14 @@ impl Project {
project
}
pub fn dap_store(&self) -> Entity<DapStore> {
self.dap_store.clone()
}
pub fn breakpoint_store(&self) -> Entity<BreakpointStore> {
self.breakpoint_store.clone()
}
pub fn lsp_store(&self) -> Entity<LspStore> {
self.lsp_store.clone()
}
@ -1857,6 +1964,12 @@ impl Project {
self.client
.subscribe_to_entity(project_id)?
.set_entity(&self.settings_observer, &mut cx.to_async()),
self.client
.subscribe_to_entity(project_id)?
.set_entity(&self.dap_store, &mut cx.to_async()),
self.client
.subscribe_to_entity(project_id)?
.set_entity(&self.breakpoint_store, &mut cx.to_async()),
self.client
.subscribe_to_entity(project_id)?
.set_entity(&self.git_store, &mut cx.to_async()),
@ -1871,6 +1984,12 @@ impl Project {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.shared(project_id, self.client.clone().into(), cx)
});
self.breakpoint_store.update(cx, |breakpoint_store, _| {
breakpoint_store.shared(project_id, self.client.clone().into())
});
self.dap_store.update(cx, |dap_store, cx| {
dap_store.shared(project_id, self.client.clone().into(), cx);
});
self.task_store.update(cx, |task_store, cx| {
task_store.shared(project_id, self.client.clone().into(), cx);
});
@ -1958,6 +2077,12 @@ impl Project {
self.task_store.update(cx, |task_store, cx| {
task_store.unshared(cx);
});
self.breakpoint_store.update(cx, |breakpoint_store, cx| {
breakpoint_store.unshared(cx);
});
self.dap_store.update(cx, |dap_store, cx| {
dap_store.unshared(cx);
});
self.settings_observer.update(cx, |settings_observer, cx| {
settings_observer.unshared(cx);
});
@ -2105,7 +2230,7 @@ impl Project {
cx: &mut Context<Self>,
) -> Task<Result<(Option<ProjectEntryId>, AnyEntity)>> {
let task = self.open_buffer(path.clone(), cx);
cx.spawn(move |_, cx| async move {
cx.spawn(move |_project, cx| async move {
let buffer = task.await?;
let project_entry_id = buffer.read_with(&cx, |buffer, cx| {
File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
@ -2469,6 +2594,23 @@ impl Project {
}
}
fn on_dap_store_event(
&mut self,
_: Entity<DapStore>,
event: &DapStoreEvent,
cx: &mut Context<Self>,
) {
match event {
DapStoreEvent::Notification(message) => {
cx.emit(Event::Toast {
notification_id: "dap".into(),
message: message.clone(),
});
}
_ => {}
}
}
fn on_lsp_store_event(
&mut self,
_: Entity<LspStore>,
@ -3875,6 +4017,29 @@ impl Project {
None
}
pub fn project_path_for_absolute_path(&self, abs_path: &Path, cx: &App) -> Option<ProjectPath> {
self.find_local_worktree(abs_path, cx)
.map(|(worktree, relative_path)| ProjectPath {
worktree_id: worktree.read(cx).id(),
path: relative_path.into(),
})
}
pub fn find_local_worktree(
&self,
abs_path: &Path,
cx: &App,
) -> Option<(Entity<Worktree>, PathBuf)> {
let trees = self.worktrees(cx);
for tree in trees {
if let Some(relative_path) = abs_path.strip_prefix(tree.read(cx).abs_path()).ok() {
return Some((tree.clone(), relative_path.into()));
}
}
None
}
pub fn get_workspace_root(&self, project_path: &ProjectPath, cx: &App) -> Option<PathBuf> {
Some(
self.worktree_for_id(project_path.worktree_id, cx)?
@ -3943,6 +4108,7 @@ impl Project {
this.buffer_store.update(cx, |buffer_store, _| {
buffer_store.forget_shared_buffers_for(&collaborator.peer_id);
});
this.breakpoint_store.read(cx).broadcast();
cx.emit(Event::CollaboratorJoined(collaborator.peer_id));
this.collaborators
.insert(collaborator.peer_id, collaborator);