File indexing completed on 2024-04-28 04:37:26
0001 /* 0002 SPDX-FileCopyrightText: 2009 Andreas Pakulat <apaku@gmx.de> 0003 SPDX-FileCopyrightText: 2008 Cédric Pasteur <cedric.pasteur@free.fr> 0004 SPDX-FileCopyrightText: 2021 Igor Kushnir <igorkuo@gmail.com> 0005 0006 SPDX-License-Identifier: LGPL-2.0-or-later 0007 */ 0008 0009 #include "sourceformattercontroller.h" 0010 0011 #include <QAction> 0012 #include <QAbstractButton> 0013 #include <QByteArray> 0014 #include <QMimeDatabase> 0015 #include <QRegExp> 0016 #include <QString> 0017 #include <QStringList> 0018 #include <QStringView> 0019 #include <QUrl> 0020 #include <QPointer> 0021 #include <QMessageBox> 0022 0023 #include <KActionCollection> 0024 #include <KIO/StoredTransferJob> 0025 #include <KLocalizedString> 0026 #include <KTextEditor/Command> 0027 #include <KTextEditor/Document> 0028 #include <KTextEditor/Editor> 0029 #include <KTextEditor/View> 0030 #include <KParts/MainWindow> 0031 0032 #include <interfaces/context.h> 0033 #include <interfaces/contextmenuextension.h> 0034 #include <interfaces/icore.h> 0035 #include <interfaces/idocument.h> 0036 #include <interfaces/idocumentcontroller.h> 0037 #include <interfaces/iplugincontroller.h> 0038 #include <interfaces/iproject.h> 0039 #include <interfaces/iprojectcontroller.h> 0040 #include <interfaces/iruncontroller.h> 0041 #include <interfaces/isession.h> 0042 #include <interfaces/isourceformatter.h> 0043 #include <interfaces/iuicontroller.h> 0044 #include <language/codegen/coderepresentation.h> 0045 #include <language/interfaces/ilanguagesupport.h> 0046 #include <project/projectmodel.h> 0047 #include <util/path.h> 0048 #include <util/owningrawpointercontainer.h> 0049 0050 #include "core.h" 0051 #include "debug.h" 0052 #include "plugincontroller.h" 0053 #include "sourceformatterconfig.h" 0054 #include "sourceformatterjob.h" 0055 #include "textdocument.h" 0056 0057 #include <algorithm> 0058 #include <memory> 0059 #include <tuple> 0060 #include <utility> 0061 0062 using namespace KDevelop; 0063 0064 namespace { 0065 namespace Config { 0066 namespace Strings { 0067 QByteArray sourceFormatter() 0068 { 0069 return QByteArrayLiteral("SourceFormatter"); 0070 } 0071 constexpr const char* useDefault = "UseDefault"; 0072 } 0073 0074 KConfigGroup projectConfig(const IProject& project) 0075 { 0076 return project.projectConfiguration()->group(Strings::sourceFormatter()); 0077 } 0078 0079 KConfigGroup sessionConfig() 0080 { 0081 return Core::self()->activeSession()->config()->group(Strings::sourceFormatter()); 0082 } 0083 0084 KConfigGroup globalConfig() 0085 { 0086 return KSharedConfig::openConfig()->group(Strings::sourceFormatter()); 0087 } 0088 0089 bool projectOverridesSession(const KConfigGroup& projectConfig) 0090 { 0091 return projectConfig.isValid() && !projectConfig.readEntry(Strings::useDefault, true); 0092 } 0093 0094 KConfigGroup configForUrl(const QUrl& url) 0095 { 0096 const auto* const project = Core::self()->projectController()->findProjectForUrl(url); 0097 if (project) { 0098 auto config = projectConfig(*project); 0099 if (projectOverridesSession(config)) { 0100 return config; 0101 } 0102 } 0103 return sessionConfig(); 0104 } 0105 0106 void populateStyleFromConfig(SourceFormatterStyle& style, const KConfigGroup& styleConfig) 0107 { 0108 style.setCaption(styleConfig.readEntry(SourceFormatterController::styleCaptionKey(), QString{})); 0109 style.setUsePreview(styleConfig.readEntry(SourceFormatterController::styleShowPreviewKey(), false)); 0110 style.setContent(styleConfig.readEntry(SourceFormatterController::styleContentKey(), QString{})); 0111 style.setMimeTypes( 0112 styleConfig.readEntry<QStringList>(SourceFormatterController::styleMimeTypesKey(), QStringList{})); 0113 style.setOverrideSample(styleConfig.readEntry(SourceFormatterController::styleSampleKey(), QString{})); 0114 } 0115 0116 struct FormatterData 0117 { 0118 const ISourceFormatter* formatter = nullptr; 0119 QString styleName; 0120 0121 bool isValid() const 0122 { 0123 return formatter; 0124 } 0125 0126 SourceFormatterStyle style() const 0127 { 0128 Q_ASSERT(formatter); 0129 SourceFormatterStyle style(styleName); 0130 const KConfigGroup config = globalConfig().group(formatter->name()); 0131 if (config.hasGroup(styleName)) { 0132 populateStyleFromConfig(style, config.group(styleName)); 0133 } 0134 return style; 0135 } 0136 }; 0137 0138 FormatterData readFormatterData(const KConfigGroup& sourceFormatterConfig, const QString& mimeTypeName, 0139 const QVector<ISourceFormatter*>& formatters) 0140 { 0141 FormatterData result{}; 0142 0143 SourceFormatter::ConfigForMimeType parser(sourceFormatterConfig, mimeTypeName); 0144 if (!parser.isValid()) { 0145 return result; 0146 } 0147 0148 const auto it = std::find_if(formatters.cbegin(), formatters.cend(), 0149 [formatterName = parser.formatterName()](const ISourceFormatter* f) { 0150 return f->name() == formatterName; 0151 }); 0152 if (it != formatters.cend()) { 0153 result.formatter = *it; 0154 result.styleName = std::move(parser).takeStyleName(); 0155 } 0156 0157 return result; 0158 } 0159 0160 } // namespace Config 0161 } // unnamed namespace 0162 0163 namespace KDevelop { 0164 class SourceFormatterControllerPrivate 0165 { 0166 public: 0167 // cache of formatter plugins, to avoid querying plugincontroller 0168 QVector<ISourceFormatter*> sourceFormatters; 0169 // GUI actions 0170 QAction* formatTextAction; 0171 QAction* formatFilesAction; 0172 QAction* formatLine; 0173 QList<KDevelop::ProjectBaseItem*> prjItems; 0174 QList<QUrl> urls; 0175 bool enabled = true; 0176 }; 0177 0178 QString SourceFormatterController::kateModeLineConfigKey() 0179 { 0180 return QStringLiteral("ModelinesEnabled"); 0181 } 0182 0183 QString SourceFormatterController::kateOverrideIndentationConfigKey() 0184 { 0185 return QStringLiteral("OverrideKateIndentation"); 0186 } 0187 0188 QString SourceFormatterController::styleCaptionKey() 0189 { 0190 return QStringLiteral("Caption"); 0191 } 0192 0193 QString SourceFormatterController::styleShowPreviewKey() 0194 { 0195 return QStringLiteral("ShowPreview"); 0196 } 0197 0198 QString SourceFormatterController::styleContentKey() 0199 { 0200 return QStringLiteral("Content"); 0201 } 0202 0203 QString SourceFormatterController::styleMimeTypesKey() 0204 { 0205 return QStringLiteral("MimeTypes"); 0206 } 0207 0208 QString SourceFormatterController::styleSampleKey() 0209 { 0210 return QStringLiteral("StyleSample"); 0211 } 0212 0213 SourceFormatterController::SourceFormatterController(QObject *parent) 0214 : ISourceFormatterController(parent) 0215 , d_ptr(new SourceFormatterControllerPrivate) 0216 { 0217 Q_D(SourceFormatterController); 0218 0219 setObjectName(QStringLiteral("SourceFormatterController")); 0220 setComponentName(QStringLiteral("kdevsourceformatter"), i18n("Source Formatter")); 0221 setXMLFile(QStringLiteral("kdevsourceformatter.rc")); 0222 0223 if (Core::self()->setupFlags() & Core::NoUi) return; 0224 0225 d->formatTextAction = actionCollection()->addAction(QStringLiteral("edit_reformat_source")); 0226 d->formatTextAction->setText(i18nc("@action", "&Reformat Source")); 0227 connect(d->formatTextAction, &QAction::triggered, this, &SourceFormatterController::beautifySource); 0228 0229 d->formatLine = actionCollection()->addAction(QStringLiteral("edit_reformat_line")); 0230 d->formatLine->setText(i18nc("@action", "Reformat Line")); 0231 connect(d->formatLine, &QAction::triggered, this, &SourceFormatterController::beautifyLine); 0232 0233 d->formatFilesAction = actionCollection()->addAction(QStringLiteral("tools_astyle")); 0234 d->formatFilesAction->setText(i18nc("@action", "Reformat Files...")); 0235 d->formatFilesAction->setToolTip(i18nc("@info:tooltip", "Format file(s) using the configured source formatter(s)")); 0236 d->formatFilesAction->setWhatsThis(i18nc("@info:whatsthis", 0237 "Formatting functionality using the configured source formatter(s).")); 0238 d->formatFilesAction->setEnabled(false); 0239 connect(d->formatFilesAction, &QAction::triggered, 0240 this, QOverload<>::of(&SourceFormatterController::formatFiles)); 0241 0242 0243 connect(Core::self()->pluginController(), &IPluginController::pluginLoaded, 0244 this, &SourceFormatterController::pluginLoaded); 0245 connect(Core::self()->pluginController(), &IPluginController::unloadingPlugin, 0246 this, &SourceFormatterController::unloadingPlugin); 0247 0248 // connect to both documentActivated & documentClosed, 0249 // otherwise we miss when the last document was closed 0250 connect(Core::self()->documentController(), &IDocumentController::documentActivated, 0251 this, &SourceFormatterController::updateFormatTextAction); 0252 connect(Core::self()->documentController(), &IDocumentController::documentClosed, 0253 this, &SourceFormatterController::updateFormatTextAction); 0254 connect(Core::self()->documentController(), &IDocumentController::documentUrlChanged, this, 0255 &SourceFormatterController::updateFormatTextAction); 0256 0257 qRegisterMetaType<QPointer<KDevelop::TextDocument>>(); 0258 connect(Core::self()->documentController(), &IDocumentController::documentLoaded, 0259 // Use a queued connection, because otherwise the view is not yet fully set up 0260 // but wrap the document in a smart pointer to guard against crashes when it 0261 // gets deleted in the meantime 0262 this, [this](IDocument *doc) { 0263 const auto textDoc = QPointer<TextDocument>(dynamic_cast<TextDocument*>(doc)); 0264 QMetaObject::invokeMethod(this, "documentLoaded", Qt::QueuedConnection, Q_ARG(QPointer<KDevelop::TextDocument>, textDoc)); 0265 }); 0266 connect(Core::self()->projectController(), &IProjectController::projectOpened, this, [d](const IProject* project) { 0267 FileFormatter::projectOpened(*project, d->sourceFormatters); 0268 }); 0269 0270 updateFormatTextAction(); 0271 } 0272 0273 void SourceFormatterController::documentLoaded(const QPointer<TextDocument>& doc) 0274 { 0275 Q_D(const SourceFormatterController); 0276 0277 // NOTE: explicitly check this here to prevent crashes on shutdown 0278 // when this slot gets called (note: delayed connection) 0279 // but the text document was already destroyed 0280 // there have been unit tests that failed due to that... 0281 if (!doc || !doc->textDocument()) { 0282 return; 0283 } 0284 FileFormatter ff(doc->url()); 0285 if (ff.readFormatterAndStyle(d->sourceFormatters)) { 0286 ff.adaptEditorIndentationMode(doc->textDocument()); 0287 } 0288 } 0289 0290 void SourceFormatterController::FileFormatter::projectOpened(const IProject& project, 0291 const QVector<ISourceFormatter*>& formatters) 0292 { 0293 // Adapt the indentation mode if a project was just opened. Otherwise if a document 0294 // is loaded before its project, it might not have the correct indentation mode set. 0295 0296 if (formatters.empty()) { 0297 return; 0298 } 0299 0300 const auto config = Config::projectConfig(project); 0301 if (!Config::projectOverridesSession(config)) { 0302 return; // The opened project does not specify indentation => nothing to do. 0303 } 0304 0305 OwningRawPointerContainer<QHash<QString, FileFormatter*>> fileFormatterCache; 0306 const auto fileFormatterForUrl = [&fileFormatterCache, &config, &formatters](QUrl&& url) { 0307 auto mimeType = QMimeDatabase().mimeTypeForUrl(url); 0308 const auto mimeTypeName = mimeType.name(); 0309 0310 auto ff = fileFormatterCache->value(mimeTypeName); 0311 if (ff) { 0312 ff->m_url = std::move(url); 0313 Q_ASSERT_X(ff->m_mimeType == mimeType, Q_FUNC_INFO, 0314 "When MIME type names are equal, the MIME types must also compare equal."); 0315 // All other ff's data members already have correct values: 0316 // * m_sourceFormatterConfig equals config for all elements of fileFormatterCache; 0317 // * m_formatter and m_style are determined by config and m_mimeType. 0318 } else { 0319 const auto data = Config::readFormatterData(config, mimeTypeName, formatters); 0320 if (data.isValid()) { 0321 ff = new FileFormatter(std::move(url), std::move(mimeType), config, data.formatter, data.style()); 0322 } 0323 fileFormatterCache->insert(mimeTypeName, ff); 0324 } 0325 return std::as_const(ff); 0326 }; 0327 0328 const auto documents = ICore::self()->documentController()->openDocuments(); 0329 for (const IDocument* doc : documents) { 0330 auto url = doc->url(); 0331 if (!project.inProject(IndexedString{url})) { 0332 continue; 0333 } 0334 if (const auto* const ff = fileFormatterForUrl(std::move(url))) { 0335 ff->adaptEditorIndentationMode(doc->textDocument()); 0336 } 0337 } 0338 } 0339 0340 void SourceFormatterController::pluginLoaded(IPlugin* plugin) 0341 { 0342 Q_D(SourceFormatterController); 0343 0344 auto* sourceFormatter = plugin->extension<ISourceFormatter>(); 0345 0346 if (!sourceFormatter || !d->enabled) { 0347 return; 0348 } 0349 0350 d->sourceFormatters << sourceFormatter; 0351 0352 resetUi(); 0353 0354 emit formatterLoaded(sourceFormatter); 0355 // with one plugin now added, hasFormatters turned to true, so report to listeners 0356 if (d->sourceFormatters.size() == 1) { 0357 emit hasFormattersChanged(true); 0358 } 0359 } 0360 0361 void SourceFormatterController::unloadingPlugin(IPlugin* plugin) 0362 { 0363 Q_D(SourceFormatterController); 0364 0365 auto* sourceFormatter = plugin->extension<ISourceFormatter>(); 0366 0367 if (!sourceFormatter || !d->enabled) { 0368 return; 0369 } 0370 0371 const int idx = d->sourceFormatters.indexOf(sourceFormatter); 0372 Q_ASSERT(idx != -1); 0373 d->sourceFormatters.remove(idx); 0374 0375 resetUi(); 0376 0377 emit formatterUnloading(sourceFormatter); 0378 if (d->sourceFormatters.isEmpty()) { 0379 emit hasFormattersChanged(false); 0380 } 0381 } 0382 0383 0384 void SourceFormatterController::initialize() 0385 { 0386 } 0387 0388 SourceFormatterController::~SourceFormatterController() 0389 { 0390 } 0391 0392 KConfigGroup SourceFormatterController::sessionConfig() const 0393 { 0394 return Config::sessionConfig(); 0395 } 0396 0397 KConfigGroup SourceFormatterController::globalConfig() const 0398 { 0399 return Config::globalConfig(); 0400 } 0401 0402 auto SourceFormatterController::stylesForFormatter(const ISourceFormatter& formatter) const -> StyleMap 0403 { 0404 StyleMap styles; 0405 0406 const auto predefinedStyles = formatter.predefinedStyles(); 0407 for (const auto& style : predefinedStyles) { 0408 const auto [it, inserted] = styles.try_emplace(style.name(), style); 0409 Q_ASSERT(it->second.name() == it->first); 0410 Q_ASSERT_X(inserted, Q_FUNC_INFO, "Duplicate predefined style!"); 0411 } 0412 0413 const auto commonConfig = globalConfig(); 0414 const QString formatterKey = formatter.name(); 0415 if (commonConfig.hasGroup(formatterKey)) { 0416 const auto formatterConfig = commonConfig.group(formatterKey); 0417 const auto subgroups = formatterConfig.groupList(); 0418 for (const QString& subgroup : subgroups) { 0419 const auto [it, inserted] = styles.insert_or_assign(subgroup, SourceFormatterStyle{subgroup}); 0420 Q_ASSERT(it->second.name() == it->first); 0421 if (!inserted) { 0422 // This overriding is an undocumented and possibly unintentional feature, which has existed 0423 // for more than 10 years. Source Formatter configuration UI creates styles named "UserN", 0424 // which cannot override predefined styles. But if the user edits kdeveloprc manually and 0425 // renames a style, it correctly overrides the predefined style for all intents and purposes. 0426 qCDebug(SHELL).noquote() << QStringLiteral( 0427 "A user-defined style config group [%1][%2][%3] " 0428 "overrides a predefined style with the same name.") 0429 .arg(QString::fromLatin1(Config::Strings::sourceFormatter()), 0430 formatterKey, subgroup); 0431 } 0432 Config::populateStyleFromConfig(it->second, formatterConfig.group(subgroup)); 0433 } 0434 } 0435 0436 return styles; 0437 } 0438 0439 SourceFormatterController::FileFormatter::FileFormatter(QUrl url) 0440 : m_url{std::move(url)} 0441 , m_mimeType{QMimeDatabase().mimeTypeForUrl(m_url)} 0442 { 0443 } 0444 0445 SourceFormatterController::FileFormatter::FileFormatter(QUrl&& url, QMimeType&& mimeType, 0446 const KConfigGroup& sourceFormatterConfig, 0447 const ISourceFormatter* formatter, SourceFormatterStyle&& style) 0448 : m_url{std::move(url)} 0449 , m_mimeType{std::move(mimeType)} 0450 , m_sourceFormatterConfig{sourceFormatterConfig} 0451 , m_formatter{formatter} 0452 , m_style{std::move(style)} 0453 { 0454 } 0455 0456 bool SourceFormatterController::FileFormatter::readFormatterAndStyle(const QVector<ISourceFormatter*>& formatters) 0457 { 0458 Q_ASSERT_X(!m_sourceFormatterConfig.isValid() && !m_formatter && m_style.name().isEmpty(), Q_FUNC_INFO, 0459 "This reinitialization must be a mistake."); 0460 0461 if (formatters.empty()) { 0462 return false; 0463 } 0464 0465 m_sourceFormatterConfig = Config::configForUrl(m_url); 0466 0467 const auto data = Config::readFormatterData(m_sourceFormatterConfig, m_mimeType.name(), formatters); 0468 if (!data.isValid()) { 0469 return false; 0470 } 0471 0472 m_formatter = data.formatter; 0473 m_style = data.style(); 0474 return true; 0475 } 0476 0477 QString SourceFormatterController::FileFormatter::formatterCaption() const 0478 { 0479 Q_ASSERT(m_formatter); 0480 return m_formatter->caption(); 0481 } 0482 0483 QString SourceFormatterController::FileFormatter::styleCaption() const 0484 { 0485 Q_ASSERT(m_formatter); 0486 auto styleCaption = m_style.caption(); 0487 if (styleCaption.isEmpty()) { 0488 // This could be an incomplete predefined style, for which only the name is stored in config. 0489 styleCaption = m_formatter->predefinedStyle(m_style.name()).caption(); 0490 } 0491 return styleCaption; 0492 } 0493 0494 QString SourceFormatterController::FileFormatter::format(const QString& text, const QString& leftContext, 0495 const QString& rightContext) const 0496 { 0497 Q_ASSERT(m_formatter); 0498 return m_formatter->formatSourceWithStyle(m_style, text, m_url, m_mimeType, leftContext, rightContext); 0499 } 0500 0501 /** 0502 * @return the name of kate indentation mode for @p mime, e.g. "cstyle", "python" 0503 */ 0504 static QString indentationMode(const QMimeType& mime) 0505 { 0506 if (mime.inherits(QStringLiteral("text/x-c++src")) || mime.inherits(QStringLiteral("text/x-chdr")) || 0507 mime.inherits(QStringLiteral("text/x-c++hdr")) || mime.inherits(QStringLiteral("text/x-csrc")) || 0508 mime.inherits(QStringLiteral("text/x-java")) || mime.inherits(QStringLiteral("text/x-csharp"))) { 0509 return QStringLiteral("cstyle"); 0510 } 0511 return QStringLiteral("none"); 0512 } 0513 0514 QString SourceFormatterController::FileFormatter::addModeline(QString input) const 0515 { 0516 Q_ASSERT(m_formatter); 0517 0518 QRegExp kateModelineWithNewline(QStringLiteral("\\s*\\n//\\s*kate:(.*)$")); 0519 0520 // If there already is a modeline in the document, adapt it while formatting, even 0521 // if "add modeline" is disabled. 0522 if (!m_sourceFormatterConfig.readEntry(SourceFormatterController::kateModeLineConfigKey(), false) 0523 && kateModelineWithNewline.indexIn(input) == -1) 0524 return input; 0525 0526 const auto indentation = m_formatter->indentation(m_style, m_url, m_mimeType); 0527 if( !indentation.isValid() ) 0528 return input; 0529 0530 QString output; 0531 QTextStream os(&output, QIODevice::WriteOnly); 0532 QTextStream is(&input, QIODevice::ReadOnly); 0533 0534 QString modeline(QStringLiteral("// kate: ") + QLatin1String("indent-mode ") + indentationMode(m_mimeType) 0535 + QLatin1String("; ")); 0536 0537 if(indentation.indentWidth) // We know something about indentation-width 0538 modeline.append(QStringLiteral("indent-width %1; ").arg(indentation.indentWidth)); 0539 0540 if(indentation.indentationTabWidth != 0) // We know something about tab-usage 0541 { 0542 const auto state = (indentation.indentationTabWidth == -1) ? QLatin1String("on") : QLatin1String("off"); 0543 modeline += QLatin1String("replace-tabs ") + state + QLatin1String("; "); 0544 if(indentation.indentationTabWidth > 0) 0545 modeline.append(QStringLiteral("tab-width %1; ").arg(indentation.indentationTabWidth)); 0546 } 0547 0548 qCDebug(SHELL) << "created modeline: " << modeline; 0549 0550 QRegExp kateModeline(QStringLiteral("^\\s*//\\s*kate:(.*)$")); 0551 0552 bool modelinefound = false; 0553 QRegExp knownOptions(QStringLiteral("\\s*(indent-width|space-indent|tab-width|indent-mode|replace-tabs)")); 0554 while (!is.atEnd()) { 0555 QString line = is.readLine(); 0556 // replace only the options we care about 0557 if (kateModeline.indexIn(line) >= 0) { // match 0558 qCDebug(SHELL) << "Found a kate modeline: " << line; 0559 modelinefound = true; 0560 QString options = kateModeline.cap(1); 0561 const QStringList optionList = options.split(QLatin1Char(';'), Qt::SkipEmptyParts); 0562 0563 os << modeline; 0564 for (QString s : optionList) { 0565 if (knownOptions.indexIn(s) < 0) { // unknown option, add it 0566 if(s.startsWith(QLatin1Char(' '))) 0567 s.remove(0, 1); 0568 os << s << ";"; 0569 qCDebug(SHELL) << "Found unknown option: " << s; 0570 } 0571 } 0572 os << Qt::endl; 0573 } else 0574 os << line << Qt::endl; 0575 } 0576 0577 if (!modelinefound) 0578 os << modeline << Qt::endl; 0579 return output; 0580 } 0581 0582 void SourceFormatterController::cleanup() 0583 { 0584 } 0585 0586 void SourceFormatterController::updateFormatTextAction() 0587 { 0588 Q_D(const SourceFormatterController); 0589 0590 const auto [enabled, tool, style] = [d] { 0591 auto disabled = std::tuple{false, QString(), QString()}; 0592 0593 const auto* doc = KDevelop::ICore::self()->documentController()->activeDocument(); 0594 if (!doc) { 0595 return disabled; 0596 } 0597 0598 FileFormatter ff(doc->url()); 0599 if (!ff.readFormatterAndStyle(d->sourceFormatters)) { 0600 return disabled; 0601 } 0602 0603 return std::tuple{true, ff.formatterCaption(), ff.styleCaption()}; 0604 }(); 0605 0606 d->formatTextAction->setEnabled(enabled); 0607 d->formatLine->setEnabled(enabled); 0608 0609 if (enabled) { 0610 d->formatTextAction->setToolTip(i18nc("@info:tooltip", "Reformat selection or file using <i>%1</i> (<b>%2</b>)", 0611 tool, style)); 0612 d->formatTextAction->setWhatsThis(i18nc("@info:whatsthis", 0613 "Reformats selected text or the entire file if nothing is selected, using <i>%1</i> tool with <b>%2</b> style.", 0614 tool, style)); 0615 d->formatLine->setToolTip(i18nc("@info:tooltip", "Reformat current line using <i>%1</i> (<b>%2</b>)", 0616 tool, style)); 0617 d->formatLine->setWhatsThis(i18nc("@info:whatsthis", 0618 "Source reformatting of line under cursor using <i>%1</i> tool with <b>%2</b> style.", 0619 tool, style)); 0620 } else { 0621 d->formatTextAction->setToolTip(i18nc("@info:tooltip", 0622 "Reformat selection or file using the configured source formatter")); 0623 d->formatTextAction->setWhatsThis(i18nc("@info:whatsthis", 0624 "Reformats selected text or the entire file if nothing is selected, using the configured source formatter.")); 0625 d->formatLine->setToolTip(i18nc("@info:tooltip", 0626 "Reformat current line using the configured source formatter")); 0627 d->formatLine->setWhatsThis(i18nc("@info:whatsthis", 0628 "Source reformatting of line under cursor using the configured source formatter.")); 0629 } 0630 } 0631 0632 void SourceFormatterController::beautifySource() 0633 { 0634 Q_D(const SourceFormatterController); 0635 0636 IDocument* idoc = KDevelop::ICore::self()->documentController()->activeDocument(); 0637 if (!idoc) 0638 return; 0639 KTextEditor::View* view = idoc->activeTextView(); 0640 if (!view) 0641 return; 0642 KTextEditor::Document* doc = view->document(); 0643 // load the appropriate formatter 0644 const auto url = idoc->url(); 0645 FileFormatter ff(url); 0646 if (!ff.readFormatterAndStyle(d->sourceFormatters)) { 0647 qCDebug(SHELL) << "no formatter available for" << url; 0648 return; 0649 } 0650 0651 // Ignore the modeline, as the modeline will be changed anyway 0652 ff.adaptEditorIndentationMode(doc, true); 0653 0654 bool has_selection = view->selection(); 0655 0656 if (has_selection) { 0657 QString original = view->selectionText(); 0658 0659 QString output = 0660 ff.format(view->selectionText(), 0661 doc->text(KTextEditor::Range(KTextEditor::Cursor(0, 0), view->selectionRange().start())), 0662 doc->text(KTextEditor::Range(view->selectionRange().end(), doc->documentRange().end()))); 0663 0664 //remove the final newline character, unless it should be there 0665 if (!original.endsWith(QLatin1Char('\n')) && output.endsWith(QLatin1Char('\n'))) 0666 output.resize(output.length() - 1); 0667 //there was a selection, so only change the part of the text related to it 0668 0669 // We don't use KTextEditor::Document directly, because CodeRepresentation transparently works 0670 // around a possible tab-replacement incompatibility between kate and kdevelop 0671 DynamicCodeRepresentation::Ptr code( dynamic_cast<DynamicCodeRepresentation*>( KDevelop::createCodeRepresentation( IndexedString( doc->url() ) ).data() ) ); 0672 Q_ASSERT( code ); 0673 code->replace( view->selectionRange(), original, output ); 0674 } else { 0675 ff.formatDocument(*idoc); 0676 } 0677 } 0678 0679 void SourceFormatterController::beautifyLine() 0680 { 0681 Q_D(const SourceFormatterController); 0682 0683 KDevelop::IDocumentController *docController = KDevelop::ICore::self()->documentController(); 0684 KDevelop::IDocument *doc = docController->activeDocument(); 0685 if (!doc) 0686 return; 0687 KTextEditor::Document *tDoc = doc->textDocument(); 0688 if (!tDoc) 0689 return; 0690 KTextEditor::View* view = doc->activeTextView(); 0691 if (!view) 0692 return; 0693 // load the appropriate formatter 0694 const auto url = doc->url(); 0695 FileFormatter ff(url); 0696 if (!ff.readFormatterAndStyle(d->sourceFormatters)) { 0697 qCDebug(SHELL) << "no formatter available for" << url; 0698 return; 0699 } 0700 0701 const KTextEditor::Cursor cursor = view->cursorPosition(); 0702 const QString line = tDoc->line(cursor.line()); 0703 const QString prev = tDoc->text(KTextEditor::Range(0, 0, cursor.line(), 0)); 0704 const QString post = QLatin1Char('\n') + tDoc->text(KTextEditor::Range(KTextEditor::Cursor(cursor.line() + 1, 0), tDoc->documentEnd())); 0705 0706 const QString formatted = ff.format(line, prev, post); 0707 0708 // We don't use KTextEditor::Document directly, because CodeRepresentation transparently works 0709 // around a possible tab-replacement incompatibility between kate and kdevelop 0710 DynamicCodeRepresentation::Ptr code(dynamic_cast<DynamicCodeRepresentation*>( KDevelop::createCodeRepresentation( IndexedString( doc->url() ) ).data() ) ); 0711 Q_ASSERT( code ); 0712 code->replace( KTextEditor::Range(cursor.line(), 0, cursor.line(), line.length()), line, formatted ); 0713 0714 // advance cursor one line 0715 view->setCursorPosition(KTextEditor::Cursor(cursor.line() + 1, 0)); 0716 } 0717 0718 void SourceFormatterController::FileFormatter::formatDocument(IDocument& doc) const 0719 { 0720 Q_ASSERT(m_formatter); 0721 Q_ASSERT(doc.url() == m_url); 0722 0723 qCDebug(SHELL) << "Running" << m_formatter->name() << "on" << m_url; 0724 0725 // We don't use KTextEditor::Document directly, because CodeRepresentation transparently works 0726 // around a possible tab-replacement incompatibility between kate and kdevelop 0727 auto code = KDevelop::createCodeRepresentation(IndexedString{m_url}); 0728 0729 const auto cursor = doc.cursorPosition(); 0730 0731 QString text = format(code->text()); 0732 text = addModeline(text); 0733 code->setText(text); 0734 0735 doc.setCursorPosition(cursor); 0736 } 0737 0738 void SourceFormatterController::settingsChanged() 0739 { 0740 Q_D(const SourceFormatterController); 0741 0742 const auto documents = ICore::self()->documentController()->openDocuments(); 0743 for (KDevelop::IDocument* doc : documents) { 0744 FileFormatter ff(doc->url()); 0745 if (ff.readFormatterAndStyle(d->sourceFormatters)) { 0746 ff.adaptEditorIndentationMode(doc->textDocument()); 0747 } 0748 } 0749 updateFormatTextAction(); 0750 } 0751 0752 /** 0753 * Kate commands: 0754 * Use spaces for indentation: 0755 * "set-replace-tabs 1" 0756 * Use tabs for indentation (eventually mixed): 0757 * "set-replace-tabs 0" 0758 * Indent width: 0759 * "set-indent-width X" 0760 * Tab width: 0761 * "set-tab-width X" 0762 * */ 0763 0764 void SourceFormatterController::FileFormatter::adaptEditorIndentationMode(KTextEditor::Document* doc, 0765 bool ignoreModeline) const 0766 { 0767 Q_ASSERT(m_formatter); 0768 if (!doc 0769 || !m_sourceFormatterConfig.readEntry(SourceFormatterController::kateOverrideIndentationConfigKey(), false)) 0770 return; 0771 0772 qCDebug(SHELL) << "adapting mode for" << m_url; 0773 0774 QRegExp kateModelineWithNewline(QStringLiteral("\\s*\\n//\\s*kate:(.*)$")); 0775 0776 // modelines should always take precedence 0777 if( !ignoreModeline && kateModelineWithNewline.indexIn( doc->text() ) != -1 ) 0778 { 0779 qCDebug(SHELL) << "ignoring because a kate modeline was found"; 0780 return; 0781 } 0782 0783 const auto indentation = m_formatter->indentation(m_style, m_url, m_mimeType); 0784 if(indentation.isValid()) 0785 { 0786 struct CommandCaller { 0787 explicit CommandCaller(KTextEditor::Document* _doc) : doc(_doc), editor(KTextEditor::Editor::instance()) { 0788 Q_ASSERT(editor); 0789 } 0790 void operator()(const QString& cmd) { 0791 KTextEditor::Command* command = editor->queryCommand( cmd ); 0792 Q_ASSERT(command); 0793 QString msg; 0794 qCDebug(SHELL) << "calling" << cmd; 0795 const auto views = doc->views(); 0796 for (KTextEditor::View* view : views) { 0797 if (!command->exec(view, cmd, msg)) 0798 qCWarning(SHELL) << "setting indentation width failed: " << msg; 0799 } 0800 } 0801 0802 KTextEditor::Document* doc; 0803 KTextEditor::Editor* editor; 0804 } call(doc); 0805 0806 if( indentation.indentWidth ) // We know something about indentation-width 0807 call( QStringLiteral("set-indent-width %1").arg(indentation.indentWidth ) ); 0808 0809 if( indentation.indentationTabWidth != 0 ) // We know something about tab-usage 0810 { 0811 call( QStringLiteral("set-replace-tabs %1").arg( (indentation.indentationTabWidth == -1) ? 1 : 0 ) ); 0812 if( indentation.indentationTabWidth > 0 ) 0813 call( QStringLiteral("set-tab-width %1").arg(indentation.indentationTabWidth ) ); 0814 } 0815 }else{ 0816 qCDebug(SHELL) << "found no valid indentation"; 0817 } 0818 } 0819 0820 void SourceFormatterController::formatFiles() 0821 { 0822 Q_D(SourceFormatterController); 0823 0824 if (d->prjItems.isEmpty() && d->urls.isEmpty()) 0825 return; 0826 0827 //get a list of all files in this folder recursively 0828 QList<KDevelop::ProjectFolderItem*> folders; 0829 for (KDevelop::ProjectBaseItem* item : qAsConst(d->prjItems)) { 0830 if (!item) 0831 continue; 0832 if (item->folder()) 0833 folders.append(item->folder()); 0834 else if (item->file()) 0835 d->urls.append(item->file()->path().toUrl()); 0836 else if (item->target()) { 0837 const auto files = item->fileList(); 0838 for (KDevelop::ProjectFileItem* f : files) { 0839 d->urls.append(f->path().toUrl()); 0840 } 0841 } 0842 } 0843 0844 while (!folders.isEmpty()) { 0845 KDevelop::ProjectFolderItem *item = folders.takeFirst(); 0846 const auto folderList = item->folderList(); 0847 for (KDevelop::ProjectFolderItem* f : folderList) { 0848 folders.append(f); 0849 } 0850 const auto targets = item->targetList(); 0851 for (KDevelop::ProjectTargetItem* f : targets) { 0852 const auto childs = f->fileList(); 0853 for (KDevelop::ProjectFileItem* child : childs) { 0854 d->urls.append(child->path().toUrl()); 0855 } 0856 } 0857 const auto files = item->fileList(); 0858 for (KDevelop::ProjectFileItem* f : files) { 0859 d->urls.append(f->path().toUrl()); 0860 } 0861 } 0862 0863 auto win = ICore::self()->uiController()->activeMainWindow()->window(); 0864 0865 QMessageBox msgBox(QMessageBox::Question, i18nc("@title:window", "Reformat Files?"), 0866 i18n("Reformat all files in the selected folder?"), 0867 QMessageBox::Ok|QMessageBox::Cancel, win); 0868 msgBox.setDefaultButton(QMessageBox::Cancel); 0869 auto okButton = msgBox.button(QMessageBox::Ok); 0870 okButton->setText(i18nc("@action:button", "Reformat")); 0871 msgBox.exec(); 0872 0873 if (msgBox.clickedButton() == okButton) { 0874 auto formatterJob = new SourceFormatterJob(this); 0875 formatterJob->setFiles(d->urls); 0876 ICore::self()->runController()->registerJob(formatterJob); 0877 } 0878 } 0879 0880 KDevelop::ContextMenuExtension SourceFormatterController::contextMenuExtension(KDevelop::Context* context, QWidget* parent) 0881 { 0882 Q_D(SourceFormatterController); 0883 0884 Q_UNUSED(parent); 0885 0886 KDevelop::ContextMenuExtension ext; 0887 d->urls.clear(); 0888 d->prjItems.clear(); 0889 0890 if (d->sourceFormatters.isEmpty()) { 0891 return ext; 0892 } 0893 0894 if (context->hasType(KDevelop::Context::EditorContext)) 0895 { 0896 if (d->formatTextAction->isEnabled()) 0897 ext.addAction(KDevelop::ContextMenuExtension::EditGroup, d->formatTextAction); 0898 } else if (context->hasType(KDevelop::Context::FileContext)) { 0899 auto* filectx = static_cast<KDevelop::FileContext*>(context); 0900 d->urls = filectx->urls(); 0901 ext.addAction(KDevelop::ContextMenuExtension::EditGroup, d->formatFilesAction); 0902 } else if (context->hasType(KDevelop::Context::CodeContext)) { 0903 } else if (context->hasType(KDevelop::Context::ProjectItemContext)) { 0904 auto* prjctx = static_cast<KDevelop::ProjectItemContext*>(context); 0905 d->prjItems = prjctx->items(); 0906 if (!d->prjItems.isEmpty()) { 0907 ext.addAction(KDevelop::ContextMenuExtension::ExtensionGroup, d->formatFilesAction); 0908 } 0909 } 0910 return ext; 0911 } 0912 0913 auto SourceFormatterController::fileFormatter(const QUrl& url) const -> FileFormatterPtr 0914 { 0915 Q_D(const SourceFormatterController); 0916 0917 auto ff = std::make_unique<FileFormatter>(url); 0918 if (ff->readFormatterAndStyle(d->sourceFormatters)) { 0919 return ff; 0920 } 0921 return nullptr; 0922 } 0923 0924 void SourceFormatterController::disableSourceFormatting() 0925 { 0926 Q_D(SourceFormatterController); 0927 0928 d->enabled = false; 0929 0930 if (d->sourceFormatters.empty()) { 0931 return; 0932 } 0933 0934 decltype(d->sourceFormatters) loadedFormatters{}; 0935 d->sourceFormatters.swap(loadedFormatters); 0936 0937 resetUi(); 0938 0939 for (auto* formatter : std::as_const(loadedFormatters)) { 0940 emit formatterUnloading(formatter); 0941 } 0942 0943 Q_ASSERT(!loadedFormatters.empty()); 0944 Q_ASSERT(d->sourceFormatters.empty()); 0945 emit hasFormattersChanged(false); 0946 } 0947 0948 bool SourceFormatterController::sourceFormattingEnabled() 0949 { 0950 Q_D(SourceFormatterController); 0951 0952 return d->enabled; 0953 } 0954 0955 bool SourceFormatterController::hasFormatters() const 0956 { 0957 Q_D(const SourceFormatterController); 0958 0959 return !d->sourceFormatters.isEmpty(); 0960 } 0961 0962 QVector<ISourceFormatter*> SourceFormatterController::formatters() const 0963 { 0964 Q_D(const SourceFormatterController); 0965 0966 return d->sourceFormatters; 0967 } 0968 0969 void SourceFormatterController::resetUi() 0970 { 0971 Q_D(SourceFormatterController); 0972 0973 d->formatFilesAction->setEnabled(!d->sourceFormatters.isEmpty()); 0974 0975 updateFormatTextAction(); 0976 } 0977 0978 } 0979 0980 #include "moc_sourceformattercontroller.cpp"