# 前言
- 由于项目需求,需要完成一个支持代码提示、代码校验、代码格式化的lua编辑器
1 技术选型
精力有限,只了解了几个主流的编辑器codemirror、ace、Monaco Editor
codemirror5
用户最多,生态最好,所以插件也相对完备,能想到的基本都有,同时也被很多线上应用在用,有什么问题百度搜下基本都能搜到。
ace
ajax团队弄的一个开源编辑器,生态、文档也还不错,相对codemirror来说,语法提示和语法校验支持的语言种类更多
Monaco Editor
微软开源的一个web代码编辑器,可以看作vscode的web版,各项功能都比较强,理论上来说vscode支持的插件Monaco Editor也支持(因为vscode的代码编辑器也是用这个实现的),但是相关文档比较少
基于vue封装的组件
- vue-codemirror
- vue2-ace-editor(不推荐使用,已长期未维护)
- vue-monaco-editor(不推荐使用,已长期未维护)
对比
各个编辑器对JavaScript、SQL、Java这些热门语言的支持度都很高,其他相对冷门语言的支持度都差一点,可以看看以下文章来帮助大家选型(以上对比图片来源于以下文章):
- Wikipedia:基于 JavaScript 的源代码编辑器的比较
- 基于JavaScript的代码编辑器的比较和选型
- 知乎的这个问题:CodeMirror 和 ACE 相比各有什么优缺点?
- 浅显的Monaco Editor 与codemirror 选型
2 技术验证
codemirror.js(vue-codemirror)
由于之前有了解过codemirror,所以第一个方案自然就尝试使用codemirror来实现lua编辑器
注意:
- codemirror已发布v6版本,本文使用的是v5版本
- vue-codemirror已兼容codemirror@6版本,只有@4版本才支持codemirror@5
安装
推荐使用vue-codemirror
npm i codemirror@5 -S
npm i vue-codemirror@4.0.6 -S
推荐安装@types/codemirror以支持类型推断
npm i @types/codemirror -D
基础使用
注册全局组件
// require lib
import Vue from 'vue'
import VueCodemirror from 'vue-codemirror'
// require styles
import 'codemirror/lib/codemirror.css'
// require more codemirror resource...
// you can set default global options and events when use
Vue.use(VueCodemirror, /* {
options: { theme: 'base16-dark', ... },
events: ['scroll', ...]
} */)
注册局部组件
// require component
import { codemirror } from 'vue-codemirror'
// require styles
import 'codemirror/lib/codemirror.css'
// require more codemirror resource...
// component
export default {
components: {
codemirror
}
}
使用组件
<template>
<codemirror v-model="code" :options="cmOptions"></codemirror>
</template>
<script>
// 引入语言
import 'codemirror/mode/javascript/javascript.js'
// 引入样式
import 'codemirror/theme/base16-dark.css'
// 按需导入codemirror其他功能
import 'codemirror/xxx'
export default {
data () {
return {
code: 'const a = 10',
cmOptions: {
// codemirror options
tabSize: 4,
mode: 'text/javascript',
theme: 'base16-dark',
lineNumbers: true,
line: true,
// more codemirror options, 更多 codemirror 的高级配置...
}
}
},
methods: {
},
computed: {
codemirror() {
return this.$refs.myCm.codemirror
}
},
mounted() {
}
}
</script>
实现lua代码编辑器
使用lua模式时,mode需要设置为text/x-lua
具体功能需要引入哪些模块已标好注释
<template>
<codemirror
class="lua-editor"
ref="editor"
:value="value"
:options="codemirrorOptions"
@input="handleInputChange"
@input-read="handleInputRead"
/>
</template>
<script>
import { codemirror } from 'vue-codemirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/mode/lua/lua'
import 'codemirror/theme/neat.css'
import 'codemirror/theme/idea.css'
// #region 搜索功能
// 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'
// #endregion
// #region 代码提示功能
// 具体语言可以从 codemirror/addon/hint/ 下引入多个
import 'codemirror/addon/hint/show-hint.css'
import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/hint/anyword-hint' // 简易的代码提示功能
// #endregion
// #region 高亮行功能
import 'codemirror/addon/selection/active-line'
import 'codemirror/addon/selection/selection-pointer'
// #endregion
// #region 覆盖scrollbar样式功能
import 'codemirror/addon/scroll/simplescrollbars.css'
import 'codemirror/addon/scroll/simplescrollbars'
// #endregion
// #region 自动括号匹配功能
import 'codemirror/addon/edit/matchbrackets.js'
// #endregion
// 全屏功能 由于项目复杂,自带的全屏功能一般不好使
// import 'codemirror/addon/display/fullscreen.css'
// import 'codemirror/addon/display/fullscreen.js'
// 显示自动刷新
import 'codemirror/addon/display/autorefresh.js'
// 多语言支持?
// import 'codemirror/addon/mode/overlay'
// import 'codemirror/addon/mode/multiplex'
import 'codemirror/addon/lint/lint.js'
import 'codemirror/addon/lint/lint.css'
// #region 代码段折叠功能
import 'codemirror/addon/fold/foldcode.js'
import 'codemirror/addon/fold/foldgutter.js'
import 'codemirror/addon/fold/foldgutter.css'
import 'codemirror/addon/fold/brace-fold.js' // 括号折叠
import 'codemirror/addon/fold/comment-fold.js'
import 'codemirror/addon/fold/indent-fold.js' // 缩进折叠
import 'codemirror/addon/fold/comment-fold.js'
// import 'codemirror/addon/fold/xml-fold.js'
// import 'codemirror/addon/fold/markdown-fold.js'
// #endregion
// #region merge功能
// import 'codemirror/addon/merge/merge.css'
// import 'codemirror/addon/merge/merge.js'
// #endregion
export default {
name: 'LuaEditor',
// #region 组件基础
components: { codemirror: codemirror },
props: {
value: {
type: String,
required: true
}
},
// #endregion
// #region 数据相关
data() {
return {
/** @type {import('@types/codemirror/index').EditorConfiguration}*/
codemirrorOptions: {
// 语言及语法模式
mode: 'text/x-lua',
// 主题
theme: 'neat',
// 显示函数
line: true,
// 显示行号
lineNumbers: true,
// 软换行
lineWrapping: true,
// tab宽度
tabSize: 4,
// 允许拖入的文件类型
allowDropFileTypes: ['text/x-lua'],
cursorScrollMargin: 5,
extraKeys: {},
// 高亮行功能
styleActiveLine: true,
// 调整scrollbar样式功能
// scrollbarStyle: 'overlay',
// 自动括号匹配功能
matchBrackets: true,
autofocus: true,
autoRefresh: true,
// #region 代码折叠
foldGutter: true,
// foldOptions: { scanUp: true },
gutters: [
'CodeMirror-linenumbers',
'CodeMirror-foldgutter',
'CodeMirror-lint-markers'
],
// #endregion
showHint: true,
lint: true,
hintOptions: {
// 避免由于提示列表只有一个提示信息时,自动填充
completeSingle: false
}
}
}
},
computed: {},
watch: {},
// #endregion
// #region 生命周期
created() {},
mounted() {},
// #endregion
methods: {
handleInputChange(value) {
this.$emit('input', value)
},
handleInputRead(cm) {
// 显示代码提示框
cm.showHint()
}
}
}
</script>
<style lang="scss">
.lua-editor {
textarea {
height: 100%;
}
}
</style>
实现效果
代码高亮:
简易的代码提示:
输入错误的代码:
由于不支持lua语言的代码段折叠、代码校验,所以无法测试
总结
优点:
- 支持lua语法高亮
- 简易的语法提示(根据当前代码中的关键字)
- 可以按需导入功能与模块
缺点:
- 不支持lua代码校验
- 不支持lua代码格式化
- lua语法提示不满足需求
- 没有全量导入功能,当导入大量模块和功能时,需要写很多import,不方便管理
.
.
.
ace.js(本文使用的方案)
安装
npm i ace-builds -S
推荐安装@types/ace以提供类型推断
npm i @types/ace -D
基础使用
全量引入(不推荐,仅开发阶段使用)
ace提供了一个全量导入模块:ace-builds/webpack-resolver
- 缺点:导致引入包过大,不需要的、使用不到的也被打包进来了,而且打包后的根目录下会有很多js文件
- 优点:不需要再去手动查找/导入会用到的模块了,适合在开发阶段使用
import ace from 'ace-builds'
import 'ace-builds/css/ace.css'
import 'ace-builds/webpack-resolver' // 全量导入
打包后输出如下:
按需引入(推荐,打包时使用)
ace-build.js建议使用按需引入,全量引入时体积过于庞大
- 按需引入需要知道会用到哪些模块,提前import进来,适合已开发完成、维护的阶段
- 需要注意的是,worker相关js文件,不能直接使用import导入,一定要使用ace.config.setModuleUrl + file-loader导入,否则会报错误
import ace from 'ace-builds'
import 'ace-builds/css/ace.css'
// #region lua语法高亮
import 'ace-builds/src-noconflict/mode-lua'
import 'ace-builds/src-noconflict/snippets/lua'
// #endregion
// #region 代码提示
import 'ace-builds/src-noconflict/ext-language_tools'
// #endregion
// #region 代码校验
ace.config.setModuleUrl(
'ace/mode/base_worker',
require('file-loader?esModule=false!ace-builds/src-noconflict/worker-base.js')
)
ace.config.setModuleUrl(
'ace/mode/lua_worker',
require('file-loader?esModule=false!ace-builds/src-noconflict/worker-lua.js')
)
// #endregion
// #region 主题
import 'ace-builds/src-noconflict/theme-chrome'
// #endregion
// #region 其他功能
import 'ace-builds/src-noconflict/ext-searchbox'
import 'ace-builds/src-noconflict/ext-keybinding_menu'
import 'ace-builds/src-noconflict/ext-settings_menu'
// #endregion
打包后输出如下,少了很多js文件,体积比全量引入小了10+MB
实现Lua代码编辑器
<template>
<div class="lua-editor">
<!-- <textarea ref="textarea"></textarea> -->
</div>
</template>
<script>
import ace from 'ace-builds'
import 'ace-builds/css/ace.css'
import 'ace-builds/webpack-resolver' // 全量导入
export default {
// #region 组件基础
components: {},
props: {
value: {
type: String,
required: true
},
options: Object
},
// #endregion
// #region 数据相关
data() {
content: this.value || '',
return {/** @type {import('ace-builds').Ace.EditorOptions}*/
editorOptions: {
// keyboardHandler: '',
mode: 'ace/mode/lua',
theme: 'ace/theme/chrome',
tabSize: 2,
selectionStyle: 'text',
// 拖动代码块
dragEnabled: true,
useWorker: true,
// 自动缩进
enableAutoIndent: true,
// 显示行号
showLineNumbers: true,
useSoftTabs: true,
// 渐变隐藏折叠按钮
fadeFoldWidgets: true,
// 输入边界
showPrintMargin: false,
// 高亮当前行
highlightActiveLine: true,
// 高亮选中词
highlightSelectedWord: true,
// 滚动动画
autoScrollEditorIntoView: true,
copyWithEmptySelection: true,
// #region 启用自动完成和代码段
// enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
enableSnippets: true
// #endregion
}}
},
computed: {
/** @return {import('ace-builds').Ace.Editor} */
_editor() {
return this.editor
}
},
watch: {
value(nval) {
this.syncContent(nval)
},
options(nval) {
this.syncOptions(nval)
}
},
// #endregion
// #region 生命周期
created() {},
mounted() {
let editor = ace.edit(this.$el, this.editorOptions)
this.editor = editor
},
beforeDestroy() {
this._editor.destroy()
},
// #endregion
methods: {
/**
* 同步内容
*/
syncContent(value = this.value) {
if (this.content != value) {
this._editor?.setValue(value, 1)
}
},
/**
* 同步配置
*/
syncOptions(option = this.option) {
let keys = Object.keys(option)
for (let key of keys) {
// 禁止修改固定的option
if (key in this.editorOptions) continue
this._editor?.setOption(key, option[key])
}
}
}
}
</script>
实现效果
功能 | 实际效果 |
---|---|
代码高亮 | |
代码提示 | |
代码校验 | |
代码段折叠 |
总结
优点:
- 支持lua语法高亮
- 支持lua代码校验
- 支持lua代码块折叠
- 基础语法提示,提供部分代码片段
- 可以按需导入功能与模块
缺点:
- 不支持lua代码格式化
- 引入的模块,打包后会在根目录下产生很多js文件
Monaco Editor
这个编辑器是在写文章的时候看到的,目前还没有尝试使用,后续有时间追加
可参考文档:
.
.
.
3 代码格式化
codemirror、ace都不支持lua的代码格式化,那只能在github找找使用javascript实现lua格式化的开源插件了,共找到如下开源库
luaparse
此开源库使用javascript语言实现将lua代码字符串分析为ast抽象语法树,但是无法直接使用,需要二次开发,有一定的开发成本
@appguru/luafmt(本文使用的方案)
此开源库基于luaparse库开发,luaparse将lua语言转换为ast树,@appguru/luafmt在此基础上对ast树进行分析,以实现代码格式化
安装&使用
安装
npm i @appguru/luafmt -S
使用
import { ast } from '@appguru/luafmt'
import { parse } from 'luaparse'
// 创建格式化器实例
let formatter = ast.formatter({
indent: ' ', // 缩进符号
newline: '\n', // 换行符号
extra_newlines: true, // 是否额外换行
// tabWidth: 2, // tab宽度
// useTabs: true,
// semi: false, // 是否加分号
inline: { // 单行宽度设置
block: {
max_exp_length: 60
},
table: {
max_field_count: 0,
max_field_length: 0
}
}
})
// 使用
let result = formatter('lua code')
实际效果
原始代码:
格式化后:
lua-format
此库应该是自行实现了ast语法树分析,所以体积会比较大
安装&使用
安装
npm i lua-format -S
使用
import luafmt from 'luamin'
let result = luafmt.Beautify(code, {
RenameVariables: false, // 重命名变量
RenameGlobals: false, // 重命名全局变量
SolveMath: false
})
实际效果
原始代码:
格式化后:
多次格式化后:
多行注释测试:
luamin
此开源库也是基于luaparse库开发,不过是内部集成的方式,仅支持lua代码压缩,没有其他功能
安装&使用
安装
npm i luamin -S
使用
import luamin from 'luamin'
let result = luamin.minify(code)
实际效果
原始代码:
压缩后:
总结
推荐使用@appguru/luafmt,或者基于luaparse自行实现
- | 打包后体积(粗略数值) | 开源协议 | 最近一次提交 | 存在的问题 |
---|---|---|---|---|
@appguru/luafmt | 8.59kb | MIT | 2020-11 | 1. 同行注释会跑到代码下面(强制换行) 2. 数值变量会变成指数形式 |
lua-format | 32.1kb(gzip: 10.4kb) | ISC | 2022-07 | 1.会输出:–discord.gg/boronide, code generated using luamin.js™ + 换行 * 4,需要手动去除 2.会将多行注释删除 |
luamin | 64.1kb(gzip: 15.2kb) | MIT | 2019-08 | 只支持压缩lua代码 |
4 lua代码编辑器
源码
<template>
<div class="lua-editor">
<!-- <textarea ref="textarea"></textarea> -->
</div>
</template>
<script>
import ace from 'ace-builds'
// import 'ace-builds/webpack-resolver'
import 'ace-builds/css/ace.css'
// #region lua语法高亮
import 'ace-builds/src-noconflict/mode-lua'
import 'ace-builds/src-noconflict/snippets/lua'
// #endregion
// #region 代码提示
import 'ace-builds/src-noconflict/ext-language_tools'
// #endregion
// #region 代码校验
// import 'ace-builds/src-noconflict/worker-base.js'
// import 'ace-builds/src-noconflict/worker-lua.js'
ace.config.setModuleUrl(
'ace/mode/base_worker',
require('file-loader?esModule=false!ace-builds/src-noconflict/worker-base.js')
)
ace.config.setModuleUrl(
'ace/mode/lua_worker',
require('file-loader?esModule=false!ace-builds/src-noconflict/worker-lua.js')
)
// #endregion
// #region 主题
import 'ace-builds/src-noconflict/theme-chrome'
// #endregion
// #region 其他功能
import 'ace-builds/src-noconflict/ext-searchbox'
import 'ace-builds/src-noconflict/ext-keybinding_menu'
import 'ace-builds/src-noconflict/ext-settings_menu'
// #endregion
import { ast } from '@appguru/luafmt'
// import { ast } from './appguru'
import { parse } from 'luaparse'
const events = [
'bulr',
'change',
'changeSelectionStyle',
'changeSession',
'copy',
'focus',
'paste',
'mousemove',
'mouseup',
'mousewheel',
'click'
]
/**
* LuaEditor
* @author lcm
* @createTime
* @description
*/
export default {
name: 'LuaEditor',
// #region 组件基础
components: {},
props: {
value: {
type: String,
required: true
},
options: Object,
theme: String,
mode: String,
readonly: Boolean
},
// #endregion
// #region 数据相关
data() {
return {
content: this.value || '',
cursorPos: null,
/** @type {import('ace-builds').Ace.EditorOptions}*/
editorOptions: {
// keyboardHandler: '',
mode: 'ace/mode/lua',
theme: 'ace/theme/chrome',
tabSize: 2,
selectionStyle: 'text',
// 拖动代码块
dragEnabled: true,
useWorker: true,
// 自动缩进
enableAutoIndent: true,
// 显示行号
showLineNumbers: true,
useSoftTabs: true,
// 渐变隐藏折叠按钮
fadeFoldWidgets: true,
// 输入边界
showPrintMargin: false,
// 高亮当前行
highlightActiveLine: true,
// 高亮选中词
highlightSelectedWord: true,
// 滚动动画
autoScrollEditorIntoView: true,
copyWithEmptySelection: true,
// #region 启用自动完成和代码段
// enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
enableSnippets: true
// #endregion
}
}
},
computed: {
/** @return {import('ace-builds').Ace.Editor} */
_editor() {
return this.editor
}
},
watch: {
value(nval) {
this.syncContent(nval)
},
theme(nval) {
this._editor?.setTheme('ace/theme/' + nval)
},
mode(nval) {
this._editor?.getSession().setMode('ace/mode/' + nval)
},
readonly(nval) {
this._editor?.setReadOnly(nval)
},
options(nval) {
this.syncOptions(nval)
}
},
// #endregion
// #region 生命周期
created() {},
mounted() {
if (this.theme) {
this.editorOptions.theme = this.theme
}
// if (this.lang) {
// this.editorOptions.mode = this.lang
// }
//@appguru/luafmt
this.formatter = ast.formatter({
indent: ' ',
newline: '\n',
extra_newlines: true,
// tabWidth: 2,
// useTabs: true,
// semi: false,
inline: {
block: {
max_exp_length: 60
},
table: {
max_field_count: 0,
max_field_length: 0
}
}
})
let editor = ace.edit(this.$el, this.editorOptions)
editor.setValue(this.value, 1)
// editor.setOption('auto', 1)
editor.commands.addCommands([
{
name: 'showSettingsMenu',
bindKey: { win: 'Ctrl-q', mac: 'Ctrl-q' },
exec(editor) {
ace.config.loadModule('ace/ext/settings_menu', function (module) {
module.init(editor)
editor.showSettingsMenu()
})
},
readOnly: true
}
])
// 快捷键帮助面板
editor.commands.addCommand({
name: 'showKeyboardShortcuts',
bindKey: { win: 'Ctrl-Alt-h', mac: 'Command-Alt-h' },
exec(editor) {
ace.config.loadModule('ace/ext/keybinding_menu', function (module) {
module.init(editor)
editor.showKeyboardShortcuts()
})
}
})
//格式化
editor.commands.addCommand({
name: 'formattingCode',
bindKey: { win: 'Shift-Alt-F', mac: 'Shift-Alt-F' },
exec: (editor) => {
this.formattingCode()
}
})
editor.selection.on('changeCursor', (e) => {
this.cursorPos = editor.getCursorPosition()
this.$emit('changeCursor', e)
})
editor.on('change', (ev) => {
this.cursorPos = editor.getCursorPosition()
let value = editor.getValue()
this.content = value
console.log('cursorPos', this.cursorPos)
this.$emit('input', value)
})
for (let name of events) {
editor.on(name, (ev) => {
this.$emit(name, ev)
})
}
this.editor = editor
},
beforeDestroy() {
this._editor.destroy()
},
// #endregion
methods: {
/**
* 格式化代码
*/
formattingCode() {
try {
const srcAST = parse(this.content)
ast.fixRanges(srcAST)
ast.insertComments(srcAST)
let result = this.formatter(srcAST)
this.content = result
this.editor.setValue(this.content)
// 获取当前光标位置
// let cursorPos = this._editor.getCursorPosition()
// this._editor.setValue(result, 0)
// 保持光标位置不变
// this._editor.navigateTo(cursorPos.row, cursorPos.column)
return true
} catch (error) {
console.log('格式化失败 ', error)
return false
}
},
/**
* 同步内容
*/
syncContent(value = this.value) {
if (this.content != value) {
this._editor?.setValue(value, 1)
}
},
/**
* 同步配置
*/
syncOptions(option = this.option) {
let keys = Object.keys(option)
for (let key of keys) {
// 禁止修改固定的option
if (key in this.editorOptions) continue
this._editor?.setOption(key, option[key])
}
}
}
}
</script>
<style lang="scss">
.lua-editor {
textarea {
height: 100%;
}
}
</style