// Copyright 2012 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 i18n from '../../core/i18n/i18n.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 * as Formatter from '../formatter/formatter.js'; import * as TextUtils from '../text_utils/text_utils.js'; import * as Workspace from '../workspace/workspace.js'; import {ContentProviderBasedProject} from './ContentProviderBasedProject.js'; import {type DebuggerSourceMapping, DebuggerWorkspaceBinding} from './DebuggerWorkspaceBinding.js'; import {NetworkProject} from './NetworkProject.js'; import {metadataForURL} from './ResourceUtils.js'; const UIStrings = { /** * @description Error text displayed in the console when editing a live script fails. LiveEdit is *the name of the feature for editing code that is already running. * @example {warning} PH1 */ liveEditFailed: '`LiveEdit` failed: {PH1}', /** * @description Error text displayed in the console when compiling a live-edited script fails. LiveEdit is *the name of the feature for editing code that is already running. * @example {connection lost} PH1 */ liveEditCompileFailed: '`LiveEdit` compile failed: {PH1}', } as const; const str_ = i18n.i18n.registerUIStrings('models/bindings/ResourceScriptMapping.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export class ResourceScriptMapping implements DebuggerSourceMapping { readonly debuggerModel: SDK.DebuggerModel.DebuggerModel; #workspace: Workspace.Workspace.WorkspaceImpl; readonly debuggerWorkspaceBinding: DebuggerWorkspaceBinding; readonly #uiSourceCodeToScriptFile: Map; readonly #projects: Map; readonly #scriptToUISourceCode: Map; readonly #eventListeners: Common.EventTarget.EventDescriptor[]; constructor( debuggerModel: SDK.DebuggerModel.DebuggerModel, workspace: Workspace.Workspace.WorkspaceImpl, debuggerWorkspaceBinding: DebuggerWorkspaceBinding) { this.debuggerModel = debuggerModel; this.#workspace = workspace; this.debuggerWorkspaceBinding = debuggerWorkspaceBinding; this.#uiSourceCodeToScriptFile = new Map(); this.#projects = new Map(); this.#scriptToUISourceCode = new Map(); const runtimeModel = debuggerModel.runtimeModel(); this.#eventListeners = [ this.debuggerModel.addEventListener( SDK.DebuggerModel.Events.ParsedScriptSource, event => this.addScript(event.data), this), this.debuggerModel.addEventListener(SDK.DebuggerModel.Events.GlobalObjectCleared, this.globalObjectCleared, this), runtimeModel.addEventListener( SDK.RuntimeModel.Events.ExecutionContextDestroyed, this.executionContextDestroyed, this), runtimeModel.target().targetManager().addEventListener( SDK.TargetManager.Events.INSPECTED_URL_CHANGED, this.inspectedURLChanged, this), ]; } private project(script: SDK.Script.Script): ContentProviderBasedProject { const prefix = script.isContentScript() ? 'js:extensions:' : 'js::'; const projectId = prefix + this.debuggerModel.target().id() + ':' + script.frameId; let project = this.#projects.get(projectId); if (!project) { const projectType = script.isContentScript() ? Workspace.Workspace.projectTypes.ContentScripts : Workspace.Workspace.projectTypes.Network; project = new ContentProviderBasedProject( this.#workspace, projectId, projectType, '' /* displayName */, false /* isServiceProject */); NetworkProject.setTargetForProject(project, this.debuggerModel.target()); this.#projects.set(projectId, project); } return project; } uiSourceCodeForScript(script: SDK.Script.Script): Workspace.UISourceCode.UISourceCode|null { return this.#scriptToUISourceCode.get(script) ?? null; } rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location): Workspace.UISourceCode.UILocation|null { const script = rawLocation.script(); if (!script) { return null; } const uiSourceCode = this.#scriptToUISourceCode.get(script); if (!uiSourceCode) { return null; } const scriptFile = this.#uiSourceCodeToScriptFile.get(uiSourceCode); if (!scriptFile) { return null; } if ((scriptFile.hasDivergedFromVM() && !scriptFile.isMergingToVM()) || scriptFile.isDivergingFromVM()) { return null; } if (scriptFile.script !== script) { return null; } const {lineNumber, columnNumber = 0} = rawLocation; return uiSourceCode.uiLocation(lineNumber, columnNumber); } uiLocationToRawLocations(uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number, columnNumber: number): SDK.DebuggerModel.Location[] { const scriptFile = this.#uiSourceCodeToScriptFile.get(uiSourceCode); if (!scriptFile) { return []; } const {script} = scriptFile; if (!script) { return []; } return [this.debuggerModel.createRawLocation(script, lineNumber, columnNumber)]; } uiLocationRangeToRawLocationRanges( uiSourceCode: Workspace.UISourceCode.UISourceCode, {startLine, startColumn, endLine, endColumn}: TextUtils.TextRange.TextRange): SDK.DebuggerModel.LocationRange[]|null { const scriptFile = this.#uiSourceCodeToScriptFile.get(uiSourceCode); if (!scriptFile) { return null; } const {script} = scriptFile; if (!script) { return null; } const start = this.debuggerModel.createRawLocation(script, startLine, startColumn); const end = this.debuggerModel.createRawLocation(script, endLine, endColumn); return [{start, end}]; } private inspectedURLChanged(event: Common.EventTarget.EventTargetEvent): void { for (let target: SDK.Target.Target|null = this.debuggerModel.target(); target !== event.data; target = target.parentTarget()) { if (target === null) { return; } } // Just remove and readd all scripts to ensure their URLs are reflected correctly. for (const script of Array.from(this.#scriptToUISourceCode.keys())) { this.removeScripts([script]); this.addScript(script); } } async functionBoundsAtRawLocation(rawLocation: SDK.DebuggerModel.Location): Promise { const script = rawLocation.script(); if (!script) { return null; } const uiSourceCode = this.#scriptToUISourceCode.get(script); if (!uiSourceCode) { return null; } const scopeTreeAndText = script ? await SDK.ScopeTreeCache.scopeTreeForScript(script) : null; if (!scopeTreeAndText) { return null; } // Find the inner-most scope that maps to the given position. const offset = scopeTreeAndText.text.offsetFromPosition(rawLocation.lineNumber, rawLocation.columnNumber); const results = []; (function walk(nodes: Formatter.FormatterWorkerPool.ScopeTreeNode[]) { for (const node of nodes) { if (!(offset >= node.start && offset < node.end)) { continue; } results.push(node); walk(node.children); } })([scopeTreeAndText.scopeTree]); const result = results.findLast( node => node.kind === Formatter.FormatterWorkerPool.ScopeKind.FUNCTION || node.kind === Formatter.FormatterWorkerPool.ScopeKind.ARROW_FUNCTION); if (!result) { return null; } // Map back to positions. const startPosition = scopeTreeAndText.text.positionFromOffset(result.start); const endPosition = scopeTreeAndText.text.positionFromOffset(result.end); const name = ''; // TODO(crbug.com/452333154): update ScopeVariableAnalysis to include function name. const range = new TextUtils.TextRange.TextRange( startPosition.lineNumber, startPosition.columnNumber, endPosition.lineNumber, endPosition.columnNumber); return new Workspace.UISourceCode.UIFunctionBounds(uiSourceCode, range, name); } private addScript(script: SDK.Script.Script): void { // Ignore live edit scripts here. if (script.isLiveEdit() || script.isBreakpointCondition) { return; } let url = script.sourceURL; if (!url) { return; } if (script.hasSourceURL) { // Try to resolve `//# sourceURL=` annotations relative to // the base URL, according to the sourcemap specification. url = SDK.SourceMapManager.SourceMapManager.resolveRelativeSourceURL(script.debuggerModel.target(), url); } else { // Ignore inline