最近接到一个项目中增加在线代码编辑器的功能需求,经过对比(其实算是半指定),选择了monaco-editor,这个库跟VSCode的源码高度重合,所以本身功能是比较强大的,包含自定义主题、自定义提示、设置代码对比、多文件编辑、代码信息提示等功能。但是目前项目中暂时不需要比较复杂的功能,主要功能需求点就是获取指定函数的函数体,添加代码注释及提示以及部分行禁用的功能,以下进行简单介绍以及遇到的踩坑点。
项目采用Vue + TS, 使用的monaco-editor及Vue的版本为:
"vue": "^3.4.19",
"monaco-editor": "0.30.1",
"typescript": "^5.3.3",
1. 安装
yarn 或者 npm 或者pnpm
2. 引入(使用前准备)
import * as monaco from 'monaco-editor';
集成到项目中的时候控制台报了一个错误,关于getWorker报错,在网上找的解决办法:
// 解决getWorker报错问题 Uncaught (in promise) Error: Unexpected usage at _EditorSimpleWorker.loadForeignModule
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
// @ts-ignore
self.MonacoEnvironment = {
getWorker(_, label) {
if (label === 'json') {
return new jsonWorker();
}
if (label === 'css' || label === 'scss' || label === 'less') {
return new cssWorker();
}
if (label === 'html' || label === 'handlebars' || label === 'razor') {
return new htmlWorker();
}
if (label === 'typescript' || label === 'javascript') {
return new tsWorker();
}
return new editorWorker();
},
};
3. 使用
1. 编辑器结构(可支持展示多个编辑器,进行分别输入、取值等)
// 如果仅需一个编辑器
<div id="container" class="app-monaco"></div>
// 如果项目中需要多个编辑器,可以传入对应的id(不可相同)
<div :id="containerId" class="app-monaco"></div>
// monaco 实例
const editor = ref<monaco.editor.IStandaloneCodeEditor | null>(null);
// 因为会存在一个页面上有两个monaco-editor实例的问题,所以动态展示id
const containerId = ref(props.containerTitle ?? 'container');
2. 配置项(可根据项目需要进行选择)
// monaco editor配置
const monacoEditorConfig = ref({
automaticLayout: true, // 自动布局,编辑器自适应大小
theme: 'vs-dark', // 官方自带三种主题vs, hc-black, or vs-dark
minimap: {
enabled: true, // 是否开启右侧代码小窗
},
folding: true, // 是否折叠
foldingHighlight: true, // 折叠等高线
foldingStrategy: 'auto', // 折叠方式
showFoldingControls: 'always', // 是否一直显示折叠
disableLayerHinting: true, // 等宽优化
emptySelectionClipboard: false, // 空选择剪切板
selectionClipboard: false, // 选择剪切板
codeLens: true, // 代码镜头
scrollBeyondLastLine: false, // 滚动完最后一行后再滚动一屏幕
colorDecorators: true, // 颜色装饰器
accessibilitySupport: 'on', // 辅助功能支持"auto" | "off" | "on"
selectOnLineNumbers: true, //显示行号
lineNumbers: 'on', // 行号 取值: "on" | "off" | "relative" | "interval" | function
lineNumbersMinChars: 4, // 行号最小字符 number
enableSplitViewResizing: false,
readOnly: false, //是否只读 取值 true | false
fontSize: 18,
cursorStyle: 'line', //光标样式
glyphMargin: true, //字形边缘
useTabStops: false,
autoIndent: true, //自动布局
quickSuggestionsDelay: 100, //代码提示延时
dropIntoEditor: {
enabled: false, // 能否把文字拖拽进编辑器
},
});
3. 创建编辑器
// 初始化manoca编辑器
const initEditor = () => {
const config = Object.assign({}, monacoEditorConfig.value, {
language: props.language,
value: textValue.value,
});
// 创建 monaco 实例
editor.value = monaco.editor.create(document.getElementById(containerId.value)!, config);
};
4. 编辑器取值事件(通过onDidChangeCursorPosition方法,通过onChange函数暴露出去)
// 编辑器改变的回调,用来取方法内的值
// 编辑器内容change事件
editor.value.onDidChangeModelContent((event: any) => {
// @ts-ignore
// 触发父组件的 change 事件,通知编辑器内容变化(直接获取编辑器内容)
// props.onChange?.(rawEditor.getValue());
// 只取方法内的内容
const content = getLineVal();
props.onChange?.(content);
}
5. 获取值(getValue方法)
editor.value.getValue();
6.设置值(setValue方法)
editor.value.setValue('123');
7. 获取指定范围的内容(主要是通过确定对应的行号及列号,getValueInRange方法)
const getLineVal = () => {
const model = toRaw(editor.value)?.getModel();
// 获取行数
const lineNum = model?.getLineCount();
let value;
if (lineNum) {
// 去掉注释以及函数头的行,再去掉最后返回值以及大括号的行
value = model?.getValueInRange({
startLineNumber: 11,
startColumn: 1,
endLineNumber: lineNum - 1,
endColumn: 1,
});
}
return value;
};
8. 如果父组件有禁用事件,动态修改编辑器是否可用(updateOptions方法)
// 监听disabled变化,设置禁用
watchEffect(() => {
if (props.disabled) {
toRaw(editor.value)?.updateOptions({ readOnly: true });
}
});
9. 编辑器重新布局(layout方法)
// 当容器尺寸发生变化的时候(例如:浏览器 resize),需要通过 layout 接口让 MonacoEditor 重新计算布局
// 编辑器 resize
const layout = () => {
if (editor.value) {
toRaw(editor.value)?.layout();
}
};
// 可以加个防抖
const debounedLayout = debounce(layout, 500);
// 重新计算尺寸
window.addEventListener('resize', debounedLayout);
10. 改变光标位置(setPosition方法)
const changePosition = () => {
toRaw(editor.value)?.setPosition({ lineNumber: 1, column: 1 });
toRaw(editor.value)?.focus();
};
11. 设置主题(setTheme方法)
// 编辑器设置主题 并非在实例上
const setEditorTheme = () => {
monaco.editor.setTheme('vs');
};
12. 获取选中的代码(getSelection方法)
const getSelectionVal = () => {
const selection: monaco.Selection | null | undefined = toRaw(editor.value)?.getSelection();
console.log('===selection', selection);
if (selection) {
const { startLineNumber, endLineNumber, startColumn, endColumn } = selection;
const model = toRaw(editor.value)?.getModel();
return model?.getValueInRange({
startLineNumber,
startColumn,
endLineNumber,
endColumn,
});
}
};
13. 向指定位置插入代码(executeEdits方法)
const insertCode = (code: string) => {
const rawEditor = toRaw(editor.value);
const model = rawEditor?.getModel();
// 获取行数
const lineNum = model?.getLineCount();
if (lineNum) {
// 计算范围,插入到第一行和最后一行中间
const range = {
startLineNumber: 2,
startColumn: 1,
endLineNumber: lineNum,
endColumn: 1,
};
// 增加空格
if (!code.startsWith(' ')) {
code = ' ' + code;
}
// 增加三行空格,以免返回的数据没有空格,不好看
// const textCode = [code, '', '', ''].join('\n');
rawEditor?.executeEdits('', [
{
range: range, // 要修改的范围
// text: textCode, // 要插入的内容
text: code, // 要插入的内容
forceMoveMarkers: true, // 是否强制移动光标
},
]);
}
};
14. 指定行设置禁用(就是改变光标的位置,只让它达到你的只读范围)
// 光标改变的回调,用来设置某些行不可用
editor.value.onDidChangeCursorPosition(function (e) {
// 获取行数
const lineNum = model?.getLineCount();
// 前10行不可编辑
if (e.position.lineNumber < 11) {
rawEditor?.setPosition({
lineNumber: 11,
column: 1,
});
}
// 后2行不可编辑
if (e.position.lineNumber > lineNum - 2) {
rawEditor?.setPosition({
lineNumber: lineNum - 2,
column: 1,
});
}
});
15. 销毁编辑器实例
const destroyEditor = () => {
if (editor.value == null) return;
// 销毁编辑器
toRaw(editor.value)?.dispose();
editor.value = null;
};
4. 遇到的大坑
1. 可以看到,我的代码中有时会使用toRaw这个方法,是因为editor的实例在创建时被代理了,导致本身实例上的很多方法没有了,所以将其转为纯粹的变量,有些方法才可以使用,如果没遇到此问题的话,可以直接editor.(相关方法);
2. position或者range相关的,包含开始和结束的位置或者范围信息,包含开始,但是不包含结束,比如startLine是1,endLine是10,getValueRange时就是前9行,这个使用时注意下;
暂时就发现这些问题,以及基本的方法使用,后续需求更新时再加,希望对各位有点帮助~