相关文章推荐
乖乖的电池  ·  中南大学商学院·  5 天前    · 
千杯不醉的仙人球  ·  混音师_百度百科·  1 周前    · 
活泼的伤疤  ·  井陉县_百度百科·  8 月前    · 
大气的针织衫  ·  回顾张艺谋张伟平合作之路:缘起于" ...·  1 年前    · 
俊逸的围巾  ·  25句伤感的说说,每一句都深情走心,致傻乎乎 ...·  1 年前    · 
Code  ›  手把手教你实现在Monaco Editor中使用VSCode主题开发者社区
editor 前端开发 const vscode
https://cloud.tencent.com/developer/article/1960602
不爱学习的番茄
1 年前
作者头像
街角小林
0 篇文章

手把手教你实现在Monaco Editor中使用VSCode主题

前往专栏
腾讯云
开发者社区
文档 意见反馈 控制台
首页
学习
活动
专区
工具
TVP
文章/答案/技术大牛
发布
首页
学习
活动
专区
工具
TVP
返回腾讯云官网
社区首页 > 专栏 > 有意思的前端世界 > 手把手教你实现在Monaco Editor中使用VSCode主题

手把手教你实现在Monaco Editor中使用VSCode主题

作者头像
街角小林
发布 于 2022-03-21 20:07:47
1.8K 0
发布 于 2022-03-21 20:07:47
举报

背景

笔者开源了一个小项目 code-run ,类似 codepen 的一个工具,其中代码编辑器使用的是微软的 Monaco Editor ,这个库是直接从 VSCode 的源码中生成的,只不过是做了一点修改让它支持在浏览器中运行,但是功能基本是和 VSCode 一样强大的,所以在笔者看来 Monaco Editor 等于 VSCode 的编辑器核心。

另外笔者是一个颜控,不管做什么项目,都热衷于配套一些好看的皮肤、主题,所以 Moncao Editor 仅仅内置了三种主题是远远满足不了笔者需求的,况且还都很丑,于是结合 Monaco Editor 和 VSCode 的关系就很自然的想到,能不能直接复用 VSCode 的主题,接下来就给大家介绍一下笔者的探索之路。

ps.想直接了解如何实现的可以跳转到【具体实现】小节。

基本使用

先看一下 Monaco Editor 的基本使用,首先安装:

npm install monaco-editor

然后引入:

import * as monaco from 'monaco-editor'
// 创建一个js编辑器
const editor = monaco.editor.create(document.getElementById('container'), {
    value: ['function x() {', '\tconsole.log("Hello world!");', '}'].join('\n'),
    language: 'javascript',
    theme: 'vs'
})

这样就可以在 container 元素上创建一个 js 语言的编辑器,并且使用了内置的 vs-dark 主题。如果遇到报错或者语法提示不生效,那么可能需要配置一下 worker 文件的路径,可以参考官方示例 browser-esm-webpack 。

自定义主题

Monaco Editor 支持自定义主题,方法如下:

// 定义主题
monaco.editor.defineTheme(themeName, themeData)
// 使用定义的主题
monaco.editor.setTheme(themeName)

themeName 是要自定义的主题名称,比如 OneDarkPro , themeData 是一个对象,即主题数据,基本结构如下:

