Skip to main content

Monaco Editor

The monaco editor is one of the libraries which requires runtime imports. This requires the application to serve the index.html file through a custom scheme and also serve the required files.

Install

Add the monaco editor to your project by using the following command:

npx add-dependencies monaco-editor

Copy Source Files to Bin

The monaco editor requires some files to be present "somewhere on disk". In this case next to the application in "dynamic_sources". To achieve this, we have to add the following to the root CMakeLists.txt:

if (EMSCRIPTEN)
# ...
else()
# ...

# Copy bin files to destination.
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_BINARY_DIR}/module_${PROJECT_NAME}/bin" "$<TARGET_FILE_DIR:${PROJECT_NAME}>/dynamic_sources"

# The example code uses the index.html file that is bundled.
# Remove this if you want to also load your index from disk.
COMMAND ${CMAKE_COMMAND} -E rm "$<TARGET_FILE_DIR:${PROJECT_NAME}>/dynamic_sources/index.html"
COMMAND ${CMAKE_COMMAND} -E rm "$<TARGET_FILE_DIR:${PROJECT_NAME}>/dynamic_sources/index.js"
VERBATIM
)
endif()

Serve Application Through Custom Scheme

To serve the application through a custom scheme, you have to register the scheme.

backend/main.hpp:

#pragma once

#include <memory>
#include <filesystem>

class Main
{
public:
Main(std::filesystem::path const& programDirectory);
~Main();
Main(Main const&) = delete;
Main(Main&&);
Main& operator=(Main const&) = delete;
Main& operator=(Main&&)

void run();

private:
struct Implementation;
std::unique_ptr<Implementation> impl_;
};

backend/main.cpp:

#include <backend/main.hpp>

// This file is generated by nui.
#include <index.hpp>

#include <nui/core.hpp>
#include <nui/window.hpp>
#include <roar/mime_type.hpp>

#include <string>
#include <sstream>
#include <fstream>

using namespace std::string_literals;

struct Main::Implementation
{
Nui::Window window;

// This function is called when a "myScheme://" url is requested.
auto onSchemeRequest(std::filesystem::path const& programDirectory, Nui::CustomSchemeRequest const& request)
{
using namespace Nui;
const auto makeCorsHeader = [](std::string const& contentType) {
return std::unordered_multimap<std::string, std::string>{
{"Content-Type"s, contentType},
// Do not forget to allow CORS
{"Access-Control-Allow-Origin"s, "*"s},
};
};

const auto makeCorsResponse =
[&makeCorsHeader](
int statusCode, std::string const& reasonPhrase, std::string const& contentType, std::string body) {
return CustomSchemeResponse{
.statusCode = statusCode,
.reasonPhrase = reasonPhrase,
.headers = makeCorsHeader(contentType),
.body = std::move(body),
};
};

const auto url = request.parseUrl();
const auto pathString = url->pathAsString();

if (!url)
return makeCorsResponse(400, "Bad Request", "text/plain"s, "Bad Request");

// return index from memory (not necessary, can also load from disk, if you want).
if (pathString == "/index.html")
return makeCorsResponse(200, "OK", "text/html"s, index());

// Point path to dynamic_sources folder.
const auto file = programDirectory / "dynamic_sources" / std::filesystem::relative(pathString, "/");

// Check if file exists and return 404 if not
if (!std::filesystem::exists(file))
return makeCorsResponse(404, "Not Found", "text/plain"s, "Not Found");

// Read file
std::ifstream reader{file, std::ios::binary};
if (!reader)
{
return makeCorsResponse(
500, "Internal Server Error", "text/plain"s, "Internal Server Error - Could not open file.");
}
std::ostringstream payload;
payload << reader.rdbuf();
const std::string content = std::move(payload).str();

// Get mime type
const std::string mime = Roar::extensionToMime(file.extension().string()).value_or("application/octet-stream");

// Return file
return makeCorsResponse(content.empty() ? 204 : 200, "OK", mime, std::move(content));
}

auto createSchemeHandler(std::filesystem::path const& programDirectory)
{
return Nui::CustomScheme{
.scheme = "myScheme"s,
.allowedOrigins = {"*"s},
.onRequest = std::bind(&Implementation::onSchemeRequest, this, programDirectory, std::placeholders::_1),
.treatAsSecure = true,
.hasAuthorityComponent = true,
};
}

Implementation(std::filesystem::path const& programDirectory)
: window{Nui::WindowOptions{
.title = "Nui",
.customSchemes = {createSchemeHandler(programDirectory)},
}}
{
window.setSize(1650, 960, Nui::WebViewHint::WEBVIEW_HINT_NONE);
}
};

