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