KTextEditor

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