UNPKG

chrome-devtools-frontend

Version:
767 lines (680 loc) • 33.6 kB
// Copyright 2014 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.js'; import * as Platform from '../../core/platform/platform.js'; import * as Root from '../../core/root/root.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Protocol from '../../generated/protocol.js'; import type * as StackTrace from '../stack_trace/stack_trace.js'; // eslint-disable-next-line @devtools/es-modules-import import * as StackTraceImpl from '../stack_trace/stack_trace_impl.js'; import type * as TextUtils from '../text_utils/text_utils.js'; import * as Workspace from '../workspace/workspace.js'; import {CompilerScriptMapping} from './CompilerScriptMapping.js'; import {DebuggerLanguagePluginManager} from './DebuggerLanguagePlugins.js'; import {DefaultScriptMapping} from './DefaultScriptMapping.js'; import {type LiveLocation, type LiveLocationPool, LiveLocationWithPool} from './LiveLocation.js'; import {NetworkProject} from './NetworkProject.js'; import type {ResourceMapping} from './ResourceMapping.js'; import {type ResourceScriptFile, ResourceScriptMapping} from './ResourceScriptMapping.js'; export class DebuggerWorkspaceBinding implements SDK.TargetManager.SDKModelObserver<SDK.DebuggerModel.DebuggerModel> { readonly resourceMapping: ResourceMapping; readonly #debuggerModelToData: Map<SDK.DebuggerModel.DebuggerModel, ModelData>; readonly #liveLocationPromises: Set<Promise<void|Location|StackTraceTopFrameLocation|null>>; readonly pluginManager: DebuggerLanguagePluginManager; readonly ignoreListManager: Workspace.IgnoreListManager.IgnoreListManager; readonly workspace: Workspace.Workspace.WorkspaceImpl; constructor( resourceMapping: ResourceMapping, targetManager: SDK.TargetManager.TargetManager, ignoreListManager: Workspace.IgnoreListManager.IgnoreListManager, workspace: Workspace.Workspace.WorkspaceImpl) { this.resourceMapping = resourceMapping; this.resourceMapping.debuggerWorkspaceBinding = this; this.ignoreListManager = ignoreListManager; this.workspace = workspace; this.#debuggerModelToData = new Map(); targetManager.addModelListener( SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.GlobalObjectCleared, this.globalObjectCleared, this); targetManager.addModelListener( SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.DebuggerResumed, this.debuggerResumed, this); targetManager.observeModels(SDK.DebuggerModel.DebuggerModel, this); this.ignoreListManager.addEventListener( Workspace.IgnoreListManager.Events.IGNORED_SCRIPT_RANGES_UPDATED, event => this.updateLocations(event.data)); this.#liveLocationPromises = new Set(); this.pluginManager = new DebuggerLanguagePluginManager(targetManager, resourceMapping.workspace, this); } setFunctionRanges( uiSourceCode: Workspace.UISourceCode.UISourceCode, ranges: SDK.SourceMapFunctionRanges.NamedFunctionRange[]): void { for (const modelData of this.#debuggerModelToData.values()) { modelData.compilerMapping.setFunctionRanges(uiSourceCode, ranges); } } static instance(opts: { forceNew: boolean|null, resourceMapping: ResourceMapping|null, targetManager: SDK.TargetManager.TargetManager|null, ignoreListManager: Workspace.IgnoreListManager.IgnoreListManager|null, workspace: Workspace.Workspace.WorkspaceImpl|null, } = {forceNew: null, resourceMapping: null, targetManager: null, ignoreListManager: null, workspace: null}): DebuggerWorkspaceBinding { const {forceNew, resourceMapping, targetManager, ignoreListManager, workspace} = opts; if (forceNew) { if (!resourceMapping || !targetManager || !ignoreListManager || !workspace) { throw new Error( `Unable to create DebuggerWorkspaceBinding: resourceMapping, targetManager and IgnoreLIstManager must be provided: ${ new Error().stack}`); } Root.DevToolsContext.globalInstance().set( DebuggerWorkspaceBinding, new DebuggerWorkspaceBinding(resourceMapping, targetManager, ignoreListManager, workspace)); } return Root.DevToolsContext.globalInstance().get(DebuggerWorkspaceBinding); } static removeInstance(): void { Root.DevToolsContext.globalInstance().delete(DebuggerWorkspaceBinding); } private async computeAutoStepRanges(mode: SDK.DebuggerModel.StepMode, callFrame: SDK.DebuggerModel.CallFrame): Promise<SDK.DebuggerModel.LocationRange[]> { function contained(location: SDK.DebuggerModel.Location, range: SDK.DebuggerModel.LocationRange): boolean { const {start, end} = range; if (start.scriptId !== location.scriptId) { return false; } if (location.lineNumber < start.lineNumber || location.lineNumber > end.lineNumber) { return false; } if (location.lineNumber === start.lineNumber && location.columnNumber < start.columnNumber) { return false; } if (location.lineNumber === end.lineNumber && location.columnNumber >= end.columnNumber) { return false; } return true; } const rawLocation = callFrame.location(); if (!rawLocation) { return []; } const pluginManager = this.pluginManager; let ranges: SDK.DebuggerModel.LocationRange[] = []; if (mode === SDK.DebuggerModel.StepMode.STEP_OUT) { // Step out of inline function. return await pluginManager.getInlinedFunctionRanges(rawLocation); } const uiLocation = await pluginManager.rawLocationToUILocation(rawLocation); if (uiLocation) { ranges = await pluginManager.uiLocationToRawLocationRanges( uiLocation.uiSourceCode, uiLocation.lineNumber, uiLocation.columnNumber) || []; // TODO(bmeurer): Remove the {rawLocation} from the {ranges}? ranges = ranges.filter(range => contained(rawLocation, range)); if (mode === SDK.DebuggerModel.StepMode.STEP_OVER) { // Step over an inlined function. ranges = ranges.concat(await pluginManager.getInlinedCalleesRanges(rawLocation)); } return ranges; } const compilerMapping = this.#debuggerModelToData.get(rawLocation.debuggerModel)?.compilerMapping; if (!compilerMapping) { return []; } ranges = compilerMapping.getLocationRangesForSameSourceLocation(rawLocation); ranges = ranges.filter(range => contained(rawLocation, range)); return ranges; } modelAdded(debuggerModel: SDK.DebuggerModel.DebuggerModel): void { debuggerModel.setBeforePausedCallback(this.shouldPause.bind(this)); this.#debuggerModelToData.set(debuggerModel, new ModelData(debuggerModel, this)); debuggerModel.setComputeAutoStepRangesCallback(this.computeAutoStepRanges.bind(this)); } modelRemoved(debuggerModel: SDK.DebuggerModel.DebuggerModel): void { debuggerModel.setComputeAutoStepRangesCallback(null); const modelData = this.#debuggerModelToData.get(debuggerModel); if (modelData) { modelData.dispose(); this.#debuggerModelToData.delete(debuggerModel); } } /** * The promise returned by this function is resolved once all *currently* * pending LiveLocations are processed. */ async pendingLiveLocationChangesPromise(): Promise<void|Location|StackTraceTopFrameLocation|null> { await Promise.all(this.#liveLocationPromises); } private recordLiveLocationChange(promise: Promise<void|Location|StackTraceTopFrameLocation|null>): void { void promise.then(() => { this.#liveLocationPromises.delete(promise); }); this.#liveLocationPromises.add(promise); } async updateLocations(script: SDK.Script.Script): Promise<void> { const updatePromises = [script.target() .model(StackTraceImpl.StackTraceModel.StackTraceModel) ?.scriptInfoChanged(script, this.#translateRawFrames.bind(this))]; const modelData = this.#debuggerModelToData.get(script.debuggerModel); if (modelData) { const updatePromise = modelData.updateLocations(script); this.recordLiveLocationChange(updatePromise); updatePromises.push(updatePromise); } await Promise.all(updatePromises); } async createStackTraceFromProtocolRuntime(stackTrace: Protocol.Runtime.StackTrace, target: SDK.Target.Target): Promise<StackTrace.StackTrace.StackTrace> { const model = target.model(StackTraceImpl.StackTraceModel.StackTraceModel) as StackTraceImpl.StackTraceModel.StackTraceModel; return await model.createFromProtocolRuntime(stackTrace, this.#translateRawFrames.bind(this)); } async createStackTraceFromDebuggerPaused( pausedDetails: SDK.DebuggerModel.DebuggerPausedDetails, target: SDK.Target.Target): Promise<StackTrace.StackTrace.DebuggableStackTrace> { const model = target.model(StackTraceImpl.StackTraceModel.StackTraceModel) as StackTraceImpl.StackTraceModel.StackTraceModel; return await model.createFromDebuggerPaused(pausedDetails, this.#translateRawFrames.bind(this)); } async createLiveLocation( rawLocation: SDK.DebuggerModel.Location, updateDelegate: (arg0: LiveLocation) => Promise<void>, locationPool: LiveLocationPool): Promise<Location|null> { const modelData = this.#debuggerModelToData.get(rawLocation.debuggerModel); if (!modelData) { return null; } const liveLocationPromise = modelData.createLiveLocation(rawLocation, updateDelegate, locationPool); this.recordLiveLocationChange(liveLocationPromise); return await liveLocationPromise; } async createStackTraceTopFrameLiveLocation( rawLocations: SDK.DebuggerModel.Location[], updateDelegate: (arg0: LiveLocation) => Promise<void>, locationPool: LiveLocationPool): Promise<LiveLocation> { console.assert(rawLocations.length > 0); const locationPromise = StackTraceTopFrameLocation.createStackTraceTopFrameLocation(rawLocations, this, updateDelegate, locationPool); this.recordLiveLocationChange(locationPromise); return await locationPromise; } async createCallFrameLiveLocation( location: SDK.DebuggerModel.Location, updateDelegate: (arg0: LiveLocation) => Promise<void>, locationPool: LiveLocationPool): Promise<Location|null> { const script = location.script(); if (!script) { return null; } const debuggerModel = location.debuggerModel; const liveLocationPromise = this.createLiveLocation(location, updateDelegate, locationPool); this.recordLiveLocationChange(liveLocationPromise); const liveLocation = await liveLocationPromise; if (!liveLocation) { return null; } this.registerCallFrameLiveLocation(debuggerModel, liveLocation); return liveLocation; } async rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location): Promise<Workspace.UISourceCode.UILocation|null> { const uiLocation = await this.pluginManager.rawLocationToUILocation(rawLocation); if (uiLocation) { return uiLocation; } const modelData = this.#debuggerModelToData.get(rawLocation.debuggerModel); return modelData ? modelData.rawLocationToUILocation(rawLocation) : null; } uiSourceCodeForSourceMapSourceURL( debuggerModel: SDK.DebuggerModel.DebuggerModel, url: Platform.DevToolsPath.UrlString, isContentScript: boolean): Workspace.UISourceCode.UISourceCode|null { const modelData = this.#debuggerModelToData.get(debuggerModel); if (!modelData) { return null; } return modelData.compilerMapping.uiSourceCodeForURL(url, isContentScript); } async uiSourceCodeForSourceMapSourceURLPromise( debuggerModel: SDK.DebuggerModel.DebuggerModel, url: Platform.DevToolsPath.UrlString, isContentScript: boolean): Promise<Workspace.UISourceCode.UISourceCode> { const uiSourceCode = this.uiSourceCodeForSourceMapSourceURL(debuggerModel, url, isContentScript); return await (uiSourceCode || this.waitForUISourceCodeAdded(url, debuggerModel.target())); } async uiSourceCodeForDebuggerLanguagePluginSourceURLPromise( debuggerModel: SDK.DebuggerModel.DebuggerModel, url: Platform.DevToolsPath.UrlString): Promise<Workspace.UISourceCode.UISourceCode|null> { const uiSourceCode = this.pluginManager.uiSourceCodeForURL(debuggerModel, url); return await (uiSourceCode || this.waitForUISourceCodeAdded(url, debuggerModel.target())); } uiSourceCodeForScript(script: SDK.Script.Script): Workspace.UISourceCode.UISourceCode|null { const modelData = this.#debuggerModelToData.get(script.debuggerModel); if (!modelData) { return null; } return modelData.uiSourceCodeForScript(script); } waitForUISourceCodeAdded(url: Platform.DevToolsPath.UrlString, target: SDK.Target.Target): Promise<Workspace.UISourceCode.UISourceCode> { return new Promise(resolve => { const descriptor = this.workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeAdded, event => { const uiSourceCode = event.data; if (uiSourceCode.url() === url && NetworkProject.targetForUISourceCode(uiSourceCode) === target) { this.workspace.removeEventListener(Workspace.Workspace.Events.UISourceCodeAdded, descriptor.listener); resolve(uiSourceCode); } }); }); } async uiLocationToRawLocations( uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number, columnNumber?: number): Promise<SDK.DebuggerModel.Location[]> { const locations = await this.pluginManager.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber); if (locations) { return locations; } for (const modelData of this.#debuggerModelToData.values()) { const locations = modelData.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber); if (locations.length) { return locations; } } return []; } /** * Computes all the raw location ranges that intersect with the {@link textRange} in the given * {@link uiSourceCode}. The reverse mappings of the returned ranges must not be fully contained * with the {@link textRange} and it's the responsibility of the caller to appropriately filter or * clamp if desired. * * It's important to note that for a contiguous range in the {@link uiSourceCode} there can be a * variety of non-contiguous raw location ranges that intersect with the {@link textRange}. A * simple example is that of an HTML document with multiple inline `<script>`s in the same line, * so just asking for the raw locations in this single line will return a set of location ranges * in different scripts. * * This method returns an empty array if this {@link uiSourceCode} is not provided by any of the * mappings for this instance. * * @param uiSourceCode the {@link UISourceCode} to which the {@link textRange} belongs. * @param textRange the text range in terms of the UI. * @returns the list of raw location ranges that intersect with the text range or `[]` if * the {@link uiSourceCode} does not belong to this instance. */ async uiLocationRangeToRawLocationRanges( uiSourceCode: Workspace.UISourceCode.UISourceCode, textRange: TextUtils.TextRange.TextRange): Promise<SDK.DebuggerModel.LocationRange[]> { const ranges = await this.pluginManager.uiLocationRangeToRawLocationRanges(uiSourceCode, textRange); if (ranges) { return ranges; } for (const modelData of this.#debuggerModelToData.values()) { const ranges = modelData.uiLocationRangeToRawLocationRanges(uiSourceCode, textRange); if (ranges) { return ranges; } } return []; } async functionBoundsAtRawLocation(rawLocation: SDK.DebuggerModel.Location): Promise<Workspace.UISourceCode.UIFunctionBounds|null> { // TODO(crbug.com/463452667): first try pluginManager. const modelData = this.#debuggerModelToData.get(rawLocation.debuggerModel); return modelData ? await modelData.functionBoundsAtRawLocation(rawLocation) : null; } async normalizeUILocation(uiLocation: Workspace.UISourceCode.UILocation): Promise<Workspace.UISourceCode.UILocation> { const rawLocations = await this.uiLocationToRawLocations(uiLocation.uiSourceCode, uiLocation.lineNumber, uiLocation.columnNumber); for (const location of rawLocations) { const uiLocationCandidate = await this.rawLocationToUILocation(location); if (uiLocationCandidate) { return uiLocationCandidate; } } return uiLocation; } /** * Computes the set of lines in the {@link uiSourceCode} that map to scripts by either looking at * the debug info (if any) or checking for inline scripts within documents. If this set cannot be * computed or all the lines in the {@link uiSourceCode} correspond to lines in a script, `null` * is returned here. * * @param uiSourceCode the source entity. * @returns a set of known mapped lines for {@link uiSourceCode} or `null` if it's impossible to * determine the set or the {@link uiSourceCode} does not map to or include any scripts. */ async getMappedLines(uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<Set<number>|null> { for (const modelData of this.#debuggerModelToData.values()) { const mappedLines = modelData.getMappedLines(uiSourceCode); if (mappedLines !== null) { return mappedLines; } } return await this.pluginManager.getMappedLines(uiSourceCode); } scriptFile(uiSourceCode: Workspace.UISourceCode.UISourceCode, debuggerModel: SDK.DebuggerModel.DebuggerModel): ResourceScriptFile|null { const modelData = this.#debuggerModelToData.get(debuggerModel); return modelData ? modelData.getResourceScriptMapping().scriptFile(uiSourceCode) : null; } scriptsForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): SDK.Script.Script[] { const scripts = new Set<SDK.Script.Script>(); this.pluginManager.scriptsForUISourceCode(uiSourceCode).forEach(script => scripts.add(script)); for (const modelData of this.#debuggerModelToData.values()) { const resourceScriptFile = modelData.getResourceScriptMapping().scriptFile(uiSourceCode); if (resourceScriptFile?.script) { scripts.add(resourceScriptFile.script); } modelData.compilerMapping.scriptsForUISourceCode(uiSourceCode).forEach(script => scripts.add(script)); } return [...scripts]; } supportsConditionalBreakpoints(uiSourceCode: Workspace.UISourceCode.UISourceCode): boolean { const scripts = this.pluginManager.scriptsForUISourceCode(uiSourceCode); return scripts.every(script => script.isJavaScript()); } private globalObjectCleared(event: Common.EventTarget.EventTargetEvent<SDK.DebuggerModel.DebuggerModel>): void { this.reset(event.data); } private reset(debuggerModel: SDK.DebuggerModel.DebuggerModel): void { const modelData = this.#debuggerModelToData.get(debuggerModel); if (!modelData) { return; } for (const location of modelData.callFrameLocations.values()) { this.removeLiveLocation(location); } modelData.callFrameLocations.clear(); } resetForTest(target: SDK.Target.Target): void { const debuggerModel = (target.model(SDK.DebuggerModel.DebuggerModel) as SDK.DebuggerModel.DebuggerModel); const modelData = this.#debuggerModelToData.get(debuggerModel); if (modelData) { modelData.getResourceScriptMapping().resetForTest(); } } private registerCallFrameLiveLocation(debuggerModel: SDK.DebuggerModel.DebuggerModel, location: Location): void { const modelData = this.#debuggerModelToData.get(debuggerModel); if (modelData) { const locations = modelData.callFrameLocations; locations.add(location); } } removeLiveLocation(location: Location): void { const modelData = this.#debuggerModelToData.get(location.rawLocation.debuggerModel); if (modelData) { modelData.disposeLocation(location); } } private debuggerResumed(event: Common.EventTarget.EventTargetEvent<SDK.DebuggerModel.DebuggerModel>): void { this.reset(event.data); } private async shouldPause( debuggerPausedDetails: SDK.DebuggerModel.DebuggerPausedDetails, autoSteppingContext: SDK.DebuggerModel.Location|null): Promise<boolean> { // This function returns false if the debugger should continue stepping const {callFrames: [frame]} = debuggerPausedDetails; if (!frame) { return false; } const functionLocation = frame.functionLocation(); if (!autoSteppingContext || debuggerPausedDetails.reason !== Protocol.Debugger.PausedEventReason.Step || !functionLocation || !frame.script.isWasm() || !Common.Settings.moduleSetting('wasm-auto-stepping').get() || !this.pluginManager.hasPluginForScript(frame.script)) { return true; } const uiLocation = await this.pluginManager.rawLocationToUILocation(frame.location()); if (uiLocation) { return true; } return autoSteppingContext.script() !== functionLocation.script() || autoSteppingContext.columnNumber !== functionLocation.columnNumber || autoSteppingContext.lineNumber !== functionLocation.lineNumber; } async #translateRawFrames(frames: readonly StackTraceImpl.Trie.RawFrame[], target: SDK.Target.Target): ReturnType<StackTraceImpl.StackTraceModel.TranslateRawFrames> { const rawFrames = frames.slice(0); const translatedFrames: Awaited<ReturnType<StackTraceImpl.StackTraceModel.TranslateRawFrames>> = []; while (rawFrames.length) { await this.#translateRawFramesStep(rawFrames, translatedFrames, target); } return translatedFrames; } async #translateRawFramesStep( rawFrames: StackTraceImpl.Trie.RawFrame[], translatedFrames: Awaited<ReturnType<StackTraceImpl.StackTraceModel.TranslateRawFrames>>, target: SDK.Target.Target): Promise<void> { if (await this.pluginManager.translateRawFramesStep(rawFrames, translatedFrames, target)) { return; } const modelData = this.#debuggerModelToData.get(target.model(SDK.DebuggerModel.DebuggerModel) as SDK.DebuggerModel.DebuggerModel); if (modelData) { modelData.translateRawFramesStep(rawFrames, translatedFrames); return; } const frame = rawFrames.shift() as StackTraceImpl.Trie.RawFrame; const {url, lineNumber, columnNumber, functionName} = frame; translatedFrames.push([{url, line: lineNumber, column: columnNumber, name: functionName}]); } } class ModelData { readonly #debuggerModel: SDK.DebuggerModel.DebuggerModel; readonly #debuggerWorkspaceBinding: DebuggerWorkspaceBinding; callFrameLocations: Set<Location>; #defaultMapping: DefaultScriptMapping; readonly #resourceMapping: ResourceMapping; #resourceScriptMapping: ResourceScriptMapping; readonly compilerMapping: CompilerScriptMapping; readonly #locations: Platform.MapUtilities.Multimap<string, Location>; constructor(debuggerModel: SDK.DebuggerModel.DebuggerModel, debuggerWorkspaceBinding: DebuggerWorkspaceBinding) { this.#debuggerModel = debuggerModel; this.#debuggerWorkspaceBinding = debuggerWorkspaceBinding; this.callFrameLocations = new Set(); const {workspace} = debuggerWorkspaceBinding.resourceMapping; this.#defaultMapping = new DefaultScriptMapping(debuggerModel, workspace, debuggerWorkspaceBinding); this.#resourceMapping = debuggerWorkspaceBinding.resourceMapping; this.#resourceScriptMapping = new ResourceScriptMapping(debuggerModel, workspace, debuggerWorkspaceBinding); this.compilerMapping = new CompilerScriptMapping(debuggerModel, workspace, debuggerWorkspaceBinding); this.#locations = new Platform.MapUtilities.Multimap(); } async createLiveLocation( rawLocation: SDK.DebuggerModel.Location, updateDelegate: (arg0: LiveLocation) => Promise<void>, locationPool: LiveLocationPool): Promise<Location> { console.assert(rawLocation.scriptId !== ''); const scriptId = rawLocation.scriptId; const location = new Location(scriptId, rawLocation, this.#debuggerWorkspaceBinding, updateDelegate, locationPool); this.#locations.set(scriptId, location); await location.update(); return location; } disposeLocation(location: Location): void { this.#locations.delete(location.scriptId, location); } async updateLocations(script: SDK.Script.Script): Promise<void> { const promises = []; for (const location of this.#locations.get(script.scriptId)) { promises.push(location.update()); } await Promise.all(promises); } rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location): Workspace.UISourceCode.UILocation|null { let uiLocation = this.compilerMapping.rawLocationToUILocation(rawLocation); uiLocation = uiLocation || this.#resourceScriptMapping.rawLocationToUILocation(rawLocation); uiLocation = uiLocation || this.#resourceMapping.jsLocationToUILocation(rawLocation); uiLocation = uiLocation || this.#defaultMapping.rawLocationToUILocation(rawLocation); return uiLocation; } uiSourceCodeForScript(script: SDK.Script.Script): Workspace.UISourceCode.UISourceCode|null { let uiSourceCode: Workspace.UISourceCode.UISourceCode|null = null; uiSourceCode = uiSourceCode || this.#resourceScriptMapping.uiSourceCodeForScript(script); uiSourceCode = uiSourceCode || this.#resourceMapping.uiSourceCodeForScript(script); uiSourceCode = uiSourceCode || this.#defaultMapping.uiSourceCodeForScript(script); return uiSourceCode; } uiLocationToRawLocations( uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number, columnNumber: number|undefined = 0): SDK.DebuggerModel.Location[] { // TODO(crbug.com/1153123): Revisit the `#columnNumber = 0` and also preserve `undefined` for source maps? let locations = this.compilerMapping.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber); locations = locations.length ? locations : this.#resourceScriptMapping.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber); locations = locations.length ? locations : this.#resourceMapping.uiLocationToJSLocations(uiSourceCode, lineNumber, columnNumber); locations = locations.length ? locations : this.#defaultMapping.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber); return locations; } uiLocationRangeToRawLocationRanges( uiSourceCode: Workspace.UISourceCode.UISourceCode, textRange: TextUtils.TextRange.TextRange): SDK.DebuggerModel.LocationRange[]|null { let ranges = this.compilerMapping.uiLocationRangeToRawLocationRanges(uiSourceCode, textRange); ranges ??= this.#resourceScriptMapping.uiLocationRangeToRawLocationRanges(uiSourceCode, textRange); ranges ??= this.#resourceMapping.uiLocationRangeToJSLocationRanges(uiSourceCode, textRange); ranges ??= this.#defaultMapping.uiLocationRangeToRawLocationRanges(uiSourceCode, textRange); return ranges; } async functionBoundsAtRawLocation(rawLocation: SDK.DebuggerModel.Location): Promise<Workspace.UISourceCode.UIFunctionBounds|null> { let scope: Workspace.UISourceCode.UIFunctionBounds|null = null; // Check source maps. scope = scope || await this.compilerMapping.functionBoundsAtRawLocation(rawLocation); // Check debugger scripts. scope = scope || await this.#resourceScriptMapping.functionBoundsAtRawLocation(rawLocation); // Check inline scripts inside HTML resources. scope = scope || await this.#resourceMapping.functionBoundsAtRawLocation(rawLocation); return scope; } translateRawFramesStep( rawFrames: StackTraceImpl.Trie.RawFrame[], translatedFrames: Awaited<ReturnType<StackTraceImpl.StackTraceModel.TranslateRawFrames>>): void { if (!this.compilerMapping.translateRawFramesStep(rawFrames, translatedFrames)) { this.#defaultTranslateRawFramesStep(rawFrames, translatedFrames); } } /** The default implementation translates one frame at a time and only translates the location, but not the function name. */ #defaultTranslateRawFramesStep( rawFrames: StackTraceImpl.Trie.RawFrame[], translatedFrames: Awaited<ReturnType<StackTraceImpl.StackTraceModel.TranslateRawFrames>>): void { const frame = rawFrames.shift() as StackTraceImpl.Trie.RawFrame; const {scriptId, url, lineNumber, columnNumber, functionName} = frame; const rawLocation = scriptId ? this.#debuggerModel.createRawLocationByScriptId(scriptId, lineNumber, columnNumber) : url ? this.#debuggerModel.createRawLocationByURL(url, lineNumber, columnNumber) : null; if (rawLocation) { const uiLocation = this.rawLocationToUILocation(rawLocation); if (uiLocation) { translatedFrames.push([{ uiSourceCode: uiLocation.uiSourceCode, name: functionName, line: uiLocation.lineNumber, column: uiLocation.columnNumber ?? -1 }]); return; } } translatedFrames.push([{url, line: lineNumber, column: columnNumber, name: functionName}]); } getMappedLines(uiSourceCode: Workspace.UISourceCode.UISourceCode): Set<number>|null { const mappedLines = this.compilerMapping.getMappedLines(uiSourceCode); // TODO(crbug.com/1411431): The scripts from the ResourceMapping appear over time, // and there's currently no way to inform the UI to update. // mappedLines = mappedLines ?? this.#resourceMapping.getMappedLines(uiSourceCode); return mappedLines; } dispose(): void { this.#debuggerModel.setBeforePausedCallback(null); this.compilerMapping.dispose(); this.#resourceScriptMapping.dispose(); this.#defaultMapping.dispose(); } getResourceScriptMapping(): ResourceScriptMapping { return this.#resourceScriptMapping; } } export class Location extends LiveLocationWithPool { readonly scriptId: string; readonly rawLocation: SDK.DebuggerModel.Location; readonly #binding: DebuggerWorkspaceBinding; constructor( scriptId: string, rawLocation: SDK.DebuggerModel.Location, binding: DebuggerWorkspaceBinding, updateDelegate: (arg0: LiveLocation) => Promise<void>, locationPool: LiveLocationPool) { super(updateDelegate, locationPool); this.scriptId = scriptId; this.rawLocation = rawLocation; this.#binding = binding; } override async uiLocation(): Promise<Workspace.UISourceCode.UILocation|null> { const debuggerModelLocation = this.rawLocation; return await this.#binding.rawLocationToUILocation(debuggerModelLocation); } override dispose(): void { super.dispose(); this.#binding.removeLiveLocation(this); } } class StackTraceTopFrameLocation extends LiveLocationWithPool { #updateScheduled: boolean; #current: LiveLocation|null; #locations: LiveLocation[]|null; constructor(updateDelegate: (arg0: LiveLocation) => Promise<void>, locationPool: LiveLocationPool) { super(updateDelegate, locationPool); this.#updateScheduled = true; this.#current = null; this.#locations = null; } static async createStackTraceTopFrameLocation( rawLocations: SDK.DebuggerModel.Location[], binding: DebuggerWorkspaceBinding, updateDelegate: (arg0: LiveLocation) => Promise<void>, locationPool: LiveLocationPool): Promise<StackTraceTopFrameLocation> { const location = new StackTraceTopFrameLocation(updateDelegate, locationPool); const locationsPromises = rawLocations.map( rawLocation => binding.createLiveLocation(rawLocation, location.scheduleUpdate.bind(location), locationPool)); location.#locations = ((await Promise.all(locationsPromises)).filter(l => !!l)); await location.updateLocation(); return location; } override async uiLocation(): Promise<Workspace.UISourceCode.UILocation|null> { return this.#current ? await this.#current.uiLocation() : null; } override dispose(): void { super.dispose(); if (this.#locations) { for (const location of this.#locations) { location.dispose(); } } this.#locations = null; this.#current = null; } private async scheduleUpdate(): Promise<void> { if (this.#updateScheduled) { return; } this.#updateScheduled = true; queueMicrotask(() => { void this.updateLocation(); }); } private async updateLocation(): Promise<void> { this.#updateScheduled = false; if (!this.#locations || this.#locations.length === 0) { return; } this.#current = this.#locations[0]; for (const location of this.#locations) { const uiLocation = await location.uiLocation(); if (!uiLocation?.isIgnoreListed()) { this.#current = location; break; } } void this.update(); } } export interface DebuggerSourceMapping { rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location): Workspace.UISourceCode.UILocation|null; uiLocationToRawLocations( uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number, columnNumber?: number): SDK.DebuggerModel.Location[]; uiLocationRangeToRawLocationRanges( uiSourceCode: Workspace.UISourceCode.UISourceCode, textRange: TextUtils.TextRange.TextRange): SDK.DebuggerModel.LocationRange[]|null; }