react codemirror6 及公式编辑器—001 codemirror6
### 1.背景介绍
001.web-editor三幻神
cm6:
-
体积小、高模块化、移动端支持体验好、可扩展性非常好
-
文档大示例少难懂全靠社区论坛、
-
函数式编程、比较未来可期
Monaco:
-
微软vscode核心、全面、文档示例多易读
-
体积大(打完包5M)、性能消耗大、不能移动端
ACE
哪里都根均衡,但是太旧
Ace,CodeMirror 和 Monaco:Web 代码编辑器的对比
带你全面了解 Web Editor 架构设计、技术选型
002.react&cm
-
cm版本
cm5
cm6: 更新 虚拟DOM+1、模块化+1、插件系统+1=>更好定制化
-
react-cm
uiwjs/react-codemirror | codemirror6 +react16.8+ | 新、文档多、但是新、缺少部分自定义 |
react-codemirror | codemirror5 +react16 | 老、社区资源多、但是与新版本react匹配有问题 |
2.CM6
架构
核心包:
编辑器: view
:@codemirror/view 管显示上的东西,操作交互(如:获取行信息、光标…
`state`:@codemirror/state 编辑器逻辑状态(如:自动补全、按键事件、语言包...
预置配置: commands
:@codemirror/commands (如:默认按键命令、 快捷字符操作函数…
EditorState=>EditorView=>parent: 绑定父元素 dom
原生初始化
const CodeMirror: React.FC = () =>
{
const editorRef = useRef(null);
useEffect(() =>
{
// 初始化CodeMirror编辑器
const state = EditorState.create({
doc: 'hello world!',
extensions: [
basicSetup,
],
});
const editor = new EditorView({
state,
parent: editorRef.current as any,
});
return () =>
{
editor.destroy(); // 注意:此后此处要随组件销毁
};
},[]);
return <> <div ref={editorRef}></div>
</>;
}
@uiw/react-codemirror 版
...
const editorRefUiw = useRef<ReactCodeMirrorRef>(null);
...
return <>
<ReactCodeMirror
extensions={[...]]} ref={editorRefUiw} height="200" placeholder="UIW" />
</>;
...
3.核心功能
basicSetup]: extensions 默认配置项,包含了一些比较常用的配置自动补全、高亮、主题等
state
EditorState.create({…配置属性…})
-
doc:当前编辑器内容
]: 有一些内容处理函数,直接在该类下
-
selection:当前光标
]: 其余同上
-
extension
(核心插件包)before :
优先级:extension里面的配置是有优先级的,越后越优先,优先级函数:prec、high、lowest...
扁平化:你可以在扩展之间可以互用一些扩展对象 添加-删除插件:删除=过滤出配置
内置 自定义 语言 cm6 把语言包都独立出来了,需要什么语言去官网单独下下来,导入,里面已经集成完了autocompletion(自动补全)、高亮、snippets(参数控制)、linter 自己加autocompletion、高亮、snippets(参数)、linter 主题 独立主题包、社区有资源 独立主题包、社区有资源 import {javascript} from ‘@codemirror/lang-javascript’; // 引入语言包
import {oneDark} from ‘@codemirror/theme-one-dark’;
| 见后详解 |
| 其他 | 见后详解 | |
内置实现了什么?
-
语言解析器
codemirror中有对于不同使用对象的语言解释器和对应的构造解析器:内置语言包、LRLanguage基于 Lezer 的语言解析)、StreamLanguage(基于文本流)、Tree-sitter 解析器(增量解析)
优点 | 缺点 | 注 | |
---|---|---|---|
LRLanguage /lezer语法 | 适用于复杂的编程语言解析,能够处理嵌套结构、多种语法规则等复杂场景。 codemirror6的语言包用的这个 | 1.使用:Lezer 语法文件=>LRLanguage 创建语言支持2.Lezer JavaScript 解析器生成器 (Parser Generator) 解释器 LR 上下文无关 | |
StreamLanguage /string+正则 | 小 快 简单(cm5基础) | 1.使用 2.状态机机制 类express | |
Tree-sitter /Tree-sitter 代码解析树 | 1.使用:导入编写的Tree-sitter文件=>使用 2. |
cm6的三种语言解析器 “主要介绍了LRLanguag”
[语言解析器专题codemirror6]: ./语言解析器专题codemirror6.md
LRLanguage | 源码
初始化解析配置parser(定义要解析出来的类型和一些正则匹配规则)
=>定义parser逻辑
=>LRLanguage.define()声明一些要解析的类型
->new LanguageSupport(EXAMPLELanguage) 语言支持器导出
这里匹配到关键字处理 缩进、回车、高亮
“Lezer解释器”
官方给的解释器demo
cm6-Lezer编写指南
StreamLanguage | 如何实现一个简单的语言解析器
import {StreamLanguage} from "@codemirror/language";
// startState:初始化解析状态。
// token:定义如何解析每个文本流的片段。
解析器将处理每一行并应用相应的 token 类型
const mySimpleLanguage = StreamLanguage.define({
startState() {return {};},
token(stream,state)
{
if (stream.match("//"))
{
stream.skipToEnd();
return "comment";
}
if (stream.match("error"))
{
return "error";
}
if (stream.eatWhile(/\w/))
{
return "variable";
}
stream.next();
return null;
}
});
- 解析类型及配置项
const snippets = [
/*@__PURE__*/snippetCompletion("function ${name}(${params}) {\n\t${}\n}", {
label: "function",
detail: "definition",
type: "keyword"
}),
/*@__PURE__*/snippetCompletion("for (let ${index} = 0; ${index} < ${bound}; ${index}++) {\n\t${}\n}", {
label: "for",
detail: "loop",
type: "keyword"
}),
/*@__PURE__*/snippetCompletion("for (let ${name} of ${collection}) {\n\t${}\n}", {
label: "for",
detail: "of loop",
type: "keyword"
}),...]
其他常见功能及模块
这里可以参考basicSetup,大部分会用到的功能都可以通过这里面给出的默认配置找到
- keymap
extensions: [
basicSetup,
javascript(), // 在extensions中配置语言
oneDark,
keymap.of([{
key: 'Tab',
run: (state: any) =>
{
console.log("state------------",state);
return true;// 表示事件处理完了
}
},
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap
])
],
其他各种监听器和状态更新
doc : **EditorView**.updateListener.**of**
const state = EditorState.create({
doc: 'select * from table',
extensions: [
// ...
EditorView.updateListener.of((v) => {
console.log(v.state.doc.toString()) //监测得到的最新代码
}),
],
});
-
装饰
cm6 不加建议我们硬操作cm编辑器生成的dom,建议走cm6生成,让我们用各种包装器包装]:您的代码不应尝试直接更改 CodeMirror 为其内容创建的 DOM 结构,否则将不起作用。相反,影响事物绘制方式的方法是提供装饰,这可以添加样式或用替代表示替换内容。
MatchDecorator 匹配装饰器
初始化需要配置的属性:
regexp: 匹配正则表达式 要/g
The regular expression to match against the content. Will only be matched inside lines (not across them). Should have its 'g' flag set.]: 这里实际中应该是只有匹配上的那一行会装饰器生效
decoration:应用的装饰,返回空或者一个装饰Decoration
fn(match: RegExpExecArray, view: EditorView, pos: number) → Decoration | [null
装饰有4种,可以生成不同的装饰品 标记-mark、组件DOM-widget、线条 line、替换对象 replace(将匹配内容替换而不是额外添加)
#Decoration replace({...args}) 根据参数替换给定范围进行装饰 参数: 1.widget:组件装饰(返回一个类dom的类,类型为cm6给定类型WidgetType)|line 线装饰 2.WidgetType的一些属性: constructor(构造器)、eq(额外比较器,何时生效)、toDOM(要装饰成的dom),详情见链接 const placeholderMatcher = new MatchDecorator({ regexp: /\[\[(.+?)\]\]/g, decoration: (match, view, pos) => { return Decoration.replace({ widget: new PlaceholderWidget(match[1]), }); }, });
-
行提示器(侧面显示装订线)
config 自定义行号显示
gutterLineClass 行号类
-
高亮控制、缩进、搜索匹配、撤消历史记录、lint(诊断)、matchbrackets括号匹配…
高亮:
自定义配置问题
先明确一个思路
自定义语言包(简略版)=自动补全+高亮
-
自动补全 拓展 autocompletion
form&to
form 匹配区域起始位置
to 匹配区域结束位置
pos
是从开头-起始位置,开头-结束位置,
替换语言包
语言规则及相关
用户输入参数字段:${} 或者 #{} 自定义参数顺序:默认先选中第一个${},${1} 或 ${1:defaultText} 改顺序 激活:参数被选中 ---要字段处于活动状态,用户就可以在具有 Tab 和 Shift-Tab 的字段之间移动 e.g: "function ${name}(${params}) {\n\t${}\n}"
完全覆盖 override
function myCompletions(context: CompletionContext) { let word = context.matchBefore(/\w*/) if (word.from === word.to && !context.explicit) return null return { from: word.from, options: [ // label 选取器中显示的标签 // label:在自动补全弹窗中显示的选项标签。 // type:指示该选项的类型【 class constant enum function interface keyword method namespace property text type variable 】 // info:提供有关选项的附加信息,通常用于显示在选项旁边或下方的更详细的信息。 //apply:在用户选择此选项后应用的文本。可以是一个字符串,也可以是一个函数,用于生成要应用的文本。 //detail:提供关于选项的更详细描述,通常用于提供有关选项的更多上下文信息。 {label: "match", type: "keyword"}, {label: "hello", type: "variable", info: "(World)"}, {label: "magic", type: "text", apply: "⠁⭒*.✩.*⭒⠁", detail: "macro"} ] } } const state = EditorState.create({ doc, extensions: [ // ... autocompletion({ override: [myCompletions]}) ], });
部分覆盖 改包
snippetCompletion 函数补全带snippet参数
node_modules@codemirror\lang-javascript\dist\index.cjs
... autocomplete.snippetCompletion("function ${name}(${params}) {\n\t${}\n}", { label: "function", detail: "definition", type: "keyword" }), ...
覆盖语言源override
autocompletion
语言模版规则
^keymap-
高亮
syntaxHighlighting 高亮主题配置
基础用法
import {HighlightStyle,syntaxHighlighting} from "@codemirror/language" import {tags as t} from "@lezer/highlight"; ... const myHighlightStyle = HighlightStyle.define([ { tag: t.keyword, color: "#ff0000", fontWeight: "bold" }, { tag: t.string, color: "#00ff00" }, { tag: t.comment, color: "#888888", fontStyle: "italic" }, { tag: t.variableName, color: "#0000ff" }, { tag: t.number, color: "#ff00ff" }, ]); // tag codemirror对关键字定义了一个类管理tag // HighlightStyle.define可以进行的配置class、style行内样式(style-mod版,就是react-dom的style属性)、高亮事件控制(函数,根据条件返回不同高亮样式) ... const state = EditorState.create({ doc: 'function example() {\n console.log("hello, world");\n}', extensions: [basicSetup, ...syntaxHighlighting(myHighlightStyle)] });
... [tag文档](https://lezer.codemirror.net/docs/ref/#highlight.Tag) [HighlightStyle.define配置项](https://codemirror.net/docs/ref/#language.TagStyle) `自定义关键词高亮` 语法识别+样式控制 识别=原生(修改关键词配置)|编写解释器 (编写新的解释器,改变匹配逻辑)* 样式=自定义主题|修改部分原生样式 `1.改关键词表` `2.新语言包` import { StreamLanguage } from "@codemirror/stream-parser"; import { javascript } from "@codemirror/lang-javascript"; const customKeywords = ["specialFunc", "magicNumber"];// 自定义关键词 const myHighlightStyle = HighlightStyle.define([ { tag: customKeywords, color: "#0077aa", fontWeight: "bold" } ]);// 自定义高亮样式 //实现一个简单的语言解释器 const customJavaScript = StreamLanguage.define({ startState() { return { inString: false }; }, token(stream, state) { if (stream.match("specialFunc") || stream.match("magicNumber")) { return "keyword"; } return javascript.token(stream, state); } });
问题与解决
001.语言包、语言解析器之间存在互相覆盖、冲突关系。
codemirror6会尝试协同工作。然而,如果它们之间存在冲突,通常后声明的或更具体的配置会有更高的优先级(参考优先级部分)。
语言包 语言解析器 自定义autocompletion 语言包 后面覆盖前面的 会互相覆盖关键词=>建立公共关键词表,在语言解析器 完全覆盖 语言解析器 会互相覆盖关键词=>建立公共关键词表,在语言解析器 Lezer合并,类似中间件 冲突,完全被覆盖=>建立公共关键词表,在语言解析器中提高优先级 -
如何编写自定义语言解释器]( 见 内置实现了什么? 语言解释器部分)
view
new EditorView({state,parent: editorRef.current as any, });
- *tate:EditorState
- *parent:编辑器所在父元素
- 一些相关的dom或者dom信息: dom scrollDOM contentDOM | documentTop scaleX scaleY contentHeight | 样式 lineWrapping是否换行
- 事件及监听:光标事件(moveByxxxx)、setTabFocusMode、
- 状态控制:update setstate
command
自定义linter