diff --git a/src/completion/katecompletionmodel.cpp b/src/completion/katecompletionmodel.cpp index e0682e5b..3af4f905 100644 --- a/src/completion/katecompletionmodel.cpp +++ b/src/completion/katecompletionmodel.cpp @@ -1,2432 +1,2432 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2005-2006 Hamish Rodda * Copyright (C) 2007-2008 David Nolden * * 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 "katecompletionmodel.h" #include "katecompletionwidget.h" #include "katecompletiontree.h" #include "katecompletiondelegate.h" #include "kateargumenthintmodel.h" #include "kateview.h" #include "katerenderer.h" #include "kateconfig.h" #include #include "katepartdebug.h" #include #include #include #include #include #include using namespace KTextEditor; ///A helper-class for handling completion-models with hierarchical grouping/optimization class HierarchicalModelHandler { public: explicit HierarchicalModelHandler(CodeCompletionModel *model); void addValue(CodeCompletionModel::ExtraItemDataRoles role, const QVariant &value); //Walks the index upwards and collects all defined completion-roles on the way void collectRoles(const QModelIndex &index); void takeRole(const QModelIndex &index); CodeCompletionModel *model() const; //Assumes that index is a sub-index of the indices where role-values were taken QVariant getData(CodeCompletionModel::ExtraItemDataRoles role, const QModelIndex &index) const; bool hasHierarchicalRoles() const; int inheritanceDepth(const QModelIndex &i) const; QString customGroup() const { return m_customGroup; } int customGroupingKey() const { return m_groupSortingKey; } private: typedef QMap RoleMap; RoleMap m_roleValues; QString m_customGroup; int m_groupSortingKey; CodeCompletionModel *m_model; }; CodeCompletionModel *HierarchicalModelHandler::model() const { return m_model; } bool HierarchicalModelHandler::hasHierarchicalRoles() const { return !m_roleValues.isEmpty(); } void HierarchicalModelHandler::collectRoles(const QModelIndex &index) { if (index.parent().isValid()) { collectRoles(index.parent()); } if (m_model->rowCount(index) != 0) { takeRole(index); } } int HierarchicalModelHandler::inheritanceDepth(const QModelIndex &i) const { return getData(CodeCompletionModel::InheritanceDepth, i).toInt(); } void HierarchicalModelHandler::takeRole(const QModelIndex &index) { QVariant v = index.data(CodeCompletionModel::GroupRole); if (v.isValid() && v.canConvert(QVariant::Int)) { QVariant value = index.data(v.toInt()); if (v.toInt() == Qt::DisplayRole) { m_customGroup = index.data(Qt::DisplayRole).toString(); QVariant sortingKey = index.data(CodeCompletionModel::InheritanceDepth); if (sortingKey.canConvert(QVariant::Int)) { m_groupSortingKey = sortingKey.toInt(); } } else { m_roleValues[(CodeCompletionModel::ExtraItemDataRoles)v.toInt()] = value; } } else { qCDebug(LOG_KTE) << "Did not return valid GroupRole in hierarchical completion-model"; } } QVariant HierarchicalModelHandler::getData(CodeCompletionModel::ExtraItemDataRoles role, const QModelIndex &index) const { RoleMap::const_iterator it = m_roleValues.find(role); if (it != m_roleValues.end()) { return *it; } else { return index.data(role); } } HierarchicalModelHandler::HierarchicalModelHandler(CodeCompletionModel *model) : m_groupSortingKey(-1), m_model(model) { } void HierarchicalModelHandler::addValue(CodeCompletionModel::ExtraItemDataRoles role, const QVariant &value) { m_roleValues[role] = value; } KateCompletionModel::KateCompletionModel(KateCompletionWidget *parent) : ExpandingWidgetModel(parent) , m_ungrouped(new Group({}, 0, this)) , m_argumentHints(new Group(i18n("Argument-hints"), -1, this)) , m_bestMatches(new Group(i18n("Best matches"), BestMatchesProperty, this)) , m_filterAttributes(KTextEditor::CodeCompletionModel::NoProperty) { m_emptyGroups.append(m_ungrouped); m_emptyGroups.append(m_argumentHints); m_emptyGroups.append(m_bestMatches); m_updateBestMatchesTimer = new QTimer(this); m_updateBestMatchesTimer->setSingleShot(true); connect(m_updateBestMatchesTimer, SIGNAL(timeout()), this, SLOT(updateBestMatches())); m_groupHash.insert(0, m_ungrouped); m_groupHash.insert(-1, m_argumentHints); m_groupHash.insert(BestMatchesProperty, m_argumentHints); } KateCompletionModel::~KateCompletionModel() { clearCompletionModels(); delete m_argumentHints; delete m_ungrouped; delete m_bestMatches; } QTreeView *KateCompletionModel::treeView() const { return view()->completionWidget()->treeView(); } QVariant KateCompletionModel::data(const QModelIndex &index, int role) const { if (!hasCompletionModel() || !index.isValid()) { return QVariant(); } if (role == Qt::DecorationRole && index.column() == KTextEditor::CodeCompletionModel::Prefix && isExpandable(index)) { cacheIcons(); if (!isExpanded(index)) { return QVariant(m_collapsedIcon); } else { return QVariant(m_expandedIcon); } } //groupOfParent returns a group when the index is a member of that group, but not the group head/label. if (!hasGroups() || groupOfParent(index)) { if ( role == Qt::TextAlignmentRole ) { if (isColumnMergingEnabled() && !m_columnMerges.isEmpty()) { int c = 0; foreach (const QList &list, m_columnMerges) { if (index.column() < c + list.size()) { c += list.size(); continue; } else if (list.count() == 1 && list.first() == CodeCompletionModel::Scope) { return Qt::AlignRight; } else { return QVariant(); } } } else if ((!isColumnMergingEnabled() || m_columnMerges.isEmpty()) && index.column() == CodeCompletionModel::Scope) { return Qt::AlignRight; } } // Merge text for column merging if (role == Qt::DisplayRole && !m_columnMerges.isEmpty() && isColumnMergingEnabled()) { QString text; foreach (int column, m_columnMerges[index.column()]) { QModelIndex sourceIndex = mapToSource(createIndex(index.row(), column, index.internalPointer())); text.append(sourceIndex.data(role).toString()); } return text; } if (role == CodeCompletionModel::HighlightingMethod) { //Return that we are doing custom-highlighting of one of the sub-strings does it. Unfortunately internal highlighting does not work for the other substrings. foreach (int column, m_columnMerges[index.column()]) { QModelIndex sourceIndex = mapToSource(createIndex(index.row(), column, index.internalPointer())); QVariant method = sourceIndex.data(CodeCompletionModel::HighlightingMethod); if (method.type() == QVariant::Int && method.toInt() == CodeCompletionModel::CustomHighlighting) { return QVariant(CodeCompletionModel::CustomHighlighting); } } return QVariant(); } if (role == CodeCompletionModel::CustomHighlight) { //Merge custom highlighting if multiple columns were merged QStringList strings; //Collect strings foreach (int column, m_columnMerges[index.column()]) { strings << mapToSource(createIndex(index.row(), column, index.internalPointer())).data(Qt::DisplayRole).toString(); } QList highlights; //Collect custom-highlightings foreach (int column, m_columnMerges[index.column()]) { highlights << mapToSource(createIndex(index.row(), column, index.internalPointer())).data(CodeCompletionModel::CustomHighlight).toList(); } return mergeCustomHighlighting(strings, highlights, 0); } QVariant v = mapToSource(index).data(role); if (v.isValid()) { return v; } else { return ExpandingWidgetModel::data(index, role); } } //Returns a nonzero group if this index is the head of a group(A Label in the list) Group *g = groupForIndex(index); if (g && (!g->isEmpty)) { switch (role) { case Qt::DisplayRole: if (!index.column()) { return g->title; } break; case Qt::FontRole: if (!index.column()) { QFont f = view()->renderer()->config()->font(); f.setBold(true); return f; } break; case Qt::ForegroundRole: return QApplication::palette().toolTipText().color(); case Qt::BackgroundRole: return QApplication::palette().toolTipBase().color(); } } return QVariant(); } int KateCompletionModel::contextMatchQuality(const QModelIndex &index) const { if (!index.isValid()) { return 0; } Group *g = groupOfParent(index); if (!g || g->filtered.size() < index.row()) { return 0; } return contextMatchQuality(g->filtered[index.row()].sourceRow()); } int KateCompletionModel::contextMatchQuality(const ModelRow &source) const { QModelIndex realIndex = source.second; int bestMatch = -1; //Iterate through all argument-hints and find the best match-quality foreach (const Item &item, m_argumentHints->filtered) { const ModelRow &row(item.sourceRow()); if (realIndex.model() != row.first) { continue; //We can only match within the same source-model } QModelIndex hintIndex = row.second; QVariant depth = hintIndex.data(CodeCompletionModel::ArgumentHintDepth); if (!depth.isValid() || depth.type() != QVariant::Int || depth.toInt() != 1) { continue; //Only match completion-items to argument-hints of depth 1(the ones the item will be given to as argument) } hintIndex.data(CodeCompletionModel::SetMatchContext); QVariant matchQuality = realIndex.data(CodeCompletionModel::MatchQuality); if (matchQuality.isValid() && matchQuality.type() == QVariant::Int) { int m = matchQuality.toInt(); if (m > bestMatch) { bestMatch = m; } } } if (m_argumentHints->filtered.isEmpty()) { QVariant matchQuality = realIndex.data(CodeCompletionModel::MatchQuality); if (matchQuality.isValid() && matchQuality.type() == QVariant::Int) { int m = matchQuality.toInt(); if (m > bestMatch) { bestMatch = m; } } } return bestMatch; } Qt::ItemFlags KateCompletionModel::flags(const QModelIndex &index) const { if (!hasCompletionModel() || !index.isValid()) { return Qt::NoItemFlags; } if (!hasGroups() || groupOfParent(index)) { return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } return Qt::ItemIsEnabled; } KateCompletionWidget *KateCompletionModel::widget() const { return static_cast(QObject::parent()); } KTextEditor::ViewPrivate *KateCompletionModel::view() const { return widget()->view(); } void KateCompletionModel::setMatchCaseSensitivity(Qt::CaseSensitivity match_cs) { m_matchCaseSensitivity = match_cs; Q_ASSERT(m_exactMatchCaseSensitivity == m_matchCaseSensitivity || m_matchCaseSensitivity == Qt::CaseInsensitive); } void KateCompletionModel::setMatchCaseSensitivity(Qt::CaseSensitivity match_cs, Qt::CaseSensitivity exact_match_cs) { m_matchCaseSensitivity = match_cs; m_exactMatchCaseSensitivity = exact_match_cs; Q_ASSERT(m_exactMatchCaseSensitivity == m_matchCaseSensitivity || m_matchCaseSensitivity == Qt::CaseInsensitive); } int KateCompletionModel::columnCount(const QModelIndex &) const { return isColumnMergingEnabled() && !m_columnMerges.isEmpty() ? m_columnMerges.count() : KTextEditor::CodeCompletionModel::ColumnCount; } KateCompletionModel::ModelRow KateCompletionModel::modelRowPair(const QModelIndex &index) const { return qMakePair(static_cast(const_cast(index.model())), index); } bool KateCompletionModel::hasChildren(const QModelIndex &parent) const { if (!hasCompletionModel()) { return false; } if (!parent.isValid()) { if (hasGroups()) { return true; } return !m_ungrouped->filtered.isEmpty(); } if (parent.column() != 0) { return false; } if (!hasGroups()) { return false; } if (Group *g = groupForIndex(parent)) { return !g->filtered.isEmpty(); } return false; } QModelIndex KateCompletionModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || column < 0 || column >= columnCount(QModelIndex())) { return QModelIndex(); } if (parent.isValid() || !hasGroups()) { if (parent.isValid() && parent.column() != 0) { return QModelIndex(); } Group *g = groupForIndex(parent); if (!g) { return QModelIndex(); } if (row >= g->filtered.count()) { //qCWarning(LOG_KTE) << "Invalid index requested: row " << row << " beyond individual range in group " << g; return QModelIndex(); } //qCDebug(LOG_KTE) << "Returning index for child " << row << " of group " << g; return createIndex(row, column, g); } if (row >= m_rowTable.count()) { //qCWarning(LOG_KTE) << "Invalid index requested: row " << row << " beyond group range."; return QModelIndex(); } //qCDebug(LOG_KTE) << "Returning index for group " << m_rowTable[row]; return createIndex(row, column, quintptr(0)); } /*QModelIndex KateCompletionModel::sibling( int row, int column, const QModelIndex & index ) const { if (row < 0 || column < 0 || column >= columnCount(QModelIndex())) return QModelIndex(); if (!index.isValid()) { } if (Group* g = groupOfParent(index)) { if (row >= g->filtered.count()) return QModelIndex(); return createIndex(row, column, g); } if (hasGroups()) return QModelIndex(); if (row >= m_ungrouped->filtered.count()) return QModelIndex(); return createIndex(row, column, m_ungrouped); }*/ bool KateCompletionModel::hasIndex(int row, int column, const QModelIndex &parent) const { if (row < 0 || column < 0 || column >= columnCount(QModelIndex())) { return false; } if (parent.isValid() || !hasGroups()) { if (parent.isValid() && parent.column() != 0) { return false; } Group *g = groupForIndex(parent); if (row >= g->filtered.count()) { return false; } return true; } if (row >= m_rowTable.count()) { return false; } return true; } QModelIndex KateCompletionModel::indexForRow(Group *g, int row) const { if (row < 0 || row >= g->filtered.count()) { return QModelIndex(); } return createIndex(row, 0, g); } QModelIndex KateCompletionModel::indexForGroup(Group *g) const { if (!hasGroups()) { return QModelIndex(); } int row = m_rowTable.indexOf(g); if (row == -1) { return QModelIndex(); } return createIndex(row, 0, quintptr(0)); } void KateCompletionModel::clearGroups() { clearExpanding(); m_ungrouped->clear(); m_argumentHints->clear(); m_bestMatches->clear(); // Don't bother trying to work out where it is m_rowTable.removeAll(m_ungrouped); m_emptyGroups.removeAll(m_ungrouped); m_rowTable.removeAll(m_argumentHints); m_emptyGroups.removeAll(m_argumentHints); m_rowTable.removeAll(m_bestMatches); m_emptyGroups.removeAll(m_bestMatches); qDeleteAll(m_rowTable); qDeleteAll(m_emptyGroups); m_rowTable.clear(); m_emptyGroups.clear(); m_groupHash.clear(); m_customGroupHash.clear(); m_emptyGroups.append(m_ungrouped); m_groupHash.insert(0, m_ungrouped); m_emptyGroups.append(m_argumentHints); m_groupHash.insert(-1, m_argumentHints); m_emptyGroups.append(m_bestMatches); m_groupHash.insert(BestMatchesProperty, m_bestMatches); } QSet KateCompletionModel::createItems(const HierarchicalModelHandler &_handler, const QModelIndex &i, bool notifyModel) { HierarchicalModelHandler handler(_handler); QSet ret; if (handler.model()->rowCount(i) == 0) { //Leaf node, create an item ret.insert(createItem(handler, i, notifyModel)); } else { //Non-leaf node, take the role from the node, and recurse to the sub-nodes handler.takeRole(i); for (int a = 0; a < handler.model()->rowCount(i); a++) { ret += createItems(handler, i.child(a, 0), notifyModel); } } return ret; } QSet KateCompletionModel::deleteItems(const QModelIndex &i) { QSet ret; if (i.model()->rowCount(i) == 0) { //Leaf node, delete the item Group *g = groupForIndex(mapFromSource(i)); ret.insert(g); g->removeItem(ModelRow(const_cast(static_cast(i.model())), i)); } else { //Non-leaf node for (int a = 0; a < i.model()->rowCount(i); a++) { ret += deleteItems(i.child(a, 0)); } } return ret; } void KateCompletionModel::createGroups() { beginResetModel(); //After clearing the model, it has to be reset, else we will be in an invalid state while inserting //new groups. clearGroups(); bool has_groups = false; foreach (CodeCompletionModel *sourceModel, m_completionModels) { has_groups |= sourceModel->hasGroups(); for (int i = 0; i < sourceModel->rowCount(); ++i) { createItems(HierarchicalModelHandler(sourceModel), sourceModel->index(i, 0)); } } m_hasGroups = has_groups; //debugStats(); foreach (Group *g, m_rowTable) { hideOrShowGroup(g); } foreach (Group *g, m_emptyGroups) { hideOrShowGroup(g); } makeGroupItemsUnique(); updateBestMatches(); endResetModel(); } KateCompletionModel::Group *KateCompletionModel::createItem(const HierarchicalModelHandler &handler, const QModelIndex &sourceIndex, bool notifyModel) { //QModelIndex sourceIndex = sourceModel->index(row, CodeCompletionModel::Name, QModelIndex()); int completionFlags = handler.getData(CodeCompletionModel::CompletionRole, sourceIndex).toInt(); //Scope is expensive, should not be used with big models QString scopeIfNeeded = (groupingMethod() & Scope) ? sourceIndex.sibling(sourceIndex.row(), CodeCompletionModel::Scope).data(Qt::DisplayRole).toString() : QString(); int argumentHintDepth = handler.getData(CodeCompletionModel::ArgumentHintDepth, sourceIndex).toInt(); Group *g; if (argumentHintDepth) { g = m_argumentHints; } else { QString customGroup = handler.customGroup(); if (!customGroup.isNull() && m_hasGroups) { if (m_customGroupHash.contains(customGroup)) { g = m_customGroupHash[customGroup]; } else { g = new Group(customGroup, 0, this); g->customSortingKey = handler.customGroupingKey(); m_emptyGroups.append(g); m_customGroupHash.insert(customGroup, g); } } else { g = fetchGroup(completionFlags, scopeIfNeeded, handler.hasHierarchicalRoles()); } } Item item = Item(g != m_argumentHints, this, handler, ModelRow(handler.model(), sourceIndex)); if (g != m_argumentHints) { item.match(); } g->addItem(item, notifyModel); return g; } void KateCompletionModel::slotRowsInserted(const QModelIndex &parent, int start, int end) { QSet affectedGroups; HierarchicalModelHandler handler(static_cast(sender())); if (parent.isValid()) { handler.collectRoles(parent); } for (int i = start; i <= end; ++i) { affectedGroups += createItems(handler, parent.isValid() ? parent.child(i, 0) : handler.model()->index(i, 0), true); } foreach (Group *g, affectedGroups) { hideOrShowGroup(g, true); } } void KateCompletionModel::slotRowsRemoved(const QModelIndex &parent, int start, int end) { CodeCompletionModel *source = static_cast(sender()); QSet affectedGroups; for (int i = start; i <= end; ++i) { QModelIndex index = parent.isValid() ? parent.child(i, 0) : source->index(i, 0); affectedGroups += deleteItems(index); } foreach (Group *g, affectedGroups) { hideOrShowGroup(g, true); } } KateCompletionModel::Group *KateCompletionModel::fetchGroup(int attribute, const QString &scope, bool forceGrouping) { Q_UNUSED(forceGrouping); ///@todo use forceGrouping if (!hasGroups()) { return m_ungrouped; } int groupingAttribute = groupingAttributes(attribute); //qCDebug(LOG_KTE) << attribute << " " << groupingAttribute; if (m_groupHash.contains(groupingAttribute)) { if (groupingMethod() & Scope) { for (QHash::ConstIterator it = m_groupHash.constFind(groupingAttribute); it != m_groupHash.constEnd() && it.key() == groupingAttribute; ++it) if (it.value()->scope == scope) { return it.value(); } } else { return m_groupHash.value(groupingAttribute); } } QString st, at, it; QString title; if (groupingMethod() & ScopeType) { if (attribute & KTextEditor::CodeCompletionModel::GlobalScope) { st = QStringLiteral("Global"); } else if (attribute & KTextEditor::CodeCompletionModel::NamespaceScope) { st = QStringLiteral("Namespace"); } else if (attribute & KTextEditor::CodeCompletionModel::LocalScope) { st = QStringLiteral("Local"); } title = st; } if (groupingMethod() & Scope) { if (!title.isEmpty()) { - title.append(QLatin1String(" ")); + title.append(QLatin1Char(' ')); } title.append(scope); } if (groupingMethod() & AccessType) { if (attribute & KTextEditor::CodeCompletionModel::Public) { at = QStringLiteral("Public"); } else if (attribute & KTextEditor::CodeCompletionModel::Protected) { at = QStringLiteral("Protected"); } else if (attribute & KTextEditor::CodeCompletionModel::Private) { at = QStringLiteral("Private"); } if (accessIncludeStatic() && attribute & KTextEditor::CodeCompletionModel::Static) { at.append(QLatin1String(" Static")); } if (accessIncludeConst() && attribute & KTextEditor::CodeCompletionModel::Const) { at.append(QLatin1String(" Const")); } if (!at.isEmpty()) { if (!title.isEmpty()) { title.append(QLatin1String(", ")); } title.append(at); } } if (groupingMethod() & ItemType) { if (attribute & CodeCompletionModel::Namespace) { it = i18n("Namespaces"); } else if (attribute & CodeCompletionModel::Class) { it = i18n("Classes"); } else if (attribute & CodeCompletionModel::Struct) { it = i18n("Structs"); } else if (attribute & CodeCompletionModel::Union) { it = i18n("Unions"); } else if (attribute & CodeCompletionModel::Function) { it = i18n("Functions"); } else if (attribute & CodeCompletionModel::Variable) { it = i18n("Variables"); } else if (attribute & CodeCompletionModel::Enum) { it = i18n("Enumerations"); } if (!it.isEmpty()) { if (!title.isEmpty()) { - title.append(QLatin1String(" ")); + title.append(QLatin1Char(' ')); } title.append(it); } } Group *ret = new Group(title, attribute, this); ret->scope = scope; m_emptyGroups.append(ret); m_groupHash.insert(groupingAttribute, ret); return ret; } bool KateCompletionModel::hasGroups() const { //qCDebug(LOG_KTE) << "m_groupHash.size()"<= m_rowTable.count()) { return m_ungrouped; } return m_rowTable[index.row()]; } /*QMap< int, QVariant > KateCompletionModel::itemData( const QModelIndex & index ) const { if (!hasGroups() || groupOfParent(index)) { QModelIndex index = mapToSource(index); if (index.isValid()) return index.model()->itemData(index); } return QAbstractItemModel::itemData(index); }*/ QModelIndex KateCompletionModel::parent(const QModelIndex &index) const { if (!index.isValid()) { return QModelIndex(); } if (Group *g = groupOfParent(index)) { if (!hasGroups()) { Q_ASSERT(g == m_ungrouped); return QModelIndex(); } int row = m_rowTable.indexOf(g); if (row == -1) { qCWarning(LOG_KTE) << "Couldn't find parent for index" << index; return QModelIndex(); } return createIndex(row, 0, quintptr(0)); } return QModelIndex(); } int KateCompletionModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) { if (hasGroups()) { //qCDebug(LOG_KTE) << "Returning row count for toplevel " << m_rowTable.count(); return m_rowTable.count(); } else { //qCDebug(LOG_KTE) << "Returning ungrouped row count for toplevel " << m_ungrouped->filtered.count(); return m_ungrouped->filtered.count(); } } if (parent.column() > 0) { // only the first column has children return 0; } Group *g = groupForIndex(parent); // This is not an error, seems you don't have to check hasChildren() if (!g) { return 0; } //qCDebug(LOG_KTE) << "Returning row count for group " << g << " as " << g->filtered.count(); return g->filtered.count(); } void KateCompletionModel::sort(int column, Qt::SortOrder order) { Q_UNUSED(column) Q_UNUSED(order) } QModelIndex KateCompletionModel::mapToSource(const QModelIndex &proxyIndex) const { if (!proxyIndex.isValid()) { return QModelIndex(); } if (Group *g = groupOfParent(proxyIndex)) { if (proxyIndex.row() >= 0 && proxyIndex.row() < g->filtered.count()) { ModelRow source = g->filtered[proxyIndex.row()].sourceRow(); return source.second.sibling(source.second.row(), proxyIndex.column()); } else { qCDebug(LOG_KTE) << "Invalid proxy-index"; } } return QModelIndex(); } QModelIndex KateCompletionModel::mapFromSource(const QModelIndex &sourceIndex) const { if (!sourceIndex.isValid()) { return QModelIndex(); } if (!hasGroups()) { return index(m_ungrouped->rowOf(modelRowPair(sourceIndex)), sourceIndex.column(), QModelIndex()); } foreach (Group *g, m_rowTable) { int row = g->rowOf(modelRowPair(sourceIndex)); if (row != -1) { return index(row, sourceIndex.column(), indexForGroup(g)); } } // Copied from above foreach (Group *g, m_emptyGroups) { int row = g->rowOf(modelRowPair(sourceIndex)); if (row != -1) { return index(row, sourceIndex.column(), indexForGroup(g)); } } return QModelIndex(); } void KateCompletionModel::setCurrentCompletion(KTextEditor::CodeCompletionModel *model, const QString &completion) { if (m_currentMatch[model] == completion) { return; } if (!hasCompletionModel()) { m_currentMatch[model] = completion; return; } changeTypes changeType = Change; if (m_currentMatch[model].length() > completion.length() && m_currentMatch[model].startsWith(completion, m_matchCaseSensitivity)) { // Filter has been broadened changeType = Broaden; } else if (m_currentMatch[model].length() < completion.length() && completion.startsWith(m_currentMatch[model], m_matchCaseSensitivity)) { // Filter has been narrowed changeType = Narrow; } //qCDebug(LOG_KTE) << model << "Old match: " << m_currentMatch[model] << ", new: " << completion << ", type: " << changeType; m_currentMatch[model] = completion; const bool resetModel = (changeType != Narrow); if (resetModel) { beginResetModel(); } if (!hasGroups()) { changeCompletions(m_ungrouped, changeType, !resetModel); } else { foreach (Group *g, m_rowTable) { if (g != m_argumentHints) { changeCompletions(g, changeType, !resetModel); } } foreach (Group *g, m_emptyGroups) { if (g != m_argumentHints) { changeCompletions(g, changeType, !resetModel); } } } // NOTE: best matches are also updated in resort resort(); if (resetModel) { endResetModel(); } clearExpanding(); //We need to do this, or be aware of expanding-widgets while filtering. emit layoutChanged(); } QString KateCompletionModel::commonPrefixInternal(const QString &forcePrefix) const { QString commonPrefix; // isNull() = true QList< Group * > groups = m_rowTable; groups += m_ungrouped; foreach (Group *g, groups) { foreach (const Item &item, g->filtered) { uint startPos = m_currentMatch[item.sourceRow().first].length(); const QString candidate = item.name().mid(startPos); if (!candidate.startsWith(forcePrefix)) { continue; } if (commonPrefix.isNull()) { commonPrefix = candidate; //Replace QString::null prefix with QString(), so we won't initialize it again if (commonPrefix.isNull()) { commonPrefix = QString(); // isEmpty() = true, isNull() = false } } else { commonPrefix = commonPrefix.left(candidate.length()); for (int a = 0; a < commonPrefix.length(); ++a) { if (commonPrefix[a] != candidate[a]) { commonPrefix = commonPrefix.left(a); break; } } } } } return commonPrefix; } QString KateCompletionModel::commonPrefix(QModelIndex selectedIndex) const { QString commonPrefix = commonPrefixInternal(QString()); if (commonPrefix.isEmpty() && selectedIndex.isValid()) { Group *g = m_ungrouped; if (hasGroups()) { g = groupOfParent(selectedIndex); } if (g && selectedIndex.row() < g->filtered.size()) { //Follow the path of the selected item, finding the next non-empty common prefix Item item = g->filtered[selectedIndex.row()]; int matchLength = m_currentMatch[item.sourceRow().first].length(); commonPrefix = commonPrefixInternal(item.name().mid(matchLength).left(1)); } } return commonPrefix; } void KateCompletionModel::changeCompletions(Group *g, changeTypes changeType, bool notifyModel) { if (changeType != Narrow) { g->filtered = g->prefilter; //In the "Broaden" or "Change" case, just re-filter everything, //and don't notify the model. The model is notified afterwards through a reset(). } //This code determines what of the filtered items still fit, and computes the ranges that were removed, giving //them to beginRemoveRows(..) in batches QList newFiltered; int deleteUntil = -1; //In each state, the range [currentRow+1, deleteUntil] needs to be deleted for (int currentRow = g->filtered.count() - 1; currentRow >= 0; --currentRow) { if (g->filtered[currentRow].match()) { //This row does not need to be deleted, which means that currentRow+1 to deleteUntil need to be deleted now if (deleteUntil != -1 && notifyModel) { beginRemoveRows(indexForGroup(g), currentRow + 1, deleteUntil); endRemoveRows(); } deleteUntil = -1; newFiltered.prepend(g->filtered[currentRow]); } else { if (deleteUntil == -1) { deleteUntil = currentRow; //Mark that this row needs to be deleted } } } if (deleteUntil != -1 && notifyModel) { beginRemoveRows(indexForGroup(g), 0, deleteUntil); endRemoveRows(); } g->filtered = newFiltered; hideOrShowGroup(g, notifyModel); } int KateCompletionModel::Group::orderNumber() const { if (this == model->m_ungrouped) { return 700; } if (customSortingKey != -1) { return customSortingKey; } if (attribute & BestMatchesProperty) { return 1; } if (attribute & KTextEditor::CodeCompletionModel::LocalScope) { return 100; } else if (attribute & KTextEditor::CodeCompletionModel::Public) { return 200; } else if (attribute & KTextEditor::CodeCompletionModel::Protected) { return 300; } else if (attribute & KTextEditor::CodeCompletionModel::Private) { return 400; } else if (attribute & KTextEditor::CodeCompletionModel::NamespaceScope) { return 500; } else if (attribute & KTextEditor::CodeCompletionModel::GlobalScope) { return 600; } return 700; } bool KateCompletionModel::Group::orderBefore(Group *other) const { return orderNumber() < other->orderNumber(); } void KateCompletionModel::hideOrShowGroup(Group *g, bool notifyModel) { if (g == m_argumentHints) { emit argumentHintsChanged(); m_updateBestMatchesTimer->start(200); //We have new argument-hints, so we have new best matches return; //Never show argument-hints in the normal completion-list } if (!g->isEmpty) { if (g->filtered.isEmpty()) { // Move to empty group list g->isEmpty = true; int row = m_rowTable.indexOf(g); if (row != -1) { if (hasGroups() && notifyModel) { beginRemoveRows(QModelIndex(), row, row); } m_rowTable.removeAt(row); if (hasGroups() && notifyModel) { endRemoveRows(); } m_emptyGroups.append(g); } else { qCWarning(LOG_KTE) << "Group " << g << " not found in row table!!"; } } } else { if (!g->filtered.isEmpty()) { // Move off empty group list g->isEmpty = false; int row = 0; //Find row where to insert for (int a = 0; a < m_rowTable.count(); a++) { if (g->orderBefore(m_rowTable[a])) { row = a; break; } row = a + 1; } if (notifyModel) { if (hasGroups()) { beginInsertRows(QModelIndex(), row, row); } else { beginInsertRows(QModelIndex(), 0, g->filtered.count()); } } m_rowTable.insert(row, g); if (notifyModel) { endInsertRows(); } m_emptyGroups.removeAll(g); } } } bool KateCompletionModel::indexIsItem(const QModelIndex &index) const { if (!hasGroups()) { return true; } if (groupOfParent(index)) { return true; } return false; } void KateCompletionModel::slotModelReset() { createGroups(); //debugStats(); } void KateCompletionModel::debugStats() { if (!hasGroups()) { qCDebug(LOG_KTE) << "Model groupless, " << m_ungrouped->filtered.count() << " items."; } else { qCDebug(LOG_KTE) << "Model grouped (" << m_rowTable.count() << " groups):"; foreach (Group *g, m_rowTable) { qCDebug(LOG_KTE) << "Group" << g << "count" << g->filtered.count(); } } } bool KateCompletionModel::hasCompletionModel() const { return !m_completionModels.isEmpty(); } void KateCompletionModel::setFilteringEnabled(bool enable) { if (m_filteringEnabled != enable) { m_filteringEnabled = enable; } } void KateCompletionModel::setSortingEnabled(bool enable) { if (m_sortingEnabled != enable) { m_sortingEnabled = enable; beginResetModel(); resort(); endResetModel(); } } void KateCompletionModel::setGroupingEnabled(bool enable) { if (m_groupingEnabled != enable) { m_groupingEnabled = enable; } } void KateCompletionModel::setColumnMergingEnabled(bool enable) { if (m_columnMergingEnabled != enable) { m_columnMergingEnabled = enable; } } bool KateCompletionModel::isColumnMergingEnabled() const { return m_columnMergingEnabled; } bool KateCompletionModel::isGroupingEnabled() const { return m_groupingEnabled; } bool KateCompletionModel::isFilteringEnabled() const { return m_filteringEnabled; } bool KateCompletionModel::isSortingEnabled() const { return m_sortingEnabled; } QString KateCompletionModel::columnName(int column) { switch (column) { case KTextEditor::CodeCompletionModel::Prefix: return i18n("Prefix"); case KTextEditor::CodeCompletionModel::Icon: return i18n("Icon"); case KTextEditor::CodeCompletionModel::Scope: return i18n("Scope"); case KTextEditor::CodeCompletionModel::Name: return i18n("Name"); case KTextEditor::CodeCompletionModel::Arguments: return i18n("Arguments"); case KTextEditor::CodeCompletionModel::Postfix: return i18n("Postfix"); } return QString(); } const QList< QList < int > > &KateCompletionModel::columnMerges() const { return m_columnMerges; } void KateCompletionModel::setColumnMerges(const QList< QList < int > > &columnMerges) { beginResetModel(); m_columnMerges = columnMerges; endResetModel(); } int KateCompletionModel::translateColumn(int sourceColumn) const { if (m_columnMerges.isEmpty()) { return sourceColumn; } /* Debugging - dump column merge list QString columnMerge; foreach (const QList& list, m_columnMerges) { columnMerge += '['; foreach (int column, list) { - columnMerge += QString::number(column) + " "; + columnMerge += QString::number(column) + QLatin1Char(' '); } columnMerge += "] "; } qCDebug(LOG_KTE) << k_funcinfo << columnMerge;*/ int c = 0; foreach (const QList &list, m_columnMerges) { foreach (int column, list) { if (column == sourceColumn) { return c; } } c++; } return -1; } int KateCompletionModel::groupingAttributes(int attribute) const { int ret = 0; if (m_groupingMethod & ScopeType) { if (countBits(attribute & ScopeTypeMask) > 1) { qCWarning(LOG_KTE) << "Invalid completion model metadata: more than one scope type modifier provided."; } if (attribute & KTextEditor::CodeCompletionModel::GlobalScope) { ret |= KTextEditor::CodeCompletionModel::GlobalScope; } else if (attribute & KTextEditor::CodeCompletionModel::NamespaceScope) { ret |= KTextEditor::CodeCompletionModel::NamespaceScope; } else if (attribute & KTextEditor::CodeCompletionModel::LocalScope) { ret |= KTextEditor::CodeCompletionModel::LocalScope; } } if (m_groupingMethod & AccessType) { if (countBits(attribute & AccessTypeMask) > 1) { qCWarning(LOG_KTE) << "Invalid completion model metadata: more than one access type modifier provided."; } if (attribute & KTextEditor::CodeCompletionModel::Public) { ret |= KTextEditor::CodeCompletionModel::Public; } else if (attribute & KTextEditor::CodeCompletionModel::Protected) { ret |= KTextEditor::CodeCompletionModel::Protected; } else if (attribute & KTextEditor::CodeCompletionModel::Private) { ret |= KTextEditor::CodeCompletionModel::Private; } if (accessIncludeStatic() && attribute & KTextEditor::CodeCompletionModel::Static) { ret |= KTextEditor::CodeCompletionModel::Static; } if (accessIncludeConst() && attribute & KTextEditor::CodeCompletionModel::Const) { ret |= KTextEditor::CodeCompletionModel::Const; } } if (m_groupingMethod & ItemType) { if (countBits(attribute & ItemTypeMask) > 1) { qCWarning(LOG_KTE) << "Invalid completion model metadata: more than one item type modifier provided."; } if (attribute & KTextEditor::CodeCompletionModel::Namespace) { ret |= KTextEditor::CodeCompletionModel::Namespace; } else if (attribute & KTextEditor::CodeCompletionModel::Class) { ret |= KTextEditor::CodeCompletionModel::Class; } else if (attribute & KTextEditor::CodeCompletionModel::Struct) { ret |= KTextEditor::CodeCompletionModel::Struct; } else if (attribute & KTextEditor::CodeCompletionModel::Union) { ret |= KTextEditor::CodeCompletionModel::Union; } else if (attribute & KTextEditor::CodeCompletionModel::Function) { ret |= KTextEditor::CodeCompletionModel::Function; } else if (attribute & KTextEditor::CodeCompletionModel::Variable) { ret |= KTextEditor::CodeCompletionModel::Variable; } else if (attribute & KTextEditor::CodeCompletionModel::Enum) { ret |= KTextEditor::CodeCompletionModel::Enum; } /* if (itemIncludeTemplate() && attribute & KTextEditor::CodeCompletionModel::Template) ret |= KTextEditor::CodeCompletionModel::Template;*/ } return ret; } void KateCompletionModel::setGroupingMethod(GroupingMethods m) { m_groupingMethod = m; createGroups(); } bool KateCompletionModel::accessIncludeConst() const { return m_accessConst; } void KateCompletionModel::setAccessIncludeConst(bool include) { if (m_accessConst != include) { m_accessConst = include; if (groupingMethod() & AccessType) { createGroups(); } } } bool KateCompletionModel::accessIncludeStatic() const { return m_accessStatic; } void KateCompletionModel::setAccessIncludeStatic(bool include) { if (m_accessStatic != include) { m_accessStatic = include; if (groupingMethod() & AccessType) { createGroups(); } } } bool KateCompletionModel::accessIncludeSignalSlot() const { return m_accesSignalSlot; } void KateCompletionModel::setAccessIncludeSignalSlot(bool include) { if (m_accesSignalSlot != include) { m_accesSignalSlot = include; if (groupingMethod() & AccessType) { createGroups(); } } } int KateCompletionModel::countBits(int value) const { int count = 0; for (int i = 1; i; i <<= 1) if (i & value) { count++; } return count; } KateCompletionModel::GroupingMethods KateCompletionModel::groupingMethod() const { return m_groupingMethod; } bool KateCompletionModel::isSortingByInheritanceDepth() const { return m_isSortingByInheritance; } void KateCompletionModel::setSortingByInheritanceDepth(bool byInheritance) { m_isSortingByInheritance = byInheritance; } bool KateCompletionModel::isSortingAlphabetical() const { return m_sortingAlphabetical; } Qt::CaseSensitivity KateCompletionModel::sortingCaseSensitivity() const { return m_sortingCaseSensitivity; } KateCompletionModel::Item::Item(bool doInitialMatch, KateCompletionModel *m, const HierarchicalModelHandler &handler, ModelRow sr) : model(m) , m_sourceRow(sr) , matchCompletion(StartsWithMatch) , matchFilters(true) , m_haveExactMatch(false) { inheritanceDepth = handler.getData(CodeCompletionModel::InheritanceDepth, m_sourceRow.second).toInt(); m_unimportant = handler.getData(CodeCompletionModel::UnimportantItemRole, m_sourceRow.second).toBool(); QModelIndex nameSibling = sr.second.sibling(sr.second.row(), CodeCompletionModel::Name); m_nameColumn = nameSibling.data(Qt::DisplayRole).toString(); if (doInitialMatch) { filter(); match(); } } bool KateCompletionModel::Item::operator <(const Item &rhs) const { int ret = 0; //qCDebug(LOG_KTE) << c1 << " c/w " << c2 << " -> " << (model->isSortingReverse() ? ret > 0 : ret < 0) << " (" << ret << ")"; if(m_unimportant && !rhs.m_unimportant){ return false; } if(!m_unimportant && rhs.m_unimportant){ return true; } if (matchCompletion < rhs.matchCompletion) { // enums are ordered in the order items should be displayed return true; } if (matchCompletion > rhs.matchCompletion) { return false; } if (ret == 0) { const QString& filter = rhs.model->currentCompletion(rhs.m_sourceRow.first); bool thisStartWithFilter = m_nameColumn.startsWith(filter, Qt::CaseSensitive); bool rhsStartsWithFilter = rhs.m_nameColumn.startsWith(filter, Qt::CaseSensitive); if( thisStartWithFilter && !rhsStartsWithFilter ) { return true; } if( rhsStartsWithFilter && !thisStartWithFilter ) { return false; } } if (model->isSortingByInheritanceDepth()) { ret = inheritanceDepth - rhs.inheritanceDepth; } if (ret == 0 && model->isSortingAlphabetical()) { // Do not use localeAwareCompare, because it is simply too slow for a list of about 1000 items ret = QString::compare(m_nameColumn, rhs.m_nameColumn, model->sortingCaseSensitivity()); } if (ret == 0) { // FIXME need to define a better default ordering for multiple model display ret = m_sourceRow.second.row() - rhs.m_sourceRow.second.row(); } return ret < 0; } void KateCompletionModel::Group::addItem(Item i, bool notifyModel) { if (isEmpty) { notifyModel = false; } QModelIndex groupIndex; if (notifyModel) { groupIndex = model->indexForGroup(this); } if (model->isSortingEnabled()) { prefilter.insert(std::upper_bound(prefilter.begin(), prefilter.end(), i), i); if (i.isVisible()) { QList::iterator it = std::upper_bound(filtered.begin(), filtered.end(), i); uint rowNumber = it - filtered.begin(); if (notifyModel) { model->beginInsertRows(groupIndex, rowNumber, rowNumber); } filtered.insert(it, i); } } else { if (notifyModel) { model->beginInsertRows(groupIndex, prefilter.size(), prefilter.size()); } if (i.isVisible()) { prefilter.append(i); } } if (notifyModel) { model->endInsertRows(); } } bool KateCompletionModel::Group::removeItem(const ModelRow &row) { for (int pi = 0; pi < prefilter.count(); ++pi) if (prefilter[pi].sourceRow() == row) { int index = rowOf(row); if (index != -1) { model->beginRemoveRows(model->indexForGroup(this), index, index); } filtered.removeAt(index); prefilter.removeAt(pi); if (index != -1) { model->endRemoveRows(); } return index != -1; } Q_ASSERT(false); return false; } KateCompletionModel::Group::Group(const QString& title, int attribute, KateCompletionModel *m) : model(m) , attribute(attribute) // ugly hack to add some left margin , title(QLatin1Char(' ') + title) , isEmpty(true) , customSortingKey(-1) { Q_ASSERT(model); } void KateCompletionModel::setSortingAlphabetical(bool alphabetical) { if (m_sortingAlphabetical != alphabetical) { m_sortingAlphabetical = alphabetical; beginResetModel(); resort(); endResetModel(); } } void KateCompletionModel::Group::resort() { std::stable_sort(filtered.begin(), filtered.end()); model->hideOrShowGroup(this); } void KateCompletionModel::setSortingCaseSensitivity(Qt::CaseSensitivity cs) { if (m_sortingCaseSensitivity != cs) { m_sortingCaseSensitivity = cs; beginResetModel(); resort(); endResetModel(); } } void KateCompletionModel::resort() { foreach (Group *g, m_rowTable) { g->resort(); } foreach (Group *g, m_emptyGroups) { g->resort(); } // call updateBestMatches here, so they are moved to the top again. updateBestMatches(); } bool KateCompletionModel::Item::isValid() const { return model && m_sourceRow.first && m_sourceRow.second.row() >= 0; } void KateCompletionModel::Group::clear() { prefilter.clear(); filtered.clear(); isEmpty = true; } bool KateCompletionModel::filterContextMatchesOnly() const { return m_filterContextMatchesOnly; } void KateCompletionModel::setFilterContextMatchesOnly(bool filter) { if (m_filterContextMatchesOnly != filter) { m_filterContextMatchesOnly = filter; refilter(); } } bool KateCompletionModel::filterByAttribute() const { return m_filterByAttribute; } void KateCompletionModel::setFilterByAttribute(bool filter) { if (m_filterByAttribute == filter) { m_filterByAttribute = filter; refilter(); } } KTextEditor::CodeCompletionModel::CompletionProperties KateCompletionModel::filterAttributes() const { return m_filterAttributes; } void KateCompletionModel::setFilterAttributes(KTextEditor::CodeCompletionModel::CompletionProperties attributes) { if (m_filterAttributes == attributes) { m_filterAttributes = attributes; refilter(); } } int KateCompletionModel::maximumInheritanceDepth() const { return m_maximumInheritanceDepth; } void KateCompletionModel::setMaximumInheritanceDepth(int maxDepth) { if (m_maximumInheritanceDepth != maxDepth) { m_maximumInheritanceDepth = maxDepth; refilter(); } } void KateCompletionModel::refilter() { beginResetModel(); m_ungrouped->refilter(); foreach (Group *g, m_rowTable) if (g != m_argumentHints) { g->refilter(); } foreach (Group *g, m_emptyGroups) if (g != m_argumentHints) { g->refilter(); } updateBestMatches(); clearExpanding(); //We need to do this, or be aware of expanding-widgets while filtering. endResetModel(); } void KateCompletionModel::Group::refilter() { filtered.clear(); foreach (const Item &i, prefilter) if (!i.isFiltered()) { filtered.append(i); } } bool KateCompletionModel::Item::filter() { matchFilters = false; if (model->isFilteringEnabled()) { QModelIndex sourceIndex = m_sourceRow.second.sibling(m_sourceRow.second.row(), CodeCompletionModel::Name); if (model->filterContextMatchesOnly()) { QVariant contextMatch = sourceIndex.data(CodeCompletionModel::MatchQuality); if (contextMatch.canConvert(QVariant::Int) && !contextMatch.toInt()) { return false; } } if (model->filterByAttribute()) { int completionFlags = sourceIndex.data(CodeCompletionModel::CompletionRole).toInt(); if (model->filterAttributes() & completionFlags) { return false; } } if (model->maximumInheritanceDepth() > 0) { int inheritanceDepth = sourceIndex.data(CodeCompletionModel::InheritanceDepth).toInt(); if (inheritanceDepth > model->maximumInheritanceDepth()) { return false; } } } matchFilters = true; return matchFilters; } uint KateCompletionModel::filteredItemCount() const { uint ret = 0; foreach (Group *group, m_rowTable) { ret += group->filtered.size(); } return ret; } bool KateCompletionModel::shouldMatchHideCompletionList() const { // @todo Make this faster bool doHide = false; CodeCompletionModel *hideModel = nullptr; foreach (Group *group, m_rowTable) foreach (const Item &item, group->filtered) if (item.haveExactMatch()) { KTextEditor::CodeCompletionModelControllerInterface *iface3 = dynamic_cast(item.sourceRow().first); bool hide = false; if (!iface3) { hide = true; } if (iface3 && iface3->matchingItem(item.sourceRow().second) == KTextEditor::CodeCompletionModelControllerInterface::HideListIfAutomaticInvocation) { hide = true; } if (hide) { doHide = true; hideModel = item.sourceRow().first; } } if (doHide) { // Check if all other visible items are from the same model foreach (Group *group, m_rowTable) foreach (const Item &item, group->filtered) if (item.sourceRow().first != hideModel) { return false; } } return doHide; } static inline QChar toLowerIfInsensitive(QChar c, Qt::CaseSensitivity caseSensitive) { return (caseSensitive == Qt::CaseInsensitive) ? c.toLower() : c; } static inline bool matchesAbbreviationHelper(const QString &word, const QString &typed, const QVarLengthArray &offsets, Qt::CaseSensitivity caseSensitive, int &depth, int atWord = -1, int i = 0) { int atLetter = 1; for (; i < typed.size(); i++) { const QChar c = toLowerIfInsensitive(typed.at(i), caseSensitive); bool haveNextWord = offsets.size() > atWord + 1; bool canCompare = atWord != -1 && word.size() > offsets.at(atWord) + atLetter; if (canCompare && c == toLowerIfInsensitive(word.at(offsets.at(atWord) + atLetter), caseSensitive)) { // the typed letter matches a letter after the current word beginning if (! haveNextWord || c != toLowerIfInsensitive(word.at(offsets.at(atWord + 1)), caseSensitive)) { // good, simple case, no conflict atLetter += 1; continue; } // For maliciously crafted data, the code used here theoretically can have very high // complexity. Thus ensure we don't run into this case, by limiting the amount of branches // we walk through to 128. depth++; if (depth > 128) { return false; } // the letter matches both the next word beginning and the next character in the word if (haveNextWord && matchesAbbreviationHelper(word, typed, offsets, caseSensitive, depth, atWord + 1, i + 1)) { // resolving the conflict by taking the next word's first character worked, fine return true; } // otherwise, continue by taking the next letter in the current word. atLetter += 1; continue; } else if (haveNextWord && c == toLowerIfInsensitive(word.at(offsets.at(atWord + 1)), caseSensitive)) { // the typed letter matches the next word beginning atWord++; atLetter = 1; continue; } // no match return false; } // all characters of the typed word were matched return true; } bool KateCompletionModel::matchesAbbreviation(const QString &word, const QString &typed, Qt::CaseSensitivity caseSensitive) { // A mismatch is very likely for random even for the first letter, // thus this optimization makes sense. if (toLowerIfInsensitive(word.at(0), caseSensitive) != toLowerIfInsensitive(typed.at(0), caseSensitive)) { return false; } // First, check if all letters are contained in the word in the right order. int atLetter = 0; foreach (const QChar c, typed) { while (toLowerIfInsensitive(c, caseSensitive) != toLowerIfInsensitive(word.at(atLetter), caseSensitive)) { atLetter += 1; if (atLetter >= word.size()) { return false; } } } bool haveUnderscore = true; QVarLengthArray offsets; // We want to make "KComplM" match "KateCompletionModel"; this means we need // to allow parts of the typed text to be not part of the actual abbreviation, // which consists only of the uppercased / underscored letters (so "KCM" in this case). // However it might be ambiguous whether a letter is part of such a word or part of // the following abbreviation, so we need to find all possible word offsets first, // then compare. for (int i = 0; i < word.size(); i++) { const QChar c = word.at(i); if (c == QLatin1Char('_')) { haveUnderscore = true; } else if (haveUnderscore || c.isUpper()) { offsets.append(i); haveUnderscore = false; } } int depth = 0; return matchesAbbreviationHelper(word, typed, offsets, caseSensitive, depth); } static inline bool containsAtWordBeginning(const QString &word, const QString &typed, Qt::CaseSensitivity caseSensitive) { for (int i = 1; i < word.size(); i++) { // The current position is a word beginning if the previous character was an underscore // or if the current character is uppercase. Subsequent uppercase characters do not count, // to handle the special case of UPPER_CASE_VARS properly. const QChar c = word.at(i); const QChar prev = word.at(i - 1); if (!(prev == QLatin1Char('_') || (c.isUpper() && !prev.isUpper()))) { continue; } if (word.midRef(i).startsWith(typed, caseSensitive)) { return true; } } return false; } KateCompletionModel::Item::MatchType KateCompletionModel::Item::match() { QString match = model->currentCompletion(m_sourceRow.first); m_haveExactMatch = false; // Hehe, everything matches nothing! (ie. everything matches a blank string) if (match.isEmpty()) { return PerfectMatch; } if (m_nameColumn.isEmpty()) { return NoMatch; } matchCompletion = (m_nameColumn.startsWith(match, model->matchCaseSensitivity()) ? StartsWithMatch : NoMatch); if (matchCompletion == NoMatch) { // if no match, try for "contains" // Only match when the occurrence is at a "word" beginning, marked by // an underscore or a capital. So Foo matches BarFoo and Bar_Foo, but not barfoo. // Starting at 1 saves looking at the beginning of the word, that was already checked above. if (containsAtWordBeginning(m_nameColumn, match, model->matchCaseSensitivity())) { matchCompletion = ContainsMatch; } } if (matchCompletion == NoMatch && !m_nameColumn.isEmpty() && !match.isEmpty()) { // if still no match, try abbreviation matching if (matchesAbbreviation(m_nameColumn, match, model->matchCaseSensitivity())) { matchCompletion = AbbreviationMatch; } } if (matchCompletion && match.length() == m_nameColumn.length()) { if (model->matchCaseSensitivity() == Qt::CaseInsensitive && model->exactMatchCaseSensitivity() == Qt::CaseSensitive && !m_nameColumn.startsWith(match, Qt::CaseSensitive)) { return matchCompletion; } matchCompletion = PerfectMatch; m_haveExactMatch = true; } return matchCompletion; } QString KateCompletionModel::propertyName(KTextEditor::CodeCompletionModel::CompletionProperty property) { switch (property) { case CodeCompletionModel::Public: return i18n("Public"); case CodeCompletionModel::Protected: return i18n("Protected"); case CodeCompletionModel::Private: return i18n("Private"); case CodeCompletionModel::Static: return i18n("Static"); case CodeCompletionModel::Const: return i18n("Constant"); case CodeCompletionModel::Namespace: return i18n("Namespace"); case CodeCompletionModel::Class: return i18n("Class"); case CodeCompletionModel::Struct: return i18n("Struct"); case CodeCompletionModel::Union: return i18n("Union"); case CodeCompletionModel::Function: return i18n("Function"); case CodeCompletionModel::Variable: return i18n("Variable"); case CodeCompletionModel::Enum: return i18n("Enumeration"); case CodeCompletionModel::Template: return i18n("Template"); case CodeCompletionModel::Virtual: return i18n("Virtual"); case CodeCompletionModel::Override: return i18n("Override"); case CodeCompletionModel::Inline: return i18n("Inline"); case CodeCompletionModel::Friend: return i18n("Friend"); case CodeCompletionModel::Signal: return i18n("Signal"); case CodeCompletionModel::Slot: return i18n("Slot"); case CodeCompletionModel::LocalScope: return i18n("Local Scope"); case CodeCompletionModel::NamespaceScope: return i18n("Namespace Scope"); case CodeCompletionModel::GlobalScope: return i18n("Global Scope"); default: return i18n("Unknown Property"); } } bool KateCompletionModel::Item::isVisible() const { return matchCompletion && matchFilters; } bool KateCompletionModel::Item::isFiltered() const { return !matchFilters; } bool KateCompletionModel::Item::isMatching() const { return matchFilters; } const KateCompletionModel::ModelRow &KateCompletionModel::Item::sourceRow() const { return m_sourceRow; } QString KateCompletionModel::currentCompletion(KTextEditor::CodeCompletionModel *model) const { return m_currentMatch.value(model); } Qt::CaseSensitivity KateCompletionModel::matchCaseSensitivity() const { return m_matchCaseSensitivity; } Qt::CaseSensitivity KateCompletionModel::exactMatchCaseSensitivity() const { return m_exactMatchCaseSensitivity; } void KateCompletionModel::addCompletionModel(KTextEditor::CodeCompletionModel *model) { if (m_completionModels.contains(model)) { return; } m_completionModels.append(model); connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(slotRowsInserted(QModelIndex,int,int))); connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), SLOT(slotRowsRemoved(QModelIndex,int,int))); connect(model, SIGNAL(modelReset()), SLOT(slotModelReset())); // This performs the reset createGroups(); } void KateCompletionModel::setCompletionModel(KTextEditor::CodeCompletionModel *model) { clearCompletionModels(); addCompletionModel(model); } void KateCompletionModel::setCompletionModels(const QList &models) { //if (m_completionModels == models) //return; clearCompletionModels(); m_completionModels = models; foreach (KTextEditor::CodeCompletionModel *model, models) { connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(slotRowsInserted(QModelIndex,int,int))); connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), SLOT(slotRowsRemoved(QModelIndex,int,int))); connect(model, SIGNAL(modelReset()), SLOT(slotModelReset())); } // This performs the reset createGroups(); } QList< KTextEditor::CodeCompletionModel * > KateCompletionModel::completionModels() const { return m_completionModels; } void KateCompletionModel::removeCompletionModel(CodeCompletionModel *model) { if (!model || !m_completionModels.contains(model)) { return; } beginResetModel(); m_currentMatch.remove(model); clearGroups(); model->disconnect(this); m_completionModels.removeAll(model); endResetModel(); if (!m_completionModels.isEmpty()) { // This performs the reset createGroups(); } } void KateCompletionModel::makeGroupItemsUnique(bool onlyFiltered) { struct FilterItems { FilterItems(KateCompletionModel &model, const QVector &needShadowing) : m_model(model), m_needShadowing(needShadowing) { } QHash had; KateCompletionModel &m_model; const QVector< KTextEditor::CodeCompletionModel * > m_needShadowing; void filter(QList &items) { QList temp; foreach (const Item &item, items) { QHash::const_iterator it = had.constFind(item.name()); if (it != had.constEnd() && *it != item.sourceRow().first && m_needShadowing.contains(item.sourceRow().first)) { continue; } had.insert(item.name(), item.sourceRow().first); temp.push_back(item); } items = temp; } void filter(Group *group, bool onlyFiltered) { if (group->prefilter.size() == group->filtered.size()) { // Filter only once filter(group->filtered); if (!onlyFiltered) { group->prefilter = group->filtered; } } else { // Must filter twice filter(group->filtered); if (!onlyFiltered) { filter(group->prefilter); } } if (group->filtered.isEmpty()) { m_model.hideOrShowGroup(group); } } }; QVector needShadowing; foreach (KTextEditor::CodeCompletionModel *model, m_completionModels) { KTextEditor::CodeCompletionModelControllerInterface *v4 = dynamic_cast(model); if (v4 && v4->shouldHideItemsWithEqualNames()) { needShadowing.push_back(model); } } if (needShadowing.isEmpty()) { return; } FilterItems filter(*this, needShadowing); filter.filter(m_ungrouped, onlyFiltered); foreach (Group *group, m_rowTable) { filter.filter(group, onlyFiltered); } } //Updates the best-matches group void KateCompletionModel::updateBestMatches() { int maxMatches = 300; //We cannot do too many operations here, because they are all executed whenever a character is added. Would be nice if we could split the operations up somewhat using a timer. m_updateBestMatchesTimer->stop(); //Maps match-qualities to ModelRows paired together with the BestMatchesCount returned by the items. typedef QMultiMap > BestMatchMap; BestMatchMap matches; if (!hasGroups()) { //If there is no grouping, just change the order of the items, moving the best matching ones to the front QMultiMap rowsForQuality; int row = 0; foreach (const Item &item, m_ungrouped->filtered) { ModelRow source = item.sourceRow(); QVariant v = source.second.data(CodeCompletionModel::BestMatchesCount); if (v.type() == QVariant::Int && v.toInt() > 0) { int quality = contextMatchQuality(source); if (quality > 0) { rowsForQuality.insert(quality, row); } } ++row; --maxMatches; if (maxMatches < 0) { break; } } if (!rowsForQuality.isEmpty()) { //Rewrite m_ungrouped->filtered in a new order QSet movedToFront; QList newFiltered; for (QMultiMap::const_iterator it = rowsForQuality.constBegin(); it != rowsForQuality.constEnd(); ++it) { newFiltered.prepend(m_ungrouped->filtered[it.value()]); movedToFront.insert(it.value()); } { int size = m_ungrouped->filtered.size(); for (int a = 0; a < size; ++a) if (!movedToFront.contains(a)) { newFiltered.append(m_ungrouped->filtered[a]); } } m_ungrouped->filtered = newFiltered; } return; } ///@todo Cache the CodeCompletionModel::BestMatchesCount foreach (Group *g, m_rowTable) { if (g == m_bestMatches) { continue; } for (int a = 0; a < g->filtered.size(); a++) { ModelRow source = g->filtered[a].sourceRow(); QVariant v = source.second.data(CodeCompletionModel::BestMatchesCount); if (v.type() == QVariant::Int && v.toInt() > 0) { //Return the best match with any of the argument-hints int quality = contextMatchQuality(source); if (quality > 0) { matches.insert(quality, qMakePair(v.toInt(), g->filtered[a].sourceRow())); } --maxMatches; } if (maxMatches < 0) { break; } } if (maxMatches < 0) { break; } } //Now choose how many of the matches will be taken. This is done with the rule: //The count of shown best-matches should equal the average count of their BestMatchesCounts int cnt = 0; int matchesSum = 0; BestMatchMap::const_iterator it = matches.constEnd(); while (it != matches.constBegin()) { --it; ++cnt; matchesSum += (*it).first; if (cnt > matchesSum / cnt) { break; } } m_bestMatches->filtered.clear(); it = matches.constEnd(); while (it != matches.constBegin() && cnt > 0) { --it; --cnt; m_bestMatches->filtered.append(Item(true, this, HierarchicalModelHandler((*it).second.first), (*it).second)); } hideOrShowGroup(m_bestMatches); } void KateCompletionModel::rowSelected(const QModelIndex &row) { ExpandingWidgetModel::rowSelected(row); ///@todo delay this int rc = widget()->argumentHintModel()->rowCount(QModelIndex()); if (rc == 0) { return; } //For now, simply update the whole column 0 QModelIndex start = widget()->argumentHintModel()->index(0, 0); QModelIndex end = widget()->argumentHintModel()->index(rc - 1, 0); widget()->argumentHintModel()->emitDataChanged(start, end); } void KateCompletionModel::clearCompletionModels() { if (m_completionModels.isEmpty()) { return; } beginResetModel(); foreach (CodeCompletionModel *model, m_completionModels) { model->disconnect(this); } m_completionModels.clear(); m_currentMatch.clear(); clearGroups(); endResetModel(); } diff --git a/src/document/katedocument.cpp b/src/document/katedocument.cpp index 4d3cdd6c..a14da0ab 100644 --- a/src/document/katedocument.cpp +++ b/src/document/katedocument.cpp @@ -1,6085 +1,6085 @@ /* 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 "config.h" #include "katedocument.h" #include "kateglobal.h" #include "katedialogs.h" #include "katehighlight.h" #include "kateview.h" #include "kateautoindent.h" #include "katetextline.h" #include "katerenderer.h" #include "kateregexp.h" #include "kateplaintextsearch.h" #include "kateregexpsearch.h" #include "kateconfig.h" #include "katemodemanager.h" #include "kateschema.h" #include "katebuffer.h" #include "kateundomanager.h" #include "spellcheck/prefixstore.h" #include "spellcheck/ontheflycheck.h" #include "spellcheck/spellcheck.h" #include "katescriptmanager.h" #include "kateswapfile.h" #include "katepartdebug.h" #include "printing/kateprinter.h" #include "kateabstractinputmode.h" #include "katetemplatehandler.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 foreach (KTextEditor::Message *message, m_messageHash.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; foreach (KTextEditor::Mark *mark, m_marks) { msave.append(*mark); } editStart(); // delete the text clear(); // insert the new text insertText(KTextEditor::Cursor(), s); editEnd(); foreach (KTextEditor::Mark mark, msave) { setMark(mark.line, mark.type); } return true; } bool KTextEditor::DocumentPrivate::setText(const QStringList &text) { if (!isReadWrite()) { return false; } QList msave; foreach (KTextEditor::Mark *mark, m_marks) { msave.append(*mark); } editStart(); // delete the text clear(); // insert the new text insertText(KTextEditor::Cursor::start(), text); editEnd(); foreach (KTextEditor::Mark mark, msave) { setMark(mark.line, mark.type); } return true; } bool KTextEditor::DocumentPrivate::clear() { if (!isReadWrite()) { return false; } foreach (KTextEditor::ViewPrivate *view, 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(QLatin1String("\n")), block); + 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; foreach (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(); foreach (KTextEditor::ViewPrivate *view, 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 !!!!!!!!! foreach (KTextEditor::ViewPrivate *view, 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, QLatin1String(" ")); } 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; foreach (KTextEditor::Mark *mark, m_marks) { int line = mark->line; if (line > to) { list << line; } else if (line >= from) { rmark << line; } } foreach (int line, rmark) { delete m_marks.take(line); } foreach (int line, 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(QLatin1String("\n")) + QLatin1Char('\n')); + 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) { updateFileType(name); return true; } 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(); foreach (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(QLatin1String("SkipEncoding"))) { // get the encoding QString tmpenc = kconfig.readEntry("Encoding"); if (!tmpenc.isEmpty() && (tmpenc != encoding())) { setEncoding(tmpenc); } } if (!flags.contains(QLatin1String("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(QLatin1String("SkipMode"))) { // restore the filetype if (kconfig.hasKey("Mode")) { updateFileType(kconfig.readEntry("Mode", fileType())); // restore if set by user, too! m_fileTypeSetByUser = kconfig.readEntry("Mode Set By User", false); } } if (!flags.contains(QLatin1String("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(QLatin1String("SkipUrl"))) { // save url kconfig.writeEntry("URL", this->url().toString()); } if (!flags.contains(QLatin1String("SkipEncoding"))) { // save encoding kconfig.writeEntry("Encoding", encoding()); } if (!flags.contains(QLatin1String("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(QLatin1String("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); } 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()); } 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 //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 // foreach (KTextEditor::ViewPrivate *view, 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 */ if (config()->backupPrefix().contains(QDir::separator())) { /** * replace complete path, as prefix is a path! */ u.setPath(config()->backupPrefix() + u.fileName() + config()->backupSuffix()); } else { /** * replace filename in url */ const QString fileName = u.fileName(); u = u.adjusted(QUrl::RemoveFilename); u.setPath(u.path() + config()->backupPrefix() + fileName + config()->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()) { QList keys = m_messageHash.keys(); foreach (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 foreach (KTextEditor::ViewPrivate *view, 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); foreach (KTextEditor::ViewPrivate *view, m_views) { view->slotUpdateUndo(); view->slotReadWriteChanged(); } emit readWriteChanged(this); } void KTextEditor::DocumentPrivate::setModified(bool m) { if (isModified() != m) { KParts::ReadWritePart::setModified(m); foreach (KTextEditor::ViewPrivate *view, m_views) { view->slotUpdateUndo(); } emit modifiedChanged(this); } m_undoManager->setModified(m); } //END //BEGIN Kate specific stuff ;) void KTextEditor::DocumentPrivate::makeAttribs(bool needInvalidate) { foreach (KTextEditor::ViewPrivate *view, m_views) { view->renderer()->updateAttributes(); } if (needInvalidate) { m_buffer->invalidateHighlighting(); } foreach (KTextEditor::ViewPrivate *view, 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); } // Refesh 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(QLatin1String(" "))) { // 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()) { QStringList pasteLines = s.split(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()); Q_FOREACH (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 += QStringLiteral(" ").repeated(spacesToInsert); 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, QLatin1String(" ")); } } 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) { foreach (KTextEditor::ViewPrivate *view, m_views) { view->tagLines(start, end, true); } } void KTextEditor::DocumentPrivate::repaintViews(bool paintOnlyDirty) { foreach (KTextEditor::ViewPrivate *view, 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; foreach (KTextEditor::DocumentPrivate *doc, KTextEditor::EditorPrivate::self()->kateDocuments()) { 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... foreach (KTextEditor::ViewPrivate *view, 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! KTextEditor::ViewPrivate *v; foreach (v, 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(); } foreach (v, m_views) { v->config()->configEnd(); v->renderer()->config()->configEnd(); } } void KTextEditor::DocumentPrivate::readVariableLine(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; foreach (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(QString var, QString val) { KTextEditor::ViewPrivate *v; bool state; int n; QColor c; foreach (v, 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()->config()->font()); 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(QString val, int *result) { bool ret(false); *result = val.toInt(&ret); return ret; } bool KTextEditor::DocumentPrivate::checkColorValue(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 } } void KTextEditor::DocumentPrivate::updateFileType(const QString &newType, bool user) { if (user || !m_fileTypeSetByUser) { if (!newType.isEmpty()) { // 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) && !KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).hl.isEmpty()) { int hl(KateHlManager::self()->nameFind(KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).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 && !KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).indenter.isEmpty()) { config()->setIndentationMode(KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).indenter); } // views! KTextEditor::ViewPrivate *v; foreach (v, m_views) { v->config()->configStart(); v->renderer()->config()->configStart(); } bool bom_settings = false; if (m_bomSetByUser) { bom_settings = m_config->bom(); } readVariableLine(KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).varLine); if (m_bomSetByUser) { m_config->setBom(bom_settings); } m_config->configEnd(); foreach (v, m_views) { v->config()->configEnd(); v->renderer()->config()->configEnd(); } } } // fixme, make this better... emit modeChanged(this); } 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; } foreach (KTextEditor::ViewPrivate *view, 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); } // make sure the message is registered even if no actions and no views exist m_messageHash[message] = QList >(); // reparent actions, as we want full control over when they are deleted foreach (QAction *action, message->actions()) { action->setParent(nullptr); m_messageHash[message].append(QSharedPointer(action)); } // post message to requested view, or to all views if (KTextEditor::ViewPrivate *view = qobject_cast(message->view())) { view->postMessage(message, m_messageHash[message]); } else { foreach (KTextEditor::ViewPrivate *view, m_views) { view->postMessage(message, m_messageHash[message]); } } // also catch if the user manually calls delete message connect(message, SIGNAL(closed(KTextEditor::Message*)), SLOT(messageDestroyed(KTextEditor::Message*))); 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/inputmode/kateviinputmode.cpp b/src/inputmode/kateviinputmode.cpp index 64af38ed..9da073c7 100644 --- a/src/inputmode/kateviinputmode.cpp +++ b/src/inputmode/kateviinputmode.cpp @@ -1,334 +1,334 @@ /* This file is part of the KDE libraries and the Kate part. * * 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 "kateviinputmode.h" #include "kateviewinternal.h" #include "kateconfig.h" #include #include #include #include #include #include #include #include #include namespace { QString viModeToString(KateVi::ViMode mode) { QString modeStr; switch (mode) { case KateVi::InsertMode: modeStr = i18n("VI: INSERT MODE"); break; case KateVi::NormalMode: modeStr = i18n("VI: NORMAL MODE"); break; case KateVi::VisualMode: modeStr = i18n("VI: VISUAL"); break; case KateVi::VisualBlockMode: modeStr = i18n("VI: VISUAL BLOCK"); break; case KateVi::VisualLineMode: modeStr = i18n("VI: VISUAL LINE"); break; case KateVi::ReplaceMode: modeStr = i18n("VI: REPLACE"); break; } return modeStr; } } KateViInputMode::KateViInputMode(KateViewInternal *viewInternal, KateVi::GlobalState *global) : KateAbstractInputMode(viewInternal) , m_viModeEmulatedCommandBar(nullptr) , m_viGlobal(global) , m_caret(KateRenderer::Block) , m_nextKeypressIsOverriddenShortCut(false) , m_activated(false) { m_relLineNumbers = KateViewConfig::global()->viRelativeLineNumbers(); m_viModeManager = new KateVi::InputModeManager(this, view(), viewInternal); } KateViInputMode::~KateViInputMode() { delete m_viModeManager; } void KateViInputMode::activate() { m_activated = true; setCaretStyle(KateRenderer::Block); // TODO: can we end up in insert mode? reset(); // TODO: is this necessary? (well, not anymore I guess) if (view()->selection()) { m_viModeManager->changeViMode(KateVi::VisualMode); view()->setCursorPosition(KTextEditor::Cursor(view()->selectionRange().end().line(), view()->selectionRange().end().column() - 1)); m_viModeManager->m_viVisualMode->updateSelection(); } viewInternal()->iconBorder()->setRelLineNumbersOn(m_relLineNumbers); } void KateViInputMode::deactivate() { if (m_viModeEmulatedCommandBar) { m_viModeEmulatedCommandBar->hideMe(); } // make sure to turn off edits merging when leaving vi input mode view()->doc()->setUndoMergeAllEdits(false); m_activated = false; viewInternal()->iconBorder()->setRelLineNumbersOn(false); } void KateViInputMode::reset() { if (m_viModeEmulatedCommandBar) { m_viModeEmulatedCommandBar->hideMe(); } delete m_viModeManager; m_viModeManager = new KateVi::InputModeManager(this, view(), viewInternal()); if (m_viModeEmulatedCommandBar) { m_viModeEmulatedCommandBar->setViInputModeManager(m_viModeManager); } } bool KateViInputMode::overwrite() const { return m_viModeManager->getCurrentViMode() == KateVi::ViMode::ReplaceMode; } void KateViInputMode::overwrittenChar(const QChar &c) { m_viModeManager->getViReplaceMode()->overwrittenChar(c); } void KateViInputMode::clearSelection() { // do nothing, handled elsewhere } bool KateViInputMode::stealKey(QKeyEvent *k) { if (!KateViewConfig::global()->viInputModeStealKeys()) { return false; } // Actually see if we can make use of this key - if so, we've stolen it; if not, // let Qt's shortcut handling system deal with it. const bool stolen = keyPress(k); if (stolen) { // Qt will replay this QKeyEvent, next time as an ordinary KeyPress. m_nextKeypressIsOverriddenShortCut = true; } return stolen; } KTextEditor::View::InputMode KateViInputMode::viewInputMode() const { return KTextEditor::View::ViInputMode; } QString KateViInputMode::viewInputModeHuman() const { return i18n("vi-mode"); } KTextEditor::View::ViewMode KateViInputMode::viewMode() const { return m_viModeManager->getCurrentViewMode(); } QString KateViInputMode::viewModeHuman() const { QString currentMode = viModeToString(m_viModeManager->getCurrentViMode()); if (m_viModeManager->macroRecorder()->isRecording()) { - currentMode.prepend(QLatin1String("(") + i18n("recording") + QLatin1String(") ")); + currentMode.prepend(QLatin1Char('(') + i18n("recording") + QLatin1String(") ")); } QString cmd = m_viModeManager->getVerbatimKeys(); if (!cmd.isEmpty()) { currentMode.prepend(QStringLiteral("%1 ").arg(cmd)); } return QStringLiteral("%1").arg(currentMode); } void KateViInputMode::gotFocus() { // nothing to do } void KateViInputMode::lostFocus() { // nothing to do } void KateViInputMode::readSessionConfig(const KConfigGroup &config) { // restore vi registers and jump list m_viModeManager->readSessionConfig(config); } void KateViInputMode::writeSessionConfig(KConfigGroup &config) { // save vi registers and jump list m_viModeManager->writeSessionConfig(config); } void KateViInputMode::updateConfig() { KateViewConfig *cfg = view()->config(); // whether relative line numbers should be used or not. m_relLineNumbers = cfg->viRelativeLineNumbers(); if (m_activated) { viewInternal()->iconBorder()->setRelLineNumbersOn(m_relLineNumbers); } } void KateViInputMode::readWriteChanged(bool) { // nothing todo } void KateViInputMode::find() { showViModeEmulatedCommandBar(); viModeEmulatedCommandBar()->init(KateVi::EmulatedCommandBar::SearchForward); } void KateViInputMode::findSelectedForwards() { m_viModeManager->searcher()->findNext(); } void KateViInputMode::findSelectedBackwards() { m_viModeManager->searcher()->findPrevious(); } void KateViInputMode::findReplace() { showViModeEmulatedCommandBar(); viModeEmulatedCommandBar()->init(KateVi::EmulatedCommandBar::SearchForward); } void KateViInputMode::findNext() { m_viModeManager->searcher()->findNext(); } void KateViInputMode::findPrevious() { m_viModeManager->searcher()->findPrevious(); } void KateViInputMode::activateCommandLine() { showViModeEmulatedCommandBar(); viModeEmulatedCommandBar()->init(KateVi::EmulatedCommandBar::Command); } void KateViInputMode::showViModeEmulatedCommandBar() { view()->bottomViewBar()->addBarWidget(viModeEmulatedCommandBar()); view()->bottomViewBar()->showBarWidget(viModeEmulatedCommandBar()); } KateVi::EmulatedCommandBar *KateViInputMode::viModeEmulatedCommandBar() { if (!m_viModeEmulatedCommandBar) { m_viModeEmulatedCommandBar = new KateVi::EmulatedCommandBar(this, m_viModeManager, view()); m_viModeEmulatedCommandBar->hide(); } return m_viModeEmulatedCommandBar; } void KateViInputMode::updateRendererConfig() { // do nothing } bool KateViInputMode::keyPress(QKeyEvent *e) { if (m_nextKeypressIsOverriddenShortCut) { // This is just the replay of a shortcut that we stole, this time as a QKeyEvent. // Ignore it, as we'll have already handled it via stealKey()! m_nextKeypressIsOverriddenShortCut = false; return true; } if (m_viModeManager->handleKeypress(e)) { emit view()->viewModeChanged(view(), viewMode()); return true; } return false; } bool KateViInputMode::blinkCaret() const { return false; } KateRenderer::caretStyles KateViInputMode::caretStyle() const { return m_caret; } void KateViInputMode::toggleInsert() { // do nothing } void KateViInputMode::launchInteractiveCommand(const QString &) { // do nothing so far } QString KateViInputMode::bookmarkLabel(int line) const { return m_viModeManager->marks()->getMarksOnTheLine(line); } void KateViInputMode::setCaretStyle(const KateRenderer::caretStyles caret) { if (m_caret != caret) { m_caret = caret; view()->renderer()->setCaretStyle(m_caret); view()->renderer()->setDrawCaret(true); viewInternal()->paintCursor(); } } diff --git a/src/mode/katemodeconfigpage.cpp b/src/mode/katemodeconfigpage.cpp index d477e0ca..39a7f76c 100644 --- a/src/mode/katemodeconfigpage.cpp +++ b/src/mode/katemodeconfigpage.cpp @@ -1,303 +1,303 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2001-2010 Christoph Cullmann * * 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. */ //BEGIN Includes #include "katemodeconfigpage.h" #include "katedocument.h" #include "kateconfig.h" #include "kateview.h" #include "kateglobal.h" #include "kateautoindent.h" #include "katesyntaxmanager.h" #include "ui_filetypeconfigwidget.h" #include #include "katepartdebug.h" #include #include #include #include #include #include #include #include #include //END Includes ModeConfigPage::ModeConfigPage(QWidget *parent) : KateConfigPage(parent) { m_lastType = -1; // This will let us have more separation between this page and // the QTabWidget edge (ereslibre) QVBoxLayout *layout = new QVBoxLayout; QWidget *newWidget = new QWidget(this); ui = new Ui::FileTypeConfigWidget(); ui->setupUi(newWidget); ui->cmbHl->addItem(i18n(""), QVariant(QString())); for (const auto &hl : KateHlManager::self()->modeList()) { if (hl.section().length() > 0) - ui->cmbHl->addItem(hl.section() + QLatin1String("/") + hl.translatedName(), QVariant(hl.name())); + ui->cmbHl->addItem(hl.section() + QLatin1Char('/') + hl.translatedName(), QVariant(hl.name())); else { ui->cmbHl->addItem(hl.translatedName(), QVariant(hl.name())); } } QStringList indentationModes; indentationModes << i18n("Use Default"); indentationModes << KateAutoIndent::listModes(); ui->cmbIndenter->addItems(indentationModes); connect(ui->cmbFiletypes, SIGNAL(activated(int)), this, SLOT(typeChanged(int))); connect(ui->btnNew, SIGNAL(clicked()), this, SLOT(newType())); connect(ui->btnDelete, SIGNAL(clicked()), this, SLOT(deleteType())); ui->btnMimeTypes->setIcon(QIcon::fromTheme(QStringLiteral("tools-wizard"))); connect(ui->btnMimeTypes, SIGNAL(clicked()), this, SLOT(showMTDlg())); reload(); connect(ui->edtName, SIGNAL(textChanged(QString)), this, SLOT(slotChanged())); connect(ui->edtSection, SIGNAL(textChanged(QString)), this, SLOT(slotChanged())); connect(ui->edtVariables, SIGNAL(textChanged(QString)), this, SLOT(slotChanged())); connect(ui->edtFileExtensions, SIGNAL(textChanged(QString)), this, SLOT(slotChanged())); connect(ui->edtMimeTypes, SIGNAL(textChanged(QString)), this, SLOT(slotChanged())); connect(ui->sbPriority, SIGNAL(valueChanged(int)), this, SLOT(slotChanged())); connect(ui->cmbHl, SIGNAL(activated(int)), this, SLOT(slotChanged())); connect(ui->cmbIndenter, SIGNAL(activated(int)), this, SLOT(slotChanged())); // make the context help a bit easier to access ui->sbPriority->setToolTip(ui->sbPriority->whatsThis()); layout->addWidget(newWidget); setLayout(layout); } ModeConfigPage::~ModeConfigPage() { qDeleteAll(m_types); delete ui; } void ModeConfigPage::apply() { if (!hasChanged()) { return; } save(); KTextEditor::EditorPrivate::self()->modeManager()->save(m_types); } void ModeConfigPage::reload() { qDeleteAll(m_types); m_types.clear(); // deep copy... foreach (KateFileType *type, KTextEditor::EditorPrivate::self()->modeManager()->list()) { KateFileType *t = new KateFileType(); *t = *type; m_types.append(t); } update(); } void ModeConfigPage::reset() { reload(); } void ModeConfigPage::defaults() { reload(); } void ModeConfigPage::update() { m_lastType = -1; ui->cmbFiletypes->clear(); foreach (KateFileType *type, m_types) { if (!type->sectionTranslated().isEmpty()) { - ui->cmbFiletypes->addItem(type->sectionTranslated() + QLatin1String("/") + type->nameTranslated()); + ui->cmbFiletypes->addItem(type->sectionTranslated() + QLatin1Char('/') + type->nameTranslated()); } else { ui->cmbFiletypes->addItem(type->nameTranslated()); } } // get current filetype from active view via the host application int currentIndex = 0; KTextEditor::ViewPrivate *kv = qobject_cast(KTextEditor::EditorPrivate::self()->application()->activeMainWindow()->activeView()); if (kv) { const QString filetypeName = kv->doc()->fileType(); for (int i = 0; i < m_types.size(); ++i) { if (filetypeName == m_types[i]->name) { currentIndex = i; break; } } } ui->cmbFiletypes->setCurrentIndex(currentIndex); typeChanged(currentIndex); ui->cmbFiletypes->setEnabled(ui->cmbFiletypes->count() > 0); } void ModeConfigPage::deleteType() { int type = ui->cmbFiletypes->currentIndex(); if (type > -1 && type < m_types.count()) { delete m_types[type]; m_types.removeAt(type); update(); } } void ModeConfigPage::newType() { QString newN = i18n("New Filetype"); for (int i = 0; i < m_types.count(); ++i) { KateFileType *type = m_types.at(i); if (type->name == newN) { ui->cmbFiletypes->setCurrentIndex(i); typeChanged(i); return; } } KateFileType *newT = new KateFileType(); newT->priority = 0; newT->name = newN; newT->hlGenerated = false; m_types.prepend(newT); update(); } void ModeConfigPage::save() { if (m_lastType != -1) { if (!m_types[m_lastType]->hlGenerated) { m_types[m_lastType]->name = ui->edtName->text(); m_types[m_lastType]->section = ui->edtSection->text(); } m_types[m_lastType]->varLine = ui->edtVariables->text(); m_types[m_lastType]->wildcards = ui->edtFileExtensions->text().split(QLatin1Char(';'), QString::SkipEmptyParts); m_types[m_lastType]->mimetypes = ui->edtMimeTypes->text().split(QLatin1Char(';'), QString::SkipEmptyParts); m_types[m_lastType]->priority = ui->sbPriority->value(); m_types[m_lastType]->hl = ui->cmbHl->itemData(ui->cmbHl->currentIndex()).toString(); if (ui->cmbIndenter->currentIndex() > 0) { m_types[m_lastType]->indenter = KateAutoIndent::modeName(ui->cmbIndenter->currentIndex() - 1); } else { m_types[m_lastType]->indenter = QString(); } } } void ModeConfigPage::typeChanged(int type) { save(); ui->cmbHl->setEnabled(true); ui->btnDelete->setEnabled(true); ui->edtName->setEnabled(true); ui->edtSection->setEnabled(true); if (type > -1 && type < m_types.count()) { KateFileType *t = m_types.at(type); ui->gbProperties->setTitle(i18n("Properties of %1", ui->cmbFiletypes->currentText())); ui->gbProperties->setEnabled(true); ui->btnDelete->setEnabled(true); ui->edtName->setText(t->nameTranslated()); ui->edtSection->setText(t->sectionTranslated()); ui->edtVariables->setText(t->varLine); ui->edtFileExtensions->setText(t->wildcards.join(QLatin1Char(';'))); ui->edtMimeTypes->setText(t->mimetypes.join(QLatin1Char(';'))); ui->sbPriority->setValue(t->priority); ui->cmbHl->setEnabled(!t->hlGenerated); ui->btnDelete->setEnabled(!t->hlGenerated); ui->edtName->setEnabled(!t->hlGenerated); ui->edtSection->setEnabled(!t->hlGenerated); // activate current hl... for (int i = 0; i < ui->cmbHl->count(); ++i) if (ui->cmbHl->itemData(i).toString() == t->hl) { ui->cmbHl->setCurrentIndex(i); } // activate the right indenter int indenterIndex = 0; if (!t->indenter.isEmpty()) { indenterIndex = KateAutoIndent::modeNumber(t->indenter) + 1; } ui->cmbIndenter->setCurrentIndex(indenterIndex); } else { ui->gbProperties->setTitle(i18n("Properties")); ui->gbProperties->setEnabled(false); ui->btnDelete->setEnabled(false); ui->edtName->clear(); ui->edtSection->clear(); ui->edtVariables->clear(); ui->edtFileExtensions->clear(); ui->edtMimeTypes->clear(); ui->sbPriority->setValue(0); ui->cmbHl->setCurrentIndex(0); ui->cmbIndenter->setCurrentIndex(0); } m_lastType = type; } void ModeConfigPage::showMTDlg() { QString text = i18n("Select the MimeTypes you want for this file type.\nPlease note that this will automatically edit the associated file extensions as well."); QStringList list = ui->edtMimeTypes->text().split(QRegularExpression(QStringLiteral("\\s*;\\s*")), QString::SkipEmptyParts); KMimeTypeChooserDialog d(i18n("Select Mime Types"), text, list, QStringLiteral("text"), this); if (d.exec() == QDialog::Accepted) { // do some checking, warn user if mime types or patterns are removed. // if the lists are empty, and the fields not, warn. ui->edtFileExtensions->setText(d.chooser()->patterns().join(QLatin1Char(';'))); ui->edtMimeTypes->setText(d.chooser()->mimeTypes().join(QLatin1Char(';'))); } } QString ModeConfigPage::name() const { return i18n("Modes && Filetypes"); } diff --git a/src/schema/kateschemaconfig.cpp b/src/schema/kateschemaconfig.cpp index a66b7292..3dd038c8 100644 --- a/src/schema/kateschemaconfig.cpp +++ b/src/schema/kateschemaconfig.cpp @@ -1,1380 +1,1380 @@ /* This file is part of the KDE libraries Copyright (C) 2007, 2008 Matthew Woehlke Copyright (C) 2001-2003 Christoph Cullmann Copyright (C) 2002, 2003 Anders Lund Copyright (C) 2012-2018 Dominik Haumann * * 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. */ //BEGIN Includes #include "kateschemaconfig.h" #include "katedocument.h" #include "kateschema.h" #include "kateconfig.h" #include "kateglobal.h" #include "kateview.h" #include "katerenderer.h" #include "katestyletreewidget.h" #include "katecolortreewidget.h" #include "katepartdebug.h" #include "katedefaultcolors.h" #include "ui_howtoimportschema.h" #include #include #include #include #include #include #include #include #include //END //BEGIN KateSchemaConfigColorTab -- 'Colors' tab KateSchemaConfigColorTab::KateSchemaConfigColorTab() { QGridLayout *l = new QGridLayout(this); setLayout(l); ui = new KateColorTreeWidget(this); QPushButton *btnUseColorScheme = new QPushButton(i18n("Use KDE Color Scheme"), this); l->addWidget(ui, 0, 0, 1, 2); l->addWidget(btnUseColorScheme, 1, 1); l->setColumnStretch(0, 1); l->setColumnStretch(1, 0); connect(btnUseColorScheme, SIGNAL(clicked()), ui, SLOT(selectDefaults())); connect(ui, SIGNAL(changed()), SIGNAL(changed())); } KateSchemaConfigColorTab::~KateSchemaConfigColorTab() { } QVector KateSchemaConfigColorTab::colorItemList() const { QVector items; // use global color instance, creation is expensive! const KateDefaultColors &colors(KTextEditor::EditorPrivate::self()->defaultColors()); // // editor background colors // KateColorItem ci; ci.category = i18n("Editor Background Colors"); ci.name = i18n("Text Area"); ci.key = QStringLiteral("Color Background"); ci.whatsThis = i18n("

Sets the background color of the editing area.

"); ci.defaultColor = colors.color(Kate::Background); items.append(ci); ci.name = i18n("Selected Text"); ci.key = QStringLiteral("Color Selection"); ci.whatsThis = i18n("

Sets the background color of the selection.

To set the text color for selected text, use the "Configure Highlighting" dialog.

"); ci.defaultColor = colors.color(Kate::SelectionBackground); items.append(ci); ci.name = i18n("Current Line"); ci.key = QStringLiteral("Color Highlighted Line"); ci.whatsThis = i18n("

Sets the background color of the currently active line, which means the line where your cursor is positioned.

"); ci.defaultColor = colors.color(Kate::HighlightedLineBackground); items.append(ci); ci.name = i18n("Search Highlight"); ci.key = QStringLiteral("Color Search Highlight"); ci.whatsThis = i18n("

Sets the background color of search results.

"); ci.defaultColor = colors.color(Kate::SearchHighlight); items.append(ci); ci.name = i18n("Replace Highlight"); ci.key = QStringLiteral("Color Replace Highlight"); ci.whatsThis = i18n("

Sets the background color of replaced text.

"); ci.defaultColor = colors.color(Kate::ReplaceHighlight); items.append(ci); // // icon border // ci.category = i18n("Icon Border"); ci.name = i18n("Background Area"); ci.key = QStringLiteral("Color Icon Bar"); ci.whatsThis = i18n("

Sets the background color of the icon border.

"); ci.defaultColor = colors.color(Kate::IconBar); items.append(ci); ci.name = i18n("Line Numbers"); ci.key = QStringLiteral("Color Line Number"); ci.whatsThis = i18n("

This color will be used to draw the line numbers (if enabled).

"); ci.defaultColor = colors.color(Kate::LineNumber); items.append(ci); ci.name = i18n("Current Line Number"); ci.key = QStringLiteral("Color Current Line Number"); ci.whatsThis = i18n("

This color will be used to draw the number of the current line (if enabled).

"); ci.defaultColor = colors.color(Kate::CurrentLineNumber); items.append(ci); ci.name = i18n("Separator"); ci.key = QStringLiteral("Color Separator"); ci.whatsThis = i18n("

This color will be used to draw the line between line numbers and the icon borders, if both are enabled.

"); ci.defaultColor = colors.color(Kate::Separator); items.append(ci); ci.name = i18n("Word Wrap Marker"); ci.key = QStringLiteral("Color Word Wrap Marker"); ci.whatsThis = i18n("

Sets the color of Word Wrap-related markers:

Static Word Wrap
A vertical line which shows the column where text is going to be wrapped
Dynamic Word Wrap
An arrow shown to the left of visually-wrapped lines
"); ci.defaultColor = colors.color(Kate::WordWrapMarker); items.append(ci); ci.name = i18n("Code Folding"); ci.key = QStringLiteral("Color Code Folding"); ci.whatsThis = i18n("

Sets the color of the code folding bar.

"); ci.defaultColor = colors.color(Kate::CodeFolding); items.append(ci); ci.name = i18n("Modified Lines"); ci.key = QStringLiteral("Color Modified Lines"); ci.whatsThis = i18n("

Sets the color of the line modification marker for modified lines.

"); ci.defaultColor = colors.color(Kate::ModifiedLine); items.append(ci); ci.name = i18n("Saved Lines"); ci.key = QStringLiteral("Color Saved Lines"); ci.whatsThis = i18n("

Sets the color of the line modification marker for saved lines.

"); ci.defaultColor = colors.color(Kate::SavedLine); items.append(ci); // // text decorations // ci.category = i18n("Text Decorations"); ci.name = i18n("Spelling Mistake Line"); ci.key = QStringLiteral("Color Spelling Mistake Line"); ci.whatsThis = i18n("

Sets the color of the line that is used to indicate spelling mistakes.

"); ci.defaultColor = colors.color(Kate::SpellingMistakeLine); items.append(ci); ci.name = i18n("Tab and Space Markers"); ci.key = QStringLiteral("Color Tab Marker"); ci.whatsThis = i18n("

Sets the color of the tabulator marks.

"); ci.defaultColor = colors.color(Kate::TabMarker); items.append(ci); ci.name = i18n("Indentation Line"); ci.key = QStringLiteral("Color Indentation Line"); ci.whatsThis = i18n("

Sets the color of the vertical indentation lines.

"); ci.defaultColor = colors.color(Kate::IndentationLine); items.append(ci); ci.name = i18n("Bracket Highlight"); ci.key = QStringLiteral("Color Highlighted Bracket"); ci.whatsThis = i18n("

Sets the bracket matching color. This means, if you place the cursor e.g. at a (, the matching ) will be highlighted with this color.

"); ci.defaultColor = colors.color(Kate::HighlightedBracket); items.append(ci); // // marker colors // ci.category = i18n("Marker Colors"); const QString markerNames[Kate::LAST_MARK + 1] = { i18n("Bookmark"), i18n("Active Breakpoint"), i18n("Reached Breakpoint"), i18n("Disabled Breakpoint"), i18n("Execution"), i18n("Warning"), i18n("Error") }; ci.whatsThis = i18n("

Sets the background color of mark type.

Note: The marker color is displayed lightly because of transparency.

"); for (int i = Kate::FIRST_MARK; i <= Kate::LAST_MARK; ++i) { ci.defaultColor = colors.mark(i); ci.name = markerNames[i]; ci.key = QLatin1String("Color MarkType ") + QString::number(i + 1); items.append(ci); } // // text templates // ci.category = i18n("Text Templates & Snippets"); ci.whatsThis = QString(); // TODO: add whatsThis for text templates ci.name = i18n("Background"); ci.key = QStringLiteral("Color Template Background"); ci.defaultColor = colors.color(Kate::TemplateBackground); items.append(ci); ci.name = i18n("Editable Placeholder"); ci.key = QStringLiteral("Color Template Editable Placeholder"); ci.defaultColor = colors.color(Kate::TemplateEditablePlaceholder); items.append(ci); ci.name = i18n("Focused Editable Placeholder"); ci.key = QStringLiteral("Color Template Focused Editable Placeholder"); ci.defaultColor = colors.color(Kate::TemplateFocusedEditablePlaceholder); items.append(ci); ci.name = i18n("Not Editable Placeholder"); ci.key = QStringLiteral("Color Template Not Editable Placeholder"); ci.defaultColor = colors.color(Kate::TemplateNotEditablePlaceholder); items.append(ci); // // finally, add all elements // return items; } void KateSchemaConfigColorTab::schemaChanged(const QString &newSchema) { // save current schema if (!m_currentSchema.isEmpty()) { if (m_schemas.contains(m_currentSchema)) { m_schemas.remove(m_currentSchema); // clear this color schema } // now add it again m_schemas[m_currentSchema] = ui->colorItems(); } if (newSchema == m_currentSchema) { return; } // switch m_currentSchema = newSchema; // If we havent this schema, read in from config file if (! m_schemas.contains(newSchema)) { KConfigGroup config = KTextEditor::EditorPrivate::self()->schemaManager()->schema(newSchema); QVector items = readConfig(config); m_schemas[ newSchema ] = items; } // first block signals otherwise setColor emits changed const bool blocked = blockSignals(true); ui->clear(); ui->addColorItems(m_schemas[m_currentSchema]); blockSignals(blocked); } QVector KateSchemaConfigColorTab::readConfig(KConfigGroup &config) { QVector items = colorItemList(); for (int i = 0; i < items.count(); ++i) { KateColorItem &item(items[i]); item.useDefault = !config.hasKey(item.key); if (item.useDefault) { item.color = item.defaultColor; } else { item.color = config.readEntry(item.key, item.defaultColor); if (!item.color.isValid()) { config.deleteEntry(item.key); item.useDefault = true; item.color = item.defaultColor; } } } return items; } void KateSchemaConfigColorTab::importSchema(KConfigGroup &config) { m_schemas[m_currentSchema] = readConfig(config); // first block signals otherwise setColor emits changed const bool blocked = blockSignals(true); ui->clear(); ui->addColorItems(m_schemas[m_currentSchema]); blockSignals(blocked); } void KateSchemaConfigColorTab::exportSchema(KConfigGroup &config) { QVector items = ui->colorItems(); foreach (const KateColorItem &item, items) { QColor c = item.useDefault ? item.defaultColor : item.color; config.writeEntry(item.key, c); } } void KateSchemaConfigColorTab::apply() { schemaChanged(m_currentSchema); QMap >::Iterator it; for (it = m_schemas.begin(); it != m_schemas.end(); ++it) { KConfigGroup config = KTextEditor::EditorPrivate::self()->schemaManager()->schema(it.key()); foreach (const KateColorItem &item, m_schemas[it.key()]) { if (item.useDefault) { config.deleteEntry(item.key); } else { config.writeEntry(item.key, item.color); } } // add dummy entry to prevent the config group from being empty. // As if the group is empty, KateSchemaManager will not find it anymore. config.writeEntry("dummy", "prevent-empty-group"); } // all colors are written, so throw away all cached schemas m_schemas.clear(); } void KateSchemaConfigColorTab::reload() { // drop all cached data m_schemas.clear(); // load from config KConfigGroup config = KTextEditor::EditorPrivate::self()->schemaManager()->schema(m_currentSchema); QVector items = readConfig(config); // first block signals otherwise setColor emits changed const bool blocked = blockSignals(true); ui->clear(); ui->addColorItems(items); blockSignals(blocked); } QColor KateSchemaConfigColorTab::backgroundColor() const { return ui->findColor(QStringLiteral("Color Background")); } QColor KateSchemaConfigColorTab::selectionColor() const { return ui->findColor(QStringLiteral("Color Selection")); } //END KateSchemaConfigColorTab //BEGIN FontConfig -- 'Fonts' tab KateSchemaConfigFontTab::KateSchemaConfigFontTab() { QGridLayout *grid = new QGridLayout(this); m_fontchooser = new KFontChooser(this, KFontChooser::NoDisplayFlags); grid->addWidget(m_fontchooser, 0, 0); } KateSchemaConfigFontTab::~KateSchemaConfigFontTab() { } void KateSchemaConfigFontTab::slotFontSelected(const QFont &font) { if (!m_currentSchema.isEmpty()) { m_fonts[m_currentSchema] = font; emit changed(); } } void KateSchemaConfigFontTab::apply() { QMap::Iterator it; for (it = m_fonts.begin(); it != m_fonts.end(); ++it) { KTextEditor::EditorPrivate::self()->schemaManager()->schema(it.key()).writeEntry("Font", it.value()); } // all fonts are written, so throw away all cached schemas m_fonts.clear(); } void KateSchemaConfigFontTab::reload() { // drop all cached data m_fonts.clear(); // now set current schema font in the font chooser schemaChanged(m_currentSchema); } void KateSchemaConfigFontTab::schemaChanged(const QString &newSchema) { m_currentSchema = newSchema; // reuse font, if cached QFont newFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); if (m_fonts.contains(m_currentSchema)) { newFont = m_fonts[m_currentSchema]; } else { newFont = KTextEditor::EditorPrivate::self()->schemaManager()->schema(m_currentSchema).readEntry("Font", newFont); } m_fontchooser->disconnect(this); m_fontchooser->setFont(newFont); connect(m_fontchooser, SIGNAL(fontSelected(QFont)), this, SLOT(slotFontSelected(QFont))); } void KateSchemaConfigFontTab::importSchema(KConfigGroup &config) { QFont f(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_fontchooser->setFont(config.readEntry("Font", f)); m_fonts[m_currentSchema] = m_fontchooser->font(); } void KateSchemaConfigFontTab::exportSchema(KConfigGroup &config) { config.writeEntry("Font", m_fontchooser->font()); } //END FontConfig //BEGIN FontColorConfig -- 'Normal Text Styles' tab KateSchemaConfigDefaultStylesTab::KateSchemaConfigDefaultStylesTab(KateSchemaConfigColorTab *colorTab) { m_colorTab = colorTab; // size management QGridLayout *grid = new QGridLayout(this); m_defaultStyles = new KateStyleTreeWidget(this); connect(m_defaultStyles, SIGNAL(changed()), this, SIGNAL(changed())); grid->addWidget(m_defaultStyles, 0, 0); m_defaultStyles->setWhatsThis(i18n( "

This list displays the default styles for the current schema and " "offers the means to edit them. The style name reflects the current " "style settings.

" "

To edit the colors, click the colored squares, or select the color " "to edit from the popup menu.

You can unset the Background and Selected " "Background colors from the popup menu when appropriate.

")); } KateSchemaConfigDefaultStylesTab::~KateSchemaConfigDefaultStylesTab() { qDeleteAll(m_defaultStyleLists); } KateAttributeList *KateSchemaConfigDefaultStylesTab::attributeList(const QString &schema) { if (!m_defaultStyleLists.contains(schema)) { KateAttributeList *list = new KateAttributeList(); KateHlManager::self()->getDefaults(schema, *list); m_defaultStyleLists.insert(schema, list); } return m_defaultStyleLists[schema]; } void KateSchemaConfigDefaultStylesTab::schemaChanged(const QString &schema) { m_currentSchema = schema; m_defaultStyles->clear(); KateAttributeList *l = attributeList(schema); updateColorPalette(l->at(0)->foreground().color()); // normal text and source code QTreeWidgetItem *parent = new QTreeWidgetItem(m_defaultStyles, QStringList() << i18nc("@item:intable", "Normal Text & Source Code")); parent->setFirstColumnSpanned(true); for (int i = (int)KTextEditor::dsNormal; i <= (int)KTextEditor::dsAttribute; ++i) { m_defaultStyles->addItem(parent, KateHlManager::self()->defaultStyleName(i, true), l->at(i)); } // Number, Types & Constants parent = new QTreeWidgetItem(m_defaultStyles, QStringList() << i18nc("@item:intable", "Numbers, Types & Constants")); parent->setFirstColumnSpanned(true); for (int i = (int)KTextEditor::dsDataType; i <= (int)KTextEditor::dsConstant; ++i) { m_defaultStyles->addItem(parent, KateHlManager::self()->defaultStyleName(i, true), l->at(i)); } // strings & characters parent = new QTreeWidgetItem(m_defaultStyles, QStringList() << i18nc("@item:intable", "Strings & Characters")); parent->setFirstColumnSpanned(true); for (int i = (int)KTextEditor::dsChar; i <= (int)KTextEditor::dsImport; ++i) { m_defaultStyles->addItem(parent, KateHlManager::self()->defaultStyleName(i, true), l->at(i)); } // comments & documentation parent = new QTreeWidgetItem(m_defaultStyles, QStringList() << i18nc("@item:intable", "Comments & Documentation")); parent->setFirstColumnSpanned(true); for (int i = (int)KTextEditor::dsComment; i <= (int)KTextEditor::dsAlert; ++i) { m_defaultStyles->addItem(parent, KateHlManager::self()->defaultStyleName(i, true), l->at(i)); } // Misc parent = new QTreeWidgetItem(m_defaultStyles, QStringList() << i18nc("@item:intable", "Miscellaneous")); parent->setFirstColumnSpanned(true); for (int i = (int)KTextEditor::dsOthers; i <= (int)KTextEditor::dsError; ++i) { m_defaultStyles->addItem(parent, KateHlManager::self()->defaultStyleName(i, true), l->at(i)); } m_defaultStyles->expandAll(); } void KateSchemaConfigDefaultStylesTab::updateColorPalette(const QColor &textColor) { QPalette p(m_defaultStyles->palette()); p.setColor(QPalette::Base, m_colorTab->backgroundColor()); p.setColor(QPalette::Highlight, m_colorTab->selectionColor()); p.setColor(QPalette::Text, textColor); m_defaultStyles->setPalette(p); } void KateSchemaConfigDefaultStylesTab::reload() { m_defaultStyles->clear(); qDeleteAll(m_defaultStyleLists); m_defaultStyleLists.clear(); schemaChanged(m_currentSchema); } void KateSchemaConfigDefaultStylesTab::apply() { QHashIterator it = m_defaultStyleLists; while (it.hasNext()) { it.next(); KateHlManager::self()->setDefaults(it.key(), *it.value()); } } void KateSchemaConfigDefaultStylesTab::exportSchema(const QString &schema, KConfig *cfg) { KateHlManager::self()->setDefaults(schema, *(m_defaultStyleLists[schema]), cfg); } void KateSchemaConfigDefaultStylesTab::importSchema(const QString &schemaName, const QString &schema, KConfig *cfg) { KateHlManager::self()->getDefaults(schemaName, *(m_defaultStyleLists[schema]), cfg); } void KateSchemaConfigDefaultStylesTab::showEvent(QShowEvent *event) { if (!event->spontaneous() && !m_currentSchema.isEmpty()) { KateAttributeList *l = attributeList(m_currentSchema); Q_ASSERT(l != nullptr); updateColorPalette(l->at(0)->foreground().color()); } QWidget::showEvent(event); } //END FontColorConfig //BEGIN KateSchemaConfigHighlightTab -- 'Highlighting Text Styles' tab KateSchemaConfigHighlightTab::KateSchemaConfigHighlightTab(KateSchemaConfigDefaultStylesTab *page, KateSchemaConfigColorTab *colorTab) { m_defaults = page; m_colorTab = colorTab; m_hl = 0; QVBoxLayout *layout = new QVBoxLayout(this); QHBoxLayout *headerLayout = new QHBoxLayout; layout->addLayout(headerLayout); QLabel *lHl = new QLabel(i18n("H&ighlight:"), this); headerLayout->addWidget(lHl); hlCombo = new KComboBox(this); hlCombo->setEditable(false); headerLayout->addWidget(hlCombo); lHl->setBuddy(hlCombo); connect(hlCombo, SIGNAL(activated(int)), this, SLOT(hlChanged(int))); QPushButton *btnexport = new QPushButton(i18n("Export..."), this); headerLayout->addWidget(btnexport); connect(btnexport, SIGNAL(clicked()), this, SLOT(exportHl())); QPushButton *btnimport = new QPushButton(i18n("Import..."), this); headerLayout->addWidget(btnimport); connect(btnimport, SIGNAL(clicked()), this, SLOT(importHl())); headerLayout->addStretch(); for (const auto &hl : KateHlManager::self()->modeList()) { if (hl.section().length() > 0) { - hlCombo->addItem(hl.section() + QLatin1String("/") + hl.translatedName()); + hlCombo->addItem(hl.section() + QLatin1Char('/') + hl.translatedName()); } else { hlCombo->addItem(hl.translatedName()); } } hlCombo->setCurrentIndex(0); // styles listview m_styles = new KateStyleTreeWidget(this, true); connect(m_styles, SIGNAL(changed()), this, SIGNAL(changed())); layout->addWidget(m_styles, 999); // get current highlighting from the host application int hl = 0; KTextEditor::ViewPrivate *kv = qobject_cast(KTextEditor::EditorPrivate::self()->application()->activeMainWindow()->activeView()); if (kv) { const QString hlName = kv->doc()->highlight()->name(); hl = KateHlManager::self()->nameFind(hlName); Q_ASSERT(hl >= 0); } hlCombo->setCurrentIndex(hl); hlChanged(hl); m_styles->setWhatsThis(i18n( "

This list displays the contexts of the current syntax highlight mode and " "offers the means to edit them. The context name reflects the current " "style settings.

To edit using the keyboard, press " "<SPACE> and choose a property from the popup menu.

" "

To edit the colors, click the colored squares, or select the color " "to edit from the popup menu.

You can unset the Background and Selected " "Background colors from the context menu when appropriate.

")); } KateSchemaConfigHighlightTab::~KateSchemaConfigHighlightTab() { } void KateSchemaConfigHighlightTab::hlChanged(int z) { m_hl = z; schemaChanged(m_schema); } bool KateSchemaConfigHighlightTab::loadAllHlsForSchema(const QString &schema) { QProgressDialog progress(i18n("Loading all highlightings for schema"), QString(), 0, KateHlManager::self()->modeList().size(), this); progress.setWindowModality(Qt::WindowModal); for (int i = 0; i < KateHlManager::self()->modeList().size(); ++i) { if (!m_hlDict[schema].contains(i)) { QVector list; KateHlManager::self()->getHl(i)->getKateExtendedAttributeListCopy(schema, list); m_hlDict[schema].insert(i, list); } progress.setValue(progress.value() + 1); } progress.setValue(KateHlManager::self()->modeList().size()); return true; } void KateSchemaConfigHighlightTab::schemaChanged(const QString &schema) { m_schema = schema; m_styles->clear(); if (!m_hlDict.contains(m_schema)) { m_hlDict.insert(schema, QHash >()); } if (!m_hlDict[m_schema].contains(m_hl)) { QVector list; KateHlManager::self()->getHl(m_hl)->getKateExtendedAttributeListCopy(m_schema, list); m_hlDict[m_schema].insert(m_hl, list); } KateAttributeList *l = m_defaults->attributeList(schema); // Set listview colors updateColorPalette(l->at(0)->foreground().color()); QHash prefixes; QVector::ConstIterator it = m_hlDict[m_schema][m_hl].constBegin(); while (it != m_hlDict[m_schema][m_hl].constEnd()) { const KTextEditor::Attribute::Ptr itemData = *it; Q_ASSERT(itemData); // All stylenames have their language mode prefixed, e.g. HTML:Comment // split them and put them into nice substructures. int c = itemData->name().indexOf(QLatin1Char(':')); if (c > 0) { QString prefix = itemData->name().left(c); QString name = itemData->name().mid(c + 1); QTreeWidgetItem *parent = prefixes[prefix]; if (! parent) { parent = new QTreeWidgetItem(m_styles, QStringList() << prefix); m_styles->expandItem(parent); prefixes.insert(prefix, parent); } m_styles->addItem(parent, name, l->at(itemData->defaultStyle()), itemData); } else { m_styles->addItem(itemData->name(), l->at(itemData->defaultStyle()), itemData); } ++it; } m_styles->resizeColumns(); } void KateSchemaConfigHighlightTab::updateColorPalette(const QColor &textColor) { QPalette p(m_styles->palette()); p.setColor(QPalette::Base, m_colorTab->backgroundColor()); p.setColor(QPalette::Highlight, m_colorTab->selectionColor()); p.setColor(QPalette::Text, textColor); m_styles->setPalette(p); } void KateSchemaConfigHighlightTab::reload() { m_styles->clear(); m_hlDict.clear(); hlChanged(hlCombo->currentIndex()); } void KateSchemaConfigHighlightTab::apply() { QMutableHashIterator > > it = m_hlDict; while (it.hasNext()) { it.next(); QMutableHashIterator > it2 = it.value(); while (it2.hasNext()) { it2.next(); KateHlManager::self()->getHl(it2.key())->setKateExtendedAttributeList(it.key(), it2.value()); } } } QList KateSchemaConfigHighlightTab::hlsForSchema(const QString &schema) { return m_hlDict[schema].keys(); } void KateSchemaConfigHighlightTab::importHl(const QString &fromSchemaName, QString schema, int hl, KConfig *cfg) { QString schemaNameForLoading(fromSchemaName); QString hlName; bool doManage = (cfg == nullptr); if (schema.isEmpty()) { schema = m_schema; } if (doManage) { QString srcName = QFileDialog::getOpenFileName(this, i18n("Importing colors for single highlighting"), KateHlManager::self()->getHl(hl)->name() + QLatin1String(".katehlcolor"), QStringLiteral("%1 (*.katehlcolor)").arg(i18n("Kate color schema"))); if (srcName.isEmpty()) { return; } cfg = new KConfig(srcName, KConfig::SimpleConfig); KConfigGroup grp(cfg, "KateHLColors"); hlName = grp.readEntry("highlight", QString()); schemaNameForLoading = grp.readEntry("schema", QString()); if ((grp.readEntry("full schema", "true").toUpper() != QLatin1String("FALSE")) || hlName.isEmpty() || schemaNameForLoading.isEmpty()) { //ERROR - file format KMessageBox::information( this, i18n("File is not a single highlighting color file"), i18n("Fileformat error")); hl = -1; schemaNameForLoading = QString(); } else { hl = KateHlManager::self()->nameFind(hlName); if (hl == -1) { //hl not found KMessageBox::information( this, i18n("The selected file contains colors for a non existing highlighting:%1", hlName), i18n("Import failure")); hl = -1; schemaNameForLoading = QString(); } } } if ((hl != -1) && (!schemaNameForLoading.isEmpty())) { QVector list; KateHlManager::self()->getHl(hl)->getKateExtendedAttributeListCopy(schemaNameForLoading, list, cfg); KateHlManager::self()->getHl(hl)->setKateExtendedAttributeList(schema, list); m_hlDict[schema].insert(hl, list); } if (cfg && doManage) { apply(); delete cfg; cfg = nullptr; if ((hl != -1) && (!schemaNameForLoading.isEmpty())) { hlChanged(m_hl); KMessageBox::information( this, i18n("Colors have been imported for highlighting: %1", hlName), i18n("Import has finished")); } } } void KateSchemaConfigHighlightTab::exportHl(QString schema, int hl, KConfig *cfg) { bool doManage = (cfg == nullptr); if (schema.isEmpty()) { schema = m_schema; } if (hl == -1) { hl = m_hl; } QVector items = m_hlDict[schema][hl]; if (doManage) { QString destName = QFileDialog::getSaveFileName(this, i18n("Exporting colors for single highlighting: %1", KateHlManager::self()->getHl(hl)->name()), KateHlManager::self()->getHl(hl)->name() + QLatin1String(".katehlcolor"), QStringLiteral("%1 (*.katehlcolor)").arg(i18n("Kate color schema"))); if (destName.isEmpty()) { return; } cfg = new KConfig(destName, KConfig::SimpleConfig); KConfigGroup grp(cfg, "KateHLColors"); grp.writeEntry("highlight", KateHlManager::self()->getHl(hl)->name()); grp.writeEntry("schema", schema); grp.writeEntry("full schema", "false"); } KateHlManager::self()->getHl(hl)->setKateExtendedAttributeList(schema, items, cfg, doManage); if (doManage) { cfg->sync(); delete cfg; } } void KateSchemaConfigHighlightTab::showEvent(QShowEvent *event) { if (!event->spontaneous()) { KateAttributeList *l = m_defaults->attributeList(m_schema); Q_ASSERT(l != nullptr); updateColorPalette(l->at(0)->foreground().color()); } QWidget::showEvent(event); } //END KateSchemaConfigHighlightTab //BEGIN KateSchemaConfigPage -- Main dialog page KateSchemaConfigPage::KateSchemaConfigPage(QWidget *parent) : KateConfigPage(parent), m_currentSchema(-1) { QVBoxLayout *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); // header QHBoxLayout *headerLayout = new QHBoxLayout; layout->addLayout(headerLayout); QLabel *lHl = new QLabel(i18n("&Schema:"), this); headerLayout->addWidget(lHl); schemaCombo = new KComboBox(this); schemaCombo->setEditable(false); lHl->setBuddy(schemaCombo); headerLayout->addWidget(schemaCombo); connect(schemaCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(comboBoxIndexChanged(int))); QPushButton *btnnew = new QPushButton(i18n("&New..."), this); headerLayout->addWidget(btnnew); connect(btnnew, SIGNAL(clicked()), this, SLOT(newSchema())); btndel = new QPushButton(i18n("&Delete"), this); headerLayout->addWidget(btndel); connect(btndel, SIGNAL(clicked()), this, SLOT(deleteSchema())); QPushButton *btnexport = new QPushButton(i18n("Export..."), this); headerLayout->addWidget(btnexport); connect(btnexport, SIGNAL(clicked()), this, SLOT(exportFullSchema())); QPushButton *btnimport = new QPushButton(i18n("Import..."), this); headerLayout->addWidget(btnimport); connect(btnimport, SIGNAL(clicked()), this, SLOT(importFullSchema())); headerLayout->addStretch(); // tabs QTabWidget *tabWidget = new QTabWidget(this); layout->addWidget(tabWidget); m_colorTab = new KateSchemaConfigColorTab(); tabWidget->addTab(m_colorTab, i18n("Colors")); connect(m_colorTab, SIGNAL(changed()), SLOT(slotChanged())); m_fontTab = new KateSchemaConfigFontTab(); tabWidget->addTab(m_fontTab, i18n("Font")); connect(m_fontTab, SIGNAL(changed()), SLOT(slotChanged())); m_defaultStylesTab = new KateSchemaConfigDefaultStylesTab(m_colorTab); tabWidget->addTab(m_defaultStylesTab, i18n("Default Text Styles")); connect(m_defaultStylesTab, SIGNAL(changed()), SLOT(slotChanged())); m_highlightTab = new KateSchemaConfigHighlightTab(m_defaultStylesTab, m_colorTab); tabWidget->addTab(m_highlightTab, i18n("Highlighting Text Styles")); connect(m_highlightTab, SIGNAL(changed()), SLOT(slotChanged())); QHBoxLayout *footLayout = new QHBoxLayout; layout->addLayout(footLayout); lHl = new QLabel(i18n("&Default schema for %1:", QCoreApplication::applicationName()), this); footLayout->addWidget(lHl); defaultSchemaCombo = new KComboBox(this); footLayout->addWidget(defaultSchemaCombo); defaultSchemaCombo->setEditable(false); lHl->setBuddy(defaultSchemaCombo); reload(); connect(defaultSchemaCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotChanged())); } KateSchemaConfigPage::~KateSchemaConfigPage() { } void KateSchemaConfigPage::exportFullSchema() { // get save destination const QString currentSchemaName = m_currentSchema; QString destName = QFileDialog::getSaveFileName(this, i18n("Exporting color schema: %1", currentSchemaName), currentSchemaName + QLatin1String(".kateschema"), QStringLiteral("%1 (*.kateschema)").arg(i18n("Kate color schema"))); if (destName.isEmpty()) { return; } // open config file KConfig cfg(destName, KConfig::SimpleConfig); // // export editor Colors (background, ...) // KConfigGroup colorConfigGroup(&cfg, "Editor Colors"); m_colorTab->exportSchema(colorConfigGroup); // // export Default Styles // m_defaultStylesTab->exportSchema(m_currentSchema, &cfg); // // export Highlighting Text Styles // // force a load of all Highlighting Text Styles QStringList hlList; m_highlightTab->loadAllHlsForSchema(m_currentSchema); QList hls = m_highlightTab->hlsForSchema(m_currentSchema); int cnt = 0; QProgressDialog progress(i18n("Exporting schema"), QString(), 0, hls.count(), this); progress.setWindowModality(Qt::WindowModal); foreach (int hl, hls) { hlList << KateHlManager::self()->getHl(hl)->name(); m_highlightTab->exportHl(m_currentSchema, hl, &cfg); progress.setValue(++cnt); if (progress.wasCanceled()) { break; } } progress.setValue(hls.count()); KConfigGroup grp(&cfg, "KateSchema"); grp.writeEntry("full schema", "true"); grp.writeEntry("highlightings", hlList); grp.writeEntry("schema", currentSchemaName); m_fontTab->exportSchema(grp); cfg.sync(); } QString KateSchemaConfigPage::requestSchemaName(const QString &suggestedName) { QString schemaName = suggestedName; bool reask = true; do { QDialog howToImportDialog(this); Ui_KateHowToImportSchema howToImport; QVBoxLayout *mainLayout = new QVBoxLayout; howToImportDialog.setLayout(mainLayout); QWidget *w = new QWidget(&howToImportDialog); mainLayout->addWidget(w); howToImport.setupUi(w); QDialogButtonBox *buttons = new QDialogButtonBox(&howToImportDialog); mainLayout->addWidget(buttons); QPushButton *okButton = new QPushButton; okButton->setDefault(true); KGuiItem::assign(okButton, KStandardGuiItem::ok()); buttons->addButton(okButton, QDialogButtonBox::AcceptRole); connect(okButton, SIGNAL(clicked()), &howToImportDialog, SLOT(accept())); QPushButton *cancelButton = new QPushButton; KGuiItem::assign(cancelButton, KStandardGuiItem::cancel()); buttons->addButton(cancelButton, QDialogButtonBox::RejectRole); connect(cancelButton, SIGNAL(clicked()), &howToImportDialog, SLOT(reject())); // // if schema exists, prepare option to replace if (KTextEditor::EditorPrivate::self()->schemaManager()->schema(schemaName).exists()) { howToImport.radioReplaceExisting->show(); howToImport.radioReplaceExisting->setText(i18n("Replace existing schema %1", schemaName)); howToImport.radioReplaceExisting->setChecked(true); } else { howToImport.radioReplaceExisting->hide(); howToImport.newName->setText(schemaName); } // cancel pressed? if (howToImportDialog.exec() == QDialog::Rejected) { schemaName.clear(); reask = false; } // check what the user wants else { // replace existing if (howToImport.radioReplaceExisting->isChecked()) { reask = false; } // replace current else if (howToImport.radioReplaceCurrent->isChecked()) { schemaName = m_currentSchema; reask = false; } // new one, check again, whether the schema already exists else if (howToImport.radioAsNew->isChecked()) { schemaName = howToImport.newName->text(); if (KTextEditor::EditorPrivate::self()->schemaManager()->schema(schemaName).exists()) { reask = true; } else { reask = false; } } // should never happen else { reask = true; } } } while (reask); return schemaName; } void KateSchemaConfigPage::importFullSchema() { const QString srcName = QFileDialog::getOpenFileName(this, i18n("Importing Color Schema"), QString(), QStringLiteral("%1 (*.kateschema)").arg(i18n("Kate color schema"))); if (srcName.isEmpty()) { return; } // carete config + sanity check for full color schema KConfig cfg(srcName, KConfig::SimpleConfig); KConfigGroup schemaGroup(&cfg, "KateSchema"); if (schemaGroup.readEntry("full schema", "false").toUpper() != QLatin1String("TRUE")) { KMessageBox::sorry(this, i18n("The file does not contain a full color schema."), i18n("Fileformat error")); return; } // read color schema name const QStringList highlightings = schemaGroup.readEntry("highlightings", QStringList()); const QString fromSchemaName = schemaGroup.readEntry("schema", i18n("Name unspecified")); // request valid schema name const QString schemaName = requestSchemaName(fromSchemaName); if (schemaName.isEmpty()) { return; } // if the schema already exists, select it in the combo box if (schemaCombo->findData(schemaName) != -1) { schemaCombo->setCurrentIndex(schemaCombo->findData(schemaName)); } else { // it is really a new schema, easy meat :-) newSchema(schemaName); } // make sure the correct schema is activated schemaChanged(schemaName); // Finally, the correct schema is activated. // Next, start importing. // // import editor Colors (background, ...) // KConfigGroup colorConfigGroup(&cfg, "Editor Colors"); m_colorTab->importSchema(colorConfigGroup); // // import font // m_fontTab->importSchema(schemaGroup); // // import Default Styles // m_defaultStylesTab->importSchema(fromSchemaName, schemaName, &cfg); // // import all Highlighting Text Styles // // create mapping from highlighting name to internal id const int hlCount = KateHlManager::self()->modeList().size(); QHash nameToId; for (int i = 0; i < hlCount; ++i) { nameToId.insert(KateHlManager::self()->modeList().at(i).name(), i); } // may take some time, as we have > 200 highlightings int cnt = 0; QProgressDialog progress(i18n("Importing schema"), QString(), 0, highlightings.count(), this); progress.setWindowModality(Qt::WindowModal); foreach (const QString &hl, highlightings) { if (nameToId.contains(hl)) { const int i = nameToId[hl]; m_highlightTab->importHl(fromSchemaName, schemaName, i, &cfg); } progress.setValue(++cnt); } progress.setValue(highlightings.count()); } void KateSchemaConfigPage::apply() { // remember name + index const QString schemaName = schemaCombo->itemData(schemaCombo->currentIndex()).toString(); // first apply all tabs m_colorTab->apply(); m_fontTab->apply(); m_defaultStylesTab->apply(); m_highlightTab->apply(); // just sync the config and reload KTextEditor::EditorPrivate::self()->schemaManager()->config().sync(); KTextEditor::EditorPrivate::self()->schemaManager()->config().reparseConfiguration(); // clear all attributes for (int i = 0; i < KateHlManager::self()->modeList().size(); ++i) { KateHlManager::self()->getHl(i)->clearAttributeArrays(); } // than reload the whole stuff KateRendererConfig::global()->setSchema(defaultSchemaCombo->itemData(defaultSchemaCombo->currentIndex()).toString()); KateRendererConfig::global()->reloadSchema(); // sync the hl config for real KateHlManager::self()->getKConfig()->sync(); // KateSchemaManager::update() sorts the schema alphabetically, hence the // schema indexes change. Thus, repopulate the schema list... refillCombos(schemaCombo->itemData(schemaCombo->currentIndex()).toString(), defaultSchemaCombo->itemData(defaultSchemaCombo->currentIndex()).toString()); schemaChanged(schemaName); } void KateSchemaConfigPage::reload() { // now reload the config from disc KTextEditor::EditorPrivate::self()->schemaManager()->config().reparseConfiguration(); // reinitialize combo boxes refillCombos(KateRendererConfig::global()->schema(), KateRendererConfig::global()->schema()); // finally, activate the current schema again schemaChanged(schemaCombo->itemData(schemaCombo->currentIndex()).toString()); // all tabs need to reload to discard all the cached data, as the index // mapping may have changed m_colorTab->reload(); m_fontTab->reload(); m_defaultStylesTab->reload(); m_highlightTab->reload(); } void KateSchemaConfigPage::refillCombos(const QString &schemaName, const QString &defaultSchemaName) { schemaCombo->blockSignals(true); defaultSchemaCombo->blockSignals(true); // reinitialize combo boxes schemaCombo->clear(); defaultSchemaCombo->clear(); QList schemaList = KTextEditor::EditorPrivate::self()->schemaManager()->list(); foreach (const KateSchema &s, schemaList) { schemaCombo->addItem(s.translatedName(), s.rawName); defaultSchemaCombo->addItem(s.translatedName(), s.rawName); } // set the correct indexes again, fallback to always existing "Normal" int schemaIndex = schemaCombo->findData(schemaName); if (schemaIndex == -1) { schemaIndex = schemaCombo->findData(QLatin1String("Normal")); } int defaultSchemaIndex = defaultSchemaCombo->findData(defaultSchemaName); if (defaultSchemaIndex == -1) { defaultSchemaIndex = defaultSchemaCombo->findData(QLatin1String("Normal")); } Q_ASSERT(schemaIndex != -1); Q_ASSERT(defaultSchemaIndex != -1); defaultSchemaCombo->setCurrentIndex(defaultSchemaIndex); schemaCombo->setCurrentIndex(schemaIndex); schemaCombo->blockSignals(false); defaultSchemaCombo->blockSignals(false); } void KateSchemaConfigPage::reset() { reload(); } void KateSchemaConfigPage::defaults() { reload(); } void KateSchemaConfigPage::deleteSchema() { const int comboIndex = schemaCombo->currentIndex(); const QString schemaNameToDelete = schemaCombo->itemData(comboIndex).toString(); if (KTextEditor::EditorPrivate::self()->schemaManager()->schemaData(schemaNameToDelete).shippedDefaultSchema) { // Default and Printing schema cannot be deleted. return; } // kill group KTextEditor::EditorPrivate::self()->schemaManager()->config().deleteGroup(schemaNameToDelete); // fallback to Default schema schemaCombo->setCurrentIndex(schemaCombo->findData(QVariant(QStringLiteral("Normal")))); if (defaultSchemaCombo->currentIndex() == defaultSchemaCombo->findData(schemaNameToDelete)) { defaultSchemaCombo->setCurrentIndex(defaultSchemaCombo->findData(QVariant(QStringLiteral("Normal")))); } // remove schema from combo box schemaCombo->removeItem(comboIndex); defaultSchemaCombo->removeItem(comboIndex); // Reload the color tab, since it uses cached schemas m_colorTab->reload(); } bool KateSchemaConfigPage::newSchema(const QString &newName) { // get sane name QString schemaName(newName); if (newName.isEmpty()) { bool ok = false; schemaName = QInputDialog::getText(this, i18n("Name for New Schema"), i18n("Name:"), QLineEdit::Normal, i18n("New Schema"), &ok); if (!ok) { return false; } } // try if schema already around if (KTextEditor::EditorPrivate::self()->schemaManager()->schema(schemaName).exists()) { KMessageBox::information(this, i18n("

The schema %1 already exists.

Please choose a different schema name.

", schemaName), i18n("New Schema")); return false; } // append items to combo boxes schemaCombo->addItem(schemaName, QVariant(schemaName)); defaultSchemaCombo->addItem(schemaName, QVariant(schemaName)); // finally, activate new schema (last item in the list) schemaCombo->setCurrentIndex(schemaCombo->count() - 1); return true; } void KateSchemaConfigPage::schemaChanged(const QString &schema) { btndel->setEnabled(!KTextEditor::EditorPrivate::self()->schemaManager()->schemaData(schema).shippedDefaultSchema); // propagate changed schema to all tabs m_colorTab->schemaChanged(schema); m_fontTab->schemaChanged(schema); m_defaultStylesTab->schemaChanged(schema); m_highlightTab->schemaChanged(schema); // save current schema index m_currentSchema = schema; } void KateSchemaConfigPage::comboBoxIndexChanged(int currentIndex) { schemaChanged(schemaCombo->itemData(currentIndex).toString()); } QString KateSchemaConfigPage::name() const { return i18n("Fonts & Colors"); } QString KateSchemaConfigPage::fullName() const { return i18n("Font & Color Schemas"); } QIcon KateSchemaConfigPage::icon() const { return QIcon::fromTheme(QStringLiteral("preferences-desktop-color")); } //END KateSchemaConfigPage diff --git a/src/search/kateplaintextsearch.cpp b/src/search/kateplaintextsearch.cpp index 2d295be1..174ed39b 100644 --- a/src/search/kateplaintextsearch.cpp +++ b/src/search/kateplaintextsearch.cpp @@ -1,145 +1,145 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2009-2010 Bernhard Beschow * Copyright (C) 2007 Sebastian Pipping * * 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. */ //BEGIN includes #include "kateplaintextsearch.h" #include "kateregexpsearch.h" #include #include "katepartdebug.h" #include //END includes //BEGIN d'tor, c'tor // // KateSearch Constructor // KatePlainTextSearch::KatePlainTextSearch(const KTextEditor::Document *document, Qt::CaseSensitivity caseSensitivity, bool wholeWords) : m_document(document) , m_caseSensitivity(caseSensitivity) , m_wholeWords(wholeWords) { } // // KateSearch Destructor // KatePlainTextSearch::~KatePlainTextSearch() { } //END KTextEditor::Range KatePlainTextSearch::search(const QString &text, const KTextEditor::Range &inputRange, bool backwards) { // abuse regex for whole word plaintext search if (m_wholeWords) { // escape dot and friends const QString workPattern = QStringLiteral("\\b%1\\b").arg(QRegularExpression::escape(text)); return KateRegExpSearch(m_document, m_caseSensitivity).search(workPattern, inputRange, backwards).at(0); } if (text.isEmpty() || !inputRange.isValid() || (inputRange.start() == inputRange.end())) { return KTextEditor::Range::invalid(); } // split multi-line needle into single lines - const QStringList needleLines = text.split(QStringLiteral("\n")); + const QStringList needleLines = text.split(QLatin1Char('\n')); if (needleLines.count() > 1) { // multi-line plaintext search (both forwards or backwards) const int forMin = inputRange.start().line(); // first line in range const int forMax = inputRange.end().line() + 1 - needleLines.count(); // last line in range const int forInit = backwards ? forMax : forMin; const int forInc = backwards ? -1 : +1; for (int j = forInit; (forMin <= j) && (j <= forMax); j += forInc) { // try to match all lines const int startCol = m_document->lineLength(j) - needleLines[0].length(); for (int k = 0; k < needleLines.count(); k++) { // which lines to compare const QString &needleLine = needleLines[k]; const QString &hayLine = m_document->line(j + k); // position specific comparison (first, middle, last) if (k == 0) { // first line if (forMin == j && startCol < inputRange.start().column()) { break; } // NOTE: QString("")::endsWith("") is false in Qt, therefore we need the additional checks. const bool endsWith = hayLine.endsWith(needleLine, m_caseSensitivity) || (hayLine.isEmpty() && needleLine.isEmpty()); if (!endsWith) { break; } } else if (k == needleLines.count() - 1) { // last line const int maxRight = (j + k == inputRange.end().line()) ? inputRange.end().column() : hayLine.length(); // NOTE: QString("")::startsWith("") is false in Qt, therefore we need the additional checks. const bool startsWith = hayLine.startsWith(needleLine, m_caseSensitivity) || (hayLine.isEmpty() && needleLine.isEmpty()); if (startsWith && needleLine.length() <= maxRight) { return KTextEditor::Range(j, startCol, j + k, needleLine.length()); } } else { // mid lines if (hayLine.compare(needleLine, m_caseSensitivity) != 0) { break; } } } } // not found return KTextEditor::Range::invalid(); } else { // single-line plaintext search (both forward of backward mode) const int startCol = inputRange.start().column(); const int endCol = inputRange.end().column(); // first not included const int startLine = inputRange.start().line(); const int endLine = inputRange.end().line(); const int forInc = backwards ? -1 : +1; for (int line = backwards ? endLine : startLine; (startLine <= line) && (line <= endLine); line += forInc) { if ((line < 0) || (m_document->lines() <= line)) { qCWarning(LOG_KTE) << "line " << line << " is not within interval [0.." << m_document->lines() << ") ... returning invalid range"; return KTextEditor::Range::invalid(); } const QString textLine = m_document->line(line); const int offset = (line == startLine) ? startCol : 0; const int line_end = (line == endLine) ? endCol : textLine.length(); const int foundAt = backwards ? textLine.lastIndexOf(text, line_end - text.length(), m_caseSensitivity) : textLine.indexOf(text, offset, m_caseSensitivity); if ((offset <= foundAt) && (foundAt + text.length() <= line_end)) { return KTextEditor::Range(line, foundAt, line, foundAt + text.length()); } } } return KTextEditor::Range::invalid(); } diff --git a/src/search/kateregexpsearch.cpp b/src/search/kateregexpsearch.cpp index 6650366e..671b71a4 100644 --- a/src/search/kateregexpsearch.cpp +++ b/src/search/kateregexpsearch.cpp @@ -1,759 +1,758 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2010 Bernhard Beschow * Copyright (C) 2007 Sebastian Pipping * * 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. */ //BEGIN includes #include "kateregexpsearch.h" #include "kateregexp.h" #include //END includes // 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 class KateRegExpSearch::ReplacementStream { public: struct counter { counter(int value, int minWidth) : value(value) , minWidth(minWidth) {} const int value; const int minWidth; }; struct cap { cap(int n) : n(n) {} const int n; }; enum CaseConversion { upperCase, ///< \U ... uppercase from now on upperCaseFirst, ///< \u ... uppercase the first letter lowerCase, ///< \L ... lowercase from now on lowerCaseFirst, ///< \l ... lowercase the first letter keepCase ///< \E ... back to original case }; public: ReplacementStream(const QStringList &capturedTexts); QString str() const { return m_str; } ReplacementStream &operator<<(const QString &); ReplacementStream &operator<<(const counter &); ReplacementStream &operator<<(const cap &); ReplacementStream &operator<<(CaseConversion); private: const QStringList m_capturedTexts; CaseConversion m_caseConversion; QString m_str; }; KateRegExpSearch::ReplacementStream::ReplacementStream(const QStringList &capturedTexts) : m_capturedTexts(capturedTexts) , m_caseConversion(keepCase) { } KateRegExpSearch::ReplacementStream &KateRegExpSearch::ReplacementStream::operator<<(const QString &str) { switch (m_caseConversion) { case upperCase: // Copy as uppercase m_str.append(str.toUpper()); break; case upperCaseFirst: if (str.length() > 0) { m_str.append(str.at(0).toUpper()); m_str.append(str.midRef(1)); m_caseConversion = keepCase; } break; case lowerCase: // Copy as lowercase m_str.append(str.toLower()); break; case lowerCaseFirst: if (str.length() > 0) { m_str.append(str.at(0).toLower()); m_str.append(str.midRef(1)); m_caseConversion = keepCase; } break; case keepCase: // FALLTHROUGH default: // Copy unmodified m_str.append(str); break; } return *this; } KateRegExpSearch::ReplacementStream &KateRegExpSearch::ReplacementStream::operator<<(const counter &c) { // Zero padded counter value m_str.append(QStringLiteral("%1").arg(c.value, c.minWidth, 10, QLatin1Char('0'))); return *this; } KateRegExpSearch::ReplacementStream &KateRegExpSearch::ReplacementStream::operator<<(const cap &cap) { if (0 <= cap.n && cap.n < m_capturedTexts.size()) { (*this) << m_capturedTexts[cap.n]; } else { // Insert just the number to be consistent with QRegExp ("\c" becomes "c") m_str.append(QString::number(cap.n)); } return *this; } KateRegExpSearch::ReplacementStream &KateRegExpSearch::ReplacementStream::operator<<(CaseConversion caseConversion) { m_caseConversion = caseConversion; return *this; } //BEGIN d'tor, c'tor // // KateSearch Constructor // KateRegExpSearch::KateRegExpSearch(const KTextEditor::Document *document, Qt::CaseSensitivity caseSensitivity) : m_document(document) , m_caseSensitivity(caseSensitivity) { } // // KateSearch Destructor // KateRegExpSearch::~KateRegExpSearch() { } // helper structs for captures re-construction struct TwoViewCursor { int index; int openLine; int openCol; int closeLine; int closeCol; // note: open/close distinction does not seem needed // anymore. i keep it to make a potential way back // easier. overhead is minimal. }; struct IndexPair { int openIndex; int closeIndex; }; QVector KateRegExpSearch::search( const QString &pattern, const KTextEditor::Range &inputRange, bool backwards) { // regex search KateRegExp regexp(pattern, m_caseSensitivity); if (regexp.isEmpty() || !regexp.isValid() || !inputRange.isValid() || (inputRange.start() == inputRange.end())) { QVector result; result.append(KTextEditor::Range::invalid()); return result; } // detect pattern type (single- or mutli-line) bool isMultiLine; // detect '.' and '\s' and fix them const bool dotMatchesNewline = false; // TODO const int replacements = regexp.repairPattern(isMultiLine); if (dotMatchesNewline && (replacements > 0)) { isMultiLine = true; } const int firstLineIndex = inputRange.start().line(); const int minColStart = inputRange.start().column(); // const int maxColEnd = inputRange.end().column(); if (isMultiLine) { // multi-line regex search (both forward and backward mode) QString wholeDocument; const int inputLineCount = inputRange.end().line() - inputRange.start().line() + 1; FAST_DEBUG("multi line search (lines " << firstLineIndex << ".." << firstLineIndex + inputLineCount - 1 << ")"); // nothing to do... if (firstLineIndex >= m_document->lines()) { QVector result; result.append(KTextEditor::Range::invalid()); return result; } QVector lineLens(inputLineCount); // first line if (firstLineIndex < 0 || m_document->lines() <= firstLineIndex) { QVector result; result.append(KTextEditor::Range::invalid()); return result; } const QString firstLine = m_document->line(firstLineIndex); const int firstLineLen = firstLine.length() - minColStart; wholeDocument.append(firstLine.rightRef(firstLineLen)); lineLens[0] = firstLineLen; FAST_DEBUG(" line" << 0 << "has length" << lineLens[0]); // second line and after - const QString sep = QStringLiteral("\n"); for (int i = 1; i < inputLineCount; i++) { const int lineNum = firstLineIndex + i; if (lineNum < 0 || m_document->lines() <= lineNum) { QVector result; result.append(KTextEditor::Range::invalid()); return result; } const QString text = m_document->line(lineNum); lineLens[i] = text.length(); - wholeDocument.append(sep); + wholeDocument.append(QLatin1Char('\n')); wholeDocument.append(text); FAST_DEBUG(" line" << i << "has length" << lineLens[i]); } const int pos = backwards ? regexp.lastIndexIn(wholeDocument, 0, wholeDocument.length()) : regexp.indexIn(wholeDocument, 0, wholeDocument.length()); if (pos == -1) { // no match FAST_DEBUG("not found"); { QVector result; result.append(KTextEditor::Range::invalid()); return result; } } #ifdef FAST_DEBUG_ENABLE const int matchLen = regexp.matchedLength(); FAST_DEBUG("found at relative pos " << pos << ", length " << matchLen); #endif // save opening and closing indices and build a map. // the correct values will be written into it later. QMap indicesToCursors; const int numCaptures = regexp.numCaptures(); QVector indexPairs(1 + numCaptures); for (int z = 0; z <= numCaptures; z++) { const int openIndex = regexp.pos(z); IndexPair &pair = indexPairs[z]; if (openIndex == -1) { // empty capture gives invalid pair.openIndex = -1; pair.closeIndex = -1; FAST_DEBUG("capture []"); } else { const int closeIndex = openIndex + regexp.cap(z).length(); pair.openIndex = openIndex; pair.closeIndex = closeIndex; FAST_DEBUG("capture [" << pair.openIndex << ".." << pair.closeIndex << "]"); // each key no more than once if (!indicesToCursors.contains(openIndex)) { TwoViewCursor *twoViewCursor = new TwoViewCursor; twoViewCursor->index = openIndex; indicesToCursors.insert(openIndex, twoViewCursor); FAST_DEBUG(" border index added: " << openIndex); } if (!indicesToCursors.contains(closeIndex)) { TwoViewCursor *twoViewCursor = new TwoViewCursor; twoViewCursor->index = closeIndex; indicesToCursors.insert(closeIndex, twoViewCursor); FAST_DEBUG(" border index added: " << closeIndex); } } } // find out where they belong int curRelLine = 0; int curRelCol = 0; int curRelIndex = 0; QMap::const_iterator iter = indicesToCursors.constBegin(); while (iter != indicesToCursors.constEnd()) { // forward to index, save line/col const int index = (*iter)->index; FAST_DEBUG("resolving position" << index); TwoViewCursor &twoViewCursor = *(*iter); while (curRelIndex <= index) { FAST_DEBUG("walk pos (" << curRelLine << "," << curRelCol << ") = " << curRelIndex << "relative, steps more to go" << index - curRelIndex); const int curRelLineLen = lineLens[curRelLine]; const int curLineRemainder = curRelLineLen - curRelCol; const int lineFeedIndex = curRelIndex + curLineRemainder; if (index <= lineFeedIndex) { if (index == lineFeedIndex) { // on this line _on_ line feed FAST_DEBUG(" on line feed"); const int absLine = curRelLine + firstLineIndex; twoViewCursor.openLine = twoViewCursor.closeLine = absLine; twoViewCursor.openCol = twoViewCursor.closeCol = ((curRelLine == 0) ? minColStart : 0) + curRelLineLen; // advance to next line const int advance = (index - curRelIndex) + 1; curRelLine++; curRelCol = 0; curRelIndex += advance; } else { // index < lineFeedIndex // on this line _before_ line feed FAST_DEBUG(" before line feed"); const int diff = (index - curRelIndex); const int absLine = curRelLine + firstLineIndex; const int absCol = ((curRelLine == 0) ? minColStart : 0) + curRelCol + diff; twoViewCursor.openLine = twoViewCursor.closeLine = absLine; twoViewCursor.openCol = twoViewCursor.closeCol = absCol; // advance on same line const int advance = diff + 1; curRelCol += advance; curRelIndex += advance; } FAST_DEBUG("open(" << twoViewCursor.openLine << "," << twoViewCursor.openCol << ") close(" << twoViewCursor.closeLine << "," << twoViewCursor.closeCol << ")"); } else { // if (index > lineFeedIndex) // not on this line // advance to next line FAST_DEBUG(" not on this line"); const int advance = curLineRemainder + 1; curRelLine++; curRelCol = 0; curRelIndex += advance; } } ++iter; } // build result array QVector result(1 + numCaptures); for (int y = 0; y <= numCaptures; y++) { IndexPair &pair = indexPairs[y]; if ((pair.openIndex == -1) || (pair.closeIndex == -1)) { result[y] = KTextEditor::Range::invalid(); } else { const TwoViewCursor *const openCursors = indicesToCursors[pair.openIndex]; const TwoViewCursor *const closeCursors = indicesToCursors[pair.closeIndex]; const int startLine = openCursors->openLine; const int startCol = openCursors->openCol; const int endLine = closeCursors->closeLine; const int endCol = closeCursors->closeCol; FAST_DEBUG("range " << y << ": (" << startLine << ", " << startCol << ")..(" << endLine << ", " << endCol << ")"); result[y] = KTextEditor::Range(startLine, startCol, endLine, endCol); } } // free structs allocated for indicesToCursors iter = indicesToCursors.constBegin(); while (iter != indicesToCursors.constEnd()) { TwoViewCursor *const twoViewCursor = *iter; delete twoViewCursor; ++iter; } return result; } else { // single-line regex search (both forward of backward mode) const int minLeft = inputRange.start().column(); const uint maxRight = inputRange.end().column(); // first not included const int forMin = inputRange.start().line(); const int forMax = inputRange.end().line(); const int forInit = backwards ? forMax : forMin; const int forInc = backwards ? -1 : +1; FAST_DEBUG("single line " << (backwards ? forMax : forMin) << ".." << (backwards ? forMin : forMax)); for (int j = forInit; (forMin <= j) && (j <= forMax); j += forInc) { if (j < 0 || m_document->lines() <= j) { FAST_DEBUG("searchText | line " << j << ": no"); QVector result; result.append(KTextEditor::Range::invalid()); return result; } const QString textLine = m_document->line(j); // Find (and don't match ^ in between...) const int first = (j == forMin) ? minLeft : 0; const int last = (j == forMax) ? maxRight : textLine.length(); const int foundAt = (backwards ? regexp.lastIndexIn(textLine, first, last) : regexp.indexIn(textLine, first, last)); const bool found = (foundAt != -1); /* TODO do we still need this? // A special case which can only occur when searching with a regular expression consisting // only of a lookahead (e.g. ^(?=\{) for a function beginning without selecting '{'). if (myMatchLen == 0 && line == startPosition.line() && foundAt == (uint) col) { if (col < lineLength(line)) col++; else { line++; col = 0; } continue; } */ if (found) { FAST_DEBUG("line " << j << ": yes"); // build result array const int numCaptures = regexp.numCaptures(); QVector result(1 + numCaptures); result[0] = KTextEditor::Range(j, foundAt, j, foundAt + regexp.matchedLength()); FAST_DEBUG("result range " << 0 << ": (" << j << ", " << foundAt << ")..(" << j << ", " << foundAt + regexp.matchedLength() << ")"); for (int y = 1; y <= numCaptures; y++) { const int openIndex = regexp.pos(y); if (openIndex == -1) { result[y] = KTextEditor::Range::invalid(); FAST_DEBUG("capture []"); } else { const int closeIndex = openIndex + regexp.cap(y).length(); FAST_DEBUG("result range " << y << ": (" << j << ", " << openIndex << ")..(" << j << ", " << closeIndex << ")"); result[y] = KTextEditor::Range(j, openIndex, j, closeIndex); } } return result; } else { FAST_DEBUG("searchText | line " << j << ": no"); } } } QVector result; result.append(KTextEditor::Range::invalid()); return result; } /*static*/ QString KateRegExpSearch::escapePlaintext(const QString &text) { return buildReplacement(text, QStringList(), 0, false); } /*static*/ QString KateRegExpSearch::buildReplacement(const QString &text, const QStringList &capturedTexts, int replacementCounter) { return buildReplacement(text, capturedTexts, replacementCounter, true); } /*static*/ QString KateRegExpSearch::buildReplacement(const QString &text, const QStringList &capturedTexts, int replacementCounter, bool replacementGoodies) { // get input const int inputLen = text.length(); int input = 0; // walker index // prepare output ReplacementStream out(capturedTexts); while (input < inputLen) { switch (text[input].unicode()) { case L'\n': out << text[input]; input++; break; case L'\\': if (input + 1 >= inputLen) { // copy backslash out << text[input]; input++; break; } switch (text[input + 1].unicode()) { case L'0': // "\0000".."\0377" if (input + 4 >= inputLen) { out << ReplacementStream::cap(0); input += 2; } else { bool stripAndSkip = false; const ushort text_2 = text[input + 2].unicode(); if ((text_2 >= L'0') && (text_2 <= L'3')) { const ushort text_3 = text[input + 3].unicode(); if ((text_3 >= L'0') && (text_3 <= L'7')) { const ushort text_4 = text[input + 4].unicode(); if ((text_4 >= L'0') && (text_4 <= L'7')) { int digits[3]; for (int i = 0; i < 3; i++) { digits[i] = 7 - (L'7' - text[input + 2 + i].unicode()); } const int ch = 64 * digits[0] + 8 * digits[1] + digits[2]; out << QChar(ch); input += 5; } else { stripAndSkip = true; } } else { stripAndSkip = true; } } else { stripAndSkip = true; } if (stripAndSkip) { out << ReplacementStream::cap(0); input += 2; } } break; // single letter captures \x case L'1': case L'2': case L'3': case L'4': case L'5': case L'6': case L'7': case L'8': case L'9': out << ReplacementStream::cap(9 - (L'9' - text[input + 1].unicode())); input += 2; break; // multi letter captures \{xxxx} case L'{': { // allow {1212124}.... captures, see bug 365124 + testReplaceManyCapturesBug365124 int capture = 0; int captureSize = 2; while ((input + captureSize) < inputLen) { const ushort nextDigit = text[input + captureSize].unicode(); if ((nextDigit >= L'0') && (nextDigit <= L'9')) { capture = (10 * capture) + (9 - (L'9' - nextDigit)); ++captureSize; continue; } if (nextDigit == L'}') { ++captureSize; break; } break; } out << ReplacementStream::cap(capture); input += captureSize; break; } case L'E': // FALLTHROUGH case L'L': // FALLTHROUGH case L'l': // FALLTHROUGH case L'U': // FALLTHROUGH case L'u': if (!replacementGoodies) { // strip backslash ("\?" -> "?") out << text[input + 1]; } else { // handle case switcher switch (text[input + 1].unicode()) { case L'L': out << ReplacementStream::lowerCase; break; case L'l': out << ReplacementStream::lowerCaseFirst; break; case L'U': out << ReplacementStream::upperCase; break; case L'u': out << ReplacementStream::upperCaseFirst; break; case L'E': // FALLTHROUGH default: out << ReplacementStream::keepCase; } } input += 2; break; case L'#': if (!replacementGoodies) { // strip backslash ("\?" -> "?") out << text[input + 1]; input += 2; } else { // handle replacement counter // eat and count all following hash marks // each hash stands for a leading zero: \### will produces 001, 002, ... int minWidth = 1; while ((input + minWidth + 1 < inputLen) && (text[input + minWidth + 1].unicode() == L'#')) { minWidth++; } out << ReplacementStream::counter(replacementCounter, minWidth); input += 1 + minWidth; } break; case L'a': out << QChar(0x07); input += 2; break; case L'f': out << QChar(0x0c); input += 2; break; case L'n': out << QChar(0x0a); input += 2; break; case L'r': out << QChar(0x0d); input += 2; break; case L't': out << QChar(0x09); input += 2; break; case L'v': out << QChar(0x0b); input += 2; break; case L'x': // "\x0000".."\xffff" if (input + 5 >= inputLen) { // strip backslash ("\x" -> "x") out << text[input + 1]; input += 2; } else { bool stripAndSkip = false; const ushort text_2 = text[input + 2].unicode(); if (((text_2 >= L'0') && (text_2 <= L'9')) || ((text_2 >= L'a') && (text_2 <= L'f')) || ((text_2 >= L'A') && (text_2 <= L'F'))) { const ushort text_3 = text[input + 3].unicode(); if (((text_3 >= L'0') && (text_3 <= L'9')) || ((text_3 >= L'a') && (text_3 <= L'f')) || ((text_3 >= L'A') && (text_3 <= L'F'))) { const ushort text_4 = text[input + 4].unicode(); if (((text_4 >= L'0') && (text_4 <= L'9')) || ((text_4 >= L'a') && (text_4 <= L'f')) || ((text_4 >= L'A') && (text_4 <= L'F'))) { const ushort text_5 = text[input + 5].unicode(); if (((text_5 >= L'0') && (text_5 <= L'9')) || ((text_5 >= L'a') && (text_5 <= L'f')) || ((text_5 >= L'A') && (text_5 <= L'F'))) { int digits[4]; for (int i = 0; i < 4; i++) { const ushort cur = text[input + 2 + i].unicode(); if ((cur >= L'0') && (cur <= L'9')) { digits[i] = 9 - (L'9' - cur); } else if ((cur >= L'a') && (cur <= L'f')) { digits[i] = 15 - (L'f' - cur); } else { // if ((cur >= L'A') && (cur <= L'F'))) digits[i] = 15 - (L'F' - cur); } } const int ch = 4096 * digits[0] + 256 * digits[1] + 16 * digits[2] + digits[3]; out << QChar(ch); input += 6; } else { stripAndSkip = true; } } else { stripAndSkip = true; } } else { stripAndSkip = true; } } if (stripAndSkip) { // strip backslash ("\x" -> "x") out << text[input + 1]; input += 2; } } break; default: // strip backslash ("\?" -> "?") out << text[input + 1]; input += 2; } break; default: out << text[input]; input++; } } return out.str(); } // Kill our helpers again #ifdef FAST_DEBUG_ENABLE # undef FAST_DEBUG_ENABLE #endif #undef FAST_DEBUG diff --git a/src/search/katesearchbar.cpp b/src/search/katesearchbar.cpp index 5b404fd9..78625bd9 100644 --- a/src/search/katesearchbar.cpp +++ b/src/search/katesearchbar.cpp @@ -1,1741 +1,1741 @@ /* This file is part of the KDE libraries 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 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 "katesearchbar.h" #include "kateregexp.h" #include "katematch.h" #include "kateview.h" #include "katedocument.h" #include "kateundomanager.h" #include "kateconfig.h" #include "katerenderer.h" #include "kateglobal.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(); QTime 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()); if (iface) { iface->setMarkDescription(KTextEditor::MarkInterface::SearchMatch, i18n("SearchHighLight")); iface->setMarkPixmap(KTextEditor::MarkInterface::SearchMatch, QIcon().pixmap(0,0)); 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) - ? (QString::fromLatin1(" = (") + capturePatterns[i - 1].left(30)) + QLatin1String(")") + ? (QString::fromLatin1(" = (") + capturePatterns[i - 1].left(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/swapfile/kateswapfile.cpp b/src/swapfile/kateswapfile.cpp index bf1d368f..6d2d680a 100644 --- a/src/swapfile/kateswapfile.cpp +++ b/src/swapfile/kateswapfile.cpp @@ -1,668 +1,668 @@ /* This file is part of the Kate project. * * Copyright (C) 2010-2018 Dominik Haumann * Copyright (C) 2010 Diana-Victoria Tiriplica * * 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 "config.h" #include "kateswapfile.h" #include "kateconfig.h" #include "kateswapdiffcreator.h" #include "kateundomanager.h" #include "katepartdebug.h" #include #include #include #include #include #include #include #ifndef Q_OS_WIN #include #endif // swap file version header const static char swapFileVersionString[] = "Kate Swap File 2.0"; // tokens for swap files const static qint8 EA_StartEditing = 'S'; const static qint8 EA_FinishEditing = 'E'; const static qint8 EA_WrapLine = 'W'; const static qint8 EA_UnwrapLine = 'U'; const static qint8 EA_InsertText = 'I'; const static qint8 EA_RemoveText = 'R'; namespace Kate { QTimer *SwapFile::s_timer = nullptr; SwapFile::SwapFile(KTextEditor::DocumentPrivate *document) : QObject(document) , m_document(document) , m_trackingEnabled(false) , m_recovered(false) , m_needSync(false) { // fixed version of serialisation m_stream.setVersion(QDataStream::Qt_4_6); // connect the timer connect(syncTimer(), SIGNAL(timeout()), this, SLOT(writeFileToDisk()), Qt::DirectConnection); // connecting the signals connect(&m_document->buffer(), SIGNAL(saved(QString)), this, SLOT(fileSaved(QString))); connect(&m_document->buffer(), SIGNAL(loaded(QString,bool)), this, SLOT(fileLoaded(QString))); connect(m_document, SIGNAL(configChanged()), this, SLOT(configChanged())); // tracking on! setTrackingEnabled(true); } SwapFile::~SwapFile() { // only remove swap file after data recovery (bug #304576) if (!shouldRecover()) { removeSwapFile(); } } void SwapFile::configChanged() { } void SwapFile::setTrackingEnabled(bool enable) { if (m_trackingEnabled == enable) { return; } m_trackingEnabled = enable; TextBuffer &buffer = m_document->buffer(); if (m_trackingEnabled) { connect(&buffer, SIGNAL(editingStarted()), this, SLOT(startEditing())); connect(&buffer, SIGNAL(editingFinished()), this, SLOT(finishEditing())); connect(m_document, SIGNAL(modifiedChanged(KTextEditor::Document*)), this, SLOT(modifiedChanged())); connect(&buffer, SIGNAL(lineWrapped(KTextEditor::Cursor)), this, SLOT(wrapLine(KTextEditor::Cursor))); connect(&buffer, SIGNAL(lineUnwrapped(int)), this, SLOT(unwrapLine(int))); connect(&buffer, SIGNAL(textInserted(KTextEditor::Cursor,QString)), this, SLOT(insertText(KTextEditor::Cursor,QString))); connect(&buffer, SIGNAL(textRemoved(KTextEditor::Range,QString)), this, SLOT(removeText(KTextEditor::Range))); } else { disconnect(&buffer, SIGNAL(editingStarted()), this, SLOT(startEditing())); disconnect(&buffer, SIGNAL(editingFinished()), this, SLOT(finishEditing())); disconnect(m_document, SIGNAL(modifiedChanged(KTextEditor::Document*)), this, SLOT(modifiedChanged())); disconnect(&buffer, SIGNAL(lineWrapped(KTextEditor::Cursor)), this, SLOT(wrapLine(KTextEditor::Cursor))); disconnect(&buffer, SIGNAL(lineUnwrapped(int)), this, SLOT(unwrapLine(int))); disconnect(&buffer, SIGNAL(textInserted(KTextEditor::Cursor,QString)), this, SLOT(insertText(KTextEditor::Cursor,QString))); disconnect(&buffer, SIGNAL(textRemoved(KTextEditor::Range,QString)), this, SLOT(removeText(KTextEditor::Range))); } } void SwapFile::fileClosed() { // remove old swap file, file is now closed if (!shouldRecover()) { removeSwapFile(); } else { m_document->setReadWrite(true); } // purge filename updateFileName(); } KTextEditor::DocumentPrivate *SwapFile::document() { return m_document; } bool SwapFile::isValidSwapFile(QDataStream &stream, bool checkDigest) const { // read and check header QByteArray header; stream >> header; if (header != swapFileVersionString) { qCWarning(LOG_KTE) << "Can't open swap file, wrong version"; return false; } // read checksum QByteArray checksum; stream >> checksum; //qCDebug(LOG_KTE) << "DIGEST:" << checksum << m_document->checksum(); if (checkDigest && checksum != m_document->checksum()) { qCWarning(LOG_KTE) << "Can't recover from swap file, checksum of document has changed"; return false; } return true; } void SwapFile::fileLoaded(const QString &) { // look for swap file if (!updateFileName()) { return; } if (!m_swapfile.exists()) { //qCDebug(LOG_KTE) << "No swap file"; return; } if (!QFileInfo(m_swapfile).isReadable()) { qCWarning(LOG_KTE) << "Can't open swap file (missing permissions)"; return; } // sanity check QFile peekFile(fileName()); if (peekFile.open(QIODevice::ReadOnly)) { QDataStream stream(&peekFile); if (!isValidSwapFile(stream, true)) { removeSwapFile(); return; } peekFile.close(); } else { qCWarning(LOG_KTE) << "Can't open swap file:" << fileName(); return; } // show swap file message m_document->setReadWrite(false); showSwapFileMessage(); } void SwapFile::modifiedChanged() { if (!m_document->isModified() && !shouldRecover()) { m_needSync = false; // the file is not modified and we are not in recover mode removeSwapFile(); } } void SwapFile::recover() { m_document->setReadWrite(true); // if isOpen() returns true, the swap file likely changed already (appended data) // Example: The document was falsely marked as writable and the user changed // text even though the recover bar was visible. In this case, a replay of // the swap file across wrong document content would happen -> certainly wrong if (m_swapfile.isOpen()) { qCWarning(LOG_KTE) << "Attempt to recover an already modified document. Aborting"; removeSwapFile(); return; } // if the file doesn't exist, abort (user might have deleted it, or use two editor instances) if (!m_swapfile.open(QIODevice::ReadOnly)) { qCWarning(LOG_KTE) << "Can't open swap file"; return; } // remember that the file has recovered m_recovered = true; // open data stream m_stream.setDevice(&m_swapfile); // replay the swap file bool success = recover(m_stream); // close swap file m_stream.setDevice(nullptr); m_swapfile.close(); if (!success) { removeSwapFile(); } // recover can also be called through the KTE::RecoveryInterface. // Make sure, the message is hidden in this case as well. if (m_swapMessage) { m_swapMessage->deleteLater(); } } bool SwapFile::recover(QDataStream &stream, bool checkDigest) { if (!isValidSwapFile(stream, checkDigest)) { return false; } // disconnect current signals setTrackingEnabled(false); // needed to set undo/redo cursors in a sane way bool firstEditInGroup = false; KTextEditor::Cursor undoCursor = KTextEditor::Cursor::invalid(); KTextEditor::Cursor redoCursor = KTextEditor::Cursor::invalid(); // replay swapfile bool editRunning = false; bool brokenSwapFile = false; while (!stream.atEnd()) { if (brokenSwapFile) { break; } qint8 type; stream >> type; switch (type) { case EA_StartEditing: { m_document->editStart(); editRunning = true; firstEditInGroup = true; undoCursor = KTextEditor::Cursor::invalid(); redoCursor = KTextEditor::Cursor::invalid(); break; } case EA_FinishEditing: { m_document->editEnd(); // empty editStart() / editEnd() groups exist: only set cursor if required if (!firstEditInGroup) { // set undo/redo cursor of last KateUndoGroup of the undo manager m_document->undoManager()->setUndoRedoCursorsOfLastGroup(undoCursor, redoCursor); m_document->undoManager()->undoSafePoint(); } firstEditInGroup = false; editRunning = false; break; } case EA_WrapLine: { if (!editRunning) { brokenSwapFile = true; break; } int line = 0, column = 0; stream >> line >> column; // emulate buffer unwrapLine with document m_document->editWrapLine(line, column, true); // track undo/redo cursor if (firstEditInGroup) { firstEditInGroup = false; undoCursor = KTextEditor::Cursor(line, column); } redoCursor = KTextEditor::Cursor(line + 1, 0); break; } case EA_UnwrapLine: { if (!editRunning) { brokenSwapFile = true; break; } int line = 0; stream >> line; // assert valid line Q_ASSERT(line > 0); const int undoColumn = m_document->lineLength(line - 1); // emulate buffer unwrapLine with document m_document->editUnWrapLine(line - 1, true, 0); // track undo/redo cursor if (firstEditInGroup) { firstEditInGroup = false; undoCursor = KTextEditor::Cursor(line, 0); } redoCursor = KTextEditor::Cursor(line - 1, undoColumn); break; } case EA_InsertText: { if (!editRunning) { brokenSwapFile = true; break; } int line, column; QByteArray text; stream >> line >> column >> text; m_document->insertText(KTextEditor::Cursor(line, column), QString::fromUtf8(text.data(), text.size())); // track undo/redo cursor if (firstEditInGroup) { firstEditInGroup = false; undoCursor = KTextEditor::Cursor(line, column); } redoCursor = KTextEditor::Cursor(line, column + text.size()); break; } case EA_RemoveText: { if (!editRunning) { brokenSwapFile = true; break; } int line, startColumn, endColumn; stream >> line >> startColumn >> endColumn; m_document->removeText(KTextEditor::Range(KTextEditor::Cursor(line, startColumn), KTextEditor::Cursor(line, endColumn))); // track undo/redo cursor if (firstEditInGroup) { firstEditInGroup = false; undoCursor = KTextEditor::Cursor(line, endColumn); } redoCursor = KTextEditor::Cursor(line, startColumn); break; } default: { qCWarning(LOG_KTE) << "Unknown type:" << type; } } } // balanced editStart and editEnd? if (editRunning) { brokenSwapFile = true; m_document->editEnd(); } // warn the user if the swap file is not complete if (brokenSwapFile) { qCWarning(LOG_KTE) << "Some data might be lost"; } else { // set sane final cursor, if possible KTextEditor::View *view = m_document->activeView(); redoCursor = m_document->undoManager()->lastRedoCursor(); if (view && redoCursor.isValid()) { view->setCursorPosition(redoCursor); } } // reconnect the signals setTrackingEnabled(true); return true; } void SwapFile::fileSaved(const QString &) { m_needSync = false; // remove old swap file (e.g. if a file A was "saved as" B) removeSwapFile(); // set the name for the new swap file updateFileName(); } void SwapFile::startEditing() { // no swap file, no work if (m_swapfile.fileName().isEmpty()) { return; } // if swap file doesn't exists, open it in WriteOnly mode // if it does, append the data to the existing swap file, // in case you recover and start editing again if (!m_swapfile.exists()) { // create path if not there if (KateDocumentConfig::global()->swapFileMode() == KateDocumentConfig::SwapFilePresetDirectory && !QDir(KateDocumentConfig::global()->swapDirectory()).exists()) { QDir().mkpath(KateDocumentConfig::global()->swapDirectory()); } m_swapfile.open(QIODevice::WriteOnly); m_swapfile.setPermissions(QFileDevice::ReadOwner|QFileDevice::WriteOwner); m_stream.setDevice(&m_swapfile); // write file header m_stream << QByteArray(swapFileVersionString); // write checksum m_stream << m_document->checksum(); } else if (m_stream.device() == nullptr) { m_swapfile.open(QIODevice::Append); m_swapfile.setPermissions(QFileDevice::ReadOwner|QFileDevice::WriteOwner); m_stream.setDevice(&m_swapfile); } // format: qint8 m_stream << EA_StartEditing; } void SwapFile::finishEditing() { // skip if not open if (!m_swapfile.isOpen()) { return; } // write the file to the disk every 15 seconds (default) // skip this if we disabled that if (m_document->config()->swapSyncInterval() != 0 && !syncTimer()->isActive()) { // important: we store the interval as seconds, start wants milliseconds! syncTimer()->start(m_document->config()->swapSyncInterval() * 1000); } // format: qint8 m_stream << EA_FinishEditing; m_swapfile.flush(); } void SwapFile::wrapLine(const KTextEditor::Cursor &position) { // skip if not open if (!m_swapfile.isOpen()) { return; } // format: qint8, int, int m_stream << EA_WrapLine << position.line() << position.column(); m_needSync = true; } void SwapFile::unwrapLine(int line) { // skip if not open if (!m_swapfile.isOpen()) { return; } // format: qint8, int m_stream << EA_UnwrapLine << line; m_needSync = true; } void SwapFile::insertText(const KTextEditor::Cursor &position, const QString &text) { // skip if not open if (!m_swapfile.isOpen()) { return; } // format: qint8, int, int, bytearray m_stream << EA_InsertText << position.line() << position.column() << text.toUtf8(); m_needSync = true; } void SwapFile::removeText(const KTextEditor::Range &range) { // skip if not open if (!m_swapfile.isOpen()) { return; } // format: qint8, int, int, int Q_ASSERT(range.start().line() == range.end().line()); m_stream << EA_RemoveText << range.start().line() << range.start().column() << range.end().column(); m_needSync = true; } bool SwapFile::shouldRecover() const { // should not recover if the file has already recovered in another view if (m_recovered) { return false; } return !m_swapfile.fileName().isEmpty() && m_swapfile.exists() && m_stream.device() == nullptr; } void SwapFile::discard() { m_document->setReadWrite(true); removeSwapFile(); // discard can also be called through the KTE::RecoveryInterface. // Make sure, the message is hidden in this case as well. if (m_swapMessage) { m_swapMessage->deleteLater(); } } void SwapFile::removeSwapFile() { if (!m_swapfile.fileName().isEmpty() && m_swapfile.exists()) { m_stream.setDevice(nullptr); m_swapfile.close(); m_swapfile.remove(); } } bool SwapFile::updateFileName() { // first clear filename m_swapfile.setFileName(QString()); // get the new path QString path = fileName(); if (path.isNull()) { return false; } m_swapfile.setFileName(path); return true; } QString SwapFile::fileName() { const QUrl &url = m_document->url(); if (url.isEmpty() || !url.isLocalFile()) { return QString(); } const QString fullLocalPath(url.toLocalFile()); QString path; if (KateDocumentConfig::global()->swapFileMode() == KateDocumentConfig::SwapFilePresetDirectory) { path = KateDocumentConfig::global()->swapDirectory(); path.append(QLatin1Char('/')); // append the sha1 sum of the full path + filename, to avoid "too long" paths created path.append(QString::fromLatin1(QCryptographicHash::hash(fullLocalPath.toUtf8(), QCryptographicHash::Sha1).toHex())); - path.append(QLatin1String("-")); + path.append(QLatin1Char('-')); path.append(QFileInfo(fullLocalPath).fileName()); path.append(QLatin1String(".kate-swp")); } else { path = fullLocalPath; int poz = path.lastIndexOf(QLatin1Char('/')); - path.insert(poz + 1, QLatin1String(".")); + path.insert(poz + 1, QLatin1Char('.')); path.append(QLatin1String(".kate-swp")); } return path; } QTimer *SwapFile::syncTimer() { if (s_timer == nullptr) { s_timer = new QTimer(QApplication::instance()); s_timer->setSingleShot(true); } return s_timer; } void SwapFile::writeFileToDisk() { if (m_needSync) { m_needSync = false; #ifndef Q_OS_WIN // ensure that the file is written to disk #if HAVE_FDATASYNC fdatasync(m_swapfile.handle()); #else fsync(m_swapfile.handle()); #endif #endif } } void SwapFile::showSwapFileMessage() { m_swapMessage = new KTextEditor::Message(i18n("The file was not closed properly."), KTextEditor::Message::Warning); m_swapMessage->setWordWrap(true); QAction *diffAction = new QAction(QIcon::fromTheme(QStringLiteral("split")), i18n("View Changes"), nullptr); QAction *recoverAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-redo")), i18n("Recover Data"), nullptr); QAction *discardAction = new QAction(KStandardGuiItem::discard().icon(), i18n("Discard"), nullptr); m_swapMessage->addAction(diffAction, false); m_swapMessage->addAction(recoverAction); m_swapMessage->addAction(discardAction); connect(diffAction, SIGNAL(triggered()), SLOT(showDiff())); connect(recoverAction, SIGNAL(triggered()), SLOT(recover()), Qt::QueuedConnection); connect(discardAction, SIGNAL(triggered()), SLOT(discard()), Qt::QueuedConnection); m_document->postMessage(m_swapMessage); } void SwapFile::showDiff() { // the diff creator deletes itself through deleteLater() when it's done SwapDiffCreator *diffCreator = new SwapDiffCreator(this); diffCreator->viewDiff(); } } diff --git a/src/utils/katecommandrangeexpressionparser.cpp b/src/utils/katecommandrangeexpressionparser.cpp index 6106dc11..ef285894 100644 --- a/src/utils/katecommandrangeexpressionparser.cpp +++ b/src/utils/katecommandrangeexpressionparser.cpp @@ -1,173 +1,173 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2008 - 2009 Erlend Hamberg * Copyright (C) 2011 Svyatoslav Kuzmich * Copyright (C) 2012 Vegard Øye * Copyright (C) 2013 Simon St James * * 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 "katecommandrangeexpressionparser.h" #include "kateview.h" #include "katedocument.h" #include #include using KTextEditor::Range; using KTextEditor::Cursor; CommandRangeExpressionParser::CommandRangeExpressionParser() { m_line.setPattern(QStringLiteral("\\d+")); m_lastLine.setPattern(QStringLiteral("\\$")); m_thisLine.setPattern(QStringLiteral("\\.")); m_forwardSearch.setPattern(QStringLiteral("/([^/]*)/?")); m_forwardSearch2.setPattern(QStringLiteral("/[^/]*/?")); // no group m_backwardSearch.setPattern(QStringLiteral("\\?([^?]*)\\??")); m_backwardSearch2.setPattern(QStringLiteral("\\?[^?]*\\??")); // no group m_base.setPattern(QLatin1String("(?:") + m_mark.pattern() + QLatin1String(")|(?:") + m_line.pattern() + QLatin1String(")|(?:") + m_thisLine.pattern() + QLatin1String(")|(?:") + m_lastLine.pattern() + QLatin1String(")|(?:") + m_forwardSearch2.pattern() + QLatin1String(")|(?:") + - m_backwardSearch2.pattern() + QLatin1String(")")); + m_backwardSearch2.pattern() + QLatin1Char(')')); m_offset.setPattern(QLatin1String("[+-](?:") + m_base.pattern() + QLatin1String(")?")); // The position regexp contains two groups: the base and the offset. // The offset may be empty. - m_position.setPattern(QLatin1String("(") + m_base.pattern() + QLatin1String(")((?:") + m_offset.pattern() + QLatin1String(")*)")); + m_position.setPattern(QLatin1Char('(') + m_base.pattern() + QLatin1String(")((?:") + m_offset.pattern() + QLatin1String(")*)")); // The range regexp contains seven groups: the first is the start position, the second is // the base of the start position, the third is the offset of the start position, the // fourth is the end position including a leading comma, the fifth is end position // without the comma, the sixth is the base of the end position, and the seventh is the // offset of the end position. The third and fourth groups may be empty, and the // fifth, sixth and seventh groups are contingent on the fourth group. m_cmdRange.setPattern(QLatin1String("^(") + m_position.pattern() + QLatin1String(")((?:,(") + m_position.pattern() + QLatin1String("))?)")); } Range CommandRangeExpressionParser::parseRangeExpression(const QString &command, KTextEditor::ViewPrivate *view, QString &destRangeExpression, QString &destTransformedCommand) { CommandRangeExpressionParser rangeExpressionParser; return rangeExpressionParser.parseRangeExpression(command, destRangeExpression, destTransformedCommand, view); } Range CommandRangeExpressionParser::parseRangeExpression(const QString &command, QString &destRangeExpression, QString &destTransformedCommand, KTextEditor::ViewPrivate *view) { Range parsedRange(0, -1, 0, -1); if (command.isEmpty()) { return parsedRange; } QString commandTmp = command; bool leadingRangeWasPercent = false; // expand '%' to '1,$' ("all lines") if at the start of the line if (commandTmp.at(0) == QLatin1Char('%')) { commandTmp.replace(0, 1, QStringLiteral("1,$")); leadingRangeWasPercent = true; } if (m_cmdRange.indexIn(commandTmp) != -1 && m_cmdRange.matchedLength() > 0) { commandTmp.remove(m_cmdRange); QString position_string1 = m_cmdRange.capturedTexts().at(1); QString position_string2 = m_cmdRange.capturedTexts().at(4); int position1 = calculatePosition(position_string1, view); int position2; if (!position_string2.isEmpty()) { // remove the comma position_string2 = m_cmdRange.capturedTexts().at(5); position2 = calculatePosition(position_string2, view); } else { position2 = position1; } // special case: if the command is just a number with an optional +/- prefix, rewrite to "goto" if (commandTmp.isEmpty()) { commandTmp = QStringLiteral("goto %1").arg(position1); } else { parsedRange.setRange(KTextEditor::Range(position1 - 1, 0, position2 - 1, 0)); } destRangeExpression = (leadingRangeWasPercent ? QStringLiteral("%") : m_cmdRange.cap(0)); destTransformedCommand = commandTmp; } return parsedRange; } int CommandRangeExpressionParser::calculatePosition(const QString &string, KTextEditor::ViewPrivate *view) { int pos = 0; QList operators_list; const QStringList split = string.split(QRegularExpression(QStringLiteral("[-+](?!([+-]|$))"))); QList values; for (const QString &line : split) { pos += line.size(); if (pos < string.size()) { if (string.at(pos) == QLatin1Char('+')) { operators_list.push_back(true); } else if (string.at(pos) == QLatin1Char('-')) { operators_list.push_back(false); } else { Q_ASSERT(false); } } ++pos; if (m_line.exactMatch(line)) { values.push_back(line.toInt()); } else if (m_lastLine.exactMatch(line)) { values.push_back(view->doc()->lines()); } else if (m_thisLine.exactMatch(line)) { values.push_back(view->cursorPosition().line() + 1); } else if (m_forwardSearch.exactMatch(line)) { m_forwardSearch.indexIn(line); QString pattern = m_forwardSearch.capturedTexts().at(1); int match = view->doc()->searchText(Range(view->cursorPosition(), view->doc()->documentEnd()), pattern, KTextEditor::Regex).first().start().line(); values.push_back((match < 0) ? -1 : match + 1); } else if (m_backwardSearch.exactMatch(line)) { m_backwardSearch.indexIn(line); QString pattern = m_backwardSearch.capturedTexts().at(1); int match = view->doc()->searchText(Range(Cursor(0, 0), view->cursorPosition()), pattern, KTextEditor::Regex).first().start().line(); values.push_back((match < 0) ? -1 : match + 1); } } if (values.isEmpty()) { return -1; } int result = values.at(0); for (int i = 0; i < operators_list.size(); ++i) { if (operators_list.at(i) == true) { result += values.at(i + 1); } else { result -= values.at(i + 1); } } return result; } diff --git a/src/variableeditor/variablelistview.cpp b/src/variableeditor/variablelistview.cpp index 42532c38..7d8f6b88 100644 --- a/src/variableeditor/variablelistview.cpp +++ b/src/variableeditor/variablelistview.cpp @@ -1,142 +1,142 @@ /* This file is part of the KDE project Copyright (C) 2011-2018 Dominik Haumann 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 "variableeditor.h" #include "variablelistview.h" #include "variableitem.h" #include #include VariableListView::VariableListView(const QString &variableLine, QWidget *parent) : QScrollArea(parent) { setBackgroundRole(QPalette::Base); setWidget(new QWidget(this)); parseVariables(variableLine); } VariableListView::~VariableListView() { } void VariableListView::parseVariables(const QString &line) { QString tmp = line.trimmed(); if (tmp.startsWith(QLatin1String("kate:"))) { tmp.remove(0, 5); } QStringList variables = tmp.split(QLatin1Char(';'), QString::SkipEmptyParts); const QRegularExpression sep(QStringLiteral("\\s+")); for (int i = 0; i < variables.size(); ++i) { QStringList pair = variables[i].split(sep, QString::SkipEmptyParts); if (pair.size() < 2) { continue; } if (pair.size() > 2) { // e.g. fonts have spaces in the value. Hence, join all value items again QString key = pair[0]; pair.removeAt(0); QString value = pair.join(QLatin1Char(' ')); pair.clear(); pair << key << value; } m_variables[pair[0]] = pair[1]; } } void VariableListView::addItem(VariableItem *item) { // overwrite default value when variable exists in modeline if (m_variables.contains(item->variable())) { item->setValueByString(m_variables[item->variable()]); item->setActive(true); } VariableEditor *editor = item->createEditor(widget()); editor->setBackgroundRole((m_editors.size() % 2) ? QPalette::AlternateBase : QPalette::Base); m_editors << editor; m_items << item; connect(editor, SIGNAL(valueChanged()), this, SIGNAL(changed())); } void VariableListView::resizeEvent(QResizeEvent *event) { QScrollArea::resizeEvent(event); // calculate sum of all editor heights int listHeight = 0; foreach (QWidget *w, m_editors) { listHeight += w->sizeHint().height(); } // resize scroll area widget QWidget *top = widget(); top->resize(event->size().width(), listHeight); // set client geometries correctly int h = 0; foreach (QWidget *w, m_editors) { w->setGeometry(0, h, top->width(), w->sizeHint().height()); h += w->sizeHint().height(); } } void VariableListView::hideEvent(QHideEvent *event) { if (!event->spontaneous()) { emit aboutToHide(); } QScrollArea::hideEvent(event); } QString VariableListView::variableLine() { for (int i = 0; i < m_items.size(); ++i) { VariableItem *item = m_items[i]; if (item->isActive()) { m_variables[item->variable()] = item->valueAsString(); } else if (m_variables.contains(item->variable())) { m_variables.remove(item->variable()); } } QString line; QMap::const_iterator it = m_variables.constBegin(); while (it != m_variables.constEnd()) { if (!line.isEmpty()) { - line += QLatin1String(" "); + line += QLatin1Char(' '); } line += it.key() + QLatin1Char(' ') + it.value() + QLatin1Char(';'); ++it; } line.prepend(QLatin1String("kate: ")); return line; } diff --git a/src/view/kateviewhelpers.cpp b/src/view/kateviewhelpers.cpp index 666b8556..d1527ded 100644 --- a/src/view/kateviewhelpers.cpp +++ b/src/view/kateviewhelpers.cpp @@ -1,3107 +1,3107 @@ /* 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 "katecmd.h" #include #include #include #include "kateconfig.h" #include "katedocument.h" #include #include "katerenderer.h" #include "kateannotationitemdelegate.h" #include "kateview.h" #include "kateviewinternal.h" #include "katelayoutcache.h" #include "katetextlayout.h" #include "kateglobal.h" #include "katepartdebug.h" #include "katecommandrangeexpressionparser.h" #include "kateabstractinputmode.h" #include "katetextpreview.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 //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.8); 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 foreach (const QTextLayout::FormatRange &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(QLatin1String("\\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) { 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; // the following commands changes the focus themselves, so bar should be hidden before execution. if (QRegularExpression(QLatin1String("^(buffer|b|new|vnew|bp|bprev|bn|bnext|bf|bfirst|bl|blast|edit|e)$")).match(cmd.split(QLatin1Char(' ')).at(0)).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; // the following commands change the focus themselves if (!QRegularExpression(QLatin1String("^(buffer|b|new|vnew|bp|bprev|bn|bnext|bf|bfirst|bl|blast|edit|e)$")).match(cmd.split(QLatin1Char(' ')).at(0)).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(QLatin1String("^[\\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)); 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()->config()->fontMetrics(); 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.width(QChar(i))); 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()->config()->font()); // 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()); 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); // 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); p.drawPixmap(lnX + x_px, y + y_px, px_mark); } } } } 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); 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()->config()->fontMetrics(); } 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 http://www.iana.org/assignments/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; foreach (const QStringList &encodingsForScript, KCharsets::charsets()->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); foreach (KSelectAction *action, 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; Q_FOREACH (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(QLatin1String("\n"), QLatin1String(" ")), this, SLOT(paste())); + 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]); } diff --git a/src/vimode/appcommands.cpp b/src/vimode/appcommands.cpp index 45ac55e2..0252fa04 100644 --- a/src/vimode/appcommands.cpp +++ b/src/vimode/appcommands.cpp @@ -1,525 +1,525 @@ /* This file is part of the KDE libraries Copyright (C) 2009 Erlend Hamberg Copyright (C) 2011 Svyatoslav Kuzmich 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 #include #include #include #include #include #include #include #include #include using namespace KateVi; //BEGIN AppCommands AppCommands *AppCommands::m_instance = nullptr; AppCommands::AppCommands() : KTextEditor::Command({ QStringLiteral("q"), QStringLiteral("qa"), QStringLiteral("qall"), QStringLiteral("q!"), QStringLiteral("qa!"), QStringLiteral("qall!") , QStringLiteral("w"), QStringLiteral("wq"), QStringLiteral("wa"), QStringLiteral("wqa"), QStringLiteral("x"), QStringLiteral("xa"), QStringLiteral("new") , QStringLiteral("vnew"), QStringLiteral("e"), QStringLiteral("edit"), QStringLiteral("enew"), QStringLiteral("sp"), QStringLiteral("split"), QStringLiteral("vs") , QStringLiteral("vsplit"), QStringLiteral("only"), QStringLiteral("tabe"), QStringLiteral("tabedit"), QStringLiteral("tabnew"), QStringLiteral("bd") , QStringLiteral("bdelete"), QStringLiteral("tabc"), QStringLiteral("tabclose"), QStringLiteral("clo"), QStringLiteral("close") }) , re_write(QStringLiteral("^w(a)?$")) , re_close(QStringLiteral("^bd(elete)?|tabc(lose)?$")) , re_quit(QStringLiteral("^(w)?q(a|all)?(!)?$")) , re_exit(QStringLiteral("^x(a)?$")) , re_edit(QStringLiteral("^e(dit)?|tabe(dit)?|tabnew$")) , re_tabedit(QStringLiteral("^tabe(dit)?|tabnew$")) , re_new(QStringLiteral("^(v)?new$")) , re_split(QStringLiteral("^sp(lit)?$")) , re_vsplit(QStringLiteral("^vs(plit)?$")) , re_vclose(QStringLiteral("^clo(se)?$")) , re_only(QStringLiteral("^on(ly)?$")) { } AppCommands::~AppCommands() { m_instance = nullptr; } bool AppCommands::exec(KTextEditor::View *view, const QString &cmd, QString &msg, const KTextEditor::Range &) { QStringList args(cmd.split(QRegularExpression(QLatin1String("\\s+")), QString::SkipEmptyParts)) ; QString command(args.takeFirst()); KTextEditor::MainWindow *mainWin = view->mainWindow(); KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); QRegularExpressionMatch match; if ((match = re_write.match(command)).hasMatch()) { //TODO: handle writing to specific file if (!match.captured(1).isEmpty()) { // [a]ll Q_FOREACH(KTextEditor::Document *doc, app->documents()) { doc->save(); } msg = i18n("All documents written to disk"); } else { view->document()->documentSave(); msg = i18n("Document written to disk"); } } // Other buffer commands are implemented by the KateFileTree plugin else if ((match = re_close.match(command)).hasMatch()) { QTimer::singleShot(0, [app, view](){ app->closeDocument(view->document()); }); } else if ((match = re_quit.match(command)).hasMatch()) { const bool save = !match.captured(1).isEmpty(); // :[w]q const bool allDocuments = !match.captured(2).isEmpty(); // :q[all] const bool doNotPromptForSave = !match.captured(3).isEmpty(); // :q[!] if (allDocuments) { if (save) { Q_FOREACH(KTextEditor::Document *doc, app->documents()) { doc->save(); } } if (doNotPromptForSave) { Q_FOREACH(KTextEditor::Document *doc, app->documents()) { if (doc->isModified()) { doc->setModified(false); } } } QTimer::singleShot(0, this, SLOT(quit())); } else { if (save && view->document()->isModified()) { view->document()->documentSave(); } if (doNotPromptForSave) { view->document()->setModified(false); } if (mainWin->views().size() > 1) { QTimer::singleShot(0, this, SLOT(closeCurrentView())); } else { if (app->documents().size() > 1) { QTimer::singleShot(0, this, SLOT(closeCurrentDocument())); } else { QTimer::singleShot(0, this, SLOT(quit())); } } } } else if ((match = re_exit.match(command)).hasMatch()) { if (!match.captured(1).isEmpty()) { // a[ll] Q_FOREACH(KTextEditor::Document *doc, app->documents()) { doc->save(); } QTimer::singleShot(0, this, SLOT(quit())); } else { if (view->document()->isModified()) { view->document()->documentSave(); } if (app->documents().size() > 1) { QTimer::singleShot(0, this, SLOT(closeCurrentDocument())); } else { QTimer::singleShot(0, this, SLOT(quit())); } } } else if ((match = re_edit.match(command)).hasMatch()) { QString argument = args.join(QLatin1Char(' ')); if (argument.isEmpty() || argument == QLatin1String("!")) { if ((match = re_tabedit.match(command)).hasMatch()) { if (auto doc = app->openUrl(QUrl())) { QTimer::singleShot(0, [mainWin, doc](){ mainWin->activateView(doc); }); } } else { view->document()->documentReload(); } } else { QUrl base = view->document()->url(); QUrl url; QUrl arg2path(argument); if (base.isValid()) { // first try to use the same path as the current open document has url = QUrl(base.resolved(arg2path)); //resolved handles the case where the args is a relative path, and is the same as using QUrl(args) elsewise } else { // else use the cwd - url = QUrl(QUrl(QDir::currentPath() + QLatin1String("/")).resolved(arg2path)); // + "/" is needed because of http://lists.qt.nokia.com/public/qt-interest/2011-May/033913.html + url = QUrl(QUrl(QDir::currentPath() + QLatin1Char('/')).resolved(arg2path)); // + "/" is needed because of http://lists.qt.nokia.com/public/qt-interest/2011-May/033913.html } QFileInfo file(url.toLocalFile()); KTextEditor::Document *doc = app->findUrl(url); if (!doc) { if (file.exists()) { doc = app->openUrl(url); } else { if ((doc = app->openUrl(QUrl()))) { doc->saveAs(url); } } } if (doc) { QTimer::singleShot(0, [mainWin, doc](){ mainWin->activateView(doc); }); } } // splitView() orientations are reversed from the usual editor convention. // 'vsplit' and 'vnew' use Qt::Horizontal to match vi and the Kate UI actions. } else if ((match = re_new.match(command)).hasMatch()) { if (match.captured(1) == QLatin1String("v")) { // vertical split mainWin->splitView(Qt::Horizontal); } else { // horizontal split mainWin->splitView(Qt::Vertical); } mainWin->openUrl(QUrl()); } else if (command == QLatin1String("enew")) { mainWin->openUrl(QUrl()); } else if ((match = re_split.match(command)).hasMatch()) { mainWin->splitView(Qt::Vertical); // see above } else if ((match = re_vsplit.match(command)).hasMatch()) { mainWin->splitView(Qt::Horizontal); } else if ((match = re_vclose.match(command)).hasMatch()) { QTimer::singleShot(0, this, SLOT(closeCurrentSplitView())); } else if ((match = re_only.match(command)).hasMatch()) { QTimer::singleShot(0, this, SLOT(closeOtherSplitViews())); } return true; } bool AppCommands::help(KTextEditor::View *view, const QString &cmd, QString &msg) { Q_UNUSED(view); if (re_write.match(cmd).hasMatch()) { msg = i18n("

w/wa — write document(s) to disk

" "

Usage: w[a]

" "

Writes the current document(s) to disk. " "It can be called in two ways:
" " w — writes the current document to disk
" " wa — writes all documents to disk.

" "

If no file name is associated with the document, " "a file dialog will be shown.

"); return true; } else if (re_quit.match(cmd).hasMatch()) { msg = i18n("

q/qa/wq/wqa — [write and] quit

" "

Usage: [w]q[a]

" "

Quits the application. If w is prepended, it also writes" " the document(s) to disk. This command " "can be called in several ways:
" " q — closes the current view.
" " qa — closes all views, effectively quitting the application.
" " wq — writes the current document to disk and closes its view.
" " wqa — writes all documents to disk and quits.

" "

In all cases, if the view being closed is the last view, the application quits. " "If no file name is associated with the document and it should be written to disk, " "a file dialog will be shown.

"); return true; } else if (re_exit.match(cmd).hasMatch()) { msg = i18n("

x/xa — write and quit

" "

Usage: x[a]

" "

Saves document(s) and quits (exits). This command " "can be called in two ways:
" " x — closes the current view.
" " xa — closes all views, effectively quitting the application.

" "

In all cases, if the view being closed is the last view, the application quits. " "If no file name is associated with the document and it should be written to disk, " "a file dialog will be shown.

" "

Unlike the 'w' commands, this command only writes the document if it is modified." "

"); return true; } else if (re_split.match(cmd).hasMatch()) { msg = i18n("

sp,split— Split horizontally the current view into two

" "

Usage: sp[lit]

" "

The result is two views on the same document.

"); return true; } else if (re_vsplit.match(cmd).hasMatch()) { msg = i18n("

vs,vsplit— Split vertically the current view into two

" "

Usage: vs[plit]

" "

The result is two views on the same document.

"); return true; } else if (re_vclose.match(cmd).hasMatch()) { msg = i18n("

clo[se]— Close the current view

" "

Usage: clo[se]

" "

After executing it, the current view will be closed.

"); return true; } else if (re_new.match(cmd).hasMatch()) { msg = i18n("

[v]new — split view and create new document

" "

Usage: [v]new

" "

Splits the current view and opens a new document in the new view." " This command can be called in two ways:
" " new — splits the view horizontally and opens a new document.
" " vnew — splits the view vertically and opens a new document.
" "

"); return true; } else if (re_edit.match(cmd).hasMatch()) { msg = i18n("

e[dit] — reload current document

" "

Usage: e[dit]

" "

Starts editing the current document again. This is useful to re-edit" " the current file, when it has been changed by another program.

"); return true; } return false; } KTextEditor::View * AppCommands::findViewInDifferentSplitView(KTextEditor::MainWindow *window, KTextEditor::View *view) { Q_FOREACH (KTextEditor::View *it, window->views()) { if (!window->viewsInSameSplitView(it, view)) { return it; } } return nullptr; } void AppCommands::closeCurrentDocument() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); KTextEditor::Document *doc = app->activeMainWindow()->activeView()->document(); QTimer::singleShot(0, [app, doc](){ app->closeDocument(doc); }); } void AppCommands::closeCurrentView() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); KTextEditor::MainWindow *mw = app->activeMainWindow(); mw->closeView(mw->activeView()); } void AppCommands::closeCurrentSplitView() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); KTextEditor::MainWindow *mw = app->activeMainWindow(); mw->closeSplitView(mw->activeView()); } void AppCommands::closeOtherSplitViews() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); KTextEditor::MainWindow *mw = app->activeMainWindow(); KTextEditor::View *view = mw->activeView(); KTextEditor::View *viewToRemove = nullptr; while ((viewToRemove = findViewInDifferentSplitView(mw, view))) { mw->closeSplitView(viewToRemove); } } void AppCommands::quit() { KTextEditor::Editor::instance()->application()->quit(); } //END AppCommands //BEGIN KateViBufferCommand BufferCommands *BufferCommands::m_instance = nullptr; BufferCommands::BufferCommands() : KTextEditor::Command({ QStringLiteral("ls") , QStringLiteral("b"), QStringLiteral("buffer") , QStringLiteral("bn"), QStringLiteral("bnext"), QStringLiteral("bp"), QStringLiteral("bprevious") , QStringLiteral("tabn"), QStringLiteral("tabnext"), QStringLiteral("tabp"), QStringLiteral("tabprevious") , QStringLiteral("bf"), QStringLiteral("bfirst"), QStringLiteral("bl"), QStringLiteral("blast") , QStringLiteral("tabf"), QStringLiteral("tabfirst"), QStringLiteral("tabl"), QStringLiteral("tablast")}) { } BufferCommands::~BufferCommands() { m_instance = nullptr; } bool BufferCommands::exec(KTextEditor::View *view, const QString &cmd, QString &, const KTextEditor::Range &) { // create list of args QStringList args(cmd.split(QLatin1Char(' '), QString::KeepEmptyParts)); QString command = args.takeFirst(); // same as cmd if split failed QString argument = args.join(QLatin1Char(' ')); if (command == QLatin1String("ls")) { // TODO: open quickview } else if (command == QLatin1String("b") || command == QLatin1String("buffer")) { switchDocument(view, argument); } else if (command == QLatin1String("bp") || command == QLatin1String("bprevious")) { prevBuffer(view); } else if (command == QLatin1String("bn") || command == QLatin1String("bnext")) { nextBuffer(view); } else if (command == QLatin1String("bf") || command == QLatin1String("bfirst")) { firstBuffer(view); } else if (command == QLatin1String("bl") || command == QLatin1String("blast")) { lastBuffer(view); } else if (command == QLatin1String("tabn") || command == QLatin1String("tabnext")) { nextTab(view); } else if (command == QLatin1String("tabp") || command == QLatin1String("tabprevious")) { prevTab(view); } else if (command == QLatin1String("tabf") || command == QLatin1String("tabfirst")) { firstTab(view); } else if (command == QLatin1String("tabl") || command == QLatin1String("tablast")) { lastTab(view); } return true; } void BufferCommands::switchDocument(KTextEditor::View *view, const QString &address) { if (address.isEmpty()) { // no argument: switch to the previous document prevBuffer(view); return; } const int idx = address.toInt(); QList docs = documents(); if (idx > 0 && idx <= docs.size()) { // numerical argument: switch to the nth document activateDocument(view, docs.at(idx - 1)); } else { // string argument: switch to the given file KTextEditor::Document *doc = nullptr; Q_FOREACH(KTextEditor::Document *it, docs) { if (it->documentName() == address) { doc = it; break; } } if (doc) { activateDocument(view, doc); } } } void BufferCommands::prevBuffer(KTextEditor::View *view) { QList docs = documents(); const int idx = docs.indexOf(view->document()); if (idx > 0) { activateDocument(view, docs.at(idx - 1)); } else if (!docs.isEmpty()) { // wrap activateDocument(view, docs.last()); } } void BufferCommands::nextBuffer(KTextEditor::View *view) { QList docs = documents(); const int idx = docs.indexOf(view->document()); if (idx + 1 < docs.size()) { activateDocument(view, docs.at(idx + 1)); } else if (!docs.isEmpty()) { // wrap activateDocument(view, docs.first()); } } void BufferCommands::firstBuffer(KTextEditor::View *view) { auto docs = documents(); if (!docs.isEmpty()) { activateDocument(view, documents().at(0)); } } void BufferCommands::lastBuffer(KTextEditor::View *view) { auto docs = documents(); if (!docs.isEmpty()) { activateDocument(view, documents().last()); } } void BufferCommands::prevTab(KTextEditor::View *view) { prevBuffer(view); // TODO: implement properly, when interface is added } void BufferCommands::nextTab(KTextEditor::View *view) { nextBuffer(view); // TODO: implement properly, when interface is added } void BufferCommands::firstTab(KTextEditor::View *view) { firstBuffer(view); // TODO: implement properly, when interface is added } void BufferCommands::lastTab(KTextEditor::View *view) { lastBuffer(view); // TODO: implement properly, when interface is added } void BufferCommands::activateDocument(KTextEditor::View *view, KTextEditor::Document *doc) { KTextEditor::MainWindow *mainWindow = view->mainWindow(); QTimer::singleShot(0, [mainWindow, doc]() { mainWindow->activateView(doc); }); } QList< KTextEditor::Document * > BufferCommands::documents() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); return app->documents(); } bool BufferCommands::help(KTextEditor::View * /*view*/, const QString &cmd, QString &msg) { if (cmd == QLatin1String("b") || cmd == QLatin1String("buffer")) { msg = i18n("

b,buffer — Edit document N from the document list

" "

Usage: b[uffer] [N]

"); return true; } else if (cmd == QLatin1String("bp") || cmd == QLatin1String("bprevious") || cmd == QLatin1String("tabp") || cmd == QLatin1String("tabprevious")) { msg = i18n("

bp,bprev — previous buffer

" "

Usage: bp[revious] [N]

" "

Goes to [N]th previous document (\"buffer\") in document list.

" "

[N] defaults to one.

" "

Wraps around the start of the document list.

"); return true; } else if (cmd == QLatin1String("bn") || cmd == QLatin1String("bnext") || cmd == QLatin1String("tabn") || cmd == QLatin1String("tabnext")) { msg = i18n("

bn,bnext — switch to next document

" "

Usage: bn[ext] [N]

" "

Goes to [N]th next document (\"buffer\") in document list." "[N] defaults to one.

" "

Wraps around the end of the document list.

"); return true; } else if (cmd == QLatin1String("bf") || cmd == QLatin1String("bfirst") || cmd == QLatin1String("tabf") || cmd == QLatin1String("tabfirst")) { msg = i18n("

bf,bfirst — first document

" "

Usage: bf[irst]

" "

Goes to the first document (\"buffer\") in document list.

"); return true; } else if (cmd == QLatin1String("bl") || cmd == QLatin1String("blast") || cmd == QLatin1String("tabl") || cmd == QLatin1String("tablast")) { msg = i18n("

bl,blast — last document

" "

Usage: bl[ast]

" "

Goes to the last document (\"buffer\") in document list.

"); return true; } else if (cmd == QLatin1String("ls")) { msg = i18n("

ls

" "

list current buffers

"); } return false; } //END KateViBufferCommand diff --git a/src/vimode/inputmodemanager.cpp b/src/vimode/inputmodemanager.cpp index 11e2a643..a5967b16 100644 --- a/src/vimode/inputmodemanager.cpp +++ b/src/vimode/inputmodemanager.cpp @@ -1,482 +1,482 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2008 - 2009 Erlend Hamberg * Copyright (C) 2009 Paul Gideon Dann * Copyright (C) 2011 Svyatoslav Kuzmich * Copyright (C) 2012 - 2013 Simon St James * * 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 #include #include #include #include #include #include #include "kateconfig.h" #include "kateglobal.h" #include "globalstate.h" #include "kateviewinternal.h" #include #include #include #include #include #include #include #include "kateviinputmode.h" #include "marks.h" #include "jumps.h" #include "macros.h" #include "registers.h" #include "searcher.h" #include "completionrecorder.h" #include "completionreplayer.h" #include "macrorecorder.h" #include "lastchangerecorder.h" using namespace KateVi; InputModeManager::InputModeManager(KateViInputMode *inputAdapter, KTextEditor::ViewPrivate *view, KateViewInternal *viewInternal) : m_inputAdapter(inputAdapter) { m_currentViMode = ViMode::NormalMode; m_previousViMode = ViMode::NormalMode; m_viNormalMode = new NormalViMode(this, view, viewInternal); m_viInsertMode = new InsertViMode(this, view, viewInternal); m_viVisualMode = new VisualViMode(this, view, viewInternal); m_viReplaceMode = new ReplaceViMode(this, view, viewInternal); m_view = view; m_viewInternal = viewInternal; m_insideHandlingKeyPressCount = 0; m_keyMapperStack.push(QSharedPointer(new KeyMapper(this, m_view->doc(), m_view))); m_temporaryNormalMode = false; m_jumps = new Jumps(); m_marks = new Marks(this); m_searcher = new Searcher(this); m_completionRecorder = new CompletionRecorder(this); m_completionReplayer = new CompletionReplayer(this); m_macroRecorder = new MacroRecorder(this); m_lastChangeRecorder = new LastChangeRecorder(this); // We have to do this outside of NormalMode, as we don't want // VisualMode (which inherits from NormalMode) to respond // to changes in the document as well. m_viNormalMode->beginMonitoringDocumentChanges(); } InputModeManager::~InputModeManager() { delete m_viNormalMode; delete m_viInsertMode; delete m_viVisualMode; delete m_viReplaceMode; delete m_jumps; delete m_marks; delete m_searcher; delete m_macroRecorder; delete m_completionRecorder; delete m_completionReplayer; delete m_lastChangeRecorder; } bool InputModeManager::handleKeypress(const QKeyEvent *e) { m_insideHandlingKeyPressCount++; bool res = false; bool keyIsPartOfMapping = false; const bool isSyntheticSearchCompletedKeyPress = m_inputAdapter->viModeEmulatedCommandBar()->isSendingSyntheticSearchCompletedKeypress(); // With macros, we want to record the keypresses *before* they are mapped, but if they end up *not* being part // of a mapping, we don't want to record them when they are played back by m_keyMapper, hence // the "!isPlayingBackRejectedKeys()". And obviously, since we're recording keys before they are mapped, we don't // want to also record the executed mapping, as when we replayed the macro, we'd get duplication! if (m_macroRecorder->isRecording() && !m_macroRecorder->isReplaying() && !isSyntheticSearchCompletedKeyPress && !keyMapper()->isExecutingMapping() && !keyMapper()->isPlayingBackRejectedKeys() && !lastChangeRecorder()->isReplaying()) { m_macroRecorder->record(*e); } if (!m_lastChangeRecorder->isReplaying() && !isSyntheticSearchCompletedKeyPress) { if (e->key() == Qt::Key_AltGr) { return true; // do nothing } // Hand off to the key mapper, and decide if this key is part of a mapping. if (e->key() != Qt::Key_Control && e->key() != Qt::Key_Shift && e->key() != Qt::Key_Alt && e->key() != Qt::Key_Meta) { const QChar key = KeyParser::self()->KeyEventToQChar(*e); if (keyMapper()->handleKeypress(key)) { keyIsPartOfMapping = true; res = true; } } } if (!keyIsPartOfMapping) { if (!m_lastChangeRecorder->isReplaying() && !isSyntheticSearchCompletedKeyPress) { // record key press so that it can be repeated via "." m_lastChangeRecorder->record(*e); } if (m_inputAdapter->viModeEmulatedCommandBar()->isActive()) { res = m_inputAdapter->viModeEmulatedCommandBar()->handleKeyPress(e); } else { res = getCurrentViModeHandler()->handleKeypress(e); } } m_insideHandlingKeyPressCount--; Q_ASSERT(m_insideHandlingKeyPressCount >= 0); return res; } void InputModeManager::feedKeyPresses(const QString &keyPresses) const { int key; Qt::KeyboardModifiers mods; QString text; foreach (QChar c, keyPresses) { QString decoded = KeyParser::self()->decodeKeySequence(QString(c)); key = -1; mods = Qt::NoModifier; text.clear(); if (decoded.length() > 1) { // special key // remove the angle brackets decoded.remove(0, 1); - decoded.remove(decoded.indexOf(QLatin1String(">")), 1); + decoded.remove(decoded.indexOf(QLatin1Char('>')), 1); // check if one or more modifier keys where used if (decoded.indexOf(QLatin1String("s-")) != -1 || decoded.indexOf(QLatin1String("c-")) != -1 || decoded.indexOf(QLatin1String("m-")) != -1 || decoded.indexOf(QLatin1String("a-")) != -1) { int s = decoded.indexOf(QLatin1String("s-")); if (s != -1) { mods |= Qt::ShiftModifier; decoded.remove(s, 2); } int c = decoded.indexOf(QLatin1String("c-")); if (c != -1) { mods |= Qt::ControlModifier; decoded.remove(c, 2); } int a = decoded.indexOf(QLatin1String("a-")); if (a != -1) { mods |= Qt::AltModifier; decoded.remove(a, 2); } int m = decoded.indexOf(QLatin1String("m-")); if (m != -1) { mods |= Qt::MetaModifier; decoded.remove(m, 2); } if (decoded.length() > 1) { key = KeyParser::self()->vi2qt(decoded); } else if (decoded.length() == 1) { key = int(decoded.at(0).toUpper().toLatin1()); text = decoded.at(0); } } else { // no modifiers key = KeyParser::self()->vi2qt(decoded); } } else { key = decoded.at(0).unicode(); text = decoded.at(0); } if (key == -1) continue; // We have to be clever about which widget we dispatch to, as we can trigger // shortcuts if we're not careful (even if Vim mode is configured to steal shortcuts). QKeyEvent k(QEvent::KeyPress, key, mods, text); QWidget *destWidget = nullptr; if (QApplication::activePopupWidget()) { // According to the docs, the activePopupWidget, if present, takes all events. destWidget = QApplication::activePopupWidget(); } else if (QApplication::focusWidget()) { if (QApplication::focusWidget()->focusProxy()) { destWidget = QApplication::focusWidget()->focusProxy(); } else { destWidget = QApplication::focusWidget(); } } else { destWidget = m_view->focusProxy(); } QApplication::sendEvent(destWidget, &k); } } bool InputModeManager::isHandlingKeypress() const { return m_insideHandlingKeyPressCount > 0; } void InputModeManager::storeLastChangeCommand() { m_lastChange = m_lastChangeRecorder->encodedChanges(); m_lastChangeCompletionsLog = m_completionRecorder->currentChangeCompletionsLog(); } void InputModeManager::repeatLastChange() { m_lastChangeRecorder->replay(m_lastChange, m_lastChangeCompletionsLog); } void InputModeManager::clearCurrentChangeLog() { m_lastChangeRecorder->clear(); m_completionRecorder->clearCurrentChangeCompletionsLog(); } void InputModeManager::doNotLogCurrentKeypress() { m_macroRecorder->dropLast(); m_lastChangeRecorder->dropLast(); } void InputModeManager::changeViMode(ViMode newMode) { m_previousViMode = m_currentViMode; m_currentViMode = newMode; } ViMode InputModeManager::getCurrentViMode() const { return m_currentViMode; } KTextEditor::View::ViewMode InputModeManager::getCurrentViewMode() const { switch (m_currentViMode) { case ViMode::InsertMode: return KTextEditor::View::ViModeInsert; case ViMode::VisualMode: return KTextEditor::View::ViModeVisual; case ViMode::VisualLineMode: return KTextEditor::View::ViModeVisualLine; case ViMode::VisualBlockMode: return KTextEditor::View::ViModeVisualBlock; case ViMode::ReplaceMode: return KTextEditor::View::ViModeReplace; case ViMode::NormalMode: default: return KTextEditor::View::ViModeNormal; } } ViMode InputModeManager::getPreviousViMode() const { return m_previousViMode; } bool InputModeManager::isAnyVisualMode() const { return ((m_currentViMode == ViMode::VisualMode) || (m_currentViMode == ViMode::VisualLineMode) || (m_currentViMode == ViMode::VisualBlockMode)); } ::ModeBase *InputModeManager::getCurrentViModeHandler() const { switch (m_currentViMode) { case ViMode::NormalMode: return m_viNormalMode; case ViMode::InsertMode: return m_viInsertMode; case ViMode::VisualMode: case ViMode::VisualLineMode: case ViMode::VisualBlockMode: return m_viVisualMode; case ViMode::ReplaceMode: return m_viReplaceMode; } return nullptr; } void InputModeManager::viEnterNormalMode() { bool moveCursorLeft = (m_currentViMode == ViMode::InsertMode || m_currentViMode == ViMode::ReplaceMode) && m_viewInternal->cursorPosition().column() > 0; if (!m_lastChangeRecorder->isReplaying() && (m_currentViMode == ViMode::InsertMode || m_currentViMode == ViMode::ReplaceMode)) { // '^ is the insert mark and "^ is the insert register, // which holds the last inserted text KTextEditor::Range r(m_view->cursorPosition(), m_marks->getInsertStopped()); if (r.isValid()) { QString insertedText = m_view->doc()->text(r); m_inputAdapter->globalState()->registers()->setInsertStopped(insertedText); } m_marks->setInsertStopped(KTextEditor::Cursor(m_view->cursorPosition())); } changeViMode(ViMode::NormalMode); if (moveCursorLeft) { m_viewInternal->cursorPrevChar(); } m_inputAdapter->setCaretStyle(KateRenderer::Block); m_viewInternal->update(); } void InputModeManager::viEnterInsertMode() { changeViMode(ViMode::InsertMode); m_marks->setInsertStopped(KTextEditor::Cursor(m_view->cursorPosition())); if (getTemporaryNormalMode()) { // Ensure the key log contains a request to re-enter Insert mode, else the keystrokes made // after returning from temporary normal mode will be treated as commands! m_lastChangeRecorder->record(QKeyEvent(QEvent::KeyPress, Qt::Key_I, Qt::NoModifier, QStringLiteral("i"))); } m_inputAdapter->setCaretStyle(KateRenderer::Line); setTemporaryNormalMode(false); m_viewInternal->update(); } void InputModeManager::viEnterVisualMode(ViMode mode) { changeViMode(mode); // If the selection is inclusive, the caret should be a block. // If the selection is exclusive, the caret should be a line. m_inputAdapter->setCaretStyle(KateRenderer::Block); m_viewInternal->update(); getViVisualMode()->setVisualModeType(mode); getViVisualMode()->init(); } void InputModeManager::viEnterReplaceMode() { changeViMode(ViMode::ReplaceMode); m_marks->setStartEditYanked(KTextEditor::Cursor(m_view->cursorPosition())); m_inputAdapter->setCaretStyle(KateRenderer::Underline); m_viewInternal->update(); } NormalViMode *InputModeManager::getViNormalMode() { return m_viNormalMode; } InsertViMode *InputModeManager::getViInsertMode() { return m_viInsertMode; } VisualViMode *InputModeManager::getViVisualMode() { return m_viVisualMode; } ReplaceViMode *InputModeManager::getViReplaceMode() { return m_viReplaceMode; } const QString InputModeManager::getVerbatimKeys() const { QString cmd; switch (getCurrentViMode()) { case ViMode::NormalMode: cmd = m_viNormalMode->getVerbatimKeys(); break; case ViMode::InsertMode: case ViMode::ReplaceMode: // ... break; case ViMode::VisualMode: case ViMode::VisualLineMode: case ViMode::VisualBlockMode: cmd = m_viVisualMode->getVerbatimKeys(); break; } return cmd; } void InputModeManager::readSessionConfig(const KConfigGroup &config) { m_jumps->readSessionConfig(config); m_marks->readSessionConfig(config); } void InputModeManager::writeSessionConfig(KConfigGroup &config) { m_jumps->writeSessionConfig(config); m_marks->writeSessionConfig(config); } void InputModeManager::reset() { if (m_viVisualMode) { m_viVisualMode->reset(); } } KeyMapper *InputModeManager::keyMapper() { return m_keyMapperStack.top().data(); } void InputModeManager::updateCursor(const KTextEditor::Cursor &c) { m_inputAdapter->updateCursor(c); } GlobalState *InputModeManager::globalState() const { return m_inputAdapter->globalState(); } KTextEditor::ViewPrivate *InputModeManager::view() const { return m_view; } void InputModeManager::pushKeyMapper(QSharedPointer mapper) { m_keyMapperStack.push(mapper); } void InputModeManager::popKeyMapper() { m_keyMapperStack.pop(); } diff --git a/src/vimode/keyparser.cpp b/src/vimode/keyparser.cpp index 936fd75d..f7501c62 100644 --- a/src/vimode/keyparser.cpp +++ b/src/vimode/keyparser.cpp @@ -1,694 +1,694 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2008 Erlend Hamberg * Copyright (C) 2008 Evgeniy Ivanov * * 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 #include #include using namespace KateVi; KeyParser *KeyParser::m_instance = nullptr; KeyParser::KeyParser() { initKeyTables(); } KeyParser *KeyParser::self() { if (m_instance == nullptr) { m_instance = new KeyParser(); } return m_instance; } void KeyParser::initKeyTables() { m_qt2katevi.insert(Qt::Key_Escape, QStringLiteral("esc")); m_qt2katevi.insert(Qt::Key_Tab, QStringLiteral("tab")); m_qt2katevi.insert(Qt::Key_Backtab, QStringLiteral("backtab")); m_qt2katevi.insert(Qt::Key_Backspace, QStringLiteral("backspace")); m_qt2katevi.insert(Qt::Key_Return, QStringLiteral("return")); m_qt2katevi.insert(Qt::Key_Enter, QStringLiteral("enter")); m_qt2katevi.insert(Qt::Key_Insert, QStringLiteral("insert")); m_qt2katevi.insert(Qt::Key_Delete, QStringLiteral("delete")); m_qt2katevi.insert(Qt::Key_Pause, QStringLiteral("pause")); m_qt2katevi.insert(Qt::Key_Print, QStringLiteral("print")); m_qt2katevi.insert(Qt::Key_SysReq, QStringLiteral("sysreq")); m_qt2katevi.insert(Qt::Key_Clear, QStringLiteral("clear")); m_qt2katevi.insert(Qt::Key_Home, QStringLiteral("home")); m_qt2katevi.insert(Qt::Key_End, QStringLiteral("end")); m_qt2katevi.insert(Qt::Key_Left, QStringLiteral("left")); m_qt2katevi.insert(Qt::Key_Up, QStringLiteral("up")); m_qt2katevi.insert(Qt::Key_Right, QStringLiteral("right")); m_qt2katevi.insert(Qt::Key_Down, QStringLiteral("down")); m_qt2katevi.insert(Qt::Key_PageUp, QStringLiteral("pageup")); m_qt2katevi.insert(Qt::Key_PageDown, QStringLiteral("pagedown")); m_qt2katevi.insert(Qt::Key_Shift, QStringLiteral("shift")); m_qt2katevi.insert(Qt::Key_Control, QStringLiteral("control")); m_qt2katevi.insert(Qt::Key_Meta, QStringLiteral("meta")); m_qt2katevi.insert(Qt::Key_Alt, QStringLiteral("alt")); m_qt2katevi.insert(Qt::Key_AltGr, QStringLiteral("altgr")); m_qt2katevi.insert(Qt::Key_CapsLock, QStringLiteral("capslock")); m_qt2katevi.insert(Qt::Key_NumLock, QStringLiteral("numlock")); m_qt2katevi.insert(Qt::Key_ScrollLock, QStringLiteral("scrolllock")); m_qt2katevi.insert(Qt::Key_F1, QStringLiteral("f1")); m_qt2katevi.insert(Qt::Key_F2, QStringLiteral("f2")); m_qt2katevi.insert(Qt::Key_F3, QStringLiteral("f3")); m_qt2katevi.insert(Qt::Key_F4, QStringLiteral("f4")); m_qt2katevi.insert(Qt::Key_F5, QStringLiteral("f5")); m_qt2katevi.insert(Qt::Key_F6, QStringLiteral("f6")); m_qt2katevi.insert(Qt::Key_F7, QStringLiteral("f7")); m_qt2katevi.insert(Qt::Key_F8, QStringLiteral("f8")); m_qt2katevi.insert(Qt::Key_F9, QStringLiteral("f9")); m_qt2katevi.insert(Qt::Key_F10, QStringLiteral("f10")); m_qt2katevi.insert(Qt::Key_F11, QStringLiteral("f11")); m_qt2katevi.insert(Qt::Key_F12, QStringLiteral("f12")); m_qt2katevi.insert(Qt::Key_F13, QStringLiteral("f13")); m_qt2katevi.insert(Qt::Key_F14, QStringLiteral("f14")); m_qt2katevi.insert(Qt::Key_F15, QStringLiteral("f15")); m_qt2katevi.insert(Qt::Key_F16, QStringLiteral("f16")); m_qt2katevi.insert(Qt::Key_F17, QStringLiteral("f17")); m_qt2katevi.insert(Qt::Key_F18, QStringLiteral("f18")); m_qt2katevi.insert(Qt::Key_F19, QStringLiteral("f19")); m_qt2katevi.insert(Qt::Key_F20, QStringLiteral("f20")); m_qt2katevi.insert(Qt::Key_F21, QStringLiteral("f21")); m_qt2katevi.insert(Qt::Key_F22, QStringLiteral("f22")); m_qt2katevi.insert(Qt::Key_F23, QStringLiteral("f23")); m_qt2katevi.insert(Qt::Key_F24, QStringLiteral("f24")); m_qt2katevi.insert(Qt::Key_F25, QStringLiteral("f25")); m_qt2katevi.insert(Qt::Key_F26, QStringLiteral("f26")); m_qt2katevi.insert(Qt::Key_F27, QStringLiteral("f27")); m_qt2katevi.insert(Qt::Key_F28, QStringLiteral("f28")); m_qt2katevi.insert(Qt::Key_F29, QStringLiteral("f29")); m_qt2katevi.insert(Qt::Key_F30, QStringLiteral("f30")); m_qt2katevi.insert(Qt::Key_F31, QStringLiteral("f31")); m_qt2katevi.insert(Qt::Key_F32, QStringLiteral("f32")); m_qt2katevi.insert(Qt::Key_F33, QStringLiteral("f33")); m_qt2katevi.insert(Qt::Key_F34, QStringLiteral("f34")); m_qt2katevi.insert(Qt::Key_F35, QStringLiteral("f35")); m_qt2katevi.insert(Qt::Key_Super_L, QStringLiteral("super_l")); m_qt2katevi.insert(Qt::Key_Super_R, QStringLiteral("super_r")); m_qt2katevi.insert(Qt::Key_Menu, QStringLiteral("menu")); m_qt2katevi.insert(Qt::Key_Hyper_L, QStringLiteral("hyper_l")); m_qt2katevi.insert(Qt::Key_Hyper_R, QStringLiteral("hyper_r")); m_qt2katevi.insert(Qt::Key_Help, QStringLiteral("help")); m_qt2katevi.insert(Qt::Key_Direction_L, QStringLiteral("direction_l")); m_qt2katevi.insert(Qt::Key_Direction_R, QStringLiteral("direction_r")); m_qt2katevi.insert(Qt::Key_Multi_key, QStringLiteral("multi_key")); m_qt2katevi.insert(Qt::Key_Codeinput, QStringLiteral("codeinput")); m_qt2katevi.insert(Qt::Key_SingleCandidate, QStringLiteral("singlecandidate")); m_qt2katevi.insert(Qt::Key_MultipleCandidate, QStringLiteral("multiplecandidate")); m_qt2katevi.insert(Qt::Key_PreviousCandidate, QStringLiteral("previouscandidate")); m_qt2katevi.insert(Qt::Key_Mode_switch, QStringLiteral("mode_switch")); m_qt2katevi.insert(Qt::Key_Kanji, QStringLiteral("kanji")); m_qt2katevi.insert(Qt::Key_Muhenkan, QStringLiteral("muhenkan")); m_qt2katevi.insert(Qt::Key_Henkan, QStringLiteral("henkan")); m_qt2katevi.insert(Qt::Key_Romaji, QStringLiteral("romaji")); m_qt2katevi.insert(Qt::Key_Hiragana, QStringLiteral("hiragana")); m_qt2katevi.insert(Qt::Key_Katakana, QStringLiteral("katakana")); m_qt2katevi.insert(Qt::Key_Hiragana_Katakana, QStringLiteral("hiragana_katakana")); m_qt2katevi.insert(Qt::Key_Zenkaku, QStringLiteral("zenkaku")); m_qt2katevi.insert(Qt::Key_Hankaku, QStringLiteral("hankaku")); m_qt2katevi.insert(Qt::Key_Zenkaku_Hankaku, QStringLiteral("zenkaku_hankaku")); m_qt2katevi.insert(Qt::Key_Touroku, QStringLiteral("touroku")); m_qt2katevi.insert(Qt::Key_Massyo, QStringLiteral("massyo")); m_qt2katevi.insert(Qt::Key_Kana_Lock, QStringLiteral("kana_lock")); m_qt2katevi.insert(Qt::Key_Kana_Shift, QStringLiteral("kana_shift")); m_qt2katevi.insert(Qt::Key_Eisu_Shift, QStringLiteral("eisu_shift")); m_qt2katevi.insert(Qt::Key_Eisu_toggle, QStringLiteral("eisu_toggle")); m_qt2katevi.insert(Qt::Key_Hangul, QStringLiteral("hangul")); m_qt2katevi.insert(Qt::Key_Hangul_Start, QStringLiteral("hangul_start")); m_qt2katevi.insert(Qt::Key_Hangul_End, QStringLiteral("hangul_end")); m_qt2katevi.insert(Qt::Key_Hangul_Hanja, QStringLiteral("hangul_hanja")); m_qt2katevi.insert(Qt::Key_Hangul_Jamo, QStringLiteral("hangul_jamo")); m_qt2katevi.insert(Qt::Key_Hangul_Romaja, QStringLiteral("hangul_romaja")); m_qt2katevi.insert(Qt::Key_Hangul_Jeonja, QStringLiteral("hangul_jeonja")); m_qt2katevi.insert(Qt::Key_Hangul_Banja, QStringLiteral("hangul_banja")); m_qt2katevi.insert(Qt::Key_Hangul_PreHanja, QStringLiteral("hangul_prehanja")); m_qt2katevi.insert(Qt::Key_Hangul_PostHanja, QStringLiteral("hangul_posthanja")); m_qt2katevi.insert(Qt::Key_Hangul_Special, QStringLiteral("hangul_special")); m_qt2katevi.insert(Qt::Key_Dead_Grave, QStringLiteral("dead_grave")); m_qt2katevi.insert(Qt::Key_Dead_Acute, QStringLiteral("dead_acute")); m_qt2katevi.insert(Qt::Key_Dead_Circumflex, QStringLiteral("dead_circumflex")); m_qt2katevi.insert(Qt::Key_Dead_Tilde, QStringLiteral("dead_tilde")); m_qt2katevi.insert(Qt::Key_Dead_Macron, QStringLiteral("dead_macron")); m_qt2katevi.insert(Qt::Key_Dead_Breve, QStringLiteral("dead_breve")); m_qt2katevi.insert(Qt::Key_Dead_Abovedot, QStringLiteral("dead_abovedot")); m_qt2katevi.insert(Qt::Key_Dead_Diaeresis, QStringLiteral("dead_diaeresis")); m_qt2katevi.insert(Qt::Key_Dead_Abovering, QStringLiteral("dead_abovering")); m_qt2katevi.insert(Qt::Key_Dead_Doubleacute, QStringLiteral("dead_doubleacute")); m_qt2katevi.insert(Qt::Key_Dead_Caron, QStringLiteral("dead_caron")); m_qt2katevi.insert(Qt::Key_Dead_Cedilla, QStringLiteral("dead_cedilla")); m_qt2katevi.insert(Qt::Key_Dead_Ogonek, QStringLiteral("dead_ogonek")); m_qt2katevi.insert(Qt::Key_Dead_Iota, QStringLiteral("dead_iota")); m_qt2katevi.insert(Qt::Key_Dead_Voiced_Sound, QStringLiteral("dead_voiced_sound")); m_qt2katevi.insert(Qt::Key_Dead_Semivoiced_Sound, QStringLiteral("dead_semivoiced_sound")); m_qt2katevi.insert(Qt::Key_Dead_Belowdot, QStringLiteral("dead_belowdot")); m_qt2katevi.insert(Qt::Key_Dead_Hook, QStringLiteral("dead_hook")); m_qt2katevi.insert(Qt::Key_Dead_Horn, QStringLiteral("dead_horn")); m_qt2katevi.insert(Qt::Key_Back, QStringLiteral("back")); m_qt2katevi.insert(Qt::Key_Forward, QStringLiteral("forward")); m_qt2katevi.insert(Qt::Key_Stop, QStringLiteral("stop")); m_qt2katevi.insert(Qt::Key_Refresh, QStringLiteral("refresh")); m_qt2katevi.insert(Qt::Key_VolumeDown, QStringLiteral("volumedown")); m_qt2katevi.insert(Qt::Key_VolumeMute, QStringLiteral("volumemute")); m_qt2katevi.insert(Qt::Key_VolumeUp, QStringLiteral("volumeup")); m_qt2katevi.insert(Qt::Key_BassBoost, QStringLiteral("bassboost")); m_qt2katevi.insert(Qt::Key_BassUp, QStringLiteral("bassup")); m_qt2katevi.insert(Qt::Key_BassDown, QStringLiteral("bassdown")); m_qt2katevi.insert(Qt::Key_TrebleUp, QStringLiteral("trebleup")); m_qt2katevi.insert(Qt::Key_TrebleDown, QStringLiteral("trebledown")); m_qt2katevi.insert(Qt::Key_MediaPlay, QStringLiteral("mediaplay")); m_qt2katevi.insert(Qt::Key_MediaStop, QStringLiteral("mediastop")); m_qt2katevi.insert(Qt::Key_MediaPrevious, QStringLiteral("mediaprevious")); m_qt2katevi.insert(Qt::Key_MediaNext, QStringLiteral("medianext")); m_qt2katevi.insert(Qt::Key_MediaRecord, QStringLiteral("mediarecord")); m_qt2katevi.insert(Qt::Key_HomePage, QStringLiteral("homepage")); m_qt2katevi.insert(Qt::Key_Favorites, QStringLiteral("favorites")); m_qt2katevi.insert(Qt::Key_Search, QStringLiteral("search")); m_qt2katevi.insert(Qt::Key_Standby, QStringLiteral("standby")); m_qt2katevi.insert(Qt::Key_OpenUrl, QStringLiteral("openurl")); m_qt2katevi.insert(Qt::Key_LaunchMail, QStringLiteral("launchmail")); m_qt2katevi.insert(Qt::Key_LaunchMedia, QStringLiteral("launchmedia")); m_qt2katevi.insert(Qt::Key_Launch0, QStringLiteral("launch0")); m_qt2katevi.insert(Qt::Key_Launch1, QStringLiteral("launch1")); m_qt2katevi.insert(Qt::Key_Launch2, QStringLiteral("launch2")); m_qt2katevi.insert(Qt::Key_Launch3, QStringLiteral("launch3")); m_qt2katevi.insert(Qt::Key_Launch4, QStringLiteral("launch4")); m_qt2katevi.insert(Qt::Key_Launch5, QStringLiteral("launch5")); m_qt2katevi.insert(Qt::Key_Launch6, QStringLiteral("launch6")); m_qt2katevi.insert(Qt::Key_Launch7, QStringLiteral("launch7")); m_qt2katevi.insert(Qt::Key_Launch8, QStringLiteral("launch8")); m_qt2katevi.insert(Qt::Key_Launch9, QStringLiteral("launch9")); m_qt2katevi.insert(Qt::Key_LaunchA, QStringLiteral("launcha")); m_qt2katevi.insert(Qt::Key_LaunchB, QStringLiteral("launchb")); m_qt2katevi.insert(Qt::Key_LaunchC, QStringLiteral("launchc")); m_qt2katevi.insert(Qt::Key_LaunchD, QStringLiteral("launchd")); m_qt2katevi.insert(Qt::Key_LaunchE, QStringLiteral("launche")); m_qt2katevi.insert(Qt::Key_LaunchF, QStringLiteral("launchf")); m_qt2katevi.insert(Qt::Key_MediaLast, QStringLiteral("medialast")); m_qt2katevi.insert(Qt::Key_unknown, QStringLiteral("unknown")); m_qt2katevi.insert(Qt::Key_Call, QStringLiteral("call")); m_qt2katevi.insert(Qt::Key_Context1, QStringLiteral("context1")); m_qt2katevi.insert(Qt::Key_Context2, QStringLiteral("context2")); m_qt2katevi.insert(Qt::Key_Context3, QStringLiteral("context3")); m_qt2katevi.insert(Qt::Key_Context4, QStringLiteral("context4")); m_qt2katevi.insert(Qt::Key_Flip, QStringLiteral("flip")); m_qt2katevi.insert(Qt::Key_Hangup, QStringLiteral("hangup")); m_qt2katevi.insert(Qt::Key_No, QStringLiteral("no")); m_qt2katevi.insert(Qt::Key_Select, QStringLiteral("select")); m_qt2katevi.insert(Qt::Key_Yes, QStringLiteral("yes")); m_qt2katevi.insert(Qt::Key_Execute, QStringLiteral("execute")); m_qt2katevi.insert(Qt::Key_Printer, QStringLiteral("printer")); m_qt2katevi.insert(Qt::Key_Play, QStringLiteral("play")); m_qt2katevi.insert(Qt::Key_Sleep, QStringLiteral("sleep")); m_qt2katevi.insert(Qt::Key_Zoom, QStringLiteral("zoom")); m_qt2katevi.insert(Qt::Key_Cancel, QStringLiteral("cancel")); for (QHash::const_iterator i = m_qt2katevi.constBegin(); i != m_qt2katevi.constEnd(); ++i) { m_katevi2qt.insert(i.value(), i.key()); } m_katevi2qt.insert(QStringLiteral("cr"), Qt::Key_Enter); m_nameToKeyCode.insert(QStringLiteral("invalid"), -1); m_nameToKeyCode.insert(QStringLiteral("esc"), 1); m_nameToKeyCode.insert(QStringLiteral("tab"), 2); m_nameToKeyCode.insert(QStringLiteral("backtab"), 3); m_nameToKeyCode.insert(QStringLiteral("backspace"), 4); m_nameToKeyCode.insert(QStringLiteral("return"), 5); m_nameToKeyCode.insert(QStringLiteral("enter"), 6); m_nameToKeyCode.insert(QStringLiteral("insert"), 7); m_nameToKeyCode.insert(QStringLiteral("delete"), 8); m_nameToKeyCode.insert(QStringLiteral("pause"), 9); m_nameToKeyCode.insert(QStringLiteral("print"), 10); m_nameToKeyCode.insert(QStringLiteral("sysreq"), 11); m_nameToKeyCode.insert(QStringLiteral("clear"), 12); m_nameToKeyCode.insert(QStringLiteral("home"), 13); m_nameToKeyCode.insert(QStringLiteral("end"), 14); m_nameToKeyCode.insert(QStringLiteral("left"), 15); m_nameToKeyCode.insert(QStringLiteral("up"), 16); m_nameToKeyCode.insert(QStringLiteral("right"), 17); m_nameToKeyCode.insert(QStringLiteral("down"), 18); m_nameToKeyCode.insert(QStringLiteral("pageup"), 19); m_nameToKeyCode.insert(QStringLiteral("pagedown"), 20); m_nameToKeyCode.insert(QStringLiteral("shift"), 21); m_nameToKeyCode.insert(QStringLiteral("control"), 22); m_nameToKeyCode.insert(QStringLiteral("meta"), 23); m_nameToKeyCode.insert(QStringLiteral("alt"), 24); m_nameToKeyCode.insert(QStringLiteral("altgr"), 25); m_nameToKeyCode.insert(QStringLiteral("capslock"), 26); m_nameToKeyCode.insert(QStringLiteral("numlock"), 27); m_nameToKeyCode.insert(QStringLiteral("scrolllock"), 28); m_nameToKeyCode.insert(QStringLiteral("f1"), 29); m_nameToKeyCode.insert(QStringLiteral("f2"), 30); m_nameToKeyCode.insert(QStringLiteral("f3"), 31); m_nameToKeyCode.insert(QStringLiteral("f4"), 32); m_nameToKeyCode.insert(QStringLiteral("f5"), 33); m_nameToKeyCode.insert(QStringLiteral("f6"), 34); m_nameToKeyCode.insert(QStringLiteral("f7"), 35); m_nameToKeyCode.insert(QStringLiteral("f8"), 36); m_nameToKeyCode.insert(QStringLiteral("f9"), 37); m_nameToKeyCode.insert(QStringLiteral("f10"), 38); m_nameToKeyCode.insert(QStringLiteral("f11"), 39); m_nameToKeyCode.insert(QStringLiteral("f12"), 40); m_nameToKeyCode.insert(QStringLiteral("f13"), 41); m_nameToKeyCode.insert(QStringLiteral("f14"), 42); m_nameToKeyCode.insert(QStringLiteral("f15"), 43); m_nameToKeyCode.insert(QStringLiteral("f16"), 44); m_nameToKeyCode.insert(QStringLiteral("f17"), 45); m_nameToKeyCode.insert(QStringLiteral("f18"), 46); m_nameToKeyCode.insert(QStringLiteral("f19"), 47); m_nameToKeyCode.insert(QStringLiteral("f20"), 48); m_nameToKeyCode.insert(QStringLiteral("f21"), 49); m_nameToKeyCode.insert(QStringLiteral("f22"), 50); m_nameToKeyCode.insert(QStringLiteral("f23"), 51); m_nameToKeyCode.insert(QStringLiteral("f24"), 52); m_nameToKeyCode.insert(QStringLiteral("f25"), 53); m_nameToKeyCode.insert(QStringLiteral("f26"), 54); m_nameToKeyCode.insert(QStringLiteral("f27"), 55); m_nameToKeyCode.insert(QStringLiteral("f28"), 56); m_nameToKeyCode.insert(QStringLiteral("f29"), 57); m_nameToKeyCode.insert(QStringLiteral("f30"), 58); m_nameToKeyCode.insert(QStringLiteral("f31"), 59); m_nameToKeyCode.insert(QStringLiteral("f32"), 60); m_nameToKeyCode.insert(QStringLiteral("f33"), 61); m_nameToKeyCode.insert(QStringLiteral("f34"), 62); m_nameToKeyCode.insert(QStringLiteral("f35"), 63); m_nameToKeyCode.insert(QStringLiteral("super_l"), 64); m_nameToKeyCode.insert(QStringLiteral("super_r"), 65); m_nameToKeyCode.insert(QStringLiteral("menu"), 66); m_nameToKeyCode.insert(QStringLiteral("hyper_l"), 67); m_nameToKeyCode.insert(QStringLiteral("hyper_r"), 68); m_nameToKeyCode.insert(QStringLiteral("help"), 69); m_nameToKeyCode.insert(QStringLiteral("direction_l"), 70); m_nameToKeyCode.insert(QStringLiteral("direction_r"), 71); m_nameToKeyCode.insert(QStringLiteral("multi_key"), 172); m_nameToKeyCode.insert(QStringLiteral("codeinput"), 173); m_nameToKeyCode.insert(QStringLiteral("singlecandidate"), 174); m_nameToKeyCode.insert(QStringLiteral("multiplecandidate"), 175); m_nameToKeyCode.insert(QStringLiteral("previouscandidate"), 176); m_nameToKeyCode.insert(QStringLiteral("mode_switch"), 177); m_nameToKeyCode.insert(QStringLiteral("kanji"), 178); m_nameToKeyCode.insert(QStringLiteral("muhenkan"), 179); m_nameToKeyCode.insert(QStringLiteral("henkan"), 180); m_nameToKeyCode.insert(QStringLiteral("romaji"), 181); m_nameToKeyCode.insert(QStringLiteral("hiragana"), 182); m_nameToKeyCode.insert(QStringLiteral("katakana"), 183); m_nameToKeyCode.insert(QStringLiteral("hiragana_katakana"), 184); m_nameToKeyCode.insert(QStringLiteral("zenkaku"), 185); m_nameToKeyCode.insert(QStringLiteral("hankaku"), 186); m_nameToKeyCode.insert(QStringLiteral("zenkaku_hankaku"), 187); m_nameToKeyCode.insert(QStringLiteral("touroku"), 188); m_nameToKeyCode.insert(QStringLiteral("massyo"), 189); m_nameToKeyCode.insert(QStringLiteral("kana_lock"), 190); m_nameToKeyCode.insert(QStringLiteral("kana_shift"), 191); m_nameToKeyCode.insert(QStringLiteral("eisu_shift"), 192); m_nameToKeyCode.insert(QStringLiteral("eisu_toggle"), 193); m_nameToKeyCode.insert(QStringLiteral("hangul"), 194); m_nameToKeyCode.insert(QStringLiteral("hangul_start"), 195); m_nameToKeyCode.insert(QStringLiteral("hangul_end"), 196); m_nameToKeyCode.insert(QStringLiteral("hangul_hanja"), 197); m_nameToKeyCode.insert(QStringLiteral("hangul_jamo"), 198); m_nameToKeyCode.insert(QStringLiteral("hangul_romaja"), 199); m_nameToKeyCode.insert(QStringLiteral("hangul_jeonja"), 200); m_nameToKeyCode.insert(QStringLiteral("hangul_banja"), 201); m_nameToKeyCode.insert(QStringLiteral("hangul_prehanja"), 202); m_nameToKeyCode.insert(QStringLiteral("hangul_posthanja"), 203); m_nameToKeyCode.insert(QStringLiteral("hangul_special"), 204); m_nameToKeyCode.insert(QStringLiteral("dead_grave"), 205); m_nameToKeyCode.insert(QStringLiteral("dead_acute"), 206); m_nameToKeyCode.insert(QStringLiteral("dead_circumflex"), 207); m_nameToKeyCode.insert(QStringLiteral("dead_tilde"), 208); m_nameToKeyCode.insert(QStringLiteral("dead_macron"), 209); m_nameToKeyCode.insert(QStringLiteral("dead_breve"), 210); m_nameToKeyCode.insert(QStringLiteral("dead_abovedot"), 211); m_nameToKeyCode.insert(QStringLiteral("dead_diaeresis"), 212); m_nameToKeyCode.insert(QStringLiteral("dead_abovering"), 213); m_nameToKeyCode.insert(QStringLiteral("dead_doubleacute"), 214); m_nameToKeyCode.insert(QStringLiteral("dead_caron"), 215); m_nameToKeyCode.insert(QStringLiteral("dead_cedilla"), 216); m_nameToKeyCode.insert(QStringLiteral("dead_ogonek"), 217); m_nameToKeyCode.insert(QStringLiteral("dead_iota"), 218); m_nameToKeyCode.insert(QStringLiteral("dead_voiced_sound"), 219); m_nameToKeyCode.insert(QStringLiteral("dead_semivoiced_sound"), 220); m_nameToKeyCode.insert(QStringLiteral("dead_belowdot"), 221); m_nameToKeyCode.insert(QStringLiteral("dead_hook"), 222); m_nameToKeyCode.insert(QStringLiteral("dead_horn"), 223); m_nameToKeyCode.insert(QStringLiteral("back"), 224); m_nameToKeyCode.insert(QStringLiteral("forward"), 225); m_nameToKeyCode.insert(QStringLiteral("stop"), 226); m_nameToKeyCode.insert(QStringLiteral("refresh"), 227); m_nameToKeyCode.insert(QStringLiteral("volumedown"), 228); m_nameToKeyCode.insert(QStringLiteral("volumemute"), 229); m_nameToKeyCode.insert(QStringLiteral("volumeup"), 230); m_nameToKeyCode.insert(QStringLiteral("bassboost"), 231); m_nameToKeyCode.insert(QStringLiteral("bassup"), 232); m_nameToKeyCode.insert(QStringLiteral("bassdown"), 233); m_nameToKeyCode.insert(QStringLiteral("trebleup"), 234); m_nameToKeyCode.insert(QStringLiteral("trebledown"), 235); m_nameToKeyCode.insert(QStringLiteral("mediaplay"), 236); m_nameToKeyCode.insert(QStringLiteral("mediastop"), 237); m_nameToKeyCode.insert(QStringLiteral("mediaprevious"), 238); m_nameToKeyCode.insert(QStringLiteral("medianext"), 239); m_nameToKeyCode.insert(QStringLiteral("mediarecord"), 240); m_nameToKeyCode.insert(QStringLiteral("homepage"), 241); m_nameToKeyCode.insert(QStringLiteral("favorites"), 242); m_nameToKeyCode.insert(QStringLiteral("search"), 243); m_nameToKeyCode.insert(QStringLiteral("standby"), 244); m_nameToKeyCode.insert(QStringLiteral("openurl"), 245); m_nameToKeyCode.insert(QStringLiteral("launchmail"), 246); m_nameToKeyCode.insert(QStringLiteral("launchmedia"), 247); m_nameToKeyCode.insert(QStringLiteral("launch0"), 248); m_nameToKeyCode.insert(QStringLiteral("launch1"), 249); m_nameToKeyCode.insert(QStringLiteral("launch2"), 250); m_nameToKeyCode.insert(QStringLiteral("launch3"), 251); m_nameToKeyCode.insert(QStringLiteral("launch4"), 252); m_nameToKeyCode.insert(QStringLiteral("launch5"), 253); m_nameToKeyCode.insert(QStringLiteral("launch6"), 254); m_nameToKeyCode.insert(QStringLiteral("launch7"), 255); m_nameToKeyCode.insert(QStringLiteral("launch8"), 256); m_nameToKeyCode.insert(QStringLiteral("launch9"), 257); m_nameToKeyCode.insert(QStringLiteral("launcha"), 258); m_nameToKeyCode.insert(QStringLiteral("launchb"), 259); m_nameToKeyCode.insert(QStringLiteral("launchc"), 260); m_nameToKeyCode.insert(QStringLiteral("launchd"), 261); m_nameToKeyCode.insert(QStringLiteral("launche"), 262); m_nameToKeyCode.insert(QStringLiteral("launchf"), 263); m_nameToKeyCode.insert(QStringLiteral("medialast"), 264); m_nameToKeyCode.insert(QStringLiteral("unknown"), 265); m_nameToKeyCode.insert(QStringLiteral("call"), 266); m_nameToKeyCode.insert(QStringLiteral("context1"), 267); m_nameToKeyCode.insert(QStringLiteral("context2"), 268); m_nameToKeyCode.insert(QStringLiteral("context3"), 269); m_nameToKeyCode.insert(QStringLiteral("context4"), 270); m_nameToKeyCode.insert(QStringLiteral("flip"), 271); m_nameToKeyCode.insert(QStringLiteral("hangup"), 272); m_nameToKeyCode.insert(QStringLiteral("no"), 273); m_nameToKeyCode.insert(QStringLiteral("select"), 274); m_nameToKeyCode.insert(QStringLiteral("yes"), 275); m_nameToKeyCode.insert(QStringLiteral("execute"), 276); m_nameToKeyCode.insert(QStringLiteral("printer"), 277); m_nameToKeyCode.insert(QStringLiteral("play"), 278); m_nameToKeyCode.insert(QStringLiteral("sleep"), 279); m_nameToKeyCode.insert(QStringLiteral("zoom"), 280); m_nameToKeyCode.insert(QStringLiteral("cancel"), 281); m_nameToKeyCode.insert(QStringLiteral("a"), 282); m_nameToKeyCode.insert(QStringLiteral("b"), 283); m_nameToKeyCode.insert(QStringLiteral("c"), 284); m_nameToKeyCode.insert(QStringLiteral("d"), 285); m_nameToKeyCode.insert(QStringLiteral("e"), 286); m_nameToKeyCode.insert(QStringLiteral("f"), 287); m_nameToKeyCode.insert(QStringLiteral("g"), 288); m_nameToKeyCode.insert(QStringLiteral("h"), 289); m_nameToKeyCode.insert(QStringLiteral("i"), 290); m_nameToKeyCode.insert(QStringLiteral("j"), 291); m_nameToKeyCode.insert(QStringLiteral("k"), 292); m_nameToKeyCode.insert(QStringLiteral("l"), 293); m_nameToKeyCode.insert(QStringLiteral("m"), 294); m_nameToKeyCode.insert(QStringLiteral("n"), 295); m_nameToKeyCode.insert(QStringLiteral("o"), 296); m_nameToKeyCode.insert(QStringLiteral("p"), 297); m_nameToKeyCode.insert(QStringLiteral("q"), 298); m_nameToKeyCode.insert(QStringLiteral("r"), 299); m_nameToKeyCode.insert(QStringLiteral("s"), 300); m_nameToKeyCode.insert(QStringLiteral("t"), 301); m_nameToKeyCode.insert(QStringLiteral("u"), 302); m_nameToKeyCode.insert(QStringLiteral("v"), 303); m_nameToKeyCode.insert(QStringLiteral("w"), 304); m_nameToKeyCode.insert(QStringLiteral("x"), 305); m_nameToKeyCode.insert(QStringLiteral("y"), 306); m_nameToKeyCode.insert(QStringLiteral("z"), 307); m_nameToKeyCode.insert(QStringLiteral("`"), 308); m_nameToKeyCode.insert(QStringLiteral("!"), 309); m_nameToKeyCode.insert(QStringLiteral("\""), 310); m_nameToKeyCode.insert(QStringLiteral("$"), 311); m_nameToKeyCode.insert(QStringLiteral("%"), 312); m_nameToKeyCode.insert(QStringLiteral("^"), 313); m_nameToKeyCode.insert(QStringLiteral("&"), 314); m_nameToKeyCode.insert(QStringLiteral("*"), 315); m_nameToKeyCode.insert(QStringLiteral("("), 316); m_nameToKeyCode.insert(QStringLiteral(")"), 317); m_nameToKeyCode.insert(QStringLiteral("-"), 318); m_nameToKeyCode.insert(QStringLiteral("_"), 319); m_nameToKeyCode.insert(QStringLiteral("="), 320); m_nameToKeyCode.insert(QStringLiteral("+"), 321); m_nameToKeyCode.insert(QStringLiteral("["), 322); m_nameToKeyCode.insert(QStringLiteral("]"), 323); m_nameToKeyCode.insert(QStringLiteral("{"), 324); m_nameToKeyCode.insert(QStringLiteral("}"), 325); m_nameToKeyCode.insert(QStringLiteral(":"), 326); m_nameToKeyCode.insert(QStringLiteral(";"), 327); m_nameToKeyCode.insert(QStringLiteral("@"), 328); m_nameToKeyCode.insert(QStringLiteral("'"), 329); m_nameToKeyCode.insert(QStringLiteral("#"), 330); m_nameToKeyCode.insert(QStringLiteral("~"), 331); m_nameToKeyCode.insert(QStringLiteral("\\"), 332); m_nameToKeyCode.insert(QStringLiteral("|"), 333); m_nameToKeyCode.insert(QStringLiteral(","), 334); m_nameToKeyCode.insert(QStringLiteral("."), 335); //m_nameToKeyCode.insert( QLatin1String( ">" ), 336 ); m_nameToKeyCode.insert(QStringLiteral("/"), 337); m_nameToKeyCode.insert(QStringLiteral("?"), 338); m_nameToKeyCode.insert(QStringLiteral(" "), 339); //m_nameToKeyCode.insert( QLatin1String( "<" ), 341 ); m_nameToKeyCode.insert(QStringLiteral("0"), 340); m_nameToKeyCode.insert(QStringLiteral("1"), 341); m_nameToKeyCode.insert(QStringLiteral("2"), 342); m_nameToKeyCode.insert(QStringLiteral("3"), 343); m_nameToKeyCode.insert(QStringLiteral("4"), 344); m_nameToKeyCode.insert(QStringLiteral("5"), 345); m_nameToKeyCode.insert(QStringLiteral("6"), 346); m_nameToKeyCode.insert(QStringLiteral("7"), 347); m_nameToKeyCode.insert(QStringLiteral("8"), 348); m_nameToKeyCode.insert(QStringLiteral("9"), 349); m_nameToKeyCode.insert(QStringLiteral("cr"), 350); m_nameToKeyCode.insert(QStringLiteral("leader"), 351); m_nameToKeyCode.insert(QStringLiteral("nop"), 352); for (QHash::const_iterator i = m_nameToKeyCode.constBegin(); i != m_nameToKeyCode.constEnd(); ++i) { m_keyCodeToName.insert(i.value(), i.key()); } } QString KeyParser::qt2vi(int key) const { return (m_qt2katevi.contains(key) ? m_qt2katevi.value(key) : QStringLiteral("invalid")); } int KeyParser::vi2qt(const QString &keypress) const { return (m_katevi2qt.contains(keypress) ? m_katevi2qt.value(keypress) : -1); } int KeyParser::encoded2qt(const QString &keypress) const { QString key = KeyParser::self()->decodeKeySequence(keypress); if (key.length() > 2 && key[0] == QLatin1Char('<') && key[key.length() - 1] == QLatin1Char('>')) { key = key.mid(1, key.length() - 2); } return (m_katevi2qt.contains(key) ? m_katevi2qt.value(key) : -1); } const QString KeyParser::encodeKeySequence(const QString &keys) const { QString encodedSequence; unsigned int keyCodeTemp = 0; bool insideTag = false; QChar c; for (int i = 0; i < keys.length(); i++) { c = keys.at(i); if (insideTag) { if (c == QLatin1Char('>')) { QString temp; temp.setNum(0xE000 + keyCodeTemp, 16); QChar code(0xE000 + keyCodeTemp); encodedSequence.append(code); keyCodeTemp = 0; insideTag = false; continue; } else { // contains modifiers if (keys.midRef(i).indexOf(QLatin1Char('-')) != -1 && keys.midRef(i).indexOf(QLatin1Char('-')) < keys.midRef(i).indexOf(QLatin1Char('>'))) { // Perform something similar to a split on '-', except that we want to keep the occurrences of '-' // e.g. will equate to the list of tokens "c-", "s-", "a". // A straight split on '-' would give us "c", "s", "a", in which case we lose the piece of information that // 'a' is just the 'a' key, not the 'alt' modifier. const QString untilClosing = keys.mid(i, keys.midRef(i).indexOf(QLatin1Char('>'))).toLower(); QStringList tokens; int currentPos = 0; int nextHypen = -1; while ((nextHypen = untilClosing.indexOf(QLatin1Char('-'), currentPos)) != -1) { tokens << untilClosing.mid(currentPos, nextHypen - currentPos + 1); currentPos = nextHypen + 1; } tokens << untilClosing.mid(currentPos); foreach (const QString &str, tokens) { if (str == QLatin1String("s-") && (keyCodeTemp & 0x01) != 0x1) { keyCodeTemp += 0x1; } else if (str == QLatin1String("c-") && (keyCodeTemp & 0x02) != 0x2) { keyCodeTemp += 0x2; } else if (str == QLatin1String("a-") && (keyCodeTemp & 0x04) != 0x4) { keyCodeTemp += 0x4; } else if (str == QLatin1String("m-") && (keyCodeTemp & 0x08) != 0x8) { keyCodeTemp += 0x8; } else { if (m_nameToKeyCode.contains(str) || (str.length() == 1 && str.at(0).isLetterOrNumber())) { if (m_nameToKeyCode.contains(str)) { keyCodeTemp += m_nameToKeyCode.value(str) * 0x10; } else { keyCodeTemp += str.at(0).unicode() * 0x10; } } else { int endOfBlock = keys.indexOf(QLatin1Char('>')); if (endOfBlock -= -1) { endOfBlock = keys.length() - 1; } encodedSequence.clear(); encodedSequence.append(m_nameToKeyCode.value(QStringLiteral("invalid"))); break; } } } } else { QString str = keys.mid(i, keys.indexOf(QLatin1Char('>'), i) - i).toLower(); if (keys.indexOf(QLatin1Char('>'), i) != -1 && (m_nameToKeyCode.contains(str) || (str.length() == 1 && str.at(0).isLetterOrNumber()))) { if (m_nameToKeyCode.contains(str)) { keyCodeTemp += m_nameToKeyCode.value(str) * 0x10; } else { keyCodeTemp += str.at(0).unicode() * 0x10; } } else { int endOfBlock = keys.indexOf(QLatin1Char('>')); if (endOfBlock -= -1) { endOfBlock = keys.length() - 1; } encodedSequence.clear(); keyCodeTemp = m_nameToKeyCode.value(QStringLiteral("invalid")) * 0x10; } } i += keys.midRef(i, keys.midRef(i).indexOf(QLatin1Char('>'))).length() - 1; } } else { if (c == QLatin1Char('<')) { // If there's no closing '>', or if there is an opening '<' before the next '>', interpret as a literal '<' // If we are , encode as a literal " ". QString rest = keys.mid(i); if (rest.indexOf(QLatin1Char('>'), 1) != -1 && rest.mid(1, rest.indexOf(QLatin1Char('>'), 1) - 1) == QLatin1String("space")) { - encodedSequence.append(QLatin1String(" ")); + encodedSequence.append(QLatin1Char(' ')); i += rest.indexOf(QLatin1Char('>'), 1); continue; } else if (rest.indexOf(QLatin1Char('>'), 1) == -1 || (rest.indexOf(QLatin1Char('<'), 1) < rest.indexOf(QLatin1Char('>'), 1) && rest.indexOf(QLatin1Char('<'), 1) != -1)) { encodedSequence.append(c); continue; } insideTag = true; continue; } else { encodedSequence.append(c); } } } return encodedSequence; } const QString KeyParser::decodeKeySequence(const QString &keys) const { QString ret; for (int i = 0; i < keys.length(); i++) { QChar c = keys.at(i); int keycode = c.unicode(); if ((keycode & 0xE000) != 0xE000) { ret.append(c); } else { ret.append(QLatin1Char('<')); if ((keycode & 0x1) == 0x1) { ret.append(QLatin1String("s-")); //keycode -= 0x1; } if ((keycode & 0x2) == 0x2) { ret.append(QLatin1String("c-")); //keycode -= 0x2; } if ((keycode & 0x4) == 0x4) { ret.append(QLatin1String("a-")); //keycode -= 0x4; } if ((keycode & 0x8) == 0x8) { ret.append(QLatin1String("m-")); //keycode -= 0x8; } if ((keycode & 0xE000) == 0xE000) { ret.append(m_keyCodeToName.value((keycode - 0xE000) / 0x10)); } else { ret.append(QChar(keycode)); } ret.append(QLatin1Char('>')); } } return ret; } const QChar KeyParser::KeyEventToQChar(const QKeyEvent &keyEvent) { const int keyCode = keyEvent.key(); const QString &text = keyEvent.text(); const Qt::KeyboardModifiers mods = keyEvent.modifiers(); // If previous key press was AltGr, return key value right away and don't go // down the "handle modifiers" code path. AltGr is really confusing... if (mods & Qt::GroupSwitchModifier) { return (!text.isEmpty()) ? text.at(0) : QChar(); } if (text.isEmpty() || (text.length() == 1 && text.at(0) < 0x20) || keyCode == Qt::Key_Delete || (mods != Qt::NoModifier && mods != Qt::ShiftModifier && mods != Qt::KeypadModifier)) { QString keyPress; keyPress.append(QLatin1Char('<')); keyPress.append((mods & Qt::ShiftModifier) ? QStringLiteral("s-") : QString()); keyPress.append((mods & Qt::ControlModifier) ? QStringLiteral("c-") : QString()); keyPress.append((mods & Qt::AltModifier) ? QStringLiteral("a-") : QString()); keyPress.append((mods & Qt::MetaModifier) ? QStringLiteral("m-") : QString()); keyPress.append(keyCode <= 0xFF ? QChar(keyCode) : qt2vi(keyCode)); keyPress.append(QLatin1Char('>')); return encodeKeySequence(keyPress).at(0); } else { return text.at(0); } } diff --git a/src/vimode/macros.cpp b/src/vimode/macros.cpp index adc2850e..7e7b0e84 100644 --- a/src/vimode/macros.cpp +++ b/src/vimode/macros.cpp @@ -1,153 +1,153 @@ /* This file is part of the KDE libraries * * 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 "macros.h" #include #include using namespace KateVi; Macros::Macros() { } Macros::~Macros() { } void Macros::writeConfig(KConfigGroup &config) const { QStringList macroRegisters; foreach (const QChar ¯oRegister, m_macros.keys()) { macroRegisters.append(macroRegister); } QStringList macroContents; foreach (const QChar ¯oRegister, m_macros.keys()) { macroContents.append(KeyParser::self()->decodeKeySequence(m_macros[macroRegister])); } QStringList macroCompletions; foreach (const QChar ¯oRegister, m_macros.keys()) { macroCompletions.append(QString::number(m_completions[macroRegister].length())); foreach (const Completion &completionForMacro, m_completions[macroRegister]) { macroCompletions.append(encodeMacroCompletionForConfig(completionForMacro)); } } config.writeEntry("Macro Registers", macroRegisters); config.writeEntry("Macro Contents", macroContents); config.writeEntry("Macro Completions", macroCompletions); } void Macros::readConfig(const KConfigGroup &config) { const QStringList macroRegisters = config.readEntry("Macro Registers", QStringList()); const QStringList macroContents = config.readEntry("Macro Contents", QStringList()); const QStringList macroCompletions = config.readEntry("Macro Completions", QStringList()); int macroCompletionsIndex = 0; if (macroRegisters.length() == macroContents.length()) { for (int macroIndex = 0; macroIndex < macroRegisters.length(); macroIndex++) { const QChar macroRegister = macroRegisters[macroIndex].at(0); m_macros[macroRegister] = KeyParser::self()->encodeKeySequence(macroContents[macroIndex]); macroCompletionsIndex = readMacroCompletions(macroRegister, macroCompletions, macroCompletionsIndex); } } } void Macros::clear() { m_macros.clear(); } void Macros::remove(const QChar ®) { m_macros.remove(reg); } void Macros::store(const QChar ®, const QList ¯oKeyEventLog, const CompletionList &completions) { m_macros[reg].clear(); QList withoutClosingQ = macroKeyEventLog; Q_ASSERT(!macroKeyEventLog.isEmpty() && macroKeyEventLog.last().key() == Qt::Key_Q); withoutClosingQ.pop_back(); foreach (const QKeyEvent &keyEvent, withoutClosingQ) { const QChar key = KeyParser::self()->KeyEventToQChar(keyEvent); m_macros[reg].append(key); } m_completions[reg] = completions; } QString Macros::get(const QChar ®) const { return m_macros.contains(reg) ? m_macros[reg] : QString(); } CompletionList Macros::getCompletions(const QChar ®) const { return m_completions.contains(reg) ? m_completions[reg] : CompletionList(); } int Macros::readMacroCompletions(const QChar ®, const QStringList &encodedMacroCompletions, int macroCompletionsIndex) { if (macroCompletionsIndex < encodedMacroCompletions.length()) { bool parsedNumCompletionsSuccessfully = false; const QString numCompletionsAsString = encodedMacroCompletions[macroCompletionsIndex++]; const int numCompletions = numCompletionsAsString.toInt(&parsedNumCompletionsSuccessfully); int count = 0; m_completions[reg].clear(); while (count < numCompletions && macroCompletionsIndex < encodedMacroCompletions.length()) { const QString encodedMacroCompletion = encodedMacroCompletions[macroCompletionsIndex++]; count++; m_completions[reg].append(decodeMacroCompletionFromConfig(encodedMacroCompletion)); } } return macroCompletionsIndex; } QString Macros::encodeMacroCompletionForConfig(const Completion &completionForMacro) const { const bool endedWithSemiColon = completionForMacro.completedText().endsWith(QLatin1Char(';')); - QString encodedMacroCompletion = completionForMacro.completedText().remove(QStringLiteral("()")).remove(QStringLiteral(";")); + QString encodedMacroCompletion = completionForMacro.completedText().remove(QStringLiteral("()")).remove(QLatin1Char(';')); if (completionForMacro.completionType() == Completion::FunctionWithArgs) { encodedMacroCompletion += QLatin1String("(...)"); } else if (completionForMacro.completionType() == Completion::FunctionWithoutArgs) { encodedMacroCompletion += QLatin1String("()"); } if (endedWithSemiColon) { encodedMacroCompletion += QLatin1Char(';'); } if (completionForMacro.removeTail()) { encodedMacroCompletion += QLatin1Char('|'); } return encodedMacroCompletion; } Completion Macros::decodeMacroCompletionFromConfig(const QString &encodedMacroCompletion) { - const bool removeTail = encodedMacroCompletion.endsWith(QLatin1String("|")); + const bool removeTail = encodedMacroCompletion.endsWith(QLatin1Char('|')); Completion::CompletionType completionType = Completion::PlainText; if (encodedMacroCompletion.contains(QLatin1String("(...)"))) { completionType = Completion::FunctionWithArgs; } else if (encodedMacroCompletion.contains(QLatin1String("()"))) { completionType = Completion::FunctionWithoutArgs; } QString completionText = encodedMacroCompletion; completionText.replace(QLatin1String("(...)"), QLatin1String("()")).remove(QLatin1Char('|')); return Completion(completionText, removeTail, completionType); } diff --git a/src/vimode/marks.cpp b/src/vimode/marks.cpp index 1e82438c..6396a9ee 100644 --- a/src/vimode/marks.cpp +++ b/src/vimode/marks.cpp @@ -1,315 +1,315 @@ /* This file is part of the KDE libraries * * 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 "marks.h" #include "kateview.h" #include "katedocument.h" #include #include #include using namespace KateVi; namespace { const QChar BeginEditYanked = QLatin1Char('['); const QChar EndEditYanked = QLatin1Char(']'); const QChar LastChange = QLatin1Char('.'); const QChar InsertStopped = QLatin1Char('^'); const QChar SelectionBegin = QLatin1Char('<'); const QChar SelectionEnd = QLatin1Char('>'); const QChar FirstUserMark = QLatin1Char('a'); const QChar LastUserMark = QLatin1Char('z'); const QChar BeforeJump = QLatin1Char('\''); const QChar BeforeJumpAlter = QLatin1Char('`'); const QChar UserMarks[] = {QLatin1Char('a'), QLatin1Char('b'), QLatin1Char('c'), QLatin1Char('d'), QLatin1Char('e'), QLatin1Char('f'), QLatin1Char('g'), QLatin1Char('h'), QLatin1Char('i'), QLatin1Char('j'), QLatin1Char('k'), QLatin1Char('l'), QLatin1Char('m'), QLatin1Char('n'), QLatin1Char('o'), QLatin1Char('p'), QLatin1Char('q'), QLatin1Char('r'), QLatin1Char('s'), QLatin1Char('t'), QLatin1Char('u'), QLatin1Char('v'), QLatin1Char('w'), QLatin1Char('x'), QLatin1Char('y'), QLatin1Char('z')}; } Marks::Marks(InputModeManager *imm) : m_inputModeManager(imm) , m_doc(imm->view()->doc()) , m_settingMark(false) { connect(m_doc, &KTextEditor::DocumentPrivate::markChanged, this, &Marks::markChanged); } Marks::~Marks() { } void Marks::readSessionConfig(const KConfigGroup &config) { QStringList marks = config.readEntry("ViMarks", QStringList()); for (int i = 0; i + 2 < marks.size(); i += 3) { KTextEditor::Cursor c(marks.at(i + 1).toInt(), marks.at(i + 2).toInt()); setMark(marks.at(i).at(0), c); } syncViMarksAndBookmarks(); } void Marks::writeSessionConfig(KConfigGroup &config) const { QStringList l; Q_FOREACH (QChar key, m_marks.keys()) { l << key << QString::number(m_marks.value(key)->line()) << QString::number(m_marks.value(key)->column()); } config.writeEntry("ViMarks", l); } void Marks::setMark(const QChar &_mark, const KTextEditor::Cursor &pos) { // move on insert is type based, this allows to reuse cursors! // reuse is important for editing intensive things like replace-all const bool moveoninsert = _mark != BeginEditYanked; m_settingMark = true; // ` and ' is the same register (position before jump) const QChar mark = (_mark == BeforeJumpAlter) ? BeforeJump : _mark; // if we have already a cursor for this type: adjust it bool needToAdjustVisibleMark = true; if (KTextEditor::MovingCursor *oldCursor = m_marks.value(mark)) { // cleanup mark display only if line changes needToAdjustVisibleMark = oldCursor->line() != pos.line(); if (needToAdjustVisibleMark) { int number_of_marks = 0; foreach (QChar c, m_marks.keys()) { if (m_marks.value(c)->line() == oldCursor->line()) { number_of_marks++; } } if (number_of_marks == 1) { m_doc->removeMark(oldCursor->line(), KTextEditor::MarkInterface::markType01); } } // adjust position oldCursor->setPosition(pos); } else { // if no old mark of that type, create new one const KTextEditor::MovingCursor::InsertBehavior behavior = moveoninsert ? KTextEditor::MovingCursor::MoveOnInsert : KTextEditor::MovingCursor::StayOnInsert; m_marks.insert(mark, m_doc->newMovingCursor(pos, behavior)); } // Showing what mark we set, can be skipped if we did not change the line if (isShowable(mark)) { if (needToAdjustVisibleMark && !(m_doc->mark(pos.line()) & KTextEditor::MarkInterface::markType01)) { m_doc->addMark(pos.line(), KTextEditor::MarkInterface::markType01); } // only show message for active view if (m_inputModeManager->view()->viewInputMode() == KTextEditor::View::ViInputMode) { if (m_doc->activeView() == m_inputModeManager->view()) { m_inputModeManager->getViNormalMode()->message(i18n("Mark set: %1", mark)); } } } m_settingMark = false; } KTextEditor::Cursor Marks::getMarkPosition(const QChar &mark) const { if (m_marks.contains(mark)) { KTextEditor::MovingCursor *c = m_marks.value(mark); return KTextEditor::Cursor(c->line(), c->column()); } return KTextEditor::Cursor::invalid(); } void Marks::markChanged(KTextEditor::Document *doc, KTextEditor::Mark mark, KTextEditor::MarkInterface::MarkChangeAction action) { Q_UNUSED(doc) if (mark.type != KTextEditor::MarkInterface::Bookmark || m_settingMark) { return; } if (action == KTextEditor::MarkInterface::MarkRemoved) { foreach (QChar markerChar, m_marks.keys()) { if (m_marks.value(markerChar)->line() == mark.line) { m_marks.remove(markerChar); } } } else if (action == KTextEditor::MarkInterface::MarkAdded) { bool freeMarkerCharFound = false; for (const QChar &markerChar : UserMarks) { if (!m_marks.value(markerChar)) { setMark(markerChar, KTextEditor::Cursor(mark.line, 0)); freeMarkerCharFound = true; break; } } if (!freeMarkerCharFound) { m_inputModeManager->getViNormalMode()->error(i18n("There are no more chars for the next bookmark.")); } } } void Marks::syncViMarksAndBookmarks() { const QHash &marks = m_doc->marks(); // Each bookmark should have a vi mark on the same line. for (auto mark : marks) { if (!(mark->type & KTextEditor::MarkInterface::markType01)) { continue; } bool thereIsViMarkForThisLine = false; Q_FOREACH (const KTextEditor::MovingCursor *cursor, m_marks) { if (cursor->line() == mark->line) { thereIsViMarkForThisLine = true; break; } } if (thereIsViMarkForThisLine) { continue; } for (const QChar &markerChar : UserMarks) { if (!m_marks.value(markerChar)) { setMark(markerChar, KTextEditor::Cursor(mark->line, 0)); break; } } } // For showable vi mark a line should be bookmarked. Q_FOREACH (const QChar &markChar, m_marks.keys()) { if (!isShowable(markChar)) { continue; } bool thereIsKateMarkForThisLine = false; for (auto mark : marks) { if (!(mark->type & KTextEditor::MarkInterface::markType01)) { continue; } if (m_marks.value(markChar)->line() == mark->line) { thereIsKateMarkForThisLine = true; break; } } if (!thereIsKateMarkForThisLine) { m_doc->addMark(m_marks.value(markChar)->line(), KTextEditor::MarkInterface::markType01); } } } QString Marks::getMarksOnTheLine(int line) const { QString res; Q_FOREACH (QChar markerChar, m_marks.keys()) { if (m_marks.value(markerChar)->line() == line) { - res += markerChar + QLatin1String(":") + QString::number(m_marks.value(markerChar)->column()) + QLatin1String(" "); + res += markerChar + QLatin1Char(':') + QString::number(m_marks.value(markerChar)->column()) + QLatin1Char(' '); } } return res; } bool Marks::isShowable(const QChar &mark) { return FirstUserMark <= mark && mark <= LastUserMark; } void Marks::setStartEditYanked(const KTextEditor::Cursor &pos) { setMark(BeginEditYanked, pos); } void Marks::setFinishEditYanked(const KTextEditor::Cursor &pos) { setMark(EndEditYanked, pos); } void Marks::setLastChange(const KTextEditor::Cursor &pos) { setMark(LastChange, pos); } void Marks::setInsertStopped(const KTextEditor::Cursor &pos) { setMark(InsertStopped, pos); } void Marks::setSelectionStart(const KTextEditor::Cursor &pos) { setMark(SelectionBegin, pos); } void Marks::setSelectionFinish(const KTextEditor::Cursor &pos) { setMark(SelectionEnd, pos); } void Marks::setUserMark(const QChar &mark, const KTextEditor::Cursor &pos) { Q_ASSERT(FirstUserMark <= mark && mark <= LastUserMark); setMark(mark, pos); } KTextEditor::Cursor Marks::getStartEditYanked() const { return getMarkPosition(BeginEditYanked); } KTextEditor::Cursor Marks::getFinishEditYanked() const { return getMarkPosition(EndEditYanked); } KTextEditor::Cursor Marks::getSelectionStart() const { return getMarkPosition(SelectionBegin); } KTextEditor::Cursor Marks::getSelectionFinish() const { return getMarkPosition(SelectionEnd); } KTextEditor::Cursor Marks::getLastChange() const { return getMarkPosition(LastChange); } KTextEditor::Cursor Marks::getInsertStopped() const { return getMarkPosition(InsertStopped); } diff --git a/src/vimode/modes/normalvimode.cpp b/src/vimode/modes/normalvimode.cpp index 12a8b866..b02aa223 100644 --- a/src/vimode/modes/normalvimode.cpp +++ b/src/vimode/modes/normalvimode.cpp @@ -1,4147 +1,4147 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2008-2009 Erlend Hamberg * Copyright (C) 2008 Evgeniy Ivanov * Copyright (C) 2009 Paul Gideon Dann * Copyright (C) 2011 Svyatoslav Kuzmich * Copyright (C) 2012 - 2013 Simon St James * * 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 "katebuffer.h" #include "katecompletionwidget.h" #include "kateconfig.h" #include "kateglobal.h" #include "katepartdebug.h" #include "kateundomanager.h" #include #include #include "kateviewhelpers.h" #include "kateviewinternal.h" #include #include #include #include #include #include #include #include #include #include #include "katecmd.h" #include #include "kateviinputmode.h" #include #include #include #include #include #include #include #include using namespace KateVi; #define ADDCMD(STR, FUNC, FLGS) m_commands.push_back( \ new Command( this, QStringLiteral(STR), &NormalViMode::FUNC, FLGS ) ); #define ADDMOTION(STR, FUNC, FLGS) m_motions.push_back( \ new Motion( this, QStringLiteral(STR), &NormalViMode::FUNC, FLGS ) ); NormalViMode::NormalViMode(InputModeManager *viInputModeManager, KTextEditor::ViewPrivate *view, KateViewInternal *viewInternal) : ModeBase() { m_view = view; m_viewInternal = viewInternal; m_viInputModeManager = viInputModeManager; m_stickyColumn = -1; m_lastMotionWasVisualLineUpOrDown = false; m_currentMotionWasVisualLineUpOrDown = false; // FIXME: make configurable m_extraWordCharacters = QString(); m_matchingItems[QStringLiteral("/*")] = QStringLiteral("*/"); m_matchingItems[QStringLiteral("*/")] = QStringLiteral("-/*"); m_matchItemRegex = generateMatchingItemRegex(); m_scroll_count_limit = 1000; // Limit of count for scroll commands. initializeCommands(); m_pendingResetIsDueToExit = false; m_isRepeatedTFcommand = false; m_lastMotionWasLinewiseInnerBlock = false; m_motionCanChangeWholeVisualModeSelection = false; resetParser(); // initialise with start configuration m_isUndo = false; connect(doc()->undoManager(), SIGNAL(undoStart(KTextEditor::Document*)), this, SLOT(undoBeginning())); connect(doc()->undoManager(), SIGNAL(undoEnd(KTextEditor::Document*)), this, SLOT(undoEnded())); updateYankHighlightAttrib(); connect(view, SIGNAL(configChanged()), this, SLOT(updateYankHighlightAttrib())); connect(doc(), SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)), this, SLOT(clearYankHighlight())); connect(doc(), SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document*)), this, SLOT(aboutToDeleteMovingInterfaceContent())); } NormalViMode::~NormalViMode() { qDeleteAll(m_commands); qDeleteAll(m_motions); qDeleteAll(m_highlightedYanks); } /** * parses a key stroke to check if it's a valid (part of) a command * @return true if a command was completed and executed, false otherwise */ bool NormalViMode::handleKeypress(const QKeyEvent *e) { const int keyCode = e->key(); // ignore modifier keys alone if (keyCode == Qt::Key_Shift || keyCode == Qt::Key_Control || keyCode == Qt::Key_Alt || keyCode == Qt::Key_Meta) { return false; } clearYankHighlight(); if (keyCode == Qt::Key_Escape || (keyCode == Qt::Key_C && e->modifiers() == Qt::ControlModifier) || (keyCode == Qt::Key_BracketLeft && e->modifiers() == Qt::ControlModifier)) { m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Block); m_pendingResetIsDueToExit = true; // Vim in weird as if we e.g. i it claims (in the status bar) to still be in insert mode, // but behaves as if it's in normal mode. I'm treating the status bar thing as a bug and just exiting // insert mode altogether. m_viInputModeManager->setTemporaryNormalMode(false); reset(); return true; } const QChar key = KeyParser::self()->KeyEventToQChar(*e); const QChar lastChar = m_keys.isEmpty() ? QChar::Null : m_keys.at(m_keys.size() - 1); const bool waitingForRegisterOrCharToSearch = this->waitingForRegisterOrCharToSearch(); // Use replace caret when reading a character for "r" if (key == QLatin1Char('r') && !waitingForRegisterOrCharToSearch) { m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Underline); } m_keysVerbatim.append(KeyParser::self()->decodeKeySequence(key)); if ((keyCode >= Qt::Key_0 && keyCode <= Qt::Key_9 && lastChar != QLatin1Char('"')) // key 0-9 && (m_countTemp != 0 || keyCode != Qt::Key_0) // first digit can't be 0 && (!waitingForRegisterOrCharToSearch) // Not in the middle of "find char" motions or replacing char. && e->modifiers() == Qt::NoModifier) { m_countTemp *= 10; m_countTemp += keyCode - Qt::Key_0; return true; } else if (m_countTemp != 0) { m_count = getCount() * m_countTemp; m_countTemp = 0; m_iscounted = true; } m_keys.append(key); if (m_viInputModeManager->macroRecorder()->isRecording() && key == QLatin1Char('q')) { // Need to special case this "finish macro" q, as the "begin macro" q // needs a parameter whereas the finish macro does not. m_viInputModeManager->macroRecorder()->stop(); resetParser(); return true; } if ((key == QLatin1Char('/') || key == QLatin1Char('?')) && !waitingForRegisterOrCharToSearch) { // Special case for "/" and "?": these should be motions, but this is complicated by // the fact that the user must interact with the search bar before the range of the // motion can be determined. // We hack around this by showing the search bar immediately, and, when the user has // finished interacting with it, have the search bar send a "synthetic" keypresses // that will either abort everything (if the search was aborted) or "complete" the motion // otherwise. m_positionWhenIncrementalSearchBegan = m_view->cursorPosition(); if (key == QLatin1Char('/')) { commandSearchForward(); } else { commandSearchBackward(); } return true; } // Special case: "cw" and "cW" work the same as "ce" and "cE" if the cursor is // on a non-blank. This is because Vim interprets "cw" as change-word, and a // word does not include the following white space. (:help cw in vim) if ((m_keys == QLatin1String("cw") || m_keys == QLatin1String("cW")) && !getCharUnderCursor().isSpace()) { // Special case of the special case: :-) // If the cursor is at the end of the current word rewrite to "cl" const bool isWORD = (m_keys.at(1) == QLatin1Char('W')); const KTextEditor::Cursor currentPosition(m_view->cursorPosition()); const KTextEditor::Cursor endOfWordOrWORD = (isWORD ? findWORDEnd(currentPosition.line(), currentPosition.column() - 1, true) : findWordEnd(currentPosition.line(), currentPosition.column() - 1, true)); if (currentPosition == endOfWordOrWORD) { m_keys = QStringLiteral("cl"); } else { if (isWORD) { m_keys = QStringLiteral("cE"); } else { m_keys = QStringLiteral("ce"); } } } if (m_keys[ 0 ] == Qt::Key_QuoteDbl) { if (m_keys.size() < 2) { return true; // waiting for a register } else { QChar r = m_keys[ 1 ].toLower(); if ((r >= QLatin1Char('0') && r <= QLatin1Char('9')) || (r >= QLatin1Char('a') && r <= QLatin1Char('z')) || r == QLatin1Char('_') || r == QLatin1Char('+') || r == QLatin1Char('*') || r == QLatin1Char('#') || r == QLatin1Char('^')) { m_register = r; m_keys.clear(); return true; } else { resetParser(); return true; } } } // if we have any matching commands so far, check which ones still match if (!m_matchingCommands.isEmpty()) { int n = m_matchingCommands.size() - 1; // remove commands not matching anymore for (int i = n; i >= 0; i--) { if (!m_commands.at(m_matchingCommands.at(i))->matches(m_keys)) { if (m_commands.at(m_matchingCommands.at(i))->needsMotion()) { // "cache" command needing a motion for later m_motionOperatorIndex = m_matchingCommands.at(i); } m_matchingCommands.remove(i); } } // check if any of the matching commands need a motion/text object, if so // push the current command length to m_awaitingMotionOrTextObject so one // knows where to split the command between the operator and the motion for (int i = 0; i < m_matchingCommands.size(); i++) { if (m_commands.at(m_matchingCommands.at(i))->needsMotion()) { m_awaitingMotionOrTextObject.push(m_keys.size()); break; } } } else { // go through all registered commands and put possible matches in m_matchingCommands for (int i = 0; i < m_commands.size(); i++) { if (m_commands.at(i)->matches(m_keys)) { m_matchingCommands.push_back(i); if (m_commands.at(i)->needsMotion() && m_commands.at(i)->pattern().length() == m_keys.size()) { m_awaitingMotionOrTextObject.push(m_keys.size()); } } } } // this indicates where in the command string one should start looking for a motion command int checkFrom = (m_awaitingMotionOrTextObject.isEmpty() ? 0 : m_awaitingMotionOrTextObject.top()); // Use operator-pending caret when reading a motion for an operator // in normal mode. We need to check that we are indeed in normal mode // since visual mode inherits from it. if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode && !m_awaitingMotionOrTextObject.isEmpty()) { m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Half); } // look for matching motion commands from position 'checkFrom' // FIXME: if checkFrom hasn't changed, only motions whose index is in // m_matchingMotions should be checked bool motionExecuted = false; if (checkFrom < m_keys.size()) { for (int i = 0; i < m_motions.size(); i++) { if (m_motions.at(i)->matches(m_keys.mid(checkFrom))) { m_lastMotionWasLinewiseInnerBlock = false; m_matchingMotions.push_back(i); // if it matches exact, we have found the motion command to execute if (m_motions.at(i)->matchesExact(m_keys.mid(checkFrom))) { m_currentMotionWasVisualLineUpOrDown = false; motionExecuted = true; if (checkFrom == 0) { // no command given before motion, just move the cursor to wherever // the motion says it should go to Range r = m_motions.at(i)->execute(); m_motionCanChangeWholeVisualModeSelection = m_motions.at(i)->canChangeWholeVisualModeSelection(); // jump over folding regions since we are just moving the cursor int currLine = m_view->cursorPosition().line(); int delta = r.endLine - currLine; int vline = m_view->textFolding().lineToVisibleLine(currLine); r.endLine = m_view->textFolding().visibleLineToLine(qMax(vline + delta, 0) /* ensure we have a valid line */); if (r.endLine >= doc()->lines()) { r.endLine = doc()->lines() - 1; } // make sure the position is valid before moving the cursor there // TODO: can this be simplified? :/ if (r.valid && r.endLine >= 0 && (r.endLine == 0 || r.endLine <= doc()->lines() - 1) && r.endColumn >= 0) { if (r.endColumn >= doc()->lineLength(r.endLine) && doc()->lineLength(r.endLine) > 0) { r.endColumn = doc()->lineLength(r.endLine) - 1; } goToPos(r); // in the case of VisualMode we need to remember the motion commands as well. if (!m_viInputModeManager->isAnyVisualMode()) { m_viInputModeManager->clearCurrentChangeLog(); } } else { qCDebug(LOG_KTE) << "Invalid position: (" << r.endLine << "," << r.endColumn << ")"; } resetParser(); // if normal mode was started by using Ctrl-O in insert mode, // it's time to go back to insert mode. if (m_viInputModeManager->getTemporaryNormalMode()) { startInsertMode(); m_viewInternal->repaint(); } m_lastMotionWasVisualLineUpOrDown = m_currentMotionWasVisualLineUpOrDown; break; } else { // execute the specified command and supply the position returned from // the motion m_commandRange = m_motions.at(i)->execute(); m_linewiseCommand = m_motions.at(i)->isLineWise(); // if we didn't get an explicit start position, use the current cursor position if (m_commandRange.startLine == -1) { KTextEditor::Cursor c(m_view->cursorPosition()); m_commandRange.startLine = c.line(); m_commandRange.startColumn = c.column(); } // special case: When using the "w" motion in combination with an operator and // the last word moved over is at the end of a line, the end of that word // becomes the end of the operated text, not the first word in the next line. if (m_motions.at(i)->pattern() == QLatin1String("w") || m_motions.at(i)->pattern() == QLatin1String("W")) { if (m_commandRange.endLine != m_commandRange.startLine && m_commandRange.endColumn == getFirstNonBlank(m_commandRange.endLine)) { m_commandRange.endLine--; m_commandRange.endColumn = doc()->lineLength(m_commandRange.endLine); } } m_commandWithMotion = true; if (m_commandRange.valid) { executeCommand(m_commands.at(m_motionOperatorIndex)); } else { qCDebug(LOG_KTE) << "Invalid range: " << "from (" << m_commandRange.startLine << "," << m_commandRange.startColumn << ")" << "to (" << m_commandRange.endLine << "," << m_commandRange.endColumn << ")"; } if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Block); } m_commandWithMotion = false; reset(); break; } } } } } if (this->waitingForRegisterOrCharToSearch()) { // If we are waiting for a char to search or a new register, // don't translate next character; we need the actual character so that e.g. // 'ab' is translated to 'fb' if the mappings 'a' -> 'f' and 'b' -> something else // exist. m_viInputModeManager->keyMapper()->setDoNotMapNextKeypress(); } if (motionExecuted) { return true; } // if we have only one match, check if it is a perfect match and if so, execute it // if it's not waiting for a motion or a text object if (m_matchingCommands.size() == 1) { if (m_commands.at(m_matchingCommands.at(0))->matchesExact(m_keys) && !m_commands.at(m_matchingCommands.at(0))->needsMotion()) { if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Block); } Command *cmd = m_commands.at(m_matchingCommands.at(0)); executeCommand(cmd); // check if reset() should be called. some commands in visual mode should not end visual mode if (cmd->shouldReset()) { reset(); m_view->setBlockSelection(false); } resetParser(); return true; } } else if (m_matchingCommands.size() == 0 && m_matchingMotions.size() == 0) { resetParser(); // A bit ugly: we haven't made use of the key event, // but don't want "typeable" keypresses (e.g. a, b, 3, etc) to be marked // as unused as they will then be added to the document, but we don't // want to swallow all keys in case this was a shortcut. // So say we made use of it if and only if it was *not* a shortcut. return e->type() != QEvent::ShortcutOverride; } m_matchingMotions.clear(); return true; // TODO - need to check this - it's currently required for making tests pass, but seems odd. } /** * (re)set to start configuration. This is done when a command is completed * executed or when a command is aborted */ void NormalViMode::resetParser() { m_keys.clear(); m_keysVerbatim.clear(); m_count = 0; m_oneTimeCountOverride = -1; m_iscounted = false; m_countTemp = 0; m_register = QChar::Null; m_findWaitingForChar = false; m_matchingCommands.clear(); m_matchingMotions.clear(); m_awaitingMotionOrTextObject.clear(); m_motionOperatorIndex = 0; m_commandWithMotion = false; m_linewiseCommand = true; m_deleteCommand = false; m_commandShouldKeepSelection = false; m_currentChangeEndMarker = KTextEditor::Cursor::invalid(); if(m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Block); } } // reset the command parser void NormalViMode::reset() { resetParser(); m_commandRange.startLine = -1; m_commandRange.startColumn = -1; } void NormalViMode::beginMonitoringDocumentChanges() { connect(doc(), &KTextEditor::DocumentPrivate::textInserted, this, &NormalViMode::textInserted); connect(doc(), &KTextEditor::DocumentPrivate::textRemoved, this, &NormalViMode::textRemoved); } void NormalViMode::executeCommand(const Command *cmd) { const ViMode originalViMode = m_viInputModeManager->getCurrentViMode(); cmd->execute(); // if normal mode was started by using Ctrl-O in insert mode, // it's time to go back to insert mode. if (m_viInputModeManager->getTemporaryNormalMode()) { startInsertMode(); m_viewInternal->repaint(); } // if the command was a change, and it didn't enter insert mode, store the key presses so that // they can be repeated with '.' if (m_viInputModeManager->getCurrentViMode() != ViMode::InsertMode && m_viInputModeManager->getCurrentViMode() != ViMode::ReplaceMode) { if (cmd->isChange() && !m_viInputModeManager->lastChangeRecorder()->isReplaying()) { m_viInputModeManager->storeLastChangeCommand(); } // when we transition to visual mode, remember the command in the keys history (V, v, ctrl-v, ...) // this will later result in buffer filled with something like this "Vjj>" which we can use later with repeat "." const bool commandSwitchedToVisualMode = ((originalViMode == ViMode::NormalMode) && m_viInputModeManager->isAnyVisualMode()); if (!commandSwitchedToVisualMode) { m_viInputModeManager->clearCurrentChangeLog(); } } // make sure the cursor does not end up after the end of the line KTextEditor::Cursor c(m_view->cursorPosition()); if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { int lineLength = doc()->lineLength(c.line()); if (c.column() >= lineLength) { if (lineLength == 0) { c.setColumn(0); } else { c.setColumn(lineLength - 1); } } updateCursor(c); } } //////////////////////////////////////////////////////////////////////////////// // COMMANDS AND OPERATORS //////////////////////////////////////////////////////////////////////////////// /** * enter insert mode at the cursor position */ bool NormalViMode::commandEnterInsertMode() { m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setCount(getCount()); return startInsertMode(); } /** * enter insert mode after the current character */ bool NormalViMode::commandEnterInsertModeAppend() { KTextEditor::Cursor c(m_view->cursorPosition()); c.setColumn(c.column() + 1); // if empty line, the cursor should start at column 0 if (doc()->lineLength(c.line()) == 0) { c.setColumn(0); } // cursor should never be in a column > number of columns if (c.column() > doc()->lineLength(c.line())) { c.setColumn(doc()->lineLength(c.line())); } updateCursor(c); m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setCount(getCount()); return startInsertMode(); } /** * start insert mode after the last character of the line */ bool NormalViMode::commandEnterInsertModeAppendEOL() { KTextEditor::Cursor c(m_view->cursorPosition()); c.setColumn(doc()->lineLength(c.line())); updateCursor(c); m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setCount(getCount()); return startInsertMode(); } bool NormalViMode::commandEnterInsertModeBeforeFirstNonBlankInLine() { KTextEditor::Cursor cursor(m_view->cursorPosition()); int c = getFirstNonBlank(); cursor.setColumn(c); updateCursor(cursor); m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setCount(getCount()); return startInsertMode(); } /** * enter insert mode at the last insert position */ bool NormalViMode::commandEnterInsertModeLast() { KTextEditor::Cursor c = m_viInputModeManager->marks()->getInsertStopped(); if (c.isValid()) { updateCursor(c); } m_stickyColumn = -1; return startInsertMode(); } bool NormalViMode::commandEnterVisualLineMode() { if (m_viInputModeManager->getCurrentViMode() == VisualLineMode) { reset(); return true; } return startVisualLineMode(); } bool NormalViMode::commandEnterVisualBlockMode() { if (m_viInputModeManager->getCurrentViMode() == VisualBlockMode) { reset(); return true; } return startVisualBlockMode(); } bool NormalViMode::commandReselectVisual() { // start last visual mode and set start = `< and cursor = `> KTextEditor::Cursor c1 = m_viInputModeManager->marks()->getSelectionStart(); KTextEditor::Cursor c2 = m_viInputModeManager->marks()->getSelectionFinish(); // we should either get two valid cursors or two invalid cursors Q_ASSERT(c1.isValid() == c2.isValid()); if (c1.isValid() && c2.isValid()) { m_viInputModeManager->getViVisualMode()->setStart(c1); bool returnValue = false; switch (m_viInputModeManager->getViVisualMode()->getLastVisualMode()) { case ViMode::VisualMode: returnValue = commandEnterVisualMode(); break; case ViMode::VisualLineMode: returnValue = commandEnterVisualLineMode(); break; case ViMode::VisualBlockMode: returnValue = commandEnterVisualBlockMode(); break; default: Q_ASSERT("invalid visual mode"); } m_viInputModeManager->getViVisualMode()->goToPos(c2); return returnValue; } else { error(QStringLiteral("No previous visual selection")); } return false; } bool NormalViMode::commandEnterVisualMode() { if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualMode) { reset(); return true; } return startVisualMode(); } bool NormalViMode::commandToOtherEnd() { if (m_viInputModeManager->isAnyVisualMode()) { m_viInputModeManager->getViVisualMode()->switchStartEnd(); return true; } return false; } bool NormalViMode::commandEnterReplaceMode() { m_stickyColumn = -1; m_viInputModeManager->getViReplaceMode()->setCount(getCount()); return startReplaceMode(); } bool NormalViMode::commandDeleteLine() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r; r.startLine = c.line(); r.endLine = c.line() + getCount() - 1; int column = c.column(); bool ret = deleteRange(r, LineWise); c = m_view->cursorPosition(); if (column > doc()->lineLength(c.line()) - 1) { column = doc()->lineLength(c.line()) - 1; } if (column < 0) { column = 0; } if (c.line() > doc()->lines() - 1) { c.setLine(doc()->lines() - 1); } c.setColumn(column); m_stickyColumn = -1; updateCursor(c); m_deleteCommand = true; return ret; } bool NormalViMode::commandDelete() { m_deleteCommand = true; return deleteRange(m_commandRange, getOperationMode()); } bool NormalViMode::commandDeleteToEOL() { KTextEditor::Cursor c(m_view->cursorPosition()); OperationMode m = CharWise; m_commandRange.endColumn = KateVi::EOL; switch (m_viInputModeManager->getCurrentViMode()) { case ViMode::NormalMode: m_commandRange.startLine = c.line(); m_commandRange.startColumn = c.column(); m_commandRange.endLine = c.line() + getCount() - 1; break; case ViMode::VisualMode: case ViMode::VisualLineMode: m = LineWise; break; case ViMode::VisualBlockMode: m_commandRange.normalize(); m = Block; break; default: /* InsertMode and ReplaceMode will never call this method. */ Q_ASSERT(false); } bool r = deleteRange(m_commandRange, m); switch (m) { case CharWise: c.setColumn(doc()->lineLength(c.line()) - 1); break; case LineWise: c.setLine(m_commandRange.startLine); c.setColumn(getFirstNonBlank(qMin(doc()->lastLine(), m_commandRange.startLine))); break; case Block: c.setLine(m_commandRange.startLine); c.setColumn(m_commandRange.startColumn - 1); break; } // make sure cursor position is valid after deletion if (c.line() < 0) { c.setLine(0); } if (c.line() > doc()->lastLine()) { c.setLine(doc()->lastLine()); } if (c.column() > doc()->lineLength(c.line()) - 1) { c.setColumn(doc()->lineLength(c.line()) - 1); } if (c.column() < 0) { c.setColumn(0); } updateCursor(c); m_deleteCommand = true; return r; } bool NormalViMode::commandMakeLowercase() { KTextEditor::Cursor c = m_view->cursorPosition(); OperationMode m = getOperationMode(); QString text = getRange(m_commandRange, m); if (m == LineWise) { text = text.left(text.size() - 1); // don't need '\n' at the end; } QString lowerCase = text.toLower(); m_commandRange.normalize(); KTextEditor::Cursor start(m_commandRange.startLine, m_commandRange.startColumn); KTextEditor::Cursor end(m_commandRange.endLine, m_commandRange.endColumn); KTextEditor::Range range(start, end); doc()->replaceText(range, lowerCase, m == Block); if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { updateCursor(start); } else { updateCursor(c); } return true; } bool NormalViMode::commandMakeLowercaseLine() { KTextEditor::Cursor c(m_view->cursorPosition()); if (doc()->lineLength(c.line()) == 0) { // Nothing to do. return true; } m_commandRange.startLine = c.line(); m_commandRange.endLine = c.line() + getCount() - 1; m_commandRange.startColumn = 0; m_commandRange.endColumn = doc()->lineLength(c.line()) - 1; return commandMakeLowercase(); } bool NormalViMode::commandMakeUppercase() { if (!m_commandRange.valid) { return false; } KTextEditor::Cursor c = m_view->cursorPosition(); OperationMode m = getOperationMode(); QString text = getRange(m_commandRange, m); if (m == LineWise) { text = text.left(text.size() - 1); // don't need '\n' at the end; } QString upperCase = text.toUpper(); m_commandRange.normalize(); KTextEditor::Cursor start(m_commandRange.startLine, m_commandRange.startColumn); KTextEditor::Cursor end(m_commandRange.endLine, m_commandRange.endColumn); KTextEditor::Range range(start, end); doc()->replaceText(range, upperCase, m == Block); if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { updateCursor(start); } else { updateCursor(c); } return true; } bool NormalViMode::commandMakeUppercaseLine() { KTextEditor::Cursor c(m_view->cursorPosition()); if (doc()->lineLength(c.line()) == 0) { // Nothing to do. return true; } m_commandRange.startLine = c.line(); m_commandRange.endLine = c.line() + getCount() - 1; m_commandRange.startColumn = 0; m_commandRange.endColumn = doc()->lineLength(c.line()) - 1; return commandMakeUppercase(); } bool NormalViMode::commandChangeCase() { switchView(); QString text; KTextEditor::Range range; KTextEditor::Cursor c(m_view->cursorPosition()); // in visual mode, the range is from start position to end position... if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualMode || m_viInputModeManager->getCurrentViMode() == ViMode::VisualBlockMode) { KTextEditor::Cursor c2 = m_viInputModeManager->getViVisualMode()->getStart(); if (c2 > c) { c2.setColumn(c2.column() + 1); } else { c.setColumn(c.column() + 1); } range.setRange(c, c2); // ... in visual line mode, the range is from column 0 on the first line to // the line length of the last line... } else if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualLineMode) { KTextEditor::Cursor c2 = m_viInputModeManager->getViVisualMode()->getStart(); if (c2 > c) { c2.setColumn(doc()->lineLength(c2.line())); c.setColumn(0); } else { c.setColumn(doc()->lineLength(c.line())); c2.setColumn(0); } range.setRange(c, c2); // ... and in normal mode the range is from the current position to the // current position + count } else { KTextEditor::Cursor c2 = c; c2.setColumn(c.column() + getCount()); if (c2.column() > doc()->lineLength(c.line())) { c2.setColumn(doc()->lineLength(c.line())); } range.setRange(c, c2); } bool block = m_viInputModeManager->getCurrentViMode() == ViMode::VisualBlockMode; // get the text the command should operate on text = doc()->text(range, block); // for every character, switch its case for (int i = 0; i < text.length(); i++) { if (text.at(i).isUpper()) { text[i] = text.at(i).toLower(); } else if (text.at(i).isLower()) { text[i] = text.at(i).toUpper(); } } // replace the old text with the modified text doc()->replaceText(range, text, block); // in normal mode, move the cursor to the right, in visual mode move the // cursor to the start of the selection if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { updateCursor(range.end()); } else { updateCursor(range.start()); } return true; } bool NormalViMode::commandChangeCaseRange() { OperationMode m = getOperationMode(); QString changedCase = getRange(m_commandRange, m); if (m == LineWise) { changedCase = changedCase.left(changedCase.size() - 1); // don't need '\n' at the end; } KTextEditor::Range range = KTextEditor::Range(m_commandRange.startLine, m_commandRange.startColumn, m_commandRange.endLine, m_commandRange.endColumn); // get the text the command should operate on // for every character, switch its case for (int i = 0; i < changedCase.length(); i++) { if (changedCase.at(i).isUpper()) { changedCase[i] = changedCase.at(i).toLower(); } else if (changedCase.at(i).isLower()) { changedCase[i] = changedCase.at(i).toUpper(); } } doc()->replaceText(range, changedCase, m == Block); return true; } bool NormalViMode::commandChangeCaseLine() { KTextEditor::Cursor c(m_view->cursorPosition()); if (doc()->lineLength(c.line()) == 0) { // Nothing to do. return true; } m_commandRange.startLine = c.line(); m_commandRange.endLine = c.line() + getCount() - 1; m_commandRange.startColumn = 0; m_commandRange.endColumn = doc()->lineLength(c.line()) - 1; // -1 is for excluding '\0' if (!commandChangeCaseRange()) { return false; } KTextEditor::Cursor start(m_commandRange.startLine, m_commandRange.startColumn); if (getCount() > 1) { updateCursor(c); } else { updateCursor(start); } return true; } bool NormalViMode::commandOpenNewLineUnder() { doc()->setUndoMergeAllEdits(true); KTextEditor::Cursor c(m_view->cursorPosition()); c.setColumn(doc()->lineLength(c.line())); updateCursor(c); doc()->newLine(m_view); m_stickyColumn = -1; startInsertMode(); m_viInputModeManager->getViInsertMode()->setCount(getCount()); m_viInputModeManager->getViInsertMode()->setCountedRepeatsBeginOnNewLine(true); return true; } bool NormalViMode::commandOpenNewLineOver() { doc()->setUndoMergeAllEdits(true); KTextEditor::Cursor c(m_view->cursorPosition()); if (c.line() == 0) { doc()->insertLine(0, QString()); c.setColumn(0); c.setLine(0); updateCursor(c); } else { c.setLine(c.line() - 1); c.setColumn(getLine(c.line()).length()); updateCursor(c); doc()->newLine(m_view); } m_stickyColumn = -1; startInsertMode(); m_viInputModeManager->getViInsertMode()->setCount(getCount()); m_viInputModeManager->getViInsertMode()->setCountedRepeatsBeginOnNewLine(true); return true; } bool NormalViMode::commandJoinLines() { KTextEditor::Cursor c(m_view->cursorPosition()); unsigned int from = c.line(); unsigned int to = c.line() + ((getCount() == 1) ? 1 : getCount() - 1); // if we were given a range of lines, this information overrides the previous if (m_commandRange.startLine != -1 && m_commandRange.endLine != -1) { m_commandRange.normalize(); c.setLine(m_commandRange.startLine); from = m_commandRange.startLine; to = m_commandRange.endLine; } if (to >= (unsigned int)doc()->lines()) { return false; } bool nonEmptyLineFound = false; for (unsigned int lineNum = from; lineNum <= to; lineNum++) { if (!doc()->line(lineNum).isEmpty()) { nonEmptyLineFound = true; } } const int firstNonWhitespaceOnLastLine = doc()->kateTextLine(to)->firstChar(); QString leftTrimmedLastLine; if (firstNonWhitespaceOnLastLine != -1) { leftTrimmedLastLine = doc()->line(to).mid(firstNonWhitespaceOnLastLine); } joinLines(from, to); if (nonEmptyLineFound && leftTrimmedLastLine.isEmpty()) { // joinLines won't have added a trailing " ", whereas Vim does - follow suit. doc()->insertText(KTextEditor::Cursor(from, doc()->lineLength(from)), QLatin1String(" ")); } // Position cursor just before first non-whitesspace character of what was the last line joined. c.setColumn(doc()->lineLength(from) - leftTrimmedLastLine.length() - 1); if (c.column() >= 0) { updateCursor(c); } m_deleteCommand = true; return true; } bool NormalViMode::commandChange() { KTextEditor::Cursor c(m_view->cursorPosition()); OperationMode m = getOperationMode(); doc()->setUndoMergeAllEdits(true); commandDelete(); if (m == LineWise) { // if we deleted several lines, insert an empty line and put the cursor there. doc()->insertLine(m_commandRange.startLine, QString()); c.setLine(m_commandRange.startLine); c.setColumn(0); } else if (m == Block) { // block substitute can be simulated by first deleting the text // (done above) and then starting block prepend. return commandPrependToBlock(); } else { if (m_commandRange.startLine < m_commandRange.endLine) { c.setLine(m_commandRange.startLine); } c.setColumn(m_commandRange.startColumn); } updateCursor(c); setCount(0); // The count was for the motion, not the insertion. commandEnterInsertMode(); // correct indentation level if (m == LineWise) { m_view->align(); } m_deleteCommand = true; return true; } bool NormalViMode::commandChangeToEOL() { commandDeleteToEOL(); if (getOperationMode() == Block) { return commandPrependToBlock(); } m_deleteCommand = true; return commandEnterInsertModeAppend(); } bool NormalViMode::commandChangeLine() { m_deleteCommand = true; KTextEditor::Cursor c(m_view->cursorPosition()); c.setColumn(0); updateCursor(c); doc()->setUndoMergeAllEdits(true); // if count >= 2 start by deleting the whole lines if (getCount() >= 2) { Range r(c.line(), 0, c.line() + getCount() - 2, 0, InclusiveMotion); deleteRange(r); } // ... then delete the _contents_ of the last line, but keep the line Range r(c.line(), c.column(), c.line(), doc()->lineLength(c.line()) - 1, InclusiveMotion); deleteRange(r, CharWise, true); // ... then enter insert mode if (getOperationMode() == Block) { return commandPrependToBlock(); } commandEnterInsertModeAppend(); // correct indentation level m_view->align(); return true; } bool NormalViMode::commandSubstituteChar() { if (commandDeleteChar()) { // The count is only used for deletion of chars; the inserted text is not repeated setCount(0); return commandEnterInsertMode(); } m_deleteCommand = true; return false; } bool NormalViMode::commandSubstituteLine() { m_deleteCommand = true; return commandChangeLine(); } bool NormalViMode::commandYank() { bool r = false; QString yankedText; OperationMode m = getOperationMode(); yankedText = getRange(m_commandRange, m); highlightYank(m_commandRange, m); QChar chosen_register = getChosenRegister(ZeroRegister); fillRegister(chosen_register, yankedText, m); yankToClipBoard(chosen_register, yankedText); return r; } bool NormalViMode::commandYankLine() { KTextEditor::Cursor c(m_view->cursorPosition()); QString lines; int linenum = c.line(); for (int i = 0; i < getCount(); i++) { lines.append(getLine(linenum + i) + QLatin1Char('\n')); } Range yankRange(linenum, 0, linenum + getCount() - 1, getLine(linenum + getCount() - 1).length(), InclusiveMotion); highlightYank(yankRange); QChar chosen_register = getChosenRegister(ZeroRegister); fillRegister(chosen_register, lines, LineWise); yankToClipBoard(chosen_register, lines); return true; } bool NormalViMode::commandYankToEOL() { OperationMode m = CharWise; KTextEditor::Cursor c(m_view->cursorPosition()); MotionType motion = m_commandRange.motionType; m_commandRange.endLine = c.line() + getCount() - 1; m_commandRange.endColumn = doc()->lineLength(m_commandRange.endLine) - 1; m_commandRange.motionType = InclusiveMotion; switch (m_viInputModeManager->getCurrentViMode()) { case ViMode::NormalMode: m_commandRange.startLine = c.line(); m_commandRange.startColumn = c.column(); break; case ViMode::VisualMode: case ViMode::VisualLineMode: m = LineWise; { VisualViMode *visual = static_cast(this); visual->setStart(KTextEditor::Cursor(visual->getStart().line(), 0)); } break; case ViMode::VisualBlockMode: m = Block; break; default: /* InsertMode and ReplaceMode will never call this method. */ Q_ASSERT(false); } const QString &yankedText = getRange(m_commandRange, m); m_commandRange.motionType = motion; highlightYank(m_commandRange); QChar chosen_register = getChosenRegister(ZeroRegister); fillRegister(chosen_register, yankedText, m); yankToClipBoard(chosen_register, yankedText); return true; } // Insert the text in the given register after the cursor position. // This is the non-g version of paste, so the cursor will usually // end up on the last character of the pasted text, unless the text // was multi-line or linewise in which case it will end up // on the *first* character of the pasted text(!) // If linewise, will paste after the current line. bool NormalViMode::commandPaste() { return paste(AfterCurrentPosition, false, false); } // As with commandPaste, except that the text is pasted *at* the cursor position bool NormalViMode::commandPasteBefore() { return paste(AtCurrentPosition, false, false); } // As with commandPaste, except that the cursor will generally be placed *after* the // last pasted character (assuming the last pasted character is not at the end of the line). // If linewise, cursor will be at the beginning of the line *after* the last line of pasted text, // unless that line is the last line of the document; then it will be placed at the beginning of the // last line pasted. bool NormalViMode::commandgPaste() { return paste(AfterCurrentPosition, true, false); } // As with commandgPaste, except that it pastes *at* the current cursor position or, if linewise, // at the current line. bool NormalViMode::commandgPasteBefore() { return paste(AtCurrentPosition, true, false); } bool NormalViMode::commandIndentedPaste() { return paste(AfterCurrentPosition, false, true); } bool NormalViMode::commandIndentedPasteBefore() { return paste(AtCurrentPosition, false, true); } bool NormalViMode::commandDeleteChar() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c.line(), c.column(), c.line(), c.column() + getCount(), ExclusiveMotion); if (m_commandRange.startLine != -1 && m_commandRange.startColumn != -1) { r = m_commandRange; } else { if (r.endColumn > doc()->lineLength(r.startLine)) { r.endColumn = doc()->lineLength(r.startLine); } } // should delete entire lines if in visual line mode and selection in visual block mode OperationMode m = CharWise; if (m_viInputModeManager->getCurrentViMode() == VisualLineMode) { m = LineWise; } else if (m_viInputModeManager->getCurrentViMode() == VisualBlockMode) { m = Block; } m_deleteCommand = true; return deleteRange(r, m); } bool NormalViMode::commandDeleteCharBackward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c.line(), c.column() - getCount(), c.line(), c.column(), ExclusiveMotion); if (m_commandRange.startLine != -1 && m_commandRange.startColumn != -1) { r = m_commandRange; } else { if (r.startColumn < 0) { r.startColumn = 0; } } // should delete entire lines if in visual line mode and selection in visual block mode OperationMode m = CharWise; if (m_viInputModeManager->getCurrentViMode() == VisualLineMode) { m = LineWise; } else if (m_viInputModeManager->getCurrentViMode() == VisualBlockMode) { m = Block; } m_deleteCommand = true; return deleteRange(r, m); } bool NormalViMode::commandReplaceCharacter() { QString key = KeyParser::self()->decodeKeySequence(m_keys.right(1)); // Filter out some special keys. const int keyCode = KeyParser::self()->encoded2qt(m_keys.right(1)); switch (keyCode) { case Qt::Key_Left: case Qt::Key_Right: case Qt::Key_Up: case Qt::Key_Down: case Qt::Key_Home: case Qt::Key_End: case Qt::Key_PageUp: case Qt::Key_PageDown: case Qt::Key_Delete: case Qt::Key_Insert: case Qt::Key_Backspace: case Qt::Key_CapsLock: return true; case Qt::Key_Return: case Qt::Key_Enter: key = QStringLiteral("\n"); } bool r; if (m_viInputModeManager->isAnyVisualMode()) { OperationMode m = getOperationMode(); QString text = getRange(m_commandRange, m); if (m == LineWise) { text = text.left(text.size() - 1); // don't need '\n' at the end; } text.replace(QRegExp(QLatin1String("[^\n]")), key); m_commandRange.normalize(); KTextEditor::Cursor start(m_commandRange.startLine, m_commandRange.startColumn); KTextEditor::Cursor end(m_commandRange.endLine, m_commandRange.endColumn); KTextEditor::Range range(start, end); r = doc()->replaceText(range, text, m == Block); } else { KTextEditor::Cursor c1(m_view->cursorPosition()); KTextEditor::Cursor c2(m_view->cursorPosition()); c2.setColumn(c2.column() + getCount()); if (c2.column() > doc()->lineLength(m_view->cursorPosition().line())) { return false; } r = doc()->replaceText(KTextEditor::Range(c1, c2), key.repeated(getCount())); updateCursor(c1); } return r; } bool NormalViMode::commandSwitchToCmdLine() { QString initialText; if (m_viInputModeManager->isAnyVisualMode()) { // if in visual mode, make command range == visual selection m_viInputModeManager->getViVisualMode()->saveRangeMarks(); initialText = QStringLiteral("'<,'>"); } else if (getCount() != 1) { // if a count is given, the range [current line] to [current line] + // count should be prepended to the command line initialText = QLatin1String(".,.+") + QString::number(getCount() - 1); } m_viInputModeManager->inputAdapter()->showViModeEmulatedCommandBar(); m_viInputModeManager->inputAdapter()->viModeEmulatedCommandBar()->init(EmulatedCommandBar::Command, initialText); m_commandShouldKeepSelection = true; return true; } bool NormalViMode::commandSearchBackward() { m_viInputModeManager->inputAdapter()->showViModeEmulatedCommandBar(); m_viInputModeManager->inputAdapter()->viModeEmulatedCommandBar()->init(EmulatedCommandBar::SearchBackward); return true; } bool NormalViMode::commandSearchForward() { m_viInputModeManager->inputAdapter()->showViModeEmulatedCommandBar(); m_viInputModeManager->inputAdapter()->viModeEmulatedCommandBar()->init(EmulatedCommandBar::SearchForward); return true; } bool NormalViMode::commandUndo() { // See BUG #328277 m_viInputModeManager->clearCurrentChangeLog(); if (doc()->undoCount() > 0) { const bool mapped = m_viInputModeManager->keyMapper()->isExecutingMapping(); if (mapped) doc()->editEnd(); doc()->undo(); if (mapped) doc()->editBegin(); if (m_viInputModeManager->isAnyVisualMode()) { m_viInputModeManager->getViVisualMode()->setStart(KTextEditor::Cursor(-1, -1)); m_view->clearSelection(); startNormalMode(); } return true; } return false; } bool NormalViMode::commandRedo() { if (doc()->redoCount() > 0) { const bool mapped = m_viInputModeManager->keyMapper()->isExecutingMapping(); if (mapped) doc()->editEnd(); doc()->redo(); if (mapped) doc()->editBegin(); if (m_viInputModeManager->isAnyVisualMode()) { m_viInputModeManager->getViVisualMode()->setStart(KTextEditor::Cursor(-1, -1)); m_view->clearSelection(); startNormalMode(); } return true; } return false; } bool NormalViMode::commandSetMark() { KTextEditor::Cursor c(m_view->cursorPosition()); QChar mark = m_keys.at(m_keys.size() - 1); m_viInputModeManager->marks()->setUserMark(mark, c); return true; } bool NormalViMode::commandIndentLine() { KTextEditor::Cursor c(m_view->cursorPosition()); doc()->indent(KTextEditor::Range(c.line(), 0, c.line() + getCount(), 0), 1); return true; } bool NormalViMode::commandUnindentLine() { KTextEditor::Cursor c(m_view->cursorPosition()); doc()->indent(KTextEditor::Range(c.line(), 0, c.line() + getCount(), 0), -1); return true; } bool NormalViMode::commandIndentLines() { const bool downwards = m_commandRange.startLine < m_commandRange.endLine; m_commandRange.normalize(); int line1 = m_commandRange.startLine; int line2 = m_commandRange.endLine; int col = getLine(line2).length(); doc()->indent(KTextEditor::Range(line1, 0, line2, col), getCount()); if (downwards) { updateCursor(KTextEditor::Cursor(m_commandRange.startLine, m_commandRange.startColumn)); } else { updateCursor(KTextEditor::Cursor(m_commandRange.endLine, m_commandRange.endColumn)); } return true; } bool NormalViMode::commandUnindentLines() { const bool downwards = m_commandRange.startLine < m_commandRange.endLine; m_commandRange.normalize(); int line1 = m_commandRange.startLine; int line2 = m_commandRange.endLine; doc()->indent(KTextEditor::Range(line1, 0, line2, doc()->lineLength(line2)), -getCount()); if (downwards) { updateCursor(KTextEditor::Cursor(m_commandRange.startLine, m_commandRange.startColumn)); } else { updateCursor(KTextEditor::Cursor(m_commandRange.endLine, m_commandRange.endColumn)); } return true; } bool NormalViMode::commandScrollPageDown() { if (getCount() < m_scroll_count_limit) { for (int i = 0; i < getCount(); i++) { m_view->pageDown(); } } return true; } bool NormalViMode::commandScrollPageUp() { if (getCount() < m_scroll_count_limit) { for (int i = 0; i < getCount(); i++) { m_view->pageUp(); } } return true; } bool NormalViMode::commandScrollHalfPageUp() { if (getCount() < m_scroll_count_limit) { for (int i = 0; i < getCount(); i++) { m_viewInternal->pageUp(false, true); } } return true; } bool NormalViMode::commandScrollHalfPageDown() { if (getCount() < m_scroll_count_limit) { for (int i = 0; i < getCount(); i++) { m_viewInternal->pageDown(false, true); } } return true; } bool NormalViMode::commandCenterView(bool onFirst) { KTextEditor::Cursor c(m_view->cursorPosition()); const int virtualCenterLine = m_viewInternal->startLine() + linesDisplayed() / 2; const int virtualCursorLine = m_view->textFolding().lineToVisibleLine(c.line()); scrollViewLines(virtualCursorLine - virtualCenterLine); if (onFirst) { c.setColumn(getFirstNonBlank()); updateCursor(c); } return true; } bool NormalViMode::commandCenterViewOnNonBlank() { return commandCenterView(true); } bool NormalViMode::commandCenterViewOnCursor() { return commandCenterView(false); } bool NormalViMode::commandTopView(bool onFirst) { KTextEditor::Cursor c(m_view->cursorPosition()); const int virtualCenterLine = m_viewInternal->startLine(); const int virtualCursorLine = m_view->textFolding().lineToVisibleLine(c.line()); scrollViewLines(virtualCursorLine - virtualCenterLine); if (onFirst) { c.setColumn(getFirstNonBlank()); updateCursor(c); } return true; } bool NormalViMode::commandTopViewOnNonBlank() { return commandTopView(true); } bool NormalViMode::commandTopViewOnCursor() { return commandTopView(false); } bool NormalViMode::commandBottomView(bool onFirst) { KTextEditor::Cursor c(m_view->cursorPosition()); const int virtualCenterLine = m_viewInternal->endLine(); const int virtualCursorLine = m_view->textFolding().lineToVisibleLine(c.line()); scrollViewLines(virtualCursorLine - virtualCenterLine); if (onFirst) { c.setColumn(getFirstNonBlank()); updateCursor(c); } return true; } bool NormalViMode::commandBottomViewOnNonBlank() { return commandBottomView(true); } bool NormalViMode::commandBottomViewOnCursor() { return commandBottomView(false); } bool NormalViMode::commandAbort() { m_pendingResetIsDueToExit = true; reset(); return true; } bool NormalViMode::commandPrintCharacterCode() { QChar ch = getCharUnderCursor(); if (ch == QChar::Null) { message(QStringLiteral("NUL")); } else { int code = ch.unicode(); QString dec = QString::number(code); QString hex = QString::number(code, 16); QString oct = QString::number(code, 8); if (oct.length() < 3) { oct.prepend(QLatin1Char('0')); } if (code > 0x80 && code < 0x1000) { hex.prepend((code < 0x100 ? QLatin1String("00") : QLatin1String("0"))); } message(i18n("'%1' %2, Hex %3, Octal %4", ch, dec, hex, oct)); } return true; } bool NormalViMode::commandRepeatLastChange() { const int repeatCount = getCount(); resetParser(); if (repeatCount > 1) { m_oneTimeCountOverride = repeatCount; } doc()->editStart(); m_viInputModeManager->repeatLastChange(); doc()->editEnd(); return true; } bool NormalViMode::commandAlignLine() { const int line = m_view->cursorPosition().line(); KTextEditor::Range alignRange(KTextEditor::Cursor(line, 0), KTextEditor::Cursor(line, 0)); doc()->align(m_view, alignRange); return true; } bool NormalViMode::commandAlignLines() { m_commandRange.normalize(); KTextEditor::Cursor start(m_commandRange.startLine, 0); KTextEditor::Cursor end(m_commandRange.endLine, 0); doc()->align(m_view, KTextEditor::Range(start, end)); return true; } bool NormalViMode::commandAddToNumber() { addToNumberUnderCursor(getCount()); return true; } bool NormalViMode::commandSubtractFromNumber() { addToNumberUnderCursor(-getCount()); return true; } bool NormalViMode::commandPrependToBlock() { KTextEditor::Cursor c(m_view->cursorPosition()); // move cursor to top left corner of selection m_commandRange.normalize(); c.setColumn(m_commandRange.startColumn); c.setLine(m_commandRange.startLine); updateCursor(c); m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setBlockPrependMode(m_commandRange); return startInsertMode(); } bool NormalViMode::commandAppendToBlock() { KTextEditor::Cursor c(m_view->cursorPosition()); m_commandRange.normalize(); if (m_stickyColumn == (unsigned int)KateVi::EOL) { // append to EOL // move cursor to end of first line c.setLine(m_commandRange.startLine); c.setColumn(doc()->lineLength(c.line())); updateCursor(c); m_viInputModeManager->getViInsertMode()->setBlockAppendMode(m_commandRange, AppendEOL); } else { m_viInputModeManager->getViInsertMode()->setBlockAppendMode(m_commandRange, Append); // move cursor to top right corner of selection c.setColumn(m_commandRange.endColumn + 1); c.setLine(m_commandRange.startLine); updateCursor(c); } m_stickyColumn = -1; return startInsertMode(); } bool NormalViMode::commandGoToNextJump() { KTextEditor::Cursor c = getNextJump(m_view->cursorPosition()); updateCursor(c); return true; } bool NormalViMode::commandGoToPrevJump() { KTextEditor::Cursor c = getPrevJump(m_view->cursorPosition()); updateCursor(c); return true; } bool NormalViMode::commandSwitchToLeftView() { switchView(Left); return true; } bool NormalViMode::commandSwitchToDownView() { switchView(Down); return true; } bool NormalViMode::commandSwitchToUpView() { switchView(Up); return true; } bool NormalViMode::commandSwitchToRightView() { switchView(Right); return true; } bool NormalViMode::commandSwitchToNextView() { switchView(Next); return true; } bool NormalViMode::commandSplitHoriz() { return executeKateCommand(QStringLiteral("split")); } bool NormalViMode::commandSplitVert() { return executeKateCommand(QStringLiteral("vsplit")); } bool NormalViMode::commandCloseView() { return executeKateCommand(QStringLiteral("close")); } bool NormalViMode::commandSwitchToNextTab() { QString command = QStringLiteral("bn"); if (m_iscounted) { command = command + QLatin1Char(' ') + QString::number(getCount()); } return executeKateCommand(command); } bool NormalViMode::commandSwitchToPrevTab() { QString command = QStringLiteral("bp"); if (m_iscounted) { command = command + QLatin1Char(' ') + QString::number(getCount()); } return executeKateCommand(command); } bool NormalViMode::commandFormatLine() { KTextEditor::Cursor c(m_view->cursorPosition()); reformatLines(c.line(), c.line() + getCount() - 1); return true; } bool NormalViMode::commandFormatLines() { reformatLines(m_commandRange.startLine, m_commandRange.endLine); return true; } bool NormalViMode::commandCollapseToplevelNodes() { #if 0 //FIXME FOLDING doc()->foldingTree()->collapseToplevelNodes(); #endif return true; } bool NormalViMode::commandStartRecordingMacro() { const QChar reg = m_keys[m_keys.size() - 1]; m_viInputModeManager->macroRecorder()->start(reg); return true; } bool NormalViMode::commandReplayMacro() { // "@" will have been added to the log; it needs to be cleared // *before* we replay the macro keypresses, else it can cause an infinite loop // if the macro contains a "." m_viInputModeManager->clearCurrentChangeLog(); const QChar reg = m_keys[m_keys.size() - 1]; const unsigned int count = getCount(); resetParser(); doc()->editBegin(); for (unsigned int i = 0; i < count; i++) { m_viInputModeManager->macroRecorder()->replay(reg); } doc()->editEnd(); return true; } bool NormalViMode::commandCloseNocheck() { return executeKateCommand(QStringLiteral("q!")); } bool NormalViMode::commandCloseWrite() { return executeKateCommand(QStringLiteral("wq")); } bool NormalViMode::commandCollapseLocal() { #if 0 //FIXME FOLDING KTextEditor::Cursor c(m_view->cursorPosition()); doc()->foldingTree()->collapseOne(c.line(), c.column()); #endif return true; } bool NormalViMode::commandExpandAll() { #if 0 //FIXME FOLDING doc()->foldingTree()->expandAll(); #endif return true; } bool NormalViMode::commandExpandLocal() { #if 0 //FIXME FOLDING KTextEditor::Cursor c(m_view->cursorPosition()); doc()->foldingTree()->expandOne(c.line() + 1, c.column()); #endif return true; } bool NormalViMode::commandToggleRegionVisibility() { #if 0 //FIXME FOLDING KTextEditor::Cursor c(m_view->cursorPosition()); doc()->foldingTree()->toggleRegionVisibility(c.line()); #endif return true; } //////////////////////////////////////////////////////////////////////////////// // MOTIONS //////////////////////////////////////////////////////////////////////////////// Range NormalViMode::motionDown() { return goLineDown(); } Range NormalViMode::motionUp() { return goLineUp(); } Range NormalViMode::motionLeft() { KTextEditor::Cursor cursor(m_view->cursorPosition()); m_stickyColumn = -1; Range r(cursor, ExclusiveMotion); r.endColumn -= getCount(); if (r.endColumn < 0) { r.endColumn = 0; } return r; } Range NormalViMode::motionRight() { KTextEditor::Cursor cursor(m_view->cursorPosition()); m_stickyColumn = -1; Range r(cursor, ExclusiveMotion); r.endColumn += getCount(); // make sure end position isn't > line length if (r.endColumn > doc()->lineLength(r.endLine)) { r.endColumn = doc()->lineLength(r.endLine); } return r; } Range NormalViMode::motionPageDown() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); r.endLine += linesDisplayed(); if (r.endLine >= doc()->lines()) { r.endLine = doc()->lines() - 1; } return r; } Range NormalViMode::motionPageUp() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); r.endLine -= linesDisplayed(); if (r.endLine < 0) { r.endLine = 0; } return r; } Range NormalViMode::motionHalfPageDown() { if (commandScrollHalfPageDown()) { KTextEditor::Cursor c = m_view->cursorPosition(); m_commandRange.endLine = c.line(); m_commandRange.endColumn = c.column(); return m_commandRange; } return Range::invalid(); } Range NormalViMode::motionHalfPageUp() { if (commandScrollHalfPageUp()) { KTextEditor::Cursor c = m_view->cursorPosition(); m_commandRange.endLine = c.line(); m_commandRange.endColumn = c.column(); return m_commandRange; } return Range::invalid(); } Range NormalViMode::motionDownToFirstNonBlank() { Range r = goLineDown(); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionUpToFirstNonBlank() { Range r = goLineUp(); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionWordForward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, ExclusiveMotion); m_stickyColumn = -1; // Special case: If we're already on the very last character in the document, the motion should be // inclusive so the last character gets included if (c.line() == doc()->lines() - 1 && c.column() == doc()->lineLength(c.line()) - 1) { r.motionType = InclusiveMotion; } else { for (int i = 0; i < getCount(); i++) { c = findNextWordStart(c.line(), c.column()); // stop when at the last char in the document if (!c.isValid()) { c = doc()->documentEnd(); // if we still haven't "used up the count", make the motion inclusive, so that the last char // is included if (i < getCount()) { r.motionType = InclusiveMotion; } break; } } } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionWordBackward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, ExclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findPrevWordStart(c.line(), c.column()); if (!c.isValid()) { c = KTextEditor::Cursor(0, 0); break; } } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionWORDForward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, ExclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findNextWORDStart(c.line(), c.column()); // stop when at the last char in the document if (c.line() == doc()->lines() - 1 && c.column() == doc()->lineLength(c.line()) - 1) { break; } } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionWORDBackward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, ExclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findPrevWORDStart(c.line(), c.column()); if (!c.isValid()) { c = KTextEditor::Cursor(0, 0); } } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionToEndOfWord() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findWordEnd(c.line(), c.column()); } if (!c.isValid()) { c = doc()->documentEnd(); } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionToEndOfWORD() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findWORDEnd(c.line(), c.column()); } if (!c.isValid()) { c = doc()->documentEnd(); } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionToEndOfPrevWord() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findPrevWordEnd(c.line(), c.column()); if (c.isValid()) { r.endColumn = c.column(); r.endLine = c.line(); } else { r.endColumn = 0; r.endLine = 0; break; } } return r; } Range NormalViMode::motionToEndOfPrevWORD() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findPrevWORDEnd(c.line(), c.column()); if (c.isValid()) { r.endColumn = c.column(); r.endLine = c.line(); } else { r.endColumn = 0; r.endLine = 0; break; } } return r; } Range NormalViMode::motionToEOL() { KTextEditor::Cursor c(m_view->cursorPosition()); // set sticky column to a ridiculously high value so that the cursor will stick to EOL, // but only if it's a regular motion if (m_keys.size() == 1) { m_stickyColumn = KateVi::EOL; } unsigned int line = c.line() + (getCount() - 1); Range r(line, doc()->lineLength(line) - 1, InclusiveMotion); return r; } Range NormalViMode::motionToColumn0() { m_stickyColumn = -1; KTextEditor::Cursor cursor(m_view->cursorPosition()); Range r(cursor.line(), 0, ExclusiveMotion); return r; } Range NormalViMode::motionToFirstCharacterOfLine() { m_stickyColumn = -1; KTextEditor::Cursor cursor(m_view->cursorPosition()); int c = getFirstNonBlank(); Range r(cursor.line(), c, ExclusiveMotion); return r; } Range NormalViMode::motionFindChar() { m_lastTFcommand = m_keys; KTextEditor::Cursor cursor(m_view->cursorPosition()); QString line = getLine(); m_stickyColumn = -1; int matchColumn = cursor.column(); for (int i = 0; i < getCount(); i++) { matchColumn = line.indexOf(m_keys.rightRef(1), matchColumn + 1); if (matchColumn == -1) { break; } } Range r; if (matchColumn != -1) { r.endColumn = matchColumn; r.endLine = cursor.line(); } else { return Range::invalid(); } return r; } Range NormalViMode::motionFindCharBackward() { m_lastTFcommand = m_keys; KTextEditor::Cursor cursor(m_view->cursorPosition()); QString line = getLine(); m_stickyColumn = -1; int matchColumn = -1; int hits = 0; int i = cursor.column() - 1; while (hits != getCount() && i >= 0) { if (line.at(i) == m_keys.at(m_keys.size() - 1)) { hits++; } if (hits == getCount()) { matchColumn = i; } i--; } Range r(cursor, ExclusiveMotion); if (matchColumn != -1) { r.endColumn = matchColumn; r.endLine = cursor.line(); } else { return Range::invalid(); } return r; } Range NormalViMode::motionToChar() { m_lastTFcommand = m_keys; KTextEditor::Cursor cursor(m_view->cursorPosition()); QString line = getLine(); m_stickyColumn = -1; Range r; r.endColumn = -1; r.endLine = -1; int matchColumn = cursor.column() + (m_isRepeatedTFcommand ? 2 : 1); for (int i = 0; i < getCount(); i++) { const int lastColumn = matchColumn; matchColumn = line.indexOf(m_keys.right(1), matchColumn + ((i > 0) ? 1 : 0)); if (matchColumn == -1) { if (m_isRepeatedTFcommand) { matchColumn = lastColumn; } else { return Range::invalid(); } break; } } r.endColumn = matchColumn - 1; r.endLine = cursor.line(); m_isRepeatedTFcommand = false; return r; } Range NormalViMode::motionToCharBackward() { m_lastTFcommand = m_keys; KTextEditor::Cursor cursor(m_view->cursorPosition()); QString line = getLine(); const int originalColumn = cursor.column(); m_stickyColumn = -1; int matchColumn = originalColumn - 1; int hits = 0; int i = cursor.column() - (m_isRepeatedTFcommand ? 2 : 1); Range r(cursor, ExclusiveMotion); while (hits != getCount() && i >= 0) { if (line.at(i) == m_keys.at(m_keys.size() - 1)) { hits++; } if (hits == getCount()) { matchColumn = i; } i--; } if (hits == getCount()) { r.endColumn = matchColumn + 1; r.endLine = cursor.line(); } else { r.valid = false; } m_isRepeatedTFcommand = false; return r; } Range NormalViMode::motionRepeatlastTF() { if (!m_lastTFcommand.isEmpty()) { m_isRepeatedTFcommand = true; m_keys = m_lastTFcommand; if (m_keys.at(0) == QLatin1Char('f')) { return motionFindChar(); } else if (m_keys.at(0) == QLatin1Char('F')) { return motionFindCharBackward(); } else if (m_keys.at(0) == QLatin1Char('t')) { return motionToChar(); } else if (m_keys.at(0) == QLatin1Char('T')) { return motionToCharBackward(); } } // there was no previous t/f command return Range::invalid(); } Range NormalViMode::motionRepeatlastTFBackward() { if (!m_lastTFcommand.isEmpty()) { m_isRepeatedTFcommand = true; m_keys = m_lastTFcommand; if (m_keys.at(0) == QLatin1Char('f')) { return motionFindCharBackward(); } else if (m_keys.at(0) == QLatin1Char('F')) { return motionFindChar(); } else if (m_keys.at(0) == QLatin1Char('t')) { return motionToCharBackward(); } else if (m_keys.at(0) == QLatin1Char('T')) { return motionToChar(); } } // there was no previous t/f command return Range::invalid(); } Range NormalViMode::motionToLineFirst() { Range r(getCount() - 1, 0, InclusiveMotion); m_stickyColumn = -1; if (r.endLine > doc()->lines() - 1) { r.endLine = doc()->lines() - 1; } r.jump = true; return r; } Range NormalViMode::motionToLineLast() { Range r(doc()->lines() - 1, 0, InclusiveMotion); m_stickyColumn = -1; // don't use getCount() here, no count and a count of 1 is different here... if (m_count != 0) { r.endLine = m_count - 1; } if (r.endLine > doc()->lines() - 1) { r.endLine = doc()->lines() - 1; } r.jump = true; return r; } Range NormalViMode::motionToScreenColumn() { m_stickyColumn = -1; KTextEditor::Cursor c(m_view->cursorPosition()); int column = getCount() - 1; if (doc()->lineLength(c.line()) - 1 < (int)getCount() - 1) { column = doc()->lineLength(c.line()) - 1; } return Range(c.line(), column, ExclusiveMotion); } Range NormalViMode::motionToMark() { Range r; m_stickyColumn = -1; QChar reg = m_keys.at(m_keys.size() - 1); KTextEditor::Cursor c = m_viInputModeManager->marks()->getMarkPosition(reg); if (c.isValid()) { r.endLine = c.line(); r.endColumn = c.column(); } else { error(i18n("Mark not set: %1", m_keys.right(1))); r.valid = false; } r.jump = true; return r; } Range NormalViMode::motionToMarkLine() { Range r = motionToMark(); r.endColumn = getFirstNonBlank(r.endLine); r.jump = true; m_stickyColumn = -1; return r; } Range NormalViMode::motionToMatchingItem() { Range r; int lines = doc()->lines(); // If counted, then it's not a motion to matching item anymore, // but a motion to the N'th percentage of the document if (isCounted()) { int count = getCount(); if (count > 100) { return r; } r.endLine = qRound(lines * count / 100.0) - 1; r.endColumn = 0; return r; } KTextEditor::Cursor c(m_view->cursorPosition()); QString l = getLine(); int n1 = l.indexOf(m_matchItemRegex, c.column()); m_stickyColumn = -1; if (n1 < 0) { return Range::invalid(); } QRegExp brackets(QLatin1String("[(){}\\[\\]]")); // use Kate's built-in matching bracket finder for brackets if (brackets.indexIn(l, n1) == n1) { // findMatchingBracket requires us to move the cursor to the // first bracket, but we don't want the cursor to really move // in case this is e.g. a yank, so restore it to its original // position afterwards. c.setColumn(n1 + 1); const KTextEditor::Cursor oldCursorPos = m_view->cursorPosition(); updateCursor(c); // find the matching one c = m_viewInternal->findMatchingBracket(); if (c > m_view->cursorPosition()) { c.setColumn(c.column() - 1); } m_view->setCursorPosition(oldCursorPos); } else { // text item we want to find a matching item for int n2 = l.indexOf(QRegExp(QLatin1String("\\b|\\s|$")), n1); QString item = l.mid(n1, n2 - n1); QString matchingItem = m_matchingItems[ item ]; int toFind = 1; int line = c.line(); int column = n2 - item.length(); bool reverse = false; - if (matchingItem.left(1) == QLatin1String("-")) { + if (matchingItem.startsWith(QLatin1Char('-'))) { matchingItem.remove(0, 1); // remove the '-' reverse = true; } // make sure we don't hit the text item we started the search from if (column == 0 && reverse) { column -= item.length(); } int itemIdx; int matchItemIdx; while (toFind > 0) { if (reverse) { itemIdx = l.lastIndexOf(item, column - 1); matchItemIdx = l.lastIndexOf(matchingItem, column - 1); if (itemIdx != -1 && (matchItemIdx == -1 || itemIdx > matchItemIdx)) { ++toFind; } } else { itemIdx = l.indexOf(item, column); matchItemIdx = l.indexOf(matchingItem, column); if (itemIdx != -1 && (matchItemIdx == -1 || itemIdx < matchItemIdx)) { ++toFind; } } if (matchItemIdx != -1 || itemIdx != -1) { if (!reverse) { column = qMin((unsigned int)itemIdx, (unsigned int)matchItemIdx); } else { column = qMax(itemIdx, matchItemIdx); } } if (matchItemIdx != -1) { // match on current line if (matchItemIdx == column) { --toFind; c.setLine(line); c.setColumn(column); } } else { // no match, advance one line if possible (reverse) ? --line : ++line; column = 0; if ((!reverse && line >= lines) || (reverse && line < 0)) { r.valid = false; break; } else { l = getLine(line); } } } } r.endLine = c.line(); r.endColumn = c.column(); r.jump = true; return r; } Range NormalViMode::motionToNextBraceBlockStart() { Range r; m_stickyColumn = -1; int line = findLineStartingWitchChar(QLatin1Char('{'), getCount()); if (line == -1) { return Range::invalid(); } r.endLine = line; r.endColumn = 0; r.jump = true; if (motionWillBeUsedWithCommand()) { // Delete from cursor (inclusive) to the '{' (exclusive). // If we are on the first column, then delete the entire current line. r.motionType = ExclusiveMotion; if (m_view->cursorPosition().column() != 0) { r.endLine--; r.endColumn = doc()->lineLength(r.endLine); } } return r; } Range NormalViMode::motionToPreviousBraceBlockStart() { Range r; m_stickyColumn = -1; int line = findLineStartingWitchChar(QLatin1Char('{'), getCount(), false); if (line == -1) { return Range::invalid(); } r.endLine = line; r.endColumn = 0; r.jump = true; if (motionWillBeUsedWithCommand()) { // With a command, do not include the { or the cursor position. r.motionType = ExclusiveMotion; } return r; } Range NormalViMode::motionToNextBraceBlockEnd() { Range r; m_stickyColumn = -1; int line = findLineStartingWitchChar(QLatin1Char('}'), getCount()); if (line == -1) { return Range::invalid(); } r.endLine = line; r.endColumn = 0; r.jump = true; if (motionWillBeUsedWithCommand()) { // Delete from cursor (inclusive) to the '}' (exclusive). // If we are on the first column, then delete the entire current line. r.motionType = ExclusiveMotion; if (m_view->cursorPosition().column() != 0) { r.endLine--; r.endColumn = doc()->lineLength(r.endLine); } } return r; } Range NormalViMode::motionToPreviousBraceBlockEnd() { Range r; m_stickyColumn = -1; int line = findLineStartingWitchChar(QLatin1Char('}'), getCount(), false); if (line == -1) { return Range::invalid(); } r.endLine = line; r.endColumn = 0; r.jump = true; if (motionWillBeUsedWithCommand()) { r.motionType = ExclusiveMotion; } return r; } Range NormalViMode::motionToNextOccurrence() { const QString word = getWordUnderCursor(); const Range match = m_viInputModeManager->searcher()->findWordForMotion(word, false, getWordRangeUnderCursor().start(), getCount()); return Range(match.startLine, match.startColumn, ExclusiveMotion); } Range NormalViMode::motionToPrevOccurrence() { const QString word = getWordUnderCursor(); const Range match = m_viInputModeManager->searcher()->findWordForMotion(word, true, getWordRangeUnderCursor().start(), getCount()); return Range(match.startLine, match.startColumn, ExclusiveMotion); } Range NormalViMode::motionToFirstLineOfWindow() { int lines_to_go; if (linesDisplayed() <= (unsigned int) m_viewInternal->endLine()) { lines_to_go = m_viewInternal->endLine() - linesDisplayed() - m_view->cursorPosition().line() + 1; } else { lines_to_go = - m_view->cursorPosition().line(); } Range r = goLineUpDown(lines_to_go); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionToMiddleLineOfWindow() { int lines_to_go; if (linesDisplayed() <= (unsigned int) m_viewInternal->endLine()) { lines_to_go = m_viewInternal->endLine() - linesDisplayed() / 2 - m_view->cursorPosition().line(); } else { lines_to_go = m_viewInternal->endLine() / 2 - m_view->cursorPosition().line(); } Range r = goLineUpDown(lines_to_go); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionToLastLineOfWindow() { int lines_to_go; if (linesDisplayed() <= (unsigned int) m_viewInternal->endLine()) { lines_to_go = m_viewInternal->endLine() - m_view->cursorPosition().line(); } else { lines_to_go = m_viewInternal->endLine() - m_view->cursorPosition().line(); } Range r = goLineUpDown(lines_to_go); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionToNextVisualLine() { return goVisualLineUpDown(getCount()); } Range NormalViMode::motionToPrevVisualLine() { return goVisualLineUpDown(-getCount()); } Range NormalViMode::motionToPreviousSentence() { KTextEditor::Cursor c = findSentenceStart(); int linenum = c.line(), column; const bool skipSpaces = doc()->line(linenum).isEmpty(); if (skipSpaces) { linenum--; if (linenum >= 0) { column = doc()->line(linenum).size() - 1; } } else { column = c.column(); } for (int i = linenum; i >= 0; i--) { const QString &line = doc()->line(i); if (line.isEmpty() && !skipSpaces) { return Range(i, 0, InclusiveMotion); } if (column < 0 && !line.isEmpty()) { column = line.size() - 1; } for (int j = column; j >= 0; j--) { if (skipSpaces || QStringLiteral(".?!").indexOf(line.at(j)) != -1) { c.setLine(i); c.setColumn(j); updateCursor(c); c = findSentenceStart(); return Range(c, InclusiveMotion); } } column = line.size() - 1; } return Range(0, 0, InclusiveMotion); } Range NormalViMode::motionToNextSentence() { KTextEditor::Cursor c = findSentenceEnd(); int linenum = c.line(), column = c.column() + 1; const bool skipSpaces = doc()->line(linenum).isEmpty(); for (int i = linenum; i < doc()->lines(); i++) { const QString &line = doc()->line(i); if (line.isEmpty() && !skipSpaces) { return Range(i, 0, InclusiveMotion); } for (int j = column; j < line.size(); j++) { if (!line.at(j).isSpace()) { return Range(i, j, InclusiveMotion); } } column = 0; } c = doc()->documentEnd(); return Range(c, InclusiveMotion); } Range NormalViMode::motionToBeforeParagraph() { KTextEditor::Cursor c(m_view->cursorPosition()); int line = c.line(); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { // advance at least one line, but if there are consecutive blank lines // skip them all do { line--; } while (line >= 0 && getLine(line + 1).length() == 0); while (line > 0 && getLine(line).length() != 0) { line--; } } if (line < 0) { line = 0; } Range r(line, 0, InclusiveMotion); return r; } Range NormalViMode::motionToAfterParagraph() { KTextEditor::Cursor c(m_view->cursorPosition()); int line = c.line(); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { // advance at least one line, but if there are consecutive blank lines // skip them all do { line++; } while (line <= doc()->lines() - 1 && getLine(line - 1).length() == 0); while (line < doc()->lines() - 1 && getLine(line).length() != 0) { line++; } } if (line >= doc()->lines()) { line = doc()->lines() - 1; } // if we ended up on the last line, the cursor should be placed on the last column int column = (line == doc()->lines() - 1) ? qMax(getLine(line).length() - 1, 0) : 0; return Range(line, column, InclusiveMotion); } Range NormalViMode::motionToIncrementalSearchMatch() { return Range(m_positionWhenIncrementalSearchBegan.line(), m_positionWhenIncrementalSearchBegan.column(), m_view->cursorPosition().line(), m_view->cursorPosition().column(), ExclusiveMotion); } //////////////////////////////////////////////////////////////////////////////// // TEXT OBJECTS //////////////////////////////////////////////////////////////////////////////// Range NormalViMode::textObjectAWord() { KTextEditor::Cursor c(m_view->cursorPosition()); KTextEditor::Cursor c1 = c; bool startedOnSpace = false; if (doc()->characterAt(c).isSpace()) { startedOnSpace = true; } else { c1 = findPrevWordStart(c.line(), c.column() + 1, true); if (!c1.isValid()) { c1 = KTextEditor::Cursor(0, 0); } } KTextEditor::Cursor c2 = KTextEditor::Cursor(c.line(), c.column() - 1); for (int i = 1; i <= getCount(); i++) { c2 = findWordEnd(c2.line(), c2.column()); } if (!c1.isValid() || !c2.isValid()) { return Range::invalid(); } // Adhere to some of Vim's bizarre rules of whether to swallow ensuing spaces or not. // Don't ask ;) const KTextEditor::Cursor nextWordStart = findNextWordStart(c2.line(), c2.column()); if (nextWordStart.isValid() && nextWordStart.line() == c2.line()) { if (!startedOnSpace) { c2 = KTextEditor::Cursor(nextWordStart.line(), nextWordStart.column() - 1); } } else { c2 = KTextEditor::Cursor(c2.line(), doc()->lineLength(c2.line()) - 1); } bool swallowCarriageReturnAtEndOfLine = false; if (c2.line() != c.line() && c2.column() == doc()->lineLength(c2.line()) - 1) { // Greedily descend to the next line, so as to swallow the carriage return on this line. c2 = KTextEditor::Cursor(c2.line() + 1, 0); swallowCarriageReturnAtEndOfLine = true; } const bool swallowPrecedingSpaces = (c2.column() == doc()->lineLength(c2.line()) - 1 && !doc()->characterAt(c2).isSpace()) || startedOnSpace || swallowCarriageReturnAtEndOfLine; if (swallowPrecedingSpaces) { if (c1.column() != 0) { const KTextEditor::Cursor previousNonSpace = findPrevWordEnd(c.line(), c.column()); if (previousNonSpace.isValid() && previousNonSpace.line() == c1.line()) { c1 = KTextEditor::Cursor(previousNonSpace.line(), previousNonSpace.column() + 1); } else if (startedOnSpace || swallowCarriageReturnAtEndOfLine) { c1 = KTextEditor::Cursor(c1.line(), 0); } } } return Range(c1, c2, !swallowCarriageReturnAtEndOfLine ? InclusiveMotion : ExclusiveMotion); } Range NormalViMode::textObjectInnerWord() { KTextEditor::Cursor c(m_view->cursorPosition()); KTextEditor::Cursor c1 = findPrevWordStart(c.line(), c.column() + 1, true); if (!c1.isValid()) { c1 = KTextEditor::Cursor(0, 0); } // need to start search in column-1 because it might be a one-character word KTextEditor::Cursor c2(c.line(), c.column() - 1); for (int i = 0; i < getCount(); i++) { c2 = findWordEnd(c2.line(), c2.column(), true); } if (!c2.isValid()) { c2 = doc()->documentEnd(); } // sanity check if (c1.line() != c2.line() || c1.column() > c2.column()) { return Range::invalid(); } return Range(c1, c2, InclusiveMotion); } Range NormalViMode::textObjectAWORD() { KTextEditor::Cursor c(m_view->cursorPosition()); KTextEditor::Cursor c1 = c; bool startedOnSpace = false; if (doc()->characterAt(c).isSpace()) { startedOnSpace = true; } else { c1 = findPrevWORDStart(c.line(), c.column() + 1, true); if (!c1.isValid()) { c1 = KTextEditor::Cursor(0, 0); } } KTextEditor::Cursor c2 = KTextEditor::Cursor(c.line(), c.column() - 1); for (int i = 1; i <= getCount(); i++) { c2 = findWORDEnd(c2.line(), c2.column()); } if (!c1.isValid() || !c2.isValid()) { return Range::invalid(); } // Adhere to some of Vim's bizarre rules of whether to swallow ensuing spaces or not. // Don't ask ;) const KTextEditor::Cursor nextWordStart = findNextWordStart(c2.line(), c2.column()); if (nextWordStart.isValid() && nextWordStart.line() == c2.line()) { if (!startedOnSpace) { c2 = KTextEditor::Cursor(nextWordStart.line(), nextWordStart.column() - 1); } } else { c2 = KTextEditor::Cursor(c2.line(), doc()->lineLength(c2.line()) - 1); } bool swallowCarriageReturnAtEndOfLine = false; if (c2.line() != c.line() && c2.column() == doc()->lineLength(c2.line()) - 1) { // Greedily descend to the next line, so as to swallow the carriage return on this line. c2 = KTextEditor::Cursor(c2.line() + 1, 0); swallowCarriageReturnAtEndOfLine = true; } const bool swallowPrecedingSpaces = (c2.column() == doc()->lineLength(c2.line()) - 1 && !doc()->characterAt(c2).isSpace()) || startedOnSpace || swallowCarriageReturnAtEndOfLine; if (swallowPrecedingSpaces) { if (c1.column() != 0) { const KTextEditor::Cursor previousNonSpace = findPrevWORDEnd(c.line(), c.column()); if (previousNonSpace.isValid() && previousNonSpace.line() == c1.line()) { c1 = KTextEditor::Cursor(previousNonSpace.line(), previousNonSpace.column() + 1); } else if (startedOnSpace || swallowCarriageReturnAtEndOfLine) { c1 = KTextEditor::Cursor(c1.line(), 0); } } } return Range(c1, c2, !swallowCarriageReturnAtEndOfLine ? InclusiveMotion : ExclusiveMotion); } Range NormalViMode::textObjectInnerWORD() { KTextEditor::Cursor c(m_view->cursorPosition()); KTextEditor::Cursor c1 = findPrevWORDStart(c.line(), c.column() + 1, true); if (!c1.isValid()) { c1 = KTextEditor::Cursor(0, 0); } KTextEditor::Cursor c2(c); for (int i = 0; i < getCount(); i++) { c2 = findWORDEnd(c2.line(), c2.column(), true); } if (!c2.isValid()) { c2 = doc()->documentEnd(); } // sanity check if (c1.line() != c2.line() || c1.column() > c2.column()) { return Range::invalid(); } return Range(c1, c2, InclusiveMotion); } KTextEditor::Cursor NormalViMode::findSentenceStart() { KTextEditor::Cursor c(m_view->cursorPosition()); int linenum = c.line(), column = c.column(); int prev = column; for (int i = linenum; i >= 0; i--) { const QString &line = doc()->line(i); if (i != linenum) { column = line.size() - 1; } // An empty line is the end of a paragraph. if (line.isEmpty()) { return KTextEditor::Cursor((i != linenum) ? i + 1 : i, prev); } prev = column; for (int j = column; j >= 0; j--) { if (line.at(j).isSpace()) { int lastSpace = j--; for (; j >= 0 && QStringLiteral("\"')]").indexOf(line.at(j)) != -1; j--); if (j >= 0 && QStringLiteral(".!?").indexOf(line.at(j)) != -1) { return KTextEditor::Cursor(i, prev); } j = lastSpace; } else prev = j; } } return KTextEditor::Cursor(0, 0); } KTextEditor::Cursor NormalViMode::findSentenceEnd() { KTextEditor::Cursor c(m_view->cursorPosition()); int linenum = c.line(), column = c.column(); int j = 0, prev = 0; for (int i = linenum; i < doc()->lines(); i++) { const QString &line = doc()->line(i); // An empty line is the end of a paragraph. if (line.isEmpty()) { return KTextEditor::Cursor(linenum, j); } // Iterating over the line to reach any '.', '!', '?' for (j = column; j < line.size(); j++) { if (QStringLiteral(".!?").indexOf(line.at(j)) != -1) { prev = j++; // Skip possible closing characters. for (; j < line.size() && QString::fromLatin1("\"')]").indexOf(line.at(j)) != -1; j++); if (j >= line.size()) { return KTextEditor::Cursor(i, j - 1); } // And hopefully we're done... if (line.at(j).isSpace()) { return KTextEditor::Cursor(i, j - 1); } j = prev; } } linenum = i; prev = column; column = 0; } return KTextEditor::Cursor(linenum, j - 1); } KTextEditor::Cursor NormalViMode::findParagraphStart() { KTextEditor::Cursor c(m_view->cursorPosition()); const bool firstBlank = doc()->line(c.line()).isEmpty(); int prev = c.line(); for (int i = prev; i >= 0; i--) { if (doc()->line(i).isEmpty()) { if (i != prev) { prev = i + 1; } /* Skip consecutive empty lines. */ if (firstBlank) { i--; for (; i >= 0 && doc()->line(i).isEmpty(); i--, prev--); } return KTextEditor::Cursor(prev, 0); } } return KTextEditor::Cursor(0, 0); } KTextEditor::Cursor NormalViMode::findParagraphEnd() { KTextEditor::Cursor c(m_view->cursorPosition()); int prev = c.line(), lines = doc()->lines(); const bool firstBlank = doc()->line(prev).isEmpty(); for (int i = prev; i < lines; i++) { if (doc()->line(i).isEmpty()) { if (i != prev) { prev = i - 1; } /* Skip consecutive empty lines. */ if (firstBlank) { i++; for (; i < lines && doc()->line(i).isEmpty(); i++, prev++); } int length = doc()->lineLength(prev); return KTextEditor::Cursor(prev, (length <= 0) ? 0 : length - 1); } } return doc()->documentEnd(); } Range NormalViMode::textObjectInnerSentence() { Range r; KTextEditor::Cursor c1 = findSentenceStart(); KTextEditor::Cursor c2 = findSentenceEnd(); updateCursor(c1); r.startLine = c1.line(); r.startColumn = c1.column(); r.endLine = c2.line(); r.endColumn = c2.column(); return r; } Range NormalViMode::textObjectASentence() { int i; Range r = textObjectInnerSentence(); const QString &line = doc()->line(r.endLine); // Skip whitespaces and tabs. for (i = r.endColumn + 1; i < line.size(); i++) { if (!line.at(i).isSpace()) { break; } } r.endColumn = i - 1; // Remove preceding spaces. if (r.startColumn != 0) { if (r.endColumn == line.size() - 1 && !line.at(r.endColumn).isSpace()) { const QString &line = doc()->line(r.startLine); for (i = r.startColumn - 1; i >= 0; i--) { if (!line.at(i).isSpace()) { break; } } r.startColumn = i + 1; } } return r; } Range NormalViMode::textObjectInnerParagraph() { Range r; KTextEditor::Cursor c1 = findParagraphStart(); KTextEditor::Cursor c2 = findParagraphEnd(); updateCursor(c1); r.startLine = c1.line(); r.startColumn = c1.column(); r.endLine = c2.line(); r.endColumn = c2.column(); return r; } Range NormalViMode::textObjectAParagraph() { Range r = textObjectInnerParagraph(); int lines = doc()->lines(); if (r.endLine + 1 < lines) { // If the next line is empty, remove all subsequent empty lines. // Otherwise we'll grab the next paragraph. if (doc()->line(r.endLine + 1).isEmpty()) { for (int i = r.endLine + 1; i < lines && doc()->line(i).isEmpty(); i++) { r.endLine++; } r.endColumn = 0; } else { KTextEditor::Cursor prev = m_view->cursorPosition(); KTextEditor::Cursor c(r.endLine + 1, 0); updateCursor(c); c = findParagraphEnd(); updateCursor(prev); r.endLine = c.line(); r.endColumn = c.column(); } } else if (doc()->lineLength(r.startLine) > 0) { // We went too far, but maybe we can grab previous empty lines. for (int i = r.startLine - 1; i >= 0 && doc()->line(i).isEmpty(); i--) { r.startLine--; } r.startColumn = 0; updateCursor(KTextEditor::Cursor(r.startLine, r.startColumn)); } else { // We went too far and we're on empty lines, do nothing. return Range::invalid(); } return r; } Range NormalViMode::textObjectAQuoteDouble() { return findSurroundingQuotes(QLatin1Char('"'), false); } Range NormalViMode::textObjectInnerQuoteDouble() { return findSurroundingQuotes(QLatin1Char('"'), true); } Range NormalViMode::textObjectAQuoteSingle() { return findSurroundingQuotes(QLatin1Char('\''), false); } Range NormalViMode::textObjectInnerQuoteSingle() { return findSurroundingQuotes(QLatin1Char('\''), true); } Range NormalViMode::textObjectABackQuote() { return findSurroundingQuotes(QLatin1Char('`'), false); } Range NormalViMode::textObjectInnerBackQuote() { return findSurroundingQuotes(QLatin1Char('`'), true); } Range NormalViMode::textObjectAParen() { return findSurroundingBrackets(QLatin1Char('('), QLatin1Char(')'), false, QLatin1Char('('), QLatin1Char(')')); } Range NormalViMode::textObjectInnerParen() { return findSurroundingBrackets(QLatin1Char('('), QLatin1Char(')'), true, QLatin1Char('('), QLatin1Char(')')); } Range NormalViMode::textObjectABracket() { return findSurroundingBrackets(QLatin1Char('['), QLatin1Char(']'), false, QLatin1Char('['), QLatin1Char(']')); } Range NormalViMode::textObjectInnerBracket() { return findSurroundingBrackets(QLatin1Char('['), QLatin1Char(']'), true, QLatin1Char('['), QLatin1Char(']')); } Range NormalViMode::textObjectACurlyBracket() { return findSurroundingBrackets(QLatin1Char('{'), QLatin1Char('}'), false, QLatin1Char('{'), QLatin1Char('}')); } Range NormalViMode::textObjectInnerCurlyBracket() { const Range allBetweenCurlyBrackets = findSurroundingBrackets(QLatin1Char('{'), QLatin1Char('}'), true, QLatin1Char('{'), QLatin1Char('}')); // Emulate the behaviour of vim, which tries to leave the closing bracket on its own line // if it was originally on a line different to that of the opening bracket. Range innerCurlyBracket(allBetweenCurlyBrackets); if (innerCurlyBracket.startLine != innerCurlyBracket.endLine) { const bool openingBraceIsLastCharOnLine = innerCurlyBracket.startColumn == doc()->line(innerCurlyBracket.startLine).length(); const bool stuffToDeleteIsAllOnEndLine = openingBraceIsLastCharOnLine && innerCurlyBracket.endLine == innerCurlyBracket.startLine + 1; const QString textLeadingClosingBracket = doc()->line(innerCurlyBracket.endLine).mid(0, innerCurlyBracket.endColumn + 1); const bool closingBracketHasLeadingNonWhitespace = !textLeadingClosingBracket.trimmed().isEmpty(); if (stuffToDeleteIsAllOnEndLine) { if (!closingBracketHasLeadingNonWhitespace) { // Nothing there to select - abort. return Range::invalid(); } else { // Shift the beginning of the range to the start of the line containing the closing bracket. innerCurlyBracket.startLine++; innerCurlyBracket.startColumn = 0; } } else { if (openingBraceIsLastCharOnLine && !closingBracketHasLeadingNonWhitespace) { innerCurlyBracket.startLine++; innerCurlyBracket.startColumn = 0; m_lastMotionWasLinewiseInnerBlock = true; } { // The line containing the end bracket is left alone if the end bracket is preceded by just whitespace, // else we need to delete everything (i.e. end up with "{}") if (!closingBracketHasLeadingNonWhitespace) { // Shrink the endpoint of the range so that it ends at the end of the line above, // leaving the closing bracket on its own line. innerCurlyBracket.endLine--; innerCurlyBracket.endColumn = doc()->line(innerCurlyBracket.endLine).length(); } } } } return innerCurlyBracket; } Range NormalViMode::textObjectAInequalitySign() { return findSurroundingBrackets(QLatin1Char('<'), QLatin1Char('>'), false, QLatin1Char('<'), QLatin1Char('>')); } Range NormalViMode::textObjectInnerInequalitySign() { return findSurroundingBrackets(QLatin1Char('<'), QLatin1Char('>'), true, QLatin1Char('<'), QLatin1Char('>')); } Range NormalViMode::textObjectAComma() { return textObjectComma(false); } Range NormalViMode::textObjectInnerComma() { return textObjectComma(true); } // add commands // when adding commands here, remember to add them to visual mode too (if applicable) void NormalViMode::initializeCommands() { ADDCMD("a", commandEnterInsertModeAppend, IS_CHANGE); ADDCMD("A", commandEnterInsertModeAppendEOL, IS_CHANGE); ADDCMD("i", commandEnterInsertMode, IS_CHANGE); ADDCMD("", commandEnterInsertMode, IS_CHANGE); ADDCMD("I", commandEnterInsertModeBeforeFirstNonBlankInLine, IS_CHANGE); ADDCMD("gi", commandEnterInsertModeLast, IS_CHANGE); ADDCMD("v", commandEnterVisualMode, 0); ADDCMD("V", commandEnterVisualLineMode, 0); ADDCMD("", commandEnterVisualBlockMode, 0); ADDCMD("gv", commandReselectVisual, SHOULD_NOT_RESET); ADDCMD("o", commandOpenNewLineUnder, IS_CHANGE); ADDCMD("O", commandOpenNewLineOver, IS_CHANGE); ADDCMD("J", commandJoinLines, IS_CHANGE); ADDCMD("c", commandChange, IS_CHANGE | NEEDS_MOTION); ADDCMD("C", commandChangeToEOL, IS_CHANGE); ADDCMD("cc", commandChangeLine, IS_CHANGE); ADDCMD("s", commandSubstituteChar, IS_CHANGE); ADDCMD("S", commandSubstituteLine, IS_CHANGE); ADDCMD("dd", commandDeleteLine, IS_CHANGE); ADDCMD("d", commandDelete, IS_CHANGE | NEEDS_MOTION); ADDCMD("D", commandDeleteToEOL, IS_CHANGE); ADDCMD("x", commandDeleteChar, IS_CHANGE); ADDCMD("", commandDeleteChar, IS_CHANGE); ADDCMD("X", commandDeleteCharBackward, IS_CHANGE); ADDCMD("gu", commandMakeLowercase, IS_CHANGE | NEEDS_MOTION); ADDCMD("guu", commandMakeLowercaseLine, IS_CHANGE); ADDCMD("gU", commandMakeUppercase, IS_CHANGE | NEEDS_MOTION); ADDCMD("gUU", commandMakeUppercaseLine, IS_CHANGE); ADDCMD("y", commandYank, NEEDS_MOTION); ADDCMD("yy", commandYankLine, 0); ADDCMD("Y", commandYankToEOL, 0); ADDCMD("p", commandPaste, IS_CHANGE); ADDCMD("P", commandPasteBefore, IS_CHANGE); ADDCMD("gp", commandgPaste, IS_CHANGE); ADDCMD("gP", commandgPasteBefore, IS_CHANGE); ADDCMD("]p", commandIndentedPaste, IS_CHANGE); ADDCMD("[p", commandIndentedPasteBefore, IS_CHANGE); ADDCMD("r.", commandReplaceCharacter, IS_CHANGE | REGEX_PATTERN); ADDCMD("R", commandEnterReplaceMode, IS_CHANGE); ADDCMD(":", commandSwitchToCmdLine, 0); ADDCMD("u", commandUndo, 0); ADDCMD("", commandRedo, 0); ADDCMD("U", commandRedo, 0); ADDCMD("m.", commandSetMark, REGEX_PATTERN); ADDCMD(">>", commandIndentLine, IS_CHANGE); ADDCMD("<<", commandUnindentLine, IS_CHANGE); ADDCMD(">", commandIndentLines, IS_CHANGE | NEEDS_MOTION); ADDCMD("<", commandUnindentLines, IS_CHANGE | NEEDS_MOTION); ADDCMD("", commandScrollPageDown, 0); ADDCMD("", commandScrollPageDown, 0); ADDCMD("", commandScrollPageUp, 0); ADDCMD("", commandScrollPageUp, 0); ADDCMD("", commandScrollHalfPageUp, 0); ADDCMD("", commandScrollHalfPageDown, 0); ADDCMD("z.", commandCenterViewOnNonBlank, 0); ADDCMD("zz", commandCenterViewOnCursor, 0); ADDCMD("z", commandTopViewOnNonBlank, 0); ADDCMD("zt", commandTopViewOnCursor, 0); ADDCMD("z-", commandBottomViewOnNonBlank, 0); ADDCMD("zb", commandBottomViewOnCursor, 0); ADDCMD("ga", commandPrintCharacterCode, SHOULD_NOT_RESET); ADDCMD(".", commandRepeatLastChange, 0); ADDCMD("==", commandAlignLine, IS_CHANGE); ADDCMD("=", commandAlignLines, IS_CHANGE | NEEDS_MOTION); ADDCMD("~", commandChangeCase, IS_CHANGE); ADDCMD("g~", commandChangeCaseRange, IS_CHANGE | NEEDS_MOTION); ADDCMD("g~~", commandChangeCaseLine, IS_CHANGE); ADDCMD("", commandAddToNumber, IS_CHANGE); ADDCMD("", commandSubtractFromNumber, IS_CHANGE); ADDCMD("", commandGoToPrevJump, 0); ADDCMD("", commandGoToNextJump, 0); ADDCMD("h", commandSwitchToLeftView, 0); ADDCMD("", commandSwitchToLeftView, 0); ADDCMD("", commandSwitchToLeftView, 0); ADDCMD("j", commandSwitchToDownView, 0); ADDCMD("", commandSwitchToDownView, 0); ADDCMD("", commandSwitchToDownView, 0); ADDCMD("k", commandSwitchToUpView, 0); ADDCMD("", commandSwitchToUpView, 0); ADDCMD("", commandSwitchToUpView, 0); ADDCMD("l", commandSwitchToRightView, 0); ADDCMD("", commandSwitchToRightView, 0); ADDCMD("", commandSwitchToRightView, 0); ADDCMD("w", commandSwitchToNextView, 0); ADDCMD("", commandSwitchToNextView, 0); ADDCMD("s", commandSplitHoriz, 0); ADDCMD("S", commandSplitHoriz, 0); ADDCMD("", commandSplitHoriz, 0); ADDCMD("v", commandSplitVert, 0); ADDCMD("", commandSplitVert, 0); ADDCMD("c", commandCloseView, 0); ADDCMD("gt", commandSwitchToNextTab, 0); ADDCMD("gT", commandSwitchToPrevTab, 0); ADDCMD("gqq", commandFormatLine, IS_CHANGE); ADDCMD("gq", commandFormatLines, IS_CHANGE | NEEDS_MOTION); ADDCMD("zo", commandExpandLocal, 0); ADDCMD("zc", commandCollapseLocal, 0); ADDCMD("za", commandToggleRegionVisibility, 0); ADDCMD("zr", commandExpandAll, 0); ADDCMD("zm", commandCollapseToplevelNodes, 0); ADDCMD("q.", commandStartRecordingMacro, REGEX_PATTERN); ADDCMD("@.", commandReplayMacro, REGEX_PATTERN); ADDCMD("ZZ", commandCloseWrite, 0); ADDCMD("ZQ", commandCloseNocheck, 0); // regular motions ADDMOTION("h", motionLeft, 0); ADDMOTION("", motionLeft, 0); ADDMOTION("", motionLeft, 0); ADDMOTION("j", motionDown, 0); ADDMOTION("", motionDown, 0); ADDMOTION("", motionDownToFirstNonBlank, 0); ADDMOTION("", motionDownToFirstNonBlank, 0); ADDMOTION("k", motionUp, 0); ADDMOTION("", motionUp, 0); ADDMOTION("-", motionUpToFirstNonBlank, 0); ADDMOTION("l", motionRight, 0); ADDMOTION("", motionRight, 0); ADDMOTION(" ", motionRight, 0); ADDMOTION("$", motionToEOL, 0); ADDMOTION("", motionToEOL, 0); ADDMOTION("0", motionToColumn0, 0); ADDMOTION("", motionToColumn0, 0); ADDMOTION("^", motionToFirstCharacterOfLine, 0); ADDMOTION("f.", motionFindChar, REGEX_PATTERN); ADDMOTION("F.", motionFindCharBackward, REGEX_PATTERN); ADDMOTION("t.", motionToChar, REGEX_PATTERN); ADDMOTION("T.", motionToCharBackward, REGEX_PATTERN); ADDMOTION(";", motionRepeatlastTF, 0); ADDMOTION(",", motionRepeatlastTFBackward, 0); ADDMOTION("n", motionFindNext, 0); ADDMOTION("N", motionFindPrev, 0); ADDMOTION("gg", motionToLineFirst, 0); ADDMOTION("G", motionToLineLast, 0); ADDMOTION("w", motionWordForward, IS_NOT_LINEWISE); ADDMOTION("W", motionWORDForward, IS_NOT_LINEWISE); ADDMOTION("", motionWordForward, IS_NOT_LINEWISE); ADDMOTION("", motionWordBackward, IS_NOT_LINEWISE); ADDMOTION("b", motionWordBackward, 0); ADDMOTION("B", motionWORDBackward, 0); ADDMOTION("e", motionToEndOfWord, 0); ADDMOTION("E", motionToEndOfWORD, 0); ADDMOTION("ge", motionToEndOfPrevWord, 0); ADDMOTION("gE", motionToEndOfPrevWORD, 0); ADDMOTION("|", motionToScreenColumn, 0); ADDMOTION("%", motionToMatchingItem, IS_NOT_LINEWISE); ADDMOTION("`[a-zA-Z^><\\.\\[\\]]", motionToMark, REGEX_PATTERN); ADDMOTION("'[a-zA-Z^><]", motionToMarkLine, REGEX_PATTERN); ADDMOTION("[[", motionToPreviousBraceBlockStart, IS_NOT_LINEWISE); ADDMOTION("]]", motionToNextBraceBlockStart, IS_NOT_LINEWISE); ADDMOTION("[]", motionToPreviousBraceBlockEnd, IS_NOT_LINEWISE); ADDMOTION("][", motionToNextBraceBlockEnd, IS_NOT_LINEWISE); ADDMOTION("*", motionToNextOccurrence, 0); ADDMOTION("#", motionToPrevOccurrence, 0); ADDMOTION("H", motionToFirstLineOfWindow, 0); ADDMOTION("M", motionToMiddleLineOfWindow, 0); ADDMOTION("L", motionToLastLineOfWindow, 0); ADDMOTION("gj", motionToNextVisualLine, 0); ADDMOTION("gk", motionToPrevVisualLine, 0); ADDMOTION("(", motionToPreviousSentence, 0 ); ADDMOTION(")", motionToNextSentence, 0 ); ADDMOTION("{", motionToBeforeParagraph, 0); ADDMOTION("}", motionToAfterParagraph, 0); // text objects ADDMOTION("iw", textObjectInnerWord, 0); ADDMOTION("aw", textObjectAWord, IS_NOT_LINEWISE); ADDMOTION("iW", textObjectInnerWORD, 0); ADDMOTION("aW", textObjectAWORD, IS_NOT_LINEWISE); ADDMOTION("is", textObjectInnerSentence, IS_NOT_LINEWISE ); ADDMOTION("as", textObjectASentence, IS_NOT_LINEWISE ); ADDMOTION("ip", textObjectInnerParagraph, IS_NOT_LINEWISE ); ADDMOTION("ap", textObjectAParagraph, IS_NOT_LINEWISE ); ADDMOTION("i\"", textObjectInnerQuoteDouble, IS_NOT_LINEWISE); ADDMOTION("a\"", textObjectAQuoteDouble, IS_NOT_LINEWISE); ADDMOTION("i'", textObjectInnerQuoteSingle, IS_NOT_LINEWISE); ADDMOTION("a'", textObjectAQuoteSingle, IS_NOT_LINEWISE); ADDMOTION("i`", textObjectInnerBackQuote, IS_NOT_LINEWISE); ADDMOTION("a`", textObjectABackQuote, IS_NOT_LINEWISE); ADDMOTION("i[()b]", textObjectInnerParen, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("a[()b]", textObjectAParen, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("i[{}B]", textObjectInnerCurlyBracket, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("a[{}B]", textObjectACurlyBracket, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("i[><]", textObjectInnerInequalitySign, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("a[><]", textObjectAInequalitySign, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("i[\\[\\]]", textObjectInnerBracket, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("a[\\[\\]]", textObjectABracket, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("i,", textObjectInnerComma, IS_NOT_LINEWISE); ADDMOTION("a,", textObjectAComma, IS_NOT_LINEWISE); ADDMOTION("/", motionToIncrementalSearchMatch, IS_NOT_LINEWISE); ADDMOTION("?", motionToIncrementalSearchMatch, IS_NOT_LINEWISE); } QRegExp NormalViMode::generateMatchingItemRegex() const { QString pattern(QStringLiteral("\\[|\\]|\\{|\\}|\\(|\\)|")); QList keys = m_matchingItems.keys(); for (int i = 0; i < keys.size(); i++) { QString s = m_matchingItems[ keys[ i ] ]; s.replace(QRegExp(QLatin1String("^-")), QChar()); s.replace(QRegExp(QLatin1String("\\*")), QStringLiteral("\\*")); s.replace(QRegExp(QLatin1String("\\+")), QStringLiteral("\\+")); s.replace(QRegExp(QLatin1String("\\[")), QStringLiteral("\\[")); s.replace(QRegExp(QLatin1String("\\]")), QStringLiteral("\\]")); s.replace(QRegExp(QLatin1String("\\(")), QStringLiteral("\\(")); s.replace(QRegExp(QLatin1String("\\)")), QStringLiteral("\\)")); s.replace(QRegExp(QLatin1String("\\{")), QStringLiteral("\\{")); s.replace(QRegExp(QLatin1String("\\}")), QStringLiteral("\\}")); pattern.append(s); if (i != keys.size() - 1) { pattern.append(QLatin1Char('|')); } } return QRegExp(pattern); } // returns the operation mode that should be used. this is decided by using the following heuristic: // 1. if we're in visual block mode, it should be Block // 2. if we're in visual line mode OR the range spans several lines, it should be LineWise // 3. if neither of these is true, CharWise is returned // 4. there are some motion that makes all operator charwise, if we have one of them mode will be CharWise OperationMode NormalViMode::getOperationMode() const { OperationMode m = CharWise; if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualBlockMode) { m = Block; } else if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualLineMode || (m_commandRange.startLine != m_commandRange.endLine && m_viInputModeManager->getCurrentViMode() != ViMode::VisualMode)) { m = LineWise; } if (m_commandWithMotion && !m_linewiseCommand) { m = CharWise; } if (m_lastMotionWasLinewiseInnerBlock) { m = LineWise; } return m; } bool NormalViMode::paste(PasteLocation pasteLocation, bool isgPaste, bool isIndentedPaste) { KTextEditor::Cursor pasteAt(m_view->cursorPosition()); KTextEditor::Cursor cursorAfterPaste = pasteAt; QChar reg = getChosenRegister(UnnamedRegister); OperationMode m = getRegisterFlag(reg); QString textToInsert = getRegisterContent(reg); const bool isTextMultiLine = textToInsert.count(QLatin1Char('\n')) > 0; // In temporary normal mode, p/P act as gp/gP. isgPaste |= m_viInputModeManager->getTemporaryNormalMode(); if (textToInsert.isEmpty()) { error(i18n("Nothing in register %1", reg)); return false; } if (getCount() > 1) { textToInsert = textToInsert.repeated(getCount()); // FIXME: does this make sense for blocks? } if (m == LineWise) { pasteAt.setColumn(0); if (isIndentedPaste) { // Note that this does indeed work if there is no non-whitespace on the current line or if // the line is empty! const QString leadingWhiteSpaceOnCurrentLine = doc()->line(pasteAt.line()).mid(0, doc()->line(pasteAt.line()).indexOf(QRegExp(QLatin1String("[^\\s]")))); const QString leadingWhiteSpaceOnFirstPastedLine = textToInsert.mid(0, textToInsert.indexOf(QRegExp(QLatin1String("[^\\s]")))); // QString has no "left trim" method, bizarrely. while (textToInsert[0].isSpace()) { textToInsert = textToInsert.mid(1); } textToInsert.prepend(leadingWhiteSpaceOnCurrentLine); // Remove the last \n, temporarily: we're going to alter the indentation of each pasted line // by doing a search and replace on '\n's, but don't want to alter this one. textToInsert.chop(1); textToInsert.replace(QLatin1Char('\n') + leadingWhiteSpaceOnFirstPastedLine, QLatin1Char('\n') + leadingWhiteSpaceOnCurrentLine); textToInsert.append(QLatin1Char('\n')); // Re-add the temporarily removed last '\n'. } if (pasteLocation == AfterCurrentPosition) { textToInsert.chop(1); // remove the last \n pasteAt.setColumn(doc()->lineLength(pasteAt.line())); // paste after the current line and ... textToInsert.prepend(QLatin1Char('\n')); // ... prepend a \n, so the text starts on a new line cursorAfterPaste.setLine(cursorAfterPaste.line() + 1); } if (isgPaste) { cursorAfterPaste.setLine(cursorAfterPaste.line() + textToInsert.count(QLatin1Char('\n'))); } } else { if (pasteLocation == AfterCurrentPosition) { // Move cursor forward one before we paste. The position after the paste must also // be updated accordingly. if (getLine(pasteAt.line()).length() > 0) { pasteAt.setColumn(pasteAt.column() + 1); } cursorAfterPaste = pasteAt; } const bool leaveCursorAtStartOfPaste = isTextMultiLine && !isgPaste; if (!leaveCursorAtStartOfPaste) { cursorAfterPaste = cursorPosAtEndOfPaste(pasteAt, textToInsert); if (!isgPaste) { cursorAfterPaste.setColumn(cursorAfterPaste.column() - 1); } } } doc()->editBegin(); if (m_view->selection()) { pasteAt = m_view->selectionRange().start(); doc()->removeText(m_view->selectionRange()); } doc()->insertText(pasteAt, textToInsert, m == Block); doc()->editEnd(); if (cursorAfterPaste.line() >= doc()->lines()) { cursorAfterPaste.setLine(doc()->lines() - 1); } updateCursor(cursorAfterPaste); return true; } KTextEditor::Cursor NormalViMode::cursorPosAtEndOfPaste(const KTextEditor::Cursor &pasteLocation, const QString &pastedText) const { KTextEditor::Cursor cAfter = pasteLocation; const QStringList textLines = pastedText.split(QLatin1Char('\n')); if (textLines.length() == 1) { cAfter.setColumn(cAfter.column() + pastedText.length()); } else { cAfter.setColumn(textLines.last().length() - 0); cAfter.setLine(cAfter.line() + textLines.length() - 1); } return cAfter; } void NormalViMode::joinLines(unsigned int from, unsigned int to) const { // make sure we don't try to join lines past the document end if (to >= (unsigned int)(doc()->lines())) { to = doc()->lines() - 1; } // joining one line is a no-op if (from == to) { return; } doc()->joinLines(from, to); } void NormalViMode::reformatLines(unsigned int from, unsigned int to) const { joinLines(from, to); doc()->wrapText(from, to); } int NormalViMode::getFirstNonBlank(int line) const { if (line < 0) { line = m_view->cursorPosition().line(); } // doc()->plainKateTextLine returns NULL if the line is out of bounds. Kate::TextLine l = doc()->plainKateTextLine(line); Q_ASSERT(l); int c = l->firstChar(); return (c < 0) ? 0 : c; } // Tries to shrinks toShrink so that it fits tightly around rangeToShrinkTo. void NormalViMode::shrinkRangeAroundCursor(Range &toShrink, const Range &rangeToShrinkTo) const { if (!toShrink.valid || !rangeToShrinkTo.valid) { return; } KTextEditor::Cursor cursorPos = m_view->cursorPosition(); if (rangeToShrinkTo.startLine >= cursorPos.line()) { if (rangeToShrinkTo.startLine > cursorPos.line()) { // Does not surround cursor; aborting. return; } Q_ASSERT(rangeToShrinkTo.startLine == cursorPos.line()); if (rangeToShrinkTo.startColumn > cursorPos.column()) { // Does not surround cursor; aborting. return; } } if (rangeToShrinkTo.endLine <= cursorPos.line()) { if (rangeToShrinkTo.endLine < cursorPos.line()) { // Does not surround cursor; aborting. return; } Q_ASSERT(rangeToShrinkTo.endLine == cursorPos.line()); if (rangeToShrinkTo.endColumn < cursorPos.column()) { // Does not surround cursor; aborting. return; } } if (toShrink.startLine <= rangeToShrinkTo.startLine) { if (toShrink.startLine < rangeToShrinkTo.startLine) { toShrink.startLine = rangeToShrinkTo.startLine; toShrink.startColumn = rangeToShrinkTo.startColumn; } Q_ASSERT(toShrink.startLine == rangeToShrinkTo.startLine); if (toShrink.startColumn < rangeToShrinkTo.startColumn) { toShrink.startColumn = rangeToShrinkTo.startColumn; } } if (toShrink.endLine >= rangeToShrinkTo.endLine) { if (toShrink.endLine > rangeToShrinkTo.endLine) { toShrink.endLine = rangeToShrinkTo.endLine; toShrink.endColumn = rangeToShrinkTo.endColumn; } Q_ASSERT(toShrink.endLine == rangeToShrinkTo.endLine); if (toShrink.endColumn > rangeToShrinkTo.endColumn) { toShrink.endColumn = rangeToShrinkTo.endColumn; } } } Range NormalViMode::textObjectComma(bool inner) const { // Basic algorithm: look left and right of the cursor for all combinations // of enclosing commas and the various types of brackets, and pick the pair // closest to the cursor that surrounds the cursor. Range r(0, 0, m_view->doc()->lines(), m_view->doc()->line(m_view->doc()->lastLine()).length(), InclusiveMotion); shrinkRangeAroundCursor(r, findSurroundingQuotes(QLatin1Char(','), inner)); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('('), QLatin1Char(')'), inner, QLatin1Char('('), QLatin1Char(')'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('{'), QLatin1Char('}'), inner, QLatin1Char('{'), QLatin1Char('}'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char(','), QLatin1Char(')'), inner, QLatin1Char('('), QLatin1Char(')'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char(','), QLatin1Char(']'), inner, QLatin1Char('['), QLatin1Char(']'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char(','), QLatin1Char('}'), inner, QLatin1Char('{'), QLatin1Char('}'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('('), QLatin1Char(','), inner, QLatin1Char('('), QLatin1Char(')'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('['), QLatin1Char(','), inner, QLatin1Char('['), QLatin1Char(']'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('{'), QLatin1Char(','), inner, QLatin1Char('{'), QLatin1Char('}'))); return r; } void NormalViMode::updateYankHighlightAttrib() { if (!m_highlightYankAttribute) { m_highlightYankAttribute = new KTextEditor::Attribute; } const QColor &yankedColor = m_view->renderer()->config()->savedLineColor(); m_highlightYankAttribute->setBackground(yankedColor); KTextEditor::Attribute::Ptr mouseInAttribute(new KTextEditor::Attribute()); mouseInAttribute->setFontBold(true); m_highlightYankAttribute->setDynamicAttribute(KTextEditor::Attribute::ActivateMouseIn, mouseInAttribute); m_highlightYankAttribute->dynamicAttribute(KTextEditor::Attribute::ActivateMouseIn)->setBackground(yankedColor); } void NormalViMode::highlightYank(const Range &range, const OperationMode mode) { clearYankHighlight(); // current MovingRange doesn't support block mode selection so split the // block range into per-line ranges if (mode == Block) { for (int i = range.startLine; i <= range.endLine; i++) { addHighlightYank(KTextEditor::Range(i, range.startColumn, i, range.endColumn)); } } else { addHighlightYank(KTextEditor::Range(range.startLine, range.startColumn, range.endLine, range.endColumn)); } } void NormalViMode::addHighlightYank(const KTextEditor::Range &yankRange) { KTextEditor::MovingRange *highlightedYank = m_view->doc()->newMovingRange(yankRange, Kate::TextRange::DoNotExpand); highlightedYank->setView(m_view); // show only in this view highlightedYank->setAttributeOnlyForViews(true); // use z depth defined in moving ranges interface highlightedYank->setZDepth(-10000.0); highlightedYank->setAttribute(m_highlightYankAttribute); highlightedYankForDocument().insert(highlightedYank); } void NormalViMode::clearYankHighlight() { QSet &pHighlightedYanks = highlightedYankForDocument(); qDeleteAll(pHighlightedYanks); pHighlightedYanks.clear(); } void NormalViMode::aboutToDeleteMovingInterfaceContent() { QSet &pHighlightedYanks = highlightedYankForDocument(); // Prevent double-deletion in case this NormalMode is deleted. pHighlightedYanks.clear(); } QSet &NormalViMode::highlightedYankForDocument() { // Work around the fact that both Normal and Visual mode will have their own m_highlightedYank - // make Normal's the canonical one. return m_viInputModeManager->getViNormalMode()->m_highlightedYanks; } bool NormalViMode::waitingForRegisterOrCharToSearch() { // r, q, @ are never preceded by operators. There will always be a keys size of 1 for them. // f, t, F, T can be preceded by a delete/replace/yank/indent operator. size = 2 in that case. // f, t, F, T can be preceded by 'g' case/formatting operators. size = 3 in that case. const int keysSize = m_keys.size(); if (keysSize < 1) { // Just being defensive there. return false; } if (keysSize > 1) { // Multi-letter operation. QChar cPrefix = m_keys[0]; if (keysSize == 2) { // delete/replace/yank/indent operator? if (cPrefix != QLatin1Char('c') && cPrefix != QLatin1Char('d') && cPrefix != QLatin1Char('y') && cPrefix != QLatin1Char('=') && cPrefix != QLatin1Char('>') && cPrefix != QLatin1Char('<')) { return false; } } else if (keysSize == 3) { // We need to look deeper. Is it a g motion? QChar cNextfix = m_keys[1]; if (cPrefix != QLatin1Char('g') || ( cNextfix != QLatin1Char('U') && cNextfix != QLatin1Char('u') && cNextfix != QLatin1Char('~') && cNextfix != QLatin1Char('q') && cNextfix != QLatin1Char('w') && cNextfix != QLatin1Char('@'))) { return false; } } else { return false; } } QChar ch = m_keys[keysSize - 1]; return (ch == QLatin1Char('f') || ch == QLatin1Char('t') || ch == QLatin1Char('F') || ch == QLatin1Char('T') // c/d prefix unapplicable for the following cases. || (keysSize == 1 && (ch == QLatin1Char('r') || ch == QLatin1Char('q') || ch == QLatin1Char('@')))); } void NormalViMode::textInserted(KTextEditor::Document *document, KTextEditor::Range range) { Q_UNUSED(document); const bool isInsertReplaceMode = (m_viInputModeManager->getCurrentViMode() == ViMode::InsertMode || m_viInputModeManager->getCurrentViMode() == ViMode::ReplaceMode); const bool continuesInsertion = range.start().line() == m_currentChangeEndMarker.line() && range.start().column() == m_currentChangeEndMarker.column(); const bool beginsWithNewline = doc()->text(range).at(0) == QLatin1Char('\n'); if (!continuesInsertion) { KTextEditor::Cursor newBeginMarkerPos = range.start(); if (beginsWithNewline && !isInsertReplaceMode) { // Presumably a linewise paste, in which case we ignore the leading '\n' newBeginMarkerPos = KTextEditor::Cursor(newBeginMarkerPos.line() + 1, 0); } m_viInputModeManager->marks()->setStartEditYanked(newBeginMarkerPos); } m_viInputModeManager->marks()->setLastChange(range.start()); KTextEditor::Cursor editEndMarker = range.end(); if (!isInsertReplaceMode) { editEndMarker.setColumn(editEndMarker.column() - 1); } m_viInputModeManager->marks()->setFinishEditYanked(editEndMarker); m_currentChangeEndMarker = range.end(); if (m_isUndo) { const bool addsMultipleLines = range.start().line() != range.end().line(); m_viInputModeManager->marks()->setStartEditYanked(KTextEditor::Cursor(m_viInputModeManager->marks()->getStartEditYanked().line(), 0)); if (addsMultipleLines) { m_viInputModeManager->marks()->setFinishEditYanked(KTextEditor::Cursor(m_viInputModeManager->marks()->getFinishEditYanked().line() + 1, 0)); m_viInputModeManager->marks()->setLastChange(KTextEditor::Cursor(m_viInputModeManager->marks()->getLastChange().line() + 1, 0)); } else { m_viInputModeManager->marks()->setFinishEditYanked(KTextEditor::Cursor(m_viInputModeManager->marks()->getFinishEditYanked().line(), 0)); m_viInputModeManager->marks()->setLastChange(KTextEditor::Cursor(m_viInputModeManager->marks()->getLastChange().line(), 0)); } } } void NormalViMode::textRemoved(KTextEditor::Document *document, KTextEditor::Range range) { Q_UNUSED(document); const bool isInsertReplaceMode = (m_viInputModeManager->getCurrentViMode() == ViMode::InsertMode || m_viInputModeManager->getCurrentViMode() == ViMode::ReplaceMode); m_viInputModeManager->marks()->setLastChange(range.start()); if (!isInsertReplaceMode) { // Don't go resetting [ just because we did a Ctrl-h! m_viInputModeManager->marks()->setStartEditYanked(range.start()); } else { // Don't go disrupting our continued insertion just because we did a Ctrl-h! m_currentChangeEndMarker = range.start(); } m_viInputModeManager->marks()->setFinishEditYanked(range.start()); if (m_isUndo) { // Slavishly follow Vim's weird rules: if an undo removes several lines, then all markers should // be at the beginning of the line after the last line removed, else they should at the beginning // of the line above that. const int markerLineAdjustment = (range.start().line() != range.end().line()) ? 1 : 0; m_viInputModeManager->marks()->setStartEditYanked(KTextEditor::Cursor(m_viInputModeManager->marks()->getStartEditYanked().line() + markerLineAdjustment, 0)); m_viInputModeManager->marks()->setFinishEditYanked(KTextEditor::Cursor(m_viInputModeManager->marks()->getFinishEditYanked().line() + markerLineAdjustment, 0)); m_viInputModeManager->marks()->setLastChange(KTextEditor::Cursor(m_viInputModeManager->marks()->getLastChange().line() + markerLineAdjustment, 0)); } } void NormalViMode::undoBeginning() { m_isUndo = true; } void NormalViMode::undoEnded() { m_isUndo = false; } bool NormalViMode::executeKateCommand(const QString &command) { KTextEditor::Command *p = KateCmd::self()->queryCommand(command); if (!p) { return false; } QString msg; return p->exec(m_view, command, msg); }