Detailed changes
@@ -3042,6 +3042,7 @@ dependencies = [
"strum 0.27.1",
"subtle",
"supermaven_api",
+ "task",
"telemetry_events",
"text",
"theme",
@@ -4189,6 +4190,7 @@ dependencies = [
"command_palette_hooks",
"dap",
"db",
+ "debugger_tools",
"editor",
"env_logger 0.11.8",
"feature_flags",
@@ -4198,6 +4200,7 @@ dependencies = [
"language",
"log",
"menu",
+ "parking_lot",
"picker",
"pretty_assertions",
"project",
@@ -128,6 +128,7 @@ serde_json.workspace = true
session = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
sqlx = { version = "0.8", features = ["sqlite"] }
+task.workspace = true
theme.workspace = true
unindent.workspace = true
util.workspace = true
@@ -1,7 +1,7 @@
use call::ActiveCall;
-use dap::requests::{Initialize, Launch, StackTrace};
use dap::DebugRequestType;
-use dap::{requests::SetBreakpoints, SourceBreakpoint};
+use dap::requests::{Initialize, Launch, StackTrace};
+use dap::{SourceBreakpoint, requests::SetBreakpoints};
use debugger_ui::debugger_panel::DebugPanel;
use debugger_ui::session::DebugSession;
use editor::Editor;
@@ -13,7 +13,7 @@ use std::{
path::Path,
sync::atomic::{AtomicBool, Ordering},
};
-use workspace::{dock::Panel, Workspace};
+use workspace::{Workspace, dock::Panel};
use super::{TestClient, TestServer};
@@ -2,11 +2,13 @@ use crate::tests::TestServer;
use call::ActiveCall;
use collections::{HashMap, HashSet};
+use debugger_ui::debugger_panel::DebugPanel;
use extension::ExtensionHostProxy;
use fs::{FakeFs, Fs as _, RemoveOptions};
use futures::StreamExt as _;
use gpui::{
AppContext as _, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal as _,
+ VisualContext,
};
use http_client::BlockedHttpClient;
use language::{
@@ -24,6 +26,7 @@ use project::{
};
use remote::SshRemoteClient;
use remote_server::{HeadlessAppState, HeadlessProject};
+use rpc::proto;
use serde_json::json;
use settings::SettingsStore;
use std::{path::Path, sync::Arc};
@@ -576,3 +579,108 @@ async fn test_ssh_collaboration_formatting_with_prettier(
"Prettier formatting was not applied to client buffer after host's request"
);
}
+
+#[gpui::test]
+async fn test_remote_server_debugger(cx_a: &mut TestAppContext, server_cx: &mut TestAppContext) {
+ cx_a.update(|cx| {
+ release_channel::init(SemanticVersion::default(), cx);
+ command_palette_hooks::init(cx);
+ if std::env::var("RUST_LOG").is_ok() {
+ env_logger::try_init().ok();
+ }
+ });
+ server_cx.update(|cx| {
+ release_channel::init(SemanticVersion::default(), cx);
+ });
+ let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
+ let remote_fs = FakeFs::new(server_cx.executor());
+ remote_fs
+ .insert_tree(
+ path!("/code"),
+ json!({
+ "lib.rs": "fn one() -> usize { 1 }"
+ }),
+ )
+ .await;
+
+ // User A connects to the remote project via SSH.
+ server_cx.update(HeadlessProject::init);
+ let remote_http_client = Arc::new(BlockedHttpClient);
+ let node = NodeRuntime::unavailable();
+ let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
+ let _headless_project = server_cx.new(|cx| {
+ client::init_settings(cx);
+ HeadlessProject::new(
+ HeadlessAppState {
+ session: server_ssh,
+ fs: remote_fs.clone(),
+ http_client: remote_http_client,
+ node_runtime: node,
+ languages,
+ extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
+ },
+ cx,
+ )
+ });
+
+ let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
+ let mut server = TestServer::start(server_cx.executor()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ cx_a.update(|cx| {
+ debugger_ui::init(cx);
+ command_palette_hooks::init(cx);
+ });
+ let (project_a, _) = client_a
+ .build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
+ .await;
+
+ let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
+
+ let debugger_panel = workspace
+ .update_in(cx_a, |_workspace, window, cx| {
+ cx.spawn_in(window, DebugPanel::load)
+ })
+ .await
+ .unwrap();
+
+ workspace.update_in(cx_a, |workspace, window, cx| {
+ workspace.add_panel(debugger_panel, window, cx);
+ });
+
+ cx_a.run_until_parked();
+ let debug_panel = workspace
+ .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
+ .unwrap();
+
+ let workspace_window = cx_a
+ .window_handle()
+ .downcast::<workspace::Workspace>()
+ .unwrap();
+
+ let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
+ cx_a.run_until_parked();
+ debug_panel.update(cx_a, |debug_panel, cx| {
+ assert_eq!(
+ debug_panel.active_session().unwrap().read(cx).session(cx),
+ session
+ )
+ });
+
+ session.update(cx_a, |session, _| {
+ assert_eq!(session.binary().command, "ssh");
+ });
+
+ let shutdown_session = workspace.update(cx_a, |workspace, cx| {
+ workspace.project().update(cx, |project, cx| {
+ project.dap_store().update(cx, |dap_store, cx| {
+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
+ })
+ })
+ });
+
+ client_ssh.update(cx_a, |a, _| {
+ a.shutdown_processes(Some(proto::ShutdownRemoteServer {}))
+ });
+
+ shutdown_session.await.unwrap();
+}
@@ -22,7 +22,7 @@ use std::{
time::Duration,
};
use task::TcpArgumentsTemplate;
-use util::ResultExt as _;
+use util::{ResultExt as _, TryFutureExt};
use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
@@ -126,6 +126,7 @@ pub(crate) struct TransportDelegate {
pending_requests: Requests,
transport: Transport,
server_tx: Arc<Mutex<Option<Sender<Message>>>>,
+ _tasks: Vec<gpui::Task<Option<()>>>,
}
impl TransportDelegate {
@@ -140,6 +141,7 @@ impl TransportDelegate {
log_handlers: Default::default(),
current_requests: Default::default(),
pending_requests: Default::default(),
+ _tasks: Default::default(),
};
let messages = this.start_handlers(transport_pipes, cx).await?;
Ok((messages, this))
@@ -166,35 +168,43 @@ impl TransportDelegate {
cx.update(|cx| {
if let Some(stdout) = params.stdout.take() {
- cx.background_executor()
- .spawn(Self::handle_adapter_log(stdout, log_handler.clone()))
- .detach_and_log_err(cx);
+ self._tasks.push(
+ cx.background_executor()
+ .spawn(Self::handle_adapter_log(stdout, log_handler.clone()).log_err()),
+ );
}
- cx.background_executor()
- .spawn(Self::handle_output(
- params.output,
- client_tx,
- self.pending_requests.clone(),
- log_handler.clone(),
- ))
- .detach_and_log_err(cx);
+ self._tasks.push(
+ cx.background_executor().spawn(
+ Self::handle_output(
+ params.output,
+ client_tx,
+ self.pending_requests.clone(),
+ log_handler.clone(),
+ )
+ .log_err(),
+ ),
+ );
if let Some(stderr) = params.stderr.take() {
- cx.background_executor()
- .spawn(Self::handle_error(stderr, self.log_handlers.clone()))
- .detach_and_log_err(cx);
+ self._tasks.push(
+ cx.background_executor()
+ .spawn(Self::handle_error(stderr, self.log_handlers.clone()).log_err()),
+ );
}
- cx.background_executor()
- .spawn(Self::handle_input(
- params.input,
- client_rx,
- self.current_requests.clone(),
- self.pending_requests.clone(),
- log_handler.clone(),
- ))
- .detach_and_log_err(cx);
+ self._tasks.push(
+ cx.background_executor().spawn(
+ Self::handle_input(
+ params.input,
+ client_rx,
+ self.current_requests.clone(),
+ self.pending_requests.clone(),
+ log_handler.clone(),
+ )
+ .log_err(),
+ ),
+ );
})?;
{
@@ -367,6 +377,7 @@ impl TransportDelegate {
where
Stderr: AsyncRead + Unpin + Send + 'static,
{
+ log::debug!("Handle error started");
let mut buffer = String::new();
let mut reader = BufReader::new(stderr);
@@ -12,6 +12,9 @@ workspace = true
path = "src/debugger_tools.rs"
doctest = false
+[features]
+test-support = []
+
[dependencies]
anyhow.workspace = true
dap.workspace = true
@@ -41,7 +41,7 @@ struct DapLogView {
_subscriptions: Vec<Subscription>,
}
-struct LogStore {
+pub struct LogStore {
projects: HashMap<WeakEntity<Project>, ProjectState>,
debug_clients: HashMap<SessionId, DebugAdapterState>,
rpc_tx: UnboundedSender<(SessionId, IoKind, String)>,
@@ -101,7 +101,7 @@ impl DebugAdapterState {
}
impl LogStore {
- fn new(cx: &Context<Self>) -> Self {
+ pub fn new(cx: &Context<Self>) -> Self {
let (rpc_tx, mut rpc_rx) = unbounded::<(SessionId, IoKind, String)>();
cx.spawn(async move |this, cx| {
while let Some((client_id, io_kind, message)) = rpc_rx.next().await {
@@ -845,3 +845,29 @@ impl EventEmitter<Event> for LogStore {}
impl EventEmitter<Event> for DapLogView {}
impl EventEmitter<EditorEvent> for DapLogView {}
impl EventEmitter<SearchEvent> for DapLogView {}
+
+#[cfg(any(test, feature = "test-support"))]
+impl LogStore {
+ pub fn contained_session_ids(&self) -> Vec<SessionId> {
+ self.debug_clients.keys().cloned().collect()
+ }
+
+ pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> {
+ self.debug_clients
+ .get(&session_id)
+ .expect("This session should exist if a test is calling")
+ .rpc_messages
+ .messages
+ .clone()
+ .into()
+ }
+
+ pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> {
+ self.debug_clients
+ .get(&session_id)
+ .expect("This session should exist if a test is calling")
+ .log_messages
+ .clone()
+ .into()
+ }
+}
@@ -20,6 +20,9 @@ test-support = [
"project/test-support",
"util/test-support",
"workspace/test-support",
+ "env_logger",
+ "unindent",
+ "debugger_tools"
]
[dependencies]
@@ -37,6 +40,7 @@ gpui.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true
+parking_lot.workspace = true
picker.workspace = true
pretty_assertions.workspace = true
project.workspace = true
@@ -53,9 +57,13 @@ ui.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
+env_logger = { workspace = true, optional = true }
+debugger_tools = { workspace = true, optional = true }
+unindent = { workspace = true, optional = true }
[dev-dependencies]
dap = { workspace = true, features = ["test-support"] }
+debugger_tools = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] }
@@ -1,7 +1,7 @@
use dap::DebugRequest;
use fuzzy::{StringMatch, StringMatchCandidate};
-use gpui::Subscription;
use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render};
+use gpui::{Subscription, WeakEntity};
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
@@ -9,7 +9,9 @@ use sysinfo::System;
use ui::{Context, Tooltip, prelude::*};
use ui::{ListItem, ListItemSpacing};
use util::debug_panic;
-use workspace::ModalView;
+use workspace::{ModalView, Workspace};
+
+use crate::debugger_panel::DebugPanel;
#[derive(Debug, Clone)]
pub(super) struct Candidate {
@@ -22,19 +24,19 @@ pub(crate) struct AttachModalDelegate {
selected_index: usize,
matches: Vec<StringMatch>,
placeholder_text: Arc<str>,
- project: Entity<project::Project>,
+ workspace: WeakEntity<Workspace>,
pub(crate) debug_config: task::DebugTaskDefinition,
candidates: Arc<[Candidate]>,
}
impl AttachModalDelegate {
fn new(
- project: Entity<project::Project>,
+ workspace: Entity<Workspace>,
debug_config: task::DebugTaskDefinition,
candidates: Arc<[Candidate]>,
) -> Self {
Self {
- project,
+ workspace: workspace.downgrade(),
debug_config,
candidates,
selected_index: 0,
@@ -51,7 +53,7 @@ pub struct AttachModal {
impl AttachModal {
pub fn new(
- project: Entity<project::Project>,
+ workspace: Entity<Workspace>,
debug_config: task::DebugTaskDefinition,
modal: bool,
window: &mut Window,
@@ -75,11 +77,11 @@ impl AttachModal {
.collect();
processes.sort_by_key(|k| k.name.clone());
let processes = processes.into_iter().collect();
- Self::with_processes(project, debug_config, processes, modal, window, cx)
+ Self::with_processes(workspace, debug_config, processes, modal, window, cx)
}
pub(super) fn with_processes(
- project: Entity<project::Project>,
+ workspace: Entity<Workspace>,
debug_config: task::DebugTaskDefinition,
processes: Arc<[Candidate]>,
modal: bool,
@@ -88,7 +90,7 @@ impl AttachModal {
) -> Self {
let picker = cx.new(|cx| {
Picker::uniform_list(
- AttachModalDelegate::new(project, debug_config, processes),
+ AttachModalDelegate::new(workspace, debug_config, processes),
window,
cx,
)
@@ -202,7 +204,7 @@ impl PickerDelegate for AttachModalDelegate {
})
}
- fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
+ fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let candidate = self
.matches
.get(self.selected_index())
@@ -225,14 +227,17 @@ impl PickerDelegate for AttachModalDelegate {
}
}
- let config = self.debug_config.clone();
- self.project
- .update(cx, |project, cx| {
- let ret = project.start_debug_session(config, cx);
- ret
- })
- .detach_and_log_err(cx);
-
+ let definition = self.debug_config.clone();
+ let panel = self
+ .workspace
+ .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
+ .ok()
+ .flatten();
+ if let Some(panel) = panel {
+ panel.update(cx, |panel, cx| {
+ panel.start_session(definition, window, cx);
+ });
+ }
cx.emit(DismissEvent);
}
@@ -6,6 +6,7 @@ use crate::{new_session_modal::NewSessionModal, session::DebugSession};
use anyhow::{Result, anyhow};
use collections::HashMap;
use command_palette_hooks::CommandPaletteFilter;
+use dap::StartDebuggingRequestArguments;
use dap::{
ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
client::SessionId, debugger_settings::DebuggerSettings,
@@ -17,6 +18,7 @@ use gpui::{
actions, anchored, deferred,
};
+use project::debugger::session::{Session, SessionStateEvent};
use project::{
Project,
debugger::{
@@ -30,10 +32,9 @@ use settings::Settings;
use std::any::TypeId;
use std::path::Path;
use std::sync::Arc;
-use task::DebugTaskDefinition;
+use task::{DebugTaskDefinition, DebugTaskTemplate};
use terminal_view::terminal_panel::TerminalPanel;
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
-use util::debug_panic;
use workspace::{
Workspace,
dock::{DockPosition, Panel, PanelEvent},
@@ -63,7 +64,7 @@ pub struct DebugPanel {
active_session: Option<Entity<DebugSession>>,
/// This represents the last debug definition that was created in the new session modal
pub(crate) past_debug_definition: Option<DebugTaskDefinition>,
- project: WeakEntity<Project>,
+ project: Entity<Project>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
@@ -97,10 +98,10 @@ impl DebugPanel {
window,
|panel, _, event: &tasks_ui::ShowAttachModal, window, cx| {
panel.workspace.update(cx, |workspace, cx| {
- let project = workspace.project().clone();
+ let workspace_handle = cx.entity().clone();
workspace.toggle_modal(window, cx, |window, cx| {
crate::attach_modal::AttachModal::new(
- project,
+ workspace_handle,
event.debug_config.clone(),
true,
window,
@@ -127,7 +128,7 @@ impl DebugPanel {
_subscriptions,
past_debug_definition: None,
focus_handle: cx.focus_handle(),
- project: project.downgrade(),
+ project,
workspace: workspace.weak_handle(),
context_menu: None,
};
@@ -219,7 +220,7 @@ impl DebugPanel {
pub fn load(
workspace: WeakEntity<Workspace>,
- cx: AsyncWindowContext,
+ cx: &mut AsyncWindowContext,
) -> Task<Result<Entity<Self>>> {
cx.spawn(async move |cx| {
workspace.update_in(cx, |workspace, window, cx| {
@@ -245,114 +246,226 @@ impl DebugPanel {
});
})
.detach();
+ workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone()));
debug_panel
})
})
}
- pub fn active_session(&self) -> Option<Entity<DebugSession>> {
- self.active_session.clone()
- }
-
- pub fn debug_panel_items_by_client(
- &self,
- client_id: &SessionId,
- cx: &Context<Self>,
- ) -> Vec<Entity<DebugSession>> {
- self.sessions
- .iter()
- .filter(|item| item.read(cx).session_id(cx) == *client_id)
- .map(|item| item.clone())
- .collect()
- }
-
- pub fn debug_panel_item_by_client(
- &self,
- client_id: SessionId,
- cx: &mut Context<Self>,
- ) -> Option<Entity<DebugSession>> {
- self.sessions
- .iter()
- .find(|item| {
- let item = item.read(cx);
-
- item.session_id(cx) == client_id
- })
- .cloned()
- }
-
- fn handle_dap_store_event(
+ pub fn start_session(
&mut self,
- dap_store: &Entity<DapStore>,
- event: &dap_store::DapStoreEvent,
+ definition: DebugTaskDefinition,
window: &mut Window,
cx: &mut Context<Self>,
) {
- match event {
- dap_store::DapStoreEvent::DebugSessionInitialized(session_id) => {
- let Some(session) = dap_store.read(cx).session_by_id(session_id) else {
- return log::error!(
- "Couldn't get session with id: {session_id:?} from DebugClientStarted event"
- );
+ let task_contexts = self
+ .workspace
+ .update(cx, |workspace, cx| {
+ tasks_ui::task_contexts(workspace, window, cx)
+ })
+ .ok();
+ let dap_store = self.project.read(cx).dap_store().clone();
+
+ cx.spawn_in(window, async move |this, cx| {
+ let task_context = if let Some(task) = task_contexts {
+ task.await
+ .active_worktree_context
+ .map_or(task::TaskContext::default(), |context| context.1)
+ } else {
+ task::TaskContext::default()
+ };
+
+ let (session, task) = dap_store.update(cx, |dap_store, cx| {
+ let template = DebugTaskTemplate {
+ locator: None,
+ definition: definition.clone(),
+ };
+ let session = if let Some(debug_config) = template
+ .to_zed_format()
+ .resolve_task("debug_task", &task_context)
+ .and_then(|resolved_task| resolved_task.resolved_debug_adapter_config())
+ {
+ dap_store.new_session(debug_config.definition, None, cx)
+ } else {
+ dap_store.new_session(definition.clone(), None, cx)
};
- let adapter_name = session.read(cx).adapter_name();
+ (session.clone(), dap_store.boot_session(session, cx))
+ })?;
- let session_id = *session_id;
- cx.spawn_in(window, async move |this, cx| {
- let serialized_layout =
- persistence::get_serialized_pane_layout(adapter_name).await;
+ match task.await {
+ Err(e) => {
+ this.update(cx, |this, cx| {
+ this.workspace
+ .update(cx, |workspace, cx| {
+ workspace.show_error(&e, cx);
+ })
+ .ok();
+ })
+ .ok();
- this.update_in(cx, |this, window, cx| {
- let Some(project) = this.project.upgrade() else {
- return log::error!(
- "Debug Panel out lived it's weak reference to Project"
- );
- };
+ session
+ .update(cx, |session, cx| session.shutdown(cx))?
+ .await;
+ }
+ Ok(_) => Self::register_session(this, session, cx).await?,
+ }
- if this
- .sessions
- .iter()
- .any(|item| item.read(cx).session_id(cx) == session_id)
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+
+ async fn register_session(
+ this: WeakEntity<Self>,
+ session: Entity<Session>,
+ cx: &mut AsyncWindowContext,
+ ) -> Result<()> {
+ let adapter_name = session.update(cx, |session, _| session.adapter_name())?;
+ this.update_in(cx, |_, window, cx| {
+ cx.subscribe_in(
+ &session,
+ window,
+ move |_, session, event: &SessionStateEvent, window, cx| match event {
+ SessionStateEvent::Restart => {
+ let mut curr_session = session.clone();
+ while let Some(parent_session) = curr_session
+ .read_with(cx, |session, _| session.parent_session().cloned())
{
- // We already have an item for this session.
- debug_panic!("We should never reuse session ids");
- return;
+ curr_session = parent_session;
}
- this.sessions.retain(|session| {
- session
- .read(cx)
- .mode()
- .as_running()
- .map_or(false, |running_state| {
- !running_state.read(cx).session().read(cx).is_terminated()
- })
- });
-
- let session_item = DebugSession::running(
- project,
- this.workspace.clone(),
- session,
- cx.weak_entity(),
- serialized_layout,
- window,
- cx,
- );
+ let definition = curr_session.update(cx, |session, _| session.definition());
+ let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
- if let Some(running) = session_item.read(cx).mode().as_running().cloned() {
- // We might want to make this an event subscription and only notify when a new thread is selected
- // This is used to filter the command menu correctly
- cx.observe(&running, |_, _, cx| cx.notify()).detach();
- }
+ let definition = definition.clone();
+ cx.spawn_in(window, async move |this, cx| {
+ task.await;
+
+ this.update_in(cx, |this, window, cx| {
+ this.start_session(definition, window, cx)
+ })
+ })
+ .detach_and_log_err(cx);
+ }
+ _ => {}
+ },
+ )
+ .detach();
+ })
+ .ok();
+
+ let serialized_layout = persistence::get_serialized_pane_layout(adapter_name).await;
+
+ let workspace = this.update_in(cx, |this, window, cx| {
+ this.sessions.retain(|session| {
+ session
+ .read(cx)
+ .mode()
+ .as_running()
+ .map_or(false, |running_state| {
+ !running_state.read(cx).session().read(cx).is_terminated()
+ })
+ });
+
+ let session_item = DebugSession::running(
+ this.project.clone(),
+ this.workspace.clone(),
+ session,
+ cx.weak_entity(),
+ serialized_layout,
+ window,
+ cx,
+ );
+
+ if let Some(running) = session_item.read(cx).mode().as_running().cloned() {
+ // We might want to make this an event subscription and only notify when a new thread is selected
+ // This is used to filter the command menu correctly
+ cx.observe(&running, |_, _, cx| cx.notify()).detach();
+ }
+
+ this.sessions.push(session_item.clone());
+ this.activate_session(session_item, window, cx);
+ this.workspace.clone()
+ })?;
+
+ workspace.update_in(cx, |workspace, window, cx| {
+ workspace.focus_panel::<Self>(window, cx);
+ })?;
+ Ok(())
+ }
+
+ pub fn start_child_session(
+ &mut self,
+ request: &StartDebuggingRequestArguments,
+ parent_session: Entity<Session>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let Some(worktree) = parent_session.read(cx).worktree() else {
+ log::error!("Attempted to start a child session from non local debug session");
+ return;
+ };
+
+ let dap_store_handle = self.project.read(cx).dap_store().clone();
+ let breakpoint_store = self.project.read(cx).breakpoint_store();
+ let definition = parent_session.read(cx).definition().clone();
+ let mut binary = parent_session.read(cx).binary().clone();
+ binary.request_args = request.clone();
- this.sessions.push(session_item.clone());
- this.activate_session(session_item, window, cx);
+ cx.spawn_in(window, async move |this, cx| {
+ let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
+ let session =
+ dap_store.new_session(definition.clone(), Some(parent_session.clone()), cx);
+
+ let task = session.update(cx, |session, cx| {
+ session.boot(
+ binary,
+ worktree,
+ breakpoint_store,
+ dap_store_handle.downgrade(),
+ cx,
+ )
+ });
+ (session, task)
+ })?;
+
+ match task.await {
+ Err(e) => {
+ this.update(cx, |this, cx| {
+ this.workspace
+ .update(cx, |workspace, cx| {
+ workspace.show_error(&e, cx);
+ })
+ .ok();
})
- })
- .detach();
+ .ok();
+
+ session
+ .update(cx, |session, cx| session.shutdown(cx))?
+ .await;
+ }
+ Ok(_) => Self::register_session(this, session, cx).await?,
}
+
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+
+ pub fn active_session(&self) -> Option<Entity<DebugSession>> {
+ self.active_session.clone()
+ }
+
+ fn handle_dap_store_event(
+ &mut self,
+ _dap_store: &Entity<DapStore>,
+ event: &dap_store::DapStoreEvent,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ match event {
dap_store::DapStoreEvent::RunInTerminal {
title,
cwd,
@@ -374,6 +487,12 @@ impl DebugPanel {
)
.detach_and_log_err(cx);
}
+ dap_store::DapStoreEvent::SpawnChildSession {
+ request,
+ parent_session,
+ } => {
+ self.start_child_session(request, parent_session.clone(), window, cx);
+ }
_ => {}
}
}
@@ -408,7 +527,7 @@ impl DebugPanel {
cwd,
title,
},
- task::RevealStrategy::Always,
+ task::RevealStrategy::Never,
window,
cx,
);
@@ -468,8 +587,6 @@ impl DebugPanel {
let session = this.dap_store().read(cx).session_by_id(session_id);
session.map(|session| !session.read(cx).is_terminated())
})
- .ok()
- .flatten()
.unwrap_or_default();
cx.spawn_in(window, async move |this, cx| {
@@ -893,7 +1010,6 @@ impl DebugPanel {
impl EventEmitter<PanelEvent> for DebugPanel {}
impl EventEmitter<DebugPanelEvent> for DebugPanel {}
-impl EventEmitter<project::Event> for DebugPanel {}
impl Focusable for DebugPanel {
fn focus_handle(&self, _: &App) -> FocusHandle {
@@ -1039,3 +1155,15 @@ impl Render for DebugPanel {
.into_any()
}
}
+
+struct DebuggerProvider(Entity<DebugPanel>);
+
+impl workspace::DebuggerProvider for DebuggerProvider {
+ fn start_session(&self, definition: DebugTaskDefinition, window: &mut Window, cx: &mut App) {
+ self.0.update(cx, |_, cx| {
+ cx.defer_in(window, |this, window, cx| {
+ this.start_session(definition, window, cx);
+ })
+ })
+ }
+}
@@ -16,7 +16,7 @@ mod new_session_modal;
mod persistence;
pub(crate) mod session;
-#[cfg(test)]
+#[cfg(any(test, feature = "test-support"))]
pub mod tests;
actions!(
@@ -4,14 +4,12 @@ use std::{
path::{Path, PathBuf},
};
-use anyhow::{Result, anyhow};
use dap::{DapRegistry, DebugRequest};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, TextStyle,
WeakEntity,
};
-use project::Project;
use settings::Settings;
use task::{DebugTaskDefinition, DebugTaskTemplate, LaunchRequest};
use theme::ThemeSettings;
@@ -21,7 +19,6 @@ use ui::{
LabelCommon as _, ParentElement, RenderOnce, SharedString, Styled, StyledExt, ToggleButton,
ToggleState, Toggleable, Window, div, h_flex, relative, rems, v_flex,
};
-use util::ResultExt;
use workspace::{ModalView, Workspace};
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
@@ -88,11 +85,11 @@ impl NewSessionModal {
}
}
- fn debug_config(&self, cx: &App) -> Option<DebugTaskDefinition> {
+ fn debug_config(&self, cx: &App, debugger: &str) -> DebugTaskDefinition {
let request = self.mode.debug_task(cx);
- Some(DebugTaskDefinition {
- adapter: self.debugger.clone()?.to_string(),
- label: suggested_label(&request, self.debugger.as_deref()?),
+ DebugTaskDefinition {
+ adapter: debugger.to_owned(),
+ label: suggested_label(&request, debugger),
request,
initialize_args: self.initialize_args.clone(),
tcp_connection: None,
@@ -100,26 +97,26 @@ impl NewSessionModal {
ToggleState::Selected => Some(true),
_ => None,
},
- })
+ }
}
- fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) -> Result<()> {
- let workspace = self.workspace.clone();
- let config = self
- .debug_config(cx)
- .ok_or_else(|| anyhow!("Failed to create a debug config"))?;
-
- let _ = self.debug_panel.update(cx, |panel, _| {
- panel.past_debug_definition = Some(config.clone());
- });
+ fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) {
+ let Some(debugger) = self.debugger.as_ref() else {
+ // todo: show in UI.
+ log::error!("No debugger selected");
+ return;
+ };
+ let config = self.debug_config(cx, debugger);
+ let debug_panel = self.debug_panel.clone();
- let task_contexts = workspace
+ let task_contexts = self
+ .workspace
.update(cx, |workspace, cx| {
tasks_ui::task_contexts(workspace, window, cx)
})
.ok();
- cx.spawn(async move |this, cx| {
+ cx.spawn_in(window, async move |this, cx| {
let task_context = if let Some(task) = task_contexts {
task.await
.active_worktree_context
@@ -127,9 +124,8 @@ impl NewSessionModal {
} else {
task::TaskContext::default()
};
- let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
- let task = project.update(cx, |this, cx| {
+ debug_panel.update_in(cx, |debug_panel, window, cx| {
let template = DebugTaskTemplate {
locator: None,
definition: config.clone(),
@@ -139,23 +135,18 @@ impl NewSessionModal {
.resolve_task("debug_task", &task_context)
.and_then(|resolved_task| resolved_task.resolved_debug_adapter_config())
{
- this.start_debug_session(debug_config.definition, cx)
+ debug_panel.start_session(debug_config.definition, window, cx)
} else {
- this.start_debug_session(config, cx)
+ debug_panel.start_session(config, window, cx)
}
})?;
- let spawn_result = task.await;
- if spawn_result.is_ok() {
- this.update(cx, |_, cx| {
- cx.emit(DismissEvent);
- })
- .ok();
- }
- spawn_result?;
+ this.update(cx, |_, cx| {
+ cx.emit(DismissEvent);
+ })
+ .ok();
anyhow::Result::<_, anyhow::Error>::Ok(())
})
.detach_and_log_err(cx);
- Ok(())
}
fn update_attach_picker(
@@ -249,15 +240,12 @@ impl NewSessionModal {
);
}
DebugRequest::Attach(_) => {
- let Ok(project) = this
- .workspace
- .read_with(cx, |this, _| this.project().clone())
- else {
+ let Some(workspace) = this.workspace.upgrade() else {
return;
};
this.mode = NewSessionMode::attach(
this.debugger.clone(),
- project,
+ workspace,
window,
cx,
);
@@ -357,7 +345,7 @@ struct AttachMode {
impl AttachMode {
fn new(
debugger: Option<SharedString>,
- project: Entity<Project>,
+ workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Entity<Self> {
@@ -370,7 +358,7 @@ impl AttachMode {
stop_on_entry: Some(false),
};
let attach_picker = cx.new(|cx| {
- let modal = AttachModal::new(project, debug_definition.clone(), false, window, cx);
+ let modal = AttachModal::new(workspace, debug_definition.clone(), false, window, cx);
window.focus(&modal.focus_handle(cx));
modal
@@ -470,11 +458,11 @@ impl RenderOnce for NewSessionMode {
impl NewSessionMode {
fn attach(
debugger: Option<SharedString>,
- project: Entity<Project>,
+ workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Self {
- Self::Attach(AttachMode::new(debugger, project, window, cx))
+ Self::Attach(AttachMode::new(debugger, workspace, window, cx))
}
fn launch(
past_launch_config: Option<LaunchRequest>,
@@ -569,15 +557,12 @@ impl Render for NewSessionModal {
.toggle_state(matches!(self.mode, NewSessionMode::Attach(_)))
.style(ui::ButtonStyle::Subtle)
.on_click(cx.listener(|this, _, window, cx| {
- let Ok(project) = this
- .workspace
- .read_with(cx, |this, _| this.project().clone())
- else {
+ let Some(workspace) = this.workspace.upgrade() else {
return;
};
this.mode = NewSessionMode::attach(
this.debugger.clone(),
- project,
+ workspace,
window,
cx,
);
@@ -631,7 +616,7 @@ impl Render for NewSessionModal {
.child(
Button::new("debugger-spawn", "Start")
.on_click(cx.listener(|this, _, window, cx| {
- this.start_new_session(window, cx).log_err();
+ this.start_new_session(window, cx);
}))
.disabled(self.debugger.is_none()),
),
@@ -88,6 +88,12 @@ impl DebugSession {
}
}
+ pub fn session(&self, cx: &App) -> Entity<Session> {
+ match &self.mode {
+ DebugSessionState::Running(entity) => entity.read(cx).session().clone(),
+ }
+ }
+
pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
match &self.mode {
DebugSessionState::Running(state) => state.update(cx, |state, cx| state.shutdown(cx)),
@@ -115,13 +121,7 @@ impl DebugSession {
};
self.label
- .get_or_init(|| {
- session
- .read(cx)
- .as_local()
- .expect("Remote Debug Sessions are not implemented yet")
- .label()
- })
+ .get_or_init(|| session.read(cx).label())
.to_owned()
}
@@ -418,6 +418,19 @@ impl RunningState {
let threads = this.session.update(cx, |this, cx| this.threads(cx));
this.select_current_thread(&threads, cx);
}
+ SessionEvent::CapabilitiesLoaded => {
+ let capabilities = this.capabilities(cx);
+ if !capabilities.supports_modules_request.unwrap_or(false) {
+ this.remove_pane_item(DebuggerPaneItem::Modules, window, cx);
+ }
+ if !capabilities
+ .supports_loaded_sources_request
+ .unwrap_or(false)
+ {
+ this.remove_pane_item(DebuggerPaneItem::LoadedSources, window, cx);
+ }
+ }
+
_ => {}
}
cx.notify()
@@ -447,35 +460,14 @@ impl RunningState {
workspace::PaneGroup::with_root(root)
} else {
pane_close_subscriptions.clear();
- let module_list = if session
- .read(cx)
- .capabilities()
- .supports_modules_request
- .unwrap_or(false)
- {
- Some(&module_list)
- } else {
- None
- };
-
- let loaded_source_list = if session
- .read(cx)
- .capabilities()
- .supports_loaded_sources_request
- .unwrap_or(false)
- {
- Some(&loaded_source_list)
- } else {
- None
- };
let root = Self::default_pane_layout(
project,
&workspace,
&stack_frame_list,
&variable_list,
- module_list,
- loaded_source_list,
+ &module_list,
+ &loaded_source_list,
&console,
&breakpoint_list,
&mut pane_close_subscriptions,
@@ -512,11 +504,6 @@ impl RunningState {
window: &mut Window,
cx: &mut Context<Self>,
) {
- debug_assert!(
- item_kind.is_supported(self.session.read(cx).capabilities()),
- "We should only allow removing supported item kinds"
- );
-
if let Some((pane, item_id)) = self.panes.panes().iter().find_map(|pane| {
Some(pane).zip(
pane.read(cx)
@@ -946,8 +933,8 @@ impl RunningState {
workspace: &WeakEntity<Workspace>,
stack_frame_list: &Entity<StackFrameList>,
variable_list: &Entity<VariableList>,
- module_list: Option<&Entity<ModuleList>>,
- loaded_source_list: Option<&Entity<LoadedSourceList>>,
+ module_list: &Entity<ModuleList>,
+ loaded_source_list: &Entity<LoadedSourceList>,
console: &Entity<Console>,
breakpoints: &Entity<BreakpointList>,
subscriptions: &mut HashMap<EntityId, Subscription>,
@@ -1003,41 +990,36 @@ impl RunningState {
window,
cx,
);
- if let Some(module_list) = module_list {
- this.add_item(
- Box::new(SubView::new(
- module_list.focus_handle(cx),
- module_list.clone().into(),
- DebuggerPaneItem::Modules,
- None,
- cx,
- )),
- false,
- false,
+ this.add_item(
+ Box::new(SubView::new(
+ module_list.focus_handle(cx),
+ module_list.clone().into(),
+ DebuggerPaneItem::Modules,
None,
- window,
cx,
- );
- this.activate_item(0, false, false, window, cx);
- }
+ )),
+ false,
+ false,
+ None,
+ window,
+ cx,
+ );
- if let Some(loaded_source_list) = loaded_source_list {
- this.add_item(
- Box::new(SubView::new(
- loaded_source_list.focus_handle(cx),
- loaded_source_list.clone().into(),
- DebuggerPaneItem::LoadedSources,
- None,
- cx,
- )),
- false,
- false,
+ this.add_item(
+ Box::new(SubView::new(
+ loaded_source_list.focus_handle(cx),
+ loaded_source_list.clone().into(),
+ DebuggerPaneItem::LoadedSources,
None,
- window,
cx,
- );
- this.activate_item(1, false, false, window, cx);
- }
+ )),
+ false,
+ false,
+ None,
+ window,
+ cx,
+ );
+ this.activate_item(0, false, false, window, cx);
});
let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
@@ -1,16 +1,29 @@
+use std::sync::Arc;
+
+use anyhow::{Result, anyhow};
+use dap::{DebugRequest, client::DebugAdapterClient};
use gpui::{Entity, TestAppContext, WindowHandle};
-use project::Project;
+use project::{Project, debugger::session::Session};
use settings::SettingsStore;
+use task::DebugTaskDefinition;
use terminal_view::terminal_panel::TerminalPanel;
use workspace::Workspace;
use crate::{debugger_panel::DebugPanel, session::DebugSession};
+#[cfg(test)]
mod attach_modal;
+#[cfg(test)]
mod console;
+#[cfg(test)]
+mod dap_logger;
+#[cfg(test)]
mod debugger_panel;
+#[cfg(test)]
mod module_list;
+#[cfg(test)]
mod stack_frame_list;
+#[cfg(test)]
mod variable_list;
pub fn init_test(cx: &mut gpui::TestAppContext) {
@@ -42,7 +55,7 @@ pub async fn init_test_workspace(
let debugger_panel = workspace_handle
.update(cx, |_, window, cx| {
cx.spawn_in(window, async move |this, cx| {
- DebugPanel::load(this, cx.clone()).await
+ DebugPanel::load(this, cx).await
})
})
.unwrap()
@@ -82,3 +95,46 @@ pub fn active_debug_session_panel(
})
.unwrap()
}
+
+pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
+ workspace: &WindowHandle<Workspace>,
+ cx: &mut gpui::TestAppContext,
+ config: DebugTaskDefinition,
+ configure: T,
+) -> Result<Entity<Session>> {
+ let _subscription = project::debugger::test::intercept_debug_sessions(cx, configure);
+ workspace.update(cx, |workspace, window, cx| {
+ workspace.start_debug_session(config, window, cx)
+ })?;
+ cx.run_until_parked();
+ let session = workspace.read_with(cx, |workspace, cx| {
+ workspace
+ .panel::<DebugPanel>(cx)
+ .and_then(|panel| panel.read(cx).active_session())
+ .and_then(|session| session.read(cx).mode().as_running().cloned())
+ .map(|running| running.read(cx).session().clone())
+ .ok_or_else(|| anyhow!("Failed to get active session"))
+ })??;
+
+ Ok(session)
+}
+
+pub fn start_debug_session<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
+ workspace: &WindowHandle<Workspace>,
+ cx: &mut gpui::TestAppContext,
+ configure: T,
+) -> Result<Entity<Session>> {
+ start_debug_session_with(
+ workspace,
+ cx,
+ DebugTaskDefinition {
+ adapter: "fake-adapter".to_string(),
+ request: DebugRequest::Launch(Default::default()),
+ label: "test".to_string(),
+ initialize_args: None,
+ tcp_connection: None,
+ stop_on_entry: None,
+ },
+ configure,
+ )
+}
@@ -1,4 +1,4 @@
-use crate::{attach_modal::Candidate, *};
+use crate::{attach_modal::Candidate, tests::start_debug_session_with, *};
use attach_modal::AttachModal;
use dap::{FakeAdapter, client::SessionId};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
@@ -26,8 +26,8 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let session = debugger::test::start_debug_session_with(
- &project,
+ let session = start_debug_session_with(
+ &workspace,
cx,
DebugTaskDefinition {
adapter: "fake-adapter".to_string(),
@@ -47,7 +47,6 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
});
},
)
- .await
.unwrap();
cx.run_until_parked();
@@ -99,9 +98,10 @@ async fn test_show_attach_modal_and_select_process(
});
let attach_modal = workspace
.update(cx, |workspace, window, cx| {
+ let workspace_handle = cx.entity();
workspace.toggle_modal(window, cx, |window, cx| {
AttachModal::with_processes(
- project.clone(),
+ workspace_handle,
DebugTaskDefinition {
adapter: FakeAdapter::ADAPTER_NAME.into(),
request: dap::DebugRequest::Attach(AttachRequest::default()),
@@ -1,4 +1,7 @@
-use crate::{tests::active_debug_session_panel, *};
+use crate::{
+ tests::{active_debug_session_panel, start_debug_session},
+ *,
+};
use dap::requests::StackTrace;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
@@ -28,9 +31,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp
})
.unwrap();
- let session = debugger::test::start_debug_session(&project, cx, |_| {})
- .await
- .unwrap();
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<StackTrace, _>(move |_, _| {
@@ -0,0 +1,118 @@
+use crate::tests::{init_test, init_test_workspace, start_debug_session};
+use dap::requests::{StackTrace, Threads};
+use debugger_tools::LogStore;
+use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
+use project::Project;
+use serde_json::json;
+use std::cell::OnceCell;
+
+#[gpui::test]
+async fn test_dap_logger_captures_all_session_rpc_messages(
+ executor: BackgroundExecutor,
+ cx: &mut TestAppContext,
+) {
+ let log_store_cell = std::rc::Rc::new(OnceCell::new());
+
+ cx.update(|cx| {
+ let log_store_cell = log_store_cell.clone();
+ cx.observe_new::<LogStore>(move |_, _, cx| {
+ log_store_cell.set(cx.entity()).unwrap();
+ })
+ .detach();
+ debugger_tools::init(cx);
+ });
+ init_test(cx);
+
+ let log_store = log_store_cell.get().unwrap().clone();
+
+ // Create a filesystem with a simple project
+ let fs = project::FakeFs::new(executor.clone());
+ fs.insert_tree(
+ "/project",
+ json!({
+ "main.rs": "fn main() {\n println!(\"Hello, world!\");\n}"
+ }),
+ )
+ .await;
+
+ assert!(
+ log_store.read_with(cx, |log_store, _| log_store
+ .contained_session_ids()
+ .is_empty()),
+ "log_store shouldn't contain any session IDs before any sessions were created"
+ );
+
+ let project = Project::test(fs, ["/project".as_ref()], cx).await;
+
+ let workspace = init_test_workspace(&project, cx).await;
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+
+ // Start a debug session
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
+ let session_id = session.read_with(cx, |session, _| session.session_id());
+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
+
+ assert_eq!(
+ log_store.read_with(cx, |log_store, _| log_store.contained_session_ids().len()),
+ 1,
+ );
+
+ assert!(
+ log_store.read_with(cx, |log_store, _| log_store
+ .contained_session_ids()
+ .contains(&session_id)),
+ "log_store should contain the session IDs of the started session"
+ );
+
+ assert!(
+ !log_store.read_with(cx, |log_store, _| log_store
+ .rpc_messages_for_session_id(session_id)
+ .is_empty()),
+ "We should have the initialization sequence in the log store"
+ );
+
+ // Set up basic responses for common requests
+ client.on_request::<Threads, _>(move |_, _| {
+ Ok(dap::ThreadsResponse {
+ threads: vec![dap::Thread {
+ id: 1,
+ name: "Thread 1".into(),
+ }],
+ })
+ });
+
+ client.on_request::<StackTrace, _>(move |_, _| {
+ Ok(dap::StackTraceResponse {
+ stack_frames: Vec::default(),
+ total_frames: None,
+ })
+ });
+
+ // Run until all pending tasks are executed
+ cx.run_until_parked();
+
+ // Simulate a stopped event to generate more DAP messages
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ cx.run_until_parked();
+
+ // Shutdown the debug session
+ let shutdown_session = project.update(cx, |project, cx| {
+ project.dap_store().update(cx, |dap_store, cx| {
+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
+ })
+ });
+
+ shutdown_session.await.unwrap();
+ cx.run_until_parked();
+}
@@ -1,4 +1,4 @@
-use crate::*;
+use crate::{tests::start_debug_session, *};
use dap::{
ErrorResponse, Message, RunInTerminalRequestArguments, SourceBreakpoint,
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
@@ -48,9 +48,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let session = debugger::test::start_debug_session(&project, cx, |_| {})
- .await
- .unwrap();
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<Threads, _>(move |_, _| {
@@ -187,9 +185,7 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let session = debugger::test::start_debug_session(&project, cx, |_| {})
- .await
- .unwrap();
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<Threads, _>(move |_, _| {
@@ -354,9 +350,7 @@ async fn test_handle_successful_run_in_terminal_reverse_request(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let session = debugger::test::start_debug_session(&project, cx, |_| {})
- .await
- .unwrap();
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client
@@ -419,6 +413,86 @@ async fn test_handle_successful_run_in_terminal_reverse_request(
shutdown_session.await.unwrap();
}
+#[gpui::test]
+async fn test_handle_start_debugging_request(
+ executor: BackgroundExecutor,
+ cx: &mut TestAppContext,
+) {
+ init_test(cx);
+
+ let fs = FakeFs::new(executor.clone());
+
+ fs.insert_tree(
+ "/project",
+ json!({
+ "main.rs": "First line\nSecond line\nThird line\nFourth line",
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, ["/project".as_ref()], cx).await;
+ let workspace = init_test_workspace(&project, cx).await;
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
+
+ let fake_config = json!({"one": "two"});
+ let launched_with = Arc::new(parking_lot::Mutex::new(None));
+
+ let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
+ let launched_with = launched_with.clone();
+ move |client| {
+ let launched_with = launched_with.clone();
+ client.on_request::<dap::requests::Launch, _>(move |_, args| {
+ launched_with.lock().replace(args.raw);
+ Ok(())
+ });
+ client.on_request::<dap::requests::Attach, _>(move |_, _| {
+ assert!(false, "should not get attach request");
+ Ok(())
+ });
+ }
+ });
+
+ client
+ .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
+ request: StartDebuggingRequestArgumentsRequest::Launch,
+ configuration: fake_config.clone(),
+ })
+ .await;
+
+ cx.run_until_parked();
+
+ workspace
+ .update(cx, |workspace, _window, cx| {
+ let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
+ let active_session = debug_panel
+ .read(cx)
+ .active_session()
+ .unwrap()
+ .read(cx)
+ .session(cx);
+ let parent_session = active_session.read(cx).parent_session().unwrap();
+
+ assert_eq!(
+ active_session.read(cx).definition(),
+ parent_session.read(cx).definition()
+ );
+ })
+ .unwrap();
+
+ assert_eq!(&fake_config, launched_with.lock().as_ref().unwrap());
+
+ let shutdown_session = project.update(cx, |project, cx| {
+ project.dap_store().update(cx, |dap_store, cx| {
+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
+ })
+ });
+
+ shutdown_session.await.unwrap();
+}
+
// // covers that we always send a response back, if something when wrong,
// // while spawning the terminal
#[gpui::test]
@@ -444,9 +518,7 @@ async fn test_handle_error_run_in_terminal_reverse_request(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let session = debugger::test::start_debug_session(&project, cx, |_| {})
- .await
- .unwrap();
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client
@@ -522,9 +594,7 @@ async fn test_handle_start_debugging_reverse_request(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let session = debugger::test::start_debug_session(&project, cx, |_| {})
- .await
- .unwrap();
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -629,9 +699,7 @@ async fn test_shutdown_children_when_parent_session_shutdown(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let parent_session = debugger::test::start_debug_session(&project, cx, |_| {})
- .await
- .unwrap();
+ let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -737,9 +805,7 @@ async fn test_shutdown_parent_session_if_all_children_are_shutdown(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let parent_session = debugger::test::start_debug_session(&project, cx, |_| {})
- .await
- .unwrap();
+ let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_response::<StartDebugging, _>(move |_| {}).await;
@@ -858,7 +924,7 @@ async fn test_debug_panel_item_thread_status_reset_on_failure(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let session = debugger::test::start_debug_session(&project, cx, |client| {
+ let session = start_debug_session(&workspace, cx, |client| {
client.on_request::<dap::requests::Initialize, _>(move |_, _| {
Ok(dap::Capabilities {
supports_step_back: Some(true),
@@ -866,7 +932,6 @@ async fn test_debug_panel_item_thread_status_reset_on_failure(
})
});
})
- .await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
@@ -1073,9 +1138,7 @@ async fn test_send_breakpoints_when_editor_has_been_saved(
.update(cx, |_, _, cx| worktree.read(cx).id())
.unwrap();
- let session = debugger::test::start_debug_session(&project, cx, |_| {})
- .await
- .unwrap();
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
let buffer = project
@@ -1290,9 +1353,7 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
});
- let session = debugger::test::start_debug_session(&project, cx, |_| {})
- .await
- .unwrap();
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
let called_set_breakpoints = Arc::new(AtomicBool::new(false));
@@ -1358,7 +1419,7 @@ async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let task = project::debugger::test::start_debug_session(&project, cx, |client| {
+ start_debug_session(&workspace, cx, |client| {
client.on_request::<dap::requests::Initialize, _>(|_, _| {
Err(ErrorResponse {
error: Some(Message {
@@ -1372,12 +1433,8 @@ async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
}),
})
});
- });
-
- assert!(
- task.await.is_err(),
- "Session should failed to start if launch request fails"
- );
+ })
+ .ok();
cx.run_until_parked();
@@ -1,16 +1,13 @@
use crate::{
debugger_panel::DebugPanel,
- tests::{active_debug_session_panel, init_test, init_test_workspace},
+ tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
};
use dap::{
StoppedEvent,
requests::{Initialize, Modules},
};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
-use project::{
- FakeFs, Project,
- debugger::{self},
-};
+use project::{FakeFs, Project};
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicI32, Ordering},
@@ -31,7 +28,7 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
.unwrap();
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let session = debugger::test::start_debug_session(&project, cx, |client| {
+ let session = start_debug_session(&workspace, cx, |client| {
client.on_request::<Initialize, _>(move |_, _| {
Ok(dap::Capabilities {
supports_modules_request: Some(true),
@@ -39,7 +36,6 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
})
});
})
- .await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
@@ -1,7 +1,7 @@
use crate::{
debugger_panel::DebugPanel,
session::running::stack_frame_list::StackFrameEntry,
- tests::{active_debug_session_panel, init_test, init_test_workspace},
+ tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
};
use dap::{
StackFrame,
@@ -9,7 +9,7 @@ use dap::{
};
use editor::{Editor, ToPoint as _};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
-use project::{FakeFs, Project, debugger};
+use project::{FakeFs, Project};
use serde_json::json;
use std::sync::Arc;
use unindent::Unindent as _;
@@ -50,9 +50,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let session = debugger::test::start_debug_session(&project, cx, |_| {})
- .await
- .unwrap();
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
@@ -229,9 +227,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
});
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let session = debugger::test::start_debug_session(&project, cx, |_| {})
- .await
- .unwrap();
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<Threads, _>(move |_, _| {
@@ -495,9 +491,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let session = debugger::test::start_debug_session(&project, cx, |_| {})
- .await
- .unwrap();
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<Threads, _>(move |_, _| {
@@ -6,7 +6,7 @@ use std::sync::{
use crate::{
DebugPanel,
session::running::variable_list::{CollapseSelectedEntry, ExpandSelectedEntry},
- tests::{active_debug_session_panel, init_test, init_test_workspace},
+ tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
};
use collections::HashMap;
use dap::{
@@ -15,7 +15,7 @@ use dap::{
};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use menu::{SelectFirst, SelectNext, SelectPrevious};
-use project::{FakeFs, Project, debugger};
+use project::{FakeFs, Project};
use serde_json::json;
use unindent::Unindent as _;
use util::path;
@@ -54,9 +54,7 @@ async fn test_basic_fetch_initial_scope_and_variables(
})
.unwrap();
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let session = debugger::test::start_debug_session(&project, cx, |_| {})
- .await
- .unwrap();
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -266,9 +264,7 @@ async fn test_fetch_variables_for_multiple_scopes(
.unwrap();
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let session = debugger::test::start_debug_session(&project, cx, |_| {})
- .await
- .unwrap();
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -528,9 +524,7 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp
})
.unwrap();
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let session = debugger::test::start_debug_session(&project, cx, |_| {})
- .await
- .unwrap();
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -1313,9 +1307,7 @@ async fn test_variable_list_only_sends_requests_when_rendering(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let session = debugger::test::start_debug_session(&project, cx, |_| {})
- .await
- .unwrap();
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -1560,9 +1552,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
.unwrap();
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let session = debugger::test::start_debug_session(&project, cx, |_| {})
- .await
- .unwrap();
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -4,33 +4,35 @@ use super::{
session::{self, Session, SessionStateEvent},
};
use crate::{
- ProjectEnvironment, debugger, project_settings::ProjectSettings, worktree_store::WorktreeStore,
+ ProjectEnvironment,
+ project_settings::ProjectSettings,
+ terminals::{SshCommand, wrap_for_ssh},
+ worktree_store::WorktreeStore,
};
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, DebugAdapterBinary, DebugAdapterName},
+ Capabilities, CompletionItem, CompletionsArguments, DapRegistry, EvaluateArguments,
+ EvaluateArgumentsContext, EvaluateResponse, RunInTerminalRequestArguments, Source,
+ StartDebuggingRequestArguments,
+ adapters::{DapStatus, DebugAdapterBinary, DebugAdapterName, TcpArguments},
client::SessionId,
messages::Message,
requests::{Completions, Evaluate, Request as _, RunInTerminal, StartDebugging},
};
use fs::Fs;
use futures::{
- channel::{mpsc, oneshot},
+ channel::mpsc,
future::{Shared, join_all},
};
-use gpui::{
- App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity,
-};
+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 remote::SshRemoteClient;
use rpc::{
AnyProtoClient, TypedEnvelope,
proto::{self},
@@ -42,6 +44,7 @@ use std::{
borrow::Borrow,
collections::{BTreeMap, HashSet},
ffi::OsStr,
+ net::Ipv4Addr,
path::{Path, PathBuf},
sync::Arc,
};
@@ -66,6 +69,10 @@ pub enum DapStoreEvent {
envs: HashMap<String, String>,
sender: mpsc::Sender<Result<u32>>,
},
+ SpawnChildSession {
+ request: StartDebuggingRequestArguments,
+ parent_session: Entity<Session>,
+ },
Notification(String),
RemoteHasInitialized,
}
@@ -83,12 +90,12 @@ pub struct LocalDapStore {
http_client: Arc<dyn HttpClient>,
environment: Entity<ProjectEnvironment>,
language_registry: Arc<LanguageRegistry>,
- worktree_store: Entity<WorktreeStore>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
locators: HashMap<String, Arc<dyn DapLocator>>,
}
pub struct SshDapStore {
+ ssh_client: Entity<SshRemoteClient>,
upstream_client: AnyProtoClient,
upstream_project_id: u64,
}
@@ -97,6 +104,7 @@ pub struct DapStore {
mode: DapStoreMode,
downstream_client: Option<(AnyProtoClient, u64)>,
breakpoint_store: Entity<BreakpointStore>,
+ worktree_store: Entity<WorktreeStore>,
sessions: BTreeMap<SessionId, Entity<Session>>,
next_session_id: u32,
start_debugging_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
@@ -136,40 +144,43 @@ impl DapStore {
http_client,
node_runtime,
toolchain_store,
- worktree_store,
language_registry,
locators,
});
- Self::new(mode, breakpoint_store, cx)
+ Self::new(mode, breakpoint_store, worktree_store, cx)
}
pub fn new_ssh(
project_id: u64,
- upstream_client: AnyProtoClient,
+ ssh_client: Entity<SshRemoteClient>,
breakpoint_store: Entity<BreakpointStore>,
+ worktree_store: Entity<WorktreeStore>,
cx: &mut Context<Self>,
) -> Self {
let mode = DapStoreMode::Ssh(SshDapStore {
- upstream_client,
+ upstream_client: ssh_client.read(cx).proto_client(),
+ ssh_client,
upstream_project_id: project_id,
});
- Self::new(mode, breakpoint_store, cx)
+ Self::new(mode, breakpoint_store, worktree_store, cx)
}
pub fn new_collab(
_project_id: u64,
_upstream_client: AnyProtoClient,
breakpoint_store: Entity<BreakpointStore>,
+ worktree_store: Entity<WorktreeStore>,
cx: &mut Context<Self>,
) -> Self {
- Self::new(DapStoreMode::Collab, breakpoint_store, cx)
+ Self::new(DapStoreMode::Collab, breakpoint_store, worktree_store, cx)
}
fn new(
mode: DapStoreMode,
breakpoint_store: Entity<BreakpointStore>,
+ worktree_store: Entity<WorktreeStore>,
cx: &mut Context<Self>,
) -> Self {
let (start_debugging_tx, mut message_rx) =
@@ -202,6 +213,7 @@ impl DapStore {
next_session_id: 0,
downstream_client: None,
breakpoint_store,
+ worktree_store,
sessions: Default::default(),
}
}
@@ -212,8 +224,8 @@ impl DapStore {
cx: &mut Context<Self>,
) -> Task<Result<DebugAdapterBinary>> {
match &self.mode {
- DapStoreMode::Local(local) => {
- let Some(worktree) = local.worktree_store.read(cx).visible_worktrees(cx).next()
+ DapStoreMode::Local(_) => {
+ let Some(worktree) = self.worktree_store.read(cx).visible_worktrees(cx).next()
else {
return Task::ready(Err(anyhow!("Failed to find a worktree")));
};
@@ -261,10 +273,49 @@ impl DapStore {
project_id: ssh.upstream_project_id,
task: Some(definition.to_proto()),
});
+ let ssh_client = ssh.ssh_client.clone();
- cx.background_spawn(async move {
+ cx.spawn(async move |_, cx| {
let response = request.await?;
- DebugAdapterBinary::from_proto(response)
+ let binary = DebugAdapterBinary::from_proto(response)?;
+ let mut ssh_command = ssh_client.update(cx, |ssh, _| {
+ anyhow::Ok(SshCommand {
+ arguments: ssh
+ .ssh_args()
+ .ok_or_else(|| anyhow!("SSH arguments not found"))?,
+ })
+ })??;
+
+ let mut connection = None;
+ if let Some(c) = binary.connection {
+ let local_bind_addr = Ipv4Addr::new(127, 0, 0, 1);
+ let port =
+ dap::transport::TcpTransport::unused_port(local_bind_addr).await?;
+
+ ssh_command.add_port_forwarding(port, c.host.to_string(), c.port);
+ connection = Some(TcpArguments {
+ port: c.port,
+ host: local_bind_addr,
+ timeout: c.timeout,
+ })
+ }
+
+ let (program, args) = wrap_for_ssh(
+ &ssh_command,
+ Some((&binary.command, &binary.arguments)),
+ binary.cwd.as_deref(),
+ binary.envs,
+ None,
+ );
+
+ Ok(DebugAdapterBinary {
+ command: program,
+ arguments: args,
+ envs: HashMap::default(),
+ cwd: None,
+ connection,
+ request_args: binary.request_args,
+ })
})
}
DapStoreMode::Collab => {
@@ -316,27 +367,79 @@ impl DapStore {
}
}
- pub fn add_remote_client(
+ pub fn new_session(
&mut self,
- session_id: SessionId,
- ignore: Option<bool>,
+ template: DebugTaskDefinition,
+ parent_session: Option<Entity<Session>>,
cx: &mut Context<Self>,
- ) {
- if let DapStoreMode::Ssh(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);
+ ) -> Entity<Session> {
+ let session_id = SessionId(util::post_inc(&mut self.next_session_id));
+
+ if let Some(session) = &parent_session {
+ session.update(cx, |session, _| {
+ session.add_child_session_id(session_id);
+ });
}
+
+ 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,
+ );
+
+ self.sessions.insert(session_id, session.clone());
+ cx.notify();
+
+ cx.subscribe(&session, {
+ move |this: &mut DapStore, _, event: &SessionStateEvent, cx| match event {
+ SessionStateEvent::Shutdown => {
+ this.shutdown_session(session_id, cx).detach_and_log_err(cx);
+ }
+ SessionStateEvent::Restart => {}
+ SessionStateEvent::Running => {
+ cx.emit(DapStoreEvent::DebugClientStarted(session_id));
+ }
+ }
+ })
+ .detach();
+
+ session
+ }
+
+ pub fn boot_session(
+ &self,
+ session: Entity<Session>,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<()>> {
+ let Some(worktree) = self.worktree_store.read(cx).visible_worktrees(cx).next() else {
+ return Task::ready(Err(anyhow!("Failed to find a worktree")));
+ };
+
+ let dap_store = cx.weak_entity();
+ let breakpoint_store = self.breakpoint_store.clone();
+ let definition = session.read(cx).definition();
+
+ cx.spawn({
+ let session = session.clone();
+ async move |this, cx| {
+ let binary = this
+ .update(cx, |this, cx| {
+ this.get_debug_adapter_binary(definition.clone(), cx)
+ })?
+ .await?;
+
+ session
+ .update(cx, |session, cx| {
+ session.boot(binary, worktree, breakpoint_store, dap_store, cx)
+ })?
+ .await
+ }
+ })
}
pub fn session_by_id(
@@ -367,6 +470,10 @@ impl DapStore {
&self.breakpoint_store
}
+ pub fn worktree_store(&self) -> &Entity<WorktreeStore> {
+ &self.worktree_store
+ }
+
#[allow(dead_code)]
async fn handle_ignore_breakpoint_state(
this: Entity<Self>,
@@ -407,52 +514,6 @@ impl DapStore {
)
}
- pub fn new_session(
- &mut self,
- binary: DebugAdapterBinary,
- config: DebugTaskDefinition,
- worktree: WeakEntity<Worktree>,
- parent_session: Option<Entity<Session>>,
- cx: &mut Context<Self>,
- ) -> (SessionId, Task<Result<Entity<Session>>>) {
- let session_id = SessionId(util::post_inc(&mut self.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_debugging_tx = self.start_debugging_tx.clone();
-
- let task = cx.spawn(async move |this, cx| {
- let start_client_task = this.update(cx, |this, cx| {
- Session::local(
- this.breakpoint_store.clone(),
- worktree.clone(),
- session_id,
- parent_session,
- binary,
- config,
- start_debugging_tx.clone(),
- initialized_tx,
- cx,
- )
- })?;
-
- let ret = this
- .update(cx, |_, cx| {
- create_new_session(session_id, initialized_rx, start_client_task, worktree, cx)
- })?
- .await;
- ret
- });
-
- (session_id, task)
- }
-
fn handle_start_debugging_request(
&mut self,
session_id: SessionId,
@@ -462,56 +523,35 @@ impl DapStore {
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 Some(worktree) = parent_session
- .read(cx)
- .as_local()
- .map(|local| local.worktree().clone())
- else {
- return Task::ready(Err(anyhow!(
- "Cannot handle start debugging request from remote end"
- )));
- };
-
- let args = serde_json::from_value::<StartDebuggingRequestArguments>(
- request.arguments.unwrap_or_default(),
- )
- .expect("To parse StartDebuggingRequestArguments");
- let mut binary = parent_session.read(cx).binary().clone();
- let config = parent_session.read(cx).configuration().unwrap().clone();
- binary.request_args = args;
+ let launch_request: Option<Result<StartDebuggingRequestArguments, _>> = request
+ .arguments
+ .as_ref()
+ .map(|value| serde_json::from_value(value.clone()));
- let new_session_task = self
- .new_session(binary, config, worktree, Some(parent_session.clone()), cx)
- .1;
+ 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;
+ }
- let request_seq = request.seq;
cx.spawn(async move |_, cx| {
- 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(cx, |session, cx| {
session.respond_to_client(
request_seq,
success,
StartDebugging::COMMAND.to_string(),
- body,
+ None,
cx,
)
})?
@@ -752,7 +792,7 @@ impl DapStore {
let shutdown_parent_task = if let Some(parent_session) = session
.read(cx)
- .parent_id()
+ .parent_id(cx)
.and_then(|session_id| self.session_by_id(session_id))
{
let shutdown_id = parent_session.update(cx, |parent_session, _| {
@@ -842,121 +882,6 @@ impl DapStore {
}
}
-fn create_new_session(
- session_id: SessionId,
- initialized_rx: oneshot::Receiver<()>,
- start_client_task: Task<Result<Entity<Session>, anyhow::Error>>,
- worktree: WeakEntity<Worktree>,
- cx: &mut Context<DapStore>,
-) -> Task<Result<Entity<Session>>> {
- let task = cx.spawn(async move |this, cx| {
- let session = match start_client_task.await {
- Ok(session) => session,
- Err(error) => {
- this.update(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(cx, |store, cx| {
- store.sessions.insert(session_id, session.clone());
- cx.emit(DapStoreEvent::DebugClientStarted(session_id));
- cx.notify();
- })?;
- let seq_result = async || {
- session
- .update(cx, |session, cx| session.request_initialize(cx))?
- .await?;
-
- session
- .update(cx, |session, cx| {
- session.initialize_sequence(initialized_rx, this.clone(), cx)
- })?
- .await
- };
- 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
- .log_err();
-
- return Err(error);
- }
- }
-
- this.update(cx, |_, cx| {
- cx.subscribe(
- &session,
- move |this: &mut DapStore, session, event: &SessionStateEvent, cx| match event {
- SessionStateEvent::Shutdown => {
- this.shutdown_session(session_id, cx).detach_and_log_err(cx);
- }
- SessionStateEvent::Restart => {
- let mut curr_session = session;
- while let Some(parent_id) = curr_session.read(cx).parent_id() {
- if let Some(parent_session) = this.sessions.get(&parent_id).cloned() {
- curr_session = parent_session;
- } else {
- log::error!("Failed to get parent session from parent session id");
- break;
- }
- }
-
- let Some((config, binary)) = curr_session.read_with(cx, |session, _| {
- session
- .configuration()
- .map(|config| (config, session.root_binary().clone()))
- }) else {
- log::error!("Failed to get debug config from session");
- return;
- };
-
- let session_id = curr_session.read(cx).session_id();
-
- let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
-
- let worktree = worktree.clone();
- cx.spawn(async move |this, cx| {
- task.await;
-
- this.update(cx, |this, cx| {
- this.sessions.remove(&session_id);
- this.new_session(
- binary.as_ref().clone(),
- config,
- worktree,
- None,
- cx,
- )
- })?
- .1
- .await?;
-
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
- }
- },
- )
- .detach();
- cx.emit(DapStoreEvent::DebugSessionInitialized(session_id));
- })?;
-
- Ok(session)
- });
- task
-}
-
#[derive(Clone)]
pub struct DapAdapterDelegate {
fs: Arc<dyn Fs>,
@@ -28,7 +28,6 @@ use gpui::{
Task, WeakEntity,
};
-use rpc::AnyProtoClient;
use serde_json::{Value, json};
use smol::stream::StreamExt;
use std::any::TypeId;
@@ -115,54 +114,14 @@ impl From<dap::Thread> for Thread {
}
}
-type UpstreamProjectId = u64;
-
-struct RemoteConnection {
- _client: AnyProtoClient,
- _upstream_project_id: UpstreamProjectId,
- _adapter_name: SharedString,
-}
-
-impl RemoteConnection {
- fn send_proto_client_request<R: DapCommand>(
- &self,
- _request: R,
- _session_id: SessionId,
- cx: &mut App,
- ) -> Task<Result<R::Response>> {
- // let message = request.to_proto(session_id, self.upstream_project_id);
- // let upstream_client = self.client.clone();
- cx.background_executor().spawn(async move {
- // debugger(todo): Properly send messages when we wrap dap_commands in envelopes again
- // let response = upstream_client.request(message).await?;
- // request.response_from_proto(response)
- Err(anyhow!("Sending dap commands over RPC isn't supported yet"))
- })
- }
-
- fn request<R: DapCommand>(
- &self,
- request: R,
- session_id: SessionId,
- cx: &mut App,
- ) -> Task<Result<R::Response>>
- where
- <R::DapRequest as dap::requests::Request>::Response: 'static,
- <R::DapRequest as dap::requests::Request>::Arguments: 'static + Send,
- {
- return self.send_proto_client_request::<R>(request, session_id, cx);
- }
-}
-
enum Mode {
- Local(LocalMode),
- Remote(RemoteConnection),
+ Building,
+ Running(LocalMode),
}
#[derive(Clone)]
pub struct LocalMode {
client: Arc<DebugAdapterClient>,
- definition: DebugTaskDefinition,
binary: DebugAdapterBinary,
root_binary: Option<Arc<DebugAdapterBinary>>,
pub(crate) breakpoint_store: Entity<BreakpointStore>,
@@ -186,56 +145,47 @@ fn client_source(abs_path: &Path) -> dap::Source {
}
impl LocalMode {
- fn new(
+ async fn new(
session_id: SessionId,
parent_session: Option<Entity<Session>>,
worktree: WeakEntity<Worktree>,
breakpoint_store: Entity<BreakpointStore>,
- config: DebugTaskDefinition,
binary: DebugAdapterBinary,
messages_tx: futures::channel::mpsc::UnboundedSender<Message>,
cx: AsyncApp,
- ) -> Task<Result<Self>> {
- cx.spawn(async move |cx| {
- let message_handler = Box::new(move |message| {
- messages_tx.unbounded_send(message).ok();
- });
+ ) -> Result<Self> {
+ let message_handler = Box::new(move |message| {
+ messages_tx.unbounded_send(message).ok();
+ });
- let root_binary = if let Some(parent_session) = parent_session.as_ref() {
- Some(parent_session.read_with(cx, |session, _| session.root_binary().clone())?)
- } else {
- None
- };
+ let root_binary = if let Some(parent_session) = parent_session.as_ref() {
+ Some(parent_session.read_with(&cx, |session, _| session.root_binary().clone())?)
+ } else {
+ None
+ };
- let client = Arc::new(
- if let Some(client) = parent_session
- .and_then(|session| cx.update(|cx| session.read(cx).adapter_client()).ok())
- .flatten()
- {
- client
- .reconnect(session_id, binary.clone(), message_handler, cx.clone())
- .await?
- } else {
- DebugAdapterClient::start(
- session_id,
- binary.clone(),
- message_handler,
- cx.clone(),
- )
+ let client = Arc::new(
+ if let Some(client) = parent_session
+ .and_then(|session| cx.update(|cx| session.read(cx).adapter_client()).ok())
+ .flatten()
+ {
+ client
+ .reconnect(session_id, binary.clone(), message_handler, cx.clone())
+ .await?
+ } else {
+ DebugAdapterClient::start(session_id, binary.clone(), message_handler, cx.clone())
.await
.with_context(|| "Failed to start communication with debug adapter")?
- },
- );
+ },
+ );
- Ok(Self {
- client,
- breakpoint_store,
- worktree,
- tmp_breakpoint: None,
- definition: config,
- root_binary,
- binary,
- })
+ Ok(Self {
+ client,
+ breakpoint_store,
+ worktree,
+ tmp_breakpoint: None,
+ root_binary,
+ binary,
})
}
@@ -371,19 +321,10 @@ impl LocalMode {
})
}
- pub fn label(&self) -> String {
- self.definition.label.clone()
- }
-
- fn request_initialization(&self, cx: &App) -> Task<Result<Capabilities>> {
- let adapter_id = self.definition.adapter.clone();
-
- self.request(Initialize { adapter_id }, cx.background_executor().clone())
- }
-
fn initialize_sequence(
&self,
capabilities: &Capabilities,
+ definition: &DebugTaskDefinition,
initialized_rx: oneshot::Receiver<()>,
dap_store: WeakEntity<DapStore>,
cx: &App,
@@ -391,7 +332,7 @@ impl LocalMode {
let mut raw = self.binary.request_args.clone();
merge_json_value_into(
- self.definition.initialize_args.clone().unwrap_or(json!({})),
+ definition.initialize_args.clone().unwrap_or(json!({})),
&mut raw.configuration,
);
@@ -426,9 +367,9 @@ impl LocalMode {
let supports_exception_filters = capabilities
.supports_exception_filter_options
.unwrap_or_default();
+ let this = self.clone();
+ let worktree = self.worktree().clone();
let configuration_sequence = cx.spawn({
- let this = self.clone();
- let worktree = self.worktree().clone();
async move |cx| {
initialized_rx.await?;
let errors_by_path = cx
@@ -511,16 +452,10 @@ impl LocalMode {
})
}
}
-impl From<RemoteConnection> for Mode {
- fn from(value: RemoteConnection) -> Self {
- Self::Remote(value)
- }
-}
impl Mode {
fn request_dap<R: DapCommand>(
&self,
- session_id: SessionId,
request: R,
cx: &mut Context<Session>,
) -> Task<Result<R::Response>>
@@ -529,10 +464,13 @@ impl Mode {
<R::DapRequest as dap::requests::Request>::Arguments: 'static + Send,
{
match self {
- Mode::Local(debug_adapter_client) => {
+ Mode::Running(debug_adapter_client) => {
debug_adapter_client.request(request, cx.background_executor().clone())
}
- Mode::Remote(remote_connection) => remote_connection.request(request, session_id, cx),
+ Mode::Building => Task::ready(Err(anyhow!(
+ "no adapter running to send request: {:?}",
+ request
+ ))),
}
}
}
@@ -609,10 +547,11 @@ pub struct OutputToken(pub usize);
/// Represents a current state of a single debug adapter and provides ways to mutate it.
pub struct Session {
mode: Mode,
+ definition: DebugTaskDefinition,
pub(super) capabilities: Capabilities,
id: SessionId,
child_session_ids: HashSet<SessionId>,
- parent_id: Option<SessionId>,
+ parent_session: Option<Entity<Session>>,
ignore_breakpoints: bool,
modules: Vec<dap::Module>,
loaded_sources: Vec<dap::Source>,
@@ -626,7 +565,8 @@ pub struct Session {
is_session_terminated: bool,
requests: HashMap<TypeId, HashMap<RequestSlot, Shared<Task<Option<()>>>>>,
exception_breakpoints: BTreeMap<String, (ExceptionBreakpointsFilter, IsEnabled)>,
- _background_tasks: Vec<Task<()>>,
+ start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
+ background_tasks: Vec<Task<()>>,
}
trait CacheableCommand: Any + Send + Sync {
@@ -708,9 +648,12 @@ pub enum SessionEvent {
StackTrace,
Variables,
Threads,
+ CapabilitiesLoaded,
}
-pub(super) enum SessionStateEvent {
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum SessionStateEvent {
+ Running,
Shutdown,
Restart,
}
@@ -722,80 +665,140 @@ impl EventEmitter<SessionStateEvent> for Session {}
// remote side will only send breakpoint updates when it is a breakpoint created by that peer
// BreakpointStore notifies session on breakpoint changes
impl Session {
- pub(crate) fn local(
+ pub(crate) fn new(
breakpoint_store: Entity<BreakpointStore>,
- worktree: WeakEntity<Worktree>,
session_id: SessionId,
parent_session: Option<Entity<Session>>,
- binary: DebugAdapterBinary,
- config: DebugTaskDefinition,
+ template: DebugTaskDefinition,
start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
- initialized_tx: oneshot::Sender<()>,
cx: &mut App,
- ) -> Task<Result<Entity<Self>>> {
- let (message_tx, message_rx) = futures::channel::mpsc::unbounded();
+ ) -> Entity<Self> {
+ cx.new::<Self>(|cx| {
+ cx.subscribe(&breakpoint_store, |this, _, event, cx| match event {
+ BreakpointStoreEvent::BreakpointsUpdated(path, reason) => {
+ if let Some(local) = (!this.ignore_breakpoints)
+ .then(|| this.as_local_mut())
+ .flatten()
+ {
+ local
+ .send_breakpoints_from_path(path.clone(), *reason, cx)
+ .detach();
+ };
+ }
+ BreakpointStoreEvent::BreakpointsCleared(paths) => {
+ if let Some(local) = (!this.ignore_breakpoints)
+ .then(|| this.as_local_mut())
+ .flatten()
+ {
+ local.unset_breakpoints_from_paths(paths, cx).detach();
+ }
+ }
+ BreakpointStoreEvent::ActiveDebugLineChanged => {}
+ })
+ .detach();
+
+ let this = Self {
+ mode: Mode::Building,
+ id: session_id,
+ child_session_ids: HashSet::default(),
+ parent_session,
+ capabilities: Capabilities::default(),
+ ignore_breakpoints: false,
+ variables: Default::default(),
+ stack_frames: Default::default(),
+ thread_states: ThreadStates::default(),
+ output_token: OutputToken(0),
+ output: circular_buffer::CircularBuffer::boxed(),
+ requests: HashMap::default(),
+ modules: Vec::default(),
+ loaded_sources: Vec::default(),
+ threads: IndexMap::default(),
+ background_tasks: Vec::default(),
+ locations: Default::default(),
+ is_session_terminated: false,
+ exception_breakpoints: Default::default(),
+ definition: template,
+ start_debugging_requests_tx,
+ };
+
+ this
+ })
+ }
+
+ pub fn worktree(&self) -> Option<Entity<Worktree>> {
+ match &self.mode {
+ Mode::Building => None,
+ Mode::Running(local_mode) => local_mode.worktree.upgrade(),
+ }
+ }
- cx.spawn(async move |cx| {
+ pub fn boot(
+ &mut self,
+ binary: DebugAdapterBinary,
+ worktree: Entity<Worktree>,
+ breakpoint_store: Entity<BreakpointStore>,
+ dap_store: WeakEntity<DapStore>,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<()>> {
+ let (message_tx, mut message_rx) = futures::channel::mpsc::unbounded();
+ let (initialized_tx, initialized_rx) = futures::channel::oneshot::channel();
+ let session_id = self.session_id();
+
+ let background_tasks = vec![cx.spawn(async move |this: WeakEntity<Session>, cx| {
+ let mut initialized_tx = Some(initialized_tx);
+ while let Some(message) = message_rx.next().await {
+ if let Message::Event(event) = message {
+ if let Events::Initialized(_) = *event {
+ if let Some(tx) = initialized_tx.take() {
+ tx.send(()).ok();
+ }
+ } else {
+ let Ok(_) = this.update(cx, |session, cx| {
+ session.handle_dap_event(event, cx);
+ }) else {
+ break;
+ };
+ }
+ } else {
+ let Ok(Ok(_)) = this.update(cx, |this, _| {
+ this.start_debugging_requests_tx
+ .unbounded_send((session_id, message))
+ }) else {
+ break;
+ };
+ }
+ }
+ })];
+ self.background_tasks = background_tasks;
+ let id = self.id;
+ let parent_session = self.parent_session.clone();
+
+ cx.spawn(async move |this, cx| {
let mode = LocalMode::new(
- session_id,
- parent_session.clone(),
- worktree,
+ id,
+ parent_session,
+ worktree.downgrade(),
breakpoint_store.clone(),
- config.clone(),
binary,
message_tx,
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,
- )
- })
+ this.update(cx, |this, cx| {
+ this.mode = Mode::Running(mode);
+ cx.emit(SessionStateEvent::Running);
+ })?;
+
+ this.update(cx, |session, cx| session.request_initialize(cx))?
+ .await?;
+
+ this.update(cx, |session, cx| {
+ session.initialize_sequence(initialized_rx, dap_store.clone(), cx)
+ })?
+ .await
})
}
- pub(crate) fn remote(
- session_id: SessionId,
- client: AnyProtoClient,
- upstream_project_id: u64,
- ignore_breakpoints: bool,
- ) -> Self {
- Self {
- mode: Mode::Remote(RemoteConnection {
- _adapter_name: SharedString::new(""), // todo(debugger) we need to pipe in the right values to deserialize the debugger pane layout
- _client: client,
- _upstream_project_id: upstream_project_id,
- }),
- id: session_id,
- child_session_ids: HashSet::default(),
- parent_id: None,
- capabilities: Capabilities::default(),
- ignore_breakpoints,
- variables: Default::default(),
- stack_frames: Default::default(),
- thread_states: ThreadStates::default(),
- output_token: OutputToken(0),
- output: circular_buffer::CircularBuffer::boxed(),
- requests: HashMap::default(),
- modules: Vec::default(),
- loaded_sources: Vec::default(),
- threads: IndexMap::default(),
- _background_tasks: Vec::default(),
- locations: Default::default(),
- is_session_terminated: false,
- exception_breakpoints: Default::default(),
- }
- }
-
pub fn session_id(&self) -> SessionId {
self.id
}
@@ -812,8 +815,14 @@ impl Session {
self.child_session_ids.remove(&session_id);
}
- pub fn parent_id(&self) -> Option<SessionId> {
- self.parent_id
+ pub fn parent_id(&self, cx: &App) -> Option<SessionId> {
+ self.parent_session
+ .as_ref()
+ .map(|session| session.read(cx).id)
+ }
+
+ pub fn parent_session(&self) -> Option<&Entity<Self>> {
+ self.parent_session.as_ref()
}
pub fn capabilities(&self) -> &Capabilities {
@@ -821,35 +830,35 @@ impl Session {
}
pub(crate) fn root_binary(&self) -> Arc<DebugAdapterBinary> {
- let Mode::Local(local_mode) = &self.mode else {
- panic!("Session is not local");
- };
- local_mode
- .root_binary
- .clone()
- .unwrap_or_else(|| Arc::new(local_mode.binary.clone()))
+ match &self.mode {
+ Mode::Building => {
+ // todo(debugger): Implement root_binary for building mode
+ unimplemented!()
+ }
+ Mode::Running(running) => running
+ .root_binary
+ .clone()
+ .unwrap_or_else(|| Arc::new(running.binary.clone())),
+ }
}
pub fn binary(&self) -> &DebugAdapterBinary {
- let Mode::Local(local_mode) = &self.mode else {
+ let Mode::Running(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.definition.adapter.clone().into(),
- Mode::Remote(remote_mode) => remote_mode._adapter_name.clone(),
- }
+ self.definition.adapter.clone().into()
}
- pub fn configuration(&self) -> Option<DebugTaskDefinition> {
- if let Mode::Local(local_mode) = &self.mode {
- Some(local_mode.definition.clone())
- } else {
- None
- }
+ pub fn label(&self) -> String {
+ self.definition.label.clone()
+ }
+
+ pub fn definition(&self) -> DebugTaskDefinition {
+ self.definition.clone()
}
pub fn is_terminated(&self) -> bool {
@@ -857,31 +866,33 @@ impl Session {
}
pub fn is_local(&self) -> bool {
- matches!(self.mode, Mode::Local(_))
+ matches!(self.mode, Mode::Running(_))
}
pub fn as_local_mut(&mut self) -> Option<&mut LocalMode> {
match &mut self.mode {
- Mode::Local(local_mode) => Some(local_mode),
- Mode::Remote(_) => None,
+ Mode::Running(local_mode) => Some(local_mode),
+ Mode::Building => None,
}
}
pub fn as_local(&self) -> Option<&LocalMode> {
match &self.mode {
- Mode::Local(local_mode) => Some(local_mode),
- Mode::Remote(_) => None,
+ Mode::Running(local_mode) => Some(local_mode),
+ Mode::Building => None,
}
}
pub(super) fn request_initialize(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
+ let adapter_id = self.definition.adapter.clone();
+ let request = Initialize { adapter_id };
match &self.mode {
- Mode::Local(local_mode) => {
- let capabilities = local_mode.clone().request_initialization(cx);
+ Mode::Running(local_mode) => {
+ let capabilities = local_mode.request(request, cx.background_executor().clone());
cx.spawn(async move |this, cx| {
let capabilities = capabilities.await?;
- this.update(cx, |session, _| {
+ this.update(cx, |session, cx| {
session.capabilities = capabilities;
let filters = session
.capabilities
@@ -895,12 +906,13 @@ impl Session {
.entry(filter.filter.clone())
.or_insert_with(|| (filter, default));
}
+ cx.emit(SessionEvent::CapabilitiesLoaded);
})?;
Ok(())
})
}
- Mode::Remote(_) => Task::ready(Err(anyhow!(
- "Cannot send initialize request from remote session"
+ Mode::Building => Task::ready(Err(anyhow!(
+ "Cannot send initialize request, task still building"
))),
}
}
@@ -912,10 +924,14 @@ impl Session {
cx: &mut Context<Self>,
) -> Task<Result<()>> {
match &self.mode {
- Mode::Local(local_mode) => {
- local_mode.initialize_sequence(&self.capabilities, initialize_rx, dap_store, cx)
- }
- Mode::Remote(_) => Task::ready(Err(anyhow!("cannot initialize remote session"))),
+ Mode::Running(local_mode) => local_mode.initialize_sequence(
+ &self.capabilities,
+ &self.definition,
+ initialize_rx,
+ dap_store,
+ cx,
+ ),
+ Mode::Building => Task::ready(Err(anyhow!("cannot initialize, still building"))),
}
}
@@ -926,7 +942,7 @@ impl Session {
cx: &mut Context<Self>,
) {
match &mut self.mode {
- Mode::Local(local_mode) => {
+ Mode::Running(local_mode) => {
if !matches!(
self.thread_states.thread_state(active_thread_id),
Some(ThreadStatus::Stopped)
@@ -949,7 +965,7 @@ impl Session {
})
.detach();
}
- Mode::Remote(_) => {}
+ Mode::Building => {}
}
}
@@ -983,13 +999,13 @@ impl Session {
body: Option<serde_json::Value>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
- let Some(local_session) = self.as_local().cloned() else {
+ let Some(local_session) = self.as_local() else {
unreachable!("Cannot respond to remote client");
};
+ let client = local_session.client.clone();
cx.background_spawn(async move {
- local_session
- .client
+ client
.send_message(Message::Response(Response {
body,
success,
@@ -1178,7 +1194,6 @@ impl Session {
let task = Self::request_inner::<Arc<T>>(
&self.capabilities,
- self.id,
&self.mode,
command,
process_result,
@@ -1199,7 +1214,6 @@ impl Session {
fn request_inner<T: DapCommand + PartialEq + Eq + Hash>(
capabilities: &Capabilities,
- session_id: SessionId,
mode: &Mode,
request: T,
process_result: impl FnOnce(
@@ -1225,7 +1239,7 @@ impl Session {
});
}
- let request = mode.request_dap(session_id, request, cx);
+ let request = mode.request_dap(request, cx);
cx.spawn(async move |this, cx| {
let result = request.await;
this.update(cx, |this, cx| process_result(this, result, cx))
@@ -1245,14 +1259,7 @@ impl Session {
+ 'static,
cx: &mut Context<Self>,
) -> Task<Option<T::Response>> {
- Self::request_inner(
- &self.capabilities,
- self.id,
- &self.mode,
- request,
- process_result,
- cx,
- )
+ Self::request_inner(&self.capabilities, &self.mode, request, process_result, cx)
}
fn invalidate_command_type<Command: DapCommand>(&mut self) {
@@ -1569,8 +1576,8 @@ impl Session {
pub fn adapter_client(&self) -> Option<Arc<DebugAdapterClient>> {
match self.mode {
- Mode::Local(ref local) => Some(local.client.clone()),
- Mode::Remote(_) => None,
+ Mode::Running(ref local) => Some(local.client.clone()),
+ Mode::Building => None,
}
}
@@ -1936,83 +1943,3 @@ impl Session {
}
}
}
-
-fn create_local_session(
- breakpoint_store: Entity<BreakpointStore>,
- session_id: SessionId,
- parent_session: Option<Entity<Session>>,
- start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
- initialized_tx: oneshot::Sender<()>,
- mut message_rx: futures::channel::mpsc::UnboundedReceiver<Message>,
- mode: LocalMode,
- cx: &mut Context<Session>,
-) -> Session {
- let _background_tasks = vec![cx.spawn(async move |this: WeakEntity<Session>, cx| {
- let mut initialized_tx = Some(initialized_tx);
- while let Some(message) = message_rx.next().await {
- if let Message::Event(event) = message {
- if let Events::Initialized(_) = *event {
- if let Some(tx) = initialized_tx.take() {
- tx.send(()).ok();
- }
- } else {
- let Ok(_) = this.update(cx, |session, cx| {
- session.handle_dap_event(event, cx);
- }) else {
- break;
- };
- }
- } else {
- let Ok(_) = start_debugging_requests_tx.unbounded_send((session_id, message))
- else {
- break;
- };
- }
- }
- })];
-
- cx.subscribe(&breakpoint_store, |this, _, event, cx| match event {
- BreakpointStoreEvent::BreakpointsUpdated(path, reason) => {
- if let Some(local) = (!this.ignore_breakpoints)
- .then(|| this.as_local_mut())
- .flatten()
- {
- local
- .send_breakpoints_from_path(path.clone(), *reason, cx)
- .detach();
- };
- }
- BreakpointStoreEvent::BreakpointsCleared(paths) => {
- if let Some(local) = (!this.ignore_breakpoints)
- .then(|| this.as_local_mut())
- .flatten()
- {
- local.unset_breakpoints_from_paths(paths, cx).detach();
- }
- }
- BreakpointStoreEvent::ActiveDebugLineChanged => {}
- })
- .detach();
-
- Session {
- mode: Mode::Local(mode),
- id: session_id,
- child_session_ids: HashSet::default(),
- parent_id: parent_session.map(|session| session.read(cx).id),
- variables: Default::default(),
- capabilities: Capabilities::default(),
- thread_states: ThreadStates::default(),
- output_token: OutputToken(0),
- ignore_breakpoints: false,
- output: circular_buffer::CircularBuffer::boxed(),
- requests: HashMap::default(),
- modules: Vec::default(),
- loaded_sources: Vec::default(),
- threads: IndexMap::default(),
- stack_frames: IndexMap::default(),
- locations: Default::default(),
- exception_breakpoints: Default::default(),
- _background_tasks,
- is_session_terminated: false,
- }
-}
@@ -1,68 +1,39 @@
use std::{path::Path, sync::Arc};
-use anyhow::Result;
-use dap::{DebugRequest, client::DebugAdapterClient};
-use gpui::{App, AppContext, Entity, Subscription, Task};
-use task::DebugTaskDefinition;
+use dap::client::DebugAdapterClient;
+use gpui::{App, AppContext, Subscription};
-use crate::Project;
-
-use super::session::Session;
+use super::session::{Session, SessionStateEvent};
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
+ let configure = Arc::new(configure);
+ cx.observe_new::<Session>(move |_, _, cx| {
+ let configure = configure.clone();
+ cx.subscribe_self(move |session, event, cx| {
+ let configure = configure.clone();
+ if matches!(event, SessionStateEvent::Running) {
+ 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();
+ }
})
.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: DebugRequest::Launch(Default::default()),
- label: "test".to_string(),
- initialize_args: None,
- tcp_connection: 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
@@ -25,7 +25,6 @@ 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;
@@ -39,17 +38,13 @@ use client::{
};
use clock::ReplicaId;
-use dap::{
- adapters::{DebugAdapterBinary, TcpArguments},
- client::DebugAdapterClient,
-};
+use dap::client::DebugAdapterClient;
use collections::{BTreeSet, HashMap, HashSet};
use debounced_delay::DebouncedDelay;
use debugger::{
breakpoint_store::BreakpointStore,
dap_store::{DapStore, DapStoreEvent},
- session::Session,
};
pub use environment::ProjectEnvironment;
#[cfg(test)]
@@ -97,7 +92,6 @@ use snippet::Snippet;
use snippet_provider::SnippetProvider;
use std::{
borrow::Cow,
- net::Ipv4Addr,
ops::Range,
path::{Component, Path, PathBuf},
pin::pin,
@@ -107,7 +101,7 @@ use std::{
};
use task_store::TaskStore;
-use terminals::{SshCommand, Terminals, wrap_for_ssh};
+use terminals::Terminals;
use text::{Anchor, BufferId};
use toolchain_store::EmptyToolchainStore;
use util::{
@@ -1072,8 +1066,9 @@ impl Project {
let dap_store = cx.new(|cx| {
DapStore::new_ssh(
SSH_PROJECT_ID,
- ssh_proto.clone(),
+ ssh.clone(),
breakpoint_store.clone(),
+ worktree_store.clone(),
cx,
)
});
@@ -1258,6 +1253,7 @@ impl Project {
remote_id,
client.clone().into(),
breakpoint_store.clone(),
+ worktree_store.clone(),
cx,
)
})?;
@@ -1463,79 +1459,6 @@ impl Project {
}
}
- pub fn start_debug_session(
- &mut self,
- definition: DebugTaskDefinition,
- cx: &mut Context<Self>,
- ) -> Task<Result<Entity<Session>>> {
- let Some(worktree) = self.worktrees(cx).find(|tree| tree.read(cx).is_visible()) else {
- return Task::ready(Err(anyhow!("Failed to find a worktree")));
- };
-
- let ssh_client = self.ssh_client().clone();
-
- let result = cx.spawn(async move |this, cx| {
- let mut binary = this
- .update(cx, |this, cx| {
- this.dap_store.update(cx, |dap_store, cx| {
- dap_store.get_debug_adapter_binary(definition.clone(), cx)
- })
- })?
- .await?;
-
- if let Some(ssh_client) = ssh_client {
- let mut ssh_command = ssh_client.update(cx, |ssh, _| {
- anyhow::Ok(SshCommand {
- arguments: ssh
- .ssh_args()
- .ok_or_else(|| anyhow!("SSH arguments not found"))?,
- })
- })??;
-
- let mut connection = None;
- if let Some(c) = binary.connection {
- let local_bind_addr = Ipv4Addr::new(127, 0, 0, 1);
- let port = dap::transport::TcpTransport::unused_port(local_bind_addr).await?;
-
- ssh_command.add_port_forwarding(port, c.host.to_string(), c.port);
- connection = Some(TcpArguments {
- port: c.port,
- host: local_bind_addr,
- timeout: c.timeout,
- })
- }
-
- let (program, args) = wrap_for_ssh(
- &ssh_command,
- Some((&binary.command, &binary.arguments)),
- binary.cwd.as_deref(),
- binary.envs,
- None,
- );
-
- binary = DebugAdapterBinary {
- command: program,
- arguments: args,
- envs: HashMap::default(),
- cwd: None,
- connection,
- request_args: binary.request_args,
- }
- };
-
- let ret = this
- .update(cx, |project, cx| {
- project.dap_store.update(cx, |dap_store, cx| {
- dap_store.new_session(binary, definition, worktree.downgrade(), None, cx)
- })
- })?
- .1
- .await;
- ret
- });
- result
- }
-
#[cfg(any(test, feature = "test-support"))]
pub async fn example(
root_paths: impl IntoIterator<Item = &Path>,
@@ -4,7 +4,7 @@ use anyhow::{Result, anyhow};
use gpui::{Context, Task};
use project::TaskSourceKind;
use remote::ConnectionState;
-use task::{ResolvedTask, SpawnInTerminal, TaskContext, TaskTemplate};
+use task::{DebugTaskDefinition, ResolvedTask, SpawnInTerminal, TaskContext, TaskTemplate};
use ui::Window;
use crate::Workspace;
@@ -109,14 +109,26 @@ impl Workspace {
debug_config.definition
};
- project
- .update(cx, |project, cx| project.start_debug_session(config, cx))?
- .await?;
+ workspace.update_in(cx, |workspace, window, cx| {
+ workspace.start_debug_session(config, window, cx);
+ })?;
+
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
+ pub fn start_debug_session(
+ &mut self,
+ definition: DebugTaskDefinition,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if let Some(provider) = self.debugger_provider.as_mut() {
+ provider.start_session(definition, window, cx)
+ }
+ }
+
pub fn spawn_in_terminal(
self: &mut Workspace,
spawn_in_terminal: SpawnInTerminal,
@@ -96,7 +96,7 @@ use std::{
sync::{Arc, LazyLock, Weak, atomic::AtomicUsize},
time::Duration,
};
-use task::SpawnInTerminal;
+use task::{DebugTaskDefinition, SpawnInTerminal};
use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
pub use ui;
@@ -139,6 +139,10 @@ pub trait TerminalProvider {
) -> Task<Result<ExitStatus>>;
}
+pub trait DebuggerProvider {
+ fn start_session(&self, definition: DebugTaskDefinition, window: &mut Window, cx: &mut App);
+}
+
actions!(
workspace,
[
@@ -860,6 +864,7 @@ pub struct Workspace {
on_prompt_for_new_path: Option<PromptForNewPath>,
on_prompt_for_open_path: Option<PromptForOpenPath>,
terminal_provider: Option<Box<dyn TerminalProvider>>,
+ debugger_provider: Option<Box<dyn DebuggerProvider>>,
serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
serialized_ssh_project: Option<SerializedSshProject>,
_items_serializer: Task<Result<()>>,
@@ -1186,6 +1191,7 @@ impl Workspace {
on_prompt_for_new_path: None,
on_prompt_for_open_path: None,
terminal_provider: None,
+ debugger_provider: None,
serializable_items_tx,
_items_serializer,
session_id: Some(session_id),
@@ -1705,6 +1711,10 @@ impl Workspace {
self.terminal_provider = Some(Box::new(provider));
}
+ pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) {
+ self.debugger_provider = Some(Box::new(provider));
+ }
+
pub fn serialized_ssh_project(&self) -> Option<SerializedSshProject> {
self.serialized_ssh_project.clone()
}
@@ -444,7 +444,7 @@ fn initialize_panels(
window,
async move |workspace: gpui::WeakEntity<Workspace>,
cx: &mut AsyncWindowContext| {
- let debug_panel = DebugPanel::load(workspace.clone(), cx.clone()).await?;
+ let debug_panel = DebugPanel::load(workspace.clone(), cx).await?;
workspace.update_in(cx, |workspace, window, cx| {
workspace.add_panel(debug_panel, window, cx);
})?;