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

@ -22,6 +22,8 @@ test-support = [
"prettier/test-support",
"worktree/test-support",
"gpui/test-support",
"dap/test-support",
"dap_adapters/test-support",
]
[dependencies]
@ -30,9 +32,12 @@ anyhow.workspace = true
askpass.workspace = true
async-trait.workspace = true
buffer_diff.workspace = true
circular-buffer.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true
dap.workspace = true
dap_adapters.workspace = true
extension.workspace = true
fancy-regex.workspace = true
fs.workspace = true
@ -44,6 +49,7 @@ gpui.workspace = true
http_client.workspace = true
image.workspace = true
itertools.workspace = true
indexmap.workspace = true
language.workspace = true
log.workspace = true
lsp.workspace = true
@ -81,17 +87,19 @@ worktree.workspace = true
client = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
buffer_diff = { workspace = true, features = ["test-support"] }
dap = { workspace = true, features = ["test-support"] }
dap_adapters = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
fs = { workspace = true, features = ["test-support"] }
git2.workspace = true
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
release_channel.workspace = true
lsp = { workspace = true, features = ["test-support"] }
prettier = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
worktree = { workspace = true, features = ["test-support"] }
release_channel.workspace = true
rpc = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
unindent.workspace = true
util = { workspace = true, features = ["test-support"] }
worktree = { workspace = true, features = ["test-support"] }

View file

