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