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