dap_store.rs

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