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

source code of ktexteditor/src/document/katedocument.cpp