Add clang-format support
MODIFY
src/ccmanager.cpp
src/ccmanager.cpp
#include "loaddialog.h"
#include "processorhandler.h"
#include "ripessettings.h"
+#include "utilities/systemutils.h"
#include <QProcess>
#include <QProgressDialog>
@@ -60,18 +61,9 @@ bool CCManager::hasValidCC() {
}
QString CCManager::tryAutodetectCC() {
- for (const auto& path : s_validAutodetectedCCs) {
- QProcess process;
-
- process.start(path, QStringList());
- process.waitForFinished();
-
- if (process.error() != QProcess::FailedToStart) {
- // We have detected that a valid compiler exists in path
- return path;
- }
- }
-
+ auto ccIt = llvm::find_if(s_validAutodetectedCCs, [&](const QString& path) { return isExecutable(path); });
+ if (ccIt != s_validAutodetectedCCs.end())
+ return *ccIt;
return QString();
}

MODIFY
src/editor/codeeditor.cpp
src/editor/codeeditor.cpp
#include "colors.h"
#include "csyntaxhighlighter.h"
#include "fonts.h"
+#include "formattermanager.h"
#include "processorhandler.h"
#include "ripessettings.h"
#include "rvsyntaxhighlighter.h"
+#include "statusmanager.h"
#include "syntaxhighlighter.h"
namespace Ripes {
@@ -48,6 +50,86 @@ CodeEditor::CodeEditor(QWidget* parent) : QPlainTextEdit(parent) {
setupChangedTimer();
}
+struct ClangFormatResult {
+ // remapped cursor position. see clang-format --cursor for more info.
+ unsigned cursor;
+ QString formattedText;
+};
+
+static std::optional<ClangFormatResult> parseClangFormatOutput(const QString& cfOutput) {
+ // The first line should contain a JSON formatted array detailing the remapped cursor position and formatting
+ // status. If this is not the case, the clang-format version might be outdated or something else went wrong.
+ unsigned firstNewline = cfOutput.indexOf('\n');
+ auto firstLine = cfOutput.left(firstNewline);
+ auto rest = cfOutput.mid(firstNewline + 1);
+ auto jsonHeader = QJsonDocument::fromJson(firstLine.toUtf8());
+ if (jsonHeader.isNull())
+ return std::nullopt;
+
+ if (!jsonHeader.object().contains("Cursor"))
+ return std::nullopt;
+
+ ClangFormatResult res;
+ res.cursor = jsonHeader["Cursor"].toInt();
+ res.formattedText = rest;
+ return res;
+}
+
+void CodeEditor::onSave() {
+ // run the formatter, if applicable.
+
+ if (m_sourceType != SourceType::C)
+ return;
+
+ if (!RipesSettings::value(RIPES_SETTING_FORMAT_ON_SAVE).toBool())
+ return;
+
+ if (!FormatterManager::hasValidProgram())
+ return;
+
+ // We want to execute the formatter in the savefile's directory to adhere to any formatting config files.
+ auto savePath = RipesSettings::value(RIPES_SETTING_SAVEPATH).toString();
+ if (savePath.isEmpty())
+ return;
+
+ auto tmpDir = QFileInfo(savePath).dir();
+ const auto tempFileTemplate =
+ QString(tmpDir.path() + QDir::separator() + QCoreApplication::applicationName() + ".XXXXXX.c");
+ QTemporaryFile tmpSrc(tempFileTemplate);
+ tmpSrc.setAutoRemove(true);
+ if (tmpSrc.open()) {
+ tmpSrc.write(this->document()->toPlainText().toUtf8());
+ tmpSrc.close();
+ // Feed the current program to the formatter as stdin
+ QStringList clangFormatArgs;
+ // Track cursor changes
+ clangFormatArgs << "--cursor=" + QString::number(this->textCursor().position());
+ // Temporary file to apply format changes to
+ clangFormatArgs << tmpSrc.fileName();
+ auto [stdOut, stdErr] = FormatterManager::run(clangFormatArgs);
+ if (!stdErr.isEmpty()) {
+ QMessageBox::warning(this, "Ripes",
+ "Error while executing formatter. Make sure the formatter path points to a "
+ "valid version of clang-format\n"
+ "Error message was:\n" +
+ stdErr,
+ QMessageBox::Ok);
+ return;
+ }
+
+ if (!stdOut.isEmpty()) {
+ auto cfOutput = parseClangFormatOutput(stdOut);
+ if (!cfOutput)
+ return;
+ QTextCursor curs(this->document());
+ curs.select(QTextCursor::Document);
+ curs.insertText(cfOutput->formattedText);
+ curs.setPosition(cfOutput->cursor);
+ this->setTextCursor(curs);
+ }
+ }
+}
+
void CodeEditor::setupChangedTimer() {
m_changeTimer = new QTimer(this);
// configures the change-timer and assembler connectivity with Parser

MODIFY
src/editor/codeeditor.h
src/editor/codeeditor.h
@@ -30,6 +30,7 @@ public:
int lineNumberAreaWidth();
void setupChangedTimer();
void rehighlight();
+ void onSave();
void setErrors(const std::shared_ptr<Assembler::Errors>& errors) { m_errors = errors; }

MODIFY
src/edittab.cpp
src/edittab.cpp
@@ -131,6 +131,10 @@ void EditTab::showSymbolNavigator() {
}
}
+void EditTab::onSave() {
+ m_ui->codeEditor->onSave();
+}
+
bool EditTab::loadExternalFile(const LoadFileParams& params) {
if (params.type == SourceType::C) {
// Try to enable C input and verify that source type was changed successfully. This allows us to trigger the

MODIFY
src/edittab.h
src/edittab.h
@@ -48,6 +48,7 @@ signals:
void editorStateChanged(bool enabled);
public slots:
+ void onSave();
void onProcessorChanged();
void updateProgramViewerHighlighting();

ADD
src/formattermanager.h
+#pragma once
+
+#include <QCoreApplication>
+
+#include "processmanager.h"
+#include "ripessettings.h"
+
+namespace Ripes {
+
+class FormatterManager : public ProcessManager<FormatterManager> {
+ friend class ProcessManager<FormatterManager>;
+
+private:
+ bool verifyProgram(const QString& path) { return true; }
+ QString getSettingsPath() { return RIPES_SETTING_FORMATTER_PATH; }
+ QStringList getAlwaysArguments() {
+ return RipesSettings::value(RIPES_SETTING_FORMATTER_ARGS).toString().split(" ");
+ }
+ QStringList getDefaultPaths() { return {"clang-format"}; }
+ QString getWorkingDirectory() { return QCoreApplication::applicationDirPath(); }
+};
+
+} // namespace Ripes

MODIFY
src/mainwindow.cpp
src/mainwindow.cpp
@@ -118,6 +118,8 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), m_ui(new Ui::Main
connect(cacheTab, &CacheTab::focusAddressChanged, memoryTab, &MemoryTab::setCentralAddress);
+ connect(this, &MainWindow::prepareSave, editTab, &EditTab::onSave);
+
m_currentTabID = ProcessorTabID;
m_ui->tabbar->setActiveIndex(m_currentTabID);
}
@@ -349,6 +351,8 @@ void MainWindow::saveFilesTriggered() {
return;
}
+ emit prepareSave();
+
bool didSave = false;
QStringList savedFiles;

MODIFY
src/mainwindow.h
src/mainwindow.h
@@ -40,6 +40,9 @@ public:
void closeEvent(QCloseEvent* event) override;
void fitToView();
+signals:
+ void prepareSave();
+
private slots:
void wiki();
void version();

ADD
src/processmanager.h
+#pragma once
+
+#include <QObject>
+#include <QProcess>
+#include <QProgressDialog>
+#include <QString>
+
+#include "ripessettings.h"
+#include "utilities/systemutils.h"
+
+namespace Ripes {
+
+// A process manager is responsible for managing the autodetection and execution of processes. A program manager is a
+// singleton for each specialization of the template. This is done through static polymorphism.
+template <typename Impl>
+class ProcessManager {
+public:
+ struct ProcessResult {
+ QString stdOut;
+ QString stdErr;
+ };
+ ProcessManager() { initialize(); }
+ virtual ~ProcessManager() = default;
+
+ static bool hasValidProgram() { return !get().m_programPath.isEmpty(); }
+ static const QString& program() { return get().m_programPath; }
+ static QString getError() { return get().m_error; }
+ static ProcessResult run(const QStringList& args, bool showProgressDialog = false) {
+ return get()._run(args, showProgressDialog);
+ }
+
+ static Impl& get() {
+ static Impl instance;
+ return instance;
+ }
+
+signals:
+
+ bool trySetProgram(const QString& path) {
+ if (static_cast<Impl*>(this)->verifyProgram(path)) {
+ m_programPath = path;
+ } else {
+ m_programPath = QString();
+ }
+ return !m_programPath.isEmpty();
+ }
+
+protected:
+ QString m_programPath;
+ QProcess m_process;
+ bool m_errored = false;
+ bool m_aborted = false;
+
+private:
+ // Tries to set the program from a set of optional default program names. These program names are expected to be
+ // found in the PATH.
+ bool tryAutodetectProgram() {
+ for (auto defaultPath : static_cast<Impl*>(this)->getDefaultPaths()) {
+ if (trySetProgram(defaultPath))
+ return true;
+ }
+ return false;
+ }
+
+ // Initializes the program.
+ bool initialize() {
+ auto settingsPath = static_cast<Impl*>(this)->getSettingsPath();
+ if (!settingsPath.isEmpty() && RipesSettings::hasSetting(settingsPath)) {
+ // This program has a settings path associated with it. Link up with the settings path, ensuring that we
+ // update and revalidate the program on each settings path change.
+ auto* observer = RipesSettings::getObserver(settingsPath);
+ observer->connect(observer, &SettingObserver::modified,
+ [&](const QVariant& value) { trySetProgram(value.toString()); });
+
+ auto settingsPathValue = RipesSettings::value<QString>(settingsPath);
+ if (trySetProgram(settingsPathValue))
+ return true;
+ }
+
+ // Couldn't initialize from settings, try autodetecting.
+ return tryAutodetectProgram();
+ }
+
+ // Run the program with a set of arguments.
+ ProcessResult _run(const QStringList& args, bool showProgressDialog) {
+ /**
+ * 1. m_process will itself spawn its own thread to execute the compiler.
+ * 2. QProcess should not be started in a separate QThread, so it is started in the gui thread
+ * 3. We want to execute a progress dialog which may abort the QProcess.
+ * 4. To facilitate all of this, we have to spin on the process state in a separate thread, to
+ * not block the execution of the progress dialog.
+ */
+ m_aborted = false;
+ m_errored = false;
+ m_process.close();
+ QProgressDialog progressDiag = QProgressDialog("Executing program...", "Abort", 0, 0, nullptr);
+ m_process.connect(&progressDiag, &QProgressDialog::canceled, &m_process, &QProcess::kill);
+ m_process.connect(&progressDiag, &QProgressDialog::canceled, &m_process, [this] { m_aborted = true; });
+ m_process.connect(&m_process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), &progressDiag,
+ &QProgressDialog::reset);
+ m_process.connect(&m_process, &QProcess::errorOccurred, &progressDiag, &QProgressDialog::reset);
+ m_process.connect(&m_process, &QProcess::errorOccurred, [this]() { m_errored = true; });
+ m_process.setWorkingDirectory(static_cast<Impl*>(this)->getWorkingDirectory());
+ m_process.setProgram(m_programPath);
+ QStringList allArgs = static_cast<Impl*>(this)->getAlwaysArguments();
+ allArgs.append(args);
+ m_process.setArguments(allArgs);
+
+ m_process.start();
+ /** @todo: It is seen that if the process fails upon startup, errorOccurred executes QProgressDialog::reset.
+ * However, this call does not prevent the exec() loop from running. Below we check for this case, however this
+ * does not remove the race condition. Such race condition seems inherintly tied to how QDialog::exec works and
+ * no proper fix has been able to be found (yet).*/
+ if (!m_errored && showProgressDialog) {
+ progressDiag.exec();
+ }
+ m_process.waitForFinished();
+ return ProcessResult{QString(m_process.readAllStandardOutput()), QString(m_process.readAllStandardError())};
+ }
+};
+
+} // namespace Ripes

MODIFY
src/ripessettings.cpp
src/ripessettings.cpp
@@ -12,6 +12,9 @@ const std::map<QString, QVariant> s_defaultSettings = {
// User-modifyable settings
{RIPES_SETTING_REWINDSTACKSIZE, 100},
{RIPES_SETTING_CCPATH, ""},
+ {RIPES_SETTING_FORMATTER_PATH, "clang-format"},
+ {RIPES_SETTING_FORMAT_ON_SAVE, false},
+ {RIPES_SETTING_FORMATTER_ARGS, "--style=file --fallback-style=LLVM"},
{RIPES_SETTING_CCARGS, "-O0"},
{RIPES_SETTING_LDARGS, "-static -lm"}, // Ensure statically linked executable + link with math library
{RIPES_SETTING_CONSOLEECHO, "true"},

MODIFY
src/ripessettings.h
src/ripessettings.h
@@ -11,6 +11,9 @@ namespace Ripes {
#define RIPES_SETTING_REWINDSTACKSIZE ("simulator_rewindstacksize")
#define RIPES_SETTING_CCPATH ("compiler_path")
#define RIPES_SETTING_CCARGS ("compiler_args")
+#define RIPES_SETTING_FORMATTER_PATH ("formatter_path")
+#define RIPES_SETTING_FORMAT_ON_SAVE ("format_on_save")
+#define RIPES_SETTING_FORMATTER_ARGS ("formatter_args")
#define RIPES_SETTING_LDARGS ("linker_args")
#define RIPES_SETTING_CONSOLEECHO ("console_echo")
#define RIPES_SETTING_CONSOLEBG ("console_bg_color")

MODIFY
src/settingsdialog.cpp
src/settingsdialog.cpp
#include "ui_settingsdialog.h"
#include "ccmanager.h"
+#include "formattermanager.h"
#include "ripessettings.h"
#include <QCheckBox>
@@ -301,6 +302,39 @@ QWidget* SettingsDialog::createEditorPage() {
appendToLayout({consoleLabel, consoleCheckbox}, pageLayout,
"Show (or hide) a view of the console in the editor tab.");
+ // ===== Source formatter
+ auto* formatterGroupBox = new QGroupBox("Formatter");
+ appendToLayout(formatterGroupBox, pageLayout);
+ auto* formatterLayout = new QGridLayout();
+ auto* formatterDesc = new QLabel("Format .c files using clang-format, if available.");
+ formatterDesc->setWordWrap(true);
+ appendToLayout(formatterDesc, formatterLayout);
+ formatterGroupBox->setLayout(formatterLayout);
+
+ // Formatter path
+ auto* FormatterPathLayout = new QHBoxLayout();
+ auto [formatterlabel, formatterpath] =
+ createSettingsWidgets<QLineEdit>(RIPES_SETTING_FORMATTER_PATH, "clang-format path:");
+ appendToLayout(FormatterPathLayout, formatterLayout);
+ FormatterPathLayout->addWidget(formatterlabel);
+ FormatterPathLayout->addWidget(formatterpath);
+ auto* pathBrowseButton = new QPushButton("Browse");
+ FormatterPathLayout->addWidget(pathBrowseButton);
+ connect(pathBrowseButton, &QPushButton::clicked, formatterpath, [=, formatterpath = formatterpath] {
+ QFileDialog dialog(this);
+ dialog.setAcceptMode(QFileDialog::AcceptOpen);
+ if (dialog.exec()) {
+ formatterpath->setText(dialog.selectedFiles().at(0));
+ }
+ });
+
+ // Format on save
+ appendToLayout(createSettingsWidgets<QCheckBox>(RIPES_SETTING_FORMAT_ON_SAVE, "Format on save:"), formatterLayout);
+
+ // Formatter arguments
+ appendToLayout(createSettingsWidgets<QLineEdit>(RIPES_SETTING_FORMATTER_ARGS, "Formatter arguments:"),
+ formatterLayout);
+
return pageWidget;
}
@@ -359,8 +393,12 @@ QWidget* SettingsDialog::createEnvironmentPage() {
return pageWidget;
}
-void SettingsDialog::appendToLayout(QGroupBox* groupBox, QGridLayout* pageLayout, int colSpan) {
- pageLayout->addWidget(groupBox, pageLayout->rowCount(), 0, 1, colSpan);
+void SettingsDialog::appendToLayout(QLayout* layout, QGridLayout* pageLayout, int colSpan) {
+ pageLayout->addLayout(layout, pageLayout->rowCount(), 0, 1, colSpan);
+}
+
+void SettingsDialog::appendToLayout(QWidget* widget, QGridLayout* pageLayout, int colSpan) {
+ pageLayout->addWidget(widget, pageLayout->rowCount(), 0, 1, colSpan);
}
void SettingsDialog::appendToLayout(std::pair<QLabel*, QWidget*> settingsWidgets, QGridLayout* pageLayout,

MODIFY
src/settingsdialog.h
src/settingsdialog.h
@@ -45,7 +45,8 @@ private:
void addPage(const QString& name, QWidget* page);
void appendToLayout(std::pair<QLabel*, QWidget*> settingsWidgets, QGridLayout* pageLayout,
const QString& description = QString());
- void appendToLayout(QGroupBox* groupBox, QGridLayout* pageLayout, int colSpan = 2);
+ void appendToLayout(QWidget* widget, QGridLayout* pageLayout, int colSpan = 2);
+ void appendToLayout(QLayout* layout, QGridLayout* pageLayout, int colSpan = 2);
std::map<QString, int> m_pageIndex;
};

ADD
src/utilities/systemutils.cpp
+#include "systemutils.h"
+
+#include <QProcess>
+namespace Ripes {
+bool isExecutable(const QString& path, const QStringList& dummyArgs) {
+ QProcess process;
+ process.start(path, dummyArgs);
+ process.waitForFinished();
+ return (process.error() != QProcess::FailedToStart);
+}
+
+} // namespace Ripes

ADD
src/utilities/systemutils.h
+#pragma once
+
+#include <QStringList>
+
+namespace Ripes {
+
+bool isExecutable(const QString& path, const QStringList& dummyArgs = {});
+
+} // namespace Ripes

Add clang-format support #181 (#182)