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