KTextEditor

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

KDE's Doxygen guidelines are available online.