如何用Monaco Editor做一个公式输入框?
今日需求
做一个智能的输入框,让用户更方便地输入公式,我们会预定义一些常量,用户输入时会弹出自动补全的提示。
比如currentPrice - previousPrice
这类公式,其中的currentPrice
和previousPrice
就是我们预定义的变量。
这种输入框让我想到了Excel,在输入不同的东西时会跳出不同的提示:
或者像Jira里高级搜索那样:
先放上最终实现的效果,下面具体介绍方法:
实现
1. 选定工具库
我选用Monaco Editor,这是微软出的代码编辑器。由于这是个react
项目,我通过npm i @monaco-editor/react
安装了这个编辑器。
2. 画一个最简单的代码编辑器
用文档中的示例写一个最简单的代码编辑器:
// Page.tsx
import Editor from '@monaco-editor/react';
import Stack from '@mui/material/Stack';
import React, { useRef } from 'react';
const Formula = () => {
const editorRef = useRef(null);
function handleEditorDidMount(editor, monaco) {
editorRef.current = editor;
// 后面对monaco editor的设置,代码写在这里
}
function showValue() {
alert(editorRef.current.getValue());
}
return (
<Stack spacing={2}>
<div>
<h1>1. Use code editor</h1>
<button onClick={showValue}>Show value</button>
<Editor
height="300px"
width="500px"
defaultLanguage="javascript"
defaultValue="// some comment"
theme="vs-dark"
onMount={handleEditorDidMount}
/>
</div>
</Stack>
);
};
export default Formula;
页面上我可以看到这样的效果了:
3. 注册语言
我不想让用户在这个编辑器里随意输入代码,这会有潜在的安全隐患,由于我们的公式种类不多,我可以自己定义语言,把这个提示功能限制在预定义的值里。
对于Monaco Editor
来说,自定义语言很简单,只需一行代码:monaco.languages.register({ id: 'dqcPython' });
这里面的monaco
对象,我们在上一步的注释处已经可以拿到了。
4. 自定义语言
下面我们要给这个dqcPython
语言做定义。通常一门编程语言有各种关键字、函数、常量、变量、字符串、数字和其他特定语法的内容,代码编辑器会把这类词用不同颜色和样式显示他们,以便用户更清楚地阅读。
由于我这个需求仅用来输入公式,公式的底层是按python
语法来的,而用户不需要写一段完整的代码,这让我们定义语言的工作简化了不少,下面我们来为这个新语言下定义:
// keywords不需要定义诸如'class', 'private', 'public'这类,因为用户只能输入公式,而不能定义一个类
const keywords = ['lambda', 'for'];
// 公式里少不了运算符,这里定义得更全面,包括了比如'is not'这种语义化的操作符
const operators = ['+', '-', '*', '/', '=', '>', '<', '==', '!=', '>=', '<=', 'and', 'or', 'not', 'in', 'is', 'not in', 'is not'];
// 业务需求里有预定义的常量供用户调取
const constants = ['rflist', 'current_shock', 'previous_shock'];
// 用户可用的函数
const functions = ['len', 'find', 'count', 'match', 'all', 'any', 'set', 'abs', 'max', 'min', 'round', 'sum'];
这些定义不是必需的,如果你的语言和某个官方语言相似,只需要定义不同的地方。
5. Tokenize
上一步我们定义了一部分词法,这一步我们要让编辑器知道哪些是keywords
,哪些是operators
,这就叫tokenize
:
monaco.languages.setMonarchTokensProvider('dqcPython', {
keywords,
operators,
constants,
functions,
tokenizer: {
root: [
[/".*?"/, 'string'],
[/[{}()[\]]/, { cases: { '@operators': 'operator' } }],
[/\d*\.\d+([eE][-+]?\d+)?/, 'number.float'],
[/\d+/, 'number'],
[/[=<>!]+/, { cases: { '@operators': 'operator' } }],
[/[;,.]/, 'delimiter'],
[/\b(?:and|or|not|in|is|not in|is not)\b/, { cases: { '@operators': 'operator' } }],
[/\b(?:isnan|range|len|list|dict|find|count|match|all|any|set|abs|max|min|round|sum|re_search|containsAny|issubset|issuperset|get)\b/, { cases: { '@functions': 'function' } }],
[/\b(?:lambda|for)\b/, { cases: { '@keywords': 'keyword' } }],
[/\b(?:scenario|rflist|rfdict|shocks|ccy1|ccy2|yc|rftype|tenor|rfdict|shift_type|domicile|shift|product)\b/, { cases: { '@constants': 'constant' } }],
],
},
});
6. 自动补全提示
公式编辑器另一个非常重要的功能,就是能提供一串自动补全的提示,下面我们来定义这个提示:
monaco.languages.registerCompletionItemProvider('dqcPython', {
provideCompletionItems: (model, position) => {
const suggestions = [
...keywords.map(k => {
return {
label: k,
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: k,
};
}),
...operators.map(o => {
return {
label: o,
kind: monaco.languages.CompletionItemKind.Operator,
insertText: o,
};
}),
...functions.map(f => {
return {
label: f,
kind: monaco.languages.CompletionItemKind.Function,
insertText: f,
};
}),
...constants.map(c => {
return {
label: c,
kind: monaco.languages.CompletionItemKind.Constant,
insertText: c,
};
}),
];
return { suggestions };
},
});
这样,我们不仅能看到代码中不同的词有不同的样式:
我们在打字时还能看到自定义的提示:
7. 提示优化
我想让用户在输入了'+', '-', '*', '/'和括号这一类运算符后,能自动弹出常量的补全提示,可以加如下代码实现:
monaco.languages.registerCompletionItemProvider('dqcPython', {
// Run this function when one of below characters is typed (and anything after a space)
triggerCharacters: ['+', '-', '/', '>', '<', '=', '('],
provideCompletionItems: function (model, position, token) {
const suggestions = [
...constants.map(c => {
return {
label: c,
kind: monaco.languages.CompletionItemKind.Constant,
insertText: c,
};
}),
];
return { suggestions };
},
});
这样我们不仅在输入字母时会有提示,在输入运算符时也会有提示:
8. 样式优化
我们的公式输入框基本上就做好啦,另外编辑器中左边的行数,和右边预览用的minimap
这些功能不是我需要的,所以我把它们禁用掉了。最后我换成浅色主题,调整一下大小就完成了,这部分代码如下:
<Editor
height="60px"
width="500px"
defaultLanguage="dqcPython"
defaultValue="len(rflist) > 0"
theme="light"
options={{
lineNumbers: 'off',
glyphMargin: false,
folding: false,
minimap: {
enabled: false,
},
}}
onMount={handleEditorDidMount} />
静态效果(颜色高亮,没有行数,没有minimap):
当然如果你想自定义主题也能做到,可以参考我在文章最下面附上的文档。
引用
转载自:https://juejin.cn/post/7392527720816984100