820da8f8772b1ffdcdca7a1ad43d4baf4d87974eec767c700310acfdd54fe1f79806df88727f30b10d0125d611824a6ac4b0abad1bd84591517810d7b5afc1 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import { countColumn } from "../util/misc.js"
  2. import { copyState, innerMode, startState } from "../modes.js"
  3. import StringStream from "../util/StringStream.js"
  4. import { getLine, lineNo } from "./utils_line.js"
  5. import { clipPos } from "./pos.js"
  6. class SavedContext {
  7. constructor(state, lookAhead) {
  8. this.state = state
  9. this.lookAhead = lookAhead
  10. }
  11. }
  12. class Context {
  13. constructor(doc, state, line, lookAhead) {
  14. this.state = state
  15. this.doc = doc
  16. this.line = line
  17. this.maxLookAhead = lookAhead || 0
  18. this.baseTokens = null
  19. this.baseTokenPos = 1
  20. }
  21. lookAhead(n) {
  22. let line = this.doc.getLine(this.line + n)
  23. if (line != null && n > this.maxLookAhead) this.maxLookAhead = n
  24. return line
  25. }
  26. baseToken(n) {
  27. if (!this.baseTokens) return null
  28. while (this.baseTokens[this.baseTokenPos] <= n)
  29. this.baseTokenPos += 2
  30. let type = this.baseTokens[this.baseTokenPos + 1]
  31. return {type: type && type.replace(/( |^)overlay .*/, ""),
  32. size: this.baseTokens[this.baseTokenPos] - n}
  33. }
  34. nextLine() {
  35. this.line++
  36. if (this.maxLookAhead > 0) this.maxLookAhead--
  37. }
  38. static fromSaved(doc, saved, line) {
  39. if (saved instanceof SavedContext)
  40. return new Context(doc, copyState(doc.mode, saved.state), line, saved.lookAhead)
  41. else
  42. return new Context(doc, copyState(doc.mode, saved), line)
  43. }
  44. save(copy) {
  45. let state = copy !== false ? copyState(this.doc.mode, this.state) : this.state
  46. return this.maxLookAhead > 0 ? new SavedContext(state, this.maxLookAhead) : state
  47. }
  48. }
  49. // Compute a style array (an array starting with a mode generation
  50. // -- for invalidation -- followed by pairs of end positions and
  51. // style strings), which is used to highlight the tokens on the
  52. // line.
  53. export function highlightLine(cm, line, context, forceToEnd) {
  54. // A styles array always starts with a number identifying the
  55. // mode/overlays that it is based on (for easy invalidation).
  56. let st = [cm.state.modeGen], lineClasses = {}
  57. // Compute the base array of styles
  58. runMode(cm, line.text, cm.doc.mode, context, (end, style) => st.push(end, style),
  59. lineClasses, forceToEnd)
  60. let state = context.state
  61. // Run overlays, adjust style array.
  62. for (let o = 0; o < cm.state.overlays.length; ++o) {
  63. context.baseTokens = st
  64. let overlay = cm.state.overlays[o], i = 1, at = 0
  65. context.state = true
  66. runMode(cm, line.text, overlay.mode, context, (end, style) => {
  67. let start = i
  68. // Ensure there's a token end at the current position, and that i points at it
  69. while (at < end) {
  70. let i_end = st[i]
  71. if (i_end > end)
  72. st.splice(i, 1, end, st[i+1], i_end)
  73. i += 2
  74. at = Math.min(end, i_end)
  75. }
  76. if (!style) return
  77. if (overlay.opaque) {
  78. st.splice(start, i - start, end, "overlay " + style)
  79. i = start + 2
  80. } else {
  81. for (; start < i; start += 2) {
  82. let cur = st[start+1]
  83. st[start+1] = (cur ? cur + " " : "") + "overlay " + style
  84. }
  85. }
  86. }, lineClasses)
  87. context.state = state
  88. context.baseTokens = null
  89. context.baseTokenPos = 1
  90. }
  91. return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null}
  92. }
  93. export function getLineStyles(cm, line, updateFrontier) {
  94. if (!line.styles || line.styles[0] != cm.state.modeGen) {
  95. let context = getContextBefore(cm, lineNo(line))
  96. let resetState = line.text.length > cm.options.maxHighlightLength && copyState(cm.doc.mode, context.state)
  97. let result = highlightLine(cm, line, context)
  98. if (resetState) context.state = resetState
  99. line.stateAfter = context.save(!resetState)
  100. line.styles = result.styles
  101. if (result.classes) line.styleClasses = result.classes
  102. else if (line.styleClasses) line.styleClasses = null
  103. if (updateFrontier === cm.doc.highlightFrontier)
  104. cm.doc.modeFrontier = Math.max(cm.doc.modeFrontier, ++cm.doc.highlightFrontier)
  105. }
  106. return line.styles
  107. }
  108. export function getContextBefore(cm, n, precise) {
  109. let doc = cm.doc, display = cm.display
  110. if (!doc.mode.startState) return new Context(doc, true, n)
  111. let start = findStartLine(cm, n, precise)
  112. let saved = start > doc.first && getLine(doc, start - 1).stateAfter
  113. let context = saved ? Context.fromSaved(doc, saved, start) : new Context(doc, startState(doc.mode), start)
  114. doc.iter(start, n, line => {
  115. processLine(cm, line.text, context)
  116. let pos = context.line
  117. line.stateAfter = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo ? context.save() : null
  118. context.nextLine()
  119. })
  120. if (precise) doc.modeFrontier = context.line
  121. return context
  122. }
  123. // Lightweight form of highlight -- proceed over this line and
  124. // update state, but don't save a style array. Used for lines that
  125. // aren't currently visible.
  126. export function processLine(cm, text, context, startAt) {
  127. let mode = cm.doc.mode
  128. let stream = new StringStream(text, cm.options.tabSize, context)
  129. stream.start = stream.pos = startAt || 0
  130. if (text == "") callBlankLine(mode, context.state)
  131. while (!stream.eol()) {
  132. readToken(mode, stream, context.state)
  133. stream.start = stream.pos
  134. }
  135. }
  136. function callBlankLine(mode, state) {
  137. if (mode.blankLine) return mode.blankLine(state)
  138. if (!mode.innerMode) return
  139. let inner = innerMode(mode, state)
  140. if (inner.mode.blankLine) return inner.mode.blankLine(inner.state)
  141. }
  142. function readToken(mode, stream, state, inner) {
  143. for (let i = 0; i < 10; i++) {
  144. if (inner) inner[0] = innerMode(mode, state).mode
  145. let style = mode.token(stream, state)
  146. if (stream.pos > stream.start) return style
  147. }
  148. throw new Error("Mode " + mode.name + " failed to advance stream.")
  149. }
  150. class Token {
  151. constructor(stream, type, state) {
  152. this.start = stream.start; this.end = stream.pos
  153. this.string = stream.current()
  154. this.type = type || null
  155. this.state = state
  156. }
  157. }
  158. // Utility for getTokenAt and getLineTokens
  159. export function takeToken(cm, pos, precise, asArray) {
  160. let doc = cm.doc, mode = doc.mode, style
  161. pos = clipPos(doc, pos)
  162. let line = getLine(doc, pos.line), context = getContextBefore(cm, pos.line, precise)
  163. let stream = new StringStream(line.text, cm.options.tabSize, context), tokens
  164. if (asArray) tokens = []
  165. while ((asArray || stream.pos < pos.ch) && !stream.eol()) {
  166. stream.start = stream.pos
  167. style = readToken(mode, stream, context.state)
  168. if (asArray) tokens.push(new Token(stream, style, copyState(doc.mode, context.state)))
  169. }
  170. return asArray ? tokens : new Token(stream, style, context.state)
  171. }
  172. function extractLineClasses(type, output) {
  173. if (type) for (;;) {
  174. let lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/)
  175. if (!lineClass) break
  176. type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length)
  177. let prop = lineClass[1] ? "bgClass" : "textClass"
  178. if (output[prop] == null)
  179. output[prop] = lineClass[2]
  180. else if (!(new RegExp("(?:^|\\s)" + lineClass[2] + "(?:$|\\s)")).test(output[prop]))
  181. output[prop] += " " + lineClass[2]
  182. }
  183. return type
  184. }
  185. // Run the given mode's parser over a line, calling f for each token.
  186. function runMode(cm, text, mode, context, f, lineClasses, forceToEnd) {
  187. let flattenSpans = mode.flattenSpans
  188. if (flattenSpans == null) flattenSpans = cm.options.flattenSpans
  189. let curStart = 0, curStyle = null
  190. let stream = new StringStream(text, cm.options.tabSize, context), style
  191. let inner = cm.options.addModeClass && [null]
  192. if (text == "") extractLineClasses(callBlankLine(mode, context.state), lineClasses)
  193. while (!stream.eol()) {
  194. if (stream.pos > cm.options.maxHighlightLength) {
  195. flattenSpans = false
  196. if (forceToEnd) processLine(cm, text, context, stream.pos)
  197. stream.pos = text.length
  198. style = null
  199. } else {
  200. style = extractLineClasses(readToken(mode, stream, context.state, inner), lineClasses)
  201. }
  202. if (inner) {
  203. let mName = inner[0].name
  204. if (mName) style = "m-" + (style ? mName + " " + style : mName)
  205. }
  206. if (!flattenSpans || curStyle != style) {
  207. while (curStart < stream.start) {
  208. curStart = Math.min(stream.start, curStart + 5000)
  209. f(curStart, curStyle)
  210. }
  211. curStyle = style
  212. }
  213. stream.start = stream.pos
  214. }
  215. while (curStart < stream.pos) {
  216. // Webkit seems to refuse to render text nodes longer than 57444
  217. // characters, and returns inaccurate measurements in nodes
  218. // starting around 5000 chars.
  219. let pos = Math.min(stream.pos, curStart + 5000)
  220. f(pos, curStyle)
  221. curStart = pos
  222. }
  223. }
  224. // Finds the line to start with when starting a parse. Tries to
  225. // find a line with a stateAfter, so that it can start with a
  226. // valid state. If that fails, it returns the line with the
  227. // smallest indentation, which tends to need the least context to
  228. // parse correctly.
  229. function findStartLine(cm, n, precise) {
  230. let minindent, minline, doc = cm.doc
  231. let lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100)
  232. for (let search = n; search > lim; --search) {
  233. if (search <= doc.first) return doc.first
  234. let line = getLine(doc, search - 1), after = line.stateAfter
  235. if (after && (!precise || search + (after instanceof SavedContext ? after.lookAhead : 0) <= doc.modeFrontier))
  236. return search
  237. let indented = countColumn(line.text, null, cm.options.tabSize)
  238. if (minline == null || minindent > indented) {
  239. minline = search - 1
  240. minindent = indented
  241. }
  242. }
  243. return minline
  244. }
  245. export function retreatFrontier(doc, n) {
  246. doc.modeFrontier = Math.min(doc.modeFrontier, n)
  247. if (doc.highlightFrontier < n - 10) return
  248. let start = doc.first
  249. for (let line = n - 1; line > start; line--) {
  250. let saved = getLine(doc, line).stateAfter
  251. // change is on 3
  252. // state on line 1 looked ahead 2 -- so saw 3
  253. // test 1 + 2 < 3 should cover this
  254. if (saved && (!(saved instanceof SavedContext) || line + saved.lookAhead < n)) {
  255. start = line + 1
  256. break
  257. }
  258. }
  259. doc.highlightFrontier = Math.min(doc.highlightFrontier, start)
  260. }