| // Copyright 2011 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /* eslint-disable @typescript-eslint/no-explicit-any */ |
| |
| 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 TextUtils from '../text_utils/text_utils.js'; |
| |
| import {IgnoreListManager} from './IgnoreListManager.js'; |
| import {Events as WorkspaceImplEvents, type Project} from './WorkspaceImpl.js'; |
| |
| const UIStrings = { |
| /** |
| * @description Text for the index of something |
| */ |
| index: '(index)', |
| /** |
| * @description Text in UISource Code of the DevTools local workspace |
| */ |
| thisFileWasChangedExternally: 'This file was changed externally. Would you like to reload it?', |
| } as const; |
| const str_ = i18n.i18n.registerUIStrings('models/workspace/UISourceCode.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| export class UISourceCode extends Common.ObjectWrapper.ObjectWrapper<EventTypes> implements |
| TextUtils.ContentProvider.ContentProvider { |
| readonly #origin: Platform.DevToolsPath.UrlString; |
| readonly #parentURL: Platform.DevToolsPath.UrlString; |
| #project: Project; |
| #url: Platform.DevToolsPath.UrlString; |
| #name: string; |
| #contentType: Common.ResourceType.ResourceType; |
| #requestContentPromise: Promise<TextUtils.ContentData.ContentDataOrError>|null = null; |
| #decorations = new Map<string, any>(); |
| #hasCommits = false; |
| #messages: Set<Message>|null = null; |
| #content: TextUtils.ContentData.ContentDataOrError|null = null; |
| #forceLoadOnCheckContent = false; |
| #checkingContent = false; |
| #lastAcceptedContent: string|null = null; |
| #workingCopy: string|null = null; |
| #workingCopyGetter: (() => string)|null = null; |
| #disableEdit = false; |
| #contentEncoded: boolean|undefined; |
| #isKnownThirdParty = false; |
| #isUnconditionallyIgnoreListed = false; |
| #containsAiChanges = false; |
| |
| constructor(project: Project, url: Platform.DevToolsPath.UrlString, contentType: Common.ResourceType.ResourceType) { |
| super(); |
| this.#project = project; |
| this.#url = url; |
| |
| const parsedURL = Common.ParsedURL.ParsedURL.fromString(url); |
| if (parsedURL) { |
| this.#origin = parsedURL.securityOrigin(); |
| this.#parentURL = Common.ParsedURL.ParsedURL.concatenate(this.#origin, parsedURL.folderPathComponents); |
| if (parsedURL.queryParams && !(parsedURL.lastPathComponent && contentType.isFromSourceMap())) { |
| // If there is a query param, display it like a URL. Unless it is from a source map, |
| // in which case the query param is probably a hash that is best left hidden. |
| this.#name = parsedURL.lastPathComponent + '?' + parsedURL.queryParams; |
| } else { |
| // file name looks best decoded |
| try { |
| this.#name = decodeURIComponent(parsedURL.lastPathComponent); |
| } catch { |
| // Decoding might fail. |
| this.#name = parsedURL.lastPathComponent; |
| } |
| } |
| } else { |
| this.#origin = Platform.DevToolsPath.EmptyUrlString; |
| this.#parentURL = Platform.DevToolsPath.EmptyUrlString; |
| this.#name = url; |
| } |
| |
| this.#contentType = contentType; |
| } |
| |
| requestMetadata(): Promise<UISourceCodeMetadata|null> { |
| return this.#project.requestMetadata(this); |
| } |
| |
| name(): string { |
| return this.#name; |
| } |
| |
| mimeType(): string { |
| return this.#project.mimeType(this); |
| } |
| |
| url(): Platform.DevToolsPath.UrlString { |
| return this.#url; |
| } |
| |
| // Identifier used for deduplicating scripts that are considered by the |
| // DevTools UI to be the same script. For now this is just the url but this |
| // is likely to change in the future. |
| canonicalScriptId(): string { |
| return `${this.#contentType.name()},${this.#url}`; |
| } |
| |
| parentURL(): Platform.DevToolsPath.UrlString { |
| return this.#parentURL; |
| } |
| |
| origin(): Platform.DevToolsPath.UrlString { |
| return this.#origin; |
| } |
| |
| fullDisplayName(): string { |
| return this.#project.fullDisplayName(this); |
| } |
| |
| displayName(skipTrim?: boolean): string { |
| if (!this.#name) { |
| return i18nString(UIStrings.index); |
| } |
| const name = this.#name; |
| return skipTrim ? name : Platform.StringUtilities.trimEndWithMaxLength(name, 100); |
| } |
| |
| canRename(): boolean { |
| return this.#project.canRename(); |
| } |
| |
| rename(newName: Platform.DevToolsPath.RawPathString): Promise<boolean> { |
| const {resolve, promise} = Promise.withResolvers<boolean>(); |
| this.#project.rename(this, newName, innerCallback.bind(this)); |
| return promise; |
| |
| function innerCallback( |
| this: UISourceCode, success: boolean, newName?: string, newURL?: Platform.DevToolsPath.UrlString, |
| newContentType?: Common.ResourceType.ResourceType): void { |
| if (success) { |
| this.#updateName( |
| newName as Platform.DevToolsPath.RawPathString, newURL as Platform.DevToolsPath.UrlString, |
| newContentType as Common.ResourceType.ResourceType); |
| } |
| resolve(success); |
| } |
| } |
| |
| remove(): void { |
| this.#project.deleteFile(this); |
| } |
| |
| #updateName( |
| name: Platform.DevToolsPath.RawPathString, url: Platform.DevToolsPath.UrlString, |
| contentType?: Common.ResourceType.ResourceType): void { |
| const oldURL = this.#url; |
| this.#name = name; |
| if (url) { |
| this.#url = url; |
| } else { |
| this.#url = Common.ParsedURL.ParsedURL.relativePathToUrlString(name, oldURL); |
| } |
| if (contentType) { |
| this.#contentType = contentType; |
| } |
| this.dispatchEventToListeners(Events.TitleChanged, this); |
| this.project().workspace().dispatchEventToListeners( |
| WorkspaceImplEvents.UISourceCodeRenamed, {oldURL, uiSourceCode: this}); |
| } |
| |
| contentURL(): Platform.DevToolsPath.UrlString { |
| return this.url(); |
| } |
| |
| contentType(): Common.ResourceType.ResourceType { |
| return this.#contentType; |
| } |
| |
| project(): Project { |
| return this.#project; |
| } |
| |
| requestContentData({cachedWasmOnly}: {cachedWasmOnly?: boolean} = {}): |
| Promise<TextUtils.ContentData.ContentDataOrError> { |
| if (this.#requestContentPromise) { |
| return this.#requestContentPromise; |
| } |
| |
| if (this.#content) { |
| return Promise.resolve(this.#content); |
| } |
| |
| if (cachedWasmOnly && this.mimeType() === 'application/wasm') { |
| return Promise.resolve(new TextUtils.WasmDisassembly.WasmDisassembly([], [], [])); |
| } |
| |
| this.#requestContentPromise = this.#requestContent(); |
| return this.#requestContentPromise; |
| } |
| |
| async #requestContent(): Promise<TextUtils.ContentData.ContentDataOrError> { |
| if (this.#content) { |
| throw new Error('Called UISourceCode#requestContentImpl even though content is available for ' + this.#url); |
| } |
| |
| try { |
| this.#content = await this.#project.requestFileContent(this); |
| } catch (err) { |
| this.#content = {error: err ? String(err) : ''}; |
| } |
| |
| return this.#content; |
| } |
| |
| #decodeContent(content: TextUtils.ContentProvider.DeferredContent|null): string|null { |
| if (!content) { |
| return null; |
| } |
| return content.isEncoded && content.content ? window.atob(content.content) : content.content; |
| } |
| |
| /** Only used to compare whether content changed */ |
| #unsafeDecodeContentData(content: TextUtils.ContentData.ContentDataOrError|null): string|null { |
| if (!content || TextUtils.ContentData.ContentData.isError(content)) { |
| return null; |
| } |
| return content.createdFromBase64 ? window.atob(content.base64) : content.text; |
| } |
| |
| async checkContentUpdated(): Promise<void> { |
| if (!this.#content && !this.#forceLoadOnCheckContent) { |
| return; |
| } |
| |
| if (!this.#project.canSetFileContent() || this.#checkingContent) { |
| return; |
| } |
| |
| this.#checkingContent = true; |
| const updatedContent = |
| TextUtils.ContentData.ContentData.asDeferredContent(await this.#project.requestFileContent(this)); |
| if ('error' in updatedContent) { |
| return; |
| } |
| this.#checkingContent = false; |
| if (updatedContent.content === null) { |
| const workingCopy = this.workingCopy(); |
| this.#contentCommitted('', false); |
| this.setWorkingCopy(workingCopy); |
| return; |
| } |
| if (this.#lastAcceptedContent === updatedContent.content) { |
| return; |
| } |
| |
| if (this.#unsafeDecodeContentData(this.#content) === this.#decodeContent(updatedContent)) { |
| this.#lastAcceptedContent = null; |
| return; |
| } |
| |
| if (!this.isDirty() || this.#workingCopy === updatedContent.content) { |
| this.#contentCommitted(updatedContent.content, false); |
| return; |
| } |
| |
| await Common.Revealer.reveal(this); |
| |
| // Make sure we are in the next frame before stopping the world with confirm |
| await new Promise(resolve => window.setTimeout(resolve, 0)); |
| |
| const shouldUpdate = window.confirm(i18nString(UIStrings.thisFileWasChangedExternally)); |
| if (shouldUpdate) { |
| this.#contentCommitted(updatedContent.content, false); |
| } else { |
| this.#lastAcceptedContent = updatedContent.content; |
| } |
| } |
| |
| forceLoadOnCheckContent(): void { |
| this.#forceLoadOnCheckContent = true; |
| } |
| |
| #commitContent(content: string): void { |
| if (this.#project.canSetFileContent()) { |
| void this.#project.setFileContent(this, content, false); |
| } |
| this.#contentCommitted(content, true); |
| } |
| |
| #contentCommitted(content: string, committedByUser: boolean): void { |
| this.#lastAcceptedContent = null; |
| this.#content = new TextUtils.ContentData.ContentData(content, Boolean(this.#contentEncoded), this.mimeType()); |
| this.#requestContentPromise = null; |
| |
| this.#hasCommits = true; |
| |
| this.#resetWorkingCopy(); |
| const data = {uiSourceCode: this, content, encoded: this.#contentEncoded}; |
| this.dispatchEventToListeners(Events.WorkingCopyCommitted, data); |
| this.#project.workspace().dispatchEventToListeners(WorkspaceImplEvents.WorkingCopyCommitted, data); |
| if (committedByUser) { |
| this.#project.workspace().dispatchEventToListeners(WorkspaceImplEvents.WorkingCopyCommittedByUser, data); |
| } |
| } |
| |
| addRevision(content: string): void { |
| this.#commitContent(content); |
| } |
| |
| hasCommits(): boolean { |
| return this.#hasCommits; |
| } |
| |
| workingCopy(): string { |
| return this.workingCopyContent().content || ''; |
| } |
| |
| workingCopyContent(): TextUtils.ContentProvider.DeferredContent { |
| return this.workingCopyContentData().asDeferedContent(); |
| } |
| |
| workingCopyContentData(): TextUtils.ContentData.ContentData { |
| if (this.#workingCopyGetter) { |
| this.#workingCopy = this.#workingCopyGetter(); |
| this.#workingCopyGetter = null; |
| } |
| const contentData = this.#content ? TextUtils.ContentData.ContentData.contentDataOrEmpty(this.#content) : |
| TextUtils.ContentData.EMPTY_TEXT_CONTENT_DATA; |
| if (this.#workingCopy !== null) { |
| return new TextUtils.ContentData.ContentData(this.#workingCopy, /* isBase64 */ false, contentData.mimeType); |
| } |
| return contentData; |
| } |
| |
| resetWorkingCopy(): void { |
| this.#resetWorkingCopy(); |
| this.#workingCopyChanged(); |
| } |
| |
| #resetWorkingCopy(): void { |
| this.#workingCopy = null; |
| this.#workingCopyGetter = null; |
| this.setContainsAiChanges(false); |
| } |
| |
| setWorkingCopy(newWorkingCopy: string): void { |
| this.#workingCopy = newWorkingCopy; |
| this.#workingCopyGetter = null; |
| this.#workingCopyChanged(); |
| } |
| |
| setContainsAiChanges(containsAiChanges: boolean): void { |
| this.#containsAiChanges = containsAiChanges; |
| } |
| |
| containsAiChanges(): boolean { |
| return this.#containsAiChanges; |
| } |
| |
| setContent(content: string, isBase64: boolean): void { |
| this.#contentEncoded = isBase64; |
| if (this.#project.canSetFileContent()) { |
| void this.#project.setFileContent(this, content, isBase64); |
| } |
| this.#contentCommitted(content, true); |
| } |
| |
| setWorkingCopyGetter(workingCopyGetter: () => string): void { |
| this.#workingCopyGetter = workingCopyGetter; |
| this.#workingCopyChanged(); |
| } |
| |
| #workingCopyChanged(): void { |
| this.#removeAllMessages(); |
| this.dispatchEventToListeners(Events.WorkingCopyChanged, this); |
| this.#project.workspace().dispatchEventToListeners(WorkspaceImplEvents.WorkingCopyChanged, {uiSourceCode: this}); |
| } |
| |
| removeWorkingCopyGetter(): void { |
| if (!this.#workingCopyGetter) { |
| return; |
| } |
| this.#workingCopy = this.#workingCopyGetter(); |
| this.#workingCopyGetter = null; |
| } |
| |
| commitWorkingCopy(): void { |
| if (this.isDirty()) { |
| this.#commitContent(this.workingCopy()); |
| } |
| } |
| |
| isDirty(): boolean { |
| return this.#workingCopy !== null || this.#workingCopyGetter !== null; |
| } |
| |
| isKnownThirdParty(): boolean { |
| return this.#isKnownThirdParty; |
| } |
| |
| markKnownThirdParty(): void { |
| this.#isKnownThirdParty = true; |
| } |
| |
| /** |
| * {@link markAsUnconditionallyIgnoreListed} |
| */ |
| isUnconditionallyIgnoreListed(): boolean { |
| return this.#isUnconditionallyIgnoreListed; |
| } |
| |
| isFetchXHR(): boolean { |
| return [Common.ResourceType.resourceTypes.XHR, Common.ResourceType.resourceTypes.Fetch].includes( |
| this.contentType()); |
| } |
| |
| /** |
| * Unconditionally ignore list this UISourcecode, ignoring any user |
| * setting. We use this to mark breakpoint/logpoint condition scripts for now. |
| */ |
| markAsUnconditionallyIgnoreListed(): void { |
| this.#isUnconditionallyIgnoreListed = true; |
| } |
| |
| extension(): string { |
| return Common.ParsedURL.ParsedURL.extractExtension(this.#name); |
| } |
| |
| content(): string { |
| if (!this.#content || 'error' in this.#content) { |
| return ''; |
| } |
| return this.#content.text; |
| } |
| |
| loadError(): string|null { |
| return (this.#content && 'error' in this.#content && this.#content.error) || null; |
| } |
| |
| searchInContent(query: string, caseSensitive: boolean, isRegex: boolean): |
| Promise<TextUtils.ContentProvider.SearchMatch[]> { |
| if (!this.#content || 'error' in this.#content) { |
| return this.#project.searchInFileContent(this, query, caseSensitive, isRegex); |
| } |
| return Promise.resolve( |
| TextUtils.TextUtils.performSearchInContentData(this.#content, query, caseSensitive, isRegex)); |
| } |
| |
| contentLoaded(): boolean { |
| return Boolean(this.#content); |
| } |
| |
| uiLocation(lineNumber: number, columnNumber?: number): UILocation { |
| return new UILocation(this, lineNumber, columnNumber); |
| } |
| |
| messages(): Set<Message> { |
| return this.#messages ? new Set(this.#messages) : new Set(); |
| } |
| |
| addLineMessage( |
| level: Message.Level, text: string, lineNumber: number, columnNumber?: number, |
| clickHandler?: (() => void)): Message { |
| const range = TextUtils.TextRange.TextRange.createFromLocation(lineNumber, columnNumber || 0); |
| const message = new Message(level, text, clickHandler, range); |
| this.addMessage(message); |
| return message; |
| } |
| |
| addMessage(message: Message): void { |
| if (!this.#messages) { |
| this.#messages = new Set(); |
| } |
| this.#messages.add(message); |
| this.dispatchEventToListeners(Events.MessageAdded, message); |
| } |
| |
| removeMessage(message: Message): void { |
| if (this.#messages?.delete(message)) { |
| this.dispatchEventToListeners(Events.MessageRemoved, message); |
| } |
| } |
| |
| #removeAllMessages(): void { |
| if (!this.#messages) { |
| return; |
| } |
| for (const message of this.#messages) { |
| this.dispatchEventToListeners(Events.MessageRemoved, message); |
| } |
| this.#messages = null; |
| } |
| |
| setDecorationData(type: string, data: any): void { |
| if (data !== this.#decorations.get(type)) { |
| this.#decorations.set(type, data); |
| this.dispatchEventToListeners(Events.DecorationChanged, type); |
| } |
| } |
| |
| getDecorationData(type: string): any { |
| return this.#decorations.get(type); |
| } |
| |
| disableEdit(): void { |
| this.#disableEdit = true; |
| } |
| |
| editDisabled(): boolean { |
| return this.#disableEdit; |
| } |
| |
| isIgnoreListed(): boolean { |
| return IgnoreListManager.instance().isUserOrSourceMapIgnoreListedUISourceCode(this); |
| } |
| } |
| |
| export enum Events { |
| /* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */ |
| WorkingCopyChanged = 'WorkingCopyChanged', |
| WorkingCopyCommitted = 'WorkingCopyCommitted', |
| TitleChanged = 'TitleChanged', |
| MessageAdded = 'MessageAdded', |
| MessageRemoved = 'MessageRemoved', |
| DecorationChanged = 'DecorationChanged', |
| /* eslint-enable @typescript-eslint/naming-convention */ |
| } |
| |
| export interface WorkingCopyCommittedEvent { |
| uiSourceCode: UISourceCode; |
| content: string; |
| encoded: boolean|undefined; |
| } |
| |
| export interface EventTypes { |
| [Events.WorkingCopyChanged]: UISourceCode; |
| [Events.WorkingCopyCommitted]: WorkingCopyCommittedEvent; |
| [Events.TitleChanged]: UISourceCode; |
| [Events.MessageAdded]: Message; |
| [Events.MessageRemoved]: Message; |
| [Events.DecorationChanged]: string; |
| } |
| |
| export class UILocation { |
| uiSourceCode: UISourceCode; |
| lineNumber: number; |
| columnNumber: number|undefined; |
| constructor(uiSourceCode: UISourceCode, lineNumber: number, columnNumber?: number) { |
| this.uiSourceCode = uiSourceCode; |
| this.lineNumber = lineNumber; |
| this.columnNumber = columnNumber; |
| } |
| |
| linkText(skipTrim = false, showColumnNumber = false): string { |
| const displayName = this.uiSourceCode.displayName(skipTrim); |
| const lineAndColumnText = this.lineAndColumnText(showColumnNumber); |
| let text = lineAndColumnText ? displayName + ':' + lineAndColumnText : displayName; |
| if (this.uiSourceCode.isDirty()) { |
| text = '*' + text; |
| } |
| return text; |
| } |
| |
| lineAndColumnText(showColumnNumber = false): string|undefined { |
| let lineAndColumnText; |
| if (this.uiSourceCode.mimeType() === 'application/wasm') { |
| // For WebAssembly locations, we follow the conventions described in |
| // github.com/WebAssembly/design/blob/master/Web.md#developer-facing-display-conventions |
| if (typeof this.columnNumber === 'number') { |
| lineAndColumnText = `0x${this.columnNumber.toString(16)}`; |
| } |
| } else { |
| lineAndColumnText = `${this.lineNumber + 1}`; |
| if (showColumnNumber && typeof this.columnNumber === 'number') { |
| lineAndColumnText += ':' + (this.columnNumber + 1); |
| } |
| } |
| return lineAndColumnText; |
| } |
| |
| id(): string { |
| if (typeof this.columnNumber === 'number') { |
| return this.uiSourceCode.project().id() + ':' + this.uiSourceCode.url() + ':' + this.lineNumber + ':' + |
| this.columnNumber; |
| } |
| return this.lineId(); |
| } |
| |
| lineId(): string { |
| return this.uiSourceCode.project().id() + ':' + this.uiSourceCode.url() + ':' + this.lineNumber; |
| } |
| |
| static comparator(location1: UILocation, location2: UILocation): number { |
| return location1.compareTo(location2); |
| } |
| |
| compareTo(other: UILocation): number { |
| if (this.uiSourceCode.url() !== other.uiSourceCode.url()) { |
| return this.uiSourceCode.url() > other.uiSourceCode.url() ? 1 : -1; |
| } |
| if (this.lineNumber !== other.lineNumber) { |
| return this.lineNumber - other.lineNumber; |
| } |
| // We consider `undefined` less than an actual column number, since |
| // UI location without a column number corresponds to the whole line. |
| if (this.columnNumber === other.columnNumber) { |
| return 0; |
| } |
| if (typeof this.columnNumber !== 'number') { |
| return -1; |
| } |
| if (typeof other.columnNumber !== 'number') { |
| return 1; |
| } |
| return this.columnNumber - other.columnNumber; |
| } |
| |
| isIgnoreListed(): boolean { |
| return this.uiSourceCode.isIgnoreListed(); |
| } |
| } |
| |
| /** |
| * A text range inside a specific {@link UISourceCode}. |
| * |
| * We use a class instead of an interface so we can implement a revealer for it. |
| */ |
| export class UILocationRange { |
| readonly uiSourceCode: UISourceCode; |
| readonly range: TextUtils.TextRange.TextRange; |
| |
| constructor(uiSourceCode: UISourceCode, range: TextUtils.TextRange.TextRange) { |
| this.uiSourceCode = uiSourceCode; |
| this.range = range; |
| } |
| } |
| |
| /** |
| * A text range inside a specific {@link UISourceCode}, representing a function. |
| */ |
| export class UIFunctionBounds { |
| readonly uiSourceCode: UISourceCode; |
| readonly range: TextUtils.TextRange.TextRange; |
| readonly name: string; |
| |
| constructor(uiSourceCode: UISourceCode, range: TextUtils.TextRange.TextRange, name: string) { |
| this.uiSourceCode = uiSourceCode; |
| this.range = range; |
| this.name = name; |
| } |
| } |
| |
| /** |
| * A message associated with a range in a `UISourceCode`. The range will be |
| * underlined starting at the range's start and ending at the line end (the |
| * end of the range is currently disregarded). |
| * An icon is going to appear at the end of the line according to the |
| * `level` of the Message. This is only the model; displaying is handled |
| * where UISourceCode displaying is handled. |
| */ |
| export class Message { |
| readonly #level: Message.Level; |
| readonly #text: string; |
| range: TextUtils.TextRange.TextRange; |
| readonly #clickHandler?: (() => void); |
| |
| constructor(level: Message.Level, text: string, clickHandler?: (() => void), range?: TextUtils.TextRange.TextRange) { |
| this.#level = level; |
| this.#text = text; |
| this.range = range ?? new TextUtils.TextRange.TextRange(0, 0, 0, 0); |
| this.#clickHandler = clickHandler; |
| } |
| |
| level(): Message.Level { |
| return this.#level; |
| } |
| |
| text(): string { |
| return this.#text; |
| } |
| |
| clickHandler(): (() => void)|undefined { |
| return this.#clickHandler; |
| } |
| |
| lineNumber(): number { |
| return this.range.startLine; |
| } |
| |
| columnNumber(): number|undefined { |
| return this.range.startColumn; |
| } |
| |
| isEqual(another: Message): boolean { |
| return this.text() === another.text() && this.level() === another.level() && this.range.equal(another.range); |
| } |
| } |
| |
| export namespace Message { |
| export const enum Level { |
| ERROR = 'Error', |
| ISSUE = 'Issue', |
| WARNING = 'Warning', |
| } |
| } |
| |
| export class UISourceCodeMetadata { |
| modificationTime: Date|null; |
| contentSize: number|null; |
| |
| constructor(modificationTime: Date|null, contentSize: number|null) { |
| this.modificationTime = modificationTime; |
| this.contentSize = contentSize; |
| } |
| } |
| |
| export const enum DecoratorType { |
| PERFORMANCE = 'performance', |
| MEMORY = 'memory', |
| COVERAGE = 'coverage', |
| } |
| |
| /** 1-based. line => column => value */ |
| export type LineColumnProfileMap = Map<number, Map<number, number>>; |
| /** Used by ProfilePlugin to track runtime/memory costs. */ |
| export type ProfileDataMap = Map<UISourceCode, LineColumnProfileMap>; |
| |
| /** |
| * Converts an existing LineColumnProfileMap to a new one using the provided mapping. |
| * |
| * The input and output line/column of originalToMappedLocation is 0-indexed. |
| */ |
| export function createMappedProfileData( |
| profileData: LineColumnProfileMap, |
| originalToMappedLocation: (line: number, column: number) => number[] | null): LineColumnProfileMap { |
| const mappedProfileData: LineColumnProfileMap = new Map(); |
| for (const [lineNumber, columnData] of profileData) { |
| for (const [columnNumber, data] of columnData) { |
| const mappedLocation = originalToMappedLocation(lineNumber - 1, columnNumber - 1); |
| if (!mappedLocation) { |
| continue; |
| } |
| |
| const oneBasedFormattedLineNumber = mappedLocation[0] + 1; |
| const oneBasedFormattedColumnNumber = mappedLocation[1] + 1; |
| let mappedColumnData = mappedProfileData.get(oneBasedFormattedLineNumber); |
| if (!mappedColumnData) { |
| mappedColumnData = new Map(); |
| mappedProfileData.set(oneBasedFormattedLineNumber, mappedColumnData); |
| } |
| mappedColumnData.set( |
| oneBasedFormattedColumnNumber, (mappedColumnData.get(oneBasedFormattedColumnNumber) || 0) + data); |
| } |
| } |
| |
| return mappedProfileData; |
| } |