前言:上一篇文章讲了如何在我们下载的源码项目中,增加一个自己的路由页面供我们自由发挥,下面的若干篇文章,我会和大家一起探索 Monaco Editor 的 API,探索 Monaco Editor 的功能。Monaco Editor 的官方文档特别的简便,这也是学习来困难的原因之一。但是学习它一定是非常有价值的。接下来继续本篇文章的探索吧!一起加油吧!
一、获取、修改、监听内容
书接上回,上文中我们使用 monaco.editor.create()
方法,创建出来一个 Monaco Editor 实例,传的参数第一个是编辑器的容器,第二个是初始内容。那么,如果我们后续想修改编辑器里面的内容,该怎么修改呢?monaco.editor.create()
方法创建出来的东西,Monaco 把它叫做 model,model 可以暂且理解为是一种语言模型,其实就是一个对象。
要获取这个 model,需要使用 editor.getModel()
方法,还是在上文中创建的 website/static/study/study.js 中继续我们的旅程
require(['vs/editor/editor.main'], function () {
var editor = monaco.editor.create(document.getElementById('container'), {
value: ['function x() {', '\tconsole.log("Hello world!");', '}'].join('\n'),
language: 'javascript'
});
console.log("editor.getModel()")
console.log(editor.getModel())
});
我们可以打印瞅一下返回值长啥样,返回值的类型是 ITextModel,关于这个接口的定义,我们可以在代码中直接搜索一下,也可以在本地的项目中的 document 路由页面中看到
${自己项目的根路径}/docs.html#interfaces/editor.ITextModel.html
这个页面里有所有 model 上的方法,但是解释的特别简约
node_modules/monaco-editor-core/monaco.d.ts 文件里面有 ITextModel 的类型定义,其中的 getValue()
方法,是获取编辑器文本内容,setValue()
方法,是设置编辑器文本内容
const model = editor.getModel();
console.log("model.getValue()")
console.log(model.getValue())
下面我们执行 setValue()
修改一下编辑器的内容
const model = editor.getModel();
model.setValue('console.log(90909)')
可以看到编辑区域的代码就被修改了
model 上还提供了监听器,让我们可以监听编辑区域内容的修改
${项目根路径}/docs.html#interfaces/editor.ITextModel.html#onDidChangeContent
model.onDidChangeContent(e=>{
console.log(e)
})
其中回调函数接收的参数的类型是 IModelContentChangedEvent
在 node_modules/monaco-editor-core/monaco.d.ts 文件中有这个接口的定义。有的我也看不太懂嘤嘤嘤,只能尽量猜测了。没有关系,后续慢慢学,用到的话就会看懂啦
/**
* 描述在model中的文本修改事件.
*/
export interface IModelContentChangedEvent {
readonly changes: IModelContentChange[];
/**
* 新的行的行尾符 默认是 换行符.
*/
readonly eol: string;
/**
* model的新的版本id 每次都会修改.
*/
readonly versionId: number;
/**
* 标识是否是撤销事件.
*/
readonly isUndoing: boolean;
/**
* 是否是redo事件.
*/
readonly isRedoing: boolean;
/**
* 是否丢失样式.
* model 会重置.
*/
readonly isFlush: boolean;
/**
* 行尾符是否修改.
*/
readonly isEolChange: boolean;
}
我们看一下其中的 changes
属性
export interface IModelContentChange {
/**
* 修改的范围.
*/
readonly range: IRange;
/**
* 修改的位置.
*/
readonly rangeOffset: number;
/**
* 修改的长度.
*/
readonly rangeLength: number;
/**
* 修改的文本.
*/
readonly text: string;
}
有了这些个属性,我们想在代码修改的时候做点事情就太简单啦,比如说在修改的时候蹦出来的代码提示,就需要使用到这个监听器
二、多文件编辑、tab切换
在我们编码的过程中,通常都是要同时打开好几个文件,这一章,我们来探索一下如何实现多文件同时编辑和tab切换
每一个文件的编辑都需要一个 model 来维护,所以每一个文件都需要创建自己的 model,并且在切换到其他文件的时候,需要保存当前文件的编辑状态,也就是说,当前我们选中一段文本,并且光标在下图所示位置,切换tab到其他文件,然后切换回来,选中的文本以及光标的位置需要和切换前的状态保持一致
分析一下这个功能的需求点:
1、编辑区域上方有tab可以切换,点击tab切换编辑区域的内容
2、切换时会保存 model 的状态,切换回来会恢复 model 的状态
3、几个文件的语言可能不一样,所以需要修改 model 的语言
关于文件状态:
${项目根地址}/docs.html#interfaces/editor.ICodeEditorViewState.html
在上面这个接口里定义,包含滚动条、选中的范围、焦点位置等等由于操作形成的状态。通过 editor.saveViewState()
方法,可以获取当前的 model 的状态,通过 editor.restoreViewState(state)
方法,可以设置 model 的状态
这里我们就不使用 fs 去读文件了,专注于咱们的 monaco 功能的学习,后续写完整的 demo 的时候再完善这些边边角角
(一)创建 tabs 和 files
website/static/study-static.html
<p>
<button class="tab-btn">index.html</button>
<button class="tab-btn">index.css</button>
<button class="tab-btn">index.js</button>
</p>
website/static/study/study.js
const files = new Map();
files.set('index.html', {
content: `<h1>我是file1</h1>`,
language: 'html',
state: null,
model: null,
})
files.set('index.js', {
content: `console.log('我是 index.js')`,
language: 'javascript',
state: null,
model: null,
})
files.set('index.css', {
content: `h1 {color: #00aff4;}`,
language: 'css',
state: null,
model: null,
})
(二)点击tab修改 model
点击按钮时,可以获取点击的元素的 innerText,以 innerText 为 key,找 map 里面存储的文件对象
通过 editor.saveViewState()
获取当前的状态,然后找到切换之前的文件,把状态存到文件里面
然后看当前文件有没有model,如果没有的话,就需要创建新的 model,如果有的话,editor 直接应用它的 model 即可,同时,需要通过 editor.restoreViewState
方法,恢复目标文件的state
最后,需要通过 editor.focus()
方法,恢复焦点的位置
$('.tab-btn').click(e => {
// 根据按钮的innerText获取对应的file
const fileName = e.target.innerText;
const file = files.get(fileName);
// 获取当前文件的编辑状态
const currentState = editor.saveViewState();
const currentModel = editor.getModel();
// 将状态存到map里面
files.forEach((value, key) => {
if (value.model == currentModel) {
value.state = currentState;
}
})
if (file.model) {
editor.setModel(file.model);
editor.restoreViewState(file.state); // 恢复文件的编辑状态
} else {
const newModel = monaco.editor.createModel(file.content, file.language);
editor.setModel(newModel);
file.model = newModel;
}
editor.focus();
})
完整代码
website/static/study-static.html
<!DOCTYPE html>
<html>
<head>
<title>Hello World Monaco Editor</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
</head>
<body>
<p>
<button class="tab-btn">index.html</button>
<button class="tab-btn">index.css</button>
<button class="tab-btn">index.js</button>
</p>
<div id="container" style="width: 800px; height: 600px; border: 1px solid grey">
</div>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.9.1/jquery.min.js"
integrity="sha256-wS9gmOZBqsqWxgIVgA8Y9WcQOa7PgSIX+rPA0VL2rbQ="
crossorigin="anonymous"
></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.0/bootstrap.min.js"
integrity="sha256-u+l2mGjpmGK/mFgUncmMcFKdMijvV+J3odlDJZSNUu8="
crossorigin="anonymous"
></script>
<script>
var require = {
paths: { vs: "./node_modules/monaco-editor/dev/vs" },
};
</script>
<script src="./node_modules/monaco-editor/dev/vs/loader.js"></script>
<script src="./node_modules/monaco-editor/dev/vs/editor/editor.main.nls.js"></script>
<script src="./node_modules/monaco-editor/dev/vs/editor/editor.main.js"></script>
<script data-inline="yes-please" src="./study/study.js"></script>
</body>
</html>
website/static/study/study.js
require(['vs/editor/editor.main'], function () {
var editor = monaco.editor.create(document.getElementById('container'));
// 多文件编辑
const files = new Map();
files.set('index.html', {
content: `<h1>我是file1</h1>`,
language: 'html',
state: null,
model: null,
})
files.set('index.js', {
content: `console.log('我是 index.js')`,
language: 'javascript',
state: null,
model: null,
})
files.set('index.css', {
content: `h1 {color: #00aff4;}`,
language: 'css',
state: null,
model: null,
})
$('.tab-btn').click(e => {
// 根据按钮的innerText获取对应的file
const fileName = e.target.innerText;
const file = files.get(fileName);
// 获取当前文件的编辑状态
const currentState = editor.saveViewState();
const currentModel = editor.getModel();
// 将状态存到map里面
files.forEach((value, key) => {
if (value.model == currentModel) {
value.state = currentState;
}
})
if (file.model) {
editor.setModel(file.model);
editor.restoreViewState(file.state); // 恢复文件的编辑状态
} else {
const newModel = monaco.editor.createModel(file.content, file.language);
editor.setModel(newModel);
file.model = newModel;
}
editor.focus();
})
});
最终效果:
三个文件可以成功切换自如啦!并且各自都可以保持编辑的状态
扩:如果不修改 model,只修改语言,monaco 也提供了对应的方法 editor.setModelLanguage(model, language)
三、切换主题
身为一名有个性的程序员,与我们朝夕相处的IDE工具一定要高颜值并且与众不同!在 Monaco Editor 中背景色、高亮色等等等等,都是可以自定义的!
(一)设置主题
关键方法就是 monaco.editor.setTheme(themeName)
方法,这个方法的作用是设置编辑器的主题。Monaco 默认提供的主题有四个,下面我们一起来看一下效果:
- vs 白底主题 也就是默认的主题
- vs-dark 黑底主题
- hc-black 高对比度黑底主题
- hc-light 高对比度白底主题
这几个主题的定义在 node_modules/monaco-editor-core/monaco.d.ts 文件里面
(二)自定义主题
自定义主题方法是 monaco.editor.defineTheme(主题名, 配置对象)
主题名 setTheme()
的时候作为参数传递进去,配置对象的介绍在这里
${项目根路径}/docs.html#interfaces/editor.IStandaloneThemeData.html
项目中的定义在 node_modules/monaco-editor-core/monaco.d.ts 里
export interface IStandaloneThemeData {
base: BuiltinTheme;
inherit: boolean;
rules: ITokenThemeRule[];
encodedTokensColors?: string[];
colors: IColors;
}
base
: 以哪个主题为基础,也就是上面提到的默认提供的四个主题inherit
: 是否继承base
指定的主题,如果想完全自定义,就填 falserules
:
例:{ token: '', foreground: '000000'}
控制代码高亮,常见的token
有string
(字符串)、comment
(注释)、keyword
(关键词)等等。我们
可以先参考一下vscode中关于主题的定义,看一下 vscode 中的tokens
是怎么定义的:theme.ts
Monaco 中对于 tokens 的定义比较抽象,其实对于不同的语言而言,关键词都是不一样的,关于tokens的定义其实是在 src/basic-languages/*/*.ts 文件里面,针对不同的语言做了不同的定义。在我们的本地项目的网页上,也可以可视化的清晰地看到 token 的定义哦!就是在 ${根路径}/monarch.html 路由页面
咱们来看看 javascript 语言的定义,下面的代码并没有列出全部代码哦,只以其中的一部分为例,理解一下这里的定义,其他的也跟我给出的例子类似
return {
// 默认的token
defaultToken: 'invalid',
// 文件后缀为 .js
tokenPostfix: '.js',
// 定义变量 keywords
keywords: [
'break', 'case', 'catch', 'class', 'continue', 'const',...
],
// 定义变量 typeKeywords
typeKeywords: [
'any', 'boolean', 'number', 'object', 'string', 'undefined'
],
// 定义语言的 token
tokenizer: {
common: [
// 匹配正则表达式 /[a-z_$][\w$]*/ ,
// 匹配以小写字母、下划线或美元符号开头,后跟零个或多个小写字母、大写字母、数字、下划线或美元符号的标识符名称
// 如果字符串在 typeKeywords 或者keywords 变量里面出现了,说明字符串的 token 是 keyword;默认是 identifier
[/[a-z_$][\w$]*/, {
cases: {
'@typeKeywords': 'keyword',
'@keywords': 'keyword',
'@default': 'identifier'
}
}],
],
},
};
- encodedTokensColors:来定义主题中的编码 token 的颜色。编码 token 是指一些特殊的 token,它们的值是一些 Unicode 字符,例如 emoji 表情等。
- colors: 各种颜色定义
妈呀妈呀,黄天不负有心人,千辛万苦在找资料的过程中找到一个学习网站!!Monaco Editor学习网站,但是但是但是,可能是由于版本的问题,encodedTokensColors
按照这个网站给出的案例写的话就会报错。。。但是这个属性我费老鼻子劲也没找到别的解释,一定是不重要。
颜色有很多很多,具体可以看 colors
接下来给出我定义的主题示例,虽然只写了小小一部分
function defineTheme() {
monaco.editor.defineTheme('naruto', {
base: 'vs', // 以哪个默认主题为基础:"vs" | "vs-dark" | "hc-black" | "hc-light"
inherit: true,
rules: [ // 高亮规则,即给代码里不同token类型的代码设置不同的显示样式
{ token: 'identifier', foreground: '#d06733'},
{ token: 'number', foreground: '#6bbeeb', fontStyle:'italic'},
{ token: 'keyword', foreground: '#05a4d5' },
],
colors: {
'scrollbarSlider.background': '#edcaa6', // 滚动条背景
'editor.foreground': '#0d0b09', // 基础字体颜色
'editor.background': '#ffffff', // 背景颜色
'editorCursor.foreground': '#d4b886', // 焦点颜色
'editor.lineHighlightBackground': '#6492a520', // 焦点所在的一行的背景颜色
'editorLineNumber.foreground': '#008800', // 行号字体颜色
}
})
}
defineTheme()
monaco.editor.setTheme('naruto');
清爽色系
参考文章:
🌟 手把手教你实现在Monaco Editor中使用VSCode主题