Doc.js (17215B)
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 return lines.join(lineSep || this.lineSeparator()) 90 }, 91 92 getLine: function(line) {let l = this.getLineHandle(line); return l && l.text}, 93 94 getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line)}, 95 getLineNumber: function(line) {return lineNo(line)}, 96 97 getLineHandleVisualStart: function(line) { 98 if (typeof line == "number") line = getLine(this, line) 99 return visualLine(line) 100 }, 101 102 lineCount: function() {return this.size}, 103 firstLine: function() {return this.first}, 104 lastLine: function() {return this.first + this.size - 1}, 105 106 clipPos: function(pos) {return clipPos(this, pos)}, 107 108 getCursor: function(start) { 109 let range = this.sel.primary(), pos 110 if (start == null || start == "head") pos = range.head 111 else if (start == "anchor") pos = range.anchor 112 else if (start == "end" || start == "to" || start === false) pos = range.to() 113 else pos = range.from() 114 return pos 115 }, 116 listSelections: function() { return this.sel.ranges }, 117 somethingSelected: function() {return this.sel.somethingSelected()}, 118 119 setCursor: docMethodOp(function(line, ch, options) { 120 setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options) 121 }), 122 setSelection: docMethodOp(function(anchor, head, options) { 123 setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options) 124 }), 125 extendSelection: docMethodOp(function(head, other, options) { 126 extendSelection(this, clipPos(this, head), other && clipPos(this, other), options) 127 }), 128 extendSelections: docMethodOp(function(heads, options) { 129 extendSelections(this, clipPosArray(this, heads), options) 130 }), 131 extendSelectionsBy: docMethodOp(function(f, options) { 132 let heads = map(this.sel.ranges, f) 133 extendSelections(this, clipPosArray(this, heads), options) 134 }), 135 setSelections: docMethodOp(function(ranges, primary, options) { 136 if (!ranges.length) return 137 let out = [] 138 for (let i = 0; i < ranges.length; i++) 139 out[i] = new Range(clipPos(this, ranges[i].anchor), 140 clipPos(this, ranges[i].head)) 141 if (primary == null) primary = Math.min(ranges.length - 1, this.sel.primIndex) 142 setSelection(this, normalizeSelection(out, primary), options) 143 }), 144 addSelection: docMethodOp(function(anchor, head, options) { 145 let ranges = this.sel.ranges.slice(0) 146 ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))) 147 setSelection(this, normalizeSelection(ranges, ranges.length - 1), options) 148 }), 149 150 getSelection: function(lineSep) { 151 let ranges = this.sel.ranges, lines 152 for (let i = 0; i < ranges.length; i++) { 153 let sel = getBetween(this, ranges[i].from(), ranges[i].to()) 154 lines = lines ? lines.concat(sel) : sel 155 } 156 if (lineSep === false) return lines 157 else return lines.join(lineSep || this.lineSeparator()) 158 }, 159 getSelections: function(lineSep) { 160 let parts = [], ranges = this.sel.ranges 161 for (let i = 0; i < ranges.length; i++) { 162 let sel = getBetween(this, ranges[i].from(), ranges[i].to()) 163 if (lineSep !== false) sel = sel.join(lineSep || this.lineSeparator()) 164 parts[i] = sel 165 } 166 return parts 167 }, 168 replaceSelection: function(code, collapse, origin) { 169 let dup = [] 170 for (let i = 0; i < this.sel.ranges.length; i++) 171 dup[i] = code 172 this.replaceSelections(dup, collapse, origin || "+input") 173 }, 174 replaceSelections: docMethodOp(function(code, collapse, origin) { 175 let changes = [], sel = this.sel 176 for (let i = 0; i < sel.ranges.length; i++) { 177 let range = sel.ranges[i] 178 changes[i] = {from: range.from(), to: range.to(), text: this.splitLines(code[i]), origin: origin} 179 } 180 let newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse) 181 for (let i = changes.length - 1; i >= 0; i--) 182 makeChange(this, changes[i]) 183 if (newSel) setSelectionReplaceHistory(this, newSel) 184 else if (this.cm) ensureCursorVisible(this.cm) 185 }), 186 undo: docMethodOp(function() {makeChangeFromHistory(this, "undo")}), 187 redo: docMethodOp(function() {makeChangeFromHistory(this, "redo")}), 188 undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true)}), 189 redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true)}), 190 191 setExtending: function(val) {this.extend = val}, 192 getExtending: function() {return this.extend}, 193 194 historySize: function() { 195 let hist = this.history, done = 0, undone = 0 196 for (let i = 0; i < hist.done.length; i++) if (!hist.done[i].ranges) ++done 197 for (let i = 0; i < hist.undone.length; i++) if (!hist.undone[i].ranges) ++undone 198 return {undo: done, redo: undone} 199 }, 200 clearHistory: function() {this.history = new History(this.history.maxGeneration)}, 201 202 markClean: function() { 203 this.cleanGeneration = this.changeGeneration(true) 204 }, 205 changeGeneration: function(forceSplit) { 206 if (forceSplit) 207 this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null 208 return this.history.generation 209 }, 210 isClean: function (gen) { 211 return this.history.generation == (gen || this.cleanGeneration) 212 }, 213 214 getHistory: function() { 215 return {done: copyHistoryArray(this.history.done), 216 undone: copyHistoryArray(this.history.undone)} 217 }, 218 setHistory: function(histData) { 219 let hist = this.history = new History(this.history.maxGeneration) 220 hist.done = copyHistoryArray(histData.done.slice(0), null, true) 221 hist.undone = copyHistoryArray(histData.undone.slice(0), null, true) 222 }, 223 224 setGutterMarker: docMethodOp(function(line, gutterID, value) { 225 return changeLine(this, line, "gutter", line => { 226 let markers = line.gutterMarkers || (line.gutterMarkers = {}) 227 markers[gutterID] = value 228 if (!value && isEmpty(markers)) line.gutterMarkers = null 229 return true 230 }) 231 }), 232 233 clearGutter: docMethodOp(function(gutterID) { 234 this.iter(line => { 235 if (line.gutterMarkers && line.gutterMarkers[gutterID]) { 236 changeLine(this, line, "gutter", () => { 237 line.gutterMarkers[gutterID] = null 238 if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null 239 return true 240 }) 241 } 242 }) 243 }), 244 245 lineInfo: function(line) { 246 let n 247 if (typeof line == "number") { 248 if (!isLine(this, line)) return null 249 n = line 250 line = getLine(this, line) 251 if (!line) return null 252 } else { 253 n = lineNo(line) 254 if (n == null) return null 255 } 256 return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, 257 textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, 258 widgets: line.widgets} 259 }, 260 261 addLineClass: docMethodOp(function(handle, where, cls) { 262 return changeLine(this, handle, where == "gutter" ? "gutter" : "class", line => { 263 let prop = where == "text" ? "textClass" 264 : where == "background" ? "bgClass" 265 : where == "gutter" ? "gutterClass" : "wrapClass" 266 if (!line[prop]) line[prop] = cls 267 else if (classTest(cls).test(line[prop])) return false 268 else line[prop] += " " + cls 269 return true 270 }) 271 }), 272 removeLineClass: docMethodOp(function(handle, where, cls) { 273 return changeLine(this, handle, where == "gutter" ? "gutter" : "class", line => { 274 let prop = where == "text" ? "textClass" 275 : where == "background" ? "bgClass" 276 : where == "gutter" ? "gutterClass" : "wrapClass" 277 let cur = line[prop] 278 if (!cur) return false 279 else if (cls == null) line[prop] = null 280 else { 281 let found = cur.match(classTest(cls)) 282 if (!found) return false 283 let end = found.index + found[0].length 284 line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null 285 } 286 return true 287 }) 288 }), 289 290 addLineWidget: docMethodOp(function(handle, node, options) { 291 return addLineWidget(this, handle, node, options) 292 }), 293 removeLineWidget: function(widget) { widget.clear() }, 294 295 markText: function(from, to, options) { 296 return markText(this, clipPos(this, from), clipPos(this, to), options, options && options.type || "range") 297 }, 298 setBookmark: function(pos, options) { 299 let realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), 300 insertLeft: options && options.insertLeft, 301 clearWhenEmpty: false, shared: options && options.shared, 302 handleMouseEvents: options && options.handleMouseEvents} 303 pos = clipPos(this, pos) 304 return markText(this, pos, pos, realOpts, "bookmark") 305 }, 306 findMarksAt: function(pos) { 307 pos = clipPos(this, pos) 308 let markers = [], spans = getLine(this, pos.line).markedSpans 309 if (spans) for (let i = 0; i < spans.length; ++i) { 310 let span = spans[i] 311 if ((span.from == null || span.from <= pos.ch) && 312 (span.to == null || span.to >= pos.ch)) 313 markers.push(span.marker.parent || span.marker) 314 } 315 return markers 316 }, 317 findMarks: function(from, to, filter) { 318 from = clipPos(this, from); to = clipPos(this, to) 319 let found = [], lineNo = from.line 320 this.iter(from.line, to.line + 1, line => { 321 let spans = line.markedSpans 322 if (spans) for (let i = 0; i < spans.length; i++) { 323 let span = spans[i] 324 if (!(span.to != null && lineNo == from.line && from.ch >= span.to || 325 span.from == null && lineNo != from.line || 326 span.from != null && lineNo == to.line && span.from >= to.ch) && 327 (!filter || filter(span.marker))) 328 found.push(span.marker.parent || span.marker) 329 } 330 ++lineNo 331 }) 332 return found 333 }, 334 getAllMarks: function() { 335 let markers = [] 336 this.iter(line => { 337 let sps = line.markedSpans 338 if (sps) for (let i = 0; i < sps.length; ++i) 339 if (sps[i].from != null) markers.push(sps[i].marker) 340 }) 341 return markers 342 }, 343 344 posFromIndex: function(off) { 345 let ch, lineNo = this.first, sepSize = this.lineSeparator().length 346 this.iter(line => { 347 let sz = line.text.length + sepSize 348 if (sz > off) { ch = off; return true } 349 off -= sz 350 ++lineNo 351 }) 352 return clipPos(this, Pos(lineNo, ch)) 353 }, 354 indexFromPos: function (coords) { 355 coords = clipPos(this, coords) 356 let index = coords.ch 357 if (coords.line < this.first || coords.ch < 0) return 0 358 let sepSize = this.lineSeparator().length 359 this.iter(this.first, coords.line, line => { // iter aborts when callback returns a truthy value 360 index += line.text.length + sepSize 361 }) 362 return index 363 }, 364 365 copy: function(copyHistory) { 366 let doc = new Doc(getLines(this, this.first, this.first + this.size), 367 this.modeOption, this.first, this.lineSep, this.direction) 368 doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft 369 doc.sel = this.sel 370 doc.extend = false 371 if (copyHistory) { 372 doc.history.undoDepth = this.history.undoDepth 373 doc.setHistory(this.getHistory()) 374 } 375 return doc 376 }, 377 378 linkedDoc: function(options) { 379 if (!options) options = {} 380 let from = this.first, to = this.first + this.size 381 if (options.from != null && options.from > from) from = options.from 382 if (options.to != null && options.to < to) to = options.to 383 let copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep, this.direction) 384 if (options.sharedHist) copy.history = this.history 385 ;(this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}) 386 copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}] 387 copySharedMarkers(copy, findSharedMarkers(this)) 388 return copy 389 }, 390 unlinkDoc: function(other) { 391 if (other instanceof CodeMirror) other = other.doc 392 if (this.linked) for (let i = 0; i < this.linked.length; ++i) { 393 let link = this.linked[i] 394 if (link.doc != other) continue 395 this.linked.splice(i, 1) 396 other.unlinkDoc(this) 397 detachSharedMarkers(findSharedMarkers(this)) 398 break 399 } 400 // If the histories were shared, split them again 401 if (other.history == this.history) { 402 let splitIds = [other.id] 403 linkedDocs(other, doc => splitIds.push(doc.id), true) 404 other.history = new History(null) 405 other.history.done = copyHistoryArray(this.history.done, splitIds) 406 other.history.undone = copyHistoryArray(this.history.undone, splitIds) 407 } 408 }, 409 iterLinkedDocs: function(f) {linkedDocs(this, f)}, 410 411 getMode: function() {return this.mode}, 412 getEditor: function() {return this.cm}, 413 414 splitLines: function(str) { 415 if (this.lineSep) return str.split(this.lineSep) 416 return splitLinesAuto(str) 417 }, 418 lineSeparator: function() { return this.lineSep || "\n" }, 419 420 setDirection: docMethodOp(function (dir) { 421 if (dir != "rtl") dir = "ltr" 422 if (dir == this.direction) return 423 this.direction = dir 424 this.iter(line => line.order = null) 425 if (this.cm) directionChanged(this.cm) 426 }) 427 }) 428 429 // Public alias. 430 Doc.prototype.eachLine = Doc.prototype.iter 431 432 export default Doc