CodeMirror 是基于js的源代码编辑器组件,它支持javascript等多种高级语言,tampermonkey内置的代码编辑器就是基于它。它的按键组合方式兼容vim,emacs等,调用者还可自定义”自动完成“的列表窗口,自由度极高,相当成熟。
但是这个库也是问题多多,很需要深度配置、改良优化。
前排提示:由于信息闭塞,所以才研究了 codemirror 5。研究完才发现, codemirror 5官方基本处于弃坑状态,issue没人理。最新版本是 codemirror 6,仍在开发当中,但各方面效果已经,明显优于 codemirror 5。
0. 入门
入门很简单,可以F12观察官方demo是怎么实现的:
https://codemirror.net/ (codemirror 6)
https://codemirror.net/examples/
https://codemirror.net/5/
关键代码:
<!-- Create a simple CodeMirror instance -->
<link rel="stylesheet" href="lib/codemirror.css">
<script src="lib/codemirror.js"></script>
<script>
var editor = CodeMirror.fromTextArea(myTextarea, {
lineNumbers: true
});
v
</script>
官方demo都是直接运行在文档页面上的。而且在编辑框内,显示的是demo自身的代码。有许多这样的小例子。
附本地运行方式:
- 从GitHub下载整个仓库,里面有些(比如核心库codemirror.js)是需要编译后才能运行,但有些就是普通的js代码(比如各种addon、mode),以及样式文件 codemirror.css。
- 从cdn或官方的demo示例中查找核心库地址:codemirror.js,这虽然是多模块编译出来的,但未经混淆,直接用vscode阅读修改并无不可,甚至效率更高。
- 编辑并预览 demo.html。
1. 需求:用任意键触发自动完成
https://stackoverflow.com/questions/13744176/codemirror-autocomplete-after-any-keyup
关键代码:
editor.on("keyup", function (cm, event) {
// …… 判断条件,过滤一些按键,过滤某些上下文情况。
CodeMirror.commands.autocomplete(cm, null, {completeSingle: false});
});
如果只要ctrl+space触发自动完成的话,是这样配置的:
var editor=CodeMirror.fromTextArea(document.getElementById("code"),{
……
, extraKeys:{"Ctrl-Space":"autocomplete"}
// 或者 :
, extraKeys:{"Ctrl-Space":'Ctrl-Space': (cm) =>
{
cm.showHint()
}}
}
});
2. 让自动完成窗口同时 ① 包含 buffer 中提取的上下文关键词(anyword
)②包含 javascript 语言的关键词。
https://discuss.codemirror.net/t/anyword-hinting-while-in-javascript-mode/1506/7
关键代码:
function hintingFunction(cm) {
const anyhint = CodeMirror.hint.anyword(cm, options)
const jshint = CodeMirror.hint.javascript(cm, options)
const words = new Set([...anyhint.list, ...jshint.list])
if (words.size > 0) {
return {
list: Array.from(words),
from: jshint.from,
to: jshint.to
}
}
}
…… { completeSingle: false, hint: hintingFunction } ……
如果只学习官方的短demo:
CodeMirror.commands.autocomplete = function(cm) {
CodeMirror.showHint(cm, CodeMirror.hint.anyword);
}
,这样是不行的,因为只返回了anyword分析出的上下文关键词,导致javascript关键词被覆盖!
而 gpt 给的答案则更是搞笑:
CodeMirror.commands.autocomplete = function(cm) {
CodeMirror.showHint(cm, CodeMirror.hint.anyword);
CodeMirror.showHint(cm, CodeMirror.hint.javascript);
}
整天瞎编,机器人这会儿的鼻子已经到达银河系中心了吧!
注意需要正确引入 “codemirror5-master/mode/javascript/javascript.js”,不然没有 CodeMirror.hint.javascript
(自动完成),还要引入 “codemirror5-master/addon/hint/javascript-hint.js”,不然也没有 mode:"application/javascript"
(语法高亮)!
3. 换行回车时,保持原有缩进。
这是 tampermonkey 编辑器最让我不爽的一点:每次在方法之间换行,都把缩进吃了。后来干脆换用vscode + @require file:///本地文件.user.js
的方式,不再用它的内置编辑器。现在,我几乎站在了同一起点,自然要比前辈做得好!
查阅资料后发现,原来“把缩进吃了”是特性,而不是bug,关掉即可:
mode : ……
, smartIndent: false
真是杀马特啊。
可关掉之后,function() { 回车后,就不会自动添加应该有的缩进了,伤脑筋……
最后通过修改 codemirror.js,让杀马特indent只自动增、不自动减:
if (how == "smart") {
……
indentation = Math.max(state.indented, indentation);
4. 禁止自动滚动
给安卓用,需要禁止在点击屏幕、设置光标的时候,自动发生横向滚动,以便检阅代码。
复述需求:当移动设备用户检阅代码时,很有可能正在查看长代码行,到触碰到代码行底部时,可能意外导致横向滚动归零。
此功能看起来难于实现,实则很简单:新增options.scrollOnClick,未设置时,不触发 ensureCursorVisible
即可:
function setSelectionNoUndo(doc, sel, options) {
……
if(doc.cm && doc.cm.options.scrollOnClick) // 不自动触发横向滚动,除非打字。
if (!(options && options.scroll === false) && doc.cm && doc.cm.getOption("readOnly") != "nocursor")
{ ensureCursorVisible(doc.cm); }
}
5. 重排自动完成列表,使常用关键词前置化
比如 敲v,自动列表为:
不合理。
通过进一步修改上面的 hintingFunction 来完成:
if (words.size > 0) {
var list = Array.from(words);
console.log(list);
if(list[0][0]=='v'){
var varIdx = list.indexOf("var");
if(varIdx>0) { // 如果关键词 var 排位大于零,则交换。
var tmp = list[0];
list[0] = "var";
list[varIdx] = tmp;
}
}
……
方法简单粗暴,但管用:
6. 多文档与编辑状态
像scintilla这种,是一个ui呈现,切换多份不同的文件buffer。
CodeMirror 有所不同,可以从多个 TextArea 创建多份 editor 实例,比较方便。
编辑状态:需要知晓当前文档是否可以:保存、撤销、重做等状态。
↳→→ 查看 文档是否被修改:editor.doc.isClean()
↳→→ 设置 文档已保存:editor.doc.markClean()
查看 当前修改堆栈:editor.doc.history
↳→→→ 分为 : editor.doc.history.done 撤销栈、 editor.doc.history.undone 重做栈,是否读取长度就可以了呢?不行,因为修改文本选择也会算入其中。因此需要筛选出其中的 changes:
不关键代码:
function updatePreview() {
console.log('编辑器状态 需要保存='+!editor.doc.isClean(), "可撤销="+backable(), "可重做="+forwardable())
}
7. 实战之实现安卓端 用户脚本编辑器
webview-gm 可以让普通的安卓weview也支持运行用户脚本(如via浏览器)。webview-gm-demo 项目中自带的编辑器就是一个 editortext,可以用 codemirror 改进。
实测在安卓端发现一个比较严重的BUG,如果用 \t 缩进,则用输入法打字的时候,会自动删除 \t 后面打出来的字符,而且是持续化删除,不知如何解决,已在github上反馈问题,顺便帮via浏览器出出名,锦上添花。(android 10 chars got deleted automatically & continuously sometimes #7036)
反馈问题后就睡觉了,第二天还是没消息。无奈只好自己下手。有人可能会问, – "为什么不用空格缩进呢? " – 空格缩进 太乱,有的用四个空格,有的用两个,甚至一个,不规范。而且用 tab 缩进还有一个空格缩进无法比拟的好处:如果需要将输入光标移动至行首,tab缩进的,只需点击第一个 tab 的前半部分即可;而多个空格缩进的,则需要精确点击第一个空格之前才行,实为反人类设计。
这种小问题无非是哪处开关问题。我用肉眼稍微扫描一遍代码,就已经定位问题所在:
option("inputStyle", mobile ? "contenteditable" : "textarea", function () {
通过在editor构造参数中传入 inputStyle : “textarea” 似乎即可解决问题。
然而…… 在 textarea 的编辑方式下,安卓上的文本选择没有了,无法选择代码段,这个bug一样严重。
所以……需要继续研究。需要开启打log模式,通过不断地运行、记录,来摸清代码运行路线,来定位问题症结所在。
最终,发现在 inputStyle : “contenteditable” 的编辑模式下,安卓输入法会将文本输入到 tab 元素之中,而tab元素有个 cm-text
属性,导致新输入的文本被忽略:
function walk(node) {
if (node.nodeType == 1) {
var cmText = node.getAttribute("cm-text");
if (cmText) {
addText(cmText);
return
}
<span class="cm-tab" role="presentation" cm-text=" "> h</span>
可以看到,tab 元素里面其实还是四个空格,只不过用 cm-text=" "
标明了这其实是一个 \t
符号。可以修改相关代码,取消 cm-text 的遮蔽作用。
令人欣慰的是,codemirror 竟然会检测环境中的变量,比如在 via 浏览器中是这样的:
打出来后,似乎还能进一步读取对象的属性!
codemirror 6 更进一步,默认配置就是这样的,融合了anyword hints 与 javascript hints,var关键词也处于首位,而且还有方法模板、关键词双击高亮,更像ide了。
不过 codemirror 6 首页demo里的库是混淆过的,github 仓库里代码很少,不知是怎么一个结构。