TextEditorHighlighter.js 12 KB

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