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 12:55:25 -04:00
committed by GitHub
co-authored by Piotr Osiewicz Anthony Eid Anthony Piotr Osiewicz Piotr
parent ed4e654fdf
commit 41a60ffecf
156 changed files with 25840 additions and 451 deletions
File diff suppressed because it is too large Load Diff
+204 -1
View File
@@ -21,7 +21,7 @@ use language::{
};
use project::{
project_settings::{InlineBlameSettings, ProjectSettings},
SERVER_PROGRESS_THROTTLE_TIMEOUT,
ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
};
use recent_projects::disconnected_overlay::DisconnectedOverlay;
use rpc::RECEIVE_TIMEOUT;
@@ -2408,6 +2408,209 @@ fn main() { let foo = other::foo(); }"};
);
}
#[gpui::test]
async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
let executor = cx_a.executor();
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
cx_a.update(editor::init);
cx_b.update(editor::init);
client_a
.fs()
.insert_tree(
"/a",
json!({
"test.txt": "one\ntwo\nthree\nfour\nfive",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let project_path = ProjectPath {
worktree_id,
path: Arc::from(Path::new(&"test.txt")),
};
let abs_path = project_a.read_with(cx_a, |project, cx| {
project
.absolute_path(&project_path, cx)
.map(|path_buf| Arc::from(path_buf.to_owned()))
.unwrap()
});
active_call_a
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await
.unwrap();
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.join_remote_project(project_id, cx_b).await;
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
.unwrap();
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
// Client A opens an editor.
let editor_a = workspace_a
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path(project_path.clone(), None, true, window, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
// Client B opens same editor as A.
let editor_b = workspace_b
.update_in(cx_b, |workspace, window, cx| {
workspace.open_path(project_path.clone(), None, true, window, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
cx_a.run_until_parked();
cx_b.run_until_parked();
// Client A adds breakpoint on line (1)
editor_a.update_in(cx_a, |editor, window, cx| {
editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
});
cx_a.run_until_parked();
cx_b.run_until_parked();
let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
editor
.breakpoint_store()
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
editor
.breakpoint_store()
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.clone()
});
assert_eq!(1, breakpoints_a.len());
assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
assert_eq!(breakpoints_a, breakpoints_b);
// Client B adds breakpoint on line(2)
editor_b.update_in(cx_b, |editor, window, cx| {
editor.move_down(&editor::actions::MoveDown, window, cx);
editor.move_down(&editor::actions::MoveDown, window, cx);
editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
});
cx_a.run_until_parked();
cx_b.run_until_parked();
let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
editor
.breakpoint_store()
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
editor
.breakpoint_store()
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.clone()
});
assert_eq!(1, breakpoints_a.len());
assert_eq!(breakpoints_a, breakpoints_b);
assert_eq!(2, breakpoints_a.get(&abs_path).unwrap().len());
// Client A removes last added breakpoint from client B
editor_a.update_in(cx_a, |editor, window, cx| {
editor.move_down(&editor::actions::MoveDown, window, cx);
editor.move_down(&editor::actions::MoveDown, window, cx);
editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
});
cx_a.run_until_parked();
cx_b.run_until_parked();
let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
editor
.breakpoint_store()
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
editor
.breakpoint_store()
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.clone()
});
assert_eq!(1, breakpoints_a.len());
assert_eq!(breakpoints_a, breakpoints_b);
assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
// Client B removes first added breakpoint by client A
editor_b.update_in(cx_b, |editor, window, cx| {
editor.move_up(&editor::actions::MoveUp, window, cx);
editor.move_up(&editor::actions::MoveUp, window, cx);
editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
});
cx_a.run_until_parked();
cx_b.run_until_parked();
let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
editor
.breakpoint_store()
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
editor
.breakpoint_store()
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.clone()
});
assert_eq!(0, breakpoints_a.len());
assert_eq!(breakpoints_a, breakpoints_b);
}
#[track_caller]
fn tab_undo_assert(
cx_a: &mut EditorTestContext,