File indexing completed on 2024-04-28 04:38:29

0001 /*
0002     SPDX-FileCopyrightText: 2008 Cédric Pasteur <cedric.pasteur@free.fr>
0003     SPDX-FileCopyrightText: 2011 David Nolden <david.nolden.kdevelop@art-master.de>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "customscript_plugin.h"
0009 
0010 #include <KPluginFactory>
0011 #include <QTextStream>
0012 #include <QTemporaryFile>
0013 #include <KProcess>
0014 #include <interfaces/icore.h>
0015 #include <interfaces/isourceformatter.h>
0016 #include <QDir>
0017 #include <QTimer>
0018 
0019 #include <util/formattinghelpers.h>
0020 #include <interfaces/iprojectcontroller.h>
0021 #include <interfaces/iproject.h>
0022 #include <KMessageBox>
0023 #include <interfaces/iuicontroller.h>
0024 #include <KParts/MainWindow>
0025 #include <KLocalizedString>
0026 #include <interfaces/ilanguagecontroller.h>
0027 #include <interfaces/ilanguagesupport.h>
0028 #include <util/path.h>
0029 #include <debug.h>
0030 
0031 #include <QMimeType>
0032 
0033 #include <memory>
0034 
0035 using namespace KDevelop;
0036 
0037 static QPointer<CustomScriptPlugin> indentPluginSingleton;
0038 
0039 K_PLUGIN_FACTORY_WITH_JSON(CustomScriptFactory, "kdevcustomscript.json", registerPlugin<CustomScriptPlugin>(); )
0040 
0041 // Replaces ${KEY} in command with variables[KEY]
0042 static QString replaceVariables(QString command, const QMap<QString, QString>& variables)
0043 {
0044     while (command.contains(QLatin1String("${"))) {
0045         int pos = command.indexOf(QLatin1String("${"));
0046         int end = command.indexOf(QLatin1Char('}'), pos + 2);
0047         if (end == -1) {
0048             break;
0049         }
0050         QString key = command.mid(pos + 2, end - pos - 2);
0051 
0052         const auto variableIt = variables.constFind(key);
0053         if (variableIt != variables.constEnd()) {
0054             command.replace(pos, 1 + end - pos, *variableIt );
0055         } else {
0056             qCDebug(CUSTOMSCRIPT) << "found no variable while replacing in shell-command" << command << "key" << key << "available:" << variables;
0057             command.remove(pos, 1 + end - pos);
0058         }
0059     }
0060     return command;
0061 }
0062 
0063 CustomScriptPlugin::CustomScriptPlugin(QObject* parent, const QVariantList&)
0064     : IPlugin(QStringLiteral("kdevcustomscript"), parent)
0065 {
0066     indentPluginSingleton = this;
0067 }
0068 
0069 CustomScriptPlugin::~CustomScriptPlugin()
0070 {
0071 }
0072 
0073 QString CustomScriptPlugin::name() const
0074 {
0075     // This needs to match the X-KDE-PluginInfo-Name entry from the .desktop file!
0076     return QStringLiteral("kdevcustomscript");
0077 }
0078 
0079 QString CustomScriptPlugin::caption() const
0080 {
0081     return QStringLiteral("Custom Script Formatter");
0082 }
0083 
0084 QString CustomScriptPlugin::description() const
0085 {
0086     return i18n("<b>Indent and Format Source Code.</b><br />"
0087                 "This plugin allows using powerful external formatting tools "
0088                 "that can be invoked through the command-line.<br />"
0089                 "For example, the <b>uncrustify</b>, <b>astyle</b> or <b>indent</b> "
0090                 "formatters can be used.<br />"
0091                 "The advantage of command-line formatters is that formatting configurations "
0092                 "can be easily shared by all team members, independent of their preferred IDE.");
0093 }
0094 
0095 QString CustomScriptPlugin::usageHint() const
0096 {
0097     return i18nc("@info formatter usage hint",
0098                  "Note: each custom script style requires a certain tool executable "
0099                  "to be installed. Otherwise, code will not be formatted.");
0100 }
0101 
0102 QString CustomScriptPlugin::formatSourceWithStyle(const SourceFormatterStyle& style, const QString& text,
0103                                                   const QUrl& url, const QMimeType& mime, const QString& leftContext,
0104                                                   const QString& rightContext) const
0105 {
0106     KProcess proc;
0107     QTextStream ios(&proc);
0108 
0109     std::unique_ptr<QTemporaryFile> tmpFile;
0110 
0111     QString styleContent = style.content();
0112     if (styleContent.isEmpty()) {
0113         styleContent = predefinedStyle(style.name()).content();
0114         if (styleContent.isEmpty()) {
0115             qCWarning(CUSTOMSCRIPT) << "Empty contents for style" << style.name() << "for indent plugin";
0116             return text;
0117         }
0118     }
0119     // NOTE: from now on, only one member function of @p style may be called: name(), because only the
0120     // name of an incomplete style is guaranteed to match that of the corresponding predefined style.
0121 
0122     QString useText = text;
0123     useText = leftContext + useText + rightContext;
0124 
0125     QMap<QString, QString> projectVariables;
0126     const auto projects = ICore::self()->projectController()->projects();
0127     for (IProject* project : projects) {
0128         projectVariables[project->name()] = project->path().toUrl().toLocalFile();
0129     }
0130 
0131     QString command = styleContent;
0132 
0133     // Replace ${<project name>} with the project path
0134     command = replaceVariables(command, projectVariables);
0135     command.replace(QLatin1String("$FILE"), url.toLocalFile());
0136 
0137     if (command.contains(QLatin1String("$TMPFILE"))) {
0138         tmpFile.reset(new QTemporaryFile(QDir::tempPath() + QLatin1String("/code")));
0139         if (tmpFile->open()) {
0140             qCDebug(CUSTOMSCRIPT) << "using temporary file" << tmpFile->fileName();
0141             command.replace(QLatin1String("$TMPFILE"), tmpFile->fileName());
0142             QByteArray useTextArray = useText.toLocal8Bit();
0143             if (tmpFile->write(useTextArray) != useTextArray.size()) {
0144                 qCWarning(CUSTOMSCRIPT) << "failed to write text to temporary file";
0145                 return text;
0146             }
0147         } else {
0148             qCWarning(CUSTOMSCRIPT) << "Failed to create a temporary file";
0149             return text;
0150         }
0151         tmpFile->close();
0152     }
0153 
0154     qCDebug(CUSTOMSCRIPT) << "using shell command for indentation: " << command;
0155     proc.setShellCommand(command);
0156     proc.setOutputChannelMode(KProcess::OnlyStdoutChannel);
0157 
0158     proc.start();
0159     if (!proc.waitForStarted()) {
0160         qCDebug(CUSTOMSCRIPT) << "Unable to start indent";
0161         return text;
0162     }
0163 
0164     if (!tmpFile.get()) {
0165         proc.write(useText.toLocal8Bit());
0166     }
0167 
0168     proc.closeWriteChannel();
0169     if (!proc.waitForFinished()) {
0170         qCDebug(CUSTOMSCRIPT) << "Process doesn't finish";
0171         return text;
0172     }
0173 
0174     QString output;
0175 
0176     if (tmpFile.get()) {
0177         QFile f(tmpFile->fileName());
0178         if (f.open(QIODevice::ReadOnly)) {
0179             output = QString::fromLocal8Bit(f.readAll());
0180         } else {
0181             qCWarning(CUSTOMSCRIPT) << "Failed opening the temporary file for reading";
0182             return text;
0183         }
0184     } else {
0185         output = ios.readAll();
0186     }
0187     if (output.isEmpty()) {
0188         qCWarning(CUSTOMSCRIPT) << styleContent << "command returned empty text for style" << style.name();
0189         return text;
0190     }
0191 
0192     int tabWidth = 4;
0193     if ((!leftContext.isEmpty() || !rightContext.isEmpty()) && (text.contains(QLatin1Char(' ')) || output.contains(QLatin1Char('\t')))) {
0194         // If we have to do context-matching with tabs, determine the correct tab-width so that the context
0195         // can be matched correctly
0196         const auto indent = indentation(style, url, mime);
0197         if (indent.indentationTabWidth > 0) {
0198             tabWidth = indent.indentationTabWidth;
0199         }
0200     }
0201 
0202     return KDevelop::extractFormattedTextFromContext(output, text, leftContext, rightContext, tabWidth);
0203 }
0204 
0205 namespace {
0206 QVector<SourceFormatterStyle> stylesFromLanguagePlugins()
0207 {
0208     QVector<KDevelop::SourceFormatterStyle> styles;
0209     const auto loadedLanguages = ICore::self()->languageController()->loadedLanguages();
0210     for (auto* lang : loadedLanguages) {
0211         const SourceFormatterItemList& languageStyles = lang->sourceFormatterItems();
0212         for (const SourceFormatterStyleItem& item: languageStyles) {
0213             if (item.engine == QLatin1String("customscript")) {
0214                 styles << item.style;
0215             }
0216         }
0217     }
0218 
0219     return styles;
0220 }
0221 
0222 namespace BuiltInStyles {
0223 SourceFormatterStyle gnuIndentGnu()
0224 {
0225     SourceFormatterStyle result(QStringLiteral("GNU_indent_GNU"));
0226     result.setCaption(i18n("Gnu Indent: GNU"));
0227     result.setContent(QStringLiteral("indent"));
0228     result.setUsePreview(true);
0229 
0230     result.setMimeTypes(ISourceFormatter::mimeTypesSupportedByBuiltInStyles());
0231     return result;
0232 }
0233 
0234 SourceFormatterStyle gnuIndentKr()
0235 {
0236     SourceFormatterStyle result(QStringLiteral("GNU_indent_KR"));
0237     result.setCaption(i18n("Gnu Indent: Kernighan & Ritchie"));
0238     result.setContent(QStringLiteral("indent -kr"));
0239     result.setUsePreview(true);
0240 
0241     result.setMimeTypes(ISourceFormatter::mimeTypesSupportedByBuiltInStyles());
0242     return result;
0243 }
0244 
0245 SourceFormatterStyle gnuIndentOrig()
0246 {
0247     SourceFormatterStyle result(QStringLiteral("GNU_indent_orig"));
0248     result.setCaption(i18n("Gnu Indent: Original Berkeley indent style"));
0249     result.setContent(QStringLiteral("indent -orig"));
0250     result.setUsePreview(true);
0251 
0252     result.setMimeTypes(ISourceFormatter::mimeTypesSupportedByBuiltInStyles());
0253     return result;
0254 }
0255 
0256 SourceFormatterStyle clangFormat()
0257 {
0258     SourceFormatterStyle result(QStringLiteral("clang_format"));
0259     result.setCaption(i18n("Clang Format"));
0260     result.setContent(QStringLiteral("clang-format -assume-filename=\"$FILE\""));
0261     result.setUsePreview(false);
0262     result.setDescription(i18n("Description:<br /><br />"
0263                                "<b>clang-format</b> is an automatic source formater by the LLVM "
0264                                "project. It supports a variety of formatting style options via "
0265                                "a <b>.clang-format</b> configuration file, usually located in "
0266                                "the project root directory."));
0267 
0268     result.setMimeTypes(ISourceFormatter::mimeTypesSupportedByBuiltInStyles());
0269     return result;
0270 }
0271 
0272 SourceFormatterStyle kdevFormatSource()
0273 {
0274     SourceFormatterStyle result(QStringLiteral("kdev_format_source"));
0275     result.setCaption(QStringLiteral("KDevelop: kdev_format_source"));
0276     result.setContent(QStringLiteral("kdev_format_source $FILE $TMPFILE"));
0277     result.setUsePreview(false);
0278     result.setDescription(i18n("Description:<br />"
0279                                "<b>kdev_format_source</b> is a script bundled with KDevelop "
0280                                "which allows using fine-grained formatting rules by placing "
0281                                "meta-files called <b>format_sources</b> into the file-system.<br /><br />"
0282                                "Each line of the <b>format_sources</b> files defines a list of wildcards "
0283                                "followed by a colon and the used formatting-command.<br /><br />"
0284                                "The formatting-command should use <b>$TMPFILE</b> to reference the "
0285                                "temporary file to reformat.<br /><br />"
0286                                "Example:<br />"
0287                                "<b>*.cpp *.h : myformatter $TMPFILE</b><br />"
0288                                "This will reformat all files ending with <b>.cpp</b> or <b>.h</b> using "
0289                                "the custom formatting script <b>myformatter</b>.<br /><br />"
0290                                "Example: <br />"
0291                                "<b>subdir/* : uncrustify -l CPP -f $TMPFILE -c uncrustify.config -o $TMPFILE</b> <br />"
0292                                "This will reformat all files in subdirectory <b>subdir</b> using the <b>uncrustify</b> "
0293                                "tool with the config-file <b>uncrustify.config</b>."));
0294 
0295     result.setMimeTypes(ISourceFormatter::mimeTypesSupportedByBuiltInStyles());
0296     return result;
0297 }
0298 }
0299 } // unnamed namespace
0300 
0301 QVector<SourceFormatterStyle> CustomScriptPlugin::predefinedStyles() const
0302 {
0303     static const QVector<SourceFormatterStyle> builtInStyles = {
0304         BuiltInStyles::kdevFormatSource(),
0305         BuiltInStyles::clangFormat(),
0306         BuiltInStyles::gnuIndentGnu(),
0307         BuiltInStyles::gnuIndentKr(),
0308         BuiltInStyles::gnuIndentOrig(),
0309     };
0310 
0311     auto styles = stylesFromLanguagePlugins();
0312     styles += builtInStyles; // Use operator+= rather than operator+ to avoid detaching.
0313     return styles;
0314 }
0315 
0316 bool CustomScriptPlugin::hasEditStyleWidget() const
0317 {
0318     return true;
0319 }
0320 
0321 SettingsWidgetPtr CustomScriptPlugin::editStyleWidget(const QMimeType& mime) const
0322 {
0323     Q_UNUSED(mime);
0324     return SettingsWidgetPtr{new CustomScriptPreferences()};
0325 }
0326 
0327 static QString defaultSample()
0328 {
0329     return QStringLiteral(
0330         "// Formatting\n"
0331         "void func(){\n"
0332         "\tif(isFoo(a,b))\n"
0333         "\tbar(a,b);\n"
0334         "if(isFoo)\n"
0335         "\ta=bar((b-c)*a,*d--);\n"
0336         "if(  isFoo( a,b ) )\n"
0337         "\tbar(a, b);\n"
0338         "if (isFoo) {isFoo=false;cat << isFoo <<endl;}\n"
0339         "if(isFoo)DoBar();if (isFoo){\n"
0340         "\tbar();\n"
0341         "}\n"
0342         "\telse if(isBar()){\n"
0343         "\tannotherBar();\n"
0344         "}\n"
0345         "int var = 1;\n"
0346         "int *ptr = &var;\n"
0347         "int &ref = i;\n"
0348         "\n"
0349         "QList<int>::const_iterator it = list.begin();\n"
0350         "}\n"
0351         "namespace A {\n"
0352         "namespace B {\n"
0353         "void foo() {\n"
0354         "  if (true) {\n"
0355         "    func();\n"
0356         "  } else {\n"
0357         "    // bla\n"
0358         "  }\n"
0359         "}\n"
0360         "}\n"
0361         "}\n"
0362 
0363         "\n\n"
0364         "// Indentation\n"
0365         "#define foobar(A)\\\n"
0366         "{Foo();Bar();}\n"
0367         "#define anotherFoo(B)\\\n"
0368         "return Bar()\n"
0369         "\n"
0370         "namespace Bar\n"
0371         "{\n"
0372         "class Foo\n"
0373         "{public:\n"
0374         "Foo();\n"
0375         "virtual ~Foo();\n"
0376         "};\n"
0377         "void bar(int foo)\n"
0378         "{\n"
0379         "switch (foo)\n"
0380         "{\n"
0381         "case 1:\n"
0382         "a+=1;\n"
0383         "break;\n"
0384         "case 2:\n"
0385         "{\n"
0386         "a += 2;\n"
0387         " break;\n"
0388         "}\n"
0389         "}\n"
0390         "if (isFoo)\n"
0391         "{\n"
0392         "bar();\n"
0393         "}\n"
0394         "else\n"
0395         "{\n"
0396         "anotherBar();\n"
0397         "}\n"
0398         "}\n"
0399         "int foo()\n"
0400         "\twhile(isFoo)\n"
0401         "\t\t{\n"
0402         "\t\t\t// ...\n"
0403         "\t\t\tgoto error;\n"
0404         "\t\t/* .... */\n"
0405         "\t\terror:\n"
0406         "\t\t\t//...\n"
0407         "\t\t}\n"
0408         "\t}\n"
0409         "fooArray[]={ red,\n"
0410         "\tgreen,\n"
0411         "\tdarkblue};\n"
0412         "fooFunction(barArg1,\n"
0413         "\tbarArg2,\n"
0414         "\tbarArg3);\n"
0415         );
0416 }
0417 
0418 QString CustomScriptPlugin::previewText(const SourceFormatterStyle& style, const QMimeType& /*mime*/) const
0419 {
0420     if (!style.overrideSample().isEmpty()) {
0421         return style.overrideSample();
0422     }
0423     return defaultSample();
0424 }
0425 
0426 QStringList CustomScriptPlugin::computeIndentationFromSample(const SourceFormatterStyle& style, const QUrl& url,
0427                                                              const QMimeType& mime) const
0428 {
0429     QStringList ret;
0430 
0431     const auto languages = ICore::self()->languageController()->languagesForUrl(url);
0432     if (languages.isEmpty()) {
0433         return ret;
0434     }
0435     const auto& language = *languages.constFirst();
0436     const auto sample = language.indentationSample();
0437     if (sample.isEmpty()) {
0438         qCWarning(CUSTOMSCRIPT) << "Cannot compute indentation because of missing indentation sample in language plugin"
0439                                 << language.name();
0440         return ret;
0441     }
0442     const QString formattedSample = formatSourceWithStyle(style, sample, url, mime, QString(), QString());
0443 
0444     const QStringList lines = formattedSample.split(QLatin1Char('\n'));
0445     for (const QString& line : lines) {
0446         if (!line.isEmpty() && line[0].isSpace()) {
0447             QString indent;
0448             for (const QChar c : line) {
0449                 if (c.isSpace()) {
0450                     indent.push_back(c);
0451                 } else {
0452                     break;
0453                 }
0454             }
0455 
0456             if (!indent.isEmpty() && !ret.contains(indent)) {
0457                 ret.push_back(indent);
0458             }
0459         }
0460     }
0461 
0462     return ret;
0463 }
0464 
0465 CustomScriptPlugin::Indentation CustomScriptPlugin::indentation(const SourceFormatterStyle& style, const QUrl& url,
0466                                                                 const QMimeType& mime) const
0467 {
0468     Indentation ret;
0469     const QStringList indent = computeIndentationFromSample(style, url, mime);
0470     if (indent.isEmpty()) {
0471         qCDebug(CUSTOMSCRIPT) << "failed extracting a valid indentation from sample for url" << url;
0472         return ret; // No valid indentation could be extracted
0473     }
0474 
0475     if (indent[0].contains(QLatin1Char(' '))) {
0476         ret.indentWidth = indent[0].count(QLatin1Char(' '));
0477     }
0478 
0479     if (!indent.join(QString()).contains(QLatin1Char('  '))) {
0480         ret.indentationTabWidth = -1;         // Tabs are not used for indentation
0481     }
0482     if (indent[0] == QLatin1String("    ")) {
0483         // The script indents with tabs-only
0484         // The problem is that we don't know how
0485         // wide a tab is supposed to be.
0486         //
0487         // We need indentation-width=tab-width
0488         // to make the editor do tab-only formatting,
0489         // so choose a random with of 4.
0490         ret.indentWidth = 4;
0491         ret.indentationTabWidth = 4;
0492     } else if (ret.indentWidth)   {
0493         // Tabs are used for indentation, alongside with spaces
0494         // Try finding out how many spaces one tab stands for.
0495         // Do it by assuming a uniform indentation-step with each level.
0496 
0497         for (int pos = 0; pos < indent.size(); ++pos) {
0498             if (indent[pos] == QLatin1String("  ")&& pos >= 1) {
0499                 // This line consists of only a tab.
0500                 int prevWidth = indent[pos - 1].length();
0501                 int prevPrevWidth = (pos >= 2) ? indent[pos - 2].length() : 0;
0502                 int step = prevWidth - prevPrevWidth;
0503                 qCDebug(CUSTOMSCRIPT) << "found in line " << pos << prevWidth << prevPrevWidth << step;
0504                 if (step > 0 && step <= prevWidth) {
0505                     qCDebug(CUSTOMSCRIPT) << "Done";
0506                     ret.indentationTabWidth = prevWidth + step;
0507                     break;
0508                 }
0509             }
0510         }
0511     }
0512 
0513     qCDebug(CUSTOMSCRIPT) << "indent-sample" << QLatin1Char('\"') + indent.join(QLatin1Char('\n')) + QLatin1Char('\"') << "extracted tab-width" << ret.indentationTabWidth << "extracted indentation width" << ret.indentWidth;
0514 
0515     return ret;
0516 }
0517 
0518 void CustomScriptPreferences::updateTimeout()
0519 {
0520     const QString& text = indentPluginSingleton.data()->previewText(m_style, QMimeType());
0521     emit previewTextChanged(text);
0522 }
0523 
0524 CustomScriptPreferences::CustomScriptPreferences()
0525 {
0526     m_updateTimer = new QTimer(this);
0527     m_updateTimer->setSingleShot(true);
0528     m_updateTimer->setInterval(1000);
0529     connect(m_updateTimer, &QTimer::timeout, this, &CustomScriptPreferences::updateTimeout);
0530     m_vLayout = new QVBoxLayout(this);
0531     m_vLayout->setContentsMargins(0, 0, 0, 0);
0532     m_captionLabel = new QLabel;
0533     m_vLayout->addWidget(m_captionLabel);
0534     m_vLayout->addSpacing(10);
0535     m_hLayout = new QHBoxLayout;
0536     m_vLayout->addLayout(m_hLayout);
0537     m_commandLabel = new QLabel;
0538     m_commandLabel->setText(i18nc("@label:textbox", "Command:"));
0539     m_hLayout->addWidget(m_commandLabel);
0540     m_commandEdit = new QLineEdit;
0541     m_hLayout->addWidget(m_commandEdit);
0542     m_vLayout->addSpacing(10);
0543     m_bottomLabel = new QLabel;
0544     m_vLayout->addWidget(m_bottomLabel);
0545     m_bottomLabel->setTextFormat(Qt::RichText);
0546     m_bottomLabel->setText(
0547         i18n("<i>You can enter an arbitrary shell command.</i><br />"
0548              "The unformatted source-code is reached to the command <br />"
0549              "through the standard input, and the <br />"
0550              "formatted result is read from the standard output.<br />"
0551              "<br />"
0552              "If you add <b>$TMPFILE</b> into the command, then <br />"
0553              "a temporary file is used for transferring the code."));
0554     connect(m_commandEdit, &QLineEdit::textEdited,
0555             m_updateTimer, QOverload<>::of(&QTimer::start));
0556 
0557     m_vLayout->addSpacing(10);
0558 
0559     m_moreVariablesButton = new QPushButton(i18nc("@action:button", "More Variables"));
0560     connect(m_moreVariablesButton, &QPushButton::clicked, this, &CustomScriptPreferences::moreVariablesClicked);
0561     m_vLayout->addWidget(m_moreVariablesButton);
0562     m_vLayout->addStretch();
0563 }
0564 
0565 void CustomScriptPreferences::load(const KDevelop::SourceFormatterStyle& style)
0566 {
0567     m_style = style;
0568     m_commandEdit->setText(style.content());
0569     m_captionLabel->setText(i18n("Style: %1", style.caption()));
0570 
0571     updateTimeout();
0572 }
0573 
0574 QString CustomScriptPreferences::save() const
0575 {
0576     return m_commandEdit->text();
0577 }
0578 
0579 void CustomScriptPreferences::moreVariablesClicked(bool)
0580 {
0581     KMessageBox::information(ICore::self()->uiController()->activeMainWindow(),
0582                              i18n("<b>$TMPFILE</b> will be replaced with the path to a temporary file. <br />"
0583                                   "The code will be written into the file, the temporary <br />"
0584                                   "file will be substituted into that position, and the result <br />"
0585                                   "will be read out of that file. <br />"
0586                                   "<br />"
0587                                   "<b>$FILE</b> will be replaced with the path of the original file. <br />"
0588                                   "The contents of the file must not be modified, changes are allowed <br />"
0589                                   "only in $TMPFILE.<br />"
0590                                   "<br />"
0591                                   "<b>${&lt;project name&gt;}</b> will be replaced by the path of <br />"
0592                                   "a currently open project whose name is &lt;project name&gt;."
0593 
0594                                   ), i18nc("@title:window", "Variable Replacements"));
0595 }
0596 
0597 #include "customscript_plugin.moc"
0598 #include "moc_customscript_plugin.cpp"