@ -85,6 +85,10 @@ enum OpenBuffer {
pub enum BufferStoreEvent {
BufferAdded(Entity<Buffer>),
BufferOpened {
buffer: Entity<Buffer>,
project_path: ProjectPath,
},
SharedBufferClosed(proto::PeerId, BufferId),
BufferDropped(BufferId),
BufferChangedFilePath {
@ -802,6 +806,13 @@ impl BufferStore {
}
}
fn as_local(&self) -> Option<&LocalBufferStore> {
match &self.state {
BufferStoreState::Local(state) => Some(state),
_ => None,
}
}
fn as_local_mut(&mut self) -> Option<&mut LocalBufferStore> {
match &mut self.state {
BufferStoreState::Local(state) => Some(state),
@ -829,6 +840,11 @@ impl BufferStore {
cx: &mut Context<Self>,
) -> Task<Result<Entity<Buffer>>> {
if let Some(buffer) = self.get_by_path(&project_path, cx) {
cx.emit(BufferStoreEvent::BufferOpened {
buffer: buffer.clone(),
project_path,
});
return Task::ready(Ok(buffer));
}
@ -852,12 +868,18 @@ impl BufferStore {
.insert(
cx.spawn(move |this, mut cx| async move {
let load_result = load_buffer.await;
this.update(&mut cx, |this, _cx| {
this.update(&mut cx, |this, cx| {
// Record the fact that the buffer is no longer loading.
this.loading_buffers.remove(&project_path);
})
.ok();
load_result.map_err(Arc::new)
let buffer = load_result.map_err(Arc::new)?;
cx.emit(BufferStoreEvent::BufferOpened {
buffer: buffer.clone(),
project_path,
});
Ok(buffer)
})?
})
.shared(),
)
@ -1147,6 +1169,11 @@ impl BufferStore {
})
}
pub fn buffer_id_for_project_path(&self, project_path: &ProjectPath) -> Option<&BufferId> {
self.as_local()
.and_then(|state| state.local_buffer_ids_by_path.get(project_path))
}
pub fn get_by_path(&self, path: &ProjectPath, cx: &App) -> Option<Entity<Buffer>> {
self.buffers().find_map(|buffer| {
let file = File::from_dyn(buffer.read(cx).file())?;

View file

@ -0,0 +1,19 @@
//! Zed's debugger data layer is implemented in terms of 3 concepts:
//! - DAP store - that knows about all of the available debug sessions.
//! - Debug sessions - that bear responsibility of communicating with debug adapters and managing the state of each individual session.
//! For the most part it is agnostic over the communication layer (it'll use RPC for peers and actual DAP requests for the host).
//! - Breakpoint store - that knows about all breakpoints set for a project.
//!
//! There are few reasons for this divide:
//! - Breakpoints persist across debug sessions and they're not really specific to any particular session. Sure, we have to send protocol messages for them
//! (so they're a "thing" in the protocol), but we also want to set them before any session starts up.
//! - Debug clients are doing the heavy lifting, and this is where UI grabs all of it's data from. They also rely on breakpoint store during initialization to obtain
//! current set of breakpoints.
//! - Since DAP store knows about all of the available debug sessions, it is responsible for routing RPC requests to sessions. It also knows how to find adapters for particular kind of session.
pub mod breakpoint_store;
pub mod dap_command;
pub mod dap_store;
pub mod session;
pub use dap_adapters::attach_processes;

View file

@ -0,0 +1,350 @@
# Debugger
Zed uses the Debug Adapter Protocol (DAP) to provide debugging functionality across multiple programming languages.
DAP is a standardized protocol that defines how debuggers, editors, and IDEs communicate with each other.
It allows Zed to support various debuggers without needing to implement language-specific debugging logic.
This protocol enables features like setting breakpoints, stepping through code, inspecting variables,
and more, in a consistent manner across different programming languages and runtime environments.
## Supported Debug Adapters
Zed supports a variety of debug adapters for different programming languages:
- JavaScript (node): Enables debugging of Node.js applications, including setting breakpoints, stepping through code, and inspecting variables in JavaScript.
- Python (debugpy): Provides debugging capabilities for Python applications, supporting features like remote debugging, multi-threaded debugging, and Django/Flask application debugging.
- LLDB: A powerful debugger for C, C++, Objective-C, and Swift, offering low-level debugging features and support for Apple platforms.
- GDB: The GNU Debugger, which supports debugging for multiple programming languages including C, C++, Go, and Rust, across various platforms.
- Go (dlv): Delve, a debugger for the Go programming language, offering both local and remote debugging capabilities with full support for Go's runtime and standard library.
- PHP (xdebug): Provides debugging and profiling capabilities for PHP applications, including remote debugging and code coverage analysis.
- Custom: Allows you to configure any debug adapter that supports the Debug Adapter Protocol, enabling debugging for additional languages or specialized environments not natively supported by Zed.
These adapters enable Zed to provide a consistent debugging experience across multiple languages while leveraging the specific features and capabilities of each debugger.
## How To Get Started
To start a debug session, we added few default debug configurations for each supported language that supports generic configuration options. To see all the available debug configurations, you can use the command palette `debugger: start` action, this should list all the available debug configurations.
### Configuration
To create a custom debug configuration you have to create a `.zed/debug.json` file in your project root directory. This file should contain an array of debug configurations, each with a unique label and adapter the other option are optional/required based on the adapter.
```json
[
{
// The label for the debug configuration and used to identify the debug session inside the debug panel
"label": "Example Start debugger config"
// The debug adapter that Zed should use to debug the program
"adapter": "custom",
// Request: defaults to launch
// - launch: Zed will launch the program if specified or shows a debug terminal with the right configuration
// - attach: Zed will attach to a running program to debug it or when the process_id is not specified we will show a process picker (only supported for node currently)
"request": "launch",
// cwd: defaults to the current working directory of your project ($ZED_WORKTREE_ROOT)
// this field also supports task variables e.g. $ZED_WORKTREE_ROOT
"cwd": "$ZED_WORKTREE_ROOT",
// program: The program that you want to debug
// this fields also support task variables e.g. $ZED_FILE
// Note: this field should only contain the path to the program you want to debug
"program": "path_to_program",
// initialize_args: This field should contain all the adapter specific initialization arguments that are directly send to the debug adapter
"initialize_args": {
// "stopOnEntry": true // e.g. to stop on the first line of the program (These args are DAP specific)
}
}
]
```
### Using Attach [WIP]
Only javascript and lldb supports starting a debug session using attach.
When using the attach request with a process ID the syntax is as follows:
```json
{
"label": "Attach to Process",
"adapter": "javascript",
"request": {
"attach": {
"process_id": "12345"
}
}
}
```
Without process ID the syntax is as follows:
```json
{
"label": "Attach to Process",
"adapter": "javascript",
"request": {
"attach": {}
}
}
```
#### JavaScript Configuration
##### Debug Active File
This configuration allows you to debug a JavaScript file in your project.
```json
{
"label": "JavaScript: Debug Active File",
"adapter": "javascript",
"program": "$ZED_FILE",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
}
```
##### Debug Terminal
This configuration will spawn a debug terminal where you could start you program by typing `node test.js`, and the debug adapter will automatically attach to the process.
```json
{
"label": "JavaScript: Debug Terminal",
"adapter": "javascript",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT",
// "program": "$ZED_FILE", // optional if you pass this in, you will see the output inside the terminal itself
"initialize_args": {
"console": "integratedTerminal"
}
}
```
#### PHP Configuration
##### Debug Active File
This configuration allows you to debug a PHP file in your project.
```json
{
"label": "PHP: Debug Active File",
"adapter": "php",
"program": "$ZED_FILE",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
}
```
#### Python Configuration
##### Debug Active File
This configuration allows you to debug a Python file in your project.
```json
{
"label": "Python: Debug Active File",
"adapter": "python",
"program": "$ZED_FILE",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
}
```
#### GDB Configuration
**NOTE:** This configuration is for Linux systems only & intel macbooks.
##### Debug Program
This configuration allows you to debug a program using GDB e.g. Zed itself.
```json
{
"label": "GDB: Debug program",
"adapter": "gdb",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
}
```
#### LLDB Configuration
##### Debug Program
This configuration allows you to debug a program using LLDB e.g. Zed itself.
```json
{
"label": "LLDB: Debug program",
"adapter": "lldb",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
}
```
## Breakpoints
Zed currently supports these types of breakpoints
- Log Breakpoints: Output a log message instead of stopping at the breakpoint when it's hit
- Standard Breakpoints: Stop at the breakpoint when it's hit
Standard breakpoints can be toggled by left clicking on the editor gutter or using the Toggle Breakpoint action. Right clicking on a breakpoint, code action symbol, or code runner symbol brings up the breakpoint context menu. That has options for toggling breakpoints and editing log breakpoints.
Log breakpoints can also be edited/added through the edit log breakpoint action
## Settings
- `stepping_granularity`: Determines the stepping granularity.
- `save_breakpoints`: Whether the breakpoints should be reused across Zed sessions.
- `button`: Whether to show the debug button in the status bar.
- `timeout`: Time in milliseconds until timeout error when connecting to a TCP debug adapter.
- `log_dap_communications`: Whether to log messages between active debug adapters and Zed
- `format_dap_log_messages`: Whether to format dap messages in when adding them to debug adapter logger
### Stepping granularity
- Description: The Step granularity that the debugger will use
- Default: line
- Setting: debugger.stepping_granularity
**Options**
1. Statement - The step should allow the program to run until the current statement has finished executing.
The meaning of a statement is determined by the adapter and it may be considered equivalent to a line.
For example 'for(int i = 0; i < 10; i++)' could be considered to have 3 statements 'int i = 0', 'i < 10', and 'i++'.
```json
{
"debugger": {
"stepping_granularity": "statement"
}
}
```
2. Line - The step should allow the program to run until the current source line has executed.
```json
{
"debugger": {
"stepping_granularity": "line"
}
}
```
3. Instruction - The step should allow one instruction to execute (e.g. one x86 instruction).
```json
{
"debugger": {
"stepping_granularity": "instruction"
}
}
```
### Save Breakpoints
- Description: Whether the breakpoints should be saved across Zed sessions.
- Default: true
- Setting: debugger.save_breakpoints
**Options**
`boolean` values
```json
{
"debugger": {
"save_breakpoints": true
}
}
```
### Button
- Description: Whether the button should be displayed in the debugger toolbar.
- Default: true
- Setting: debugger.show_button
**Options**
`boolean` values
```json
{
"debugger": {
"show_button": true
}
}
```
### Timeout
- Description: Time in milliseconds until timeout error when connecting to a TCP debug adapter.
- Default: 2000ms
- Setting: debugger.timeout
**Options**
`integer` values
```json
{
"debugger": {
"timeout": 3000
}
}
```
### Log Dap Communications
- Description: Whether to log messages between active debug adapters and Zed. (Used for DAP development)
- Default: false
- Setting: debugger.log_dap_communications
**Options**
`boolean` values
```json
{
"debugger": {
"log_dap_communications": true
}
}
```
### Format Dap Log Messages
- Description: Whether to format dap messages in when adding them to debug adapter logger. (Used for DAP development)
- Default: false
- Setting: debugger.format_dap_log_messages
**Options**
`boolean` values
```json
{
"debugger": {
"format_dap_log_messages": true
}
}
```
## Theme
The Debugger supports the following theme options
/// Color used to accent some of the debuggers elements
/// Only accent breakpoint & breakpoint related symbols right now
**debugger.accent**: Color used to accent breakpoint & breakpoint related symbols
**editor.debugger_active_line.background**: Background color of active debug line

View file

@ -0,0 +1,612 @@
//! Module for managing breakpoints in a project.
//!
//! Breakpoints are separate from a session because they're not associated with any particular debug session. They can also be set up without a session running.
use anyhow::{anyhow, Result};
use breakpoints_in_file::BreakpointsInFile;
use collections::BTreeMap;
use dap::client::SessionId;
use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, Task};
use language::{proto::serialize_anchor as serialize_text_anchor, Buffer, BufferSnapshot};
use rpc::{
proto::{self},
AnyProtoClient, TypedEnvelope,
};
use std::{
hash::{Hash, Hasher},
ops::Range,
path::Path,
sync::Arc,
};
use text::PointUtf16;
use crate::{buffer_store::BufferStore, worktree_store::WorktreeStore, Project, ProjectPath};
mod breakpoints_in_file {
use language::BufferEvent;
use super::*;
#[derive(Clone)]
pub(super) struct BreakpointsInFile {
pub(super) buffer: Entity<Buffer>,
// TODO: This is.. less than ideal, as it's O(n) and does not return entries in order. We'll have to change TreeMap to support passing in the context for comparisons
pub(super) breakpoints: Vec<(text::Anchor, Breakpoint)>,
_subscription: Arc<gpui::Subscription>,
}
impl BreakpointsInFile {
pub(super) fn new(buffer: Entity<Buffer>, cx: &mut Context<BreakpointStore>) -> Self {
let subscription =
Arc::from(cx.subscribe(&buffer, |_, buffer, event, cx| match event {
BufferEvent::Saved => {
if let Some(abs_path) = BreakpointStore::abs_path_from_buffer(&buffer, cx) {
cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
abs_path,
BreakpointUpdatedReason::FileSaved,
));
}
}
_ => {}
}));
BreakpointsInFile {
buffer,
breakpoints: Vec::new(),
_subscription: subscription,
}
}
}
}
#[derive(Clone)]
struct RemoteBreakpointStore {
upstream_client: AnyProtoClient,
_upstream_project_id: u64,
}
#[derive(Clone)]
struct LocalBreakpointStore {
worktree_store: Entity<WorktreeStore>,
buffer_store: Entity<BufferStore>,
}
#[derive(Clone)]
enum BreakpointStoreMode {
Local(LocalBreakpointStore),
Remote(RemoteBreakpointStore),
}
pub struct BreakpointStore {
breakpoints: BTreeMap<Arc<Path>, BreakpointsInFile>,
downstream_client: Option<(AnyProtoClient, u64)>,
active_stack_frame: Option<(SessionId, Arc<Path>, text::Anchor)>,
// E.g ssh
mode: BreakpointStoreMode,
}
impl BreakpointStore {
pub fn init(client: &AnyProtoClient) {
client.add_entity_request_handler(Self::handle_toggle_breakpoint);
client.add_entity_message_handler(Self::handle_breakpoints_for_file);
}
pub fn local(worktree_store: Entity<WorktreeStore>, buffer_store: Entity<BufferStore>) -> Self {
BreakpointStore {
breakpoints: BTreeMap::new(),
mode: BreakpointStoreMode::Local(LocalBreakpointStore {
worktree_store,
buffer_store,
}),
downstream_client: None,
active_stack_frame: Default::default(),
}
}
pub(crate) fn remote(upstream_project_id: u64, upstream_client: AnyProtoClient) -> Self {
BreakpointStore {
breakpoints: BTreeMap::new(),
mode: BreakpointStoreMode::Remote(RemoteBreakpointStore {
upstream_client,
_upstream_project_id: upstream_project_id,
}),
downstream_client: None,
active_stack_frame: Default::default(),
}
}
pub(crate) fn shared(&mut self, project_id: u64, downstream_client: AnyProtoClient) {
self.downstream_client = Some((downstream_client.clone(), project_id));
}
pub(crate) fn unshared(&mut self, cx: &mut Context<Self>) {
self.downstream_client.take();
cx.notify();
}
async fn handle_breakpoints_for_file(
this: Entity<Project>,
message: TypedEnvelope<proto::BreakpointsForFile>,
mut cx: AsyncApp,
) -> Result<()> {
let breakpoints = cx.update(|cx| this.read(cx).breakpoint_store())?;
if message.payload.breakpoints.is_empty() {
return Ok(());
}
let buffer = this
.update(&mut cx, |this, cx| {
let path =
this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)?;
Some(this.open_buffer(path, cx))
})
.ok()
.flatten()
.ok_or_else(|| anyhow!("Invalid project path"))?
.await?;
breakpoints.update(&mut cx, move |this, cx| {
let bps = this
.breakpoints
.entry(Arc::<Path>::from(message.payload.path.as_ref()))
.or_insert_with(|| BreakpointsInFile::new(buffer, cx));
bps.breakpoints = message
.payload
.breakpoints
.into_iter()
.filter_map(|breakpoint| {
let anchor = language::proto::deserialize_anchor(breakpoint.position.clone()?)?;
let breakpoint = Breakpoint::from_proto(breakpoint)?;
Some((anchor, breakpoint))
})
.collect();
cx.notify();
})?;
Ok(())
}
async fn handle_toggle_breakpoint(
this: Entity<Project>,
message: TypedEnvelope<proto::ToggleBreakpoint>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
let breakpoints = this.update(&mut cx, |this, _| this.breakpoint_store())?;
let path = this
.update(&mut cx, |this, cx| {
this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)
})?
.ok_or_else(|| anyhow!("Could not resolve provided abs path"))?;
let buffer = this
.update(&mut cx, |this, cx| {
this.buffer_store().read(cx).get_by_path(&path, cx)
})?
.ok_or_else(|| anyhow!("Could not find buffer for a given path"))?;
let breakpoint = message
.payload
.breakpoint
.ok_or_else(|| anyhow!("Breakpoint not present in RPC payload"))?;
let anchor = language::proto::deserialize_anchor(
breakpoint
.position
.clone()
.ok_or_else(|| anyhow!("Anchor not present in RPC payload"))?,
)
.ok_or_else(|| anyhow!("Anchor deserialization failed"))?;
let breakpoint = Breakpoint::from_proto(breakpoint)
.ok_or_else(|| anyhow!("Could not deserialize breakpoint"))?;
breakpoints.update(&mut cx, |this, cx| {
this.toggle_breakpoint(
buffer,
(anchor, breakpoint),
BreakpointEditAction::Toggle,
cx,
);
})?;
Ok(proto::Ack {})
}
pub(crate) fn broadcast(&self) {
if let Some((client, project_id)) = &self.downstream_client {
for (path, breakpoint_set) in &self.breakpoints {
let _ = client.send(proto::BreakpointsForFile {
project_id: *project_id,
path: path.to_str().map(ToOwned::to_owned).unwrap(),
breakpoints: breakpoint_set
.breakpoints
.iter()
.filter_map(|(anchor, bp)| bp.to_proto(&path, anchor))
.collect(),
});
}
}
}
fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
worktree::File::from_dyn(buffer.read(cx).file())
.and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok())
.map(Arc::<Path>::from)
}
pub fn toggle_breakpoint(
&mut self,
buffer: Entity<Buffer>,
mut breakpoint: (text::Anchor, Breakpoint),
edit_action: BreakpointEditAction,
cx: &mut Context<Self>,
) {
let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else {
return;
};
let breakpoint_set = self
.breakpoints
.entry(abs_path.clone())
.or_insert_with(|| BreakpointsInFile::new(buffer, cx));
match edit_action {
BreakpointEditAction::Toggle => {
let len_before = breakpoint_set.breakpoints.len();
breakpoint_set
.breakpoints
.retain(|value| &breakpoint != value);
if len_before == breakpoint_set.breakpoints.len() {
// We did not remove any breakpoint, hence let's toggle one.
breakpoint_set.breakpoints.push(breakpoint.clone());
}
}
BreakpointEditAction::EditLogMessage(log_message) => {
if !log_message.is_empty() {
breakpoint.1.kind = BreakpointKind::Log(log_message.clone());
let found_bp =
breakpoint_set
.breakpoints
.iter_mut()
.find_map(|(other_pos, other_bp)| {
if breakpoint.0 == *other_pos {
Some(other_bp)
} else {
None
}
});
if let Some(found_bp) = found_bp {
found_bp.kind = BreakpointKind::Log(log_message.clone());
} else {
// We did not remove any breakpoint, hence let's toggle one.
breakpoint_set.breakpoints.push(breakpoint.clone());
}
} else if matches!(&breakpoint.1.kind, BreakpointKind::Log(_)) {
breakpoint_set
.breakpoints
.retain(|(other_pos, other_kind)| {
&breakpoint.0 != other_pos
&& matches!(other_kind.kind, BreakpointKind::Standard)
});
}
}
}
if breakpoint_set.breakpoints.is_empty() {
self.breakpoints.remove(&abs_path);
}
if let BreakpointStoreMode::Remote(remote) = &self.mode {
if let Some(breakpoint) = breakpoint.1.to_proto(&abs_path, &breakpoint.0) {
cx.background_spawn(remote.upstream_client.request(proto::ToggleBreakpoint {
project_id: remote._upstream_project_id,
path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
breakpoint: Some(breakpoint),
}))
.detach();
}
} else if let Some((client, project_id)) = &self.downstream_client {
let breakpoints = self
.breakpoints
.get(&abs_path)
.map(|breakpoint_set| {
breakpoint_set
.breakpoints
.iter()
.filter_map(|(anchor, bp)| bp.to_proto(&abs_path, anchor))
.collect()
})
.unwrap_or_default();
let _ = client.send(proto::BreakpointsForFile {
project_id: *project_id,
path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
breakpoints,
});
}
cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
abs_path,
BreakpointUpdatedReason::Toggled,
));
cx.notify();
}
pub fn on_file_rename(
&mut self,
old_path: Arc<Path>,
new_path: Arc<Path>,
cx: &mut Context<Self>,
) {
if let Some(breakpoints) = self.breakpoints.remove(&old_path) {
self.breakpoints.insert(new_path.clone(), breakpoints);
cx.notify();
}
}
pub fn breakpoints<'a>(
&'a self,
buffer: &'a Entity<Buffer>,
range: Option<Range<text::Anchor>>,
buffer_snapshot: BufferSnapshot,
cx: &App,
) -> impl Iterator<Item = &'a (text::Anchor, Breakpoint)> + 'a {
let abs_path = Self::abs_path_from_buffer(buffer, cx);
abs_path
.and_then(|path| self.breakpoints.get(&path))
.into_iter()
.flat_map(move |file_breakpoints| {
file_breakpoints.breakpoints.iter().filter({
let range = range.clone();
let buffer_snapshot = buffer_snapshot.clone();
move |(position, _)| {
if let Some(range) = &range {
position.cmp(&range.start, &buffer_snapshot).is_ge()
&& position.cmp(&range.end, &buffer_snapshot).is_le()
} else {
true
}
}
})
})
}
pub fn active_position(&self) -> Option<&(SessionId, Arc<Path>, text::Anchor)> {
self.active_stack_frame.as_ref()
}
pub fn remove_active_position(
&mut self,
session_id: Option<SessionId>,
cx: &mut Context<Self>,
) {
if let Some(session_id) = session_id {
self.active_stack_frame
.take_if(|(id, _, _)| *id == session_id);
} else {
self.active_stack_frame.take();
}
cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged);
cx.notify();
}
pub fn set_active_position(
&mut self,
position: (SessionId, Arc<Path>, text::Anchor),
cx: &mut Context<Self>,
) {
self.active_stack_frame = Some(position);
cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged);
cx.notify();
}
pub fn breakpoints_from_path(&self, path: &Arc<Path>, cx: &App) -> Vec<SerializedBreakpoint> {
self.breakpoints
.get(path)
.map(|bp| {
let snapshot = bp.buffer.read(cx).snapshot();
bp.breakpoints
.iter()
.map(|(position, breakpoint)| {
let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
SerializedBreakpoint {
position,
path: path.clone(),
kind: breakpoint.kind.clone(),
}
})
.collect()
})
.unwrap_or_default()
}
pub fn all_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>> {
self.breakpoints
.iter()
.map(|(path, bp)| {
let snapshot = bp.buffer.read(cx).snapshot();
(
path.clone(),
bp.breakpoints
.iter()
.map(|(position, breakpoint)| {
let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
SerializedBreakpoint {
position,
path: path.clone(),
kind: breakpoint.kind.clone(),
}
})
.collect(),
)
})
.collect()
}
pub fn with_serialized_breakpoints(
&self,
breakpoints: BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>>,
cx: &mut Context<'_, BreakpointStore>,
) -> Task<Result<()>> {
if let BreakpointStoreMode::Local(mode) = &self.mode {
let mode = mode.clone();
cx.spawn(move |this, mut cx| async move {
let mut new_breakpoints = BTreeMap::default();
for (path, bps) in breakpoints {
if bps.is_empty() {
continue;
}
let (worktree, relative_path) = mode
.worktree_store
.update(&mut cx, |this, cx| {
this.find_or_create_worktree(&path, false, cx)
})?
.await?;
let buffer = mode
.buffer_store
.update(&mut cx, |this, cx| {
let path = ProjectPath {
worktree_id: worktree.read(cx).id(),
path: relative_path.into(),
};
this.open_buffer(path, cx)
})?
.await;
let Ok(buffer) = buffer else {
log::error!("Todo: Serialized breakpoints which do not have buffer (yet)");
continue;
};
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
let mut breakpoints_for_file =
this.update(&mut cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
for bp in bps {
let position = snapshot.anchor_before(PointUtf16::new(bp.position, 0));
breakpoints_for_file
.breakpoints
.push((position, Breakpoint { kind: bp.kind }))
}
new_breakpoints.insert(path, breakpoints_for_file);
}
this.update(&mut cx, |this, cx| {
this.breakpoints = new_breakpoints;
cx.notify();
})?;
Ok(())
})
} else {
Task::ready(Ok(()))
}
}
}
#[derive(Clone, Copy)]
pub enum BreakpointUpdatedReason {
Toggled,
FileSaved,
}
pub enum BreakpointStoreEvent {
ActiveDebugLineChanged,
BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
}
impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
type LogMessage = Arc<str>;
#[derive(Clone, Debug)]
pub enum BreakpointEditAction {
Toggle,
EditLogMessage(LogMessage),
}
#[derive(Clone, Debug)]
pub enum BreakpointKind {
Standard,
Log(LogMessage),
}
impl BreakpointKind {
pub fn to_int(&self) -> i32 {
match self {
BreakpointKind::Standard => 0,
BreakpointKind::Log(_) => 1,
}
}
pub fn log_message(&self) -> Option<LogMessage> {
match self {
BreakpointKind::Standard => None,
BreakpointKind::Log(message) => Some(message.clone()),
}
}
}
impl PartialEq for BreakpointKind {
fn eq(&self, other: &Self) -> bool {
std::mem::discriminant(self) == std::mem::discriminant(other)
}
}
impl Eq for BreakpointKind {}
impl Hash for BreakpointKind {
fn hash<H: Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Breakpoint {
pub kind: BreakpointKind,
}
impl Breakpoint {
fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
Some(client::proto::Breakpoint {
position: Some(serialize_text_anchor(position)),
kind: match self.kind {
BreakpointKind::Standard => proto::BreakpointKind::Standard.into(),
BreakpointKind::Log(_) => proto::BreakpointKind::Log.into(),
},
message: if let BreakpointKind::Log(message) = &self.kind {
Some(message.to_string())
} else {
None
},
})
}
fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
Some(Self {
kind: match proto::BreakpointKind::from_i32(breakpoint.kind) {
Some(proto::BreakpointKind::Log) => {
BreakpointKind::Log(breakpoint.message.clone().unwrap_or_default().into())
}
None | Some(proto::BreakpointKind::Standard) => BreakpointKind::Standard,
},
})
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct SerializedBreakpoint {
pub position: u32,
pub path: Arc<Path>,
pub kind: BreakpointKind,
}
impl From<SerializedBreakpoint> for dap::SourceBreakpoint {
fn from(bp: SerializedBreakpoint) -> Self {
Self {
line: bp.position as u64 + 1,
column: None,
condition: None,
hit_condition: None,
log_message: bp.kind.log_message().as_deref().map(Into::into),
mode: None,
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,882 @@
use super::{
breakpoint_store::BreakpointStore,
// Will need to uncomment this once we implement rpc message handler again
// dap_command::{
// ContinueCommand, DapCommand, DisconnectCommand, NextCommand, PauseCommand, RestartCommand,
// RestartStackFrameCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand,
// TerminateCommand, TerminateThreadsCommand, VariablesCommand,
// },
session::{self, Session},
};
use crate::{debugger, worktree_store::WorktreeStore, ProjectEnvironment};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use collections::HashMap;
use dap::{
adapters::{DapStatus, DebugAdapterName},
client::SessionId,
messages::Message,
requests::{
Completions, Evaluate, Request as _, RunInTerminal, SetExpression, SetVariable,
StartDebugging,
},
Capabilities, CompletionItem, CompletionsArguments, ErrorResponse, EvaluateArguments,
EvaluateArgumentsContext, EvaluateResponse, RunInTerminalRequestArguments,
SetExpressionArguments, SetVariableArguments, Source, StartDebuggingRequestArguments,
StartDebuggingRequestArgumentsRequest,
};
use fs::Fs;
use futures::{
channel::{mpsc, oneshot},
future::Shared,
};
use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};
use http_client::HttpClient;
use language::{BinaryStatus, LanguageRegistry, LanguageToolchainStore};
use lsp::LanguageServerName;
use node_runtime::NodeRuntime;
use rpc::{
proto::{self},
AnyProtoClient, TypedEnvelope,
};
use serde_json::Value;
use settings::WorktreeId;
use smol::{lock::Mutex, stream::StreamExt};
use std::{
borrow::Borrow,
collections::{BTreeMap, HashSet},
ffi::OsStr,
path::PathBuf,
sync::{atomic::Ordering::SeqCst, Arc},
};
use std::{collections::VecDeque, sync::atomic::AtomicU32};
use task::{AttachConfig, DebugAdapterConfig, DebugRequestType};
use util::ResultExt as _;
use worktree::Worktree;
pub enum DapStoreEvent {
DebugClientStarted(SessionId),
DebugClientShutdown(SessionId),
DebugClientEvent {
session_id: SessionId,
message: Message,
},
RunInTerminal {
session_id: SessionId,
title: Option<String>,
cwd: PathBuf,
command: Option<String>,
args: Vec<String>,
envs: HashMap<String, String>,
sender: mpsc::Sender<Result<u32>>,
},
Notification(String),
RemoteHasInitialized,
}
#[allow(clippy::large_enum_variant)]
pub enum DapStoreMode {
Local(LocalDapStore), // ssh host and collab host
Remote(RemoteDapStore), // collab guest
}
pub struct LocalDapStore {
fs: Arc<dyn Fs>,
node_runtime: NodeRuntime,
next_session_id: AtomicU32,
http_client: Arc<dyn HttpClient>,
worktree_store: Entity<WorktreeStore>,
environment: Entity<ProjectEnvironment>,
language_registry: Arc<LanguageRegistry>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
start_debugging_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
_start_debugging_task: Task<()>,
}
impl LocalDapStore {
fn next_session_id(&self) -> SessionId {
SessionId(self.next_session_id.fetch_add(1, SeqCst))
}
}
pub struct RemoteDapStore {
upstream_client: AnyProtoClient,
upstream_project_id: u64,
event_queue: Option<VecDeque<DapStoreEvent>>,
}
pub struct DapStore {
mode: DapStoreMode,
downstream_client: Option<(AnyProtoClient, u64)>,
breakpoint_store: Entity<BreakpointStore>,
sessions: BTreeMap<SessionId, Entity<Session>>,
}
impl EventEmitter<DapStoreEvent> for DapStore {}
impl DapStore {
pub fn init(_client: &AnyProtoClient) {
// todo(debugger): Reenable these after we finish handle_dap_command refactor
// client.add_entity_request_handler(Self::handle_dap_command::<NextCommand>);
// client.add_entity_request_handler(Self::handle_dap_command::<StepInCommand>);
// client.add_entity_request_handler(Self::handle_dap_command::<StepOutCommand>);
// client.add_entity_request_handler(Self::handle_dap_command::<StepBackCommand>);
// client.add_entity_request_handler(Self::handle_dap_command::<ContinueCommand>);
// client.add_entity_request_handler(Self::handle_dap_command::<PauseCommand>);
// client.add_entity_request_handler(Self::handle_dap_command::<DisconnectCommand>);
// client.add_entity_request_handler(Self::handle_dap_command::<TerminateThreadsCommand>);
// client.add_entity_request_handler(Self::handle_dap_command::<TerminateCommand>);
// client.add_entity_request_handler(Self::handle_dap_command::<RestartCommand>);
// client.add_entity_request_handler(Self::handle_dap_command::<VariablesCommand>);
// client.add_entity_request_handler(Self::handle_dap_command::<RestartStackFrameCommand>);
}
#[expect(clippy::too_many_arguments)]
pub fn new_local(
http_client: Arc<dyn HttpClient>,
node_runtime: NodeRuntime,
fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>,
environment: Entity<ProjectEnvironment>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
breakpoint_store: Entity<BreakpointStore>,
worktree_store: Entity<WorktreeStore>,
cx: &mut Context<Self>,
) -> Self {
cx.on_app_quit(Self::shutdown_sessions).detach();
let (start_debugging_tx, mut message_rx) =
futures::channel::mpsc::unbounded::<(SessionId, Message)>();
let _start_debugging_task = cx.spawn(move |this, mut cx| async move {
while let Some((session_id, message)) = message_rx.next().await {
match message {
Message::Request(request) => {
let _ = this
.update(&mut cx, |this, cx| {
if request.command == StartDebugging::COMMAND {
this.handle_start_debugging_request(session_id, request, cx)
.detach_and_log_err(cx);
} else if request.command == RunInTerminal::COMMAND {
this.handle_run_in_terminal_request(session_id, request, cx)
.detach_and_log_err(cx);
}
})
.log_err();
}
_ => {}
}
}
});
Self {
mode: DapStoreMode::Local(LocalDapStore {
fs,
environment,
http_client,
node_runtime,
worktree_store,
toolchain_store,
language_registry,
start_debugging_tx,
_start_debugging_task,
next_session_id: Default::default(),
}),
downstream_client: None,
breakpoint_store,
sessions: Default::default(),
}
}
pub fn new_remote(
project_id: u64,
upstream_client: AnyProtoClient,
breakpoint_store: Entity<BreakpointStore>,
) -> Self {
Self {
mode: DapStoreMode::Remote(RemoteDapStore {
upstream_client,
upstream_project_id: project_id,
event_queue: Some(VecDeque::default()),
}),
downstream_client: None,
breakpoint_store,
sessions: Default::default(),
}
}
pub fn as_remote(&self) -> Option<&RemoteDapStore> {
match &self.mode {
DapStoreMode::Remote(remote_dap_store) => Some(remote_dap_store),
_ => None,
}
}
pub fn remote_event_queue(&mut self) -> Option<VecDeque<DapStoreEvent>> {
if let DapStoreMode::Remote(remote) = &mut self.mode {
remote.event_queue.take()
} else {
None
}
}
pub fn as_local(&self) -> Option<&LocalDapStore> {
match &self.mode {
DapStoreMode::Local(local_dap_store) => Some(local_dap_store),
_ => None,
}
}
pub fn as_local_mut(&mut self) -> Option<&mut LocalDapStore> {
match &mut self.mode {
DapStoreMode::Local(local_dap_store) => Some(local_dap_store),
_ => None,
}
}
pub fn upstream_client(&self) -> Option<(AnyProtoClient, u64)> {
match &self.mode {
DapStoreMode::Remote(RemoteDapStore {
upstream_client,
upstream_project_id,
..
}) => Some((upstream_client.clone(), *upstream_project_id)),
DapStoreMode::Local(_) => None,
}
}
pub fn downstream_client(&self) -> Option<&(AnyProtoClient, u64)> {
self.downstream_client.as_ref()
}
pub fn add_remote_client(
&mut self,
session_id: SessionId,
ignore: Option<bool>,
cx: &mut Context<Self>,
) {
if let DapStoreMode::Remote(remote) = &self.mode {
self.sessions.insert(
session_id,
cx.new(|_| {
debugger::session::Session::remote(
session_id,
remote.upstream_client.clone(),
remote.upstream_project_id,
ignore.unwrap_or(false),
)
}),
);
} else {
debug_assert!(false);
}
}
pub fn session_by_id(
&self,
session_id: impl Borrow<SessionId>,
) -> Option<Entity<session::Session>> {
let session_id = session_id.borrow();
let client = self.sessions.get(session_id).cloned();
client
}
pub fn sessions(&self) -> impl Iterator<Item = &Entity<Session>> {
self.sessions.values()
}
pub fn capabilities_by_id(
&self,
session_id: impl Borrow<SessionId>,
cx: &App,
) -> Option<Capabilities> {
let session_id = session_id.borrow();
self.sessions
.get(session_id)
.map(|client| client.read(cx).capabilities.clone())
}
pub fn breakpoint_store(&self) -> &Entity<BreakpointStore> {
&self.breakpoint_store
}
#[allow(dead_code)]
async fn handle_ignore_breakpoint_state(
this: Entity<Self>,
envelope: TypedEnvelope<proto::IgnoreBreakpointState>,
mut cx: AsyncApp,
) -> Result<()> {
let session_id = SessionId::from_proto(envelope.payload.session_id);
this.update(&mut cx, |this, cx| {
if let Some(session) = this.session_by_id(&session_id) {
session.update(cx, |session, cx| {
session.set_ignore_breakpoints(envelope.payload.ignore, cx)
})
} else {
Task::ready(())
}
})?
.await;
Ok(())
}
pub fn new_session(
&mut self,
config: DebugAdapterConfig,
worktree: &Entity<Worktree>,
parent_session: Option<Entity<Session>>,
cx: &mut Context<Self>,
) -> (SessionId, Task<Result<Entity<Session>>>) {
let Some(local_store) = self.as_local() else {
unimplemented!("Starting session on remote side");
};
let delegate = DapAdapterDelegate::new(
local_store.fs.clone(),
worktree.read(cx).id(),
local_store.node_runtime.clone(),
local_store.http_client.clone(),
local_store.language_registry.clone(),
local_store.toolchain_store.clone(),
local_store.environment.update(cx, |env, cx| {
let worktree = worktree.read(cx);
env.get_environment(Some(worktree.id()), Some(worktree.abs_path()), cx)
}),
);
let session_id = local_store.next_session_id();
let (initialized_tx, initialized_rx) = oneshot::channel();
let start_client_task = Session::local(
self.breakpoint_store.clone(),
session_id,
parent_session,
delegate,
config,
local_store.start_debugging_tx.clone(),
initialized_tx,
cx,
);
let task = cx.spawn(|this, mut cx| async move {
let session = match start_client_task.await {
Ok(session) => session,
Err(error) => {
this.update(&mut cx, |_, cx| {
cx.emit(DapStoreEvent::Notification(error.to_string()));
})
.log_err();
return Err(error);
}
};
// we have to insert the session early, so we can handle reverse requests
// that need the session to be available
this.update(&mut cx, |store, cx| {
store.sessions.insert(session_id, session.clone());
cx.emit(DapStoreEvent::DebugClientStarted(session_id));
cx.notify();
})?;
match session
.update(&mut cx, |session, cx| {
session.initialize_sequence(initialized_rx, cx)
})?
.await
{
Ok(_) => {}
Err(error) => {
this.update(&mut cx, |this, cx| {
cx.emit(DapStoreEvent::Notification(error.to_string()));
this.shutdown_session(session_id, cx)
})?
.await
.log_err();
return Err(error);
}
}
Ok(session)
});
(session_id, task)
}
fn handle_start_debugging_request(
&mut self,
session_id: SessionId,
request: dap::messages::Request,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(local_store) = self.as_local() else {
unreachable!("Cannot response for non-local session");
};
let Some(parent_session) = self.session_by_id(session_id) else {
return Task::ready(Err(anyhow!("Session not found")));
};
let args = serde_json::from_value::<StartDebuggingRequestArguments>(
request.arguments.unwrap_or_default(),
)
.expect("To parse StartDebuggingRequestArguments");
let worktree = local_store
.worktree_store
.update(cx, |this, _| this.worktrees().next())
.expect("worktree-less project");
let Some(config) = parent_session.read(cx).configuration() else {
unreachable!("there must be a config for local sessions");
};
let (_, new_session_task) = self.new_session(
DebugAdapterConfig {
label: config.label,
kind: config.kind,
request: match &args.request {
StartDebuggingRequestArgumentsRequest::Launch => DebugRequestType::Launch,
StartDebuggingRequestArgumentsRequest::Attach => {
DebugRequestType::Attach(AttachConfig::default())
}
},
program: config.program,
cwd: config.cwd,
initialize_args: Some(args.configuration),
supports_attach: config.supports_attach,
},
&worktree,
Some(parent_session.clone()),
cx,
);
let request_seq = request.seq;
cx.spawn(|_, mut cx| async move {
let (success, body) = match new_session_task.await {
Ok(_) => (true, None),
Err(error) => (
false,
Some(serde_json::to_value(ErrorResponse {
error: Some(dap::Message {
id: request_seq,
format: error.to_string(),
variables: None,
send_telemetry: None,
show_user: None,
url: None,
url_label: None,
}),
})?),
),
};
parent_session
.update(&mut cx, |session, cx| {
session.respond_to_client(
request_seq,
success,
StartDebugging::COMMAND.to_string(),
body,
cx,
)
})?
.await
})
}
fn handle_run_in_terminal_request(
&mut self,
session_id: SessionId,
request: dap::messages::Request,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(session) = self.session_by_id(session_id) else {
return Task::ready(Err(anyhow!("Session not found")));
};
let request_args = serde_json::from_value::<RunInTerminalRequestArguments>(
request.arguments.unwrap_or_default(),
)
.expect("To parse StartDebuggingRequestArguments");
let seq = request.seq;
let cwd = PathBuf::from(request_args.cwd);
match cwd.try_exists() {
Ok(true) => (),
Ok(false) | Err(_) => {
return session.update(cx, |session, cx| {
session.respond_to_client(
seq,
false,
RunInTerminal::COMMAND.to_string(),
serde_json::to_value(dap::ErrorResponse {
error: Some(dap::Message {
id: seq,
format: format!("Received invalid/unknown cwd: {cwd:?}"),
variables: None,
send_telemetry: None,
show_user: None,
url: None,
url_label: None,
}),
})
.ok(),
cx,
)
})
}
}
let mut args = request_args.args.clone();
// Handle special case for NodeJS debug adapter
// If only the Node binary path is provided, we set the command to None
// This prevents the NodeJS REPL from appearing, which is not the desired behavior
// The expected usage is for users to provide their own Node command, e.g., `node test.js`
// This allows the NodeJS debug client to attach correctly
let command = if args.len() > 1 {
Some(args.remove(0))
} else {
None
};
let mut envs: HashMap<String, String> = Default::default();
if let Some(Value::Object(env)) = request_args.env {
for (key, value) in env {
let value_str = match (key.as_str(), value) {
(_, Value::String(value)) => value,
_ => continue,
};
envs.insert(key, value_str);
}
}
let (tx, mut rx) = mpsc::channel::<Result<u32>>(1);
cx.emit(DapStoreEvent::RunInTerminal {
session_id,
title: request_args.title,
cwd,
command,
args,
envs,
sender: tx,
});
cx.notify();
let session = session.downgrade();
cx.spawn(|_, mut cx| async move {
let (success, body) = match rx.next().await {
Some(Ok(pid)) => (
true,
serde_json::to_value(dap::RunInTerminalResponse {
process_id: None,
shell_process_id: Some(pid as u64),
})
.ok(),
),
Some(Err(error)) => (
false,
serde_json::to_value(dap::ErrorResponse {
error: Some(dap::Message {
id: seq,
format: error.to_string(),
variables: None,
send_telemetry: None,
show_user: None,
url: None,
url_label: None,
}),
})
.ok(),
),
None => (
false,
serde_json::to_value(dap::ErrorResponse {
error: Some(dap::Message {
id: seq,
format: "failed to receive response from spawn terminal".to_string(),
variables: None,
send_telemetry: None,
show_user: None,
url: None,
url_label: None,
}),
})
.ok(),
),
};
session
.update(&mut cx, |session, cx| {
session.respond_to_client(
seq,
success,
RunInTerminal::COMMAND.to_string(),
body,
cx,
)
})?
.await
})
}
pub fn evaluate(
&self,
session_id: &SessionId,
stack_frame_id: u64,
expression: String,
context: EvaluateArgumentsContext,
source: Option<Source>,
cx: &mut Context<Self>,
) -> Task<Result<EvaluateResponse>> {
let Some(client) = self
.session_by_id(session_id)
.and_then(|client| client.read(cx).adapter_client())
else {
return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id)));
};
cx.background_executor().spawn(async move {
client
.request::<Evaluate>(EvaluateArguments {
expression: expression.clone(),
frame_id: Some(stack_frame_id),
context: Some(context),
format: None,
line: None,
column: None,
source,
})
.await
})
}
pub fn completions(
&self,
session_id: &SessionId,
stack_frame_id: u64,
text: String,
completion_column: u64,
cx: &mut Context<Self>,
) -> Task<Result<Vec<CompletionItem>>> {
let Some(client) = self
.session_by_id(session_id)
.and_then(|client| client.read(cx).adapter_client())
else {
return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id)));
};
cx.background_executor().spawn(async move {
Ok(client
.request::<Completions>(CompletionsArguments {
frame_id: Some(stack_frame_id),
line: None,
text,
column: completion_column,
})
.await?
.targets)
})
}
#[allow(clippy::too_many_arguments)]
pub fn set_variable_value(
&self,
session_id: &SessionId,
stack_frame_id: u64,
variables_reference: u64,
name: String,
value: String,
evaluate_name: Option<String>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(client) = self
.session_by_id(session_id)
.and_then(|client| client.read(cx).adapter_client())
else {
return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id)));
};
let supports_set_expression = self
.capabilities_by_id(session_id, cx)
.and_then(|caps| caps.supports_set_expression)
.unwrap_or_default();
cx.background_executor().spawn(async move {
if let Some(evaluate_name) = supports_set_expression.then(|| evaluate_name).flatten() {
client
.request::<SetExpression>(SetExpressionArguments {
expression: evaluate_name,
value,
frame_id: Some(stack_frame_id),
format: None,
})
.await?;
} else {
client
.request::<SetVariable>(SetVariableArguments {
variables_reference,
name,
value,
format: None,
})
.await?;
}
Ok(())
})
}
// .. get the client and what not
// let _ = client.modules(); // This can fire a request to a dap adapter or be a cheap getter.
// client.wait_for_request(request::Modules); // This ensures that the request that we've fired off runs to completions
// let returned_value = client.modules(); // this is a cheap getter.
pub fn shutdown_sessions(&mut self, cx: &mut Context<Self>) -> Task<()> {
let mut tasks = vec![];
for session_id in self.sessions.keys().cloned().collect::<Vec<_>>() {
tasks.push(self.shutdown_session(session_id, cx));
}
cx.background_executor().spawn(async move {
futures::future::join_all(tasks).await;
})
}
pub fn shutdown_session(
&mut self,
session_id: SessionId,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(_) = self.as_local_mut() else {
return Task::ready(Err(anyhow!("Cannot shutdown session on remote side")));
};
let Some(session) = self.sessions.remove(&session_id) else {
return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id)));
};
let shutdown_parent_task = session
.read(cx)
.parent_id()
.map(|parent_id| self.shutdown_session(parent_id, cx));
let shutdown_task = session.update(cx, |this, cx| this.shutdown(cx));
cx.background_spawn(async move {
shutdown_task.await;
if let Some(parent_task) = shutdown_parent_task {
parent_task.await?;
}
Ok(())
})
}
pub fn shared(
&mut self,
project_id: u64,
downstream_client: AnyProtoClient,
_: &mut Context<Self>,
) {
self.downstream_client = Some((downstream_client.clone(), project_id));
}
pub fn unshared(&mut self, cx: &mut Context<Self>) {
self.downstream_client.take();
cx.notify();
}
}
#[derive(Clone)]
pub struct DapAdapterDelegate {
fs: Arc<dyn Fs>,
worktree_id: WorktreeId,
node_runtime: NodeRuntime,
http_client: Arc<dyn HttpClient>,
language_registry: Arc<LanguageRegistry>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
updated_adapters: Arc<Mutex<HashSet<DebugAdapterName>>>,
load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
}
impl DapAdapterDelegate {
pub fn new(
fs: Arc<dyn Fs>,
worktree_id: WorktreeId,
node_runtime: NodeRuntime,
http_client: Arc<dyn HttpClient>,
language_registry: Arc<LanguageRegistry>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
) -> Self {
Self {
fs,
worktree_id,
http_client,
node_runtime,
toolchain_store,
language_registry,
load_shell_env_task,
updated_adapters: Default::default(),
}
}
}
#[async_trait(?Send)]
impl dap::adapters::DapDelegate for DapAdapterDelegate {
fn worktree_id(&self) -> WorktreeId {
self.worktree_id
}
fn http_client(&self) -> Arc<dyn HttpClient> {
self.http_client.clone()
}
fn node_runtime(&self) -> NodeRuntime {
self.node_runtime.clone()
}
fn fs(&self) -> Arc<dyn Fs> {
self.fs.clone()
}
fn updated_adapters(&self) -> Arc<Mutex<HashSet<DebugAdapterName>>> {
self.updated_adapters.clone()
}
fn update_status(&self, dap_name: DebugAdapterName, status: dap::adapters::DapStatus) {
let name = SharedString::from(dap_name.to_string());
let status = match status {
DapStatus::None => BinaryStatus::None,
DapStatus::Downloading => BinaryStatus::Downloading,
DapStatus::Failed { error } => BinaryStatus::Failed { error },
DapStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate,
};
self.language_registry
.update_dap_status(LanguageServerName(name), status);
}
fn which(&self, command: &OsStr) -> Option<PathBuf> {
which::which(command).ok()
}
async fn shell_env(&self) -> HashMap<String, String> {
let task = self.load_shell_env_task.clone();
task.await.unwrap_or_default()
}
fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
self.toolchain_store.clone()
}
}

