背景
笔者开源了一个小我的项目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
里看到如下成果时:
果决放弃,这显然是要进行语义剖析才行,否则谁晓得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
的实际效果如下:
在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-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.jsconst 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.jsconst 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文件被加载了后再wirelet hasGetAllWorkUrl = falsewindow.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语法