1/* 2 * Copyright (C) 2011 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 * @extends {WebInspector.View} 33 * @constructor 34 * @param {WebInspector.ContentProvider} contentProvider 35 */ 36WebInspector.SourceFrame = function(contentProvider) 37{ 38 WebInspector.View.call(this); 39 this.element.addStyleClass("script-view"); 40 this.element.addStyleClass("fill"); 41 42 this._url = contentProvider.contentURL(); 43 this._contentProvider = contentProvider; 44 45 var textEditorDelegate = new WebInspector.TextEditorDelegateForSourceFrame(this); 46 47 if (WebInspector.experimentsSettings.codemirror.isEnabled()) { 48 loadScript("CodeMirrorTextEditor.js"); 49 this._textEditor = new WebInspector.CodeMirrorTextEditor(this._url, textEditorDelegate); 50 } else if (WebInspector.experimentsSettings.aceTextEditor.isEnabled()) { 51 loadScript("AceTextEditor.js"); 52 this._textEditor = new WebInspector.AceTextEditor(this._url, textEditorDelegate); 53 } else 54 this._textEditor = new WebInspector.DefaultTextEditor(this._url, textEditorDelegate); 55 56 this._currentSearchResultIndex = -1; 57 this._searchResults = []; 58 59 this._messages = []; 60 this._rowMessages = {}; 61 this._messageBubbles = {}; 62 63 this._textEditor.setReadOnly(!this.canEditSource()); 64 65 this._shortcuts = {}; 66 this._shortcuts[WebInspector.KeyboardShortcut.makeKey("s", WebInspector.KeyboardShortcut.Modifiers.CtrlOrMeta)] = this._commitEditing.bind(this); 67 this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false); 68 69 this._sourcePositionElement = document.createElement("div"); 70 this._sourcePositionElement.className = "source-frame-cursor-position"; 71} 72 73/** 74 * @param {string} query 75 * @param {string=} modifiers 76 */ 77WebInspector.SourceFrame.createSearchRegex = function(query, modifiers) 78{ 79 var regex; 80 modifiers = modifiers || ""; 81 82 // First try creating regex if user knows the / / hint. 83 try { 84 if (/^\/.*\/$/.test(query)) 85 regex = new RegExp(query.substring(1, query.length - 1), modifiers); 86 } catch (e) { 87 // Silent catch. 88 } 89 90 // Otherwise just do case-insensitive search. 91 if (!regex) 92 regex = createPlainTextSearchRegex(query, "i" + modifiers); 93 94 return regex; 95} 96 97WebInspector.SourceFrame.Events = { 98 ScrollChanged: "ScrollChanged", 99 SelectionChanged: "SelectionChanged" 100} 101 102WebInspector.SourceFrame.prototype = { 103 wasShown: function() 104 { 105 this._ensureContentLoaded(); 106 this._textEditor.show(this.element); 107 this._editorAttached = true; 108 this._wasShownOrLoaded(); 109 }, 110 111 /** 112 * @return {boolean} 113 */ 114 _isEditorShowing: function() 115 { 116 return this.isShowing() && this._editorAttached; 117 }, 118 119 willHide: function() 120 { 121 WebInspector.View.prototype.willHide.call(this); 122 123 this._clearLineHighlight(); 124 this._clearLineToReveal(); 125 }, 126 127 /** 128 * @return {?Element} 129 */ 130 statusBarText: function() 131 { 132 return this._sourcePositionElement; 133 }, 134 135 /** 136 * @return {Array.<Element>} 137 */ 138 statusBarItems: function() 139 { 140 return []; 141 }, 142 143 defaultFocusedElement: function() 144 { 145 return this._textEditor.defaultFocusedElement(); 146 }, 147 148 get loaded() 149 { 150 return this._loaded; 151 }, 152 153 hasContent: function() 154 { 155 return true; 156 }, 157 158 get textEditor() 159 { 160 return this._textEditor; 161 }, 162 163 _ensureContentLoaded: function() 164 { 165 if (!this._contentRequested) { 166 this._contentRequested = true; 167 this._contentProvider.requestContent(this.setContent.bind(this)); 168 } 169 }, 170 171 addMessage: function(msg) 172 { 173 this._messages.push(msg); 174 if (this.loaded) 175 this.addMessageToSource(msg.line - 1, msg); 176 }, 177 178 clearMessages: function() 179 { 180 for (var line in this._messageBubbles) { 181 var bubble = this._messageBubbles[line]; 182 var lineNumber = parseInt(line, 10); 183 this._textEditor.removeDecoration(lineNumber, bubble); 184 } 185 186 this._messages = []; 187 this._rowMessages = {}; 188 this._messageBubbles = {}; 189 }, 190 191 /** 192 * @param {number} line 193 */ 194 canHighlightLine: function(line) 195 { 196 return true; 197 }, 198 199 /** 200 * @param {number} line 201 */ 202 highlightLine: function(line) 203 { 204 this._clearLineToReveal(); 205 this._clearLineToScrollTo(); 206 this._lineToHighlight = line; 207 this._innerHighlightLineIfNeeded(); 208 this._textEditor.setSelection(WebInspector.TextRange.createFromLocation(line, 0)); 209 }, 210 211 _innerHighlightLineIfNeeded: function() 212 { 213 if (typeof this._lineToHighlight === "number") { 214 if (this.loaded && this._isEditorShowing()) { 215 this._textEditor.highlightLine(this._lineToHighlight); 216 delete this._lineToHighlight 217 } 218 } 219 }, 220 221 _clearLineHighlight: function() 222 { 223 this._textEditor.clearLineHighlight(); 224 delete this._lineToHighlight; 225 }, 226 227 /** 228 * @param {number} line 229 */ 230 revealLine: function(line) 231 { 232 this._clearLineHighlight(); 233 this._clearLineToScrollTo(); 234 this._lineToReveal = line; 235 this._innerRevealLineIfNeeded(); 236 }, 237 238 _innerRevealLineIfNeeded: function() 239 { 240 if (typeof this._lineToReveal === "number") { 241 if (this.loaded && this._isEditorShowing()) { 242 this._textEditor.revealLine(this._lineToReveal); 243 delete this._lineToReveal 244 } 245 } 246 }, 247 248 _clearLineToReveal: function() 249 { 250 delete this._lineToReveal; 251 }, 252 253 /** 254 * @param {number} line 255 */ 256 scrollToLine: function(line) 257 { 258 this._clearLineHighlight(); 259 this._clearLineToReveal(); 260 this._lineToScrollTo = line; 261 this._innerScrollToLineIfNeeded(); 262 }, 263 264 _innerScrollToLineIfNeeded: function() 265 { 266 if (typeof this._lineToScrollTo === "number") { 267 if (this.loaded && this._isEditorShowing()) { 268 this._textEditor.scrollToLine(this._lineToScrollTo); 269 delete this._lineToScrollTo; 270 } 271 } 272 }, 273 274 _clearLineToScrollTo: function() 275 { 276 delete this._lineToScrollTo; 277 }, 278 279 /** 280 * @param {WebInspector.TextRange} textRange 281 */ 282 setSelection: function(textRange) 283 { 284 this._selectionToSet = textRange; 285 this._innerSetSelectionIfNeeded(); 286 }, 287 288 _innerSetSelectionIfNeeded: function() 289 { 290 if (this._selectionToSet && this.loaded && this._isEditorShowing()) { 291 this._textEditor.setSelection(this._selectionToSet); 292 delete this._selectionToSet; 293 } 294 }, 295 296 _wasShownOrLoaded: function() 297 { 298 this._innerHighlightLineIfNeeded(); 299 this._innerRevealLineIfNeeded(); 300 this._innerScrollToLineIfNeeded(); 301 this._innerSetSelectionIfNeeded(); 302 }, 303 304 onTextChanged: function(oldRange, newRange) 305 { 306 if (!this._isReplacing) 307 WebInspector.searchController.cancelSearch(); 308 this.clearMessages(); 309 }, 310 311 /** 312 * @param {?string} content 313 * @param {boolean} contentEncoded 314 * @param {string} mimeType 315 */ 316 setContent: function(content, contentEncoded, mimeType) 317 { 318 this._textEditor.mimeType = mimeType; 319 320 if (!this._loaded) { 321 this._loaded = true; 322 this._textEditor.setText(content || ""); 323 } else 324 this._textEditor.editRange(this._textEditor.range(), content || ""); 325 326 this._textEditor.beginUpdates(); 327 328 this._setTextEditorDecorations(); 329 330 this._wasShownOrLoaded(); 331 332 if (this._delayedFindSearchMatches) { 333 this._delayedFindSearchMatches(); 334 delete this._delayedFindSearchMatches; 335 } 336 337 this.onTextEditorContentLoaded(); 338 339 this._textEditor.endUpdates(); 340 }, 341 342 onTextEditorContentLoaded: function() {}, 343 344 _setTextEditorDecorations: function() 345 { 346 this._rowMessages = {}; 347 this._messageBubbles = {}; 348 349 this._textEditor.beginUpdates(); 350 351 this._addExistingMessagesToSource(); 352 353 this._textEditor.endUpdates(); 354 }, 355 356 /** 357 * @param {string} query 358 * @param {function(WebInspector.View, number)} callback 359 */ 360 performSearch: function(query, callback) 361 { 362 // Call searchCanceled since it will reset everything we need before doing a new search. 363 this.searchCanceled(); 364 365 function doFindSearchMatches(query) 366 { 367 this._currentSearchResultIndex = -1; 368 this._searchResults = []; 369 370 var regex = WebInspector.SourceFrame.createSearchRegex(query); 371 this._searchResults = this._collectRegexMatches(regex); 372 var shiftToIndex = 0; 373 var selection = this._textEditor.lastSelection(); 374 for (var i = 0; selection && i < this._searchResults.length; ++i) { 375 if (this._searchResults[i].compareTo(selection) >= 0) { 376 shiftToIndex = i; 377 break; 378 } 379 } 380 381 if (shiftToIndex) 382 this._searchResults = this._searchResults.rotate(shiftToIndex); 383 384 callback(this, this._searchResults.length); 385 } 386 387 if (this.loaded) 388 doFindSearchMatches.call(this, query); 389 else 390 this._delayedFindSearchMatches = doFindSearchMatches.bind(this, query); 391 392 this._ensureContentLoaded(); 393 }, 394 395 searchCanceled: function() 396 { 397 delete this._delayedFindSearchMatches; 398 if (!this.loaded) 399 return; 400 401 this._currentSearchResultIndex = -1; 402 this._searchResults = []; 403 this._textEditor.markAndRevealRange(null); 404 }, 405 406 hasSearchResults: function() 407 { 408 return this._searchResults.length > 0; 409 }, 410 411 jumpToFirstSearchResult: function() 412 { 413 this.jumpToSearchResult(0); 414 }, 415 416 jumpToLastSearchResult: function() 417 { 418 this.jumpToSearchResult(this._searchResults.length - 1); 419 }, 420 421 jumpToNextSearchResult: function() 422 { 423 this.jumpToSearchResult(this._currentSearchResultIndex + 1); 424 }, 425 426 jumpToPreviousSearchResult: function() 427 { 428 this.jumpToSearchResult(this._currentSearchResultIndex - 1); 429 }, 430 431 showingFirstSearchResult: function() 432 { 433 return this._searchResults.length && this._currentSearchResultIndex === 0; 434 }, 435 436 showingLastSearchResult: function() 437 { 438 return this._searchResults.length && this._currentSearchResultIndex === (this._searchResults.length - 1); 439 }, 440 441 get currentSearchResultIndex() 442 { 443 return this._currentSearchResultIndex; 444 }, 445 446 jumpToSearchResult: function(index) 447 { 448 if (!this.loaded || !this._searchResults.length) 449 return; 450 this._currentSearchResultIndex = (index + this._searchResults.length) % this._searchResults.length; 451 this._textEditor.markAndRevealRange(this._searchResults[this._currentSearchResultIndex]); 452 }, 453 454 /** 455 * @param {string} text 456 */ 457 replaceSearchMatchWith: function(text) 458 { 459 var range = this._searchResults[this._currentSearchResultIndex]; 460 if (!range) 461 return; 462 this._textEditor.markAndRevealRange(null); 463 464 this._isReplacing = true; 465 var newRange = this._textEditor.editRange(range, text); 466 delete this._isReplacing; 467 468 this._textEditor.setSelection(newRange.collapseToEnd()); 469 }, 470 471 /** 472 * @param {string} query 473 * @param {string} replacement 474 */ 475 replaceAllWith: function(query, replacement) 476 { 477 this._textEditor.markAndRevealRange(null); 478 479 var text = this._textEditor.text(); 480 var range = this._textEditor.range(); 481 text = text.replace(WebInspector.SourceFrame.createSearchRegex(query, "g"), replacement); 482 483 this._isReplacing = true; 484 this._textEditor.editRange(range, text); 485 delete this._isReplacing; 486 }, 487 488 _collectRegexMatches: function(regexObject) 489 { 490 var ranges = []; 491 for (var i = 0; i < this._textEditor.linesCount; ++i) { 492 var line = this._textEditor.line(i); 493 var offset = 0; 494 do { 495 var match = regexObject.exec(line); 496 if (match) { 497 if (match[0].length) 498 ranges.push(new WebInspector.TextRange(i, offset + match.index, i, offset + match.index + match[0].length)); 499 offset += match.index + 1; 500 line = line.substring(match.index + 1); 501 } 502 } while (match && line); 503 } 504 return ranges; 505 }, 506 507 _addExistingMessagesToSource: function() 508 { 509 var length = this._messages.length; 510 for (var i = 0; i < length; ++i) 511 this.addMessageToSource(this._messages[i].line - 1, this._messages[i]); 512 }, 513 514 /** 515 * @param {number} lineNumber 516 * @param {WebInspector.ConsoleMessage} msg 517 */ 518 addMessageToSource: function(lineNumber, msg) 519 { 520 if (lineNumber >= this._textEditor.linesCount) 521 lineNumber = this._textEditor.linesCount - 1; 522 if (lineNumber < 0) 523 lineNumber = 0; 524 525 var rowMessages = this._rowMessages[lineNumber]; 526 if (!rowMessages) { 527 rowMessages = []; 528 this._rowMessages[lineNumber] = rowMessages; 529 } 530 531 for (var i = 0; i < rowMessages.length; ++i) { 532 if (rowMessages[i].consoleMessage.isEqual(msg)) { 533 rowMessages[i].repeatCount = msg.totalRepeatCount; 534 this._updateMessageRepeatCount(rowMessages[i]); 535 return; 536 } 537 } 538 539 var rowMessage = { consoleMessage: msg }; 540 rowMessages.push(rowMessage); 541 542 this._textEditor.beginUpdates(); 543 var messageBubbleElement = this._messageBubbles[lineNumber]; 544 if (!messageBubbleElement) { 545 messageBubbleElement = document.createElement("div"); 546 messageBubbleElement.className = "webkit-html-message-bubble"; 547 this._messageBubbles[lineNumber] = messageBubbleElement; 548 this._textEditor.addDecoration(lineNumber, messageBubbleElement); 549 } 550 551 var imageURL; 552 switch (msg.level) { 553 case WebInspector.ConsoleMessage.MessageLevel.Error: 554 messageBubbleElement.addStyleClass("webkit-html-error-message"); 555 imageURL = "Images/errorIcon.png"; 556 break; 557 case WebInspector.ConsoleMessage.MessageLevel.Warning: 558 messageBubbleElement.addStyleClass("webkit-html-warning-message"); 559 imageURL = "Images/warningIcon.png"; 560 break; 561 } 562 563 var messageLineElement = document.createElement("div"); 564 messageLineElement.className = "webkit-html-message-line"; 565 messageBubbleElement.appendChild(messageLineElement); 566 567 // Create the image element in the Inspector's document so we can use relative image URLs. 568 var image = document.createElement("img"); 569 image.src = imageURL; 570 image.className = "webkit-html-message-icon"; 571 messageLineElement.appendChild(image); 572 messageLineElement.appendChild(document.createTextNode(msg.message)); 573 574 rowMessage.element = messageLineElement; 575 rowMessage.repeatCount = msg.totalRepeatCount; 576 this._updateMessageRepeatCount(rowMessage); 577 this._textEditor.endUpdates(); 578 }, 579 580 _updateMessageRepeatCount: function(rowMessage) 581 { 582 if (rowMessage.repeatCount < 2) 583 return; 584 585 if (!rowMessage.repeatCountElement) { 586 var repeatCountElement = document.createElement("span"); 587 rowMessage.element.appendChild(repeatCountElement); 588 rowMessage.repeatCountElement = repeatCountElement; 589 } 590 591 rowMessage.repeatCountElement.textContent = WebInspector.UIString(" (repeated %d times)", rowMessage.repeatCount); 592 }, 593 594 /** 595 * @param {number} lineNumber 596 * @param {WebInspector.ConsoleMessage} msg 597 */ 598 removeMessageFromSource: function(lineNumber, msg) 599 { 600 if (lineNumber >= this._textEditor.linesCount) 601 lineNumber = this._textEditor.linesCount - 1; 602 if (lineNumber < 0) 603 lineNumber = 0; 604 605 var rowMessages = this._rowMessages[lineNumber]; 606 for (var i = 0; rowMessages && i < rowMessages.length; ++i) { 607 var rowMessage = rowMessages[i]; 608 if (rowMessage.consoleMessage !== msg) 609 continue; 610 611 var messageLineElement = rowMessage.element; 612 var messageBubbleElement = messageLineElement.parentElement; 613 messageBubbleElement.removeChild(messageLineElement); 614 rowMessages.remove(rowMessage); 615 if (!rowMessages.length) 616 delete this._rowMessages[lineNumber]; 617 if (!messageBubbleElement.childElementCount) { 618 this._textEditor.removeDecoration(lineNumber, messageBubbleElement); 619 delete this._messageBubbles[lineNumber]; 620 } 621 break; 622 } 623 }, 624 625 populateLineGutterContextMenu: function(contextMenu, lineNumber) 626 { 627 }, 628 629 populateTextAreaContextMenu: function(contextMenu, lineNumber) 630 { 631 }, 632 633 inheritScrollPositions: function(sourceFrame) 634 { 635 this._textEditor.inheritScrollPositions(sourceFrame._textEditor); 636 }, 637 638 /** 639 * @return {boolean} 640 */ 641 canEditSource: function() 642 { 643 return false; 644 }, 645 646 /** 647 * @param {string} text 648 */ 649 commitEditing: function(text) 650 { 651 }, 652 653 /** 654 * @param {WebInspector.TextRange} textRange 655 */ 656 selectionChanged: function(textRange) 657 { 658 this._updateSourcePosition(textRange); 659 this.dispatchEventToListeners(WebInspector.SourceFrame.Events.SelectionChanged, textRange); 660 }, 661 662 /** 663 * @param {WebInspector.TextRange} textRange 664 */ 665 _updateSourcePosition: function(textRange) 666 { 667 if (!textRange) 668 return; 669 670 if (textRange.isEmpty()) { 671 this._sourcePositionElement.textContent = WebInspector.UIString("Line %d, Column %d", textRange.endLine + 1, textRange.endColumn + 1); 672 return; 673 } 674 textRange = textRange.normalize(); 675 676 var selectedText = this._textEditor.copyRange(textRange); 677 if (textRange.startLine === textRange.endLine) 678 this._sourcePositionElement.textContent = WebInspector.UIString("%d characters selected", selectedText.length); 679 else 680 this._sourcePositionElement.textContent = WebInspector.UIString("%d lines, %d characters selected", textRange.endLine - textRange.startLine + 1, selectedText.length); 681 }, 682 683 /** 684 * @param {number} lineNumber 685 */ 686 scrollChanged: function(lineNumber) 687 { 688 this.dispatchEventToListeners(WebInspector.SourceFrame.Events.ScrollChanged, lineNumber); 689 }, 690 691 _handleKeyDown: function(e) 692 { 693 var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e); 694 var handler = this._shortcuts[shortcutKey]; 695 if (handler && handler()) 696 e.consume(true); 697 }, 698 699 _commitEditing: function() 700 { 701 if (this._textEditor.readOnly()) 702 return false; 703 704 var content = this._textEditor.text(); 705 this.commitEditing(content); 706 return true; 707 }, 708 709 __proto__: WebInspector.View.prototype 710} 711 712 713/** 714 * @implements {WebInspector.TextEditorDelegate} 715 * @constructor 716 */ 717WebInspector.TextEditorDelegateForSourceFrame = function(sourceFrame) 718{ 719 this._sourceFrame = sourceFrame; 720} 721 722WebInspector.TextEditorDelegateForSourceFrame.prototype = { 723 onTextChanged: function(oldRange, newRange) 724 { 725 this._sourceFrame.onTextChanged(oldRange, newRange); 726 }, 727 728 /** 729 * @param {WebInspector.TextRange} textRange 730 */ 731 selectionChanged: function(textRange) 732 { 733 this._sourceFrame.selectionChanged(textRange); 734 }, 735 736 /** 737 * @param {number} lineNumber 738 */ 739 scrollChanged: function(lineNumber) 740 { 741 this._sourceFrame.scrollChanged(lineNumber); 742 }, 743 744 populateLineGutterContextMenu: function(contextMenu, lineNumber) 745 { 746 this._sourceFrame.populateLineGutterContextMenu(contextMenu, lineNumber); 747 }, 748 749 populateTextAreaContextMenu: function(contextMenu, lineNumber) 750 { 751 this._sourceFrame.populateTextAreaContextMenu(contextMenu, lineNumber); 752 }, 753 754 /** 755 * @param {string} hrefValue 756 * @param {boolean} isExternal 757 * @return {Element} 758 */ 759 createLink: function(hrefValue, isExternal) 760 { 761 var targetLocation = WebInspector.ParsedURL.completeURL(this._sourceFrame._url, hrefValue); 762 return WebInspector.linkifyURLAsNode(targetLocation || hrefValue, hrefValue, undefined, isExternal); 763 }, 764 765 __proto__: WebInspector.TextEditorDelegate.prototype 766} 767