Bootstrap

Monaco Editor系列(三)修改内容、多文件编辑、自定义主题

前言:上一篇文章讲了如何在我们下载的源码项目中,增加一个自己的路由页面供我们自由发挥,下面的若干篇文章,我会和大家一起探索 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 指定的主题,如果想完全自定义,就填 false
  • rules:
    例:{ token: '', foreground: '000000'}
    控制代码高亮,常见的 tokenstring(字符串)、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主题

;