background

I’ve opened source a small project code-run, a tool similar to codepen, in which the code Editor uses Microsoft’s Monaco Editor, which is generated directly from VSCode’s source code with a few modifications to make it run in the browser. However, the Monaco Editor is basically as powerful as VSCode, so in my opinion, the Monaco Editor is the core of VSCode.

In addition, the author is a beauty lover, and no matter what project he does, he is keen on matching some beautiful skin and themes. Therefore, the Moncao Editor only has three built-in themes, which are far from meeting the author’s needs. Besides, all of them are ugly. Can you directly reuse VSCode theme, next I will introduce you to explore the path of the author.

Ps. If you want to know how to implement it directly, you can go to the “Concrete Implementation” section.

The basic use

Let’s take a look at the Monaco Editor basics, starting with the following installation:

npm install monaco-editor
Copy the code

Then introduce:

import * as monaco from 'monaco-editor'

// Create a js editor
const editor = monaco.editor.create(document.getElementById('container'), {
    value: ['function x() {'.'\tconsole.log("Hello world!" ); '.'} '].join('\n'),
    language: 'javascript'.theme: 'vs'
})
Copy the code

This allows you to create a JS editor on top of the Container element, using the built-in VS-Dark theme. If you encounter an error or syntax message that does not take effect, you may need to configure the path to the worker file. See the official example browser-ESm-webpack.

Custom themes

The Monaco Editor supports custom themes as follows:

// Define the topic
monaco.editor.defineTheme(themeName, themeData)
// Use the theme defined
monaco.editor.setTheme(themeName)
Copy the code

ThemeName is a custom themeName, such as OneDarkPro. ThemeData is an object, such as themeData.

{
    base: 'vs'.// The base theme to inherit, namely the built-in three: VS, VS-Dark, and HC-Black
    inherit: false.// Whether to inherit
    rules: [// Highlight the rule that sets different display styles for different token types in the code
        { token: ' '.foreground: '000000'.background: 'fffffe'}].colors: {// Color of other parts of the code that are not part of the code, such as background, scrollbar, etc
        [editorBackground]: '#FFFFFE'}}Copy the code

Rules is used to highlight code. Common tokens include string, comment, keyword, etc. For a complete list, please refer to themes.ts. The Monaco Editor has a built-in syntax shader, Monarch, which essentially matches with regular expressions and then names the matched content as a token.

To view the token corresponding to a block of code directly in the editor, press F1 or right click on the Command Palette, and then go to Developer: Inspect Tokens. Click on each piece of code and it will display the corresponding information, including the token type and the current color of the application.

Hit the pit

The initial idea was simple, just go to VSCode’s theme file and use it with a custom theme.

To obtainVSCodeTheme files

There are two ways. If a theme is already installed and in use in your VSCode, you can press F1 or Command/Control + Shift + P or right click on the Command Palette/ Command Palette. Then go to Developer:Generate Color Theme From Current Setting/ Developer: Use the Current Settings to Generate the Color Theme. VSCode will then Generate a JSON data and save it.

If a theme is not installed, you can search for it in the theme store of vscode, enter the theme details page and click the Download Extension button on the right to Download the theme. After downloading, you can find the downloaded file, which should end with.vsix and change the suffix to.zip. Then unzip the file and open the /extension/themes/ folder. Json file is the theme file. Open the file and copy the JSON data.

theVSCodeTopic conversion toMonaco EditorTheme format

After the previous step you should notice that the format of the VSCode theme looks like this:

{
	"$schema": "vscode://schemas/color-theme"."type": "dark"."colors": {
		"activityBar.background": "#282c34"
    },
    "tokenColors": [{"scope": "variable.other.generic-type.haskell"."settings": {
				"foreground": "#C678DD"}}, {"scope": [
				"punctuation.section.embedded.begin.php"."punctuation.section.embedded.end.php"]."settings": {
				"foreground": "#BE5046"}}}]Copy the code

There is a difference between the Monaco Editor’s theme format and the Monaco Editor’s theme format. Is it possible to write a conversion method to convert it to something like this:

{
    base: 'vs'.inherit: false.rules: [{token: 'variable.other.generic-type.haskell'.foreground: '#C678DD' },
        { token: 'punctuation.section.embedded.begin.php'.foreground: '#BE5046' },
        { token: 'punctuation.section.embedded.end.php'.foreground: '#BE5046'}].colors: {
        "activityBar.background": "#282c34"}}Copy the code

