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::{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) =
258                        ssh_client.read_with(cx, |ssh, _| {
259                            let (SshArgs { arguments, envs }, path_style) =
260                                ssh.ssh_info().context("SSH arguments not found")?;
261                            anyhow::Ok((
262                                SshCommand { arguments },
263                                envs.unwrap_or_default(),
264                                path_style,
265                            ))
266                        })??;
267
268                    let mut connection = None;
269                    if let Some(c) = binary.connection {
270                        let local_bind_addr = Ipv4Addr::LOCALHOST;
271                        let port =
272                            dap::transport::TcpTransport::unused_port(local_bind_addr).await?;
273
274                        ssh_command.add_port_forwarding(port, c.host.to_string(), c.port);
275                        connection = Some(TcpArguments {
276                            port,
277                            host: local_bind_addr,
278                            timeout: c.timeout,
279                        })
280                    }
281
282                    let (program, args) = wrap_for_ssh(
283                        &ssh_command,
284                        binary
285                            .command
286                            .as_ref()
287                            .map(|command| (command, &binary.arguments)),
288                        binary.cwd.as_deref(),
289                        binary.envs,
290                        None,
291                        path_style,
292                    );
293
294                    Ok(DebugAdapterBinary {
295                        command: Some(program),
296                        arguments: args,
297                        envs,
298                        cwd: None,
299                        connection,
300                        request_args: binary.request_args,
301                    })
302                })
303            }
304            DapStoreMode::Collab => {
305                Task::ready(Err(anyhow!("Debugging is not yet supported via collab")))
306            }
307        }
308    }
309
310    pub fn debug_scenario_for_build_task(
311        &self,
312        build: TaskTemplate,
313        adapter: DebugAdapterName,
314        label: SharedString,
315        cx: &mut App,
316    ) -> Task<Option<DebugScenario>> {
317        let locators = DapRegistry::global(cx).locators();
318
319        cx.background_spawn(async move {
320            for locator in locators.values() {
321                if let Some(scenario) = locator.create_scenario(&build, &label, &adapter).await {
322                    return Some(scenario);
323                }
324            }
325            None
326        })
327    }
328
329    pub fn run_debug_locator(
330        &mut self,
331        locator_name: &str,
332        build_command: SpawnInTerminal,
333        cx: &mut Context<Self>,
334    ) -> Task<Result<DebugRequest>> {
335        match &self.mode {
336            DapStoreMode::Local(_) => {
337                // Pre-resolve args with existing environment.
338                let locators = DapRegistry::global(cx).locators();
339                let locator = locators.get(locator_name);
340
341                if let Some(locator) = locator.cloned() {
342                    cx.background_spawn(async move {
343                        let result = locator
344                            .run(build_command.clone())
345                            .await
346                            .log_with_level(log::Level::Error);
347                        if let Some(result) = result {
348                            return Ok(result);
349                        }
350
351                        anyhow::bail!(
352                            "None of the locators for task `{}` completed successfully",
353                            build_command.label
354                        )
355                    })
356                } else {
357                    Task::ready(Err(anyhow!(
358                        "Couldn't find any locator for task `{}`. Specify the `attach` or `launch` arguments in your debug scenario definition",
359                        build_command.label
360                    )))
361                }
362            }
363            DapStoreMode::Ssh(ssh) => {
364                let request = ssh.upstream_client.request(proto::RunDebugLocators {
365                    project_id: ssh.upstream_project_id,
366                    build_command: Some(build_command.to_proto()),
367                    locator: locator_name.to_owned(),
368                });
369                cx.background_spawn(async move {
370                    let response = request.await?;
371                    DebugRequest::from_proto(response)
372                })
373            }
374            DapStoreMode::Collab => {
375                Task::ready(Err(anyhow!("Debugging is not yet supported via collab")))
376            }
377        }
378    }
379
380    fn as_local(&self) -> Option<&LocalDapStore> {
381        match &self.mode {
382            DapStoreMode::Local(local_dap_store) => Some(local_dap_store),
383            _ => None,
384        }
385    }
386
387    pub fn new_session(
388        &mut self,
389        label: Option<SharedString>,
390        adapter: DebugAdapterName,
391        task_context: TaskContext,
392        parent_session: Option<Entity<Session>>,
393        quirks: SessionQuirks,
394        cx: &mut Context<Self>,
395    ) -> Entity<Session> {
396        let session_id = SessionId(util::post_inc(&mut self.next_session_id));
397
398        if let Some(session) = &parent_session {
399            session.update(cx, |session, _| {
400                session.add_child_session_id(session_id);
401            });
402        }
403
404        let session = Session::new(
405            self.breakpoint_store.clone(),
406            session_id,
407            parent_session,
408            label,
409            adapter,
410            task_context,
411            quirks,
412            cx,
413        );
414
415        self.sessions.insert(session_id, session.clone());
416        cx.notify();
417
418        cx.subscribe(&session, {
419            move |this: &mut DapStore, _, event: &SessionStateEvent, cx| match event {
420                SessionStateEvent::Shutdown => {
421                    this.shutdown_session(session_id, cx).detach_and_log_err(cx);
422                }
423                SessionStateEvent::Restart | SessionStateEvent::SpawnChildSession { .. } => {}
424                SessionStateEvent::Running => {
425                    cx.emit(DapStoreEvent::DebugClientStarted(session_id));
426                }
427            }
428        })
429        .detach();
430
431        session
432    }
433
434    pub fn boot_session(
435        &self,
436        session: Entity<Session>,
437        definition: DebugTaskDefinition,
438        worktree: Entity<Worktree>,
439        cx: &mut Context<Self>,
440    ) -> Task<Result<()>> {
441        let dap_store = cx.weak_entity();
442        let console = session.update(cx, |session, cx| session.console_output(cx));
443        let session_id = session.read(cx).session_id();
444
445        cx.spawn({
446            let session = session.clone();
447            async move |this, cx| {
448                let binary = this
449                    .update(cx, |this, cx| {
450                        this.get_debug_adapter_binary(
451                            definition.clone(),
452                            session_id,
453                            &worktree,
454                            console,
455                            cx,
456                        )
457                    })?
458                    .await?;
459                session
460                    .update(cx, |session, cx| {
461                        session.boot(binary, worktree, dap_store, cx)
462                    })?
463                    .await
464            }
465        })
466    }
467
468    pub fn session_by_id(
469        &self,
470        session_id: impl Borrow<SessionId>,
471    ) -> Option<Entity<session::Session>> {
472        let session_id = session_id.borrow();
473        let client = self.sessions.get(session_id).cloned();
474
475        client
476    }
477    pub fn sessions(&self) -> impl Iterator<Item = &Entity<Session>> {
478        self.sessions.values()
479    }
480
481    pub fn capabilities_by_id(
482        &self,
483        session_id: impl Borrow<SessionId>,
484        cx: &App,
485    ) -> Option<Capabilities> {
486        let session_id = session_id.borrow();
487        self.sessions
488            .get(session_id)
489            .map(|client| client.read(cx).capabilities.clone())
490    }
491
492    pub fn breakpoint_store(&self) -> &Entity<BreakpointStore> {
493        &self.breakpoint_store
494    }
495
496    pub fn worktree_store(&self) -> &Entity<WorktreeStore> {
497        &self.worktree_store
498    }
499
500    #[allow(dead_code)]
501    async fn handle_ignore_breakpoint_state(
502        this: Entity<Self>,
503        envelope: TypedEnvelope<proto::IgnoreBreakpointState>,
504        mut cx: AsyncApp,
505    ) -> Result<()> {
506        let session_id = SessionId::from_proto(envelope.payload.session_id);
507
508        this.update(&mut cx, |this, cx| {
509            if let Some(session) = this.session_by_id(&session_id) {
510                session.update(cx, |session, cx| {
511                    session.set_ignore_breakpoints(envelope.payload.ignore, cx)
512                })
513            } else {
514                Task::ready(HashMap::default())
515            }
516        })?
517        .await;
518
519        Ok(())
520    }
521
522    fn delegate(
523        &self,
524        worktree: &Entity<Worktree>,
525        console: UnboundedSender<String>,
526        cx: &mut App,
527    ) -> Arc<dyn DapDelegate> {
528        let Some(local_store) = self.as_local() else {
529            unimplemented!("Starting session on remote side");
530        };
531
532        Arc::new(DapAdapterDelegate::new(
533            local_store.fs.clone(),
534            worktree.read(cx).snapshot(),
535            console,
536            local_store.node_runtime.clone(),
537            local_store.http_client.clone(),
538            local_store.toolchain_store.clone(),
539            local_store.environment.update(cx, |env, cx| {
540                env.get_worktree_environment(worktree.clone(), cx)
541            }),
542        ))
543    }
544
545    pub fn resolve_inline_value_locations(
546        &self,
547        session: Entity<Session>,
548        stack_frame_id: StackFrameId,
549        buffer_handle: Entity<Buffer>,
550        inline_value_locations: Vec<dap::inline_value::InlineValueLocation>,
551        cx: &mut Context<Self>,
552    ) -> Task<Result<Vec<InlayHint>>> {
553        let snapshot = buffer_handle.read(cx).snapshot();
554        let local_variables =
555            session
556                .read(cx)
557                .variables_by_stack_frame_id(stack_frame_id, false, true);
558        let global_variables =
559            session
560                .read(cx)
561                .variables_by_stack_frame_id(stack_frame_id, true, false);
562
563        fn format_value(mut value: String) -> String {
564            const LIMIT: usize = 100;
565
566            if let Some(index) = value.find("\n") {
567                value.truncate(index);
568                value.push_str("");
569            }
570
571            if value.len() > LIMIT {
572                let mut index = LIMIT;
573                // If index isn't a char boundary truncate will cause a panic
574                while !value.is_char_boundary(index) {
575                    index -= 1;
576                }
577                value.truncate(index);
578                value.push_str("");
579            }
580
581            format!(": {}", value)
582        }
583
584        cx.spawn(async move |_, cx| {
585            let mut inlay_hints = Vec::with_capacity(inline_value_locations.len());
586            for inline_value_location in inline_value_locations.iter() {
587                let point = snapshot.point_to_point_utf16(language::Point::new(
588                    inline_value_location.row as u32,
589                    inline_value_location.column as u32,
590                ));
591                let position = snapshot.anchor_after(point);
592
593                match inline_value_location.lookup {
594                    VariableLookupKind::Variable => {
595                        let variable_search =
596                            if inline_value_location.scope
597                                == dap::inline_value::VariableScope::Local
598                            {
599                                local_variables.iter().chain(global_variables.iter()).find(
600                                    |variable| variable.name == inline_value_location.variable_name,
601                                )
602                            } else {
603                                global_variables.iter().find(|variable| {
604                                    variable.name == inline_value_location.variable_name
605                                })
606                            };
607
608                        let Some(variable) = variable_search else {
609                            continue;
610                        };
611
612                        inlay_hints.push(InlayHint {
613                            position,
614                            label: InlayHintLabel::String(format_value(variable.value.clone())),
615                            kind: Some(InlayHintKind::Type),
616                            padding_left: false,
617                            padding_right: false,
618                            tooltip: None,
619                            resolve_state: ResolveState::Resolved,
620                        });
621                    }
622                    VariableLookupKind::Expression => {
623                        let Ok(eval_task) = session.read_with(cx, |session, _| {
624                            session.mode.request_dap(EvaluateCommand {
625                                expression: inline_value_location.variable_name.clone(),
626                                frame_id: Some(stack_frame_id),
627                                source: None,
628                                context: Some(EvaluateArgumentsContext::Variables),
629                            })
630                        }) else {
631                            continue;
632                        };
633
634                        if let Some(response) = eval_task.await.log_err() {
635                            inlay_hints.push(InlayHint {
636                                position,
637                                label: InlayHintLabel::String(format_value(response.result)),
638                                kind: Some(InlayHintKind::Type),
639                                padding_left: false,
640                                padding_right: false,
641                                tooltip: None,
642                                resolve_state: ResolveState::Resolved,
643                            });
644                        };
645                    }
646                };
647            }
648
649            Ok(inlay_hints)
650        })
651    }
652
653    pub fn shutdown_sessions(&mut self, cx: &mut Context<Self>) -> Task<()> {
654        let mut tasks = vec![];
655        for session_id in self.sessions.keys().cloned().collect::<Vec<_>>() {
656            tasks.push(self.shutdown_session(session_id, cx));
657        }
658
659        cx.background_executor().spawn(async move {
660            futures::future::join_all(tasks).await;
661        })
662    }
663
664    pub fn shutdown_session(
665        &mut self,
666        session_id: SessionId,
667        cx: &mut Context<Self>,
668    ) -> Task<Result<()>> {
669        let Some(session) = self.sessions.remove(&session_id) else {
670            return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id)));
671        };
672
673        let shutdown_children = session
674            .read(cx)
675            .child_session_ids()
676            .iter()
677            .map(|session_id| self.shutdown_session(*session_id, cx))
678            .collect::<Vec<_>>();
679
680        let shutdown_parent_task = if let Some(parent_session) = session
681            .read(cx)
682            .parent_id(cx)
683            .and_then(|session_id| self.session_by_id(session_id))
684        {
685            let shutdown_id = parent_session.update(cx, |parent_session, _| {
686                parent_session.remove_child_session_id(session_id);
687
688                if parent_session.child_session_ids().len() == 0 {
689                    Some(parent_session.session_id())
690                } else {
691                    None
692                }
693            });
694
695            shutdown_id.map(|session_id| self.shutdown_session(session_id, cx))
696        } else {
697            None
698        };
699
700        let shutdown_task = session.update(cx, |this, cx| this.shutdown(cx));
701
702        cx.emit(DapStoreEvent::DebugClientShutdown(session_id));
703
704        cx.background_spawn(async move {
705            if shutdown_children.len() > 0 {
706                let _ = join_all(shutdown_children).await;
707            }
708
709            shutdown_task.await;
710
711            if let Some(parent_task) = shutdown_parent_task {
712                parent_task.await?;
713            }
714
715            Ok(())
716        })
717    }
718
719    pub fn shared(
720        &mut self,
721        project_id: u64,
722        downstream_client: AnyProtoClient,
723        _: &mut Context<Self>,
724    ) {
725        self.downstream_client = Some((downstream_client.clone(), project_id));
726    }
727
728    pub fn unshared(&mut self, cx: &mut Context<Self>) {
729        self.downstream_client.take();
730
731        cx.notify();
732    }
733
734    async fn handle_run_debug_locator(
735        this: Entity<Self>,
736        envelope: TypedEnvelope<proto::RunDebugLocators>,
737        mut cx: AsyncApp,
738    ) -> Result<proto::DebugRequest> {
739        let task = envelope
740            .payload
741            .build_command
742            .context("missing definition")?;
743        let build_task = SpawnInTerminal::from_proto(task);
744        let locator = envelope.payload.locator;
745        let request = this
746            .update(&mut cx, |this, cx| {
747                this.run_debug_locator(&locator, build_task, cx)
748            })?
749            .await?;
750
751        Ok(request.to_proto())
752    }
753
754    async fn handle_get_debug_adapter_binary(
755        this: Entity<Self>,
756        envelope: TypedEnvelope<proto::GetDebugAdapterBinary>,
757        mut cx: AsyncApp,
758    ) -> Result<proto::DebugAdapterBinary> {
759        let definition = DebugTaskDefinition::from_proto(
760            envelope.payload.definition.context("missing definition")?,
761        )?;
762        let (tx, mut rx) = mpsc::unbounded();
763        let session_id = envelope.payload.session_id;
764        cx.spawn({
765            let this = this.clone();
766            async move |cx| {
767                while let Some(message) = rx.next().await {
768                    this.read_with(cx, |this, _| {
769                        if let Some((downstream, project_id)) = this.downstream_client.clone() {
770                            downstream
771                                .send(proto::LogToDebugConsole {
772                                    project_id,
773                                    session_id,
774                                    message,
775                                })
776                                .ok();
777                        }
778                    })
779                    .ok();
780                }
781            }
782        })
783        .detach();
784
785        let worktree = this
786            .update(&mut cx, |this, cx| {
787                this.worktree_store
788                    .read(cx)
789                    .worktree_for_id(WorktreeId::from_proto(envelope.payload.worktree_id), cx)
790            })?
791            .context("Failed to find worktree with a given ID")?;
792        let binary = this
793            .update(&mut cx, |this, cx| {
794                this.get_debug_adapter_binary(
795                    definition,
796                    SessionId::from_proto(session_id),
797                    &worktree,
798                    tx,
799                    cx,
800                )
801            })?
802            .await?;
803        Ok(binary.to_proto())
804    }
805
806    async fn handle_log_to_debug_console(
807        this: Entity<Self>,
808        envelope: TypedEnvelope<proto::LogToDebugConsole>,
809        mut cx: AsyncApp,
810    ) -> Result<()> {
811        let session_id = SessionId::from_proto(envelope.payload.session_id);
812        this.update(&mut cx, |this, cx| {
813            let Some(session) = this.sessions.get(&session_id) else {
814                return;
815            };
816            session.update(cx, |session, cx| {
817                session
818                    .console_output(cx)
819                    .unbounded_send(envelope.payload.message)
820                    .ok();
821            })
822        })
823    }
824
825    pub fn sync_adapter_options(
826        &mut self,
827        session: &Entity<Session>,
828        cx: &App,
829    ) -> Arc<PersistedAdapterOptions> {
830        let session = session.read(cx);
831        let adapter = session.adapter();
832        let exceptions = session.exception_breakpoints();
833        let exception_breakpoints = exceptions
834            .map(|(exception, enabled)| {
835                (
836                    exception.filter.clone(),
837                    PersistedExceptionBreakpoint { enabled: *enabled },
838                )
839            })
840            .collect();
841        let options = Arc::new(PersistedAdapterOptions {
842            exception_breakpoints,
843        });
844        self.adapter_options.insert(adapter, options.clone());
845        options
846    }
847
848    pub fn set_adapter_options(
849        &mut self,
850        adapter: DebugAdapterName,
851        options: PersistedAdapterOptions,
852    ) {
853        self.adapter_options.insert(adapter, Arc::new(options));
854    }
855
856    pub fn adapter_options(&self, name: &str) -> Option<Arc<PersistedAdapterOptions>> {
857        self.adapter_options.get(name).cloned()
858    }
859
860    pub fn all_adapter_options(&self) -> &BTreeMap<DebugAdapterName, Arc<PersistedAdapterOptions>> {
861        &self.adapter_options
862    }
863}
864
865#[derive(Clone)]
866pub struct DapAdapterDelegate {
867    fs: Arc<dyn Fs>,
868    console: mpsc::UnboundedSender<String>,
869    worktree: worktree::Snapshot,
870    node_runtime: NodeRuntime,
871    http_client: Arc<dyn HttpClient>,
872    toolchain_store: Arc<dyn LanguageToolchainStore>,
873    load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
874}
875
876impl DapAdapterDelegate {
877    pub fn new(
878        fs: Arc<dyn Fs>,
879        worktree: worktree::Snapshot,
880        status: mpsc::UnboundedSender<String>,
881        node_runtime: NodeRuntime,
882        http_client: Arc<dyn HttpClient>,
883        toolchain_store: Arc<dyn LanguageToolchainStore>,
884        load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
885    ) -> Self {
886        Self {
887            fs,
888            console: status,
889            worktree,
890            http_client,
891            node_runtime,
892            toolchain_store,
893            load_shell_env_task,
894        }
895    }
896}
897
898#[async_trait]
899impl dap::adapters::DapDelegate for DapAdapterDelegate {
900    fn worktree_id(&self) -> WorktreeId {
901        self.worktree.id()
902    }
903
904    fn worktree_root_path(&self) -> &Path {
905        &self.worktree.abs_path()
906    }
907    fn http_client(&self) -> Arc<dyn HttpClient> {
908        self.http_client.clone()
909    }
910
911    fn node_runtime(&self) -> NodeRuntime {
912        self.node_runtime.clone()
913    }
914
915    fn fs(&self) -> Arc<dyn Fs> {
916        self.fs.clone()
917    }
918
919    fn output_to_console(&self, msg: String) {
920        self.console.unbounded_send(msg).ok();
921    }
922
923    #[cfg(not(target_os = "windows"))]
924    async fn which(&self, command: &OsStr) -> Option<PathBuf> {
925        let worktree_abs_path = self.worktree.abs_path();
926        let shell_path = self.shell_env().await.get("PATH").cloned();
927        which::which_in(command, shell_path.as_ref(), worktree_abs_path).ok()
928    }
929
930    #[cfg(target_os = "windows")]
931    async fn which(&self, command: &OsStr) -> Option<PathBuf> {
932        // On Windows, `PATH` is handled differently from Unix. Windows generally expects users to modify the `PATH` themselves,
933        // and every program loads it directly from the system at startup.
934        // 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
935        // from a specific directory doesn’t make sense on Windows.
936        which::which(command).ok()
937    }
938
939    async fn shell_env(&self) -> HashMap<String, String> {
940        let task = self.load_shell_env_task.clone();
941        task.await.unwrap_or_default()
942    }
943
944    fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
945        self.toolchain_store.clone()
946    }
947    async fn read_text_file(&self, path: PathBuf) -> Result<String> {
948        let entry = self
949            .worktree
950            .entry_for_path(&path)
951            .with_context(|| format!("no worktree entry for path {path:?}"))?;
952        let abs_path = self
953            .worktree
954            .absolutize(&entry.path)
955            .with_context(|| format!("cannot absolutize path {path:?}"))?;
956
957        self.fs.load(&abs_path).await
958    }
959}