Javascript  |  667行  |  23.73 KB

/*
 * Copyright (C) 2009 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

WebInspector.TextViewer = function(textModel, platform, url)
{
    this._textModel = textModel;
    this._textModel.changeListener = this._buildChunks.bind(this);
    this._highlighter = new WebInspector.TextEditorHighlighter(this._textModel, this._highlightDataReady.bind(this));

    this.element = document.createElement("div");
    this.element.className = "text-editor monospace";
    this.element.tabIndex = 0;

    this.element.addEventListener("scroll", this._scroll.bind(this), false);

    this._url = url;

    this._linesContainerElement = document.createElement("table");
    this._linesContainerElement.className = "text-editor-lines";
    this._linesContainerElement.setAttribute("cellspacing", 0);
    this._linesContainerElement.setAttribute("cellpadding", 0);
    this.element.appendChild(this._linesContainerElement);

    this._defaultChunkSize = 50;
    this._paintCoalescingLevel = 0;
}

WebInspector.TextViewer.prototype = {
    set mimeType(mimeType)
    {
        this._highlighter.mimeType = mimeType;
    },

    get textModel()
    {
        return this._textModel;
    },

    revealLine: function(lineNumber)
    {
        if (lineNumber >= this._textModel.linesCount)
            return;

        var chunk = this._makeLineAChunk(lineNumber);
        chunk.element.scrollIntoViewIfNeeded();
    },

    addDecoration: function(lineNumber, decoration)
    {
        var chunk = this._makeLineAChunk(lineNumber);
        chunk.addDecoration(decoration);
    },

    removeDecoration: function(lineNumber, decoration)
    {
        var chunk = this._makeLineAChunk(lineNumber);
        chunk.removeDecoration(decoration);
    },

    markAndRevealRange: function(range)
    {
        if (this._rangeToMark) {
            var markedLine = this._rangeToMark.startLine;
            this._rangeToMark = null;
            this._paintLines(markedLine, markedLine + 1);
        }

        if (range) {
            this._rangeToMark = range;
            this.revealLine(range.startLine);
            this._paintLines(range.startLine, range.startLine + 1);
        }
    },

    highlightLine: function(lineNumber)
    {
        if (typeof this._highlightedLine === "number") {
            var chunk = this._makeLineAChunk(this._highlightedLine);
            chunk.removeDecoration("webkit-highlighted-line");
        }
        this._highlightedLine = lineNumber;
        this.revealLine(lineNumber);
        var chunk = this._makeLineAChunk(lineNumber);
        chunk.addDecoration("webkit-highlighted-line");
    },

    _buildChunks: function()
    {
        this._linesContainerElement.removeChildren();

        var paintLinesCallback = this._paintLines.bind(this);
        this._textChunks = [];
        for (var i = 0; i < this._textModel.linesCount; i += this._defaultChunkSize) {
            var chunk = new WebInspector.TextChunk(this._textModel, i, i + this._defaultChunkSize, paintLinesCallback);
            this._textChunks.push(chunk);
            this._linesContainerElement.appendChild(chunk.element);
        }
        this._indexChunks();
        this._repaintAll();
    },

    _makeLineAChunk: function(lineNumber)
    {
        if (!this._textChunks)
            this._buildChunks();

        var chunkNumber = this._chunkNumberForLine(lineNumber);
        var oldChunk = this._textChunks[chunkNumber];
        if (oldChunk.linesCount === 1)
            return oldChunk;

        var wasExpanded = oldChunk.expanded;
        oldChunk.expanded = false;

        var insertIndex = oldChunk.chunkNumber + 1;
        var paintLinesCallback = this._paintLines.bind(this);

        // Prefix chunk.
        if (lineNumber > oldChunk.startLine) {
            var prefixChunk = new WebInspector.TextChunk(this._textModel, oldChunk.startLine, lineNumber, paintLinesCallback);
            this._textChunks.splice(insertIndex++, 0, prefixChunk);
            this._linesContainerElement.insertBefore(prefixChunk.element, oldChunk.element);
        }

        // Line chunk.
        var lineChunk = new WebInspector.TextChunk(this._textModel, lineNumber, lineNumber + 1, paintLinesCallback);
        this._textChunks.splice(insertIndex++, 0, lineChunk);
        this._linesContainerElement.insertBefore(lineChunk.element, oldChunk.element);

        // Suffix chunk.
        if (oldChunk.startLine + oldChunk.linesCount > lineNumber + 1) {
            var suffixChunk = new WebInspector.TextChunk(this._textModel, lineNumber + 1, oldChunk.startLine + oldChunk.linesCount, paintLinesCallback);
            this._textChunks.splice(insertIndex, 0, suffixChunk);
            this._linesContainerElement.insertBefore(suffixChunk.element, oldChunk.element);
        }

        // Remove enclosing chunk.
        this._textChunks.splice(oldChunk.chunkNumber, 1);
        this._linesContainerElement.removeChild(oldChunk.element);
        this._indexChunks();

        if (wasExpanded) {
            if (prefixChunk)
                prefixChunk.expanded = true;
            lineChunk.expanded = true;
            if (suffixChunk)
                suffixChunk.expanded = true;
        }

        return lineChunk;
    },

    _indexChunks: function()
    {
        for (var i = 0; i < this._textChunks.length; ++i)
            this._textChunks[i].chunkNumber = i;
    },

    _scroll: function()
    {
        this._repaintAll();
    },

    beginUpdates: function(enabled)
    {
        this._paintCoalescingLevel++;
    },

    endUpdates: function(enabled)
    {
        this._paintCoalescingLevel--;
        if (!this._paintCoalescingLevel)
            this._repaintAll();
    },

    _chunkForOffset: function(offset)
    {
        var currentOffset = 0;
        var row = this._linesContainerElement.firstChild;
        while (row) {
            var rowHeight = row.offsetHeight;
            if (offset >= currentOffset && offset < currentOffset + rowHeight)
                return row.chunkNumber;
            row = row.nextSibling;
            currentOffset += rowHeight;
        }
        return this._textChunks.length - 1;
    },

    _chunkNumberForLine: function(lineNumber)
    {
        for (var i = 0; i < this._textChunks.length; ++i) {
            var line = this._textChunks[i].startLine;
            if (lineNumber >= this._textChunks[i].startLine && lineNumber < this._textChunks[i].startLine + this._textChunks[i].linesCount)
                return i;
        }
        return this._textChunks.length - 1;
    },

    _chunkForLine: function(lineNumber)
    {
        return this._textChunks[this._chunkNumberForLine(lineNumber)];
    },

    _chunkStartLine: function(chunkNumber)
    {
        var lineNumber = 0;
        for (var i = 0; i < chunkNumber && i < this._textChunks.length; ++i)
            lineNumber += this._textChunks[i].linesCount;
        return lineNumber;
    },

    _repaintAll: function()
    {
        if (this._paintCoalescingLevel)
            return;

        if (!this._textChunks)
            this._buildChunks();

        var visibleFrom = this.element.scrollTop;
        var visibleTo = this.element.scrollTop + this.element.clientHeight;

        var offset = 0;
        var firstVisibleLine = -1;
        var lastVisibleLine = 0;
        var toExpand = [];
        var toCollapse = [];
        for (var i = 0; i < this._textChunks.length; ++i) {
            var chunk = this._textChunks[i];
            var chunkHeight = chunk.height;
            if (offset + chunkHeight > visibleFrom && offset < visibleTo) {
                toExpand.push(chunk);
                if (firstVisibleLine === -1)
                    firstVisibleLine = chunk.startLine;
                lastVisibleLine = chunk.startLine + chunk.linesCount;
            } else {
                toCollapse.push(chunk);
                if (offset >= visibleTo)
                    break;
            }
            offset += chunkHeight;
        }

        for (var j = i; j < this._textChunks.length; ++j)
            toCollapse.push(this._textChunks[i]);

        var selection = this._getSelection();

        this._muteHighlightListener = true;
        this._highlighter.highlight(lastVisibleLine);
        delete this._muteHighlightListener;

        for (var i = 0; i < toCollapse.length; ++i)
            toCollapse[i].expanded = false;
        for (var i = 0; i < toExpand.length; ++i)
            toExpand[i].expanded = true;

        this._restoreSelection(selection);
    },

    _highlightDataReady: function(fromLine, toLine)
    {
        if (this._muteHighlightListener)
            return;

        var selection;
        for (var i = fromLine; i < toLine; ++i) {
            var lineRow = this._textModel.getAttribute(i, "line-row");
            if (!lineRow || lineRow.highlighted)
                continue;
            if (!selection)
                selection = this._getSelection();
            this._paintLine(lineRow, i);
        }
        this._restoreSelection(selection);
    },

    _paintLines: function(fromLine, toLine)
    {
        for (var i = fromLine; i < toLine; ++i) {
            var lineRow = this._textModel.getAttribute(i, "line-row");
            if (lineRow)
                this._paintLine(lineRow, i);
        }
    },

    _paintLine: function(lineRow, lineNumber)
    {
        var element = lineRow.lastChild;
        var highlighterState = this._textModel.getAttribute(lineNumber, "highlighter-state");
        var line = this._textModel.line(lineNumber);

        if (!highlighterState) {
            if (this._rangeToMark && this._rangeToMark.startLine === lineNumber)
                this._markRange(element, line, this._rangeToMark.startColumn, this._rangeToMark.endColumn);
            return;
        }

        element.removeChildren();

        var plainTextStart = -1;
        for (var j = 0; j < line.length;) {
            if (j > 1000) {
                // This line is too long - do not waste cycles on minified js highlighting.
                break;
            }
            var attribute = highlighterState && highlighterState.attributes[j];
            if (!attribute || !attribute.style) {
                if (plainTextStart === -1)
                    plainTextStart = j;
                j++;
            } else {
                if (plainTextStart !== -1) {
                    element.appendChild(document.createTextNode(line.substring(plainTextStart, j)));
                    plainTextStart = -1;
                }
                element.appendChild(this._createSpan(line.substring(j, j + attribute.length), attribute.tokenType));
                j += attribute.length;
            }
        }
        if (plainTextStart !== -1)
            element.appendChild(document.createTextNode(line.substring(plainTextStart, line.length)));
        if (this._rangeToMark && this._rangeToMark.startLine === lineNumber)
            this._markRange(element, line, this._rangeToMark.startColumn, this._rangeToMark.endColumn);
        if (lineRow.decorationsElement)
            element.appendChild(lineRow.decorationsElement);
    },

    _getSelection: function()
    {
        var selection = window.getSelection();
        if (selection.isCollapsed)
            return null;
        var selectionRange = selection.getRangeAt(0);
        var start = this._selectionToPosition(selectionRange.startContainer, selectionRange.startOffset);
        var end = this._selectionToPosition(selectionRange.endContainer, selectionRange.endOffset);
        return new WebInspector.TextRange(start.line, start.column, end.line, end.column);
    },

    _restoreSelection: function(range)
    {
        if (!range)
            return;
        var startRow = this._textModel.getAttribute(range.startLine, "line-row");
        if (startRow)
            var start = startRow.lastChild.rangeBoundaryForOffset(range.startColumn);
        else {
            var offset = range.startColumn;
            var chunkNumber = this._chunkNumberForLine(range.startLine);
            for (var i = this._chunkStartLine(chunkNumber); i < range.startLine; ++i)
                offset += this._textModel.line(i).length + 1; // \n
            var lineCell = this._textChunks[chunkNumber].element.lastChild;
            if (lineCell.firstChild)
                var start = { container: lineCell.firstChild, offset: offset };
            else
                var start = { container: lineCell, offset: 0 };
        }

        var endRow = this._textModel.getAttribute(range.endLine, "line-row");
        if (endRow)
            var end = endRow.lastChild.rangeBoundaryForOffset(range.endColumn);
        else {
            var offset = range.endColumn;
            var chunkNumber = this._chunkNumberForLine(range.endLine);
            for (var i = this._chunkStartLine(chunkNumber); i < range.endLine; ++i)
                offset += this._textModel.line(i).length + 1; // \n
            var lineCell = this._textChunks[chunkNumber].element.lastChild;
            if (lineCell.firstChild)
                var end = { container: lineCell.firstChild, offset: offset };
            else
                var end = { container: lineCell, offset: 0 };
        }

        var selectionRange = document.createRange();
        selectionRange.setStart(start.container, start.offset);
        selectionRange.setEnd(end.container, end.offset);

        var selection = window.getSelection();
        selection.removeAllRanges();
        selection.addRange(selectionRange);
    },

    _selectionToPosition: function(container, offset)
    {
        if (container === this.element && offset === 0)
            return { line: 0, column: 0 };
        if (container === this.element && offset === 1)
            return { line: this._textModel.linesCount - 1, column: this._textModel.lineLength(this._textModel.linesCount - 1) };

        var lineRow = container.enclosingNodeOrSelfWithNodeName("tr");
        var lineNumber = lineRow.lineNumber;
        if (container.nodeName === "TD" && offset === 0)
            return { line: lineNumber, column: 0 };
        if (container.nodeName === "TD" && offset === 1)
            return { line: lineNumber, column: this._textModel.lineLength(lineNumber) };

        var column = 0;
        if (lineRow.chunk) {
            // This is chunk.
            var text = lineRow.lastChild.textContent;
            for (var i = 0; i < offset; ++i) {
                if (text.charAt(i) === "\n") {
                    lineNumber++;
                    column = 0;
                } else
                    column++; 
            }
            return { line: lineNumber, column: column };
        }

        // This is individul line.
        var column = 0;
        var node = lineRow.lastChild.traverseNextTextNode(lineRow.lastChild);
        while (node && node !== container) {
            column += node.textContent.length;
            node = node.traverseNextTextNode(lineRow.lastChild);
        }
        column += offset;
        return { line: lineRow.lineNumber, column: column };
    },

    _createSpan: function(content, className)
    {
        if (className === "html-resource-link" || className === "html-external-link")
            return this._createLink(content, className === "html-external-link");

        var span = document.createElement("span");
        span.className = "webkit-" + className;
        span.appendChild(document.createTextNode(content));
        return span;
    },

    _createLink: function(content, isExternal)
    {
        var quote = content.charAt(0);
        if (content.length > 1 && (quote === "\"" ||   quote === "'"))
            content = content.substring(1, content.length - 1);
        else
            quote = null;

        var a = WebInspector.linkifyURLAsNode(this._rewriteHref(content), content, null, isExternal);
        var span = document.createElement("span");
        span.className = "webkit-html-attribute-value";
        if (quote)
            span.appendChild(document.createTextNode(quote));
        span.appendChild(a);
        if (quote)
            span.appendChild(document.createTextNode(quote));
        return span;
    },

    _rewriteHref: function(hrefValue, isExternal)
    {
        if (!this._url || !hrefValue || hrefValue.indexOf("://") > 0)
            return hrefValue;
        return WebInspector.completeURL(this._url, hrefValue);
    },

    _markRange: function(element, lineText, startOffset, endOffset)
    {
        var markNode = document.createElement("span");
        markNode.className = "webkit-markup";
        markNode.textContent = lineText.substring(startOffset, endOffset);

        var markLength = endOffset - startOffset;
        var boundary = element.rangeBoundaryForOffset(startOffset);
        var textNode = boundary.container;
        var text = textNode.textContent;

        if (boundary.offset + markLength < text.length) {
            // Selection belong to a single split mode.
            textNode.textContent = text.substring(boundary.offset + markLength);
            textNode.parentElement.insertBefore(markNode, textNode);
            var prefixNode = document.createTextNode(text.substring(0, boundary.offset));
            textNode.parentElement.insertBefore(prefixNode, markNode);
            return;
        }

        var parentElement = textNode.parentElement;
        var anchorElement = textNode.nextSibling;

        markLength -= text.length - boundary.offset;
        textNode.textContent = text.substring(0, boundary.offset);
        textNode = textNode.traverseNextTextNode(element);

        while (textNode) {
            var text = textNode.textContent;
            if (markLength < text.length) {
                textNode.textContent = text.substring(markLength);
                break;
            }

            markLength -= text.length;
            textNode.textContent = "";
            textNode = textNode.traverseNextTextNode(element);
        }

        parentElement.insertBefore(markNode, anchorElement);
    },

    resize: function()
    {
        this._repaintAll();
    }
}

WebInspector.TextChunk = function(textModel, startLine, endLine, paintLinesCallback)
{
    this.element = document.createElement("tr");
    this._textModel = textModel;
    this.element.chunk = this;
    this.element.lineNumber = startLine;

    this.startLine = startLine;
    endLine = Math.min(this._textModel.linesCount, endLine);
    this.linesCount = endLine - startLine;

    this._lineNumberElement = document.createElement("td");
    this._lineNumberElement.className = "webkit-line-number";
    this._lineNumberElement.textContent = this._lineNumberText(this.startLine);
    this.element.appendChild(this._lineNumberElement);

    this._lineContentElement = document.createElement("td");
    this._lineContentElement.className = "webkit-line-content";
    this.element.appendChild(this._lineContentElement);

    this._expanded = false;

    var lines = [];
    for (var i = this.startLine; i < this.startLine + this.linesCount; ++i)
        lines.push(this._textModel.line(i));
    this._lineContentElement.textContent = lines.join("\n");
    this._paintLines = paintLinesCallback;
}

WebInspector.TextChunk.prototype = {
    addDecoration: function(decoration)
    {
        if (typeof decoration === "string") {
            this.element.addStyleClass(decoration);
            return;
        }
        if (!this.element.decorationsElement) {
            this.element.decorationsElement = document.createElement("div");
            this._lineContentElement.appendChild(this.element.decorationsElement);
        }
        this.element.decorationsElement.appendChild(decoration);
    },

    removeDecoration: function(decoration)
    {
        if (typeof decoration === "string") {
            this.element.removeStyleClass(decoration);
            return;
        }
        if (!this.element.decorationsElement)
            return;
        this.element.decorationsElement.removeChild(decoration);
    },

    get expanded()
    {
        return this._expanded;
    },

    set expanded(expanded)
    {
        if (this._expanded === expanded)
            return;

        this._expanded = expanded;

        if (this.linesCount === 1) {
            this._textModel.setAttribute(this.startLine, "line-row", this.element);
            if (expanded)
                this._paintLines(this.startLine, this.startLine + 1);
            return;
        }

        if (expanded) {
            var parentElement = this.element.parentElement;
            for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
                var lineRow = document.createElement("tr");
                lineRow.lineNumber = i;

                var lineNumberElement = document.createElement("td");
                lineNumberElement.className = "webkit-line-number";
                lineNumberElement.textContent = this._lineNumberText(i);
                lineRow.appendChild(lineNumberElement);

                var lineContentElement = document.createElement("td");
                lineContentElement.className = "webkit-line-content";
                lineContentElement.textContent = this._textModel.line(i);
                lineRow.appendChild(lineContentElement);

                this._textModel.setAttribute(i, "line-row", lineRow);
                parentElement.insertBefore(lineRow, this.element);
            }
            parentElement.removeChild(this.element);

            this._paintLines(this.startLine, this.startLine + this.linesCount);
        } else {
            var firstLine = this._textModel.getAttribute(this.startLine, "line-row");
            var parentElement = firstLine.parentElement;

            parentElement.insertBefore(this.element, firstLine);
            for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
                var lineRow = this._textModel.getAttribute(i, "line-row");
                this._textModel.removeAttribute(i, "line-row");
                parentElement.removeChild(lineRow);
            }
        }
    },

    get height()
    {
        if (!this._expanded)
            return this.element.offsetHeight;
        var result = 0;
        for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
            var lineRow = this._textModel.getAttribute(i, "line-row");
            result += lineRow.offsetHeight;
        }
        return result;
    },

    _lineNumberText: function(lineNumber)
    {
        var totalDigits = Math.ceil(Math.log(this._textModel.linesCount + 1) / Math.log(10));
        var digits = Math.ceil(Math.log(lineNumber + 2) / Math.log(10));

        var text = "";
        for (var i = digits; i < totalDigits; ++i)
            text += " ";
        text += lineNumber + 1;
        return text;
    }
}