// 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 { readonly resourceMapping: ResourceMapping; readonly #debuggerModelToData: Map; readonly #liveLocationPromises: Set; 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.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 { 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 { await Promise.all(this.#liveLocationPromises); } private recordLiveLocationChange(promise: Promise): void { void promise.then(() => { this.#liveLocationPromises.delete(promise); }); this.#liveLocationPromises.add(promise); } async updateLocations(script: SDK.Script.Script): Promise { const stackTraceUpdatePromise = script.target() .model(StackTraceImpl.StackTraceModel.StackTraceModel) ?.scriptInfoChanged(script, this.#translateRawFrames.bind(this)); if (stackTraceUpdatePromise) { this.recordLiveLocationChange(stackTraceUpdatePromise); } const updatePromises = [stackTraceUpdatePromise]; 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 { const model = target.model(StackTraceImpl.StackTraceModel.StackTraceModel) as StackTraceImpl.StackTraceModel.StackTraceModel; const stackTracePromise = model.createFromProtocolRuntime(stackTrace, this.#translateRawFrames.bind(this)); this.recordLiveLocationChange(stackTracePromise); return await stackTracePromise; } async createStackTraceFromDebuggerPaused( pausedDetails: SDK.DebuggerModel.DebuggerPausedDetails, target: SDK.Target.Target): Promise { const model = target.model(StackTraceImpl.StackTraceModel.StackTraceModel) as StackTraceImpl.StackTraceModel.StackTraceModel; const stackTracePromise = model.createFromDebuggerPaused(pausedDetails, this.#translateRawFrames.bind(this)); this.recordLiveLocationChange(stackTracePromise); return await stackTracePromise; } async createLiveLocation( rawLocation: SDK.DebuggerModel.Location, updateDelegate: (arg0: LiveLocation) => Promise, locationPool: LiveLocationPool): Promise { 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, locationPool: LiveLocationPool): Promise { console.assert(rawLocations.length > 0); const locationPromise = StackTraceTopFrameLocation.createStackTraceTopFrameLocation(rawLocations, this, updateDelegate, locationPool); this.recordLiveLocationChange(locationPromise); return await locationPromise; } async rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location): Promise { 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 { 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 { 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 { 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 { 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 `