Mercurial
comparison .cms/lib/codemirror/src/model/mark_text.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 { eltP } from "../util/dom.js" | |
2 import { eventMixin, hasHandler, on } from "../util/event.js" | |
3 import { endOperation, operation, runInOp, startOperation } from "../display/operations.js" | |
4 import { clipPos, cmp, Pos } from "../line/pos.js" | |
5 import { lineNo, updateLineHeight } from "../line/utils_line.js" | |
6 import { clearLineMeasurementCacheFor, findViewForLine, textHeight } from "../measurement/position_measurement.js" | |
7 import { seeReadOnlySpans, seeCollapsedSpans } from "../line/saw_special_spans.js" | |
8 import { addMarkedSpan, conflictingCollapsedRange, getMarkedSpanFor, lineIsHidden, lineLength, MarkedSpan, removeMarkedSpan, visualLine } from "../line/spans.js" | |
9 import { copyObj, indexOf, lst } from "../util/misc.js" | |
10 import { signalLater } from "../util/operation_group.js" | |
11 import { widgetHeight } from "../measurement/widgets.js" | |
12 import { regChange, regLineChange } from "../display/view_tracking.js" | |
13 | |
14 import { linkedDocs } from "./document_data.js" | |
15 import { addChangeToHistory } from "./history.js" | |
16 import { reCheckSelection } from "./selection_updates.js" | |
17 | |
18 // TEXTMARKERS | |
19 | |
20 // Created with markText and setBookmark methods. A TextMarker is a | |
21 // handle that can be used to clear or find a marked position in the | |
22 // document. Line objects hold arrays (markedSpans) containing | |
23 // {from, to, marker} object pointing to such marker objects, and | |
24 // indicating that such a marker is present on that line. Multiple | |
25 // lines may point to the same marker when it spans across lines. | |
26 // The spans will have null for their from/to properties when the | |
27 // marker continues beyond the start/end of the line. Markers have | |
28 // links back to the lines they currently touch. | |
29 | |
30 // Collapsed markers have unique ids, in order to be able to order | |
31 // them, which is needed for uniquely determining an outer marker | |
32 // when they overlap (they may nest, but not partially overlap). | |
33 let nextMarkerId = 0 | |
34 | |
35 export class TextMarker { | |
36 constructor(doc, type) { | |
37 this.lines = [] | |
38 this.type = type | |
39 this.doc = doc | |
40 this.id = ++nextMarkerId | |
41 } | |
42 | |
43 // Clear the marker. | |
44 clear() { | |
45 if (this.explicitlyCleared) return | |
46 let cm = this.doc.cm, withOp = cm && !cm.curOp | |
47 if (withOp) startOperation(cm) | |
48 if (hasHandler(this, "clear")) { | |
49 let found = this.find() | |
50 if (found) signalLater(this, "clear", found.from, found.to) | |
51 } | |
52 let min = null, max = null | |
53 for (let i = 0; i < this.lines.length; ++i) { | |
54 let line = this.lines[i] | |
55 let span = getMarkedSpanFor(line.markedSpans, this) | |
56 if (cm && !this.collapsed) regLineChange(cm, lineNo(line), "text") | |
57 else if (cm) { | |
58 if (span.to != null) max = lineNo(line) | |
59 if (span.from != null) min = lineNo(line) | |
60 } | |
61 line.markedSpans = removeMarkedSpan(line.markedSpans, span) | |
62 if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm) | |
63 updateLineHeight(line, textHeight(cm.display)) | |
64 } | |
65 if (cm && this.collapsed && !cm.options.lineWrapping) for (let i = 0; i < this.lines.length; ++i) { | |
66 let visual = visualLine(this.lines[i]), len = lineLength(visual) | |
67 if (len > cm.display.maxLineLength) { | |
68 cm.display.maxLine = visual | |
69 cm.display.maxLineLength = len | |
70 cm.display.maxLineChanged = true | |
71 } | |
72 } | |
73 | |
74 if (min != null && cm && this.collapsed) regChange(cm, min, max + 1) | |
75 this.lines.length = 0 | |
76 this.explicitlyCleared = true | |
77 if (this.atomic && this.doc.cantEdit) { | |
78 this.doc.cantEdit = false | |
79 if (cm) reCheckSelection(cm.doc) | |
80 } | |
81 if (cm) signalLater(cm, "markerCleared", cm, this, min, max) | |
82 if (withOp) endOperation(cm) | |
83 if (this.parent) this.parent.clear() | |
84 } | |
85 | |
86 // Find the position of the marker in the document. Returns a {from, | |
87 // to} object by default. Side can be passed to get a specific side | |
88 // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the | |
89 // Pos objects returned contain a line object, rather than a line | |
90 // number (used to prevent looking up the same line twice). | |
91 find(side, lineObj) { | |
92 if (side == null && this.type == "bookmark") side = 1 | |
93 let from, to | |
94 for (let i = 0; i < this.lines.length; ++i) { | |
95 let line = this.lines[i] | |
96 let span = getMarkedSpanFor(line.markedSpans, this) | |
97 if (span.from != null) { | |
98 from = Pos(lineObj ? line : lineNo(line), span.from) | |
99 if (side == -1) return from | |
100 } | |
101 if (span.to != null) { | |
102 to = Pos(lineObj ? line : lineNo(line), span.to) | |
103 if (side == 1) return to | |
104 } | |
105 } | |
106 return from && {from: from, to: to} | |
107 } | |
108 | |
109 // Signals that the marker's widget changed, and surrounding layout | |
110 // should be recomputed. | |
111 changed() { | |
112 let pos = this.find(-1, true), widget = this, cm = this.doc.cm | |
113 if (!pos || !cm) return | |
114 runInOp(cm, () => { | |
115 let line = pos.line, lineN = lineNo(pos.line) | |
116 let view = findViewForLine(cm, lineN) | |
117 if (view) { | |
118 clearLineMeasurementCacheFor(view) | |
119 cm.curOp.selectionChanged = cm.curOp.forceUpdate = true | |
120 } | |
121 cm.curOp.updateMaxLine = true | |
122 if (!lineIsHidden(widget.doc, line) && widget.height != null) { | |
123 let oldHeight = widget.height | |
124 widget.height = null | |
125 let dHeight = widgetHeight(widget) - oldHeight | |
126 if (dHeight) | |
127 updateLineHeight(line, line.height + dHeight) | |
128 } | |
129 signalLater(cm, "markerChanged", cm, this) | |
130 }) | |
131 } | |
132 | |
133 attachLine(line) { | |
134 if (!this.lines.length && this.doc.cm) { | |
135 let op = this.doc.cm.curOp | |
136 if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) | |
137 (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this) | |
138 } | |
139 this.lines.push(line) | |
140 } | |
141 | |
142 detachLine(line) { | |
143 this.lines.splice(indexOf(this.lines, line), 1) | |
144 if (!this.lines.length && this.doc.cm) { | |
145 let op = this.doc.cm.curOp | |
146 ;(op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this) | |
147 } | |
148 } | |
149 } | |
150 eventMixin(TextMarker) | |
151 | |
152 // Create a marker, wire it up to the right lines, and | |
153 export function markText(doc, from, to, options, type) { | |
154 // Shared markers (across linked documents) are handled separately | |
155 // (markTextShared will call out to this again, once per | |
156 // document). | |
157 if (options && options.shared) return markTextShared(doc, from, to, options, type) | |
158 // Ensure we are in an operation. | |
159 if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type) | |
160 | |
161 let marker = new TextMarker(doc, type), diff = cmp(from, to) | |
162 if (options) copyObj(options, marker, false) | |
163 // Don't connect empty markers unless clearWhenEmpty is false | |
164 if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) | |
165 return marker | |
166 if (marker.replacedWith) { | |
167 // Showing up as a widget implies collapsed (widget replaces text) | |
168 marker.collapsed = true | |
169 marker.widgetNode = eltP("span", [marker.replacedWith], "CodeMirror-widget") | |
170 if (!options.handleMouseEvents) marker.widgetNode.setAttribute("cm-ignore-events", "true") | |
171 if (options.insertLeft) marker.widgetNode.insertLeft = true | |
172 } | |
173 if (marker.collapsed) { | |
174 if (conflictingCollapsedRange(doc, from.line, from, to, marker) || | |
175 from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker)) | |
176 throw new Error("Inserting collapsed marker partially overlapping an existing one") | |
177 seeCollapsedSpans() | |
178 } | |
179 | |
180 if (marker.addToHistory) | |
181 addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN) | |
182 | |
183 let curLine = from.line, cm = doc.cm, updateMaxLine | |
184 doc.iter(curLine, to.line + 1, line => { | |
185 if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine) | |
186 updateMaxLine = true | |
187 if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0) | |
188 addMarkedSpan(line, new MarkedSpan(marker, | |
189 curLine == from.line ? from.ch : null, | |
190 curLine == to.line ? to.ch : null), doc.cm && doc.cm.curOp) | |
191 ++curLine | |
192 }) | |
193 // lineIsHidden depends on the presence of the spans, so needs a second pass | |
194 if (marker.collapsed) doc.iter(from.line, to.line + 1, line => { | |
195 if (lineIsHidden(doc, line)) updateLineHeight(line, 0) | |
196 }) | |
197 | |
198 if (marker.clearOnEnter) on(marker, "beforeCursorEnter", () => marker.clear()) | |
199 | |
200 if (marker.readOnly) { | |
201 seeReadOnlySpans() | |
202 if (doc.history.done.length || doc.history.undone.length) | |
203 doc.clearHistory() | |
204 } | |
205 if (marker.collapsed) { | |
206 marker.id = ++nextMarkerId | |
207 marker.atomic = true | |
208 } | |
209 if (cm) { | |
210 // Sync editor state | |
211 if (updateMaxLine) cm.curOp.updateMaxLine = true | |
212 if (marker.collapsed) | |
213 regChange(cm, from.line, to.line + 1) | |
214 else if (marker.className || marker.startStyle || marker.endStyle || marker.css || | |
215 marker.attributes || marker.title) | |
216 for (let i = from.line; i <= to.line; i++) regLineChange(cm, i, "text") | |
217 if (marker.atomic) reCheckSelection(cm.doc) | |
218 signalLater(cm, "markerAdded", cm, marker) | |
219 } | |
220 return marker | |
221 } | |
222 | |
223 // SHARED TEXTMARKERS | |
224 | |
225 // A shared marker spans multiple linked documents. It is | |
226 // implemented as a meta-marker-object controlling multiple normal | |
227 // markers. | |
228 export class SharedTextMarker { | |
229 constructor(markers, primary) { | |
230 this.markers = markers | |
231 this.primary = primary | |
232 for (let i = 0; i < markers.length; ++i) | |
233 markers[i].parent = this | |
234 } | |
235 | |
236 clear() { | |
237 if (this.explicitlyCleared) return | |
238 this.explicitlyCleared = true | |
239 for (let i = 0; i < this.markers.length; ++i) | |
240 this.markers[i].clear() | |
241 signalLater(this, "clear") | |
242 } | |
243 | |
244 find(side, lineObj) { | |
245 return this.primary.find(side, lineObj) | |
246 } | |
247 } | |
248 eventMixin(SharedTextMarker) | |
249 | |
250 function markTextShared(doc, from, to, options, type) { | |
251 options = copyObj(options) | |
252 options.shared = false | |
253 let markers = [markText(doc, from, to, options, type)], primary = markers[0] | |
254 let widget = options.widgetNode | |
255 linkedDocs(doc, doc => { | |
256 if (widget) options.widgetNode = widget.cloneNode(true) | |
257 markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)) | |
258 for (let i = 0; i < doc.linked.length; ++i) | |
259 if (doc.linked[i].isParent) return | |
260 primary = lst(markers) | |
261 }) | |
262 return new SharedTextMarker(markers, primary) | |
263 } | |
264 | |
265 export function findSharedMarkers(doc) { | |
266 return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), m => m.parent) | |
267 } | |
268 | |
269 export function copySharedMarkers(doc, markers) { | |
270 for (let i = 0; i < markers.length; i++) { | |
271 let marker = markers[i], pos = marker.find() | |
272 let mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to) | |
273 if (cmp(mFrom, mTo)) { | |
274 let subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type) | |
275 marker.markers.push(subMark) | |
276 subMark.parent = marker | |
277 } | |
278 } | |
279 } | |
280 | |
281 export function detachSharedMarkers(markers) { | |
282 for (let i = 0; i < markers.length; i++) { | |
283 let marker = markers[i], linked = [marker.primary.doc] | |
284 linkedDocs(marker.primary.doc, d => linked.push(d)) | |
285 for (let j = 0; j < marker.markers.length; j++) { | |
286 let subMarker = marker.markers[j] | |
287 if (indexOf(linked, subMarker.doc) == -1) { | |
288 subMarker.parent = null | |
289 marker.markers.splice(j--, 1) | |
290 } | |
291 } | |
292 } | |
293 } |