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>${<project name>}</b> will be replaced by the path of <br />" 0592 "a currently open project whose name is <project name>." 0593 0594 ), i18nc("@title:window", "Variable Replacements")); 0595 } 0596 0597 #include "customscript_plugin.moc" 0598 #include "moc_customscript_plugin.cpp"