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