Can, of course, this also is not difficult, but in the end when you will find that after using this custom theme doesn’t work, why, take a look at the Monarch after the corresponding language parsing configuration will find, there just isn’t VSCode defined in the theme of the token, have the effect is strange, that what can we do, our extended this parsing configuration? This is what I did in the beginning, and it should not be too difficult to write regular expressions. For this reason, I also translated the Monarch document into Chinese, but when I saw the following effect in VSCode:

Give up decisively, this obviously requires semantic analysis, otherwise who knows ABC is a variable.

In fact, VSCode uses TextMate for syntax highlighting, while Monaco Editor uses Monarch. The two are not the same thing. Why don’t the Monaco Editor use TextMate instead of developing a new thing? The reason is that VSCode uses vscode-textmate to parse the textmate syntax. This library relies on the Oniguruma regular expression library, which is developed in C language and does not support running on browsers.

The next best thing

Since VSCode themes cannot be used directly, we can only use as many as we can, because the Monaco Editor only has so many built-in theme tokens. Why not change all of the token colors to VSCode theme colors? Although the semantic highlighting is missing, it still looks better than the default theme colors. The implementation is also very simple. First, the basic colors part can be used directly, and the token part can be used through the method Developer described above: Inspect Tokens finds the color of the code block in VSCode and copies it to the Monaco Editor theme’s corresponding token. For example, the actual effect of OneDarkPro converted by the author is as follows:

In VSCode it looks like this:

Look at it at first glance, not in detail.

Someone else has already done this. You can refer to the Monaco – Themes of the warehouse, which help you to transform some common themes for direct use.

A new dawn

After giving up the idea of using the VSCode theme directly in the Monaco Editor, I noticed that the Editor theme effect of codesandbox and leetcode is basically the same as that of VSCode, and you can clearly see the files that switch the theme request in leetcode:

The format is basically the same as VSCode theme, which means that it is possible to implement the VSCode theme in the Monaco Editor, so the question becomes how.

implementation

Have to say, this aspect of information is really very little, the relevant article is basically no, baidu search results only one or two related links, but also enough to solve the problem, related links see the tail of the article.

The main tool to use is Monaco-editor-TextMate (github is also an important search engine besides Baidu and Google).

npm i monaco-editor-textmate
Copy the code

NPM will help you install monaco-TextMate, Onigasm and Monaco-Editor at the same time. Needless to say, we have installed the Monaco-Editor by ourselves. We can check the other two by ourselves, if not, we need to install them by ourselves.

Tool is introduced

A brief introduction to these bags.

onigasm

This library is designed to solve the problem that Oniguruma is not supported by browsers. The solution is to compile Oniguruma into WebAssembly, which is an intermediate format that can compile non-JS code into.wASM files. The browser can then load and run it, WebAssembly is already a standard on the WEB, and over time, compatibility is not an issue.

monaco-textmate

This library is based on the vscode-TextMate library used by VSCode to make it available in browsers. The main function is to parse the TextMate syntax, which depends on onigasm.

monaco-editor-textmate

The main function of this library is to help us associate monaco-Editor and Monaco-Textmate. Inside, we will first load the textmate syntax file of the corresponding language. It then calls the Monaco. Languages. SetTokensProvider method token from definition language parser.

Take a look at an example of its use:

import { loadWASM } from 'onigasm'
import { Registry } from 'monaco-textmate'
import { wireTmGrammars } from 'monaco-editor-textmate'
export async function liftOff() {
    await loadWASM(`path/to/onigasm.wasm`)
    const registry = new Registry({
        getGrammarDefinition: async (scopeName) => {
            return {
                format: 'json'.content: await (await fetch(`static/grammars/css.tmGrammar.json`)).text()
            }
        }
    })
    const grammars = new Map()
    grammars.set('css'.'source.css')
    grammars.set('html'.'text.html.basic')
    grammars.set('typescript'.'source.ts')
    monaco.editor.defineTheme('vs-code-theme-converted'{});var editor = monaco.editor.create(document.getElementById('container'), {
        value: [
            'html, body {'.' margin: 0; '.'} '
        ].join('\n'),
        language: 'css'.theme: 'vs-code-theme-converted'
    })
    await wireTmGrammars(monaco, registry, grammars, editor)
}
Copy the code

The specific implementation

After looking at the previous usage examples, let’s take a closer look at how to use them.

Loading onigasm

The first thing we need to do is load the wASM file for onigASM. This file needs to be loaded first and only needs to be loaded once, so we need to load it before the editor is initialized:

import { loadWASM } from 'onigasm'
const init = async() = > {await loadWASM(`${base}/onigasm/onigasm.wasm`)
    // Create an editor...
}
init()
Copy the code

