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