1/*
2 * Copyright (C) 2013 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 * @constructor
33 * @implements {WebInspector.ProjectDelegate}
34 * @extends {WebInspector.Object}
35 * @param {WebInspector.IsolatedFileSystem} isolatedFileSystem
36 * @param {WebInspector.Workspace} workspace
37 */
38WebInspector.FileSystemProjectDelegate = function(isolatedFileSystem, workspace)
39{
40    this._fileSystem = isolatedFileSystem;
41    this._normalizedFileSystemPath = this._fileSystem.path();
42    if (WebInspector.isWin())
43        this._normalizedFileSystemPath = this._normalizedFileSystemPath.replace(/\\/g, "/");
44    this._fileSystemURL = "file://" + this._normalizedFileSystemPath + "/";
45    this._workspace = workspace;
46    /** @type {Object.<number, function(Array.<string>)>} */
47    this._searchCallbacks = {};
48    /** @type {Object.<number, function()>} */
49    this._indexingCallbacks = {};
50    /** @type {Object.<number, WebInspector.Progress>} */
51    this._indexingProgresses = {};
52}
53
54WebInspector.FileSystemProjectDelegate._scriptExtensions = ["js", "java", "coffee", "ts", "dart"].keySet();
55WebInspector.FileSystemProjectDelegate._styleSheetExtensions = ["css", "scss", "sass", "less"].keySet();
56WebInspector.FileSystemProjectDelegate._documentExtensions = ["htm", "html", "asp", "aspx", "phtml", "jsp"].keySet();
57
58WebInspector.FileSystemProjectDelegate.projectId = function(fileSystemPath)
59{
60    return "filesystem:" + fileSystemPath;
61}
62
63WebInspector.FileSystemProjectDelegate._lastRequestId = 0;
64
65WebInspector.FileSystemProjectDelegate.prototype = {
66    /**
67     * @return {string}
68     */
69    id: function()
70    {
71        return WebInspector.FileSystemProjectDelegate.projectId(this._fileSystem.path());
72    },
73
74    /**
75     * @return {string}
76     */
77    type: function()
78    {
79        return WebInspector.projectTypes.FileSystem;
80    },
81
82    /**
83     * @return {string}
84     */
85    fileSystemPath: function()
86    {
87        return this._fileSystem.path();
88    },
89
90    /**
91     * @return {string}
92     */
93    displayName: function()
94    {
95        return this._normalizedFileSystemPath.substr(this._normalizedFileSystemPath.lastIndexOf("/") + 1);
96    },
97
98    /**
99     * @param {string} path
100     * @return {string}
101     */
102    _filePathForPath: function(path)
103    {
104        return "/" + path;
105    },
106
107    /**
108     * @param {string} path
109     * @param {function(?string,boolean,string)} callback
110     */
111    requestFileContent: function(path, callback)
112    {
113        var filePath = this._filePathForPath(path);
114        this._fileSystem.requestFileContent(filePath, innerCallback.bind(this));
115
116        /**
117         * @param {?string} content
118         */
119        function innerCallback(content)
120        {
121            var extension = this._extensionForPath(path);
122            var mimeType = WebInspector.ResourceType.mimeTypesForExtensions[extension];
123            callback(content, false, mimeType);
124        }
125    },
126
127    /**
128     * @param {string} path
129     * @param {function(?Date, ?number)} callback
130     */
131    requestMetadata: function(path, callback)
132    {
133        var filePath = this._filePathForPath(path);
134        this._fileSystem.requestMetadata(filePath, callback);
135    },
136
137    /**
138     * @return {boolean}
139     */
140    canSetFileContent: function()
141    {
142        return true;
143    },
144
145    /**
146     * @param {string} path
147     * @param {string} newContent
148     * @param {function(?string)} callback
149     */
150    setFileContent: function(path, newContent, callback)
151    {
152        var filePath = this._filePathForPath(path);
153        this._fileSystem.setFileContent(filePath, newContent, callback.bind(this, ""));
154    },
155
156    /**
157     * @return {boolean}
158     */
159    canRename: function()
160    {
161        return true;
162    },
163
164    /**
165     * @param {string} path
166     * @param {string} newName
167     * @param {function(boolean, string=)} callback
168     */
169    rename: function(path, newName, callback)
170    {
171        var filePath = this._filePathForPath(path);
172        this._fileSystem.renameFile(filePath, newName, callback);
173    },
174
175    /**
176     * @param {string} path
177     * @param {string} query
178     * @param {boolean} caseSensitive
179     * @param {boolean} isRegex
180     * @param {function(Array.<WebInspector.ContentProvider.SearchMatch>)} callback
181     */
182    searchInFileContent: function(path, query, caseSensitive, isRegex, callback)
183    {
184        var filePath = this._filePathForPath(path);
185        this._fileSystem.requestFileContent(filePath, contentCallback.bind(this));
186
187        /**
188         * @param {?string} content
189         */
190        function contentCallback(content)
191        {
192            var result = [];
193            if (content !== null)
194                result = WebInspector.ContentProvider.performSearchInContent(content, query, caseSensitive, isRegex);
195            callback(result);
196        }
197    },
198
199    /**
200     * @param {string} query
201     * @param {boolean} caseSensitive
202     * @param {boolean} isRegex
203     * @param {WebInspector.Progress} progress
204     * @param {function(StringMap)} callback
205     */
206    searchInContent: function(query, caseSensitive, isRegex, progress, callback)
207    {
208        var requestId = ++WebInspector.FileSystemProjectDelegate._lastRequestId;
209        this._searchCallbacks[requestId] = innerCallback.bind(this);
210        InspectorFrontendHost.searchInPath(requestId, this._fileSystem.path(), isRegex ? "" : query);
211
212        function innerCallback(files)
213        {
214            function trimAndNormalizeFileSystemPath(fullPath)
215            {
216                var trimmedPath = fullPath.substr(this._fileSystem.path().length + 1);
217                if (WebInspector.isWin())
218                    trimmedPath = trimmedPath.replace(/\\/g, "/");
219                return trimmedPath;
220            }
221
222            files = files.map(trimAndNormalizeFileSystemPath.bind(this));
223            var result = new StringMap();
224            progress.setTotalWork(files.length);
225            if (files.length === 0) {
226                progress.done();
227                callback(result);
228                return;
229            }
230
231            var fileIndex = 0;
232            var maxFileContentRequests = 20;
233            var callbacksLeft = 0;
234
235            function searchInNextFiles()
236            {
237                for (; callbacksLeft < maxFileContentRequests; ++callbacksLeft) {
238                    if (fileIndex >= files.length)
239                        break;
240                    var path = files[fileIndex++];
241                    var filePath = this._filePathForPath(path);
242                    this._fileSystem.requestFileContent(filePath, contentCallback.bind(this, path));
243                }
244            }
245
246            searchInNextFiles.call(this);
247
248            /**
249             * @param {string} path
250             * @param {?string} content
251             */
252            function contentCallback(path, content)
253            {
254                var matches = [];
255                if (content !== null)
256                    matches = WebInspector.ContentProvider.performSearchInContent(content, query, caseSensitive, isRegex);
257
258                result.put(path, matches);
259                progress.worked(1);
260
261                --callbacksLeft;
262                if (fileIndex < files.length) {
263                    searchInNextFiles.call(this);
264                } else {
265                    if (callbacksLeft)
266                        return;
267                    progress.done();
268                    callback(result);
269                }
270            }
271        }
272    },
273
274    /**
275     * @param {number} requestId
276     * @param {Array.<string>} files
277     */
278    searchCompleted: function(requestId, files)
279    {
280        if (!this._searchCallbacks[requestId])
281            return;
282        var callback = this._searchCallbacks[requestId];
283        delete this._searchCallbacks[requestId];
284        callback(files);
285    },
286
287    /**
288     * @param {WebInspector.Progress} progress
289     * @param {function()} callback
290     */
291    indexContent: function(progress, callback)
292    {
293        var requestId = ++WebInspector.FileSystemProjectDelegate._lastRequestId;
294        this._indexingCallbacks[requestId] = callback;
295        this._indexingProgresses[requestId] = progress;
296        progress.setTotalWork(1);
297        progress.addEventListener(WebInspector.Progress.Events.Canceled, this._indexingCanceled.bind(this, requestId));
298        InspectorFrontendHost.indexPath(requestId, this._fileSystem.path());
299    },
300
301    /**
302     * @param {number} requestId
303     */
304    _indexingCanceled: function(requestId)
305    {
306        if (!this._indexingProgresses[requestId])
307            return;
308        InspectorFrontendHost.stopIndexing(requestId);
309        delete this._indexingProgresses[requestId];
310        delete this._indexingCallbacks[requestId];
311    },
312
313    /**
314     * @param {number} requestId
315     * @param {number} totalWork
316     */
317    indexingTotalWorkCalculated: function(requestId, totalWork)
318    {
319        if (!this._indexingProgresses[requestId])
320            return;
321        var progress = this._indexingProgresses[requestId];
322        progress.setTotalWork(totalWork);
323    },
324
325    /**
326     * @param {number} requestId
327     * @param {number} worked
328     */
329    indexingWorked: function(requestId, worked)
330    {
331        if (!this._indexingProgresses[requestId])
332            return;
333        var progress = this._indexingProgresses[requestId];
334        progress.worked(worked);
335    },
336
337    /**
338     * @param {number} requestId
339     */
340    indexingDone: function(requestId)
341    {
342        if (!this._indexingProgresses[requestId])
343            return;
344        var progress = this._indexingProgresses[requestId];
345        var callback = this._indexingCallbacks[requestId];
346        delete this._indexingProgresses[requestId];
347        delete this._indexingCallbacks[requestId];
348        progress.done();
349        callback.call();
350    },
351
352    /**
353     * @param {string} path
354     * @return {string}
355     */
356    _extensionForPath: function(path)
357    {
358        var extensionIndex = path.lastIndexOf(".");
359        if (extensionIndex === -1)
360            return "";
361        return path.substring(extensionIndex + 1).toLowerCase();
362    },
363
364    /**
365     * @param {string} extension
366     * @return {WebInspector.ResourceType}
367     */
368    _contentTypeForExtension: function(extension)
369    {
370        if (WebInspector.FileSystemProjectDelegate._scriptExtensions[extension])
371            return WebInspector.resourceTypes.Script;
372        if (WebInspector.FileSystemProjectDelegate._styleSheetExtensions[extension])
373            return WebInspector.resourceTypes.Stylesheet;
374        if (WebInspector.FileSystemProjectDelegate._documentExtensions[extension])
375            return WebInspector.resourceTypes.Document;
376        return WebInspector.resourceTypes.Other;
377    },
378
379    populate: function()
380    {
381        this._fileSystem.requestFilesRecursive("", this._addFile.bind(this));
382    },
383
384    /**
385     * @param {string} path
386     */
387    refresh: function(path)
388    {
389        this._fileSystem.requestFilesRecursive(path, this._addFile.bind(this));
390    },
391
392    /**
393     * @param {string} path
394     * @param {?string} name
395     * @param {function(?string)} callback
396     */
397    createFile: function(path, name, callback)
398    {
399        this._fileSystem.createFile(path, name, innerCallback.bind(this));
400
401        function innerCallback(filePath)
402        {
403            this._addFile(filePath);
404            callback(filePath);
405        }
406    },
407
408    /**
409     * @param {string} path
410     */
411    deleteFile: function(path)
412    {
413        this._fileSystem.deleteFile(path);
414        this._removeFile(path);
415    },
416
417    remove: function()
418    {
419        WebInspector.isolatedFileSystemManager.removeFileSystem(this._fileSystem.path());
420    },
421
422    /**
423     * @param {string} filePath
424     */
425    _addFile: function(filePath)
426    {
427        if (!filePath)
428            console.assert(false);
429
430        var slash = filePath.lastIndexOf("/");
431        var parentPath = filePath.substring(0, slash);
432        var name = filePath.substring(slash + 1);
433
434        var url = this._workspace.urlForPath(this._fileSystem.path(), filePath);
435        var extension = this._extensionForPath(name);
436        var contentType = this._contentTypeForExtension(extension);
437
438        var fileDescriptor = new WebInspector.FileDescriptor(parentPath, name, this._fileSystemURL + filePath, url, contentType, true);
439        this.dispatchEventToListeners(WebInspector.ProjectDelegate.Events.FileAdded, fileDescriptor);
440    },
441
442    /**
443     * @param {string} path
444     */
445    _removeFile: function(path)
446    {
447        this.dispatchEventToListeners(WebInspector.ProjectDelegate.Events.FileRemoved, path);
448    },
449
450    reset: function()
451    {
452        this.dispatchEventToListeners(WebInspector.ProjectDelegate.Events.Reset, null);
453    },
454
455    __proto__: WebInspector.Object.prototype
456}
457
458/**
459 * @type {?WebInspector.FileSystemProjectDelegate}
460 */
461WebInspector.fileSystemProjectDelegate = null;
462
463/**
464 * @constructor
465 * @param {WebInspector.IsolatedFileSystemManager} isolatedFileSystemManager
466 * @param {WebInspector.Workspace} workspace
467 */
468WebInspector.FileSystemWorkspaceProvider = function(isolatedFileSystemManager, workspace)
469{
470    this._isolatedFileSystemManager = isolatedFileSystemManager;
471    this._workspace = workspace;
472    this._isolatedFileSystemManager.addEventListener(WebInspector.IsolatedFileSystemManager.Events.FileSystemAdded, this._fileSystemAdded, this);
473    this._isolatedFileSystemManager.addEventListener(WebInspector.IsolatedFileSystemManager.Events.FileSystemRemoved, this._fileSystemRemoved, this);
474    this._projectDelegates = {};
475}
476
477WebInspector.FileSystemWorkspaceProvider.prototype = {
478    /**
479     * @param {WebInspector.Event} event
480     */
481    _fileSystemAdded: function(event)
482    {
483        var fileSystem = /** @type {WebInspector.IsolatedFileSystem} */ (event.data);
484        var projectId = WebInspector.FileSystemProjectDelegate.projectId(fileSystem.path());
485        var projectDelegate = new WebInspector.FileSystemProjectDelegate(fileSystem, this._workspace)
486        this._projectDelegates[projectDelegate.id()] = projectDelegate;
487        console.assert(!this._workspace.project(projectDelegate.id()));
488        this._workspace.addProject(projectDelegate);
489        projectDelegate.populate();
490    },
491
492    /**
493     * @param {WebInspector.Event} event
494     */
495    _fileSystemRemoved: function(event)
496    {
497        var fileSystem = /** @type {WebInspector.IsolatedFileSystem} */ (event.data);
498        var projectId = WebInspector.FileSystemProjectDelegate.projectId(fileSystem.path());
499        this._workspace.removeProject(projectId);
500        delete this._projectDelegates[projectId];
501    },
502
503    /**
504     * @param {WebInspector.UISourceCode} uiSourceCode
505     */
506    fileSystemPath: function(uiSourceCode)
507    {
508        var projectDelegate = this._projectDelegates[uiSourceCode.project().id()];
509        return projectDelegate.fileSystemPath();
510    },
511
512    /**
513     * @param {WebInspector.FileSystemProjectDelegate} fileSystemPath
514     */
515    delegate: function(fileSystemPath)
516    {
517        var projectId = WebInspector.FileSystemProjectDelegate.projectId(fileSystemPath);
518        return this._projectDelegates[projectId];
519    }
520}
521
522/**
523 * @type {?WebInspector.FileSystemWorkspaceProvider}
524 */
525WebInspector.fileSystemWorkspaceProvider = null;
526