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 * @extends {WebInspector.Object} 34 * @param {WebInspector.Workspace} workspace 35 */ 36 WebInspector.ScriptSnippetModel = function(workspace) 37 { 38 this._workspace = workspace; 39 /** @type {!Object.<string, WebInspector.UISourceCode>} */ 40 this._uiSourceCodeForScriptId = {}; 41 /** @type {!Map.<WebInspector.UISourceCode, WebInspector.Script>} */ 42 this._scriptForUISourceCode = new Map(); 43 /** @type {!Object.<string, WebInspector.UISourceCode>} */ 44 this._uiSourceCodeForSnippetId = {}; 45 /** @type {!Map.<WebInspector.UISourceCode, string>} */ 46 this._snippetIdForUISourceCode = new Map(); 47 48 this._snippetStorage = new WebInspector.SnippetStorage("script", "Script snippet #"); 49 this._lastSnippetEvaluationIndexSetting = WebInspector.settings.createSetting("lastSnippetEvaluationIndex", 0); 50 this._snippetScriptMapping = new WebInspector.SnippetScriptMapping(this); 51 this._projectDelegate = new WebInspector.SnippetsProjectDelegate(this); 52 this._project = this._workspace.addProject(this._projectDelegate); 53 this.reset(); 54 WebInspector.debuggerModel.addEventListener(WebInspector.DebuggerModel.Events.GlobalObjectCleared, this._debuggerReset, this); 55 } 56 57 WebInspector.ScriptSnippetModel.prototype = { 58 /** 59 * @return {WebInspector.SnippetScriptMapping} 60 */ 61 get scriptMapping() 62 { 63 return this._snippetScriptMapping; 64 }, 65 66 /** 67 * @return {WebInspector.Project} 68 */ 69 project: function() 70 { 71 return this._project; 72 }, 73 74 _loadSnippets: function() 75 { 76 var snippets = this._snippetStorage.snippets(); 77 for (var i = 0; i < snippets.length; ++i) 78 this._addScriptSnippet(snippets[i]); 79 }, 80 81 /** 82 * @return {string} 83 */ 84 createScriptSnippet: function() 85 { 86 var snippet = this._snippetStorage.createSnippet(); 87 return this._addScriptSnippet(snippet); 88 }, 89 90 /** 91 * @param {WebInspector.Snippet} snippet 92 * @return {string} 93 */ 94 _addScriptSnippet: function(snippet) 95 { 96 var path = this._projectDelegate.addSnippet(snippet.name, new WebInspector.SnippetContentProvider(snippet)); 97 var uiSourceCode = this._workspace.uiSourceCode(this._projectDelegate.id(), path); 98 var scriptFile = new WebInspector.SnippetScriptFile(this, uiSourceCode); 99 uiSourceCode.setScriptFile(scriptFile); 100 this._snippetIdForUISourceCode.put(uiSourceCode, snippet.id); 101 uiSourceCode.setSourceMapping(this._snippetScriptMapping); 102 this._uiSourceCodeForSnippetId[snippet.id] = uiSourceCode; 103 return path; 104 }, 105 106 /** 107 * @param {string} path 108 */ 109 deleteScriptSnippet: function(path) 110 { 111 var uiSourceCode = this._workspace.uiSourceCode(this._projectDelegate.id(), path); 112 var snippetId = this._snippetIdForUISourceCode.get(uiSourceCode) || ""; 113 var snippet = this._snippetStorage.snippetForId(snippetId); 114 this._snippetStorage.deleteSnippet(snippet); 115 this._removeBreakpoints(uiSourceCode); 116 this._releaseSnippetScript(uiSourceCode); 117 delete this._uiSourceCodeForSnippetId[snippet.id]; 118 this._snippetIdForUISourceCode.remove(uiSourceCode); 119 this._projectDelegate.removeFile(snippet.name); 120 }, 121 122 /** 123 * @param {string} name 124 * @param {string} newName 125 * @param {function(boolean, string=)} callback 126 */ 127 renameScriptSnippet: function(name, newName, callback) 128 { 129 newName = newName.trim(); 130 if (!newName || newName.indexOf("/") !== -1 || name === newName || this._snippetStorage.snippetForName(newName)) { 131 callback(false); 132 return; 133 } 134 var snippet = this._snippetStorage.snippetForName(name); 135 console.assert(snippet, "Snippet '" + name + "' was not found."); 136 var uiSourceCode = this._uiSourceCodeForSnippetId[snippet.id]; 137 console.assert(uiSourceCode, "No uiSourceCode was found for snippet '" + name + "'."); 138 139 var breakpointLocations = this._removeBreakpoints(uiSourceCode); 140 snippet.name = newName; 141 this._restoreBreakpoints(uiSourceCode, breakpointLocations); 142 callback(true, newName); 143 }, 144 145 /** 146 * @param {string} name 147 * @param {string} newContent 148 */ 149 _setScriptSnippetContent: function(name, newContent) 150 { 151 var snippet = this._snippetStorage.snippetForName(name); 152 snippet.content = newContent; 153 }, 154 155 /** 156 * @param {WebInspector.UISourceCode} uiSourceCode 157 */ 158 _scriptSnippetEdited: function(uiSourceCode) 159 { 160 var script = this._scriptForUISourceCode.get(uiSourceCode); 161 if (!script) 162 return; 163 164 var breakpointLocations = this._removeBreakpoints(uiSourceCode); 165 this._releaseSnippetScript(uiSourceCode); 166 this._restoreBreakpoints(uiSourceCode, breakpointLocations); 167 var scriptUISourceCode = script.rawLocationToUILocation(0, 0).uiSourceCode; 168 if (scriptUISourceCode) 169 this._restoreBreakpoints(scriptUISourceCode, breakpointLocations); 170 }, 171 172 /** 173 * @param {string} snippetId 174 * @return {number} 175 */ 176 _nextEvaluationIndex: function(snippetId) 177 { 178 var evaluationIndex = this._lastSnippetEvaluationIndexSetting.get() + 1; 179 this._lastSnippetEvaluationIndexSetting.set(evaluationIndex); 180 return evaluationIndex; 181 }, 182 183 /** 184 * @param {WebInspector.UISourceCode} uiSourceCode 185 */ 186 evaluateScriptSnippet: function(uiSourceCode) 187 { 188 var breakpointLocations = this._removeBreakpoints(uiSourceCode); 189 this._releaseSnippetScript(uiSourceCode); 190 this._restoreBreakpoints(uiSourceCode, breakpointLocations); 191 var snippetId = this._snippetIdForUISourceCode.get(uiSourceCode) || ""; 192 var evaluationIndex = this._nextEvaluationIndex(snippetId); 193 uiSourceCode._evaluationIndex = evaluationIndex; 194 var evaluationUrl = this._evaluationSourceURL(uiSourceCode); 195 196 var expression = uiSourceCode.workingCopy(); 197 198 // In order to stop on the breakpoints during the snippet evaluation we need to compile and run it separately. 199 // If separate compilation and execution is not supported by the port we fall back to evaluation in console. 200 // In case we don't need that since debugger is already paused. 201 // We do the same when we are stopped on the call frame since debugger is already paused and can not stop on breakpoint anymore. 202 if (WebInspector.debuggerModel.selectedCallFrame()) { 203 expression = uiSourceCode.workingCopy() + "\n//# sourceURL=" + evaluationUrl + "\n"; 204 WebInspector.evaluateInConsole(expression, true); 205 return; 206 } 207 208 WebInspector.showConsole(); 209 DebuggerAgent.compileScript(expression, evaluationUrl, compileCallback.bind(this)); 210 211 /** 212 * @param {?string} error 213 * @param {string=} scriptId 214 * @param {string=} syntaxErrorMessage 215 */ 216 function compileCallback(error, scriptId, syntaxErrorMessage) 217 { 218 if (!uiSourceCode || uiSourceCode._evaluationIndex !== evaluationIndex) 219 return; 220 221 if (error) { 222 console.error(error); 223 return; 224 } 225 226 if (!scriptId) { 227 var consoleMessage = WebInspector.ConsoleMessage.create( 228 WebInspector.ConsoleMessage.MessageSource.JS, 229 WebInspector.ConsoleMessage.MessageLevel.Error, 230 syntaxErrorMessage || ""); 231 WebInspector.console.addMessage(consoleMessage); 232 return; 233 } 234 235 var breakpointLocations = this._removeBreakpoints(uiSourceCode); 236 this._restoreBreakpoints(uiSourceCode, breakpointLocations); 237 238 this._runScript(scriptId); 239 } 240 }, 241 242 /** 243 * @param {DebuggerAgent.ScriptId} scriptId 244 */ 245 _runScript: function(scriptId) 246 { 247 var currentExecutionContext = WebInspector.runtimeModel.currentExecutionContext(); 248 DebuggerAgent.runScript(scriptId, currentExecutionContext ? currentExecutionContext.id : undefined, "console", false, runCallback.bind(this)); 249 250 /** 251 * @param {?string} error 252 * @param {?RuntimeAgent.RemoteObject} result 253 * @param {boolean=} wasThrown 254 */ 255 function runCallback(error, result, wasThrown) 256 { 257 if (error) { 258 console.error(error); 259 return; 260 } 261 262 this._printRunScriptResult(result, wasThrown); 263 } 264 }, 265 266 /** 267 * @param {?RuntimeAgent.RemoteObject} result 268 * @param {boolean=} wasThrown 269 */ 270 _printRunScriptResult: function(result, wasThrown) 271 { 272 var level = (wasThrown ? WebInspector.ConsoleMessage.MessageLevel.Error : WebInspector.ConsoleMessage.MessageLevel.Log); 273 var message = WebInspector.ConsoleMessage.create(WebInspector.ConsoleMessage.MessageSource.JS, level, "", undefined, undefined, undefined, undefined, undefined, [result]); 274 WebInspector.console.addMessage(message) 275 }, 276 277 /** 278 * @param {WebInspector.DebuggerModel.Location} rawLocation 279 * @return {WebInspector.UILocation} 280 */ 281 _rawLocationToUILocation: function(rawLocation) 282 { 283 var uiSourceCode = this._uiSourceCodeForScriptId[rawLocation.scriptId]; 284 if (!uiSourceCode) 285 return null; 286 return new WebInspector.UILocation(uiSourceCode, rawLocation.lineNumber, rawLocation.columnNumber || 0); 287 }, 288 289 /** 290 * @param {WebInspector.UISourceCode} uiSourceCode 291 * @param {number} lineNumber 292 * @param {number} columnNumber 293 * @return {WebInspector.DebuggerModel.Location} 294 */ 295 _uiLocationToRawLocation: function(uiSourceCode, lineNumber, columnNumber) 296 { 297 var script = this._scriptForUISourceCode.get(uiSourceCode); 298 if (!script) 299 return null; 300 301 return WebInspector.debuggerModel.createRawLocation(script, lineNumber, columnNumber); 302 }, 303 304 /** 305 * @param {WebInspector.Script} script 306 */ 307 _addScript: function(script) 308 { 309 var snippetId = this._snippetIdForSourceURL(script.sourceURL); 310 if (!snippetId) 311 return; 312 var uiSourceCode = this._uiSourceCodeForSnippetId[snippetId]; 313 314 if (!uiSourceCode || this._evaluationSourceURL(uiSourceCode) !== script.sourceURL) 315 return; 316 317 console.assert(!this._scriptForUISourceCode.get(uiSourceCode)); 318 this._uiSourceCodeForScriptId[script.scriptId] = uiSourceCode; 319 this._scriptForUISourceCode.put(uiSourceCode, script); 320 uiSourceCode.scriptFile().setHasDivergedFromVM(false); 321 script.pushSourceMapping(this._snippetScriptMapping); 322 }, 323 324 /** 325 * @param {WebInspector.UISourceCode} uiSourceCode 326 * @return {Array.<Object>} 327 */ 328 _removeBreakpoints: function(uiSourceCode) 329 { 330 var breakpointLocations = WebInspector.breakpointManager.breakpointLocationsForUISourceCode(uiSourceCode); 331 for (var i = 0; i < breakpointLocations.length; ++i) 332 breakpointLocations[i].breakpoint.remove(); 333 return breakpointLocations; 334 }, 335 336 /** 337 * @param {WebInspector.UISourceCode} uiSourceCode 338 * @param {Array.<Object>} breakpointLocations 339 */ 340 _restoreBreakpoints: function(uiSourceCode, breakpointLocations) 341 { 342 for (var i = 0; i < breakpointLocations.length; ++i) { 343 var uiLocation = breakpointLocations[i].uiLocation; 344 var breakpoint = breakpointLocations[i].breakpoint; 345 WebInspector.breakpointManager.setBreakpoint(uiSourceCode, uiLocation.lineNumber, breakpoint.condition(), breakpoint.enabled()); 346 } 347 }, 348 349 /** 350 * @param {WebInspector.UISourceCode} uiSourceCode 351 */ 352 _releaseSnippetScript: function(uiSourceCode) 353 { 354 var script = this._scriptForUISourceCode.get(uiSourceCode); 355 if (!script) 356 return null; 357 358 uiSourceCode.scriptFile().setIsDivergingFromVM(true); 359 uiSourceCode.scriptFile().setHasDivergedFromVM(true); 360 delete this._uiSourceCodeForScriptId[script.scriptId]; 361 this._scriptForUISourceCode.remove(uiSourceCode); 362 delete uiSourceCode._evaluationIndex; 363 uiSourceCode.scriptFile().setIsDivergingFromVM(false); 364 }, 365 366 _debuggerReset: function() 367 { 368 for (var snippetId in this._uiSourceCodeForSnippetId) { 369 var uiSourceCode = this._uiSourceCodeForSnippetId[snippetId]; 370 this._releaseSnippetScript(uiSourceCode); 371 } 372 }, 373 374 /** 375 * @param {WebInspector.UISourceCode} uiSourceCode 376 * @return {string} 377 */ 378 _evaluationSourceURL: function(uiSourceCode) 379 { 380 var evaluationSuffix = "_" + uiSourceCode._evaluationIndex; 381 var snippetId = this._snippetIdForUISourceCode.get(uiSourceCode); 382 return WebInspector.Script.snippetSourceURLPrefix + snippetId + evaluationSuffix; 383 }, 384 385 /** 386 * @param {string} sourceURL 387 * @return {string|null} 388 */ 389 _snippetIdForSourceURL: function(sourceURL) 390 { 391 var snippetPrefix = WebInspector.Script.snippetSourceURLPrefix; 392 if (!sourceURL.startsWith(snippetPrefix)) 393 return null; 394 var splitURL = sourceURL.substring(snippetPrefix.length).split("_"); 395 var snippetId = splitURL[0]; 396 return snippetId; 397 }, 398 399 reset: function() 400 { 401 /** @type {!Object.<string, WebInspector.UISourceCode>} */ 402 this._uiSourceCodeForScriptId = {}; 403 this._scriptForUISourceCode = new Map(); 404 /** @type {!Object.<string, WebInspector.UISourceCode>} */ 405 this._uiSourceCodeForSnippetId = {}; 406 this._snippetIdForUISourceCode = new Map(); 407 this._projectDelegate.reset(); 408 this._loadSnippets(); 409 }, 410 411 __proto__: WebInspector.Object.prototype 412 } 413 414 /** 415 * @constructor 416 * @implements {WebInspector.ScriptFile} 417 * @extends {WebInspector.Object} 418 * @param {WebInspector.ScriptSnippetModel} scriptSnippetModel 419 * @param {WebInspector.UISourceCode} uiSourceCode 420 */ 421 WebInspector.SnippetScriptFile = function(scriptSnippetModel, uiSourceCode) 422 { 423 WebInspector.ScriptFile.call(this); 424 this._scriptSnippetModel = scriptSnippetModel; 425 this._uiSourceCode = uiSourceCode; 426 this._hasDivergedFromVM = true; 427 this._uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this); 428 } 429 430 WebInspector.SnippetScriptFile.prototype = { 431 /** 432 * @return {boolean} 433 */ 434 hasDivergedFromVM: function() 435 { 436 return this._hasDivergedFromVM; 437 }, 438 439 /** 440 * @param {boolean} hasDivergedFromVM 441 */ 442 setHasDivergedFromVM: function(hasDivergedFromVM) 443 { 444 this._hasDivergedFromVM = hasDivergedFromVM; 445 }, 446 447 /** 448 * @return {boolean} 449 */ 450 isDivergingFromVM: function() 451 { 452 return this._isDivergingFromVM; 453 }, 454 455 checkMapping: function() 456 { 457 }, 458 459 /** 460 * @return {boolean} 461 */ 462 isMergingToVM: function() 463 { 464 return false; 465 }, 466 467 /** 468 * @param {boolean} isDivergingFromVM 469 */ 470 setIsDivergingFromVM: function(isDivergingFromVM) 471 { 472 this._isDivergingFromVM = isDivergingFromVM; 473 }, 474 475 _workingCopyChanged: function() 476 { 477 this._scriptSnippetModel._scriptSnippetEdited(this._uiSourceCode); 478 }, 479 480 __proto__: WebInspector.Object.prototype 481 } 482 483 /** 484 * @constructor 485 * @implements {WebInspector.ScriptSourceMapping} 486 * @param {WebInspector.ScriptSnippetModel} scriptSnippetModel 487 */ 488 WebInspector.SnippetScriptMapping = function(scriptSnippetModel) 489 { 490 this._scriptSnippetModel = scriptSnippetModel; 491 } 492 493 WebInspector.SnippetScriptMapping.prototype = { 494 /** 495 * @param {WebInspector.RawLocation} rawLocation 496 * @return {WebInspector.UILocation} 497 */ 498 rawLocationToUILocation: function(rawLocation) 499 { 500 var debuggerModelLocation = /** @type {WebInspector.DebuggerModel.Location} */(rawLocation); 501 return this._scriptSnippetModel._rawLocationToUILocation(debuggerModelLocation); 502 }, 503 504 /** 505 * @param {WebInspector.UISourceCode} uiSourceCode 506 * @param {number} lineNumber 507 * @param {number} columnNumber 508 * @return {WebInspector.DebuggerModel.Location} 509 */ 510 uiLocationToRawLocation: function(uiSourceCode, lineNumber, columnNumber) 511 { 512 return this._scriptSnippetModel._uiLocationToRawLocation(uiSourceCode, lineNumber, columnNumber); 513 }, 514 515 /** 516 * @return {boolean} 517 */ 518 isIdentity: function() 519 { 520 return true; 521 }, 522 523 /** 524 * @param {string} sourceURL 525 * @return {string|null} 526 */ 527 snippetIdForSourceURL: function(sourceURL) 528 { 529 return this._scriptSnippetModel._snippetIdForSourceURL(sourceURL); 530 }, 531 532 /** 533 * @param {WebInspector.Script} script 534 */ 535 addScript: function(script) 536 { 537 this._scriptSnippetModel._addScript(script); 538 } 539 } 540 541 /** 542 * @constructor 543 * @implements {WebInspector.ContentProvider} 544 * @param {WebInspector.Snippet} snippet 545 */ 546 WebInspector.SnippetContentProvider = function(snippet) 547 { 548 this._snippet = snippet; 549 } 550 551 WebInspector.SnippetContentProvider.prototype = { 552 /** 553 * @return {string} 554 */ 555 contentURL: function() 556 { 557 return ""; 558 }, 559 560 /** 561 * @return {WebInspector.ResourceType} 562 */ 563 contentType: function() 564 { 565 return WebInspector.resourceTypes.Script; 566 }, 567 568 /** 569 * @param {function(?string,boolean,string)} callback 570 */ 571 requestContent: function(callback) 572 { 573 callback(this._snippet.content, false, WebInspector.resourceTypes.Script.canonicalMimeType()); 574 }, 575 576 /** 577 * @param {string} query 578 * @param {boolean} caseSensitive 579 * @param {boolean} isRegex 580 * @param {function(Array.<WebInspector.ContentProvider.SearchMatch>)} callback 581 */ 582 searchInContent: function(query, caseSensitive, isRegex, callback) 583 { 584 function performSearch() 585 { 586 callback(WebInspector.ContentProvider.performSearchInContent(this._snippet.content, query, caseSensitive, isRegex)); 587 } 588 589 // searchInContent should call back later. 590 window.setTimeout(performSearch.bind(this), 0); 591 }, 592 593 __proto__: WebInspector.ContentProvider.prototype 594 } 595 596 /** 597 * @constructor 598 * @extends {WebInspector.ContentProviderBasedProjectDelegate} 599 * @param {WebInspector.ScriptSnippetModel} model 600 */ 601 WebInspector.SnippetsProjectDelegate = function(model) 602 { 603 WebInspector.ContentProviderBasedProjectDelegate.call(this, WebInspector.projectTypes.Snippets); 604 this._model = model; 605 } 606 607 WebInspector.SnippetsProjectDelegate.prototype = { 608 /** 609 * @override 610 * @return {string} 611 */ 612 id: function() 613 { 614 return WebInspector.projectTypes.Snippets + ":"; 615 }, 616 617 /** 618 * @param {string} name 619 * @param {WebInspector.ContentProvider} contentProvider 620 * @return {string} 621 */ 622 addSnippet: function(name, contentProvider) 623 { 624 return this.addContentProvider("", name, name, contentProvider, true, false); 625 }, 626 627 /** 628 * @return {boolean} 629 */ 630 canSetFileContent: function() 631 { 632 return true; 633 }, 634 635 /** 636 * @param {string} path 637 * @param {string} newContent 638 * @param {function(?string)} callback 639 */ 640 setFileContent: function(path, newContent, callback) 641 { 642 this._model._setScriptSnippetContent(path, newContent); 643 callback(""); 644 }, 645 646 /** 647 * @return {boolean} 648 */ 649 canRename: function() 650 { 651 return true; 652 }, 653 654 /** 655 * @param {string} path 656 * @param {string} newName 657 * @param {function(boolean, string=)} callback 658 */ 659 performRename: function(path, newName, callback) 660 { 661 this._model.renameScriptSnippet(path, newName, callback); 662 }, 663 664 /** 665 * @param {string} path 666 * @param {?string} name 667 * @param {function(?string)} callback 668 */ 669 createFile: function(path, name, callback) 670 { 671 var filePath = this._model.createScriptSnippet(); 672 callback(filePath); 673 }, 674 675 /** 676 * @param {string} path 677 */ 678 deleteFile: function(path) 679 { 680 this._model.deleteScriptSnippet(path); 681 }, 682 683 __proto__: WebInspector.ContentProviderBasedProjectDelegate.prototype 684 } 685 686 /** 687 * @type {?WebInspector.ScriptSnippetModel} 688 */ 689 WebInspector.scriptSnippetModel = null; 690