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"