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
54    /** @type {!Array.<!WebInspector.Revision>} */
55    this.history = [];
56}
57
58/**
59 * @enum {string}
60 */
61WebInspector.UISourceCode.Events = {
62    WorkingCopyChanged: "WorkingCopyChanged",
63    WorkingCopyCommitted: "WorkingCopyCommitted",
64    TitleChanged: "TitleChanged",
65    SavedStateUpdated: "SavedStateUpdated",
66    SourceMappingChanged: "SourceMappingChanged",
67}
68
69WebInspector.UISourceCode.prototype = {
70    /**
71     * @return {string}
72     */
73    get url()
74    {
75        return this._url;
76    },
77
78    /**
79     * @return {string}
80     */
81    name: function()
82    {
83        return this._name;
84    },
85
86    /**
87     * @return {string}
88     */
89    parentPath: function()
90    {
91        return this._parentPath;
92    },
93
94    /**
95     * @return {string}
96     */
97    path: function()
98    {
99        return this._parentPath ? this._parentPath + "/" + this._name : this._name;
100    },
101
102    /**
103     * @return {string}
104     */
105    fullDisplayName: function()
106    {
107        return this._project.displayName() + "/" + (this._parentPath ? this._parentPath + "/" : "") + this.displayName(true);
108    },
109
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    },
119
120    /**
121     * @return {string}
122     */
123    uri: function()
124    {
125        var path = this.path();
126        if (!this._project.id())
127            return path;
128        if (!path)
129            return this._project.id();
130        return this._project.id() + "/" + path;
131    },
132
133    /**
134     * @return {string}
135     */
136    originURL: function()
137    {
138        return this._originURL;
139    },
140
141    /**
142     * @return {boolean}
143     */
144    canRename: function()
145    {
146        return this._project.canRename();
147    },
148
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));
156
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    },
172
173    remove: function()
174    {
175        this._project.deleteFile(this.path());
176    },
177
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    },
196
197    /**
198     * @return {string}
199     */
200    contentURL: function()
201    {
202        return this.originURL();
203    },
204
205    /**
206     * @return {!WebInspector.ResourceType}
207     */
208    contentType: function()
209    {
210        return this._contentType;
211    },
212
213    /**
214     * @return {!WebInspector.Project}
215     */
216    project: function()
217    {
218        return this._project;
219    },
220
221    /**
222     * @param {function(?Date, ?number)} callback
223     */
224    requestMetadata: function(callback)
225    {
226        this._project.requestMetadata(this, callback);
227    },
228
229    /**
230     * @param {function(?string)} callback
231     */
232    requestContent: function(callback)
233    {
234        if (this._content || this._contentLoaded) {
235            callback(this._content);
236            return;
237        }
238        this._requestContentCallbacks.push(callback);
239        if (this._requestContentCallbacks.length === 1)
240            this._project.requestFileContent(this, this._fireContentAvailable.bind(this));
241    },
242
243    /**
244     * @param {function()} callback
245     */
246    _pushCheckContentUpdatedCallback: function(callback)
247    {
248        if (!this._checkContentUpdatedCallbacks)
249            this._checkContentUpdatedCallbacks = [];
250        this._checkContentUpdatedCallbacks.push(callback);
251    },
252
253    _terminateContentCheck: function()
254    {
255        delete this._checkingContent;
256        if (this._checkContentUpdatedCallbacks) {
257            this._checkContentUpdatedCallbacks.forEach(function(callback) { callback(); });
258            delete this._checkContentUpdatedCallbacks;
259        }
260    },
261
262    /**
263     * @param {function()=} callback
264     */
265    checkContentUpdated: function(callback)
266    {
267        callback = callback || function() {};
268        if (!this._project.canSetFileContent()) {
269            callback();
270            return;
271        }
272        this._pushCheckContentUpdatedCallback(callback);
273
274        if (this._checkingContent) {
275            return;
276        }
277        this._checkingContent = true;
278        this._project.requestFileContent(this, contentLoaded.bind(this));
279
280        /**
281         * @param {?string} updatedContent
282         * @this {WebInspector.UISourceCode}
283         */
284        function contentLoaded(updatedContent)
285        {
286            if (updatedContent === null) {
287                var workingCopy = this.workingCopy();
288                this._commitContent("", false);
289                this.setWorkingCopy(workingCopy);
290                this._terminateContentCheck();
291                return;
292            }
293            if (typeof this._lastAcceptedContent === "string" && this._lastAcceptedContent === updatedContent) {
294                this._terminateContentCheck();
295                return;
296            }
297            if (this._content === updatedContent) {
298                delete this._lastAcceptedContent;
299                this._terminateContentCheck();
300                return;
301            }
302
303            if (!this.isDirty()) {
304                this._commitContent(updatedContent, false);
305                this._terminateContentCheck();
306                return;
307            }
308
309            var shouldUpdate = window.confirm(WebInspector.UIString("This file was changed externally. Would you like to reload it?"));
310            if (shouldUpdate)
311                this._commitContent(updatedContent, false);
312            else
313                this._lastAcceptedContent = updatedContent;
314            this._terminateContentCheck();
315        }
316    },
317
318    /**
319     * @param {function(?string)} callback
320     */
321    requestOriginalContent: function(callback)
322    {
323        this._project.requestFileContent(this, callback);
324    },
325
326    /**
327     * @param {string} content
328     * @param {boolean} shouldSetContentInProject
329     */
330    _commitContent: function(content, shouldSetContentInProject)
331    {
332        delete this._lastAcceptedContent;
333        this._content = content;
334        this._contentLoaded = true;
335
336        var lastRevision = this.history.length ? this.history[this.history.length - 1] : null;
337        if (!lastRevision || lastRevision._content !== this._content) {
338            var revision = new WebInspector.Revision(this, this._content, new Date());
339            this.history.push(revision);
340        }
341
342        this._innerResetWorkingCopy();
343        this._hasCommittedChanges = true;
344        this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyCommitted);
345        if (this._url && WebInspector.fileManager.isURLSaved(this._url))
346            this._saveURLWithFileManager(false, this._content);
347        if (shouldSetContentInProject)
348            this._project.setFileContent(this, this._content, function() { });
349    },
350
351    /**
352     * @param {boolean} forceSaveAs
353     * @param {?string} content
354     */
355    _saveURLWithFileManager: function(forceSaveAs, content)
356    {
357        WebInspector.fileManager.save(this._url, /** @type {string} */ (content), forceSaveAs, callback.bind(this));
358        WebInspector.fileManager.close(this._url);
359
360        /**
361         * @param {boolean} accepted
362         * @this {WebInspector.UISourceCode}
363         */
364        function callback(accepted)
365        {
366            if (!accepted)
367                return;
368            this._savedWithFileManager = true;
369            this.dispatchEventToListeners(WebInspector.UISourceCode.Events.SavedStateUpdated);
370        }
371    },
372
373    /**
374     * @param {boolean} forceSaveAs
375     */
376    save: function(forceSaveAs)
377    {
378        if (this.project().type() === WebInspector.projectTypes.FileSystem || this.project().type() === WebInspector.projectTypes.Snippets) {
379            this.commitWorkingCopy();
380            return;
381        }
382        if (this.isDirty()) {
383            this._saveURLWithFileManager(forceSaveAs, this.workingCopy());
384            this.commitWorkingCopy();
385            return;
386        }
387        this.requestContent(this._saveURLWithFileManager.bind(this, forceSaveAs));
388    },
389
390    /**
391     * @return {boolean}
392     */
393    hasUnsavedCommittedChanges: function()
394    {
395        if (this._savedWithFileManager || this.project().canSetFileContent() || this._project.isServiceProject())
396            return false;
397        if (this._project.workspace().hasResourceContentTrackingExtensions())
398            return false;
399        return !!this._hasCommittedChanges;
400    },
401
402    /**
403     * @param {string} content
404     */
405    addRevision: function(content)
406    {
407        this._commitContent(content, true);
408    },
409
410    revertToOriginal: function()
411    {
412        /**
413         * @this {WebInspector.UISourceCode}
414         * @param {?string} content
415         */
416        function callback(content)
417        {
418            if (typeof content !== "string")
419                return;
420
421            this.addRevision(content);
422        }
423
424        this.requestOriginalContent(callback.bind(this));
425    },
426
427    /**
428     * @param {function(!WebInspector.UISourceCode)} callback
429     */
430    revertAndClearHistory: function(callback)
431    {
432        /**
433         * @this {WebInspector.UISourceCode}
434         * @param {?string} content
435         */
436        function revert(content)
437        {
438            if (typeof content !== "string")
439                return;
440
441            this.addRevision(content);
442            this.history = [];
443            callback(this);
444        }
445
446        this.requestOriginalContent(revert.bind(this));
447    },
448
449    /**
450     * @return {string}
451     */
452    workingCopy: function()
453    {
454        if (this._workingCopyGetter) {
455            this._workingCopy = this._workingCopyGetter();
456            delete this._workingCopyGetter;
457        }
458        if (this.isDirty())
459            return this._workingCopy;
460        return this._content;
461    },
462
463    resetWorkingCopy: function()
464    {
465        this._innerResetWorkingCopy();
466        this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged);
467    },
468
469    _innerResetWorkingCopy: function()
470    {
471        delete this._workingCopy;
472        delete this._workingCopyGetter;
473    },
474
475    /**
476     * @param {string} newWorkingCopy
477     */
478    setWorkingCopy: function(newWorkingCopy)
479    {
480        this._workingCopy = newWorkingCopy;
481        delete this._workingCopyGetter;
482        this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged);
483    },
484
485    setWorkingCopyGetter: function(workingCopyGetter)
486    {
487        this._workingCopyGetter = workingCopyGetter;
488        this.dispatchEventToListeners(WebInspector.UISourceCode.Events.WorkingCopyChanged);
489    },
490
491    removeWorkingCopyGetter: function()
492    {
493        if (!this._workingCopyGetter)
494            return;
495        this._workingCopy = this._workingCopyGetter();
496        delete this._workingCopyGetter;
497    },
498
499    commitWorkingCopy: function()
500    {
501        if (this.isDirty())
502            this._commitContent(this.workingCopy(), true);
503    },
504
505    /**
506     * @return {boolean}
507     */
508    isDirty: function()
509    {
510        return typeof this._workingCopy !== "undefined" || typeof this._workingCopyGetter !== "undefined";
511    },
512
513    /**
514     * @return {string}
515     */
516    highlighterType: function()
517    {
518        if (this._project.type() === WebInspector.projectTypes.Network)
519            return this.contentType().canonicalMimeType();
520        var lastIndexOfDot = this._name.lastIndexOf(".");
521        var extension = lastIndexOfDot !== -1 ? this._name.substr(lastIndexOfDot + 1) : "";
522        var indexOfQuestionMark = extension.indexOf("?");
523        if (indexOfQuestionMark !== -1)
524            extension = extension.substr(0, indexOfQuestionMark);
525        var mimeType = WebInspector.ResourceType.mimeTypesForExtensions[extension.toLowerCase()];
526        return mimeType || this.contentType().canonicalMimeType();
527    },
528
529    /**
530     * @return {?string}
531     */
532    content: function()
533    {
534        return this._content;
535    },
536
537    /**
538     * @param {string} query
539     * @param {boolean} caseSensitive
540     * @param {boolean} isRegex
541     * @param {function(!Array.<!WebInspector.ContentProvider.SearchMatch>)} callback
542     */
543    searchInContent: function(query, caseSensitive, isRegex, callback)
544    {
545        var content = this.content();
546        if (content) {
547            var provider = new WebInspector.StaticContentProvider(this.contentType(), content);
548            provider.searchInContent(query, caseSensitive, isRegex, callback);
549            return;
550        }
551
552        this._project.searchInFileContent(this, query, caseSensitive, isRegex, callback);
553    },
554
555    /**
556     * @param {?string} content
557     */
558    _fireContentAvailable: function(content)
559    {
560        this._contentLoaded = true;
561        this._content = content;
562
563        var callbacks = this._requestContentCallbacks.slice();
564        this._requestContentCallbacks = [];
565        for (var i = 0; i < callbacks.length; ++i)
566            callbacks[i](content);
567    },
568
569    /**
570     * @return {boolean}
571     */
572    contentLoaded: function()
573    {
574        return this._contentLoaded;
575    },
576
577    /**
578     * @param {number} lineNumber
579     * @param {number=} columnNumber
580     * @return {!WebInspector.UILocation}
581     */
582    uiLocation: function(lineNumber, columnNumber)
583    {
584        if (typeof columnNumber === "undefined")
585            columnNumber = 0;
586        return new WebInspector.UILocation(this, lineNumber, columnNumber);
587    },
588
589    __proto__: WebInspector.Object.prototype
590}
591
592/**
593 * @constructor
594 * @param {!WebInspector.UISourceCode} uiSourceCode
595 * @param {number} lineNumber
596 * @param {number} columnNumber
597 */
598WebInspector.UILocation = function(uiSourceCode, lineNumber, columnNumber)
599{
600    this.uiSourceCode = uiSourceCode;
601    this.lineNumber = lineNumber;
602    this.columnNumber = columnNumber;
603}
604
605WebInspector.UILocation.prototype = {
606    /**
607     * @return {string}
608     */
609    linkText: function()
610    {
611        var linkText = this.uiSourceCode.displayName();
612        if (typeof this.lineNumber === "number")
613            linkText += ":" + (this.lineNumber + 1);
614        return linkText;
615    },
616
617    /**
618     * @return {string}
619     */
620    id: function()
621    {
622        return this.uiSourceCode.uri() + ":" + this.lineNumber + ":" + this.columnNumber;
623    },
624}
625
626/**
627 * @constructor
628 * @implements {WebInspector.ContentProvider}
629 * @param {!WebInspector.UISourceCode} uiSourceCode
630 * @param {?string|undefined} content
631 * @param {!Date} timestamp
632 */
633WebInspector.Revision = function(uiSourceCode, content, timestamp)
634{
635    this._uiSourceCode = uiSourceCode;
636    this._content = content;
637    this._timestamp = timestamp;
638}
639
640WebInspector.Revision.prototype = {
641    /**
642     * @return {!WebInspector.UISourceCode}
643     */
644    get uiSourceCode()
645    {
646        return this._uiSourceCode;
647    },
648
649    /**
650     * @return {!Date}
651     */
652    get timestamp()
653    {
654        return this._timestamp;
655    },
656
657    /**
658     * @return {?string}
659     */
660    get content()
661    {
662        return this._content || null;
663    },
664
665    revertToThis: function()
666    {
667        /**
668         * @param {string} content
669         * @this {WebInspector.Revision}
670         */
671        function revert(content)
672        {
673            if (this._uiSourceCode._content !== content)
674                this._uiSourceCode.addRevision(content);
675        }
676        this.requestContent(revert.bind(this));
677    },
678
679    /**
680     * @return {string}
681     */
682    contentURL: function()
683    {
684        return this._uiSourceCode.originURL();
685    },
686
687    /**
688     * @return {!WebInspector.ResourceType}
689     */
690    contentType: function()
691    {
692        return this._uiSourceCode.contentType();
693    },
694
695    /**
696     * @param {function(string)} callback
697     */
698    requestContent: function(callback)
699    {
700        callback(this._content || "");
701    },
702
703    /**
704     * @param {string} query
705     * @param {boolean} caseSensitive
706     * @param {boolean} isRegex
707     * @param {function(!Array.<!WebInspector.ContentProvider.SearchMatch>)} callback
708     */
709    searchInContent: function(query, caseSensitive, isRegex, callback)
710    {
711        callback([]);
712    }
713}
714