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