• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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