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