diff --git a/src/document/katedocument.cpp b/src/document/katedocument.cpp index a0b0f67e..312a591f 100644 --- a/src/document/katedocument.cpp +++ b/src/document/katedocument.cpp @@ -1,6080 +1,6092 @@ /* This file is part of the KDE libraries Copyright (C) 2001-2004 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 1999 Jochen Wilhelmy Copyright (C) 2006 Hamish Rodda Copyright (C) 2007 Mirko Stocker Copyright (C) 2009-2010 Michel Ludwig Copyright (C) 2013 Gerald Senarclens de Grancy Copyright (C) 2013 Andrey Matveyakin This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-13020, USA. */ // BEGIN includes #include "katedocument.h" #include "config.h" #include "kateabstractinputmode.h" #include "kateautoindent.h" #include "katebuffer.h" #include "kateconfig.h" #include "katedialogs.h" #include "kateglobal.h" #include "katehighlight.h" #include "katemodemanager.h" #include "katepartdebug.h" #include "kateplaintextsearch.h" #include "kateregexp.h" #include "kateregexpsearch.h" #include "katerenderer.h" #include "kateschema.h" #include "katescriptmanager.h" #include "kateswapfile.h" #include "katetemplatehandler.h" #include "katetextline.h" #include "kateundomanager.h" #include "katevariableexpansionmanager.h" #include "kateview.h" #include "printing/kateprinter.h" #include "spellcheck/ontheflycheck.h" #include "spellcheck/prefixstore.h" #include "spellcheck/spellcheck.h" #if EDITORCONFIG_FOUND #include "editorconfig.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if LIBGIT2_FOUND #include #include #include #endif // END includes #if 0 #define EDIT_DEBUG qCDebug(LOG_KTE) #else #define EDIT_DEBUG \ if (0) \ qCDebug(LOG_KTE) #endif template static int indexOf(const std::initializer_list &list, const E &entry) { auto it = std::find(list.begin(), list.end(), entry); return it == list.end() ? -1 : std::distance(list.begin(), it); } template static bool contains(const std::initializer_list &list, const E &entry) { return indexOf(list, entry) >= 0; } static inline QChar matchingStartBracket(const QChar c) { switch (c.toLatin1()) { case '}': return QLatin1Char('{'); case ']': return QLatin1Char('['); case ')': return QLatin1Char('('); } return QChar(); } static inline QChar matchingEndBracket(const QChar c, bool withQuotes = true) { switch (c.toLatin1()) { case '{': return QLatin1Char('}'); case '[': return QLatin1Char(']'); case '(': return QLatin1Char(')'); case '\'': return withQuotes ? QLatin1Char('\'') : QChar(); case '"': return withQuotes ? QLatin1Char('"') : QChar(); } return QChar(); } static inline QChar matchingBracket(const QChar c) { QChar bracket = matchingStartBracket(c); if (bracket.isNull()) { bracket = matchingEndBracket(c, /*withQuotes=*/false); } return bracket; } static inline bool isStartBracket(const QChar c) { return !matchingEndBracket(c, /*withQuotes=*/false).isNull(); } static inline bool isEndBracket(const QChar c) { return !matchingStartBracket(c).isNull(); } static inline bool isBracket(const QChar c) { return isStartBracket(c) || isEndBracket(c); } /** * normalize given url * @param url input url * @return normalized url */ static QUrl normalizeUrl(const QUrl &url) { /** * only normalize local urls */ if (url.isEmpty() || !url.isLocalFile()) return url; /** * don't normalize if not existing! * canonicalFilePath won't work! */ const QString normalizedUrl(QFileInfo(url.toLocalFile()).canonicalFilePath()); if (normalizedUrl.isEmpty()) return url; /** * else: use canonicalFilePath to normalize */ return QUrl::fromLocalFile(normalizedUrl); } // BEGIN d'tor, c'tor // // KTextEditor::DocumentPrivate Constructor // KTextEditor::DocumentPrivate::DocumentPrivate(bool bSingleViewMode, bool bReadOnly, QWidget *parentWidget, QObject *parent) : KTextEditor::Document(this, parent) , m_bSingleViewMode(bSingleViewMode) , m_bReadOnly(bReadOnly) , m_undoManager(new KateUndoManager(this)) , m_buffer(new KateBuffer(this)) , m_indenter(new KateAutoIndent(this)) , m_docName(QStringLiteral("need init")) , m_fileType(QStringLiteral("Normal")) , m_config(new KateDocumentConfig(this)) { /** * no plugins from kparts here */ setPluginLoadingMode(DoNotLoadPlugins); /** * pass on our component data, do this after plugin loading is off */ setComponentData(KTextEditor::EditorPrivate::self()->aboutData()); /** * avoid spamming plasma and other window managers with progress dialogs * we show such stuff inline in the views! */ setProgressInfoEnabled(false); // register doc at factory KTextEditor::EditorPrivate::self()->registerDocument(this); // normal hl m_buffer->setHighlight(0); // swap file m_swapfile = (config()->swapFileMode() == KateDocumentConfig::DisableSwapFile) ? nullptr : new Kate::SwapFile(this); // some nice signals from the buffer connect(m_buffer, SIGNAL(tagLines(int, int)), this, SLOT(tagLines(int, int))); // if the user changes the highlight with the dialog, notify the doc connect(KateHlManager::self(), SIGNAL(changed()), SLOT(internalHlChanged())); // signals for mod on hd connect(KTextEditor::EditorPrivate::self()->dirWatch(), SIGNAL(dirty(QString)), this, SLOT(slotModOnHdDirty(QString))); connect(KTextEditor::EditorPrivate::self()->dirWatch(), SIGNAL(created(QString)), this, SLOT(slotModOnHdCreated(QString))); connect(KTextEditor::EditorPrivate::self()->dirWatch(), SIGNAL(deleted(QString)), this, SLOT(slotModOnHdDeleted(QString))); /** * singleshot timer to handle updates of mod on hd state delayed */ m_modOnHdTimer.setSingleShot(true); m_modOnHdTimer.setInterval(200); connect(&m_modOnHdTimer, SIGNAL(timeout()), this, SLOT(slotDelayedHandleModOnHd())); // Setup auto reload stuff m_autoReloadMode = new KToggleAction(i18n("Auto Reload Document"), this); m_autoReloadMode->setWhatsThis(i18n("Automatic reload the document when it was changed on disk")); connect(m_autoReloadMode, &KToggleAction::triggered, this, &DocumentPrivate::autoReloadToggled); // Prepare some reload amok protector... m_autoReloadThrottle.setSingleShot(true); //...but keep the value small in unit tests m_autoReloadThrottle.setInterval(KTextEditor::EditorPrivate::self()->unitTestMode() ? 50 : 3000); connect(&m_autoReloadThrottle, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload); /** * load handling * this is needed to ensure we signal the user if a file ist still loading * and to disallow him to edit in that time */ connect(this, SIGNAL(started(KIO::Job *)), this, SLOT(slotStarted(KIO::Job *))); connect(this, SIGNAL(completed()), this, SLOT(slotCompleted())); connect(this, SIGNAL(canceled(QString)), this, SLOT(slotCanceled())); connect(this, SIGNAL(urlChanged(QUrl)), this, SLOT(slotUrlChanged(QUrl))); // update doc name updateDocName(); // if single view mode, like in the konqui embedding, create a default view ;) // be lazy, only create it now, if any parentWidget is given, otherwise widget() // will create it on demand... if (m_bSingleViewMode && parentWidget) { KTextEditor::View *view = (KTextEditor::View *)createView(parentWidget); insertChildClient(view); view->setContextMenu(view->defaultContextMenu()); setWidget(view); } connect(m_undoManager, SIGNAL(undoChanged()), this, SIGNAL(undoChanged())); connect(m_undoManager, SIGNAL(undoStart(KTextEditor::Document *)), this, SIGNAL(editingStarted(KTextEditor::Document *))); connect(m_undoManager, SIGNAL(undoEnd(KTextEditor::Document *)), this, SIGNAL(editingFinished(KTextEditor::Document *))); connect(m_undoManager, SIGNAL(redoStart(KTextEditor::Document *)), this, SIGNAL(editingStarted(KTextEditor::Document *))); connect(m_undoManager, SIGNAL(redoEnd(KTextEditor::Document *)), this, SIGNAL(editingFinished(KTextEditor::Document *))); connect(this, SIGNAL(sigQueryClose(bool *, bool *)), this, SLOT(slotQueryClose_save(bool *, bool *))); connect(this, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(clearEditingPosStack())); onTheFlySpellCheckingEnabled(config()->onTheFlySpellCheck()); // make sure correct defaults are set (indenter, ...) updateConfig(); } // // KTextEditor::DocumentPrivate Destructor // KTextEditor::DocumentPrivate::~DocumentPrivate() { // delete pending mod-on-hd message, if applicable delete m_modOnHdHandler; /** * we are about to delete cursors/ranges/... */ emit aboutToDeleteMovingInterfaceContent(this); // kill it early, it has ranges! delete m_onTheFlyChecker; m_onTheFlyChecker = nullptr; clearDictionaryRanges(); // Tell the world that we're about to close (== destruct) // Apps must receive this in a direct signal-slot connection, and prevent // any further use of interfaces once they return. emit aboutToClose(this); // remove file from dirwatch deactivateDirWatch(); // thanks for offering, KPart, but we're already self-destructing setAutoDeleteWidget(false); setAutoDeletePart(false); // clean up remaining views qDeleteAll(m_views.keys()); m_views.clear(); // cu marks for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { delete i.value(); } m_marks.clear(); delete m_config; KTextEditor::EditorPrivate::self()->deregisterDocument(this); } // END void KTextEditor::DocumentPrivate::saveEditingPositions(const KTextEditor::Cursor &cursor) { if (m_editingStackPosition != m_editingStack.size() - 1) { m_editingStack.resize(m_editingStackPosition); } // try to be clever: reuse existing cursors if possible QSharedPointer mc; // we might pop last one: reuse that if (!m_editingStack.isEmpty() && cursor.line() == m_editingStack.top()->line()) { mc = m_editingStack.pop(); } // we might expire oldest one, reuse that one, if not already one there // we prefer the other one for reuse, as already on the right line aka in the right block! const int editingStackSizeLimit = 32; if (m_editingStack.size() >= editingStackSizeLimit) { if (mc) { m_editingStack.removeFirst(); } else { mc = m_editingStack.takeFirst(); } } // new cursor needed? or adjust existing one? if (mc) { mc->setPosition(cursor); } else { mc = QSharedPointer(newMovingCursor(cursor)); } // add new one as top of stack m_editingStack.push(mc); m_editingStackPosition = m_editingStack.size() - 1; } KTextEditor::Cursor KTextEditor::DocumentPrivate::lastEditingPosition(EditingPositionKind nextOrPrev, KTextEditor::Cursor currentCursor) { if (m_editingStack.isEmpty()) { return KTextEditor::Cursor::invalid(); } auto targetPos = m_editingStack.at(m_editingStackPosition)->toCursor(); if (targetPos == currentCursor) { if (nextOrPrev == Previous) { m_editingStackPosition--; } else { m_editingStackPosition++; } m_editingStackPosition = qBound(0, m_editingStackPosition, m_editingStack.size() - 1); } return m_editingStack.at(m_editingStackPosition)->toCursor(); } void KTextEditor::DocumentPrivate::clearEditingPosStack() { m_editingStack.clear(); m_editingStackPosition = -1; } // on-demand view creation QWidget *KTextEditor::DocumentPrivate::widget() { // no singleViewMode -> no widget()... if (!singleViewMode()) { return nullptr; } // does a widget exist already? use it! if (KTextEditor::Document::widget()) { return KTextEditor::Document::widget(); } // create and return one... KTextEditor::View *view = (KTextEditor::View *)createView(nullptr); insertChildClient(view); view->setContextMenu(view->defaultContextMenu()); setWidget(view); return view; } // BEGIN KTextEditor::Document stuff KTextEditor::View *KTextEditor::DocumentPrivate::createView(QWidget *parent, KTextEditor::MainWindow *mainWindow) { KTextEditor::ViewPrivate *newView = new KTextEditor::ViewPrivate(this, parent, mainWindow); if (m_fileChangedDialogsActivated) { connect(newView, SIGNAL(focusIn(KTextEditor::View *)), this, SLOT(slotModifiedOnDisk())); } emit viewCreated(this, newView); // post existing messages to the new view, if no specific view is given const auto keys = m_messageHash.keys(); for (KTextEditor::Message *message : keys) { if (!message->view()) { newView->postMessage(message, m_messageHash[message]); } } return newView; } KTextEditor::Range KTextEditor::DocumentPrivate::rangeOnLine(KTextEditor::Range range, int line) const { const int col1 = toVirtualColumn(range.start()); const int col2 = toVirtualColumn(range.end()); return KTextEditor::Range(line, fromVirtualColumn(line, col1), line, fromVirtualColumn(line, col2)); } // BEGIN KTextEditor::EditInterface stuff bool KTextEditor::DocumentPrivate::isEditingTransactionRunning() const { return editSessionNumber > 0; } QString KTextEditor::DocumentPrivate::text() const { return m_buffer->text(); } QString KTextEditor::DocumentPrivate::text(const KTextEditor::Range &range, bool blockwise) const { if (!range.isValid()) { qCWarning(LOG_KTE) << "Text requested for invalid range" << range; return QString(); } QString s; if (range.start().line() == range.end().line()) { if (range.start().column() > range.end().column()) { return QString(); } Kate::TextLine textLine = m_buffer->plainLine(range.start().line()); if (!textLine) { return QString(); } return textLine->string(range.start().column(), range.end().column() - range.start().column()); } else { for (int i = range.start().line(); (i <= range.end().line()) && (i < m_buffer->count()); ++i) { Kate::TextLine textLine = m_buffer->plainLine(i); if (!blockwise) { if (i == range.start().line()) { s.append(textLine->string(range.start().column(), textLine->length() - range.start().column())); } else if (i == range.end().line()) { s.append(textLine->string(0, range.end().column())); } else { s.append(textLine->string()); } } else { KTextEditor::Range subRange = rangeOnLine(range, i); s.append(textLine->string(subRange.start().column(), subRange.columnWidth())); } if (i < range.end().line()) { s.append(QLatin1Char('\n')); } } } return s; } QChar KTextEditor::DocumentPrivate::characterAt(const KTextEditor::Cursor &position) const { Kate::TextLine textLine = m_buffer->plainLine(position.line()); if (!textLine) { return QChar(); } return textLine->at(position.column()); } QString KTextEditor::DocumentPrivate::wordAt(const KTextEditor::Cursor &cursor) const { return text(wordRangeAt(cursor)); } KTextEditor::Range KTextEditor::DocumentPrivate::wordRangeAt(const KTextEditor::Cursor &cursor) const { // get text line const int line = cursor.line(); Kate::TextLine textLine = m_buffer->plainLine(line); if (!textLine) { return KTextEditor::Range::invalid(); } // make sure the cursor is const int lineLenth = textLine->length(); if (cursor.column() > lineLenth) { return KTextEditor::Range::invalid(); } int start = cursor.column(); int end = start; while (start > 0 && highlight()->isInWord(textLine->at(start - 1), textLine->attribute(start - 1))) { start--; } while (end < lineLenth && highlight()->isInWord(textLine->at(end), textLine->attribute(end))) { end++; } return KTextEditor::Range(line, start, line, end); } bool KTextEditor::DocumentPrivate::isValidTextPosition(const KTextEditor::Cursor &cursor) const { const int ln = cursor.line(); const int col = cursor.column(); // cursor in document range? if (ln < 0 || col < 0 || ln >= lines() || col > lineLength(ln)) { return false; } const QString str = line(ln); Q_ASSERT(str.length() >= col); // cursor at end of line? const int len = lineLength(ln); if (col == 0 || col == len) { return true; } // cursor in the middle of a valid utf32-surrogate? return (!str.at(col).isLowSurrogate()) || (!str.at(col - 1).isHighSurrogate()); } QStringList KTextEditor::DocumentPrivate::textLines(const KTextEditor::Range &range, bool blockwise) const { QStringList ret; if (!range.isValid()) { qCWarning(LOG_KTE) << "Text requested for invalid range" << range; return ret; } if (blockwise && (range.start().column() > range.end().column())) { return ret; } if (range.start().line() == range.end().line()) { Q_ASSERT(range.start() <= range.end()); Kate::TextLine textLine = m_buffer->plainLine(range.start().line()); if (!textLine) { return ret; } ret << textLine->string(range.start().column(), range.end().column() - range.start().column()); } else { for (int i = range.start().line(); (i <= range.end().line()) && (i < m_buffer->count()); ++i) { Kate::TextLine textLine = m_buffer->plainLine(i); if (!blockwise) { if (i == range.start().line()) { ret << textLine->string(range.start().column(), textLine->length() - range.start().column()); } else if (i == range.end().line()) { ret << textLine->string(0, range.end().column()); } else { ret << textLine->string(); } } else { KTextEditor::Range subRange = rangeOnLine(range, i); ret << textLine->string(subRange.start().column(), subRange.columnWidth()); } } } return ret; } QString KTextEditor::DocumentPrivate::line(int line) const { Kate::TextLine l = m_buffer->plainLine(line); if (!l) { return QString(); } return l->string(); } bool KTextEditor::DocumentPrivate::setText(const QString &s) { if (!isReadWrite()) { return false; } QList msave; msave.reserve(m_marks.size()); for (KTextEditor::Mark *mark : qAsConst(m_marks)) { msave.append(*mark); } editStart(); // delete the text clear(); // insert the new text insertText(KTextEditor::Cursor(), s); editEnd(); for (KTextEditor::Mark mark : qAsConst(msave)) { setMark(mark.line, mark.type); } return true; } bool KTextEditor::DocumentPrivate::setText(const QStringList &text) { if (!isReadWrite()) { return false; } QList msave; msave.reserve(m_marks.size()); for (KTextEditor::Mark *mark : qAsConst(m_marks)) { msave.append(*mark); } editStart(); // delete the text clear(); // insert the new text insertText(KTextEditor::Cursor::start(), text); editEnd(); for (KTextEditor::Mark mark : qAsConst(msave)) { setMark(mark.line, mark.type); } return true; } bool KTextEditor::DocumentPrivate::clear() { if (!isReadWrite()) { return false; } for (auto view : qAsConst(m_views)) { view->clear(); view->tagAll(); view->update(); } clearMarks(); emit aboutToInvalidateMovingInterfaceContent(this); m_buffer->invalidateRanges(); emit aboutToRemoveText(documentRange()); return editRemoveLines(0, lastLine()); } bool KTextEditor::DocumentPrivate::insertText(const KTextEditor::Cursor &position, const QString &text, bool block) { if (!isReadWrite()) { return false; } if (text.isEmpty()) { return true; } editStart(); int currentLine = position.line(); int currentLineStart = 0; const int totalLength = text.length(); int insertColumn = position.column(); // pad with empty lines, if insert position is after last line if (position.line() > lines()) { int line = lines(); while (line <= position.line()) { editInsertLine(line, QString()); line++; } } // compute expanded column for block mode int positionColumnExpanded = insertColumn; const int tabWidth = config()->tabWidth(); if (block) { if (auto l = plainKateTextLine(currentLine)) { positionColumnExpanded = l->toVirtualColumn(insertColumn, tabWidth); } } int pos = 0; for (; pos < totalLength; pos++) { const QChar &ch = text.at(pos); if (ch == QLatin1Char('\n')) { // Only perform the text insert if there is text to insert if (currentLineStart < pos) { editInsertText(currentLine, insertColumn, text.mid(currentLineStart, pos - currentLineStart)); } if (!block) { editWrapLine(currentLine, insertColumn + pos - currentLineStart); insertColumn = 0; } currentLine++; if (block) { auto l = plainKateTextLine(currentLine); if (currentLine == lastLine() + 1) { editInsertLine(currentLine, QString()); } insertColumn = positionColumnExpanded; if (l) { insertColumn = l->fromVirtualColumn(insertColumn, tabWidth); } } currentLineStart = pos + 1; } } // Only perform the text insert if there is text to insert if (currentLineStart < pos) { editInsertText(currentLine, insertColumn, text.mid(currentLineStart, pos - currentLineStart)); } editEnd(); return true; } bool KTextEditor::DocumentPrivate::insertText(const KTextEditor::Cursor &position, const QStringList &textLines, bool block) { if (!isReadWrite()) { return false; } // just reuse normal function return insertText(position, textLines.join(QLatin1Char('\n')), block); } bool KTextEditor::DocumentPrivate::removeText(const KTextEditor::Range &_range, bool block) { KTextEditor::Range range = _range; if (!isReadWrite()) { return false; } // Should now be impossible to trigger with the new Range class Q_ASSERT(range.start().line() <= range.end().line()); if (range.start().line() > lastLine()) { return false; } if (!block) { emit aboutToRemoveText(range); } editStart(); if (!block) { if (range.end().line() > lastLine()) { range.setEnd(KTextEditor::Cursor(lastLine() + 1, 0)); } if (range.onSingleLine()) { editRemoveText(range.start().line(), range.start().column(), range.columnWidth()); } else { int from = range.start().line(); int to = range.end().line(); // remove last line if (to <= lastLine()) { editRemoveText(to, 0, range.end().column()); } // editRemoveLines() will be called on first line (to remove bookmark) if (range.start().column() == 0 && from > 0) { --from; } // remove middle lines editRemoveLines(from + 1, to - 1); // remove first line if not already removed by editRemoveLines() if (range.start().column() > 0 || range.start().line() == 0) { editRemoveText(from, range.start().column(), m_buffer->plainLine(from)->length() - range.start().column()); editUnWrapLine(from); } } } // if ( ! block ) else { int startLine = qMax(0, range.start().line()); int vc1 = toVirtualColumn(range.start()); int vc2 = toVirtualColumn(range.end()); for (int line = qMin(range.end().line(), lastLine()); line >= startLine; --line) { int col1 = fromVirtualColumn(line, vc1); int col2 = fromVirtualColumn(line, vc2); editRemoveText(line, qMin(col1, col2), qAbs(col2 - col1)); } } editEnd(); return true; } bool KTextEditor::DocumentPrivate::insertLine(int l, const QString &str) { if (!isReadWrite()) { return false; } if (l < 0 || l > lines()) { return false; } return editInsertLine(l, str); } bool KTextEditor::DocumentPrivate::insertLines(int line, const QStringList &text) { if (!isReadWrite()) { return false; } if (line < 0 || line > lines()) { return false; } bool success = true; for (const QString &string : text) { success &= editInsertLine(line++, string); } return success; } bool KTextEditor::DocumentPrivate::removeLine(int line) { if (!isReadWrite()) { return false; } if (line < 0 || line > lastLine()) { return false; } return editRemoveLine(line); } int KTextEditor::DocumentPrivate::totalCharacters() const { int l = 0; for (int i = 0; i < m_buffer->count(); ++i) { Kate::TextLine line = m_buffer->plainLine(i); if (line) { l += line->length(); } } return l; } int KTextEditor::DocumentPrivate::lines() const { return m_buffer->count(); } int KTextEditor::DocumentPrivate::lineLength(int line) const { if (line < 0 || line > lastLine()) { return -1; } Kate::TextLine l = m_buffer->plainLine(line); if (!l) { return -1; } return l->length(); } bool KTextEditor::DocumentPrivate::isLineModified(int line) const { if (line < 0 || line >= lines()) { return false; } Kate::TextLine l = m_buffer->plainLine(line); Q_ASSERT(l); return l->markedAsModified(); } bool KTextEditor::DocumentPrivate::isLineSaved(int line) const { if (line < 0 || line >= lines()) { return false; } Kate::TextLine l = m_buffer->plainLine(line); Q_ASSERT(l); return l->markedAsSavedOnDisk(); } bool KTextEditor::DocumentPrivate::isLineTouched(int line) const { if (line < 0 || line >= lines()) { return false; } Kate::TextLine l = m_buffer->plainLine(line); Q_ASSERT(l); return l->markedAsModified() || l->markedAsSavedOnDisk(); } // END // BEGIN KTextEditor::EditInterface internal stuff // // Starts an edit session with (or without) undo, update of view disabled during session // bool KTextEditor::DocumentPrivate::editStart() { editSessionNumber++; if (editSessionNumber > 1) { return false; } editIsRunning = true; // no last change cursor at start m_editLastChangeStartCursor = KTextEditor::Cursor::invalid(); m_undoManager->editStart(); for (auto view : qAsConst(m_views)) { view->editStart(); } m_buffer->editStart(); return true; } // // End edit session and update Views // bool KTextEditor::DocumentPrivate::editEnd() { if (editSessionNumber == 0) { Q_ASSERT(0); return false; } // wrap the new/changed text, if something really changed! if (m_buffer->editChanged() && (editSessionNumber == 1)) if (m_undoManager->isActive() && config()->wordWrap()) { wrapText(m_buffer->editTagStart(), m_buffer->editTagEnd()); } editSessionNumber--; if (editSessionNumber > 0) { return false; } // end buffer edit, will trigger hl update // this will cause some possible adjustment of tagline start/end m_buffer->editEnd(); m_undoManager->editEnd(); // edit end for all views !!!!!!!!! for (auto view : qAsConst(m_views)) { view->editEnd(m_buffer->editTagStart(), m_buffer->editTagEnd(), m_buffer->editTagFrom()); } if (m_buffer->editChanged()) { setModified(true); emit textChanged(this); } // remember last change position in the stack, if any // this avoid costly updates for longer editing transactions // before we did that on textInsert/Removed if (m_editLastChangeStartCursor.isValid()) saveEditingPositions(m_editLastChangeStartCursor); editIsRunning = false; return true; } void KTextEditor::DocumentPrivate::pushEditState() { editStateStack.push(editSessionNumber); } void KTextEditor::DocumentPrivate::popEditState() { if (editStateStack.isEmpty()) { return; } int count = editStateStack.pop() - editSessionNumber; while (count < 0) { ++count; editEnd(); } while (count > 0) { --count; editStart(); } } void KTextEditor::DocumentPrivate::inputMethodStart() { m_undoManager->inputMethodStart(); } void KTextEditor::DocumentPrivate::inputMethodEnd() { m_undoManager->inputMethodEnd(); } bool KTextEditor::DocumentPrivate::wrapText(int startLine, int endLine) { if (startLine < 0 || endLine < 0) { return false; } if (!isReadWrite()) { return false; } int col = config()->wordWrapAt(); if (col == 0) { return false; } editStart(); for (int line = startLine; (line <= endLine) && (line < lines()); line++) { Kate::TextLine l = kateTextLine(line); if (!l) { break; } // qCDebug(LOG_KTE) << "try wrap line: " << line; if (l->virtualLength(m_buffer->tabWidth()) > col) { Kate::TextLine nextl = kateTextLine(line + 1); // qCDebug(LOG_KTE) << "do wrap line: " << line; int eolPosition = l->length() - 1; // take tabs into account here, too int x = 0; const QString &t = l->string(); int z2 = 0; for (; z2 < l->length(); z2++) { static const QChar tabChar(QLatin1Char('\t')); if (t.at(z2) == tabChar) { x += m_buffer->tabWidth() - (x % m_buffer->tabWidth()); } else { x++; } if (x > col) { break; } } const int colInChars = qMin(z2, l->length() - 1); int searchStart = colInChars; // If where we are wrapping is an end of line and is a space we don't // want to wrap there if (searchStart == eolPosition && t.at(searchStart).isSpace()) { searchStart--; } // Scan backwards looking for a place to break the line // We are not interested in breaking at the first char // of the line (if it is a space), but we are at the second // anders: if we can't find a space, try breaking on a word // boundary, using KateHighlight::canBreakAt(). // This could be a priority (setting) in the hl/filetype/document int z = -1; int nw = -1; // alternative position, a non word character for (z = searchStart; z >= 0; z--) { if (t.at(z).isSpace()) { break; } if ((nw < 0) && highlight()->canBreakAt(t.at(z), l->attribute(z))) { nw = z; } } if (z >= 0) { // So why don't we just remove the trailing space right away? // Well, the (view's) cursor may be directly in front of that space // (user typing text before the last word on the line), and if that // happens, the cursor would be moved to the next line, which is not // what we want (bug #106261) z++; } else { // There was no space to break at so break at a nonword character if // found, or at the wrapcolumn ( that needs be configurable ) // Don't try and add any white space for the break if ((nw >= 0) && nw < colInChars) { nw++; // break on the right side of the character } z = (nw >= 0) ? nw : colInChars; } if (nextl && !nextl->isAutoWrapped()) { editWrapLine(line, z, true); editMarkLineAutoWrapped(line + 1, true); endLine++; } else { if (nextl && (nextl->length() > 0) && !nextl->at(0).isSpace() && ((l->length() < 1) || !l->at(l->length() - 1).isSpace())) { editInsertText(line + 1, 0, QStringLiteral(" ")); } bool newLineAdded = false; editWrapLine(line, z, false, &newLineAdded); editMarkLineAutoWrapped(line + 1, true); endLine++; } } } editEnd(); return true; } bool KTextEditor::DocumentPrivate::wrapParagraph(int first, int last) { if (first == last) { return wrapText(first, last); } if (first < 0 || last < first) { return false; } if (last >= lines() || first > last) { return false; } if (!isReadWrite()) { return false; } editStart(); // Because we shrink and expand lines, we need to track the working set by powerful "MovingStuff" std::unique_ptr range(newMovingRange(KTextEditor::Range(first, 0, last, 0))); std::unique_ptr curr(newMovingCursor(KTextEditor::Cursor(range->start()))); // Scan the selected range for paragraphs, whereas each empty line trigger a new paragraph for (int line = first; line <= range->end().line(); ++line) { // Is our first line a somehow filled line? if (plainKateTextLine(first)->firstChar() < 0) { // Fast forward to first non empty line ++first; curr->setPosition(curr->line() + 1, 0); continue; } // Is our current line a somehow filled line? If not, wrap the paragraph if (plainKateTextLine(line)->firstChar() < 0) { curr->setPosition(line, 0); // Set on empty line joinLines(first, line - 1); // Don't wrap twice! That may cause a bad result if (!wordWrap()) { wrapText(first, first); } first = curr->line() + 1; line = first; } } // If there was no paragraph, we need to wrap now bool needWrap = (curr->line() != range->end().line()); if (needWrap && plainKateTextLine(first)->firstChar() != -1) { joinLines(first, range->end().line()); // Don't wrap twice! That may cause a bad result if (!wordWrap()) { wrapText(first, first); } } editEnd(); return true; } bool KTextEditor::DocumentPrivate::editInsertText(int line, int col, const QString &s) { // verbose debug EDIT_DEBUG << "editInsertText" << line << col << s; if (line < 0 || col < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); if (!l) { return false; } // nothing to do, do nothing! if (s.isEmpty()) { return true; } editStart(); QString s2 = s; int col2 = col; if (col2 > l->length()) { s2 = QString(col2 - l->length(), QLatin1Char(' ')) + s; col2 = l->length(); } m_undoManager->slotTextInserted(line, col2, s2); // remember last change cursor m_editLastChangeStartCursor = KTextEditor::Cursor(line, col2); // insert text into line m_buffer->insertText(m_editLastChangeStartCursor, s2); emit textInserted(this, KTextEditor::Range(line, col2, line, col2 + s2.length())); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editRemoveText(int line, int col, int len) { // verbose debug EDIT_DEBUG << "editRemoveText" << line << col << len; if (line < 0 || col < 0 || len < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); if (!l) { return false; } // nothing to do, do nothing! if (len == 0) { return true; } // wrong column if (col >= l->text().size()) { return false; } // don't try to remove what's not there len = qMin(len, l->text().size() - col); editStart(); QString oldText = l->string().mid(col, len); m_undoManager->slotTextRemoved(line, col, oldText); // remember last change cursor m_editLastChangeStartCursor = KTextEditor::Cursor(line, col); // remove text from line m_buffer->removeText(KTextEditor::Range(m_editLastChangeStartCursor, KTextEditor::Cursor(line, col + len))); emit textRemoved(this, KTextEditor::Range(line, col, line, col + len), oldText); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editMarkLineAutoWrapped(int line, bool autowrapped) { // verbose debug EDIT_DEBUG << "editMarkLineAutoWrapped" << line << autowrapped; if (line < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); if (!l) { return false; } editStart(); m_undoManager->slotMarkLineAutoWrapped(line, autowrapped); l->setAutoWrapped(autowrapped); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editWrapLine(int line, int col, bool newLine, bool *newLineAdded) { // verbose debug EDIT_DEBUG << "editWrapLine" << line << col << newLine; if (line < 0 || col < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); if (!l) { return false; } editStart(); Kate::TextLine nextLine = kateTextLine(line + 1); const int length = l->length(); m_undoManager->slotLineWrapped(line, col, length - col, (!nextLine || newLine)); if (!nextLine || newLine) { m_buffer->wrapLine(KTextEditor::Cursor(line, col)); QList list; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { if (i.value()->line >= line) { if ((col == 0) || (i.value()->line > line)) { list.append(i.value()); } } } for (int i = 0; i < list.size(); ++i) { m_marks.take(list.at(i)->line); } for (int i = 0; i < list.size(); ++i) { list.at(i)->line++; m_marks.insert(list.at(i)->line, list.at(i)); } if (!list.isEmpty()) { emit marksChanged(this); } // yes, we added a new line ! if (newLineAdded) { (*newLineAdded) = true; } } else { m_buffer->wrapLine(KTextEditor::Cursor(line, col)); m_buffer->unwrapLine(line + 2); // no, no new line added ! if (newLineAdded) { (*newLineAdded) = false; } } // remember last change cursor m_editLastChangeStartCursor = KTextEditor::Cursor(line, col); emit textInserted(this, KTextEditor::Range(line, col, line + 1, 0)); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editUnWrapLine(int line, bool removeLine, int length) { // verbose debug EDIT_DEBUG << "editUnWrapLine" << line << removeLine << length; if (line < 0 || length < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); Kate::TextLine nextLine = kateTextLine(line + 1); if (!l || !nextLine) { return false; } editStart(); int col = l->length(); m_undoManager->slotLineUnWrapped(line, col, length, removeLine); if (removeLine) { m_buffer->unwrapLine(line + 1); } else { m_buffer->wrapLine(KTextEditor::Cursor(line + 1, length)); m_buffer->unwrapLine(line + 1); } QList list; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { if (i.value()->line >= line + 1) { list.append(i.value()); } if (i.value()->line == line + 1) { KTextEditor::Mark *mark = m_marks.take(line); if (mark) { i.value()->type |= mark->type; } } } for (int i = 0; i < list.size(); ++i) { m_marks.take(list.at(i)->line); } for (int i = 0; i < list.size(); ++i) { list.at(i)->line--; m_marks.insert(list.at(i)->line, list.at(i)); } if (!list.isEmpty()) { emit marksChanged(this); } // remember last change cursor m_editLastChangeStartCursor = KTextEditor::Cursor(line, col); emit textRemoved(this, KTextEditor::Range(line, col, line + 1, 0), QStringLiteral("\n")); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editInsertLine(int line, const QString &s) { // verbose debug EDIT_DEBUG << "editInsertLine" << line << s; if (line < 0) { return false; } if (!isReadWrite()) { return false; } if (line > lines()) { return false; } editStart(); m_undoManager->slotLineInserted(line, s); // wrap line if (line > 0) { Kate::TextLine previousLine = m_buffer->line(line - 1); m_buffer->wrapLine(KTextEditor::Cursor(line - 1, previousLine->text().size())); } else { m_buffer->wrapLine(KTextEditor::Cursor(0, 0)); } // insert text m_buffer->insertText(KTextEditor::Cursor(line, 0), s); Kate::TextLine tl = m_buffer->line(line); QList list; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { if (i.value()->line >= line) { list.append(i.value()); } } for (int i = 0; i < list.size(); ++i) { m_marks.take(list.at(i)->line); } for (int i = 0; i < list.size(); ++i) { list.at(i)->line++; m_marks.insert(list.at(i)->line, list.at(i)); } if (!list.isEmpty()) { emit marksChanged(this); } KTextEditor::Range rangeInserted(line, 0, line, tl->length()); if (line) { Kate::TextLine prevLine = plainKateTextLine(line - 1); rangeInserted.setStart(KTextEditor::Cursor(line - 1, prevLine->length())); } else { rangeInserted.setEnd(KTextEditor::Cursor(line + 1, 0)); } // remember last change cursor m_editLastChangeStartCursor = rangeInserted.start(); emit textInserted(this, rangeInserted); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editRemoveLine(int line) { return editRemoveLines(line, line); } bool KTextEditor::DocumentPrivate::editRemoveLines(int from, int to) { // verbose debug EDIT_DEBUG << "editRemoveLines" << from << to; if (to < from || from < 0 || to > lastLine()) { return false; } if (!isReadWrite()) { return false; } if (lines() == 1) { return editRemoveText(0, 0, kateTextLine(0)->length()); } editStart(); QStringList oldText; /** * first remove text */ for (int line = to; line >= from; --line) { Kate::TextLine tl = m_buffer->line(line); oldText.prepend(this->line(line)); m_undoManager->slotLineRemoved(line, this->line(line)); m_buffer->removeText(KTextEditor::Range(KTextEditor::Cursor(line, 0), KTextEditor::Cursor(line, tl->text().size()))); } /** * then collapse lines */ for (int line = to; line >= from; --line) { /** * unwrap all lines, prefer to unwrap line behind, skip to wrap line 0 */ if (line + 1 < m_buffer->lines()) { m_buffer->unwrapLine(line + 1); } else if (line) { m_buffer->unwrapLine(line); } } QList rmark; QList list; for (KTextEditor::Mark *mark : qAsConst(m_marks)) { int line = mark->line; if (line > to) { list << line; } else if (line >= from) { rmark << line; } } for (int line : qAsConst(rmark)) { delete m_marks.take(line); } for (int line : qAsConst(list)) { KTextEditor::Mark *mark = m_marks.take(line); mark->line -= to - from + 1; m_marks.insert(mark->line, mark); } if (!list.isEmpty()) { emit marksChanged(this); } KTextEditor::Range rangeRemoved(from, 0, to + 1, 0); if (to == lastLine() + to - from + 1) { rangeRemoved.setEnd(KTextEditor::Cursor(to, oldText.last().length())); if (from > 0) { Kate::TextLine prevLine = plainKateTextLine(from - 1); rangeRemoved.setStart(KTextEditor::Cursor(from - 1, prevLine->length())); } } // remember last change cursor m_editLastChangeStartCursor = rangeRemoved.start(); emit textRemoved(this, rangeRemoved, oldText.join(QLatin1Char('\n')) + QLatin1Char('\n')); editEnd(); return true; } // END // BEGIN KTextEditor::UndoInterface stuff uint KTextEditor::DocumentPrivate::undoCount() const { return m_undoManager->undoCount(); } uint KTextEditor::DocumentPrivate::redoCount() const { return m_undoManager->redoCount(); } void KTextEditor::DocumentPrivate::undo() { m_undoManager->undo(); } void KTextEditor::DocumentPrivate::redo() { m_undoManager->redo(); } // END // BEGIN KTextEditor::SearchInterface stuff QVector KTextEditor::DocumentPrivate::searchText(const KTextEditor::Range &range, const QString &pattern, const KTextEditor::SearchOptions options) const { const bool escapeSequences = options.testFlag(KTextEditor::EscapeSequences); const bool regexMode = options.testFlag(KTextEditor::Regex); const bool backwards = options.testFlag(KTextEditor::Backwards); const bool wholeWords = options.testFlag(KTextEditor::WholeWords); const Qt::CaseSensitivity caseSensitivity = options.testFlag(KTextEditor::CaseInsensitive) ? Qt::CaseInsensitive : Qt::CaseSensitive; if (regexMode) { // regexp search // escape sequences are supported by definition KateRegExpSearch searcher(this, caseSensitivity); return searcher.search(pattern, range, backwards); } if (escapeSequences) { // escaped search KatePlainTextSearch searcher(this, caseSensitivity, wholeWords); KTextEditor::Range match = searcher.search(KateRegExpSearch::escapePlaintext(pattern), range, backwards); QVector result; result.append(match); return result; } // plaintext search KatePlainTextSearch searcher(this, caseSensitivity, wholeWords); KTextEditor::Range match = searcher.search(pattern, range, backwards); QVector result; result.append(match); return result; } // END QWidget *KTextEditor::DocumentPrivate::dialogParent() { QWidget *w = widget(); if (!w) { w = activeView(); if (!w) { w = QApplication::activeWindow(); } } return w; } // BEGIN KTextEditor::HighlightingInterface stuff bool KTextEditor::DocumentPrivate::setMode(const QString &name) { return updateFileType(name); } KTextEditor::DefaultStyle KTextEditor::DocumentPrivate::defaultStyleAt(const KTextEditor::Cursor &position) const { // TODO, FIXME KDE5: in surrogate, use 2 bytes before if (!isValidTextPosition(position)) { return dsNormal; } int ds = const_cast(this)->defStyleNum(position.line(), position.column()); if (ds < 0 || ds > static_cast(dsError)) { return dsNormal; } return static_cast(ds); } QString KTextEditor::DocumentPrivate::mode() const { return m_fileType; } QStringList KTextEditor::DocumentPrivate::modes() const { QStringList m; const QList &modeList = KTextEditor::EditorPrivate::self()->modeManager()->list(); m.reserve(modeList.size()); for (KateFileType *type : modeList) { m << type->name; } return m; } bool KTextEditor::DocumentPrivate::setHighlightingMode(const QString &name) { int mode = KateHlManager::self()->nameFind(name); if (mode == -1) { return false; } m_buffer->setHighlight(mode); return true; } QString KTextEditor::DocumentPrivate::highlightingMode() const { return highlight()->name(); } QStringList KTextEditor::DocumentPrivate::highlightingModes() const { QStringList hls; for (const auto &hl : KateHlManager::self()->modeList()) { hls << hl.name(); } return hls; } QString KTextEditor::DocumentPrivate::highlightingModeSection(int index) const { return KateHlManager::self()->modeList().at(index).section(); } QString KTextEditor::DocumentPrivate::modeSection(int index) const { return KTextEditor::EditorPrivate::self()->modeManager()->list().at(index)->section; } void KTextEditor::DocumentPrivate::bufferHlChanged() { // update all views makeAttribs(false); // deactivate indenter if necessary m_indenter->checkRequiredStyle(); emit highlightingModeChanged(this); } void KTextEditor::DocumentPrivate::setDontChangeHlOnSave() { m_hlSetByUser = true; } void KTextEditor::DocumentPrivate::bomSetByUser() { m_bomSetByUser = true; } // END // BEGIN KTextEditor::SessionConfigInterface and KTextEditor::ParameterizedSessionConfigInterface stuff void KTextEditor::DocumentPrivate::readSessionConfig(const KConfigGroup &kconfig, const QSet &flags) { if (!flags.contains(QStringLiteral("SkipEncoding"))) { // get the encoding QString tmpenc = kconfig.readEntry("Encoding"); if (!tmpenc.isEmpty() && (tmpenc != encoding())) { setEncoding(tmpenc); } } if (!flags.contains(QStringLiteral("SkipUrl"))) { // restore the url QUrl url(kconfig.readEntry("URL")); // open the file if url valid if (!url.isEmpty() && url.isValid()) { openUrl(url); } else { completed(); // perhaps this should be emitted at the end of this function } } else { completed(); // perhaps this should be emitted at the end of this function } if (!flags.contains(QStringLiteral("SkipMode"))) { // restore the filetype // NOTE: if the session config file contains an invalid Mode // (for example, one that was deleted or renamed), do not apply it if (kconfig.hasKey("Mode") && updateFileType(kconfig.readEntry("Mode"))) { // restore if set by user, too! m_fileTypeSetByUser = kconfig.readEntry("Mode Set By User", false); } } if (!flags.contains(QStringLiteral("SkipHighlighting"))) { // restore the hl stuff if (kconfig.hasKey("Highlighting")) { const int mode = KateHlManager::self()->nameFind(kconfig.readEntry("Highlighting")); if (mode >= 0) { m_buffer->setHighlight(mode); // restore if set by user, too! see bug 332605, otherwise we loose the hl later again on save m_hlSetByUser = kconfig.readEntry("Highlighting Set By User", false); } } } // indent mode config()->setIndentationMode(kconfig.readEntry("Indentation Mode", config()->indentationMode())); // Restore Bookmarks const QList marks = kconfig.readEntry("Bookmarks", QList()); for (int i = 0; i < marks.count(); i++) { addMark(marks.at(i), KTextEditor::DocumentPrivate::markType01); } } void KTextEditor::DocumentPrivate::writeSessionConfig(KConfigGroup &kconfig, const QSet &flags) { if (this->url().isLocalFile()) { const QString path = this->url().toLocalFile(); if (path.startsWith(QDir::tempPath())) { return; // inside tmp resource, do not save } } if (!flags.contains(QStringLiteral("SkipUrl"))) { // save url kconfig.writeEntry("URL", this->url().toString()); } if (!flags.contains(QStringLiteral("SkipEncoding"))) { // save encoding kconfig.writeEntry("Encoding", encoding()); } if (!flags.contains(QStringLiteral("SkipMode"))) { // save file type kconfig.writeEntry("Mode", m_fileType); // save if set by user, too! kconfig.writeEntry("Mode Set By User", m_fileTypeSetByUser); } if (!flags.contains(QStringLiteral("SkipHighlighting"))) { // save hl kconfig.writeEntry("Highlighting", highlight()->name()); // save if set by user, too! see bug 332605, otherwise we loose the hl later again on save kconfig.writeEntry("Highlighting Set By User", m_hlSetByUser); } // indent mode kconfig.writeEntry("Indentation Mode", config()->indentationMode()); // Save Bookmarks QList marks; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) if (i.value()->type & KTextEditor::MarkInterface::markType01) { marks << i.value()->line; } kconfig.writeEntry("Bookmarks", marks); } // END KTextEditor::SessionConfigInterface and KTextEditor::ParameterizedSessionConfigInterface stuff uint KTextEditor::DocumentPrivate::mark(int line) { KTextEditor::Mark *m = m_marks.value(line); if (!m) { return 0; } return m->type; } void KTextEditor::DocumentPrivate::setMark(int line, uint markType) { clearMark(line); addMark(line, markType); } void KTextEditor::DocumentPrivate::clearMark(int line) { if (line < 0 || line > lastLine()) { return; } if (!m_marks.value(line)) { return; } KTextEditor::Mark *mark = m_marks.take(line); emit markChanged(this, *mark, MarkRemoved); emit marksChanged(this); delete mark; tagLines(line, line); repaintViews(true); } void KTextEditor::DocumentPrivate::addMark(int line, uint markType) { KTextEditor::Mark *mark; if (line < 0 || line > lastLine()) { return; } if (markType == 0) { return; } if ((mark = m_marks.value(line))) { // Remove bits already set markType &= ~mark->type; if (markType == 0) { return; } // Add bits mark->type |= markType; } else { mark = new KTextEditor::Mark; mark->line = line; mark->type = markType; m_marks.insert(line, mark); } // Emit with a mark having only the types added. KTextEditor::Mark temp; temp.line = line; temp.type = markType; emit markChanged(this, temp, MarkAdded); emit marksChanged(this); tagLines(line, line); repaintViews(true); } void KTextEditor::DocumentPrivate::removeMark(int line, uint markType) { if (line < 0 || line > lastLine()) { return; } KTextEditor::Mark *mark = m_marks.value(line); if (!mark) { return; } // Remove bits not set markType &= mark->type; if (markType == 0) { return; } // Subtract bits mark->type &= ~markType; // Emit with a mark having only the types removed. KTextEditor::Mark temp; temp.line = line; temp.type = markType; emit markChanged(this, temp, MarkRemoved); if (mark->type == 0) { m_marks.remove(line); delete mark; } emit marksChanged(this); tagLines(line, line); repaintViews(true); } const QHash &KTextEditor::DocumentPrivate::marks() { return m_marks; } void KTextEditor::DocumentPrivate::requestMarkTooltip(int line, QPoint position) { KTextEditor::Mark *mark = m_marks.value(line); if (!mark) { return; } bool handled = false; emit markToolTipRequested(this, *mark, position, handled); } bool KTextEditor::DocumentPrivate::handleMarkClick(int line) { bool handled = false; KTextEditor::Mark *mark = m_marks.value(line); if (!mark) { emit markClicked(this, KTextEditor::Mark {line, 0}, handled); } else { emit markClicked(this, *mark, handled); } return handled; } bool KTextEditor::DocumentPrivate::handleMarkContextMenu(int line, QPoint position) { bool handled = false; KTextEditor::Mark *mark = m_marks.value(line); if (!mark) { emit markContextMenuRequested(this, KTextEditor::Mark {line, 0}, position, handled); } else { emit markContextMenuRequested(this, *mark, position, handled); } return handled; } void KTextEditor::DocumentPrivate::clearMarks() { while (!m_marks.isEmpty()) { QHash::iterator it = m_marks.begin(); KTextEditor::Mark mark = *it.value(); delete it.value(); m_marks.erase(it); emit markChanged(this, mark, MarkRemoved); tagLines(mark.line, mark.line); } m_marks.clear(); emit marksChanged(this); repaintViews(true); } void KTextEditor::DocumentPrivate::setMarkPixmap(MarkInterface::MarkTypes type, const QPixmap &pixmap) { - m_markPixmaps.insert(type, pixmap); + m_markIcons.insert(type, QVariant::fromValue(pixmap)); } void KTextEditor::DocumentPrivate::setMarkDescription(MarkInterface::MarkTypes type, const QString &description) { m_markDescriptions.insert(type, description); } QPixmap KTextEditor::DocumentPrivate::markPixmap(MarkInterface::MarkTypes type) const { - return m_markPixmaps.value(type, QPixmap()); + auto icon = m_markIcons.value(type, QVariant::fromValue(QPixmap())); + return (static_cast(icon.type()) == QMetaType::QIcon) ? icon.value().pixmap(32) : icon.value(); } QColor KTextEditor::DocumentPrivate::markColor(MarkInterface::MarkTypes type) const { uint reserved = (0x1 << KTextEditor::MarkInterface::reservedMarkersCount()) - 1; if ((uint)type >= (uint)markType01 && (uint)type <= reserved) { return KateRendererConfig::global()->lineMarkerColor(type); } else { return QColor(); } } QString KTextEditor::DocumentPrivate::markDescription(MarkInterface::MarkTypes type) const { return m_markDescriptions.value(type, QString()); } void KTextEditor::DocumentPrivate::setEditableMarks(uint markMask) { m_editableMarks = markMask; } uint KTextEditor::DocumentPrivate::editableMarks() const { return m_editableMarks; } // END +void KTextEditor::DocumentPrivate::setMarkIcon(MarkInterface::MarkTypes markType, const QIcon &icon) +{ + m_markIcons.insert(markType, QVariant::fromValue(icon)); +} + +QIcon KTextEditor::DocumentPrivate::markIcon(MarkInterface::MarkTypes markType) const +{ + auto icon = m_markIcons.value(markType, QVariant::fromValue(QIcon())); + return (static_cast(icon.type()) == QMetaType::QIcon) ? icon.value() : QIcon(icon.value()); +} + // BEGIN KTextEditor::PrintInterface stuff bool KTextEditor::DocumentPrivate::print() { return KatePrinter::print(this); } void KTextEditor::DocumentPrivate::printPreview() { KatePrinter::printPreview(this); } // END KTextEditor::PrintInterface stuff // BEGIN KTextEditor::DocumentInfoInterface (### unfinished) QString KTextEditor::DocumentPrivate::mimeType() { /** * collect first 4k of text * only heuristic */ QByteArray buf; for (int i = 0; (i < lines()) && (buf.size() <= 4096); ++i) { buf.append(line(i).toUtf8()); buf.append('\n'); } // use path of url, too, if set if (!url().path().isEmpty()) { return QMimeDatabase().mimeTypeForFileNameAndData(url().path(), buf).name(); } // else only use the content return QMimeDatabase().mimeTypeForData(buf).name(); } // END KTextEditor::DocumentInfoInterface // BEGIN: error void KTextEditor::DocumentPrivate::showAndSetOpeningErrorAccess() { QPointer message = new KTextEditor::Message( i18n("The file %1 could not be loaded, as it was not possible to read from it.
Check if you have read access to this file.", this->url().toDisplayString(QUrl::PreferLocalFile)), KTextEditor::Message::Error); message->setWordWrap(true); QAction *tryAgainAction = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("translators: you can also translate 'Try Again' with 'Reload'", "Try Again"), nullptr); connect(tryAgainAction, SIGNAL(triggered()), SLOT(documentReload()), Qt::QueuedConnection); QAction *closeAction = new QAction(QIcon::fromTheme(QStringLiteral("window-close")), i18n("&Close"), nullptr); closeAction->setToolTip(i18n("Close message")); // add try again and close actions message->addAction(tryAgainAction); message->addAction(closeAction); // finally post message postMessage(message); // remember error m_openingError = true; m_openingErrorMessage = i18n("The file %1 could not be loaded, as it was not possible to read from it.\n\nCheck if you have read access to this file.", this->url().toDisplayString(QUrl::PreferLocalFile)); } // END: error void KTextEditor::DocumentPrivate::openWithLineLengthLimitOverride() { // raise line length limit to the next power of 2 const int longestLine = m_buffer->longestLineLoaded(); int newLimit = pow(2, ceil(log2(longestLine))); if (newLimit <= longestLine) { newLimit *= 2; } // do the raise config()->setLineLengthLimit(newLimit); // just reload m_buffer->clear(); openFile(); if (!m_openingError) { setReadWrite(true); m_readWriteStateBeforeLoading = true; } } int KTextEditor::DocumentPrivate::lineLengthLimit() const { return config()->lineLengthLimit(); } // BEGIN KParts::ReadWrite stuff bool KTextEditor::DocumentPrivate::openFile() { /** * we are about to invalidate all cursors/ranges/.. => m_buffer->openFile will do so */ emit aboutToInvalidateMovingInterfaceContent(this); // no open errors until now... m_openingError = false; m_openingErrorMessage.clear(); // add new m_file to dirwatch activateDirWatch(); // remember current encoding QString currentEncoding = encoding(); // // mime type magic to get encoding right // QString mimeType = arguments().mimeType(); int pos = mimeType.indexOf(QLatin1Char(';')); if (pos != -1 && !(m_reloading && m_userSetEncodingForNextReload)) { setEncoding(mimeType.mid(pos + 1)); } // update file type, we do this here PRE-LOAD, therefore pass file name for reading from updateFileType(KTextEditor::EditorPrivate::self()->modeManager()->fileType(this, localFilePath())); // read dir config (if possible and wanted) // do this PRE-LOAD to get encoding info! readDirConfig(); // perhaps we need to re-set again the user encoding if (m_reloading && m_userSetEncodingForNextReload && (currentEncoding != encoding())) { setEncoding(currentEncoding); } bool success = m_buffer->openFile(localFilePath(), (m_reloading && m_userSetEncodingForNextReload)); // // yeah, success // read variables // if (success) { readVariables(); } // // update views // for (auto view : qAsConst(m_views)) { // This is needed here because inserting the text moves the view's start position (it is a MovingCursor) view->setCursorPosition(KTextEditor::Cursor()); view->updateView(true); } // Inform that the text has changed (required as we're not inside the usual editStart/End stuff) emit textChanged(this); emit loaded(this); // // to houston, we are not modified // if (m_modOnHd) { m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, m_modOnHd, m_modOnHdReason); } // // display errors // if (!success) { showAndSetOpeningErrorAccess(); } // warn: broken encoding if (m_buffer->brokenEncoding()) { // this file can't be saved again without killing it setReadWrite(false); m_readWriteStateBeforeLoading = false; QPointer message = new KTextEditor::Message(i18n("The file %1 was opened with %2 encoding but contained invalid characters.
" "It is set to read-only mode, as saving might destroy its content.
" "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.", this->url().toDisplayString(QUrl::PreferLocalFile), QString::fromLatin1(m_buffer->textCodec()->name())), KTextEditor::Message::Warning); message->setWordWrap(true); postMessage(message); // remember error m_openingError = true; m_openingErrorMessage = i18n( "The file %1 was opened with %2 encoding but contained invalid characters." " It is set to read-only mode, as saving might destroy its content." " 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.", this->url().toDisplayString(QUrl::PreferLocalFile), QString::fromLatin1(m_buffer->textCodec()->name())); } // warn: too long lines if (m_buffer->tooLongLinesWrapped()) { // this file can't be saved again without modifications setReadWrite(false); m_readWriteStateBeforeLoading = false; QPointer message = new KTextEditor::Message(i18n("The file %1 was opened and contained lines longer than the configured Line Length Limit (%2 characters).
" "The longest of those lines was %3 characters long
" "Those lines were wrapped and the document is set to read-only mode, as saving will modify its content.", this->url().toDisplayString(QUrl::PreferLocalFile), config()->lineLengthLimit(), m_buffer->longestLineLoaded()), KTextEditor::Message::Warning); QAction *increaseAndReload = new QAction(i18n("Temporarily raise limit and reload file"), message); connect(increaseAndReload, SIGNAL(triggered()), this, SLOT(openWithLineLengthLimitOverride())); message->addAction(increaseAndReload, true); message->addAction(new QAction(i18n("Close"), message), true); message->setWordWrap(true); postMessage(message); // remember error m_openingError = true; m_openingErrorMessage = i18n( "The file %1 was opened and contained lines longer than the configured Line Length Limit (%2 characters).
" "The longest of those lines was %3 characters long
" "Those lines were wrapped and the document is set to read-only mode, as saving will modify its content.", this->url().toDisplayString(QUrl::PreferLocalFile), config()->lineLengthLimit(), m_buffer->longestLineLoaded()); } // // return the success // return success; } bool KTextEditor::DocumentPrivate::saveFile() { // delete pending mod-on-hd message if applicable. delete m_modOnHdHandler; // some warnings, if file was changed by the outside! if (!url().isEmpty()) { if (m_fileChangedDialogsActivated && m_modOnHd) { QString str = reasonedMOHString() + QLatin1String("\n\n"); if (!isModified()) { if (KMessageBox::warningContinueCancel( dialogParent(), str + i18n("Do you really want to save this unmodified file? You could overwrite changed data in the file on disk."), i18n("Trying to Save Unmodified File"), KGuiItem(i18n("Save Nevertheless"))) != KMessageBox::Continue) { return false; } } else { if (KMessageBox::warningContinueCancel(dialogParent(), str + i18n("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."), i18n("Possible Data Loss"), KGuiItem(i18n("Save Nevertheless"))) != KMessageBox::Continue) { return false; } } } } // // can we encode it if we want to save it ? // if (!m_buffer->canEncode() && (KMessageBox::warningContinueCancel(dialogParent(), i18n("The selected encoding cannot encode every Unicode character in this document. Do you really want to save it? There could be some data lost."), i18n("Possible Data Loss"), KGuiItem(i18n("Save Nevertheless"))) != KMessageBox::Continue)) { return false; } /** * create a backup file or abort if that fails! * if no backup file wanted, this routine will just return true */ if (!createBackupFile()) return false; // update file type, pass no file path, read file type content from this document QString oldPath = m_dirWatchFile; // only update file type if path has changed so that variables are not overridden on normal save if (oldPath != localFilePath()) { updateFileType(KTextEditor::EditorPrivate::self()->modeManager()->fileType(this, QString())); if (url().isLocalFile()) { // if file is local then read dir config for new path readDirConfig(); } } // read our vars readVariables(); // remove file from dirwatch deactivateDirWatch(); // remove all trailing spaces in the document (as edit actions) // NOTE: we need this as edit actions, since otherwise the edit actions // in the swap file recovery may happen at invalid cursor positions removeTrailingSpaces(); // // try to save // if (!m_buffer->saveFile(localFilePath())) { // add m_file again to dirwatch activateDirWatch(oldPath); KMessageBox::error(dialogParent(), 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 that enough disk space is available.\nThe original file may be lost or damaged. " "Don't quit the application until the file is successfully written.", this->url().toDisplayString(QUrl::PreferLocalFile))); return false; } // update the checksum createDigest(); // add m_file again to dirwatch activateDirWatch(); // // we are not modified // if (m_modOnHd) { m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, m_modOnHd, m_modOnHdReason); } // (dominik) mark last undo group as not mergeable, otherwise the next // edit action might be merged and undo will never stop at the saved state m_undoManager->undoSafePoint(); m_undoManager->updateLineModifications(); // // return success // return true; } bool KTextEditor::DocumentPrivate::createBackupFile() { /** * backup for local or remote files wanted? */ const bool backupLocalFiles = config()->backupOnSaveLocal(); const bool backupRemoteFiles = config()->backupOnSaveRemote(); /** * early out, before mount check: backup wanted at all? * => if not, all fine, just return */ if (!backupLocalFiles && !backupRemoteFiles) { return true; } /** * decide if we need backup based on locality * skip that, if we always want backups, as currentMountPoints is not that fast */ QUrl u(url()); bool needBackup = backupLocalFiles && backupRemoteFiles; if (!needBackup) { bool slowOrRemoteFile = !u.isLocalFile(); if (!slowOrRemoteFile) { // could be a mounted remote filesystem (e.g. nfs, sshfs, cifs) // we have the early out above to skip this, if we want no backup, which is the default KMountPoint::Ptr mountPoint = KMountPoint::currentMountPoints().findByDevice(u.toLocalFile()); slowOrRemoteFile = (mountPoint && mountPoint->probablySlow()); } needBackup = (!slowOrRemoteFile && backupLocalFiles) || (slowOrRemoteFile && backupRemoteFiles); } /** * no backup needed? be done */ if (!needBackup) { return true; } /** * else: try to backup */ const auto backupPrefix = KTextEditor::EditorPrivate::self()->variableExpansionManager()->expandText(config()->backupPrefix(), nullptr); const auto backupSuffix = KTextEditor::EditorPrivate::self()->variableExpansionManager()->expandText(config()->backupSuffix(), nullptr); if (backupPrefix.isEmpty() && backupSuffix.isEmpty()) { // no sane backup possible return true; } if (backupPrefix.contains(QDir::separator())) { /** * replace complete path, as prefix is a path! */ u.setPath(backupPrefix + u.fileName() + backupSuffix); } else { /** * replace filename in url */ const QString fileName = u.fileName(); u = u.adjusted(QUrl::RemoveFilename); u.setPath(u.path() + backupPrefix + fileName + backupSuffix); } qCDebug(LOG_KTE) << "backup src file name: " << url(); qCDebug(LOG_KTE) << "backup dst file name: " << u; // handle the backup... bool backupSuccess = false; // local file mode, no kio if (u.isLocalFile()) { if (QFile::exists(url().toLocalFile())) { // first: check if backupFile is already there, if true, unlink it QFile backupFile(u.toLocalFile()); if (backupFile.exists()) { backupFile.remove(); } backupSuccess = QFile::copy(url().toLocalFile(), u.toLocalFile()); } else { backupSuccess = true; } } else { // remote file mode, kio // get the right permissions, start with safe default KIO::StatJob *statJob = KIO::stat(url(), KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, QApplication::activeWindow()); if (statJob->exec()) { // do a evil copy which will overwrite target if possible KFileItem item(statJob->statResult(), url()); KIO::FileCopyJob *job = KIO::file_copy(url(), u, item.permissions(), KIO::Overwrite); KJobWidgets::setWindow(job, QApplication::activeWindow()); backupSuccess = job->exec(); } else { backupSuccess = true; } } // backup has failed, ask user how to proceed if (!backupSuccess && (KMessageBox::warningContinueCancel(dialogParent(), i18n("For file %1 no backup copy could be created before saving." " If an error occurs while saving, you might lose the data of this file." " A reason could be that the media you write to is full or the directory of the file is read-only for you.", url().toDisplayString(QUrl::PreferLocalFile)), i18n("Failed to create backup copy."), KGuiItem(i18n("Try to Save Nevertheless")), KStandardGuiItem::cancel(), QStringLiteral("Backup Failed Warning")) != KMessageBox::Continue)) { return false; } return true; } void KTextEditor::DocumentPrivate::readDirConfig() { if (!url().isLocalFile()) { return; } /** * first search .kateconfig upwards * with recursion guard */ QSet seenDirectories; QDir dir(QFileInfo(localFilePath()).absolutePath()); while (!seenDirectories.contains(dir.absolutePath())) { /** * fill recursion guard */ seenDirectories.insert(dir.absolutePath()); // try to open config file in this dir QFile f(dir.absolutePath() + QLatin1String("/.kateconfig")); if (f.open(QIODevice::ReadOnly)) { QTextStream stream(&f); uint linesRead = 0; QString line = stream.readLine(); while ((linesRead < 32) && !line.isNull()) { readVariableLine(line); line = stream.readLine(); linesRead++; } return; } /** * else: cd up, if possible or abort */ if (!dir.cdUp()) { break; } } #if EDITORCONFIG_FOUND // if there wasn’t any .kateconfig file and KTextEditor was compiled with // EditorConfig support, try to load document config from a .editorconfig // file, if such is provided EditorConfig editorConfig(this); editorConfig.parse(); #endif } void KTextEditor::DocumentPrivate::activateDirWatch(const QString &useFileName) { QString fileToUse = useFileName; if (fileToUse.isEmpty()) { fileToUse = localFilePath(); } QFileInfo fileInfo = QFileInfo(fileToUse); if (fileInfo.isSymLink()) { // Monitor the actual data and not the symlink fileToUse = fileInfo.canonicalFilePath(); } // same file as we are monitoring, return if (fileToUse == m_dirWatchFile) { return; } // remove the old watched file deactivateDirWatch(); // add new file if needed if (url().isLocalFile() && !fileToUse.isEmpty()) { KTextEditor::EditorPrivate::self()->dirWatch()->addFile(fileToUse); m_dirWatchFile = fileToUse; } } void KTextEditor::DocumentPrivate::deactivateDirWatch() { if (!m_dirWatchFile.isEmpty()) { KTextEditor::EditorPrivate::self()->dirWatch()->removeFile(m_dirWatchFile); } m_dirWatchFile.clear(); } bool KTextEditor::DocumentPrivate::openUrl(const QUrl &url) { if (!m_reloading) { // Reset filetype when opening url m_fileTypeSetByUser = false; } bool res = KTextEditor::Document::openUrl(normalizeUrl(url)); updateDocName(); return res; } bool KTextEditor::DocumentPrivate::closeUrl() { // // file mod on hd // if (!m_reloading && !url().isEmpty()) { if (m_fileChangedDialogsActivated && m_modOnHd) { // make sure to not forget pending mod-on-hd handler delete m_modOnHdHandler; QWidget *parentWidget(dialogParent()); if (!(KMessageBox::warningContinueCancel(parentWidget, reasonedMOHString() + QLatin1String("\n\n") + i18n("Do you really want to continue to close this file? Data loss may occur."), i18n("Possible Data Loss"), KGuiItem(i18n("Close Nevertheless")), KStandardGuiItem::cancel(), QStringLiteral("kate_close_modonhd_%1").arg(m_modOnHdReason)) == KMessageBox::Continue)) { /** * reset reloading */ m_reloading = false; return false; } } } // // first call the normal kparts implementation // if (!KParts::ReadWritePart::closeUrl()) { /** * reset reloading */ m_reloading = false; return false; } // Tell the world that we're about to go ahead with the close if (!m_reloading) { emit aboutToClose(this); } /** * delete all KTE::Messages */ if (!m_messageHash.isEmpty()) { const auto keys = m_messageHash.keys(); for (KTextEditor::Message *message : keys) { delete message; } } /** * we are about to invalidate all cursors/ranges/.. => m_buffer->clear will do so */ emit aboutToInvalidateMovingInterfaceContent(this); // remove file from dirwatch deactivateDirWatch(); // // empty url + fileName // setUrl(QUrl()); setLocalFilePath(QString()); // we are not modified if (m_modOnHd) { m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, m_modOnHd, m_modOnHdReason); } // remove all marks clearMarks(); // clear the buffer m_buffer->clear(); // clear undo/redo history m_undoManager->clearUndo(); m_undoManager->clearRedo(); // no, we are no longer modified setModified(false); // we have no longer any hl m_buffer->setHighlight(0); // update all our views for (auto view : qAsConst(m_views)) { view->clearSelection(); // fix bug #118588 view->clear(); } // purge swap file if (m_swapfile) { m_swapfile->fileClosed(); } // success return true; } bool KTextEditor::DocumentPrivate::isDataRecoveryAvailable() const { return m_swapfile && m_swapfile->shouldRecover(); } void KTextEditor::DocumentPrivate::recoverData() { if (isDataRecoveryAvailable()) { m_swapfile->recover(); } } void KTextEditor::DocumentPrivate::discardDataRecovery() { if (isDataRecoveryAvailable()) { m_swapfile->discard(); } } void KTextEditor::DocumentPrivate::setReadWrite(bool rw) { if (isReadWrite() == rw) { return; } KParts::ReadWritePart::setReadWrite(rw); for (auto view : qAsConst(m_views)) { view->slotUpdateUndo(); view->slotReadWriteChanged(); } emit readWriteChanged(this); } void KTextEditor::DocumentPrivate::setModified(bool m) { if (isModified() != m) { KParts::ReadWritePart::setModified(m); for (auto view : qAsConst(m_views)) { view->slotUpdateUndo(); } emit modifiedChanged(this); } m_undoManager->setModified(m); } // END // BEGIN Kate specific stuff ;) void KTextEditor::DocumentPrivate::makeAttribs(bool needInvalidate) { for (auto view : qAsConst(m_views)) { view->renderer()->updateAttributes(); } if (needInvalidate) { m_buffer->invalidateHighlighting(); } for (auto view : qAsConst(m_views)) { view->tagAll(); view->updateView(true); } } // the attributes of a hl have changed, update void KTextEditor::DocumentPrivate::internalHlChanged() { makeAttribs(); } void KTextEditor::DocumentPrivate::addView(KTextEditor::View *view) { Q_ASSERT(!m_views.contains(view)); m_views.insert(view, static_cast(view)); m_viewsCache.append(view); // apply the view & renderer vars from the file type if (!m_fileType.isEmpty()) { readVariableLine(KTextEditor::EditorPrivate::self()->modeManager()->fileType(m_fileType).varLine, true); } // apply the view & renderer vars from the file readVariables(true); setActiveView(view); } void KTextEditor::DocumentPrivate::removeView(KTextEditor::View *view) { Q_ASSERT(m_views.contains(view)); m_views.remove(view); m_viewsCache.removeAll(view); if (activeView() == view) { setActiveView(nullptr); } } void KTextEditor::DocumentPrivate::setActiveView(KTextEditor::View *view) { if (m_activeView == view) { return; } m_activeView = static_cast(view); } bool KTextEditor::DocumentPrivate::ownedView(KTextEditor::ViewPrivate *view) { // do we own the given view? return (m_views.contains(view)); } int KTextEditor::DocumentPrivate::toVirtualColumn(int line, int column) const { Kate::TextLine textLine = m_buffer->plainLine(line); if (textLine) { return textLine->toVirtualColumn(column, config()->tabWidth()); } else { return 0; } } int KTextEditor::DocumentPrivate::toVirtualColumn(const KTextEditor::Cursor &cursor) const { return toVirtualColumn(cursor.line(), cursor.column()); } int KTextEditor::DocumentPrivate::fromVirtualColumn(int line, int column) const { Kate::TextLine textLine = m_buffer->plainLine(line); if (textLine) { return textLine->fromVirtualColumn(column, config()->tabWidth()); } else { return 0; } } int KTextEditor::DocumentPrivate::fromVirtualColumn(const KTextEditor::Cursor &cursor) const { return fromVirtualColumn(cursor.line(), cursor.column()); } void KTextEditor::DocumentPrivate::typeChars(KTextEditor::ViewPrivate *view, QString chars) { // nop for empty chars if (chars.isEmpty()) { return; } // auto bracket handling QChar closingBracket; if (view->config()->autoBrackets()) { // Check if entered closing bracket is already balanced const QChar typedChar = chars.at(0); const QChar openBracket = matchingStartBracket(typedChar); if (!openBracket.isNull()) { KTextEditor::Cursor curPos = view->cursorPosition(); if ((characterAt(curPos) == typedChar) && findMatchingBracket(curPos, 123 /*Which value may best?*/).isValid()) { // Do nothing view->cursorRight(); return; } } // for newly inserted text: remember if we should auto add some bracket if (chars.size() == 1) { // we inserted a bracket? => remember the matching closing one closingBracket = matchingEndBracket(typedChar); // closing bracket for the autobracket we inserted earlier? if (m_currentAutobraceClosingChar == typedChar && m_currentAutobraceRange) { // do nothing m_currentAutobraceRange.reset(nullptr); view->cursorRight(); return; } } } // Treat some char also as "auto bracket" only when we have a selection if (view->selection() && closingBracket.isNull() && view->config()->encloseSelectionInChars()) { const QChar typedChar = chars.at(0); if (view->config()->charsToEncloseSelection().contains(typedChar)) { // The unconditional mirroring cause no harm, but allows funny brackets closingBracket = typedChar.mirroredChar(); } } editStart(); /** * special handling if we want to add auto brackets to a selection */ if (view->selection() && !closingBracket.isNull()) { std::unique_ptr selectionRange(newMovingRange(view->selectionRange())); const int startLine = qMax(0, selectionRange->start().line()); const int endLine = qMin(selectionRange->end().line(), lastLine()); const bool blockMode = view->blockSelection() && (startLine != endLine); if (blockMode) { if (selectionRange->start().column() > selectionRange->end().column()) { // Selection was done from right->left, requires special setting to ensure the new // added brackets will not be part of the selection selectionRange->setInsertBehaviors(MovingRange::ExpandLeft | MovingRange::ExpandRight); } // Add brackets to each line of the block const int startColumn = qMin(selectionRange->start().column(), selectionRange->end().column()); const int endColumn = qMax(selectionRange->start().column(), selectionRange->end().column()); const KTextEditor::Range workingRange(startLine, startColumn, endLine, endColumn); for (int line = startLine; line <= endLine; ++line) { const KTextEditor::Range r(rangeOnLine(workingRange, line)); insertText(r.end(), QString(closingBracket)); view->slotTextInserted(view, r.end(), QString(closingBracket)); insertText(r.start(), chars); view->slotTextInserted(view, r.start(), chars); } } else { // No block, just add to start & end of selection insertText(selectionRange->end(), QString(closingBracket)); view->slotTextInserted(view, selectionRange->end(), QString(closingBracket)); insertText(selectionRange->start(), chars); view->slotTextInserted(view, selectionRange->start(), chars); } // Refresh selection view->setSelection(selectionRange->toRange()); view->setCursorPosition(selectionRange->end()); editEnd(); return; } /** * normal handling */ if (!view->config()->persistentSelection() && view->selection()) { view->removeSelectedText(); } const KTextEditor::Cursor oldCur(view->cursorPosition()); const bool multiLineBlockMode = view->blockSelection() && view->selection(); if (view->currentInputMode()->overwrite()) { // blockmode multiline selection case: remove chars in every line const KTextEditor::Range selectionRange = view->selectionRange(); const int startLine = multiLineBlockMode ? qMax(0, selectionRange.start().line()) : view->cursorPosition().line(); const int endLine = multiLineBlockMode ? qMin(selectionRange.end().line(), lastLine()) : startLine; const int virtualColumn = toVirtualColumn(multiLineBlockMode ? selectionRange.end() : view->cursorPosition()); for (int line = endLine; line >= startLine; --line) { Kate::TextLine textLine = m_buffer->plainLine(line); Q_ASSERT(textLine); const int column = fromVirtualColumn(line, virtualColumn); KTextEditor::Range r = KTextEditor::Range(KTextEditor::Cursor(line, column), qMin(chars.length(), textLine->length() - column)); // replace mode needs to know what was removed so it can be restored with backspace if (oldCur.column() < lineLength(line)) { QChar removed = characterAt(KTextEditor::Cursor(line, column)); view->currentInputMode()->overwrittenChar(removed); } removeText(r); } } chars = eventuallyReplaceTabs(view->cursorPosition(), chars); if (multiLineBlockMode) { KTextEditor::Range selectionRange = view->selectionRange(); const int startLine = qMax(0, selectionRange.start().line()); const int endLine = qMin(selectionRange.end().line(), lastLine()); const int column = toVirtualColumn(selectionRange.end()); for (int line = endLine; line >= startLine; --line) { editInsertText(line, fromVirtualColumn(line, column), chars); } int newSelectionColumn = toVirtualColumn(view->cursorPosition()); selectionRange.setRange(KTextEditor::Cursor(selectionRange.start().line(), fromVirtualColumn(selectionRange.start().line(), newSelectionColumn)), KTextEditor::Cursor(selectionRange.end().line(), fromVirtualColumn(selectionRange.end().line(), newSelectionColumn))); view->setSelection(selectionRange); } else { insertText(view->cursorPosition(), chars); } /** * auto bracket handling for newly inserted text * we inserted a bracket? * => add the matching closing one to the view + input chars * try to preserve the cursor position */ bool skipAutobrace = closingBracket == QLatin1Char('\''); if (highlight() && skipAutobrace) { // skip adding ' in spellchecked areas, because those are text skipAutobrace = highlight()->spellCheckingRequiredForLocation(this, view->cursorPosition() - Cursor {0, 1}); } const auto cursorPos(view->cursorPosition()); if (!skipAutobrace && (closingBracket == QLatin1Char('\''))) { // skip auto quotes when these looks already balanced, bug 405089 Kate::TextLine textLine = m_buffer->plainLine(cursorPos.line()); // RegEx match quote, but not excaped quote, thanks to https://stackoverflow.com/a/11819111 const int count = textLine->text().left(cursorPos.column()).count(QRegularExpression(QStringLiteral("(?plainLine(cursorPos.line()); const int count = textLine->text().left(cursorPos.column()).count(QRegularExpression(QStringLiteral("(?document()->text({cursorPos, cursorPos + Cursor {0, 1}}).trimmed(); if (nextChar.isEmpty() || !nextChar.at(0).isLetterOrNumber()) { insertText(view->cursorPosition(), QString(closingBracket)); const auto insertedAt(view->cursorPosition()); view->setCursorPosition(cursorPos); m_currentAutobraceRange.reset(newMovingRange({cursorPos - Cursor {0, 1}, insertedAt}, KTextEditor::MovingRange::DoNotExpand)); connect(view, &View::cursorPositionChanged, this, &DocumentPrivate::checkCursorForAutobrace, Qt::UniqueConnection); // add bracket to chars inserted! needed for correct signals + indent chars.append(closingBracket); } m_currentAutobraceClosingChar = closingBracket; } // end edit session here, to have updated HL in userTypedChar! editEnd(); // trigger indentation KTextEditor::Cursor b(view->cursorPosition()); m_indenter->userTypedChar(view, b, chars.isEmpty() ? QChar() : chars.at(chars.length() - 1)); /** * inform the view about the original inserted chars */ view->slotTextInserted(view, oldCur, chars); } void KTextEditor::DocumentPrivate::checkCursorForAutobrace(KTextEditor::View *, const KTextEditor::Cursor &newPos) { if (m_currentAutobraceRange && !m_currentAutobraceRange->toRange().contains(newPos)) { m_currentAutobraceRange.clear(); } } void KTextEditor::DocumentPrivate::newLine(KTextEditor::ViewPrivate *v, KTextEditor::DocumentPrivate::NewLineIndent indent) { editStart(); if (!v->config()->persistentSelection() && v->selection()) { v->removeSelectedText(); v->clearSelection(); } // query cursor position KTextEditor::Cursor c = v->cursorPosition(); if (c.line() > lastLine()) { c.setLine(lastLine()); } if (c.line() < 0) { c.setLine(0); } int ln = c.line(); Kate::TextLine textLine = plainKateTextLine(ln); if (c.column() > textLine->length()) { c.setColumn(textLine->length()); } // first: wrap line editWrapLine(c.line(), c.column()); // end edit session here, to have updated HL in userTypedChar! editEnd(); // second: if "indent" is true, indent the new line, if needed... if (indent == KTextEditor::DocumentPrivate::Indent) { m_indenter->userTypedChar(v, v->cursorPosition(), QLatin1Char('\n')); } } void KTextEditor::DocumentPrivate::transpose(const KTextEditor::Cursor &cursor) { Kate::TextLine textLine = m_buffer->plainLine(cursor.line()); if (!textLine || (textLine->length() < 2)) { return; } uint col = cursor.column(); if (col > 0) { col--; } if ((textLine->length() - col) < 2) { return; } uint line = cursor.line(); QString s; // clever swap code if first character on the line swap right&left // otherwise left & right s.append(textLine->at(col + 1)); s.append(textLine->at(col)); // do the swap // do it right, never ever manipulate a textline editStart(); editRemoveText(line, col, 2); editInsertText(line, col, s); editEnd(); } void KTextEditor::DocumentPrivate::backspace(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &c) { if (!view->config()->persistentSelection() && view->selection()) { KTextEditor::Range range = view->selectionRange(); editStart(); // Avoid bad selection in case of undo if (view->blockSelection() && view->selection() && range.start().column() > 0 && toVirtualColumn(range.start()) == toVirtualColumn(range.end())) { // Remove one character before vertical selection line by expanding the selection range.setStart(KTextEditor::Cursor(range.start().line(), range.start().column() - 1)); view->setSelection(range); } view->removeSelectedText(); editEnd(); return; } uint col = qMax(c.column(), 0); uint line = qMax(c.line(), 0); if ((col == 0) && (line == 0)) { return; } const Kate::TextLine textLine = m_buffer->plainLine(line); // don't forget this check!!!! really!!!! if (!textLine) { return; } if (col > 0) { bool useNextBlock = false; if (config()->backspaceIndents()) { // backspace indents: erase to next indent position int colX = textLine->toVirtualColumn(col, config()->tabWidth()); int pos = textLine->firstChar(); if (pos > 0) { pos = textLine->toVirtualColumn(pos, config()->tabWidth()); } if (pos < 0 || pos >= (int)colX) { // only spaces on left side of cursor indent(KTextEditor::Range(line, 0, line, 0), -1); } else { useNextBlock = true; } } if (!config()->backspaceIndents() || useNextBlock) { KTextEditor::Cursor beginCursor(line, 0); KTextEditor::Cursor endCursor(line, col); if (!view->config()->backspaceRemoveComposed()) { // Normal backspace behavior beginCursor.setColumn(col - 1); // move to left of surrogate pair if (!isValidTextPosition(beginCursor)) { Q_ASSERT(col >= 2); beginCursor.setColumn(col - 2); } } else { beginCursor.setColumn(view->textLayout(c)->previousCursorPosition(c.column())); } removeText(KTextEditor::Range(beginCursor, endCursor)); // in most cases cursor is moved by removeText, but we should do it manually // for past-end-of-line cursors in block mode view->setCursorPosition(beginCursor); } } else { // col == 0: wrap to previous line const Kate::TextLine textLine = m_buffer->plainLine(line - 1); if (line > 0 && textLine) { if (config()->wordWrap() && textLine->endsWith(QStringLiteral(" "))) { // gg: in hard wordwrap mode, backspace must also eat the trailing space removeText(KTextEditor::Range(line - 1, textLine->length() - 1, line, 0)); } else { removeText(KTextEditor::Range(line - 1, textLine->length(), line, 0)); } } } if (m_currentAutobraceRange) { const auto r = m_currentAutobraceRange->toRange(); if (r.columnWidth() == 1 && view->cursorPosition() == r.start()) { // start parenthesis removed and range length is 1, remove end as well del(view, view->cursorPosition()); m_currentAutobraceRange.clear(); } } } void KTextEditor::DocumentPrivate::del(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &c) { if (!view->config()->persistentSelection() && view->selection()) { KTextEditor::Range range = view->selectionRange(); editStart(); // Avoid bad selection in case of undo if (view->blockSelection() && toVirtualColumn(range.start()) == toVirtualColumn(range.end())) { // Remove one character after vertical selection line by expanding the selection range.setEnd(KTextEditor::Cursor(range.end().line(), range.end().column() + 1)); view->setSelection(range); } view->removeSelectedText(); editEnd(); return; } if (c.column() < (int)m_buffer->plainLine(c.line())->length()) { KTextEditor::Cursor endCursor(c.line(), view->textLayout(c)->nextCursorPosition(c.column())); removeText(KTextEditor::Range(c, endCursor)); } else if (c.line() < lastLine()) { removeText(KTextEditor::Range(c.line(), c.column(), c.line() + 1, 0)); } } void KTextEditor::DocumentPrivate::paste(KTextEditor::ViewPrivate *view, const QString &text) { // nop if nothing to paste if (text.isEmpty()) { return; } // normalize line endings, to e.g. catch issues with \r\n in paste buffer // see bug 410951 QString s = text; s.replace(QRegularExpression(QStringLiteral("(\r\n|\r|\n)")), QStringLiteral("\n")); int lines = s.count(QLatin1Char('\n')); m_undoManager->undoSafePoint(); editStart(); KTextEditor::Cursor pos = view->cursorPosition(); if (!view->config()->persistentSelection() && view->selection()) { pos = view->selectionRange().start(); if (view->blockSelection()) { pos = rangeOnLine(view->selectionRange(), pos.line()).start(); if (lines == 0) { s += QLatin1Char('\n'); s = s.repeated(view->selectionRange().numberOfLines() + 1); s.chop(1); } } view->removeSelectedText(); } if (config()->ovr()) { const auto pasteLines = s.splitRef(QLatin1Char('\n')); if (!view->blockSelection()) { int endColumn = (pasteLines.count() == 1 ? pos.column() : 0) + pasteLines.last().length(); removeText(KTextEditor::Range(pos, pos.line() + pasteLines.count() - 1, endColumn)); } else { int maxi = qMin(pos.line() + pasteLines.count(), this->lines()); for (int i = pos.line(); i < maxi; ++i) { int pasteLength = pasteLines.at(i - pos.line()).length(); removeText(KTextEditor::Range(i, pos.column(), i, qMin(pasteLength + pos.column(), lineLength(i)))); } } } insertText(pos, s, view->blockSelection()); editEnd(); // move cursor right for block select, as the user is moved right internal // even in that case, but user expects other behavior in block selection // mode ! // just let cursor stay, that was it before I changed to moving ranges! if (view->blockSelection()) { view->setCursorPositionInternal(pos); } if (config()->indentPastedText()) { KTextEditor::Range range = KTextEditor::Range(KTextEditor::Cursor(pos.line(), 0), KTextEditor::Cursor(pos.line() + lines, 0)); m_indenter->indent(view, range); } if (!view->blockSelection()) { emit charactersSemiInteractivelyInserted(pos, s); } m_undoManager->undoSafePoint(); } void KTextEditor::DocumentPrivate::indent(KTextEditor::Range range, int change) { if (!isReadWrite()) { return; } editStart(); m_indenter->changeIndent(range, change); editEnd(); } void KTextEditor::DocumentPrivate::align(KTextEditor::ViewPrivate *view, const KTextEditor::Range &range) { m_indenter->indent(view, range); } void KTextEditor::DocumentPrivate::insertTab(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &) { if (!isReadWrite()) { return; } int lineLen = line(view->cursorPosition().line()).length(); KTextEditor::Cursor c = view->cursorPosition(); editStart(); if (!view->config()->persistentSelection() && view->selection()) { view->removeSelectedText(); } else if (view->currentInputMode()->overwrite() && c.column() < lineLen) { KTextEditor::Range r = KTextEditor::Range(view->cursorPosition(), 1); // replace mode needs to know what was removed so it can be restored with backspace QChar removed = line(view->cursorPosition().line()).at(r.start().column()); view->currentInputMode()->overwrittenChar(removed); removeText(r); } c = view->cursorPosition(); editInsertText(c.line(), c.column(), QStringLiteral("\t")); editEnd(); } /* Remove a given string at the beginning of the current line. */ bool KTextEditor::DocumentPrivate::removeStringFromBeginning(int line, const QString &str) { Kate::TextLine textline = m_buffer->plainLine(line); KTextEditor::Cursor cursor(line, 0); bool there = textline->startsWith(str); if (!there) { cursor.setColumn(textline->firstChar()); there = textline->matchesAt(cursor.column(), str); } if (there) { // Remove some chars removeText(KTextEditor::Range(cursor, str.length())); } return there; } /* Remove a given string at the end of the current line. */ bool KTextEditor::DocumentPrivate::removeStringFromEnd(int line, const QString &str) { Kate::TextLine textline = m_buffer->plainLine(line); KTextEditor::Cursor cursor(line, 0); bool there = textline->endsWith(str); if (there) { cursor.setColumn(textline->length() - str.length()); } else { cursor.setColumn(textline->lastChar() - str.length() + 1); there = textline->matchesAt(cursor.column(), str); } if (there) { // Remove some chars removeText(KTextEditor::Range(cursor, str.length())); } return there; } /* Replace tabs by spaces in the given string, if enabled. */ QString KTextEditor::DocumentPrivate::eventuallyReplaceTabs(const KTextEditor::Cursor &cursorPos, const QString &str) const { const bool replacetabs = config()->replaceTabsDyn(); if (!replacetabs) { return str; } const int indentWidth = config()->indentationWidth(); static const QLatin1Char tabChar('\t'); int column = cursorPos.column(); // The result will always be at least as long as the input QString result; result.reserve(str.size()); for (const QChar ch : str) { if (ch == tabChar) { // Insert only enough spaces to align to the next indentWidth column // This fixes bug #340212 int spacesToInsert = indentWidth - (column % indentWidth); result += QString(spacesToInsert, QLatin1Char(' ')); column += spacesToInsert; } else { // Just keep all other typed characters as-is result += ch; ++column; } } return result; } /* Add to the current line a comment line mark at the beginning. */ void KTextEditor::DocumentPrivate::addStartLineCommentToSingleLine(int line, int attrib) { QString commentLineMark = highlight()->getCommentSingleLineStart(attrib); int pos = -1; if (highlight()->getCommentSingleLinePosition(attrib) == KSyntaxHighlighting::CommentPosition::StartOfLine) { pos = 0; commentLineMark += QLatin1Char(' '); } else { const Kate::TextLine l = kateTextLine(line); pos = l->firstChar(); } if (pos >= 0) { insertText(KTextEditor::Cursor(line, pos), commentLineMark); } } /* Remove from the current line a comment line mark at the beginning if there is one. */ bool KTextEditor::DocumentPrivate::removeStartLineCommentFromSingleLine(int line, int attrib) { const QString shortCommentMark = highlight()->getCommentSingleLineStart(attrib); const QString longCommentMark = shortCommentMark + QLatin1Char(' '); editStart(); // Try to remove the long comment mark first bool removed = (removeStringFromBeginning(line, longCommentMark) || removeStringFromBeginning(line, shortCommentMark)); editEnd(); return removed; } /* Add to the current line a start comment mark at the beginning and a stop comment mark at the end. */ void KTextEditor::DocumentPrivate::addStartStopCommentToSingleLine(int line, int attrib) { const QString startCommentMark = highlight()->getCommentStart(attrib) + QLatin1Char(' '); const QString stopCommentMark = QLatin1Char(' ') + highlight()->getCommentEnd(attrib); editStart(); // Add the start comment mark insertText(KTextEditor::Cursor(line, 0), startCommentMark); // Go to the end of the line const int col = m_buffer->plainLine(line)->length(); // Add the stop comment mark insertText(KTextEditor::Cursor(line, col), stopCommentMark); editEnd(); } /* Remove from the current line a start comment mark at the beginning and a stop comment mark at the end. */ bool KTextEditor::DocumentPrivate::removeStartStopCommentFromSingleLine(int line, int attrib) { const QString shortStartCommentMark = highlight()->getCommentStart(attrib); const QString longStartCommentMark = shortStartCommentMark + QLatin1Char(' '); const QString shortStopCommentMark = highlight()->getCommentEnd(attrib); const QString longStopCommentMark = QLatin1Char(' ') + shortStopCommentMark; editStart(); // Try to remove the long start comment mark first const bool removedStart = (removeStringFromBeginning(line, longStartCommentMark) || removeStringFromBeginning(line, shortStartCommentMark)); // Try to remove the long stop comment mark first const bool removedStop = removedStart && (removeStringFromEnd(line, longStopCommentMark) || removeStringFromEnd(line, shortStopCommentMark)); editEnd(); return (removedStart || removedStop); } /* Add to the current selection a start comment mark at the beginning and a stop comment mark at the end. */ void KTextEditor::DocumentPrivate::addStartStopCommentToSelection(KTextEditor::ViewPrivate *view, int attrib) { const QString startComment = highlight()->getCommentStart(attrib); const QString endComment = highlight()->getCommentEnd(attrib); KTextEditor::Range range = view->selectionRange(); if ((range.end().column() == 0) && (range.end().line() > 0)) { range.setEnd(KTextEditor::Cursor(range.end().line() - 1, lineLength(range.end().line() - 1))); } editStart(); if (!view->blockSelection()) { insertText(range.end(), endComment); insertText(range.start(), startComment); } else { for (int line = range.start().line(); line <= range.end().line(); line++) { KTextEditor::Range subRange = rangeOnLine(range, line); insertText(subRange.end(), endComment); insertText(subRange.start(), startComment); } } editEnd(); // selection automatically updated (MovingRange) } /* Add to the current selection a comment line mark at the beginning of each line. */ void KTextEditor::DocumentPrivate::addStartLineCommentToSelection(KTextEditor::ViewPrivate *view, int attrib) { // const QString commentLineMark = highlight()->getCommentSingleLineStart(attrib) + QLatin1Char(' '); int sl = view->selectionRange().start().line(); int el = view->selectionRange().end().line(); // if end of selection is in column 0 in last line, omit the last line if ((view->selectionRange().end().column() == 0) && (el > 0)) { el--; } editStart(); // For each line of the selection for (int z = el; z >= sl; z--) { // insertText (z, 0, commentLineMark); addStartLineCommentToSingleLine(z, attrib); } editEnd(); // selection automatically updated (MovingRange) } bool KTextEditor::DocumentPrivate::nextNonSpaceCharPos(int &line, int &col) { for (; line < (int)m_buffer->count(); line++) { Kate::TextLine textLine = m_buffer->plainLine(line); if (!textLine) { break; } col = textLine->nextNonSpaceChar(col); if (col != -1) { return true; // Next non-space char found } col = 0; } // No non-space char found line = -1; col = -1; return false; } bool KTextEditor::DocumentPrivate::previousNonSpaceCharPos(int &line, int &col) { while (true) { Kate::TextLine textLine = m_buffer->plainLine(line); if (!textLine) { break; } col = textLine->previousNonSpaceChar(col); if (col != -1) { return true; } if (line == 0) { return false; } --line; col = textLine->length(); } // No non-space char found line = -1; col = -1; return false; } /* Remove from the selection a start comment mark at the beginning and a stop comment mark at the end. */ bool KTextEditor::DocumentPrivate::removeStartStopCommentFromSelection(KTextEditor::ViewPrivate *view, int attrib) { const QString startComment = highlight()->getCommentStart(attrib); const QString endComment = highlight()->getCommentEnd(attrib); int sl = qMax(0, view->selectionRange().start().line()); int el = qMin(view->selectionRange().end().line(), lastLine()); int sc = view->selectionRange().start().column(); int ec = view->selectionRange().end().column(); // The selection ends on the char before selectEnd if (ec != 0) { --ec; } else if (el > 0) { --el; ec = m_buffer->plainLine(el)->length() - 1; } const int startCommentLen = startComment.length(); const int endCommentLen = endComment.length(); // had this been perl or sed: s/^\s*$startComment(.+?)$endComment\s*/$2/ bool remove = nextNonSpaceCharPos(sl, sc) && m_buffer->plainLine(sl)->matchesAt(sc, startComment) && previousNonSpaceCharPos(el, ec) && ((ec - endCommentLen + 1) >= 0) && m_buffer->plainLine(el)->matchesAt(ec - endCommentLen + 1, endComment); if (remove) { editStart(); removeText(KTextEditor::Range(el, ec - endCommentLen + 1, el, ec + 1)); removeText(KTextEditor::Range(sl, sc, sl, sc + startCommentLen)); editEnd(); // selection automatically updated (MovingRange) } return remove; } bool KTextEditor::DocumentPrivate::removeStartStopCommentFromRegion(const KTextEditor::Cursor &start, const KTextEditor::Cursor &end, int attrib) { const QString startComment = highlight()->getCommentStart(attrib); const QString endComment = highlight()->getCommentEnd(attrib); const int startCommentLen = startComment.length(); const int endCommentLen = endComment.length(); const bool remove = m_buffer->plainLine(start.line())->matchesAt(start.column(), startComment) && m_buffer->plainLine(end.line())->matchesAt(end.column() - endCommentLen, endComment); if (remove) { editStart(); removeText(KTextEditor::Range(end.line(), end.column() - endCommentLen, end.line(), end.column())); removeText(KTextEditor::Range(start, startCommentLen)); editEnd(); } return remove; } /* Remove from the beginning of each line of the selection a start comment line mark. */ bool KTextEditor::DocumentPrivate::removeStartLineCommentFromSelection(KTextEditor::ViewPrivate *view, int attrib) { const QString shortCommentMark = highlight()->getCommentSingleLineStart(attrib); const QString longCommentMark = shortCommentMark + QLatin1Char(' '); int sl = view->selectionRange().start().line(); int el = view->selectionRange().end().line(); if ((view->selectionRange().end().column() == 0) && (el > 0)) { el--; } bool removed = false; editStart(); // For each line of the selection for (int z = el; z >= sl; z--) { // Try to remove the long comment mark first removed = (removeStringFromBeginning(z, longCommentMark) || removeStringFromBeginning(z, shortCommentMark) || removed); } editEnd(); // selection automatically updated (MovingRange) return removed; } /* Comment or uncomment the selection or the current line if there is no selection. */ void KTextEditor::DocumentPrivate::comment(KTextEditor::ViewPrivate *v, uint line, uint column, int change) { // skip word wrap bug #105373 const bool skipWordWrap = wordWrap(); if (skipWordWrap) { setWordWrap(false); } bool hassel = v->selection(); int c = 0; if (hassel) { c = v->selectionRange().start().column(); } int startAttrib = 0; Kate::TextLine ln = kateTextLine(line); if (c < ln->length()) { startAttrib = ln->attribute(c); } else if (!ln->attributesList().isEmpty()) { startAttrib = ln->attributesList().back().attributeValue; } bool hasStartLineCommentMark = !(highlight()->getCommentSingleLineStart(startAttrib).isEmpty()); bool hasStartStopCommentMark = (!(highlight()->getCommentStart(startAttrib).isEmpty()) && !(highlight()->getCommentEnd(startAttrib).isEmpty())); if (change > 0) { // comment if (!hassel) { if (hasStartLineCommentMark) { addStartLineCommentToSingleLine(line, startAttrib); } else if (hasStartStopCommentMark) { addStartStopCommentToSingleLine(line, startAttrib); } } else { // anders: prefer single line comment to avoid nesting probs // If the selection starts after first char in the first line // or ends before the last char of the last line, we may use // multiline comment markers. // TODO We should try to detect nesting. // - if selection ends at col 0, most likely she wanted that // line ignored const KTextEditor::Range sel = v->selectionRange(); if (hasStartStopCommentMark && (!hasStartLineCommentMark || ((sel.start().column() > m_buffer->plainLine(sel.start().line())->firstChar()) || (sel.end().column() > 0 && sel.end().column() < (m_buffer->plainLine(sel.end().line())->length()))))) { addStartStopCommentToSelection(v, startAttrib); } else if (hasStartLineCommentMark) { addStartLineCommentToSelection(v, startAttrib); } } } else { // uncomment bool removed = false; if (!hassel) { removed = (hasStartLineCommentMark && removeStartLineCommentFromSingleLine(line, startAttrib)) || (hasStartStopCommentMark && removeStartStopCommentFromSingleLine(line, startAttrib)); } else { // anders: this seems like it will work with above changes :) removed = (hasStartStopCommentMark && removeStartStopCommentFromSelection(v, startAttrib)) || (hasStartLineCommentMark && removeStartLineCommentFromSelection(v, startAttrib)); } // recursive call for toggle comment if (!removed && change == 0) { comment(v, line, column, 1); } } if (skipWordWrap) { setWordWrap(true); // see begin of function ::comment (bug #105373) } } void KTextEditor::DocumentPrivate::transform(KTextEditor::ViewPrivate *v, const KTextEditor::Cursor &c, KTextEditor::DocumentPrivate::TextTransform t) { if (v->selection()) { editStart(); // cache the selection and cursor, so we can be sure to restore. KTextEditor::Range selection = v->selectionRange(); KTextEditor::Range range(selection.start(), 0); while (range.start().line() <= selection.end().line()) { int start = 0; int end = lineLength(range.start().line()); if (range.start().line() == selection.start().line() || v->blockSelection()) { start = selection.start().column(); } if (range.start().line() == selection.end().line() || v->blockSelection()) { end = selection.end().column(); } if (start > end) { int swapCol = start; start = end; end = swapCol; } range.setStart(KTextEditor::Cursor(range.start().line(), start)); range.setEnd(KTextEditor::Cursor(range.end().line(), end)); QString s = text(range); QString old = s; if (t == Uppercase) { s = s.toUpper(); } else if (t == Lowercase) { s = s.toLower(); } else { // Capitalize Kate::TextLine l = m_buffer->plainLine(range.start().line()); int p(0); while (p < s.length()) { // If bol or the character before is not in a word, up this one: // 1. if both start and p is 0, upper char. // 2. if blockselect or first line, and p == 0 and start-1 is not in a word, upper // 3. if p-1 is not in a word, upper. if ((!range.start().column() && !p) || ((range.start().line() == selection.start().line() || v->blockSelection()) && !p && !highlight()->isInWord(l->at(range.start().column() - 1))) || (p && !highlight()->isInWord(s.at(p - 1)))) { s[p] = s.at(p).toUpper(); } p++; } } if (s != old) { removeText(range); insertText(range.start(), s); } range.setBothLines(range.start().line() + 1); } editEnd(); // restore selection & cursor v->setSelection(selection); v->setCursorPosition(c); } else { // no selection editStart(); // get cursor KTextEditor::Cursor cursor = c; QString old = text(KTextEditor::Range(cursor, 1)); QString s; switch (t) { case Uppercase: s = old.toUpper(); break; case Lowercase: s = old.toLower(); break; case Capitalize: { Kate::TextLine l = m_buffer->plainLine(cursor.line()); while (cursor.column() > 0 && highlight()->isInWord(l->at(cursor.column() - 1), l->attribute(cursor.column() - 1))) { cursor.setColumn(cursor.column() - 1); } old = text(KTextEditor::Range(cursor, 1)); s = old.toUpper(); } break; default: break; } removeText(KTextEditor::Range(cursor, 1)); insertText(cursor, s); editEnd(); } } void KTextEditor::DocumentPrivate::joinLines(uint first, uint last) { // if ( first == last ) last += 1; editStart(); int line(first); while (first < last) { // Normalize the whitespace in the joined lines by making sure there's // always exactly one space between the joined lines // This cannot be done in editUnwrapLine, because we do NOT want this // behavior when deleting from the start of a line, just when explicitly // calling the join command Kate::TextLine l = kateTextLine(line); Kate::TextLine tl = kateTextLine(line + 1); if (!l || !tl) { editEnd(); return; } int pos = tl->firstChar(); if (pos >= 0) { if (pos != 0) { editRemoveText(line + 1, 0, pos); } if (!(l->length() == 0 || l->at(l->length() - 1).isSpace())) { editInsertText(line + 1, 0, QStringLiteral(" ")); } } else { // Just remove the whitespace and let Kate handle the rest editRemoveText(line + 1, 0, tl->length()); } editUnWrapLine(line); first++; } editEnd(); } void KTextEditor::DocumentPrivate::tagLines(int start, int end) { for (auto view : qAsConst(m_views)) { view->tagLines(start, end, true); } } void KTextEditor::DocumentPrivate::repaintViews(bool paintOnlyDirty) { for (auto view : qAsConst(m_views)) { view->repaintText(paintOnlyDirty); } } /* Bracket matching uses the following algorithm: If in overwrite mode, match the bracket currently underneath the cursor. Otherwise, if the character to the left is a bracket, match it. Otherwise if the character to the right of the cursor is a bracket, match it. Otherwise, don't match anything. */ KTextEditor::Range KTextEditor::DocumentPrivate::findMatchingBracket(const KTextEditor::Cursor &start, int maxLines) { if (maxLines < 0) { return KTextEditor::Range::invalid(); } Kate::TextLine textLine = m_buffer->plainLine(start.line()); if (!textLine) { return KTextEditor::Range::invalid(); } KTextEditor::Range range(start, start); const QChar right = textLine->at(range.start().column()); const QChar left = textLine->at(range.start().column() - 1); QChar bracket; if (config()->ovr()) { if (isBracket(right)) { bracket = right; } else { return KTextEditor::Range::invalid(); } } else if (isBracket(right)) { bracket = right; } else if (isBracket(left)) { range.setStart(KTextEditor::Cursor(range.start().line(), range.start().column() - 1)); bracket = left; } else { return KTextEditor::Range::invalid(); } const QChar opposite = matchingBracket(bracket); if (opposite.isNull()) { return KTextEditor::Range::invalid(); } const int searchDir = isStartBracket(bracket) ? 1 : -1; uint nesting = 0; const int minLine = qMax(range.start().line() - maxLines, 0); const int maxLine = qMin(range.start().line() + maxLines, documentEnd().line()); range.setEnd(range.start()); KTextEditor::DocumentCursor cursor(this); cursor.setPosition(range.start()); int validAttr = kateTextLine(cursor.line())->attribute(cursor.column()); while (cursor.line() >= minLine && cursor.line() <= maxLine) { if (!cursor.move(searchDir)) { return KTextEditor::Range::invalid(); } Kate::TextLine textLine = kateTextLine(cursor.line()); if (textLine->attribute(cursor.column()) == validAttr) { // Check for match QChar c = textLine->at(cursor.column()); if (c == opposite) { if (nesting == 0) { if (searchDir > 0) { // forward range.setEnd(cursor.toCursor()); } else { range.setStart(cursor.toCursor()); } return range; } nesting--; } else if (c == bracket) { nesting++; } } } return KTextEditor::Range::invalid(); } // helper: remove \r and \n from visible document name (bug #170876) inline static QString removeNewLines(const QString &str) { QString tmp(str); return tmp.replace(QLatin1String("\r\n"), QLatin1String(" ")).replace(QLatin1Char('\r'), QLatin1Char(' ')).replace(QLatin1Char('\n'), QLatin1Char(' ')); } void KTextEditor::DocumentPrivate::updateDocName() { // if the name is set, and starts with FILENAME, it should not be changed! if (!url().isEmpty() && (m_docName == removeNewLines(url().fileName()) || m_docName.startsWith(removeNewLines(url().fileName()) + QLatin1String(" (")))) { return; } int count = -1; const auto docs = KTextEditor::EditorPrivate::self()->kateDocuments(); for (KTextEditor::DocumentPrivate *doc : docs) { if ((doc != this) && (doc->url().fileName() == url().fileName())) if (doc->m_docNameNumber > count) { count = doc->m_docNameNumber; } } m_docNameNumber = count + 1; QString oldName = m_docName; m_docName = removeNewLines(url().fileName()); m_isUntitled = m_docName.isEmpty(); if (m_isUntitled) { m_docName = i18n("Untitled"); } if (m_docNameNumber > 0) { m_docName = QString(m_docName + QLatin1String(" (%1)")).arg(m_docNameNumber + 1); } /** * avoid to emit this, if name doesn't change! */ if (oldName != m_docName) { emit documentNameChanged(this); } } void KTextEditor::DocumentPrivate::slotModifiedOnDisk(KTextEditor::View * /*v*/) { if (url().isEmpty() || !m_modOnHd) { return; } if (!isModified() && isAutoReload()) { onModOnHdAutoReload(); return; } if (!m_fileChangedDialogsActivated || m_modOnHdHandler) { return; } // don't ask the user again and again the same thing if (m_modOnHdReason == m_prevModOnHdReason) { return; } m_prevModOnHdReason = m_modOnHdReason; m_modOnHdHandler = new KateModOnHdPrompt(this, m_modOnHdReason, reasonedMOHString()); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::saveAsTriggered, this, &DocumentPrivate::onModOnHdSaveAs); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::closeTriggered, this, &DocumentPrivate::onModOnHdClose); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::reloadTriggered, this, &DocumentPrivate::onModOnHdReload); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::autoReloadTriggered, this, &DocumentPrivate::onModOnHdAutoReload); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::ignoreTriggered, this, &DocumentPrivate::onModOnHdIgnore); } void KTextEditor::DocumentPrivate::onModOnHdSaveAs() { m_modOnHd = false; QWidget *parentWidget(dialogParent()); const QUrl res = QFileDialog::getSaveFileUrl(parentWidget, i18n("Save File"), url()); if (!res.isEmpty()) { if (!saveAs(res)) { KMessageBox::error(parentWidget, i18n("Save failed")); m_modOnHd = true; } else { delete m_modOnHdHandler; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, false, OnDiskUnmodified); } } else { // the save as dialog was canceled, we are still modified on disk m_modOnHd = true; } } void KTextEditor::DocumentPrivate::onModOnHdClose() { // avoid prompt in closeUrl() m_fileChangedDialogsActivated = false; // close the file without prompt confirmation closeUrl(); // Useful for kate only closeDocumentInApplication(); } void KTextEditor::DocumentPrivate::onModOnHdReload() { m_modOnHd = false; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, false, OnDiskUnmodified); documentReload(); delete m_modOnHdHandler; } void KTextEditor::DocumentPrivate::autoReloadToggled(bool b) { m_autoReloadMode->setChecked(b); if (b) { connect(&m_modOnHdTimer, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload); } else { disconnect(&m_modOnHdTimer, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload); } } bool KTextEditor::DocumentPrivate::isAutoReload() { return m_autoReloadMode->isChecked(); } void KTextEditor::DocumentPrivate::delayAutoReload() { if (isAutoReload()) { m_autoReloadThrottle.start(); } } void KTextEditor::DocumentPrivate::onModOnHdAutoReload() { if (m_modOnHdHandler) { delete m_modOnHdHandler; autoReloadToggled(true); } if (!isAutoReload()) { return; } if (m_modOnHd && !m_reloading && !m_autoReloadThrottle.isActive()) { m_modOnHd = false; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, false, OnDiskUnmodified); documentReload(); m_autoReloadThrottle.start(); } } void KTextEditor::DocumentPrivate::onModOnHdIgnore() { // ignore as long as m_prevModOnHdReason == m_modOnHdReason delete m_modOnHdHandler; } void KTextEditor::DocumentPrivate::setModifiedOnDisk(ModifiedOnDiskReason reason) { m_modOnHdReason = reason; m_modOnHd = (reason != OnDiskUnmodified); emit modifiedOnDisk(this, (reason != OnDiskUnmodified), reason); } class KateDocumentTmpMark { public: QString line; KTextEditor::Mark mark; }; void KTextEditor::DocumentPrivate::setModifiedOnDiskWarning(bool on) { m_fileChangedDialogsActivated = on; } bool KTextEditor::DocumentPrivate::documentReload() { if (url().isEmpty()) { return false; } // typically, the message for externally modified files is visible. Since it // does not make sense showing an additional dialog, just hide the message. delete m_modOnHdHandler; emit aboutToReload(this); QList tmp; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { KateDocumentTmpMark m; m.line = line(i.value()->line); m.mark = *i.value(); tmp.append(m); } const QString oldMode = mode(); const bool byUser = m_fileTypeSetByUser; const QString hl_mode = highlightingMode(); m_storedVariables.clear(); // save cursor positions for all views QHash cursorPositions; for (auto it = m_views.constBegin(); it != m_views.constEnd(); ++it) { auto v = it.value(); cursorPositions.insert(v, v->cursorPosition()); } m_reloading = true; KTextEditor::DocumentPrivate::openUrl(url()); // reset some flags only valid for one reload! m_userSetEncodingForNextReload = false; // restore cursor positions for all views for (auto it = m_views.constBegin(); it != m_views.constEnd(); ++it) { auto v = it.value(); setActiveView(v); v->setCursorPosition(cursorPositions.value(v)); if (v->isVisible()) { v->repaintText(false); } } for (int z = 0; z < tmp.size(); z++) { if (z < lines()) { if (line(tmp.at(z).mark.line) == tmp.at(z).line) { setMark(tmp.at(z).mark.line, tmp.at(z).mark.type); } } } if (byUser) { setMode(oldMode); } setHighlightingMode(hl_mode); emit reloaded(this); return true; } bool KTextEditor::DocumentPrivate::documentSave() { if (!url().isValid() || !isReadWrite()) { return documentSaveAs(); } return save(); } bool KTextEditor::DocumentPrivate::documentSaveAs() { const QUrl saveUrl = QFileDialog::getSaveFileUrl(dialogParent(), i18n("Save File"), url()); if (saveUrl.isEmpty()) { return false; } return saveAs(saveUrl); } bool KTextEditor::DocumentPrivate::documentSaveAsWithEncoding(const QString &encoding) { const QUrl saveUrl = QFileDialog::getSaveFileUrl(dialogParent(), i18n("Save File"), url()); if (saveUrl.isEmpty()) { return false; } setEncoding(encoding); return saveAs(saveUrl); } bool KTextEditor::DocumentPrivate::documentSaveCopyAs() { const QUrl saveUrl = QFileDialog::getSaveFileUrl(dialogParent(), i18n("Save Copy of File"), url()); if (saveUrl.isEmpty()) { return false; } QTemporaryFile file; if (!file.open()) { return false; } if (!m_buffer->saveFile(file.fileName())) { KMessageBox::error( dialogParent(), 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 that enough disk space is available.", this->url().toDisplayString(QUrl::PreferLocalFile))); return false; } // get the right permissions, start with safe default KIO::StatJob *statJob = KIO::stat(url(), KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, QApplication::activeWindow()); int permissions = -1; if (statJob->exec()) { permissions = KFileItem(statJob->statResult(), url()).permissions(); } // KIO move, important: allow overwrite, we checked above! KIO::FileCopyJob *job = KIO::file_copy(QUrl::fromLocalFile(file.fileName()), saveUrl, permissions, KIO::Overwrite); KJobWidgets::setWindow(job, QApplication::activeWindow()); return job->exec(); } void KTextEditor::DocumentPrivate::setWordWrap(bool on) { config()->setWordWrap(on); } bool KTextEditor::DocumentPrivate::wordWrap() const { return config()->wordWrap(); } void KTextEditor::DocumentPrivate::setWordWrapAt(uint col) { config()->setWordWrapAt(col); } unsigned int KTextEditor::DocumentPrivate::wordWrapAt() const { return config()->wordWrapAt(); } void KTextEditor::DocumentPrivate::setPageUpDownMovesCursor(bool on) { config()->setPageUpDownMovesCursor(on); } bool KTextEditor::DocumentPrivate::pageUpDownMovesCursor() const { return config()->pageUpDownMovesCursor(); } // END bool KTextEditor::DocumentPrivate::setEncoding(const QString &e) { return m_config->setEncoding(e); } QString KTextEditor::DocumentPrivate::encoding() const { return m_config->encoding(); } void KTextEditor::DocumentPrivate::updateConfig() { m_undoManager->updateConfig(); // switch indenter if needed and update config.... m_indenter->setMode(m_config->indentationMode()); m_indenter->updateConfig(); // set tab width there, too m_buffer->setTabWidth(config()->tabWidth()); // update all views, does tagAll and updateView... for (auto view : qAsConst(m_views)) { view->updateDocumentConfig(); } // update on-the-fly spell checking as spell checking defaults might have changes if (m_onTheFlyChecker) { m_onTheFlyChecker->updateConfig(); } emit configChanged(); } // BEGIN Variable reader // "local variable" feature by anders, 2003 /* TODO add config options (how many lines to read, on/off) add interface for plugins/apps to set/get variables add view stuff */ void KTextEditor::DocumentPrivate::readVariables(bool onlyViewAndRenderer) { if (!onlyViewAndRenderer) { m_config->configStart(); } // views! for (auto v : qAsConst(m_views)) { v->config()->configStart(); v->renderer()->config()->configStart(); } // read a number of lines in the top/bottom of the document for (int i = 0; i < qMin(9, lines()); ++i) { readVariableLine(line(i), onlyViewAndRenderer); } if (lines() > 10) { for (int i = qMax(10, lines() - 10); i < lines(); i++) { readVariableLine(line(i), onlyViewAndRenderer); } } if (!onlyViewAndRenderer) { m_config->configEnd(); } for (auto v : qAsConst(m_views)) { v->config()->configEnd(); v->renderer()->config()->configEnd(); } } void KTextEditor::DocumentPrivate::readVariableLine(const QString &t, bool onlyViewAndRenderer) { static const QRegularExpression kvLine(QStringLiteral("kate:(.*)")); static const QRegularExpression kvLineWildcard(QStringLiteral("kate-wildcard\\((.*)\\):(.*)")); static const QRegularExpression kvLineMime(QStringLiteral("kate-mimetype\\((.*)\\):(.*)")); static const QRegularExpression kvVar(QStringLiteral("([\\w\\-]+)\\s+([^;]+)")); // simple check first, no regex // no kate inside, no vars, simple... if (!t.contains(QLatin1String("kate"))) { return; } // found vars, if any QString s; // now, try first the normal ones auto match = kvLine.match(t); if (match.hasMatch()) { s = match.captured(1); // qCDebug(LOG_KTE) << "normal variable line kate: matched: " << s; } else if ((match = kvLineWildcard.match(t)).hasMatch()) { // regex given const QStringList wildcards(match.captured(1).split(QLatin1Char(';'), QString::SkipEmptyParts)); const QString nameOfFile = url().fileName(); bool found = false; for (const QString &pattern : wildcards) { QRegExp wildcard(pattern, Qt::CaseSensitive, QRegExp::Wildcard); found = wildcard.exactMatch(nameOfFile); // Qt 5.12, no ifdef, more to test // QRegularExpression wildcard(QLatin1Char('^') + QRegularExpression::wildcardToRegularExpression(pattern) + QLatin1Char('$')); // found = wildcard.match(nameOfFile).hasMatch(); if (found) { break; } } // nothing usable found... if (!found) { return; } s = match.captured(2); // qCDebug(LOG_KTE) << "guarded variable line kate-wildcard: matched: " << s; } else if ((match = kvLineMime.match(t)).hasMatch()) { // mime-type given const QStringList types(match.captured(1).split(QLatin1Char(';'), QString::SkipEmptyParts)); // no matching type found if (!types.contains(mimeType())) { return; } s = match.captured(2); // qCDebug(LOG_KTE) << "guarded variable line kate-mimetype: matched: " << s; } else { // nothing found return; } // view variable names static const auto vvl = {QLatin1String("dynamic-word-wrap"), QLatin1String("dynamic-word-wrap-indicators"), QLatin1String("line-numbers"), QLatin1String("icon-border"), QLatin1String("folding-markers"), QLatin1String("folding-preview"), QLatin1String("bookmark-sorting"), QLatin1String("auto-center-lines"), QLatin1String("icon-bar-color"), QLatin1String("scrollbar-minimap"), QLatin1String("scrollbar-preview") // renderer , QLatin1String("background-color"), QLatin1String("selection-color"), QLatin1String("current-line-color"), QLatin1String("bracket-highlight-color"), QLatin1String("word-wrap-marker-color"), QLatin1String("font"), QLatin1String("font-size"), QLatin1String("scheme")}; int spaceIndent = -1; // for backward compatibility; see below bool replaceTabsSet = false; int startPos(0); QString var, val; while ((match = kvVar.match(s, startPos)).hasMatch()) { startPos = match.capturedEnd(0); var = match.captured(1); val = match.captured(2).trimmed(); bool state; // store booleans here int n; // store ints here // only apply view & renderer config stuff if (onlyViewAndRenderer) { if (contains(vvl, var)) { // FIXME define above setViewVariable(var, val); } } else { // BOOL SETTINGS if (var == QLatin1String("word-wrap") && checkBoolValue(val, &state)) { setWordWrap(state); // ??? FIXME CHECK } // KateConfig::configFlags // FIXME should this be optimized to only a few calls? how? else if (var == QLatin1String("backspace-indents") && checkBoolValue(val, &state)) { m_config->setBackspaceIndents(state); } else if (var == QLatin1String("indent-pasted-text") && checkBoolValue(val, &state)) { m_config->setIndentPastedText(state); } else if (var == QLatin1String("replace-tabs") && checkBoolValue(val, &state)) { m_config->setReplaceTabsDyn(state); replaceTabsSet = true; // for backward compatibility; see below } else if (var == QLatin1String("remove-trailing-space") && checkBoolValue(val, &state)) { qCWarning(LOG_KTE) << i18n( "Using deprecated modeline 'remove-trailing-space'. " "Please replace with 'remove-trailing-spaces modified;', see " "https://docs.kde.org/stable5/en/applications/katepart/config-variables.html#variable-remove-trailing-spaces"); m_config->setRemoveSpaces(state ? 1 : 0); } else if (var == QLatin1String("replace-trailing-space-save") && checkBoolValue(val, &state)) { qCWarning(LOG_KTE) << i18n( "Using deprecated modeline 'replace-trailing-space-save'. " "Please replace with 'remove-trailing-spaces all;', see " "https://docs.kde.org/stable5/en/applications/katepart/config-variables.html#variable-remove-trailing-spaces"); m_config->setRemoveSpaces(state ? 2 : 0); } else if (var == QLatin1String("overwrite-mode") && checkBoolValue(val, &state)) { m_config->setOvr(state); } else if (var == QLatin1String("keep-extra-spaces") && checkBoolValue(val, &state)) { m_config->setKeepExtraSpaces(state); } else if (var == QLatin1String("tab-indents") && checkBoolValue(val, &state)) { m_config->setTabIndents(state); } else if (var == QLatin1String("show-tabs") && checkBoolValue(val, &state)) { m_config->setShowTabs(state); } else if (var == QLatin1String("show-trailing-spaces") && checkBoolValue(val, &state)) { m_config->setShowSpaces(state ? KateDocumentConfig::Trailing : KateDocumentConfig::None); } else if (var == QLatin1String("space-indent") && checkBoolValue(val, &state)) { // this is for backward compatibility; see below spaceIndent = state; } else if (var == QLatin1String("smart-home") && checkBoolValue(val, &state)) { m_config->setSmartHome(state); } else if (var == QLatin1String("newline-at-eof") && checkBoolValue(val, &state)) { m_config->setNewLineAtEof(state); } // INTEGER SETTINGS else if (var == QLatin1String("tab-width") && checkIntValue(val, &n)) { m_config->setTabWidth(n); } else if (var == QLatin1String("indent-width") && checkIntValue(val, &n)) { m_config->setIndentationWidth(n); } else if (var == QLatin1String("indent-mode")) { m_config->setIndentationMode(val); } else if (var == QLatin1String("word-wrap-column") && checkIntValue(val, &n) && n > 0) { // uint, but hard word wrap at 0 will be no fun ;) m_config->setWordWrapAt(n); } // STRING SETTINGS else if (var == QLatin1String("eol") || var == QLatin1String("end-of-line")) { const auto l = {QLatin1String("unix"), QLatin1String("dos"), QLatin1String("mac")}; if ((n = indexOf(l, val.toLower())) != -1) { /** * set eol + avoid that it is overwritten by auto-detection again! * this fixes e.g. .kateconfig files with // kate: eol dos; to work, bug 365705 */ m_config->setEol(n); m_config->setAllowEolDetection(false); } } else if (var == QLatin1String("bom") || var == QLatin1String("byte-order-mark") || var == QLatin1String("byte-order-marker")) { if (checkBoolValue(val, &state)) { m_config->setBom(state); } } else if (var == QLatin1String("remove-trailing-spaces")) { val = val.toLower(); if (val == QLatin1String("1") || val == QLatin1String("modified") || val == QLatin1String("mod") || val == QLatin1String("+")) { m_config->setRemoveSpaces(1); } else if (val == QLatin1String("2") || val == QLatin1String("all") || val == QLatin1String("*")) { m_config->setRemoveSpaces(2); } else { m_config->setRemoveSpaces(0); } } else if (var == QLatin1String("syntax") || var == QLatin1String("hl")) { setHighlightingMode(val); } else if (var == QLatin1String("mode")) { setMode(val); } else if (var == QLatin1String("encoding")) { setEncoding(val); } else if (var == QLatin1String("default-dictionary")) { setDefaultDictionary(val); } else if (var == QLatin1String("automatic-spell-checking") && checkBoolValue(val, &state)) { onTheFlySpellCheckingEnabled(state); } // VIEW SETTINGS else if (contains(vvl, var)) { setViewVariable(var, val); } else { m_storedVariables.insert(var, val); } } } // Backward compatibility // If space-indent was set, but replace-tabs was not set, we assume // that the user wants to replace tabulators and set that flag. // If both were set, replace-tabs has precedence. // At this point spaceIndent is -1 if it was never set, // 0 if it was set to off, and 1 if it was set to on. // Note that if onlyViewAndRenderer was requested, spaceIndent is -1. if (!replaceTabsSet && spaceIndent >= 0) { m_config->setReplaceTabsDyn(spaceIndent > 0); } } void KTextEditor::DocumentPrivate::setViewVariable(const QString &var, const QString &val) { bool state; int n; QColor c; for (auto v : qAsConst(m_views)) { // First, try the new config interface QVariant help(val); // Special treatment to catch "on"/"off" if (checkBoolValue(val, &state)) { help = state; } if (v->config()->setValue(var, help)) { } else if (v->renderer()->config()->setValue(var, help)) { // No success? Go the old way } else if (var == QLatin1String("dynamic-word-wrap") && checkBoolValue(val, &state)) { v->config()->setDynWordWrap(state); } else if (var == QLatin1String("block-selection") && checkBoolValue(val, &state)) { v->setBlockSelection(state); // else if ( var = "dynamic-word-wrap-indicators" ) } else if (var == QLatin1String("icon-bar-color") && checkColorValue(val, c)) { v->renderer()->config()->setIconBarColor(c); } // RENDERER else if (var == QLatin1String("background-color") && checkColorValue(val, c)) { v->renderer()->config()->setBackgroundColor(c); } else if (var == QLatin1String("selection-color") && checkColorValue(val, c)) { v->renderer()->config()->setSelectionColor(c); } else if (var == QLatin1String("current-line-color") && checkColorValue(val, c)) { v->renderer()->config()->setHighlightedLineColor(c); } else if (var == QLatin1String("bracket-highlight-color") && checkColorValue(val, c)) { v->renderer()->config()->setHighlightedBracketColor(c); } else if (var == QLatin1String("word-wrap-marker-color") && checkColorValue(val, c)) { v->renderer()->config()->setWordWrapMarkerColor(c); } else if (var == QLatin1String("font") || (checkIntValue(val, &n) && var == QLatin1String("font-size"))) { QFont _f(v->renderer()->currentFont()); if (var == QLatin1String("font")) { _f.setFamily(val); _f.setFixedPitch(QFont(val).fixedPitch()); } else { _f.setPointSize(n); } v->renderer()->config()->setFont(_f); } else if (var == QLatin1String("scheme")) { v->renderer()->config()->setSchema(val); } } } bool KTextEditor::DocumentPrivate::checkBoolValue(QString val, bool *result) { val = val.trimmed().toLower(); static const auto trueValues = {QLatin1String("1"), QLatin1String("on"), QLatin1String("true")}; if (contains(trueValues, val)) { *result = true; return true; } static const auto falseValues = {QLatin1String("0"), QLatin1String("off"), QLatin1String("false")}; if (contains(falseValues, val)) { *result = false; return true; } return false; } bool KTextEditor::DocumentPrivate::checkIntValue(const QString &val, int *result) { bool ret(false); *result = val.toInt(&ret); return ret; } bool KTextEditor::DocumentPrivate::checkColorValue(const QString &val, QColor &c) { c.setNamedColor(val); return c.isValid(); } // KTextEditor::variable QString KTextEditor::DocumentPrivate::variable(const QString &name) const { return m_storedVariables.value(name, QString()); } void KTextEditor::DocumentPrivate::setVariable(const QString &name, const QString &value) { QString s = QStringLiteral("kate: "); s.append(name); s.append(QLatin1Char(' ')); s.append(value); readVariableLine(s); } // END void KTextEditor::DocumentPrivate::slotModOnHdDirty(const QString &path) { if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskModified)) { m_modOnHd = true; m_modOnHdReason = OnDiskModified; if (!m_modOnHdTimer.isActive()) { m_modOnHdTimer.start(); } } } void KTextEditor::DocumentPrivate::slotModOnHdCreated(const QString &path) { if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskCreated)) { m_modOnHd = true; m_modOnHdReason = OnDiskCreated; if (!m_modOnHdTimer.isActive()) { m_modOnHdTimer.start(); } } } void KTextEditor::DocumentPrivate::slotModOnHdDeleted(const QString &path) { if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskDeleted)) { m_modOnHd = true; m_modOnHdReason = OnDiskDeleted; if (!m_modOnHdTimer.isActive()) { m_modOnHdTimer.start(); } } } void KTextEditor::DocumentPrivate::slotDelayedHandleModOnHd() { // compare git hash with the one we have (if we have one) const QByteArray oldDigest = checksum(); if (!oldDigest.isEmpty() && !url().isEmpty() && url().isLocalFile()) { /** * if current checksum == checksum of new file => unmodified */ if (m_modOnHdReason != OnDiskDeleted && createDigest() && oldDigest == checksum()) { m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; } #if LIBGIT2_FOUND /** * if still modified, try to take a look at git * skip that, if document is modified! * only do that, if the file is still there, else reload makes no sense! */ if (m_modOnHd && !isModified() && QFile::exists(url().toLocalFile())) { /** * try to discover the git repo of this file * libgit2 docs state that UTF-8 is the right encoding, even on windows * I hope that is correct! */ git_repository *repository = nullptr; const QByteArray utf8Path = url().toLocalFile().toUtf8(); if (git_repository_open_ext(&repository, utf8Path.constData(), 0, nullptr) == 0) { /** * if we have repo, convert the git hash to an OID */ git_oid oid; if (git_oid_fromstr(&oid, oldDigest.toHex().data()) == 0) { /** * finally: is there a blob for this git hash? */ git_blob *blob = nullptr; if (git_blob_lookup(&blob, repository, &oid) == 0) { /** * this hash exists still in git => just reload */ m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; documentReload(); } git_blob_free(blob); } } git_repository_free(repository); } #endif } /** * emit our signal to the outside! */ emit modifiedOnDisk(this, m_modOnHd, m_modOnHdReason); } QByteArray KTextEditor::DocumentPrivate::checksum() const { return m_buffer->digest(); } bool KTextEditor::DocumentPrivate::createDigest() { QByteArray digest; if (url().isLocalFile()) { QFile f(url().toLocalFile()); if (f.open(QIODevice::ReadOnly)) { // init the hash with the git header QCryptographicHash crypto(QCryptographicHash::Sha1); const QString header = QStringLiteral("blob %1").arg(f.size()); crypto.addData(header.toLatin1() + '\0'); while (!f.atEnd()) { crypto.addData(f.read(256 * 1024)); } digest = crypto.result(); } } /** * set new digest */ m_buffer->setDigest(digest); return !digest.isEmpty(); } QString KTextEditor::DocumentPrivate::reasonedMOHString() const { // squeeze path const QString str = KStringHandler::csqueeze(url().toDisplayString(QUrl::PreferLocalFile)); switch (m_modOnHdReason) { case OnDiskModified: return i18n("The file '%1' was modified by another program.", str); break; case OnDiskCreated: return i18n("The file '%1' was created by another program.", str); break; case OnDiskDeleted: return i18n("The file '%1' was deleted by another program.", str); break; default: return QString(); } Q_UNREACHABLE(); return QString(); } void KTextEditor::DocumentPrivate::removeTrailingSpaces() { const int remove = config()->removeSpaces(); if (remove == 0) { return; } // temporarily disable static word wrap (see bug #328900) const bool wordWrapEnabled = config()->wordWrap(); if (wordWrapEnabled) { setWordWrap(false); } editStart(); for (int line = 0; line < lines(); ++line) { Kate::TextLine textline = plainKateTextLine(line); // remove trailing spaces in entire document, remove = 2 // remove trailing spaces of touched lines, remove = 1 // remove trailing spaces of lines saved on disk, remove = 1 if (remove == 2 || textline->markedAsModified() || textline->markedAsSavedOnDisk()) { const int p = textline->lastChar() + 1; const int l = textline->length() - p; if (l > 0) { editRemoveText(line, p, l); } } } editEnd(); // enable word wrap again, if it was enabled (see bug #328900) if (wordWrapEnabled) { setWordWrap(true); // see begin of this function } } bool KTextEditor::DocumentPrivate::updateFileType(const QString &newType, bool user) { if (user || !m_fileTypeSetByUser) { if (newType.isEmpty()) { return false; } KateFileType fileType = KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType); // if the mode "newType" does not exist if (fileType.name.isEmpty()) { return false; } // remember that we got set by user m_fileTypeSetByUser = user; m_fileType = newType; m_config->configStart(); // NOTE: if the user changes the Mode, the Highlighting also changes. // m_hlSetByUser avoids resetting the highlight when saving the document, if // the current hl isn't stored (eg, in sftp:// or fish:// files) (see bug #407763) if ((user || !m_hlSetByUser) && !fileType.hl.isEmpty()) { int hl(KateHlManager::self()->nameFind(fileType.hl)); if (hl >= 0) { m_buffer->setHighlight(hl); } } /** * set the indentation mode, if any in the mode... * and user did not set it before! * NOTE: KateBuffer::setHighlight() also sets the indentation. */ if (!m_indenterSetByUser && !fileType.indenter.isEmpty()) { config()->setIndentationMode(fileType.indenter); } // views! for (auto v : qAsConst(m_views)) { v->config()->configStart(); v->renderer()->config()->configStart(); } bool bom_settings = false; if (m_bomSetByUser) { bom_settings = m_config->bom(); } readVariableLine(fileType.varLine); if (m_bomSetByUser) { m_config->setBom(bom_settings); } m_config->configEnd(); for (auto v : qAsConst(m_views)) { v->config()->configEnd(); v->renderer()->config()->configEnd(); } } // fixme, make this better... emit modeChanged(this); return true; } void KTextEditor::DocumentPrivate::slotQueryClose_save(bool *handled, bool *abortClosing) { *handled = true; *abortClosing = true; if (this->url().isEmpty()) { QWidget *parentWidget(dialogParent()); const QUrl res = QFileDialog::getSaveFileUrl(parentWidget, i18n("Save File")); if (res.isEmpty()) { *abortClosing = true; return; } saveAs(res); *abortClosing = false; } else { save(); *abortClosing = false; } } // BEGIN KTextEditor::ConfigInterface // BEGIN ConfigInterface stff QStringList KTextEditor::DocumentPrivate::configKeys() const { /** * expose all internally registered keys of the KateDocumentConfig */ return m_config->configKeys(); } QVariant KTextEditor::DocumentPrivate::configValue(const QString &key) { /** * just dispatch to internal key => value lookup */ return m_config->value(key); } void KTextEditor::DocumentPrivate::setConfigValue(const QString &key, const QVariant &value) { /** * just dispatch to internal key + value set */ m_config->setValue(key, value); } // END KTextEditor::ConfigInterface KTextEditor::Cursor KTextEditor::DocumentPrivate::documentEnd() const { return KTextEditor::Cursor(lastLine(), lineLength(lastLine())); } bool KTextEditor::DocumentPrivate::replaceText(const KTextEditor::Range &range, const QString &s, bool block) { // TODO more efficient? editStart(); bool changed = removeText(range, block); changed |= insertText(range.start(), s, block); editEnd(); return changed; } KateHighlighting *KTextEditor::DocumentPrivate::highlight() const { return m_buffer->highlight(); } Kate::TextLine KTextEditor::DocumentPrivate::kateTextLine(int i) { m_buffer->ensureHighlighted(i); return m_buffer->plainLine(i); } Kate::TextLine KTextEditor::DocumentPrivate::plainKateTextLine(int i) { return m_buffer->plainLine(i); } bool KTextEditor::DocumentPrivate::isEditRunning() const { return editIsRunning; } void KTextEditor::DocumentPrivate::setUndoMergeAllEdits(bool merge) { if (merge && m_undoMergeAllEdits) { // Don't add another undo safe point: it will override our current one, // meaning we'll need two undo's to get back there - which defeats the object! return; } m_undoManager->undoSafePoint(); m_undoManager->setAllowComplexMerge(merge); m_undoMergeAllEdits = merge; } // BEGIN KTextEditor::MovingInterface KTextEditor::MovingCursor *KTextEditor::DocumentPrivate::newMovingCursor(const KTextEditor::Cursor &position, KTextEditor::MovingCursor::InsertBehavior insertBehavior) { return new Kate::TextCursor(buffer(), position, insertBehavior); } KTextEditor::MovingRange *KTextEditor::DocumentPrivate::newMovingRange(const KTextEditor::Range &range, KTextEditor::MovingRange::InsertBehaviors insertBehaviors, KTextEditor::MovingRange::EmptyBehavior emptyBehavior) { return new Kate::TextRange(buffer(), range, insertBehaviors, emptyBehavior); } qint64 KTextEditor::DocumentPrivate::revision() const { return m_buffer->history().revision(); } qint64 KTextEditor::DocumentPrivate::lastSavedRevision() const { return m_buffer->history().lastSavedRevision(); } void KTextEditor::DocumentPrivate::lockRevision(qint64 revision) { m_buffer->history().lockRevision(revision); } void KTextEditor::DocumentPrivate::unlockRevision(qint64 revision) { m_buffer->history().unlockRevision(revision); } void KTextEditor::DocumentPrivate::transformCursor(int &line, int &column, KTextEditor::MovingCursor::InsertBehavior insertBehavior, qint64 fromRevision, qint64 toRevision) { m_buffer->history().transformCursor(line, column, insertBehavior, fromRevision, toRevision); } void KTextEditor::DocumentPrivate::transformCursor(KTextEditor::Cursor &cursor, KTextEditor::MovingCursor::InsertBehavior insertBehavior, qint64 fromRevision, qint64 toRevision) { int line = cursor.line(), column = cursor.column(); m_buffer->history().transformCursor(line, column, insertBehavior, fromRevision, toRevision); cursor.setLine(line); cursor.setColumn(column); } void KTextEditor::DocumentPrivate::transformRange(KTextEditor::Range &range, KTextEditor::MovingRange::InsertBehaviors insertBehaviors, KTextEditor::MovingRange::EmptyBehavior emptyBehavior, qint64 fromRevision, qint64 toRevision) { m_buffer->history().transformRange(range, insertBehaviors, emptyBehavior, fromRevision, toRevision); } // END // BEGIN KTextEditor::AnnotationInterface void KTextEditor::DocumentPrivate::setAnnotationModel(KTextEditor::AnnotationModel *model) { KTextEditor::AnnotationModel *oldmodel = m_annotationModel; m_annotationModel = model; emit annotationModelChanged(oldmodel, m_annotationModel); } KTextEditor::AnnotationModel *KTextEditor::DocumentPrivate::annotationModel() const { return m_annotationModel; } // END KTextEditor::AnnotationInterface // TAKEN FROM kparts.h bool KTextEditor::DocumentPrivate::queryClose() { if (!isReadWrite() || !isModified()) { return true; } QString docName = documentName(); int res = KMessageBox::warningYesNoCancel(dialogParent(), i18n("The document \"%1\" has been modified.\n" "Do you want to save your changes or discard them?", docName), i18n("Close Document"), KStandardGuiItem::save(), KStandardGuiItem::discard()); bool abortClose = false; bool handled = false; switch (res) { case KMessageBox::Yes: sigQueryClose(&handled, &abortClose); if (!handled) { if (url().isEmpty()) { QUrl url = QFileDialog::getSaveFileUrl(dialogParent()); if (url.isEmpty()) { return false; } saveAs(url); } else { save(); } } else if (abortClose) { return false; } return waitSaveComplete(); case KMessageBox::No: return true; default: // case KMessageBox::Cancel : return false; } } void KTextEditor::DocumentPrivate::slotStarted(KIO::Job *job) { /** * if we are idle before, we are now loading! */ if (m_documentState == DocumentIdle) { m_documentState = DocumentLoading; } /** * if loading: * - remember pre loading read-write mode * if remote load: * - set to read-only * - trigger possible message */ if (m_documentState == DocumentLoading) { /** * remember state */ m_readWriteStateBeforeLoading = isReadWrite(); /** * perhaps show loading message, but wait one second */ if (job) { /** * only read only if really remote file! */ setReadWrite(false); /** * perhaps some message about loading in one second! * remember job pointer, we want to be able to kill it! */ m_loadingJob = job; QTimer::singleShot(1000, this, SLOT(slotTriggerLoadingMessage())); } } } void KTextEditor::DocumentPrivate::slotCompleted() { /** * if were loading, reset back to old read-write mode before loading * and kill the possible loading message */ if (m_documentState == DocumentLoading) { setReadWrite(m_readWriteStateBeforeLoading); delete m_loadingMessage; } /** * Emit signal that we saved the document, if needed */ if (m_documentState == DocumentSaving || m_documentState == DocumentSavingAs) { emit documentSavedOrUploaded(this, m_documentState == DocumentSavingAs); } /** * back to idle mode */ m_documentState = DocumentIdle; m_reloading = false; } void KTextEditor::DocumentPrivate::slotCanceled() { /** * if were loading, reset back to old read-write mode before loading * and kill the possible loading message */ if (m_documentState == DocumentLoading) { setReadWrite(m_readWriteStateBeforeLoading); delete m_loadingMessage; showAndSetOpeningErrorAccess(); updateDocName(); } /** * back to idle mode */ m_documentState = DocumentIdle; m_reloading = false; } void KTextEditor::DocumentPrivate::slotTriggerLoadingMessage() { /** * no longer loading? * no message needed! */ if (m_documentState != DocumentLoading) { return; } /** * create message about file loading in progress */ delete m_loadingMessage; m_loadingMessage = new KTextEditor::Message(i18n("The file %2 is still loading.", url().toDisplayString(QUrl::PreferLocalFile), url().fileName())); m_loadingMessage->setPosition(KTextEditor::Message::TopInView); /** * if around job: add cancel action */ if (m_loadingJob) { QAction *cancel = new QAction(i18n("&Abort Loading"), nullptr); connect(cancel, SIGNAL(triggered()), this, SLOT(slotAbortLoading())); m_loadingMessage->addAction(cancel); } /** * really post message */ postMessage(m_loadingMessage); } void KTextEditor::DocumentPrivate::slotAbortLoading() { /** * no job, no work */ if (!m_loadingJob) { return; } /** * abort loading if any job * signal results! */ m_loadingJob->kill(KJob::EmitResult); m_loadingJob = nullptr; } void KTextEditor::DocumentPrivate::slotUrlChanged(const QUrl &url) { if (m_reloading) { // the URL is temporarily unset and then reset to the previous URL during reload // we do not want to notify the outside about this return; } Q_UNUSED(url); updateDocName(); emit documentUrlChanged(this); } bool KTextEditor::DocumentPrivate::save() { /** * no double save/load * we need to allow DocumentPreSavingAs here as state, as save is called in saveAs! */ if ((m_documentState != DocumentIdle) && (m_documentState != DocumentPreSavingAs)) { return false; } /** * if we are idle, we are now saving */ if (m_documentState == DocumentIdle) { m_documentState = DocumentSaving; } else { m_documentState = DocumentSavingAs; } /** * call back implementation for real work */ return KTextEditor::Document::save(); } bool KTextEditor::DocumentPrivate::saveAs(const QUrl &url) { /** * abort on bad URL * that is done in saveAs below, too * but we must check it here already to avoid messing up * as no signals will be send, then */ if (!url.isValid()) { return false; } /** * no double save/load */ if (m_documentState != DocumentIdle) { return false; } /** * we enter the pre save as phase */ m_documentState = DocumentPreSavingAs; /** * call base implementation for real work */ return KTextEditor::Document::saveAs(normalizeUrl(url)); } QString KTextEditor::DocumentPrivate::defaultDictionary() const { return m_defaultDictionary; } QList> KTextEditor::DocumentPrivate::dictionaryRanges() const { return m_dictionaryRanges; } void KTextEditor::DocumentPrivate::clearDictionaryRanges() { for (QList>::iterator i = m_dictionaryRanges.begin(); i != m_dictionaryRanges.end(); ++i) { delete (*i).first; } m_dictionaryRanges.clear(); if (m_onTheFlyChecker) { m_onTheFlyChecker->refreshSpellCheck(); } emit dictionaryRangesPresent(false); } void KTextEditor::DocumentPrivate::setDictionary(const QString &newDictionary, const KTextEditor::Range &range, bool blockmode) { if (blockmode) { for (int i = range.start().line(); i <= range.end().line(); ++i) { setDictionary(newDictionary, rangeOnLine(range, i)); } } else { setDictionary(newDictionary, range); } emit dictionaryRangesPresent(!m_dictionaryRanges.isEmpty()); } void KTextEditor::DocumentPrivate::setDictionary(const QString &newDictionary, const KTextEditor::Range &range) { KTextEditor::Range newDictionaryRange = range; if (!newDictionaryRange.isValid() || newDictionaryRange.isEmpty()) { return; } QList> newRanges; // all ranges is 'm_dictionaryRanges' are assumed to be mutually disjoint for (QList>::iterator i = m_dictionaryRanges.begin(); i != m_dictionaryRanges.end();) { qCDebug(LOG_KTE) << "new iteration" << newDictionaryRange; if (newDictionaryRange.isEmpty()) { break; } QPair pair = *i; QString dictionarySet = pair.second; KTextEditor::MovingRange *dictionaryRange = pair.first; qCDebug(LOG_KTE) << *dictionaryRange << dictionarySet; if (dictionaryRange->contains(newDictionaryRange) && newDictionary == dictionarySet) { qCDebug(LOG_KTE) << "dictionaryRange contains newDictionaryRange"; return; } if (newDictionaryRange.contains(*dictionaryRange)) { delete dictionaryRange; i = m_dictionaryRanges.erase(i); qCDebug(LOG_KTE) << "newDictionaryRange contains dictionaryRange"; continue; } KTextEditor::Range intersection = dictionaryRange->toRange().intersect(newDictionaryRange); if (!intersection.isEmpty() && intersection.isValid()) { if (dictionarySet == newDictionary) { // we don't have to do anything for 'intersection' // except cut off the intersection QList remainingRanges = KateSpellCheckManager::rangeDifference(newDictionaryRange, intersection); Q_ASSERT(remainingRanges.size() == 1); newDictionaryRange = remainingRanges.first(); ++i; qCDebug(LOG_KTE) << "dictionarySet == newDictionary"; continue; } QList remainingRanges = KateSpellCheckManager::rangeDifference(*dictionaryRange, intersection); for (QList::iterator j = remainingRanges.begin(); j != remainingRanges.end(); ++j) { KTextEditor::MovingRange *remainingRange = newMovingRange(*j, KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight); remainingRange->setFeedback(this); newRanges.push_back(QPair(remainingRange, dictionarySet)); } i = m_dictionaryRanges.erase(i); delete dictionaryRange; } else { ++i; } } m_dictionaryRanges += newRanges; if (!newDictionaryRange.isEmpty() && !newDictionary.isEmpty()) { // we don't add anything for the default dictionary KTextEditor::MovingRange *newDictionaryMovingRange = newMovingRange(newDictionaryRange, KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight); newDictionaryMovingRange->setFeedback(this); m_dictionaryRanges.push_back(QPair(newDictionaryMovingRange, newDictionary)); } if (m_onTheFlyChecker && !newDictionaryRange.isEmpty()) { m_onTheFlyChecker->refreshSpellCheck(newDictionaryRange); } } void KTextEditor::DocumentPrivate::setDefaultDictionary(const QString &dict) { if (m_defaultDictionary == dict) { return; } m_defaultDictionary = dict; if (m_onTheFlyChecker) { m_onTheFlyChecker->updateConfig(); refreshOnTheFlyCheck(); } emit defaultDictionaryChanged(this); } void KTextEditor::DocumentPrivate::onTheFlySpellCheckingEnabled(bool enable) { if (isOnTheFlySpellCheckingEnabled() == enable) { return; } if (enable) { Q_ASSERT(m_onTheFlyChecker == nullptr); m_onTheFlyChecker = new KateOnTheFlyChecker(this); } else { delete m_onTheFlyChecker; m_onTheFlyChecker = nullptr; } for (auto view : qAsConst(m_views)) { view->reflectOnTheFlySpellCheckStatus(enable); } } bool KTextEditor::DocumentPrivate::isOnTheFlySpellCheckingEnabled() const { return m_onTheFlyChecker != nullptr; } QString KTextEditor::DocumentPrivate::dictionaryForMisspelledRange(const KTextEditor::Range &range) const { if (!m_onTheFlyChecker) { return QString(); } else { return m_onTheFlyChecker->dictionaryForMisspelledRange(range); } } void KTextEditor::DocumentPrivate::clearMisspellingForWord(const QString &word) { if (m_onTheFlyChecker) { m_onTheFlyChecker->clearMisspellingForWord(word); } } void KTextEditor::DocumentPrivate::refreshOnTheFlyCheck(const KTextEditor::Range &range) { if (m_onTheFlyChecker) { m_onTheFlyChecker->refreshSpellCheck(range); } } void KTextEditor::DocumentPrivate::rangeInvalid(KTextEditor::MovingRange *movingRange) { deleteDictionaryRange(movingRange); } void KTextEditor::DocumentPrivate::rangeEmpty(KTextEditor::MovingRange *movingRange) { deleteDictionaryRange(movingRange); } void KTextEditor::DocumentPrivate::deleteDictionaryRange(KTextEditor::MovingRange *movingRange) { qCDebug(LOG_KTE) << "deleting" << movingRange; auto finder = [=](const QPair &item) -> bool { return item.first == movingRange; }; auto it = std::find_if(m_dictionaryRanges.begin(), m_dictionaryRanges.end(), finder); if (it != m_dictionaryRanges.end()) { m_dictionaryRanges.erase(it); delete movingRange; } Q_ASSERT(std::find_if(m_dictionaryRanges.begin(), m_dictionaryRanges.end(), finder) == m_dictionaryRanges.end()); } bool KTextEditor::DocumentPrivate::containsCharacterEncoding(const KTextEditor::Range &range) { KateHighlighting *highlighting = highlight(); Kate::TextLine textLine; const int rangeStartLine = range.start().line(); const int rangeStartColumn = range.start().column(); const int rangeEndLine = range.end().line(); const int rangeEndColumn = range.end().column(); for (int line = range.start().line(); line <= rangeEndLine; ++line) { textLine = kateTextLine(line); int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0; int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine->length(); for (int col = startColumn; col < endColumn; ++col) { int attr = textLine->attribute(col); const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr); if (!prefixStore.findPrefix(textLine, col).isEmpty()) { return true; } } } return false; } int KTextEditor::DocumentPrivate::computePositionWrtOffsets(const OffsetList &offsetList, int pos) { int previousOffset = 0; for (OffsetList::const_iterator i = offsetList.begin(); i != offsetList.end(); ++i) { if ((*i).first > pos) { break; } previousOffset = (*i).second; } return pos + previousOffset; } QString KTextEditor::DocumentPrivate::decodeCharacters(const KTextEditor::Range &range, KTextEditor::DocumentPrivate::OffsetList &decToEncOffsetList, KTextEditor::DocumentPrivate::OffsetList &encToDecOffsetList) { QString toReturn; KTextEditor::Cursor previous = range.start(); int decToEncCurrentOffset = 0, encToDecCurrentOffset = 0; int i = 0; int newI = 0; KateHighlighting *highlighting = highlight(); Kate::TextLine textLine; const int rangeStartLine = range.start().line(); const int rangeStartColumn = range.start().column(); const int rangeEndLine = range.end().line(); const int rangeEndColumn = range.end().column(); for (int line = range.start().line(); line <= rangeEndLine; ++line) { textLine = kateTextLine(line); int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0; int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine->length(); for (int col = startColumn; col < endColumn;) { int attr = textLine->attribute(col); const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr); const QHash &characterEncodingsHash = highlighting->getCharacterEncodings(attr); QString matchingPrefix = prefixStore.findPrefix(textLine, col); if (!matchingPrefix.isEmpty()) { toReturn += text(KTextEditor::Range(previous, KTextEditor::Cursor(line, col))); const QChar &c = characterEncodingsHash.value(matchingPrefix); const bool isNullChar = c.isNull(); if (!c.isNull()) { toReturn += c; } i += matchingPrefix.length(); col += matchingPrefix.length(); previous = KTextEditor::Cursor(line, col); decToEncCurrentOffset = decToEncCurrentOffset - (isNullChar ? 0 : 1) + matchingPrefix.length(); encToDecCurrentOffset = encToDecCurrentOffset - matchingPrefix.length() + (isNullChar ? 0 : 1); newI += (isNullChar ? 0 : 1); decToEncOffsetList.push_back(QPair(newI, decToEncCurrentOffset)); encToDecOffsetList.push_back(QPair(i, encToDecCurrentOffset)); continue; } ++col; ++i; ++newI; } ++i; ++newI; } if (previous < range.end()) { toReturn += text(KTextEditor::Range(previous, range.end())); } return toReturn; } void KTextEditor::DocumentPrivate::replaceCharactersByEncoding(const KTextEditor::Range &range) { KateHighlighting *highlighting = highlight(); Kate::TextLine textLine; const int rangeStartLine = range.start().line(); const int rangeStartColumn = range.start().column(); const int rangeEndLine = range.end().line(); const int rangeEndColumn = range.end().column(); for (int line = range.start().line(); line <= rangeEndLine; ++line) { textLine = kateTextLine(line); int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0; int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine->length(); for (int col = startColumn; col < endColumn;) { int attr = textLine->attribute(col); const QHash &reverseCharacterEncodingsHash = highlighting->getReverseCharacterEncodings(attr); QHash::const_iterator it = reverseCharacterEncodingsHash.find(textLine->at(col)); if (it != reverseCharacterEncodingsHash.end()) { replaceText(KTextEditor::Range(line, col, line, col + 1), *it); col += (*it).length(); continue; } ++col; } } } // // Highlighting information // KTextEditor::Attribute::Ptr KTextEditor::DocumentPrivate::attributeAt(const KTextEditor::Cursor &position) { KTextEditor::Attribute::Ptr attrib(new KTextEditor::Attribute()); KTextEditor::ViewPrivate *view = m_views.empty() ? nullptr : m_views.begin().value(); if (!view) { qCWarning(LOG_KTE) << "ATTENTION: cannot access lineAttributes() without any View (will be fixed eventually)"; return attrib; } Kate::TextLine kateLine = kateTextLine(position.line()); if (!kateLine) { return attrib; } *attrib = *view->renderer()->attribute(kateLine->attribute(position.column())); return attrib; } QStringList KTextEditor::DocumentPrivate::embeddedHighlightingModes() const { return highlight()->getEmbeddedHighlightingModes(); } QString KTextEditor::DocumentPrivate::highlightingModeAt(const KTextEditor::Cursor &position) { return highlight()->higlightingModeForLocation(this, position); } Kate::SwapFile *KTextEditor::DocumentPrivate::swapFile() { return m_swapfile; } /** * \return \c -1 if \c line or \c column invalid, otherwise one of * standard style attribute number */ int KTextEditor::DocumentPrivate::defStyleNum(int line, int column) { // Validate parameters to prevent out of range access if (line < 0 || line >= lines() || column < 0) { return -1; } // get highlighted line Kate::TextLine tl = kateTextLine(line); // make sure the textline is a valid pointer if (!tl) { return -1; } /** * either get char attribute or attribute of context still active at end of line */ int attribute = 0; if (column < tl->length()) { attribute = tl->attribute(column); } else if (column == tl->length()) { if (!tl->attributesList().isEmpty()) { attribute = tl->attributesList().back().attributeValue; } else { return -1; } } else { return -1; } return highlight()->defaultStyleForAttribute(attribute); } bool KTextEditor::DocumentPrivate::isComment(int line, int column) { const int defaultStyle = defStyleNum(line, column); return defaultStyle == KTextEditor::dsComment; } int KTextEditor::DocumentPrivate::findTouchedLine(int startLine, bool down) { const int offset = down ? 1 : -1; const int lineCount = lines(); while (startLine >= 0 && startLine < lineCount) { Kate::TextLine tl = m_buffer->plainLine(startLine); if (tl && (tl->markedAsModified() || tl->markedAsSavedOnDisk())) { return startLine; } startLine += offset; } return -1; } void KTextEditor::DocumentPrivate::setActiveTemplateHandler(KateTemplateHandler *handler) { // delete any active template handler delete m_activeTemplateHandler.data(); m_activeTemplateHandler = handler; } // BEGIN KTextEditor::MessageInterface bool KTextEditor::DocumentPrivate::postMessage(KTextEditor::Message *message) { // no message -> cancel if (!message) { return false; } // make sure the desired view belongs to this document if (message->view() && message->view()->document() != this) { qCWarning(LOG_KTE) << "trying to post a message to a view of another document:" << message->text(); return false; } message->setParent(this); message->setDocument(this); // if there are no actions, add a close action by default if widget does not auto-hide if (message->actions().count() == 0 && message->autoHide() < 0) { QAction *closeAction = new QAction(QIcon::fromTheme(QStringLiteral("window-close")), i18n("&Close"), nullptr); closeAction->setToolTip(i18n("Close message")); message->addAction(closeAction); } // reparent actions, as we want full control over when they are deleted QList> managedMessageActions; const auto messageActions = message->actions(); managedMessageActions.reserve(messageActions.size()); for (QAction *action : messageActions) { action->setParent(nullptr); managedMessageActions.append(QSharedPointer(action)); } m_messageHash.insert(message, managedMessageActions); // post message to requested view, or to all views if (KTextEditor::ViewPrivate *view = qobject_cast(message->view())) { view->postMessage(message, managedMessageActions); } else { for (auto view : qAsConst(m_views)) { view->postMessage(message, managedMessageActions); } } // also catch if the user manually calls delete message connect(message, &Message::closed, this, &DocumentPrivate::messageDestroyed); return true; } void KTextEditor::DocumentPrivate::messageDestroyed(KTextEditor::Message *message) { // KTE:Message is already in destructor Q_ASSERT(m_messageHash.contains(message)); m_messageHash.remove(message); } // END KTextEditor::MessageInterface void KTextEditor::DocumentPrivate::closeDocumentInApplication() { KTextEditor::EditorPrivate::self()->application()->closeDocument(this); } diff --git a/src/document/katedocument.h b/src/document/katedocument.h index 71c33bcc..5756d0df 100644 --- a/src/document/katedocument.h +++ b/src/document/katedocument.h @@ -1,1451 +1,1461 @@ /* SPDX-License-Identifier: LGPL-2.0-or-later Copyright (C) 2001-2004 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 1999 Jochen Wilhelmy Copyright (C) 2006 Hamish Rodda This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KATE_DOCUMENT_H_ #define _KATE_DOCUMENT_H_ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "katetextline.h" #include class KateTemplateHandler; namespace KTextEditor { class Plugin; class Attribute; class TemplateScript; } namespace KIO { class TransferJob; } namespace Kate { class SwapFile; } class KateBuffer; namespace KTextEditor { class ViewPrivate; } class KateDocumentConfig; class KateHighlighting; class KateUndoManager; class KateOnTheFlyChecker; class KateDocumentTest; class KateAutoIndent; class KateModOnHdPrompt; class KToggleAction; /** * @brief Backend of KTextEditor::Document related public KTextEditor interfaces. * * @warning This file is @e private API and not part of the public * KTextEditor interfaces. */ class KTEXTEDITOR_EXPORT KTextEditor::DocumentPrivate : public KTextEditor::Document, - public KTextEditor::MarkInterface, + public KTextEditor::MarkInterfaceV2, public KTextEditor::ModificationInterface, public KTextEditor::ConfigInterface, public KTextEditor::AnnotationInterface, public KTextEditor::MovingInterface, private KTextEditor::MovingRangeFeedback { Q_OBJECT Q_INTERFACES(KTextEditor::MarkInterface) + Q_INTERFACES(KTextEditor::MarkInterfaceV2) Q_INTERFACES(KTextEditor::ModificationInterface) Q_INTERFACES(KTextEditor::AnnotationInterface) Q_INTERFACES(KTextEditor::ConfigInterface) Q_INTERFACES(KTextEditor::MovingInterface) friend class KTextEditor::Document; friend class ::KateDocumentTest; friend class ::KateBuffer; public: explicit DocumentPrivate(bool bSingleViewMode = false, bool bReadOnly = false, QWidget *parentWidget = nullptr, QObject * = nullptr); ~DocumentPrivate() override; using ReadWritePart::closeUrl; bool closeUrl() override; bool openUrl(const QUrl &url) override; KTextEditor::Range rangeOnLine(KTextEditor::Range range, int line) const; private: void showAndSetOpeningErrorAccess(); /* * Overload this to have on-demand view creation */ public: /** * @return The widget defined by this part, set by setWidget(). */ QWidget *widget() override; public: bool readOnly() const { return m_bReadOnly; } bool singleViewMode() const { return m_bSingleViewMode; } private: // only to make part work, don't change it ! const bool m_bSingleViewMode; const bool m_bReadOnly; // // KTextEditor::Document stuff // public: KTextEditor::View *createView(QWidget *parent, KTextEditor::MainWindow *mainWindow = nullptr) override; QList views() const override { return m_viewsCache; } virtual KTextEditor::View *activeView() const { return m_activeView; } private: QHash m_views; KTextEditor::View *m_activeView = nullptr; // // KTextEditor::EditInterface stuff // public Q_SLOTS: bool setText(const QString &) override; bool setText(const QStringList &text) override; bool clear() override; bool insertText(const KTextEditor::Cursor &position, const QString &s, bool block = false) override; bool insertText(const KTextEditor::Cursor &position, const QStringList &text, bool block = false) override; bool insertLine(int line, const QString &s) override; bool insertLines(int line, const QStringList &s) override; bool removeText(const KTextEditor::Range &range, bool block = false) override; bool removeLine(int line) override; bool replaceText(const KTextEditor::Range &range, const QString &s, bool block = false) override; // unhide method... bool replaceText(const KTextEditor::Range &r, const QStringList &l, bool b) override { return KTextEditor::Document::replaceText(r, l, b); } public: bool isEditingTransactionRunning() const override; QString text(const KTextEditor::Range &range, bool blockwise = false) const override; QStringList textLines(const KTextEditor::Range &range, bool block = false) const override; QString text() const override; QString line(int line) const override; QChar characterAt(const KTextEditor::Cursor &position) const override; QString wordAt(const KTextEditor::Cursor &cursor) const override; KTextEditor::Range wordRangeAt(const KTextEditor::Cursor &cursor) const override; bool isValidTextPosition(const KTextEditor::Cursor &cursor) const override; int lines() const override; bool isLineModified(int line) const override; bool isLineSaved(int line) const override; bool isLineTouched(int line) const override; KTextEditor::Cursor documentEnd() const override; int totalCharacters() const override; int lineLength(int line) const override; Q_SIGNALS: void charactersSemiInteractivelyInserted(const KTextEditor::Cursor &position, const QString &text); /** * The \p document emits this signal whenever text was inserted. The * insertion occurred at range.start(), and new text now occupies up to * range.end(). * \param document document which emitted this signal * \param range range that the newly inserted text occupies * \see insertText(), insertLine() */ void textInserted(KTextEditor::Document *document, const KTextEditor::Range &range); /** * The \p document emits this signal whenever \p range was removed, i.e. * text was removed. * \param document document which emitted this signal * \param range range that the removed text previously occupied * \param oldText the text that has been removed * \see removeText(), removeLine(), clear() */ void textRemoved(KTextEditor::Document *document, const KTextEditor::Range &range, const QString &oldText); public: // BEGIN editStart/editEnd (start, end, undo, cursor update, view update) /** * Enclose editor actions with @p editStart() and @p editEnd() to group * them. */ bool editStart(); /** * Alias for @p editStart() */ void editBegin() { editStart(); } /** * End a editor operation. * @see editStart() */ bool editEnd(); void pushEditState(); void popEditState(); virtual bool startEditing() { return editStart(); } virtual bool finishEditing() { return editEnd(); } // END editStart/editEnd void inputMethodStart(); void inputMethodEnd(); // BEGIN LINE BASED INSERT/REMOVE STUFF (editStart() and editEnd() included) /** * Add a string in the given line/column * @param line line number * @param col column * @param s string to be inserted * @return true on success */ bool editInsertText(int line, int col, const QString &s); /** * Remove a string in the given line/column * @param line line number * @param col column * @param len length of text to be removed * @return true on success */ bool editRemoveText(int line, int col, int len); /** * Mark @p line as @p autowrapped. This is necessary if static word warp is * enabled, because we have to know whether to insert a new line or add the * wrapped words to the following line. * @param line line number * @param autowrapped autowrapped? * @return true on success */ bool editMarkLineAutoWrapped(int line, bool autowrapped); /** * Wrap @p line. If @p newLine is true, ignore the textline's flag * KateTextLine::flagAutoWrapped and force a new line. Whether a new line * was needed/added you can grab with @p newLineAdded. * @param line line number * @param col column * @param newLine if true, force a new line * @param newLineAdded return value is true, if new line was added (may be 0) * @return true on success */ bool editWrapLine(int line, int col, bool newLine = true, bool *newLineAdded = nullptr); /** * Unwrap @p line. If @p removeLine is true, we force to join the lines. If * @p removeLine is true, @p length is ignored (eg not needed). * @param line line number * @param removeLine if true, force to remove the next line * @return true on success */ bool editUnWrapLine(int line, bool removeLine = true, int length = 0); /** * Insert a string at the given line. * @param line line number * @param s string to insert * @return true on success */ bool editInsertLine(int line, const QString &s); /** * Remove a line * @param line line number * @return true on success */ bool editRemoveLine(int line); bool editRemoveLines(int from, int to); /** * Warp a line * @param startLine line to begin wrapping * @param endLine line to stop wrapping * @return true on success */ bool wrapText(int startLine, int endLine); /** * Wrap lines touched by the selection with respect of existing paragraphs. * To do so will the paragraph prior to the wrap joined as one single line * which cause an almost perfect wrapped paragraph as long as there are no * unneeded spaces exist or some formatting like this comment block. * Without any selection the current line is wrapped. * Empty lines around each paragraph are untouched. * @param first line to begin wrapping * @param last line to stop wrapping * @return true on success */ bool wrapParagraph(int first, int last); // END LINE BASED INSERT/REMOVE STUFF Q_SIGNALS: /** * Emitted when text from @p line was wrapped at position pos onto line @p nextLine. */ void editLineWrapped(int line, int col, int len); /** * Emitted each time text from @p nextLine was upwrapped onto @p line. */ void editLineUnWrapped(int line, int col); public: bool isEditRunning() const; void setUndoMergeAllEdits(bool merge); enum EditingPositionKind { Previous, Next }; /** *Returns the next or previous position cursor in this document from the stack depending on the argument passed. *@return cursor invalid if m_editingStack empty */ KTextEditor::Cursor lastEditingPosition(EditingPositionKind nextOrPrevious, KTextEditor::Cursor); private: int editSessionNumber = 0; QStack editStateStack; bool editIsRunning = false; bool m_undoMergeAllEdits = false; KTextEditor::Cursor m_editLastChangeStartCursor = KTextEditor::Cursor::invalid(); QStack> m_editingStack; int m_editingStackPosition = -1; // // KTextEditor::UndoInterface stuff // public Q_SLOTS: void undo(); void redo(); /** * Removes all the elements in m_editingStack of the respective document. */ void clearEditingPosStack(); /** * Saves the editing positions into the stack. * If the consecutive editings happens in the same line, then remove * the previous and add the new one with updated column no. */ void saveEditingPositions(const KTextEditor::Cursor &cursor); public: uint undoCount() const; uint redoCount() const; KateUndoManager *undoManager() { return m_undoManager; } protected: KateUndoManager *const m_undoManager; Q_SIGNALS: void undoChanged(); public: QVector searchText(const KTextEditor::Range &range, const QString &pattern, const KTextEditor::SearchOptions options) const; private: /** * Return a widget suitable to be used as a dialog parent. */ QWidget *dialogParent(); /* * Access to the mode/highlighting subsystem */ public: /** * @copydoc KTextEditor::Document::defaultStyleAt() */ KTextEditor::DefaultStyle defaultStyleAt(const KTextEditor::Cursor &position) const override; /** * Return the name of the currently used mode * \return name of the used mode */ QString mode() const override; /** * Return the name of the currently used mode * \return name of the used mode */ QString highlightingMode() const override; /** * Return a list of the names of all possible modes * \return list of mode names */ QStringList modes() const override; /** * Return a list of the names of all possible modes * \return list of mode names */ QStringList highlightingModes() const override; /** * Set the current mode of the document by giving its name * \param name name of the mode to use for this document * \return \e true on success, otherwise \e false */ bool setMode(const QString &name) override; /** * Set the current mode of the document by giving its name * \param name name of the mode to use for this document * \return \e true on success, otherwise \e false */ bool setHighlightingMode(const QString &name) override; /** * Returns the name of the section for a highlight given its @p index in the highlight * list (as returned by highlightModes()). * You can use this function to build a tree of the highlight names, organized in sections. * \param index in the highlight list for which to find the section name. */ QString highlightingModeSection(int index) const override; /** * Returns the name of the section for a mode given its @p index in the highlight * list (as returned by modes()). * You can use this function to build a tree of the mode names, organized in sections. * \param index index in the highlight list for which to find the section name. */ QString modeSection(int index) const override; /* * Helpers.... */ public: void bufferHlChanged(); /** * allow to mark, that we changed hl on user wish and should not reset it * atm used for the user visible menu to select highlightings */ void setDontChangeHlOnSave(); /** * Set that the BOM marker is forced via the tool menu */ void bomSetByUser(); public: /** * Read session settings from the given \p config. * * Known flags: * "SkipUrl" => don't save/restore the file * "SkipMode" => don't save/restore the mode * "SkipHighlighting" => don't save/restore the highlighting * "SkipEncoding" => don't save/restore the encoding * * \param config read the session settings from this KConfigGroup * \param flags additional flags * \see writeSessionConfig() */ void readSessionConfig(const KConfigGroup &config, const QSet &flags = QSet()) override; /** * Write session settings to the \p config. * See readSessionConfig() for more details. * * \param config write the session settings to this KConfigGroup * \param flags additional flags * \see readSessionConfig() */ void writeSessionConfig(KConfigGroup &config, const QSet &flags = QSet()) override; Q_SIGNALS: void configChanged(); // // KTextEditor::MarkInterface // public Q_SLOTS: void setMark(int line, uint markType) override; void clearMark(int line) override; void addMark(int line, uint markType) override; void removeMark(int line, uint markType) override; void clearMarks() override; void requestMarkTooltip(int line, QPoint position); /// Returns true if the click on the mark should not be further processed bool handleMarkClick(int line); /// Returns true if the context-menu event should not further be processed bool handleMarkContextMenu(int line, QPoint position); void setMarkPixmap(MarkInterface::MarkTypes, const QPixmap &) override; void setMarkDescription(MarkInterface::MarkTypes, const QString &) override; void setEditableMarks(uint markMask) override; public: uint mark(int line) override; const QHash &marks() override; QPixmap markPixmap(MarkInterface::MarkTypes) const override; QString markDescription(MarkInterface::MarkTypes) const override; virtual QColor markColor(MarkInterface::MarkTypes) const; uint editableMarks() const override; Q_SIGNALS: void markToolTipRequested(KTextEditor::Document *document, KTextEditor::Mark mark, QPoint position, bool &handled); void markContextMenuRequested(KTextEditor::Document *document, KTextEditor::Mark mark, QPoint pos, bool &handled); void markClicked(KTextEditor::Document *document, KTextEditor::Mark mark, bool &handled); void marksChanged(KTextEditor::Document *) override; void markChanged(KTextEditor::Document *, KTextEditor::Mark, KTextEditor::MarkInterface::MarkChangeAction) override; private: QHash m_marks; - QHash m_markPixmaps; + QHash m_markIcons; // QPixmap or QIcon, KF6: remove QPixmap support QHash m_markDescriptions; uint m_editableMarks = markType01; + // + // KTextEditor::MarkInterfaceV2 + // +public Q_SLOTS: + void setMarkIcon(MarkInterface::MarkTypes markType, const QIcon &icon) override; + +public: + QIcon markIcon(MarkInterface::MarkTypes markType) const override; + // KTextEditor::PrintInterface // public Q_SLOTS: bool print() override; void printPreview() override; // // KTextEditor::DocumentInfoInterface ( ### unfinished ) // public: /** * Tries to detect mime-type based on file name and content of buffer. * * @return the name of the mimetype for the document. */ QString mimeType() override; // // once was KTextEditor::VariableInterface // public: /** * Returns the value for the variable @p name. * If the Document does not have a variable called @p name, * an empty QString() is returned. * * @param name variable to query * @return value of the variable @p name * @see setVariable() */ virtual QString variable(const QString &name) const; /** * Set the variable @p name to @p value. Setting and changing a variable * has immediate effect on the Document. For instance, setting the variable * @e indent-mode to @e cstyle will immediately cause the Document to load * the C Style indenter. * * @param name the variable name * @param value the value to be set * @see variable() */ virtual void setVariable(const QString &name, const QString &value); private: QMap m_storedVariables; // // MovingInterface API // public: /** * Create a new moving cursor for this document. * @param position position of the moving cursor to create * @param insertBehavior insertion behavior * @return new moving cursor for the document */ KTextEditor::MovingCursor *newMovingCursor(const KTextEditor::Cursor &position, KTextEditor::MovingCursor::InsertBehavior insertBehavior = KTextEditor::MovingCursor::MoveOnInsert) override; /** * Create a new moving range for this document. * @param range range of the moving range to create * @param insertBehaviors insertion behaviors * @param emptyBehavior behavior on becoming empty * @return new moving range for the document */ KTextEditor::MovingRange *newMovingRange(const KTextEditor::Range &range, KTextEditor::MovingRange::InsertBehaviors insertBehaviors = KTextEditor::MovingRange::DoNotExpand, KTextEditor::MovingRange::EmptyBehavior emptyBehavior = KTextEditor::MovingRange::AllowEmpty) override; /** * Current revision * @return current revision */ qint64 revision() const override; /** * Last revision the buffer got successful saved * @return last revision buffer got saved, -1 if none */ qint64 lastSavedRevision() const override; /** * Lock a revision, this will keep it around until released again. * But all revisions will always be cleared on buffer clear() (and therefor load()) * @param revision revision to lock */ void lockRevision(qint64 revision) override; /** * Release a revision. * @param revision revision to release */ void unlockRevision(qint64 revision) override; /** * Transform a cursor from one revision to an other. * @param cursor cursor to transform * @param insertBehavior behavior of this cursor on insert of text at its position * @param fromRevision from this revision we want to transform * @param toRevision to this revision we want to transform, default of -1 is current revision */ void transformCursor(KTextEditor::Cursor &cursor, KTextEditor::MovingCursor::InsertBehavior insertBehavior, qint64 fromRevision, qint64 toRevision = -1) override; /** * Transform a cursor from one revision to an other. * @param line line number of the cursor to transform * @param column column number of the cursor to transform * @param insertBehavior behavior of this cursor on insert of text at its position * @param fromRevision from this revision we want to transform * @param toRevision to this revision we want to transform, default of -1 is current revision */ void transformCursor(int &line, int &column, KTextEditor::MovingCursor::InsertBehavior insertBehavior, qint64 fromRevision, qint64 toRevision = -1) override; /** * Transform a range from one revision to an other. * @param range range to transform * @param insertBehaviors behavior of this range on insert of text at its position * @param emptyBehavior behavior on becoming empty * @param fromRevision from this revision we want to transform * @param toRevision to this revision we want to transform, default of -1 is current revision */ void transformRange(KTextEditor::Range &range, KTextEditor::MovingRange::InsertBehaviors insertBehaviors, KTextEditor::MovingRange::EmptyBehavior emptyBehavior, qint64 fromRevision, qint64 toRevision = -1) override; // // MovingInterface Signals // Q_SIGNALS: /** * This signal is emitted before the cursors/ranges/revisions of a document are destroyed as the document is deleted. * @param document the document which the interface belongs too which is in the process of being deleted */ void aboutToDeleteMovingInterfaceContent(KTextEditor::Document *document); /** * This signal is emitted before the ranges of a document are invalidated and the revisions are deleted as the document is cleared (for example on load/reload). * While this signal is emitted, still the old document content is around before the clear. * @param document the document which the interface belongs too which will invalidate its data */ void aboutToInvalidateMovingInterfaceContent(KTextEditor::Document *document); // // Annotation Interface // public: void setAnnotationModel(KTextEditor::AnnotationModel *model) override; KTextEditor::AnnotationModel *annotationModel() const override; Q_SIGNALS: void annotationModelChanged(KTextEditor::AnnotationModel *, KTextEditor::AnnotationModel *); private: KTextEditor::AnnotationModel *m_annotationModel = nullptr; // // KParts::ReadWrite stuff // public: /** * open the file obtained by the kparts framework * the framework abstracts the loading of remote files * @return success */ bool openFile() override; /** * save the file obtained by the kparts framework * the framework abstracts the uploading of remote files * @return success */ bool saveFile() override; void setReadWrite(bool rw = true) override; void setModified(bool m) override; bool isAutoReload(); KToggleAction *autoReloadToggleAction() { return m_autoReloadMode; }; void delayAutoReload(); private Q_SLOTS: void autoReloadToggled(bool b); private: void activateDirWatch(const QString &useFileName = QString()); void deactivateDirWatch(); QString m_dirWatchFile; /** * Make backup copy during saveFile, if configured that way. * @return success? else saveFile should return false and not write the file */ bool createBackupFile(); public: /** * Type chars in a view. * Characters are filtered in KateViewInternal::isAcceptableInput() before calling typeChars. * * @param view view that received the input * @param chars characters to type */ void typeChars(KTextEditor::ViewPrivate *view, QString chars); /** * gets the last line number (lines() - 1) */ inline int lastLine() const { return lines() - 1; } // Repaint all of all of the views void repaintViews(bool paintOnlyDirty = true); KateHighlighting *highlight() const; public Q_SLOTS: void tagLines(int start, int end); private Q_SLOTS: void internalHlChanged(); public: void addView(KTextEditor::View *); /** removes the view from the list of views. The view is *not* deleted. * That's your job. Or, easier, just delete the view in the first place. * It will remove itself. TODO: this could be converted to a private slot * connected to the view's destroyed() signal. It is not currently called * anywhere except from the KTextEditor::ViewPrivate destructor. */ void removeView(KTextEditor::View *); void setActiveView(KTextEditor::View *); bool ownedView(KTextEditor::ViewPrivate *); int toVirtualColumn(int line, int column) const; int toVirtualColumn(const KTextEditor::Cursor &) const; int fromVirtualColumn(int line, int column) const; int fromVirtualColumn(const KTextEditor::Cursor &) const; enum NewLineIndent { Indent, NoIndent }; void newLine(KTextEditor::ViewPrivate *view, NewLineIndent indent = NewLineIndent::Indent); // Changes input void backspace(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &); void del(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &); void transpose(const KTextEditor::Cursor &); void paste(KTextEditor::ViewPrivate *view, const QString &text); public: void indent(KTextEditor::Range range, int change); void comment(KTextEditor::ViewPrivate *view, uint line, uint column, int change); void align(KTextEditor::ViewPrivate *view, const KTextEditor::Range &range); void insertTab(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &); enum TextTransform { Uppercase, Lowercase, Capitalize }; /** Handling uppercase, lowercase and capitalize for the view. If there is a selection, that is transformed, otherwise for uppercase or lowercase the character right of the cursor is transformed, for capitalize the word under the cursor is transformed. */ void transform(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &, TextTransform); /** Unwrap a range of lines. */ void joinLines(uint first, uint last); private: bool removeStringFromBeginning(int line, const QString &str); bool removeStringFromEnd(int line, const QString &str); /** Expand tabs to spaces in typed text, if enabled. @param cursorPos The current cursor position for the inserted characters. @param str The typed characters to expand. */ QString eventuallyReplaceTabs(const KTextEditor::Cursor &cursorPos, const QString &str) const; /** Find the position (line and col) of the next char that is not a space. If found line and col point to the found character. Otherwise they have both the value -1. @param line Line of the character which is examined first. @param col Column of the character which is examined first. @return True if the specified or a following character is not a space Otherwise false. */ bool nextNonSpaceCharPos(int &line, int &col); /** Find the position (line and col) of the previous char that is not a space. If found line and col point to the found character. Otherwise they have both the value -1. @return True if the specified or a preceding character is not a space. Otherwise false. */ bool previousNonSpaceCharPos(int &line, int &col); /** * Sets a comment marker as defined by the language providing the attribute * @p attrib on the line @p line */ void addStartLineCommentToSingleLine(int line, int attrib = 0); /** * Removes a comment marker as defined by the language providing the attribute * @p attrib on the line @p line */ bool removeStartLineCommentFromSingleLine(int line, int attrib = 0); /** * @see addStartLineCommentToSingleLine. */ void addStartStopCommentToSingleLine(int line, int attrib = 0); /** *@see removeStartLineCommentFromSingleLine. */ bool removeStartStopCommentFromSingleLine(int line, int attrib = 0); /** *@see removeStartLineCommentFromSingleLine. */ bool removeStartStopCommentFromRegion(const KTextEditor::Cursor &start, const KTextEditor::Cursor &end, int attrib = 0); /** * Add a comment marker as defined by the language providing the attribute * @p attrib to each line in the selection. */ void addStartStopCommentToSelection(KTextEditor::ViewPrivate *view, int attrib = 0); /** * @see addStartStopCommentToSelection. */ void addStartLineCommentToSelection(KTextEditor::ViewPrivate *view, int attrib = 0); /** * Removes comment markers relevant to the language providing * the attribute @p attrib from each line in the selection. * * @return whether the operation succeeded. */ bool removeStartStopCommentFromSelection(KTextEditor::ViewPrivate *view, int attrib = 0); /** * @see removeStartStopCommentFromSelection. */ bool removeStartLineCommentFromSelection(KTextEditor::ViewPrivate *view, int attrib = 0); public: KTextEditor::Range findMatchingBracket(const KTextEditor::Cursor &start, int maxLines); public: QString documentName() const override { return m_docName; } private: void updateDocName(); public: /** * @return whether the document is modified on disk since last saved */ bool isModifiedOnDisc() { return m_modOnHd; } void setModifiedOnDisk(ModifiedOnDiskReason reason) override; void setModifiedOnDiskWarning(bool on) override; public Q_SLOTS: /** * Ask the user what to do, if the file has been modified on disk. * Reimplemented from KTextEditor::Document. */ virtual void slotModifiedOnDisk(KTextEditor::View *v = nullptr); /** * Reloads the current document from disk if possible */ bool documentReload() override; bool documentSave() override; bool documentSaveAs() override; bool documentSaveAsWithEncoding(const QString &encoding); bool documentSaveCopyAs(); bool save() override; public: bool saveAs(const QUrl &url) override; Q_SIGNALS: /** * Indicate this file is modified on disk * @param doc the KTextEditor::Document object that represents the file on disk * @param isModified indicates the file was modified rather than created or deleted * @param reason the reason we are emitting the signal. */ void modifiedOnDisk(KTextEditor::Document *doc, bool isModified, KTextEditor::ModificationInterface::ModifiedOnDiskReason reason) override; private: // helper to handle the embedded notification for externally modified files QPointer m_modOnHdHandler; private Q_SLOTS: void onModOnHdSaveAs(); void onModOnHdClose(); void onModOnHdReload(); void onModOnHdAutoReload(); void onModOnHdIgnore(); public: bool setEncoding(const QString &e) override; QString encoding() const override; public Q_SLOTS: void setWordWrap(bool on); void setWordWrapAt(uint col); public: bool wordWrap() const; uint wordWrapAt() const; public Q_SLOTS: void setPageUpDownMovesCursor(bool on); public: bool pageUpDownMovesCursor() const; // code folding public: /** * Same as plainKateTextLine(), except that it is made sure * the line is highlighted. */ Kate::TextLine kateTextLine(int i); //! @copydoc KateBuffer::plainLine() Kate::TextLine plainKateTextLine(int i); Q_SIGNALS: void aboutToRemoveText(const KTextEditor::Range &); private Q_SLOTS: void slotModOnHdDirty(const QString &path); void slotModOnHdCreated(const QString &path); void slotModOnHdDeleted(const QString &path); void slotDelayedHandleModOnHd(); private: /** * Create a git compatible sha1 checksum of the file, if it is a local file. * The result can be accessed through KateBuffer::digest(). * * @return whether the operation was attempted and succeeded. */ bool createDigest(); /** * create a string for the modonhd warnings, giving the reason. */ QString reasonedMOHString() const; /** * Removes all trailing whitespace in the document. */ void removeTrailingSpaces(); public: /** * Returns a git compatible sha1 checksum of this document on disk. * @return checksum for this document on disk */ QByteArray checksum() const override; /** * @return false if @p newType is an invalid mode. */ bool updateFileType(const QString &newType, bool user = false); QString fileType() const { return m_fileType; } /** * Get access to buffer of this document. * Is needed to create cursors and ranges for example. * @return document buffer */ KateBuffer &buffer() { return *m_buffer; } /** * set indentation mode by user * this will remember that a user did set it and will avoid reset on save */ void rememberUserDidSetIndentationMode() { m_indenterSetByUser = true; } /** * User did set encoding for next reload => enforce it! */ void userSetEncodingForNextReload() { m_userSetEncodingForNextReload = true; } // // REALLY internal data ;) // private: // text buffer KateBuffer *const m_buffer; // indenter KateAutoIndent *const m_indenter; bool m_hlSetByUser = false; bool m_bomSetByUser = false; bool m_indenterSetByUser = false; bool m_userSetEncodingForNextReload = false; bool m_modOnHd = false; KToggleAction *m_autoReloadMode; QTimer m_autoReloadThrottle; ModifiedOnDiskReason m_modOnHdReason = OnDiskUnmodified; ModifiedOnDiskReason m_prevModOnHdReason = OnDiskUnmodified; QString m_docName; int m_docNameNumber = 0; // file type !!! QString m_fileType; bool m_fileTypeSetByUser = false; /** * document is still reloading a file */ bool m_reloading = false; public Q_SLOTS: void slotQueryClose_save(bool *handled, bool *abortClosing); public: bool queryClose() override; /** * Configuration */ public: KateDocumentConfig *config() { return m_config; } KateDocumentConfig *config() const { return m_config; } void updateConfig(); private: void makeAttribs(bool needInvalidate = true); KateDocumentConfig *const m_config; /** * Variable Reader * TODO add register functionality/ktexteditor interface */ private: /** * read dir config file */ void readDirConfig(); /** Reads all the variables in the document. Called when opening/saving a document */ void readVariables(bool onlyViewAndRenderer = false); /** Reads and applies the variables in a single line TODO registered variables gets saved in a [map] */ void readVariableLine(const QString &t, bool onlyViewAndRenderer = false); /** Sets a view variable in all the views. */ void setViewVariable(const QString &var, const QString &val); /** @return weather a string value could be converted to a bool value as supported. The value is put in *result. */ static bool checkBoolValue(QString value, bool *result); /** @return weather a string value could be converted to a integer value. The value is put in *result. */ static bool checkIntValue(const QString &value, int *result); /** Feeds value into @p col using QColor::setNamedColor() and returns whether the color is valid */ static bool checkColorValue(const QString &value, QColor &col); bool m_fileChangedDialogsActivated = false; // // KTextEditor::ConfigInterface // public: QStringList configKeys() const override; QVariant configValue(const QString &key) override; void setConfigValue(const QString &key, const QVariant &value) override; // // KTextEditor::RecoveryInterface // public: bool isDataRecoveryAvailable() const override; void recoverData() override; void discardDataRecovery() override; // // Highlighting information // public: QStringList embeddedHighlightingModes() const override; QString highlightingModeAt(const KTextEditor::Cursor &position) override; // TODO KDE5: move to View virtual KTextEditor::Attribute::Ptr attributeAt(const KTextEditor::Cursor &position); // // BEGIN: KTextEditor::MessageInterface // public: bool postMessage(KTextEditor::Message *message) override; public Q_SLOTS: void messageDestroyed(KTextEditor::Message *message); private: QHash>> m_messageHash; // END KTextEditor::MessageInterface public: QString defaultDictionary() const; QList> dictionaryRanges() const; bool isOnTheFlySpellCheckingEnabled() const; QString dictionaryForMisspelledRange(const KTextEditor::Range &range) const; void clearMisspellingForWord(const QString &word); public Q_SLOTS: void clearDictionaryRanges(); void setDictionary(const QString &dict, const KTextEditor::Range &range, bool blockmode); void setDictionary(const QString &dict, const KTextEditor::Range &range); void setDefaultDictionary(const QString &dict); void onTheFlySpellCheckingEnabled(bool enable); void refreshOnTheFlyCheck(const KTextEditor::Range &range = KTextEditor::Range::invalid()); Q_SIGNALS: void dictionaryRangesPresent(bool yesNo); void defaultDictionaryChanged(KTextEditor::DocumentPrivate *document); public: bool containsCharacterEncoding(const KTextEditor::Range &range); typedef QList> OffsetList; int computePositionWrtOffsets(const OffsetList &offsetList, int pos); /** * The first OffsetList is from decoded to encoded, and the second OffsetList from * encoded to decoded. **/ QString decodeCharacters(const KTextEditor::Range &range, KTextEditor::DocumentPrivate::OffsetList &decToEncOffsetList, KTextEditor::DocumentPrivate::OffsetList &encToDecOffsetList); void replaceCharactersByEncoding(const KTextEditor::Range &range); protected: KateOnTheFlyChecker *m_onTheFlyChecker = nullptr; QString m_defaultDictionary; QList> m_dictionaryRanges; // from KTextEditor::MovingRangeFeedback void rangeInvalid(KTextEditor::MovingRange *movingRange) override; void rangeEmpty(KTextEditor::MovingRange *movingRange) override; void deleteDictionaryRange(KTextEditor::MovingRange *movingRange); private: Kate::SwapFile *m_swapfile; public: Kate::SwapFile *swapFile(); // helpers for scripting and codefolding int defStyleNum(int line, int column); bool isComment(int line, int column); public: /** * Find the next modified/saved line, starting at @p startLine. If @p down * is \e true, the search is performed downwards, otherwise upwards. * @return the touched line in the requested search direction, or -1 if not found */ int findTouchedLine(int startLine, bool down); private Q_SLOTS: /** * watch for all started io jobs to remember if file is perhaps loading atm * @param job started job */ void slotStarted(KIO::Job *job); void slotCompleted(); void slotCanceled(); /** * trigger display of loading message, after 1000 ms */ void slotTriggerLoadingMessage(); /** * Abort loading */ void slotAbortLoading(); void slotUrlChanged(const QUrl &url); private: /** * different possible states */ enum DocumentStates { /** * Idle */ DocumentIdle, /** * Loading */ DocumentLoading, /** * Saving */ DocumentSaving, /** * Pre Saving As, this is between ::saveAs is called and ::save */ DocumentPreSavingAs, /** * Saving As */ DocumentSavingAs }; /** * current state */ DocumentStates m_documentState = DocumentIdle; /** * read-write state before loading started */ bool m_readWriteStateBeforeLoading = false; /** * if the document is untitled */ bool m_isUntitled = true; /** * loading job, we want to cancel with cancel in the loading message */ QPointer m_loadingJob; /** * message to show during loading */ QPointer m_loadingMessage; /** * Was there any open error on last file loading? */ bool m_openingError = false; /** * Last open file error message */ QString m_openingErrorMessage; public: /** * reads the line length limit from config, if it is not overridden */ int lineLengthLimit() const; public Q_SLOTS: void openWithLineLengthLimitOverride(); private: /** * timer for delayed handling of mod on hd */ QTimer m_modOnHdTimer; private: /** * currently active template handler; there can be only one */ QPointer m_activeTemplateHandler; private: /** * current autobrace range */ QSharedPointer m_currentAutobraceRange; /** * current autobrace closing character (e.g. ']') */ QChar m_currentAutobraceClosingChar; private Q_SLOTS: void checkCursorForAutobrace(KTextEditor::View *view, const KTextEditor::Cursor &newPos); public: void setActiveTemplateHandler(KateTemplateHandler *handler); Q_SIGNALS: void loaded(KTextEditor::DocumentPrivate *document); private Q_SLOTS: /** * trigger a close of this document in the application */ void closeDocumentInApplication(); private: // To calculate a QHash.keys() is quite expensive, // better keep a copy of that list updated when a view is added or removed. QList m_viewsCache; }; #endif diff --git a/src/include/ktexteditor/markinterface.h b/src/include/ktexteditor/markinterface.h index 92175a87..13724f63 100644 --- a/src/include/ktexteditor/markinterface.h +++ b/src/include/ktexteditor/markinterface.h @@ -1,403 +1,461 @@ /* SPDX-License-Identifier: LGPL-2.0-or-later Copyright (C) 2001 Christoph Cullmann (cullmann@kde.org) Copyright (C) 2005 Dominik Haumann (dhdev@gmx.de) (documentation) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KTEXTEDITOR_MARKINTERFACE_H #define KTEXTEDITOR_MARKINTERFACE_H #include #include #include +class QIcon; class QPixmap; class QPoint; class QMenu; namespace KTextEditor { class Document; /** * \brief Mark class containing line and mark types. * * \section mark_intro Introduction * * The class Mark represents a mark in a Document. It contains the \e line * and \e type. A line can have multiple marks, like a \e bookmark and a * \e breakpoint, i.e. the \e type contains all marks combined with a logical * \e OR (|). There are several predefined mark types, look into the * MarkInterface for further details. * * \see KTextEditor::MarkInterface, KTextEditor::Document */ class Mark { public: /** The line that contains the mark. */ int line; /** The mark types in the line, combined with logical OR. */ uint type; }; /** * \brief Mark extension interface for the Document. * * \ingroup kte_group_doc_extensions * * \section markext_intro Introduction * * The MarkInterface provides methods to enable and disable marks in a * Document, a marked line can be visualized for example with a shaded * background color and/or a pixmap in the iconborder of the Document's View. * There are a number of predefined mark types, specified in * reservedMarkersCount(). Additionally it is possible to add custom marks * and set custom pixmaps. * * \section markext_access Accessing the Interface * * The MarkInterface is supposed to be an extension interface for a Document, * i.e. the Document inherits the interface \e provided that the * KTextEditor library in use implements the interface. Use qobject_cast to access * the interface: * \code * // doc is of type KTextEditor::Document* * auto iface = qobject_cast(doc); * * if (iface) { * // the implementation supports the interface * // do stuff * } else { * // the implementation does not support the interface * } * \endcode * * \section markext_handling Handling Marks * * Get all marks in the document by calling marks(). Use clearMarks() to * remove all marks in the entire document. A single mark can be retrieved * with mark(). To remove all marks from a line call clearMark(). To add * and remove marks from a given line use addMark() and removeMark(). It is * also possible to replace all marks with setMark(), i.e. setMark() is the * same as a call of clearMark() followed by addMark(). The signals * marksChanged() and markChanged() are emitted whenever a line's marks * changed. * * \attention A mark type is represented as an \e uint. An \e uint can have * several mark types combined (see above: logical OR). That means for * all functions/signals with an \e uint parameter, e.g. setMark(), * removeMark(), etc, the \e uint may contain \e multiple marks, i.e. * you can add and remove multiple marks \e simultaneously. * * \section markext_userdefined User Defined Marks * * All marks that should be editable by the user can be specified with a mark * mask via setEditableMarks(). To set a description and pixmap of a mark type * call setMarkDescription() and setMarkPixmap(). * * \see KTextEditor::Document, KTextEditor::Mark * \author Christoph Cullmann \ */ class KTEXTEDITOR_EXPORT MarkInterface { public: MarkInterface(); /** * Virtual destructor. */ virtual ~MarkInterface(); // // slots !!! // public: /** * Get all marks set on the \p line. * \param line requested line * \return a \e uint representing of the marks set in \p line concatenated * by logical OR * \see addMark(), removeMark() */ virtual uint mark(int line) = 0; /** * Set the \p line's mark types to \p markType. * If \p line already contains a mark of the given type it has no effect. * All other marks are deleted before the mark is set. You can achieve * the same by calling * \code * clearMark(line); * addMark(line, markType); * \endcode * \param line line to set the mark * \param markType mark type * \see clearMark(), addMark(), mark() */ virtual void setMark(int line, uint markType) = 0; /** * Clear all marks set in the \p line. * \param line line to clear marks * \see clearMarks(), removeMark(), addMark() */ virtual void clearMark(int line) = 0; /** * Add marks of type \p markType to \p line. Existing marks on this line * are preserved. If the mark \p markType already is set, nothing * happens. * \param line line to set the mark * \param markType mark type * \see removeMark(), setMark() */ virtual void addMark(int line, uint markType) = 0; /** * Remove the mark mask of type \p markType from \p line. * \param line line to remove the mark * \param markType mark type to be removed * \see clearMark() */ virtual void removeMark(int line, uint markType) = 0; /** * Get a hash holding all marks in the document. * The hash key for a mark is its line. * \return a hash holding all marks in the document */ virtual const QHash &marks() = 0; /** * Clear all marks in the entire document. * \see clearMark(), removeMark() */ /// TODO: dominik: add argument unit mask = 0 virtual void clearMarks() = 0; /** * Get the number of predefined mark types we have so far. * \note FIXME: If you change this you have to make sure katepart * supports the new size! * \return number of reserved marker types */ static int reservedMarkersCount() { return 7; } /** * Predefined mark types. * * To add a new standard mark type, edit this interface and document * the type. */ enum MarkTypes { /** Bookmark */ markType01 = 0x1, /** Breakpoint active */ markType02 = 0x2, /** Breakpoint reached */ markType03 = 0x4, /** Breakpoint disabled */ markType04 = 0x8, /** Execution mark */ markType05 = 0x10, /** Warning */ markType06 = 0x20, /** Error */ markType07 = 0x40, markType08 = 0x80, markType09 = 0x100, markType10 = 0x200, markType11 = 0x400, markType12 = 0x800, markType13 = 0x1000, markType14 = 0x2000, markType15 = 0x4000, markType16 = 0x8000, markType17 = 0x10000, markType18 = 0x20000, markType19 = 0x40000, markType20 = 0x80000, markType21 = 0x100000, markType22 = 0x200000, markType23 = 0x400000, markType24 = 0x800000, markType25 = 0x1000000, markType26 = 0x2000000, markType27 = 0x4000000, markType28 = 0x8000000, markType29 = 0x10000000, markType30 = 0x20000000, markType31 = 0x40000000, markType32 = 0x80000000, /* reserved marks */ Bookmark = markType01, BreakpointActive = markType02, BreakpointReached = markType03, BreakpointDisabled = markType04, Execution = markType05, Warning = markType06, Error = markType07, SearchMatch = markType32, }; // // signals !!! // public: /** * The \p document emits this signal whenever a mark mask changed. * \param document document which emitted this signal * \see markChanged() */ virtual void marksChanged(KTextEditor::Document *document) = 0; /* * Methods to modify mark properties. */ public: /** * Set the \p mark's pixmap to \p pixmap. * \param mark mark to which the pixmap will be attached * \param pixmap new pixmap * \see setMarkDescription() */ virtual void setMarkPixmap(MarkTypes mark, const QPixmap &pixmap) = 0; /** * Get the \p mark's pixmap. * \param mark mark type. If the pixmap does not exist the resulting is null * (check with QPixmap::isNull()). * \see setMarkDescription() */ virtual QPixmap markPixmap(MarkTypes mark) const = 0; /** * Set the \p mark's description to \p text. * \param mark mark to set the description * \param text new descriptive text * \see markDescription(), setMarkPixmap() */ virtual void setMarkDescription(MarkTypes mark, const QString &text) = 0; /** * Get the \p mark's description to text. * \param mark mark to set the description * \return text of the given \p mark or QString(), if the entry does not * exist * \see setMarkDescription(), setMarkPixmap() */ virtual QString markDescription(MarkTypes mark) const = 0; /** * Set the mark mask the user is allowed to toggle to \p markMask. * I.e. concatenate all editable marks with a logical OR. If the user should * be able to add a bookmark and set a breakpoint with the context menu in * the icon pane, you have to call * \code * // iface is of Type KTextEditor::MarkInterface* * // only make bookmark and breakpoint editable * iface->setEditableMarks( MarkInterface::Bookmark | * MarkInterface::BreakpointActive ); * * // or preserve last settings, and add bookmark and breakpoint * iface->setEditableMarks( iface->editableMarks() | * MarkInterface::Bookmark | * MarkInterface::BreakpointActive ); * \endcode * \param markMask bitmap pattern * \see editableMarks(), setMarkPixmap(), setMarkDescription() */ virtual void setEditableMarks(uint markMask) = 0; /** * Get, which marks can be toggled by the user. * The returned value is a mark mask containing all editable marks combined * with a logical OR. * \return mark mask containing all editable marks * \see setEditableMarks() */ virtual uint editableMarks() const = 0; /** * Possible actions on a mark. * \see markChanged() */ enum MarkChangeAction { MarkAdded = 0, /**< action: a mark was added. */ MarkRemoved = 1 /**< action: a mark was removed. */ }; // // signals !!! // public: /** * The \p document emits this signal whenever the \p mark changes. * \param document the document which emitted the signal * \param mark changed mark * \param action action, either removed or added * \see marksChanged() */ virtual void markChanged(KTextEditor::Document *document, KTextEditor::Mark mark, KTextEditor::MarkInterface::MarkChangeAction action) = 0; Q_SIGNALS: /** * The \p document emits this signal whenever the \p mark is hovered using the mouse, * and the receiver may show a tooltip. * \param document the document which emitted the signal * \param mark mark that was hovered * \param position mouse position during the hovering * \param handled set this to 'true' if this event was handled externally */ void markToolTipRequested(KTextEditor::Document *document, KTextEditor::Mark mark, QPoint position, bool &handled); /** * The \p document emits this signal whenever the \p mark is right-clicked to show a context menu. * The receiver may show an own context menu instead of the kate internal one. * \param document the document which emitted the signal * \param mark mark that was right-clicked * \param pos position where the menu should be started * \param handled set this to 'true' if this event was handled externally, and kate should not create an own context menu. */ void markContextMenuRequested(KTextEditor::Document *document, KTextEditor::Mark mark, QPoint pos, bool &handled); /** * The \p document emits this signal whenever the \p mark is left-clicked. * \param document the document which emitted the signal * \param mark mark that was right-clicked * \param handled set this to 'true' if this event was handled externally, and kate should not do own handling of the left click. */ void markClicked(KTextEditor::Document *document, KTextEditor::Mark mark, bool &handled); private: class MarkInterfacePrivate *const d = nullptr; }; +/** + * \brief Mark extension interface for the Document, version 2 + * + * \ingroup kte_group_doc_extensions + * + * \section markextv2_intro Introduction + * + * The MarkInterfaceV2 allows to do the same as MarkInterface + * and additionally + * - (1) set an icon for a mark type instead of just a pixmap + * + * \section markextv2_access Accessing the Interface + * + * The MarkInterfaceV2 is supposed to be an extension interface for a Document, + * i.e. the Document inherits the interface \e provided that the + * KTextEditor library in use implements the interface. Use qobject_cast to access + * the interface: + * \code + * // doc is of type KTextEditor::Document* + * auto iface = qobject_cast(doc); + * + * if (iface) { + * // the implementation supports the interface + * // do stuff + * } else { + * // the implementation does not support the interface + * } + * \endcode + * + * \since 5.69 + */ +class KTEXTEDITOR_EXPORT MarkInterfaceV2 : public MarkInterface +{ + // KF6: Merge KTextEditor::MarkInterfaceV2 into KTextEditor::MarkInterface, drop QPixmap API (kossebau) +public: + virtual ~MarkInterfaceV2() + { + } + + /** + * Set the \p mark's icon to \p icon. + * \param markType mark type to which the icon will be attached + * \param icon new icon + * \see setMarkDescription() + */ + virtual void setMarkIcon(MarkTypes markType, const QIcon &icon) = 0; + + /** + * Get the \p mark's icon. + * \param markType mark type. If the icon does not exist the resulting is null + * (check with QIcon::isNull()). + * \see setMarkDescription() + */ + virtual QIcon markIcon(MarkTypes markType) const = 0; +}; + } Q_DECLARE_INTERFACE(KTextEditor::MarkInterface, "org.kde.KTextEditor.MarkInterface") +Q_DECLARE_INTERFACE(KTextEditor::MarkInterfaceV2, "org.kde.KTextEditor.MarkInterfaceV2") #endif diff --git a/src/search/katesearchbar.cpp b/src/search/katesearchbar.cpp index 8f891b30..0d43c45e 100644 --- a/src/search/katesearchbar.cpp +++ b/src/search/katesearchbar.cpp @@ -1,1686 +1,1686 @@ /* SPDX-License-Identifier: LGPL-2.0-or-later Copyright (C) 2009-2010 Bernhard Beschow Copyright (C) 2007 Sebastian Pipping Copyright (C) 2007 Matthew Woehlke Copyright (C) 2007 Thomas Friedrichsmeier This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "katesearchbar.h" #include "kateconfig.h" #include "katedocument.h" #include "kateglobal.h" #include "katematch.h" #include "kateregexp.h" #include "katerenderer.h" #include "kateundomanager.h" #include "kateview.h" #include #include #include #include "ui_searchbarincremental.h" #include "ui_searchbarpower.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // Turn debug messages on/off here // #define FAST_DEBUG_ENABLE #ifdef FAST_DEBUG_ENABLE #define FAST_DEBUG(x) qCDebug(LOG_KTE) << x #else #define FAST_DEBUG(x) #endif using namespace KTextEditor; namespace { class AddMenuManager { private: QVector m_insertBefore; QVector m_insertAfter; QSet m_actionPointers; uint m_indexWalker; QMenu *m_menu; public: AddMenuManager(QMenu *parent, int expectedItemCount) : m_insertBefore(QVector(expectedItemCount)) , m_insertAfter(QVector(expectedItemCount)) , m_indexWalker(0) , m_menu(nullptr) { Q_ASSERT(parent != nullptr); m_menu = parent->addMenu(i18n("Add...")); if (m_menu == nullptr) { return; } m_menu->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); } void enableMenu(bool enabled) { if (m_menu == nullptr) { return; } m_menu->setEnabled(enabled); } void addEntry(const QString &before, const QString &after, const QString &description, const QString &realBefore = QString(), const QString &realAfter = QString()) { if (m_menu == nullptr) { return; } QAction *const action = m_menu->addAction(before + after + QLatin1Char('\t') + description); m_insertBefore[m_indexWalker] = QString(realBefore.isEmpty() ? before : realBefore); m_insertAfter[m_indexWalker] = QString(realAfter.isEmpty() ? after : realAfter); action->setData(QVariant(m_indexWalker++)); m_actionPointers.insert(action); } void addSeparator() { if (m_menu == nullptr) { return; } m_menu->addSeparator(); } void handle(QAction *action, QLineEdit *lineEdit) { if (!m_actionPointers.contains(action)) { return; } const int cursorPos = lineEdit->cursorPosition(); const int index = action->data().toUInt(); const QString &before = m_insertBefore[index]; const QString &after = m_insertAfter[index]; lineEdit->insert(before + after); lineEdit->setCursorPosition(cursorPos + before.count()); lineEdit->setFocus(); } }; } // anon namespace KateSearchBar::KateSearchBar(bool initAsPower, KTextEditor::ViewPrivate *view, KateViewConfig *config) : KateViewBarWidget(true, view) , m_view(view) , m_config(config) , m_layout(new QVBoxLayout()) , m_widget(nullptr) , m_incUi(nullptr) , m_incInitCursor(view->cursorPosition()) , m_powerUi(nullptr) , highlightMatchAttribute(new Attribute()) , highlightReplacementAttribute(new Attribute()) , m_incHighlightAll(false) , m_incFromCursor(true) , m_incMatchCase(false) , m_powerMatchCase(true) , m_powerFromCursor(false) , m_powerHighlightAll(false) , m_powerMode(0) { connect(view, &KTextEditor::View::cursorPositionChanged, this, &KateSearchBar::updateIncInitCursor); connect(view, &KTextEditor::View::selectionChanged, this, &KateSearchBar::updateSelectionOnly); connect(this, &KateSearchBar::findOrReplaceAllFinished, this, &KateSearchBar::endFindOrReplaceAll); // init match attribute Attribute::Ptr mouseInAttribute(new Attribute()); mouseInAttribute->setFontBold(true); highlightMatchAttribute->setDynamicAttribute(Attribute::ActivateMouseIn, mouseInAttribute); Attribute::Ptr caretInAttribute(new Attribute()); caretInAttribute->setFontItalic(true); highlightMatchAttribute->setDynamicAttribute(Attribute::ActivateCaretIn, caretInAttribute); updateHighlightColors(); // Modify parent QWidget *const widget = centralWidget(); widget->setLayout(m_layout); m_layout->setContentsMargins(0, 0, 0, 0); // allow to have small size, for e.g. Kile setMinimumWidth(100); // Copy global to local config backup const auto searchFlags = m_config->searchFlags(); m_incHighlightAll = (searchFlags & KateViewConfig::IncHighlightAll) != 0; m_incFromCursor = (searchFlags & KateViewConfig::IncFromCursor) != 0; m_incMatchCase = (searchFlags & KateViewConfig::IncMatchCase) != 0; m_powerMatchCase = (searchFlags & KateViewConfig::PowerMatchCase) != 0; m_powerFromCursor = (searchFlags & KateViewConfig::PowerFromCursor) != 0; m_powerHighlightAll = (searchFlags & KateViewConfig::PowerHighlightAll) != 0; m_powerMode = ((searchFlags & KateViewConfig::PowerModeRegularExpression) != 0) ? MODE_REGEX : (((searchFlags & KateViewConfig::PowerModeEscapeSequences) != 0) ? MODE_ESCAPE_SEQUENCES : (((searchFlags & KateViewConfig::PowerModeWholeWords) != 0) ? MODE_WHOLE_WORDS : MODE_PLAIN_TEXT)); // Load one of either dialogs if (initAsPower) { enterPowerMode(); } else { enterIncrementalMode(); } updateSelectionOnly(); } KateSearchBar::~KateSearchBar() { if (!m_cancelFindOrReplace) { // Finish/Cancel the still running job to avoid a crash endFindOrReplaceAll(); } clearHighlights(); delete m_layout; delete m_widget; delete m_incUi; delete m_powerUi; } void KateSearchBar::closed() { // remove search from the view bar, because it vertically bloats up the // stacked layout in KateViewBar. if (viewBar()) { viewBar()->removeBarWidget(this); } clearHighlights(); } void KateSearchBar::setReplacementPattern(const QString &replacementPattern) { Q_ASSERT(isPower()); if (this->replacementPattern() == replacementPattern) { return; } m_powerUi->replacement->setEditText(replacementPattern); } QString KateSearchBar::replacementPattern() const { Q_ASSERT(isPower()); return m_powerUi->replacement->currentText(); } void KateSearchBar::setSearchMode(KateSearchBar::SearchMode mode) { Q_ASSERT(isPower()); m_powerUi->searchMode->setCurrentIndex(mode); } void KateSearchBar::findNext() { const bool found = find(); if (found) { QComboBox *combo = m_powerUi != nullptr ? m_powerUi->pattern : m_incUi->pattern; // Add to search history addCurrentTextToHistory(combo); } } void KateSearchBar::findPrevious() { const bool found = find(SearchBackward); if (found) { QComboBox *combo = m_powerUi != nullptr ? m_powerUi->pattern : m_incUi->pattern; // Add to search history addCurrentTextToHistory(combo); } } void KateSearchBar::showResultMessage() { QString text; if (m_replaceMode) { text = i18ncp("short translation", "1 replacement made", "%1 replacements made", m_matchCounter); } else { text = i18ncp("short translation", "1 match found", "%1 matches found", m_matchCounter); } if (m_infoMessage) { m_infoMessage->setText(text); } else { m_infoMessage = new KTextEditor::Message(text, KTextEditor::Message::Positive); m_infoMessage->setPosition(KTextEditor::Message::BottomInView); m_infoMessage->setAutoHide(3000); // 3 seconds m_infoMessage->setView(m_view); m_view->doc()->postMessage(m_infoMessage); } } void KateSearchBar::showSearchWrappedHint(SearchDirection searchDirection) { // show message widget when wrapping const QIcon icon = (searchDirection == SearchForward) ? QIcon::fromTheme(QStringLiteral("go-down-search")) : QIcon::fromTheme(QStringLiteral("go-up-search")); if (!m_wrappedMessage || m_lastSearchDirection != searchDirection) { m_lastSearchDirection = searchDirection; m_wrappedMessage = new KTextEditor::Message(i18n("Search wrapped"), KTextEditor::Message::Information); m_wrappedMessage->setIcon(icon); m_wrappedMessage->setPosition(KTextEditor::Message::BottomInView); m_wrappedMessage->setAutoHide(2000); m_wrappedMessage->setAutoHideMode(KTextEditor::Message::Immediate); m_wrappedMessage->setView(m_view); m_view->doc()->postMessage(m_wrappedMessage); } } void KateSearchBar::highlightMatch(const Range &range) { KTextEditor::MovingRange *const highlight = m_view->doc()->newMovingRange(range, Kate::TextRange::DoNotExpand); highlight->setView(m_view); // show only in this view highlight->setAttributeOnlyForViews(true); // use z depth defined in moving ranges interface highlight->setZDepth(-10000.0); highlight->setAttribute(highlightMatchAttribute); m_hlRanges.append(highlight); } void KateSearchBar::highlightReplacement(const Range &range) { KTextEditor::MovingRange *const highlight = m_view->doc()->newMovingRange(range, Kate::TextRange::DoNotExpand); highlight->setView(m_view); // show only in this view highlight->setAttributeOnlyForViews(true); // use z depth defined in moving ranges interface highlight->setZDepth(-10000.0); highlight->setAttribute(highlightReplacementAttribute); m_hlRanges.append(highlight); } void KateSearchBar::indicateMatch(MatchResult matchResult) { QLineEdit *const lineEdit = isPower() ? m_powerUi->pattern->lineEdit() : m_incUi->pattern->lineEdit(); QPalette background(lineEdit->palette()); switch (matchResult) { case MatchFound: // FALLTHROUGH case MatchWrappedForward: case MatchWrappedBackward: // Green background for line edit KColorScheme::adjustBackground(background, KColorScheme::PositiveBackground); break; case MatchMismatch: // Red background for line edit KColorScheme::adjustBackground(background, KColorScheme::NegativeBackground); break; case MatchNothing: // Reset background of line edit background = QPalette(); break; case MatchNeutral: KColorScheme::adjustBackground(background, KColorScheme::NeutralBackground); break; } // Update status label if (m_incUi != nullptr) { QPalette foreground(m_incUi->status->palette()); switch (matchResult) { case MatchFound: // FALLTHROUGH case MatchNothing: KColorScheme::adjustForeground(foreground, KColorScheme::NormalText, QPalette::WindowText, KColorScheme::Window); m_incUi->status->clear(); break; case MatchWrappedForward: case MatchWrappedBackward: KColorScheme::adjustForeground(foreground, KColorScheme::NormalText, QPalette::WindowText, KColorScheme::Window); if (matchResult == MatchWrappedBackward) { m_incUi->status->setText(i18n("Reached top, continued from bottom")); } else { m_incUi->status->setText(i18n("Reached bottom, continued from top")); } break; case MatchMismatch: KColorScheme::adjustForeground(foreground, KColorScheme::NegativeText, QPalette::WindowText, KColorScheme::Window); m_incUi->status->setText(i18n("Not found")); break; case MatchNeutral: /* do nothing */ break; } m_incUi->status->setPalette(foreground); } lineEdit->setPalette(background); } /*static*/ void KateSearchBar::selectRange(KTextEditor::ViewPrivate *view, const KTextEditor::Range &range) { view->setCursorPositionInternal(range.end()); view->setSelection(range); } void KateSearchBar::selectRange2(const KTextEditor::Range &range) { disconnect(m_view, &KTextEditor::View::selectionChanged, this, &KateSearchBar::updateSelectionOnly); selectRange(m_view, range); connect(m_view, &KTextEditor::View::selectionChanged, this, &KateSearchBar::updateSelectionOnly); } void KateSearchBar::onIncPatternChanged(const QString &pattern) { if (!m_incUi) { return; } // clear prior highlightings (deletes info message if present) clearHighlights(); m_incUi->next->setDisabled(pattern.isEmpty()); m_incUi->prev->setDisabled(pattern.isEmpty()); KateMatch match(m_view->doc(), searchOptions()); if (!pattern.isEmpty()) { // Find, first try const Range inputRange = KTextEditor::Range(m_incInitCursor, m_view->document()->documentEnd()); match.searchText(inputRange, pattern); } const bool wrap = !match.isValid() && !pattern.isEmpty(); if (wrap) { // Find, second try const KTextEditor::Range inputRange = m_view->document()->documentRange(); match.searchText(inputRange, pattern); } const MatchResult matchResult = match.isValid() ? (wrap ? MatchWrappedForward : MatchFound) : pattern.isEmpty() ? MatchNothing : MatchMismatch; const Range selectionRange = pattern.isEmpty() ? Range(m_incInitCursor, m_incInitCursor) : match.isValid() ? match.range() : Range::invalid(); // don't update m_incInitCursor when we move the cursor disconnect(m_view, &KTextEditor::View::cursorPositionChanged, this, &KateSearchBar::updateIncInitCursor); selectRange2(selectionRange); connect(m_view, &KTextEditor::View::cursorPositionChanged, this, &KateSearchBar::updateIncInitCursor); indicateMatch(matchResult); } void KateSearchBar::setMatchCase(bool matchCase) { if (this->matchCase() == matchCase) { return; } if (isPower()) { m_powerUi->matchCase->setChecked(matchCase); } else { m_incUi->matchCase->setChecked(matchCase); } } void KateSearchBar::onMatchCaseToggled(bool /*matchCase*/) { sendConfig(); if (m_incUi != nullptr) { // Re-search with new settings const QString pattern = m_incUi->pattern->currentText(); onIncPatternChanged(pattern); } else { indicateMatch(MatchNothing); } } bool KateSearchBar::matchCase() const { return isPower() ? m_powerUi->matchCase->isChecked() : m_incUi->matchCase->isChecked(); } void KateSearchBar::fixForSingleLine(Range &range, SearchDirection searchDirection) { FAST_DEBUG("Single-line workaround checking BEFORE" << range); if (searchDirection == SearchForward) { const int line = range.start().line(); const int col = range.start().column(); const int maxColWithNewline = m_view->document()->lineLength(line) + 1; if (col == maxColWithNewline) { FAST_DEBUG("Starting on a newline" << range); const int maxLine = m_view->document()->lines() - 1; if (line < maxLine) { range.setRange(Cursor(line + 1, 0), range.end()); FAST_DEBUG("Search range fixed to " << range); } else { FAST_DEBUG("Already at last line"); range = Range::invalid(); } } } else { const int col = range.end().column(); if (col == 0) { FAST_DEBUG("Ending after a newline" << range); const int line = range.end().line(); if (line > 0) { const int maxColWithNewline = m_view->document()->lineLength(line - 1); range.setRange(range.start(), Cursor(line - 1, maxColWithNewline)); FAST_DEBUG("Search range fixed to " << range); } else { FAST_DEBUG("Already at first line"); range = Range::invalid(); } } } FAST_DEBUG("Single-line workaround checking AFTER" << range); } void KateSearchBar::onReturnPressed() { const Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers(); const bool shiftDown = (modifiers & Qt::ShiftModifier) != 0; const bool controlDown = (modifiers & Qt::ControlModifier) != 0; if (shiftDown) { // Shift down, search backwards findPrevious(); } else { // Shift up, search forwards findNext(); } if (controlDown) { emit hideMe(); } } bool KateSearchBar::findOrReplace(SearchDirection searchDirection, const QString *replacement) { // What to find? if (searchPattern().isEmpty()) { return false; // == Pattern error } // don't let selectionChanged signal mess around in this routine disconnect(m_view, &KTextEditor::View::selectionChanged, this, &KateSearchBar::updateSelectionOnly); // clear previous highlights if there are any clearHighlights(); const SearchOptions enabledOptions = searchOptions(searchDirection); // Where to find? Range inputRange; const Range selection = m_view->selection() ? m_view->selectionRange() : Range::invalid(); if (selection.isValid()) { if (selectionOnly()) { // First match in selection inputRange = selection; } else { // Next match after/before selection if a match was selected before if (searchDirection == SearchForward) { inputRange.setRange(selection.start(), m_view->document()->documentEnd()); } else { inputRange.setRange(Cursor(0, 0), selection.end()); } } } else { // No selection const Cursor cursorPos = m_view->cursorPosition(); if (searchDirection == SearchForward) { inputRange.setRange(cursorPos, m_view->document()->documentEnd()); } else { inputRange.setRange(Cursor(0, 0), cursorPos); } } FAST_DEBUG("Search range is" << inputRange); { const bool regexMode = enabledOptions.testFlag(Regex); const bool multiLinePattern = regexMode ? KateRegExp(searchPattern()).isMultiLine() : false; // Single-line pattern workaround if (regexMode && !multiLinePattern) { fixForSingleLine(inputRange, searchDirection); } } KateMatch match(m_view->doc(), enabledOptions); Range afterReplace = Range::invalid(); // Find, first try match.searchText(inputRange, searchPattern()); if (match.isValid() && match.range() == selection) { // Same match again if (replacement != nullptr) { // Selection is match -> replace KTextEditor::MovingRange *smartInputRange = m_view->doc()->newMovingRange(inputRange, KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight); afterReplace = match.replace(*replacement, m_view->blockSelection()); inputRange = *smartInputRange; delete smartInputRange; } if (!selectionOnly()) { // Find, second try after old selection if (searchDirection == SearchForward) { const Cursor start = (replacement != nullptr) ? afterReplace.end() : selection.end(); inputRange.setRange(start, inputRange.end()); } else { const Cursor end = (replacement != nullptr) ? afterReplace.start() : selection.start(); inputRange.setRange(inputRange.start(), end); } } // Single-line pattern workaround fixForSingleLine(inputRange, searchDirection); match.searchText(inputRange, searchPattern()); } bool askWrap = !match.isValid() && (!selection.isValid() || !selectionOnly()); bool wrap = false; if (askWrap) { askWrap = false; wrap = true; } if (askWrap) { QString question = searchDirection == SearchForward ? i18n("Bottom of file reached. Continue from top?") : i18n("Top of file reached. Continue from bottom?"); wrap = (KMessageBox::questionYesNo(nullptr, question, i18n("Continue search?"), KStandardGuiItem::yes(), KStandardGuiItem::no(), QStringLiteral("DoNotShowAgainContinueSearchDialog")) == KMessageBox::Yes); } if (wrap) { showSearchWrappedHint(searchDirection); inputRange = m_view->document()->documentRange(); match.searchText(inputRange, searchPattern()); } if (match.isValid()) { selectRange2(match.range()); } const MatchResult matchResult = !match.isValid() ? MatchMismatch : !wrap ? MatchFound : searchDirection == SearchForward ? MatchWrappedForward : MatchWrappedBackward; indicateMatch(matchResult); // highlight replacements if applicable if (afterReplace.isValid()) { highlightReplacement(afterReplace); } // restore connection connect(m_view, &KTextEditor::View::selectionChanged, this, &KateSearchBar::updateSelectionOnly); return true; // == No pattern error } void KateSearchBar::findAll() { // clear highlightings of prior search&replace action clearHighlights(); Range inputRange = (m_view->selection() && selectionOnly()) ? m_view->selectionRange() : m_view->document()->documentRange(); beginFindAll(inputRange); } void KateSearchBar::onPowerPatternChanged(const QString & /*pattern*/) { givePatternFeedback(); indicateMatch(MatchNothing); } bool KateSearchBar::isPatternValid() const { if (searchPattern().isEmpty()) { return false; } return searchOptions().testFlag(WholeWords) ? searchPattern().trimmed() == searchPattern() : searchOptions().testFlag(Regex) ? QRegularExpression(searchPattern()).isValid() : true; } void KateSearchBar::givePatternFeedback() { // Enable/disable next/prev and replace next/all m_powerUi->findNext->setEnabled(isPatternValid()); m_powerUi->findPrev->setEnabled(isPatternValid()); m_powerUi->replaceNext->setEnabled(isPatternValid()); m_powerUi->replaceAll->setEnabled(isPatternValid()); m_powerUi->findAll->setEnabled(isPatternValid()); } void KateSearchBar::addCurrentTextToHistory(QComboBox *combo) { const QString text = combo->currentText(); const int index = combo->findText(text); if (index > 0) { combo->removeItem(index); } if (index != 0) { combo->insertItem(0, text); combo->setCurrentIndex(0); } // sync to application config KTextEditor::EditorPrivate::self()->saveSearchReplaceHistoryModels(); } void KateSearchBar::backupConfig(bool ofPower) { if (ofPower) { m_powerMatchCase = m_powerUi->matchCase->isChecked(); m_powerMode = m_powerUi->searchMode->currentIndex(); } else { m_incMatchCase = m_incUi->matchCase->isChecked(); } } void KateSearchBar::sendConfig() { const auto pastFlags = m_config->searchFlags(); auto futureFlags = pastFlags; if (m_powerUi != nullptr) { const bool OF_POWER = true; backupConfig(OF_POWER); // Update power search flags only const auto incFlagsOnly = pastFlags & (KateViewConfig::IncHighlightAll | KateViewConfig::IncFromCursor | KateViewConfig::IncMatchCase); futureFlags = incFlagsOnly | (m_powerMatchCase ? KateViewConfig::PowerMatchCase : 0) | (m_powerFromCursor ? KateViewConfig::PowerFromCursor : 0) | (m_powerHighlightAll ? KateViewConfig::PowerHighlightAll : 0) | ((m_powerMode == MODE_REGEX) ? KateViewConfig::PowerModeRegularExpression : ((m_powerMode == MODE_ESCAPE_SEQUENCES) ? KateViewConfig::PowerModeEscapeSequences : ((m_powerMode == MODE_WHOLE_WORDS) ? KateViewConfig::PowerModeWholeWords : KateViewConfig::PowerModePlainText))); } else if (m_incUi != nullptr) { const bool OF_INCREMENTAL = false; backupConfig(OF_INCREMENTAL); // Update incremental search flags only const auto powerFlagsOnly = pastFlags & (KateViewConfig::PowerMatchCase | KateViewConfig::PowerFromCursor | KateViewConfig::PowerHighlightAll | KateViewConfig::PowerModeRegularExpression | KateViewConfig::PowerModeEscapeSequences | KateViewConfig::PowerModeWholeWords | KateViewConfig::PowerModePlainText); futureFlags = powerFlagsOnly | (m_incHighlightAll ? KateViewConfig::IncHighlightAll : 0) | (m_incFromCursor ? KateViewConfig::IncFromCursor : 0) | (m_incMatchCase ? KateViewConfig::IncMatchCase : 0); } // Adjust global config m_config->setSearchFlags(futureFlags); } void KateSearchBar::replaceNext() { const QString replacement = m_powerUi->replacement->currentText(); if (findOrReplace(SearchForward, &replacement)) { // Never merge replace actions with other replace actions/user actions m_view->doc()->undoManager()->undoSafePoint(); // Add to search history addCurrentTextToHistory(m_powerUi->pattern); // Add to replace history addCurrentTextToHistory(m_powerUi->replacement); } } // replacement == NULL --> Only highlight all matches // replacement != NULL --> Replace and highlight all matches void KateSearchBar::beginFindOrReplaceAll(Range inputRange, const QString &replacement, bool replaceMode /* = true*/) { // don't let selectionChanged signal mess around in this routine disconnect(m_view, &KTextEditor::View::selectionChanged, this, &KateSearchBar::updateSelectionOnly); // Cancel job when user close the document to avoid crash connect(m_view->doc(), &KTextEditor::Document::aboutToClose, this, &KateSearchBar::endFindOrReplaceAll); if (m_powerUi) { // Offer Cancel button and disable not useful buttons m_powerUi->searchCancelStacked->setCurrentIndex(m_powerUi->searchCancelStacked->indexOf(m_powerUi->cancelPage)); m_powerUi->findNext->setEnabled(false); m_powerUi->findPrev->setEnabled(false); m_powerUi->replaceNext->setEnabled(false); } m_highlightRanges.clear(); m_inputRange = inputRange; m_workingRange = m_view->doc()->newMovingRange(m_inputRange); m_replacement = replacement; m_replaceMode = replaceMode; m_matchCounter = 0; m_cancelFindOrReplace = false; // Ensure we have a GO! findOrReplaceAll(); } void KateSearchBar::findOrReplaceAll() { const SearchOptions enabledOptions = searchOptions(SearchForward); const bool regexMode = enabledOptions.testFlag(Regex); const bool multiLinePattern = regexMode ? KateRegExp(searchPattern()).isMultiLine() : false; // we highlight all ranges of a replace, up to some hard limit // e.g. if you replace 100000 things, rendering will break down otherwise ;=) const int maxHighlightings = 65536; // reuse match object to avoid massive moving range creation KateMatch match(m_view->doc(), enabledOptions); bool block = m_view->selection() && m_view->blockSelection(); int line = m_inputRange.start().line(); QElapsedTimer rolex; // Watchog to suspend the work after some time rolex.start(); bool timeOut = false; bool done = false; do { if (block) { delete m_workingRange; // Never forget that! m_workingRange = m_view->doc()->newMovingRange(m_view->doc()->rangeOnLine(m_inputRange, line)); } do { match.searchText(*m_workingRange, searchPattern()); if (!match.isValid()) { done = true; break; } bool const originalMatchEmpty = match.isEmpty(); // Work with the match Range lastRange; if (m_replaceMode) { if (m_matchCounter == 0) { static_cast(m_view->document())->startEditing(); } // Replace lastRange = match.replace(m_replacement, false, ++m_matchCounter); } else { lastRange = match.range(); ++m_matchCounter; } // remember ranges if limit not reached if (m_matchCounter < maxHighlightings) { m_highlightRanges.push_back(lastRange); } else { m_highlightRanges.clear(); // TODO Info user that highlighting is disabled } // Continue after match if (lastRange.end() >= m_workingRange->end()) { done = true; break; } KTextEditor::DocumentCursor workingStart(m_view->doc(), lastRange.end()); if (originalMatchEmpty) { // Can happen for regex patterns like "^". // If we don't advance here we will loop forever... workingStart.move(1); } else if (regexMode && !multiLinePattern && workingStart.atEndOfLine()) { // single-line regexps might match the naked line end // therefore we better advance to the next line workingStart.move(1); } m_workingRange->setRange(workingStart.toCursor(), m_workingRange->end()); // Are we done? if (!m_workingRange->toRange().isValid() || workingStart.atEndOfDocument()) { done = true; break; } timeOut = rolex.elapsed() > 150; } while (!m_cancelFindOrReplace && !timeOut); } while (!m_cancelFindOrReplace && !timeOut && block && ++line <= m_inputRange.end().line()); if (done || m_cancelFindOrReplace) { emit findOrReplaceAllFinished(); } else if (timeOut) { QTimer::singleShot(0, this, &KateSearchBar::findOrReplaceAll); } showResultMessage(); } void KateSearchBar::endFindOrReplaceAll() { // Don't forget to remove our "crash protector" disconnect(m_view->doc(), &KTextEditor::Document::aboutToClose, this, &KateSearchBar::endFindOrReplaceAll); // After last match if (m_matchCounter > 0) { if (m_replaceMode) { static_cast(m_view->document())->finishEditing(); } } // Add ScrollBarMarks if (!m_highlightRanges.empty()) { - KTextEditor::MarkInterface *iface = qobject_cast(m_view->document()); + KTextEditor::MarkInterfaceV2 *iface = qobject_cast(m_view->document()); if (iface) { iface->setMarkDescription(KTextEditor::MarkInterface::SearchMatch, i18n("SearchHighLight")); - iface->setMarkPixmap(KTextEditor::MarkInterface::SearchMatch, QIcon().pixmap(0, 0)); + iface->setMarkIcon(KTextEditor::MarkInterface::SearchMatch, QIcon()); for (const Range &r : m_highlightRanges) { iface->addMark(r.start().line(), KTextEditor::MarkInterface::SearchMatch); } } } // Add highlights if (m_replaceMode) { for (const Range &r : qAsConst(m_highlightRanges)) { highlightReplacement(r); } // Never merge replace actions with other replace actions/user actions m_view->doc()->undoManager()->undoSafePoint(); } else { for (const Range &r : qAsConst(m_highlightRanges)) { highlightMatch(r); } // indicateMatch(m_matchCounter > 0 ? MatchFound : MatchMismatch); TODO } // Clean-Up the still hold MovingRange delete m_workingRange; // restore connection connect(m_view, &KTextEditor::View::selectionChanged, this, &KateSearchBar::updateSelectionOnly); if (m_powerUi) { // Offer Find and Replace buttons and enable again useful buttons m_powerUi->searchCancelStacked->setCurrentIndex(m_powerUi->searchCancelStacked->indexOf(m_powerUi->searchPage)); m_powerUi->findNext->setEnabled(true); m_powerUi->findPrev->setEnabled(true); m_powerUi->replaceNext->setEnabled(true); // Add to search history addCurrentTextToHistory(m_powerUi->pattern); // Add to replace history addCurrentTextToHistory(m_powerUi->replacement); } m_cancelFindOrReplace = true; // Indicate we are not running } void KateSearchBar::replaceAll() { // clear prior highlightings (deletes info message if present) clearHighlights(); // What to find/replace? const QString replacement = m_powerUi->replacement->currentText(); // Where to replace? const bool selected = m_view->selection(); Range inputRange = (selected && selectionOnly()) ? m_view->selectionRange() : m_view->document()->documentRange(); beginFindOrReplaceAll(inputRange, replacement); } void KateSearchBar::setSearchPattern(const QString &searchPattern) { if (searchPattern == this->searchPattern()) { return; } if (isPower()) { m_powerUi->pattern->setEditText(searchPattern); } else { m_incUi->pattern->setEditText(searchPattern); } } QString KateSearchBar::searchPattern() const { return (m_powerUi != nullptr) ? m_powerUi->pattern->currentText() : m_incUi->pattern->currentText(); } void KateSearchBar::setSelectionOnly(bool selectionOnly) { if (this->selectionOnly() == selectionOnly) { return; } if (isPower()) { m_powerUi->selectionOnly->setChecked(selectionOnly); } } bool KateSearchBar::selectionOnly() const { return isPower() ? m_powerUi->selectionOnly->isChecked() : false; } KTextEditor::SearchOptions KateSearchBar::searchOptions(SearchDirection searchDirection) const { SearchOptions enabledOptions = KTextEditor::Default; if (!matchCase()) { enabledOptions |= CaseInsensitive; } if (searchDirection == SearchBackward) { enabledOptions |= Backwards; } if (m_powerUi != nullptr) { switch (m_powerUi->searchMode->currentIndex()) { case MODE_WHOLE_WORDS: enabledOptions |= WholeWords; break; case MODE_ESCAPE_SEQUENCES: enabledOptions |= EscapeSequences; break; case MODE_REGEX: enabledOptions |= Regex; break; case MODE_PLAIN_TEXT: // FALLTHROUGH default: break; } } return enabledOptions; } struct ParInfo { int openIndex; bool capturing; int captureNumber; // 1..9 }; QVector KateSearchBar::getCapturePatterns(const QString &pattern) const { QVector capturePatterns; capturePatterns.reserve(9); QStack parInfos; const int inputLen = pattern.length(); int input = 0; // walker index bool insideClass = false; int captureCount = 0; while (input < inputLen) { if (insideClass) { // Wait for closing, unescaped ']' if (pattern[input].unicode() == L']') { insideClass = false; } input++; } else { switch (pattern[input].unicode()) { case L'\\': // Skip this and any next character input += 2; break; case L'(': ParInfo curInfo; curInfo.openIndex = input; curInfo.capturing = (input + 1 >= inputLen) || (pattern[input + 1].unicode() != '?'); if (curInfo.capturing) { captureCount++; } curInfo.captureNumber = captureCount; parInfos.push(curInfo); input++; break; case L')': if (!parInfos.empty()) { ParInfo &top = parInfos.top(); if (top.capturing && (top.captureNumber <= 9)) { const int start = top.openIndex + 1; const int len = input - start; if (capturePatterns.size() < top.captureNumber) { capturePatterns.resize(top.captureNumber); } capturePatterns[top.captureNumber - 1] = pattern.mid(start, len); } parInfos.pop(); } input++; break; case L'[': input++; insideClass = true; break; default: input++; break; } } } return capturePatterns; } void KateSearchBar::showExtendedContextMenu(bool forPattern, const QPoint &pos) { // Make original menu QComboBox *comboBox = forPattern ? m_powerUi->pattern : m_powerUi->replacement; QMenu *const contextMenu = comboBox->lineEdit()->createStandardContextMenu(); if (contextMenu == nullptr) { return; } bool extendMenu = false; bool regexMode = false; switch (m_powerUi->searchMode->currentIndex()) { case MODE_REGEX: regexMode = true; // FALLTHROUGH case MODE_ESCAPE_SEQUENCES: extendMenu = true; break; default: break; } AddMenuManager addMenuManager(contextMenu, 37); if (!extendMenu) { addMenuManager.enableMenu(extendMenu); } else { // Build menu if (forPattern) { if (regexMode) { addMenuManager.addEntry(QStringLiteral("^"), QString(), i18n("Beginning of line")); addMenuManager.addEntry(QStringLiteral("$"), QString(), i18n("End of line")); addMenuManager.addSeparator(); addMenuManager.addEntry(QStringLiteral("."), QString(), i18n("Any single character (excluding line breaks)")); addMenuManager.addSeparator(); addMenuManager.addEntry(QStringLiteral("+"), QString(), i18n("One or more occurrences")); addMenuManager.addEntry(QStringLiteral("*"), QString(), i18n("Zero or more occurrences")); addMenuManager.addEntry(QStringLiteral("?"), QString(), i18n("Zero or one occurrences")); addMenuManager.addEntry(QStringLiteral("{a"), QStringLiteral(",b}"), i18n(" through occurrences"), QStringLiteral("{"), QStringLiteral(",}")); addMenuManager.addSeparator(); addMenuManager.addEntry(QStringLiteral("("), QStringLiteral(")"), i18n("Group, capturing")); addMenuManager.addEntry(QStringLiteral("|"), QString(), i18n("Or")); addMenuManager.addEntry(QStringLiteral("["), QStringLiteral("]"), i18n("Set of characters")); addMenuManager.addEntry(QStringLiteral("[^"), QStringLiteral("]"), i18n("Negative set of characters")); addMenuManager.addSeparator(); } } else { addMenuManager.addEntry(QStringLiteral("\\0"), QString(), i18n("Whole match reference")); addMenuManager.addSeparator(); if (regexMode) { const QString pattern = m_powerUi->pattern->currentText(); const QVector capturePatterns = getCapturePatterns(pattern); const int captureCount = capturePatterns.count(); for (int i = 1; i <= 9; i++) { const QString number = QString::number(i); const QString &captureDetails = (i <= captureCount) ? QLatin1String(" = (") + capturePatterns[i - 1].leftRef(30) + QLatin1Char(')') : QString(); addMenuManager.addEntry(QLatin1String("\\") + number, QString(), i18n("Reference") + QLatin1Char(' ') + number + captureDetails); } addMenuManager.addSeparator(); } } addMenuManager.addEntry(QStringLiteral("\\n"), QString(), i18n("Line break")); addMenuManager.addEntry(QStringLiteral("\\t"), QString(), i18n("Tab")); if (forPattern && regexMode) { addMenuManager.addEntry(QStringLiteral("\\b"), QString(), i18n("Word boundary")); addMenuManager.addEntry(QStringLiteral("\\B"), QString(), i18n("Not word boundary")); addMenuManager.addEntry(QStringLiteral("\\d"), QString(), i18n("Digit")); addMenuManager.addEntry(QStringLiteral("\\D"), QString(), i18n("Non-digit")); addMenuManager.addEntry(QStringLiteral("\\s"), QString(), i18n("Whitespace (excluding line breaks)")); addMenuManager.addEntry(QStringLiteral("\\S"), QString(), i18n("Non-whitespace (excluding line breaks)")); addMenuManager.addEntry(QStringLiteral("\\w"), QString(), i18n("Word character (alphanumerics plus '_')")); addMenuManager.addEntry(QStringLiteral("\\W"), QString(), i18n("Non-word character")); } addMenuManager.addEntry(QStringLiteral("\\0???"), QString(), i18n("Octal character 000 to 377 (2^8-1)"), QStringLiteral("\\0")); addMenuManager.addEntry(QStringLiteral("\\x????"), QString(), i18n("Hex character 0000 to FFFF (2^16-1)"), QStringLiteral("\\x")); addMenuManager.addEntry(QStringLiteral("\\\\"), QString(), i18n("Backslash")); if (forPattern && regexMode) { addMenuManager.addSeparator(); addMenuManager.addEntry(QStringLiteral("(?:E"), QStringLiteral(")"), i18n("Group, non-capturing"), QStringLiteral("(?:")); addMenuManager.addEntry(QStringLiteral("(?=E"), QStringLiteral(")"), i18n("Lookahead"), QStringLiteral("(?=")); addMenuManager.addEntry(QStringLiteral("(?!E"), QStringLiteral(")"), i18n("Negative lookahead"), QStringLiteral("(?!")); } if (!forPattern) { addMenuManager.addSeparator(); addMenuManager.addEntry(QStringLiteral("\\L"), QString(), i18n("Begin lowercase conversion")); addMenuManager.addEntry(QStringLiteral("\\U"), QString(), i18n("Begin uppercase conversion")); addMenuManager.addEntry(QStringLiteral("\\E"), QString(), i18n("End case conversion")); addMenuManager.addEntry(QStringLiteral("\\l"), QString(), i18n("Lowercase first character conversion")); addMenuManager.addEntry(QStringLiteral("\\u"), QString(), i18n("Uppercase first character conversion")); addMenuManager.addEntry(QStringLiteral("\\#[#..]"), QString(), i18n("Replacement counter (for Replace All)"), QStringLiteral("\\#")); } } // Show menu QAction *const result = contextMenu->exec(comboBox->mapToGlobal(pos)); if (result != nullptr) { addMenuManager.handle(result, comboBox->lineEdit()); } } void KateSearchBar::onPowerModeChanged(int /*index*/) { if (m_powerUi->searchMode->currentIndex() == MODE_REGEX) { m_powerUi->matchCase->setChecked(true); } sendConfig(); indicateMatch(MatchNothing); givePatternFeedback(); } void KateSearchBar::nextMatchForSelection(KTextEditor::ViewPrivate *view, SearchDirection searchDirection) { const bool selected = view->selection(); if (selected) { const QString pattern = view->selectionText(); // How to find? SearchOptions enabledOptions(KTextEditor::Default); if (searchDirection == SearchBackward) { enabledOptions |= Backwards; } // Where to find? const Range selRange = view->selectionRange(); Range inputRange; if (searchDirection == SearchForward) { inputRange.setRange(selRange.end(), view->doc()->documentEnd()); } else { inputRange.setRange(Cursor(0, 0), selRange.start()); } // Find, first try KateMatch match(view->doc(), enabledOptions); match.searchText(inputRange, pattern); if (match.isValid()) { selectRange(view, match.range()); } else { // Find, second try showSearchWrappedHint(searchDirection); if (searchDirection == SearchForward) { inputRange.setRange(Cursor(0, 0), selRange.start()); } else { inputRange.setRange(selRange.end(), view->doc()->documentEnd()); } KateMatch match2(view->doc(), enabledOptions); match2.searchText(inputRange, pattern); if (match2.isValid()) { selectRange(view, match2.range()); } } } else { // Select current word so we can search for that the next time const Cursor cursorPos = view->cursorPosition(); KTextEditor::Range wordRange = view->document()->wordRangeAt(cursorPos); if (wordRange.isValid()) { selectRange(view, wordRange); } } } void KateSearchBar::enterPowerMode() { QString initialPattern; bool selectionOnly = false; // Guess settings from context: init pattern with current selection const bool selected = m_view->selection(); if (selected) { const Range &selection = m_view->selectionRange(); if (selection.onSingleLine()) { // ... with current selection initialPattern = m_view->selectionText(); } else { // Enable selection only selectionOnly = true; } } // If there's no new selection, we'll use the existing pattern if (initialPattern.isNull()) { // Coming from power search? const bool fromReplace = (m_powerUi != nullptr) && (m_widget->isVisible()); if (fromReplace) { QLineEdit *const patternLineEdit = m_powerUi->pattern->lineEdit(); Q_ASSERT(patternLineEdit != nullptr); patternLineEdit->selectAll(); m_powerUi->pattern->setFocus(Qt::MouseFocusReason); return; } // Coming from incremental search? const bool fromIncremental = (m_incUi != nullptr) && (m_widget->isVisible()); if (fromIncremental) { initialPattern = m_incUi->pattern->currentText(); } } // Create dialog const bool create = (m_powerUi == nullptr); if (create) { // Kill incremental widget if (m_incUi != nullptr) { // Backup current settings const bool OF_INCREMENTAL = false; backupConfig(OF_INCREMENTAL); // Kill widget delete m_incUi; m_incUi = nullptr; m_layout->removeWidget(m_widget); m_widget->deleteLater(); // I didn't get a crash here but for symmetrie to the other mutate slot^ } // Add power widget m_widget = new QWidget(this); m_powerUi = new Ui::PowerSearchBar; m_powerUi->setupUi(m_widget); m_layout->addWidget(m_widget); // Bind to shared history models m_powerUi->pattern->setDuplicatesEnabled(false); m_powerUi->pattern->setInsertPolicy(QComboBox::InsertAtTop); m_powerUi->pattern->setMaxCount(m_config->maxHistorySize()); m_powerUi->pattern->setModel(KTextEditor::EditorPrivate::self()->searchHistoryModel()); m_powerUi->pattern->lineEdit()->setClearButtonEnabled(true); m_powerUi->pattern->setCompleter(nullptr); m_powerUi->replacement->setDuplicatesEnabled(false); m_powerUi->replacement->setInsertPolicy(QComboBox::InsertAtTop); m_powerUi->replacement->setMaxCount(m_config->maxHistorySize()); m_powerUi->replacement->setModel(KTextEditor::EditorPrivate::self()->replaceHistoryModel()); m_powerUi->replacement->lineEdit()->setClearButtonEnabled(true); m_powerUi->replacement->setCompleter(nullptr); // Icons // Gnome does not seem to have all icons we want, so we use fall-back icons for those that are missing. QIcon mutateIcon = QIcon::fromTheme(QStringLiteral("games-config-options"), QIcon::fromTheme(QStringLiteral("preferences-system"))); QIcon matchCaseIcon = QIcon::fromTheme(QStringLiteral("format-text-superscript"), QIcon::fromTheme(QStringLiteral("format-text-bold"))); m_powerUi->mutate->setIcon(mutateIcon); m_powerUi->mutate->setChecked(true); m_powerUi->findNext->setIcon(QIcon::fromTheme(QStringLiteral("go-down-search"))); m_powerUi->findPrev->setIcon(QIcon::fromTheme(QStringLiteral("go-up-search"))); m_powerUi->findAll->setIcon(QIcon::fromTheme(QStringLiteral("edit-find"))); m_powerUi->matchCase->setIcon(matchCaseIcon); m_powerUi->selectionOnly->setIcon(QIcon::fromTheme(QStringLiteral("edit-select-all"))); // Focus proxy centralWidget()->setFocusProxy(m_powerUi->pattern); } m_powerUi->selectionOnly->setChecked(selectionOnly); // Restore previous settings if (create) { m_powerUi->matchCase->setChecked(m_powerMatchCase); m_powerUi->searchMode->setCurrentIndex(m_powerMode); } // force current index of -1 --> shows 1st completion entry instead of 2nd m_powerUi->pattern->setCurrentIndex(-1); m_powerUi->replacement->setCurrentIndex(-1); // Set initial search pattern QLineEdit *const patternLineEdit = m_powerUi->pattern->lineEdit(); Q_ASSERT(patternLineEdit != nullptr); patternLineEdit->setText(initialPattern); patternLineEdit->selectAll(); // Set initial replacement text QLineEdit *const replacementLineEdit = m_powerUi->replacement->lineEdit(); Q_ASSERT(replacementLineEdit != nullptr); replacementLineEdit->setText(QString()); // Propagate settings (slots are still inactive on purpose) onPowerPatternChanged(initialPattern); givePatternFeedback(); if (create) { // Slots connect(m_powerUi->mutate, SIGNAL(clicked()), this, SLOT(enterIncrementalMode())); connect(patternLineEdit, SIGNAL(textChanged(QString)), this, SLOT(onPowerPatternChanged(QString))); connect(m_powerUi->findNext, SIGNAL(clicked()), this, SLOT(findNext())); connect(m_powerUi->findPrev, SIGNAL(clicked()), this, SLOT(findPrevious())); connect(m_powerUi->replaceNext, SIGNAL(clicked()), this, SLOT(replaceNext())); connect(m_powerUi->replaceAll, SIGNAL(clicked()), this, SLOT(replaceAll())); connect(m_powerUi->searchMode, SIGNAL(currentIndexChanged(int)), this, SLOT(onPowerModeChanged(int))); connect(m_powerUi->matchCase, SIGNAL(toggled(bool)), this, SLOT(onMatchCaseToggled(bool))); connect(m_powerUi->findAll, SIGNAL(clicked()), this, SLOT(findAll())); connect(m_powerUi->cancel, &QPushButton::clicked, this, &KateSearchBar::onPowerCancelFindOrReplace); // Make [return] in pattern line edit trigger action connect(patternLineEdit, SIGNAL(returnPressed()), this, SLOT(onReturnPressed())); connect(replacementLineEdit, SIGNAL(returnPressed()), this, SLOT(replaceNext())); // Hook into line edit context menus m_powerUi->pattern->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_powerUi->pattern, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onPowerPatternContextMenuRequest(QPoint))); m_powerUi->replacement->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_powerUi->replacement, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onPowerReplacmentContextMenuRequest(QPoint))); } // Focus if (m_widget->isVisible()) { m_powerUi->pattern->setFocus(Qt::MouseFocusReason); } } void KateSearchBar::enterIncrementalMode() { QString initialPattern; // Guess settings from context: init pattern with current selection const bool selected = m_view->selection(); if (selected) { const Range &selection = m_view->selectionRange(); if (selection.onSingleLine()) { // ... with current selection initialPattern = m_view->selectionText(); } } // If there's no new selection, we'll use the existing pattern if (initialPattern.isNull()) { // Coming from incremental search? const bool fromIncremental = (m_incUi != nullptr) && (m_widget->isVisible()); if (fromIncremental) { m_incUi->pattern->lineEdit()->selectAll(); m_incUi->pattern->setFocus(Qt::MouseFocusReason); return; } // Coming from power search? const bool fromReplace = (m_powerUi != nullptr) && (m_widget->isVisible()); if (fromReplace) { initialPattern = m_powerUi->pattern->currentText(); } } // Still no search pattern? Use the word under the cursor if (initialPattern.isNull()) { const KTextEditor::Cursor cursorPosition = m_view->cursorPosition(); initialPattern = m_view->doc()->wordAt(cursorPosition); } // Create dialog const bool create = (m_incUi == nullptr); if (create) { // Kill power widget if (m_powerUi != nullptr) { // Backup current settings const bool OF_POWER = true; backupConfig(OF_POWER); // Kill widget delete m_powerUi; m_powerUi = nullptr; m_layout->removeWidget(m_widget); m_widget->deleteLater(); // deleteLater, because it's not a good idea too delete the widget and there for the button triggering this slot } // Add incremental widget m_widget = new QWidget(this); m_incUi = new Ui::IncrementalSearchBar; m_incUi->setupUi(m_widget); m_layout->addWidget(m_widget); // new QShortcut(KStandardShortcut::paste().primary(), m_incUi->pattern, SLOT(paste()), 0, Qt::WidgetWithChildrenShortcut); // if (!KStandardShortcut::paste().alternate().isEmpty()) // new QShortcut(KStandardShortcut::paste().alternate(), m_incUi->pattern, SLOT(paste()), 0, Qt::WidgetWithChildrenShortcut); // Icons // Gnome does not seem to have all icons we want, so we use fall-back icons for those that are missing. QIcon mutateIcon = QIcon::fromTheme(QStringLiteral("games-config-options"), QIcon::fromTheme(QStringLiteral("preferences-system"))); QIcon matchCaseIcon = QIcon::fromTheme(QStringLiteral("format-text-superscript"), QIcon::fromTheme(QStringLiteral("format-text-bold"))); m_incUi->mutate->setIcon(mutateIcon); m_incUi->next->setIcon(QIcon::fromTheme(QStringLiteral("go-down-search"))); m_incUi->prev->setIcon(QIcon::fromTheme(QStringLiteral("go-up-search"))); m_incUi->matchCase->setIcon(matchCaseIcon); // Ensure minimum size m_incUi->pattern->setMinimumWidth(12 * m_incUi->pattern->fontMetrics().height()); // Customize status area m_incUi->status->setTextElideMode(Qt::ElideLeft); // Focus proxy centralWidget()->setFocusProxy(m_incUi->pattern); m_incUi->pattern->setDuplicatesEnabled(false); m_incUi->pattern->setInsertPolicy(QComboBox::InsertAtTop); m_incUi->pattern->setMaxCount(m_config->maxHistorySize()); m_incUi->pattern->setModel(KTextEditor::EditorPrivate::self()->searchHistoryModel()); m_incUi->pattern->lineEdit()->setClearButtonEnabled(true); m_incUi->pattern->setCompleter(nullptr); } // Restore previous settings if (create) { m_incUi->matchCase->setChecked(m_incMatchCase); } // force current index of -1 --> shows 1st completion entry instead of 2nd m_incUi->pattern->setCurrentIndex(-1); // Set initial search pattern if (!create) { disconnect(m_incUi->pattern, SIGNAL(editTextChanged(QString)), this, SLOT(onIncPatternChanged(QString))); } m_incUi->pattern->setEditText(initialPattern); connect(m_incUi->pattern, SIGNAL(editTextChanged(QString)), this, SLOT(onIncPatternChanged(QString))); m_incUi->pattern->lineEdit()->selectAll(); // Propagate settings (slots are still inactive on purpose) if (initialPattern.isEmpty()) { // Reset edit color indicateMatch(MatchNothing); } // Enable/disable next/prev m_incUi->next->setDisabled(initialPattern.isEmpty()); m_incUi->prev->setDisabled(initialPattern.isEmpty()); if (create) { // Slots connect(m_incUi->mutate, SIGNAL(clicked()), this, SLOT(enterPowerMode())); connect(m_incUi->pattern->lineEdit(), SIGNAL(returnPressed()), this, SLOT(onReturnPressed())); connect(m_incUi->next, SIGNAL(clicked()), this, SLOT(findNext())); connect(m_incUi->prev, SIGNAL(clicked()), this, SLOT(findPrevious())); connect(m_incUi->matchCase, SIGNAL(toggled(bool)), this, SLOT(onMatchCaseToggled(bool))); } // Focus if (m_widget->isVisible()) { m_incUi->pattern->setFocus(Qt::MouseFocusReason); } } bool KateSearchBar::clearHighlights() { // Remove ScrollBarMarks KTextEditor::MarkInterface *iface = qobject_cast(m_view->document()); if (iface) { const QHash marks = iface->marks(); QHashIterator i(marks); while (i.hasNext()) { i.next(); if (i.value()->type & KTextEditor::MarkInterface::SearchMatch) { iface->removeMark(i.value()->line, KTextEditor::MarkInterface::SearchMatch); } } } if (m_infoMessage) { delete m_infoMessage; } if (m_hlRanges.isEmpty()) { return false; } qDeleteAll(m_hlRanges); m_hlRanges.clear(); return true; } void KateSearchBar::updateHighlightColors() { const QColor foregroundColor = m_view->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color(); const QColor &searchColor = m_view->renderer()->config()->searchHighlightColor(); const QColor &replaceColor = m_view->renderer()->config()->replaceHighlightColor(); // init match attribute highlightMatchAttribute->setForeground(foregroundColor); highlightMatchAttribute->setBackground(searchColor); highlightMatchAttribute->dynamicAttribute(Attribute::ActivateMouseIn)->setBackground(searchColor); highlightMatchAttribute->dynamicAttribute(Attribute::ActivateMouseIn)->setForeground(foregroundColor); highlightMatchAttribute->dynamicAttribute(Attribute::ActivateCaretIn)->setBackground(searchColor); highlightMatchAttribute->dynamicAttribute(Attribute::ActivateCaretIn)->setForeground(foregroundColor); // init replacement attribute highlightReplacementAttribute->setBackground(replaceColor); highlightReplacementAttribute->setForeground(foregroundColor); } void KateSearchBar::showEvent(QShowEvent *event) { // Update init cursor if (m_incUi != nullptr) { m_incInitCursor = m_view->cursorPosition(); } updateSelectionOnly(); KateViewBarWidget::showEvent(event); } void KateSearchBar::updateSelectionOnly() { if (m_powerUi == nullptr) { return; } // Re-init "Selection only" checkbox if power search bar open const bool selected = m_view->selection(); bool selectionOnly = selected; if (selected) { Range const &selection = m_view->selectionRange(); selectionOnly = !selection.onSingleLine(); } m_powerUi->selectionOnly->setChecked(selectionOnly); } void KateSearchBar::updateIncInitCursor() { if (m_incUi == nullptr) { return; } // Update init cursor m_incInitCursor = m_view->cursorPosition(); } void KateSearchBar::onPowerPatternContextMenuRequest(const QPoint &pos) { const bool FOR_PATTERN = true; showExtendedContextMenu(FOR_PATTERN, pos); } void KateSearchBar::onPowerPatternContextMenuRequest() { onPowerPatternContextMenuRequest(m_powerUi->pattern->mapFromGlobal(QCursor::pos())); } void KateSearchBar::onPowerReplacmentContextMenuRequest(const QPoint &pos) { const bool FOR_REPLACEMENT = false; showExtendedContextMenu(FOR_REPLACEMENT, pos); } void KateSearchBar::onPowerReplacmentContextMenuRequest() { onPowerReplacmentContextMenuRequest(m_powerUi->replacement->mapFromGlobal(QCursor::pos())); } void KateSearchBar::onPowerCancelFindOrReplace() { m_cancelFindOrReplace = true; } bool KateSearchBar::isPower() const { return m_powerUi != nullptr; } void KateSearchBar::slotReadWriteChanged() { if (!KateSearchBar::isPower()) { return; } // perhaps disable/enable m_powerUi->replaceNext->setEnabled(m_view->doc()->isReadWrite() && isPatternValid()); m_powerUi->replaceAll->setEnabled(m_view->doc()->isReadWrite() && isPatternValid()); } diff --git a/src/view/kateviewhelpers.cpp b/src/view/kateviewhelpers.cpp index 8b8c87fc..8dcca2c2 100644 --- a/src/view/kateviewhelpers.cpp +++ b/src/view/kateviewhelpers.cpp @@ -1,3050 +1,3047 @@ /* This file is part of the KDE libraries Copyright (C) 2008, 2009 Matthew Woehlke Copyright (C) 2007 Mirko Stocker Copyright (C) 2002 John Firebaugh Copyright (C) 2001 Anders Lund Copyright (C) 2001 Christoph Cullmann Copyright (C) 2011 Svyatoslav Kuzmich Copyright (C) 2012 Kåre Särs (Minimap) Copyright 2017-2018 Friedrich W. H. Kossebau This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kateviewhelpers.h" #include "kateabstractinputmode.h" #include "kateannotationitemdelegate.h" #include "katecmd.h" #include "katecommandrangeexpressionparser.h" #include "kateconfig.h" #include "katedocument.h" #include "kateglobal.h" #include "katelayoutcache.h" #include "katepartdebug.h" #include "katerenderer.h" #include "katetextlayout.h" #include "katetextpreview.h" #include "kateview.h" #include "kateviewinternal.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // BEGIN KateMessageLayout KateMessageLayout::KateMessageLayout(QWidget *parent) : QLayout(parent) { } KateMessageLayout::~KateMessageLayout() { while (QLayoutItem *item = takeAt(0)) delete item; } void KateMessageLayout::addItem(QLayoutItem *item) { Q_ASSERT(false); add(item, KTextEditor::Message::CenterInView); } void KateMessageLayout::addWidget(QWidget *widget, KTextEditor::Message::MessagePosition pos) { add(new QWidgetItem(widget), pos); } int KateMessageLayout::count() const { return m_items.size(); } QLayoutItem *KateMessageLayout::itemAt(int index) const { if (index < 0 || index >= m_items.size()) return nullptr; return m_items[index]->item; } void KateMessageLayout::setGeometry(const QRect &rect) { QLayout::setGeometry(rect); const int s = spacing(); const QRect adjustedRect = rect.adjusted(s, s, -s, -s); for (auto wrapper : m_items) { QLayoutItem *item = wrapper->item; auto position = wrapper->position; if (position == KTextEditor::Message::TopInView) { const QRect r(adjustedRect.width() - item->sizeHint().width(), s, item->sizeHint().width(), item->sizeHint().height()); item->setGeometry(r); } else if (position == KTextEditor::Message::BottomInView) { const QRect r(adjustedRect.width() - item->sizeHint().width(), adjustedRect.height() - item->sizeHint().height(), item->sizeHint().width(), item->sizeHint().height()); item->setGeometry(r); } else if (position == KTextEditor::Message::CenterInView) { QRect r(0, 0, item->sizeHint().width(), item->sizeHint().height()); r.moveCenter(adjustedRect.center()); item->setGeometry(r); } else { Q_ASSERT_X(false, "setGeometry", "Only TopInView, CenterInView, and BottomInView are supported."); } } } QSize KateMessageLayout::sizeHint() const { return QSize(); } QLayoutItem *KateMessageLayout::takeAt(int index) { if (index >= 0 && index < m_items.size()) { ItemWrapper *layoutStruct = m_items.takeAt(index); return layoutStruct->item; } return nullptr; } void KateMessageLayout::add(QLayoutItem *item, KTextEditor::Message::MessagePosition pos) { m_items.push_back(new ItemWrapper(item, pos)); } // END KateMessageLayout // BEGIN KateScrollBar static const int s_lineWidth = 100; static const int s_pixelMargin = 8; static const int s_linePixelIncLimit = 6; const unsigned char KateScrollBar::characterOpacity[256] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // <- 15 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 255, 0, 0, 0, 0, 0, // <- 31 0, 125, 41, 221, 138, 195, 218, 21, 142, 142, 137, 137, 97, 87, 87, 140, // <- 47 223, 164, 183, 190, 191, 193, 214, 158, 227, 216, 103, 113, 146, 140, 146, 149, // <- 63 248, 204, 240, 174, 217, 197, 178, 205, 209, 176, 168, 211, 160, 246, 238, 218, // <- 79 195, 229, 227, 196, 167, 212, 188, 238, 197, 169, 189, 158, 21, 151, 115, 90, // <- 95 15, 192, 209, 153, 208, 187, 162, 221, 183, 149, 161, 191, 146, 203, 167, 182, // <- 111 208, 203, 139, 166, 158, 167, 157, 189, 164, 179, 156, 167, 145, 166, 109, 0, // <- 127 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // <- 143 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // <- 159 0, 125, 184, 187, 146, 201, 127, 203, 89, 194, 156, 141, 117, 87, 202, 88, // <- 175 115, 165, 118, 121, 85, 190, 236, 87, 88, 111, 151, 140, 194, 191, 203, 148, // <- 191 215, 215, 222, 224, 223, 234, 230, 192, 208, 208, 216, 217, 187, 187, 194, 195, // <- 207 228, 255, 228, 228, 235, 239, 237, 150, 255, 222, 222, 229, 232, 180, 197, 225, // <- 223 208, 208, 216, 217, 212, 230, 218, 170, 202, 202, 211, 204, 156, 156, 165, 159, // <- 239 214, 194, 197, 197, 206, 206, 201, 132, 214, 183, 183, 192, 187, 195, 227, 198}; KateScrollBar::KateScrollBar(Qt::Orientation orientation, KateViewInternal *parent) : QScrollBar(orientation, parent->m_view) , m_middleMouseDown(false) , m_leftMouseDown(false) , m_view(parent->m_view) , m_doc(parent->doc()) , m_viewInternal(parent) , m_textPreview(nullptr) , m_showMarks(false) , m_showMiniMap(false) , m_miniMapAll(true) , m_miniMapWidth(40) , m_grooveHeight(height()) , m_linesModified(0) { connect(this, SIGNAL(valueChanged(int)), this, SLOT(sliderMaybeMoved(int))); connect(m_doc, SIGNAL(marksChanged(KTextEditor::Document *)), this, SLOT(marksChanged())); m_updateTimer.setInterval(300); m_updateTimer.setSingleShot(true); QTimer::singleShot(10, this, SLOT(updatePixmap())); // track mouse for text preview widget setMouseTracking(orientation == Qt::Vertical); // setup text preview timer m_delayTextPreviewTimer.setSingleShot(true); m_delayTextPreviewTimer.setInterval(250); connect(&m_delayTextPreviewTimer, SIGNAL(timeout()), this, SLOT(showTextPreview())); } KateScrollBar::~KateScrollBar() { delete m_textPreview; } void KateScrollBar::setShowMiniMap(bool b) { if (b && !m_showMiniMap) { connect(m_view, SIGNAL(selectionChanged(KTextEditor::View *)), &m_updateTimer, SLOT(start()), Qt::UniqueConnection); connect(m_doc, SIGNAL(textChanged(KTextEditor::Document *)), &m_updateTimer, SLOT(start()), Qt::UniqueConnection); connect(m_view, SIGNAL(delayedUpdateOfView()), &m_updateTimer, SLOT(start()), Qt::UniqueConnection); connect(&m_updateTimer, SIGNAL(timeout()), this, SLOT(updatePixmap()), Qt::UniqueConnection); connect(&(m_view->textFolding()), SIGNAL(foldingRangesChanged()), &m_updateTimer, SLOT(start()), Qt::UniqueConnection); } else if (!b) { disconnect(&m_updateTimer); } m_showMiniMap = b; updateGeometry(); update(); } QSize KateScrollBar::sizeHint() const { if (m_showMiniMap) { return QSize(m_miniMapWidth, QScrollBar::sizeHint().height()); } return QScrollBar::sizeHint(); } int KateScrollBar::minimapYToStdY(int y) { // Check if the minimap fills the whole scrollbar if (m_stdGroveRect.height() == m_mapGroveRect.height()) { return y; } // check if y is on the step up/down if ((y < m_stdGroveRect.top()) || (y > m_stdGroveRect.bottom())) { return y; } if (y < m_mapGroveRect.top()) { return m_stdGroveRect.top() + 1; } if (y > m_mapGroveRect.bottom()) { return m_stdGroveRect.bottom() - 1; } // check for div/0 if (m_mapGroveRect.height() == 0) { return y; } int newY = (y - m_mapGroveRect.top()) * m_stdGroveRect.height() / m_mapGroveRect.height(); newY += m_stdGroveRect.top(); return newY; } void KateScrollBar::mousePressEvent(QMouseEvent *e) { // delete text preview hideTextPreview(); if (e->button() == Qt::MidButton) { m_middleMouseDown = true; } else if (e->button() == Qt::LeftButton) { m_leftMouseDown = true; } if (m_showMiniMap) { if (m_leftMouseDown && e->pos().y() > m_mapGroveRect.top() && e->pos().y() < m_mapGroveRect.bottom()) { // if we show the minimap left-click jumps directly to the selected position int newVal = (e->pos().y() - m_mapGroveRect.top()) / (double)m_mapGroveRect.height() * (double)(maximum() + pageStep()) - pageStep() / 2; newVal = qBound(0, newVal, maximum()); setSliderPosition(newVal); } QMouseEvent eMod(QEvent::MouseButtonPress, QPoint(6, minimapYToStdY(e->pos().y())), e->button(), e->buttons(), e->modifiers()); QScrollBar::mousePressEvent(&eMod); } else { QScrollBar::mousePressEvent(e); } m_toolTipPos = e->globalPos() - QPoint(e->pos().x(), 0); const int fromLine = m_viewInternal->toRealCursor(m_viewInternal->startPos()).line() + 1; const int lastLine = m_viewInternal->toRealCursor(m_viewInternal->endPos()).line() + 1; QToolTip::showText(m_toolTipPos, i18nc("from line - to line", "
%1

%2
", fromLine, lastLine), this); redrawMarks(); } void KateScrollBar::mouseReleaseEvent(QMouseEvent *e) { if (e->button() == Qt::MidButton) { m_middleMouseDown = false; } else if (e->button() == Qt::LeftButton) { m_leftMouseDown = false; } redrawMarks(); if (m_leftMouseDown || m_middleMouseDown) { QToolTip::hideText(); } if (m_showMiniMap) { QMouseEvent eMod(QEvent::MouseButtonRelease, QPoint(e->pos().x(), minimapYToStdY(e->pos().y())), e->button(), e->buttons(), e->modifiers()); QScrollBar::mouseReleaseEvent(&eMod); } else { QScrollBar::mouseReleaseEvent(e); } } void KateScrollBar::mouseMoveEvent(QMouseEvent *e) { if (m_showMiniMap) { QMouseEvent eMod(QEvent::MouseMove, QPoint(e->pos().x(), minimapYToStdY(e->pos().y())), e->button(), e->buttons(), e->modifiers()); QScrollBar::mouseMoveEvent(&eMod); } else { QScrollBar::mouseMoveEvent(e); } if (e->buttons() & (Qt::LeftButton | Qt::MidButton)) { redrawMarks(); // current line tool tip m_toolTipPos = e->globalPos() - QPoint(e->pos().x(), 0); const int fromLine = m_viewInternal->toRealCursor(m_viewInternal->startPos()).line() + 1; const int lastLine = m_viewInternal->toRealCursor(m_viewInternal->endPos()).line() + 1; QToolTip::showText(m_toolTipPos, i18nc("from line - to line", "
%1

%2
", fromLine, lastLine), this); } showTextPreviewDelayed(); } void KateScrollBar::leaveEvent(QEvent *event) { hideTextPreview(); QAbstractSlider::leaveEvent(event); } bool KateScrollBar::eventFilter(QObject *object, QEvent *event) { Q_UNUSED(object) if (m_textPreview && event->type() == QEvent::WindowDeactivate) { // We need hide the scrollbar TextPreview widget hideTextPreview(); } return false; } void KateScrollBar::paintEvent(QPaintEvent *e) { if (m_doc->marks().size() != m_lines.size()) { recomputeMarksPositions(); } if (m_showMiniMap) { miniMapPaintEvent(e); } else { normalPaintEvent(e); } } void KateScrollBar::showTextPreviewDelayed() { if (!m_textPreview) { if (!m_delayTextPreviewTimer.isActive()) { m_delayTextPreviewTimer.start(); } } else { showTextPreview(); } } void KateScrollBar::showTextPreview() { if (orientation() != Qt::Vertical || isSliderDown() || (minimum() == maximum()) || !m_view->config()->scrollBarPreview()) { return; } // only show when main window is active (#392396) if (window() && !window()->isActiveWindow()) { return; } QRect grooveRect; if (m_showMiniMap) { // If mini-map is shown, the height of the map might not be the whole height grooveRect = m_mapGroveRect; } else { QStyleOptionSlider opt; opt.init(this); opt.subControls = QStyle::SC_None; opt.activeSubControls = QStyle::SC_None; opt.orientation = orientation(); opt.minimum = minimum(); opt.maximum = maximum(); opt.sliderPosition = sliderPosition(); opt.sliderValue = value(); opt.singleStep = singleStep(); opt.pageStep = pageStep(); grooveRect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, this); } if (m_view->config()->scrollPastEnd()) { // Adjust the grove size to accommodate the added pageStep at the bottom int adjust = pageStep() * grooveRect.height() / (maximum() + pageStep() - minimum()); grooveRect.adjust(0, 0, 0, -adjust); } const QPoint cursorPos = mapFromGlobal(QCursor::pos()); if (grooveRect.contains(cursorPos)) { if (!m_textPreview) { m_textPreview = new KateTextPreview(m_view, this); m_textPreview->setAttribute(Qt::WA_ShowWithoutActivating); m_textPreview->setFrameStyle(QFrame::StyledPanel); // event filter to catch application WindowDeactivate event, to hide the preview window qApp->installEventFilter(this); } const qreal posInPercent = static_cast(cursorPos.y() - grooveRect.top()) / grooveRect.height(); const qreal startLine = posInPercent * m_view->textFolding().visibleLines(); m_textPreview->resize(m_view->width() / 2, m_view->height() / 5); const int xGlobal = mapToGlobal(QPoint(0, 0)).x(); const int yGlobal = qMin(mapToGlobal(QPoint(0, height())).y() - m_textPreview->height(), qMax(mapToGlobal(QPoint(0, 0)).y(), mapToGlobal(cursorPos).y() - m_textPreview->height() / 2)); m_textPreview->move(xGlobal - m_textPreview->width(), yGlobal); m_textPreview->setLine(startLine); m_textPreview->setCenterView(true); m_textPreview->setScaleFactor(0.75); m_textPreview->raise(); m_textPreview->show(); } else { hideTextPreview(); } } void KateScrollBar::hideTextPreview() { if (m_delayTextPreviewTimer.isActive()) { m_delayTextPreviewTimer.stop(); } qApp->removeEventFilter(this); delete m_textPreview; } // This function is optimized for bing called in sequence. const QColor KateScrollBar::charColor(const QVector &attributes, int &attributeIndex, const QVector &decorations, const QColor &defaultColor, int x, QChar ch) { QColor color = defaultColor; bool styleFound = false; // Query the decorations, that is, things like search highlighting, or the // KDevelop DUChain highlighting, for a color to use for (auto &range : decorations) { if (range.start <= x && range.start + range.length > x) { // If there's a different background color set (search markers, ...) // use that, otherwise use the foreground color. if (range.format.hasProperty(QTextFormat::BackgroundBrush)) { color = range.format.background().color(); } else { color = range.format.foreground().color(); } styleFound = true; break; } } // If there's no decoration set for the current character (this will mostly be the case for // plain Kate), query the styles, that is, the default kate syntax highlighting. if (!styleFound) { // go to the block containing x while ((attributeIndex < attributes.size()) && ((attributes[attributeIndex].offset + attributes[attributeIndex].length) < x)) { ++attributeIndex; } if ((attributeIndex < attributes.size()) && (x < attributes[attributeIndex].offset + attributes[attributeIndex].length)) { color = m_view->renderer()->attribute(attributes[attributeIndex].attributeValue)->foreground().color(); } } // Query how much "blackness" the character has. // This causes for example a dot or a dash to appear less intense // than an A or similar. // This gives the pixels created a bit of structure, which makes it look more // like real text. color.setAlpha((ch.unicode() < 256) ? characterOpacity[ch.unicode()] : 222); return color; } void KateScrollBar::updatePixmap() { // QTime time; // time.start(); if (!m_showMiniMap) { // make sure no time is wasted if the option is disabled return; } // For performance reason, only every n-th line will be drawn if the widget is // sufficiently small compared to the amount of lines in the document. int docLineCount = m_view->textFolding().visibleLines(); int pixmapLineCount = docLineCount; if (m_view->config()->scrollPastEnd()) { pixmapLineCount += pageStep(); } int pixmapLinesUnscaled = pixmapLineCount; if (m_grooveHeight < 5) { m_grooveHeight = 5; } int lineDivisor = pixmapLinesUnscaled / m_grooveHeight; if (lineDivisor < 1) { lineDivisor = 1; } int charIncrement = 1; int lineIncrement = 1; if ((m_grooveHeight > 10) && (pixmapLineCount >= m_grooveHeight * 2)) { charIncrement = pixmapLineCount / m_grooveHeight; while (charIncrement > s_linePixelIncLimit) { lineIncrement++; pixmapLineCount = pixmapLinesUnscaled / lineIncrement; charIncrement = pixmapLineCount / m_grooveHeight; } pixmapLineCount /= charIncrement; } int pixmapLineWidth = s_pixelMargin + s_lineWidth / charIncrement; // qCDebug(LOG_KTE) << "l" << lineIncrement << "c" << charIncrement << "d" << lineDivisor; // qCDebug(LOG_KTE) << "pixmap" << pixmapLineCount << pixmapLineWidth << "docLines" << m_view->textFolding().visibleLines() << "height" << m_grooveHeight; const QColor backgroundColor = m_view->defaultStyleAttribute(KTextEditor::dsNormal)->background().color(); const QColor defaultTextColor = m_view->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color(); const QColor selectionBgColor = m_view->renderer()->config()->selectionColor(); QColor modifiedLineColor = m_view->renderer()->config()->modifiedLineColor(); QColor savedLineColor = m_view->renderer()->config()->savedLineColor(); // move the modified line color away from the background color modifiedLineColor.setHsv(modifiedLineColor.hue(), 255, 255 - backgroundColor.value() / 3); savedLineColor.setHsv(savedLineColor.hue(), 100, 255 - backgroundColor.value() / 3); // increase dimensions by ratio m_pixmap = QPixmap(pixmapLineWidth * m_view->devicePixelRatioF(), pixmapLineCount * m_view->devicePixelRatioF()); m_pixmap.fill(QColor("transparent")); // The text currently selected in the document, to be drawn later. const KTextEditor::Range &selection = m_view->selectionRange(); QPainter painter; if (painter.begin(&m_pixmap)) { // init pen once, afterwards, only change it if color changes to avoid a lot of allocation for setPen painter.setPen(selectionBgColor); // Do not force updates of the highlighting if the document is very large bool simpleMode = m_doc->lines() > 7500; int pixelY = 0; int drawnLines = 0; // Iterate over all visible lines, drawing them. for (int virtualLine = 0; virtualLine < docLineCount; virtualLine += lineIncrement) { int realLineNumber = m_view->textFolding().visibleLineToLine(virtualLine); QString lineText = m_doc->line(realLineNumber); if (!simpleMode) { m_doc->buffer().ensureHighlighted(realLineNumber); } const Kate::TextLine &kateline = m_doc->plainKateTextLine(realLineNumber); const QVector &attributes = kateline->attributesList(); QVector decorations = m_view->renderer()->decorationsForLine(kateline, realLineNumber); int attributeIndex = 0; // Draw selection if it is on an empty line if (selection.contains(KTextEditor::Cursor(realLineNumber, 0)) && lineText.size() == 0) { if (selectionBgColor != painter.pen().color()) { painter.setPen(selectionBgColor); } painter.drawLine(s_pixelMargin, pixelY, s_pixelMargin + s_lineWidth - 1, pixelY); } // Iterate over the line to draw the background int selStartX = -1; int selEndX = -1; int pixelX = s_pixelMargin; // use this to control the offset of the text from the left for (int x = 0; (x < lineText.size() && x < s_lineWidth); x += charIncrement) { if (pixelX >= s_lineWidth + s_pixelMargin) { break; } // Query the selection and draw it behind the character if (selection.contains(KTextEditor::Cursor(realLineNumber, x))) { if (selStartX == -1) selStartX = pixelX; selEndX = pixelX; if (lineText.size() - 1 == x) { selEndX = s_lineWidth + s_pixelMargin - 1; } } if (lineText[x] == QLatin1Char('\t')) { pixelX += qMax(4 / charIncrement, 1); // FIXME: tab width... } else { pixelX++; } } if (selStartX != -1) { if (selectionBgColor != painter.pen().color()) { painter.setPen(selectionBgColor); } painter.drawLine(selStartX, pixelY, selEndX, pixelY); } // Iterate over all the characters in the current line pixelX = s_pixelMargin; for (int x = 0; (x < lineText.size() && x < s_lineWidth); x += charIncrement) { if (pixelX >= s_lineWidth + s_pixelMargin) { break; } // draw the pixels if (lineText[x] == QLatin1Char(' ')) { pixelX++; } else if (lineText[x] == QLatin1Char('\t')) { pixelX += qMax(4 / charIncrement, 1); // FIXME: tab width... } else { const QColor newPenColor(charColor(attributes, attributeIndex, decorations, defaultTextColor, x, lineText[x])); if (newPenColor != painter.pen().color()) { painter.setPen(newPenColor); } // Actually draw the pixel with the color queried from the renderer. painter.drawPoint(pixelX, pixelY); pixelX++; } } drawnLines++; if (((drawnLines) % charIncrement) == 0) { pixelY++; } } // qCDebug(LOG_KTE) << drawnLines; // Draw line modification marker map. // Disable this if the document is really huge, // since it requires querying every line. if (m_doc->lines() < 50000) { for (int lineno = 0; lineno < docLineCount; lineno++) { int realLineNo = m_view->textFolding().visibleLineToLine(lineno); const Kate::TextLine &line = m_doc->plainKateTextLine(realLineNo); const QColor &col = line->markedAsModified() ? modifiedLineColor : savedLineColor; if (line->markedAsModified() || line->markedAsSavedOnDisk()) { painter.fillRect(2, lineno / lineDivisor, 3, 1, col); } } } // end painting painter.end(); } // set right ratio m_pixmap.setDevicePixelRatio(m_view->devicePixelRatioF()); // qCDebug(LOG_KTE) << time.elapsed(); // Redraw the scrollbar widget with the updated pixmap. update(); } void KateScrollBar::miniMapPaintEvent(QPaintEvent *e) { QScrollBar::paintEvent(e); QPainter painter(this); QStyleOptionSlider opt; opt.init(this); opt.subControls = QStyle::SC_None; opt.activeSubControls = QStyle::SC_None; opt.orientation = orientation(); opt.minimum = minimum(); opt.maximum = maximum(); opt.sliderPosition = sliderPosition(); opt.sliderValue = value(); opt.singleStep = singleStep(); opt.pageStep = pageStep(); QRect grooveRect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, this); m_stdGroveRect = grooveRect; if (style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSubLine, this).height() == 0) { int alignMargin = style()->pixelMetric(QStyle::PM_FocusFrameVMargin, &opt, this); grooveRect.moveTop(alignMargin); grooveRect.setHeight(grooveRect.height() - alignMargin); } if (style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarAddLine, this).height() == 0) { int alignMargin = style()->pixelMetric(QStyle::PM_FocusFrameVMargin, &opt, this); grooveRect.setHeight(grooveRect.height() - alignMargin); } m_grooveHeight = grooveRect.height(); const int docXMargin = 1; // style()->drawControl(QStyle::CE_ScrollBarAddLine, &opt, &painter, this); // style()->drawControl(QStyle::CE_ScrollBarSubLine, &opt, &painter, this); // calculate the document size and position const int docHeight = qMin(grooveRect.height(), int(m_pixmap.height() / m_pixmap.devicePixelRatio() * 2)) - 2 * docXMargin; const int yoffset = 1; // top-aligned in stead of center-aligned (grooveRect.height() - docHeight) / 2; const QRect docRect(QPoint(grooveRect.left() + docXMargin, yoffset + grooveRect.top()), QSize(grooveRect.width() - docXMargin, docHeight)); m_mapGroveRect = docRect; // calculate the visible area int max = qMax(maximum() + 1, 1); int visibleStart = value() * docHeight / (max + pageStep()) + docRect.top() + 0.5; int visibleEnd = (value() + pageStep()) * docHeight / (max + pageStep()) + docRect.top(); QRect visibleRect = docRect; visibleRect.moveTop(visibleStart); visibleRect.setHeight(visibleEnd - visibleStart); // calculate colors const QColor backgroundColor = m_view->defaultStyleAttribute(KTextEditor::dsNormal)->background().color(); const QColor foregroundColor = m_view->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color(); const QColor highlightColor = palette().link().color(); const int backgroundLightness = backgroundColor.lightness(); const int foregroundLightness = foregroundColor.lightness(); const int lighnessDiff = (foregroundLightness - backgroundLightness); // get a color suited for the color theme QColor darkShieldColor = palette().color(QPalette::Mid); int hue, sat, light; darkShieldColor.getHsl(&hue, &sat, &light); // apply suitable lightness darkShieldColor.setHsl(hue, sat, backgroundLightness + lighnessDiff * 0.35); // gradient for nicer results QLinearGradient gradient(0, 0, width(), 0); gradient.setColorAt(0, darkShieldColor); gradient.setColorAt(0.3, darkShieldColor.lighter(115)); gradient.setColorAt(1, darkShieldColor); QColor lightShieldColor; lightShieldColor.setHsl(hue, sat, backgroundLightness + lighnessDiff * 0.15); QColor outlineColor; outlineColor.setHsl(hue, sat, backgroundLightness + lighnessDiff * 0.5); // draw the grove background in case the document is small painter.setPen(Qt::NoPen); painter.setBrush(backgroundColor); painter.drawRect(grooveRect); // adjust the rectangles QRect sliderRect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, this); sliderRect.setX(docXMargin); sliderRect.setWidth(width() - docXMargin * 2); if ((docHeight + 2 * docXMargin >= grooveRect.height()) && (sliderRect.height() > visibleRect.height() + 2)) { visibleRect.adjust(2, 0, -3, 0); } else { visibleRect.adjust(1, 0, -1, 2); sliderRect.setTop(visibleRect.top() - 1); sliderRect.setBottom(visibleRect.bottom() + 1); } // Smooth transform only when squeezing if (grooveRect.height() < m_pixmap.height() / m_pixmap.devicePixelRatio()) { painter.setRenderHint(QPainter::SmoothPixmapTransform); } // draw the modified lines margin QRect pixmapMarginRect(QPoint(0, 0), QSize(s_pixelMargin, m_pixmap.height() / m_pixmap.devicePixelRatio())); QRect docPixmapMarginRect(QPoint(0, docRect.top()), QSize(s_pixelMargin, docRect.height())); painter.drawPixmap(docPixmapMarginRect, m_pixmap, pixmapMarginRect); // calculate the stretch and draw the stretched lines (scrollbar marks) QRect pixmapRect(QPoint(s_pixelMargin, 0), QSize(m_pixmap.width() / m_pixmap.devicePixelRatio() - s_pixelMargin, m_pixmap.height() / m_pixmap.devicePixelRatio())); QRect docPixmapRect(QPoint(s_pixelMargin, docRect.top()), QSize(docRect.width() - s_pixelMargin, docRect.height())); painter.drawPixmap(docPixmapRect, m_pixmap, pixmapRect); // delimit the end of the document const int y = docPixmapRect.height() + grooveRect.y(); if (y + 2 < grooveRect.y() + grooveRect.height()) { QColor fg(foregroundColor); fg.setAlpha(30); painter.setBrush(Qt::NoBrush); painter.setPen(QPen(fg, 1)); painter.drawLine(grooveRect.x() + 1, y + 2, width() - 1, y + 2); } // fade the invisible sections const QRect top(grooveRect.x(), grooveRect.y(), grooveRect.width(), visibleRect.y() - grooveRect.y() // Pen width ); const QRect bottom(grooveRect.x(), grooveRect.y() + visibleRect.y() + visibleRect.height() - grooveRect.y(), // Pen width grooveRect.width(), grooveRect.height() - (visibleRect.y() - grooveRect.y()) - visibleRect.height()); QColor faded(backgroundColor); faded.setAlpha(110); painter.fillRect(top, faded); painter.fillRect(bottom, faded); // add a thin line to limit the scrollbar QColor c(foregroundColor); c.setAlpha(10); painter.setPen(QPen(c, 1)); painter.drawLine(0, 0, 0, height()); if (m_showMarks) { QHashIterator it = m_lines; QPen penBg; penBg.setWidth(4); lightShieldColor.setAlpha(180); penBg.setColor(lightShieldColor); painter.setPen(penBg); while (it.hasNext()) { it.next(); int y = (it.key() - grooveRect.top()) * docHeight / grooveRect.height() + docRect.top(); ; painter.drawLine(6, y, width() - 6, y); } it = m_lines; QPen pen; pen.setWidth(2); while (it.hasNext()) { it.next(); pen.setColor(it.value()); painter.setPen(pen); int y = (it.key() - grooveRect.top()) * docHeight / grooveRect.height() + docRect.top(); painter.drawLine(6, y, width() - 6, y); } } // slider outline QColor sliderColor(highlightColor); sliderColor.setAlpha(50); painter.fillRect(sliderRect, sliderColor); painter.setPen(QPen(highlightColor, 0)); // rounded rect looks ugly for some reason, so we draw 4 lines. painter.drawLine(sliderRect.left(), sliderRect.top() + 1, sliderRect.left(), sliderRect.bottom() - 1); painter.drawLine(sliderRect.right(), sliderRect.top() + 1, sliderRect.right(), sliderRect.bottom() - 1); painter.drawLine(sliderRect.left() + 1, sliderRect.top(), sliderRect.right() - 1, sliderRect.top()); painter.drawLine(sliderRect.left() + 1, sliderRect.bottom(), sliderRect.right() - 1, sliderRect.bottom()); } void KateScrollBar::normalPaintEvent(QPaintEvent *e) { QScrollBar::paintEvent(e); if (!m_showMarks) { return; } QPainter painter(this); QStyleOptionSlider opt; opt.init(this); opt.subControls = QStyle::SC_None; opt.activeSubControls = QStyle::SC_None; opt.orientation = orientation(); opt.minimum = minimum(); opt.maximum = maximum(); opt.sliderPosition = sliderPosition(); opt.sliderValue = value(); opt.singleStep = singleStep(); opt.pageStep = pageStep(); QRect rect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, this); int sideMargin = width() - rect.width(); if (sideMargin < 4) { sideMargin = 4; } sideMargin /= 2; QHashIterator it = m_lines; while (it.hasNext()) { it.next(); painter.setPen(it.value()); if (it.key() < rect.top() || it.key() > rect.bottom()) { painter.drawLine(0, it.key(), width(), it.key()); } else { painter.drawLine(0, it.key(), sideMargin, it.key()); painter.drawLine(width() - sideMargin, it.key(), width(), it.key()); } } } void KateScrollBar::resizeEvent(QResizeEvent *e) { QScrollBar::resizeEvent(e); m_updateTimer.start(); m_lines.clear(); update(); } void KateScrollBar::sliderChange(SliderChange change) { // call parents implementation QScrollBar::sliderChange(change); if (change == QAbstractSlider::SliderValueChange) { redrawMarks(); } else if (change == QAbstractSlider::SliderRangeChange) { marksChanged(); } if (m_leftMouseDown || m_middleMouseDown) { const int fromLine = m_viewInternal->toRealCursor(m_viewInternal->startPos()).line() + 1; const int lastLine = m_viewInternal->toRealCursor(m_viewInternal->endPos()).line() + 1; QToolTip::showText(m_toolTipPos, i18nc("from line - to line", "
%1

%2
", fromLine, lastLine), this); } } void KateScrollBar::marksChanged() { m_lines.clear(); update(); } void KateScrollBar::redrawMarks() { if (!m_showMarks) { return; } update(); } void KateScrollBar::recomputeMarksPositions() { // get the style options to compute the scrollbar pixels QStyleOptionSlider opt; initStyleOption(&opt); QRect grooveRect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, this); // cache top margin and groove height const int top = grooveRect.top(); const int h = grooveRect.height() - 1; // make sure we have a sane height if (h <= 0) { return; } // get total visible (=without folded) lines in the document int visibleLines = m_view->textFolding().visibleLines() - 1; if (m_view->config()->scrollPastEnd()) { visibleLines += m_viewInternal->linesDisplayed() - 1; visibleLines -= m_view->config()->autoCenterLines(); } // now repopulate the scrollbar lines list m_lines.clear(); const QHash &marks = m_doc->marks(); for (QHash::const_iterator i = marks.constBegin(); i != marks.constEnd(); ++i) { KTextEditor::Mark *mark = i.value(); const int line = m_view->textFolding().lineToVisibleLine(mark->line); const double ratio = static_cast(line) / visibleLines; m_lines.insert(top + (int)(h * ratio), KateRendererConfig::global()->lineMarkerColor((KTextEditor::MarkInterface::MarkTypes)mark->type)); } } void KateScrollBar::sliderMaybeMoved(int value) { if (m_middleMouseDown) { // we only need to emit this signal once, as for the following slider // movements the signal sliderMoved() is already emitted. // Thus, set m_middleMouseDown to false right away. m_middleMouseDown = false; emit sliderMMBMoved(value); } } // END // BEGIN KateCmdLineEditFlagCompletion /** * This class provide completion of flags. It shows a short description of * each flag, and flags are appended. */ class KateCmdLineEditFlagCompletion : public KCompletion { public: KateCmdLineEditFlagCompletion() { ; } QString makeCompletion(const QString & /*s*/) override { return QString(); } }; // END KateCmdLineEditFlagCompletion // BEGIN KateCmdLineEdit KateCommandLineBar::KateCommandLineBar(KTextEditor::ViewPrivate *view, QWidget *parent) : KateViewBarWidget(true, parent) { QHBoxLayout *topLayout = new QHBoxLayout(); centralWidget()->setLayout(topLayout); topLayout->setContentsMargins(0, 0, 0, 0); m_lineEdit = new KateCmdLineEdit(this, view); connect(m_lineEdit, SIGNAL(hideRequested()), SIGNAL(hideMe())); topLayout->addWidget(m_lineEdit); QToolButton *helpButton = new QToolButton(this); helpButton->setAutoRaise(true); helpButton->setIcon(QIcon::fromTheme(QStringLiteral("help-contextual"))); topLayout->addWidget(helpButton); connect(helpButton, SIGNAL(clicked()), this, SLOT(showHelpPage())); setFocusProxy(m_lineEdit); } void KateCommandLineBar::showHelpPage() { KHelpClient::invokeHelp(QStringLiteral("advanced-editing-tools-commandline"), QStringLiteral("kate")); } KateCommandLineBar::~KateCommandLineBar() { } // inserts the given string in the command line edit and (if selected = true) selects it so the user // can type over it if they want to void KateCommandLineBar::setText(const QString &text, bool selected) { m_lineEdit->setText(text); if (selected) { m_lineEdit->selectAll(); } } void KateCommandLineBar::execute(const QString &text) { m_lineEdit->slotReturnPressed(text); } KateCmdLineEdit::KateCmdLineEdit(KateCommandLineBar *bar, KTextEditor::ViewPrivate *view) : KLineEdit() , m_view(view) , m_bar(bar) , m_msgMode(false) , m_histpos(0) , m_cmdend(0) , m_command(nullptr) { connect(this, SIGNAL(returnPressed(QString)), this, SLOT(slotReturnPressed(QString))); setCompletionObject(KateCmd::self()->commandCompletionObject()); setAutoDeleteCompletionObject(false); m_hideTimer = new QTimer(this); m_hideTimer->setSingleShot(true); connect(m_hideTimer, SIGNAL(timeout()), this, SLOT(hideLineEdit())); // make sure the timer is stopped when the user switches views. if not, focus will be given to the // wrong view when KateViewBar::hideCurrentBarWidget() is called after 4 seconds. (the timer is // used for showing things like "Success" for four seconds after the user has used the kate // command line) connect(m_view, SIGNAL(focusOut(KTextEditor::View *)), m_hideTimer, SLOT(stop())); } void KateCmdLineEdit::hideEvent(QHideEvent *e) { Q_UNUSED(e); } QString KateCmdLineEdit::helptext(const QPoint &) const { const QString beg = QStringLiteral("
Help: "); const QString mid = QStringLiteral("
"); const QString end = QStringLiteral("
"); const QString t = text(); static const QRegularExpression re(QStringLiteral("\\s*help\\s+(.*)")); auto match = re.match(t); if (match.hasMatch()) { QString s; // get help for command const QString name = match.captured(1); if (name == QLatin1String("list")) { return beg + i18n("Available Commands") + mid + KateCmd::self()->commandList().join(QLatin1Char(' ')) + i18n("

For help on individual commands, do 'help <command>'

") + end; } else if (!name.isEmpty()) { KTextEditor::Command *cmd = KateCmd::self()->queryCommand(name); if (cmd) { if (cmd->help(m_view, name, s)) { return beg + name + mid + s + end; } else { return beg + name + mid + i18n("No help for '%1'", name) + end; } } else { return beg + mid + i18n("No such command %1", name) + end; } } } return beg + mid + i18n("

This is the Katepart command line.
" "Syntax: command [ arguments ]
" "For a list of available commands, enter help list
" "For help for individual commands, enter help <command>

") + end; } bool KateCmdLineEdit::event(QEvent *e) { if (e->type() == QEvent::QueryWhatsThis) { setWhatsThis(helptext(QPoint())); e->accept(); return true; } return KLineEdit::event(e); } /** * Parse the text as a command. * * The following is a simple PEG grammar for the syntax of the command. * (A PEG grammar is like a BNF grammar, except that "/" stands for * ordered choice: only the first matching rule is used. In other words, * the parsing is short-circuited in the manner of the "or" operator in * programming languages, and so the grammar is unambiguous.) * * Text <- Range? Command * / Position * Range <- Position ("," Position)? * / "%" * Position <- Base Offset? * Base <- Line * / LastLine * / ThisLine * / Mark * Offset <- [+-] Base * Line <- [0-9]+ * LastLine <- "$" * ThisLine <- "." * Mark <- "'" [a-z] */ void KateCmdLineEdit::slotReturnPressed(const QString &text) { static const QRegularExpression focusChangingCommands(QStringLiteral("^(buffer|b|new|vnew|bp|bprev|bn|bnext|bf|bfirst|bl|blast|edit|e)$")); if (text.isEmpty()) { return; } // silently ignore leading space characters uint n = 0; const uint textlen = text.length(); while ((n < textlen) && (text[n].isSpace())) { n++; } if (n >= textlen) { return; } QString cmd = text.mid(n); // Parse any leading range expression, and strip it (and maybe do some other transforms on the command). QString leadingRangeExpression; KTextEditor::Range range = CommandRangeExpressionParser::parseRangeExpression(cmd, m_view, leadingRangeExpression, cmd); // Built in help: if the command starts with "help", [try to] show some help if (cmd.startsWith(QLatin1String("help"))) { QWhatsThis::showText(mapToGlobal(QPoint(0, 0)), helptext(QPoint())); clear(); KateCmd::self()->appendHistory(cmd); m_histpos = KateCmd::self()->historyLength(); m_oldText.clear(); return; } if (cmd.length() > 0) { KTextEditor::Command *p = KateCmd::self()->queryCommand(cmd); m_oldText = leadingRangeExpression + cmd; m_msgMode = true; // if the command changes the focus itself, the bar should be hidden before execution. if (focusChangingCommands.match(cmd.leftRef(cmd.indexOf(QLatin1Char(' ')))).hasMatch()) { emit hideRequested(); } if (!p) { setText(i18n("No such command: \"%1\"", cmd)); } else if (range.isValid() && !p->supportsRange(cmd)) { // Raise message, when the command does not support ranges. setText(i18n("Error: No range allowed for command \"%1\".", cmd)); } else { QString msg; if (p->exec(m_view, cmd, msg, range)) { // append command along with range (will be empty if none given) to history KateCmd::self()->appendHistory(leadingRangeExpression + cmd); m_histpos = KateCmd::self()->historyLength(); m_oldText.clear(); if (msg.length() > 0) { setText(i18n("Success: ") + msg); } else if (isVisible()) { // always hide on success without message emit hideRequested(); } } else { if (msg.length() > 0) { if (msg.contains(QLatin1Char('\n'))) { // multiline error, use widget with more space QWhatsThis::showText(mapToGlobal(QPoint(0, 0)), msg); } else { setText(msg); } } else { setText(i18n("Command \"%1\" failed.", cmd)); } } } } // clean up if (completionObject() != KateCmd::self()->commandCompletionObject()) { KCompletion *c = completionObject(); setCompletionObject(KateCmd::self()->commandCompletionObject()); delete c; } m_command = nullptr; m_cmdend = 0; if (!focusChangingCommands.match(cmd.leftRef(cmd.indexOf(QLatin1Char(' ')))).hasMatch()) { m_view->setFocus(); } if (isVisible()) { m_hideTimer->start(4000); } } void KateCmdLineEdit::hideLineEdit() // unless i have focus ;) { if (!hasFocus()) { emit hideRequested(); } } void KateCmdLineEdit::focusInEvent(QFocusEvent *ev) { if (m_msgMode) { m_msgMode = false; setText(m_oldText); selectAll(); } KLineEdit::focusInEvent(ev); } void KateCmdLineEdit::keyPressEvent(QKeyEvent *ev) { if (ev->key() == Qt::Key_Escape || (ev->key() == Qt::Key_BracketLeft && ev->modifiers() == Qt::ControlModifier)) { m_view->setFocus(); hideLineEdit(); clear(); } else if (ev->key() == Qt::Key_Up) { fromHistory(true); } else if (ev->key() == Qt::Key_Down) { fromHistory(false); } uint cursorpos = cursorPosition(); KLineEdit::keyPressEvent(ev); // during typing, let us see if we have a valid command if (!m_cmdend || cursorpos <= m_cmdend) { QChar c; if (!ev->text().isEmpty()) { c = ev->text().at(0); } if (!m_cmdend && !c.isNull()) { // we have no command, so lets see if we got one if (!c.isLetterOrNumber() && c != QLatin1Char('-') && c != QLatin1Char('_')) { m_command = KateCmd::self()->queryCommand(text().trimmed()); if (m_command) { // qCDebug(LOG_KTE)<<"keypress in commandline: We have a command! "<queryCommand(text().trimmed()); if (m_command) { // qCDebug(LOG_KTE)<<"keypress in commandline: We have a command! "<commandCompletionObject()) { KCompletion *c = completionObject(); setCompletionObject(KateCmd::self()->commandCompletionObject()); delete c; } m_cmdend = 0; } } // if we got a command, check if it wants to do something. if (m_command) { KCompletion *cmpl = m_command->completionObject(m_view, text().left(m_cmdend).trimmed()); if (cmpl) { // We need to prepend the current command name + flag string // when completion is done // qCDebug(LOG_KTE)<<"keypress in commandline: Setting completion object!"; setCompletionObject(cmpl); } } } else if (m_command && !ev->text().isEmpty()) { // check if we should call the commands processText() if (m_command->wantsToProcessText(text().left(m_cmdend).trimmed())) { m_command->processText(m_view, text()); } } } void KateCmdLineEdit::fromHistory(bool up) { if (!KateCmd::self()->historyLength()) { return; } QString s; if (up) { if (m_histpos > 0) { m_histpos--; s = KateCmd::self()->fromHistory(m_histpos); } } else { if (m_histpos < (KateCmd::self()->historyLength() - 1)) { m_histpos++; s = KateCmd::self()->fromHistory(m_histpos); } else { m_histpos = KateCmd::self()->historyLength(); setText(m_oldText); } } if (!s.isEmpty()) { // Select the argument part of the command, so that it is easy to overwrite setText(s); static const QRegularExpression reCmd(QStringLiteral("^[\\w\\-]+(?:[^a-zA-Z0-9_-]|:\\w+)(.*)")); const auto match = reCmd.match(text()); if (match.hasMatch()) { setSelection(text().length() - match.capturedLength(1), match.capturedLength(1)); } } } // END KateCmdLineEdit // BEGIN KateIconBorder using namespace KTextEditor; KateIconBorder::KateIconBorder(KateViewInternal *internalView, QWidget *parent) : QWidget(parent) , m_view(internalView->m_view) , m_doc(internalView->doc()) , m_viewInternal(internalView) , m_iconBorderOn(false) , m_lineNumbersOn(false) , m_relLineNumbersOn(false) , m_updateRelLineNumbers(false) , m_foldingMarkersOn(false) , m_dynWrapIndicatorsOn(false) , m_annotationBorderOn(false) , m_updatePositionToArea(true) , m_annotationItemDelegate(new KateAnnotationItemDelegate(m_viewInternal, this)) { setAcceptDrops(true); setAttribute(Qt::WA_StaticContents); // See: https://doc.qt.io/qt-5/qwidget.html#update. As this widget does not // have a background, there's no need for Qt to erase the widget's area // before repainting. Enabling this prevents flickering when the widget is // repainted. setAttribute(Qt::WA_OpaquePaintEvent); setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Minimum); setMouseTracking(true); m_doc->setMarkDescription(MarkInterface::markType01, i18n("Bookmark")); - m_doc->setMarkPixmap(MarkInterface::markType01, QIcon::fromTheme(QStringLiteral("bookmarks")).pixmap(32, 32)); + m_doc->setMarkIcon(MarkInterface::markType01, QIcon::fromTheme(QStringLiteral("bookmarks"))); connect(m_annotationItemDelegate, &AbstractAnnotationItemDelegate::sizeHintChanged, this, &KateIconBorder::updateAnnotationBorderWidth); updateFont(); m_antiFlickerTimer.setSingleShot(true); m_antiFlickerTimer.setInterval(300); connect(&m_antiFlickerTimer, &QTimer::timeout, this, &KateIconBorder::highlightFolding); // user interaction (scrolling) hides e.g. preview connect(m_view, SIGNAL(displayRangeChanged(KTextEditor::ViewPrivate *)), this, SLOT(displayRangeChanged())); } KateIconBorder::~KateIconBorder() { delete m_foldingPreview; delete m_foldingRange; } void KateIconBorder::setIconBorderOn(bool enable) { if (enable == m_iconBorderOn) { return; } m_iconBorderOn = enable; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::setAnnotationBorderOn(bool enable) { if (enable == m_annotationBorderOn) { return; } m_annotationBorderOn = enable; // make sure the tooltip is hidden if (!m_annotationBorderOn && !m_hoveredAnnotationGroupIdentifier.isEmpty()) { m_hoveredAnnotationGroupIdentifier.clear(); hideAnnotationTooltip(); } emit m_view->annotationBorderVisibilityChanged(m_view, enable); m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::removeAnnotationHovering() { // remove hovering if it's still there if (m_annotationBorderOn && !m_hoveredAnnotationGroupIdentifier.isEmpty()) { m_hoveredAnnotationGroupIdentifier.clear(); QTimer::singleShot(0, this, SLOT(update())); } } void KateIconBorder::setLineNumbersOn(bool enable) { if (enable == m_lineNumbersOn) { return; } m_lineNumbersOn = enable; m_dynWrapIndicatorsOn = (m_dynWrapIndicators == 1) ? enable : m_dynWrapIndicators; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::setRelLineNumbersOn(bool enable) { if (enable == m_relLineNumbersOn) { return; } m_relLineNumbersOn = enable; /* * We don't have to touch the m_dynWrapIndicatorsOn because * we already got it right from the m_lineNumbersOn */ m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::updateForCursorLineChange() { if (m_relLineNumbersOn) { m_updateRelLineNumbers = true; } // always do normal update, e.g. for different current line color! update(); } void KateIconBorder::setDynWrapIndicators(int state) { if (state == m_dynWrapIndicators) { return; } m_dynWrapIndicators = state; m_dynWrapIndicatorsOn = (state == 1) ? m_lineNumbersOn : state; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::setFoldingMarkersOn(bool enable) { if (enable == m_foldingMarkersOn) { return; } m_foldingMarkersOn = enable; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } QSize KateIconBorder::sizeHint() const { int w = 1; // Must be any value != 0 or we will never painted! const int i = m_positionToArea.size(); if (i > 0) { w = m_positionToArea.at(i - 1).first; } return QSize(w, 0); } // This function (re)calculates the maximum width of any of the digit characters (0 -> 9) // for graceful handling of variable-width fonts as the linenumber font. void KateIconBorder::updateFont() { const QFontMetricsF &fm = m_view->renderer()->currentFontMetrics(); m_maxCharWidth = 0.0; // Loop to determine the widest numeric character in the current font. // 48 is ascii '0' for (int i = 48; i < 58; i++) { const qreal charWidth = ceil(fm.boundingRect(QChar(i)).width()); m_maxCharWidth = qMax(m_maxCharWidth, charWidth); } // NOTE/TODO(or not) Take size of m_dynWrapIndicatorChar into account. // It's a multi-char and it's reported size is, even with a mono-space font, // bigger than each digit, e.g. 10 vs 12. Currently it seems to work even with // "Line Numbers Off" but all these width calculating looks slightly hacky // the icon pane scales with the font... m_iconAreaWidth = fm.height(); // Only for now, later may that become an own value m_foldingAreaWidth = m_iconAreaWidth; calcAnnotationBorderWidth(); m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } int KateIconBorder::lineNumberWidth() const { int width = 0; // Avoid unneeded expensive calculations ;-) if (m_lineNumbersOn) { // width = (number of digits + 1) * char width const int digits = (int)ceil(log10((double)(m_view->doc()->lines() + 1))); width = (int)ceil((digits + 1) * m_maxCharWidth); } if ((width < 1) && m_dynWrapIndicatorsOn && m_view->config()->dynWordWrap()) { // FIXME Why 2x? because of above (number of digits + 1) // -> looks to me like a hint for bad calculation elsewhere width = 2 * m_maxCharWidth; } return width; } void KateIconBorder::dragMoveEvent(QDragMoveEvent *event) { // FIXME Just calling m_view->m_viewInternal->dragMoveEvent(e) don't work // as intended, we need to set the cursor at column 1 // Is there a way to change the pos of the event? QPoint pos(0, event->pos().y()); // Code copy of KateViewInternal::dragMoveEvent m_view->m_viewInternal->placeCursor(pos, true, false); m_view->m_viewInternal->fixDropEvent(event); } void KateIconBorder::dropEvent(QDropEvent *event) { m_view->m_viewInternal->dropEvent(event); } void KateIconBorder::paintEvent(QPaintEvent *e) { paintBorder(e->rect().x(), e->rect().y(), e->rect().width(), e->rect().height()); } static void paintTriangle(QPainter &painter, QColor c, int xOffset, int yOffset, int width, int height, bool open) { painter.setRenderHint(QPainter::Antialiasing); qreal size = qMin(width, height); if (open) { // Paint unfolded icon less pushy if (KColorUtils::luma(c) < 0.25) { c = KColorUtils::darken(c); } else { c = KColorUtils::shade(c, 0.1); } } else { // Paint folded icon in contrast to popup highlighting if (KColorUtils::luma(c) > 0.25) { c = KColorUtils::darken(c); } else { c = KColorUtils::shade(c, 0.1); } } QPen pen; pen.setJoinStyle(Qt::RoundJoin); pen.setColor(c); pen.setWidthF(1.5); painter.setPen(pen); painter.setBrush(c); // let some border, if possible size *= 0.6; qreal halfSize = size / 2; qreal halfSizeP = halfSize * 0.6; QPointF middle(xOffset + (qreal)width / 2, yOffset + (qreal)height / 2); if (open) { QPointF points[3] = {middle + QPointF(-halfSize, -halfSizeP), middle + QPointF(halfSize, -halfSizeP), middle + QPointF(0, halfSizeP)}; painter.drawConvexPolygon(points, 3); } else { QPointF points[3] = {middle + QPointF(-halfSizeP, -halfSize), middle + QPointF(-halfSizeP, halfSize), middle + QPointF(halfSizeP, 0)}; painter.drawConvexPolygon(points, 3); } painter.setRenderHint(QPainter::Antialiasing, false); } /** * Helper class for an identifier which can be an empty or non-empty string or invalid. * Avoids complicated explicit statements in code dealing with the identifier * received as QVariant from a model. */ class KateAnnotationGroupIdentifier { public: KateAnnotationGroupIdentifier(const QVariant &identifier) : m_isValid(identifier.isValid() && identifier.canConvert()) , m_id(m_isValid ? identifier.toString() : QString()) { } KateAnnotationGroupIdentifier() = default; KateAnnotationGroupIdentifier(const KateAnnotationGroupIdentifier &rhs) = default; KateAnnotationGroupIdentifier &operator=(const KateAnnotationGroupIdentifier &rhs) { m_isValid = rhs.m_isValid; m_id = rhs.m_id; return *this; } KateAnnotationGroupIdentifier &operator=(const QVariant &identifier) { m_isValid = (identifier.isValid() && identifier.canConvert()); if (m_isValid) { m_id = identifier.toString(); } else { m_id.clear(); } return *this; } bool operator==(const KateAnnotationGroupIdentifier &rhs) const { return (m_isValid == rhs.m_isValid) && (!m_isValid || (m_id == rhs.m_id)); } bool operator!=(const KateAnnotationGroupIdentifier &rhs) const { return (m_isValid != rhs.m_isValid) || (m_isValid && (m_id != rhs.m_id)); } void clear() { m_isValid = false; m_id.clear(); } bool isValid() const { return m_isValid; } const QString &id() const { return m_id; } private: bool m_isValid = false; QString m_id; }; /** * Helper class for iterative calculation of data regarding the position * of a line with regard to annotation item grouping. */ class KateAnnotationGroupPositionState { public: /** * @param startz first rendered displayed line * @param isUsed flag whether the KateAnnotationGroupPositionState object will * be used or is just created due to being on the stack */ KateAnnotationGroupPositionState(KateViewInternal *viewInternal, const KTextEditor::AnnotationModel *model, const QString &hoveredAnnotationGroupIdentifier, uint startz, bool isUsed); /** * @param styleOption option to fill with data for the given line * @param z rendered displayed line * @param realLine real line which is rendered here (passed to avoid another look-up) */ void nextLine(KTextEditor::StyleOptionAnnotationItem &styleOption, uint z, int realLine); private: KateViewInternal *m_viewInternal; const KTextEditor::AnnotationModel *const m_model; const QString m_hoveredAnnotationGroupIdentifier; int m_visibleWrappedLineInAnnotationGroup = -1; KateAnnotationGroupIdentifier m_lastAnnotationGroupIdentifier; KateAnnotationGroupIdentifier m_nextAnnotationGroupIdentifier; bool m_isSameAnnotationGroupsSinceLast = false; }; KateAnnotationGroupPositionState::KateAnnotationGroupPositionState(KateViewInternal *viewInternal, const KTextEditor::AnnotationModel *model, const QString &hoveredAnnotationGroupIdentifier, uint startz, bool isUsed) : m_viewInternal(viewInternal) , m_model(model) , m_hoveredAnnotationGroupIdentifier(hoveredAnnotationGroupIdentifier) { if (!isUsed) { return; } if (!m_model || (static_cast(startz) >= m_viewInternal->cache()->viewCacheLineCount())) { return; } const auto realLineAtStart = m_viewInternal->cache()->viewLine(startz).line(); m_nextAnnotationGroupIdentifier = m_model->data(realLineAtStart, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole); if (m_nextAnnotationGroupIdentifier.isValid()) { // estimate state of annotation group before first rendered line if (startz == 0) { if (realLineAtStart > 0) { // TODO: here we would want to scan until the next line that would be displayed, // to see if there are any group changes until then // for now simply taking neighbour line into account, not a grave bug on the first displayed line m_lastAnnotationGroupIdentifier = m_model->data(realLineAtStart - 1, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole); m_isSameAnnotationGroupsSinceLast = (m_lastAnnotationGroupIdentifier == m_nextAnnotationGroupIdentifier); } } else { const auto realLineBeforeStart = m_viewInternal->cache()->viewLine(startz - 1).line(); m_lastAnnotationGroupIdentifier = m_model->data(realLineBeforeStart, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole); if (m_lastAnnotationGroupIdentifier.isValid()) { if (m_lastAnnotationGroupIdentifier.id() == m_nextAnnotationGroupIdentifier.id()) { m_isSameAnnotationGroupsSinceLast = true; // estimate m_visibleWrappedLineInAnnotationGroup from lines before startz for (uint z = startz; z > 0; --z) { const auto realLine = m_viewInternal->cache()->viewLine(z - 1).line(); const KateAnnotationGroupIdentifier identifier = m_model->data(realLine, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole); if (identifier != m_lastAnnotationGroupIdentifier) { break; } ++m_visibleWrappedLineInAnnotationGroup; } } } } } } void KateAnnotationGroupPositionState::nextLine(KTextEditor::StyleOptionAnnotationItem &styleOption, uint z, int realLine) { styleOption.wrappedLine = m_viewInternal->cache()->viewLine(z).viewLine(); styleOption.wrappedLineCount = m_viewInternal->cache()->viewLineCount(realLine); // Estimate position in group const KateAnnotationGroupIdentifier annotationGroupIdentifier = m_nextAnnotationGroupIdentifier; bool isSameAnnotationGroupsSinceThis = false; // Calculate next line's group identifier // shortcut: assuming wrapped lines are always displayed together, test is simple if (styleOption.wrappedLine + 1 < styleOption.wrappedLineCount) { m_nextAnnotationGroupIdentifier = annotationGroupIdentifier; isSameAnnotationGroupsSinceThis = true; } else { if (static_cast(z + 1) < m_viewInternal->cache()->viewCacheLineCount()) { const int realLineAfter = m_viewInternal->cache()->viewLine(z + 1).line(); // search for any realLine with a different group id, also the non-displayed int rl = realLine + 1; for (; rl <= realLineAfter; ++rl) { m_nextAnnotationGroupIdentifier = m_model->data(rl, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole); if (!m_nextAnnotationGroupIdentifier.isValid() || (m_nextAnnotationGroupIdentifier.id() != annotationGroupIdentifier.id())) { break; } } isSameAnnotationGroupsSinceThis = (rl > realLineAfter); if (rl < realLineAfter) { m_nextAnnotationGroupIdentifier = m_model->data(realLineAfter, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole); } } else { // TODO: check next line after display end m_nextAnnotationGroupIdentifier.clear(); isSameAnnotationGroupsSinceThis = false; } } if (annotationGroupIdentifier.isValid()) { if (m_hoveredAnnotationGroupIdentifier == annotationGroupIdentifier.id()) { styleOption.state |= QStyle::State_MouseOver; } else { styleOption.state &= ~QStyle::State_MouseOver; } if (m_isSameAnnotationGroupsSinceLast) { ++m_visibleWrappedLineInAnnotationGroup; } else { m_visibleWrappedLineInAnnotationGroup = 0; } styleOption.annotationItemGroupingPosition = StyleOptionAnnotationItem::InGroup; if (!m_isSameAnnotationGroupsSinceLast) { styleOption.annotationItemGroupingPosition |= StyleOptionAnnotationItem::GroupBegin; } if (!isSameAnnotationGroupsSinceThis) { styleOption.annotationItemGroupingPosition |= StyleOptionAnnotationItem::GroupEnd; } } else { m_visibleWrappedLineInAnnotationGroup = 0; } styleOption.visibleWrappedLineInGroup = m_visibleWrappedLineInAnnotationGroup; m_lastAnnotationGroupIdentifier = m_nextAnnotationGroupIdentifier; m_isSameAnnotationGroupsSinceLast = isSameAnnotationGroupsSinceThis; } void KateIconBorder::paintBorder(int /*x*/, int y, int /*width*/, int height) { const uint h = m_view->renderer()->lineHeight(); const uint startz = (y / h); const uint endz = qMin(startz + 1 + (height / h), static_cast(m_viewInternal->cache()->viewCacheLineCount())); const uint currentLine = m_view->cursorPosition().line(); // center the folding boxes int m_px = (h - 11) / 2; if (m_px < 0) { m_px = 0; } // Ensure we miss no change of the count of line number digits const int newNeededWidth = lineNumberWidth(); if (m_updatePositionToArea || (newNeededWidth != m_lineNumberAreaWidth)) { m_lineNumberAreaWidth = newNeededWidth; m_updatePositionToArea = true; m_positionToArea.clear(); } QPainter p(this); p.setRenderHints(QPainter::TextAntialiasing); p.setFont(m_view->renderer()->currentFont()); // for line numbers KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel(); KateAnnotationGroupPositionState annotationGroupPositionState(m_viewInternal, model, m_hoveredAnnotationGroupIdentifier, startz, m_annotationBorderOn); // Fetch often used data only once, improve readability const int w = width(); const QColor iconBarColor = m_view->renderer()->config()->iconBarColor(); // Effective our background const QColor lineNumberColor = m_view->renderer()->config()->lineNumberColor(); const QColor backgroundColor = m_view->renderer()->config()->backgroundColor(); // Of the edit area // Paint the border in chunks line by line for (uint z = startz; z < endz; z++) { // Painting coordinates, lineHeight * lineNumber const uint y = h * z; // Paint the border in chunks left->right, remember used width uint lnX = 0; // Paint background over full width... p.fillRect(lnX, y, w, h, iconBarColor); // ...and overpaint again the end to simulate some margin to the edit area, // so that the text not looks like stuck to the border p.fillRect(w - m_separatorWidth, y, w, h, backgroundColor); const KateTextLayout lineLayout = m_viewInternal->cache()->viewLine(z); int realLine = lineLayout.line(); if (realLine < 0) { // We have reached the end of the document, just paint background continue; } // icon pane if (m_iconBorderOn) { p.setPen(m_view->renderer()->config()->separatorColor()); p.setBrush(m_view->renderer()->config()->separatorColor()); p.drawLine(lnX + m_iconAreaWidth, y, lnX + m_iconAreaWidth, y + h); const uint mrk(m_doc->mark(realLine)); // call only once if (mrk && lineLayout.startCol() == 0) { for (uint bit = 0; bit < 32; bit++) { MarkInterface::MarkTypes markType = (MarkInterface::MarkTypes)(1 << bit); if (mrk & markType) { - QPixmap px_mark(m_doc->markPixmap(markType)); - px_mark.setDevicePixelRatio(devicePixelRatioF()); + const QIcon markIcon = m_doc->markIcon(markType); - if (!px_mark.isNull() && h > 0 && m_iconAreaWidth > 0) { - // scale up to a usable size - const int s = qMin(m_iconAreaWidth * devicePixelRatioF(), h * devicePixelRatioF()) - 2; - px_mark = px_mark.scaled(s, s, Qt::KeepAspectRatio, Qt::SmoothTransformation); + if (!markIcon.isNull() && h > 0 && m_iconAreaWidth > 0) { + const int s = qMin(m_iconAreaWidth, static_cast(h)) - 2; // center the mark pixmap - int x_px = 0.5 * qMax(m_iconAreaWidth - (s / devicePixelRatioF()), 0.0); - int y_px = 0.5 * qMax(h - (s / devicePixelRatioF()), 0.0); + const int x_px = qMax(m_iconAreaWidth - s, 0) / 2; + const int y_px = qMax(static_cast(h) - s, 0) / 2; - p.drawPixmap(lnX + x_px, y + y_px, px_mark); + markIcon.paint(&p, lnX + x_px, y + y_px, s, s); } } } } lnX += m_iconAreaWidth + m_separatorWidth; if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, IconBorder)); } } // annotation information if (m_annotationBorderOn) { // Draw a border line between annotations and the line numbers p.setPen(lineNumberColor); p.setBrush(lineNumberColor); const qreal borderX = lnX + m_annotationAreaWidth + 0.5; p.drawLine(QPointF(borderX, y + 0.5), QPointF(borderX, y + h - 0.5)); if (model) { KTextEditor::StyleOptionAnnotationItem styleOption; initStyleOption(&styleOption); styleOption.rect.setRect(lnX, y, m_annotationAreaWidth, h); annotationGroupPositionState.nextLine(styleOption, z, realLine); m_annotationItemDelegate->paint(&p, styleOption, model, realLine); } lnX += m_annotationAreaWidth + m_separatorWidth; if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, AnnotationBorder)); } } // line number if (m_lineNumbersOn || m_dynWrapIndicatorsOn) { QColor usedLineNumberColor; const int distanceToCurrent = abs(realLine - static_cast(currentLine)); if (distanceToCurrent == 0) { usedLineNumberColor = m_view->renderer()->config()->currentLineNumberColor(); } else { usedLineNumberColor = lineNumberColor; } p.setPen(usedLineNumberColor); p.setBrush(usedLineNumberColor); if (lineLayout.startCol() == 0) { if (m_relLineNumbersOn) { if (distanceToCurrent == 0) { p.drawText(lnX + m_maxCharWidth / 2, y, m_lineNumberAreaWidth - m_maxCharWidth, h, Qt::TextDontClip | Qt::AlignLeft | Qt::AlignVCenter, QString::number(realLine + 1)); } else { p.drawText(lnX + m_maxCharWidth / 2, y, m_lineNumberAreaWidth - m_maxCharWidth, h, Qt::TextDontClip | Qt::AlignRight | Qt::AlignVCenter, QString::number(distanceToCurrent)); } if (m_updateRelLineNumbers) { m_updateRelLineNumbers = false; update(); } } else if (m_lineNumbersOn) { p.drawText(lnX + m_maxCharWidth / 2, y, m_lineNumberAreaWidth - m_maxCharWidth, h, Qt::TextDontClip | Qt::AlignRight | Qt::AlignVCenter, QString::number(realLine + 1)); } } else if (m_dynWrapIndicatorsOn) { p.drawText(lnX + m_maxCharWidth / 2, y, m_lineNumberAreaWidth - m_maxCharWidth, h, Qt::TextDontClip | Qt::AlignRight | Qt::AlignVCenter, m_dynWrapIndicatorChar); } lnX += m_lineNumberAreaWidth + m_separatorWidth; if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, LineNumbers)); } } // modified line system if (m_view->config()->lineModification() && !m_doc->url().isEmpty()) { const Kate::TextLine tl = m_doc->plainKateTextLine(realLine); if (tl->markedAsModified()) { p.fillRect(lnX, y, m_modAreaWidth, h, m_view->renderer()->config()->modifiedLineColor()); } else if (tl->markedAsSavedOnDisk()) { p.fillRect(lnX, y, m_modAreaWidth, h, m_view->renderer()->config()->savedLineColor()); } else { p.fillRect(lnX, y, m_modAreaWidth, h, iconBarColor); } lnX += m_modAreaWidth; // No m_separatorWidth if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, None)); } } // folding markers if (m_foldingMarkersOn) { const QColor foldingColor(m_view->renderer()->config()->foldingColor()); // possible additional folding highlighting if (m_foldingRange && m_foldingRange->overlapsLine(realLine)) { p.fillRect(lnX, y, m_foldingAreaWidth, h, foldingColor); } if (lineLayout.startCol() == 0) { QVector> startingRanges = m_view->textFolding().foldingRangesStartingOnLine(realLine); bool anyFolded = false; for (int i = 0; i < startingRanges.size(); ++i) { if (startingRanges[i].second & Kate::TextFolding::Folded) { anyFolded = true; } } const Kate::TextLine tl = m_doc->kateTextLine(realLine); if (!startingRanges.isEmpty() || tl->markedAsFoldingStart()) { if (anyFolded) { paintTriangle(p, foldingColor, lnX, y, m_foldingAreaWidth, h, false); } else { // Don't try to use currentLineNumberColor, the folded icon gets also not highligted paintTriangle(p, lineNumberColor, lnX, y, m_foldingAreaWidth, h, true); } } } lnX += m_foldingAreaWidth; if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, FoldingMarkers)); } } if (m_updatePositionToArea) { m_updatePositionToArea = false; // Don't forget our "text-stuck-to-border" protector lnX += m_separatorWidth; m_positionToArea.append(AreaPosition(lnX, None)); // Now that we know our needed space, ensure we are painted properly updateGeometry(); update(); return; } } } KateIconBorder::BorderArea KateIconBorder::positionToArea(const QPoint &p) const { for (int i = 0; i < m_positionToArea.size(); ++i) { if (p.x() <= m_positionToArea.at(i).first) { return m_positionToArea.at(i).second; } } return None; } void KateIconBorder::mousePressEvent(QMouseEvent *e) { const KateTextLayout &t = m_viewInternal->yToKateTextLayout(e->y()); if (t.isValid()) { m_lastClickedLine = t.line(); const auto area = positionToArea(e->pos()); // IconBorder and AnnotationBorder have their own behavior; don't forward to view if (area != IconBorder && area != AnnotationBorder) { const auto pos = QPoint(0, e->y()); if (area == LineNumbers && e->button() == Qt::LeftButton && !(e->modifiers() & Qt::ShiftModifier)) { // setup view so the following mousePressEvent will select the line m_viewInternal->beginSelectLine(pos); } QMouseEvent forward(QEvent::MouseButtonPress, pos, e->button(), e->buttons(), e->modifiers()); m_viewInternal->mousePressEvent(&forward); } return e->accept(); } QWidget::mousePressEvent(e); } void KateIconBorder::highlightFoldingDelayed(int line) { if ((line == m_currentLine) || (line >= m_doc->buffer().lines())) { return; } m_currentLine = line; if (m_foldingRange) { // We are for a while in the folding area, no need for delay highlightFolding(); } else if (!m_antiFlickerTimer.isActive()) { m_antiFlickerTimer.start(); } } void KateIconBorder::highlightFolding() { /** * compute to which folding range we belong * FIXME: optimize + perhaps have some better threshold or use timers! */ KTextEditor::Range newRange = KTextEditor::Range::invalid(); for (int line = m_currentLine; line >= qMax(0, m_currentLine - 1024); --line) { /** * try if we have folding range from that line, should be fast per call */ KTextEditor::Range foldingRange = m_doc->buffer().computeFoldingRangeForStartLine(line); if (!foldingRange.isValid()) { continue; } /** * does the range reach us? */ if (foldingRange.overlapsLine(m_currentLine)) { newRange = foldingRange; break; } } if (newRange.isValid() && m_foldingRange && *m_foldingRange == newRange) { // new range equals the old one, nothing to do. return; } // the ranges differ, delete the old, if it exists delete m_foldingRange; m_foldingRange = nullptr; // New range, new preview! delete m_foldingPreview; bool showPreview = false; if (newRange.isValid()) { // When next line is not visible we have a folded range, only then we want a preview! showPreview = !m_view->textFolding().isLineVisible(newRange.start().line() + 1); // qCDebug(LOG_KTE) << "new folding hl-range:" << newRange; m_foldingRange = m_doc->newMovingRange(newRange, KTextEditor::MovingRange::ExpandRight); KTextEditor::Attribute::Ptr attr(new KTextEditor::Attribute()); /** * create highlighting color * we avoid alpha as overpainting leads to ugly lines (https://bugreports.qt.io/browse/QTBUG-66036) */ attr->setBackground(QBrush(m_view->renderer()->config()->foldingColor())); m_foldingRange->setView(m_view); // use z depth defined in moving ranges interface m_foldingRange->setZDepth(-100.0); m_foldingRange->setAttribute(attr); } // show text preview, if a folded region starts here... // ...but only when main window is active (#392396) const bool isWindowActive = !window() || window()->isActiveWindow(); if (showPreview && m_view->config()->foldingPreview() && isWindowActive) { m_foldingPreview = new KateTextPreview(m_view, this); m_foldingPreview->setAttribute(Qt::WA_ShowWithoutActivating); m_foldingPreview->setFrameStyle(QFrame::StyledPanel); // Calc how many lines can be displayed in the popup const int lineHeight = m_view->renderer()->lineHeight(); const int foldingStartLine = m_foldingRange->start().line(); // FIXME Is there really no easier way to find lineInDisplay? const QPoint pos = m_viewInternal->mapFrom(m_view, m_view->cursorToCoordinate(KTextEditor::Cursor(foldingStartLine, 0))); const int lineInDisplay = pos.y() / lineHeight; // Allow slightly overpainting of the view bottom to proper cover all lines const int extra = (m_viewInternal->height() % lineHeight) > (lineHeight * 0.6) ? 1 : 0; const int lineCount = qMin(m_foldingRange->numberOfLines() + 1, m_viewInternal->linesDisplayed() - lineInDisplay + extra); m_foldingPreview->resize(m_viewInternal->width(), lineCount * lineHeight + 2 * m_foldingPreview->frameWidth()); const int xGlobal = mapToGlobal(QPoint(width(), 0)).x(); const int yGlobal = m_view->mapToGlobal(m_view->cursorToCoordinate(KTextEditor::Cursor(foldingStartLine, 0))).y(); m_foldingPreview->move(QPoint(xGlobal, yGlobal) - m_foldingPreview->contentsRect().topLeft()); m_foldingPreview->setLine(foldingStartLine); m_foldingPreview->setCenterView(false); m_foldingPreview->setShowFoldedLines(true); m_foldingPreview->raise(); m_foldingPreview->show(); } } void KateIconBorder::hideFolding() { if (m_antiFlickerTimer.isActive()) { m_antiFlickerTimer.stop(); } m_currentLine = -1; delete m_foldingRange; m_foldingRange = nullptr; delete m_foldingPreview; } void KateIconBorder::leaveEvent(QEvent *event) { hideFolding(); removeAnnotationHovering(); QWidget::leaveEvent(event); } void KateIconBorder::mouseMoveEvent(QMouseEvent *e) { const KateTextLayout &t = m_viewInternal->yToKateTextLayout(e->y()); if (!t.isValid()) { // Cleanup everything which may be shown removeAnnotationHovering(); hideFolding(); } else { const BorderArea area = positionToArea(e->pos()); if (area == FoldingMarkers) { highlightFoldingDelayed(t.line()); } else { hideFolding(); } if (area == AnnotationBorder) { KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel(); if (model) { m_hoveredAnnotationGroupIdentifier = model->data(t.line(), (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole).toString(); const QPoint viewRelativePos = m_view->mapFromGlobal(e->globalPos()); QHelpEvent helpEvent(QEvent::ToolTip, viewRelativePos, e->globalPos()); KTextEditor::StyleOptionAnnotationItem styleOption; initStyleOption(&styleOption); styleOption.rect = annotationLineRectInView(t.line()); setStyleOptionLineData(&styleOption, e->y(), t.line(), model, m_hoveredAnnotationGroupIdentifier); m_annotationItemDelegate->helpEvent(&helpEvent, m_view, styleOption, model, t.line()); QTimer::singleShot(0, this, SLOT(update())); } } else { if (area == IconBorder) { m_doc->requestMarkTooltip(t.line(), e->globalPos()); } m_hoveredAnnotationGroupIdentifier.clear(); QTimer::singleShot(0, this, SLOT(update())); } if (area != IconBorder) { QPoint p = m_viewInternal->mapFromGlobal(e->globalPos()); QMouseEvent forward(QEvent::MouseMove, p, e->button(), e->buttons(), e->modifiers()); m_viewInternal->mouseMoveEvent(&forward); } } QWidget::mouseMoveEvent(e); } void KateIconBorder::mouseReleaseEvent(QMouseEvent *e) { const int cursorOnLine = m_viewInternal->yToKateTextLayout(e->y()).line(); if (cursorOnLine == m_lastClickedLine && cursorOnLine >= 0 && cursorOnLine <= m_doc->lastLine()) { const BorderArea area = positionToArea(e->pos()); if (area == IconBorder) { if (e->button() == Qt::LeftButton) { if (!m_doc->handleMarkClick(cursorOnLine)) { KateViewConfig *config = m_view->config(); const uint editBits = m_doc->editableMarks(); // is the default or the only editable mark const uint singleMark = qPopulationCount(editBits) > 1 ? editBits & config->defaultMarkType() : editBits; if (singleMark) { if (m_doc->mark(cursorOnLine) & singleMark) { m_doc->removeMark(cursorOnLine, singleMark); } else { m_doc->addMark(cursorOnLine, singleMark); } } else if (config->allowMarkMenu()) { showMarkMenu(cursorOnLine, QCursor::pos()); } } } else if (e->button() == Qt::RightButton) { showMarkMenu(cursorOnLine, QCursor::pos()); } } if (area == FoldingMarkers) { // Prefer the highlighted range over the exact clicked line const int lineToToggle = m_foldingRange ? m_foldingRange->toRange().start().line() : cursorOnLine; if (e->button() == Qt::LeftButton) { m_view->toggleFoldingOfLine(lineToToggle); } else if (e->button() == Qt::RightButton) { m_view->toggleFoldingsInRange(lineToToggle); } delete m_foldingPreview; } if (area == AnnotationBorder) { const bool singleClick = style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this); if (e->button() == Qt::LeftButton && singleClick) { emit m_view->annotationActivated(m_view, cursorOnLine); } else if (e->button() == Qt::RightButton) { showAnnotationMenu(cursorOnLine, e->globalPos()); } } } QMouseEvent forward(QEvent::MouseButtonRelease, QPoint(0, e->y()), e->button(), e->buttons(), e->modifiers()); m_viewInternal->mouseReleaseEvent(&forward); } void KateIconBorder::mouseDoubleClickEvent(QMouseEvent *e) { int cursorOnLine = m_viewInternal->yToKateTextLayout(e->y()).line(); if (cursorOnLine == m_lastClickedLine && cursorOnLine <= m_doc->lastLine()) { const BorderArea area = positionToArea(e->pos()); const bool singleClick = style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this); if (area == AnnotationBorder && !singleClick) { emit m_view->annotationActivated(m_view, cursorOnLine); } } QMouseEvent forward(QEvent::MouseButtonDblClick, QPoint(0, e->y()), e->button(), e->buttons(), e->modifiers()); m_viewInternal->mouseDoubleClickEvent(&forward); } void KateIconBorder::wheelEvent(QWheelEvent *e) { QCoreApplication::sendEvent(m_viewInternal, e); } void KateIconBorder::showMarkMenu(uint line, const QPoint &pos) { if (m_doc->handleMarkContextMenu(line, pos)) { return; } if (!m_view->config()->allowMarkMenu()) { return; } QMenu markMenu; QMenu selectDefaultMark; auto selectDefaultMarkActionGroup = new QActionGroup(&selectDefaultMark); QVector vec(33); int i = 1; for (uint bit = 0; bit < 32; bit++) { MarkInterface::MarkTypes markType = (MarkInterface::MarkTypes)(1 << bit); if (!(m_doc->editableMarks() & markType)) { continue; } QAction *mA; QAction *dMA; - const QPixmap icon = m_doc->markPixmap(markType); + const QIcon icon = m_doc->markIcon(markType); if (!m_doc->markDescription(markType).isEmpty()) { mA = markMenu.addAction(icon, m_doc->markDescription(markType)); dMA = selectDefaultMark.addAction(icon, m_doc->markDescription(markType)); } else { mA = markMenu.addAction(icon, i18n("Mark Type %1", bit + 1)); dMA = selectDefaultMark.addAction(icon, i18n("Mark Type %1", bit + 1)); } selectDefaultMarkActionGroup->addAction(dMA); mA->setData(i); mA->setCheckable(true); dMA->setData(i + 100); dMA->setCheckable(true); if (m_doc->mark(line) & markType) { mA->setChecked(true); } if (markType & KateViewConfig::global()->defaultMarkType()) { dMA->setChecked(true); } vec[i++] = markType; } if (markMenu.actions().count() == 0) { return; } if (markMenu.actions().count() > 1) { markMenu.addAction(i18n("Set Default Mark Type"))->setMenu(&selectDefaultMark); } QAction *rA = markMenu.exec(pos); if (!rA) { return; } int result = rA->data().toInt(); if (result > 100) { KateViewConfig::global()->setValue(KateViewConfig::DefaultMarkType, vec[result - 100]); } else { MarkInterface::MarkTypes markType = (MarkInterface::MarkTypes)vec[result]; if (m_doc->mark(line) & markType) { m_doc->removeMark(line, markType); } else { m_doc->addMark(line, markType); } } } KTextEditor::AbstractAnnotationItemDelegate *KateIconBorder::annotationItemDelegate() const { return m_annotationItemDelegate; } void KateIconBorder::setAnnotationItemDelegate(KTextEditor::AbstractAnnotationItemDelegate *delegate) { if (delegate == m_annotationItemDelegate) { return; } // reset to default, but already on that? if (!delegate && m_isDefaultAnnotationItemDelegate) { // nothing to do return; } // make sure the tooltip is hidden if (m_annotationBorderOn && !m_hoveredAnnotationGroupIdentifier.isEmpty()) { m_hoveredAnnotationGroupIdentifier.clear(); hideAnnotationTooltip(); } disconnect(m_annotationItemDelegate, &AbstractAnnotationItemDelegate::sizeHintChanged, this, &KateIconBorder::updateAnnotationBorderWidth); if (!m_isDefaultAnnotationItemDelegate) { disconnect(m_annotationItemDelegate, &QObject::destroyed, this, &KateIconBorder::handleDestroyedAnnotationItemDelegate); } if (!delegate) { // reset to a default delegate m_annotationItemDelegate = new KateAnnotationItemDelegate(m_viewInternal, this); m_isDefaultAnnotationItemDelegate = true; } else { // drop any default delegate if (m_isDefaultAnnotationItemDelegate) { delete m_annotationItemDelegate; m_isDefaultAnnotationItemDelegate = false; } m_annotationItemDelegate = delegate; // catch delegate being destroyed connect(delegate, &QObject::destroyed, this, &KateIconBorder::handleDestroyedAnnotationItemDelegate); } connect(m_annotationItemDelegate, &AbstractAnnotationItemDelegate::sizeHintChanged, this, &KateIconBorder::updateAnnotationBorderWidth); if (m_annotationBorderOn) { updateGeometry(); QTimer::singleShot(0, this, SLOT(update())); } } void KateIconBorder::handleDestroyedAnnotationItemDelegate() { setAnnotationItemDelegate(nullptr); } void KateIconBorder::initStyleOption(KTextEditor::StyleOptionAnnotationItem *styleOption) const { styleOption->initFrom(this); styleOption->view = m_view; styleOption->decorationSize = QSize(m_iconAreaWidth, m_iconAreaWidth); styleOption->contentFontMetrics = m_view->renderer()->currentFontMetrics(); } void KateIconBorder::setStyleOptionLineData(KTextEditor::StyleOptionAnnotationItem *styleOption, int y, int realLine, const KTextEditor::AnnotationModel *model, const QString &annotationGroupIdentifier) const { // calculate rendered displayed line const uint h = m_view->renderer()->lineHeight(); const uint z = (y / h); KateAnnotationGroupPositionState annotationGroupPositionState(m_viewInternal, model, annotationGroupIdentifier, z, true); annotationGroupPositionState.nextLine(*styleOption, z, realLine); } QRect KateIconBorder::annotationLineRectInView(int line) const { int x = 0; if (m_iconBorderOn) { x += m_iconAreaWidth + 2; } const int y = m_view->m_viewInternal->lineToY(line); return QRect(x, y, m_annotationAreaWidth, m_view->renderer()->lineHeight()); } void KateIconBorder::updateAnnotationLine(int line) { // TODO: why has the default value been 8, where is that magic number from? int width = 8; KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel(); if (model) { KTextEditor::StyleOptionAnnotationItem styleOption; initStyleOption(&styleOption); width = m_annotationItemDelegate->sizeHint(styleOption, model, line).width(); } if (width > m_annotationAreaWidth) { m_annotationAreaWidth = width; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } } void KateIconBorder::showAnnotationMenu(int line, const QPoint &pos) { QMenu menu; QAction a(i18n("Disable Annotation Bar"), &menu); a.setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); menu.addAction(&a); emit m_view->annotationContextMenuAboutToShow(m_view, &menu, line); if (menu.exec(pos) == &a) { m_view->setAnnotationBorderVisible(false); } } void KateIconBorder::hideAnnotationTooltip() { m_annotationItemDelegate->hideTooltip(m_view); } void KateIconBorder::updateAnnotationBorderWidth() { calcAnnotationBorderWidth(); m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::calcAnnotationBorderWidth() { // TODO: another magic number, not matching the one in updateAnnotationLine() m_annotationAreaWidth = 6; KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel(); if (model) { KTextEditor::StyleOptionAnnotationItem styleOption; initStyleOption(&styleOption); const int lineCount = m_view->doc()->lines(); if (lineCount > 0) { const int checkedLineCount = m_hasUniformAnnotationItemSizes ? 1 : lineCount; for (int i = 0; i < checkedLineCount; ++i) { const int curwidth = m_annotationItemDelegate->sizeHint(styleOption, model, i).width(); if (curwidth > m_annotationAreaWidth) { m_annotationAreaWidth = curwidth; } } } } } void KateIconBorder::annotationModelChanged(KTextEditor::AnnotationModel *oldmodel, KTextEditor::AnnotationModel *newmodel) { if (oldmodel) { oldmodel->disconnect(this); } if (newmodel) { connect(newmodel, SIGNAL(reset()), this, SLOT(updateAnnotationBorderWidth())); connect(newmodel, SIGNAL(lineChanged(int)), this, SLOT(updateAnnotationLine(int))); } updateAnnotationBorderWidth(); } void KateIconBorder::displayRangeChanged() { hideFolding(); removeAnnotationHovering(); } // END KateIconBorder // BEGIN KateViewEncodingAction // According to https://www.iana.org/assignments/ianacharset-mib/ianacharset-mib // the default/unknown mib value is 2. #define MIB_DEFAULT 2 bool lessThanAction(KSelectAction *a, KSelectAction *b) { return a->text() < b->text(); } void KateViewEncodingAction::Private::init() { QList actions; q->setToolBarMode(MenuMode); int i; const auto encodingsByScript = KCharsets::charsets()->encodingsByScript(); for (const QStringList &encodingsForScript : encodingsByScript) { KSelectAction *tmp = new KSelectAction(encodingsForScript.at(0), q); for (i = 1; i < encodingsForScript.size(); ++i) { tmp->addAction(encodingsForScript.at(i)); } q->connect(tmp, SIGNAL(triggered(QAction *)), q, SLOT(_k_subActionTriggered(QAction *))); // tmp->setCheckable(true); actions << tmp; } std::sort(actions.begin(), actions.end(), lessThanAction); for (KSelectAction *action : qAsConst(actions)) { q->addAction(action); } } void KateViewEncodingAction::Private::_k_subActionTriggered(QAction *action) { if (currentSubAction == action) { return; } currentSubAction = action; bool ok = false; int mib = q->mibForName(action->text(), &ok); if (ok) { emit q->KSelectAction::triggered(action->text()); emit q->triggered(q->codecForMib(mib)); } } KateViewEncodingAction::KateViewEncodingAction(KTextEditor::DocumentPrivate *_doc, KTextEditor::ViewPrivate *_view, const QString &text, QObject *parent, bool saveAsMode) : KSelectAction(text, parent) , doc(_doc) , view(_view) , d(new Private(this)) , m_saveAsMode(saveAsMode) { d->init(); connect(menu(), SIGNAL(aboutToShow()), this, SLOT(slotAboutToShow())); connect(this, SIGNAL(triggered(QString)), this, SLOT(setEncoding(QString))); } KateViewEncodingAction::~KateViewEncodingAction() { delete d; } void KateViewEncodingAction::slotAboutToShow() { setCurrentCodec(doc->config()->encoding()); } void KateViewEncodingAction::setEncoding(const QString &e) { /** * in save as mode => trigger saveAs */ if (m_saveAsMode) { doc->documentSaveAsWithEncoding(e); return; } /** * else switch encoding */ doc->userSetEncodingForNextReload(); doc->setEncoding(e); view->reloadFile(); } int KateViewEncodingAction::mibForName(const QString &codecName, bool *ok) const { // FIXME logic is good but code is ugly bool success = false; int mib = MIB_DEFAULT; KCharsets *charsets = KCharsets::charsets(); QTextCodec *codec = charsets->codecForName(codecName, success); if (!success) { // Maybe we got a description name instead codec = charsets->codecForName(charsets->encodingForName(codecName), success); } if (codec) { mib = codec->mibEnum(); } if (ok) { *ok = success; } if (success) { return mib; } qCWarning(LOG_KTE) << "Invalid codec name: " << codecName; return MIB_DEFAULT; } QTextCodec *KateViewEncodingAction::codecForMib(int mib) const { if (mib == MIB_DEFAULT) { // FIXME offer to change the default codec return QTextCodec::codecForLocale(); } else { return QTextCodec::codecForMib(mib); } } QTextCodec *KateViewEncodingAction::currentCodec() const { return codecForMib(currentCodecMib()); } bool KateViewEncodingAction::setCurrentCodec(QTextCodec *codec) { disconnect(this, SIGNAL(triggered(QString)), this, SLOT(setEncoding(QString))); int i, j; for (i = 0; i < actions().size(); ++i) { if (actions().at(i)->menu()) { for (j = 0; j < actions().at(i)->menu()->actions().size(); ++j) { if (!j && !actions().at(i)->menu()->actions().at(j)->data().isNull()) { continue; } if (actions().at(i)->menu()->actions().at(j)->isSeparator()) { continue; } if (codec == KCharsets::charsets()->codecForName(actions().at(i)->menu()->actions().at(j)->text())) { d->currentSubAction = actions().at(i)->menu()->actions().at(j); d->currentSubAction->setChecked(true); } else { actions().at(i)->menu()->actions().at(j)->setChecked(false); } } } } connect(this, SIGNAL(triggered(QString)), this, SLOT(setEncoding(QString))); return true; } QString KateViewEncodingAction::currentCodecName() const { return d->currentSubAction->text(); } bool KateViewEncodingAction::setCurrentCodec(const QString &codecName) { return setCurrentCodec(KCharsets::charsets()->codecForName(codecName)); } int KateViewEncodingAction::currentCodecMib() const { return mibForName(currentCodecName()); } bool KateViewEncodingAction::setCurrentCodec(int mib) { return setCurrentCodec(codecForMib(mib)); } // END KateViewEncodingAction // BEGIN KateViewBar related classes KateViewBarWidget::KateViewBarWidget(bool addCloseButton, QWidget *parent) : QWidget(parent) , m_viewBar(nullptr) { QHBoxLayout *layout = new QHBoxLayout(this); // NOTE: Here be cosmetics. layout->setContentsMargins(0, 0, 0, 0); // hide button if (addCloseButton) { QToolButton *hideButton = new QToolButton(this); hideButton->setAutoRaise(true); hideButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); connect(hideButton, SIGNAL(clicked()), SIGNAL(hideMe())); layout->addWidget(hideButton); layout->setAlignment(hideButton, Qt::AlignLeft | Qt::AlignTop); } // widget to be used as parent for the real content m_centralWidget = new QWidget(this); layout->addWidget(m_centralWidget); setLayout(layout); setFocusProxy(m_centralWidget); } KateViewBar::KateViewBar(bool external, QWidget *parent, KTextEditor::ViewPrivate *view) : QWidget(parent) , m_external(external) , m_view(view) , m_permanentBarWidget(nullptr) { m_layout = new QVBoxLayout(this); m_stack = new QStackedWidget(this); m_layout->addWidget(m_stack); m_layout->setContentsMargins(0, 0, 0, 0); m_stack->hide(); hide(); } void KateViewBar::addBarWidget(KateViewBarWidget *newBarWidget) { // just ignore additional adds for already existing widgets if (hasBarWidget(newBarWidget)) { return; } // add new widget, invisible... newBarWidget->hide(); m_stack->addWidget(newBarWidget); newBarWidget->setAssociatedViewBar(this); connect(newBarWidget, SIGNAL(hideMe()), SLOT(hideCurrentBarWidget())); } void KateViewBar::removeBarWidget(KateViewBarWidget *barWidget) { // remove only if there if (!hasBarWidget(barWidget)) { return; } m_stack->removeWidget(barWidget); barWidget->setAssociatedViewBar(nullptr); barWidget->hide(); disconnect(barWidget, nullptr, this, nullptr); } void KateViewBar::addPermanentBarWidget(KateViewBarWidget *barWidget) { Q_ASSERT(barWidget); Q_ASSERT(!m_permanentBarWidget); m_stack->addWidget(barWidget); m_stack->setCurrentWidget(barWidget); m_stack->show(); m_permanentBarWidget = barWidget; m_permanentBarWidget->show(); setViewBarVisible(true); } void KateViewBar::removePermanentBarWidget(KateViewBarWidget *barWidget) { Q_ASSERT(m_permanentBarWidget == barWidget); Q_UNUSED(barWidget); const bool hideBar = m_stack->currentWidget() == m_permanentBarWidget; m_permanentBarWidget->hide(); m_stack->removeWidget(m_permanentBarWidget); m_permanentBarWidget = nullptr; if (hideBar) { m_stack->hide(); setViewBarVisible(false); } } bool KateViewBar::hasPermanentWidget(KateViewBarWidget *barWidget) const { return (m_permanentBarWidget == barWidget); } void KateViewBar::showBarWidget(KateViewBarWidget *barWidget) { Q_ASSERT(barWidget != nullptr); if (barWidget != qobject_cast(m_stack->currentWidget())) { hideCurrentBarWidget(); } // raise correct widget m_stack->setCurrentWidget(barWidget); barWidget->show(); barWidget->setFocus(Qt::ShortcutFocusReason); m_stack->show(); setViewBarVisible(true); } bool KateViewBar::hasBarWidget(KateViewBarWidget *barWidget) const { return m_stack->indexOf(barWidget) != -1; } void KateViewBar::hideCurrentBarWidget() { KateViewBarWidget *current = qobject_cast(m_stack->currentWidget()); if (current) { current->closed(); } // if we have any permanent widget, make it visible again if (m_permanentBarWidget) { m_stack->setCurrentWidget(m_permanentBarWidget); } else { // else: hide the bar m_stack->hide(); setViewBarVisible(false); } m_view->setFocus(); } void KateViewBar::setViewBarVisible(bool visible) { if (m_external) { if (visible) { m_view->mainWindow()->showViewBar(m_view); } else { m_view->mainWindow()->hideViewBar(m_view); } } else { setVisible(visible); } } bool KateViewBar::hiddenOrPermanent() const { KateViewBarWidget *current = qobject_cast(m_stack->currentWidget()); if (!isVisible() || (m_permanentBarWidget && m_permanentBarWidget == current)) { return true; } return false; } void KateViewBar::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { hideCurrentBarWidget(); return; } QWidget::keyPressEvent(event); } void KateViewBar::hideEvent(QHideEvent *event) { Q_UNUSED(event); // if (!event->spontaneous()) // m_view->setFocus(); } // END KateViewBar related classes KatePasteMenu::KatePasteMenu(const QString &text, KTextEditor::ViewPrivate *view) : KActionMenu(text, view) , m_view(view) { connect(menu(), SIGNAL(aboutToShow()), this, SLOT(slotAboutToShow())); } void KatePasteMenu::slotAboutToShow() { menu()->clear(); /** * insert complete paste history */ int i = 0; for (const QString &text : KTextEditor::EditorPrivate::self()->clipboardHistory()) { /** * get text for the menu ;) */ QString leftPart = (text.size() > 48) ? (text.left(48) + QLatin1String("...")) : text; QAction *a = menu()->addAction(leftPart.replace(QLatin1Char('\n'), QLatin1Char(' ')), this, SLOT(paste())); a->setData(i++); } } void KatePasteMenu::paste() { if (!sender()) { return; } QAction *action = qobject_cast(sender()); if (!action) { return; } // get index int i = action->data().toInt(); if (i >= KTextEditor::EditorPrivate::self()->clipboardHistory().size()) { return; } // paste m_view->paste(&KTextEditor::EditorPrivate::self()->clipboardHistory()[i]); }