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