0
|
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
|