基于 vue 的 CodeMirror 代码编辑器
Codemirror是一个不错的Web代码编辑库,可以方便简单的集成。没有自动提示功能的代码编辑器是没有灵魂的,Codemirror的自动提示功能是使用show-hint库进行的,我们可以调用showHint方法或者autoComplete方法来显示提示框。
- 官网地址:https://codemirror.net/ (opens new window)
- 官方GitHub:https://github.com/codemirror/codemirror.next (opens new window)
- Vue官方GitHub: https://github.com/surmon-china/vue-codemirror (opens new window)
# 安装依赖
npm i codemirror --save
# 代码案例
<template>
<div class="coder-panel">
<textarea ref="textarea"></textarea>
</div>
</template>
<script>
// 引入全局实例
import _CodeMirror from "codemirror";
// 核心样式
// import 'codemirror/theme/ambiance.css'
import "codemirror/lib/codemirror.css";
import "codemirror/addon/hint/show-hint.css";
// 引入主题后还需要在 options 中指定主题才会生效
import "codemirror/theme/3024-day.css";
// import "codemirror/theme/cobalt.css";
// 需要引入具体的语法高亮库才会有对应的语法高亮效果
// codemirror 官方其实支持通过 /addon/mode/loadmode.js 和 /mode/meta.js 来实现动态加载对应语法高亮库
// 但 vue 貌似没有无法在实例初始化后再动态加载对应 JS ,所以此处才把对应的 JS 提前引入
import "codemirror/addon/edit/matchbrackets";
import "codemirror/addon/selection/active-line";
import "codemirror/addon/hint/show-hint";
import "codemirror/addon/hint/anyword-hint.js";
import "codemirror/mode/cypher/cypher.js";
// require('codemirror/mode/sql/sql')
// require('codemirror/addon/hint/sql-hint')
import cypherKeywords from "@/utils/CypherKeywords.js";
// 尝试获取全局实例
const CodeMirror = window.CodeMirror || _CodeMirror;
let cypherKeywords = [
'ACCESS',
'ACTIVE',
'ADMIN',
'ADMINISTRATOR',
'ALL',
'ALLSHORTESTPATHS',
'ALTER',
'AND',
'ANY',
'AS',
'ASC',
'ASCENDING',
'ASSERT',
'ASSIGN',
'BOOSTED',
'BRIEF',
'BTREE',
'BUILT',
'BY',
'CALL',
'CASE',
'CATALOG',
'CHANGE',
'COMMIT',
'CONSTRAINT',
'CONSTRAINTS',
'CONTAINS',
'COPY',
'COUNT',
'CREATE',
'CSV',
'CURRENT',
'CYPHER',
'DATABASE',
'DATABASES',
'DBMS',
'DEFAULT',
'DEFINED',
'DELETE',
'DENY',
'DESC',
'DESCENDING',
'DETACH',
'DISTINCT',
'DROP',
'EACH',
'ELEMENT',
'ELEMENTS',
'ELSE',
'END',
'ENDS',
'EXECUTABLE',
'EXECUTE',
'EXIST',
'EXISTENCE',
'EXISTS',
'EXPLAIN',
'EXTRACT',
'FALSE',
'FIELDTERMINATOR',
'FILTER',
'FOR',
'FOREACH',
'FROM',
'FULLTEXT',
'FUNCTION',
'FUNCTIONS',
'GRANT',
'GRAPH',
'GRAPHS',
'HEADERS',
'HOME',
'IF',
'IN',
'INDEX',
'INDEXES',
'IS',
'JOIN',
'KEY',
'LABEL',
'LABELS',
'LIMIT',
'LOAD',
'LOOKUP',
'MANAGEMENT',
'MATCH',
'MERGE',
'NAME',
'NAMES',
'NEW',
'NODE',
'NODES',
'NONE',
'NOT',
'NULL',
'OF',
'ON',
'OPTIONAL',
'OPTIONS',
'OR',
'ORDER',
'OUTPUT',
'PASSWORD',
'PERIODIC',
'POPULATED',
'PRIVILEGES',
'PROCEDURE',
'PROCEDURES',
'PROFILE',
'PROPERTY',
'READ',
'REDUCE',
'REL',
'RELATIONSHIP',
'RELATIONSHIPS',
'REMOVE',
'RENAME',
'REPLACE',
'REQUIRED',
'RETURN',
'REVOKE',
'ROLE',
'ROLES',
'SCAN',
'SET',
'SHORTESTPATH',
'SHOW',
'SINGLE',
'SKIP',
'START',
'STARTS',
'STATUS',
'STOP',
'SUSPENDED',
'THEN',
'TO',
'TRAVERSE',
'TRUE',
'TYPE',
'TYPES',
'UNION',
'UNIQUE',
'UNWIND',
'USER',
'USERS',
'USING',
'VERBOSE',
'WHEN',
'WHERE',
'WITH',
'WRITE',
'XOR',
'YIELD',
];
export default {
name: "in-coder",
data() {
return {
// 内部真实的内容
code: "",
// 默认的语法类型
mode: "cypher",
// 编辑器实例
coder: null,
// 默认配置
options: {
// 缩进格式
mode: "cypher",
// tabSize: 2,
// 主题,对应主题库 JS 需要提前引入 cobalt
theme: "3024-day",
// 显示行号
lineNumbers: false,
line: false,
matchBrackets: true,
// 高亮行功能
// styleActiveLine: false,
// 调整scrollbar样式功能
// scrollbarStyle: "overlay",
// smartIndent: true,
// autofocus: false,
hintOptions: {
// 自定义提示选项
hint: this.handleShowHint,
completeSingle: false,
},
},
// 支持切换的语法高亮类型,对应 JS 已经提前引入
// 使用的是 MIME-TYPE ,不过作为前缀的 text/ 在后面指定时写死了
};
},
props: {
// 外部传入的内容,用于实现双向绑定
value: String,
// 外部传入的语法类型
language: {
type: String,
default: null,
},
},
mounted() {
// 初始化
this._initialize();
},
methods: {
handleShowHint(cmInstance, hintOptions) {
// console.log(cmInstance);
let hintList = cypherKeywords;
let cur = this.coder.getCursor(),
token = this.coder.getTokenAt(cur);
let start = token.start,
end = cur.ch;
let str = token.string;
let list = hintList.filter(function (item) {
return item.toLowerCase().indexOf(str.toLowerCase()) === 0;
});
if (list.length) {
return {
list: list,
from: CodeMirror.Pos(cur.line, start),
to: CodeMirror.Pos(cur.line, end),
};
}
},
// 初始化
_initialize() {
// 初始化编辑器实例,传入需要被实例化的文本域对象和默认配置
this.coder = CodeMirror.fromTextArea(this.$refs.textarea, this.options);
// 编辑器赋值
this.coder.setValue(this.value || this.code);
// 支持双向绑定
this.coder.on("change", (coder) => {
this.code = coder.getValue();
if (this.$emit) {
this.$emit("input", this.code);
}
});
this.coder.on("inputRead", () => {
this.coder.closeHint();
this.coder.showHint();
});
// this.coder.on("cursorActivity", () => {
// this.coder.closeHint();
// this.coder.showHint();
// });
this.coder.setSize("auto", "auto");
// editor.setValue(""); //给代码框赋值
// editor.getValue(); //获取代码框的值
// editor.setOption("readOnly", true); //类似这种
// this.coder.setOption("mode", `text/${this.mode}`);
// 尝试从父容器获取语法类型
if (this.language) {
// 获取具体的语法类型对象
let modeObj = this._getLanguage(this.language);
// 判断父容器传入的语法是否被支持
if (modeObj) {
this.mode = modeObj.label;
}
}
},
// 获取当前语法类型
_getLanguage(language) {
// 在支持的语法类型列表中寻找传入的语法类型
return this.modes.find((mode) => {
// 所有的值都忽略大小写,方便比较
let currentLanguage = language.toLowerCase();
let currentLabel = mode.label.toLowerCase();
let currentValue = mode.value.toLowerCase();
// 由于真实值可能不规范,例如 java 的真实值是 x-java ,所以讲 value 和 label 同时和传入语法进行比较
return currentLabel === currentLanguage || currentValue === currentLanguage;
});
},
// 更改模式
changeMode(val) {
// 修改编辑器的语法配置
this.coder.setOption("mode", `text/${val}`);
// 获取修改后的语法
let label = this._getLanguage(val).label.toLowerCase();
// 允许父容器通过以下函数监听当前的语法值
this.$emit("language-change", label);
},
},
};
</script>
<style lang="scss">
.coder-panel {
flex-grow: 1;
display: flex;
position: relative;
.CodeMirror {
flex-grow: 1;
z-index: 1;
// height: 26px;
background: transparent !important;
// .CodeMirror-code {
// line-height: 19px;
// }
font-family: Inconsolata, Monaco, "Courier New", Terminal, monospace !important;
font-size: 18px !important;
line-height: 23px !important;
padding-top: 6px;
}
.CodeMirror-cursor {
border-left: 11px solid rgba(155, 157, 162, 0.37) !important;
}
// .CodeMirror-scroll {
// margin-bottom: 10px;
// padding-bottom: 0px;
// }
.code-mode-select {
position: absolute;
z-index: 2;
right: 10px;
top: 10px;
max-width: 130px;
}
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
示例图片
# 高级功能
<template>
<div>
<codemirror
ref="cm"
v-model="code"
:options="cmOptions"
@input="inputChange"
></codemirror>
</div>
</template>
<script>
// 全局引入vue-codemirror
import {codemirror} from 'vue-codemirror';
// 引入css文件
import 'codemirror/lib/codemirror.css'
// 引入主题 可以从 codemirror/theme/ 下引入多个
import 'codemirror/theme/idea.css'
// 引入语言模式 可以从 codemirror/mode/ 下引入多个
import 'codemirror/mode/sql/sql.js';
// 搜索功能
// find:Ctrl-F (PC), Cmd-F (Mac)
// findNext:Ctrl-G (PC), Cmd-G (Mac)
// findPrev:Shift-Ctrl-G (PC), Shift-Cmd-G (Mac)
// replace:Shift-Ctrl-F (PC), Cmd-Alt-F (Mac)
// replaceAll:Shift-Ctrl-R (PC), Shift-Cmd-Alt-F (Mac)
import 'codemirror/addon/dialog/dialog.css'
import 'codemirror/addon/dialog/dialog'
import 'codemirror/addon/search/searchcursor'
import 'codemirror/addon/search/search'
import 'codemirror/addon/search/jump-to-line'
import 'codemirror/addon/search/matchesonscrollbar'
import 'codemirror/addon/search/match-highlighter'
// 代码提示功能 具体语言可以从 codemirror/addon/hint/ 下引入多个
import 'codemirror/addon/hint/show-hint.css';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/hint/sql-hint';
// 高亮行功能
import 'codemirror/addon/selection/active-line'
import 'codemirror/addon/selection/selection-pointer'
// 调整scrollbar样式功能
import 'codemirror/addon/scroll/simplescrollbars.css'
import 'codemirror/addon/scroll/simplescrollbars'
// 自动括号匹配功能
import 'codemirror/addon/edit/matchbrackets'
// 全屏功能 由于项目复杂,自带的全屏功能一般不好使
import 'codemirror/addon/display/fullscreen.css'
import 'codemirror/addon/display/fullscreen'
// 显示自动刷新
import 'codemirror/addon/display/autorefresh'
// 多语言支持?
import 'codemirror/addon/mode/overlay'
import 'codemirror/addon/mode/multiplex'
// 代码段折叠功能
import 'codemirror/addon/fold/foldcode'
import 'codemirror/addon/fold/foldgutter'
import 'codemirror/addon/fold/foldgutter.css'
import 'codemirror/addon/fold/brace-fold'
import 'codemirror/addon/fold/comment-fold'
import 'codemirror/addon/fold/xml-fold.js';
import 'codemirror/addon/fold/indent-fold.js';
import 'codemirror/addon/fold/markdown-fold.js';
import 'codemirror/addon/fold/comment-fold.js';
// merge功能
import 'codemirror/addon/merge/merge.css'
import 'codemirror/addon/merge/merge'
// google DiffMatchPatch
import DiffMatchPatch from 'diff-match-patch'
// DiffMatchPatch config with global
window.diff_match_patch = DiffMatchPatch;
window.DIFF_DELETE = -1;
window.DIFF_INSERT = 1;
window.DIFF_EQUAL = 0;
export default {
name: 'Show',
components: {codemirror},
data() {
return {
code: 'select a from table1 where b = 1',
cmOptions: {
// 语言及语法模式
mode: 'text/x-sql',
// 主题
theme: 'idea',
// 显示函数
line: true,
lineNumbers: true,
// 软换行
lineWrapping: true,
// tab宽度
tabSize: 4,
// 代码提示功能
hintOptions: {
// 避免由于提示列表只有一个提示信息时,自动填充
completeSingle: false,
// 不同的语言支持从配置中读取自定义配置 sql语言允许配置表和字段信息,用于代码提示
tables: {
"table1": ["c1", "c2"],
},
},
// 高亮行功能
styleActiveLine: true,
// 调整scrollbar样式功能
scrollbarStyle: 'overlay',
// 自动括号匹配功能
matchBrackets: true
}
}
},
methods: {
inputChange(content) {
this.$nextTick(() => {
console.log("code:" + this.code);
console.log("content:" + content)
});
},
},
mounted() {
// 代码提示功能 当用户有输入时,显示提示信息
this.$refs.cm.codemirror.on('inputRead', cm => {
cm.showHint();
})
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# 自定义代码提示
# 自定义hint方法
在methods中自定义代码实现方法:
/**
使用自定义hint,网上没有详细的讲解,这里着重讲一下。
1. 第一个入参cmInstance指的是codeMirror实例,第二个是配置中的的hintOptions值。
2. 从cmInstance中getCursor指的是获取光标实例,光标实例里有行数、列数。
3. 可以从cmInstance的getLine方法里传入一个行数,从而获取行中的字符串。
4. token对象是cmInstance对光标所在字符串进行提取处理,从对应语言的类库中判断光标所在字符串的类型,方便hint提示。token中包含start、end、string、type等属性,start和end指的是光标所在字符串在这一行的起始位置和结束位置,string是提取的字符串,type表示该字符串是什么类型(keyword/operator/string等等不定)
5. 下面方法中返回的结果体意思是:下拉列表中展示hello和world两行提示,from和to表示当用户选择了提示内容后,这些提示内容要替换编辑区域的哪个字符串。方法中的代码含义是替换token全部字符串。
*/
handleShowHint(cmInstance, hintOptions) {
let cursor = cmInstance.getCursor();
let cursorLine = cmInstance.getLine(cursor.line);
let end = cursor.ch;
let start = end;
let token = cmInstance.getTokenAt(cursor)
console.log(cmInstance, cursor, cursorLine, end, token)
// console.log(hintOptions.tables)
// return hintOptions.tables;
return {
list: ["hello","world"],
from: {ch: token.start, line: cursor.line},
to: {ch: token.end, line: cursor.line}
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
接下来修改配置文件中的hintOptions属性,增加hint属性,并指向实现方法:
{ // 省略其他配置项...
hintOptions: {
completeSingle: false,
hint: this.handleShowHint
}
}
2
3
4
5
6
# 自定义hint展示内容
自定义代码提示内容后,如果想让弹出的内容与实际插入内容不一样,则需要将返回结果进行调整。这里有一个示例,插入内容是英文,展示内容是中文。在methods中新增方法:
handleShowHint2(cmInstance, hintOptions) {
let cursor = cmInstance.getCursor();
let cursorLine = cmInstance.getLine(cursor.line);
let end = cursor.ch;
let start = end;
let token = cmInstance.getTokenAt(cursor)
console.log(cmInstance, cursor, cursorLine, end, token)
return {
list: [{
text: "hello",
displayText: "你好呀",
displayInfo: "提示信息1",
render: this.hintRender
}, {
text: "world",
displayText: "世界",
displayInfo: "提示信息2",
render: this.hintRender
}],
from: {
ch: token.start, line: cursor.line
},
to: {
ch: token.end, line: cursor.line
}
}
},
hintRender(element, self, data) {
let div = document.createElement("div");
div.setAttribute("class", "autocomplete-div");
let divText = document.createElement("div");
divText.setAttribute("class", "autocomplete-name");
divText.innerText = data.displayText;
let divInfo = document.createElement("div");
divInfo.setAttribute("class", "autocomplete-hint");
divInfo.innerText = data.displayInfo;
div.appendChild(divText);
div.appendChild(divInfo);
element.appendChild(div);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
增加样式调整:
<style>
.autocomplete-div {
display: inline-block;
width: 100%;
}
.autocomplete-name {
display: inline-block;
}
.autocomplete-hint {
display: inline-block;
float: right;
color: #0088ff;
margin-left: 1em;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 异步返回hint结果
cm提供了一种异步hint的功能,如果我们的数据来自后端,那这个功能就用的上了。具体使用方式如下:
设置hint配置:
{ // 省略其他配置项...
hintOptions: {
completeSingle: false,
hint: this.handleShowHint,
async: true
}
}
2
3
4
5
6
7
实现自定义hint:
handleShowHint3(cmInstance, hintOptions) {
let cursor = cmInstance.getCursor();
let cursorLine = cmInstance.getLine(cursor.line);
let end = cursor.ch;
let start = end;
let token = cmInstance.getTokenAt(cursor)
console.log(cmInstance, cursor, cursorLine, end, token)
// console.log(hintOptions.tables)
// return hintOptions.tables;
// 返回一个promise即可
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
list: ["hello", "world"],
from: {ch: token.start, line: cursor.line},
to: {ch: token.end, line: cursor.line}
})
}, 2000);
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# CodeMirror命令API
具体命令见:https://codemirror.net/doc/manual.html#commands (opens new window)
实际调用方式:
methods:{
find(){
this.$refs.cm.comdemirror.execCommand("find")
}
}
2
3
4
5
# 特殊用法和踩过的坑
# 自动高度
codemirror默认的高度是300px,如果想要调整默认高度,可以在mounted方法中增加下面一段代码,这段代码的含义是调整cm高度为(当前浏览器高度-200)px,并且在窗口发生变化时,重新再做出调整。
this.$refs.cm.codemirror.setSize("auto", (document.documentElement.clientHeight - 200) + "px")
this.$nextTick(() => {
window.addEventListener('resize', () => {
//监听浏览器窗口大小改变
//浏览器变化执行动作
this.$refs.cm.codemirror.setSize("auto", (document.documentElement.clientHeight - 200) + "px")
});
})
2
3
4
5
6
7
8
# 只读模式
在官方文档里提示调整options中的readOnly参数便可以设置为只读,但实际上如果设置值为true后,用户还能在浏览器中看到光标闪烁,如果希望页面上不能编辑,则将该值设置为'nocursor'即可。
但如果设置了'nocursor',那么任何人将无法选中代码,也无法右键复制。如果还想支持选择和复制,那么需要用到以下代码:
this.$refs.cm.codemirror.setOption("readOnly",true)
// 不设的话,默认是530
this.$refs.cm.codemirror.setOption("cursorBlinkRate",-1)
2
3
# tab转空格
如果在新的一行直接使用tab键,大概率会输入一个制表符,但如果从上一行敲回车进入下一行,却默认是空格。这样的逻辑让使用者深恶痛绝,如何让tab键也变成空格呢?在配置json中增加下面配置,既可实现两者逻辑统一。
{
indentUnit:4,
extraKeys: {
Tab: (cm) => {
// 存在文本选择
if (cm.somethingSelected()) {
// 正向缩进文本
cm.indentSelection('add');
} else {
// 无文本选择
//cm.indentLine(cm.getCursor().line, "add"); // 整行缩进 不符合预期
// 光标处插入 indentUnit 个空格
//console.log(cm.getOption("tabSize"),cm.getOption("indentUnit"))
cm.replaceSelection(Array(cm.getOption("indentUnit") + 1).join(" "), "end", "+input");
}
},
"Shift-Tab": (cm) => {
// 反向缩进
if (cm.somethingSelected()) {
// 反向缩进
cm.indentSelection('subtract');
} else {
// cm.indentLine(cm.getCursor().line, "subtract"); // 直接缩进整行
const cursor = cm.getCursor();
// 光标回退 indexUnit 字符
cm.setCursor({line: cursor.line, ch: cursor.ch - cm.getOption("indentUnit")});
}
return;
},
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 参考Demo
- 02
- Node与GLIBC_2.27不兼容解决方案08-19
- 03
- Git清空本地文件跟踪缓存08-13