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