Spaces:
Sleeping
Sleeping
File size: 11,947 Bytes
07c3cdd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 | import { startWorker } from "../display/highlight_worker"
import { operation } from "../display/operations"
import { regChange, regLineChange } from "../display/view_tracking"
import { clipLine, clipPos, cmp, Pos } from "../line/pos"
import { sawReadOnlySpans } from "../line/saw_special_spans"
import { lineLength, removeReadOnlyRanges, stretchSpansOverChange, visualLine } from "../line/spans"
import { getBetween, getLine, lineNo } from "../line/utils_line"
import { estimateHeight } from "../measurement/position_measurement"
import { hasHandler, signal, signalCursorActivity } from "../util/event"
import { indexOf, lst, map, sel_dontScroll } from "../util/misc"
import { signalLater } from "../util/operation_group"
import { changeEnd, computeSelAfterChange } from "./change_measurement"
import { isWholeLineUpdate, linkedDocs, updateDoc } from "./document_data"
import { addChangeToHistory, historyChangeFromChange, mergeOldSpans, pushSelectionToHistory } from "./history"
import { Range, Selection } from "./selection"
import { setSelection, setSelectionNoUndo } from "./selection_updates"
// 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})
} 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
}
// Adjust frontier, schedule worker
doc.frontier = Math.min(doc.frontier, 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) { let tmp = to; to = from; from = tmp }
if (typeof code == "string") code = doc.splitLines(code)
makeChange(doc, {from: from, to: to, text: code, origin: 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
}
|