File indexing completed on 2024-04-21 03:57:27

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2001-2004 Christoph Cullmann <cullmann@kde.org>
0004     SPDX-FileCopyrightText: 2001 Joseph Wenninger <jowenn@kde.org>
0005     SPDX-FileCopyrightText: 1999 Jochen Wilhelmy <digisnap@cs.tu-berlin.de>
0006     SPDX-FileCopyrightText: 2006 Hamish Rodda <rodda@kde.org>
0007     SPDX-FileCopyrightText: 2007 Mirko Stocker <me@misto.ch>
0008     SPDX-FileCopyrightText: 2009-2010 Michel Ludwig <michel.ludwig@kdemail.net>
0009     SPDX-FileCopyrightText: 2013 Gerald Senarclens de Grancy <oss@senarclens.eu>
0010     SPDX-FileCopyrightText: 2013 Andrey Matveyakin <a.matveyakin@gmail.com>
0011 
0012     SPDX-License-Identifier: LGPL-2.0-only
0013 */
0014 // BEGIN includes
0015 #include "katedocument.h"
0016 #include "config.h"
0017 #include "kateabstractinputmode.h"
0018 #include "kateautoindent.h"
0019 #include "katebuffer.h"
0020 #include "katecompletionwidget.h"
0021 #include "kateconfig.h"
0022 #include "katedialogs.h"
0023 #include "kateglobal.h"
0024 #include "katehighlight.h"
0025 #include "kateindentdetecter.h"
0026 #include "katemodemanager.h"
0027 #include "katepartdebug.h"
0028 #include "kateplaintextsearch.h"
0029 #include "kateregexpsearch.h"
0030 #include "katerenderer.h"
0031 #include "katescriptmanager.h"
0032 #include "kateswapfile.h"
0033 #include "katesyntaxmanager.h"
0034 #include "katetemplatehandler.h"
0035 #include "kateundomanager.h"
0036 #include "katevariableexpansionmanager.h"
0037 #include "kateview.h"
0038 #include "printing/kateprinter.h"
0039 #include "spellcheck/ontheflycheck.h"
0040 #include "spellcheck/prefixstore.h"
0041 #include "spellcheck/spellcheck.h"
0042 #include <fcntl.h>
0043 #include <qchar.h>
0044 
0045 #if EDITORCONFIG_FOUND
0046 #include "editorconfig.h"
0047 #endif
0048 
0049 #include <KTextEditor/Attribute>
0050 #include <KTextEditor/DocumentCursor>
0051 #include <ktexteditor/message.h>
0052 
0053 #include <KConfigGroup>
0054 #include <KDirWatch>
0055 #include <KFileItem>
0056 #include <KIO/FileCopyJob>
0057 #include <KIO/JobUiDelegate>
0058 #include <KIO/StatJob>
0059 #include <KJobWidgets>
0060 #include <KMessageBox>
0061 #include <KMountPoint>
0062 #include <KNetworkMounts>
0063 #include <KParts/OpenUrlArguments>
0064 #include <KStandardAction>
0065 #include <KStringHandler>
0066 #include <KToggleAction>
0067 #include <KXMLGUIFactory>
0068 
0069 #include <QApplication>
0070 #include <QClipboard>
0071 #include <QCryptographicHash>
0072 #include <QFile>
0073 #include <QFileDialog>
0074 #include <QLocale>
0075 #include <QMimeDatabase>
0076 #include <QProcess>
0077 #include <QRegularExpression>
0078 #include <QStandardPaths>
0079 #include <QTemporaryFile>
0080 #include <QTextStream>
0081 
0082 #include <cmath>
0083 
0084 // END  includes
0085 
0086 #if 0
0087 #define EDIT_DEBUG qCDebug(LOG_KTE)
0088 #else
0089 #define EDIT_DEBUG                                                                                                                                             \
0090     if (0)                                                                                                                                                     \
0091     qCDebug(LOG_KTE)
0092 #endif
0093 
0094 template<class C, class E>
0095 static int indexOf(const std::initializer_list<C> &list, const E &entry)
0096 {
0097     auto it = std::find(list.begin(), list.end(), entry);
0098     return it == list.end() ? -1 : std::distance(list.begin(), it);
0099 }
0100 
0101 template<class C, class E>
0102 static bool contains(const std::initializer_list<C> &list, const E &entry)
0103 {
0104     return indexOf(list, entry) >= 0;
0105 }
0106 
0107 static inline QChar matchingStartBracket(const QChar c)
0108 {
0109     switch (c.toLatin1()) {
0110     case '}':
0111         return QLatin1Char('{');
0112     case ']':
0113         return QLatin1Char('[');
0114     case ')':
0115         return QLatin1Char('(');
0116     }
0117     return QChar();
0118 }
0119 
0120 static inline QChar matchingEndBracket(const QChar c, bool withQuotes = true)
0121 {
0122     switch (c.toLatin1()) {
0123     case '{':
0124         return QLatin1Char('}');
0125     case '[':
0126         return QLatin1Char(']');
0127     case '(':
0128         return QLatin1Char(')');
0129     case '\'':
0130         return withQuotes ? QLatin1Char('\'') : QChar();
0131     case '"':
0132         return withQuotes ? QLatin1Char('"') : QChar();
0133     }
0134     return QChar();
0135 }
0136 
0137 static inline QChar matchingBracket(const QChar c)
0138 {
0139     QChar bracket = matchingStartBracket(c);
0140     if (bracket.isNull()) {
0141         bracket = matchingEndBracket(c, /*withQuotes=*/false);
0142     }
0143     return bracket;
0144 }
0145 
0146 static inline bool isStartBracket(const QChar c)
0147 {
0148     return !matchingEndBracket(c, /*withQuotes=*/false).isNull();
0149 }
0150 
0151 static inline bool isEndBracket(const QChar c)
0152 {
0153     return !matchingStartBracket(c).isNull();
0154 }
0155 
0156 static inline bool isBracket(const QChar c)
0157 {
0158     return isStartBracket(c) || isEndBracket(c);
0159 }
0160 
0161 // BEGIN d'tor, c'tor
0162 //
0163 // KTextEditor::DocumentPrivate Constructor
0164 //
0165 KTextEditor::DocumentPrivate::DocumentPrivate(const KPluginMetaData &data, bool bSingleViewMode, bool bReadOnly, QWidget *parentWidget, QObject *parent)
0166     : KTextEditor::Document(this, data, parent)
0167     , m_bSingleViewMode(bSingleViewMode)
0168     , m_bReadOnly(bReadOnly)
0169     ,
0170 
0171     m_undoManager(new KateUndoManager(this))
0172     ,
0173 
0174     m_buffer(new KateBuffer(this))
0175     , m_indenter(new KateAutoIndent(this))
0176     ,
0177 
0178     m_docName(QStringLiteral("need init"))
0179     ,
0180 
0181     m_fileType(QStringLiteral("Normal"))
0182     ,
0183 
0184     m_config(new KateDocumentConfig(this))
0185 
0186 {
0187     // setup component name
0188     const auto &aboutData = EditorPrivate::self()->aboutData();
0189     setComponentName(aboutData.componentName(), aboutData.displayName());
0190 
0191     // avoid spamming plasma and other window managers with progress dialogs
0192     // we show such stuff inline in the views!
0193     setProgressInfoEnabled(false);
0194 
0195     // register doc at factory
0196     KTextEditor::EditorPrivate::self()->registerDocument(this);
0197 
0198     // normal hl
0199     m_buffer->setHighlight(0);
0200 
0201     // swap file
0202     m_swapfile = (config()->swapFileMode() == KateDocumentConfig::DisableSwapFile) ? nullptr : new Kate::SwapFile(this);
0203 
0204     // some nice signals from the buffer
0205     connect(m_buffer, &KateBuffer::tagLines, this, &KTextEditor::DocumentPrivate::tagLines);
0206 
0207     // if the user changes the highlight with the dialog, notify the doc
0208     connect(KateHlManager::self(), &KateHlManager::changed, this, &KTextEditor::DocumentPrivate::internalHlChanged);
0209 
0210     // signals for mod on hd
0211     connect(KTextEditor::EditorPrivate::self()->dirWatch(), &KDirWatch::dirty, this, &KTextEditor::DocumentPrivate::slotModOnHdDirty);
0212 
0213     connect(KTextEditor::EditorPrivate::self()->dirWatch(), &KDirWatch::created, this, &KTextEditor::DocumentPrivate::slotModOnHdCreated);
0214 
0215     connect(KTextEditor::EditorPrivate::self()->dirWatch(), &KDirWatch::deleted, this, &KTextEditor::DocumentPrivate::slotModOnHdDeleted);
0216 
0217     // singleshot timer to handle updates of mod on hd state delayed
0218     m_modOnHdTimer.setSingleShot(true);
0219     m_modOnHdTimer.setInterval(200);
0220     connect(&m_modOnHdTimer, &QTimer::timeout, this, &KTextEditor::DocumentPrivate::slotDelayedHandleModOnHd);
0221 
0222     // Setup auto reload stuff
0223     m_autoReloadMode = new KToggleAction(i18n("Auto Reload Document"), this);
0224     m_autoReloadMode->setWhatsThis(i18n("Automatic reload the document when it was changed on disk"));
0225     connect(m_autoReloadMode, &KToggleAction::triggered, this, &DocumentPrivate::autoReloadToggled);
0226     // Prepare some reload amok protector...
0227     m_autoReloadThrottle.setSingleShot(true);
0228     //...but keep the value small in unit tests
0229     m_autoReloadThrottle.setInterval(KTextEditor::EditorPrivate::self()->unitTestMode() ? 50 : 3000);
0230     connect(&m_autoReloadThrottle, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload);
0231 
0232     // load handling
0233     // this is needed to ensure we signal the user if a file is still loading
0234     // and to disallow him to edit in that time
0235     connect(this, &KTextEditor::DocumentPrivate::started, this, &KTextEditor::DocumentPrivate::slotStarted);
0236     connect(this, qOverload<>(&KTextEditor::DocumentPrivate::completed), this, &KTextEditor::DocumentPrivate::slotCompleted);
0237     connect(this, &KTextEditor::DocumentPrivate::canceled, this, &KTextEditor::DocumentPrivate::slotCanceled);
0238 
0239     // handle doc name updates
0240     connect(this, &KParts::ReadOnlyPart::urlChanged, this, &KTextEditor::DocumentPrivate::slotUrlChanged);
0241     updateDocName();
0242 
0243     // if single view mode, like in the konqui embedding, create a default view ;)
0244     // be lazy, only create it now, if any parentWidget is given, otherwise widget()
0245     // will create it on demand...
0246     if (m_bSingleViewMode && parentWidget) {
0247         KTextEditor::View *view = (KTextEditor::View *)createView(parentWidget);
0248         insertChildClient(view);
0249         view->setContextMenu(view->defaultContextMenu());
0250         setWidget(view);
0251     }
0252 
0253     connect(m_undoManager, &KateUndoManager::undoChanged, this, &KTextEditor::DocumentPrivate::undoChanged);
0254     connect(m_undoManager, &KateUndoManager::undoStart, this, &KTextEditor::DocumentPrivate::editingStarted);
0255     connect(m_undoManager, &KateUndoManager::undoEnd, this, &KTextEditor::DocumentPrivate::editingFinished);
0256     connect(m_undoManager, &KateUndoManager::redoStart, this, &KTextEditor::DocumentPrivate::editingStarted);
0257     connect(m_undoManager, &KateUndoManager::redoEnd, this, &KTextEditor::DocumentPrivate::editingFinished);
0258 
0259     connect(this, &KTextEditor::DocumentPrivate::sigQueryClose, this, &KTextEditor::DocumentPrivate::slotQueryClose_save);
0260 
0261     connect(this, &KTextEditor::DocumentPrivate::aboutToInvalidateMovingInterfaceContent, this, &KTextEditor::DocumentPrivate::clearEditingPosStack);
0262     onTheFlySpellCheckingEnabled(config()->onTheFlySpellCheck());
0263 
0264     // make sure correct defaults are set (indenter, ...)
0265     updateConfig();
0266 
0267     m_autoSaveTimer.setSingleShot(true);
0268     connect(&m_autoSaveTimer, &QTimer::timeout, this, [this] {
0269         if (isModified() && url().isLocalFile()) {
0270             documentSave();
0271         }
0272     });
0273 }
0274 
0275 //
0276 // KTextEditor::DocumentPrivate Destructor
0277 //
0278 KTextEditor::DocumentPrivate::~DocumentPrivate()
0279 {
0280     // we need to disconnect this as it triggers in destructor of KParts::ReadOnlyPart but we have already deleted
0281     // important stuff then
0282     disconnect(this, &KParts::ReadOnlyPart::urlChanged, this, &KTextEditor::DocumentPrivate::slotUrlChanged);
0283 
0284     // delete pending mod-on-hd message, if applicable
0285     delete m_modOnHdHandler;
0286 
0287     // we are about to delete cursors/ranges/...
0288     Q_EMIT aboutToDeleteMovingInterfaceContent(this);
0289 
0290     // kill it early, it has ranges!
0291     delete m_onTheFlyChecker;
0292     m_onTheFlyChecker = nullptr;
0293 
0294     clearDictionaryRanges();
0295 
0296     // Tell the world that we're about to close (== destruct)
0297     // Apps must receive this in a direct signal-slot connection, and prevent
0298     // any further use of interfaces once they return.
0299     Q_EMIT aboutToClose(this);
0300 
0301     // remove file from dirwatch
0302     deactivateDirWatch();
0303 
0304     // thanks for offering, KPart, but we're already self-destructing
0305     setAutoDeleteWidget(false);
0306     setAutoDeletePart(false);
0307 
0308     // clean up remaining views
0309     qDeleteAll(m_views);
0310     m_views.clear();
0311 
0312     // clean up marks
0313     for (auto &mark : std::as_const(m_marks)) {
0314         delete mark;
0315     }
0316     m_marks.clear();
0317 
0318     // de-register document early from global collections
0319     // otherwise we might "use" them again during destruction in a half-valid state
0320     // see e.g. bug 422546 for similar issues with view
0321     // this is still early enough, as as long as m_config is valid, this document is still "OK"
0322     KTextEditor::EditorPrivate::self()->deregisterDocument(this);
0323 }
0324 // END
0325 
0326 void KTextEditor::DocumentPrivate::saveEditingPositions(const KTextEditor::Cursor cursor)
0327 {
0328     if (m_editingStackPosition != m_editingStack.size() - 1) {
0329         m_editingStack.resize(m_editingStackPosition);
0330     }
0331 
0332     // try to be clever: reuse existing cursors if possible
0333     std::shared_ptr<KTextEditor::MovingCursor> mc;
0334 
0335     // we might pop last one: reuse that
0336     if (!m_editingStack.isEmpty() && cursor.line() == m_editingStack.top()->line()) {
0337         mc = m_editingStack.pop();
0338     }
0339 
0340     // we might expire oldest one, reuse that one, if not already one there
0341     // we prefer the other one for reuse, as already on the right line aka in the right block!
0342     const int editingStackSizeLimit = 32;
0343     if (m_editingStack.size() >= editingStackSizeLimit) {
0344         if (mc) {
0345             m_editingStack.removeFirst();
0346         } else {
0347             mc = m_editingStack.takeFirst();
0348         }
0349     }
0350 
0351     // new cursor needed? or adjust existing one?
0352     if (mc) {
0353         mc->setPosition(cursor);
0354     } else {
0355         mc = std::shared_ptr<KTextEditor::MovingCursor>(newMovingCursor(cursor));
0356     }
0357 
0358     // add new one as top of stack
0359     m_editingStack.push(mc);
0360     m_editingStackPosition = m_editingStack.size() - 1;
0361 }
0362 
0363 KTextEditor::Cursor KTextEditor::DocumentPrivate::lastEditingPosition(EditingPositionKind nextOrPrev, KTextEditor::Cursor currentCursor)
0364 {
0365     if (m_editingStack.isEmpty()) {
0366         return KTextEditor::Cursor::invalid();
0367     }
0368     auto targetPos = m_editingStack.at(m_editingStackPosition)->toCursor();
0369     if (targetPos == currentCursor) {
0370         if (nextOrPrev == Previous) {
0371             m_editingStackPosition--;
0372         } else {
0373             m_editingStackPosition++;
0374         }
0375         m_editingStackPosition = qBound(0, m_editingStackPosition, m_editingStack.size() - 1);
0376     }
0377     return m_editingStack.at(m_editingStackPosition)->toCursor();
0378 }
0379 
0380 void KTextEditor::DocumentPrivate::clearEditingPosStack()
0381 {
0382     m_editingStack.clear();
0383     m_editingStackPosition = -1;
0384 }
0385 
0386 // on-demand view creation
0387 QWidget *KTextEditor::DocumentPrivate::widget()
0388 {
0389     // no singleViewMode -> no widget()...
0390     if (!singleViewMode()) {
0391         return nullptr;
0392     }
0393 
0394     // does a widget exist already? use it!
0395     if (KTextEditor::Document::widget()) {
0396         return KTextEditor::Document::widget();
0397     }
0398 
0399     // create and return one...
0400     KTextEditor::View *view = (KTextEditor::View *)createView(nullptr);
0401     insertChildClient(view);
0402     view->setContextMenu(view->defaultContextMenu());
0403     setWidget(view);
0404     return view;
0405 }
0406 
0407 // BEGIN KTextEditor::Document stuff
0408 
0409 KTextEditor::View *KTextEditor::DocumentPrivate::createView(QWidget *parent, KTextEditor::MainWindow *mainWindow)
0410 {
0411     KTextEditor::ViewPrivate *newView = new KTextEditor::ViewPrivate(this, parent, mainWindow);
0412 
0413     if (m_fileChangedDialogsActivated) {
0414         connect(newView, &KTextEditor::ViewPrivate::focusIn, this, &KTextEditor::DocumentPrivate::slotModifiedOnDisk);
0415     }
0416 
0417     Q_EMIT viewCreated(this, newView);
0418 
0419     // post existing messages to the new view, if no specific view is given
0420     const auto keys = m_messageHash.keys();
0421     for (KTextEditor::Message *message : keys) {
0422         if (!message->view()) {
0423             newView->postMessage(message, m_messageHash[message]);
0424         }
0425     }
0426 
0427     return newView;
0428 }
0429 
0430 KTextEditor::Range KTextEditor::DocumentPrivate::rangeOnLine(KTextEditor::Range range, int line) const
0431 {
0432     const int col1 = toVirtualColumn(range.start());
0433     const int col2 = toVirtualColumn(range.end());
0434     return KTextEditor::Range(line, fromVirtualColumn(line, col1), line, fromVirtualColumn(line, col2));
0435 }
0436 
0437 // BEGIN KTextEditor::EditInterface stuff
0438 
0439 bool KTextEditor::DocumentPrivate::isEditingTransactionRunning() const
0440 {
0441     return editSessionNumber > 0;
0442 }
0443 
0444 QString KTextEditor::DocumentPrivate::text() const
0445 {
0446     return m_buffer->text();
0447 }
0448 
0449 QString KTextEditor::DocumentPrivate::text(KTextEditor::Range range, bool blockwise) const
0450 {
0451     if (!range.isValid()) {
0452         qCWarning(LOG_KTE) << "Text requested for invalid range" << range;
0453         return QString();
0454     }
0455 
0456     QString s;
0457 
0458     if (range.start().line() == range.end().line()) {
0459         if (range.start().column() > range.end().column()) {
0460             return QString();
0461         }
0462 
0463         Kate::TextLine textLine = m_buffer->plainLine(range.start().line());
0464         return textLine.string(range.start().column(), range.end().column() - range.start().column());
0465     } else {
0466         for (int i = range.start().line(); (i <= range.end().line()) && (i < m_buffer->lines()); ++i) {
0467             Kate::TextLine textLine = m_buffer->plainLine(i);
0468             if (!blockwise) {
0469                 if (i == range.start().line()) {
0470                     s.append(textLine.string(range.start().column(), textLine.length() - range.start().column()));
0471                 } else if (i == range.end().line()) {
0472                     s.append(textLine.string(0, range.end().column()));
0473                 } else {
0474                     s.append(textLine.text());
0475                 }
0476             } else {
0477                 KTextEditor::Range subRange = rangeOnLine(range, i);
0478                 s.append(textLine.string(subRange.start().column(), subRange.columnWidth()));
0479             }
0480 
0481             if (i < range.end().line()) {
0482                 s.append(QLatin1Char('\n'));
0483             }
0484         }
0485     }
0486 
0487     return s;
0488 }
0489 
0490 QChar KTextEditor::DocumentPrivate::characterAt(KTextEditor::Cursor position) const
0491 {
0492     Kate::TextLine textLine = m_buffer->plainLine(position.line());
0493     return textLine.at(position.column());
0494 }
0495 
0496 QString KTextEditor::DocumentPrivate::wordAt(KTextEditor::Cursor cursor) const
0497 {
0498     return text(wordRangeAt(cursor));
0499 }
0500 
0501 KTextEditor::Range KTextEditor::DocumentPrivate::wordRangeAt(KTextEditor::Cursor cursor) const
0502 {
0503     // get text line
0504     const int line = cursor.line();
0505     Kate::TextLine textLine = m_buffer->plainLine(line);
0506 
0507     // make sure the cursor is
0508     const int lineLenth = textLine.length();
0509     if (cursor.column() > lineLenth) {
0510         return KTextEditor::Range::invalid();
0511     }
0512 
0513     int start = cursor.column();
0514     int end = start;
0515 
0516     while (start > 0 && highlight()->isInWord(textLine.at(start - 1), textLine.attribute(start - 1))) {
0517         start--;
0518     }
0519     while (end < lineLenth && highlight()->isInWord(textLine.at(end), textLine.attribute(end))) {
0520         end++;
0521     }
0522 
0523     return KTextEditor::Range(line, start, line, end);
0524 }
0525 
0526 bool KTextEditor::DocumentPrivate::isValidTextPosition(KTextEditor::Cursor cursor) const
0527 {
0528     const int ln = cursor.line();
0529     const int col = cursor.column();
0530     // cursor in document range?
0531     if (ln < 0 || col < 0 || ln >= lines() || col > lineLength(ln)) {
0532         return false;
0533     }
0534 
0535     const QString str = line(ln);
0536     Q_ASSERT(str.length() >= col);
0537 
0538     // cursor at end of line?
0539     const int len = lineLength(ln);
0540     if (col == 0 || col == len) {
0541         return true;
0542     }
0543 
0544     // cursor in the middle of a valid utf32-surrogate?
0545     return (!str.at(col).isLowSurrogate()) || (!str.at(col - 1).isHighSurrogate());
0546 }
0547 
0548 QStringList KTextEditor::DocumentPrivate::textLines(KTextEditor::Range range, bool blockwise) const
0549 {
0550     QStringList ret;
0551 
0552     if (!range.isValid()) {
0553         qCWarning(LOG_KTE) << "Text requested for invalid range" << range;
0554         return ret;
0555     }
0556 
0557     if (blockwise && (range.start().column() > range.end().column())) {
0558         return ret;
0559     }
0560 
0561     if (range.start().line() == range.end().line()) {
0562         Q_ASSERT(range.start() <= range.end());
0563 
0564         Kate::TextLine textLine = m_buffer->plainLine(range.start().line());
0565         ret << textLine.string(range.start().column(), range.end().column() - range.start().column());
0566     } else {
0567         for (int i = range.start().line(); (i <= range.end().line()) && (i < m_buffer->lines()); ++i) {
0568             Kate::TextLine textLine = m_buffer->plainLine(i);
0569             if (!blockwise) {
0570                 if (i == range.start().line()) {
0571                     ret << textLine.string(range.start().column(), textLine.length() - range.start().column());
0572                 } else if (i == range.end().line()) {
0573                     ret << textLine.string(0, range.end().column());
0574                 } else {
0575                     ret << textLine.text();
0576                 }
0577             } else {
0578                 KTextEditor::Range subRange = rangeOnLine(range, i);
0579                 ret << textLine.string(subRange.start().column(), subRange.columnWidth());
0580             }
0581         }
0582     }
0583 
0584     return ret;
0585 }
0586 
0587 QString KTextEditor::DocumentPrivate::line(int line) const
0588 {
0589     Kate::TextLine l = m_buffer->plainLine(line);
0590     return l.text();
0591 }
0592 
0593 bool KTextEditor::DocumentPrivate::setText(const QString &s)
0594 {
0595     if (!isReadWrite()) {
0596         return false;
0597     }
0598 
0599     std::vector<KTextEditor::Mark> msave;
0600     msave.reserve(m_marks.size());
0601     std::transform(m_marks.cbegin(), m_marks.cend(), std::back_inserter(msave), [](KTextEditor::Mark *mark) {
0602         return *mark;
0603     });
0604 
0605     for (auto v : std::as_const(m_views)) {
0606         static_cast<KTextEditor::ViewPrivate *>(v)->completionWidget()->setIgnoreBufferSignals(true);
0607     }
0608 
0609     editStart();
0610 
0611     // delete the text
0612     clear();
0613 
0614     // insert the new text
0615     insertText(KTextEditor::Cursor(), s);
0616 
0617     editEnd();
0618 
0619     for (auto v : std::as_const(m_views)) {
0620         static_cast<KTextEditor::ViewPrivate *>(v)->completionWidget()->setIgnoreBufferSignals(false);
0621     }
0622 
0623     for (KTextEditor::Mark mark : msave) {
0624         setMark(mark.line, mark.type);
0625     }
0626 
0627     return true;
0628 }
0629 
0630 bool KTextEditor::DocumentPrivate::setText(const QStringList &text)
0631 {
0632     if (!isReadWrite()) {
0633         return false;
0634     }
0635 
0636     std::vector<KTextEditor::Mark> msave;
0637     msave.reserve(m_marks.size());
0638     std::transform(m_marks.cbegin(), m_marks.cend(), std::back_inserter(msave), [](KTextEditor::Mark *mark) {
0639         return *mark;
0640     });
0641 
0642     for (auto v : std::as_const(m_views)) {
0643         static_cast<KTextEditor::ViewPrivate *>(v)->completionWidget()->setIgnoreBufferSignals(true);
0644     }
0645 
0646     editStart();
0647 
0648     // delete the text
0649     clear();
0650 
0651     // insert the new text
0652     insertText(KTextEditor::Cursor::start(), text);
0653 
0654     editEnd();
0655 
0656     for (auto v : std::as_const(m_views)) {
0657         static_cast<KTextEditor::ViewPrivate *>(v)->completionWidget()->setIgnoreBufferSignals(false);
0658     }
0659 
0660     for (KTextEditor::Mark mark : msave) {
0661         setMark(mark.line, mark.type);
0662     }
0663 
0664     return true;
0665 }
0666 
0667 bool KTextEditor::DocumentPrivate::clear()
0668 {
0669     if (!isReadWrite()) {
0670         return false;
0671     }
0672 
0673     for (auto view : std::as_const(m_views)) {
0674         static_cast<ViewPrivate *>(view)->clear();
0675         static_cast<ViewPrivate *>(view)->tagAll();
0676         view->update();
0677     }
0678 
0679     clearMarks();
0680 
0681     Q_EMIT aboutToInvalidateMovingInterfaceContent(this);
0682     m_buffer->invalidateRanges();
0683 
0684     Q_EMIT aboutToRemoveText(documentRange());
0685 
0686     return editRemoveLines(0, lastLine());
0687 }
0688 
0689 bool KTextEditor::DocumentPrivate::insertText(const KTextEditor::Cursor position, const QString &text, bool block)
0690 {
0691     if (!isReadWrite()) {
0692         return false;
0693     }
0694 
0695     if (text.isEmpty()) {
0696         return true;
0697     }
0698 
0699     editStart();
0700     // Disable emitting textInsertedRange signal in every editInsertText call
0701     // we will emit a single signal at the end of this function
0702     bool notify = false;
0703 
0704     int currentLine = position.line();
0705     int currentLineStart = 0;
0706     const int totalLength = text.length();
0707     int insertColumn = position.column();
0708 
0709     // pad with empty lines, if insert position is after last line
0710     if (position.line() > lines()) {
0711         int line = lines();
0712         while (line <= position.line()) {
0713             editInsertLine(line, QString(), false);
0714             line++;
0715         }
0716     }
0717 
0718     // compute expanded column for block mode
0719     int positionColumnExpanded = insertColumn;
0720     const int tabWidth = config()->tabWidth();
0721     if (block) {
0722         if (currentLine < lines()) {
0723             positionColumnExpanded = plainKateTextLine(currentLine).toVirtualColumn(insertColumn, tabWidth);
0724         }
0725     }
0726 
0727     int endCol = 0;
0728     int pos = 0;
0729     for (; pos < totalLength; pos++) {
0730         const QChar &ch = text.at(pos);
0731 
0732         if (ch == QLatin1Char('\n')) {
0733             // Only perform the text insert if there is text to insert
0734             if (currentLineStart < pos) {
0735                 editInsertText(currentLine, insertColumn, text.mid(currentLineStart, pos - currentLineStart), notify);
0736                 endCol = insertColumn + (pos - currentLineStart);
0737             }
0738 
0739             if (!block) {
0740                 // ensure we can handle wrap positions behind maximal column, same handling as in editInsertText for invalid columns
0741                 const auto wrapColumn = insertColumn + pos - currentLineStart;
0742                 const auto currentLineLength = lineLength(currentLine);
0743                 if (wrapColumn > currentLineLength) {
0744                     editInsertText(currentLine, currentLineLength, QString(wrapColumn - currentLineLength, QLatin1Char(' ')), notify);
0745                 }
0746 
0747                 // wrap line call is now save, as wrapColumn is valid for sure!
0748                 editWrapLine(currentLine, wrapColumn, /*newLine=*/true, nullptr, notify);
0749                 insertColumn = 0;
0750                 endCol = 0;
0751             }
0752 
0753             currentLine++;
0754 
0755             if (block) {
0756                 auto l = currentLine < lines();
0757                 if (currentLine == lastLine() + 1) {
0758                     editInsertLine(currentLine, QString(), notify);
0759                     endCol = 0;
0760                 }
0761                 insertColumn = positionColumnExpanded;
0762                 if (l) {
0763                     insertColumn = plainKateTextLine(currentLine).fromVirtualColumn(insertColumn, tabWidth);
0764                 }
0765             }
0766 
0767             currentLineStart = pos + 1;
0768         }
0769     }
0770 
0771     // Only perform the text insert if there is text to insert
0772     if (currentLineStart < pos) {
0773         editInsertText(currentLine, insertColumn, text.mid(currentLineStart, pos - currentLineStart), notify);
0774         endCol = insertColumn + (pos - currentLineStart);
0775     }
0776 
0777     // let the world know that we got some new text
0778     KTextEditor::Range insertedRange(position, currentLine, endCol);
0779     Q_EMIT textInsertedRange(this, insertedRange);
0780 
0781     editEnd();
0782     return true;
0783 }
0784 
0785 bool KTextEditor::DocumentPrivate::insertText(KTextEditor::Cursor position, const QStringList &textLines, bool block)
0786 {
0787     if (!isReadWrite()) {
0788         return false;
0789     }
0790 
0791     // just reuse normal function
0792     return insertText(position, textLines.join(QLatin1Char('\n')), block);
0793 }
0794 
0795 bool KTextEditor::DocumentPrivate::removeText(KTextEditor::Range _range, bool block)
0796 {
0797     KTextEditor::Range range = _range;
0798 
0799     if (!isReadWrite()) {
0800         return false;
0801     }
0802 
0803     // Should now be impossible to trigger with the new Range class
0804     Q_ASSERT(range.start().line() <= range.end().line());
0805 
0806     if (range.start().line() > lastLine()) {
0807         return false;
0808     }
0809 
0810     if (!block) {
0811         Q_EMIT aboutToRemoveText(range);
0812     }
0813 
0814     editStart();
0815 
0816     if (!block) {
0817         if (range.end().line() > lastLine()) {
0818             range.setEnd(KTextEditor::Cursor(lastLine() + 1, 0));
0819         }
0820 
0821         if (range.onSingleLine()) {
0822             editRemoveText(range.start().line(), range.start().column(), range.columnWidth());
0823         } else {
0824             int from = range.start().line();
0825             int to = range.end().line();
0826 
0827             // remove last line
0828             if (to <= lastLine()) {
0829                 editRemoveText(to, 0, range.end().column());
0830             }
0831 
0832             // editRemoveLines() will be called on first line (to remove bookmark)
0833             if (range.start().column() == 0 && from > 0) {
0834                 --from;
0835             }
0836 
0837             // remove middle lines
0838             editRemoveLines(from + 1, to - 1);
0839 
0840             // remove first line if not already removed by editRemoveLines()
0841             if (range.start().column() > 0 || range.start().line() == 0) {
0842                 editRemoveText(from, range.start().column(), m_buffer->plainLine(from).length() - range.start().column());
0843                 editUnWrapLine(from);
0844             }
0845         }
0846     } // if ( ! block )
0847     else {
0848         int startLine = qMax(0, range.start().line());
0849         int vc1 = toVirtualColumn(range.start());
0850         int vc2 = toVirtualColumn(range.end());
0851         for (int line = qMin(range.end().line(), lastLine()); line >= startLine; --line) {
0852             int col1 = fromVirtualColumn(line, vc1);
0853             int col2 = fromVirtualColumn(line, vc2);
0854             editRemoveText(line, qMin(col1, col2), qAbs(col2 - col1));
0855         }
0856     }
0857 
0858     editEnd();
0859     return true;
0860 }
0861 
0862 bool KTextEditor::DocumentPrivate::insertLine(int l, const QString &str)
0863 {
0864     if (!isReadWrite()) {
0865         return false;
0866     }
0867 
0868     if (l < 0 || l > lines()) {
0869         return false;
0870     }
0871 
0872     return editInsertLine(l, str);
0873 }
0874 
0875 bool KTextEditor::DocumentPrivate::insertLines(int line, const QStringList &text)
0876 {
0877     if (!isReadWrite()) {
0878         return false;
0879     }
0880 
0881     if (line < 0 || line > lines()) {
0882         return false;
0883     }
0884 
0885     bool success = true;
0886     for (const QString &string : text) {
0887         success &= editInsertLine(line++, string);
0888     }
0889 
0890     return success;
0891 }
0892 
0893 bool KTextEditor::DocumentPrivate::removeLine(int line)
0894 {
0895     if (!isReadWrite()) {
0896         return false;
0897     }
0898 
0899     if (line < 0 || line > lastLine()) {
0900         return false;
0901     }
0902 
0903     return editRemoveLine(line);
0904 }
0905 
0906 qsizetype KTextEditor::DocumentPrivate::totalCharacters() const
0907 {
0908     qsizetype l = 0;
0909     for (int i = 0; i < m_buffer->lines(); ++i) {
0910         l += m_buffer->lineLength(i);
0911     }
0912     return l;
0913 }
0914 
0915 int KTextEditor::DocumentPrivate::lines() const
0916 {
0917     return m_buffer->lines();
0918 }
0919 
0920 int KTextEditor::DocumentPrivate::lineLength(int line) const
0921 {
0922     return m_buffer->lineLength(line);
0923 }
0924 
0925 qsizetype KTextEditor::DocumentPrivate::cursorToOffset(KTextEditor::Cursor c) const
0926 {
0927     return m_buffer->cursorToOffset(c);
0928 }
0929 
0930 KTextEditor::Cursor KTextEditor::DocumentPrivate::offsetToCursor(qsizetype offset) const
0931 {
0932     return m_buffer->offsetToCursor(offset);
0933 }
0934 
0935 bool KTextEditor::DocumentPrivate::isLineModified(int line) const
0936 {
0937     if (line < 0 || line >= lines()) {
0938         return false;
0939     }
0940 
0941     Kate::TextLine l = m_buffer->plainLine(line);
0942     return l.markedAsModified();
0943 }
0944 
0945 bool KTextEditor::DocumentPrivate::isLineSaved(int line) const
0946 {
0947     if (line < 0 || line >= lines()) {
0948         return false;
0949     }
0950 
0951     Kate::TextLine l = m_buffer->plainLine(line);
0952     return l.markedAsSavedOnDisk();
0953 }
0954 
0955 bool KTextEditor::DocumentPrivate::isLineTouched(int line) const
0956 {
0957     if (line < 0 || line >= lines()) {
0958         return false;
0959     }
0960 
0961     Kate::TextLine l = m_buffer->plainLine(line);
0962     return l.markedAsModified() || l.markedAsSavedOnDisk();
0963 }
0964 // END
0965 
0966 // BEGIN KTextEditor::EditInterface internal stuff
0967 //
0968 // Starts an edit session with (or without) undo, update of view disabled during session
0969 //
0970 bool KTextEditor::DocumentPrivate::editStart()
0971 {
0972     editSessionNumber++;
0973 
0974     if (editSessionNumber > 1) {
0975         return false;
0976     }
0977 
0978     editIsRunning = true;
0979 
0980     // no last change cursor at start
0981     m_editLastChangeStartCursor = KTextEditor::Cursor::invalid();
0982 
0983     m_undoManager->editStart();
0984 
0985     for (auto view : std::as_const(m_views)) {
0986         static_cast<ViewPrivate *>(view)->editStart();
0987     }
0988 
0989     m_buffer->editStart();
0990     return true;
0991 }
0992 
0993 //
0994 // End edit session and update Views
0995 //
0996 bool KTextEditor::DocumentPrivate::editEnd()
0997 {
0998     if (editSessionNumber == 0) {
0999         Q_ASSERT(0);
1000         return false;
1001     }
1002 
1003     // wrap the new/changed text, if something really changed!
1004     if (m_buffer->editChanged() && (editSessionNumber == 1)) {
1005         if (m_undoManager->isActive() && config()->wordWrap()) {
1006             wrapText(m_buffer->editTagStart(), m_buffer->editTagEnd());
1007         }
1008     }
1009 
1010     editSessionNumber--;
1011 
1012     if (editSessionNumber > 0) {
1013         return false;
1014     }
1015 
1016     // end buffer edit, will trigger hl update
1017     // this will cause some possible adjustment of tagline start/end
1018     m_buffer->editEnd();
1019 
1020     m_undoManager->editEnd();
1021 
1022     // edit end for all views !!!!!!!!!
1023     for (auto view : std::as_const(m_views)) {
1024         static_cast<ViewPrivate *>(view)->editEnd(m_buffer->editTagStart(), m_buffer->editTagEnd(), m_buffer->editTagFrom());
1025     }
1026 
1027     if (m_buffer->editChanged()) {
1028         setModified(true);
1029         Q_EMIT textChanged(this);
1030     }
1031 
1032     // remember last change position in the stack, if any
1033     // this avoid costly updates for longer editing transactions
1034     // before we did that on textInsert/Removed
1035     if (m_editLastChangeStartCursor.isValid()) {
1036         saveEditingPositions(m_editLastChangeStartCursor);
1037     }
1038 
1039     if (config()->autoSave() && config()->autoSaveInterval() > 0) {
1040         m_autoSaveTimer.start();
1041     }
1042 
1043     editIsRunning = false;
1044     return true;
1045 }
1046 
1047 void KTextEditor::DocumentPrivate::pushEditState()
1048 {
1049     editStateStack.push(editSessionNumber);
1050 }
1051 
1052 void KTextEditor::DocumentPrivate::popEditState()
1053 {
1054     if (editStateStack.isEmpty()) {
1055         return;
1056     }
1057 
1058     int count = editStateStack.pop() - editSessionNumber;
1059     while (count < 0) {
1060         ++count;
1061         editEnd();
1062     }
1063     while (count > 0) {
1064         --count;
1065         editStart();
1066     }
1067 }
1068 
1069 void KTextEditor::DocumentPrivate::inputMethodStart()
1070 {
1071     m_undoManager->inputMethodStart();
1072 }
1073 
1074 void KTextEditor::DocumentPrivate::inputMethodEnd()
1075 {
1076     m_undoManager->inputMethodEnd();
1077 }
1078 
1079 bool KTextEditor::DocumentPrivate::wrapText(int startLine, int endLine)
1080 {
1081     if (startLine < 0 || endLine < 0) {
1082         return false;
1083     }
1084 
1085     if (!isReadWrite()) {
1086         return false;
1087     }
1088 
1089     int col = config()->wordWrapAt();
1090 
1091     if (col == 0) {
1092         return false;
1093     }
1094 
1095     editStart();
1096 
1097     for (int line = startLine; (line <= endLine) && (line < lines()); line++) {
1098         Kate::TextLine l = kateTextLine(line);
1099 
1100         // qCDebug(LOG_KTE) << "try wrap line: " << line;
1101 
1102         if (l.virtualLength(m_buffer->tabWidth()) > col) {
1103             bool nextlValid = line + 1 < lines();
1104             Kate::TextLine nextl = kateTextLine(line + 1);
1105 
1106             // qCDebug(LOG_KTE) << "do wrap line: " << line;
1107 
1108             int eolPosition = l.length() - 1;
1109 
1110             // take tabs into account here, too
1111             int x = 0;
1112             const QString &t = l.text();
1113             int z2 = 0;
1114             for (; z2 < l.length(); z2++) {
1115                 static const QChar tabChar(QLatin1Char('\t'));
1116                 if (t.at(z2) == tabChar) {
1117                     x += m_buffer->tabWidth() - (x % m_buffer->tabWidth());
1118                 } else {
1119                     x++;
1120                 }
1121 
1122                 if (x > col) {
1123                     break;
1124                 }
1125             }
1126 
1127             const int colInChars = qMin(z2, l.length() - 1);
1128             int searchStart = colInChars;
1129 
1130             // If where we are wrapping is an end of line and is a space we don't
1131             // want to wrap there
1132             if (searchStart == eolPosition && t.at(searchStart).isSpace()) {
1133                 searchStart--;
1134             }
1135 
1136             // Scan backwards looking for a place to break the line
1137             // We are not interested in breaking at the first char
1138             // of the line (if it is a space), but we are at the second
1139             // anders: if we can't find a space, try breaking on a word
1140             // boundary, using KateHighlight::canBreakAt().
1141             // This could be a priority (setting) in the hl/filetype/document
1142             int z = -1;
1143             int nw = -1; // alternative position, a non word character
1144             for (z = searchStart; z >= 0; z--) {
1145                 if (t.at(z).isSpace()) {
1146                     break;
1147                 }
1148                 if ((nw < 0) && highlight()->canBreakAt(t.at(z), l.attribute(z))) {
1149                     nw = z;
1150                 }
1151             }
1152 
1153             if (z >= 0) {
1154                 // So why don't we just remove the trailing space right away?
1155                 // Well, the (view's) cursor may be directly in front of that space
1156                 // (user typing text before the last word on the line), and if that
1157                 // happens, the cursor would be moved to the next line, which is not
1158                 // what we want (bug #106261)
1159                 z++;
1160             } else {
1161                 // There was no space to break at so break at a nonword character if
1162                 // found, or at the wrapcolumn ( that needs be configurable )
1163                 // Don't try and add any white space for the break
1164                 if ((nw >= 0) && nw < colInChars) {
1165                     nw++; // break on the right side of the character
1166                 }
1167                 z = (nw >= 0) ? nw : colInChars;
1168             }
1169 
1170             if (nextlValid && !nextl.isAutoWrapped()) {
1171                 editWrapLine(line, z, true);
1172                 editMarkLineAutoWrapped(line + 1, true);
1173 
1174                 endLine++;
1175             } else {
1176                 if (nextlValid && (nextl.length() > 0) && !nextl.at(0).isSpace() && ((l.length() < 1) || !l.at(l.length() - 1).isSpace())) {
1177                     editInsertText(line + 1, 0, QStringLiteral(" "));
1178                 }
1179 
1180                 bool newLineAdded = false;
1181                 editWrapLine(line, z, false, &newLineAdded);
1182 
1183                 editMarkLineAutoWrapped(line + 1, true);
1184 
1185                 endLine++;
1186             }
1187         }
1188     }
1189 
1190     editEnd();
1191 
1192     return true;
1193 }
1194 
1195 bool KTextEditor::DocumentPrivate::wrapParagraph(int first, int last)
1196 {
1197     if (first == last) {
1198         return wrapText(first, last);
1199     }
1200 
1201     if (first < 0 || last < first) {
1202         return false;
1203     }
1204 
1205     if (last >= lines() || first > last) {
1206         return false;
1207     }
1208 
1209     if (!isReadWrite()) {
1210         return false;
1211     }
1212 
1213     editStart();
1214 
1215     // Because we shrink and expand lines, we need to track the working set by powerful "MovingStuff"
1216     std::unique_ptr<KTextEditor::MovingRange> range(newMovingRange(KTextEditor::Range(first, 0, last, 0)));
1217     std::unique_ptr<KTextEditor::MovingCursor> curr(newMovingCursor(KTextEditor::Cursor(range->start())));
1218 
1219     // Scan the selected range for paragraphs, whereas each empty line trigger a new paragraph
1220     for (int line = first; line <= range->end().line(); ++line) {
1221         // Is our first line a somehow filled line?
1222         if (plainKateTextLine(first).firstChar() < 0) {
1223             // Fast forward to first non empty line
1224             ++first;
1225             curr->setPosition(curr->line() + 1, 0);
1226             continue;
1227         }
1228 
1229         // Is our current line a somehow filled line? If not, wrap the paragraph
1230         if (plainKateTextLine(line).firstChar() < 0) {
1231             curr->setPosition(line, 0); // Set on empty line
1232             joinLines(first, line - 1);
1233             // Don't wrap twice! That may cause a bad result
1234             if (!wordWrap()) {
1235                 wrapText(first, first);
1236             }
1237             first = curr->line() + 1;
1238             line = first;
1239         }
1240     }
1241 
1242     // If there was no paragraph, we need to wrap now
1243     bool needWrap = (curr->line() != range->end().line());
1244     if (needWrap && plainKateTextLine(first).firstChar() != -1) {
1245         joinLines(first, range->end().line());
1246         // Don't wrap twice! That may cause a bad result
1247         if (!wordWrap()) {
1248             wrapText(first, first);
1249         }
1250     }
1251 
1252     editEnd();
1253     return true;
1254 }
1255 
1256 bool KTextEditor::DocumentPrivate::editInsertText(int line, int col, const QString &s, bool notify)
1257 {
1258     // verbose debug
1259     EDIT_DEBUG << "editInsertText" << line << col << s;
1260 
1261     if (line < 0 || col < 0) {
1262         return false;
1263     }
1264 
1265     // nothing to do, do nothing!
1266     if (s.isEmpty()) {
1267         return true;
1268     }
1269 
1270     if (!isReadWrite()) {
1271         return false;
1272     }
1273 
1274     auto l = plainKateTextLine(line);
1275     int length = l.length();
1276     if (length < 0) {
1277         return false;
1278     }
1279 
1280     editStart();
1281 
1282     QString s2 = s;
1283     int col2 = col;
1284     if (col2 > length) {
1285         s2 = QString(col2 - length, QLatin1Char(' ')) + s;
1286         col2 = length;
1287     }
1288 
1289     m_undoManager->slotTextInserted(line, col2, s2, l);
1290 
1291     // remember last change cursor
1292     m_editLastChangeStartCursor = KTextEditor::Cursor(line, col2);
1293 
1294     // insert text into line
1295     m_buffer->insertText(m_editLastChangeStartCursor, s2);
1296 
1297     if (notify) {
1298         Q_EMIT textInsertedRange(this, KTextEditor::Range(line, col2, line, col2 + s2.length()));
1299     }
1300 
1301     editEnd();
1302     return true;
1303 }
1304 
1305 bool KTextEditor::DocumentPrivate::editRemoveText(int line, int col, int len)
1306 {
1307     // verbose debug
1308     EDIT_DEBUG << "editRemoveText" << line << col << len;
1309 
1310     if (line < 0 || line >= lines() || col < 0 || len < 0) {
1311         return false;
1312     }
1313 
1314     if (!isReadWrite()) {
1315         return false;
1316     }
1317 
1318     Kate::TextLine l = plainKateTextLine(line);
1319 
1320     // nothing to do, do nothing!
1321     if (len == 0) {
1322         return true;
1323     }
1324 
1325     // wrong column
1326     if (col >= l.text().size()) {
1327         return false;
1328     }
1329 
1330     // don't try to remove what's not there
1331     len = qMin(len, l.text().size() - col);
1332 
1333     editStart();
1334 
1335     QString oldText = l.string(col, len);
1336 
1337     m_undoManager->slotTextRemoved(line, col, oldText, l);
1338 
1339     // remember last change cursor
1340     m_editLastChangeStartCursor = KTextEditor::Cursor(line, col);
1341 
1342     // remove text from line
1343     m_buffer->removeText(KTextEditor::Range(m_editLastChangeStartCursor, KTextEditor::Cursor(line, col + len)));
1344 
1345     Q_EMIT textRemoved(this, KTextEditor::Range(line, col, line, col + len), oldText);
1346 
1347     editEnd();
1348 
1349     return true;
1350 }
1351 
1352 bool KTextEditor::DocumentPrivate::editMarkLineAutoWrapped(int line, bool autowrapped)
1353 {
1354     // verbose debug
1355     EDIT_DEBUG << "editMarkLineAutoWrapped" << line << autowrapped;
1356 
1357     if (line < 0 || line >= lines()) {
1358         return false;
1359     }
1360 
1361     if (!isReadWrite()) {
1362         return false;
1363     }
1364 
1365     editStart();
1366 
1367     m_undoManager->slotMarkLineAutoWrapped(line, autowrapped);
1368 
1369     Kate::TextLine l = kateTextLine(line);
1370     l.setAutoWrapped(autowrapped);
1371     m_buffer->setLineMetaData(line, l);
1372 
1373     editEnd();
1374 
1375     return true;
1376 }
1377 
1378 bool KTextEditor::DocumentPrivate::editWrapLine(int line, int col, bool newLine, bool *newLineAdded, bool notify)
1379 {
1380     // verbose debug
1381     EDIT_DEBUG << "editWrapLine" << line << col << newLine;
1382 
1383     if (line < 0 || line >= lines() || col < 0) {
1384         return false;
1385     }
1386 
1387     if (!isReadWrite()) {
1388         return false;
1389     }
1390 
1391     const auto tl = plainKateTextLine(line);
1392 
1393     editStart();
1394 
1395     const bool nextLineValid = lineLength(line + 1) >= 0;
1396 
1397     m_undoManager->slotLineWrapped(line, col, tl.length() - col, (!nextLineValid || newLine), tl);
1398 
1399     if (!nextLineValid || newLine) {
1400         m_buffer->wrapLine(KTextEditor::Cursor(line, col));
1401 
1402         QVarLengthArray<KTextEditor::Mark *, 8> list;
1403         for (const auto &mark : std::as_const(m_marks)) {
1404             if (mark->line >= line) {
1405                 if ((col == 0) || (mark->line > line)) {
1406                     list.push_back(mark);
1407                 }
1408             }
1409         }
1410 
1411         for (const auto &mark : list) {
1412             m_marks.take(mark->line);
1413         }
1414 
1415         for (const auto &mark : list) {
1416             mark->line++;
1417             m_marks.insert(mark->line, mark);
1418         }
1419 
1420         if (!list.empty()) {
1421             Q_EMIT marksChanged(this);
1422         }
1423 
1424         // yes, we added a new line !
1425         if (newLineAdded) {
1426             (*newLineAdded) = true;
1427         }
1428     } else {
1429         m_buffer->wrapLine(KTextEditor::Cursor(line, col));
1430         m_buffer->unwrapLine(line + 2);
1431 
1432         // no, no new line added !
1433         if (newLineAdded) {
1434             (*newLineAdded) = false;
1435         }
1436     }
1437 
1438     // remember last change cursor
1439     m_editLastChangeStartCursor = KTextEditor::Cursor(line, col);
1440 
1441     if (notify) {
1442         Q_EMIT textInsertedRange(this, KTextEditor::Range(line, col, line + 1, 0));
1443     }
1444 
1445     editEnd();
1446 
1447     return true;
1448 }
1449 
1450 bool KTextEditor::DocumentPrivate::editUnWrapLine(int line, bool removeLine, int length)
1451 {
1452     // verbose debug
1453     EDIT_DEBUG << "editUnWrapLine" << line << removeLine << length;
1454 
1455     if (line < 0 || line >= lines() || line + 1 >= lines() || length < 0) {
1456         return false;
1457     }
1458 
1459     if (!isReadWrite()) {
1460         return false;
1461     }
1462 
1463     const Kate::TextLine tl = plainKateTextLine(line);
1464     const Kate::TextLine nextLine = plainKateTextLine(line + 1);
1465 
1466     editStart();
1467 
1468     int col = tl.length();
1469     m_undoManager->slotLineUnWrapped(line, col, length, removeLine, tl, nextLine);
1470 
1471     if (removeLine) {
1472         m_buffer->unwrapLine(line + 1);
1473     } else {
1474         m_buffer->wrapLine(KTextEditor::Cursor(line + 1, length));
1475         m_buffer->unwrapLine(line + 1);
1476     }
1477 
1478     QVarLengthArray<KTextEditor::Mark *, 8> list;
1479     for (const auto &mark : std::as_const(m_marks)) {
1480         if (mark->line >= line + 1) {
1481             list.push_back(mark);
1482         }
1483 
1484         if (mark->line == line + 1) {
1485             auto m = m_marks.take(line);
1486             if (m) {
1487                 mark->type |= m->type;
1488                 delete m;
1489             }
1490         }
1491     }
1492 
1493     for (const auto &mark : list) {
1494         m_marks.take(mark->line);
1495     }
1496 
1497     for (const auto &mark : list) {
1498         mark->line--;
1499         m_marks.insert(mark->line, mark);
1500     }
1501 
1502     if (!list.isEmpty()) {
1503         Q_EMIT marksChanged(this);
1504     }
1505 
1506     // remember last change cursor
1507     m_editLastChangeStartCursor = KTextEditor::Cursor(line, col);
1508 
1509     Q_EMIT textRemoved(this, KTextEditor::Range(line, col, line + 1, 0), QStringLiteral("\n"));
1510 
1511     editEnd();
1512 
1513     return true;
1514 }
1515 
1516 bool KTextEditor::DocumentPrivate::editInsertLine(int line, const QString &s, bool notify)
1517 {
1518     // verbose debug
1519     EDIT_DEBUG << "editInsertLine" << line << s;
1520 
1521     if (line < 0) {
1522         return false;
1523     }
1524 
1525     if (!isReadWrite()) {
1526         return false;
1527     }
1528 
1529     if (line > lines()) {
1530         return false;
1531     }
1532 
1533     editStart();
1534 
1535     m_undoManager->slotLineInserted(line, s);
1536 
1537     // wrap line
1538     if (line > 0) {
1539         Kate::TextLine previousLine = m_buffer->line(line - 1);
1540         m_buffer->wrapLine(KTextEditor::Cursor(line - 1, previousLine.text().size()));
1541     } else {
1542         m_buffer->wrapLine(KTextEditor::Cursor(0, 0));
1543     }
1544 
1545     // insert text
1546     m_buffer->insertText(KTextEditor::Cursor(line, 0), s);
1547 
1548     QVarLengthArray<KTextEditor::Mark *, 8> list;
1549     for (const auto &mark : std::as_const(m_marks)) {
1550         if (mark->line >= line) {
1551             list.push_back(mark);
1552         }
1553     }
1554 
1555     for (const auto &mark : list) {
1556         m_marks.take(mark->line);
1557     }
1558 
1559     for (const auto &mark : list) {
1560         mark->line++;
1561         m_marks.insert(mark->line, mark);
1562     }
1563 
1564     if (!list.isEmpty()) {
1565         Q_EMIT marksChanged(this);
1566     }
1567 
1568     KTextEditor::Range rangeInserted(line, 0, line, m_buffer->lineLength(line));
1569 
1570     if (line) {
1571         int prevLineLength = lineLength(line - 1);
1572         rangeInserted.setStart(KTextEditor::Cursor(line - 1, prevLineLength));
1573     } else {
1574         rangeInserted.setEnd(KTextEditor::Cursor(line + 1, 0));
1575     }
1576 
1577     // remember last change cursor
1578     m_editLastChangeStartCursor = rangeInserted.start();
1579 
1580     if (notify) {
1581         Q_EMIT textInsertedRange(this, rangeInserted);
1582     }
1583 
1584     editEnd();
1585 
1586     return true;
1587 }
1588 
1589 bool KTextEditor::DocumentPrivate::editRemoveLine(int line)
1590 {
1591     return editRemoveLines(line, line);
1592 }
1593 
1594 bool KTextEditor::DocumentPrivate::editRemoveLines(int from, int to)
1595 {
1596     // verbose debug
1597     EDIT_DEBUG << "editRemoveLines" << from << to;
1598 
1599     if (to < from || from < 0 || to > lastLine()) {
1600         return false;
1601     }
1602 
1603     if (!isReadWrite()) {
1604         return false;
1605     }
1606 
1607     if (lines() == 1) {
1608         return editRemoveText(0, 0, lineLength(0));
1609     }
1610 
1611     editStart();
1612     QStringList oldText;
1613 
1614     // first remove text
1615     for (int line = to; line >= from; --line) {
1616         const Kate::TextLine l = plainKateTextLine(line);
1617         oldText.prepend(l.text());
1618         m_undoManager->slotLineRemoved(line, l.text(), l);
1619 
1620         m_buffer->removeText(KTextEditor::Range(KTextEditor::Cursor(line, 0), KTextEditor::Cursor(line, l.length())));
1621     }
1622 
1623     // then collapse lines
1624     for (int line = to; line >= from; --line) {
1625         // unwrap all lines, prefer to unwrap line behind, skip to wrap line 0
1626         if (line + 1 < m_buffer->lines()) {
1627             m_buffer->unwrapLine(line + 1);
1628         } else if (line) {
1629             m_buffer->unwrapLine(line);
1630         }
1631     }
1632 
1633     QVarLengthArray<int, 8> rmark;
1634     QVarLengthArray<KTextEditor::Mark *, 8> list;
1635 
1636     for (KTextEditor::Mark *mark : std::as_const(m_marks)) {
1637         int line = mark->line;
1638         if (line > to) {
1639             list << mark;
1640         } else if (line >= from) {
1641             rmark << line;
1642         }
1643     }
1644 
1645     for (int line : rmark) {
1646         delete m_marks.take(line);
1647     }
1648 
1649     for (auto mark : list) {
1650         m_marks.take(mark->line);
1651     }
1652 
1653     for (auto mark : list) {
1654         mark->line -= to - from + 1;
1655         m_marks.insert(mark->line, mark);
1656     }
1657 
1658     if (!list.isEmpty()) {
1659         Q_EMIT marksChanged(this);
1660     }
1661 
1662     KTextEditor::Range rangeRemoved(from, 0, to + 1, 0);
1663 
1664     if (to == lastLine() + to - from + 1) {
1665         rangeRemoved.setEnd(KTextEditor::Cursor(to, oldText.last().length()));
1666         if (from > 0) {
1667             int prevLineLength = lineLength(from - 1);
1668             rangeRemoved.setStart(KTextEditor::Cursor(from - 1, prevLineLength));
1669         }
1670     }
1671 
1672     // remember last change cursor
1673     m_editLastChangeStartCursor = rangeRemoved.start();
1674 
1675     Q_EMIT textRemoved(this, rangeRemoved, oldText.join(QLatin1Char('\n')) + QLatin1Char('\n'));
1676 
1677     editEnd();
1678 
1679     return true;
1680 }
1681 // END
1682 
1683 // BEGIN KTextEditor::UndoInterface stuff
1684 uint KTextEditor::DocumentPrivate::undoCount() const
1685 {
1686     return m_undoManager->undoCount();
1687 }
1688 
1689 uint KTextEditor::DocumentPrivate::redoCount() const
1690 {
1691     return m_undoManager->redoCount();
1692 }
1693 
1694 void KTextEditor::DocumentPrivate::undo()
1695 {
1696     m_undoManager->undo();
1697 }
1698 
1699 void KTextEditor::DocumentPrivate::redo()
1700 {
1701     m_undoManager->redo();
1702 }
1703 // END
1704 
1705 // BEGIN KTextEditor::SearchInterface stuff
1706 QList<KTextEditor::Range>
1707 KTextEditor::DocumentPrivate::searchText(KTextEditor::Range range, const QString &pattern, const KTextEditor::SearchOptions options) const
1708 {
1709     const bool escapeSequences = options.testFlag(KTextEditor::EscapeSequences);
1710     const bool regexMode = options.testFlag(KTextEditor::Regex);
1711     const bool backwards = options.testFlag(KTextEditor::Backwards);
1712     const bool wholeWords = options.testFlag(KTextEditor::WholeWords);
1713     const Qt::CaseSensitivity caseSensitivity = options.testFlag(KTextEditor::CaseInsensitive) ? Qt::CaseInsensitive : Qt::CaseSensitive;
1714 
1715     if (regexMode) {
1716         // regexp search
1717         // escape sequences are supported by definition
1718         QRegularExpression::PatternOptions patternOptions;
1719         if (caseSensitivity == Qt::CaseInsensitive) {
1720             patternOptions |= QRegularExpression::CaseInsensitiveOption;
1721         }
1722         KateRegExpSearch searcher(this);
1723         return searcher.search(pattern, range, backwards, patternOptions);
1724     }
1725 
1726     if (escapeSequences) {
1727         // escaped search
1728         KatePlainTextSearch searcher(this, caseSensitivity, wholeWords);
1729         KTextEditor::Range match = searcher.search(KateRegExpSearch::escapePlaintext(pattern), range, backwards);
1730 
1731         QList<KTextEditor::Range> result;
1732         result.append(match);
1733         return result;
1734     }
1735 
1736     // plaintext search
1737     KatePlainTextSearch searcher(this, caseSensitivity, wholeWords);
1738     KTextEditor::Range match = searcher.search(pattern, range, backwards);
1739 
1740     QList<KTextEditor::Range> result;
1741     result.append(match);
1742     return result;
1743 }
1744 // END
1745 
1746 QWidget *KTextEditor::DocumentPrivate::dialogParent()
1747 {
1748     QWidget *w = widget();
1749 
1750     if (!w) {
1751         w = activeView();
1752 
1753         if (!w) {
1754             w = QApplication::activeWindow();
1755         }
1756     }
1757 
1758     return w;
1759 }
1760 
1761 QUrl KTextEditor::DocumentPrivate::getSaveFileUrl(const QString &dialogTitle)
1762 {
1763     // per default we use the url of the current document
1764     QUrl startUrl = url();
1765     if (startUrl.isValid()) {
1766         // for remote files we cut the file name to avoid confusion if it is some directory or not, see bug 454648
1767         if (!startUrl.isLocalFile()) {
1768             startUrl = startUrl.adjusted(QUrl::RemoveFilename);
1769         }
1770     }
1771 
1772     // if that is empty, we will try to get the url of the last used view, we assume some properly ordered views() list is around
1773     else if (auto mainWindow = KTextEditor::Editor::instance()->application()->activeMainWindow(); mainWindow) {
1774         const auto views = mainWindow->views();
1775         for (auto view : views) {
1776             if (view->document()->url().isValid()) {
1777                 // as we here pick some perhaps unrelated file, always cut the file name
1778                 startUrl = view->document()->url().adjusted(QUrl::RemoveFilename);
1779                 break;
1780             }
1781         }
1782     }
1783 
1784     // spawn the dialog, dialogParent will take care of proper parent
1785     return QFileDialog::getSaveFileUrl(dialogParent(), dialogTitle, startUrl);
1786 }
1787 
1788 // BEGIN KTextEditor::HighlightingInterface stuff
1789 bool KTextEditor::DocumentPrivate::setMode(const QString &name)
1790 {
1791     return updateFileType(name);
1792 }
1793 
1794 KSyntaxHighlighting::Theme::TextStyle KTextEditor::DocumentPrivate::defaultStyleAt(KTextEditor::Cursor position) const
1795 {
1796     return const_cast<KTextEditor::DocumentPrivate *>(this)->defStyleNum(position.line(), position.column());
1797 }
1798 
1799 QString KTextEditor::DocumentPrivate::mode() const
1800 {
1801     return m_fileType;
1802 }
1803 
1804 QStringList KTextEditor::DocumentPrivate::modes() const
1805 {
1806     QStringList m;
1807 
1808     const QList<KateFileType *> &modeList = KTextEditor::EditorPrivate::self()->modeManager()->list();
1809     m.reserve(modeList.size());
1810     for (KateFileType *type : modeList) {
1811         m << type->name;
1812     }
1813 
1814     return m;
1815 }
1816 
1817 bool KTextEditor::DocumentPrivate::setHighlightingMode(const QString &name)
1818 {
1819     int mode = KateHlManager::self()->nameFind(name);
1820     if (mode == -1) {
1821         return false;
1822     }
1823     m_buffer->setHighlight(mode);
1824     return true;
1825 }
1826 
1827 QString KTextEditor::DocumentPrivate::highlightingMode() const
1828 {
1829     return highlight()->name();
1830 }
1831 
1832 QStringList KTextEditor::DocumentPrivate::highlightingModes() const
1833 {
1834     const auto modeList = KateHlManager::self()->modeList();
1835     QStringList hls;
1836     hls.reserve(modeList.size());
1837     for (const auto &hl : modeList) {
1838         hls << hl.name();
1839     }
1840     return hls;
1841 }
1842 
1843 QString KTextEditor::DocumentPrivate::highlightingModeSection(int index) const
1844 {
1845     return KateHlManager::self()->modeList().at(index).section();
1846 }
1847 
1848 QString KTextEditor::DocumentPrivate::modeSection(int index) const
1849 {
1850     return KTextEditor::EditorPrivate::self()->modeManager()->list().at(index)->section;
1851 }
1852 
1853 void KTextEditor::DocumentPrivate::bufferHlChanged()
1854 {
1855     // update all views
1856     makeAttribs(false);
1857 
1858     // deactivate indenter if necessary
1859     m_indenter->checkRequiredStyle();
1860 
1861     Q_EMIT highlightingModeChanged(this);
1862 }
1863 
1864 void KTextEditor::DocumentPrivate::setDontChangeHlOnSave()
1865 {
1866     m_hlSetByUser = true;
1867 }
1868 
1869 void KTextEditor::DocumentPrivate::bomSetByUser()
1870 {
1871     m_bomSetByUser = true;
1872 }
1873 // END
1874 
1875 // BEGIN KTextEditor::SessionConfigInterface and KTextEditor::ParameterizedSessionConfigInterface stuff
1876 void KTextEditor::DocumentPrivate::readSessionConfig(const KConfigGroup &kconfig, const QSet<QString> &flags)
1877 {
1878     if (!flags.contains(QStringLiteral("SkipEncoding"))) {
1879         // get the encoding
1880         QString tmpenc = kconfig.readEntry("Encoding");
1881         if (!tmpenc.isEmpty() && (tmpenc != encoding())) {
1882             setEncoding(tmpenc);
1883         }
1884     }
1885 
1886     if (!flags.contains(QStringLiteral("SkipUrl"))) {
1887         // restore the url
1888         QUrl url(kconfig.readEntry("URL"));
1889 
1890         // open the file if url valid
1891         if (!url.isEmpty() && url.isValid()) {
1892             openUrl(url);
1893         } else {
1894             completed(); // perhaps this should be emitted at the end of this function
1895         }
1896     } else {
1897         completed(); // perhaps this should be emitted at the end of this function
1898     }
1899 
1900     if (!flags.contains(QStringLiteral("SkipMode"))) {
1901         // restore the filetype
1902         // NOTE: if the session config file contains an invalid Mode
1903         // (for example, one that was deleted or renamed), do not apply it
1904         if (kconfig.hasKey("Mode Set By User")) {
1905             // restore if set by user, too!
1906             m_fileTypeSetByUser = true;
1907             updateFileType(kconfig.readEntry("Mode"));
1908         }
1909     }
1910 
1911     if (!flags.contains(QStringLiteral("SkipHighlighting"))) {
1912         // restore the hl stuff
1913         if (kconfig.hasKey("Highlighting Set By User")) {
1914             const int mode = KateHlManager::self()->nameFind(kconfig.readEntry("Highlighting"));
1915             m_hlSetByUser = true;
1916             if (mode >= 0) {
1917                 // restore if set by user, too! see bug 332605, otherwise we loose the hl later again on save
1918                 m_buffer->setHighlight(mode);
1919             }
1920         }
1921     }
1922 
1923     // indent mode
1924     const QString userSetIndentMode = kconfig.readEntry("Indentation Mode");
1925     if (!userSetIndentMode.isEmpty()) {
1926         config()->setIndentationMode(userSetIndentMode);
1927     }
1928 
1929     // Restore Bookmarks
1930     const QList<int> marks = kconfig.readEntry("Bookmarks", QList<int>());
1931     for (int i = 0; i < marks.count(); i++) {
1932         addMark(marks.at(i), KTextEditor::DocumentPrivate::markType01);
1933     }
1934 }
1935 
1936 void KTextEditor::DocumentPrivate::writeSessionConfig(KConfigGroup &kconfig, const QSet<QString> &flags)
1937 {
1938     if (this->url().isLocalFile()) {
1939         const QString path = this->url().toLocalFile();
1940         if (path.startsWith(QDir::tempPath())) {
1941             return; // inside tmp resource, do not save
1942         }
1943     }
1944 
1945     if (!flags.contains(QStringLiteral("SkipUrl"))) {
1946         // save url
1947         kconfig.writeEntry("URL", this->url().toString());
1948     }
1949 
1950     // only save encoding if it's something other than utf-8
1951     if (encoding() != QLatin1String("UTF-8") && !flags.contains(QStringLiteral("SkipEncoding"))) {
1952         // save encoding
1953         kconfig.writeEntry("Encoding", encoding());
1954     }
1955 
1956     if (m_fileTypeSetByUser && !flags.contains(QStringLiteral("SkipMode"))) {
1957         // save file type
1958         kconfig.writeEntry("Mode", m_fileType);
1959         // save if set by user, too!
1960         kconfig.writeEntry("Mode Set By User", m_fileTypeSetByUser);
1961     }
1962 
1963     if (m_hlSetByUser && !flags.contains(QStringLiteral("SkipHighlighting"))) {
1964         // save hl
1965         kconfig.writeEntry("Highlighting", highlight()->name());
1966 
1967         // save if set by user, too! see bug 332605, otherwise we loose the hl later again on save
1968         kconfig.writeEntry("Highlighting Set By User", m_hlSetByUser);
1969     }
1970 
1971     // indent mode
1972     if (m_indenterSetByUser) {
1973         kconfig.writeEntry("Indentation Mode", config()->indentationMode());
1974     }
1975 
1976     // Save Bookmarks
1977     QList<int> marks;
1978     for (const auto &mark : std::as_const(m_marks)) {
1979         if (mark->type & KTextEditor::Document::markType01) {
1980             marks.push_back(mark->line);
1981         }
1982     }
1983 
1984     if (!marks.isEmpty()) {
1985         kconfig.writeEntry("Bookmarks", marks);
1986     }
1987 }
1988 
1989 // END KTextEditor::SessionConfigInterface and KTextEditor::ParameterizedSessionConfigInterface stuff
1990 
1991 uint KTextEditor::DocumentPrivate::mark(int line)
1992 {
1993     KTextEditor::Mark *m = m_marks.value(line);
1994     if (!m) {
1995         return 0;
1996     }
1997 
1998     return m->type;
1999 }
2000 
2001 void KTextEditor::DocumentPrivate::setMark(int line, uint markType)
2002 {
2003     clearMark(line);
2004     addMark(line, markType);
2005 }
2006 
2007 void KTextEditor::DocumentPrivate::clearMark(int line)
2008 {
2009     if (line < 0 || line > lastLine()) {
2010         return;
2011     }
2012 
2013     if (auto mark = m_marks.take(line)) {
2014         Q_EMIT markChanged(this, *mark, MarkRemoved);
2015         Q_EMIT marksChanged(this);
2016         delete mark;
2017         tagLine(line);
2018         repaintViews(true);
2019     }
2020 }
2021 
2022 void KTextEditor::DocumentPrivate::addMark(int line, uint markType)
2023 {
2024     KTextEditor::Mark *mark;
2025 
2026     if (line < 0 || line > lastLine()) {
2027         return;
2028     }
2029 
2030     if (markType == 0) {
2031         return;
2032     }
2033 
2034     if ((mark = m_marks.value(line))) {
2035         // Remove bits already set
2036         markType &= ~mark->type;
2037 
2038         if (markType == 0) {
2039             return;
2040         }
2041 
2042         // Add bits
2043         mark->type |= markType;
2044     } else {
2045         mark = new KTextEditor::Mark;
2046         mark->line = line;
2047         mark->type = markType;
2048         m_marks.insert(line, mark);
2049     }
2050 
2051     // Emit with a mark having only the types added.
2052     KTextEditor::Mark temp;
2053     temp.line = line;
2054     temp.type = markType;
2055     Q_EMIT markChanged(this, temp, MarkAdded);
2056 
2057     Q_EMIT marksChanged(this);
2058     tagLine(line);
2059     repaintViews(true);
2060 }
2061 
2062 void KTextEditor::DocumentPrivate::removeMark(int line, uint markType)
2063 {
2064     if (line < 0 || line > lastLine()) {
2065         return;
2066     }
2067 
2068     auto it = m_marks.find(line);
2069     if (it == m_marks.end()) {
2070         return;
2071     }
2072     KTextEditor::Mark *mark = it.value();
2073 
2074     // Remove bits not set
2075     markType &= mark->type;
2076 
2077     if (markType == 0) {
2078         return;
2079     }
2080 
2081     // Subtract bits
2082     mark->type &= ~markType;
2083 
2084     // Emit with a mark having only the types removed.
2085     KTextEditor::Mark temp;
2086     temp.line = line;
2087     temp.type = markType;
2088     Q_EMIT markChanged(this, temp, MarkRemoved);
2089 
2090     if (mark->type == 0) {
2091         m_marks.erase(it);
2092         delete mark;
2093     }
2094 
2095     Q_EMIT marksChanged(this);
2096     tagLine(line);
2097     repaintViews(true);
2098 }
2099 
2100 const QHash<int, KTextEditor::Mark *> &KTextEditor::DocumentPrivate::marks()
2101 {
2102     return m_marks;
2103 }
2104 
2105 void KTextEditor::DocumentPrivate::requestMarkTooltip(int line, QPoint position)
2106 {
2107     KTextEditor::Mark *mark = m_marks.value(line);
2108     if (!mark) {
2109         return;
2110     }
2111 
2112     bool handled = false;
2113     Q_EMIT markToolTipRequested(this, *mark, position, handled);
2114 }
2115 
2116 bool KTextEditor::DocumentPrivate::handleMarkClick(int line)
2117 {
2118     bool handled = false;
2119     KTextEditor::Mark *mark = m_marks.value(line);
2120     if (!mark) {
2121         Q_EMIT markClicked(this, KTextEditor::Mark{line, 0}, handled);
2122     } else {
2123         Q_EMIT markClicked(this, *mark, handled);
2124     }
2125 
2126     return handled;
2127 }
2128 
2129 bool KTextEditor::DocumentPrivate::handleMarkContextMenu(int line, QPoint position)
2130 {
2131     bool handled = false;
2132     KTextEditor::Mark *mark = m_marks.value(line);
2133     if (!mark) {
2134         Q_EMIT markContextMenuRequested(this, KTextEditor::Mark{line, 0}, position, handled);
2135     } else {
2136         Q_EMIT markContextMenuRequested(this, *mark, position, handled);
2137     }
2138 
2139     return handled;
2140 }
2141 
2142 void KTextEditor::DocumentPrivate::clearMarks()
2143 {
2144     /**
2145      * work on a copy as deletions below might trigger the use
2146      * of m_marks
2147      */
2148     const QHash<int, KTextEditor::Mark *> marksCopy = m_marks;
2149     m_marks.clear();
2150 
2151     for (const auto &m : marksCopy) {
2152         Q_EMIT markChanged(this, *m, MarkRemoved);
2153         tagLine(m->line);
2154         delete m;
2155     }
2156 
2157     Q_EMIT marksChanged(this);
2158     repaintViews(true);
2159 }
2160 
2161 void KTextEditor::DocumentPrivate::setMarkDescription(Document::MarkTypes type, const QString &description)
2162 {
2163     m_markDescriptions.insert(type, description);
2164 }
2165 
2166 QColor KTextEditor::DocumentPrivate::markColor(Document::MarkTypes type) const
2167 {
2168     uint reserved = (1U << KTextEditor::Document::reservedMarkersCount()) - 1;
2169     if ((uint)type >= (uint)markType01 && (uint)type <= reserved) {
2170         return KateRendererConfig::global()->lineMarkerColor(type);
2171     } else {
2172         return QColor();
2173     }
2174 }
2175 
2176 QString KTextEditor::DocumentPrivate::markDescription(Document::MarkTypes type) const
2177 {
2178     return m_markDescriptions.value(type, QString());
2179 }
2180 
2181 void KTextEditor::DocumentPrivate::setEditableMarks(uint markMask)
2182 {
2183     m_editableMarks = markMask;
2184 }
2185 
2186 uint KTextEditor::DocumentPrivate::editableMarks() const
2187 {
2188     return m_editableMarks;
2189 }
2190 // END
2191 
2192 void KTextEditor::DocumentPrivate::setMarkIcon(Document::MarkTypes markType, const QIcon &icon)
2193 {
2194     m_markIcons.insert(markType, icon);
2195 }
2196 
2197 QIcon KTextEditor::DocumentPrivate::markIcon(Document::MarkTypes markType) const
2198 {
2199     return m_markIcons.value(markType, QIcon());
2200 }
2201 
2202 // BEGIN KTextEditor::PrintInterface stuff
2203 bool KTextEditor::DocumentPrivate::print()
2204 {
2205     return KatePrinter::print(this);
2206 }
2207 
2208 void KTextEditor::DocumentPrivate::printPreview()
2209 {
2210     KatePrinter::printPreview(this);
2211 }
2212 // END KTextEditor::PrintInterface stuff
2213 
2214 // BEGIN KTextEditor::DocumentInfoInterface (### unfinished)
2215 QString KTextEditor::DocumentPrivate::mimeType()
2216 {
2217     if (!m_modOnHd && url().isLocalFile()) {
2218         // for unmodified files that reside directly on disk, we don't need to
2219         // create a temporary buffer - we can just look at the file directly
2220         return QMimeDatabase().mimeTypeForFile(url().toLocalFile()).name();
2221     }
2222     // collect first 4k of text
2223     // only heuristic
2224     QByteArray buf;
2225     for (int i = 0; (i < lines()) && (buf.size() <= 4096); ++i) {
2226         buf.append(line(i).toUtf8());
2227         buf.append('\n');
2228     }
2229 
2230     // use path of url, too, if set
2231     if (!url().path().isEmpty()) {
2232         return QMimeDatabase().mimeTypeForFileNameAndData(url().path(), buf).name();
2233     }
2234 
2235     // else only use the content
2236     return QMimeDatabase().mimeTypeForData(buf).name();
2237 }
2238 // END KTextEditor::DocumentInfoInterface
2239 
2240 // BEGIN: error
2241 void KTextEditor::DocumentPrivate::showAndSetOpeningErrorAccess()
2242 {
2243     QPointer<KTextEditor::Message> message = new KTextEditor::Message(
2244         i18n("The file %1 could not be loaded, as it was not possible to read from it.<br />Check if you have read access to this file.",
2245              this->url().toDisplayString(QUrl::PreferLocalFile)),
2246         KTextEditor::Message::Error);
2247     message->setWordWrap(true);
2248     QAction *tryAgainAction = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")),
2249                                           i18nc("translators: you can also translate 'Try Again' with 'Reload'", "Try Again"),
2250                                           nullptr);
2251     connect(tryAgainAction, &QAction::triggered, this, &KTextEditor::DocumentPrivate::documentReload, Qt::QueuedConnection);
2252 
2253     QAction *closeAction = new QAction(QIcon::fromTheme(QStringLiteral("window-close")), i18n("&Close"), nullptr);
2254     closeAction->setToolTip(i18nc("Close the message being displayed", "Close message"));
2255 
2256     // add try again and close actions
2257     message->addAction(tryAgainAction);
2258     message->addAction(closeAction);
2259 
2260     // finally post message
2261     postMessage(message);
2262 
2263     // remember error
2264     m_openingError = true;
2265 }
2266 // END: error
2267 
2268 void KTextEditor::DocumentPrivate::openWithLineLengthLimitOverride()
2269 {
2270     // raise line length limit to the next power of 2
2271     const int longestLine = m_buffer->longestLineLoaded();
2272     int newLimit = pow(2, ceil(log2(longestLine)));
2273     if (newLimit <= longestLine) {
2274         newLimit *= 2;
2275     }
2276 
2277     // do the raise
2278     config()->setLineLengthLimit(newLimit);
2279 
2280     // just reload
2281     m_buffer->clear();
2282     openFile();
2283     if (!m_openingError) {
2284         setReadWrite(true);
2285         m_readWriteStateBeforeLoading = true;
2286     }
2287 }
2288 
2289 int KTextEditor::DocumentPrivate::lineLengthLimit() const
2290 {
2291     return config()->lineLengthLimit();
2292 }
2293 
2294 // BEGIN KParts::ReadWrite stuff
2295 bool KTextEditor::DocumentPrivate::openFile()
2296 {
2297     // we are about to invalidate all cursors/ranges/.. => m_buffer->openFile will do so
2298     Q_EMIT aboutToInvalidateMovingInterfaceContent(this);
2299 
2300     // no open errors until now...
2301     m_openingError = false;
2302 
2303     // add new m_file to dirwatch
2304     activateDirWatch();
2305 
2306     // remember current encoding
2307     QString currentEncoding = encoding();
2308 
2309     //
2310     // mime type magic to get encoding right
2311     //
2312     QString mimeType = arguments().mimeType();
2313     int pos = mimeType.indexOf(QLatin1Char(';'));
2314     if (pos != -1 && !(m_reloading && m_userSetEncodingForNextReload)) {
2315         setEncoding(mimeType.mid(pos + 1));
2316     }
2317 
2318     // update file type, we do this here PRE-LOAD, therefore pass file name for reading from
2319     updateFileType(KTextEditor::EditorPrivate::self()->modeManager()->fileType(this, localFilePath()));
2320 
2321     // read dir config (if possible and wanted)
2322     // do this PRE-LOAD to get encoding info!
2323     readDirConfig();
2324 
2325     // perhaps we need to re-set again the user encoding
2326     if (m_reloading && m_userSetEncodingForNextReload && (currentEncoding != encoding())) {
2327         setEncoding(currentEncoding);
2328     }
2329 
2330     bool success = m_buffer->openFile(localFilePath(), (m_reloading && m_userSetEncodingForNextReload));
2331 
2332     //
2333     // yeah, success
2334     // read variables
2335     //
2336     if (success) {
2337         readVariables();
2338     }
2339 
2340     //
2341     // update views
2342     //
2343     for (auto view : std::as_const(m_views)) {
2344         // This is needed here because inserting the text moves the view's start position (it is a MovingCursor)
2345         view->setCursorPosition(KTextEditor::Cursor());
2346         static_cast<ViewPrivate *>(view)->updateView(true);
2347     }
2348 
2349     // Inform that the text has changed (required as we're not inside the usual editStart/End stuff)
2350     Q_EMIT textChanged(this);
2351     Q_EMIT loaded(this);
2352 
2353     //
2354     // to houston, we are not modified
2355     //
2356     if (m_modOnHd) {
2357         m_modOnHd = false;
2358         m_modOnHdReason = OnDiskUnmodified;
2359         m_prevModOnHdReason = OnDiskUnmodified;
2360         Q_EMIT modifiedOnDisk(this, m_modOnHd, m_modOnHdReason);
2361     }
2362 
2363     // Now that we have some text, try to auto detect indent if enabled
2364     // skip this if for this document already settings were done, either by the user or .e.g. modelines/.kateconfig files.
2365     if (!isEmpty() && config()->autoDetectIndent() && !config()->isSet(KateDocumentConfig::IndentationWidth)
2366         && !config()->isSet(KateDocumentConfig::ReplaceTabsWithSpaces)) {
2367         KateIndentDetecter detecter(this);
2368         auto result = detecter.detect(config()->indentationWidth(), config()->replaceTabsDyn());
2369         config()->setIndentationWidth(result.indentWidth);
2370         config()->setReplaceTabsDyn(result.indentUsingSpaces);
2371     }
2372 
2373     //
2374     // display errors
2375     //
2376     if (!success) {
2377         showAndSetOpeningErrorAccess();
2378     }
2379 
2380     // warn: broken encoding
2381     if (m_buffer->brokenEncoding()) {
2382         // this file can't be saved again without killing it
2383         setReadWrite(false);
2384         m_readWriteStateBeforeLoading = false;
2385         QPointer<KTextEditor::Message> message = new KTextEditor::Message(
2386             i18n("The file %1 was opened with %2 encoding but contained invalid characters.<br />"
2387                  "It is set to read-only mode, as saving might destroy its content.<br />"
2388                  "Either reopen the file with the correct encoding chosen or enable the read-write mode again in the tools menu to be able to edit it.",
2389                  this->url().toDisplayString(QUrl::PreferLocalFile),
2390                  m_buffer->textCodec()),
2391             KTextEditor::Message::Warning);
2392         message->setWordWrap(true);
2393         postMessage(message);
2394 
2395         // remember error
2396         m_openingError = true;
2397     }
2398 
2399     // warn: too long lines
2400     if (m_buffer->tooLongLinesWrapped()) {
2401         // this file can't be saved again without modifications
2402         setReadWrite(false);
2403         m_readWriteStateBeforeLoading = false;
2404         QPointer<KTextEditor::Message> message =
2405             new KTextEditor::Message(i18n("The file %1 was opened and contained lines longer than the configured Line Length Limit (%2 characters).<br />"
2406                                           "The longest of those lines was %3 characters long<br/>"
2407                                           "Those lines were wrapped and the document is set to read-only mode, as saving will modify its content.",
2408                                           this->url().toDisplayString(QUrl::PreferLocalFile),
2409                                           config()->lineLengthLimit(),
2410                                           m_buffer->longestLineLoaded()),
2411                                      KTextEditor::Message::Warning);
2412         QAction *increaseAndReload = new QAction(i18n("Temporarily raise limit and reload file"), message);
2413         connect(increaseAndReload, &QAction::triggered, this, &KTextEditor::DocumentPrivate::openWithLineLengthLimitOverride);
2414         message->addAction(increaseAndReload, true);
2415         message->addAction(new QAction(i18n("Close"), message), true);
2416         message->setWordWrap(true);
2417         postMessage(message);
2418 
2419         // remember error
2420         m_openingError = true;
2421     }
2422 
2423     //
2424     // return the success
2425     //
2426     return success;
2427 }
2428 
2429 bool KTextEditor::DocumentPrivate::saveFile()
2430 {
2431     // delete pending mod-on-hd message if applicable.
2432     delete m_modOnHdHandler;
2433 
2434     // some warnings, if file was changed by the outside!
2435     if (!url().isEmpty()) {
2436         if (m_fileChangedDialogsActivated && m_modOnHd) {
2437             QString str = reasonedMOHString() + QLatin1String("\n\n");
2438 
2439             if (!isModified()) {
2440                 if (KMessageBox::warningContinueCancel(
2441                         dialogParent(),
2442                         str + i18n("Do you really want to save this unmodified file? You could overwrite changed data in the file on disk."),
2443                         i18n("Trying to Save Unmodified File"),
2444                         KGuiItem(i18n("Save Nevertheless")))
2445                     != KMessageBox::Continue) {
2446                     return false;
2447                 }
2448             } else {
2449                 if (KMessageBox::warningContinueCancel(
2450                         dialogParent(),
2451                         str
2452                             + i18n(
2453                                 "Do you really want to save this file? Both your open file and the file on disk were changed. There could be some data lost."),
2454                         i18n("Possible Data Loss"),
2455                         KGuiItem(i18n("Save Nevertheless")))
2456                     != KMessageBox::Continue) {
2457                     return false;
2458                 }
2459             }
2460         }
2461     }
2462 
2463     //
2464     // can we encode it if we want to save it ?
2465     //
2466     if (!m_buffer->canEncode()
2467         && (KMessageBox::warningContinueCancel(dialogParent(),
2468                                                i18n("The selected encoding cannot encode every Unicode character in this document. Do you really want to save "
2469                                                     "it? There could be some data lost."),
2470                                                i18n("Possible Data Loss"),
2471                                                KGuiItem(i18n("Save Nevertheless")))
2472             != KMessageBox::Continue)) {
2473         return false;
2474     }
2475 
2476     // create a backup file or abort if that fails!
2477     // if no backup file wanted, this routine will just return true
2478     if (!createBackupFile()) {
2479         return false;
2480     }
2481 
2482     // update file type, pass no file path, read file type content from this document
2483     QString oldPath = m_dirWatchFile;
2484 
2485     // only update file type if path has changed so that variables are not overridden on normal save
2486     if (oldPath != localFilePath()) {
2487         updateFileType(KTextEditor::EditorPrivate::self()->modeManager()->fileType(this, QString()));
2488 
2489         if (url().isLocalFile()) {
2490             // if file is local then read dir config for new path
2491             readDirConfig();
2492         }
2493     }
2494 
2495     // read our vars
2496     const bool variablesWereRead = readVariables();
2497 
2498     // If variables were read, that means we must have updated view and render config
2499     // which would update the full view and we don't need to do any repainting. Otherwise
2500     // loop over all views and update the views if the view has modified lines in the visible
2501     // range, this should mark the line 'green' in the icon border
2502     if (!variablesWereRead) {
2503         for (auto *view : std::as_const(m_views)) {
2504             auto v = static_cast<ViewPrivate *>(view);
2505             if (v->isVisible()) {
2506                 const auto range = v->visibleRange();
2507 
2508                 bool repaint = false;
2509                 for (int i = range.start().line(); i <= range.end().line(); ++i) {
2510                     if (isLineModified(i)) {
2511                         repaint = true;
2512                         v->tagLine({i, 0});
2513                     }
2514                 }
2515 
2516                 if (repaint) {
2517                     v->updateView(true);
2518                 }
2519             }
2520         }
2521     }
2522 
2523     // remove file from dirwatch
2524     deactivateDirWatch();
2525 
2526     // remove all trailing spaces in the document and potential add a new line (as edit actions)
2527     // NOTE: we need this as edit actions, since otherwise the edit actions
2528     //       in the swap file recovery may happen at invalid cursor positions
2529     removeTrailingSpacesAndAddNewLineAtEof();
2530 
2531     //
2532     // try to save
2533     //
2534     if (!m_buffer->saveFile(localFilePath())) {
2535         // add m_file again to dirwatch
2536         activateDirWatch(oldPath);
2537         KMessageBox::error(dialogParent(),
2538                            i18n("The document could not be saved, as it was not possible to write to %1.\nCheck that you have write access to this file or "
2539                                 "that enough disk space is available.\nThe original file may be lost or damaged. "
2540                                 "Don't quit the application until the file is successfully written.",
2541                                 this->url().toDisplayString(QUrl::PreferLocalFile)));
2542         return false;
2543     }
2544 
2545     // update the checksum
2546     createDigest();
2547 
2548     // add m_file again to dirwatch
2549     activateDirWatch();
2550 
2551     //
2552     // we are not modified
2553     //
2554     if (m_modOnHd) {
2555         m_modOnHd = false;
2556         m_modOnHdReason = OnDiskUnmodified;
2557         m_prevModOnHdReason = OnDiskUnmodified;
2558         Q_EMIT modifiedOnDisk(this, m_modOnHd, m_modOnHdReason);
2559     }
2560 
2561     // (dominik) mark last undo group as not mergeable, otherwise the next
2562     // edit action might be merged and undo will never stop at the saved state
2563     m_undoManager->undoSafePoint();
2564     m_undoManager->updateLineModifications();
2565 
2566     //
2567     // return success
2568     //
2569     return true;
2570 }
2571 
2572 bool KTextEditor::DocumentPrivate::createBackupFile()
2573 {
2574     // backup for local or remote files wanted?
2575     const bool backupLocalFiles = config()->backupOnSaveLocal();
2576     const bool backupRemoteFiles = config()->backupOnSaveRemote();
2577 
2578     // early out, before mount check: backup wanted at all?
2579     // => if not, all fine, just return
2580     if (!backupLocalFiles && !backupRemoteFiles) {
2581         return true;
2582     }
2583 
2584     // decide if we need backup based on locality
2585     // skip that, if we always want backups, as currentMountPoints is not that fast
2586     QUrl u(url());
2587     bool needBackup = backupLocalFiles && backupRemoteFiles;
2588     if (!needBackup) {
2589         bool slowOrRemoteFile = !u.isLocalFile();
2590         if (!slowOrRemoteFile) {
2591             // could be a mounted remote filesystem (e.g. nfs, sshfs, cifs)
2592             // we have the early out above to skip this, if we want no backup, which is the default
2593             KMountPoint::Ptr mountPoint = KMountPoint::currentMountPoints().findByDevice(u.toLocalFile());
2594             slowOrRemoteFile = (mountPoint && mountPoint->probablySlow());
2595         }
2596         needBackup = (!slowOrRemoteFile && backupLocalFiles) || (slowOrRemoteFile && backupRemoteFiles);
2597     }
2598 
2599     // no backup needed? be done
2600     if (!needBackup) {
2601         return true;
2602     }
2603 
2604     // else: try to backup
2605     const auto backupPrefix = KTextEditor::EditorPrivate::self()->variableExpansionManager()->expandText(config()->backupPrefix(), nullptr);
2606     const auto backupSuffix = KTextEditor::EditorPrivate::self()->variableExpansionManager()->expandText(config()->backupSuffix(), nullptr);
2607     if (backupPrefix.isEmpty() && backupSuffix.isEmpty()) {
2608         // no sane backup possible
2609         return true;
2610     }
2611 
2612     if (backupPrefix.contains(QDir::separator())) {
2613         // replace complete path, as prefix is a path!
2614         u.setPath(backupPrefix + u.fileName() + backupSuffix);
2615     } else {
2616         // replace filename in url
2617         const QString fileName = u.fileName();
2618         u = u.adjusted(QUrl::RemoveFilename);
2619         u.setPath(u.path() + backupPrefix + fileName + backupSuffix);
2620     }
2621 
2622     qCDebug(LOG_KTE) << "backup src file name: " << url();
2623     qCDebug(LOG_KTE) << "backup dst file name: " << u;
2624 
2625     // handle the backup...
2626     bool backupSuccess = false;
2627 
2628     // local file mode, no kio
2629     if (u.isLocalFile()) {
2630         if (QFile::exists(url().toLocalFile())) {
2631             // first: check if backupFile is already there, if true, unlink it
2632             QFile backupFile(u.toLocalFile());
2633             if (backupFile.exists()) {
2634                 backupFile.remove();
2635             }
2636 
2637             backupSuccess = QFile::copy(url().toLocalFile(), u.toLocalFile());
2638         } else {
2639             backupSuccess = true;
2640         }
2641     } else { // remote file mode, kio
2642         // get the right permissions, start with safe default
2643         KIO::StatJob *statJob = KIO::stat(url(), KIO::StatJob::SourceSide, KIO::StatBasic);
2644         KJobWidgets::setWindow(statJob, QApplication::activeWindow());
2645         if (statJob->exec()) {
2646             // do a evil copy which will overwrite target if possible
2647             KFileItem item(statJob->statResult(), url());
2648             KIO::FileCopyJob *job = KIO::file_copy(url(), u, item.permissions(), KIO::Overwrite);
2649             KJobWidgets::setWindow(job, QApplication::activeWindow());
2650             backupSuccess = job->exec();
2651         } else {
2652             backupSuccess = true;
2653         }
2654     }
2655 
2656     // backup has failed, ask user how to proceed
2657     if (!backupSuccess
2658         && (KMessageBox::warningContinueCancel(dialogParent(),
2659                                                i18n("For file %1 no backup copy could be created before saving."
2660                                                     " If an error occurs while saving, you might lose the data of this file."
2661                                                     " A reason could be that the media you write to is full or the directory of the file is read-only for you.",
2662                                                     url().toDisplayString(QUrl::PreferLocalFile)),
2663                                                i18n("Failed to create backup copy."),
2664                                                KGuiItem(i18n("Try to Save Nevertheless")),
2665                                                KStandardGuiItem::cancel(),
2666                                                QStringLiteral("Backup Failed Warning"))
2667             != KMessageBox::Continue)) {
2668         return false;
2669     }
2670 
2671     return true;
2672 }
2673 
2674 void KTextEditor::DocumentPrivate::readDirConfig()
2675 {
2676     if (!url().isLocalFile() || KNetworkMounts::self()->isOptionEnabledForPath(url().toLocalFile(), KNetworkMounts::MediumSideEffectsOptimizations)) {
2677         return;
2678     }
2679 
2680     // first search .kateconfig upwards
2681     // with recursion guard
2682     QSet<QString> seenDirectories;
2683     QDir dir(QFileInfo(localFilePath()).absolutePath());
2684     while (!seenDirectories.contains(dir.absolutePath())) {
2685         // fill recursion guard
2686         seenDirectories.insert(dir.absolutePath());
2687 
2688         // try to open config file in this dir
2689         QFile f(dir.absolutePath() + QLatin1String("/.kateconfig"));
2690         if (f.open(QIODevice::ReadOnly)) {
2691             QTextStream stream(&f);
2692 
2693             uint linesRead = 0;
2694             QString line = stream.readLine();
2695             while ((linesRead < 32) && !line.isNull()) {
2696                 readVariableLine(line);
2697 
2698                 line = stream.readLine();
2699 
2700                 linesRead++;
2701             }
2702 
2703             return;
2704         }
2705 
2706         // else: cd up, if possible or abort
2707         if (!dir.cdUp()) {
2708             break;
2709         }
2710     }
2711 
2712 #if EDITORCONFIG_FOUND
2713     // if there wasn’t any .kateconfig file and KTextEditor was compiled with
2714     // EditorConfig support, try to load document config from a .editorconfig
2715     // file, if such is provided
2716     EditorConfig editorConfig(this);
2717     editorConfig.parse();
2718 #endif
2719 }
2720 
2721 void KTextEditor::DocumentPrivate::activateDirWatch(const QString &useFileName)
2722 {
2723     QString fileToUse = useFileName;
2724     if (fileToUse.isEmpty()) {
2725         fileToUse = localFilePath();
2726     }
2727 
2728     if (KNetworkMounts::self()->isOptionEnabledForPath(fileToUse, KNetworkMounts::KDirWatchDontAddWatches)) {
2729         return;
2730     }
2731 
2732     QFileInfo fileInfo = QFileInfo(fileToUse);
2733     if (fileInfo.isSymLink()) {
2734         // Monitor the actual data and not the symlink
2735         fileToUse = fileInfo.canonicalFilePath();
2736     }
2737 
2738     // same file as we are monitoring, return
2739     if (fileToUse == m_dirWatchFile) {
2740         return;
2741     }
2742 
2743     // remove the old watched file
2744     deactivateDirWatch();
2745 
2746     // add new file if needed
2747     if (url().isLocalFile() && !fileToUse.isEmpty()) {
2748         KTextEditor::EditorPrivate::self()->dirWatch()->addFile(fileToUse);
2749         m_dirWatchFile = fileToUse;
2750     }
2751 }
2752 
2753 void KTextEditor::DocumentPrivate::deactivateDirWatch()
2754 {
2755     if (!m_dirWatchFile.isEmpty()) {
2756         KTextEditor::EditorPrivate::self()->dirWatch()->removeFile(m_dirWatchFile);
2757     }
2758 
2759     m_dirWatchFile.clear();
2760 }
2761 
2762 bool KTextEditor::DocumentPrivate::openUrl(const QUrl &url)
2763 {
2764     if (!m_reloading) {
2765         // Reset filetype when opening url
2766         m_fileTypeSetByUser = false;
2767     }
2768     bool res = KTextEditor::Document::openUrl(url);
2769     updateDocName();
2770     return res;
2771 }
2772 
2773 bool KTextEditor::DocumentPrivate::closeUrl()
2774 {
2775     //
2776     // file mod on hd
2777     //
2778     if (!m_reloading && !url().isEmpty()) {
2779         if (m_fileChangedDialogsActivated && m_modOnHd) {
2780             // make sure to not forget pending mod-on-hd handler
2781             delete m_modOnHdHandler;
2782 
2783             QWidget *parentWidget(dialogParent());
2784             if (!(KMessageBox::warningContinueCancel(parentWidget,
2785                                                      reasonedMOHString() + QLatin1String("\n\n")
2786                                                          + i18n("Do you really want to continue to close this file? Data loss may occur."),
2787                                                      i18n("Possible Data Loss"),
2788                                                      KGuiItem(i18n("Close Nevertheless")),
2789                                                      KStandardGuiItem::cancel(),
2790                                                      QStringLiteral("kate_close_modonhd_%1").arg(m_modOnHdReason))
2791                   == KMessageBox::Continue)) {
2792                 // reset reloading
2793                 m_reloading = false;
2794                 return false;
2795             }
2796         }
2797     }
2798 
2799     //
2800     // first call the normal kparts implementation
2801     //
2802     if (!KParts::ReadWritePart::closeUrl()) {
2803         // reset reloading
2804         m_reloading = false;
2805         return false;
2806     }
2807 
2808     // Tell the world that we're about to go ahead with the close
2809     if (!m_reloading) {
2810         Q_EMIT aboutToClose(this);
2811     }
2812 
2813     // delete all KTE::Messages
2814     if (!m_messageHash.isEmpty()) {
2815         const auto keys = m_messageHash.keys();
2816         for (KTextEditor::Message *message : keys) {
2817             delete message;
2818         }
2819     }
2820 
2821     // we are about to invalidate all cursors/ranges/.. => m_buffer->clear will do so
2822     Q_EMIT aboutToInvalidateMovingInterfaceContent(this);
2823 
2824     // remove file from dirwatch
2825     deactivateDirWatch();
2826 
2827     //
2828     // empty url + fileName
2829     //
2830     setUrl(QUrl());
2831     setLocalFilePath(QString());
2832 
2833     // we are not modified
2834     if (m_modOnHd) {
2835         m_modOnHd = false;
2836         m_modOnHdReason = OnDiskUnmodified;
2837         m_prevModOnHdReason = OnDiskUnmodified;
2838         Q_EMIT modifiedOnDisk(this, m_modOnHd, m_modOnHdReason);
2839     }
2840 
2841     // remove all marks
2842     clearMarks();
2843 
2844     // clear the buffer
2845     m_buffer->clear();
2846 
2847     // clear undo/redo history
2848     m_undoManager->clearUndo();
2849     m_undoManager->clearRedo();
2850 
2851     // no, we are no longer modified
2852     setModified(false);
2853 
2854     // we have no longer any hl
2855     m_buffer->setHighlight(0);
2856 
2857     // update all our views
2858     for (auto view : std::as_const(m_views)) {
2859         static_cast<ViewPrivate *>(view)->clearSelection(); // fix bug #118588
2860         static_cast<ViewPrivate *>(view)->clear();
2861     }
2862 
2863     // purge swap file
2864     if (m_swapfile) {
2865         m_swapfile->fileClosed();
2866     }
2867 
2868     // success
2869     return true;
2870 }
2871 
2872 bool KTextEditor::DocumentPrivate::isDataRecoveryAvailable() const
2873 {
2874     return m_swapfile && m_swapfile->shouldRecover();
2875 }
2876 
2877 void KTextEditor::DocumentPrivate::recoverData()
2878 {
2879     if (isDataRecoveryAvailable()) {
2880         m_swapfile->recover();
2881     }
2882 }
2883 
2884 void KTextEditor::DocumentPrivate::discardDataRecovery()
2885 {
2886     if (isDataRecoveryAvailable()) {
2887         m_swapfile->discard();
2888     }
2889 }
2890 
2891 void KTextEditor::DocumentPrivate::setReadWrite(bool rw)
2892 {
2893     if (isReadWrite() == rw) {
2894         return;
2895     }
2896 
2897     KParts::ReadWritePart::setReadWrite(rw);
2898 
2899     for (auto v : std::as_const(m_views)) {
2900         auto view = static_cast<ViewPrivate *>(v);
2901         view->slotUpdateUndo();
2902         view->slotReadWriteChanged();
2903     }
2904 
2905     Q_EMIT readWriteChanged(this);
2906 }
2907 
2908 void KTextEditor::DocumentPrivate::setModified(bool m)
2909 {
2910     if (isModified() != m) {
2911         KParts::ReadWritePart::setModified(m);
2912 
2913         for (auto view : std::as_const(m_views)) {
2914             static_cast<ViewPrivate *>(view)->slotUpdateUndo();
2915         }
2916 
2917         Q_EMIT modifiedChanged(this);
2918     }
2919 
2920     m_undoManager->setModified(m);
2921 }
2922 // END
2923 
2924 // BEGIN Kate specific stuff ;)
2925 
2926 void KTextEditor::DocumentPrivate::makeAttribs(bool needInvalidate)
2927 {
2928     for (auto view : std::as_const(m_views)) {
2929         static_cast<ViewPrivate *>(view)->renderer()->updateAttributes();
2930     }
2931 
2932     if (needInvalidate) {
2933         m_buffer->invalidateHighlighting();
2934     }
2935 
2936     for (auto v : std::as_const(m_views)) {
2937         auto view = static_cast<ViewPrivate *>(v);
2938         view->tagAll();
2939         view->updateView(true);
2940     }
2941 }
2942 
2943 // the attributes of a hl have changed, update
2944 void KTextEditor::DocumentPrivate::internalHlChanged()
2945 {
2946     makeAttribs();
2947 }
2948 
2949 void KTextEditor::DocumentPrivate::addView(KTextEditor::View *view)
2950 {
2951     Q_ASSERT(!m_views.contains(view));
2952     m_views.append(view);
2953 
2954     // apply the view & renderer vars from the file type
2955     if (!m_fileType.isEmpty()) {
2956         readVariableLine(KTextEditor::EditorPrivate::self()->modeManager()->fileType(m_fileType).varLine, true);
2957     }
2958 
2959     // apply the view & renderer vars from the file
2960     readVariables(true);
2961 
2962     setActiveView(view);
2963 }
2964 
2965 void KTextEditor::DocumentPrivate::removeView(KTextEditor::View *view)
2966 {
2967     Q_ASSERT(m_views.contains(view));
2968     m_views.removeAll(view);
2969 
2970     if (activeView() == view) {
2971         setActiveView(nullptr);
2972     }
2973 }
2974 
2975 void KTextEditor::DocumentPrivate::setActiveView(KTextEditor::View *view)
2976 {
2977     if (m_activeView == view) {
2978         return;
2979     }
2980 
2981     m_activeView = static_cast<KTextEditor::ViewPrivate *>(view);
2982 }
2983 
2984 bool KTextEditor::DocumentPrivate::ownedView(KTextEditor::ViewPrivate *view)
2985 {
2986     // do we own the given view?
2987     return (m_views.contains(view));
2988 }
2989 
2990 int KTextEditor::DocumentPrivate::toVirtualColumn(int line, int column) const
2991 {
2992     Kate::TextLine textLine = m_buffer->plainLine(line);
2993     return textLine.toVirtualColumn(column, config()->tabWidth());
2994 }
2995 
2996 int KTextEditor::DocumentPrivate::toVirtualColumn(const KTextEditor::Cursor cursor) const
2997 {
2998     return toVirtualColumn(cursor.line(), cursor.column());
2999 }
3000 
3001 int KTextEditor::DocumentPrivate::fromVirtualColumn(int line, int column) const
3002 {
3003     Kate::TextLine textLine = m_buffer->plainLine(line);
3004     return textLine.fromVirtualColumn(column, config()->tabWidth());
3005 }
3006 
3007 int KTextEditor::DocumentPrivate::fromVirtualColumn(const KTextEditor::Cursor cursor) const
3008 {
3009     return fromVirtualColumn(cursor.line(), cursor.column());
3010 }
3011 
3012 bool KTextEditor::DocumentPrivate::skipAutoBrace(QChar closingBracket, KTextEditor::Cursor pos)
3013 {
3014     // auto bracket handling for newly inserted text
3015     // we inserted a bracket?
3016     // => add the matching closing one to the view + input chars
3017     // try to preserve the cursor position
3018     bool skipAutobrace = closingBracket == QLatin1Char('\'');
3019     if (highlight() && skipAutobrace) {
3020         // skip adding ' in spellchecked areas, because those are text
3021         skipAutobrace = highlight()->spellCheckingRequiredForLocation(this, pos - Cursor{0, 1});
3022     }
3023 
3024     if (!skipAutobrace && (closingBracket == QLatin1Char('\''))) {
3025         // skip auto quotes when these looks already balanced, bug 405089
3026         Kate::TextLine textLine = m_buffer->plainLine(pos.line());
3027         // RegEx match quote, but not escaped quote, thanks to https://stackoverflow.com/a/11819111
3028         static const QRegularExpression re(QStringLiteral("(?<!\\\\)(?:\\\\\\\\)*\\\'"));
3029         const int count = textLine.text().left(pos.column()).count(re);
3030         skipAutobrace = (count % 2 == 0) ? true : false;
3031     }
3032     if (!skipAutobrace && (closingBracket == QLatin1Char('\"'))) {
3033         // ...same trick for double quotes
3034         Kate::TextLine textLine = m_buffer->plainLine(pos.line());
3035         static const QRegularExpression re(QStringLiteral("(?<!\\\\)(?:\\\\\\\\)*\\\""));
3036         const int count = textLine.text().left(pos.column()).count(re);
3037         skipAutobrace = (count % 2 == 0) ? true : false;
3038     }
3039     return skipAutobrace;
3040 }
3041 
3042 void KTextEditor::DocumentPrivate::typeChars(KTextEditor::ViewPrivate *view, QString chars)
3043 {
3044     // nop for empty chars
3045     if (chars.isEmpty()) {
3046         return;
3047     }
3048 
3049     // auto bracket handling
3050     QChar closingBracket;
3051     if (view->config()->autoBrackets()) {
3052         // Check if entered closing bracket is already balanced
3053         const QChar typedChar = chars.at(0);
3054         const QChar openBracket = matchingStartBracket(typedChar);
3055         if (!openBracket.isNull()) {
3056             KTextEditor::Cursor curPos = view->cursorPosition();
3057             if ((characterAt(curPos) == typedChar) && findMatchingBracket(curPos, 123 /*Which value may best?*/).isValid()) {
3058                 // Do nothing
3059                 view->cursorRight();
3060                 return;
3061             }
3062         }
3063 
3064         // for newly inserted text: remember if we should auto add some bracket
3065         if (chars.size() == 1) {
3066             // we inserted a bracket? => remember the matching closing one
3067             closingBracket = matchingEndBracket(typedChar);
3068 
3069             // closing bracket for the autobracket we inserted earlier?
3070             if (m_currentAutobraceClosingChar == typedChar && m_currentAutobraceRange) {
3071                 // do nothing
3072                 m_currentAutobraceRange.reset(nullptr);
3073                 view->cursorRight();
3074                 return;
3075             }
3076         }
3077     }
3078 
3079     // Treat some char also as "auto bracket" only when we have a selection
3080     if (view->selection() && closingBracket.isNull() && view->config()->encloseSelectionInChars()) {
3081         const QChar typedChar = chars.at(0);
3082         if (view->config()->charsToEncloseSelection().contains(typedChar)) {
3083             // The unconditional mirroring cause no harm, but allows funny brackets
3084             closingBracket = typedChar.mirroredChar();
3085         }
3086     }
3087 
3088     editStart();
3089 
3090     // special handling if we want to add auto brackets to a selection
3091     if (view->selection() && !closingBracket.isNull()) {
3092         std::unique_ptr<KTextEditor::MovingRange> selectionRange(newMovingRange(view->selectionRange()));
3093         const int startLine = qMax(0, selectionRange->start().line());
3094         const int endLine = qMin(selectionRange->end().line(), lastLine());
3095         const bool blockMode = view->blockSelection() && (startLine != endLine);
3096         if (blockMode) {
3097             if (selectionRange->start().column() > selectionRange->end().column()) {
3098                 // Selection was done from right->left, requires special setting to ensure the new
3099                 // added brackets will not be part of the selection
3100                 selectionRange->setInsertBehaviors(MovingRange::ExpandLeft | MovingRange::ExpandRight);
3101             }
3102             // Add brackets to each line of the block
3103             const int startColumn = qMin(selectionRange->start().column(), selectionRange->end().column());
3104             const int endColumn = qMax(selectionRange->start().column(), selectionRange->end().column());
3105             const KTextEditor::Range workingRange(startLine, startColumn, endLine, endColumn);
3106             for (int line = startLine; line <= endLine; ++line) {
3107                 const KTextEditor::Range r(rangeOnLine(workingRange, line));
3108                 insertText(r.end(), QString(closingBracket));
3109                 view->slotTextInserted(view, r.end(), QString(closingBracket));
3110                 insertText(r.start(), chars);
3111                 view->slotTextInserted(view, r.start(), chars);
3112             }
3113 
3114         } else {
3115             for (const auto &cursor : view->secondaryCursors()) {
3116                 if (!cursor.range) {
3117                     continue;
3118                 }
3119                 const auto &currSelectionRange = cursor.range;
3120                 auto expandBehaviour = currSelectionRange->insertBehaviors();
3121                 currSelectionRange->setInsertBehaviors(KTextEditor::MovingRange::DoNotExpand);
3122                 insertText(currSelectionRange->end(), QString(closingBracket));
3123                 insertText(currSelectionRange->start(), chars);
3124                 currSelectionRange->setInsertBehaviors(expandBehaviour);
3125                 cursor.pos->setPosition(currSelectionRange->end());
3126                 auto mutableCursor = const_cast<KTextEditor::ViewPrivate::SecondaryCursor *>(&cursor);
3127                 mutableCursor->anchor = currSelectionRange->start().toCursor();
3128             }
3129 
3130             // No block, just add to start & end of selection
3131             insertText(selectionRange->end(), QString(closingBracket));
3132             view->slotTextInserted(view, selectionRange->end(), QString(closingBracket));
3133             insertText(selectionRange->start(), chars);
3134             view->slotTextInserted(view, selectionRange->start(), chars);
3135         }
3136 
3137         // Refresh selection
3138         view->setSelection(selectionRange->toRange());
3139         view->setCursorPosition(selectionRange->end());
3140 
3141         editEnd();
3142         return;
3143     }
3144 
3145     // normal handling
3146     if (!view->config()->persistentSelection() && view->selection()) {
3147         view->removeSelectedText();
3148     }
3149 
3150     const KTextEditor::Cursor oldCur(view->cursorPosition());
3151 
3152     const bool multiLineBlockMode = view->blockSelection() && view->selection();
3153     if (view->currentInputMode()->overwrite()) {
3154         // blockmode multiline selection case: remove chars in every line
3155         const KTextEditor::Range selectionRange = view->selectionRange();
3156         const int startLine = multiLineBlockMode ? qMax(0, selectionRange.start().line()) : view->cursorPosition().line();
3157         const int endLine = multiLineBlockMode ? qMin(selectionRange.end().line(), lastLine()) : startLine;
3158         const int virtualColumn = toVirtualColumn(multiLineBlockMode ? selectionRange.end() : view->cursorPosition());
3159 
3160         for (int line = endLine; line >= startLine; --line) {
3161             Kate::TextLine textLine = m_buffer->plainLine(line);
3162             const int column = fromVirtualColumn(line, virtualColumn);
3163             KTextEditor::Range r = KTextEditor::Range(KTextEditor::Cursor(line, column), qMin(chars.length(), textLine.length() - column));
3164 
3165             // replace mode needs to know what was removed so it can be restored with backspace
3166             if (oldCur.column() < lineLength(line)) {
3167                 QChar removed = characterAt(KTextEditor::Cursor(line, column));
3168                 view->currentInputMode()->overwrittenChar(removed);
3169             }
3170 
3171             removeText(r);
3172         }
3173     }
3174 
3175     chars = eventuallyReplaceTabs(view->cursorPosition(), chars);
3176 
3177     if (multiLineBlockMode) {
3178         KTextEditor::Range selectionRange = view->selectionRange();
3179         const int startLine = qMax(0, selectionRange.start().line());
3180         const int endLine = qMin(selectionRange.end().line(), lastLine());
3181         const int column = toVirtualColumn(selectionRange.end());
3182         for (int line = endLine; line >= startLine; --line) {
3183             editInsertText(line, fromVirtualColumn(line, column), chars);
3184         }
3185         int newSelectionColumn = toVirtualColumn(view->cursorPosition());
3186         selectionRange.setRange(KTextEditor::Cursor(selectionRange.start().line(), fromVirtualColumn(selectionRange.start().line(), newSelectionColumn)),
3187                                 KTextEditor::Cursor(selectionRange.end().line(), fromVirtualColumn(selectionRange.end().line(), newSelectionColumn)));
3188         view->setSelection(selectionRange);
3189     } else {
3190         // handle multi cursor input
3191         // We don't want completionWidget to be doing useless stuff, it
3192         // should only respond to main cursor text changes
3193         view->completionWidget()->setIgnoreBufferSignals(true);
3194         const auto &sc = view->secondaryCursors();
3195         const bool hasClosingBracket = !closingBracket.isNull();
3196         const QString closingChar = closingBracket;
3197         for (const auto &c : sc) {
3198             insertText(c.cursor(), chars);
3199             const auto pos = c.cursor();
3200             const auto nextChar = view->document()->text({pos, pos + Cursor{0, 1}}).trimmed();
3201             if (hasClosingBracket && !skipAutoBrace(closingBracket, pos) && (nextChar.isEmpty() || !nextChar.at(0).isLetterOrNumber())) {
3202                 insertText(c.cursor(), closingChar);
3203                 c.pos->setPosition(pos);
3204             }
3205         }
3206         view->completionWidget()->setIgnoreBufferSignals(false);
3207         // then our normal cursor
3208         insertText(view->cursorPosition(), chars);
3209     }
3210 
3211     // auto bracket handling for newly inserted text
3212     // we inserted a bracket?
3213     // => add the matching closing one to the view + input chars
3214     // try to preserve the cursor position
3215     if (!closingBracket.isNull() && !skipAutoBrace(closingBracket, view->cursorPosition())) {
3216         // add bracket to the view
3217         const auto cursorPos = view->cursorPosition();
3218         const auto nextChar = view->document()->text({cursorPos, cursorPos + Cursor{0, 1}}).trimmed();
3219         if (nextChar.isEmpty() || !nextChar.at(0).isLetterOrNumber()) {
3220             insertText(view->cursorPosition(), QString(closingBracket));
3221             const auto insertedAt(view->cursorPosition());
3222             view->setCursorPosition(cursorPos);
3223             m_currentAutobraceRange.reset(newMovingRange({cursorPos - Cursor{0, 1}, insertedAt}, KTextEditor::MovingRange::DoNotExpand));
3224             connect(view, &View::cursorPositionChanged, this, &DocumentPrivate::checkCursorForAutobrace, Qt::UniqueConnection);
3225 
3226             // add bracket to chars inserted! needed for correct signals + indent
3227             chars.append(closingBracket);
3228         }
3229         m_currentAutobraceClosingChar = closingBracket;
3230     }
3231 
3232     // end edit session here, to have updated HL in userTypedChar!
3233     editEnd();
3234 
3235     // indentation for multi cursors
3236     const auto &secondaryCursors = view->secondaryCursors();
3237     for (const auto &c : secondaryCursors) {
3238         m_indenter->userTypedChar(view, c.cursor(), chars.isEmpty() ? QChar() : chars.at(chars.length() - 1));
3239     }
3240 
3241     // trigger indentation for primary
3242     KTextEditor::Cursor b(view->cursorPosition());
3243     m_indenter->userTypedChar(view, b, chars.isEmpty() ? QChar() : chars.at(chars.length() - 1));
3244 
3245     // inform the view about the original inserted chars
3246     view->slotTextInserted(view, oldCur, chars);
3247 }
3248 
3249 void KTextEditor::DocumentPrivate::checkCursorForAutobrace(KTextEditor::View *, const KTextEditor::Cursor newPos)
3250 {
3251     if (m_currentAutobraceRange && !m_currentAutobraceRange->toRange().contains(newPos)) {
3252         m_currentAutobraceRange.reset();
3253     }
3254 }
3255 
3256 void KTextEditor::DocumentPrivate::newLine(KTextEditor::ViewPrivate *v, KTextEditor::DocumentPrivate::NewLineIndent indent, NewLinePos newLinePos)
3257 {
3258     editStart();
3259 
3260     if (!v->config()->persistentSelection() && v->selection()) {
3261         v->removeSelectedText();
3262         v->clearSelection();
3263     }
3264 
3265     auto insertNewLine = [this](KTextEditor::Cursor c) {
3266         if (c.line() > lastLine()) {
3267             c.setLine(lastLine());
3268         }
3269 
3270         if (c.line() < 0) {
3271             c.setLine(0);
3272         }
3273 
3274         int ln = c.line();
3275 
3276         int len = lineLength(ln);
3277 
3278         if (c.column() > len) {
3279             c.setColumn(len);
3280         }
3281 
3282         // first: wrap line
3283         editWrapLine(c.line(), c.column());
3284 
3285         // update highlighting to have updated HL in userTypedChar!
3286         m_buffer->updateHighlighting();
3287     };
3288 
3289     // Helper which allows adding a new line and moving the cursor there
3290     // without modifying the current line
3291     auto adjustCusorPos = [newLinePos, this](KTextEditor::Cursor pos) {
3292         // Handle primary cursor
3293         bool moveCursorToTop = false;
3294         if (newLinePos == Above) {
3295             if (pos.line() <= 0) {
3296                 pos.setLine(0);
3297                 pos.setColumn(0);
3298                 moveCursorToTop = true;
3299             } else {
3300                 pos.setLine(pos.line() - 1);
3301                 pos.setColumn(lineLength(pos.line()));
3302             }
3303         } else if (newLinePos == Below) {
3304             int lastCol = lineLength(pos.line());
3305             pos.setColumn(lastCol);
3306         }
3307         return std::pair{pos, moveCursorToTop};
3308     };
3309 
3310     // Handle multicursors
3311     const auto &secondaryCursors = v->secondaryCursors();
3312     if (!secondaryCursors.empty()) {
3313         // Save the original position of our primary cursor
3314         Kate::TextCursor savedPrimary(buffer(), v->cursorPosition(), Kate::TextCursor::MoveOnInsert);
3315         for (const auto &c : secondaryCursors) {
3316             const auto [newPos, moveCursorToTop] = adjustCusorPos(c.cursor());
3317             c.pos->setPosition(newPos);
3318             insertNewLine(c.cursor());
3319             if (moveCursorToTop) {
3320                 c.pos->setPosition({0, 0});
3321             }
3322             // second: if "indent" is true, indent the new line, if needed...
3323             if (indent == KTextEditor::DocumentPrivate::Indent) {
3324                 // Make this secondary cursor primary for a moment
3325                 // this is necessary because the scripts modify primary cursor
3326                 // position which can lead to weird indent issues with multicursor
3327                 v->setCursorPosition(c.cursor());
3328                 m_indenter->userTypedChar(v, c.cursor(), QLatin1Char('\n'));
3329                 // restore
3330                 c.pos->setPosition(v->cursorPosition());
3331             }
3332         }
3333         // Restore the original primary cursor
3334         v->setCursorPosition(savedPrimary.toCursor());
3335     }
3336 
3337     const auto [newPos, moveCursorToTop] = adjustCusorPos(v->cursorPosition());
3338     v->setCursorPosition(newPos);
3339     insertNewLine(v->cursorPosition());
3340     if (moveCursorToTop) {
3341         v->setCursorPosition({0, 0});
3342     }
3343     // second: if "indent" is true, indent the new line, if needed...
3344     if (indent == KTextEditor::DocumentPrivate::Indent) {
3345         m_indenter->userTypedChar(v, v->cursorPosition(), QLatin1Char('\n'));
3346     }
3347 
3348     editEnd();
3349 }
3350 
3351 void KTextEditor::DocumentPrivate::transpose(const KTextEditor::Cursor cursor)
3352 {
3353     Kate::TextLine textLine = m_buffer->plainLine(cursor.line());
3354     if (textLine.length() < 2) {
3355         return;
3356     }
3357 
3358     uint col = cursor.column();
3359 
3360     if (col > 0) {
3361         col--;
3362     }
3363 
3364     if ((textLine.length() - col) < 2) {
3365         return;
3366     }
3367 
3368     uint line = cursor.line();
3369     QString s;
3370 
3371     // clever swap code if first character on the line swap right&left
3372     // otherwise left & right
3373     s.append(textLine.at(col + 1));
3374     s.append(textLine.at(col));
3375     // do the swap
3376 
3377     // do it right, never ever manipulate a textline
3378     editStart();
3379     editRemoveText(line, col, 2);
3380     editInsertText(line, col, s);
3381     editEnd();
3382 }
3383 
3384 void KTextEditor::DocumentPrivate::swapTextRanges(KTextEditor::Range firstWord, KTextEditor::Range secondWord)
3385 {
3386     Q_ASSERT(firstWord.isValid() && secondWord.isValid());
3387     Q_ASSERT(!firstWord.overlaps(secondWord));
3388     // ensure that secondWord comes AFTER firstWord
3389     if (firstWord.start().column() > secondWord.start().column() || firstWord.start().line() > secondWord.start().line()) {
3390         const KTextEditor::Range tempRange = firstWord;
3391         firstWord.setRange(secondWord);
3392         secondWord.setRange(tempRange);
3393     }
3394 
3395     const QString tempString = text(secondWord);
3396     editStart();
3397     // edit secondWord first as the range might be invalidated after editing firstWord
3398     replaceText(secondWord, text(firstWord));
3399     replaceText(firstWord, tempString);
3400     editEnd();
3401 }
3402 
3403 KTextEditor::Cursor KTextEditor::DocumentPrivate::backspaceAtCursor(KTextEditor::ViewPrivate *view, KTextEditor::Cursor c)
3404 {
3405     int col = qMax(c.column(), 0);
3406     int line = qMax(c.line(), 0);
3407     if ((col == 0) && (line == 0)) {
3408         return KTextEditor::Cursor::invalid();
3409     }
3410     if (line >= lines()) {
3411         return KTextEditor::Cursor::invalid();
3412     }
3413 
3414     const Kate::TextLine textLine = m_buffer->plainLine(line);
3415 
3416     if (col > 0) {
3417         bool useNextBlock = false;
3418         if (config()->backspaceIndents()) {
3419             // backspace indents: erase to next indent position
3420             int colX = textLine.toVirtualColumn(col, config()->tabWidth());
3421             int pos = textLine.firstChar();
3422             if (pos > 0) {
3423                 pos = textLine.toVirtualColumn(pos, config()->tabWidth());
3424             }
3425             if (pos < 0 || pos >= (int)colX) {
3426                 // only spaces on left side of cursor
3427                 if ((int)col > textLine.length()) {
3428                     // beyond the end of the line, move cursor only
3429                     return KTextEditor::Cursor(line, col - 1);
3430                 }
3431                 indent(KTextEditor::Range(line, 0, line, 0), -1);
3432             } else {
3433                 useNextBlock = true;
3434             }
3435         }
3436         if (!config()->backspaceIndents() || useNextBlock) {
3437             KTextEditor::Cursor beginCursor(line, 0);
3438             KTextEditor::Cursor endCursor(line, col);
3439             if (!view->config()->backspaceRemoveComposed()) { // Normal backspace behavior
3440                 beginCursor.setColumn(col - 1);
3441                 // move to left of surrogate pair
3442                 if (!isValidTextPosition(beginCursor)) {
3443                     Q_ASSERT(col >= 2);
3444                     beginCursor.setColumn(col - 2);
3445                 }
3446             } else {
3447                 if (auto l = view->textLayout(c)) {
3448                     beginCursor.setColumn(l->previousCursorPosition(c.column()));
3449                 }
3450             }
3451             removeText(KTextEditor::Range(beginCursor, endCursor));
3452             // in most cases cursor is moved by removeText, but we should do it manually
3453             // for past-end-of-line cursors in block mode
3454             return beginCursor;
3455         }
3456         return KTextEditor::Cursor::invalid();
3457     } else {
3458         // col == 0: wrap to previous line
3459         const Kate::TextLine textLine = m_buffer->plainLine(line - 1);
3460         KTextEditor::Cursor ret = KTextEditor::Cursor::invalid();
3461 
3462         if (line > 0) {
3463             if (config()->wordWrap() && textLine.endsWith(QStringLiteral(" "))) {
3464                 // gg: in hard wordwrap mode, backspace must also eat the trailing space
3465                 ret = KTextEditor::Cursor(line - 1, textLine.length() - 1);
3466                 removeText(KTextEditor::Range(line - 1, textLine.length() - 1, line, 0));
3467             } else {
3468                 ret = KTextEditor::Cursor(line - 1, textLine.length());
3469                 removeText(KTextEditor::Range(line - 1, textLine.length(), line, 0));
3470             }
3471         }
3472         return ret;
3473     }
3474 }
3475 
3476 void KTextEditor::DocumentPrivate::backspace(KTextEditor::ViewPrivate *view)
3477 {
3478     if (!view->config()->persistentSelection() && view->hasSelections()) {
3479         KTextEditor::Range range = view->selectionRange();
3480         editStart(); // Avoid bad selection in case of undo
3481 
3482         if (view->blockSelection() && view->selection() && range.start().column() > 0 && toVirtualColumn(range.start()) == toVirtualColumn(range.end())) {
3483             // Remove one character before vertical selection line by expanding the selection
3484             range.setStart(KTextEditor::Cursor(range.start().line(), range.start().column() - 1));
3485             view->setSelection(range);
3486         }
3487         view->removeSelectedText();
3488         view->ensureUniqueCursors();
3489         editEnd();
3490         return;
3491     }
3492 
3493     editStart();
3494 
3495     // Handle multi cursors
3496     const auto &multiCursors = view->secondaryCursors();
3497     view->completionWidget()->setIgnoreBufferSignals(true);
3498     for (const auto &c : multiCursors) {
3499         const auto newPos = backspaceAtCursor(view, c.cursor());
3500         if (newPos.isValid()) {
3501             c.pos->setPosition(newPos);
3502         }
3503     }
3504     view->completionWidget()->setIgnoreBufferSignals(false);
3505 
3506     // Handle primary cursor
3507     auto newPos = backspaceAtCursor(view, view->cursorPosition());
3508     if (newPos.isValid()) {
3509         view->setCursorPosition(newPos);
3510     }
3511 
3512     view->ensureUniqueCursors();
3513 
3514     editEnd();
3515 
3516     // TODO: Handle this for multiple cursors?
3517     if (m_currentAutobraceRange) {
3518         const auto r = m_currentAutobraceRange->toRange();
3519         if (r.columnWidth() == 1 && view->cursorPosition() == r.start()) {
3520             // start parenthesis removed and range length is 1, remove end as well
3521             del(view, view->cursorPosition());
3522             m_currentAutobraceRange.reset();
3523         }
3524     }
3525 }
3526 
3527 void KTextEditor::DocumentPrivate::del(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor c)
3528 {
3529     if (!view->config()->persistentSelection() && view->selection()) {
3530         KTextEditor::Range range = view->selectionRange();
3531         editStart(); // Avoid bad selection in case of undo
3532         if (view->blockSelection() && toVirtualColumn(range.start()) == toVirtualColumn(range.end())) {
3533             // Remove one character after vertical selection line by expanding the selection
3534             range.setEnd(KTextEditor::Cursor(range.end().line(), range.end().column() + 1));
3535             view->setSelection(range);
3536         }
3537         view->removeSelectedText();
3538         editEnd();
3539         return;
3540     }
3541 
3542     if (c.column() < m_buffer->lineLength(c.line())) {
3543         KTextEditor::Cursor endCursor(c.line(), view->textLayout(c)->nextCursorPosition(c.column()));
3544         removeText(KTextEditor::Range(c, endCursor));
3545     } else if (c.line() < lastLine()) {
3546         removeText(KTextEditor::Range(c.line(), c.column(), c.line() + 1, 0));
3547     }
3548 }
3549 
3550 bool KTextEditor::DocumentPrivate::multiPaste(KTextEditor::ViewPrivate *view, const QStringList &texts)
3551 {
3552     if (texts.isEmpty() || view->isMulticursorNotAllowed() || view->secondaryCursors().size() + 1 != (size_t)texts.size()) {
3553         return false;
3554     }
3555 
3556     m_undoManager->undoSafePoint();
3557 
3558     editStart();
3559     if (view->selection()) {
3560         view->removeSelectedText();
3561     }
3562 
3563     auto plainSecondaryCursors = view->plainSecondaryCursors();
3564     KTextEditor::ViewPrivate::PlainSecondaryCursor primary;
3565     primary.pos = view->cursorPosition();
3566     primary.range = view->selectionRange();
3567     plainSecondaryCursors.append(primary);
3568     std::sort(plainSecondaryCursors.begin(), plainSecondaryCursors.end());
3569 
3570     static const QRegularExpression re(QStringLiteral("\r\n?"));
3571 
3572     for (int i = texts.size() - 1; i >= 0; --i) {
3573         QString text = texts[i];
3574         text.replace(re, QStringLiteral("\n"));
3575         KTextEditor::Cursor pos = plainSecondaryCursors[i].pos;
3576         if (pos.isValid()) {
3577             insertText(pos, text, /*blockmode=*/false);
3578         }
3579     }
3580 
3581     editEnd();
3582     return true;
3583 }
3584 
3585 void KTextEditor::DocumentPrivate::paste(KTextEditor::ViewPrivate *view, const QString &text)
3586 {
3587     // nop if nothing to paste
3588     if (text.isEmpty()) {
3589         return;
3590     }
3591 
3592     // normalize line endings, to e.g. catch issues with \r\n in paste buffer
3593     // see bug 410951
3594     QString s = text;
3595     s.replace(QRegularExpression(QStringLiteral("\r\n?")), QStringLiteral("\n"));
3596 
3597     int lines = s.count(QLatin1Char('\n'));
3598     const bool isSingleLine = lines == 0;
3599 
3600     m_undoManager->undoSafePoint();
3601 
3602     editStart();
3603 
3604     KTextEditor::Cursor pos = view->cursorPosition();
3605 
3606     bool skipIndentOnPaste = false;
3607     if (isSingleLine) {
3608         const int length = lineLength(pos.line());
3609         // if its a single line and the line already contains some text, skip indenting
3610         skipIndentOnPaste = length > 0;
3611     }
3612 
3613     if (!view->config()->persistentSelection() && view->selection()) {
3614         pos = view->selectionRange().start();
3615         if (view->blockSelection()) {
3616             pos = rangeOnLine(view->selectionRange(), pos.line()).start();
3617             if (lines == 0) {
3618                 s += QLatin1Char('\n');
3619                 s = s.repeated(view->selectionRange().numberOfLines() + 1);
3620                 s.chop(1);
3621             }
3622         }
3623         view->removeSelectedText();
3624     }
3625 
3626     if (config()->ovr()) {
3627         const auto pasteLines = QStringView(s).split(QLatin1Char('\n'));
3628 
3629         if (!view->blockSelection()) {
3630             int endColumn = (pasteLines.count() == 1 ? pos.column() : 0) + pasteLines.last().length();
3631             removeText(KTextEditor::Range(pos, pos.line() + pasteLines.count() - 1, endColumn));
3632         } else {
3633             int maxi = qMin(pos.line() + pasteLines.count(), this->lines());
3634 
3635             for (int i = pos.line(); i < maxi; ++i) {
3636                 int pasteLength = pasteLines.at(i - pos.line()).length();
3637                 removeText(KTextEditor::Range(i, pos.column(), i, qMin(pasteLength + pos.column(), lineLength(i))));
3638             }
3639         }
3640     }
3641 
3642     insertText(pos, s, view->blockSelection());
3643     editEnd();
3644 
3645     // move cursor right for block select, as the user is moved right internal
3646     // even in that case, but user expects other behavior in block selection
3647     // mode !
3648     // just let cursor stay, that was it before I changed to moving ranges!
3649     if (view->blockSelection()) {
3650         view->setCursorPositionInternal(pos);
3651     }
3652 
3653     if (config()->indentPastedText()) {
3654         KTextEditor::Range range = KTextEditor::Range(KTextEditor::Cursor(pos.line(), 0), KTextEditor::Cursor(pos.line() + lines, 0));
3655         if (!skipIndentOnPaste) {
3656             m_indenter->indent(view, range);
3657         }
3658     }
3659 
3660     if (!view->blockSelection()) {
3661         Q_EMIT charactersSemiInteractivelyInserted(pos, s);
3662     }
3663     m_undoManager->undoSafePoint();
3664 }
3665 
3666 void KTextEditor::DocumentPrivate::indent(KTextEditor::Range range, int change)
3667 {
3668     if (!isReadWrite()) {
3669         return;
3670     }
3671 
3672     editStart();
3673     m_indenter->changeIndent(range, change);
3674     editEnd();
3675 }
3676 
3677 void KTextEditor::DocumentPrivate::align(KTextEditor::ViewPrivate *view, KTextEditor::Range range)
3678 {
3679     m_indenter->indent(view, range);
3680 }
3681 
3682 void KTextEditor::DocumentPrivate::alignOn(KTextEditor::Range range, const QString &pattern, bool blockwise)
3683 {
3684     QStringList lines = textLines(range, blockwise);
3685     // if we have less then two lines in the selection there is nothing to do
3686     if (lines.size() < 2) {
3687         return;
3688     }
3689     // align on first non-blank character by default
3690     QRegularExpression re(pattern.isEmpty() ? QStringLiteral("[^\\s]") : pattern);
3691     // find all matches actual column (normal selection: first line has offset ; block selection: all lines have offset)
3692     int selectionStartColumn = range.start().column();
3693     QList<int> patternStartColumns;
3694     for (const auto &line : lines) {
3695         QRegularExpressionMatch match = re.match(line);
3696         if (!match.hasMatch()) { // no match
3697             patternStartColumns.append(-1);
3698         } else if (match.lastCapturedIndex() == 0) { // pattern has no group
3699             patternStartColumns.append(match.capturedStart(0) + (blockwise ? selectionStartColumn : 0));
3700         } else { // pattern has a group
3701             patternStartColumns.append(match.capturedStart(1) + (blockwise ? selectionStartColumn : 0));
3702         }
3703     }
3704     if (!blockwise && patternStartColumns[0] != -1) {
3705         patternStartColumns[0] += selectionStartColumn;
3706     }
3707     // find which column we'll align with
3708     int maxColumn = *std::max_element(patternStartColumns.cbegin(), patternStartColumns.cend());
3709     // align!
3710     editStart();
3711     for (int i = 0; i < lines.size(); ++i) {
3712         if (patternStartColumns[i] != -1) {
3713             insertText(KTextEditor::Cursor(range.start().line() + i, patternStartColumns[i]), QString(maxColumn - patternStartColumns[i], QChar::Space));
3714         }
3715     }
3716     editEnd();
3717 }
3718 
3719 void KTextEditor::DocumentPrivate::insertTab(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor)
3720 {
3721     if (!isReadWrite()) {
3722         return;
3723     }
3724 
3725     int lineLen = line(view->cursorPosition().line()).length();
3726     KTextEditor::Cursor c = view->cursorPosition();
3727 
3728     editStart();
3729 
3730     if (!view->config()->persistentSelection() && view->selection()) {
3731         view->removeSelectedText();
3732     } else if (view->currentInputMode()->overwrite() && c.column() < lineLen) {
3733         KTextEditor::Range r = KTextEditor::Range(view->cursorPosition(), 1);
3734 
3735         // replace mode needs to know what was removed so it can be restored with backspace
3736         QChar removed = line(view->cursorPosition().line()).at(r.start().column());
3737         view->currentInputMode()->overwrittenChar(removed);
3738         removeText(r);
3739     }
3740 
3741     c = view->cursorPosition();
3742     editInsertText(c.line(), c.column(), QStringLiteral("\t"));
3743 
3744     editEnd();
3745 }
3746 
3747 /*
3748   Remove a given string at the beginning
3749   of the current line.
3750 */
3751 bool KTextEditor::DocumentPrivate::removeStringFromBeginning(int line, const QString &str)
3752 {
3753     Kate::TextLine textline = m_buffer->plainLine(line);
3754 
3755     KTextEditor::Cursor cursor(line, 0);
3756     bool there = textline.startsWith(str);
3757 
3758     if (!there) {
3759         cursor.setColumn(textline.firstChar());
3760         there = textline.matchesAt(cursor.column(), str);
3761     }
3762 
3763     if (there) {
3764         // Remove some chars
3765         removeText(KTextEditor::Range(cursor, str.length()));
3766     }
3767 
3768     return there;
3769 }
3770 
3771 /*
3772   Remove a given string at the end
3773   of the current line.
3774 */
3775 bool KTextEditor::DocumentPrivate::removeStringFromEnd(int line, const QString &str)
3776 {
3777     Kate::TextLine textline = m_buffer->plainLine(line);
3778 
3779     KTextEditor::Cursor cursor(line, 0);
3780     bool there = textline.endsWith(str);
3781 
3782     if (there) {
3783         cursor.setColumn(textline.length() - str.length());
3784     } else {
3785         cursor.setColumn(textline.lastChar() - str.length() + 1);
3786         there = textline.matchesAt(cursor.column(), str);
3787     }
3788 
3789     if (there) {
3790         // Remove some chars
3791         removeText(KTextEditor::Range(cursor, str.length()));
3792     }
3793 
3794     return there;
3795 }
3796 
3797 /*
3798   Replace tabs by spaces in the given string, if enabled.
3799  */
3800 QString KTextEditor::DocumentPrivate::eventuallyReplaceTabs(const KTextEditor::Cursor cursorPos, const QString &str) const
3801 {
3802     const bool replacetabs = config()->replaceTabsDyn();
3803     if (!replacetabs) {
3804         return str;
3805     }
3806     const int indentWidth = config()->indentationWidth();
3807     static const QLatin1Char tabChar('\t');
3808 
3809     int column = cursorPos.column();
3810 
3811     // The result will always be at least as long as the input
3812     QString result;
3813     result.reserve(str.size());
3814 
3815     for (const QChar ch : str) {
3816         if (ch == tabChar) {
3817             // Insert only enough spaces to align to the next indentWidth column
3818             // This fixes bug #340212
3819             int spacesToInsert = indentWidth - (column % indentWidth);
3820             result += QString(spacesToInsert, QLatin1Char(' '));
3821             column += spacesToInsert;
3822         } else {
3823             // Just keep all other typed characters as-is
3824             result += ch;
3825             ++column;
3826         }
3827     }
3828     return result;
3829 }
3830 
3831 /*
3832   Add to the current line a comment line mark at the beginning.
3833 */
3834 void KTextEditor::DocumentPrivate::addStartLineCommentToSingleLine(int line, int attrib)
3835 {
3836     const QString commentLineMark = highlight()->getCommentSingleLineStart(attrib) + QLatin1Char(' ');
3837     int pos = 0;
3838 
3839     if (highlight()->getCommentSingleLinePosition(attrib) == KSyntaxHighlighting::CommentPosition::AfterWhitespace) {
3840         const Kate::TextLine l = kateTextLine(line);
3841         pos = qMax(0, l.firstChar());
3842     }
3843     insertText(KTextEditor::Cursor(line, pos), commentLineMark);
3844 }
3845 
3846 /*
3847   Remove from the current line a comment line mark at
3848   the beginning if there is one.
3849 */
3850 bool KTextEditor::DocumentPrivate::removeStartLineCommentFromSingleLine(int line, int attrib)
3851 {
3852     const QString shortCommentMark = highlight()->getCommentSingleLineStart(attrib);
3853     const QString longCommentMark = shortCommentMark + QLatin1Char(' ');
3854 
3855     editStart();
3856 
3857     // Try to remove the long comment mark first
3858     bool removed = (removeStringFromBeginning(line, longCommentMark) || removeStringFromBeginning(line, shortCommentMark));
3859 
3860     editEnd();
3861 
3862     return removed;
3863 }
3864 
3865 /*
3866   Add to the current line a start comment mark at the
3867   beginning and a stop comment mark at the end.
3868 */
3869 void KTextEditor::DocumentPrivate::addStartStopCommentToSingleLine(int line, int attrib)
3870 {
3871     const QString startCommentMark = highlight()->getCommentStart(attrib) + QLatin1Char(' ');
3872     const QString stopCommentMark = QLatin1Char(' ') + highlight()->getCommentEnd(attrib);
3873 
3874     editStart();
3875 
3876     // Add the start comment mark
3877     insertText(KTextEditor::Cursor(line, 0), startCommentMark);
3878 
3879     // Go to the end of the line
3880     const int col = m_buffer->lineLength(line);
3881 
3882     // Add the stop comment mark
3883     insertText(KTextEditor::Cursor(line, col), stopCommentMark);
3884 
3885     editEnd();
3886 }
3887 
3888 /*
3889   Remove from the current line a start comment mark at
3890   the beginning and a stop comment mark at the end.
3891 */
3892 bool KTextEditor::DocumentPrivate::removeStartStopCommentFromSingleLine(int line, int attrib)
3893 {
3894     const QString shortStartCommentMark = highlight()->getCommentStart(attrib);
3895     const QString longStartCommentMark = shortStartCommentMark + QLatin1Char(' ');
3896     const QString shortStopCommentMark = highlight()->getCommentEnd(attrib);
3897     const QString longStopCommentMark = QLatin1Char(' ') + shortStopCommentMark;
3898 
3899     editStart();
3900 
3901     // Try to remove the long start comment mark first
3902     const bool removedStart = (removeStringFromBeginning(line, longStartCommentMark) || removeStringFromBeginning(line, shortStartCommentMark));
3903 
3904     // Try to remove the long stop comment mark first
3905     const bool removedStop = removedStart && (removeStringFromEnd(line, longStopCommentMark) || removeStringFromEnd(line, shortStopCommentMark));
3906 
3907     editEnd();
3908 
3909     return (removedStart || removedStop);
3910 }
3911 
3912 /*
3913   Add to the current selection a start comment mark at the beginning
3914   and a stop comment mark at the end.
3915 */
3916 void KTextEditor::DocumentPrivate::addStartStopCommentToSelection(KTextEditor::Range selection, bool blockSelection, int attrib)
3917 {
3918     const QString startComment = highlight()->getCommentStart(attrib);
3919     const QString endComment = highlight()->getCommentEnd(attrib);
3920 
3921     KTextEditor::Range range = selection;
3922 
3923     if ((range.end().column() == 0) && (range.end().line() > 0)) {
3924         range.setEnd(KTextEditor::Cursor(range.end().line() - 1, lineLength(range.end().line() - 1)));
3925     }
3926 
3927     editStart();
3928 
3929     if (!blockSelection) {
3930         insertText(range.end(), endComment);
3931         insertText(range.start(), startComment);
3932     } else {
3933         for (int line = range.start().line(); line <= range.end().line(); line++) {
3934             KTextEditor::Range subRange = rangeOnLine(range, line);
3935             insertText(subRange.end(), endComment);
3936             insertText(subRange.start(), startComment);
3937         }
3938     }
3939 
3940     editEnd();
3941     // selection automatically updated (MovingRange)
3942 }
3943 
3944 /*
3945   Add to the current selection a comment line mark at the beginning of each line.
3946 */
3947 void KTextEditor::DocumentPrivate::addStartLineCommentToSelection(KTextEditor::Range selection, int attrib)
3948 {
3949     int sl = selection.start().line();
3950     int el = selection.end().line();
3951 
3952     // if end of selection is in column 0 in last line, omit the last line
3953     if ((selection.end().column() == 0) && (el > 0)) {
3954         el--;
3955     }
3956 
3957     if (sl < 0 || el < 0 || sl >= lines() || el >= lines()) {
3958         return;
3959     }
3960 
3961     editStart();
3962 
3963     const QString commentLineMark = highlight()->getCommentSingleLineStart(attrib) + QLatin1Char(' ');
3964 
3965     int col = 0;
3966     if (highlight()->getCommentSingleLinePosition(attrib) == KSyntaxHighlighting::CommentPosition::AfterWhitespace) {
3967         // For afterwhitespace, we add comment mark at col for all the lines,
3968         // where col == smallest indent in selection
3969         // This means that for somelines for example, a statement in an if block
3970         // might not have its comment mark exactly afterwhitespace, which is okay
3971         // and _good_ because if someone runs a formatter after commenting we will
3972         // loose indentation, which is _really_ bad and makes afterwhitespace useless
3973 
3974         col = std::numeric_limits<int>::max();
3975         // For each line in selection, try to find the smallest indent
3976         for (int l = el; l >= sl; l--) {
3977             const auto line = plainKateTextLine(l);
3978             if (line.length() == 0) {
3979                 continue;
3980             }
3981             col = qMin(col, qMax(0, line.firstChar()));
3982             if (col == 0) {
3983                 // early out: there can't be an indent smaller than 0
3984                 break;
3985             }
3986         }
3987 
3988         if (col == std::numeric_limits<int>::max()) {
3989             col = 0;
3990         }
3991         Q_ASSERT(col >= 0);
3992     }
3993 
3994     // For each line of the selection
3995     for (int l = el; l >= sl; l--) {
3996         insertText(KTextEditor::Cursor(l, col), commentLineMark);
3997     }
3998 
3999     editEnd();
4000 }
4001 
4002 bool KTextEditor::DocumentPrivate::nextNonSpaceCharPos(int &line, int &col)
4003 {
4004     for (; line >= 0 && line < m_buffer->lines(); line++) {
4005         Kate::TextLine textLine = m_buffer->plainLine(line);
4006         col = textLine.nextNonSpaceChar(col);
4007         if (col != -1) {
4008             return true; // Next non-space char found
4009         }
4010         col = 0;
4011     }
4012     // No non-space char found
4013     line = -1;
4014     col = -1;
4015     return false;
4016 }
4017 
4018 bool KTextEditor::DocumentPrivate::previousNonSpaceCharPos(int &line, int &col)
4019 {
4020     while (line >= 0 && line < m_buffer->lines()) {
4021         Kate::TextLine textLine = m_buffer->plainLine(line);
4022         col = textLine.previousNonSpaceChar(col);
4023         if (col != -1) {
4024             return true;
4025         }
4026         if (line == 0) {
4027             return false;
4028         }
4029         --line;
4030         col = textLine.length();
4031     }
4032     // No non-space char found
4033     line = -1;
4034     col = -1;
4035     return false;
4036 }
4037 
4038 /*
4039   Remove from the selection a start comment mark at
4040   the beginning and a stop comment mark at the end.
4041 */
4042 bool KTextEditor::DocumentPrivate::removeStartStopCommentFromSelection(KTextEditor::Range selection, int attrib)
4043 {
4044     const QString startComment = highlight()->getCommentStart(attrib);
4045     const QString endComment = highlight()->getCommentEnd(attrib);
4046 
4047     int sl = qMax<int>(0, selection.start().line());
4048     int el = qMin<int>(selection.end().line(), lastLine());
4049     int sc = selection.start().column();
4050     int ec = selection.end().column();
4051 
4052     // The selection ends on the char before selectEnd
4053     if (ec != 0) {
4054         --ec;
4055     } else if (el > 0) {
4056         --el;
4057         ec = m_buffer->lineLength(el) - 1;
4058     }
4059 
4060     const int startCommentLen = startComment.length();
4061     const int endCommentLen = endComment.length();
4062 
4063     // had this been perl or sed: s/^\s*$startComment(.+?)$endComment\s*/$2/
4064 
4065     bool remove = nextNonSpaceCharPos(sl, sc) && m_buffer->plainLine(sl).matchesAt(sc, startComment) && previousNonSpaceCharPos(el, ec)
4066         && ((ec - endCommentLen + 1) >= 0) && m_buffer->plainLine(el).matchesAt(ec - endCommentLen + 1, endComment);
4067 
4068     if (remove) {
4069         editStart();
4070 
4071         removeText(KTextEditor::Range(el, ec - endCommentLen + 1, el, ec + 1));
4072         removeText(KTextEditor::Range(sl, sc, sl, sc + startCommentLen));
4073 
4074         editEnd();
4075         // selection automatically updated (MovingRange)
4076     }
4077 
4078     return remove;
4079 }
4080 
4081 bool KTextEditor::DocumentPrivate::removeStartStopCommentFromRegion(const KTextEditor::Cursor start, const KTextEditor::Cursor end, int attrib)
4082 {
4083     const QString startComment = highlight()->getCommentStart(attrib);
4084     const QString endComment = highlight()->getCommentEnd(attrib);
4085     const int startCommentLen = startComment.length();
4086     const int endCommentLen = endComment.length();
4087 
4088     const bool remove = m_buffer->plainLine(start.line()).matchesAt(start.column(), startComment)
4089         && m_buffer->plainLine(end.line()).matchesAt(end.column() - endCommentLen, endComment);
4090     if (remove) {
4091         editStart();
4092         removeText(KTextEditor::Range(end.line(), end.column() - endCommentLen, end.line(), end.column()));
4093         removeText(KTextEditor::Range(start, startCommentLen));
4094         editEnd();
4095     }
4096     return remove;
4097 }
4098 
4099 /*
4100   Remove from the beginning of each line of the
4101   selection a start comment line mark.
4102 */
4103 bool KTextEditor::DocumentPrivate::removeStartLineCommentFromSelection(KTextEditor::Range selection, int attrib, bool toggleComment)
4104 {
4105     const QString shortCommentMark = highlight()->getCommentSingleLineStart(attrib);
4106     const QString longCommentMark = shortCommentMark + QLatin1Char(' ');
4107 
4108     const int startLine = selection.start().line();
4109     int endLine = selection.end().line();
4110 
4111     if ((selection.end().column() == 0) && (endLine > 0)) {
4112         endLine--;
4113     }
4114 
4115     bool removed = false;
4116 
4117     // If we are toggling, we check whether all lines in the selection start
4118     // with a comment. If they don't, we return early
4119     // NOTE: When toggling, we only remove comments if all lines in the selection
4120     // are comments, otherwise we recomment the comments
4121     if (toggleComment) {
4122         bool allLinesAreCommented = true;
4123         for (int line = endLine; line >= startLine; line--) {
4124             const auto ln = m_buffer->plainLine(line);
4125             const QString &text = ln.text();
4126             // Empty lines in between comments is ok
4127             if (text.isEmpty()) {
4128                 continue;
4129             }
4130             QStringView textView(text.data(), text.size());
4131             // Must trim any spaces at the beginning
4132             textView = textView.trimmed();
4133             if (!textView.startsWith(shortCommentMark) && !textView.startsWith(longCommentMark)) {
4134                 allLinesAreCommented = false;
4135                 break;
4136             }
4137         }
4138         if (!allLinesAreCommented) {
4139             return false;
4140         }
4141     }
4142 
4143     editStart();
4144 
4145     // For each line of the selection
4146     for (int z = endLine; z >= startLine; z--) {
4147         // Try to remove the long comment mark first
4148         removed = (removeStringFromBeginning(z, longCommentMark) || removeStringFromBeginning(z, shortCommentMark) || removed);
4149     }
4150 
4151     editEnd();
4152     // selection automatically updated (MovingRange)
4153 
4154     return removed;
4155 }
4156 
4157 void KTextEditor::DocumentPrivate::commentSelection(KTextEditor::Range selection, KTextEditor::Cursor c, bool blockSelect, CommentType changeType)
4158 {
4159     const bool hasSelection = !selection.isEmpty();
4160     int selectionCol = 0;
4161 
4162     if (hasSelection) {
4163         selectionCol = selection.start().column();
4164     }
4165     const int line = c.line();
4166 
4167     int startAttrib = 0;
4168     Kate::TextLine ln = kateTextLine(line);
4169 
4170     if (selectionCol < ln.length()) {
4171         startAttrib = ln.attribute(selectionCol);
4172     } else if (!ln.attributesList().empty()) {
4173         startAttrib = ln.attributesList().back().attributeValue;
4174     }
4175 
4176     bool hasStartLineCommentMark = !(highlight()->getCommentSingleLineStart(startAttrib).isEmpty());
4177     bool hasStartStopCommentMark = (!(highlight()->getCommentStart(startAttrib).isEmpty()) && !(highlight()->getCommentEnd(startAttrib).isEmpty()));
4178 
4179     if (changeType == Comment) {
4180         if (!hasSelection) {
4181             if (hasStartLineCommentMark) {
4182                 addStartLineCommentToSingleLine(line, startAttrib);
4183             } else if (hasStartStopCommentMark) {
4184                 addStartStopCommentToSingleLine(line, startAttrib);
4185             }
4186         } else {
4187             // anders: prefer single line comment to avoid nesting probs
4188             // If the selection starts after first char in the first line
4189             // or ends before the last char of the last line, we may use
4190             // multiline comment markers.
4191             // TODO We should try to detect nesting.
4192             //    - if selection ends at col 0, most likely she wanted that
4193             // line ignored
4194             const KTextEditor::Range sel = selection;
4195             if (hasStartStopCommentMark
4196                 && (!hasStartLineCommentMark
4197                     || ((sel.start().column() > m_buffer->plainLine(sel.start().line()).firstChar())
4198                         || (sel.end().column() > 0 && sel.end().column() < (m_buffer->plainLine(sel.end().line()).length()))))) {
4199                 addStartStopCommentToSelection(selection, blockSelect, startAttrib);
4200             } else if (hasStartLineCommentMark) {
4201                 addStartLineCommentToSelection(selection, startAttrib);
4202             }
4203         }
4204     } else { // uncomment
4205         bool removed = false;
4206         const bool toggleComment = changeType == ToggleComment;
4207         if (!hasSelection) {
4208             removed = (hasStartLineCommentMark && removeStartLineCommentFromSingleLine(line, startAttrib))
4209                 || (hasStartStopCommentMark && removeStartStopCommentFromSingleLine(line, startAttrib));
4210         } else {
4211             // anders: this seems like it will work with above changes :)
4212             removed = (hasStartStopCommentMark && removeStartStopCommentFromSelection(selection, startAttrib))
4213                 || (hasStartLineCommentMark && removeStartLineCommentFromSelection(selection, startAttrib, toggleComment));
4214         }
4215 
4216         // recursive call for toggle comment
4217         if (!removed && toggleComment) {
4218             commentSelection(selection, c, blockSelect, Comment);
4219         }
4220     }
4221 }
4222 
4223 /*
4224   Comment or uncomment the selection or the current
4225   line if there is no selection.
4226 */
4227 void KTextEditor::DocumentPrivate::comment(KTextEditor::ViewPrivate *v, uint line, uint column, CommentType change)
4228 {
4229     // skip word wrap bug #105373
4230     const bool skipWordWrap = wordWrap();
4231     if (skipWordWrap) {
4232         setWordWrap(false);
4233     }
4234 
4235     editStart();
4236 
4237     if (v->selection()) {
4238         const auto &cursors = v->secondaryCursors();
4239         for (const auto &c : cursors) {
4240             if (!c.range) {
4241                 continue;
4242             }
4243             commentSelection(c.range->toRange(), c.cursor(), false, change);
4244         }
4245         KTextEditor::Cursor c(line, column);
4246         commentSelection(v->selectionRange(), c, v->blockSelection(), change);
4247     } else {
4248         const auto &cursors = v->secondaryCursors();
4249         for (const auto &c : cursors) {
4250             commentSelection({}, c.cursor(), false, change);
4251         }
4252         commentSelection({}, KTextEditor::Cursor(line, column), false, change);
4253     }
4254 
4255     editEnd();
4256 
4257     if (skipWordWrap) {
4258         setWordWrap(true); // see begin of function ::comment (bug #105373)
4259     }
4260 }
4261 
4262 void KTextEditor::DocumentPrivate::transformCursorOrRange(KTextEditor::ViewPrivate *v,
4263                                                           KTextEditor::Cursor c,
4264                                                           KTextEditor::Range selection,
4265                                                           KTextEditor::DocumentPrivate::TextTransform t)
4266 {
4267     if (v->selection()) {
4268         editStart();
4269 
4270         KTextEditor::Range range(selection.start(), 0);
4271         while (range.start().line() <= selection.end().line()) {
4272             int start = 0;
4273             int end = lineLength(range.start().line());
4274 
4275             if (range.start().line() == selection.start().line() || v->blockSelection()) {
4276                 start = selection.start().column();
4277             }
4278 
4279             if (range.start().line() == selection.end().line() || v->blockSelection()) {
4280                 end = selection.end().column();
4281             }
4282 
4283             if (start > end) {
4284                 int swapCol = start;
4285                 start = end;
4286                 end = swapCol;
4287             }
4288             range.setStart(KTextEditor::Cursor(range.start().line(), start));
4289             range.setEnd(KTextEditor::Cursor(range.end().line(), end));
4290 
4291             QString s = text(range);
4292             QString old = s;
4293 
4294             if (t == Uppercase) {
4295                 // honor locale, see bug 467104
4296                 s = QLocale().toUpper(s);
4297             } else if (t == Lowercase) {
4298                 // honor locale, see bug 467104
4299                 s = QLocale().toLower(s);
4300             } else { // Capitalize
4301                 Kate::TextLine l = m_buffer->plainLine(range.start().line());
4302                 int p(0);
4303                 while (p < s.length()) {
4304                     // If bol or the character before is not in a word, up this one:
4305                     // 1. if both start and p is 0, upper char.
4306                     // 2. if blockselect or first line, and p == 0 and start-1 is not in a word, upper
4307                     // 3. if p-1 is not in a word, upper.
4308                     if ((!range.start().column() && !p)
4309                         || ((range.start().line() == selection.start().line() || v->blockSelection()) && !p
4310                             && !highlight()->isInWord(l.at(range.start().column() - 1)))
4311                         || (p && !highlight()->isInWord(s.at(p - 1)))) {
4312                         s[p] = s.at(p).toUpper();
4313                     }
4314                     p++;
4315                 }
4316             }
4317 
4318             if (s != old) {
4319                 removeText(range);
4320                 insertText(range.start(), s);
4321             }
4322 
4323             range.setBothLines(range.start().line() + 1);
4324         }
4325 
4326         editEnd();
4327     } else { // no selection
4328         editStart();
4329 
4330         // get cursor
4331         KTextEditor::Cursor cursor = c;
4332 
4333         QString old = text(KTextEditor::Range(cursor, 1));
4334         QString s;
4335         switch (t) {
4336         case Uppercase:
4337             s = old.toUpper();
4338             break;
4339         case Lowercase:
4340             s = old.toLower();
4341             break;
4342         case Capitalize: {
4343             Kate::TextLine l = m_buffer->plainLine(cursor.line());
4344             while (cursor.column() > 0 && highlight()->isInWord(l.at(cursor.column() - 1), l.attribute(cursor.column() - 1))) {
4345                 cursor.setColumn(cursor.column() - 1);
4346             }
4347             old = text(KTextEditor::Range(cursor, 1));
4348             s = old.toUpper();
4349         } break;
4350         default:
4351             break;
4352         }
4353 
4354         removeText(KTextEditor::Range(cursor, 1));
4355         insertText(cursor, s);
4356 
4357         editEnd();
4358     }
4359 }
4360 
4361 void KTextEditor::DocumentPrivate::transform(KTextEditor::ViewPrivate *v, const KTextEditor::Cursor c, KTextEditor::DocumentPrivate::TextTransform t)
4362 {
4363     editStart();
4364 
4365     if (v->selection()) {
4366         const auto &cursors = v->secondaryCursors();
4367         for (const auto &c : cursors) {
4368             if (!c.range) {
4369                 continue;
4370             }
4371             auto pos = c.pos->toCursor();
4372             transformCursorOrRange(v, c.anchor, c.range->toRange(), t);
4373             c.pos->setPosition(pos);
4374         }
4375         // cache the selection and cursor, so we can be sure to restore.
4376         const auto selRange = v->selectionRange();
4377         transformCursorOrRange(v, c, v->selectionRange(), t);
4378         v->setSelection(selRange);
4379         v->setCursorPosition(c);
4380     } else { // no selection
4381         const auto &secondaryCursors = v->secondaryCursors();
4382         for (const auto &c : secondaryCursors) {
4383             transformCursorOrRange(v, c.cursor(), {}, t);
4384         }
4385         transformCursorOrRange(v, c, {}, t);
4386     }
4387 
4388     editEnd();
4389 }
4390 
4391 void KTextEditor::DocumentPrivate::joinLines(uint first, uint last)
4392 {
4393     //   if ( first == last ) last += 1;
4394     editStart();
4395     int line(first);
4396     while (first < last) {
4397         if (line >= lines() || line + 1 >= lines()) {
4398             editEnd();
4399             return;
4400         }
4401 
4402         // Normalize the whitespace in the joined lines by making sure there's
4403         // always exactly one space between the joined lines
4404         // This cannot be done in editUnwrapLine, because we do NOT want this
4405         // behavior when deleting from the start of a line, just when explicitly
4406         // calling the join command
4407         Kate::TextLine l = kateTextLine(line);
4408         Kate::TextLine tl = kateTextLine(line + 1);
4409 
4410         int pos = tl.firstChar();
4411         if (pos >= 0) {
4412             if (pos != 0) {
4413                 editRemoveText(line + 1, 0, pos);
4414             }
4415             if (!(l.length() == 0 || l.at(l.length() - 1).isSpace())) {
4416                 editInsertText(line + 1, 0, QStringLiteral(" "));
4417             }
4418         } else {
4419             // Just remove the whitespace and let Kate handle the rest
4420             editRemoveText(line + 1, 0, tl.length());
4421         }
4422 
4423         editUnWrapLine(line);
4424         first++;
4425     }
4426     editEnd();
4427 }
4428 
4429 void KTextEditor::DocumentPrivate::tagLines(KTextEditor::LineRange lineRange)
4430 {
4431     for (auto view : std::as_const(m_views)) {
4432         static_cast<ViewPrivate *>(view)->tagLines(lineRange, true);
4433     }
4434 }
4435 
4436 void KTextEditor::DocumentPrivate::tagLine(int line)
4437 {
4438     tagLines({line, line});
4439 }
4440 
4441 void KTextEditor::DocumentPrivate::repaintViews(bool paintOnlyDirty)
4442 {
4443     for (auto view : std::as_const(m_views)) {
4444         static_cast<ViewPrivate *>(view)->repaintText(paintOnlyDirty);
4445     }
4446 }
4447 
4448 /*
4449    Bracket matching uses the following algorithm:
4450    If in overwrite mode, match the bracket currently underneath the cursor.
4451    Otherwise, if the character to the left is a bracket,
4452    match it. Otherwise if the character to the right of the cursor is a
4453    bracket, match it. Otherwise, don't match anything.
4454 */
4455 KTextEditor::Range KTextEditor::DocumentPrivate::findMatchingBracket(const KTextEditor::Cursor start, int maxLines)
4456 {
4457     if (maxLines < 0 || start.line() < 0 || start.line() >= lines()) {
4458         return KTextEditor::Range::invalid();
4459     }
4460 
4461     Kate::TextLine textLine = m_buffer->plainLine(start.line());
4462     KTextEditor::Range range(start, start);
4463     const QChar right = textLine.at(range.start().column());
4464     const QChar left = textLine.at(range.start().column() - 1);
4465     QChar bracket;
4466 
4467     if (config()->ovr()) {
4468         if (isBracket(right)) {
4469             bracket = right;
4470         } else {
4471             return KTextEditor::Range::invalid();
4472         }
4473     } else if (isBracket(right)) {
4474         bracket = right;
4475     } else if (isBracket(left)) {
4476         range.setStart(KTextEditor::Cursor(range.start().line(), range.start().column() - 1));
4477         bracket = left;
4478     } else {
4479         return KTextEditor::Range::invalid();
4480     }
4481 
4482     const QChar opposite = matchingBracket(bracket);
4483     if (opposite.isNull()) {
4484         return KTextEditor::Range::invalid();
4485     }
4486 
4487     const int searchDir = isStartBracket(bracket) ? 1 : -1;
4488     uint nesting = 0;
4489 
4490     const int minLine = qMax(range.start().line() - maxLines, 0);
4491     const int maxLine = qMin(range.start().line() + maxLines, documentEnd().line());
4492 
4493     range.setEnd(range.start());
4494     KTextEditor::DocumentCursor cursor(this);
4495     cursor.setPosition(range.start());
4496     int validAttr = kateTextLine(cursor.line()).attribute(cursor.column());
4497 
4498     while (cursor.line() >= minLine && cursor.line() <= maxLine) {
4499         if (!cursor.move(searchDir)) {
4500             return KTextEditor::Range::invalid();
4501         }
4502 
4503         Kate::TextLine textLine = kateTextLine(cursor.line());
4504         if (textLine.attribute(cursor.column()) == validAttr) {
4505             // Check for match
4506             QChar c = textLine.at(cursor.column());
4507             if (c == opposite) {
4508                 if (nesting == 0) {
4509                     if (searchDir > 0) { // forward
4510                         range.setEnd(cursor.toCursor());
4511                     } else {
4512                         range.setStart(cursor.toCursor());
4513                     }
4514                     return range;
4515                 }
4516                 nesting--;
4517             } else if (c == bracket) {
4518                 nesting++;
4519             }
4520         }
4521     }
4522 
4523     return KTextEditor::Range::invalid();
4524 }
4525 
4526 // helper: remove \r and \n from visible document name (bug #170876)
4527 inline static QString removeNewLines(const QString &str)
4528 {
4529     QString tmp(str);
4530     return tmp.replace(QLatin1String("\r\n"), QLatin1String(" ")).replace(QLatin1Char('\r'), QLatin1Char(' ')).replace(QLatin1Char('\n'), QLatin1Char(' '));
4531 }
4532 
4533 void KTextEditor::DocumentPrivate::updateDocName()
4534 {
4535     // if the name is set, and starts with FILENAME, it should not be changed!
4536     if (!url().isEmpty() && (m_docName == removeNewLines(url().fileName()) || m_docName.startsWith(removeNewLines(url().fileName()) + QLatin1String(" (")))) {
4537         return;
4538     }
4539 
4540     int count = -1;
4541 
4542     std::vector<KTextEditor::DocumentPrivate *> docsWithSameName;
4543 
4544     const auto docs = KTextEditor::EditorPrivate::self()->documents();
4545     for (KTextEditor::Document *kteDoc : docs) {
4546         auto doc = static_cast<KTextEditor::DocumentPrivate *>(kteDoc);
4547         if ((doc != this) && (doc->url().fileName() == url().fileName())) {
4548             if (doc->m_docNameNumber > count) {
4549                 count = doc->m_docNameNumber;
4550             }
4551             docsWithSameName.push_back(doc);
4552         }
4553     }
4554 
4555     m_docNameNumber = count + 1;
4556 
4557     QString oldName = m_docName;
4558     m_docName = removeNewLines(url().fileName());
4559 
4560     m_isUntitled = m_docName.isEmpty();
4561 
4562     if (!m_isUntitled && !docsWithSameName.empty()) {
4563         docsWithSameName.push_back(this);
4564         uniquifyDocNames(docsWithSameName);
4565         return;
4566     }
4567 
4568     if (m_isUntitled) {
4569         m_docName = i18n("Untitled");
4570     }
4571 
4572     if (m_docNameNumber > 0) {
4573         m_docName = QString(m_docName + QLatin1String(" (%1)")).arg(m_docNameNumber + 1);
4574     }
4575 
4576     // avoid to emit this, if name doesn't change!
4577     if (oldName != m_docName) {
4578         Q_EMIT documentNameChanged(this);
4579     }
4580 }
4581 
4582 /**
4583  * Find the shortest prefix for doc from urls
4584  * @p urls contains a list of urls
4585  *  - /path/to/some/file
4586  *  - /some/to/path/file
4587  *
4588  * we find the shortest path prefix which can be used to
4589  * identify @p doc
4590  *
4591  * for above, it will return "some" for first and "path" for second
4592  */
4593 static QString shortestPrefix(const std::vector<QString> &urls, KTextEditor::DocumentPrivate *doc)
4594 {
4595     const auto url = doc->url().toString(QUrl::NormalizePathSegments | QUrl::PreferLocalFile);
4596     int lastSlash = url.lastIndexOf(QLatin1Char('/'));
4597     if (lastSlash == -1) {
4598         // just filename?
4599         return url;
4600     }
4601     int fileNameStart = lastSlash;
4602 
4603     lastSlash--;
4604     lastSlash = url.lastIndexOf(QLatin1Char('/'), lastSlash);
4605     if (lastSlash == -1) {
4606         // already too short?
4607         lastSlash = 0;
4608         return url.mid(lastSlash, fileNameStart);
4609     }
4610 
4611     QStringView urlView = url;
4612     QStringView urlv = url;
4613     urlv = urlv.mid(lastSlash);
4614 
4615     for (size_t i = 0; i < urls.size(); ++i) {
4616         if (urls[i] == url) {
4617             continue;
4618         }
4619 
4620         if (urls[i].endsWith(urlv)) {
4621             lastSlash = url.lastIndexOf(QLatin1Char('/'), lastSlash - 1);
4622             if (lastSlash <= 0) {
4623                 // reached end if we either found no / or found the slash at the start
4624                 return url.mid(0, fileNameStart);
4625             }
4626             // else update urlv and match again from start
4627             urlv = urlView.mid(lastSlash);
4628             i = -1;
4629         }
4630     }
4631 
4632     return url.mid(lastSlash + 1, fileNameStart - (lastSlash + 1));
4633 }
4634 
4635 void KTextEditor::DocumentPrivate::uniquifyDocNames(const std::vector<KTextEditor::DocumentPrivate *> &docs)
4636 {
4637     std::vector<QString> paths;
4638     paths.reserve(docs.size());
4639     std::transform(docs.begin(), docs.end(), std::back_inserter(paths), [](const KTextEditor::DocumentPrivate *d) {
4640         return d->url().toString(QUrl::NormalizePathSegments | QUrl::PreferLocalFile);
4641     });
4642 
4643     for (const auto doc : docs) {
4644         const QString prefix = shortestPrefix(paths, doc);
4645         const QString fileName = doc->url().fileName();
4646         const QString oldName = doc->m_docName;
4647 
4648         if (!prefix.isEmpty()) {
4649             doc->m_docName = fileName + QStringLiteral(" - ") + prefix;
4650         } else {
4651             doc->m_docName = fileName;
4652         }
4653 
4654         if (doc->m_docName != oldName) {
4655             Q_EMIT doc->documentNameChanged(doc);
4656         }
4657     }
4658 }
4659 
4660 void KTextEditor::DocumentPrivate::slotModifiedOnDisk(KTextEditor::View * /*v*/)
4661 {
4662     if (url().isEmpty() || !m_modOnHd) {
4663         return;
4664     }
4665 
4666     if (!isModified() && isAutoReload()) {
4667         onModOnHdAutoReload();
4668         return;
4669     }
4670 
4671     if (!m_fileChangedDialogsActivated || m_modOnHdHandler) {
4672         return;
4673     }
4674 
4675     // don't ask the user again and again the same thing
4676     if (m_modOnHdReason == m_prevModOnHdReason) {
4677         return;
4678     }
4679     m_prevModOnHdReason = m_modOnHdReason;
4680 
4681     m_modOnHdHandler = new KateModOnHdPrompt(this, m_modOnHdReason, reasonedMOHString());
4682     connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::saveAsTriggered, this, &DocumentPrivate::onModOnHdSaveAs);
4683     connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::closeTriggered, this, &DocumentPrivate::onModOnHdClose);
4684     connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::reloadTriggered, this, &DocumentPrivate::onModOnHdReload);
4685     connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::autoReloadTriggered, this, &DocumentPrivate::onModOnHdAutoReload);
4686     connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::ignoreTriggered, this, &DocumentPrivate::onModOnHdIgnore);
4687 }
4688 
4689 void KTextEditor::DocumentPrivate::onModOnHdSaveAs()
4690 {
4691     m_modOnHd = false;
4692     const QUrl res = getSaveFileUrl(i18n("Save File"));
4693     if (!res.isEmpty()) {
4694         if (!saveAs(res)) {
4695             KMessageBox::error(dialogParent(), i18n("Save failed"));
4696             m_modOnHd = true;
4697         } else {
4698             delete m_modOnHdHandler;
4699             m_prevModOnHdReason = OnDiskUnmodified;
4700             Q_EMIT modifiedOnDisk(this, false, OnDiskUnmodified);
4701         }
4702     } else { // the save as dialog was canceled, we are still modified on disk
4703         m_modOnHd = true;
4704     }
4705 }
4706 
4707 void KTextEditor::DocumentPrivate::onModOnHdClose()
4708 {
4709     // avoid prompt in closeUrl()
4710     m_fileChangedDialogsActivated = false;
4711 
4712     // close the file without prompt confirmation
4713     closeUrl();
4714 
4715     // Useful for applications that provide the necessary interface
4716     // delay this, otherwise we delete ourself during e.g. event handling + deleting this is undefined!
4717     // see e.g. bug 433180
4718     QTimer::singleShot(0, this, [this]() {
4719         KTextEditor::EditorPrivate::self()->application()->closeDocument(this);
4720     });
4721 }
4722 
4723 void KTextEditor::DocumentPrivate::onModOnHdReload()
4724 {
4725     m_modOnHd = false;
4726     m_prevModOnHdReason = OnDiskUnmodified;
4727     Q_EMIT modifiedOnDisk(this, false, OnDiskUnmodified);
4728 
4729     // MUST Clear Undo/Redo here because by the time we get here
4730     // the checksum has already been updated and the undo manager
4731     // sees the new checksum and thinks nothing changed and loads
4732     // a bad undo history resulting in funny things.
4733     m_undoManager->clearUndo();
4734     m_undoManager->clearRedo();
4735 
4736     documentReload();
4737     delete m_modOnHdHandler;
4738 }
4739 
4740 void KTextEditor::DocumentPrivate::autoReloadToggled(bool b)
4741 {
4742     m_autoReloadMode->setChecked(b);
4743     if (b) {
4744         connect(&m_modOnHdTimer, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload);
4745     } else {
4746         disconnect(&m_modOnHdTimer, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload);
4747     }
4748 }
4749 
4750 bool KTextEditor::DocumentPrivate::isAutoReload()
4751 {
4752     return m_autoReloadMode->isChecked();
4753 }
4754 
4755 void KTextEditor::DocumentPrivate::delayAutoReload()
4756 {
4757     if (isAutoReload()) {
4758         m_autoReloadThrottle.start();
4759     }
4760 }
4761 
4762 void KTextEditor::DocumentPrivate::onModOnHdAutoReload()
4763 {
4764     if (m_modOnHdHandler) {
4765         delete m_modOnHdHandler;
4766         autoReloadToggled(true);
4767     }
4768 
4769     if (!isAutoReload()) {
4770         return;
4771     }
4772 
4773     if (m_modOnHd && !m_reloading && !m_autoReloadThrottle.isActive()) {
4774         m_modOnHd = false;
4775         m_prevModOnHdReason = OnDiskUnmodified;
4776         Q_EMIT modifiedOnDisk(this, false, OnDiskUnmodified);
4777 
4778         // MUST clear undo/redo. This comes way after KDirWatch signaled us
4779         // and the checksum is already updated by the time we start reload.
4780         m_undoManager->clearUndo();
4781         m_undoManager->clearRedo();
4782 
4783         documentReload();
4784         m_autoReloadThrottle.start();
4785     }
4786 }
4787 
4788 void KTextEditor::DocumentPrivate::onModOnHdIgnore()
4789 {
4790     // ignore as long as m_prevModOnHdReason == m_modOnHdReason
4791     delete m_modOnHdHandler;
4792 }
4793 
4794 void KTextEditor::DocumentPrivate::setModifiedOnDisk(ModifiedOnDiskReason reason)
4795 {
4796     m_modOnHdReason = reason;
4797     m_modOnHd = (reason != OnDiskUnmodified);
4798     Q_EMIT modifiedOnDisk(this, (reason != OnDiskUnmodified), reason);
4799 }
4800 
4801 class KateDocumentTmpMark
4802 {
4803 public:
4804     QString line;
4805     KTextEditor::Mark mark;
4806 };
4807 
4808 void KTextEditor::DocumentPrivate::setModifiedOnDiskWarning(bool on)
4809 {
4810     m_fileChangedDialogsActivated = on;
4811 }
4812 
4813 bool KTextEditor::DocumentPrivate::documentReload()
4814 {
4815     if (url().isEmpty()) {
4816         return false;
4817     }
4818 
4819     // If we are modified externally clear undo and redo
4820     // Why:
4821     // Our checksum() is already updated at this point by
4822     // slotDelayedHandleModOnHd() so we will end up restoring
4823     // undo because undo manager relies on checksum() to check
4824     // if the doc is same or different. Hence any checksum matching
4825     // is useless at this point and we must clear it here
4826     if (m_modOnHd) {
4827         m_undoManager->clearUndo();
4828         m_undoManager->clearRedo();
4829     }
4830 
4831     // typically, the message for externally modified files is visible. Since it
4832     // does not make sense showing an additional dialog, just hide the message.
4833     delete m_modOnHdHandler;
4834 
4835     Q_EMIT aboutToReload(this);
4836 
4837     QVarLengthArray<KateDocumentTmpMark> tmp;
4838     tmp.reserve(m_marks.size());
4839     std::transform(m_marks.cbegin(), m_marks.cend(), std::back_inserter(tmp), [this](KTextEditor::Mark *mark) {
4840         return KateDocumentTmpMark{line(mark->line), *mark};
4841     });
4842 
4843     // Remember some settings which may changed at reload
4844     const QString oldMode = mode();
4845     const bool modeByUser = m_fileTypeSetByUser;
4846     const QString oldHlMode = highlightingMode();
4847     const bool hlByUser = m_hlSetByUser;
4848 
4849     m_storedVariables.clear();
4850 
4851     // save cursor positions for all views
4852     QVarLengthArray<std::pair<KTextEditor::ViewPrivate *, KTextEditor::Cursor>, 4> cursorPositions;
4853     std::transform(m_views.cbegin(), m_views.cend(), std::back_inserter(cursorPositions), [](KTextEditor::View *v) {
4854         return std::pair<KTextEditor::ViewPrivate *, KTextEditor::Cursor>(static_cast<ViewPrivate *>(v), v->cursorPosition());
4855     });
4856 
4857     // clear multicursors
4858     // FIXME: Restore multicursors, at least for the case where doc is unmodified
4859     for (auto *view : m_views) {
4860         static_cast<ViewPrivate *>(view)->clearSecondaryCursors();
4861         // Clear folding state if we are modified on hd
4862         if (m_modOnHd) {
4863             static_cast<ViewPrivate *>(view)->clearFoldingState();
4864         }
4865     }
4866 
4867     m_reloading = true;
4868     KTextEditor::DocumentPrivate::openUrl(url());
4869 
4870     // reset some flags only valid for one reload!
4871     m_userSetEncodingForNextReload = false;
4872 
4873     // restore cursor positions for all views
4874     for (auto v : std::as_const(m_views)) {
4875         setActiveView(v);
4876         auto it = std::find_if(cursorPositions.cbegin(), cursorPositions.cend(), [v](const std::pair<KTextEditor::ViewPrivate *, KTextEditor::Cursor> &p) {
4877             return p.first == v;
4878         });
4879         v->setCursorPosition(it->second);
4880         if (v->isVisible()) {
4881             v->repaint();
4882         }
4883     }
4884 
4885     int z = 0;
4886     const int lines = this->lines();
4887     for (const auto &tmpMark : tmp) {
4888         if (z < lines) {
4889             if (tmpMark.line == line(tmpMark.mark.line)) {
4890                 setMark(tmpMark.mark.line, tmpMark.mark.type);
4891             }
4892         }
4893         ++z;
4894     }
4895 
4896     // Restore old settings
4897     if (modeByUser) {
4898         updateFileType(oldMode, true);
4899     }
4900     if (hlByUser) {
4901         setHighlightingMode(oldHlMode);
4902     }
4903 
4904     Q_EMIT reloaded(this);
4905 
4906     return true;
4907 }
4908 
4909 bool KTextEditor::DocumentPrivate::documentSave()
4910 {
4911     if (!url().isValid() || !isReadWrite()) {
4912         return documentSaveAs();
4913     }
4914 
4915     return save();
4916 }
4917 
4918 bool KTextEditor::DocumentPrivate::documentSaveAs()
4919 {
4920     const QUrl saveUrl = getSaveFileUrl(i18n("Save File"));
4921     if (saveUrl.isEmpty()) {
4922         return false;
4923     }
4924 
4925     return saveAs(saveUrl);
4926 }
4927 
4928 bool KTextEditor::DocumentPrivate::documentSaveAsWithEncoding(const QString &encoding)
4929 {
4930     const QUrl saveUrl = getSaveFileUrl(i18n("Save File"));
4931     if (saveUrl.isEmpty()) {
4932         return false;
4933     }
4934 
4935     setEncoding(encoding);
4936     return saveAs(saveUrl);
4937 }
4938 
4939 void KTextEditor::DocumentPrivate::documentSaveCopyAs()
4940 {
4941     const QUrl saveUrl = getSaveFileUrl(i18n("Save Copy of File"));
4942     if (saveUrl.isEmpty()) {
4943         return;
4944     }
4945 
4946     QTemporaryFile *file = new QTemporaryFile();
4947     if (!file->open()) {
4948         return;
4949     }
4950 
4951     if (!m_buffer->saveFile(file->fileName())) {
4952         KMessageBox::error(dialogParent(),
4953                            i18n("The document could not be saved, as it was not possible to write to %1.\n\nCheck that you have write access to this file or "
4954                                 "that enough disk space is available.",
4955                                 this->url().toDisplayString(QUrl::PreferLocalFile)));
4956         return;
4957     }
4958 
4959     // get the right permissions, start with safe default
4960     KIO::StatJob *statJob = KIO::stat(url(), KIO::StatJob::SourceSide, KIO::StatBasic);
4961     KJobWidgets::setWindow(statJob, QApplication::activeWindow());
4962     const auto url = this->url();
4963     connect(statJob, &KIO::StatJob::result, this, [url, file, saveUrl](KJob *j) {
4964         if (auto sj = qobject_cast<KIO::StatJob *>(j)) {
4965             const int permissions = KFileItem(sj->statResult(), url).permissions();
4966             KIO::FileCopyJob *job = KIO::file_copy(QUrl::fromLocalFile(file->fileName()), saveUrl, permissions, KIO::Overwrite);
4967             KJobWidgets::setWindow(job, QApplication::activeWindow());
4968             connect(job, &KIO::FileCopyJob::finished, file, &QTemporaryFile::deleteLater);
4969             job->start();
4970         }
4971     });
4972     statJob->start();
4973 }
4974 
4975 void KTextEditor::DocumentPrivate::setWordWrap(bool on)
4976 {
4977     config()->setWordWrap(on);
4978 }
4979 
4980 bool KTextEditor::DocumentPrivate::wordWrap() const
4981 {
4982     return config()->wordWrap();
4983 }
4984 
4985 void KTextEditor::DocumentPrivate::setWordWrapAt(uint col)
4986 {
4987     config()->setWordWrapAt(col);
4988 }
4989 
4990 unsigned int KTextEditor::DocumentPrivate::wordWrapAt() const
4991 {
4992     return config()->wordWrapAt();
4993 }
4994 
4995 void KTextEditor::DocumentPrivate::setPageUpDownMovesCursor(bool on)
4996 {
4997     config()->setPageUpDownMovesCursor(on);
4998 }
4999 
5000 bool KTextEditor::DocumentPrivate::pageUpDownMovesCursor() const
5001 {
5002     return config()->pageUpDownMovesCursor();
5003 }
5004 // END
5005 
5006 bool KTextEditor::DocumentPrivate::setEncoding(const QString &e)
5007 {
5008     return m_config->setEncoding(e);
5009 }
5010 
5011 QString KTextEditor::DocumentPrivate::encoding() const
5012 {
5013     return m_config->encoding();
5014 }
5015 
5016 void KTextEditor::DocumentPrivate::updateConfig()
5017 {
5018     m_undoManager->updateConfig();
5019 
5020     // switch indenter if needed and update config....
5021     m_indenter->setMode(m_config->indentationMode());
5022     m_indenter->updateConfig();
5023 
5024     // set tab width there, too
5025     m_buffer->setTabWidth(config()->tabWidth());
5026 
5027     // update all views, does tagAll and updateView...
5028     for (auto view : std::as_const(m_views)) {
5029         static_cast<ViewPrivate *>(view)->updateDocumentConfig();
5030     }
5031 
5032     // update on-the-fly spell checking as spell checking defaults might have changes
5033     if (m_onTheFlyChecker) {
5034         m_onTheFlyChecker->updateConfig();
5035     }
5036 
5037     if (config()->autoSave()) {
5038         int interval = config()->autoSaveInterval();
5039         if (interval == 0) {
5040             m_autoSaveTimer.stop();
5041         } else {
5042             m_autoSaveTimer.setInterval(interval * 1000);
5043             if (isModified()) {
5044                 m_autoSaveTimer.start();
5045             }
5046         }
5047     }
5048 
5049     Q_EMIT configChanged(this);
5050 }
5051 
5052 // BEGIN Variable reader
5053 // "local variable" feature by anders, 2003
5054 /* TODO
5055       add config options (how many lines to read, on/off)
5056       add interface for plugins/apps to set/get variables
5057       add view stuff
5058 */
5059 bool KTextEditor::DocumentPrivate::readVariables(bool onlyViewAndRenderer)
5060 {
5061     const bool hasVariableline = [this] {
5062         const QLatin1String s("kate");
5063         if (lines() > 10) {
5064             for (int i = qMax(10, lines() - 10); i < lines(); ++i) {
5065                 if (line(i).contains(s)) {
5066                     return true;
5067                 }
5068             }
5069         }
5070         for (int i = 0; i < qMin(9, lines()); ++i) {
5071             if (line(i).contains(s)) {
5072                 return true;
5073             }
5074         }
5075         return false;
5076     }();
5077     if (!hasVariableline) {
5078         return false;
5079     }
5080 
5081     if (!onlyViewAndRenderer) {
5082         m_config->configStart();
5083     }
5084 
5085     // views!
5086     for (auto view : std::as_const(m_views)) {
5087         auto v = static_cast<ViewPrivate *>(view);
5088         v->config()->configStart();
5089         v->rendererConfig()->configStart();
5090     }
5091     // read a number of lines in the top/bottom of the document
5092     for (int i = 0; i < qMin(9, lines()); ++i) {
5093         readVariableLine(line(i), onlyViewAndRenderer);
5094     }
5095     if (lines() > 10) {
5096         for (int i = qMax(10, lines() - 10); i < lines(); i++) {
5097             readVariableLine(line(i), onlyViewAndRenderer);
5098         }
5099     }
5100 
5101     if (!onlyViewAndRenderer) {
5102         m_config->configEnd();
5103     }
5104 
5105     for (auto view : std::as_const(m_views)) {
5106         auto v = static_cast<ViewPrivate *>(view);
5107         v->config()->configEnd();
5108         v->rendererConfig()->configEnd();
5109     }
5110     return true;
5111 }
5112 
5113 void KTextEditor::DocumentPrivate::readVariableLine(const QString &t, bool onlyViewAndRenderer)
5114 {
5115     static const QRegularExpression kvLine(QStringLiteral("kate:(.*)"));
5116     static const QRegularExpression kvLineWildcard(QStringLiteral("kate-wildcard\\((.*)\\):(.*)"));
5117     static const QRegularExpression kvLineMime(QStringLiteral("kate-mimetype\\((.*)\\):(.*)"));
5118     static const QRegularExpression kvVar(QStringLiteral("([\\w\\-]+)\\s+([^;]+)"));
5119 
5120     // simple check first, no regex
5121     // no kate inside, no vars, simple...
5122     if (!t.contains(QLatin1String("kate"))) {
5123         return;
5124     }
5125 
5126     // found vars, if any
5127     QString s;
5128 
5129     // now, try first the normal ones
5130     auto match = kvLine.match(t);
5131     if (match.hasMatch()) {
5132         s = match.captured(1);
5133 
5134         // qCDebug(LOG_KTE) << "normal variable line kate: matched: " << s;
5135     } else if ((match = kvLineWildcard.match(t)).hasMatch()) { // regex given
5136         const QStringList wildcards(match.captured(1).split(QLatin1Char(';'), Qt::SkipEmptyParts));
5137         const QString nameOfFile = url().fileName();
5138         const QString pathOfFile = url().path();
5139 
5140         bool found = false;
5141         for (const QString &pattern : wildcards) {
5142             // wildcard with path match, bug 453541, check for /
5143             // in that case we do some not anchored matching
5144             const bool matchPath = pattern.contains(QLatin1Char('/'));
5145             const QRegularExpression wildcard(QRegularExpression::wildcardToRegularExpression(pattern,
5146                                                                                               matchPath ? QRegularExpression::UnanchoredWildcardConversion
5147                                                                                                         : QRegularExpression::DefaultWildcardConversion));
5148             found = wildcard.match(matchPath ? pathOfFile : nameOfFile).hasMatch();
5149             if (found) {
5150                 break;
5151             }
5152         }
5153 
5154         // nothing usable found...
5155         if (!found) {
5156             return;
5157         }
5158 
5159         s = match.captured(2);
5160 
5161         // qCDebug(LOG_KTE) << "guarded variable line kate-wildcard: matched: " << s;
5162     } else if ((match = kvLineMime.match(t)).hasMatch()) { // mime-type given
5163         const QStringList types(match.captured(1).split(QLatin1Char(';'), Qt::SkipEmptyParts));
5164 
5165         // no matching type found
5166         if (!types.contains(mimeType())) {
5167             return;
5168         }
5169 
5170         s = match.captured(2);
5171 
5172         // qCDebug(LOG_KTE) << "guarded variable line kate-mimetype: matched: " << s;
5173     } else { // nothing found
5174         return;
5175     }
5176 
5177     // view variable names
5178     static const auto vvl = {
5179         QLatin1String("dynamic-word-wrap"),
5180         QLatin1String("dynamic-word-wrap-indicators"),
5181         QLatin1String("line-numbers"),
5182         QLatin1String("icon-border"),
5183         QLatin1String("folding-markers"),
5184         QLatin1String("folding-preview"),
5185         QLatin1String("bookmark-sorting"),
5186         QLatin1String("auto-center-lines"),
5187         QLatin1String("icon-bar-color"),
5188         QLatin1String("scrollbar-minimap"),
5189         QLatin1String("scrollbar-preview"),
5190         QLatin1String("enter-to-insert-completion")
5191         // renderer
5192         ,
5193         QLatin1String("background-color"),
5194         QLatin1String("selection-color"),
5195         QLatin1String("current-line-color"),
5196         QLatin1String("bracket-highlight-color"),
5197         QLatin1String("word-wrap-marker-color"),
5198         QLatin1String("font"),
5199         QLatin1String("font-size"),
5200         QLatin1String("scheme"),
5201     };
5202     int spaceIndent = -1; // for backward compatibility; see below
5203     bool replaceTabsSet = false;
5204     int startPos(0);
5205 
5206     QString var;
5207     QString val;
5208     while ((match = kvVar.match(s, startPos)).hasMatch()) {
5209         startPos = match.capturedEnd(0);
5210         var = match.captured(1);
5211         val = match.captured(2).trimmed();
5212         bool state; // store booleans here
5213         int n; // store ints here
5214 
5215         // only apply view & renderer config stuff
5216         if (onlyViewAndRenderer) {
5217             if (contains(vvl, var)) { // FIXME define above
5218                 setViewVariable(var, val);
5219             }
5220         } else {
5221             // BOOL  SETTINGS
5222             if (var == QLatin1String("word-wrap") && checkBoolValue(val, &state)) {
5223                 setWordWrap(state); // ??? FIXME CHECK
5224             }
5225             // KateConfig::configFlags
5226             // FIXME should this be optimized to only a few calls? how?
5227             else if (var == QLatin1String("backspace-indents") && checkBoolValue(val, &state)) {
5228                 m_config->setBackspaceIndents(state);
5229             } else if (var == QLatin1String("indent-pasted-text") && checkBoolValue(val, &state)) {
5230                 m_config->setIndentPastedText(state);
5231             } else if (var == QLatin1String("replace-tabs") && checkBoolValue(val, &state)) {
5232                 m_config->setReplaceTabsDyn(state);
5233                 replaceTabsSet = true; // for backward compatibility; see below
5234             } else if (var == QLatin1String("remove-trailing-space") && checkBoolValue(val, &state)) {
5235                 qCWarning(LOG_KTE) << i18n(
5236                     "Using deprecated modeline 'remove-trailing-space'. "
5237                     "Please replace with 'remove-trailing-spaces modified;', see "
5238                     "https://docs.kde.org/?application=katepart&branch=stable5&path=config-variables.html#variable-remove-trailing-spaces");
5239                 m_config->setRemoveSpaces(state ? 1 : 0);
5240             } else if (var == QLatin1String("replace-trailing-space-save") && checkBoolValue(val, &state)) {
5241                 qCWarning(LOG_KTE) << i18n(
5242                     "Using deprecated modeline 'replace-trailing-space-save'. "
5243                     "Please replace with 'remove-trailing-spaces all;', see "
5244                     "https://docs.kde.org/?application=katepart&branch=stable5&path=config-variables.html#variable-remove-trailing-spaces");
5245                 m_config->setRemoveSpaces(state ? 2 : 0);
5246             } else if (var == QLatin1String("overwrite-mode") && checkBoolValue(val, &state)) {
5247                 m_config->setOvr(state);
5248             } else if (var == QLatin1String("keep-extra-spaces") && checkBoolValue(val, &state)) {
5249                 m_config->setKeepExtraSpaces(state);
5250             } else if (var == QLatin1String("tab-indents") && checkBoolValue(val, &state)) {
5251                 m_config->setTabIndents(state);
5252             } else if (var == QLatin1String("show-tabs") && checkBoolValue(val, &state)) {
5253                 m_config->setShowTabs(state);
5254             } else if (var == QLatin1String("show-trailing-spaces") && checkBoolValue(val, &state)) {
5255                 m_config->setShowSpaces(state ? KateDocumentConfig::Trailing : KateDocumentConfig::None);
5256             } else if (var == QLatin1String("space-indent") && checkBoolValue(val, &state)) {
5257                 // this is for backward compatibility; see below
5258                 spaceIndent = state;
5259             } else if (var == QLatin1String("smart-home") && checkBoolValue(val, &state)) {
5260                 m_config->setSmartHome(state);
5261             } else if (var == QLatin1String("newline-at-eof") && checkBoolValue(val, &state)) {
5262                 m_config->setNewLineAtEof(state);
5263             }
5264 
5265             // INTEGER SETTINGS
5266             else if (var == QLatin1String("tab-width") && checkIntValue(val, &n)) {
5267                 m_config->setTabWidth(n);
5268             } else if (var == QLatin1String("indent-width") && checkIntValue(val, &n)) {
5269                 m_config->setIndentationWidth(n);
5270             } else if (var == QLatin1String("indent-mode")) {
5271                 m_config->setIndentationMode(val);
5272             } else if (var == QLatin1String("word-wrap-column") && checkIntValue(val, &n) && n > 0) { // uint, but hard word wrap at 0 will be no fun ;)
5273                 m_config->setWordWrapAt(n);
5274             }
5275 
5276             // STRING SETTINGS
5277             else if (var == QLatin1String("eol") || var == QLatin1String("end-of-line")) {
5278                 const auto l = {QLatin1String("unix"), QLatin1String("dos"), QLatin1String("mac")};
5279                 if ((n = indexOf(l, val.toLower())) != -1) {
5280                     // set eol + avoid that it is overwritten by auto-detection again!
5281                     // this fixes e.g. .kateconfig files with // kate: eol dos; to work, bug 365705
5282                     m_config->setEol(n);
5283                     m_config->setAllowEolDetection(false);
5284                 }
5285             } else if (var == QLatin1String("bom") || var == QLatin1String("byte-order-mark") || var == QLatin1String("byte-order-marker")) {
5286                 if (checkBoolValue(val, &state)) {
5287                     m_config->setBom(state);
5288                 }
5289             } else if (var == QLatin1String("remove-trailing-spaces")) {
5290                 val = val.toLower();
5291                 if (val == QLatin1String("1") || val == QLatin1String("modified") || val == QLatin1String("mod") || val == QLatin1String("+")) {
5292                     m_config->setRemoveSpaces(1);
5293                 } else if (val == QLatin1String("2") || val == QLatin1String("all") || val == QLatin1String("*")) {
5294                     m_config->setRemoveSpaces(2);
5295                 } else {
5296                     m_config->setRemoveSpaces(0);
5297                 }
5298             } else if (var == QLatin1String("syntax") || var == QLatin1String("hl")) {
5299                 setHighlightingMode(val);
5300             } else if (var == QLatin1String("mode")) {
5301                 setMode(val);
5302             } else if (var == QLatin1String("encoding")) {
5303                 setEncoding(val);
5304             } else if (var == QLatin1String("default-dictionary")) {
5305                 setDefaultDictionary(val);
5306             } else if (var == QLatin1String("automatic-spell-checking") && checkBoolValue(val, &state)) {
5307                 onTheFlySpellCheckingEnabled(state);
5308             }
5309 
5310             // VIEW SETTINGS
5311             else if (contains(vvl, var)) {
5312                 setViewVariable(var, val);
5313             } else {
5314                 m_storedVariables[var] = val;
5315             }
5316         }
5317     }
5318 
5319     // Backward compatibility
5320     // If space-indent was set, but replace-tabs was not set, we assume
5321     // that the user wants to replace tabulators and set that flag.
5322     // If both were set, replace-tabs has precedence.
5323     // At this point spaceIndent is -1 if it was never set,
5324     // 0 if it was set to off, and 1 if it was set to on.
5325     // Note that if onlyViewAndRenderer was requested, spaceIndent is -1.
5326     if (!replaceTabsSet && spaceIndent >= 0) {
5327         m_config->setReplaceTabsDyn(spaceIndent > 0);
5328     }
5329 }
5330 
5331 void KTextEditor::DocumentPrivate::setViewVariable(const QString &var, const QString &val)
5332 {
5333     bool state = false;
5334     int n = 0;
5335     QColor c;
5336     for (auto view : std::as_const(m_views)) {
5337         auto v = static_cast<ViewPrivate *>(view);
5338         // First, try the new config interface
5339         QVariant help(val); // Special treatment to catch "on"/"off"
5340         if (checkBoolValue(val, &state)) {
5341             help = state;
5342         }
5343         if (v->config()->setValue(var, help)) {
5344         } else if (v->rendererConfig()->setValue(var, help)) {
5345             // No success? Go the old way
5346         } else if (var == QLatin1String("dynamic-word-wrap") && checkBoolValue(val, &state)) {
5347             v->config()->setDynWordWrap(state);
5348         } else if (var == QLatin1String("block-selection") && checkBoolValue(val, &state)) {
5349             v->setBlockSelection(state);
5350 
5351             // else if ( var = "dynamic-word-wrap-indicators" )
5352         } else if (var == QLatin1String("icon-bar-color") && checkColorValue(val, c)) {
5353             v->rendererConfig()->setIconBarColor(c);
5354         }
5355         // RENDERER
5356         else if (var == QLatin1String("background-color") && checkColorValue(val, c)) {
5357             v->rendererConfig()->setBackgroundColor(c);
5358         } else if (var == QLatin1String("selection-color") && checkColorValue(val, c)) {
5359             v->rendererConfig()->setSelectionColor(c);
5360         } else if (var == QLatin1String("current-line-color") && checkColorValue(val, c)) {
5361             v->rendererConfig()->setHighlightedLineColor(c);
5362         } else if (var == QLatin1String("bracket-highlight-color") && checkColorValue(val, c)) {
5363             v->rendererConfig()->setHighlightedBracketColor(c);
5364         } else if (var == QLatin1String("word-wrap-marker-color") && checkColorValue(val, c)) {
5365             v->rendererConfig()->setWordWrapMarkerColor(c);
5366         } else if (var == QLatin1String("font") || (checkIntValue(val, &n) && n > 0 && var == QLatin1String("font-size"))) {
5367             QFont _f(v->renderer()->currentFont());
5368 
5369             if (var == QLatin1String("font")) {
5370                 _f.setFamily(val);
5371                 _f.setFixedPitch(QFont(val).fixedPitch());
5372             } else {
5373                 _f.setPointSize(n);
5374             }
5375 
5376             v->rendererConfig()->setFont(_f);
5377         } else if (var == QLatin1String("scheme")) {
5378             v->rendererConfig()->setSchema(val);
5379         }
5380     }
5381 }
5382 
5383 bool KTextEditor::DocumentPrivate::checkBoolValue(QString val, bool *result)
5384 {
5385     val = val.trimmed().toLower();
5386     static const auto trueValues = {QLatin1String("1"), QLatin1String("on"), QLatin1String("true")};
5387     if (contains(trueValues, val)) {
5388         *result = true;
5389         return true;
5390     }
5391 
5392     static const auto falseValues = {QLatin1String("0"), QLatin1String("off"), QLatin1String("false")};
5393     if (contains(falseValues, val)) {
5394         *result = false;
5395         return true;
5396     }
5397     return false;
5398 }
5399 
5400 bool KTextEditor::DocumentPrivate::checkIntValue(const QString &val, int *result)
5401 {
5402     bool ret(false);
5403     *result = val.toInt(&ret);
5404     return ret;
5405 }
5406 
5407 bool KTextEditor::DocumentPrivate::checkColorValue(const QString &val, QColor &c)
5408 {
5409     c = QColor::fromString(val);
5410     return c.isValid();
5411 }
5412 
5413 // KTextEditor::variable
5414 QString KTextEditor::DocumentPrivate::variable(const QString &name) const
5415 {
5416     auto it = m_storedVariables.find(name);
5417     if (it == m_storedVariables.end()) {
5418         return QString();
5419     }
5420     return it->second;
5421 }
5422 
5423 void KTextEditor::DocumentPrivate::setVariable(const QString &name, const QString &value)
5424 {
5425     QString s = QStringLiteral("kate: ");
5426     s.append(name);
5427     s.append(QLatin1Char(' '));
5428     s.append(value);
5429     readVariableLine(s);
5430 }
5431 
5432 // END
5433 
5434 void KTextEditor::DocumentPrivate::slotModOnHdDirty(const QString &path)
5435 {
5436     if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskModified)) {
5437         m_modOnHd = true;
5438         m_modOnHdReason = OnDiskModified;
5439 
5440         if (!m_modOnHdTimer.isActive()) {
5441             m_modOnHdTimer.start();
5442         }
5443     }
5444 }
5445 
5446 void KTextEditor::DocumentPrivate::slotModOnHdCreated(const QString &path)
5447 {
5448     if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskCreated)) {
5449         m_modOnHd = true;
5450         m_modOnHdReason = OnDiskCreated;
5451 
5452         if (!m_modOnHdTimer.isActive()) {
5453             m_modOnHdTimer.start();
5454         }
5455     }
5456 }
5457 
5458 void KTextEditor::DocumentPrivate::slotModOnHdDeleted(const QString &path)
5459 {
5460     if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskDeleted)) {
5461         m_modOnHd = true;
5462         m_modOnHdReason = OnDiskDeleted;
5463 
5464         if (!m_modOnHdTimer.isActive()) {
5465             m_modOnHdTimer.start();
5466         }
5467     }
5468 }
5469 
5470 void KTextEditor::DocumentPrivate::slotDelayedHandleModOnHd()
5471 {
5472     // compare git hash with the one we have (if we have one)
5473     const QByteArray oldDigest = checksum();
5474     if (!oldDigest.isEmpty() && !url().isEmpty() && url().isLocalFile()) {
5475         // if current checksum == checksum of new file => unmodified
5476         if (m_modOnHdReason != OnDiskDeleted && m_modOnHdReason != OnDiskCreated && createDigest() && oldDigest == checksum()) {
5477             m_modOnHd = false;
5478             m_modOnHdReason = OnDiskUnmodified;
5479             m_prevModOnHdReason = OnDiskUnmodified;
5480         }
5481 
5482         // if still modified, try to take a look at git
5483         // skip that, if document is modified!
5484         // only do that, if the file is still there, else reload makes no sense!
5485         // we have a config option to disable this
5486         if (m_modOnHd && !isModified() && QFile::exists(url().toLocalFile())
5487             && config()->value(KateDocumentConfig::AutoReloadIfStateIsInVersionControl).toBool()) {
5488             // we only want to use git from PATH, cache this
5489             static const QString fullGitPath = QStandardPaths::findExecutable(QStringLiteral("git"));
5490             if (!fullGitPath.isEmpty()) {
5491                 QProcess git;
5492                 const QStringList args{QStringLiteral("cat-file"), QStringLiteral("-e"), QString::fromUtf8(oldDigest.toHex())};
5493                 git.setWorkingDirectory(url().adjusted(QUrl::RemoveFilename).toLocalFile());
5494                 git.start(fullGitPath, args);
5495                 if (git.waitForStarted()) {
5496                     git.closeWriteChannel();
5497                     if (git.waitForFinished()) {
5498                         if (git.exitCode() == 0) {
5499                             // this hash exists still in git => just reload
5500                             m_modOnHd = false;
5501                             m_modOnHdReason = OnDiskUnmodified;
5502                             m_prevModOnHdReason = OnDiskUnmodified;
5503                             documentReload();
5504                         }
5505                     }
5506                 }
5507             }
5508         }
5509     }
5510 
5511     // emit our signal to the outside!
5512     Q_EMIT modifiedOnDisk(this, m_modOnHd, m_modOnHdReason);
5513 }
5514 
5515 QByteArray KTextEditor::DocumentPrivate::checksum() const
5516 {
5517     return m_buffer->digest();
5518 }
5519 
5520 bool KTextEditor::DocumentPrivate::createDigest()
5521 {
5522     QByteArray digest;
5523 
5524     if (url().isLocalFile()) {
5525         QFile f(url().toLocalFile());
5526         if (f.open(QIODevice::ReadOnly)) {
5527             // init the hash with the git header
5528             QCryptographicHash crypto(QCryptographicHash::Sha1);
5529             const QString header = QStringLiteral("blob %1").arg(f.size());
5530             crypto.addData(QByteArray(header.toLatin1() + '\0'));
5531 
5532             while (!f.atEnd()) {
5533                 crypto.addData(f.read(256 * 1024));
5534             }
5535 
5536             digest = crypto.result();
5537         }
5538     }
5539 
5540     // set new digest
5541     m_buffer->setDigest(digest);
5542     return !digest.isEmpty();
5543 }
5544 
5545 QString KTextEditor::DocumentPrivate::reasonedMOHString() const
5546 {
5547     // squeeze path
5548     const QString str = KStringHandler::csqueeze(url().toDisplayString(QUrl::PreferLocalFile));
5549 
5550     switch (m_modOnHdReason) {
5551     case OnDiskModified:
5552         return i18n("The file '%1' was modified on disk.", str);
5553         break;
5554     case OnDiskCreated:
5555         return i18n("The file '%1' was created on disk.", str);
5556         break;
5557     case OnDiskDeleted:
5558         return i18n("The file '%1' was deleted on disk.", str);
5559         break;
5560     default:
5561         return QString();
5562     }
5563     Q_UNREACHABLE();
5564     return QString();
5565 }
5566 
5567 void KTextEditor::DocumentPrivate::removeTrailingSpacesAndAddNewLineAtEof()
5568 {
5569     // skip all work if the user doesn't want any adjustments
5570     const int remove = config()->removeSpaces();
5571     const bool newLineAtEof = config()->newLineAtEof();
5572     if (remove == 0 && !newLineAtEof) {
5573         return;
5574     }
5575 
5576     // temporarily disable static word wrap (see bug #328900)
5577     const bool wordWrapEnabled = config()->wordWrap();
5578     if (wordWrapEnabled) {
5579         setWordWrap(false);
5580     }
5581 
5582     editStart();
5583 
5584     // handle trailing space striping if needed
5585     const int lines = this->lines();
5586     if (remove != 0) {
5587         for (int line = 0; line < lines; ++line) {
5588             Kate::TextLine textline = plainKateTextLine(line);
5589 
5590             // remove trailing spaces in entire document, remove = 2
5591             // remove trailing spaces of touched lines, remove = 1
5592             // remove trailing spaces of lines saved on disk, remove = 1
5593             if (remove == 2 || textline.markedAsModified() || textline.markedAsSavedOnDisk()) {
5594                 const int p = textline.lastChar() + 1;
5595                 const int l = textline.length() - p;
5596                 if (l > 0) {
5597                     editRemoveText(line, p, l);
5598                 }
5599             }
5600         }
5601     }
5602 
5603     // add a trailing empty line if we want a final line break
5604     // do we need to add a trailing newline char?
5605     if (newLineAtEof) {
5606         Q_ASSERT(lines > 0);
5607         const auto length = lineLength(lines - 1);
5608         if (length > 0) {
5609             // ensure the cursor is not wrapped to the next line if at the end of the document
5610             // see bug 453252
5611             const auto oldEndOfDocumentCursor = documentEnd();
5612             std::vector<KTextEditor::ViewPrivate *> viewsToRestoreCursors;
5613             for (auto view : std::as_const(m_views)) {
5614                 auto v = static_cast<ViewPrivate *>(view);
5615                 if (v->cursorPosition() == oldEndOfDocumentCursor) {
5616                     viewsToRestoreCursors.push_back(v);
5617                 }
5618             }
5619 
5620             // wrap the last line, this might move the cursor
5621             editWrapLine(lines - 1, length);
5622 
5623             // undo cursor moving
5624             for (auto v : viewsToRestoreCursors) {
5625                 v->setCursorPosition(oldEndOfDocumentCursor);
5626             }
5627         }
5628     }
5629 
5630     editEnd();
5631 
5632     // enable word wrap again, if it was enabled (see bug #328900)
5633     if (wordWrapEnabled) {
5634         setWordWrap(true); // see begin of this function
5635     }
5636 }
5637 
5638 void KTextEditor::DocumentPrivate::removeAllTrailingSpaces()
5639 {
5640     editStart();
5641     const int lines = this->lines();
5642     for (int line = 0; line < lines; ++line) {
5643         const Kate::TextLine textline = plainKateTextLine(line);
5644         const int p = textline.lastChar() + 1;
5645         const int l = textline.length() - p;
5646         if (l > 0) {
5647             editRemoveText(line, p, l);
5648         }
5649     }
5650     editEnd();
5651 }
5652 
5653 bool KTextEditor::DocumentPrivate::updateFileType(const QString &newType, bool user)
5654 {
5655     if (user || !m_fileTypeSetByUser) {
5656         if (newType.isEmpty()) {
5657             return false;
5658         }
5659         KateFileType fileType = KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType);
5660         // if the mode "newType" does not exist
5661         if (fileType.name.isEmpty()) {
5662             return false;
5663         }
5664 
5665         // remember that we got set by user
5666         m_fileTypeSetByUser = user;
5667 
5668         m_fileType = newType;
5669 
5670         m_config->configStart();
5671 
5672         // NOTE: if the user changes the Mode, the Highlighting also changes.
5673         // m_hlSetByUser avoids resetting the highlight when saving the document, if
5674         // the current hl isn't stored (eg, in sftp:// or fish:// files) (see bug #407763)
5675         if ((user || !m_hlSetByUser) && !fileType.hl.isEmpty()) {
5676             int hl(KateHlManager::self()->nameFind(fileType.hl));
5677 
5678             if (hl >= 0) {
5679                 m_buffer->setHighlight(hl);
5680             }
5681         }
5682 
5683         // set the indentation mode, if any in the mode...
5684         // and user did not set it before!
5685         // NOTE: KateBuffer::setHighlight() also sets the indentation.
5686         if (!m_indenterSetByUser && !fileType.indenter.isEmpty()) {
5687             config()->setIndentationMode(fileType.indenter);
5688         }
5689 
5690         // views!
5691         for (auto view : std::as_const(m_views)) {
5692             auto v = static_cast<ViewPrivate *>(view);
5693             v->config()->configStart();
5694             v->rendererConfig()->configStart();
5695         }
5696 
5697         bool bom_settings = false;
5698         if (m_bomSetByUser) {
5699             bom_settings = m_config->bom();
5700         }
5701         readVariableLine(fileType.varLine);
5702         if (m_bomSetByUser) {
5703             m_config->setBom(bom_settings);
5704         }
5705         m_config->configEnd();
5706         for (auto view : std::as_const(m_views)) {
5707             auto v = static_cast<ViewPrivate *>(view);
5708             v->config()->configEnd();
5709             v->rendererConfig()->configEnd();
5710         }
5711     }
5712 
5713     // fixme, make this better...
5714     Q_EMIT modeChanged(this);
5715     return true;
5716 }
5717 
5718 void KTextEditor::DocumentPrivate::slotQueryClose_save(bool *handled, bool *abortClosing)
5719 {
5720     *handled = true;
5721     *abortClosing = true;
5722     if (url().isEmpty()) {
5723         const QUrl res = getSaveFileUrl(i18n("Save File"));
5724         if (res.isEmpty()) {
5725             *abortClosing = true;
5726             return;
5727         }
5728         saveAs(res);
5729         *abortClosing = false;
5730     } else {
5731         save();
5732         *abortClosing = false;
5733     }
5734 }
5735 
5736 // BEGIN KTextEditor::ConfigInterface
5737 
5738 // BEGIN ConfigInterface stff
5739 QStringList KTextEditor::DocumentPrivate::configKeys() const
5740 {
5741     // expose all internally registered keys of the KateDocumentConfig
5742     return m_config->configKeys();
5743 }
5744 
5745 QVariant KTextEditor::DocumentPrivate::configValue(const QString &key)
5746 {
5747     // just dispatch to internal key => value lookup
5748     return m_config->value(key);
5749 }
5750 
5751 void KTextEditor::DocumentPrivate::setConfigValue(const QString &key, const QVariant &value)
5752 {
5753     // just dispatch to internal key + value set
5754     m_config->setValue(key, value);
5755 }
5756 
5757 // END KTextEditor::ConfigInterface
5758 
5759 KTextEditor::Cursor KTextEditor::DocumentPrivate::documentEnd() const
5760 {
5761     return KTextEditor::Cursor(lastLine(), lineLength(lastLine()));
5762 }
5763 
5764 bool KTextEditor::DocumentPrivate::replaceText(KTextEditor::Range range, const QString &s, bool block)
5765 {
5766     // TODO more efficient?
5767     editStart();
5768     bool changed = removeText(range, block);
5769     changed |= insertText(range.start(), s, block);
5770     editEnd();
5771     return changed;
5772 }
5773 
5774 KateHighlighting *KTextEditor::DocumentPrivate::highlight() const
5775 {
5776     return m_buffer->highlight();
5777 }
5778 
5779 Kate::TextLine KTextEditor::DocumentPrivate::kateTextLine(int i)
5780 {
5781     m_buffer->ensureHighlighted(i);
5782     return m_buffer->plainLine(i);
5783 }
5784 
5785 Kate::TextLine KTextEditor::DocumentPrivate::plainKateTextLine(int i)
5786 {
5787     return m_buffer->plainLine(i);
5788 }
5789 
5790 bool KTextEditor::DocumentPrivate::isEditRunning() const
5791 {
5792     return editIsRunning;
5793 }
5794 
5795 void KTextEditor::DocumentPrivate::setUndoMergeAllEdits(bool merge)
5796 {
5797     if (merge && m_undoMergeAllEdits) {
5798         // Don't add another undo safe point: it will override our current one,
5799         // meaning we'll need two undo's to get back there - which defeats the object!
5800         return;
5801     }
5802     m_undoManager->undoSafePoint();
5803     m_undoManager->setAllowComplexMerge(merge);
5804     m_undoMergeAllEdits = merge;
5805 }
5806 
5807 // BEGIN KTextEditor::MovingInterface
5808 KTextEditor::MovingCursor *KTextEditor::DocumentPrivate::newMovingCursor(KTextEditor::Cursor position, KTextEditor::MovingCursor::InsertBehavior insertBehavior)
5809 {
5810     return new Kate::TextCursor(buffer(), position, insertBehavior);
5811 }
5812 
5813 KTextEditor::MovingRange *KTextEditor::DocumentPrivate::newMovingRange(KTextEditor::Range range,
5814                                                                        KTextEditor::MovingRange::InsertBehaviors insertBehaviors,
5815                                                                        KTextEditor::MovingRange::EmptyBehavior emptyBehavior)
5816 {
5817     return new Kate::TextRange(buffer(), range, insertBehaviors, emptyBehavior);
5818 }
5819 
5820 qint64 KTextEditor::DocumentPrivate::revision() const
5821 {
5822     return m_buffer->history().revision();
5823 }
5824 
5825 qint64 KTextEditor::DocumentPrivate::lastSavedRevision() const
5826 {
5827     return m_buffer->history().lastSavedRevision();
5828 }
5829 
5830 void KTextEditor::DocumentPrivate::lockRevision(qint64 revision)
5831 {
5832     m_buffer->history().lockRevision(revision);
5833 }
5834 
5835 void KTextEditor::DocumentPrivate::unlockRevision(qint64 revision)
5836 {
5837     m_buffer->history().unlockRevision(revision);
5838 }
5839 
5840 void KTextEditor::DocumentPrivate::transformCursor(int &line,
5841                                                    int &column,
5842                                                    KTextEditor::MovingCursor::InsertBehavior insertBehavior,
5843                                                    qint64 fromRevision,
5844                                                    qint64 toRevision)
5845 {
5846     m_buffer->history().transformCursor(line, column, insertBehavior, fromRevision, toRevision);
5847 }
5848 
5849 void KTextEditor::DocumentPrivate::transformCursor(KTextEditor::Cursor &cursor,
5850                                                    KTextEditor::MovingCursor::InsertBehavior insertBehavior,
5851                                                    qint64 fromRevision,
5852                                                    qint64 toRevision)
5853 {
5854     int line = cursor.line();
5855     int column = cursor.column();
5856     m_buffer->history().transformCursor(line, column, insertBehavior, fromRevision, toRevision);
5857     cursor.setPosition(line, column);
5858 }
5859 
5860 void KTextEditor::DocumentPrivate::transformRange(KTextEditor::Range &range,
5861                                                   KTextEditor::MovingRange::InsertBehaviors insertBehaviors,
5862                                                   KTextEditor::MovingRange::EmptyBehavior emptyBehavior,
5863                                                   qint64 fromRevision,
5864                                                   qint64 toRevision)
5865 {
5866     m_buffer->history().transformRange(range, insertBehaviors, emptyBehavior, fromRevision, toRevision);
5867 }
5868 
5869 // END
5870 
5871 // BEGIN KTextEditor::AnnotationInterface
5872 void KTextEditor::DocumentPrivate::setAnnotationModel(KTextEditor::AnnotationModel *model)
5873 {
5874     KTextEditor::AnnotationModel *oldmodel = m_annotationModel;
5875     m_annotationModel = model;
5876     Q_EMIT annotationModelChanged(oldmodel, m_annotationModel);
5877 }
5878 
5879 KTextEditor::AnnotationModel *KTextEditor::DocumentPrivate::annotationModel() const
5880 {
5881     return m_annotationModel;
5882 }
5883 // END KTextEditor::AnnotationInterface
5884 
5885 // TAKEN FROM kparts.h
5886 bool KTextEditor::DocumentPrivate::queryClose()
5887 {
5888     if (!isModified() || (isEmpty() && url().isEmpty())) {
5889         return true;
5890     }
5891 
5892     QString docName = documentName();
5893 
5894     int res = KMessageBox::warningTwoActionsCancel(dialogParent(),
5895                                                    i18n("The document \"%1\" has been modified.\n"
5896                                                         "Do you want to save your changes or discard them?",
5897                                                         docName),
5898                                                    i18n("Close Document"),
5899                                                    KStandardGuiItem::save(),
5900                                                    KStandardGuiItem::discard());
5901 
5902     bool abortClose = false;
5903     bool handled = false;
5904 
5905     switch (res) {
5906     case KMessageBox::PrimaryAction:
5907         sigQueryClose(&handled, &abortClose);
5908         if (!handled) {
5909             if (url().isEmpty()) {
5910                 const QUrl url = getSaveFileUrl(i18n("Save File"));
5911                 if (url.isEmpty()) {
5912                     return false;
5913                 }
5914 
5915                 saveAs(url);
5916             } else {
5917                 save();
5918             }
5919         } else if (abortClose) {
5920             return false;
5921         }
5922         return waitSaveComplete();
5923     case KMessageBox::SecondaryAction:
5924         return true;
5925     default: // case KMessageBox::Cancel :
5926         return false;
5927     }
5928 }
5929 
5930 void KTextEditor::DocumentPrivate::slotStarted(KIO::Job *job)
5931 {
5932     // if we are idle before, we are now loading!
5933     if (m_documentState == DocumentIdle) {
5934         m_documentState = DocumentLoading;
5935     }
5936 
5937     // if loading:
5938     // - remember pre loading read-write mode
5939     // if remote load:
5940     // - set to read-only
5941     // - trigger possible message
5942     if (m_documentState == DocumentLoading) {
5943         // remember state
5944         m_readWriteStateBeforeLoading = isReadWrite();
5945 
5946         // perhaps show loading message, but wait one second
5947         if (job) {
5948             // only read only if really remote file!
5949             setReadWrite(false);
5950 
5951             // perhaps some message about loading in one second!
5952             // remember job pointer, we want to be able to kill it!
5953             m_loadingJob = job;
5954             QTimer::singleShot(1000, this, SLOT(slotTriggerLoadingMessage()));
5955         }
5956     }
5957 }
5958 
5959 void KTextEditor::DocumentPrivate::slotCompleted()
5960 {
5961     // if were loading, reset back to old read-write mode before loading
5962     // and kill the possible loading message
5963     if (m_documentState == DocumentLoading) {
5964         setReadWrite(m_readWriteStateBeforeLoading);
5965         delete m_loadingMessage;
5966     }
5967 
5968     // Emit signal that we saved  the document, if needed
5969     if (m_documentState == DocumentSaving || m_documentState == DocumentSavingAs) {
5970         Q_EMIT documentSavedOrUploaded(this, m_documentState == DocumentSavingAs);
5971     }
5972 
5973     // back to idle mode
5974     m_documentState = DocumentIdle;
5975     m_reloading = false;
5976 }
5977 
5978 void KTextEditor::DocumentPrivate::slotCanceled()
5979 {
5980     // if were loading, reset back to old read-write mode before loading
5981     // and kill the possible loading message
5982     if (m_documentState == DocumentLoading) {
5983         setReadWrite(m_readWriteStateBeforeLoading);
5984         delete m_loadingMessage;
5985 
5986         if (!m_openingError) {
5987             showAndSetOpeningErrorAccess();
5988         }
5989 
5990         updateDocName();
5991     }
5992 
5993     // back to idle mode
5994     m_documentState = DocumentIdle;
5995     m_reloading = false;
5996 }
5997 
5998 void KTextEditor::DocumentPrivate::slotTriggerLoadingMessage()
5999 {
6000     // no longer loading?
6001     // no message needed!
6002     if (m_documentState != DocumentLoading) {
6003         return;
6004     }
6005 
6006     // create message about file loading in progress
6007     delete m_loadingMessage;
6008     m_loadingMessage =
6009         new KTextEditor::Message(i18n("The file <a href=\"%1\">%2</a> is still loading.", url().toDisplayString(QUrl::PreferLocalFile), url().fileName()));
6010     m_loadingMessage->setPosition(KTextEditor::Message::TopInView);
6011 
6012     // if around job: add cancel action
6013     if (m_loadingJob) {
6014         QAction *cancel = new QAction(i18n("&Abort Loading"), nullptr);
6015         connect(cancel, &QAction::triggered, this, &KTextEditor::DocumentPrivate::slotAbortLoading);
6016         m_loadingMessage->addAction(cancel);
6017     }
6018 
6019     // really post message
6020     postMessage(m_loadingMessage);
6021 }
6022 
6023 void KTextEditor::DocumentPrivate::slotAbortLoading()
6024 {
6025     // no job, no work
6026     if (!m_loadingJob) {
6027         return;
6028     }
6029 
6030     // abort loading if any job
6031     // signal results!
6032     m_loadingJob->kill(KJob::EmitResult);
6033     m_loadingJob = nullptr;
6034 }
6035 
6036 void KTextEditor::DocumentPrivate::slotUrlChanged(const QUrl &url)
6037 {
6038     if (m_reloading) {
6039         // the URL is temporarily unset and then reset to the previous URL during reload
6040         // we do not want to notify the outside about this
6041         return;
6042     }
6043 
6044     Q_UNUSED(url);
6045     updateDocName();
6046     Q_EMIT documentUrlChanged(this);
6047 }
6048 
6049 bool KTextEditor::DocumentPrivate::save()
6050 {
6051     // no double save/load
6052     // we need to allow DocumentPreSavingAs here as state, as save is called in saveAs!
6053     if ((m_documentState != DocumentIdle) && (m_documentState != DocumentPreSavingAs)) {
6054         return false;
6055     }
6056 
6057     // if we are idle, we are now saving
6058     if (m_documentState == DocumentIdle) {
6059         m_documentState = DocumentSaving;
6060     } else {
6061         m_documentState = DocumentSavingAs;
6062     }
6063 
6064     // let anyone listening know that we are going to save
6065     Q_EMIT aboutToSave(this);
6066 
6067     // call back implementation for real work
6068     return KTextEditor::Document::save();
6069 }
6070 
6071 bool KTextEditor::DocumentPrivate::saveAs(const QUrl &url)
6072 {
6073     // abort on bad URL
6074     // that is done in saveAs below, too
6075     // but we must check it here already to avoid messing up
6076     // as no signals will be send, then
6077     if (!url.isValid()) {
6078         return false;
6079     }
6080 
6081     // no double save/load
6082     if (m_documentState != DocumentIdle) {
6083         return false;
6084     }
6085 
6086     // we enter the pre save as phase
6087     m_documentState = DocumentPreSavingAs;
6088 
6089     // call base implementation for real work
6090     return KTextEditor::Document::saveAs(url);
6091 }
6092 
6093 QString KTextEditor::DocumentPrivate::defaultDictionary() const
6094 {
6095     return m_defaultDictionary;
6096 }
6097 
6098 QList<QPair<KTextEditor::MovingRange *, QString>> KTextEditor::DocumentPrivate::dictionaryRanges() const
6099 {
6100     return m_dictionaryRanges;
6101 }
6102 
6103 void KTextEditor::DocumentPrivate::clearDictionaryRanges()
6104 {
6105     for (auto i = m_dictionaryRanges.cbegin(); i != m_dictionaryRanges.cend(); ++i) {
6106         delete (*i).first;
6107     }
6108     m_dictionaryRanges.clear();
6109     if (m_onTheFlyChecker) {
6110         m_onTheFlyChecker->refreshSpellCheck();
6111     }
6112     Q_EMIT dictionaryRangesPresent(false);
6113 }
6114 
6115 void KTextEditor::DocumentPrivate::setDictionary(const QString &newDictionary, KTextEditor::Range range, bool blockmode)
6116 {
6117     if (blockmode) {
6118         for (int i = range.start().line(); i <= range.end().line(); ++i) {
6119             setDictionary(newDictionary, rangeOnLine(range, i));
6120         }
6121     } else {
6122         setDictionary(newDictionary, range);
6123     }
6124 
6125     Q_EMIT dictionaryRangesPresent(!m_dictionaryRanges.isEmpty());
6126 }
6127 
6128 void KTextEditor::DocumentPrivate::setDictionary(const QString &newDictionary, KTextEditor::Range range)
6129 {
6130     KTextEditor::Range newDictionaryRange = range;
6131     if (!newDictionaryRange.isValid() || newDictionaryRange.isEmpty()) {
6132         return;
6133     }
6134     QList<QPair<KTextEditor::MovingRange *, QString>> newRanges;
6135     // all ranges is 'm_dictionaryRanges' are assumed to be mutually disjoint
6136     for (auto i = m_dictionaryRanges.begin(); i != m_dictionaryRanges.end();) {
6137         qCDebug(LOG_KTE) << "new iteration" << newDictionaryRange;
6138         if (newDictionaryRange.isEmpty()) {
6139             break;
6140         }
6141         QPair<KTextEditor::MovingRange *, QString> pair = *i;
6142         QString dictionarySet = pair.second;
6143         KTextEditor::MovingRange *dictionaryRange = pair.first;
6144         qCDebug(LOG_KTE) << *dictionaryRange << dictionarySet;
6145         if (dictionaryRange->contains(newDictionaryRange) && newDictionary == dictionarySet) {
6146             qCDebug(LOG_KTE) << "dictionaryRange contains newDictionaryRange";
6147             return;
6148         }
6149         if (newDictionaryRange.contains(*dictionaryRange)) {
6150             delete dictionaryRange;
6151             i = m_dictionaryRanges.erase(i);
6152             qCDebug(LOG_KTE) << "newDictionaryRange contains dictionaryRange";
6153             continue;
6154         }
6155 
6156         KTextEditor::Range intersection = dictionaryRange->toRange().intersect(newDictionaryRange);
6157         if (!intersection.isEmpty() && intersection.isValid()) {
6158             if (dictionarySet == newDictionary) { // we don't have to do anything for 'intersection'
6159                 // except cut off the intersection
6160                 QList<KTextEditor::Range> remainingRanges = KateSpellCheckManager::rangeDifference(newDictionaryRange, intersection);
6161                 Q_ASSERT(remainingRanges.size() == 1);
6162                 newDictionaryRange = remainingRanges.first();
6163                 ++i;
6164                 qCDebug(LOG_KTE) << "dictionarySet == newDictionary";
6165                 continue;
6166             }
6167             QList<KTextEditor::Range> remainingRanges = KateSpellCheckManager::rangeDifference(*dictionaryRange, intersection);
6168             for (auto j = remainingRanges.begin(); j != remainingRanges.end(); ++j) {
6169                 KTextEditor::MovingRange *remainingRange = newMovingRange(*j, KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight);
6170                 remainingRange->setFeedback(this);
6171                 newRanges.push_back({remainingRange, dictionarySet});
6172             }
6173             i = m_dictionaryRanges.erase(i);
6174             delete dictionaryRange;
6175         } else {
6176             ++i;
6177         }
6178     }
6179     m_dictionaryRanges += newRanges;
6180     if (!newDictionaryRange.isEmpty() && !newDictionary.isEmpty()) { // we don't add anything for the default dictionary
6181         KTextEditor::MovingRange *newDictionaryMovingRange =
6182             newMovingRange(newDictionaryRange, KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight);
6183         newDictionaryMovingRange->setFeedback(this);
6184         m_dictionaryRanges.push_back({newDictionaryMovingRange, newDictionary});
6185     }
6186     if (m_onTheFlyChecker && !newDictionaryRange.isEmpty()) {
6187         m_onTheFlyChecker->refreshSpellCheck(newDictionaryRange);
6188     }
6189 }
6190 
6191 void KTextEditor::DocumentPrivate::setDefaultDictionary(const QString &dict)
6192 {
6193     if (m_defaultDictionary == dict) {
6194         return;
6195     }
6196 
6197     m_defaultDictionary = dict;
6198 
6199     if (m_onTheFlyChecker) {
6200         m_onTheFlyChecker->updateConfig();
6201         refreshOnTheFlyCheck();
6202     }
6203     Q_EMIT defaultDictionaryChanged(this);
6204 }
6205 
6206 void KTextEditor::DocumentPrivate::onTheFlySpellCheckingEnabled(bool enable)
6207 {
6208     if (isOnTheFlySpellCheckingEnabled() == enable) {
6209         return;
6210     }
6211 
6212     if (enable) {
6213         Q_ASSERT(m_onTheFlyChecker == nullptr);
6214         m_onTheFlyChecker = new KateOnTheFlyChecker(this);
6215     } else {
6216         delete m_onTheFlyChecker;
6217         m_onTheFlyChecker = nullptr;
6218     }
6219 
6220     for (auto view : std::as_const(m_views)) {
6221         static_cast<ViewPrivate *>(view)->reflectOnTheFlySpellCheckStatus(enable);
6222     }
6223 }
6224 
6225 bool KTextEditor::DocumentPrivate::isOnTheFlySpellCheckingEnabled() const
6226 {
6227     return m_onTheFlyChecker != nullptr;
6228 }
6229 
6230 QString KTextEditor::DocumentPrivate::dictionaryForMisspelledRange(KTextEditor::Range range) const
6231 {
6232     if (!m_onTheFlyChecker) {
6233         return QString();
6234     } else {
6235         return m_onTheFlyChecker->dictionaryForMisspelledRange(range);
6236     }
6237 }
6238 
6239 void KTextEditor::DocumentPrivate::clearMisspellingForWord(const QString &word)
6240 {
6241     if (m_onTheFlyChecker) {
6242         m_onTheFlyChecker->clearMisspellingForWord(word);
6243     }
6244 }
6245 
6246 void KTextEditor::DocumentPrivate::refreshOnTheFlyCheck(KTextEditor::Range range)
6247 {
6248     if (m_onTheFlyChecker) {
6249         m_onTheFlyChecker->refreshSpellCheck(range);
6250     }
6251 }
6252 
6253 void KTextEditor::DocumentPrivate::rangeInvalid(KTextEditor::MovingRange *movingRange)
6254 {
6255     deleteDictionaryRange(movingRange);
6256 }
6257 
6258 void KTextEditor::DocumentPrivate::rangeEmpty(KTextEditor::MovingRange *movingRange)
6259 {
6260     deleteDictionaryRange(movingRange);
6261 }
6262 
6263 void KTextEditor::DocumentPrivate::deleteDictionaryRange(KTextEditor::MovingRange *movingRange)
6264 {
6265     qCDebug(LOG_KTE) << "deleting" << movingRange;
6266 
6267     auto finder = [=](const QPair<KTextEditor::MovingRange *, QString> &item) -> bool {
6268         return item.first == movingRange;
6269     };
6270 
6271     auto it = std::find_if(m_dictionaryRanges.begin(), m_dictionaryRanges.end(), finder);
6272 
6273     if (it != m_dictionaryRanges.end()) {
6274         m_dictionaryRanges.erase(it);
6275         delete movingRange;
6276     }
6277 
6278     Q_ASSERT(std::find_if(m_dictionaryRanges.begin(), m_dictionaryRanges.end(), finder) == m_dictionaryRanges.end());
6279 }
6280 
6281 bool KTextEditor::DocumentPrivate::containsCharacterEncoding(KTextEditor::Range range)
6282 {
6283     KateHighlighting *highlighting = highlight();
6284 
6285     const int rangeStartLine = range.start().line();
6286     const int rangeStartColumn = range.start().column();
6287     const int rangeEndLine = range.end().line();
6288     const int rangeEndColumn = range.end().column();
6289 
6290     for (int line = range.start().line(); line <= rangeEndLine; ++line) {
6291         const Kate::TextLine textLine = kateTextLine(line);
6292         const int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0;
6293         const int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine.length();
6294         for (int col = startColumn; col < endColumn; ++col) {
6295             int attr = textLine.attribute(col);
6296             const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr);
6297             if (!prefixStore.findPrefix(textLine, col).isEmpty()) {
6298                 return true;
6299             }
6300         }
6301     }
6302 
6303     return false;
6304 }
6305 
6306 int KTextEditor::DocumentPrivate::computePositionWrtOffsets(const OffsetList &offsetList, int pos)
6307 {
6308     int previousOffset = 0;
6309     for (auto i = offsetList.cbegin(); i != offsetList.cend(); ++i) {
6310         if (i->first > pos) {
6311             break;
6312         }
6313         previousOffset = i->second;
6314     }
6315     return pos + previousOffset;
6316 }
6317 
6318 QString KTextEditor::DocumentPrivate::decodeCharacters(KTextEditor::Range range,
6319                                                        KTextEditor::DocumentPrivate::OffsetList &decToEncOffsetList,
6320                                                        KTextEditor::DocumentPrivate::OffsetList &encToDecOffsetList)
6321 {
6322     QString toReturn;
6323     KTextEditor::Cursor previous = range.start();
6324     int decToEncCurrentOffset = 0;
6325     int encToDecCurrentOffset = 0;
6326     int i = 0;
6327     int newI = 0;
6328 
6329     KateHighlighting *highlighting = highlight();
6330     Kate::TextLine textLine;
6331 
6332     const int rangeStartLine = range.start().line();
6333     const int rangeStartColumn = range.start().column();
6334     const int rangeEndLine = range.end().line();
6335     const int rangeEndColumn = range.end().column();
6336 
6337     for (int line = range.start().line(); line <= rangeEndLine; ++line) {
6338         textLine = kateTextLine(line);
6339         int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0;
6340         int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine.length();
6341         for (int col = startColumn; col < endColumn;) {
6342             int attr = textLine.attribute(col);
6343             const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr);
6344             const QHash<QString, QChar> &characterEncodingsHash = highlighting->getCharacterEncodings(attr);
6345             QString matchingPrefix = prefixStore.findPrefix(textLine, col);
6346             if (!matchingPrefix.isEmpty()) {
6347                 toReturn += text(KTextEditor::Range(previous, KTextEditor::Cursor(line, col)));
6348                 const QChar &c = characterEncodingsHash.value(matchingPrefix);
6349                 const bool isNullChar = c.isNull();
6350                 if (!c.isNull()) {
6351                     toReturn += c;
6352                 }
6353                 i += matchingPrefix.length();
6354                 col += matchingPrefix.length();
6355                 previous = KTextEditor::Cursor(line, col);
6356                 decToEncCurrentOffset = decToEncCurrentOffset - (isNullChar ? 0 : 1) + matchingPrefix.length();
6357                 encToDecCurrentOffset = encToDecCurrentOffset - matchingPrefix.length() + (isNullChar ? 0 : 1);
6358                 newI += (isNullChar ? 0 : 1);
6359                 decToEncOffsetList.push_back(QPair<int, int>(newI, decToEncCurrentOffset));
6360                 encToDecOffsetList.push_back(QPair<int, int>(i, encToDecCurrentOffset));
6361                 continue;
6362             }
6363             ++col;
6364             ++i;
6365             ++newI;
6366         }
6367         ++i;
6368         ++newI;
6369     }
6370     if (previous < range.end()) {
6371         toReturn += text(KTextEditor::Range(previous, range.end()));
6372     }
6373     return toReturn;
6374 }
6375 
6376 void KTextEditor::DocumentPrivate::replaceCharactersByEncoding(KTextEditor::Range range)
6377 {
6378     KateHighlighting *highlighting = highlight();
6379     Kate::TextLine textLine;
6380 
6381     const int rangeStartLine = range.start().line();
6382     const int rangeStartColumn = range.start().column();
6383     const int rangeEndLine = range.end().line();
6384     const int rangeEndColumn = range.end().column();
6385 
6386     for (int line = range.start().line(); line <= rangeEndLine; ++line) {
6387         textLine = kateTextLine(line);
6388         int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0;
6389         int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine.length();
6390         for (int col = startColumn; col < endColumn;) {
6391             int attr = textLine.attribute(col);
6392             const QHash<QChar, QString> &reverseCharacterEncodingsHash = highlighting->getReverseCharacterEncodings(attr);
6393             auto it = reverseCharacterEncodingsHash.find(textLine.at(col));
6394             if (it != reverseCharacterEncodingsHash.end()) {
6395                 replaceText(KTextEditor::Range(line, col, line, col + 1), *it);
6396                 col += (*it).length();
6397                 continue;
6398             }
6399             ++col;
6400         }
6401     }
6402 }
6403 
6404 //
6405 // Highlighting information
6406 //
6407 
6408 QStringList KTextEditor::DocumentPrivate::embeddedHighlightingModes() const
6409 {
6410     return highlight()->getEmbeddedHighlightingModes();
6411 }
6412 
6413 QString KTextEditor::DocumentPrivate::highlightingModeAt(KTextEditor::Cursor position)
6414 {
6415     return highlight()->higlightingModeForLocation(this, position);
6416 }
6417 
6418 Kate::SwapFile *KTextEditor::DocumentPrivate::swapFile()
6419 {
6420     return m_swapfile;
6421 }
6422 
6423 /**
6424  * \return \c -1 if \c line or \c column invalid, otherwise one of
6425  * standard style attribute number
6426  */
6427 KSyntaxHighlighting::Theme::TextStyle KTextEditor::DocumentPrivate::defStyleNum(int line, int column)
6428 {
6429     // Validate parameters to prevent out of range access
6430     if (line < 0 || line >= lines() || column < 0) {
6431         return KSyntaxHighlighting::Theme::TextStyle::Normal;
6432     }
6433 
6434     // get highlighted line
6435     Kate::TextLine tl = kateTextLine(line);
6436 
6437     // either get char attribute or attribute of context still active at end of line
6438     int attribute = 0;
6439     if (column < tl.length()) {
6440         attribute = tl.attribute(column);
6441     } else if (column == tl.length()) {
6442         if (!tl.attributesList().empty()) {
6443             attribute = tl.attributesList().back().attributeValue;
6444         } else {
6445             return KSyntaxHighlighting::Theme::TextStyle::Normal;
6446         }
6447     } else {
6448         return KSyntaxHighlighting::Theme::TextStyle::Normal;
6449     }
6450 
6451     return highlight()->defaultStyleForAttribute(attribute);
6452 }
6453 
6454 bool KTextEditor::DocumentPrivate::isComment(int line, int column)
6455 {
6456     return defStyleNum(line, column) == KSyntaxHighlighting::Theme::TextStyle::Comment;
6457 }
6458 
6459 int KTextEditor::DocumentPrivate::findTouchedLine(int startLine, bool down)
6460 {
6461     const int offset = down ? 1 : -1;
6462     const int lineCount = lines();
6463     while (startLine >= 0 && startLine < lineCount) {
6464         Kate::TextLine tl = m_buffer->plainLine(startLine);
6465         if (tl.markedAsModified() || tl.markedAsSavedOnDisk()) {
6466             return startLine;
6467         }
6468         startLine += offset;
6469     }
6470 
6471     return -1;
6472 }
6473 
6474 void KTextEditor::DocumentPrivate::setActiveTemplateHandler(KateTemplateHandler *handler)
6475 {
6476     // delete any active template handler
6477     delete m_activeTemplateHandler.data();
6478     m_activeTemplateHandler = handler;
6479 }
6480 
6481 // BEGIN KTextEditor::MessageInterface
6482 bool KTextEditor::DocumentPrivate::postMessage(KTextEditor::Message *message)
6483 {
6484     // no message -> cancel
6485     if (!message) {
6486         return false;
6487     }
6488 
6489     // make sure the desired view belongs to this document
6490     if (message->view() && message->view()->document() != this) {
6491         qCWarning(LOG_KTE) << "trying to post a message to a view of another document:" << message->text();
6492         return false;
6493     }
6494 
6495     message->setParent(this);
6496     message->setDocument(this);
6497 
6498     // if there are no actions, add a close action by default if widget does not auto-hide
6499     if (message->actions().count() == 0 && message->autoHide() < 0) {
6500         QAction *closeAction = new QAction(QIcon::fromTheme(QStringLiteral("window-close")), i18n("&Close"), nullptr);
6501         closeAction->setToolTip(i18nc("Close the message being displayed", "Close message"));
6502         message->addAction(closeAction);
6503     }
6504 
6505     // reparent actions, as we want full control over when they are deleted
6506     QList<std::shared_ptr<QAction>> managedMessageActions;
6507     const auto messageActions = message->actions();
6508     managedMessageActions.reserve(messageActions.size());
6509     for (QAction *action : messageActions) {
6510         action->setParent(nullptr);
6511         managedMessageActions.append(std::shared_ptr<QAction>(action));
6512     }
6513     m_messageHash.insert(message, managedMessageActions);
6514 
6515     // post message to requested view, or to all views
6516     if (KTextEditor::ViewPrivate *view = qobject_cast<KTextEditor::ViewPrivate *>(message->view())) {
6517         view->postMessage(message, managedMessageActions);
6518     } else {
6519         for (auto view : std::as_const(m_views)) {
6520             static_cast<ViewPrivate *>(view)->postMessage(message, managedMessageActions);
6521         }
6522     }
6523 
6524     // also catch if the user manually calls delete message
6525     connect(message, &Message::closed, this, &DocumentPrivate::messageDestroyed);
6526 
6527     return true;
6528 }
6529 
6530 void KTextEditor::DocumentPrivate::messageDestroyed(KTextEditor::Message *message)
6531 {
6532     // KTE:Message is already in destructor
6533     Q_ASSERT(m_messageHash.contains(message));
6534     m_messageHash.remove(message);
6535 }
6536 // END KTextEditor::MessageInterface
6537 
6538 #include "moc_katedocument.cpp"