ecccf4fb108ae4626dd18a0dcd5097572a610ced7faa4d14496e5ba6a9210de6ef814994d83be335a683d05e78ab2fcc4dd2ec45a1bf438422ecc8780a1c3b 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. import { retreatFrontier } from "../line/highlight.js"
  2. import { startWorker } from "../display/highlight_worker.js"
  3. import { operation } from "../display/operations.js"
  4. import { regChange, regLineChange } from "../display/view_tracking.js"
  5. import { clipLine, clipPos, cmp, Pos } from "../line/pos.js"
  6. import { sawReadOnlySpans } from "../line/saw_special_spans.js"
  7. import { lineLength, removeReadOnlyRanges, stretchSpansOverChange, visualLine } from "../line/spans.js"
  8. import { getBetween, getLine, lineNo } from "../line/utils_line.js"
  9. import { estimateHeight } from "../measurement/position_measurement.js"
  10. import { hasHandler, signal, signalCursorActivity } from "../util/event.js"
  11. import { indexOf, lst, map, sel_dontScroll } from "../util/misc.js"
  12. import { signalLater } from "../util/operation_group.js"
  13. import { changeEnd, computeSelAfterChange } from "./change_measurement.js"
  14. import { isWholeLineUpdate, linkedDocs, updateDoc } from "./document_data.js"
  15. import { addChangeToHistory, historyChangeFromChange, mergeOldSpans, pushSelectionToHistory } from "./history.js"
  16. import { Range, Selection } from "./selection.js"
  17. import { setSelection, setSelectionNoUndo, skipAtomic } from "./selection_updates.js"
  18. // UPDATING
  19. // Allow "beforeChange" event handlers to influence a change
  20. function filterChange(doc, change, update) {
  21. let obj = {
  22. canceled: false,
  23. from: change.from,
  24. to: change.to,
  25. text: change.text,
  26. origin: change.origin,
  27. cancel: () => obj.canceled = true
  28. }
  29. if (update) obj.update = (from, to, text, origin) => {
  30. if (from) obj.from = clipPos(doc, from)
  31. if (to) obj.to = clipPos(doc, to)
  32. if (text) obj.text = text
  33. if (origin !== undefined) obj.origin = origin
  34. }
  35. signal(doc, "beforeChange", doc, obj)
  36. if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj)
  37. if (obj.canceled) {
  38. if (doc.cm) doc.cm.curOp.updateInput = 2
  39. return null
  40. }
  41. return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}
  42. }
  43. // Apply a change to a document, and add it to the document's
  44. // history, and propagating it to all linked documents.
  45. export function makeChange(doc, change, ignoreReadOnly) {
  46. if (doc.cm) {
  47. if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly)
  48. if (doc.cm.state.suppressEdits) return
  49. }
  50. if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) {
  51. change = filterChange(doc, change, true)
  52. if (!change) return
  53. }
  54. // Possibly split or suppress the update based on the presence
  55. // of read-only spans in its range.
  56. let split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to)
  57. if (split) {
  58. for (let i = split.length - 1; i >= 0; --i)
  59. makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text, origin: change.origin})
  60. } else {
  61. makeChangeInner(doc, change)
  62. }
  63. }
  64. function makeChangeInner(doc, change) {
  65. if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return
  66. let selAfter = computeSelAfterChange(doc, change)
  67. addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN)
  68. makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change))
  69. let rebased = []
  70. linkedDocs(doc, (doc, sharedHist) => {
  71. if (!sharedHist && indexOf(rebased, doc.history) == -1) {
  72. rebaseHist(doc.history, change)
  73. rebased.push(doc.history)
  74. }
  75. makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change))
  76. })
  77. }
  78. // Revert a change stored in a document's history.
  79. export function makeChangeFromHistory(doc, type, allowSelectionOnly) {
  80. let suppress = doc.cm && doc.cm.state.suppressEdits
  81. if (suppress && !allowSelectionOnly) return
  82. let hist = doc.history, event, selAfter = doc.sel
  83. let source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done
  84. // Verify that there is a useable event (so that ctrl-z won't
  85. // needlessly clear selection events)
  86. let i = 0
  87. for (; i < source.length; i++) {
  88. event = source[i]
  89. if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges)
  90. break
  91. }
  92. if (i == source.length) return
  93. hist.lastOrigin = hist.lastSelOrigin = null
  94. for (;;) {
  95. event = source.pop()
  96. if (event.ranges) {
  97. pushSelectionToHistory(event, dest)
  98. if (allowSelectionOnly && !event.equals(doc.sel)) {
  99. setSelection(doc, event, {clearRedo: false})
  100. return
  101. }
  102. selAfter = event
  103. } else if (suppress) {
  104. source.push(event)
  105. return
  106. } else break
  107. }
  108. // Build up a reverse change object to add to the opposite history
  109. // stack (redo when undoing, and vice versa).
  110. let antiChanges = []
  111. pushSelectionToHistory(selAfter, dest)
  112. dest.push({changes: antiChanges, generation: hist.generation})
  113. hist.generation = event.generation || ++hist.maxGeneration
  114. let filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")
  115. for (let i = event.changes.length - 1; i >= 0; --i) {
  116. let change = event.changes[i]
  117. change.origin = type
  118. if (filter && !filterChange(doc, change, false)) {
  119. source.length = 0
  120. return
  121. }
  122. antiChanges.push(historyChangeFromChange(doc, change))
  123. let after = i ? computeSelAfterChange(doc, change) : lst(source)
  124. makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change))
  125. if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)})
  126. let rebased = []
  127. // Propagate to the linked documents
  128. linkedDocs(doc, (doc, sharedHist) => {
  129. if (!sharedHist && indexOf(rebased, doc.history) == -1) {
  130. rebaseHist(doc.history, change)
  131. rebased.push(doc.history)
  132. }
  133. makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change))
  134. })
  135. }
  136. }
  137. // Sub-views need their line numbers shifted when text is added
  138. // above or below them in the parent document.
  139. function shiftDoc(doc, distance) {
  140. if (distance == 0) return
  141. doc.first += distance
  142. doc.sel = new Selection(map(doc.sel.ranges, range => new Range(
  143. Pos(range.anchor.line + distance, range.anchor.ch),
  144. Pos(range.head.line + distance, range.head.ch)
  145. )), doc.sel.primIndex)
  146. if (doc.cm) {
  147. regChange(doc.cm, doc.first, doc.first - distance, distance)
  148. for (let d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++)
  149. regLineChange(doc.cm, l, "gutter")
  150. }
  151. }
  152. // More lower-level change function, handling only a single document
  153. // (not linked ones).
  154. function makeChangeSingleDoc(doc, change, selAfter, spans) {
  155. if (doc.cm && !doc.cm.curOp)
  156. return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans)
  157. if (change.to.line < doc.first) {
  158. shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line))
  159. return
  160. }
  161. if (change.from.line > doc.lastLine()) return
  162. // Clip the change to the size of this doc
  163. if (change.from.line < doc.first) {
  164. let shift = change.text.length - 1 - (doc.first - change.from.line)
  165. shiftDoc(doc, shift)
  166. change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch),
  167. text: [lst(change.text)], origin: change.origin}
  168. }
  169. let last = doc.lastLine()
  170. if (change.to.line > last) {
  171. change = {from: change.from, to: Pos(last, getLine(doc, last).text.length),
  172. text: [change.text[0]], origin: change.origin}
  173. }
  174. change.removed = getBetween(doc, change.from, change.to)
  175. if (!selAfter) selAfter = computeSelAfterChange(doc, change)
  176. if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans)
  177. else updateDoc(doc, change, spans)
  178. setSelectionNoUndo(doc, selAfter, sel_dontScroll)
  179. if (doc.cantEdit && skipAtomic(doc, Pos(doc.firstLine(), 0)))
  180. doc.cantEdit = false
  181. }
  182. // Handle the interaction of a change to a document with the editor
  183. // that this document is part of.
  184. function makeChangeSingleDocInEditor(cm, change, spans) {
  185. let doc = cm.doc, display = cm.display, from = change.from, to = change.to
  186. let recomputeMaxLength = false, checkWidthStart = from.line
  187. if (!cm.options.lineWrapping) {
  188. checkWidthStart = lineNo(visualLine(getLine(doc, from.line)))
  189. doc.iter(checkWidthStart, to.line + 1, line => {
  190. if (line == display.maxLine) {
  191. recomputeMaxLength = true
  192. return true
  193. }
  194. })
  195. }
  196. if (doc.sel.contains(change.from, change.to) > -1)
  197. signalCursorActivity(cm)
  198. updateDoc(doc, change, spans, estimateHeight(cm))
  199. if (!cm.options.lineWrapping) {
  200. doc.iter(checkWidthStart, from.line + change.text.length, line => {
  201. let len = lineLength(line)
  202. if (len > display.maxLineLength) {
  203. display.maxLine = line
  204. display.maxLineLength = len
  205. display.maxLineChanged = true
  206. recomputeMaxLength = false
  207. }
  208. })
  209. if (recomputeMaxLength) cm.curOp.updateMaxLine = true
  210. }
  211. retreatFrontier(doc, from.line)
  212. startWorker(cm, 400)
  213. let lendiff = change.text.length - (to.line - from.line) - 1
  214. // Remember that these lines changed, for updating the display
  215. if (change.full)
  216. regChange(cm)
  217. else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change))
  218. regLineChange(cm, from.line, "text")
  219. else
  220. regChange(cm, from.line, to.line + 1, lendiff)
  221. let changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change")
  222. if (changeHandler || changesHandler) {
  223. let obj = {
  224. from: from, to: to,
  225. text: change.text,
  226. removed: change.removed,
  227. origin: change.origin
  228. }
  229. if (changeHandler) signalLater(cm, "change", cm, obj)
  230. if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj)
  231. }
  232. cm.display.selForContextMenu = null
  233. }
  234. export function replaceRange(doc, code, from, to, origin) {
  235. if (!to) to = from
  236. if (cmp(to, from) < 0) [from, to] = [to, from]
  237. if (typeof code == "string") code = doc.splitLines(code)
  238. makeChange(doc, {from, to, text: code, origin})
  239. }
  240. // Rebasing/resetting history to deal with externally-sourced changes
  241. function rebaseHistSelSingle(pos, from, to, diff) {
  242. if (to < pos.line) {
  243. pos.line += diff
  244. } else if (from < pos.line) {
  245. pos.line = from
  246. pos.ch = 0
  247. }
  248. }
  249. // Tries to rebase an array of history events given a change in the
  250. // document. If the change touches the same lines as the event, the
  251. // event, and everything 'behind' it, is discarded. If the change is
  252. // before the event, the event's positions are updated. Uses a
  253. // copy-on-write scheme for the positions, to avoid having to
  254. // reallocate them all on every rebase, but also avoid problems with
  255. // shared position objects being unsafely updated.
  256. function rebaseHistArray(array, from, to, diff) {
  257. for (let i = 0; i < array.length; ++i) {
  258. let sub = array[i], ok = true
  259. if (sub.ranges) {
  260. if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true }
  261. for (let j = 0; j < sub.ranges.length; j++) {
  262. rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff)
  263. rebaseHistSelSingle(sub.ranges[j].head, from, to, diff)
  264. }
  265. continue
  266. }
  267. for (let j = 0; j < sub.changes.length; ++j) {
  268. let cur = sub.changes[j]
  269. if (to < cur.from.line) {
  270. cur.from = Pos(cur.from.line + diff, cur.from.ch)
  271. cur.to = Pos(cur.to.line + diff, cur.to.ch)
  272. } else if (from <= cur.to.line) {
  273. ok = false
  274. break
  275. }
  276. }
  277. if (!ok) {
  278. array.splice(0, i + 1)
  279. i = 0
  280. }
  281. }
  282. }
  283. function rebaseHist(hist, change) {
  284. let from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1
  285. rebaseHistArray(hist.done, from, to, diff)
  286. rebaseHistArray(hist.undone, from, to, diff)
  287. }
  288. // Utility for applying a change to a line by handle or number,
  289. // returning the number and optionally registering the line as
  290. // changed.
  291. export function changeLine(doc, handle, changeType, op) {
  292. let no = handle, line = handle
  293. if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle))
  294. else no = lineNo(handle)
  295. if (no == null) return null
  296. if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType)
  297. return line
  298. }