爱看书的核桃 · javascript ...· 5 月前 · |
玩足球的筷子 · el-popover ...· 1 年前 · |
苦恼的登山鞋 · Java Review(三十四、JDBC) ...· 1 年前 · |
大气的鼠标 · 解决Gradle错误:未找到ID为'com. ...· 1 年前 · |
导读: 在阅读本文时,希望读者对于 编译原理 有一些基本的了解,以便理解本文提到的相关专业术语,从而更好的理解文章表达的思想。
文|亦心
网易云商
DSL 是什么?
DSL 即领域专用语言(domain specific language / DSL),其基本思想是“求专不求全”,不像通用编程语言那样目标范围涵盖一切软件问题,而是专门针对某一特定问题的计算机语言,非图灵完备的。
DSL 领域专用语言: SQL、JSON、正则表达式、TSX、VUE 等, 非图灵完备 。
GPL 通用编程语言: Java、JavaScript、C++、Python 等, 图灵完备 。
简单的理解,DSL 是一门便于人们理解的编程语言或规范语言,并且可以被计算机解释执行。相比于通用编程语言,只能表达有限的逻辑。因为受限的表达性,所以只会在某一些领域广泛应用。
DSL 在编程领域举例
举例: 定义一个时间,表示2周之前。
编程方式: 以 JavaScript 为例。
new Date(Date.now() - 1000 * 60 * 60 * 24 * 2)
上述代码对于开发人员来说还算简单,但是具体表示什么时间,也需要花点时间去分析代码,但是对于非开发人员,想要理解这段代码表示的含义就显的有些困难了。
所以,我们是不是可以用一种更加简洁的方式来表示两周之前呢?
// 为方便演示,本例中week以单数形式出现
2 week ago
上述编写方式,相比前面一种,变得简洁易懂,可以问问身边 懂点英语的 非程序员。
在上述举例中,我们已经有了 DSL 领域语言的设计方向, 2 week ago 其实就是一种针对时间表示的领域特定语言。
设计一门表示时间的 DSL
上面 2 周之前 的 DSL 表示为 2 week ago ,对于时间,我们不能局限于只表达 2 周之前,可能还有以下场景:
1 小时后
3 天之前
1 个月后
10 年前
... ...
所以我们需要对表达式进行抽象,在这个简单的场景中,我们可以将语言抽象成如下表达式:
count dateType actionType
count
:表示时间单位的数量。
dateType
:表示时间单位。
actionType
:表示
时间表达类型是之前 ago,还是之后 after。
据此,我们很快就能写出一个正则表达式,用来提取上述三个变量的值。
我们再写一个函数用来转换时间,简单起见,这里的月按照 30 天计算,年按照 365 天计算。
const getTime = (count, dateType, actionType) {
const timeUnitMap = {
second: 1000,
minute: 1000 * 60,
hour: 1000 * 60 * 60,
day: 1000 * 60 * 60 * 24,
week: 1000 * 60 * 60 * 24 * 7,
month: 1000 * 60 * 60 * 24 * 30,
year: 1000 * 60 * 60 * 24 * 365,
}
const timeUnit = timeUnitMap[dateType]
if (actionType === 'ago') {
return new Date(Date.now() - timeUnit * count)
}
if (actionType === 'after') {
return new Date(Date.now() + timeUnit * count)
}
return null
}
如上,我们就能通过简单的 DSL 语法来表达时间:
1 小时后:1 hour after
3 天前:3 day ago
1 月后:1 month after
10 年前:10 year ago
本例中,我们实现了一门简单的 DSL 语言用以表示时间。在实际场景中,我们设计的 DSL 通常更加复杂,所以单纯的使用正则表达式来定义和解析 DSL 文法会变得异常困难,所以我们需要一个工具来高效的定义、解析、转换 DSL,本文中,我们选取了
PEG.js
来实现这些功能。
PEG.js 介绍
PEG.js
是一个简单的 JavaScript 解析器生成器,可生成具有出色错误报告的快速解析器。您可以使用它来处理复杂的数据或计算机语言,并轻松构建转换器、解释器、编译器和其他工具。
我们通过 PEG.js 来实现上述时间表达的文法,如下:
// 定义时间表达式
Expression
= count:Integer _ dateType:DateType _ actionType:ActionType {
return [count, dateType, actionType]
}
// 定义时间单位
DateType = 'second' / 'minute' / 'hour' / 'day' / 'week' / 'month' / 'year' {
return text()
}
// 定义时间类型
ActionType = 'ago' / 'after' {
return text()
}
// 整数类型
Integer "integer"
= _ [0-9]+ { return parseInt(text(), 10); }
// 空格
_ "whitespace"
= [ \t\n\r]*
代码使用,如下:
import grammarConfig from './dsl.pegjs';
const parser = peg.generate(grammarConfig);
parser.parse("2 week ago"); // returns [2, "week", "ago"]
可以看到,通过 PEG.js 定义文法,可以拆分表达式,并通过 巴科斯范式去定义 DSL 语言的文法。这种方式让我们不必拘泥于原来复杂臃肿的正则表达式,通过这种方式,我们可以定义更加复杂的 DSL 语言。
DSL 在网易云商中的实践
在网易云商的业务中,有一项很重要的业务是 问卷调研 ,通常问卷的设计者会设置一些问卷逻辑来满足数据收集的需求,最初这些逻辑都是通过 GUI 界面操作实现的,如下:
然而随着业务的发展,越来越多的问题开始暴露。
GUI 逻辑设置存在的问题:
配置操作繁琐,相对复杂的问卷,可能用户需要操作几百上千次界面配置。
复杂的自定义逻辑无法通过 GUI 配置满足。
配置分散在各个题型当中,逻辑查阅不方便。
由于 GUI 无法实现复杂逻辑的问题,所以我们通过源代码编程的方式,实现了个别客户的定制化需求,但也面临着一些问题。
源代码自定义编程:
相对于 GUI 逻辑配置,这种方式理论上能满足客户的 任何需求 。
一般运营人员无法操作,需要开发人员投入,实施成本较高。
对外开放这种底层能力存在一定的技术风险(代码安全、兼容性)等。
所以我们迫切的需要一种方式,来满足日益增长的业务需求,基本诉求如下:
提供类似于源代码编程能力,覆盖
90%
以上的业务场景。
使用简单,普通的运营人员也能操作。
保证系统安全稳定运行。
综上,我们设计了一门用来配置问卷特定领域的 DSL 语言,来实现编程能力下沉,在 DSL 开发过程中,主要有以下内容:
DSL 文法设计。
DSL 编辑器设计。
内部 DSL 语言转 AST 设计。
问卷系统解析执行 AST。
下面我会用一些简单的例子来讲解 DSL 在问卷业务中的落地场景。
// 当题目Q1选中选项A1时,隐藏题目Q2
if Q1A1 then hide Q2
// 当题目Q3选中选项A1时,显示题目Q4
if Q3A1 then show Q4
文法设计
首先对上述场景进行文法抽象分析
if 表达式: if condition then action ,对应例子的整个表达式。
条件表达式:if condition then action,对应例子中的 Q1A1 、 Q3A1 。
动作表达式:if condition then action ,对应例子中 hide Q2 、 show Q4 。
选项表达式: Q1A1 、 Q3A1 。
问题表达式: Q2 、 Q4 。
定义基础表达式
// 数字
Number
= [0-9]+ {
return Number(text())
}
// 字符
Chars
= [a-zA-Z0-9]+ {
return text()
}
// 空格
WhiteSpace "whitespace"
= "\t"
/ "\v"
/ "\f"
/ " "
/ "\u00A0"
/ "\uFEFF"
/ Zs
Zs = [\u0020\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]
// 注释
Comment = SingleLineComment
SourceCharacter
= .
SingleLineComment
= "#" (!LineTerminator SourceCharacter)*
LineTerminator
= [\n\r\u2028\u2029]
// 换行符
LineTerminatorSequence "end of line"
= "\n"
/ "\r\n"
/ "\r"
/ "\u2028"
/ "\u2029"
// 注释 || 空格 || 换行符
__
= (WhiteSpace / LineTerminatorSequence / Comment)*
定义问题和选项的表达式
QuestionPrefix
= 'Q'
Question
= QuestionPrefix index:Number
OptionPrefix
= q:Question 'A'
Option
= q:OptionPrefix index:Number
这样配置我们的 pegjs 就可以解析 Q1A1 、 Q2 这种语法了。
定义条件表达式
Test = Option / Question
定义操作表达式
IfConsequent
= ShowCallExpression
/ HideCallExpression
HideCallExpression
= 'hide' __ arg:HideArguments
HideArguments
= first:HideItemPrimary others:(__ ',' __ HideItemPrimary)*
HideItemPrimary
= Options
/ Questions
ShowCallExpression
= 'show' __ arg:HideArguments
这样我们的文法就能够识别 hide Q2 、 show Q1 这种语法了。
定义 if 表达式
IfStatement
= 'if' __ test:Test __ 'then' __ consequent:IfConsequent
完整的文法表达式
{
function extractList(list, index) {
return list.map(function(element) { return element[index]; });
}
function buildList(head, tail, index) {
return [head].concat(extractList(tail, index));
}
function optionalList(value) {
return value !== null ? value : [];
}
function buildOption(info) {
const { qNumber, sNumber, oNumber } = info
const variableInfo = {
type: 'Option',
qNumber,
sNumber,
oNumber,
}
return variableInfo
}
function buildQuestion(info) {
const { qNumber } = info
const variableInfo = {
type: 'Question',
qNumber,
}
return variableInfo
}
function buildRangeExpression(startIndex, endIndex, builder) {
const list = []
for (let i = endIndex; i >= startIndex; i--) {
const item = builder(i)
list.unshift(item)
}
return list
}
}
Start
= __ program:Program __ { return program; }
Program
= div:SourceElements? {
return {
type: "Program",
div: optionalList(div)
};
}
SourceElements
= head:SourceElement tail:(__ SourceElement)* {
return buildList(head, tail, 1);
}
SourceElement
= IfStatement
/ ShowCallExpression
/ HideCallExpression
IfStatement
= 'if' __ test:Test __ 'then' __ consequent:IfConsequent {
return {
type: "IfStatement",
test,
consequent,
}
}
IfConsequent
= ShowCallExpression
/ HideCallExpression
Test
= Option / Question
// ==== action start ====
HideCallExpression
= Hide __ arg:HideArguments {
return {
type: 'CallExpression',
callee: 'hide',
arguments: arg,
}
}
Hide
= 'hide'
HideArguments
= first:HideItemPrimary others:(__ ',' __ HideItemPrimary)* {
const list = [...first]
others.forEach(o => {
list.push(...o[3])
})
return list
}
HideItemPrimary
= Options
/ Questions
ShowCallExpression
= Show __ arg:HideArguments {
return {
type: 'CallExpression',
callee: 'show',
arguments: arg,
}
}
Show
= 'show'
// ==== action end ====
// 问题、子问题、选项相关
RangeOperator
= '~'
RangeExpression
= start:Number RangeOperator end:Number {
if (start >= end) {
error('范围不正确')
}
return {
start,
end,
}
}
Options
=
q:OptionPrefix range:RangeExpression {
return buildRangeExpression(range.start, range.end, (index) => {
return buildOption({
qNumber: q.qNumber,
sNumber: q.sNumber,
oNumber: index,
})
})
}
/ o:Option {
return [o]
}
Questions
= QuestionPrefix range:RangeExpression {
return buildRangeExpression(range.start, range.end, (index) => {
return buildQuestion({
qNumber: index,
})
})
}
/ q:Question {
return [q]
}
QuestionPrefix
= 'Q'
Question
= QuestionPrefix index:Number {
return buildQuestion({
qNumber: index
})
}
OptionPrefix
= q:Question 'A' {
return q
}
Option
= q:OptionPrefix index:Number {
return buildOption({
qNumber: q.qNumber,
sNumber: q.sNumber,
oNumber: index,
})
}
// 基础字符 & 注释
Number
= [0-9]+ {
return Number(text())
}
Chars
= [a-zA-Z0-9]+ {
return text()
}
WhiteSpace "whitespace"
= "\t"
/ "\v"
/ "\f"
/ " "
/ "\u00A0"
/ "\uFEFF"
/ Zs
Zs = [\u0020\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]
Comment = SingleLineComment
SourceCharacter
= .
SingleLineComment
= "#" (!LineTerminator SourceCharacter)*
LineTerminator
= [\n\r\u2028\u2029]
LineTerminatorSequence "end of line"
= "\n"
/ "\r\n"
/ "\r"
/ "\u2028"
/ "\u2029"
__
= (WhiteSpace / LineTerminatorSequence / Comment)*
可以将上述文法复制到 http://pegjs.org/online 中体验,当我们输入:
if Q1A1 then hide Q2
可以得到下面的抽象语法树(AST):
{
"type": "Program",
"div": [
{
"type": "IfStatement",
"test": {
"type": "Option",
"qNumber": 1,
"sNumber": undefined,
"oNumber": 1
},
"consequent": {
"type": "CallExpression",
"callee": "hide",
"arguments": [
{
"type": "Question",
"qNumber": 2
}
]
}
}
]
}
文法设计中的注意点
由于问卷题目配置是实时更新的,包括题目、选项的
增加
、
删除
、
移动
都会导致索引发生变化,所以我们将 DSL 自定义编程代码存储到后端的时候,会将对应的
题目/选项
的索引替换成
ID 编号
,也就是
内部 DSL 语言
。
例如:
Q1 => `Q_q-4337494791297303`
Q1A1 => `Q_q-4337494791297303::A_a-8179104888736048`
当然这一过程也是通过 PEG.js 实现的,当自定义 DSL 代码回显到编辑器时,也会根据当前问卷的实时数据反解析成索引序列,也就是 外部 DSL 语言 。
例如:
`Q_q-4337494791297303` => Q1
`Q_q-4337494791297303::A_a-8179104888736048` => Q1A1
因此,问卷中 问题、选项 的索引顺序变动并不会影响到 DSL 自定义编程逻辑的执行。
当然上面的例子为了呈现实现原理逻辑比较简单,实际产品中支持许多复杂的逻辑配置,例如:
# 如果Q1的选项个数和Q2的选项个数加起来等于5,则不展示Q3
if (len Q1 + len Q2) == 5 then hide Q3
# 如果Q1选中A1 或 Q2选中A1,那么Q3自动选中A1
if Q1A1 or Q2A1 then select Q3A1
# 如果Q1没有选中A1,那么Q3的A1选项禁止勾选
if not Q1A1 then disable Q3A1
更多复杂的例子可以点击【阅读原文】参考我们的指导文档。
编辑器设计
由于问卷的编程端环境是运行在浏览器的,所以我们需要一款能在浏览器端运行的代码编辑器,通过调研,我们选出了3个比较合适的网页编辑器, Monaco Editor 、 CodeMirror 、 ACE ,并进行了简单比较:
功能点 | Monaco Editor | CodeMirror | ACE |
代码高亮
|
✓ | ✓ | ✓ |
主题
|
✓(VS/VS dark)
|
✓(内置40+) | ✓(内置20+) |
语言支持
|
✓ | ✓ | ✓ |
语言扩展
|
✓ | ✓ | ✓ |
代码提示/自动补全
|
✓ | ✓ | ✓ |
多光标操作
|
✓ | ✓ | ✓ |
自动缩进
|
✓ | ✓ | ✓ |
代码拆行
|
✓ | ✓ | ✓ |
撤销/恢复
|
✓ | ✓ | ✓ |
代码检查
|
✓ | ✓ | ✓ |
diff
|
✓ | ✓ |
-
|
行数显示
|
✓ | ✓ | ✓ |
三者从功能上对比差异不大,都能 cover 到本次编辑器设计中涉及到的所有需求点。
综合评估,团队最终选择 Monaco Editor 作为技术选型,主要考虑的以下几点:
优秀的性能表现。
Monaco Editor 是 VS Code 的底层实现。
插件开发与 VS Code 类似,团队有相关开发经验。
能覆盖已知的所有编码场景,例如代码高亮、代码补全提示、代码 hover 提示、代码错误诊断等。
基于业务场景,确定编辑器由两部分组成 language editor + language service。
language editor :运行在主线程,初始化一些自定义语言的配置,主要负责 ui 层的交互、渲染。
language service :运行在 worker 线程,负责逻辑相关的计算。
这种渲染层与逻辑层分离的结构设计,能够保证编辑器的用户操作不被打断,从而有更好的使用体验。
编辑器配置
1. 指定 worker 线程代码资源地址
由于生产环境和开发环境的静态资源地址路径不同,在 webpack 打包时注入了 webpack_public_path ,拼接出正确的资源地址。
window.MonacoEnvironment = {
getWorkerUrl: function (moduleId, label) {
const basePath = process.env.__webpack_public_path__ || '';
if (label === languageID) {
return `${basePath}dwLang.worker.js`;
}
return `${basePath}editor.worker.js`;
},
};
2. 注册语言
monaco.languages.register :注册自定义语言的 ID,后续初始化编辑器、注册主题等都会用到该 ID。
languages.register({
id: languageID
});
3. 语言配置
monaco.languages.setLanguageConfiguration: 可以进行语言缩进、折叠等功能的配置。
languages.setLanguageConfiguration(languageID, {
wordPattern: /(-?\d*\.\d\w*)|([^\`\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
})
其他功能不需要,这里我们只配置 wordPattern。
提示:
补全、hover 等功能中,我们都需要通过编辑器的 model 对象提供的 getWordAtPosition 方法获取对应位置的 word,但是这个 word 的分词是受 wordPattern 影响的。
默认的 wordPattern 遇到特殊的字符,如“~”等就会进行分词,而我们的语法中会用到“~”符号。所以在这里配置了 wordPattern,将“~”符号去除。
4. 分词配置
monaco.languages.setMonarchTokensProvider: 关键词、函数等分词配置。
我们在 tokenizer 中定义了一个 root 属性,root 是 tokenizer 中的一个 state,我们在这里编写解析规则。
每一条规则中,我们可以编写匹配文本的正则表达式,然后再给匹配到的文本设置一个 token 类型 ,我们可以根据这个 token 类型给其添加相应的配色。
monaco.languages.sss(languageID, {
// 定义默认 token,当其他 token 规则匹配失败时,就定义为 invalid
defaultToken: 'invalid',
// The main tokenizer for our languages
tokenizer: {
root: [
// token 解析规则
[/#(.*)?/, 'noUseSign'],
[/\s*(Q[0-9]*(S[0-9]*)?(A[0-9]*)?(~[0-9]+)?)\s*/, 'Question'],
[/\s*(if|then)\s*/, 'IfStatement'],
[/\s*(show|hide)\s*/, 'action'],
[/\s*(> |== |>= |<= |<)\s*/, 'compareSign'],
[/\s*(and|or|not|\(|\))\s*/, 'linkSign'],
[/\s*([0-9]\d*)\s*/, 'Number'],
[/\s*(,)\s*/, 'Gap'],
],
},
keywords: [
'if',
'then',
'and',
'or',
'not',
'show',
'hide',
],
operators: ['>', '==', '>=', '<=', '<', 'and', 'or', 'not', '(', ')'],
whitespace: [
[/[ \t\r\n]+/, 'white'],
[/#(.*)/, 'comment', '@comment'],
],
})
5. 定义主题配色
monaco.editor.defineTheme: 用来定义编辑器的主题。
官方主题示例:
http://microsoft.github.io/monaco-editor/playground.html#customizing-the-appearence-exposed-colors
editor.defineTheme(languageID, {
base: 'vs-dark',
inherit: true,
rules: [
// token 配色
{token: 'noUseSign', foreground: '#BDBDBD'},
{token: 'Question', foreground: '#9BD7CA'},
{token: 'IfStatement', foreground: '#F3CD5E'},
{token: 'action', foreground: '#D38AFF'},
{token: 'compareSign', foreground: '#F3CD5E'},
{token: 'linkSign', foreground: '#F3CD5E'},
{token: 'Number', foreground: '#FF9878'},
{token: 'Gap', foreground: '#F3CD5E'},
{token: 'invalid', foreground: '#FFFFFF'},
],
colors: {
// 编辑器背景等配色
'editor.background': '#282B33',
'editorLineNumber.background': '#33363E',
'editor.lineHighlightBackground': '#333844',
},
});
6. 启动 service
monaco 提供了一个API: monaco.editor.createWebWorker 来使用内置的 ES6 Proxies 创建代理 Web worker,创建 worker 线程之后,将 worker 线程的代理传递给后续的错误诊断、代码补全等类,让其能调用 service。
7. 错误诊断
实现 DiagnosticsAdapter 类。
class DiagnosticsAdapter {
constructor(private worker: WorkerAccessor) {
// 注册生命周期事件
// model.onDidChangeContent,当前内容修改的事件,触发时进行 validate
// editor.onDidCreateModel,模型被创建的事件,注册 model.onDidChangeContent
// editor.onWillDisposeModel,模型被销毁前的事件,销毁 model.onDidChangeContent 注册的事件
}
private async validate(model: editor.IModel): Promise<void> {
// 调用 validation service,利用返回的信息进行划线和问题提示
}
// 销毁方法
public dispose(): void {
this._disposables.forEach(d => d && d.dispose());
this._disposables.length = 0;
}
}
8. 代码补全
monaco.languages.registerCompletionItemProvider
通过 registerCompletionItemProvider 注册代码补全,需要实现 CompletionItemProvider 类。
class DwLangCompletionItemProvider implements languages.CompletionItemProvider {
constructor(
private readonly _worker: WorkerAccessor,
private readonly _triggerCharacters: string[],
) {
}
public get triggerCharacters(): string[] {
// 触发补全的字符
return this._triggerCharacters;
}
async provideCompletionItems(
model: editor.IReadOnlyModel,
position: Position,
context: languages.CompletionContext,
token: CancellationToken,
): Promise<languages.CompletionList | undefined> {
// 调用 Completion service,返回补全信息
}
}
9. 代码 hover 提示
monaco.languages.registerHoverProvider
通过 registerHoverProvider 注册 hover 提示,需要实现 HoverProvider 类。
class DwLangHoverProvider implements languages.HoverProvider {
constructor(private readonly _worker: WorkerAccessor) {
}
async provideHover(
model: editor.IReadOnlyModel,
position: Position,
token: CancellationToken,
): Promise<languages.Hover | undefined> {
// 调用 Hover service,返回提示文案
}
}
10. 完整的代码示例
查看 D emo 源码:
http://github.com/hangaoke1/dwlang
遇到的问题及解决方案
问题:
当前的业务场景,注入的初始化信息(问卷数据)是会改变的,但是 monaco 目前没有提供 update worker 的初始化数据的方法。
解决方案:
我们需要在实现 worker、DiagnosticsAdapter(错误诊断) 等类时暴露 dispose 方法,通过 setupLanguage 向外暴露统一的 dispose 方法。在组件卸载时销毁 worker 等实例,在下次渲染时传入新的问卷数据,保证补全信息等是最新的数据。
const Editor: React.FC<IEditorProps> = () => {
const editorNodeRef = React.useRef();
const editorInsRef = React.useRef<editor.IStandaloneCodeEditor>(null);
const languageInsRef = React.useRef<IDisposable>(null);
React.useEffect(() => {
languageInsRef.current = setupLanguage({})
const editorNode = editorNodeRef.current
if (editorNode) {
// 创建编辑器
editorInsRef.current = editor.create(editorNode, {
language: languageID,
theme: languageID,
// 滚动条的缩略图
minimap: {enabled: false},
// 显示行号
selectOnLineNumbers: true,
// 行号左侧的 margin 内容,可以展示错误 icon 等一些标记内容
glyphMargin: true,
// 右键菜单
contextmenu: false,
// 自适应布局
automaticLayout: true,
padding: {
top: 8,
bottom: 8,
},
fontSize: 16,
lineNumbersMinChars: 2,
lineDecorationsWidth: '0px',
// 补全信息是否展示 icon
suggest: {
showIcons: false,
},
value: '',
});
}
return () => {
// 销毁语言的补全、hover、校验、worker 等
languageInsRef.current.dispose();
// 销毁编辑器
editorInsRef.current.dispose();
}
}, [])
return <div ref={editorNodeRef} style={{height: '90vh'}}/>;
}
其他
如果想体验完整的 DSL 编程功能,欢迎【点击原文】体验网易云商问卷调研。
参考资料
基于 Monaco Editor & LSP 打造智能 IDE - 掘金:
http://juejin.cn/post/7108913087613829151
Monaco Editor:
http://microsoft.github.io/monaco-editor/
如何创造一门上万人使用的语言-腾讯问卷DSL实践之路:
http://www.doc88.com/p-27247070453743.html?r=1
Monaco Editor:
http://microsoft.github.io/monaco-editor/
monaco.languages.register:
http://microsoft.github.io/monaco-editor/api/modules/monaco.languages.html#register
monaco.languages.setLanguageConfiguration:
http://microsoft.github.io/monaco-editor/api/modules/monaco.languages.html#setLanguageConfiguration
monaco.languages.setMonarchTokensProvider:
http://microsoft.github.io/monaco-editor/api/modules/monaco.languages.html#setMonarchTokensProvider
monaco.editor.defineTheme:
http://microsoft.github.io/monaco-editor/api/modules/monaco.editor.html#defineTheme
monaco.editor.createW ebWorker:
http://microsoft.github.io/monaco-editor/api/modules/monaco.editor.html#createWebWorker
monaco.languages.registerCompletionItemProvider:
http://microsoft.github.io/monaco-editor/api/modules/monaco.languages.html#registerCompletionItemProvider
monaco.languages.registerHoverProvider
http://microsoft.github.io/monaco-editor/api/modules/monaco.languages.html#registerHoverProvider