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