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 WebInspector.TextViewer = function(textModel, platform, url, delegate) 33 { 34 WebInspector.View.call(this); 35 36 this._textModel = textModel; 37 this._textModel.changeListener = this._textChanged.bind(this); 38 this._textModel.resetUndoStack(); 39 this._delegate = delegate; 40 41 this.element.className = "text-editor monospace"; 42 43 var enterTextChangeMode = this._enterInternalTextChangeMode.bind(this); 44 var exitTextChangeMode = this._exitInternalTextChangeMode.bind(this); 45 var syncScrollListener = this._syncScroll.bind(this); 46 var syncDecorationsForLineListener = this._syncDecorationsForLine.bind(this); 47 this._mainPanel = new WebInspector.TextEditorMainPanel(this._textModel, url, syncScrollListener, syncDecorationsForLineListener, enterTextChangeMode, exitTextChangeMode); 48 this._gutterPanel = new WebInspector.TextEditorGutterPanel(this._textModel, syncDecorationsForLineListener); 49 this.element.appendChild(this._mainPanel.element); 50 this.element.appendChild(this._gutterPanel.element); 51 52 // Forward mouse wheel events from the unscrollable gutter to the main panel. 53 this._gutterPanel.element.addEventListener("mousewheel", function(e) { 54 this._mainPanel.element.dispatchEvent(e); 55 }.bind(this), false); 56 57 this.element.addEventListener("dblclick", this._doubleClick.bind(this), true); 58 this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false); 59 60 this._registerShortcuts(); 61 } 62 63 WebInspector.TextViewer.prototype = { 64 set mimeType(mimeType) 65 { 66 this._mainPanel.mimeType = mimeType; 67 }, 68 69 set readOnly(readOnly) 70 { 71 if (this._mainPanel.readOnly === readOnly) 72 return; 73 this._mainPanel.readOnly = readOnly; 74 this._delegate.readOnlyStateChanged(readOnly); 75 }, 76 77 get readOnly() 78 { 79 return this._mainPanel.readOnly; 80 }, 81 82 get textModel() 83 { 84 return this._textModel; 85 }, 86 87 revealLine: function(lineNumber) 88 { 89 this._mainPanel.revealLine(lineNumber); 90 }, 91 92 addDecoration: function(lineNumber, decoration) 93 { 94 this._mainPanel.addDecoration(lineNumber, decoration); 95 this._gutterPanel.addDecoration(lineNumber, decoration); 96 }, 97 98 removeDecoration: function(lineNumber, decoration) 99 { 100 this._mainPanel.removeDecoration(lineNumber, decoration); 101 this._gutterPanel.removeDecoration(lineNumber, decoration); 102 }, 103 104 markAndRevealRange: function(range) 105 { 106 this._mainPanel.markAndRevealRange(range); 107 }, 108 109 highlightLine: function(lineNumber) 110 { 111 if (typeof lineNumber !== "number" || lineNumber < 0) 112 return; 113 114 this._mainPanel.highlightLine(lineNumber); 115 }, 116 117 clearLineHighlight: function() 118 { 119 this._mainPanel.clearLineHighlight(); 120 }, 121 122 freeCachedElements: function() 123 { 124 this._mainPanel.freeCachedElements(); 125 this._gutterPanel.freeCachedElements(); 126 }, 127 128 get scrollTop() 129 { 130 return this._mainPanel.element.scrollTop; 131 }, 132 133 set scrollTop(scrollTop) 134 { 135 this._mainPanel.element.scrollTop = scrollTop; 136 }, 137 138 get scrollLeft() 139 { 140 return this._mainPanel.element.scrollLeft; 141 }, 142 143 set scrollLeft(scrollLeft) 144 { 145 this._mainPanel.element.scrollLeft = scrollLeft; 146 }, 147 148 beginUpdates: function() 149 { 150 this._mainPanel.beginUpdates(); 151 this._gutterPanel.beginUpdates(); 152 }, 153 154 endUpdates: function() 155 { 156 this._mainPanel.endUpdates(); 157 this._gutterPanel.endUpdates(); 158 this._updatePanelOffsets(); 159 }, 160 161 resize: function() 162 { 163 this._mainPanel.resize(); 164 this._gutterPanel.resize(); 165 this._updatePanelOffsets(); 166 }, 167 168 // WebInspector.TextModel listener 169 _textChanged: function(oldRange, newRange, oldText, newText) 170 { 171 if (!this._internalTextChangeMode) 172 this._textModel.resetUndoStack(); 173 this._mainPanel.textChanged(oldRange, newRange); 174 this._gutterPanel.textChanged(oldRange, newRange); 175 this._updatePanelOffsets(); 176 }, 177 178 _enterInternalTextChangeMode: function() 179 { 180 this._internalTextChangeMode = true; 181 this._delegate.startEditing(); 182 }, 183 184 _exitInternalTextChangeMode: function(oldRange, newRange) 185 { 186 this._internalTextChangeMode = false; 187 this._delegate.endEditing(oldRange, newRange); 188 }, 189 190 _updatePanelOffsets: function() 191 { 192 var lineNumbersWidth = this._gutterPanel.element.offsetWidth; 193 if (lineNumbersWidth) 194 this._mainPanel.element.style.setProperty("left", lineNumbersWidth + "px"); 195 else 196 this._mainPanel.element.style.removeProperty("left"); // Use default value set in CSS. 197 }, 198 199 _syncScroll: function() 200 { 201 // Async call due to performance reasons. 202 setTimeout(function() { 203 var mainElement = this._mainPanel.element; 204 var gutterElement = this._gutterPanel.element; 205 // Handle horizontal scroll bar at the bottom of the main panel. 206 this._gutterPanel.syncClientHeight(mainElement.clientHeight); 207 gutterElement.scrollTop = mainElement.scrollTop; 208 }.bind(this), 0); 209 }, 210 211 _syncDecorationsForLine: function(lineNumber) 212 { 213 if (lineNumber >= this._textModel.linesCount) 214 return; 215 216 var mainChunk = this._mainPanel.chunkForLine(lineNumber); 217 if (mainChunk.linesCount === 1 && mainChunk.decorated) { 218 var gutterChunk = this._gutterPanel.makeLineAChunk(lineNumber); 219 var height = mainChunk.height; 220 if (height) 221 gutterChunk.element.style.setProperty("height", height + "px"); 222 else 223 gutterChunk.element.style.removeProperty("height"); 224 } else { 225 var gutterChunk = this._gutterPanel.chunkForLine(lineNumber); 226 if (gutterChunk.linesCount === 1) 227 gutterChunk.element.style.removeProperty("height"); 228 } 229 }, 230 231 _doubleClick: function(event) 232 { 233 if (!this.readOnly || this._commitEditingInProgress) 234 return; 235 236 var lineRow = event.target.enclosingNodeOrSelfWithClass("webkit-line-content"); 237 if (!lineRow) 238 return; // Do not trigger editing from line numbers. 239 240 if (!this._delegate.isContentEditable()) 241 return; 242 243 this.readOnly = false; 244 window.getSelection().collapseToStart(); 245 }, 246 247 _registerShortcuts: function() 248 { 249 var keys = WebInspector.KeyboardShortcut.Keys; 250 var modifiers = WebInspector.KeyboardShortcut.Modifiers; 251 252 this._shortcuts = {}; 253 var commitEditing = this._commitEditing.bind(this); 254 var cancelEditing = this._cancelEditing.bind(this); 255 this._shortcuts[WebInspector.KeyboardShortcut.makeKey("s", modifiers.CtrlOrMeta)] = commitEditing; 256 this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Enter.code, modifiers.CtrlOrMeta)] = commitEditing; 257 this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Esc.code)] = cancelEditing; 258 259 var handleUndo = this._mainPanel.handleUndoRedo.bind(this._mainPanel, false); 260 var handleRedo = this._mainPanel.handleUndoRedo.bind(this._mainPanel, true); 261 this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.CtrlOrMeta)] = handleUndo; 262 this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.Shift | modifiers.CtrlOrMeta)] = handleRedo; 263 264 var handleTabKey = this._mainPanel.handleTabKeyPress.bind(this._mainPanel, false); 265 var handleShiftTabKey = this._mainPanel.handleTabKeyPress.bind(this._mainPanel, true); 266 this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code)] = handleTabKey; 267 this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code, modifiers.Shift)] = handleShiftTabKey; 268 }, 269 270 _handleKeyDown: function(e) 271 { 272 var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e); 273 var handler = this._shortcuts[shortcutKey]; 274 if (handler && handler.call(this)) { 275 e.preventDefault(); 276 e.stopPropagation(); 277 } 278 }, 279 280 _commitEditing: function() 281 { 282 if (this.readOnly) 283 return false; 284 285 this.readOnly = true; 286 function didCommitEditing(error) 287 { 288 this._commitEditingInProgress = false; 289 if (error) 290 this.readOnly = false; 291 } 292 this._commitEditingInProgress = true; 293 this._delegate.commitEditing(didCommitEditing.bind(this)); 294 return true; 295 }, 296 297 _cancelEditing: function() 298 { 299 if (this.readOnly) 300 return false; 301 302 this.readOnly = true; 303 this._delegate.cancelEditing(); 304 return true; 305 } 306 } 307 308 WebInspector.TextViewer.prototype.__proto__ = WebInspector.View.prototype; 309 310 WebInspector.TextViewerDelegate = function() 311 { 312 } 313 314 WebInspector.TextViewerDelegate.prototype = { 315 isContentEditable: function() 316 { 317 // Should be implemented by subclasses. 318 }, 319 320 readOnlyStateChanged: function(readOnly) 321 { 322 // Should be implemented by subclasses. 323 }, 324 325 startEditing: function() 326 { 327 // Should be implemented by subclasses. 328 }, 329 330 endEditing: function(oldRange, newRange) 331 { 332 // Should be implemented by subclasses. 333 }, 334 335 commitEditing: function() 336 { 337 // Should be implemented by subclasses. 338 }, 339 340 cancelEditing: function() 341 { 342 // Should be implemented by subclasses. 343 } 344 } 345 346 WebInspector.TextViewerDelegate.prototype.__proto__ = WebInspector.Object.prototype; 347 348 WebInspector.TextEditorChunkedPanel = function(textModel) 349 { 350 this._textModel = textModel; 351 352 this._defaultChunkSize = 50; 353 this._paintCoalescingLevel = 0; 354 this._domUpdateCoalescingLevel = 0; 355 } 356 357 WebInspector.TextEditorChunkedPanel.prototype = { 358 get textModel() 359 { 360 return this._textModel; 361 }, 362 363 revealLine: function(lineNumber) 364 { 365 if (lineNumber >= this._textModel.linesCount) 366 return; 367 368 var chunk = this.makeLineAChunk(lineNumber); 369 chunk.element.scrollIntoViewIfNeeded(); 370 }, 371 372 addDecoration: function(lineNumber, decoration) 373 { 374 if (lineNumber >= this._textModel.linesCount) 375 return; 376 377 var chunk = this.makeLineAChunk(lineNumber); 378 chunk.addDecoration(decoration); 379 }, 380 381 removeDecoration: function(lineNumber, decoration) 382 { 383 if (lineNumber >= this._textModel.linesCount) 384 return; 385 386 var chunk = this.chunkForLine(lineNumber); 387 chunk.removeDecoration(decoration); 388 }, 389 390 _buildChunks: function() 391 { 392 this.beginDomUpdates(); 393 394 this._container.removeChildren(); 395 396 this._textChunks = []; 397 for (var i = 0; i < this._textModel.linesCount; i += this._defaultChunkSize) { 398 var chunk = this._createNewChunk(i, i + this._defaultChunkSize); 399 this._textChunks.push(chunk); 400 this._container.appendChild(chunk.element); 401 } 402 403 this._repaintAll(); 404 405 this.endDomUpdates(); 406 }, 407 408 makeLineAChunk: function(lineNumber) 409 { 410 var chunkNumber = this._chunkNumberForLine(lineNumber); 411 var oldChunk = this._textChunks[chunkNumber]; 412 413 if (!oldChunk) { 414 console.error("No chunk for line number: " + lineNumber); 415 return; 416 } 417 418 if (oldChunk.linesCount === 1) 419 return oldChunk; 420 421 return this._splitChunkOnALine(lineNumber, chunkNumber); 422 }, 423 424 _splitChunkOnALine: function(lineNumber, chunkNumber) 425 { 426 this.beginDomUpdates(); 427 428 var oldChunk = this._textChunks[chunkNumber]; 429 var wasExpanded = oldChunk.expanded; 430 oldChunk.expanded = false; 431 432 var insertIndex = chunkNumber + 1; 433 434 // Prefix chunk. 435 if (lineNumber > oldChunk.startLine) { 436 var prefixChunk = this._createNewChunk(oldChunk.startLine, lineNumber); 437 this._textChunks.splice(insertIndex++, 0, prefixChunk); 438 this._container.insertBefore(prefixChunk.element, oldChunk.element); 439 } 440 441 // Line chunk. 442 var lineChunk = this._createNewChunk(lineNumber, lineNumber + 1); 443 this._textChunks.splice(insertIndex++, 0, lineChunk); 444 this._container.insertBefore(lineChunk.element, oldChunk.element); 445 446 // Suffix chunk. 447 if (oldChunk.startLine + oldChunk.linesCount > lineNumber + 1) { 448 var suffixChunk = this._createNewChunk(lineNumber + 1, oldChunk.startLine + oldChunk.linesCount); 449 this._textChunks.splice(insertIndex, 0, suffixChunk); 450 this._container.insertBefore(suffixChunk.element, oldChunk.element); 451 } 452 453 // Remove enclosing chunk. 454 this._textChunks.splice(chunkNumber, 1); 455 this._container.removeChild(oldChunk.element); 456 457 if (wasExpanded) { 458 if (prefixChunk) 459 prefixChunk.expanded = true; 460 lineChunk.expanded = true; 461 if (suffixChunk) 462 suffixChunk.expanded = true; 463 } 464 465 this.endDomUpdates(); 466 467 return lineChunk; 468 }, 469 470 _scroll: function() 471 { 472 // FIXME: Replace the "2" with the padding-left value from CSS. 473 if (this.element.scrollLeft <= 2) 474 this.element.scrollLeft = 0; 475 476 this._scheduleRepaintAll(); 477 if (this._syncScrollListener) 478 this._syncScrollListener(); 479 }, 480 481 _scheduleRepaintAll: function() 482 { 483 if (this._repaintAllTimer) 484 clearTimeout(this._repaintAllTimer); 485 this._repaintAllTimer = setTimeout(this._repaintAll.bind(this), 50); 486 }, 487 488 beginUpdates: function() 489 { 490 this._paintCoalescingLevel++; 491 }, 492 493 endUpdates: function() 494 { 495 this._paintCoalescingLevel--; 496 if (!this._paintCoalescingLevel) 497 this._repaintAll(); 498 }, 499 500 beginDomUpdates: function() 501 { 502 this._domUpdateCoalescingLevel++; 503 }, 504 505 endDomUpdates: function() 506 { 507 this._domUpdateCoalescingLevel--; 508 }, 509 510 _chunkNumberForLine: function(lineNumber) 511 { 512 function compareLineNumbers(value, chunk) 513 { 514 return value < chunk.startLine ? -1 : 1; 515 } 516 var insertBefore = insertionIndexForObjectInListSortedByFunction(lineNumber, this._textChunks, compareLineNumbers); 517 return insertBefore - 1; 518 }, 519 520 chunkForLine: function(lineNumber) 521 { 522 return this._textChunks[this._chunkNumberForLine(lineNumber)]; 523 }, 524 525 _findFirstVisibleChunkNumber: function(visibleFrom) 526 { 527 function compareOffsetTops(value, chunk) 528 { 529 return value < chunk.offsetTop ? -1 : 1; 530 } 531 var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, this._textChunks, compareOffsetTops); 532 return insertBefore - 1; 533 }, 534 535 _findVisibleChunks: function(visibleFrom, visibleTo) 536 { 537 var from = this._findFirstVisibleChunkNumber(visibleFrom); 538 for (var to = from + 1; to < this._textChunks.length; ++to) { 539 if (this._textChunks[to].offsetTop >= visibleTo) 540 break; 541 } 542 return { start: from, end: to }; 543 }, 544 545 _findFirstVisibleLineNumber: function(visibleFrom) 546 { 547 var chunk = this._textChunks[this._findFirstVisibleChunkNumber(visibleFrom)]; 548 if (!chunk.expanded) 549 return chunk.startLine; 550 551 var lineNumbers = []; 552 for (var i = 0; i < chunk.linesCount; ++i) { 553 lineNumbers.push(chunk.startLine + i); 554 } 555 556 function compareLineRowOffsetTops(value, lineNumber) 557 { 558 var lineRow = chunk.getExpandedLineRow(lineNumber); 559 return value < lineRow.offsetTop ? -1 : 1; 560 } 561 var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, lineNumbers, compareLineRowOffsetTops); 562 return lineNumbers[insertBefore - 1]; 563 }, 564 565 _repaintAll: function() 566 { 567 delete this._repaintAllTimer; 568 569 if (this._paintCoalescingLevel || this._dirtyLines) 570 return; 571 572 var visibleFrom = this.element.scrollTop; 573 var visibleTo = this.element.scrollTop + this.element.clientHeight; 574 575 if (visibleTo) { 576 var result = this._findVisibleChunks(visibleFrom, visibleTo); 577 this._expandChunks(result.start, result.end); 578 } 579 }, 580 581 _expandChunks: function(fromIndex, toIndex) 582 { 583 // First collapse chunks to collect the DOM elements into a cache to reuse them later. 584 for (var i = 0; i < fromIndex; ++i) 585 this._textChunks[i].expanded = false; 586 for (var i = toIndex; i < this._textChunks.length; ++i) 587 this._textChunks[i].expanded = false; 588 for (var i = fromIndex; i < toIndex; ++i) 589 this._textChunks[i].expanded = true; 590 }, 591 592 _totalHeight: function(firstElement, lastElement) 593 { 594 lastElement = (lastElement || firstElement).nextElementSibling; 595 if (lastElement) 596 return lastElement.offsetTop - firstElement.offsetTop; 597 598 var offsetParent = firstElement.offsetParent; 599 if (offsetParent && offsetParent.scrollHeight > offsetParent.clientHeight) 600 return offsetParent.scrollHeight - firstElement.offsetTop; 601 602 var total = 0; 603 while (firstElement && firstElement !== lastElement) { 604 total += firstElement.offsetHeight; 605 firstElement = firstElement.nextElementSibling; 606 } 607 return total; 608 }, 609 610 resize: function() 611 { 612 this._repaintAll(); 613 } 614 } 615 616 WebInspector.TextEditorGutterPanel = function(textModel, syncDecorationsForLineListener) 617 { 618 WebInspector.TextEditorChunkedPanel.call(this, textModel); 619 620 this._syncDecorationsForLineListener = syncDecorationsForLineListener; 621 622 this.element = document.createElement("div"); 623 this.element.className = "text-editor-lines"; 624 625 this._container = document.createElement("div"); 626 this._container.className = "inner-container"; 627 this.element.appendChild(this._container); 628 629 this.element.addEventListener("scroll", this._scroll.bind(this), false); 630 631 this.freeCachedElements(); 632 this._buildChunks(); 633 } 634 635 WebInspector.TextEditorGutterPanel.prototype = { 636 freeCachedElements: function() 637 { 638 this._cachedRows = []; 639 }, 640 641 _createNewChunk: function(startLine, endLine) 642 { 643 return new WebInspector.TextEditorGutterChunk(this, startLine, endLine); 644 }, 645 646 textChanged: function(oldRange, newRange) 647 { 648 this.beginDomUpdates(); 649 650 var linesDiff = newRange.linesCount - oldRange.linesCount; 651 if (linesDiff) { 652 // Remove old chunks (if needed). 653 for (var chunkNumber = this._textChunks.length - 1; chunkNumber >= 0 ; --chunkNumber) { 654 var chunk = this._textChunks[chunkNumber]; 655 if (chunk.startLine + chunk.linesCount <= this._textModel.linesCount) 656 break; 657 chunk.expanded = false; 658 this._container.removeChild(chunk.element); 659 } 660 this._textChunks.length = chunkNumber + 1; 661 662 // Add new chunks (if needed). 663 var totalLines = 0; 664 if (this._textChunks.length) { 665 var lastChunk = this._textChunks[this._textChunks.length - 1]; 666 totalLines = lastChunk.startLine + lastChunk.linesCount; 667 } 668 for (var i = totalLines; i < this._textModel.linesCount; i += this._defaultChunkSize) { 669 var chunk = this._createNewChunk(i, i + this._defaultChunkSize); 670 this._textChunks.push(chunk); 671 this._container.appendChild(chunk.element); 672 } 673 this._repaintAll(); 674 } else { 675 // Decorations may have been removed, so we may have to sync those lines. 676 var chunkNumber = this._chunkNumberForLine(newRange.startLine); 677 var chunk = this._textChunks[chunkNumber]; 678 while (chunk && chunk.startLine <= newRange.endLine) { 679 if (chunk.linesCount === 1) 680 this._syncDecorationsForLineListener(chunk.startLine); 681 chunk = this._textChunks[++chunkNumber]; 682 } 683 } 684 685 this.endDomUpdates(); 686 }, 687 688 syncClientHeight: function(clientHeight) 689 { 690 if (this.element.offsetHeight > clientHeight) 691 this._container.style.setProperty("padding-bottom", (this.element.offsetHeight - clientHeight) + "px"); 692 else 693 this._container.style.removeProperty("padding-bottom"); 694 } 695 } 696 697 WebInspector.TextEditorGutterPanel.prototype.__proto__ = WebInspector.TextEditorChunkedPanel.prototype; 698 699 WebInspector.TextEditorGutterChunk = function(textViewer, startLine, endLine) 700 { 701 this._textViewer = textViewer; 702 this._textModel = textViewer._textModel; 703 704 this.startLine = startLine; 705 endLine = Math.min(this._textModel.linesCount, endLine); 706 this.linesCount = endLine - startLine; 707 708 this._expanded = false; 709 710 this.element = document.createElement("div"); 711 this.element.lineNumber = startLine; 712 this.element.className = "webkit-line-number"; 713 714 if (this.linesCount === 1) { 715 // Single line chunks are typically created for decorations. Host line number in 716 // the sub-element in order to allow flexible border / margin management. 717 var innerSpan = document.createElement("span"); 718 innerSpan.className = "webkit-line-number-inner"; 719 innerSpan.textContent = startLine + 1; 720 var outerSpan = document.createElement("div"); 721 outerSpan.className = "webkit-line-number-outer"; 722 outerSpan.appendChild(innerSpan); 723 this.element.appendChild(outerSpan); 724 } else { 725 var lineNumbers = []; 726 for (var i = startLine; i < endLine; ++i) 727 lineNumbers.push(i + 1); 728 this.element.textContent = lineNumbers.join("\n"); 729 } 730 } 731 732 WebInspector.TextEditorGutterChunk.prototype = { 733 addDecoration: function(decoration) 734 { 735 this._textViewer.beginDomUpdates(); 736 if (typeof decoration === "string") 737 this.element.addStyleClass(decoration); 738 this._textViewer.endDomUpdates(); 739 }, 740 741 removeDecoration: function(decoration) 742 { 743 this._textViewer.beginDomUpdates(); 744 if (typeof decoration === "string") 745 this.element.removeStyleClass(decoration); 746 this._textViewer.endDomUpdates(); 747 }, 748 749 get expanded() 750 { 751 return this._expanded; 752 }, 753 754 set expanded(expanded) 755 { 756 if (this.linesCount === 1) 757 this._textViewer._syncDecorationsForLineListener(this.startLine); 758 759 if (this._expanded === expanded) 760 return; 761 762 this._expanded = expanded; 763 764 if (this.linesCount === 1) 765 return; 766 767 this._textViewer.beginDomUpdates(); 768 769 if (expanded) { 770 this._expandedLineRows = []; 771 var parentElement = this.element.parentElement; 772 for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) { 773 var lineRow = this._createRow(i); 774 parentElement.insertBefore(lineRow, this.element); 775 this._expandedLineRows.push(lineRow); 776 } 777 parentElement.removeChild(this.element); 778 } else { 779 var elementInserted = false; 780 for (var i = 0; i < this._expandedLineRows.length; ++i) { 781 var lineRow = this._expandedLineRows[i]; 782 var parentElement = lineRow.parentElement; 783 if (parentElement) { 784 if (!elementInserted) { 785 elementInserted = true; 786 parentElement.insertBefore(this.element, lineRow); 787 } 788 parentElement.removeChild(lineRow); 789 } 790 this._textViewer._cachedRows.push(lineRow); 791 } 792 delete this._expandedLineRows; 793 } 794 795 this._textViewer.endDomUpdates(); 796 }, 797 798 get height() 799 { 800 if (!this._expandedLineRows) 801 return this._textViewer._totalHeight(this.element); 802 return this._textViewer._totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]); 803 }, 804 805 get offsetTop() 806 { 807 return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop; 808 }, 809 810 _createRow: function(lineNumber) 811 { 812 var lineRow = this._textViewer._cachedRows.pop() || document.createElement("div"); 813 lineRow.lineNumber = lineNumber; 814 lineRow.className = "webkit-line-number"; 815 lineRow.textContent = lineNumber + 1; 816 return lineRow; 817 } 818 } 819 820 WebInspector.TextEditorMainPanel = function(textModel, url, syncScrollListener, syncDecorationsForLineListener, enterTextChangeMode, exitTextChangeMode) 821 { 822 WebInspector.TextEditorChunkedPanel.call(this, textModel); 823 824 this._syncScrollListener = syncScrollListener; 825 this._syncDecorationsForLineListener = syncDecorationsForLineListener; 826 this._enterTextChangeMode = enterTextChangeMode; 827 this._exitTextChangeMode = exitTextChangeMode; 828 829 this._url = url; 830 this._highlighter = new WebInspector.TextEditorHighlighter(textModel, this._highlightDataReady.bind(this)); 831 this._readOnly = true; 832 833 this.element = document.createElement("div"); 834 this.element.className = "text-editor-contents"; 835 this.element.tabIndex = 0; 836 837 this._container = document.createElement("div"); 838 this._container.className = "inner-container"; 839 this._container.tabIndex = 0; 840 this.element.appendChild(this._container); 841 842 this.element.addEventListener("scroll", this._scroll.bind(this), false); 843 844 // In WebKit the DOMNodeRemoved event is fired AFTER the node is removed, thus it should be 845 // attached to all DOM nodes that we want to track. Instead, we attach the DOMNodeRemoved 846 // listeners only on the line rows, and use DOMSubtreeModified to track node removals inside 847 // the line rows. For more info see: https://bugs.webkit.org/show_bug.cgi?id=55666 848 this._handleDOMUpdatesCallback = this._handleDOMUpdates.bind(this); 849 this._container.addEventListener("DOMCharacterDataModified", this._handleDOMUpdatesCallback, false); 850 this._container.addEventListener("DOMNodeInserted", this._handleDOMUpdatesCallback, false); 851 this._container.addEventListener("DOMSubtreeModified", this._handleDOMUpdatesCallback, false); 852 853 this.freeCachedElements(); 854 this._buildChunks(); 855 } 856 857 WebInspector.TextEditorMainPanel.prototype = { 858 set mimeType(mimeType) 859 { 860 this._highlighter.mimeType = mimeType; 861 }, 862 863 set readOnly(readOnly) 864 { 865 if (this._readOnly === readOnly) 866 return; 867 868 this.beginDomUpdates(); 869 this._readOnly = readOnly; 870 if (this._readOnly) 871 this._container.removeStyleClass("text-editor-editable"); 872 else 873 this._container.addStyleClass("text-editor-editable"); 874 this.endDomUpdates(); 875 }, 876 877 get readOnly() 878 { 879 return this._readOnly; 880 }, 881 882 markAndRevealRange: function(range) 883 { 884 if (this._rangeToMark) { 885 var markedLine = this._rangeToMark.startLine; 886 delete this._rangeToMark; 887 // Remove the marked region immediately. 888 if (!this._dirtyLines) { 889 this.beginDomUpdates(); 890 var chunk = this.chunkForLine(markedLine); 891 var wasExpanded = chunk.expanded; 892 chunk.expanded = false; 893 chunk.updateCollapsedLineRow(); 894 chunk.expanded = wasExpanded; 895 this.endDomUpdates(); 896 } else 897 this._paintLines(markedLine, markedLine + 1); 898 } 899 900 if (range) { 901 this._rangeToMark = range; 902 this.revealLine(range.startLine); 903 var chunk = this.makeLineAChunk(range.startLine); 904 this._paintLine(chunk.element); 905 if (this._markedRangeElement) 906 this._markedRangeElement.scrollIntoViewIfNeeded(); 907 } 908 delete this._markedRangeElement; 909 }, 910 911 highlightLine: function(lineNumber) 912 { 913 this.clearLineHighlight(); 914 this._highlightedLine = lineNumber; 915 this.revealLine(lineNumber); 916 this.addDecoration(lineNumber, "webkit-highlighted-line"); 917 }, 918 919 clearLineHighlight: function() 920 { 921 if (typeof this._highlightedLine === "number") { 922 this.removeDecoration(this._highlightedLine, "webkit-highlighted-line"); 923 delete this._highlightedLine; 924 } 925 }, 926 927 freeCachedElements: function() 928 { 929 this._cachedSpans = []; 930 this._cachedTextNodes = []; 931 this._cachedRows = []; 932 }, 933 934 handleUndoRedo: function(redo) 935 { 936 if (this._readOnly || this._dirtyLines) 937 return false; 938 939 this.beginUpdates(); 940 this._enterTextChangeMode(); 941 942 var callback = function(oldRange, newRange) { 943 this._exitTextChangeMode(oldRange, newRange); 944 this._enterTextChangeMode(); 945 }.bind(this); 946 947 var range = redo ? this._textModel.redo(callback) : this._textModel.undo(callback); 948 if (range) 949 this._setCaretLocation(range.endLine, range.endColumn, true); 950 951 this._exitTextChangeMode(null, null); 952 this.endUpdates(); 953 954 return true; 955 }, 956 957 handleTabKeyPress: function(shiftKey) 958 { 959 if (this._readOnly || this._dirtyLines) 960 return false; 961 962 var selection = this._getSelection(); 963 if (!selection) 964 return false; 965 966 if (shiftKey) 967 return true; 968 969 this.beginUpdates(); 970 this._enterTextChangeMode(); 971 972 var range = selection; 973 if (range.startLine > range.endLine || (range.startLine === range.endLine && range.startColumn > range.endColumn)) 974 range = new WebInspector.TextRange(range.endLine, range.endColumn, range.startLine, range.startColumn); 975 976 var newRange = this._setText(range, "\t"); 977 978 this._exitTextChangeMode(range, newRange); 979 this.endUpdates(); 980 981 this._setCaretLocation(newRange.endLine, newRange.endColumn, true); 982 return true; 983 }, 984 985 _splitChunkOnALine: function(lineNumber, chunkNumber) 986 { 987 var selection = this._getSelection(); 988 var chunk = WebInspector.TextEditorChunkedPanel.prototype._splitChunkOnALine.call(this, lineNumber, chunkNumber); 989 this._restoreSelection(selection); 990 return chunk; 991 }, 992 993 _buildChunks: function() 994 { 995 for (var i = 0; i < this._textModel.linesCount; ++i) 996 this._textModel.removeAttribute(i, "highlight"); 997 998 WebInspector.TextEditorChunkedPanel.prototype._buildChunks.call(this); 999 }, 1000 1001 _createNewChunk: function(startLine, endLine) 1002 { 1003 return new WebInspector.TextEditorMainChunk(this, startLine, endLine); 1004 }, 1005 1006 _expandChunks: function(fromIndex, toIndex) 1007 { 1008 var lastChunk = this._textChunks[toIndex - 1]; 1009 var lastVisibleLine = lastChunk.startLine + lastChunk.linesCount; 1010 1011 var selection = this._getSelection(); 1012 1013 this._muteHighlightListener = true; 1014 this._highlighter.highlight(lastVisibleLine); 1015 delete this._muteHighlightListener; 1016 1017 this._restorePaintLinesOperationsCredit(); 1018 WebInspector.TextEditorChunkedPanel.prototype._expandChunks.call(this, fromIndex, toIndex); 1019 this._adjustPaintLinesOperationsRefreshValue(); 1020 1021 this._restoreSelection(selection); 1022 }, 1023 1024 _highlightDataReady: function(fromLine, toLine) 1025 { 1026 if (this._muteHighlightListener) 1027 return; 1028 this._restorePaintLinesOperationsCredit(); 1029 this._paintLines(fromLine, toLine, true /*restoreSelection*/); 1030 }, 1031 1032 _schedulePaintLines: function(startLine, endLine) 1033 { 1034 if (startLine >= endLine) 1035 return; 1036 1037 if (!this._scheduledPaintLines) { 1038 this._scheduledPaintLines = [ { startLine: startLine, endLine: endLine } ]; 1039 this._paintScheduledLinesTimer = setTimeout(this._paintScheduledLines.bind(this), 0); 1040 } else { 1041 for (var i = 0; i < this._scheduledPaintLines.length; ++i) { 1042 var chunk = this._scheduledPaintLines[i]; 1043 if (chunk.startLine <= endLine && chunk.endLine >= startLine) { 1044 chunk.startLine = Math.min(chunk.startLine, startLine); 1045 chunk.endLine = Math.max(chunk.endLine, endLine); 1046 return; 1047 } 1048 if (chunk.startLine > endLine) { 1049 this._scheduledPaintLines.splice(i, 0, { startLine: startLine, endLine: endLine }); 1050 return; 1051 } 1052 } 1053 this._scheduledPaintLines.push({ startLine: startLine, endLine: endLine }); 1054 } 1055 }, 1056 1057 _paintScheduledLines: function(skipRestoreSelection) 1058 { 1059 if (this._paintScheduledLinesTimer) 1060 clearTimeout(this._paintScheduledLinesTimer); 1061 delete this._paintScheduledLinesTimer; 1062 1063 if (!this._scheduledPaintLines) 1064 return; 1065 1066 // Reschedule the timer if we can not paint the lines yet, or the user is scrolling. 1067 if (this._dirtyLines || this._repaintAllTimer) { 1068 this._paintScheduledLinesTimer = setTimeout(this._paintScheduledLines.bind(this), 50); 1069 return; 1070 } 1071 1072 var scheduledPaintLines = this._scheduledPaintLines; 1073 delete this._scheduledPaintLines; 1074 1075 this._restorePaintLinesOperationsCredit(); 1076 this._paintLineChunks(scheduledPaintLines, !skipRestoreSelection); 1077 this._adjustPaintLinesOperationsRefreshValue(); 1078 }, 1079 1080 _restorePaintLinesOperationsCredit: function() 1081 { 1082 if (!this._paintLinesOperationsRefreshValue) 1083 this._paintLinesOperationsRefreshValue = 250; 1084 this._paintLinesOperationsCredit = this._paintLinesOperationsRefreshValue; 1085 this._paintLinesOperationsLastRefresh = Date.now(); 1086 }, 1087 1088 _adjustPaintLinesOperationsRefreshValue: function() 1089 { 1090 var operationsDone = this._paintLinesOperationsRefreshValue - this._paintLinesOperationsCredit; 1091 if (operationsDone <= 0) 1092 return; 1093 var timePast = Date.now() - this._paintLinesOperationsLastRefresh; 1094 if (timePast <= 0) 1095 return; 1096 // Make the synchronous CPU chunk for painting the lines 50 msec. 1097 var value = Math.floor(operationsDone / timePast * 50); 1098 this._paintLinesOperationsRefreshValue = Number.constrain(value, 150, 1500); 1099 }, 1100 1101 _paintLines: function(fromLine, toLine, restoreSelection) 1102 { 1103 this._paintLineChunks([ { startLine: fromLine, endLine: toLine } ], restoreSelection); 1104 }, 1105 1106 _paintLineChunks: function(lineChunks, restoreSelection) 1107 { 1108 // First, paint visible lines, so that in case of long lines we should start highlighting 1109 // the visible area immediately, instead of waiting for the lines above the visible area. 1110 var visibleFrom = this.element.scrollTop; 1111 var firstVisibleLineNumber = this._findFirstVisibleLineNumber(visibleFrom); 1112 1113 var chunk; 1114 var selection; 1115 var invisibleLineRows = []; 1116 for (var i = 0; i < lineChunks.length; ++i) { 1117 var lineChunk = lineChunks[i]; 1118 if (this._dirtyLines || this._scheduledPaintLines) { 1119 this._schedulePaintLines(lineChunk.startLine, lineChunk.endLine); 1120 continue; 1121 } 1122 for (var lineNumber = lineChunk.startLine; lineNumber < lineChunk.endLine; ++lineNumber) { 1123 if (!chunk || lineNumber < chunk.startLine || lineNumber >= chunk.startLine + chunk.linesCount) 1124 chunk = this.chunkForLine(lineNumber); 1125 var lineRow = chunk.getExpandedLineRow(lineNumber); 1126 if (!lineRow) 1127 continue; 1128 if (lineNumber < firstVisibleLineNumber) { 1129 invisibleLineRows.push(lineRow); 1130 continue; 1131 } 1132 if (restoreSelection && !selection) 1133 selection = this._getSelection(); 1134 this._paintLine(lineRow); 1135 if (this._paintLinesOperationsCredit < 0) { 1136 this._schedulePaintLines(lineNumber + 1, lineChunk.endLine); 1137 break; 1138 } 1139 } 1140 } 1141 1142 for (var i = 0; i < invisibleLineRows.length; ++i) { 1143 if (restoreSelection && !selection) 1144 selection = this._getSelection(); 1145 this._paintLine(invisibleLineRows[i]); 1146 } 1147 1148 if (restoreSelection) 1149 this._restoreSelection(selection); 1150 }, 1151 1152 _paintLine: function(lineRow) 1153 { 1154 var lineNumber = lineRow.lineNumber; 1155 if (this._dirtyLines) { 1156 this._schedulePaintLines(lineNumber, lineNumber + 1); 1157 return; 1158 } 1159 1160 this.beginDomUpdates(); 1161 try { 1162 if (this._scheduledPaintLines || this._paintLinesOperationsCredit < 0) { 1163 this._schedulePaintLines(lineNumber, lineNumber + 1); 1164 return; 1165 } 1166 1167 var highlight = this._textModel.getAttribute(lineNumber, "highlight"); 1168 if (!highlight) 1169 return; 1170 1171 lineRow.removeChildren(); 1172 var line = this._textModel.line(lineNumber); 1173 if (!line) 1174 lineRow.appendChild(document.createElement("br")); 1175 1176 var plainTextStart = -1; 1177 for (var j = 0; j < line.length;) { 1178 if (j > 1000) { 1179 // This line is too long - do not waste cycles on minified js highlighting. 1180 if (plainTextStart === -1) 1181 plainTextStart = j; 1182 break; 1183 } 1184 var attribute = highlight[j]; 1185 if (!attribute || !attribute.tokenType) { 1186 if (plainTextStart === -1) 1187 plainTextStart = j; 1188 j++; 1189 } else { 1190 if (plainTextStart !== -1) { 1191 this._appendTextNode(lineRow, line.substring(plainTextStart, j)); 1192 plainTextStart = -1; 1193 --this._paintLinesOperationsCredit; 1194 } 1195 this._appendSpan(lineRow, line.substring(j, j + attribute.length), attribute.tokenType); 1196 j += attribute.length; 1197 --this._paintLinesOperationsCredit; 1198 } 1199 } 1200 if (plainTextStart !== -1) { 1201 this._appendTextNode(lineRow, line.substring(plainTextStart, line.length)); 1202 --this._paintLinesOperationsCredit; 1203 } 1204 if (lineRow.decorationsElement) 1205 lineRow.appendChild(lineRow.decorationsElement); 1206 } finally { 1207 if (this._rangeToMark && this._rangeToMark.startLine === lineNumber) 1208 this._markedRangeElement = highlightSearchResult(lineRow, this._rangeToMark.startColumn, this._rangeToMark.endColumn - this._rangeToMark.startColumn); 1209 this.endDomUpdates(); 1210 } 1211 }, 1212 1213 _releaseLinesHighlight: function(lineRow) 1214 { 1215 if (!lineRow) 1216 return; 1217 if ("spans" in lineRow) { 1218 var spans = lineRow.spans; 1219 for (var j = 0; j < spans.length; ++j) 1220 this._cachedSpans.push(spans[j]); 1221 delete lineRow.spans; 1222 } 1223 if ("textNodes" in lineRow) { 1224 var textNodes = lineRow.textNodes; 1225 for (var j = 0; j < textNodes.length; ++j) 1226 this._cachedTextNodes.push(textNodes[j]); 1227 delete lineRow.textNodes; 1228 } 1229 this._cachedRows.push(lineRow); 1230 }, 1231 1232 _getSelection: function() 1233 { 1234 var selection = window.getSelection(); 1235 if (!selection.rangeCount) 1236 return null; 1237 var selectionRange = selection.getRangeAt(0); 1238 // Selection may be outside of the viewer. 1239 if (!this._container.isAncestor(selectionRange.startContainer) || !this._container.isAncestor(selectionRange.endContainer)) 1240 return null; 1241 var start = this._selectionToPosition(selectionRange.startContainer, selectionRange.startOffset); 1242 var end = selectionRange.collapsed ? start : this._selectionToPosition(selectionRange.endContainer, selectionRange.endOffset); 1243 if (selection.anchorNode === selectionRange.startContainer && selection.anchorOffset === selectionRange.startOffset) 1244 return new WebInspector.TextRange(start.line, start.column, end.line, end.column); 1245 else 1246 return new WebInspector.TextRange(end.line, end.column, start.line, start.column); 1247 }, 1248 1249 _restoreSelection: function(range, scrollIntoView) 1250 { 1251 if (!range) 1252 return; 1253 var start = this._positionToSelection(range.startLine, range.startColumn); 1254 var end = range.isEmpty() ? start : this._positionToSelection(range.endLine, range.endColumn); 1255 window.getSelection().setBaseAndExtent(start.container, start.offset, end.container, end.offset); 1256 1257 if (scrollIntoView) { 1258 for (var node = end.container; node; node = node.parentElement) { 1259 if (node.scrollIntoViewIfNeeded) { 1260 node.scrollIntoViewIfNeeded(); 1261 break; 1262 } 1263 } 1264 } 1265 }, 1266 1267 _setCaretLocation: function(line, column, scrollIntoView) 1268 { 1269 var range = new WebInspector.TextRange(line, column, line, column); 1270 this._restoreSelection(range, scrollIntoView); 1271 }, 1272 1273 _selectionToPosition: function(container, offset) 1274 { 1275 if (container === this._container && offset === 0) 1276 return { line: 0, column: 0 }; 1277 if (container === this._container && offset === 1) 1278 return { line: this._textModel.linesCount - 1, column: this._textModel.lineLength(this._textModel.linesCount - 1) }; 1279 1280 var lineRow = this._enclosingLineRowOrSelf(container); 1281 var lineNumber = lineRow.lineNumber; 1282 if (container === lineRow && offset === 0) 1283 return { line: lineNumber, column: 0 }; 1284 1285 // This may be chunk and chunks may contain \n. 1286 var column = 0; 1287 var node = lineRow.nodeType === Node.TEXT_NODE ? lineRow : lineRow.traverseNextTextNode(lineRow); 1288 while (node && node !== container) { 1289 var text = node.textContent; 1290 for (var i = 0; i < text.length; ++i) { 1291 if (text.charAt(i) === "\n") { 1292 lineNumber++; 1293 column = 0; 1294 } else 1295 column++; 1296 } 1297 node = node.traverseNextTextNode(lineRow); 1298 } 1299 1300 if (node === container && offset) { 1301 var text = node.textContent; 1302 for (var i = 0; i < offset; ++i) { 1303 if (text.charAt(i) === "\n") { 1304 lineNumber++; 1305 column = 0; 1306 } else 1307 column++; 1308 } 1309 } 1310 return { line: lineNumber, column: column }; 1311 }, 1312 1313 _positionToSelection: function(line, column) 1314 { 1315 var chunk = this.chunkForLine(line); 1316 // One-lined collapsed chunks may still stay highlighted. 1317 var lineRow = chunk.linesCount === 1 ? chunk.element : chunk.getExpandedLineRow(line); 1318 if (lineRow) 1319 var rangeBoundary = lineRow.rangeBoundaryForOffset(column); 1320 else { 1321 var offset = column; 1322 for (var i = chunk.startLine; i < line; ++i) 1323 offset += this._textModel.lineLength(i) + 1; // \n 1324 lineRow = chunk.element; 1325 if (lineRow.firstChild) 1326 var rangeBoundary = { container: lineRow.firstChild, offset: offset }; 1327 else 1328 var rangeBoundary = { container: lineRow, offset: 0 }; 1329 } 1330 return rangeBoundary; 1331 }, 1332 1333 _enclosingLineRowOrSelf: function(element) 1334 { 1335 var lineRow = element.enclosingNodeOrSelfWithClass("webkit-line-content"); 1336 if (lineRow) 1337 return lineRow; 1338 for (var lineRow = element; lineRow; lineRow = lineRow.parentElement) { 1339 if (lineRow.parentElement === this._container) 1340 return lineRow; 1341 } 1342 return null; 1343 }, 1344 1345 _appendSpan: function(element, content, className) 1346 { 1347 if (className === "html-resource-link" || className === "html-external-link") { 1348 element.appendChild(this._createLink(content, className === "html-external-link")); 1349 return; 1350 } 1351 1352 var span = this._cachedSpans.pop() || document.createElement("span"); 1353 span.className = "webkit-" + className; 1354 span.textContent = content; 1355 element.appendChild(span); 1356 if (!("spans" in element)) 1357 element.spans = []; 1358 element.spans.push(span); 1359 }, 1360 1361 _appendTextNode: function(element, text) 1362 { 1363 var textNode = this._cachedTextNodes.pop(); 1364 if (textNode) 1365 textNode.nodeValue = text; 1366 else 1367 textNode = document.createTextNode(text); 1368 element.appendChild(textNode); 1369 if (!("textNodes" in element)) 1370 element.textNodes = []; 1371 element.textNodes.push(textNode); 1372 }, 1373 1374 _createLink: function(content, isExternal) 1375 { 1376 var quote = content.charAt(0); 1377 if (content.length > 1 && (quote === "\"" || quote === "'")) 1378 content = content.substring(1, content.length - 1); 1379 else 1380 quote = null; 1381 1382 var a = WebInspector.linkifyURLAsNode(this._rewriteHref(content), content, null, isExternal); 1383 var span = document.createElement("span"); 1384 span.className = "webkit-html-attribute-value"; 1385 if (quote) 1386 span.appendChild(document.createTextNode(quote)); 1387 span.appendChild(a); 1388 if (quote) 1389 span.appendChild(document.createTextNode(quote)); 1390 return span; 1391 }, 1392 1393 _rewriteHref: function(hrefValue, isExternal) 1394 { 1395 if (!this._url || !hrefValue || hrefValue.indexOf("://") > 0) 1396 return hrefValue; 1397 return WebInspector.completeURL(this._url, hrefValue); 1398 }, 1399 1400 _handleDOMUpdates: function(e) 1401 { 1402 if (this._domUpdateCoalescingLevel) 1403 return; 1404 1405 var target = e.target; 1406 if (target === this._container) 1407 return; 1408 1409 var lineRow = this._enclosingLineRowOrSelf(target); 1410 if (!lineRow) 1411 return; 1412 1413 if (lineRow.decorationsElement && (lineRow.decorationsElement === target || lineRow.decorationsElement.isAncestor(target))) { 1414 if (this._syncDecorationsForLineListener) 1415 this._syncDecorationsForLineListener(lineRow.lineNumber); 1416 return; 1417 } 1418 1419 if (this._readOnly) 1420 return; 1421 1422 if (target === lineRow && e.type === "DOMNodeInserted") { 1423 // Ensure that the newly inserted line row has no lineNumber. 1424 delete lineRow.lineNumber; 1425 } 1426 1427 var startLine = 0; 1428 for (var row = lineRow; row; row = row.previousSibling) { 1429 if (typeof row.lineNumber === "number") { 1430 startLine = row.lineNumber; 1431 break; 1432 } 1433 } 1434 1435 var endLine = startLine + 1; 1436 for (var row = lineRow.nextSibling; row; row = row.nextSibling) { 1437 if (typeof row.lineNumber === "number" && row.lineNumber > startLine) { 1438 endLine = row.lineNumber; 1439 break; 1440 } 1441 } 1442 1443 if (target === lineRow && e.type === "DOMNodeRemoved") { 1444 // Now this will no longer be valid. 1445 delete lineRow.lineNumber; 1446 } 1447 1448 if (this._dirtyLines) { 1449 this._dirtyLines.start = Math.min(this._dirtyLines.start, startLine); 1450 this._dirtyLines.end = Math.max(this._dirtyLines.end, endLine); 1451 } else { 1452 this._dirtyLines = { start: startLine, end: endLine }; 1453 setTimeout(this._applyDomUpdates.bind(this), 0); 1454 // Remove marked ranges, if any. 1455 this.markAndRevealRange(null); 1456 } 1457 }, 1458 1459 _applyDomUpdates: function() 1460 { 1461 if (!this._dirtyLines) 1462 return; 1463 1464 // Check if the editor had been set readOnly by the moment when this async callback got executed. 1465 if (this._readOnly) { 1466 delete this._dirtyLines; 1467 return; 1468 } 1469 1470 // This is a "foreign" call outside of this class. Should be before we delete the dirty lines flag. 1471 this._enterTextChangeMode(); 1472 1473 var dirtyLines = this._dirtyLines; 1474 delete this._dirtyLines; 1475 1476 var firstChunkNumber = this._chunkNumberForLine(dirtyLines.start); 1477 var startLine = this._textChunks[firstChunkNumber].startLine; 1478 var endLine = this._textModel.linesCount; 1479 1480 // Collect lines. 1481 var firstLineRow; 1482 if (firstChunkNumber) { 1483 var chunk = this._textChunks[firstChunkNumber - 1]; 1484 firstLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine + chunk.linesCount - 1) : chunk.element; 1485 firstLineRow = firstLineRow.nextSibling; 1486 } else 1487 firstLineRow = this._container.firstChild; 1488 1489 var lines = []; 1490 for (var lineRow = firstLineRow; lineRow; lineRow = lineRow.nextSibling) { 1491 if (typeof lineRow.lineNumber === "number" && lineRow.lineNumber >= dirtyLines.end) { 1492 endLine = lineRow.lineNumber; 1493 break; 1494 } 1495 // Update with the newest lineNumber, so that the call to the _getSelection method below should work. 1496 lineRow.lineNumber = startLine + lines.length; 1497 this._collectLinesFromDiv(lines, lineRow); 1498 } 1499 1500 // Try to decrease the range being replaced, if possible. 1501 var startOffset = 0; 1502 while (startLine < dirtyLines.start && startOffset < lines.length) { 1503 if (this._textModel.line(startLine) !== lines[startOffset]) 1504 break; 1505 ++startOffset; 1506 ++startLine; 1507 } 1508 1509 var endOffset = lines.length; 1510 while (endLine > dirtyLines.end && endOffset > startOffset) { 1511 if (this._textModel.line(endLine - 1) !== lines[endOffset - 1]) 1512 break; 1513 --endOffset; 1514 --endLine; 1515 } 1516 1517 lines = lines.slice(startOffset, endOffset); 1518 1519 // Try to decrease the range being replaced by column offsets, if possible. 1520 var startColumn = 0; 1521 var endColumn = this._textModel.lineLength(endLine - 1); 1522 if (lines.length > 0) { 1523 var line1 = this._textModel.line(startLine); 1524 var line2 = lines[0]; 1525 while (line1[startColumn] && line1[startColumn] === line2[startColumn]) 1526 ++startColumn; 1527 lines[0] = line2.substring(startColumn); 1528 1529 var line1 = this._textModel.line(endLine - 1); 1530 var line2 = lines[lines.length - 1]; 1531 for (var i = 0; i < endColumn && i < line2.length; ++i) { 1532 if (startLine === endLine - 1 && endColumn - i <= startColumn) 1533 break; 1534 if (line1[endColumn - i - 1] !== line2[line2.length - i - 1]) 1535 break; 1536 } 1537 if (i) { 1538 endColumn -= i; 1539 lines[lines.length - 1] = line2.substring(0, line2.length - i); 1540 } 1541 } 1542 1543 var selection = this._getSelection(); 1544 1545 if (lines.length === 0 && endLine < this._textModel.linesCount) 1546 var oldRange = new WebInspector.TextRange(startLine, 0, endLine, 0); 1547 else if (lines.length === 0 && startLine > 0) 1548 var oldRange = new WebInspector.TextRange(startLine - 1, this._textModel.lineLength(startLine - 1), endLine - 1, this._textModel.lineLength(endLine - 1)); 1549 else 1550 var oldRange = new WebInspector.TextRange(startLine, startColumn, endLine - 1, endColumn); 1551 1552 var newRange = this._setText(oldRange, lines.join("\n")); 1553 1554 this._paintScheduledLines(true); 1555 this._restoreSelection(selection); 1556 1557 this._exitTextChangeMode(oldRange, newRange); 1558 }, 1559 1560 textChanged: function(oldRange, newRange) 1561 { 1562 this.beginDomUpdates(); 1563 this._removeDecorationsInRange(oldRange); 1564 this._updateChunksForRanges(oldRange, newRange); 1565 this._updateHighlightsForRange(newRange); 1566 this.endDomUpdates(); 1567 }, 1568 1569 _setText: function(range, text) 1570 { 1571 if (this._lastEditedRange && (!text || text.indexOf("\n") !== -1 || this._lastEditedRange.endLine !== range.startLine || this._lastEditedRange.endColumn !== range.startColumn)) 1572 this._textModel.markUndoableState(); 1573 1574 var newRange = this._textModel.setText(range, text); 1575 this._lastEditedRange = newRange; 1576 1577 return newRange; 1578 }, 1579 1580 _removeDecorationsInRange: function(range) 1581 { 1582 for (var i = this._chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i) { 1583 var chunk = this._textChunks[i]; 1584 if (chunk.startLine > range.endLine) 1585 break; 1586 chunk.removeAllDecorations(); 1587 } 1588 }, 1589 1590 _updateChunksForRanges: function(oldRange, newRange) 1591 { 1592 // Update the chunks in range: firstChunkNumber <= index <= lastChunkNumber 1593 var firstChunkNumber = this._chunkNumberForLine(oldRange.startLine); 1594 var lastChunkNumber = firstChunkNumber; 1595 while (lastChunkNumber + 1 < this._textChunks.length) { 1596 if (this._textChunks[lastChunkNumber + 1].startLine > oldRange.endLine) 1597 break; 1598 ++lastChunkNumber; 1599 } 1600 1601 var startLine = this._textChunks[firstChunkNumber].startLine; 1602 var linesCount = this._textChunks[lastChunkNumber].startLine + this._textChunks[lastChunkNumber].linesCount - startLine; 1603 var linesDiff = newRange.linesCount - oldRange.linesCount; 1604 linesCount += linesDiff; 1605 1606 if (linesDiff) { 1607 // Lines shifted, update the line numbers of the chunks below. 1608 for (var chunkNumber = lastChunkNumber + 1; chunkNumber < this._textChunks.length; ++chunkNumber) 1609 this._textChunks[chunkNumber].startLine += linesDiff; 1610 } 1611 1612 var firstLineRow; 1613 if (firstChunkNumber) { 1614 var chunk = this._textChunks[firstChunkNumber - 1]; 1615 firstLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine + chunk.linesCount - 1) : chunk.element; 1616 firstLineRow = firstLineRow.nextSibling; 1617 } else 1618 firstLineRow = this._container.firstChild; 1619 1620 // Most frequent case: a chunk remained the same. 1621 for (var chunkNumber = firstChunkNumber; chunkNumber <= lastChunkNumber; ++chunkNumber) { 1622 var chunk = this._textChunks[chunkNumber]; 1623 if (chunk.startLine + chunk.linesCount > this._textModel.linesCount) 1624 break; 1625 var lineNumber = chunk.startLine; 1626 for (var lineRow = firstLineRow; lineRow && lineNumber < chunk.startLine + chunk.linesCount; lineRow = lineRow.nextSibling) { 1627 if (lineRow.lineNumber !== lineNumber || lineRow !== chunk.getExpandedLineRow(lineNumber) || lineRow.textContent !== this._textModel.line(lineNumber) || !lineRow.firstChild) 1628 break; 1629 ++lineNumber; 1630 } 1631 if (lineNumber < chunk.startLine + chunk.linesCount) 1632 break; 1633 chunk.updateCollapsedLineRow(); 1634 ++firstChunkNumber; 1635 firstLineRow = lineRow; 1636 startLine += chunk.linesCount; 1637 linesCount -= chunk.linesCount; 1638 } 1639 1640 if (firstChunkNumber > lastChunkNumber && linesCount === 0) 1641 return; 1642 1643 // Maybe merge with the next chunk, so that we should not create 1-sized chunks when appending new lines one by one. 1644 var chunk = this._textChunks[lastChunkNumber + 1]; 1645 var linesInLastChunk = linesCount % this._defaultChunkSize; 1646 if (chunk && !chunk.decorated && linesInLastChunk > 0 && linesInLastChunk + chunk.linesCount <= this._defaultChunkSize) { 1647 ++lastChunkNumber; 1648 linesCount += chunk.linesCount; 1649 } 1650 1651 var scrollTop = this.element.scrollTop; 1652 var scrollLeft = this.element.scrollLeft; 1653 1654 // Delete all DOM elements that were either controlled by the old chunks, or have just been inserted. 1655 var firstUnmodifiedLineRow = null; 1656 var chunk = this._textChunks[lastChunkNumber + 1]; 1657 if (chunk) { 1658 firstUnmodifiedLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine) : chunk.element; 1659 } 1660 while (firstLineRow && firstLineRow !== firstUnmodifiedLineRow) { 1661 var lineRow = firstLineRow; 1662 firstLineRow = firstLineRow.nextSibling; 1663 this._container.removeChild(lineRow); 1664 } 1665 1666 // Replace old chunks with the new ones. 1667 for (var chunkNumber = firstChunkNumber; linesCount > 0; ++chunkNumber) { 1668 var chunkLinesCount = Math.min(this._defaultChunkSize, linesCount); 1669 var newChunk = this._createNewChunk(startLine, startLine + chunkLinesCount); 1670 this._container.insertBefore(newChunk.element, firstUnmodifiedLineRow); 1671 1672 if (chunkNumber <= lastChunkNumber) 1673 this._textChunks[chunkNumber] = newChunk; 1674 else 1675 this._textChunks.splice(chunkNumber, 0, newChunk); 1676 startLine += chunkLinesCount; 1677 linesCount -= chunkLinesCount; 1678 } 1679 if (chunkNumber <= lastChunkNumber) 1680 this._textChunks.splice(chunkNumber, lastChunkNumber - chunkNumber + 1); 1681 1682 this.element.scrollTop = scrollTop; 1683 this.element.scrollLeft = scrollLeft; 1684 }, 1685 1686 _updateHighlightsForRange: function(range) 1687 { 1688 var visibleFrom = this.element.scrollTop; 1689 var visibleTo = this.element.scrollTop + this.element.clientHeight; 1690 1691 var result = this._findVisibleChunks(visibleFrom, visibleTo); 1692 var chunk = this._textChunks[result.end - 1]; 1693 var lastVisibleLine = chunk.startLine + chunk.linesCount; 1694 1695 lastVisibleLine = Math.max(lastVisibleLine, range.endLine + 1); 1696 lastVisibleLine = Math.min(lastVisibleLine, this._textModel.linesCount); 1697 1698 var updated = this._highlighter.updateHighlight(range.startLine, lastVisibleLine); 1699 if (!updated) { 1700 // Highlights for the chunks below are invalid, so just collapse them. 1701 for (var i = this._chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i) 1702 this._textChunks[i].expanded = false; 1703 } 1704 1705 this._repaintAll(); 1706 }, 1707 1708 _collectLinesFromDiv: function(lines, element) 1709 { 1710 var textContents = []; 1711 var node = element.nodeType === Node.TEXT_NODE ? element : element.traverseNextNode(element); 1712 while (node) { 1713 if (element.decorationsElement === node) { 1714 node = node.nextSibling; 1715 continue; 1716 } 1717 if (node.nodeName.toLowerCase() === "br") 1718 textContents.push("\n"); 1719 else if (node.nodeType === Node.TEXT_NODE) 1720 textContents.push(node.textContent); 1721 node = node.traverseNextNode(element); 1722 } 1723 1724 var textContent = textContents.join(""); 1725 // The last \n (if any) does not "count" in a DIV. 1726 textContent = textContent.replace(/\n$/, ""); 1727 1728 textContents = textContent.split("\n"); 1729 for (var i = 0; i < textContents.length; ++i) 1730 lines.push(textContents[i]); 1731 } 1732 } 1733 1734 WebInspector.TextEditorMainPanel.prototype.__proto__ = WebInspector.TextEditorChunkedPanel.prototype; 1735 1736 WebInspector.TextEditorMainChunk = function(textViewer, startLine, endLine) 1737 { 1738 this._textViewer = textViewer; 1739 this._textModel = textViewer._textModel; 1740 1741 this.element = document.createElement("div"); 1742 this.element.lineNumber = startLine; 1743 this.element.className = "webkit-line-content"; 1744 this.element.addEventListener("DOMNodeRemoved", this._textViewer._handleDOMUpdatesCallback, false); 1745 1746 this._startLine = startLine; 1747 endLine = Math.min(this._textModel.linesCount, endLine); 1748 this.linesCount = endLine - startLine; 1749 1750 this._expanded = false; 1751 1752 this.updateCollapsedLineRow(); 1753 } 1754 1755 WebInspector.TextEditorMainChunk.prototype = { 1756 addDecoration: function(decoration) 1757 { 1758 this._textViewer.beginDomUpdates(); 1759 if (typeof decoration === "string") 1760 this.element.addStyleClass(decoration); 1761 else { 1762 if (!this.element.decorationsElement) { 1763 this.element.decorationsElement = document.createElement("div"); 1764 this.element.decorationsElement.className = "webkit-line-decorations"; 1765 this.element.appendChild(this.element.decorationsElement); 1766 } 1767 this.element.decorationsElement.appendChild(decoration); 1768 } 1769 this._textViewer.endDomUpdates(); 1770 }, 1771 1772 removeDecoration: function(decoration) 1773 { 1774 this._textViewer.beginDomUpdates(); 1775 if (typeof decoration === "string") 1776 this.element.removeStyleClass(decoration); 1777 else if (this.element.decorationsElement) 1778 this.element.decorationsElement.removeChild(decoration); 1779 this._textViewer.endDomUpdates(); 1780 }, 1781 1782 removeAllDecorations: function() 1783 { 1784 this._textViewer.beginDomUpdates(); 1785 this.element.className = "webkit-line-content"; 1786 if (this.element.decorationsElement) { 1787 this.element.removeChild(this.element.decorationsElement); 1788 delete this.element.decorationsElement; 1789 } 1790 this._textViewer.endDomUpdates(); 1791 }, 1792 1793 get decorated() 1794 { 1795 return this.element.className !== "webkit-line-content" || !!(this.element.decorationsElement && this.element.decorationsElement.firstChild); 1796 }, 1797 1798 get startLine() 1799 { 1800 return this._startLine; 1801 }, 1802 1803 set startLine(startLine) 1804 { 1805 this._startLine = startLine; 1806 this.element.lineNumber = startLine; 1807 if (this._expandedLineRows) { 1808 for (var i = 0; i < this._expandedLineRows.length; ++i) 1809 this._expandedLineRows[i].lineNumber = startLine + i; 1810 } 1811 }, 1812 1813 get expanded() 1814 { 1815 return this._expanded; 1816 }, 1817 1818 set expanded(expanded) 1819 { 1820 if (this._expanded === expanded) 1821 return; 1822 1823 this._expanded = expanded; 1824 1825 if (this.linesCount === 1) { 1826 if (expanded) 1827 this._textViewer._paintLine(this.element); 1828 return; 1829 } 1830 1831 this._textViewer.beginDomUpdates(); 1832 1833 if (expanded) { 1834 this._expandedLineRows = []; 1835 var parentElement = this.element.parentElement; 1836 for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) { 1837 var lineRow = this._createRow(i); 1838 parentElement.insertBefore(lineRow, this.element); 1839 this._expandedLineRows.push(lineRow); 1840 } 1841 parentElement.removeChild(this.element); 1842 this._textViewer._paintLines(this.startLine, this.startLine + this.linesCount); 1843 } else { 1844 var elementInserted = false; 1845 for (var i = 0; i < this._expandedLineRows.length; ++i) { 1846 var lineRow = this._expandedLineRows[i]; 1847 var parentElement = lineRow.parentElement; 1848 if (parentElement) { 1849 if (!elementInserted) { 1850 elementInserted = true; 1851 parentElement.insertBefore(this.element, lineRow); 1852 } 1853 parentElement.removeChild(lineRow); 1854 } 1855 this._textViewer._releaseLinesHighlight(lineRow); 1856 } 1857 delete this._expandedLineRows; 1858 } 1859 1860 this._textViewer.endDomUpdates(); 1861 }, 1862 1863 get height() 1864 { 1865 if (!this._expandedLineRows) 1866 return this._textViewer._totalHeight(this.element); 1867 return this._textViewer._totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]); 1868 }, 1869 1870 get offsetTop() 1871 { 1872 return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop; 1873 }, 1874 1875 _createRow: function(lineNumber) 1876 { 1877 var lineRow = this._textViewer._cachedRows.pop() || document.createElement("div"); 1878 lineRow.lineNumber = lineNumber; 1879 lineRow.className = "webkit-line-content"; 1880 lineRow.addEventListener("DOMNodeRemoved", this._textViewer._handleDOMUpdatesCallback, false); 1881 lineRow.textContent = this._textModel.line(lineNumber); 1882 if (!lineRow.textContent) 1883 lineRow.appendChild(document.createElement("br")); 1884 return lineRow; 1885 }, 1886 1887 getExpandedLineRow: function(lineNumber) 1888 { 1889 if (!this._expanded || lineNumber < this.startLine || lineNumber >= this.startLine + this.linesCount) 1890 return null; 1891 if (!this._expandedLineRows) 1892 return this.element; 1893 return this._expandedLineRows[lineNumber - this.startLine]; 1894 }, 1895 1896 updateCollapsedLineRow: function() 1897 { 1898 if (this.linesCount === 1 && this._expanded) 1899 return; 1900 1901 var lines = []; 1902 for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) 1903 lines.push(this._textModel.line(i)); 1904 1905 this.element.removeChildren(); 1906 this.element.textContent = lines.join("\n"); 1907 1908 // The last empty line will get swallowed otherwise. 1909 if (!lines[lines.length - 1]) 1910 this.element.appendChild(document.createElement("br")); 1911 } 1912 } 1913