864e9d112a553ef983c4eb87585d60169aba369fc45288ad92a76ec4e1753659cd55f16280d7355991bcb4e8583929a1fd42363dd86d06e2676de903aa7f3b 17 KB

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