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