| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2011 Dominik Haumann <dhaumann@kde.org> |
| 3 | SPDX-FileCopyrightText: 2009-2010 Bernhard Beschow <bbeschow@cs.tu-berlin.de> |
| 4 | SPDX-FileCopyrightText: 2002 John Firebaugh <jfirebaugh@kde.org> |
| 5 | SPDX-FileCopyrightText: 2001 Christoph Cullmann <cullmann@kde.org> |
| 6 | SPDX-FileCopyrightText: 2001 Joseph Wenninger <jowenn@kde.org> |
| 7 | SPDX-FileCopyrightText: 2023 Waqar Ahmed <waqar.17a@gmail.com> |
| 8 | |
| 9 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 10 | */ |
| 11 | |
| 12 | #include "kateundo.h" |
| 13 | |
| 14 | #include "katebuffer.h" |
| 15 | #include "katedocument.h" |
| 16 | #include "kateundomanager.h" |
| 17 | #include "kateview.h" |
| 18 | |
| 19 | #include <ktexteditor/cursor.h> |
| 20 | #include <ktexteditor/view.h> |
| 21 | |
| 22 | KateUndoGroup::KateUndoGroup(const KTextEditor::Cursor cursorPosition, |
| 23 | KTextEditor::Range selection, |
| 24 | const QList<KTextEditor::ViewPrivate::PlainSecondaryCursor> &secondary) |
| 25 | : m_undoSelection(selection) |
| 26 | , m_redoSelection(-1, -1, -1, -1) |
| 27 | , m_undoCursor(cursorPosition) |
| 28 | , m_undoSecondaryCursors(secondary) |
| 29 | , m_redoCursor(-1, -1) |
| 30 | { |
| 31 | } |
| 32 | |
| 33 | void KateUndoGroup::undo(KateUndoManager *manager, KTextEditor::ViewPrivate *view) |
| 34 | { |
| 35 | if (m_items.empty()) { |
| 36 | return; |
| 37 | } |
| 38 | |
| 39 | manager->startUndo(); |
| 40 | |
| 41 | auto doc = manager->document(); |
| 42 | auto updateDocLine = [doc](const UndoItem &item) { |
| 43 | Kate::TextLine tl = doc->plainKateTextLine(i: item.line); |
| 44 | tl.markAsModified(modified: item.lineModFlags.testFlag(flag: UndoItem::UndoLine1Modified)); |
| 45 | tl.markAsSavedOnDisk(savedOnDisk: item.lineModFlags.testFlag(flag: UndoItem::UndoLine1Saved)); |
| 46 | doc->buffer().setLineMetaData(line: item.line, textLine: tl); |
| 47 | }; |
| 48 | |
| 49 | for (auto rit = m_items.rbegin(); rit != m_items.rend(); ++rit) { |
| 50 | auto &item = *rit; |
| 51 | switch (item.type) { |
| 52 | case UndoItem::editInsertText: |
| 53 | doc->editRemoveText(line: item.line, col: item.col, len: item.text.size()); |
| 54 | updateDocLine(item); |
| 55 | break; |
| 56 | case UndoItem::editRemoveText: |
| 57 | doc->editInsertText(line: item.line, col: item.col, s: item.text); |
| 58 | updateDocLine(item); |
| 59 | break; |
| 60 | case UndoItem::editWrapLine: |
| 61 | doc->editUnWrapLine(line: item.line, removeLine: item.newLine, length: item.len); |
| 62 | updateDocLine(item); |
| 63 | break; |
| 64 | case UndoItem::editUnWrapLine: { |
| 65 | doc->editWrapLine(line: item.line, col: item.col, newLine: item.removeLine); |
| 66 | updateDocLine(item); |
| 67 | |
| 68 | auto next = doc->plainKateTextLine(i: item.line + 1); |
| 69 | next.markAsModified(modified: item.lineModFlags.testFlag(flag: UndoItem::UndoLine2Modified)); |
| 70 | next.markAsSavedOnDisk(savedOnDisk: item.lineModFlags.testFlag(flag: UndoItem::UndoLine2Saved)); |
| 71 | doc->buffer().setLineMetaData(line: item.line + 1, textLine: next); |
| 72 | } break; |
| 73 | case UndoItem::editInsertLine: |
| 74 | doc->editRemoveLine(line: item.line); |
| 75 | break; |
| 76 | case UndoItem::editRemoveLine: |
| 77 | doc->editInsertLine(line: item.line, s: item.text); |
| 78 | updateDocLine(item); |
| 79 | break; |
| 80 | case UndoItem::editMarkLineAutoWrapped: |
| 81 | doc->editMarkLineAutoWrapped(line: item.line, autowrapped: item.autowrapped); |
| 82 | break; |
| 83 | case UndoItem::editInvalid: |
| 84 | break; |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | if (view != nullptr) { |
| 89 | if (m_undoSelection.isValid()) { |
| 90 | view->setSelection(m_undoSelection); |
| 91 | } else { |
| 92 | view->removeSelection(); |
| 93 | } |
| 94 | view->clearSecondaryCursors(); |
| 95 | view->addSecondaryCursorsWithSelection(cursorsWithSelection: m_undoSecondaryCursors); |
| 96 | |
| 97 | if (m_undoCursor.isValid()) { |
| 98 | view->setCursorPosition(m_undoCursor); |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | manager->endUndo(); |
| 103 | } |
| 104 | |
| 105 | void KateUndoGroup::redo(KateUndoManager *manager, KTextEditor::ViewPrivate *view) |
| 106 | { |
| 107 | if (m_items.empty()) { |
| 108 | return; |
| 109 | } |
| 110 | |
| 111 | manager->startUndo(); |
| 112 | |
| 113 | auto doc = manager->document(); |
| 114 | auto updateDocLine = [doc](const UndoItem &item) { |
| 115 | Kate::TextLine tl = doc->plainKateTextLine(i: item.line); |
| 116 | tl.markAsModified(modified: item.lineModFlags.testFlag(flag: UndoItem::RedoLine1Modified)); |
| 117 | tl.markAsSavedOnDisk(savedOnDisk: item.lineModFlags.testFlag(flag: UndoItem::RedoLine1Saved)); |
| 118 | doc->buffer().setLineMetaData(line: item.line, textLine: tl); |
| 119 | }; |
| 120 | |
| 121 | for (auto &item : m_items) { |
| 122 | switch (item.type) { |
| 123 | case UndoItem::editInsertText: |
| 124 | doc->editInsertText(line: item.line, col: item.col, s: item.text); |
| 125 | updateDocLine(item); |
| 126 | break; |
| 127 | case UndoItem::editRemoveText: |
| 128 | doc->editRemoveText(line: item.line, col: item.col, len: item.text.size()); |
| 129 | updateDocLine(item); |
| 130 | break; |
| 131 | case UndoItem::editWrapLine: { |
| 132 | doc->editWrapLine(line: item.line, col: item.col, newLine: item.newLine); |
| 133 | updateDocLine(item); |
| 134 | |
| 135 | Kate::TextLine next = doc->plainKateTextLine(i: item.line + 1); |
| 136 | next.markAsModified(modified: item.lineModFlags.testFlag(flag: UndoItem::RedoLine2Modified)); |
| 137 | next.markAsSavedOnDisk(savedOnDisk: item.lineModFlags.testFlag(flag: UndoItem::RedoLine2Saved)); |
| 138 | doc->buffer().setLineMetaData(line: item.line + 1, textLine: next); |
| 139 | } break; |
| 140 | case UndoItem::editUnWrapLine: |
| 141 | doc->editUnWrapLine(line: item.line, removeLine: item.removeLine, length: item.len); |
| 142 | updateDocLine(item); |
| 143 | break; |
| 144 | case UndoItem::editInsertLine: |
| 145 | doc->editInsertLine(line: item.line, s: item.text); |
| 146 | updateDocLine(item); |
| 147 | break; |
| 148 | case UndoItem::editRemoveLine: |
| 149 | doc->editRemoveLine(line: item.line); |
| 150 | break; |
| 151 | case UndoItem::editMarkLineAutoWrapped: |
| 152 | doc->editMarkLineAutoWrapped(line: item.line, autowrapped: item.autowrapped); |
| 153 | break; |
| 154 | case UndoItem::editInvalid: |
| 155 | break; |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | if (view != nullptr) { |
| 160 | if (m_redoSelection.isValid()) { |
| 161 | view->setSelection(m_redoSelection); |
| 162 | } else { |
| 163 | view->removeSelection(); |
| 164 | } |
| 165 | view->clearSecondaryCursors(); |
| 166 | view->addSecondaryCursorsWithSelection(cursorsWithSelection: m_redoSecondaryCursors); |
| 167 | |
| 168 | if (m_redoCursor.isValid()) { |
| 169 | view->setCursorPosition(m_redoCursor); |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | manager->endUndo(); |
| 174 | } |
| 175 | |
| 176 | void KateUndoGroup::editEnd(const KTextEditor::Cursor cursorPosition, |
| 177 | KTextEditor::Range selectionRange, |
| 178 | const QList<KTextEditor::ViewPrivate::PlainSecondaryCursor> &secondaryCursors) |
| 179 | { |
| 180 | m_redoCursor = cursorPosition; |
| 181 | m_redoSecondaryCursors = secondaryCursors; |
| 182 | m_redoSelection = selectionRange; |
| 183 | } |
| 184 | |
| 185 | static bool mergeUndoItems(UndoItem &base, const UndoItem &u) |
| 186 | { |
| 187 | if (base.type == UndoItem::editInsertText && u.type == UndoItem::editWrapLine) { |
| 188 | // merge insert text full line + wrap line |
| 189 | if (base.col == 0 && base.line == u.line && base.col + base.text.length() == u.col && u.newLine) { |
| 190 | base.type = UndoItem::editInsertLine; |
| 191 | base.lineModFlags.setFlag(flag: UndoItem::RedoLine1Modified); |
| 192 | return true; |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | if (base.type == UndoItem::editRemoveText && base.type == u.type) { |
| 197 | if (base.line == u.line && base.col == (u.col + u.text.size())) { |
| 198 | base.text.prepend(s: u.text); |
| 199 | base.col = u.col; |
| 200 | return true; |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | if (base.type == UndoItem::editInsertText && base.type == u.type) { |
| 205 | if (base.line == u.line && (base.col + base.text.size()) == u.col) { |
| 206 | base.text += u.text; |
| 207 | return true; |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | return false; |
| 212 | } |
| 213 | |
| 214 | void KateUndoGroup::addItem(UndoItem u) |
| 215 | { |
| 216 | // try to merge, do that only for equal types, inside mergeWith we do hard casts |
| 217 | if (!m_items.empty() && mergeUndoItems(base&: m_items.back(), u)) { |
| 218 | return; |
| 219 | } |
| 220 | |
| 221 | // default: just add new item unchanged |
| 222 | m_items.push_back(x: std::move(u)); |
| 223 | } |
| 224 | |
| 225 | bool KateUndoGroup::merge(KateUndoGroup *newGroup, bool complex) |
| 226 | { |
| 227 | if (m_safePoint) { |
| 228 | return false; |
| 229 | } |
| 230 | |
| 231 | if (newGroup->isOnlyType(type: singleType()) || complex) { |
| 232 | // Take all of its items first -> last |
| 233 | for (auto &item : newGroup->m_items) { |
| 234 | addItem(u: item); |
| 235 | } |
| 236 | newGroup->m_items.clear(); |
| 237 | |
| 238 | if (newGroup->m_safePoint) { |
| 239 | safePoint(); |
| 240 | } |
| 241 | |
| 242 | m_redoCursor = newGroup->m_redoCursor; |
| 243 | m_redoSecondaryCursors = newGroup->m_redoSecondaryCursors; |
| 244 | m_redoSelection = newGroup->m_redoSelection; |
| 245 | // m_redoSelections = newGroup->m_redoSelections; |
| 246 | |
| 247 | return true; |
| 248 | } |
| 249 | |
| 250 | return false; |
| 251 | } |
| 252 | |
| 253 | void KateUndoGroup::safePoint(bool safePoint) |
| 254 | { |
| 255 | m_safePoint = safePoint; |
| 256 | } |
| 257 | |
| 258 | void KateUndoGroup::flagSavedAsModified() |
| 259 | { |
| 260 | for (UndoItem &item : m_items) { |
| 261 | if (item.lineModFlags.testFlag(flag: UndoItem::UndoLine1Saved)) { |
| 262 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine1Saved, on: false); |
| 263 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine1Modified, on: true); |
| 264 | } |
| 265 | |
| 266 | if (item.lineModFlags.testFlag(flag: UndoItem::UndoLine2Saved)) { |
| 267 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine2Saved, on: false); |
| 268 | item.lineModFlags.setFlag(flag: UndoItem::UndoLine2Modified, on: true); |
| 269 | } |
| 270 | |
| 271 | if (item.lineModFlags.testFlag(flag: UndoItem::RedoLine1Saved)) { |
| 272 | item.lineModFlags.setFlag(flag: UndoItem::RedoLine1Saved, on: false); |
| 273 | item.lineModFlags.setFlag(flag: UndoItem::RedoLine1Modified, on: true); |
| 274 | } |
| 275 | |
| 276 | if (item.lineModFlags.testFlag(flag: UndoItem::RedoLine2Saved)) { |
| 277 | item.lineModFlags.setFlag(flag: UndoItem::RedoLine2Saved, on: false); |
| 278 | item.lineModFlags.setFlag(flag: UndoItem::RedoLine2Modified, on: true); |
| 279 | } |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | static void updateUndoSavedOnDiskFlag(UndoItem &item, QBitArray &lines) |
| 284 | { |
| 285 | const int line = item.line; |
| 286 | if (line >= lines.size()) { |
| 287 | lines.resize(size: line + 1); |
| 288 | } |
| 289 | |
| 290 | const bool wasBitSet = lines.testBit(i: line); |
| 291 | if (!wasBitSet) { |
| 292 | lines.setBit(line); |
| 293 | } |
| 294 | |
| 295 | auto &lineFlags = item.lineModFlags; |
| 296 | |
| 297 | switch (item.type) { |
| 298 | case UndoItem::editInsertText: |
| 299 | case UndoItem::editRemoveText: |
| 300 | case UndoItem::editRemoveLine: |
| 301 | if (!wasBitSet) { |
| 302 | lineFlags.setFlag(flag: UndoItem::UndoLine1Modified, on: false); |
| 303 | lineFlags.setFlag(flag: UndoItem::UndoLine1Saved, on: true); |
| 304 | } |
| 305 | break; |
| 306 | case UndoItem::editWrapLine: |
| 307 | if (lineFlags.testFlag(flag: UndoItem::UndoLine1Modified) && !wasBitSet) { |
| 308 | lineFlags.setFlag(flag: UndoItem::UndoLine1Modified, on: false); |
| 309 | lineFlags.setFlag(flag: UndoItem::UndoLine1Saved, on: true); |
| 310 | } |
| 311 | break; |
| 312 | case UndoItem::editUnWrapLine: |
| 313 | if (line + 1 >= lines.size()) { |
| 314 | lines.resize(size: line + 2); |
| 315 | } |
| 316 | if (lineFlags.testFlag(flag: UndoItem::UndoLine1Modified) && !wasBitSet) { |
| 317 | lineFlags.setFlag(flag: UndoItem::UndoLine1Modified, on: false); |
| 318 | lineFlags.setFlag(flag: UndoItem::UndoLine1Saved, on: true); |
| 319 | } |
| 320 | |
| 321 | if (lineFlags.testFlag(flag: UndoItem::UndoLine2Modified) && !lines.testBit(i: line + 1)) { |
| 322 | lines.setBit(line + 1); |
| 323 | |
| 324 | lineFlags.setFlag(flag: UndoItem::UndoLine2Modified, on: false); |
| 325 | lineFlags.setFlag(flag: UndoItem::UndoLine2Saved, on: true); |
| 326 | } |
| 327 | break; |
| 328 | case UndoItem::editInsertLine: |
| 329 | case UndoItem::editMarkLineAutoWrapped: |
| 330 | case UndoItem::editInvalid: |
| 331 | break; |
| 332 | } |
| 333 | } |
| 334 | |
| 335 | void KateUndoGroup::markUndoAsSaved(QBitArray &lines) |
| 336 | { |
| 337 | for (auto rit = m_items.rbegin(); rit != m_items.rend(); ++rit) { |
| 338 | updateUndoSavedOnDiskFlag(item&: *rit, lines); |
| 339 | } |
| 340 | } |
| 341 | |
| 342 | static void updateRedoSavedOnDiskFlag(UndoItem &item, QBitArray &lines) |
| 343 | { |
| 344 | const int line = item.line; |
| 345 | if (line >= lines.size()) { |
| 346 | lines.resize(size: line + 1); |
| 347 | } |
| 348 | |
| 349 | const bool wasBitSet = lines.testBit(i: line); |
| 350 | if (!wasBitSet) { |
| 351 | lines.setBit(line); |
| 352 | } |
| 353 | auto &lineFlags = item.lineModFlags; |
| 354 | |
| 355 | switch (item.type) { |
| 356 | case UndoItem::editInsertText: |
| 357 | case UndoItem::editRemoveText: |
| 358 | case UndoItem::editInsertLine: |
| 359 | lineFlags.setFlag(flag: UndoItem::RedoLine1Modified, on: false); |
| 360 | lineFlags.setFlag(flag: UndoItem::RedoLine1Saved, on: true); |
| 361 | break; |
| 362 | case UndoItem::editUnWrapLine: |
| 363 | if (lineFlags.testFlag(flag: UndoItem::RedoLine1Modified) && !wasBitSet) { |
| 364 | lineFlags.setFlag(flag: UndoItem::RedoLine1Modified, on: false); |
| 365 | lineFlags.setFlag(flag: UndoItem::RedoLine1Saved, on: true); |
| 366 | } |
| 367 | break; |
| 368 | case UndoItem::editWrapLine: |
| 369 | if (line + 1 >= lines.size()) { |
| 370 | lines.resize(size: line + 2); |
| 371 | } |
| 372 | |
| 373 | if (lineFlags.testFlag(flag: UndoItem::RedoLine1Modified) && !wasBitSet) { |
| 374 | lineFlags.setFlag(flag: UndoItem::RedoLine1Modified, on: false); |
| 375 | lineFlags.setFlag(flag: UndoItem::RedoLine1Saved, on: true); |
| 376 | } |
| 377 | |
| 378 | if (lineFlags.testFlag(flag: UndoItem::RedoLine2Modified) && !lines.testBit(i: line + 1)) { |
| 379 | lineFlags.setFlag(flag: UndoItem::RedoLine2Modified, on: false); |
| 380 | lineFlags.setFlag(flag: UndoItem::RedoLine2Saved, on: true); |
| 381 | } |
| 382 | break; |
| 383 | case UndoItem::editRemoveLine: |
| 384 | case UndoItem::editMarkLineAutoWrapped: |
| 385 | case UndoItem::editInvalid: |
| 386 | break; |
| 387 | } |
| 388 | } |
| 389 | |
| 390 | void KateUndoGroup::markRedoAsSaved(QBitArray &lines) |
| 391 | { |
| 392 | for (auto rit = m_items.rbegin(); rit != m_items.rend(); ++rit) { |
| 393 | updateRedoSavedOnDiskFlag(item&: *rit, lines); |
| 394 | } |
| 395 | } |
| 396 | |
| 397 | UndoItem::UndoType KateUndoGroup::singleType() const |
| 398 | { |
| 399 | UndoItem::UndoType ret = UndoItem::editInvalid; |
| 400 | |
| 401 | for (const auto &item : m_items) { |
| 402 | if (ret == UndoItem::editInvalid) { |
| 403 | ret = item.type; |
| 404 | } else if (ret != item.type) { |
| 405 | return UndoItem::editInvalid; |
| 406 | } |
| 407 | } |
| 408 | |
| 409 | return ret; |
| 410 | } |
| 411 | |
| 412 | bool KateUndoGroup::isOnlyType(UndoItem::UndoType type) const |
| 413 | { |
| 414 | if (type == UndoItem::editInvalid) { |
| 415 | return false; |
| 416 | } |
| 417 | for (const auto &item : m_items) { |
| 418 | if (item.type != type) |
| 419 | return false; |
| 420 | } |
| 421 | return true; |
| 422 | } |
| 423 | |