Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1250)

Unified Diff: Source/devtools/front_end/bindings/SASSSourceMapping.js

Issue 1307063005: DevTools: edit SASS through SourceMaps. (Closed) Base URL: svn://svn.chromium.org/blink/trunk
Patch Set: improvements Created 5 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « Source/devtools/devtools.gypi ('k') | Source/devtools/front_end/bindings/SASSStructureMapping.js » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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;
+ }
+}
« no previous file with comments | « Source/devtools/devtools.gypi ('k') | Source/devtools/front_end/bindings/SASSStructureMapping.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698