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

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