KTextEditor

katescriptmanager.cpp
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(m_indentationScripts);
48 qDeleteAll(m_commandLineScripts);
49 m_instance = nullptr;
50}
51
53{
54 KateIndentScript *highestPriorityIndenter = nullptr;
55 const auto indenters = m_languageToIndenters.value(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(value.toString());
90 }
91 }
92
93 return list;
94}
95
97{
98 // clear out the old scripts and reserve enough space
99 qDeleteAll(m_indentationScripts);
100 qDeleteAll(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
116
117 // then resources, e.g. the stuff we ship with us
118 dirs.append(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!
123 for (const QString &dir : genericDataDirs) {
124 dirs.append(dir + basedir);
125 }
126
127 QStringList list;
128 for (const QString &dir : std::as_const(dirs)) {
129 const QStringList fileNames = QDir(dir).entryList({QStringLiteral("*.js")});
130 for (const QString &file : std::as_const(fileNames)) {
131 list.append(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(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(baseName)) {
143 continue;
144 }
145
146 // remember the script
147 unique.insert(baseName);
148
149 // open file or skip it
150 QFile file(fileName);
151 if (!file.open(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('{');
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("\n};", startOfJson);
165 if (endOfJson < 0) { // as fallback, check also mac os line ending
166 endOfJson = fileContent.indexOf("\r};", 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(fileContent.mid(startOfJson, endOfJson - startOfJson), &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(endOfJson - 25, 25).replace('\n', ' ');
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(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(script);
233 }
234
235 m_indentationScriptMap.insert(indentHeader.baseName(), script);
236 m_indentationScripts.append(script);
237 break;
238 }
239 case Kate::ScriptType::CommandLine: {
240 KateCommandLineScriptHeader commandHeader;
241 commandHeader.setFunctions(jsonToStringList(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(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
278{
279 collect();
281}
282
283/// Kate::Command stuff
284
286{
287 Q_UNUSED(view)
288 Q_UNUSED(errorMsg)
289
290 const QList<QStringView> args = QStringView(_cmd).split(QRegularExpression(QStringLiteral("\\s+")), 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
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"
An object representing a section of text, from one Cursor to another.
A text widget with KXMLGUIClient that represents a Document.
Definition view.h:244
A specialized class for scripts that are of type ScriptType::Indentation.
A specialized class for scripts that are of type ScriptType::Indentation.
Manage the scripts on disks – find them and query them.
void reload()
explicitly reload all scripts
bool exec(KTextEditor::View *view, const QString &cmd, QString &errorMsg, const KTextEditor::Range &) override
execute command
void collect()
Collect all scripts.
void reloaded()
this signal is emitted when all scripts are deleted and reloaded again.
KateIndentScript * indenter(const QString &language)
Get an indentation script for the given language – if there is more than one result,...
bool help(KTextEditor::View *view, const QString &cmd, QString &msg) override
get help for a command
QJSValue global(const QString &name)
Get a QJSValue for a global item in the script given its name, or an invalid QJSValue if no such glob...
const QString & url()
The script's URL.
Definition katescript.h:122
void setGeneralHeader(const KateScriptHeader &generalHeader)
set the general header after construction of the script
QJSValue function(const QString &name)
Return a function in the script of the given name, or an invalid QJSValue if no such function exists.
QString i18n(const char *text, const TYPE &arg...)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
The KTextEditor namespace contains all the public API that is required to use the KTextEditor compone...
int indexOf(char ch, int from) const const
QByteArray mid(int pos, int len) const const
QByteArray & replace(int pos, int len, const char *after)
QStringList entryList(QDir::Filters filters, QDir::SortFlags sort) const const
virtual bool open(QIODevice::OpenMode mode) override
QString baseName() const const
void clear()
QHash::iterator insert(const Key &key, const T &value)
const T value(const Key &key) const const
QByteArray readAll()
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
bool isObject() const const
QJsonObject object() const const
QJsonValue value(const QString &key) const const
bool isString() const const
QJsonArray toArray() const const
QString toString() const const
void append(const T &value)
void clear()
T & first()
bool isEmpty() const const
void push_back(const T &value)
Q_EMITQ_EMIT
bool contains(const T &value) const const
QSet::iterator insert(const T &value)
QStringList standardLocations(QStandardPaths::StandardLocation type)
QString writableLocation(QStandardPaths::StandardLocation type)
bool isNull() const const
QString toLower() const const
SkipEmptyParts
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Feb 24 2024 20:00:58 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.