File indexing completed on 2024-04-14 03:55:20
0001 /* 0002 SPDX-FileCopyrightText: 2005 Christoph Cullmann <cullmann@kde.org> 0003 SPDX-FileCopyrightText: 2005 Joseph Wenninger <jowenn@kde.org> 0004 SPDX-FileCopyrightText: 2006-2018 Dominik Haumann <dhaumann@kde.org> 0005 SPDX-FileCopyrightText: 2008 Paul Giannaros <paul@giannaros.org> 0006 SPDX-FileCopyrightText: 2010 Joseph Wenninger <jowenn@kde.org> 0007 0008 SPDX-License-Identifier: LGPL-2.0-or-later 0009 */ 0010 0011 #include "katescriptmanager.h" 0012 0013 #include <ktexteditor_version.h> 0014 0015 #include <QDir> 0016 #include <QFile> 0017 #include <QFileInfo> 0018 #include <QJsonArray> 0019 #include <QJsonDocument> 0020 #include <QJsonObject> 0021 #include <QJsonValue> 0022 #include <QRegularExpression> 0023 #include <QStringList> 0024 #include <QUuid> 0025 0026 #include <KConfig> 0027 #include <KConfigGroup> 0028 #include <KLocalizedString> 0029 0030 #include "katecmd.h" 0031 #include "katecommandlinescript.h" 0032 #include "kateglobal.h" 0033 #include "kateindentscript.h" 0034 #include "katepartdebug.h" 0035 0036 KateScriptManager *KateScriptManager::m_instance = nullptr; 0037 0038 KateScriptManager::KateScriptManager() 0039 : KTextEditor::Command({QStringLiteral("reload-scripts")}) 0040 { 0041 // use cached info 0042 collect(); 0043 } 0044 0045 KateScriptManager::~KateScriptManager() 0046 { 0047 qDeleteAll(m_indentationScripts); 0048 qDeleteAll(m_commandLineScripts); 0049 m_instance = nullptr; 0050 } 0051 0052 KateIndentScript *KateScriptManager::indenter(const QString &language) 0053 { 0054 KateIndentScript *highestPriorityIndenter = nullptr; 0055 const auto indenters = m_languageToIndenters.value(language.toLower()); 0056 for (KateIndentScript *indenter : indenters) { 0057 // don't overwrite if there is already a result with a higher priority 0058 if (highestPriorityIndenter && indenter->indentHeader().priority() < highestPriorityIndenter->indentHeader().priority()) { 0059 #ifdef DEBUG_SCRIPTMANAGER 0060 qCDebug(LOG_KTE) << "Not overwriting indenter for" << language << "as the priority isn't big enough (" << indenter->indentHeader().priority() << '<' 0061 << highestPriorityIndenter->indentHeader().priority() << ')'; 0062 #endif 0063 } else { 0064 highestPriorityIndenter = indenter; 0065 } 0066 } 0067 0068 #ifdef DEBUG_SCRIPTMANAGER 0069 if (highestPriorityIndenter) { 0070 qCDebug(LOG_KTE) << "Found indenter" << highestPriorityIndenter->url() << "for" << language; 0071 } else { 0072 qCDebug(LOG_KTE) << "No indenter for" << language; 0073 } 0074 #endif 0075 0076 return highestPriorityIndenter; 0077 } 0078 0079 /** 0080 * Small helper: QJsonValue to QStringList 0081 */ 0082 static QStringList jsonToStringList(const QJsonValue &value) 0083 { 0084 QStringList list; 0085 0086 const auto array = value.toArray(); 0087 for (const QJsonValue &value : array) { 0088 if (value.isString()) { 0089 list.append(value.toString()); 0090 } 0091 } 0092 0093 return list; 0094 } 0095 0096 void KateScriptManager::collect() 0097 { 0098 // clear out the old scripts and reserve enough space 0099 qDeleteAll(m_indentationScripts); 0100 qDeleteAll(m_commandLineScripts); 0101 m_indentationScripts.clear(); 0102 m_commandLineScripts.clear(); 0103 0104 m_languageToIndenters.clear(); 0105 m_indentationScriptMap.clear(); 0106 0107 // now, we search all kinds of known scripts 0108 for (const auto &type : {QLatin1String("indentation"), QLatin1String("commands")}) { 0109 // basedir for filesystem lookup 0110 const QString basedir = QLatin1String("/katepart5/script/") + type; 0111 0112 QStringList dirs; 0113 0114 // first writable locations, e.g. stuff the user has provided 0115 dirs += QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + basedir; 0116 0117 // then resources, e.g. the stuff we ship with us 0118 dirs.append(QLatin1String(":/ktexteditor/script/") + type); 0119 0120 // then all other locations, this includes global stuff installed by other applications 0121 // this will not allow global stuff to overwrite the stuff we ship in our resources to allow to install a more up-to-date ktexteditor lib locally! 0122 const auto genericDataDirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); 0123 for (const QString &dir : genericDataDirs) { 0124 dirs.append(dir + basedir); 0125 } 0126 0127 QStringList list; 0128 for (const QString &dir : std::as_const(dirs)) { 0129 const QStringList fileNames = QDir(dir).entryList({QStringLiteral("*.js")}); 0130 for (const QString &file : std::as_const(fileNames)) { 0131 list.append(dir + QLatin1Char('/') + file); 0132 } 0133 } 0134 0135 // iterate through the files and read info out of cache or file, no double loading of same scripts 0136 QSet<QString> unique; 0137 for (const QString &fileName : std::as_const(list)) { 0138 // get file basename 0139 const QString baseName = QFileInfo(fileName).baseName(); 0140 0141 // only load scripts once, even if multiple installed variants found! 0142 if (unique.contains(baseName)) { 0143 continue; 0144 } 0145 0146 // remember the script 0147 unique.insert(baseName); 0148 0149 // open file or skip it 0150 QFile file(fileName); 0151 if (!file.open(QIODevice::ReadOnly)) { 0152 qCDebug(LOG_KTE) << "Script parse error: Cannot open file " << qPrintable(fileName) << '\n'; 0153 continue; 0154 } 0155 0156 // search json header or skip this file 0157 QByteArray fileContent = file.readAll(); 0158 int startOfJson = fileContent.indexOf('{'); 0159 if (startOfJson < 0) { 0160 qCDebug(LOG_KTE) << "Script parse error: Cannot find start of json header at start of file " << qPrintable(fileName) << '\n'; 0161 continue; 0162 } 0163 0164 int endOfJson = fileContent.indexOf("\n};", startOfJson); 0165 if (endOfJson < 0) { // as fallback, check also mac os line ending 0166 endOfJson = fileContent.indexOf("\r};", startOfJson); 0167 } 0168 if (endOfJson < 0) { 0169 qCDebug(LOG_KTE) << "Script parse error: Cannot find end of json header at start of file " << qPrintable(fileName) << '\n'; 0170 continue; 0171 } 0172 endOfJson += 2; // we want the end including the } but not the ; 0173 0174 // parse json header or skip this file 0175 QJsonParseError error; 0176 const QJsonDocument metaInfo(QJsonDocument::fromJson(fileContent.mid(startOfJson, endOfJson - startOfJson), &error)); 0177 if (error.error || !metaInfo.isObject()) { 0178 qCDebug(LOG_KTE) << "Script parse error: Cannot parse json header at start of file " << qPrintable(fileName) << error.errorString() << endOfJson 0179 << fileContent.mid(endOfJson - 25, 25).replace('\n', ' '); 0180 continue; 0181 } 0182 0183 // remember type 0184 KateScriptHeader generalHeader; 0185 if (type == QLatin1String("indentation")) { 0186 generalHeader.setScriptType(Kate::ScriptType::Indentation); 0187 } else if (type == QLatin1String("commands")) { 0188 generalHeader.setScriptType(Kate::ScriptType::CommandLine); 0189 } else { 0190 // should never happen, we dictate type by directory 0191 Q_ASSERT(false); 0192 } 0193 0194 const QJsonObject metaInfoObject = metaInfo.object(); 0195 generalHeader.setLicense(metaInfoObject.value(QStringLiteral("license")).toString()); 0196 generalHeader.setAuthor(metaInfoObject.value(QStringLiteral("author")).toString()); 0197 generalHeader.setRevision(metaInfoObject.value(QStringLiteral("revision")).toInt()); 0198 generalHeader.setKateVersion(metaInfoObject.value(QStringLiteral("kate-version")).toString()); 0199 0200 // now, cast accordingly based on type 0201 switch (generalHeader.scriptType()) { 0202 case Kate::ScriptType::Indentation: { 0203 KateIndentScriptHeader indentHeader; 0204 indentHeader.setName(metaInfoObject.value(QStringLiteral("name")).toString()); 0205 indentHeader.setBaseName(baseName); 0206 if (indentHeader.name().isNull()) { 0207 qCDebug(LOG_KTE) << "Script value error: No name specified in script meta data: " << qPrintable(fileName) << '\n' 0208 << "-> skipping indenter" << '\n'; 0209 continue; 0210 } 0211 0212 // required style? 0213 indentHeader.setRequiredStyle(metaInfoObject.value(QStringLiteral("required-syntax-style")).toString()); 0214 // which languages does this support? 0215 QStringList indentLanguages = jsonToStringList(metaInfoObject.value(QStringLiteral("indent-languages"))); 0216 if (!indentLanguages.isEmpty()) { 0217 indentHeader.setIndentLanguages(indentLanguages); 0218 } else { 0219 indentHeader.setIndentLanguages(QStringList() << indentHeader.name()); 0220 0221 #ifdef DEBUG_SCRIPTMANAGER 0222 qCDebug(LOG_KTE) << "Script value warning: No indent-languages specified for indent " 0223 << "script " << qPrintable(fileName) << ". Using the name (" << qPrintable(indentHeader.name()) << ")\n"; 0224 #endif 0225 } 0226 // priority 0227 indentHeader.setPriority(metaInfoObject.value(QStringLiteral("priority")).toInt()); 0228 0229 KateIndentScript *script = new KateIndentScript(fileName, indentHeader); 0230 script->setGeneralHeader(generalHeader); 0231 for (const QString &language : indentHeader.indentLanguages()) { 0232 m_languageToIndenters[language.toLower()].push_back(script); 0233 } 0234 0235 m_indentationScriptMap.insert(indentHeader.baseName(), script); 0236 m_indentationScripts.append(script); 0237 break; 0238 } 0239 case Kate::ScriptType::CommandLine: { 0240 KateCommandLineScriptHeader commandHeader; 0241 commandHeader.setFunctions(jsonToStringList(metaInfoObject.value(QStringLiteral("functions")))); 0242 commandHeader.setActions(metaInfoObject.value(QStringLiteral("actions")).toArray()); 0243 if (commandHeader.functions().isEmpty()) { 0244 qCDebug(LOG_KTE) << "Script value error: No functions specified in script meta data: " << qPrintable(fileName) << '\n' 0245 << "-> skipping script" << '\n'; 0246 continue; 0247 } 0248 KateCommandLineScript *script = new KateCommandLineScript(fileName, commandHeader); 0249 script->setGeneralHeader(generalHeader); 0250 m_commandLineScripts.push_back(script); 0251 break; 0252 } 0253 case Kate::ScriptType::Unknown: 0254 default: 0255 qCDebug(LOG_KTE) << "Script value warning: Unknown type ('" << qPrintable(type) << "'): " << qPrintable(fileName) << '\n'; 0256 } 0257 } 0258 } 0259 0260 #ifdef DEBUG_SCRIPTMANAGER 0261 // XX Test 0262 if (indenter("Python")) { 0263 qCDebug(LOG_KTE) << "Python: " << indenter("Python")->global("triggerCharacters").isValid() << "\n"; 0264 qCDebug(LOG_KTE) << "Python: " << indenter("Python")->function("triggerCharacters").isValid() << "\n"; 0265 qCDebug(LOG_KTE) << "Python: " << indenter("Python")->global("blafldsjfklas").isValid() << "\n"; 0266 qCDebug(LOG_KTE) << "Python: " << indenter("Python")->function("indent").isValid() << "\n"; 0267 } 0268 if (indenter("C")) { 0269 qCDebug(LOG_KTE) << "C: " << qPrintable(indenter("C")->url()) << "\n"; 0270 } 0271 if (indenter("lisp")) { 0272 qCDebug(LOG_KTE) << "LISP: " << qPrintable(indenter("Lisp")->url()) << "\n"; 0273 } 0274 #endif 0275 } 0276 0277 void KateScriptManager::reload() 0278 { 0279 collect(); 0280 Q_EMIT reloaded(); 0281 } 0282 0283 /// Kate::Command stuff 0284 0285 bool KateScriptManager::exec(KTextEditor::View *view, const QString &_cmd, QString &errorMsg, const KTextEditor::Range &) 0286 { 0287 Q_UNUSED(view) 0288 Q_UNUSED(errorMsg) 0289 0290 const QList<QStringView> args = QStringView(_cmd).split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts); 0291 if (args.isEmpty()) { 0292 return false; 0293 } 0294 0295 const auto cmd = args.first(); 0296 0297 if (cmd == QLatin1String("reload-scripts")) { 0298 reload(); 0299 return true; 0300 } 0301 0302 return false; 0303 } 0304 0305 bool KateScriptManager::help(KTextEditor::View *view, const QString &cmd, QString &msg) 0306 { 0307 Q_UNUSED(view) 0308 0309 if (cmd == QLatin1String("reload-scripts")) { 0310 msg = i18n("Reload all JavaScript files (indenters, command line scripts, etc)."); 0311 return true; 0312 } 0313 0314 return false; 0315 } 0316 0317 #include "moc_katescriptmanager.cpp"