diff --git a/CMakeLists.txt b/CMakeLists.txt index d4363ced..180859ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,122 +1,128 @@ cmake_minimum_required(VERSION 3.0) set(KF5_VERSION "5.51.0") # handled by release scripts set(KF5_DEP_VERSION "5.50.0") # handled by release scripts project(KTextEditor VERSION ${KF5_VERSION}) # ECM setup include(FeatureSummary) find_package(ECM 5.50.0 NO_MODULE) set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://projects.kde.org/projects/kdesupport/extra-cmake-modules") feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES) # add own modules to search path, too set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) include(ECMSetupVersion) include(ECMGenerateHeaders) include(CMakePackageConfigHelpers) include(CheckFunctionExists) include(CheckSymbolExists) include(KDEInstallDirs) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(KDECMakeSettings) include(KDEPackageAppTemplates) include(GenerateExportHeader) include(ECMAddQch) option(BUILD_QCH "Build API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)" OFF) add_feature_info(QCH ${BUILD_QCH} "API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)") ecm_setup_version( PROJECT VARIABLE_PREFIX KTEXTEDITOR VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/ktexteditor_version.h" PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF5TextEditorConfigVersion.cmake" SOVERSION 5 ) # Dependencies set(REQUIRED_QT_VERSION 5.8.0) # Required Qt5 components to build this framework find_package(Qt5 ${REQUIRED_QT_VERSION} NO_MODULE REQUIRED Core Widgets Qml PrintSupport Xml) find_package(KF5Archive ${KF5_DEP_VERSION} REQUIRED) find_package(KF5Config ${KF5_DEP_VERSION} REQUIRED) find_package(KF5GuiAddons ${KF5_DEP_VERSION} REQUIRED) find_package(KF5I18n ${KF5_DEP_VERSION} REQUIRED) find_package(KF5KIO ${KF5_DEP_VERSION} REQUIRED) find_package(KF5Parts ${KF5_DEP_VERSION} REQUIRED) find_package(KF5Sonnet ${KF5_DEP_VERSION} REQUIRED) find_package(KF5IconThemes ${KF5_DEP_VERSION} REQUIRED) find_package(KF5SyntaxHighlighting ${KF5_DEP_VERSION} REQUIRED) # libgit2 integration, at least 0.22 with proper git_libgit2_init() find_package(LibGit2 "0.22.0") # EditorConfig integration find_package(EditorConfig) # vi mode on per default option (BUILD_VIMODE "Build vimode in" ON) # Subdirectories add_definitions(-DTRANSLATION_DOMAIN=\"ktexteditor5\") if (IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/po") ki18n_install(po) endif() +add_definitions(-DQT_NO_CAST_FROM_ASCII) +add_definitions(-DQT_NO_CAST_TO_ASCII) +add_definitions(-DQT_NO_NARROWING_CONVERSIONS_IN_CONNECT) +add_definitions(-DQT_NO_URL_CAST_FROM_STRING) +add_definitions(-DQT_USE_QSTRINGBUILDER) + add_subdirectory(src) if (BUILD_TESTING) add_subdirectory(autotests) endif() add_subdirectory(templates) # Create a Config.cmake and a ConfigVersion.cmake file and install them set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF5TextEditor") if (BUILD_QCH) ecm_install_qch_export( TARGETS KF5TextEditor_QCH FILE KF5TextEditorQchTargets.cmake DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel ) set(PACKAGE_INCLUDE_QCHTARGETS "include(\"\${CMAKE_CURRENT_LIST_DIR}/KF5TextEditorQchTargets.cmake\")") endif() configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/KF5TextEditorConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/KF5TextEditorConfig.cmake" INSTALL_DESTINATION "${CMAKECONFIG_INSTALL_DIR}" ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/KF5TextEditorConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/KF5TextEditorConfigVersion.cmake" DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel ) install(EXPORT KF5TextEditorTargets DESTINATION "${CMAKECONFIG_INSTALL_DIR}" FILE KF5TextEditorTargets.cmake NAMESPACE KF5:: ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/ktexteditor_version.h" DESTINATION "${KDE_INSTALL_INCLUDEDIR_KF5}" COMPONENT Devel ) # config.h check_symbol_exists (fdatasync unistd.h HAVE_FDATASYNC) configure_file (config.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config.h) # let our config.h be found first in any case include_directories (BEFORE ${CMAKE_CURRENT_BINARY_DIR}) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/buffer/katetextfolding.cpp b/src/buffer/katetextfolding.cpp index f75794d9..5c29d804 100644 --- a/src/buffer/katetextfolding.cpp +++ b/src/buffer/katetextfolding.cpp @@ -1,1030 +1,1030 @@ /* This file is part of the Kate project. * * Copyright (C) 2013 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. */ #include "katetextfolding.h" #include "katetextbuffer.h" #include "katetextrange.h" #include "documentcursor.h" #include namespace Kate { TextFolding::FoldingRange::FoldingRange(TextBuffer &buffer, const KTextEditor::Range &range, FoldingRangeFlags _flags) : start(new TextCursor(buffer, range.start(), KTextEditor::MovingCursor::MoveOnInsert)) , end(new TextCursor(buffer, range.end(), KTextEditor::MovingCursor::MoveOnInsert)) , parent(nullptr) , flags(_flags) , id(-1) { } TextFolding::FoldingRange::~FoldingRange() { /** * kill all our data! * this will recurse all sub-structures! */ delete start; delete end; qDeleteAll(nestedRanges); } TextFolding::TextFolding(TextBuffer &buffer) : QObject() , m_buffer(buffer) , m_idCounter(-1) { /** * connect needed signals from buffer */ connect(&m_buffer, SIGNAL(cleared()), SLOT(clear())); } TextFolding::~TextFolding() { /** * only delete the folding ranges, the folded ranges and mapped ranges are the same objects */ qDeleteAll(m_foldingRanges); } void TextFolding::clear() { /** * reset counter */ m_idCounter = -1; /** * no ranges, no work */ if (m_foldingRanges.isEmpty()) { /** * assert all stuff is consistent and return! */ Q_ASSERT(m_idToFoldingRange.isEmpty()); Q_ASSERT(m_foldedFoldingRanges.isEmpty()); return; } /** * cleanup */ m_idToFoldingRange.clear(); m_foldedFoldingRanges.clear(); qDeleteAll(m_foldingRanges); m_foldingRanges.clear(); /** * folding changed! */ emit foldingRangesChanged(); } qint64 TextFolding::newFoldingRange(const KTextEditor::Range &range, FoldingRangeFlags flags) { /** * sort out invalid and empty ranges * that makes no sense, they will never grow again! */ if (!range.isValid() || range.isEmpty()) { return -1; } /** * create new folding region that we want to insert * this will internally create moving cursors! */ FoldingRange *newRange = new FoldingRange(m_buffer, range, flags); /** * the construction of the text cursors might have invalidated this * check and bail out if that happens * bail out, too, if it can't be inserted! */ if (!newRange->start->isValid() || !newRange->end->isValid() || !insertNewFoldingRange(nullptr /* no parent here */, m_foldingRanges, newRange)) { /** * cleanup and be done */ delete newRange; return -1; } /** * set id, catch overflows, even if they shall not happen */ newRange->id = ++m_idCounter; if (newRange->id < 0) { newRange->id = m_idCounter = 0; } /** * remember the range */ m_idToFoldingRange.insert(newRange->id, newRange); /** * update our folded ranges vector! */ bool updated = updateFoldedRangesForNewRange(newRange); /** * emit that something may have changed * do that only, if updateFoldedRangesForNewRange did not already do the job! */ if (!updated) { emit foldingRangesChanged(); } /** * all went fine, newRange is now registered internally! */ return newRange->id; } KTextEditor::Range TextFolding::foldingRange(qint64 id) const { FoldingRange *range = m_idToFoldingRange.value(id); if (!range) { return KTextEditor::Range::invalid(); } return KTextEditor::Range(range->start->toCursor(), range->end->toCursor()); } bool TextFolding::foldRange(qint64 id) { /** * try to find the range, else bail out */ FoldingRange *range = m_idToFoldingRange.value(id); if (!range) { return false; } /** * already folded? nothing to do */ if (range->flags & Folded) { return true; } /** * fold and be done */ range->flags |= Folded; updateFoldedRangesForNewRange(range); return true; } bool TextFolding::unfoldRange(qint64 id, bool remove) { /** * try to find the range, else bail out */ FoldingRange *range = m_idToFoldingRange.value(id); if (!range) { return false; } /** * nothing to do? * range is already unfolded and we need not to remove it! */ if (!remove && !(range->flags & Folded)) { return true; } /** * do we need to delete the range? */ const bool deleteRange = remove || !(range->flags & Persistent); /** * first: remove the range, if forced or non-persistent! */ if (deleteRange) { /** * remove from outside visible mapping! */ m_idToFoldingRange.remove(id); /** * remove from folding vectors! * FIXME: OPTIMIZE */ FoldingRange::Vector &parentVector = range->parent ? range->parent->nestedRanges : m_foldingRanges; FoldingRange::Vector newParentVector; Q_FOREACH (FoldingRange *curRange, parentVector) { /** * insert our nested ranges and reparent them */ if (curRange == range) { Q_FOREACH (FoldingRange *newRange, range->nestedRanges) { newRange->parent = range->parent; newParentVector.push_back(newRange); } continue; } /** * else just transfer elements */ newParentVector.push_back(curRange); } parentVector = newParentVector; } /** * second: unfold the range, if needed! */ bool updated = false; if (range->flags & Folded) { range->flags &= ~Folded; updated = updateFoldedRangesForRemovedRange(range); } /** * emit that something may have changed * do that only, if updateFoldedRangesForRemoveRange did not already do the job! */ if (!updated) { emit foldingRangesChanged(); } /** * really delete the range, if needed! */ if (deleteRange) { /** * clear ranges first, they got moved! */ range->nestedRanges.clear(); delete range; } /** * be done ;) */ return true; } bool TextFolding::isLineVisible(int line, qint64 *foldedRangeId) const { /** * skip if nothing folded */ if (m_foldedFoldingRanges.isEmpty()) { return true; } /** * search upper bound, index to item with start line higher than our one */ - FoldingRange::Vector::const_iterator upperBound = qUpperBound(m_foldedFoldingRanges.begin(), m_foldedFoldingRanges.end(), line, compareRangeByStartWithLine); + FoldingRange::Vector::const_iterator upperBound = std::upper_bound(m_foldedFoldingRanges.begin(), m_foldedFoldingRanges.end(), line, compareRangeByStartWithLine); if (upperBound != m_foldedFoldingRanges.begin()) { --upperBound; } /** * check if we overlap with the range in front of us */ const bool hidden = (((*upperBound)->end->line() >= line) && (line > (*upperBound)->start->line())); /** * fill in folded range id, if needed */ if (foldedRangeId) { (*foldedRangeId) = hidden ? (*upperBound)->id : -1; } /** * visible == !hidden */ return !hidden; } void TextFolding::ensureLineIsVisible(int line) { /** * skip if nothing folded */ if (m_foldedFoldingRanges.isEmpty()) { return; } /** * while not visible, unfold */ qint64 foldedRangeId = -1; while (!isLineVisible(line, &foldedRangeId)) { /** * id should be valid! */ Q_ASSERT(foldedRangeId >= 0); /** * unfold shall work! */ const bool unfolded = unfoldRange(foldedRangeId); (void) unfolded; Q_ASSERT(unfolded); } } int TextFolding::visibleLines() const { /** * start with all lines we have */ int visibleLines = m_buffer.lines(); /** * skip if nothing folded */ if (m_foldedFoldingRanges.isEmpty()) { return visibleLines; } /** * count all folded lines and subtract them from visible lines */ Q_FOREACH (FoldingRange *range, m_foldedFoldingRanges) { visibleLines -= (range->end->line() - range->start->line()); } /** * be done, assert we did no trash */ Q_ASSERT(visibleLines > 0); return visibleLines; } int TextFolding::lineToVisibleLine(int line) const { /** * valid input needed! */ Q_ASSERT(line >= 0); /** * start with identity */ int visibleLine = line; /** * skip if nothing folded or first line */ if (m_foldedFoldingRanges.isEmpty() || (line == 0)) { return visibleLine; } /** * walk over all folded ranges until we reach the line * keep track of seen visible lines, for the case we want to convert a hidden line! */ int seenVisibleLines = 0; int lastLine = 0; Q_FOREACH (FoldingRange *range, m_foldedFoldingRanges) { /** * abort if we reach our line! */ if (range->start->line() >= line) { break; } /** * count visible lines */ seenVisibleLines += (range->start->line() - lastLine); lastLine = range->end->line(); /** * we might be contained in the region, then we return last visible line */ if (line <= range->end->line()) { return seenVisibleLines; } /** * subtrace folded lines */ visibleLine -= (range->end->line() - range->start->line()); } /** * be done, assert we did no trash */ Q_ASSERT(visibleLine >= 0); return visibleLine; } int TextFolding::visibleLineToLine(int visibleLine) const { /** * valid input needed! */ Q_ASSERT(visibleLine >= 0); /** * start with identity */ int line = visibleLine; /** * skip if nothing folded or first line */ if (m_foldedFoldingRanges.isEmpty() || (visibleLine == 0)) { return line; } /** * last visible line seen, as line in buffer */ int seenVisibleLines = 0; int lastLine = 0; int lastLineVisibleLines = 0; Q_FOREACH (FoldingRange *range, m_foldedFoldingRanges) { /** * else compute visible lines and move last seen */ lastLineVisibleLines = seenVisibleLines; seenVisibleLines += (range->start->line() - lastLine); /** * bail out if enough seen */ if (seenVisibleLines >= visibleLine) { break; } lastLine = range->end->line(); } /** * check if still no enough visible! */ if (seenVisibleLines < visibleLine) { lastLineVisibleLines = seenVisibleLines; } /** * compute visible line */ line = (lastLine + (visibleLine - lastLineVisibleLines)); Q_ASSERT(line >= 0); return line; } QVector > TextFolding::foldingRangesStartingOnLine(int line) const { /** * results vector */ QVector > results; /** * recursively do binary search */ foldingRangesStartingOnLine(results, m_foldingRanges, line); /** * return found results */ return results; } void TextFolding::foldingRangesStartingOnLine(QVector > &results, const TextFolding::FoldingRange::Vector &ranges, int line) const { /** * early out for no folds */ if (ranges.isEmpty()) { return; } /** * first: lower bound of start */ - FoldingRange::Vector::const_iterator lowerBound = qLowerBound(ranges.begin(), ranges.end(), line, compareRangeByLineWithStart); + FoldingRange::Vector::const_iterator lowerBound = std::lower_bound(ranges.begin(), ranges.end(), line, compareRangeByLineWithStart); /** * second: upper bound of end */ - FoldingRange::Vector::const_iterator upperBound = qUpperBound(ranges.begin(), ranges.end(), line, compareRangeByStartWithLine); + FoldingRange::Vector::const_iterator upperBound = std::upper_bound(ranges.begin(), ranges.end(), line, compareRangeByStartWithLine); /** * we may need to go one to the left, if not already at the begin, as we might overlap with the one in front of us! */ if ((lowerBound != ranges.begin()) && ((*(lowerBound - 1))->end->line() >= line)) { --lowerBound; } /** * for all of them, check if we start at right line and recurse */ for (FoldingRange::Vector::const_iterator it = lowerBound; it != upperBound; ++it) { /** * this range already ok? add it to results */ if ((*it)->start->line() == line) { results.push_back(qMakePair((*it)->id, (*it)->flags)); } /** * recurse anyway */ foldingRangesStartingOnLine(results, (*it)->nestedRanges, line); } } QVector > TextFolding::foldingRangesForParentRange(qint64 parentRangeId) const { /** * toplevel ranges requested or real parent? */ const FoldingRange::Vector *ranges = nullptr; if (parentRangeId == -1) { ranges = &m_foldingRanges; } else if (FoldingRange *range = m_idToFoldingRange.value(parentRangeId)) { ranges = &range->nestedRanges; } /** * no ranges => nothing to do */ QVector > results; if (!ranges) return results; /** * else convert ranges to id + flags and pass that back */ for (FoldingRange::Vector::const_iterator it = ranges->begin(); it != ranges->end(); ++it) { results.append(qMakePair((*it)->id, (*it)->flags)); } return results; } QString TextFolding::debugDump() const { /** * dump toplevel ranges recursively */ return QStringLiteral("tree %1 - folded %2").arg(debugDump(m_foldingRanges, true), debugDump(m_foldedFoldingRanges, false)); } void TextFolding::debugPrint(const QString &title) const { // print title + content printf("%s\n %s\n", qPrintable(title), qPrintable(debugDump())); } QString TextFolding::debugDump(const TextFolding::FoldingRange::Vector &ranges, bool recurse) { /** * dump all ranges recursively */ QString dump; Q_FOREACH (FoldingRange *range, ranges) { if (!dump.isEmpty()) { dump += QLatin1Char(' '); } const QString persistent = (range->flags & Persistent) ? QStringLiteral("p") : QString(); const QString folded = (range->flags & Folded) ? QStringLiteral("f") : QString(); dump += QString::fromLatin1("[%1:%2 %3%4 ").arg(range->start->line()).arg(range->start->column()).arg(persistent, folded); /** * recurse */ if (recurse) { QString inner = debugDump(range->nestedRanges, recurse); if (!inner.isEmpty()) { dump += inner + QLatin1Char(' '); } } dump += QString::fromLatin1("%1:%2]").arg(range->end->line()).arg(range->end->column()); } return dump; } bool TextFolding::insertNewFoldingRange(FoldingRange *parent, FoldingRange::Vector &existingRanges, FoldingRange *newRange) { /** * existing ranges are non-overlapping and sorted * that means, we can search for lower bound of start of range and upper bound of end of range to find all "overlapping" ranges. */ /** * first: lower bound of start */ - FoldingRange::Vector::iterator lowerBound = qLowerBound(existingRanges.begin(), existingRanges.end(), newRange, compareRangeByStart); + FoldingRange::Vector::iterator lowerBound = std::lower_bound(existingRanges.begin(), existingRanges.end(), newRange, compareRangeByStart); /** * second: upper bound of end */ - FoldingRange::Vector::iterator upperBound = qUpperBound(existingRanges.begin(), existingRanges.end(), newRange, compareRangeByEnd); + FoldingRange::Vector::iterator upperBound = std::upper_bound(existingRanges.begin(), existingRanges.end(), newRange, compareRangeByEnd); /** * we may need to go one to the left, if not already at the begin, as we might overlap with the one in front of us! */ if ((lowerBound != existingRanges.begin()) && ((*(lowerBound - 1))->end->toCursor() > newRange->start->toCursor())) { --lowerBound; } /** * now: first case, we overlap with nothing or hit exactly one range! */ if (lowerBound == upperBound) { /** * nothing we overlap with? * then just insert and be done! */ if ((lowerBound == existingRanges.end()) || (newRange->start->toCursor() >= (*lowerBound)->end->toCursor()) || (newRange->end->toCursor() <= (*lowerBound)->start->toCursor())) { /** * insert + fix parent */ existingRanges.insert(lowerBound, newRange); newRange->parent = parent; /** * all done */ return true; } /** * we are contained in this one range? * then recurse! */ if ((newRange->start->toCursor() >= (*lowerBound)->start->toCursor()) && (newRange->end->toCursor() <= (*lowerBound)->end->toCursor())) { return insertNewFoldingRange((*lowerBound), (*lowerBound)->nestedRanges, newRange); } /** * else: we might contain at least this fold, or many more, if this if block is not taken at all * use the general code that checks for "we contain stuff" below! */ } /** * check if we contain other folds! */ FoldingRange::Vector::iterator it = lowerBound; bool includeUpperBound = false; FoldingRange::Vector nestedRanges; while (it != existingRanges.end()) { /** * do we need to take look at upper bound, too? * if not break */ if (it == upperBound) { if (newRange->end->toCursor() <= (*upperBound)->start->toCursor()) { break; } else { includeUpperBound = true; } } /** * if one region is not contained in the new one, abort! * then this is not well nested! */ if (!((newRange->start->toCursor() <= (*it)->start->toCursor()) && (newRange->end->toCursor() >= (*it)->end->toCursor()))) { return false; } /** * include into new nested ranges */ nestedRanges.push_back((*it)); /** * end reached */ if (it == upperBound) { break; } /** * else increment */ ++it; } /** * if we arrive here, all is nicely nested into our new range * remove the contained ones here, insert new range with new nested ranges we already constructed */ it = existingRanges.erase(lowerBound, includeUpperBound ? (upperBound + 1) : upperBound); existingRanges.insert(it, newRange); newRange->nestedRanges = nestedRanges; /** * correct parent mapping! */ newRange->parent = parent; Q_FOREACH (FoldingRange *range, newRange->nestedRanges) { range->parent = newRange; } /** * all nice */ return true; } bool TextFolding::compareRangeByStart(FoldingRange *a, FoldingRange *b) { return a->start->toCursor() < b->start->toCursor(); } bool TextFolding::compareRangeByEnd(FoldingRange *a, FoldingRange *b) { return a->end->toCursor() < b->end->toCursor(); } bool TextFolding::compareRangeByStartWithLine(int line, FoldingRange *range) { return (line < range->start->line()); } bool TextFolding::compareRangeByLineWithStart(FoldingRange *range, int line) { return (range->start->line() < line); } bool TextFolding::updateFoldedRangesForNewRange(TextFolding::FoldingRange *newRange) { /** * not folded? not interesting! we don't need to touch our m_foldedFoldingRanges vector */ if (!(newRange->flags & Folded)) { return false; } /** * any of the parents folded? not interesting, too! */ TextFolding::FoldingRange *parent = newRange->parent; while (parent) { /** * parent folded => be done */ if (parent->flags & Folded) { return false; } /** * walk up */ parent = parent->parent; } /** * ok, if we arrive here, we are a folded range and we have no folded parent * we now want to add this range to the m_foldedFoldingRanges vector, just removing any ranges that is included in it! * TODO: OPTIMIZE */ FoldingRange::Vector newFoldedFoldingRanges; bool newRangeInserted = false; Q_FOREACH (FoldingRange *range, m_foldedFoldingRanges) { /** * contained? kill */ if ((newRange->start->toCursor() <= range->start->toCursor()) && (newRange->end->toCursor() >= range->end->toCursor())) { continue; } /** * range is behind newRange? * insert newRange if not already done */ if (!newRangeInserted && (range->start->toCursor() >= newRange->end->toCursor())) { newFoldedFoldingRanges.push_back(newRange); newRangeInserted = true; } /** * just transfer range */ newFoldedFoldingRanges.push_back(range); } /** * last: insert new range, if not done */ if (!newRangeInserted) { newFoldedFoldingRanges.push_back(newRange); } /** * fixup folded ranges */ m_foldedFoldingRanges = newFoldedFoldingRanges; /** * folding changed! */ emit foldingRangesChanged(); /** * all fine, stuff done, signal emitted */ return true; } bool TextFolding::updateFoldedRangesForRemovedRange(TextFolding::FoldingRange *oldRange) { /** * folded? not interesting! we don't need to touch our m_foldedFoldingRanges vector */ if (oldRange->flags & Folded) { return false; } /** * any of the parents folded? not interesting, too! */ TextFolding::FoldingRange *parent = oldRange->parent; while (parent) { /** * parent folded => be done */ if (parent->flags & Folded) { return false; } /** * walk up */ parent = parent->parent; } /** * ok, if we arrive here, we are a unfolded range and we have no folded parent * we now want to remove this range from the m_foldedFoldingRanges vector and include our nested folded ranges! * TODO: OPTIMIZE */ FoldingRange::Vector newFoldedFoldingRanges; Q_FOREACH (FoldingRange *range, m_foldedFoldingRanges) { /** * right range? insert folded nested ranges */ if (range == oldRange) { appendFoldedRanges(newFoldedFoldingRanges, oldRange->nestedRanges); continue; } /** * just transfer range */ newFoldedFoldingRanges.push_back(range); } /** * fixup folded ranges */ m_foldedFoldingRanges = newFoldedFoldingRanges; /** * folding changed! */ emit foldingRangesChanged(); /** * all fine, stuff done, signal emitted */ return true; } void TextFolding::appendFoldedRanges(TextFolding::FoldingRange::Vector &newFoldedFoldingRanges, const TextFolding::FoldingRange::Vector &ranges) const { /** * search for folded ranges and append them */ Q_FOREACH (FoldingRange *range, ranges) { /** * itself folded? append */ if (range->flags & Folded) { newFoldedFoldingRanges.push_back(range); continue; } /** * else: recurse! */ appendFoldedRanges(newFoldedFoldingRanges, range->nestedRanges); } } QJsonDocument TextFolding::exportFoldingRanges() const { QJsonArray array; exportFoldingRanges(m_foldingRanges, array); QJsonDocument folds; folds.setArray(array); return folds; } void TextFolding::exportFoldingRanges(const TextFolding::FoldingRange::Vector &ranges, QJsonArray &folds) { /** * dump all ranges recursively */ Q_FOREACH (FoldingRange *range, ranges) { /** * construct one range and dump to folds */ QJsonObject rangeMap; rangeMap[QStringLiteral("startLine")] = range->start->line(); rangeMap[QStringLiteral("startColumn")] = range->start->column(); rangeMap[QStringLiteral("endLine")] = range->end->line(); rangeMap[QStringLiteral("endColumn")] = range->end->column(); rangeMap[QStringLiteral("flags")] = (int)range->flags; folds.append(rangeMap); /** * recurse */ exportFoldingRanges(range->nestedRanges, folds); } } void TextFolding::importFoldingRanges(const QJsonDocument &folds) { Q_FOREACH (FoldingRange *range, m_foldingRanges) { unfoldRange(range->id); } /** * try to create all folding ranges */ Q_FOREACH (const QJsonValue &rangeVariant, folds.array()) { /** * get map */ QJsonObject rangeMap = rangeVariant.toObject(); /** * construct range start/end */ const KTextEditor::Cursor start(rangeMap[QStringLiteral("startLine")].toInt(), rangeMap[QStringLiteral("startColumn")].toInt()); const KTextEditor::Cursor end(rangeMap[QStringLiteral("endLine")].toInt(), rangeMap[QStringLiteral("endColumn")].toInt()); /** * check validity (required when loading a possibly broken folding state from disk) */ if (start >= end || (m_buffer.document() && // <-- unit test katetextbuffertest does not have a KTE::Document assigned (!KTextEditor::DocumentCursor(m_buffer.document(), start).isValidTextPosition() || !KTextEditor::DocumentCursor(m_buffer.document(), end).isValidTextPosition())) ) { continue; } /** * get flags */ const int rawFlags = rangeMap[QStringLiteral("flags")].toInt(); FoldingRangeFlags flags; if (rawFlags & Persistent) { flags = Persistent; } if (rawFlags & Folded) { flags = Folded; } /** * create folding range */ newFoldingRange(KTextEditor::Range(start, end), flags); } } } diff --git a/src/completion/katecompletionmodel.cpp b/src/completion/katecompletionmodel.cpp index 94ea7e02..3cc745eb 100644 --- a/src/completion/katecompletionmodel.cpp +++ b/src/completion/katecompletionmodel.cpp @@ -1,2404 +1,2404 @@ /* 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 cs) { m_matchCaseSensitivity = cs; } 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 indivdual 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(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(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 += "] "; } 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 (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) { const QString& filter = rhs.model->currentCompletion(rhs.m_sourceRow.first); if( m_nameColumn.startsWith(filter, Qt::CaseSensitive) ) { return true; } if( rhs.m_nameColumn.startsWith(filter, Qt::CaseSensitive) ) { return false; } // 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(qUpperBound(prefilter.begin(), prefilter.end(), i), i); + prefilter.insert(std::upper_bound(prefilter.begin(), prefilter.end(), i), i); if (i.isVisible()) { - QList::iterator it = qUpperBound(filtered.begin(), filtered.end(), i); + 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() { qStableSort(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 bool matchesAbbreviationHelper(const QString &word, const QString &typed, const QVarLengthArray &offsets, int &depth, int atWord = -1, int i = 0) { int atLetter = 1; for (; i < typed.size(); i++) { const QChar c = typed.at(i).toLower(); bool haveNextWord = offsets.size() > atWord + 1; bool canCompare = atWord != -1 && word.size() > offsets.at(atWord) + atLetter; if (canCompare && c == word.at(offsets.at(atWord) + atLetter).toLower()) { // the typed letter matches a letter after the current word beginning if (! haveNextWord || c != word.at(offsets.at(atWord + 1)).toLower()) { // 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, 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 == word.at(offsets.at(atWord + 1)).toLower()) { // 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) { // A mismatch is very likely for random even for the first letter, // thus this optimization makes sense. if (word.at(0).toLower() != typed.at(0).toLower()) { 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 (c.toLower() != word.at(atLetter).toLower()) { 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 ambigous 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, 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)) { matchCompletion = AbbreviationMatch; } } if (matchCompletion && match.length() == m_nameColumn.length()) { 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; } 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/render/katelayoutcache.cpp b/src/render/katelayoutcache.cpp index 048b946d..e5c644ce 100644 --- a/src/render/katelayoutcache.cpp +++ b/src/render/katelayoutcache.cpp @@ -1,593 +1,593 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2005 Hamish Rodda * Copyright (C) 2008-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 "katelayoutcache.h" #include #include "katerenderer.h" #include "kateview.h" #include "katedocument.h" #include "katebuffer.h" #include "katepartdebug.h" namespace { bool enableLayoutCache = false; bool lessThan(const KateLineLayoutMap::LineLayoutPair &lhs, const KateLineLayoutMap::LineLayoutPair &rhs) { return lhs.first < rhs.first; } } //BEGIN KateLineLayoutMap KateLineLayoutMap::KateLineLayoutMap() { } KateLineLayoutMap::~KateLineLayoutMap() { } void KateLineLayoutMap::clear() { m_lineLayouts.clear(); } bool KateLineLayoutMap::contains(int i) const { LineLayoutMap::const_iterator it = qBinaryFind(m_lineLayouts.constBegin(), m_lineLayouts.constEnd(), LineLayoutPair(i, KateLineLayoutPtr()), lessThan); return (it != m_lineLayouts.constEnd()); } void KateLineLayoutMap::insert(int realLine, const KateLineLayoutPtr &lineLayoutPtr) { LineLayoutMap::iterator it = qBinaryFind(m_lineLayouts.begin(), m_lineLayouts.end(), LineLayoutPair(realLine, KateLineLayoutPtr()), lessThan); if (it != m_lineLayouts.end()) { (*it).second = lineLayoutPtr; } else { - it = qUpperBound(m_lineLayouts.begin(), m_lineLayouts.end(), LineLayoutPair(realLine, KateLineLayoutPtr()), lessThan); + it = std::upper_bound(m_lineLayouts.begin(), m_lineLayouts.end(), LineLayoutPair(realLine, KateLineLayoutPtr()), lessThan); m_lineLayouts.insert(it, LineLayoutPair(realLine, lineLayoutPtr)); } } void KateLineLayoutMap::viewWidthIncreased() { LineLayoutMap::iterator it = m_lineLayouts.begin(); for (; it != m_lineLayouts.end(); ++it) { if ((*it).second->isValid() && (*it).second->viewLineCount() > 1) { (*it).second->invalidateLayout(); } } } void KateLineLayoutMap::viewWidthDecreased(int newWidth) { LineLayoutMap::iterator it = m_lineLayouts.begin(); for (; it != m_lineLayouts.end(); ++it) { if ((*it).second->isValid() && ((*it).second->viewLineCount() > 1 || (*it).second->width() > newWidth)) { (*it).second->invalidateLayout(); } } } void KateLineLayoutMap::relayoutLines(int startRealLine, int endRealLine) { LineLayoutMap::iterator start = - qLowerBound(m_lineLayouts.begin(), m_lineLayouts.end(), LineLayoutPair(startRealLine, KateLineLayoutPtr()), lessThan); + std::lower_bound(m_lineLayouts.begin(), m_lineLayouts.end(), LineLayoutPair(startRealLine, KateLineLayoutPtr()), lessThan); LineLayoutMap::iterator end = - qUpperBound(start, m_lineLayouts.end(), LineLayoutPair(endRealLine, KateLineLayoutPtr()), lessThan); + std::upper_bound(start, m_lineLayouts.end(), LineLayoutPair(endRealLine, KateLineLayoutPtr()), lessThan); while (start != end) { (*start).second->setLayoutDirty(); ++start; } } void KateLineLayoutMap::slotEditDone(int fromLine, int toLine, int shiftAmount) { LineLayoutMap::iterator start = - qLowerBound(m_lineLayouts.begin(), m_lineLayouts.end(), LineLayoutPair(fromLine, KateLineLayoutPtr()), lessThan); + std::lower_bound(m_lineLayouts.begin(), m_lineLayouts.end(), LineLayoutPair(fromLine, KateLineLayoutPtr()), lessThan); LineLayoutMap::iterator end = - qUpperBound(start, m_lineLayouts.end(), LineLayoutPair(toLine, KateLineLayoutPtr()), lessThan); + std::upper_bound(start, m_lineLayouts.end(), LineLayoutPair(toLine, KateLineLayoutPtr()), lessThan); LineLayoutMap::iterator it; if (shiftAmount != 0) { for (it = end; it != m_lineLayouts.end(); ++it) { (*it).first += shiftAmount; (*it).second->setLine((*it).second->line() + shiftAmount); } for (it = start; it != end; ++it) { (*it).second->clear(); } m_lineLayouts.erase(start, end); } else { for (it = start; it != end; ++it) { (*it).second->setLayoutDirty(); } } } KateLineLayoutPtr &KateLineLayoutMap::operator[](int i) { LineLayoutMap::iterator it = qBinaryFind(m_lineLayouts.begin(), m_lineLayouts.end(), LineLayoutPair(i, KateLineLayoutPtr()), lessThan); return (*it).second; } //END KateLineLayoutMap KateLayoutCache::KateLayoutCache(KateRenderer *renderer, QObject *parent) : QObject(parent) , m_renderer(renderer) , m_startPos(-1, -1) , m_viewWidth(0) , m_wrap(false) , m_acceptDirtyLayouts(false) { Q_ASSERT(m_renderer); /** * connect to all possible editing primitives */ connect(&m_renderer->doc()->buffer(), SIGNAL(lineWrapped(KTextEditor::Cursor)), this, SLOT(wrapLine(KTextEditor::Cursor))); connect(&m_renderer->doc()->buffer(), SIGNAL(lineUnwrapped(int)), this, SLOT(unwrapLine(int))); connect(&m_renderer->doc()->buffer(), SIGNAL(textInserted(KTextEditor::Cursor,QString)), this, SLOT(insertText(KTextEditor::Cursor,QString))); connect(&m_renderer->doc()->buffer(), SIGNAL(textRemoved(KTextEditor::Range,QString)), this, SLOT(removeText(KTextEditor::Range))); } void KateLayoutCache::updateViewCache(const KTextEditor::Cursor &startPos, int newViewLineCount, int viewLinesScrolled) { //qCDebug(LOG_KTE) << startPos << " nvlc " << newViewLineCount << " vls " << viewLinesScrolled; int oldViewLineCount = m_textLayouts.count(); if (newViewLineCount == -1) { newViewLineCount = oldViewLineCount; } enableLayoutCache = true; int realLine; if (newViewLineCount == -1) { realLine = m_renderer->folding().visibleLineToLine(m_renderer->folding().lineToVisibleLine(startPos.line())); } else { realLine = m_renderer->folding().visibleLineToLine(startPos.line()); } int _viewLine = 0; if (wrap()) { // TODO check these assumptions are ok... probably they don't give much speedup anyway? if (startPos == m_startPos && !m_textLayouts.isEmpty()) { _viewLine = m_textLayouts.first().viewLine(); } else if (viewLinesScrolled > 0 && viewLinesScrolled < m_textLayouts.count()) { _viewLine = m_textLayouts[viewLinesScrolled].viewLine(); } else { KateLineLayoutPtr l = line(realLine); if (l) { Q_ASSERT(l->isValid()); Q_ASSERT(l->length() >= startPos.column() || m_renderer->view()->wrapCursor()); bool found = false; for (; _viewLine < l->viewLineCount(); ++_viewLine) { const KateTextLayout &t = l->viewLine(_viewLine); if (t.startCol() >= startPos.column() || _viewLine == l->viewLineCount() - 1) { found = true; break; } } // FIXME FIXME need to calculate past-end-of-line position here... Q_ASSERT(found); Q_UNUSED(found); } } } m_startPos = startPos; // Move the text layouts if we've just scrolled... if (viewLinesScrolled != 0) { // loop backwards if we've just scrolled up... bool forwards = viewLinesScrolled >= 0 ? true : false; for (int z = forwards ? 0 : m_textLayouts.count() - 1; forwards ? (z < m_textLayouts.count()) : (z >= 0); forwards ? z++ : z--) { int oldZ = z + viewLinesScrolled; if (oldZ >= 0 && oldZ < m_textLayouts.count()) { m_textLayouts[z] = m_textLayouts[oldZ]; } } } // Resize functionality if (newViewLineCount > oldViewLineCount) { m_textLayouts.reserve(newViewLineCount); } else if (newViewLineCount < oldViewLineCount) { /* FIXME reintroduce... check we're not missing any int lastLine = m_textLayouts[newSize - 1].line(); for (int i = oldSize; i < newSize; i++) { const KateTextLayout& layout = m_textLayouts[i]; if (layout.line() > lastLine && !layout.viewLine()) layout.kateLineLayout()->layout()->setCacheEnabled(false); }*/ m_textLayouts.resize(newViewLineCount); } KateLineLayoutPtr l = line(realLine); for (int i = 0; i < newViewLineCount; ++i) { if (!l) { if (i < m_textLayouts.count()) { if (m_textLayouts[i].isValid()) { m_textLayouts[i] = KateTextLayout::invalid(); } } else { m_textLayouts.append(KateTextLayout::invalid()); } continue; } Q_ASSERT(l->isValid()); Q_ASSERT(_viewLine < l->viewLineCount()); if (i < m_textLayouts.count()) { bool dirty = false; if (m_textLayouts[i].line() != realLine || m_textLayouts[i].viewLine() != _viewLine || (!m_textLayouts[i].isValid() && l->viewLine(_viewLine).isValid())) { dirty = true; } m_textLayouts[i] = l->viewLine(_viewLine); if (dirty) { m_textLayouts[i].setDirty(true); } } else { m_textLayouts.append(l->viewLine(_viewLine)); } //qCDebug(LOG_KTE) << "Laid out line " << realLine << " (" << l << "), viewLine " << _viewLine << " (" << m_textLayouts[i].kateLineLayout().data() << ")"; //m_textLayouts[i].debugOutput(); _viewLine++; if (_viewLine > l->viewLineCount() - 1) { int virtualLine = l->virtualLine() + 1; realLine = m_renderer->folding().visibleLineToLine(virtualLine); _viewLine = 0; if (realLine < m_renderer->doc()->lines()) { l = line(realLine, virtualLine); } else { l = nullptr; } } } enableLayoutCache = false; } KateLineLayoutPtr KateLayoutCache::line(int realLine, int virtualLine) { if (m_lineLayouts.contains(realLine)) { KateLineLayoutPtr l = m_lineLayouts[realLine]; // ensure line is OK Q_ASSERT(l->line() == realLine); Q_ASSERT(realLine < m_renderer->doc()->buffer().lines()); if (virtualLine != -1) { l->setVirtualLine(virtualLine); } if (!l->isValid()) { l->setUsePlainTextLine(acceptDirtyLayouts()); l->textLine(!acceptDirtyLayouts()); m_renderer->layoutLine(l, wrap() ? m_viewWidth : -1, enableLayoutCache); } else if (l->isLayoutDirty() && !acceptDirtyLayouts()) { // reset textline l->setUsePlainTextLine(false); l->textLine(true); m_renderer->layoutLine(l, wrap() ? m_viewWidth : -1, enableLayoutCache); } Q_ASSERT(l->isValid() && (!l->isLayoutDirty() || acceptDirtyLayouts())); return l; } if (realLine < 0 || realLine >= m_renderer->doc()->lines()) { return KateLineLayoutPtr(); } KateLineLayoutPtr l(new KateLineLayout(*m_renderer)); l->setLine(realLine, virtualLine); // Mark it dirty, because it may not have the syntax highlighting applied // mark this here, to allow layoutLine to use plainLines... if (acceptDirtyLayouts()) { l->setUsePlainTextLine(true); } m_renderer->layoutLine(l, wrap() ? m_viewWidth : -1, enableLayoutCache); Q_ASSERT(l->isValid()); if (acceptDirtyLayouts()) { l->setLayoutDirty(true); } m_lineLayouts.insert(realLine, l); return l; } KateLineLayoutPtr KateLayoutCache::line(const KTextEditor::Cursor &realCursor) { return line(realCursor.line()); } KateTextLayout KateLayoutCache::textLayout(const KTextEditor::Cursor &realCursor) { /*if (realCursor >= viewCacheStart() && (realCursor < viewCacheEnd() || realCursor == viewCacheEnd() && !m_textLayouts.last().wrap())) foreach (const KateTextLayout& l, m_textLayouts) if (l.line() == realCursor.line() && (l.endCol() < realCursor.column() || !l.wrap())) return l;*/ return line(realCursor.line())->viewLine(viewLine(realCursor)); } KateTextLayout KateLayoutCache::textLayout(uint realLine, int _viewLine) { /*if (m_textLayouts.count() && (realLine >= m_textLayouts.first().line() && _viewLine >= m_textLayouts.first().viewLine()) && (realLine <= m_textLayouts.last().line() && _viewLine <= m_textLayouts.first().viewLine())) foreach (const KateTextLayout& l, m_textLayouts) if (l.line() == realLine && l.viewLine() == _viewLine) return const_cast(l);*/ return line(realLine)->viewLine(_viewLine); } KateTextLayout &KateLayoutCache::viewLine(int _viewLine) { Q_ASSERT(_viewLine >= 0 && _viewLine < m_textLayouts.count()); return m_textLayouts[_viewLine]; } int KateLayoutCache::viewCacheLineCount() const { return m_textLayouts.count(); } KTextEditor::Cursor KateLayoutCache::viewCacheStart() const { return !m_textLayouts.isEmpty() ? m_textLayouts.first().start() : KTextEditor::Cursor(); } KTextEditor::Cursor KateLayoutCache::viewCacheEnd() const { return !m_textLayouts.isEmpty() ? m_textLayouts.last().end() : KTextEditor::Cursor(); } int KateLayoutCache::viewWidth() const { return m_viewWidth; } /** * This returns the view line upon which realCursor is situated. * The view line is the number of lines in the view from the first line * The supplied cursor should be in real lines. */ int KateLayoutCache::viewLine(const KTextEditor::Cursor &realCursor) { if (realCursor.column() <= 0 || realCursor.line() < 0) { return 0; } Q_ASSERT(realCursor.line() < m_renderer->doc()->lines()); KateLineLayoutPtr thisLine = line(realCursor.line()); for (int i = 0; i < thisLine->viewLineCount(); ++i) { const KateTextLayout &l = thisLine->viewLine(i); if (realCursor.column() >= l.startCol() && realCursor.column() < l.endCol()) { return i; } } return thisLine->viewLineCount() - 1; } int KateLayoutCache::displayViewLine(const KTextEditor::Cursor &virtualCursor, bool limitToVisible) { if (!virtualCursor.isValid()) { return -1; } KTextEditor::Cursor work = viewCacheStart(); // only try this with valid lines! if (work.isValid()) { work.setLine(m_renderer->folding().lineToVisibleLine(work.line())); } if (!work.isValid()) { return virtualCursor.line(); } int limit = m_textLayouts.count(); // Efficient non-word-wrapped path if (!m_renderer->view()->dynWordWrap()) { int ret = virtualCursor.line() - work.line(); if (limitToVisible && (ret < 0 || ret > limit)) { return -1; } else { return ret; } } if (work == virtualCursor) { return 0; } int ret = -(int)viewLine(viewCacheStart()); bool forwards = (work < virtualCursor); // FIXME switch to using ranges? faster? if (forwards) { while (work.line() != virtualCursor.line()) { ret += viewLineCount(m_renderer->folding().visibleLineToLine(work.line())); work.setLine(work.line() + 1); if (limitToVisible && ret > limit) { return -1; } } } else { while (work.line() != virtualCursor.line()) { work.setLine(work.line() - 1); ret -= viewLineCount(m_renderer->folding().visibleLineToLine(work.line())); if (limitToVisible && ret < 0) { return -1; } } } // final difference KTextEditor::Cursor realCursor = virtualCursor; realCursor.setLine(m_renderer->folding().visibleLineToLine(realCursor.line())); if (realCursor.column() == -1) { realCursor.setColumn(m_renderer->doc()->lineLength(realCursor.line())); } ret += viewLine(realCursor); if (limitToVisible && (ret < 0 || ret > limit)) { return -1; } return ret; } int KateLayoutCache::lastViewLine(int realLine) { if (!m_renderer->view()->dynWordWrap()) { return 0; } KateLineLayoutPtr l = line(realLine); Q_ASSERT(l); return l->viewLineCount() - 1; } int KateLayoutCache::viewLineCount(int realLine) { return lastViewLine(realLine) + 1; } void KateLayoutCache::viewCacheDebugOutput() const { qCDebug(LOG_KTE) << "Printing values for " << m_textLayouts.count() << " lines:"; if (!m_textLayouts.isEmpty()) { foreach (const KateTextLayout &t, m_textLayouts) if (t.isValid()) { t.debugOutput(); } else { qCDebug(LOG_KTE) << "Line Invalid."; } } } void KateLayoutCache::wrapLine(const KTextEditor::Cursor &position) { m_lineLayouts.slotEditDone(position.line(), position.line() + 1, 1); } void KateLayoutCache::unwrapLine(int line) { m_lineLayouts.slotEditDone(line - 1, line, -1); } void KateLayoutCache::insertText(const KTextEditor::Cursor &position, const QString &) { m_lineLayouts.slotEditDone(position.line(), position.line(), 0); } void KateLayoutCache::removeText(const KTextEditor::Range &range) { m_lineLayouts.slotEditDone(range.start().line(), range.start().line(), 0); } void KateLayoutCache::clear() { m_textLayouts.clear(); m_lineLayouts.clear(); m_startPos = KTextEditor::Cursor(-1, -1); } void KateLayoutCache::setViewWidth(int width) { bool wider = width > m_viewWidth; m_viewWidth = width; m_lineLayouts.clear(); m_startPos = KTextEditor::Cursor(-1, -1); // Only get rid of layouts that we have to if (wider) { m_lineLayouts.viewWidthIncreased(); } else { m_lineLayouts.viewWidthDecreased(width); } } bool KateLayoutCache::wrap() const { return m_wrap; } void KateLayoutCache::setWrap(bool wrap) { m_wrap = wrap; clear(); } void KateLayoutCache::relayoutLines(int startRealLine, int endRealLine) { if (startRealLine > endRealLine) { qCWarning(LOG_KTE) << "start" << startRealLine << "before end" << endRealLine; } m_lineLayouts.relayoutLines(startRealLine, endRealLine); } bool KateLayoutCache::acceptDirtyLayouts() { return m_acceptDirtyLayouts; } void KateLayoutCache::setAcceptDirtyLayouts(bool accept) { m_acceptDirtyLayouts = accept; }