Teach you how to use the VSCode theme in Monaco Editor

background

The author opened a small project code-run , a tool similar to codepen, in which the code editor uses Microsoft Monaco Editor , this library is directly generated from the source code of VSCode, but it has been modified to support running in the browser, but the function is basically as powerful as VSCode, so in my opinion, Monaco Editor is equal to the core of VSCode editor.

In addition, the author is a face control. No matter what project he does, he is keen to support some good-looking skin and themes. Therefore, only three themes are built in Moncao Editor, which is far from meeting the author's needs. Moreover, they are ugly. Therefore, combined with the relationship between Monaco Editor and VSCode, it is natural to think about whether the themes of VSCode can be reused directly, Next, let's introduce the writer's way of exploration.

ps. if you want to know how to implement it directly, you can jump to the [specific implementation] section.

Basic use

Let's take a look at the basic use of Monaco Editor. First, install:

npm install monaco-editor

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'
})

In this way, you can create a js language editor on the container element and use the built-in vs dark theme. If you encounter an error or the syntax prompt does not take effect, you may need to configure the path of the worker file. You can refer to the official example browser-esm-webpack.

Custom theme

Monaco Editor supports custom themes as follows:

// Define theme
monaco.editor.defineTheme(themeName, themeData)
// Use defined topics
monaco.editor.setTheme(themeName)

themeName is the subject name to be customized, such as OneDarkPro. themeData is an object, that is, subject data. The basic structure is as follows:

{
    base: 'vs',// The basic topics to inherit are the built-in three: vs, vs dark and HC black
    inherit: false,// Inherit
    rules: [// Highlight rules, that is, set different display styles for codes with different token types in the code
        { token: '', foreground: '000000', background: 'fffffe' }
    ],
    colors: {// Colors of other parts of non code parts, such as background, scroll bar, etc
        [editorBackground]: '#FFFFFE'
    }
}

rules are used to highlight the code. Common token s include string, comment, keyword, etc. Please move to the next step themes.ts , how are these token s determined? Monaco Editor has a built-in syntax shader Monarch , the essence is to match by regular expression, and then name the matched content as a token.

You can directly view the token corresponding to a piece of code in the editor, press F1 or right-click the Command Palette, and then find and click Developer: Inspect Tokens. Next, click which piece of code, and the corresponding information will be displayed, including token type, current applied color, etc.

Stepping pit

The initial idea is very simple. You can directly find the theme file of VSCode and use it through custom themes.

Get VSCode theme file

There are two methods. If a theme has been installed and is being used in your VSCode, you can press F1 or Command/Control + Shift + P or right-click the Command Palette / command panel, and then find and click Developer:Generate Color Theme From Current Setting / Developer: generate color theme using current settings, Then VSCode will generate a json data and save it.

If a theme is not installed, you can go vscode Theme Store Search for the topic, click the Download Extension button on the right after entering the topic details page to download the topic. After downloading, find the file just downloaded. The file should end with. vsix. Change the suffix to. zip directly, then unzip it, and finally open the / extension/themes / folder. The. json file in it is the topic file, Open the file and copy the json data.

Convert VSCode theme to Monaco Editor theme format

After the previous step, you should find that the format of the VSCode topic is as follows:

{
    "$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"
            }
        }
    ]
}  

It is a little different from the topic format of Monaco Editor. Can I write a conversion method to convert it into the following:

{
    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"
    }
}

Of course, it's not difficult, but in the end, when you use this custom theme, you will find that it has no effect. Why Monarch After looking at the parsing configuration of the corresponding language, you will find that there are no token s defined in the VSCode topic at all. It's strange that they are effective. What should I do? Do I extend the parsing configuration myself? I did it at the beginning. It should not be difficult to write regular expressions. Therefore, I also translated the Monarch document completely Monarch Chinese However, when the author sees the following effects in VSCode:

Give up decisively, which obviously requires semantic analysis. Otherwise, who knows abc is a variable.

In fact, TextMate is used for syntax highlighting in VSCode and Monarch is used in Monaco Editor. They are not the same thing at all. Why does Monaco Editor not use TextMate but develop a new thing? The reason is that VSCode uses vscode-textmate To parse TextMate syntax, this library relies on an Oniguruma regular expression library, which is developed in C language and does not support running on browsers.

