Home | History | Annotate | Download | only in front_end
      1 /*
      2  * Copyright (C) 2009 Google Inc. All rights reserved.
      3  * Copyright (C) 2009 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 /**
     33  * @constructor
     34  */
     35 WebInspector.TextEditorHighlighter = function(textModel, damageCallback)
     36 {
     37     this._textModel = textModel;
     38     this._mimeType = "text/html";
     39     this._tokenizer = WebInspector.SourceTokenizer.Registry.getInstance().getTokenizer(this._mimeType);
     40     this._damageCallback = damageCallback;
     41     this._highlightChunkLimit = 1000;
     42     this._highlightLineLimit = 500;
     43 }
     44 
     45 WebInspector.TextEditorHighlighter._MaxLineCount = 10000;
     46 
     47 WebInspector.TextEditorHighlighter.prototype = {
     48 
     49     get mimeType()
     50     {
     51         return this._mimeType;
     52     },
     53 
     54     /**
     55      * @param {string} mimeType
     56      */
     57     set mimeType(mimeType)
     58     {
     59         var tokenizer = WebInspector.SourceTokenizer.Registry.getInstance().getTokenizer(mimeType);
     60         if (tokenizer) {
     61             this._tokenizer = tokenizer;
     62             this._mimeType = mimeType;
     63         }
     64     },
     65 
     66     set highlightChunkLimit(highlightChunkLimit)
     67     {
     68         this._highlightChunkLimit = highlightChunkLimit;
     69     },
     70 
     71     /**
     72      * @param {number} highlightLineLimit
     73      */
     74     setHighlightLineLimit: function(highlightLineLimit)
     75     {
     76         this._highlightLineLimit = highlightLineLimit;
     77     },
     78 
     79     /**
     80      * @param {boolean=} forceRun
     81      */
     82     highlight: function(endLine, forceRun)
     83     {
     84         if (this._textModel.linesCount > WebInspector.TextEditorHighlighter._MaxLineCount)
     85             return;
     86 
     87         // First check if we have work to do.
     88         var state = this._textModel.getAttribute(endLine - 1, "highlight");
     89         if (state && state.postConditionStringified) {
     90             // Last line is highlighted, just exit.
     91             return;
     92         }
     93 
     94         this._requestedEndLine = endLine;
     95 
     96         if (this._highlightTimer && !forceRun) {
     97             // There is a timer scheduled, it will catch the new job based on the new endLine set.
     98             return;
     99         }
    100 
    101         // We will be highlighting. First rewind to the last highlighted line to gain proper highlighter context.
    102         var startLine = endLine;
    103         while (startLine > 0) {
    104             state = this._textModel.getAttribute(startLine - 1, "highlight");
    105             if (state && state.postConditionStringified)
    106                 break;
    107             startLine--;
    108         }
    109 
    110         // Do small highlight synchronously. This will provide instant highlight on PageUp / PageDown, gentle scrolling.
    111         this._highlightInChunks(startLine, endLine);
    112     },
    113 
    114     updateHighlight: function(startLine, endLine)
    115     {
    116         if (this._textModel.linesCount > WebInspector.TextEditorHighlighter._MaxLineCount)
    117             return;
    118 
    119         // Start line was edited, we should highlight everything until endLine.
    120         this._clearHighlightState(startLine);
    121 
    122         if (startLine) {
    123             var state = this._textModel.getAttribute(startLine - 1, "highlight");
    124             if (!state || !state.postConditionStringified) {
    125                 // Highlighter did not reach this point yet, nothing to update. It will reach it on subsequent timer tick and do the job.
    126                 return false;
    127             }
    128         }
    129 
    130         var restored = this._highlightLines(startLine, endLine);
    131         if (!restored) {
    132             for (var i = this._lastHighlightedLine; i < this._textModel.linesCount; ++i) {
    133                 var state = this._textModel.getAttribute(i, "highlight");
    134                 if (!state && i > endLine)
    135                     break;
    136                 this._textModel.setAttribute(i, "highlight-outdated", state);
    137                 this._textModel.removeAttribute(i, "highlight");
    138             }
    139 
    140             if (this._highlightTimer) {
    141                 clearTimeout(this._highlightTimer);
    142                 this._requestedEndLine = endLine;
    143                 this._highlightTimer = setTimeout(this._highlightInChunks.bind(this, this._lastHighlightedLine, this._requestedEndLine), 10);
    144             }
    145         }
    146         return restored;
    147     },
    148 
    149     _highlightInChunks: function(startLine, endLine)
    150     {
    151         delete this._highlightTimer;
    152 
    153         // First we always check if we have work to do. Could be that user scrolled back and we can quit.
    154         var state = this._textModel.getAttribute(this._requestedEndLine - 1, "highlight");
    155         if (state && state.postConditionStringified)
    156             return;
    157 
    158         if (this._requestedEndLine !== endLine) {
    159             // User keeps updating the job in between of our timer ticks. Just reschedule self, don't eat CPU (they must be scrolling).
    160             this._highlightTimer = setTimeout(this._highlightInChunks.bind(this, startLine, this._requestedEndLine), 100);
    161             return;
    162         }
    163 
    164         // The textModel may have been already updated.
    165         if (this._requestedEndLine > this._textModel.linesCount)
    166             this._requestedEndLine = this._textModel.linesCount;
    167 
    168         this._highlightLines(startLine, this._requestedEndLine);
    169 
    170         // Schedule tail highlight if necessary.
    171         if (this._lastHighlightedLine < this._requestedEndLine)
    172             this._highlightTimer = setTimeout(this._highlightInChunks.bind(this, this._lastHighlightedLine, this._requestedEndLine), 10);
    173     },
    174 
    175     _highlightLines: function(startLine, endLine)
    176     {
    177         // Restore highlighter context taken from previous line.
    178         var state = this._textModel.getAttribute(startLine - 1, "highlight");
    179         var postConditionStringified = state ? state.postConditionStringified : JSON.stringify(this._tokenizer.createInitialCondition());
    180 
    181         var tokensCount = 0;
    182         for (var lineNumber = startLine; lineNumber < endLine; ++lineNumber) {
    183             state = this._selectHighlightState(lineNumber, postConditionStringified);
    184             if (state.postConditionStringified) {
    185                 // This line is already highlighted.
    186                 postConditionStringified = state.postConditionStringified;
    187             } else {
    188                 var lastHighlightedColumn = 0;
    189                 if (state.midConditionStringified) {
    190                     lastHighlightedColumn = state.lastHighlightedColumn;
    191                     postConditionStringified = state.midConditionStringified;
    192                 }
    193 
    194                 var line = this._textModel.line(lineNumber);
    195                 this._tokenizer.line = line;
    196                 this._tokenizer.condition = JSON.parse(postConditionStringified);
    197 
    198                 // Highlight line.
    199                 state.ranges = state.ranges || [];
    200                 state.braces = state.braces || [];
    201                 do {
    202                     var newColumn = this._tokenizer.nextToken(lastHighlightedColumn);
    203                     var tokenType = this._tokenizer.tokenType;
    204                     if (tokenType && lastHighlightedColumn < this._highlightLineLimit) {
    205                         if (tokenType === "brace-start" || tokenType === "brace-end" || tokenType === "block-start" || tokenType === "block-end") {
    206                             state.braces.push({
    207                                 startColumn: lastHighlightedColumn,
    208                                 endColumn: newColumn - 1,
    209                                 token: tokenType
    210                             });
    211                         } else {
    212                             state.ranges.push({
    213                                 startColumn: lastHighlightedColumn,
    214                                 endColumn: newColumn - 1,
    215                                 token: tokenType
    216                             });
    217                         }
    218                     }
    219                     lastHighlightedColumn = newColumn;
    220                     if (++tokensCount > this._highlightChunkLimit)
    221                         break;
    222                 } while (lastHighlightedColumn < line.length);
    223 
    224                 postConditionStringified = JSON.stringify(this._tokenizer.condition);
    225 
    226                 if (lastHighlightedColumn < line.length) {
    227                     // Too much work for single chunk - exit.
    228                     state.lastHighlightedColumn = lastHighlightedColumn;
    229                     state.midConditionStringified = postConditionStringified;
    230                     break;
    231                 } else {
    232                     delete state.lastHighlightedColumn;
    233                     delete state.midConditionStringified;
    234                     state.postConditionStringified = postConditionStringified;
    235                 }
    236             }
    237 
    238             var nextLineState = this._textModel.getAttribute(lineNumber + 1, "highlight");
    239             if (nextLineState && nextLineState.preConditionStringified === state.postConditionStringified) {
    240                 // Following lines are up to date, no need re-highlight.
    241                 ++lineNumber;
    242                 this._damageCallback(startLine, lineNumber);
    243 
    244                 // Advance the "pointer" to the last highlighted line within the given chunk.
    245                 for (; lineNumber < endLine; ++lineNumber) {
    246                     state = this._textModel.getAttribute(lineNumber, "highlight");
    247                     if (!state || !state.postConditionStringified)
    248                         break;
    249                 }
    250                 this._lastHighlightedLine = lineNumber;
    251                 return true;
    252             }
    253         }
    254 
    255         this._damageCallback(startLine, lineNumber);
    256         this._lastHighlightedLine = lineNumber;
    257         return false;
    258     },
    259 
    260     _selectHighlightState: function(lineNumber, preConditionStringified)
    261     {
    262         var state = this._textModel.getAttribute(lineNumber, "highlight");
    263         if (state && state.preConditionStringified === preConditionStringified)
    264             return state;
    265 
    266         var outdatedState = this._textModel.getAttribute(lineNumber, "highlight-outdated");
    267         if (outdatedState && outdatedState.preConditionStringified === preConditionStringified) {
    268             // Swap states.
    269             this._textModel.setAttribute(lineNumber, "highlight", outdatedState);
    270             this._textModel.setAttribute(lineNumber, "highlight-outdated", state);
    271             return outdatedState;
    272         }
    273 
    274         if (state)
    275             this._textModel.setAttribute(lineNumber, "highlight-outdated", state);
    276 
    277         state = {};
    278         state.preConditionStringified = preConditionStringified;
    279         this._textModel.setAttribute(lineNumber, "highlight", state);
    280         return state;
    281     },
    282 
    283     _clearHighlightState: function(lineNumber)
    284     {
    285         this._textModel.removeAttribute(lineNumber, "highlight");
    286         this._textModel.removeAttribute(lineNumber, "highlight-outdated");
    287     }
    288 }
    289