| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2010 Christoph Cullmann <cullmann@kde.org> |
| 3 | |
| 4 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 5 | */ |
| 6 | #ifndef KATE_TEXTBUFFER_H |
| 7 | #define KATE_TEXTBUFFER_H |
| 8 | |
| 9 | #include <QList> |
| 10 | #include <QObject> |
| 11 | #include <QSet> |
| 12 | #include <QString> |
| 13 | |
| 14 | #include "katetextblock.h" |
| 15 | #include "katetexthistory.h" |
| 16 | #include <ktexteditor_export.h> |
| 17 | |
| 18 | // encoding prober |
| 19 | #include <KEncodingProber> |
| 20 | |
| 21 | namespace KTextEditor |
| 22 | { |
| 23 | class DocumentPrivate; |
| 24 | } |
| 25 | |
| 26 | class KCompressionDevice; |
| 27 | |
| 28 | namespace Kate |
| 29 | { |
| 30 | class ; |
| 31 | class TextCursor; |
| 32 | class TextBlock; |
| 33 | |
| 34 | constexpr int BufferBlockSize = 64; |
| 35 | |
| 36 | /** |
| 37 | * Class representing a text buffer. |
| 38 | * The interface is line based, internally the text will be stored in blocks of text lines. |
| 39 | */ |
| 40 | class KTEXTEDITOR_EXPORT TextBuffer : public QObject |
| 41 | { |
| 42 | friend class TextCursor; |
| 43 | friend class TextRange; |
| 44 | friend class TextBlock; |
| 45 | |
| 46 | Q_OBJECT |
| 47 | |
| 48 | public: |
| 49 | /** |
| 50 | * End of line mode |
| 51 | */ |
| 52 | enum EndOfLineMode { |
| 53 | eolUnknown = -1, |
| 54 | eolUnix = 0, |
| 55 | eolDos = 1, |
| 56 | eolMac = 2 |
| 57 | }; |
| 58 | |
| 59 | /** |
| 60 | * Construct an empty text buffer. |
| 61 | * Empty means one empty line in one block. |
| 62 | * @param parent parent qobject |
| 63 | */ |
| 64 | explicit TextBuffer(KTextEditor::DocumentPrivate *parent, bool alwaysUseKAuth = false); |
| 65 | |
| 66 | /** |
| 67 | * Destruct the text buffer |
| 68 | * Virtual, we allow inheritance |
| 69 | */ |
| 70 | ~TextBuffer() override; |
| 71 | |
| 72 | /** |
| 73 | * Clears the buffer, reverts to initial empty state. |
| 74 | * Empty means one empty line in one block. |
| 75 | * Virtual, can be overwritten. |
| 76 | */ |
| 77 | virtual void clear(); |
| 78 | |
| 79 | /** |
| 80 | * Set encoding prober type for this buffer to use for load. |
| 81 | * @param proberType prober type to use for encoding |
| 82 | */ |
| 83 | void setEncodingProberType(KEncodingProber::ProberType proberType) |
| 84 | { |
| 85 | m_encodingProberType = proberType; |
| 86 | } |
| 87 | |
| 88 | /** |
| 89 | * Get encoding prober type for this buffer |
| 90 | * @return currently in use prober type of this buffer |
| 91 | */ |
| 92 | KEncodingProber::ProberType encodingProberType() const |
| 93 | { |
| 94 | return m_encodingProberType; |
| 95 | } |
| 96 | |
| 97 | /** |
| 98 | * Set fallback codec for this buffer to use for load. |
| 99 | * @param codec fallback to use for encoding |
| 100 | */ |
| 101 | void setFallbackTextCodec(const QString &codec) |
| 102 | { |
| 103 | m_fallbackTextCodec = codec; |
| 104 | } |
| 105 | |
| 106 | /** |
| 107 | * Get fallback codec for this buffer |
| 108 | * @return currently in use fallback codec of this buffer |
| 109 | */ |
| 110 | QString fallbackTextCodec() const |
| 111 | { |
| 112 | return m_fallbackTextCodec; |
| 113 | } |
| 114 | |
| 115 | /** |
| 116 | * Set codec for this buffer to use for load/save. |
| 117 | * Loading might overwrite this, if it encounters problems and finds a better codec. |
| 118 | * Might change BOM setting. |
| 119 | * @param codec to use for encoding |
| 120 | */ |
| 121 | void setTextCodec(const QString &codec); |
| 122 | |
| 123 | /** |
| 124 | * Get codec for this buffer |
| 125 | * @return currently in use codec of this buffer |
| 126 | */ |
| 127 | QString textCodec() const |
| 128 | { |
| 129 | return m_textCodec; |
| 130 | } |
| 131 | |
| 132 | /** |
| 133 | * Generate byte order mark on save. |
| 134 | * Loading might overwrite this setting, if there is a BOM found inside the file. |
| 135 | * @param generateByteOrderMark should BOM be generated? |
| 136 | */ |
| 137 | void setGenerateByteOrderMark(bool generateByteOrderMark) |
| 138 | { |
| 139 | m_generateByteOrderMark = generateByteOrderMark; |
| 140 | } |
| 141 | |
| 142 | /** |
| 143 | * Generate byte order mark on save? |
| 144 | * @return should BOM be generated? |
| 145 | */ |
| 146 | bool generateByteOrderMark() const |
| 147 | { |
| 148 | return m_generateByteOrderMark; |
| 149 | } |
| 150 | |
| 151 | /** |
| 152 | * Set end of line mode for this buffer, not allowed to be set to unknown. |
| 153 | * Loading might overwrite this setting, if there is a eol found inside the file. |
| 154 | * @param endOfLineMode new eol mode |
| 155 | */ |
| 156 | void setEndOfLineMode(EndOfLineMode endOfLineMode) |
| 157 | { |
| 158 | Q_ASSERT(endOfLineMode != eolUnknown); |
| 159 | m_endOfLineMode = endOfLineMode; |
| 160 | } |
| 161 | |
| 162 | /** |
| 163 | * Get end of line mode |
| 164 | * @return end of line mode |
| 165 | */ |
| 166 | EndOfLineMode endOfLineMode() const |
| 167 | { |
| 168 | return m_endOfLineMode; |
| 169 | } |
| 170 | |
| 171 | /** |
| 172 | * Set line length limit |
| 173 | * @param lineLengthLimit new line length limit |
| 174 | */ |
| 175 | void setLineLengthLimit(int lineLengthLimit) |
| 176 | { |
| 177 | m_lineLengthLimit = lineLengthLimit; |
| 178 | } |
| 179 | |
| 180 | /** |
| 181 | * Load the given file. This will first clear the buffer and then load the file. |
| 182 | * Even on error during loading the buffer will still be cleared. |
| 183 | * Before calling this, setTextCodec must have been used to set codec! |
| 184 | * @param filename file to open |
| 185 | * @param encodingErrors were there problems occurred while decoding the file? |
| 186 | * @param tooLongLinesWrapped were too long lines found and wrapped? |
| 187 | * @param longestLineLoaded the longest line in the file (before wrapping) |
| 188 | * @param enforceTextCodec enforce to use only the set text codec |
| 189 | * @return success, the file got loaded, perhaps with encoding errors |
| 190 | * Virtual, can be overwritten. |
| 191 | */ |
| 192 | virtual bool load(const QString &filename, bool &encodingErrors, bool &tooLongLinesWrapped, int &longestLineLoaded, bool enforceTextCodec); |
| 193 | |
| 194 | /** |
| 195 | * Save the current buffer content to the given file. |
| 196 | * Before calling this, setTextCodec and setFallbackTextCodec must have been used to set codec! |
| 197 | * @param filename file to save |
| 198 | * @return success |
| 199 | * Virtual, can be overwritten. |
| 200 | */ |
| 201 | virtual bool save(const QString &filename); |
| 202 | |
| 203 | /** |
| 204 | * Lines currently stored in this buffer. |
| 205 | * This is never 0, even clear will let one empty line remain. |
| 206 | */ |
| 207 | int lines() const |
| 208 | { |
| 209 | Q_ASSERT(m_lines > 0); |
| 210 | return m_lines; |
| 211 | } |
| 212 | |
| 213 | /** |
| 214 | * Revision of this buffer. Is set to 0 on construction, clear() (load will trigger clear()). |
| 215 | * Is incremented on each change to the buffer. |
| 216 | * @return current revision |
| 217 | */ |
| 218 | qint64 revision() const |
| 219 | { |
| 220 | return m_revision; |
| 221 | } |
| 222 | |
| 223 | /** |
| 224 | * Retrieve a text line. |
| 225 | * @param line wanted line number |
| 226 | * @return text line |
| 227 | */ |
| 228 | TextLine line(int line) const; |
| 229 | |
| 230 | /** |
| 231 | * Transfer all non text attributes for the given line from the given text line to the one in the buffer. |
| 232 | * @param line line number to set attributes |
| 233 | * @param textLine line reference to get attributes from |
| 234 | */ |
| 235 | void setLineMetaData(int line, const TextLine &textLine); |
| 236 | |
| 237 | /** |
| 238 | * Retrieve length for @p line |
| 239 | * @param line wanted line number |
| 240 | * @return length of the line |
| 241 | */ |
| 242 | int lineLength(int line) const |
| 243 | { |
| 244 | // get block, this will assert on invalid line |
| 245 | int blockIndex = blockForLine(line); |
| 246 | |
| 247 | // get line length |
| 248 | return m_blocks.at(n: blockIndex)->lineLength(line); |
| 249 | } |
| 250 | |
| 251 | /** |
| 252 | * Retrieve offset in text for the given cursor position |
| 253 | */ |
| 254 | int cursorToOffset(KTextEditor::Cursor c) const; |
| 255 | |
| 256 | /** |
| 257 | * Retrieve cursor in text for the given offset |
| 258 | */ |
| 259 | KTextEditor::Cursor offsetToCursor(int offset) const; |
| 260 | |
| 261 | /** |
| 262 | * Retrieve text of complete buffer. |
| 263 | * @return text for this buffer, lines separated by '\n' |
| 264 | */ |
| 265 | QString text() const; |
| 266 | |
| 267 | /** |
| 268 | * Start an editing transaction, the wrapLine/unwrapLine/insertText and removeText functions |
| 269 | * are only allowed to be called inside a editing transaction. |
| 270 | * Editing transactions can stack. The number of startEdit and endEdit calls must match. |
| 271 | * @return returns true, if no transaction was already running |
| 272 | * Virtual, can be overwritten. |
| 273 | */ |
| 274 | virtual bool startEditing(); |
| 275 | |
| 276 | /** |
| 277 | * Finish an editing transaction. Only allowed to be called if editing transaction is started. |
| 278 | * @return returns true, if this finished last running transaction |
| 279 | * Virtual, can be overwritten. |
| 280 | */ |
| 281 | virtual bool finishEditing(); |
| 282 | |
| 283 | /** |
| 284 | * Query the number of editing transactions running atm. |
| 285 | * @return number of running transactions |
| 286 | */ |
| 287 | int editingTransactions() const |
| 288 | { |
| 289 | return m_editingTransactions; |
| 290 | } |
| 291 | |
| 292 | /** |
| 293 | * Query the revision of this buffer before the ongoing editing transactions. |
| 294 | * @return revision of buffer before current editing transaction altered it |
| 295 | */ |
| 296 | qint64 editingLastRevision() const |
| 297 | { |
| 298 | return m_editingLastRevision; |
| 299 | } |
| 300 | |
| 301 | /** |
| 302 | * Query the number of lines of this buffer before the ongoing editing transactions. |
| 303 | * @return number of lines of buffer before current editing transaction altered it |
| 304 | */ |
| 305 | int editingLastLines() const |
| 306 | { |
| 307 | return m_editingLastLines; |
| 308 | } |
| 309 | |
| 310 | /** |
| 311 | * Query information from the last editing transaction: was the content of the buffer changed? |
| 312 | * This is checked by comparing the editingLastRevision() with the current revision(). |
| 313 | * @return content of buffer was changed in last transaction? |
| 314 | */ |
| 315 | bool editingChangedBuffer() const |
| 316 | { |
| 317 | return editingLastRevision() != revision(); |
| 318 | } |
| 319 | |
| 320 | /** |
| 321 | * Query information from the last editing transaction: was the number of lines of the buffer changed? |
| 322 | * This is checked by comparing the editingLastLines() with the current lines(). |
| 323 | * @return content of buffer was changed in last transaction? |
| 324 | */ |
| 325 | bool editingChangedNumberOfLines() const |
| 326 | { |
| 327 | return editingLastLines() != lines(); |
| 328 | } |
| 329 | |
| 330 | /** |
| 331 | * Get minimal line number changed by last editing transaction |
| 332 | * @return maximal line number changed by last editing transaction, or -1, if none changed |
| 333 | */ |
| 334 | int editingMinimalLineChanged() const |
| 335 | { |
| 336 | return m_editingMinimalLineChanged; |
| 337 | } |
| 338 | |
| 339 | /** |
| 340 | * Get maximal line number changed by last editing transaction |
| 341 | * @return maximal line number changed by last editing transaction, or -1, if none changed |
| 342 | */ |
| 343 | int editingMaximalLineChanged() const |
| 344 | { |
| 345 | return m_editingMaximalLineChanged; |
| 346 | } |
| 347 | |
| 348 | /** |
| 349 | * Wrap line at given cursor position. |
| 350 | * @param position line/column as cursor where to wrap |
| 351 | * Virtual, can be overwritten. |
| 352 | */ |
| 353 | virtual void wrapLine(const KTextEditor::Cursor position); |
| 354 | |
| 355 | /** |
| 356 | * Unwrap given line. |
| 357 | * @param line line to unwrap |
| 358 | * Virtual, can be overwritten. |
| 359 | */ |
| 360 | virtual void unwrapLine(int line); |
| 361 | |
| 362 | /** |
| 363 | * Insert text at given cursor position. Does nothing if text is empty, beside some consistency checks. |
| 364 | * @param position position where to insert text |
| 365 | * @param text text to insert |
| 366 | * Virtual, can be overwritten. |
| 367 | */ |
| 368 | virtual void insertText(const KTextEditor::Cursor position, const QString &text); |
| 369 | |
| 370 | /** |
| 371 | * Remove text at given range. Does nothing if range is empty, beside some consistency checks. |
| 372 | * @param range range of text to remove, must be on one line only. |
| 373 | * Virtual, can be overwritten. |
| 374 | */ |
| 375 | virtual void removeText(KTextEditor::Range range); |
| 376 | |
| 377 | /** |
| 378 | * TextHistory of this buffer |
| 379 | * @return text history for this buffer |
| 380 | */ |
| 381 | TextHistory &history() |
| 382 | { |
| 383 | return m_history; |
| 384 | } |
| 385 | |
| 386 | Q_SIGNALS: |
| 387 | /** |
| 388 | * Buffer got cleared. This is emitted when constructor or load have called clear() internally, |
| 389 | * or when the user of the buffer has called clear() itself. |
| 390 | */ |
| 391 | void cleared(); |
| 392 | |
| 393 | /** |
| 394 | * Buffer loaded successfully a file |
| 395 | * @param filename file which was loaded |
| 396 | * @param encodingErrors were there problems occurred while decoding the file? |
| 397 | */ |
| 398 | void loaded(const QString &filename, bool encodingErrors); |
| 399 | |
| 400 | /** |
| 401 | * Buffer saved successfully a file |
| 402 | * @param filename file which was saved |
| 403 | */ |
| 404 | void saved(const QString &filename); |
| 405 | |
| 406 | private: |
| 407 | /** |
| 408 | * Save result which indicates an abstract reason why the operation has |
| 409 | * failed |
| 410 | */ |
| 411 | enum class SaveResult { |
| 412 | Failed = 0, |
| 413 | MissingPermissions, |
| 414 | Success |
| 415 | }; |
| 416 | |
| 417 | /** |
| 418 | * Find block containing given line. |
| 419 | * @param line we want to find block for this line |
| 420 | * @return index of found block |
| 421 | */ |
| 422 | int blockForLine(int line) const; |
| 423 | // exported for movingrange_test |
| 424 | |
| 425 | /** |
| 426 | * Fix start lines of all blocks after the given one |
| 427 | * @param startBlock index of block from which we start to fix |
| 428 | */ |
| 429 | KTEXTEDITOR_NO_EXPORT |
| 430 | void fixStartLines(int startBlock, int value); |
| 431 | |
| 432 | /** |
| 433 | * Balance the given block. Look if it is too small or too large. |
| 434 | * @param index block to balance |
| 435 | */ |
| 436 | KTEXTEDITOR_NO_EXPORT |
| 437 | void balanceBlock(int index); |
| 438 | |
| 439 | /** |
| 440 | * A range changed, notify the views, in case of attributes or feedback. |
| 441 | * @param view which view is affected? nullptr for all views |
| 442 | * @param lineRange line range that the change spans |
| 443 | * @param needsRepaint do we need to trigger repaints? e.g. if ranges with attributes change |
| 444 | * @param deletedRange if set, points to the range that is deleted |
| 445 | */ |
| 446 | KTEXTEDITOR_NO_EXPORT |
| 447 | void (KTextEditor::View *view, KTextEditor::LineRange lineRange, bool needsRepaint, TextRange *deletedRange = nullptr); |
| 448 | |
| 449 | /** |
| 450 | * Mark all modified lines as lines saved on disk (modified line system). |
| 451 | */ |
| 452 | KTEXTEDITOR_NO_EXPORT |
| 453 | void markModifiedLinesAsSaved(); |
| 454 | |
| 455 | /** |
| 456 | * Save the current buffer content to the given already opened device |
| 457 | * |
| 458 | * @param filename path name for display/debugging purposes |
| 459 | * @param saveFile open device to write the buffer to |
| 460 | */ |
| 461 | KTEXTEDITOR_NO_EXPORT |
| 462 | bool saveBuffer(const QString &filename, KCompressionDevice &saveFile); |
| 463 | |
| 464 | /** |
| 465 | * Attempt to save the buffer content in the given filename location using |
| 466 | * current privileges. |
| 467 | */ |
| 468 | KTEXTEDITOR_NO_EXPORT |
| 469 | SaveResult saveBufferUnprivileged(const QString &filename); |
| 470 | |
| 471 | /** |
| 472 | * Attempt to save the buffer content in the given filename location using |
| 473 | * escalated privileges. |
| 474 | */ |
| 475 | KTEXTEDITOR_NO_EXPORT |
| 476 | bool saveBufferEscalated(const QString &filename); |
| 477 | |
| 478 | public: |
| 479 | /** |
| 480 | * Gets the document to which this buffer is bound. |
| 481 | * \return a pointer to the document |
| 482 | */ |
| 483 | KTextEditor::DocumentPrivate *document() const |
| 484 | { |
| 485 | return m_document; |
| 486 | } |
| 487 | |
| 488 | /** |
| 489 | * Debug output, print whole buffer content with line numbers and line length |
| 490 | * @param title title for this output |
| 491 | */ |
| 492 | void debugPrint(const QString &title) const; |
| 493 | |
| 494 | /** |
| 495 | * Return the ranges which affect the given line. |
| 496 | * @param line line to look at |
| 497 | * @param view only return ranges associated with given view |
| 498 | * @param rangesWithAttributeOnly only return ranges which have a attribute set |
| 499 | * @return list of ranges affecting this line |
| 500 | */ |
| 501 | QList<TextRange *> rangesForLine(int line, KTextEditor::View *view, bool rangesWithAttributeOnly) const |
| 502 | { |
| 503 | // get block, this will assert on invalid line |
| 504 | QList<TextRange *> outRanges; |
| 505 | rangesForLine(line, view, rangesWithAttributeOnly, outRanges); |
| 506 | return outRanges; |
| 507 | } |
| 508 | |
| 509 | void (int line, KTextEditor::View *view, bool rangesWithAttributeOnly, QList<TextRange *> &outRanges) const; |
| 510 | |
| 511 | /** |
| 512 | * Invalidate all ranges in this buffer. |
| 513 | */ |
| 514 | void invalidateRanges(); |
| 515 | |
| 516 | /** |
| 517 | * Add/Remove a multiline range that spans multiple blocks |
| 518 | */ |
| 519 | KTEXTEDITOR_NO_EXPORT void (TextRange *range); |
| 520 | KTEXTEDITOR_NO_EXPORT void (TextRange *range); |
| 521 | bool hasMultlineRange(KTextEditor::MovingRange *range) const; |
| 522 | |
| 523 | // |
| 524 | // checksum handling |
| 525 | // |
| 526 | public: |
| 527 | /** |
| 528 | * Checksum of the document on disk, set either through file loading |
| 529 | * in openFile() or in KTextEditor::DocumentPrivate::saveFile() |
| 530 | * @return git compatible sha1 checksum for this document |
| 531 | */ |
| 532 | const QByteArray &digest() const; |
| 533 | |
| 534 | /** |
| 535 | * Set the checksum of this buffer. Make sure this checksum is up-to-date |
| 536 | * when reading digest(). |
| 537 | * @param checksum git compatible sha1 digest for the document on disk |
| 538 | */ |
| 539 | void setDigest(const QByteArray &checksum); |
| 540 | |
| 541 | private: |
| 542 | QByteArray m_digest; |
| 543 | |
| 544 | private: |
| 545 | /** |
| 546 | * parent document |
| 547 | */ |
| 548 | KTextEditor::DocumentPrivate *m_document; |
| 549 | |
| 550 | /** |
| 551 | * text history |
| 552 | */ |
| 553 | TextHistory m_history; |
| 554 | |
| 555 | /** |
| 556 | * List of starting lines of the blocks in m_blocks |
| 557 | */ |
| 558 | std::vector<int> m_startLines; |
| 559 | |
| 560 | /** |
| 561 | * List of blocks which contain the lines of this buffer |
| 562 | */ |
| 563 | std::vector<TextBlock *> m_blocks; |
| 564 | |
| 565 | /** |
| 566 | * List of blocksizes of each block in m_blocks |
| 567 | */ |
| 568 | std::vector<int> m_blockSizes; |
| 569 | |
| 570 | /** |
| 571 | * Number of lines in buffer |
| 572 | */ |
| 573 | int m_lines; |
| 574 | |
| 575 | /** |
| 576 | * Revision of the buffer. |
| 577 | */ |
| 578 | qint64 m_revision; |
| 579 | |
| 580 | /** |
| 581 | * Current number of running edit transactions |
| 582 | */ |
| 583 | int m_editingTransactions; |
| 584 | |
| 585 | /** |
| 586 | * Revision remembered at start of current editing transaction |
| 587 | */ |
| 588 | qint64 m_editingLastRevision; |
| 589 | |
| 590 | /** |
| 591 | * Number of lines remembered at start of current editing transaction |
| 592 | */ |
| 593 | int m_editingLastLines; |
| 594 | |
| 595 | /** |
| 596 | * minimal line number changed by last editing transaction |
| 597 | */ |
| 598 | int m_editingMinimalLineChanged; |
| 599 | |
| 600 | /** |
| 601 | * maximal line number changed by last editing transaction |
| 602 | */ |
| 603 | int m_editingMaximalLineChanged; |
| 604 | |
| 605 | /** |
| 606 | * Multiline ranges that span multiple blocks |
| 607 | */ |
| 608 | std::vector<TextRange *> m_multilineRanges; |
| 609 | |
| 610 | /** |
| 611 | * Encoding prober type to use |
| 612 | */ |
| 613 | KEncodingProber::ProberType m_encodingProberType; |
| 614 | |
| 615 | /** |
| 616 | * Fallback text codec to use |
| 617 | */ |
| 618 | QString m_fallbackTextCodec; |
| 619 | |
| 620 | /** |
| 621 | * Text codec to use |
| 622 | */ |
| 623 | QString m_textCodec; |
| 624 | |
| 625 | /** |
| 626 | * Mime-Type used for transparent compression/decompression support |
| 627 | * Set by load(), reset by clear() |
| 628 | */ |
| 629 | QString m_mimeTypeForFilterDev; |
| 630 | |
| 631 | /** |
| 632 | * Should byte order mark be created? |
| 633 | */ |
| 634 | bool m_generateByteOrderMark; |
| 635 | |
| 636 | /** |
| 637 | * End of line mode, default is Unix |
| 638 | */ |
| 639 | EndOfLineMode m_endOfLineMode; |
| 640 | |
| 641 | /** |
| 642 | * Limit for line length, longer lines will be wrapped on load |
| 643 | */ |
| 644 | int m_lineLengthLimit; |
| 645 | |
| 646 | /** |
| 647 | * For unit-testing purposes only. |
| 648 | */ |
| 649 | bool m_alwaysUseKAuthForSave; |
| 650 | |
| 651 | /** |
| 652 | * For copying QBuffer -> QTemporaryFile while saving document in privileged mode |
| 653 | */ |
| 654 | static const qint64 bufferLength = 4096; |
| 655 | }; |
| 656 | |
| 657 | } |
| 658 | |
| 659 | #endif |
| 660 | |