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"