Mercurial
comparison .cms/lib/codemirror/src/model/history.js @ 0:78edf6b517a0 draft
24.10
author | Coffee CMS <info@coffee-cms.ru> |
---|---|
date | Fri, 11 Oct 2024 22:40:23 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:78edf6b517a0 |
---|---|
1 import { cmp, copyPos } from "../line/pos.js" | |
2 import { stretchSpansOverChange } from "../line/spans.js" | |
3 import { getBetween } from "../line/utils_line.js" | |
4 import { signal } from "../util/event.js" | |
5 import { indexOf, lst } from "../util/misc.js" | |
6 | |
7 import { changeEnd } from "./change_measurement.js" | |
8 import { linkedDocs } from "./document_data.js" | |
9 import { Selection } from "./selection.js" | |
10 | |
11 export function History(prev) { | |
12 // Arrays of change events and selections. Doing something adds an | |
13 // event to done and clears undo. Undoing moves events from done | |
14 // to undone, redoing moves them in the other direction. | |
15 this.done = []; this.undone = [] | |
16 this.undoDepth = prev ? prev.undoDepth : Infinity | |
17 // Used to track when changes can be merged into a single undo | |
18 // event | |
19 this.lastModTime = this.lastSelTime = 0 | |
20 this.lastOp = this.lastSelOp = null | |
21 this.lastOrigin = this.lastSelOrigin = null | |
22 // Used by the isClean() method | |
23 this.generation = this.maxGeneration = prev ? prev.maxGeneration : 1 | |
24 } | |
25 | |
26 // Create a history change event from an updateDoc-style change | |
27 // object. | |
28 export function historyChangeFromChange(doc, change) { | |
29 let histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)} | |
30 attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1) | |
31 linkedDocs(doc, doc => attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1), true) | |
32 return histChange | |
33 } | |
34 | |
35 // Pop all selection events off the end of a history array. Stop at | |
36 // a change event. | |
37 function clearSelectionEvents(array) { | |
38 while (array.length) { | |
39 let last = lst(array) | |
40 if (last.ranges) array.pop() | |
41 else break | |
42 } | |
43 } | |
44 | |
45 // Find the top change event in the history. Pop off selection | |
46 // events that are in the way. | |
47 function lastChangeEvent(hist, force) { | |
48 if (force) { | |
49 clearSelectionEvents(hist.done) | |
50 return lst(hist.done) | |
51 } else if (hist.done.length && !lst(hist.done).ranges) { | |
52 return lst(hist.done) | |
53 } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) { | |
54 hist.done.pop() | |
55 return lst(hist.done) | |
56 } | |
57 } | |
58 | |
59 // Register a change in the history. Merges changes that are within | |
60 // a single operation, or are close together with an origin that | |
61 // allows merging (starting with "+") into a single event. | |
62 export function addChangeToHistory(doc, change, selAfter, opId) { | |
63 let hist = doc.history | |
64 hist.undone.length = 0 | |
65 let time = +new Date, cur | |
66 let last | |
67 | |
68 if ((hist.lastOp == opId || | |
69 hist.lastOrigin == change.origin && change.origin && | |
70 ((change.origin.charAt(0) == "+" && hist.lastModTime > time - (doc.cm ? doc.cm.options.historyEventDelay : 500)) || | |
71 change.origin.charAt(0) == "*")) && | |
72 (cur = lastChangeEvent(hist, hist.lastOp == opId))) { | |
73 // Merge this change into the last event | |
74 last = lst(cur.changes) | |
75 if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) { | |
76 // Optimized case for simple insertion -- don't want to add | |
77 // new changesets for every character typed | |
78 last.to = changeEnd(change) | |
79 } else { | |
80 // Add new sub-event | |
81 cur.changes.push(historyChangeFromChange(doc, change)) | |
82 } | |
83 } else { | |
84 // Can not be merged, start a new event. | |
85 let before = lst(hist.done) | |
86 if (!before || !before.ranges) | |
87 pushSelectionToHistory(doc.sel, hist.done) | |
88 cur = {changes: [historyChangeFromChange(doc, change)], | |
89 generation: hist.generation} | |
90 hist.done.push(cur) | |
91 while (hist.done.length > hist.undoDepth) { | |
92 hist.done.shift() | |
93 if (!hist.done[0].ranges) hist.done.shift() | |
94 } | |
95 } | |
96 hist.done.push(selAfter) | |
97 hist.generation = ++hist.maxGeneration | |
98 hist.lastModTime = hist.lastSelTime = time | |
99 hist.lastOp = hist.lastSelOp = opId | |
100 hist.lastOrigin = hist.lastSelOrigin = change.origin | |
101 | |
102 if (!last) signal(doc, "historyAdded") | |
103 } | |
104 | |
105 function selectionEventCanBeMerged(doc, origin, prev, sel) { | |
106 let ch = origin.charAt(0) | |
107 return ch == "*" || | |
108 ch == "+" && | |
109 prev.ranges.length == sel.ranges.length && | |
110 prev.somethingSelected() == sel.somethingSelected() && | |
111 new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500) | |
112 } | |
113 | |
114 // Called whenever the selection changes, sets the new selection as | |
115 // the pending selection in the history, and pushes the old pending | |
116 // selection into the 'done' array when it was significantly | |
117 // different (in number of selected ranges, emptiness, or time). | |
118 export function addSelectionToHistory(doc, sel, opId, options) { | |
119 let hist = doc.history, origin = options && options.origin | |
120 | |
121 // A new event is started when the previous origin does not match | |
122 // the current, or the origins don't allow matching. Origins | |
123 // starting with * are always merged, those starting with + are | |
124 // merged when similar and close together in time. | |
125 if (opId == hist.lastSelOp || | |
126 (origin && hist.lastSelOrigin == origin && | |
127 (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin || | |
128 selectionEventCanBeMerged(doc, origin, lst(hist.done), sel)))) | |
129 hist.done[hist.done.length - 1] = sel | |
130 else | |
131 pushSelectionToHistory(sel, hist.done) | |
132 | |
133 hist.lastSelTime = +new Date | |
134 hist.lastSelOrigin = origin | |
135 hist.lastSelOp = opId | |
136 if (options && options.clearRedo !== false) | |
137 clearSelectionEvents(hist.undone) | |
138 } | |
139 | |
140 export function pushSelectionToHistory(sel, dest) { | |
141 let top = lst(dest) | |
142 if (!(top && top.ranges && top.equals(sel))) | |
143 dest.push(sel) | |
144 } | |
145 | |
146 // Used to store marked span information in the history. | |
147 function attachLocalSpans(doc, change, from, to) { | |
148 let existing = change["spans_" + doc.id], n = 0 | |
149 doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), line => { | |
150 if (line.markedSpans) | |
151 (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans | |
152 ++n | |
153 }) | |
154 } | |
155 | |
156 // When un/re-doing restores text containing marked spans, those | |
157 // that have been explicitly cleared should not be restored. | |
158 function removeClearedSpans(spans) { | |
159 if (!spans) return null | |
160 let out | |
161 for (let i = 0; i < spans.length; ++i) { | |
162 if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i) } | |
163 else if (out) out.push(spans[i]) | |
164 } | |
165 return !out ? spans : out.length ? out : null | |
166 } | |
167 | |
168 // Retrieve and filter the old marked spans stored in a change event. | |
169 function getOldSpans(doc, change) { | |
170 let found = change["spans_" + doc.id] | |
171 if (!found) return null | |
172 let nw = [] | |
173 for (let i = 0; i < change.text.length; ++i) | |
174 nw.push(removeClearedSpans(found[i])) | |
175 return nw | |
176 } | |
177 | |
178 // Used for un/re-doing changes from the history. Combines the | |
179 // result of computing the existing spans with the set of spans that | |
180 // existed in the history (so that deleting around a span and then | |
181 // undoing brings back the span). | |
182 export function mergeOldSpans(doc, change) { | |
183 let old = getOldSpans(doc, change) | |
184 let stretched = stretchSpansOverChange(doc, change) | |
185 if (!old) return stretched | |
186 if (!stretched) return old | |
187 | |
188 for (let i = 0; i < old.length; ++i) { | |
189 let oldCur = old[i], stretchCur = stretched[i] | |
190 if (oldCur && stretchCur) { | |
191 spans: for (let j = 0; j < stretchCur.length; ++j) { | |
192 let span = stretchCur[j] | |
193 for (let k = 0; k < oldCur.length; ++k) | |
194 if (oldCur[k].marker == span.marker) continue spans | |
195 oldCur.push(span) | |
196 } | |
197 } else if (stretchCur) { | |
198 old[i] = stretchCur | |
199 } | |
200 } | |
201 return old | |
202 } | |
203 | |
204 // Used both to provide a JSON-safe object in .getHistory, and, when | |
205 // detaching a document, to split the history in two | |
206 export function copyHistoryArray(events, newGroup, instantiateSel) { | |
207 let copy = [] | |
208 for (let i = 0; i < events.length; ++i) { | |
209 let event = events[i] | |
210 if (event.ranges) { | |
211 copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event) | |
212 continue | |
213 } | |
214 let changes = event.changes, newChanges = [] | |
215 copy.push({changes: newChanges}) | |
216 for (let j = 0; j < changes.length; ++j) { | |
217 let change = changes[j], m | |
218 newChanges.push({from: change.from, to: change.to, text: change.text}) | |
219 if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) { | |
220 if (indexOf(newGroup, Number(m[1])) > -1) { | |
221 lst(newChanges)[prop] = change[prop] | |
222 delete change[prop] | |
223 } | |
224 } | |
225 } | |
226 } | |
227 return copy | |
228 } |