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