diff --git a/src/adapter/breakpoints/breakpointBase.ts b/src/adapter/breakpoints/breakpointBase.ts index 20f0d2816..196bb9b68 100644 --- a/src/adapter/breakpoints/breakpointBase.ts +++ b/src/adapter/breakpoints/breakpointBase.ts @@ -5,7 +5,7 @@ import Cdp from '../../cdp/api'; import { LogTag } from '../../common/logging'; import { IPosition } from '../../common/positions'; -import { absolutePathToFileUrl, urlToRegex } from '../../common/urlUtils'; +import { absolutePathToFileUrl } from '../../common/urlUtils'; import Dap from '../../dap/api'; import { BreakpointManager } from '../breakpoints'; import { ISourceScript, IUiLocation, Source, SourceFromMap, base1To0 } from '../source'; @@ -314,7 +314,7 @@ export abstract class Breakpoint { const promises: Promise[] = []; for (const uiLocation of uiLocations) { - promises.push(this._setByScriptId(thread, script, source.offsetSourceToScript(uiLocation))); + promises.push(this._setForSpecific(thread, script, source.offsetSourceToScript(uiLocation))); } // If we get a source map that references this script exact URL, then @@ -480,7 +480,7 @@ export abstract class Breakpoint { private async _setByUiLocation(thread: Thread, uiLocation: IUiLocation): Promise { await Promise.all( - uiLocation.source.scripts.map(script => this._setByScriptId(thread, script, uiLocation)), + uiLocation.source.scripts.map(script => this._setForSpecific(thread, script, uiLocation)), ); } @@ -539,7 +539,9 @@ export abstract class Breakpoint { lcEqual(bp.args.location, lineColumn)) || (script.url && isSetByUrl(bp.args) && - new RegExp(bp.args.urlRegex ?? '').test(script.url) && + (bp.args.urlRegex + ? new RegExp(bp.args.urlRegex).test(script.url) + : script.url === bp.args.url) && lcEqual(bp.args, lineColumn)), ); } @@ -549,23 +551,60 @@ export abstract class Breakpoint { * at the provided script by url regexp already. This is used to deduplicate breakpoint * requests to avoid triggering any logpoint breakpoints multiple times. */ - protected hasSetOnLocationByRegexp(urlRegexp: string, lineColumn: LineColumn) { + protected hasSetOnLocationByUrl(kind: 're' | 'url', input: string, lineColumn: LineColumn) { return this.cdpBreakpoints.find(bp => { if (isSetByUrl(bp.args)) { - return bp.args.urlRegex === urlRegexp && lcEqual(bp.args, lineColumn); + if (!lcEqual(bp.args, lineColumn)) { + return false; + } + + if (kind === 'url') { + return bp.args.urlRegex + ? new RegExp(bp.args.urlRegex).test(input) + : bp.args.url === input; + } else { + return kind === 're' && bp.args.urlRegex === input; + } } const script = this._manager._sourceContainer.getScriptById(bp.args.location.scriptId); if (script) { - return lcEqual(bp.args.location, lineColumn) && new RegExp(urlRegexp).test(script.url); + return lcEqual(bp.args.location, lineColumn) && kind === 're' + ? new RegExp(input).test(script.url) + : script.url === input; } return undefined; }); } + protected async _setForSpecific(thread: Thread, script: ISourceScript, lineColumn: LineColumn) { + // prefer to set on script URL for non-anonymous scripts, since url breakpoints + // will survive and be hit on reload. + if (script.url) { + return this._setByUrl(thread, script.url, lineColumn); + } else { + return this._setByScriptId(thread, script, lineColumn); + } + } + protected async _setByUrl(thread: Thread, url: string, lineColumn: LineColumn): Promise { - return this._setByUrlRegexp(thread, urlToRegex(url), lineColumn); + lineColumn = base1To0(lineColumn); + + const previous = this.hasSetOnLocationByUrl('url', url, lineColumn); + if (previous) { + if (previous.state === CdpReferenceState.Pending) { + await previous.done; + } + + return; + } + + return this._setAny(thread, { + url, + condition: this.getBreakCondition(), + ...lineColumn, + }); } protected async _setByUrlRegexp( @@ -573,12 +612,9 @@ export abstract class Breakpoint { urlRegex: string, lineColumn: LineColumn, ): Promise { - lineColumn = { - columnNumber: lineColumn.columnNumber - 1, - lineNumber: lineColumn.lineNumber - 1, - }; + lineColumn = base1To0(lineColumn); - const previous = this.hasSetOnLocationByRegexp(urlRegex, lineColumn); + const previous = this.hasSetOnLocationByUrl('re', urlRegex, lineColumn); if (previous) { if (previous.state === CdpReferenceState.Pending) { await previous.done; diff --git a/src/adapter/source.ts b/src/adapter/source.ts index e44a0b737..39649789d 100644 --- a/src/adapter/source.ts +++ b/src/adapter/source.ts @@ -509,6 +509,7 @@ export const isSourceWithWasm = ( source: unknown, ): source is ISourceWithMap => isSourceWithMap(source) && source.sourceMap.type === SourceLocationType.WasmSymbols; + export type ContentGetter = () => Promise; export type LineColumn = { lineNumber: number; columnNumber: number }; // 1-based diff --git a/src/adapter/variableStore.ts b/src/adapter/variableStore.ts index 2cb4e9984..f5dda2f61 100644 --- a/src/adapter/variableStore.ts +++ b/src/adapter/variableStore.ts @@ -7,6 +7,7 @@ import { generate } from 'astring'; import { inject, injectable } from 'inversify'; import Cdp from '../cdp/api'; import { ICdpApi } from '../cdp/connection'; +import { groupBy, iteratorFirst } from '../common/arrayUtils'; import { flatten, isInstanceOf, once } from '../common/objUtils'; import { parseSource, statementsToFunction } from '../common/sourceCodeManipulations'; import { IRenameProvider } from '../common/sourceMaps/renameProvider'; @@ -18,8 +19,9 @@ import { ProtocolError } from '../dap/protocolError'; import * as objectPreview from './objectPreview'; import { MapPreview, SetPreview } from './objectPreview/betterTypes'; import { PreviewContextType } from './objectPreview/contexts'; +import { SourceFromMap, isSourceWithWasm } from './source'; import { StackFrame, StackTrace } from './stackTrace'; -import { getSourceSuffix, RemoteException, RemoteObjectId } from './templates'; +import { RemoteException, RemoteObjectId, getSourceSuffix } from './templates'; import { getArrayProperties } from './templates/getArrayProperties'; import { getArraySlots } from './templates/getArraySlots'; import { @@ -30,6 +32,7 @@ import { import { invokeGetter } from './templates/invokeGetter'; import { readMemory } from './templates/readMemory'; import { writeMemory } from './templates/writeMemory'; +import { IWasmVariable, IWasmVariableEvaluation, WasmScope } from './wasmSymbolProvider'; const getVariableId = (() => { let last = 0; @@ -160,7 +163,7 @@ export interface IStoreSettings { customPropertiesGenerator?: string; } -type VariableCtor = { +type VariableCtor = { new (context: VariableContext, ...rest: TRestArgs): R; }; @@ -177,6 +180,12 @@ interface IContextSettings { descriptionSymbols?: Promise; } +const wasmScopeNames: { [K in WasmScope]: { name: string; sortOrder: number } } = { + [WasmScope.Parameter]: { name: l10n.t('Native Parameters'), sortOrder: -10 }, + [WasmScope.Local]: { name: l10n.t('Native Locals'), sortOrder: -9 }, + [WasmScope.Global]: { name: l10n.t('Native Globals'), sortOrder: -8 }, +}; + class VariableContext { /** When in a Variable, the name that this variable is accessible as from its parent scope or object */ public readonly name: string; @@ -191,11 +200,11 @@ class VariableContext { constructor( public readonly cdp: Cdp.Api, - public readonly parent: undefined | Variable | Scope, + public readonly parent: undefined | IVariable | Scope, ctx: IContextInit, private readonly vars: VariablesMap, public readonly locationProvider: IVariableStoreLocationProvider, - private readonly currentRef: undefined | (() => Variable | Scope), + private readonly currentRef: undefined | (() => IVariable | Scope), private readonly settings: IContextSettings, ) { this.name = ctx.name; @@ -271,6 +280,21 @@ class VariableContext { return this.createVariable(Variable, ctx, object); } + public createVariableForWasm(variables: IWasmVariable[], scopeRef: IScopeRef) { + const groups = groupBy(variables, v => v.scope); + const groupVars: WasmScopeVariable[] = []; + for (const [key, display] of Object.entries(wasmScopeNames)) { + const vars = groups.get(key as WasmScope); + if (vars) { + groupVars.push( + this.createVariable(WasmScopeVariable, display, key as WasmScope, vars, scopeRef), + ); + } + } + + return groupVars; + } + /** * Ensures symbols for custom descriptions are available, must be used * before getStringProps/getToStringIfCustom @@ -546,14 +570,14 @@ class Variable implements IVariable { */ public get accessor(): string { const { parent, name } = this.context; - if (!parent || parent instanceof Scope) { - return this.context.name; - } - if (parent instanceof AccessorVariable) { return parent.accessor; } + if (!(parent instanceof Variable)) { + return this.context.name; + } + // Maps and sets: const grandparent = parent.context.parent; if (grandparent instanceof Variable) { @@ -1023,6 +1047,131 @@ class GetterVariable extends AccessorVariable { } } +class WasmVariable implements IVariable, IMemoryReadable { + public static presentationHint: Dap.VariablePresentationHint = { + attributes: ['readOnly'], + }; + + /** @inheritdoc */ + public readonly id = getVariableId(); + + /** @inheritdoc */ + public readonly sortOrder = 0; + + constructor( + private readonly context: VariableContext, + private readonly variable: IWasmVariableEvaluation, + private readonly scopeRef: IScopeRef, + ) {} + + public toDap(): Promise { + return Promise.resolve({ + name: this.context.name, + value: this.variable.description || '', + variablesReference: this.variable.getChildren ? this.id : 0, + memoryReference: this.variable.linearMemoryAddress ? String(this.id) : undefined, + presentationHint: this.context.presentationHint, + }); + } + + public async getChildren(): Promise { + const children = (await this.variable.getChildren?.()) || []; + return children.map(c => + this.context.createVariable( + WasmVariable, + { name: c.name, presentationHint: WasmVariable.presentationHint }, + c.value, + this.scopeRef, + ), + ); + } + + /** @inheritdoc */ + public async readMemory(offset: number, count: number): Promise { + const addr = this.variable.linearMemoryAddress; + if (addr === undefined) { + return undefined; + } + + const result = await this.context.cdp.Debugger.evaluateOnCallFrame({ + callFrameId: this.scopeRef.callFrameId, + expression: `(${readMemory.source}).call(memories[0].buffer, ${ + +addr + offset + }, ${+count}) ${getSourceSuffix()}`, + returnByValue: true, + }); + + if (!result) { + throw new RemoteException({ + exceptionId: 0, + text: 'No response from CDP', + lineNumber: 0, + columnNumber: 0, + }); + } + + return Buffer.from(result.result.value, 'hex'); + } + + /** @inheritdoc */ + public async writeMemory(offset: number, memory: Buffer): Promise { + const addr = this.variable.linearMemoryAddress; + if (addr === undefined) { + return 0; + } + + const result = await this.context.cdp.Debugger.evaluateOnCallFrame({ + callFrameId: this.scopeRef.callFrameId, + expression: `(${writeMemory.source}).call(memories[0].buffer, ${ + +addr + offset + }, ${JSON.stringify(memory.toString('hex'))}) ${getSourceSuffix()}`, + returnByValue: true, + }); + + return result?.result.value || 0; + } +} + +class WasmScopeVariable implements IVariable { + /** @inheritdoc */ + public readonly id = getVariableId(); + + /** @inheritdoc */ + public readonly sortOrder = wasmScopeNames[this.kind].sortOrder; + + constructor( + private readonly context: VariableContext, + public readonly kind: WasmScope, + private readonly variables: readonly IWasmVariable[], + private readonly scopeRef: IScopeRef, + ) {} + + toDap(): Promise { + return Promise.resolve({ + name: wasmScopeNames[this.kind].name, + value: '', + variablesReference: this.id, + }); + } + + getChildren(): Promise { + return Promise.all( + this.variables.map(async v => { + const evaluated = await v.evaluate(); + return this.context.createVariable( + WasmVariable, + { + name: v.name, + presentationHint: WasmVariable.presentationHint, + }, + evaluated, + this.scopeRef, + ); + }), + ); + } +} + class Scope implements IVariableContainer { /** @inheritdoc */ public readonly id = getVariableId(); @@ -1035,7 +1184,7 @@ class Scope implements IVariableContainer { private readonly renameProvider: IRenameProvider, ) {} - public async getChildren(_params: Dap.VariablesParams): Promise { + public async getChildren(_params: Dap.VariablesParams): Promise { const variables = await this.context.createObjectPropertyVars(this.remoteObject); const existing = new Set(variables.map(v => v.name)); for (const extraProperty of this.extraProperties) { @@ -1046,6 +1195,23 @@ class Scope implements IVariableContainer { } } + // special case: if the stack frame is in a wasm that had a symbolicated + // source at this location, use its symbol mapping instead of native symbols. + if (this.ref.scopeNumber === 0) { + const location = await this.ref.stackFrame.uiLocation(); + if (location?.source instanceof SourceFromMap) { + const c = iteratorFirst(location.source.compiledToSourceUrl.keys()); + if (isSourceWithWasm(c) && c.sourceMap.value.settledValue?.getVariablesInScope) { + const wasmVars = await c.sourceMap.value.settledValue.getVariablesInScope( + this.ref.callFrameId, + this.ref.stackFrame.rawPosition, + ); + const resolved: IVariable[] = this.context.createVariableForWasm(wasmVars, this.ref); + return resolved.concat(variables); + } + } + } + return variables; } diff --git a/src/adapter/wasmSymbolProvider.ts b/src/adapter/wasmSymbolProvider.ts index 747c8d7a6..67b9bcda1 100644 --- a/src/adapter/wasmSymbolProvider.ts +++ b/src/adapter/wasmSymbolProvider.ts @@ -12,16 +12,11 @@ import { IDisposable } from '../common/disposable'; import { ILogger, LogTag } from '../common/logging'; import { once } from '../common/objUtils'; import { Base0Position, IPosition } from '../common/positions'; -import { StackFrame } from './stackTrace'; import { getSourceSuffix } from './templates'; -import { Thread } from './threads'; export const IWasmSymbolProvider = Symbol('IWasmSymbolProvider'); export interface IWasmSymbolProvider { - /** Sets the thread, required to interact with the stacktrace state */ - setThread(thread: Thread): void; - /** Loads WebAssembly symbols for the given wasm script, returning symbol information if it exists. */ loadWasmSymbols(script: Cdp.Debugger.ScriptParsedEvent): Promise; } @@ -30,10 +25,6 @@ export interface IWasmSymbolProvider { export class StubWasmSymbolProvider implements IWasmSymbolProvider { constructor(@inject(ICdpApi) private readonly cdp: Cdp.Api) {} - setThread(): void { - // no-op - } - public loadWasmSymbols(script: Cdp.Debugger.ScriptParsedEvent): Promise { return Promise.resolve(new DecompiledWasmSymbols(script, this.cdp, [])); } @@ -42,7 +33,6 @@ export class StubWasmSymbolProvider implements IWasmSymbolProvider { @injectable() export class WasmSymbolProvider implements IWasmSymbolProvider, IDisposable { private worker?: IWasmWorker; - private thread!: Thread; constructor( private readonly spawnDwarf: typeof spawn, @@ -50,11 +40,6 @@ export class WasmSymbolProvider implements IWasmSymbolProvider, IDisposable { @inject(ILogger) private readonly logger: ILogger, ) {} - /** @inheritdoc */ - public setThread(thread: Thread) { - this.thread = thread; - } - public async loadWasmSymbols(script: Cdp.Debugger.ScriptParsedEvent): Promise { const rpc = await this.getWorker(); const moduleId = randomUUID(); @@ -108,7 +93,7 @@ export class WasmSymbolProvider implements IWasmSymbolProvider, IDisposable { this.loadWasmValue( `[].slice.call(new Uint8Array(memories[0].buffer, ${+offset}, ${+length}))`, stopId, - ), + ).then((v: number[]) => new Uint8Array(v).buffer), }); this.worker.rpc.sendMessage('hello', [], false); @@ -117,12 +102,7 @@ export class WasmSymbolProvider implements IWasmSymbolProvider, IDisposable { } private async loadWasmValue(expression: string, stopId: unknown) { - const frame = this.stopIdToFrame(stopId as bigint); - const callFrameId = frame.callFrameId(); - if (!callFrameId) { - throw new Error('variables not available on this frame'); - } - + const callFrameId = stopId as string; const result = await this.cdp.Debugger.evaluateOnCallFrame({ callFrameId, expression: expression + getSourceSuffix(), @@ -137,15 +117,27 @@ export class WasmSymbolProvider implements IWasmSymbolProvider, IDisposable { return result.result.value; } +} - private stopIdToFrame(stopId: bigint): StackFrame { - const frame = this.thread.pausedDetails()?.stackTrace.frames[Number(stopId)]; - if (!frame || !('callFrameId' in frame)) { - throw new Error('frame not found'); - } +export interface IWasmVariableEvaluation { + type: string; + description: string | undefined; + linearMemoryAddress?: number; + linearMemorySize?: number; + getChildren?: () => Promise<{ name: string; value: IWasmVariableEvaluation }[]>; +} - return frame; - } +export const enum WasmScope { + Local = 'LOCAL', + Global = 'GLOBAL', + Parameter = 'PARAMETER', +} + +export interface IWasmVariable { + scope: WasmScope; + name: string; + type: string; + evaluate: () => Promise; } export interface IWasmSymbols extends IDisposable { @@ -168,7 +160,7 @@ export interface IWasmSymbols extends IDisposable { /** * Gets the source position for the given position in compiled code. * - * Following CDP semantics, it assumes the position is line 0 with the column + * Following CDP semantics, it returns a position on line 0 with the column * offset being the byte offset in webassembly. */ originalPositionFor( @@ -182,6 +174,15 @@ export interface IWasmSymbols extends IDisposable { * offset being the byte offset in webassembly. */ compiledPositionFor(sourceUrl: string, sourcePosition: IPosition): Promise; + + /** + * Gets variables in the program scope at the given position. If not + * implemented, the variable store should use its default behavior. + * + * Following CDP semantics, it assumes the position is line 0 with the column + * offset being the byte offset in webassembly. + */ + getVariablesInScope?(callFrameId: string, position: IPosition): Promise; } class DecompiledWasmSymbols implements IWasmSymbols { @@ -361,4 +362,67 @@ class WasmSymbols extends DecompiledWasmSymbols { public override dispose() { return this.rpc.sendMessage('removeRawModule', this.moduleId); } + + /** @inheritdoc */ + public async getVariablesInScope( + callFrameId: string, + position: IPosition, + ): Promise { + const location = { + codeOffset: position.base0.columnNumber - this.codeOffset, + inlineFrameIndex: 0, + rawModuleId: this.moduleId, + }; + + const variables = await this.rpc.sendMessage('listVariablesInScope', location); + + return variables.map( + (v): IWasmVariable => ({ + name: v.name, + scope: v.scope as WasmScope, + type: v.type, + evaluate: async () => { + const result = await this.rpc.sendMessage('evaluate', v.name, location, callFrameId); + return result ? new WasmVariableEvaluation(result, this.rpc) : nullType; + }, + }), + ); + } +} + +const nullType: IWasmVariableEvaluation = { + type: 'null', + description: 'no properties', +}; + +class WasmVariableEvaluation implements IWasmVariableEvaluation { + public readonly type: string; + public readonly description: string | undefined; + public readonly linearMemoryAddress: number | undefined; + public readonly linearMemorySize: number | undefined; + + public readonly getChildren?: () => Promise<{ name: string; value: IWasmVariableEvaluation }[]>; + + constructor(evaluation: NonNullable>, rpc: IWasmWorker['rpc']) { + this.type = evaluation.type; + this.description = evaluation.description; + this.linearMemoryAddress = evaluation.linearMemoryAddress; + this.linearMemorySize = evaluation.linearMemoryAddress; + + if (evaluation.objectId && evaluation.hasChildren) { + const oid = evaluation.objectId; + this.getChildren = once(() => this._getChildren(rpc, oid)); + } + } + + private async _getChildren( + rpc: IWasmWorker['rpc'], + objectId: string, + ): Promise<{ name: string; value: IWasmVariableEvaluation }[]> { + const vars = await rpc.sendMessage('getProperties', objectId); + return vars.map(v => ({ + name: v.name, + value: new WasmVariableEvaluation(v.value, rpc), + })); + } } diff --git a/src/common/arrayUtils.ts b/src/common/arrayUtils.ts index 325a486cd..0dd7fcd0a 100644 --- a/src/common/arrayUtils.ts +++ b/src/common/arrayUtils.ts @@ -32,3 +32,26 @@ export function binarySearch( return low; } + +/** + * Groups an array using an accessor function. + */ +export function groupBy(array: T[], accessor: (item: T) => K): Map { + const groups: Map = new Map(); + + for (const item of array) { + const key = accessor(item); + const group = groups.get(key); + if (group) { + group.push(item); + } else { + groups.set(key, [item]); + } + } + + return groups; +} + +export function iteratorFirst(it: IterableIterator): T | undefined { + return it.next().value; +} diff --git a/src/common/sourceMaps/renameProvider.ts b/src/common/sourceMaps/renameProvider.ts index f5c772dd8..e180045b9 100644 --- a/src/common/sourceMaps/renameProvider.ts +++ b/src/common/sourceMaps/renameProvider.ts @@ -3,9 +3,10 @@ *--------------------------------------------------------*/ import { inject, injectable } from 'inversify'; -import { ISourceWithMap, Source, SourceFromMap, isSourceWithSourceMap } from '../../adapter/source'; +import { Source, SourceFromMap, isSourceWithSourceMap } from '../../adapter/source'; import { StackFrame } from '../../adapter/stackTrace'; import { AnyLaunchConfiguration } from '../../configuration'; +import { iteratorFirst } from '../arrayUtils'; import { Base01Position, IPosition } from '../positions'; import { PositionToOffset } from '../stringUtils'; import { SourceMap } from './sourceMap'; @@ -73,7 +74,7 @@ export class RenameProvider implements IRenameProvider { return RenameMapping.None; } - const original: ISourceWithMap | undefined = source.compiledToSourceUrl.keys().next().value; + const original = iteratorFirst(source.compiledToSourceUrl.keys()); if (!original) { throw new Error('unreachable'); } diff --git a/src/common/urlUtils.ts b/src/common/urlUtils.ts index b3be677b6..8d5e5479f 100644 --- a/src/common/urlUtils.ts +++ b/src/common/urlUtils.ts @@ -4,10 +4,11 @@ import { promises as dns } from 'dns'; import * as path from 'path'; -import { parse as urlParse, URL } from 'url'; +import { URL, parse as urlParse } from 'url'; import Cdp from '../cdp/api'; import { AnyChromiumConfiguration } from '../configuration'; import { BrowserTargetType } from '../targets/browser/browserTargets'; +import { iteratorFirst } from './arrayUtils'; import { MapUsingProjection } from './datastructure/mapUsingProjection'; import { IFsUtils } from './fsUtils'; import { memoize } from './objUtils'; @@ -362,7 +363,7 @@ const createReGroup = (patterns: ReadonlySet): string => { case 0: return ''; case 1: - return patterns.values().next().value; + return iteratorFirst(patterns.values()) as string; default: // Prefer the more compacy [aA] form if we're only matching single // characters, produce a non-capturing group otherwise. diff --git a/src/targets/node/nodeLauncher.ts b/src/targets/node/nodeLauncher.ts index 2d0e12943..c0dde842c 100644 --- a/src/targets/node/nodeLauncher.ts +++ b/src/targets/node/nodeLauncher.ts @@ -7,20 +7,19 @@ import { extname, resolve } from 'path'; import { IBreakpointsPredictor } from '../../adapter/breakpointPredictor'; import { IPortLeaseTracker } from '../../adapter/portLeaseTracker'; import Cdp from '../../cdp/api'; -import { asArray } from '../../common/arrayUtils'; +import { asArray, iteratorFirst } from '../../common/arrayUtils'; import { DebugType } from '../../common/contributionUtils'; import { IFsUtils, LocalFsUtils } from '../../common/fsUtils'; import { ILogger, LogTag } from '../../common/logging'; import { fixDriveLetterAndSlashes } from '../../common/pathUtils'; import { delay } from '../../common/promiseUtil'; -import { ISourceMapMetadata } from '../../common/sourceMaps/sourceMap'; import { absolutePathToFileUrl, urlToRegex } from '../../common/urlUtils'; import { AnyLaunchConfiguration, INodeLaunchConfiguration } from '../../configuration'; import { fixInspectFlags } from '../../ui/configurationUtils'; import { retryGetNodeEndpoint } from '../browser/spawn/endpoints'; import { ISourcePathResolverFactory } from '../sourcePathResolverFactory'; import { CallbackFile } from './callback-file'; -import { getRunScript, hideDebugInfoFromConsole, INodeBinaryProvider } from './nodeBinaryProvider'; +import { INodeBinaryProvider, getRunScript, hideDebugInfoFromConsole } from './nodeBinaryProvider'; import { IProcessTelemetry, IRunData, NodeLauncherBase } from './nodeLauncherBase'; import { INodeTargetLifecycleHooks } from './nodeTarget'; import { IPackageJsonProvider } from './packageJsonProvider'; @@ -305,7 +304,7 @@ export class NodeLauncher extends NodeLauncherBase { // There can be more than one compile file per source file. Just pick // whichever one in that case. - const entry: ISourceMapMetadata = mapped.values().next().value; + const entry = iteratorFirst(mapped.values()); if (!entry) { return targetProgram; } diff --git a/src/ui/profiling/uiProfileManager.ts b/src/ui/profiling/uiProfileManager.ts index 2a1b9ad74..c8d34a4c7 100644 --- a/src/ui/profiling/uiProfileManager.ts +++ b/src/ui/profiling/uiProfileManager.ts @@ -7,7 +7,8 @@ import { inject, injectable, multiInject } from 'inversify'; import { homedir } from 'os'; import { basename, join } from 'path'; import * as vscode from 'vscode'; -import { getDefaultProfileName, ProfilerFactory } from '../../adapter/profiling'; +import { ProfilerFactory, getDefaultProfileName } from '../../adapter/profiling'; +import { iteratorFirst } from '../../common/arrayUtils'; import { Commands, ContextKey, setContextKey } from '../../common/contributionUtils'; import { DisposableList, IDisposable } from '../../common/disposable'; import { moveFile } from '../../common/fsUtils'; @@ -260,8 +261,8 @@ export class UiProfileManager implements IDisposable { setContextKey(vscode.commands, ContextKey.IsProfiling, true); - if (this.activeSessions.size === 1) { - const session: UiProfileSession = this.activeSessions.values().next().value; + const session = iteratorFirst(this.activeSessions.values()); + if (session && this.activeSessions.size === 1) { this.statusBarItem.text = session.status ? l10n.t('{0} Click to Stop Profiling ({1})', '$(loading~spin)', session.status) : l10n.t('{0} Click to Stop Profiling', '$(loading~spin)');