The onigasm.wasm file can be found in the /node_modules/onigasm/lib/ directory and copied to the project’s /public/onigasm/ directory so that requests can be made over HTTP.

Create a scope map

Next create a mapping from language IDS to scope names:

const grammars = new Map()
grammars.set('css'.'source.css')
Copy the code

The scope names of other languages can be found here in the syntax lists of various languages. For example, if we want to know the scope names of CSS, we go to the CSS directory and open the package.json file, and we see a grammars field in it:

"grammars": [{"language": "css"."scopeName": "source.css"."path": "./syntaxes/css.tmLanguage.json"."tokenTypes": {
            "meta.function.url string.quoted": "other"}}]Copy the code

Language is the language id and scopeName is the scopeName. Common ones are as follows:

const scopeNameMap = {
    html: 'text.html.basic'.pug: 'text.pug'.css: 'source.css'.less: 'source.css.less'.scss: 'source.css.scss'.typescript: 'source.ts'.javascript: 'source.js'.javascriptreact: 'source.js.jsx'.coffeescript: 'source.coffee'
}
Copy the code

Register syntax mappings

Then register the syntax mapping of TextMate, so that it can load and create the corresponding syntax by scope name:

import {
    Registry
} from 'monaco-textmate'

// Create a registry that loads syntax files from scope names
const registry = new Registry({
    getGrammarDefinition: async (scopeName) => {
        return {
            format: 'json'.// Syntax file format, json, plist
            content: await (await fetch(`${base}grammars/css.tmLanguage.json`)).text()
        }
    }
})
Copy the code

Grammars: grammars: grammars: grammars: grammars: grammars: grammars: grammars: grammars: grammars: grammars: grammars: grammars: grammars: grammars: grammars: grammars: grammars: grammars: grammars: grammars: grammars

"grammars": [{"language": "css"."scopeName": "source.css"."path": "./syntaxes/css.tmLanguage.json"."tokenTypes": {
            "meta.function.url string.quoted": "other"}}]Copy the code

The path field is the path of the corresponding syntax files. We copy these JSON files to the /public/grammars/ directory of the project, so that the fetch can be requested.

Custom theme

The Monaco Editor theme format is a bit different from the Monaco Code theme format, so you need to convert the Monaco Editor theme format. You can convert the Monaco Editor theme format yourself or use the Monaco-vscode-textmate-Converter tool directly. It can convert multiple local files at the same time:

// convertTheme.js
const converter = require('monaco-vscode-textmate-theme-converter')
const path = require('path')

const run = async() = > {try {
        await converter.convertThemeFromDir(
            path.resolve(__dirname, './vscodeThemes'), 
            path.resolve(__dirname, '.. /public/themes')); }catch (error) {
        console.log(error)
    }
}
run()
Copy the code

Running the node./ converttheme. js command will convert all of your VSCode theme files under the vscodeThemes directory to the Monaco Editor theme files and export them to the public/themes directory. We then fetch the theme file directly in our code and define the theme using the defineTheme method:

// Request the OneDarkPro theme file
const themeData = await (
    await fetch(`${base}themes/OneDarkPro.json`)
).json()
// Define the topic
monaco.editor.defineTheme('OneDarkPro', themeData)
Copy the code

Set the token resolver

The last step is to set up the Monaco Editor’s token parser, which uses the built-in Monarch by default. We’ll switch to the Monaco-Editor-TextMate parser, which is what monaco-Editor-TextMate does:

import {
    wireTmGrammars
} from 'monaco-editor-textmate'
import * as monaco from 'monaco-editor'

let editor = monaco.editor.create(document.getElementById('container'), {
    value: [
        'html, body {'.' margin: 0; '.'} '
    ].join('\n'),
    language: 'css'.theme: 'OneDarkPro'
})

await wireTmGrammars(monaco, registry, grammars, editor)
Copy the code

Question 1

After the previous step, you should see that the VSCode theme works in the Monaco Editor, but if you try it a few times, you may find that it occasionally fails. The reason is that the Monaco Editor built-in language is lazily loaded, and a token parser is also registered after loading, which overwrites our theme. See issue: setTokensProvider Unable to override Existing tokenizer.

One solution is to remove the built-in language, which can be done using the Monaco-editor-webpack-plugin.

Installation:

npm install monaco-editor-webpack-plugin -D
Copy the code

Vue project configuration is as follows:

// vue.config.js
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')

