File indexing completed on 2025-01-05 05:19:43

0001 /*
0002     SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com>
0003     SPDX-License-Identifier: LGPL-2.0-or-later
0004 */
0005 #include "Formatters.h"
0006 
0007 #include "FormatApply.h"
0008 #include "ModifiedLines.h"
0009 
0010 #include <QDebug>
0011 #include <QElapsedTimer>
0012 #include <QFile>
0013 #include <QFileInfo>
0014 #include <QJsonArray>
0015 #include <QJsonDocument>
0016 #include <QJsonObject>
0017 
0018 #include <KTextEditor/Editor>
0019 
0020 static QStringList readCommandFromJson(const QJsonObject &o)
0021 {
0022     const auto arr = o.value(QStringLiteral("command")).toArray();
0023     QStringList args;
0024     args.reserve(arr.size());
0025     for (const auto &v : arr) {
0026         args.push_back(v.toString());
0027     }
0028     return args;
0029 }
0030 
0031 // Makes up a fake file name for doc mode
0032 static QString filenameFromMode(KTextEditor::Document *doc)
0033 {
0034     const QString m = doc->highlightingMode();
0035     auto is = [m](const char *s) {
0036         return m.compare(QLatin1String(s), Qt::CaseInsensitive) == 0;
0037     };
0038     auto is_or_contains = [m](const char *s) {
0039         return m.compare(QLatin1String(s), Qt::CaseInsensitive) == 0 || m.contains(QLatin1String(s));
0040     };
0041 
0042     QString path = doc->url().toLocalFile();
0043     bool needsStdinFileName;
0044     if (path.isEmpty()) {
0045         needsStdinFileName = true;
0046     }
0047     QFileInfo fi(path);
0048     const auto suffix = fi.suffix();
0049     const auto base = fi.baseName();
0050     // If the basename or suffix is missing, try to create a filename
0051     needsStdinFileName = fi.suffix().isEmpty() || fi.baseName().isEmpty();
0052 
0053     if (!needsStdinFileName) {
0054         return path;
0055     }
0056 
0057     QString prefix;
0058     if (!path.isEmpty()) {
0059         const auto fi = QFileInfo(path);
0060         prefix = fi.absolutePath();
0061         if (!prefix.isEmpty() && !prefix.endsWith(QLatin1Char('/'))) {
0062             prefix += QLatin1String("/");
0063         }
0064         const auto base = fi.baseName();
0065         if (!base.isEmpty()) {
0066             prefix += base + QStringLiteral("/");
0067         } else {
0068             prefix += QLatin1String("a");
0069         }
0070     } else {
0071         prefix = QStringLiteral("a");
0072     }
0073 
0074     if (is_or_contains("c++")) {
0075         return prefix.append(QLatin1String(".cpp"));
0076     } else if (is("c")) {
0077         return prefix.append(QLatin1String(".c"));
0078     } else if (is("json")) {
0079         return prefix.append(QLatin1String(".json"));
0080     } else if (is("objective-c")) {
0081         return prefix.append(QLatin1String(".m"));
0082     } else if (is("objective-c++")) {
0083         return prefix.append(QLatin1String(".mm"));
0084     } else if (is("protobuf")) {
0085         return prefix.append(QLatin1String(".proto"));
0086     } else if (is("javascript")) {
0087         return prefix.append(QLatin1String(".js"));
0088     } else if (is("typescript")) {
0089         return prefix.append(QLatin1String(".ts"));
0090     } else if (is("javascript react (jsx)")) {
0091         return prefix.append(QLatin1String(".jsx"));
0092     } else if (is("typescript react (tsx)")) {
0093         return prefix.append(QLatin1String(".tsx"));
0094     } else if (is("css")) {
0095         return prefix.append(QLatin1String(".css"));
0096     } else if (is("html")) {
0097         return prefix.append(QLatin1String(".html"));
0098     }
0099     return {};
0100 }
0101 
0102 void AbstractFormatter::run(KTextEditor::Document *doc)
0103 {
0104     // QElapsedTimer t;
0105     // t.start();
0106     m_config = m_globalConfig.value(name()).toObject();
0107     // Arguments supplied by the user are prepended
0108     auto command = readCommandFromJson(m_config);
0109     if (command.isEmpty()) {
0110         Q_EMIT error(i18n("%1: Unexpected empty command!", this->name()));
0111         return;
0112     }
0113     QString path = command.takeFirst();
0114     const auto args = command + this->args(doc);
0115     const auto name = safeExecutableName(!path.isEmpty() ? path : this->name());
0116     if (name.isEmpty()) {
0117         Q_EMIT error(i18n("%1 is not installed, please install it to be able to format this document!", this->name()));
0118         return;
0119     }
0120 
0121     RunOutput o;
0122 
0123     m_procHandle = new QProcess(this);
0124     QProcess *p = m_procHandle;
0125     connect(p, &QProcess::finished, this, [this, p](int exit, QProcess::ExitStatus) {
0126         RunOutput o;
0127         o.exitCode = exit;
0128         o.out = p->readAllStandardOutput();
0129         o.err = p->readAllStandardError();
0130         onResultReady(o);
0131 
0132         p->deleteLater();
0133         deleteLater();
0134     });
0135 
0136     if (!workingDir().isEmpty()) {
0137         m_procHandle->setWorkingDirectory(workingDir());
0138     } else {
0139         m_procHandle->setWorkingDirectory(QFileInfo(doc->url().toDisplayString(QUrl::PreferLocalFile)).absolutePath());
0140     }
0141     m_procHandle->setProcessEnvironment(env());
0142 
0143     startHostProcess(*p, name, args);
0144 
0145     if (supportsStdin()) {
0146         const auto stdinText = textForStdin();
0147         if (!stdinText.isEmpty()) {
0148             p->write(stdinText);
0149             p->closeWriteChannel();
0150         }
0151     }
0152 }
0153 
0154 void AbstractFormatter::onResultReady(const RunOutput &o)
0155 {
0156     if (!o.err.isEmpty()) {
0157         Q_EMIT error(QString::fromUtf8(o.err));
0158         return;
0159     } else if (!o.out.isEmpty()) {
0160         Q_EMIT textFormatted(this, m_doc, o.out);
0161     }
0162 }
0163 
0164 QStringList ClangFormat::args(KTextEditor::Document *doc) const
0165 {
0166     QString file = doc->url().toLocalFile();
0167     int off = m_doc->cursorToOffset(m_pos);
0168     QStringList args;
0169     if (off != -1) {
0170         const_cast<ClangFormat *>(this)->m_withCursor = true;
0171         args << QStringLiteral("--cursor=%1").arg(off);
0172     }
0173 
0174     args << QStringLiteral("--assume-filename=%1").arg(filenameFromMode(doc));
0175 
0176     if (file.isEmpty()) {
0177         return args;
0178     }
0179 
0180     if (!m_config.value(QStringLiteral("formatModifiedLinesOnly")).toBool()) {
0181         return args;
0182     }
0183 
0184     const auto lines = getModifiedLines(file);
0185     if (lines.has_value()) {
0186         for (auto ll : *lines) {
0187             args.push_back(QStringLiteral("--lines=%1:%2").arg(ll.startLine).arg(ll.endline));
0188         }
0189     }
0190     return args;
0191 }
0192 
0193 void ClangFormat::onResultReady(const RunOutput &o)
0194 {
0195     if (!o.err.isEmpty()) {
0196         Q_EMIT error(QString::fromUtf8(o.err));
0197         return;
0198     }
0199     if (!o.out.isEmpty()) {
0200         if (m_withCursor) {
0201             int p = o.out.indexOf('\n');
0202             if (p >= 0) {
0203                 QJsonParseError e;
0204                 auto jd = QJsonDocument::fromJson(o.out.mid(0, p), &e);
0205                 if (e.error == QJsonParseError::NoError && jd.isObject()) {
0206                     auto v = jd.object()[QLatin1String("Cursor")].toInt(-1);
0207                     Q_EMIT textFormatted(this, m_doc, o.out.mid(p + 1), v);
0208                 }
0209             }
0210         } else {
0211             Q_EMIT textFormatted(this, m_doc, o.out);
0212         }
0213     }
0214 }
0215 
0216 void DartFormat::onResultReady(const RunOutput &out)
0217 {
0218     if (out.exitCode == 0) {
0219         // No change
0220         return;
0221     } else if (out.exitCode > 1) {
0222         if (!out.err.isEmpty()) {
0223             Q_EMIT error(QString::fromUtf8(out.err));
0224         }
0225     } else if (out.exitCode == 1) {
0226         Q_EMIT textFormatted(this, m_doc, out.out);
0227     }
0228 }
0229 
0230 void RustFormat::onResultReady(const RunOutput &out)
0231 {
0232     if (!out.err.isEmpty()) {
0233         Q_EMIT error(QString::fromUtf8(out.err));
0234         return;
0235     }
0236     if (out.out.isEmpty()) {
0237         return;
0238     }
0239 
0240     textFormatted(this, m_doc, out.out);
0241 }
0242 
0243 void PrettierFormat::setupNode()
0244 {
0245     if (s_nodeProcess && s_nodeProcess->state() == QProcess::Running) {
0246         return;
0247     }
0248 
0249     const QString path = m_config.value(QLatin1String("path")).toString();
0250     const auto node = safeExecutableName(!path.isEmpty() ? path : QStringLiteral("node"));
0251     if (node.isEmpty()) {
0252         Q_EMIT error(i18n("Please install node and prettier"));
0253         return;
0254     }
0255 
0256     delete s_tempFile;
0257     s_tempFile = new QTemporaryFile(KTextEditor::Editor::instance());
0258     if (!s_tempFile->open()) {
0259         Q_EMIT error(i18n("PrettierFormat: Failed to create temporary file"));
0260         return;
0261     }
0262     QFile prettierServer(QStringLiteral(":/formatting/prettier_script.js"));
0263     bool opened = prettierServer.open(QFile::ReadOnly);
0264     Q_ASSERT(opened);
0265     s_tempFile->write(prettierServer.readAll());
0266     s_tempFile->close();
0267 
0268     // Static node process
0269     s_nodeProcess = new QProcess(KTextEditor::Editor::instance());
0270     connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::destroyed, s_nodeProcess, [] {
0271         s_nodeProcess->kill();
0272         s_nodeProcess->waitForFinished();
0273     });
0274 
0275     s_nodeProcess->setProgram(node);
0276     s_nodeProcess->setArguments({s_tempFile->fileName()});
0277 
0278     startHostProcess(*s_nodeProcess, QProcess::ReadWrite);
0279     s_nodeProcess->waitForStarted();
0280 }
0281 
0282 void PrettierFormat::onReadyReadOut()
0283 {
0284     m_runOutput.out += s_nodeProcess->readAllStandardOutput();
0285     if (m_runOutput.out.endsWith("[[{END_PRETTIER_SCRIPT}]]")) {
0286         m_runOutput.out.truncate(m_runOutput.out.size() - (sizeof("[[{END_PRETTIER_SCRIPT}]]") - 1));
0287         QJsonParseError e;
0288         QJsonDocument doc = QJsonDocument::fromJson(m_runOutput.out, &e);
0289         m_runOutput.out = {};
0290         if (e.error != QJsonParseError::NoError) {
0291             Q_EMIT error(e.errorString());
0292         } else {
0293             const auto obj = doc.object();
0294             const auto formatted = obj[QStringLiteral("formatted")].toString().toUtf8();
0295             const auto cursor = obj[QStringLiteral("cursorOffset")].toInt(-1);
0296             Q_EMIT textFormatted(this, m_doc, formatted, cursor);
0297         }
0298     }
0299 }
0300 
0301 void PrettierFormat::onReadyReadErr()
0302 {
0303     const auto err = s_nodeProcess->readAllStandardError();
0304     if (!err.isEmpty()) {
0305         Q_EMIT error(QString::fromUtf8(err));
0306     }
0307 }
0308 
0309 void PrettierFormat::run(KTextEditor::Document *doc)
0310 {
0311     setupNode();
0312     if (!s_nodeProcess) {
0313         return;
0314     }
0315 
0316     const auto path = doc->url().toLocalFile();
0317     connect(s_nodeProcess, &QProcess::readyReadStandardOutput, this, &PrettierFormat::onReadyReadOut);
0318     connect(s_nodeProcess, &QProcess::readyReadStandardError, this, &PrettierFormat::onReadyReadErr);
0319 
0320     QJsonObject o;
0321     o[QStringLiteral("filePath")] = path;
0322     o[QStringLiteral("stdinFilePath")] = filenameFromMode(doc);
0323     o[QStringLiteral("source")] = originalText;
0324     o[QStringLiteral("cursorOffset")] = doc->cursorToOffset(m_pos);
0325     s_nodeProcess->write(QJsonDocument(o).toJson(QJsonDocument::Compact) + '\0');
0326 }
0327 
0328 void GoFormat::onResultReady(const RunOutput &out)
0329 {
0330     if (out.exitCode == 0) {
0331         const auto parsed = parseDiff(m_doc, QString::fromUtf8(out.out));
0332         Q_EMIT textFormattedPatch(m_doc, parsed);
0333     } else if (!out.err.isEmpty()) {
0334         Q_EMIT error(QString::fromUtf8(out.err));
0335     }
0336 }
0337 
0338 #include "moc_Formatters.cpp"