Home | History | Annotate | Download | only in front_end
      1 /*
      2  * Copyright (C) 2011 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 /**
     33  * @constructor
     34  * @extends {WebInspector.Object}
     35  * @implements {WebInspector.ContentProvider}
     36  * @param {!WebInspector.Project} project
     37  * @param {string} parentPath
     38  * @param {string} name
     39  * @param {string} url
     40  * @param {!WebInspector.ResourceType} contentType
     41  * @param {boolean} isEditable
     42  */
     43 WebInspector.UISourceCode = function(project, parentPath, name, originURL, url, contentType, isEditable)
     44 {
     45     this._project = project;
     46     this._parentPath = parentPath;
     47     this._name = name;
     48     this._originURL = originURL;
     49     this._url = url;
     50     this._contentType = contentType;
     51     this._isEditable = isEditable;
     52     /** @type {!Array.<function(?string)>} */
     53     this._requestContentCallbacks = [];
     54     /** @type {!Set.<!WebInspector.LiveLocation>} */
     55     this._liveLocations = new Set();
     56     /** @type {!Array.<!WebInspector.PresentationConsoleMessage>} */
     57     this._consoleMessages = [];
     58 
     59     /** @type {!Array.<!WebInspector.Revision>} */
     60     this.history = [];
     61     if (this.isEditable() && this._url)
     62         this._restoreRevisionHistory();
     63     this._formatterMapping = new WebInspector.IdentityFormatterSourceMapping();
     64 }
     65 
     66 WebInspector.UISourceCode.Events = {
     67     FormattedChanged: "FormattedChanged",
     68     WorkingCopyChanged: "WorkingCopyChanged",
     69     WorkingCopyCommitted: "WorkingCopyCommitted",
     70     TitleChanged: "TitleChanged",
     71     SavedStateUpdated: "SavedStateUpdated",
     72     ConsoleMessageAdded: "ConsoleMessageAdded",
     73     ConsoleMessageRemoved: "ConsoleMessageRemoved",
     74     ConsoleMessagesCleared: "ConsoleMessagesCleared",
     75     SourceMappingChanged: "SourceMappingChanged",
     76 }
     77 
     78 WebInspector.UISourceCode.prototype = {
     79     /**
     80      * @return {string}
     81      */
     82     get url()
     83     {
     84         return this._url;
     85     },
     86 
     87     /**
     88      * @return {string}
     89      */
     90     name: function()
     91     {
     92         return this._name;
     93     },
     94 
     95     /**
     96      * @return {string}
     97      */
     98     parentPath: function()
     99     {
    100         return this._parentPath;
    101     },
    102 
    103     /**
    104      * @return {string}
    105      */
    106     path: function()
    107     {
    108         return this._parentPath ? this._parentPath + "/" + this._name : this._name;
    109     },
    110 
    111     /**
    112      * @return {string}
    113      */
    114     fullDisplayName: function()
    115     {
    116         return this._project.displayName() + "/" + (this._parentPath ? this._parentPath + "/" : "") + this.displayName(true);
    117     },
    118 
    119     /**
    120      * @param {boolean=} skipTrim
    121      * @return {string}
    122      */
    123     displayName: function(skipTrim)
    124     {
    125         var displayName = this.name() || WebInspector.UIString("(index)");
    126         return skipTrim ? displayName : displayName.trimEnd(100);
    127     },
    128 
    129     /**
    130      * @return {string}
    131      */
    132     uri: function()
    133     {
    134         var path = this.path();
    135         if (!this._project.id())
    136             return path;
    137         if (!path)
    138             return this._project.id();
    139         return this._project.id() + "/" + path;
    140     },
    141 
    142     /**
    143      * @return {string}
    144      */
    145     originURL: function()
    146     {
    147         return this._originURL;
    148     },
    149 
    150     /**
    151      * @return {boolean}
    152      */
    153     canRename: function()
    154     {
    155         return this._project.canRename();
    156     },
    157 
    158     /**
    159      * @param {string} newName
    160      * @param {function(boolean)} callback
    161      */
    162     rename: function(newName, callback)
    163     {
    164         this._project.rename(this, newName, innerCallback.bind(this));
    165 
    166         /**
    167          * @param {boolean} success
    168          * @param {string=} newName
    169          * @param {string=} newURL
    170          * @param {string=} newOriginURL
    171          * @param {!WebInspector.ResourceType=} newContentType
    172          * @this {WebInspector.UISourceCode}
    173          */
    174         function innerCallback(success, newName, newURL, newOriginURL, newContentType)
    175         {
    176             if (success)
    177                 this._updateName(/** @type {string} */ (newName), /** @type {string} */ (newURL), /** @type {string} */ (newOriginURL), /** @type {!WebInspector.ResourceType} */ (newContentType));
    178             callback(success);
    179         }
    180     },
    181 
    182     /**
    183      * @param {string} name
    184      * @param {string} url
    185      * @param {string} originURL
    186      * @param {!WebInspector.ResourceType=} contentType
    187      */
    188     _updateName: function(name, url, originURL, contentType)
    189     {
    190         var oldURI = this.uri();
    191         this._name = name;
    192         if (url)
    193             this._url = url;
    194         if (originURL)
    195             this._originURL = originURL;
    196         if (contentType)
    197             this._contentType = contentType;
    198         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.TitleChanged, oldURI);
    199     },
    200 
    201     /**
    202      * @return {string}
    203      */
    204     contentURL: function()
    205     {
    206         return this.originURL();
    207     },
    208 
    209     /**
    210      * @return {!WebInspector.ResourceType}
    211      */
    212     contentType: function()
    213     {
    214         return this._contentType;
    215     },
    216 
    217     /**
    218      * @return {?WebInspector.ScriptFile}
    219      */
    220     scriptFile: function()
    221     {
    222         return this._scriptFile;
    223     },
    224 
    225     /**
    226      * @param {?WebInspector.ScriptFile} scriptFile
    227      */
    228     setScriptFile: function(scriptFile)
    229     {
    230         this._scriptFile = scriptFile;
    231     },
    232 
    233     /**
    234      * @return {!WebInspector.Project}
    235      */
    236     project: function()
    237     {
    238         return this._project;
    239     },
    240 
    241     /**
    242      * @param {function(?Date, ?number)} callback
    243      */
    244     requestMetadata: function(callback)
    245     {
    246         this._project.requestMetadata(this, callback);
    247     },
    248 
    249     /**
    250      * @param {function(?string)} callback
    251      */
    252     requestContent: function(callback)
    253     {
    254         if (this._content || this._contentLoaded) {
    255             callback(this._content);
    256             return;
    257         }
    258         this._requestContentCallbacks.push(callback);
    259         if (this._requestContentCallbacks.length === 1)
    260             this._project.requestFileContent(this, this._fireContentAvailable.bind(this));
    261     },
    262 
    263     /**
    264      * @param {function()=} callback
    265      */
    266     checkContentUpdated: function(callback)
    267     {
    268         if (!this._project.canSetFileContent())
    269             return;
    270         if (this._checkingContent)
    271             return;
    272         this._checkingContent = true;
    273         this._project.requestFileContent(this, contentLoaded.bind(this));
    274 
    275         /**
    276          * @param {?string} updatedContent
    277          * @this {WebInspector.UISourceCode}
    278          */
    279         function contentLoaded(updatedContent)
    280         {
    281             if (updatedContent === null) {
    282                 var workingCopy = this.workingCopy();
    283                 this._commitContent("", false);
    284                 this.setWorkingCopy(workingCopy);
    285                 delete this._checkingContent;
    286                 if (callback)
    287                     callback();
    288                 return;
    289             }
    290             if (typeof this._lastAcceptedContent === "string" && this._lastAcceptedContent === updatedContent) {
    291                 delete this._checkingContent;
    292                 if (callback)
    293                     callback();
    294                 return;
    295             }
    296             if (this._content === updatedContent) {
    297                 delete this._lastAcceptedContent;
    298                 delete this._checkingContent;
    299                 if (callback)
    300                     callback();
    301                 return;
    302             }
    303 
    304             if (!this.isDirty()) {
    305                 this._commitContent(updatedContent, false);
    306                 delete this._checkingContent;
    307                 if (callback)
    308                     callback();
    309                 return;
    310             }
    311 
    312             var shouldUpdate = window.confirm(WebInspector.UIString("This file was changed externally. Would you like to reload it?"));
    313             if (shouldUpdate)
    314                 this._commitContent(updatedContent, false);
    315             else
    316                 this._lastAcceptedContent = updatedContent;
    317             delete this._checkingContent;
    318             if (callback)
    319                 callback();
    320         }
    321     },
    322 
    323     /**
    324      * @param {function(?string)} callback
    325      */
    326     requestOriginalContent: function(callback)
    327     {
    328         this._project.requestFileContent(this, callback);
    329     },
    330 
    331     /**
    332      * @param {string} content
    333      * @param {boolean} shouldSetContentInProject
    334      */
    335     _commitContent: function(content, shouldSetContentInProject)
    336     {
    337         delete this._lastAcceptedContent;
    338         this._content = content;
    339         this._contentLoaded = true;
    340 
    341         var lastRevision = this.history.length ? this.history[this.history.length - 1] : null;
    342         if (!lastRevision || lastRevision._content !== this._content) {
    343             var revision = new WebInspector.Revision(this, this._content, new Date());
    344             this.history.push(revision);
    345             revision._persist();
    346         }
    347 
    348         this._innerResetWorkingCopy();
    349         this._hasCommittedChanges = true;
    350         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyCommitted);
    351         if (this._url && WebInspector.fileManager.isURLSaved(this._url))
    352             this._saveURLWithFileManager(false, this._content);
    353         if (shouldSetContentInProject)
    354             this._project.setFileContent(this, this._content, function() { });
    355     },
    356 
    357     /**
    358      * @param {boolean} forceSaveAs
    359      * @param {?string} content
    360      */
    361     _saveURLWithFileManager: function(forceSaveAs, content)
    362     {
    363         WebInspector.fileManager.save(this._url, content, forceSaveAs, callback.bind(this));
    364         WebInspector.fileManager.close(this._url);
    365 
    366         /**
    367          * @param {boolean} accepted
    368          * @this {WebInspector.UISourceCode}
    369          */
    370         function callback(accepted)
    371         {
    372             if (!accepted)
    373                 return;
    374             this._savedWithFileManager = true;
    375             this.dispatchEventToListeners(WebInspector.UISourceCode.Events.SavedStateUpdated);
    376         }
    377     },
    378 
    379     /**
    380      * @param {boolean} forceSaveAs
    381      */
    382     saveToFileSystem: function(forceSaveAs)
    383     {
    384         if (this.isDirty()) {
    385             this._saveURLWithFileManager(forceSaveAs, this.workingCopy());
    386             this.commitWorkingCopy(function() { });
    387             return;
    388         }
    389         this.requestContent(this._saveURLWithFileManager.bind(this, forceSaveAs));
    390     },
    391 
    392     /**
    393      * @return {boolean}
    394      */
    395     hasUnsavedCommittedChanges: function()
    396     {
    397         if (this._savedWithFileManager || this.project().canSetFileContent() || !this._isEditable)
    398             return false;
    399         if (WebInspector.extensionServer.hasSubscribers(WebInspector.extensionAPI.Events.ResourceContentCommitted))
    400             return false;
    401         return !!this._hasCommittedChanges;
    402     },
    403 
    404     /**
    405      * @param {string} content
    406      */
    407     addRevision: function(content)
    408     {
    409         this._commitContent(content, true);
    410     },
    411 
    412     _restoreRevisionHistory: function()
    413     {
    414         if (!window.localStorage)
    415             return;
    416 
    417         var registry = WebInspector.Revision._revisionHistoryRegistry();
    418         var historyItems = registry[this.url];
    419         if (!historyItems)
    420             return;
    421 
    422         function filterOutStale(historyItem)
    423         {
    424             // FIXME: Main frame might not have been loaded yet when uiSourceCodes for snippets are created.
    425             if (!WebInspector.resourceTreeModel.mainFrame)
    426                 return false;
    427             return historyItem.loaderId === WebInspector.resourceTreeModel.mainFrame.loaderId;
    428         }
    429 
    430         historyItems = historyItems.filter(filterOutStale);
    431         if (!historyItems.length)
    432             return;
    433 
    434         for (var i = 0; i < historyItems.length; ++i) {
    435             var content = window.localStorage[historyItems[i].key];
    436             var timestamp = new Date(historyItems[i].timestamp);
    437             var revision = new WebInspector.Revision(this, content, timestamp);
    438             this.history.push(revision);
    439         }
    440         this._content = this.history[this.history.length - 1].content;
    441         this._hasCommittedChanges = true;
    442         this._contentLoaded = true;
    443     },
    444 
    445     _clearRevisionHistory: function()
    446     {
    447         if (!window.localStorage)
    448             return;
    449 
    450         var registry = WebInspector.Revision._revisionHistoryRegistry();
    451         var historyItems = registry[this.url];
    452         for (var i = 0; historyItems && i < historyItems.length; ++i)
    453             delete window.localStorage[historyItems[i].key];
    454         delete registry[this.url];
    455         window.localStorage["revision-history"] = JSON.stringify(registry);
    456     },
    457 
    458     revertToOriginal: function()
    459     {
    460         /**
    461          * @this {WebInspector.UISourceCode}
    462          * @param {?string} content
    463          */
    464         function callback(content)
    465         {
    466             if (typeof content !== "string")
    467                 return;
    468 
    469             this.addRevision(content);
    470         }
    471 
    472         this.requestOriginalContent(callback.bind(this));
    473 
    474         WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, {
    475             action: WebInspector.UserMetrics.UserActionNames.ApplyOriginalContent,
    476             url: this.url
    477         });
    478     },
    479 
    480     /**
    481      * @param {function(!WebInspector.UISourceCode)} callback
    482      */
    483     revertAndClearHistory: function(callback)
    484     {
    485         /**
    486          * @this {WebInspector.UISourceCode}
    487          * @param {?string} content
    488          */
    489         function revert(content)
    490         {
    491             if (typeof content !== "string")
    492                 return;
    493 
    494             this.addRevision(content);
    495             this._clearRevisionHistory();
    496             this.history = [];
    497             callback(this);
    498         }
    499 
    500         this.requestOriginalContent(revert.bind(this));
    501 
    502         WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, {
    503             action: WebInspector.UserMetrics.UserActionNames.RevertRevision,
    504             url: this.url
    505         });
    506     },
    507 
    508     /**
    509      * @return {boolean}
    510      */
    511     isEditable: function()
    512     {
    513         return this._isEditable;
    514     },
    515 
    516     /**
    517      * @return {string}
    518      */
    519     workingCopy: function()
    520     {
    521         if (this._workingCopyGetter) {
    522             this._workingCopy = this._workingCopyGetter();
    523             delete this._workingCopyGetter;
    524         }
    525         if (this.isDirty())
    526             return this._workingCopy;
    527         return this._content;
    528     },
    529 
    530     resetWorkingCopy: function()
    531     {
    532         this._innerResetWorkingCopy();
    533         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged);
    534     },
    535 
    536     _innerResetWorkingCopy: function()
    537     {
    538         delete this._workingCopy;
    539         delete this._workingCopyGetter;
    540     },
    541 
    542     /**
    543      * @param {string} newWorkingCopy
    544      */
    545     setWorkingCopy: function(newWorkingCopy)
    546     {
    547         this._workingCopy = newWorkingCopy;
    548         delete this._workingCopyGetter;
    549         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged);
    550     },
    551 
    552     setWorkingCopyGetter: function(workingCopyGetter)
    553     {
    554         this._workingCopyGetter = workingCopyGetter;
    555         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged);
    556     },
    557 
    558     removeWorkingCopyGetter: function()
    559     {
    560         if (!this._workingCopyGetter)
    561             return;
    562         this._workingCopy = this._workingCopyGetter();
    563         delete this._workingCopyGetter;
    564     },
    565 
    566     /**
    567      * @param {function(?string)} callback
    568      */
    569     commitWorkingCopy: function(callback)
    570     {
    571         if (!this.isDirty()) {
    572             callback(null);
    573             return;
    574         }
    575 
    576         this._commitContent(this.workingCopy(), true);
    577         callback(null);
    578 
    579         WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, {
    580             action: WebInspector.UserMetrics.UserActionNames.FileSaved,
    581             url: this.url
    582         });
    583     },
    584 
    585     /**
    586      * @return {boolean}
    587      */
    588     isDirty: function()
    589     {
    590         return typeof this._workingCopy !== "undefined" || typeof this._workingCopyGetter !== "undefined";
    591     },
    592 
    593     /**
    594      * @return {string}
    595      */
    596     _mimeType: function()
    597     {
    598         return this.contentType().canonicalMimeType();
    599     },
    600 
    601     /**
    602      * @return {string}
    603      */
    604     highlighterType: function()
    605     {
    606         var lastIndexOfDot = this._name.lastIndexOf(".");
    607         var extension = lastIndexOfDot !== -1 ? this._name.substr(lastIndexOfDot + 1) : "";
    608         var indexOfQuestionMark = extension.indexOf("?");
    609         if (indexOfQuestionMark !== -1)
    610             extension = extension.substr(0, indexOfQuestionMark);
    611         var mimeType = WebInspector.ResourceType.mimeTypesForExtensions[extension.toLowerCase()];
    612         return mimeType || this.contentType().canonicalMimeType();
    613     },
    614 
    615     /**
    616      * @return {?string}
    617      */
    618     content: function()
    619     {
    620         return this._content;
    621     },
    622 
    623     /**
    624      * @param {string} query
    625      * @param {boolean} caseSensitive
    626      * @param {boolean} isRegex
    627      * @param {function(!Array.<!WebInspector.ContentProvider.SearchMatch>)} callback
    628      */
    629     searchInContent: function(query, caseSensitive, isRegex, callback)
    630     {
    631         var content = this.content();
    632         if (content) {
    633             var provider = new WebInspector.StaticContentProvider(this.contentType(), content);
    634             provider.searchInContent(query, caseSensitive, isRegex, callback);
    635             return;
    636         }
    637 
    638         this._project.searchInFileContent(this, query, caseSensitive, isRegex, callback);
    639     },
    640 
    641     /**
    642      * @param {?string} content
    643      */
    644     _fireContentAvailable: function(content)
    645     {
    646         this._contentLoaded = true;
    647         this._content = content;
    648 
    649         var callbacks = this._requestContentCallbacks.slice();
    650         this._requestContentCallbacks = [];
    651         for (var i = 0; i < callbacks.length; ++i)
    652             callbacks[i](content);
    653 
    654         if (this._formatOnLoad) {
    655             delete this._formatOnLoad;
    656             this.setFormatted(true);
    657         }
    658     },
    659 
    660     /**
    661      * @return {boolean}
    662      */
    663     contentLoaded: function()
    664     {
    665         return this._contentLoaded;
    666     },
    667 
    668     /**
    669      * @param {number} lineNumber
    670      * @param {number} columnNumber
    671      * @return {?WebInspector.RawLocation}
    672      */
    673     uiLocationToRawLocation: function(lineNumber, columnNumber)
    674     {
    675         if (!this._sourceMapping)
    676             return null;
    677         var location = this._formatterMapping.formattedToOriginal(lineNumber, columnNumber);
    678         return this._sourceMapping.uiLocationToRawLocation(this, location[0], location[1]);
    679     },
    680 
    681     /**
    682      * @param {!WebInspector.LiveLocation} liveLocation
    683      */
    684     addLiveLocation: function(liveLocation)
    685     {
    686         this._liveLocations.add(liveLocation);
    687     },
    688 
    689     /**
    690      * @param {!WebInspector.LiveLocation} liveLocation
    691      */
    692     removeLiveLocation: function(liveLocation)
    693     {
    694         this._liveLocations.remove(liveLocation);
    695     },
    696 
    697     updateLiveLocations: function()
    698     {
    699         var items = this._liveLocations.items();
    700         for (var i = 0; i < items.length; ++i)
    701             items[i].update();
    702     },
    703 
    704     /**
    705      * @param {!WebInspector.UILocation} uiLocation
    706      */
    707     overrideLocation: function(uiLocation)
    708     {
    709         var location = this._formatterMapping.originalToFormatted(uiLocation.lineNumber, uiLocation.columnNumber);
    710         uiLocation.lineNumber = location[0];
    711         uiLocation.columnNumber = location[1];
    712         return uiLocation;
    713     },
    714 
    715     /**
    716      * @return {!Array.<!WebInspector.PresentationConsoleMessage>}
    717      */
    718     consoleMessages: function()
    719     {
    720         return this._consoleMessages;
    721     },
    722 
    723     /**
    724      * @param {!WebInspector.PresentationConsoleMessage} message
    725      */
    726     consoleMessageAdded: function(message)
    727     {
    728         this._consoleMessages.push(message);
    729         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.ConsoleMessageAdded, message);
    730     },
    731 
    732     /**
    733      * @param {!WebInspector.PresentationConsoleMessage} message
    734      */
    735     consoleMessageRemoved: function(message)
    736     {
    737         this._consoleMessages.remove(message);
    738         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.ConsoleMessageRemoved, message);
    739     },
    740 
    741     consoleMessagesCleared: function()
    742     {
    743         this._consoleMessages = [];
    744         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.ConsoleMessagesCleared);
    745     },
    746 
    747     /**
    748      * @return {boolean}
    749      */
    750     formatted: function()
    751     {
    752         return !!this._formatted;
    753     },
    754 
    755     /**
    756      * @param {boolean} formatted
    757      */
    758     setFormatted: function(formatted)
    759     {
    760         if (!this.contentLoaded()) {
    761             this._formatOnLoad = formatted;
    762             return;
    763         }
    764 
    765         if (this._formatted === formatted)
    766             return;
    767 
    768         if (this.isDirty())
    769             return;
    770 
    771         this._formatted = formatted;
    772 
    773         // Re-request content
    774         this._contentLoaded = false;
    775         this._content = false;
    776         this.requestContent(didGetContent.bind(this));
    777 
    778         /**
    779          * @this {WebInspector.UISourceCode}
    780          * @param {?string} content
    781          */
    782         function didGetContent(content)
    783         {
    784             var formatter;
    785             if (!formatted)
    786                 formatter = new WebInspector.IdentityFormatter();
    787             else
    788                 formatter = WebInspector.Formatter.createFormatter(this.contentType());
    789             formatter.formatContent(this.highlighterType(), content || "", formattedChanged.bind(this));
    790 
    791             /**
    792              * @this {WebInspector.UISourceCode}
    793              * @param {string} content
    794              * @param {!WebInspector.FormatterSourceMapping} formatterMapping
    795              */
    796             function formattedChanged(content, formatterMapping)
    797             {
    798                 this._content = content;
    799                 this._innerResetWorkingCopy();
    800                 var oldFormatter = this._formatterMapping;
    801                 this._formatterMapping = formatterMapping;
    802                 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.FormattedChanged, {
    803                     content: content,
    804                     oldFormatter: oldFormatter,
    805                     newFormatter: this._formatterMapping,
    806                 });
    807                 this.updateLiveLocations();
    808             }
    809         }
    810     },
    811 
    812     /**
    813      * @return {?WebInspector.Formatter} formatter
    814      */
    815     createFormatter: function()
    816     {
    817         // overridden by subclasses.
    818         return null;
    819     },
    820 
    821     /**
    822      * @return {boolean}
    823      */
    824     hasSourceMapping: function()
    825     {
    826         return !!this._sourceMapping;
    827     },
    828 
    829     /**
    830      * @param {?WebInspector.SourceMapping} sourceMapping
    831      */
    832     setSourceMapping: function(sourceMapping)
    833     {
    834         if (this._sourceMapping === sourceMapping)
    835             return;
    836         this._sourceMapping = sourceMapping;
    837         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.SourceMappingChanged);
    838     },
    839 
    840     __proto__: WebInspector.Object.prototype
    841 }
    842 
    843 /**
    844  * @constructor
    845  * @param {!WebInspector.UISourceCode} uiSourceCode
    846  * @param {number} lineNumber
    847  * @param {number} columnNumber
    848  */
    849 WebInspector.UILocation = function(uiSourceCode, lineNumber, columnNumber)
    850 {
    851     this.uiSourceCode = uiSourceCode;
    852     this.lineNumber = lineNumber;
    853     this.columnNumber = columnNumber;
    854 }
    855 
    856 WebInspector.UILocation.prototype = {
    857     /**
    858      * @return {?WebInspector.RawLocation}
    859      */
    860     uiLocationToRawLocation: function()
    861     {
    862         return this.uiSourceCode.uiLocationToRawLocation(this.lineNumber, this.columnNumber);
    863     },
    864 
    865     /**
    866      * @return {?string}
    867      */
    868     url: function()
    869     {
    870         return this.uiSourceCode.contentURL();
    871     },
    872 
    873     /**
    874      * @return {string}
    875      */
    876     linkText: function()
    877     {
    878         var linkText = this.uiSourceCode.displayName();
    879         if (typeof this.lineNumber === "number")
    880             linkText += ":" + (this.lineNumber + 1);
    881         return linkText;
    882     }
    883 }
    884 
    885 /**
    886  * @interface
    887  */
    888 WebInspector.RawLocation = function()
    889 {
    890 }
    891 
    892 /**
    893  * @constructor
    894  * @param {!WebInspector.RawLocation} rawLocation
    895  * @param {function(!WebInspector.UILocation):(boolean|undefined)} updateDelegate
    896  */
    897 WebInspector.LiveLocation = function(rawLocation, updateDelegate)
    898 {
    899     this._rawLocation = rawLocation;
    900     this._updateDelegate = updateDelegate;
    901     this._uiSourceCodes = [];
    902 }
    903 
    904 WebInspector.LiveLocation.prototype = {
    905     update: function()
    906     {
    907         var uiLocation = this.uiLocation();
    908         if (uiLocation) {
    909             var uiSourceCode = uiLocation.uiSourceCode;
    910             if (this._uiSourceCodes.indexOf(uiSourceCode) === -1) {
    911                 uiSourceCode.addLiveLocation(this);
    912                 this._uiSourceCodes.push(uiSourceCode);
    913             }
    914             var oneTime = this._updateDelegate(uiLocation);
    915             if (oneTime)
    916                 this.dispose();
    917         }
    918     },
    919 
    920     /**
    921      * @return {!WebInspector.RawLocation}
    922      */
    923     rawLocation: function()
    924     {
    925         return this._rawLocation;
    926     },
    927 
    928     /**
    929      * @return {!WebInspector.UILocation}
    930      */
    931     uiLocation: function()
    932     {
    933         // Should be overridden by subclasses.
    934     },
    935 
    936     dispose: function()
    937     {
    938         for (var i = 0; i < this._uiSourceCodes.length; ++i)
    939             this._uiSourceCodes[i].removeLiveLocation(this);
    940         this._uiSourceCodes = [];
    941     }
    942 }
    943 
    944 /**
    945  * @constructor
    946  * @implements {WebInspector.ContentProvider}
    947  * @param {!WebInspector.UISourceCode} uiSourceCode
    948  * @param {?string|undefined} content
    949  * @param {!Date} timestamp
    950  */
    951 WebInspector.Revision = function(uiSourceCode, content, timestamp)
    952 {
    953     this._uiSourceCode = uiSourceCode;
    954     this._content = content;
    955     this._timestamp = timestamp;
    956 }
    957 
    958 WebInspector.Revision._revisionHistoryRegistry = function()
    959 {
    960     if (!WebInspector.Revision._revisionHistoryRegistryObject) {
    961         if (window.localStorage) {
    962             var revisionHistory = window.localStorage["revision-history"];
    963             try {
    964                 WebInspector.Revision._revisionHistoryRegistryObject = revisionHistory ? JSON.parse(revisionHistory) : {};
    965             } catch (e) {
    966                 WebInspector.Revision._revisionHistoryRegistryObject = {};
    967             }
    968         } else
    969             WebInspector.Revision._revisionHistoryRegistryObject = {};
    970     }
    971     return WebInspector.Revision._revisionHistoryRegistryObject;
    972 }
    973 
    974 WebInspector.Revision.filterOutStaleRevisions = function()
    975 {
    976     if (!window.localStorage)
    977         return;
    978 
    979     var registry = WebInspector.Revision._revisionHistoryRegistry();
    980     var filteredRegistry = {};
    981     for (var url in registry) {
    982         var historyItems = registry[url];
    983         var filteredHistoryItems = [];
    984         for (var i = 0; historyItems && i < historyItems.length; ++i) {
    985             var historyItem = historyItems[i];
    986             if (historyItem.loaderId === WebInspector.resourceTreeModel.mainFrame.loaderId) {
    987                 filteredHistoryItems.push(historyItem);
    988                 filteredRegistry[url] = filteredHistoryItems;
    989             } else
    990                 delete window.localStorage[historyItem.key];
    991         }
    992     }
    993     WebInspector.Revision._revisionHistoryRegistryObject = filteredRegistry;
    994 
    995     function persist()
    996     {
    997         window.localStorage["revision-history"] = JSON.stringify(filteredRegistry);
    998     }
    999 
   1000     // Schedule async storage.
   1001     setTimeout(persist, 0);
   1002 }
   1003 
   1004 WebInspector.Revision.prototype = {
   1005     /**
   1006      * @return {!WebInspector.UISourceCode}
   1007      */
   1008     get uiSourceCode()
   1009     {
   1010         return this._uiSourceCode;
   1011     },
   1012 
   1013     /**
   1014      * @return {!Date}
   1015      */
   1016     get timestamp()
   1017     {
   1018         return this._timestamp;
   1019     },
   1020 
   1021     /**
   1022      * @return {?string}
   1023      */
   1024     get content()
   1025     {
   1026         return this._content || null;
   1027     },
   1028 
   1029     revertToThis: function()
   1030     {
   1031         /**
   1032          * @param {string} content
   1033          * @this {WebInspector.Revision}
   1034          */
   1035         function revert(content)
   1036         {
   1037             if (this._uiSourceCode._content !== content)
   1038                 this._uiSourceCode.addRevision(content);
   1039         }
   1040         this.requestContent(revert.bind(this));
   1041     },
   1042 
   1043     /**
   1044      * @return {string}
   1045      */
   1046     contentURL: function()
   1047     {
   1048         return this._uiSourceCode.originURL();
   1049     },
   1050 
   1051     /**
   1052      * @return {!WebInspector.ResourceType}
   1053      */
   1054     contentType: function()
   1055     {
   1056         return this._uiSourceCode.contentType();
   1057     },
   1058 
   1059     /**
   1060      * @param {function(string)} callback
   1061      */
   1062     requestContent: function(callback)
   1063     {
   1064         callback(this._content || "");
   1065     },
   1066 
   1067     /**
   1068      * @param {string} query
   1069      * @param {boolean} caseSensitive
   1070      * @param {boolean} isRegex
   1071      * @param {function(!Array.<!WebInspector.ContentProvider.SearchMatch>)} callback
   1072      */
   1073     searchInContent: function(query, caseSensitive, isRegex, callback)
   1074     {
   1075         callback([]);
   1076     },
   1077 
   1078     _persist: function()
   1079     {
   1080         if (this._uiSourceCode.project().type() === WebInspector.projectTypes.FileSystem)
   1081             return;
   1082 
   1083         if (!window.localStorage)
   1084             return;
   1085 
   1086         var url = this.contentURL();
   1087         if (!url || url.startsWith("inspector://"))
   1088             return;
   1089 
   1090         var loaderId = WebInspector.resourceTreeModel.mainFrame.loaderId;
   1091         var timestamp = this.timestamp.getTime();
   1092         var key = "revision-history|" + url + "|" + loaderId + "|" + timestamp;
   1093 
   1094         var registry = WebInspector.Revision._revisionHistoryRegistry();
   1095 
   1096         var historyItems = registry[url];
   1097         if (!historyItems) {
   1098             historyItems = [];
   1099             registry[url] = historyItems;
   1100         }
   1101         historyItems.push({url: url, loaderId: loaderId, timestamp: timestamp, key: key});
   1102 
   1103         /**
   1104          * @this {WebInspector.Revision}
   1105          */
   1106         function persist()
   1107         {
   1108             window.localStorage[key] = this._content;
   1109             window.localStorage["revision-history"] = JSON.stringify(registry);
   1110         }
   1111 
   1112         // Schedule async storage.
   1113         setTimeout(persist.bind(this), 0);
   1114     }
   1115 }
   1116