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
42 WebInspector.UISourceCode = function(project, parentPath, name, originURL, url, contentType)
44 this._project = project;
45 this._parentPath = parentPath;
47 this._originURL = originURL;
49 this._contentType = contentType;
50 /** @type {!Array.<function(?string)>} */
51 this._requestContentCallbacks = [];
52 /** @type {!Array.<!WebInspector.PresentationConsoleMessage>} */
53 this._consoleMessages = [];
55 /** @type {!Array.<!WebInspector.Revision>} */
57 if (!this._project.isServiceProject() && this._url)
58 this._restoreRevisionHistory();
61 WebInspector.UISourceCode.Events = {
62 WorkingCopyChanged: "WorkingCopyChanged",
63 WorkingCopyCommitted: "WorkingCopyCommitted",
64 TitleChanged: "TitleChanged",
65 SavedStateUpdated: "SavedStateUpdated",
66 ConsoleMessageAdded: "ConsoleMessageAdded",
67 ConsoleMessageRemoved: "ConsoleMessageRemoved",
68 ConsoleMessagesCleared: "ConsoleMessagesCleared",
69 SourceMappingChanged: "SourceMappingChanged",
72 WebInspector.UISourceCode.prototype = {
92 parentPath: function()
94 return this._parentPath;
102 return this._parentPath ? this._parentPath + "/" + this._name : this._name;
108 fullDisplayName: function()
110 return this._project.displayName() + "/" + (this._parentPath ? this._parentPath + "/" : "") + this.displayName(true);
114 * @param {boolean=} skipTrim
117 displayName: function(skipTrim)
119 var displayName = this.name() || WebInspector.UIString("(index)");
120 return skipTrim ? displayName : displayName.trimEnd(100);
128 var path = this.path();
129 if (!this._project.id())
132 return this._project.id();
133 return this._project.id() + "/" + path;
139 originURL: function()
141 return this._originURL;
147 canRename: function()
149 return this._project.canRename();
153 * @param {string} newName
154 * @param {function(boolean)} callback
156 rename: function(newName, callback)
158 this._project.rename(this, newName, innerCallback.bind(this));
161 * @param {boolean} success
162 * @param {string=} newName
163 * @param {string=} newURL
164 * @param {string=} newOriginURL
165 * @param {!WebInspector.ResourceType=} newContentType
166 * @this {WebInspector.UISourceCode}
168 function innerCallback(success, newName, newURL, newOriginURL, newContentType)
171 this._updateName(/** @type {string} */ (newName), /** @type {string} */ (newURL), /** @type {string} */ (newOriginURL), /** @type {!WebInspector.ResourceType} */ (newContentType));
178 this._project.deleteFile(this.path());
182 * @param {string} name
183 * @param {string} url
184 * @param {string} originURL
185 * @param {!WebInspector.ResourceType=} contentType
187 _updateName: function(name, url, originURL, contentType)
189 var oldURI = this.uri();
194 this._originURL = originURL;
196 this._contentType = contentType;
197 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.TitleChanged, oldURI);
203 contentURL: function()
205 return this.originURL();
209 * @return {!WebInspector.ResourceType}
211 contentType: function()
213 return this._contentType;
217 * @return {?WebInspector.ScriptFile}
219 scriptFile: function()
221 return this._scriptFile;
225 * @param {?WebInspector.ScriptFile} scriptFile
227 setScriptFile: function(scriptFile)
229 this._scriptFile = scriptFile;
233 * @return {!WebInspector.Project}
237 return this._project;
241 * @param {function(?Date, ?number)} callback
243 requestMetadata: function(callback)
245 this._project.requestMetadata(this, callback);
249 * @param {function(?string)} callback
251 requestContent: function(callback)
253 if (this._content || this._contentLoaded) {
254 callback(this._content);
257 this._requestContentCallbacks.push(callback);
258 if (this._requestContentCallbacks.length === 1)
259 this._project.requestFileContent(this, this._fireContentAvailable.bind(this));
263 * @param {function()} callback
265 _pushCheckContentUpdatedCallback: function(callback)
267 if (!this._checkContentUpdatedCallbacks)
268 this._checkContentUpdatedCallbacks = [];
269 this._checkContentUpdatedCallbacks.push(callback);
272 _terminateContentCheck: function()
274 delete this._checkingContent;
275 if (this._checkContentUpdatedCallbacks) {
276 this._checkContentUpdatedCallbacks.forEach(function(callback) { callback(); });
277 delete this._checkContentUpdatedCallbacks;
282 * @param {function()=} callback
284 checkContentUpdated: function(callback)
286 callback = callback || function() {};
287 if (!this._project.canSetFileContent()) {
291 this._pushCheckContentUpdatedCallback(callback);
293 if (this._checkingContent) {
296 this._checkingContent = true;
297 this._project.requestFileContent(this, contentLoaded.bind(this));
300 * @param {?string} updatedContent
301 * @this {WebInspector.UISourceCode}
303 function contentLoaded(updatedContent)
305 if (updatedContent === null) {
306 var workingCopy = this.workingCopy();
307 this._commitContent("", false);
308 this.setWorkingCopy(workingCopy);
309 this._terminateContentCheck();
312 if (typeof this._lastAcceptedContent === "string" && this._lastAcceptedContent === updatedContent) {
313 this._terminateContentCheck();
316 if (this._content === updatedContent) {
317 delete this._lastAcceptedContent;
318 this._terminateContentCheck();
322 if (!this.isDirty()) {
323 this._commitContent(updatedContent, false);
324 this._terminateContentCheck();
328 var shouldUpdate = window.confirm(WebInspector.UIString("This file was changed externally. Would you like to reload it?"));
330 this._commitContent(updatedContent, false);
332 this._lastAcceptedContent = updatedContent;
333 this._terminateContentCheck();
338 * @param {function(?string)} callback
340 requestOriginalContent: function(callback)
342 this._project.requestFileContent(this, callback);
346 * @param {string} content
347 * @param {boolean} shouldSetContentInProject
349 _commitContent: function(content, shouldSetContentInProject)
351 delete this._lastAcceptedContent;
352 this._content = content;
353 this._contentLoaded = true;
355 var lastRevision = this.history.length ? this.history[this.history.length - 1] : null;
356 if (!lastRevision || lastRevision._content !== this._content) {
357 var revision = new WebInspector.Revision(this, this._content, new Date());
358 this.history.push(revision);
362 this._innerResetWorkingCopy();
363 this._hasCommittedChanges = true;
364 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyCommitted);
365 if (this._url && WebInspector.fileManager.isURLSaved(this._url))
366 this._saveURLWithFileManager(false, this._content);
367 if (shouldSetContentInProject)
368 this._project.setFileContent(this, this._content, function() { });
372 * @param {boolean} forceSaveAs
373 * @param {?string} content
375 _saveURLWithFileManager: function(forceSaveAs, content)
377 WebInspector.fileManager.save(this._url, /** @type {string} */ (content), forceSaveAs, callback.bind(this));
378 WebInspector.fileManager.close(this._url);
381 * @param {boolean} accepted
382 * @this {WebInspector.UISourceCode}
384 function callback(accepted)
388 this._savedWithFileManager = true;
389 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.SavedStateUpdated);
394 * @param {boolean} forceSaveAs
396 saveToFileSystem: function(forceSaveAs)
398 if (this.isDirty()) {
399 this._saveURLWithFileManager(forceSaveAs, this.workingCopy());
400 this.commitWorkingCopy(function() { });
403 this.requestContent(this._saveURLWithFileManager.bind(this, forceSaveAs));
409 hasUnsavedCommittedChanges: function()
411 if (this._savedWithFileManager || this.project().canSetFileContent() || this._project.isServiceProject())
413 if (this._project.workspace().hasResourceContentTrackingExtensions())
415 return !!this._hasCommittedChanges;
419 * @param {string} content
421 addRevision: function(content)
423 this._commitContent(content, true);
426 _restoreRevisionHistory: function()
428 if (!window.localStorage)
431 var registry = WebInspector.Revision._revisionHistoryRegistry();
432 var historyItems = registry[this.url];
436 function filterOutStale(historyItem)
438 // FIXME: Main frame might not have been loaded yet when uiSourceCodes for snippets are created.
439 if (!WebInspector.resourceTreeModel.mainFrame)
441 return historyItem.loaderId === WebInspector.resourceTreeModel.mainFrame.loaderId;
444 historyItems = historyItems.filter(filterOutStale);
445 if (!historyItems.length)
448 for (var i = 0; i < historyItems.length; ++i) {
449 var content = window.localStorage[historyItems[i].key];
450 var timestamp = new Date(historyItems[i].timestamp);
451 var revision = new WebInspector.Revision(this, content, timestamp);
452 this.history.push(revision);
454 this._content = this.history[this.history.length - 1].content;
455 this._hasCommittedChanges = true;
456 this._contentLoaded = true;
459 _clearRevisionHistory: function()
461 if (!window.localStorage)
464 var registry = WebInspector.Revision._revisionHistoryRegistry();
465 var historyItems = registry[this.url];
466 for (var i = 0; historyItems && i < historyItems.length; ++i)
467 delete window.localStorage[historyItems[i].key];
468 delete registry[this.url];
469 window.localStorage["revision-history"] = JSON.stringify(registry);
472 revertToOriginal: function()
475 * @this {WebInspector.UISourceCode}
476 * @param {?string} content
478 function callback(content)
480 if (typeof content !== "string")
483 this.addRevision(content);
486 this.requestOriginalContent(callback.bind(this));
488 WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, {
489 action: WebInspector.UserMetrics.UserActionNames.ApplyOriginalContent,
495 * @param {function(!WebInspector.UISourceCode)} callback
497 revertAndClearHistory: function(callback)
500 * @this {WebInspector.UISourceCode}
501 * @param {?string} content
503 function revert(content)
505 if (typeof content !== "string")
508 this.addRevision(content);
509 this._clearRevisionHistory();
514 this.requestOriginalContent(revert.bind(this));
516 WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, {
517 action: WebInspector.UserMetrics.UserActionNames.RevertRevision,
525 workingCopy: function()
527 if (this._workingCopyGetter) {
528 this._workingCopy = this._workingCopyGetter();
529 delete this._workingCopyGetter;
532 return this._workingCopy;
533 return this._content;
536 resetWorkingCopy: function()
538 this._innerResetWorkingCopy();
539 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged);
542 _innerResetWorkingCopy: function()
544 delete this._workingCopy;
545 delete this._workingCopyGetter;
549 * @param {string} newWorkingCopy
551 setWorkingCopy: function(newWorkingCopy)
553 this._workingCopy = newWorkingCopy;
554 delete this._workingCopyGetter;
555 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged);
558 setWorkingCopyGetter: function(workingCopyGetter)
560 this._workingCopyGetter = workingCopyGetter;
561 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged);
564 removeWorkingCopyGetter: function()
566 if (!this._workingCopyGetter)
568 this._workingCopy = this._workingCopyGetter();
569 delete this._workingCopyGetter;
573 * @param {function(?string)} callback
575 commitWorkingCopy: function(callback)
577 if (!this.isDirty()) {
582 this._commitContent(this.workingCopy(), true);
585 WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, {
586 action: WebInspector.UserMetrics.UserActionNames.FileSaved,
596 return typeof this._workingCopy !== "undefined" || typeof this._workingCopyGetter !== "undefined";
602 highlighterType: function()
604 var lastIndexOfDot = this._name.lastIndexOf(".");
605 var extension = lastIndexOfDot !== -1 ? this._name.substr(lastIndexOfDot + 1) : "";
606 var indexOfQuestionMark = extension.indexOf("?");
607 if (indexOfQuestionMark !== -1)
608 extension = extension.substr(0, indexOfQuestionMark);
609 var mimeType = WebInspector.ResourceType.mimeTypesForExtensions[extension.toLowerCase()];
610 return mimeType || this.contentType().canonicalMimeType();
618 return this._content;
622 * @param {string} query
623 * @param {boolean} caseSensitive
624 * @param {boolean} isRegex
625 * @param {function(!Array.<!WebInspector.ContentProvider.SearchMatch>)} callback
627 searchInContent: function(query, caseSensitive, isRegex, callback)
629 var content = this.content();
631 var provider = new WebInspector.StaticContentProvider(this.contentType(), content);
632 provider.searchInContent(query, caseSensitive, isRegex, callback);
636 this._project.searchInFileContent(this, query, caseSensitive, isRegex, callback);
640 * @param {?string} content
642 _fireContentAvailable: function(content)
644 this._contentLoaded = true;
645 this._content = content;
647 var callbacks = this._requestContentCallbacks.slice();
648 this._requestContentCallbacks = [];
649 for (var i = 0; i < callbacks.length; ++i)
650 callbacks[i](content);
656 contentLoaded: function()
658 return this._contentLoaded;
662 * @param {number} lineNumber
663 * @param {number} columnNumber
664 * @return {?WebInspector.RawLocation}
666 uiLocationToRawLocation: function(lineNumber, columnNumber)
668 if (!this._sourceMapping)
670 return this._sourceMapping.uiLocationToRawLocation(this, lineNumber, columnNumber);
674 * @return {!Array.<!WebInspector.PresentationConsoleMessage>}
676 consoleMessages: function()
678 return this._consoleMessages;
682 * @param {!WebInspector.PresentationConsoleMessage} message
684 consoleMessageAdded: function(message)
686 this._consoleMessages.push(message);
687 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.ConsoleMessageAdded, message);
691 * @param {!WebInspector.PresentationConsoleMessage} message
693 consoleMessageRemoved: function(message)
695 this._consoleMessages.remove(message);
696 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.ConsoleMessageRemoved, message);
699 consoleMessagesCleared: function()
701 this._consoleMessages = [];
702 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.ConsoleMessagesCleared);
708 hasSourceMapping: function()
710 return !!this._sourceMapping;
714 * @param {?WebInspector.SourceMapping} sourceMapping
716 setSourceMapping: function(sourceMapping)
718 if (this._sourceMapping === sourceMapping)
720 this._sourceMapping = sourceMapping;
722 data.isIdentity = this._sourceMapping && this._sourceMapping.isIdentity();
723 this.dispatchEventToListeners(WebInspector.UISourceCode.Events.SourceMappingChanged, data);
727 * @param {number} lineNumber
728 * @param {number=} columnNumber
729 * @return {!WebInspector.UILocation}
731 uiLocation: function(lineNumber, columnNumber)
733 if (typeof columnNumber === "undefined")
735 return new WebInspector.UILocation(this, lineNumber, columnNumber);
738 __proto__: WebInspector.Object.prototype
743 * @param {!WebInspector.UISourceCode} uiSourceCode
744 * @param {number} lineNumber
745 * @param {number} columnNumber
747 WebInspector.UILocation = function(uiSourceCode, lineNumber, columnNumber)
749 this.uiSourceCode = uiSourceCode;
750 this.lineNumber = lineNumber;
751 this.columnNumber = columnNumber;
754 WebInspector.UILocation.prototype = {
756 * @return {?WebInspector.RawLocation}
758 uiLocationToRawLocation: function()
760 return this.uiSourceCode.uiLocationToRawLocation(this.lineNumber, this.columnNumber);
768 var linkText = this.uiSourceCode.displayName();
769 if (typeof this.lineNumber === "number")
770 linkText += ":" + (this.lineNumber + 1);
778 WebInspector.RawLocation = function()
782 WebInspector.RawLocation.prototype = {
784 * @return {?WebInspector.UILocation}
786 toUILocation: function() { }
791 * @param {!WebInspector.RawLocation} rawLocation
792 * @param {function(!WebInspector.UILocation):(boolean|undefined)} updateDelegate
794 WebInspector.LiveLocation = function(rawLocation, updateDelegate)
796 this._rawLocation = rawLocation;
797 this._updateDelegate = updateDelegate;
800 WebInspector.LiveLocation.prototype = {
803 var uiLocation = this.uiLocation();
806 if (this._updateDelegate(uiLocation))
811 * @return {!WebInspector.RawLocation}
813 rawLocation: function()
815 return this._rawLocation;
819 * @return {!WebInspector.UILocation}
821 uiLocation: function()
823 throw "Not implemented";
828 // Overridden by subclasses.
834 * @implements {WebInspector.ContentProvider}
835 * @param {!WebInspector.UISourceCode} uiSourceCode
836 * @param {?string|undefined} content
837 * @param {!Date} timestamp
839 WebInspector.Revision = function(uiSourceCode, content, timestamp)
841 this._uiSourceCode = uiSourceCode;
842 this._content = content;
843 this._timestamp = timestamp;
846 WebInspector.Revision._revisionHistoryRegistry = function()
848 if (!WebInspector.Revision._revisionHistoryRegistryObject) {
849 if (window.localStorage) {
850 var revisionHistory = window.localStorage["revision-history"];
852 WebInspector.Revision._revisionHistoryRegistryObject = revisionHistory ? JSON.parse(revisionHistory) : {};
854 WebInspector.Revision._revisionHistoryRegistryObject = {};
857 WebInspector.Revision._revisionHistoryRegistryObject = {};
859 return WebInspector.Revision._revisionHistoryRegistryObject;
862 WebInspector.Revision.filterOutStaleRevisions = function()
864 if (!window.localStorage)
867 var registry = WebInspector.Revision._revisionHistoryRegistry();
868 var filteredRegistry = {};
869 for (var url in registry) {
870 var historyItems = registry[url];
871 var filteredHistoryItems = [];
872 for (var i = 0; historyItems && i < historyItems.length; ++i) {
873 var historyItem = historyItems[i];
874 if (historyItem.loaderId === WebInspector.resourceTreeModel.mainFrame.loaderId) {
875 filteredHistoryItems.push(historyItem);
876 filteredRegistry[url] = filteredHistoryItems;
878 delete window.localStorage[historyItem.key];
881 WebInspector.Revision._revisionHistoryRegistryObject = filteredRegistry;
885 window.localStorage["revision-history"] = JSON.stringify(filteredRegistry);
888 // Schedule async storage.
889 setTimeout(persist, 0);
892 WebInspector.Revision.prototype = {
894 * @return {!WebInspector.UISourceCode}
898 return this._uiSourceCode;
906 return this._timestamp;
914 return this._content || null;
917 revertToThis: function()
920 * @param {string} content
921 * @this {WebInspector.Revision}
923 function revert(content)
925 if (this._uiSourceCode._content !== content)
926 this._uiSourceCode.addRevision(content);
928 this.requestContent(revert.bind(this));
934 contentURL: function()
936 return this._uiSourceCode.originURL();
940 * @return {!WebInspector.ResourceType}
942 contentType: function()
944 return this._uiSourceCode.contentType();
948 * @param {function(string)} callback
950 requestContent: function(callback)
952 callback(this._content || "");
956 * @param {string} query
957 * @param {boolean} caseSensitive
958 * @param {boolean} isRegex
959 * @param {function(!Array.<!WebInspector.ContentProvider.SearchMatch>)} callback
961 searchInContent: function(query, caseSensitive, isRegex, callback)
968 if (this._uiSourceCode.project().type() === WebInspector.projectTypes.FileSystem)
971 if (!window.localStorage)
974 var url = this.contentURL();
975 if (!url || url.startsWith("inspector://"))
978 var loaderId = WebInspector.resourceTreeModel.mainFrame.loaderId;
979 var timestamp = this.timestamp.getTime();
980 var key = "revision-history|" + url + "|" + loaderId + "|" + timestamp;
982 var registry = WebInspector.Revision._revisionHistoryRegistry();
984 var historyItems = registry[url];
987 registry[url] = historyItems;
989 historyItems.push({url: url, loaderId: loaderId, timestamp: timestamp, key: key});
992 * @this {WebInspector.Revision}
996 window.localStorage[key] = this._content;
997 window.localStorage["revision-history"] = JSON.stringify(registry);
1000 // Schedule async storage.
1001 setTimeout(persist.bind(this), 0);