KTextEditor

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