8c25f7d4aa
Prefer directory-capable adapters when selecting a launch adapter for a resolved directory program. This keeps Go package directories on dlv in mixed projects that also expose native-debugger root markers such as a Makefile, instead of selecting gdb/lldb-dap first and rejecting the directory during validation. Added a regression test covering a Go module with both go.mod and Makefile plus local dlv/gdb adapter shims. Fixes #2020
190 lines
7.1 KiB
TypeScript
190 lines
7.1 KiB
TypeScript
import * as path from "node:path";
|
|
import { isRecord } from "@oh-my-pi/pi-utils";
|
|
import { hasRootMarkers, resolveCommand } from "../lsp/config";
|
|
import DEFAULTS from "./defaults.json" with { type: "json" };
|
|
import type { DapAdapterConfig, DapResolvedAdapter } from "./types";
|
|
|
|
const EXTENSIONLESS_DEBUGGER_ORDER = ["gdb", "lldb-dap"] as const;
|
|
|
|
function normalizeStringArray(value: unknown): string[] {
|
|
if (!Array.isArray(value)) return [];
|
|
return value.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
|
|
}
|
|
|
|
function normalizeObject(value: unknown): Record<string, unknown> {
|
|
return isRecord(value) ? { ...value } : {};
|
|
}
|
|
|
|
function normalizeAdapterConfig(config: unknown): DapAdapterConfig | null {
|
|
if (!isRecord(config)) return null;
|
|
if (typeof config.command !== "string" || config.command.length === 0) return null;
|
|
const connectMode = config.connectMode === "socket" ? ("socket" as const) : undefined;
|
|
return {
|
|
command: config.command,
|
|
args: normalizeStringArray(config.args),
|
|
languages: normalizeStringArray(config.languages),
|
|
fileTypes: normalizeStringArray(config.fileTypes).map(entry => entry.toLowerCase()),
|
|
rootMarkers: normalizeStringArray(config.rootMarkers),
|
|
launchDefaults: normalizeObject(config.launchDefaults),
|
|
attachDefaults: normalizeObject(config.attachDefaults),
|
|
acceptsDirectoryProgram: config.acceptsDirectoryProgram === true,
|
|
...(connectMode ? { connectMode } : {}),
|
|
};
|
|
}
|
|
|
|
function getDefaults(): Record<string, DapAdapterConfig> {
|
|
const adapters: Record<string, DapAdapterConfig> = {};
|
|
for (const [name, config] of Object.entries(DEFAULTS)) {
|
|
const normalized = normalizeAdapterConfig(config);
|
|
if (normalized) {
|
|
adapters[name] = normalized;
|
|
}
|
|
}
|
|
return adapters;
|
|
}
|
|
|
|
const DEFAULT_ADAPTERS = getDefaults();
|
|
|
|
export function getAdapterConfigs(): Record<string, DapAdapterConfig> {
|
|
return { ...DEFAULT_ADAPTERS };
|
|
}
|
|
|
|
export function resolveAdapter(adapterName: string, cwd: string): DapResolvedAdapter | null {
|
|
const config = DEFAULT_ADAPTERS[adapterName];
|
|
if (!config) return null;
|
|
const resolvedCommand = resolveCommand(config.command, cwd);
|
|
if (!resolvedCommand) return null;
|
|
return {
|
|
name: adapterName,
|
|
command: config.command,
|
|
args: config.args ?? [],
|
|
resolvedCommand,
|
|
languages: config.languages ?? [],
|
|
fileTypes: config.fileTypes ?? [],
|
|
rootMarkers: config.rootMarkers ?? [],
|
|
launchDefaults: config.launchDefaults ?? {},
|
|
attachDefaults: config.attachDefaults ?? {},
|
|
connectMode: config.connectMode ?? "stdio",
|
|
acceptsDirectoryProgram: config.acceptsDirectoryProgram === true,
|
|
};
|
|
}
|
|
|
|
export function getAvailableAdapters(cwd: string): DapResolvedAdapter[] {
|
|
return Object.keys(DEFAULT_ADAPTERS)
|
|
.map(name => resolveAdapter(name, cwd))
|
|
.filter((adapter): adapter is DapResolvedAdapter => adapter !== null);
|
|
}
|
|
|
|
function getMatchingAdapters(program: string, cwd: string): DapResolvedAdapter[] {
|
|
const extension = path.extname(program).toLowerCase();
|
|
const available = getAvailableAdapters(cwd);
|
|
if (!extension) {
|
|
// For extensionless binaries, only consider native debuggers (gdb, lldb-dap)
|
|
// or adapters that match by root markers. Don't silently fall back to
|
|
// unrelated adapters like debugpy for a C binary.
|
|
const nativeDebuggers: ReadonlySet<string> = new Set(EXTENSIONLESS_DEBUGGER_ORDER);
|
|
return available.filter(
|
|
adapter =>
|
|
nativeDebuggers.has(adapter.name) ||
|
|
(adapter.rootMarkers.length > 0 && hasRootMarkers(cwd, adapter.rootMarkers)),
|
|
);
|
|
}
|
|
const exactMatches = available.filter(adapter => adapter.fileTypes.includes(extension));
|
|
if (exactMatches.length > 0) {
|
|
return exactMatches;
|
|
}
|
|
return available;
|
|
}
|
|
|
|
function sortAdaptersForLaunch(program: string, cwd: string, adapters: DapResolvedAdapter[]): DapResolvedAdapter[] {
|
|
const extension = path.extname(program).toLowerCase();
|
|
const rootAware = adapters.map(adapter => ({
|
|
adapter,
|
|
hasExtensionMatch: extension.length > 0 && adapter.fileTypes.includes(extension),
|
|
hasRootMatch: adapter.rootMarkers.length > 0 && hasRootMarkers(cwd, adapter.rootMarkers),
|
|
}));
|
|
rootAware.sort((left, right) => {
|
|
if (left.hasExtensionMatch !== right.hasExtensionMatch) {
|
|
return left.hasExtensionMatch ? -1 : 1;
|
|
}
|
|
if (left.hasRootMatch !== right.hasRootMatch) {
|
|
return left.hasRootMatch ? -1 : 1;
|
|
}
|
|
const leftDebuggerRank = EXTENSIONLESS_DEBUGGER_ORDER.indexOf(
|
|
left.adapter.name as (typeof EXTENSIONLESS_DEBUGGER_ORDER)[number],
|
|
);
|
|
const rightDebuggerRank = EXTENSIONLESS_DEBUGGER_ORDER.indexOf(
|
|
right.adapter.name as (typeof EXTENSIONLESS_DEBUGGER_ORDER)[number],
|
|
);
|
|
const normalizedLeftRank = leftDebuggerRank === -1 ? Number.MAX_SAFE_INTEGER : leftDebuggerRank;
|
|
const normalizedRightRank = rightDebuggerRank === -1 ? Number.MAX_SAFE_INTEGER : rightDebuggerRank;
|
|
if (normalizedLeftRank !== normalizedRightRank) {
|
|
return normalizedLeftRank - normalizedRightRank;
|
|
}
|
|
return left.adapter.name.localeCompare(right.adapter.name);
|
|
});
|
|
return rootAware.map(entry => entry.adapter);
|
|
}
|
|
|
|
export function selectLaunchAdapter(
|
|
program: string,
|
|
cwd: string,
|
|
adapterName?: string,
|
|
programKind: LaunchProgramKind = "file",
|
|
): DapResolvedAdapter | null {
|
|
if (adapterName) {
|
|
return resolveAdapter(adapterName, cwd);
|
|
}
|
|
const matches = getMatchingAdapters(program, cwd);
|
|
const candidates =
|
|
programKind === "directory" ? matches.filter(adapter => adapter.acceptsDirectoryProgram) : matches;
|
|
const sorted = sortAdaptersForLaunch(program, cwd, candidates.length > 0 ? candidates : matches);
|
|
return sorted[0] ?? null;
|
|
}
|
|
|
|
export function selectAttachAdapter(cwd: string, adapterName?: string, port?: number): DapResolvedAdapter | null {
|
|
if (adapterName) {
|
|
return resolveAdapter(adapterName, cwd);
|
|
}
|
|
const available = getAvailableAdapters(cwd);
|
|
if (port !== undefined) {
|
|
const debugpy = available.find(adapter => adapter.name === "debugpy");
|
|
if (debugpy) return debugpy;
|
|
}
|
|
for (const preferred of EXTENSIONLESS_DEBUGGER_ORDER) {
|
|
const match = available.find(adapter => adapter.name === preferred);
|
|
if (match) return match;
|
|
}
|
|
return available[0] ?? null;
|
|
}
|
|
|
|
/** How the launch `program` resolves on disk. `"missing"` is reserved for
|
|
* programs the adapter creates on demand (rare); we treat them like files. */
|
|
export type LaunchProgramKind = "file" | "directory" | "missing";
|
|
|
|
/** Compute adapter-specific launch arguments that depend on the resolved
|
|
* program. Returned values are spread over `adapter.launchDefaults` so they
|
|
* take precedence over the static defaults but can still be overridden by
|
|
* the fields `DapSessionManager.launch` sets explicitly (program, cwd, args).
|
|
*
|
|
* Currently scoped to dlv, where `mode` selects how the program path is
|
|
* interpreted: directories and `.go` source files debug as a Go package
|
|
* (`mode=debug`), anything else is treated as a compiled binary (`mode=exec`).
|
|
*/
|
|
export function resolveLaunchOverrides(
|
|
adapter: DapResolvedAdapter,
|
|
program: string,
|
|
programKind: LaunchProgramKind,
|
|
): Record<string, unknown> {
|
|
if (adapter.name === "dlv") {
|
|
const extension = path.extname(program).toLowerCase();
|
|
if (programKind === "directory" || extension === ".go") {
|
|
return { mode: "debug" };
|
|
}
|
|
if (programKind === "file") {
|
|
return { mode: "exec" };
|
|
}
|
|
}
|
|
return {};
|
|
}
|