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
36KateScriptManager *KateScriptManager::m_instance = nullptr;
37
38KateScriptManager::KateScriptManager()
39 : KTextEditor::Command({QStringLiteral("reload-scripts")})
40{
41 // use cached info
42 collect();
43}
44
45KateScriptManager::~KateScriptManager()
46{
47 qDeleteAll(c: m_indentationScripts);
48 qDeleteAll(c: m_commandLineScripts);
49 m_instance = nullptr;
50}
51
52KateIndentScript *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 */
82static 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
96void 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(c: '{');
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 generalHeader;
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 indentHeader;
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
277void KateScriptManager::reload()
278{
279 collect();
280 Q_EMIT reloaded();
281}
282
283/// Kate::Command stuff
284
285bool 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
305bool 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

source code of ktexteditor/src/script/katescriptmanager.cpp