File diff suppressed because it is too large Load diff

View file

@ -40,10 +40,10 @@ use language::{
},
point_to_lsp,
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel,
Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, File as _, Language, LanguageRegistry,
LanguageServerBinaryStatus, LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate,
Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
range_from_lsp, range_to_lsp, Bias, BinaryStatus, Buffer, BufferSnapshot, CachedLspAdapter,
CodeLabel, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, File as _, Language,
LanguageRegistry, LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch,
PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
};
use lsp::{
notification::DidRenameFiles, CodeActionKind, CompletionContext, DiagnosticSeverity,
@ -361,7 +361,7 @@ impl LocalLspStore {
let log = stderr_capture.lock().take().unwrap_or_default();
delegate.update_status(
adapter.name(),
LanguageServerBinaryStatus::Failed {
BinaryStatus::Failed {
error: format!("{err}\n-- stderr--\n{}", log),
},
);
@ -437,7 +437,7 @@ impl LocalLspStore {
)
.await;
delegate.update_status(adapter.name.clone(), LanguageServerBinaryStatus::None);
delegate.update_status(adapter.name.clone(), BinaryStatus::None);
let mut binary = binary_result?;
if let Some(arguments) = settings.and_then(|b| b.arguments) {
@ -9434,11 +9434,7 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate {
))
}
fn update_status(
&self,
server_name: LanguageServerName,
status: language::LanguageServerBinaryStatus,
) {
fn update_status(&self, server_name: LanguageServerName, status: language::BinaryStatus) {
self.language_registry
.update_lsp_status(server_name, status);
}

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);

View file

@ -1,11 +1,12 @@
use anyhow::Context as _;
use collections::HashMap;
use dap::adapters::DebugAdapterName;
use fs::Fs;
use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter};
use lsp::LanguageServerName;
use paths::{
local_settings_file_relative_path, local_tasks_file_relative_path,
local_vscode_tasks_file_relative_path, EDITORCONFIG_NAME,
local_debug_file_relative_path, local_settings_file_relative_path,
local_tasks_file_relative_path, local_vscode_tasks_file_relative_path, EDITORCONFIG_NAME,
};
use rpc::{
proto::{self, FromProto, ToProto},
@ -15,7 +16,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{
parse_json_with_comments, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation,
SettingsSources, SettingsStore,
SettingsSources, SettingsStore, TaskKind,
};
use std::{path::Path, sync::Arc, time::Duration};
use task::{TaskTemplates, VsCodeTaskFile};
@ -40,6 +41,10 @@ pub struct ProjectSettings {
#[serde(default)]
pub lsp: HashMap<LanguageServerName, LspSettings>,
/// Configuration for Debugger-related features
#[serde(default)]
pub dap: HashMap<DebugAdapterName, DapSettings>,
/// Configuration for Diagnostics-related features.
#[serde(default)]
pub diagnostics: DiagnosticsSettings,
@ -61,6 +66,12 @@ pub struct ProjectSettings {
pub session: SessionSettings,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct DapSettings {
pub binary: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct NodeBinarySettings {
/// The path to the Node binary.
@ -483,7 +494,7 @@ impl SettingsObserver {
)
.unwrap(),
);
(settings_dir, LocalSettingsKind::Tasks)
(settings_dir, LocalSettingsKind::Tasks(TaskKind::Script))
} else if path.ends_with(local_vscode_tasks_file_relative_path()) {
let settings_dir = Arc::<Path>::from(
path.ancestors()
@ -495,7 +506,19 @@ impl SettingsObserver {
)
.unwrap(),
);
(settings_dir, LocalSettingsKind::Tasks)
(settings_dir, LocalSettingsKind::Tasks(TaskKind::Script))
} else if path.ends_with(local_debug_file_relative_path()) {
let settings_dir = Arc::<Path>::from(
path.ancestors()
.nth(
local_debug_file_relative_path()
.components()
.count()
.saturating_sub(1),
)
.unwrap(),
);
(settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug))
} else if path.ends_with(EDITORCONFIG_NAME) {
let Some(settings_dir) = path.parent().map(Arc::from) else {
continue;
@ -616,7 +639,7 @@ impl SettingsObserver {
}
}
}),
LocalSettingsKind::Tasks => task_store.update(cx, |task_store, cx| {
LocalSettingsKind::Tasks(task_kind) => task_store.update(cx, |task_store, cx| {
task_store
.update_user_tasks(
Some(SettingsLocation {
@ -624,6 +647,7 @@ impl SettingsObserver {
path: directory.as_ref(),
}),
file_content.as_deref(),
task_kind,
cx,
)
.log_err();
@ -648,7 +672,7 @@ impl SettingsObserver {
pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
match kind {
proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks(TaskKind::Script),
proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
}
}
@ -656,7 +680,7 @@ pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSe
pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
match kind {
LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
LocalSettingsKind::Tasks(_) => proto::LocalSettingsKind::Tasks,
LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
}
}

View file

@ -341,6 +341,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
}])
.to_string(),
),
settings::TaskKind::Script,
)
.unwrap();
});