Second best

Since the theme of VSCode cannot be used directly, you can only use as many as you can, because there are only so many theme tokens built in Monaco Editor, wouldn't it be OK to replace all its token colors with the theme color of VSCode? Although there is no semantic highlight, it is always better than the default theme. The implementation is also very simple. Firstly, the colors part can be used directly, while the token part can be used through the method described above. Developer: Inspect Tokens can find the color of the corresponding code block in VSCode and copy it to the corresponding token of the Monaco Editor theme. For example, the actual effect of OneDarkPro after the author's conversion is as follows:

The effect in VSCode is as follows:

You can only look at it roughly, not carefully.

Someone has already done this. You can refer to this warehouse monaco-themes , it helps you convert some common topics, which can be used directly.

New dawn

Just after I gave up the idea of using the VSCode theme directly in Monaco Editor, I inadvertently found that codesandbox and leetcode The editor theme effect in the two websites is basically the same as that in VSCode, and you can obviously see the files requested to switch themes in leetcode:

The basic format is the same as that of the VSCode topic, which shows that the VSCode topic can be implemented in Monaco Editor, so the problem becomes how to implement it.

realization

I have to say that there is really little information in this regard. There are basically no relevant articles. There are only one or two relevant links in Baidu search results, but it is enough to solve the problem. See the tail of the article for relevant links.

The main use is monaco-editor-textmate This tool (so besides Baidu and Google, github is also a very important search engine). First install:

npm i monaco-editor-textmate

npm should help you reinstall it at the same time monaco-textmate,onigasm Monaco editor does not need to say that we have installed the other two packages. If not, we need to install them ourselves.

Tool introduction

Briefly introduce these packages.

onigasm

This library is used to solve the problem that the above browser does not support Oniguruma written in C language. The solution is to compile Oniguruma into WebAssembly , WebAssembly is an intermediate format. You can compile non js code into. wasm format files, and then the browser can load and run it. WebAssembly has become one of the WEB standards. Over time, I believe compatibility is not a problem.

monaco-textmate

This library is modified based on the VSCode TextMate library used by VSCode so that it can be used on the browser. The main function is to parse TextMate syntax. This library depends on the previous onigasm.

monaco-editor-textmate

The main function of this library is to help us associate monaco-editor with monaco-textmate. First, we load the TextMate syntax file of the corresponding language, and then call it. monaco.languages.setTokensProvider Method comes from the token parser of the definition language.

Take a look at its usage example:

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)
}

Concrete implementation

After reading the previous usage examples, let's take a detailed look at how to use them.

Load onigasm

First, we need to load the wasm file of onigasm. This file needs to be loaded first and only once, so we load it before the editor initialization:

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

The onigasm.wasm file can be found in / node_ Find the modules / onigasm / lib / directory, and then copy it to the / public/onigasm / directory of the project, so that you can make a request through http.

Create scope mapping

Next, create a mapping from the language id to the scope Name:

const grammars = new Map()
grammars.set('css', 'source.css')

Scope names for other languages can be found in Syntax list of various languages For example, if you want to know the scope name of css, go to the css directory and open the package.json file. You can see that there is a grammars field:

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

Language is the language id and scopeName is the scope name. Common 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'
}

Register syntax mapping

Then register the syntax mapping relationship of TextMate, so that the corresponding syntax can be loaded and created through the action domain name:

import {
    Registry
} from 'monaco-textmate'

// Create a registry to load the corresponding syntax file from the active domain name
const registry = new Registry({
    getGrammarDefinition: async (scopeName) => {
        return {
            format: 'json',// Syntax file formats include json and plist
            content: await (await fetch(`${base}grammars/css.tmLanguage.json`)).text()
        }
    }
})

The syntax file is the same as the previous scope name Syntax list of various languages Here, take css language as an example, or look at the grammars field of package.json:

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

The path field is the path of the corresponding syntax file. We copy these json files to the / public/grammars / directory of the project, so that we can request them through fetch.

Define theme

As mentioned earlier, the topic format of Monaco Editor is a little different from that of VSCode, so it needs to be converted. The conversion can be implemented or used directly monaco-vscode-textmate-theme-converter This tool 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()

