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