Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / devtools / front_end / workspace / UISourceCode.js
blob5c6597aff3557b60b93be09f899abedd25f386cc
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  */
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  */
43 WebInspector.UISourceCode = function(project, parentPath, name, originURL, url, contentType)
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 = [];
54     /** @type {!Array.<!WebInspector.Revision>} */
55     this.history = [];
58 /**
59  * @enum {string}
60  */
61 WebInspector.UISourceCode.Events = {
62     WorkingCopyChanged: "WorkingCopyChanged",
63     WorkingCopyCommitted: "WorkingCopyCommitted",
64     TitleChanged: "TitleChanged",
65     SavedStateUpdated: "SavedStateUpdated",
66     SourceMappingChanged: "SourceMappingChanged",
69 WebInspector.UISourceCode.prototype = {
70     /**
71      * @return {string}
72      */
73     networkURL: function()
74     {
75         return this._url;
76     },
78     /**
79      * @return {string}
80      */
81     name: function()
82     {
83         return this._name;
84     },
86     /**
87      * @return {string}
88      */
89     parentPath: function()
90     {
91         return this._parentPath;
92     },
94     /**
95      * @return {string}
96      */
97     path: function()
98     {
99         return this._parentPath ? this._parentPath + "/" + this._name : this._name;
100     },
102     /**
103      * @return {string}
104      */
105     fullDisplayName: function()
106     {
107         return this._project.displayName() + "/" + (this._parentPath ? this._parentPath + "/" : "") + this.displayName(true);
108     },
110     /**
111      * @param {boolean=} skipTrim
112      * @return {string}
113      */
114     displayName: function(skipTrim)
115     {
116         var displayName = this.name() || WebInspector.UIString("(index)");
117         return skipTrim ? displayName : displayName.trimEnd(100);
118     },
120     /**
121      * @return {string}
122      */
123     uri: function()
124     {
125         var path = this.path();
126         if (!this._project.url())
127             return path;
128         if (!path)
129             return this._project.url();
130         return this._project.url() + "/" + path;
131     },
133     /**
134      * @return {string}
135      */
136     originURL: function()
137     {
138         return this._originURL;
139     },
141     /**
142      * @return {boolean}
143      */
144     canRename: function()
145     {
146         return this._project.canRename();
147     },
149     /**
150      * @param {string} newName
151      * @param {function(boolean)} callback
152      */
153     rename: function(newName, callback)
154     {
155         this._project.rename(this, newName, innerCallback.bind(this));
157         /**
158          * @param {boolean} success
159          * @param {string=} newName
160          * @param {string=} newURL
161          * @param {string=} newOriginURL
162          * @param {!WebInspector.ResourceType=} newContentType
163          * @this {WebInspector.UISourceCode}
164          */
165         function innerCallback(success, newName, newURL, newOriginURL, newContentType)
166         {
167             if (success)
168                 this._updateName(/** @type {string} */ (newName), /** @type {string} */ (newURL), /** @type {string} */ (newOriginURL), /** @type {!WebInspector.ResourceType} */ (newContentType));
169             callback(success);
170         }
171     },
173     remove: function()
174     {
175         this._project.deleteFile(this.path());
176     },
178     /**
179      * @param {string} name
180      * @param {string} url
181      * @param {string} originURL
182      * @param {!WebInspector.ResourceType=} contentType
183      */
184     _updateName: function(name, url, originURL, contentType)
185     {
186         var oldURI = this.uri();
187         this._name = name;
188         if (url)
189             this._url = url;
190         if (originURL)
191             this._originURL = originURL;
192         if (contentType)
193             this._contentType = contentType;
194         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.TitleChanged, oldURI);
195     },
197     /**
198      * @override
199      * @return {string}
200      */
201     contentURL: function()
202     {
203         return this.originURL();
204     },
206     /**
207      * @override
208      * @return {!WebInspector.ResourceType}
209      */
210     contentType: function()
211     {
212         return this._contentType;
213     },
215     /**
216      * @return {!WebInspector.Project}
217      */
218     project: function()
219     {
220         return this._project;
221     },
223     /**
224      * @param {function(?Date, ?number)} callback
225      */
226     requestMetadata: function(callback)
227     {
228         this._project.requestMetadata(this, callback);
229     },
231     /**
232      * @override
233      * @param {function(?string)} callback
234      */
235     requestContent: function(callback)
236     {
237         if (this._content || this._contentLoaded) {
238             callback(this._content);
239             return;
240         }
241         this._requestContentCallbacks.push(callback);
242         if (this._requestContentCallbacks.length === 1)
243             this._project.requestFileContent(this, this._fireContentAvailable.bind(this));
244     },
246     /**
247      * @param {function()} callback
248      */
249     _pushCheckContentUpdatedCallback: function(callback)
250     {
251         if (!this._checkContentUpdatedCallbacks)
252             this._checkContentUpdatedCallbacks = [];
253         this._checkContentUpdatedCallbacks.push(callback);
254     },
256     _terminateContentCheck: function()
257     {
258         delete this._checkingContent;
259         if (this._checkContentUpdatedCallbacks) {
260             this._checkContentUpdatedCallbacks.forEach(function(callback) { callback(); });
261             delete this._checkContentUpdatedCallbacks;
262         }
263     },
265     /**
266      * @param {function()=} callback
267      */
268     checkContentUpdated: function(callback)
269     {
270         callback = callback || function() {};
271         if (!this._project.canSetFileContent()) {
272             callback();
273             return;
274         }
275         this._pushCheckContentUpdatedCallback(callback);
277         if (this._checkingContent) {
278             return;
279         }
280         this._checkingContent = true;
281         this._project.requestFileContent(this, contentLoaded.bind(this));
283         /**
284          * @param {?string} updatedContent
285          * @this {WebInspector.UISourceCode}
286          */
287         function contentLoaded(updatedContent)
288         {
289             if (updatedContent === null) {
290                 var workingCopy = this.workingCopy();
291                 this._commitContent("", false);
292                 this.setWorkingCopy(workingCopy);
293                 this._terminateContentCheck();
294                 return;
295             }
296             if (typeof this._lastAcceptedContent === "string" && this._lastAcceptedContent === updatedContent) {
297                 this._terminateContentCheck();
298                 return;
299             }
301             if (this._content === updatedContent) {
302                 delete this._lastAcceptedContent;
303                 this._terminateContentCheck();
304                 return;
305             }
307             if (!this.isDirty() || this._workingCopy === updatedContent) {
308                 this._commitContent(updatedContent, false);
309                 this._terminateContentCheck();
310                 return;
311             }
313             var shouldUpdate = window.confirm(WebInspector.UIString("This file was changed externally. Would you like to reload it?"));
314             if (shouldUpdate)
315                 this._commitContent(updatedContent, false);
316             else
317                 this._lastAcceptedContent = updatedContent;
318             this._terminateContentCheck();
319         }
320     },
322     /**
323      * @param {function(?string)} callback
324      */
325     requestOriginalContent: function(callback)
326     {
327         this._project.requestFileContent(this, callback);
328     },
330     /**
331      * @param {string} content
332      * @param {boolean} shouldSetContentInProject
333      */
334     _commitContent: function(content, shouldSetContentInProject)
335     {
336         delete this._lastAcceptedContent;
337         this._content = content;
338         this._contentLoaded = true;
340         var lastRevision = this.history.length ? this.history[this.history.length - 1] : null;
341         if (!lastRevision || lastRevision._content !== this._content) {
342             var revision = new WebInspector.Revision(this, this._content, new Date());
343             this.history.push(revision);
344         }
346         this._innerResetWorkingCopy();
347         this._hasCommittedChanges = true;
348         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyCommitted);
349         if (this._url && WebInspector.fileManager.isURLSaved(this._url))
350             this._saveURLWithFileManager(false, this._content);
351         if (shouldSetContentInProject)
352             this._project.setFileContent(this, this._content, function() { });
353     },
355     /**
356      * @param {boolean} forceSaveAs
357      * @param {?string} content
358      */
359     _saveURLWithFileManager: function(forceSaveAs, content)
360     {
361         WebInspector.fileManager.save(this._url, /** @type {string} */ (content), forceSaveAs, callback.bind(this));
362         WebInspector.fileManager.close(this._url);
364         /**
365          * @param {boolean} accepted
366          * @this {WebInspector.UISourceCode}
367          */
368         function callback(accepted)
369         {
370             this._savedWithFileManager = accepted;
371             if (accepted)
372                 this._hasCommittedChanges = false;
373             this.dispatchEventToListeners(WebInspector.UISourceCode.Events.SavedStateUpdated);
374         }
375     },
377     /**
378      * @param {boolean} forceSaveAs
379      */
380     save: function(forceSaveAs)
381     {
382         if (this.project().type() === WebInspector.projectTypes.FileSystem || this.project().type() === WebInspector.projectTypes.Snippets) {
383             this.commitWorkingCopy();
384             return;
385         }
386         if (this.isDirty()) {
387             this._saveURLWithFileManager(forceSaveAs, this.workingCopy());
388             this.commitWorkingCopy();
389             return;
390         }
391         this.requestContent(this._saveURLWithFileManager.bind(this, forceSaveAs));
392     },
394     /**
395      * @return {boolean}
396      */
397     hasUnsavedCommittedChanges: function()
398     {
399         if (this._savedWithFileManager || this.project().canSetFileContent() || this._project.isServiceProject())
400             return false;
401         if (this._project.workspace().hasResourceContentTrackingExtensions())
402             return false;
403         return !!this._hasCommittedChanges;
404     },
406     /**
407      * @param {string} content
408      */
409     addRevision: function(content)
410     {
411         this._commitContent(content, true);
412     },
414     revertToOriginal: function()
415     {
416         /**
417          * @this {WebInspector.UISourceCode}
418          * @param {?string} content
419          */
420         function callback(content)
421         {
422             if (typeof content !== "string")
423                 return;
425             this.addRevision(content);
426         }
428         WebInspector.userMetrics.RevisionApplied.record();
429         this.requestOriginalContent(callback.bind(this));
430     },
432     /**
433      * @param {function(!WebInspector.UISourceCode)} callback
434      */
435     revertAndClearHistory: function(callback)
436     {
437         /**
438          * @this {WebInspector.UISourceCode}
439          * @param {?string} content
440          */
441         function revert(content)
442         {
443             if (typeof content !== "string")
444                 return;
446             this.addRevision(content);
447             this.history = [];
448             callback(this);
449         }
451         WebInspector.userMetrics.RevisionApplied.record();
452         this.requestOriginalContent(revert.bind(this));
453     },
455     /**
456      * @return {string}
457      */
458     workingCopy: function()
459     {
460         if (this._workingCopyGetter) {
461             this._workingCopy = this._workingCopyGetter();
462             delete this._workingCopyGetter;
463         }
464         if (this.isDirty())
465             return this._workingCopy;
466         return this._content;
467     },
469     resetWorkingCopy: function()
470     {
471         this._innerResetWorkingCopy();
472         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged);
473     },
475     _innerResetWorkingCopy: function()
476     {
477         delete this._workingCopy;
478         delete this._workingCopyGetter;
479     },
481     /**
482      * @param {string} newWorkingCopy
483      */
484     setWorkingCopy: function(newWorkingCopy)
485     {
486         this._workingCopy = newWorkingCopy;
487         delete this._workingCopyGetter;
488         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged);
489         this._project.workspace().dispatchEventToListeners(WebInspector.Workspace.Events.UISourceCodeWorkingCopyChanged, { uiSourceCode: this });
490     },
492     setWorkingCopyGetter: function(workingCopyGetter)
493     {
494         this._workingCopyGetter = workingCopyGetter;
495         this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged);
496         this._project.workspace().dispatchEventToListeners(WebInspector.Workspace.Events.UISourceCodeWorkingCopyChanged, { uiSourceCode: this  });
497     },
499     removeWorkingCopyGetter: function()
500     {
501         if (!this._workingCopyGetter)
502             return;
503         this._workingCopy = this._workingCopyGetter();
504         delete this._workingCopyGetter;
505     },
507     commitWorkingCopy: function()
508     {
509         if (this.isDirty())
510             this._commitContent(this.workingCopy(), true);
511     },
513     /**
514      * @return {boolean}
515      */
516     isDirty: function()
517     {
518         return typeof this._workingCopy !== "undefined" || typeof this._workingCopyGetter !== "undefined";
519     },
521     /**
522      * @return {string}
523      */
524     extension: function()
525     {
526         var lastIndexOfDot = this._name.lastIndexOf(".");
527         var extension = lastIndexOfDot !== -1 ? this._name.substr(lastIndexOfDot + 1) : "";
528         var indexOfQuestionMark = extension.indexOf("?");
529         if (indexOfQuestionMark !== -1)
530             extension = extension.substr(0, indexOfQuestionMark);
531         return extension;
532     },
534     /**
535      * @return {?string}
536      */
537     content: function()
538     {
539         return this._content;
540     },
542     /**
543      * @override
544      * @param {string} query
545      * @param {boolean} caseSensitive
546      * @param {boolean} isRegex
547      * @param {function(!Array.<!WebInspector.ContentProvider.SearchMatch>)} callback
548      */
549     searchInContent: function(query, caseSensitive, isRegex, callback)
550     {
551         var content = this.content();
552         if (content) {
553             WebInspector.StaticContentProvider.searchInContent(content, query, caseSensitive, isRegex, callback);
554             return;
555         }
557         this._project.searchInFileContent(this, query, caseSensitive, isRegex, callback);
558     },
560     /**
561      * @param {?string} content
562      */
563     _fireContentAvailable: function(content)
564     {
565         this._contentLoaded = true;
566         this._content = content;
568         var callbacks = this._requestContentCallbacks.slice();
569         this._requestContentCallbacks = [];
570         for (var i = 0; i < callbacks.length; ++i)
571             callbacks[i](content);
572     },
574     /**
575      * @return {boolean}
576      */
577     contentLoaded: function()
578     {
579         return this._contentLoaded;
580     },
582     /**
583      * @param {number} lineNumber
584      * @param {number=} columnNumber
585      * @return {!WebInspector.UILocation}
586      */
587     uiLocation: function(lineNumber, columnNumber)
588     {
589         if (typeof columnNumber === "undefined")
590             columnNumber = 0;
591         return new WebInspector.UILocation(this, lineNumber, columnNumber);
592     },
594     __proto__: WebInspector.Object.prototype
598  * @constructor
599  * @param {!WebInspector.UISourceCode} uiSourceCode
600  * @param {number} lineNumber
601  * @param {number} columnNumber
602  */
603 WebInspector.UILocation = function(uiSourceCode, lineNumber, columnNumber)
605     this.uiSourceCode = uiSourceCode;
606     this.lineNumber = lineNumber;
607     this.columnNumber = columnNumber;
610 WebInspector.UILocation.prototype = {
611     /**
612      * @return {string}
613      */
614     linkText: function()
615     {
616         var linkText = this.uiSourceCode.displayName();
617         if (typeof this.lineNumber === "number")
618             linkText += ":" + (this.lineNumber + 1);
619         return linkText;
620     },
622     /**
623      * @return {string}
624      */
625     id: function()
626     {
627         return this.uiSourceCode.project().id() + ":" + this.uiSourceCode.uri() + ":" + this.lineNumber + ":" + this.columnNumber;
628     },
630     /**
631      * @return {string}
632      */
633     toUIString: function()
634     {
635         return this.uiSourceCode.uri() + ":" + (this.lineNumber + 1);
636     }
640  * @constructor
641  * @implements {WebInspector.ContentProvider}
642  * @param {!WebInspector.UISourceCode} uiSourceCode
643  * @param {?string|undefined} content
644  * @param {!Date} timestamp
645  */
646 WebInspector.Revision = function(uiSourceCode, content, timestamp)
648     this._uiSourceCode = uiSourceCode;
649     this._content = content;
650     this._timestamp = timestamp;
653 WebInspector.Revision.prototype = {
654     /**
655      * @return {!WebInspector.UISourceCode}
656      */
657     get uiSourceCode()
658     {
659         return this._uiSourceCode;
660     },
662     /**
663      * @return {!Date}
664      */
665     get timestamp()
666     {
667         return this._timestamp;
668     },
670     /**
671      * @return {?string}
672      */
673     get content()
674     {
675         return this._content || null;
676     },
678     revertToThis: function()
679     {
680         /**
681          * @param {string} content
682          * @this {WebInspector.Revision}
683          */
684         function revert(content)
685         {
686             if (this._uiSourceCode._content !== content)
687                 this._uiSourceCode.addRevision(content);
688         }
689         WebInspector.userMetrics.RevisionApplied.record();
690         this.requestContent(revert.bind(this));
691     },
693     /**
694      * @override
695      * @return {string}
696      */
697     contentURL: function()
698     {
699         return this._uiSourceCode.originURL();
700     },
702     /**
703      * @override
704      * @return {!WebInspector.ResourceType}
705      */
706     contentType: function()
707     {
708         return this._uiSourceCode.contentType();
709     },
711     /**
712      * @override
713      * @param {function(string)} callback
714      */
715     requestContent: function(callback)
716     {
717         callback(this._content || "");
718     },
720     /**
721      * @override
722      * @param {string} query
723      * @param {boolean} caseSensitive
724      * @param {boolean} isRegex
725      * @param {function(!Array.<!WebInspector.ContentProvider.SearchMatch>)} callback
726      */
727     searchInContent: function(query, caseSensitive, isRegex, callback)
728     {
729         callback([]);
730     }