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