Mercurial
comparison .cms/lib/codemirror/src/model/Doc.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 CodeMirror from "../edit/CodeMirror.js" | |
2 import { docMethodOp } from "../display/operations.js" | |
3 import { Line } from "../line/line_data.js" | |
4 import { clipPos, clipPosArray, Pos } from "../line/pos.js" | |
5 import { visualLine } from "../line/spans.js" | |
6 import { getBetween, getLine, getLines, isLine, lineNo } from "../line/utils_line.js" | |
7 import { classTest } from "../util/dom.js" | |
8 import { splitLinesAuto } from "../util/feature_detection.js" | |
9 import { createObj, map, isEmpty, sel_dontScroll } from "../util/misc.js" | |
10 import { ensureCursorVisible, scrollToCoords } from "../display/scrolling.js" | |
11 | |
12 import { changeLine, makeChange, makeChangeFromHistory, replaceRange } from "./changes.js" | |
13 import { computeReplacedSel } from "./change_measurement.js" | |
14 import { BranchChunk, LeafChunk } from "./chunk.js" | |
15 import { directionChanged, linkedDocs, updateDoc } from "./document_data.js" | |
16 import { copyHistoryArray, History } from "./history.js" | |
17 import { addLineWidget } from "./line_widget.js" | |
18 import { copySharedMarkers, detachSharedMarkers, findSharedMarkers, markText } from "./mark_text.js" | |
19 import { normalizeSelection, Range, simpleSelection } from "./selection.js" | |
20 import { extendSelection, extendSelections, setSelection, setSelectionReplaceHistory, setSimpleSelection } from "./selection_updates.js" | |
21 | |
22 let nextDocId = 0 | |
23 let Doc = function(text, mode, firstLine, lineSep, direction) { | |
24 if (!(this instanceof Doc)) return new Doc(text, mode, firstLine, lineSep, direction) | |
25 if (firstLine == null) firstLine = 0 | |
26 | |
27 BranchChunk.call(this, [new LeafChunk([new Line("", null)])]) | |
28 this.first = firstLine | |
29 this.scrollTop = this.scrollLeft = 0 | |
30 this.cantEdit = false | |
31 this.cleanGeneration = 1 | |
32 this.modeFrontier = this.highlightFrontier = firstLine | |
33 let start = Pos(firstLine, 0) | |
34 this.sel = simpleSelection(start) | |
35 this.history = new History(null) | |
36 this.id = ++nextDocId | |
37 this.modeOption = mode | |
38 this.lineSep = lineSep | |
39 this.direction = (direction == "rtl") ? "rtl" : "ltr" | |
40 this.extend = false | |
41 | |
42 if (typeof text == "string") text = this.splitLines(text) | |
43 updateDoc(this, {from: start, to: start, text: text}) | |
44 setSelection(this, simpleSelection(start), sel_dontScroll) | |
45 } | |
46 | |
47 Doc.prototype = createObj(BranchChunk.prototype, { | |
48 constructor: Doc, | |
49 // Iterate over the document. Supports two forms -- with only one | |
50 // argument, it calls that for each line in the document. With | |
51 // three, it iterates over the range given by the first two (with | |
52 // the second being non-inclusive). | |
53 iter: function(from, to, op) { | |
54 if (op) this.iterN(from - this.first, to - from, op) | |
55 else this.iterN(this.first, this.first + this.size, from) | |
56 }, | |
57 | |
58 // Non-public interface for adding and removing lines. | |
59 insert: function(at, lines) { | |
60 let height = 0 | |
61 for (let i = 0; i < lines.length; ++i) height += lines[i].height | |
62 this.insertInner(at - this.first, lines, height) | |
63 }, | |
64 remove: function(at, n) { this.removeInner(at - this.first, n) }, | |
65 | |
66 // From here, the methods are part of the public interface. Most | |
67 // are also available from CodeMirror (editor) instances. | |
68 | |
69 getValue: function(lineSep) { | |
70 let lines = getLines(this, this.first, this.first + this.size) | |
71 if (lineSep === false) return lines | |
72 return lines.join(lineSep || this.lineSeparator()) | |
73 }, | |
74 setValue: docMethodOp(function(code) { | |
75 let top = Pos(this.first, 0), last = this.first + this.size - 1 | |
76 makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), | |
77 text: this.splitLines(code), origin: "setValue", full: true}, true) | |
78 if (this.cm) scrollToCoords(this.cm, 0, 0) | |
79 setSelection(this, simpleSelection(top), sel_dontScroll) | |
80 }), | |
81 replaceRange: function(code, from, to, origin) { | |
82 from = clipPos(this, from) | |
83 to = to ? clipPos(this, to) : from | |
84 replaceRange(this, code, from, to, origin) | |
85 }, | |
86 getRange: function(from, to, lineSep) { | |
87 let lines = getBetween(this, clipPos(this, from), clipPos(this, to)) | |
88 if (lineSep === false) return lines | |
89 if (lineSep === '') return lines.join('') | |
90 return lines.join(lineSep || this.lineSeparator()) | |
91 }, | |
92 | |
93 getLine: function(line) {let l = this.getLineHandle(line); return l && l.text}, | |
94 | |
95 getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line)}, | |
96 getLineNumber: function(line) {return lineNo(line)}, | |
97 | |
98 getLineHandleVisualStart: function(line) { | |
99 if (typeof line == "number") line = getLine(this, line) | |
100 return visualLine(line) | |
101 }, | |
102 | |
103 lineCount: function() {return this.size}, | |
104 firstLine: function() {return this.first}, | |
105 lastLine: function() {return this.first + this.size - 1}, | |
106 | |
107 clipPos: function(pos) {return clipPos(this, pos)}, | |
108 | |
109 getCursor: function(start) { | |
110 let range = this.sel.primary(), pos | |
111 if (start == null || start == "head") pos = range.head | |
112 else if (start == "anchor") pos = range.anchor | |
113 else if (start == "end" || start == "to" || start === false) pos = range.to() | |
114 else pos = range.from() | |
115 return pos | |
116 }, | |
117 listSelections: function() { return this.sel.ranges }, | |
118 somethingSelected: function() {return this.sel.somethingSelected()}, | |
119 | |
120 setCursor: docMethodOp(function(line, ch, options) { | |
121 setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options) | |
122 }), | |
123 setSelection: docMethodOp(function(anchor, head, options) { | |
124 setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options) | |
125 }), | |
126 extendSelection: docMethodOp(function(head, other, options) { | |
127 extendSelection(this, clipPos(this, head), other && clipPos(this, other), options) | |
128 }), | |
129 extendSelections: docMethodOp(function(heads, options) { | |
130 extendSelections(this, clipPosArray(this, heads), options) | |
131 }), | |
132 extendSelectionsBy: docMethodOp(function(f, options) { | |
133 let heads = map(this.sel.ranges, f) | |
134 extendSelections(this, clipPosArray(this, heads), options) | |
135 }), | |
136 setSelections: docMethodOp(function(ranges, primary, options) { | |
137 if (!ranges.length) return | |
138 let out = [] | |
139 for (let i = 0; i < ranges.length; i++) | |
140 out[i] = new Range(clipPos(this, ranges[i].anchor), | |
141 clipPos(this, ranges[i].head || ranges[i].anchor)) | |
142 if (primary == null) primary = Math.min(ranges.length - 1, this.sel.primIndex) | |
143 setSelection(this, normalizeSelection(this.cm, out, primary), options) | |
144 }), | |
145 addSelection: docMethodOp(function(anchor, head, options) { | |
146 let ranges = this.sel.ranges.slice(0) | |
147 ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))) | |
148 setSelection(this, normalizeSelection(this.cm, ranges, ranges.length - 1), options) | |
149 }), | |
150 | |
151 getSelection: function(lineSep) { | |
152 let ranges = this.sel.ranges, lines | |
153 for (let i = 0; i < ranges.length; i++) { | |
154 let sel = getBetween(this, ranges[i].from(), ranges[i].to()) | |
155 lines = lines ? lines.concat(sel) : sel | |
156 } | |
157 if (lineSep === false) return lines | |
158 else return lines.join(lineSep || this.lineSeparator()) | |
159 }, | |
160 getSelections: function(lineSep) { | |
161 let parts = [], ranges = this.sel.ranges | |
162 for (let i = 0; i < ranges.length; i++) { | |
163 let sel = getBetween(this, ranges[i].from(), ranges[i].to()) | |
164 if (lineSep !== false) sel = sel.join(lineSep || this.lineSeparator()) | |
165 parts[i] = sel | |
166 } | |
167 return parts | |
168 }, | |
169 replaceSelection: function(code, collapse, origin) { | |
170 let dup = [] | |
171 for (let i = 0; i < this.sel.ranges.length; i++) | |
172 dup[i] = code | |
173 this.replaceSelections(dup, collapse, origin || "+input") | |
174 }, | |
175 replaceSelections: docMethodOp(function(code, collapse, origin) { | |
176 let changes = [], sel = this.sel | |
177 for (let i = 0; i < sel.ranges.length; i++) { | |
178 let range = sel.ranges[i] | |
179 changes[i] = {from: range.from(), to: range.to(), text: this.splitLines(code[i]), origin: origin} | |
180 } | |
181 let newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse) | |
182 for (let i = changes.length - 1; i >= 0; i--) | |
183 makeChange(this, changes[i]) | |
184 if (newSel) setSelectionReplaceHistory(this, newSel) | |
185 else if (this.cm) ensureCursorVisible(this.cm) | |
186 }), | |
187 undo: docMethodOp(function() {makeChangeFromHistory(this, "undo")}), | |
188 redo: docMethodOp(function() {makeChangeFromHistory(this, "redo")}), | |
189 undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true)}), | |
190 redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true)}), | |
191 | |
192 setExtending: function(val) {this.extend = val}, | |
193 getExtending: function() {return this.extend}, | |
194 | |
195 historySize: function() { | |
196 let hist = this.history, done = 0, undone = 0 | |
197 for (let i = 0; i < hist.done.length; i++) if (!hist.done[i].ranges) ++done | |
198 for (let i = 0; i < hist.undone.length; i++) if (!hist.undone[i].ranges) ++undone | |
199 return {undo: done, redo: undone} | |
200 }, | |
201 clearHistory: function() { | |
202 this.history = new History(this.history) | |
203 linkedDocs(this, doc => doc.history = this.history, true) | |
204 }, | |
205 | |
206 markClean: function() { | |
207 this.cleanGeneration = this.changeGeneration(true) | |
208 }, | |
209 changeGeneration: function(forceSplit) { | |
210 if (forceSplit) | |
211 this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null | |
212 return this.history.generation | |
213 }, | |
214 isClean: function (gen) { | |
215 return this.history.generation == (gen || this.cleanGeneration) | |
216 }, | |
217 | |
218 getHistory: function() { | |
219 return {done: copyHistoryArray(this.history.done), | |
220 undone: copyHistoryArray(this.history.undone)} | |
221 }, | |
222 setHistory: function(histData) { | |
223 let hist = this.history = new History(this.history) | |
224 hist.done = copyHistoryArray(histData.done.slice(0), null, true) | |
225 hist.undone = copyHistoryArray(histData.undone.slice(0), null, true) | |
226 }, | |
227 | |
228 setGutterMarker: docMethodOp(function(line, gutterID, value) { | |
229 return changeLine(this, line, "gutter", line => { | |
230 let markers = line.gutterMarkers || (line.gutterMarkers = {}) | |
231 markers[gutterID] = value | |
232 if (!value && isEmpty(markers)) line.gutterMarkers = null | |
233 return true | |
234 }) | |
235 }), | |
236 | |
237 clearGutter: docMethodOp(function(gutterID) { | |
238 this.iter(line => { | |
239 if (line.gutterMarkers && line.gutterMarkers[gutterID]) { | |
240 changeLine(this, line, "gutter", () => { | |
241 line.gutterMarkers[gutterID] = null | |
242 if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null | |
243 return true | |
244 }) | |
245 } | |
246 }) | |
247 }), | |
248 | |
249 lineInfo: function(line) { | |
250 let n | |
251 if (typeof line == "number") { | |
252 if (!isLine(this, line)) return null | |
253 n = line | |
254 line = getLine(this, line) | |
255 if (!line) return null | |
256 } else { | |
257 n = lineNo(line) | |
258 if (n == null) return null | |
259 } | |
260 return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, | |
261 textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, | |
262 widgets: line.widgets} | |
263 }, | |
264 | |
265 addLineClass: docMethodOp(function(handle, where, cls) { | |
266 return changeLine(this, handle, where == "gutter" ? "gutter" : "class", line => { | |
267 let prop = where == "text" ? "textClass" | |
268 : where == "background" ? "bgClass" | |
269 : where == "gutter" ? "gutterClass" : "wrapClass" | |
270 if (!line[prop]) line[prop] = cls | |
271 else if (classTest(cls).test(line[prop])) return false | |
272 else line[prop] += " " + cls | |
273 return true | |
274 }) | |
275 }), | |
276 removeLineClass: docMethodOp(function(handle, where, cls) { | |
277 return changeLine(this, handle, where == "gutter" ? "gutter" : "class", line => { | |
278 let prop = where == "text" ? "textClass" | |
279 : where == "background" ? "bgClass" | |
280 : where == "gutter" ? "gutterClass" : "wrapClass" | |
281 let cur = line[prop] | |
282 if (!cur) return false | |
283 else if (cls == null) line[prop] = null | |
284 else { | |
285 let found = cur.match(classTest(cls)) | |
286 if (!found) return false | |
287 let end = found.index + found[0].length | |
288 line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null | |
289 } | |
290 return true | |
291 }) | |
292 }), | |
293 | |
294 addLineWidget: docMethodOp(function(handle, node, options) { | |
295 return addLineWidget(this, handle, node, options) | |
296 }), | |
297 removeLineWidget: function(widget) { widget.clear() }, | |
298 | |
299 markText: function(from, to, options) { | |
300 return markText(this, clipPos(this, from), clipPos(this, to), options, options && options.type || "range") | |
301 }, | |
302 setBookmark: function(pos, options) { | |
303 let realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), | |
304 insertLeft: options && options.insertLeft, | |
305 clearWhenEmpty: false, shared: options && options.shared, | |
306 handleMouseEvents: options && options.handleMouseEvents} | |
307 pos = clipPos(this, pos) | |
308 return markText(this, pos, pos, realOpts, "bookmark") | |
309 }, | |
310 findMarksAt: function(pos) { | |
311 pos = clipPos(this, pos) | |
312 let markers = [], spans = getLine(this, pos.line).markedSpans | |
313 if (spans) for (let i = 0; i < spans.length; ++i) { | |
314 let span = spans[i] | |
315 if ((span.from == null || span.from <= pos.ch) && | |
316 (span.to == null || span.to >= pos.ch)) | |
317 markers.push(span.marker.parent || span.marker) | |
318 } | |
319 return markers | |
320 }, | |
321 findMarks: function(from, to, filter) { | |
322 from = clipPos(this, from); to = clipPos(this, to) | |
323 let found = [], lineNo = from.line | |
324 this.iter(from.line, to.line + 1, line => { | |
325 let spans = line.markedSpans | |
326 if (spans) for (let i = 0; i < spans.length; i++) { | |
327 let span = spans[i] | |
328 if (!(span.to != null && lineNo == from.line && from.ch >= span.to || | |
329 span.from == null && lineNo != from.line || | |
330 span.from != null && lineNo == to.line && span.from >= to.ch) && | |
331 (!filter || filter(span.marker))) | |
332 found.push(span.marker.parent || span.marker) | |
333 } | |
334 ++lineNo | |
335 }) | |
336 return found | |
337 }, | |
338 getAllMarks: function() { | |
339 let markers = [] | |
340 this.iter(line => { | |
341 let sps = line.markedSpans | |
342 if (sps) for (let i = 0; i < sps.length; ++i) | |
343 if (sps[i].from != null) markers.push(sps[i].marker) | |
344 }) | |
345 return markers | |
346 }, | |
347 | |
348 posFromIndex: function(off) { | |
349 let ch, lineNo = this.first, sepSize = this.lineSeparator().length | |
350 this.iter(line => { | |
351 let sz = line.text.length + sepSize | |
352 if (sz > off) { ch = off; return true } | |
353 off -= sz | |
354 ++lineNo | |
355 }) | |
356 return clipPos(this, Pos(lineNo, ch)) | |
357 }, | |
358 indexFromPos: function (coords) { | |
359 coords = clipPos(this, coords) | |
360 let index = coords.ch | |
361 if (coords.line < this.first || coords.ch < 0) return 0 | |
362 let sepSize = this.lineSeparator().length | |
363 this.iter(this.first, coords.line, line => { // iter aborts when callback returns a truthy value | |
364 index += line.text.length + sepSize | |
365 }) | |
366 return index | |
367 }, | |
368 | |
369 copy: function(copyHistory) { | |
370 let doc = new Doc(getLines(this, this.first, this.first + this.size), | |
371 this.modeOption, this.first, this.lineSep, this.direction) | |
372 doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft | |
373 doc.sel = this.sel | |
374 doc.extend = false | |
375 if (copyHistory) { | |
376 doc.history.undoDepth = this.history.undoDepth | |
377 doc.setHistory(this.getHistory()) | |
378 } | |
379 return doc | |
380 }, | |
381 | |
382 linkedDoc: function(options) { | |
383 if (!options) options = {} | |
384 let from = this.first, to = this.first + this.size | |
385 if (options.from != null && options.from > from) from = options.from | |
386 if (options.to != null && options.to < to) to = options.to | |
387 let copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep, this.direction) | |
388 if (options.sharedHist) copy.history = this.history | |
389 ;(this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}) | |
390 copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}] | |
391 copySharedMarkers(copy, findSharedMarkers(this)) | |
392 return copy | |
393 }, | |
394 unlinkDoc: function(other) { | |
395 if (other instanceof CodeMirror) other = other.doc | |
396 if (this.linked) for (let i = 0; i < this.linked.length; ++i) { | |
397 let link = this.linked[i] | |
398 if (link.doc != other) continue | |
399 this.linked.splice(i, 1) | |
400 other.unlinkDoc(this) | |
401 detachSharedMarkers(findSharedMarkers(this)) | |
402 break | |
403 } | |
404 // If the histories were shared, split them again | |
405 if (other.history == this.history) { | |
406 let splitIds = [other.id] | |
407 linkedDocs(other, doc => splitIds.push(doc.id), true) | |
408 other.history = new History(null) | |
409 other.history.done = copyHistoryArray(this.history.done, splitIds) | |
410 other.history.undone = copyHistoryArray(this.history.undone, splitIds) | |
411 } | |
412 }, | |
413 iterLinkedDocs: function(f) {linkedDocs(this, f)}, | |
414 | |
415 getMode: function() {return this.mode}, | |
416 getEditor: function() {return this.cm}, | |
417 | |
418 splitLines: function(str) { | |
419 if (this.lineSep) return str.split(this.lineSep) | |
420 return splitLinesAuto(str) | |
421 }, | |
422 lineSeparator: function() { return this.lineSep || "\n" }, | |
423 | |
424 setDirection: docMethodOp(function (dir) { | |
425 if (dir != "rtl") dir = "ltr" | |
426 if (dir == this.direction) return | |
427 this.direction = dir | |
428 this.iter(line => line.order = null) | |
429 if (this.cm) directionChanged(this.cm) | |
430 }) | |
431 }) | |
432 | |
433 // Public alias. | |
434 Doc.prototype.eachLine = Doc.prototype.iter | |
435 | |
436 export default Doc |