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