KTextEditor

katetexthistory.cpp
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
10namespace Kate
11{
12TextHistory::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
20TextHistory::~TextHistory() = default;
21
23{
24 // just output last revisions of buffer
25 return m_buffer.revision();
26}
27
28void 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(Entry());
36
37 // first entry will again belong to the current revision
38 m_firstHistoryEntryRevision = revision();
39}
40
41void TextHistory::setLastSavedRevision()
42{
43 // current revision was successful saved
44 m_lastSavedRevision = revision();
45}
46
47void 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
57void 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
68void 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
80void 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
92void 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(entry);
112}
113
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
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(m_historyEntries.begin(), m_historyEntries.begin() + unreferencedEdits);
155
156 // patch first entry revision
157 m_firstHistoryEntryRevision += unreferencedEdits;
158 }
159 }
160}
161
162void 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
256void 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
352void 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(rev);
383 entry.transformCursor(line, 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(rev);
388 entry.reverseTransformCursor(line, column, moveOnInsert);
389 }
390 }
391}
392
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()) {
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(rev);
443
444 entry.transformCursor(startLine, startColumn, moveOnInsertStart);
445
446 entry.transformCursor(endLine, endColumn, moveOnInsertEnd);
447
448 // got empty?
449 if (endLine < startLine || (endLine == startLine && endColumn <= startColumn)) {
450 if (invalidateIfEmpty) {
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(rev);
463
464 entry.reverseTransformCursor(startLine, startColumn, moveOnInsertStart);
465
466 entry.reverseTransformCursor(endLine, endColumn, moveOnInsertEnd);
467
468 // got empty?
469 if (endLine < startLine || (endLine == startLine && endColumn <= startColumn)) {
470 if (invalidateIfEmpty) {
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(KTextEditor::Cursor(startLine, startColumn), KTextEditor::Cursor(endLine, endColumn));
484}
485
486}
The Cursor represents a position in a Document.
Definition cursor.h:75
constexpr int column() const noexcept
Retrieve the column on which this cursor is situated.
Definition cursor.h:192
constexpr int line() const noexcept
Retrieve the line on which this cursor is situated.
Definition cursor.h:174
InsertBehavior
Insert behavior of this cursor, should it stay if text is insert at its position or should it move.
@ MoveOnInsert
move on insert
QFlags< InsertBehavior > InsertBehaviors
Stores a combination of InsertBehavior values.
EmptyBehavior
Behavior of range if it becomes empty.
@ InvalidateIfEmpty
invalidate range, if it becomes empty
@ ExpandRight
Expand to encapsulate new characters to the right of the range.
@ ExpandLeft
Expand to encapsulate new characters to the left of the range.
An object representing a section of text, from one Cursor to another.
constexpr Cursor end() const noexcept
Get the end position of this range.
constexpr Cursor start() const noexcept
Get the start position of this range.
void setRange(Range range) noexcept
Set the start and end cursors to range.start() and range.end() respectively.
static constexpr Range invalid() noexcept
Returns an invalid range.
Class representing a text buffer.
void unlockRevision(qint64 revision)
Release a revision.
qint64 revision() const
Current revision, just relay the revision of the buffer.
void transformCursor(int &line, int &column, KTextEditor::MovingCursor::InsertBehavior insertBehavior, qint64 fromRevision, qint64 toRevision=-1)
Transform a cursor from one revision to an other.
void transformRange(KTextEditor::Range &range, KTextEditor::MovingRange::InsertBehaviors insertBehaviors, KTextEditor::MovingRange::EmptyBehavior emptyBehavior, qint64 fromRevision, qint64 toRevision=-1)
Transform a range from one revision to an other.
void lockRevision(qint64 revision)
Lock a revision, this will keep it around until released again.
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 28 2025 12:00:11 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.