Tidy up DAP initialization (#28730)

To make DAP work over SSH we want to create the binary
at the project level (so we can wrap it in an `ssh` invocation
transparently).

This means not pushing the adapter down into the session, and resolving
more information ahead-of-time.

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Piotr <piotr@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Piotr <piotr@zed.dev>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Anthony <anthony@zed.dev>
This commit is contained in:
Conrad Irwin 2025-04-15 09:11:29 -06:00 committed by GitHub
parent 6f0951ff77
commit aef78dcffd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1319 additions and 1738 deletions

View file

@ -16,3 +16,6 @@ pub mod dap_command;
pub mod dap_store;
mod locator_store;
pub mod session;
#[cfg(any(feature = "test-support", test))]
pub mod test;

View file

@ -3,15 +3,15 @@ use super::{
locator_store::LocatorStore,
session::{self, Session, SessionStateEvent},
};
use crate::{ProjectEnvironment, debugger, worktree_store::WorktreeStore};
use crate::{ProjectEnvironment, debugger};
use anyhow::{Result, anyhow};
use async_trait::async_trait;
use collections::HashMap;
use dap::{
Capabilities, CompletionItem, CompletionsArguments, DapRegistry, ErrorResponse,
EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, RunInTerminalRequestArguments,
Source, StartDebuggingRequestArguments,
adapters::{DapStatus, DebugAdapterName},
Capabilities, CompletionItem, CompletionsArguments, ErrorResponse, EvaluateArguments,
EvaluateArgumentsContext, EvaluateResponse, RunInTerminalRequestArguments, Source,
StartDebuggingRequestArguments,
adapters::{DapStatus, DebugAdapterBinary, DebugAdapterName},
client::SessionId,
messages::Message,
requests::{Completions, Evaluate, Request as _, RunInTerminal, StartDebugging},
@ -42,7 +42,7 @@ use std::{
sync::{Arc, atomic::Ordering::SeqCst},
};
use std::{collections::VecDeque, sync::atomic::AtomicU32};
use task::{DebugAdapterConfig, DebugRequestDisposition};
use task::DebugTaskDefinition;
use util::ResultExt as _;
use worktree::Worktree;
@ -78,10 +78,8 @@ pub struct LocalDapStore {
node_runtime: NodeRuntime,
next_session_id: AtomicU32,
http_client: Arc<dyn HttpClient>,
worktree_store: Entity<WorktreeStore>,
environment: Entity<ProjectEnvironment>,
language_registry: Arc<LanguageRegistry>,
debug_adapters: Arc<DapRegistry>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
locator_store: Arc<LocatorStore>,
start_debugging_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
@ -132,11 +130,9 @@ impl DapStore {
node_runtime: NodeRuntime,
fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>,
debug_adapters: Arc<DapRegistry>,
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();
@ -170,10 +166,8 @@ impl DapStore {
environment,
http_client,
node_runtime,
worktree_store,
toolchain_store,
language_registry,
debug_adapters,
start_debugging_tx,
_start_debugging_task,
locator_store: Arc::from(LocatorStore::new()),
@ -320,18 +314,12 @@ impl DapStore {
Ok(())
}
pub fn new_session(
&mut self,
mut config: DebugAdapterConfig,
worktree: &Entity<Worktree>,
parent_session: Option<Entity<Session>>,
cx: &mut Context<Self>,
) -> (SessionId, Task<Result<Entity<Session>>>) {
pub fn delegate(&self, worktree: &Entity<Worktree>, cx: &mut App) -> DapAdapterDelegate {
let Some(local_store) = self.as_local() else {
unimplemented!("Starting session on remote side");
};
let delegate = DapAdapterDelegate::new(
DapAdapterDelegate::new(
local_store.fs.clone(),
worktree.read(cx).id(),
local_store.node_runtime.clone(),
@ -341,7 +329,20 @@ impl DapStore {
local_store.environment.update(cx, |env, cx| {
env.get_worktree_environment(worktree.clone(), cx)
}),
);
)
}
pub fn new_session(
&mut self,
binary: DebugAdapterBinary,
mut config: DebugTaskDefinition,
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 session_id = local_store.next_session_id();
if let Some(session) = &parent_session {
@ -352,7 +353,6 @@ impl DapStore {
let (initialized_tx, initialized_rx) = oneshot::channel();
let locator_store = local_store.locator_store.clone();
let debug_adapters = local_store.debug_adapters.clone();
let start_debugging_tx = local_store.start_debugging_tx.clone();
@ -373,86 +373,31 @@ impl DapStore {
this.breakpoint_store.clone(),
session_id,
parent_session,
delegate,
binary,
config,
start_debugging_tx.clone(),
initialized_tx,
debug_adapters,
cx,
)
})?;
this.update(cx, |_, cx| {
create_new_session(session_id, initialized_rx, start_client_task, cx)
})?
.await
let ret = this
.update(cx, |_, cx| {
create_new_session(session_id, initialized_rx, start_client_task, cx)
})?
.await;
ret
});
(session_id, task)
}
#[cfg(any(test, feature = "test-support"))]
pub fn new_fake_session(
&mut self,
config: DebugAdapterConfig,
worktree: &Entity<Worktree>,
parent_session: Option<Entity<Session>>,
caps: Capabilities,
fails: bool,
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| {
env.get_worktree_environment(worktree.clone(), cx)
}),
);
let session_id = local_store.next_session_id();
if let Some(session) = &parent_session {
session.update(cx, |session, _| {
session.add_child_session_id(session_id);
});
}
let (initialized_tx, initialized_rx) = oneshot::channel();
let start_client_task = Session::fake(
self.breakpoint_store.clone(),
session_id,
parent_session,
delegate,
config,
local_store.start_debugging_tx.clone(),
initialized_tx,
caps,
fails,
cx,
);
let task = create_new_session(session_id, initialized_rx, start_client_task, cx);
(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")));
};
@ -461,41 +406,12 @@ impl DapStore {
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 mut binary = parent_session.read(cx).binary().clone();
let config = parent_session.read(cx).configuration().unwrap().clone();
binary.request_args = args;
let Some(config) = parent_session.read(cx).configuration() else {
unreachable!("there must be a config for local sessions");
};
let debug_config = DebugAdapterConfig {
label: config.label,
adapter: config.adapter,
request: DebugRequestDisposition::ReverseRequest(args),
initialize_args: config.initialize_args.clone(),
tcp_connection: config.tcp_connection.clone(),
locator: None,
stop_on_entry: config.stop_on_entry,
};
#[cfg(any(test, feature = "test-support"))]
let new_session_task = {
let caps = parent_session.read(cx).capabilities.clone();
self.new_fake_session(
debug_config,
&worktree,
Some(parent_session.clone()),
caps,
false,
cx,
)
.1
};
#[cfg(not(any(test, feature = "test-support")))]
let new_session_task = self
.new_session(debug_config, &worktree, Some(parent_session.clone()), cx)
.new_session(binary, config, Some(parent_session.clone()), cx)
.1;
let request_seq = request.seq;
@ -607,13 +523,7 @@ impl DapStore {
.map(Arc::from)
.or_else(|| {
self.session_by_id(session_id)
.and_then(|session| {
session
.read(cx)
.configuration()
.and_then(|config| config.request.cwd())
})
.map(Arc::from)
.and_then(|session| session.read(cx).binary().cwd.as_deref().map(Arc::from))
});
cx.emit(DapStoreEvent::RunInTerminal {
session_id,
@ -852,7 +762,7 @@ fn create_new_session(
cx.emit(DapStoreEvent::DebugClientStarted(session_id));
cx.notify();
})?;
let seq_result = {
let seq_result = async || {
session
.update(cx, |session, cx| session.request_initialize(cx))?
.await?;
@ -863,12 +773,11 @@ fn create_new_session(
})?
.await
};
match seq_result {
match seq_result().await {
Ok(_) => {}
Err(error) => {
this.update(cx, |this, cx| {
cx.emit(DapStoreEvent::Notification(error.to_string()));
this.shutdown_session(session_id, cx)
})?
.await

View file

@ -1,9 +1,9 @@
use anyhow::{Result, anyhow};
use cargo::CargoLocator;
use collections::HashMap;
use dap::DebugAdapterConfig;
use gpui::SharedString;
use locators::DapLocator;
use task::DebugTaskDefinition;
mod cargo;
mod locators;
@ -23,7 +23,7 @@ impl LocatorStore {
pub(super) async fn resolve_debug_config(
&self,
debug_config: &mut DebugAdapterConfig,
debug_config: &mut DebugTaskDefinition,
) -> Result<()> {
let Some(locator_name) = &debug_config.locator else {
log::debug!("Attempted to resolve debug config without a locator field");

View file

@ -1,12 +1,12 @@
use super::DapLocator;
use anyhow::{Result, anyhow};
use async_trait::async_trait;
use dap::DebugAdapterConfig;
use serde_json::{Value, json};
use smol::{
io::AsyncReadExt,
process::{Command, Stdio},
};
use task::DebugTaskDefinition;
use util::maybe;
pub(super) struct CargoLocator;
@ -38,11 +38,9 @@ async fn find_best_executable(executables: &[String], test_name: &str) -> Option
}
#[async_trait]
impl DapLocator for CargoLocator {
async fn run_locator(&self, debug_config: &mut DebugAdapterConfig) -> Result<()> {
async fn run_locator(&self, debug_config: &mut DebugTaskDefinition) -> Result<()> {
let Some(launch_config) = (match &mut debug_config.request {
task::DebugRequestDisposition::UserConfigured(task::DebugRequestType::Launch(
launch_config,
)) => Some(launch_config),
task::DebugRequestType::Launch(launch_config) => Some(launch_config),
_ => None,
}) else {
return Err(anyhow!("Couldn't get launch config in locator"));

View file

@ -1,8 +1,8 @@
use anyhow::Result;
use async_trait::async_trait;
use dap::DebugAdapterConfig;
use task::DebugTaskDefinition;
#[async_trait]
pub(super) trait DapLocator: Send + Sync {
async fn run_locator(&self, debug_config: &mut DebugAdapterConfig) -> Result<()>;
async fn run_locator(&self, debug_config: &mut DebugTaskDefinition) -> Result<()>;
}

View file

@ -1,5 +1,3 @@
use crate::project_settings::ProjectSettings;
use super::breakpoint_store::{
BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason, SourceBreakpoint,
};
@ -11,22 +9,17 @@ use super::dap_command::{
StepBackCommand, StepCommand, StepInCommand, StepOutCommand, TerminateCommand,
TerminateThreadsCommand, ThreadsCommand, VariablesCommand,
};
use super::dap_store::DapAdapterDelegate;
use anyhow::{Context as _, Result, anyhow};
use collections::{HashMap, HashSet, IndexMap, IndexSet};
use dap::adapters::{DebugAdapter, DebugAdapterBinary};
use dap::adapters::DebugAdapterBinary;
use dap::messages::Response;
use dap::{
Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, StackFrameId,
SteppingGranularity, StoppedEvent, VariableReference,
adapters::{DapDelegate, DapStatus},
client::{DebugAdapterClient, SessionId},
messages::{Events, Message},
};
use dap::{
DapRegistry, DebugRequestType, ExceptionBreakpointsFilter, ExceptionFilterOptions,
OutputEventCategory,
};
use dap::{ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEventCategory};
use futures::channel::oneshot;
use futures::{FutureExt, future::Shared};
use gpui::{
@ -35,11 +28,9 @@ use gpui::{
};
use rpc::AnyProtoClient;
use serde_json::{Value, json};
use settings::Settings;
use smol::stream::StreamExt;
use std::any::TypeId;
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::u64;
use std::{
any::Any,
@ -48,7 +39,7 @@ use std::{
path::Path,
sync::Arc,
};
use task::{DebugAdapterConfig, DebugTaskDefinition};
use task::DebugTaskDefinition;
use text::{PointUtf16, ToPointUtf16};
use util::{ResultExt, merge_json_value_into};
@ -168,9 +159,9 @@ enum Mode {
#[derive(Clone)]
pub struct LocalMode {
client: Arc<DebugAdapterClient>,
config: DebugAdapterConfig,
adapter: Arc<dyn DebugAdapter>,
breakpoint_store: Entity<BreakpointStore>,
definition: DebugTaskDefinition,
binary: DebugAdapterBinary,
pub(crate) breakpoint_store: Entity<BreakpointStore>,
tmp_breakpoint: Option<SourceBreakpoint>,
}
@ -191,185 +182,37 @@ fn client_source(abs_path: &Path) -> dap::Source {
impl LocalMode {
fn new(
debug_adapters: Arc<DapRegistry>,
session_id: SessionId,
parent_session: Option<Entity<Session>>,
breakpoint_store: Entity<BreakpointStore>,
config: DebugAdapterConfig,
delegate: DapAdapterDelegate,
config: DebugTaskDefinition,
binary: DebugAdapterBinary,
messages_tx: futures::channel::mpsc::UnboundedSender<Message>,
cx: AsyncApp,
) -> Task<Result<Self>> {
Self::new_inner(
debug_adapters,
session_id,
parent_session,
breakpoint_store,
config,
delegate,
binary,
messages_tx,
async |_, _| {},
cx,
)
}
#[cfg(any(test, feature = "test-support"))]
fn new_fake(
session_id: SessionId,
parent_session: Option<Entity<Session>>,
breakpoint_store: Entity<BreakpointStore>,
config: DebugAdapterConfig,
delegate: DapAdapterDelegate,
messages_tx: futures::channel::mpsc::UnboundedSender<Message>,
caps: Capabilities,
fail: bool,
cx: AsyncApp,
) -> Task<Result<Self>> {
use task::DebugRequestDisposition;
let request = match config.request.clone() {
DebugRequestDisposition::UserConfigured(request) => request,
DebugRequestDisposition::ReverseRequest(reverse_request_args) => {
match reverse_request_args.request {
dap::StartDebuggingRequestArgumentsRequest::Launch => {
DebugRequestType::Launch(task::LaunchConfig {
program: "".to_owned(),
cwd: None,
args: Default::default(),
})
}
dap::StartDebuggingRequestArgumentsRequest::Attach => {
DebugRequestType::Attach(task::AttachConfig {
process_id: Some(0),
})
}
}
}
};
let callback = async move |session: &mut LocalMode, cx: AsyncApp| {
session
.client
.on_request::<dap::requests::Initialize, _>(move |_, _| Ok(caps.clone()))
.await;
let paths = cx
.update(|cx| session.breakpoint_store.read(cx).breakpoint_paths())
.expect("Breakpoint store should exist in all tests that start debuggers");
session
.client
.on_request::<dap::requests::SetBreakpoints, _>(move |_, args| {
let p = Arc::from(Path::new(&args.source.path.unwrap()));
if !paths.contains(&p) {
panic!("Sent breakpoints for path without any")
}
Ok(dap::SetBreakpointsResponse {
breakpoints: Vec::default(),
})
})
.await;
match request {
dap::DebugRequestType::Launch(_) => {
if fail {
session
.client
.on_request::<dap::requests::Launch, _>(move |_, _| {
Err(dap::ErrorResponse {
error: Some(dap::Message {
id: 1,
format: "error".into(),
variables: None,
send_telemetry: None,
show_user: None,
url: None,
url_label: None,
}),
})
})
.await;
} else {
session
.client
.on_request::<dap::requests::Launch, _>(move |_, _| Ok(()))
.await;
}
}
dap::DebugRequestType::Attach(attach_config) => {
if fail {
session
.client
.on_request::<dap::requests::Attach, _>(move |_, _| {
Err(dap::ErrorResponse {
error: Some(dap::Message {
id: 1,
format: "error".into(),
variables: None,
send_telemetry: None,
show_user: None,
url: None,
url_label: None,
}),
})
})
.await;
} else {
session
.client
.on_request::<dap::requests::Attach, _>(move |_, args| {
assert_eq!(
json!({"request": "attach", "process_id": attach_config.process_id.unwrap()}),
args.raw
);
Ok(())
})
.await;
}
}
}
session
.client
.on_request::<dap::requests::SetExceptionBreakpoints, _>(move |_, _| {
Ok(dap::SetExceptionBreakpointsResponse { breakpoints: None })
})
.await;
session
.client
.on_request::<dap::requests::Disconnect, _>(move |_, _| Ok(()))
.await;
session.client.fake_event(Events::Initialized(None)).await;
};
Self::new_inner(
DapRegistry::fake().into(),
session_id,
parent_session,
breakpoint_store,
config,
delegate,
messages_tx,
callback,
cx,
)
}
fn new_inner(
registry: Arc<DapRegistry>,
session_id: SessionId,
parent_session: Option<Entity<Session>>,
breakpoint_store: Entity<BreakpointStore>,
config: DebugAdapterConfig,
delegate: DapAdapterDelegate,
config: DebugTaskDefinition,
binary: DebugAdapterBinary,
messages_tx: futures::channel::mpsc::UnboundedSender<Message>,
on_initialized: impl AsyncFnOnce(&mut LocalMode, AsyncApp) + 'static,
cx: AsyncApp,
) -> Task<Result<Self>> {
cx.spawn(async move |cx| {
let (adapter, binary) =
Self::get_adapter_binary(&registry, &config, &delegate, cx).await?;
let message_handler = Box::new(move |message| {
messages_tx.unbounded_send(message).ok();
});
@ -380,13 +223,12 @@ impl LocalMode {
.flatten()
{
client
.reconnect(session_id, binary, message_handler, cx.clone())
.reconnect(session_id, binary.clone(), message_handler, cx.clone())
.await?
} else {
DebugAdapterClient::start(
session_id,
adapter.name(),
binary,
binary.clone(),
message_handler,
cx.clone(),
)
@ -397,10 +239,10 @@ impl LocalMode {
let mut session = Self {
client,
adapter,
breakpoint_store,
tmp_breakpoint: None,
config: config.clone(),
definition: config,
binary,
};
on_initialized(&mut session, cx.clone()).await;
@ -533,55 +375,12 @@ impl LocalMode {
})
}
async fn get_adapter_binary(
registry: &Arc<DapRegistry>,
config: &DebugAdapterConfig,
delegate: &DapAdapterDelegate,
cx: &mut AsyncApp,
) -> Result<(Arc<dyn DebugAdapter>, DebugAdapterBinary)> {
let adapter = registry
.adapter(&config.adapter)
.ok_or_else(|| anyhow!("Debug adapter with name `{}` was not found", config.adapter))?;
let binary = cx.update(|cx| {
ProjectSettings::get_global(cx)
.dap
.get(&adapter.name())
.and_then(|s| s.binary.as_ref().map(PathBuf::from))
})?;
let binary = match adapter.get_binary(delegate, &config, binary, cx).await {
Err(error) => {
delegate.update_status(
adapter.name(),
DapStatus::Failed {
error: error.to_string(),
},
);
return Err(error);
}
Ok(mut binary) => {
delegate.update_status(adapter.name(), DapStatus::None);
let shell_env = delegate.shell_env().await;
let mut envs = binary.envs.unwrap_or_default();
envs.extend(shell_env);
binary.envs = Some(envs);
binary
}
};
Ok((adapter, binary))
}
pub fn label(&self) -> String {
self.config.label.clone()
self.definition.label.clone()
}
fn request_initialization(&self, cx: &App) -> Task<Result<Capabilities>> {
let adapter_id = self.adapter.name().to_string();
let adapter_id = self.binary.adapter_name.to_string();
self.request(Initialize { adapter_id }, cx.background_executor().clone())
}
@ -592,36 +391,26 @@ impl LocalMode {
initialized_rx: oneshot::Receiver<()>,
cx: &App,
) -> Task<Result<()>> {
let (mut raw, is_launch) = match &self.config.request {
task::DebugRequestDisposition::UserConfigured(_) => {
let Ok(raw) = DebugTaskDefinition::try_from(self.config.clone()) else {
debug_assert!(false, "This part of code should be unreachable in practice");
return Task::ready(Err(anyhow!(
"Expected debug config conversion to succeed"
)));
};
let is_launch = matches!(raw.request, DebugRequestType::Launch(_));
let raw = self.adapter.request_args(&raw);
(raw, is_launch)
}
task::DebugRequestDisposition::ReverseRequest(start_debugging_request_arguments) => (
start_debugging_request_arguments.configuration.clone(),
matches!(
start_debugging_request_arguments.request,
dap::StartDebuggingRequestArgumentsRequest::Launch
),
),
};
let mut raw = self.binary.request_args.clone();
merge_json_value_into(
self.config.initialize_args.clone().unwrap_or(json!({})),
&mut raw,
self.definition.initialize_args.clone().unwrap_or(json!({})),
&mut raw.configuration,
);
// Of relevance: https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522
let launch = if is_launch {
self.request(Launch { raw }, cx.background_executor().clone())
} else {
self.request(Attach { raw }, cx.background_executor().clone())
let launch = match raw.request {
dap::StartDebuggingRequestArgumentsRequest::Launch => self.request(
Launch {
raw: raw.configuration,
},
cx.background_executor().clone(),
),
dap::StartDebuggingRequestArgumentsRequest::Attach => self.request(
Attach {
raw: raw.configuration,
},
cx.background_executor().clone(),
),
};
let configuration_done_supported = ConfigurationDone::is_supported(capabilities);
@ -656,12 +445,13 @@ impl LocalMode {
})?
.await
.ok();
if configuration_done_supported {
let ret = if configuration_done_supported {
this.request(ConfigurationDone {}, cx.background_executor().clone())
} else {
Task::ready(Ok(()))
}
.await
.await;
ret
}
});
@ -909,23 +699,21 @@ impl Session {
breakpoint_store: Entity<BreakpointStore>,
session_id: SessionId,
parent_session: Option<Entity<Session>>,
delegate: DapAdapterDelegate,
config: DebugAdapterConfig,
binary: DebugAdapterBinary,
config: DebugTaskDefinition,
start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
initialized_tx: oneshot::Sender<()>,
debug_adapters: Arc<DapRegistry>,
cx: &mut App,
) -> Task<Result<Entity<Self>>> {
let (message_tx, message_rx) = futures::channel::mpsc::unbounded();
cx.spawn(async move |cx| {
let mode = LocalMode::new(
debug_adapters,
session_id,
parent_session.clone(),
breakpoint_store.clone(),
config.clone(),
delegate,
binary,
message_tx,
cx.clone(),
)
@ -946,50 +734,6 @@ impl Session {
})
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn fake(
breakpoint_store: Entity<BreakpointStore>,
session_id: SessionId,
parent_session: Option<Entity<Session>>,
delegate: DapAdapterDelegate,
config: DebugAdapterConfig,
start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
initialized_tx: oneshot::Sender<()>,
caps: Capabilities,
fails: bool,
cx: &mut App,
) -> Task<Result<Entity<Session>>> {
let (message_tx, message_rx) = futures::channel::mpsc::unbounded();
cx.spawn(async move |cx| {
let mode = LocalMode::new_fake(
session_id,
parent_session.clone(),
breakpoint_store.clone(),
config.clone(),
delegate,
message_tx,
caps,
fails,
cx.clone(),
)
.await?;
cx.new(|cx| {
create_local_session(
breakpoint_store,
session_id,
parent_session,
start_debugging_requests_tx,
initialized_tx,
message_rx,
mode,
cx,
)
})
})
}
pub(crate) fn remote(
session_id: SessionId,
client: AnyProtoClient,
@ -1047,16 +791,23 @@ impl Session {
&self.capabilities
}
pub fn binary(&self) -> &DebugAdapterBinary {
let Mode::Local(local_mode) = &self.mode else {
panic!("Session is not local");
};
&local_mode.binary
}
pub fn adapter_name(&self) -> SharedString {
match &self.mode {
Mode::Local(local_mode) => local_mode.adapter.name().into(),
Mode::Local(local_mode) => local_mode.definition.adapter.clone().into(),
Mode::Remote(remote_mode) => remote_mode._adapter_name.clone(),
}
}
pub fn configuration(&self) -> Option<DebugAdapterConfig> {
pub fn configuration(&self) -> Option<DebugTaskDefinition> {
if let Mode::Local(local_mode) = &self.mode {
Some(local_mode.config.clone())
Some(local_mode.definition.clone())
} else {
None
}

View file

@ -0,0 +1,98 @@
use std::{path::Path, sync::Arc};
use anyhow::Result;
use dap::{DebugRequestType, client::DebugAdapterClient};
use gpui::{App, AppContext, Entity, Subscription, Task};
use task::DebugTaskDefinition;
use crate::Project;
use super::session::Session;
pub fn intercept_debug_sessions<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
cx: &mut gpui::TestAppContext,
configure: T,
) -> Subscription {
cx.update(|cx| {
cx.observe_new::<Session>(move |session, _, cx| {
let client = session.adapter_client().unwrap();
register_default_handlers(session, &client, cx);
configure(&client);
cx.background_spawn(async move {
client
.fake_event(dap::messages::Events::Initialized(Some(Default::default())))
.await
})
.detach();
})
})
}
pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
project: &Entity<Project>,
cx: &mut gpui::TestAppContext,
config: DebugTaskDefinition,
configure: T,
) -> Task<Result<Entity<Session>>> {
let subscription = intercept_debug_sessions(cx, configure);
let task = project.update(cx, |project, cx| project.start_debug_session(config, cx));
cx.spawn(async move |_| {
let result = task.await;
drop(subscription);
result
})
}
pub fn start_debug_session<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
project: &Entity<Project>,
cx: &mut gpui::TestAppContext,
configure: T,
) -> Task<Result<Entity<Session>>> {
start_debug_session_with(
project,
cx,
DebugTaskDefinition {
adapter: "fake-adapter".to_string(),
request: DebugRequestType::Launch(Default::default()),
label: "test".to_string(),
initialize_args: None,
tcp_connection: None,
locator: None,
stop_on_entry: None,
},
configure,
)
}
fn register_default_handlers(session: &Session, client: &Arc<DebugAdapterClient>, cx: &mut App) {
client.on_request::<dap::requests::Initialize, _>(move |_, _| Ok(Default::default()));
let paths = session
.as_local()
.unwrap()
.breakpoint_store
.read(cx)
.breakpoint_paths();
client.on_request::<dap::requests::SetBreakpoints, _>(move |_, args| {
let p = Arc::from(Path::new(&args.source.path.unwrap()));
if !paths.contains(&p) {
panic!("Sent breakpoints for path without any")
}
Ok(dap::SetBreakpointsResponse {
breakpoints: Vec::default(),
})
});
client.on_request::<dap::requests::Launch, _>(move |_, _| Ok(()));
client.on_request::<dap::requests::SetExceptionBreakpoints, _>(move |_, _| {
Ok(dap::SetExceptionBreakpointsResponse { breakpoints: None })
});
client.on_request::<dap::requests::Disconnect, _>(move |_, _| Ok(()));
client.on_request::<dap::requests::Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse { threads: vec![] })
});
}

View file

@ -25,6 +25,7 @@ mod environment;
use buffer_diff::BufferDiff;
pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent};
use git_store::{Repository, RepositoryId};
use task::DebugTaskDefinition;
pub mod search_history;
mod yarn;
@ -38,7 +39,7 @@ use client::{
};
use clock::ReplicaId;
use dap::{DapRegistry, DebugAdapterConfig, client::DebugAdapterClient};
use dap::{DapRegistry, client::DebugAdapterClient};
use collections::{BTreeSet, HashMap, HashSet};
use debounced_delay::DebouncedDelay;
@ -106,7 +107,7 @@ use terminals::Terminals;
use text::{Anchor, BufferId};
use toolchain_store::EmptyToolchainStore;
use util::{
ResultExt as _, maybe,
ResultExt as _,
paths::{SanitizedPath, compare_paths},
};
use worktree::{CreatedEntry, Snapshot, Traversal};
@ -870,11 +871,9 @@ impl Project {
node.clone(),
fs.clone(),
languages.clone(),
debug_adapters.clone(),
environment.clone(),
toolchain_store.read(cx).as_language_toolchain_store(),
breakpoint_store.clone(),
worktree_store.clone(),
cx,
)
});
@ -1458,64 +1457,46 @@ impl Project {
}
}
pub fn queue_debug_session(&mut self, config: DebugAdapterConfig, cx: &mut Context<Self>) {
if config.locator.is_none() {
self.start_debug_session(config, cx).detach_and_log_err(cx);
}
}
pub fn start_debug_session(
&mut self,
config: DebugAdapterConfig,
config: DebugTaskDefinition,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Session>>> {
let worktree = maybe!({ self.worktrees(cx).next() });
let Some(worktree) = &worktree else {
let Some(worktree) = self.worktrees(cx).next() 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 fn fake_debug_session(
&mut self,
request: task::DebugRequestType,
caps: Option<dap::Capabilities>,
fails: bool,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Session>>> {
use dap::{Capabilities, FakeAdapter};
use task::DebugRequestDisposition;
let worktree = maybe!({ self.worktrees(cx).next() });
let Some(worktree) = &worktree else {
return Task::ready(Err(anyhow!("Failed to find a worktree")));
let Some(adapter) = self.debug_adapters.adapter(&config.adapter) else {
return Task::ready(Err(anyhow!("Failed to find a debug adapter")));
};
let config = DebugAdapterConfig {
label: "test config".into(),
adapter: FakeAdapter::ADAPTER_NAME.into(),
request: DebugRequestDisposition::UserConfigured(request),
initialize_args: None,
tcp_connection: None,
locator: None,
stop_on_entry: None,
};
let caps = caps.unwrap_or(Capabilities {
supports_step_back: Some(false),
..Default::default()
let user_installed_path = ProjectSettings::get_global(cx)
.dap
.get(&adapter.name())
.and_then(|s| s.binary.as_ref().map(PathBuf::from));
let result = cx.spawn(async move |this, cx| {
let delegate = this.update(cx, |project, cx| {
project
.dap_store
.update(cx, |dap_store, cx| dap_store.delegate(&worktree, cx))
})?;
let binary = adapter
.get_binary(&delegate, &config, user_installed_path, cx)
.await?;
let ret = this
.update(cx, |project, cx| {
project.dap_store.update(cx, |dap_store, cx| {
dap_store.new_session(binary, config, None, cx)
})
})?
.1
.await;
ret
});
self.dap_store
.update(cx, |dap_store, cx| {
dap_store.new_fake_session(config, worktree, None, caps, fails, cx)
})
.1
result
}
#[cfg(any(test, feature = "test-support"))]

View file

@ -39,7 +39,7 @@ use std::{env, mem, num::NonZeroU32, ops::Range, str::FromStr, sync::OnceLock, t
use task::{ResolvedTask, TaskContext};
use unindent::Unindent as _;
use util::{
TryFutureExt as _, assert_set_eq, path,
TryFutureExt as _, assert_set_eq, maybe, path,
paths::PathMatcher,
separator,
test::{TempTree, marked_text_offsets},