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