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:
parent
ed4e654fdf
commit
41a60ffecf
156 changed files with 25840 additions and 451 deletions
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue