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