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

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

Issue 1331083002: DevTools: [STRUCT] edit SASS through SourceMaps (Closed) Base URL: https://chromium.googlesource.com/chromium/blink.git@master
Patch Set: rebaseline atop master 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 | « no previous file | Source/devtools/front_end/bindings/SASSSupport.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 370fe3bdc50d1608ef166eed7b763a8964d963a0..c17cc3a95857c19ff5f89b24b8cd53d74dce5d59 100644
--- a/Source/devtools/front_end/bindings/SASSSourceMapping.js
+++ b/Source/devtools/front_end/bindings/SASSSourceMapping.js
@@ -64,6 +64,7 @@ WebInspector.SASSSourceMapping.prototype = {
--this._addingRevisionCounter;
return;
}
+ return;
var header = this._cssModel.styleSheetHeaderForId(id);
if (!header)
return;
@@ -236,6 +237,11 @@ WebInspector.SASSSourceMapping.prototype = {
{
WebInspector.cssWorkspaceBinding.pushSourceMapping(header, this);
var cssURL = header.sourceURL;
+ var rawURL = header.sourceURL;
+ if (!this._structureMapByURL[rawURL]) {
+ var structureMap = new WebInspector.SASSSourceMapping.StructureMap(this._workspace, this._networkMapping, this._cssModel, this, rawURL, sourceMap);
+ this._structureMapByURL[rawURL] = structureMap;
+ }
var sources = sourceMap.sources();
for (var i = 0; i < sources.length; ++i) {
var sassURL = sources[i];
@@ -351,6 +357,8 @@ WebInspector.SASSSourceMapping.prototype = {
this._sourceMapByURL = {};
this._sourceMapByStyleSheetURL = {};
this._pollManager.reset();
+
+ this._structureMapByURL = {};
}
}
@@ -642,4 +650,388 @@ WebInspector.SASSSourceMapping.PollManager.prototype = {
}
}
}
-}
+}
+
+WebInspector.SASSSourceMapping.StructureMap = function(workspace, networkMapping, cssModel, sassSourceMapping, cssURL, sourceMap)
+{
+ this._workspace = workspace;
+ this._networkMapping = networkMapping;
+ this._cssModel = cssModel;
+ this._sassSourceMapping = sassSourceMapping;
+ this._cssURL = cssURL;
+ this._sourceMap = sourceMap;
+
+ this._throttler = new WebInspector.Throttler(0);
+
+ this._sources = new Map();
+ this._models = new Map();
+ this._mapping = new WebInspector.SASSSourceMapping.CssToSassMapping();
+
+ this._unloadedSourceURLs = new Set();
+ for (var source of this._sourceMap.sources())
+ this._unloadedSourceURLs.add(source);
+ this._unloadedSourceURLs.add(this._cssURL);
+
+ for (var url of this._unloadedSourceURLs) {
+ var uiSourceCode = this._networkMapping.uiSourceCodeForURL(url, this._cssModel.target());
+ if (!uiSourceCode)
+ continue;
+ uiSourceCode.requestContentPromise().then(this._onSourceLoaded.bind(this, url));
+ }
+ this._workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeAdded, this._uiSourceCodeAdded, this);
+}
+
+WebInspector.SASSSourceMapping.StructureMap.prototype = {
+ _uiSourceCodeAdded: function(event)
+ {
+ var uiSourceCode = event.data;
+ var url = uiSourceCode.originURL();
+ if (this._unloadedSourceURLs.has(url))
+ uiSourceCode.requestContentPromise().then(this._onSourceLoaded.bind(this, url));
+ },
+
+ _onSourceLoaded: function(url, content)
+ {
+ if (typeof content !== "string")
+ throw new Error("Failed to fetch content of " + url);
+ this._sources.set(url, content);
+ this._unloadedSourceURLs.delete(url);
+ if (this._unloadedSourceURLs.size)
+ return;
+ this._workspace.removeEventListener(WebInspector.Workspace.Events.UISourceCodeAdded, this._uiSourceCodeAdded, this);
+ var tokenizerPromise = this._loadTokenizer();
+ var cssASTPromise = WebInspector.SASSSupport.parseCSS(this._cssURL, this._sources.get(this._cssURL));
+
+ Promise.all([parsePromise, tokenizerPromise])
+ .spread(this._initialize.bind(this))
+ .catch(this._killMapping.bind(this));
+ },
+
+ _loadTokenizer: function()
+ {
+ return self.runtime.instancePromise(WebInspector.TokenizerFactory).then(onTokenizer.bind(this));
+
+ function onTokenizer(tokenizer)
+ {
+ this._tokenizerFactory = tokenizer;
+ }
+ },
+
+ _initialize: function(cssAST)
+ {
+ this._models.set(this._cssURL, cssAST);
+
+ var uiSourceCode = this._networkMapping.uiSourceCodeForURL(this._cssURL, this._cssModel.target());
+ uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._sourceCodeCommitted, this);
+
+ // FIXME: this works for O(N^2).
+ for (var rule of cssAST.rules) {
+ for (var property of rule.properties) {
+ this._mapCssNodeToSassNode(property.name);
+ this._mapCssNodeToSassNode(property.value);
+ }
+ }
+ console.log("Initialized!");
+ },
+
+ _mapCssNodeToSassNode: function(cssNode)
+ {
+ var entry = this._sourceMap.findEntry(cssNode.range.endLine, cssNode.range.endColumn);
+ if (!entry) {
+ console.log("Missing mapping for entry!");
+ return;
+ }
+ var sassNode = this._findSassForMapping(entry);
+ if (!sassNode) {
+ console.log("Missing sass node for entry!");
+ return;
+ }
+ this._mapping.mapCssToSass(cssNode, sassNode);
+ },
+
+ _findSassForMapping: function(entry)
+ {
+ var sassModel = this._modelForURL(entry.sourceURL);
+ for (var rule of sassModel.rules) {
+ for (var property of rule.properties) {
+ if (property.name.range.containsLocation(entry.sourceLineNumber, entry.sourceColumnNumber))
+ return property.name;
+ if (property.value.range.containsLocation(entry.sourceLineNumber, entry.sourceColumnNumber))
+ return property.value;
+ }
+ }
+ return null;
+ },
+
+ _modelForURL: function(url)
+ {
+ if (this._models.has(url))
+ return this._models.get(url);
+ var source = this._sources.get(url);
+ var ast = WebInspector.SASSSupport.parseSASS(url, source, this._tokenizerFactory);
+ this._models.set(url, ast);
+ return ast;
+ },
+
+ _killMapping: function(error)
+ {
+ if (error)
+ console.error(error);
+ var ids = this._cssModel.styleSheetIdsForURL(this._cssURL);
+ for (var id of ids) {
+ var header = this._cssModel.styleSheetHeaderForId(id);
+ this._sassSourceMapping.removeHeader(header);
+ }
+ var uiSourceCode = this._networkMapping.uiSourceCodeForURL(this._cssURL, this._cssModel.target());
+ uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._sourceCodeCommitted, this);
+ },
+
+ _sourceCodeCommitted: function(event)
+ {
+ if (this._muteSourceCodeCommitted)
+ return;
+
+ var sourceURL = this._networkMapping.networkURL(event.target);
+ // Break mapping on SASS manual edits.
+ if (sourceURL !== this._cssURL) {
+ this._killMapping();
+ return;
+ }
+ this._throttler.schedule(this._handleCSSChange.bind(this));
+ },
+
+ _handleCSSChange: function()
+ {
+ console.log("React!");
+ var uiSourceCode = this._networkMapping.uiSourceCodeForURL(this._cssURL, this._cssModel.target());
+ var newCSSContent = uiSourceCode.history[uiSourceCode.history.length - 1].content;
+ return WebInspector.SASSSupport.parseCSS(this._cssURL, newCSSContent).then(this._onCurrentCSSParsed.bind(this)).catch(this._killMapping.bind(this));
+ },
+
+ _newMappingForDiff: function(oldMapping, diff)
+ {
+ var newMapping = new WebInspector.SASSSourceMapping.CssToSassMapping();
+ for (var currentProperty of diff.bToA.keys()) {
+ var oldProperty = diff.bToA.get(currentProperty);
+ var sassName = oldMapping.cssToSass(oldProperty.name);
+ var sassValue = oldMapping.cssToSass(oldProperty.value);
+ newMapping.mapCssToSass(currentProperty.name, sassName);
+ newMapping.mapCssToSass(currentProperty.value, sassValue);
+ }
+ return newMapping;
+ },
+
+ _onCurrentCSSParsed: function(currentCSSModel)
+ {
+ var oldCSSModel = this._modelForURL(this._cssURL);
+ var modelDiff = WebInspector.SASSSourceMapping.diffModels(oldCSSModel, currentCSSModel);
+ var newMapping = this._newMappingForDiff(this._mapping, modelDiff);
+
+ var edits = [];
+ // Traverse diff in a reversed order to preserve add-property sequence.
+ for (var i = modelDiff.structuralDiff.length - 1; i >= 0; --i) {
+ //FIXME: handle other types of edits.
+ var diff = modelDiff.structuralDiff[i];
+ if (diff.type === "ValueChanged") {
+ var cssProperty = diff.property;
+ var newText = " " + cssProperty.value.text.trim();
+ edits = edits.concat(newMapping.setCSSText(cssProperty.value, newText));
+ } else if (diff.type === "NameChanged") {
+ var cssProperty = diff.property;
+ var newText = cssProperty.name.text.trim();
+ edits = edits.concat(newMapping.setCSSText(cssProperty.name, newText));
+ } else if (diff.type === "PropertyAdded") {
+ var cssProperty = diff.property;
+ if (diff.after)
+ edits = edits.concat(newMapping.insertCSSPropertyAfter(cssProperty, diff.after));
+ else if (diff.before)
+ edits = edits.concat(newMapping.insertCSSPropertyBefore(cssProperty, diff.before));
+ } else if (diff.type === "PropertyRemoved") {
+ var cssProperty = diff.property;
+ var sassName = this._mapping.cssToSass(cssProperty.name);
+ edits = edits.concat(newMapping.removeSASSProperty(sassName.parent));
+ } else if (diff.type === "toggleDisabled") {
+ var cssProperty = diff.property;
+ edits = edits.concat(newMapping.toggleDisabled(cssProperty));
+ }
+ }
+
+ // Categorize edits per url.
+ var editsPerURL = new Multimap();
+ for (var edit of edits)
+ editsPerURL.set(edit.sourceURL, edit);
+
+ // Apply to uiSourceCodes.
+ this._sources.set(this._cssURL, currentCSSModel.text);
+ var urls = editsPerURL.keysArray();
+ for (var url of urls) {
+ var source = this._sources.get(url);
+ var edits = editsPerURL.get(url).valuesArray();
+ edits.stableSort(sequentialOrder);
+ // Apply edits in a reversed order so that they do not conflict with each other.
+ for (var i = edits.length - 1; i >= 0; --i) {
+ var edit = edits[i];
+ source = edit.applyToText(source);
+ }
+ this._sources.set(url, source);
+ this._muteSourceCodeCommitted = true;
+ var uiSourceCode = this._networkMapping.uiSourceCodeForURL(url, this._cssModel.target());
+ uiSourceCode.addRevision(source);
+ this._muteSourceCodeCommitted = false;
+ }
+
+ for (var url of urls) {
+ if (url === this._cssURL)
+ continue;
+ this._updateSASSMapping(newMapping, url);
+ }
+
+ return WebInspector.SASSSupport.parseCSS(this._cssURL, this._sources.get(this._cssURL)).then(onNewCSSModel.bind(this));
+
+ function onNewCSSModel(newCSSModel)
+ {
+ var modelDiff = WebInspector.SASSSourceMapping.diffModels(currentCSSModel, newCSSModel);
+ this._models.set(this._cssURL, newCSSModel);
+ this._mapping = this._newMappingForDiff(newMapping, modelDiff);
+ }
+
+ function sequentialOrder(range1, range2)
+ {
+ return range1.oldRange.follows(range2.oldRange) ? 1 : -1;
+ }
+ },
+
+ _updateSASSMapping: function(mapping, url)
+ {
+ var oldAST = this._modelForURL(url);
+ this._models.delete(url);
+ var newAST = this._modelForURL(url);
+ var modelDiff = WebInspector.SASSSourceMapping.diffModels(oldAST, newAST);
+ for (var oldProperty of modelDiff.aToB.keys()) {
+ var cssNames = mapping.sassToCss(oldProperty.name);
+ var cssValues = mapping.sassToCss(oldProperty.value);
+ var currentProperty = modelDiff.aToB.get(oldProperty);
+ for (var i = 0; i < cssNames.length; ++i) {
+ mapping.unmapCssToSass(cssNames[i], oldProperty.name);
+ mapping.mapCssToSass(cssNames[i], currentProperty.name);
+ }
+ for (var i = 0; i < cssValues.length; ++i) {
+ mapping.unmapCssToSass(cssValues[i], oldProperty.value);
+ mapping.mapCssToSass(cssValues[i], currentProperty.value);
+ }
+ }
+ }
+}
+
+WebInspector.SASSSourceMapping.CssToSassMapping = function()
+{
+ this._cssToSass = new Map();
+ this._sassToCss = new Multimap();
+}
+
+WebInspector.SASSSourceMapping.CssToSassMapping.prototype = {
+ unmapCssToSass: function(css, sass)
+ {
+ this._cssToSass.delete(css);
+ this._sassToCss.remove(sass, css);
+ },
+
+ mapCssToSass: function(css, sass)
+ {
+ this._cssToSass.set(css, sass);
+ this._sassToCss.set(sass, css);
+ },
+
+ cssToSass: function(css)
+ {
+ return this._cssToSass.get(css);
+ },
+
+ sassToCss: function(sass)
+ {
+ return this._sassToCss.get(sass).valuesArray();
+ },
+
+ setCSSText: function(cssNode, text)
+ {
+ var sassNode = this._cssToSass.get(cssNode);
+ var cssNodes = this._sassToCss.get(sassNode);
+
+ var edits = [];
+ edits.push(WebInspector.SASSSupport.setText(sassNode, text));
+ for (var node of cssNodes) {
+ if (node === cssNode)
+ continue;
+ edits.push(WebInspector.SASSSupport.setText(node, text));
+ }
+ return edits;
+ },
+
+ insertCSSPropertyAfter: function(cssProperty, afterProperty)
+ {
+ var sassNode = this._cssToSass.get(afterProperty.name);
+ var sassClone = cssProperty.clone();
+
+ var edits = [];
+ edits.push(WebInspector.SASSSupport.insertPropertyAfter(sassClone, sassNode.parent));
+ this.mapCssToSass(cssProperty.name, sassClone.name);
+ this.mapCssToSass(cssProperty.value, sassClone.value);
+
+ var cssNodes = this._sassToCss.get(sassNode);
+ for (var node of cssNodes) {
+ if (node === afterProperty.name)
+ continue;
+ var cssClone = cssProperty.clone();
+ edits.push(WebInspector.SASSSupport.insertPropertyAfter(cssClone, node.parent));
+ this.mapCssToSass(cssClone.name, sassClone.name);
+ this.mapCssToSass(cssClone.value, sassClone.value);
+ }
+ return edits;
+ },
+
+ insertCSSPropertyBefore: function(cssProperty, beforeProperty)
+ {
+ var sassNode = this._cssToSass.get(beforeProperty.name);
+ var sassClone = cssProperty.clone();
+
+ var edits = [];
+ edits.push(WebInspector.SASSSupport.insertPropertyBefore(sassClone, sassNode.parent));
+ this.mapCssToSass(cssProperty.name, sassClone.name);
+ this.mapCssToSass(cssProperty.value, sassClone.value);
+
+ var cssNodes = this._sassToCss.get(sassNode);
+ for (var node of cssNodes) {
+ if (node === beforeProperty.name)
+ continue;
+ var cssClone = cssProperty.clone();
+ edits.push(WebInspector.SASSSupport.insertPropertyBefore(cssClone, node.parent));
+ this.mapCssToSass(cssClone.name, sassClone.name);
+ this.mapCssToSass(cssClone.value, sassClone.value);
+ }
+ return edits;
+ },
+
+ toggleDisabled: function(cssProperty)
+ {
+ var sassNode = this._cssToSass.get(cssProperty.name);
+ var cssNodes = this._sassToCss.get(sassNode);
+
+ var edits = WebInspector.SASSSupport.toggleDisabled(sassNode.parent, cssProperty.disabled);
+ for (var node of cssNodes) {
+ if (node === cssProperty.name)
+ continue;
+ edits = edits.concat(WebInspector.SASSSupport.toggleDisabled(node.parent, cssProperty.disabled));
+ }
+ return edits;
+ },
+
+ removeSASSProperty: function(sassProperty)
+ {
+ var edits = [];
+ edits.push(WebInspector.SASSSupport.removeProperty(sassProperty));
+ var cssNodes = this._sassToCss.get(sassProperty.name);
+ for (var node of cssNodes)
+ edits.push(WebInspector.SASSSupport.removeProperty(node.parent));
+ return edits;
+ },
+}
« no previous file with comments | « no previous file | Source/devtools/front_end/bindings/SASSSupport.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698