| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2005 Christoph Cullmann <cullmann@kde.org> |
| 3 | SPDX-FileCopyrightText: 2005 Joseph Wenninger <jowenn@kde.org> |
| 4 | SPDX-FileCopyrightText: 2006-2018 Dominik Haumann <dhaumann@kde.org> |
| 5 | SPDX-FileCopyrightText: 2008 Paul Giannaros <paul@giannaros.org> |
| 6 | SPDX-FileCopyrightText: 2010 Joseph Wenninger <jowenn@kde.org> |
| 7 | |
| 8 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 9 | */ |
| 10 | |
| 11 | #include "katescriptmanager.h" |
| 12 | |
| 13 | #include <ktexteditor_version.h> |
| 14 | |
| 15 | #include <QDir> |
| 16 | #include <QFile> |
| 17 | #include <QFileInfo> |
| 18 | #include <QJsonArray> |
| 19 | #include <QJsonDocument> |
| 20 | #include <QJsonObject> |
| 21 | #include <QJsonValue> |
| 22 | #include <QRegularExpression> |
| 23 | #include <QStringList> |
| 24 | #include <QUuid> |
| 25 | |
| 26 | #include <KConfig> |
| 27 | #include <KConfigGroup> |
| 28 | #include <KLocalizedString> |
| 29 | |
| 30 | #include "katecmd.h" |
| 31 | #include "katecommandlinescript.h" |
| 32 | #include "kateglobal.h" |
| 33 | #include "kateindentscript.h" |
| 34 | #include "katepartdebug.h" |
| 35 | |
| 36 | KateScriptManager *KateScriptManager::m_instance = nullptr; |
| 37 | |
| 38 | KateScriptManager::KateScriptManager() |
| 39 | : KTextEditor::Command({QStringLiteral("reload-scripts" )}) |
| 40 | { |
| 41 | // use cached info |
| 42 | collect(); |
| 43 | } |
| 44 | |
| 45 | KateScriptManager::~KateScriptManager() |
| 46 | { |
| 47 | qDeleteAll(c: m_indentationScripts); |
| 48 | qDeleteAll(c: m_commandLineScripts); |
| 49 | m_instance = nullptr; |
| 50 | } |
| 51 | |
| 52 | KateIndentScript *KateScriptManager::indenter(const QString &language) |
| 53 | { |
| 54 | KateIndentScript *highestPriorityIndenter = nullptr; |
| 55 | const auto indenters = m_languageToIndenters.value(key: language.toLower()); |
| 56 | for (KateIndentScript *indenter : indenters) { |
| 57 | // don't overwrite if there is already a result with a higher priority |
| 58 | if (highestPriorityIndenter && indenter->indentHeader().priority() < highestPriorityIndenter->indentHeader().priority()) { |
| 59 | #ifdef DEBUG_SCRIPTMANAGER |
| 60 | qCDebug(LOG_KTE) << "Not overwriting indenter for" << language << "as the priority isn't big enough (" << indenter->indentHeader().priority() << '<' |
| 61 | << highestPriorityIndenter->indentHeader().priority() << ')'; |
| 62 | #endif |
| 63 | } else { |
| 64 | highestPriorityIndenter = indenter; |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | #ifdef DEBUG_SCRIPTMANAGER |
| 69 | if (highestPriorityIndenter) { |
| 70 | qCDebug(LOG_KTE) << "Found indenter" << highestPriorityIndenter->url() << "for" << language; |
| 71 | } else { |
| 72 | qCDebug(LOG_KTE) << "No indenter for" << language; |
| 73 | } |
| 74 | #endif |
| 75 | |
| 76 | return highestPriorityIndenter; |
| 77 | } |
| 78 | |
| 79 | /** |
| 80 | * Small helper: QJsonValue to QStringList |
| 81 | */ |
| 82 | static QStringList jsonToStringList(const QJsonValue &value) |
| 83 | { |
| 84 | QStringList list; |
| 85 | |
| 86 | const auto array = value.toArray(); |
| 87 | for (const QJsonValue &value : array) { |
| 88 | if (value.isString()) { |
| 89 | list.append(t: value.toString()); |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | return list; |
| 94 | } |
| 95 | |
| 96 | void KateScriptManager::collect() |
| 97 | { |
| 98 | // clear out the old scripts and reserve enough space |
| 99 | qDeleteAll(c: m_indentationScripts); |
| 100 | qDeleteAll(c: m_commandLineScripts); |
| 101 | m_indentationScripts.clear(); |
| 102 | m_commandLineScripts.clear(); |
| 103 | |
| 104 | m_languageToIndenters.clear(); |
| 105 | m_indentationScriptMap.clear(); |
| 106 | |
| 107 | // now, we search all kinds of known scripts |
| 108 | for (const auto &type : {QLatin1String("indentation" ), QLatin1String("commands" )}) { |
| 109 | // basedir for filesystem lookup |
| 110 | const QString basedir = QLatin1String("/katepart5/script/" ) + type; |
| 111 | |
| 112 | QStringList dirs; |
| 113 | |
| 114 | // first writable locations, e.g. stuff the user has provided |
| 115 | dirs += QStandardPaths::writableLocation(type: QStandardPaths::GenericDataLocation) + basedir; |
| 116 | |
| 117 | // then resources, e.g. the stuff we ship with us |
| 118 | dirs.append(t: QLatin1String(":/ktexteditor/script/" ) + type); |
| 119 | |
| 120 | // then all other locations, this includes global stuff installed by other applications |
| 121 | // 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! |
| 122 | const auto genericDataDirs = QStandardPaths::standardLocations(type: QStandardPaths::GenericDataLocation); |
| 123 | for (const QString &dir : genericDataDirs) { |
| 124 | dirs.append(t: dir + basedir); |
| 125 | } |
| 126 | |
| 127 | QStringList list; |
| 128 | for (const QString &dir : std::as_const(t&: dirs)) { |
| 129 | const QStringList fileNames = QDir(dir).entryList(nameFilters: {QStringLiteral("*.js" )}); |
| 130 | for (const QString &file : std::as_const(t: fileNames)) { |
| 131 | list.append(t: dir + QLatin1Char('/') + file); |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | // iterate through the files and read info out of cache or file, no double loading of same scripts |
| 136 | QSet<QString> unique; |
| 137 | for (const QString &fileName : std::as_const(t&: list)) { |
| 138 | // get file basename |
| 139 | const QString baseName = QFileInfo(fileName).baseName(); |
| 140 | |
| 141 | // only load scripts once, even if multiple installed variants found! |
| 142 | if (unique.contains(value: baseName)) { |
| 143 | continue; |
| 144 | } |
| 145 | |
| 146 | // remember the script |
| 147 | unique.insert(value: baseName); |
| 148 | |
| 149 | // open file or skip it |
| 150 | QFile file(fileName); |
| 151 | if (!file.open(flags: QIODevice::ReadOnly)) { |
| 152 | qCDebug(LOG_KTE) << "Script parse error: Cannot open file " << qPrintable(fileName) << '\n'; |
| 153 | continue; |
| 154 | } |
| 155 | |
| 156 | // search json header or skip this file |
| 157 | QByteArray fileContent = file.readAll(); |
| 158 | int startOfJson = fileContent.indexOf(ch: '{'); |
| 159 | if (startOfJson < 0) { |
| 160 | qCDebug(LOG_KTE) << "Script parse error: Cannot find start of json header at start of file " << qPrintable(fileName) << '\n'; |
| 161 | continue; |
| 162 | } |
| 163 | |
| 164 | int endOfJson = fileContent.indexOf(bv: "\n};" , from: startOfJson); |
| 165 | if (endOfJson < 0) { // as fallback, check also mac os line ending |
| 166 | endOfJson = fileContent.indexOf(bv: "\r};" , from: startOfJson); |
| 167 | } |
| 168 | if (endOfJson < 0) { |
| 169 | qCDebug(LOG_KTE) << "Script parse error: Cannot find end of json header at start of file " << qPrintable(fileName) << '\n'; |
| 170 | continue; |
| 171 | } |
| 172 | endOfJson += 2; // we want the end including the } but not the ; |
| 173 | |
| 174 | // parse json header or skip this file |
| 175 | QJsonParseError error; |
| 176 | const QJsonDocument metaInfo(QJsonDocument::fromJson(json: fileContent.mid(index: startOfJson, len: endOfJson - startOfJson), error: &error)); |
| 177 | if (error.error || !metaInfo.isObject()) { |
| 178 | qCDebug(LOG_KTE) << "Script parse error: Cannot parse json header at start of file " << qPrintable(fileName) << error.errorString() << endOfJson |
| 179 | << fileContent.mid(index: endOfJson - 25, len: 25).replace(before: '\n', after: ' '); |
| 180 | continue; |
| 181 | } |
| 182 | |
| 183 | // remember type |
| 184 | KateScriptHeader ; |
| 185 | if (type == QLatin1String("indentation" )) { |
| 186 | generalHeader.setScriptType(Kate::ScriptType::Indentation); |
| 187 | } else if (type == QLatin1String("commands" )) { |
| 188 | generalHeader.setScriptType(Kate::ScriptType::CommandLine); |
| 189 | } else { |
| 190 | // should never happen, we dictate type by directory |
| 191 | Q_ASSERT(false); |
| 192 | } |
| 193 | |
| 194 | const QJsonObject metaInfoObject = metaInfo.object(); |
| 195 | generalHeader.setLicense(metaInfoObject.value(QStringLiteral("license" )).toString()); |
| 196 | generalHeader.setAuthor(metaInfoObject.value(QStringLiteral("author" )).toString()); |
| 197 | generalHeader.setRevision(metaInfoObject.value(QStringLiteral("revision" )).toInt()); |
| 198 | generalHeader.setKateVersion(metaInfoObject.value(QStringLiteral("kate-version" )).toString()); |
| 199 | |
| 200 | // now, cast accordingly based on type |
| 201 | switch (generalHeader.scriptType()) { |
| 202 | case Kate::ScriptType::Indentation: { |
| 203 | KateIndentScriptHeader ; |
| 204 | indentHeader.setName(metaInfoObject.value(QStringLiteral("name" )).toString()); |
| 205 | indentHeader.setBaseName(baseName); |
| 206 | if (indentHeader.name().isNull()) { |
| 207 | qCDebug(LOG_KTE) << "Script value error: No name specified in script meta data: " << qPrintable(fileName) << '\n' |
| 208 | << "-> skipping indenter" << '\n'; |
| 209 | continue; |
| 210 | } |
| 211 | |
| 212 | // required style? |
| 213 | indentHeader.setRequiredStyle(metaInfoObject.value(QStringLiteral("required-syntax-style" )).toString()); |
| 214 | // which languages does this support? |
| 215 | QStringList indentLanguages = jsonToStringList(value: metaInfoObject.value(QStringLiteral("indent-languages" ))); |
| 216 | if (!indentLanguages.isEmpty()) { |
| 217 | indentHeader.setIndentLanguages(indentLanguages); |
| 218 | } else { |
| 219 | indentHeader.setIndentLanguages(QStringList() << indentHeader.name()); |
| 220 | |
| 221 | #ifdef DEBUG_SCRIPTMANAGER |
| 222 | qCDebug(LOG_KTE) << "Script value warning: No indent-languages specified for indent " |
| 223 | << "script " << qPrintable(fileName) << ". Using the name (" << qPrintable(indentHeader.name()) << ")\n" ; |
| 224 | #endif |
| 225 | } |
| 226 | // priority |
| 227 | indentHeader.setPriority(metaInfoObject.value(QStringLiteral("priority" )).toInt()); |
| 228 | |
| 229 | KateIndentScript *script = new KateIndentScript(fileName, indentHeader); |
| 230 | script->setGeneralHeader(generalHeader); |
| 231 | for (const QString &language : indentHeader.indentLanguages()) { |
| 232 | m_languageToIndenters[language.toLower()].push_back(t: script); |
| 233 | } |
| 234 | |
| 235 | m_indentationScriptMap.insert(key: indentHeader.baseName(), value: script); |
| 236 | m_indentationScripts.append(t: script); |
| 237 | break; |
| 238 | } |
| 239 | case Kate::ScriptType::CommandLine: { |
| 240 | KateCommandLineScriptHeader commandHeader; |
| 241 | commandHeader.setFunctions(jsonToStringList(value: metaInfoObject.value(QStringLiteral("functions" )))); |
| 242 | commandHeader.setActions(metaInfoObject.value(QStringLiteral("actions" )).toArray()); |
| 243 | if (commandHeader.functions().isEmpty()) { |
| 244 | qCDebug(LOG_KTE) << "Script value error: No functions specified in script meta data: " << qPrintable(fileName) << '\n' |
| 245 | << "-> skipping script" << '\n'; |
| 246 | continue; |
| 247 | } |
| 248 | KateCommandLineScript *script = new KateCommandLineScript(fileName, commandHeader); |
| 249 | script->setGeneralHeader(generalHeader); |
| 250 | m_commandLineScripts.push_back(t: script); |
| 251 | break; |
| 252 | } |
| 253 | case Kate::ScriptType::Unknown: |
| 254 | default: |
| 255 | qCDebug(LOG_KTE) << "Script value warning: Unknown type ('" << qPrintable(type) << "'): " << qPrintable(fileName) << '\n'; |
| 256 | } |
| 257 | } |
| 258 | } |
| 259 | |
| 260 | #ifdef DEBUG_SCRIPTMANAGER |
| 261 | // XX Test |
| 262 | if (indenter("Python" )) { |
| 263 | qCDebug(LOG_KTE) << "Python: " << indenter("Python" )->global("triggerCharacters" ).isValid() << "\n" ; |
| 264 | qCDebug(LOG_KTE) << "Python: " << indenter("Python" )->function("triggerCharacters" ).isValid() << "\n" ; |
| 265 | qCDebug(LOG_KTE) << "Python: " << indenter("Python" )->global("blafldsjfklas" ).isValid() << "\n" ; |
| 266 | qCDebug(LOG_KTE) << "Python: " << indenter("Python" )->function("indent" ).isValid() << "\n" ; |
| 267 | } |
| 268 | if (indenter("C" )) { |
| 269 | qCDebug(LOG_KTE) << "C: " << qPrintable(indenter("C" )->url()) << "\n" ; |
| 270 | } |
| 271 | if (indenter("lisp" )) { |
| 272 | qCDebug(LOG_KTE) << "LISP: " << qPrintable(indenter("Lisp" )->url()) << "\n" ; |
| 273 | } |
| 274 | #endif |
| 275 | } |
| 276 | |
| 277 | void KateScriptManager::reload() |
| 278 | { |
| 279 | collect(); |
| 280 | Q_EMIT reloaded(); |
| 281 | } |
| 282 | |
| 283 | /// Kate::Command stuff |
| 284 | |
| 285 | bool KateScriptManager::exec(KTextEditor::View *view, const QString &_cmd, QString &errorMsg, const KTextEditor::Range &) |
| 286 | { |
| 287 | Q_UNUSED(view) |
| 288 | Q_UNUSED(errorMsg) |
| 289 | |
| 290 | const QList<QStringView> args = QStringView(_cmd).split(sep: QRegularExpression(QStringLiteral("\\s+" )), behavior: Qt::SkipEmptyParts); |
| 291 | if (args.isEmpty()) { |
| 292 | return false; |
| 293 | } |
| 294 | |
| 295 | const auto cmd = args.first(); |
| 296 | |
| 297 | if (cmd == QLatin1String("reload-scripts" )) { |
| 298 | reload(); |
| 299 | return true; |
| 300 | } |
| 301 | |
| 302 | return false; |
| 303 | } |
| 304 | |
| 305 | bool KateScriptManager::help(KTextEditor::View *view, const QString &cmd, QString &msg) |
| 306 | { |
| 307 | Q_UNUSED(view) |
| 308 | |
| 309 | if (cmd == QLatin1String("reload-scripts" )) { |
| 310 | msg = i18n("Reload all JavaScript files (indenters, command line scripts, etc)." ); |
| 311 | return true; |
| 312 | } |
| 313 | |
| 314 | return false; |
| 315 | } |
| 316 | |
| 317 | #include "moc_katescriptmanager.cpp" |
| 318 | |