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
32WebInspector.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
40WebInspector.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