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"