Main::Main(std::filesystem::path const& programDirectory)
: impl_{std::make_unique<Implementation>(programDirectory)}
{}
~Main::Main() = default;
Main::Main(Main&&) = default;
Main& Main::operator=(Main&&) = default;

void Main::run()
{
// app.example is intentional to circumvent DNS timeout on windows:
impl_->window.navigate("myScheme://app.example/index.html");
impl_->window.run();
}

int main(int, char** argv)
{
Main main{std::filesystem::path{argv[0]}.parent_path()};
main.run();
return 0;
}

Create an Editor Class

editor.hpp:

#pragma once

#include <nui/frontend/element_renderer.hpp>

class Editor
{
public:
Nui::ElementRenderer operator()();
};

The inline part can also be placed in any javascript file. You will have better language support from your IDE if you do. This just packs everything close together. editor.cpp:

#include <editor.hpp>

#include <nui/frontend/elements.hpp>
#include <nui/frontend/attributes.hpp>

// clang-format off
#ifdef NUI_INLINE
// @inline(js, monaco-editor)
js_import JSONWorker from 'url:monaco-editor/esm/vs/language/json/json.worker.js';
js_import CSSWorker from 'url:monaco-editor/esm/vs/language/css/css.worker.js';
js_import HTMLWorker from 'url:monaco-editor/esm/vs/language/html/html.worker.js';
js_import TSWorker from 'url:monaco-editor/esm/vs/language/typescript/ts.worker.js';
js_import EditorWorker from 'url:monaco-editor/esm/vs/editor/editor.worker.js';

js_import * as monaco from 'monaco-editor';
globalThis.monaco = monaco;

globalThis.MonacoEnvironment = {
getWorker(_workerId, label) {
if (label === 'json') {
return new JSONWorker();
}
if (label === 'css' || label === 'scss' || label === 'less') {
return new CSSWorker();
}
if (label === 'html' || label === 'handlebars' || label === 'razor') {
return new HTMLWorker();
}
if (label === 'typescript' || label === 'javascript') {
return new TSWorker();
}
return new EditorWorker();
}
};

globalThis.monacoEditors = {};
// @endinline
#endif
// clang-format on

Nui::ElementRenderer Editor::operator()()
{
using namespace Nui::Elements;
using namespace Nui::Attributes;
using Nui::Elements::div;

auto createEditor = [](Nui::val element) {
Nui::val options = Nui::val::object();
options.set("value", "{\"a\": 1}");
options.set("language", "javascript");
options.set("automaticLayout", true);
options.set("theme", "vs-dark");
Nui::val::global("monacoEditors")
.set("main-editor", Nui::val::global("monaco")["editor"].call<Nui::val>("create", element, options));
};

return div{
reference.onMaterialize([&createEditor](Nui::val element) {
createEditor(element);
}),
}();
}

Use the Editor

frontend/main_page.hpp:

#pragma once

#include <nui/frontend/element_renderer.hpp>

#include <memory>

class MainPage
{
public:
MainPage();
~MainPage();
MainPage(MainPage const&) = delete;
MainPage(MainPage&&);
MainPage& operator=(MainPage const&) = delete;
MainPage& operator=(MainPage&&);

Nui::ElementRenderer operator()();

private:
struct Implementation;
std::unique_ptr<Implementation> impl_;
};

frontend/main_page.cpp:

#include <frontend/main_page.hpp>
#include <frontend/editor.hpp>

#include <nui/frontend/elements.hpp>

struct MainPage::Implementation
{
Editor editor{};
};

MainPage::MainPage()
: impl_{std::make_unique<Implementation>()}
{}
MainPage::~MainPage() = default;
MainPage::MainPage(MainPage&&) = default;
MainPage& MainPage::operator=(MainPage&&) = default;

Nui::ElementRenderer MainPage::operator()()
{
using namespace Nui;
using namespace Nui::Elements;
using Nui::Elements::div; // because of the global div.

return body{}(impl_->editor());
}