Index: Source/devtools/front_end/bindings/SASSSourceMapping.js |
diff --git a/Source/devtools/front_end/bindings/SASSSourceMapping.js b/Source/devtools/front_end/bindings/SASSSourceMapping.js |
index 422b8c3224ce25b1e2a2f6de2d8d2ef6fea054e3..8778a0417fd879307817424c11c205a12ec5b872 100644 |
--- a/Source/devtools/front_end/bindings/SASSSourceMapping.js |
+++ b/Source/devtools/front_end/bindings/SASSSourceMapping.js |
@@ -52,6 +52,8 @@ WebInspector.SASSSourceMapping = function(cssModel, workspace, networkMapping, n |
this._workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeContentCommitted, this._uiSourceCodeContentCommitted, this); |
this._workspace.addEventListener(WebInspector.Workspace.Events.ProjectRemoved, this._reset, this); |
this._networkMapping = networkMapping; |
+ |
+ this._editor = new WebInspector.SASSSourceMapping.Editor(workspace, networkMapping, cssModel, this); |
} |
WebInspector.SASSSourceMapping.prototype = { |
@@ -65,6 +67,7 @@ WebInspector.SASSSourceMapping.prototype = { |
--this._addingRevisionCounter; |
return; |
} |
+ return; |
var header = this._cssModel.styleSheetHeaderForId(id); |
if (!header) |
return; |
@@ -651,5 +654,425 @@ WebInspector.SASSSourceMapping.prototype = { |
/** @type {!Object.<string, !WebInspector.SourceMap>} */ |
this._sourceMapByURL = {}; |
this._sourceMapByStyleSheetURL = {}; |
+ }, |
+ |
+ _sourceMapForCSSURL: function(cssURL) |
+ { |
+ var sassURL = this._completeSourceMapURLForCSSURL[cssURL]; |
+ if (!sassURL) |
+ return null; |
+ return this._sourceMapByURL[sassURL] || null; |
} |
} |
+ |
+WebInspector.SASSSourceMapping.Editor = function(workspace, networkMapping, cssModel, sassSourceMapping) |
+{ |
+ this._workspace = workspace; |
+ this._networkMapping = networkMapping; |
+ this._cssModel = cssModel; |
+ this._sassSourceMapping = sassSourceMapping; |
+ |
+ this._workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeAdded, this._uiSourceCodeAddedToWorkspace, this); |
+ this._workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeRemoved, this._uiSourceCodeRemoved, this); |
+ |
+ this._contents = new Map(); |
+ this._changedURLs = new Set(); |
+ this._throttler = new WebInspector.Throttler(0); |
+} |
+ |
+WebInspector.SASSSourceMapping.Editor.prototype = { |
+ _uiSourceCodeRemoved: function(event) |
+ { |
+ event.data.removeEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._sourceCodeCommitted, this); |
+ }, |
+ |
+ _uiSourceCodeAddedToWorkspace: function(event) |
+ { |
+ var uiSourceCode = event.data; |
+ var url = uiSourceCode.originURL(); |
+ if (!this._cssModel.styleSheetIdsForURL(url).length) |
+ return; |
+ uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._sourceCodeCommitted, this); |
+ uiSourceCode.requestContentPromise().then(setContent.bind(this)); |
+ |
+ function setContent(text) |
+ { |
+ this._contents.set(url, text); |
+ } |
+ }, |
+ |
+ _killMappingForURL: function(cssURL) |
+ { |
+ var ids = this._cssModel.styleSheetIdsForURL(cssURL); |
+ for (var id of ids) { |
+ var header = this._cssModel.styleSheetHeaderForId(id); |
+ this._sassSourceMapping.removeHeader(header); |
+ } |
+ }, |
+ |
+ _sourceCodeCommitted: function(event) |
+ { |
+ if (this._muteSourceCodeCommitted) |
+ return; |
+ |
+ var sourceURL = this._networkMapping.networkURL(event.target); |
+ this._changedURLs.add(sourceURL); |
+ this._throttler.schedule(this._processChangedSourceCodes.bind(this)); |
+ }, |
+ |
+ _processChangedSourceCodes: function() |
+ { |
+ console.log("React!"); |
+ //FIXME: handle all changes. |
+ var cssSourceURL = this._changedURLs.valuesArray()[0]; |
+ this._changedURLs.clear(); |
+ |
+ var uiSourceCode = this._networkMapping.uiSourceCodeForURL(cssSourceURL, this._cssModel.target()); |
+ var historyLength = uiSourceCode.history.length; |
+ var oldCSSContent = this._contents.get(cssSourceURL); |
+ var newCSSContent = uiSourceCode.history[historyLength - 1].content; |
+ |
+ var sourceMap = this._sassSourceMapping._sourceMapForCSSURL(cssSourceURL); |
+ if (!sourceMap) |
+ return Promise.resolve(); |
+ var edits = WebInspector.SourceMapEditor.computeEdits(cssSourceURL, oldCSSContent, newCSSContent); |
+ edits.sort(sequentialOrder); |
+ // New sourcemap which maps to content. |
+ var newSourceMap = sourceMap.clone(); |
+ for (var i = edits.length - 1; i >= 0; --i) |
+ newSourceMap.compiledRangeEdited(edits[i].oldRange, edits[i].newRange()); |
+ |
+ var parser = new WebInspector.CSSParser(); |
+ |
+ var oldCSSStruct, newCSSStruct; |
+ |
+ var parsePromise = parser.parsePromise(oldCSSContent) |
+ .then(function(struct) { oldCSSStruct = struct; return parser.parsePromise(newCSSContent);}) |
+ .then(function(struct) { newCSSStruct = struct; parser.dispose(); }) |
+ .then(onCSSStructs); |
+ |
+ var urlsToRequest = new Set(); |
+ for (var i = 0; i < edits.length; ++i) { |
+ var edit = edits[i]; |
+ urlsToRequest.add(edit.sourceURL); |
+ var mapping = sourceMap.findEntry(edit.oldRange.startLine, edit.oldRange.startColumn); |
+ urlsToRequest.add(mapping.sourceURL); |
+ } |
+ |
+ var sources = new Map(); |
+ var sourcePromises = []; |
+ for (var url of urlsToRequest) { |
+ var uiSourceCode = this._networkMapping.uiSourceCodeForURL(url, this._cssModel.target()); |
+ var promise = uiSourceCode.requestContentPromise().then(setSource.bind(null, sources, url)); |
+ sourcePromises.push(promise); |
+ } |
+ |
+ return Promise.all([parsePromise, Promise.all(sourcePromises)]).spread(doEdits.bind(this)) |
+ .catch(killMapping.bind(this, cssSourceURL)); |
+ |
+ function setSource(sources, sourceURL, content) |
+ { |
+ if (typeof content !== "string") |
+ throw new Error("Failed to fetch content of " + sourceURL); |
+ sources.set(sourceURL, content); |
+ } |
+ |
+ function onCSSStructs() |
+ { |
+ return WebInspector.SASSSourceMapping.diffCSSStructs(oldCSSStruct, newCSSStruct); |
+ } |
+ |
+ function doEdits(structuralDiff) |
+ { |
+ function mapCSStoSASS(cssLine, cssColumn, sassEdit, useEnd) |
+ { |
+ var sassLineNumber = useEnd ? sassEdit.newRange().endLine : sassEdit.newRange().startLine; |
+ var sassColumnNumber = useEnd ? sassEdit.newRange().endColumn : sassEdit.newRange().startColumn; |
+ var mapping = new WebInspector.SourceMap.Entry(cssLine, cssColumn, sassEdit.sourceURL, sassLineNumber, sassColumnNumber); |
+ ensureMappings.set(sassEdit, mapping); |
+ } |
+ |
+ if (this._changedURLs.size > 0) |
+ return; |
+ var structureMapping = new WebInspector.SASSStructureMapping(newCSSStruct, sources); |
+ |
+ var edits = []; |
+ var ensureMappings = new Map(); |
+ for (var i = 0; i < structuralDiff.length; ++i) { |
+ //FIXME: handle other types of edits. |
+ var diff = structuralDiff[i]; |
+ if (diff.type === "ValueChanged") { |
+ var cssProperty = diff.property; |
+ var sassValue = structureMapping.cssPropertyValueToSASS(WebInspector.TextRange.fromObject(cssProperty.valueRange), newSourceMap); |
+ var sassContent = sources.get(sassValue.url); |
+ var sassEdit = new WebInspector.SourceMapEdit(sassValue.url, sassValue.range, sassValue.range.extract(sassContent), " " + cssProperty.value.trim()); |
+ edits.push(sassEdit); |
+ mapCSStoSASS(cssProperty.valueRange.startLine, cssProperty.valueRange.startColumn, sassEdit, false); |
+ |
+ var backEdits = structureMapping.sassPropertyValueToCSS(sassValue.url, sassValue.range, newSourceMap); |
+ if (backEdits.length === 1) |
+ continue; |
+ for (var j = 0; j < backEdits.length; ++j) { |
+ var backEditProperty = backEdits[j]; |
+ if (backEditProperty === cssProperty) |
+ continue; |
+ var oldRange = WebInspector.TextRange.fromObject(backEditProperty.valueRange); |
+ var oldText = oldRange.extract(newCSSContent); |
+ var cssEdit = new WebInspector.SourceMapEdit(cssSourceURL, oldRange, oldText, sassEdit.newText); |
+ edits.push(cssEdit); |
+ var mapping = new WebInspector.SourceMap.Entry(oldRange.startLine, oldRange.startColumn, sassValue.url, sassValue.range.startLine, sassValue.range.startColumn); |
+ ensureMappings.set(cssEdit, mapping); |
+ } |
+ } else if (diff.type === "PropertyAdded") { |
+ var cssProperty = diff.property; |
+ if (diff.after) { |
+ var afterCSSProperty = diff.after; |
+ var afterSASSProperty = structureMapping.cssPropertyToSASS(afterCSSProperty.range, newSourceMap); |
+ var url = afterSASSProperty.url; |
+ var sassContent = sources.get(url); |
+ var oldRange = WebInspector.TextRange.createFromLocation(afterSASSProperty.range.endLine, afterSASSProperty.range.endColumn); |
+ var indent = (new WebInspector.TextRange(afterSASSProperty.range.startLine, 0, afterSASSProperty.range.startLine, afterSASSProperty.range.startColumn)).extract(sassContent); |
+ if (!/^\s+$/.test(indent)) indent = ""; |
+ |
+ // Split property addition into chunks to preserve mappings. |
+ var newText = String.sprintf("\n%s", indent); |
+ var edit1 = new WebInspector.SourceMapEdit(url, oldRange, "", newText); |
+ mapCSStoSASS(cssProperty.range.startLine, cssProperty.range.startColumn, edit1, true); |
+ |
+ var newText = String.sprintf("%s:", cssProperty.name.trim()); |
+ var edit2 = new WebInspector.SourceMapEdit(url, oldRange, "", newText); |
+ mapCSStoSASS(cssProperty.valueRange.startLine, cssProperty.valueRange.startColumn, edit2, true); |
+ |
+ var newText = String.sprintf(" %s;", cssProperty.value.trim()); |
+ var edit3 = new WebInspector.SourceMapEdit(url, oldRange, "", newText); |
+ mapCSStoSASS(cssProperty.range.endLine, cssProperty.range.endColumn, edit3, true); |
+ |
+ edits.push(edit3, edit2, edit1); |
+ } else if (diff.before) { |
+ var beforeCSSProperty = diff.before; |
+ var beforeSASSProperty = structureMapping.cssPropertyToSASS(beforeCSSProperty.range, newSourceMap); |
+ var url = beforeSASSProperty.url; |
+ var sassContent = sources.get(url); |
+ var oldRange = WebInspector.TextRange.createFromLocation(beforeSASSProperty.range.startLine, beforeSASSProperty.range.startColumn); |
+ var indent = (new WebInspector.TextRange(beforeSASSProperty.range.startLine, 0, beforeSASSProperty.range.startLine, beforeSASSProperty.range.startColumn)).extract(sassContent); |
+ if (!/^\s+$/.test(indent)) indent = ""; |
+ |
+ // Split property addition into chunks to preserve mappings. |
+ var newText = String.sprintf("%s:", cssProperty.name.trim()); |
+ var edit1 = new WebInspector.SourceMapEdit(url, oldRange, "", newText); |
+ mapCSStoSASS(cssProperty.range.startLine, cssProperty.range.startColumn, edit1); |
+ |
+ var newText = String.sprintf(" %s;", cssProperty.value.trim()); |
+ var edit2 = new WebInspector.SourceMapEdit(url, oldRange, "", newText); |
+ mapCSStoSASS(cssProperty.valueRange.startLine, cssProperty.valueRange.startColumn, edit2); |
+ |
+ var newText = String.sprintf("\n%s", indent); |
+ var edit3 = new WebInspector.SourceMapEdit(url, oldRange, "", newText); |
+ mapCSStoSASS(cssProperty.range.endLine, cssProperty.range.endColumn, edit3); |
+ |
+ edits.push(edit3, edit2, edit1); |
+ } |
+ //FIXME: do back edits. |
+ } else if (diff.type === "PropertyRemoved") { |
+ var cssProperty = diff.property; |
+ var sassProperty = structureMapping.cssPropertyToSASS(cssProperty.range, sourceMap); |
+ var sassContent = sources.get(sassProperty.url); |
+ var lineRemoveRange = new WebInspector.TextRange(sassProperty.range.startLine, 0, sassProperty.range.endLine + 1, 0); |
+ var oldRange = (lineRemoveRange.extract(sassContent).trim() === sassProperty.range.extract(sassContent).trim()) ? lineRemoveRange : sassProperty.range; |
+ var sassEdit = new WebInspector.SourceMapEdit(sassProperty.url, oldRange, oldRange.extract(sassContent), ""); |
+ edits.push(sassEdit); |
+ //FIXME: do back edits. |
+ } |
+ } |
+ |
+ // Categorize edits per url. |
+ var editsPerURL = new Map(); |
+ for (var edit of edits) { |
+ if (!editsPerURL.has(edit.sourceURL)) |
+ editsPerURL.set(edit.sourceURL, []); |
+ var list = editsPerURL.get(edit.sourceURL); |
+ list.push(edit); |
+ } |
+ |
+ // Apply to CSS. |
+ var cssEdits = editsPerURL.get(cssSourceURL); |
+ if (cssEdits) { |
+ cssEdits.stableSort(sequentialOrder); |
+ // Apply edits in a reversed order so that they do not conflict with each other. |
+ for (var i = cssEdits.length - 1; i >= 0; --i) { |
+ var cssEdit = cssEdits[i]; |
+ newCSSContent = cssEdit.applyToText(newCSSContent); |
+ newSourceMap.compiledRangeEdited(cssEdit.oldRange, cssEdit.newRange()); |
+ var ensureMapping = ensureMappings.get(cssEdit); |
+ // Add missing mappings to source map. |
+ if (ensureMapping) |
+ newSourceMap.ensureHasMapping(ensureMapping); |
+ } |
+ var uiSourceCode = this._networkMapping.uiSourceCodeForURL(cssSourceURL, this._cssModel.target()); |
+ this._muteSourceCodeCommitted = true; |
+ uiSourceCode.addRevision(newCSSContent); |
+ this._muteSourceCodeCommitted = false; |
+ } |
+ this._contents.set(cssSourceURL, newCSSContent); |
+ |
+ // Apply to SASS. |
+ for (var sassURL of editsPerURL.keys()) { |
+ if (sassURL === cssSourceURL) |
+ continue; |
+ var sassContent = sources.get(sassURL); |
+ var sassEdits = editsPerURL.get(sassURL); |
+ sassEdits.stableSort(sequentialOrder); |
+ // Apply edits in a reversed order so that they do not conflict with each other. |
+ for (var i = sassEdits.length - 1; i >= 0; --i) { |
+ var sassEdit = sassEdits[i]; |
+ sassContent = sassEdit.applyToText(sassContent); |
+ newSourceMap.sourceRangeEdited(sassEdit.sourceURL, sassEdit.oldRange, sassEdit.newRange()); |
+ var ensureMapping = ensureMappings.get(sassEdit); |
+ // Add missing mappings to source map. |
+ if (ensureMapping) |
+ newSourceMap.ensureHasMapping(ensureMapping); |
+ } |
+ var uiSourceCode = this._networkMapping.uiSourceCodeForURL(sassURL, this._cssModel.target()); |
+ uiSourceCode.addRevision(sassContent); |
+ } |
+ |
+ // Hot-Swap source maps. |
+ sourceMap._mappings = newSourceMap._mappings; |
+ sourceMap._reverseMappingsBySourceURL = newSourceMap._reverseMappingsBySourceURL; |
+ } |
+ |
+ function sequentialOrder(range1, range2) |
+ { |
+ return range1.oldRange.follows(range2.oldRange) ? 1 : -1; |
+ } |
+ |
+ function killMapping(sourceURL, e) |
+ { |
+ console.error(e); |
+ this._killMappingForURL(sourceURL); |
+ } |
+ } |
+} |
+ |
+WebInspector.SASSSourceMapping.diffCSSStructs = function(oldCSSStruct, newCSSStruct) |
+{ |
+ oldCSSStruct = oldCSSStruct.filter(styleRulesFilter); |
+ newCSSStruct = newCSSStruct.filter(styleRulesFilter); |
+ if (oldCSSStruct.length !== newCSSStruct.length) |
+ throw new Error("not implemented for rule diff."); |
+ var structuralDiff = []; |
+ for (var i = 0; i < oldCSSStruct.length; ++i) { |
+ var oldRule = oldCSSStruct[i]; |
+ var newRule = newCSSStruct[i]; |
+ var removedSet = new Set(); |
+ var addedSet = new Set(); |
+ if (oldRule.properties.length !== newRule.properties.length) |
+ WebInspector.SASSSourceMapping.cssPropertiesDiff(oldRule.properties, newRule.properties, removedSet, addedSet); |
+ for (var property of removedSet.values()) { |
+ structuralDiff.push({ |
+ type: "PropertyRemoved", |
+ property: property |
+ }); |
+ } |
+ var firstValidProperty = null; |
+ for (var j = 0; j < newRule.properties.length; ++j) { |
+ var property = newRule.properties[j]; |
+ if (!addedSet.has(property)) { |
+ firstValidProperty = property; |
+ break; |
+ } |
+ } |
+ var lastValidProperty = null; |
+ for (var j = 0; j < newRule.properties.length; ++j) { |
+ var property = newRule.properties[j]; |
+ if (!addedSet.has(property)) { |
+ lastValidProperty = property; |
+ continue; |
+ } |
+ var diff = { |
+ type: "PropertyAdded", |
+ property: property, |
+ }; |
+ if (lastValidProperty) |
+ diff.after = lastValidProperty; |
+ else if (firstValidProperty) |
+ diff.before = firstValidProperty; |
+ structuralDiff.push(diff); |
+ } |
+ |
+ var p1 = 0; |
+ var p2 = 0; |
+ while (p1 < oldRule.properties.length && p2 < newRule.properties.length) { |
+ if (removedSet.has(oldRule.properties[p1])) { |
+ ++p1; |
+ continue; |
+ } |
+ if (addedSet.has(newRule.properties[p2])) { |
+ ++p2; |
+ continue; |
+ } |
+ var oldProperty = oldRule.properties[p1++]; |
+ var newProperty = newRule.properties[p2++]; |
+ if (oldProperty.name !== newProperty.name) { |
+ structuralDiff.push({ |
+ type: "NameChanged", |
+ property: newProperty |
+ }); |
+ } else if (oldProperty.value !== newProperty.value) { |
+ structuralDiff.push({ |
+ type: "ValueChanged", |
+ property: newProperty |
+ }); |
+ } |
+ } |
+ } |
+ return structuralDiff; |
+ |
+ function styleRulesFilter(rule) |
+ { |
+ return !!rule.properties; |
+ } |
+} |
+ |
+WebInspector.SASSSourceMapping.cssPropertiesDiff = function(properties1, properties2, removedSet, addedSet) |
+{ |
+ var charCode = 33; |
+ var encodedProperties = new Map(); |
+ var text1 = doEncode(properties1); |
+ var text2 = doEncode(properties2); |
+ var differ = new diff_match_patch(); |
+ var diff = differ.diff_main(text1, text2); |
+ |
+ var p1 = 0, p2 = 0; |
+ for (var i = 0; i < diff.length; ++i) { |
+ var token = diff[i]; |
+ if (token[0] === 0) { |
+ p1 += token[1].length; |
+ p2 += token[1].length; |
+ } else if (token[0] === -1) { |
+ for (var j = 0; j < token[1].length; ++j) { |
+ var property = properties1[p1++]; |
+ removedSet.add(property); |
+ } |
+ } else { |
+ for (var j = 0; j < token[1].length; ++j) { |
+ var property = properties2[p2++]; |
+ addedSet.add(property); |
+ } |
+ } |
+ } |
+ |
+ function doEncode(properties) |
+ { |
+ var text = ""; |
+ for (var i = 0; i < properties.length; ++i) { |
+ var encoded = encodedProperties.get(properties[i].name); |
+ if (!encoded) { |
+ encoded = String.fromCharCode(charCode++); |
+ encodedProperties.set(properties[i].name, encoded); |
+ } |
+ text += encoded; |
+ } |
+ return text; |
+ } |
+} |