GitRaven: How to setup Monaco Editor for QWebEngine
Hello,
This blog post will talk about integrating Monaco Editor into a Qt + C++ app using QWebEngine.
Background
GitRaven is being built to serve as a near-identical replacement to VSCode’s source control management. It aims to offer a similar, yet opinionated, user experience.
I personally like side-by-side diff when dealing with file changes. It feels natural to me and I guess, I am pampered by VSCode for a couple of years.
In my first GitRaven post, you might have seen a diff viewer in the screenshots. It was built using a custom side-by-side implementation using KTextEditor. I gave up on that approach as it was a difficult problem to solve with my current level.
My future goal (distant) is to replace Monaco based diff viewer with a C++ native side-by-side diff viewer as seen in Kompare or Kate’s diff viewers.
AI Usage
This project uses AI to help me overcome challenges faced in the project. Some code written here has been derived from code snippets shared by LLMs.
Theory
The idea is to instantiate QWebEngineView into the app which loads index.html file which contains essential code to initialize Monaco editor as well as setup communication with C++ side as needed.
Create a new class
RavenMonacowhich extends fromQWebEngineViewand set it up in main window class.Create a new class which extends QWebEnginePage to send commands to Monaco editor. This is primarily used to communicate with Monaco, to init the widget or update diff text when a different file is chosen. We also use it to log messages to
stdoutprinted on JS’sconsolefor debugging purposes.Create the “bridge” class which maintains communication between JS ←—→ C++ side. It is currently used to save text from Monaco’s modified buffer to C++.
A custom class is needed which handles rendering of Monaco editor on-demand like when a user clicks on diff item from LHS tree. This class is also responsible to save text buffer contents sent in (3) to disk.
Lastly, we need a class that can serve Monaco code via a HTTP server for enabling Web Workers.
HTTP server is needed for improving performance of Monaco editor. You can find more info in FAQ section of the project.
Implementation
Let’s start with custom web engine view class.
ravenmonaco.h
#ifndef RAVENMONACO_H
#define RAVENMONACO_H
#include "ravenmonacopage.h"
#include "ravenmonacohttpserver.h"
#include "ravenmonacobridge.h"
#include <QWebEngineView>
#include <QWidget>
#include <qevent.h>
#include <QJsonObject>
class RavenMonaco : public QWebEngineView
{
Q_OBJECT
public:
explicit RavenMonaco(QWidget *parent = nullptr);
RavenMonacoPage *page() const;
protected:
void resizeEvent(QResizeEvent *event) override
{
// Ensure that the web view resizes dynamically when the parent widget is resized
resize(event->size()); // Resize webView to match the parent widget
QWidget::resizeEvent(event);
}
void setDefaultUrl();
public slots:
// Nice to have :)
void setTheme(Qt::ColorScheme colorScheme);
private:
RavenMonacoPage *m_page;
RavenMonacoHTTPServer *m_server;
RavenMonacoBridge *m_bridge;
QWebChannel *m_channel;
};
#endif // RAVENMONACO_H
This is pretty straightforward. I store references to page, http server, bridge class and channel for future use.
ravenmonaco.cpp
#include "ravenmonaco.h"
#include <QStyleHints>
#include <QWebChannel>
#include <qmessagebox.h>
namespace fs = std::filesystem;
RavenMonaco::RavenMonaco(QWidget *parent)
: QWebEngineView{parent}
{
// Init page
m_page = new RavenMonacoPage(this);
setPage(m_page);
// Light/dark theme switcher
QStyleHints *hint = QGuiApplication::styleHints();
// Init monaco when the page load is finished.
connect(page(), &QWebEnginePage::loadFinished, this, [this](bool ok) {
if (!ok) {
qCritical() << "Failed to load Monaco editor, check Monaco HTTP server.";
QMessageBox errorMsg(QMessageBox::Critical, "GitRaven" , "Failed to load Diff Viewer components.", QMessageBox::Ok);
errorMsg.exec();
std::exit(-1);
}
// Initialize Monaco internally (doesn't show up in UI yet)
page()->runJavaScript("init()", 0, [this](const QVariant &) {
// Update theme
setTheme(QGuiApplication::styleHints()->colorScheme());
});
});
// Init HTTP server for monaco-editor
m_server = new RavenMonacoHTTPServer(this);
m_server->init();
setDefaultUrl();
// Init bridge
m_bridge = new RavenMonacoBridge(this, (RavenEditor*)parent);
m_channel = new QWebChannel(this);
m_page->setWebChannel(m_channel);
// Inform JS side of a JS object available in
// `window` that can communicate with C++ world.
m_channel->registerObject("cppBridge", m_bridge);
// Light/dark theme switcher
connect(hint, &QStyleHints::colorSchemeChanged, this, &RavenMonaco::setTheme);
}
RavenMonacoPage *RavenMonaco::page() const
{
return m_page;
}
void RavenMonaco::setDefaultUrl()
{
setUrl(QUrl("http://localhost:9191/index.html"));
load(url());
}
void RavenMonaco::setTheme(Qt::ColorScheme colorScheme)
{
QJsonObject obj;
obj["theme"] = colorScheme == Qt::ColorScheme::Light ? "light" : "dark";
QJsonDocument jd(obj);
m_page->runJavaScript(QString("setTheme({opt})").replace("{opt}", jd.toJson()));
}
This class does a couple of things:
Calls
init()in the JS side to initialize Monaco editor. I want to make the app “ready for rendering diffs” ASAP and so I came up with this logic.Then, we setup color scheme detection based on OS’s preference (light/dark) and listen to this event. I personally like this feature.
It’s a (great) gesture from app developers and I appreciate every time an app does it. Most Linux apps follow this by default (at least ones I use) and naturally, I wanted to participate :)
- We initialize bridge class as well as QWebChannel which is the tech magic that enables us to communicate with/from C++ & JS side.
ravenmonacopage.h
#ifndef RAVENMONACOPAGE_H
#define RAVENMONACOPAGE_H
#include <QWebEnginePage>
#include "gitmanager.h"
class RavenMonacoPage : public QWebEnginePage
{
public:
explicit RavenMonacoPage(QObject *parent = nullptr);
// This is needed to disable editing Monaco's modified buffer when dealing with
// staged files.
void setReadonly(bool readonly);
// Used to update old/new text content inside Monaco on each file item click.
void updateText(GitManager::GitDiffItem diffItem);
private:
void javaScriptConsoleMessage(JavaScriptConsoleMessageLevel level,
const QString &message, int lineNumber,
const QString &sourceID) override;
};
#endif // RAVENMONACOPAGE_H
ravenmonacopage.cpp
#include "ravenmonacopage.h"
#include <QJsonObject>
RavenMonacoPage::RavenMonacoPage(QObject *parent)
: QWebEnginePage(parent)
{}
void RavenMonacoPage::setReadonly(bool readonly)
{
runJavaScript(QString("setReadonly({opt})")
.replace("{opt}", QVariant(readonly).toString()));
}
void RavenMonacoPage::updateText(GitManager::GitDiffItem diffItem)
{
// Build JSON payload
// Note: Is there a better way?
QJsonObject payloadJ;
payloadJ["oldText"] = diffItem.oldFileContent;
payloadJ["oldPath"] = diffItem.oldFilePath;
payloadJ["newText"] = diffItem.newFileContent;
payloadJ["newPath"] = diffItem.newFilePath;
QJsonDocument payloadJD(payloadJ);
QString payloadJDStr = QString(payloadJD.toJson());
// Send request
runJavaScript(QString("update({opt})").replace("{opt}", payloadJDStr));
}
void RavenMonacoPage::javaScriptConsoleMessage(
JavaScriptConsoleMessageLevel level,
const QString &message,
int lineNumber, const
QString &sourceID
)
{
qDebug() << "RavenMonacoPage::javaScriptConsoleMessage";
qDebug() << level << message << lineNumber << sourceID;
}
ravenmonacobridge.h
#ifndef RAVENMONACOBRIDGE_H
#define RAVENMONACOBRIDGE_H
#include <QObject>
#include <QDebug>
// Forward declarations
class RavenEditor;
class RavenMonacoBridge : public QObject
{
Q_OBJECT
public:
explicit RavenMonacoBridge(QObject *parent = nullptr, RavenEditor *editor = nullptr);
Q_INVOKABLE void saveModifiedChanges(QString modified);
private:
RavenEditor *m_ravenEditor;
};
#endif // RAVENMONACOBRIDGE_H
ravenmonacobridge.cpp
#include "ravenmonacobridge.h"
#include "raveneditor.h"
RavenMonacoBridge::RavenMonacoBridge(QObject *parent, RavenEditor *editor)
: QObject{parent}
{
m_ravenEditor = editor;
}
/**
* @brief This function is called by Monaco when user has modified file contents.
* @param modifiedText - The modified text contents from Monaco side.
*/
void RavenMonacoBridge::saveModifiedChanges(QString modifiedText)
{
qDebug() << "RavenMonacoBridge::saveModifiedChanges called";
emit m_ravenEditor->signalSaveModifiedChanges(modifiedText);
}
This class exists to trigger saveModifiedChanges from JS side and forward it to RavenEditor class which actually holds the business logic for processing the request.
Also, here’s the code that allows us to render either a placeholder or Monaco editor as required.
ravenrhsview.h
#ifndef RAVENRHSVIEW_H
#define RAVENRHSVIEW_H
#include "gitmanager.h"
#include "mainwindow.h"
#include <QVBoxLayout>
#include <QWidget>
class RavenRHSView : public QWidget
{
Q_OBJECT
public:
explicit RavenRHSView(QWidget *parent);
~RavenRHSView() override;
void initLandingInfo();
public slots:
void updateUI(std::optional<GitManager::GitDiffItem> item);
private:
bool m_showLandingInfo = true;
MainWindow *m_mainWindow;
RavenTree *m_ravenTree;
RavenEditor *m_ravenEditor;
QWidget *m_landingInfoWidget;
};
#endif // RAVENRHSVIEW_H
ravenrhsview.cpp
#include "ravenrhsview.h"
#include "raveneditor.h"
#include <QLabel>
RavenRHSView::RavenRHSView(QWidget *parent)
: QWidget{parent},
m_mainWindow(static_cast<MainWindow*>(topLevelWidget()->window())),
m_ravenEditor(new RavenEditor(this)), // Editor widget
m_landingInfoWidget(new QWidget(this)) // placeholder widget
{
// Widget config
setLayout(new QVBoxLayout(this));
layout()->addWidget(m_landingInfoWidget);
updateUI(std::nullopt);
m_ravenTree = m_mainWindow->getRavenLHSView()->getRavenTree();
connect(m_ravenTree, &RavenTree::renderDiffItem, this, &RavenRHSView::updateUI);
}
RavenRHSView::~RavenRHSView()
{
// cleanup
disconnect(m_ravenTree, &RavenTree::renderDiffItem, this, &RavenRHSView::updateUI);
}
void RavenRHSView::updateUI(std::optional<GitManager::GitDiffItem> item)
{
qDebug() << "RavenRHSView::updateUI called";
m_showLandingInfo = !item.has_value();
// Determine whether we show Diff widget or the placeholder
if (!m_showLandingInfo) {
m_landingInfoWidget->hide();
layout()->addWidget(m_ravenEditor);
m_ravenEditor->openDiffItem(std::move(item.value()));
} else {
initLandingInfo();
}
}
void RavenRHSView::initLandingInfo()
{
auto widget = m_landingInfoWidget;
auto layout = new QGridLayout(widget);
layout->setAlignment(Qt::AlignCenter);
widget->setLayout(layout);
widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
auto *label = new QLabel(widget);
label->setText("GitRaven");
auto icon = QIcon::fromTheme("git");
auto iconLabel = new QLabel(widget);
iconLabel->setPixmap(icon.pixmap(64, 64));
layout->addWidget(iconLabel);
layout->addWidget(label);
}
ravenmonacohttpserver.h
#ifndef RAVENMONACOHTTPSERVER_H
#define RAVENMONACOHTTPSERVER_H
#include <QObject>
#include <QHttpServer>
#include <QTcpServer>
class RavenMonacoHTTPServer : QObject
{
Q_OBJECT
public:
RavenMonacoHTTPServer(QObject *parent);
~RavenMonacoHTTPServer();
int init();
private:
QUrl *m_url;
int PORT = 9191;
QHttpServer *m_server = new QHttpServer(this);
QTcpServer *m_tcpserver = new QTcpServer(this);
};
#endif // RAVENMONACOHTTPSERVER_H
ravenmonacohttpserver.cpp
#include "ravenmonacohttpserver.h"
#include <filesystem>
#include "ravenutils.h"
using std::filesystem::absolute;
using std::filesystem::path;
RavenMonacoHTTPServer::RavenMonacoHTTPServer(QObject *parent)
: QObject(parent) {}
RavenMonacoHTTPServer::~RavenMonacoHTTPServer()
{
m_tcpserver->close();
}
int RavenMonacoHTTPServer::init()
{
qDebug() << "RavenMonacoHTTPServer::init() called";
if (!m_tcpserver->listen(QHostAddress::LocalHost, PORT) || !m_server->bind(m_tcpserver)) {
qDebug() << "RavenMonacoHTTPServer::init() Failed to bind port for HTTP server.";
return -1;
}
// Listen to / path and return the requested file in URL.
// Note: Can this be a security risk? For ex: "/index.html/../../../../etc/passwd"
m_server->route("/<arg>", [] (const QUrl &url) {
QString urlPath = url.path();
if (urlPath.length() < 5) {
return QHttpServerResponse("");
}
// Locate editor directory
path editorDirStdPath = path(RavenUtils::getEditorDirPath());
path absolutePath = absolute(editorDirStdPath / path(urlPath.toStdString()));
return QHttpServerResponse::fromFile(QString::fromStdString(absolutePath));
});
return 0;
}
Results
Default state (placeholder)

Render a diff with Monaco (respecting OS color scheme)

Conclusion
Thanks for your time!
I reluctantly introduced this feature into the project because I couldn’t find a viable alternative to it and building something similar is way too advanced for me at the moment.
Anyway, I hope you liked this post. Please give it a Like to show your appreciation. If you feel there’s something I missed or can do better, let me know in the comments.
Bye for now :-)