Index: Source/devtools/front_end/bindings/SourceMapEditor.js |
diff --git a/Source/devtools/front_end/bindings/SourceMapEditor.js b/Source/devtools/front_end/bindings/SourceMapEditor.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..fd1e0fd8a30b24af6c7634493e4c36b29f51b0a3 |
--- /dev/null |
+++ b/Source/devtools/front_end/bindings/SourceMapEditor.js |
@@ -0,0 +1,260 @@ |
+/** |
+ * @constructor |
+ * @param {!Map.<string, string>} sources |
+ * @param {!WebInspector.SourceMapEditorFlavor} flavor |
+ */ |
+WebInspector.SourceMapEditor = function(sources, flavor) |
+{ |
+ this._sources = sources; |
+ this._editFlavor = flavor; |
+} |
+/** |
+ * @param {string} sourceURL |
+ * @param {string} contentV1 |
+ * @param {string} contentV2 |
+ * @return {!Array.<!WebInspector.SourceMapEdit>} |
+ */ |
+WebInspector.SourceMapEditor.computeEdits = function(sourceURL, contentV1, contentV2) |
+{ |
+ var differ = new diff_match_patch(); |
+ var diff = differ.diff_main(contentV1, contentV2); |
+ differ.diff_cleanupEfficiency(diff); |
+ var oldSourceOffset = 0; |
+ var edits = []; |
+ var i = 0; |
+ while (i < diff.length) { |
+ var token = diff[i]; |
+ if (token[0] === 0) { |
+ oldSourceOffset += token[1].length; |
+ ++i; |
+ continue; |
+ } |
+ |
+ var nextToken = diff[i + 1]; |
+ if (nextToken && nextToken[0] === -token[0]) { |
+ // If this is an EDIT command. |
+ var add, remove; |
+ if (token[0] === 1) { |
+ add = token; |
+ remove = nextToken; |
+ } else { |
+ add = nextToken; |
+ remove = token; |
+ } |
+ var sourceRange = new WebInspector.SourceRange(oldSourceOffset, remove[1].length); |
+ var oldRange = WebInspector.TextRange.createFromSourceRange(contentV1, sourceRange); |
+ edits.push(new WebInspector.SourceMapEdit(sourceURL, oldRange, remove[1], add[1])); |
+ oldSourceOffset += remove[1].length; |
+ i += 2; |
+ } else if (token[0] === 1) { |
+ // If this is an ADD command. |
+ var sourceRange = new WebInspector.SourceRange(oldSourceOffset, 0); |
+ var oldRange = WebInspector.TextRange.createFromSourceRange(contentV1, sourceRange); |
+ edits.push(new WebInspector.SourceMapEdit(sourceURL, oldRange, "", token[1])); |
+ i += 1; |
+ } else { |
+ // If this is a REMOVE command. |
+ var sourceRange = new WebInspector.SourceRange(oldSourceOffset, token[1].length); |
+ var oldRange = WebInspector.TextRange.createFromSourceRange(contentV1, sourceRange); |
+ edits.push(new WebInspector.SourceMapEdit(sourceURL, oldRange, token[1], "")); |
+ oldSourceOffset += token[1].length; |
+ i += 1; |
+ } |
+ } |
+ return edits; |
+} |
+ |
+WebInspector.SourceMapEditor.prototype = { |
+ /** |
+ * @param {!WebInspector.SourceMap} map |
+ * @param {!Array<!WebInspector.SourceMapEdit>} edits |
+ * @return {!Set<string, string>} |
+ */ |
+ handleCompiledEdits: function(map, edits) |
+ { |
+ var backEditInfos = []; |
+ for (var i = 0; i < edits.length; ++i) |
+ backEditInfos.push(this._doCompiledEdit(map, edits[i])); |
+ |
+ var didBackEdits = false; |
+ for (var i = 0; i < edits.length; ++i) |
+ didBackEdits = this._doBackEdits(map, edits[i], backEditInfos[i]) || didBackEdits; |
+ |
+ var result = new Set(); |
+ if (didBackEdits) |
+ result.add(edits[0].sourceURL); |
+ for (var i = 0; i < backEditInfos.length; ++i) { |
+ var sourceURL = backEditInfos[i].compiledBaseMapping.sourceURL; |
+ result.add(sourceURL); |
+ } |
+ return result; |
+ }, |
+ |
+ /** |
+ * @param {!WebInspector.SourceMap} map |
+ * @param {!WebInspector.SourceMapEdit} edit |
+ * @return {!WebInspector.SourceMapEditor.BackEditInfo} |
+ */ |
+ _doCompiledEdit: function(map, edit) |
+ { |
+ var startMapping = map.findEntry(edit.oldRange.startLine, edit.oldRange.startColumn); |
+ var endMapping = map.findEntry(edit.oldRange.endLine, edit.oldRange.endColumn); |
+ if (startMapping.sourceURL !== endMapping.sourceURL) |
+ throw new Error("An edit spans multiple source files."); |
+ |
+ var sourceURL = startMapping.sourceURL; |
+ var compiledText = this._sources.get(edit.sourceURL); |
+ var sourceText = this._sources.get(sourceURL); |
+ |
+ // Return if we don't have source for mapped entries. |
+ if (!compiledText || !sourceText) |
+ throw new Error("Source of " + sourceURL + " is missing"); |
+ |
+ var startLocation = this._mapCompiledLocation(startMapping, edit.oldRange.startLine, edit.oldRange.startColumn); |
+ var endLocation = this._mapCompiledLocation(endMapping, edit.oldRange.endLine, edit.oldRange.endColumn); |
+ var sourceOldRange = new WebInspector.TextRange(startLocation.line, startLocation.column, endLocation.line, endLocation.column); |
+ var sourceOldText = sourceOldRange.extract(sourceText); |
+ // If the text in compiled and original differs, then we cannot edit it. |
+ var sourceNewText = this._editFlavor.sourceEditText(edit.oldText, sourceOldText, edit.newText) |
+ if (sourceNewText === null) |
+ throw new Error("The edited range is not equal in origin and source"); |
+ |
+ var newTextLineCount = sourceNewText.lineEndings().length; |
+ if (newTextLineCount > 1) |
+ throw new Error("Not implemented for multi-line edit"); |
+ |
+ if (!map.compiledRangeEdited(edit.oldRange, edit.newRange())) |
+ throw new Error("Failed to update compiled locations in sourcemap."); |
+ |
+ var sourceEdit = new WebInspector.SourceMapEdit(sourceURL, sourceOldRange, sourceOldText, sourceNewText); |
+ if (!map.sourceRangeEdited(sourceURL, sourceEdit.oldRange, sourceEdit.newRange())) |
+ throw new Error("Failed to update source locations in sourcemap."); |
+ |
+ this._sources.set(sourceURL, sourceEdit.applyToText(sourceText)); |
+ |
+ var offsetRange = edit.oldRange.relativeTo(startMapping.lineNumber, startMapping.columnNumber); |
+ var sourceNewRange = sourceEdit.newRange(); |
+ var reversedMappings = map.findEntriesReversed(sourceEdit.sourceURL, sourceNewRange.startLine, sourceNewRange.startColumn + 1); |
+ return new WebInspector.SourceMapEditor.BackEditInfo(startMapping, offsetRange, reversedMappings); |
+ }, |
+ |
+ _doBackEdits: function(map, compiledEdit, backEditInfo) |
+ { |
+ // There is no multimapping from source position to compiled position. |
+ if (backEditInfo.reversedMappings.length < 2) |
+ return false; |
+ var didEdit = false; |
+ var compiledText = this._sources.get(compiledEdit.sourceURL); |
+ for (var i = 0; i < backEditInfo.reversedMappings.length; ++i) { |
+ var mapping = backEditInfo.reversedMappings[i]; |
+ if (mapping === backEditInfo.compiledBaseMapping) |
+ continue; |
+ var oldEditRange = rebaseOffsetRange(backEditInfo.offsetRange, mapping); |
+ var reverseEdit = new WebInspector.SourceMapEdit(compiledEdit.sourceURL, oldEditRange, oldEditRange.extract(compiledText), compiledEdit.newText); |
+ if (reverseEdit.oldText !== compiledEdit.oldText) { |
+ console.error("compiled texts differ."); |
+ continue; |
+ } |
+ if (!map.compiledRangeEdited(reverseEdit.oldRange, reverseEdit.newRange())) |
+ throw new Error("Failed to update reversed compiled locations in sourcemap."); |
+ compiledText = reverseEdit.applyToText(compiledText); |
+ didEdit = true; |
+ } |
+ if (didEdit) |
+ this._sources.set(compiledEdit.sourceURL, compiledText); |
+ |
+ return didEdit; |
+ |
+ function rebaseOffsetRange(range, mapping) |
+ { |
+ return new WebInspector.TextRange( |
+ range.startLine + mapping.lineNumber, |
+ range.startColumn + mapping.columnNumber, |
+ range.endLine + mapping.lineNumber, |
+ range.endColumn + mapping.columnNumber |
+ ); |
+ } |
+ }, |
+ |
+ /** |
+ * @param {!WebInspector.SourceMap.Entry} entry |
+ * @param {number} lineNumber |
+ * @param {number} columnNumber |
+ * @return {!{line: number, column: number}} |
+ */ |
+ _mapCompiledLocation: function(entry, lineNumber, columnNumber) |
+ { |
+ var lineOffset = lineNumber - entry.lineNumber; |
+ var columnOffset = columnNumber - entry.columnNumber; |
+ return {line: entry.sourceLineNumber + lineOffset, column: entry.sourceColumnNumber + columnOffset}; |
+ } |
+} |
+ |
+/** |
+ * @constructor |
+ * @param {string} sourceURL |
+ * @param {!WebInspector.TextRange} oldRange |
+ * @param {string} oldText |
+ * @param {string} newText |
+ */ |
+WebInspector.SourceMapEdit = function(sourceURL, oldRange, oldText, newText) |
+{ |
+ this.sourceURL = sourceURL; |
+ this.oldRange = oldRange; |
+ this.oldText = oldText; |
+ this.newText = newText; |
+} |
+ |
+WebInspector.SourceMapEdit.prototype = { |
+ /** |
+ * @return {!WebInspector.TextRange} |
+ */ |
+ newRange: function() |
+ { |
+ var endLine = this.oldRange.startLine; |
+ var endColumn = this.oldRange.startColumn + this.newText.length; |
+ var lineEndings = this.newText.lineEndings(); |
+ if (lineEndings.length > 1) { |
+ endLine = this.oldRange.startLine + lineEndings.length - 1; |
+ var len = lineEndings.length; |
+ endColumn = lineEndings[len - 1] - lineEndings[len - 2] - 1; |
+ } |
+ return new WebInspector.TextRange( |
+ this.oldRange.startLine, |
+ this.oldRange.startColumn, |
+ endLine, |
+ endColumn); |
+ }, |
+ |
+ applyToText: function(text) |
+ { |
+ return this.oldRange.replaceInText(text, this.newText); |
+ }, |
+} |
+ |
+WebInspector.SourceMapEditor.BackEditInfo = function(compiledBaseMapping, offsetRange, reversedMappings) |
+{ |
+ this.compiledBaseMapping = compiledBaseMapping; |
+ this.offsetRange = offsetRange; |
+ this.reversedMappings = reversedMappings; |
+} |
+ |
+/** |
+ * @interface |
+ */ |
+WebInspector.SourceMapEditorFlavor = function() { } |
+ |
+WebInspector.SourceMapEditorFlavor.prototype = { |
+ /** |
+ * @param {string} compiledURL |
+ * @param {string} compiledText |
+ * @param {!WebInspector.TextRange} compiledRange |
+ * @param {string} sourceURL |
+ * @param {string} sourceText |
+ * @param {!WebInspector.TextRange} sourceRange |
+ * @return {?string} |
+ */ |
+ sourceEditText: function(compiledURL, compiledText, compiledRange, sourceText, sourceURL, sourceRange) |
+ { |
+ }, |
+} |