Home | History | Annotate | Download | only in sdk
      1 /*
      2  * Copyright (C) 2012 Google Inc. All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions are
      6  * met:
      7  *
      8  *     * Redistributions of source code must retain the above copyright
      9  * notice, this list of conditions and the following disclaimer.
     10  *     * Redistributions in binary form must reproduce the above
     11  * copyright notice, this list of conditions and the following disclaimer
     12  * in the documentation and/or other materials provided with the
     13  * distribution.
     14  *     * Neither the name of Google Inc. nor the names of its
     15  * contributors may be used to endorse or promote products derived from
     16  * this software without specific prior written permission.
     17  *
     18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29  */
     30 
     31 /**
     32  * @constructor
     33  * @implements {WebInspector.SourceMapping}
     34  * @param {!WebInspector.CSSStyleModel} cssModel
     35  * @param {!WebInspector.Workspace} workspace
     36  * @param {!WebInspector.NetworkWorkspaceBinding} networkWorkspaceBinding
     37  */
     38 WebInspector.SASSSourceMapping = function(cssModel, workspace, networkWorkspaceBinding)
     39 {
     40     this.pollPeriodMs = 5000;
     41     this.pollIntervalMs = 200;
     42 
     43     this._cssModel = cssModel;
     44     this._workspace = workspace;
     45     this._networkWorkspaceBinding = networkWorkspaceBinding;
     46     this._addingRevisionCounter = 0;
     47     this._reset();
     48     WebInspector.fileManager.addEventListener(WebInspector.FileManager.EventTypes.SavedURL, this._fileSaveFinished, this);
     49     WebInspector.settings.cssSourceMapsEnabled.addChangeListener(this._toggleSourceMapSupport, this)
     50     this._cssModel.addEventListener(WebInspector.CSSStyleModel.Events.StyleSheetChanged, this._styleSheetChanged, this);
     51     this._workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeAdded, this._uiSourceCodeAdded, this);
     52     this._workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeContentCommitted, this._uiSourceCodeContentCommitted, this);
     53     this._workspace.addEventListener(WebInspector.Workspace.Events.ProjectRemoved, this._reset, this);
     54 }
     55 
     56 WebInspector.SASSSourceMapping.prototype = {
     57     /**
     58      * @param {!WebInspector.Event} event
     59      */
     60     _styleSheetChanged: function(event)
     61     {
     62         var id = /** @type {!CSSAgent.StyleSheetId} */ (event.data.styleSheetId);
     63         if (this._addingRevisionCounter) {
     64             --this._addingRevisionCounter;
     65             return;
     66         }
     67         var header = this._cssModel.styleSheetHeaderForId(id);
     68         if (!header)
     69             return;
     70 
     71         this.removeHeader(header);
     72     },
     73 
     74     /**
     75      * @param {!WebInspector.Event} event
     76      */
     77     _toggleSourceMapSupport: function(event)
     78     {
     79         var enabled = /** @type {boolean} */ (event.data);
     80         var headers = this._cssModel.styleSheetHeaders();
     81         for (var i = 0; i < headers.length; ++i) {
     82             if (enabled)
     83                 this.addHeader(headers[i]);
     84             else
     85                 this.removeHeader(headers[i]);
     86         }
     87     },
     88 
     89     /**
     90      * @param {!WebInspector.Event} event
     91      */
     92     _fileSaveFinished: function(event)
     93     {
     94         var sassURL = /** @type {string} */ (event.data);
     95         this._sassFileSaved(sassURL, false);
     96     },
     97 
     98     /**
     99      * @param {string} headerName
    100      * @param {!NetworkAgent.Headers} headers
    101      * @return {?string}
    102      */
    103     _headerValue: function(headerName, headers)
    104     {
    105         headerName = headerName.toLowerCase();
    106         var value = null;
    107         for (var name in headers) {
    108             if (name.toLowerCase() === headerName) {
    109                 value = headers[name];
    110                 break;
    111             }
    112         }
    113         return value;
    114     },
    115 
    116     /**
    117      * @param {!NetworkAgent.Headers} headers
    118      * @return {?Date}
    119      */
    120     _lastModified: function(headers)
    121     {
    122         var lastModifiedHeader = this._headerValue("last-modified", headers);
    123         if (!lastModifiedHeader)
    124             return null;
    125         var lastModified = new Date(lastModifiedHeader);
    126         if (isNaN(lastModified.getTime()))
    127             return null;
    128         return lastModified;
    129     },
    130 
    131     /**
    132      * @param {!NetworkAgent.Headers} headers
    133      * @param {string} url
    134      * @return {?Date}
    135      */
    136     _checkLastModified: function(headers, url)
    137     {
    138         var lastModified = this._lastModified(headers);
    139         if (lastModified)
    140             return lastModified;
    141 
    142         var etagMessage = this._headerValue("etag", headers) ? ", \"ETag\" response header found instead" : "";
    143         var message = String.sprintf("The \"Last-Modified\" response header is missing or invalid for %s%s. The CSS auto-reload functionality will not work correctly.", url, etagMessage);
    144         this._cssModel.target().consoleModel.log(message);
    145         return null;
    146     },
    147 
    148     /**
    149      * @param {string} sassURL
    150      * @param {boolean} wasLoadedFromFileSystem
    151      */
    152     _sassFileSaved: function(sassURL, wasLoadedFromFileSystem)
    153     {
    154         var cssURLs = this._cssURLsForSASSURL[sassURL];
    155         if (!cssURLs)
    156             return;
    157         if (!WebInspector.settings.cssReloadEnabled.get())
    158             return;
    159 
    160         var sassFile = this._workspace.uiSourceCodeForURL(sassURL);
    161         console.assert(sassFile);
    162         if (wasLoadedFromFileSystem)
    163             sassFile.requestMetadata(metadataReceived.bind(this));
    164         else
    165             NetworkAgent.loadResourceForFrontend(WebInspector.resourceTreeModel.mainFrame.id, sassURL, undefined, sassLoadedViaNetwork.bind(this));
    166 
    167         /**
    168          * @param {?Protocol.Error} error
    169          * @param {number} statusCode
    170          * @param {!NetworkAgent.Headers} headers
    171          * @param {string} content
    172          * @this {WebInspector.SASSSourceMapping}
    173          */
    174         function sassLoadedViaNetwork(error, statusCode, headers, content)
    175         {
    176             if (error || statusCode >= 400) {
    177                 console.error("Could not load content for " + sassURL + " : " + (error || ("HTTP status code: " + statusCode)));
    178                 return;
    179             }
    180             var lastModified = this._checkLastModified(headers, sassURL);
    181             if (!lastModified)
    182                 return;
    183             metadataReceived.call(this, lastModified);
    184         }
    185 
    186         /**
    187          * @param {?Date} timestamp
    188          * @this {WebInspector.SASSSourceMapping}
    189          */
    190         function metadataReceived(timestamp)
    191         {
    192             if (!timestamp)
    193                 return;
    194 
    195             var now = Date.now();
    196             var deadlineMs = now + this.pollPeriodMs;
    197             var pollData = this._pollDataForSASSURL[sassURL];
    198             if (pollData) {
    199                 var dataByURL = pollData.dataByURL;
    200                 for (var url in dataByURL)
    201                     clearTimeout(dataByURL[url].timer);
    202             }
    203             pollData = { dataByURL: {}, deadlineMs: deadlineMs, sassTimestamp: timestamp };
    204             this._pollDataForSASSURL[sassURL] = pollData;
    205             for (var i = 0; i < cssURLs.length; ++i) {
    206                 pollData.dataByURL[cssURLs[i]] = { previousPoll: now };
    207                 this._pollCallback(cssURLs[i], sassURL, false);
    208             }
    209         }
    210     },
    211 
    212     /**
    213      * @param {string} cssURL
    214      * @param {string} sassURL
    215      * @param {boolean} stopPolling
    216      */
    217     _pollCallback: function(cssURL, sassURL, stopPolling)
    218     {
    219         var now;
    220         var pollData = this._pollDataForSASSURL[sassURL];
    221         if (!pollData)
    222             return;
    223 
    224         if (stopPolling || (now = new Date().getTime()) > pollData.deadlineMs) {
    225             delete pollData.dataByURL[cssURL];
    226             if (!Object.keys(pollData.dataByURL).length)
    227                 delete this._pollDataForSASSURL[sassURL];
    228             return;
    229         }
    230         var nextPoll = this.pollIntervalMs + pollData.dataByURL[cssURL].previousPoll;
    231         var remainingTimeoutMs = Math.max(0, nextPoll - now);
    232         pollData.dataByURL[cssURL].previousPoll = now + remainingTimeoutMs;
    233         pollData.dataByURL[cssURL].timer = setTimeout(this._reloadCSS.bind(this, cssURL, sassURL, this._pollCallback.bind(this)), remainingTimeoutMs);
    234     },
    235 
    236     /**
    237      * @param {string} cssURL
    238      * @param {string} sassURL
    239      * @param {function(string, string, boolean)} callback
    240      */
    241     _reloadCSS: function(cssURL, sassURL, callback)
    242     {
    243         var cssUISourceCode = this._workspace.uiSourceCodeForURL(cssURL);
    244         if (!cssUISourceCode) {
    245             WebInspector.messageSink.addMessage(WebInspector.UIString("%s resource missing. Please reload the page.", cssURL));
    246             callback(cssURL, sassURL, true);
    247             return;
    248         }
    249 
    250         if (this._workspace.hasMappingForURL(sassURL))
    251             this._reloadCSSFromFileSystem(cssUISourceCode, sassURL, callback);
    252         else
    253             this._reloadCSSFromNetwork(cssUISourceCode, sassURL, callback);
    254     },
    255 
    256     /**
    257      * @param {!WebInspector.UISourceCode} cssUISourceCode
    258      * @param {string} sassURL
    259      * @param {function(string, string, boolean)} callback
    260      */
    261     _reloadCSSFromNetwork: function(cssUISourceCode, sassURL, callback)
    262     {
    263         var cssURL = cssUISourceCode.url;
    264         var data = this._pollDataForSASSURL[sassURL];
    265         if (!data) {
    266             callback(cssURL, sassURL, true);
    267             return;
    268         }
    269         var headers = { "if-modified-since": new Date(data.sassTimestamp.getTime() - 1000).toUTCString() };
    270         NetworkAgent.loadResourceForFrontend(WebInspector.resourceTreeModel.mainFrame.id, cssURL, headers, contentLoaded.bind(this));
    271 
    272         /**
    273          * @param {?Protocol.Error} error
    274          * @param {number} statusCode
    275          * @param {!NetworkAgent.Headers} headers
    276          * @param {string} content
    277          * @this {WebInspector.SASSSourceMapping}
    278          */
    279         function contentLoaded(error, statusCode, headers, content)
    280         {
    281             if (error || statusCode >= 400) {
    282                 console.error("Could not load content for " + cssURL + " : " + (error || ("HTTP status code: " + statusCode)));
    283                 callback(cssURL, sassURL, true);
    284                 return;
    285             }
    286             if (!this._pollDataForSASSURL[sassURL]) {
    287                 callback(cssURL, sassURL, true);
    288                 return;
    289             }
    290             if (statusCode === 304) {
    291                 callback(cssURL, sassURL, false);
    292                 return;
    293             }
    294             var lastModified = this._checkLastModified(headers, cssURL);
    295             if (!lastModified) {
    296                 callback(cssURL, sassURL, true);
    297                 return;
    298             }
    299             if (lastModified.getTime() < data.sassTimestamp.getTime()) {
    300                 callback(cssURL, sassURL, false);
    301                 return;
    302             }
    303             this._updateCSSRevision(cssUISourceCode, content, sassURL, callback);
    304         }
    305     },
    306 
    307     /**
    308      * @param {!WebInspector.UISourceCode} cssUISourceCode
    309      * @param {string} content
    310      * @param {string} sassURL
    311      * @param {function(string, string, boolean)} callback
    312      */
    313     _updateCSSRevision: function(cssUISourceCode, content, sassURL, callback)
    314     {
    315         ++this._addingRevisionCounter;
    316         cssUISourceCode.addRevision(content);
    317         this._cssUISourceCodeUpdated(cssUISourceCode.url, sassURL, callback);
    318     },
    319 
    320     /**
    321      * @param {!WebInspector.UISourceCode} cssUISourceCode
    322      * @param {string} sassURL
    323      * @param {function(string, string, boolean)} callback
    324      */
    325     _reloadCSSFromFileSystem: function(cssUISourceCode, sassURL, callback)
    326     {
    327         cssUISourceCode.requestMetadata(metadataCallback.bind(this));
    328 
    329         /**
    330          * @param {?Date} timestamp
    331          * @this {WebInspector.SASSSourceMapping}
    332          */
    333         function metadataCallback(timestamp)
    334         {
    335             var cssURL = cssUISourceCode.url;
    336             if (!timestamp) {
    337                 callback(cssURL, sassURL, false);
    338                 return;
    339             }
    340             var cssTimestamp = timestamp.getTime();
    341             var pollData = this._pollDataForSASSURL[sassURL];
    342             if (!pollData) {
    343                 callback(cssURL, sassURL, true);
    344                 return;
    345             }
    346 
    347             if (cssTimestamp < pollData.sassTimestamp.getTime()) {
    348                 callback(cssURL, sassURL, false);
    349                 return;
    350             }
    351 
    352             cssUISourceCode.requestOriginalContent(contentCallback.bind(this));
    353 
    354             /**
    355              * @param {?string} content
    356              * @this {WebInspector.SASSSourceMapping}
    357              */
    358             function contentCallback(content)
    359             {
    360                 // Empty string is a valid value, null means error.
    361                 if (content === null)
    362                     return;
    363                 this._updateCSSRevision(cssUISourceCode, content, sassURL, callback);
    364             }
    365         }
    366     },
    367 
    368     /**
    369      * @param {string} cssURL
    370      * @param {string} sassURL
    371      * @param {function(string, string, boolean)} callback
    372      */
    373     _cssUISourceCodeUpdated: function(cssURL, sassURL, callback)
    374     {
    375         var completeSourceMapURL = this._completeSourceMapURLForCSSURL[cssURL];
    376         if (!completeSourceMapURL)
    377             return;
    378         var ids = this._cssModel.styleSheetIdsForURL(cssURL);
    379         if (!ids)
    380             return;
    381         var headers = [];
    382         for (var i = 0; i < ids.length; ++i)
    383             headers.push(this._cssModel.styleSheetHeaderForId(ids[i]));
    384         for (var i = 0; i < ids.length; ++i)
    385             this._loadSourceMapAndBindUISourceCode(headers, true, completeSourceMapURL);
    386         callback(cssURL, sassURL, true);
    387     },
    388 
    389     /**
    390      * @param {!WebInspector.CSSStyleSheetHeader} header
    391      */
    392     addHeader: function(header)
    393     {
    394         if (!header.sourceMapURL || !header.sourceURL || header.isInline || !WebInspector.settings.cssSourceMapsEnabled.get())
    395             return;
    396         var completeSourceMapURL = WebInspector.ParsedURL.completeURL(header.sourceURL, header.sourceMapURL);
    397         if (!completeSourceMapURL)
    398             return;
    399         this._completeSourceMapURLForCSSURL[header.sourceURL] = completeSourceMapURL;
    400         this._loadSourceMapAndBindUISourceCode([header], false, completeSourceMapURL);
    401     },
    402 
    403     /**
    404      * @param {!WebInspector.CSSStyleSheetHeader} header
    405      */
    406     removeHeader: function(header)
    407     {
    408         var sourceURL = header.sourceURL;
    409         if (!sourceURL || !header.sourceMapURL || header.isInline || !this._completeSourceMapURLForCSSURL[sourceURL])
    410             return;
    411         delete this._sourceMapByStyleSheetURL[sourceURL];
    412         delete this._completeSourceMapURLForCSSURL[sourceURL];
    413         for (var sassURL in this._cssURLsForSASSURL) {
    414             var urls = this._cssURLsForSASSURL[sassURL];
    415             urls.remove(sourceURL);
    416             if (!urls.length)
    417                 delete this._cssURLsForSASSURL[sassURL];
    418         }
    419         var completeSourceMapURL = WebInspector.ParsedURL.completeURL(sourceURL, header.sourceMapURL);
    420         if (completeSourceMapURL)
    421             delete this._sourceMapByURL[completeSourceMapURL];
    422         header.updateLocations();
    423     },
    424 
    425     /**
    426      * @param {!Array.<!WebInspector.CSSStyleSheetHeader>} headersWithSameSourceURL
    427      * @param {boolean} forceRebind
    428      * @param {string} completeSourceMapURL
    429      */
    430     _loadSourceMapAndBindUISourceCode: function(headersWithSameSourceURL, forceRebind, completeSourceMapURL)
    431     {
    432         console.assert(headersWithSameSourceURL.length);
    433         var sourceURL = headersWithSameSourceURL[0].sourceURL;
    434         this._loadSourceMapForStyleSheet(completeSourceMapURL, sourceURL, forceRebind, sourceMapLoaded.bind(this));
    435 
    436         /**
    437          * @param {?WebInspector.SourceMap} sourceMap
    438          * @this {WebInspector.SASSSourceMapping}
    439          */
    440         function sourceMapLoaded(sourceMap)
    441         {
    442             if (!sourceMap)
    443                 return;
    444 
    445             this._sourceMapByStyleSheetURL[sourceURL] = sourceMap;
    446             for (var i = 0; i < headersWithSameSourceURL.length; ++i) {
    447                 if (forceRebind)
    448                     headersWithSameSourceURL[i].updateLocations();
    449                 else
    450                     this._bindUISourceCode(headersWithSameSourceURL[i], sourceMap);
    451             }
    452         }
    453     },
    454 
    455     /**
    456      * @param {string} cssURL
    457      * @param {string} sassURL
    458      */
    459     _addCSSURLforSASSURL: function(cssURL, sassURL)
    460     {
    461         var cssURLs;
    462         if (this._cssURLsForSASSURL.hasOwnProperty(sassURL))
    463             cssURLs = this._cssURLsForSASSURL[sassURL];
    464         else {
    465             cssURLs = [];
    466             this._cssURLsForSASSURL[sassURL] = cssURLs;
    467         }
    468         if (cssURLs.indexOf(cssURL) === -1)
    469             cssURLs.push(cssURL);
    470     },
    471 
    472     /**
    473      * @param {string} completeSourceMapURL
    474      * @param {string} completeStyleSheetURL
    475      * @param {boolean} forceReload
    476      * @param {function(?WebInspector.SourceMap)} callback
    477      */
    478     _loadSourceMapForStyleSheet: function(completeSourceMapURL, completeStyleSheetURL, forceReload, callback)
    479     {
    480         var sourceMap = this._sourceMapByURL[completeSourceMapURL];
    481         if (sourceMap && !forceReload) {
    482             callback(sourceMap);
    483             return;
    484         }
    485 
    486         var pendingCallbacks = this._pendingSourceMapLoadingCallbacks[completeSourceMapURL];
    487         if (pendingCallbacks) {
    488             pendingCallbacks.push(callback);
    489             return;
    490         }
    491 
    492         pendingCallbacks = [callback];
    493         this._pendingSourceMapLoadingCallbacks[completeSourceMapURL] = pendingCallbacks;
    494 
    495         WebInspector.SourceMap.load(completeSourceMapURL, completeStyleSheetURL, sourceMapLoaded.bind(this));
    496 
    497         /**
    498          * @param {?WebInspector.SourceMap} sourceMap
    499          * @this {WebInspector.SASSSourceMapping}
    500          */
    501         function sourceMapLoaded(sourceMap)
    502         {
    503             var callbacks = this._pendingSourceMapLoadingCallbacks[completeSourceMapURL];
    504             delete this._pendingSourceMapLoadingCallbacks[completeSourceMapURL];
    505             if (!callbacks)
    506                 return;
    507             if (sourceMap)
    508                 this._sourceMapByURL[completeSourceMapURL] = sourceMap;
    509             else
    510                 delete this._sourceMapByURL[completeSourceMapURL];
    511             for (var i = 0; i < callbacks.length; ++i)
    512                 callbacks[i](sourceMap);
    513         }
    514     },
    515 
    516     /**
    517      * @param {!WebInspector.CSSStyleSheetHeader} header
    518      * @param {!WebInspector.SourceMap} sourceMap
    519      */
    520     _bindUISourceCode: function(header, sourceMap)
    521     {
    522         header.pushSourceMapping(this);
    523         var rawURL = header.sourceURL;
    524         var sources = sourceMap.sources();
    525         for (var i = 0; i < sources.length; ++i) {
    526             var url = sources[i];
    527             this._addCSSURLforSASSURL(rawURL, url);
    528             if (!this._workspace.hasMappingForURL(url) && !this._workspace.uiSourceCodeForURL(url)) {
    529                 var contentProvider = sourceMap.sourceContentProvider(url, WebInspector.resourceTypes.Stylesheet);
    530                 this._networkWorkspaceBinding.addFileForURL(url, contentProvider);
    531             }
    532         }
    533     },
    534 
    535     /**
    536      * @param {!WebInspector.RawLocation} rawLocation
    537      * @return {?WebInspector.UILocation}
    538      */
    539     rawLocationToUILocation: function(rawLocation)
    540     {
    541         var location = /** @type WebInspector.CSSLocation */ (rawLocation);
    542         var entry;
    543         var sourceMap = this._sourceMapByStyleSheetURL[location.url];
    544         if (!sourceMap)
    545             return null;
    546         entry = sourceMap.findEntry(location.lineNumber, location.columnNumber);
    547         if (!entry || entry.length === 2)
    548             return null;
    549         var uiSourceCode = this._workspace.uiSourceCodeForURL(entry[2]);
    550         if (!uiSourceCode)
    551             return null;
    552         return uiSourceCode.uiLocation(entry[3], entry[4]);
    553     },
    554 
    555     /**
    556      * @param {!WebInspector.UISourceCode} uiSourceCode
    557      * @param {number} lineNumber
    558      * @param {number} columnNumber
    559      * @return {!WebInspector.RawLocation}
    560      */
    561     uiLocationToRawLocation: function(uiSourceCode, lineNumber, columnNumber)
    562     {
    563         // FIXME: Implement this when ui -> raw mapping has clients.
    564         return new WebInspector.CSSLocation(this._cssModel.target(), uiSourceCode.url || "", lineNumber, columnNumber);
    565     },
    566 
    567     /**
    568      * @return {boolean}
    569      */
    570     isIdentity: function()
    571     {
    572         return false;
    573     },
    574 
    575     /**
    576      * @return {!WebInspector.Target}
    577      */
    578     target: function()
    579     {
    580         return this._cssModel.target();
    581     },
    582 
    583     /**
    584      * @param {!WebInspector.Event} event
    585      */
    586     _uiSourceCodeAdded: function(event)
    587     {
    588         var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data);
    589         var cssURLs = this._cssURLsForSASSURL[uiSourceCode.url];
    590         if (!cssURLs)
    591             return;
    592         for (var i = 0; i < cssURLs.length; ++i) {
    593             var ids = this._cssModel.styleSheetIdsForURL(cssURLs[i]);
    594             for (var j = 0; j < ids.length; ++j) {
    595                 var header = this._cssModel.styleSheetHeaderForId(ids[j]);
    596                 console.assert(header);
    597                 header.updateLocations();
    598             }
    599         }
    600     },
    601 
    602     /**
    603      * @param {!WebInspector.Event} event
    604      */
    605     _uiSourceCodeContentCommitted: function(event)
    606     {
    607         var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data.uiSourceCode);
    608         if (uiSourceCode.project().type() === WebInspector.projectTypes.FileSystem)
    609             this._sassFileSaved(uiSourceCode.url, true);
    610     },
    611 
    612     _reset: function()
    613     {
    614         this._addingRevisionCounter = 0;
    615         this._completeSourceMapURLForCSSURL = {};
    616         this._cssURLsForSASSURL = {};
    617         /** @type {!Object.<string, !Array.<function(?WebInspector.SourceMap)>>} */
    618         this._pendingSourceMapLoadingCallbacks = {};
    619         /** @type {!Object.<string, !{deadlineMs: number, dataByURL: !Object.<string, !{timer: number, previousPoll: number}>}>} */
    620         this._pollDataForSASSURL = {};
    621         /** @type {!Object.<string, !WebInspector.SourceMap>} */
    622         this._sourceMapByURL = {};
    623         this._sourceMapByStyleSheetURL = {};
    624     }
    625 }
    626