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"