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