| // Copyright 2017 The Chromium Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| Sources.SourceFormatData = class { | |
| /** | |
| * @param {!Workspace.UISourceCode} originalSourceCode | |
| * @param {!Workspace.UISourceCode} formattedSourceCode | |
| * @param {!Formatter.FormatterSourceMapping} mapping | |
| */ | |
| constructor(originalSourceCode, formattedSourceCode, mapping) { | |
| this.originalSourceCode = originalSourceCode; | |
| this.formattedSourceCode = formattedSourceCode; | |
| this.mapping = mapping; | |
| } | |
| originalPath() { | |
| return this.originalSourceCode.project().id() + ':' + this.originalSourceCode.url(); | |
| } | |
| /** | |
| * @param {!Object} object | |
| * @return {?Sources.SourceFormatData} | |
| */ | |
| static _for(object) { | |
| return object[Sources.SourceFormatData._formatDataSymbol]; | |
| } | |
| }; | |
| Sources.SourceFormatData._formatDataSymbol = Symbol('formatData'); | |
| Sources.SourceFormatter = class { | |
| constructor() { | |
| this._projectId = 'formatter:'; | |
| this._project = new Bindings.ContentProviderBasedProject( | |
| Workspace.workspace, this._projectId, Workspace.projectTypes.Formatter, 'formatter', | |
| true /* isServiceProject */); | |
| /** @type {!Map<!Workspace.UISourceCode, !{promise: !Promise<!Sources.SourceFormatData>, formatData: ?Sources.SourceFormatData}>} */ | |
| this._formattedSourceCodes = new Map(); | |
| this._scriptMapping = new Sources.SourceFormatter.ScriptMapping(); | |
| this._styleMapping = new Sources.SourceFormatter.StyleMapping(); | |
| Workspace.workspace.addEventListener( | |
| Workspace.Workspace.Events.UISourceCodeRemoved, this._onUISourceCodeRemoved, this); | |
| } | |
| /** | |
| * @param {!Common.Event} event | |
| */ | |
| _onUISourceCodeRemoved(event) { | |
| const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data); | |
| const cacheEntry = this._formattedSourceCodes.get(uiSourceCode); | |
| if (cacheEntry && cacheEntry.formatData) { | |
| this._discardFormatData(cacheEntry.formatData); | |
| } | |
| this._formattedSourceCodes.remove(uiSourceCode); | |
| } | |
| /** | |
| * @param {!Workspace.UISourceCode} formattedUISourceCode | |
| * @return {?Workspace.UISourceCode} | |
| */ | |
| discardFormattedUISourceCode(formattedUISourceCode) { | |
| const formatData = Sources.SourceFormatData._for(formattedUISourceCode); | |
| if (!formatData) { | |
| return null; | |
| } | |
| this._discardFormatData(formatData); | |
| this._formattedSourceCodes.remove(formatData.originalSourceCode); | |
| return formatData.originalSourceCode; | |
| } | |
| /** | |
| * @param {!Sources.SourceFormatData} formatData | |
| */ | |
| _discardFormatData(formatData) { | |
| delete formatData.formattedSourceCode[Sources.SourceFormatData._formatDataSymbol]; | |
| this._scriptMapping._setSourceMappingEnabled(formatData, false); | |
| this._styleMapping._setSourceMappingEnabled(formatData, false); | |
| this._project.removeFile(formatData.formattedSourceCode.url()); | |
| } | |
| /** | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| * @return {boolean} | |
| */ | |
| hasFormatted(uiSourceCode) { | |
| return this._formattedSourceCodes.has(uiSourceCode); | |
| } | |
| /** | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| * @return {!Workspace.UISourceCode} | |
| */ | |
| getOriginalUISourceCode(uiSourceCode) { | |
| const formatData = | |
| /** @type {?Sources.SourceFormatData} */ (uiSourceCode[Sources.SourceFormatData._formatDataSymbol]); | |
| if (!formatData) { | |
| return uiSourceCode; | |
| } | |
| return formatData.originalSourceCode; | |
| } | |
| /** | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| * @return {!Promise<!Sources.SourceFormatData>} | |
| */ | |
| async format(uiSourceCode) { | |
| const cacheEntry = this._formattedSourceCodes.get(uiSourceCode); | |
| if (cacheEntry) { | |
| return cacheEntry.promise; | |
| } | |
| let fulfillFormatPromise; | |
| const resultPromise = new Promise(fulfill => { | |
| fulfillFormatPromise = fulfill; | |
| }); | |
| this._formattedSourceCodes.set(uiSourceCode, {promise: resultPromise, formatData: null}); | |
| const {content} = await uiSourceCode.requestContent(); | |
| // ------------ ASYNC ------------ | |
| Formatter.Formatter.format( | |
| uiSourceCode.contentType(), uiSourceCode.mimeType(), content || '', formatDone.bind(this)); | |
| return resultPromise; | |
| /** | |
| * @this Sources.SourceFormatter | |
| * @param {string} formattedContent | |
| * @param {!Formatter.FormatterSourceMapping} formatterMapping | |
| */ | |
| function formatDone(formattedContent, formatterMapping) { | |
| const cacheEntry = this._formattedSourceCodes.get(uiSourceCode); | |
| if (!cacheEntry || cacheEntry.promise !== resultPromise) { | |
| return; | |
| } | |
| let formattedURL; | |
| let count = 0; | |
| let suffix = ''; | |
| do { | |
| formattedURL = `${uiSourceCode.url()}:formatted${suffix}`; | |
| suffix = `:${count++}`; | |
| } while (this._project.uiSourceCodeForURL(formattedURL)); | |
| const contentProvider = | |
| Common.StaticContentProvider.fromString(formattedURL, uiSourceCode.contentType(), formattedContent); | |
| const formattedUISourceCode = | |
| this._project.addContentProvider(formattedURL, contentProvider, uiSourceCode.mimeType()); | |
| const formatData = new Sources.SourceFormatData(uiSourceCode, formattedUISourceCode, formatterMapping); | |
| formattedUISourceCode[Sources.SourceFormatData._formatDataSymbol] = formatData; | |
| this._scriptMapping._setSourceMappingEnabled(formatData, true); | |
| this._styleMapping._setSourceMappingEnabled(formatData, true); | |
| cacheEntry.formatData = formatData; | |
| for (const decoration of uiSourceCode.allDecorations()) { | |
| const range = decoration.range(); | |
| const startLocation = formatterMapping.originalToFormatted(range.startLine, range.startColumn); | |
| const endLocation = formatterMapping.originalToFormatted(range.endLine, range.endColumn); | |
| formattedUISourceCode.addDecoration( | |
| new TextUtils.TextRange(startLocation[0], startLocation[1], endLocation[0], endLocation[1]), | |
| /** @type {string} */ (decoration.type()), decoration.data()); | |
| } | |
| fulfillFormatPromise(formatData); | |
| } | |
| } | |
| }; | |
| /** | |
| * @implements {Bindings.DebuggerSourceMapping} | |
| */ | |
| Sources.SourceFormatter.ScriptMapping = class { | |
| constructor() { | |
| Bindings.debuggerWorkspaceBinding.addSourceMapping(this); | |
| } | |
| /** | |
| * @override | |
| * @param {!SDK.DebuggerModel.Location} rawLocation | |
| * @return {?Workspace.UILocation} | |
| */ | |
| rawLocationToUILocation(rawLocation) { | |
| const script = rawLocation.script(); | |
| const formatData = script && Sources.SourceFormatData._for(script); | |
| if (!formatData) { | |
| return null; | |
| } | |
| if (script.isInlineScriptWithSourceURL()) { | |
| // Inline scripts with #sourceURL= have lineEndings wrt. the inline script (and not wrt. the containing document), | |
| // but `rawLocation` will always use locations wrt. the containing document, because that is what the back-end is | |
| // sending. This is a hack, because what we are really doing here is deciding the location based on /how/ the | |
| // script is displayed, which is really something this layer cannot and should not have to decide: The | |
| // SourceFormatter should not have to know wether a script is displayed inline (in its containing document) or | |
| // stand-alone. | |
| const [relativeLineNumber, relativeColumnNumber] = script.toRelativeLocation(rawLocation); | |
| const [formattedLineNumber, formattedColumnNumber] = | |
| formatData.mapping.originalToFormatted(relativeLineNumber, relativeColumnNumber); | |
| return formatData.formattedSourceCode.uiLocation(formattedLineNumber, formattedColumnNumber); | |
| } | |
| // Here we either have an inline script without a #sourceURL= or a stand-alone script. For stand-alone scripts, no | |
| // translation must be applied. For inline scripts, also no translation must be applied, because the line-endings | |
| // tables in the mapping are the same as in the containing document. | |
| const [lineNumber, columnNumber] = | |
| formatData.mapping.originalToFormatted(rawLocation.lineNumber, rawLocation.columnNumber || 0); | |
| return formatData.formattedSourceCode.uiLocation(lineNumber, columnNumber); | |
| } | |
| /** | |
| * @override | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| * @param {number} lineNumber | |
| * @param {number} columnNumber | |
| * @return {!Array<!SDK.DebuggerModel.Location>} | |
| */ | |
| uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber) { | |
| const formatData = Sources.SourceFormatData._for(uiSourceCode); | |
| if (!formatData) { | |
| return []; | |
| } | |
| const [originalLine, originalColumn] = formatData.mapping.formattedToOriginal(lineNumber, columnNumber); | |
| if (formatData.originalSourceCode.contentType().isScript()) { | |
| // Here we have a script that is displayed on its own (i.e. it has a dedicated uiSourceCode). This means it is | |
| // either a stand-alone script or an inline script with a #sourceURL= and in both cases we can just forward the | |
| // question to the original (unformatted) source code. | |
| const rawLocations = Bindings.debuggerWorkspaceBinding.uiLocationToRawLocations( | |
| formatData.originalSourceCode, originalLine, originalColumn); | |
| console.assert(rawLocations.every(l => l && !!l.script())); | |
| return rawLocations; | |
| } | |
| if (formatData.originalSourceCode.contentType() === Common.resourceTypes.Document) { | |
| const target = Bindings.NetworkProject.targetForUISourceCode(formatData.originalSourceCode); | |
| const debuggerModel = target && target.model(SDK.DebuggerModel); | |
| if (debuggerModel) { | |
| const scripts = debuggerModel.scriptsForSourceURL(formatData.originalSourceCode.url()) | |
| .filter(script => script.isInlineScript() && !script.hasSourceURL); | |
| // Here we have an inline script, which was formatted together with the containing document, so we must not | |
| // translate locations as they are relative to the start of the document. | |
| const locations = scripts.map(script => script.rawLocation(originalLine, originalColumn)).filter(l => !!l); | |
| console.assert(locations.every(l => l && !!l.script())); | |
| return locations; | |
| } | |
| } | |
| return []; | |
| } | |
| /** | |
| * @param {!Sources.SourceFormatData} formatData | |
| * @param {boolean} enabled | |
| */ | |
| _setSourceMappingEnabled(formatData, enabled) { | |
| const scripts = this._scriptsForUISourceCode(formatData.originalSourceCode); | |
| if (!scripts.length) { | |
| return; | |
| } | |
| if (enabled) { | |
| for (const script of scripts) { | |
| script[Sources.SourceFormatData._formatDataSymbol] = formatData; | |
| } | |
| } else { | |
| for (const script of scripts) { | |
| delete script[Sources.SourceFormatData._formatDataSymbol]; | |
| } | |
| } | |
| for (const script of scripts) { | |
| Bindings.debuggerWorkspaceBinding.updateLocations(script); | |
| } | |
| } | |
| /** | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| * @return {!Array<!SDK.Script>} | |
| */ | |
| _scriptsForUISourceCode(uiSourceCode) { | |
| if (uiSourceCode.contentType() === Common.resourceTypes.Document) { | |
| const target = Bindings.NetworkProject.targetForUISourceCode(uiSourceCode); | |
| const debuggerModel = target && target.model(SDK.DebuggerModel); | |
| if (debuggerModel) { | |
| const scripts = debuggerModel.scriptsForSourceURL(uiSourceCode.url()) | |
| .filter(script => script.isInlineScript() && !script.hasSourceURL); | |
| return scripts; | |
| } | |
| } | |
| if (uiSourceCode.contentType().isScript()) { | |
| const rawLocations = Bindings.debuggerWorkspaceBinding.uiLocationToRawLocations(uiSourceCode, 0, 0); | |
| return rawLocations.map(location => location.script()).filter(script => !!script); | |
| } | |
| return []; | |
| } | |
| }; | |
| /** | |
| * @implements {Bindings.CSSWorkspaceBinding.SourceMapping} | |
| */ | |
| Sources.SourceFormatter.StyleMapping = class { | |
| constructor() { | |
| Bindings.cssWorkspaceBinding.addSourceMapping(this); | |
| this._headersSymbol = Symbol('Sources.SourceFormatter.StyleMapping._headersSymbol'); | |
| } | |
| /** | |
| * @override | |
| * @param {!SDK.CSSLocation} rawLocation | |
| * @return {?Workspace.UILocation} | |
| */ | |
| rawLocationToUILocation(rawLocation) { | |
| const styleHeader = rawLocation.header(); | |
| const formatData = styleHeader && Sources.SourceFormatData._for(styleHeader); | |
| if (!formatData) { | |
| return null; | |
| } | |
| const formattedLocation = | |
| formatData.mapping.originalToFormatted(rawLocation.lineNumber, rawLocation.columnNumber || 0); | |
| return formatData.formattedSourceCode.uiLocation(formattedLocation[0], formattedLocation[1]); | |
| } | |
| /** | |
| * @override | |
| * @param {!Workspace.UILocation} uiLocation | |
| * @return {!Array<!SDK.CSSLocation>} | |
| */ | |
| uiLocationToRawLocations(uiLocation) { | |
| const formatData = Sources.SourceFormatData._for(uiLocation.uiSourceCode); | |
| if (!formatData) { | |
| return []; | |
| } | |
| const [originalLine, originalColumn] = | |
| formatData.mapping.formattedToOriginal(uiLocation.lineNumber, uiLocation.columnNumber); | |
| const headers = formatData.originalSourceCode[this._headersSymbol].filter( | |
| header => header.containsLocation(originalLine, originalColumn)); | |
| return headers.map(header => new SDK.CSSLocation(header, originalLine, originalColumn)); | |
| } | |
| /** | |
| * @param {!Sources.SourceFormatData} formatData | |
| * @param {boolean} enable | |
| */ | |
| _setSourceMappingEnabled(formatData, enable) { | |
| const original = formatData.originalSourceCode; | |
| const headers = this._headersForUISourceCode(original); | |
| if (enable) { | |
| original[this._headersSymbol] = headers; | |
| headers.forEach(header => header[Sources.SourceFormatData._formatDataSymbol] = formatData); | |
| } else { | |
| original[this._headersSymbol] = null; | |
| headers.forEach(header => delete header[Sources.SourceFormatData._formatDataSymbol]); | |
| } | |
| headers.forEach(header => Bindings.cssWorkspaceBinding.updateLocations(header)); | |
| } | |
| /** | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| * @return {!Array<!SDK.CSSStyleSheetHeader>} | |
| */ | |
| _headersForUISourceCode(uiSourceCode) { | |
| if (uiSourceCode.contentType() === Common.resourceTypes.Document) { | |
| const target = Bindings.NetworkProject.targetForUISourceCode(uiSourceCode); | |
| const cssModel = target && target.model(SDK.CSSModel); | |
| if (cssModel) { | |
| return cssModel.headersForSourceURL(uiSourceCode.url()) | |
| .filter(header => header.isInline && !header.hasSourceURL); | |
| } | |
| } else if (uiSourceCode.contentType().isStyleSheet()) { | |
| const rawLocations = Bindings.cssWorkspaceBinding.uiLocationToRawLocations(uiSourceCode.uiLocation(0, 0)); | |
| return rawLocations.map(rawLocation => rawLocation.header()).filter(header => !!header); | |
| } | |
| return []; | |
| } | |
| }; | |
| Sources.sourceFormatter = new Sources.SourceFormatter(); |