| 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;
|
| + }
|
| +}
|
|
|