View file

@ -13,9 +13,10 @@ use collections::{HashMap, HashSet, VecDeque};
use gpui::{App, AppContext as _, Entity, SharedString, Task};
use itertools::Itertools;
use language::{ContextProvider, File, Language, LanguageToolchainStore, Location};
use settings::{parse_json_with_comments, SettingsLocation};
use settings::{parse_json_with_comments, SettingsLocation, TaskKind};
use task::{
ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, TaskVariables, VariableName,
DebugTaskDefinition, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates,
TaskVariables, VariableName,
};
use text::{Point, ToPoint};
use util::{paths::PathExt as _, post_inc, NumericPrefixWithSuffix, ResultExt as _};
@ -33,7 +34,7 @@ pub struct Inventory {
#[derive(Debug, Default)]
struct ParsedTemplates {
global: Vec<TaskTemplate>,
worktree: HashMap<WorktreeId, HashMap<Arc<Path>, Vec<TaskTemplate>>>,
worktree: HashMap<WorktreeId, HashMap<(Arc<Path>, TaskKind), Vec<TaskTemplate>>>,
}
/// Kind of a source the tasks are fetched from, used to display more source information in the UI.
@ -328,8 +329,14 @@ impl Inventory {
.map(|template| {
(
TaskSourceKind::AbsPath {
id_base: Cow::Borrowed("global tasks.json"),
abs_path: paths::tasks_file().clone(),
id_base: match template.task_type {
task::TaskType::Script => Cow::Borrowed("global tasks.json"),
task::TaskType::Debug(_) => Cow::Borrowed("global debug.json"),
},
abs_path: match template.task_type {
task::TaskType::Script => paths::tasks_file().clone(),
task::TaskType::Debug(_) => paths::debug_tasks_file().clone(),
},
},
template,
)
@ -349,7 +356,7 @@ impl Inventory {
.flat_map(|(directory, templates)| {
templates.iter().map(move |template| (directory, template))
})
.map(move |(directory, template)| {
.map(move |((directory, _task_kind), template)| {
(
TaskSourceKind::Worktree {
id: worktree,
@ -372,13 +379,19 @@ impl Inventory {
&mut self,
location: Option<SettingsLocation<'_>>,
raw_tasks_json: Option<&str>,
task_kind: TaskKind,
) -> anyhow::Result<()> {
let raw_tasks =
parse_json_with_comments::<Vec<serde_json::Value>>(raw_tasks_json.unwrap_or("[]"))
.context("parsing tasks file content as a JSON array")?;
let new_templates = raw_tasks.into_iter().filter_map(|raw_template| {
serde_json::from_value::<TaskTemplate>(raw_template).log_err()
});
let new_templates = raw_tasks
.into_iter()
.filter_map(|raw_template| match &task_kind {
TaskKind::Script => serde_json::from_value::<TaskTemplate>(raw_template).log_err(),
TaskKind::Debug => serde_json::from_value::<DebugTaskDefinition>(raw_template)
.log_err()
.and_then(|content| content.to_zed_format().log_err()),
});
let parsed_templates = &mut self.templates_from_settings;
match location {
@ -388,14 +401,14 @@ impl Inventory {
if let Some(worktree_tasks) =
parsed_templates.worktree.get_mut(&location.worktree_id)
{
worktree_tasks.remove(location.path);
worktree_tasks.remove(&(Arc::from(location.path), task_kind));
}
} else {
parsed_templates
.worktree
.entry(location.worktree_id)
.or_default()
.insert(Arc::from(location.path), new_templates);
.insert((Arc::from(location.path), task_kind), new_templates);
}
}
None => parsed_templates.global = new_templates.collect(),
@ -675,6 +688,7 @@ mod tests {
Some(&mock_tasks_from_names(
expected_initial_state.iter().map(|name| name.as_str()),
)),
settings::TaskKind::Script,
)
.unwrap();
});
@ -730,6 +744,7 @@ mod tests {
.into_iter()
.chain(expected_initial_state.iter().map(|name| name.as_str())),
)),
settings::TaskKind::Script,
)
.unwrap();
});
@ -854,6 +869,7 @@ mod tests {
.iter()
.map(|(_, name)| name.as_str()),
)),
settings::TaskKind::Script,
)
.unwrap();
inventory
@ -865,6 +881,7 @@ mod tests {
Some(&mock_tasks_from_names(
worktree_1_tasks.iter().map(|(_, name)| name.as_str()),
)),
settings::TaskKind::Script,
)
.unwrap();
inventory
@ -876,6 +893,7 @@ mod tests {
Some(&mock_tasks_from_names(
worktree_2_tasks.iter().map(|(_, name)| name.as_str()),
)),
settings::TaskKind::Script,
)
.unwrap();
});

View file

@ -10,7 +10,7 @@ use language::{
ContextProvider as _, LanguageToolchainStore, Location,
};
use rpc::{proto, AnyProtoClient, TypedEnvelope};
use settings::{watch_config_file, SettingsLocation};
use settings::{watch_config_file, SettingsLocation, TaskKind};
use task::{TaskContext, TaskVariables, VariableName};
use text::{BufferId, OffsetRangeExt};
use util::ResultExt;
@ -32,7 +32,7 @@ pub struct StoreState {
buffer_store: WeakEntity<BufferStore>,
worktree_store: Entity<WorktreeStore>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
_global_task_config_watcher: Task<()>,
_global_task_config_watchers: (Task<()>, Task<()>),
}
enum StoreMode {
@ -168,7 +168,20 @@ impl TaskStore {
buffer_store,
toolchain_store,
worktree_store,
_global_task_config_watcher: Self::subscribe_to_global_task_file_changes(fs, cx),
_global_task_config_watchers: (
Self::subscribe_to_global_task_file_changes(
fs.clone(),
TaskKind::Script,
paths::tasks_file().clone(),
cx,
),
Self::subscribe_to_global_task_file_changes(
fs.clone(),
TaskKind::Debug,
paths::debug_tasks_file().clone(),
cx,
),
),
})
}
@ -190,7 +203,20 @@ impl TaskStore {
buffer_store,
toolchain_store,
worktree_store,
_global_task_config_watcher: Self::subscribe_to_global_task_file_changes(fs, cx),
_global_task_config_watchers: (
Self::subscribe_to_global_task_file_changes(
fs.clone(),
TaskKind::Script,
paths::tasks_file().clone(),
cx,
),
Self::subscribe_to_global_task_file_changes(
fs.clone(),
TaskKind::Debug,
paths::debug_tasks_file().clone(),
cx,
),
),
})
}
@ -262,6 +288,7 @@ impl TaskStore {
&self,
location: Option<SettingsLocation<'_>>,
raw_tasks_json: Option<&str>,
task_type: TaskKind,
cx: &mut Context<'_, Self>,
) -> anyhow::Result<()> {
let task_inventory = match self {
@ -273,22 +300,23 @@ impl TaskStore {
.filter(|json| !json.is_empty());
task_inventory.update(cx, |inventory, _| {
inventory.update_file_based_tasks(location, raw_tasks_json)
inventory.update_file_based_tasks(location, raw_tasks_json, task_type)
})
}
fn subscribe_to_global_task_file_changes(
fs: Arc<dyn Fs>,
task_kind: TaskKind,
file_path: PathBuf,
cx: &mut Context<'_, Self>,
) -> Task<()> {
let mut user_tasks_file_rx =
watch_config_file(&cx.background_executor(), fs, paths::tasks_file().clone());
let mut user_tasks_file_rx = watch_config_file(&cx.background_executor(), fs, file_path);
let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
cx.spawn(move |task_store, mut cx| async move {
if let Some(user_tasks_content) = user_tasks_content {
let Ok(_) = task_store.update(&mut cx, |task_store, cx| {
task_store
.update_user_tasks(None, Some(&user_tasks_content), cx)
.update_user_tasks(None, Some(&user_tasks_content), task_kind, cx)
.log_err();
}) else {
return;
@ -296,12 +324,17 @@ impl TaskStore {
}
while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
let Ok(()) = task_store.update(&mut cx, |task_store, cx| {
let result = task_store.update_user_tasks(None, Some(&user_tasks_content), cx);
let result = task_store.update_user_tasks(
None,
Some(&user_tasks_content),
task_kind,
cx,
);
if let Err(err) = &result {
log::error!("Failed to load user tasks: {err}");
log::error!("Failed to load user {:?} tasks: {err}", task_kind);
cx.emit(crate::Event::Toast {
notification_id: "load-user-tasks".into(),
message: format!("Invalid global tasks file\n{err}"),
notification_id: format!("load-user-{:?}-tasks", task_kind).into(),
message: format!("Invalid global {:?} tasks file\n{err}", task_kind),
});
}
cx.refresh_windows();

View file

@ -35,6 +35,14 @@ pub enum TerminalKind {
Shell(Option<PathBuf>),
/// Run a task.
Task(SpawnInTerminal),
/// Run a debug terminal.
Debug {
command: Option<String>,
args: Vec<String>,
envs: HashMap<String, String>,
cwd: PathBuf,
title: Option<String>,
},
}
/// SshCommand describes how to connect to a remote server
@ -93,6 +101,7 @@ impl Project {
self.active_project_directory(cx)
}
}
TerminalKind::Debug { cwd, .. } => Some(Arc::from(cwd.as_path())),
};
let mut settings_location = None;
@ -196,6 +205,7 @@ impl Project {
this.active_project_directory(cx)
}
}
TerminalKind::Debug { cwd, .. } => Some(Arc::from(cwd.as_path())),
};
let ssh_details = this.ssh_details(cx);
@ -229,6 +239,7 @@ impl Project {
};
let mut python_venv_activate_command = None;
let debug_terminal = matches!(kind, TerminalKind::Debug { .. });
let (spawn_task, shell) = match kind {
TerminalKind::Shell(_) => {
@ -324,6 +335,27 @@ impl Project {
}
}
}
TerminalKind::Debug {
command,
args,
envs,
title,
..
} => {
env.extend(envs);
let shell = if let Some(program) = command {
Shell::WithArguments {
program,
args,
title_override: Some(title.unwrap_or("Debug Terminal".into()).into()),
}
} else {
settings.shell.clone()
};
(None, shell)
}
};
TerminalBuilder::new(
local_path.map(|path| path.to_path_buf()),
@ -337,6 +369,7 @@ impl Project {
ssh_details.is_some(),
window,
completion_tx,
debug_terminal,
cx,
)
.map(|builder| {