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 * @implements {WebInspector.ScriptSourceMapping}
34 * @param {!WebInspector.Workspace} workspace
35 */
36WebInspector.ResourceScriptMapping = function(workspace)
37{
38    this._workspace = workspace;
39    this._workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeAdded, this._uiSourceCodeAddedToWorkspace, this);
40
41    WebInspector.debuggerModel.addEventListener(WebInspector.DebuggerModel.Events.GlobalObjectCleared, this._debuggerReset, this);
42    this._initialize();
43}
44
45WebInspector.ResourceScriptMapping.prototype = {
46    /**
47     * @param {!WebInspector.RawLocation} rawLocation
48     * @return {?WebInspector.UILocation}
49     */
50    rawLocationToUILocation: function(rawLocation)
51    {
52        var debuggerModelLocation = /** @type {!WebInspector.DebuggerModel.Location} */ (rawLocation);
53        var script = WebInspector.debuggerModel.scriptForId(debuggerModelLocation.scriptId);
54        var uiSourceCode = this._workspaceUISourceCodeForScript(script);
55        if (!uiSourceCode)
56            return null;
57        var scriptFile = uiSourceCode.scriptFile();
58        if (scriptFile && ((scriptFile.hasDivergedFromVM() && !scriptFile.isMergingToVM()) || scriptFile.isDivergingFromVM()))
59            return null;
60        return new WebInspector.UILocation(uiSourceCode, debuggerModelLocation.lineNumber, debuggerModelLocation.columnNumber || 0);
61    },
62
63    /**
64     * @param {!WebInspector.UISourceCode} uiSourceCode
65     * @param {number} lineNumber
66     * @param {number} columnNumber
67     * @return {?WebInspector.DebuggerModel.Location}
68     */
69    uiLocationToRawLocation: function(uiSourceCode, lineNumber, columnNumber)
70    {
71        var scripts = this._scriptsForUISourceCode(uiSourceCode);
72        console.assert(scripts.length);
73        return WebInspector.debuggerModel.createRawLocation(scripts[0], lineNumber, columnNumber);
74    },
75
76    /**
77     * @param {!WebInspector.Script} script
78     */
79    addScript: function(script)
80    {
81        if (script.isAnonymousScript())
82            return;
83        script.pushSourceMapping(this);
84
85        var scriptsForSourceURL = script.isInlineScript() ? this._inlineScriptsForSourceURL : this._nonInlineScriptsForSourceURL;
86        scriptsForSourceURL.put(script.sourceURL, scriptsForSourceURL.get(script.sourceURL) || []);
87        scriptsForSourceURL.get(script.sourceURL).push(script);
88
89        var uiSourceCode = this._workspaceUISourceCodeForScript(script);
90        if (!uiSourceCode)
91            return;
92
93        this._bindUISourceCodeToScripts(uiSourceCode, [script]);
94    },
95
96    _uiSourceCodeAddedToWorkspace: function(event)
97    {
98        var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data);
99        if (!uiSourceCode.url)
100            return;
101
102        var scripts = this._scriptsForUISourceCode(uiSourceCode);
103        if (!scripts.length)
104            return;
105
106        this._bindUISourceCodeToScripts(uiSourceCode, scripts);
107    },
108
109    /**
110     * @param {!WebInspector.UISourceCode} uiSourceCode
111     */
112    _hasMergedToVM: function(uiSourceCode)
113    {
114        var scripts = this._scriptsForUISourceCode(uiSourceCode);
115        if (!scripts.length)
116            return;
117        for (var i = 0; i < scripts.length; ++i)
118            scripts[i].updateLocations();
119    },
120
121    /**
122     * @param {!WebInspector.UISourceCode} uiSourceCode
123     */
124    _hasDivergedFromVM: function(uiSourceCode)
125    {
126        var scripts = this._scriptsForUISourceCode(uiSourceCode);
127        if (!scripts.length)
128            return;
129        for (var i = 0; i < scripts.length; ++i)
130            scripts[i].updateLocations();
131    },
132
133    /**
134     * @param {!WebInspector.Script} script
135     * @return {?WebInspector.UISourceCode}
136     */
137    _workspaceUISourceCodeForScript: function(script)
138    {
139        if (script.isAnonymousScript())
140            return null;
141        return this._workspace.uiSourceCodeForURL(script.sourceURL);
142    },
143
144    /**
145     * @param {!WebInspector.UISourceCode} uiSourceCode
146     * @return {!Array.<!WebInspector.Script>}
147     */
148    _scriptsForUISourceCode: function(uiSourceCode)
149    {
150        var isInlineScript;
151        switch (uiSourceCode.contentType()) {
152        case WebInspector.resourceTypes.Document:
153            isInlineScript = true;
154            break;
155        case WebInspector.resourceTypes.Script:
156            isInlineScript = false;
157            break;
158        default:
159            return [];
160        }
161        if (!uiSourceCode.url)
162            return [];
163        var scriptsForSourceURL = isInlineScript ? this._inlineScriptsForSourceURL : this._nonInlineScriptsForSourceURL;
164        return scriptsForSourceURL.get(uiSourceCode.url) || [];
165    },
166
167    /**
168     * @param {!WebInspector.UISourceCode} uiSourceCode
169     * @param {!Array.<!WebInspector.Script>} scripts
170     */
171    _bindUISourceCodeToScripts: function(uiSourceCode, scripts)
172    {
173        console.assert(scripts.length);
174        var scriptFile = new WebInspector.ResourceScriptFile(this, uiSourceCode, scripts);
175        uiSourceCode.setScriptFile(scriptFile);
176        for (var i = 0; i < scripts.length; ++i)
177            scripts[i].updateLocations();
178        uiSourceCode.setSourceMapping(this);
179    },
180
181    /**
182     * @param {!WebInspector.UISourceCode} uiSourceCode
183     * @param {!Array.<!WebInspector.Script>} scripts
184     */
185    _unbindUISourceCodeFromScripts: function(uiSourceCode, scripts)
186    {
187        console.assert(scripts.length);
188        var scriptFile = /** @type {!WebInspector.ResourceScriptFile} */ (uiSourceCode.scriptFile());
189        if (scriptFile) {
190            scriptFile.dispose();
191            uiSourceCode.setScriptFile(null);
192        }
193        uiSourceCode.setSourceMapping(null);
194    },
195
196    _initialize: function()
197    {
198        /** @type {!StringMap.<!Array.<!WebInspector.Script>>} */
199        this._inlineScriptsForSourceURL = new StringMap();
200        /** @type {!StringMap.<!Array.<!WebInspector.Script>>} */
201        this._nonInlineScriptsForSourceURL = new StringMap();
202    },
203
204    _debuggerReset: function()
205    {
206        /**
207         * @param {!Array.<!WebInspector.Script>} scripts
208         * @this {WebInspector.ResourceScriptMapping}
209         */
210        function unbindUISourceCodesForScripts(scripts)
211        {
212            if (!scripts.length)
213                return;
214            var uiSourceCode = this._workspaceUISourceCodeForScript(scripts[0]);
215            if (!uiSourceCode)
216                return;
217            this._unbindUISourceCodeFromScripts(uiSourceCode, scripts);
218        }
219
220        this._inlineScriptsForSourceURL.values().forEach(unbindUISourceCodesForScripts.bind(this));
221        this._nonInlineScriptsForSourceURL.values().forEach(unbindUISourceCodesForScripts.bind(this));
222        this._initialize();
223    },
224}
225
226/**
227 * @interface
228 * @extends {WebInspector.EventTarget}
229 */
230WebInspector.ScriptFile = function()
231{
232}
233
234WebInspector.ScriptFile.Events = {
235    DidMergeToVM: "DidMergeToVM",
236    DidDivergeFromVM: "DidDivergeFromVM",
237}
238
239WebInspector.ScriptFile.prototype = {
240    /**
241     * @return {boolean}
242     */
243    hasDivergedFromVM: function() { return false; },
244
245    /**
246     * @return {boolean}
247     */
248    isDivergingFromVM: function() { return false; },
249
250    /**
251     * @return {boolean}
252     */
253    isMergingToVM: function() { return false; },
254
255    checkMapping: function() { },
256}
257
258/**
259 * @constructor
260 * @implements {WebInspector.ScriptFile}
261 * @extends {WebInspector.Object}
262 * @param {!WebInspector.ResourceScriptMapping} resourceScriptMapping
263 * @param {!WebInspector.UISourceCode} uiSourceCode
264 */
265WebInspector.ResourceScriptFile = function(resourceScriptMapping, uiSourceCode, scripts)
266{
267    console.assert(scripts.length);
268
269    WebInspector.ScriptFile.call(this);
270    this._resourceScriptMapping = resourceScriptMapping;
271    this._uiSourceCode = uiSourceCode;
272
273    if (this._uiSourceCode.contentType() === WebInspector.resourceTypes.Script)
274        this._script = scripts[0];
275
276    this._uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this);
277    this._uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this);
278    this._update();
279}
280
281WebInspector.ResourceScriptFile.prototype = {
282    _workingCopyCommitted: function(event)
283    {
284        /**
285         * @param {?string} error
286         * @param {!DebuggerAgent.SetScriptSourceError=} errorData
287         * @this {WebInspector.ResourceScriptFile}
288         */
289        function innerCallback(error, errorData)
290        {
291            if (error) {
292                this._update();
293                WebInspector.LiveEditSupport.logDetailedError(error, errorData, this._script);
294                return;
295            }
296
297            this._scriptSource = source;
298            this._update();
299            WebInspector.LiveEditSupport.logSuccess();
300        }
301        if (!this._script)
302            return;
303        var source = this._uiSourceCode.workingCopy();
304        if (this._script.hasSourceURL && !this._sourceEndsWithSourceURL(source))
305            source += "\n //# sourceURL=" + this._script.sourceURL;
306        WebInspector.debuggerModel.setScriptSource(this._script.scriptId, source, innerCallback.bind(this));
307    },
308
309    /**
310     * @return {boolean}
311     */
312    _isDiverged: function()
313    {
314        if (this._uiSourceCode.formatted())
315            return false;
316        if (this._uiSourceCode.isDirty())
317            return true;
318        if (!this._script)
319            return false;
320        if (typeof this._scriptSource === "undefined")
321            return false;
322        return !this._sourceMatchesScriptSource(this._uiSourceCode.workingCopy(), this._scriptSource);
323    },
324
325    /**
326     * @param {string} source
327     * @param {string} scriptSource
328     * @return {boolean}
329     */
330    _sourceMatchesScriptSource: function(source, scriptSource)
331    {
332        if (!scriptSource.startsWith(source))
333            return false;
334        var scriptSourceTail = scriptSource.substr(source.length).trim();
335        return !scriptSourceTail || !!scriptSourceTail.match(/^\/\/[@#]\ssourceURL=\s*(\S*?)\s*$/m);
336    },
337
338    /**
339     * @param {string} source
340     * @return {boolean}
341     */
342    _sourceEndsWithSourceURL: function(source)
343    {
344        return !!source.match(/\/\/[@#]\ssourceURL=\s*(\S*?)\s*$/m);
345    },
346
347    /**
348     * @param {!WebInspector.Event} event
349     */
350    _workingCopyChanged: function(event)
351    {
352        this._update();
353    },
354
355    _update: function()
356    {
357        if (this._isDiverged() && !this._hasDivergedFromVM)
358            this._divergeFromVM();
359        else if (!this._isDiverged() && this._hasDivergedFromVM)
360            this._mergeToVM();
361    },
362
363    _divergeFromVM: function()
364    {
365        this._isDivergingFromVM = true;
366        this._resourceScriptMapping._hasDivergedFromVM(this._uiSourceCode);
367        delete this._isDivergingFromVM;
368        this._hasDivergedFromVM = true;
369        this.dispatchEventToListeners(WebInspector.ScriptFile.Events.DidDivergeFromVM, this._uiSourceCode);
370    },
371
372    _mergeToVM: function()
373    {
374        delete this._hasDivergedFromVM;
375        this._isMergingToVM = true;
376        this._resourceScriptMapping._hasMergedToVM(this._uiSourceCode);
377        delete this._isMergingToVM;
378        this.dispatchEventToListeners(WebInspector.ScriptFile.Events.DidMergeToVM, this._uiSourceCode);
379    },
380
381    /**
382     * @return {boolean}
383     */
384    hasDivergedFromVM: function()
385    {
386        return this._hasDivergedFromVM;
387    },
388
389    /**
390     * @return {boolean}
391     */
392    isDivergingFromVM: function()
393    {
394        return this._isDivergingFromVM;
395    },
396
397    /**
398     * @return {boolean}
399     */
400    isMergingToVM: function()
401    {
402        return this._isMergingToVM;
403    },
404
405    checkMapping: function()
406    {
407        if (!this._script)
408            return;
409        if (typeof this._scriptSource !== "undefined")
410            return;
411        this._script.requestContent(callback.bind(this));
412
413        /**
414         * @param {?string} source
415         * @this {WebInspector.ResourceScriptFile}
416         */
417        function callback(source)
418        {
419            this._scriptSource = source;
420            this._update();
421        }
422    },
423
424    dispose: function()
425    {
426        this._uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this);
427        this._uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this);
428    },
429
430    __proto__: WebInspector.Object.prototype
431}
432