1/*
2 * Copyright (C) 2012 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 */
34WebInspector.WorkspaceController = function(workspace)
35{
36    this._workspace = workspace;
37    WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.InspectedURLChanged, this._inspectedURLChanged, this);
38}
39
40WebInspector.WorkspaceController.prototype = {
41    /**
42     * @param {WebInspector.Event} event
43     */
44    _inspectedURLChanged: function(event)
45    {
46        WebInspector.Revision.filterOutStaleRevisions();
47    }
48}
49
50/**
51 * @constructor
52 * @param {Array.<string>} path
53 * @param {string} originURL
54 * @param {string} url
55 * @param {WebInspector.ResourceType} contentType
56 * @param {boolean} isEditable
57 * @param {boolean=} isContentScript
58 */
59WebInspector.FileDescriptor = function(path, originURL, url, contentType, isEditable, isContentScript)
60{
61    this.path = path;
62    this.originURL = originURL;
63    this.url = url;
64    this.contentType = contentType;
65    this.isEditable = isEditable;
66    this.isContentScript = isContentScript || false;
67}
68
69/**
70 * @interface
71 * @extends {WebInspector.EventTarget}
72 */
73WebInspector.ProjectDelegate = function() { }
74
75WebInspector.ProjectDelegate.Events = {
76    FileAdded: "FileAdded",
77    FileRemoved: "FileRemoved",
78    Reset: "Reset",
79}
80
81WebInspector.ProjectDelegate.prototype = {
82    /**
83     * @return {string}
84     */
85    id: function() { },
86
87    /**
88     * @return {string}
89     */
90    type: function() { },
91
92    /**
93     * @return {string}
94     */
95    displayName: function() { },
96
97    /**
98     * @param {Array.<string>} path
99     * @param {function(?string,boolean,string)} callback
100     */
101    requestFileContent: function(path, callback) { },
102
103    /**
104     * @return {boolean}
105     */
106    canSetFileContent: function() { },
107
108    /**
109     * @param {Array.<string>} path
110     * @param {string} newContent
111     * @param {function(?string)} callback
112     */
113    setFileContent: function(path, newContent, callback) { },
114
115    /**
116     * @param {Array.<string>} path
117     * @param {string} query
118     * @param {boolean} caseSensitive
119     * @param {boolean} isRegex
120     * @param {function(Array.<WebInspector.ContentProvider.SearchMatch>)} callback
121     */
122    searchInFileContent: function(path, query, caseSensitive, isRegex, callback) { }
123}
124
125/**
126 * @type {?WebInspector.WorkspaceController}
127 */
128WebInspector.workspaceController = null;
129
130/**
131 * @param {WebInspector.Workspace} workspace
132 * @param {WebInspector.ProjectDelegate} projectDelegate
133 * @constructor
134 */
135WebInspector.Project = function(workspace, projectDelegate)
136{
137    /** @type {Object.<string, WebInspector.UISourceCode>} */
138    this._uiSourceCodes = {};
139    this._workspace = workspace;
140    this._projectDelegate = projectDelegate;
141    this._projectDelegate.addEventListener(WebInspector.ProjectDelegate.Events.FileAdded, this._fileAdded, this);
142    this._projectDelegate.addEventListener(WebInspector.ProjectDelegate.Events.FileRemoved, this._fileRemoved, this);
143    this._projectDelegate.addEventListener(WebInspector.ProjectDelegate.Events.Reset, this._reset, this);
144}
145
146WebInspector.Project.prototype = {
147    /**
148     * @return {string}
149     */
150    id: function()
151    {
152        return this._projectDelegate.id();
153    },
154
155    /**
156     * @return {string}
157     */
158    type: function()
159    {
160        return this._projectDelegate.type();
161    },
162
163    /**
164     * @return {string}
165     */
166    displayName: function()
167    {
168        return this._projectDelegate.displayName();
169    },
170
171    /**
172     * @return {boolean}
173     */
174    isServiceProject: function()
175    {
176        return this._projectDelegate.type() === WebInspector.projectTypes.Debugger || this._projectDelegate.type() === WebInspector.projectTypes.LiveEdit;
177    },
178
179    _fileAdded: function(event)
180    {
181        var fileDescriptor = /** @type {WebInspector.FileDescriptor} */ (event.data);
182        var uiSourceCode = this.uiSourceCode(fileDescriptor.path);
183        if (uiSourceCode) {
184            // FIXME: Implement
185            return;
186        }
187
188        uiSourceCode = new WebInspector.UISourceCode(this, fileDescriptor.path, fileDescriptor.originURL, fileDescriptor.url, fileDescriptor.contentType, fileDescriptor.isEditable);
189        uiSourceCode.isContentScript = fileDescriptor.isContentScript;
190        this._uiSourceCodes[uiSourceCode.path().join("/")] = uiSourceCode;
191        this._workspace.dispatchEventToListeners(WebInspector.UISourceCodeProvider.Events.UISourceCodeAdded, uiSourceCode);
192    },
193
194    _fileRemoved: function(event)
195    {
196        var path = /** @type {Array.<string>} */ (event.data);
197        var uiSourceCode = this.uiSourceCode(path);
198        if (!uiSourceCode)
199            return;
200        delete this._uiSourceCodes[uiSourceCode.path().join("/")];
201        this._workspace.dispatchEventToListeners(WebInspector.UISourceCodeProvider.Events.UISourceCodeRemoved, uiSourceCode);
202    },
203
204    _reset: function()
205    {
206        this._workspace.dispatchEventToListeners(WebInspector.Workspace.Events.ProjectWillReset, this);
207        this._uiSourceCodes = {};
208    },
209
210    /**
211     * @param {Array.<string>} path
212     * @return {?WebInspector.UISourceCode}
213     */
214    uiSourceCode: function(path)
215    {
216        return this._uiSourceCodes[path.join("/")] || null;
217    },
218
219    /**
220     * @param {string} originURL
221     * @return {?WebInspector.UISourceCode}
222     */
223    uiSourceCodeForOriginURL: function(originURL)
224    {
225        for (var path in this._uiSourceCodes) {
226            var uiSourceCode = this._uiSourceCodes[path];
227            if (uiSourceCode.originURL() === originURL)
228                return uiSourceCode;
229        }
230        return null;
231    },
232
233    /**
234     * @return {Array.<WebInspector.UISourceCode>}
235     */
236    uiSourceCodes: function()
237    {
238        return Object.values(this._uiSourceCodes);
239    },
240
241    /**
242     * @param {WebInspector.UISourceCode} uiSourceCode
243     * @param {function(?string,boolean,string)} callback
244     */
245    requestFileContent: function(uiSourceCode, callback)
246    {
247        this._projectDelegate.requestFileContent(uiSourceCode.path(), callback);
248    },
249
250    /**
251     * @return {boolean}
252     */
253    canSetFileContent: function()
254    {
255        return this._projectDelegate.canSetFileContent();
256    },
257
258    /**
259     * @param {WebInspector.UISourceCode} uiSourceCode
260     * @param {string} newContent
261     * @param {function(?string)} callback
262     */
263    setFileContent: function(uiSourceCode, newContent, callback)
264    {
265        this._projectDelegate.setFileContent(uiSourceCode.path(), newContent, callback);
266        this._workspace.dispatchEventToListeners(WebInspector.Workspace.Events.UISourceCodeContentCommitted, { uiSourceCode: uiSourceCode, content: newContent });
267    },
268
269    /**
270     * @param {WebInspector.UISourceCode} uiSourceCode
271     * @param {string} query
272     * @param {boolean} caseSensitive
273     * @param {boolean} isRegex
274     * @param {function(Array.<WebInspector.ContentProvider.SearchMatch>)} callback
275     */
276    searchInFileContent: function(uiSourceCode, query, caseSensitive, isRegex, callback)
277    {
278        this._projectDelegate.searchInFileContent(uiSourceCode.path(), query, caseSensitive, isRegex, callback);
279    },
280
281    dispose: function()
282    {
283        this._projectDelegate.reset();
284    }
285}
286
287WebInspector.projectTypes = {
288    Debugger: "debugger",
289    LiveEdit: "liveedit",
290    Network: "network",
291    Snippets: "snippets",
292    FileSystem: "filesystem"
293}
294
295/**
296 * @constructor
297 * @implements {WebInspector.UISourceCodeProvider}
298 * @extends {WebInspector.Object}
299 * @param {WebInspector.FileMapping} fileMapping
300 * @param {WebInspector.FileSystemMapping} fileSystemMapping
301 */
302WebInspector.Workspace = function(fileMapping, fileSystemMapping)
303{
304    this._fileMapping = fileMapping;
305    this._fileSystemMapping = fileSystemMapping;
306    /** @type {!Object.<string, WebInspector.Project>} */
307    this._projects = {};
308}
309
310WebInspector.Workspace.Events = {
311    UISourceCodeContentCommitted: "UISourceCodeContentCommitted",
312    ProjectWillReset: "ProjectWillReset"
313}
314
315WebInspector.Workspace.prototype = {
316    /**
317     * @param {string} projectId
318     * @param {Array.<string>} path
319     * @return {?WebInspector.UISourceCode}
320     */
321    uiSourceCode: function(projectId, path)
322    {
323        var project = this._projects[projectId];
324        return project ? project.uiSourceCode(path) : null;
325    },
326
327    /**
328     * @param {string} originURL
329     * @return {?WebInspector.UISourceCode}
330     */
331    uiSourceCodeForOriginURL: function(originURL)
332    {
333        var networkProjects = this.projectsForType(WebInspector.projectTypes.Network)
334        for (var i = 0; i < networkProjects.length; ++i) {
335            var project = networkProjects[i];
336            var uiSourceCode = project.uiSourceCodeForOriginURL(originURL);
337            if (uiSourceCode)
338                return uiSourceCode;
339        }
340        return null;
341    },
342
343    /**
344     * @param {string} type
345     * @return {Array.<WebInspector.UISourceCode>}
346     */
347    uiSourceCodesForProjectType: function(type)
348    {
349        var result = [];
350        for (var projectName in this._projects) {
351            var project = this._projects[projectName];
352            if (project.type() === type)
353                result = result.concat(project.uiSourceCodes());
354        }
355        return result;
356    },
357
358    /**
359     * @param {WebInspector.ProjectDelegate} projectDelegate
360     * @return {WebInspector.Project}
361     */
362    addProject: function(projectDelegate)
363    {
364        var projectId = projectDelegate.id();
365        this._projects[projectId] = new WebInspector.Project(this, projectDelegate);
366        return this._projects[projectId];
367    },
368
369    /**
370     * @param {string} projectId
371     */
372    removeProject: function(projectId)
373    {
374        var project = this._projects[projectId];
375        if (!project)
376            return;
377        project.dispose();
378        delete this._projects[projectId];
379    },
380
381    /**
382     * @param {string} projectId
383     * @return {WebInspector.Project}
384     */
385    project: function(projectId)
386    {
387        return this._projects[projectId];
388    },
389
390    /**
391     * @return {Array.<WebInspector.Project>}
392     */
393    projects: function()
394    {
395        return Object.values(this._projects);
396    },
397
398    /**
399     * @param {string} type
400     * @return {Array.<WebInspector.Project>}
401     */
402    projectsForType: function(type)
403    {
404        function filterByType(project)
405        {
406            return project.type() === type;
407        }
408        return this.projects().filter(filterByType);
409    },
410
411    /**
412     * @return {Array.<WebInspector.UISourceCode>}
413     */
414    uiSourceCodes: function()
415    {
416        var result = [];
417        for (var projectId in this._projects) {
418            var project = this._projects[projectId];
419            result = result.concat(project.uiSourceCodes());
420        }
421        return result;
422    },
423
424    /**
425     * @param {string} url
426     * @return {boolean}
427     */
428    hasMappingForURL: function(url)
429    {
430        var entry = this._fileMapping.mappingEntryForURL(url);
431        if (!entry)
432            return false;
433        return !!this._fileSystemPathForEntry(entry);
434    },
435
436    /**
437     * @param {WebInspector.FileMapping.Entry} entry
438     * @return {?string}
439     */
440    _fileSystemPathForEntry: function(entry)
441    {
442        return this._fileSystemMapping.fileSystemPathForPrefix(entry.pathPrefix);
443    },
444    /**
445     * @param {string} url
446     * @return {WebInspector.UISourceCode}
447     */
448    uiSourceCodeForURL: function(url)
449    {
450        var entry = this._fileMapping.mappingEntryForURL(url);
451        var fileSystemPath = entry ? this._fileSystemPathForEntry(entry) : null;
452        if (!fileSystemPath) {
453            var splittedURL = WebInspector.ParsedURL.splitURL(url);
454            var projectId = WebInspector.SimpleProjectDelegate.projectId(splittedURL[0], WebInspector.projectTypes.Network);
455            var path = WebInspector.SimpleWorkspaceProvider.pathForSplittedURL(splittedURL);
456            var project = this.project(projectId);
457            return project ? project.uiSourceCode(path) : null;
458        }
459
460        var projectId = WebInspector.FileSystemProjectDelegate.projectId(fileSystemPath);
461        var pathPrefix = entry.pathPrefix.substr(fileSystemPath.length + 1);
462        var path = pathPrefix + url.substr(entry.urlPrefix.length);
463        var project = this.project(projectId);
464        return project ? project.uiSourceCode(path.split("/")) : null;
465    },
466
467    /**
468     * @param {string} path
469     * @return {string}
470     */
471    urlForPath: function(path)
472    {
473        var entry = this._fileMapping.mappingEntryForPath(path);
474        if (!entry)
475            return "";
476        return entry.urlPrefix + path.substring(entry.pathPrefix.length);
477    },
478
479    /**
480     * @param {WebInspector.UISourceCode} networkUISourceCode
481     * @param {WebInspector.UISourceCode} uiSourceCode
482     * @param {WebInspector.FileSystemWorkspaceProvider} fileSystemWorkspaceProvider
483     */
484    addMapping: function(networkUISourceCode, uiSourceCode, fileSystemWorkspaceProvider)
485    {
486        var url = networkUISourceCode.url;
487        var path = uiSourceCode.path();
488        var suffix = "";
489        for (var i = path.length - 1; i >= 0; --i) {
490            var nextSuffix = "/" + path[i] + suffix;
491            if (!url.endsWith(nextSuffix))
492                break;
493            suffix = nextSuffix;
494        }
495        var fileSystemPath = fileSystemWorkspaceProvider.fileSystemPath(uiSourceCode);
496        var filePath = "/" + path.join("/");
497        var pathPrefix = fileSystemPath + filePath.substr(0, filePath.length - suffix.length) + "/";
498        var urlPrefix = url.substr(0, url.length - suffix.length) + "/";
499
500        var entries = this._fileMapping.mappingEntries();
501        var entry = new WebInspector.FileMapping.Entry(urlPrefix, pathPrefix);
502        entries.push(entry);
503        this._fileMapping.setMappingEntries(entries);
504        WebInspector.suggestReload();
505    },
506
507    /**
508     * @param {WebInspector.UISourceCode} uiSourceCode
509     */
510    removeMapping: function(uiSourceCode)
511    {
512        var entry = this._fileMapping.mappingEntryForURL(uiSourceCode.url);
513        var entries = this._fileMapping.mappingEntries();
514        entries.remove(entry);
515        this._fileMapping.setMappingEntries(entries);
516        WebInspector.suggestReload();
517    },
518
519    __proto__: WebInspector.Object.prototype
520}
521
522/**
523 * @type {?WebInspector.Workspace}
524 */
525WebInspector.workspace = null;
526