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(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 ; |
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 | |