Doc.js 17 KB

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