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