1/* 2 * Copyright (C) 2009 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31WebInspector.TextViewer = function(textModel, platform, url) 32{ 33 this._textModel = textModel; 34 this._textModel.changeListener = this._buildChunks.bind(this); 35 this._highlighter = new WebInspector.TextEditorHighlighter(this._textModel, this._highlightDataReady.bind(this)); 36 37 this.element = document.createElement("div"); 38 this.element.className = "text-editor monospace"; 39 this.element.tabIndex = 0; 40 41 this.element.addEventListener("scroll", this._scroll.bind(this), false); 42 43 this._url = url; 44 45 this._linesContainerElement = document.createElement("table"); 46 this._linesContainerElement.className = "text-editor-lines"; 47 this._linesContainerElement.setAttribute("cellspacing", 0); 48 this._linesContainerElement.setAttribute("cellpadding", 0); 49 this.element.appendChild(this._linesContainerElement); 50 51 this._defaultChunkSize = 50; 52 this._paintCoalescingLevel = 0; 53} 54 55WebInspector.TextViewer.prototype = { 56 set mimeType(mimeType) 57 { 58 this._highlighter.mimeType = mimeType; 59 }, 60 61 get textModel() 62 { 63 return this._textModel; 64 }, 65 66 revealLine: function(lineNumber) 67 { 68 if (lineNumber >= this._textModel.linesCount) 69 return; 70 71 var chunk = this._makeLineAChunk(lineNumber); 72 chunk.element.scrollIntoViewIfNeeded(); 73 }, 74 75 addDecoration: function(lineNumber, decoration) 76 { 77 var chunk = this._makeLineAChunk(lineNumber); 78 chunk.addDecoration(decoration); 79 }, 80 81 removeDecoration: function(lineNumber, decoration) 82 { 83 var chunk = this._makeLineAChunk(lineNumber); 84 chunk.removeDecoration(decoration); 85 }, 86 87 markAndRevealRange: function(range) 88 { 89 if (this._rangeToMark) { 90 var markedLine = this._rangeToMark.startLine; 91 this._rangeToMark = null; 92 this._paintLines(markedLine, markedLine + 1); 93 } 94 95 if (range) { 96 this._rangeToMark = range; 97 this.revealLine(range.startLine); 98 this._paintLines(range.startLine, range.startLine + 1); 99 } 100 }, 101 102 highlightLine: function(lineNumber) 103 { 104 if (typeof this._highlightedLine === "number") { 105 var chunk = this._makeLineAChunk(this._highlightedLine); 106 chunk.removeDecoration("webkit-highlighted-line"); 107 } 108 this._highlightedLine = lineNumber; 109 this.revealLine(lineNumber); 110 var chunk = this._makeLineAChunk(lineNumber); 111 chunk.addDecoration("webkit-highlighted-line"); 112 }, 113 114 _buildChunks: function() 115 { 116 this._linesContainerElement.removeChildren(); 117 118 var paintLinesCallback = this._paintLines.bind(this); 119 this._textChunks = []; 120 for (var i = 0; i < this._textModel.linesCount; i += this._defaultChunkSize) { 121 var chunk = new WebInspector.TextChunk(this._textModel, i, i + this._defaultChunkSize, paintLinesCallback); 122 this._textChunks.push(chunk); 123 this._linesContainerElement.appendChild(chunk.element); 124 } 125 this._indexChunks(); 126 this._repaintAll(); 127 }, 128 129 _makeLineAChunk: function(lineNumber) 130 { 131 if (!this._textChunks) 132 this._buildChunks(); 133 134 var chunkNumber = this._chunkNumberForLine(lineNumber); 135 var oldChunk = this._textChunks[chunkNumber]; 136 if (oldChunk.linesCount === 1) 137 return oldChunk; 138 139 var wasExpanded = oldChunk.expanded; 140 oldChunk.expanded = false; 141 142 var insertIndex = oldChunk.chunkNumber + 1; 143 var paintLinesCallback = this._paintLines.bind(this); 144 145 // Prefix chunk. 146 if (lineNumber > oldChunk.startLine) { 147 var prefixChunk = new WebInspector.TextChunk(this._textModel, oldChunk.startLine, lineNumber, paintLinesCallback); 148 this._textChunks.splice(insertIndex++, 0, prefixChunk); 149 this._linesContainerElement.insertBefore(prefixChunk.element, oldChunk.element); 150 } 151 152 // Line chunk. 153 var lineChunk = new WebInspector.TextChunk(this._textModel, lineNumber, lineNumber + 1, paintLinesCallback); 154 this._textChunks.splice(insertIndex++, 0, lineChunk); 155 this._linesContainerElement.insertBefore(lineChunk.element, oldChunk.element); 156 157 // Suffix chunk. 158 if (oldChunk.startLine + oldChunk.linesCount > lineNumber + 1) { 159 var suffixChunk = new WebInspector.TextChunk(this._textModel, lineNumber + 1, oldChunk.startLine + oldChunk.linesCount, paintLinesCallback); 160 this._textChunks.splice(insertIndex, 0, suffixChunk); 161 this._linesContainerElement.insertBefore(suffixChunk.element, oldChunk.element); 162 } 163 164 // Remove enclosing chunk. 165 this._textChunks.splice(oldChunk.chunkNumber, 1); 166 this._linesContainerElement.removeChild(oldChunk.element); 167 this._indexChunks(); 168 169 if (wasExpanded) { 170 if (prefixChunk) 171 prefixChunk.expanded = true; 172 lineChunk.expanded = true; 173 if (suffixChunk) 174 suffixChunk.expanded = true; 175 } 176 177 return lineChunk; 178 }, 179 180 _indexChunks: function() 181 { 182 for (var i = 0; i < this._textChunks.length; ++i) 183 this._textChunks[i].chunkNumber = i; 184 }, 185 186 _scroll: function() 187 { 188 this._repaintAll(); 189 }, 190 191 beginUpdates: function(enabled) 192 { 193 this._paintCoalescingLevel++; 194 }, 195 196 endUpdates: function(enabled) 197 { 198 this._paintCoalescingLevel--; 199 if (!this._paintCoalescingLevel) 200 this._repaintAll(); 201 }, 202 203 _chunkForOffset: function(offset) 204 { 205 var currentOffset = 0; 206 var row = this._linesContainerElement.firstChild; 207 while (row) { 208 var rowHeight = row.offsetHeight; 209 if (offset >= currentOffset && offset < currentOffset + rowHeight) 210 return row.chunkNumber; 211 row = row.nextSibling; 212 currentOffset += rowHeight; 213 } 214 return this._textChunks.length - 1; 215 }, 216 217 _chunkNumberForLine: function(lineNumber) 218 { 219 for (var i = 0; i < this._textChunks.length; ++i) { 220 var line = this._textChunks[i].startLine; 221 if (lineNumber >= this._textChunks[i].startLine && lineNumber < this._textChunks[i].startLine + this._textChunks[i].linesCount) 222 return i; 223 } 224 return this._textChunks.length - 1; 225 }, 226 227 _chunkForLine: function(lineNumber) 228 { 229 return this._textChunks[this._chunkNumberForLine(lineNumber)]; 230 }, 231 232 _chunkStartLine: function(chunkNumber) 233 { 234 var lineNumber = 0; 235 for (var i = 0; i < chunkNumber && i < this._textChunks.length; ++i) 236 lineNumber += this._textChunks[i].linesCount; 237 return lineNumber; 238 }, 239 240 _repaintAll: function() 241 { 242 if (this._paintCoalescingLevel) 243 return; 244 245 if (!this._textChunks) 246 this._buildChunks(); 247 248 var visibleFrom = this.element.scrollTop; 249 var visibleTo = this.element.scrollTop + this.element.clientHeight; 250 251 var offset = 0; 252 var firstVisibleLine = -1; 253 var lastVisibleLine = 0; 254 var toExpand = []; 255 var toCollapse = []; 256 for (var i = 0; i < this._textChunks.length; ++i) { 257 var chunk = this._textChunks[i]; 258 var chunkHeight = chunk.height; 259 if (offset + chunkHeight > visibleFrom && offset < visibleTo) { 260 toExpand.push(chunk); 261 if (firstVisibleLine === -1) 262 firstVisibleLine = chunk.startLine; 263 lastVisibleLine = chunk.startLine + chunk.linesCount; 264 } else { 265 toCollapse.push(chunk); 266 if (offset >= visibleTo) 267 break; 268 } 269 offset += chunkHeight; 270 } 271 272 for (var j = i; j < this._textChunks.length; ++j) 273 toCollapse.push(this._textChunks[i]); 274 275 var selection = this._getSelection(); 276 277 this._muteHighlightListener = true; 278 this._highlighter.highlight(lastVisibleLine); 279 delete this._muteHighlightListener; 280 281 for (var i = 0; i < toCollapse.length; ++i) 282 toCollapse[i].expanded = false; 283 for (var i = 0; i < toExpand.length; ++i) 284 toExpand[i].expanded = true; 285 286 this._restoreSelection(selection); 287 }, 288 289 _highlightDataReady: function(fromLine, toLine) 290 { 291 if (this._muteHighlightListener) 292 return; 293 294 var selection; 295 for (var i = fromLine; i < toLine; ++i) { 296 var lineRow = this._textModel.getAttribute(i, "line-row"); 297 if (!lineRow || lineRow.highlighted) 298 continue; 299 if (!selection) 300 selection = this._getSelection(); 301 this._paintLine(lineRow, i); 302 } 303 this._restoreSelection(selection); 304 }, 305 306 _paintLines: function(fromLine, toLine) 307 { 308 for (var i = fromLine; i < toLine; ++i) { 309 var lineRow = this._textModel.getAttribute(i, "line-row"); 310 if (lineRow) 311 this._paintLine(lineRow, i); 312 } 313 }, 314 315 _paintLine: function(lineRow, lineNumber) 316 { 317 var element = lineRow.lastChild; 318 var highlighterState = this._textModel.getAttribute(lineNumber, "highlighter-state"); 319 var line = this._textModel.line(lineNumber); 320 321 if (!highlighterState) { 322 if (this._rangeToMark && this._rangeToMark.startLine === lineNumber) 323 this._markRange(element, line, this._rangeToMark.startColumn, this._rangeToMark.endColumn); 324 return; 325 } 326 327 element.removeChildren(); 328 329 var plainTextStart = -1; 330 for (var j = 0; j < line.length;) { 331 if (j > 1000) { 332 // This line is too long - do not waste cycles on minified js highlighting. 333 break; 334 } 335 var attribute = highlighterState && highlighterState.attributes[j]; 336 if (!attribute || !attribute.style) { 337 if (plainTextStart === -1) 338 plainTextStart = j; 339 j++; 340 } else { 341 if (plainTextStart !== -1) { 342 element.appendChild(document.createTextNode(line.substring(plainTextStart, j))); 343 plainTextStart = -1; 344 } 345 element.appendChild(this._createSpan(line.substring(j, j + attribute.length), attribute.tokenType)); 346 j += attribute.length; 347 } 348 } 349 if (plainTextStart !== -1) 350 element.appendChild(document.createTextNode(line.substring(plainTextStart, line.length))); 351 if (this._rangeToMark && this._rangeToMark.startLine === lineNumber) 352 this._markRange(element, line, this._rangeToMark.startColumn, this._rangeToMark.endColumn); 353 if (lineRow.decorationsElement) 354 element.appendChild(lineRow.decorationsElement); 355 }, 356 357 _getSelection: function() 358 { 359 var selection = window.getSelection(); 360 if (selection.isCollapsed) 361 return null; 362 var selectionRange = selection.getRangeAt(0); 363 var start = this._selectionToPosition(selectionRange.startContainer, selectionRange.startOffset); 364 var end = this._selectionToPosition(selectionRange.endContainer, selectionRange.endOffset); 365 return new WebInspector.TextRange(start.line, start.column, end.line, end.column); 366 }, 367 368 _restoreSelection: function(range) 369 { 370 if (!range) 371 return; 372 var startRow = this._textModel.getAttribute(range.startLine, "line-row"); 373 if (startRow) 374 var start = startRow.lastChild.rangeBoundaryForOffset(range.startColumn); 375 else { 376 var offset = range.startColumn; 377 var chunkNumber = this._chunkNumberForLine(range.startLine); 378 for (var i = this._chunkStartLine(chunkNumber); i < range.startLine; ++i) 379 offset += this._textModel.line(i).length + 1; // \n 380 var lineCell = this._textChunks[chunkNumber].element.lastChild; 381 if (lineCell.firstChild) 382 var start = { container: lineCell.firstChild, offset: offset }; 383 else 384 var start = { container: lineCell, offset: 0 }; 385 } 386 387 var endRow = this._textModel.getAttribute(range.endLine, "line-row"); 388 if (endRow) 389 var end = endRow.lastChild.rangeBoundaryForOffset(range.endColumn); 390 else { 391 var offset = range.endColumn; 392 var chunkNumber = this._chunkNumberForLine(range.endLine); 393 for (var i = this._chunkStartLine(chunkNumber); i < range.endLine; ++i) 394 offset += this._textModel.line(i).length + 1; // \n 395 var lineCell = this._textChunks[chunkNumber].element.lastChild; 396 if (lineCell.firstChild) 397 var end = { container: lineCell.firstChild, offset: offset }; 398 else 399 var end = { container: lineCell, offset: 0 }; 400 } 401 402 var selectionRange = document.createRange(); 403 selectionRange.setStart(start.container, start.offset); 404 selectionRange.setEnd(end.container, end.offset); 405 406 var selection = window.getSelection(); 407 selection.removeAllRanges(); 408 selection.addRange(selectionRange); 409 }, 410 411 _selectionToPosition: function(container, offset) 412 { 413 if (container === this.element && offset === 0) 414 return { line: 0, column: 0 }; 415 if (container === this.element && offset === 1) 416 return { line: this._textModel.linesCount - 1, column: this._textModel.lineLength(this._textModel.linesCount - 1) }; 417 418 var lineRow = container.enclosingNodeOrSelfWithNodeName("tr"); 419 var lineNumber = lineRow.lineNumber; 420 if (container.nodeName === "TD" && offset === 0) 421 return { line: lineNumber, column: 0 }; 422 if (container.nodeName === "TD" && offset === 1) 423 return { line: lineNumber, column: this._textModel.lineLength(lineNumber) }; 424 425 var column = 0; 426 if (lineRow.chunk) { 427 // This is chunk. 428 var text = lineRow.lastChild.textContent; 429 for (var i = 0; i < offset; ++i) { 430 if (text.charAt(i) === "\n") { 431 lineNumber++; 432 column = 0; 433 } else 434 column++; 435 } 436 return { line: lineNumber, column: column }; 437 } 438 439 // This is individul line. 440 var column = 0; 441 var node = lineRow.lastChild.traverseNextTextNode(lineRow.lastChild); 442 while (node && node !== container) { 443 column += node.textContent.length; 444 node = node.traverseNextTextNode(lineRow.lastChild); 445 } 446 column += offset; 447 return { line: lineRow.lineNumber, column: column }; 448 }, 449 450 _createSpan: function(content, className) 451 { 452 if (className === "html-resource-link" || className === "html-external-link") 453 return this._createLink(content, className === "html-external-link"); 454 455 var span = document.createElement("span"); 456 span.className = "webkit-" + className; 457 span.appendChild(document.createTextNode(content)); 458 return span; 459 }, 460 461 _createLink: function(content, isExternal) 462 { 463 var quote = content.charAt(0); 464 if (content.length > 1 && (quote === "\"" || quote === "'")) 465 content = content.substring(1, content.length - 1); 466 else 467 quote = null; 468 469 var a = WebInspector.linkifyURLAsNode(this._rewriteHref(content), content, null, isExternal); 470 var span = document.createElement("span"); 471 span.className = "webkit-html-attribute-value"; 472 if (quote) 473 span.appendChild(document.createTextNode(quote)); 474 span.appendChild(a); 475 if (quote) 476 span.appendChild(document.createTextNode(quote)); 477 return span; 478 }, 479 480 _rewriteHref: function(hrefValue, isExternal) 481 { 482 if (!this._url || !hrefValue || hrefValue.indexOf("://") > 0) 483 return hrefValue; 484 return WebInspector.completeURL(this._url, hrefValue); 485 }, 486 487 _markRange: function(element, lineText, startOffset, endOffset) 488 { 489 var markNode = document.createElement("span"); 490 markNode.className = "webkit-markup"; 491 markNode.textContent = lineText.substring(startOffset, endOffset); 492 493 var markLength = endOffset - startOffset; 494 var boundary = element.rangeBoundaryForOffset(startOffset); 495 var textNode = boundary.container; 496 var text = textNode.textContent; 497 498 if (boundary.offset + markLength < text.length) { 499 // Selection belong to a single split mode. 500 textNode.textContent = text.substring(boundary.offset + markLength); 501 textNode.parentElement.insertBefore(markNode, textNode); 502 var prefixNode = document.createTextNode(text.substring(0, boundary.offset)); 503 textNode.parentElement.insertBefore(prefixNode, markNode); 504 return; 505 } 506 507 var parentElement = textNode.parentElement; 508 var anchorElement = textNode.nextSibling; 509 510 markLength -= text.length - boundary.offset; 511 textNode.textContent = text.substring(0, boundary.offset); 512 textNode = textNode.traverseNextTextNode(element); 513 514 while (textNode) { 515 var text = textNode.textContent; 516 if (markLength < text.length) { 517 textNode.textContent = text.substring(markLength); 518 break; 519 } 520 521 markLength -= text.length; 522 textNode.textContent = ""; 523 textNode = textNode.traverseNextTextNode(element); 524 } 525 526 parentElement.insertBefore(markNode, anchorElement); 527 }, 528 529 resize: function() 530 { 531 this._repaintAll(); 532 } 533} 534 535WebInspector.TextChunk = function(textModel, startLine, endLine, paintLinesCallback) 536{ 537 this.element = document.createElement("tr"); 538 this._textModel = textModel; 539 this.element.chunk = this; 540 this.element.lineNumber = startLine; 541 542 this.startLine = startLine; 543 endLine = Math.min(this._textModel.linesCount, endLine); 544 this.linesCount = endLine - startLine; 545 546 this._lineNumberElement = document.createElement("td"); 547 this._lineNumberElement.className = "webkit-line-number"; 548 this._lineNumberElement.textContent = this._lineNumberText(this.startLine); 549 this.element.appendChild(this._lineNumberElement); 550 551 this._lineContentElement = document.createElement("td"); 552 this._lineContentElement.className = "webkit-line-content"; 553 this.element.appendChild(this._lineContentElement); 554 555 this._expanded = false; 556 557 var lines = []; 558 for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) 559 lines.push(this._textModel.line(i)); 560 this._lineContentElement.textContent = lines.join("\n"); 561 this._paintLines = paintLinesCallback; 562} 563 564WebInspector.TextChunk.prototype = { 565 addDecoration: function(decoration) 566 { 567 if (typeof decoration === "string") { 568 this.element.addStyleClass(decoration); 569 return; 570 } 571 if (!this.element.decorationsElement) { 572 this.element.decorationsElement = document.createElement("div"); 573 this._lineContentElement.appendChild(this.element.decorationsElement); 574 } 575 this.element.decorationsElement.appendChild(decoration); 576 }, 577 578 removeDecoration: function(decoration) 579 { 580 if (typeof decoration === "string") { 581 this.element.removeStyleClass(decoration); 582 return; 583 } 584 if (!this.element.decorationsElement) 585 return; 586 this.element.decorationsElement.removeChild(decoration); 587 }, 588 589 get expanded() 590 { 591 return this._expanded; 592 }, 593 594 set expanded(expanded) 595 { 596 if (this._expanded === expanded) 597 return; 598 599 this._expanded = expanded; 600 601 if (this.linesCount === 1) { 602 this._textModel.setAttribute(this.startLine, "line-row", this.element); 603 if (expanded) 604 this._paintLines(this.startLine, this.startLine + 1); 605 return; 606 } 607 608 if (expanded) { 609 var parentElement = this.element.parentElement; 610 for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) { 611 var lineRow = document.createElement("tr"); 612 lineRow.lineNumber = i; 613 614 var lineNumberElement = document.createElement("td"); 615 lineNumberElement.className = "webkit-line-number"; 616 lineNumberElement.textContent = this._lineNumberText(i); 617 lineRow.appendChild(lineNumberElement); 618 619 var lineContentElement = document.createElement("td"); 620 lineContentElement.className = "webkit-line-content"; 621 lineContentElement.textContent = this._textModel.line(i); 622 lineRow.appendChild(lineContentElement); 623 624 this._textModel.setAttribute(i, "line-row", lineRow); 625 parentElement.insertBefore(lineRow, this.element); 626 } 627 parentElement.removeChild(this.element); 628 629 this._paintLines(this.startLine, this.startLine + this.linesCount); 630 } else { 631 var firstLine = this._textModel.getAttribute(this.startLine, "line-row"); 632 var parentElement = firstLine.parentElement; 633 634 parentElement.insertBefore(this.element, firstLine); 635 for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) { 636 var lineRow = this._textModel.getAttribute(i, "line-row"); 637 this._textModel.removeAttribute(i, "line-row"); 638 parentElement.removeChild(lineRow); 639 } 640 } 641 }, 642 643 get height() 644 { 645 if (!this._expanded) 646 return this.element.offsetHeight; 647 var result = 0; 648 for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) { 649 var lineRow = this._textModel.getAttribute(i, "line-row"); 650 result += lineRow.offsetHeight; 651 } 652 return result; 653 }, 654 655 _lineNumberText: function(lineNumber) 656 { 657 var totalDigits = Math.ceil(Math.log(this._textModel.linesCount + 1) / Math.log(10)); 658 var digits = Math.ceil(Math.log(lineNumber + 2) / Math.log(10)); 659 660 var text = ""; 661 for (var i = digits; i < totalDigits; ++i) 662 text += " "; 663 text += lineNumber + 1; 664 return text; 665 } 666} 667