File indexing completed on 2024-04-28 04:37:17
0001 /* 0002 SPDX-FileCopyrightText: 2006 Adam Treat <treat@kde.org> 0003 SPDX-FileCopyrightText: 2007 Alexander Dymo <adymo@kdevelop.org> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "languagecontroller.h" 0009 0010 #include <algorithm> 0011 #include <utility> 0012 #include <vector> 0013 0014 #include <QHash> 0015 #include <QMimeDatabase> 0016 #include <QMultiHash> 0017 #include <QMutexLocker> 0018 #include <QRecursiveMutex> 0019 #include <QRegularExpression> 0020 #include <QThread> 0021 0022 #include <interfaces/idocument.h> 0023 #include <interfaces/idocumentcontroller.h> 0024 #include <interfaces/iplugin.h> 0025 #include <interfaces/iplugincontroller.h> 0026 #include <language/assistant/staticassistantsmanager.h> 0027 #include <language/interfaces/ilanguagesupport.h> 0028 #include <language/backgroundparser/backgroundparser.h> 0029 #include <language/duchain/duchain.h> 0030 0031 #include "problemmodelset.h" 0032 0033 #include "core.h" 0034 #include "settings/languagepreferences.h" 0035 #include "completionsettings.h" 0036 #include "debug.h" 0037 0038 namespace { 0039 QString KEY_SupportedMimeTypes() { return QStringLiteral("X-KDevelop-SupportedMimeTypes"); } 0040 QString KEY_ILanguageSupport() { return QStringLiteral("ILanguageSupport"); } 0041 0042 bool containsWildcardCharacter(QString::const_iterator first, QString::const_iterator last) 0043 { 0044 const auto isWildcardCharacter = [](QChar character) { 0045 const auto u = character.unicode(); 0046 return u == '*' || u == '?' || u == '['; 0047 }; 0048 return std::any_of(first, last, isWildcardCharacter); 0049 } 0050 0051 using KDevelop::ILanguageSupport; 0052 0053 class MimeTypeCache 0054 { 0055 public: 0056 void addMimeType(const QString& mimeTypeName, ILanguageSupport* language); 0057 QList<ILanguageSupport*> languagesForFileName(const QString& fileName) const; 0058 0059 private: 0060 void addGlobPattern(const QString& pattern, ILanguageSupport* language); 0061 0062 using StringLanguagePair = std::pair<QString, ILanguageSupport*>; 0063 0064 /// key() is the lower-cased last character of the suffix; value().first is e.g. ".cpp". 0065 /// The last character key performs better than the entire lower-cased (last) extension key. Perhaps 0066 /// because no lower-cased temporary fileName's extension string is created in languagesForFileName(). 0067 QMultiHash<QChar, StringLanguagePair> m_suffixes; 0068 std::vector<StringLanguagePair> m_literalPatterns; ///< contains e.g. "CMakeLists.txt" as front().first 0069 /// fallback, hopefully empty in practice 0070 std::vector<std::pair<QRegularExpression, ILanguageSupport*>> m_regularExpressions; 0071 }; 0072 0073 void MimeTypeCache::addMimeType(const QString& mimeTypeName, ILanguageSupport* language) 0074 { 0075 const QMimeType mime = QMimeDatabase().mimeTypeForName(mimeTypeName); 0076 if (!mime.isValid()) { 0077 qCWarning(SHELL) << "could not create mime-type" << mimeTypeName; 0078 return; 0079 } 0080 0081 const auto globPatterns = mime.globPatterns(); 0082 for (const QString& pattern : globPatterns) { 0083 addGlobPattern(pattern, language); 0084 } 0085 } 0086 0087 QList<ILanguageSupport*> MimeTypeCache::languagesForFileName(const QString& fileName) const 0088 { 0089 if (fileName.isEmpty()) { 0090 // The file name of a remote URL that ends with a slash is empty, but such a URL can still reference a file. 0091 // An empty file name cannot match a MIME type. 0092 return {}; 0093 } 0094 0095 QList<ILanguageSupport*> languages; 0096 // lastLanguageEquals() helps to improve performance by skipping checks for an already added language. 0097 const auto lastLanguageEquals = [&languages](const ILanguageSupport* lang) { 0098 return !languages.empty() && languages.constLast() == lang; 0099 }; 0100 0101 const auto lastChar = fileName.back().toLower(); 0102 for (auto it = m_suffixes.constFind(lastChar); it != m_suffixes.cend() && it.key() == lastChar; ++it) { 0103 const auto lang = it.value().second; 0104 if (!lastLanguageEquals(lang) && fileName.endsWith(it.value().first, Qt::CaseInsensitive)) { 0105 languages.push_back(lang); 0106 } 0107 } 0108 0109 for (const auto& p : m_literalPatterns) { 0110 if (fileName.compare(p.first, Qt::CaseInsensitive) == 0 && !lastLanguageEquals(p.second)) { 0111 languages.push_back(p.second); 0112 } 0113 } 0114 0115 for (const auto& p : m_regularExpressions) { 0116 if (!lastLanguageEquals(p.second) && p.first.match(fileName).hasMatch()) { 0117 languages.push_back(p.second); 0118 } 0119 } 0120 0121 return languages; 0122 } 0123 0124 void MimeTypeCache::addGlobPattern(const QString& pattern, ILanguageSupport* language) 0125 { 0126 if (pattern.isEmpty()) { 0127 qCWarning(SHELL) << "Attempt to add an invalid empty glob pattern."; 0128 return; // An empty pattern won't match a filename. 0129 } 0130 0131 if (pattern.front() == QLatin1Char{'*'}) { 0132 if (pattern.size() > 1 && !containsWildcardCharacter(pattern.cbegin() + 1, pattern.cend())) { 0133 const auto lastChar = pattern.back().toLower(); 0134 StringLanguagePair suffix{pattern.mid(1).toLower(), language}; 0135 if (!m_suffixes.contains(lastChar, suffix)) { 0136 m_suffixes.insert(lastChar, std::move(suffix)); 0137 } 0138 return; 0139 } 0140 } else if (!containsWildcardCharacter(pattern.cbegin(), pattern.cend())) { 0141 m_literalPatterns.emplace_back(pattern, language); 0142 return; 0143 } 0144 0145 QRegularExpression regularExpression(QRegularExpression::wildcardToRegularExpression(pattern), 0146 QRegularExpression::CaseInsensitiveOption); 0147 m_regularExpressions.emplace_back(std::move(regularExpression), language); 0148 } 0149 0150 } // namespace 0151 0152 namespace KDevelop { 0153 0154 0155 using LanguageHash = QHash<QString, ILanguageSupport*>; 0156 using LanguageCache = QHash<QString, QList<ILanguageSupport*>>; 0157 0158 class LanguageControllerPrivate 0159 { 0160 public: 0161 explicit LanguageControllerPrivate(LanguageController *controller) 0162 : backgroundParser(new BackgroundParser(controller)) 0163 , staticAssistantsManager(nullptr) 0164 , m_cleanedUp(false) 0165 , problemModelSet(new ProblemModelSet(controller)) 0166 {} 0167 0168 mutable QRecursiveMutex dataMutex; 0169 0170 LanguageHash languages; //Maps language-names to languages 0171 LanguageCache languageCache; //Maps mimetype-names to languages 0172 MimeTypeCache mimeTypeCache; //Maps mimetype-glob-patterns to languages 0173 0174 BackgroundParser* const backgroundParser; 0175 StaticAssistantsManager* staticAssistantsManager; 0176 bool m_cleanedUp; 0177 0178 void addLanguageSupport(ILanguageSupport* support, const QStringList& mimetypes); 0179 void addLanguageSupport(ILanguageSupport* support); 0180 0181 ProblemModelSet* const problemModelSet; 0182 }; 0183 0184 void LanguageControllerPrivate::addLanguageSupport(ILanguageSupport* languageSupport, 0185 const QStringList& mimetypes) 0186 { 0187 Q_ASSERT(!languages.contains(languageSupport->name())); 0188 languages.insert(languageSupport->name(), languageSupport); 0189 0190 for (const QString& mimeTypeName : mimetypes) { 0191 qCDebug(SHELL) << "adding supported mimetype:" << mimeTypeName << "language:" << languageSupport->name(); 0192 languageCache[mimeTypeName] << languageSupport; 0193 mimeTypeCache.addMimeType(mimeTypeName, languageSupport); 0194 } 0195 } 0196 0197 void LanguageControllerPrivate::addLanguageSupport(KDevelop::ILanguageSupport* languageSupport) 0198 { 0199 if (languages.contains(languageSupport->name())) 0200 return; 0201 0202 Q_ASSERT(dynamic_cast<IPlugin*>(languageSupport)); 0203 0204 KPluginMetaData info = Core::self()->pluginController()->pluginInfo(dynamic_cast<IPlugin*>(languageSupport)); 0205 const auto mimetypes = info.value(KEY_SupportedMimeTypes(), QStringList()); 0206 addLanguageSupport(languageSupport, mimetypes); 0207 } 0208 0209 LanguageController::LanguageController(QObject *parent) 0210 : ILanguageController(parent) 0211 , d_ptr(new LanguageControllerPrivate(this)) 0212 { 0213 setObjectName(QStringLiteral("LanguageController")); 0214 } 0215 0216 LanguageController::~LanguageController() = default; 0217 0218 void LanguageController::initialize() 0219 { 0220 Q_D(LanguageController); 0221 0222 d->languages = {}; 0223 d->languageCache = {}; 0224 d->mimeTypeCache = {}; 0225 0226 d->backgroundParser->loadSettings(); 0227 0228 delete d->staticAssistantsManager; 0229 d->staticAssistantsManager = new StaticAssistantsManager(this); 0230 0231 d->m_cleanedUp = false; 0232 0233 // make sure the DUChain is setup before we try to access it from different threads at the same time 0234 DUChain::self(); 0235 } 0236 0237 void LanguageController::cleanup() 0238 { 0239 Q_D(LanguageController); 0240 0241 QMutexLocker lock(&d->dataMutex); 0242 d->m_cleanedUp = true; 0243 } 0244 0245 StaticAssistantsManager* LanguageController::staticAssistantsManager() const 0246 { 0247 Q_D(const LanguageController); 0248 0249 return d->staticAssistantsManager; 0250 } 0251 0252 ICompletionSettings *LanguageController::completionSettings() const 0253 { 0254 return &CompletionSettings::self(); 0255 } 0256 0257 ProblemModelSet* LanguageController::problemModelSet() const 0258 { 0259 Q_D(const LanguageController); 0260 0261 return d->problemModelSet; 0262 } 0263 0264 QList<ILanguageSupport*> LanguageController::loadedLanguages() const 0265 { 0266 Q_D(const LanguageController); 0267 0268 QMutexLocker lock(&d->dataMutex); 0269 QList<ILanguageSupport*> ret; 0270 0271 if(d->m_cleanedUp) 0272 return ret; 0273 0274 ret.reserve(d->languages.size()); 0275 for (ILanguageSupport* lang : qAsConst(d->languages)) { 0276 ret << lang; 0277 } 0278 return ret; 0279 } 0280 0281 ILanguageSupport* LanguageController::language(const QString &name) const 0282 { 0283 Q_D(const LanguageController); 0284 0285 QMutexLocker lock(&d->dataMutex); 0286 0287 if(d->m_cleanedUp) 0288 return nullptr; 0289 0290 const auto languageIt = d->languages.constFind(name); 0291 if (languageIt != d->languages.constEnd()) 0292 return *languageIt; 0293 0294 // temporary support for deprecated-in-5.1 "X-KDevelop-Language" as fallback 0295 // remove in later version 0296 const QString keys[2] = { 0297 QStringLiteral("X-KDevelop-Languages"), 0298 QStringLiteral("X-KDevelop-Language") 0299 }; 0300 QList<IPlugin*> supports; 0301 for (const auto& key : keys) { 0302 QVariantMap constraints; 0303 constraints.insert(key, name); 0304 supports = Core::self()->pluginController()->allPluginsForExtension(KEY_ILanguageSupport(), constraints); 0305 if (key == keys[1]) { 0306 for (auto support : qAsConst(supports)) { 0307 qCWarning(SHELL) << "Plugin" << Core::self()->pluginController()->pluginInfo(support).name() << " has deprecated (since 5.1) metadata key \"X-KDevelop-Language\", needs porting to: \"X-KDevelop-Languages\": ["<<name<<"]'"; 0308 } 0309 } 0310 if (!supports.isEmpty()) { 0311 break; 0312 } 0313 } 0314 0315 if(!supports.isEmpty()) { 0316 auto *languageSupport = supports[0]->extension<ILanguageSupport>(); 0317 if(languageSupport) { 0318 const_cast<LanguageControllerPrivate*>(d)->addLanguageSupport(languageSupport); 0319 return languageSupport; 0320 } 0321 } 0322 return nullptr; 0323 } 0324 0325 QList<ILanguageSupport*> LanguageController::languagesForUrl(const QUrl &url) 0326 { 0327 if (url.isEmpty()) { 0328 // The URL of an unsaved document is empty. 0329 // The code below cannot find a language for an empty URL. 0330 return {}; 0331 } 0332 0333 Q_D(LanguageController); 0334 0335 QMutexLocker lock(&d->dataMutex); 0336 0337 if(d->m_cleanedUp) 0338 return {}; 0339 0340 ///non-crashy part: Use the mime-types of known languages 0341 auto languages = d->mimeTypeCache.languagesForFileName(url.fileName()); 0342 0343 //Never use findByUrl from within a background thread, and never load a language support 0344 //from within the backgruond thread. Both is unsafe, and can lead to crashes 0345 if(!languages.isEmpty() || QThread::currentThread() != thread()) 0346 return languages; 0347 0348 QMimeType mimeType; 0349 0350 if (url.isLocalFile()) { 0351 mimeType = QMimeDatabase().mimeTypeForFile(url.toLocalFile()); 0352 } else { 0353 // remote file, only look at the extension 0354 mimeType = QMimeDatabase().mimeTypeForUrl(url); 0355 } 0356 if (mimeType.isDefault()) { 0357 // ask the document controller about a more concrete mimetype 0358 IDocument* doc = ICore::self()->documentController()->documentForUrl(url); 0359 if (doc) { 0360 mimeType = doc->mimeType(); 0361 } 0362 } 0363 0364 languages = languagesForMimetype(mimeType.name()); 0365 0366 return languages; 0367 } 0368 0369 QList<ILanguageSupport*> LanguageController::languagesForMimetype(const QString& mimetype) 0370 { 0371 Q_D(LanguageController); 0372 0373 QMutexLocker lock(&d->dataMutex); 0374 0375 QList<ILanguageSupport*> languages; 0376 LanguageCache::ConstIterator it = d->languageCache.constFind(mimetype); 0377 if (it != d->languageCache.constEnd()) { 0378 languages = it.value(); 0379 } else { 0380 QVariantMap constraints; 0381 constraints.insert(KEY_SupportedMimeTypes(), mimetype); 0382 const QList<IPlugin*> supports = Core::self()->pluginController()->allPluginsForExtension(KEY_ILanguageSupport(), constraints); 0383 0384 if (supports.isEmpty()) { 0385 qCDebug(SHELL) << "no languages for mimetype:" << mimetype; 0386 d->languageCache.insert(mimetype, QList<ILanguageSupport*>()); 0387 } else { 0388 for (IPlugin *support : supports) { 0389 auto* languageSupport = support->extension<ILanguageSupport>(); 0390 qCDebug(SHELL) << "language-support:" << languageSupport; 0391 if(languageSupport) { 0392 d->addLanguageSupport(languageSupport); 0393 languages << languageSupport; 0394 } 0395 } 0396 } 0397 } 0398 return languages; 0399 } 0400 0401 QList<QString> LanguageController::mimetypesForLanguageName(const QString& languageName) 0402 { 0403 Q_D(LanguageController); 0404 0405 QMutexLocker lock(&d->dataMutex); 0406 0407 QList<QString> mimetypes; 0408 for (LanguageCache::ConstIterator iter = d->languageCache.constBegin(); iter != d->languageCache.constEnd(); ++iter) { 0409 bool isFromLanguage = std::any_of(iter.value().begin(), iter.value().end(), [&] (ILanguageSupport* language ) { 0410 return (language->name() == languageName); 0411 }); 0412 if (isFromLanguage) { 0413 mimetypes << iter.key(); 0414 } 0415 } 0416 return mimetypes; 0417 } 0418 0419 BackgroundParser *LanguageController::backgroundParser() const 0420 { 0421 Q_D(const LanguageController); 0422 0423 return d->backgroundParser; 0424 } 0425 0426 void LanguageController::addLanguageSupport(ILanguageSupport* languageSupport, const QStringList& mimetypes) 0427 { 0428 Q_D(LanguageController); 0429 0430 d->addLanguageSupport(languageSupport, mimetypes); 0431 } 0432 0433 } 0434 0435 #include "moc_languagecontroller.cpp"