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 invalidate 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...
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
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++) {
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
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
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
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) {
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
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
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
1634 QVarLengthArray<int, 8> rmark;
1635 QVarLengthArray<KTextEditor::Mark *, 8> list;
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
1723QList<KTextEditor::Range>
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
1748 QList<KTextEditor::Range> result;
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
1757 QList<KTextEditor::Range> result;
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 return QFileDialog::getSaveFileUrl(dialogParent(), dialogTitle, startUrlForFileDialog());
1783}
1784
1785// BEGIN KTextEditor::HighlightingInterface stuff
1787{
1788 return updateFileType(name);
1789}
1790
1795
1797{
1798 return m_fileType;
1799}
1800
1802{
1803 QStringList m;
1804
1806 m.reserve(modeList.size());
1807 for (KateFileType *type : modeList) {
1808 m << type->name;
1809 }
1810
1811 return m;
1812}
1813
1815{
1816 int mode = KateHlManager::self()->nameFind(name);
1817 if (mode == -1) {
1818 return false;
1819 }
1820 m_buffer->setHighlight(mode);
1821 return true;
1822}
1823
1825{
1826 return highlight()->name();
1827}
1828
1830{
1831 const auto modeList = KateHlManager::self()->modeList();
1832 QStringList hls;
1833 hls.reserve(modeList.size());
1834 for (const auto &hl : modeList) {
1835 hls << hl.name();
1836 }
1837 return hls;
1838}
1839
1841{
1842 return KateHlManager::self()->modeList().at(index).section();
1843}
1844
1846{
1847 return KTextEditor::EditorPrivate::self()->modeManager()->list().at(index)->section;
1848}
1849
1850void KTextEditor::DocumentPrivate::bufferHlChanged()
1851{
1852 // update all views
1853 makeAttribs(false);
1854
1855 // deactivate indenter if necessary
1856 m_indenter->checkRequiredStyle();
1857
1858 Q_EMIT highlightingModeChanged(this);
1859}
1860
1862{
1863 m_hlSetByUser = true;
1864}
1865
1867{
1868 m_bomSetByUser = true;
1869}
1870// END
1871
1872// BEGIN KTextEditor::SessionConfigInterface and KTextEditor::ParameterizedSessionConfigInterface stuff
1874{
1875 if (!flags.contains(QStringLiteral("SkipEncoding"))) {
1876 // get the encoding
1877 QString tmpenc = kconfig.readEntry("Encoding");
1878 if (!tmpenc.isEmpty() && (tmpenc != encoding())) {
1879 setEncoding(tmpenc);
1880 }
1881 }
1882
1883 if (!flags.contains(QStringLiteral("SkipUrl"))) {
1884 // restore the url
1885 QUrl url(kconfig.readEntry("URL"));
1886
1887 // open the file if url valid
1888 if (!url.isEmpty() && url.isValid()) {
1889 openUrl(url);
1890 } else {
1891 completed(); // perhaps this should be emitted at the end of this function
1892 }
1893 } else {
1894 completed(); // perhaps this should be emitted at the end of this function
1895 }
1896
1897 // restore if set by user, too!
1898 if (!flags.contains(QStringLiteral("SkipMode")) && kconfig.hasKey("Mode Set By User")) {
1899 updateFileType(kconfig.readEntry("Mode"), true /* set by user */);
1900 }
1901
1902 if (!flags.contains(QStringLiteral("SkipHighlighting"))) {
1903 // restore the hl stuff
1904 if (kconfig.hasKey("Highlighting Set By User")) {
1905 const int mode = KateHlManager::self()->nameFind(kconfig.readEntry("Highlighting"));
1906 m_hlSetByUser = true;
1907 if (mode >= 0) {
1908 // restore if set by user, too! see bug 332605, otherwise we loose the hl later again on save
1909 m_buffer->setHighlight(mode);
1910 }
1911 }
1912 }
1913
1914 // indent mode
1915 const QString userSetIndentMode = kconfig.readEntry("Indentation Mode");
1916 if (!userSetIndentMode.isEmpty()) {
1917 config()->setIndentationMode(userSetIndentMode);
1918 }
1919
1920 // Restore Bookmarks
1921 const QList<int> marks = kconfig.readEntry("Bookmarks", QList<int>());
1922 for (int i = 0; i < marks.count(); i++) {
1924 }
1925}
1926
1928{
1929 // ensure we don't amass stuff
1930 kconfig.deleteGroup();
1931
1932 if (this->url().isLocalFile()) {
1933 const QString path = this->url().toLocalFile();
1934 if (path.startsWith(QDir::tempPath())) {
1935 return; // inside tmp resource, do not save
1936 }
1937 }
1938
1939 if (!flags.contains(QStringLiteral("SkipUrl"))) {
1940 // save url
1941 kconfig.writeEntry("URL", this->url().toString());
1942 }
1943
1944 // only save encoding if it's something other than utf-8 and we didn't detect it was wrong, bug 445015
1945 if (encoding() != QLatin1String("UTF-8") && !m_buffer->brokenEncoding() && !flags.contains(QStringLiteral("SkipEncoding"))) {
1946 // save encoding
1947 kconfig.writeEntry("Encoding", encoding());
1948 }
1949
1950 // save if set by user, too!
1951 if (m_fileTypeSetByUser && !flags.contains(QStringLiteral("SkipMode"))) {
1952 kconfig.writeEntry("Mode Set By User", true);
1953 kconfig.writeEntry("Mode", m_fileType);
1954 }
1955
1956 if (m_hlSetByUser && !flags.contains(QStringLiteral("SkipHighlighting"))) {
1957 // save hl
1958 kconfig.writeEntry("Highlighting", highlight()->name());
1959
1960 // save if set by user, too! see bug 332605, otherwise we loose the hl later again on save
1961 kconfig.writeEntry("Highlighting Set By User", m_hlSetByUser);
1962 }
1963
1964 // indent mode
1965 if (m_indenterSetByUser) {
1966 kconfig.writeEntry("Indentation Mode", config()->indentationMode());
1967 }
1968
1969 // Save Bookmarks
1971 for (const auto &mark : std::as_const(m_marks)) {
1973 marks.push_back(mark->line);
1974 }
1975 }
1976
1977 if (!marks.isEmpty()) {
1978 kconfig.writeEntry("Bookmarks", marks);
1979 }
1980}
1981
1982// END KTextEditor::SessionConfigInterface and KTextEditor::ParameterizedSessionConfigInterface stuff
1983
1985{
1986 KTextEditor::Mark *m = m_marks.value(line);
1987 if (!m) {
1988 return 0;
1989 }
1990
1991 return m->type;
1992}
1993
1994void KTextEditor::DocumentPrivate::setMark(int line, uint markType)
1995{
1996 clearMark(line);
1997 addMark(line, markType);
1998}
1999
2000void KTextEditor::DocumentPrivate::clearMark(int line)
2001{
2002 if (line < 0 || line > lastLine()) {
2003 return;
2004 }
2005
2006 if (auto mark = m_marks.take(line)) {
2007 Q_EMIT markChanged(this, *mark, MarkRemoved);
2008 Q_EMIT marksChanged(this);
2009 delete mark;
2010 tagLine(line);
2011 repaintViews(true);
2012 }
2013}
2014
2015void KTextEditor::DocumentPrivate::addMark(int line, uint markType)
2016{
2017 KTextEditor::Mark *mark;
2018
2019 if (line < 0 || line > lastLine()) {
2020 return;
2021 }
2022
2023 if (markType == 0) {
2024 return;
2025 }
2026
2027 if ((mark = m_marks.value(line))) {
2028 // Remove bits already set
2029 markType &= ~mark->type;
2030
2031 if (markType == 0) {
2032 return;
2033 }
2034
2035 // Add bits
2036 mark->type |= markType;
2037 } else {
2038 mark = new KTextEditor::Mark;
2039 mark->line = line;
2040 mark->type = markType;
2041 m_marks.insert(line, mark);
2042 }
2043
2044 // Emit with a mark having only the types added.
2045 KTextEditor::Mark temp;
2046 temp.line = line;
2047 temp.type = markType;
2048 Q_EMIT markChanged(this, temp, MarkAdded);
2049
2050 Q_EMIT marksChanged(this);
2051 tagLine(line);
2052 repaintViews(true);
2053}
2054
2055void KTextEditor::DocumentPrivate::removeMark(int line, uint markType)
2056{
2057 if (line < 0 || line > lastLine()) {
2058 return;
2059 }
2060
2061 auto it = m_marks.find(line);
2062 if (it == m_marks.end()) {
2063 return;
2064 }
2065 KTextEditor::Mark *mark = it.value();
2066
2067 // Remove bits not set
2068 markType &= mark->type;
2069
2070 if (markType == 0) {
2071 return;
2072 }
2073
2074 // Subtract bits
2075 mark->type &= ~markType;
2076
2077 // Emit with a mark having only the types removed.
2078 KTextEditor::Mark temp;
2079 temp.line = line;
2080 temp.type = markType;
2081 Q_EMIT markChanged(this, temp, MarkRemoved);
2082
2083 if (mark->type == 0) {
2084 m_marks.erase(it);
2085 delete mark;
2086 }
2087
2088 Q_EMIT marksChanged(this);
2089 tagLine(line);
2090 repaintViews(true);
2091}
2092
2097
2098void KTextEditor::DocumentPrivate::requestMarkTooltip(int line, QPoint position)
2099{
2100 KTextEditor::Mark *mark = m_marks.value(line);
2101 if (!mark) {
2102 return;
2103 }
2104
2105 bool handled = false;
2106 Q_EMIT markToolTipRequested(this, *mark, position, handled);
2107}
2108
2110{
2111 bool handled = false;
2112 KTextEditor::Mark *mark = m_marks.value(line);
2113 if (!mark) {
2114 Q_EMIT markClicked(this, KTextEditor::Mark{.line = line, .type = 0}, handled);
2115 } else {
2116 Q_EMIT markClicked(this, *mark, handled);
2117 }
2118
2119 return handled;
2120}
2121
2123{
2124 bool handled = false;
2125 KTextEditor::Mark *mark = m_marks.value(line);
2126 if (!mark) {
2127 Q_EMIT markContextMenuRequested(this, KTextEditor::Mark{.line = line, .type = 0}, position, handled);
2128 } else {
2129 Q_EMIT markContextMenuRequested(this, *mark, position, handled);
2130 }
2131
2132 return handled;
2133}
2134
2136{
2137 /**
2138 * work on a copy as deletions below might trigger the use
2139 * of m_marks
2140 */
2141 const QHash<int, KTextEditor::Mark *> marksCopy = m_marks;
2142 m_marks.clear();
2143
2144 for (const auto &m : marksCopy) {
2145 Q_EMIT markChanged(this, *m, MarkRemoved);
2146 tagLine(m->line);
2147 delete m;
2148 }
2149
2150 Q_EMIT marksChanged(this);
2151 repaintViews(true);
2152}
2153
2154void KTextEditor::DocumentPrivate::setMarkDescription(Document::MarkTypes type, const QString &description)
2155{
2156 m_markDescriptions.insert(type, description);
2157}
2158
2159QColor KTextEditor::DocumentPrivate::markColor(Document::MarkTypes type) const
2160{
2161 uint reserved = (1U << KTextEditor::Document::reservedMarkersCount()) - 1;
2162 if ((uint)type >= (uint)markType01 && (uint)type <= reserved) {
2163 return KateRendererConfig::global()->lineMarkerColor(type);
2164 } else {
2165 return QColor();
2166 }
2167}
2168
2170{
2171 return m_markDescriptions.value(type, QString());
2172}
2173
2174void KTextEditor::DocumentPrivate::setEditableMarks(uint markMask)
2175{
2176 m_editableMarks = markMask;
2177}
2178
2180{
2181 return m_editableMarks;
2182}
2183// END
2184
2185void KTextEditor::DocumentPrivate::setMarkIcon(Document::MarkTypes markType, const QIcon &icon)
2186{
2187 m_markIcons.insert(markType, icon);
2188}
2189
2191{
2192 return m_markIcons.value(markType, QIcon());
2193}
2194
2195// BEGIN KTextEditor::PrintInterface stuff
2196bool KTextEditor::DocumentPrivate::print()
2197{
2198 return KatePrinter::print(this);
2199}
2200
2201void KTextEditor::DocumentPrivate::printPreview()
2202{
2203 KatePrinter::printPreview(this);
2204}
2205// END KTextEditor::PrintInterface stuff
2206
2207// BEGIN KTextEditor::DocumentInfoInterface (### unfinished)
2209{
2210 if (!m_modOnHd && url().isLocalFile()) {
2211 // for unmodified files that reside directly on disk, we don't need to
2212 // create a temporary buffer - we can just look at the file directly
2213 return QMimeDatabase().mimeTypeForFile(url().toLocalFile()).name();
2214 }
2215 // collect first 4k of text
2216 // only heuristic
2217 QByteArray buf;
2218 for (int i = 0; (i < lines()) && (buf.size() <= 4096); ++i) {
2219 buf.append(line(i).toUtf8());
2220 buf.append('\n');
2221 }
2222
2223 // use path of url, too, if set
2224 if (!url().path().isEmpty()) {
2225 return QMimeDatabase().mimeTypeForFileNameAndData(url().path(), buf).name();
2226 }
2227
2228 // else only use the content
2229 return QMimeDatabase().mimeTypeForData(buf).name();
2230}
2231// END KTextEditor::DocumentInfoInterface
2232
2233// BEGIN: error
2234void KTextEditor::DocumentPrivate::showAndSetOpeningErrorAccess()
2235{
2237 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.",
2238 this->url().toDisplayString(QUrl::PreferLocalFile)),
2240 message->setWordWrap(true);
2241 QAction *tryAgainAction = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")),
2242 i18nc("translators: you can also translate 'Try Again' with 'Reload'", "Try Again"),
2243 nullptr);
2245
2246 QAction *closeAction = new QAction(QIcon::fromTheme(QStringLiteral("window-close")), i18n("&Close"), nullptr);
2247 closeAction->setToolTip(i18nc("Close the message being displayed", "Close message"));
2248
2249 // add try again and close actions
2250 message->addAction(tryAgainAction);
2251 message->addAction(closeAction);
2252
2253 // finally post message
2254 postMessage(message);
2255
2256 // remember error
2257 m_openingError = true;
2258}
2259// END: error
2260
2261void KTextEditor::DocumentPrivate::openWithLineLengthLimitOverride()
2262{
2263 // raise line length limit to the next power of 2
2264 const int longestLine = m_buffer->longestLineLoaded();
2265 int newLimit = pow(2, ceil(log2(longestLine)));
2266 if (newLimit <= longestLine) {
2267 newLimit *= 2;
2268 }
2269
2270 // do the raise
2271 config()->setLineLengthLimit(newLimit);
2272
2273 // just reload
2274 m_buffer->clear();
2275 openFile();
2276 if (!m_openingError) {
2277 setReadWrite(true);
2278 m_readWriteStateBeforeLoading = true;
2279 }
2280}
2281
2283{
2284 return config()->lineLengthLimit();
2285}
2286
2287// BEGIN KParts::ReadWrite stuff
2289{
2290 // we are about to invalidate all cursors/ranges/.. => m_buffer->openFile will do so
2292
2293 // no open errors until now...
2294 m_openingError = false;
2295
2296 // add new m_file to dirwatch
2297 activateDirWatch();
2298
2299 // remember current encoding
2300 QString currentEncoding = encoding();
2301
2302 //
2303 // mime type magic to get encoding right
2304 //
2305 QString mimeType = arguments().mimeType();
2306 int pos = mimeType.indexOf(QLatin1Char(';'));
2307 if (pos != -1 && !(m_reloading && m_userSetEncodingForNextReload)) {
2308 setEncoding(mimeType.mid(pos + 1));
2309 }
2310
2311 // update file type, we do this here PRE-LOAD, therefore pass file name for reading from
2312 updateFileType(KTextEditor::EditorPrivate::self()->modeManager()->fileType(this, localFilePath()));
2313
2314 // read dir config (if possible and wanted)
2315 // do this PRE-LOAD to get encoding info!
2316 readDirConfig();
2317
2318 // perhaps we need to re-set again the user encoding
2319 if (m_reloading && m_userSetEncodingForNextReload && (currentEncoding != encoding())) {
2320 setEncoding(currentEncoding);
2321 }
2322
2323 bool success = m_buffer->openFile(localFilePath(), (m_reloading && m_userSetEncodingForNextReload));
2324
2325 //
2326 // yeah, success
2327 // read variables
2328 //
2329 if (success) {
2330 readVariables();
2331 }
2332
2333 //
2334 // update views
2335 //
2336 for (auto view : std::as_const(m_views)) {
2337 // This is needed here because inserting the text moves the view's start position (it is a MovingCursor)
2339 static_cast<ViewPrivate *>(view)->updateView(true);
2340 }
2341
2342 // Inform that the text has changed (required as we're not inside the usual editStart/End stuff)
2343 Q_EMIT textChanged(this);
2344 Q_EMIT loaded(this);
2345
2346 //
2347 // to houston, we are not modified
2348 //
2349 if (m_modOnHd) {
2350 m_modOnHd = false;
2351 m_modOnHdReason = OnDiskUnmodified;
2352 m_prevModOnHdReason = OnDiskUnmodified;
2353 Q_EMIT modifiedOnDisk(this, m_modOnHd, m_modOnHdReason);
2354 }
2355
2356 // Now that we have some text, try to auto detect indent if enabled
2357 // skip this if for this document already settings were done, either by the user or .e.g. modelines/.kateconfig files.
2358 if (!isEmpty() && config()->autoDetectIndent() && !config()->isSet(KateDocumentConfig::IndentationWidth)
2359 && !config()->isSet(KateDocumentConfig::ReplaceTabsWithSpaces)) {
2360 KateIndentDetecter detecter(this);
2361 auto result = detecter.detect(config()->indentationWidth(), config()->replaceTabsDyn());
2362 config()->setIndentationWidth(result.indentWidth);
2363 config()->setReplaceTabsDyn(result.indentUsingSpaces);
2364 }
2365
2366 //
2367 // display errors
2368 //
2369 if (!success) {
2370 showAndSetOpeningErrorAccess();
2371 }
2372
2373 // warn: broken encoding
2374 if (m_buffer->brokenEncoding()) {
2375 // this file can't be saved again without killing it
2376 setReadWrite(false);
2377 m_readWriteStateBeforeLoading = false;
2379 i18n("The file %1 was opened with %2 encoding but contained invalid characters.<br />"
2380 "It is set to read-only mode, as saving might destroy its content.<br />"
2381 "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.",
2382 this->url().toDisplayString(QUrl::PreferLocalFile),
2383 m_buffer->textCodec()),
2385 message->setWordWrap(true);
2386 postMessage(message);
2387
2388 // remember error
2389 m_openingError = true;
2390 }
2391
2392 // warn: too long lines
2393 if (m_buffer->tooLongLinesWrapped()) {
2394 // this file can't be saved again without modifications
2395 setReadWrite(false);
2396 m_readWriteStateBeforeLoading = false;
2398 new KTextEditor::Message(i18n("The file %1 was opened and contained lines longer than the configured Line Length Limit (%2 characters).<br />"
2399 "The longest of those lines was %3 characters long<br/>"
2400 "Those lines were wrapped and the document is set to read-only mode, as saving will modify its content.",
2401 this->url().toDisplayString(QUrl::PreferLocalFile),
2403 m_buffer->longestLineLoaded()),
2405 QAction *increaseAndReload = new QAction(i18n("Temporarily raise limit and reload file"), message);
2406 connect(increaseAndReload, &QAction::triggered, this, &KTextEditor::DocumentPrivate::openWithLineLengthLimitOverride);
2407 message->addAction(increaseAndReload, true);
2408 message->addAction(new QAction(i18n("Close"), message), true);
2409 message->setWordWrap(true);
2410 postMessage(message);
2411
2412 // remember error
2413 m_openingError = true;
2414 }
2415
2416 //
2417 // return the success
2418 //
2419 return success;
2420}
2421
2423{
2424 // delete pending mod-on-hd message if applicable.
2425 delete m_modOnHdHandler;
2426
2427 // some warnings, if file was changed by the outside!
2428 if (!url().isEmpty()) {
2429 if (m_fileChangedDialogsActivated && m_modOnHd) {
2430 QString str = reasonedMOHString() + QLatin1String("\n\n");
2431
2432 if (!isModified()) {
2434 dialogParent(),
2435 str + i18n("Do you really want to save this unmodified file? You could overwrite changed data in the file on disk."),
2436 i18n("Trying to Save Unmodified File"),
2437 KGuiItem(i18n("Save Nevertheless")))
2439 return false;
2440 }
2441 } else {
2443 dialogParent(),
2444 str
2445 + i18n(
2446 "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."),
2447 i18n("Possible Data Loss"),
2448 KGuiItem(i18n("Save Nevertheless")))
2450 return false;
2451 }
2452 }
2453 }
2454 }
2455
2456 //
2457 // can we encode it if we want to save it ?
2458 //
2459 if (!m_buffer->canEncode()
2460 && (KMessageBox::warningContinueCancel(dialogParent(),
2461 i18n("The selected encoding cannot encode every Unicode character in this document. Do you really want to save "
2462 "it? There could be some data lost."),
2463 i18n("Possible Data Loss"),
2464 KGuiItem(i18n("Save Nevertheless")))
2466 return false;
2467 }
2468
2469 // create a backup file or abort if that fails!
2470 // if no backup file wanted, this routine will just return true
2471 if (!createBackupFile()) {
2472 return false;
2473 }
2474
2475 // update file type, pass no file path, read file type content from this document
2476 QString oldPath = m_dirWatchFile;
2477
2478 // only update file type if path has changed so that variables are not overridden on normal save
2479 if (oldPath != localFilePath()) {
2480 updateFileType(KTextEditor::EditorPrivate::self()->modeManager()->fileType(this, QString()));
2481
2482 if (url().isLocalFile()) {
2483 // if file is local then read dir config for new path
2484 readDirConfig();
2485 }
2486 }
2487
2488 // read our vars
2489 const bool variablesWereRead = readVariables();
2490
2491 // If variables were read, that means we must have updated view and render config
2492 // which would update the full view and we don't need to do any repainting. Otherwise
2493 // loop over all views and update the views if the view has modified lines in the visible
2494 // range, this should mark the line 'green' in the icon border
2495 if (!variablesWereRead) {
2496 for (auto *view : std::as_const(m_views)) {
2497 auto v = static_cast<ViewPrivate *>(view);
2498 if (v->isVisible()) {
2499 const auto range = v->visibleRange();
2500
2501 bool repaint = false;
2502 for (int i = range.start().line(); i <= range.end().line(); ++i) {
2503 if (isLineModified(i)) {
2504 repaint = true;
2505 v->tagLine({i, 0});
2506 }
2507 }
2508
2509 if (repaint) {
2510 v->updateView(true);
2511 }
2512 }
2513 }
2514 }
2515
2516 // remove file from dirwatch
2517 deactivateDirWatch();
2518
2519 // remove all trailing spaces in the document and potential add a new line (as edit actions)
2520 // NOTE: we need this as edit actions, since otherwise the edit actions
2521 // in the swap file recovery may happen at invalid cursor positions
2522 removeTrailingSpacesAndAddNewLineAtEof();
2523
2524 //
2525 // try to save
2526 //
2527 if (!m_buffer->saveFile(localFilePath())) {
2528 // add m_file again to dirwatch
2529 activateDirWatch(oldPath);
2530 KMessageBox::error(dialogParent(),
2531 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 "
2532 "that enough disk space is available.\nThe original file may be lost or damaged. "
2533 "Don't quit the application until the file is successfully written.",
2534 this->url().toDisplayString(QUrl::PreferLocalFile)));
2535 return false;
2536 }
2537
2538 // update the checksum
2539 createDigest();
2540
2541 // add m_file again to dirwatch
2542 activateDirWatch();
2543
2544 //
2545 // we are not modified
2546 //
2547 if (m_modOnHd) {
2548 m_modOnHd = false;
2549 m_modOnHdReason = OnDiskUnmodified;
2550 m_prevModOnHdReason = OnDiskUnmodified;
2551 Q_EMIT modifiedOnDisk(this, m_modOnHd, m_modOnHdReason);
2552 }
2553
2554 // (dominik) mark last undo group as not mergeable, otherwise the next
2555 // edit action might be merged and undo will never stop at the saved state
2556 m_undoManager->undoSafePoint();
2557 m_undoManager->updateLineModifications();
2558
2559 //
2560 // return success
2561 //
2562 return true;
2563}
2564
2565bool KTextEditor::DocumentPrivate::createBackupFile()
2566{
2567 // backup for local or remote files wanted?
2568 const bool backupLocalFiles = config()->backupOnSaveLocal();
2569 const bool backupRemoteFiles = config()->backupOnSaveRemote();
2570
2571 // early out, before mount check: backup wanted at all?
2572 // => if not, all fine, just return
2573 if (!backupLocalFiles && !backupRemoteFiles) {
2574 return true;
2575 }
2576
2577 // decide if we need backup based on locality
2578 // skip that, if we always want backups, as currentMountPoints is not that fast
2579 QUrl u(url());
2580 bool needBackup = backupLocalFiles && backupRemoteFiles;
2581 if (!needBackup) {
2582 bool slowOrRemoteFile = !u.isLocalFile();
2583 if (!slowOrRemoteFile) {
2584 // could be a mounted remote filesystem (e.g. nfs, sshfs, cifs)
2585 // we have the early out above to skip this, if we want no backup, which is the default
2586 KMountPoint::Ptr mountPoint = KMountPoint::currentMountPoints().findByDevice(u.toLocalFile());
2587 slowOrRemoteFile = (mountPoint && mountPoint->probablySlow());
2588 }
2589 needBackup = (!slowOrRemoteFile && backupLocalFiles) || (slowOrRemoteFile && backupRemoteFiles);
2590 }
2591
2592 // no backup needed? be done
2593 if (!needBackup) {
2594 return true;
2595 }
2596
2597 // else: try to backup
2598 const auto backupPrefix = KTextEditor::EditorPrivate::self()->variableExpansionManager()->expandText(config()->backupPrefix(), nullptr);
2599 const auto backupSuffix = KTextEditor::EditorPrivate::self()->variableExpansionManager()->expandText(config()->backupSuffix(), nullptr);
2600 if (backupPrefix.isEmpty() && backupSuffix.isEmpty()) {
2601 // no sane backup possible
2602 return true;
2603 }
2604
2605 if (backupPrefix.contains(QDir::separator())) {
2606 // replace complete path, as prefix is a path!
2607 u.setPath(backupPrefix + u.fileName() + backupSuffix);
2608 } else {
2609 // replace filename in url
2610 const QString fileName = u.fileName();
2611 u = u.adjusted(QUrl::RemoveFilename);
2612 u.setPath(u.path() + backupPrefix + fileName + backupSuffix);
2613 }
2614
2615 qCDebug(LOG_KTE) << "backup src file name: " << url();
2616 qCDebug(LOG_KTE) << "backup dst file name: " << u;
2617
2618 // handle the backup...
2619 bool backupSuccess = false;
2620
2621 // local file mode, no kio
2622 if (u.isLocalFile()) {
2623 if (QFile::exists(url().toLocalFile())) {
2624 // first: check if backupFile is already there, if true, unlink it
2625 QFile backupFile(u.toLocalFile());
2626 if (backupFile.exists()) {
2627 backupFile.remove();
2628 }
2629
2630 backupSuccess = QFile::copy(url().toLocalFile(), u.toLocalFile());
2631 } else {
2632 backupSuccess = true;
2633 }
2634 } else { // remote file mode, kio
2635 // get the right permissions, start with safe default
2636 KIO::StatJob *statJob = KIO::stat(url(), KIO::StatJob::SourceSide, KIO::StatBasic);
2638 if (statJob->exec()) {
2639 // do a evil copy which will overwrite target if possible
2640 KFileItem item(statJob->statResult(), url());
2641 KIO::FileCopyJob *job = KIO::file_copy(url(), u, item.permissions(), KIO::Overwrite);
2643 backupSuccess = job->exec();
2644 } else {
2645 backupSuccess = true;
2646 }
2647 }
2648
2649 // backup has failed, ask user how to proceed
2650 if (!backupSuccess
2651 && (KMessageBox::warningContinueCancel(dialogParent(),
2652 i18n("For file %1 no backup copy could be created before saving."
2653 " If an error occurs while saving, you might lose the data of this file."
2654 " A reason could be that the media you write to is full or the directory of the file is read-only for you.",
2655 url().toDisplayString(QUrl::PreferLocalFile)),
2656 i18n("Failed to create backup copy."),
2657 KGuiItem(i18n("Try to Save Nevertheless")),
2659 QStringLiteral("Backup Failed Warning"))
2661 return false;
2662 }
2663
2664 return true;
2665}
2666
2667void KTextEditor::DocumentPrivate::readDirConfig(KTextEditor::ViewPrivate *v)
2668{
2669 if (!url().isLocalFile() || KNetworkMounts::self()->isOptionEnabledForPath(url().toLocalFile(), KNetworkMounts::MediumSideEffectsOptimizations)) {
2670 return;
2671 }
2672
2673 // first search .kateconfig upwards
2674 // with recursion guard
2675 QSet<QString> seenDirectories;
2676 QDir dir(QFileInfo(localFilePath()).absolutePath());
2677 while (!seenDirectories.contains(dir.absolutePath())) {
2678 // fill recursion guard
2679 seenDirectories.insert(dir.absolutePath());
2680
2681 // try to open config file in this dir
2682 QFile f(dir.absolutePath() + QLatin1String("/.kateconfig"));
2683 if (f.open(QIODevice::ReadOnly)) {
2684 QTextStream stream(&f);
2685
2686 uint linesRead = 0;
2687 QString line = stream.readLine();
2688 while ((linesRead < 32) && !line.isNull()) {
2689 readVariableLine(line, v);
2690
2691 line = stream.readLine();
2692
2693 linesRead++;
2694 }
2695
2696 return;
2697 }
2698
2699 // else: cd up, if possible or abort
2700 if (!dir.cdUp()) {
2701 break;
2702 }
2703 }
2704
2705#if EDITORCONFIG_FOUND
2706 // if v is set, we are only loading config for the view variables
2707 if (!v && config()->value(KateDocumentConfig::UseEditorConfig).toBool()) {
2708 // if there wasn’t any .kateconfig file and KTextEditor was compiled with
2709 // EditorConfig support, try to load document config from a .editorconfig
2710 // file, if such is provided
2711 EditorConfig editorConfig(this);
2712 editorConfig.parse();
2713 }
2714#endif
2715}
2716
2717void KTextEditor::DocumentPrivate::activateDirWatch(const QString &useFileName)
2718{
2719 QString fileToUse = useFileName;
2720 if (fileToUse.isEmpty()) {
2721 fileToUse = localFilePath();
2722 }
2723
2724 if (KNetworkMounts::self()->isOptionEnabledForPath(fileToUse, KNetworkMounts::KDirWatchDontAddWatches)) {
2725 return;
2726 }
2727
2728 QFileInfo fileInfo = QFileInfo(fileToUse);
2729 if (fileInfo.isSymLink()) {
2730 // Monitor the actual data and not the symlink
2731 fileToUse = fileInfo.canonicalFilePath();
2732 }
2733
2734 // same file as we are monitoring, return
2735 if (fileToUse == m_dirWatchFile) {
2736 return;
2737 }
2738
2739 // remove the old watched file
2740 deactivateDirWatch();
2741
2742 // add new file if needed
2743 if (url().isLocalFile() && !fileToUse.isEmpty()) {
2745 m_dirWatchFile = fileToUse;
2746 }
2747}
2748
2749void KTextEditor::DocumentPrivate::deactivateDirWatch()
2750{
2751 if (!m_dirWatchFile.isEmpty()) {
2753 }
2754
2755 m_dirWatchFile.clear();
2756}
2757
2758bool KTextEditor::DocumentPrivate::openUrl(const QUrl &url)
2759{
2760 if (!m_reloading) {
2761 // Reset filetype when opening url
2762 m_fileTypeSetByUser = false;
2763 }
2764 bool res = KTextEditor::Document::openUrl(url);
2765 updateDocName();
2766 return res;
2767}
2768
2769bool KTextEditor::DocumentPrivate::closeUrl()
2770{
2771 //
2772 // file mod on hd
2773 //
2774 if (!m_reloading && !url().isEmpty()) {
2775 if (m_fileChangedDialogsActivated && m_modOnHd) {
2776 // make sure to not forget pending mod-on-hd handler
2777 delete m_modOnHdHandler;
2778
2779 QWidget *parentWidget(dialogParent());
2780 if (!(KMessageBox::warningContinueCancel(parentWidget,
2781 reasonedMOHString() + QLatin1String("\n\n")
2782 + i18n("Do you really want to continue to close this file? Data loss may occur."),
2783 i18n("Possible Data Loss"),
2784 KGuiItem(i18n("Close Nevertheless")),
2786 QStringLiteral("kate_close_modonhd_%1").arg(m_modOnHdReason))
2788 // reset reloading
2789 m_reloading = false;
2790 return false;
2791 }
2792 }
2793 }
2794
2795 //
2796 // first call the normal kparts implementation
2797 //
2799 // reset reloading
2800 m_reloading = false;
2801 return false;
2802 }
2803
2804 // Tell the world that we're about to go ahead with the close
2805 if (!m_reloading) {
2806 Q_EMIT aboutToClose(this);
2807 }
2808
2809 // delete all KTE::Messages
2810 if (!m_messageHash.isEmpty()) {
2811 const auto keys = m_messageHash.keys();
2812 for (KTextEditor::Message *message : keys) {
2813 delete message;
2814 }
2815 }
2816
2817 // we are about to invalidate all cursors/ranges/.. => m_buffer->clear will do so
2818 Q_EMIT aboutToInvalidateMovingInterfaceContent(this);
2819
2820 // remove file from dirwatch
2821 deactivateDirWatch();
2822
2823 // clear the local file path
2824 setLocalFilePath(QString());
2825
2826 // we are not modified
2827 if (m_modOnHd) {
2828 m_modOnHd = false;
2829 m_modOnHdReason = OnDiskUnmodified;
2830 m_prevModOnHdReason = OnDiskUnmodified;
2831 Q_EMIT modifiedOnDisk(this, m_modOnHd, m_modOnHdReason);
2832 }
2833
2834 // remove all marks
2835 clearMarks();
2836
2837 // clear the buffer
2838 m_buffer->clear();
2839
2840 // clear undo/redo history
2841 m_undoManager->clearUndo();
2842 m_undoManager->clearRedo();
2843
2844 // no, we are no longer modified
2845 setModified(false);
2846
2847 // we have no longer any hl
2848 m_buffer->setHighlight(0);
2849
2850 // update all our views
2851 for (auto view : std::as_const(m_views)) {
2852 static_cast<ViewPrivate *>(view)->clearSelection(); // fix bug #118588
2853 static_cast<ViewPrivate *>(view)->clear();
2854 }
2855
2856 // purge swap file
2857 if (m_swapfile) {
2858 m_swapfile->fileClosed();
2859 }
2860
2861 // success
2862 return true;
2863}
2864
2866{
2867 return m_swapfile && m_swapfile->shouldRecover();
2868}
2869
2871{
2873 m_swapfile->recover();
2874 }
2875}
2876
2878{
2880 m_swapfile->discard();
2881 }
2882}
2883
2884void KTextEditor::DocumentPrivate::setReadWrite(bool rw)
2885{
2886 if (isReadWrite() == rw) {
2887 return;
2888 }
2889
2891
2892 for (auto v : std::as_const(m_views)) {
2893 auto view = static_cast<ViewPrivate *>(v);
2894 view->slotUpdateUndo();
2895 view->slotReadWriteChanged();
2896 }
2897
2898 Q_EMIT readWriteChanged(this);
2899}
2900
2902{
2903 if (isModified() != m) {
2905
2906 for (auto view : std::as_const(m_views)) {
2907 static_cast<ViewPrivate *>(view)->slotUpdateUndo();
2908 }
2909
2910 Q_EMIT modifiedChanged(this);
2911 }
2912
2913 m_undoManager->setModified(m);
2914}
2915// END
2916
2917// BEGIN Kate specific stuff ;)
2918
2919void KTextEditor::DocumentPrivate::makeAttribs(bool needInvalidate)
2920{
2921 for (auto view : std::as_const(m_views)) {
2922 static_cast<ViewPrivate *>(view)->renderer()->updateAttributes();
2923 }
2924
2925 if (needInvalidate) {
2926 m_buffer->invalidateHighlighting();
2927 }
2928
2929 for (auto v : std::as_const(m_views)) {
2930 auto view = static_cast<ViewPrivate *>(v);
2931 view->tagAll();
2932 view->updateView(true);
2933 }
2934}
2935
2936// the attributes of a hl have changed, update
2937void KTextEditor::DocumentPrivate::internalHlChanged()
2938{
2939 makeAttribs();
2940}
2941
2942void KTextEditor::DocumentPrivate::addView(KTextEditor::View *view)
2943{
2944 Q_ASSERT(!m_views.contains(view));
2945 m_views.append(view);
2946 auto *v = static_cast<KTextEditor::ViewPrivate *>(view);
2947
2948 // apply the view & renderer vars from the file type
2949 if (!m_fileType.isEmpty()) {
2950 readVariableLine(KTextEditor::EditorPrivate::self()->modeManager()->fileType(m_fileType).varLine, v);
2951 }
2952
2953 readDirConfig(v);
2954
2955 // apply the view & renderer vars from the file
2956 readVariables(v);
2957
2958 setActiveView(view);
2959}
2960
2962{
2963 Q_ASSERT(m_views.contains(view));
2964 m_views.removeAll(view);
2965
2966 if (activeView() == view) {
2967 setActiveView(nullptr);
2968 }
2969}
2970
2971void KTextEditor::DocumentPrivate::setActiveView(KTextEditor::View *view)
2972{
2973 if (m_activeView == view) {
2974 return;
2975 }
2976
2977 m_activeView = static_cast<KTextEditor::ViewPrivate *>(view);
2978}
2979
2980bool KTextEditor::DocumentPrivate::ownedView(KTextEditor::ViewPrivate *view)
2981{
2982 // do we own the given view?
2983 return (m_views.contains(view));
2984}
2985
2986int KTextEditor::DocumentPrivate::toVirtualColumn(int line, int column) const
2987{
2988 Kate::TextLine textLine = m_buffer->plainLine(line);
2989 return textLine.toVirtualColumn(column, config()->tabWidth());
2990}
2991
2992int KTextEditor::DocumentPrivate::toVirtualColumn(const KTextEditor::Cursor cursor) const
2993{
2994 return toVirtualColumn(cursor.line(), cursor.column());
2995}
2996
2997int KTextEditor::DocumentPrivate::fromVirtualColumn(int line, int column) const
2998{
2999 Kate::TextLine textLine = m_buffer->plainLine(line);
3000 return textLine.fromVirtualColumn(column, config()->tabWidth());
3001}
3002
3003int KTextEditor::DocumentPrivate::fromVirtualColumn(const KTextEditor::Cursor cursor) const
3004{
3005 return fromVirtualColumn(cursor.line(), cursor.column());
3006}
3007
3008bool KTextEditor::DocumentPrivate::skipAutoBrace(QChar closingBracket, KTextEditor::Cursor pos)
3009{
3010 // auto bracket handling for newly inserted text
3011 // we inserted a bracket?
3012 // => add the matching closing one to the view + input chars
3013 // try to preserve the cursor position
3014 bool skipAutobrace = closingBracket == QLatin1Char('\'');
3015 if (highlight() && skipAutobrace) {
3016 // skip adding ' in spellchecked areas, because those are text
3017 skipAutobrace = highlight()->spellCheckingRequiredForLocation(this, pos - Cursor{0, 1});
3018 }
3019
3020 if (!skipAutobrace && (closingBracket == QLatin1Char('\''))) {
3021 // skip auto quotes when these looks already balanced, bug 405089
3022 Kate::TextLine textLine = m_buffer->plainLine(pos.line());
3023 // RegEx match quote, but not escaped quote, thanks to https://stackoverflow.com/a/11819111
3024 static const QRegularExpression re(QStringLiteral("(?<!\\\\)(?:\\\\\\\\)*\\\'"));
3025 const int count = textLine.text().left(pos.column()).count(re);
3026 skipAutobrace = (count % 2 == 0) ? true : false;
3027 }
3028 if (!skipAutobrace && (closingBracket == QLatin1Char('\"'))) {
3029 // ...same trick for double quotes
3030 Kate::TextLine textLine = m_buffer->plainLine(pos.line());
3031 static const QRegularExpression re(QStringLiteral("(?<!\\\\)(?:\\\\\\\\)*\\\""));
3032 const int count = textLine.text().left(pos.column()).count(re);
3033 skipAutobrace = (count % 2 == 0) ? true : false;
3034 }
3035 return skipAutobrace;
3036}
3037
3038void KTextEditor::DocumentPrivate::typeChars(KTextEditor::ViewPrivate *view, QString chars)
3039{
3040 // nop for empty chars
3041 if (chars.isEmpty()) {
3042 return;
3043 }
3044
3045 // auto bracket handling
3046 QChar closingBracket;
3047 if (view->config()->autoBrackets()) {
3048 // Check if entered closing bracket is already balanced
3049 const QChar typedChar = chars.at(0);
3050 const QChar openBracket = matchingStartBracket(typedChar);
3051 if (!openBracket.isNull()) {
3052 KTextEditor::Cursor curPos = view->cursorPosition();
3053 if ((characterAt(curPos) == typedChar) && findMatchingBracket(curPos, 123 /*Which value may best?*/).isValid()) {
3054 // Do nothing
3055 view->cursorRight();
3056 return;
3057 }
3058 }
3059
3060 // for newly inserted text: remember if we should auto add some bracket
3061 if (chars.size() == 1) {
3062 // we inserted a bracket? => remember the matching closing one
3063 closingBracket = matchingEndBracket(typedChar);
3064
3065 // closing bracket for the autobracket we inserted earlier?
3066 if (m_currentAutobraceClosingChar == typedChar && m_currentAutobraceRange) {
3067 // do nothing
3068 m_currentAutobraceRange.reset(nullptr);
3069 view->cursorRight();
3070 return;
3071 }
3072 }
3073 }
3074
3075 // Treat some char also as "auto bracket" only when we have a selection
3076 if (view->selection() && closingBracket.isNull() && view->config()->encloseSelectionInChars()) {
3077 const QChar typedChar = chars.at(0);
3078 if (view->config()->charsToEncloseSelection().contains(typedChar)) {
3079 // The unconditional mirroring cause no harm, but allows funny brackets
3080 closingBracket = typedChar.mirroredChar();
3081 }
3082 }
3083
3084 editStart();
3085
3086 // special handling if we want to add auto brackets to a selection
3087 if (view->selection() && !closingBracket.isNull()) {
3088 std::unique_ptr<KTextEditor::MovingRange> selectionRange(newMovingRange(view->selectionRange()));
3089 const int startLine = qMax(0, selectionRange->start().line());
3090 const int endLine = qMin(selectionRange->end().line(), lastLine());
3091 const bool blockMode = view->blockSelection() && (startLine != endLine);
3092 if (blockMode) {
3093 if (selectionRange->start().column() > selectionRange->end().column()) {
3094 // Selection was done from right->left, requires special setting to ensure the new
3095 // added brackets will not be part of the selection
3096 selectionRange->setInsertBehaviors(MovingRange::ExpandLeft | MovingRange::ExpandRight);
3097 }
3098 // Add brackets to each line of the block
3099 const int startColumn = qMin(selectionRange->start().column(), selectionRange->end().column());
3100 const int endColumn = qMax(selectionRange->start().column(), selectionRange->end().column());
3101 const KTextEditor::Range workingRange(startLine, startColumn, endLine, endColumn);
3102 for (int line = startLine; line <= endLine; ++line) {
3103 const KTextEditor::Range r(rangeOnLine(workingRange, line));
3104 insertText(r.end(), QString(closingBracket));
3105 view->slotTextInserted(view, r.end(), QString(closingBracket));
3106 insertText(r.start(), chars);
3107 view->slotTextInserted(view, r.start(), chars);
3108 }
3109
3110 } else {
3111 for (const auto &cursor : view->secondaryCursors()) {
3112 if (!cursor.range) {
3113 continue;
3114 }
3115 const auto &currSelectionRange = cursor.range;
3116 auto expandBehaviour = currSelectionRange->insertBehaviors();
3117 currSelectionRange->setInsertBehaviors(KTextEditor::MovingRange::DoNotExpand);
3118 insertText(currSelectionRange->end(), QString(closingBracket));
3119 insertText(currSelectionRange->start(), chars);
3120 currSelectionRange->setInsertBehaviors(expandBehaviour);
3121 cursor.pos->setPosition(currSelectionRange->end());
3122 auto mutableCursor = const_cast<KTextEditor::ViewPrivate::SecondaryCursor *>(&cursor);
3123 mutableCursor->anchor = currSelectionRange->start().toCursor();
3124 }
3125
3126 // No block, just add to start & end of selection
3127 insertText(selectionRange->end(), QString(closingBracket));
3128 view->slotTextInserted(view, selectionRange->end(), QString(closingBracket));
3129 insertText(selectionRange->start(), chars);
3130 view->slotTextInserted(view, selectionRange->start(), chars);
3131 }
3132
3133 // Refresh selection
3134 view->setSelection(selectionRange->toRange());
3135 view->setCursorPosition(selectionRange->end());
3136
3137 editEnd();
3138 return;
3139 }
3140
3141 // normal handling
3142 if (!view->config()->persistentSelection() && view->selection()) {
3143 view->removeSelectedText();
3144 }
3145
3146 const KTextEditor::Cursor oldCur(view->cursorPosition());
3147
3148 const bool multiLineBlockMode = view->blockSelection() && view->selection();
3149 if (view->currentInputMode()->overwrite()) {
3150 // blockmode multiline selection case: remove chars in every line
3151 const KTextEditor::Range selectionRange = view->selectionRange();
3152 const int startLine = multiLineBlockMode ? qMax(0, selectionRange.start().line()) : view->cursorPosition().line();
3153 const int endLine = multiLineBlockMode ? qMin(selectionRange.end().line(), lastLine()) : startLine;
3154 const int virtualColumn = toVirtualColumn(multiLineBlockMode ? selectionRange.end() : view->cursorPosition());
3155
3156 for (int line = endLine; line >= startLine; --line) {
3157 Kate::TextLine textLine = m_buffer->plainLine(line);
3158 const int column = fromVirtualColumn(line, virtualColumn);
3159 KTextEditor::Range r = KTextEditor::Range(KTextEditor::Cursor(line, column), qMin(chars.length(), textLine.length() - column));
3160
3161 // replace mode needs to know what was removed so it can be restored with backspace
3162 if (oldCur.column() < lineLength(line)) {
3163 QChar removed = characterAt(KTextEditor::Cursor(line, column));
3164 view->currentInputMode()->overwrittenChar(removed);
3165 }
3166
3167 removeText(r);
3168 }
3169 }
3170
3171 chars = eventuallyReplaceTabs(view->cursorPosition(), chars);
3172
3173 if (multiLineBlockMode) {
3174 KTextEditor::Range selectionRange = view->selectionRange();
3175 const int startLine = qMax(0, selectionRange.start().line());
3176 const int endLine = qMin(selectionRange.end().line(), lastLine());
3177 const int column = toVirtualColumn(selectionRange.end());
3178 for (int line = endLine; line >= startLine; --line) {
3179 editInsertText(line, fromVirtualColumn(line, column), chars);
3180 }
3181 int newSelectionColumn = toVirtualColumn(view->cursorPosition());
3182 selectionRange.setRange(KTextEditor::Cursor(selectionRange.start().line(), fromVirtualColumn(selectionRange.start().line(), newSelectionColumn)),
3183 KTextEditor::Cursor(selectionRange.end().line(), fromVirtualColumn(selectionRange.end().line(), newSelectionColumn)));
3184 view->setSelection(selectionRange);
3185 } else {
3186 // handle multi cursor input
3187 // We don't want completionWidget to be doing useless stuff, it
3188 // should only respond to main cursor text changes
3189 view->completionWidget()->setIgnoreBufferSignals(true);
3190 const auto &sc = view->secondaryCursors();
3191 KTextEditor::Cursor lastInsertionCursor = KTextEditor::Cursor::invalid();
3192 const bool hasClosingBracket = !closingBracket.isNull();
3193 const QString closingChar = closingBracket;
3194
3195 std::vector<std::pair<Kate::TextCursor *, KTextEditor::Cursor>> freezedCursors;
3196 for (auto it = sc.begin(); it != sc.end(); ++it) {
3197 auto pos = it->cursor();
3198 if (it != sc.begin() && pos == std::prev(it)->cursor()) {
3199 freezedCursors.push_back({std::prev(it)->pos.get(), std::prev(it)->cursor()});
3200 }
3201
3202 lastInsertionCursor = pos;
3203 insertText(pos, chars);
3204 pos = it->cursor();
3205
3206 if (it->cursor() == view->cursorPosition()) {
3207 freezedCursors.push_back({it->pos.get(), it->cursor()});
3208 }
3209
3210 const auto nextChar = view->document()->text({pos, pos + Cursor{0, 1}}).trimmed();
3211 if (hasClosingBracket && !skipAutoBrace(closingBracket, pos) && (nextChar.isEmpty() || !nextChar.at(0).isLetterOrNumber())) {
3212 insertText(it->cursor(), closingChar);
3213 it->pos->setPosition(pos);
3214 }
3215 }
3216
3217 view->completionWidget()->setIgnoreBufferSignals(false);
3218 // then our normal cursor
3219 insertText(view->cursorPosition(), chars);
3220
3221 for (auto &freezed : freezedCursors) {
3222 freezed.first->setPosition(freezed.second);
3223 }
3224 }
3225
3226 // auto bracket handling for newly inserted text
3227 // we inserted a bracket?
3228 // => add the matching closing one to the view + input chars
3229 // try to preserve the cursor position
3230 if (!closingBracket.isNull() && !skipAutoBrace(closingBracket, view->cursorPosition())) {
3231 // add bracket to the view
3232 const auto cursorPos = view->cursorPosition();
3233 const auto nextChar = view->document()->text({cursorPos, cursorPos + Cursor{0, 1}}).trimmed();
3234 if (nextChar.isEmpty() || !nextChar.at(0).isLetterOrNumber()) {
3235 insertText(view->cursorPosition(), QString(closingBracket));
3236 const auto insertedAt(view->cursorPosition());
3237 view->setCursorPosition(cursorPos);
3238 m_currentAutobraceRange.reset(newMovingRange({cursorPos - Cursor{0, 1}, insertedAt}, KTextEditor::MovingRange::DoNotExpand));
3239 connect(view, &View::cursorPositionChanged, this, &DocumentPrivate::checkCursorForAutobrace, Qt::UniqueConnection);
3240
3241 // add bracket to chars inserted! needed for correct signals + indent
3242 chars.append(closingBracket);
3243 }
3244 m_currentAutobraceClosingChar = closingBracket;
3245 }
3246
3247 // end edit session here, to have updated HL in userTypedChar!
3248 editEnd();
3249
3250 // indentation for multi cursors
3251 const auto &secondaryCursors = view->secondaryCursors();
3252 for (const auto &c : secondaryCursors) {
3253 m_indenter->userTypedChar(view, c.cursor(), chars.isEmpty() ? QChar() : chars.at(chars.length() - 1));
3254 }
3255
3256 // trigger indentation for primary
3257 KTextEditor::Cursor b(view->cursorPosition());
3258 m_indenter->userTypedChar(view, b, chars.isEmpty() ? QChar() : chars.at(chars.length() - 1));
3259
3260 // inform the view about the original inserted chars
3261 view->slotTextInserted(view, oldCur, chars);
3262}
3263
3264void KTextEditor::DocumentPrivate::checkCursorForAutobrace(KTextEditor::View *, const KTextEditor::Cursor newPos)
3265{
3266 if (m_currentAutobraceRange && !m_currentAutobraceRange->toRange().contains(newPos)) {
3267 m_currentAutobraceRange.reset();
3268 }
3269}
3270
3271void KTextEditor::DocumentPrivate::newLine(KTextEditor::ViewPrivate *v, KTextEditor::DocumentPrivate::NewLineIndent indent, NewLinePos newLinePos)
3272{
3273 editStart();
3274
3275 if (!v->config()->persistentSelection() && v->selection()) {
3276 v->removeSelectedText();
3277 v->clearSelection();
3278 }
3279
3280 auto insertNewLine = [this](KTextEditor::Cursor c) {
3281 if (c.line() > lastLine()) {
3282 c.setLine(lastLine());
3283 }
3284
3285 if (c.line() < 0) {
3286 c.setLine(0);
3287 }
3288
3289 int ln = c.line();
3290
3291 int len = lineLength(ln);
3292
3293 if (c.column() > len) {
3294 c.setColumn(len);
3295 }
3296
3297 // first: wrap line
3298 editWrapLine(c.line(), c.column());
3299
3300 // update highlighting to have updated HL in userTypedChar!
3301 m_buffer->updateHighlighting();
3302 };
3303
3304 // Helper which allows adding a new line and moving the cursor there
3305 // without modifying the current line
3306 auto adjustCusorPos = [newLinePos, this](KTextEditor::Cursor pos) {
3307 // Handle primary cursor
3308 bool moveCursorToTop = false;
3309 if (newLinePos == Above) {
3310 if (pos.line() <= 0) {
3311 pos.setLine(0);
3312 pos.setColumn(0);
3313 moveCursorToTop = true;
3314 } else {
3315 pos.setLine(pos.line() - 1);
3316 pos.setColumn(lineLength(pos.line()));
3317 }
3318 } else if (newLinePos == Below) {
3319 int lastCol = lineLength(pos.line());
3320 pos.setColumn(lastCol);
3321 }
3322 return std::pair{pos, moveCursorToTop};
3323 };
3324
3325 // Handle multicursors
3326 const auto &secondaryCursors = v->secondaryCursors();
3327 if (!secondaryCursors.empty()) {
3328 // Save the original position of our primary cursor
3329 Kate::TextCursor savedPrimary(m_buffer, v->cursorPosition(), Kate::TextCursor::MoveOnInsert);
3330 for (const auto &c : secondaryCursors) {
3331 const auto [newPos, moveCursorToTop] = adjustCusorPos(c.cursor());
3332 c.pos->setPosition(newPos);
3333 insertNewLine(c.cursor());
3334 if (moveCursorToTop) {
3335 c.pos->setPosition({0, 0});
3336 }
3337 // second: if "indent" is true, indent the new line, if needed...
3338 if (indent == KTextEditor::DocumentPrivate::Indent) {
3339 // Make this secondary cursor primary for a moment
3340 // this is necessary because the scripts modify primary cursor
3341 // position which can lead to weird indent issues with multicursor
3342 v->setCursorPosition(c.cursor());
3343 m_indenter->userTypedChar(v, c.cursor(), QLatin1Char('\n'));
3344 // restore
3345 c.pos->setPosition(v->cursorPosition());
3346 }
3347 }
3348 // Restore the original primary cursor
3349 v->setCursorPosition(savedPrimary.toCursor());
3350 }
3351
3352 const auto [newPos, moveCursorToTop] = adjustCusorPos(v->cursorPosition());
3353 v->setCursorPosition(newPos);
3354 insertNewLine(v->cursorPosition());
3355 if (moveCursorToTop) {
3356 v->setCursorPosition({0, 0});
3357 }
3358 // second: if "indent" is true, indent the new line, if needed...
3359 if (indent == KTextEditor::DocumentPrivate::Indent) {
3360 m_indenter->userTypedChar(v, v->cursorPosition(), QLatin1Char('\n'));
3361 }
3362
3363 editEnd();
3364}
3365
3366void KTextEditor::DocumentPrivate::transpose(const KTextEditor::Cursor cursor)
3367{
3368 Kate::TextLine textLine = m_buffer->plainLine(cursor.line());
3369 if (textLine.length() < 2) {
3370 return;
3371 }
3372
3373 uint col = cursor.column();
3374
3375 if (col > 0) {
3376 col--;
3377 }
3378
3379 if ((textLine.length() - col) < 2) {
3380 return;
3381 }
3382
3383 uint line = cursor.line();
3384 QString s;
3385
3386 // clever swap code if first character on the line swap right&left
3387 // otherwise left & right
3388 s.append(textLine.at(col + 1));
3389 s.append(textLine.at(col));
3390 // do the swap
3391
3392 // do it right, never ever manipulate a textline
3393 editStart();
3394 editRemoveText(line, col, 2);
3395 editInsertText(line, col, s);
3396 editEnd();
3397}
3398
3399void KTextEditor::DocumentPrivate::swapTextRanges(KTextEditor::Range firstWord, KTextEditor::Range secondWord)
3400{
3401 Q_ASSERT(firstWord.isValid() && secondWord.isValid());
3402 Q_ASSERT(!firstWord.overlaps(secondWord));
3403 // ensure that secondWord comes AFTER firstWord
3404 if (firstWord.start().column() > secondWord.start().column() || firstWord.start().line() > secondWord.start().line()) {
3405 const KTextEditor::Range tempRange = firstWord;
3406 firstWord.setRange(secondWord);
3407 secondWord.setRange(tempRange);
3408 }
3409
3410 const QString tempString = text(secondWord);
3411 editStart();
3412 // edit secondWord first as the range might be invalidated after editing firstWord
3413 replaceText(secondWord, text(firstWord));
3414 replaceText(firstWord, tempString);
3415 editEnd();
3416}
3417
3418KTextEditor::Cursor KTextEditor::DocumentPrivate::backspaceAtCursor(KTextEditor::ViewPrivate *view, KTextEditor::Cursor c)
3419{
3420 int col = qMax(c.column(), 0);
3421 int line = qMax(c.line(), 0);
3422 if ((col == 0) && (line == 0)) {
3424 }
3425 if (line >= lines()) {
3427 }
3428
3429 const Kate::TextLine textLine = m_buffer->plainLine(line);
3430
3431 if (col > 0) {
3432 bool useNextBlock = false;
3433 if (config()->backspaceIndents()) {
3434 // backspace indents: erase to next indent position
3435 int colX = textLine.toVirtualColumn(col, config()->tabWidth());
3436 int pos = textLine.firstChar();
3437 if (pos > 0) {
3438 pos = textLine.toVirtualColumn(pos, config()->tabWidth());
3439 }
3440 if (pos < 0 || pos >= (int)colX) {
3441 // only spaces on left side of cursor
3442 if ((int)col > textLine.length()) {
3443 // beyond the end of the line, move cursor only
3444 return KTextEditor::Cursor(line, col - 1);
3445 }
3446 indent(KTextEditor::Range(line, 0, line, 0), -1);
3447 } else {
3448 useNextBlock = true;
3449 }
3450 }
3451 if (!config()->backspaceIndents() || useNextBlock) {
3452 KTextEditor::Cursor beginCursor(line, 0);
3453 KTextEditor::Cursor endCursor(line, col);
3454 if (!view->config()->backspaceRemoveComposed()) { // Normal backspace behavior
3455 beginCursor.setColumn(col - 1);
3456 // move to left of surrogate pair
3457 if (!isValidTextPosition(beginCursor)) {
3458 Q_ASSERT(col >= 2);
3459 beginCursor.setColumn(col - 2);
3460 }
3461 } else {
3462 if (auto l = view->textLayout(c)) {
3463 beginCursor.setColumn(l->previousCursorPosition(c.column()));
3464 }
3465 }
3466 removeText(KTextEditor::Range(beginCursor, endCursor));
3467 // in most cases cursor is moved by removeText, but we should do it manually
3468 // for past-end-of-line cursors in block mode
3469 return beginCursor;
3470 }
3472 } else {
3473 // col == 0: wrap to previous line
3474 const Kate::TextLine textLine = m_buffer->plainLine(line - 1);
3475 KTextEditor::Cursor ret = KTextEditor::Cursor::invalid();
3476
3477 if (line > 0) {
3478 if (config()->wordWrap() && textLine.endsWith(QStringLiteral(" "))) {
3479 // gg: in hard wordwrap mode, backspace must also eat the trailing space
3480 ret = KTextEditor::Cursor(line - 1, textLine.length() - 1);
3481 removeText(KTextEditor::Range(line - 1, textLine.length() - 1, line, 0));
3482 } else {
3483 ret = KTextEditor::Cursor(line - 1, textLine.length());
3484 removeText(KTextEditor::Range(line - 1, textLine.length(), line, 0));
3485 }
3486 }
3487 return ret;
3488 }
3489}
3490
3491void KTextEditor::DocumentPrivate::backspace(KTextEditor::ViewPrivate *view)
3492{
3493 if (!view->config()->persistentSelection() && view->hasSelections()) {
3494 KTextEditor::Range range = view->selectionRange();
3495 editStart(); // Avoid bad selection in case of undo
3496
3497 if (view->blockSelection() && view->selection() && range.start().column() > 0 && toVirtualColumn(range.start()) == toVirtualColumn(range.end())) {
3498 // Remove one character before vertical selection line by expanding the selection
3499 range.setStart(KTextEditor::Cursor(range.start().line(), range.start().column() - 1));
3500 view->setSelection(range);
3501 }
3502 view->removeSelectedText();
3503 view->ensureUniqueCursors();
3504 editEnd();
3505 return;
3506 }
3507
3508 editStart();
3509
3510 // Handle multi cursors
3511 const auto &multiCursors = view->secondaryCursors();
3512 view->completionWidget()->setIgnoreBufferSignals(true);
3513 for (const auto &c : multiCursors) {
3514 const auto newPos = backspaceAtCursor(view, c.cursor());
3515 if (newPos.isValid()) {
3516 c.pos->setPosition(newPos);
3517 }
3518 }
3519 view->completionWidget()->setIgnoreBufferSignals(false);
3520
3521 // Handle primary cursor
3522 auto newPos = backspaceAtCursor(view, view->cursorPosition());
3523 if (newPos.isValid()) {
3524 view->setCursorPosition(newPos);
3525 }
3526
3527 view->ensureUniqueCursors();
3528
3529 editEnd();
3530
3531 // TODO: Handle this for multiple cursors?
3532 if (m_currentAutobraceRange) {
3533 const auto r = m_currentAutobraceRange->toRange();
3534 if (r.columnWidth() == 1 && view->cursorPosition() == r.start()) {
3535 // start parenthesis removed and range length is 1, remove end as well
3536 del(view, view->cursorPosition());
3537 m_currentAutobraceRange.reset();
3538 }
3539 }
3540}
3541
3542void KTextEditor::DocumentPrivate::del(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor c)
3543{
3544 if (!view->config()->persistentSelection() && view->selection()) {
3545 KTextEditor::Range range = view->selectionRange();
3546 editStart(); // Avoid bad selection in case of undo
3547 if (view->blockSelection() && toVirtualColumn(range.start()) == toVirtualColumn(range.end())) {
3548 // Remove one character after vertical selection line by expanding the selection
3549 range.setEnd(KTextEditor::Cursor(range.end().line(), range.end().column() + 1));
3550 view->setSelection(range);
3551 }
3552 view->removeSelectedText();
3553 editEnd();
3554 return;
3555 }
3556
3557 if (c.column() < m_buffer->lineLength(c.line())) {
3558 KTextEditor::Cursor endCursor(c.line(), view->textLayout(c)->nextCursorPosition(c.column()));
3559 removeText(KTextEditor::Range(c, endCursor));
3560 } else if (c.line() < lastLine()) {
3561 removeText(KTextEditor::Range(c.line(), c.column(), c.line() + 1, 0));
3562 }
3563}
3564
3565bool KTextEditor::DocumentPrivate::multiPaste(KTextEditor::ViewPrivate *view, const QStringList &texts)
3566{
3567 if (texts.isEmpty() || view->isMulticursorNotAllowed() || view->secondaryCursors().size() + 1 != (size_t)texts.size()) {
3568 return false;
3569 }
3570
3571 m_undoManager->undoSafePoint();
3572
3573 editStart();
3574 if (view->selection()) {
3575 view->removeSelectedText();
3576 }
3577
3578 auto plainSecondaryCursors = view->plainSecondaryCursors();
3579 KTextEditor::ViewPrivate::PlainSecondaryCursor primary;
3580 primary.pos = view->cursorPosition();
3581 primary.range = view->selectionRange();
3582 plainSecondaryCursors.append(primary);
3583 std::sort(plainSecondaryCursors.begin(), plainSecondaryCursors.end());
3584
3585 static const QRegularExpression re(QStringLiteral("\r\n?"));
3586
3587 for (int i = texts.size() - 1; i >= 0; --i) {
3588 QString text = texts[i];
3589 text.replace(re, QStringLiteral("\n"));
3590 KTextEditor::Cursor pos = plainSecondaryCursors[i].pos;
3591 if (pos.isValid()) {
3592 insertText(pos, text, /*blockmode=*/false);
3593 }
3594 }
3595
3596 editEnd();
3597 return true;
3598}
3599
3600void KTextEditor::DocumentPrivate::paste(KTextEditor::ViewPrivate *view, const QString &text)
3601{
3602 // nop if nothing to paste
3603 if (text.isEmpty()) {
3604 return;
3605 }
3606
3607 // normalize line endings, to e.g. catch issues with \r\n in paste buffer
3608 // see bug 410951
3609 QString s = text;
3610 s.replace(QRegularExpression(QStringLiteral("\r\n?")), QStringLiteral("\n"));
3611
3612 int lines = s.count(QLatin1Char('\n'));
3613 const bool isSingleLine = lines == 0;
3614
3615 m_undoManager->undoSafePoint();
3616
3617 editStart();
3618
3619 KTextEditor::Cursor pos = view->cursorPosition();
3620
3621 bool skipIndentOnPaste = false;
3622 if (isSingleLine) {
3623 const int length = lineLength(pos.line());
3624 // if its a single line and the line already contains some text, skip indenting
3625 skipIndentOnPaste = length > 0;
3626 }
3627
3628 if (!view->config()->persistentSelection() && view->selection()) {
3629 pos = view->selectionRange().start();
3630 if (view->blockSelection()) {
3631 pos = rangeOnLine(view->selectionRange(), pos.line()).start();
3632 if (lines == 0) {
3633 s += QLatin1Char('\n');
3634 s = s.repeated(view->selectionRange().numberOfLines() + 1);
3635 s.chop(1);
3636 }
3637 }
3638 view->removeSelectedText();
3639 }
3640
3641 if (config()->ovr()) {
3642 const auto pasteLines = QStringView(s).split(QLatin1Char('\n'));
3643
3644 if (!view->blockSelection()) {
3645 int endColumn = (pasteLines.count() == 1 ? pos.column() : 0) + pasteLines.last().length();
3646 removeText(KTextEditor::Range(pos, pos.line() + pasteLines.count() - 1, endColumn));
3647 } else {
3648 int maxi = qMin(pos.line() + pasteLines.count(), this->lines());
3649
3650 for (int i = pos.line(); i < maxi; ++i) {
3651 int pasteLength = pasteLines.at(i - pos.line()).length();
3652 removeText(KTextEditor::Range(i, pos.column(), i, qMin(pasteLength + pos.column(), lineLength(i))));
3653 }
3654 }
3655 }
3656
3657 insertText(pos, s, view->blockSelection());
3658 editEnd();
3659
3660 // move cursor right for block select, as the user is moved right internal
3661 // even in that case, but user expects other behavior in block selection
3662 // mode !
3663 // just let cursor stay, that was it before I changed to moving ranges!
3664 if (view->blockSelection()) {
3665 view->setCursorPositionInternal(pos);
3666 }
3667
3668 if (config()->indentPastedText()) {
3669 KTextEditor::Range range = KTextEditor::Range(KTextEditor::Cursor(pos.line(), 0), KTextEditor::Cursor(pos.line() + lines, 0));
3670 if (!skipIndentOnPaste) {
3671 m_indenter->indent(view, range);
3672 }
3673 }
3674
3675 if (!view->blockSelection()) {
3676 Q_EMIT charactersSemiInteractivelyInserted(pos, s);
3677 }
3678 m_undoManager->undoSafePoint();
3679}
3680
3681void KTextEditor::DocumentPrivate::indent(KTextEditor::Range range, int change)
3682{
3683 if (!isReadWrite()) {
3684 return;
3685 }
3686
3687 editStart();
3688 m_indenter->changeIndent(range, change);
3689 editEnd();
3690}
3691
3692void KTextEditor::DocumentPrivate::align(KTextEditor::ViewPrivate *view, KTextEditor::Range range)
3693{
3694 m_indenter->indent(view, range);
3695}
3696
3697void KTextEditor::DocumentPrivate::alignOn(KTextEditor::Range range, const QString &pattern, bool blockwise)
3698{
3699 QStringList lines = textLines(range, blockwise);
3700 // if we have less then two lines in the selection there is nothing to do
3701 if (lines.size() < 2) {
3702 return;
3703 }
3704 // align on first non-blank character by default
3705 QRegularExpression re(pattern.isEmpty() ? QStringLiteral("[^\\s]") : pattern);
3706 // find all matches actual column (normal selection: first line has offset ; block selection: all lines have offset)
3707 int selectionStartColumn = range.start().column();
3708 QList<int> patternStartColumns;
3709 for (const auto &line : lines) {
3710 QRegularExpressionMatch match = re.match(line);
3711 if (!match.hasMatch()) { // no match
3712 patternStartColumns.append(-1);
3713 } else if (match.lastCapturedIndex() == 0) { // pattern has no group
3714 patternStartColumns.append(match.capturedStart(0) + (blockwise ? selectionStartColumn : 0));
3715 } else { // pattern has a group
3716 patternStartColumns.append(match.capturedStart(1) + (blockwise ? selectionStartColumn : 0));
3717 }
3718 }
3719 if (!blockwise && patternStartColumns[0] != -1) {
3720 patternStartColumns[0] += selectionStartColumn;
3721 }
3722 // find which column we'll align with
3723 int maxColumn = *std::max_element(patternStartColumns.cbegin(), patternStartColumns.cend());
3724 // align!
3725 editStart();
3726 for (int i = 0; i < lines.size(); ++i) {
3727 if (patternStartColumns[i] != -1) {
3728 insertText(KTextEditor::Cursor(range.start().line() + i, patternStartColumns[i]), QString(maxColumn - patternStartColumns[i], QChar::Space));
3729 }
3730 }
3731 editEnd();
3732}
3733
3734void KTextEditor::DocumentPrivate::insertTab(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor)
3735{
3736 if (!isReadWrite()) {
3737 return;
3738 }
3739
3740 int lineLen = line(view->cursorPosition().line()).length();
3741 KTextEditor::Cursor c = view->cursorPosition();
3742
3743 editStart();
3744
3745 if (!view->config()->persistentSelection() && view->selection()) {
3746 view->removeSelectedText();
3747 } else if (view->currentInputMode()->overwrite() && c.column() < lineLen) {
3748 KTextEditor::Range r = KTextEditor::Range(view->cursorPosition(), 1);
3749
3750 // replace mode needs to know what was removed so it can be restored with backspace
3751 QChar removed = line(view->cursorPosition().line()).at(r.start().column());
3752 view->currentInputMode()->overwrittenChar(removed);
3753 removeText(r);
3754 }
3755
3756 c = view->cursorPosition();
3757 editInsertText(c.line(), c.column(), QStringLiteral("\t"));
3758
3759 editEnd();
3760}
3761
3762/*
3763 Remove a given string at the beginning
3764 of the current line.
3765*/
3766bool KTextEditor::DocumentPrivate::removeStringFromBeginning(int line, const QString &str)
3767{
3768 Kate::TextLine textline = m_buffer->plainLine(line);
3769
3770 KTextEditor::Cursor cursor(line, 0);
3771 bool there = textline.startsWith(str);
3772
3773 if (!there) {
3774 cursor.setColumn(textline.firstChar());
3775 there = textline.matchesAt(cursor.column(), str);
3776 }
3777
3778 if (there) {
3779 // Remove some chars
3780 removeText(KTextEditor::Range(cursor, str.length()));
3781 }
3782
3783 return there;
3784}
3785
3786/*
3787 Remove a given string at the end
3788 of the current line.
3789*/
3790bool KTextEditor::DocumentPrivate::removeStringFromEnd(int line, const QString &str)
3791{
3792 Kate::TextLine textline = m_buffer->plainLine(line);
3793
3794 KTextEditor::Cursor cursor(line, 0);
3795 bool there = textline.endsWith(str);
3796
3797 if (there) {
3798 cursor.setColumn(textline.length() - str.length());
3799 } else {
3800 cursor.setColumn(textline.lastChar() - str.length() + 1);
3801 there = textline.matchesAt(cursor.column(), str);
3802 }
3803
3804 if (there) {
3805 // Remove some chars
3806 removeText(KTextEditor::Range(cursor, str.length()));
3807 }
3808
3809 return there;
3810}
3811
3812/*
3813 Replace tabs by spaces in the given string, if enabled.
3814 */
3815QString KTextEditor::DocumentPrivate::eventuallyReplaceTabs(const KTextEditor::Cursor cursorPos, const QString &str) const
3816{
3817 const bool replacetabs = config()->replaceTabsDyn();
3818 if (!replacetabs) {
3819 return str;
3820 }
3821 const int indentWidth = config()->indentationWidth();
3822 static const QLatin1Char tabChar('\t');
3823
3824 int column = cursorPos.column();
3825
3826 // The result will always be at least as long as the input
3827 QString result;
3828 result.reserve(str.size());
3829
3830 for (const QChar ch : str) {
3831 if (ch == tabChar) {
3832 // Insert only enough spaces to align to the next indentWidth column
3833 // This fixes bug #340212
3834 int spacesToInsert = indentWidth - (column % indentWidth);
3835 result += QString(spacesToInsert, QLatin1Char(' '));
3836 column += spacesToInsert;
3837 } else {
3838 // Just keep all other typed characters as-is
3839 result += ch;
3840 ++column;
3841 }
3842 }
3843 return result;
3844}
3845
3846/*
3847 Add to the current line a comment line mark at the beginning.
3848*/
3849void KTextEditor::DocumentPrivate::addStartLineCommentToSingleLine(int line, int attrib)
3850{
3851 const QString commentLineMark = highlight()->getCommentSingleLineStart(attrib) + QLatin1Char(' ');
3852 int pos = 0;
3853
3854 if (highlight()->getCommentSingleLinePosition(attrib) == KSyntaxHighlighting::CommentPosition::AfterWhitespace) {
3855 const Kate::TextLine l = kateTextLine(line);
3856 pos = qMax(0, l.firstChar());
3857 }
3858 insertText(KTextEditor::Cursor(line, pos), commentLineMark);
3859}
3860
3861/*
3862 Remove from the current line a comment line mark at
3863 the beginning if there is one.
3864*/
3865bool KTextEditor::DocumentPrivate::removeStartLineCommentFromSingleLine(int line, int attrib)
3866{
3867 const QString shortCommentMark = highlight()->getCommentSingleLineStart(attrib);
3868 const QString longCommentMark = shortCommentMark + QLatin1Char(' ');
3869
3870 editStart();
3871
3872 // Try to remove the long comment mark first
3873 bool removed = (removeStringFromBeginning(line, longCommentMark) || removeStringFromBeginning(line, shortCommentMark));
3874
3875 editEnd();
3876
3877 return removed;
3878}
3879
3880/*
3881 Add to the current line a start comment mark at the
3882 beginning and a stop comment mark at the end.
3883*/
3884void KTextEditor::DocumentPrivate::addStartStopCommentToSingleLine(int line, int attrib)
3885{
3886 const QString startCommentMark = highlight()->getCommentStart(attrib) + QLatin1Char(' ');
3887 const QString stopCommentMark = QLatin1Char(' ') + highlight()->getCommentEnd(attrib);
3888
3889 editStart();
3890
3891 // Add the start comment mark
3892 insertText(KTextEditor::Cursor(line, 0), startCommentMark);
3893
3894 // Go to the end of the line
3895 const int col = m_buffer->lineLength(line);
3896
3897 // Add the stop comment mark
3898 insertText(KTextEditor::Cursor(line, col), stopCommentMark);
3899
3900 editEnd();
3901}
3902
3903/*
3904 Remove from the current line a start comment mark at
3905 the beginning and a stop comment mark at the end.
3906*/
3907bool KTextEditor::DocumentPrivate::removeStartStopCommentFromSingleLine(int line, int attrib)
3908{
3909 const QString shortStartCommentMark = highlight()->getCommentStart(attrib);
3910 const QString longStartCommentMark = shortStartCommentMark + QLatin1Char(' ');
3911 const QString shortStopCommentMark = highlight()->getCommentEnd(attrib);
3912 const QString longStopCommentMark = QLatin1Char(' ') + shortStopCommentMark;
3913
3914 editStart();
3915
3916 // Try to remove the long start comment mark first
3917 const bool removedStart = (removeStringFromBeginning(line, longStartCommentMark) || removeStringFromBeginning(line, shortStartCommentMark));
3918
3919 // Try to remove the long stop comment mark first
3920 const bool removedStop = removedStart && (removeStringFromEnd(line, longStopCommentMark) || removeStringFromEnd(line, shortStopCommentMark));
3921
3922 editEnd();
3923
3924 return (removedStart || removedStop);
3925}
3926
3927/*
3928 Add to the current selection a start comment mark at the beginning
3929 and a stop comment mark at the end.
3930*/
3931void KTextEditor::DocumentPrivate::addStartStopCommentToSelection(KTextEditor::Range selection, bool blockSelection, int attrib)
3932{
3933 const QString startComment = highlight()->getCommentStart(attrib);
3934 const QString endComment = highlight()->getCommentEnd(attrib);
3935
3936 KTextEditor::Range range = selection;
3937
3938 if ((range.end().column() == 0) && (range.end().line() > 0)) {
3939 range.setEnd(KTextEditor::Cursor(range.end().line() - 1, lineLength(range.end().line() - 1)));
3940 }
3941
3942 editStart();
3943
3944 if (!blockSelection) {
3945 insertText(range.end(), endComment);
3946 insertText(range.start(), startComment);
3947 } else {
3948 for (int line = range.start().line(); line <= range.end().line(); line++) {
3949 KTextEditor::Range subRange = rangeOnLine(range, line);
3950 insertText(subRange.end(), endComment);
3951 insertText(subRange.start(), startComment);
3952 }
3953 }
3954
3955 editEnd();
3956 // selection automatically updated (MovingRange)
3957}
3958
3959/*
3960 Add to the current selection a comment line mark at the beginning of each line.
3961*/
3962void KTextEditor::DocumentPrivate::addStartLineCommentToSelection(KTextEditor::Range selection, int attrib)
3963{
3964 int sl = selection.start().line();
3965 int el = selection.end().line();
3966
3967 // if end of selection is in column 0 in last line, omit the last line
3968 if ((selection.end().column() == 0) && (el > 0)) {
3969 el--;
3970 }
3971
3972 if (sl < 0 || el < 0 || sl >= lines() || el >= lines()) {
3973 return;
3974 }
3975
3976 editStart();
3977
3978 const QString commentLineMark = highlight()->getCommentSingleLineStart(attrib) + QLatin1Char(' ');
3979
3980 int col = 0;
3981 if (highlight()->getCommentSingleLinePosition(attrib) == KSyntaxHighlighting::CommentPosition::AfterWhitespace) {
3982 // For afterwhitespace, we add comment mark at col for all the lines,
3983 // where col == smallest indent in selection
3984 // This means that for somelines for example, a statement in an if block
3985 // might not have its comment mark exactly afterwhitespace, which is okay
3986 // and _good_ because if someone runs a formatter after commenting we will
3987 // loose indentation, which is _really_ bad and makes afterwhitespace useless
3988
3989 col = std::numeric_limits<int>::max();
3990 // For each line in selection, try to find the smallest indent
3991 for (int l = el; l >= sl; l--) {
3992 const auto line = plainKateTextLine(l);
3993 if (line.length() == 0) {
3994 continue;
3995 }
3996 col = qMin(col, qMax(0, line.firstChar()));
3997 if (col == 0) {
3998 // early out: there can't be an indent smaller than 0
3999 break;
4000 }
4001 }
4002
4003 if (col == std::numeric_limits<int>::max()) {
4004 col = 0;
4005 }
4006 Q_ASSERT(col >= 0);
4007 }
4008
4009 // For each line of the selection
4010 for (int l = el; l >= sl; l--) {
4011 insertText(KTextEditor::Cursor(l, col), commentLineMark);
4012 }
4013
4014 editEnd();
4015}
4016
4017bool KTextEditor::DocumentPrivate::nextNonSpaceCharPos(int &line, int &col)
4018{
4019 for (; line >= 0 && line < m_buffer->lines(); line++) {
4020 Kate::TextLine textLine = m_buffer->plainLine(line);
4021 col = textLine.nextNonSpaceChar(col);
4022 if (col != -1) {
4023 return true; // Next non-space char found
4024 }
4025 col = 0;
4026 }
4027 // No non-space char found
4028 line = -1;
4029 col = -1;
4030 return false;
4031}
4032
4033bool KTextEditor::DocumentPrivate::previousNonSpaceCharPos(int &line, int &col)
4034{
4035 while (line >= 0 && line < m_buffer->lines()) {
4036 Kate::TextLine textLine = m_buffer->plainLine(line);
4037 col = textLine.previousNonSpaceChar(col);
4038 if (col != -1) {
4039 return true;
4040 }
4041 if (line == 0) {
4042 return false;
4043 }
4044 --line;
4045 col = textLine.length();
4046 }
4047 // No non-space char found
4048 line = -1;
4049 col = -1;
4050 return false;
4051}
4052
4053/*
4054 Remove from the selection a start comment mark at
4055 the beginning and a stop comment mark at the end.
4056*/
4057bool KTextEditor::DocumentPrivate::removeStartStopCommentFromSelection(KTextEditor::Range selection, int attrib)
4058{
4059 const QString startComment = highlight()->getCommentStart(attrib);
4060 const QString endComment = highlight()->getCommentEnd(attrib);
4061
4062 int sl = qMax<int>(0, selection.start().line());
4063 int el = qMin<int>(selection.end().line(), lastLine());
4064 int sc = selection.start().column();
4065 int ec = selection.end().column();
4066
4067 // The selection ends on the char before selectEnd
4068 if (ec != 0) {
4069 --ec;
4070 } else if (el > 0) {
4071 --el;
4072 ec = m_buffer->lineLength(el) - 1;
4073 }
4074
4075 const int startCommentLen = startComment.length();
4076 const int endCommentLen = endComment.length();
4077
4078 // had this been perl or sed: s/^\s*$startComment(.+?)$endComment\s*/$2/
4079
4080 bool remove = nextNonSpaceCharPos(sl, sc) && m_buffer->plainLine(sl).matchesAt(sc, startComment) && previousNonSpaceCharPos(el, ec)
4081 && ((ec - endCommentLen + 1) >= 0) && m_buffer->plainLine(el).matchesAt(ec - endCommentLen + 1, endComment);
4082
4083 if (remove) {
4084 editStart();
4085
4086 removeText(KTextEditor::Range(el, ec - endCommentLen + 1, el, ec + 1));
4087 removeText(KTextEditor::Range(sl, sc, sl, sc + startCommentLen));
4088
4089 editEnd();
4090 // selection automatically updated (MovingRange)
4091 }
4092
4093 return remove;
4094}
4095
4096bool KTextEditor::DocumentPrivate::removeStartStopCommentFromRegion(const KTextEditor::Cursor start, const KTextEditor::Cursor end, int attrib)
4097{
4098 const QString startComment = highlight()->getCommentStart(attrib);
4099 const QString endComment = highlight()->getCommentEnd(attrib);
4100 const int startCommentLen = startComment.length();
4101 const int endCommentLen = endComment.length();
4102
4103 const bool remove = m_buffer->plainLine(start.line()).matchesAt(start.column(), startComment)
4104 && m_buffer->plainLine(end.line()).matchesAt(end.column() - endCommentLen, endComment);
4105 if (remove) {
4106 editStart();
4107 removeText(KTextEditor::Range(end.line(), end.column() - endCommentLen, end.line(), end.column()));
4108 removeText(KTextEditor::Range(start, startCommentLen));
4109 editEnd();
4110 }
4111 return remove;
4112}
4113
4114/*
4115 Remove from the beginning of each line of the
4116 selection a start comment line mark.
4117*/
4118bool KTextEditor::DocumentPrivate::removeStartLineCommentFromSelection(KTextEditor::Range selection, int attrib, bool toggleComment)
4119{
4120 const QString shortCommentMark = highlight()->getCommentSingleLineStart(attrib);
4121 const QString longCommentMark = shortCommentMark + QLatin1Char(' ');
4122
4123 const int startLine = selection.start().line();
4124 int endLine = selection.end().line();
4125
4126 if ((selection.end().column() == 0) && (endLine > 0)) {
4127 endLine--;
4128 }
4129
4130 bool removed = false;
4131
4132 // If we are toggling, we check whether all lines in the selection start
4133 // with a comment. If they don't, we return early
4134 // NOTE: When toggling, we only remove comments if all lines in the selection
4135 // are comments, otherwise we recomment the comments
4136 if (toggleComment) {
4137 bool allLinesAreCommented = true;
4138 for (int line = endLine; line >= startLine; line--) {
4139 const auto ln = m_buffer->plainLine(line);
4140 const QString &text = ln.text();
4141 // Empty lines in between comments is ok
4142 if (text.isEmpty()) {
4143 continue;
4144 }
4145 QStringView textView(text.data(), text.size());
4146 // Must trim any spaces at the beginning
4147 textView = textView.trimmed();
4148 if (!textView.startsWith(shortCommentMark) && !textView.startsWith(longCommentMark)) {
4149 allLinesAreCommented = false;
4150 break;
4151 }
4152 }
4153 if (!allLinesAreCommented) {
4154 return false;
4155 }
4156 }
4157
4158 editStart();
4159
4160 // For each line of the selection
4161 for (int z = endLine; z >= startLine; z--) {
4162 // Try to remove the long comment mark first
4163 removed = (removeStringFromBeginning(z, longCommentMark) || removeStringFromBeginning(z, shortCommentMark) || removed);
4164 }
4165
4166 editEnd();
4167 // selection automatically updated (MovingRange)
4168
4169 return removed;
4170}
4171
4172void KTextEditor::DocumentPrivate::commentSelection(KTextEditor::Range selection, KTextEditor::Cursor c, bool blockSelect, CommentType changeType)
4173{
4174 const bool hasSelection = !selection.isEmpty();
4175 int selectionCol = 0;
4176
4177 if (hasSelection) {
4178 selectionCol = selection.start().column();
4179 }
4180 const int line = c.line();
4181
4182 int startAttrib = 0;
4183 Kate::TextLine ln = kateTextLine(line);
4184
4185 if (selectionCol < ln.length()) {
4186 startAttrib = ln.attribute(selectionCol);
4187 } else if (!ln.attributesList().empty()) {
4188 startAttrib = ln.attributesList().back().attributeValue;
4189 }
4190
4191 bool hasStartLineCommentMark = !(highlight()->getCommentSingleLineStart(startAttrib).isEmpty());
4192 bool hasStartStopCommentMark = (!(highlight()->getCommentStart(startAttrib).isEmpty()) && !(highlight()->getCommentEnd(startAttrib).isEmpty()));
4193
4194 if (changeType == Comment) {
4195 if (!hasSelection) {
4196 if (hasStartLineCommentMark) {
4197 addStartLineCommentToSingleLine(line, startAttrib);
4198 } else if (hasStartStopCommentMark) {
4199 addStartStopCommentToSingleLine(line, startAttrib);
4200 }
4201 } else {
4202 // anders: prefer single line comment to avoid nesting probs
4203 // If the selection starts after first char in the first line
4204 // or ends before the last char of the last line, we may use
4205 // multiline comment markers.
4206 // TODO We should try to detect nesting.
4207 // - if selection ends at col 0, most likely she wanted that
4208 // line ignored
4209 const KTextEditor::Range sel = selection;
4210 if (hasStartStopCommentMark
4211 && (!hasStartLineCommentMark
4212 || ((sel.start().column() > m_buffer->plainLine(sel.start().line()).firstChar())
4213 || (sel.end().column() > 0 && sel.end().column() < (m_buffer->plainLine(sel.end().line()).length()))))) {
4214 addStartStopCommentToSelection(selection, blockSelect, startAttrib);
4215 } else if (hasStartLineCommentMark) {
4216 addStartLineCommentToSelection(selection, startAttrib);
4217 }
4218 }
4219 } else { // uncomment
4220 bool removed = false;
4221 const bool toggleComment = changeType == ToggleComment;
4222 if (!hasSelection) {
4223 removed = (hasStartLineCommentMark && removeStartLineCommentFromSingleLine(line, startAttrib))
4224 || (hasStartStopCommentMark && removeStartStopCommentFromSingleLine(line, startAttrib));
4225 } else {
4226 // anders: this seems like it will work with above changes :)
4227 removed = (hasStartStopCommentMark && removeStartStopCommentFromSelection(selection, startAttrib))
4228 || (hasStartLineCommentMark && removeStartLineCommentFromSelection(selection, startAttrib, toggleComment));
4229 }
4230
4231 // recursive call for toggle comment
4232 if (!removed && toggleComment) {
4233 commentSelection(selection, c, blockSelect, Comment);
4234 }
4235 }
4236}
4237
4238/*
4239 Comment or uncomment the selection or the current
4240 line if there is no selection.
4241*/
4242void KTextEditor::DocumentPrivate::comment(KTextEditor::ViewPrivate *v, uint line, uint column, CommentType change)
4243{
4244 // skip word wrap bug #105373
4245 const bool skipWordWrap = wordWrap();
4246 if (skipWordWrap) {
4247 setWordWrap(false);
4248 }
4249
4250 editStart();
4251
4252 if (v->selection()) {
4253 const auto &cursors = v->secondaryCursors();
4254 for (const auto &c : cursors) {
4255 if (!c.range) {
4256 continue;
4257 }
4258 commentSelection(c.range->toRange(), c.cursor(), false, change);
4259 }
4260 KTextEditor::Cursor c(line, column);
4261 commentSelection(v->selectionRange(), c, v->blockSelection(), change);
4262 } else {
4263 const auto &cursors = v->secondaryCursors();
4264 for (const auto &c : cursors) {
4265 commentSelection({}, c.cursor(), false, change);
4266 }
4267 commentSelection({}, KTextEditor::Cursor(line, column), false, change);
4268 }
4269
4270 editEnd();
4271
4272 if (skipWordWrap) {
4273 setWordWrap(true); // see begin of function ::comment (bug #105373)
4274 }
4275}
4276
4277void KTextEditor::DocumentPrivate::transformCursorOrRange(KTextEditor::ViewPrivate *v,
4278 KTextEditor::Cursor c,
4279 KTextEditor::Range selection,
4280 KTextEditor::DocumentPrivate::TextTransform t)
4281{
4282 if (v->selection()) {
4283 editStart();
4284
4285 KTextEditor::Range range(selection.start(), 0);
4286 while (range.start().line() <= selection.end().line()) {
4287 int start = 0;
4288 int end = lineLength(range.start().line());
4289
4290 if (range.start().line() == selection.start().line() || v->blockSelection()) {
4291 start = selection.start().column();
4292 }
4293
4294 if (range.start().line() == selection.end().line() || v->blockSelection()) {
4295 end = selection.end().column();
4296 }
4297
4298 if (start > end) {
4299 int swapCol = start;
4300 start = end;
4301 end = swapCol;
4302 }
4303 range.setStart(KTextEditor::Cursor(range.start().line(), start));
4304 range.setEnd(KTextEditor::Cursor(range.end().line(), end));
4305
4306 QString s = text(range);
4307 QString old = s;
4308
4309 if (t == Uppercase) {
4310 // honor locale, see bug 467104
4311 s = QLocale().toUpper(s);
4312 } else if (t == Lowercase) {
4313 // honor locale, see bug 467104
4314 s = QLocale().toLower(s);
4315 } else { // Capitalize
4316 Kate::TextLine l = m_buffer->plainLine(range.start().line());
4317 int p(0);
4318 while (p < s.length()) {
4319 // If bol or the character before is not in a word, up this one:
4320 // 1. if both start and p is 0, upper char.
4321 // 2. if blockselect or first line, and p == 0 and start-1 is not in a word, upper
4322 // 3. if p-1 is not in a word, upper.
4323 if ((!range.start().column() && !p)
4324 || ((range.start().line() == selection.start().line() || v->blockSelection()) && !p
4325 && !highlight()->isInWord(l.at(range.start().column() - 1)))
4326 || (p && !highlight()->isInWord(s.at(p - 1)))) {
4327 s[p] = s.at(p).toUpper();
4328 }
4329 p++;
4330 }
4331 }
4332
4333 if (s != old) {
4334 removeText(range);
4335 insertText(range.start(), s);
4336 }
4337
4338 range.setBothLines(range.start().line() + 1);
4339 }
4340
4341 editEnd();
4342 } else { // no selection
4343 editStart();
4344
4345 // get cursor
4346 KTextEditor::Cursor cursor = c;
4347
4348 QString old = text(KTextEditor::Range(cursor, 1));
4349 QString s;
4350 switch (t) {
4351 case Uppercase:
4352 s = old.toUpper();
4353 break;
4354 case Lowercase:
4355 s = old.toLower();
4356 break;
4357 case Capitalize: {
4358 Kate::TextLine l = m_buffer->plainLine(cursor.line());
4359 while (cursor.column() > 0 && highlight()->isInWord(l.at(cursor.column() - 1), l.attribute(cursor.column() - 1))) {
4360 cursor.setColumn(cursor.column() - 1);
4361 }
4362 old = text(KTextEditor::Range(cursor, 1));
4363 s = old.toUpper();
4364 } break;
4365 default:
4366 break;
4367 }
4368
4369 removeText(KTextEditor::Range(cursor, 1));
4370 insertText(cursor, s);
4371
4372 editEnd();
4373 }
4374}
4375
4376void KTextEditor::DocumentPrivate::transform(KTextEditor::ViewPrivate *v, const KTextEditor::Cursor c, KTextEditor::DocumentPrivate::TextTransform t)
4377{
4378 editStart();
4379
4380 if (v->selection()) {
4381 const auto &cursors = v->secondaryCursors();
4382 for (const auto &c : cursors) {
4383 if (!c.range) {
4384 continue;
4385 }
4386 auto pos = c.pos->toCursor();
4387 transformCursorOrRange(v, c.anchor, c.range->toRange(), t);
4388 c.pos->setPosition(pos);
4389 }
4390 // cache the selection and cursor, so we can be sure to restore.
4391 const auto selRange = v->selectionRange();
4392 transformCursorOrRange(v, c, v->selectionRange(), t);
4393 v->setSelection(selRange);
4394 v->setCursorPosition(c);
4395 } else { // no selection
4396 const auto &secondaryCursors = v->secondaryCursors();
4397 for (const auto &c : secondaryCursors) {
4398 transformCursorOrRange(v, c.cursor(), {}, t);
4399 }
4400 transformCursorOrRange(v, c, {}, t);
4401 }
4402
4403 editEnd();
4404}
4405
4407{
4408 // if ( first == last ) last += 1;
4409 editStart();
4410 int line(first);
4411 while (first < last) {
4412 if (line >= lines() || line + 1 >= lines()) {
4413 editEnd();
4414 return;
4415 }
4416
4417 // Normalize the whitespace in the joined lines by making sure there's
4418 // always exactly one space between the joined lines
4419 // This cannot be done in editUnwrapLine, because we do NOT want this
4420 // behavior when deleting from the start of a line, just when explicitly
4421 // calling the join command
4424
4425 int pos = tl.firstChar();
4426 if (pos >= 0) {
4427 if (pos != 0) {
4428 editRemoveText(line + 1, 0, pos);
4429 }
4430 if (!(l.length() == 0 || l.at(l.length() - 1).isSpace())) {
4431 editInsertText(line + 1, 0, QStringLiteral(" "));
4432 }
4433 } else {
4434 // Just remove the whitespace and let Kate handle the rest
4435 editRemoveText(line + 1, 0, tl.length());
4436 }
4437
4439 first++;
4440 }
4441 editEnd();
4442}
4443
4444void KTextEditor::DocumentPrivate::tagLines(KTextEditor::LineRange lineRange)
4445{
4446 for (auto view : std::as_const(m_views)) {
4447 static_cast<ViewPrivate *>(view)->tagLines(lineRange, true);
4448 }
4449}
4450
4451void KTextEditor::DocumentPrivate::tagLine(int line)
4452{
4453 tagLines({line, line});
4454}
4455
4456void KTextEditor::DocumentPrivate::repaintViews(bool paintOnlyDirty)
4457{
4458 for (auto view : std::as_const(m_views)) {
4459 static_cast<ViewPrivate *>(view)->repaintText(paintOnlyDirty);
4460 }
4461}
4462
4463/*
4464 Bracket matching uses the following algorithm:
4465 If in overwrite mode, match the bracket currently underneath the cursor.
4466 Otherwise, if the character to the left is a bracket,
4467 match it. Otherwise if the character to the right of the cursor is a
4468 bracket, match it. Otherwise, don't match anything.
4469*/
4470KTextEditor::Range KTextEditor::DocumentPrivate::findMatchingBracket(const KTextEditor::Cursor start, int maxLines)
4471{
4472 if (maxLines < 0 || start.line() < 0 || start.line() >= lines()) {
4474 }
4475
4476 Kate::TextLine textLine = m_buffer->plainLine(start.line());
4477 KTextEditor::Range range(start, start);
4478 const QChar right = textLine.at(range.start().column());
4479 const QChar left = textLine.at(range.start().column() - 1);
4480 QChar bracket;
4481
4482 if (config()->ovr()) {
4483 if (isBracket(right)) {
4484 bracket = right;
4485 } else {
4487 }
4488 } else if (isBracket(right)) {
4489 bracket = right;
4490 } else if (isBracket(left)) {
4491 range.setStart(KTextEditor::Cursor(range.start().line(), range.start().column() - 1));
4492 bracket = left;
4493 } else {
4495 }
4496
4497 const QChar opposite = matchingBracket(bracket);
4498 if (opposite.isNull()) {
4500 }
4501
4502 const int searchDir = isStartBracket(bracket) ? 1 : -1;
4503 uint nesting = 0;
4504
4505 const int minLine = qMax(range.start().line() - maxLines, 0);
4506 const int maxLine = qMin(range.start().line() + maxLines, documentEnd().line());
4507
4508 range.setEnd(range.start());
4509 KTextEditor::DocumentCursor cursor(this);
4510 cursor.setPosition(range.start());
4511 int validAttr = kateTextLine(cursor.line()).attribute(cursor.column());
4512
4513 while (cursor.line() >= minLine && cursor.line() <= maxLine) {
4514 if (!cursor.move(searchDir)) {
4516 }
4517
4518 Kate::TextLine textLine = kateTextLine(cursor.line());
4519 if (textLine.attribute(cursor.column()) == validAttr) {
4520 // Check for match
4521 QChar c = textLine.at(cursor.column());
4522 if (c == opposite) {
4523 if (nesting == 0) {
4524 if (searchDir > 0) { // forward
4525 range.setEnd(cursor.toCursor());
4526 } else {
4527 range.setStart(cursor.toCursor());
4528 }
4529 return range;
4530 }
4531 nesting--;
4532 } else if (c == bracket) {
4533 nesting++;
4534 }
4535 }
4536 }
4537
4539}
4540
4541// helper: remove \r and \n from visible document name (bug #170876)
4542inline static QString removeNewLines(const QString &str)
4543{
4544 QString tmp(str);
4545 return tmp.replace(QLatin1String("\r\n"), QLatin1String(" ")).replace(QLatin1Char('\r'), QLatin1Char(' ')).replace(QLatin1Char('\n'), QLatin1Char(' '));
4546}
4547
4548void KTextEditor::DocumentPrivate::updateDocName()
4549{
4550 // if the name is set, and starts with FILENAME, it should not be changed!
4551 if (!url().isEmpty() && (m_docName == removeNewLines(url().fileName()) || m_docName.startsWith(removeNewLines(url().fileName()) + QLatin1String(" (")))) {
4552 return;
4553 }
4554
4555 int count = -1;
4556
4557 std::vector<KTextEditor::DocumentPrivate *> docsWithSameName;
4558
4559 const auto docs = KTextEditor::EditorPrivate::self()->documents();
4560 for (KTextEditor::Document *kteDoc : docs) {
4561 auto doc = static_cast<KTextEditor::DocumentPrivate *>(kteDoc);
4562 if ((doc != this) && (doc->url().fileName() == url().fileName())) {
4563 if (doc->m_docNameNumber > count) {
4564 count = doc->m_docNameNumber;
4565 }
4566 docsWithSameName.push_back(doc);
4567 }
4568 }
4569
4570 m_docNameNumber = count + 1;
4571
4572 QString oldName = m_docName;
4573 m_docName = removeNewLines(url().fileName());
4574
4575 m_isUntitled = m_docName.isEmpty();
4576
4577 if (!m_isUntitled && !docsWithSameName.empty()) {
4578 docsWithSameName.push_back(this);
4579 uniquifyDocNames(docsWithSameName);
4580 return;
4581 }
4582
4583 if (m_isUntitled) {
4584 m_docName = i18n("Untitled");
4585 }
4586
4587 if (m_docNameNumber > 0) {
4588 m_docName = QString(m_docName + QLatin1String(" (%1)")).arg(m_docNameNumber + 1);
4589 }
4590
4591 // avoid to emit this, if name doesn't change!
4592 if (oldName != m_docName) {
4593 Q_EMIT documentNameChanged(this);
4594 }
4595}
4596
4597/**
4598 * Find the shortest prefix for doc from urls
4599 * @p urls contains a list of urls
4600 * - /path/to/some/file
4601 * - /some/to/path/file
4602 *
4603 * we find the shortest path prefix which can be used to
4604 * identify @p doc
4605 *
4606 * for above, it will return "some" for first and "path" for second
4607 */
4608static QString shortestPrefix(const std::vector<QString> &urls, KTextEditor::DocumentPrivate *doc)
4609{
4611 int lastSlash = url.lastIndexOf(QLatin1Char('/'));
4612 if (lastSlash == -1) {
4613 // just filename?
4614 return url;
4615 }
4616 int fileNameStart = lastSlash;
4617
4618 lastSlash--;
4619 lastSlash = url.lastIndexOf(QLatin1Char('/'), lastSlash);
4620 if (lastSlash == -1) {
4621 // already too short?
4622 lastSlash = 0;
4623 return url.mid(lastSlash, fileNameStart);
4624 }
4625
4626 QStringView urlView = url;
4627 QStringView urlv = url;
4628 urlv = urlv.mid(lastSlash);
4629
4630 for (size_t i = 0; i < urls.size(); ++i) {
4631 if (urls[i] == url) {
4632 continue;
4633 }
4634
4635 if (urls[i].endsWith(urlv)) {
4636 lastSlash = url.lastIndexOf(QLatin1Char('/'), lastSlash - 1);
4637 if (lastSlash <= 0) {
4638 // reached end if we either found no / or found the slash at the start
4639 return url.mid(0, fileNameStart);
4640 }
4641 // else update urlv and match again from start
4642 urlv = urlView.mid(lastSlash);
4643 i = -1;
4644 }
4645 }
4646
4647 return url.mid(lastSlash + 1, fileNameStart - (lastSlash + 1));
4648}
4649
4650void KTextEditor::DocumentPrivate::uniquifyDocNames(const std::vector<KTextEditor::DocumentPrivate *> &docs)
4651{
4652 std::vector<QString> paths;
4653 paths.reserve(docs.size());
4654 std::transform(docs.begin(), docs.end(), std::back_inserter(paths), [](const KTextEditor::DocumentPrivate *d) {
4655 return d->url().toString(QUrl::NormalizePathSegments | QUrl::PreferLocalFile);
4656 });
4657
4658 for (const auto doc : docs) {
4659 const QString prefix = shortestPrefix(paths, doc);
4660 const QString fileName = doc->url().fileName();
4661 const QString oldName = doc->m_docName;
4662
4663 if (!prefix.isEmpty()) {
4664 doc->m_docName = fileName + QStringLiteral(" - ") + prefix;
4665 } else {
4666 doc->m_docName = fileName;
4667 }
4668
4669 if (doc->m_docName != oldName) {
4670 Q_EMIT doc->documentNameChanged(doc);
4671 }
4672 }
4673}
4674
4676{
4677 if (url().isEmpty() || !m_modOnHd) {
4678 return;
4679 }
4680
4681 if (!isModified() && isAutoReload()) {
4682 onModOnHdAutoReload();
4683 return;
4684 }
4685
4686 if (!m_fileChangedDialogsActivated || m_modOnHdHandler) {
4687 return;
4688 }
4689
4690 // don't ask the user again and again the same thing
4691 if (m_modOnHdReason == m_prevModOnHdReason) {
4692 return;
4693 }
4694 m_prevModOnHdReason = m_modOnHdReason;
4695
4696 m_modOnHdHandler = new KateModOnHdPrompt(this, m_modOnHdReason, reasonedMOHString());
4697 connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::saveAsTriggered, this, &DocumentPrivate::onModOnHdSaveAs);
4698 connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::closeTriggered, this, &DocumentPrivate::onModOnHdClose);
4699 connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::reloadTriggered, this, &DocumentPrivate::onModOnHdReload);
4700 connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::autoReloadTriggered, this, &DocumentPrivate::onModOnHdAutoReload);
4701 connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::ignoreTriggered, this, &DocumentPrivate::onModOnHdIgnore);
4702}
4703
4704void KTextEditor::DocumentPrivate::onModOnHdSaveAs()
4705{
4706 m_modOnHd = false;
4707 const QUrl res = getSaveFileUrl(i18n("Save File"));
4708 if (!res.isEmpty()) {
4709 if (!saveAs(res)) {
4710 KMessageBox::error(dialogParent(), i18n("Save failed"));
4711 m_modOnHd = true;
4712 } else {
4713 delete m_modOnHdHandler;
4714 m_prevModOnHdReason = OnDiskUnmodified;
4715 Q_EMIT modifiedOnDisk(this, false, OnDiskUnmodified);
4716 }
4717 } else { // the save as dialog was canceled, we are still modified on disk
4718 m_modOnHd = true;
4719 }
4720}
4721
4722void KTextEditor::DocumentPrivate::onModOnHdClose()
4723{
4724 // delay this, otherwise we delete ourself during e.g. event handling + deleting this is undefined!
4725 // see e.g. bug 433180
4726 QTimer::singleShot(0, this, [this]() {
4727 // avoid a prompt in closeDocument()
4728 m_fileChangedDialogsActivated = false;
4729
4730 // allow the application to delete the document with url still intact
4731 if (!KTextEditor::EditorPrivate::self()->application()->closeDocument(this)) {
4732 // restore correct prompt visibility state
4733 m_fileChangedDialogsActivated = true;
4734 }
4735 });
4736}
4737
4738void KTextEditor::DocumentPrivate::onModOnHdReload()
4739{
4740 m_modOnHd = false;
4741 m_prevModOnHdReason = OnDiskUnmodified;
4742 Q_EMIT modifiedOnDisk(this, false, OnDiskUnmodified);
4743
4744 // MUST Clear Undo/Redo here because by the time we get here
4745 // the checksum has already been updated and the undo manager
4746 // sees the new checksum and thinks nothing changed and loads
4747 // a bad undo history resulting in funny things.
4748 m_undoManager->clearUndo();
4749 m_undoManager->clearRedo();
4750
4751 documentReload();
4752 delete m_modOnHdHandler;
4753}
4754
4755void KTextEditor::DocumentPrivate::autoReloadToggled(bool b)
4756{
4757 m_autoReloadMode->setChecked(b);
4758 if (b) {
4759 connect(&m_modOnHdTimer, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload);
4760 } else {
4761 disconnect(&m_modOnHdTimer, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload);
4762 }
4763}
4764
4765bool KTextEditor::DocumentPrivate::isAutoReload()
4766{
4767 return m_autoReloadMode->isChecked();
4768}
4769
4770void KTextEditor::DocumentPrivate::delayAutoReload()
4771{
4772 if (isAutoReload()) {
4773 m_autoReloadThrottle.start();
4774 }
4775}
4776
4777void KTextEditor::DocumentPrivate::onModOnHdAutoReload()
4778{
4779 if (m_modOnHdHandler) {
4780 delete m_modOnHdHandler;
4781 autoReloadToggled(true);
4782 }
4783
4784 if (!isAutoReload()) {
4785 return;
4786 }
4787
4788 if (m_modOnHd && !m_reloading && !m_autoReloadThrottle.isActive()) {
4789 m_modOnHd = false;
4790 m_prevModOnHdReason = OnDiskUnmodified;
4791 Q_EMIT modifiedOnDisk(this, false, OnDiskUnmodified);
4792
4793 // MUST clear undo/redo. This comes way after KDirWatch signaled us
4794 // and the checksum is already updated by the time we start reload.
4795 m_undoManager->clearUndo();
4796 m_undoManager->clearRedo();
4797
4798 documentReload();
4799 m_autoReloadThrottle.start();
4800 }
4801}
4802
4803void KTextEditor::DocumentPrivate::onModOnHdIgnore()
4804{
4805 // ignore as long as m_prevModOnHdReason == m_modOnHdReason
4806 delete m_modOnHdHandler;
4807}
4808
4810{
4811 m_modOnHdReason = reason;
4812 m_modOnHd = (reason != OnDiskUnmodified);
4813 Q_EMIT modifiedOnDisk(this, (reason != OnDiskUnmodified), reason);
4814}
4815
4816class KateDocumentTmpMark
4817{
4818public:
4819 QString line;
4820 KTextEditor::Mark mark;
4821};
4822
4824{
4825 m_fileChangedDialogsActivated = on;
4826}
4827
4829{
4830 if (url().isEmpty()) {
4831 return false;
4832 }
4833
4834 // If we are modified externally clear undo and redo
4835 // Why:
4836 // Our checksum() is already updated at this point by
4837 // slotDelayedHandleModOnHd() so we will end up restoring
4838 // undo because undo manager relies on checksum() to check
4839 // if the doc is same or different. Hence any checksum matching
4840 // is useless at this point and we must clear it here
4841 if (m_modOnHd) {
4842 m_undoManager->clearUndo();
4843 m_undoManager->clearRedo();
4844 }
4845
4846 // typically, the message for externally modified files is visible. Since it
4847 // does not make sense showing an additional dialog, just hide the message.
4848 delete m_modOnHdHandler;
4849
4850 Q_EMIT aboutToReload(this);
4851
4853 tmp.reserve(m_marks.size());
4854 std::transform(m_marks.cbegin(), m_marks.cend(), std::back_inserter(tmp), [this](KTextEditor::Mark *mark) {
4855 return KateDocumentTmpMark{.line = line(mark->line), .mark = *mark};
4856 });
4857
4858 // Remember some settings which may changed at reload
4859 const QString oldMode = mode();
4860 const bool modeByUser = m_fileTypeSetByUser;
4861 const QString oldHlMode = highlightingMode();
4862 const bool hlByUser = m_hlSetByUser;
4863
4864 m_storedVariables.clear();
4865
4866 // save cursor positions for all views
4868 std::transform(m_views.cbegin(), m_views.cend(), std::back_inserter(cursorPositions), [](KTextEditor::View *v) {
4869 return std::pair<KTextEditor::ViewPrivate *, KTextEditor::Cursor>(static_cast<ViewPrivate *>(v), v->cursorPosition());
4870 });
4871
4872 // clear multicursors
4873 // FIXME: Restore multicursors, at least for the case where doc is unmodified
4874 for (auto *view : m_views) {
4875 static_cast<ViewPrivate *>(view)->clearSecondaryCursors();
4876 // Clear folding state if we are modified on hd
4877 if (m_modOnHd) {
4878 static_cast<ViewPrivate *>(view)->clearFoldingState();
4879 }
4880 }
4881
4882 m_reloading = true;
4883 KTextEditor::DocumentPrivate::openUrl(url());
4884
4885 // reset some flags only valid for one reload!
4886 m_userSetEncodingForNextReload = false;
4887
4888 // restore cursor positions for all views
4889 for (auto v : std::as_const(m_views)) {
4890 setActiveView(v);
4891 auto it = std::find_if(cursorPositions.cbegin(), cursorPositions.cend(), [v](const std::pair<KTextEditor::ViewPrivate *, KTextEditor::Cursor> &p) {
4892 return p.first == v;
4893 });
4894 v->setCursorPosition(it->second);
4895 if (v->isVisible()) {
4896 v->repaint();
4897 }
4898 }
4899
4900 const int lines = this->lines();
4901 for (const auto &tmpMark : tmp) {
4902 if (tmpMark.mark.line < lines) {
4903 if (tmpMark.line == line(tmpMark.mark.line)) {
4904 setMark(tmpMark.mark.line, tmpMark.mark.type);
4905 }
4906 }
4907 }
4908
4909 // Restore old settings
4910 if (modeByUser) {
4911 updateFileType(oldMode, true);
4912 }
4913 if (hlByUser) {
4914 setHighlightingMode(oldHlMode);
4915 }
4916
4917 Q_EMIT reloaded(this);
4918
4919 return true;
4920}
4921
4922bool KTextEditor::DocumentPrivate::documentSave()
4923{
4924 if (!url().isValid() || !isReadWrite()) {
4925 return documentSaveAs();
4926 }
4927
4928 return save();
4929}
4930
4931bool KTextEditor::DocumentPrivate::documentSaveAs()
4932{
4933 const QUrl saveUrl = getSaveFileUrl(i18n("Save File"));
4934 if (saveUrl.isEmpty()) {
4935 return false;
4936 }
4937
4938 return saveAs(saveUrl);
4939}
4940
4941bool KTextEditor::DocumentPrivate::documentSaveAsWithEncoding(const QString &encoding)
4942{
4943 const QUrl saveUrl = getSaveFileUrl(i18n("Save File"));
4944 if (saveUrl.isEmpty()) {
4945 return false;
4946 }
4947
4948 setEncoding(encoding);
4949 return saveAs(saveUrl);
4950}
4951
4952void KTextEditor::DocumentPrivate::documentSaveCopyAs()
4953{
4954 const QUrl saveUrl = getSaveFileUrl(i18n("Save Copy of File"));
4955 if (saveUrl.isEmpty()) {
4956 return;
4957 }
4958
4959 QTemporaryFile *file = new QTemporaryFile();
4960 if (!file->open()) {
4961 return;
4962 }
4963
4964 if (!m_buffer->saveFile(file->fileName())) {
4965 KMessageBox::error(dialogParent(),
4966 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 "
4967 "that enough disk space is available.",
4968 this->url().toDisplayString(QUrl::PreferLocalFile)));
4969 return;
4970 }
4971
4972 // get the right permissions, start with safe default
4973 KIO::StatJob *statJob = KIO::stat(url(), KIO::StatJob::SourceSide, KIO::StatBasic);
4975 const auto url = this->url();
4976 connect(statJob, &KIO::StatJob::result, this, [url, file, saveUrl](KJob *j) {
4977 if (auto sj = qobject_cast<KIO::StatJob *>(j)) {
4978 const int permissions = KFileItem(sj->statResult(), url).permissions();
4979 KIO::FileCopyJob *job = KIO::file_copy(QUrl::fromLocalFile(file->fileName()), saveUrl, permissions, KIO::Overwrite);
4982 job->start();
4983 }
4984 });
4985 statJob->start();
4986}
4987
4988void KTextEditor::DocumentPrivate::setWordWrap(bool on)
4989{
4990 config()->setWordWrap(on);
4991}
4992
4993bool KTextEditor::DocumentPrivate::wordWrap() const
4994{
4995 return config()->wordWrap();
4996}
4997
4998void KTextEditor::DocumentPrivate::setWordWrapAt(uint col)
4999{
5000 config()->setWordWrapAt(col);
5001}
5002
5003unsigned int KTextEditor::DocumentPrivate::wordWrapAt() const
5004{
5005 return config()->wordWrapAt();
5006}
5007
5008void KTextEditor::DocumentPrivate::setPageUpDownMovesCursor(bool on)
5009{
5010 config()->setPageUpDownMovesCursor(on);
5011}
5012
5013bool KTextEditor::DocumentPrivate::pageUpDownMovesCursor() const
5014{
5015 return config()->pageUpDownMovesCursor();
5016}
5017// END
5018
5020{
5021 return m_config->setEncoding(e);
5022}
5023
5025{
5026 return m_config->encoding();
5027}
5028
5029void KTextEditor::DocumentPrivate::updateConfig()
5030{
5031 m_undoManager->updateConfig();
5032
5033 // switch indenter if needed and update config....
5034 m_indenter->setMode(m_config->indentationMode());
5035 m_indenter->updateConfig();
5036
5037 // set tab width there, too
5038 m_buffer->setTabWidth(config()->tabWidth());
5039
5040 // update all views, does tagAll and updateView...
5041 for (auto view : std::as_const(m_views)) {
5042 static_cast<ViewPrivate *>(view)->updateDocumentConfig();
5043 }
5044
5045 // update on-the-fly spell checking as spell checking defaults might have changes
5046 if (m_onTheFlyChecker) {
5047 m_onTheFlyChecker->updateConfig();
5048 }
5049
5050 if (config()->autoSave()) {
5051 int interval = config()->autoSaveInterval();
5052 if (interval == 0) {
5053 m_autoSaveTimer.stop();
5054 } else {
5055 m_autoSaveTimer.setInterval(interval * 1000);
5056 if (isModified()) {
5057 m_autoSaveTimer.start();
5058 }
5059 }
5060 }
5061
5062 Q_EMIT configChanged(this);
5063}
5064
5065// BEGIN Variable reader
5066// "local variable" feature by anders, 2003
5067/* TODO
5068 add config options (how many lines to read, on/off)
5069 add interface for plugins/apps to set/get variables
5070 add view stuff
5071*/
5072bool KTextEditor::DocumentPrivate::readVariables(KTextEditor::ViewPrivate *view)
5073{
5074 const bool hasVariableline = [this] {
5075 const QLatin1String s("kate");
5076 if (lines() > 10) {
5077 for (int i = qMax(10, lines() - 10); i < lines(); ++i) {
5078 if (line(i).contains(s)) {
5079 return true;
5080 }
5081 }
5082 }
5083 for (int i = 0; i < qMin(9, lines()); ++i) {
5084 if (line(i).contains(s)) {
5085 return true;
5086 }
5087 }
5088 return false;
5089 }();
5090 if (!hasVariableline) {
5091 return false;
5092 }
5093
5094 if (!view) {
5095 m_config->configStart();
5096 for (auto view : std::as_const(m_views)) {
5097 auto v = static_cast<ViewPrivate *>(view);
5098 v->config()->configStart();
5099 v->rendererConfig()->configStart();
5100 }
5101 } else {
5102 view->config()->configStart();
5103 view->rendererConfig()->configStart();
5104 }
5105
5106 // views!
5107 // read a number of lines in the top/bottom of the document
5108 for (int i = 0; i < qMin(9, lines()); ++i) {
5109 readVariableLine(line(i), view);
5110 }
5111 if (lines() > 10) {
5112 for (int i = qMax(10, lines() - 10); i < lines(); i++) {
5113 readVariableLine(line(i), view);
5114 }
5115 }
5116
5117 if (!view) {
5118 m_config->configEnd();
5119 for (auto view : std::as_const(m_views)) {
5120 auto v = static_cast<ViewPrivate *>(view);
5121 v->config()->configEnd();
5122 v->rendererConfig()->configEnd();
5123 }
5124 } else {
5125 view->config()->configEnd();
5126 view->rendererConfig()->configEnd();
5127 }
5128
5129 return true;
5130}
5131
5132void KTextEditor::DocumentPrivate::readVariableLine(const QString &t, KTextEditor::ViewPrivate *view)
5133{
5134 static const QRegularExpression kvLine(QStringLiteral("kate:(.*)"));
5135 static const QRegularExpression kvLineWildcard(QStringLiteral("kate-wildcard\\((.*)\\):(.*)"));
5136 static const QRegularExpression kvLineMime(QStringLiteral("kate-mimetype\\((.*)\\):(.*)"));
5137 static const QRegularExpression kvVar(QStringLiteral("([\\w\\-]+)\\s+([^;]+)"));
5138
5139 // simple check first, no regex
5140 // no kate inside, no vars, simple...
5141 if (!t.contains(QLatin1String("kate"))) {
5142 return;
5143 }
5144
5145 // found vars, if any
5146 QString s;
5147
5148 // now, try first the normal ones
5149 auto match = kvLine.match(t);
5150 if (match.hasMatch()) {
5151 s = match.captured(1);
5152
5153 // qCDebug(LOG_KTE) << "normal variable line kate: matched: " << s;
5154 } else if ((match = kvLineWildcard.match(t)).hasMatch()) { // regex given
5155 const QStringList wildcards(match.captured(1).split(QLatin1Char(';'), Qt::SkipEmptyParts));
5156 const QString nameOfFile = url().fileName();
5157 const QString pathOfFile = url().path();
5158
5159 bool found = false;
5160 for (const QString &pattern : wildcards) {
5161 // wildcard with path match, bug 453541, check for /
5162 // in that case we do some not anchored matching
5163 const bool matchPath = pattern.contains(QLatin1Char('/'));
5164 const QRegularExpression wildcard(QRegularExpression::wildcardToRegularExpression(pattern,
5167 found = wildcard.match(matchPath ? pathOfFile : nameOfFile).hasMatch();
5168 if (found) {
5169 break;
5170 }
5171 }
5172
5173 // nothing usable found...
5174 if (!found) {
5175 return;
5176 }
5177
5178 s = match.captured(2);
5179
5180 // qCDebug(LOG_KTE) << "guarded variable line kate-wildcard: matched: " << s;
5181 } else if ((match = kvLineMime.match(t)).hasMatch()) { // mime-type given
5182 const QStringList types(match.captured(1).split(QLatin1Char(';'), Qt::SkipEmptyParts));
5183
5184 // no matching type found
5185 if (!types.contains(mimeType())) {
5186 return;
5187 }
5188
5189 s = match.captured(2);
5190
5191 // qCDebug(LOG_KTE) << "guarded variable line kate-mimetype: matched: " << s;
5192 } else { // nothing found
5193 return;
5194 }
5195
5196 // view variable names
5197 static const auto vvl = {
5198 QLatin1String("dynamic-word-wrap"),
5199 QLatin1String("dynamic-word-wrap-indicators"),
5200 QLatin1String("line-numbers"),
5201 QLatin1String("icon-border"),
5202 QLatin1String("folding-markers"),
5203 QLatin1String("folding-preview"),
5204 QLatin1String("bookmark-sorting"),
5205 QLatin1String("auto-center-lines"),
5206 QLatin1String("icon-bar-color"),
5207 QLatin1String("scrollbar-minimap"),
5208 QLatin1String("scrollbar-preview"),
5209 QLatin1String("enter-to-insert-completion")
5210 // renderer
5211 ,
5212 QLatin1String("background-color"),
5213 QLatin1String("selection-color"),
5214 QLatin1String("current-line-color"),
5215 QLatin1String("bracket-highlight-color"),
5216 QLatin1String("word-wrap-marker-color"),
5217 QLatin1String("font"),
5218 QLatin1String("font-size"),
5219 QLatin1String("scheme"),
5220 };
5221 int spaceIndent = -1; // for backward compatibility; see below
5222 bool replaceTabsSet = false;
5223 int startPos(0);
5224
5225 QString var;
5226 QString val;
5227 while ((match = kvVar.match(s, startPos)).hasMatch()) {
5228 startPos = match.capturedEnd(0);
5229 var = match.captured(1);
5230 val = match.captured(2).trimmed();
5231 bool state; // store booleans here
5232 int n; // store ints here
5233
5234 // only apply view & renderer config stuff
5235 if (view) {
5236 if (contains(vvl, var)) { // FIXME define above
5237 KTextEditor::View *v = static_cast<KTextEditor::View *>(view);
5238 setViewVariable(var, val, {&v, 1});
5239 }
5240 } else {
5241 // BOOL SETTINGS
5242 if (var == QLatin1String("word-wrap") && checkBoolValue(val, &state)) {
5243 setWordWrap(state); // ??? FIXME CHECK
5244 }
5245 // KateConfig::configFlags
5246 // FIXME should this be optimized to only a few calls? how?
5247 else if (var == QLatin1String("backspace-indents") && checkBoolValue(val, &state)) {
5248 m_config->setBackspaceIndents(state);
5249 } else if (var == QLatin1String("indent-pasted-text") && checkBoolValue(val, &state)) {
5250 m_config->setIndentPastedText(state);
5251 } else if (var == QLatin1String("replace-tabs") && checkBoolValue(val, &state)) {
5252 m_config->setReplaceTabsDyn(state);
5253 replaceTabsSet = true; // for backward compatibility; see below
5254 } else if (var == QLatin1String("remove-trailing-space") && checkBoolValue(val, &state)) {
5255 qCWarning(LOG_KTE) << i18n(
5256 "Using deprecated modeline 'remove-trailing-space'. "
5257 "Please replace with 'remove-trailing-spaces modified;', see "
5258 "https://docs.kde.org/?application=katepart&branch=stable5&path=config-variables.html#variable-remove-trailing-spaces");
5259 m_config->setRemoveSpaces(state ? 1 : 0);
5260 } else if (var == QLatin1String("replace-trailing-space-save") && checkBoolValue(val, &state)) {
5261 qCWarning(LOG_KTE) << i18n(
5262 "Using deprecated modeline 'replace-trailing-space-save'. "
5263 "Please replace with 'remove-trailing-spaces all;', see "
5264 "https://docs.kde.org/?application=katepart&branch=stable5&path=config-variables.html#variable-remove-trailing-spaces");
5265 m_config->setRemoveSpaces(state ? 2 : 0);
5266 } else if (var == QLatin1String("overwrite-mode") && checkBoolValue(val, &state)) {
5267 m_config->setOvr(state);
5268 } else if (var == QLatin1String("keep-extra-spaces") && checkBoolValue(val, &state)) {
5269 m_config->setKeepExtraSpaces(state);
5270 } else if (var == QLatin1String("tab-indents") && checkBoolValue(val, &state)) {
5271 m_config->setTabIndents(state);
5272 } else if (var == QLatin1String("show-tabs") && checkBoolValue(val, &state)) {
5273 m_config->setShowTabs(state);
5274 } else if (var == QLatin1String("show-trailing-spaces") && checkBoolValue(val, &state)) {
5275 m_config->setShowSpaces(state ? KateDocumentConfig::Trailing : KateDocumentConfig::None);
5276 } else if (var == QLatin1String("space-indent") && checkBoolValue(val, &state)) {
5277 // this is for backward compatibility; see below
5278 spaceIndent = state;
5279 } else if (var == QLatin1String("smart-home") && checkBoolValue(val, &state)) {
5280 m_config->setSmartHome(state);
5281 } else if (var == QLatin1String("newline-at-eof") && checkBoolValue(val, &state)) {
5282 m_config->setNewLineAtEof(state);
5283 }
5284
5285 // INTEGER SETTINGS
5286 else if (var == QLatin1String("tab-width") && checkIntValue(val, &n)) {
5287 m_config->setTabWidth(n);
5288 } else if (var == QLatin1String("indent-width") && checkIntValue(val, &n)) {
5289 m_config->setIndentationWidth(n);
5290 } else if (var == QLatin1String("indent-mode")) {
5291 m_config->setIndentationMode(val);
5292 } else if (var == QLatin1String("word-wrap-column") && checkIntValue(val, &n) && n > 0) { // uint, but hard word wrap at 0 will be no fun ;)
5293 m_config->setWordWrapAt(n);
5294 }
5295
5296 // STRING SETTINGS
5297 else if (var == QLatin1String("eol") || var == QLatin1String("end-of-line")) {
5298 const auto l = {QLatin1String("unix"), QLatin1String("dos"), QLatin1String("mac")};
5299 if ((n = indexOf(l, val.toLower())) != -1) {
5300 // set eol + avoid that it is overwritten by auto-detection again!
5301 // this fixes e.g. .kateconfig files with // kate: eol dos; to work, bug 365705
5302 m_config->setEol(n);
5303 m_config->setAllowEolDetection(false);
5304 }
5305 } else if (var == QLatin1String("bom") || var == QLatin1String("byte-order-mark") || var == QLatin1String("byte-order-marker")) {
5306 if (checkBoolValue(val, &state)) {
5307 m_config->setBom(state);
5308 }
5309 } else if (var == QLatin1String("remove-trailing-spaces")) {
5310 val = val.toLower();
5311 if (val == QLatin1String("1") || val == QLatin1String("modified") || val == QLatin1String("mod") || val == QLatin1String("+")) {
5312 m_config->setRemoveSpaces(1);
5313 } else if (val == QLatin1String("2") || val == QLatin1String("all") || val == QLatin1String("*")) {
5314 m_config->setRemoveSpaces(2);
5315 } else {
5316 m_config->setRemoveSpaces(0);
5317 }
5318 } else if (var == QLatin1String("syntax") || var == QLatin1String("hl")) {
5319 setHighlightingMode(val);
5320 } else if (var == QLatin1String("mode")) {
5321 setMode(val);
5322 } else if (var == QLatin1String("encoding")) {
5323 setEncoding(val);
5324 } else if (var == QLatin1String("default-dictionary")) {
5325 setDefaultDictionary(val);
5326 } else if (var == QLatin1String("automatic-spell-checking") && checkBoolValue(val, &state)) {
5327 onTheFlySpellCheckingEnabled(state);
5328 }
5329
5330 // VIEW SETTINGS
5331 else if (contains(vvl, var)) {
5332 setViewVariable(var, val, m_views);
5333 } else {
5334 m_storedVariables[var] = val;
5335 }
5336 }
5337 }
5338
5339 // Backward compatibility
5340 // If space-indent was set, but replace-tabs was not set, we assume
5341 // that the user wants to replace tabulators and set that flag.
5342 // If both were set, replace-tabs has precedence.
5343 // At this point spaceIndent is -1 if it was never set,
5344 // 0 if it was set to off, and 1 if it was set to on.
5345 // Note that if onlyViewAndRenderer was requested, spaceIndent is -1.
5346 if (!replaceTabsSet && spaceIndent >= 0) {
5347 m_config->setReplaceTabsDyn(spaceIndent > 0);
5348 }
5349}
5350
5351void KTextEditor::DocumentPrivate::setViewVariable(const QString &var, const QString &val, std::span<KTextEditor::View *> views)
5352{
5353 bool state = false;
5354 int n = 0;
5355 QColor c;
5356 for (auto view : views) {
5357 auto v = static_cast<ViewPrivate *>(view);
5358 // First, try the new config interface
5359 QVariant help(val); // Special treatment to catch "on"/"off"
5360 if (checkBoolValue(val, &state)) {
5361 help = state;
5362 }
5363 if (v->config()->setValue(var, help)) {
5364 } else if (v->rendererConfig()->setValue(var, help)) {
5365 // No success? Go the old way
5366 } else if (var == QLatin1String("dynamic-word-wrap") && checkBoolValue(val, &state)) {
5367 v->config()->setDynWordWrap(state);
5368 } else if (var == QLatin1String("block-selection") && checkBoolValue(val, &state)) {
5369 v->setBlockSelection(state);
5370
5371 // else if ( var = "dynamic-word-wrap-indicators" )
5372 } else if (var == QLatin1String("icon-bar-color") && checkColorValue(val, c)) {
5373 v->rendererConfig()->setIconBarColor(c);
5374 }
5375 // RENDERER
5376 else if (var == QLatin1String("background-color") && checkColorValue(val, c)) {
5377 v->rendererConfig()->setBackgroundColor(c);
5378 } else if (var == QLatin1String("selection-color") && checkColorValue(val, c)) {
5379 v->rendererConfig()->setSelectionColor(c);
5380 } else if (var == QLatin1String("current-line-color") && checkColorValue(val, c)) {
5381 v->rendererConfig()->setHighlightedLineColor(c);
5382 } else if (var == QLatin1String("bracket-highlight-color") && checkColorValue(val, c)) {
5383 v->rendererConfig()->setHighlightedBracketColor(c);
5384 } else if (var == QLatin1String("word-wrap-marker-color") && checkColorValue(val, c)) {
5385 v->rendererConfig()->setWordWrapMarkerColor(c);
5386 } else if (var == QLatin1String("font") || (checkIntValue(val, &n) && n > 0 && var == QLatin1String("font-size"))) {
5387 QFont _f(v->renderer()->currentFont());
5388
5389 if (var == QLatin1String("font")) {
5390 _f.setFamily(val);
5391 _f.setFixedPitch(QFont(val).fixedPitch());
5392 } else {
5393 _f.setPointSize(n);
5394 }
5395
5396 v->rendererConfig()->setFont(_f);
5397 } else if (var == QLatin1String("scheme")) {
5398 v->rendererConfig()->setSchema(val);
5399 }
5400 }
5401}
5402
5403bool KTextEditor::DocumentPrivate::checkBoolValue(QString val, bool *result)
5404{
5405 val = val.trimmed().toLower();
5406 static const auto trueValues = {QLatin1String("1"), QLatin1String("on"), QLatin1String("true")};
5407 if (contains(trueValues, val)) {
5408 *result = true;
5409 return true;
5410 }
5411
5412 static const auto falseValues = {QLatin1String("0"), QLatin1String("off"), QLatin1String("false")};
5413 if (contains(falseValues, val)) {
5414 *result = false;
5415 return true;
5416 }
5417 return false;
5418}
5419
5420bool KTextEditor::DocumentPrivate::checkIntValue(const QString &val, int *result)
5421{
5422 bool ret(false);
5423 *result = val.toInt(&ret);
5424 return ret;
5425}
5426
5427bool KTextEditor::DocumentPrivate::checkColorValue(const QString &val, QColor &c)
5428{
5429 c = QColor::fromString(val);
5430 return c.isValid();
5431}
5432
5433// KTextEditor::variable
5435{
5436 auto it = m_storedVariables.find(name);
5437 if (it == m_storedVariables.end()) {
5438 return QString();
5439 }
5440 return it->second;
5441}
5442
5444{
5445 QString s = QStringLiteral("kate: ");
5446 s.append(name);
5447 s.append(QLatin1Char(' '));
5448 s.append(value);
5449 readVariableLine(s);
5450}
5451
5452// END
5453
5454void KTextEditor::DocumentPrivate::slotModOnHdDirty(const QString &path)
5455{
5456 if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskModified)) {
5457 m_modOnHd = true;
5458 m_modOnHdReason = OnDiskModified;
5459
5460 if (!m_modOnHdTimer.isActive()) {
5461 m_modOnHdTimer.start();
5462 }
5463 }
5464}
5465
5466void KTextEditor::DocumentPrivate::slotModOnHdCreated(const QString &path)
5467{
5468 if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskCreated)) {
5469 m_modOnHd = true;
5470 m_modOnHdReason = OnDiskCreated;
5471
5472 if (!m_modOnHdTimer.isActive()) {
5473 m_modOnHdTimer.start();
5474 }
5475 }
5476}
5477
5478void KTextEditor::DocumentPrivate::slotModOnHdDeleted(const QString &path)
5479{
5480 if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskDeleted)) {
5481 m_modOnHd = true;
5482 m_modOnHdReason = OnDiskDeleted;
5483
5484 if (!m_modOnHdTimer.isActive()) {
5485 m_modOnHdTimer.start();
5486 }
5487 }
5488}
5489
5490void KTextEditor::DocumentPrivate::slotDelayedHandleModOnHd()
5491{
5492 // compare git hash with the one we have (if we have one)
5493 const QByteArray oldDigest = checksum();
5494 if (!oldDigest.isEmpty() && !url().isEmpty() && url().isLocalFile()) {
5495 // if current checksum == checksum of new file => unmodified
5496 if (m_modOnHdReason != OnDiskDeleted && m_modOnHdReason != OnDiskCreated && createDigest() && oldDigest == checksum()) {
5497 m_modOnHd = false;
5498 m_modOnHdReason = OnDiskUnmodified;
5499 m_prevModOnHdReason = OnDiskUnmodified;
5500 }
5501
5502 // if still modified, try to take a look at git
5503 // skip that, if document is modified!
5504 // only do that, if the file is still there, else reload makes no sense!
5505 // we have a config option to disable this
5506 if (m_modOnHd && !isModified() && QFile::exists(url().toLocalFile())
5507 && config()->value(KateDocumentConfig::AutoReloadIfStateIsInVersionControl).toBool()) {
5508 // we only want to use git from PATH, cache this
5509 static const QString fullGitPath = QStandardPaths::findExecutable(QStringLiteral("git"));
5510 if (!fullGitPath.isEmpty()) {
5511 QProcess git;
5512 const QStringList args{QStringLiteral("cat-file"), QStringLiteral("-e"), QString::fromUtf8(oldDigest.toHex())};
5513 git.setWorkingDirectory(url().adjusted(QUrl::RemoveFilename).toLocalFile());
5514 git.start(fullGitPath, args);
5515 if (git.waitForStarted()) {
5516 git.closeWriteChannel();
5517 if (git.waitForFinished()) {
5518 if (git.exitCode() == 0) {
5519 // this hash exists still in git => just reload
5520 m_modOnHd = false;
5521 m_modOnHdReason = OnDiskUnmodified;
5522 m_prevModOnHdReason = OnDiskUnmodified;
5523 documentReload();
5524 }
5525 }
5526 }
5527 }
5528 }
5529 }
5530
5531 // emit our signal to the outside!
5532 Q_EMIT modifiedOnDisk(this, m_modOnHd, m_modOnHdReason);
5533}
5534
5536{
5537 return m_buffer->digest();
5538}
5539
5540bool KTextEditor::DocumentPrivate::createDigest()
5541{
5542 QByteArray digest;
5543
5544 if (url().isLocalFile()) {
5545 QFile f(url().toLocalFile());
5546 if (f.open(QIODevice::ReadOnly)) {
5547 // init the hash with the git header
5549 const QString header = QStringLiteral("blob %1").arg(f.size());
5550 crypto.addData(QByteArray(header.toLatin1() + '\0'));
5551 crypto.addData(&f);
5552 digest = crypto.result();
5553 }
5554 }
5555
5556 // set new digest
5557 m_buffer->setDigest(digest);
5558 return !digest.isEmpty();
5559}
5560
5561QString KTextEditor::DocumentPrivate::reasonedMOHString() const
5562{
5563 // squeeze path
5564 const QString str = KStringHandler::csqueeze(url().toDisplayString(QUrl::PreferLocalFile));
5565
5566 switch (m_modOnHdReason) {
5567 case OnDiskModified:
5568 return i18n("The file '%1' was modified on disk.", str);
5569 break;
5570 case OnDiskCreated:
5571 return i18n("The file '%1' was created on disk.", str);
5572 break;
5573 case OnDiskDeleted:
5574 return i18n("The file '%1' was deleted or moved on disk.", str);
5575 break;
5576 default:
5577 return QString();
5578 }
5579 Q_UNREACHABLE();
5580 return QString();
5581}
5582
5583void KTextEditor::DocumentPrivate::removeTrailingSpacesAndAddNewLineAtEof()
5584{
5585 // skip all work if the user doesn't want any adjustments
5586 const int remove = config()->removeSpaces();
5587 const bool newLineAtEof = config()->newLineAtEof();
5588 if (remove == 0 && !newLineAtEof) {
5589 return;
5590 }
5591
5592 // temporarily disable static word wrap (see bug #328900)
5593 const bool wordWrapEnabled = config()->wordWrap();
5594 if (wordWrapEnabled) {
5595 setWordWrap(false);
5596 }
5597
5598 editStart();
5599
5600 // handle trailing space striping if needed
5601 const int lines = this->lines();
5602 if (remove != 0) {
5603 for (int line = 0; line < lines; ++line) {
5604 Kate::TextLine textline = plainKateTextLine(line);
5605
5606 // remove trailing spaces in entire document, remove = 2
5607 // remove trailing spaces of touched lines, remove = 1
5608 // remove trailing spaces of lines saved on disk, remove = 1
5609 if (remove == 2 || textline.markedAsModified() || textline.markedAsSavedOnDisk()) {
5610 const int p = textline.lastChar() + 1;
5611 const int l = textline.length() - p;
5612 if (l > 0) {
5613 editRemoveText(line, p, l);
5614 }
5615 }
5616 }
5617 }
5618
5619 // add a trailing empty line if we want a final line break
5620 // do we need to add a trailing newline char?
5621 if (newLineAtEof) {
5622 Q_ASSERT(lines > 0);
5623 const auto length = lineLength(lines - 1);
5624 if (length > 0) {
5625 // ensure the cursor is not wrapped to the next line if at the end of the document
5626 // see bug 453252
5627 const auto oldEndOfDocumentCursor = documentEnd();
5628 std::vector<KTextEditor::ViewPrivate *> viewsToRestoreCursors;
5629 for (auto view : std::as_const(m_views)) {
5630 auto v = static_cast<ViewPrivate *>(view);
5631 if (v->cursorPosition() == oldEndOfDocumentCursor) {
5632 viewsToRestoreCursors.push_back(v);
5633 }
5634 }
5635
5636 // wrap the last line, this might move the cursor
5637 editWrapLine(lines - 1, length);
5638
5639 // undo cursor moving
5640 for (auto v : viewsToRestoreCursors) {
5641 v->setCursorPosition(oldEndOfDocumentCursor);
5642 }
5643 }
5644 }
5645
5646 editEnd();
5647
5648 // enable word wrap again, if it was enabled (see bug #328900)
5649 if (wordWrapEnabled) {
5650 setWordWrap(true); // see begin of this function
5651 }
5652}
5653
5655{
5656 editStart();
5657 const int lines = this->lines();
5658 for (int line = 0; line < lines; ++line) {
5659 const Kate::TextLine textline = plainKateTextLine(line);
5660 const int p = textline.lastChar() + 1;
5661 const int l = textline.length() - p;
5662 if (l > 0) {
5663 editRemoveText(line, p, l);
5664 }
5665 }
5666 editEnd();
5667}
5668
5670{
5671 if (user || !m_fileTypeSetByUser) {
5672 if (newType.isEmpty()) {
5673 return false;
5674 }
5675 KateFileType fileType = KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType);
5676 // if the mode "newType" does not exist
5677 if (fileType.name.isEmpty()) {
5678 return false;
5679 }
5680
5681 // remember that we got set by user
5682 m_fileTypeSetByUser = user;
5683
5684 m_fileType = newType;
5685
5686 m_config->configStart();
5687
5688 // NOTE: if the user changes the Mode, the Highlighting also changes.
5689 // m_hlSetByUser avoids resetting the highlight when saving the document, if
5690 // the current hl isn't stored (eg, in sftp:// or fish:// files) (see bug #407763)
5691 if ((user || !m_hlSetByUser) && !fileType.hl.isEmpty()) {
5692 int hl(KateHlManager::self()->nameFind(fileType.hl));
5693
5694 if (hl >= 0) {
5695 m_buffer->setHighlight(hl);
5696 }
5697 }
5698
5699 // set the indentation mode, if any in the mode...
5700 // and user did not set it before!
5701 // NOTE: KateBuffer::setHighlight() also sets the indentation.
5702 if (!m_indenterSetByUser && !fileType.indenter.isEmpty()) {
5703 config()->setIndentationMode(fileType.indenter);
5704 }
5705
5706 // views!
5707 for (auto view : std::as_const(m_views)) {
5708 auto v = static_cast<ViewPrivate *>(view);
5709 v->config()->configStart();
5710 v->rendererConfig()->configStart();
5711 }
5712
5713 bool bom_settings = false;
5714 if (m_bomSetByUser) {
5715 bom_settings = m_config->bom();
5716 }
5717 readVariableLine(fileType.varLine);
5718 if (m_bomSetByUser) {
5719 m_config->setBom(bom_settings);
5720 }
5721 m_config->configEnd();
5722 for (auto view : std::as_const(m_views)) {
5723 auto v = static_cast<ViewPrivate *>(view);
5724 v->config()->configEnd();
5725 v->rendererConfig()->configEnd();
5726 }
5727 }
5728
5729 // fixme, make this better...
5730 Q_EMIT modeChanged(this);
5731 return true;
5732}
5733
5734void KTextEditor::DocumentPrivate::slotQueryClose_save(bool *handled, bool *abortClosing)
5735{
5736 *handled = true;
5737 *abortClosing = true;
5738 if (url().isEmpty()) {
5739 const QUrl res = getSaveFileUrl(i18n("Save File"));
5740 if (res.isEmpty()) {
5741 *abortClosing = true;
5742 return;
5743 }
5744 saveAs(res);
5745 *abortClosing = false;
5746 } else {
5747 save();
5748 *abortClosing = false;
5749 }
5750}
5751
5752// BEGIN KTextEditor::ConfigInterface
5753
5754// BEGIN ConfigInterface stff
5756{
5757 // expose all internally registered keys of the KateDocumentConfig
5758 return m_config->configKeys();
5759}
5760
5762{
5763 // just dispatch to internal key => value lookup
5764 return m_config->value(key);
5765}
5766
5768{
5769 // just dispatch to internal key + value set
5770 m_config->setValue(key, value);
5771}
5772
5773// END KTextEditor::ConfigInterface
5774
5779
5780bool KTextEditor::DocumentPrivate::replaceText(KTextEditor::Range range, const QString &s, bool block)
5781{
5782 // TODO more efficient?
5783 editStart();
5784 bool changed = removeText(range, block);
5785 changed |= insertText(range.start(), s, block);
5786 editEnd();
5787 return changed;
5788}
5789
5790KateHighlighting *KTextEditor::DocumentPrivate::highlight() const
5791{
5792 return m_buffer->highlight();
5793}
5794
5796{
5797 m_buffer->ensureHighlighted(i);
5798 return m_buffer->plainLine(i);
5799}
5800
5802{
5803 return m_buffer->plainLine(i);
5804}
5805
5806bool KTextEditor::DocumentPrivate::isEditRunning() const
5807{
5808 return editIsRunning;
5809}
5810
5811void KTextEditor::DocumentPrivate::setUndoMergeAllEdits(bool merge)
5812{
5813 if (merge && m_undoMergeAllEdits) {
5814 // Don't add another undo safe point: it will override our current one,
5815 // meaning we'll need two undo's to get back there - which defeats the object!
5816 return;
5817 }
5818 m_undoManager->undoSafePoint();
5819 m_undoManager->setAllowComplexMerge(merge);
5820 m_undoMergeAllEdits = merge;
5821}
5822
5823// BEGIN KTextEditor::MovingInterface
5828
5832{
5833 return new Kate::TextRange(m_buffer, range, insertBehaviors, emptyBehavior);
5834}
5835
5837{
5838 return m_buffer->history().revision();
5839}
5840
5842{
5843 return m_buffer->history().lastSavedRevision();
5844}
5845
5847{
5848 m_buffer->history().lockRevision(revision);
5849}
5850
5852{
5853 m_buffer->history().unlockRevision(revision);
5854}
5855
5857 int &column,
5859 qint64 fromRevision,
5860 qint64 toRevision)
5861{
5862 m_buffer->history().transformCursor(line, column, insertBehavior, fromRevision, toRevision);
5863}
5864
5867 qint64 fromRevision,
5868 qint64 toRevision)
5869{
5870 int line = cursor.line();
5871 int column = cursor.column();
5872 m_buffer->history().transformCursor(line, column, insertBehavior, fromRevision, toRevision);
5873 cursor.setPosition(line, column);
5874}
5875
5879 qint64 fromRevision,
5880 qint64 toRevision)
5881{
5882 m_buffer->history().transformRange(range, insertBehaviors, emptyBehavior, fromRevision, toRevision);
5883}
5884
5885// END
5886
5887// BEGIN KTextEditor::AnnotationInterface
5889{
5890 KTextEditor::AnnotationModel *oldmodel = m_annotationModel;
5891 m_annotationModel = model;
5892 Q_EMIT annotationModelChanged(oldmodel, m_annotationModel);
5893}
5894
5896{
5897 return m_annotationModel;
5898}
5899// END KTextEditor::AnnotationInterface
5900
5901// TAKEN FROM kparts.h
5902bool KTextEditor::DocumentPrivate::queryClose()
5903{
5904 if (!isModified() || (isEmpty() && url().isEmpty())) {
5905 return true;
5906 }
5907
5908 QString docName = documentName();
5909
5910 int res = KMessageBox::warningTwoActionsCancel(dialogParent(),
5911 i18n("The document \"%1\" has been modified.\n"
5912 "Do you want to save your changes or discard them?",
5913 docName),
5914 i18n("Close Document"),
5917
5918 bool abortClose = false;
5919 bool handled = false;
5920
5921 switch (res) {
5923 sigQueryClose(&handled, &abortClose);
5924 if (!handled) {
5925 if (url().isEmpty()) {
5926 const QUrl url = getSaveFileUrl(i18n("Save File"));
5927 if (url.isEmpty()) {
5928 return false;
5929 }
5930
5931 saveAs(url);
5932 } else {
5933 save();
5934 }
5935 } else if (abortClose) {
5936 return false;
5937 }
5938 return waitSaveComplete();
5940 return true;
5941 default: // case KMessageBox::Cancel :
5942 return false;
5943 }
5944}
5945
5946void KTextEditor::DocumentPrivate::slotStarted(KIO::Job *job)
5947{
5948 // if we are idle before, we are now loading!
5949 if (m_documentState == DocumentIdle) {
5950 m_documentState = DocumentLoading;
5951 }
5952
5953 // if loading:
5954 // - remember pre loading read-write mode
5955 // if remote load:
5956 // - set to read-only
5957 // - trigger possible message
5958 if (m_documentState == DocumentLoading) {
5959 // remember state
5960 m_readWriteStateBeforeLoading = isReadWrite();
5961
5962 // perhaps show loading message, but wait one second
5963 if (job) {
5964 // only read only if really remote file!
5965 setReadWrite(false);
5966
5967 // perhaps some message about loading in one second!
5968 // remember job pointer, we want to be able to kill it!
5969 m_loadingJob = job;
5970 QTimer::singleShot(1000, this, SLOT(slotTriggerLoadingMessage()));
5971 }
5972 }
5973}
5974
5975void KTextEditor::DocumentPrivate::slotCompleted()
5976{
5977 // if were loading, reset back to old read-write mode before loading
5978 // and kill the possible loading message
5979 if (m_documentState == DocumentLoading) {
5980 setReadWrite(m_readWriteStateBeforeLoading);
5981 delete m_loadingMessage;
5982 m_reloading = false;
5983 }
5984
5985 // Emit signal that we saved the document, if needed
5986 if (m_documentState == DocumentSaving || m_documentState == DocumentSavingAs) {
5987 Q_EMIT documentSavedOrUploaded(this, m_documentState == DocumentSavingAs);
5988 }
5989
5990 // back to idle mode
5991 m_documentState = DocumentIdle;
5992}
5993
5994void KTextEditor::DocumentPrivate::slotCanceled()
5995{
5996 // if were loading, reset back to old read-write mode before loading
5997 // and kill the possible loading message
5998 if (m_documentState == DocumentLoading) {
5999 setReadWrite(m_readWriteStateBeforeLoading);
6000 delete m_loadingMessage;
6001 m_reloading = false;
6002
6003 if (!m_openingError) {
6004 showAndSetOpeningErrorAccess();
6005 }
6006
6007 updateDocName();
6008 }
6009
6010 // back to idle mode
6011 m_documentState = DocumentIdle;
6012}
6013
6014void KTextEditor::DocumentPrivate::slotTriggerLoadingMessage()
6015{
6016 // no longer loading?
6017 // no message needed!
6018 if (m_documentState != DocumentLoading) {
6019 return;
6020 }
6021
6022 // create message about file loading in progress
6023 delete m_loadingMessage;
6024 m_loadingMessage =
6025 new KTextEditor::Message(i18n("The file <a href=\"%1\">%2</a> is still loading.", url().toDisplayString(QUrl::PreferLocalFile), url().fileName()));
6026 m_loadingMessage->setPosition(KTextEditor::Message::TopInView);
6027
6028 // if around job: add cancel action
6029 if (m_loadingJob) {
6030 QAction *cancel = new QAction(i18n("&Abort Loading"), nullptr);
6031 connect(cancel, &QAction::triggered, this, &KTextEditor::DocumentPrivate::slotAbortLoading);
6032 m_loadingMessage->addAction(cancel);
6033 }
6034
6035 // really post message
6036 postMessage(m_loadingMessage);
6037}
6038
6039void KTextEditor::DocumentPrivate::slotAbortLoading()
6040{
6041 // no job, no work
6042 if (!m_loadingJob) {
6043 return;
6044 }
6045
6046 // abort loading if any job
6047 // signal results!
6048 m_loadingJob->kill(KJob::EmitResult);
6049 m_loadingJob = nullptr;
6050}
6051
6052void KTextEditor::DocumentPrivate::slotUrlChanged(const QUrl &url)
6053{
6054 Q_UNUSED(url);
6055 updateDocName();
6056 Q_EMIT documentUrlChanged(this);
6057}
6058
6059bool KTextEditor::DocumentPrivate::save()
6060{
6061 // no double save/load
6062 // we need to allow DocumentPreSavingAs here as state, as save is called in saveAs!
6063 if ((m_documentState != DocumentIdle) && (m_documentState != DocumentPreSavingAs)) {
6064 return false;
6065 }
6066
6067 // if we are idle, we are now saving
6068 if (m_documentState == DocumentIdle) {
6069 m_documentState = DocumentSaving;
6070 } else {
6071 m_documentState = DocumentSavingAs;
6072 }
6073
6074 // let anyone listening know that we are going to save
6075 Q_EMIT aboutToSave(this);
6076
6077 // call back implementation for real work
6079}
6080
6081bool KTextEditor::DocumentPrivate::saveAs(const QUrl &url)
6082{
6083 // abort on bad URL
6084 // that is done in saveAs below, too
6085 // but we must check it here already to avoid messing up
6086 // as no signals will be send, then
6087 if (!url.isValid()) {
6088 return false;
6089 }
6090
6091 // no double save/load
6092 if (m_documentState != DocumentIdle) {
6093 return false;
6094 }
6095
6096 // we enter the pre save as phase
6097 m_documentState = DocumentPreSavingAs;
6098
6099 // call base implementation for real work
6101}
6102
6103QUrl KTextEditor::DocumentPrivate::startUrlForFileDialog()
6104{
6105 QUrl startUrl = url();
6106 if (startUrl.isValid()) {
6107 // for remote files we cut the file name to avoid confusion if it is some directory or not, see bug 454648
6108 if (!startUrl.isLocalFile()) {
6109 startUrl = startUrl.adjusted(QUrl::RemoveFilename);
6110 }
6111 }
6112
6113 // 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
6114 else if (auto mainWindow = KTextEditor::Editor::instance()->application()->activeMainWindow(); mainWindow) {
6115 const auto views = mainWindow->views();
6116 for (auto view : views) {
6117 if (view->document()->url().isValid()) {
6118 // as we here pick some perhaps unrelated file, always cut the file name
6119 startUrl = view->document()->url().adjusted(QUrl::RemoveFilename);
6120 break;
6121 }
6122 }
6123 }
6124
6125 return startUrl;
6126}
6127
6128QString KTextEditor::DocumentPrivate::defaultDictionary() const
6129{
6130 return m_defaultDictionary;
6131}
6132
6133QList<QPair<KTextEditor::MovingRange *, QString>> KTextEditor::DocumentPrivate::dictionaryRanges() const
6134{
6135 return m_dictionaryRanges;
6136}
6137
6138void KTextEditor::DocumentPrivate::clearDictionaryRanges()
6139{
6140 for (auto i = m_dictionaryRanges.cbegin(); i != m_dictionaryRanges.cend(); ++i) {
6141 delete (*i).first;
6142 }
6143 m_dictionaryRanges.clear();
6144 if (m_onTheFlyChecker) {
6145 m_onTheFlyChecker->refreshSpellCheck();
6146 }
6147 Q_EMIT dictionaryRangesPresent(false);
6148}
6149
6150void KTextEditor::DocumentPrivate::setDictionary(const QString &newDictionary, KTextEditor::Range range, bool blockmode)
6151{
6152 if (blockmode) {
6153 for (int i = range.start().line(); i <= range.end().line(); ++i) {
6154 setDictionary(newDictionary, rangeOnLine(range, i));
6155 }
6156 } else {
6157 setDictionary(newDictionary, range);
6158 }
6159
6160 Q_EMIT dictionaryRangesPresent(!m_dictionaryRanges.isEmpty());
6161}
6162
6163void KTextEditor::DocumentPrivate::setDictionary(const QString &newDictionary, KTextEditor::Range range)
6164{
6165 KTextEditor::Range newDictionaryRange = range;
6166 if (!newDictionaryRange.isValid() || newDictionaryRange.isEmpty()) {
6167 return;
6168 }
6169 QList<QPair<KTextEditor::MovingRange *, QString>> newRanges;
6170 // all ranges is 'm_dictionaryRanges' are assumed to be mutually disjoint
6171 for (auto i = m_dictionaryRanges.begin(); i != m_dictionaryRanges.end();) {
6172 qCDebug(LOG_KTE) << "new iteration" << newDictionaryRange;
6173 if (newDictionaryRange.isEmpty()) {
6174 break;
6175 }
6176 QPair<KTextEditor::MovingRange *, QString> pair = *i;
6177 QString dictionarySet = pair.second;
6178 KTextEditor::MovingRange *dictionaryRange = pair.first;
6179 qCDebug(LOG_KTE) << *dictionaryRange << dictionarySet;
6180 if (dictionaryRange->contains(newDictionaryRange) && newDictionary == dictionarySet) {
6181 qCDebug(LOG_KTE) << "dictionaryRange contains newDictionaryRange";
6182 return;
6183 }
6184 if (newDictionaryRange.contains(*dictionaryRange)) {
6185 delete dictionaryRange;
6186 i = m_dictionaryRanges.erase(i);
6187 qCDebug(LOG_KTE) << "newDictionaryRange contains dictionaryRange";
6188 continue;
6189 }
6190
6191 KTextEditor::Range intersection = dictionaryRange->toRange().intersect(newDictionaryRange);
6192 if (!intersection.isEmpty() && intersection.isValid()) {
6193 if (dictionarySet == newDictionary) { // we don't have to do anything for 'intersection'
6194 // except cut off the intersection
6195 QList<KTextEditor::Range> remainingRanges = KateSpellCheckManager::rangeDifference(newDictionaryRange, intersection);
6196 Q_ASSERT(remainingRanges.size() == 1);
6197 newDictionaryRange = remainingRanges.first();
6198 ++i;
6199 qCDebug(LOG_KTE) << "dictionarySet == newDictionary";
6200 continue;
6201 }
6202 QList<KTextEditor::Range> remainingRanges = KateSpellCheckManager::rangeDifference(*dictionaryRange, intersection);
6203 for (auto j = remainingRanges.begin(); j != remainingRanges.end(); ++j) {
6204 KTextEditor::MovingRange *remainingRange = newMovingRange(*j, KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight);
6205 remainingRange->setFeedback(this);
6206 newRanges.push_back({remainingRange, dictionarySet});
6207 }
6208 i = m_dictionaryRanges.erase(i);
6209 delete dictionaryRange;
6210 } else {
6211 ++i;
6212 }
6213 }
6214 m_dictionaryRanges += newRanges;
6215 if (!newDictionaryRange.isEmpty() && !newDictionary.isEmpty()) { // we don't add anything for the default dictionary
6216 KTextEditor::MovingRange *newDictionaryMovingRange =
6218 newDictionaryMovingRange->setFeedback(this);
6219 m_dictionaryRanges.push_back({newDictionaryMovingRange, newDictionary});
6220 }
6221 if (m_onTheFlyChecker && !newDictionaryRange.isEmpty()) {
6222 m_onTheFlyChecker->refreshSpellCheck(newDictionaryRange);
6223 }
6224}
6225
6226void KTextEditor::DocumentPrivate::setDefaultDictionary(const QString &dict)
6227{
6228 if (m_defaultDictionary == dict) {
6229 return;
6230 }
6231
6232 m_defaultDictionary = dict;
6233
6234 if (m_onTheFlyChecker) {
6235 m_onTheFlyChecker->updateConfig();
6236 refreshOnTheFlyCheck();
6237 }
6238 Q_EMIT defaultDictionaryChanged(this);
6239}
6240
6241void KTextEditor::DocumentPrivate::onTheFlySpellCheckingEnabled(bool enable)
6242{
6243 if (isOnTheFlySpellCheckingEnabled() == enable) {
6244 return;
6245 }
6246
6247 if (enable) {
6248 Q_ASSERT(m_onTheFlyChecker == nullptr);
6249 m_onTheFlyChecker = new KateOnTheFlyChecker(this);
6250 } else {
6251 delete m_onTheFlyChecker;
6252 m_onTheFlyChecker = nullptr;
6253 }
6254
6255 for (auto view : std::as_const(m_views)) {
6256 static_cast<ViewPrivate *>(view)->reflectOnTheFlySpellCheckStatus(enable);
6257 }
6258}
6259
6260bool KTextEditor::DocumentPrivate::isOnTheFlySpellCheckingEnabled() const
6261{
6262 return m_onTheFlyChecker != nullptr;
6263}
6264
6265QString KTextEditor::DocumentPrivate::dictionaryForMisspelledRange(KTextEditor::Range range) const
6266{
6267 if (!m_onTheFlyChecker) {
6268 return QString();
6269 } else {
6270 return m_onTheFlyChecker->dictionaryForMisspelledRange(range);
6271 }
6272}
6273
6274void KTextEditor::DocumentPrivate::clearMisspellingForWord(const QString &word)
6275{
6276 if (m_onTheFlyChecker) {
6277 m_onTheFlyChecker->clearMisspellingForWord(word);
6278 }
6279}
6280
6281void KTextEditor::DocumentPrivate::refreshOnTheFlyCheck(KTextEditor::Range range)
6282{
6283 if (m_onTheFlyChecker) {
6284 m_onTheFlyChecker->refreshSpellCheck(range);
6285 }
6286}
6287
6289{
6290 deleteDictionaryRange(movingRange);
6291}
6292
6294{
6295 deleteDictionaryRange(movingRange);
6296}
6297
6298void KTextEditor::DocumentPrivate::deleteDictionaryRange(KTextEditor::MovingRange *movingRange)
6299{
6300 qCDebug(LOG_KTE) << "deleting" << movingRange;
6301
6302 auto finder = [=](const QPair<KTextEditor::MovingRange *, QString> &item) -> bool {
6303 return item.first == movingRange;
6304 };
6305
6306 auto it = std::find_if(m_dictionaryRanges.begin(), m_dictionaryRanges.end(), finder);
6307
6308 if (it != m_dictionaryRanges.end()) {
6309 m_dictionaryRanges.erase(it);
6310 delete movingRange;
6311 }
6312
6313 Q_ASSERT(std::find_if(m_dictionaryRanges.begin(), m_dictionaryRanges.end(), finder) == m_dictionaryRanges.end());
6314}
6315
6316bool KTextEditor::DocumentPrivate::containsCharacterEncoding(KTextEditor::Range range)
6317{
6318 KateHighlighting *highlighting = highlight();
6319
6320 const int rangeStartLine = range.start().line();
6321 const int rangeStartColumn = range.start().column();
6322 const int rangeEndLine = range.end().line();
6323 const int rangeEndColumn = range.end().column();
6324
6325 for (int line = range.start().line(); line <= rangeEndLine; ++line) {
6326 const Kate::TextLine textLine = kateTextLine(line);
6327 const int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0;
6328 const int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine.length();
6329 for (int col = startColumn; col < endColumn; ++col) {
6330 int attr = textLine.attribute(col);
6331 const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr);
6332 if (!prefixStore.findPrefix(textLine, col).isEmpty()) {
6333 return true;
6334 }
6335 }
6336 }
6337
6338 return false;
6339}
6340
6341int KTextEditor::DocumentPrivate::computePositionWrtOffsets(const OffsetList &offsetList, int pos)
6342{
6343 int previousOffset = 0;
6344 for (auto i = offsetList.cbegin(); i != offsetList.cend(); ++i) {
6345 if (i->first > pos) {
6346 break;
6347 }
6348 previousOffset = i->second;
6349 }
6350 return pos + previousOffset;
6351}
6352
6354 KTextEditor::DocumentPrivate::OffsetList &decToEncOffsetList,
6355 KTextEditor::DocumentPrivate::OffsetList &encToDecOffsetList)
6356{
6357 QString toReturn;
6358 KTextEditor::Cursor previous = range.start();
6359 int decToEncCurrentOffset = 0;
6360 int encToDecCurrentOffset = 0;
6361 int i = 0;
6362 int newI = 0;
6363
6364 KateHighlighting *highlighting = highlight();
6365 Kate::TextLine textLine;
6366
6367 const int rangeStartLine = range.start().line();
6368 const int rangeStartColumn = range.start().column();
6369 const int rangeEndLine = range.end().line();
6370 const int rangeEndColumn = range.end().column();
6371
6372 for (int line = range.start().line(); line <= rangeEndLine; ++line) {
6373 textLine = kateTextLine(line);
6374 int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0;
6375 int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine.length();
6376 for (int col = startColumn; col < endColumn;) {
6377 int attr = textLine.attribute(col);
6378 const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr);
6379 const QHash<QString, QChar> &characterEncodingsHash = highlighting->getCharacterEncodings(attr);
6380 QString matchingPrefix = prefixStore.findPrefix(textLine, col);
6381 if (!matchingPrefix.isEmpty()) {
6382 toReturn += text(KTextEditor::Range(previous, KTextEditor::Cursor(line, col)));
6383 const QChar &c = characterEncodingsHash.value(matchingPrefix);
6384 const bool isNullChar = c.isNull();
6385 if (!c.isNull()) {
6386 toReturn += c;
6387 }
6388 i += matchingPrefix.length();
6389 col += matchingPrefix.length();
6390 previous = KTextEditor::Cursor(line, col);
6391 decToEncCurrentOffset = decToEncCurrentOffset - (isNullChar ? 0 : 1) + matchingPrefix.length();
6392 encToDecCurrentOffset = encToDecCurrentOffset - matchingPrefix.length() + (isNullChar ? 0 : 1);
6393 newI += (isNullChar ? 0 : 1);
6394 decToEncOffsetList.push_back(QPair<int, int>(newI, decToEncCurrentOffset));
6395 encToDecOffsetList.push_back(QPair<int, int>(i, encToDecCurrentOffset));
6396 continue;
6397 }
6398 ++col;
6399 ++i;
6400 ++newI;
6401 }
6402 ++i;
6403 ++newI;
6404 }
6405 if (previous < range.end()) {
6406 toReturn += text(KTextEditor::Range(previous, range.end()));
6407 }
6408 return toReturn;
6409}
6410
6411void KTextEditor::DocumentPrivate::replaceCharactersByEncoding(KTextEditor::Range range)
6412{
6413 KateHighlighting *highlighting = highlight();
6414 Kate::TextLine textLine;
6415
6416 const int rangeStartLine = range.start().line();
6417 const int rangeStartColumn = range.start().column();
6418 const int rangeEndLine = range.end().line();
6419 const int rangeEndColumn = range.end().column();
6420
6421 for (int line = range.start().line(); line <= rangeEndLine; ++line) {
6422 textLine = kateTextLine(line);
6423 int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0;
6424 int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine.length();
6425 for (int col = startColumn; col < endColumn;) {
6426 int attr = textLine.attribute(col);
6427 const QHash<QChar, QString> &reverseCharacterEncodingsHash = highlighting->getReverseCharacterEncodings(attr);
6428 auto it = reverseCharacterEncodingsHash.find(textLine.at(col));
6429 if (it != reverseCharacterEncodingsHash.end()) {
6430 replaceText(KTextEditor::Range(line, col, line, col + 1), *it);
6431 col += (*it).length();
6432 continue;
6433 }
6434 ++col;
6435 }
6436 }
6437}
6438
6439//
6440// Highlighting information
6441//
6442
6444{
6445 return highlight()->getEmbeddedHighlightingModes();
6446}
6447
6449{
6450 return highlight()->higlightingModeForLocation(this, position);
6451}
6452
6453Kate::SwapFile *KTextEditor::DocumentPrivate::swapFile()
6454{
6455 return m_swapfile;
6456}
6457
6458/**
6459 * \return \c -1 if \c line or \c column invalid, otherwise one of
6460 * standard style attribute number
6461 */
6463{
6464 // Validate parameters to prevent out of range access
6465 if (line < 0 || line >= lines() || column < 0) {
6466 return KSyntaxHighlighting::Theme::TextStyle::Normal;
6467 }
6468
6469 // get highlighted line
6471
6472 // either get char attribute or attribute of context still active at end of line
6473 int attribute = 0;
6474 if (column < tl.length()) {
6475 attribute = tl.attribute(column);
6476 } else if (column == tl.length()) {
6477 if (!tl.attributesList().empty()) {
6478 attribute = tl.attributesList().back().attributeValue;
6479 } else {
6480 return KSyntaxHighlighting::Theme::TextStyle::Normal;
6481 }
6482 } else {
6483 return KSyntaxHighlighting::Theme::TextStyle::Normal;
6484 }
6485
6486 return highlight()->defaultStyleForAttribute(attribute);
6487}
6488
6489bool KTextEditor::DocumentPrivate::isComment(int line, int column)
6490{
6491 return defStyleNum(line, column) == KSyntaxHighlighting::Theme::TextStyle::Comment;
6492}
6493
6495{
6496 const int offset = down ? 1 : -1;
6497 const int lineCount = lines();
6498 while (startLine >= 0 && startLine < lineCount) {
6499 Kate::TextLine tl = m_buffer->plainLine(startLine);
6500 if (tl.markedAsModified() || tl.markedAsSavedOnDisk()) {
6501 return startLine;
6502 }
6503 startLine += offset;
6504 }
6505
6506 return -1;
6507}
6508
6509void KTextEditor::DocumentPrivate::setActiveTemplateHandler(KateTemplateHandler *handler)
6510{
6511 // delete any active template handler
6512 delete m_activeTemplateHandler.data();
6513 m_activeTemplateHandler = handler;
6514}
6515
6516// BEGIN KTextEditor::MessageInterface
6518{
6519 // no message -> cancel
6520 if (!message) {
6521 return false;
6522 }
6523
6524 // make sure the desired view belongs to this document
6525 if (message->view() && message->view()->document() != this) {
6526 qCWarning(LOG_KTE) << "trying to post a message to a view of another document:" << message->text();
6527 return false;
6528 }
6529
6530 message->setParent(this);
6531 message->setDocument(this);
6532
6533 // if there are no actions, add a close action by default if widget does not auto-hide
6534 if (message->actions().count() == 0 && message->autoHide() < 0) {
6535 QAction *closeAction = new QAction(QIcon::fromTheme(QStringLiteral("window-close")), i18n("&Close"), nullptr);
6536 closeAction->setToolTip(i18nc("Close the message being displayed", "Close message"));
6537 message->addAction(closeAction);
6538 }
6539
6540 // reparent actions, as we want full control over when they are deleted
6541 QList<std::shared_ptr<QAction>> managedMessageActions;
6542 const auto messageActions = message->actions();
6543 managedMessageActions.reserve(messageActions.size());
6544 for (QAction *action : messageActions) {
6545 action->setParent(nullptr);
6546 managedMessageActions.append(std::shared_ptr<QAction>(action));
6547 }
6548 m_messageHash.insert(message, managedMessageActions);
6549
6550 // post message to requested view, or to all views
6551 if (KTextEditor::ViewPrivate *view = qobject_cast<KTextEditor::ViewPrivate *>(message->view())) {
6552 view->postMessage(message, managedMessageActions);
6553 } else {
6554 for (auto view : std::as_const(m_views)) {
6555 static_cast<ViewPrivate *>(view)->postMessage(message, managedMessageActions);
6556 }
6557 }
6558
6559 // also catch if the user manually calls delete message
6560 connect(message, &Message::closed, this, &DocumentPrivate::messageDestroyed);
6561
6562 return true;
6563}
6564
6565void KTextEditor::DocumentPrivate::messageDestroyed(KTextEditor::Message *message)
6566{
6567 // KTE:Message is already in destructor
6568 Q_ASSERT(m_messageHash.contains(message));
6569 m_messageHash.remove(message);
6570}
6571// END KTextEditor::MessageInterface
6572
6573#include "moc_katedocument.cpp"
bool hasKey(const char *key) const
void writeEntry(const char *key, const char *value, WriteConfigFlags pFlags=Normal)
void deleteGroup(const QString &group, WriteConfigFlags flags=Normal)
QString readEntry(const char *key, const char *aDefault=nullptr) const
void addFile(const QString &file)
void removeFile(const QString &file)
void deleted(const QString &path)
void dirty(const QString &path)
void created(const QString &path)
const UDSEntry & statResult() const
bool exec()
void result(KJob *job)
void finished(KJob *job)
virtual Q_SCRIPTABLE void start()=0
Ptr findByDevice(const QString &device) const
static List currentMountPoints(DetailsNeededFlags infoNeeded=BasicInfoNeeded)
static KNetworkMounts * self()
virtual QWidget * widget()
virtual void setWidget(QWidget *widget)
void setAutoDeletePart(bool autoDeletePart)
void setAutoDeleteWidget(bool autoDeleteWidget)
virtual bool openUrl(const QUrl &url)
void urlChanged(const QUrl &url)
OpenUrlArguments arguments() const
void canceled(const QString &errMsg)
void started(KIO::Job *job)
QString localFilePath() const
virtual void setReadWrite(bool readwrite=true)
virtual bool saveAs(const QUrl &url)
bool isModified() const
virtual bool save()
void sigQueryClose(bool *handled, bool *abortClosing)
bool isReadWrite() const
bool closeUrl() override
An model for providing line annotation information.
KTextEditor::MainWindow * activeMainWindow()
Accessor to the active main window.
The Cursor represents a position in a Document.
Definition cursor.h:75
constexpr int column() const noexcept
Retrieve the column on which this cursor is situated.
Definition cursor.h:192
void setColumn(int column) noexcept
Set the cursor column to column.
Definition cursor.h:201
void setPosition(Cursor position) noexcept
Set the current cursor position to position.
Definition cursor.h:150
constexpr bool isValid() const noexcept
Returns whether the current position of this cursor is a valid position (line + column must both be >...
Definition cursor.h:102
static constexpr Cursor start() noexcept
Returns a cursor representing the start of any document - i.e., line 0, column 0.
Definition cursor.h:120
void setLine(int line) noexcept
Set the cursor line to line.
Definition cursor.h:183
constexpr int line() const noexcept
Retrieve the line on which this cursor is situated.
Definition cursor.h:174
static constexpr Cursor invalid() noexcept
Returns an invalid cursor.
Definition cursor.h:112
Backend of KTextEditor::Document related public KTextEditor interfaces.
bool setEncoding(const QString &e) override
Set the encoding for this document.
bool saveFile() override
save the file obtained by the kparts framework the framework abstracts the uploading of remote files
void transformRange(KTextEditor::Range &range, KTextEditor::MovingRange::InsertBehaviors insertBehaviors, KTextEditor::MovingRange::EmptyBehavior emptyBehavior, qint64 fromRevision, qint64 toRevision=-1) override
Transform a range from one revision to an other.
void lockRevision(qint64 revision) override
Lock a revision, this will keep it around until released again.
void recoverData() override
If recover data is available, calling recoverData() will trigger the recovery of the data.
QString highlightingModeSection(int index) const override
Returns the name of the section for a highlight given its index in the highlight list (as returned by...
bool handleMarkContextMenu(int line, QPoint position)
Returns true if the context-menu event should not further be processed.
void readSessionConfig(const KConfigGroup &config, const QSet< QString > &flags=QSet< QString >()) override
Read session settings from the given config.
QStringList highlightingModes() const override
Return a list of the names of all possible modes.
bool documentReload() override
Reloads the current document from disk if possible.
void setModifiedOnDisk(ModifiedOnDiskReason reason) override
Set the document's modified-on-disk state to reason.
KTextEditor::Cursor documentEnd() const override
End position of the document.
QStringList configKeys() const override
Get a list of all available keys.
bool postMessage(KTextEditor::Message *message) override
Post message to the Document and its Views.
int lineLengthLimit() const
reads the line length limit from config, if it is not overridden
virtual void slotModifiedOnDisk(KTextEditor::View *v=nullptr)
Ask the user what to do, if the file has been modified on disk.
void joinLines(uint first, uint last)
Unwrap a range of lines.
KTextEditor::MovingRange * newMovingRange(KTextEditor::Range range, KTextEditor::MovingRange::InsertBehaviors insertBehaviors=KTextEditor::MovingRange::DoNotExpand, KTextEditor::MovingRange::EmptyBehavior emptyBehavior=KTextEditor::MovingRange::AllowEmpty) override
Create a new moving range for this document.
virtual QString variable(const QString &name) const
Returns the value for the variable name.
QString text(KTextEditor::Range range, bool blockwise=false) const override
Get the document content within the given range.
bool setHighlightingMode(const QString &name) override
Set the current mode of the document by giving its name.
QStringList textLines(KTextEditor::Range range, bool block=false) const override
Get the document content within the given range.
void clearEditingPosStack()
Removes all the elements in m_editingStack of the respective document.
void transform(KTextEditor::ViewPrivate *view, KTextEditor::Cursor, TextTransform)
Handling uppercase, lowercase and capitalize for the view.
void rangeInvalid(KTextEditor::MovingRange *movingRange) override
The range is now invalid (ie.
void discardDataRecovery() override
If recover data is available, calling discardDataRecovery() will discard the recover data and the rec...
bool updateFileType(const QString &newType, bool user=false)
bool isLineModified(int line) const override
Check whether line currently contains unsaved data.
bool setMode(const QString &name) override
Set the current mode of the document by giving its name.
qsizetype totalCharacters() const override
Get the count of characters in the document.
QString highlightingMode() const override
Return the name of the currently used mode.
QString line(int line) const override
Get a single text line.
KTextEditor::AnnotationModel * annotationModel() const override
returns the currently set AnnotationModel or 0 if there's none set
QString markDescription(Document::MarkTypes) const override
Get the mark's description to text.
qint64 revision() const override
Current revision.
QString mimeType() override
Tries to detect mime-type based on file name and content of buffer.
KTextEditor::MovingCursor * newMovingCursor(KTextEditor::Cursor position, KTextEditor::MovingCursor::InsertBehavior insertBehavior=KTextEditor::MovingCursor::MoveOnInsert) override
Create a new moving cursor for this document.
QChar characterAt(KTextEditor::Cursor position) const override
Get the character at text position cursor.
qsizetype cursorToOffset(KTextEditor::Cursor c) const override
Retrives the offset for the given cursor position NOTE: It will return -1 if the cursor was invalid o...
QStringList modes() const override
Return a list of the names of all possible modes.
void setAnnotationModel(KTextEditor::AnnotationModel *model) override
Sets a new AnnotationModel for this document to provide annotation information for each line.
bool editUnWrapLine(int line, bool removeLine=true, int length=0)
Unwrap line.
bool editInsertText(int line, int col, const QString &s, bool notify=true)
Add a string in the given line/column.
int lastLine() const
gets the last line number (lines() - 1)
KTextEditor::Cursor offsetToCursor(qsizetype offset) const override
Retrives the cursor position for given offset NOTE: It will return an invalid cursor(-1,...
bool openFile() override
open the file obtained by the kparts framework the framework abstracts the loading of remote files
bool isLineSaved(int line) const override
Check whether line currently contains only saved text.
QWidget * widget() override
void writeSessionConfig(KConfigGroup &config, const QSet< QString > &flags=QSet< QString >()) override
Write session settings to the config.
QByteArray checksum() const override
Returns a git compatible sha1 checksum of this document on disk.
bool isLineTouched(int line) const override
Check whether line was touched since the file was opened.
void rangeEmpty(KTextEditor::MovingRange *movingRange) override
The range is now empty (ie.
QString text() const override
Get the document content.
int lines() const override
Get the count of lines of the document.
bool handleMarkClick(int line)
Returns true if the click on the mark should not be further processed.
KTextEditor::View * createView(QWidget *parent, KTextEditor::MainWindow *mainWindow=nullptr) override
Create a new view attached to parent.
bool isDataRecoveryAvailable() const override
Returns whether a recovery is available for the current document.
void bomSetByUser()
Set that the BOM marker is forced via the tool menu.
void textRemoved(KTextEditor::Document *document, KTextEditor::Range range, const QString &oldText)
The document emits this signal whenever range was removed, i.e.
bool editStart()
Enclose editor actions with editStart() and editEnd() to group them.
KTextEditor::Cursor lastEditingPosition(EditingPositionKind nextOrPrevious, KTextEditor::Cursor)
Returns the next or previous position cursor in this document from the stack depending on the argumen...
void setConfigValue(const QString &key, const QVariant &value) override
Set a the key's value to value.
KSyntaxHighlighting::Theme::TextStyle defStyleNum(int line, int column)
KateDocumentConfig * config()
Configuration.
void setModifiedOnDiskWarning(bool on) override
Control, whether the editor should show a warning dialog whenever a file was modified on disk.
QIcon markIcon(Document::MarkTypes markType) const override
Get the mark's icon.
KSyntaxHighlighting::Theme::TextStyle defaultStyleAt(KTextEditor::Cursor position) const override
Get the default style of the character located at position.
bool editWrapLine(int line, int col, bool newLine=true, bool *newLineAdded=nullptr, bool notify=true)
Wrap line.
void typeChars(KTextEditor::ViewPrivate *view, QString chars)
Type chars in a view.
QString highlightingModeAt(KTextEditor::Cursor position) override
Get the highlight mode used at a given position in the document.
QVariant configValue(const QString &key) override
Get a value for the key.
bool wrapText(int startLine, int endLine)
Warp a line.
bool editRemoveText(int line, int col, int len)
Remove a string in the given line/column.
qint64 lastSavedRevision() const override
Last revision the buffer got successful saved.
void setDontChangeHlOnSave()
allow to mark, that we changed hl on user wish and should not reset it atm used for the user visible ...
QString decodeCharacters(KTextEditor::Range range, KTextEditor::DocumentPrivate::OffsetList &decToEncOffsetList, KTextEditor::DocumentPrivate::OffsetList &encToDecOffsetList)
The first OffsetList is from decoded to encoded, and the second OffsetList from encoded to decoded.
void saveEditingPositions(const KTextEditor::Cursor cursor)
Saves the editing positions into the stack.
uint editableMarks() const override
Get, which marks can be toggled by the user.
Kate::TextLine plainKateTextLine(int i)
Return line lineno.
uint mark(int line) override
Get all marks set on the line.
void unlockRevision(qint64 revision) override
Release a revision.
QString mode() const override
Return the name of the currently used mode.
bool isValidTextPosition(KTextEditor::Cursor cursor) const override
Get whether cursor is a valid text position.
void removeAllTrailingSpaces()
This function doesn't check for config and is available for use all the time via an action.
void transformCursor(KTextEditor::Cursor &cursor, KTextEditor::MovingCursor::InsertBehavior insertBehavior, qint64 fromRevision, qint64 toRevision=-1) override
Transform a cursor from one revision to an other.
QString modeSection(int index) const override
Returns the name of the section for a mode given its index in the highlight list (as returned by mode...
bool editMarkLineAutoWrapped(int line, bool autowrapped)
Mark line as autowrapped.
bool editInsertLine(int line, const QString &s, bool notify=true)
Insert a string at the given line.
QStringList embeddedHighlightingModes() const override
Get all available highlighting modes for the current document.
bool editRemoveLine(int line)
Remove a line.
void textInsertedRange(KTextEditor::Document *document, KTextEditor::Range range)
The document emits this signal whenever text was inserted.
bool isEditingTransactionRunning() const override
Check whether an editing transaction is currently running.
Kate::TextLine kateTextLine(int i)
Same as plainKateTextLine(), except that it is made sure the line is highlighted.
QString encoding() const override
Get the current chosen encoding.
bool editEnd()
End a editor operation.
virtual void setVariable(const QString &name, const QString &value)
Set the variable name to value.
int findTouchedLine(int startLine, bool down)
Find the next modified/saved line, starting at startLine.
QString wordAt(KTextEditor::Cursor cursor) const override
Get the word at the text position cursor.
int lineLength(int line) const override
Get the length of a given line in characters.
KTextEditor::Range wordRangeAt(KTextEditor::Cursor cursor) const override
Get the text range for the word located under the text position cursor.
const QHash< int, KTextEditor::Mark * > & marks() override
Get a hash holding all marks in the document.
void removeView(KTextEditor::View *)
removes the view from the list of views.
bool wrapParagraph(int first, int last)
Wrap lines touched by the selection with respect of existing paragraphs.
void documentNameChanged(KTextEditor::Document *document)
This signal is emitted whenever the document name changes.
void textChanged(KTextEditor::Document *document)
The document emits this signal whenever its text changes.
void marksChanged(KTextEditor::Document *document)
The document emits this signal whenever a mark mask changed.
void markClicked(KTextEditor::Document *document, KTextEditor::Mark mark, bool &handled)
The document emits this signal whenever the mark is left-clicked.
MarkTypes
Predefined mark types.
Definition document.h:1557
@ markType01
Bookmark.
Definition document.h:1559
void viewCreated(KTextEditor::Document *document, KTextEditor::View *view)
This signal is emitted whenever the document creates a new view.
virtual bool isEmpty() const
Returns if the document is empty.
Definition document.cpp:103
virtual QString text() const =0
Get the document content.
void aboutToInvalidateMovingInterfaceContent(KTextEditor::Document *document)
This signal is emitted before the ranges of a document are invalidated and the revisions are deleted ...
static int reservedMarkersCount()
Get the number of predefined mark types we have so far.
Definition document.h:1546
void modeChanged(KTextEditor::Document *document)
Warn anyone listening that the current document's mode has changed.
void aboutToClose(KTextEditor::Document *document)
Warn anyone listening that the current document is about to close.
@ MarkRemoved
action: a mark was removed.
Definition document.h:1666
void modifiedOnDisk(KTextEditor::Document *document, bool isModified, KTextEditor::Document::ModifiedOnDiskReason reason)
This signal is emitted whenever the document changed its modified-on-disk state.
void markChanged(KTextEditor::Document *document, KTextEditor::Mark mark, KTextEditor::Document::MarkChangeAction action)
The document emits this signal whenever the mark changes.
void aboutToReload(KTextEditor::Document *document)
Warn anyone listening that the current document is about to reload.
void markContextMenuRequested(KTextEditor::Document *document, KTextEditor::Mark mark, QPoint pos, bool &handled)
The document emits this signal whenever the mark is right-clicked to show a context menu.
ModifiedOnDiskReason
Reasons why a document is modified on disk.
Definition document.h:1429
@ OnDiskUnmodified
Not modified.
Definition document.h:1430
KateModeManager * modeManager()
global mode manager used to manage the modes centrally
Definition kateglobal.h:229
void deregisterDocument(KTextEditor::DocumentPrivate *doc)
unregister document at the factory
KTextEditor::Application * application() const override
Current hosting application, if any set.
Definition kateglobal.h:120
KDirWatch * dirWatch()
global dirwatch
Definition kateglobal.h:213
void registerDocument(KTextEditor::DocumentPrivate *doc)
register document at the factory this allows us to loop over all docs for example on config changes
static KTextEditor::EditorPrivate * self()
Kate Part Internal stuff ;)
QList< KTextEditor::Document * > documents() override
Returns a list of all documents of this editor.
Definition kateglobal.h:99
KateVariableExpansionManager * variableExpansionManager()
Returns the variable expansion manager.
static Editor * instance()
Accessor to get the Editor instance.
An object representing lines from a start line to an end line.
Definition linerange.h:41
This class allows the application that embeds the KTextEditor component to allow it to access parts o...
Definition mainwindow.h:47
QWidget * window()
Get the toplevel widget.
uint type
The mark types in the line, combined with logical OR.
Definition document.h:79
int line
The line that contains the mark.
Definition document.h:76
This class holds a Message to display in Views.
Definition message.h:94
@ TopInView
show message as view overlay in the top right corner.
Definition message.h:123
KTextEditor::View * view() const
This function returns the view you set by setView().
int autoHide() const
Returns the auto hide time in milliseconds.
void addAction(QAction *action, bool closeOnTrigger=true)
Adds an action to the message.
QString text() const
Returns the text set in the constructor.
void closed(KTextEditor::Message *message)
This signal is emitted before the message is deleted.
@ Error
error message type
Definition message.h:110
@ Warning
warning message type
Definition message.h:109
void setDocument(KTextEditor::Document *document)
Set the document pointer to document.
QList< QAction * > actions() const
Accessor to all actions, mainly used in the internal implementation to add the actions into the gui.
A Cursor which is bound to a specific Document, and maintains its position.
InsertBehavior
Insert behavior of this cursor, should it stay if text is insert at its position or should it move.
@ MoveOnInsert
move on insert
A range that is bound to a specific Document, and maintains its position.
QFlags< InsertBehavior > InsertBehaviors
Stores a combination of InsertBehavior values.
EmptyBehavior
Behavior of range if it becomes empty.
const Range toRange() const
Convert this clever range into a dumb one.
virtual void setFeedback(MovingRangeFeedback *feedback)=0
Sets the currently active MovingRangeFeedback for this range.
bool contains(const Range &range) const
Check whether the this range wholly encompasses range.
@ DoNotExpand
Don't expand to encapsulate new characters in either direction. This is the default.
@ ExpandRight
Expand to encapsulate new characters to the right of the range.
@ ExpandLeft
Expand to encapsulate new characters to the left of the range.
An object representing a section of text, from one Cursor to another.
constexpr Cursor end() const noexcept
Get the end position of this range.
constexpr Cursor start() const noexcept
Get the start position of this range.
void setEnd(Cursor end) noexcept
Set the end cursor to end.
constexpr bool isEmpty() const noexcept
Returns true if this range contains no characters, ie.
void setRange(Range range) noexcept
Set the start and end cursors to range.start() and range.end() respectively.
constexpr bool overlaps(Range range) const noexcept
Check whether the this range overlaps with range.
constexpr int columnWidth() const noexcept
Returns the number of columns separating the start() and end() positions.
constexpr bool onSingleLine() const noexcept
Check whether this range is wholly contained within one line, ie.
static constexpr Range invalid() noexcept
Returns an invalid range.
constexpr bool isValid() const noexcept
Validity check.
constexpr bool contains(Range range) const noexcept
Check whether the this range wholly encompasses range.
constexpr Range intersect(Range range) const noexcept
Intersects this range with another, returning the shared area of the two ranges.
void setBothLines(int line) noexcept
Convenience function.
constexpr int numberOfLines() const noexcept
Returns the number of lines separating the start() and end() positions.
void setStart(Cursor start) noexcept
Set the start cursor to start.
A text widget with KXMLGUIClient that represents a Document.
Definition view.h:244
virtual QMenu * defaultContextMenu(QMenu *menu=nullptr) const =0
Populate menu with default text editor actions.
virtual bool setCursorPosition(Cursor position)=0
Set the view's new cursor to position.
virtual Document * document() const =0
Get the view's document, that means the view is a view of the returned document.
void focusIn(KTextEditor::View *view)
This signal is emitted whenever the view gets the focus.
void cursorPositionChanged(KTextEditor::View *view, KTextEditor::Cursor newPosition)
This signal is emitted whenever the view's cursor position changed.
virtual void setContextMenu(QMenu *menu)=0
Set a context menu for this view to menu.
virtual QAction * action(const QDomElement &element) const
void insertChildClient(KXMLGUIClient *child)
Provides Auto-Indent functionality for katepart.
The KateBuffer class maintains a collections of lines.
Definition katebuffer.h:31
void tagLines(KTextEditor::LineRange lineRange)
Emitted when the highlighting of a certain range has changed.
void configEnd()
End a config change transaction, update the concerned KateDocumentConfig/KateDocumentConfig/KateDocum...
void configStart()
Start some config changes.
bool setValue(const int key, const QVariant &value)
Set a config value.
File indentation detecter.
This dialog will prompt the user for what do with a file that is modified on disk.
This class can be used to efficiently search for occurrences of strings in a given string.
Definition prefixstore.h:27
QString findPrefix(const QString &s, int start=0) const
Returns the shortest prefix of the given string that is contained in this prefix store starting at po...
static QString escapePlaintext(const QString &text)
Returns a modified version of text where escape sequences are resolved, e.g.
const QFont & currentFont() const
Access currently used font.
Inserts a template and offers advanced snippet features, like navigation and mirroring.
KateUndoManager implements a document's history.
Class for tracking editing actions.
Class representing a 'clever' text cursor.
Class representing a single text line.
int attribute(int pos) const
Gets the attribute at the given position use KRenderer::attributes to get the KTextAttribute for this...
const QString & text() const
Accessor to the text contained in this line.
bool endsWith(const QString &match) const
Returns true, if the line ends with match, otherwise returns false.
void setAutoWrapped(bool wrapped)
set auto-wrapped property
const QList< Attribute > & attributesList() const
Accessor to attributes.
int virtualLength(int tabWidth) const
Returns the text length with each tab expanded into tabWidth characters.
QString string(int column, int length) const
Returns the substring with length beginning at the given column.
int previousNonSpaceChar(int pos) const
Find the position of the previous char that is not a space.
int length() const
Returns the line's length.
bool isAutoWrapped() const
Returns true, if the line was automagically wrapped, otherwise returns false.
bool startsWith(const QString &match) const
Returns true, if the line starts with match, otherwise returns false.
int lastChar() const
Returns the position of the last non-whitespace character.
QChar at(int column) const
Returns the character at the given column.
int firstChar() const
Returns the position of the first non-whitespace character.
int toVirtualColumn(int column, int tabWidth) const
Returns the column with each tab expanded into tabWidth characters.
bool matchesAt(int column, const QString &match) const
Returns true, if match equals to the text at position column, otherwise returns false.
int nextNonSpaceChar(int pos) const
Find the position of the next char that is not a space.
int fromVirtualColumn(int column, int tabWidth) const
Returns the "real" column where each tab only counts one character.
Class representing a 'clever' text range.
Q_SCRIPTABLE QString start(QString train="")
Q_SCRIPTABLE Q_NOREPLY void start()
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
KCALUTILS_EXPORT QString mimeType()
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
KIOCORE_EXPORT StatJob * stat(const QUrl &url, JobFlags flags=DefaultFlags)
KIOCORE_EXPORT FileCopyJob * file_copy(const QUrl &src, const QUrl &dest, int permissions=-1, JobFlags flags=DefaultFlags)
void setWindow(QObject *job, QWidget *widget)
ButtonCode warningContinueCancel(QWidget *parent, const QString &text, const QString &title=QString(), const KGuiItem &buttonContinue=KStandardGuiItem::cont(), const KGuiItem &buttonCancel=KStandardGuiItem::cancel(), const QString &dontAskAgainName=QString(), Options options=Notify)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
ButtonCode warningTwoActionsCancel(QWidget *parent, const QString &text, const QString &title, const KGuiItem &primaryAction, const KGuiItem &secondaryAction, const KGuiItem &cancelAction=KStandardGuiItem::cancel(), const QString &dontAskAgainName=QString(), Options options=Options(Notify|Dangerous))
QStringView merge(QStringView lhs, QStringView rhs)
bool isValid(QStringView ifopt)
KIOCORE_EXPORT QString dir(const QString &fileClass)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
KGuiItem remove()
KGuiItem saveAs()
KGuiItem cancel()
KGuiItem save()
KGuiItem discard()
KGuiItem clear()
KGuiItem del()
KGuiItem closeDocument()
KGuiItem help()
const QList< QKeySequence > & end()
KCOREADDONS_EXPORT QString csqueeze(const QString &str, int maxlen=40)
The KTextEditor namespace contains all the public API that is required to use the KTextEditor compone...
@ CaseInsensitive
Ignores cases, e.g. "a" matches "A".
Definition document.h:54
@ Regex
Treats the pattern as a regular expression.
Definition document.h:51
@ Backwards
Searches in backward direction.
Definition document.h:55
@ EscapeSequences
Plaintext mode: Processes escape sequences.
Definition document.h:58
@ WholeWords
Plaintext mode: Whole words only, e.g. not "amp" in "example".
Definition document.h:59
QFlags< SearchOption > SearchOptions
Stores a combination of SearchOption values.
Definition document.h:65
void setToolTip(const QString &tip)
void triggered(bool checked)
QWidget * activeWindow()
QByteArray & append(QByteArrayView data)
bool isEmpty() const const
qsizetype size() const const
QByteArray toHex(char separator) const const
bool isLowSurrogate(char32_t ucs4)
bool isNull() const const
bool isSpace(char32_t ucs4)
char32_t mirroredChar(char32_t ucs4)
char toLatin1() const const
char32_t toUpper(char32_t ucs4)
QColor fromString(QAnyStringView name)
bool isValid() const const
QChar separator()
QString tempPath()
bool copy(const QString &fileName, const QString &newName)
bool exists() const const
QUrl getSaveFileUrl(QWidget *parent, const QString &caption, const QUrl &dir, const QString &filter, QString *selectedFilter, Options options, const QStringList &supportedSchemes)
QString canonicalFilePath() const const
bool testFlag(Enum flag) const const
void clear()
iterator end()
iterator find(const Key &key)
T value(const Key &key) const const
QIcon fromTheme(const QString &name)
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
iterator begin()
const_iterator cbegin() const const
const_iterator cend() const const
qsizetype count() const const
bool empty() const const
iterator end()
T & first()
bool isEmpty() const const
T & last()
void prepend(parameter_type value)
void push_back(parameter_type value)
void reserve(qsizetype size)
qsizetype size() const const
QMimeType mimeTypeForData(QIODevice *device) const const
QMimeType mimeTypeForFile(const QFileInfo &fileInfo, MatchMode mode) const const
QMimeType mimeTypeForFileNameAndData(const QString &fileName, QIODevice *device) const const
Q_EMITQ_EMIT
void deleteLater()
bool disconnect(const QMetaObject::Connection &connection)
QObject * parent() const const
T qobject_cast(QObject *object)
void setParent(QObject *parent)
void closeWriteChannel()
int exitCode() const const
void setWorkingDirectory(const QString &dir)
void start(OpenMode mode)
bool waitForFinished(int msecs)
bool waitForStarted(int msecs)
QString wildcardToRegularExpression(QStringView pattern, WildcardConversionOptions options)
bool contains(const QSet< T > &other) const const
iterator insert(const T &value)
QString findExecutable(const QString &executableName, const QStringList &paths)
qsizetype count() const const
QString & append(QChar ch)
QString arg(Args &&... args) const const
const QChar at(qsizetype position) const const
void chop(qsizetype n)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QChar * data()
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
bool isNull() const const
QString left(qsizetype n) const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QString repeated(qsizetype times) const const
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
void reserve(qsizetype size)
qsizetype size() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
int toInt(bool *ok, int base) const const
QByteArray toLatin1() const const
QString toLower() const const
QString toUpper() const const
QString trimmed() const const
QString join(QChar separator) const const
QStringView mid(qsizetype start, qsizetype length) const const
CaseSensitivity
QueuedConnection
SkipEmptyParts
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
virtual QString fileName() const const override
int nextCursorPosition(int oldPos, CursorMode mode) const const
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
PreferLocalFile
QUrl adjusted(FormattingOptions options) const const
QString fileName(ComponentFormattingOptions options) const const
QUrl fromLocalFile(const QString &localFile)
bool isEmpty() const const
bool isLocalFile() const const
bool isValid() const const
QString path(ComponentFormattingOptions options) const const
QString toString(FormattingOptions options) const const
T value() const const
const_iterator cbegin() const const
const_iterator cend() const const
void reserve(qsizetype size)
void repaint()
void update()
bool isVisible() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 24 2025 11:55:24 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.