Home | History | Annotate | Download | only in front_end
      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  */
     36 WebInspector.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 
     45 WebInspector.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  */
    230 WebInspector.ScriptFile = function()
    231 {
    232 }
    233 
    234 WebInspector.ScriptFile.Events = {
    235     DidMergeToVM: "DidMergeToVM",
    236     DidDivergeFromVM: "DidDivergeFromVM",
    237 }
    238 
    239 WebInspector.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  */
    265 WebInspector.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 
    281 WebInspector.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