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