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