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 */
43WebInspector.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
72WebInspector.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
83WebInspector.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 */
786WebInspector.UILocation = function(uiSourceCode, lineNumber, columnNumber)
787{
788    this.uiSourceCode = uiSourceCode;
789    this.lineNumber = lineNumber;
790    this.columnNumber = columnNumber;
791}
792
793WebInspector.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 */
834WebInspector.RawLocation = function()
835{
836}
837
838WebInspector.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 */
850WebInspector.LiveLocation = function(rawLocation, updateDelegate)
851{
852    this._rawLocation = rawLocation;
853    this._updateDelegate = updateDelegate;
854}
855
856WebInspector.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 */
895WebInspector.Revision = function(uiSourceCode, content, timestamp)
896{
897    this._uiSourceCode = uiSourceCode;
898    this._content = content;
899    this._timestamp = timestamp;
900}
901
902WebInspector.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
918WebInspector.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
948WebInspector.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