{
    base: 'vs',// 要继承的基础主题,即内置的三个:vs、vs-dark、hc-black
    inherit: false,// 是否继承
    rules: [// 高亮规则,即给代码里不同token类型的代码设置不同的显示样式
        { token: '', foreground: '000000', background: 'fffffe' }
    colors: {// 非代码部分的其他部分的颜色,比如背景、滚动条等
        [editorBackground]: '#FFFFFE'
}

rules 里面就是用来给代码进行高亮的,常见的 token 有 string (字符串)、 comment (注释)、 keyword (关键词)等等,完整的请移步 themes.ts ,这些 token 是怎么确定的呢, Monaco Editor 内置了一个语法着色器 Monarch ,本质是通过正则表达式来匹配,然后给匹配到的内容命名为一个 token 。

可以直接在编辑器中查看代码某块对应的 token ,按 F1 或鼠标右键点击 Command Palette ,然后再找到并点击 Developer: Inspect Tokens ,接下来鼠标点哪一块代码,就会显示对应的信息,包括 token 类型,当前应用的颜色等。

踩坑

最开始的想法很简单,直接找到 VSCode 的主题文件,然后通过自定义主题来使用。

获取 VSCode 主题文件

有两种方法,如果某个主题已经在你的 VSCode 里安装并正在使用的话,那么可以按 F1 或 Command/Control + Shift + P 或鼠标右键点击 Command Palette/命令面板 ,接着找到并点击 Developer:Generate Color Theme From Current Setting/开发人员:使用当前设置生成颜色主题 ,然后 VSCode 就会生成一份 json 数据,保存即可。

如果某个主题没有安装的话,那么可以去 vscode主题商店 搜索该主题,进入主题详情页面后点击右侧的 Download Extension 按钮即可下载该主题,下载完成后找到刚才下载的文件,文件应该是以 .vsix 结尾的,直接把该后缀改成 .zip ,然后解压缩,最后打开里面的 /extension/themes/ 文件夹,里面的 .json 文件即主题文件,打开该文件复制 json 数据即可。

把 VSCode 主题转换成 Monaco Editor 主题格式

上一步过后你应该可以发现 VSCode 主题的格式是这样的:

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

跟 Monaco Editor 的主题格式有一点区别,那是不是可以写一个转换方法把它转换成下面这样呢:

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

当然可以,这也不难,但是最后当你使用这个自定义的主题后会发现,没有效果,为什么呢,去 Monarch 看一下对应语言的解析配置后就会发现,压根就没有 VSCode 主题里定义的这些 token ,有效果才奇怪,那怎么办呢,自己扩展这个解析的配置吗,笔者最开始就是这么做的,写正则表达式嘛,应该也不是很难,为此,笔者还把 Monarch 文档完整翻译了一遍 Monarch中文 ,但是当笔者在 VSCode 里看到如下效果时:

image-20210918142132745.png

果断放弃,这显然是要进行语义分析才行,否则谁知道 abc 是个变量。

其实在 VSCode 里语法高亮使用的是 TextMate ,而在 Monaco Editor 里使用的是 Monarch ,两者压根不是一个东西,为什么 Monaco Editor 不使用 TextMate ,而是要开发一个新的东西呢,原因是 VSCode 使用的是 vscode-textmate 来解析 TextMate 语法,这个库依赖一个 Oniguruma 正则表达式库,而这个正则表达式库是使用 C 语言开发的,当然不支持在浏览器上运行。

退而求其次

既然 VSCode 的主题不能直接使用,那么就只能能用多少用多少,因为 Monaco Editor 内置的主题 token 就只有那么多,那么把它所有的 token 颜色换成 VSCode 的主题颜色不就行了吗,虽然语义高亮没有,但是总比默认主题好看。实现也很简单,首先 colors 部分的基本可以直接使用,而 token 部分可以通过上面介绍的方法 Developer: Inspect Tokens 在 VSCode 里找到对应代码块的颜色,复制到 Monaco Editor 主题的对应 token 上即可,比如笔者转换后的 OneDarkPro 的实际效果如下:

image-20210918143406409.png

在 VSCode 里的效果如下:

image-20210918143427581.png

只可粗看,不要细究。

这个事情也有人已经做了,可以参考这个仓库 monaco-themes ,里面帮你转换了一些常见的主题,可以拿来直接使用。

新的曙光

就在笔者已经放弃在 Monaco Editor 中直接使用 VSCode 主题的想法后,无意间发现 codesandbox 和 leetcode 两个网站中的编辑器主题效果和 VSCode 中基本一致,而且可以明显的看到在 leetcode 中切换主题请求的文件:

image-20210918161935357.png

基本和 VSCode 主题格式是一样的,这就说明在 Monaco Editor 中使用 VSCode 主题是可以实现的,那么问题就变成了怎么实现。

实现

不得不说,这方面资料真的很少,相关文章基本没有,百度搜索结果里只有一两个相关的链接,不过也足以解决问题了,相关链接详见文章尾部。

主要使用的是 monaco-editor-textmate 这个工具(所以除了百度谷歌之外, github 也是一个很重要的搜索引擎啊),先安装:

npm i monaco-editor-textmate

npm 应该会同时帮你再安装 monaco-textmate 、 onigasm 、 monaco-editor 这几个包, monaco-editor 自不必说,我们自己都装了,其他两个可以自行检查一下,如果没有的话需要自行安装。

工具介绍

简单介绍一下这几个包。

onigasm

这个库就是用来解决上述浏览器不支持 C 语言编写的 Oniguruma 的问题,解决方法是把 Oniguruma 编译为 WebAssembly , WebAssembly 是一种中间格式,可以把非 js 代码编译成 .wasm 格式的文件,然后浏览器就可以加载并运行它了, WebAssembly 已经是 WEB 的标准之一了,随着时间的推移,相信兼容性也不是问题。

monaco-textmate

这个库是在 VSCode 使用的 vscode-textmate 库的基础上修改的, 以便让它在浏览器上使用。主要作用是解析 TextMate 语法,这个库依赖前面的 onigasm 。

monaco-editor-textmate

这个库的主要作用是帮我们把 monaco-editor 和 monaco-textmate 关联起来,内部首先会加载对应语言的 TextMate 语法文件,然后调用 monaco.languages.setTokensProvider 方法来自定义语言的 token 解析器。

看一下它的使用示例:

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

具体实现

看完前面的使用示例后,接下来我们详细看一下如何使用。

加载onigasm

首先我们要做的是加载 onigasm 的 wasm 文件,这个文件需要首先被加载,且加载一次就可以了,所以我们在编辑器初始化前进行加载:

import { loadWASM } from 'onigasm'
const init = async () => {
    await loadWASM(`${base}/onigasm/onigasm.wasm`)
    // 创建编辑器...
init()

onigasm.wasm 文件可以在 /node_modules/onigasm/lib/ 目录下找到,然后复制到项目的 /public/onigasm/ 目录下,这样可以通过 http 进行请求。

创建作用域映射

接下来创建语言 id 到作用域名称的映射:

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

其他语言的作用域名称可以在 各种语言的语法列表 这里找到,比如想知道 css 的作用域名称,我们进入 css 目录,然后打开 package.json 文件,可以看到其中有一个 grammars 字段:

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

language 就是语言 id , scopeName 就是作用域名称。常见的如下:

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

注册语法映射

再接着注册 TextMate 的语法映射关系,这样可以通过作用域名称来加载并创建对应的语法:

import {
    Registry
} from 'monaco-textmate'
// 创建一个注册表,可以从作用域名称来加载对应的语法文件
const registry = new Registry({
    getGrammarDefinition: async (scopeName) => {
        return {
            format: 'json',// 语法文件格式,有json、plist
            content: await (await fetch(`${base}grammars/css.tmLanguage.json`)).text()
})

语法文件和前面的作用域名称一样,也是在 各种语言的语法列表 这里找,同样以 css 语言为例,还是看它的 package.json 的 grammars 字段:

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

path 字段就是对应的语法文件的路径,我们把这些 json 文件复制到项目的 /public/grammars/ 目录下,这样就可以通过 fetch 来请求到。

定义主题

前面介绍过, Monaco Editor 的主题格式和 VSCode 的格式是有点不一样的,所以需要进行转换,转换可以自己实现,也可以直接使用 monaco-vscode-textmate-theme-converter 这个工具,它可以同时转换多个本地文件:

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

运行 node ./convertTheme.js 命令后,就会把你放在 vscodeThemes 目录下所有 VSCode 的主题文件转换成 Monaco Editor 的主题文件并输出到 public/themes 目录下,然后我们在代码里直接通过 fetch 来请求主题文件并使用 defineTheme 方法定义主题即可:

// 请求OneDarkPro主题文件
const themeData = await (
    await fetch(`${base}themes/OneDarkPro.json`)
).json()
// 定义主题
monaco.editor.defineTheme('OneDarkPro', themeData)

设置token解析器

经过前面这些准备工作,最后一步要做的是设置 Monaco Editor 的 token 解析器,默认使用的是内置的 Monarch ,我们要换成 TextMate 的解析器,也就是 monaco-editor-textmate 做的事情:

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)

问题1

上一步后应该可以看到 VSCode 的主题在 Monaco Editor 上生效了,但是多试几次可能会发现偶尔会失效,原因是 Monaco Editor 内置的语言是延迟加载的,并且加载完后也会同样注册一个 token 解析器,所以会把我们的给覆盖掉,详见 issue : setTokensProvider unable to override existing tokenizer 。

一种解决方法是去除内置的语言,这可以使用 monaco-editor-webpack-plugin 。

安装:

npm install monaco-editor-webpack-plugin -D

Vue 项目配置如下:

// vue.config.js
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')
module.exports = {
    configureWebpack: {
        plugins: [
            new MonacoWebpackPlugin({
                languages: []
}

languages 选项用来指定要包含的语言,我们直接设为空,啥也不要。

然后修改 Monaco Editor 的引入方式为:

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

最后需要手动注册我们需要的语言,因为所有内置语言都被去除了嘛,比如我们要使用 js 语言的话:

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

这种方法虽然可以完美解决该问题,但是很大的一个副作用是语法提示不生效了,因为只有包含了内置的 html 、 css 、 typescript 时才会去加载对应的 worker 文件,没有语法提示笔者也是无法接受的,所以最后笔者使用了一种比较 low 的 hack 方式:

// 插件配置
new MonacoWebpackPlugin({
    languages: ['css', 'html', 'javascript', 'less', 'pug', 'scss', 'typescript', 'coffee']
// 注释掉语言注册语句
// monaco.languages.register({id: 'javascript'})
// 当worker文件被加载了后再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'
// 循环检测
let loop = () => {
    if (hasGetAllWorkUrl) {
        Promise.resolve().then(async () => {
            await wireTmGrammars(monaco, registry, grammars, editor)
    } else {
        setTimeout(() => {
            loop()
        }, 100)
loop()

问题2

笔者遇到的另外一个问题是,转换后有些主题的默认颜色并未设置,所以都是黑色,很丑:

image-20210924105525593.png

这个问题的解决方法是可以给主题的 rules 数组添加一个空的 token ,用来作为没有匹配到的默认 token :

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

foreground 的色值可以取 colors 选项里的 editor.foreground 的值,要手动修改每个色值比较麻烦,可以在之前的转换主题的步骤里顺便进行,会在下一个问题里一起解决。

问题3

monaco-vscode-textmate-theme-converter 这个包本质算是 nodejs 环境下的工具,所以想在纯前端环境下使用不太方便,另外它对于非标准 json 格式的 VSCode 主题转换时会报错,因为很多主题格式是 .jsonc ,内容是带有很多注释的,所以都需要自己先进行检查并修改,不是很方便,基于这两个问题,笔者 fork 了它的代码,然后修改并分成了两个包,分别对应 nodejs 和 浏览器 环境,详见 https://github.com/wanglin2/monaco-vscode-textmate-theme-converter 。

所以我们可以替换掉 monaco-vscode-textmate-theme-converter ,改成安装笔者的:

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

使用方式基本是一样的:

// 只要修改引入为笔者的包即可
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')
 
推荐文章
乖乖的电池  ·  中南大学商学院
5 天前
千杯不醉的仙人球  ·  混音师_百度百科
1 周前
活泼的伤疤  ·  井陉县_百度百科
8 月前
大气的针织衫  ·  回顾张艺谋张伟平合作之路:缘起于"有话好好说"-中新网
1 年前
俊逸的围巾  ·  25句伤感的说说,每一句都深情走心,致傻乎乎的自己! - 知乎
1 年前
今天看啥   ·   Py中国   ·   codingpro   ·   小百科   ·   link之家   ·   卧龙AI搜索
删除内容请联系邮箱 2879853325@qq.com
Code - 代码工具平台
© 2024 ~ 沪ICP备11025650号