File indexing completed on 2024-04-28 11:45: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 <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"