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