diff options
Diffstat (limited to 'packages/bun-debug-adapter-protocol/src/debugger/adapter.ts')
-rw-r--r-- | packages/bun-debug-adapter-protocol/src/debugger/adapter.ts | 1130 |
1 files changed, 901 insertions, 229 deletions
diff --git a/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts b/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts index 1996863e6..f3a60c793 100644 --- a/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts +++ b/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts @@ -4,13 +4,91 @@ import type { InspectorEventMap } from "../../../bun-inspector-protocol/src/insp // @ts-ignore import { WebSocketInspector, remoteObjectToString } from "../../../bun-inspector-protocol/index"; import type { ChildProcess } from "node:child_process"; -import { spawn, spawnSync } from "node:child_process"; -import capabilities from "./capabilities"; +import { spawn } from "node:child_process"; import { Location, SourceMap } from "./sourcemap"; -import { compare, parse } from "semver"; import { EventEmitter } from "node:events"; import { UnixSignal, randomUnixPath } from "./signal"; +const capabilities: DAP.Capabilities = { + supportsConfigurationDoneRequest: true, + supportsFunctionBreakpoints: true, + supportsConditionalBreakpoints: true, + supportsHitConditionalBreakpoints: true, + supportsEvaluateForHovers: true, + exceptionBreakpointFilters: [ + { + filter: "all", + label: "All Exceptions", + default: false, + supportsCondition: true, + description: "Breaks on all throw errors, even if they're caught later.", + conditionDescription: `error.name == "CustomError"`, + }, + { + filter: "uncaught", + label: "Uncaught Exceptions", + default: false, + supportsCondition: true, + description: "Breaks only on errors or promise rejections that are not handled.", + conditionDescription: `error.name == "CustomError"`, + }, + { + filter: "debugger", + label: "Debugger Statements", + default: true, + supportsCondition: false, + description: "Breaks on `debugger` statements.", + }, + { + filter: "assert", + label: "Assertion Failures", + default: false, + supportsCondition: false, + description: "Breaks on failed assertions.", + }, + { + filter: "microtask", + label: "Microtasks", + default: false, + supportsCondition: false, + description: "Breaks on microtasks.", + }, + ], + supportsStepBack: false, + supportsSetVariable: true, + supportsRestartFrame: false, + supportsGotoTargetsRequest: true, + supportsStepInTargetsRequest: false, + supportsCompletionsRequest: true, + completionTriggerCharacters: [".", "[", '"', "'"], + supportsModulesRequest: false, + additionalModuleColumns: [], + supportedChecksumAlgorithms: [], + supportsRestartRequest: false, // TODO + supportsExceptionOptions: false, // TODO + supportsValueFormattingOptions: false, + supportsExceptionInfoRequest: true, + supportTerminateDebuggee: true, + supportSuspendDebuggee: false, + supportsDelayedStackTraceLoading: true, + supportsLoadedSourcesRequest: true, + supportsLogPoints: true, + supportsTerminateThreadsRequest: false, + supportsSetExpression: true, + supportsTerminateRequest: true, + supportsDataBreakpoints: false, // TODO + supportsReadMemoryRequest: false, + supportsWriteMemoryRequest: false, + supportsDisassembleRequest: false, + supportsCancelRequest: false, + supportsBreakpointLocationsRequest: true, + supportsClipboardContext: false, + supportsSteppingGranularity: false, + supportsInstructionBreakpoints: false, + supportsExceptionFilterOptions: false, + supportsSingleThreadExecutionRequests: false, +}; + type InitializeRequest = DAP.InitializeRequest & { supportsConfigurationDoneRequest?: boolean; }; @@ -25,16 +103,17 @@ type LaunchRequest = DAP.LaunchRequest & { strictEnv?: boolean; stopOnEntry?: boolean; noDebug?: boolean; - watch?: boolean | "hot"; + watchMode?: boolean | "hot"; }; type AttachRequest = DAP.AttachRequest & { url?: string; - hostname?: string; - port?: number; - restart?: boolean; + noDebug?: boolean; + stopOnEntry?: boolean; }; +type DebuggerOptions = (LaunchRequest & { type: "launch" }) | (AttachRequest & { type: "attach" }); + type Source = DAP.Source & { scriptId: string; sourceMap: SourceMap; @@ -54,7 +133,11 @@ type Source = DAP.Source & { type Breakpoint = DAP.Breakpoint & { id: number; breakpointId: string; - generatedLocation: JSC.Debugger.Location; + generatedLocation?: JSC.Debugger.Location; + source?: Source; +}; + +type Target = (DAP.GotoTarget | DAP.StepInTarget) & { source: Source; }; @@ -103,8 +186,10 @@ export type DebugAdapterEventMap = InspectorEventMap & { "Process.stderr": [string]; }; +const isDebug = process.env.NODE_ENV === "development"; +const debugSilentEvents = new Set(["Adapter.event", "Inspector.event"]); + let threadId = 1; -let isDebug = process.env.NODE_ENV === "development"; export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements IDebugAdapter { #threadId: number; @@ -115,13 +200,15 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements #sources: Map<string | number, Source>; #stackFrames: StackFrame[]; #stopped?: DAP.StoppedEvent["reason"]; + #exception?: Variable; #breakpointId: number; - #breakpoints: Breakpoint[]; + #breakpoints: Map<string, Breakpoint>; #functionBreakpoints: Map<string, FunctionBreakpoint>; + #targets: Map<number, Target>; #variableId: number; #variables: Map<number, Variable>; #initialized?: InitializeRequest; - #launched?: LaunchRequest; + #options?: DebuggerOptions; constructor(url?: string | URL) { super(); @@ -138,10 +225,11 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements this.#pendingSources = new Map(); this.#sources = new Map(); this.#stackFrames = []; - this.#stopped = undefined; + this.#stopped = "start"; this.#breakpointId = 1; - this.#breakpoints = []; + this.#breakpoints = new Map(); this.#functionBreakpoints = new Map(); + this.#targets = new Map(); this.#variableId = 1; this.#variables = new Map(); } @@ -159,7 +247,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements * @returns if the inspector was able to connect */ start(url?: string): Promise<boolean> { - return this.#inspector.start(url); + return this.#attach({ url }); } /** @@ -187,7 +275,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements * @returns if the event was sent to a listener */ emit<E extends keyof DebugAdapterEventMap>(event: E, ...args: DebugAdapterEventMap[E] | []): boolean { - if (isDebug && event !== "Adapter.event" && event !== "Inspector.event") { + if (isDebug && !debugSilentEvents.has(event)) { console.log(this.#threadId, event, ...args); } @@ -224,12 +312,11 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements }); } - #reverseSend<R extends keyof DAP.RequestMap>(name: R, request: DAP.RequestMap[R]): void { - this.emit("Adapter.request", { - type: "request", - seq: 0, - command: name, - arguments: request, + #emitAfterResponse<E extends keyof DAP.EventMap>(event: E, body?: DAP.EventMap[E]): void { + this.once("Adapter.response", () => { + process.nextTick(() => { + this.#emit(event, body); + }); }); } @@ -240,11 +327,29 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements return; } + let timerId: number | undefined; let result: unknown; try { - // @ts-ignore - result = await this[command](args); + result = await Promise.race([ + // @ts-ignore + this[command](args), + new Promise((_, reject) => { + timerId = +setTimeout(() => reject(new Error(`Timed out: ${command}`)), 15_000); + }), + ]); } catch (cause) { + if (cause === Cancel) { + this.emit("Adapter.response", { + type: "response", + command, + success: false, + message: "cancelled", + request_seq: request.seq, + seq: 0, + }); + return; + } + const error = unknownToError(cause); this.emit("Adapter.error", error); @@ -258,6 +363,10 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements seq: 0, }); return; + } finally { + if (timerId) { + clearTimeout(timerId); + } } this.emit("Adapter.response", { @@ -276,21 +385,25 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements } initialize(request: InitializeRequest): DAP.InitializeResponse { - const { clientID, supportsConfigurationDoneRequest } = (this.#initialized = request); + this.#initialized = request; this.send("Inspector.enable"); this.send("Runtime.enable"); this.send("Console.enable"); - this.send("Debugger.enable"); + this.send("Debugger.enable").catch(error => { + const { message } = unknownToError(error); + if (message !== "Debugger domain already enabled") { + throw error; + } + }); this.send("Debugger.setAsyncStackTraceDepth", { depth: 200 }); - this.send("Debugger.setPauseOnDebuggerStatements", { enabled: true }); - this.send("Debugger.setBlackboxBreakpointEvaluations", { blackboxBreakpointEvaluations: true }); this.send("Debugger.setBreakpointsActive", { active: true }); + this.send("Debugger.pause"); + this.send("Inspector.initialized"); - // If the client will not send a `configurationDone` request, then we need to - // tell the debugger that everything is ready. + const { clientID, supportsConfigurationDoneRequest } = request; if (!supportsConfigurationDoneRequest && clientID !== "vscode") { - this.send("Inspector.initialized"); + this.configurationDone(); } // Tell the client what capabilities this adapter supports. @@ -300,21 +413,21 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements configurationDone(): void { // If the client requested that `noDebug` mode be enabled, // then we need to disable all breakpoints and pause on statements. - if (this.#launched?.noDebug) { + if (this.#options?.noDebug) { this.send("Debugger.setBreakpointsActive", { active: false }); - this.send("Debugger.setPauseOnExceptions", { state: "none" }); - this.send("Debugger.setPauseOnDebuggerStatements", { enabled: false }); - this.send("Debugger.setPauseOnMicrotasks", { enabled: false }); - this.send("Debugger.setPauseForInternalScripts", { shouldPause: false }); - this.send("Debugger.setPauseOnAssertions", { enabled: false }); + this.setExceptionBreakpoints({ filters: [] }); } - // Tell the debugger that everything is ready. - this.send("Inspector.initialized"); + if (this.#options?.stopOnEntry) { + this.send("Debugger.pause"); + } else { + // TODO: Check that the current location is not on a breakpoint before resuming. + this.send("Debugger.resume"); + } } async launch(request: DAP.LaunchRequest): Promise<void> { - this.#launched = request; + this.#options = { ...request, type: "launch" }; try { await this.#launch(request); @@ -339,9 +452,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements cwd, env = {}, strictEnv = false, - stopOnEntry = false, - noDebug = false, - watch = false, + watchMode = false, } = request; if (!program) { @@ -358,8 +469,8 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements processArgs.unshift("test"); } - if (watch && !runtimeArgs.includes("--watch") && !runtimeArgs.includes("--hot")) { - processArgs.unshift(watch === "hot" ? "--hot" : "--watch"); + if (watchMode && !runtimeArgs.includes("--watch") && !runtimeArgs.includes("--hot")) { + processArgs.unshift(watchMode === "hot" ? "--hot" : "--watch"); } const processEnv = strictEnv @@ -374,10 +485,24 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements const url = `ws+unix://${randomUnixPath()}`; const signal = new UnixSignal(); - const i = stopOnEntry ? "2" : "1"; - processEnv["BUN_INSPECT"] = `${i}${url}`; + signal.on("Signal.received", () => { + this.#attach({ url }); + }); + + this.once("Adapter.terminated", () => { + signal.close(); + }); + + // Break on entry is always set so the debugger has a chance + // to set breakpoints before the program starts. If `stopOnEntry` + // was not set, then the debugger will auto-continue after the first pause. + processEnv["BUN_INSPECT"] = `${url}?break=1`; processEnv["BUN_INSPECT_NOTIFY"] = signal.url; + + // This is probably not correct, but it's the best we can do for now. processEnv["FORCE_COLOR"] = "1"; + processEnv["BUN_QUIET_DEBUG_LOGS"] = "1"; + processEnv["BUN_DEBUG_QUIET_LOGS"] = "1"; const started = await this.#spawn({ command: runtime, @@ -390,28 +515,6 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements if (!started) { throw new Error("Program could not be started."); } - - await new Promise<void>(resolve => { - signal.on("Signal.received", () => { - resolve(); - }); - setTimeout(resolve, 5000); - }); - - const attached = await this.#attach(url); - - if (attached) { - return; - } - - const { stdout: version } = spawnSync(runtime, ["--version"], { stdio: "pipe", encoding: "utf-8" }); - - const minVersion = "0.8.2"; - if (parse(version, true) && compare(minVersion, version, true)) { - throw new Error(`This extension requires Bun v${minVersion} or later. Please upgrade by running: bun upgrade`); - } - - throw new Error("Program started, but the debugger could not be attached."); } async #spawn(options: { @@ -458,6 +561,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements this.#emit("exited", { exitCode: code ?? -1, }); + this.#emit("terminated"); } }); @@ -477,10 +581,10 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements } async attach(request: AttachRequest): Promise<void> { - const { url } = request; + this.#options = { ...request, type: "attach" }; try { - await this.#attach(url); + await this.#attach(request); } catch (error) { // Some clients, like VSCode, will show a system-level popup when a `launch` request fails. // Instead, we want to show the error as a sidebar notification. @@ -493,19 +597,27 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements } } - async #attach(url?: string | URL): Promise<boolean> { - for (let i = 0; i < 5; i++) { + async #attach(request: AttachRequest): Promise<boolean> { + const { url } = request; + + for (let i = 0; i < 3; i++) { const ok = await this.#inspector.start(url); if (ok) { return true; } await new Promise(resolve => setTimeout(resolve, 100 * i)); } + return false; } terminate(): void { - this.#process?.kill(); + if (!this.#process?.kill()) { + this.#evaluate({ + expression: "process.exit(0)", + }); + } + this.#emit("terminated"); } @@ -521,7 +633,6 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements async source(request: DAP.SourceRequest): Promise<DAP.SourceResponse> { const { source } = request; - const { scriptId } = await this.#getSource(sourceToId(source)); const { scriptSource } = await this.send("Debugger.getScriptSource", { scriptId }); @@ -570,14 +681,9 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements const { line, endLine, column, endColumn, source: source0 } = request; const source = await this.#getSource(sourceToId(source0)); - const [start, end] = await Promise.all([ - this.#generatedLocation(source, line, column), - this.#generatedLocation(source, endLine ?? line + 1, endColumn), - ]); - const { locations } = await this.send("Debugger.getBreakpointLocations", { - start, - end, + start: this.#generatedLocation(source, line, column), + end: this.#generatedLocation(source, endLine ?? line + 1, endColumn), }); return { @@ -636,7 +742,9 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements return { line: this.#lineFrom0BasedLine(oline), - column: this.#columnFrom0BasedColumn(ocolumn), + // For now, remove the column from locations because + // it can be inaccurate and causes weird rendering issues in VSCode. + column: this.#columnFrom0BasedColumn(0), // ocolumn }; } @@ -655,13 +763,52 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements } async setBreakpoints(request: DAP.SetBreakpointsRequest): Promise<DAP.SetBreakpointsResponse> { - const { source: source0, breakpoints: requests } = request; - const sourceId = sourceToId(source0); - const source = await this.#getSource(sourceId); + const { source, breakpoints } = request; + if (!breakpoints?.length) { + return { + breakpoints: [], + }; + } + + const { path } = source; + const verifiedSource = this.#getSourceIfPresent(sourceToId(source)); + + let results: Breakpoint[]; + if (verifiedSource) { + results = await this.#setBreakpointsById(verifiedSource, breakpoints); + } else if (path) { + results = await this.#setBreakpointsByUrl(path, breakpoints); + } else { + results = []; + } + + return { + breakpoints: results, + }; + } + + async #setBreakpointsByUrl(url: string, requests: DAP.SourceBreakpoint[]): Promise<Breakpoint[]> { + const source = await this.#getSourceByUrl(url); + + // If there is no source, this is likely a breakpoint from an unrelated + // file that can be ignored. Return an unverified breakpoint for each request. + if (!source) { + return requests.map(() => { + const breakpointId = this.#breakpointId++; + return this.#addBreakpoint({ + id: breakpointId, + breakpointId: `${breakpointId}`, + verified: false, + }); + }); + } + + const sourceId = sourceToId(source); const oldBreakpoints = this.#getBreakpoints(sourceId); + const breakpoints = await Promise.all( - requests!.map(async ({ line, column, ...options }) => { + requests.map(async ({ line, column, ...options }) => { const location = this.#generatedLocation(source, line, column); for (const breakpoint of oldBreakpoints) { @@ -671,25 +818,102 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements } } + let result; try { - const { breakpointId, actualLocation } = await this.send("Debugger.setBreakpoint", { - location, + result = await this.send("Debugger.setBreakpointByUrl", { + url, options: breakpointOptions(options), + ...location, }); + } catch (error) { + // If there was an error setting the breakpoint, + // mark it as unverified and add a message. + const { message } = unknownToError(error); + const breakpointId = this.#breakpointId++; + return this.#addBreakpoint({ + id: breakpointId, + breakpointId: `${breakpointId}`, + line, + column, + source, + verified: false, + message, + generatedLocation: location, + }); + } - const originalLocation = this.#originalLocation(source, actualLocation); + const { breakpointId, locations } = result; + if (!locations.length) { return this.#addBreakpoint({ id: this.#breakpointId++, breakpointId, + line, + column, source, - verified: true, + verified: false, generatedLocation: location, - ...originalLocation, + }); + } + + const originalLocation = this.#originalLocation(source, locations[0]); + return this.#addBreakpoint({ + id: this.#breakpointId++, + breakpointId, + source, + verified: true, + generatedLocation: location, + ...originalLocation, + }); + }), + ); + + await Promise.all( + oldBreakpoints.map(async ({ breakpointId }) => { + const isRemoved = !breakpoints.filter(({ breakpointId: id }) => breakpointId === id).length; + if (isRemoved) { + await this.send("Debugger.removeBreakpoint", { + breakpointId, + }); + this.#removeBreakpoint(breakpointId); + } + }), + ); + + const duplicateBreakpoints = breakpoints.filter( + ({ message }) => message === "Breakpoint for given location already exists", + ); + for (const { breakpointId } of duplicateBreakpoints) { + this.#removeBreakpoint(breakpointId); + } + + return breakpoints; + } + + async #setBreakpointsById(source: Source, requests: DAP.SourceBreakpoint[]): Promise<Breakpoint[]> { + const { sourceId } = source; + const oldBreakpoints = this.#getBreakpoints(sourceId); + + const breakpoints = await Promise.all( + requests!.map(async ({ line, column, ...options }) => { + const location = this.#generatedLocation(source, line, column); + + for (const breakpoint of oldBreakpoints) { + const { generatedLocation } = breakpoint; + if (locationIsSame(generatedLocation, location)) { + return breakpoint; + } + } + + let result; + try { + result = await this.send("Debugger.setBreakpoint", { + location, + options: breakpointOptions(options), }); } catch (error) { - const { message } = unknownToError(error); // If there was an error setting the breakpoint, // mark it as unverified and add a message. + const { message } = unknownToError(error); const breakpointId = this.#breakpointId++; return this.#addBreakpoint({ id: breakpointId, @@ -702,6 +926,18 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements generatedLocation: location, }); } + + const { breakpointId, actualLocation } = result; + const originalLocation = this.#originalLocation(source, actualLocation); + + return this.#addBreakpoint({ + id: this.#breakpointId++, + breakpointId, + source, + verified: true, + generatedLocation: location, + ...originalLocation, + }); }), ); @@ -717,17 +953,26 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements }), ); - return { - breakpoints, - }; + const duplicateBreakpoints = breakpoints.filter( + ({ message }) => message === "Breakpoint for given location already exists", + ); + for (const { breakpointId } of duplicateBreakpoints) { + this.#removeBreakpoint(breakpointId); + } + + return breakpoints; } - #getBreakpoints(sourceId: string | number): Breakpoint[] { + #getBreakpoints(sourceId?: string | number): Breakpoint[] { const breakpoints: Breakpoint[] = []; + if (!sourceId) { + return breakpoints; + } + for (const breakpoint of this.#breakpoints.values()) { const { source } = breakpoint; - if (sourceId === sourceToId(source)) { + if (source && sourceId === sourceToId(source)) { breakpoints.push(breakpoint); } } @@ -736,28 +981,19 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements } #addBreakpoint(breakpoint: Breakpoint): Breakpoint { - this.#breakpoints.push(breakpoint); - - // For now, remove the column from breakpoints because - // it can be inaccurate and causes weird rendering issues in VSCode. - breakpoint.column = this.#lineFrom0BasedLine(0); - - this.#emit("breakpoint", { - reason: "changed", - breakpoint, - }); - + const { breakpointId } = breakpoint; + this.#breakpoints.set(breakpointId, breakpoint); return breakpoint; } #removeBreakpoint(breakpointId: string): void { - const breakpoint = this.#breakpoints.find(({ breakpointId: id }) => id === breakpointId); - if (!breakpoint) { + const breakpoint = this.#breakpoints.get(breakpointId); + + if (!breakpoint || !this.#breakpoints.delete(breakpointId)) { return; } - this.#breakpoints = this.#breakpoints.filter(({ breakpointId: id }) => id !== breakpointId); - this.#emit("breakpoint", { + this.#emitAfterResponse("breakpoint", { reason: "removed", breakpoint, }); @@ -831,12 +1067,6 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements #addFunctionBreakpoint(breakpoint: FunctionBreakpoint): FunctionBreakpoint { const { name } = breakpoint; this.#functionBreakpoints.set(name, breakpoint); - - this.#emit("breakpoint", { - reason: "changed", - breakpoint, - }); - return breakpoint; } @@ -847,22 +1077,86 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements return; } - this.#emit("breakpoint", { + this.#emitAfterResponse("breakpoint", { reason: "removed", breakpoint, }); } - async setExceptionBreakpoints(request: DAP.SetExceptionBreakpointsRequest): Promise<void> { - const { filters, filterOptions } = request; + async setExceptionBreakpoints( + request: DAP.SetExceptionBreakpointsRequest, + ): Promise<DAP.SetExceptionBreakpointsResponse> { + const { filters } = request; - const filterIds = [...filters]; - if (filterOptions) { - filterIds.push(...filterOptions.map(({ filterId }) => filterId)); + let state: "all" | "uncaught" | "none"; + if (filters.includes("all")) { + state = "all"; + } else if (filters.includes("uncaught")) { + state = "uncaught"; + } else { + state = "none"; } - await this.send("Debugger.setPauseOnExceptions", { - state: exceptionFiltersToPauseOnExceptionsState(filterIds), + await Promise.all([ + this.send("Debugger.setPauseOnExceptions", { state }), + this.send("Debugger.setPauseOnAssertions", { + enabled: filters.includes("assert"), + }), + this.send("Debugger.setPauseOnDebuggerStatements", { + enabled: filters.includes("debugger"), + }), + this.send("Debugger.setPauseOnMicrotasks", { + enabled: filters.includes("microtask"), + }), + ]); + + return { + breakpoints: [], + }; + } + + async gotoTargets(request: DAP.GotoTargetsRequest): Promise<DAP.GotoTargetsResponse> { + const { source: source0 } = request; + const source = await this.#getSource(sourceToId(source0)); + + const { breakpoints } = await this.breakpointLocations(request); + const targets = breakpoints.map(({ line, column }) => + this.#addTarget({ + id: this.#targets.size, + label: `${line}:${column}`, + source, + line, + column, + }), + ); + + return { + targets, + }; + } + + #addTarget<T extends DAP.GotoTarget | DAP.StepInTarget>(target: T & { source: Source }): T { + const { id } = target; + this.#targets.set(id, target); + return target; + } + + #getTarget(targetId: number): Target | undefined { + return this.#targets.get(targetId); + } + + async goto(request: DAP.GotoRequest): Promise<void> { + const { targetId } = request; + const target = this.#getTarget(targetId); + if (!target) { + throw new Error("No target found."); + } + + const { source, line, column } = target; + const location = this.#generatedLocation(source, line, column); + + await this.send("Debugger.continueToLocation", { + location, }); } @@ -871,14 +1165,18 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements const callFrameId = this.#getCallFrameId(frameId); const objectGroup = callFrameId ? "debugger" : context; - const { result, wasThrown } = await this.#evaluate(expression, objectGroup, callFrameId); - const { className } = result; + const { result, wasThrown } = await this.#evaluate({ + expression, + objectGroup, + callFrameId, + }); - if (context === "hover" && wasThrown && (className === "SyntaxError" || className === "ReferenceError")) { - return { - result: "", - variablesReference: 0, - }; + if (wasThrown) { + if (context === "hover" && isSyntaxError(result)) { + throw Cancel; + } + + throw new Error(remoteObjectToString(result)); } const { name, value, ...variable } = this.#addObject(result, { objectGroup }); @@ -888,11 +1186,12 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements }; } - async #evaluate( - expression: string, - objectGroup?: string, - callFrameId?: string, - ): Promise<JSC.Runtime.EvaluateResponse> { + async #evaluate(options: { + expression: string; + objectGroup?: string; + callFrameId?: string; + }): Promise<JSC.Runtime.EvaluateResponse> { + const { expression, objectGroup, callFrameId } = options; const method = callFrameId ? "Debugger.evaluateOnCallFrame" : "Runtime.evaluate"; return this.send(method, { @@ -906,14 +1205,40 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements }); } - restart(): void { - this.initialize(this.#initialized!); - this.configurationDone(); + async completions(request: DAP.CompletionsRequest): Promise<DAP.CompletionsResponse> { + const { text, column, frameId } = request; + const callFrameId = this.#getCallFrameId(frameId); - this.#emit("output", { - category: "debug console", - output: "Debugger reloaded.\n", + const { expression, hint } = completionToExpression(text); + const { result, wasThrown } = await this.#evaluate({ + expression: expression || "this", + callFrameId, + objectGroup: "repl", + }); + + if (wasThrown) { + if (isSyntaxError(result)) { + return { + targets: [], + }; + } + throw new Error(remoteObjectToString(result)); + } + + const variable = this.#addObject(result, { + objectGroup: "repl", + evaluateName: expression, }); + + const properties = await this.#getProperties(variable); + const targets = properties + .filter(({ name }) => isIdentifier(name) && (!hint || name.includes(hint))) + .sort(variablesSortBy) + .map(variableToCompletionItem); + + return { + targets, + }; } ["Inspector.connected"](): void { @@ -939,8 +1264,11 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements }); } - this.#emit("terminated"); this.#reset(); + + if (this.#process?.exitCode !== null) { + this.#emit("terminated"); + } } async ["Debugger.scriptParsed"](event: JSC.Debugger.ScriptParsedEvent): Promise<void> { @@ -986,7 +1314,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements ["Debugger.scriptFailedToParse"](event: JSC.Debugger.ScriptFailedToParseEvent): void { const { url, errorMessage, errorLine } = event; - // If no url is present, the script is from a `evaluate` request. + // If no url is present, the script is from an `evaluate` request. if (!url) { return; } @@ -1004,14 +1332,24 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements ["Debugger.paused"](event: JSC.Debugger.PausedEvent): void { const { reason, callFrames, asyncStackTrace, data } = event; - // if (reason === "PauseOnNextStatement") { - // for (const { functionName } of callFrames) { - // if (functionName === "module code") { - // this.send("Debugger.resume"); - // return; - // } - // } - // } + if (reason === "PauseOnNextStatement") { + if (this.#stopped === "start" && !this.#options?.stopOnEntry) { + this.#stopped = undefined; + return; + } + } + + if (reason === "DebuggerStatement") { + // FIXME: This is a hacky fix for the `Debugger.paused` event being fired + // when the debugger is started in hot mode. + for (const { functionName } of callFrames) { + // @ts-ignore + if (functionName === "module code" && this.#options?.watchMode === "hot") { + this.send("Debugger.resume"); + return; + } + } + } this.#stackFrames.length = 0; this.#stopped ||= stoppedReason(reason); @@ -1030,6 +1368,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements if (data) { if (reason === "exception") { const remoteObject = data as JSC.Runtime.RemoteObject; + this.#exception = this.#addObject(remoteObject, { objectGroup: "debugger" }); } if (reason === "FunctionCall") { @@ -1059,16 +1398,16 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements }); } - ["Debugger.resumed"](event: JSC.Debugger.ResumedEvent): void { + ["Debugger.resumed"](): void { this.#stackFrames.length = 0; this.#stopped = undefined; - console.log("VARIABLES BEFORE", this.#variables.size); + this.#exception = undefined; for (const { variablesReference, objectGroup } of this.#variables.values()) { if (objectGroup === "debugger") { this.#variables.delete(variablesReference); } } - console.log("VARIABLES AFTER", this.#variables.size); + this.#emit("continued", { threadId: this.#threadId, }); @@ -1240,6 +1579,62 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements }); } + async #getSourceByUrl(url: string): Promise<Source | undefined> { + const source = this.#getSourceIfPresent(url); + if (source) { + return source; + } + + // If the source is not present, the debugger did not send the `Debugger.scriptParsed` event. + // Since there is no request to retrieve a script by its url, the hacky solution is to set + // a no-op breakpoint by url, then immediately remove it. + let result; + try { + result = await this.send("Debugger.setBreakpointByUrl", { + url, + lineNumber: 0, + options: { + autoContinue: true, + }, + }); + } catch { + // If there was an error setting the breakpoint, + // the source probably does not exist. + return undefined; + } + + const { breakpointId, locations } = result; + await this.send("Debugger.removeBreakpoint", { + breakpointId, + }); + + if (!locations.length) { + return undefined; + } + + // It is possible that the source was loaded between the time it took + // to set and remove the breakpoint, so check again. + const [{ scriptId }] = locations; + const recentSource = this.#getSourceIfPresent(scriptId) || this.#getSourceIfPresent(url); + if (recentSource) { + return recentSource; + } + + // Otherwise, retrieve the source and source map url and add the source. + const { scriptSource } = await this.send("Debugger.getScriptSource", { + scriptId, + }); + + return this.#addSource({ + scriptId, + sourceId: url, + name: sourceName(url), + path: url, + sourceMap: SourceMap(scriptSource), + presentationHint: sourcePresentationHint(url), + }); + } + async stackTrace(request: DAP.StackTraceRequest): Promise<DAP.StackTraceResponse> { const { length } = this.#stackFrames; const { startFrame = 0, levels } = request; @@ -1279,7 +1674,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements } #addStackFrame(callFrame: JSC.Debugger.CallFrame): StackFrame { - const { callFrameId, functionName, location, scopeChain } = callFrame; + const { callFrameId, functionName, location, scopeChain, this: thisObject } = callFrame; const { scriptId } = location; const source = this.#getSourceIfPresent(scriptId); @@ -1398,7 +1793,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements async variables(request: DAP.VariablesRequest): Promise<DAP.VariablesResponse> { const { variablesReference, start, count } = request; - const variable = this.#variables.get(variablesReference); + const variable = this.#getVariable(variablesReference); let variables: Variable[]; if (!variable) { @@ -1414,68 +1809,88 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements }; } + async setVariable(request: DAP.SetVariableRequest): Promise<DAP.SetVariableResponse> { + const { variablesReference, name, value } = request; + + const variable = this.#getVariable(variablesReference); + if (!variable) { + throw new Error("Variable not found."); + } + + const { objectId, objectGroup } = variable; + if (!objectId) { + throw new Error("Variable cannot be modified."); + } + + const { result, wasThrown } = await this.send("Runtime.callFunctionOn", { + objectId, + functionDeclaration: `function (name) { this[name] = ${value}; return this[name]; }`, + arguments: [{ value: name }], + doNotPauseOnExceptionsAndMuteConsole: true, + }); + + if (wasThrown) { + throw new Error(remoteObjectToString(result)); + } + + return this.#addObject(result, { name, objectGroup }); + } + + async setExpression(request: DAP.SetExpressionRequest): Promise<DAP.SetExpressionResponse> { + const { expression, value, frameId } = request; + const callFrameId = this.#getCallFrameId(frameId); + const objectGroup = callFrameId ? "debugger" : "repl"; + + const { result, wasThrown } = await this.#evaluate({ + expression: `${expression} = (${value});`, + objectGroup: "repl", + callFrameId, + }); + + if (wasThrown) { + throw new Error(remoteObjectToString(result)); + } + + return this.#addObject(result, { objectGroup }); + } + + #getVariable(variablesReference?: number): Variable | undefined { + if (!variablesReference) { + return undefined; + } + return this.#variables.get(variablesReference); + } + #addObject( remoteObject: JSC.Runtime.RemoteObject, - propertyDescriptor?: Partial<JSC.Runtime.PropertyDescriptor> & { objectGroup?: string }, + propertyDescriptor?: Partial<JSC.Runtime.PropertyDescriptor> & { objectGroup?: string; evaluateName?: string }, ): Variable { const { objectId, type, subtype, size } = remoteObject; + const { objectGroup, evaluateName } = propertyDescriptor ?? {}; const variablesReference = objectId ? this.#variableId++ : 0; const variable: Variable = { objectId, - objectGroup: propertyDescriptor?.objectGroup, - name: propertyDescriptorToName(propertyDescriptor), + objectGroup, + variablesReference, type: subtype || type, value: remoteObjectToString(remoteObject), - variablesReference, - indexedVariables: isIndexed(subtype) ? size : undefined, - namedVariables: isNamedIndexed(subtype) ? size : undefined, + name: propertyDescriptorToName(propertyDescriptor), + evaluateName: propertyDescriptorToEvaluateName(propertyDescriptor, evaluateName), + indexedVariables: isArrayLike(subtype) ? size : undefined, + namedVariables: isMap(subtype) ? size : undefined, presentationHint: remoteObjectToVariablePresentationHint(remoteObject, propertyDescriptor), }; if (variablesReference) { this.#variables.set(variablesReference, variable); - console.log("addObject", variablesReference, variable); } return variable; } - #addProperty( - propertyDescriptor: - | JSC.Runtime.PropertyDescriptor - | (JSC.Runtime.InternalPropertyDescriptor & { objectGroup?: string }), - ): Variable[] { - const { value, get, set, symbol } = propertyDescriptor as JSC.Runtime.PropertyDescriptor; - const variables: Variable[] = []; - - if (value) { - variables.push(this.#addObject(value, propertyDescriptor)); - } - - if (get) { - const { type } = get; - if (type !== "undefined") { - variables.push(this.#addObject(get, propertyDescriptor)); - } - } - - if (set) { - const { type } = set; - if (type !== "undefined") { - variables.push(this.#addObject(set, propertyDescriptor)); - } - } - - if (symbol) { - variables.push(this.#addObject(symbol, propertyDescriptor)); - } - - return variables; - } - async #getProperties(variable: Variable, offset?: number, count?: number): Promise<Variable[]> { - const { objectId, objectGroup, type, indexedVariables, namedVariables } = variable; + const { objectId, objectGroup, type, evaluateName, indexedVariables, namedVariables } = variable; const variables: Variable[] = []; if (!objectId || type === "symbol") { @@ -1488,12 +1903,14 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements }); for (const property of properties) { - variables.push(...this.#addProperty({ ...property, objectGroup })); + variables.push(...this.#addProperty(property, { objectGroup, evaluateName, parentType: type })); } if (internalProperties) { for (const property of internalProperties) { - variables.push(...this.#addProperty({ ...property, objectGroup, configurable: false })); + variables.push( + ...this.#addProperty(property, { objectGroup, evaluateName, parentType: type, isSynthetic: true }), + ); } } @@ -1512,29 +1929,135 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements const { value, description } = key; name = String(value ?? description); } - variables.push(this.#addObject(value, { name, objectGroup })); + variables.push( + ...this.#addProperty( + { name, value }, + { + objectGroup, + evaluateName, + parentType: type, + isSynthetic: true, + }, + ), + ); } } return variables; } + #addProperty( + propertyDescriptor: JSC.Runtime.PropertyDescriptor | JSC.Runtime.InternalPropertyDescriptor, + options?: { + objectGroup?: string; + evaluateName?: string; + isSynthetic?: boolean; + parentType?: JSC.Runtime.RemoteObject["type"] | JSC.Runtime.RemoteObject["subtype"]; + }, + ): Variable[] { + const { value, get, set, symbol } = propertyDescriptor as JSC.Runtime.PropertyDescriptor; + const descriptor = { ...propertyDescriptor, ...options }; + const variables: Variable[] = []; + + if (value) { + variables.push(this.#addObject(value, descriptor)); + } + + if (get) { + const { type } = get; + if (type !== "undefined") { + variables.push(this.#addObject(get, descriptor)); + } + } + + if (set) { + const { type } = set; + if (type !== "undefined") { + variables.push(this.#addObject(set, descriptor)); + } + } + + if (symbol) { + variables.push(this.#addObject(symbol, descriptor)); + } + + return variables; + } + + async exceptionInfo(): Promise<DAP.ExceptionInfoResponse> { + const exception = this.#exception; + if (!exception) { + throw new Error("No exception found."); + } + + const { code, ...details } = await this.#getExceptionDetails(exception); + return { + exceptionId: code || "", + breakMode: "always", + details, + }; + } + + async #getExceptionDetails(variable: Variable): Promise<DAP.ExceptionDetails & { code?: string }> { + const properties = await this.#getProperties(variable); + + let fullTypeName: string | undefined; + let message: string | undefined; + let code: string | undefined; + let stackTrace: string | undefined; + let innerException: DAP.ExceptionDetails[] | undefined; + + for (const property of properties) { + const { name, value, type } = property; + if (name === "name") { + fullTypeName = value; + } else if (name === "message") { + message = type === "string" ? JSON.parse(value) : value; + } else if (name === "stack") { + stackTrace = type === "string" ? JSON.parse(value) : value; + } else if (name === "code") { + code = type === "string" ? JSON.parse(value) : value; + } else if (name === "cause") { + const cause = await this.#getExceptionDetails(property); + innerException = [cause]; + } else if (name === "errors") { + const errors = await this.#getProperties(property); + innerException = await Promise.all(errors.map(error => this.#getExceptionDetails(error))); + } + } + + if (!stackTrace) { + const { value } = variable; + stackTrace ||= value; + } + + return { + fullTypeName, + message, + code, + stackTrace: stripAnsi(stackTrace), + innerException, + }; + } + close(): void { this.#process?.kill(); + // this.#signal?.close(); this.#inspector.close(); + this.#reset(); } #reset(): void { this.#pendingSources.clear(); this.#sources.clear(); this.#stackFrames.length = 0; - this.#stopped = undefined; - this.#breakpointId = 1; - this.#breakpoints.length = 0; + this.#stopped = "start"; + this.#exception = undefined; + this.#breakpoints.clear(); this.#functionBreakpoints.clear(); + this.#targets.clear(); this.#variables.clear(); - this.#launched = undefined; - this.#initialized = undefined; + this.#options = undefined; } } @@ -1602,31 +2125,86 @@ function scopePresentationHint(type: JSC.Debugger.Scope["type"]): DAP.Scope["pre } } -function isIndexed(subtype: JSC.Runtime.RemoteObject["subtype"]): boolean { - return subtype === "array" || subtype === "set" || subtype === "weakset"; +function isSet(subtype: JSC.Runtime.RemoteObject["type"] | JSC.Runtime.RemoteObject["subtype"]): boolean { + return subtype === "set" || subtype === "weakset"; } -function isNamedIndexed(subtype: JSC.Runtime.RemoteObject["subtype"]): boolean { +function isArrayLike(subtype: JSC.Runtime.RemoteObject["type"] | JSC.Runtime.RemoteObject["subtype"]): boolean { + return subtype === "array" || isSet(subtype); +} + +function isMap(subtype: JSC.Runtime.RemoteObject["type"] | JSC.Runtime.RemoteObject["subtype"]): boolean { return subtype === "map" || subtype === "weakmap"; } -function exceptionFiltersToPauseOnExceptionsState( - filters?: string[], -): JSC.Debugger.SetPauseOnExceptionsRequest["state"] { - if (filters?.includes("all")) { - return "all"; +function breakpointOptions(breakpoint: Partial<DAP.SourceBreakpoint>): JSC.Debugger.BreakpointOptions { + const { condition, hitCondition, logMessage } = breakpoint; + return { + condition, + ignoreCount: hitConditionToIgnoreCount(hitCondition), + autoContinue: !!logMessage, + actions: [ + { + type: "evaluate", + data: logMessageToExpression(logMessage), + emulateUserGesture: true, + }, + ], + }; +} + +function hitConditionToIgnoreCount(hitCondition?: string): number | undefined { + if (!hitCondition) { + return undefined; + } + + if (hitCondition.includes("<")) { + throw new Error("Hit condition with '<' is not supported, use '>' or '>=' instead."); } - if (filters?.includes("uncaught")) { - return "uncaught"; + + const count = parseInt(hitCondition.replace(/[^\d+]/g, "")); + if (isNaN(count)) { + throw new Error("Hit condition is not a number."); } - return "none"; + + if (hitCondition.includes(">") && !hitCondition.includes("=")) { + return Math.max(0, count); + } + return Math.max(0, count - 1); } -function breakpointOptions(breakpoint?: Partial<DAP.SourceBreakpoint>): JSC.Debugger.BreakpointOptions { - const { condition } = breakpoint ?? {}; - // TODO: hitCondition, logMessage +function logMessageToExpression(logMessage?: string): string | undefined { + if (!logMessage) { + return undefined; + } + // Convert expressions from "hello {name}!" to "`hello ${name}!`" + return `console.log(\`${logMessage.replace(/\$?{/g, "${")}\`);`; +} + +function completionToExpression(completion: string): { expression: string; hint?: string } { + const lastDot = completion.lastIndexOf("."); + const last = (s0: string, s1: string) => { + const i0 = completion.lastIndexOf(s0); + const i1 = completion.lastIndexOf(s1); + return i1 > i0 ? i1 + 1 : i0; + }; + + const lastIdentifier = Math.max(lastDot, last("[", "]"), last("(", ")"), last("{", "}")); + + let expression: string; + let remainder: string; + if (lastIdentifier > 0) { + expression = completion.slice(0, lastIdentifier); + remainder = completion.slice(lastIdentifier); + } else { + expression = ""; + remainder = completion; + } + + const [hint] = completion.slice(lastIdentifier).match(/[#$a-z_][0-9a-z_$]*/gi) ?? []; return { - condition, + expression, + hint, }; } @@ -1674,10 +2252,13 @@ function sanitizeExpression(expression: string): string { function remoteObjectToVariablePresentationHint( remoteObject: JSC.Runtime.RemoteObject, - propertyDescriptor?: Partial<JSC.Runtime.PropertyDescriptor>, + propertyDescriptor?: Partial<JSC.Runtime.PropertyDescriptor> & { + isSynthetic?: boolean; + parentType?: JSC.Runtime.RemoteObject["type"] | JSC.Runtime.RemoteObject["subtype"]; + }, ): DAP.VariablePresentationHint { const { type, subtype } = remoteObject; - const { name, configurable, writable, isPrivate, symbol, get, set, wasThrown } = propertyDescriptor ?? {}; + const { name, enumerable, writable, isPrivate, isSynthetic, symbol, get, set, wasThrown } = propertyDescriptor ?? {}; const hasGetter = get?.type === "function"; const hasSetter = set?.type === "function"; const hasSymbol = symbol?.type === "symbol"; @@ -1693,13 +2274,16 @@ function remoteObjectToVariablePresentationHint( if (subtype === "class") { kind = "class"; } - if (isPrivate || configurable === false || hasSymbol || name === "__proto__") { + if (isSynthetic || isPrivate || hasSymbol) { + visibility = "protected"; + } + if (enumerable === false || name === "__proto__") { visibility = "internal"; } if (type === "string") { attributes.push("rawString"); } - if (writable === false || (hasGetter && !hasSetter)) { + if (isSynthetic || writable === false || (hasGetter && !hasSetter)) { attributes.push("readOnly"); } if (wasThrown || hasGetter) { @@ -1726,6 +2310,51 @@ function propertyDescriptorToName(propertyDescriptor?: Partial<JSC.Runtime.Prope return name ?? ""; } +function propertyDescriptorToEvaluateName( + propertyDescriptor?: Partial<JSC.Runtime.PropertyDescriptor> & { + isSynthetic?: boolean; + parentType?: JSC.Runtime.RemoteObject["type"] | JSC.Runtime.RemoteObject["subtype"]; + }, + evaluateName?: string, +): string | undefined { + if (!propertyDescriptor) { + return evaluateName; + } + const { name: property, isSynthetic, parentType: type } = propertyDescriptor; + if (!property) { + return evaluateName; + } + if (!evaluateName) { + return property; + } + if (isSynthetic) { + if (isMap(type)) { + if (isNumeric(property)) { + return `${evaluateName}.get(${property})`; + } + return `${evaluateName}.get(${JSON.stringify(property)})`; + } + if (isSet(type)) { + return `[...${evaluateName}.values()][${property}]`; + } + } + if (isNumeric(property)) { + return `${evaluateName}[${property}]`; + } + if (isIdentifier(property)) { + return `${evaluateName}.${property}`; + } + return `${evaluateName}[${JSON.stringify(property)}]`; +} + +function isNumeric(string: string): boolean { + return /^\d+$/.test(string); +} + +function isIdentifier(string: string): boolean { + return /^[#$a-z_][0-9a-z_$]*$/i.test(string); +} + function unknownToError(input: unknown): Error { if (input instanceof Error) { return input; @@ -1741,6 +2370,36 @@ function isTestJavaScript(path: string): boolean { return /\.(test|spec)\.(c|m)?(j|t)sx?$/.test(path); } +function isSyntaxError(remoteObject: JSC.Runtime.RemoteObject): boolean { + const { className } = remoteObject; + + switch (className) { + case "SyntaxError": + case "ReferenceError": + return true; + } + + return false; +} + +function variableToCompletionItem(variable: Variable): DAP.CompletionItem { + const { name, type } = variable; + return { + label: name, + type: variableTypeToCompletionItemType(type), + }; +} + +function variableTypeToCompletionItemType(type: Variable["type"]): DAP.CompletionItem["type"] { + switch (type) { + case "class": + return "class"; + case "function": + return "function"; + } + return "property"; +} + function variablesSortBy(a: DAP.Variable, b: DAP.Variable): number { const visibility = (variable: DAP.Variable): number => { const { presentationHint } = variable; @@ -1766,6 +2425,13 @@ function variablesSortBy(a: DAP.Variable, b: DAP.Variable): number { if (isFinite(index)) { return index; } + switch (name[0]) { + case "_": + case "$": + return 1; + case "#": + return 2; + } return 0; }; const av = visibility(a); @@ -1797,6 +2463,12 @@ function numberIsValid(number?: number): number is number { return typeof number === "number" && isFinite(number) && number >= 0; } -function locationIsSame(a: JSC.Debugger.Location, b: JSC.Debugger.Location): boolean { - return a.scriptId === b.scriptId && a.lineNumber === b.lineNumber && a.columnNumber === b.columnNumber; +function locationIsSame(a?: JSC.Debugger.Location, b?: JSC.Debugger.Location): boolean { + return a?.scriptId === b?.scriptId && a?.lineNumber === b?.lineNumber && a?.columnNumber === b?.columnNumber; } + +function stripAnsi(string: string): string { + return string.replace(/\u001b\[\d+m/g, ""); +} + +const Cancel = Symbol("Cancel"); |