File indexing completed on 2024-04-28 15:30:28

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