Unreviewed, rolling out r143100.
[WebKit-https.git] / Source / WebCore / inspector / front-end / UISourceCode.js
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} path
38  * @param {string} url
39  * @param {WebInspector.ResourceType} contentType
40  * @param {boolean} isEditable
41  */
42 WebInspector.UISourceCode = function(project, path, originURL, url, contentType, isEditable)
43 {
44     this._project = project;
45     this._path = path;
46     this._originURL = originURL;
47     this._url = url;
48     this._parsedURL = new WebInspector.ParsedURL(originURL);
49     this._contentType = contentType;
50     this._isEditable = isEditable;
51     /**
52      * @type Array.<function(?string,boolean,string)>
53      */
54     this._requestContentCallbacks = [];
55     /**
56      * @type Array.<WebInspector.LiveLocation>
57      */
58     this._liveLocations = [];
59     /**
60      * @type {Array.<WebInspector.PresentationConsoleMessage>}
61      */
62     this._consoleMessages = [];
63     
64     /**
65      * @type {Array.<WebInspector.Revision>}
66      */
67     this.history = [];
68     if (this.isEditable() && this._url)
69         this._restoreRevisionHistory();
70     this._formatterMapping = new WebInspector.IdentityFormatterSourceMapping();
71 }
72
73 WebInspector.UISourceCode.Events = {
74     FormattedChanged: "FormattedChanged",
75     WorkingCopyChanged: "WorkingCopyChanged",
76     WorkingCopyCommitted: "WorkingCopyCommitted",
77     TitleChanged: "TitleChanged",
78     ConsoleMessageAdded: "ConsoleMessageAdded",
79     ConsoleMessageRemoved: "ConsoleMessageRemoved",
80     ConsoleMessagesCleared: "ConsoleMessagesCleared",
81     SourceMappingChanged: "SourceMappingChanged",
82 }
83
84 WebInspector.UISourceCode.prototype = {
85     /**
86      * @return {string}
87      */
88     get url()
89     {
90         return this._url;
91     },
92
93     /**
94      * @return {string}
95      */
96     path: function()
97     {
98         return this._path;
99     },
100
101     /**
102      * @return {string}
103      */
104     uri: function()
105     {
106         return this._path;
107     },
108
109     /**
110      * @return {string}
111      */
112     originURL: function()
113     {
114         return this._originURL;
115     },
116
117     /**
118      * @param {string} url
119      */
120     urlChanged: function(url)
121     {
122         this._url = url;
123         this._originURL = url;
124         this._parsedURL = new WebInspector.ParsedURL(url);
125         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.TitleChanged, null);
126     },
127
128     /**
129      * @return {WebInspector.ParsedURL}
130      */
131     get parsedURL()
132     {
133         return this._parsedURL;
134     },
135
136     /**
137      * @return {string}
138      */
139     contentURL: function()
140     {
141         return this.originURL();
142     },
143
144     /**
145      * @return {WebInspector.ResourceType}
146      */
147     contentType: function()
148     {
149         return this._contentType;
150     },
151
152     /**
153      * @return {WebInspector.ScriptFile}
154      */
155     scriptFile: function()
156     {
157         return this._scriptFile;
158     },
159
160     /**
161      * @param {WebInspector.ScriptFile} scriptFile
162      */
163     setScriptFile: function(scriptFile)
164     {
165         this._scriptFile = scriptFile;
166     },
167
168     /**
169      * @return {WebInspector.StyleFile}
170      */
171     styleFile: function()
172     {
173         return this._styleFile;
174     },
175
176     /**
177      * @param {WebInspector.StyleFile} styleFile
178      */
179     setStyleFile: function(styleFile)
180     {
181         this._styleFile = styleFile;
182     },
183
184     /**
185      * @return {WebInspector.Project}
186      */
187     project: function()
188     {
189         return this._project;
190     },
191
192     /**
193      * @param {function(?string,boolean,string)} callback
194      */
195     requestContent: function(callback)
196     {
197         if (this._content || this._contentLoaded) {
198             callback(this._content, false, this._mimeType);
199             return;
200         }
201         this._requestContentCallbacks.push(callback);
202         if (this._requestContentCallbacks.length === 1)
203             this._project.requestFileContent(this, this._fireContentAvailable.bind(this));
204     },
205
206     /**
207      * @param {function(?string,boolean,string)} callback
208      */
209     requestOriginalContent: function(callback)
210     {
211         this._project.requestFileContent(this, callback);
212     },
213
214     /**
215      * @param {string} content
216      */
217     _commitContent: function(content)
218     {
219         this._content = content;
220         this._contentLoaded = true;
221         
222         var lastRevision = this.history.length ? this.history[this.history.length - 1] : null;
223         if (!lastRevision || lastRevision._content !== this._content) {
224             var revision = new WebInspector.Revision(this, this._content, new Date());
225             this.history.push(revision);
226             revision._persist();
227         }
228
229         var oldWorkingCopy = this._workingCopy;
230         delete this._workingCopy;
231         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyCommitted, {oldWorkingCopy: oldWorkingCopy, workingCopy: this.workingCopy()});
232         if (this._url && WebInspector.fileManager.isURLSaved(this._url)) {
233             WebInspector.fileManager.save(this._url, this._content, false);
234             WebInspector.fileManager.close(this._url);
235         }
236         this._project.setFileContent(this, this._content, function() { });
237     },
238
239     /**
240      * @param {string} content
241      */
242     addRevision: function(content)
243     {
244         this._commitContent(content);
245     },
246
247     _restoreRevisionHistory: function()
248     {
249         if (!window.localStorage)
250             return;
251
252         var registry = WebInspector.Revision._revisionHistoryRegistry();
253         var historyItems = registry[this.url];
254         if (!historyItems || !historyItems.length)
255             return;
256         for (var i = 0; i < historyItems.length; ++i) {
257             var content = window.localStorage[historyItems[i].key];
258             var timestamp = new Date(historyItems[i].timestamp);
259             var revision = new WebInspector.Revision(this, content, timestamp);
260             this.history.push(revision);
261         }
262         this._content = this.history[this.history.length - 1].content;
263         this._contentLoaded = true;
264         this._mimeType = this.canonicalMimeType();
265     },
266
267     _clearRevisionHistory: function()
268     {
269         if (!window.localStorage)
270             return;
271
272         var registry = WebInspector.Revision._revisionHistoryRegistry();
273         var historyItems = registry[this.url];
274         for (var i = 0; historyItems && i < historyItems.length; ++i)
275             delete window.localStorage[historyItems[i].key];
276         delete registry[this.url];
277         window.localStorage["revision-history"] = JSON.stringify(registry);
278     },
279    
280     revertToOriginal: function()
281     {
282         /**
283          * @this {WebInspector.UISourceCode}
284          * @param {?string} content
285          * @param {boolean} contentEncoded
286          * @param {string} mimeType
287          */
288         function callback(content, contentEncoded, mimeType)
289         {
290             if (typeof content !== "string")
291                 return;
292
293             this.addRevision(content);
294         }
295
296         this.requestOriginalContent(callback.bind(this));
297
298         WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, {
299             action: WebInspector.UserMetrics.UserActionNames.ApplyOriginalContent,
300             url: this.url
301         });
302     },
303
304     /**
305      * @param {function(WebInspector.UISourceCode)} callback
306      */
307     revertAndClearHistory: function(callback)
308     {
309         /**
310          * @this {WebInspector.UISourceCode}
311          * @param {?string} content
312          * @param {boolean} contentEncoded
313          * @param {string} mimeType
314          */
315         function revert(content, contentEncoded, mimeType)
316         {
317             if (typeof content !== "string")
318                 return;
319
320             this.addRevision(content);
321             this._clearRevisionHistory();
322             this.history = [];
323             callback(this);
324         }
325
326         this.requestOriginalContent(revert.bind(this));
327
328         WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, {
329             action: WebInspector.UserMetrics.UserActionNames.RevertRevision,
330             url: this.url
331         });
332     },
333
334     /**
335      * @return {boolean}
336      */
337     isEditable: function()
338     {
339         return this._isEditable;
340     },
341
342     /**
343      * @return {string}
344      */
345     workingCopy: function()
346     {
347         if (this.isDirty())
348             return this._workingCopy;
349         return this._content;
350     },
351
352     /**
353      * @param {string} newWorkingCopy
354      */
355     setWorkingCopy: function(newWorkingCopy)
356     {
357         var wasDirty = this.isDirty();        
358         this._mimeType = this.canonicalMimeType();
359         var oldWorkingCopy = this._workingCopy;
360         if (this._content === newWorkingCopy)
361             delete this._workingCopy;
362         else
363             this._workingCopy = newWorkingCopy;
364         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged, {oldWorkingCopy: oldWorkingCopy, workingCopy: this.workingCopy(), wasDirty: wasDirty});
365     },
366
367     /**
368      * @param {function(?string)} callback
369      */
370     commitWorkingCopy: function(callback)
371     {
372         if (!this.isDirty()) {
373             callback(null);
374             return;
375         }
376
377         this._commitContent(this._workingCopy);
378         callback(null);
379
380         WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, {
381             action: WebInspector.UserMetrics.UserActionNames.FileSaved,
382             url: this.url
383         });
384     },
385
386     /**
387      * @return {boolean}
388      */
389     isDirty: function()
390     {
391         return typeof this._workingCopy !== "undefined" && this._workingCopy !== this._content;
392     },
393
394     /**
395      * @return {string}
396      */
397     mimeType: function()
398     {
399         return this._mimeType;
400     },
401
402     /**
403      * @return {string}
404      */
405     canonicalMimeType: function()
406     {
407         return this.contentType().canonicalMimeType() || this._mimeType;
408     },
409
410     /**
411      * @return {?string}
412      */
413     content: function()
414     {
415         return this._content;
416     },
417
418     /**
419      * @param {string} query
420      * @param {boolean} caseSensitive
421      * @param {boolean} isRegex
422      * @param {function(Array.<WebInspector.ContentProvider.SearchMatch>)} callback
423      */
424     searchInContent: function(query, caseSensitive, isRegex, callback)
425     {
426         var content = this.content();
427         if (content) {
428             var provider = new WebInspector.StaticContentProvider(this.contentType(), content);
429             provider.searchInContent(query, caseSensitive, isRegex, callback);
430             return;
431         }
432
433         this._project.searchInFileContent(this, query, caseSensitive, isRegex, callback);
434     },
435
436     /**
437      * @param {?string} content
438      * @param {boolean} contentEncoded
439      * @param {string} mimeType
440      */
441     _fireContentAvailable: function(content, contentEncoded, mimeType)
442     {
443         this._contentLoaded = true;
444         this._mimeType = mimeType;
445         this._content = content;
446
447         var callbacks = this._requestContentCallbacks.slice();
448         this._requestContentCallbacks = [];
449         for (var i = 0; i < callbacks.length; ++i)
450             callbacks[i](content, contentEncoded, mimeType);
451
452         if (this._formatOnLoad) {
453             delete this._formatOnLoad;
454             this.setFormatted(true);
455         }
456     },
457
458     /**
459      * @return {boolean}
460      */
461     contentLoaded: function()
462     {
463         return this._contentLoaded;
464     },
465
466     /**
467      * @param {number} lineNumber
468      * @param {number} columnNumber
469      * @return {WebInspector.RawLocation}
470      */
471     uiLocationToRawLocation: function(lineNumber, columnNumber)
472     {
473         if (!this._sourceMapping)
474             return null;
475         var location = this._formatterMapping.formattedToOriginal(lineNumber, columnNumber);
476         return this._sourceMapping.uiLocationToRawLocation(this, location[0], location[1]);
477     },
478
479     /**
480      * @param {WebInspector.LiveLocation} liveLocation
481      */
482     addLiveLocation: function(liveLocation)
483     {
484         this._liveLocations.push(liveLocation);
485     },
486
487     /**
488      * @param {WebInspector.LiveLocation} liveLocation
489      */
490     removeLiveLocation: function(liveLocation)
491     {
492         this._liveLocations.remove(liveLocation);
493     },
494
495     updateLiveLocations: function()
496     {
497         var locationsCopy = this._liveLocations.slice();
498         for (var i = 0; i < locationsCopy.length; ++i)
499             locationsCopy[i].update();
500     },
501
502     /**
503      * @param {WebInspector.UILocation} uiLocation
504      */
505     overrideLocation: function(uiLocation)
506     {
507         var location = this._formatterMapping.originalToFormatted(uiLocation.lineNumber, uiLocation.columnNumber);
508         uiLocation.lineNumber = location[0];
509         uiLocation.columnNumber = location[1];
510         return uiLocation;
511     },
512
513     /**
514      * @return {Array.<WebInspector.PresentationConsoleMessage>}
515      */
516     consoleMessages: function()
517     {
518         return this._consoleMessages;
519     },
520
521     /**
522      * @param {WebInspector.PresentationConsoleMessage} message
523      */
524     consoleMessageAdded: function(message)
525     {
526         this._consoleMessages.push(message);
527         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.ConsoleMessageAdded, message);
528     },
529
530     /**
531      * @param {WebInspector.PresentationConsoleMessage} message
532      */
533     consoleMessageRemoved: function(message)
534     {
535         this._consoleMessages.remove(message);
536         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.ConsoleMessageRemoved, message);
537     },
538
539     consoleMessagesCleared: function()
540     {
541         this._consoleMessages = [];
542         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.ConsoleMessagesCleared);
543     },
544
545     /**
546      * @return {boolean}
547      */
548     formatted: function()
549     {
550         return !!this._formatted;
551     },
552
553     /**
554      * @param {boolean} formatted
555      */
556     setFormatted: function(formatted)
557     {
558         if (!this.contentLoaded()) {
559             this._formatOnLoad = formatted;
560             return;
561         }
562
563         if (this._formatted === formatted)
564             return;
565
566         this._formatted = formatted;
567
568         // Re-request content
569         this._contentLoaded = false;
570         this._content = false;
571         WebInspector.UISourceCode.prototype.requestContent.call(this, didGetContent.bind(this));
572   
573         /**
574          * @this {WebInspector.UISourceCode}
575          * @param {?string} content
576          * @param {boolean} contentEncoded
577          * @param {string} mimeType
578          */
579         function didGetContent(content, contentEncoded, mimeType)
580         {
581             var formatter;
582             if (!formatted)
583                 formatter = new WebInspector.IdentityFormatter();
584             else
585                 formatter = WebInspector.Formatter.createFormatter(this.contentType());
586             formatter.formatContent(mimeType, content || "", formattedChanged.bind(this));
587   
588             /**
589              * @this {WebInspector.UISourceCode}
590              * @param {string} content
591              * @param {WebInspector.FormatterSourceMapping} formatterMapping
592              */
593             function formattedChanged(content, formatterMapping)
594             {
595                 this._content = content;
596                 delete this._workingCopy;
597                 this._formatterMapping = formatterMapping;
598                 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.FormattedChanged, {content: content});
599                 this.updateLiveLocations();
600             }
601         }
602     },
603
604     /**
605      * @return {WebInspector.Formatter} formatter
606      */
607     createFormatter: function()
608     {
609         // overridden by subclasses.
610         return null;
611     },
612
613     /**
614      * @param {WebInspector.SourceMapping} sourceMapping
615      */
616     setSourceMapping: function(sourceMapping)
617     {
618         this._sourceMapping = sourceMapping;
619         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.SourceMappingChanged, null);
620     },
621
622     __proto__: WebInspector.Object.prototype
623 }
624
625 /**
626  * @interface
627  */
628 WebInspector.UISourceCodeProvider = function()
629 {
630 }
631
632 WebInspector.UISourceCodeProvider.Events = {
633     UISourceCodeAdded: "UISourceCodeAdded",
634     UISourceCodeRemoved: "UISourceCodeRemoved"
635 }
636
637 WebInspector.UISourceCodeProvider.prototype = {
638     /**
639      * @return {Array.<WebInspector.UISourceCode>}
640      */
641     uiSourceCodes: function() {},
642
643     /**
644      * @param {string} eventType
645      * @param {function(WebInspector.Event)} listener
646      * @param {Object=} thisObject
647      */
648     addEventListener: function(eventType, listener, thisObject) { },
649
650     /**
651      * @param {string} eventType
652      * @param {function(WebInspector.Event)} listener
653      * @param {Object=} thisObject
654      */
655     removeEventListener: function(eventType, listener, thisObject) { }
656 }
657
658 /**
659  * @constructor
660  * @param {WebInspector.UISourceCode} uiSourceCode
661  * @param {number} lineNumber
662  * @param {number} columnNumber
663  */
664 WebInspector.UILocation = function(uiSourceCode, lineNumber, columnNumber)
665 {
666     this.uiSourceCode = uiSourceCode;
667     this.lineNumber = lineNumber;
668     this.columnNumber = columnNumber;
669 }
670
671 WebInspector.UILocation.prototype = {
672     /**
673      * @return {WebInspector.RawLocation}
674      */
675     uiLocationToRawLocation: function()
676     {
677         return this.uiSourceCode.uiLocationToRawLocation(this.lineNumber, this.columnNumber);
678     },
679
680     /**
681      * @return {?string}
682      */
683     url: function()
684     {
685         return this.uiSourceCode.contentURL();
686     }
687 }
688
689 /**
690  * @interface
691  */
692 WebInspector.RawLocation = function()
693 {
694 }
695
696 /**
697  * @constructor
698  * @param {WebInspector.RawLocation} rawLocation
699  * @param {function(WebInspector.UILocation):(boolean|undefined)} updateDelegate
700  */
701 WebInspector.LiveLocation = function(rawLocation, updateDelegate)
702 {
703     this._rawLocation = rawLocation;
704     this._updateDelegate = updateDelegate;
705     this._uiSourceCodes = [];
706 }
707
708 WebInspector.LiveLocation.prototype = {
709     update: function()
710     {
711         var uiLocation = this.uiLocation();
712         if (uiLocation) {
713             var uiSourceCode = uiLocation.uiSourceCode;
714             if (this._uiSourceCodes.indexOf(uiSourceCode) === -1) {
715                 uiSourceCode.addLiveLocation(this);
716                 this._uiSourceCodes.push(uiSourceCode);
717             }
718             var oneTime = this._updateDelegate(uiLocation);
719             if (oneTime)
720                 this.dispose();
721         }
722     },
723
724     /**
725      * @return {WebInspector.RawLocation}
726      */
727     rawLocation: function()
728     {
729         return this._rawLocation;
730     },
731
732     /**
733      * @return {WebInspector.UILocation}
734      */
735     uiLocation: function()
736     {
737         // Should be overridden by subclasses.
738     },
739
740     dispose: function()
741     {
742         for (var i = 0; i < this._uiSourceCodes.length; ++i)
743             this._uiSourceCodes[i].removeLiveLocation(this);
744         this._uiSourceCodes = [];
745     }
746 }
747
748 /**
749  * @constructor
750  * @implements {WebInspector.ContentProvider}
751  * @param {WebInspector.UISourceCode} uiSourceCode
752  * @param {?string|undefined} content
753  * @param {Date} timestamp
754  */
755 WebInspector.Revision = function(uiSourceCode, content, timestamp)
756 {
757     this._uiSourceCode = uiSourceCode;
758     this._content = content;
759     this._timestamp = timestamp;
760 }
761
762 WebInspector.Revision._revisionHistoryRegistry = function()
763 {
764     if (!WebInspector.Revision._revisionHistoryRegistryObject) {
765         if (window.localStorage) {
766             var revisionHistory = window.localStorage["revision-history"];
767             try {
768                 WebInspector.Revision._revisionHistoryRegistryObject = revisionHistory ? JSON.parse(revisionHistory) : {};
769             } catch (e) {
770                 WebInspector.Revision._revisionHistoryRegistryObject = {};
771             }
772         } else
773             WebInspector.Revision._revisionHistoryRegistryObject = {};
774     }
775     return WebInspector.Revision._revisionHistoryRegistryObject;
776 }
777
778 WebInspector.Revision.filterOutStaleRevisions = function()
779 {
780     if (!window.localStorage)
781         return;
782
783     var registry = WebInspector.Revision._revisionHistoryRegistry();
784     var filteredRegistry = {};
785     for (var url in registry) {
786         var historyItems = registry[url];
787         var filteredHistoryItems = [];
788         for (var i = 0; historyItems && i < historyItems.length; ++i) {
789             var historyItem = historyItems[i];
790             if (historyItem.loaderId === WebInspector.resourceTreeModel.mainFrame.loaderId) {
791                 filteredHistoryItems.push(historyItem);
792                 filteredRegistry[url] = filteredHistoryItems;
793             } else
794                 delete window.localStorage[historyItem.key];
795         }
796     }
797     WebInspector.Revision._revisionHistoryRegistryObject = filteredRegistry;
798
799     function persist()
800     {
801         window.localStorage["revision-history"] = JSON.stringify(filteredRegistry);
802     }
803
804     // Schedule async storage.
805     setTimeout(persist, 0);
806 }
807
808 WebInspector.Revision.prototype = {
809     /**
810      * @return {WebInspector.UISourceCode}
811      */
812     get uiSourceCode()
813     {
814         return this._uiSourceCode;
815     },
816
817     /**
818      * @return {Date}
819      */
820     get timestamp()
821     {
822         return this._timestamp;
823     },
824
825     /**
826      * @return {?string}
827      */
828     get content()
829     {
830         return this._content || null;
831     },
832
833     revertToThis: function()
834     {
835         function revert(content)
836         {
837             if (this._uiSourceCode._content !== content)
838                 this._uiSourceCode.addRevision(content);
839         }
840         this.requestContent(revert.bind(this));
841     },
842
843     /**
844      * @return {string}
845      */
846     contentURL: function()
847     {
848         return this._uiSourceCode.originURL();
849     },
850
851     /**
852      * @return {WebInspector.ResourceType}
853      */
854     contentType: function()
855     {
856         return this._uiSourceCode.contentType();
857     },
858
859     /**
860      * @param {function(?string, boolean, string)} callback
861      */
862     requestContent: function(callback)
863     {
864         callback(this._content || "", false, this.uiSourceCode.canonicalMimeType());
865     },
866
867     /**
868      * @param {string} query
869      * @param {boolean} caseSensitive
870      * @param {boolean} isRegex
871      * @param {function(Array.<WebInspector.ContentProvider.SearchMatch>)} callback
872      */
873     searchInContent: function(query, caseSensitive, isRegex, callback)
874     {
875         callback([]);
876     },
877
878     _persist: function()
879     {
880         if (!window.localStorage)
881             return;
882
883         var url = this.contentURL();
884         if (!url || url.startsWith("inspector://"))
885             return;
886
887         var loaderId = WebInspector.resourceTreeModel.mainFrame.loaderId;
888         var timestamp = this.timestamp.getTime();
889         var key = "revision-history|" + url + "|" + loaderId + "|" + timestamp;
890
891         var registry = WebInspector.Revision._revisionHistoryRegistry();
892
893         var historyItems = registry[url];
894         if (!historyItems) {
895             historyItems = [];
896             registry[url] = historyItems;
897         }
898         historyItems.push({url: url, loaderId: loaderId, timestamp: timestamp, key: key});
899
900         function persist()
901         {
902             window.localStorage[key] = this._content;
903             window.localStorage["revision-history"] = JSON.stringify(registry);
904         }
905
906         // Schedule async storage.
907         setTimeout(persist.bind(this), 0);
908     }
909 }