1 /*
2     This file is part of the KDE libraries
3     SPDX-FileCopyrightText: 2001-2004 Christoph Cullmann <cullmann@kde.org>
4     SPDX-FileCopyrightText: 2001 Joseph Wenninger <jowenn@kde.org>
5     SPDX-FileCopyrightText: 1999 Jochen Wilhelmy <digisnap@cs.tu-berlin.de>
6     SPDX-FileCopyrightText: 2006 Hamish Rodda <rodda@kde.org>
7     SPDX-FileCopyrightText: 2007 Mirko Stocker <me@misto.ch>
8     SPDX-FileCopyrightText: 2009-2010 Michel Ludwig <michel.ludwig@kdemail.net>
9     SPDX-FileCopyrightText: 2013 Gerald Senarclens de Grancy <oss@senarclens.eu>
10     SPDX-FileCopyrightText: 2013 Andrey Matveyakin <a.matveyakin@gmail.com>
11 
12     SPDX-License-Identifier: LGPL-2.0-only
13 */
14 // BEGIN includes
15 #include "katedocument.h"
16 #include "config.h"
17 #include "kateabstractinputmode.h"
18 #include "kateautoindent.h"
19 #include "katebuffer.h"
20 #include "kateconfig.h"
21 #include "katedialogs.h"
22 #include "kateglobal.h"
23 #include "katehighlight.h"
24 #include "katemodemanager.h"
25 #include "katepartdebug.h"
26 #include "kateplaintextsearch.h"
27 #include "kateregexpsearch.h"
28 #include "katerenderer.h"
29 #include "katescriptmanager.h"
30 #include "kateswapfile.h"
31 #include "katesyntaxmanager.h"
32 #include "katetemplatehandler.h"
33 #include "kateundomanager.h"
34 #include "katevariableexpansionmanager.h"
35 #include "kateview.h"
36 #include "printing/kateprinter.h"
37 #include "spellcheck/ontheflycheck.h"
38 #include "spellcheck/prefixstore.h"
39 #include "spellcheck/spellcheck.h"
40 
41 #if EDITORCONFIG_FOUND
42 #include "editorconfig.h"
43 #endif
44 
45 #include <KTextEditor/Attribute>
46 #include <KTextEditor/DocumentCursor>
47 
48 #include <KConfigGroup>
49 #include <KDirWatch>
50 #include <KFileItem>
51 #include <KIO/Job>
52 #include <KIO/JobUiDelegate>
53 #include <KJobWidgets>
54 #include <KMessageBox>
55 #include <KMountPoint>
56 #include <KNetworkMounts>
57 #include <KParts/OpenUrlArguments>
58 #include <KStandardAction>
59 #include <KStringHandler>
60 #include <KToggleAction>
61 #include <KXMLGUIFactory>
62 
63 #include <QApplication>
64 #include <QClipboard>
65 #include <QCryptographicHash>
66 #include <QFile>
67 #include <QFileDialog>
68 #include <QMap>
69 #include <QMimeDatabase>
70 #include <QProcess>
71 #include <QRegularExpression>
72 #include <QTemporaryFile>
73 #include <QTextCodec>
74 #include <QTextStream>
75 
76 #include <cmath>
77 
78 // END  includes
79 
80 #if 0
81 #define EDIT_DEBUG qCDebug(LOG_KTE)
82 #else
83 #define EDIT_DEBUG                                                                                                                                             \
84     if (0)                                                                                                                                                     \
85     qCDebug(LOG_KTE)
86 #endif
87 
88 template<class C, class E>
indexOf(const std::initializer_list<C> & list,const E & entry)89 static int indexOf(const std::initializer_list<C> &list, const E &entry)
90 {
91     auto it = std::find(list.begin(), list.end(), entry);
92     return it == list.end() ? -1 : std::distance(list.begin(), it);
93 }
94 
95 template<class C, class E>
contains(const std::initializer_list<C> & list,const E & entry)96 static bool contains(const std::initializer_list<C> &list, const E &entry)
97 {
98     return indexOf(list, entry) >= 0;
99 }
100 
matchingStartBracket(const QChar c)101 static inline QChar matchingStartBracket(const QChar c)
102 {
103     switch (c.toLatin1()) {
104     case '}':
105         return QLatin1Char('{');
106     case ']':
107         return QLatin1Char('[');
108     case ')':
109         return QLatin1Char('(');
110     }
111     return QChar();
112 }
113 
matchingEndBracket(const QChar c,bool withQuotes=true)114 static inline QChar matchingEndBracket(const QChar c, bool withQuotes = true)
115 {
116     switch (c.toLatin1()) {
117     case '{':
118         return QLatin1Char('}');
119     case '[':
120         return QLatin1Char(']');
121     case '(':
122         return QLatin1Char(')');
123     case '\'':
124         return withQuotes ? QLatin1Char('\'') : QChar();
125     case '"':
126         return withQuotes ? QLatin1Char('"') : QChar();
127     }
128     return QChar();
129 }
130 
matchingBracket(const QChar c)131 static inline QChar matchingBracket(const QChar c)
132 {
133     QChar bracket = matchingStartBracket(c);
134     if (bracket.isNull()) {
135         bracket = matchingEndBracket(c, /*withQuotes=*/false);
136     }
137     return bracket;
138 }
139 
isStartBracket(const QChar c)140 static inline bool isStartBracket(const QChar c)
141 {
142     return !matchingEndBracket(c, /*withQuotes=*/false).isNull();
143 }
144 
isEndBracket(const QChar c)145 static inline bool isEndBracket(const QChar c)
146 {
147     return !matchingStartBracket(c).isNull();
148 }
149 
isBracket(const QChar c)150 static inline bool isBracket(const QChar c)
151 {
152     return isStartBracket(c) || isEndBracket(c);
153 }
154 
155 /**
156  * normalize given url
157  * @param url input url
158  * @return normalized url
159  */
normalizeUrl(const QUrl & url)160 static QUrl normalizeUrl(const QUrl &url)
161 {
162     // only normalize local urls
163 
164     if (url.isEmpty() || !url.isLocalFile()
165         || KNetworkMounts::self()->isOptionEnabledForPath(url.toLocalFile(), KNetworkMounts::StrongSideEffectsOptimizations)) {
166         return url;
167     }
168     // don't normalize if not existing!
169     // canonicalFilePath won't work!
170     const QString normalizedUrl(QFileInfo(url.toLocalFile()).canonicalFilePath());
171     if (normalizedUrl.isEmpty()) {
172         return url;
173     }
174 
175     // else: use canonicalFilePath to normalize
176     return QUrl::fromLocalFile(normalizedUrl);
177 }
178 
179 // BEGIN d'tor, c'tor
180 //
181 // KTextEditor::DocumentPrivate Constructor
182 //
DocumentPrivate(bool bSingleViewMode,bool bReadOnly,QWidget * parentWidget,QObject * parent)183 KTextEditor::DocumentPrivate::DocumentPrivate(bool bSingleViewMode, bool bReadOnly, QWidget *parentWidget, QObject *parent)
184     : KTextEditor::Document(this, parent)
185     , m_bSingleViewMode(bSingleViewMode)
186     , m_bReadOnly(bReadOnly)
187     ,
188 
189     m_undoManager(new KateUndoManager(this))
190     ,
191 
192     m_buffer(new KateBuffer(this))
193     , m_indenter(new KateAutoIndent(this))
194     ,
195 
196     m_docName(QStringLiteral("need init"))
197     ,
198 
199     m_fileType(QStringLiteral("Normal"))
200     ,
201 
202     m_config(new KateDocumentConfig(this))
203 
204 {
205     // no plugins from kparts here
206     setPluginLoadingMode(DoNotLoadPlugins);
207 
208     // setup component name
209     const auto &aboutData = EditorPrivate::self()->aboutData();
210     setComponentName(aboutData.componentName(), aboutData.displayName());
211 
212     // avoid spamming plasma and other window managers with progress dialogs
213     // we show such stuff inline in the views!
214     setProgressInfoEnabled(false);
215 
216     // register doc at factory
217     KTextEditor::EditorPrivate::self()->registerDocument(this);
218 
219     // normal hl
220     m_buffer->setHighlight(0);
221 
222     // swap file
223     m_swapfile = (config()->swapFileMode() == KateDocumentConfig::DisableSwapFile) ? nullptr : new Kate::SwapFile(this);
224 
225     // some nice signals from the buffer
226     connect(m_buffer, &KateBuffer::tagLines, this, &KTextEditor::DocumentPrivate::tagLines);
227 
228     // if the user changes the highlight with the dialog, notify the doc
229     connect(KateHlManager::self(), &KateHlManager::changed, this, &KTextEditor::DocumentPrivate::internalHlChanged);
230 
231     // signals for mod on hd
232     connect(KTextEditor::EditorPrivate::self()->dirWatch(), &KDirWatch::dirty, this, &KTextEditor::DocumentPrivate::slotModOnHdDirty);
233 
234     connect(KTextEditor::EditorPrivate::self()->dirWatch(), &KDirWatch::created, this, &KTextEditor::DocumentPrivate::slotModOnHdCreated);
235 
236     connect(KTextEditor::EditorPrivate::self()->dirWatch(), &KDirWatch::deleted, this, &KTextEditor::DocumentPrivate::slotModOnHdDeleted);
237 
238     // singleshot timer to handle updates of mod on hd state delayed
239     m_modOnHdTimer.setSingleShot(true);
240     m_modOnHdTimer.setInterval(200);
241     connect(&m_modOnHdTimer, &QTimer::timeout, this, &KTextEditor::DocumentPrivate::slotDelayedHandleModOnHd);
242 
243     // Setup auto reload stuff
244     m_autoReloadMode = new KToggleAction(i18n("Auto Reload Document"), this);
245     m_autoReloadMode->setWhatsThis(i18n("Automatic reload the document when it was changed on disk"));
246     connect(m_autoReloadMode, &KToggleAction::triggered, this, &DocumentPrivate::autoReloadToggled);
247     // Prepare some reload amok protector...
248     m_autoReloadThrottle.setSingleShot(true);
249     //...but keep the value small in unit tests
250     m_autoReloadThrottle.setInterval(KTextEditor::EditorPrivate::self()->unitTestMode() ? 50 : 3000);
251     connect(&m_autoReloadThrottle, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload);
252 
253     // load handling
254     // this is needed to ensure we signal the user if a file is still loading
255     // and to disallow him to edit in that time
256     connect(this, &KTextEditor::DocumentPrivate::started, this, &KTextEditor::DocumentPrivate::slotStarted);
257     connect(this, qOverload<>(&KTextEditor::DocumentPrivate::completed), this, &KTextEditor::DocumentPrivate::slotCompleted);
258     connect(this, &KTextEditor::DocumentPrivate::canceled, this, &KTextEditor::DocumentPrivate::slotCanceled);
259 
260     // handle doc name updates
261     connect(this, &KParts::ReadOnlyPart::urlChanged, this, &KTextEditor::DocumentPrivate::slotUrlChanged);
262     updateDocName();
263 
264     // if single view mode, like in the konqui embedding, create a default view ;)
265     // be lazy, only create it now, if any parentWidget is given, otherwise widget()
266     // will create it on demand...
267     if (m_bSingleViewMode && parentWidget) {
268         KTextEditor::View *view = (KTextEditor::View *)createView(parentWidget);
269         insertChildClient(view);
270         view->setContextMenu(view->defaultContextMenu());
271         setWidget(view);
272     }
273 
274     connect(m_undoManager, &KateUndoManager::undoChanged, this, &KTextEditor::DocumentPrivate::undoChanged);
275     connect(m_undoManager, &KateUndoManager::undoStart, this, &KTextEditor::DocumentPrivate::editingStarted);
276     connect(m_undoManager, &KateUndoManager::undoEnd, this, &KTextEditor::DocumentPrivate::editingFinished);
277     connect(m_undoManager, &KateUndoManager::redoStart, this, &KTextEditor::DocumentPrivate::editingStarted);
278     connect(m_undoManager, &KateUndoManager::redoEnd, this, &KTextEditor::DocumentPrivate::editingFinished);
279 
280     connect(this, &KTextEditor::DocumentPrivate::sigQueryClose, this, &KTextEditor::DocumentPrivate::slotQueryClose_save);
281 
282     connect(this, &KTextEditor::DocumentPrivate::aboutToInvalidateMovingInterfaceContent, this, &KTextEditor::DocumentPrivate::clearEditingPosStack);
283     onTheFlySpellCheckingEnabled(config()->onTheFlySpellCheck());
284 
285     // make sure correct defaults are set (indenter, ...)
286     updateConfig();
287 }
288 
289 //
290 // KTextEditor::DocumentPrivate Destructor
291 //
~DocumentPrivate()292 KTextEditor::DocumentPrivate::~DocumentPrivate()
293 {
294     // we need to disconnect this as it triggers in destructor of KParts::ReadOnlyPart but we have already deleted
295     // important stuff then
296     disconnect(this, &KParts::ReadOnlyPart::urlChanged, this, &KTextEditor::DocumentPrivate::slotUrlChanged);
297 
298     // delete pending mod-on-hd message, if applicable
299     delete m_modOnHdHandler;
300 
301     // we are about to delete cursors/ranges/...
302     Q_EMIT aboutToDeleteMovingInterfaceContent(this);
303 
304     // kill it early, it has ranges!
305     delete m_onTheFlyChecker;
306     m_onTheFlyChecker = nullptr;
307 
308     clearDictionaryRanges();
309 
310     // Tell the world that we're about to close (== destruct)
311     // Apps must receive this in a direct signal-slot connection, and prevent
312     // any further use of interfaces once they return.
313     Q_EMIT aboutToClose(this);
314 
315     // remove file from dirwatch
316     deactivateDirWatch();
317 
318     // thanks for offering, KPart, but we're already self-destructing
319     setAutoDeleteWidget(false);
320     setAutoDeletePart(false);
321 
322     // clean up remaining views
323     qDeleteAll(m_views.keys());
324     m_views.clear();
325 
326     // clean up marks
327     for (auto &mark : std::as_const(m_marks)) {
328         delete mark;
329     }
330     m_marks.clear();
331 
332     // de-register document early from global collections
333     // otherwise we might "use" them again during destruction in a half-valid state
334     // see e.g. bug 422546 for similar issues with view
335     // this is still early enough, as as long as m_config is valid, this document is still "OK"
336     KTextEditor::EditorPrivate::self()->deregisterDocument(this);
337 }
338 // END
339 
saveEditingPositions(const KTextEditor::Cursor cursor)340 void KTextEditor::DocumentPrivate::saveEditingPositions(const KTextEditor::Cursor cursor)
341 {
342     if (m_editingStackPosition != m_editingStack.size() - 1) {
343         m_editingStack.resize(m_editingStackPosition);
344     }
345 
346     // try to be clever: reuse existing cursors if possible
347     QSharedPointer<KTextEditor::MovingCursor> mc;
348 
349     // we might pop last one: reuse that
350     if (!m_editingStack.isEmpty() && cursor.line() == m_editingStack.top()->line()) {
351         mc = m_editingStack.pop();
352     }
353 
354     // we might expire oldest one, reuse that one, if not already one there
355     // we prefer the other one for reuse, as already on the right line aka in the right block!
356     const int editingStackSizeLimit = 32;
357     if (m_editingStack.size() >= editingStackSizeLimit) {
358         if (mc) {
359             m_editingStack.removeFirst();
360         } else {
361             mc = m_editingStack.takeFirst();
362         }
363     }
364 
365     // new cursor needed? or adjust existing one?
366     if (mc) {
367         mc->setPosition(cursor);
368     } else {
369         mc = QSharedPointer<KTextEditor::MovingCursor>(newMovingCursor(cursor));
370     }
371 
372     // add new one as top of stack
373     m_editingStack.push(mc);
374     m_editingStackPosition = m_editingStack.size() - 1;
375 }
376 
lastEditingPosition(EditingPositionKind nextOrPrev,KTextEditor::Cursor currentCursor)377 KTextEditor::Cursor KTextEditor::DocumentPrivate::lastEditingPosition(EditingPositionKind nextOrPrev, KTextEditor::Cursor currentCursor)
378 {
379     if (m_editingStack.isEmpty()) {
380         return KTextEditor::Cursor::invalid();
381     }
382     auto targetPos = m_editingStack.at(m_editingStackPosition)->toCursor();
383     if (targetPos == currentCursor) {
384         if (nextOrPrev == Previous) {
385             m_editingStackPosition--;
386         } else {
387             m_editingStackPosition++;
388         }
389         m_editingStackPosition = qBound(0, m_editingStackPosition, m_editingStack.size() - 1);
390     }
391     return m_editingStack.at(m_editingStackPosition)->toCursor();
392 }
393 
clearEditingPosStack()394 void KTextEditor::DocumentPrivate::clearEditingPosStack()
395 {
396     m_editingStack.clear();
397     m_editingStackPosition = -1;
398 }
399 
400 // on-demand view creation
widget()401 QWidget *KTextEditor::DocumentPrivate::widget()
402 {
403     // no singleViewMode -> no widget()...
404     if (!singleViewMode()) {
405         return nullptr;
406     }
407 
408     // does a widget exist already? use it!
409     if (KTextEditor::Document::widget()) {
410         return KTextEditor::Document::widget();
411     }
412 
413     // create and return one...
414     KTextEditor::View *view = (KTextEditor::View *)createView(nullptr);
415     insertChildClient(view);
416     view->setContextMenu(view->defaultContextMenu());
417     setWidget(view);
418     return view;
419 }
420 
421 // BEGIN KTextEditor::Document stuff
422 
createView(QWidget * parent,KTextEditor::MainWindow * mainWindow)423 KTextEditor::View *KTextEditor::DocumentPrivate::createView(QWidget *parent, KTextEditor::MainWindow *mainWindow)
424 {
425     KTextEditor::ViewPrivate *newView = new KTextEditor::ViewPrivate(this, parent, mainWindow);
426 
427     if (m_fileChangedDialogsActivated) {
428         connect(newView, &KTextEditor::ViewPrivate::focusIn, this, &KTextEditor::DocumentPrivate::slotModifiedOnDisk);
429     }
430 
431     Q_EMIT viewCreated(this, newView);
432 
433     // post existing messages to the new view, if no specific view is given
434     const auto keys = m_messageHash.keys();
435     for (KTextEditor::Message *message : keys) {
436         if (!message->view()) {
437             newView->postMessage(message, m_messageHash[message]);
438         }
439     }
440 
441     return newView;
442 }
443 
rangeOnLine(KTextEditor::Range range,int line) const444 KTextEditor::Range KTextEditor::DocumentPrivate::rangeOnLine(KTextEditor::Range range, int line) const
445 {
446     const int col1 = toVirtualColumn(range.start());
447     const int col2 = toVirtualColumn(range.end());
448     return KTextEditor::Range(line, fromVirtualColumn(line, col1), line, fromVirtualColumn(line, col2));
449 }
450 
setMetaData(const KPluginMetaData & metaData)451 void KTextEditor::DocumentPrivate::setMetaData(const KPluginMetaData &metaData)
452 {
453     KParts::Part::setMetaData(metaData);
454 }
455 
456 // BEGIN KTextEditor::EditInterface stuff
457 
isEditingTransactionRunning() const458 bool KTextEditor::DocumentPrivate::isEditingTransactionRunning() const
459 {
460     return editSessionNumber > 0;
461 }
462 
text() const463 QString KTextEditor::DocumentPrivate::text() const
464 {
465     return m_buffer->text();
466 }
467 
text(const KTextEditor::Range & range,bool blockwise) const468 QString KTextEditor::DocumentPrivate::text(const KTextEditor::Range &range, bool blockwise) const
469 {
470     if (!range.isValid()) {
471         qCWarning(LOG_KTE) << "Text requested for invalid range" << range;
472         return QString();
473     }
474 
475     QString s;
476 
477     if (range.start().line() == range.end().line()) {
478         if (range.start().column() > range.end().column()) {
479             return QString();
480         }
481 
482         Kate::TextLine textLine = m_buffer->plainLine(range.start().line());
483 
484         if (!textLine) {
485             return QString();
486         }
487 
488         return textLine->string(range.start().column(), range.end().column() - range.start().column());
489     } else {
490         for (int i = range.start().line(); (i <= range.end().line()) && (i < m_buffer->count()); ++i) {
491             Kate::TextLine textLine = m_buffer->plainLine(i);
492 
493             if (!blockwise) {
494                 if (i == range.start().line()) {
495                     s.append(textLine->string(range.start().column(), textLine->length() - range.start().column()));
496                 } else if (i == range.end().line()) {
497                     s.append(textLine->string(0, range.end().column()));
498                 } else {
499                     s.append(textLine->string());
500                 }
501             } else {
502                 KTextEditor::Range subRange = rangeOnLine(range, i);
503                 s.append(textLine->string(subRange.start().column(), subRange.columnWidth()));
504             }
505 
506             if (i < range.end().line()) {
507                 s.append(QLatin1Char('\n'));
508             }
509         }
510     }
511 
512     return s;
513 }
514 
characterAt(const KTextEditor::Cursor & position) const515 QChar KTextEditor::DocumentPrivate::characterAt(const KTextEditor::Cursor &position) const
516 {
517     Kate::TextLine textLine = m_buffer->plainLine(position.line());
518 
519     if (!textLine) {
520         return QChar();
521     }
522 
523     return textLine->at(position.column());
524 }
525 
wordAt(const KTextEditor::Cursor & cursor) const526 QString KTextEditor::DocumentPrivate::wordAt(const KTextEditor::Cursor &cursor) const
527 {
528     return text(wordRangeAt(cursor));
529 }
530 
wordRangeAt(const KTextEditor::Cursor & cursor) const531 KTextEditor::Range KTextEditor::DocumentPrivate::wordRangeAt(const KTextEditor::Cursor &cursor) const
532 {
533     // get text line
534     const int line = cursor.line();
535     Kate::TextLine textLine = m_buffer->plainLine(line);
536     if (!textLine) {
537         return KTextEditor::Range::invalid();
538     }
539 
540     // make sure the cursor is
541     const int lineLenth = textLine->length();
542     if (cursor.column() > lineLenth) {
543         return KTextEditor::Range::invalid();
544     }
545 
546     int start = cursor.column();
547     int end = start;
548 
549     while (start > 0 && highlight()->isInWord(textLine->at(start - 1), textLine->attribute(start - 1))) {
550         start--;
551     }
552     while (end < lineLenth && highlight()->isInWord(textLine->at(end), textLine->attribute(end))) {
553         end++;
554     }
555 
556     return KTextEditor::Range(line, start, line, end);
557 }
558 
isValidTextPosition(const KTextEditor::Cursor & cursor) const559 bool KTextEditor::DocumentPrivate::isValidTextPosition(const KTextEditor::Cursor &cursor) const
560 {
561     const int ln = cursor.line();
562     const int col = cursor.column();
563     // cursor in document range?
564     if (ln < 0 || col < 0 || ln >= lines() || col > lineLength(ln)) {
565         return false;
566     }
567 
568     const QString str = line(ln);
569     Q_ASSERT(str.length() >= col);
570 
571     // cursor at end of line?
572     const int len = lineLength(ln);
573     if (col == 0 || col == len) {
574         return true;
575     }
576 
577     // cursor in the middle of a valid utf32-surrogate?
578     return (!str.at(col).isLowSurrogate()) || (!str.at(col - 1).isHighSurrogate());
579 }
580 
textLines(const KTextEditor::Range & range,bool blockwise) const581 QStringList KTextEditor::DocumentPrivate::textLines(const KTextEditor::Range &range, bool blockwise) const
582 {
583     QStringList ret;
584 
585     if (!range.isValid()) {
586         qCWarning(LOG_KTE) << "Text requested for invalid range" << range;
587         return ret;
588     }
589 
590     if (blockwise && (range.start().column() > range.end().column())) {
591         return ret;
592     }
593 
594     if (range.start().line() == range.end().line()) {
595         Q_ASSERT(range.start() <= range.end());
596 
597         Kate::TextLine textLine = m_buffer->plainLine(range.start().line());
598 
599         if (!textLine) {
600             return ret;
601         }
602 
603         ret << textLine->string(range.start().column(), range.end().column() - range.start().column());
604     } else {
605         for (int i = range.start().line(); (i <= range.end().line()) && (i < m_buffer->count()); ++i) {
606             Kate::TextLine textLine = m_buffer->plainLine(i);
607 
608             if (!blockwise) {
609                 if (i == range.start().line()) {
610                     ret << textLine->string(range.start().column(), textLine->length() - range.start().column());
611                 } else if (i == range.end().line()) {
612                     ret << textLine->string(0, range.end().column());
613                 } else {
614                     ret << textLine->string();
615                 }
616             } else {
617                 KTextEditor::Range subRange = rangeOnLine(range, i);
618                 ret << textLine->string(subRange.start().column(), subRange.columnWidth());
619             }
620         }
621     }
622 
623     return ret;
624 }
625 
line(int line) const626 QString KTextEditor::DocumentPrivate::line(int line) const
627 {
628     Kate::TextLine l = m_buffer->plainLine(line);
629 
630     if (!l) {
631         return QString();
632     }
633 
634     return l->string();
635 }
636 
setText(const QString & s)637 bool KTextEditor::DocumentPrivate::setText(const QString &s)
638 {
639     if (!isReadWrite()) {
640         return false;
641     }
642 
643     std::vector<KTextEditor::Mark> msave;
644     msave.reserve(m_marks.size());
645     std::transform(m_marks.cbegin(), m_marks.cend(), std::back_inserter(msave), [](KTextEditor::Mark *mark) {
646         return *mark;
647     });
648 
649     editStart();
650 
651     // delete the text
652     clear();
653 
654     // insert the new text
655     insertText(KTextEditor::Cursor(), s);
656 
657     editEnd();
658 
659     for (KTextEditor::Mark mark : msave) {
660         setMark(mark.line, mark.type);
661     }
662 
663     return true;
664 }
665 
setText(const QStringList & text)666 bool KTextEditor::DocumentPrivate::setText(const QStringList &text)
667 {
668     if (!isReadWrite()) {
669         return false;
670     }
671 
672     std::vector<KTextEditor::Mark> msave;
673     msave.reserve(m_marks.size());
674     std::transform(m_marks.cbegin(), m_marks.cend(), std::back_inserter(msave), [](KTextEditor::Mark *mark) {
675         return *mark;
676     });
677 
678     editStart();
679 
680     // delete the text
681     clear();
682 
683     // insert the new text
684     insertText(KTextEditor::Cursor::start(), text);
685 
686     editEnd();
687 
688     for (KTextEditor::Mark mark : msave) {
689         setMark(mark.line, mark.type);
690     }
691 
692     return true;
693 }
694 
clear()695 bool KTextEditor::DocumentPrivate::clear()
696 {
697     if (!isReadWrite()) {
698         return false;
699     }
700 
701     for (auto view : std::as_const(m_views)) {
702         view->clear();
703         view->tagAll();
704         view->update();
705     }
706 
707     clearMarks();
708 
709     Q_EMIT aboutToInvalidateMovingInterfaceContent(this);
710     m_buffer->invalidateRanges();
711 
712     Q_EMIT aboutToRemoveText(documentRange());
713 
714     return editRemoveLines(0, lastLine());
715 }
716 
insertText(const KTextEditor::Cursor & position,const QString & text,bool block)717 bool KTextEditor::DocumentPrivate::insertText(const KTextEditor::Cursor &position, const QString &text, bool block)
718 {
719     if (!isReadWrite()) {
720         return false;
721     }
722 
723     if (text.isEmpty()) {
724         return true;
725     }
726 
727     editStart();
728 
729     int currentLine = position.line();
730     int currentLineStart = 0;
731     const int totalLength = text.length();
732     int insertColumn = position.column();
733 
734     // pad with empty lines, if insert position is after last line
735     if (position.line() > lines()) {
736         int line = lines();
737         while (line <= position.line()) {
738             editInsertLine(line, QString());
739             line++;
740         }
741     }
742 
743     // compute expanded column for block mode
744     int positionColumnExpanded = insertColumn;
745     const int tabWidth = config()->tabWidth();
746     if (block) {
747         if (auto l = plainKateTextLine(currentLine)) {
748             positionColumnExpanded = l->toVirtualColumn(insertColumn, tabWidth);
749         }
750     }
751 
752     int pos = 0;
753     for (; pos < totalLength; pos++) {
754         const QChar &ch = text.at(pos);
755 
756         if (ch == QLatin1Char('\n')) {
757             // Only perform the text insert if there is text to insert
758             if (currentLineStart < pos) {
759                 editInsertText(currentLine, insertColumn, text.mid(currentLineStart, pos - currentLineStart));
760             }
761 
762             if (!block) {
763                 editWrapLine(currentLine, insertColumn + pos - currentLineStart);
764                 insertColumn = 0;
765             }
766 
767             currentLine++;
768 
769             if (block) {
770                 auto l = plainKateTextLine(currentLine);
771                 if (currentLine == lastLine() + 1) {
772                     editInsertLine(currentLine, QString());
773                 }
774                 insertColumn = positionColumnExpanded;
775                 if (l) {
776                     insertColumn = l->fromVirtualColumn(insertColumn, tabWidth);
777                 }
778             }
779 
780             currentLineStart = pos + 1;
781         }
782     }
783 
784     // Only perform the text insert if there is text to insert
785     if (currentLineStart < pos) {
786         editInsertText(currentLine, insertColumn, text.mid(currentLineStart, pos - currentLineStart));
787     }
788 
789     editEnd();
790     return true;
791 }
792 
insertText(const KTextEditor::Cursor & position,const QStringList & textLines,bool block)793 bool KTextEditor::DocumentPrivate::insertText(const KTextEditor::Cursor &position, const QStringList &textLines, bool block)
794 {
795     if (!isReadWrite()) {
796         return false;
797     }
798 
799     // just reuse normal function
800     return insertText(position, textLines.join(QLatin1Char('\n')), block);
801 }
802 
removeText(const KTextEditor::Range & _range,bool block)803 bool KTextEditor::DocumentPrivate::removeText(const KTextEditor::Range &_range, bool block)
804 {
805     KTextEditor::Range range = _range;
806 
807     if (!isReadWrite()) {
808         return false;
809     }
810 
811     // Should now be impossible to trigger with the new Range class
812     Q_ASSERT(range.start().line() <= range.end().line());
813 
814     if (range.start().line() > lastLine()) {
815         return false;
816     }
817 
818     if (!block) {
819         Q_EMIT aboutToRemoveText(range);
820     }
821 
822     editStart();
823 
824     if (!block) {
825         if (range.end().line() > lastLine()) {
826             range.setEnd(KTextEditor::Cursor(lastLine() + 1, 0));
827         }
828 
829         if (range.onSingleLine()) {
830             editRemoveText(range.start().line(), range.start().column(), range.columnWidth());
831         } else {
832             int from = range.start().line();
833             int to = range.end().line();
834 
835             // remove last line
836             if (to <= lastLine()) {
837                 editRemoveText(to, 0, range.end().column());
838             }
839 
840             // editRemoveLines() will be called on first line (to remove bookmark)
841             if (range.start().column() == 0 && from > 0) {
842                 --from;
843             }
844 
845             // remove middle lines
846             editRemoveLines(from + 1, to - 1);
847 
848             // remove first line if not already removed by editRemoveLines()
849             if (range.start().column() > 0 || range.start().line() == 0) {
850                 editRemoveText(from, range.start().column(), m_buffer->plainLine(from)->length() - range.start().column());
851                 editUnWrapLine(from);
852             }
853         }
854     } // if ( ! block )
855     else {
856         int startLine = qMax(0, range.start().line());
857         int vc1 = toVirtualColumn(range.start());
858         int vc2 = toVirtualColumn(range.end());
859         for (int line = qMin(range.end().line(), lastLine()); line >= startLine; --line) {
860             int col1 = fromVirtualColumn(line, vc1);
861             int col2 = fromVirtualColumn(line, vc2);
862             editRemoveText(line, qMin(col1, col2), qAbs(col2 - col1));
863         }
864     }
865 
866     editEnd();
867     return true;
868 }
869 
insertLine(int l,const QString & str)870 bool KTextEditor::DocumentPrivate::insertLine(int l, const QString &str)
871 {
872     if (!isReadWrite()) {
873         return false;
874     }
875 
876     if (l < 0 || l > lines()) {
877         return false;
878     }
879 
880     return editInsertLine(l, str);
881 }
882 
insertLines(int line,const QStringList & text)883 bool KTextEditor::DocumentPrivate::insertLines(int line, const QStringList &text)
884 {
885     if (!isReadWrite()) {
886         return false;
887     }
888 
889     if (line < 0 || line > lines()) {
890         return false;
891     }
892 
893     bool success = true;
894     for (const QString &string : text) {
895         success &= editInsertLine(line++, string);
896     }
897 
898     return success;
899 }
900 
removeLine(int line)901 bool KTextEditor::DocumentPrivate::removeLine(int line)
902 {
903     if (!isReadWrite()) {
904         return false;
905     }
906 
907     if (line < 0 || line > lastLine()) {
908         return false;
909     }
910 
911     return editRemoveLine(line);
912 }
913 
totalCharacters() const914 int KTextEditor::DocumentPrivate::totalCharacters() const
915 {
916     int l = 0;
917 
918     for (int i = 0; i < m_buffer->count(); ++i) {
919         Kate::TextLine line = m_buffer->plainLine(i);
920 
921         if (line) {
922             l += line->length();
923         }
924     }
925 
926     return l;
927 }
928 
lines() const929 int KTextEditor::DocumentPrivate::lines() const
930 {
931     return m_buffer->count();
932 }
933 
lineLength(int line) const934 int KTextEditor::DocumentPrivate::lineLength(int line) const
935 {
936     return m_buffer->lineLength(line);
937 }
938 
isLineModified(int line) const939 bool KTextEditor::DocumentPrivate::isLineModified(int line) const
940 {
941     if (line < 0 || line >= lines()) {
942         return false;
943     }
944 
945     Kate::TextLine l = m_buffer->plainLine(line);
946     Q_ASSERT(l);
947 
948     return l->markedAsModified();
949 }
950 
isLineSaved(int line) const951 bool KTextEditor::DocumentPrivate::isLineSaved(int line) const
952 {
953     if (line < 0 || line >= lines()) {
954         return false;
955     }
956 
957     Kate::TextLine l = m_buffer->plainLine(line);
958     Q_ASSERT(l);
959 
960     return l->markedAsSavedOnDisk();
961 }
962 
isLineTouched(int line) const963 bool KTextEditor::DocumentPrivate::isLineTouched(int line) const
964 {
965     if (line < 0 || line >= lines()) {
966         return false;
967     }
968 
969     Kate::TextLine l = m_buffer->plainLine(line);
970     Q_ASSERT(l);
971 
972     return l->markedAsModified() || l->markedAsSavedOnDisk();
973 }
974 // END
975 
976 // BEGIN KTextEditor::EditInterface internal stuff
977 //
978 // Starts an edit session with (or without) undo, update of view disabled during session
979 //
editStart()980 bool KTextEditor::DocumentPrivate::editStart()
981 {
982     editSessionNumber++;
983 
984     if (editSessionNumber > 1) {
985         return false;
986     }
987 
988     editIsRunning = true;
989 
990     // no last change cursor at start
991     m_editLastChangeStartCursor = KTextEditor::Cursor::invalid();
992 
993     m_undoManager->editStart();
994 
995     for (auto view : std::as_const(m_views)) {
996         view->editStart();
997     }
998 
999     m_buffer->editStart();
1000     return true;
1001 }
1002 
1003 //
1004 // End edit session and update Views
1005 //
editEnd()1006 bool KTextEditor::DocumentPrivate::editEnd()
1007 {
1008     if (editSessionNumber == 0) {
1009         Q_ASSERT(0);
1010         return false;
1011     }
1012 
1013     // wrap the new/changed text, if something really changed!
1014     if (m_buffer->editChanged() && (editSessionNumber == 1)) {
1015         if (m_undoManager->isActive() && config()->wordWrap()) {
1016             wrapText(m_buffer->editTagStart(), m_buffer->editTagEnd());
1017         }
1018     }
1019 
1020     editSessionNumber--;
1021 
1022     if (editSessionNumber > 0) {
1023         return false;
1024     }
1025 
1026     // end buffer edit, will trigger hl update
1027     // this will cause some possible adjustment of tagline start/end
1028     m_buffer->editEnd();
1029 
1030     m_undoManager->editEnd();
1031 
1032     // edit end for all views !!!!!!!!!
1033     for (auto view : std::as_const(m_views)) {
1034         view->editEnd(m_buffer->editTagStart(), m_buffer->editTagEnd(), m_buffer->editTagFrom());
1035     }
1036 
1037     if (m_buffer->editChanged()) {
1038         setModified(true);
1039         Q_EMIT textChanged(this);
1040     }
1041 
1042     // remember last change position in the stack, if any
1043     // this avoid costly updates for longer editing transactions
1044     // before we did that on textInsert/Removed
1045     if (m_editLastChangeStartCursor.isValid()) {
1046         saveEditingPositions(m_editLastChangeStartCursor);
1047     }
1048 
1049     editIsRunning = false;
1050     return true;
1051 }
1052 
pushEditState()1053 void KTextEditor::DocumentPrivate::pushEditState()
1054 {
1055     editStateStack.push(editSessionNumber);
1056 }
1057 
popEditState()1058 void KTextEditor::DocumentPrivate::popEditState()
1059 {
1060     if (editStateStack.isEmpty()) {
1061         return;
1062     }
1063 
1064     int count = editStateStack.pop() - editSessionNumber;
1065     while (count < 0) {
1066         ++count;
1067         editEnd();
1068     }
1069     while (count > 0) {
1070         --count;
1071         editStart();
1072     }
1073 }
1074 
inputMethodStart()1075 void KTextEditor::DocumentPrivate::inputMethodStart()
1076 {
1077     m_undoManager->inputMethodStart();
1078 }
1079 
inputMethodEnd()1080 void KTextEditor::DocumentPrivate::inputMethodEnd()
1081 {
1082     m_undoManager->inputMethodEnd();
1083 }
1084 
wrapText(int startLine,int endLine)1085 bool KTextEditor::DocumentPrivate::wrapText(int startLine, int endLine)
1086 {
1087     if (startLine < 0 || endLine < 0) {
1088         return false;
1089     }
1090 
1091     if (!isReadWrite()) {
1092         return false;
1093     }
1094 
1095     int col = config()->wordWrapAt();
1096 
1097     if (col == 0) {
1098         return false;
1099     }
1100 
1101     editStart();
1102 
1103     for (int line = startLine; (line <= endLine) && (line < lines()); line++) {
1104         Kate::TextLine l = kateTextLine(line);
1105 
1106         if (!l) {
1107             break;
1108         }
1109 
1110         // qCDebug(LOG_KTE) << "try wrap line: " << line;
1111 
1112         if (l->virtualLength(m_buffer->tabWidth()) > col) {
1113             Kate::TextLine nextl = kateTextLine(line + 1);
1114 
1115             // qCDebug(LOG_KTE) << "do wrap line: " << line;
1116 
1117             int eolPosition = l->length() - 1;
1118 
1119             // take tabs into account here, too
1120             int x = 0;
1121             const QString &t = l->string();
1122             int z2 = 0;
1123             for (; z2 < l->length(); z2++) {
1124                 static const QChar tabChar(QLatin1Char('\t'));
1125                 if (t.at(z2) == tabChar) {
1126                     x += m_buffer->tabWidth() - (x % m_buffer->tabWidth());
1127                 } else {
1128                     x++;
1129                 }
1130 
1131                 if (x > col) {
1132                     break;
1133                 }
1134             }
1135 
1136             const int colInChars = qMin(z2, l->length() - 1);
1137             int searchStart = colInChars;
1138 
1139             // If where we are wrapping is an end of line and is a space we don't
1140             // want to wrap there
1141             if (searchStart == eolPosition && t.at(searchStart).isSpace()) {
1142                 searchStart--;
1143             }
1144 
1145             // Scan backwards looking for a place to break the line
1146             // We are not interested in breaking at the first char
1147             // of the line (if it is a space), but we are at the second
1148             // anders: if we can't find a space, try breaking on a word
1149             // boundary, using KateHighlight::canBreakAt().
1150             // This could be a priority (setting) in the hl/filetype/document
1151             int z = -1;
1152             int nw = -1; // alternative position, a non word character
1153             for (z = searchStart; z >= 0; z--) {
1154                 if (t.at(z).isSpace()) {
1155                     break;
1156                 }
1157                 if ((nw < 0) && highlight()->canBreakAt(t.at(z), l->attribute(z))) {
1158                     nw = z;
1159                 }
1160             }
1161 
1162             if (z >= 0) {
1163                 // So why don't we just remove the trailing space right away?
1164                 // Well, the (view's) cursor may be directly in front of that space
1165                 // (user typing text before the last word on the line), and if that
1166                 // happens, the cursor would be moved to the next line, which is not
1167                 // what we want (bug #106261)
1168                 z++;
1169             } else {
1170                 // There was no space to break at so break at a nonword character if
1171                 // found, or at the wrapcolumn ( that needs be configurable )
1172                 // Don't try and add any white space for the break
1173                 if ((nw >= 0) && nw < colInChars) {
1174                     nw++; // break on the right side of the character
1175                 }
1176                 z = (nw >= 0) ? nw : colInChars;
1177             }
1178 
1179             if (nextl && !nextl->isAutoWrapped()) {
1180                 editWrapLine(line, z, true);
1181                 editMarkLineAutoWrapped(line + 1, true);
1182 
1183                 endLine++;
1184             } else {
1185                 if (nextl && (nextl->length() > 0) && !nextl->at(0).isSpace() && ((l->length() < 1) || !l->at(l->length() - 1).isSpace())) {
1186                     editInsertText(line + 1, 0, QStringLiteral(" "));
1187                 }
1188 
1189                 bool newLineAdded = false;
1190                 editWrapLine(line, z, false, &newLineAdded);
1191 
1192                 editMarkLineAutoWrapped(line + 1, true);
1193 
1194                 endLine++;
1195             }
1196         }
1197     }
1198 
1199     editEnd();
1200 
1201     return true;
1202 }
1203 
wrapParagraph(int first,int last)1204 bool KTextEditor::DocumentPrivate::wrapParagraph(int first, int last)
1205 {
1206     if (first == last) {
1207         return wrapText(first, last);
1208     }
1209 
1210     if (first < 0 || last < first) {
1211         return false;
1212     }
1213 
1214     if (last >= lines() || first > last) {
1215         return false;
1216     }
1217 
1218     if (!isReadWrite()) {
1219         return false;
1220     }
1221 
1222     editStart();
1223 
1224     // Because we shrink and expand lines, we need to track the working set by powerful "MovingStuff"
1225     std::unique_ptr<KTextEditor::MovingRange> range(newMovingRange(KTextEditor::Range(first, 0, last, 0)));
1226     std::unique_ptr<KTextEditor::MovingCursor> curr(newMovingCursor(KTextEditor::Cursor(range->start())));
1227 
1228     // Scan the selected range for paragraphs, whereas each empty line trigger a new paragraph
1229     for (int line = first; line <= range->end().line(); ++line) {
1230         // Is our first line a somehow filled line?
1231         if (plainKateTextLine(first)->firstChar() < 0) {
1232             // Fast forward to first non empty line
1233             ++first;
1234             curr->setPosition(curr->line() + 1, 0);
1235             continue;
1236         }
1237 
1238         // Is our current line a somehow filled line? If not, wrap the paragraph
1239         if (plainKateTextLine(line)->firstChar() < 0) {
1240             curr->setPosition(line, 0); // Set on empty line
1241             joinLines(first, line - 1);
1242             // Don't wrap twice! That may cause a bad result
1243             if (!wordWrap()) {
1244                 wrapText(first, first);
1245             }
1246             first = curr->line() + 1;
1247             line = first;
1248         }
1249     }
1250 
1251     // If there was no paragraph, we need to wrap now
1252     bool needWrap = (curr->line() != range->end().line());
1253     if (needWrap && plainKateTextLine(first)->firstChar() != -1) {
1254         joinLines(first, range->end().line());
1255         // Don't wrap twice! That may cause a bad result
1256         if (!wordWrap()) {
1257             wrapText(first, first);
1258         }
1259     }
1260 
1261     editEnd();
1262     return true;
1263 }
1264 
editInsertText(int line,int col,const QString & s)1265 bool KTextEditor::DocumentPrivate::editInsertText(int line, int col, const QString &s)
1266 {
1267     // verbose debug
1268     EDIT_DEBUG << "editInsertText" << line << col << s;
1269 
1270     if (line < 0 || col < 0) {
1271         return false;
1272     }
1273 
1274     if (!isReadWrite()) {
1275         return false;
1276     }
1277 
1278     int length = lineLength(line);
1279 
1280     if (length < 0) {
1281         return false;
1282     }
1283 
1284     // nothing to do, do nothing!
1285     if (s.isEmpty()) {
1286         return true;
1287     }
1288 
1289     editStart();
1290 
1291     QString s2 = s;
1292     int col2 = col;
1293     if (col2 > length) {
1294         s2 = QString(col2 - length, QLatin1Char(' ')) + s;
1295         col2 = length;
1296     }
1297 
1298     m_undoManager->slotTextInserted(line, col2, s2);
1299 
1300     // remember last change cursor
1301     m_editLastChangeStartCursor = KTextEditor::Cursor(line, col2);
1302 
1303     // insert text into line
1304     m_buffer->insertText(m_editLastChangeStartCursor, s2);
1305 
1306     Q_EMIT textInsertedRange(this, KTextEditor::Range(line, col2, line, col2 + s2.length()));
1307 
1308     editEnd();
1309 
1310     return true;
1311 }
1312 
editRemoveText(int line,int col,int len)1313 bool KTextEditor::DocumentPrivate::editRemoveText(int line, int col, int len)
1314 {
1315     // verbose debug
1316     EDIT_DEBUG << "editRemoveText" << line << col << len;
1317 
1318     if (line < 0 || col < 0 || len < 0) {
1319         return false;
1320     }
1321 
1322     if (!isReadWrite()) {
1323         return false;
1324     }
1325 
1326     Kate::TextLine l = plainKateTextLine(line);
1327 
1328     if (!l) {
1329         return false;
1330     }
1331 
1332     // nothing to do, do nothing!
1333     if (len == 0) {
1334         return true;
1335     }
1336 
1337     // wrong column
1338     if (col >= l->text().size()) {
1339         return false;
1340     }
1341 
1342     // don't try to remove what's not there
1343     len = qMin(len, l->text().size() - col);
1344 
1345     editStart();
1346 
1347     QString oldText = l->string().mid(col, len);
1348 
1349     m_undoManager->slotTextRemoved(line, col, oldText);
1350 
1351     // remember last change cursor
1352     m_editLastChangeStartCursor = KTextEditor::Cursor(line, col);
1353 
1354     // remove text from line
1355     m_buffer->removeText(KTextEditor::Range(m_editLastChangeStartCursor, KTextEditor::Cursor(line, col + len)));
1356 
1357     Q_EMIT textRemoved(this, KTextEditor::Range(line, col, line, col + len), oldText);
1358 
1359     editEnd();
1360 
1361     return true;
1362 }
1363 
editMarkLineAutoWrapped(int line,bool autowrapped)1364 bool KTextEditor::DocumentPrivate::editMarkLineAutoWrapped(int line, bool autowrapped)
1365 {
1366     // verbose debug
1367     EDIT_DEBUG << "editMarkLineAutoWrapped" << line << autowrapped;
1368 
1369     if (line < 0) {
1370         return false;
1371     }
1372 
1373     if (!isReadWrite()) {
1374         return false;
1375     }
1376 
1377     Kate::TextLine l = kateTextLine(line);
1378 
1379     if (!l) {
1380         return false;
1381     }
1382 
1383     editStart();
1384 
1385     m_undoManager->slotMarkLineAutoWrapped(line, autowrapped);
1386 
1387     l->setAutoWrapped(autowrapped);
1388 
1389     editEnd();
1390 
1391     return true;
1392 }
1393 
editWrapLine(int line,int col,bool newLine,bool * newLineAdded)1394 bool KTextEditor::DocumentPrivate::editWrapLine(int line, int col, bool newLine, bool *newLineAdded)
1395 {
1396     // verbose debug
1397     EDIT_DEBUG << "editWrapLine" << line << col << newLine;
1398 
1399     if (line < 0 || col < 0) {
1400         return false;
1401     }
1402 
1403     if (!isReadWrite()) {
1404         return false;
1405     }
1406 
1407     int length = lineLength(line);
1408 
1409     if (length < 0) {
1410         return false;
1411     }
1412 
1413     editStart();
1414 
1415     const bool nextLineValid = lineLength(line + 1) >= 0;
1416 
1417     m_undoManager->slotLineWrapped(line, col, length - col, (!nextLineValid || newLine));
1418 
1419     if (!nextLineValid || newLine) {
1420         m_buffer->wrapLine(KTextEditor::Cursor(line, col));
1421 
1422         QVarLengthArray<KTextEditor::Mark *, 8> list;
1423         for (const auto &mark : std::as_const(m_marks)) {
1424             if (mark->line >= line) {
1425                 if ((col == 0) || (mark->line > line)) {
1426                     list.push_back(mark);
1427                 }
1428             }
1429         }
1430 
1431         for (const auto &mark : list) {
1432             m_marks.take(mark->line);
1433         }
1434 
1435         for (const auto &mark : list) {
1436             mark->line++;
1437             m_marks.insert(mark->line, mark);
1438         }
1439 
1440         if (!list.empty()) {
1441             Q_EMIT marksChanged(this);
1442         }
1443 
1444         // yes, we added a new line !
1445         if (newLineAdded) {
1446             (*newLineAdded) = true;
1447         }
1448     } else {
1449         m_buffer->wrapLine(KTextEditor::Cursor(line, col));
1450         m_buffer->unwrapLine(line + 2);
1451 
1452         // no, no new line added !
1453         if (newLineAdded) {
1454             (*newLineAdded) = false;
1455         }
1456     }
1457 
1458     // remember last change cursor
1459     m_editLastChangeStartCursor = KTextEditor::Cursor(line, col);
1460 
1461     Q_EMIT textInsertedRange(this, KTextEditor::Range(line, col, line + 1, 0));
1462 
1463     editEnd();
1464 
1465     return true;
1466 }
1467 
editUnWrapLine(int line,bool removeLine,int length)1468 bool KTextEditor::DocumentPrivate::editUnWrapLine(int line, bool removeLine, int length)
1469 {
1470     // verbose debug
1471     EDIT_DEBUG << "editUnWrapLine" << line << removeLine << length;
1472 
1473     if (line < 0 || length < 0) {
1474         return false;
1475     }
1476 
1477     if (!isReadWrite()) {
1478         return false;
1479     }
1480 
1481     int col = lineLength(line);
1482     bool lineValid = col >= 0;
1483     bool nextLineValid = lineLength(line + 1) >= 0;
1484 
1485     if (!lineValid || !nextLineValid) {
1486         return false;
1487     }
1488 
1489     editStart();
1490 
1491     m_undoManager->slotLineUnWrapped(line, col, length, removeLine);
1492 
1493     if (removeLine) {
1494         m_buffer->unwrapLine(line + 1);
1495     } else {
1496         m_buffer->wrapLine(KTextEditor::Cursor(line + 1, length));
1497         m_buffer->unwrapLine(line + 1);
1498     }
1499 
1500     QVarLengthArray<KTextEditor::Mark *, 8> list;
1501     for (const auto &mark : std::as_const(m_marks)) {
1502         if (mark->line >= line + 1) {
1503             list.push_back(mark);
1504         }
1505 
1506         if (mark->line == line + 1) {
1507             auto m = m_marks.take(line);
1508             if (m) {
1509                 mark->type |= m->type;
1510                 delete m;
1511             }
1512         }
1513     }
1514 
1515     for (const auto &mark : list) {
1516         m_marks.take(mark->line);
1517     }
1518 
1519     for (const auto &mark : list) {
1520         mark->line--;
1521         m_marks.insert(mark->line, mark);
1522     }
1523 
1524     if (!list.isEmpty()) {
1525         Q_EMIT marksChanged(this);
1526     }
1527 
1528     // remember last change cursor
1529     m_editLastChangeStartCursor = KTextEditor::Cursor(line, col);
1530 
1531     Q_EMIT textRemoved(this, KTextEditor::Range(line, col, line + 1, 0), QStringLiteral("\n"));
1532 
1533     editEnd();
1534 
1535     return true;
1536 }
1537 
editInsertLine(int line,const QString & s)1538 bool KTextEditor::DocumentPrivate::editInsertLine(int line, const QString &s)
1539 {
1540     // verbose debug
1541     EDIT_DEBUG << "editInsertLine" << line << s;
1542 
1543     if (line < 0) {
1544         return false;
1545     }
1546 
1547     if (!isReadWrite()) {
1548         return false;
1549     }
1550 
1551     if (line > lines()) {
1552         return false;
1553     }
1554 
1555     editStart();
1556 
1557     m_undoManager->slotLineInserted(line, s);
1558 
1559     // wrap line
1560     if (line > 0) {
1561         Kate::TextLine previousLine = m_buffer->line(line - 1);
1562         m_buffer->wrapLine(KTextEditor::Cursor(line - 1, previousLine->text().size()));
1563     } else {
1564         m_buffer->wrapLine(KTextEditor::Cursor(0, 0));
1565     }
1566 
1567     // insert text
1568     m_buffer->insertText(KTextEditor::Cursor(line, 0), s);
1569 
1570     Kate::TextLine tl = m_buffer->line(line);
1571 
1572     QVarLengthArray<KTextEditor::Mark *, 8> list;
1573     for (const auto &mark : std::as_const(m_marks)) {
1574         if (mark->line >= line) {
1575             list.push_back(mark);
1576         }
1577     }
1578 
1579     for (const auto &mark : list) {
1580         m_marks.take(mark->line);
1581     }
1582 
1583     for (const auto &mark : list) {
1584         mark->line++;
1585         m_marks.insert(mark->line, mark);
1586     }
1587 
1588     if (!list.isEmpty()) {
1589         Q_EMIT marksChanged(this);
1590     }
1591 
1592     KTextEditor::Range rangeInserted(line, 0, line, tl->length());
1593 
1594     if (line) {
1595         int prevLineLength = lineLength(line - 1);
1596         rangeInserted.setStart(KTextEditor::Cursor(line - 1, prevLineLength));
1597     } else {
1598         rangeInserted.setEnd(KTextEditor::Cursor(line + 1, 0));
1599     }
1600 
1601     // remember last change cursor
1602     m_editLastChangeStartCursor = rangeInserted.start();
1603 
1604     Q_EMIT textInsertedRange(this, rangeInserted);
1605 
1606     editEnd();
1607 
1608     return true;
1609 }
1610 
editRemoveLine(int line)1611 bool KTextEditor::DocumentPrivate::editRemoveLine(int line)
1612 {
1613     return editRemoveLines(line, line);
1614 }
1615 
editRemoveLines(int from,int to)1616 bool KTextEditor::DocumentPrivate::editRemoveLines(int from, int to)
1617 {
1618     // verbose debug
1619     EDIT_DEBUG << "editRemoveLines" << from << to;
1620 
1621     if (to < from || from < 0 || to > lastLine()) {
1622         return false;
1623     }
1624 
1625     if (!isReadWrite()) {
1626         return false;
1627     }
1628 
1629     if (lines() == 1) {
1630         return editRemoveText(0, 0, lineLength(0));
1631     }
1632 
1633     editStart();
1634     QStringList oldText;
1635 
1636     // first remove text
1637     for (int line = to; line >= from; --line) {
1638         Kate::TextLine tl = m_buffer->line(line);
1639         oldText.prepend(this->line(line));
1640         m_undoManager->slotLineRemoved(line, this->line(line));
1641 
1642         m_buffer->removeText(KTextEditor::Range(KTextEditor::Cursor(line, 0), KTextEditor::Cursor(line, tl->text().size())));
1643     }
1644 
1645     // then collapse lines
1646     for (int line = to; line >= from; --line) {
1647         // unwrap all lines, prefer to unwrap line behind, skip to wrap line 0
1648         if (line + 1 < m_buffer->lines()) {
1649             m_buffer->unwrapLine(line + 1);
1650         } else if (line) {
1651             m_buffer->unwrapLine(line);
1652         }
1653     }
1654 
1655     QVarLengthArray<int, 8> rmark;
1656     QVarLengthArray<KTextEditor::Mark *, 8> list;
1657 
1658     for (KTextEditor::Mark *mark : std::as_const(m_marks)) {
1659         int line = mark->line;
1660         if (line > to) {
1661             list << mark;
1662         } else if (line >= from) {
1663             rmark << line;
1664         }
1665     }
1666 
1667     for (int line : rmark) {
1668         delete m_marks.take(line);
1669     }
1670 
1671     for (auto mark : list) {
1672         m_marks.take(mark->line);
1673     }
1674 
1675     for (auto mark : list) {
1676         mark->line -= to - from + 1;
1677         m_marks.insert(mark->line, mark);
1678     }
1679 
1680     if (!list.isEmpty()) {
1681         Q_EMIT marksChanged(this);
1682     }
1683 
1684     KTextEditor::Range rangeRemoved(from, 0, to + 1, 0);
1685 
1686     if (to == lastLine() + to - from + 1) {
1687         rangeRemoved.setEnd(KTextEditor::Cursor(to, oldText.last().length()));
1688         if (from > 0) {
1689             int prevLineLength = lineLength(from - 1);
1690             rangeRemoved.setStart(KTextEditor::Cursor(from - 1, prevLineLength));
1691         }
1692     }
1693 
1694     // remember last change cursor
1695     m_editLastChangeStartCursor = rangeRemoved.start();
1696 
1697     Q_EMIT textRemoved(this, rangeRemoved, oldText.join(QLatin1Char('\n')) + QLatin1Char('\n'));
1698 
1699     editEnd();
1700 
1701     return true;
1702 }
1703 // END
1704 
1705 // BEGIN KTextEditor::UndoInterface stuff
undoCount() const1706 uint KTextEditor::DocumentPrivate::undoCount() const
1707 {
1708     return m_undoManager->undoCount();
1709 }
1710 
redoCount() const1711 uint KTextEditor::DocumentPrivate::redoCount() const
1712 {
1713     return m_undoManager->redoCount();
1714 }
1715 
undo()1716 void KTextEditor::DocumentPrivate::undo()
1717 {
1718     m_undoManager->undo();
1719 }
1720 
redo()1721 void KTextEditor::DocumentPrivate::redo()
1722 {
1723     m_undoManager->redo();
1724 }
1725 // END
1726 
1727 // BEGIN KTextEditor::SearchInterface stuff
1728 QVector<KTextEditor::Range>
searchText(const KTextEditor::Range & range,const QString & pattern,const KTextEditor::SearchOptions options) const1729 KTextEditor::DocumentPrivate::searchText(const KTextEditor::Range &range, const QString &pattern, const KTextEditor::SearchOptions options) const
1730 {
1731     const bool escapeSequences = options.testFlag(KTextEditor::EscapeSequences);
1732     const bool regexMode = options.testFlag(KTextEditor::Regex);
1733     const bool backwards = options.testFlag(KTextEditor::Backwards);
1734     const bool wholeWords = options.testFlag(KTextEditor::WholeWords);
1735     const Qt::CaseSensitivity caseSensitivity = options.testFlag(KTextEditor::CaseInsensitive) ? Qt::CaseInsensitive : Qt::CaseSensitive;
1736 
1737     if (regexMode) {
1738         // regexp search
1739         // escape sequences are supported by definition
1740         QRegularExpression::PatternOptions patternOptions;
1741         if (caseSensitivity == Qt::CaseInsensitive) {
1742             patternOptions |= QRegularExpression::CaseInsensitiveOption;
1743         }
1744         KateRegExpSearch searcher(this);
1745         return searcher.search(pattern, range, backwards, patternOptions);
1746     }
1747 
1748     if (escapeSequences) {
1749         // escaped search
1750         KatePlainTextSearch searcher(this, caseSensitivity, wholeWords);
1751         KTextEditor::Range match = searcher.search(KateRegExpSearch::escapePlaintext(pattern), range, backwards);
1752 
1753         QVector<KTextEditor::Range> result;
1754         result.append(match);
1755         return result;
1756     }
1757 
1758     // plaintext search
1759     KatePlainTextSearch searcher(this, caseSensitivity, wholeWords);
1760     KTextEditor::Range match = searcher.search(pattern, range, backwards);
1761 
1762     QVector<KTextEditor::Range> result;
1763     result.append(match);
1764     return result;
1765 }
1766 // END
1767 
dialogParent()1768 QWidget *KTextEditor::DocumentPrivate::dialogParent()
1769 {
1770     QWidget *w = widget();
1771 
1772     if (!w) {
1773         w = activeView();
1774 
1775         if (!w) {
1776             w = QApplication::activeWindow();
1777         }
1778     }
1779 
1780     return w;
1781 }
1782 
1783 // BEGIN KTextEditor::HighlightingInterface stuff
setMode(const QString & name)1784 bool KTextEditor::DocumentPrivate::setMode(const QString &name)
1785 {
1786     return updateFileType(name);
1787 }
1788 
defaultStyleAt(const KTextEditor::Cursor & position) const1789 KTextEditor::DefaultStyle KTextEditor::DocumentPrivate::defaultStyleAt(const KTextEditor::Cursor &position) const
1790 {
1791     // TODO, FIXME KDE5: in surrogate, use 2 bytes before
1792     if (!isValidTextPosition(position)) {
1793         return dsNormal;
1794     }
1795 
1796     int ds = const_cast<KTextEditor::DocumentPrivate *>(this)->defStyleNum(position.line(), position.column());
1797     if (ds < 0 || ds > static_cast<int>(dsError)) {
1798         return dsNormal;
1799     }
1800 
1801     return static_cast<DefaultStyle>(ds);
1802 }
1803 
mode() const1804 QString KTextEditor::DocumentPrivate::mode() const
1805 {
1806     return m_fileType;
1807 }
1808 
modes() const1809 QStringList KTextEditor::DocumentPrivate::modes() const
1810 {
1811     QStringList m;
1812 
1813     const QList<KateFileType *> &modeList = KTextEditor::EditorPrivate::self()->modeManager()->list();
1814     m.reserve(modeList.size());
1815     for (KateFileType *type : modeList) {
1816         m << type->name;
1817     }
1818 
1819     return m;
1820 }
1821 
setHighlightingMode(const QString & name)1822 bool KTextEditor::DocumentPrivate::setHighlightingMode(const QString &name)
1823 {
1824     int mode = KateHlManager::self()->nameFind(name);
1825     if (mode == -1) {
1826         return false;
1827     }
1828     m_buffer->setHighlight(mode);
1829     return true;
1830 }
1831 
highlightingMode() const1832 QString KTextEditor::DocumentPrivate::highlightingMode() const
1833 {
1834     return highlight()->name();
1835 }
1836 
highlightingModes() const1837 QStringList KTextEditor::DocumentPrivate::highlightingModes() const
1838 {
1839     const auto modeList = KateHlManager::self()->modeList();
1840     QStringList hls;
1841     hls.reserve(modeList.size());
1842     for (const auto &hl : modeList) {
1843         hls << hl.name();
1844     }
1845     return hls;
1846 }
1847 
highlightingModeSection(int index) const1848 QString KTextEditor::DocumentPrivate::highlightingModeSection(int index) const
1849 {
1850     return KateHlManager::self()->modeList().at(index).section();
1851 }
1852 
modeSection(int index) const1853 QString KTextEditor::DocumentPrivate::modeSection(int index) const
1854 {
1855     return KTextEditor::EditorPrivate::self()->modeManager()->list().at(index)->section;
1856 }
1857 
bufferHlChanged()1858 void KTextEditor::DocumentPrivate::bufferHlChanged()
1859 {
1860     // update all views
1861     makeAttribs(false);
1862 
1863     // deactivate indenter if necessary
1864     m_indenter->checkRequiredStyle();
1865 
1866     Q_EMIT highlightingModeChanged(this);
1867 }
1868 
setDontChangeHlOnSave()1869 void KTextEditor::DocumentPrivate::setDontChangeHlOnSave()
1870 {
1871     m_hlSetByUser = true;
1872 }
1873 
bomSetByUser()1874 void KTextEditor::DocumentPrivate::bomSetByUser()
1875 {
1876     m_bomSetByUser = true;
1877 }
1878 // END
1879 
1880 // BEGIN KTextEditor::SessionConfigInterface and KTextEditor::ParameterizedSessionConfigInterface stuff
readSessionConfig(const KConfigGroup & kconfig,const QSet<QString> & flags)1881 void KTextEditor::DocumentPrivate::readSessionConfig(const KConfigGroup &kconfig, const QSet<QString> &flags)
1882 {
1883     if (!flags.contains(QStringLiteral("SkipEncoding"))) {
1884         // get the encoding
1885         QString tmpenc = kconfig.readEntry("Encoding");
1886         if (!tmpenc.isEmpty() && (tmpenc != encoding())) {
1887             setEncoding(tmpenc);
1888         }
1889     }
1890 
1891     if (!flags.contains(QStringLiteral("SkipUrl"))) {
1892         // restore the url
1893         QUrl url(kconfig.readEntry("URL"));
1894 
1895         // open the file if url valid
1896         if (!url.isEmpty() && url.isValid()) {
1897             openUrl(url);
1898         } else {
1899             completed(); // perhaps this should be emitted at the end of this function
1900         }
1901     } else {
1902         completed(); // perhaps this should be emitted at the end of this function
1903     }
1904 
1905     if (!flags.contains(QStringLiteral("SkipMode"))) {
1906         // restore the filetype
1907         // NOTE: if the session config file contains an invalid Mode
1908         // (for example, one that was deleted or renamed), do not apply it
1909         if (kconfig.hasKey("Mode")) {
1910             // restore if set by user, too!
1911             m_fileTypeSetByUser = kconfig.readEntry("Mode Set By User", false);
1912             if (m_fileTypeSetByUser) {
1913                 updateFileType(kconfig.readEntry("Mode"));
1914             } else {
1915                 // Not set by user:
1916                 // - if it's not the default ("Normal") use the mode from the config file
1917                 // - if it's "Normal", use m_fileType which was detected by the code in openFile()
1918                 const QString modeFromCfg = kconfig.readEntry("Mode");
1919                 updateFileType(modeFromCfg != QLatin1String("Normal") ? modeFromCfg : m_fileType);
1920             }
1921         }
1922     }
1923 
1924     if (!flags.contains(QStringLiteral("SkipHighlighting"))) {
1925         // restore the hl stuff
1926         if (kconfig.hasKey("Highlighting")) {
1927             const int mode = KateHlManager::self()->nameFind(kconfig.readEntry("Highlighting"));
1928             if (mode >= 0) {
1929                 // restore if set by user, too! see bug 332605, otherwise we loose the hl later again on save
1930                 m_hlSetByUser = kconfig.readEntry("Highlighting Set By User", false);
1931 
1932                 if (m_hlSetByUser) {
1933                     m_buffer->setHighlight(mode);
1934                 } else {
1935                     // Not set by user, only set highlighting if it's not 0, the default,
1936                     // otherwise leave it the same as the highlighting set by updateFileType()
1937                     // which has already been called by openFile()
1938                     if (mode > 0) {
1939                         m_buffer->setHighlight(mode);
1940                     }
1941                 }
1942             }
1943         }
1944     }
1945 
1946     // indent mode
1947     config()->setIndentationMode(kconfig.readEntry("Indentation Mode", config()->indentationMode()));
1948 
1949     // Restore Bookmarks
1950     const QList<int> marks = kconfig.readEntry("Bookmarks", QList<int>());
1951     for (int i = 0; i < marks.count(); i++) {
1952         addMark(marks.at(i), KTextEditor::DocumentPrivate::markType01);
1953     }
1954 }
1955 
writeSessionConfig(KConfigGroup & kconfig,const QSet<QString> & flags)1956 void KTextEditor::DocumentPrivate::writeSessionConfig(KConfigGroup &kconfig, const QSet<QString> &flags)
1957 {
1958     if (this->url().isLocalFile()) {
1959         const QString path = this->url().toLocalFile();
1960         if (path.startsWith(QDir::tempPath())) {
1961             return; // inside tmp resource, do not save
1962         }
1963     }
1964 
1965     if (!flags.contains(QStringLiteral("SkipUrl"))) {
1966         // save url
1967         kconfig.writeEntry("URL", this->url().toString());
1968     }
1969 
1970     if (!flags.contains(QStringLiteral("SkipEncoding"))) {
1971         // save encoding
1972         kconfig.writeEntry("Encoding", encoding());
1973     }
1974 
1975     if (!flags.contains(QStringLiteral("SkipMode"))) {
1976         // save file type
1977         kconfig.writeEntry("Mode", m_fileType);
1978         // save if set by user, too!
1979         kconfig.writeEntry("Mode Set By User", m_fileTypeSetByUser);
1980     }
1981 
1982     if (!flags.contains(QStringLiteral("SkipHighlighting"))) {
1983         // save hl
1984         kconfig.writeEntry("Highlighting", highlight()->name());
1985 
1986         // save if set by user, too! see bug 332605, otherwise we loose the hl later again on save
1987         kconfig.writeEntry("Highlighting Set By User", m_hlSetByUser);
1988     }
1989 
1990     // indent mode
1991     kconfig.writeEntry("Indentation Mode", config()->indentationMode());
1992 
1993     // Save Bookmarks
1994     QList<int> marks;
1995     for (const auto &mark : std::as_const(m_marks)) {
1996         if (mark->type & KTextEditor::MarkInterface::markType01) {
1997             marks.push_back(mark->line);
1998         }
1999     }
2000 
2001     kconfig.writeEntry("Bookmarks", marks);
2002 }
2003 
2004 // END KTextEditor::SessionConfigInterface and KTextEditor::ParameterizedSessionConfigInterface stuff
2005 
mark(int line)2006 uint KTextEditor::DocumentPrivate::mark(int line)
2007 {
2008     KTextEditor::Mark *m = m_marks.value(line);
2009     if (!m) {
2010         return 0;
2011     }
2012 
2013     return m->type;
2014 }
2015 
setMark(int line,uint markType)2016 void KTextEditor::DocumentPrivate::setMark(int line, uint markType)
2017 {
2018     clearMark(line);
2019     addMark(line, markType);
2020 }
2021 
clearMark(int line)2022 void KTextEditor::DocumentPrivate::clearMark(int line)
2023 {
2024     if (line < 0 || line > lastLine()) {
2025         return;
2026     }
2027 
2028     if (auto mark = m_marks.take(line)) {
2029         Q_EMIT markChanged(this, *mark, MarkRemoved);
2030         Q_EMIT marksChanged(this);
2031         delete mark;
2032         tagLine(line);
2033         repaintViews(true);
2034     }
2035 }
2036 
addMark(int line,uint markType)2037 void KTextEditor::DocumentPrivate::addMark(int line, uint markType)
2038 {
2039     KTextEditor::Mark *mark;
2040 
2041     if (line < 0 || line > lastLine()) {
2042         return;
2043     }
2044 
2045     if (markType == 0) {
2046         return;
2047     }
2048 
2049     if ((mark = m_marks.value(line))) {
2050         // Remove bits already set
2051         markType &= ~mark->type;
2052 
2053         if (markType == 0) {
2054             return;
2055         }
2056 
2057         // Add bits
2058         mark->type |= markType;
2059     } else {
2060         mark = new KTextEditor::Mark;
2061         mark->line = line;
2062         mark->type = markType;
2063         m_marks.insert(line, mark);
2064     }
2065 
2066     // Emit with a mark having only the types added.
2067     KTextEditor::Mark temp;
2068     temp.line = line;
2069     temp.type = markType;
2070     Q_EMIT markChanged(this, temp, MarkAdded);
2071 
2072     Q_EMIT marksChanged(this);
2073     tagLine(line);
2074     repaintViews(true);
2075 }
2076 
removeMark(int line,uint markType)2077 void KTextEditor::DocumentPrivate::removeMark(int line, uint markType)
2078 {
2079     if (line < 0 || line > lastLine()) {
2080         return;
2081     }
2082 
2083     auto it = m_marks.find(line);
2084     if (it == m_marks.end()) {
2085         return;
2086     }
2087     KTextEditor::Mark *mark = it.value();
2088 
2089     // Remove bits not set
2090     markType &= mark->type;
2091 
2092     if (markType == 0) {
2093         return;
2094     }
2095 
2096     // Subtract bits
2097     mark->type &= ~markType;
2098 
2099     // Emit with a mark having only the types removed.
2100     KTextEditor::Mark temp;
2101     temp.line = line;
2102     temp.type = markType;
2103     Q_EMIT markChanged(this, temp, MarkRemoved);
2104 
2105     if (mark->type == 0) {
2106         m_marks.erase(it);
2107         delete mark;
2108     }
2109 
2110     Q_EMIT marksChanged(this);
2111     tagLine(line);
2112     repaintViews(true);
2113 }
2114 
marks()2115 const QHash<int, KTextEditor::Mark *> &KTextEditor::DocumentPrivate::marks()
2116 {
2117     return m_marks;
2118 }
2119 
requestMarkTooltip(int line,QPoint position)2120 void KTextEditor::DocumentPrivate::requestMarkTooltip(int line, QPoint position)
2121 {
2122     KTextEditor::Mark *mark = m_marks.value(line);
2123     if (!mark) {
2124         return;
2125     }
2126 
2127     bool handled = false;
2128     Q_EMIT markToolTipRequested(this, *mark, position, handled);
2129 }
2130 
handleMarkClick(int line)2131 bool KTextEditor::DocumentPrivate::handleMarkClick(int line)
2132 {
2133     bool handled = false;
2134     KTextEditor::Mark *mark = m_marks.value(line);
2135     if (!mark) {
2136         Q_EMIT markClicked(this, KTextEditor::Mark{line, 0}, handled);
2137     } else {
2138         Q_EMIT markClicked(this, *mark, handled);
2139     }
2140 
2141     return handled;
2142 }
2143 
handleMarkContextMenu(int line,QPoint position)2144 bool KTextEditor::DocumentPrivate::handleMarkContextMenu(int line, QPoint position)
2145 {
2146     bool handled = false;
2147     KTextEditor::Mark *mark = m_marks.value(line);
2148     if (!mark) {
2149         Q_EMIT markContextMenuRequested(this, KTextEditor::Mark{line, 0}, position, handled);
2150     } else {
2151         Q_EMIT markContextMenuRequested(this, *mark, position, handled);
2152     }
2153 
2154     return handled;
2155 }
2156 
clearMarks()2157 void KTextEditor::DocumentPrivate::clearMarks()
2158 {
2159     /**
2160      * work on a copy as deletions below might trigger the use
2161      * of m_marks
2162      */
2163     const QHash<int, KTextEditor::Mark *> marksCopy = m_marks;
2164     m_marks.clear();
2165 
2166     for (const auto &m : marksCopy) {
2167         Q_EMIT markChanged(this, *m, MarkRemoved);
2168         tagLine(m->line);
2169         delete m;
2170     }
2171 
2172     Q_EMIT marksChanged(this);
2173     repaintViews(true);
2174 }
2175 
setMarkPixmap(MarkInterface::MarkTypes type,const QPixmap & pixmap)2176 void KTextEditor::DocumentPrivate::setMarkPixmap(MarkInterface::MarkTypes type, const QPixmap &pixmap)
2177 {
2178     m_markIcons.insert(type, QVariant::fromValue(pixmap));
2179 }
2180 
setMarkDescription(MarkInterface::MarkTypes type,const QString & description)2181 void KTextEditor::DocumentPrivate::setMarkDescription(MarkInterface::MarkTypes type, const QString &description)
2182 {
2183     m_markDescriptions.insert(type, description);
2184 }
2185 
markPixmap(MarkInterface::MarkTypes type) const2186 QPixmap KTextEditor::DocumentPrivate::markPixmap(MarkInterface::MarkTypes type) const
2187 {
2188     auto icon = m_markIcons.value(type, QVariant::fromValue(QPixmap()));
2189     return (static_cast<QMetaType::Type>(icon.type()) == QMetaType::QIcon) ? icon.value<QIcon>().pixmap(32) : icon.value<QPixmap>();
2190 }
2191 
markColor(MarkInterface::MarkTypes type) const2192 QColor KTextEditor::DocumentPrivate::markColor(MarkInterface::MarkTypes type) const
2193 {
2194     uint reserved = (0x1 << KTextEditor::MarkInterface::reservedMarkersCount()) - 1;
2195     if ((uint)type >= (uint)markType01 && (uint)type <= reserved) {
2196         return KateRendererConfig::global()->lineMarkerColor(type);
2197     } else {
2198         return QColor();
2199     }
2200 }
2201 
markDescription(MarkInterface::MarkTypes type) const2202 QString KTextEditor::DocumentPrivate::markDescription(MarkInterface::MarkTypes type) const
2203 {
2204     return m_markDescriptions.value(type, QString());
2205 }
2206 
setEditableMarks(uint markMask)2207 void KTextEditor::DocumentPrivate::setEditableMarks(uint markMask)
2208 {
2209     m_editableMarks = markMask;
2210 }
2211 
editableMarks() const2212 uint KTextEditor::DocumentPrivate::editableMarks() const
2213 {
2214     return m_editableMarks;
2215 }
2216 // END
2217 
setMarkIcon(MarkInterface::MarkTypes markType,const QIcon & icon)2218 void KTextEditor::DocumentPrivate::setMarkIcon(MarkInterface::MarkTypes markType, const QIcon &icon)
2219 {
2220     m_markIcons.insert(markType, QVariant::fromValue(icon));
2221 }
2222 
markIcon(MarkInterface::MarkTypes markType) const2223 QIcon KTextEditor::DocumentPrivate::markIcon(MarkInterface::MarkTypes markType) const
2224 {
2225     auto icon = m_markIcons.value(markType, QVariant::fromValue(QIcon()));
2226     return (static_cast<QMetaType::Type>(icon.type()) == QMetaType::QIcon) ? icon.value<QIcon>() : QIcon(icon.value<QPixmap>());
2227 }
2228 
2229 // BEGIN KTextEditor::PrintInterface stuff
print()2230 bool KTextEditor::DocumentPrivate::print()
2231 {
2232     return KatePrinter::print(this);
2233 }
2234 
printPreview()2235 void KTextEditor::DocumentPrivate::printPreview()
2236 {
2237     KatePrinter::printPreview(this);
2238 }
2239 // END KTextEditor::PrintInterface stuff
2240 
2241 // BEGIN KTextEditor::DocumentInfoInterface (### unfinished)
mimeType()2242 QString KTextEditor::DocumentPrivate::mimeType()
2243 {
2244     if (!m_modOnHd && url().isLocalFile()) {
2245         // for unmodified files that reside directly on disk, we don't need to
2246         // create a temporary buffer - we can just look at the file directly
2247         return QMimeDatabase().mimeTypeForFile(url().toLocalFile()).name();
2248     }
2249     // collect first 4k of text
2250     // only heuristic
2251     QByteArray buf;
2252     for (int i = 0; (i < lines()) && (buf.size() <= 4096); ++i) {
2253         buf.append(line(i).toUtf8());
2254         buf.append('\n');
2255     }
2256 
2257     // use path of url, too, if set
2258     if (!url().path().isEmpty()) {
2259         return QMimeDatabase().mimeTypeForFileNameAndData(url().path(), buf).name();
2260     }
2261 
2262     // else only use the content
2263     return QMimeDatabase().mimeTypeForData(buf).name();
2264 }
2265 // END KTextEditor::DocumentInfoInterface
2266 
2267 // BEGIN: error
showAndSetOpeningErrorAccess()2268 void KTextEditor::DocumentPrivate::showAndSetOpeningErrorAccess()
2269 {
2270     QPointer<KTextEditor::Message> message = new KTextEditor::Message(
2271         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.",
2272              this->url().toDisplayString(QUrl::PreferLocalFile)),
2273         KTextEditor::Message::Error);
2274     message->setWordWrap(true);
2275     QAction *tryAgainAction = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")),
2276                                           i18nc("translators: you can also translate 'Try Again' with 'Reload'", "Try Again"),
2277                                           nullptr);
2278     connect(tryAgainAction, &QAction::triggered, this, &KTextEditor::DocumentPrivate::documentReload, Qt::QueuedConnection);
2279 
2280     QAction *closeAction = new QAction(QIcon::fromTheme(QStringLiteral("window-close")), i18n("&Close"), nullptr);
2281     closeAction->setToolTip(i18n("Close message"));
2282 
2283     // add try again and close actions
2284     message->addAction(tryAgainAction);
2285     message->addAction(closeAction);
2286 
2287     // finally post message
2288     postMessage(message);
2289 
2290     // remember error
2291     m_openingError = true;
2292     m_openingErrorMessage = i18n("The file %1 could not be loaded, as it was not possible to read from it.\n\nCheck if you have read access to this file.",
2293                                  this->url().toDisplayString(QUrl::PreferLocalFile));
2294 }
2295 // END: error
2296 
openWithLineLengthLimitOverride()2297 void KTextEditor::DocumentPrivate::openWithLineLengthLimitOverride()
2298 {
2299     // raise line length limit to the next power of 2
2300     const int longestLine = m_buffer->longestLineLoaded();
2301     int newLimit = pow(2, ceil(log2(longestLine)));
2302     if (newLimit <= longestLine) {
2303         newLimit *= 2;
2304     }
2305 
2306     // do the raise
2307     config()->setLineLengthLimit(newLimit);
2308 
2309     // just reload
2310     m_buffer->clear();
2311     openFile();
2312     if (!m_openingError) {
2313         setReadWrite(true);
2314         m_readWriteStateBeforeLoading = true;
2315     }
2316 }
2317 
lineLengthLimit() const2318 int KTextEditor::DocumentPrivate::lineLengthLimit() const
2319 {
2320     return config()->lineLengthLimit();
2321 }
2322 
2323 // BEGIN KParts::ReadWrite stuff
openFile()2324 bool KTextEditor::DocumentPrivate::openFile()
2325 {
2326     // we are about to invalidate all cursors/ranges/.. => m_buffer->openFile will do so
2327     Q_EMIT aboutToInvalidateMovingInterfaceContent(this);
2328 
2329     // no open errors until now...
2330     m_openingError = false;
2331     m_openingErrorMessage.clear();
2332 
2333     // add new m_file to dirwatch
2334     activateDirWatch();
2335 
2336     // remember current encoding
2337     QString currentEncoding = encoding();
2338 
2339     //
2340     // mime type magic to get encoding right
2341     //
2342     QString mimeType = arguments().mimeType();
2343     int pos = mimeType.indexOf(QLatin1Char(';'));
2344     if (pos != -1 && !(m_reloading && m_userSetEncodingForNextReload)) {
2345         setEncoding(mimeType.mid(pos + 1));
2346     }
2347 
2348     // update file type, we do this here PRE-LOAD, therefore pass file name for reading from
2349     updateFileType(KTextEditor::EditorPrivate::self()->modeManager()->fileType(this, localFilePath()));
2350 
2351     // read dir config (if possible and wanted)
2352     // do this PRE-LOAD to get encoding info!
2353     readDirConfig();
2354 
2355     // perhaps we need to re-set again the user encoding
2356     if (m_reloading && m_userSetEncodingForNextReload && (currentEncoding != encoding())) {
2357         setEncoding(currentEncoding);
2358     }
2359 
2360     bool success = m_buffer->openFile(localFilePath(), (m_reloading && m_userSetEncodingForNextReload));
2361 
2362     //
2363     // yeah, success
2364     // read variables
2365     //
2366     if (success) {
2367         readVariables();
2368     }
2369 
2370     //
2371     // update views
2372     //
2373     for (auto view : std::as_const(m_views)) {
2374         // This is needed here because inserting the text moves the view's start position (it is a MovingCursor)
2375         view->setCursorPosition(KTextEditor::Cursor());
2376         view->updateView(true);
2377     }
2378 
2379     // Inform that the text has changed (required as we're not inside the usual editStart/End stuff)
2380     Q_EMIT textChanged(this);
2381     Q_EMIT loaded(this);
2382 
2383     //
2384     // to houston, we are not modified
2385     //
2386     if (m_modOnHd) {
2387         m_modOnHd = false;
2388         m_modOnHdReason = OnDiskUnmodified;
2389         m_prevModOnHdReason = OnDiskUnmodified;
2390         Q_EMIT modifiedOnDisk(this, m_modOnHd, m_modOnHdReason);
2391     }
2392 
2393     //
2394     // display errors
2395     //
2396     if (!success) {
2397         showAndSetOpeningErrorAccess();
2398     }
2399 
2400     // warn: broken encoding
2401     if (m_buffer->brokenEncoding()) {
2402         // this file can't be saved again without killing it
2403         setReadWrite(false);
2404         m_readWriteStateBeforeLoading = false;
2405         QPointer<KTextEditor::Message> message = new KTextEditor::Message(
2406             i18n("The file %1 was opened with %2 encoding but contained invalid characters.<br />"
2407                  "It is set to read-only mode, as saving might destroy its content.<br />"
2408                  "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.",
2409                  this->url().toDisplayString(QUrl::PreferLocalFile),
2410                  QString::fromLatin1(m_buffer->textCodec()->name())),
2411             KTextEditor::Message::Warning);
2412         message->setWordWrap(true);
2413         postMessage(message);
2414 
2415         // remember error
2416         m_openingError = true;
2417         m_openingErrorMessage = i18n(
2418             "The file %1 was opened with %2 encoding but contained invalid characters."
2419             " It is set to read-only mode, as saving might destroy its content."
2420             " 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.",
2421             this->url().toDisplayString(QUrl::PreferLocalFile),
2422             QString::fromLatin1(m_buffer->textCodec()->name()));
2423     }
2424 
2425     // warn: too long lines
2426     if (m_buffer->tooLongLinesWrapped()) {
2427         // this file can't be saved again without modifications
2428         setReadWrite(false);
2429         m_readWriteStateBeforeLoading = false;
2430         QPointer<KTextEditor::Message> message =
2431             new KTextEditor::Message(i18n("The file %1 was opened and contained lines longer than the configured Line Length Limit (%2 characters).<br />"
2432                                           "The longest of those lines was %3 characters long<br/>"
2433                                           "Those lines were wrapped and the document is set to read-only mode, as saving will modify its content.",
2434                                           this->url().toDisplayString(QUrl::PreferLocalFile),
2435                                           config()->lineLengthLimit(),
2436                                           m_buffer->longestLineLoaded()),
2437                                      KTextEditor::Message::Warning);
2438         QAction *increaseAndReload = new QAction(i18n("Temporarily raise limit and reload file"), message);
2439         connect(increaseAndReload, &QAction::triggered, this, &KTextEditor::DocumentPrivate::openWithLineLengthLimitOverride);
2440         message->addAction(increaseAndReload, true);
2441         message->addAction(new QAction(i18n("Close"), message), true);
2442         message->setWordWrap(true);
2443         postMessage(message);
2444 
2445         // remember error
2446         m_openingError = true;
2447         m_openingErrorMessage = i18n(
2448             "The file %1 was opened and contained lines longer than the configured Line Length Limit (%2 characters).<br/>"
2449             "The longest of those lines was %3 characters long<br/>"
2450             "Those lines were wrapped and the document is set to read-only mode, as saving will modify its content.",
2451             this->url().toDisplayString(QUrl::PreferLocalFile),
2452             config()->lineLengthLimit(),
2453             m_buffer->longestLineLoaded());
2454     }
2455 
2456     //
2457     // return the success
2458     //
2459     return success;
2460 }
2461 
saveFile()2462 bool KTextEditor::DocumentPrivate::saveFile()
2463 {
2464     // delete pending mod-on-hd message if applicable.
2465     delete m_modOnHdHandler;
2466 
2467     // some warnings, if file was changed by the outside!
2468     if (!url().isEmpty()) {
2469         if (m_fileChangedDialogsActivated && m_modOnHd) {
2470             QString str = reasonedMOHString() + QLatin1String("\n\n");
2471 
2472             if (!isModified()) {
2473                 if (KMessageBox::warningContinueCancel(
2474                         dialogParent(),
2475                         str + i18n("Do you really want to save this unmodified file? You could overwrite changed data in the file on disk."),
2476                         i18n("Trying to Save Unmodified File"),
2477                         KGuiItem(i18n("Save Nevertheless")))
2478                     != KMessageBox::Continue) {
2479                     return false;
2480                 }
2481             } else {
2482                 if (KMessageBox::warningContinueCancel(
2483                         dialogParent(),
2484                         str
2485                             + i18n(
2486                                 "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."),
2487                         i18n("Possible Data Loss"),
2488                         KGuiItem(i18n("Save Nevertheless")))
2489                     != KMessageBox::Continue) {
2490                     return false;
2491                 }
2492             }
2493         }
2494     }
2495 
2496     //
2497     // can we encode it if we want to save it ?
2498     //
2499     if (!m_buffer->canEncode()
2500         && (KMessageBox::warningContinueCancel(dialogParent(),
2501                                                i18n("The selected encoding cannot encode every Unicode character in this document. Do you really want to save "
2502                                                     "it? There could be some data lost."),
2503                                                i18n("Possible Data Loss"),
2504                                                KGuiItem(i18n("Save Nevertheless")))
2505             != KMessageBox::Continue)) {
2506         return false;
2507     }
2508 
2509     // create a backup file or abort if that fails!
2510     // if no backup file wanted, this routine will just return true
2511     if (!createBackupFile()) {
2512         return false;
2513     }
2514 
2515     // update file type, pass no file path, read file type content from this document
2516     QString oldPath = m_dirWatchFile;
2517 
2518     // only update file type if path has changed so that variables are not overridden on normal save
2519     if (oldPath != localFilePath()) {
2520         updateFileType(KTextEditor::EditorPrivate::self()->modeManager()->fileType(this, QString()));
2521 
2522         if (url().isLocalFile()) {
2523             // if file is local then read dir config for new path
2524             readDirConfig();
2525         }
2526     }
2527 
2528     // read our vars
2529     readVariables();
2530 
2531     // remove file from dirwatch
2532     deactivateDirWatch();
2533 
2534     // remove all trailing spaces in the document (as edit actions)
2535     // NOTE: we need this as edit actions, since otherwise the edit actions
2536     //       in the swap file recovery may happen at invalid cursor positions
2537     removeTrailingSpaces();
2538 
2539     //
2540     // try to save
2541     //
2542     if (!m_buffer->saveFile(localFilePath())) {
2543         // add m_file again to dirwatch
2544         activateDirWatch(oldPath);
2545         KMessageBox::error(dialogParent(),
2546                            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 "
2547                                 "that enough disk space is available.\nThe original file may be lost or damaged. "
2548                                 "Don't quit the application until the file is successfully written.",
2549                                 this->url().toDisplayString(QUrl::PreferLocalFile)));
2550         return false;
2551     }
2552 
2553     // update the checksum
2554     createDigest();
2555 
2556     // add m_file again to dirwatch
2557     activateDirWatch();
2558 
2559     //
2560     // we are not modified
2561     //
2562     if (m_modOnHd) {
2563         m_modOnHd = false;
2564         m_modOnHdReason = OnDiskUnmodified;
2565         m_prevModOnHdReason = OnDiskUnmodified;
2566         Q_EMIT modifiedOnDisk(this, m_modOnHd, m_modOnHdReason);
2567     }
2568 
2569     // (dominik) mark last undo group as not mergeable, otherwise the next
2570     // edit action might be merged and undo will never stop at the saved state
2571     m_undoManager->undoSafePoint();
2572     m_undoManager->updateLineModifications();
2573 
2574     //
2575     // return success
2576     //
2577     return true;
2578 }
2579 
createBackupFile()2580 bool KTextEditor::DocumentPrivate::createBackupFile()
2581 {
2582     // backup for local or remote files wanted?
2583     const bool backupLocalFiles = config()->backupOnSaveLocal();
2584     const bool backupRemoteFiles = config()->backupOnSaveRemote();
2585 
2586     // early out, before mount check: backup wanted at all?
2587     // => if not, all fine, just return
2588     if (!backupLocalFiles && !backupRemoteFiles) {
2589         return true;
2590     }
2591 
2592     // decide if we need backup based on locality
2593     // skip that, if we always want backups, as currentMountPoints is not that fast
2594     QUrl u(url());
2595     bool needBackup = backupLocalFiles && backupRemoteFiles;
2596     if (!needBackup) {
2597         bool slowOrRemoteFile = !u.isLocalFile();
2598         if (!slowOrRemoteFile) {
2599             // could be a mounted remote filesystem (e.g. nfs, sshfs, cifs)
2600             // we have the early out above to skip this, if we want no backup, which is the default
2601             KMountPoint::Ptr mountPoint = KMountPoint::currentMountPoints().findByDevice(u.toLocalFile());
2602             slowOrRemoteFile = (mountPoint && mountPoint->probablySlow());
2603         }
2604         needBackup = (!slowOrRemoteFile && backupLocalFiles) || (slowOrRemoteFile && backupRemoteFiles);
2605     }
2606 
2607     // no backup needed? be done
2608     if (!needBackup) {
2609         return true;
2610     }
2611 
2612     // else: try to backup
2613     const auto backupPrefix = KTextEditor::EditorPrivate::self()->variableExpansionManager()->expandText(config()->backupPrefix(), nullptr);
2614     const auto backupSuffix = KTextEditor::EditorPrivate::self()->variableExpansionManager()->expandText(config()->backupSuffix(), nullptr);
2615     if (backupPrefix.isEmpty() && backupSuffix.isEmpty()) {
2616         // no sane backup possible
2617         return true;
2618     }
2619 
2620     if (backupPrefix.contains(QDir::separator())) {
2621         // replace complete path, as prefix is a path!
2622         u.setPath(backupPrefix + u.fileName() + backupSuffix);
2623     } else {
2624         // replace filename in url
2625         const QString fileName = u.fileName();
2626         u = u.adjusted(QUrl::RemoveFilename);
2627         u.setPath(u.path() + backupPrefix + fileName + backupSuffix);
2628     }
2629 
2630     qCDebug(LOG_KTE) << "backup src file name: " << url();
2631     qCDebug(LOG_KTE) << "backup dst file name: " << u;
2632 
2633     // handle the backup...
2634     bool backupSuccess = false;
2635 
2636     // local file mode, no kio
2637     if (u.isLocalFile()) {
2638         if (QFile::exists(url().toLocalFile())) {
2639             // first: check if backupFile is already there, if true, unlink it
2640             QFile backupFile(u.toLocalFile());
2641             if (backupFile.exists()) {
2642                 backupFile.remove();
2643             }
2644 
2645             backupSuccess = QFile::copy(url().toLocalFile(), u.toLocalFile());
2646         } else {
2647             backupSuccess = true;
2648         }
2649     } else { // remote file mode, kio
2650         // get the right permissions, start with safe default
2651         KIO::StatJob *statJob = KIO::statDetails(url(), KIO::StatJob::SourceSide, KIO::StatBasic);
2652         KJobWidgets::setWindow(statJob, QApplication::activeWindow());
2653         if (statJob->exec()) {
2654             // do a evil copy which will overwrite target if possible
2655             KFileItem item(statJob->statResult(), url());
2656             KIO::FileCopyJob *job = KIO::file_copy(url(), u, item.permissions(), KIO::Overwrite);
2657             KJobWidgets::setWindow(job, QApplication::activeWindow());
2658             backupSuccess = job->exec();
2659         } else {
2660             backupSuccess = true;
2661         }
2662     }
2663 
2664     // backup has failed, ask user how to proceed
2665     if (!backupSuccess
2666         && (KMessageBox::warningContinueCancel(dialogParent(),
2667                                                i18n("For file %1 no backup copy could be created before saving."
2668                                                     " If an error occurs while saving, you might lose the data of this file."
2669                                                     " A reason could be that the media you write to is full or the directory of the file is read-only for you.",
2670                                                     url().toDisplayString(QUrl::PreferLocalFile)),
2671                                                i18n("Failed to create backup copy."),
2672                                                KGuiItem(i18n("Try to Save Nevertheless")),
2673                                                KStandardGuiItem::cancel(),
2674                                                QStringLiteral("Backup Failed Warning"))
2675             != KMessageBox::Continue)) {
2676         return false;
2677     }
2678 
2679     return true;
2680 }
2681 
readDirConfig()2682 void KTextEditor::DocumentPrivate::readDirConfig()
2683 {
2684     if (!url().isLocalFile() || KNetworkMounts::self()->isOptionEnabledForPath(url().toLocalFile(), KNetworkMounts::MediumSideEffectsOptimizations)) {
2685         return;
2686     }
2687 
2688     // first search .kateconfig upwards
2689     // with recursion guard
2690     QSet<QString> seenDirectories;
2691     QDir dir(QFileInfo(localFilePath()).absolutePath());
2692     while (!seenDirectories.contains(dir.absolutePath())) {
2693         // fill recursion guard
2694         seenDirectories.insert(dir.absolutePath());
2695 
2696         // try to open config file in this dir
2697         QFile f(dir.absolutePath() + QLatin1String("/.kateconfig"));
2698         if (f.open(QIODevice::ReadOnly)) {
2699             QTextStream stream(&f);
2700 
2701             uint linesRead = 0;
2702             QString line = stream.readLine();
2703             while ((linesRead < 32) && !line.isNull()) {
2704                 readVariableLine(line);
2705 
2706                 line = stream.readLine();
2707 
2708                 linesRead++;
2709             }
2710 
2711             return;
2712         }
2713 
2714         // else: cd up, if possible or abort
2715         if (!dir.cdUp()) {
2716             break;
2717         }
2718     }
2719 
2720 #if EDITORCONFIG_FOUND
2721     // if there wasn’t any .kateconfig file and KTextEditor was compiled with
2722     // EditorConfig support, try to load document config from a .editorconfig
2723     // file, if such is provided
2724     EditorConfig editorConfig(this);
2725     editorConfig.parse();
2726 #endif
2727 }
2728 
activateDirWatch(const QString & useFileName)2729 void KTextEditor::DocumentPrivate::activateDirWatch(const QString &useFileName)
2730 {
2731     QString fileToUse = useFileName;
2732     if (fileToUse.isEmpty()) {
2733         fileToUse = localFilePath();
2734     }
2735 
2736     if (KNetworkMounts::self()->isOptionEnabledForPath(fileToUse, KNetworkMounts::KDirWatchDontAddWatches)) {
2737         return;
2738     }
2739 
2740     QFileInfo fileInfo = QFileInfo(fileToUse);
2741     if (fileInfo.isSymLink()) {
2742         // Monitor the actual data and not the symlink
2743         fileToUse = fileInfo.canonicalFilePath();
2744     }
2745 
2746     // same file as we are monitoring, return
2747     if (fileToUse == m_dirWatchFile) {
2748         return;
2749     }
2750 
2751     // remove the old watched file
2752     deactivateDirWatch();
2753 
2754     // add new file if needed
2755     if (url().isLocalFile() && !fileToUse.isEmpty()) {
2756         KTextEditor::EditorPrivate::self()->dirWatch()->addFile(fileToUse);
2757         m_dirWatchFile = fileToUse;
2758     }
2759 }
2760 
deactivateDirWatch()2761 void KTextEditor::DocumentPrivate::deactivateDirWatch()
2762 {
2763     if (!m_dirWatchFile.isEmpty()) {
2764         KTextEditor::EditorPrivate::self()->dirWatch()->removeFile(m_dirWatchFile);
2765     }
2766 
2767     m_dirWatchFile.clear();
2768 }
2769 
openUrl(const QUrl & url)2770 bool KTextEditor::DocumentPrivate::openUrl(const QUrl &url)
2771 {
2772     if (!m_reloading) {
2773         // Reset filetype when opening url
2774         m_fileTypeSetByUser = false;
2775     }
2776     bool res = KTextEditor::Document::openUrl(normalizeUrl(url));
2777     updateDocName();
2778     return res;
2779 }
2780 
closeUrl()2781 bool KTextEditor::DocumentPrivate::closeUrl()
2782 {
2783     //
2784     // file mod on hd
2785     //
2786     if (!m_reloading && !url().isEmpty()) {
2787         if (m_fileChangedDialogsActivated && m_modOnHd) {
2788             // make sure to not forget pending mod-on-hd handler
2789             delete m_modOnHdHandler;
2790 
2791             QWidget *parentWidget(dialogParent());
2792             if (!(KMessageBox::warningContinueCancel(parentWidget,
2793                                                      reasonedMOHString() + QLatin1String("\n\n")
2794                                                          + i18n("Do you really want to continue to close this file? Data loss may occur."),
2795                                                      i18n("Possible Data Loss"),
2796                                                      KGuiItem(i18n("Close Nevertheless")),
2797                                                      KStandardGuiItem::cancel(),
2798                                                      QStringLiteral("kate_close_modonhd_%1").arg(m_modOnHdReason))
2799                   == KMessageBox::Continue)) {
2800                 // reset reloading
2801                 m_reloading = false;
2802                 return false;
2803             }
2804         }
2805     }
2806 
2807     //
2808     // first call the normal kparts implementation
2809     //
2810     if (!KParts::ReadWritePart::closeUrl()) {
2811         // reset reloading
2812         m_reloading = false;
2813         return false;
2814     }
2815 
2816     // Tell the world that we're about to go ahead with the close
2817     if (!m_reloading) {
2818         Q_EMIT aboutToClose(this);
2819     }
2820 
2821     // delete all KTE::Messages
2822     if (!m_messageHash.isEmpty()) {
2823         const auto keys = m_messageHash.keys();
2824         for (KTextEditor::Message *message : keys) {
2825             delete message;
2826         }
2827     }
2828 
2829     // we are about to invalidate all cursors/ranges/.. => m_buffer->clear will do so
2830     Q_EMIT aboutToInvalidateMovingInterfaceContent(this);
2831 
2832     // remove file from dirwatch
2833     deactivateDirWatch();
2834 
2835     //
2836     // empty url + fileName
2837     //
2838     setUrl(QUrl());
2839     setLocalFilePath(QString());
2840 
2841     // we are not modified
2842     if (m_modOnHd) {
2843         m_modOnHd = false;
2844         m_modOnHdReason = OnDiskUnmodified;
2845         m_prevModOnHdReason = OnDiskUnmodified;
2846         Q_EMIT modifiedOnDisk(this, m_modOnHd, m_modOnHdReason);
2847     }
2848 
2849     // remove all marks
2850     clearMarks();
2851 
2852     // clear the buffer
2853     m_buffer->clear();
2854 
2855     // clear undo/redo history
2856     m_undoManager->clearUndo();
2857     m_undoManager->clearRedo();
2858 
2859     // no, we are no longer modified
2860     setModified(false);
2861 
2862     // we have no longer any hl
2863     m_buffer->setHighlight(0);
2864 
2865     // update all our views
2866     for (auto view : std::as_const(m_views)) {
2867         view->clearSelection(); // fix bug #118588
2868         view->clear();
2869     }
2870 
2871     // purge swap file
2872     if (m_swapfile) {
2873         m_swapfile->fileClosed();
2874     }
2875 
2876     // success
2877     return true;
2878 }
2879 
isDataRecoveryAvailable() const2880 bool KTextEditor::DocumentPrivate::isDataRecoveryAvailable() const
2881 {
2882     return m_swapfile && m_swapfile->shouldRecover();
2883 }
2884 
recoverData()2885 void KTextEditor::DocumentPrivate::recoverData()
2886 {
2887     if (isDataRecoveryAvailable()) {
2888         m_swapfile->recover();
2889     }
2890 }
2891 
discardDataRecovery()2892 void KTextEditor::DocumentPrivate::discardDataRecovery()
2893 {
2894     if (isDataRecoveryAvailable()) {
2895         m_swapfile->discard();
2896     }
2897 }
2898 
setReadWrite(bool rw)2899 void KTextEditor::DocumentPrivate::setReadWrite(bool rw)
2900 {
2901     if (isReadWrite() == rw) {
2902         return;
2903     }
2904 
2905     KParts::ReadWritePart::setReadWrite(rw);
2906 
2907     for (auto view : std::as_const(m_views)) {
2908         view->slotUpdateUndo();
2909         view->slotReadWriteChanged();
2910     }
2911 
2912     Q_EMIT readWriteChanged(this);
2913 }
2914 
setModified(bool m)2915 void KTextEditor::DocumentPrivate::setModified(bool m)
2916 {
2917     if (isModified() != m) {
2918         KParts::ReadWritePart::setModified(m);
2919 
2920         for (auto view : std::as_const(m_views)) {
2921             view->slotUpdateUndo();
2922         }
2923 
2924         Q_EMIT modifiedChanged(this);
2925     }
2926 
2927     m_undoManager->setModified(m);
2928 }
2929 // END
2930 
2931 // BEGIN Kate specific stuff ;)
2932 
makeAttribs(bool needInvalidate)2933 void KTextEditor::DocumentPrivate::makeAttribs(bool needInvalidate)
2934 {
2935     for (auto view : std::as_const(m_views)) {
2936         view->renderer()->updateAttributes();
2937     }
2938 
2939     if (needInvalidate) {
2940         m_buffer->invalidateHighlighting();
2941     }
2942 
2943     for (auto view : std::as_const(m_views)) {
2944         view->tagAll();
2945         view->updateView(true);
2946     }
2947 }
2948 
2949 // the attributes of a hl have changed, update
internalHlChanged()2950 void KTextEditor::DocumentPrivate::internalHlChanged()
2951 {
2952     makeAttribs();
2953 }
2954 
addView(KTextEditor::View * view)2955 void KTextEditor::DocumentPrivate::addView(KTextEditor::View *view)
2956 {
2957     Q_ASSERT(!m_views.contains(view));
2958     m_views.insert(view, static_cast<KTextEditor::ViewPrivate *>(view));
2959     m_viewsCache.append(view);
2960 
2961     // apply the view & renderer vars from the file type
2962     if (!m_fileType.isEmpty()) {
2963         readVariableLine(KTextEditor::EditorPrivate::self()->modeManager()->fileType(m_fileType).varLine, true);
2964     }
2965 
2966     // apply the view & renderer vars from the file
2967     readVariables(true);
2968 
2969     setActiveView(view);
2970 }
2971 
removeView(KTextEditor::View * view)2972 void KTextEditor::DocumentPrivate::removeView(KTextEditor::View *view)
2973 {
2974     Q_ASSERT(m_views.contains(view));
2975     m_views.remove(view);
2976     m_viewsCache.removeAll(view);
2977 
2978     if (activeView() == view) {
2979         setActiveView(nullptr);
2980     }
2981 }
2982 
setActiveView(KTextEditor::View * view)2983 void KTextEditor::DocumentPrivate::setActiveView(KTextEditor::View *view)
2984 {
2985     if (m_activeView == view) {
2986         return;
2987     }
2988 
2989     m_activeView = static_cast<KTextEditor::ViewPrivate *>(view);
2990 }
2991 
ownedView(KTextEditor::ViewPrivate * view)2992 bool KTextEditor::DocumentPrivate::ownedView(KTextEditor::ViewPrivate *view)
2993 {
2994     // do we own the given view?
2995     return (m_views.contains(view));
2996 }
2997 
toVirtualColumn(int line,int column) const2998 int KTextEditor::DocumentPrivate::toVirtualColumn(int line, int column) const
2999 {
3000     Kate::TextLine textLine = m_buffer->plainLine(line);
3001 
3002     if (textLine) {
3003         return textLine->toVirtualColumn(column, config()->tabWidth());
3004     } else {
3005         return 0;
3006     }
3007 }
3008 
toVirtualColumn(const KTextEditor::Cursor cursor) const3009 int KTextEditor::DocumentPrivate::toVirtualColumn(const KTextEditor::Cursor cursor) const
3010 {
3011     return toVirtualColumn(cursor.line(), cursor.column());
3012 }
3013 
fromVirtualColumn(int line,int column) const3014 int KTextEditor::DocumentPrivate::fromVirtualColumn(int line, int column) const
3015 {
3016     Kate::TextLine textLine = m_buffer->plainLine(line);
3017 
3018     if (textLine) {
3019         return textLine->fromVirtualColumn(column, config()->tabWidth());
3020     } else {
3021         return 0;
3022     }
3023 }
3024 
fromVirtualColumn(const KTextEditor::Cursor cursor) const3025 int KTextEditor::DocumentPrivate::fromVirtualColumn(const KTextEditor::Cursor cursor) const
3026 {
3027     return fromVirtualColumn(cursor.line(), cursor.column());
3028 }
3029 
typeChars(KTextEditor::ViewPrivate * view,QString chars)3030 void KTextEditor::DocumentPrivate::typeChars(KTextEditor::ViewPrivate *view, QString chars)
3031 {
3032     // nop for empty chars
3033     if (chars.isEmpty()) {
3034         return;
3035     }
3036 
3037     // auto bracket handling
3038     QChar closingBracket;
3039     if (view->config()->autoBrackets()) {
3040         // Check if entered closing bracket is already balanced
3041         const QChar typedChar = chars.at(0);
3042         const QChar openBracket = matchingStartBracket(typedChar);
3043         if (!openBracket.isNull()) {
3044             KTextEditor::Cursor curPos = view->cursorPosition();
3045             if ((characterAt(curPos) == typedChar) && findMatchingBracket(curPos, 123 /*Which value may best?*/).isValid()) {
3046                 // Do nothing
3047                 view->cursorRight();
3048                 return;
3049             }
3050         }
3051 
3052         // for newly inserted text: remember if we should auto add some bracket
3053         if (chars.size() == 1) {
3054             // we inserted a bracket? => remember the matching closing one
3055             closingBracket = matchingEndBracket(typedChar);
3056 
3057             // closing bracket for the autobracket we inserted earlier?
3058             if (m_currentAutobraceClosingChar == typedChar && m_currentAutobraceRange) {
3059                 // do nothing
3060                 m_currentAutobraceRange.reset(nullptr);
3061                 view->cursorRight();
3062                 return;
3063             }
3064         }
3065     }
3066 
3067     // Treat some char also as "auto bracket" only when we have a selection
3068     if (view->selection() && closingBracket.isNull() && view->config()->encloseSelectionInChars()) {
3069         const QChar typedChar = chars.at(0);
3070         if (view->config()->charsToEncloseSelection().contains(typedChar)) {
3071             // The unconditional mirroring cause no harm, but allows funny brackets
3072             closingBracket = typedChar.mirroredChar();
3073         }
3074     }
3075 
3076     editStart();
3077 
3078     // special handling if we want to add auto brackets to a selection
3079     if (view->selection() && !closingBracket.isNull()) {
3080         std::unique_ptr<KTextEditor::MovingRange> selectionRange(newMovingRange(view->selectionRange()));
3081         const int startLine = qMax(0, selectionRange->start().line());
3082         const int endLine = qMin(selectionRange->end().line(), lastLine());
3083         const bool blockMode = view->blockSelection() && (startLine != endLine);
3084         if (blockMode) {
3085             if (selectionRange->start().column() > selectionRange->end().column()) {
3086                 // Selection was done from right->left, requires special setting to ensure the new
3087                 // added brackets will not be part of the selection
3088                 selectionRange->setInsertBehaviors(MovingRange::ExpandLeft | MovingRange::ExpandRight);
3089             }
3090             // Add brackets to each line of the block
3091             const int startColumn = qMin(selectionRange->start().column(), selectionRange->end().column());
3092             const int endColumn = qMax(selectionRange->start().column(), selectionRange->end().column());
3093             const KTextEditor::Range workingRange(startLine, startColumn, endLine, endColumn);
3094             for (int line = startLine; line <= endLine; ++line) {
3095                 const KTextEditor::Range r(rangeOnLine(workingRange, line));
3096                 insertText(r.end(), QString(closingBracket));
3097                 view->slotTextInserted(view, r.end(), QString(closingBracket));
3098                 insertText(r.start(), chars);
3099                 view->slotTextInserted(view, r.start(), chars);
3100             }
3101 
3102         } else {
3103             // No block, just add to start & end of selection
3104             insertText(selectionRange->end(), QString(closingBracket));
3105             view->slotTextInserted(view, selectionRange->end(), QString(closingBracket));
3106             insertText(selectionRange->start(), chars);
3107             view->slotTextInserted(view, selectionRange->start(), chars);
3108         }
3109 
3110         // Refresh selection
3111         view->setSelection(selectionRange->toRange());
3112         view->setCursorPosition(selectionRange->end());
3113 
3114         editEnd();
3115         return;
3116     }
3117 
3118     // normal handling
3119     if (!view->config()->persistentSelection() && view->selection()) {
3120         view->removeSelectedText();
3121     }
3122 
3123     const KTextEditor::Cursor oldCur(view->cursorPosition());
3124 
3125     const bool multiLineBlockMode = view->blockSelection() && view->selection();
3126     if (view->currentInputMode()->overwrite()) {
3127         // blockmode multiline selection case: remove chars in every line
3128         const KTextEditor::Range selectionRange = view->selectionRange();
3129         const int startLine = multiLineBlockMode ? qMax(0, selectionRange.start().line()) : view->cursorPosition().line();
3130         const int endLine = multiLineBlockMode ? qMin(selectionRange.end().line(), lastLine()) : startLine;
3131         const int virtualColumn = toVirtualColumn(multiLineBlockMode ? selectionRange.end() : view->cursorPosition());
3132 
3133         for (int line = endLine; line >= startLine; --line) {
3134             Kate::TextLine textLine = m_buffer->plainLine(line);
3135             Q_ASSERT(textLine);
3136             const int column = fromVirtualColumn(line, virtualColumn);
3137             KTextEditor::Range r = KTextEditor::Range(KTextEditor::Cursor(line, column), qMin(chars.length(), textLine->length() - column));
3138 
3139             // replace mode needs to know what was removed so it can be restored with backspace
3140             if (oldCur.column() < lineLength(line)) {
3141                 QChar removed = characterAt(KTextEditor::Cursor(line, column));
3142                 view->currentInputMode()->overwrittenChar(removed);
3143             }
3144 
3145             removeText(r);
3146         }
3147     }
3148 
3149     chars = eventuallyReplaceTabs(view->cursorPosition(), chars);
3150 
3151     if (multiLineBlockMode) {
3152         KTextEditor::Range selectionRange = view->selectionRange();
3153         const int startLine = qMax(0, selectionRange.start().line());
3154         const int endLine = qMin(selectionRange.end().line(), lastLine());
3155         const int column = toVirtualColumn(selectionRange.end());
3156         for (int line = endLine; line >= startLine; --line) {
3157             editInsertText(line, fromVirtualColumn(line, column), chars);
3158         }
3159         int newSelectionColumn = toVirtualColumn(view->cursorPosition());
3160         selectionRange.setRange(KTextEditor::Cursor(selectionRange.start().line(), fromVirtualColumn(selectionRange.start().line(), newSelectionColumn)),
3161                                 KTextEditor::Cursor(selectionRange.end().line(), fromVirtualColumn(selectionRange.end().line(), newSelectionColumn)));
3162         view->setSelection(selectionRange);
3163     } else {
3164         insertText(view->cursorPosition(), chars);
3165     }
3166 
3167     // auto bracket handling for newly inserted text
3168     // we inserted a bracket?
3169     // => add the matching closing one to the view + input chars
3170     // try to preserve the cursor position
3171     bool skipAutobrace = closingBracket == QLatin1Char('\'');
3172     if (highlight() && skipAutobrace) {
3173         // skip adding ' in spellchecked areas, because those are text
3174         skipAutobrace = highlight()->spellCheckingRequiredForLocation(this, view->cursorPosition() - Cursor{0, 1});
3175     }
3176 
3177     const auto cursorPos(view->cursorPosition());
3178     if (!skipAutobrace && (closingBracket == QLatin1Char('\''))) {
3179         // skip auto quotes when these looks already balanced, bug 405089
3180         Kate::TextLine textLine = m_buffer->plainLine(cursorPos.line());
3181         // RegEx match quote, but not escaped quote, thanks to https://stackoverflow.com/a/11819111
3182         static const QRegularExpression re(QStringLiteral("(?<!\\\\)(?:\\\\\\\\)*\\\'"));
3183         const int count = textLine->text().left(cursorPos.column()).count(re);
3184         skipAutobrace = (count % 2 == 0) ? true : false;
3185     }
3186     if (!skipAutobrace && (closingBracket == QLatin1Char('\"'))) {
3187         // ...same trick for double quotes
3188         Kate::TextLine textLine = m_buffer->plainLine(cursorPos.line());
3189         static const QRegularExpression re(QStringLiteral("(?<!\\\\)(?:\\\\\\\\)*\\\""));
3190         const int count = textLine->text().left(cursorPos.column()).count(re);
3191         skipAutobrace = (count % 2 == 0) ? true : false;
3192     }
3193 
3194     if (!closingBracket.isNull() && !skipAutobrace) {
3195         // add bracket to the view
3196         const auto nextChar = view->document()->text({cursorPos, cursorPos + Cursor{0, 1}}).trimmed();
3197         if (nextChar.isEmpty() || !nextChar.at(0).isLetterOrNumber()) {
3198             insertText(view->cursorPosition(), QString(closingBracket));
3199             const auto insertedAt(view->cursorPosition());
3200             view->setCursorPosition(cursorPos);
3201             m_currentAutobraceRange.reset(newMovingRange({cursorPos - Cursor{0, 1}, insertedAt}, KTextEditor::MovingRange::DoNotExpand));
3202             connect(view, &View::cursorPositionChanged, this, &DocumentPrivate::checkCursorForAutobrace, Qt::UniqueConnection);
3203 
3204             // add bracket to chars inserted! needed for correct signals + indent
3205             chars.append(closingBracket);
3206         }
3207         m_currentAutobraceClosingChar = closingBracket;
3208     }
3209 
3210     // end edit session here, to have updated HL in userTypedChar!
3211     editEnd();
3212 
3213     // trigger indentation
3214     KTextEditor::Cursor b(view->cursorPosition());
3215     m_indenter->userTypedChar(view, b, chars.isEmpty() ? QChar() : chars.at(chars.length() - 1));
3216 
3217     // inform the view about the original inserted chars
3218     view->slotTextInserted(view, oldCur, chars);
3219 }
3220 
checkCursorForAutobrace(KTextEditor::View *,const KTextEditor::Cursor newPos)3221 void KTextEditor::DocumentPrivate::checkCursorForAutobrace(KTextEditor::View *, const KTextEditor::Cursor newPos)
3222 {
3223     if (m_currentAutobraceRange && !m_currentAutobraceRange->toRange().contains(newPos)) {
3224         m_currentAutobraceRange.reset();
3225     }
3226 }
3227 
newLine(KTextEditor::ViewPrivate * v,KTextEditor::DocumentPrivate::NewLineIndent indent)3228 void KTextEditor::DocumentPrivate::newLine(KTextEditor::ViewPrivate *v, KTextEditor::DocumentPrivate::NewLineIndent indent)
3229 {
3230     editStart();
3231 
3232     if (!v->config()->persistentSelection() && v->selection()) {
3233         v->removeSelectedText();
3234         v->clearSelection();
3235     }
3236 
3237     // query cursor position
3238     KTextEditor::Cursor c = v->cursorPosition();
3239 
3240     if (c.line() > lastLine()) {
3241         c.setLine(lastLine());
3242     }
3243 
3244     if (c.line() < 0) {
3245         c.setLine(0);
3246     }
3247 
3248     int ln = c.line();
3249 
3250     int len = lineLength(ln);
3251 
3252     if (c.column() > len) {
3253         c.setColumn(len);
3254     }
3255 
3256     // first: wrap line
3257     editWrapLine(c.line(), c.column());
3258 
3259     // update highlighting to have updated HL in userTypedChar!
3260     m_buffer->updateHighlighting();
3261 
3262     // second: if "indent" is true, indent the new line, if needed...
3263     if (indent == KTextEditor::DocumentPrivate::Indent) {
3264         m_indenter->userTypedChar(v, v->cursorPosition(), QLatin1Char('\n'));
3265     }
3266 
3267     editEnd();
3268 }
3269 
transpose(const KTextEditor::Cursor cursor)3270 void KTextEditor::DocumentPrivate::transpose(const KTextEditor::Cursor cursor)
3271 {
3272     Kate::TextLine textLine = m_buffer->plainLine(cursor.line());
3273 
3274     if (!textLine || (textLine->length() < 2)) {
3275         return;
3276     }
3277 
3278     uint col = cursor.column();
3279 
3280     if (col > 0) {
3281         col--;
3282     }
3283 
3284     if ((textLine->length() - col) < 2) {
3285         return;
3286     }
3287 
3288     uint line = cursor.line();
3289     QString s;
3290 
3291     // clever swap code if first character on the line swap right&left
3292     // otherwise left & right
3293     s.append(textLine->at(col + 1));
3294     s.append(textLine->at(col));
3295     // do the swap
3296 
3297     // do it right, never ever manipulate a textline
3298     editStart();
3299     editRemoveText(line, col, 2);
3300     editInsertText(line, col, s);
3301     editEnd();
3302 }
3303 
swapTextRanges(KTextEditor::Range firstWord,KTextEditor::Range secondWord)3304 void KTextEditor::DocumentPrivate::swapTextRanges(KTextEditor::Range firstWord, KTextEditor::Range secondWord)
3305 {
3306     Q_ASSERT(firstWord.isValid() && secondWord.isValid());
3307     Q_ASSERT(!firstWord.overlaps(secondWord));
3308     // ensure that secondWord comes AFTER firstWord
3309     if (firstWord.start().column() > secondWord.start().column() || firstWord.start().line() > secondWord.start().line()) {
3310         const KTextEditor::Range tempRange = firstWord;
3311         firstWord.setRange(secondWord);
3312         secondWord.setRange(tempRange);
3313     }
3314 
3315     const QString tempString = text(secondWord);
3316     editStart();
3317     // edit secondWord first as the range might be invalidated after editing firstWord
3318     replaceText(secondWord, text(firstWord));
3319     replaceText(firstWord, tempString);
3320     editEnd();
3321 }
3322 
backspace(KTextEditor::ViewPrivate * view,const KTextEditor::Cursor c)3323 void KTextEditor::DocumentPrivate::backspace(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor c)
3324 {
3325     if (!view->config()->persistentSelection() && view->selection()) {
3326         KTextEditor::Range range = view->selectionRange();
3327         editStart(); // Avoid bad selection in case of undo
3328         if (view->blockSelection() && view->selection() && range.start().column() > 0 && toVirtualColumn(range.start()) == toVirtualColumn(range.end())) {
3329             // Remove one character before vertical selection line by expanding the selection
3330             range.setStart(KTextEditor::Cursor(range.start().line(), range.start().column() - 1));
3331             view->setSelection(range);
3332         }
3333         view->removeSelectedText();
3334         editEnd();
3335         return;
3336     }
3337 
3338     uint col = qMax(c.column(), 0);
3339     uint line = qMax(c.line(), 0);
3340     if ((col == 0) && (line == 0)) {
3341         return;
3342     }
3343 
3344     const Kate::TextLine textLine = m_buffer->plainLine(line);
3345     // don't forget this check!!!! really!!!!
3346     if (!textLine) {
3347         return;
3348     }
3349 
3350     if (col > 0) {
3351         bool useNextBlock = false;
3352         if (config()->backspaceIndents()) {
3353             // backspace indents: erase to next indent position
3354             int colX = textLine->toVirtualColumn(col, config()->tabWidth());
3355             int pos = textLine->firstChar();
3356             if (pos > 0) {
3357                 pos = textLine->toVirtualColumn(pos, config()->tabWidth());
3358             }
3359             if (pos < 0 || pos >= (int)colX) {
3360                 // only spaces on left side of cursor
3361                 indent(KTextEditor::Range(line, 0, line, 0), -1);
3362             } else {
3363                 useNextBlock = true;
3364             }
3365         }
3366         if (!config()->backspaceIndents() || useNextBlock) {
3367             KTextEditor::Cursor beginCursor(line, 0);
3368             KTextEditor::Cursor endCursor(line, col);
3369             if (!view->config()->backspaceRemoveComposed()) { // Normal backspace behavior
3370                 beginCursor.setColumn(col - 1);
3371                 // move to left of surrogate pair
3372                 if (!isValidTextPosition(beginCursor)) {
3373                     Q_ASSERT(col >= 2);
3374                     beginCursor.setColumn(col - 2);
3375                 }
3376             } else {
3377                 beginCursor.setColumn(view->textLayout(c)->previousCursorPosition(c.column()));
3378             }
3379             removeText(KTextEditor::Range(beginCursor, endCursor));
3380             // in most cases cursor is moved by removeText, but we should do it manually
3381             // for past-end-of-line cursors in block mode
3382             view->setCursorPosition(beginCursor);
3383         }
3384 
3385     } else {
3386         // col == 0: wrap to previous line
3387         const Kate::TextLine textLine = m_buffer->plainLine(line - 1);
3388 
3389         if (line > 0 && textLine) {
3390             if (config()->wordWrap() && textLine->endsWith(QStringLiteral(" "))) {
3391                 // gg: in hard wordwrap mode, backspace must also eat the trailing space
3392                 removeText(KTextEditor::Range(line - 1, textLine->length() - 1, line, 0));
3393             } else {
3394                 removeText(KTextEditor::Range(line - 1, textLine->length(), line, 0));
3395             }
3396         }
3397     }
3398 
3399     if (m_currentAutobraceRange) {
3400         const auto r = m_currentAutobraceRange->toRange();
3401         if (r.columnWidth() == 1 && view->cursorPosition() == r.start()) {
3402             // start parenthesis removed and range length is 1, remove end as well
3403             del(view, view->cursorPosition());
3404             m_currentAutobraceRange.reset();
3405         }
3406     }
3407 }
3408 
del(KTextEditor::ViewPrivate * view,const KTextEditor::Cursor c)3409 void KTextEditor::DocumentPrivate::del(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor c)
3410 {
3411     if (!view->config()->persistentSelection() && view->selection()) {
3412         KTextEditor::Range range = view->selectionRange();
3413         editStart(); // Avoid bad selection in case of undo
3414         if (view->blockSelection() && toVirtualColumn(range.start()) == toVirtualColumn(range.end())) {
3415             // Remove one character after vertical selection line by expanding the selection
3416             range.setEnd(KTextEditor::Cursor(range.end().line(), range.end().column() + 1));
3417             view->setSelection(range);
3418         }
3419         view->removeSelectedText();
3420         editEnd();
3421         return;
3422     }
3423 
3424     if (c.column() < (int)m_buffer->plainLine(c.line())->length()) {
3425         KTextEditor::Cursor endCursor(c.line(), view->textLayout(c)->nextCursorPosition(c.column()));
3426         removeText(KTextEditor::Range(c, endCursor));
3427     } else if (c.line() < lastLine()) {
3428         removeText(KTextEditor::Range(c.line(), c.column(), c.line() + 1, 0));
3429     }
3430 }
3431 
paste(KTextEditor::ViewPrivate * view,const QString & text)3432 void KTextEditor::DocumentPrivate::paste(KTextEditor::ViewPrivate *view, const QString &text)
3433 {
3434     // nop if nothing to paste
3435     if (text.isEmpty()) {
3436         return;
3437     }
3438 
3439     // normalize line endings, to e.g. catch issues with \r\n in paste buffer
3440     // see bug 410951
3441     QString s = text;
3442     s.replace(QRegularExpression(QStringLiteral("\r\n?")), QStringLiteral("\n"));
3443 
3444     int lines = s.count(QLatin1Char('\n'));
3445     const bool isSingleLine = lines == 0;
3446 
3447     m_undoManager->undoSafePoint();
3448 
3449     editStart();
3450 
3451     KTextEditor::Cursor pos = view->cursorPosition();
3452 
3453     bool skipIndentOnPaste = false;
3454     if (isSingleLine) {
3455         const int length = lineLength(pos.line());
3456         // if its a single line and the line already contains some text, skip indenting
3457         skipIndentOnPaste = length > 0;
3458     }
3459 
3460     if (!view->config()->persistentSelection() && view->selection()) {
3461         pos = view->selectionRange().start();
3462         if (view->blockSelection()) {
3463             pos = rangeOnLine(view->selectionRange(), pos.line()).start();
3464             if (lines == 0) {
3465                 s += QLatin1Char('\n');
3466                 s = s.repeated(view->selectionRange().numberOfLines() + 1);
3467                 s.chop(1);
3468             }
3469         }
3470         view->removeSelectedText();
3471     }
3472 
3473     if (config()->ovr()) {
3474         const auto pasteLines = s.splitRef(QLatin1Char('\n'));
3475 
3476         if (!view->blockSelection()) {
3477             int endColumn = (pasteLines.count() == 1 ? pos.column() : 0) + pasteLines.last().length();
3478             removeText(KTextEditor::Range(pos, pos.line() + pasteLines.count() - 1, endColumn));
3479         } else {
3480             int maxi = qMin(pos.line() + pasteLines.count(), this->lines());
3481 
3482             for (int i = pos.line(); i < maxi; ++i) {
3483                 int pasteLength = pasteLines.at(i - pos.line()).length();
3484                 removeText(KTextEditor::Range(i, pos.column(), i, qMin(pasteLength + pos.column(), lineLength(i))));
3485             }
3486         }
3487     }
3488 
3489     insertText(pos, s, view->blockSelection());
3490     editEnd();
3491 
3492     // move cursor right for block select, as the user is moved right internal
3493     // even in that case, but user expects other behavior in block selection
3494     // mode !
3495     // just let cursor stay, that was it before I changed to moving ranges!
3496     if (view->blockSelection()) {
3497         view->setCursorPositionInternal(pos);
3498     }
3499 
3500     if (config()->indentPastedText()) {
3501         KTextEditor::Range range = KTextEditor::Range(KTextEditor::Cursor(pos.line(), 0), KTextEditor::Cursor(pos.line() + lines, 0));
3502         if (!skipIndentOnPaste) {
3503             m_indenter->indent(view, range);
3504         }
3505     }
3506 
3507     if (!view->blockSelection()) {
3508         Q_EMIT charactersSemiInteractivelyInserted(pos, s);
3509     }
3510     m_undoManager->undoSafePoint();
3511 }
3512 
indent(KTextEditor::Range range,int change)3513 void KTextEditor::DocumentPrivate::indent(KTextEditor::Range range, int change)
3514 {
3515     if (!isReadWrite()) {
3516         return;
3517     }
3518 
3519     editStart();
3520     m_indenter->changeIndent(range, change);
3521     editEnd();
3522 }
3523 
align(KTextEditor::ViewPrivate * view,const KTextEditor::Range & range)3524 void KTextEditor::DocumentPrivate::align(KTextEditor::ViewPrivate *view, const KTextEditor::Range &range)
3525 {
3526     m_indenter->indent(view, range);
3527 }
3528 
insertTab(KTextEditor::ViewPrivate * view,const KTextEditor::Cursor)3529 void KTextEditor::DocumentPrivate::insertTab(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor)
3530 {
3531     if (!isReadWrite()) {
3532         return;
3533     }
3534 
3535     int lineLen = line(view->cursorPosition().line()).length();
3536     KTextEditor::Cursor c = view->cursorPosition();
3537 
3538     editStart();
3539 
3540     if (!view->config()->persistentSelection() && view->selection()) {
3541         view->removeSelectedText();
3542     } else if (view->currentInputMode()->overwrite() && c.column() < lineLen) {
3543         KTextEditor::Range r = KTextEditor::Range(view->cursorPosition(), 1);
3544 
3545         // replace mode needs to know what was removed so it can be restored with backspace
3546         QChar removed = line(view->cursorPosition().line()).at(r.start().column());
3547         view->currentInputMode()->overwrittenChar(removed);
3548         removeText(r);
3549     }
3550 
3551     c = view->cursorPosition();
3552     editInsertText(c.line(), c.column(), QStringLiteral("\t"));
3553 
3554     editEnd();
3555 }
3556 
3557 /*
3558   Remove a given string at the beginning
3559   of the current line.
3560 */
removeStringFromBeginning(int line,const QString & str)3561 bool KTextEditor::DocumentPrivate::removeStringFromBeginning(int line, const QString &str)
3562 {
3563     Kate::TextLine textline = m_buffer->plainLine(line);
3564 
3565     KTextEditor::Cursor cursor(line, 0);
3566     bool there = textline->startsWith(str);
3567 
3568     if (!there) {
3569         cursor.setColumn(textline->firstChar());
3570         there = textline->matchesAt(cursor.column(), str);
3571     }
3572 
3573     if (there) {
3574         // Remove some chars
3575         removeText(KTextEditor::Range(cursor, str.length()));
3576     }
3577 
3578     return there;
3579 }
3580 
3581 /*
3582   Remove a given string at the end
3583   of the current line.
3584 */
removeStringFromEnd(int line,const QString & str)3585 bool KTextEditor::DocumentPrivate::removeStringFromEnd(int line, const QString &str)
3586 {
3587     Kate::TextLine textline = m_buffer->plainLine(line);
3588 
3589     KTextEditor::Cursor cursor(line, 0);
3590     bool there = textline->endsWith(str);
3591 
3592     if (there) {
3593         cursor.setColumn(textline->length() - str.length());
3594     } else {
3595         cursor.setColumn(textline->lastChar() - str.length() + 1);
3596         there = textline->matchesAt(cursor.column(), str);
3597     }
3598 
3599     if (there) {
3600         // Remove some chars
3601         removeText(KTextEditor::Range(cursor, str.length()));
3602     }
3603 
3604     return there;
3605 }
3606 
3607 /*
3608   Replace tabs by spaces in the given string, if enabled.
3609  */
eventuallyReplaceTabs(const KTextEditor::Cursor cursorPos,const QString & str) const3610 QString KTextEditor::DocumentPrivate::eventuallyReplaceTabs(const KTextEditor::Cursor cursorPos, const QString &str) const
3611 {
3612     const bool replacetabs = config()->replaceTabsDyn();
3613     if (!replacetabs) {
3614         return str;
3615     }
3616     const int indentWidth = config()->indentationWidth();
3617     static const QLatin1Char tabChar('\t');
3618 
3619     int column = cursorPos.column();
3620 
3621     // The result will always be at least as long as the input
3622     QString result;
3623     result.reserve(str.size());
3624 
3625     for (const QChar ch : str) {
3626         if (ch == tabChar) {
3627             // Insert only enough spaces to align to the next indentWidth column
3628             // This fixes bug #340212
3629             int spacesToInsert = indentWidth - (column % indentWidth);
3630             result += QString(spacesToInsert, QLatin1Char(' '));
3631             column += spacesToInsert;
3632         } else {
3633             // Just keep all other typed characters as-is
3634             result += ch;
3635             ++column;
3636         }
3637     }
3638     return result;
3639 }
3640 
3641 /*
3642   Add to the current line a comment line mark at the beginning.
3643 */
addStartLineCommentToSingleLine(int line,int attrib)3644 void KTextEditor::DocumentPrivate::addStartLineCommentToSingleLine(int line, int attrib)
3645 {
3646     QString commentLineMark = highlight()->getCommentSingleLineStart(attrib);
3647     int pos = -1;
3648 
3649     if (highlight()->getCommentSingleLinePosition(attrib) == KSyntaxHighlighting::CommentPosition::StartOfLine) {
3650         pos = 0;
3651         commentLineMark += QLatin1Char(' ');
3652     } else {
3653         const Kate::TextLine l = kateTextLine(line);
3654         pos = l->firstChar();
3655     }
3656 
3657     if (pos >= 0) {
3658         insertText(KTextEditor::Cursor(line, pos), commentLineMark);
3659     }
3660 }
3661 
3662 /*
3663   Remove from the current line a comment line mark at
3664   the beginning if there is one.
3665 */
removeStartLineCommentFromSingleLine(int line,int attrib)3666 bool KTextEditor::DocumentPrivate::removeStartLineCommentFromSingleLine(int line, int attrib)
3667 {
3668     const QString shortCommentMark = highlight()->getCommentSingleLineStart(attrib);
3669     const QString longCommentMark = shortCommentMark + QLatin1Char(' ');
3670 
3671     editStart();
3672 
3673     // Try to remove the long comment mark first
3674     bool removed = (removeStringFromBeginning(line, longCommentMark) || removeStringFromBeginning(line, shortCommentMark));
3675 
3676     editEnd();
3677 
3678     return removed;
3679 }
3680 
3681 /*
3682   Add to the current line a start comment mark at the
3683   beginning and a stop comment mark at the end.
3684 */
addStartStopCommentToSingleLine(int line,int attrib)3685 void KTextEditor::DocumentPrivate::addStartStopCommentToSingleLine(int line, int attrib)
3686 {
3687     const QString startCommentMark = highlight()->getCommentStart(attrib) + QLatin1Char(' ');
3688     const QString stopCommentMark = QLatin1Char(' ') + highlight()->getCommentEnd(attrib);
3689 
3690     editStart();
3691 
3692     // Add the start comment mark
3693     insertText(KTextEditor::Cursor(line, 0), startCommentMark);
3694 
3695     // Go to the end of the line
3696     const int col = m_buffer->plainLine(line)->length();
3697 
3698     // Add the stop comment mark
3699     insertText(KTextEditor::Cursor(line, col), stopCommentMark);
3700 
3701     editEnd();
3702 }
3703 
3704 /*
3705   Remove from the current line a start comment mark at
3706   the beginning and a stop comment mark at the end.
3707 */
removeStartStopCommentFromSingleLine(int line,int attrib)3708 bool KTextEditor::DocumentPrivate::removeStartStopCommentFromSingleLine(int line, int attrib)
3709 {
3710     const QString shortStartCommentMark = highlight()->getCommentStart(attrib);
3711     const QString longStartCommentMark = shortStartCommentMark + QLatin1Char(' ');
3712     const QString shortStopCommentMark = highlight()->getCommentEnd(attrib);
3713     const QString longStopCommentMark = QLatin1Char(' ') + shortStopCommentMark;
3714 
3715     editStart();
3716 
3717     // Try to remove the long start comment mark first
3718     const bool removedStart = (removeStringFromBeginning(line, longStartCommentMark) || removeStringFromBeginning(line, shortStartCommentMark));
3719 
3720     // Try to remove the long stop comment mark first
3721     const bool removedStop = removedStart && (removeStringFromEnd(line, longStopCommentMark) || removeStringFromEnd(line, shortStopCommentMark));
3722 
3723     editEnd();
3724 
3725     return (removedStart || removedStop);
3726 }
3727 
3728 /*
3729   Add to the current selection a start comment mark at the beginning
3730   and a stop comment mark at the end.
3731 */
addStartStopCommentToSelection(KTextEditor::ViewPrivate * view,int attrib)3732 void KTextEditor::DocumentPrivate::addStartStopCommentToSelection(KTextEditor::ViewPrivate *view, int attrib)
3733 {
3734     const QString startComment = highlight()->getCommentStart(attrib);
3735     const QString endComment = highlight()->getCommentEnd(attrib);
3736 
3737     KTextEditor::Range range = view->selectionRange();
3738 
3739     if ((range.end().column() == 0) && (range.end().line() > 0)) {
3740         range.setEnd(KTextEditor::Cursor(range.end().line() - 1, lineLength(range.end().line() - 1)));
3741     }
3742 
3743     editStart();
3744 
3745     if (!view->blockSelection()) {
3746         insertText(range.end(), endComment);
3747         insertText(range.start(), startComment);
3748     } else {
3749         for (int line = range.start().line(); line <= range.end().line(); line++) {
3750             KTextEditor::Range subRange = rangeOnLine(range, line);
3751             insertText(subRange.end(), endComment);
3752             insertText(subRange.start(), startComment);
3753         }
3754     }
3755 
3756     editEnd();
3757     // selection automatically updated (MovingRange)
3758 }
3759 
3760 /*
3761   Add to the current selection a comment line mark at the beginning of each line.
3762 */
addStartLineCommentToSelection(KTextEditor::ViewPrivate * view,int attrib)3763 void KTextEditor::DocumentPrivate::addStartLineCommentToSelection(KTextEditor::ViewPrivate *view, int attrib)
3764 {
3765     // const QString commentLineMark = highlight()->getCommentSingleLineStart(attrib) + QLatin1Char(' ');
3766 
3767     int sl = view->selectionRange().start().line();
3768     int el = view->selectionRange().end().line();
3769 
3770     // if end of selection is in column 0 in last line, omit the last line
3771     if ((view->selectionRange().end().column() == 0) && (el > 0)) {
3772         el--;
3773     }
3774 
3775     editStart();
3776 
3777     // For each line of the selection
3778     for (int z = el; z >= sl; z--) {
3779         // insertText (z, 0, commentLineMark);
3780         addStartLineCommentToSingleLine(z, attrib);
3781     }
3782 
3783     editEnd();
3784     // selection automatically updated (MovingRange)
3785 }
3786 
nextNonSpaceCharPos(int & line,int & col)3787 bool KTextEditor::DocumentPrivate::nextNonSpaceCharPos(int &line, int &col)
3788 {
3789     for (; line < (int)m_buffer->count(); line++) {
3790         Kate::TextLine textLine = m_buffer->plainLine(line);
3791 
3792         if (!textLine) {
3793             break;
3794         }
3795 
3796         col = textLine->nextNonSpaceChar(col);
3797         if (col != -1) {
3798             return true; // Next non-space char found
3799         }
3800         col = 0;
3801     }
3802     // No non-space char found
3803     line = -1;
3804     col = -1;
3805     return false;
3806 }
3807 
previousNonSpaceCharPos(int & line,int & col)3808 bool KTextEditor::DocumentPrivate::previousNonSpaceCharPos(int &line, int &col)
3809 {
3810     while (true) {
3811         Kate::TextLine textLine = m_buffer->plainLine(line);
3812 
3813         if (!textLine) {
3814             break;
3815         }
3816 
3817         col = textLine->previousNonSpaceChar(col);
3818         if (col != -1) {
3819             return true;
3820         }
3821         if (line == 0) {
3822             return false;
3823         }
3824         --line;
3825         col = textLine->length();
3826     }
3827     // No non-space char found
3828     line = -1;
3829     col = -1;
3830     return false;
3831 }
3832 
3833 /*
3834   Remove from the selection a start comment mark at
3835   the beginning and a stop comment mark at the end.
3836 */
removeStartStopCommentFromSelection(KTextEditor::ViewPrivate * view,int attrib)3837 bool KTextEditor::DocumentPrivate::removeStartStopCommentFromSelection(KTextEditor::ViewPrivate *view, int attrib)
3838 {
3839     const QString startComment = highlight()->getCommentStart(attrib);
3840     const QString endComment = highlight()->getCommentEnd(attrib);
3841 
3842     int sl = qMax<int>(0, view->selectionRange().start().line());
3843     int el = qMin<int>(view->selectionRange().end().line(), lastLine());
3844     int sc = view->selectionRange().start().column();
3845     int ec = view->selectionRange().end().column();
3846 
3847     // The selection ends on the char before selectEnd
3848     if (ec != 0) {
3849         --ec;
3850     } else if (el > 0) {
3851         --el;
3852         ec = m_buffer->plainLine(el)->length() - 1;
3853     }
3854 
3855     const int startCommentLen = startComment.length();
3856     const int endCommentLen = endComment.length();
3857 
3858     // had this been perl or sed: s/^\s*$startComment(.+?)$endComment\s*/$2/
3859 
3860     bool remove = nextNonSpaceCharPos(sl, sc) && m_buffer->plainLine(sl)->matchesAt(sc, startComment) && previousNonSpaceCharPos(el, ec)
3861         && ((ec - endCommentLen + 1) >= 0) && m_buffer->plainLine(el)->matchesAt(ec - endCommentLen + 1, endComment);
3862 
3863     if (remove) {
3864         editStart();
3865 
3866         removeText(KTextEditor::Range(el, ec - endCommentLen + 1, el, ec + 1));
3867         removeText(KTextEditor::Range(sl, sc, sl, sc + startCommentLen));
3868 
3869         editEnd();
3870         // selection automatically updated (MovingRange)
3871     }
3872 
3873     return remove;
3874 }
3875 
removeStartStopCommentFromRegion(const KTextEditor::Cursor start,const KTextEditor::Cursor end,int attrib)3876 bool KTextEditor::DocumentPrivate::removeStartStopCommentFromRegion(const KTextEditor::Cursor start, const KTextEditor::Cursor end, int attrib)
3877 {
3878     const QString startComment = highlight()->getCommentStart(attrib);
3879     const QString endComment = highlight()->getCommentEnd(attrib);
3880     const int startCommentLen = startComment.length();
3881     const int endCommentLen = endComment.length();
3882 
3883     const bool remove = m_buffer->plainLine(start.line())->matchesAt(start.column(), startComment)
3884         && m_buffer->plainLine(end.line())->matchesAt(end.column() - endCommentLen, endComment);
3885     if (remove) {
3886         editStart();
3887         removeText(KTextEditor::Range(end.line(), end.column() - endCommentLen, end.line(), end.column()));
3888         removeText(KTextEditor::Range(start, startCommentLen));
3889         editEnd();
3890     }
3891     return remove;
3892 }
3893 
3894 /*
3895   Remove from the beginning of each line of the
3896   selection a start comment line mark.
3897 */
removeStartLineCommentFromSelection(KTextEditor::ViewPrivate * view,int attrib)3898 bool KTextEditor::DocumentPrivate::removeStartLineCommentFromSelection(KTextEditor::ViewPrivate *view, int attrib)
3899 {
3900     const QString shortCommentMark = highlight()->getCommentSingleLineStart(attrib);
3901     const QString longCommentMark = shortCommentMark + QLatin1Char(' ');
3902 
3903     int sl = view->selectionRange().start().line();
3904     int el = view->selectionRange().end().line();
3905 
3906     if ((view->selectionRange().end().column() == 0) && (el > 0)) {
3907         el--;
3908     }
3909 
3910     bool removed = false;
3911 
3912     editStart();
3913 
3914     // For each line of the selection
3915     for (int z = el; z >= sl; z--) {
3916         // Try to remove the long comment mark first
3917         removed = (removeStringFromBeginning(z, longCommentMark) || removeStringFromBeginning(z, shortCommentMark) || removed);
3918     }
3919 
3920     editEnd();
3921     // selection automatically updated (MovingRange)
3922 
3923     return removed;
3924 }
3925 
3926 /*
3927   Comment or uncomment the selection or the current
3928   line if there is no selection.
3929 */
comment(KTextEditor::ViewPrivate * v,uint line,uint column,int change)3930 void KTextEditor::DocumentPrivate::comment(KTextEditor::ViewPrivate *v, uint line, uint column, int change)
3931 {
3932     // skip word wrap bug #105373
3933     const bool skipWordWrap = wordWrap();
3934     if (skipWordWrap) {
3935         setWordWrap(false);
3936     }
3937 
3938     bool hassel = v->selection();
3939     int c = 0;
3940 
3941     if (hassel) {
3942         c = v->selectionRange().start().column();
3943     }
3944 
3945     int startAttrib = 0;
3946     Kate::TextLine ln = kateTextLine(line);
3947 
3948     if (c < ln->length()) {
3949         startAttrib = ln->attribute(c);
3950     } else if (!ln->attributesList().isEmpty()) {
3951         startAttrib = ln->attributesList().back().attributeValue;
3952     }
3953 
3954     bool hasStartLineCommentMark = !(highlight()->getCommentSingleLineStart(startAttrib).isEmpty());
3955     bool hasStartStopCommentMark = (!(highlight()->getCommentStart(startAttrib).isEmpty()) && !(highlight()->getCommentEnd(startAttrib).isEmpty()));
3956 
3957     if (change > 0) { // comment
3958         if (!hassel) {
3959             if (hasStartLineCommentMark) {
3960                 addStartLineCommentToSingleLine(line, startAttrib);
3961             } else if (hasStartStopCommentMark) {
3962                 addStartStopCommentToSingleLine(line, startAttrib);
3963             }
3964         } else {
3965             // anders: prefer single line comment to avoid nesting probs
3966             // If the selection starts after first char in the first line
3967             // or ends before the last char of the last line, we may use
3968             // multiline comment markers.
3969             // TODO We should try to detect nesting.
3970             //    - if selection ends at col 0, most likely she wanted that
3971             // line ignored
3972             const KTextEditor::Range sel = v->selectionRange();
3973             if (hasStartStopCommentMark
3974                 && (!hasStartLineCommentMark
3975                     || ((sel.start().column() > m_buffer->plainLine(sel.start().line())->firstChar())
3976                         || (sel.end().column() > 0 && sel.end().column() < (m_buffer->plainLine(sel.end().line())->length()))))) {
3977                 addStartStopCommentToSelection(v, startAttrib);
3978             } else if (hasStartLineCommentMark) {
3979                 addStartLineCommentToSelection(v, startAttrib);
3980             }
3981         }
3982     } else { // uncomment
3983         bool removed = false;
3984         if (!hassel) {
3985             removed = (hasStartLineCommentMark && removeStartLineCommentFromSingleLine(line, startAttrib))
3986                 || (hasStartStopCommentMark && removeStartStopCommentFromSingleLine(line, startAttrib));
3987         } else {
3988             // anders: this seems like it will work with above changes :)
3989             removed = (hasStartStopCommentMark && removeStartStopCommentFromSelection(v, startAttrib))
3990                 || (hasStartLineCommentMark && removeStartLineCommentFromSelection(v, startAttrib));
3991         }
3992 
3993         // recursive call for toggle comment
3994         if (!removed && change == 0) {
3995             comment(v, line, column, 1);
3996         }
3997     }
3998 
3999     if (skipWordWrap) {
4000         setWordWrap(true); // see begin of function ::comment (bug #105373)
4001     }
4002 }
4003 
transform(KTextEditor::ViewPrivate * v,const KTextEditor::Cursor c,KTextEditor::DocumentPrivate::TextTransform t)4004 void KTextEditor::DocumentPrivate::transform(KTextEditor::ViewPrivate *v, const KTextEditor::Cursor c, KTextEditor::DocumentPrivate::TextTransform t)
4005 {
4006     if (v->selection()) {
4007         editStart();
4008 
4009         // cache the selection and cursor, so we can be sure to restore.
4010         KTextEditor::Range selection = v->selectionRange();
4011 
4012         KTextEditor::Range range(selection.start(), 0);
4013         while (range.start().line() <= selection.end().line()) {
4014             int start = 0;
4015             int end = lineLength(range.start().line());
4016 
4017             if (range.start().line() == selection.start().line() || v->blockSelection()) {
4018                 start = selection.start().column();
4019             }
4020 
4021             if (range.start().line() == selection.end().line() || v->blockSelection()) {
4022                 end = selection.end().column();
4023             }
4024 
4025             if (start > end) {
4026                 int swapCol = start;
4027                 start = end;
4028                 end = swapCol;
4029             }
4030             range.setStart(KTextEditor::Cursor(range.start().line(), start));
4031             range.setEnd(KTextEditor::Cursor(range.end().line(), end));
4032 
4033             QString s = text(range);
4034             QString old = s;
4035 
4036             if (t == Uppercase) {
4037                 s = s.toUpper();
4038             } else if (t == Lowercase) {
4039                 s = s.toLower();
4040             } else { // Capitalize
4041                 Kate::TextLine l = m_buffer->plainLine(range.start().line());
4042                 int p(0);
4043                 while (p < s.length()) {
4044                     // If bol or the character before is not in a word, up this one:
4045                     // 1. if both start and p is 0, upper char.
4046                     // 2. if blockselect or first line, and p == 0 and start-1 is not in a word, upper
4047                     // 3. if p-1 is not in a word, upper.
4048                     if ((!range.start().column() && !p)
4049                         || ((range.start().line() == selection.start().line() || v->blockSelection()) && !p
4050                             && !highlight()->isInWord(l->at(range.start().column() - 1)))
4051                         || (p && !highlight()->isInWord(s.at(p - 1)))) {
4052                         s[p] = s.at(p).toUpper();
4053                     }
4054                     p++;
4055                 }
4056             }
4057 
4058             if (s != old) {
4059                 removeText(range);
4060                 insertText(range.start(), s);
4061             }
4062 
4063             range.setBothLines(range.start().line() + 1);
4064         }
4065 
4066         editEnd();
4067 
4068         // restore selection & cursor
4069         v->setSelection(selection);
4070         v->setCursorPosition(c);
4071 
4072     } else { // no selection
4073         editStart();
4074 
4075         // get cursor
4076         KTextEditor::Cursor cursor = c;
4077 
4078         QString old = text(KTextEditor::Range(cursor, 1));
4079         QString s;
4080         switch (t) {
4081         case Uppercase:
4082             s = old.toUpper();
4083             break;
4084         case Lowercase:
4085             s = old.toLower();
4086             break;
4087         case Capitalize: {
4088             Kate::TextLine l = m_buffer->plainLine(cursor.line());
4089             while (cursor.column() > 0 && highlight()->isInWord(l->at(cursor.column() - 1), l->attribute(cursor.column() - 1))) {
4090                 cursor.setColumn(cursor.column() - 1);
4091             }
4092             old = text(KTextEditor::Range(cursor, 1));
4093             s = old.toUpper();
4094         } break;
4095         default:
4096             break;
4097         }
4098 
4099         removeText(KTextEditor::Range(cursor, 1));
4100         insertText(cursor, s);
4101 
4102         editEnd();
4103     }
4104 }
4105 
joinLines(uint first,uint last)4106 void KTextEditor::DocumentPrivate::joinLines(uint first, uint last)
4107 {
4108     //   if ( first == last ) last += 1;
4109     editStart();
4110     int line(first);
4111     while (first < last) {
4112         // Normalize the whitespace in the joined lines by making sure there's
4113         // always exactly one space between the joined lines
4114         // This cannot be done in editUnwrapLine, because we do NOT want this
4115         // behavior when deleting from the start of a line, just when explicitly
4116         // calling the join command
4117         Kate::TextLine l = kateTextLine(line);
4118         Kate::TextLine tl = kateTextLine(line + 1);
4119 
4120         if (!l || !tl) {
4121             editEnd();
4122             return;
4123         }
4124 
4125         int pos = tl->firstChar();
4126         if (pos >= 0) {
4127             if (pos != 0) {
4128                 editRemoveText(line + 1, 0, pos);
4129             }
4130             if (!(l->length() == 0 || l->at(l->length() - 1).isSpace())) {
4131                 editInsertText(line + 1, 0, QStringLiteral(" "));
4132             }
4133         } else {
4134             // Just remove the whitespace and let Kate handle the rest
4135             editRemoveText(line + 1, 0, tl->length());
4136         }
4137 
4138         editUnWrapLine(line);
4139         first++;
4140     }
4141     editEnd();
4142 }
4143 
tagLines(KTextEditor::LineRange lineRange)4144 void KTextEditor::DocumentPrivate::tagLines(KTextEditor::LineRange lineRange)
4145 {
4146     for (auto view : std::as_const(m_views)) {
4147         view->tagLines(lineRange, true);
4148     }
4149 }
4150 
tagLine(int line)4151 void KTextEditor::DocumentPrivate::tagLine(int line)
4152 {
4153     tagLines({line, line});
4154 }
4155 
repaintViews(bool paintOnlyDirty)4156 void KTextEditor::DocumentPrivate::repaintViews(bool paintOnlyDirty)
4157 {
4158     for (auto view : std::as_const(m_views)) {
4159         view->repaintText(paintOnlyDirty);
4160     }
4161 }
4162 
4163 /*
4164    Bracket matching uses the following algorithm:
4165    If in overwrite mode, match the bracket currently underneath the cursor.
4166    Otherwise, if the character to the left is a bracket,
4167    match it. Otherwise if the character to the right of the cursor is a
4168    bracket, match it. Otherwise, don't match anything.
4169 */
findMatchingBracket(const KTextEditor::Cursor start,int maxLines)4170 KTextEditor::Range KTextEditor::DocumentPrivate::findMatchingBracket(const KTextEditor::Cursor start, int maxLines)
4171 {
4172     if (maxLines < 0) {
4173         return KTextEditor::Range::invalid();
4174     }
4175 
4176     Kate::TextLine textLine = m_buffer->plainLine(start.line());
4177     if (!textLine) {
4178         return KTextEditor::Range::invalid();
4179     }
4180 
4181     KTextEditor::Range range(start, start);
4182     const QChar right = textLine->at(range.start().column());
4183     const QChar left = textLine->at(range.start().column() - 1);
4184     QChar bracket;
4185 
4186     if (config()->ovr()) {
4187         if (isBracket(right)) {
4188             bracket = right;
4189         } else {
4190             return KTextEditor::Range::invalid();
4191         }
4192     } else if (isBracket(right)) {
4193         bracket = right;
4194     } else if (isBracket(left)) {
4195         range.setStart(KTextEditor::Cursor(range.start().line(), range.start().column() - 1));
4196         bracket = left;
4197     } else {
4198         return KTextEditor::Range::invalid();
4199     }
4200 
4201     const QChar opposite = matchingBracket(bracket);
4202     if (opposite.isNull()) {
4203         return KTextEditor::Range::invalid();
4204     }
4205 
4206     const int searchDir = isStartBracket(bracket) ? 1 : -1;
4207     uint nesting = 0;
4208 
4209     const int minLine = qMax(range.start().line() - maxLines, 0);
4210     const int maxLine = qMin(range.start().line() + maxLines, documentEnd().line());
4211 
4212     range.setEnd(range.start());
4213     KTextEditor::DocumentCursor cursor(this);
4214     cursor.setPosition(range.start());
4215     int validAttr = kateTextLine(cursor.line())->attribute(cursor.column());
4216 
4217     while (cursor.line() >= minLine && cursor.line() <= maxLine) {
4218         if (!cursor.move(searchDir)) {
4219             return KTextEditor::Range::invalid();
4220         }
4221 
4222         Kate::TextLine textLine = kateTextLine(cursor.line());
4223         if (textLine->attribute(cursor.column()) == validAttr) {
4224             // Check for match
4225             QChar c = textLine->at(cursor.column());
4226             if (c == opposite) {
4227                 if (nesting == 0) {
4228                     if (searchDir > 0) { // forward
4229                         range.setEnd(cursor.toCursor());
4230                     } else {
4231                         range.setStart(cursor.toCursor());
4232                     }
4233                     return range;
4234                 }
4235                 nesting--;
4236             } else if (c == bracket) {
4237                 nesting++;
4238             }
4239         }
4240     }
4241 
4242     return KTextEditor::Range::invalid();
4243 }
4244 
4245 // helper: remove \r and \n from visible document name (bug #170876)
removeNewLines(const QString & str)4246 inline static QString removeNewLines(const QString &str)
4247 {
4248     QString tmp(str);
4249     return tmp.replace(QLatin1String("\r\n"), QLatin1String(" ")).replace(QLatin1Char('\r'), QLatin1Char(' ')).replace(QLatin1Char('\n'), QLatin1Char(' '));
4250 }
4251 
updateDocName()4252 void KTextEditor::DocumentPrivate::updateDocName()
4253 {
4254     // if the name is set, and starts with FILENAME, it should not be changed!
4255     if (!url().isEmpty() && (m_docName == removeNewLines(url().fileName()) || m_docName.startsWith(removeNewLines(url().fileName()) + QLatin1String(" (")))) {
4256         return;
4257     }
4258 
4259     int count = -1;
4260 
4261     const auto docs = KTextEditor::EditorPrivate::self()->kateDocuments();
4262     for (KTextEditor::DocumentPrivate *doc : docs) {
4263         if ((doc != this) && (doc->url().fileName() == url().fileName())) {
4264             if (doc->m_docNameNumber > count) {
4265                 count = doc->m_docNameNumber;
4266             }
4267         }
4268     }
4269 
4270     m_docNameNumber = count + 1;
4271 
4272     QString oldName = m_docName;
4273     m_docName = removeNewLines(url().fileName());
4274 
4275     m_isUntitled = m_docName.isEmpty();
4276     if (m_isUntitled) {
4277         m_docName = i18n("Untitled");
4278     }
4279 
4280     if (m_docNameNumber > 0) {
4281         m_docName = QString(m_docName + QLatin1String(" (%1)")).arg(m_docNameNumber + 1);
4282     }
4283 
4284     // avoid to emit this, if name doesn't change!
4285     if (oldName != m_docName) {
4286         Q_EMIT documentNameChanged(this);
4287     }
4288 }
4289 
slotModifiedOnDisk(KTextEditor::View *)4290 void KTextEditor::DocumentPrivate::slotModifiedOnDisk(KTextEditor::View * /*v*/)
4291 {
4292     if (url().isEmpty() || !m_modOnHd) {
4293         return;
4294     }
4295 
4296     if (!isModified() && isAutoReload()) {
4297         onModOnHdAutoReload();
4298         return;
4299     }
4300 
4301     if (!m_fileChangedDialogsActivated || m_modOnHdHandler) {
4302         return;
4303     }
4304 
4305     // don't ask the user again and again the same thing
4306     if (m_modOnHdReason == m_prevModOnHdReason) {
4307         return;
4308     }
4309     m_prevModOnHdReason = m_modOnHdReason;
4310 
4311     m_modOnHdHandler = new KateModOnHdPrompt(this, m_modOnHdReason, reasonedMOHString());
4312     connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::saveAsTriggered, this, &DocumentPrivate::onModOnHdSaveAs);
4313     connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::closeTriggered, this, &DocumentPrivate::onModOnHdClose);
4314     connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::reloadTriggered, this, &DocumentPrivate::onModOnHdReload);
4315     connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::autoReloadTriggered, this, &DocumentPrivate::onModOnHdAutoReload);
4316     connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::ignoreTriggered, this, &DocumentPrivate::onModOnHdIgnore);
4317 }
4318 
onModOnHdSaveAs()4319 void KTextEditor::DocumentPrivate::onModOnHdSaveAs()
4320 {
4321     m_modOnHd = false;
4322     QWidget *parentWidget(dialogParent());
4323     const QUrl res = QFileDialog::getSaveFileUrl(parentWidget, i18n("Save File"), url());
4324     if (!res.isEmpty()) {
4325         if (!saveAs(res)) {
4326             KMessageBox::error(parentWidget, i18n("Save failed"));
4327             m_modOnHd = true;
4328         } else {
4329             delete m_modOnHdHandler;
4330             m_prevModOnHdReason = OnDiskUnmodified;
4331             Q_EMIT modifiedOnDisk(this, false, OnDiskUnmodified);
4332         }
4333     } else { // the save as dialog was canceled, we are still modified on disk
4334         m_modOnHd = true;
4335     }
4336 }
4337 
onModOnHdClose()4338 void KTextEditor::DocumentPrivate::onModOnHdClose()
4339 {
4340     // avoid prompt in closeUrl()
4341     m_fileChangedDialogsActivated = false;
4342 
4343     // close the file without prompt confirmation
4344     closeUrl();
4345 
4346     // Useful for applications that provide the necessary interface
4347     // delay this, otherwise we delete ourself during e.g. event handling + deleting this is undefined!
4348     // see e.g. bug 433180
4349     QTimer::singleShot(0, this, [this]() {
4350         KTextEditor::EditorPrivate::self()->application()->closeDocument(this);
4351     });
4352 }
4353 
onModOnHdReload()4354 void KTextEditor::DocumentPrivate::onModOnHdReload()
4355 {
4356     m_modOnHd = false;
4357     m_prevModOnHdReason = OnDiskUnmodified;
4358     Q_EMIT modifiedOnDisk(this, false, OnDiskUnmodified);
4359     documentReload();
4360     delete m_modOnHdHandler;
4361 }
4362 
autoReloadToggled(bool b)4363 void KTextEditor::DocumentPrivate::autoReloadToggled(bool b)
4364 {
4365     m_autoReloadMode->setChecked(b);
4366     if (b) {
4367         connect(&m_modOnHdTimer, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload);
4368     } else {
4369         disconnect(&m_modOnHdTimer, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload);
4370     }
4371 }
4372 
isAutoReload()4373 bool KTextEditor::DocumentPrivate::isAutoReload()
4374 {
4375     return m_autoReloadMode->isChecked();
4376 }
4377 
delayAutoReload()4378 void KTextEditor::DocumentPrivate::delayAutoReload()
4379 {
4380     if (isAutoReload()) {
4381         m_autoReloadThrottle.start();
4382     }
4383 }
4384 
onModOnHdAutoReload()4385 void KTextEditor::DocumentPrivate::onModOnHdAutoReload()
4386 {
4387     if (m_modOnHdHandler) {
4388         delete m_modOnHdHandler;
4389         autoReloadToggled(true);
4390     }
4391 
4392     if (!isAutoReload()) {
4393         return;
4394     }
4395 
4396     if (m_modOnHd && !m_reloading && !m_autoReloadThrottle.isActive()) {
4397         m_modOnHd = false;
4398         m_prevModOnHdReason = OnDiskUnmodified;
4399         Q_EMIT modifiedOnDisk(this, false, OnDiskUnmodified);
4400         documentReload();
4401         m_autoReloadThrottle.start();
4402     }
4403 }
4404 
onModOnHdIgnore()4405 void KTextEditor::DocumentPrivate::onModOnHdIgnore()
4406 {
4407     // ignore as long as m_prevModOnHdReason == m_modOnHdReason
4408     delete m_modOnHdHandler;
4409 }
4410 
setModifiedOnDisk(ModifiedOnDiskReason reason)4411 void KTextEditor::DocumentPrivate::setModifiedOnDisk(ModifiedOnDiskReason reason)
4412 {
4413     m_modOnHdReason = reason;
4414     m_modOnHd = (reason != OnDiskUnmodified);
4415     Q_EMIT modifiedOnDisk(this, (reason != OnDiskUnmodified), reason);
4416 }
4417 
4418 class KateDocumentTmpMark
4419 {
4420 public:
4421     QString line;
4422     KTextEditor::Mark mark;
4423 };
4424 
setModifiedOnDiskWarning(bool on)4425 void KTextEditor::DocumentPrivate::setModifiedOnDiskWarning(bool on)
4426 {
4427     m_fileChangedDialogsActivated = on;
4428 }
4429 
documentReload()4430 bool KTextEditor::DocumentPrivate::documentReload()
4431 {
4432     if (url().isEmpty()) {
4433         return false;
4434     }
4435 
4436     // typically, the message for externally modified files is visible. Since it
4437     // does not make sense showing an additional dialog, just hide the message.
4438     delete m_modOnHdHandler;
4439 
4440     Q_EMIT aboutToReload(this);
4441 
4442     QVarLengthArray<KateDocumentTmpMark> tmp;
4443     tmp.reserve(m_marks.size());
4444     std::transform(m_marks.cbegin(), m_marks.cend(), std::back_inserter(tmp), [this](KTextEditor::Mark *mark) {
4445         return KateDocumentTmpMark{line(mark->line), *mark};
4446     });
4447 
4448     const QString oldMode = mode();
4449     const bool modeByUser = m_fileTypeSetByUser;
4450     const QString oldHlMode = highlightingMode();
4451     const bool hlByUser = m_hlSetByUser;
4452 
4453     m_storedVariables.clear();
4454 
4455     // save cursor positions for all views
4456     QVarLengthArray<std::pair<KTextEditor::ViewPrivate *, KTextEditor::Cursor>, 4> cursorPositions;
4457     std::transform(m_views.cbegin(), m_views.cend(), std::back_inserter(cursorPositions), [](KTextEditor::ViewPrivate *v) {
4458         return std::pair<KTextEditor::ViewPrivate *, KTextEditor::Cursor>(v, v->cursorPosition());
4459     });
4460 
4461     m_reloading = true;
4462     KTextEditor::DocumentPrivate::openUrl(url());
4463 
4464     // reset some flags only valid for one reload!
4465     m_userSetEncodingForNextReload = false;
4466 
4467     // restore cursor positions for all views
4468     for (auto v : std::as_const(m_views)) {
4469         setActiveView(v);
4470         auto it = std::find_if(cursorPositions.cbegin(), cursorPositions.cend(), [v](const std::pair<KTextEditor::ViewPrivate *, KTextEditor::Cursor> &p) {
4471             return p.first == v;
4472         });
4473         v->setCursorPosition(it->second);
4474         if (v->isVisible()) {
4475             v->repaintText(false);
4476         }
4477     }
4478 
4479     int z = 0;
4480     const int lines = this->lines();
4481     for (const auto &tmpMark : tmp) {
4482         if (z < lines) {
4483             if (tmpMark.line == line(tmpMark.mark.line)) {
4484                 setMark(tmpMark.mark.line, tmpMark.mark.type);
4485             }
4486         }
4487         ++z;
4488     }
4489 
4490     if (modeByUser) {
4491         setMode(oldMode);
4492     }
4493     if (hlByUser) {
4494         setHighlightingMode(oldHlMode);
4495     }
4496 
4497     Q_EMIT reloaded(this);
4498 
4499     return true;
4500 }
4501 
documentSave()4502 bool KTextEditor::DocumentPrivate::documentSave()
4503 {
4504     if (!url().isValid() || !isReadWrite()) {
4505         return documentSaveAs();
4506     }
4507 
4508     return save();
4509 }
4510 
documentSaveAs()4511 bool KTextEditor::DocumentPrivate::documentSaveAs()
4512 {
4513     const QUrl saveUrl = QFileDialog::getSaveFileUrl(dialogParent(), i18n("Save File"), url());
4514     if (saveUrl.isEmpty()) {
4515         return false;
4516     }
4517 
4518     return saveAs(saveUrl);
4519 }
4520 
documentSaveAsWithEncoding(const QString & encoding)4521 bool KTextEditor::DocumentPrivate::documentSaveAsWithEncoding(const QString &encoding)
4522 {
4523     const QUrl saveUrl = QFileDialog::getSaveFileUrl(dialogParent(), i18n("Save File"), url());
4524     if (saveUrl.isEmpty()) {
4525         return false;
4526     }
4527 
4528     setEncoding(encoding);
4529     return saveAs(saveUrl);
4530 }
4531 
documentSaveCopyAs()4532 bool KTextEditor::DocumentPrivate::documentSaveCopyAs()
4533 {
4534     const QUrl saveUrl = QFileDialog::getSaveFileUrl(dialogParent(), i18n("Save Copy of File"), url());
4535     if (saveUrl.isEmpty()) {
4536         return false;
4537     }
4538 
4539     QTemporaryFile file;
4540     if (!file.open()) {
4541         return false;
4542     }
4543 
4544     if (!m_buffer->saveFile(file.fileName())) {
4545         KMessageBox::error(dialogParent(),
4546                            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 "
4547                                 "that enough disk space is available.",
4548                                 this->url().toDisplayString(QUrl::PreferLocalFile)));
4549         return false;
4550     }
4551 
4552     // get the right permissions, start with safe default
4553     KIO::StatJob *statJob = KIO::statDetails(url(), KIO::StatJob::SourceSide, KIO::StatBasic);
4554     KJobWidgets::setWindow(statJob, QApplication::activeWindow());
4555     int permissions = -1;
4556     if (statJob->exec()) {
4557         permissions = KFileItem(statJob->statResult(), url()).permissions();
4558     }
4559 
4560     // KIO move, important: allow overwrite, we checked above!
4561     KIO::FileCopyJob *job = KIO::file_copy(QUrl::fromLocalFile(file.fileName()), saveUrl, permissions, KIO::Overwrite);
4562     KJobWidgets::setWindow(job, QApplication::activeWindow());
4563     return job->exec();
4564 }
4565 
setWordWrap(bool on)4566 void KTextEditor::DocumentPrivate::setWordWrap(bool on)
4567 {
4568     config()->setWordWrap(on);
4569 }
4570 
wordWrap() const4571 bool KTextEditor::DocumentPrivate::wordWrap() const
4572 {
4573     return config()->wordWrap();
4574 }
4575 
setWordWrapAt(uint col)4576 void KTextEditor::DocumentPrivate::setWordWrapAt(uint col)
4577 {
4578     config()->setWordWrapAt(col);
4579 }
4580 
wordWrapAt() const4581 unsigned int KTextEditor::DocumentPrivate::wordWrapAt() const
4582 {
4583     return config()->wordWrapAt();
4584 }
4585 
setPageUpDownMovesCursor(bool on)4586 void KTextEditor::DocumentPrivate::setPageUpDownMovesCursor(bool on)
4587 {
4588     config()->setPageUpDownMovesCursor(on);
4589 }
4590 
pageUpDownMovesCursor() const4591 bool KTextEditor::DocumentPrivate::pageUpDownMovesCursor() const
4592 {
4593     return config()->pageUpDownMovesCursor();
4594 }
4595 // END
4596 
setEncoding(const QString & e)4597 bool KTextEditor::DocumentPrivate::setEncoding(const QString &e)
4598 {
4599     return m_config->setEncoding(e);
4600 }
4601 
encoding() const4602 QString KTextEditor::DocumentPrivate::encoding() const
4603 {
4604     return m_config->encoding();
4605 }
4606 
updateConfig()4607 void KTextEditor::DocumentPrivate::updateConfig()
4608 {
4609     m_undoManager->updateConfig();
4610 
4611     // switch indenter if needed and update config....
4612     m_indenter->setMode(m_config->indentationMode());
4613     m_indenter->updateConfig();
4614 
4615     // set tab width there, too
4616     m_buffer->setTabWidth(config()->tabWidth());
4617 
4618     // update all views, does tagAll and updateView...
4619     for (auto view : std::as_const(m_views)) {
4620         view->updateDocumentConfig();
4621     }
4622 
4623     // update on-the-fly spell checking as spell checking defaults might have changes
4624     if (m_onTheFlyChecker) {
4625         m_onTheFlyChecker->updateConfig();
4626     }
4627 
4628     Q_EMIT configChanged(this);
4629 }
4630 
4631 // BEGIN Variable reader
4632 // "local variable" feature by anders, 2003
4633 /* TODO
4634       add config options (how many lines to read, on/off)
4635       add interface for plugins/apps to set/get variables
4636       add view stuff
4637 */
readVariables(bool onlyViewAndRenderer)4638 void KTextEditor::DocumentPrivate::readVariables(bool onlyViewAndRenderer)
4639 {
4640     if (!onlyViewAndRenderer) {
4641         m_config->configStart();
4642     }
4643 
4644     // views!
4645     for (auto v : std::as_const(m_views)) {
4646         v->config()->configStart();
4647         v->renderer()->config()->configStart();
4648     }
4649     // read a number of lines in the top/bottom of the document
4650     for (int i = 0; i < qMin(9, lines()); ++i) {
4651         readVariableLine(line(i), onlyViewAndRenderer);
4652     }
4653     if (lines() > 10) {
4654         for (int i = qMax(10, lines() - 10); i < lines(); i++) {
4655             readVariableLine(line(i), onlyViewAndRenderer);
4656         }
4657     }
4658 
4659     if (!onlyViewAndRenderer) {
4660         m_config->configEnd();
4661     }
4662 
4663     for (auto v : std::as_const(m_views)) {
4664         v->config()->configEnd();
4665         v->renderer()->config()->configEnd();
4666     }
4667 }
4668 
readVariableLine(const QString & t,bool onlyViewAndRenderer)4669 void KTextEditor::DocumentPrivate::readVariableLine(const QString &t, bool onlyViewAndRenderer)
4670 {
4671     static const QRegularExpression kvLine(QStringLiteral("kate:(.*)"));
4672     static const QRegularExpression kvLineWildcard(QStringLiteral("kate-wildcard\\((.*)\\):(.*)"));
4673     static const QRegularExpression kvLineMime(QStringLiteral("kate-mimetype\\((.*)\\):(.*)"));
4674     static const QRegularExpression kvVar(QStringLiteral("([\\w\\-]+)\\s+([^;]+)"));
4675 
4676     // simple check first, no regex
4677     // no kate inside, no vars, simple...
4678     if (!t.contains(QLatin1String("kate"))) {
4679         return;
4680     }
4681 
4682     // found vars, if any
4683     QString s;
4684 
4685     // now, try first the normal ones
4686     auto match = kvLine.match(t);
4687     if (match.hasMatch()) {
4688         s = match.captured(1);
4689 
4690         // qCDebug(LOG_KTE) << "normal variable line kate: matched: " << s;
4691     } else if ((match = kvLineWildcard.match(t)).hasMatch()) { // regex given
4692         const QStringList wildcards(match.captured(1).split(QLatin1Char(';'), Qt::SkipEmptyParts));
4693         const QString nameOfFile = url().fileName();
4694 
4695         bool found = false;
4696         for (const QString &pattern : wildcards) {
4697             QRegularExpression wildcard(QRegularExpression::wildcardToRegularExpression(pattern));
4698             found = wildcard.match(nameOfFile).hasMatch();
4699 
4700             if (found) {
4701                 break;
4702             }
4703         }
4704 
4705         // nothing usable found...
4706         if (!found) {
4707             return;
4708         }
4709 
4710         s = match.captured(2);
4711 
4712         // qCDebug(LOG_KTE) << "guarded variable line kate-wildcard: matched: " << s;
4713     } else if ((match = kvLineMime.match(t)).hasMatch()) { // mime-type given
4714         const QStringList types(match.captured(1).split(QLatin1Char(';'), Qt::SkipEmptyParts));
4715 
4716         // no matching type found
4717         if (!types.contains(mimeType())) {
4718             return;
4719         }
4720 
4721         s = match.captured(2);
4722 
4723         // qCDebug(LOG_KTE) << "guarded variable line kate-mimetype: matched: " << s;
4724     } else { // nothing found
4725         return;
4726     }
4727 
4728     // view variable names
4729     static const auto vvl = {QLatin1String("dynamic-word-wrap"),
4730                              QLatin1String("dynamic-word-wrap-indicators"),
4731                              QLatin1String("line-numbers"),
4732                              QLatin1String("icon-border"),
4733                              QLatin1String("folding-markers"),
4734                              QLatin1String("folding-preview"),
4735                              QLatin1String("bookmark-sorting"),
4736                              QLatin1String("auto-center-lines"),
4737                              QLatin1String("icon-bar-color"),
4738                              QLatin1String("scrollbar-minimap"),
4739                              QLatin1String("scrollbar-preview")
4740                              // renderer
4741                              ,
4742                              QLatin1String("background-color"),
4743                              QLatin1String("selection-color"),
4744                              QLatin1String("current-line-color"),
4745                              QLatin1String("bracket-highlight-color"),
4746                              QLatin1String("word-wrap-marker-color"),
4747                              QLatin1String("font"),
4748                              QLatin1String("font-size"),
4749                              QLatin1String("scheme")};
4750     int spaceIndent = -1; // for backward compatibility; see below
4751     bool replaceTabsSet = false;
4752     int startPos(0);
4753 
4754     QString var;
4755     QString val;
4756     while ((match = kvVar.match(s, startPos)).hasMatch()) {
4757         startPos = match.capturedEnd(0);
4758         var = match.captured(1);
4759         val = match.captured(2).trimmed();
4760         bool state; // store booleans here
4761         int n; // store ints here
4762 
4763         // only apply view & renderer config stuff
4764         if (onlyViewAndRenderer) {
4765             if (contains(vvl, var)) { // FIXME define above
4766                 setViewVariable(var, val);
4767             }
4768         } else {
4769             // BOOL  SETTINGS
4770             if (var == QLatin1String("word-wrap") && checkBoolValue(val, &state)) {
4771                 setWordWrap(state); // ??? FIXME CHECK
4772             }
4773             // KateConfig::configFlags
4774             // FIXME should this be optimized to only a few calls? how?
4775             else if (var == QLatin1String("backspace-indents") && checkBoolValue(val, &state)) {
4776                 m_config->setBackspaceIndents(state);
4777             } else if (var == QLatin1String("indent-pasted-text") && checkBoolValue(val, &state)) {
4778                 m_config->setIndentPastedText(state);
4779             } else if (var == QLatin1String("replace-tabs") && checkBoolValue(val, &state)) {
4780                 m_config->setReplaceTabsDyn(state);
4781                 replaceTabsSet = true; // for backward compatibility; see below
4782             } else if (var == QLatin1String("remove-trailing-space") && checkBoolValue(val, &state)) {
4783                 qCWarning(LOG_KTE) << i18n(
4784                     "Using deprecated modeline 'remove-trailing-space'. "
4785                     "Please replace with 'remove-trailing-spaces modified;', see "
4786                     "https://docs.kde.org/?application=katepart&branch=stable5&path=config-variables.html#variable-remove-trailing-spaces");
4787                 m_config->setRemoveSpaces(state ? 1 : 0);
4788             } else if (var == QLatin1String("replace-trailing-space-save") && checkBoolValue(val, &state)) {
4789                 qCWarning(LOG_KTE) << i18n(
4790                     "Using deprecated modeline 'replace-trailing-space-save'. "
4791                     "Please replace with 'remove-trailing-spaces all;', see "
4792                     "https://docs.kde.org/?application=katepart&branch=stable5&path=config-variables.html#variable-remove-trailing-spaces");
4793                 m_config->setRemoveSpaces(state ? 2 : 0);
4794             } else if (var == QLatin1String("overwrite-mode") && checkBoolValue(val, &state)) {
4795                 m_config->setOvr(state);
4796             } else if (var == QLatin1String("keep-extra-spaces") && checkBoolValue(val, &state)) {
4797                 m_config->setKeepExtraSpaces(state);
4798             } else if (var == QLatin1String("tab-indents") && checkBoolValue(val, &state)) {
4799                 m_config->setTabIndents(state);
4800             } else if (var == QLatin1String("show-tabs") && checkBoolValue(val, &state)) {
4801                 m_config->setShowTabs(state);
4802             } else if (var == QLatin1String("show-trailing-spaces") && checkBoolValue(val, &state)) {
4803                 m_config->setShowSpaces(state ? KateDocumentConfig::Trailing : KateDocumentConfig::None);
4804             } else if (var == QLatin1String("space-indent") && checkBoolValue(val, &state)) {
4805                 // this is for backward compatibility; see below
4806                 spaceIndent = state;
4807             } else if (var == QLatin1String("smart-home") && checkBoolValue(val, &state)) {
4808                 m_config->setSmartHome(state);
4809             } else if (var == QLatin1String("newline-at-eof") && checkBoolValue(val, &state)) {
4810                 m_config->setNewLineAtEof(state);
4811             }
4812 
4813             // INTEGER SETTINGS
4814             else if (var == QLatin1String("tab-width") && checkIntValue(val, &n)) {
4815                 m_config->setTabWidth(n);
4816             } else if (var == QLatin1String("indent-width") && checkIntValue(val, &n)) {
4817                 m_config->setIndentationWidth(n);
4818             } else if (var == QLatin1String("indent-mode")) {
4819                 m_config->setIndentationMode(val);
4820             } else if (var == QLatin1String("word-wrap-column") && checkIntValue(val, &n) && n > 0) { // uint, but hard word wrap at 0 will be no fun ;)
4821                 m_config->setWordWrapAt(n);
4822             }
4823 
4824             // STRING SETTINGS
4825             else if (var == QLatin1String("eol") || var == QLatin1String("end-of-line")) {
4826                 const auto l = {QLatin1String("unix"), QLatin1String("dos"), QLatin1String("mac")};
4827                 if ((n = indexOf(l, val.toLower())) != -1) {
4828                     // set eol + avoid that it is overwritten by auto-detection again!
4829                     // this fixes e.g. .kateconfig files with // kate: eol dos; to work, bug 365705
4830                     m_config->setEol(n);
4831                     m_config->setAllowEolDetection(false);
4832                 }
4833             } else if (var == QLatin1String("bom") || var == QLatin1String("byte-order-mark") || var == QLatin1String("byte-order-marker")) {
4834                 if (checkBoolValue(val, &state)) {
4835                     m_config->setBom(state);
4836                 }
4837             } else if (var == QLatin1String("remove-trailing-spaces")) {
4838                 val = val.toLower();
4839                 if (val == QLatin1String("1") || val == QLatin1String("modified") || val == QLatin1String("mod") || val == QLatin1String("+")) {
4840                     m_config->setRemoveSpaces(1);
4841                 } else if (val == QLatin1String("2") || val == QLatin1String("all") || val == QLatin1String("*")) {
4842                     m_config->setRemoveSpaces(2);
4843                 } else {
4844                     m_config->setRemoveSpaces(0);
4845                 }
4846             } else if (var == QLatin1String("syntax") || var == QLatin1String("hl")) {
4847                 setHighlightingMode(val);
4848             } else if (var == QLatin1String("mode")) {
4849                 setMode(val);
4850             } else if (var == QLatin1String("encoding")) {
4851                 setEncoding(val);
4852             } else if (var == QLatin1String("default-dictionary")) {
4853                 setDefaultDictionary(val);
4854             } else if (var == QLatin1String("automatic-spell-checking") && checkBoolValue(val, &state)) {
4855                 onTheFlySpellCheckingEnabled(state);
4856             }
4857 
4858             // VIEW SETTINGS
4859             else if (contains(vvl, var)) {
4860                 setViewVariable(var, val);
4861             } else {
4862                 m_storedVariables[var] = val;
4863             }
4864         }
4865     }
4866 
4867     // Backward compatibility
4868     // If space-indent was set, but replace-tabs was not set, we assume
4869     // that the user wants to replace tabulators and set that flag.
4870     // If both were set, replace-tabs has precedence.
4871     // At this point spaceIndent is -1 if it was never set,
4872     // 0 if it was set to off, and 1 if it was set to on.
4873     // Note that if onlyViewAndRenderer was requested, spaceIndent is -1.
4874     if (!replaceTabsSet && spaceIndent >= 0) {
4875         m_config->setReplaceTabsDyn(spaceIndent > 0);
4876     }
4877 }
4878 
setViewVariable(const QString & var,const QString & val)4879 void KTextEditor::DocumentPrivate::setViewVariable(const QString &var, const QString &val)
4880 {
4881     bool state;
4882     int n;
4883     QColor c;
4884     for (auto v : std::as_const(m_views)) {
4885         // First, try the new config interface
4886         QVariant help(val); // Special treatment to catch "on"/"off"
4887         if (checkBoolValue(val, &state)) {
4888             help = state;
4889         }
4890         if (v->config()->setValue(var, help)) {
4891         } else if (v->renderer()->config()->setValue(var, help)) {
4892             // No success? Go the old way
4893         } else if (var == QLatin1String("dynamic-word-wrap") && checkBoolValue(val, &state)) {
4894             v->config()->setDynWordWrap(state);
4895         } else if (var == QLatin1String("block-selection") && checkBoolValue(val, &state)) {
4896             v->setBlockSelection(state);
4897 
4898             // else if ( var = "dynamic-word-wrap-indicators" )
4899         } else if (var == QLatin1String("icon-bar-color") && checkColorValue(val, c)) {
4900             v->renderer()->config()->setIconBarColor(c);
4901         }
4902         // RENDERER
4903         else if (var == QLatin1String("background-color") && checkColorValue(val, c)) {
4904             v->renderer()->config()->setBackgroundColor(c);
4905         } else if (var == QLatin1String("selection-color") && checkColorValue(val, c)) {
4906             v->renderer()->config()->setSelectionColor(c);
4907         } else if (var == QLatin1String("current-line-color") && checkColorValue(val, c)) {
4908             v->renderer()->config()->setHighlightedLineColor(c);
4909         } else if (var == QLatin1String("bracket-highlight-color") && checkColorValue(val, c)) {
4910             v->renderer()->config()->setHighlightedBracketColor(c);
4911         } else if (var == QLatin1String("word-wrap-marker-color") && checkColorValue(val, c)) {
4912             v->renderer()->config()->setWordWrapMarkerColor(c);
4913         } else if (var == QLatin1String("font") || (checkIntValue(val, &n) && var == QLatin1String("font-size"))) {
4914             QFont _f(v->renderer()->currentFont());
4915 
4916             if (var == QLatin1String("font")) {
4917                 _f.setFamily(val);
4918                 _f.setFixedPitch(QFont(val).fixedPitch());
4919             } else {
4920                 _f.setPointSize(n);
4921             }
4922 
4923             v->renderer()->config()->setFont(_f);
4924         } else if (var == QLatin1String("scheme")) {
4925             v->renderer()->config()->setSchema(val);
4926         }
4927     }
4928 }
4929 
checkBoolValue(QString val,bool * result)4930 bool KTextEditor::DocumentPrivate::checkBoolValue(QString val, bool *result)
4931 {
4932     val = val.trimmed().toLower();
4933     static const auto trueValues = {QLatin1String("1"), QLatin1String("on"), QLatin1String("true")};
4934     if (contains(trueValues, val)) {
4935         *result = true;
4936         return true;
4937     }
4938 
4939     static const auto falseValues = {QLatin1String("0"), QLatin1String("off"), QLatin1String("false")};
4940     if (contains(falseValues, val)) {
4941         *result = false;
4942         return true;
4943     }
4944     return false;
4945 }
4946 
checkIntValue(const QString & val,int * result)4947 bool KTextEditor::DocumentPrivate::checkIntValue(const QString &val, int *result)
4948 {
4949     bool ret(false);
4950     *result = val.toInt(&ret);
4951     return ret;
4952 }
4953 
checkColorValue(const QString & val,QColor & c)4954 bool KTextEditor::DocumentPrivate::checkColorValue(const QString &val, QColor &c)
4955 {
4956     c.setNamedColor(val);
4957     return c.isValid();
4958 }
4959 
4960 // KTextEditor::variable
variable(const QString & name) const4961 QString KTextEditor::DocumentPrivate::variable(const QString &name) const
4962 {
4963     auto it = m_storedVariables.find(name);
4964     if (it == m_storedVariables.end()) {
4965         return QString();
4966     }
4967     return it->second;
4968 }
4969 
setVariable(const QString & name,const QString & value)4970 void KTextEditor::DocumentPrivate::setVariable(const QString &name, const QString &value)
4971 {
4972     QString s = QStringLiteral("kate: ");
4973     s.append(name);
4974     s.append(QLatin1Char(' '));
4975     s.append(value);
4976     readVariableLine(s);
4977 }
4978 
4979 // END
4980 
slotModOnHdDirty(const QString & path)4981 void KTextEditor::DocumentPrivate::slotModOnHdDirty(const QString &path)
4982 {
4983     if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskModified)) {
4984         m_modOnHd = true;
4985         m_modOnHdReason = OnDiskModified;
4986 
4987         if (!m_modOnHdTimer.isActive()) {
4988             m_modOnHdTimer.start();
4989         }
4990     }
4991 }
4992 
slotModOnHdCreated(const QString & path)4993 void KTextEditor::DocumentPrivate::slotModOnHdCreated(const QString &path)
4994 {
4995     if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskCreated)) {
4996         m_modOnHd = true;
4997         m_modOnHdReason = OnDiskCreated;
4998 
4999         if (!m_modOnHdTimer.isActive()) {
5000             m_modOnHdTimer.start();
5001         }
5002     }
5003 }
5004 
slotModOnHdDeleted(const QString & path)5005 void KTextEditor::DocumentPrivate::slotModOnHdDeleted(const QString &path)
5006 {
5007     if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskDeleted)) {
5008         m_modOnHd = true;
5009         m_modOnHdReason = OnDiskDeleted;
5010 
5011         if (!m_modOnHdTimer.isActive()) {
5012             m_modOnHdTimer.start();
5013         }
5014     }
5015 }
5016 
slotDelayedHandleModOnHd()5017 void KTextEditor::DocumentPrivate::slotDelayedHandleModOnHd()
5018 {
5019     // compare git hash with the one we have (if we have one)
5020     const QByteArray oldDigest = checksum();
5021     if (!oldDigest.isEmpty() && !url().isEmpty() && url().isLocalFile()) {
5022         // if current checksum == checksum of new file => unmodified
5023         if (m_modOnHdReason != OnDiskDeleted && createDigest() && oldDigest == checksum()) {
5024             m_modOnHd = false;
5025             m_modOnHdReason = OnDiskUnmodified;
5026             m_prevModOnHdReason = OnDiskUnmodified;
5027         }
5028 
5029         // if still modified, try to take a look at git
5030         // skip that, if document is modified!
5031         // only do that, if the file is still there, else reload makes no sense!
5032         if (m_modOnHd && !isModified() && QFile::exists(url().toLocalFile())) {
5033             QProcess git;
5034             const QStringList args{QStringLiteral("cat-file"), QStringLiteral("-e"), QString::fromUtf8(oldDigest)};
5035             git.start(QStringLiteral("git"), args);
5036             if (git.waitForStarted()) {
5037                 git.closeWriteChannel();
5038                 if (git.waitForFinished()) {
5039                     if (git.exitCode() == 0) {
5040                         // this hash exists still in git => just reload
5041                         m_modOnHd = false;
5042                         m_modOnHdReason = OnDiskUnmodified;
5043                         m_prevModOnHdReason = OnDiskUnmodified;
5044                         documentReload();
5045                     }
5046                 }
5047             }
5048         }
5049     }
5050 
5051     // emit our signal to the outside!
5052     Q_EMIT modifiedOnDisk(this, m_modOnHd, m_modOnHdReason);
5053 }
5054 
checksum() const5055 QByteArray KTextEditor::DocumentPrivate::checksum() const
5056 {
5057     return m_buffer->digest();
5058 }
5059 
createDigest()5060 bool KTextEditor::DocumentPrivate::createDigest()
5061 {
5062     QByteArray digest;
5063 
5064     if (url().isLocalFile()) {
5065         QFile f(url().toLocalFile());
5066         if (f.open(QIODevice::ReadOnly)) {
5067             // init the hash with the git header
5068             QCryptographicHash crypto(QCryptographicHash::Sha1);
5069             const QString header = QStringLiteral("blob %1").arg(f.size());
5070             crypto.addData(header.toLatin1() + '\0');
5071 
5072             while (!f.atEnd()) {
5073                 crypto.addData(f.read(256 * 1024));
5074             }
5075 
5076             digest = crypto.result();
5077         }
5078     }
5079 
5080     // set new digest
5081     m_buffer->setDigest(digest);
5082     return !digest.isEmpty();
5083 }
5084 
reasonedMOHString() const5085 QString KTextEditor::DocumentPrivate::reasonedMOHString() const
5086 {
5087     // squeeze path
5088     const QString str = KStringHandler::csqueeze(url().toDisplayString(QUrl::PreferLocalFile));
5089 
5090     switch (m_modOnHdReason) {
5091     case OnDiskModified:
5092         return i18n("The file '%1' was modified by another program.", str);
5093         break;
5094     case OnDiskCreated:
5095         return i18n("The file '%1' was created by another program.", str);
5096         break;
5097     case OnDiskDeleted:
5098         return i18n("The file '%1' was deleted by another program.", str);
5099         break;
5100     default:
5101         return QString();
5102     }
5103     Q_UNREACHABLE();
5104     return QString();
5105 }
5106 
removeTrailingSpaces()5107 void KTextEditor::DocumentPrivate::removeTrailingSpaces()
5108 {
5109     const int remove = config()->removeSpaces();
5110     if (remove == 0) {
5111         return;
5112     }
5113 
5114     // temporarily disable static word wrap (see bug #328900)
5115     const bool wordWrapEnabled = config()->wordWrap();
5116     if (wordWrapEnabled) {
5117         setWordWrap(false);
5118     }
5119 
5120     editStart();
5121 
5122     const int lines = this->lines();
5123     for (int line = 0; line < lines; ++line) {
5124         Kate::TextLine textline = plainKateTextLine(line);
5125 
5126         // remove trailing spaces in entire document, remove = 2
5127         // remove trailing spaces of touched lines, remove = 1
5128         // remove trailing spaces of lines saved on disk, remove = 1
5129         if (remove == 2 || textline->markedAsModified() || textline->markedAsSavedOnDisk()) {
5130             const int p = textline->lastChar() + 1;
5131             const int l = textline->length() - p;
5132             if (l > 0) {
5133                 editRemoveText(line, p, l);
5134             }
5135         }
5136     }
5137 
5138     editEnd();
5139 
5140     // enable word wrap again, if it was enabled (see bug #328900)
5141     if (wordWrapEnabled) {
5142         setWordWrap(true); // see begin of this function
5143     }
5144 }
5145 
updateFileType(const QString & newType,bool user)5146 bool KTextEditor::DocumentPrivate::updateFileType(const QString &newType, bool user)
5147 {
5148     if (user || !m_fileTypeSetByUser) {
5149         if (newType.isEmpty()) {
5150             return false;
5151         }
5152         KateFileType fileType = KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType);
5153         // if the mode "newType" does not exist
5154         if (fileType.name.isEmpty()) {
5155             return false;
5156         }
5157 
5158         // remember that we got set by user
5159         m_fileTypeSetByUser = user;
5160 
5161         m_fileType = newType;
5162 
5163         m_config->configStart();
5164 
5165         // NOTE: if the user changes the Mode, the Highlighting also changes.
5166         // m_hlSetByUser avoids resetting the highlight when saving the document, if
5167         // the current hl isn't stored (eg, in sftp:// or fish:// files) (see bug #407763)
5168         if ((user || !m_hlSetByUser) && !fileType.hl.isEmpty()) {
5169             int hl(KateHlManager::self()->nameFind(fileType.hl));
5170 
5171             if (hl >= 0) {
5172                 m_buffer->setHighlight(hl);
5173             }
5174         }
5175 
5176         // set the indentation mode, if any in the mode...
5177         // and user did not set it before!
5178         // NOTE: KateBuffer::setHighlight() also sets the indentation.
5179         if (!m_indenterSetByUser && !fileType.indenter.isEmpty()) {
5180             config()->setIndentationMode(fileType.indenter);
5181         }
5182 
5183         // views!
5184         for (auto v : std::as_const(m_views)) {
5185             v->config()->configStart();
5186             v->renderer()->config()->configStart();
5187         }
5188 
5189         bool bom_settings = false;
5190         if (m_bomSetByUser) {
5191             bom_settings = m_config->bom();
5192         }
5193         readVariableLine(fileType.varLine);
5194         if (m_bomSetByUser) {
5195             m_config->setBom(bom_settings);
5196         }
5197         m_config->configEnd();
5198         for (auto v : std::as_const(m_views)) {
5199             v->config()->configEnd();
5200             v->renderer()->config()->configEnd();
5201         }
5202     }
5203 
5204     // fixme, make this better...
5205     Q_EMIT modeChanged(this);
5206     return true;
5207 }
5208 
slotQueryClose_save(bool * handled,bool * abortClosing)5209 void KTextEditor::DocumentPrivate::slotQueryClose_save(bool *handled, bool *abortClosing)
5210 {
5211     *handled = true;
5212     *abortClosing = true;
5213     if (this->url().isEmpty()) {
5214         QWidget *parentWidget(dialogParent());
5215         const QUrl res = QFileDialog::getSaveFileUrl(parentWidget, i18n("Save File"));
5216         if (res.isEmpty()) {
5217             *abortClosing = true;
5218             return;
5219         }
5220         saveAs(res);
5221         *abortClosing = false;
5222     } else {
5223         save();
5224         *abortClosing = false;
5225     }
5226 }
5227 
5228 // BEGIN KTextEditor::ConfigInterface
5229 
5230 // BEGIN ConfigInterface stff
configKeys() const5231 QStringList KTextEditor::DocumentPrivate::configKeys() const
5232 {
5233     // expose all internally registered keys of the KateDocumentConfig
5234     return m_config->configKeys();
5235 }
5236 
configValue(const QString & key)5237 QVariant KTextEditor::DocumentPrivate::configValue(const QString &key)
5238 {
5239     // just dispatch to internal key => value lookup
5240     return m_config->value(key);
5241 }
5242 
setConfigValue(const QString & key,const QVariant & value)5243 void KTextEditor::DocumentPrivate::setConfigValue(const QString &key, const QVariant &value)
5244 {
5245     // just dispatch to internal key + value set
5246     m_config->setValue(key, value);
5247 }
5248 
5249 // END KTextEditor::ConfigInterface
5250 
documentEnd() const5251 KTextEditor::Cursor KTextEditor::DocumentPrivate::documentEnd() const
5252 {
5253     return KTextEditor::Cursor(lastLine(), lineLength(lastLine()));
5254 }
5255 
replaceText(const KTextEditor::Range & range,const QString & s,bool block)5256 bool KTextEditor::DocumentPrivate::replaceText(const KTextEditor::Range &range, const QString &s, bool block)
5257 {
5258     // TODO more efficient?
5259     editStart();
5260     bool changed = removeText(range, block);
5261     changed |= insertText(range.start(), s, block);
5262     editEnd();
5263     return changed;
5264 }
5265 
highlight() const5266 KateHighlighting *KTextEditor::DocumentPrivate::highlight() const
5267 {
5268     return m_buffer->highlight();
5269 }
5270 
kateTextLine(int i)5271 Kate::TextLine KTextEditor::DocumentPrivate::kateTextLine(int i)
5272 {
5273     m_buffer->ensureHighlighted(i);
5274     return m_buffer->plainLine(i);
5275 }
5276 
plainKateTextLine(int i)5277 Kate::TextLine KTextEditor::DocumentPrivate::plainKateTextLine(int i)
5278 {
5279     return m_buffer->plainLine(i);
5280 }
5281 
isEditRunning() const5282 bool KTextEditor::DocumentPrivate::isEditRunning() const
5283 {
5284     return editIsRunning;
5285 }
5286 
setUndoMergeAllEdits(bool merge)5287 void KTextEditor::DocumentPrivate::setUndoMergeAllEdits(bool merge)
5288 {
5289     if (merge && m_undoMergeAllEdits) {
5290         // Don't add another undo safe point: it will override our current one,
5291         // meaning we'll need two undo's to get back there - which defeats the object!
5292         return;
5293     }
5294     m_undoManager->undoSafePoint();
5295     m_undoManager->setAllowComplexMerge(merge);
5296     m_undoMergeAllEdits = merge;
5297 }
5298 
5299 // BEGIN KTextEditor::MovingInterface
newMovingCursor(const KTextEditor::Cursor & position,KTextEditor::MovingCursor::InsertBehavior insertBehavior)5300 KTextEditor::MovingCursor *KTextEditor::DocumentPrivate::newMovingCursor(const KTextEditor::Cursor &position,
5301                                                                          KTextEditor::MovingCursor::InsertBehavior insertBehavior)
5302 {
5303     return new Kate::TextCursor(buffer(), position, insertBehavior);
5304 }
5305 
newMovingRange(const KTextEditor::Range & range,KTextEditor::MovingRange::InsertBehaviors insertBehaviors,KTextEditor::MovingRange::EmptyBehavior emptyBehavior)5306 KTextEditor::MovingRange *KTextEditor::DocumentPrivate::newMovingRange(const KTextEditor::Range &range,
5307                                                                        KTextEditor::MovingRange::InsertBehaviors insertBehaviors,
5308                                                                        KTextEditor::MovingRange::EmptyBehavior emptyBehavior)
5309 {
5310     return new Kate::TextRange(buffer(), range, insertBehaviors, emptyBehavior);
5311 }
5312 
revision() const5313 qint64 KTextEditor::DocumentPrivate::revision() const
5314 {
5315     return m_buffer->history().revision();
5316 }
5317 
lastSavedRevision() const5318 qint64 KTextEditor::DocumentPrivate::lastSavedRevision() const
5319 {
5320     return m_buffer->history().lastSavedRevision();
5321 }
5322 
lockRevision(qint64 revision)5323 void KTextEditor::DocumentPrivate::lockRevision(qint64 revision)
5324 {
5325     m_buffer->history().lockRevision(revision);
5326 }
5327 
unlockRevision(qint64 revision)5328 void KTextEditor::DocumentPrivate::unlockRevision(qint64 revision)
5329 {
5330     m_buffer->history().unlockRevision(revision);
5331 }
5332 
transformCursor(int & line,int & column,KTextEditor::MovingCursor::InsertBehavior insertBehavior,qint64 fromRevision,qint64 toRevision)5333 void KTextEditor::DocumentPrivate::transformCursor(int &line,
5334                                                    int &column,
5335                                                    KTextEditor::MovingCursor::InsertBehavior insertBehavior,
5336                                                    qint64 fromRevision,
5337                                                    qint64 toRevision)
5338 {
5339     m_buffer->history().transformCursor(line, column, insertBehavior, fromRevision, toRevision);
5340 }
5341 
transformCursor(KTextEditor::Cursor & cursor,KTextEditor::MovingCursor::InsertBehavior insertBehavior,qint64 fromRevision,qint64 toRevision)5342 void KTextEditor::DocumentPrivate::transformCursor(KTextEditor::Cursor &cursor,
5343                                                    KTextEditor::MovingCursor::InsertBehavior insertBehavior,
5344                                                    qint64 fromRevision,
5345                                                    qint64 toRevision)
5346 {
5347     int line = cursor.line();
5348     int column = cursor.column();
5349     m_buffer->history().transformCursor(line, column, insertBehavior, fromRevision, toRevision);
5350     cursor.setPosition(line, column);
5351 }
5352 
transformRange(KTextEditor::Range & range,KTextEditor::MovingRange::InsertBehaviors insertBehaviors,KTextEditor::MovingRange::EmptyBehavior emptyBehavior,qint64 fromRevision,qint64 toRevision)5353 void KTextEditor::DocumentPrivate::transformRange(KTextEditor::Range &range,
5354                                                   KTextEditor::MovingRange::InsertBehaviors insertBehaviors,
5355                                                   KTextEditor::MovingRange::EmptyBehavior emptyBehavior,
5356                                                   qint64 fromRevision,
5357                                                   qint64 toRevision)
5358 {
5359     m_buffer->history().transformRange(range, insertBehaviors, emptyBehavior, fromRevision, toRevision);
5360 }
5361 
5362 // END
5363 
5364 // BEGIN KTextEditor::AnnotationInterface
setAnnotationModel(KTextEditor::AnnotationModel * model)5365 void KTextEditor::DocumentPrivate::setAnnotationModel(KTextEditor::AnnotationModel *model)
5366 {
5367     KTextEditor::AnnotationModel *oldmodel = m_annotationModel;
5368     m_annotationModel = model;
5369     Q_EMIT annotationModelChanged(oldmodel, m_annotationModel);
5370 }
5371 
annotationModel() const5372 KTextEditor::AnnotationModel *KTextEditor::DocumentPrivate::annotationModel() const
5373 {
5374     return m_annotationModel;
5375 }
5376 // END KTextEditor::AnnotationInterface
5377 
5378 // TAKEN FROM kparts.h
queryClose()5379 bool KTextEditor::DocumentPrivate::queryClose()
5380 {
5381     if (!isReadWrite() // Can't be modified
5382         || !isModified() // Nothing was modified
5383         || (url() == QUrl() && lines() == 1 && text() == QString()) // Unsaved and blank
5384     ) {
5385         return true;
5386     }
5387 
5388     QString docName = documentName();
5389 
5390     int res = KMessageBox::warningYesNoCancel(dialogParent(),
5391                                               i18n("The document \"%1\" has been modified.\n"
5392                                                    "Do you want to save your changes or discard them?",
5393                                                    docName),
5394                                               i18n("Close Document"),
5395                                               KStandardGuiItem::save(),
5396                                               KStandardGuiItem::discard());
5397 
5398     bool abortClose = false;
5399     bool handled = false;
5400 
5401     switch (res) {
5402     case KMessageBox::Yes:
5403         sigQueryClose(&handled, &abortClose);
5404         if (!handled) {
5405             if (url().isEmpty()) {
5406                 QUrl url = QFileDialog::getSaveFileUrl(dialogParent());
5407                 if (url.isEmpty()) {
5408                     return false;
5409                 }
5410 
5411                 saveAs(url);
5412             } else {
5413                 save();
5414             }
5415         } else if (abortClose) {
5416             return false;
5417         }
5418         return waitSaveComplete();
5419     case KMessageBox::No:
5420         return true;
5421     default: // case KMessageBox::Cancel :
5422         return false;
5423     }
5424 }
5425 
slotStarted(KIO::Job * job)5426 void KTextEditor::DocumentPrivate::slotStarted(KIO::Job *job)
5427 {
5428     // if we are idle before, we are now loading!
5429     if (m_documentState == DocumentIdle) {
5430         m_documentState = DocumentLoading;
5431     }
5432 
5433     // if loading:
5434     // - remember pre loading read-write mode
5435     // if remote load:
5436     // - set to read-only
5437     // - trigger possible message
5438     if (m_documentState == DocumentLoading) {
5439         // remember state
5440         m_readWriteStateBeforeLoading = isReadWrite();
5441 
5442         // perhaps show loading message, but wait one second
5443         if (job) {
5444             // only read only if really remote file!
5445             setReadWrite(false);
5446 
5447             // perhaps some message about loading in one second!
5448             // remember job pointer, we want to be able to kill it!
5449             m_loadingJob = job;
5450             QTimer::singleShot(1000, this, SLOT(slotTriggerLoadingMessage()));
5451         }
5452     }
5453 }
5454 
slotCompleted()5455 void KTextEditor::DocumentPrivate::slotCompleted()
5456 {
5457     // if were loading, reset back to old read-write mode before loading
5458     // and kill the possible loading message
5459     if (m_documentState == DocumentLoading) {
5460         setReadWrite(m_readWriteStateBeforeLoading);
5461         delete m_loadingMessage;
5462     }
5463 
5464     // Emit signal that we saved  the document, if needed
5465     if (m_documentState == DocumentSaving || m_documentState == DocumentSavingAs) {
5466         Q_EMIT documentSavedOrUploaded(this, m_documentState == DocumentSavingAs);
5467     }
5468 
5469     // back to idle mode
5470     m_documentState = DocumentIdle;
5471     m_reloading = false;
5472 }
5473 
slotCanceled()5474 void KTextEditor::DocumentPrivate::slotCanceled()
5475 {
5476     // if were loading, reset back to old read-write mode before loading
5477     // and kill the possible loading message
5478     if (m_documentState == DocumentLoading) {
5479         setReadWrite(m_readWriteStateBeforeLoading);
5480         delete m_loadingMessage;
5481 
5482         showAndSetOpeningErrorAccess();
5483 
5484         updateDocName();
5485     }
5486 
5487     // back to idle mode
5488     m_documentState = DocumentIdle;
5489     m_reloading = false;
5490 }
5491 
slotTriggerLoadingMessage()5492 void KTextEditor::DocumentPrivate::slotTriggerLoadingMessage()
5493 {
5494     // no longer loading?
5495     // no message needed!
5496     if (m_documentState != DocumentLoading) {
5497         return;
5498     }
5499 
5500     // create message about file loading in progress
5501     delete m_loadingMessage;
5502     m_loadingMessage =
5503         new KTextEditor::Message(i18n("The file <a href=\"%1\">%2</a> is still loading.", url().toDisplayString(QUrl::PreferLocalFile), url().fileName()));
5504     m_loadingMessage->setPosition(KTextEditor::Message::TopInView);
5505 
5506     // if around job: add cancel action
5507     if (m_loadingJob) {
5508         QAction *cancel = new QAction(i18n("&Abort Loading"), nullptr);
5509         connect(cancel, &QAction::triggered, this, &KTextEditor::DocumentPrivate::slotAbortLoading);
5510         m_loadingMessage->addAction(cancel);
5511     }
5512 
5513     // really post message
5514     postMessage(m_loadingMessage);
5515 }
5516 
slotAbortLoading()5517 void KTextEditor::DocumentPrivate::slotAbortLoading()
5518 {
5519     // no job, no work
5520     if (!m_loadingJob) {
5521         return;
5522     }
5523 
5524     // abort loading if any job
5525     // signal results!
5526     m_loadingJob->kill(KJob::EmitResult);
5527     m_loadingJob = nullptr;
5528 }
5529 
slotUrlChanged(const QUrl & url)5530 void KTextEditor::DocumentPrivate::slotUrlChanged(const QUrl &url)
5531 {
5532     if (m_reloading) {
5533         // the URL is temporarily unset and then reset to the previous URL during reload
5534         // we do not want to notify the outside about this
5535         return;
5536     }
5537 
5538     Q_UNUSED(url);
5539     updateDocName();
5540     Q_EMIT documentUrlChanged(this);
5541 }
5542 
save()5543 bool KTextEditor::DocumentPrivate::save()
5544 {
5545     // no double save/load
5546     // we need to allow DocumentPreSavingAs here as state, as save is called in saveAs!
5547     if ((m_documentState != DocumentIdle) && (m_documentState != DocumentPreSavingAs)) {
5548         return false;
5549     }
5550 
5551     // if we are idle, we are now saving
5552     if (m_documentState == DocumentIdle) {
5553         m_documentState = DocumentSaving;
5554     } else {
5555         m_documentState = DocumentSavingAs;
5556     }
5557 
5558     // call back implementation for real work
5559     return KTextEditor::Document::save();
5560 }
5561 
saveAs(const QUrl & url)5562 bool KTextEditor::DocumentPrivate::saveAs(const QUrl &url)
5563 {
5564     // abort on bad URL
5565     // that is done in saveAs below, too
5566     // but we must check it here already to avoid messing up
5567     // as no signals will be send, then
5568     if (!url.isValid()) {
5569         return false;
5570     }
5571 
5572     // no double save/load
5573     if (m_documentState != DocumentIdle) {
5574         return false;
5575     }
5576 
5577     // we enter the pre save as phase
5578     m_documentState = DocumentPreSavingAs;
5579 
5580     // call base implementation for real work
5581     return KTextEditor::Document::saveAs(normalizeUrl(url));
5582 }
5583 
defaultDictionary() const5584 QString KTextEditor::DocumentPrivate::defaultDictionary() const
5585 {
5586     return m_defaultDictionary;
5587 }
5588 
dictionaryRanges() const5589 QList<QPair<KTextEditor::MovingRange *, QString>> KTextEditor::DocumentPrivate::dictionaryRanges() const
5590 {
5591     return m_dictionaryRanges;
5592 }
5593 
clearDictionaryRanges()5594 void KTextEditor::DocumentPrivate::clearDictionaryRanges()
5595 {
5596     for (QList<QPair<KTextEditor::MovingRange *, QString>>::iterator i = m_dictionaryRanges.begin(); i != m_dictionaryRanges.end(); ++i) {
5597         delete (*i).first;
5598     }
5599     m_dictionaryRanges.clear();
5600     if (m_onTheFlyChecker) {
5601         m_onTheFlyChecker->refreshSpellCheck();
5602     }
5603     Q_EMIT dictionaryRangesPresent(false);
5604 }
5605 
setDictionary(const QString & newDictionary,const KTextEditor::Range & range,bool blockmode)5606 void KTextEditor::DocumentPrivate::setDictionary(const QString &newDictionary, const KTextEditor::Range &range, bool blockmode)
5607 {
5608     if (blockmode) {
5609         for (int i = range.start().line(); i <= range.end().line(); ++i) {
5610             setDictionary(newDictionary, rangeOnLine(range, i));
5611         }
5612     } else {
5613         setDictionary(newDictionary, range);
5614     }
5615 
5616     Q_EMIT dictionaryRangesPresent(!m_dictionaryRanges.isEmpty());
5617 }
5618 
setDictionary(const QString & newDictionary,const KTextEditor::Range & range)5619 void KTextEditor::DocumentPrivate::setDictionary(const QString &newDictionary, const KTextEditor::Range &range)
5620 {
5621     KTextEditor::Range newDictionaryRange = range;
5622     if (!newDictionaryRange.isValid() || newDictionaryRange.isEmpty()) {
5623         return;
5624     }
5625     QList<QPair<KTextEditor::MovingRange *, QString>> newRanges;
5626     // all ranges is 'm_dictionaryRanges' are assumed to be mutually disjoint
5627     for (auto i = m_dictionaryRanges.begin(); i != m_dictionaryRanges.end();) {
5628         qCDebug(LOG_KTE) << "new iteration" << newDictionaryRange;
5629         if (newDictionaryRange.isEmpty()) {
5630             break;
5631         }
5632         QPair<KTextEditor::MovingRange *, QString> pair = *i;
5633         QString dictionarySet = pair.second;
5634         KTextEditor::MovingRange *dictionaryRange = pair.first;
5635         qCDebug(LOG_KTE) << *dictionaryRange << dictionarySet;
5636         if (dictionaryRange->contains(newDictionaryRange) && newDictionary == dictionarySet) {
5637             qCDebug(LOG_KTE) << "dictionaryRange contains newDictionaryRange";
5638             return;
5639         }
5640         if (newDictionaryRange.contains(*dictionaryRange)) {
5641             delete dictionaryRange;
5642             i = m_dictionaryRanges.erase(i);
5643             qCDebug(LOG_KTE) << "newDictionaryRange contains dictionaryRange";
5644             continue;
5645         }
5646 
5647         KTextEditor::Range intersection = dictionaryRange->toRange().intersect(newDictionaryRange);
5648         if (!intersection.isEmpty() && intersection.isValid()) {
5649             if (dictionarySet == newDictionary) { // we don't have to do anything for 'intersection'
5650                 // except cut off the intersection
5651                 QList<KTextEditor::Range> remainingRanges = KateSpellCheckManager::rangeDifference(newDictionaryRange, intersection);
5652                 Q_ASSERT(remainingRanges.size() == 1);
5653                 newDictionaryRange = remainingRanges.first();
5654                 ++i;
5655                 qCDebug(LOG_KTE) << "dictionarySet == newDictionary";
5656                 continue;
5657             }
5658             QList<KTextEditor::Range> remainingRanges = KateSpellCheckManager::rangeDifference(*dictionaryRange, intersection);
5659             for (auto j = remainingRanges.begin(); j != remainingRanges.end(); ++j) {
5660                 KTextEditor::MovingRange *remainingRange = newMovingRange(*j, KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight);
5661                 remainingRange->setFeedback(this);
5662                 newRanges.push_back({remainingRange, dictionarySet});
5663             }
5664             i = m_dictionaryRanges.erase(i);
5665             delete dictionaryRange;
5666         } else {
5667             ++i;
5668         }
5669     }
5670     m_dictionaryRanges += newRanges;
5671     if (!newDictionaryRange.isEmpty() && !newDictionary.isEmpty()) { // we don't add anything for the default dictionary
5672         KTextEditor::MovingRange *newDictionaryMovingRange =
5673             newMovingRange(newDictionaryRange, KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight);
5674         newDictionaryMovingRange->setFeedback(this);
5675         m_dictionaryRanges.push_back({newDictionaryMovingRange, newDictionary});
5676     }
5677     if (m_onTheFlyChecker && !newDictionaryRange.isEmpty()) {
5678         m_onTheFlyChecker->refreshSpellCheck(newDictionaryRange);
5679     }
5680 }
5681 
setDefaultDictionary(const QString & dict)5682 void KTextEditor::DocumentPrivate::setDefaultDictionary(const QString &dict)
5683 {
5684     if (m_defaultDictionary == dict) {
5685         return;
5686     }
5687 
5688     m_defaultDictionary = dict;
5689 
5690     if (m_onTheFlyChecker) {
5691         m_onTheFlyChecker->updateConfig();
5692         refreshOnTheFlyCheck();
5693     }
5694     Q_EMIT defaultDictionaryChanged(this);
5695 }
5696 
onTheFlySpellCheckingEnabled(bool enable)5697 void KTextEditor::DocumentPrivate::onTheFlySpellCheckingEnabled(bool enable)
5698 {
5699     if (isOnTheFlySpellCheckingEnabled() == enable) {
5700         return;
5701     }
5702 
5703     if (enable) {
5704         Q_ASSERT(m_onTheFlyChecker == nullptr);
5705         m_onTheFlyChecker = new KateOnTheFlyChecker(this);
5706     } else {
5707         delete m_onTheFlyChecker;
5708         m_onTheFlyChecker = nullptr;
5709     }
5710 
5711     for (auto view : std::as_const(m_views)) {
5712         view->reflectOnTheFlySpellCheckStatus(enable);
5713     }
5714 }
5715 
isOnTheFlySpellCheckingEnabled() const5716 bool KTextEditor::DocumentPrivate::isOnTheFlySpellCheckingEnabled() const
5717 {
5718     return m_onTheFlyChecker != nullptr;
5719 }
5720 
dictionaryForMisspelledRange(const KTextEditor::Range & range) const5721 QString KTextEditor::DocumentPrivate::dictionaryForMisspelledRange(const KTextEditor::Range &range) const
5722 {
5723     if (!m_onTheFlyChecker) {
5724         return QString();
5725     } else {
5726         return m_onTheFlyChecker->dictionaryForMisspelledRange(range);
5727     }
5728 }
5729 
clearMisspellingForWord(const QString & word)5730 void KTextEditor::DocumentPrivate::clearMisspellingForWord(const QString &word)
5731 {
5732     if (m_onTheFlyChecker) {
5733         m_onTheFlyChecker->clearMisspellingForWord(word);
5734     }
5735 }
5736 
refreshOnTheFlyCheck(const KTextEditor::Range & range)5737 void KTextEditor::DocumentPrivate::refreshOnTheFlyCheck(const KTextEditor::Range &range)
5738 {
5739     if (m_onTheFlyChecker) {
5740         m_onTheFlyChecker->refreshSpellCheck(range);
5741     }
5742 }
5743 
rangeInvalid(KTextEditor::MovingRange * movingRange)5744 void KTextEditor::DocumentPrivate::rangeInvalid(KTextEditor::MovingRange *movingRange)
5745 {
5746     deleteDictionaryRange(movingRange);
5747 }
5748 
rangeEmpty(KTextEditor::MovingRange * movingRange)5749 void KTextEditor::DocumentPrivate::rangeEmpty(KTextEditor::MovingRange *movingRange)
5750 {
5751     deleteDictionaryRange(movingRange);
5752 }
5753 
deleteDictionaryRange(KTextEditor::MovingRange * movingRange)5754 void KTextEditor::DocumentPrivate::deleteDictionaryRange(KTextEditor::MovingRange *movingRange)
5755 {
5756     qCDebug(LOG_KTE) << "deleting" << movingRange;
5757 
5758     auto finder = [=](const QPair<KTextEditor::MovingRange *, QString> &item) -> bool {
5759         return item.first == movingRange;
5760     };
5761 
5762     auto it = std::find_if(m_dictionaryRanges.begin(), m_dictionaryRanges.end(), finder);
5763 
5764     if (it != m_dictionaryRanges.end()) {
5765         m_dictionaryRanges.erase(it);
5766         delete movingRange;
5767     }
5768 
5769     Q_ASSERT(std::find_if(m_dictionaryRanges.begin(), m_dictionaryRanges.end(), finder) == m_dictionaryRanges.end());
5770 }
5771 
containsCharacterEncoding(const KTextEditor::Range & range)5772 bool KTextEditor::DocumentPrivate::containsCharacterEncoding(const KTextEditor::Range &range)
5773 {
5774     KateHighlighting *highlighting = highlight();
5775 
5776     const int rangeStartLine = range.start().line();
5777     const int rangeStartColumn = range.start().column();
5778     const int rangeEndLine = range.end().line();
5779     const int rangeEndColumn = range.end().column();
5780 
5781     for (int line = range.start().line(); line <= rangeEndLine; ++line) {
5782         const Kate::TextLine textLine = kateTextLine(line);
5783         const int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0;
5784         const int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine->length();
5785         for (int col = startColumn; col < endColumn; ++col) {
5786             int attr = textLine->attribute(col);
5787             const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr);
5788             if (!prefixStore.findPrefix(textLine, col).isEmpty()) {
5789                 return true;
5790             }
5791         }
5792     }
5793 
5794     return false;
5795 }
5796 
computePositionWrtOffsets(const OffsetList & offsetList,int pos)5797 int KTextEditor::DocumentPrivate::computePositionWrtOffsets(const OffsetList &offsetList, int pos)
5798 {
5799     int previousOffset = 0;
5800     for (auto i = offsetList.cbegin(); i != offsetList.cend(); ++i) {
5801         if (i->first > pos) {
5802             break;
5803         }
5804         previousOffset = i->second;
5805     }
5806     return pos + previousOffset;
5807 }
5808 
decodeCharacters(const KTextEditor::Range & range,KTextEditor::DocumentPrivate::OffsetList & decToEncOffsetList,KTextEditor::DocumentPrivate::OffsetList & encToDecOffsetList)5809 QString KTextEditor::DocumentPrivate::decodeCharacters(const KTextEditor::Range &range,
5810                                                        KTextEditor::DocumentPrivate::OffsetList &decToEncOffsetList,
5811                                                        KTextEditor::DocumentPrivate::OffsetList &encToDecOffsetList)
5812 {
5813     QString toReturn;
5814     KTextEditor::Cursor previous = range.start();
5815     int decToEncCurrentOffset = 0;
5816     int encToDecCurrentOffset = 0;
5817     int i = 0;
5818     int newI = 0;
5819 
5820     KateHighlighting *highlighting = highlight();
5821     Kate::TextLine textLine;
5822 
5823     const int rangeStartLine = range.start().line();
5824     const int rangeStartColumn = range.start().column();
5825     const int rangeEndLine = range.end().line();
5826     const int rangeEndColumn = range.end().column();
5827 
5828     for (int line = range.start().line(); line <= rangeEndLine; ++line) {
5829         textLine = kateTextLine(line);
5830         int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0;
5831         int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine->length();
5832         for (int col = startColumn; col < endColumn;) {
5833             int attr = textLine->attribute(col);
5834             const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr);
5835             const QHash<QString, QChar> &characterEncodingsHash = highlighting->getCharacterEncodings(attr);
5836             QString matchingPrefix = prefixStore.findPrefix(textLine, col);
5837             if (!matchingPrefix.isEmpty()) {
5838                 toReturn += text(KTextEditor::Range(previous, KTextEditor::Cursor(line, col)));
5839                 const QChar &c = characterEncodingsHash.value(matchingPrefix);
5840                 const bool isNullChar = c.isNull();
5841                 if (!c.isNull()) {
5842                     toReturn += c;
5843                 }
5844                 i += matchingPrefix.length();
5845                 col += matchingPrefix.length();
5846                 previous = KTextEditor::Cursor(line, col);
5847                 decToEncCurrentOffset = decToEncCurrentOffset - (isNullChar ? 0 : 1) + matchingPrefix.length();
5848                 encToDecCurrentOffset = encToDecCurrentOffset - matchingPrefix.length() + (isNullChar ? 0 : 1);
5849                 newI += (isNullChar ? 0 : 1);
5850                 decToEncOffsetList.push_back(QPair<int, int>(newI, decToEncCurrentOffset));
5851                 encToDecOffsetList.push_back(QPair<int, int>(i, encToDecCurrentOffset));
5852                 continue;
5853             }
5854             ++col;
5855             ++i;
5856             ++newI;
5857         }
5858         ++i;
5859         ++newI;
5860     }
5861     if (previous < range.end()) {
5862         toReturn += text(KTextEditor::Range(previous, range.end()));
5863     }
5864     return toReturn;
5865 }
5866 
replaceCharactersByEncoding(const KTextEditor::Range & range)5867 void KTextEditor::DocumentPrivate::replaceCharactersByEncoding(const KTextEditor::Range &range)
5868 {
5869     KateHighlighting *highlighting = highlight();
5870     Kate::TextLine textLine;
5871 
5872     const int rangeStartLine = range.start().line();
5873     const int rangeStartColumn = range.start().column();
5874     const int rangeEndLine = range.end().line();
5875     const int rangeEndColumn = range.end().column();
5876 
5877     for (int line = range.start().line(); line <= rangeEndLine; ++line) {
5878         textLine = kateTextLine(line);
5879         int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0;
5880         int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine->length();
5881         for (int col = startColumn; col < endColumn;) {
5882             int attr = textLine->attribute(col);
5883             const QHash<QChar, QString> &reverseCharacterEncodingsHash = highlighting->getReverseCharacterEncodings(attr);
5884             auto it = reverseCharacterEncodingsHash.find(textLine->at(col));
5885             if (it != reverseCharacterEncodingsHash.end()) {
5886                 replaceText(KTextEditor::Range(line, col, line, col + 1), *it);
5887                 col += (*it).length();
5888                 continue;
5889             }
5890             ++col;
5891         }
5892     }
5893 }
5894 
5895 //
5896 // Highlighting information
5897 //
5898 
embeddedHighlightingModes() const5899 QStringList KTextEditor::DocumentPrivate::embeddedHighlightingModes() const
5900 {
5901     return highlight()->getEmbeddedHighlightingModes();
5902 }
5903 
highlightingModeAt(const KTextEditor::Cursor & position)5904 QString KTextEditor::DocumentPrivate::highlightingModeAt(const KTextEditor::Cursor &position)
5905 {
5906     return highlight()->higlightingModeForLocation(this, position);
5907 }
5908 
swapFile()5909 Kate::SwapFile *KTextEditor::DocumentPrivate::swapFile()
5910 {
5911     return m_swapfile;
5912 }
5913 
5914 /**
5915  * \return \c -1 if \c line or \c column invalid, otherwise one of
5916  * standard style attribute number
5917  */
defStyleNum(int line,int column)5918 int KTextEditor::DocumentPrivate::defStyleNum(int line, int column)
5919 {
5920     // Validate parameters to prevent out of range access
5921     if (line < 0 || line >= lines() || column < 0) {
5922         return -1;
5923     }
5924 
5925     // get highlighted line
5926     Kate::TextLine tl = kateTextLine(line);
5927 
5928     // make sure the textline is a valid pointer
5929     if (!tl) {
5930         return -1;
5931     }
5932 
5933     // either get char attribute or attribute of context still active at end of line
5934     int attribute = 0;
5935     if (column < tl->length()) {
5936         attribute = tl->attribute(column);
5937     } else if (column == tl->length()) {
5938         if (!tl->attributesList().isEmpty()) {
5939             attribute = tl->attributesList().back().attributeValue;
5940         } else {
5941             return -1;
5942         }
5943     } else {
5944         return -1;
5945     }
5946 
5947     return highlight()->defaultStyleForAttribute(attribute);
5948 }
5949 
isComment(int line,int column)5950 bool KTextEditor::DocumentPrivate::isComment(int line, int column)
5951 {
5952     const int defaultStyle = defStyleNum(line, column);
5953     return defaultStyle == KTextEditor::dsComment;
5954 }
5955 
findTouchedLine(int startLine,bool down)5956 int KTextEditor::DocumentPrivate::findTouchedLine(int startLine, bool down)
5957 {
5958     const int offset = down ? 1 : -1;
5959     const int lineCount = lines();
5960     while (startLine >= 0 && startLine < lineCount) {
5961         Kate::TextLine tl = m_buffer->plainLine(startLine);
5962         if (tl && (tl->markedAsModified() || tl->markedAsSavedOnDisk())) {
5963             return startLine;
5964         }
5965         startLine += offset;
5966     }
5967 
5968     return -1;
5969 }
5970 
setActiveTemplateHandler(KateTemplateHandler * handler)5971 void KTextEditor::DocumentPrivate::setActiveTemplateHandler(KateTemplateHandler *handler)
5972 {
5973     // delete any active template handler
5974     delete m_activeTemplateHandler.data();
5975     m_activeTemplateHandler = handler;
5976 }
5977 
5978 // BEGIN KTextEditor::MessageInterface
postMessage(KTextEditor::Message * message)5979 bool KTextEditor::DocumentPrivate::postMessage(KTextEditor::Message *message)
5980 {
5981     // no message -> cancel
5982     if (!message) {
5983         return false;
5984     }
5985 
5986     // make sure the desired view belongs to this document
5987     if (message->view() && message->view()->document() != this) {
5988         qCWarning(LOG_KTE) << "trying to post a message to a view of another document:" << message->text();
5989         return false;
5990     }
5991 
5992     message->setParent(this);
5993     message->setDocument(this);
5994 
5995     // if there are no actions, add a close action by default if widget does not auto-hide
5996     if (message->actions().count() == 0 && message->autoHide() < 0) {
5997         QAction *closeAction = new QAction(QIcon::fromTheme(QStringLiteral("window-close")), i18n("&Close"), nullptr);
5998         closeAction->setToolTip(i18n("Close message"));
5999         message->addAction(closeAction);
6000     }
6001 
6002     // reparent actions, as we want full control over when they are deleted
6003     QList<QSharedPointer<QAction>> managedMessageActions;
6004     const auto messageActions = message->actions();
6005     managedMessageActions.reserve(messageActions.size());
6006     for (QAction *action : messageActions) {
6007         action->setParent(nullptr);
6008         managedMessageActions.append(QSharedPointer<QAction>(action));
6009     }
6010     m_messageHash.insert(message, managedMessageActions);
6011 
6012     // post message to requested view, or to all views
6013     if (KTextEditor::ViewPrivate *view = qobject_cast<KTextEditor::ViewPrivate *>(message->view())) {
6014         view->postMessage(message, managedMessageActions);
6015     } else {
6016         for (auto view : std::as_const(m_views)) {
6017             view->postMessage(message, managedMessageActions);
6018         }
6019     }
6020 
6021     // also catch if the user manually calls delete message
6022     connect(message, &Message::closed, this, &DocumentPrivate::messageDestroyed);
6023 
6024     return true;
6025 }
6026 
messageDestroyed(KTextEditor::Message * message)6027 void KTextEditor::DocumentPrivate::messageDestroyed(KTextEditor::Message *message)
6028 {
6029     // KTE:Message is already in destructor
6030     Q_ASSERT(m_messageHash.contains(message));
6031     m_messageHash.remove(message);
6032 }
6033 // END KTextEditor::MessageInterface
6034