热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

背景笔者开源了一个小项目code-run,类似codepen的一个工具,其中代码编辑器使用的是微软的MonacoEditor,这个库是直接从VSCode的源码中生成的,只不过是

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

背景

笔者开源了一个小项目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() {", "	console.log("Hello world!");", "}"].join("
"),
    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里看到如下效果时:

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 TokensVSCode里找到对应代码块的颜色,复制到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-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("
"),
        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("
"),
    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

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

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")
        );
    } catch (error) {
        console.log(error)
    }
}
run()

现在就可以直接转换.jsonc文件,而且输出统一为.json文件,另外内部会自动添加一个空的token作为没有匹配到的默认token,效果如下:

image.png

最佳实践

VSCode主题除了代码主题外,一般还包含编辑器其他部分的主题,比如标题栏、状态栏、侧边栏、按钮等等,所以我们也可以在页面应用这些样式,达到整个页面的主题也能随编辑器代码主题一起切换的效果,这样能让页面整体更加协调,具体的实现上,我们可以使用CSS变量,先把页面所有涉及到的颜色都定义成CSS变量,然后在切换主题时根据主题的colors选项里的指定字段来更新变量即可,具体使用哪个字段来对应页面的哪个部分可以根据实际情况来确定,VSCode主题的所有可配置项可以在theme-color这里找到。效果如下:

2021-09-27-10-46-47.gif

总结

本文完整详细的介绍了笔者对于Monaco Editor编辑器主题的探索,希望能给有主题定制需求的小伙伴们一点帮助,完整的代码请参考本项目源码:code-run。

参考链接

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

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

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

讨论:如何在Monaco Editor中使用VSC主题?

讨论:使用WebAssembly来支持TextMate语法

原文链接:https://www.cnblogs.com/wanglinmantan/archive/2021/09/27/15345204.html


推荐阅读
  • 如何优化Webpack打包后的代码分割
    本文介绍了如何通过优化Webpack的代码分割来减小打包后的文件大小。主要包括拆分业务逻辑代码和引入第三方包的代码、配置Webpack插件、异步代码的处理、代码分割重命名、配置vendors和cacheGroups等方面的内容。通过合理配置和优化,可以有效减小打包后的文件大小,提高应用的加载速度。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文讨论了在手机移动端如何使用HTML5和JavaScript实现视频上传并压缩视频质量,或者降低手机摄像头拍摄质量的问题。作者指出HTML5和JavaScript无法直接压缩视频,只能通过将视频传送到服务器端由后端进行压缩。对于控制相机拍摄质量,只有使用JAVA编写Android客户端才能实现压缩。此外,作者还解释了在交作业时使用zip格式压缩包导致CSS文件和图片音乐丢失的原因,并提供了解决方法。最后,作者还介绍了一个用于处理图片的类,可以实现图片剪裁处理和生成缩略图的功能。 ... [详细]
  • vue使用
    关键词: ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 本文介绍了如何使用Express App提供静态文件,同时提到了一些不需要使用的文件,如package.json和/.ssh/known_hosts,并解释了为什么app.get('*')无法捕获所有请求以及为什么app.use(express.static(__dirname))可能会提供不需要的文件。 ... [详细]
  • VueCLI多页分目录打包的步骤记录
    本文介绍了使用VueCLI进行多页分目录打包的步骤,包括页面目录结构、安装依赖、获取Vue CLI需要的多页对象等内容。同时还提供了自定义不同模块页面标题的方法。 ... [详细]
  • 超级简单加解密工具的方案和功能
    本文介绍了一个超级简单的加解密工具的方案和功能。该工具可以读取文件头,并根据特定长度进行加密,加密后将加密部分写入源文件。同时,该工具也支持解密操作。加密和解密过程是可逆的。本文还提到了一些相关的功能和使用方法,并给出了Python代码示例。 ... [详细]
  • java drools5_Java Drools5.1 规则流基础【示例】(中)
    五、规则文件及规则流EduInfoRule.drl:packagemyrules;importsample.Employ;ruleBachelorruleflow-group ... [详细]
  • 本文总结和分析了JDK核心源码(2)中lang包下的基础知识,包括常用的对象类型包和异常类型包。在对象类型包中,介绍了Object类、String类、StringBuilder类、StringBuffer类和基本元素的包装类。在异常类型包中,介绍了Throwable类、Error类型和Exception类型。这些基础知识对于理解和使用JDK核心源码具有重要意义。 ... [详细]
  • 从批量eml文件中提取附件的Python代码实现方法
    本文介绍了使用Python代码从批量eml文件中提取附件的实现方法,包括获取eml附件信息、递归文件夹下所有文件、创建目的文件夹等步骤。通过该方法可以方便地提取eml文件中的附件,并保存到指定的文件夹中。 ... [详细]
author-avatar
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有