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