TextEditorHighlighter.js   [plain text]


/*
 * Copyright (C) 2009 Google Inc. All rights reserved.
 * Copyright (C) 2009 Apple 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.TextEditorHighlighter = function(textModel, damageCallback)
{
    this._textModel = textModel;

    this._styles = [];

    this._styles["css-comment"] = "rgb(0, 116, 0)";
    this._styles["css-params"] = "rgb(7, 144, 154)";
    this._styles["css-string"] = "rgb(7, 144, 154)";
    this._styles["css-keyword"] = "rgb(7, 144, 154)";
    this._styles["css-number"] = "rgb(50, 0, 255)";
    this._styles["css-property"] = "rgb(200, 0, 0)";
    this._styles["css-at-rule"] = "rgb(200, 0, 0)";
    this._styles["css-selector"] = "rgb(0, 0, 0)";
    this._styles["css-important"] = "rgb(200, 0, 180)";

    /* Keep this in sync with inspector.css and view-source.css */
    this._styles["html-tag"] = "rgb(136, 18, 128)";
    this._styles["html-attr-name"] = "rgb(153, 69, 0)";
    this._styles["html-attr-value"] = "rgb(26, 26, 166)";
    this._styles["html-comment"] = "rgb(35, 110, 37)";
    this._styles["html-doctype"] = "rgb(192, 192, 192)";

    this._styles["javascript-comment"] = "rgb(0, 116, 0)";
    this._styles["javascript-string"] = "rgb(196, 26, 22)";
    this._styles["javascript-regexp"] = "rgb(196, 26, 22)";
    this._styles["javascript-keyword"] = "rgb(170, 13, 145)";
    this._styles["javascript-number"] = "rgb(28, 0, 207)";

    this.mimeType = "text/html";
    this._damageCallback = damageCallback;    
}

WebInspector.TextEditorHighlighter.prototype = {
    set mimeType(mimeType)
    {
        var tokenizer = WebInspector.SourceTokenizer.Registry.getInstance().getTokenizer(mimeType);
        if (tokenizer)
            this._tokenizer = tokenizer;
    },

    highlight: function(endLine)
    {
        // First check if we have work to do.
        var state = this._textModel.getAttribute(endLine - 1, "highlighter-state")
        if (state && !state.outOfDate) {
            // Last line is highlighted, just exit.
            return;
        }

        this._requestedEndLine = endLine;

        if (this._highlightTimer) {
            // There is a timer scheduled, it will catch the new job based on the new endLine set.
            return;
        }

        // We will be highlighting. First rewind to the last highlighted line to gain proper highlighter context.
        var startLine = endLine;
        while (startLine > 0) {
            var state = this._textModel.getAttribute(startLine - 1, "highlighter-state");
            if (state && !state.outOfDate)
                break;
            startLine--;
        }

        // Do small highlight synchronously. This will provide instant highlight on PageUp / PageDown, gentle scrolling.
        var toLine = Math.min(startLine + 200, endLine);
        this._highlightInChunks(startLine, toLine);

        // Schedule tail highlight if necessary.
        if (endLine > toLine)
            this._highlightTimer = setTimeout(this._highlightInChunks.bind(this, toLine, endLine), 100);
    },

    _highlightInChunks: function(startLine, endLine)
    {
        delete this._highlightTimer;

        // First we always check if we have work to do. Could be that user scrolled back and we can quit.
        var state = this._textModel.getAttribute(this._requestedEndLine - 1, "highlighter-state");
        if (state && !state.outOfDate)
            return;

        if (this._requestedEndLine !== endLine) {
            // User keeps updating the job in between of our timer ticks. Just reschedule self, don't eat CPU (they must be scrolling).
            this._highlightTimer = setTimeout(this._highlightInChunks.bind(this, startLine, this._requestedEndLine), 100);
            return;
        }

        // Highlight 500 lines chunk.
        var toLine = Math.min(startLine + 500, this._requestedEndLine);
        this._highlightLines(startLine, toLine);

        // Schedule tail highlight if necessary.
        if (toLine < this._requestedEndLine)
            this._highlightTimer = setTimeout(this._highlightInChunks.bind(this, toLine, this._requestedEndLine), 10);
    },

    updateHighlight: function(startLine, endLine)
    {
        // Start line was edited, we should highlight everything until endLine synchronously.
        if (startLine) {
            var state = this._textModel.getAttribute(startLine - 1, "highlighter-state");
            if (!state || state.outOfDate) {
                // Highlighter did not reach this point yet, nothing to update. It will reach it on subsequent timer tick and do the job.
                return;
            }
        }

        var restored = this._highlightLines(startLine, endLine);

        // Set invalidated flag to the subsequent lines.
        for (var i = endLine; i < this._textModel.linesCount; ++i) {
            var highlighterState = this._textModel.getAttribute(i, "highlighter-state");
            if (highlighterState)
                highlighterState.outOfDate = !restored;
            else
                return;
        }
    },

    _highlightLines: function(startLine, endLine)
    {
        // Restore highlighter context taken from previous line.
        var state = this._textModel.getAttribute(startLine - 1, "highlighter-state");
        if (state)
            this._tokenizer.condition = state.postCondition;
        else
            this._tokenizer.condition = this._tokenizer.initialCondition;

        for (var i = startLine; i < endLine; ++i) {
            state = {};
            state.preCondition = this._tokenizer.condition;
            state.attributes = {};

            this._lex(this._textModel.line(i), i, state.attributes);

            state.postCondition = this._tokenizer.condition;
            this._textModel.setAttribute(i, "highlighter-state", state);

            var nextLineState = this._textModel.getAttribute(i + 1, "highlighter-state");
            if (nextLineState && this._tokenizer.hasCondition(nextLineState.preCondition)) {
                // Following lines are up to date, no need re-highlight.
                this._damageCallback(startLine, i + 1);
                return true;
            }
        }
        this._damageCallback(startLine, endLine);
        return false;
    },

    _lex: function(line, lineNumber, attributes) {
         this._tokenizer.line = line;
         var column = 0;
         do {
             var newColumn = this._tokenizer.nextToken(column);
             var tokenType = this._tokenizer.tokenType;
             if (tokenType)
                 attributes[column] = { length: newColumn - column, style: this._styles[tokenType] };
             column = newColumn;
         } while (column < line.length)
    }
}