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 }