| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import type * as Platform from '../../core/platform/platform.js'; |
| import * as SDK from '../../core/sdk/sdk.js'; |
| import * as Bindings from '../bindings/bindings.js'; |
| import * as Formatter from '../formatter/formatter.js'; |
| import * as TextUtils from '../text_utils/text_utils.js'; |
| import * as Workspace from '../workspace/workspace.js'; |
| |
| /** Represents the source code for a given function, including additional context of surrounding lines. */ |
| export interface FunctionCode { |
| functionBounds: Workspace.UISourceCode.UIFunctionBounds; |
| /** The text of `uiSourceCode`. */ |
| text: TextUtils.Text.Text; |
| /** The function text. */ |
| code: string; |
| /** The range of `code` within `text`. */ |
| range: TextUtils.TextRange.TextRange; |
| /** The function text, plus some additional context before and after. The actual function is wrapped in <FUNCTION_START>...<FUNCTION_END> */ |
| codeWithContext: string; |
| /** The range of `codeWithContext` within `text`. */ |
| rangeWithContext: TextUtils.TextRange.TextRange; |
| } |
| |
| export interface CreateFunctionCodeOptions { |
| /** Number of characters to include before and after the function. Stacks with `contextLineLength`. */ |
| contextLength?: number; |
| /** Number of lines to include before and after the function. Stacks with `contextLength`. */ |
| contextLineLength?: number; |
| /** If true, appends profile data from the trace at the end of every line of the function in `codeWithContext`. This should match what is seen in the formatted view in the Sources panel. */ |
| appendProfileData?: boolean; |
| } |
| |
| interface InputData { |
| text: TextUtils.Text.Text; |
| formattedContent: Formatter.ScriptFormatter.FormattedContent|null; |
| performanceData: Workspace.UISourceCode.LineColumnProfileMap|undefined; |
| } |
| |
| const inputCache = new WeakMap<Workspace.UISourceCode.UISourceCode, Promise<InputData>>(); |
| |
| async function prepareInput(uiSourceCode: Workspace.UISourceCode.UISourceCode, content: string): Promise<InputData> { |
| const formattedContent = await format(uiSourceCode, content); |
| const text = new TextUtils.Text.Text(formattedContent ? formattedContent.formattedContent : content); |
| let performanceData = uiSourceCode.getDecorationData(Workspace.UISourceCode.DecoratorType.PERFORMANCE) as |
| Workspace.UISourceCode.LineColumnProfileMap | |
| undefined; |
| |
| // Map profile data to the formatted view of the text. |
| if (formattedContent && performanceData) { |
| performanceData = Workspace.UISourceCode.createMappedProfileData(performanceData, (line, column) => { |
| return formattedContent.formattedMapping.originalToFormatted(line, column); |
| }); |
| } |
| |
| return {text, formattedContent, performanceData}; |
| } |
| |
| /** Formatting and parsing line endings for Text is expensive, so cache it. */ |
| async function prepareInputAndCache( |
| uiSourceCode: Workspace.UISourceCode.UISourceCode, content: string): Promise<InputData> { |
| let cachedPromise = inputCache.get(uiSourceCode); |
| if (cachedPromise) { |
| return await cachedPromise; |
| } |
| |
| cachedPromise = prepareInput(uiSourceCode, content); |
| inputCache.set(uiSourceCode, cachedPromise); |
| return await cachedPromise; |
| } |
| |
| function extractPerformanceDataByLine( |
| textRange: TextUtils.TextRange.TextRange, performanceData: Workspace.UISourceCode.LineColumnProfileMap): number[] { |
| const {startLine, startColumn, endLine, endColumn} = textRange; |
| const byLine = new Array(endLine - startLine + 1).fill(0); |
| |
| for (let line = startLine; line <= endLine; line++) { |
| const lineData = performanceData.get(line + 1); |
| if (!lineData) { |
| continue; |
| } |
| |
| // Fast-path for when the entire line's data is relevant. |
| if (line !== startLine && line !== endLine) { |
| byLine[line - startLine] = lineData.values().reduce((acc, cur) => acc + cur); |
| continue; |
| } |
| |
| const column0 = line === startLine ? startColumn + 1 : 0; |
| const column1 = line === endLine ? endColumn + 1 : Number.POSITIVE_INFINITY; |
| |
| let totalData = 0; |
| for (const [column, data] of lineData) { |
| if (column >= column0 && column <= column1) { |
| totalData += data; |
| } |
| } |
| |
| byLine[line - startLine] = totalData; |
| } |
| |
| return byLine.map(data => Math.round(data * 10) / 10); |
| } |
| |
| function createFunctionCode( |
| inputData: InputData, functionBounds: Workspace.UISourceCode.UIFunctionBounds, |
| options?: CreateFunctionCodeOptions): FunctionCode { |
| let {startLine, startColumn, endLine, endColumn} = functionBounds.range; |
| if (inputData.formattedContent) { |
| const startMapped = inputData.formattedContent.formattedMapping.originalToFormatted(startLine, startColumn); |
| startLine = startMapped[0]; |
| startColumn = startMapped[1]; |
| |
| const endMapped = inputData.formattedContent.formattedMapping.originalToFormatted(endLine, endColumn); |
| endLine = endMapped[0]; |
| endColumn = endMapped[1]; |
| } |
| |
| const text = inputData.text; |
| const content = text.value(); |
| |
| // Define two ranges - the first is just the function bounds, the second includes |
| // that plus some surrounding context as dictated by the options. |
| const range = new TextUtils.TextRange.TextRange(startLine, startColumn, endLine, endColumn); |
| |
| const functionStartOffset = text.offsetFromPosition(startLine, startColumn); |
| const functionEndOffset = text.offsetFromPosition(endLine, endColumn); |
| |
| let contextStartOffset = 0; |
| if (options?.contextLength !== undefined) { |
| const contextLength = options.contextLength; |
| contextStartOffset = Math.max(contextStartOffset, functionStartOffset - contextLength); |
| } |
| if (options?.contextLineLength !== undefined) { |
| const contextLineLength = options.contextLineLength; |
| const position = text.offsetFromPosition(Math.max(startLine - contextLineLength, 0), 0); |
| contextStartOffset = Math.max(contextStartOffset, position); |
| } |
| |
| let contextEndOffset = content.length; |
| if (options?.contextLength !== undefined) { |
| const contextLength = options.contextLength; |
| contextEndOffset = Math.min(contextEndOffset, functionEndOffset + contextLength); |
| } |
| if (options?.contextLineLength !== undefined) { |
| const contextLineLength = options.contextLineLength; |
| const position = |
| text.offsetFromPosition(Math.min(endLine + contextLineLength, text.lineCount() - 1), Number.POSITIVE_INFINITY); |
| contextEndOffset = Math.min(contextEndOffset, position); |
| } |
| |
| const contextStart = text.positionFromOffset(contextStartOffset); |
| const contextEnd = text.positionFromOffset(contextEndOffset); |
| const rangeWithContext = new TextUtils.TextRange.TextRange( |
| contextStart.lineNumber, contextStart.columnNumber, contextEnd.lineNumber, contextEnd.columnNumber); |
| |
| // Grab substrings for the function range, and for the context range. |
| const code = content.substring(functionStartOffset, functionEndOffset); |
| const before = content.substring(contextStartOffset, functionStartOffset); |
| const after = content.substring(functionEndOffset, contextEndOffset); |
| |
| let codeWithContext; |
| if (options?.appendProfileData && inputData.performanceData) { |
| const performanceDataByLine = extractPerformanceDataByLine(range, inputData.performanceData); |
| const lines = performanceDataByLine.map((data, i) => { |
| let line = text.lineAt(startLine + i); |
| |
| const isLastLine = i === performanceDataByLine.length - 1; |
| if (i === 0) { |
| if (isLastLine) { |
| line = line.substring(startColumn, endColumn); |
| } else { |
| line = line.substring(startColumn); |
| } |
| } else if (isLastLine) { |
| line = line.substring(0, endColumn); |
| } |
| |
| if (isLastLine) { |
| // Don't ever annotate the last line - it could make the rest of the code on |
| // that line get commented out. |
| data = 0; |
| } |
| |
| return data ? `${line} // ${data} ms` : line; |
| }); |
| const annotatedCode = lines.join('\n'); |
| codeWithContext = before + `<FUNCTION_START>${annotatedCode}<FUNCTION_END>` + after; |
| } else { |
| codeWithContext = before + `<FUNCTION_START>${code}<FUNCTION_END>` + after; |
| } |
| |
| return { |
| functionBounds, |
| text, |
| code, |
| range, |
| codeWithContext, |
| rangeWithContext, |
| }; |
| } |
| |
| /** |
| * The input location may be a source mapped location or a raw location. |
| */ |
| export async function getFunctionCodeFromLocation( |
| target: SDK.Target.Target, url: Platform.DevToolsPath.UrlString, line: number, column: number, |
| options?: CreateFunctionCodeOptions): Promise<FunctionCode|null> { |
| const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); |
| if (!debuggerModel) { |
| throw new Error('missing debugger model'); |
| } |
| |
| let uiSourceCode; |
| const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance(); |
| const projects = debuggerWorkspaceBinding.workspace.projectsForType(Workspace.Workspace.projectTypes.Network); |
| for (const project of projects) { |
| uiSourceCode = project.uiSourceCodeForURL(url); |
| if (uiSourceCode) { |
| break; |
| } |
| } |
| |
| if (!uiSourceCode) { |
| return null; |
| } |
| |
| const rawLocations = await debuggerWorkspaceBinding.uiLocationToRawLocations(uiSourceCode, line, column); |
| const rawLocation = rawLocations.at(-1); |
| if (!rawLocation) { |
| return null; |
| } |
| |
| return await getFunctionCodeFromRawLocation(rawLocation, options); |
| } |
| |
| async function format(uiSourceCode: Workspace.UISourceCode.UISourceCode, content: string): |
| Promise<Formatter.ScriptFormatter.FormattedContent|null> { |
| const contentType = uiSourceCode.contentType(); |
| const shouldFormat = !contentType.isFromSourceMap() && (contentType.isDocument() || contentType.isScript()) && |
| TextUtils.TextUtils.isMinified(content); |
| if (!shouldFormat) { |
| return null; |
| } |
| |
| return await Formatter.ScriptFormatter.formatScriptContent(contentType.canonicalMimeType(), content, '\t'); |
| } |
| |
| /** |
| * Returns a {@link FunctionCode} for the given raw location. |
| */ |
| export async function getFunctionCodeFromRawLocation( |
| rawLocation: SDK.DebuggerModel.Location, options?: CreateFunctionCodeOptions): Promise<FunctionCode|null> { |
| const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance(); |
| const functionBounds = await debuggerWorkspaceBinding.functionBoundsAtRawLocation(rawLocation); |
| if (!functionBounds) { |
| return null; |
| } |
| |
| await functionBounds.uiSourceCode.requestContentData(); |
| const content = functionBounds.uiSourceCode.content(); |
| if (!content) { |
| return null; |
| } |
| |
| const inputData = await prepareInputAndCache(functionBounds.uiSourceCode, content); |
| return createFunctionCode(inputData, functionBounds, options); |
| } |