Stop routing session events via the DAP store (#29588)

This cleans up a bunch of indirection and will make it easier to
show the session building state in the debugger terminal

Closes #ISSUE

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin 2025-04-29 13:51:05 -06:00 committed by GitHub
parent fde1cc78a1
commit 15a83b5a10
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 267 additions and 425 deletions

View file

@ -15,20 +15,16 @@ use async_trait::async_trait;
use collections::HashMap;
use dap::{
Capabilities, CompletionItem, CompletionsArguments, DapRegistry, DebugRequest,
EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, RunInTerminalRequestArguments,
Source, StackFrameId, StartDebuggingRequestArguments,
EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, Source, StackFrameId,
adapters::{
DapStatus, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments,
},
client::SessionId,
messages::Message,
requests::{Completions, Evaluate, Request as _, RunInTerminal, StartDebugging},
requests::{Completions, Evaluate},
};
use fs::Fs;
use futures::{
channel::mpsc,
future::{Shared, join_all},
};
use futures::future::{Shared, join_all};
use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};
use http_client::HttpClient;
use language::{
@ -43,9 +39,8 @@ use rpc::{
AnyProtoClient, TypedEnvelope,
proto::{self},
};
use serde_json::Value;
use settings::{Settings, WorktreeId};
use smol::{lock::Mutex, stream::StreamExt};
use smol::lock::Mutex;
use std::{
borrow::Borrow,
collections::{BTreeMap, HashSet},
@ -67,19 +62,6 @@ pub enum DapStoreEvent {
session_id: SessionId,
message: Message,
},
RunInTerminal {
session_id: SessionId,
title: Option<String>,
cwd: Option<Arc<Path>>,
command: Option<String>,
args: Vec<String>,
envs: HashMap<String, String>,
sender: mpsc::Sender<Result<u32>>,
},
SpawnChildSession {
request: StartDebuggingRequestArguments,
parent_session: Entity<Session>,
},
Notification(String),
RemoteHasInitialized,
}
@ -113,8 +95,6 @@ pub struct DapStore {
worktree_store: Entity<WorktreeStore>,
sessions: BTreeMap<SessionId, Entity<Session>>,
next_session_id: u32,
start_debugging_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
_start_debugging_task: Task<()>,
}
impl EventEmitter<DapStoreEvent> for DapStore {}
@ -184,35 +164,10 @@ impl DapStore {
mode: DapStoreMode,
breakpoint_store: Entity<BreakpointStore>,
worktree_store: Entity<WorktreeStore>,
cx: &mut Context<Self>,
_cx: &mut Context<Self>,
) -> Self {
let (start_debugging_tx, mut message_rx) =
futures::channel::mpsc::unbounded::<(SessionId, Message)>();
let task = cx.spawn(async move |this, cx| {
while let Some((session_id, message)) = message_rx.next().await {
match message {
Message::Request(request) => {
let _ = this
.update(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,
_start_debugging_task: task,
start_debugging_tx,
next_session_id: 0,
downstream_client: None,
breakpoint_store,
@ -450,14 +405,11 @@ impl DapStore {
});
}
let start_debugging_tx = self.start_debugging_tx.clone();
let session = Session::new(
self.breakpoint_store.clone(),
session_id,
parent_session,
template.clone(),
start_debugging_tx,
cx,
);
@ -469,7 +421,7 @@ impl DapStore {
SessionStateEvent::Shutdown => {
this.shutdown_session(session_id, cx).detach_and_log_err(cx);
}
SessionStateEvent::Restart => {}
SessionStateEvent::Restart | SessionStateEvent::SpawnChildSession { .. } => {}
SessionStateEvent::Running => {
cx.emit(DapStoreEvent::DebugClientStarted(session_id));
}
@ -583,196 +535,6 @@ impl DapStore {
)
}
fn handle_start_debugging_request(
&mut self,
session_id: SessionId,
request: dap::messages::Request,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(parent_session) = self.session_by_id(session_id) else {
return Task::ready(Err(anyhow!("Session not found")));
};
let request_seq = request.seq;
let launch_request: Option<Result<StartDebuggingRequestArguments, _>> = request
.arguments
.as_ref()
.map(|value| serde_json::from_value(value.clone()));
let mut success = true;
if let Some(Ok(request)) = launch_request {
cx.emit(DapStoreEvent::SpawnChildSession {
request,
parent_session: parent_session.clone(),
});
} else {
log::error!(
"Failed to parse launch request arguments: {:?}",
request.arguments
);
success = false;
}
cx.spawn(async move |_, cx| {
parent_session
.update(cx, |session, cx| {
session.respond_to_client(
request_seq,
success,
StartDebugging::COMMAND.to_string(),
None,
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 = Path::new(&request_args.cwd);
match cwd.try_exists() {
Ok(false) | Err(_) if !request_args.cwd.is_empty() => {
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);
let cwd = Some(cwd)
.filter(|cwd| cwd.as_os_str().len() > 0)
.map(Arc::from)
.or_else(|| {
self.session_by_id(session_id)
.and_then(|session| session.read(cx).binary().cwd.as_deref().map(Arc::from))
});
cx.emit(DapStoreEvent::RunInTerminal {
session_id,
title: request_args.title,
cwd,
command,
args,
envs,
sender: tx,
});
cx.notify();
let session = session.downgrade();
cx.spawn(async move |_, cx| {
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(cx, |session, cx| {
session.respond_to_client(
seq,
success,
RunInTerminal::COMMAND.to_string(),
body,
cx,
)
})?
.await
})
}
pub fn evaluate(
&self,
session_id: &SessionId,