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