import { countColumn } from "../util/misc.js" import { copyState, innerMode, startState } from "../modes.js" import StringStream from "../util/StringStream.js" import { getLine, lineNo } from "./utils_line.js" import { clipPos } from "./pos.js" class SavedContext { constructor(state, lookAhead) { this.state = state this.lookAhead = lookAhead } } class Context { constructor(doc, state, line, lookAhead) { this.state = state this.doc = doc this.line = line this.maxLookAhead = lookAhead || 0 this.baseTokens = null this.baseTokenPos = 1 } lookAhead(n) { let line = this.doc.getLine(this.line + n) if (line != null && n > this.maxLookAhead) this.maxLookAhead = n return line } baseToken(n) { if (!this.baseTokens) return null while (this.baseTokens[this.baseTokenPos] <= n) this.baseTokenPos += 2 let type = this.baseTokens[this.baseTokenPos + 1] return {type: type && type.replace(/( |^)overlay .*/, ""), size: this.baseTokens[this.baseTokenPos] - n} } nextLine() { this.line++ if (this.maxLookAhead > 0) this.maxLookAhead-- } static fromSaved(doc, saved, line) { if (saved instanceof SavedContext) return new Context(doc, copyState(doc.mode, saved.state), line, saved.lookAhead) else return new Context(doc, copyState(doc.mode, saved), line) } save(copy) { let state = copy !== false ? copyState(this.doc.mode, this.state) : this.state return this.maxLookAhead > 0 ? new SavedContext(state, this.maxLookAhead) : state } } // Compute a style array (an array starting with a mode generation // -- for invalidation -- followed by pairs of end positions and // style strings), which is used to highlight the tokens on the // line. export function highlightLine(cm, line, context, forceToEnd) { // A styles array always starts with a number identifying the // mode/overlays that it is based on (for easy invalidation). let st = [cm.state.modeGen], lineClasses = {} // Compute the base array of styles runMode(cm, line.text, cm.doc.mode, context, (end, style) => st.push(end, style), lineClasses, forceToEnd) let state = context.state // Run overlays, adjust style array. for (let o = 0; o < cm.state.overlays.length; ++o) { context.baseTokens = st let overlay = cm.state.overlays[o], i = 1, at = 0 context.state = true runMode(cm, line.text, overlay.mode, context, (end, style) => { let start = i // Ensure there's a token end at the current position, and that i points at it while (at < end) { let i_end = st[i] if (i_end > end) st.splice(i, 1, end, st[i+1], i_end) i += 2 at = Math.min(end, i_end) } if (!style) return if (overlay.opaque) { st.splice(start, i - start, end, "overlay " + style) i = start + 2 } else { for (; start < i; start += 2) { let cur = st[start+1] st[start+1] = (cur ? cur + " " : "") + "overlay " + style } } }, lineClasses) context.state = state context.baseTokens = null context.baseTokenPos = 1 } return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null} } export function getLineStyles(cm, line, updateFrontier) { if (!line.styles || line.styles[0] != cm.state.modeGen) { let context = getContextBefore(cm, lineNo(line)) let resetState = line.text.length > cm.options.maxHighlightLength && copyState(cm.doc.mode, context.state) let result = highlightLine(cm, line, context) if (resetState) context.state = resetState line.stateAfter = context.save(!resetState) line.styles = result.styles if (result.classes) line.styleClasses = result.classes else if (line.styleClasses) line.styleClasses = null if (updateFrontier === cm.doc.highlightFrontier) cm.doc.modeFrontier = Math.max(cm.doc.modeFrontier, ++cm.doc.highlightFrontier) } return line.styles } export function getContextBefore(cm, n, precise) { let doc = cm.doc, display = cm.display if (!doc.mode.startState) return new Context(doc, true, n) let start = findStartLine(cm, n, precise) let saved = start > doc.first && getLine(doc, start - 1).stateAfter let context = saved ? Context.fromSaved(doc, saved, start) : new Context(doc, startState(doc.mode), start) doc.iter(start, n, line => { processLine(cm, line.text, context) let pos = context.line line.stateAfter = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo ? context.save() : null context.nextLine() }) if (precise) doc.modeFrontier = context.line return context } // Lightweight form of highlight -- proceed over this line and // update state, but don't save a style array. Used for lines that // aren't currently visible. export function processLine(cm, text, context, startAt) { let mode = cm.doc.mode let stream = new StringStream(text, cm.options.tabSize, context) stream.start = stream.pos = startAt || 0 if (text == "") callBlankLine(mode, context.state) while (!stream.eol()) { readToken(mode, stream, context.state) stream.start = stream.pos } } function callBlankLine(mode, state) { if (mode.blankLine) return mode.blankLine(state) if (!mode.innerMode) return let inner = innerMode(mode, state) if (inner.mode.blankLine) return inner.mode.blankLine(inner.state) } function readToken(mode, stream, state, inner) { for (let i = 0; i < 10; i++) { if (inner) inner[0] = innerMode(mode, state).mode let style = mode.token(stream, state) if (stream.pos > stream.start) return style } throw new Error("Mode " + mode.name + " failed to advance stream.") } class Token { constructor(stream, type, state) { this.start = stream.start; this.end = stream.pos this.string = stream.current() this.type = type || null this.state = state } } // Utility for getTokenAt and getLineTokens export function takeToken(cm, pos, precise, asArray) { let doc = cm.doc, mode = doc.mode, style pos = clipPos(doc, pos) let line = getLine(doc, pos.line), context = getContextBefore(cm, pos.line, precise) let stream = new StringStream(line.text, cm.options.tabSize, context), tokens if (asArray) tokens = [] while ((asArray || stream.pos < pos.ch) && !stream.eol()) { stream.start = stream.pos style = readToken(mode, stream, context.state) if (asArray) tokens.push(new Token(stream, style, copyState(doc.mode, context.state))) } return asArray ? tokens : new Token(stream, style, context.state) } function extractLineClasses(type, output) { if (type) for (;;) { let lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/) if (!lineClass) break type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length) let prop = lineClass[1] ? "bgClass" : "textClass" if (output[prop] == null) output[prop] = lineClass[2] else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop])) output[prop] += " " + lineClass[2] } return type } // Run the given mode's parser over a line, calling f for each token. function runMode(cm, text, mode, context, f, lineClasses, forceToEnd) { let flattenSpans = mode.flattenSpans if (flattenSpans == null) flattenSpans = cm.options.flattenSpans let curStart = 0, curStyle = null let stream = new StringStream(text, cm.options.tabSize, context), style let inner = cm.options.addModeClass && [null] if (text == "") extractLineClasses(callBlankLine(mode, context.state), lineClasses) while (!stream.eol()) { if (stream.pos > cm.options.maxHighlightLength) { flattenSpans = false if (forceToEnd) processLine(cm, text, context, stream.pos) stream.pos = text.length style = null } else { style = extractLineClasses(readToken(mode, stream, context.state, inner), lineClasses) } if (inner) { let mName = inner[0].name if (mName) style = "m-" + (style ? mName + " " + style : mName) } if (!flattenSpans || curStyle != style) { while (curStart < stream.start) { curStart = Math.min(stream.start, curStart + 5000) f(curStart, curStyle) } curStyle = style } stream.start = stream.pos } while (curStart < stream.pos) { // Webkit seems to refuse to render text nodes longer than 57444 // characters, and returns inaccurate measurements in nodes // starting around 5000 chars. let pos = Math.min(stream.pos, curStart + 5000) f(pos, curStyle) curStart = pos } } // Finds the line to start with when starting a parse. Tries to // find a line with a stateAfter, so that it can start with a // valid state. If that fails, it returns the line with the // smallest indentation, which tends to need the least context to // parse correctly. function findStartLine(cm, n, precise) { let minindent, minline, doc = cm.doc let lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100) for (let search = n; search > lim; --search) { if (search <= doc.first) return doc.first let line = getLine(doc, search - 1), after = line.stateAfter if (after && (!precise || search + (after instanceof SavedContext ? after.lookAhead : 0) <= doc.modeFrontier)) return search let indented = countColumn(line.text, null, cm.options.tabSize) if (minline == null || minindent > indented) { minline = search - 1 minindent = indented } } return minline } export function retreatFrontier(doc, n) { doc.modeFrontier = Math.min(doc.modeFrontier, n) if (doc.highlightFrontier < n - 10) return let start = doc.first for (let line = n - 1; line > start; line--) { let saved = getLine(doc, line).stateAfter // change is on 3 // state on line 1 looked ahead 2 -- so saw 3 // test 1 + 2 < 3 should cover this if (saved && (!(saved instanceof SavedContext) || line + saved.lookAhead < n)) { start = line + 1 break } } doc.highlightFrontier = Math.min(doc.highlightFrontier, start) }