File indexing completed on 2024-06-09 05:44:30

0001 /*
0002     SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com>
0003     SPDX-License-Identifier: LGPL-2.0-or-later
0004 */
0005 #include "FormatPlugin.h"
0006 
0007 #include "CursorPositionRestorer.h"
0008 #include "FormatConfig.h"
0009 #include "FormatterFactory.h"
0010 #include "Formatters.h"
0011 #include <json_utils.h>
0012 
0013 #include <KActionCollection>
0014 #include <KConfigGroup>
0015 #include <KLocalizedString>
0016 #include <KPluginFactory>
0017 #include <KSharedConfig>
0018 #include <KTextEditor/DocumentCursor>
0019 #include <KTextEditor/View>
0020 #include <KXMLGUIFactory>
0021 
0022 #include <QDir>
0023 #include <QPointer>
0024 
0025 K_PLUGIN_FACTORY_WITH_JSON(FormatPluginFactory, "FormatPlugin.json", registerPlugin<FormatPlugin>();)
0026 
0027 static QJsonDocument readDefaultConfig()
0028 {
0029     QFile defaultConfigFile(QStringLiteral(":/formatting/FormatterSettings.json"));
0030     defaultConfigFile.open(QIODevice::ReadOnly);
0031     Q_ASSERT(defaultConfigFile.isOpen());
0032     QJsonParseError err;
0033     auto doc = QJsonDocument::fromJson(defaultConfigFile.readAll(), &err);
0034     Q_ASSERT(err.error == QJsonParseError::NoError);
0035     return doc;
0036 }
0037 
0038 FormatPlugin::FormatPlugin(QObject *parent, const QVariantList &)
0039     : KTextEditor::Plugin(parent)
0040     , m_defaultConfig(readDefaultConfig())
0041 {
0042     readConfig();
0043 }
0044 
0045 QObject *FormatPlugin::createView(KTextEditor::MainWindow *mainWindow)
0046 {
0047     return new FormatPluginView(this, mainWindow);
0048 }
0049 
0050 KTextEditor::ConfigPage *FormatPlugin::configPage(int number, QWidget *parent)
0051 {
0052     if (number == 0) {
0053         return new FormatConfigPage(this, parent);
0054     }
0055     return nullptr;
0056 }
0057 
0058 void FormatPlugin::readConfig()
0059 {
0060     QString settingsPath(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + QStringLiteral("/formatting"));
0061     QDir().mkpath(settingsPath);
0062     readJsonConfig();
0063 
0064     formatOnSave = m_formatterConfig.value(QStringLiteral("formatOnSave")).toBool(true);
0065 }
0066 
0067 void FormatPlugin::readJsonConfig()
0068 {
0069     QJsonDocument userConfig;
0070     const QString path = userConfigPath();
0071     if (QFile::exists(path)) {
0072         QFile f(path);
0073         if (f.open(QFile::ReadOnly)) {
0074             QJsonParseError err;
0075             const QByteArray text = f.readAll();
0076             if (!text.isEmpty()) {
0077                 userConfig = QJsonDocument::fromJson(text, &err);
0078                 if (err.error != QJsonParseError::NoError) {
0079                     QMetaObject::invokeMethod(
0080                         this,
0081                         [err] {
0082                             Utils::showMessage(i18n("Failed to read settings.json. Error: %1", err.errorString()), QIcon(), i18n("Format"), MessageType::Error);
0083                         },
0084                         Qt::QueuedConnection);
0085                 }
0086             }
0087         }
0088     }
0089 
0090     if (!userConfig.isEmpty()) {
0091         m_formatterConfig = json::merge(m_defaultConfig.object(), userConfig.object());
0092     } else {
0093         m_formatterConfig = m_defaultConfig.object();
0094     }
0095 }
0096 
0097 QString FormatPlugin::userConfigPath() const
0098 {
0099     return QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + QStringLiteral("/formatting/settings.json");
0100 }
0101 
0102 QJsonObject FormatPlugin::formatterConfig() const
0103 {
0104     return m_formatterConfig;
0105 }
0106 
0107 FormatPluginView::FormatPluginView(FormatPlugin *plugin, KTextEditor::MainWindow *mainWin)
0108     : QObject(plugin)
0109     , m_plugin(plugin)
0110     , m_mainWindow(mainWin)
0111 {
0112     KXMLGUIClient::setComponentName(QStringLiteral("formatplugin"), i18n("Formatting"));
0113 
0114     connect(m_plugin, &FormatPlugin::configChanged, this, &FormatPluginView::onConfigChanged);
0115 
0116     auto ac = actionCollection();
0117     auto a = ac->addAction(QStringLiteral("format_document"), this, &FormatPluginView::format);
0118     a->setText(i18n("Format Document"));
0119     connect(mainWin, &KTextEditor::MainWindow::viewChanged, this, &FormatPluginView::onActiveViewChanged);
0120 
0121     const QString guiDescription = QStringLiteral(
0122         ""
0123         "<!DOCTYPE gui><gui name=\"formatplugin\">"
0124         "<MenuBar>"
0125         "    <Menu name=\"tools\">"
0126         "        <Action name=\"format_on_save\"/>"
0127         "    </Menu>"
0128         "</MenuBar>"
0129         "</gui>");
0130     setXML(guiDescription);
0131     a = actionCollection()->addAction(QStringLiteral("format_on_save"), this, [this](bool b) {
0132         m_plugin->formatOnSave = b;
0133         onActiveViewChanged(nullptr);
0134         onActiveViewChanged(m_mainWindow->activeView());
0135     });
0136     a->setText(i18n("Format on Save"));
0137     a->setCheckable(true);
0138     a->setChecked(formatOnSave());
0139     a->setToolTip(i18n("Disable formatting on save without persisting it in settings"));
0140 
0141     m_mainWindow->guiFactory()->addClient(this);
0142 }
0143 
0144 FormatPluginView::~FormatPluginView()
0145 {
0146     disconnect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &FormatPluginView::onActiveViewChanged);
0147     m_mainWindow->guiFactory()->removeClient(this);
0148 }
0149 
0150 void FormatPluginView::onConfigChanged()
0151 {
0152     m_lastChecksum = {};
0153     onActiveViewChanged(nullptr);
0154     onActiveViewChanged(m_mainWindow->activeView());
0155 }
0156 
0157 void FormatPluginView::onActiveViewChanged(KTextEditor::View *v)
0158 {
0159     if (!v || !v->document()) {
0160         if (m_activeDoc) {
0161             disconnect(m_activeDoc, &KTextEditor::Document::documentSavedOrUploaded, this, &FormatPluginView::runFormatOnSave);
0162         }
0163         m_activeDoc = nullptr;
0164         return;
0165     }
0166 
0167     if (v->document() == m_activeDoc) {
0168         return;
0169     }
0170 
0171     if (m_activeDoc) {
0172         disconnect(m_activeDoc, &KTextEditor::Document::documentSavedOrUploaded, this, &FormatPluginView::runFormatOnSave);
0173     }
0174 
0175     m_activeDoc = v->document();
0176     m_lastChecksum = {};
0177 
0178     if (formatOnSave()) {
0179         connect(m_activeDoc, &KTextEditor::Document::documentSavedOrUploaded, this, &FormatPluginView::runFormatOnSave, Qt::QueuedConnection);
0180     }
0181 }
0182 
0183 void FormatPluginView::runFormatOnSave()
0184 {
0185     m_triggeredOnSave = true;
0186     format();
0187     m_triggeredOnSave = false;
0188 }
0189 
0190 void FormatPluginView::format()
0191 {
0192     if (!m_activeDoc) {
0193         return;
0194     }
0195 
0196     // save if modified
0197     if (m_activeDoc->isModified() && !m_activeDoc->url().toLocalFile().isEmpty()) {
0198         saveDocument(m_activeDoc);
0199     }
0200 
0201     if (!m_lastChecksum.isEmpty() && m_activeDoc->checksum() == m_lastChecksum) {
0202         return;
0203     }
0204 
0205     const QVariant projectConfig = Utils::projectMapForDocument(m_activeDoc).value(QStringLiteral("formatting"));
0206     if (projectConfig != m_lastProjectConfig) {
0207         m_lastProjectConfig = projectConfig;
0208         if (projectConfig.isValid()) {
0209             const auto formattingConfig = QJsonDocument::fromVariant(projectConfig).object();
0210             if (!formattingConfig.isEmpty()) {
0211                 m_lastMergedConfig = json::merge(m_plugin->formatterConfig(), formattingConfig);
0212             }
0213         } else {
0214             // clear otherwise
0215             m_lastMergedConfig = QJsonObject();
0216         }
0217     }
0218 
0219     if (m_lastMergedConfig.isEmpty()) {
0220         m_lastMergedConfig = m_plugin->formatterConfig();
0221     }
0222 
0223     auto formatter = formatterForDoc(m_activeDoc, m_lastMergedConfig);
0224     if (!formatter) {
0225         return;
0226     }
0227 
0228     if (m_triggeredOnSave && !formatter->formatOnSaveEnabled(formatOnSave())) {
0229         delete formatter;
0230         return;
0231     }
0232 
0233     if (m_activeDoc == m_mainWindow->activeView()->document()) {
0234         formatter->setCursorPosition(m_mainWindow->activeView()->cursorPosition());
0235     }
0236 
0237     connect(formatter, &AbstractFormatter::textFormatted, this, &FormatPluginView::onFormattedTextReceived);
0238     connect(formatter, &AbstractFormatter::error, this, [formatter](const QString &error) {
0239         formatter->deleteLater();
0240         const QString msg = formatter->cmdline() + QStringLiteral("\n") + error;
0241         Utils::showMessage(msg, {}, i18n("Format"), MessageType::Error);
0242     });
0243     connect(formatter, &AbstractFormatter::textFormattedPatch, this, [this, formatter](KTextEditor::Document *doc, const std::vector<PatchLine> &patch) {
0244         formatter->deleteLater();
0245         onFormattedPatchReceived(doc, patch, true);
0246     });
0247     formatter->run(m_activeDoc);
0248 }
0249 
0250 void FormatPluginView::onFormattedTextReceived(AbstractFormatter *formatter, KTextEditor::Document *doc, const QByteArray &formattedText, int offset)
0251 {
0252     formatter->deleteLater();
0253     if (!doc) {
0254         qWarning() << Q_FUNC_INFO << "invalid null doc";
0255         return;
0256     }
0257 
0258     // No formattted => no work to do
0259     if (formattedText.isEmpty()) {
0260         return;
0261     }
0262 
0263     // if the user typed something while the formatter ran, ignore the formatted text
0264     if (formatter->originalText != doc->text()) {
0265         // qDebug() << "text changed, ignoring format" << doc->documentName();
0266         return;
0267     }
0268 
0269     auto setCursorPositionFromOffset = [this, offset, doc] {
0270         if (offset > -1 && m_mainWindow->activeView()->document() == doc) {
0271             m_mainWindow->activeView()->setCursorPosition(doc->offsetToCursor(offset));
0272         }
0273     };
0274 
0275     // non local file or untitled?
0276     if (doc->url().toLocalFile().isEmpty()) {
0277         doc->setText(QString::fromUtf8(formattedText));
0278         if (m_activeDoc == doc && !m_lastChecksum.isEmpty()) {
0279             m_lastChecksum.clear();
0280         }
0281         setCursorPositionFromOffset();
0282         return;
0283     }
0284 
0285     // get a git diff
0286     const QString patch = diff(doc, formattedText);
0287     // no difference? => do nothing
0288     if (patch.isEmpty()) {
0289         return;
0290     }
0291 
0292     // create applyable edits
0293     const std::vector<PatchLine> edits = parseDiff(doc, patch);
0294     // If the edits are too many, just do "setText" as it can be very slow
0295     if ((int)edits.size() >= (doc->lines() / 2) && doc->lines() > 1000) {
0296         for (const auto &p : edits) {
0297             delete p.pos;
0298         }
0299         doc->setText(QString::fromUtf8(formattedText));
0300         saveDocument(doc);
0301         if (m_activeDoc == doc) {
0302             m_lastChecksum = doc->checksum();
0303         }
0304         setCursorPositionFromOffset();
0305         return;
0306     }
0307 
0308     onFormattedPatchReceived(doc, edits, offset == -1);
0309     setCursorPositionFromOffset();
0310 }
0311 
0312 void FormatPluginView::onFormattedPatchReceived(KTextEditor::Document *doc, const std::vector<PatchLine> &patch, bool setCursor)
0313 {
0314     // Helper to restore cursor position for all views of doc
0315     CursorPositionRestorer restorer(setCursor ? doc : nullptr);
0316 
0317     applyPatch(doc, patch);
0318     // finally save the document
0319     saveDocument(doc);
0320     if (m_activeDoc == doc) {
0321         m_lastChecksum = doc->checksum();
0322     }
0323 
0324     if (setCursor) {
0325         restorer.restore();
0326     }
0327 }
0328 
0329 void FormatPluginView::saveDocument(KTextEditor::Document *doc)
0330 {
0331     if (doc->url().isValid() && doc->isModified()) {
0332         if (formatOnSave() && doc == m_activeDoc) {
0333             disconnect(doc, &KTextEditor::Document::documentSavedOrUploaded, this, &FormatPluginView::runFormatOnSave);
0334         }
0335 
0336         doc->documentSave();
0337 
0338         if (formatOnSave() && doc == m_activeDoc) {
0339             connect(doc, &KTextEditor::Document::documentSavedOrUploaded, this, &FormatPluginView::runFormatOnSave, Qt::QueuedConnection);
0340         }
0341     }
0342 }
0343 
0344 #include "FormatPlugin.moc"
0345 #include "moc_FormatPlugin.cpp"