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