KTextEditor

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

KDE's Doxygen guidelines are available online.