Mercurial
comparison .cms/lib/codemirror/src/line/spans.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 { indexOf, lst } from "../util/misc.js" | |
2 | |
3 import { cmp } from "./pos.js" | |
4 import { sawCollapsedSpans } from "./saw_special_spans.js" | |
5 import { getLine, isLine, lineNo } from "./utils_line.js" | |
6 | |
7 // TEXTMARKER SPANS | |
8 | |
9 export function MarkedSpan(marker, from, to) { | |
10 this.marker = marker | |
11 this.from = from; this.to = to | |
12 } | |
13 | |
14 // Search an array of spans for a span matching the given marker. | |
15 export function getMarkedSpanFor(spans, marker) { | |
16 if (spans) for (let i = 0; i < spans.length; ++i) { | |
17 let span = spans[i] | |
18 if (span.marker == marker) return span | |
19 } | |
20 } | |
21 | |
22 // Remove a span from an array, returning undefined if no spans are | |
23 // left (we don't store arrays for lines without spans). | |
24 export function removeMarkedSpan(spans, span) { | |
25 let r | |
26 for (let i = 0; i < spans.length; ++i) | |
27 if (spans[i] != span) (r || (r = [])).push(spans[i]) | |
28 return r | |
29 } | |
30 | |
31 // Add a span to a line. | |
32 export function addMarkedSpan(line, span, op) { | |
33 let inThisOp = op && window.WeakSet && (op.markedSpans || (op.markedSpans = new WeakSet)) | |
34 if (inThisOp && line.markedSpans && inThisOp.has(line.markedSpans)) { | |
35 line.markedSpans.push(span) | |
36 } else { | |
37 line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span] | |
38 if (inThisOp) inThisOp.add(line.markedSpans) | |
39 } | |
40 span.marker.attachLine(line) | |
41 } | |
42 | |
43 // Used for the algorithm that adjusts markers for a change in the | |
44 // document. These functions cut an array of spans at a given | |
45 // character position, returning an array of remaining chunks (or | |
46 // undefined if nothing remains). | |
47 function markedSpansBefore(old, startCh, isInsert) { | |
48 let nw | |
49 if (old) for (let i = 0; i < old.length; ++i) { | |
50 let span = old[i], marker = span.marker | |
51 let startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh) | |
52 if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { | |
53 let endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh) | |
54 ;(nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)) | |
55 } | |
56 } | |
57 return nw | |
58 } | |
59 function markedSpansAfter(old, endCh, isInsert) { | |
60 let nw | |
61 if (old) for (let i = 0; i < old.length; ++i) { | |
62 let span = old[i], marker = span.marker | |
63 let endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh) | |
64 if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { | |
65 let startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh) | |
66 ;(nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, | |
67 span.to == null ? null : span.to - endCh)) | |
68 } | |
69 } | |
70 return nw | |
71 } | |
72 | |
73 // Given a change object, compute the new set of marker spans that | |
74 // cover the line in which the change took place. Removes spans | |
75 // entirely within the change, reconnects spans belonging to the | |
76 // same marker that appear on both sides of the change, and cuts off | |
77 // spans partially within the change. Returns an array of span | |
78 // arrays with one element for each line in (after) the change. | |
79 export function stretchSpansOverChange(doc, change) { | |
80 if (change.full) return null | |
81 let oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans | |
82 let oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans | |
83 if (!oldFirst && !oldLast) return null | |
84 | |
85 let startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0 | |
86 // Get the spans that 'stick out' on both sides | |
87 let first = markedSpansBefore(oldFirst, startCh, isInsert) | |
88 let last = markedSpansAfter(oldLast, endCh, isInsert) | |
89 | |
90 // Next, merge those two ends | |
91 let sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0) | |
92 if (first) { | |
93 // Fix up .to properties of first | |
94 for (let i = 0; i < first.length; ++i) { | |
95 let span = first[i] | |
96 if (span.to == null) { | |
97 let found = getMarkedSpanFor(last, span.marker) | |
98 if (!found) span.to = startCh | |
99 else if (sameLine) span.to = found.to == null ? null : found.to + offset | |
100 } | |
101 } | |
102 } | |
103 if (last) { | |
104 // Fix up .from in last (or move them into first in case of sameLine) | |
105 for (let i = 0; i < last.length; ++i) { | |
106 let span = last[i] | |
107 if (span.to != null) span.to += offset | |
108 if (span.from == null) { | |
109 let found = getMarkedSpanFor(first, span.marker) | |
110 if (!found) { | |
111 span.from = offset | |
112 if (sameLine) (first || (first = [])).push(span) | |
113 } | |
114 } else { | |
115 span.from += offset | |
116 if (sameLine) (first || (first = [])).push(span) | |
117 } | |
118 } | |
119 } | |
120 // Make sure we didn't create any zero-length spans | |
121 if (first) first = clearEmptySpans(first) | |
122 if (last && last != first) last = clearEmptySpans(last) | |
123 | |
124 let newMarkers = [first] | |
125 if (!sameLine) { | |
126 // Fill gap with whole-line-spans | |
127 let gap = change.text.length - 2, gapMarkers | |
128 if (gap > 0 && first) | |
129 for (let i = 0; i < first.length; ++i) | |
130 if (first[i].to == null) | |
131 (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i].marker, null, null)) | |
132 for (let i = 0; i < gap; ++i) | |
133 newMarkers.push(gapMarkers) | |
134 newMarkers.push(last) | |
135 } | |
136 return newMarkers | |
137 } | |
138 | |
139 // Remove spans that are empty and don't have a clearWhenEmpty | |
140 // option of false. | |
141 function clearEmptySpans(spans) { | |
142 for (let i = 0; i < spans.length; ++i) { | |
143 let span = spans[i] | |
144 if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false) | |
145 spans.splice(i--, 1) | |
146 } | |
147 if (!spans.length) return null | |
148 return spans | |
149 } | |
150 | |
151 // Used to 'clip' out readOnly ranges when making a change. | |
152 export function removeReadOnlyRanges(doc, from, to) { | |
153 let markers = null | |
154 doc.iter(from.line, to.line + 1, line => { | |
155 if (line.markedSpans) for (let i = 0; i < line.markedSpans.length; ++i) { | |
156 let mark = line.markedSpans[i].marker | |
157 if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) | |
158 (markers || (markers = [])).push(mark) | |
159 } | |
160 }) | |
161 if (!markers) return null | |
162 let parts = [{from: from, to: to}] | |
163 for (let i = 0; i < markers.length; ++i) { | |
164 let mk = markers[i], m = mk.find(0) | |
165 for (let j = 0; j < parts.length; ++j) { | |
166 let p = parts[j] | |
167 if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) continue | |
168 let newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to) | |
169 if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) | |
170 newParts.push({from: p.from, to: m.from}) | |
171 if (dto > 0 || !mk.inclusiveRight && !dto) | |
172 newParts.push({from: m.to, to: p.to}) | |
173 parts.splice.apply(parts, newParts) | |
174 j += newParts.length - 3 | |
175 } | |
176 } | |
177 return parts | |
178 } | |
179 | |
180 // Connect or disconnect spans from a line. | |
181 export function detachMarkedSpans(line) { | |
182 let spans = line.markedSpans | |
183 if (!spans) return | |
184 for (let i = 0; i < spans.length; ++i) | |
185 spans[i].marker.detachLine(line) | |
186 line.markedSpans = null | |
187 } | |
188 export function attachMarkedSpans(line, spans) { | |
189 if (!spans) return | |
190 for (let i = 0; i < spans.length; ++i) | |
191 spans[i].marker.attachLine(line) | |
192 line.markedSpans = spans | |
193 } | |
194 | |
195 // Helpers used when computing which overlapping collapsed span | |
196 // counts as the larger one. | |
197 function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0 } | |
198 function extraRight(marker) { return marker.inclusiveRight ? 1 : 0 } | |
199 | |
200 // Returns a number indicating which of two overlapping collapsed | |
201 // spans is larger (and thus includes the other). Falls back to | |
202 // comparing ids when the spans cover exactly the same range. | |
203 export function compareCollapsedMarkers(a, b) { | |
204 let lenDiff = a.lines.length - b.lines.length | |
205 if (lenDiff != 0) return lenDiff | |
206 let aPos = a.find(), bPos = b.find() | |
207 let fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b) | |
208 if (fromCmp) return -fromCmp | |
209 let toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b) | |
210 if (toCmp) return toCmp | |
211 return b.id - a.id | |
212 } | |
213 | |
214 // Find out whether a line ends or starts in a collapsed span. If | |
215 // so, return the marker for that span. | |
216 function collapsedSpanAtSide(line, start) { | |
217 let sps = sawCollapsedSpans && line.markedSpans, found | |
218 if (sps) for (let sp, i = 0; i < sps.length; ++i) { | |
219 sp = sps[i] | |
220 if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && | |
221 (!found || compareCollapsedMarkers(found, sp.marker) < 0)) | |
222 found = sp.marker | |
223 } | |
224 return found | |
225 } | |
226 export function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true) } | |
227 export function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false) } | |
228 | |
229 export function collapsedSpanAround(line, ch) { | |
230 let sps = sawCollapsedSpans && line.markedSpans, found | |
231 if (sps) for (let i = 0; i < sps.length; ++i) { | |
232 let sp = sps[i] | |
233 if (sp.marker.collapsed && (sp.from == null || sp.from < ch) && (sp.to == null || sp.to > ch) && | |
234 (!found || compareCollapsedMarkers(found, sp.marker) < 0)) found = sp.marker | |
235 } | |
236 return found | |
237 } | |
238 | |
239 // Test whether there exists a collapsed span that partially | |
240 // overlaps (covers the start or end, but not both) of a new span. | |
241 // Such overlap is not allowed. | |
242 export function conflictingCollapsedRange(doc, lineNo, from, to, marker) { | |
243 let line = getLine(doc, lineNo) | |
244 let sps = sawCollapsedSpans && line.markedSpans | |
245 if (sps) for (let i = 0; i < sps.length; ++i) { | |
246 let sp = sps[i] | |
247 if (!sp.marker.collapsed) continue | |
248 let found = sp.marker.find(0) | |
249 let fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker) | |
250 let toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker) | |
251 if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue | |
252 if (fromCmp <= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.to, from) >= 0 : cmp(found.to, from) > 0) || | |
253 fromCmp >= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.from, to) <= 0 : cmp(found.from, to) < 0)) | |
254 return true | |
255 } | |
256 } | |
257 | |
258 // A visual line is a line as drawn on the screen. Folding, for | |
259 // example, can cause multiple logical lines to appear on the same | |
260 // visual line. This finds the start of the visual line that the | |
261 // given line is part of (usually that is the line itself). | |
262 export function visualLine(line) { | |
263 let merged | |
264 while (merged = collapsedSpanAtStart(line)) | |
265 line = merged.find(-1, true).line | |
266 return line | |
267 } | |
268 | |
269 export function visualLineEnd(line) { | |
270 let merged | |
271 while (merged = collapsedSpanAtEnd(line)) | |
272 line = merged.find(1, true).line | |
273 return line | |
274 } | |
275 | |
276 // Returns an array of logical lines that continue the visual line | |
277 // started by the argument, or undefined if there are no such lines. | |
278 export function visualLineContinued(line) { | |
279 let merged, lines | |
280 while (merged = collapsedSpanAtEnd(line)) { | |
281 line = merged.find(1, true).line | |
282 ;(lines || (lines = [])).push(line) | |
283 } | |
284 return lines | |
285 } | |
286 | |
287 // Get the line number of the start of the visual line that the | |
288 // given line number is part of. | |
289 export function visualLineNo(doc, lineN) { | |
290 let line = getLine(doc, lineN), vis = visualLine(line) | |
291 if (line == vis) return lineN | |
292 return lineNo(vis) | |
293 } | |
294 | |
295 // Get the line number of the start of the next visual line after | |
296 // the given line. | |
297 export function visualLineEndNo(doc, lineN) { | |
298 if (lineN > doc.lastLine()) return lineN | |
299 let line = getLine(doc, lineN), merged | |
300 if (!lineIsHidden(doc, line)) return lineN | |
301 while (merged = collapsedSpanAtEnd(line)) | |
302 line = merged.find(1, true).line | |
303 return lineNo(line) + 1 | |
304 } | |
305 | |
306 // Compute whether a line is hidden. Lines count as hidden when they | |
307 // are part of a visual line that starts with another line, or when | |
308 // they are entirely covered by collapsed, non-widget span. | |
309 export function lineIsHidden(doc, line) { | |
310 let sps = sawCollapsedSpans && line.markedSpans | |
311 if (sps) for (let sp, i = 0; i < sps.length; ++i) { | |
312 sp = sps[i] | |
313 if (!sp.marker.collapsed) continue | |
314 if (sp.from == null) return true | |
315 if (sp.marker.widgetNode) continue | |
316 if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) | |
317 return true | |
318 } | |
319 } | |
320 function lineIsHiddenInner(doc, line, span) { | |
321 if (span.to == null) { | |
322 let end = span.marker.find(1, true) | |
323 return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker)) | |
324 } | |
325 if (span.marker.inclusiveRight && span.to == line.text.length) | |
326 return true | |
327 for (let sp, i = 0; i < line.markedSpans.length; ++i) { | |
328 sp = line.markedSpans[i] | |
329 if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && | |
330 (sp.to == null || sp.to != span.from) && | |
331 (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && | |
332 lineIsHiddenInner(doc, line, sp)) return true | |
333 } | |
334 } | |
335 | |
336 // Find the height above the given line. | |
337 export function heightAtLine(lineObj) { | |
338 lineObj = visualLine(lineObj) | |
339 | |
340 let h = 0, chunk = lineObj.parent | |
341 for (let i = 0; i < chunk.lines.length; ++i) { | |
342 let line = chunk.lines[i] | |
343 if (line == lineObj) break | |
344 else h += line.height | |
345 } | |
346 for (let p = chunk.parent; p; chunk = p, p = chunk.parent) { | |
347 for (let i = 0; i < p.children.length; ++i) { | |
348 let cur = p.children[i] | |
349 if (cur == chunk) break | |
350 else h += cur.height | |
351 } | |
352 } | |
353 return h | |
354 } | |
355 | |
356 // Compute the character length of a line, taking into account | |
357 // collapsed ranges (see markText) that might hide parts, and join | |
358 // other lines onto it. | |
359 export function lineLength(line) { | |
360 if (line.height == 0) return 0 | |
361 let len = line.text.length, merged, cur = line | |
362 while (merged = collapsedSpanAtStart(cur)) { | |
363 let found = merged.find(0, true) | |
364 cur = found.from.line | |
365 len += found.from.ch - found.to.ch | |
366 } | |
367 cur = line | |
368 while (merged = collapsedSpanAtEnd(cur)) { | |
369 let found = merged.find(0, true) | |
370 len -= cur.text.length - found.from.ch | |
371 cur = found.to.line | |
372 len += cur.text.length - found.to.ch | |
373 } | |
374 return len | |
375 } | |
376 | |
377 // Find the longest line in the document. | |
378 export function findMaxLine(cm) { | |
379 let d = cm.display, doc = cm.doc | |
380 d.maxLine = getLine(doc, doc.first) | |
381 d.maxLineLength = lineLength(d.maxLine) | |
382 d.maxLineChanged = true | |
383 doc.iter(line => { | |
384 let len = lineLength(line) | |
385 if (len > d.maxLineLength) { | |
386 d.maxLineLength = len | |
387 d.maxLine = line | |
388 } | |
389 }) | |
390 } |