module.exports = {
    configureWebpack: {
        plugins: [
            new MonacoWebpackPlugin({
                languages: []})]}}Copy the code

The languages option is used to specify the language to include, so let’s just leave it empty.

Then modify the Monaco Editor introduction as follows:

import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'
Copy the code

Finally, we need to manually register the language we need, because all the built-in languages have been removed, for example if we want to use JS:

monaco.languages.register({id: 'javascript'})
Copy the code

Although this method can solve the problem perfectly, but a big side effect is that the syntax prompt does not take effect, because only when the built-in HTML, CSS and typescript are included, the corresponding worker file will be loaded. Without the syntax prompt, the author cannot accept it. Therefore, the author finally uses a low hack:

// Plug-in configuration
new MonacoWebpackPlugin({
    languages: ['css'.'html'.'javascript'.'less'.'pug'.'scss'.'typescript'.'coffee']})// Comment out language registration statements
// monaco.languages.register({id: 'javascript'})

// Wire when the worker file is loaded
let hasGetAllWorkUrl = false
window.MonacoEnvironment = {
    getWorkerUrl: function (moduleId, label) {
        hasGetAllWorkUrl = true
        if (label === 'json') {
            return './monaco/json.worker.bundle.js'
        }
        if (label === 'css' || label === 'scss' || label === 'less') {
            return './monaco/css.worker.bundle.js'
        }
        if (label === 'html' || label === 'handlebars' || label === 'razor') {
            return './monaco/html.worker.bundle.js'
        }
        if (label === 'typescript' || label === 'javascript') {
            return './monaco/ts.worker.bundle.js'
        }
        return './monaco/editor.worker.bundle.js'}},// Loop check
let loop = () = > {
    if (hasGetAllWorkUrl) {
        Promise.resolve().then(async() = > {await wireTmGrammars(monaco, registry, grammars, editor)
        })
    } else {
        setTimeout(() = > {
            loop()
        }, 100)
    }
}
loop()
Copy the code

Question 2

Another problem I encountered was that the default color of some themes was not set after conversion, so they were all black, which was ugly:

The solution to this problem is to add an empty token to the topic’s rules array to be used as the default token that is not matched:

{
    "rules": [{"foreground": "#abb2bf"."token": ""}}]Copy the code

The color foreground value can be the same as the editor.foreground value in the colors option. It is troublesome to modify each color value manually.

Question 3

Monaco-vscode-textmate-theme-converter is a monaco-vscode-textmate-Converter package, which is a nodeJS tool, so it is not convenient to use in a pure front-end environment. In addition, it will report errors when converting non-standard JSON vscode themes. Because many themes are in. Jsonc format and have a lot of comments, they need to be checked and modified by themselves, which is not very convenient. Based on these two problems, the author fork its code, and then modify and divide it into two packages, corresponding to nodeJS and browser environment respectively. See github.com/wanglin2/mo… .

So we can replace monaco-vscode-textmate-Theme-Converter with the author’s:

npm i vscode-theme-to-monaco-theme-node -D
Copy the code

The usage is basically the same:

// Just modify the package imported as the author
const converter = require('vscode-theme-to-monaco-theme-node')
const path = require('path')

const run = async() = > {try {
        await converter.convertThemeFromDir(
            path.resolve(__dirname, './vscodeThemes'), 
            path.resolve(__dirname, '.. /public/themes')); }catch (error) {
        console.log(error)
    }
}
run()
Copy the code

Jsonc files can now be converted directly and output as a. Json file. In addition, an empty token is automatically added internally as the default token that is not matched.

Best practices

VSCode theme in addition to code the theme, generally also includes other parts of the theme editor, such as the title bar, status bar, the sidebar, buttons and so on, so we also can apply these styles on the page, to the theme of the entire page can also with the theme editor code switching effect, so can make the page more harmonious whole, specific implementation, We can use CSS variables, first define all the colors involved in the page as CSS variables, and then update the variables according to the specified field in the colors option of the theme when switching the theme. The specific field to use for which part of the page can be determined according to the actual situation. All configurable items for the VSCode theme can be found here in theme-color. The effect is as follows:

conclusion

This article provides a complete and detailed introduction to my exploration of the Monaco Editor theme, hoping to help those who need to customize the theme. For the complete code, please refer to the source code of this project: code-run.

Refer to the link

Monaco is highlighted on the browser using vscode syntax

Article: How does CodesandBox solve the topic problem

Chat Monaco Editor- Custom language Monarch

Discussion: How to use VSC themes in the Monaco Editor?

Discussion: Using WebAssembly to support TextMate syntax