2 * Copyright (C) 2011 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
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
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.
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.
34 * @extends {WebInspector.Object}
35 * @implements {WebInspector.ContentProvider}
36 * @param {!WebInspector.Project} project
37 * @param {string} parentPath
38 * @param {string} name
40 * @param {!WebInspector.ResourceType} contentType
41 * @param {boolean} isEditable
43 WebInspector.UISourceCode = function(project, parentPath, name, originURL, url, contentType, isEditable)
45 this._project = project;
46 this._parentPath = parentPath;
48 this._originURL = originURL;
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 = [];
59 /** @type {!Array.<!WebInspector.Revision>} */
61 if (this.isEditable() && this._url)
62 this._restoreRevisionHistory();
63 this._formatterMapping = new WebInspector.IdentityFormatterSourceMapping();
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",
78 WebInspector.UISourceCode.prototype = {
98 parentPath: function()
100 return this._parentPath;
108 return this._parentPath ? this._parentPath + "/" + this._name : this._name;
114 fullDisplayName: function()
116 return this._project.displayName() + "/" + (this._parentPath ? this._parentPath + "/" : "") + this.displayName(true);
120 * @param {boolean=} skipTrim
123 displayName: function(skipTrim)
125 var displayName = this.name() || WebInspector.UIString("(index)");
126 return skipTrim ? displayName : displayName.trimEnd(100);
134 var path = this.path();
135 if (!this._project.id())
138 return this._project.id();
139 return this._project.id() + "/" + path;
145 originURL: function()
147 return this._originURL;
153 canRename: function()
155 return this._project.canRename();
159 * @param {string} newName
160 * @param {function(boolean)} callback
162 rename: function(newName, callback)
164 this._project.rename(this, newName, innerCallback.bind(this));
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}
174 function innerCallback(success, newName, newURL, newOriginURL, newContentType)
177 this._updateName(/** @type {string} */ (newName), /** @type {string} */ (newURL), /** @type {string} */ (newOriginURL), /** @type {!WebInspector.ResourceType} */ (newContentType));
183 * @param {string} name
184 * @param {string} url
185 * @param {string} originURL
186 * @param {!WebInspector.ResourceType=} contentType
188 _updateName: function(name, url, originURL, contentType)
190 var oldURI = this.uri();
195 this._originURL = originURL;
197 this._contentType = contentType;
198 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.TitleChanged, oldURI);
204 contentURL: function()
206 return this.originURL();
210 * @return {!WebInspector.ResourceType}
212 contentType: function()
214 return this._contentType;
218 * @return {?WebInspector.ScriptFile}
220 scriptFile: function()
222 return this._scriptFile;
226 * @param {?WebInspector.ScriptFile} scriptFile
228 setScriptFile: function(scriptFile)
230 this._scriptFile = scriptFile;
234 * @return {!WebInspector.Project}
238 return this._project;
242 * @param {function(?Date, ?number)} callback
244 requestMetadata: function(callback)
246 this._project.requestMetadata(this, callback);
250 * @param {function(?string)} callback
252 requestContent: function(callback)
254 if (this._content || this._contentLoaded) {
255 callback(this._content);
258 this._requestContentCallbacks.push(callback);
259 if (this._requestContentCallbacks.length === 1)
260 this._project.requestFileContent(this, this._fireContentAvailable.bind(this));
264 * @param {function()=} callback
266 checkContentUpdated: function(callback)
268 if (!this._project.canSetFileContent())
270 if (this._checkingContent)
272 this._checkingContent = true;
273 this._project.requestFileContent(this, contentLoaded.bind(this));
276 * @param {?string} updatedContent
277 * @this {WebInspector.UISourceCode}
279 function contentLoaded(updatedContent)
281 if (updatedContent === null) {
282 var workingCopy = this.workingCopy();
283 this._commitContent("", false);
284 this.setWorkingCopy(workingCopy);
285 delete this._checkingContent;
290 if (typeof this._lastAcceptedContent === "string" && this._lastAcceptedContent === updatedContent) {
291 delete this._checkingContent;
296 if (this._content === updatedContent) {
297 delete this._lastAcceptedContent;
298 delete this._checkingContent;
304 if (!this.isDirty()) {
305 this._commitContent(updatedContent, false);
306 delete this._checkingContent;
312 var shouldUpdate = window.confirm(WebInspector.UIString("This file was changed externally. Would you like to reload it?"));
314 this._commitContent(updatedContent, false);
316 this._lastAcceptedContent = updatedContent;
317 delete this._checkingContent;
324 * @param {function(?string)} callback
326 requestOriginalContent: function(callback)
328 this._project.requestFileContent(this, callback);
332 * @param {string} content
333 * @param {boolean} shouldSetContentInProject
335 _commitContent: function(content, shouldSetContentInProject)
337 delete this._lastAcceptedContent;
338 this._content = content;
339 this._contentLoaded = true;
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);
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() { });
358 * @param {boolean} forceSaveAs
359 * @param {?string} content
361 _saveURLWithFileManager: function(forceSaveAs, content)
363 WebInspector.fileManager.save(this._url, /** @type {string} */ (content), forceSaveAs, callback.bind(this));
364 WebInspector.fileManager.close(this._url);
367 * @param {boolean} accepted
368 * @this {WebInspector.UISourceCode}
370 function callback(accepted)
374 this._savedWithFileManager = true;
375 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.SavedStateUpdated);
380 * @param {boolean} forceSaveAs
382 saveToFileSystem: function(forceSaveAs)
384 if (this.isDirty()) {
385 this._saveURLWithFileManager(forceSaveAs, this.workingCopy());
386 this.commitWorkingCopy(function() { });
389 this.requestContent(this._saveURLWithFileManager.bind(this, forceSaveAs));
395 hasUnsavedCommittedChanges: function()
397 if (this._savedWithFileManager || this.project().canSetFileContent() || !this._isEditable)
399 if (this._project.workspace().hasResourceContentTrackingExtensions())
401 return !!this._hasCommittedChanges;
405 * @param {string} content
407 addRevision: function(content)
409 this._commitContent(content, true);
412 _restoreRevisionHistory: function()
414 if (!window.localStorage)
417 var registry = WebInspector.Revision._revisionHistoryRegistry();
418 var historyItems = registry[this.url];
422 function filterOutStale(historyItem)
424 // FIXME: Main frame might not have been loaded yet when uiSourceCodes for snippets are created.
425 if (!WebInspector.resourceTreeModel.mainFrame)
427 return historyItem.loaderId === WebInspector.resourceTreeModel.mainFrame.loaderId;
430 historyItems = historyItems.filter(filterOutStale);
431 if (!historyItems.length)
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);
440 this._content = this.history[this.history.length - 1].content;
441 this._hasCommittedChanges = true;
442 this._contentLoaded = true;
445 _clearRevisionHistory: function()
447 if (!window.localStorage)
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);
458 revertToOriginal: function()
461 * @this {WebInspector.UISourceCode}
462 * @param {?string} content
464 function callback(content)
466 if (typeof content !== "string")
469 this.addRevision(content);
472 this.requestOriginalContent(callback.bind(this));
474 WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, {
475 action: WebInspector.UserMetrics.UserActionNames.ApplyOriginalContent,
481 * @param {function(!WebInspector.UISourceCode)} callback
483 revertAndClearHistory: function(callback)
486 * @this {WebInspector.UISourceCode}
487 * @param {?string} content
489 function revert(content)
491 if (typeof content !== "string")
494 this.addRevision(content);
495 this._clearRevisionHistory();
500 this.requestOriginalContent(revert.bind(this));
502 WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, {
503 action: WebInspector.UserMetrics.UserActionNames.RevertRevision,
511 isEditable: function()
513 return this._isEditable;
519 workingCopy: function()
521 if (this._workingCopyGetter) {
522 this._workingCopy = this._workingCopyGetter();
523 delete this._workingCopyGetter;
526 return this._workingCopy;
527 return this._content;
530 resetWorkingCopy: function()
532 this._innerResetWorkingCopy();
533 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged);
536 _innerResetWorkingCopy: function()
538 delete this._workingCopy;
539 delete this._workingCopyGetter;
543 * @param {string} newWorkingCopy
545 setWorkingCopy: function(newWorkingCopy)
547 this._workingCopy = newWorkingCopy;
548 delete this._workingCopyGetter;
549 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged);
552 setWorkingCopyGetter: function(workingCopyGetter)
554 this._workingCopyGetter = workingCopyGetter;
555 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged);
558 removeWorkingCopyGetter: function()
560 if (!this._workingCopyGetter)
562 this._workingCopy = this._workingCopyGetter();
563 delete this._workingCopyGetter;
567 * @param {function(?string)} callback
569 commitWorkingCopy: function(callback)
571 if (!this.isDirty()) {
576 this._commitContent(this.workingCopy(), true);
579 WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, {
580 action: WebInspector.UserMetrics.UserActionNames.FileSaved,
590 return typeof this._workingCopy !== "undefined" || typeof this._workingCopyGetter !== "undefined";
596 _mimeType: function()
598 return this.contentType().canonicalMimeType();
604 highlighterType: function()
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();
620 return this._content;
624 * @param {string} query
625 * @param {boolean} caseSensitive
626 * @param {boolean} isRegex
627 * @param {function(!Array.<!WebInspector.ContentProvider.SearchMatch>)} callback
629 searchInContent: function(query, caseSensitive, isRegex, callback)
631 var content = this.content();
633 var provider = new WebInspector.StaticContentProvider(this.contentType(), content);
634 provider.searchInContent(query, caseSensitive, isRegex, callback);
638 this._project.searchInFileContent(this, query, caseSensitive, isRegex, callback);
642 * @param {?string} content
644 _fireContentAvailable: function(content)
646 this._contentLoaded = true;
647 this._content = content;
649 var callbacks = this._requestContentCallbacks.slice();
650 this._requestContentCallbacks = [];
651 for (var i = 0; i < callbacks.length; ++i)
652 callbacks[i](content);
654 if (this._formatOnLoad) {
655 delete this._formatOnLoad;
656 this.setFormatted(true);
663 contentLoaded: function()
665 return this._contentLoaded;
669 * @param {number} lineNumber
670 * @param {number} columnNumber
671 * @return {?WebInspector.RawLocation}
673 uiLocationToRawLocation: function(lineNumber, columnNumber)
675 if (!this._sourceMapping)
677 var location = this._formatterMapping.formattedToOriginal(lineNumber, columnNumber);
678 return this._sourceMapping.uiLocationToRawLocation(this, location[0], location[1]);
682 * @param {!WebInspector.LiveLocation} liveLocation
684 addLiveLocation: function(liveLocation)
686 this._liveLocations.add(liveLocation);
690 * @param {!WebInspector.LiveLocation} liveLocation
692 removeLiveLocation: function(liveLocation)
694 this._liveLocations.remove(liveLocation);
697 updateLiveLocations: function()
699 var items = this._liveLocations.items();
700 for (var i = 0; i < items.length; ++i)
705 * @param {!WebInspector.UILocation} uiLocation
706 * @return {!WebInspector.UILocation}
708 overrideLocation: function(uiLocation)
710 var location = this._formatterMapping.originalToFormatted(uiLocation.lineNumber, uiLocation.columnNumber);
711 uiLocation.lineNumber = location[0];
712 uiLocation.columnNumber = location[1];
717 * @return {!Array.<!WebInspector.PresentationConsoleMessage>}
719 consoleMessages: function()
721 return this._consoleMessages;
725 * @param {!WebInspector.PresentationConsoleMessage} message
727 consoleMessageAdded: function(message)
729 this._consoleMessages.push(message);
730 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.ConsoleMessageAdded, message);
734 * @param {!WebInspector.PresentationConsoleMessage} message
736 consoleMessageRemoved: function(message)
738 this._consoleMessages.remove(message);
739 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.ConsoleMessageRemoved, message);
742 consoleMessagesCleared: function()
744 this._consoleMessages = [];
745 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.ConsoleMessagesCleared);
751 formatted: function()
753 return !!this._formatted;
757 * @param {boolean} formatted
759 setFormatted: function(formatted)
761 if (!this.contentLoaded()) {
762 this._formatOnLoad = formatted;
766 if (this._formatted === formatted)
772 this._formatted = formatted;
774 // Re-request content
775 this._contentLoaded = false;
776 this._content = false;
777 this.requestContent(didGetContent.bind(this));
780 * @this {WebInspector.UISourceCode}
781 * @param {?string} content
783 function didGetContent(content)
787 formatter = new WebInspector.IdentityFormatter();
789 formatter = WebInspector.Formatter.createFormatter(this.contentType());
790 formatter.formatContent(this.highlighterType(), content || "", formattedChanged.bind(this));
793 * @this {WebInspector.UISourceCode}
794 * @param {string} content
795 * @param {!WebInspector.FormatterSourceMapping} formatterMapping
797 function formattedChanged(content, formatterMapping)
799 this._content = content;
800 this._innerResetWorkingCopy();
801 var oldFormatter = this._formatterMapping;
802 this._formatterMapping = formatterMapping;
803 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.FormattedChanged, {
805 oldFormatter: oldFormatter,
806 newFormatter: this._formatterMapping,
808 this.updateLiveLocations();
814 * @return {?WebInspector.Formatter} formatter
816 createFormatter: function()
818 // overridden by subclasses.
825 hasSourceMapping: function()
827 return !!this._sourceMapping;
831 * @param {?WebInspector.SourceMapping} sourceMapping
833 setSourceMapping: function(sourceMapping)
835 if (this._sourceMapping === sourceMapping)
837 this._sourceMapping = sourceMapping;
838 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.SourceMappingChanged);
841 __proto__: WebInspector.Object.prototype
846 * @param {!WebInspector.UISourceCode} uiSourceCode
847 * @param {number} lineNumber
848 * @param {number} columnNumber
850 WebInspector.UILocation = function(uiSourceCode, lineNumber, columnNumber)
852 this.uiSourceCode = uiSourceCode;
853 this.lineNumber = lineNumber;
854 this.columnNumber = columnNumber;
857 WebInspector.UILocation.prototype = {
859 * @return {?WebInspector.RawLocation}
861 uiLocationToRawLocation: function()
863 return this.uiSourceCode.uiLocationToRawLocation(this.lineNumber, this.columnNumber);
871 return this.uiSourceCode.contentURL();
879 var linkText = this.uiSourceCode.displayName();
880 if (typeof this.lineNumber === "number")
881 linkText += ":" + (this.lineNumber + 1);
887 var revealer = WebInspector.moduleManager.instance(WebInspector.Revealer, this);
889 revealer.reveal(this);
896 WebInspector.RawLocation = function()
902 * @param {!WebInspector.RawLocation} rawLocation
903 * @param {function(!WebInspector.UILocation):(boolean|undefined)} updateDelegate
905 WebInspector.LiveLocation = function(rawLocation, updateDelegate)
907 this._rawLocation = rawLocation;
908 this._updateDelegate = updateDelegate;
909 this._uiSourceCodes = [];
912 WebInspector.LiveLocation.prototype = {
915 var uiLocation = this.uiLocation();
917 var uiSourceCode = uiLocation.uiSourceCode;
918 if (this._uiSourceCodes.indexOf(uiSourceCode) === -1) {
919 uiSourceCode.addLiveLocation(this);
920 this._uiSourceCodes.push(uiSourceCode);
922 var oneTime = this._updateDelegate(uiLocation);
929 * @return {!WebInspector.RawLocation}
931 rawLocation: function()
933 return this._rawLocation;
937 * @return {!WebInspector.UILocation}
939 uiLocation: function()
941 throw "Not implemented";
946 for (var i = 0; i < this._uiSourceCodes.length; ++i)
947 this._uiSourceCodes[i].removeLiveLocation(this);
948 this._uiSourceCodes = [];
954 * @implements {WebInspector.ContentProvider}
955 * @param {!WebInspector.UISourceCode} uiSourceCode
956 * @param {?string|undefined} content
957 * @param {!Date} timestamp
959 WebInspector.Revision = function(uiSourceCode, content, timestamp)
961 this._uiSourceCode = uiSourceCode;
962 this._content = content;
963 this._timestamp = timestamp;
966 WebInspector.Revision._revisionHistoryRegistry = function()
968 if (!WebInspector.Revision._revisionHistoryRegistryObject) {
969 if (window.localStorage) {
970 var revisionHistory = window.localStorage["revision-history"];
972 WebInspector.Revision._revisionHistoryRegistryObject = revisionHistory ? JSON.parse(revisionHistory) : {};
974 WebInspector.Revision._revisionHistoryRegistryObject = {};
977 WebInspector.Revision._revisionHistoryRegistryObject = {};
979 return WebInspector.Revision._revisionHistoryRegistryObject;
982 WebInspector.Revision.filterOutStaleRevisions = function()
984 if (!window.localStorage)
987 var registry = WebInspector.Revision._revisionHistoryRegistry();
988 var filteredRegistry = {};
989 for (var url in registry) {
990 var historyItems = registry[url];
991 var filteredHistoryItems = [];
992 for (var i = 0; historyItems && i < historyItems.length; ++i) {
993 var historyItem = historyItems[i];
994 if (historyItem.loaderId === WebInspector.resourceTreeModel.mainFrame.loaderId) {
995 filteredHistoryItems.push(historyItem);
996 filteredRegistry[url] = filteredHistoryItems;
998 delete window.localStorage[historyItem.key];
1001 WebInspector.Revision._revisionHistoryRegistryObject = filteredRegistry;
1005 window.localStorage["revision-history"] = JSON.stringify(filteredRegistry);
1008 // Schedule async storage.
1009 setTimeout(persist, 0);
1012 WebInspector.Revision.prototype = {
1014 * @return {!WebInspector.UISourceCode}
1018 return this._uiSourceCode;
1026 return this._timestamp;
1034 return this._content || null;
1037 revertToThis: function()
1040 * @param {string} content
1041 * @this {WebInspector.Revision}
1043 function revert(content)
1045 if (this._uiSourceCode._content !== content)
1046 this._uiSourceCode.addRevision(content);
1048 this.requestContent(revert.bind(this));
1054 contentURL: function()
1056 return this._uiSourceCode.originURL();
1060 * @return {!WebInspector.ResourceType}
1062 contentType: function()
1064 return this._uiSourceCode.contentType();
1068 * @param {function(string)} callback
1070 requestContent: function(callback)
1072 callback(this._content || "");
1076 * @param {string} query
1077 * @param {boolean} caseSensitive
1078 * @param {boolean} isRegex
1079 * @param {function(!Array.<!WebInspector.ContentProvider.SearchMatch>)} callback
1081 searchInContent: function(query, caseSensitive, isRegex, callback)
1086 _persist: function()
1088 if (this._uiSourceCode.project().type() === WebInspector.projectTypes.FileSystem)
1091 if (!window.localStorage)
1094 var url = this.contentURL();
1095 if (!url || url.startsWith("inspector://"))
1098 var loaderId = WebInspector.resourceTreeModel.mainFrame.loaderId;
1099 var timestamp = this.timestamp.getTime();
1100 var key = "revision-history|" + url + "|" + loaderId + "|" + timestamp;
1102 var registry = WebInspector.Revision._revisionHistoryRegistry();
1104 var historyItems = registry[url];
1105 if (!historyItems) {
1107 registry[url] = historyItems;
1109 historyItems.push({url: url, loaderId: loaderId, timestamp: timestamp, key: key});
1112 * @this {WebInspector.Revision}
1116 window.localStorage[key] = this._content;
1117 window.localStorage["revision-history"] = JSON.stringify(registry);
1120 // Schedule async storage.
1121 setTimeout(persist.bind(this), 0);