sailfish-safe

Sailfish frontend for safe(1)
git clone git://git.z3bra.org/sailfish-safe.git
Log | Files | Refs | README | LICENSE

abbreviations.cpp (7509B)


      1 /*
      2  * Borrowed from KDevelop (kdevplatform/language/interfaces/abbreviations.cpp)
      3  *
      4  * Copyright 2014 Sven Brauch <svenbrauch@gmail.com>
      5  *
      6  * This library is free software; you can redistribute it and/or
      7  * modify it under the terms of the GNU Library General Public
      8  * License as published by the Free Software Foundation; either
      9  * version 2 of the License, or (at your option) any later version.
     10  *
     11  * This library is distributed in the hope that it will be useful,
     12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
     13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     14  * Library General Public License for more details.
     15  *
     16  * You should have received a copy of the GNU Library General Public License
     17  * along with this library; see the file COPYING.LIB.  If not, write to
     18  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
     19  * Boston, MA 02110-1301, USA.
     20  */
     21 
     22 #include "abbreviations.h"
     23 
     24 #include <QStringList>
     25 #include <QVarLengthArray>
     26 
     27 namespace {
     28 
     29 // Taken and adapted for kdevelop from katecompletionmodel.cpp
     30 static bool matchesAbbreviationHelper(const QStringRef &word, const QStringRef &typed,
     31                                       const QVarLengthArray< int, 32 > &offsets,
     32                                       int &depth, int atWord = -1, int i = 0)
     33 {
     34     int atLetter = 1;
     35     for ( ; i < typed.size(); i++ ) {
     36         const QChar c = typed.at(i).toLower();
     37         bool haveNextWord = offsets.size() > atWord + 1;
     38         bool canCompare = atWord != -1 && word.size() > offsets.at(atWord) + atLetter;
     39         if (canCompare && c == word.at(offsets.at(atWord) + atLetter).toLower()) {
     40             // the typed letter matches a letter after the current word beginning
     41             if (!haveNextWord || c != word.at(offsets.at(atWord + 1)).toLower()) {
     42                 // good, simple case, no conflict
     43                 atLetter += 1;
     44                 continue;
     45             }
     46             // For maliciously crafted data, the code used here theoretically can have very high
     47             // complexity. Thus ensure we don't run into this case, by limiting the amount of branches
     48             // we walk through to 128.
     49             depth++;
     50             if (depth > 128) {
     51                 return false;
     52             }
     53             // the letter matches both the next word beginning and the next character in the word
     54             if (haveNextWord && matchesAbbreviationHelper(word, typed, offsets, depth, atWord + 1, i + 1)) {
     55                 // resolving the conflict by taking the next word's first character worked, fine
     56                 return true;
     57             }
     58             // otherwise, continue by taking the next letter in the current word.
     59             atLetter += 1;
     60             continue;
     61         } else if (haveNextWord && c == word.at(offsets.at(atWord + 1)).toLower()) {
     62             // the typed letter matches the next word beginning
     63             atWord++;
     64             atLetter = 1;
     65             continue;
     66         }
     67         // no match
     68         return false;
     69     }
     70     // all characters of the typed word were matched
     71     return true;
     72 }
     73 
     74 }
     75 
     76 bool PlasmaPass::matchesAbbreviation(const QStringRef &word, const QStringRef &typed)
     77 {
     78     // A mismatch is very likely for random even for the first letter,
     79     // thus this optimization makes sense.
     80     if (word.at(0).toLower() != typed.at(0).toLower()) {
     81         return false;
     82     }
     83 
     84     // First, check if all letters are contained in the word in the right order.
     85     int atLetter = 0;
     86     for (const auto c : typed) {
     87         while (c.toLower() != word.at(atLetter).toLower()) {
     88             atLetter += 1;
     89             if (atLetter >= word.size()) {
     90                 return false;
     91             }
     92         }
     93     }
     94 
     95     bool haveUnderscore = true;
     96     QVarLengthArray<int, 32> offsets;
     97     // We want to make "KComplM" match "KateCompletionModel"; this means we need
     98     // to allow parts of the typed text to be not part of the actual abbreviation,
     99     // which consists only of the uppercased / underscored letters (so "KCM" in this case).
    100     // However it might be ambigous whether a letter is part of such a word or part of
    101     // the following abbreviation, so we need to find all possible word offsets first,
    102     // then compare.
    103     for (int i = 0; i < word.size(); ++i) {
    104         const QChar c = word.at(i);
    105         if (c == QLatin1Char('_') || c == QLatin1Char('-')) {
    106             haveUnderscore = true;
    107         } else if (haveUnderscore || c.isUpper()) {
    108             offsets.append(i);
    109             haveUnderscore = false;
    110         }
    111     }
    112     int depth = 0;
    113     return matchesAbbreviationHelper(word, typed, offsets, depth);
    114 }
    115 
    116 bool PlasmaPass::matchesPath(const QStringRef &path, const QStringRef &typed)
    117 {
    118     int consumed = 0;
    119     int pos = 0;
    120     // try to find all the characters in typed in the right order in the path;
    121     // jumps are allowed everywhere
    122     while (consumed < typed.size() && pos < path.size()) {
    123         if (typed.at(consumed).toLower() == path.at(pos).toLower()) {
    124             consumed++;
    125         }
    126         pos++;
    127     }
    128     return consumed == typed.size();
    129 }
    130 
    131 int PlasmaPass::matchPathFilter(const QVector<QStringRef> &toFilter, const QVector<QStringRef> &text)
    132 {
    133     enum PathFilterMatchQuality {
    134         NoMatch = -1,
    135         ExactMatch = 0,
    136         StartMatch = 1,
    137         OtherMatch = 2 // and anything higher than that
    138     };
    139     const auto segments = toFilter;
    140 
    141     if (text.count() > segments.count()) {
    142         // number of segments mismatches, thus item cannot match
    143         return NoMatch;
    144     }
    145 
    146     bool allMatched = true;
    147     int searchIndex = text.size() - 1;
    148     int pathIndex = segments.size() - 1;
    149     int lastMatchIndex = -1;
    150     // stop early if more search fragments remain than available after path index
    151     while (pathIndex >= 0 && searchIndex >= 0
    152             && (pathIndex + text.size() - searchIndex - 1) < segments.size())
    153     {
    154         const auto &segment = segments.at(pathIndex);
    155         const auto &typedSegment = text.at(searchIndex);
    156         const int matchIndex = segment.indexOf(typedSegment, 0, Qt::CaseInsensitive);
    157         const bool isLastPathSegment = pathIndex == segments.size() - 1;
    158         const bool isLastSearchSegment = searchIndex == text.size() - 1;
    159 
    160         // check for exact matches
    161         allMatched &= matchIndex == 0 && segment.size() == typedSegment.size();
    162 
    163         // check for fuzzy matches
    164         bool isMatch = matchIndex != -1;
    165         // do fuzzy path matching on the last segment
    166         if (!isMatch && isLastPathSegment && isLastSearchSegment) {
    167             isMatch = matchesPath(segment, typedSegment);
    168         } else if (!isMatch) { // check other segments for abbreviations
    169             isMatch = matchesAbbreviation(segment.mid(0), typedSegment);
    170         }
    171 
    172         if (!isMatch) {
    173             // no match, try with next path segment
    174             --pathIndex;
    175             continue;
    176         }
    177         // else we matched
    178         if (isLastPathSegment) {
    179             lastMatchIndex = matchIndex;
    180         }
    181         --searchIndex;
    182         --pathIndex;
    183     }
    184 
    185     if (searchIndex != -1) {
    186         return NoMatch;
    187     }
    188 
    189     const int segmentMatchDistance = segments.size() - (pathIndex + 1);
    190 
    191     if (allMatched) {
    192         return ExactMatch;
    193     } else if (lastMatchIndex == 0) {
    194         // prefer matches whose last element starts with the filter
    195         return StartMatch;
    196     } else {
    197         // prefer matches closer to the end of the path
    198         return OtherMatch + segmentMatchDistance;
    199     }
    200 }