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"