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 {!Array.<!WebInspector.PresentationConsoleMessage>} */
55 this._consoleMessages = [];
57 /** @type {!Array.<!WebInspector.Revision>} */
59 if (this.isEditable() && this._url)
60 this._restoreRevisionHistory();
63 WebInspector.UISourceCode.Events = {
64 WorkingCopyChanged: "WorkingCopyChanged",
65 WorkingCopyCommitted: "WorkingCopyCommitted",
66 TitleChanged: "TitleChanged",
67 SavedStateUpdated: "SavedStateUpdated",
68 ConsoleMessageAdded: "ConsoleMessageAdded",
69 ConsoleMessageRemoved: "ConsoleMessageRemoved",
70 ConsoleMessagesCleared: "ConsoleMessagesCleared",
71 SourceMappingChanged: "SourceMappingChanged",
74 WebInspector.UISourceCode.prototype = {
94 parentPath: function()
96 return this._parentPath;
104 return this._parentPath ? this._parentPath + "/" + this._name : this._name;
110 fullDisplayName: function()
112 return this._project.displayName() + "/" + (this._parentPath ? this._parentPath + "/" : "") + this.displayName(true);
116 * @param {boolean=} skipTrim
119 displayName: function(skipTrim)
121 var displayName = this.name() || WebInspector.UIString("(index)");
122 return skipTrim ? displayName : displayName.trimEnd(100);
130 var path = this.path();
131 if (!this._project.id())
134 return this._project.id();
135 return this._project.id() + "/" + path;
141 originURL: function()
143 return this._originURL;
149 canRename: function()
151 return this._project.canRename();
155 * @param {string} newName
156 * @param {function(boolean)} callback
158 rename: function(newName, callback)
160 this._project.rename(this, newName, innerCallback.bind(this));
163 * @param {boolean} success
164 * @param {string=} newName
165 * @param {string=} newURL
166 * @param {string=} newOriginURL
167 * @param {!WebInspector.ResourceType=} newContentType
168 * @this {WebInspector.UISourceCode}
170 function innerCallback(success, newName, newURL, newOriginURL, newContentType)
173 this._updateName(/** @type {string} */ (newName), /** @type {string} */ (newURL), /** @type {string} */ (newOriginURL), /** @type {!WebInspector.ResourceType} */ (newContentType));
180 this._project.deleteFile(this.path());
184 * @param {string} name
185 * @param {string} url
186 * @param {string} originURL
187 * @param {!WebInspector.ResourceType=} contentType
189 _updateName: function(name, url, originURL, contentType)
191 var oldURI = this.uri();
196 this._originURL = originURL;
198 this._contentType = contentType;
199 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.TitleChanged, oldURI);
205 contentURL: function()
207 return this.originURL();
211 * @return {!WebInspector.ResourceType}
213 contentType: function()
215 return this._contentType;
219 * @return {?WebInspector.ScriptFile}
221 scriptFile: function()
223 return this._scriptFile;
227 * @param {?WebInspector.ScriptFile} scriptFile
229 setScriptFile: function(scriptFile)
231 this._scriptFile = scriptFile;
235 * @return {!WebInspector.Project}
239 return this._project;
243 * @param {function(?Date, ?number)} callback
245 requestMetadata: function(callback)
247 this._project.requestMetadata(this, callback);
251 * @param {function(?string)} callback
253 requestContent: function(callback)
255 if (this._content || this._contentLoaded) {
256 callback(this._content);
259 this._requestContentCallbacks.push(callback);
260 if (this._requestContentCallbacks.length === 1)
261 this._project.requestFileContent(this, this._fireContentAvailable.bind(this));
265 * @param {function()=} callback
267 checkContentUpdated: function(callback)
269 if (!this._project.canSetFileContent())
271 if (this._checkingContent)
273 this._checkingContent = true;
274 this._project.requestFileContent(this, contentLoaded.bind(this));
277 * @param {?string} updatedContent
278 * @this {WebInspector.UISourceCode}
280 function contentLoaded(updatedContent)
282 if (updatedContent === null) {
283 var workingCopy = this.workingCopy();
284 this._commitContent("", false);
285 this.setWorkingCopy(workingCopy);
286 delete this._checkingContent;
291 if (typeof this._lastAcceptedContent === "string" && this._lastAcceptedContent === updatedContent) {
292 delete this._checkingContent;
297 if (this._content === updatedContent) {
298 delete this._lastAcceptedContent;
299 delete this._checkingContent;
305 if (!this.isDirty()) {
306 this._commitContent(updatedContent, false);
307 delete this._checkingContent;
313 var shouldUpdate = window.confirm(WebInspector.UIString("This file was changed externally. Would you like to reload it?"));
315 this._commitContent(updatedContent, false);
317 this._lastAcceptedContent = updatedContent;
318 delete this._checkingContent;
325 * @param {function(?string)} callback
327 requestOriginalContent: function(callback)
329 this._project.requestFileContent(this, callback);
333 * @param {string} content
334 * @param {boolean} shouldSetContentInProject
336 _commitContent: function(content, shouldSetContentInProject)
338 delete this._lastAcceptedContent;
339 this._content = content;
340 this._contentLoaded = true;
342 var lastRevision = this.history.length ? this.history[this.history.length - 1] : null;
343 if (!lastRevision || lastRevision._content !== this._content) {
344 var revision = new WebInspector.Revision(this, this._content, new Date());
345 this.history.push(revision);
349 this._innerResetWorkingCopy();
350 this._hasCommittedChanges = true;
351 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyCommitted);
352 if (this._url && WebInspector.fileManager.isURLSaved(this._url))
353 this._saveURLWithFileManager(false, this._content);
354 if (shouldSetContentInProject)
355 this._project.setFileContent(this, this._content, function() { });
359 * @param {boolean} forceSaveAs
360 * @param {?string} content
362 _saveURLWithFileManager: function(forceSaveAs, content)
364 WebInspector.fileManager.save(this._url, /** @type {string} */ (content), forceSaveAs, callback.bind(this));
365 WebInspector.fileManager.close(this._url);
368 * @param {boolean} accepted
369 * @this {WebInspector.UISourceCode}
371 function callback(accepted)
375 this._savedWithFileManager = true;
376 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.SavedStateUpdated);
381 * @param {boolean} forceSaveAs
383 saveToFileSystem: function(forceSaveAs)
385 if (this.isDirty()) {
386 this._saveURLWithFileManager(forceSaveAs, this.workingCopy());
387 this.commitWorkingCopy(function() { });
390 this.requestContent(this._saveURLWithFileManager.bind(this, forceSaveAs));
396 hasUnsavedCommittedChanges: function()
398 if (this._savedWithFileManager || this.project().canSetFileContent() || !this._isEditable)
400 if (this._project.workspace().hasResourceContentTrackingExtensions())
402 return !!this._hasCommittedChanges;
406 * @param {string} content
408 addRevision: function(content)
410 this._commitContent(content, true);
413 _restoreRevisionHistory: function()
415 if (!window.localStorage)
418 var registry = WebInspector.Revision._revisionHistoryRegistry();
419 var historyItems = registry[this.url];
423 function filterOutStale(historyItem)
425 // FIXME: Main frame might not have been loaded yet when uiSourceCodes for snippets are created.
426 if (!WebInspector.resourceTreeModel.mainFrame)
428 return historyItem.loaderId === WebInspector.resourceTreeModel.mainFrame.loaderId;
431 historyItems = historyItems.filter(filterOutStale);
432 if (!historyItems.length)
435 for (var i = 0; i < historyItems.length; ++i) {
436 var content = window.localStorage[historyItems[i].key];
437 var timestamp = new Date(historyItems[i].timestamp);
438 var revision = new WebInspector.Revision(this, content, timestamp);
439 this.history.push(revision);
441 this._content = this.history[this.history.length - 1].content;
442 this._hasCommittedChanges = true;
443 this._contentLoaded = true;
446 _clearRevisionHistory: function()
448 if (!window.localStorage)
451 var registry = WebInspector.Revision._revisionHistoryRegistry();
452 var historyItems = registry[this.url];
453 for (var i = 0; historyItems && i < historyItems.length; ++i)
454 delete window.localStorage[historyItems[i].key];
455 delete registry[this.url];
456 window.localStorage["revision-history"] = JSON.stringify(registry);
459 revertToOriginal: function()
462 * @this {WebInspector.UISourceCode}
463 * @param {?string} content
465 function callback(content)
467 if (typeof content !== "string")
470 this.addRevision(content);
473 this.requestOriginalContent(callback.bind(this));
475 WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, {
476 action: WebInspector.UserMetrics.UserActionNames.ApplyOriginalContent,
482 * @param {function(!WebInspector.UISourceCode)} callback
484 revertAndClearHistory: function(callback)
487 * @this {WebInspector.UISourceCode}
488 * @param {?string} content
490 function revert(content)
492 if (typeof content !== "string")
495 this.addRevision(content);
496 this._clearRevisionHistory();
501 this.requestOriginalContent(revert.bind(this));
503 WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, {
504 action: WebInspector.UserMetrics.UserActionNames.RevertRevision,
512 isEditable: function()
514 return this._isEditable;
520 workingCopy: function()
522 if (this._workingCopyGetter) {
523 this._workingCopy = this._workingCopyGetter();
524 delete this._workingCopyGetter;
527 return this._workingCopy;
528 return this._content;
531 resetWorkingCopy: function()
533 this._innerResetWorkingCopy();
534 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged);
537 _innerResetWorkingCopy: function()
539 delete this._workingCopy;
540 delete this._workingCopyGetter;
544 * @param {string} newWorkingCopy
546 setWorkingCopy: function(newWorkingCopy)
548 this._workingCopy = newWorkingCopy;
549 delete this._workingCopyGetter;
550 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged);
553 setWorkingCopyGetter: function(workingCopyGetter)
555 this._workingCopyGetter = workingCopyGetter;
556 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged);
559 removeWorkingCopyGetter: function()
561 if (!this._workingCopyGetter)
563 this._workingCopy = this._workingCopyGetter();
564 delete this._workingCopyGetter;
568 * @param {function(?string)} callback
570 commitWorkingCopy: function(callback)
572 if (!this.isDirty()) {
577 this._commitContent(this.workingCopy(), true);
580 WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, {
581 action: WebInspector.UserMetrics.UserActionNames.FileSaved,
591 return typeof this._workingCopy !== "undefined" || typeof this._workingCopyGetter !== "undefined";
597 highlighterType: function()
599 var lastIndexOfDot = this._name.lastIndexOf(".");
600 var extension = lastIndexOfDot !== -1 ? this._name.substr(lastIndexOfDot + 1) : "";
601 var indexOfQuestionMark = extension.indexOf("?");
602 if (indexOfQuestionMark !== -1)
603 extension = extension.substr(0, indexOfQuestionMark);
604 var mimeType = WebInspector.ResourceType.mimeTypesForExtensions[extension.toLowerCase()];
605 return mimeType || this.contentType().canonicalMimeType();
613 return this._content;
617 * @param {string} query
618 * @param {boolean} caseSensitive
619 * @param {boolean} isRegex
620 * @param {function(!Array.<!WebInspector.ContentProvider.SearchMatch>)} callback
622 searchInContent: function(query, caseSensitive, isRegex, callback)
624 var content = this.content();
626 var provider = new WebInspector.StaticContentProvider(this.contentType(), content);
627 provider.searchInContent(query, caseSensitive, isRegex, callback);
631 this._project.searchInFileContent(this, query, caseSensitive, isRegex, callback);
635 * @param {?string} content
637 _fireContentAvailable: function(content)
639 this._contentLoaded = true;
640 this._content = content;
642 var callbacks = this._requestContentCallbacks.slice();
643 this._requestContentCallbacks = [];
644 for (var i = 0; i < callbacks.length; ++i)
645 callbacks[i](content);
651 contentLoaded: function()
653 return this._contentLoaded;
657 * @param {number} lineNumber
658 * @param {number} columnNumber
659 * @return {?WebInspector.RawLocation}
661 uiLocationToRawLocation: function(lineNumber, columnNumber)
663 if (!this._sourceMapping)
665 return this._sourceMapping.uiLocationToRawLocation(this, lineNumber, columnNumber);
669 * @return {!Array.<!WebInspector.PresentationConsoleMessage>}
671 consoleMessages: function()
673 return this._consoleMessages;
677 * @param {!WebInspector.PresentationConsoleMessage} message
679 consoleMessageAdded: function(message)
681 this._consoleMessages.push(message);
682 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.ConsoleMessageAdded, message);
686 * @param {!WebInspector.PresentationConsoleMessage} message
688 consoleMessageRemoved: function(message)
690 this._consoleMessages.remove(message);
691 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.ConsoleMessageRemoved, message);
694 consoleMessagesCleared: function()
696 this._consoleMessages = [];
697 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.ConsoleMessagesCleared);
703 hasSourceMapping: function()
705 return !!this._sourceMapping;
709 * @param {?WebInspector.SourceMapping} sourceMapping
711 setSourceMapping: function(sourceMapping)
713 if (this._sourceMapping === sourceMapping)
715 this._sourceMapping = sourceMapping;
717 data.isIdentity = this._sourceMapping && this._sourceMapping.isIdentity();
718 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.SourceMappingChanged, data);
721 __proto__: WebInspector.Object.prototype
726 * @param {!WebInspector.UISourceCode} uiSourceCode
727 * @param {number} lineNumber
728 * @param {number} columnNumber
730 WebInspector.UILocation = function(uiSourceCode, lineNumber, columnNumber)
732 this.uiSourceCode = uiSourceCode;
733 this.lineNumber = lineNumber;
734 this.columnNumber = columnNumber;
737 WebInspector.UILocation.prototype = {
739 * @return {?WebInspector.RawLocation}
741 uiLocationToRawLocation: function()
743 return this.uiSourceCode.uiLocationToRawLocation(this.lineNumber, this.columnNumber);
751 return this.uiSourceCode.contentURL();
759 var linkText = this.uiSourceCode.displayName();
760 if (typeof this.lineNumber === "number")
761 linkText += ":" + (this.lineNumber + 1);
769 WebInspector.RawLocation = function()
775 * @param {!WebInspector.RawLocation} rawLocation
776 * @param {function(!WebInspector.UILocation):(boolean|undefined)} updateDelegate
778 WebInspector.LiveLocation = function(rawLocation, updateDelegate)
780 this._rawLocation = rawLocation;
781 this._updateDelegate = updateDelegate;
784 WebInspector.LiveLocation.prototype = {
787 var uiLocation = this.uiLocation();
790 if (this._updateDelegate(uiLocation))
795 * @return {!WebInspector.RawLocation}
797 rawLocation: function()
799 return this._rawLocation;
803 * @return {!WebInspector.UILocation}
805 uiLocation: function()
807 throw "Not implemented";
812 // Overridden by subclasses.
818 * @implements {WebInspector.ContentProvider}
819 * @param {!WebInspector.UISourceCode} uiSourceCode
820 * @param {?string|undefined} content
821 * @param {!Date} timestamp
823 WebInspector.Revision = function(uiSourceCode, content, timestamp)
825 this._uiSourceCode = uiSourceCode;
826 this._content = content;
827 this._timestamp = timestamp;
830 WebInspector.Revision._revisionHistoryRegistry = function()
832 if (!WebInspector.Revision._revisionHistoryRegistryObject) {
833 if (window.localStorage) {
834 var revisionHistory = window.localStorage["revision-history"];
836 WebInspector.Revision._revisionHistoryRegistryObject = revisionHistory ? JSON.parse(revisionHistory) : {};
838 WebInspector.Revision._revisionHistoryRegistryObject = {};
841 WebInspector.Revision._revisionHistoryRegistryObject = {};
843 return WebInspector.Revision._revisionHistoryRegistryObject;
846 WebInspector.Revision.filterOutStaleRevisions = function()
848 if (!window.localStorage)
851 var registry = WebInspector.Revision._revisionHistoryRegistry();
852 var filteredRegistry = {};
853 for (var url in registry) {
854 var historyItems = registry[url];
855 var filteredHistoryItems = [];
856 for (var i = 0; historyItems && i < historyItems.length; ++i) {
857 var historyItem = historyItems[i];
858 if (historyItem.loaderId === WebInspector.resourceTreeModel.mainFrame.loaderId) {
859 filteredHistoryItems.push(historyItem);
860 filteredRegistry[url] = filteredHistoryItems;
862 delete window.localStorage[historyItem.key];
865 WebInspector.Revision._revisionHistoryRegistryObject = filteredRegistry;
869 window.localStorage["revision-history"] = JSON.stringify(filteredRegistry);
872 // Schedule async storage.
873 setTimeout(persist, 0);
876 WebInspector.Revision.prototype = {
878 * @return {!WebInspector.UISourceCode}
882 return this._uiSourceCode;
890 return this._timestamp;
898 return this._content || null;
901 revertToThis: function()
904 * @param {string} content
905 * @this {WebInspector.Revision}
907 function revert(content)
909 if (this._uiSourceCode._content !== content)
910 this._uiSourceCode.addRevision(content);
912 this.requestContent(revert.bind(this));
918 contentURL: function()
920 return this._uiSourceCode.originURL();
924 * @return {!WebInspector.ResourceType}
926 contentType: function()
928 return this._uiSourceCode.contentType();
932 * @param {function(string)} callback
934 requestContent: function(callback)
936 callback(this._content || "");
940 * @param {string} query
941 * @param {boolean} caseSensitive
942 * @param {boolean} isRegex
943 * @param {function(!Array.<!WebInspector.ContentProvider.SearchMatch>)} callback
945 searchInContent: function(query, caseSensitive, isRegex, callback)
952 if (this._uiSourceCode.project().type() === WebInspector.projectTypes.FileSystem)
955 if (!window.localStorage)
958 var url = this.contentURL();
959 if (!url || url.startsWith("inspector://"))
962 var loaderId = WebInspector.resourceTreeModel.mainFrame.loaderId;
963 var timestamp = this.timestamp.getTime();
964 var key = "revision-history|" + url + "|" + loaderId + "|" + timestamp;
966 var registry = WebInspector.Revision._revisionHistoryRegistry();
968 var historyItems = registry[url];
971 registry[url] = historyItems;
973 historyItems.push({url: url, loaderId: loaderId, timestamp: timestamp, key: key});
976 * @this {WebInspector.Revision}
980 window.localStorage[key] = this._content;
981 window.localStorage["revision-history"] = JSON.stringify(registry);
984 // Schedule async storage.
985 setTimeout(persist.bind(this), 0);