import { retreatFrontier } from "../line/highlight.js" import { startWorker } from "../display/highlight_worker.js" import { operation } from "../display/operations.js" import { regChange, regLineChange } from "../display/view_tracking.js" import { clipLine, clipPos, cmp, Pos } from "../line/pos.js" import { sawReadOnlySpans } from "../line/saw_special_spans.js" import { lineLength, removeReadOnlyRanges, stretchSpansOverChange, visualLine } from "../line/spans.js" import { getBetween, getLine, lineNo } from "../line/utils_line.js" import { estimateHeight } from "../measurement/position_measurement.js" import { hasHandler, signal, signalCursorActivity } from "../util/event.js" import { indexOf, lst, map, sel_dontScroll } from "../util/misc.js" import { signalLater } from "../util/operation_group.js" import { changeEnd, computeSelAfterChange } from "./change_measurement.js" import { isWholeLineUpdate, linkedDocs, updateDoc } from "./document_data.js" import { addChangeToHistory, historyChangeFromChange, mergeOldSpans, pushSelectionToHistory } from "./history.js" import { Range, Selection } from "./selection.js" import { setSelection, setSelectionNoUndo } from "./selection_updates.js" // UPDATING // Allow "beforeChange" event handlers to influence a change function filterChange(doc, change, update) { let obj = { canceled: false, from: change.from, to: change.to, text: change.text, origin: change.origin, cancel: () => obj.canceled = true } if (update) obj.update = (from, to, text, origin) => { if (from) obj.from = clipPos(doc, from) if (to) obj.to = clipPos(doc, to) if (text) obj.text = text if (origin !== undefined) obj.origin = origin } signal(doc, "beforeChange", doc, obj) if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj) if (obj.canceled) return null return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin} } // Apply a change to a document, and add it to the document's // history, and propagating it to all linked documents. export function makeChange(doc, change, ignoreReadOnly) { if (doc.cm) { if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly) if (doc.cm.state.suppressEdits) return } if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { change = filterChange(doc, change, true) if (!change) return } // Possibly split or suppress the update based on the presence // of read-only spans in its range. let split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to) if (split) { for (let i = split.length - 1; i >= 0; --i) makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text, origin: change.origin}) } else { makeChangeInner(doc, change) } } function makeChangeInner(doc, change) { if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return let selAfter = computeSelAfterChange(doc, change) addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN) makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)) let rebased = [] linkedDocs(doc, (doc, sharedHist) => { if (!sharedHist && indexOf(rebased, doc.history) == -1) { rebaseHist(doc.history, change) rebased.push(doc.history) } makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)) }) } // Revert a change stored in a document's history. export function makeChangeFromHistory(doc, type, allowSelectionOnly) { if (doc.cm && doc.cm.state.suppressEdits && !allowSelectionOnly) return let hist = doc.history, event, selAfter = doc.sel let source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done // Verify that there is a useable event (so that ctrl-z won't // needlessly clear selection events) let i = 0 for (; i < source.length; i++) { event = source[i] if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) break } if (i == source.length) return hist.lastOrigin = hist.lastSelOrigin = null for (;;) { event = source.pop() if (event.ranges) { pushSelectionToHistory(event, dest) if (allowSelectionOnly && !event.equals(doc.sel)) { setSelection(doc, event, {clearRedo: false}) return } selAfter = event } else break } // Build up a reverse change object to add to the opposite history // stack (redo when undoing, and vice versa). let antiChanges = [] pushSelectionToHistory(selAfter, dest) dest.push({changes: antiChanges, generation: hist.generation}) hist.generation = event.generation || ++hist.maxGeneration let filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange") for (let i = event.changes.length - 1; i >= 0; --i) { let change = event.changes[i] change.origin = type if (filter && !filterChange(doc, change, false)) { source.length = 0 return } antiChanges.push(historyChangeFromChange(doc, change)) let after = i ? computeSelAfterChange(doc, change) : lst(source) makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)) if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)}) let rebased = [] // Propagate to the linked documents linkedDocs(doc, (doc, sharedHist) => { if (!sharedHist && indexOf(rebased, doc.history) == -1) { rebaseHist(doc.history, change) rebased.push(doc.history) } makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)) }) } } // Sub-views need their line numbers shifted when text is added // above or below them in the parent document. function shiftDoc(doc, distance) { if (distance == 0) return doc.first += distance doc.sel = new Selection(map(doc.sel.ranges, range => new Range( Pos(range.anchor.line + distance, range.anchor.ch), Pos(range.head.line + distance, range.head.ch) )), doc.sel.primIndex) if (doc.cm) { regChange(doc.cm, doc.first, doc.first - distance, distance) for (let d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++) regLineChange(doc.cm, l, "gutter") } } // More lower-level change function, handling only a single document // (not linked ones). function makeChangeSingleDoc(doc, change, selAfter, spans) { if (doc.cm && !doc.cm.curOp) return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans) if (change.to.line < doc.first) { shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)) return } if (change.from.line > doc.lastLine()) return // Clip the change to the size of this doc if (change.from.line < doc.first) { let shift = change.text.length - 1 - (doc.first - change.from.line) shiftDoc(doc, shift) change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), text: [lst(change.text)], origin: change.origin} } let last = doc.lastLine() if (change.to.line > last) { change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), text: [change.text[0]], origin: change.origin} } change.removed = getBetween(doc, change.from, change.to) if (!selAfter) selAfter = computeSelAfterChange(doc, change) if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans) else updateDoc(doc, change, spans) setSelectionNoUndo(doc, selAfter, sel_dontScroll) } // Handle the interaction of a change to a document with the editor // that this document is part of. function makeChangeSingleDocInEditor(cm, change, spans) { let doc = cm.doc, display = cm.display, from = change.from, to = change.to let recomputeMaxLength = false, checkWidthStart = from.line if (!cm.options.lineWrapping) { checkWidthStart = lineNo(visualLine(getLine(doc, from.line))) doc.iter(checkWidthStart, to.line + 1, line => { if (line == display.maxLine) { recomputeMaxLength = true return true } }) } if (doc.sel.contains(change.from, change.to) > -1) signalCursorActivity(cm) updateDoc(doc, change, spans, estimateHeight(cm)) if (!cm.options.lineWrapping) { doc.iter(checkWidthStart, from.line + change.text.length, line => { let len = lineLength(line) if (len > display.maxLineLength) { display.maxLine = line display.maxLineLength = len display.maxLineChanged = true recomputeMaxLength = false } }) if (recomputeMaxLength) cm.curOp.updateMaxLine = true } retreatFrontier(doc, from.line) startWorker(cm, 400) let lendiff = change.text.length - (to.line - from.line) - 1 // Remember that these lines changed, for updating the display if (change.full) regChange(cm) else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) regLineChange(cm, from.line, "text") else regChange(cm, from.line, to.line + 1, lendiff) let changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change") if (changeHandler || changesHandler) { let obj = { from: from, to: to, text: change.text, removed: change.removed, origin: change.origin } if (changeHandler) signalLater(cm, "change", cm, obj) if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj) } cm.display.selForContextMenu = null } export function replaceRange(doc, code, from, to, origin) { if (!to) to = from if (cmp(to, from) < 0) [from, to] = [to, from] if (typeof code == "string") code = doc.splitLines(code) makeChange(doc, {from, to, text: code, origin}) } // Rebasing/resetting history to deal with externally-sourced changes function rebaseHistSelSingle(pos, from, to, diff) { if (to < pos.line) { pos.line += diff } else if (from < pos.line) { pos.line = from pos.ch = 0 } } // Tries to rebase an array of history events given a change in the // document. If the change touches the same lines as the event, the // event, and everything 'behind' it, is discarded. If the change is // before the event, the event's positions are updated. Uses a // copy-on-write scheme for the positions, to avoid having to // reallocate them all on every rebase, but also avoid problems with // shared position objects being unsafely updated. function rebaseHistArray(array, from, to, diff) { for (let i = 0; i < array.length; ++i) { let sub = array[i], ok = true if (sub.ranges) { if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true } for (let j = 0; j < sub.ranges.length; j++) { rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff) rebaseHistSelSingle(sub.ranges[j].head, from, to, diff) } continue } for (let j = 0; j < sub.changes.length; ++j) { let cur = sub.changes[j] if (to < cur.from.line) { cur.from = Pos(cur.from.line + diff, cur.from.ch) cur.to = Pos(cur.to.line + diff, cur.to.ch) } else if (from <= cur.to.line) { ok = false break } } if (!ok) { array.splice(0, i + 1) i = 0 } } } function rebaseHist(hist, change) { let from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1 rebaseHistArray(hist.done, from, to, diff) rebaseHistArray(hist.undone, from, to, diff) } // Utility for applying a change to a line by handle or number, // returning the number and optionally registering the line as // changed. export function changeLine(doc, handle, changeType, op) { let no = handle, line = handle if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle)) else no = lineNo(handle) if (no == null) return null if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType) return line }