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.VBox} 33 * @constructor 34 * @implements {WebInspector.Replaceable} 35 * @param {!WebInspector.ContentProvider} contentProvider 36 */ 37 WebInspector.SourceFrame = function(contentProvider) 38 { 39 WebInspector.VBox.call(this); 40 this.element.classList.add("script-view"); 41 42 this._url = contentProvider.contentURL(); 43 this._contentProvider = contentProvider; 44 45 var textEditorDelegate = new WebInspector.TextEditorDelegateForSourceFrame(this); 46 47 this._textEditor = new WebInspector.CodeMirrorTextEditor(this._url, textEditorDelegate); 48 49 this._currentSearchResultIndex = -1; 50 this._searchResults = []; 51 52 this._messages = []; 53 this._rowMessageBuckets = {}; 54 55 this._textEditor.setReadOnly(!this.canEditSource()); 56 57 this._shortcuts = {}; 58 this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false); 59 60 this._sourcePosition = new WebInspector.StatusBarText("", "source-frame-cursor-position"); 61 62 this._errorPopoverHelper = new WebInspector.PopoverHelper(this.element, this._getErrorAnchor.bind(this), this._showErrorPopover.bind(this)); 63 this._errorPopoverHelper.setTimeout(100, 100); 64 } 65 66 /** 67 * @param {string} query 68 * @param {string=} modifiers 69 * @return {!RegExp} 70 */ 71 WebInspector.SourceFrame.createSearchRegex = function(query, modifiers) 72 { 73 var regex; 74 modifiers = modifiers || ""; 75 76 // First try creating regex if user knows the / / hint. 77 try { 78 if (/^\/.+\/$/.test(query)) { 79 regex = new RegExp(query.substring(1, query.length - 1), modifiers); 80 regex.__fromRegExpQuery = true; 81 } 82 } catch (e) { 83 // Silent catch. 84 } 85 86 // Otherwise just do case-insensitive search. 87 if (!regex) 88 regex = createPlainTextSearchRegex(query, "i" + modifiers); 89 90 return regex; 91 } 92 93 WebInspector.SourceFrame.Events = { 94 ScrollChanged: "ScrollChanged", 95 SelectionChanged: "SelectionChanged", 96 JumpHappened: "JumpHappened" 97 } 98 99 WebInspector.SourceFrame.prototype = { 100 /** 101 * @param {!Element} target 102 * @param {!Event} event 103 * @return {(!Element|undefined)} 104 */ 105 _getErrorAnchor: function(target, event) 106 { 107 var element = target.enclosingNodeOrSelfWithClass("text-editor-line-decoration-icon") 108 || target.enclosingNodeOrSelfWithClass("text-editor-line-decoration-wave"); 109 if (!element) 110 return; 111 this._errorWavePopoverAnchor = new AnchorBox(event.clientX, event.clientY, 1, 1); 112 return element; 113 }, 114 115 /** 116 * @param {!Element} anchor 117 * @param {!WebInspector.Popover} popover 118 */ 119 _showErrorPopover: function(anchor, popover) 120 { 121 var messageBucket = anchor.enclosingNodeOrSelfWithClass("text-editor-line-decoration")._messageBucket; 122 var messagesOutline = messageBucket.messagesDescription(); 123 var popoverAnchor = anchor.enclosingNodeOrSelfWithClass("text-editor-line-decoration-icon") ? anchor : this._errorWavePopoverAnchor; 124 popover.show(messagesOutline, popoverAnchor); 125 }, 126 127 /** 128 * @param {number} key 129 * @param {function():boolean} handler 130 */ 131 addShortcut: function(key, handler) 132 { 133 this._shortcuts[key] = handler; 134 }, 135 136 wasShown: function() 137 { 138 this._ensureContentLoaded(); 139 this._textEditor.show(this.element); 140 this._editorAttached = true; 141 for (var line in this._rowMessageBuckets) { 142 var bucket = this._rowMessageBuckets[line]; 143 bucket._updateDecorationPosition(); 144 } 145 this._wasShownOrLoaded(); 146 }, 147 148 /** 149 * @return {boolean} 150 */ 151 _isEditorShowing: function() 152 { 153 return this.isShowing() && this._editorAttached; 154 }, 155 156 willHide: function() 157 { 158 WebInspector.View.prototype.willHide.call(this); 159 160 this._clearPositionToReveal(); 161 }, 162 163 /** 164 * @return {?Element} 165 */ 166 statusBarText: function() 167 { 168 return this._sourcePosition.element; 169 }, 170 171 /** 172 * @return {!Array.<!Element>} 173 */ 174 statusBarItems: function() 175 { 176 return []; 177 }, 178 179 /** 180 * @return {!Element} 181 */ 182 defaultFocusedElement: function() 183 { 184 return this._textEditor.defaultFocusedElement(); 185 }, 186 187 get loaded() 188 { 189 return this._loaded; 190 }, 191 192 /** 193 * @return {boolean} 194 */ 195 hasContent: function() 196 { 197 return true; 198 }, 199 200 get textEditor() 201 { 202 return this._textEditor; 203 }, 204 205 _ensureContentLoaded: function() 206 { 207 if (!this._contentRequested) { 208 this._contentRequested = true; 209 this._contentProvider.requestContent(this.setContent.bind(this)); 210 } 211 }, 212 213 addMessage: function(msg) 214 { 215 this._messages.push(msg); 216 if (this.loaded) 217 this.addMessageToSource(msg.line - 1, msg); 218 }, 219 220 clearMessages: function() 221 { 222 for (var line in this._rowMessageBuckets) { 223 var bubble = this._rowMessageBuckets[line]; 224 bubble.detachFromEditor(); 225 } 226 227 this._messages = []; 228 this._rowMessageBuckets = {}; 229 }, 230 231 /** 232 * @param {number} line 0-based 233 * @param {number=} column 234 * @param {boolean=} shouldHighlight 235 */ 236 revealPosition: function(line, column, shouldHighlight) 237 { 238 this._clearLineToScrollTo(); 239 this._clearSelectionToSet(); 240 this._positionToReveal = { line: line, column: column, shouldHighlight: shouldHighlight }; 241 this._innerRevealPositionIfNeeded(); 242 }, 243 244 _innerRevealPositionIfNeeded: function() 245 { 246 if (!this._positionToReveal) 247 return; 248 249 if (!this.loaded || !this._isEditorShowing()) 250 return; 251 252 this._textEditor.revealPosition(this._positionToReveal.line, this._positionToReveal.column, this._positionToReveal.shouldHighlight); 253 delete this._positionToReveal; 254 }, 255 256 _clearPositionToReveal: function() 257 { 258 this._textEditor.clearPositionHighlight(); 259 delete this._positionToReveal; 260 }, 261 262 /** 263 * @param {number} line 264 */ 265 scrollToLine: function(line) 266 { 267 this._clearPositionToReveal(); 268 this._lineToScrollTo = line; 269 this._innerScrollToLineIfNeeded(); 270 }, 271 272 _innerScrollToLineIfNeeded: function() 273 { 274 if (typeof this._lineToScrollTo === "number") { 275 if (this.loaded && this._isEditorShowing()) { 276 this._textEditor.scrollToLine(this._lineToScrollTo); 277 delete this._lineToScrollTo; 278 } 279 } 280 }, 281 282 _clearLineToScrollTo: function() 283 { 284 delete this._lineToScrollTo; 285 }, 286 287 /** 288 * @return {!WebInspector.TextRange} 289 */ 290 selection: function() 291 { 292 return this.textEditor.selection(); 293 }, 294 295 /** 296 * @param {!WebInspector.TextRange} textRange 297 */ 298 setSelection: function(textRange) 299 { 300 this._selectionToSet = textRange; 301 this._innerSetSelectionIfNeeded(); 302 }, 303 304 _innerSetSelectionIfNeeded: function() 305 { 306 if (this._selectionToSet && this.loaded && this._isEditorShowing()) { 307 this._textEditor.setSelection(this._selectionToSet); 308 delete this._selectionToSet; 309 } 310 }, 311 312 _clearSelectionToSet: function() 313 { 314 delete this._selectionToSet; 315 }, 316 317 _wasShownOrLoaded: function() 318 { 319 this._innerRevealPositionIfNeeded(); 320 this._innerSetSelectionIfNeeded(); 321 this._innerScrollToLineIfNeeded(); 322 }, 323 324 onTextChanged: function(oldRange, newRange) 325 { 326 if (this._searchResultsChangedCallback) 327 this._searchResultsChangedCallback(); 328 this.clearMessages(); 329 }, 330 331 _simplifyMimeType: function(content, mimeType) 332 { 333 if (!mimeType) 334 return ""; 335 if (mimeType.indexOf("javascript") >= 0 || 336 mimeType.indexOf("jscript") >= 0 || 337 mimeType.indexOf("ecmascript") >= 0) 338 return "text/javascript"; 339 // A hack around the fact that files with "php" extension might be either standalone or html embedded php scripts. 340 if (mimeType === "text/x-php" && content.match(/\<\?.*\?\>/g)) 341 return "application/x-httpd-php"; 342 return mimeType; 343 }, 344 345 /** 346 * @param {string} highlighterType 347 */ 348 setHighlighterType: function(highlighterType) 349 { 350 this._highlighterType = highlighterType; 351 this._updateHighlighterType(""); 352 }, 353 354 /** 355 * @param {string} content 356 */ 357 _updateHighlighterType: function(content) 358 { 359 this._textEditor.setMimeType(this._simplifyMimeType(content, this._highlighterType)); 360 }, 361 362 /** 363 * @param {?string} content 364 */ 365 setContent: function(content) 366 { 367 if (!this._loaded) { 368 this._loaded = true; 369 this._textEditor.setText(content || ""); 370 this._textEditor.markClean(); 371 } else { 372 var firstLine = this._textEditor.firstVisibleLine(); 373 var selection = this._textEditor.selection(); 374 this._textEditor.setText(content || ""); 375 this._textEditor.scrollToLine(firstLine); 376 this._textEditor.setSelection(selection); 377 } 378 379 this._updateHighlighterType(content || ""); 380 381 this._textEditor.beginUpdates(); 382 383 this._setTextEditorDecorations(); 384 385 this._wasShownOrLoaded(); 386 387 if (this._delayedFindSearchMatches) { 388 this._delayedFindSearchMatches(); 389 delete this._delayedFindSearchMatches; 390 } 391 392 this.onTextEditorContentLoaded(); 393 394 this._textEditor.endUpdates(); 395 }, 396 397 onTextEditorContentLoaded: function() {}, 398 399 _setTextEditorDecorations: function() 400 { 401 this._rowMessageBuckets = {}; 402 403 this._textEditor.beginUpdates(); 404 this._addExistingMessagesToSource(); 405 this._textEditor.endUpdates(); 406 }, 407 408 /** 409 * @param {string} query 410 * @param {boolean} shouldJump 411 * @param {boolean} jumpBackwards 412 * @param {function(!WebInspector.View, number)} callback 413 * @param {function(number)} currentMatchChangedCallback 414 * @param {function()} searchResultsChangedCallback 415 */ 416 performSearch: function(query, shouldJump, jumpBackwards, callback, currentMatchChangedCallback, searchResultsChangedCallback) 417 { 418 /** 419 * @param {string} query 420 * @this {WebInspector.SourceFrame} 421 */ 422 function doFindSearchMatches(query) 423 { 424 this._currentSearchResultIndex = -1; 425 this._searchResults = []; 426 427 var regex = WebInspector.SourceFrame.createSearchRegex(query); 428 this._searchRegex = regex; 429 this._searchResults = this._collectRegexMatches(regex); 430 if (!this._searchResults.length) 431 this._textEditor.cancelSearchResultsHighlight(); 432 else if (shouldJump && jumpBackwards) 433 this.jumpToPreviousSearchResult(); 434 else if (shouldJump) 435 this.jumpToNextSearchResult(); 436 else 437 this._textEditor.highlightSearchResults(regex, null); 438 callback(this, this._searchResults.length); 439 } 440 441 this._resetSearch(); 442 this._currentSearchMatchChangedCallback = currentMatchChangedCallback; 443 this._searchResultsChangedCallback = searchResultsChangedCallback; 444 if (this.loaded) 445 doFindSearchMatches.call(this, query); 446 else 447 this._delayedFindSearchMatches = doFindSearchMatches.bind(this, query); 448 449 this._ensureContentLoaded(); 450 }, 451 452 _editorFocused: function() 453 { 454 this._resetCurrentSearchResultIndex(); 455 }, 456 457 _resetCurrentSearchResultIndex: function() 458 { 459 if (!this._searchResults.length) 460 return; 461 this._currentSearchResultIndex = -1; 462 if (this._currentSearchMatchChangedCallback) 463 this._currentSearchMatchChangedCallback(this._currentSearchResultIndex); 464 this._textEditor.highlightSearchResults(this._searchRegex, null); 465 }, 466 467 _resetSearch: function() 468 { 469 delete this._delayedFindSearchMatches; 470 delete this._currentSearchMatchChangedCallback; 471 delete this._searchResultsChangedCallback; 472 this._currentSearchResultIndex = -1; 473 this._searchResults = []; 474 delete this._searchRegex; 475 }, 476 477 searchCanceled: function() 478 { 479 var range = this._currentSearchResultIndex !== -1 ? this._searchResults[this._currentSearchResultIndex] : null; 480 this._resetSearch(); 481 if (!this.loaded) 482 return; 483 this._textEditor.cancelSearchResultsHighlight(); 484 if (range) 485 this._textEditor.setSelection(range); 486 }, 487 488 /** 489 * @return {boolean} 490 */ 491 hasSearchResults: function() 492 { 493 return this._searchResults.length > 0; 494 }, 495 496 jumpToFirstSearchResult: function() 497 { 498 this.jumpToSearchResult(0); 499 }, 500 501 jumpToLastSearchResult: function() 502 { 503 this.jumpToSearchResult(this._searchResults.length - 1); 504 }, 505 506 /** 507 * @return {number} 508 */ 509 _searchResultIndexForCurrentSelection: function() 510 { 511 return insertionIndexForObjectInListSortedByFunction(this._textEditor.selection(), this._searchResults, WebInspector.TextRange.comparator); 512 }, 513 514 jumpToNextSearchResult: function() 515 { 516 var currentIndex = this._searchResultIndexForCurrentSelection(); 517 var nextIndex = this._currentSearchResultIndex === -1 ? currentIndex : currentIndex + 1; 518 this.jumpToSearchResult(nextIndex); 519 }, 520 521 jumpToPreviousSearchResult: function() 522 { 523 var currentIndex = this._searchResultIndexForCurrentSelection(); 524 this.jumpToSearchResult(currentIndex - 1); 525 }, 526 527 /** 528 * @return {boolean} 529 */ 530 showingFirstSearchResult: function() 531 { 532 return this._searchResults.length && this._currentSearchResultIndex === 0; 533 }, 534 535 /** 536 * @return {boolean} 537 */ 538 showingLastSearchResult: function() 539 { 540 return this._searchResults.length && this._currentSearchResultIndex === (this._searchResults.length - 1); 541 }, 542 543 get currentSearchResultIndex() 544 { 545 return this._currentSearchResultIndex; 546 }, 547 548 jumpToSearchResult: function(index) 549 { 550 if (!this.loaded || !this._searchResults.length) 551 return; 552 this._currentSearchResultIndex = (index + this._searchResults.length) % this._searchResults.length; 553 if (this._currentSearchMatchChangedCallback) 554 this._currentSearchMatchChangedCallback(this._currentSearchResultIndex); 555 this._textEditor.highlightSearchResults(this._searchRegex, this._searchResults[this._currentSearchResultIndex]); 556 }, 557 558 /** 559 * @param {string} text 560 */ 561 replaceSelectionWith: function(text) 562 { 563 var range = this._searchResults[this._currentSearchResultIndex]; 564 if (!range) 565 return; 566 this._textEditor.highlightSearchResults(this._searchRegex, null); 567 var newRange = this._textEditor.editRange(range, text); 568 this._textEditor.setSelection(newRange.collapseToEnd()); 569 }, 570 571 /** 572 * @param {string} query 573 * @param {string} replacement 574 */ 575 replaceAllWith: function(query, replacement) 576 { 577 this._resetCurrentSearchResultIndex(); 578 579 var text = this._textEditor.text(); 580 var range = this._textEditor.range(); 581 var regex = WebInspector.SourceFrame.createSearchRegex(query, "g"); 582 if (regex.__fromRegExpQuery) 583 text = text.replace(regex, replacement); 584 else 585 text = text.replace(regex, function() { return replacement; }); 586 587 var ranges = this._collectRegexMatches(regex); 588 if (!ranges.length) 589 return; 590 591 // Calculate the position of the end of the last range to be edited. 592 var currentRangeIndex = insertionIndexForObjectInListSortedByFunction(this._textEditor.selection(), ranges, WebInspector.TextRange.comparator); 593 var lastRangeIndex = mod(currentRangeIndex - 1, ranges.length); 594 var lastRange = ranges[lastRangeIndex]; 595 var replacementLineEndings = replacement.lineEndings(); 596 var replacementLineCount = replacementLineEndings.length; 597 var lastLineNumber = lastRange.startLine + replacementLineEndings.length - 1; 598 var lastColumnNumber = lastRange.startColumn; 599 if (replacementLineEndings.length > 1) 600 lastColumnNumber = replacementLineEndings[replacementLineCount - 1] - replacementLineEndings[replacementLineCount - 2] - 1; 601 602 this._textEditor.editRange(range, text); 603 this._textEditor.revealPosition(lastLineNumber, lastColumnNumber); 604 this._textEditor.setSelection(WebInspector.TextRange.createFromLocation(lastLineNumber, lastColumnNumber)); 605 }, 606 607 _collectRegexMatches: function(regexObject) 608 { 609 var ranges = []; 610 for (var i = 0; i < this._textEditor.linesCount; ++i) { 611 var line = this._textEditor.line(i); 612 var offset = 0; 613 do { 614 var match = regexObject.exec(line); 615 if (match) { 616 if (match[0].length) 617 ranges.push(new WebInspector.TextRange(i, offset + match.index, i, offset + match.index + match[0].length)); 618 offset += match.index + 1; 619 line = line.substring(match.index + 1); 620 } 621 } while (match && line); 622 } 623 return ranges; 624 }, 625 626 _addExistingMessagesToSource: function() 627 { 628 var length = this._messages.length; 629 for (var i = 0; i < length; ++i) 630 this.addMessageToSource(this._messages[i].line - 1, this._messages[i]); 631 }, 632 633 /** 634 * @param {number} lineNumber 635 * @param {!WebInspector.ConsoleMessage} consoleMessage 636 */ 637 addMessageToSource: function(lineNumber, consoleMessage) 638 { 639 if (lineNumber >= this._textEditor.linesCount) 640 lineNumber = this._textEditor.linesCount - 1; 641 if (lineNumber < 0) 642 lineNumber = 0; 643 644 if (!this._rowMessageBuckets[lineNumber]) 645 this._rowMessageBuckets[lineNumber] = new WebInspector.SourceFrame.RowMessageBucket(this, this._textEditor, lineNumber); 646 var messageBucket = this._rowMessageBuckets[lineNumber]; 647 messageBucket.addMessage(consoleMessage); 648 }, 649 650 /** 651 * @param {number} lineNumber 652 * @param {!WebInspector.ConsoleMessage} msg 653 */ 654 removeMessageFromSource: function(lineNumber, msg) 655 { 656 if (lineNumber >= this._textEditor.linesCount) 657 lineNumber = this._textEditor.linesCount - 1; 658 if (lineNumber < 0) 659 lineNumber = 0; 660 661 var messageBucket = this._rowMessageBuckets[lineNumber]; 662 if (!messageBucket) 663 return; 664 messageBucket.removeMessage(msg); 665 if (!messageBucket.uniqueMessagesCount()) { 666 messageBucket.detachFromEditor(); 667 delete this._rowMessageBuckets[lineNumber]; 668 } 669 }, 670 671 populateLineGutterContextMenu: function(contextMenu, lineNumber) 672 { 673 }, 674 675 populateTextAreaContextMenu: function(contextMenu, lineNumber) 676 { 677 }, 678 679 /** 680 * @param {?WebInspector.TextRange} from 681 * @param {?WebInspector.TextRange} to 682 */ 683 onJumpToPosition: function(from, to) 684 { 685 this.dispatchEventToListeners(WebInspector.SourceFrame.Events.JumpHappened, { 686 from: from, 687 to: to 688 }); 689 }, 690 691 inheritScrollPositions: function(sourceFrame) 692 { 693 this._textEditor.inheritScrollPositions(sourceFrame._textEditor); 694 }, 695 696 /** 697 * @return {boolean} 698 */ 699 canEditSource: function() 700 { 701 return false; 702 }, 703 704 /** 705 * @param {!WebInspector.TextRange} textRange 706 */ 707 selectionChanged: function(textRange) 708 { 709 this._updateSourcePosition(); 710 this.dispatchEventToListeners(WebInspector.SourceFrame.Events.SelectionChanged, textRange); 711 WebInspector.notifications.dispatchEventToListeners(WebInspector.SourceFrame.Events.SelectionChanged, textRange); 712 }, 713 714 _updateSourcePosition: function() 715 { 716 var selections = this._textEditor.selections(); 717 if (!selections.length) 718 return; 719 if (selections.length > 1) { 720 this._sourcePosition.setText(WebInspector.UIString("%d selection regions", selections.length)); 721 return; 722 } 723 var textRange = selections[0]; 724 if (textRange.isEmpty()) { 725 this._sourcePosition.setText(WebInspector.UIString("Line %d, Column %d", textRange.endLine + 1, textRange.endColumn + 1)); 726 return; 727 } 728 textRange = textRange.normalize(); 729 730 var selectedText = this._textEditor.copyRange(textRange); 731 if (textRange.startLine === textRange.endLine) 732 this._sourcePosition.setText(WebInspector.UIString("%d characters selected", selectedText.length)); 733 else 734 this._sourcePosition.setText(WebInspector.UIString("%d lines, %d characters selected", textRange.endLine - textRange.startLine + 1, selectedText.length)); 735 }, 736 737 /** 738 * @param {number} lineNumber 739 */ 740 scrollChanged: function(lineNumber) 741 { 742 this.dispatchEventToListeners(WebInspector.SourceFrame.Events.ScrollChanged, lineNumber); 743 }, 744 745 _handleKeyDown: function(e) 746 { 747 var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e); 748 var handler = this._shortcuts[shortcutKey]; 749 if (handler && handler()) 750 e.consume(true); 751 }, 752 753 __proto__: WebInspector.VBox.prototype 754 } 755 756 WebInspector.SourceFrame._iconClassPerLevel = {}; 757 WebInspector.SourceFrame._iconClassPerLevel[WebInspector.ConsoleMessage.MessageLevel.Error] = "error-icon-small"; 758 WebInspector.SourceFrame._iconClassPerLevel[WebInspector.ConsoleMessage.MessageLevel.Warning] = "warning-icon-small"; 759 760 WebInspector.SourceFrame._lineClassPerLevel = {}; 761 WebInspector.SourceFrame._lineClassPerLevel[WebInspector.ConsoleMessage.MessageLevel.Error] = "text-editor-line-with-error"; 762 WebInspector.SourceFrame._lineClassPerLevel[WebInspector.ConsoleMessage.MessageLevel.Warning] = "text-editor-line-with-warning"; 763 764 /** 765 * @constructor 766 * @param {!WebInspector.ConsoleMessage} consoleMessage 767 */ 768 WebInspector.SourceFrame.RowMessage = function(consoleMessage) 769 { 770 this._consoleMessage = consoleMessage; 771 this._repeatCount = 1; 772 this.element = document.createElementWithClass("div", "text-editor-row-message"); 773 this._icon = this.element.createChild("span", "text-editor-row-message-icon"); 774 this._icon.classList.add(WebInspector.SourceFrame._iconClassPerLevel[consoleMessage.level]); 775 this._repeatCountElement = this.element.createChild("span", "bubble-repeat-count hidden error"); 776 var linesContainer = this.element.createChild("div", "text-editor-row-message-lines"); 777 var lines = this._consoleMessage.messageText.split("\n"); 778 for (var i = 0; i < lines.length; ++i) { 779 var messageLine = linesContainer.createChild("div"); 780 messageLine.textContent = lines[i]; 781 } 782 } 783 784 WebInspector.SourceFrame.RowMessage.prototype = { 785 /** 786 * @return {!WebInspector.ConsoleMessage} 787 */ 788 consoleMessage: function() 789 { 790 return this._consoleMessage; 791 }, 792 793 /** 794 * @return {number} 795 */ 796 repeatCount: function() 797 { 798 return this._repeatCount; 799 }, 800 801 setRepeatCount: function(repeatCount) 802 { 803 if (this._repeatCount === repeatCount) 804 return; 805 this._repeatCount = repeatCount; 806 this._updateMessageRepeatCount(); 807 }, 808 809 _updateMessageRepeatCount: function() 810 { 811 this._repeatCountElement.textContent = this._repeatCount; 812 var showRepeatCount = this._repeatCount > 1; 813 this._repeatCountElement.classList.toggle("hidden", !showRepeatCount); 814 this._icon.classList.toggle("hidden", showRepeatCount); 815 } 816 } 817 818 /** 819 * @constructor 820 * @param {!WebInspector.SourceFrame} sourceFrame 821 * @param {!WebInspector.TextEditor} textEditor 822 * @param {number} lineNumber 823 */ 824 WebInspector.SourceFrame.RowMessageBucket = function(sourceFrame, textEditor, lineNumber) 825 { 826 this._sourceFrame = sourceFrame; 827 this._textEditor = textEditor; 828 this._lineHandle = textEditor.textEditorPositionHandle(lineNumber, 0); 829 this._decoration = document.createElementWithClass("div", "text-editor-line-decoration"); 830 this._decoration._messageBucket = this; 831 this._wave = this._decoration.createChild("div", "text-editor-line-decoration-wave"); 832 this._icon = this._wave.createChild("div", "text-editor-line-decoration-icon"); 833 834 this._textEditor.addDecoration(lineNumber, this._decoration); 835 836 this._messagesDescriptionElement = document.createElementWithClass("div", "text-editor-messages-description-container"); 837 /** @type {!Array.<!WebInspector.SourceFrame.RowMessage>} */ 838 this._messages = []; 839 840 this._updateDecorationPosition(); 841 842 this._level = null; 843 } 844 845 WebInspector.SourceFrame.RowMessageBucket.prototype = { 846 _updateDecorationPosition: function() 847 { 848 if (!this._sourceFrame._isEditorShowing()) 849 return; 850 var position = this._lineHandle.resolve(); 851 if (!position) 852 return; 853 var lineNumber = position.lineNumber; 854 var lineText = this._textEditor.line(lineNumber); 855 var lineIndent = WebInspector.TextUtils.lineIndent(lineText).length; 856 var base = this._textEditor.cursorPositionToCoordinates(lineNumber, 0); 857 var start = this._textEditor.cursorPositionToCoordinates(lineNumber, lineIndent); 858 var end = this._textEditor.cursorPositionToCoordinates(lineNumber, lineText.length); 859 /** @const */ 860 var codeMirrorLinesLeftPadding = 4; 861 this._wave.style.left = (start.x - base.x + codeMirrorLinesLeftPadding) + "px"; 862 this._wave.style.width = (end.x - start.x) + "px"; 863 }, 864 865 /** 866 * @return {!Element} 867 */ 868 messagesDescription: function() 869 { 870 this._messagesDescriptionElement.removeChildren(); 871 for (var i = 0; i < this._messages.length; ++i) { 872 this._messagesDescriptionElement.appendChild(this._messages[i].element); 873 } 874 return this._messagesDescriptionElement; 875 }, 876 877 detachFromEditor: function() 878 { 879 var position = this._lineHandle.resolve(); 880 if (!position) 881 return; 882 var lineNumber = position.lineNumber; 883 if (this._level) 884 this._textEditor.toggleLineClass(lineNumber, WebInspector.SourceFrame._lineClassPerLevel[this._level], false); 885 this._textEditor.removeDecoration(lineNumber, this._decoration); 886 }, 887 888 /** 889 * @return {number} 890 */ 891 uniqueMessagesCount: function() 892 { 893 return this._messages.length; 894 }, 895 896 /** 897 * @param {!WebInspector.ConsoleMessage} consoleMessage 898 */ 899 addMessage: function(consoleMessage) 900 { 901 for (var i = 0; i < this._messages.length; ++i) { 902 var message = this._messages[i]; 903 if (message.consoleMessage().isEqual(consoleMessage)) { 904 message.setRepeatCount(message.repeatCount() + 1); 905 return; 906 } 907 } 908 909 var rowMessage = new WebInspector.SourceFrame.RowMessage(consoleMessage); 910 this._messages.push(rowMessage); 911 this._updateBucketLevel(); 912 }, 913 914 /** 915 * @param {!WebInspector.ConsoleMessage} consoleMessage 916 */ 917 removeMessage: function(consoleMessage) 918 { 919 for (var i = 0; i < this._messages.length; ++i) { 920 var rowMessage = this._messages[i]; 921 if (!rowMessage.consoleMessage().isEqual(consoleMessage)) 922 continue; 923 rowMessage.setRepeatCount(rowMessage.repeatCount() - 1); 924 if (!rowMessage.repeatCount()) 925 this._messages.splice(i, 1); 926 this._updateBucketLevel(); 927 return; 928 } 929 }, 930 931 _updateBucketLevel: function() 932 { 933 if (!this._messages.length) 934 return; 935 var position = this._lineHandle.resolve(); 936 if (!position) 937 return; 938 939 var lineNumber = position.lineNumber; 940 var maxMessage = null; 941 for (var i = 0; i < this._messages.length; ++i) { 942 var message = this._messages[i].consoleMessage();; 943 if (!maxMessage || WebInspector.ConsoleMessage.messageLevelComparator(maxMessage, message) < 0) 944 maxMessage = message; 945 } 946 947 if (this._level) { 948 this._textEditor.toggleLineClass(lineNumber, WebInspector.SourceFrame._lineClassPerLevel[this._level], false); 949 this._icon.classList.toggle(WebInspector.SourceFrame._iconClassPerLevel[this._level], false); 950 } 951 this._level = maxMessage.level; 952 if (!this._level) 953 return; 954 this._textEditor.toggleLineClass(lineNumber, WebInspector.SourceFrame._lineClassPerLevel[this._level], true); 955 this._icon.classList.toggle(WebInspector.SourceFrame._iconClassPerLevel[this._level], true); 956 } 957 } 958 959 /** 960 * @implements {WebInspector.TextEditorDelegate} 961 * @constructor 962 */ 963 WebInspector.TextEditorDelegateForSourceFrame = function(sourceFrame) 964 { 965 this._sourceFrame = sourceFrame; 966 } 967 968 WebInspector.TextEditorDelegateForSourceFrame.prototype = { 969 onTextChanged: function(oldRange, newRange) 970 { 971 this._sourceFrame.onTextChanged(oldRange, newRange); 972 }, 973 974 /** 975 * @param {!WebInspector.TextRange} textRange 976 */ 977 selectionChanged: function(textRange) 978 { 979 this._sourceFrame.selectionChanged(textRange); 980 }, 981 982 /** 983 * @param {number} lineNumber 984 */ 985 scrollChanged: function(lineNumber) 986 { 987 this._sourceFrame.scrollChanged(lineNumber); 988 }, 989 990 editorFocused: function() 991 { 992 this._sourceFrame._editorFocused(); 993 }, 994 995 populateLineGutterContextMenu: function(contextMenu, lineNumber) 996 { 997 this._sourceFrame.populateLineGutterContextMenu(contextMenu, lineNumber); 998 }, 999 1000 populateTextAreaContextMenu: function(contextMenu, lineNumber) 1001 { 1002 this._sourceFrame.populateTextAreaContextMenu(contextMenu, lineNumber); 1003 }, 1004 1005 /** 1006 * @param {string} hrefValue 1007 * @param {boolean} isExternal 1008 * @return {!Element} 1009 */ 1010 createLink: function(hrefValue, isExternal) 1011 { 1012 var targetLocation = WebInspector.ParsedURL.completeURL(this._sourceFrame._url, hrefValue); 1013 return WebInspector.linkifyURLAsNode(targetLocation || hrefValue, hrefValue, undefined, isExternal); 1014 }, 1015 1016 /** 1017 * @param {?WebInspector.TextRange} from 1018 * @param {?WebInspector.TextRange} to 1019 */ 1020 onJumpToPosition: function(from, to) 1021 { 1022 this._sourceFrame.onJumpToPosition(from, to); 1023 } 1024 } 1025