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

KDE's Doxygen guidelines are available online.