1/* 2 * Copyright (C) 2011 Google Inc. All rights reserved. 3 * Copyright (C) 2010 Apple Inc. All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are 7 * met: 8 * 9 * * Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * * Redistributions in binary form must reproduce the above 12 * copyright notice, this list of conditions and the following disclaimer 13 * in the documentation and/or other materials provided with the 14 * distribution. 15 * * Neither the name of Google Inc. nor the names of its 16 * contributors may be used to endorse or promote products derived from 17 * this software without specific prior written permission. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32/** 33 * @constructor 34 * @extends {WebInspector.View} 35 * @implements {WebInspector.TextEditor} 36 * @param {?string} url 37 * @param {WebInspector.TextEditorDelegate} delegate 38 */ 39WebInspector.DefaultTextEditor = function(url, delegate) 40{ 41 WebInspector.View.call(this); 42 this._delegate = delegate; 43 this._url = url; 44 45 this.registerRequiredCSS("textEditor.css"); 46 47 this.element.className = "text-editor monospace"; 48 this.markAsLayoutBoundary(); 49 50 // Prevent middle-click pasting in the editor unless it is explicitly enabled for certain component. 51 this.element.addEventListener("mouseup", preventDefaultOnMouseUp.bind(this), false); 52 function preventDefaultOnMouseUp(event) 53 { 54 if (event.button === 1) 55 event.consume(true); 56 } 57 58 this._textModel = new WebInspector.TextEditorModel(); 59 this._textModel.addEventListener(WebInspector.TextEditorModel.Events.TextChanged, this._textChanged, this); 60 61 var syncScrollListener = this._syncScroll.bind(this); 62 var syncDecorationsForLineListener = this._syncDecorationsForLine.bind(this); 63 var syncLineHeightListener = this._syncLineHeight.bind(this); 64 this._mainPanel = new WebInspector.TextEditorMainPanel(this._delegate, this._textModel, url, syncScrollListener, syncDecorationsForLineListener); 65 this._gutterPanel = new WebInspector.TextEditorGutterPanel(this._textModel, syncDecorationsForLineListener, syncLineHeightListener); 66 67 this._mainPanel.element.addEventListener("scroll", this._handleScrollChanged.bind(this), false); 68 69 this._gutterPanel.element.addEventListener("mousedown", this._onMouseDown.bind(this), true); 70 71 // Explicitly enable middle-click pasting in the editor main panel. 72 this._mainPanel.element.addEventListener("mouseup", consumeMouseUp.bind(this), false); 73 function consumeMouseUp(event) 74 { 75 if (event.button === 1) 76 event.consume(false); 77 } 78 79 this.element.appendChild(this._mainPanel.element); 80 this.element.appendChild(this._gutterPanel.element); 81 82 // Forward mouse wheel events from the unscrollable gutter to the main panel. 83 function forwardWheelEvent(event) 84 { 85 var clone = document.createEvent("WheelEvent"); 86 clone.initWebKitWheelEvent(event.wheelDeltaX, event.wheelDeltaY, 87 event.view, 88 event.screenX, event.screenY, 89 event.clientX, event.clientY, 90 event.ctrlKey, event.altKey, event.shiftKey, event.metaKey); 91 this._mainPanel.element.dispatchEvent(clone); 92 } 93 this._gutterPanel.element.addEventListener("mousewheel", forwardWheelEvent.bind(this), false); 94 95 this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false); 96 this.element.addEventListener("contextmenu", this._contextMenu.bind(this), true); 97 98 this._wordMovementController = new WebInspector.DefaultTextEditor.WordMovementController(this, this._textModel); 99 this._registerShortcuts(); 100} 101 102/** 103 * @constructor 104 * @param {WebInspector.TextRange} range 105 * @param {string} text 106 */ 107WebInspector.DefaultTextEditor.EditInfo = function(range, text) 108{ 109 this.range = range; 110 this.text = text; 111} 112 113WebInspector.DefaultTextEditor.prototype = { 114 /** 115 * @return {boolean} 116 */ 117 isClean: function() 118 { 119 return this._textModel.isClean(); 120 }, 121 122 markClean: function() 123 { 124 this._textModel.markClean(); 125 }, 126 /** 127 * @param {number} lineNumber 128 * @param {number} column 129 * @return {?{startColumn: number, endColumn: number, type: string}} 130 */ 131 tokenAtTextPosition: function(lineNumber, column) 132 { 133 return this._mainPanel.tokenAtTextPosition(lineNumber, column); 134 }, 135 136 /* 137 * @param {number} lineNumber 138 * @param {number} column 139 * @return {?{x: number, y: number, height: number}} 140 */ 141 cursorPositionToCoordinates: function(lineNumber, column) 142 { 143 return this._mainPanel.cursorPositionToCoordinates(lineNumber, column); 144 }, 145 146 /** 147 * @param {number} x 148 * @param {number} y 149 * @return {?WebInspector.TextRange} 150 */ 151 coordinatesToCursorPosition: function(x, y) 152 { 153 return this._mainPanel.coordinatesToCursorPosition(x, y); 154 }, 155 156 /** 157 * @param {WebInspector.TextRange} range 158 * @return {string} 159 */ 160 copyRange: function(range) 161 { 162 return this._textModel.copyRange(range); 163 }, 164 165 /** 166 * @param {string} regex 167 * @param {string} cssClass 168 * @return {Object} 169 */ 170 highlightRegex: function(regex, cssClass) 171 { 172 return this._mainPanel.highlightRegex(regex, cssClass); 173 }, 174 175 /** 176 * @param {Object} highlightDescriptor 177 */ 178 removeHighlight: function(highlightDescriptor) 179 { 180 this._mainPanel.removeHighlight(highlightDescriptor); 181 }, 182 183 /** 184 * @param {WebInspector.TextRange} range 185 * @param {string} cssClass 186 * @return {Object} 187 */ 188 highlightRange: function(range, cssClass) 189 { 190 return this._mainPanel.highlightRange(range, cssClass); 191 }, 192 193 /** 194 * @param {string} mimeType 195 */ 196 set mimeType(mimeType) 197 { 198 this._mainPanel.mimeType = mimeType; 199 }, 200 201 /** 202 * @param {boolean} readOnly 203 */ 204 setReadOnly: function(readOnly) 205 { 206 if (this._mainPanel.readOnly() === readOnly) 207 return; 208 this._mainPanel.setReadOnly(readOnly, this.isShowing()); 209 WebInspector.markBeingEdited(this.element, !readOnly); 210 }, 211 212 /** 213 * @return {boolean} 214 */ 215 readOnly: function() 216 { 217 return this._mainPanel.readOnly(); 218 }, 219 220 /** 221 * @return {Element} 222 */ 223 defaultFocusedElement: function() 224 { 225 return this._mainPanel.defaultFocusedElement(); 226 }, 227 228 /** 229 * @param {number} lineNumber 230 */ 231 revealLine: function(lineNumber) 232 { 233 this._mainPanel.revealLine(lineNumber); 234 }, 235 236 _onMouseDown: function(event) 237 { 238 var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number"); 239 if (!target) 240 return; 241 this.dispatchEventToListeners(WebInspector.TextEditor.Events.GutterClick, { lineNumber: target.lineNumber, event: event }); 242 }, 243 244 /** 245 * @param {number} lineNumber 246 * @param {boolean} disabled 247 * @param {boolean} conditional 248 */ 249 addBreakpoint: function(lineNumber, disabled, conditional) 250 { 251 this.beginUpdates(); 252 this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint"); 253 if (disabled) 254 this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint-disabled"); 255 else 256 this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-disabled"); 257 if (conditional) 258 this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint-conditional"); 259 else 260 this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-conditional"); 261 this.endUpdates(); 262 }, 263 264 /** 265 * @param {number} lineNumber 266 */ 267 removeBreakpoint: function(lineNumber) 268 { 269 this.beginUpdates(); 270 this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint"); 271 this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-disabled"); 272 this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-conditional"); 273 this.endUpdates(); 274 }, 275 276 /** 277 * @param {number} lineNumber 278 */ 279 setExecutionLine: function(lineNumber) 280 { 281 this._executionLineNumber = lineNumber; 282 this._mainPanel.addDecoration(lineNumber, "webkit-execution-line"); 283 this._gutterPanel.addDecoration(lineNumber, "webkit-execution-line"); 284 }, 285 286 clearExecutionLine: function() 287 { 288 if (typeof this._executionLineNumber === "number") { 289 this._mainPanel.removeDecoration(this._executionLineNumber, "webkit-execution-line"); 290 this._gutterPanel.removeDecoration(this._executionLineNumber, "webkit-execution-line"); 291 } 292 delete this._executionLineNumber; 293 }, 294 295 /** 296 * @param {number} lineNumber 297 * @param {Element} element 298 */ 299 addDecoration: function(lineNumber, element) 300 { 301 this._mainPanel.addDecoration(lineNumber, element); 302 this._gutterPanel.addDecoration(lineNumber, element); 303 this._syncDecorationsForLine(lineNumber); 304 }, 305 306 /** 307 * @param {number} lineNumber 308 * @param {Element} element 309 */ 310 removeDecoration: function(lineNumber, element) 311 { 312 this._mainPanel.removeDecoration(lineNumber, element); 313 this._gutterPanel.removeDecoration(lineNumber, element); 314 this._syncDecorationsForLine(lineNumber); 315 }, 316 317 /** 318 * @param {WebInspector.TextRange} range 319 */ 320 markAndRevealRange: function(range) 321 { 322 if (range) 323 this.setSelection(range); 324 this._mainPanel.markAndRevealRange(range); 325 }, 326 327 /** 328 * @param {number} lineNumber 329 */ 330 highlightLine: function(lineNumber) 331 { 332 if (typeof lineNumber !== "number" || lineNumber < 0) 333 return; 334 335 lineNumber = Math.min(lineNumber, this._textModel.linesCount - 1); 336 this._mainPanel.highlightLine(lineNumber); 337 }, 338 339 clearLineHighlight: function() 340 { 341 this._mainPanel.clearLineHighlight(); 342 }, 343 344 /** 345 * @return {Array.<Element>} 346 */ 347 elementsToRestoreScrollPositionsFor: function() 348 { 349 return [this._mainPanel.element]; 350 }, 351 352 /** 353 * @param {WebInspector.TextEditor} textEditor 354 */ 355 inheritScrollPositions: function(textEditor) 356 { 357 this._mainPanel.element._scrollTop = textEditor._mainPanel.element.scrollTop; 358 this._mainPanel.element._scrollLeft = textEditor._mainPanel.element.scrollLeft; 359 }, 360 361 beginUpdates: function() 362 { 363 this._mainPanel.beginUpdates(); 364 this._gutterPanel.beginUpdates(); 365 }, 366 367 endUpdates: function() 368 { 369 this._mainPanel.endUpdates(); 370 this._gutterPanel.endUpdates(); 371 this._updatePanelOffsets(); 372 }, 373 374 onResize: function() 375 { 376 this._mainPanel.resize(); 377 this._gutterPanel.resize(); 378 this._updatePanelOffsets(); 379 }, 380 381 _textChanged: function(event) 382 { 383 this._mainPanel.textChanged(event.data.oldRange, event.data.newRange); 384 this._gutterPanel.textChanged(event.data.oldRange, event.data.newRange); 385 this._updatePanelOffsets(); 386 if (event.data.editRange) 387 this._delegate.onTextChanged(event.data.oldRange, event.data.newRange); 388 }, 389 390 /** 391 * @param {WebInspector.TextRange} range 392 * @param {string} text 393 * @return {WebInspector.TextRange} 394 */ 395 editRange: function(range, text) 396 { 397 return this._textModel.editRange(range, text, this.lastSelection()); 398 }, 399 400 _updatePanelOffsets: function() 401 { 402 var lineNumbersWidth = this._gutterPanel.element.offsetWidth; 403 if (lineNumbersWidth) 404 this._mainPanel.element.style.setProperty("left", (lineNumbersWidth + 2) + "px"); 405 else 406 this._mainPanel.element.style.removeProperty("left"); // Use default value set in CSS. 407 }, 408 409 _syncScroll: function() 410 { 411 var mainElement = this._mainPanel.element; 412 var gutterElement = this._gutterPanel.element; 413 // Handle horizontal scroll bar at the bottom of the main panel. 414 this._gutterPanel.syncClientHeight(mainElement.clientHeight); 415 gutterElement.scrollTop = mainElement.scrollTop; 416 }, 417 418 /** 419 * @param {number} lineNumber 420 */ 421 _syncDecorationsForLine: function(lineNumber) 422 { 423 if (lineNumber >= this._textModel.linesCount) 424 return; 425 426 var mainChunk = this._mainPanel.chunkForLine(lineNumber); 427 if (mainChunk.linesCount === 1 && mainChunk.isDecorated()) { 428 var gutterChunk = this._gutterPanel.makeLineAChunk(lineNumber); 429 var height = mainChunk.height; 430 if (height) 431 gutterChunk.element.style.setProperty("height", height + "px"); 432 else 433 gutterChunk.element.style.removeProperty("height"); 434 } else { 435 var gutterChunk = this._gutterPanel.chunkForLine(lineNumber); 436 if (gutterChunk.linesCount === 1) 437 gutterChunk.element.style.removeProperty("height"); 438 } 439 }, 440 441 /** 442 * @param {Element} gutterRow 443 */ 444 _syncLineHeight: function(gutterRow) 445 { 446 if (this._lineHeightSynced) 447 return; 448 if (gutterRow && gutterRow.offsetHeight) { 449 // Force equal line heights for the child panels. 450 this.element.style.setProperty("line-height", gutterRow.offsetHeight + "px"); 451 this._lineHeightSynced = true; 452 } 453 }, 454 455 _registerShortcuts: function() 456 { 457 var keys = WebInspector.KeyboardShortcut.Keys; 458 var modifiers = WebInspector.KeyboardShortcut.Modifiers; 459 460 this._shortcuts = {}; 461 462 this._shortcuts[WebInspector.KeyboardShortcut.SelectAll] = this._handleSelectAll.bind(this); 463 this._wordMovementController._registerShortcuts(this._shortcuts); 464 }, 465 466 _handleSelectAll: function() 467 { 468 this.setSelection(this._textModel.range()); 469 return true; 470 }, 471 472 _handleKeyDown: function(e) 473 { 474 // If the event was not triggered from the entire editor, then 475 // ignore it. https://bugs.webkit.org/show_bug.cgi?id=102906 476 if (e.target.enclosingNodeOrSelfWithClass("webkit-line-decorations")) 477 return; 478 479 var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e); 480 481 var handler = this._shortcuts[shortcutKey]; 482 if (handler && handler()) { 483 e.consume(true); 484 return; 485 } 486 this._mainPanel.handleKeyDown(shortcutKey, e); 487 }, 488 489 _contextMenu: function(event) 490 { 491 var anchor = event.target.enclosingNodeOrSelfWithNodeName("a"); 492 if (anchor) 493 return; 494 var contextMenu = new WebInspector.ContextMenu(event); 495 var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number"); 496 if (target) 497 this._delegate.populateLineGutterContextMenu(contextMenu, target.lineNumber); 498 else { 499 this._mainPanel.populateContextMenu(event.target, contextMenu); 500 } 501 contextMenu.show(); 502 }, 503 504 _handleScrollChanged: function(event) 505 { 506 var visibleFrom = this._mainPanel.scrollTop(); 507 var firstVisibleLineNumber = this._mainPanel.lineNumberAtOffset(visibleFrom); 508 this._delegate.scrollChanged(firstVisibleLineNumber); 509 }, 510 511 /** 512 * @param {number} lineNumber 513 */ 514 scrollToLine: function(lineNumber) 515 { 516 this._mainPanel.scrollToLine(lineNumber); 517 }, 518 519 /** 520 * @return {WebInspector.TextRange} 521 */ 522 selection: function() 523 { 524 return this._mainPanel.selection(); 525 }, 526 527 /** 528 * @return {WebInspector.TextRange?} 529 */ 530 lastSelection: function() 531 { 532 return this._mainPanel.lastSelection(); 533 }, 534 535 /** 536 * @param {WebInspector.TextRange} textRange 537 */ 538 setSelection: function(textRange) 539 { 540 this._mainPanel.setSelection(textRange); 541 }, 542 543 /** 544 * @param {string} text 545 */ 546 setText: function(text) 547 { 548 this._textModel.setText(text); 549 }, 550 551 /** 552 * @return {string} 553 */ 554 text: function() 555 { 556 return this._textModel.text(); 557 }, 558 559 /** 560 * @return {WebInspector.TextRange} 561 */ 562 range: function() 563 { 564 return this._textModel.range(); 565 }, 566 567 /** 568 * @param {number} lineNumber 569 * @return {string} 570 */ 571 line: function(lineNumber) 572 { 573 return this._textModel.line(lineNumber); 574 }, 575 576 /** 577 * @return {number} 578 */ 579 get linesCount() 580 { 581 return this._textModel.linesCount; 582 }, 583 584 /** 585 * @param {number} line 586 * @param {string} name 587 * @param {?Object} value 588 */ 589 setAttribute: function(line, name, value) 590 { 591 this._textModel.setAttribute(line, name, value); 592 }, 593 594 /** 595 * @param {number} line 596 * @param {string} name 597 * @return {?Object} value 598 */ 599 getAttribute: function(line, name) 600 { 601 return this._textModel.getAttribute(line, name); 602 }, 603 604 /** 605 * @param {number} line 606 * @param {string} name 607 */ 608 removeAttribute: function(line, name) 609 { 610 this._textModel.removeAttribute(line, name); 611 }, 612 613 wasShown: function() 614 { 615 if (!this.readOnly()) 616 WebInspector.markBeingEdited(this.element, true); 617 618 this._mainPanel.wasShown(); 619 }, 620 621 willHide: function() 622 { 623 this._mainPanel.willHide(); 624 this._gutterPanel.willHide(); 625 626 if (!this.readOnly()) 627 WebInspector.markBeingEdited(this.element, false); 628 }, 629 630 /** 631 * @param {Element} element 632 * @param {Array.<Object>} resultRanges 633 * @param {string} styleClass 634 * @param {Array.<Object>=} changes 635 */ 636 highlightRangesWithStyleClass: function(element, resultRanges, styleClass, changes) 637 { 638 this._mainPanel.beginDomUpdates(); 639 WebInspector.highlightRangesWithStyleClass(element, resultRanges, styleClass, changes); 640 this._mainPanel.endDomUpdates(); 641 }, 642 643 /** 644 * @param {number} scrollTop 645 * @param {number} clientHeight 646 * @param {number} chunkSize 647 */ 648 overrideViewportForTest: function(scrollTop, clientHeight, chunkSize) 649 { 650 this._mainPanel.overrideViewportForTest(scrollTop, clientHeight, chunkSize); 651 }, 652 653 __proto__: WebInspector.View.prototype 654} 655 656/** 657 * @constructor 658 * @param {WebInspector.TextEditorModel} textModel 659 */ 660WebInspector.TextEditorChunkedPanel = function(textModel) 661{ 662 this._textModel = textModel; 663 664 this.element = document.createElement("div"); 665 this.element.addEventListener("scroll", this._scroll.bind(this), false); 666 667 this._defaultChunkSize = 50; 668 this._paintCoalescingLevel = 0; 669 this._domUpdateCoalescingLevel = 0; 670} 671 672WebInspector.TextEditorChunkedPanel.prototype = { 673 /** 674 * @param {number} lineNumber 675 */ 676 scrollToLine: function(lineNumber) 677 { 678 if (lineNumber >= this._textModel.linesCount) 679 return; 680 681 var chunk = this.makeLineAChunk(lineNumber); 682 this.element.scrollTop = chunk.offsetTop; 683 }, 684 685 /** 686 * @param {number} lineNumber 687 */ 688 revealLine: function(lineNumber) 689 { 690 if (lineNumber >= this._textModel.linesCount) 691 return; 692 693 var chunk = this.makeLineAChunk(lineNumber); 694 chunk.element.scrollIntoViewIfNeeded(); 695 }, 696 697 /** 698 * @param {number} lineNumber 699 * @param {string|Element} decoration 700 */ 701 addDecoration: function(lineNumber, decoration) 702 { 703 if (lineNumber >= this._textModel.linesCount) 704 return; 705 706 var chunk = this.makeLineAChunk(lineNumber); 707 chunk.addDecoration(decoration); 708 }, 709 710 /** 711 * @param {number} lineNumber 712 * @param {string|Element} decoration 713 */ 714 removeDecoration: function(lineNumber, decoration) 715 { 716 if (lineNumber >= this._textModel.linesCount) 717 return; 718 719 var chunk = this.chunkForLine(lineNumber); 720 chunk.removeDecoration(decoration); 721 }, 722 723 buildChunks: function() 724 { 725 this.beginDomUpdates(); 726 727 this._container.removeChildren(); 728 729 this._textChunks = []; 730 for (var i = 0; i < this._textModel.linesCount; i += this._defaultChunkSize) { 731 var chunk = this.createNewChunk(i, i + this._defaultChunkSize); 732 this._textChunks.push(chunk); 733 this._container.appendChild(chunk.element); 734 } 735 736 this.repaintAll(); 737 738 this.endDomUpdates(); 739 }, 740 741 /** 742 * @param {number} lineNumber 743 * @return {Object} 744 */ 745 makeLineAChunk: function(lineNumber) 746 { 747 var chunkNumber = this.chunkNumberForLine(lineNumber); 748 var oldChunk = this._textChunks[chunkNumber]; 749 750 if (!oldChunk) { 751 console.error("No chunk for line number: " + lineNumber); 752 return null; 753 } 754 755 if (oldChunk.linesCount === 1) 756 return oldChunk; 757 758 return this.splitChunkOnALine(lineNumber, chunkNumber, true); 759 }, 760 761 /** 762 * @param {number} lineNumber 763 * @param {number} chunkNumber 764 * @param {boolean=} createSuffixChunk 765 * @return {Object} 766 */ 767 splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk) 768 { 769 this.beginDomUpdates(); 770 771 var oldChunk = this._textChunks[chunkNumber]; 772 var wasExpanded = oldChunk.expanded(); 773 oldChunk.collapse(); 774 775 var insertIndex = chunkNumber + 1; 776 777 // Prefix chunk. 778 if (lineNumber > oldChunk.startLine) { 779 var prefixChunk = this.createNewChunk(oldChunk.startLine, lineNumber); 780 this._textChunks.splice(insertIndex++, 0, prefixChunk); 781 this._container.insertBefore(prefixChunk.element, oldChunk.element); 782 } 783 784 // Line chunk. 785 var endLine = createSuffixChunk ? lineNumber + 1 : oldChunk.startLine + oldChunk.linesCount; 786 var lineChunk = this.createNewChunk(lineNumber, endLine); 787 this._textChunks.splice(insertIndex++, 0, lineChunk); 788 this._container.insertBefore(lineChunk.element, oldChunk.element); 789 790 // Suffix chunk. 791 if (oldChunk.startLine + oldChunk.linesCount > endLine) { 792 var suffixChunk = this.createNewChunk(endLine, oldChunk.startLine + oldChunk.linesCount); 793 this._textChunks.splice(insertIndex, 0, suffixChunk); 794 this._container.insertBefore(suffixChunk.element, oldChunk.element); 795 } 796 797 // Remove enclosing chunk. 798 this._textChunks.splice(chunkNumber, 1); 799 this._container.removeChild(oldChunk.element); 800 801 if (wasExpanded) { 802 if (prefixChunk) 803 prefixChunk.expand(); 804 lineChunk.expand(); 805 if (suffixChunk) 806 suffixChunk.expand(); 807 } 808 809 this.endDomUpdates(); 810 811 return lineChunk; 812 }, 813 814 createNewChunk: function(startLine, endLine) 815 { 816 throw new Error("createNewChunk() should be implemented by descendants"); 817 }, 818 819 _scroll: function() 820 { 821 this._scheduleRepaintAll(); 822 if (this._syncScrollListener) 823 this._syncScrollListener(); 824 }, 825 826 _scheduleRepaintAll: function() 827 { 828 if (this._repaintAllTimer) 829 clearTimeout(this._repaintAllTimer); 830 this._repaintAllTimer = setTimeout(this.repaintAll.bind(this), 50); 831 }, 832 833 beginUpdates: function() 834 { 835 this._paintCoalescingLevel++; 836 }, 837 838 endUpdates: function() 839 { 840 this._paintCoalescingLevel--; 841 if (!this._paintCoalescingLevel) 842 this.repaintAll(); 843 }, 844 845 beginDomUpdates: function() 846 { 847 this._domUpdateCoalescingLevel++; 848 }, 849 850 endDomUpdates: function() 851 { 852 this._domUpdateCoalescingLevel--; 853 }, 854 855 /** 856 * @param {number} lineNumber 857 * @return {number} 858 */ 859 chunkNumberForLine: function(lineNumber) 860 { 861 function compareLineNumbers(value, chunk) 862 { 863 return value < chunk.startLine ? -1 : 1; 864 } 865 var insertBefore = insertionIndexForObjectInListSortedByFunction(lineNumber, this._textChunks, compareLineNumbers); 866 return insertBefore - 1; 867 }, 868 869 /** 870 * @param {number} lineNumber 871 * @return {Object} 872 */ 873 chunkForLine: function(lineNumber) 874 { 875 return this._textChunks[this.chunkNumberForLine(lineNumber)]; 876 }, 877 878 /** 879 * @param {number} visibleFrom 880 * @return {number} 881 */ 882 _findFirstVisibleChunkNumber: function(visibleFrom) 883 { 884 function compareOffsetTops(value, chunk) 885 { 886 return value < chunk.offsetTop ? -1 : 1; 887 } 888 var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, this._textChunks, compareOffsetTops); 889 return insertBefore - 1; 890 }, 891 892 /** 893 * @param {number} visibleFrom 894 * @param {number} visibleTo 895 * @return {{start: number, end: number}} 896 */ 897 findVisibleChunks: function(visibleFrom, visibleTo) 898 { 899 var span = (visibleTo - visibleFrom) * 0.5; 900 visibleFrom = Math.max(visibleFrom - span, 0); 901 visibleTo = visibleTo + span; 902 903 var from = this._findFirstVisibleChunkNumber(visibleFrom); 904 for (var to = from + 1; to < this._textChunks.length; ++to) { 905 if (this._textChunks[to].offsetTop >= visibleTo) 906 break; 907 } 908 return { start: from, end: to }; 909 }, 910 911 /** 912 * @param {number} visibleFrom 913 * @return {number} 914 */ 915 lineNumberAtOffset: function(visibleFrom) 916 { 917 var chunk = this._textChunks[this._findFirstVisibleChunkNumber(visibleFrom)]; 918 if (!chunk.expanded()) 919 return chunk.startLine; 920 921 var lineNumbers = []; 922 for (var i = 0; i < chunk.linesCount; ++i) { 923 lineNumbers.push(chunk.startLine + i); 924 } 925 926 function compareLineRowOffsetTops(value, lineNumber) 927 { 928 var lineRow = chunk.expandedLineRow(lineNumber); 929 return value < lineRow.offsetTop ? -1 : 1; 930 } 931 var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, lineNumbers, compareLineRowOffsetTops); 932 return lineNumbers[insertBefore - 1]; 933 }, 934 935 repaintAll: function() 936 { 937 delete this._repaintAllTimer; 938 939 if (this._paintCoalescingLevel) 940 return; 941 942 var visibleFrom = this.scrollTop(); 943 var visibleTo = visibleFrom + this.clientHeight(); 944 945 if (visibleTo) { 946 var result = this.findVisibleChunks(visibleFrom, visibleTo); 947 this.expandChunks(result.start, result.end); 948 } 949 }, 950 951 scrollTop: function() 952 { 953 return typeof this._scrollTopOverrideForTest === "number" ? this._scrollTopOverrideForTest : this.element.scrollTop; 954 }, 955 956 clientHeight: function() 957 { 958 return typeof this._clientHeightOverrideForTest === "number" ? this._clientHeightOverrideForTest : this.element.clientHeight; 959 }, 960 961 /** 962 * @param {number} fromIndex 963 * @param {number} toIndex 964 */ 965 expandChunks: function(fromIndex, toIndex) 966 { 967 // First collapse chunks to collect the DOM elements into a cache to reuse them later. 968 for (var i = 0; i < fromIndex; ++i) 969 this._textChunks[i].collapse(); 970 for (var i = toIndex; i < this._textChunks.length; ++i) 971 this._textChunks[i].collapse(); 972 for (var i = fromIndex; i < toIndex; ++i) 973 this._textChunks[i].expand(); 974 }, 975 976 /** 977 * @param {Element} firstElement 978 * @param {Element=} lastElement 979 * @return {number} 980 */ 981 totalHeight: function(firstElement, lastElement) 982 { 983 lastElement = (lastElement || firstElement).nextElementSibling; 984 if (lastElement) 985 return lastElement.offsetTop - firstElement.offsetTop; 986 987 var offsetParent = firstElement.offsetParent; 988 if (offsetParent && offsetParent.scrollHeight > offsetParent.clientHeight) 989 return offsetParent.scrollHeight - firstElement.offsetTop; 990 991 var total = 0; 992 while (firstElement && firstElement !== lastElement) { 993 total += firstElement.offsetHeight; 994 firstElement = firstElement.nextElementSibling; 995 } 996 return total; 997 }, 998 999 resize: function() 1000 { 1001 this.repaintAll(); 1002 } 1003} 1004 1005/** 1006 * @constructor 1007 * @extends {WebInspector.TextEditorChunkedPanel} 1008 * @param {WebInspector.TextEditorModel} textModel 1009 * @param {function(number)} syncDecorationsForLineListener 1010 * @param {function(Element)} syncLineHeightListener 1011 */ 1012WebInspector.TextEditorGutterPanel = function(textModel, syncDecorationsForLineListener, syncLineHeightListener) 1013{ 1014 WebInspector.TextEditorChunkedPanel.call(this, textModel); 1015 1016 this._syncDecorationsForLineListener = syncDecorationsForLineListener; 1017 this._syncLineHeightListener = syncLineHeightListener; 1018 1019 this.element.className = "text-editor-lines"; 1020 1021 this._container = document.createElement("div"); 1022 this._container.className = "inner-container"; 1023 this.element.appendChild(this._container); 1024 1025 this._freeCachedElements(); 1026 this.buildChunks(); 1027 this._decorations = {}; 1028} 1029 1030WebInspector.TextEditorGutterPanel.prototype = { 1031 _freeCachedElements: function() 1032 { 1033 this._cachedRows = []; 1034 }, 1035 1036 willHide: function() 1037 { 1038 this._freeCachedElements(); 1039 }, 1040 1041 /** 1042 * @param {number} startLine 1043 * @param {number} endLine 1044 * @return {WebInspector.TextEditorGutterChunk} 1045 */ 1046 createNewChunk: function(startLine, endLine) 1047 { 1048 return new WebInspector.TextEditorGutterChunk(this, startLine, endLine); 1049 }, 1050 1051 /** 1052 * @param {WebInspector.TextRange} oldRange 1053 * @param {WebInspector.TextRange} newRange 1054 */ 1055 textChanged: function(oldRange, newRange) 1056 { 1057 this.beginDomUpdates(); 1058 1059 var linesDiff = newRange.linesCount - oldRange.linesCount; 1060 if (linesDiff) { 1061 // Remove old chunks (if needed). 1062 for (var chunkNumber = this._textChunks.length - 1; chunkNumber >= 0; --chunkNumber) { 1063 var chunk = this._textChunks[chunkNumber]; 1064 if (chunk.startLine + chunk.linesCount <= this._textModel.linesCount) 1065 break; 1066 chunk.collapse(); 1067 this._container.removeChild(chunk.element); 1068 } 1069 this._textChunks.length = chunkNumber + 1; 1070 1071 // Add new chunks (if needed). 1072 var totalLines = 0; 1073 if (this._textChunks.length) { 1074 var lastChunk = this._textChunks[this._textChunks.length - 1]; 1075 totalLines = lastChunk.startLine + lastChunk.linesCount; 1076 } 1077 1078 for (var i = totalLines; i < this._textModel.linesCount; i += this._defaultChunkSize) { 1079 var chunk = this.createNewChunk(i, i + this._defaultChunkSize); 1080 this._textChunks.push(chunk); 1081 this._container.appendChild(chunk.element); 1082 } 1083 1084 // Shift decorations if necessary 1085 var decorationsToRestore = {}; 1086 for (var lineNumber in this._decorations) { 1087 lineNumber = parseInt(lineNumber, 10); 1088 1089 // Do not move decorations before the start position. 1090 if (lineNumber < oldRange.startLine) 1091 continue; 1092 // Decorations follow the first character of line. 1093 if (lineNumber === oldRange.startLine && oldRange.startColumn) 1094 continue; 1095 1096 var lineDecorationsCopy = this._decorations[lineNumber].slice(); 1097 for (var i = 0; i < lineDecorationsCopy.length; ++i) 1098 this.removeDecoration(lineNumber, lineDecorationsCopy[i]); 1099 // Do not restore the decorations before the end position. 1100 if (lineNumber >= oldRange.endLine) 1101 decorationsToRestore[lineNumber] = lineDecorationsCopy; 1102 } 1103 for (var lineNumber in decorationsToRestore) { 1104 lineNumber = parseInt(lineNumber, 10); 1105 var lineDecorationsCopy = decorationsToRestore[lineNumber]; 1106 for (var i = 0; i < lineDecorationsCopy.length; ++i) 1107 this.addDecoration(lineNumber + linesDiff, lineDecorationsCopy[i]); 1108 } 1109 1110 1111 this.repaintAll(); 1112 } else { 1113 // Decorations may have been removed, so we may have to sync those lines. 1114 var chunkNumber = this.chunkNumberForLine(newRange.startLine); 1115 var chunk = this._textChunks[chunkNumber]; 1116 while (chunk && chunk.startLine <= newRange.endLine) { 1117 if (chunk.linesCount === 1) 1118 this._syncDecorationsForLineListener(chunk.startLine); 1119 chunk = this._textChunks[++chunkNumber]; 1120 } 1121 } 1122 1123 this.endDomUpdates(); 1124 }, 1125 1126 /** 1127 * @param {number} clientHeight 1128 */ 1129 syncClientHeight: function(clientHeight) 1130 { 1131 if (this.element.offsetHeight > clientHeight) 1132 this._container.style.setProperty("padding-bottom", (this.element.offsetHeight - clientHeight) + "px"); 1133 else 1134 this._container.style.removeProperty("padding-bottom"); 1135 }, 1136 1137 /** 1138 * @param {number} lineNumber 1139 * @param {string|Element} decoration 1140 */ 1141 addDecoration: function(lineNumber, decoration) 1142 { 1143 WebInspector.TextEditorChunkedPanel.prototype.addDecoration.call(this, lineNumber, decoration); 1144 var decorations = this._decorations[lineNumber]; 1145 if (!decorations) { 1146 decorations = []; 1147 this._decorations[lineNumber] = decorations; 1148 } 1149 decorations.push(decoration); 1150 }, 1151 1152 /** 1153 * @param {number} lineNumber 1154 * @param {string|Element} decoration 1155 */ 1156 removeDecoration: function(lineNumber, decoration) 1157 { 1158 WebInspector.TextEditorChunkedPanel.prototype.removeDecoration.call(this, lineNumber, decoration); 1159 var decorations = this._decorations[lineNumber]; 1160 if (decorations) { 1161 decorations.remove(decoration); 1162 if (!decorations.length) 1163 delete this._decorations[lineNumber]; 1164 } 1165 }, 1166 1167 __proto__: WebInspector.TextEditorChunkedPanel.prototype 1168} 1169 1170/** 1171 * @constructor 1172 * @param {WebInspector.TextEditorGutterPanel} chunkedPanel 1173 * @param {number} startLine 1174 * @param {number} endLine 1175 */ 1176WebInspector.TextEditorGutterChunk = function(chunkedPanel, startLine, endLine) 1177{ 1178 this._chunkedPanel = chunkedPanel; 1179 this._textModel = chunkedPanel._textModel; 1180 1181 this.startLine = startLine; 1182 endLine = Math.min(this._textModel.linesCount, endLine); 1183 this.linesCount = endLine - startLine; 1184 1185 this._expanded = false; 1186 1187 this.element = document.createElement("div"); 1188 this.element.lineNumber = startLine; 1189 this.element.className = "webkit-line-number"; 1190 1191 if (this.linesCount === 1) { 1192 // Single line chunks are typically created for decorations. Host line number in 1193 // the sub-element in order to allow flexible border / margin management. 1194 var innerSpan = document.createElement("span"); 1195 innerSpan.className = "webkit-line-number-inner"; 1196 innerSpan.textContent = startLine + 1; 1197 var outerSpan = document.createElement("div"); 1198 outerSpan.className = "webkit-line-number-outer"; 1199 outerSpan.appendChild(innerSpan); 1200 this.element.appendChild(outerSpan); 1201 } else { 1202 var lineNumbers = []; 1203 for (var i = startLine; i < endLine; ++i) 1204 lineNumbers.push(i + 1); 1205 this.element.textContent = lineNumbers.join("\n"); 1206 } 1207} 1208 1209WebInspector.TextEditorGutterChunk.prototype = { 1210 /** 1211 * @param {string} decoration 1212 */ 1213 addDecoration: function(decoration) 1214 { 1215 this._chunkedPanel.beginDomUpdates(); 1216 if (typeof decoration === "string") 1217 this.element.addStyleClass(decoration); 1218 this._chunkedPanel.endDomUpdates(); 1219 }, 1220 1221 /** 1222 * @param {string} decoration 1223 */ 1224 removeDecoration: function(decoration) 1225 { 1226 this._chunkedPanel.beginDomUpdates(); 1227 if (typeof decoration === "string") 1228 this.element.removeStyleClass(decoration); 1229 this._chunkedPanel.endDomUpdates(); 1230 }, 1231 1232 /** 1233 * @return {boolean} 1234 */ 1235 expanded: function() 1236 { 1237 return this._expanded; 1238 }, 1239 1240 expand: function() 1241 { 1242 if (this.linesCount === 1) 1243 this._chunkedPanel._syncDecorationsForLineListener(this.startLine); 1244 1245 if (this._expanded) 1246 return; 1247 1248 this._expanded = true; 1249 1250 if (this.linesCount === 1) 1251 return; 1252 1253 this._chunkedPanel.beginDomUpdates(); 1254 1255 this._expandedLineRows = []; 1256 var parentElement = this.element.parentElement; 1257 for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) { 1258 var lineRow = this._createRow(i); 1259 parentElement.insertBefore(lineRow, this.element); 1260 this._expandedLineRows.push(lineRow); 1261 } 1262 parentElement.removeChild(this.element); 1263 this._chunkedPanel._syncLineHeightListener(this._expandedLineRows[0]); 1264 1265 this._chunkedPanel.endDomUpdates(); 1266 }, 1267 1268 collapse: function() 1269 { 1270 if (this.linesCount === 1) 1271 this._chunkedPanel._syncDecorationsForLineListener(this.startLine); 1272 1273 if (!this._expanded) 1274 return; 1275 1276 this._expanded = false; 1277 1278 if (this.linesCount === 1) 1279 return; 1280 1281 this._chunkedPanel.beginDomUpdates(); 1282 1283 var elementInserted = false; 1284 for (var i = 0; i < this._expandedLineRows.length; ++i) { 1285 var lineRow = this._expandedLineRows[i]; 1286 var parentElement = lineRow.parentElement; 1287 if (parentElement) { 1288 if (!elementInserted) { 1289 elementInserted = true; 1290 parentElement.insertBefore(this.element, lineRow); 1291 } 1292 parentElement.removeChild(lineRow); 1293 } 1294 this._chunkedPanel._cachedRows.push(lineRow); 1295 } 1296 delete this._expandedLineRows; 1297 1298 this._chunkedPanel.endDomUpdates(); 1299 }, 1300 1301 /** 1302 * @return {number} 1303 */ 1304 get height() 1305 { 1306 if (!this._expandedLineRows) 1307 return this._chunkedPanel.totalHeight(this.element); 1308 return this._chunkedPanel.totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]); 1309 }, 1310 1311 /** 1312 * @return {number} 1313 */ 1314 get offsetTop() 1315 { 1316 return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop; 1317 }, 1318 1319 /** 1320 * @param {number} lineNumber 1321 * @return {Element} 1322 */ 1323 _createRow: function(lineNumber) 1324 { 1325 var lineRow = this._chunkedPanel._cachedRows.pop() || document.createElement("div"); 1326 lineRow.lineNumber = lineNumber; 1327 lineRow.className = "webkit-line-number"; 1328 lineRow.textContent = lineNumber + 1; 1329 return lineRow; 1330 } 1331} 1332 1333/** 1334 * @constructor 1335 * @extends {WebInspector.TextEditorChunkedPanel} 1336 * @param {WebInspector.TextEditorDelegate} delegate 1337 * @param {WebInspector.TextEditorModel} textModel 1338 * @param {?string} url 1339 * @param {function()} syncScrollListener 1340 * @param {function(number)} syncDecorationsForLineListener 1341 */ 1342WebInspector.TextEditorMainPanel = function(delegate, textModel, url, syncScrollListener, syncDecorationsForLineListener) 1343{ 1344 WebInspector.TextEditorChunkedPanel.call(this, textModel); 1345 1346 this._delegate = delegate; 1347 this._syncScrollListener = syncScrollListener; 1348 this._syncDecorationsForLineListener = syncDecorationsForLineListener; 1349 1350 this._url = url; 1351 this._highlighter = new WebInspector.TextEditorHighlighter(textModel, this._highlightDataReady.bind(this)); 1352 this._readOnly = true; 1353 1354 this.element.className = "text-editor-contents"; 1355 this.element.tabIndex = 0; 1356 1357 this._container = document.createElement("div"); 1358 this._container.className = "inner-container"; 1359 this._container.tabIndex = 0; 1360 this.element.appendChild(this._container); 1361 1362 this.element.addEventListener("focus", this._handleElementFocus.bind(this), false); 1363 this.element.addEventListener("textInput", this._handleTextInput.bind(this), false); 1364 this.element.addEventListener("cut", this._handleCut.bind(this), false); 1365 this.element.addEventListener("keypress", this._handleKeyPress.bind(this), false); 1366 1367 this._showWhitespace = WebInspector.experimentsSettings.showWhitespaceInEditor.isEnabled(); 1368 1369 this._container.addEventListener("focus", this._handleFocused.bind(this), false); 1370 1371 this._highlightDescriptors = []; 1372 1373 this._tokenHighlighter = new WebInspector.TextEditorMainPanel.TokenHighlighter(this, textModel); 1374 this._braceMatcher = new WebInspector.TextEditorModel.BraceMatcher(textModel); 1375 this._braceHighlighter = new WebInspector.TextEditorMainPanel.BraceHighlightController(this, textModel, this._braceMatcher); 1376 this._smartBraceController = new WebInspector.TextEditorMainPanel.SmartBraceController(this, textModel, this._braceMatcher); 1377 1378 this._freeCachedElements(); 1379 this.buildChunks(); 1380 this._registerShortcuts(); 1381} 1382 1383WebInspector.TextEditorMainPanel._ConsecutiveWhitespaceChars = { 1384 1: " ", 1385 2: " ", 1386 4: " ", 1387 8: " ", 1388 16: " " 1389}; 1390 1391WebInspector.TextEditorMainPanel.prototype = { 1392 /** 1393 * @param {number} lineNumber 1394 * @param {number} column 1395 * @return {?{startColumn: number, endColumn: number, type: string}} 1396 */ 1397 tokenAtTextPosition: function(lineNumber, column) 1398 { 1399 if (lineNumber >= this._textModel.linesCount || lineNumber < 0) 1400 return null; 1401 var line = this._textModel.line(lineNumber); 1402 if (column >= line.length || column < 0) 1403 return null; 1404 var highlight = this._textModel.getAttribute(lineNumber, "highlight"); 1405 if (!highlight) 1406 return this._tokenAtUnhighlightedLine(line, column); 1407 function compare(value, object) 1408 { 1409 if (value >= object.startColumn && value <= object.endColumn) 1410 return 0; 1411 return value - object.startColumn; 1412 } 1413 var index = binarySearch(column, highlight.ranges, compare); 1414 if (index >= 0) { 1415 var range = highlight.ranges[index]; 1416 return { 1417 startColumn: range.startColumn, 1418 endColumn: range.endColumn, 1419 type: range.token 1420 }; 1421 } 1422 return null; 1423 }, 1424 1425 /** 1426 * @param {number} lineNumber 1427 * @param {number} column 1428 * @return {?{x: number, y: number, height: number}} 1429 */ 1430 cursorPositionToCoordinates: function(lineNumber, column) 1431 { 1432 if (lineNumber >= this._textModel.linesCount || lineNumber < 0) 1433 return null; 1434 var line = this._textModel.line(lineNumber); 1435 if (column > line.length || column < 0) 1436 return null; 1437 1438 var chunk = this.chunkForLine(lineNumber); 1439 if (!chunk.expanded()) 1440 return null; 1441 var lineRow = chunk.expandedLineRow(lineNumber); 1442 var ranges = [{ 1443 startColumn: column, 1444 endColumn: column, 1445 token: "measure-cursor-position" 1446 }]; 1447 var selection = this.selection(); 1448 1449 this.beginDomUpdates(); 1450 this._renderRanges(lineRow, line, ranges); 1451 var spans = lineRow.getElementsByClassName("webkit-measure-cursor-position"); 1452 if (WebInspector.debugDefaultTextEditor) 1453 console.assert(spans.length === 0); 1454 var totalOffset = spans[0].totalOffset(); 1455 var height = spans[0].offsetHeight; 1456 this._paintLineRows([lineRow]); 1457 this.endDomUpdates(); 1458 1459 this._restoreSelection(selection); 1460 return { 1461 x: totalOffset.left, 1462 y: totalOffset.top, 1463 height: height 1464 }; 1465 }, 1466 1467 /** 1468 * @param {number} x 1469 * @param {number} y 1470 * @return {?WebInspector.TextRange} 1471 */ 1472 coordinatesToCursorPosition: function(x, y) 1473 { 1474 var element = document.elementFromPoint(x, y); 1475 if (!element) 1476 return null; 1477 var lineRow = element.enclosingNodeOrSelfWithClass("webkit-line-content"); 1478 if (!lineRow) 1479 return null; 1480 1481 var line = this._textModel.line(lineRow.lineNumber) + " "; 1482 var ranges = []; 1483 const prefix = "character-position-"; 1484 for(var i = 0; i < line.length; ++i) { 1485 ranges.push({ 1486 startColumn: i, 1487 endColumn: i, 1488 token: prefix + i 1489 }); 1490 } 1491 1492 var selection = this.selection(); 1493 1494 this.beginDomUpdates(); 1495 this._renderRanges(lineRow, line, ranges); 1496 var charElement = document.elementFromPoint(x, y); 1497 this._paintLineRows([lineRow]); 1498 this.endDomUpdates(); 1499 1500 this._restoreSelection(selection); 1501 var className = charElement.className; 1502 if (className.indexOf(prefix) < 0) 1503 return null; 1504 var column = parseInt(className.substring(className.indexOf(prefix) + prefix.length), 10); 1505 1506 return WebInspector.TextRange.createFromLocation(lineRow.lineNumber, column); 1507 }, 1508 1509 /** 1510 * @param {string} line 1511 * @param {number} column 1512 * @return {?{startColumn: number, endColumn: number, type: string}} 1513 */ 1514 _tokenAtUnhighlightedLine: function(line, column) 1515 { 1516 var tokenizer = WebInspector.SourceTokenizer.Registry.getInstance().getTokenizer(this.mimeType); 1517 tokenizer.condition = tokenizer.createInitialCondition(); 1518 tokenizer.line = line; 1519 var lastTokenizedColumn = 0; 1520 while (lastTokenizedColumn < line.length) { 1521 var newColumn = tokenizer.nextToken(lastTokenizedColumn); 1522 if (column < newColumn) { 1523 if (!tokenizer.tokenType) 1524 return null; 1525 return { 1526 startColumn: lastTokenizedColumn, 1527 endColumn: newColumn - 1, 1528 type: tokenizer.tokenType 1529 }; 1530 } else 1531 lastTokenizedColumn = newColumn; 1532 } 1533 return null; 1534 }, 1535 1536 _registerShortcuts: function() 1537 { 1538 var keys = WebInspector.KeyboardShortcut.Keys; 1539 var modifiers = WebInspector.KeyboardShortcut.Modifiers; 1540 1541 this._shortcuts = {}; 1542 1543 this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Enter.code, WebInspector.KeyboardShortcut.Modifiers.None)] = this._handleEnterKey.bind(this); 1544 this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.CtrlOrMeta)] = this._handleUndoRedo.bind(this, false); 1545 1546 var handleRedo = this._handleUndoRedo.bind(this, true); 1547 this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.Shift | modifiers.CtrlOrMeta)] = handleRedo; 1548 if (!WebInspector.isMac()) 1549 this._shortcuts[WebInspector.KeyboardShortcut.makeKey("y", modifiers.CtrlOrMeta)] = handleRedo; 1550 1551 var handleTabKey = this._handleTabKeyPress.bind(this, false); 1552 var handleShiftTabKey = this._handleTabKeyPress.bind(this, true); 1553 this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code)] = handleTabKey; 1554 this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code, modifiers.Shift)] = handleShiftTabKey; 1555 1556 var homeKey = WebInspector.isMac() ? keys.Right : keys.Home; 1557 var homeModifier = WebInspector.isMac() ? modifiers.Meta : modifiers.None; 1558 this._shortcuts[WebInspector.KeyboardShortcut.makeKey(homeKey.code, homeModifier)] = this._handleHomeKey.bind(this, false); 1559 this._shortcuts[WebInspector.KeyboardShortcut.makeKey(homeKey.code, homeModifier | modifiers.Shift)] = this._handleHomeKey.bind(this, true); 1560 1561 this._charOverrides = {}; 1562 1563 this._smartBraceController.registerShortcuts(this._shortcuts); 1564 this._smartBraceController.registerCharOverrides(this._charOverrides); 1565 }, 1566 1567 _handleKeyPress: function(event) 1568 { 1569 var char = String.fromCharCode(event.which); 1570 var handler = this._charOverrides[char]; 1571 if (handler && handler()) { 1572 event.consume(true); 1573 return; 1574 } 1575 this._keyDownCode = event.keyCode; 1576 }, 1577 1578 /** 1579 * @param {boolean} shift 1580 */ 1581 _handleHomeKey: function(shift) 1582 { 1583 var selection = this.selection(); 1584 1585 var line = this._textModel.line(selection.endLine); 1586 var firstNonBlankCharacter = 0; 1587 while (firstNonBlankCharacter < line.length) { 1588 var char = line.charAt(firstNonBlankCharacter); 1589 if (char === " " || char === "\t") 1590 ++firstNonBlankCharacter; 1591 else 1592 break; 1593 } 1594 if (firstNonBlankCharacter >= line.length || selection.endColumn === firstNonBlankCharacter) 1595 return false; 1596 1597 selection.endColumn = firstNonBlankCharacter; 1598 if (!shift) 1599 selection = selection.collapseToEnd(); 1600 this._restoreSelection(selection); 1601 return true; 1602 }, 1603 1604 /** 1605 * @param {string} regex 1606 * @param {string} cssClass 1607 * @return {Object} 1608 */ 1609 highlightRegex: function(regex, cssClass) 1610 { 1611 var highlightDescriptor = new WebInspector.TextEditorMainPanel.RegexHighlightDescriptor(new RegExp(regex, "g"), cssClass); 1612 this._highlightDescriptors.push(highlightDescriptor); 1613 this._repaintLineRowsAffectedByHighlightDescriptors([highlightDescriptor]); 1614 return highlightDescriptor; 1615 }, 1616 1617 /** 1618 * @param {Object} highlightDescriptor 1619 */ 1620 removeHighlight: function(highlightDescriptor) 1621 { 1622 this._highlightDescriptors.remove(highlightDescriptor); 1623 this._repaintLineRowsAffectedByHighlightDescriptors([highlightDescriptor]); 1624 }, 1625 1626 /** 1627 * @param {WebInspector.TextRange} range 1628 * @param {string} cssClass 1629 * @return {Object} 1630 */ 1631 highlightRange: function(range, cssClass) 1632 { 1633 var highlightDescriptor = new WebInspector.TextEditorMainPanel.RangeHighlightDescriptor(range, cssClass); 1634 this._highlightDescriptors.push(highlightDescriptor); 1635 this._repaintLineRowsAffectedByHighlightDescriptors([highlightDescriptor]); 1636 return highlightDescriptor; 1637 }, 1638 1639 /** 1640 * @param {Array.<WebInspector.TextEditorMainPanel.HighlightDescriptor>} highlightDescriptors 1641 */ 1642 _repaintLineRowsAffectedByHighlightDescriptors: function(highlightDescriptors) 1643 { 1644 var visibleFrom = this.scrollTop(); 1645 var visibleTo = visibleFrom + this.clientHeight(); 1646 1647 var visibleChunks = this.findVisibleChunks(visibleFrom, visibleTo); 1648 1649 var affectedLineRows = []; 1650 for (var i = visibleChunks.start; i < visibleChunks.end; ++i) { 1651 var chunk = this._textChunks[i]; 1652 if (!chunk.expanded()) 1653 continue; 1654 for (var lineNumber = chunk.startLine; lineNumber < chunk.startLine + chunk.linesCount; ++lineNumber) { 1655 var lineRow = chunk.expandedLineRow(lineNumber); 1656 var line = this._textModel.line(lineNumber); 1657 for(var j = 0; j < highlightDescriptors.length; ++j) { 1658 if (highlightDescriptors[j].affectsLine(lineNumber, line)) { 1659 affectedLineRows.push(lineRow); 1660 break; 1661 } 1662 } 1663 } 1664 } 1665 if (affectedLineRows.length === 0) 1666 return; 1667 var selection = this.selection(); 1668 this._paintLineRows(affectedLineRows); 1669 this._restoreSelection(selection); 1670 }, 1671 1672 resize: function() 1673 { 1674 WebInspector.TextEditorChunkedPanel.prototype.resize.call(this); 1675 this._repaintLineRowsAffectedByHighlightDescriptors(this._highlightDescriptors); 1676 }, 1677 1678 wasShown: function() 1679 { 1680 this._boundSelectionChangeListener = this._handleSelectionChange.bind(this); 1681 document.addEventListener("selectionchange", this._boundSelectionChangeListener, false); 1682 1683 this._isShowing = true; 1684 this._attachMutationObserver(); 1685 }, 1686 1687 willHide: function() 1688 { 1689 document.removeEventListener("selectionchange", this._boundSelectionChangeListener, false); 1690 delete this._boundSelectionChangeListener; 1691 1692 this._detachMutationObserver(); 1693 this._isShowing = false; 1694 this._freeCachedElements(); 1695 }, 1696 1697 /** 1698 * @param {Element} eventTarget 1699 * @param {WebInspector.ContextMenu} contextMenu 1700 */ 1701 populateContextMenu: function(eventTarget, contextMenu) 1702 { 1703 var target = this._enclosingLineRowOrSelf(eventTarget); 1704 this._delegate.populateTextAreaContextMenu(contextMenu, target && target.lineNumber); 1705 }, 1706 1707 /** 1708 * @param {WebInspector.TextRange} textRange 1709 */ 1710 setSelection: function(textRange) 1711 { 1712 this._lastSelection = textRange; 1713 if (this.element.isAncestor(document.activeElement)) 1714 this._restoreSelection(textRange); 1715 }, 1716 1717 _handleFocused: function() 1718 { 1719 if (this._lastSelection) 1720 this.setSelection(this._lastSelection); 1721 }, 1722 1723 _attachMutationObserver: function() 1724 { 1725 if (!this._isShowing) 1726 return; 1727 1728 if (this._mutationObserver) 1729 this._mutationObserver.disconnect(); 1730 this._mutationObserver = new NonLeakingMutationObserver(this._handleMutations.bind(this)); 1731 this._mutationObserver.observe(this._container, { subtree: true, childList: true, characterData: true }); 1732 }, 1733 1734 _detachMutationObserver: function() 1735 { 1736 if (!this._isShowing) 1737 return; 1738 1739 if (this._mutationObserver) { 1740 this._mutationObserver.disconnect(); 1741 delete this._mutationObserver; 1742 } 1743 }, 1744 1745 /** 1746 * @param {string} mimeType 1747 */ 1748 set mimeType(mimeType) 1749 { 1750 this._highlighter.mimeType = mimeType; 1751 }, 1752 1753 get mimeType() 1754 { 1755 return this._highlighter.mimeType; 1756 }, 1757 1758 /** 1759 * @param {boolean} readOnly 1760 * @param {boolean} requestFocus 1761 */ 1762 setReadOnly: function(readOnly, requestFocus) 1763 { 1764 if (this._readOnly === readOnly) 1765 return; 1766 1767 this.beginDomUpdates(); 1768 this._readOnly = readOnly; 1769 if (this._readOnly) 1770 this._container.removeStyleClass("text-editor-editable"); 1771 else { 1772 this._container.addStyleClass("text-editor-editable"); 1773 if (requestFocus) 1774 this._updateSelectionOnStartEditing(); 1775 } 1776 this.endDomUpdates(); 1777 }, 1778 1779 /** 1780 * @return {boolean} 1781 */ 1782 readOnly: function() 1783 { 1784 return this._readOnly; 1785 }, 1786 1787 _handleElementFocus: function() 1788 { 1789 if (!this._readOnly) 1790 this._container.focus(); 1791 }, 1792 1793 /** 1794 * @return {Element} 1795 */ 1796 defaultFocusedElement: function() 1797 { 1798 if (this._readOnly) 1799 return this.element; 1800 return this._container; 1801 }, 1802 1803 _updateSelectionOnStartEditing: function() 1804 { 1805 // focus() needs to go first for the case when the last selection was inside the editor and 1806 // the "Edit" button was clicked. In this case we bail at the check below, but the 1807 // editor does not receive the focus, thus "Esc" does not cancel editing until at least 1808 // one change has been made to the editor contents. 1809 this._container.focus(); 1810 var selection = window.getSelection(); 1811 if (selection.rangeCount) { 1812 var commonAncestorContainer = selection.getRangeAt(0).commonAncestorContainer; 1813 if (this._container.isSelfOrAncestor(commonAncestorContainer)) 1814 return; 1815 } 1816 1817 selection.removeAllRanges(); 1818 var range = document.createRange(); 1819 range.setStart(this._container, 0); 1820 range.setEnd(this._container, 0); 1821 selection.addRange(range); 1822 }, 1823 1824 /** 1825 * @param {WebInspector.TextRange} range 1826 */ 1827 markAndRevealRange: function(range) 1828 { 1829 if (this._rangeToMark) { 1830 var markedLine = this._rangeToMark.startLine; 1831 delete this._rangeToMark; 1832 // Remove the marked region immediately. 1833 this.beginDomUpdates(); 1834 var chunk = this.chunkForLine(markedLine); 1835 var wasExpanded = chunk.expanded(); 1836 chunk.collapse(); 1837 chunk.updateCollapsedLineRow(); 1838 if (wasExpanded) 1839 chunk.expand(); 1840 this.endDomUpdates(); 1841 } 1842 1843 if (range) { 1844 this._rangeToMark = range; 1845 this.revealLine(range.startLine); 1846 var chunk = this.makeLineAChunk(range.startLine); 1847 this._paintLines(chunk.startLine, chunk.startLine + 1); 1848 if (this._markedRangeElement) 1849 this._markedRangeElement.scrollIntoViewIfNeeded(); 1850 } 1851 delete this._markedRangeElement; 1852 }, 1853 1854 /** 1855 * @param {number} lineNumber 1856 */ 1857 highlightLine: function(lineNumber) 1858 { 1859 this.clearLineHighlight(); 1860 this._highlightedLine = lineNumber; 1861 this.revealLine(lineNumber); 1862 1863 if (!this._readOnly) 1864 this._restoreSelection(WebInspector.TextRange.createFromLocation(lineNumber, 0), false); 1865 1866 this.addDecoration(lineNumber, "webkit-highlighted-line"); 1867 }, 1868 1869 clearLineHighlight: function() 1870 { 1871 if (typeof this._highlightedLine === "number") { 1872 this.removeDecoration(this._highlightedLine, "webkit-highlighted-line"); 1873 delete this._highlightedLine; 1874 } 1875 }, 1876 1877 _freeCachedElements: function() 1878 { 1879 this._cachedSpans = []; 1880 this._cachedTextNodes = []; 1881 this._cachedRows = []; 1882 }, 1883 1884 /** 1885 * @param {boolean} redo 1886 * @return {boolean} 1887 */ 1888 _handleUndoRedo: function(redo) 1889 { 1890 if (this.readOnly()) 1891 return false; 1892 1893 this.beginUpdates(); 1894 1895 var range = redo ? this._textModel.redo() : this._textModel.undo(); 1896 1897 this.endUpdates(); 1898 1899 // Restore location post-repaint. 1900 if (range) 1901 this._restoreSelection(range, true); 1902 1903 return true; 1904 }, 1905 1906 /** 1907 * @param {boolean} shiftKey 1908 * @return {boolean} 1909 */ 1910 _handleTabKeyPress: function(shiftKey) 1911 { 1912 if (this.readOnly()) 1913 return false; 1914 1915 var selection = this.selection(); 1916 if (!selection) 1917 return false; 1918 1919 var range = selection.normalize(); 1920 1921 this.beginUpdates(); 1922 1923 var newRange; 1924 var rangeWasEmpty = range.isEmpty(); 1925 if (shiftKey) 1926 newRange = this._textModel.unindentLines(range); 1927 else { 1928 if (rangeWasEmpty) 1929 newRange = this._textModel.editRange(range, WebInspector.settings.textEditorIndent.get()); 1930 else 1931 newRange = this._textModel.indentLines(range); 1932 } 1933 1934 this.endUpdates(); 1935 if (rangeWasEmpty) 1936 newRange.startColumn = newRange.endColumn; 1937 this._restoreSelection(newRange, true); 1938 return true; 1939 }, 1940 1941 _handleEnterKey: function() 1942 { 1943 if (this.readOnly()) 1944 return false; 1945 1946 var range = this.selection(); 1947 if (!range) 1948 return false; 1949 1950 range = range.normalize(); 1951 1952 if (range.endColumn === 0) 1953 return false; 1954 1955 var line = this._textModel.line(range.startLine); 1956 var linePrefix = line.substring(0, range.startColumn); 1957 var indentMatch = linePrefix.match(/^\s+/); 1958 var currentIndent = indentMatch ? indentMatch[0] : ""; 1959 1960 var textEditorIndent = WebInspector.settings.textEditorIndent.get(); 1961 var indent = WebInspector.TextEditorModel.endsWithBracketRegex.test(linePrefix) ? currentIndent + textEditorIndent : currentIndent; 1962 1963 if (!indent) 1964 return false; 1965 1966 this.beginDomUpdates(); 1967 1968 var lineBreak = this._textModel.lineBreak; 1969 var newRange; 1970 if (range.isEmpty() && line.substr(range.endColumn - 1, 2) === '{}') { 1971 // {|} 1972 // becomes 1973 // { 1974 // | 1975 // } 1976 newRange = this._textModel.editRange(range, lineBreak + indent + lineBreak + currentIndent); 1977 newRange.endLine--; 1978 newRange.endColumn += textEditorIndent.length; 1979 } else 1980 newRange = this._textModel.editRange(range, lineBreak + indent); 1981 1982 this.endDomUpdates(); 1983 this._restoreSelection(newRange.collapseToEnd(), true); 1984 1985 return true; 1986 }, 1987 1988 /** 1989 * @param {number} lineNumber 1990 * @param {number} chunkNumber 1991 * @param {boolean=} createSuffixChunk 1992 * @return {Object} 1993 */ 1994 splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk) 1995 { 1996 var selection = this.selection(); 1997 var chunk = WebInspector.TextEditorChunkedPanel.prototype.splitChunkOnALine.call(this, lineNumber, chunkNumber, createSuffixChunk); 1998 this._restoreSelection(selection); 1999 return chunk; 2000 }, 2001 2002 beginDomUpdates: function() 2003 { 2004 if (!this._domUpdateCoalescingLevel) 2005 this._detachMutationObserver(); 2006 WebInspector.TextEditorChunkedPanel.prototype.beginDomUpdates.call(this); 2007 }, 2008 2009 endDomUpdates: function() 2010 { 2011 WebInspector.TextEditorChunkedPanel.prototype.endDomUpdates.call(this); 2012 if (!this._domUpdateCoalescingLevel) 2013 this._attachMutationObserver(); 2014 }, 2015 2016 buildChunks: function() 2017 { 2018 for (var i = 0; i < this._textModel.linesCount; ++i) 2019 this._textModel.removeAttribute(i, "highlight"); 2020 2021 WebInspector.TextEditorChunkedPanel.prototype.buildChunks.call(this); 2022 }, 2023 2024 /** 2025 * @param {number} startLine 2026 * @param {number} endLine 2027 * @return {WebInspector.TextEditorMainChunk} 2028 */ 2029 createNewChunk: function(startLine, endLine) 2030 { 2031 return new WebInspector.TextEditorMainChunk(this, startLine, endLine); 2032 }, 2033 2034 /** 2035 * @param {number} fromIndex 2036 * @param {number} toIndex 2037 */ 2038 expandChunks: function(fromIndex, toIndex) 2039 { 2040 var lastChunk = this._textChunks[toIndex - 1]; 2041 var lastVisibleLine = lastChunk.startLine + lastChunk.linesCount; 2042 2043 var selection = this.selection(); 2044 2045 this._muteHighlightListener = true; 2046 this._highlighter.highlight(lastVisibleLine); 2047 delete this._muteHighlightListener; 2048 2049 WebInspector.TextEditorChunkedPanel.prototype.expandChunks.call(this, fromIndex, toIndex); 2050 2051 this._restoreSelection(selection); 2052 }, 2053 2054 /** 2055 * @param {number} fromLine 2056 * @param {number} toLine 2057 */ 2058 _highlightDataReady: function(fromLine, toLine) 2059 { 2060 if (this._muteHighlightListener) 2061 return; 2062 this._paintLines(fromLine, toLine, true /*restoreSelection*/); 2063 }, 2064 2065 /** 2066 * @param {number} fromLine 2067 * @param {number} toLine 2068 * @param {boolean=} restoreSelection 2069 */ 2070 _paintLines: function(fromLine, toLine, restoreSelection) 2071 { 2072 var lineRows = []; 2073 var chunk; 2074 for (var lineNumber = fromLine; lineNumber < toLine; ++lineNumber) { 2075 if (!chunk || lineNumber < chunk.startLine || lineNumber >= chunk.startLine + chunk.linesCount) 2076 chunk = this.chunkForLine(lineNumber); 2077 var lineRow = chunk.expandedLineRow(lineNumber); 2078 if (!lineRow) 2079 continue; 2080 lineRows.push(lineRow); 2081 } 2082 if (lineRows.length === 0) 2083 return; 2084 2085 var selection; 2086 if (restoreSelection) 2087 selection = this.selection(); 2088 2089 this._paintLineRows(lineRows); 2090 2091 if (restoreSelection) 2092 this._restoreSelection(selection); 2093 }, 2094 2095 /** 2096 * @param {Array.<Element>} lineRows 2097 */ 2098 _paintLineRows: function(lineRows) 2099 { 2100 var highlight = {}; 2101 this.beginDomUpdates(); 2102 for(var i = 0; i < this._highlightDescriptors.length; ++i) { 2103 var highlightDescriptor = this._highlightDescriptors[i]; 2104 this._measureHighlightDescriptor(highlight, lineRows, highlightDescriptor); 2105 } 2106 2107 for(var i = 0; i < lineRows.length; ++i) 2108 this._paintLine(lineRows[i], highlight[lineRows[i].lineNumber]); 2109 2110 this.endDomUpdates(); 2111 }, 2112 2113 /** 2114 * @param {Object.<number, Array.<WebInspector.TextEditorMainPanel.LineOverlayHighlight>>} highlight 2115 * @param {Array.<Element>} lineRows 2116 * @param {WebInspector.TextEditorMainPanel.HighlightDescriptor} highlightDescriptor 2117 */ 2118 _measureHighlightDescriptor: function(highlight, lineRows, highlightDescriptor) 2119 { 2120 var rowsToMeasure = []; 2121 for(var i = 0; i < lineRows.length; ++i) { 2122 var lineRow = lineRows[i]; 2123 var line = this._textModel.line(lineRow.lineNumber); 2124 var ranges = highlightDescriptor.rangesForLine(lineRow.lineNumber, line); 2125 if (ranges.length === 0) 2126 continue; 2127 for(var j = 0; j < ranges.length; ++j) 2128 ranges[j].token = "measure-span"; 2129 2130 this._renderRanges(lineRow, line, ranges); 2131 rowsToMeasure.push(lineRow); 2132 } 2133 2134 for(var i = 0; i < rowsToMeasure.length; ++i) { 2135 var lineRow = rowsToMeasure[i]; 2136 var lineNumber = lineRow.lineNumber; 2137 var metrics = this._measureSpans(lineRow); 2138 2139 if (!highlight[lineNumber]) 2140 highlight[lineNumber] = []; 2141 2142 highlight[lineNumber].push(new WebInspector.TextEditorMainPanel.LineOverlayHighlight(metrics, highlightDescriptor.cssClass())); 2143 } 2144 }, 2145 2146 /** 2147 * @param {Element} lineRow 2148 * @return {Array.<WebInspector.TextEditorMainPanel.ElementMetrics>} 2149 */ 2150 _measureSpans: function(lineRow) 2151 { 2152 var spans = lineRow.getElementsByClassName("webkit-measure-span"); 2153 var metrics = []; 2154 for(var i = 0; i < spans.length; ++i) 2155 metrics.push(new WebInspector.TextEditorMainPanel.ElementMetrics(spans[i])); 2156 return metrics; 2157 }, 2158 2159 /** 2160 * @param {Element} lineRow 2161 * @param {WebInspector.TextEditorMainPanel.LineOverlayHighlight} highlight 2162 */ 2163 _appendOverlayHighlight: function(lineRow, highlight) 2164 { 2165 var metrics = highlight.metrics; 2166 var cssClass = highlight.cssClass; 2167 for(var i = 0; i < metrics.length; ++i) { 2168 var highlightSpan = document.createElement("span"); 2169 highlightSpan._isOverlayHighlightElement = true; 2170 highlightSpan.addStyleClass(cssClass); 2171 highlightSpan.style.left = metrics[i].left + "px"; 2172 highlightSpan.style.width = metrics[i].width + "px"; 2173 highlightSpan.style.height = metrics[i].height + "px"; 2174 highlightSpan.addStyleClass("text-editor-overlay-highlight"); 2175 lineRow.insertBefore(highlightSpan, lineRow.decorationsElement); 2176 } 2177 }, 2178 2179 /** 2180 * @param {Element} lineRow 2181 * @param {string} line 2182 * @param {Array.<{startColumn: number, endColumn: number, token: ?string}>} ranges 2183 * @param {boolean=} splitWhitespaceSequences 2184 */ 2185 _renderRanges: function(lineRow, line, ranges, splitWhitespaceSequences) 2186 { 2187 var decorationsElement = lineRow.decorationsElement; 2188 2189 if (!decorationsElement) 2190 lineRow.removeChildren(); 2191 else { 2192 while (true) { 2193 var child = lineRow.firstChild; 2194 if (!child || child === decorationsElement) 2195 break; 2196 lineRow.removeChild(child); 2197 } 2198 } 2199 2200 if (!line) 2201 lineRow.insertBefore(document.createElement("br"), decorationsElement); 2202 2203 var plainTextStart = 0; 2204 for(var i = 0; i < ranges.length; i++) { 2205 var rangeStart = ranges[i].startColumn; 2206 var rangeEnd = ranges[i].endColumn; 2207 2208 if (plainTextStart < rangeStart) { 2209 this._insertSpanBefore(lineRow, decorationsElement, line.substring(plainTextStart, rangeStart)); 2210 } 2211 2212 if (splitWhitespaceSequences && ranges[i].token === "whitespace") 2213 this._renderWhitespaceCharsWithFixedSizeSpans(lineRow, decorationsElement, rangeEnd - rangeStart + 1); 2214 else 2215 this._insertSpanBefore(lineRow, decorationsElement, line.substring(rangeStart, rangeEnd + 1), ranges[i].token ? "webkit-" + ranges[i].token : ""); 2216 plainTextStart = rangeEnd + 1; 2217 } 2218 if (plainTextStart < line.length) { 2219 this._insertSpanBefore(lineRow, decorationsElement, line.substring(plainTextStart, line.length)); 2220 } 2221 }, 2222 2223 /** 2224 * @param {Element} lineRow 2225 * @param {Element} decorationsElement 2226 * @param {number} length 2227 */ 2228 _renderWhitespaceCharsWithFixedSizeSpans: function(lineRow, decorationsElement, length) 2229 { 2230 for (var whitespaceLength = 16; whitespaceLength > 0; whitespaceLength >>= 1) { 2231 var cssClass = "webkit-whitespace webkit-whitespace-" + whitespaceLength; 2232 for (; length >= whitespaceLength; length -= whitespaceLength) 2233 this._insertSpanBefore(lineRow, decorationsElement, WebInspector.TextEditorMainPanel._ConsecutiveWhitespaceChars[whitespaceLength], cssClass); 2234 } 2235 }, 2236 2237 /** 2238 * @param {Element} lineRow 2239 * @param {Array.<WebInspector.TextEditorMainPanel.LineOverlayHighlight>} overlayHighlight 2240 */ 2241 _paintLine: function(lineRow, overlayHighlight) 2242 { 2243 var lineNumber = lineRow.lineNumber; 2244 2245 this.beginDomUpdates(); 2246 try { 2247 var syntaxHighlight = this._textModel.getAttribute(lineNumber, "highlight"); 2248 2249 var line = this._textModel.line(lineNumber); 2250 var ranges = syntaxHighlight ? syntaxHighlight.ranges : []; 2251 this._renderRanges(lineRow, line, ranges, this._showWhitespace); 2252 2253 if (overlayHighlight) 2254 for(var i = 0; i < overlayHighlight.length; ++i) 2255 this._appendOverlayHighlight(lineRow, overlayHighlight[i]); 2256 } finally { 2257 if (this._rangeToMark && this._rangeToMark.startLine === lineNumber) 2258 this._markedRangeElement = WebInspector.highlightSearchResult(lineRow, this._rangeToMark.startColumn, this._rangeToMark.endColumn - this._rangeToMark.startColumn); 2259 this.endDomUpdates(); 2260 } 2261 }, 2262 2263 /** 2264 * @param {Element} lineRow 2265 */ 2266 _releaseLinesHighlight: function(lineRow) 2267 { 2268 if (!lineRow) 2269 return; 2270 if ("spans" in lineRow) { 2271 var spans = lineRow.spans; 2272 for (var j = 0; j < spans.length; ++j) 2273 this._cachedSpans.push(spans[j]); 2274 delete lineRow.spans; 2275 } 2276 if ("textNodes" in lineRow) { 2277 var textNodes = lineRow.textNodes; 2278 for (var j = 0; j < textNodes.length; ++j) 2279 this._cachedTextNodes.push(textNodes[j]); 2280 delete lineRow.textNodes; 2281 } 2282 this._cachedRows.push(lineRow); 2283 }, 2284 2285 /** 2286 * @param {?Node=} lastUndamagedLineRow 2287 * @return {WebInspector.TextRange} 2288 */ 2289 selection: function(lastUndamagedLineRow) 2290 { 2291 var selection = window.getSelection(); 2292 if (!selection.rangeCount) 2293 return null; 2294 // Selection may be outside of the editor. 2295 if (!this._container.isAncestor(selection.anchorNode) || !this._container.isAncestor(selection.focusNode)) 2296 return null; 2297 // Selection may be inside one of decorations. 2298 if (selection.focusNode.enclosingNodeOrSelfWithClass("webkit-line-decorations", this._container)) 2299 return null; 2300 var start = this._selectionToPosition(selection.anchorNode, selection.anchorOffset, lastUndamagedLineRow); 2301 var end = selection.isCollapsed ? start : this._selectionToPosition(selection.focusNode, selection.focusOffset, lastUndamagedLineRow); 2302 return new WebInspector.TextRange(start.line, start.column, end.line, end.column); 2303 }, 2304 2305 lastSelection: function() 2306 { 2307 return this._lastSelection; 2308 }, 2309 2310 /** 2311 * @param {boolean=} scrollIntoView 2312 */ 2313 _restoreSelection: function(range, scrollIntoView) 2314 { 2315 if (!range) 2316 return; 2317 2318 var start = this._positionToSelection(range.startLine, range.startColumn); 2319 var end = range.isEmpty() ? start : this._positionToSelection(range.endLine, range.endColumn); 2320 window.getSelection().setBaseAndExtent(start.container, start.offset, end.container, end.offset); 2321 2322 if (scrollIntoView) { 2323 for (var node = end.container; node; node = node.parentElement) { 2324 if (node.scrollIntoViewIfNeeded) { 2325 node.scrollIntoViewIfNeeded(); 2326 break; 2327 } 2328 } 2329 } 2330 this._lastSelection = range; 2331 }, 2332 2333 /** 2334 * @param {Node} container 2335 * @param {number} offset 2336 * @param {?Node=} lastUndamagedLineRow 2337 * @return {{line: number, column: number}} 2338 */ 2339 _selectionToPosition: function(container, offset, lastUndamagedLineRow) 2340 { 2341 if (container === this._container && offset === 0) 2342 return { line: 0, column: 0 }; 2343 if (container === this._container && offset === 1) 2344 return { line: this._textModel.linesCount - 1, column: this._textModel.lineLength(this._textModel.linesCount - 1) }; 2345 2346 // This method can be called on the damaged DOM (when DOM does not match model). 2347 // We need to start counting lines from the first undamaged line if it is given. 2348 var lineNumber; 2349 var column = 0; 2350 var node; 2351 var scopeNode; 2352 if (lastUndamagedLineRow === null) { 2353 // Last undamaged row is given, but is null - force traverse from the beginning 2354 node = this._container.firstChild; 2355 scopeNode = this._container; 2356 lineNumber = 0; 2357 } else { 2358 var lineRow = this._enclosingLineRowOrSelf(container); 2359 if (!lastUndamagedLineRow || (typeof lineRow.lineNumber === "number" && lineRow.lineNumber <= lastUndamagedLineRow.lineNumber)) { 2360 // DOM is consistent (or we belong to the first damaged row)- lookup the row we belong to and start with it. 2361 node = lineRow; 2362 scopeNode = node; 2363 lineNumber = node.lineNumber; 2364 } else { 2365 // Start with the node following undamaged row. It corresponds to lineNumber + 1. 2366 node = lastUndamagedLineRow.nextSibling; 2367 scopeNode = this._container; 2368 lineNumber = lastUndamagedLineRow.lineNumber + 1; 2369 } 2370 } 2371 2372 // Fast return the line start. 2373 if (container === node && offset === 0) 2374 return { line: lineNumber, column: 0 }; 2375 2376 // Traverse text and increment lineNumber / column. 2377 for (; node && node !== container; node = node.traverseNextNode(scopeNode)) { 2378 if (node.nodeName.toLowerCase() === "br") { 2379 lineNumber++; 2380 column = 0; 2381 } else if (node.nodeType === Node.TEXT_NODE) { 2382 var text = node.textContent; 2383 for (var i = 0; i < text.length; ++i) { 2384 if (text.charAt(i) === "\n") { 2385 lineNumber++; 2386 column = 0; 2387 } else 2388 column++; 2389 } 2390 } 2391 } 2392 2393 // We reached our container node, traverse within itself until we reach given offset. 2394 if (node === container && offset) { 2395 var text = node.textContent; 2396 // In case offset == 1 and lineRow is a chunk div, we need to traverse it all. 2397 var textOffset = (node._chunk && offset === 1) ? text.length : offset; 2398 for (var i = 0; i < textOffset; ++i) { 2399 if (text.charAt(i) === "\n") { 2400 lineNumber++; 2401 column = 0; 2402 } else 2403 column++; 2404 } 2405 } 2406 return { line: lineNumber, column: column }; 2407 }, 2408 2409 /** 2410 * @param {number} line 2411 * @param {number} column 2412 * @return {{container: Element, offset: number}} 2413 */ 2414 _positionToSelection: function(line, column) 2415 { 2416 var chunk = this.chunkForLine(line); 2417 // One-lined collapsed chunks may still stay highlighted. 2418 var lineRow = chunk.linesCount === 1 ? chunk.element : chunk.expandedLineRow(line); 2419 if (lineRow) 2420 var rangeBoundary = lineRow.rangeBoundaryForOffset(column); 2421 else { 2422 var offset = column; 2423 for (var i = chunk.startLine; i < line && i < this._textModel.linesCount; ++i) 2424 offset += this._textModel.lineLength(i) + 1; // \n 2425 lineRow = chunk.element; 2426 if (lineRow.firstChild) 2427 var rangeBoundary = { container: lineRow.firstChild, offset: offset }; 2428 else 2429 var rangeBoundary = { container: lineRow, offset: 0 }; 2430 } 2431 return rangeBoundary; 2432 }, 2433 2434 /** 2435 * @param {Node} element 2436 * @return {?Node} 2437 */ 2438 _enclosingLineRowOrSelf: function(element) 2439 { 2440 var lineRow = element.enclosingNodeOrSelfWithClass("webkit-line-content"); 2441 if (lineRow) 2442 return lineRow; 2443 2444 for (lineRow = element; lineRow; lineRow = lineRow.parentElement) { 2445 if (lineRow.parentElement === this._container) 2446 return lineRow; 2447 } 2448 return null; 2449 }, 2450 2451 /** 2452 * @param {Element} element 2453 * @param {Element} oldChild 2454 * @param {string} content 2455 * @param {string=} className 2456 */ 2457 _insertSpanBefore: function(element, oldChild, content, className) 2458 { 2459 if (className === "html-resource-link" || className === "html-external-link") { 2460 element.insertBefore(this._createLink(content, className === "html-external-link"), oldChild); 2461 return; 2462 } 2463 2464 var span = this._cachedSpans.pop() || document.createElement("span"); 2465 if (!className) 2466 span.removeAttribute("class"); 2467 else 2468 span.className = className; 2469 if (WebInspector.FALSE) // For paint debugging. 2470 span.addStyleClass("debug-fadeout"); 2471 span.textContent = content; 2472 element.insertBefore(span, oldChild); 2473 if (!("spans" in element)) 2474 element.spans = []; 2475 element.spans.push(span); 2476 }, 2477 2478 /** 2479 * @param {Element} element 2480 * @param {Element} oldChild 2481 * @param {string} text 2482 */ 2483 _insertTextNodeBefore: function(element, oldChild, text) 2484 { 2485 var textNode = this._cachedTextNodes.pop(); 2486 if (textNode) 2487 textNode.nodeValue = text; 2488 else 2489 textNode = document.createTextNode(text); 2490 element.insertBefore(textNode, oldChild); 2491 if (!("textNodes" in element)) 2492 element.textNodes = []; 2493 element.textNodes.push(textNode); 2494 }, 2495 2496 /** 2497 * @param {string} content 2498 * @param {boolean} isExternal 2499 * @return {Element} 2500 */ 2501 _createLink: function(content, isExternal) 2502 { 2503 var quote = content.charAt(0); 2504 if (content.length > 1 && (quote === "\"" || quote === "'")) 2505 content = content.substring(1, content.length - 1); 2506 else 2507 quote = null; 2508 2509 var span = document.createElement("span"); 2510 span.className = "webkit-html-attribute-value"; 2511 if (quote) 2512 span.appendChild(document.createTextNode(quote)); 2513 span.appendChild(this._delegate.createLink(content, isExternal)); 2514 if (quote) 2515 span.appendChild(document.createTextNode(quote)); 2516 return span; 2517 }, 2518 2519 /** 2520 * @param {Array.<WebKitMutation>} mutations 2521 */ 2522 _handleMutations: function(mutations) 2523 { 2524 if (this._readOnly) { 2525 delete this._keyDownCode; 2526 return; 2527 } 2528 2529 // Annihilate noop BR addition + removal that takes place upon line removal. 2530 var filteredMutations = mutations.slice(); 2531 var addedBRs = new Map(); 2532 for (var i = 0; i < mutations.length; ++i) { 2533 var mutation = mutations[i]; 2534 if (mutation.type !== "childList") 2535 continue; 2536 if (mutation.addedNodes.length === 1 && mutation.addedNodes[0].nodeName === "BR") 2537 addedBRs.put(mutation.addedNodes[0], mutation); 2538 else if (mutation.removedNodes.length === 1 && mutation.removedNodes[0].nodeName === "BR") { 2539 var noopMutation = addedBRs.get(mutation.removedNodes[0]); 2540 if (noopMutation) { 2541 filteredMutations.remove(mutation); 2542 filteredMutations.remove(noopMutation); 2543 } 2544 } 2545 } 2546 2547 var dirtyLines; 2548 for (var i = 0; i < filteredMutations.length; ++i) { 2549 var mutation = filteredMutations[i]; 2550 var changedNodes = []; 2551 if (mutation.type === "childList" && mutation.addedNodes.length) 2552 changedNodes = Array.prototype.slice.call(mutation.addedNodes); 2553 else if (mutation.type === "childList" && mutation.removedNodes.length) 2554 changedNodes = Array.prototype.slice.call(mutation.removedNodes); 2555 changedNodes.push(mutation.target); 2556 2557 for (var j = 0; j < changedNodes.length; ++j) { 2558 var lines = this._collectDirtyLines(mutation, changedNodes[j]); 2559 if (!lines) 2560 continue; 2561 if (!dirtyLines) { 2562 dirtyLines = lines; 2563 continue; 2564 } 2565 dirtyLines.start = Math.min(dirtyLines.start, lines.start); 2566 dirtyLines.end = Math.max(dirtyLines.end, lines.end); 2567 } 2568 } 2569 if (dirtyLines) { 2570 delete this._rangeToMark; 2571 this._applyDomUpdates(dirtyLines); 2572 } 2573 2574 this._assertDOMMatchesTextModel(); 2575 2576 delete this._keyDownCode; 2577 }, 2578 2579 /** 2580 * @param {WebKitMutation} mutation 2581 * @param {Node} target 2582 * @return {?Object} 2583 */ 2584 _collectDirtyLines: function(mutation, target) 2585 { 2586 var lineRow = this._enclosingLineRowOrSelf(target); 2587 if (!lineRow) 2588 return null; 2589 2590 if (lineRow.decorationsElement && lineRow.decorationsElement.isSelfOrAncestor(target)) { 2591 if (this._syncDecorationsForLineListener) 2592 this._syncDecorationsForLineListener(lineRow.lineNumber); 2593 return null; 2594 } 2595 2596 if (typeof lineRow.lineNumber !== "number") 2597 return null; 2598 2599 var startLine = lineRow.lineNumber; 2600 var endLine = lineRow._chunk ? lineRow._chunk.endLine - 1 : lineRow.lineNumber; 2601 return { start: startLine, end: endLine }; 2602 }, 2603 2604 /** 2605 * @param {Object} dirtyLines 2606 */ 2607 _applyDomUpdates: function(dirtyLines) 2608 { 2609 var lastUndamagedLineNumber = dirtyLines.start - 1; // Can be -1 2610 var firstUndamagedLineNumber = dirtyLines.end + 1; // Can be this._textModel.linesCount 2611 2612 var lastUndamagedLineChunk = lastUndamagedLineNumber >= 0 ? this._textChunks[this.chunkNumberForLine(lastUndamagedLineNumber)] : null; 2613 var firstUndamagedLineChunk = firstUndamagedLineNumber < this._textModel.linesCount ? this._textChunks[this.chunkNumberForLine(firstUndamagedLineNumber)] : null; 2614 2615 var collectLinesFromNode = lastUndamagedLineChunk ? lastUndamagedLineChunk.lineRowContainingLine(lastUndamagedLineNumber) : null; 2616 var collectLinesToNode = firstUndamagedLineChunk ? firstUndamagedLineChunk.lineRowContainingLine(firstUndamagedLineNumber) : null; 2617 var lines = this._collectLinesFromDOM(collectLinesFromNode, collectLinesToNode); 2618 2619 var startLine = dirtyLines.start; 2620 var endLine = dirtyLines.end; 2621 2622 var originalSelection = this._lastSelection; 2623 var editInfo = this._guessEditRangeBasedOnSelection(startLine, endLine, lines); 2624 if (!editInfo) { 2625 if (WebInspector.debugDefaultTextEditor) 2626 console.warn("Falling back to expensive edit"); 2627 var range = new WebInspector.TextRange(startLine, 0, endLine, this._textModel.lineLength(endLine)); 2628 if (!lines.length) { 2629 // Entire damaged area has collapsed. Replace everything between start and end lines with nothing. 2630 editInfo = new WebInspector.DefaultTextEditor.EditInfo(this._textModel.growRangeRight(range), ""); 2631 } else 2632 editInfo = new WebInspector.DefaultTextEditor.EditInfo(range, lines.join("\n")); 2633 } 2634 2635 var selection = this.selection(collectLinesFromNode); 2636 2637 // Unindent after block 2638 if (editInfo.text === "}" && editInfo.range.isEmpty() && selection.isEmpty() && !this._textModel.line(editInfo.range.endLine).trim()) { 2639 var offset = this._closingBlockOffset(editInfo.range); 2640 if (offset >= 0) { 2641 editInfo.range.startColumn = offset; 2642 selection.startColumn = offset + 1; 2643 selection.endColumn = offset + 1; 2644 } 2645 } 2646 2647 this._textModel.editRange(editInfo.range, editInfo.text, originalSelection); 2648 this._restoreSelection(selection); 2649 }, 2650 2651 /** 2652 * @param {number} startLine 2653 * @param {number} endLine 2654 * @param {Array.<string>} lines 2655 * @return {?WebInspector.DefaultTextEditor.EditInfo} 2656 */ 2657 _guessEditRangeBasedOnSelection: function(startLine, endLine, lines) 2658 { 2659 // Analyze input data 2660 var textInputData = this._textInputData; 2661 delete this._textInputData; 2662 var isBackspace = this._keyDownCode === WebInspector.KeyboardShortcut.Keys.Backspace.code; 2663 var isDelete = this._keyDownCode === WebInspector.KeyboardShortcut.Keys.Delete.code; 2664 2665 if (!textInputData && (isDelete || isBackspace)) 2666 textInputData = ""; 2667 2668 // Return if there is no input data or selection 2669 if (typeof textInputData === "undefined" || !this._lastSelection) 2670 return null; 2671 2672 // Adjust selection based on the keyboard actions (grow for backspace, etc.). 2673 textInputData = textInputData || ""; 2674 var range = this._lastSelection.normalize(); 2675 if (isBackspace && range.isEmpty()) 2676 range = this._textModel.growRangeLeft(range); 2677 else if (isDelete && range.isEmpty()) 2678 range = this._textModel.growRangeRight(range); 2679 2680 // Test that selection intersects damaged lines 2681 if (startLine > range.endLine || endLine < range.startLine) 2682 return null; 2683 2684 var replacementLineCount = textInputData.split("\n").length - 1; 2685 var lineCountDelta = replacementLineCount - range.linesCount; 2686 if (startLine + lines.length - endLine - 1 !== lineCountDelta) 2687 return null; 2688 2689 // Clone text model of the size that fits both: selection before edit and the damaged lines after edit. 2690 var cloneFromLine = Math.min(range.startLine, startLine); 2691 var postLastLine = startLine + lines.length + lineCountDelta; 2692 var cloneToLine = Math.min(Math.max(postLastLine, range.endLine) + 1, this._textModel.linesCount); 2693 var domModel = this._textModel.slice(cloneFromLine, cloneToLine); 2694 domModel.editRange(range.shift(-cloneFromLine), textInputData); 2695 2696 // Then we'll test if this new model matches the DOM lines. 2697 for (var i = 0; i < lines.length; ++i) { 2698 if (domModel.line(i + startLine - cloneFromLine) !== lines[i]) 2699 return null; 2700 } 2701 return new WebInspector.DefaultTextEditor.EditInfo(range, textInputData); 2702 }, 2703 2704 _assertDOMMatchesTextModel: function() 2705 { 2706 if (!WebInspector.debugDefaultTextEditor) 2707 return; 2708 2709 console.assert(this.element.innerText === this._textModel.text() + "\n", "DOM does not match model."); 2710 for (var lineRow = this._container.firstChild; lineRow; lineRow = lineRow.nextSibling) { 2711 var lineNumber = lineRow.lineNumber; 2712 if (typeof lineNumber !== "number") { 2713 console.warn("No line number on line row"); 2714 continue; 2715 } 2716 if (lineRow._chunk) { 2717 var chunk = lineRow._chunk; 2718 console.assert(lineNumber === chunk.startLine); 2719 var chunkText = this._textModel.copyRange(new WebInspector.TextRange(chunk.startLine, 0, chunk.endLine - 1, this._textModel.lineLength(chunk.endLine - 1))); 2720 if (chunkText !== lineRow.textContent) 2721 console.warn("Chunk is not matching: %d %O", lineNumber, lineRow); 2722 } else if (this._textModel.line(lineNumber) !== lineRow.textContent) 2723 console.warn("Line is not matching: %d %O", lineNumber, lineRow); 2724 } 2725 }, 2726 2727 /** 2728 * @param {WebInspector.TextRange} oldRange 2729 * @return {number} 2730 */ 2731 _closingBlockOffset: function(oldRange) 2732 { 2733 var leftBrace = this._braceMatcher.findLeftCandidate(oldRange.startLine, oldRange.startColumn); 2734 if (!leftBrace || leftBrace.token !== "block-start") 2735 return -1; 2736 var lineContent = this._textModel.line(leftBrace.lineNumber); 2737 return lineContent.length - lineContent.trimLeft().length; 2738 }, 2739 2740 /** 2741 * @param {WebInspector.TextRange} oldRange 2742 * @param {WebInspector.TextRange} newRange 2743 */ 2744 textChanged: function(oldRange, newRange) 2745 { 2746 this.beginDomUpdates(); 2747 this._removeDecorationsInRange(oldRange); 2748 this._updateChunksForRanges(oldRange, newRange); 2749 this._updateHighlightsForRange(newRange); 2750 this.endDomUpdates(); 2751 }, 2752 2753 /** 2754 * @param {WebInspector.TextRange} range 2755 */ 2756 _removeDecorationsInRange: function(range) 2757 { 2758 for (var i = this.chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i) { 2759 var chunk = this._textChunks[i]; 2760 if (chunk.startLine > range.endLine) 2761 break; 2762 chunk.removeAllDecorations(); 2763 } 2764 }, 2765 2766 /** 2767 * @param {WebInspector.TextRange} oldRange 2768 * @param {WebInspector.TextRange} newRange 2769 */ 2770 _updateChunksForRanges: function(oldRange, newRange) 2771 { 2772 var firstDamagedChunkNumber = this.chunkNumberForLine(oldRange.startLine); 2773 var lastDamagedChunkNumber = firstDamagedChunkNumber; 2774 while (lastDamagedChunkNumber + 1 < this._textChunks.length) { 2775 if (this._textChunks[lastDamagedChunkNumber + 1].startLine > oldRange.endLine) 2776 break; 2777 ++lastDamagedChunkNumber; 2778 } 2779 2780 var firstDamagedChunk = this._textChunks[firstDamagedChunkNumber]; 2781 var lastDamagedChunk = this._textChunks[lastDamagedChunkNumber]; 2782 2783 var linesDiff = newRange.linesCount - oldRange.linesCount; 2784 2785 // First, detect chunks that have not been modified and simply shift them. 2786 if (linesDiff) { 2787 for (var chunkNumber = lastDamagedChunkNumber + 1; chunkNumber < this._textChunks.length; ++chunkNumber) 2788 this._textChunks[chunkNumber].startLine += linesDiff; 2789 } 2790 2791 // Remove damaged chunks from DOM and from textChunks model. 2792 var lastUndamagedChunk = firstDamagedChunkNumber > 0 ? this._textChunks[firstDamagedChunkNumber - 1] : null; 2793 var firstUndamagedChunk = lastDamagedChunkNumber + 1 < this._textChunks.length ? this._textChunks[lastDamagedChunkNumber + 1] : null; 2794 2795 var removeDOMFromNode = lastUndamagedChunk ? lastUndamagedChunk.lastElement().nextSibling : this._container.firstChild; 2796 var removeDOMToNode = firstUndamagedChunk ? firstUndamagedChunk.firstElement() : null; 2797 2798 // Fast case - patch single expanded chunk that did not grow / shrink during edit. 2799 if (!linesDiff && firstDamagedChunk === lastDamagedChunk && firstDamagedChunk._expandedLineRows) { 2800 var lastUndamagedLineRow = lastDamagedChunk.expandedLineRow(oldRange.startLine - 1); 2801 var firstUndamagedLineRow = firstDamagedChunk.expandedLineRow(oldRange.endLine + 1); 2802 var localRemoveDOMFromNode = lastUndamagedLineRow ? lastUndamagedLineRow.nextSibling : removeDOMFromNode; 2803 var localRemoveDOMToNode = firstUndamagedLineRow || removeDOMToNode; 2804 removeSubsequentNodes(localRemoveDOMFromNode, localRemoveDOMToNode); 2805 for (var i = newRange.startLine; i < newRange.endLine + 1; ++i) { 2806 var row = firstDamagedChunk._createRow(i); 2807 firstDamagedChunk._expandedLineRows[i - firstDamagedChunk.startLine] = row; 2808 this._container.insertBefore(row, localRemoveDOMToNode); 2809 } 2810 firstDamagedChunk.updateCollapsedLineRow(); 2811 this._assertDOMMatchesTextModel(); 2812 return; 2813 } 2814 2815 removeSubsequentNodes(removeDOMFromNode, removeDOMToNode); 2816 this._textChunks.splice(firstDamagedChunkNumber, lastDamagedChunkNumber - firstDamagedChunkNumber + 1); 2817 2818 // Compute damaged chunks span 2819 var startLine = firstDamagedChunk.startLine; 2820 var endLine = lastDamagedChunk.endLine + linesDiff; 2821 var lineSpan = endLine - startLine; 2822 2823 // Re-create chunks for damaged area. 2824 var insertionIndex = firstDamagedChunkNumber; 2825 var chunkSize = Math.ceil(lineSpan / Math.ceil(lineSpan / this._defaultChunkSize)); 2826 2827 for (var i = startLine; i < endLine; i += chunkSize) { 2828 var chunk = this.createNewChunk(i, Math.min(endLine, i + chunkSize)); 2829 this._textChunks.splice(insertionIndex++, 0, chunk); 2830 this._container.insertBefore(chunk.element, removeDOMToNode); 2831 } 2832 2833 this._assertDOMMatchesTextModel(); 2834 }, 2835 2836 /** 2837 * @param {WebInspector.TextRange} range 2838 */ 2839 _updateHighlightsForRange: function(range) 2840 { 2841 var visibleFrom = this.scrollTop(); 2842 var visibleTo = visibleFrom + this.clientHeight(); 2843 2844 var result = this.findVisibleChunks(visibleFrom, visibleTo); 2845 var chunk = this._textChunks[result.end - 1]; 2846 var lastVisibleLine = chunk.startLine + chunk.linesCount; 2847 2848 lastVisibleLine = Math.max(lastVisibleLine, range.endLine + 1); 2849 lastVisibleLine = Math.min(lastVisibleLine, this._textModel.linesCount); 2850 2851 var updated = this._highlighter.updateHighlight(range.startLine, lastVisibleLine); 2852 if (!updated) { 2853 // Highlights for the chunks below are invalid, so just collapse them. 2854 for (var i = this.chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i) 2855 this._textChunks[i].collapse(); 2856 } 2857 2858 this.repaintAll(); 2859 }, 2860 2861 /** 2862 * @param {Node} from 2863 * @param {Node} to 2864 * @return {Array.<string>} 2865 */ 2866 _collectLinesFromDOM: function(from, to) 2867 { 2868 var textContents = []; 2869 var hasContent = false; 2870 for (var node = from ? from.nextSibling : this._container; node && node !== to; node = node.traverseNextNode(this._container)) { 2871 // Skip all children of the decoration container and overlay highlight spans. 2872 while (node && node !== to && (node._isDecorationsElement || node._isOverlayHighlightElement)) 2873 node = node.nextSibling; 2874 if (!node || node === to) 2875 break; 2876 2877 hasContent = true; 2878 if (node.nodeName.toLowerCase() === "br") 2879 textContents.push("\n"); 2880 else if (node.nodeType === Node.TEXT_NODE) 2881 textContents.push(node.textContent); 2882 } 2883 if (!hasContent) 2884 return []; 2885 2886 var textContent = textContents.join(""); 2887 // The last \n (if any) does not "count" in a DIV. 2888 textContent = textContent.replace(/\n$/, ""); 2889 2890 return textContent.split("\n"); 2891 }, 2892 2893 /** 2894 * @param {Event} event 2895 */ 2896 _handleSelectionChange: function(event) 2897 { 2898 var textRange = this.selection(); 2899 if (textRange) 2900 this._lastSelection = textRange; 2901 2902 this._tokenHighlighter.handleSelectionChange(textRange); 2903 this._braceHighlighter.handleSelectionChange(textRange); 2904 this._delegate.selectionChanged(textRange); 2905 }, 2906 2907 /** 2908 * @param {Event} event 2909 */ 2910 _handleTextInput: function(event) 2911 { 2912 this._textInputData = event.data; 2913 }, 2914 2915 /** 2916 * @param {number} shortcutKey 2917 * @param {Event} event 2918 */ 2919 handleKeyDown: function(shortcutKey, event) 2920 { 2921 var handler = this._shortcuts[shortcutKey]; 2922 if (handler && handler()) { 2923 event.consume(true); 2924 return; 2925 } 2926 2927 this._keyDownCode = event.keyCode; 2928 }, 2929 2930 /** 2931 * @param {Event} event 2932 */ 2933 _handleCut: function(event) 2934 { 2935 this._keyDownCode = WebInspector.KeyboardShortcut.Keys.Delete.code; 2936 }, 2937 2938 /** 2939 * @param {number} scrollTop 2940 * @param {number} clientHeight 2941 * @param {number} chunkSize 2942 */ 2943 overrideViewportForTest: function(scrollTop, clientHeight, chunkSize) 2944 { 2945 this._scrollTopOverrideForTest = scrollTop; 2946 this._clientHeightOverrideForTest = clientHeight; 2947 this._defaultChunkSize = chunkSize; 2948 }, 2949 2950 __proto__: WebInspector.TextEditorChunkedPanel.prototype 2951} 2952 2953/** 2954 * @interface 2955 */ 2956WebInspector.TextEditorMainPanel.HighlightDescriptor = function() { } 2957 2958WebInspector.TextEditorMainPanel.HighlightDescriptor.prototype = { 2959 /** 2960 * @param {number} lineNumber 2961 * @param {string} line 2962 * @return {boolean} 2963 */ 2964 affectsLine: function(lineNumber, line) { return false; }, 2965 2966 /** 2967 * @param {number} lineNumber 2968 * @param {string} line 2969 * @return {Array.<{startColumn: number, endColumn: number}>} 2970 */ 2971 rangesForLine: function(lineNumber, line) { return []; }, 2972 2973 /** 2974 * @return {string} 2975 */ 2976 cssClass: function() { return ""; }, 2977} 2978 2979/** 2980 * @constructor 2981 * @implements {WebInspector.TextEditorMainPanel.HighlightDescriptor} 2982 */ 2983WebInspector.TextEditorMainPanel.RegexHighlightDescriptor = function(regex, cssClass) 2984{ 2985 this._cssClass = cssClass; 2986 this._regex = regex; 2987} 2988 2989WebInspector.TextEditorMainPanel.RegexHighlightDescriptor.prototype = { 2990 /** 2991 * @param {number} lineNumber 2992 * @param {string} line 2993 * @return {boolean} 2994 */ 2995 affectsLine: function(lineNumber, line) 2996 { 2997 this._regex.lastIndex = 0; 2998 return this._regex.test(line); 2999 }, 3000 3001 /** 3002 * @param {number} lineNumber 3003 * @param {string} line 3004 * @return {Array.<{startColumn: number, endColumn: number}>} 3005 */ 3006 rangesForLine: function(lineNumber, line) 3007 { 3008 var ranges = []; 3009 var regexResult; 3010 this._regex.lastIndex = 0; 3011 while (regexResult = this._regex.exec(line)) { 3012 ranges.push({ 3013 startColumn: regexResult.index, 3014 endColumn: regexResult.index + regexResult[0].length - 1 3015 }); 3016 } 3017 return ranges; 3018 }, 3019 3020 /** 3021 * @return {string} 3022 */ 3023 cssClass: function() 3024 { 3025 return this._cssClass; 3026 } 3027} 3028 3029/** 3030 * @constructor 3031 * @implements {WebInspector.TextEditorMainPanel.HighlightDescriptor} 3032 * @param {WebInspector.TextRange} range 3033 * @param {string} cssClass 3034 */ 3035WebInspector.TextEditorMainPanel.RangeHighlightDescriptor = function(range, cssClass) 3036{ 3037 this._cssClass = cssClass; 3038 this._range = range; 3039} 3040 3041WebInspector.TextEditorMainPanel.RangeHighlightDescriptor.prototype = { 3042 /** 3043 * @param {number} lineNumber 3044 * @param {string} line 3045 * @return {boolean} 3046 */ 3047 affectsLine: function(lineNumber, line) 3048 { 3049 return this._range.startLine <= lineNumber && lineNumber <= this._range.endLine && line.length > 0; 3050 }, 3051 3052 /** 3053 * @param {number} lineNumber 3054 * @param {string} line 3055 * @return {Array.<{startColumn: number, endColumn: number}>} 3056 */ 3057 rangesForLine: function(lineNumber, line) 3058 { 3059 if (!this.affectsLine(lineNumber, line)) 3060 return []; 3061 3062 var startColumn = lineNumber === this._range.startLine ? this._range.startColumn : 0; 3063 var endColumn = lineNumber === this._range.endLine ? Math.min(this._range.endColumn, line.length) : line.length; 3064 return [{ 3065 startColumn: startColumn, 3066 endColumn: endColumn 3067 }]; 3068 }, 3069 3070 /** 3071 * @return {string} 3072 */ 3073 cssClass: function() 3074 { 3075 return this._cssClass; 3076 } 3077} 3078 3079/** 3080 * @constructor 3081 * @param {Element} element 3082 */ 3083WebInspector.TextEditorMainPanel.ElementMetrics = function(element) 3084{ 3085 this.width = element.offsetWidth; 3086 this.height = element.offsetHeight; 3087 this.left = element.offsetLeft; 3088} 3089 3090/** 3091 * @constructor 3092 * @param {Array.<WebInspector.TextEditorMainPanel.ElementMetrics>} metrics 3093 * @param {string} cssClass 3094 */ 3095WebInspector.TextEditorMainPanel.LineOverlayHighlight = function(metrics, cssClass) 3096{ 3097 this.metrics = metrics; 3098 this.cssClass = cssClass; 3099} 3100 3101/** 3102 * @constructor 3103 * @param {WebInspector.TextEditorChunkedPanel} chunkedPanel 3104 * @param {number} startLine 3105 * @param {number} endLine 3106 */ 3107WebInspector.TextEditorMainChunk = function(chunkedPanel, startLine, endLine) 3108{ 3109 this._chunkedPanel = chunkedPanel; 3110 this._textModel = chunkedPanel._textModel; 3111 3112 this.element = document.createElement("div"); 3113 this.element.lineNumber = startLine; 3114 this.element.className = "webkit-line-content"; 3115 this.element._chunk = this; 3116 3117 this._startLine = startLine; 3118 endLine = Math.min(this._textModel.linesCount, endLine); 3119 this.linesCount = endLine - startLine; 3120 3121 this._expanded = false; 3122 3123 this.updateCollapsedLineRow(); 3124} 3125 3126WebInspector.TextEditorMainChunk.prototype = { 3127 /** 3128 * @param {Element|string} decoration 3129 */ 3130 addDecoration: function(decoration) 3131 { 3132 this._chunkedPanel.beginDomUpdates(); 3133 if (typeof decoration === "string") 3134 this.element.addStyleClass(decoration); 3135 else { 3136 if (!this.element.decorationsElement) { 3137 this.element.decorationsElement = document.createElement("div"); 3138 this.element.decorationsElement.className = "webkit-line-decorations"; 3139 this.element.decorationsElement._isDecorationsElement = true; 3140 this.element.appendChild(this.element.decorationsElement); 3141 } 3142 this.element.decorationsElement.appendChild(decoration); 3143 } 3144 this._chunkedPanel.endDomUpdates(); 3145 }, 3146 3147 /** 3148 * @param {string|Element} decoration 3149 */ 3150 removeDecoration: function(decoration) 3151 { 3152 this._chunkedPanel.beginDomUpdates(); 3153 if (typeof decoration === "string") 3154 this.element.removeStyleClass(decoration); 3155 else if (this.element.decorationsElement) 3156 this.element.decorationsElement.removeChild(decoration); 3157 this._chunkedPanel.endDomUpdates(); 3158 }, 3159 3160 removeAllDecorations: function() 3161 { 3162 this._chunkedPanel.beginDomUpdates(); 3163 this.element.className = "webkit-line-content"; 3164 if (this.element.decorationsElement) { 3165 if (this.element.decorationsElement.parentElement) 3166 this.element.removeChild(this.element.decorationsElement); 3167 delete this.element.decorationsElement; 3168 } 3169 this._chunkedPanel.endDomUpdates(); 3170 }, 3171 3172 /** 3173 * @return {boolean} 3174 */ 3175 isDecorated: function() 3176 { 3177 return this.element.className !== "webkit-line-content" || !!(this.element.decorationsElement && this.element.decorationsElement.firstChild); 3178 }, 3179 3180 /** 3181 * @return {number} 3182 */ 3183 get startLine() 3184 { 3185 return this._startLine; 3186 }, 3187 3188 /** 3189 * @return {number} 3190 */ 3191 get endLine() 3192 { 3193 return this._startLine + this.linesCount; 3194 }, 3195 3196 set startLine(startLine) 3197 { 3198 this._startLine = startLine; 3199 this.element.lineNumber = startLine; 3200 if (this._expandedLineRows) { 3201 for (var i = 0; i < this._expandedLineRows.length; ++i) 3202 this._expandedLineRows[i].lineNumber = startLine + i; 3203 } 3204 }, 3205 3206 /** 3207 * @return {boolean} 3208 */ 3209 expanded: function() 3210 { 3211 return this._expanded; 3212 }, 3213 3214 expand: function() 3215 { 3216 if (this._expanded) 3217 return; 3218 3219 this._expanded = true; 3220 3221 if (this.linesCount === 1) { 3222 this._chunkedPanel._paintLines(this.startLine, this.startLine + 1); 3223 return; 3224 } 3225 3226 this._chunkedPanel.beginDomUpdates(); 3227 3228 this._expandedLineRows = []; 3229 var parentElement = this.element.parentElement; 3230 for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) { 3231 var lineRow = this._createRow(i); 3232 parentElement.insertBefore(lineRow, this.element); 3233 this._expandedLineRows.push(lineRow); 3234 } 3235 parentElement.removeChild(this.element); 3236 this._chunkedPanel._paintLines(this.startLine, this.startLine + this.linesCount); 3237 3238 this._chunkedPanel.endDomUpdates(); 3239 }, 3240 3241 collapse: function() 3242 { 3243 if (!this._expanded) 3244 return; 3245 3246 this._expanded = false; 3247 if (this.linesCount === 1) 3248 return; 3249 3250 this._chunkedPanel.beginDomUpdates(); 3251 3252 var elementInserted = false; 3253 for (var i = 0; i < this._expandedLineRows.length; ++i) { 3254 var lineRow = this._expandedLineRows[i]; 3255 var parentElement = lineRow.parentElement; 3256 if (parentElement) { 3257 if (!elementInserted) { 3258 elementInserted = true; 3259 parentElement.insertBefore(this.element, lineRow); 3260 } 3261 parentElement.removeChild(lineRow); 3262 } 3263 this._chunkedPanel._releaseLinesHighlight(lineRow); 3264 } 3265 delete this._expandedLineRows; 3266 3267 this._chunkedPanel.endDomUpdates(); 3268 }, 3269 3270 /** 3271 * @return {number} 3272 */ 3273 get height() 3274 { 3275 if (!this._expandedLineRows) 3276 return this._chunkedPanel.totalHeight(this.element); 3277 return this._chunkedPanel.totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]); 3278 }, 3279 3280 /** 3281 * @return {number} 3282 */ 3283 get offsetTop() 3284 { 3285 return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop; 3286 }, 3287 3288 /** 3289 * @param {number} lineNumber 3290 * @return {Element} 3291 */ 3292 _createRow: function(lineNumber) 3293 { 3294 var lineRow = this._chunkedPanel._cachedRows.pop() || document.createElement("div"); 3295 lineRow.lineNumber = lineNumber; 3296 lineRow.className = "webkit-line-content"; 3297 lineRow.textContent = this._textModel.line(lineNumber); 3298 if (!lineRow.textContent) 3299 lineRow.appendChild(document.createElement("br")); 3300 return lineRow; 3301 }, 3302 3303 /** 3304 * Called on potentially damaged / inconsistent chunk 3305 * @param {number} lineNumber 3306 * @return {?Node} 3307 */ 3308 lineRowContainingLine: function(lineNumber) 3309 { 3310 if (!this._expanded) 3311 return this.element; 3312 return this.expandedLineRow(lineNumber); 3313 }, 3314 3315 /** 3316 * @param {number} lineNumber 3317 * @return {Element} 3318 */ 3319 expandedLineRow: function(lineNumber) 3320 { 3321 if (!this._expanded || lineNumber < this.startLine || lineNumber >= this.startLine + this.linesCount) 3322 return null; 3323 if (!this._expandedLineRows) 3324 return this.element; 3325 return this._expandedLineRows[lineNumber - this.startLine]; 3326 }, 3327 3328 updateCollapsedLineRow: function() 3329 { 3330 if (this.linesCount === 1 && this._expanded) 3331 return; 3332 3333 var lines = []; 3334 for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) 3335 lines.push(this._textModel.line(i)); 3336 3337 if (WebInspector.FALSE) 3338 console.log("Rebuilding chunk with " + lines.length + " lines"); 3339 3340 this.element.removeChildren(); 3341 this.element.textContent = lines.join("\n"); 3342 // The last empty line will get swallowed otherwise. 3343 if (!lines[lines.length - 1]) 3344 this.element.appendChild(document.createElement("br")); 3345 }, 3346 3347 firstElement: function() 3348 { 3349 return this._expandedLineRows ? this._expandedLineRows[0] : this.element; 3350 }, 3351 3352 /** 3353 * @return {Element} 3354 */ 3355 lastElement: function() 3356 { 3357 return this._expandedLineRows ? this._expandedLineRows[this._expandedLineRows.length - 1] : this.element; 3358 } 3359} 3360 3361/** 3362 * @constructor 3363 * @param {WebInspector.TextEditorMainPanel} mainPanel 3364 * @param {WebInspector.TextEditorModel} textModel 3365 */ 3366WebInspector.TextEditorMainPanel.TokenHighlighter = function(mainPanel, textModel) 3367{ 3368 this._mainPanel = mainPanel; 3369 this._textModel = textModel; 3370} 3371 3372WebInspector.TextEditorMainPanel.TokenHighlighter.prototype = { 3373 /** 3374 * @param {WebInspector.TextRange} range 3375 */ 3376 handleSelectionChange: function(range) 3377 { 3378 if (!range) { 3379 this._removeHighlight(); 3380 return; 3381 } 3382 3383 if (range.startLine !== range.endLine) { 3384 this._removeHighlight(); 3385 return; 3386 } 3387 3388 range = range.normalize(); 3389 var selectedText = this._textModel.copyRange(range); 3390 if (selectedText === this._selectedWord) 3391 return; 3392 3393 if (selectedText === "") { 3394 this._removeHighlight(); 3395 return; 3396 } 3397 3398 if (this._isWord(range, selectedText)) 3399 this._highlight(selectedText); 3400 else 3401 this._removeHighlight(); 3402 }, 3403 3404 /** 3405 * @param {string} word 3406 */ 3407 _regexString: function(word) 3408 { 3409 return "\\b" + word + "\\b"; 3410 }, 3411 3412 /** 3413 * @param {string} selectedWord 3414 */ 3415 _highlight: function(selectedWord) 3416 { 3417 this._removeHighlight(); 3418 this._selectedWord = selectedWord; 3419 this._highlightDescriptor = this._mainPanel.highlightRegex(this._regexString(selectedWord), "text-editor-token-highlight") 3420 }, 3421 3422 _removeHighlight: function() 3423 { 3424 if (this._selectedWord) { 3425 this._mainPanel.removeHighlight(this._highlightDescriptor); 3426 delete this._selectedWord; 3427 delete this._highlightDescriptor; 3428 } 3429 }, 3430 3431 /** 3432 * @param {WebInspector.TextRange} range 3433 * @param {string} selectedText 3434 * @return {boolean} 3435 */ 3436 _isWord: function(range, selectedText) 3437 { 3438 var line = this._textModel.line(range.startLine); 3439 var leftBound = range.startColumn === 0 || !WebInspector.TextUtils.isWordChar(line.charAt(range.startColumn - 1)); 3440 var rightBound = range.endColumn === line.length || !WebInspector.TextUtils.isWordChar(line.charAt(range.endColumn)); 3441 return leftBound && rightBound && WebInspector.TextUtils.isWord(selectedText); 3442 } 3443} 3444 3445/** 3446 * @constructor 3447 * @param {WebInspector.TextEditorModel} textModel 3448 * @param {WebInspector.TextEditor} textEditor 3449 */ 3450WebInspector.DefaultTextEditor.WordMovementController = function(textEditor, textModel) 3451{ 3452 this._textModel = textModel; 3453 this._textEditor = textEditor; 3454} 3455 3456WebInspector.DefaultTextEditor.WordMovementController.prototype = { 3457 3458 /** 3459 * @param {Object.<number, function()>} shortcuts 3460 */ 3461 _registerShortcuts: function(shortcuts) 3462 { 3463 var keys = WebInspector.KeyboardShortcut.Keys; 3464 var modifiers = WebInspector.KeyboardShortcut.Modifiers; 3465 3466 const wordJumpModifier = WebInspector.isMac() ? modifiers.Alt : modifiers.Ctrl; 3467 shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Backspace.code, wordJumpModifier)] = this._handleCtrlBackspace.bind(this); 3468 shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Left.code, wordJumpModifier)] = this._handleCtrlArrow.bind(this, "left"); 3469 shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Right.code, wordJumpModifier)] = this._handleCtrlArrow.bind(this, "right"); 3470 shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Left.code, modifiers.Shift | wordJumpModifier)] = this._handleCtrlShiftArrow.bind(this, "left"); 3471 shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Right.code, modifiers.Shift | wordJumpModifier)] = this._handleCtrlShiftArrow.bind(this, "right"); 3472 }, 3473 3474 /** 3475 * @param {WebInspector.TextRange} selection 3476 * @param {string} direction 3477 * @return {WebInspector.TextRange} 3478 */ 3479 _rangeForCtrlArrowMove: function(selection, direction) 3480 { 3481 const isStopChar = WebInspector.TextUtils.isStopChar; 3482 const isSpaceChar = WebInspector.TextUtils.isSpaceChar; 3483 3484 var lineNumber = selection.endLine; 3485 var column = selection.endColumn; 3486 if (direction === "left") 3487 --column; 3488 3489 if (column === -1 && direction === "left") { 3490 if (lineNumber > 0) 3491 return new WebInspector.TextRange(selection.startLine, selection.startColumn, lineNumber - 1, this._textModel.line(lineNumber - 1).length); 3492 else 3493 return selection.clone(); 3494 } 3495 3496 var line = this._textModel.line(lineNumber); 3497 if (column === line.length && direction === "right") { 3498 if (lineNumber + 1 < this._textModel.linesCount) 3499 return new WebInspector.TextRange(selection.startLine, selection.startColumn, selection.endLine + 1, 0); 3500 else 3501 return selection.clone(); 3502 } 3503 3504 var delta = direction === "left" ? -1 : +1; 3505 var directionDependentEndColumnOffset = (delta + 1) / 2; 3506 3507 if (isSpaceChar(line.charAt(column))) { 3508 while(column + delta >= 0 && column + delta < line.length && isSpaceChar(line.charAt(column + delta))) 3509 column += delta; 3510 if (column + delta < 0 || column + delta === line.length) 3511 return new WebInspector.TextRange(selection.startLine, selection.startColumn, lineNumber, column + directionDependentEndColumnOffset); 3512 else 3513 column += delta; 3514 } 3515 3516 var group = isStopChar(line.charAt(column)); 3517 3518 while(column + delta >= 0 && column + delta < line.length && isStopChar(line.charAt(column + delta)) === group && !isSpaceChar(line.charAt(column + delta))) 3519 column += delta; 3520 3521 return new WebInspector.TextRange(selection.startLine, selection.startColumn, lineNumber, column + directionDependentEndColumnOffset); 3522 }, 3523 3524 /** 3525 * @param {string} direction 3526 * @return {boolean} 3527 */ 3528 _handleCtrlArrow: function(direction) 3529 { 3530 var newSelection = this._rangeForCtrlArrowMove(this._textEditor.selection(), direction); 3531 this._textEditor.setSelection(newSelection.collapseToEnd()); 3532 return true; 3533 }, 3534 3535 /** 3536 * @param {string} direction 3537 * @return {boolean} 3538 */ 3539 _handleCtrlShiftArrow: function(direction) 3540 { 3541 this._textEditor.setSelection(this._rangeForCtrlArrowMove(this._textEditor.selection(), direction)); 3542 return true; 3543 }, 3544 3545 /** 3546 * @return {boolean} 3547 */ 3548 _handleCtrlBackspace: function() 3549 { 3550 var selection = this._textEditor.selection(); 3551 if (!selection.isEmpty()) 3552 return false; 3553 3554 var newSelection = this._rangeForCtrlArrowMove(selection, "left"); 3555 this._textModel.editRange(newSelection.normalize(), "", selection); 3556 3557 this._textEditor.setSelection(newSelection.collapseToEnd()); 3558 return true; 3559 } 3560} 3561 3562/** 3563 * @constructor 3564 * @param {WebInspector.TextEditorMainPanel} textEditor 3565 * @param {WebInspector.TextEditorModel} textModel 3566 * @param {WebInspector.TextEditorModel.BraceMatcher} braceMatcher 3567 */ 3568WebInspector.TextEditorMainPanel.BraceHighlightController = function(textEditor, textModel, braceMatcher) 3569{ 3570 this._textEditor = textEditor; 3571 this._textModel = textModel; 3572 this._braceMatcher = braceMatcher; 3573 this._highlightDescriptors = []; 3574} 3575 3576WebInspector.TextEditorMainPanel.BraceHighlightController.prototype = { 3577 /** 3578 * @param {string} line 3579 * @param {number} column 3580 * @return {number} 3581 */ 3582 activeBraceColumnForCursorPosition: function(line, column) 3583 { 3584 var char = line.charAt(column); 3585 if (WebInspector.TextUtils.isOpeningBraceChar(char)) 3586 return column; 3587 3588 var previousChar = line.charAt(column - 1); 3589 if (WebInspector.TextUtils.isBraceChar(previousChar)) 3590 return column - 1; 3591 3592 if (WebInspector.TextUtils.isBraceChar(char)) 3593 return column; 3594 else 3595 return -1; 3596 }, 3597 3598 /** 3599 * @param {WebInspector.TextRange} selectionRange 3600 */ 3601 handleSelectionChange: function(selectionRange) 3602 { 3603 if (!selectionRange || !selectionRange.isEmpty()) { 3604 this._removeHighlight(); 3605 return; 3606 } 3607 3608 if (this._highlightedRange && this._highlightedRange.compareTo(selectionRange) === 0) 3609 return; 3610 3611 this._removeHighlight(); 3612 var lineNumber = selectionRange.startLine; 3613 var column = selectionRange.startColumn; 3614 var line = this._textModel.line(lineNumber); 3615 column = this.activeBraceColumnForCursorPosition(line, column); 3616 if (column < 0) 3617 return; 3618 3619 var enclosingBraces = this._braceMatcher.enclosingBraces(lineNumber, column); 3620 if (!enclosingBraces) 3621 return; 3622 3623 this._highlightedRange = selectionRange; 3624 this._highlightDescriptors.push(this._textEditor.highlightRange(WebInspector.TextRange.createFromLocation(enclosingBraces.leftBrace.lineNumber, enclosingBraces.leftBrace.column), "text-editor-brace-match")); 3625 this._highlightDescriptors.push(this._textEditor.highlightRange(WebInspector.TextRange.createFromLocation(enclosingBraces.rightBrace.lineNumber, enclosingBraces.rightBrace.column), "text-editor-brace-match")); 3626 }, 3627 3628 _removeHighlight: function() 3629 { 3630 if (!this._highlightDescriptors.length) 3631 return; 3632 3633 for(var i = 0; i < this._highlightDescriptors.length; ++i) 3634 this._textEditor.removeHighlight(this._highlightDescriptors[i]); 3635 3636 this._highlightDescriptors = []; 3637 delete this._highlightedRange; 3638 } 3639} 3640 3641/** 3642 * @constructor 3643 * @param {WebInspector.TextEditorMainPanel} mainPanel 3644 * @param {WebInspector.TextEditorModel} textModel 3645 * @param {WebInspector.TextEditorModel.BraceMatcher} braceMatcher 3646 */ 3647WebInspector.TextEditorMainPanel.SmartBraceController = function(mainPanel, textModel, braceMatcher) 3648{ 3649 this._mainPanel = mainPanel; 3650 this._textModel = textModel; 3651 this._braceMatcher = braceMatcher 3652} 3653 3654WebInspector.TextEditorMainPanel.SmartBraceController.prototype = { 3655 /** 3656 * @param {Object.<number, function()>} shortcuts 3657 */ 3658 registerShortcuts: function(shortcuts) 3659 { 3660 if (!WebInspector.experimentsSettings.textEditorSmartBraces.isEnabled()) 3661 return; 3662 3663 var keys = WebInspector.KeyboardShortcut.Keys; 3664 var modifiers = WebInspector.KeyboardShortcut.Modifiers; 3665 3666 shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Backspace.code, modifiers.None)] = this._handleBackspace.bind(this); 3667 }, 3668 3669 /** 3670 * @param {Object.<string, function()>} charOverrides 3671 */ 3672 registerCharOverrides: function(charOverrides) 3673 { 3674 if (!WebInspector.experimentsSettings.textEditorSmartBraces.isEnabled()) 3675 return; 3676 charOverrides["("] = this._handleBracePairInsertion.bind(this, "()"); 3677 charOverrides[")"] = this._handleClosingBraceOverride.bind(this, ")"); 3678 charOverrides["{"] = this._handleBracePairInsertion.bind(this, "{}"); 3679 charOverrides["}"] = this._handleClosingBraceOverride.bind(this, "}"); 3680 }, 3681 3682 _handleBackspace: function() 3683 { 3684 var selection = this._mainPanel.lastSelection(); 3685 if (!selection || !selection.isEmpty()) 3686 return false; 3687 3688 var column = selection.startColumn; 3689 if (column == 0) 3690 return false; 3691 3692 var lineNumber = selection.startLine; 3693 var line = this._textModel.line(lineNumber); 3694 if (column === line.length) 3695 return false; 3696 3697 var pair = line.substr(column - 1, 2); 3698 if (pair === "()" || pair === "{}") { 3699 this._textModel.editRange(new WebInspector.TextRange(lineNumber, column - 1, lineNumber, column + 1), ""); 3700 this._mainPanel.setSelection(WebInspector.TextRange.createFromLocation(lineNumber, column - 1)); 3701 return true; 3702 } else 3703 return false; 3704 }, 3705 3706 /** 3707 * @param {string} bracePair 3708 * @return {boolean} 3709 */ 3710 _handleBracePairInsertion: function(bracePair) 3711 { 3712 var selection = this._mainPanel.lastSelection().normalize(); 3713 if (selection.isEmpty()) { 3714 var lineNumber = selection.startLine; 3715 var column = selection.startColumn; 3716 var line = this._textModel.line(lineNumber); 3717 if (column < line.length) { 3718 var char = line.charAt(column); 3719 if (WebInspector.TextUtils.isWordChar(char) || (!WebInspector.TextUtils.isBraceChar(char) && WebInspector.TextUtils.isStopChar(char))) 3720 return false; 3721 } 3722 } 3723 this._textModel.editRange(selection, bracePair); 3724 this._mainPanel.setSelection(WebInspector.TextRange.createFromLocation(selection.startLine, selection.startColumn + 1)); 3725 return true; 3726 }, 3727 3728 /** 3729 * @param {string} brace 3730 * @return {boolean} 3731 */ 3732 _handleClosingBraceOverride: function(brace) 3733 { 3734 var selection = this._mainPanel.lastSelection().normalize(); 3735 if (!selection || !selection.isEmpty()) 3736 return false; 3737 3738 var lineNumber = selection.startLine; 3739 var column = selection.startColumn; 3740 var line = this._textModel.line(lineNumber); 3741 if (line.charAt(column) !== brace) 3742 return false; 3743 3744 var braces = this._braceMatcher.enclosingBraces(lineNumber, column); 3745 if (braces && braces.rightBrace.lineNumber === lineNumber && braces.rightBrace.column === column) { 3746 this._mainPanel.setSelection(WebInspector.TextRange.createFromLocation(lineNumber, column + 1)); 3747 return true; 3748 } else 3749 return false; 3750 }, 3751} 3752 3753WebInspector.debugDefaultTextEditor = false; 3754