File indexing completed on 2024-05-05 16:41:32
0001 /* 0002 SPDX-FileCopyrightText: 2011-2012 Sven Brauch <svenbrauch@googlemail.com> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 // Note to confused people reading this code: This is not the parser. 0008 // It's just a minimalist helper class for code completion. The parser is in the parser/ directory. 0009 0010 #include "helpers.h" 0011 0012 #include <language/duchain/abstractfunctiondeclaration.h> 0013 #include <language/duchain/duchainutils.h> 0014 #include <language/duchain/ducontext.h> 0015 #include <language/duchain/declaration.h> 0016 #include <language/duchain/types/functiontype.h> 0017 #include <language/duchain/types/integraltype.h> 0018 #include <language/codecompletion/normaldeclarationcompletionitem.h> 0019 0020 #include <QStringList> 0021 #include <QTextFormat> 0022 0023 #include <QDebug> 0024 #include "codecompletiondebug.h" 0025 0026 #include "duchain/declarations/functiondeclaration.h" 0027 #include "parser/codehelpers.h" 0028 0029 using namespace KDevelop; 0030 0031 namespace Python { 0032 0033 QString camelCaseToUnderscore(const QString& camelCase) 0034 { 0035 QString underscore; 0036 for ( int i = 0; i < camelCase.size(); i++ ) { 0037 const QChar& c = camelCase.at(i); 0038 if ( c.isUpper() && i != 0 ) { 0039 underscore.append('_'); 0040 } 0041 underscore.append(c.toLower()); 0042 } 0043 return underscore; 0044 } 0045 0046 int identifierMatchQuality(const QString& identifier1_, const QString& identifier2_) 0047 { 0048 QString identifier1 = camelCaseToUnderscore(identifier1_).toLower().replace('.', '_'); 0049 QString identifier2 = camelCaseToUnderscore(identifier2_).toLower().replace('.', '_'); 0050 0051 if ( identifier1 == identifier2 ) { 0052 return 3; 0053 } 0054 if ( identifier1.contains(identifier2) || identifier2.contains(identifier1) ) { 0055 return 2; 0056 } 0057 QStringList parts1 = identifier1.split('_'); 0058 QStringList parts2 = identifier2.split('_'); 0059 parts1.removeAll(""); 0060 parts2.removeAll(""); 0061 parts1.removeDuplicates(); 0062 parts2.removeDuplicates(); 0063 if ( parts1.length() > 5 || parts2.length() > 5 ) { 0064 // don't waste time comparing huge identifiers, 0065 // the matching is probably pointless anyways for people using 0066 // more than 5 words for their variable names 0067 return 0; 0068 } 0069 foreach ( const QString& part1, parts1 ) { 0070 foreach ( const QString& part2, parts2 ) { 0071 // Don't take very short name parts into account, 0072 // those are not very descriptive eventually 0073 if ( part1.size() < 3 || part2.size() < 3 ) { 0074 continue; 0075 } 0076 if ( part1 == part2 ) { 0077 // partial match 0078 return 1; 0079 } 0080 } 0081 } 0082 return 0; 0083 } 0084 0085 typedef QPair<QString, ExpressionParser::Status> keyword; 0086 0087 static QList<keyword> supportedKeywords; 0088 static QList<keyword> controlChars; 0089 static QList<QString> miscKeywords; 0090 static QList<QString> noCompletionKeywords; 0091 static QMutex keywordPopulationLock; 0092 0093 // Keywords known to me: 0094 // and del for is raise 0095 // assert elif from lambda return 0096 // break else global not try 0097 // class except if or while 0098 // continue exec import pass yield 0099 // def finally in print with 0100 // async await 0101 0102 ExpressionParser::ExpressionParser(QString code) 0103 : m_code(code) 0104 , m_cursorPositionInString(m_code.length()) 0105 { 0106 keywordPopulationLock.lock(); 0107 if ( supportedKeywords.isEmpty() ) { 0108 noCompletionKeywords << "break" << "class" << "continue" << "pass" << "try" 0109 << "else" << "as" << "finally" << "global" << "lambda"; 0110 miscKeywords << "and" << "assert" << "del" << "elif" << "exec" << "if" << "is" << "not" 0111 << "or" << "print" << "return" << "while" << "yield" << "with" << "await"; 0112 supportedKeywords << keyword("import", ExpressionParser::ImportFound); 0113 supportedKeywords << keyword("from", ExpressionParser::FromFound); 0114 supportedKeywords << keyword("raise", ExpressionParser::RaiseFound); 0115 supportedKeywords << keyword("in", ExpressionParser::InFound); 0116 supportedKeywords << keyword("for", ExpressionParser::ForFound); 0117 supportedKeywords << keyword("class", ExpressionParser::ClassFound); 0118 supportedKeywords << keyword("def", ExpressionParser::DefFound); 0119 supportedKeywords << keyword("except", ExpressionParser::ExceptFound); 0120 controlChars << keyword(":", ExpressionParser::ColonFound); 0121 controlChars << keyword(",", ExpressionParser::CommaFound); 0122 controlChars << keyword("(", ExpressionParser::InitializerFound); 0123 controlChars << keyword("{", ExpressionParser::InitializerFound); 0124 controlChars << keyword("[", ExpressionParser::InitializerFound); 0125 controlChars << keyword(".", ExpressionParser::MemberAccessFound); 0126 controlChars << keyword("=", ExpressionParser::EqualsFound); 0127 } 0128 keywordPopulationLock.unlock(); 0129 } 0130 0131 QString ExpressionParser::getRemainingCode() 0132 { 0133 return m_code.mid(0, m_cursorPositionInString); 0134 } 0135 0136 QString ExpressionParser::getScannedCode() 0137 { 0138 return m_code.mid(m_cursorPositionInString, m_code.length() - m_cursorPositionInString); 0139 } 0140 0141 int ExpressionParser::trailingWhitespace() 0142 { 0143 int ws = 0; 0144 int index = m_cursorPositionInString - 1; 0145 while ( index >= 0 ) { 0146 if ( m_code.at(index).isSpace() ) { 0147 ws++; 0148 index --; 0149 } 0150 else { 0151 break; 0152 } 0153 } 0154 return ws; 0155 } 0156 0157 void ExpressionParser::reset() 0158 { 0159 m_cursorPositionInString = m_code.length(); 0160 } 0161 0162 QString ExpressionParser::skipUntilStatus(ExpressionParser::Status requestedStatus, bool* ok, int* expressionsSkipped) 0163 { 0164 if ( expressionsSkipped ) { 0165 *expressionsSkipped = 0; 0166 } 0167 QString lastExpression; 0168 Status currentStatus = InvalidStatus; 0169 while ( currentStatus != requestedStatus ) { 0170 lastExpression = popExpression(¤tStatus); 0171 qCDebug(KDEV_PYTHON_CODECOMPLETION) << lastExpression << currentStatus; 0172 if ( currentStatus == NothingFound ) { 0173 *ok = ( requestedStatus == NothingFound ); // ok exactly if the caller requested NothingFound as end status 0174 return QString(); 0175 } 0176 if ( expressionsSkipped && currentStatus == ExpressionFound ) { 0177 *expressionsSkipped += 1; 0178 } 0179 } 0180 *ok = true; 0181 return lastExpression; 0182 } 0183 0184 TokenList ExpressionParser::popAll() 0185 { 0186 Status currentStatus = InvalidStatus; 0187 TokenList items; 0188 while ( currentStatus != NothingFound ) { 0189 QString result = popExpression(¤tStatus); 0190 items << TokenListEntry(currentStatus, result, m_cursorPositionInString); 0191 } 0192 std::reverse(items.begin(), items.end()); 0193 return items; 0194 } 0195 0196 bool endsWithSeperatedKeyword(const QString& str, const QString& shouldEndWith) { 0197 bool endsWith = str.endsWith(shouldEndWith); 0198 if ( ! endsWith ) { 0199 return false; 0200 } 0201 int l = shouldEndWith.length(); 0202 if ( str.length() == l ) { 0203 return true; 0204 } 0205 if ( str.right(l + 1).at(0).isSpace() ) { 0206 return true; 0207 } 0208 return false; 0209 } 0210 0211 QString ExpressionParser::popExpression(ExpressionParser::Status* status) 0212 { 0213 const auto remaining = getRemainingCode(); 0214 auto trimmed = remaining.trimmed(); 0215 auto operatingOn = trimmed.replace('\t', ' '); 0216 bool lineIsEmpty = false; 0217 for ( auto it = remaining.constEnd()-1; it != remaining.constEnd(); it-- ) { 0218 if ( ! it->isSpace() ) { 0219 break; 0220 } 0221 if ( *it == '\n' ) { 0222 lineIsEmpty = true; 0223 break; 0224 } 0225 } 0226 if ( operatingOn.isEmpty() || lineIsEmpty ) { 0227 m_cursorPositionInString = 0; 0228 *status = NothingFound; 0229 return QString(); 0230 } 0231 bool lastCharIsSpace = getRemainingCode().right(1).at(0).isSpace(); 0232 m_cursorPositionInString -= trailingWhitespace(); 0233 if ( operatingOn.endsWith('(') ) { 0234 qCDebug(KDEV_PYTHON_CODECOMPLETION) << "eventual call found"; 0235 m_cursorPositionInString -= 1; 0236 *status = EventualCallFound; 0237 return QString(); 0238 } 0239 foreach ( const keyword& kw, controlChars ) { 0240 if ( operatingOn.endsWith(kw.first) ) { 0241 m_cursorPositionInString -= kw.first.length(); 0242 *status = kw.second; 0243 return QString(); 0244 } 0245 } 0246 if ( lastCharIsSpace ) { 0247 foreach ( const keyword& kw, supportedKeywords ) { 0248 if ( endsWithSeperatedKeyword(operatingOn, kw.first) ) { 0249 m_cursorPositionInString -= kw.first.length(); 0250 *status = kw.second; 0251 return QString(); 0252 } 0253 } 0254 foreach ( const QString& kw, miscKeywords ) { 0255 if ( endsWithSeperatedKeyword(operatingOn, kw) ) { 0256 m_cursorPositionInString -= kw.length(); 0257 *status = MeaninglessKeywordFound; 0258 return QString(); 0259 } 0260 } 0261 foreach ( const QString& kw, noCompletionKeywords ) { 0262 if ( endsWithSeperatedKeyword(operatingOn, kw) ) { 0263 m_cursorPositionInString -= kw.length(); 0264 *status = NoCompletionKeywordFound; 0265 return QString(); 0266 } 0267 } 0268 } 0269 // Otherwise, there's a real expression at the cursor, so scan it. 0270 QStringList lines = operatingOn.split('\n'); 0271 Python::TrivialLazyLineFetcher f(lines); 0272 int lastLine = lines.length()-1; 0273 KTextEditor::Cursor startCursor; 0274 QString expr = CodeHelpers::expressionUnderCursor(f, KTextEditor::Cursor(lastLine, f.fetchLine(lastLine).length() - 1), 0275 startCursor, true); 0276 if ( expr.isEmpty() ) { 0277 *status = NothingFound; 0278 } 0279 else { 0280 *status = ExpressionFound; 0281 } 0282 m_cursorPositionInString -= expr.length(); 0283 return expr; 0284 } 0285 0286 0287 // This is stolen from PHP. For credits, see helpers.cpp in PHP. 0288 void createArgumentList(Declaration* dec_, QString& ret, QList< QVariant >* highlighting, int atArg, bool includeTypes) 0289 { 0290 auto dec = dynamic_cast<Python::FunctionDeclaration*>(dec_); 0291 if ( ! dec ) { 0292 return; 0293 } 0294 int textFormatStart = 0; 0295 QTextFormat normalFormat(QTextFormat::CharFormat); 0296 QTextFormat highlightFormat(QTextFormat::CharFormat); 0297 highlightFormat.setBackground(QColor::fromRgb(142, 186, 255)); 0298 highlightFormat.setProperty(QTextFormat::FontWeight, 99); 0299 0300 AbstractFunctionDeclaration* decl = dynamic_cast<AbstractFunctionDeclaration*>(dec); 0301 FunctionType::Ptr functionType = dec->type<FunctionType>(); 0302 0303 if (functionType && decl) { 0304 0305 QVector<Declaration*> parameters; 0306 if (DUChainUtils::argumentContext(dec)) 0307 parameters = DUChainUtils::argumentContext(dec)->localDeclarations(); 0308 0309 ret = '('; 0310 bool first = true; 0311 int num = 0; 0312 0313 bool skipFirst = false; 0314 if ( dec->context() && dec->context()->type() == DUContext::Class && ! dec->isStatic() ) { 0315 // the function is a class method, and its first argument is "self". Don't display that. 0316 skipFirst = true; 0317 } 0318 0319 uint defaultParamNum = 0; 0320 int firstDefaultParam = parameters.count() - decl->defaultParametersSize() - skipFirst; 0321 0322 // disable highlighting when in default arguments, it doesn't make much sense then 0323 bool disableHighlighting = false; 0324 0325 foreach(Declaration* dec, parameters) { 0326 if ( skipFirst ) { 0327 skipFirst = false; 0328 continue; 0329 } 0330 // that has nothing to do with the skip, it's just for the comma 0331 if (first) 0332 first = false; 0333 else 0334 ret += ", "; 0335 0336 bool doHighlight = false; 0337 QTextFormat doFormat; 0338 0339 if ( num == atArg - 1 ) 0340 doFormat = highlightFormat; 0341 else 0342 doFormat = normalFormat; 0343 0344 if ( num == firstDefaultParam ) { 0345 ret += "["; 0346 ++defaultParamNum; 0347 disableHighlighting = true; 0348 } 0349 0350 if ( ! disableHighlighting ) { 0351 doHighlight = true; 0352 } 0353 0354 if ( includeTypes ) { 0355 if (num < functionType->arguments().count()) { 0356 if (AbstractType::Ptr type = functionType->arguments().at(num)) { 0357 if ( type->toString() != "<unknown>" ) { 0358 ret += type->toString() + ' '; 0359 } 0360 } 0361 } 0362 0363 if (doHighlight) { 0364 if (highlighting && ret.length() != textFormatStart) { 0365 //Add a default-highlighting for the passed text 0366 *highlighting << QVariant(textFormatStart); 0367 *highlighting << QVariant(ret.length() - textFormatStart); 0368 *highlighting << QVariant(normalFormat); 0369 textFormatStart = ret.length(); 0370 } 0371 } 0372 } 0373 0374 0375 ret += dec->identifier().toString(); 0376 0377 if (doHighlight) { 0378 if (highlighting && ret.length() != textFormatStart) { 0379 *highlighting << QVariant(textFormatStart + 1); 0380 *highlighting << QVariant(ret.length() - textFormatStart - 1); 0381 *highlighting << doFormat; 0382 textFormatStart = ret.length(); 0383 } 0384 } 0385 0386 ++num; 0387 } 0388 if ( defaultParamNum != 0 ) { 0389 ret += "]"; 0390 } 0391 ret += ')'; 0392 0393 if (highlighting && ret.length() != textFormatStart) { 0394 *highlighting << QVariant(textFormatStart); 0395 *highlighting << QVariant(ret.length()); 0396 *highlighting << normalFormat; 0397 textFormatStart = ret.length(); 0398 } 0399 return; 0400 } 0401 } 0402 0403 StringFormatter::StringFormatter(const QString &string) 0404 : m_string(string) 0405 { 0406 qCDebug(KDEV_PYTHON_CODECOMPLETION) << "String being parsed: " << string; 0407 QRegExp regex("\\{(\\w+)(?:!([rs]))?(?:\\:(.*))?\\}"); 0408 regex.setMinimal(true); 0409 int pos = 0; 0410 while ( (pos = regex.indexIn(string, pos)) != -1 ) { 0411 QString identifier = regex.cap(1); 0412 QString conversionStr = regex.cap(2); 0413 QChar conversion = (conversionStr.isNull() || conversionStr.isEmpty()) ? QChar() : conversionStr.at(0); 0414 QString formatSpec = regex.cap(3); 0415 0416 qCDebug(KDEV_PYTHON_CODECOMPLETION) << "variable: " << regex.cap(0); 0417 0418 // The regex guarantees that conversion is only a single character 0419 ReplacementVariable variable(identifier, conversion, formatSpec); 0420 m_replacementVariables.append(variable); 0421 0422 RangeInString variablePosition(pos, pos + regex.matchedLength()); 0423 m_variablePositions.append(variablePosition); 0424 0425 pos += regex.matchedLength(); 0426 } 0427 } 0428 0429 bool StringFormatter::isInsideReplacementVariable(int cursorPosition) const 0430 { 0431 return getReplacementVariable(cursorPosition) != nullptr; 0432 } 0433 0434 const ReplacementVariable *StringFormatter::getReplacementVariable(int cursorPosition) const 0435 { 0436 int index = 0; 0437 foreach ( const RangeInString &variablePosition, m_variablePositions ) { 0438 if ( cursorPosition >= variablePosition.beginIndex && cursorPosition <= variablePosition.endIndex ) { 0439 return &m_replacementVariables.at(index); 0440 } 0441 index++; 0442 } 0443 0444 return nullptr; 0445 } 0446 0447 RangeInString StringFormatter::getVariablePosition(int cursorPosition) const 0448 { 0449 int index = 0; 0450 foreach ( const RangeInString &variablePosition, m_variablePositions ) { 0451 if ( cursorPosition >= variablePosition.beginIndex && cursorPosition <= variablePosition.endIndex ) { 0452 return m_variablePositions.at(index); 0453 } 0454 index++; 0455 } 0456 return RangeInString(); 0457 } 0458 0459 int StringFormatter::nextIdentifierId() const 0460 { 0461 int highestIdFound = -1; 0462 foreach ( const ReplacementVariable &variable, m_replacementVariables ) { 0463 bool isNumeric; 0464 int identifier = variable.identifier().toInt(&isNumeric); 0465 if ( isNumeric && identifier > highestIdFound ) { 0466 highestIdFound = identifier; 0467 } 0468 } 0469 return highestIdFound + 1; 0470 } 0471 0472 }