关于javascript:手把手教你实现在Monaco-Editor中使用VSCode主题

8次阅读

共计 10676 个字符,预计需要花费 27 分钟才能阅读完成。

背景

笔者开源了一个小我的项目 code-run,相似 codepen 的一个工具,其中代码编辑器应用的是微软的 Monaco Editor,这个库是间接从 VSCode 的源码中生成的,只不过是做了一点批改让它反对在浏览器中运行,然而性能根本是和 VSCode 一样弱小的,所以在笔者看来 Monaco Editor 等于 VSCode 的编辑器外围。

另外笔者是一个颜控,不论做什么我的项目,都热衷于配套一些难看的皮肤、主题,所以 Moncao Editor 仅仅内置了三种主题是远远满足不了笔者需要的,况且还都很丑,于是联合 Monaco EditorVSCode的关系就很天然的想到,能不能间接复用 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是要自定义的主题名称,比方 OneDarkProthemeData 是一个对象,即主题数据,根本构造如下:

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

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

能够间接在编辑器中查看代码某块对应的 token,按F1 或鼠标右键点击 Command Palette,而后再找到并点击Developer: Inspect Tokens,接下来鼠标点哪一块代码,就会显示对应的信息,包含token 类型,以后利用的色彩等。

踩坑

最开始的想法很简略,间接找到 VSCode 的主题文件,而后通过自定义主题来应用。

获取 VSCode 主题文件

有两种办法,如果某个主题曾经在你的 VSCode 里装置并正在应用的话,那么能够按 F1Command/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 里看到如下成果时:

果决放弃,这显然是要进行语义剖析才行,否则谁晓得 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 TokensVSCode里找到对应代码块的色彩,复制到 Monaco Editor 主题的对应 token 上即可,比方笔者转换后的 OneDarkPro 的实际效果如下:

VSCode 里的成果如下:

只可粗看,不要细究。

这个事件也有人曾经做了,能够参考这个仓库 monaco-themes,外面帮你转换了一些常见的主题,能够拿来间接应用。

新的曙光

就在笔者曾经放弃在 Monaco Editor 中间接应用 VSCode 主题的想法后,无意间发现 codesandbox 和 leetcode 两个网站中的编辑器主题成果和 VSCode 中基本一致,而且能够显著的看到在 leetcode 中切换主题申请的文件:

根本和 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-editormonaco-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

首先咱们要做的是加载 onigasmwasm文件,这个文件须要首先被加载,且加载一次就能够了,所以咱们在编辑器初始化前进行加载:

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就是语言 idscopeName 就是作用域名称。常见的如下:

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.jsongrammars字段:

"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 Editortoken解析器,默认应用的是内置的 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'})

这种办法尽管能够完满解决该问题,然而很大的一个副作用是语法提醒不失效了,因为只有蕴含了内置的 htmlcsstypescript 时才会去加载对应的 worker 文件,没有语法提醒笔者也是无奈承受的,所以最初笔者应用了一种比拟 lowhack形式:

// 插件配置
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

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

这个问题的解决办法是能够给主题的 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')
        );
    } catch (error) {console.log(error)
    }
}
run()

当初就能够间接转换 .jsonc 文件,而且输入对立为 .json 文件,另外外部会主动增加一个空的 token 作为没有匹配到的默认token,成果如下:

最佳实际

VSCode主题除了代码主题外,个别还蕴含编辑器其余局部的主题,比方标题栏、状态栏、侧边栏、按钮等等,所以咱们也能够在页面利用这些款式,达到整个页面的主题也能随编辑器代码主题一起切换的成果,这样能让页面整体更加协调,具体的实现上,咱们能够应用 CSS 变量,先把页面所有波及到的色彩都定义成 CSS 变量,而后在切换主题时依据主题的 colors 选项里的指定字段来更新变量即可,具体应用哪个字段来对应页面的哪个局部能够依据理论状况来确定,VSCode主题的所有可配置项能够在 theme-color 这里找到。成果如下:

总结

本文残缺具体的介绍了笔者对于 Monaco Editor 编辑器主题的摸索,心愿能给有主题定制需要的小伙伴们一点帮忙,残缺的代码请参考本我的项目源码:code-run。

参考链接

文章:monaco 应用 vscode 相干语法高亮在浏览器上显示

文章:codesandbox 是如何解决主题的问题

文章:闲聊 Monaco Editor- 自定义语言之 Monarch

探讨:如何在 Monaco Editor 中应用 VSC 主题?

探讨:应用 WebAssembly 来反对 TextMate 语法

正文完
 0