dap_store.rs

  1use super::{
  2    breakpoint_store::BreakpointStore,
  3    dap_command::EvaluateCommand,
  4    locators,
  5    session::{self, Session, SessionStateEvent},
  6};
  7use crate::{
  8    InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState,
  9    debugger::session::SessionQuirks,
 10    project_settings::ProjectSettings,
 11    terminals::{SshCommand, wrap_for_ssh},
 12    worktree_store::WorktreeStore,
 13};
 14use anyhow::{Context as _, Result, anyhow};
 15use async_trait::async_trait;
 16use collections::HashMap;
 17use dap::{
 18    Capabilities, DapRegistry, DebugRequest, EvaluateArgumentsContext, StackFrameId,
 19    adapters::{
 20        DapDelegate, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments,
 21    },
 22    client::SessionId,
 23    inline_value::VariableLookupKind,
 24    messages::Message,
 25};
 26use fs::Fs;
 27use futures::{
 28    StreamExt,
 29    channel::mpsc::{self, UnboundedSender},
 30    future::{Shared, join_all},
 31};
 32use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};
 33use http_client::HttpClient;
 34use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind};
 35use node_runtime::NodeRuntime;
 36
 37use remote::{SshInfo, SshRemoteClient, ssh_session::SshArgs};
 38use rpc::{
 39    AnyProtoClient, TypedEnvelope,
 40    proto::{self},
 41};
 42use serde::{Deserialize, Serialize};
 43use settings::{Settings, SettingsLocation, WorktreeId};
 44use std::{
 45    borrow::Borrow,
 46    collections::BTreeMap,
 47    ffi::OsStr,
 48    net::Ipv4Addr,
 49    path::{Path, PathBuf},
 50    sync::{Arc, Once},
 51};
 52use task::{DebugScenario, SpawnInTerminal, TaskContext, TaskTemplate};
 53use util::ResultExt as _;
 54use worktree::Worktree;
 55
 56#[derive(Debug)]
 57pub enum DapStoreEvent {
 58    DebugClientStarted(SessionId),
 59    DebugSessionInitialized(SessionId),
 60    DebugClientShutdown(SessionId),
 61    DebugClientEvent {
 62        session_id: SessionId,
 63        message: Message,
 64    },
 65    Notification(String),
 66    RemoteHasInitialized,
 67}
 68
 69enum DapStoreMode {
 70    Local(LocalDapStore),
 71    Ssh(SshDapStore),
 72    Collab,
 73}
 74
 75pub struct LocalDapStore {
 76    fs: Arc<dyn Fs>,
 77    node_runtime: NodeRuntime,
 78    http_client: Arc<dyn HttpClient>,
 79    environment: Entity<ProjectEnvironment>,
 80    toolchain_store: Arc<dyn LanguageToolchainStore>,
 81}
 82
 83pub struct SshDapStore {
 84    ssh_client: Entity<SshRemoteClient>,
 85    upstream_client: AnyProtoClient,
 86    upstream_project_id: u64,
 87}
 88
 89pub struct DapStore {
 90    mode: DapStoreMode,
 91    downstream_client: Option<(AnyProtoClient, u64)>,
 92    breakpoint_store: Entity<BreakpointStore>,
 93    worktree_store: Entity<WorktreeStore>,
 94    sessions: BTreeMap<SessionId, Entity<Session>>,
 95    next_session_id: u32,
 96    adapter_options: BTreeMap<DebugAdapterName, Arc<PersistedAdapterOptions>>,
 97}
 98
 99impl EventEmitter<DapStoreEvent> for DapStore {}
