initial import
[vuplus_webkit] / Source / WebCore / inspector / front-end / TextEditorHighlighter.js
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.createInitialCondition());
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 }