After running the node. / converttheme.js command, all VSCode theme files you put in the vscodeThemes directory will be converted into Monaco Editor theme files and output to the public/themes directory. Then we can directly request theme files through fetch in the code and use the defineTheme method to define themes:

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

Set token parser

After the above preparations, the last step is to set the token parser of Monaco Editor. The built-in Monarch is used by default. We need to change to the parser of TextMate, that 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)

Question 1

After the previous step, you should see that the topic of VSCode has taken effect on Monaco Editor. However, if you try several times, you may find that it will fail occasionally. The reason is that the built-in language of Monaco Editor is delayed loading, and a token parser will also be registered after loading, so our will be overwritten. See issue for details: setTokensProvider unable to override existing tokenizer.

One solution is to remove the built-in language, which can be used monaco-editor-webpack-plugin.

Installation:

npm install monaco-editor-webpack-plugin -D

Vue project configuration is as follows:

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

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

The languages option is used to specify the language to be included. We directly set it to blank and don't want anything.

Then modify the import method of Monaco Editor as follows:

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

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

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

Although this method can perfectly solve this problem, a big side effect is that the syntax prompt does not take effect, because the corresponding worker file will be loaded only when the built-in html, css and typescript are included. Without syntax prompt, the author is also unacceptable. Finally, the author uses a low hack method:

// Plug in configuration
new MonacoWebpackPlugin({
    languages: ['css', 'html', 'javascript', 'less', 'pug', 'scss', 'typescript', 'coffee']
})

// Comment out the language registration statement
// monaco.languages.register({id: 'javascript'})

// When the worker file is loaded, the wire
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'
    },
}
// Cycle detection
let loop = () => {
    if (hasGetAllWorkUrl) {
        Promise.resolve().then(async () => {
            await wireTmGrammars(monaco, registry, grammars, editor)
        })
    } else {
        setTimeout(() => {
            loop()
        }, 100)
    }
}
loop()

Question 2

Another problem encountered by the author is that the default colors of some themes after conversion are not set, so they are black and ugly:

The solution to this problem is to add an empty token to the rules array of the topic as the default token that does not match:

{
    "rules": [
        {
            "foreground": "#abb2bf",
            "token": ""
        }
     ]
}

The color value of foreground can take the value of editor.foreground in the colors option. It is troublesome to manually modify each color value, which can be carried out in the previous steps of topic conversion, and will be solved together in the next problem.

Question 3

monaco-vscode-textmate-theme-converter This package is essentially a tool in the nodejs environment, so it is not convenient to use it in the pure front-end environment. In addition, it will report errors when converting VSCode topics in non-standard json format. Because many topic formats are. jsonc and the content contains many comments, they need to be checked and modified first. It is not very convenient. Based on these two problems, The author fork ed its code, then modified it and divided it into two packages, corresponding to nodejs and browser environment respectively. See https://github.com/wanglin2/monaco-vscode-textmate-theme-converter.

So we can replace Monaco vscode Textmate Theme Converter and install the author's:

npm i vscode-theme-to-monaco-theme-node -D

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()

Now you can directly convert the. jsonc file, and the output is unified into. json file. In addition, an empty token will be automatically added as the default token that is not matched. The effect is as follows:

Best practices

In addition to the code theme, the VSCode theme generally includes the themes of other parts of the editor, such as title bar, status bar, sidebar, button, etc. Therefore, we can also apply these styles on the page to achieve the effect that the theme of the whole page can also be switched with the editor code theme, which can make the page more coordinated as a whole and 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 fields in the colors option of the theme when switching themes. The specific field used to correspond to which part of the page can be determined according to the actual situation. All configurable items of VSCode theme can be theme-color Find it here. The effects are as follows:

summary

This article introduces the author's exploration of the Monaco Editor theme in detail, hoping to give some help to the partners who need to customize the theme. For the complete code, please refer to the source code of this project: code-run.

Reference link

article: monaco uses vscode related syntax to highlight on the browser

article: How does codesandbox solve the problem of theme

article: Chat about Monaco Editor - Monarch of custom language

Discussion: How do I use VSC themes in Monaco Editor?

Discussion: Use WebAssembly to support TextMate syntax

Tags: Front-end

Posted by skyriders on Mon, 27 Sep 2021 17:38:45 +0530