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