100
101#[derive(Clone, Serialize, Deserialize)]
102pub struct PersistedExceptionBreakpoint {
103    pub enabled: bool,
104}
105
106/// Represents best-effort serialization of adapter state during last session (e.g. watches)
107#[derive(Clone, Default, Serialize, Deserialize)]
108pub struct PersistedAdapterOptions {
109    /// Which exception breakpoints were enabled during the last session with this adapter?
110    pub exception_breakpoints: BTreeMap<String, PersistedExceptionBreakpoint>,
111}
112
113impl DapStore {
114    pub fn init(client: &AnyProtoClient, cx: &mut App) {
115        static ADD_LOCATORS: Once = Once::new();
116        ADD_LOCATORS.call_once(|| {
117            let registry = DapRegistry::global(cx);
118            registry.add_locator(Arc::new(locators::cargo::CargoLocator {}));
119            registry.add_locator(Arc::new(locators::go::GoLocator {}));
120            registry.add_locator(Arc::new(locators::node::NodeLocator));
121            registry.add_locator(Arc::new(locators::python::PythonLocator));
122        });
123        client.add_entity_request_handler(Self::handle_run_debug_locator);
124        client.add_entity_request_handler(Self::handle_get_debug_adapter_binary);
125        client.add_entity_message_handler(Self::handle_log_to_debug_console);
126    }
127
128    #[expect(clippy::too_many_arguments)]
129    pub fn new_local(
130        http_client: Arc<dyn HttpClient>,
131        node_runtime: NodeRuntime,
132        fs: Arc<dyn Fs>,
133        environment: Entity<ProjectEnvironment>,
134        toolchain_store: Arc<dyn LanguageToolchainStore>,
135        worktree_store: Entity<WorktreeStore>,
136        breakpoint_store: Entity<BreakpointStore>,
137        cx: &mut Context<Self>,
138    ) -> Self {
139        let mode = DapStoreMode::Local(LocalDapStore {
140            fs,
141            environment,
142            http_client,
143            node_runtime,
144            toolchain_store,
145        });
146
147        Self::new(mode, breakpoint_store, worktree_store, cx)
148    }
149
150    pub fn new_ssh(
151        project_id: u64,
152        ssh_client: Entity<SshRemoteClient>,
153        breakpoint_store: Entity<BreakpointStore>,
154        worktree_store: Entity<WorktreeStore>,
155        cx: &mut Context<Self>,
156    ) -> Self {
157        let mode = DapStoreMode::Ssh(SshDapStore {
158            upstream_client: ssh_client.read(cx).proto_client(),
159            ssh_client,
160            upstream_project_id: project_id,
161        });
162
163        Self::new(mode, breakpoint_store, worktree_store, cx)
164    }
165
166    pub fn new_collab(
167        _project_id: u64,
168        _upstream_client: AnyProtoClient,
169        breakpoint_store: Entity<BreakpointStore>,
170        worktree_store: Entity<WorktreeStore>,
171        cx: &mut Context<Self>,
172    ) -> Self {
173        Self::new(DapStoreMode::Collab, breakpoint_store, worktree_store, cx)
174    }
175
176    fn new(
177        mode: DapStoreMode,
178        breakpoint_store: Entity<BreakpointStore>,
179        worktree_store: Entity<WorktreeStore>,
180        _cx: &mut Context<Self>,
181    ) -> Self {
182        Self {
183            mode,
184            next_session_id: 0,
185            downstream_client: None,
186            breakpoint_store,
187            worktree_store,
188            sessions: Default::default(),
189            adapter_options: Default::default(),
190        }
191    }
192
193    pub fn get_debug_adapter_binary(
194        &mut self,
195        definition: DebugTaskDefinition,
196        session_id: SessionId,
197        worktree: &Entity<Worktree>,
198        console: UnboundedSender<String>,
199        cx: &mut Context<Self>,
200    ) -> Task<Result<DebugAdapterBinary>> {
201        match &self.mode {
202            DapStoreMode::Local(_) => {
203                let Some(adapter) = DapRegistry::global(cx).adapter(&definition.adapter) else {
204                    return Task::ready(Err(anyhow!("Failed to find a debug adapter")));
205                };
206
207                let settings_location = SettingsLocation {
208                    worktree_id: worktree.read(cx).id(),
209                    path: Path::new(""),
210                };
211                let dap_settings = ProjectSettings::get(Some(settings_location), cx)
212                    .dap
213                    .get(&adapter.name());
214                let user_installed_path =
215                    dap_settings.and_then(|s| s.binary.as_ref().map(PathBuf::from));
216                let user_args = dap_settings.map(|s| s.args.clone());
217
218                let delegate = self.delegate(worktree, console, cx);
219                let cwd: Arc<Path> = worktree.read(cx).abs_path().as_ref().into();
220
221                cx.spawn(async move |this, cx| {
222                    let mut binary = adapter
223                        .get_binary(&delegate, &definition, user_installed_path, user_args, cx)
224                        .await?;
225
226                    let env = this
227                        .update(cx, |this, cx| {
228                            this.as_local()
229                                .unwrap()
230                                .environment
231                                .update(cx, |environment, cx| {
232                                    environment.get_directory_environment(cwd, cx)
233                                })
234                        })?
235                        .await;
236
237                    if let Some(mut env) = env {
238                        env.extend(std::mem::take(&mut binary.envs));
239                        binary.envs = env;
240                    }
241
242                    Ok(binary)
243                })
244            }
245            DapStoreMode::Ssh(ssh) => {
246                let request = ssh.upstream_client.request(proto::GetDebugAdapterBinary {
247                    session_id: session_id.to_proto(),
248                    project_id: ssh.upstream_project_id,
249                    worktree_id: worktree.read(cx).id().to_proto(),
250                    definition: Some(definition.to_proto()),
251                });
252                let ssh_client = ssh.ssh_client.clone();
253
254                cx.spawn(async move |_, cx| {
255                    let response = request.await?;
256                    let binary = DebugAdapterBinary::from_proto(response)?;
257                    let (mut ssh_command, envs, path_style, ssh_shell) =
258                        ssh_client.read_with(cx, |ssh, _| {
259                            let SshInfo {
260                                args: SshArgs { arguments, envs },
261                                path_style,
262                                shell,
263                            } = ssh.ssh_info().context("SSH arguments not found")?;
264                            anyhow::Ok((
265                                SshCommand { arguments },
266                                envs.unwrap_or_default(),
267                                path_style,
268                                shell,
269                            ))
270                        })??;
271
272                    let mut connection = None;
273                    if let Some(c) = binary.connection {
274                        let local_bind_addr = Ipv4Addr::LOCALHOST;
275                        let port =
276                            dap::transport::TcpTransport::unused_port(local_bind_addr).await?;
277
278                        ssh_command.add_port_forwarding(port, c.host.to_string(), c.port);
279                        connection = Some(TcpArguments {
280                            port,
281                            host: local_bind_addr,
282                            timeout: c.timeout,
283                        })
284                    }
285
286                    let (program, args) = wrap_for_ssh(
287                        &ssh_shell,
288                        &ssh_command,
289                        binary
290                            .command
291                            .as_ref()
292                            .map(|command| (command, &binary.arguments)),
293                        binary.cwd.as_deref(),
294                        binary.envs,
295                        None,
296                        path_style,
297                    );
298
299                    Ok(DebugAdapterBinary {
300                        command: Some(program),
301                        arguments: args,
302                        envs,
303                        cwd: None,
304                        connection,
305                        request_args: binary.request_args,
306                    })
307                })
308            }
309            DapStoreMode::Collab => {
310                Task::ready(Err(anyhow!("Debugging is not yet supported via collab")))
311            }
312        }
313    }
314
315    pub fn debug_scenario_for_build_task(
316        &self,
317        build: TaskTemplate,
318        adapter: DebugAdapterName,
319        label: SharedString,
320        cx: &mut App,
321    ) -> Task<Option<DebugScenario>> {
322        let locators = DapRegistry::global(cx).locators();
323
324        cx.background_spawn(async move {
325            for locator in locators.values() {
326                if let Some(scenario) = locator.create_scenario(&build, &label, &adapter).await {
327                    return Some(scenario);
328                }
329            }
330            None
331        })
332    }
333
334    pub fn run_debug_locator(
335        &mut self,
336        locator_name: &str,
337        build_command: SpawnInTerminal,
338        cx: &mut Context<Self>,
339    ) -> Task<Result<DebugRequest>> {
340        match &self.mode {
341            DapStoreMode::Local(_) => {
342                // Pre-resolve args with existing environment.
343                let locators = DapRegistry::global(cx).locators();
344                let locator = locators.get(locator_name);
345
346                if let Some(locator) = locator.cloned() {
347                    cx.background_spawn(async move {
348                        let result = locator
349                            .run(build_command.clone())
350                            .await
351                            .log_with_level(log::Level::Error);
352                        if let Some(result) = result {
353                            return Ok(result);
354                        }
355
356                        anyhow::bail!(
357                            "None of the locators for task `{}` completed successfully",
358                            build_command.label
359                        )
360                    })
361                } else {
362                    Task::ready(Err(anyhow!(
363                        "Couldn't find any locator for task `{}`. Specify the `attach` or `launch` arguments in your debug scenario definition",
364                        build_command.label
365                    )))
366                }
367            }
368            DapStoreMode::Ssh(ssh) => {
369                let request = ssh.upstream_client.request(proto::RunDebugLocators {
370                    project_id: ssh.upstream_project_id,
371                    build_command: Some(build_command.to_proto()),
372                    locator: locator_name.to_owned(),
373                });
374                cx.background_spawn(async move {
375                    let response = request.await?;
376                    DebugRequest::from_proto(response)
377                })
378            }
379            DapStoreMode::Collab => {
380                Task::ready(Err(anyhow!("Debugging is not yet supported via collab")))
381            }
382        }
383    }
384
385    fn as_local(&self) -> Option<&LocalDapStore> {
386        match &self.mode {
387            DapStoreMode::Local(local_dap_store) => Some(local_dap_store),
388            _ => None,
389        }
390    }
391
392    pub fn new_session(
393        &mut self,
394        label: Option<SharedString>,
395        adapter: DebugAdapterName,
396        task_context: TaskContext,
397        parent_session: Option<Entity<Session>>,
398        quirks: SessionQuirks,
399        cx: &mut Context<Self>,
400    ) -> Entity<Session> {
401        let session_id = SessionId(util::post_inc(&mut self.next_session_id));
402
403        if let Some(session) = &parent_session {
404            session.update(cx, |session, _| {
405                session.add_child_session_id(session_id);
406            });
407        }
408
409        let session = Session::new(
410            self.breakpoint_store.clone(),
411            session_id,
412            parent_session,
413            label,
414            adapter,
415            task_context,
416            quirks,
417            cx,
418        );
419
420        self.sessions.insert(session_id, session.clone());
421        cx.notify();
422
423        cx.subscribe(&session, {
424            move |this: &mut DapStore, _, event: &SessionStateEvent, cx| match event {
425                SessionStateEvent::Shutdown => {
426                    this.shutdown_session(session_id, cx).detach_and_log_err(cx);
427                }
428                SessionStateEvent::Restart | SessionStateEvent::SpawnChildSession { .. } => {}
429                SessionStateEvent::Running => {
430                    cx.emit(DapStoreEvent::DebugClientStarted(session_id));
431                }
432            }
433        })
434        .detach();
435
436        session
437    }
438
439    pub fn boot_session(
440        &self,
441        session: Entity<Session>,
442        definition: DebugTaskDefinition,
443        worktree: Entity<Worktree>,
444        cx: &mut Context<Self>,
445    ) -> Task<Result<()>> {
446        let dap_store = cx.weak_entity();
447        let console = session.update(cx, |session, cx| session.console_output(cx));
448        let session_id = session.read(cx).session_id();
449
450        cx.spawn({
451            let session = session.clone();
452            async move |this, cx| {
453                let binary = this
454                    .update(cx, |this, cx| {
455                        this.get_debug_adapter_binary(
456                            definition.clone(),
457                            session_id,
458                            &worktree,
459                            console,
460                            cx,
461                        )
462                    })?
463                    .await?;
464                session
465                    .update(cx, |session, cx| {
466                        session.boot(binary, worktree, dap_store, cx)
467                    })?
468                    .await
469            }
470        })
471    }
472
473    pub fn session_by_id(
474        &self,
475        session_id: impl Borrow<SessionId>,
476    ) -> Option<Entity<session::Session>> {
477        let session_id = session_id.borrow();
478
479        self.sessions.get(session_id).cloned()
480    }
481    pub fn sessions(&self) -> impl Iterator<Item = &Entity<Session>> {
482        self.sessions.values()
483    }
484
485    pub fn capabilities_by_id(
486        &self,
487        session_id: impl Borrow<SessionId>,
488        cx: &App,
489    ) -> Option<Capabilities> {
490        let session_id = session_id.borrow();
491        self.sessions
492            .get(session_id)
493            .map(|client| client.read(cx).capabilities.clone())
494    }
495
496    pub fn breakpoint_store(&self) -> &Entity<BreakpointStore> {
497        &self.breakpoint_store
498    }
499
500    pub fn worktree_store(&self) -> &Entity<WorktreeStore> {
501        &self.worktree_store
502    }
503
504    #[allow(dead_code)]
505    async fn handle_ignore_breakpoint_state(
506        this: Entity<Self>,
507        envelope: TypedEnvelope<proto::IgnoreBreakpointState>,
508        mut cx: AsyncApp,
509    ) -> Result<()> {
510        let session_id = SessionId::from_proto(envelope.payload.session_id);
511
512        this.update(&mut cx, |this, cx| {
513            if let Some(session) = this.session_by_id(&session_id) {
514                session.update(cx, |session, cx| {
515                    session.set_ignore_breakpoints(envelope.payload.ignore, cx)
516                })
517            } else {
518                Task::ready(HashMap::default())
519            }
520        })?
521        .await;
522
523        Ok(())
524    }
525
526    fn delegate(
527        &self,
528        worktree: &Entity<Worktree>,
529        console: UnboundedSender<String>,
530        cx: &mut App,
531    ) -> Arc<dyn DapDelegate> {
532        let Some(local_store) = self.as_local() else {
533            unimplemented!("Starting session on remote side");
534        };
535
536        Arc::new(DapAdapterDelegate::new(
537            local_store.fs.clone(),
538            worktree.read(cx).snapshot(),
539            console,
540            local_store.node_runtime.clone(),
541            local_store.http_client.clone(),
542            local_store.toolchain_store.clone(),
543            local_store.environment.update(cx, |env, cx| {
544                env.get_worktree_environment(worktree.clone(), cx)
545            }),
546        ))
547    }
548
549    pub fn resolve_inline_value_locations(
550        &self,
551        session: Entity<Session>,
552        stack_frame_id: StackFrameId,
553        buffer_handle: Entity<Buffer>,
554        inline_value_locations: Vec<dap::inline_value::InlineValueLocation>,
555        cx: &mut Context<Self>,
556    ) -> Task<Result<Vec<InlayHint>>> {
557        let snapshot = buffer_handle.read(cx).snapshot();
558        let local_variables =
559            session
560                .read(cx)
561                .variables_by_stack_frame_id(stack_frame_id, false, true);
562        let global_variables =
563            session
564                .read(cx)
565                .variables_by_stack_frame_id(stack_frame_id, true, false);
566
567        fn format_value(mut value: String) -> String {
568            const LIMIT: usize = 100;
569
570            if let Some(index) = value.find("\n") {
571                value.truncate(index);
572                value.push_str("");
573            }
574
575            if value.len() > LIMIT {
576                let mut index = LIMIT;
577                // If index isn't a char boundary truncate will cause a panic
578                while !value.is_char_boundary(index) {
579                    index -= 1;
580                }
581                value.truncate(index);
582                value.push_str("");
583            }
584
585            format!(": {}", value)
586        }
587
588        cx.spawn(async move |_, cx| {
589            let mut inlay_hints = Vec::with_capacity(inline_value_locations.len());
590            for inline_value_location in inline_value_locations.iter() {
591                let point = snapshot.point_to_point_utf16(language::Point::new(
592                    inline_value_location.row as u32,
593                    inline_value_location.column as u32,
594                ));
595                let position = snapshot.anchor_after(point);
596
597                match inline_value_location.lookup {
598                    VariableLookupKind::Variable => {
599                        let variable_search =
600                            if inline_value_location.scope
601                                == dap::inline_value::VariableScope::Local
602                            {
603                                local_variables.iter().chain(global_variables.iter()).find(
604                                    |variable| variable.name == inline_value_location.variable_name,
605                                )
606                            } else {
607                                global_variables.iter().find(|variable| {
608                                    variable.name == inline_value_location.variable_name
609                                })
610                            };
611
612                        let Some(variable) = variable_search else {
613                            continue;
614                        };
615
616                        inlay_hints.push(InlayHint {
617                            position,
618                            label: InlayHintLabel::String(format_value(variable.value.clone())),
619                            kind: Some(InlayHintKind::Type),
620                            padding_left: false,
621                            padding_right: false,
622                            tooltip: None,
623                            resolve_state: ResolveState::Resolved,
624                        });
625                    }
626                    VariableLookupKind::Expression => {
627                        let Ok(eval_task) = session.read_with(cx, |session, _| {
628                            session.mode.request_dap(EvaluateCommand {
629                                expression: inline_value_location.variable_name.clone(),
630                                frame_id: Some(stack_frame_id),
631                                source: None,
632                                context: Some(EvaluateArgumentsContext::Variables),
633                            })
634                        }) else {
635                            continue;
636                        };
637
638                        if let Some(response) = eval_task.await.log_err() {
639                            inlay_hints.push(InlayHint {
640                                position,
641                                label: InlayHintLabel::String(format_value(response.result)),
642                                kind: Some(InlayHintKind::Type),
643                                padding_left: false,
644                                padding_right: false,
645                                tooltip: None,
646                                resolve_state: ResolveState::Resolved,
647                            });
648                        };
649                    }
650                };
651            }
652
653            Ok(inlay_hints)
654        })
655    }
656
657    pub fn shutdown_sessions(&mut self, cx: &mut Context<Self>) -> Task<()> {
658        let mut tasks = vec![];
659        for session_id in self.sessions.keys().cloned().collect::<Vec<_>>() {
660            tasks.push(self.shutdown_session(session_id, cx));
661        }
662
663        cx.background_executor().spawn(async move {
664            futures::future::join_all(tasks).await;
665        })
666    }
667
668    pub fn shutdown_session(
669        &mut self,
670        session_id: SessionId,
671        cx: &mut Context<Self>,
672    ) -> Task<Result<()>> {
673        let Some(session) = self.sessions.remove(&session_id) else {
674            return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id)));
675        };
676
677        let shutdown_children = session
678            .read(cx)
679            .child_session_ids()
680            .iter()
681            .map(|session_id| self.shutdown_session(*session_id, cx))
682            .collect::<Vec<_>>();
683
684        let shutdown_parent_task = if let Some(parent_session) = session
685            .read(cx)
686            .parent_id(cx)
687            .and_then(|session_id| self.session_by_id(session_id))
688        {
689            let shutdown_id = parent_session.update(cx, |parent_session, _| {
690                parent_session.remove_child_session_id(session_id);
691
692                if parent_session.child_session_ids().is_empty() {
693                    Some(parent_session.session_id())
694                } else {
695                    None
696                }
697            });
698
699            shutdown_id.map(|session_id| self.shutdown_session(session_id, cx))
700        } else {
701            None
702        };
703
704        let shutdown_task = session.update(cx, |this, cx| this.shutdown(cx));
705
706        cx.emit(DapStoreEvent::DebugClientShutdown(session_id));
707
708        cx.background_spawn(async move {
709            if !shutdown_children.is_empty() {
710                let _ = join_all(shutdown_children).await;
711            }
712
713            shutdown_task.await;
714
715            if let Some(parent_task) = shutdown_parent_task {
716                parent_task.await?;
717            }
718
719            Ok(())
720        })
721    }
722
723    pub fn shared(
724        &mut self,
725        project_id: u64,
726        downstream_client: AnyProtoClient,
727        _: &mut Context<Self>,
728    ) {
729        self.downstream_client = Some((downstream_client, project_id));
730    }
731
732    pub fn unshared(&mut self, cx: &mut Context<Self>) {
733        self.downstream_client.take();
734
735        cx.notify();
736    }
737
738    async fn handle_run_debug_locator(
739        this: Entity<Self>,
740        envelope: TypedEnvelope<proto::RunDebugLocators>,
741        mut cx: AsyncApp,
742    ) -> Result<proto::DebugRequest> {
743        let task = envelope
744            .payload
745            .build_command
746            .context("missing definition")?;
747        let build_task = SpawnInTerminal::from_proto(task);
748        let locator = envelope.payload.locator;
749        let request = this
750            .update(&mut cx, |this, cx| {
751                this.run_debug_locator(&locator, build_task, cx)
752            })?
753            .await?;
754
755        Ok(request.to_proto())
756    }
757
758    async fn handle_get_debug_adapter_binary(
759        this: Entity<Self>,
760        envelope: TypedEnvelope<proto::GetDebugAdapterBinary>,
761        mut cx: AsyncApp,
762    ) -> Result<proto::DebugAdapterBinary> {
763        let definition = DebugTaskDefinition::from_proto(
764            envelope.payload.definition.context("missing definition")?,
765        )?;
766        let (tx, mut rx) = mpsc::unbounded();
767        let session_id = envelope.payload.session_id;
768        cx.spawn({
769            let this = this.clone();
770            async move |cx| {
771                while let Some(message) = rx.next().await {
772                    this.read_with(cx, |this, _| {
773                        if let Some((downstream, project_id)) = this.downstream_client.clone() {
774                            downstream
775                                .send(proto::LogToDebugConsole {
776                                    project_id,
777                                    session_id,
778                                    message,
779                                })
780                                .ok();
781                        }
782                    })
783                    .ok();
784                }
785            }
786        })
787        .detach();
788
789        let worktree = this
790            .update(&mut cx, |this, cx| {
791                this.worktree_store
792                    .read(cx)
793                    .worktree_for_id(WorktreeId::from_proto(envelope.payload.worktree_id), cx)
794            })?
795            .context("Failed to find worktree with a given ID")?;
796        let binary = this
797            .update(&mut cx, |this, cx| {
798                this.get_debug_adapter_binary(
799                    definition,
800                    SessionId::from_proto(session_id),
801                    &worktree,
802                    tx,
803                    cx,
804                )
805            })?
806            .await?;
807        Ok(binary.to_proto())
808    }
809
810    async fn handle_log_to_debug_console(
811        this: Entity<Self>,
812        envelope: TypedEnvelope<proto::LogToDebugConsole>,
813        mut cx: AsyncApp,
814    ) -> Result<()> {
815        let session_id = SessionId::from_proto(envelope.payload.session_id);
816        this.update(&mut cx, |this, cx| {
817            let Some(session) = this.sessions.get(&session_id) else {
818                return;
819            };
820            session.update(cx, |session, cx| {
821                session
822                    .console_output(cx)
823                    .unbounded_send(envelope.payload.message)
824                    .ok();
825            })
826        })
827    }
828
829    pub fn sync_adapter_options(
830        &mut self,
831        session: &Entity<Session>,
832        cx: &App,
833    ) -> Arc<PersistedAdapterOptions> {
834        let session = session.read(cx);
835        let adapter = session.adapter();
836        let exceptions = session.exception_breakpoints();
837        let exception_breakpoints = exceptions
838            .map(|(exception, enabled)| {
839                (
840                    exception.filter.clone(),
841                    PersistedExceptionBreakpoint { enabled: *enabled },
842                )
843            })
844            .collect();
845        let options = Arc::new(PersistedAdapterOptions {
846            exception_breakpoints,
847        });
848        self.adapter_options.insert(adapter, options.clone());
849        options
850    }
851
852    pub fn set_adapter_options(
853        &mut self,
854        adapter: DebugAdapterName,
855        options: PersistedAdapterOptions,
856    ) {
857        self.adapter_options.insert(adapter, Arc::new(options));
858    }
859
860    pub fn adapter_options(&self, name: &str) -> Option<Arc<PersistedAdapterOptions>> {
861        self.adapter_options.get(name).cloned()
862    }
863
864    pub fn all_adapter_options(&self) -> &BTreeMap<DebugAdapterName, Arc<PersistedAdapterOptions>> {
865        &self.adapter_options
866    }
867}
868
869#[derive(Clone)]
870pub struct DapAdapterDelegate {
871    fs: Arc<dyn Fs>,
872    console: mpsc::UnboundedSender<String>,
873    worktree: worktree::Snapshot,
874    node_runtime: NodeRuntime,
875    http_client: Arc<dyn HttpClient>,
876    toolchain_store: Arc<dyn LanguageToolchainStore>,
877    load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
878}
879
880impl DapAdapterDelegate {
881    pub fn new(
882        fs: Arc<dyn Fs>,
883        worktree: worktree::Snapshot,
884        status: mpsc::UnboundedSender<String>,
885        node_runtime: NodeRuntime,
886        http_client: Arc<dyn HttpClient>,
887        toolchain_store: Arc<dyn LanguageToolchainStore>,
888        load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
889    ) -> Self {
890        Self {
891            fs,
892            console: status,
893            worktree,
894            http_client,
895            node_runtime,
896            toolchain_store,
897            load_shell_env_task,
898        }
899    }
900}
901
902#[async_trait]
903impl dap::adapters::DapDelegate for DapAdapterDelegate {
904    fn worktree_id(&self) -> WorktreeId {
905        self.worktree.id()
906    }
907
908    fn worktree_root_path(&self) -> &Path {
909        self.worktree.abs_path()
910    }
911    fn http_client(&self) -> Arc<dyn HttpClient> {
912        self.http_client.clone()
913    }
914
915    fn node_runtime(&self) -> NodeRuntime {
916        self.node_runtime.clone()
917    }
918
919    fn fs(&self) -> Arc<dyn Fs> {
920        self.fs.clone()
921    }
922
923    fn output_to_console(&self, msg: String) {
924        self.console.unbounded_send(msg).ok();
925    }
926
927    #[cfg(not(target_os = "windows"))]
928    async fn which(&self, command: &OsStr) -> Option<PathBuf> {
929        let worktree_abs_path = self.worktree.abs_path();
930        let shell_path = self.shell_env().await.get("PATH").cloned();
931        which::which_in(command, shell_path.as_ref(), worktree_abs_path).ok()
932    }
933
934    #[cfg(target_os = "windows")]
935    async fn which(&self, command: &OsStr) -> Option<PathBuf> {
936        // On Windows, `PATH` is handled differently from Unix. Windows generally expects users to modify the `PATH` themselves,
937        // and every program loads it directly from the system at startup.
938        // There's also no concept of a default shell on Windows, and you can't really retrieve one, so trying to get shell environment variables
939        // from a specific directory doesn’t make sense on Windows.
940        which::which(command).ok()
941    }
942
943    async fn shell_env(&self) -> HashMap<String, String> {
944        let task = self.load_shell_env_task.clone();
945        task.await.unwrap_or_default()
946    }
947
948    fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
949        self.toolchain_store.clone()
950    }
951    async fn read_text_file(&self, path: PathBuf) -> Result<String> {
952        let entry = self
953            .worktree
954            .entry_for_path(&path)
955            .with_context(|| format!("no worktree entry for path {path:?}"))?;
956        let abs_path = self
957            .worktree
958            .absolutize(&entry.path)
959            .with_context(|| format!("cannot absolutize path {path:?}"))?;
960
961        self.fs.load(&abs_path).await
962    }
963}