1 | /* |
2 | SPDX-FileCopyrightText: 2013 Christoph Cullmann <cullmann@kde.org> |
3 | |
4 | SPDX-License-Identifier: LGPL-2.0-or-later |
5 | */ |
6 | |
7 | #include "katetexthistory.h" |
8 | #include "katetextbuffer.h" |
9 | |
10 | namespace Kate |
11 | { |
12 | TextHistory::TextHistory(TextBuffer &buffer) |
13 | : m_buffer(buffer) |
14 | , m_lastSavedRevision(-1) |
15 | , m_firstHistoryEntryRevision(0) |
16 | { |
17 | // just call clear to init |
18 | clear(); |
19 | } |
20 | |
21 | TextHistory::~TextHistory() = default; |
22 | |
23 | qint64 TextHistory::revision() const |
24 | { |
25 | // just output last revisions of buffer |
26 | return m_buffer.revision(); |
27 | } |
28 | |
29 | void TextHistory::clear() |
30 | { |
31 | // reset last saved revision |
32 | m_lastSavedRevision = -1; |
33 | |
34 | // remove all history entries and add no-change dummy for first revision |
35 | m_historyEntries.clear(); |
36 | m_historyEntries.push_back(x: Entry()); |
37 | |
38 | // first entry will again belong to first revision |
39 | m_firstHistoryEntryRevision = 0; |
40 | } |
41 | |
42 | void TextHistory::setLastSavedRevision() |
43 | { |
44 | // current revision was successful saved |
45 | m_lastSavedRevision = revision(); |
46 | } |
47 | |
48 | void TextHistory::wrapLine(const KTextEditor::Cursor position) |
49 | { |
50 | // create and add new entry |
51 | Entry entry; |
52 | entry.type = Entry::WrapLine; |
53 | entry.line = position.line(); |
54 | entry.column = position.column(); |
55 | addEntry(entry); |
56 | } |
57 | |
58 | void TextHistory::unwrapLine(int line, int oldLineLength) |
59 | { |
60 | // create and add new entry |
61 | Entry entry; |
62 | entry.type = Entry::UnwrapLine; |
63 | entry.line = line; |
64 | entry.column = 0; |
65 | entry.oldLineLength = oldLineLength; |
66 | addEntry(entry); |
67 | } |
68 | |
69 | void TextHistory::insertText(const KTextEditor::Cursor position, int length, int oldLineLength) |
70 | { |
71 | // create and add new entry |
72 | Entry entry; |
73 | entry.type = Entry::InsertText; |
74 | entry.line = position.line(); |
75 | entry.column = position.column(); |
76 | entry.length = length; |
77 | entry.oldLineLength = oldLineLength; |
78 | addEntry(entry); |
79 | } |
80 | |
81 | void TextHistory::removeText(KTextEditor::Range range, int oldLineLength) |
82 | { |
83 | // create and add new entry |
84 | Entry entry; |
85 | entry.type = Entry::RemoveText; |
86 | entry.line = range.start().line(); |
87 | entry.column = range.start().column(); |
88 | entry.length = range.end().column() - range.start().column(); |
89 | entry.oldLineLength = oldLineLength; |
90 | addEntry(entry); |
91 | } |
92 | |
93 | void TextHistory::addEntry(const Entry &entry) |
94 | { |
95 | // history should never be empty |
96 | Q_ASSERT(!m_historyEntries.empty()); |
97 | |
98 | // simple efficient check: if we only have one entry, and the entry is not referenced |
99 | // just replace it with the new one and adjust the revision |
100 | if ((m_historyEntries.size() == 1) && !m_historyEntries.front().referenceCounter) { |
101 | // remember new revision for first element, it is the revision we get after this change |
102 | m_firstHistoryEntryRevision = revision() + 1; |
103 | |
104 | // remember edit |
105 | m_historyEntries.front() = entry; |
106 | |
107 | // be done... |
108 | return; |
109 | } |
110 | |
111 | // ok, we have more than one entry or the entry is referenced, just add up new entries |
112 | m_historyEntries.push_back(x: entry); |
113 | } |
114 | |
115 | void TextHistory::lockRevision(qint64 revision) |
116 | { |
117 | // some invariants must hold |
118 | Q_ASSERT(!m_historyEntries.empty()); |
119 | Q_ASSERT(revision >= m_firstHistoryEntryRevision); |
120 | Q_ASSERT(revision < (m_firstHistoryEntryRevision + qint64(m_historyEntries.size()))); |
121 | |
122 | // increment revision reference counter |
123 | Entry &entry = m_historyEntries[revision - m_firstHistoryEntryRevision]; |
124 | ++entry.referenceCounter; |
125 | } |
126 | |
127 | void TextHistory::unlockRevision(qint64 revision) |
128 | { |
129 | // some invariants must hold |
130 | Q_ASSERT(!m_historyEntries.empty()); |
131 | Q_ASSERT(revision >= m_firstHistoryEntryRevision); |
132 | Q_ASSERT(revision < (m_firstHistoryEntryRevision + qint64(m_historyEntries.size()))); |
133 | |
134 | // decrement revision reference counter |
135 | Entry &entry = m_historyEntries[revision - m_firstHistoryEntryRevision]; |
136 | Q_ASSERT(entry.referenceCounter); |
137 | --entry.referenceCounter; |
138 | |
139 | // clean up no longer used revisions... |
140 | if (!entry.referenceCounter) { |
141 | // search for now unused stuff |
142 | qint64 unreferencedEdits = 0; |
143 | for (qint64 i = 0; i + 1 < qint64(m_historyEntries.size()); ++i) { |
144 | if (m_historyEntries[i].referenceCounter) { |
145 | break; |
146 | } |
147 | |
148 | // remember deleted count |
149 | ++unreferencedEdits; |
150 | } |
151 | |
152 | // remove unreferred from the list now |
153 | if (unreferencedEdits > 0) { |
154 | // remove stuff from history |
155 | m_historyEntries.erase(first: m_historyEntries.begin(), last: m_historyEntries.begin() + unreferencedEdits); |
156 | |
157 | // patch first entry revision |
158 | m_firstHistoryEntryRevision += unreferencedEdits; |
159 | } |
160 | } |
161 | } |
162 | |
163 | void TextHistory::Entry::transformCursor(int &cursorLine, int &cursorColumn, bool moveOnInsert) const |
164 | { |
165 | // simple stuff, sort out generic things |
166 | |
167 | // no change, if this change is in line behind cursor |
168 | if (line > cursorLine) { |
169 | return; |
170 | } |
171 | |
172 | // handle all history types |
173 | switch (type) { |
174 | // Wrap a line |
175 | case WrapLine: |
176 | // we wrap this line |
177 | if (cursorLine == line) { |
178 | // skip cursors with too small column |
179 | if (cursorColumn <= column) { |
180 | if (cursorColumn < column || !moveOnInsert) { |
181 | return; |
182 | } |
183 | } |
184 | |
185 | // adjust column |
186 | cursorColumn = cursorColumn - column; |
187 | } |
188 | |
189 | // always increment cursor line |
190 | cursorLine += 1; |
191 | return; |
192 | |
193 | // Unwrap a line |
194 | case UnwrapLine: |
195 | // we unwrap this line, adjust column |
196 | if (cursorLine == line) { |
197 | cursorColumn += oldLineLength; |
198 | } |
199 | |
200 | // decrease cursor line |
201 | cursorLine -= 1; |
202 | return; |
203 | |
204 | // Insert text |
205 | case InsertText: |
206 | // only interesting, if same line |
207 | if (cursorLine != line) { |
208 | return; |
209 | } |
210 | |
211 | // skip cursors with too small column |
212 | if (cursorColumn <= column) { |
213 | if (cursorColumn < column || !moveOnInsert) { |
214 | return; |
215 | } |
216 | } |
217 | |
218 | // patch column of cursor |
219 | if (cursorColumn <= oldLineLength) { |
220 | cursorColumn += length; |
221 | } |
222 | |
223 | // special handling if cursor behind the real line, e.g. non-wrapping cursor in block selection mode |
224 | else if (cursorColumn < oldLineLength + length) { |
225 | cursorColumn = oldLineLength + length; |
226 | } |
227 | |
228 | return; |
229 | |
230 | // Remove text |
231 | case RemoveText: |
232 | // only interesting, if same line |
233 | if (cursorLine != line) { |
234 | return; |
235 | } |
236 | |
237 | // skip cursors with too small column |
238 | if (cursorColumn <= column) { |
239 | return; |
240 | } |
241 | |
242 | // patch column of cursor |
243 | if (cursorColumn <= column + length) { |
244 | cursorColumn = column; |
245 | } else { |
246 | cursorColumn -= length; |
247 | } |
248 | |
249 | return; |
250 | |
251 | // nothing |
252 | default: |
253 | return; |
254 | } |
255 | } |
256 | |
257 | void TextHistory::Entry::reverseTransformCursor(int &cursorLine, int &cursorColumn, bool moveOnInsert) const |
258 | { |
259 | // handle all history types |
260 | switch (type) { |
261 | // Wrap a line |
262 | case WrapLine: |
263 | // ignore this line |
264 | if (cursorLine <= line) { |
265 | return; |
266 | } |
267 | |
268 | // next line is unwrapped |
269 | if (cursorLine == line + 1) { |
270 | // adjust column |
271 | cursorColumn = cursorColumn + column; |
272 | } |
273 | |
274 | // always decrement cursor line |
275 | cursorLine -= 1; |
276 | return; |
277 | |
278 | // Unwrap a line |
279 | case UnwrapLine: |
280 | // ignore lines before unwrapped one |
281 | if (cursorLine < line - 1) { |
282 | return; |
283 | } |
284 | |
285 | // we unwrap this line, try to adjust cursor column if needed |
286 | if (cursorLine == line - 1) { |
287 | // skip cursors with to small columns |
288 | if (cursorColumn <= oldLineLength) { |
289 | if (cursorColumn < oldLineLength || !moveOnInsert) { |
290 | return; |
291 | } |
292 | } |
293 | |
294 | cursorColumn -= oldLineLength; |
295 | } |
296 | |
297 | // increase cursor line |
298 | cursorLine += 1; |
299 | return; |
300 | |
301 | // Insert text |
302 | case InsertText: |
303 | // only interesting, if same line |
304 | if (cursorLine != line) { |
305 | return; |
306 | } |
307 | |
308 | // skip cursors with too small column |
309 | if (cursorColumn <= column) { |
310 | return; |
311 | } |
312 | |
313 | // patch column of cursor |
314 | if (cursorColumn - length < column) { |
315 | cursorColumn = column; |
316 | } else { |
317 | cursorColumn -= length; |
318 | } |
319 | |
320 | return; |
321 | |
322 | // Remove text |
323 | case RemoveText: |
324 | // only interesting, if same line |
325 | if (cursorLine != line) { |
326 | return; |
327 | } |
328 | |
329 | // skip cursors with too small column |
330 | if (cursorColumn <= column) { |
331 | if (cursorColumn < column || !moveOnInsert) { |
332 | return; |
333 | } |
334 | } |
335 | |
336 | // patch column of cursor |
337 | if (cursorColumn <= oldLineLength) { |
338 | cursorColumn += length; |
339 | } |
340 | |
341 | // special handling if cursor behind the real line, e.g. non-wrapping cursor in block selection mode |
342 | else if (cursorColumn < oldLineLength + length) { |
343 | cursorColumn = oldLineLength + length; |
344 | } |
345 | return; |
346 | |
347 | // nothing |
348 | default: |
349 | return; |
350 | } |
351 | } |
352 | |
353 | void TextHistory::transformCursor(int &line, int &column, KTextEditor::MovingCursor::InsertBehavior insertBehavior, qint64 fromRevision, qint64 toRevision) |
354 | { |
355 | // -1 special meaning for from/toRevision |
356 | if (fromRevision == -1) { |
357 | fromRevision = revision(); |
358 | } |
359 | |
360 | if (toRevision == -1) { |
361 | toRevision = revision(); |
362 | } |
363 | |
364 | // shortcut, same revision |
365 | if (fromRevision == toRevision) { |
366 | return; |
367 | } |
368 | |
369 | // some invariants must hold |
370 | Q_ASSERT(!m_historyEntries.empty()); |
371 | Q_ASSERT(fromRevision != toRevision); |
372 | Q_ASSERT(fromRevision >= m_firstHistoryEntryRevision); |
373 | Q_ASSERT(fromRevision < (m_firstHistoryEntryRevision + qint64(m_historyEntries.size()))); |
374 | Q_ASSERT(toRevision >= m_firstHistoryEntryRevision); |
375 | Q_ASSERT(toRevision < (m_firstHistoryEntryRevision + qint64(m_historyEntries.size()))); |
376 | |
377 | // transform cursor |
378 | bool moveOnInsert = insertBehavior == KTextEditor::MovingCursor::MoveOnInsert; |
379 | |
380 | // forward or reverse transform? |
381 | if (toRevision > fromRevision) { |
382 | for (int rev = fromRevision - m_firstHistoryEntryRevision + 1; rev <= (toRevision - m_firstHistoryEntryRevision); ++rev) { |
383 | const Entry &entry = m_historyEntries.at(n: rev); |
384 | entry.transformCursor(cursorLine&: line, cursorColumn&: column, moveOnInsert); |
385 | } |
386 | } else { |
387 | for (int rev = fromRevision - m_firstHistoryEntryRevision; rev >= (toRevision - m_firstHistoryEntryRevision + 1); --rev) { |
388 | const Entry &entry = m_historyEntries.at(n: rev); |
389 | entry.reverseTransformCursor(cursorLine&: line, cursorColumn&: column, moveOnInsert); |
390 | } |
391 | } |
392 | } |
393 | |
394 | void TextHistory::transformRange(KTextEditor::Range &range, |
395 | KTextEditor::MovingRange::InsertBehaviors insertBehaviors, |
396 | KTextEditor::MovingRange::EmptyBehavior emptyBehavior, |
397 | qint64 fromRevision, |
398 | qint64 toRevision) |
399 | { |
400 | // invalidate on empty? |
401 | bool invalidateIfEmpty = emptyBehavior == KTextEditor::MovingRange::InvalidateIfEmpty; |
402 | if (invalidateIfEmpty && range.end() <= range.start()) { |
403 | range = KTextEditor::Range::invalid(); |
404 | return; |
405 | } |
406 | |
407 | // -1 special meaning for from/toRevision |
408 | if (fromRevision == -1) { |
409 | fromRevision = revision(); |
410 | } |
411 | |
412 | if (toRevision == -1) { |
413 | toRevision = revision(); |
414 | } |
415 | |
416 | // shortcut, same revision |
417 | if (fromRevision == toRevision) { |
418 | return; |
419 | } |
420 | |
421 | // some invariants must hold |
422 | Q_ASSERT(!m_historyEntries.empty()); |
423 | Q_ASSERT(fromRevision != toRevision); |
424 | Q_ASSERT(fromRevision >= m_firstHistoryEntryRevision); |
425 | Q_ASSERT(fromRevision < (m_firstHistoryEntryRevision + qint64(m_historyEntries.size()))); |
426 | Q_ASSERT(toRevision >= m_firstHistoryEntryRevision); |
427 | Q_ASSERT(toRevision < (m_firstHistoryEntryRevision + qint64(m_historyEntries.size()))); |
428 | |
429 | // transform cursors |
430 | |
431 | // first: copy cursors, without range association |
432 | int startLine = range.start().line(); |
433 | int startColumn = range.start().column(); |
434 | int endLine = range.end().line(); |
435 | int endColumn = range.end().column(); |
436 | |
437 | bool moveOnInsertStart = !(insertBehaviors & KTextEditor::MovingRange::ExpandLeft); |
438 | bool moveOnInsertEnd = (insertBehaviors & KTextEditor::MovingRange::ExpandRight); |
439 | |
440 | // forward or reverse transform? |
441 | if (toRevision > fromRevision) { |
442 | for (int rev = fromRevision - m_firstHistoryEntryRevision + 1; rev <= (toRevision - m_firstHistoryEntryRevision); ++rev) { |
443 | const Entry &entry = m_historyEntries.at(n: rev); |
444 | |
445 | entry.transformCursor(cursorLine&: startLine, cursorColumn&: startColumn, moveOnInsert: moveOnInsertStart); |
446 | |
447 | entry.transformCursor(cursorLine&: endLine, cursorColumn&: endColumn, moveOnInsert: moveOnInsertEnd); |
448 | |
449 | // got empty? |
450 | if (endLine < startLine || (endLine == startLine && endColumn <= startColumn)) { |
451 | if (invalidateIfEmpty) { |
452 | range = KTextEditor::Range::invalid(); |
453 | return; |
454 | } else { |
455 | // else normalize them |
456 | endLine = startLine; |
457 | endColumn = startColumn; |
458 | } |
459 | } |
460 | } |
461 | } else { |
462 | for (int rev = fromRevision - m_firstHistoryEntryRevision; rev >= (toRevision - m_firstHistoryEntryRevision + 1); --rev) { |
463 | const Entry &entry = m_historyEntries.at(n: rev); |
464 | |
465 | entry.reverseTransformCursor(cursorLine&: startLine, cursorColumn&: startColumn, moveOnInsert: moveOnInsertStart); |
466 | |
467 | entry.reverseTransformCursor(cursorLine&: endLine, cursorColumn&: endColumn, moveOnInsert: moveOnInsertEnd); |
468 | |
469 | // got empty? |
470 | if (endLine < startLine || (endLine == startLine && endColumn <= startColumn)) { |
471 | if (invalidateIfEmpty) { |
472 | range = KTextEditor::Range::invalid(); |
473 | return; |
474 | } else { |
475 | // else normalize them |
476 | endLine = startLine; |
477 | endColumn = startColumn; |
478 | } |
479 | } |
480 | } |
481 | } |
482 | |
483 | // now, copy cursors back |
484 | range.setRange(start: KTextEditor::Cursor(startLine, startColumn), end: KTextEditor::Cursor(endLine, endColumn)); |
485 | } |
486 | |
487 | } |
488 | |