Mercurial
comparison .cms/lib/codemirror/src/line/highlight.js @ 0:78edf6b517a0 draft
24.10
author | Coffee CMS <info@coffee-cms.ru> |
---|---|
date | Fri, 11 Oct 2024 22:40:23 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:78edf6b517a0 |
---|---|
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 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 } |