CodeMirror State 模块
编辑器状态
编辑器状态 state 的最基本形式由一个文档(内容)对象和一个选区对象构成,也可以通过插件对其进行扩展(在编辑器状态里添加其他字段 field)
EditorState 类
EditorState 类提供了一些静态方法进行实例化:
- static
EditorState.create(config?)静态方法:基于(可选)配置对象config(它需要符合一种 TypeScript interfaceEditorStateConfig,默认值是空对象{})创建一个新的 state 状态对象EditorStateConfig
TypeScript interface
EditorStateConfig描述编辑器状态可接受哪些配置参数doc(可选)属性:设置编辑器初始内容,它的值可以是一个字符串,会基于lineSeparator(facet 动态可配置项)的值作为分隔符,对其进行分割转换为一行行的内容;也可以是一个Text类实例selection(可选)属性:设置编辑器的初始选区,它的值可以是一个EditorSelection类的实例,也可以是一个对象{anchor: number, head?: number}(该对象的两个属性分别表示选区两端所在文档中的位置)extensions(可选)属性:设置应用到编辑器状态上的插件,它的值可以有多种形式(需要符合一种 TypeScript typeExtension)Extension
TypeScript type
Extension是一种递归类型,用于表示嵌套结构,以下称为「插件」tsexport type Extension = {extension: Extension} | readonly Extension[]它可以是一个对象或一个数组。如果是对象则它具有属性
extension值的类型也是Extension;如果是数组,元素的类型也是Extension所以属性
extensions的值是十分灵活的、具有扩展性的,开发者在使用的时候(可以将插件进行嵌套组合)不必担心嵌套层次问题,CodeMirror 最终会将它们 flatten 扁平化但该类型并没有具体指定嵌套的终点/叶子节点可以包含什么类型的值,实际上 CodeMirror 是在(核心库)内部指定/约束了一些具体的类型可以「视为」
Extension类型,它们才可以作为终点/叶子节点:- class
StateField的实例化对象视为插件 - 通过调用 class
Facet实例的相关方法可以返回插件,例如方法facet.of() - 通过调用 class
Compartment实例的相关方法可以返回插件,例如方法compartment.of() - class
ViewPlugin的实例化对象视为插件
另外还有一些类型是基于上述核心库的几种类型进行扩展,也提供视为
Extension终点/叶子节点类型的值,例如(通过Facet.define()生成)keymap扩展点,可以通过调用keymap.of()创建一个插件- class
提示
通常只在初始化编辑器时才需要创建一个全新的 state 编辑器状态对象,一般通过分发 transaction 事务(基于旧的/原有编辑器状态)创建一个新的 state
- static
EditorState.fromJSON(json, config?, fields?)静态方法:基于(可选)配置参数config(它需要符合一种 TypeScript interfaceEditorStateConfig,默认值是空对象{})将 JSON 对象json进行反序列化生成一个 state 状态对象。第三个(可选)参数field是一个对象,用于设置自定义的编辑器状态字段应该如何反序列化自定义字段
以上静态方法所解析的 JSON 对象是由编辑器状态对象调用方法
editorState.toJSON(fields?)生成的这里的(可选)参数
fields和以上静态方法的第三个(可选)参数需要相对应(更精确来讲两者应该要相同,才可以无缝地进行序列化和反序列化),都是一个对象{[prop: string]: StateField<any>}以键值对的方式表示编辑器状态对象中所含有的自定义的字段其中键名是字符串,表示自定义的字段名称,对应的值是 class
StateField实例对象,它包含了如何对该自定义字段的数据进行序列化(或反序列)
EditorState 类的实例化对象表示编辑器的一个 state 对象,以下称作「编辑器状态」
注意
编辑器状态 state 虽然是一个 JavaScript 对象,但它应该是 immutable 持久化的,即其不应该直接修改它(及其属性),而应该基于原有的状态,(通过 apply transaction 应用事务)生成一个新的状态
state 编辑器状态对象包含一些属性和方法:
doc属性:一个Text类的实例,表示当前的文档内容selection属性:一个EditorSelection类的实例,表示当前的选区field(stateFieldObj, require?)方法:获取给的stateFiledObj(该参数是一个StateField类的实例)自定义编辑器状态字段的值
第二个(可选)参数require是一个布尔值(默认值为true),以表示给定的stateFieldObj是否必须在编辑器状态对象中寻找到。如果该参数设置为true但是没有寻找到该字段,则抛出错误提示;如果该参数设置为false且没有寻找到该字段,则返回undefinedupdate(...specs)方法:接受一系列的的配置对象(剩余参数...specs将函数所接受的所有参数打包为一个数组,每一个元素都是一个对象,它们都需要符合一种 TypeScript interfaceTransactionSpec,用于描述/配置 transaction 事务)。该方法返回一个Transaction类的实例更新的合并
最终会将传入的多个配置对象的更新效果/作用进行合并,需要留意以下方面:
- 更改位置的参考点/起始点
spec 配置对象中的属性changes表示对文档内容的更改操作
当一次editor.update()更新中包含多个配置对象,而其中多个 spec 包含对文档的更改操作,默认情况下每一个 spec 的更改操作(对于修改位置的描述)都是基于当前文档的,即它们都是以当前文档内容作为共同的起始点,而不是应用前一个 spec 的更改后生成的新文档
除非将 spec 配置对象中的属性sequential设置为true,则当前 spec 的更改操作对于修改位置的描述是以(应用了前一个 spec 后所生成的)新文档为起始点 - 选区的设置
通过 spec 配置对象中的属性selection对选区进行设置
其中对于选区范围两端位置的描述,是基于应用了更改操作后(当该 spec 配置对象具有属性changes)所生成的新文档
而将更新效果/作用进行合并时,位于后面的 spec 配置对象对于选区的设置具有更高优先级,即后面的 spec 对于选区的设置会覆盖掉前面的设置 - 自定义变更效应的设置
通过 spec 配置对象中的属性effects为事务添加自定义变更效应
其中如果涉及到位置的描述,是基于应用了更改操作后(当该 spec 配置对象具有属性changes)所生成的新文档
最终生成的 transaction 事务会整合各个 spec 对于自定义编辑器状态字段的设置
- 更改位置的参考点/起始点
replaceSelection(content)方法:使用给定的内容content(该参数可以是字符串,也可以是Text类的实例),替换每一个选区选中的内容。该方法返回一个对象(需要符合一种 TypeScript interfaceTransactionSpec)提示
符合 TypeScript interface
TransactionSpec的对象用于描述/配置 transaction 事务可以将一系列这些对象作为参数,传递给编辑器状态对象的方法
editorState.update(),以创建一个 transaction 事务实例changes(spec?)方法:根据给定的更改描述spec创建一个文档更改集
(可选)参数spec是一个数组(其中每个元素都需要符合一种 TypeScript typeChangeSpec,用于描述文档的更改)
该方法最后返回一个ChangeSet类的实例 以表示对该文档内容的一系列更改操作(所创建的changeSet会将该文档的长度和分隔符考虑进去)changeByRange(func)方法:对选区的每一个 range 选区范围分别运行给定的函数func以更新它们
参数func是一个函数fn(range: SelectionRange) → { range: SelectionRange, changes?: ChangeSpec, effects?: StateEffect<any> | readonly StateEffect<any>[] }选区的每一个 range 选区范围会依次执行该函数,参数range就是当前所遍历的 range 选区范围;该函数的返回值是一个对象,其中各个属性的具体介绍如下:- 属性
range是一个SelectionRange类的实例 表示(当前所遍历的选区范围)更新后的选区范围(其中对于选区范围两端位置的描述,是基于应用了更改操作后所生成的新文档) - (可选)属性
changes是一个对象(需要符合一种 TypeScript typeChangeSpec,用于描述文档的更改),表示对于当前所遍历的选区范围进行更改(其中对于修改位置的描述,是基于当前未更改的文档) - (可选)属性
effects是一个StateEffect类的实例或一个数组(它的元素是一系列StateEffect类的实例),表示自定义变更效应
将每个选区的更新整合起来,该方法最后返回一个对象{ changes: ChangeSet, selection: EditorSelection, effects: readonly StateEffect<any>[] }它符合 TypeScript interfaceTransactionSpec(可以将它传递给编辑器状态对象的方法editorState.update(),以创建一个 transaction 事务实例)- 属性
toText(str)方法:基于将给定的str字符串(使用lineSeparatorfacet 动态可配置项所设置的分隔符,对其进行分割转换为一行行的内容)创建一个Text类实例sliceDoc(from?, to?)方法:获取给定选区范围的文档内容(字符串)。第一个(可选)参数from是一个数字,表示选区范围的开始(默认值是0表示编辑器整个文档的开头);第二个(可选)参数to是一个数字,表示选区范围的结束(默认值为this.doc.length表示编辑器整个文档的结尾)facet(facetObj)方法:获取给定的facetObjfacet 实例的输出值FacetReader
在官方文档中上述方法的 TypeScript 完整描述是
facet<Output>(facet: FacetReader<Output>) → Output所以入参值需要符合 TypeScript typeFacetReader该类型描述了一个 facet reader 动态配置项读取器
由于 class
Facetimplements 实现了 TypeScript typeFacetReader,所以这里参数实际需要传入的是一个 facet 实例toJSON(fields)方法:将当前的编辑器状态序列化为一个 JSON 对象,(可选)参数field是一个对象,用于设置自定义的编辑器状态字段应该如何序列化自定义编辑器状态字段
使用静态方法
EditorState.fromJSON(json, config?, fields?)可以将给定的jsonJSON 序列化为编辑器状态对象这里静态方法的第三个(可选)参数
fields,和以上方法的(可选)参数需要相对应(更精确来讲两者应该要相同,才可以无缝地进行序列化和反序列化),都是一个对象{[prop: string]: StateField<any>}以键值对的方式表示编辑器状态对象中所含有的自定义的字段其中键名是字符串,表示自定义的字段名称,对应的值是 class
StateField实例对象,它包含了如何对该自定义字段的数据进行序列化(或反序列)tabSize属性:一个数字,表示编辑器中制表符\t所占据的列宽 column number使用 facet 进行设置
可以通过插件对
tabSizefacet 动态可配置项进行设置,以更改上述属性lineBreak属性:一个字符串,它作为换行符使用 facet 进行设置
可以通过插件对
lineSeparatorfacet 动态可配置项进行设置,以更改上述属性readOnly属性:一个布尔值,表示编辑器是否为只读使用 facet 进行设置
可以通过插件对
readOnlyfacet 动态可配置项进行设置,以更改上述属性phrase(str, ...insert)方法:寻找给定字符串str的翻译版本(如果没有找到,则返回原字符串)
后面的可选参数(剩余参数)...insert是用于替换/填充第一个参数str字符串中的占位符(由符号$和数字构成),其中$1对应于...insert的第一个元素,$2对应于第二个元素,依次类推注意
如果
str里只需要一个占位符,则可以直接用符号$表示,而不需要加数字如果字符串
str所含有$符号不要识别为占位符,需要两个连用$$其中一个相当于转义符,则最终会在str里生成一个美元符号
例如editorState.phrase('replace $1 with $2', 'a', 'b')相当于editorState.phrase('replace a with b')翻译映射表
通过插件对
phrasesfacet 动态可配置项进行设置(输入值是一个对象,以键值对的形式为一些短语提供翻译版本),为编辑器添加翻译映射表然后调用以上方法
editorState.phrase(str)时会在预设的映射表里寻找相应的翻译版本languageDataAt(name, pos, side?)方法:在特定文档位置pos(和方向side)获取给定的类型name与编程语言相关的数据与编程语言相关的数据
如果编辑器要实现一些「智能」交互功能,例如提供 autocomplete 内容自动补全(提示),则编辑器就需要有能力在特定位置获取到与编程语言相关的数据(一般是基于上下文语境和编程语言的预定规则计算而得)
该方法可以实现上述的需求,获取特定位置给定类型的元信息
关于编程语言相关的数据可以细分为几种类型(即方法
languageDataAt()的第一个参数),例如:
该方法最后返回一个数组,每个元素(数据类型可以各异)都是于一个与编程语言相关的数据点,且这些数据点的类型是相同的(例如都是与自动补全相关的)流程解释
方法
languageDataAt用于获取元信息,而设置语言相关的元信息则是通过 facet 动态配置项languageData,它的输入值是一系列的函数,称为 language data provider,这些函数返回一个对象(以键值对的形式)表示与编程语言相关的数据在调用方法
editorState.languageDataAt()获取语言语法相关的元信息时,就会依次执行预设的一系列 providers,然后从它们的返回值(一个个对象,以键值对的形式表示与编程语言相关的数据)中提取出属性名称与参数name相同的,将属性值整合为一个数组返回具体可以参考
languageDataAt方法的源码charCategorizer(at)方法:在给定的文档位置(以限制上下文语境)创建一个字符分类器fn(char: string) → CharCategory(即该方法返回值是一个函数)
字符分类器所接收的参数char是需要分类的字符(它需要是一个单一的 grapheme cluster 字素簇,即视觉上表示一个字符,即使它可能对应多个 Unicode 字符)
字符分类器的返回值(需要是 TypeScript enumCharCategory枚举类型中的一个值)可以是CharCategory.Word或CharCategory.Space或CharCategory.Other这三个值之一Word表示该字符归类为字母数字类型(或者在当前编程语言的"wordChars"类型 language data 中显式设置的字符串)Space表示该字符归类为空白/空格字符类型Other表示该字符归类为其他类型
用途
对字符进行简单的分类,可以用于实现一些以单词为单位的操作,例如按住
Shift+Ctrl+LeftArrow向前选中一个单词wordAt(pos)方法:在给定的文档位置pos附近寻找一个单词的范围
该方法的返回值有两种类型- 如果能够在给定位置(前后)找到一个单词,则返回一个
SelectionRange类的实例(表示该单词的范围) - 如果无法在给定位置(及其相邻位置)找到单词,例如该位置前后都是空格,则返回
null
- 如果能够在给定位置(前后)找到一个单词,则返回一个
另外该类还提供其他的静态属性,它们是编辑器预设的 facet 动态配置项(可通过插件对其值进行设置)
如何使用插件设置动态配置项
通过调用动态配置项的方法 facet.of(value) 返回一个 extension 插件
该插件将给定的值 value 设置为/添加到 facet 动态配置项
然后将该插件应用到编辑器上(在实例化编辑器状态对象时,添加到配置对象的属性 config.extensions 上),就可以完成动态配置项的设置
EditorState.create({
// ...
extensions: [
EditorState.tabSize.of(16),
]
})
然后可以通过调用编辑器状态对象的相应方法 editorState.facet(EditorState.tabSize) 读取该动态配置项当前的值
- static
allowMultipleSelections静态属性:一个Facet<boolean, boolean>实例对象(它的输入值和输出值都是boolean布尔值),用于控制编辑器是否支持多选(即可否在编辑器中同时存在多个选区)注意
由于编辑器的选区是基于浏览器原生的 DOM selection 实现的,而浏览器一般最多支持 1 个 range 选区范围,只有 Firefox 允许使用
Ctrl+click(Mac 上用Cmd+click) 在文档中选择多个选区范围浏览器默认无法正常处理多个选区范围,可以通过插件来对其进行相关功能的增强
例如
@codemirror/view模块导出方法drawSelection(config),调用它可以创建一个插件,该插件将浏览器默认的选区和光标隐藏,以支持多选区范围的显示 - static
EditorState.tabSize静态属性:一个Facet<number, number>实例对象(它的输入值和输入值都是number数值),用于配置 tab size 制表符\t的大小,即占据的列宽 column number(默认值为4)。如果多个插件对该动态配置项进行了设置,则采用优先级最高的插件所设置的值 - static
EditorState.lineSeparator静态属性:一个Facet<string, string | undefined>实例对象(它的输入值是string字符串,输出值可以是string字符串或undefined无定义值,如果返回的是undefined则不进行换行 ❓ ),用于配置行分隔符(即将编辑器文档/字符串分割转换为一行行的内容)。
默认情况下"\n"(换行符)、"\r"(回车符)、"\r\n"(回车符+换行符)都会被视为分隔符(需要在该位置进行分割为新一行),然后 CodeMirror 使用"\n"(换行符)将每一行连起来
如果使用插件显式地设置该动态配置项(以设置特定的分隔符),则 CodeMirror 只会识别特定的分隔符,这样可以让编辑器忽略那些默认分隔符,以尽可能地保留文档的原始格式(而不是将它们都标准化为"\n"换行符) - static
EditorState.readOnly静态属性:一个Facet<boolean, boolean>实例对象(它的输入值和输出值都是boolean布尔值,默认值输出值为false即可以编辑),用于控制编辑器是否只读(内容不可修改编辑)区分
编辑器的视图也提供一个类似的动态配置项,但是它与上述的动态配置项是有区别的
- static
EditorState.readOnly编辑器状态的静态属性:一个Facet<boolean, boolean>实例对象(它的输入值和输出值也是boolean布尔值),以控制编辑器是否只读
如果该动态配置项设置为true,则对于编辑器内容的任何修改操作都无法执行,包含用户通过与(编辑器在页面上所对应的)DOM 元素的直接交互,或(通过点击菜单栏按钮)分发指令的间接操作都会被禁止 - static
EditorView.editable编辑器视图的静态属性:一个Facet<boolean, boolean>实例对象(它的输入值和输出值也是boolean布尔值),表示编辑器在页面上所对应的 DOM 元素是否添加/移除contenteditable属性,以控制用户是否可以通过直接与编辑器交互的方式来修改编辑内容
如果该动态配置项设置为false,则编辑器在页面上所对应的 DOM 元素就不会带有contenteditable属性,即用户无法直接与编辑器进行交互,但是依然可以(通过点击菜单栏按钮或按下快捷键)分发指令来间接修改编辑器的内容
- static
- static
EditorState.phrases静态属性:一个Facet<Object<string>>实例对象(它的输入值是一个对象,以键值对的形式为一些短语提供翻译版本),让编辑器可以有更好 i18n 本地化体验本地化
编辑器通过上述的 facet 动态配置项,可以很方便地对单词、短语、句子进行翻译,让编辑器实现本地化
ts// 编辑器状态的静态属性 `EditorState.phrases` 是一个 facet 动态配置项实例 // 调用该实例的方法 facet.of(value) 创建一个插件,将给定的值添加到该动态配置项上 const translateExtension = EditorState.phrases.of({ // 将以下英文短语翻译为中文 "Selection deleted": "删除选中的内容" }) // 然后将插件应用到编辑器上(在实例化编辑器状态对象时),就可以完成对该短语的翻译 EditorState.create({ extensions: [translateExtension] }) // 以上短语 `"Selection deleted"` 出现在 @codemirror/commands 模块中 // 在执行删除选区内容的操作时,它会用于生成描述性内容,以供屏幕阅读器读取 // 相关代码是 `state.phrase("Selection deleted")` // 通过 editorState.phrase(str) 方法来查看给定的字符串是否有对应的翻译, // 完整源码参考 https://github.com/codemirror/commands/blob/main/src/commands.ts#L456可以查看官方的相关例子 Example: Internationalization,完整代码可以查看 Github 仓库
在很多官方模块中都有使用到该 facet 动态配置项,这样就可以让开发者对这些编辑器的内置短语提供翻译版本,例如
@codemirror/search模块会在页面生成一个搜索栏,在各种 UI 控件上会有相应的短语以表示它们的功能作用,这些短语都使用了上述的 facet 进行「封装」,以支持开发者对其进行翻译 - static
EditorState.languageData静态属性:一个Facet<fn(state: EditorState, pos: number, side: -1 | 0 | 1) → readonly Object<any>[]>实例对象(它的输入值是一个函数,并没有显式地设置输出值),用于设置如何(在文档的特定位置)获取与编程语言相关的数据facet 的默认输出值
class
Facet<Input, Output = readonly Input[]>如果没有并没有显式地设置输出值,则默认将一系列插件所设置的输入值(作为元素)都封装到一个数组里直接返回获取与编程语言相关的数据
如果编辑器要实现一些「智能」交互功能,例如提供 autocomplete 内容自动补全(提示),则编辑器就需要有能力在特定位置获取到与编程语言相关的数据(一般是基于上下文语境和编程语言的预定规则计算而得)
上述的 facet 动态配置项正是用于实现这些需求的
通过插件设置上述的 facet 动态配置项,其输入值是一个函数,其中参数
pos和side可以知道所需分析的位置、方向;其输出值是一个数组,包含一些编程语言相关的信息官方在
@codemirror/language模块对上述的 facet 进行了设置,可以根据当前代码编辑器的内容所属的编辑器语言,获取与编程语言相关的数据也可以通过调用方法
EditorState.languageData.of()创建一个插件,添加其他额外的方式获取与编程语言相关的数据,例如EditorState.languageData.of(() => globalLanguageData)对于文档的任意位置都执行给定的函数,获取与编程语言相关的信息
输入值是一个函数fn(state: EditorState, pos: number, side: -1 | 0 | 1)所接受三个参数具体说明如下:- 第一个参数
state是一个编辑器的状态对象 - 第二个参数
pos是一个数值,表示编辑器文档的位置 - 第三个参数
side可以是-1或0或1这三个数值的任意一个,分别表示所需获取数据相对于给定位置的之前方向、当下方向、之后方向
该函数最后返回一个对象数组,在对象里以键值对的形式表示与编程语言相关的数据
输出值是一个数组(它是对输入值的封装),每个元素都是一个函数(集输入值所设置的函数)具体使用
通过插件可以为上述 facet 动态配置项设置一系列的函数,它们称为 language data provider,即执行这些函数都会返回一个对象(以键值对的形式)表示与编程语言相关的数据
调用编辑器状态实例的方法
editorState.languageDataAt(name: string, pos: number, side?: -1 | 0 | 1 = -1)就会依次执行这一系列 providers,然后从它们的返回值(一个个对象,以键值对的形式表示与编程语言相关的数据)中提取出属性名称与参数name相同的,将属性值整合为一个数组返回具体可以参考
languageDataAt方法的源码 - 第一个参数
- static
EditorState.changeFilter静态属性:一个Facet<fn(tr: Transaction) → boolean | readonly number[]>实例对象,对每次分发的 transaction 事务中所包含 changes 对文档的更改操作进行过滤(决定哪些更改操作需要执行,而哪些更改操作需要禁止)
通过插件对上述 facet 动态配置项进行设置,为编辑器注册一个 change filter 文档更改操作过滤器
输入值是一个函数fn(tr: Transaction)它的参数就是当前分发的事务对象注意
每次编辑器 dispatch transaction 分发事务时,上述 facet 所设置的函数都会执行,以对该事务里的更新操作进行过滤处理
除非在该事务的配置对象中将属性
filter设置为false,则该事务会跳过所有过滤器的检查,即上述 facet 并不会对该事务进行处理
输出值可以是两种类型:- 可以是一个
boolean布尔值,如果是true表示不进行过滤处理(即该过滤器会对当前事务所包含的所有文档修改操作「放行」,而它们最终是否会被执行,还需要考虑其他过滤器的结果),如果是false表示不允许对文档进行改变(即该事务中对文档的修改操作不可执行) - 可以是一个数组,其元素都是数字,它们每两个视为一组,以表示文档的一个选区范围 range,在这些选区范围里对文档的更改操作会被禁止。例如
[10, 20, 100, 110],表示不允许触及文档位置从10到20选区范围,以及从100到110选区范围内的更改被执行
- 可以是一个
- static
EditorState.transactionFilter静态属性:一个Facet<fn(tr: Transaction) → TransactionSpec | readonly TransactionSpec[]>实例对象,可以在相应的 transaction 应用到编辑器之前,对其配置进行更新或替换
通过插件对上述 facet 动态配置项进行设置,为编辑器注册一个 transaction filter 事务过滤器(对事务的配置进行更新或替换)注意
应该谨慎使用该 facet,随意修改事务可能会引起问题,或对用户体验产生负面影响
输入值是一个函数fn(tr: Transaction)它的参数就是当前分发的事务对象注意
每次编辑器 dispatch transaction 分发事务时,上述 facet 所设置的函数都会执行,以对该事务里的更新操作进行过滤处理
除非在该事务的配置对象中将属性
filter设置为false,则该事务会跳过所有过滤器的检查,即上述 facet 并不会对该事务进行处理
输出值有两种形式- 可以是一个对象(需要符合一种 TypeScript interface
TransactionSpec) - 可以是一个数组(它的每个元素都是对象,也需要符合一种 TypeScript interface
TransactionSpec)
提示
符合 TypeScript interface
TransactionSpec的对象可用于描述/配置 transaction 事务可以将一系列这些对象作为参数,传递给编辑器状态对象的方法
editorState.update(),以创建一个 transaction 事务实例不推荐
在过滤器中应该避免访问事务实例的属性
state,它是应用该事务后生成的新的编辑器状态对象,但该 state 是按需生成,并且可能在之后被丢弃(如果该事务被其他过滤器禁止执行) - 可以是一个对象(需要符合一种 TypeScript interface
- static
transactionExtender静态属性:一个Facet<fn(tr: Transaction) → Pick<TransactionSpec, "effects" | "annotations"> | null>实例对象,和前面的 facettransactionFilter作用类似,可以在相应的 transaction 应用到编辑器之前,对其配置进行更新(增添annotations和/或effects属性)
通过插件对上述 facet 动态配置项进行设置,为编辑器注册一个 transaction extender 事务扩展器(对事务的配置进行更新)
输入值是一个函数fn(tr: Transaction)它的参数就是当前分发的事务对象注意
每次编辑器 dispatch transaction 分发事务时,即使在该事务的配置对象中将属性
filter设置为false,上述 facet 所设置的函数都会执行所以该 facet 会对所有事务进行处理
如果编辑器同时设置了
transactionFilterfacet 和上述的transactionExtenderfacet,则会先执行前者所设置的函数,再执行后者所设置的函数
输出值有两种形式- 可以是一个对象,只能包含属性
annotations和/或effects(这两个属性的类型约束摘取自 TypeScript interfaceTransactionSpec里) - 可以是
null
区别
虽然和
transactionFilterfacet 作用类似,都会在事务应用到编辑器之前对其进行处理但是通过该 facet 所设置的扩展器对 transaction 的更新的范围是受到限制的,它不会触及/影响关于文档内容或选区的变更,只能用于为事务添加
annotations元信息和设置effects自定义变更效应所以该 facet 输出值的其中一种形式是对象,只能包含
annotations和/或effects属性 - 可以是一个对象,只能包含属性
编辑器配置可以动态修改,在下面的子标题里进行介绍
Compartment 类
compartment 是一个配置隔离区(实际是一个插件容器,所包含的插件会带有与编辑器配置相关的信息),其中所包含的配置可以在编辑器初始化之后,通过分发 transaction 进行修改
插件
通过方法 new Compartment() 进行实例化,得到一个 compartment 对象,以下称作「配置隔离区」
实例化的简化形式
class Compartment 该类并没有设置 constructor 构造函数,所以实例化时并不需要传递参数,则实例化方法可以简写为 new Compartment
但是更推荐带括号的形式,这更符合编程习惯
Compartment 类实例化对象 compartment 配置隔离区,包含一些属性和方法:
of(ext)方法:使用该配置隔离区包裹给定的ext插件
参数ext需要符合一种 TypeScript type Extension
该方法返回一个对象(它也需要符合 TypeScript typeExtension),可以将其视为经过配置隔离区封装的插件,然后将该插件应用到编辑器上(在实例化编辑器状态对象时,添加到配置对象的属性config.extensions上)reconfigure(content)方法:返回一个StateEffect类的实例,该 state effect 会将该配置隔离区的内容设定为content(它需要符合一种 TypeScript type Extension),对相应的配置进行重配置/更新自定义变更效应
自定义变更效应 state effect 用于为 transaction 事务添加附加作用/效果
可以通过手动创建 transaction 事务,并在其配置对象的属性
effects里设置该配置隔离区所生成的 state effect这种方式十分灵活,可以选择更新的时机
ts// myCompartment 是配置隔离区对象 // newExtension 是包含新配置值的插件 view.dispatch({ effects: myCompartment.reconfigure(newExtension) })也可以采用 transaction extender 扩展事务的方式,往已有的事务中添加该配置隔离区所生成的 state effect
这种方式更清晰便于管理,因为更新时机是依赖于其他事务(而不是创建一个新的事务)
ts// myCompartment 是配置隔离区对象 // newExtension 是包含新配置值的插件 // myTransactionExtender 变量是一个 facet 实例,作为 extension 插件添加到编辑器上 const myTransactionExtender = EditorState.transactionExtender.of(tr => { // ... return { effects: myCompartment.reconfigure(newExtension) } }) EditorState.create({ // ... extends: [myTransactionExtender] })
当调用方法compartment.reconfigure()进行重配置时,该配置隔离区原本包含的插件(以及它所包含的内嵌插件,以及可能包含的内嵌 compartment)会删除掉,然后替换上新的插件。可以传入空数组compartment.reconfigure([])删除原有的插件,而不引入新插件(即把相关配置从编辑器里移除)get(state)方法:基于当前编辑器状态state获取该配置隔离区的内容
该方法返回一个对象(需要符合一种 TypeScript type Extension),表示该配置隔离区当前所包含的插件;如果未设置插件,则返回undefined
compartment 的创建和使用流程
- 使用方法
new Compartment()进行初始化,得到一个compartment配置隔离区对象 - 使用方法
compartment.of(extension)包裹插件 - 使用方法
compartment.reconfigure(newExtension)创建一个 state effect,添加到 transaction 事务的属性effects上,通过分发事务来更新配置
例如让 tab size 缩进量相关的配置可以动态更改
import {basicSetup, EditorView} from "codemirror"
import {EditorState, Compartment} from "@codemirror/state"
let tabSize = new Compartment();
let state = EditorState.create({
extensions: [
basicSetup,
tabSize.of(EditorState.tabSize.of(8))
]
})
let view = new EditorView({
state,
parent: document.body
})
// 通过调用该方法,可以创建并分发一个 transaction 事务,以更新 tabSize 配置
function setTabSize(view, size) {
view.dispatch({
effects: tabSize.reconfigure(EditorState.tabSize.of(size))
})
}
对比
compartment 和 facet 都可以更新编辑器的配置值
compartment 通过手动分发 transaction 以更新相应的配置
facet 通过方法 facet.compute()、方法 facet.computeN() 或方法 facet.from() 让输入值自动随编辑器状态动态变化(相应地输出值也可能随之改变)
根据不同的需求和场景选择合适的方式来更新编辑器的配置:
- 如果需要在编辑器初始化以后再设定配置的更新逻辑,则可以使用 compartment,更新逻辑通过 state effect 添加到分发事务中,它的自由度十分高,可以随时手动设置配置的值
- 如果需要依赖编辑器状态进行配置更新,则可以使用 facet,显式指定所需监听/依赖的值,会自动按需更新,它更高效
CodeMirror 提供一些方式以扩展编辑器状态,在下文(各个子标题里)分别进行介绍
Facet 类
facet 是一个与编辑器状态相关的对象(可以通过编辑器状态对象的相应方法 editorState.facet(facetObj) 读取到相应 facet 实例的输出值)
提示
可以将 facet 视为全局变量,在编辑器或插件的任何地方使用
它支持多个插件对其值进行设置(即接受多个输入),然后将这些设置进行整合得到一个输出结果,这样就可以对编辑器状态进行自由扩展
输出结果
facet 最终输出的结果形式根据各自内部的整合方式不同而不同,具体而言可以分为两种情况:
- 可以是一个值(仅有一个元素),一般是仅保留优先级最高的插件所提供的值,例如有多个插件都通过 facet
tabSize为编辑器设置缩进量,但是最终该 facet 只会输出一个数值 - 可以是一个数组(有多个元素),一般是将插件所提供的值整合到一个数组里,并按照插件的优先级别这些值进行排序,例如多个插件通过 facet
updateListener为编辑器设置更新监听器,最终该 facet 会输出一个数值,包含一系列监听器(的响应函数),它们会在编辑器更新时依次执行
如何使用插件设置动态配置项
通过调用动态配置项的方法 facet.of(value) 返回一个 extension 插件
该插件将给定的值 value 设置为/添加到 facet 动态配置项
然后将该插件应用到编辑器上(在实例化编辑器状态对象时,添加到配置对象的属性 config.extensions 上),就可以完成动态配置项的设置
例如对于编辑器内置的 tabSize facet,可以进行如下设置
EditorState.create({
// ...
extensions: [
EditorState.tabSize.of(16),
]
})
然后通过调用编辑器状态对象的相应方法 editorState.facet(EditorState.tabSize),读取该 facet 当前的输出值
说明
即使某个 facet 并没有采用以上方式通过插件进行设置,也可以通过方法 editorState.facet(anotherFacet) 来读取到它的默认值
优先级
@codemirror/state 模块所导出了一个对象 Prec,使用该对象的 5 种方法之一封装插件 extension,就可以调整插件的优先级
- 方法
highest(ext: Extension) → Extension封装给定的插件,具有最高优先级 - 方法
high(ext: Extension) → Extension封装给定的插件,具有较高优先级级 - 方法
default(ext: Extension) → Extension封装给定的插件,默认优先级 - 方法
low(ext: Extension) → Extension封装给定的插件,具有较低优先级 - 方法
lowest(ext: Extension) → Extension封装给定的插件,具有最低优先级
通过调用静态方法 Facet.define(config?) 进行实例化,创建一个新的 facet 对象,以下称为「动态配置项」
输入值和输出值
静态方法的 TypeScript 完整描述是 static define<Input, Output = readonly Input[]>(config?: Object = {}) → Facet<Input, Output>
则表示如果 facet 没有显式地设置输出值,则默认将一系列插件为该 facet 所设置的输入值(作为元素)都封装到一个数组里直接返回
其中(可选)参数 config 是一个对象,用于配置该 facet,它包含一系列属性,具体说明如下:
combine(可选)属性:一个函数fn(value: readonly Input[]) → Output用于设置该 facet 如何将多个(插件所设置的)输入值整合为一个输出值
函数的参数value是一个数组,包含一系列的输入值。最后该函数的返回值作为该 facet 的输出值
如果省略该属性,则直接将包含输入值的数组返回(作为该 facet 的输出值)说明
在实例化 facet 时,该属性所设置的函数会立即执行,则函数会接收一个空数组作为入参(因为此时还没有使用插件对 facet 进行设置),以计算出 facet 的默认输出值(在没有任何的输入值的情况下)
combineConfig 方法
@codemirror/state 模块导出了一个辅助函数
combineConfig(config, defaults, combine?)可以更简便地设置如何将多个输入值(对象)整合为一个输出值(对象,以下称作Config最终配置对象)该方法常用于实例化 facet 时,设置如何整合多个输入值,例如应用于 @codemirror/autocomplete 模块
该函数的参数具体说明如下:
- 第一个参数
config:它是一个数组,它的每个元素都是一个对象,一般只包含最终配置对象Config的部分属性 - 第二个参数
defaults:它是一个对象,推荐只包含最终配置对象Config的可选属性,通过该参数为这些可选属性设置默认值 - 第三个(可选)参数
combine是一个对象,以键值对的方式[P in keyof Config]: fn(first: Config[P], second: Config[P]) → Config[P]为不同属性设置整合方式
即键名是最终配置对象Config的某一个属性名,对应的值为一个整合函数fn(first: Config[P], second: Config[P]) → Config[P]它的入参first和second分别表示该属性的两个值,在该函数内部的编写代码决定如何将两者整合输出为一个值,作为该属性在最终配置对象的值
如果某一个属性获得两个不相同的值,但是没有在combine里设置相应的整合函数,则默认行为是抛出错误
- 第一个参数
compare(可选)属性:一个函数fn(a: Output, b: Output) → boolean用于设置该 facet 如何比较两个输出值(参数a和b是需要比较的值,所以这两个参数的类型都要符合该 facet 的输出值格式),最后返回一个布尔值,以判断该 facet 是否发生了变化
默认情况下,直接使用三等号===对两个传入的参数值进行比较;如果前面的属性combine没有进行设置,则会直接将包含输入值封装为一个数组返回,即参数a和b都会是数组,则默认会分别对它们的相应元素一一进行比较(也是使用===三等符号)compareInput(可选)属性:一个函数fn(a: Input, b: Input) → boolean用于设置该 facet 如何比较两个输入值,最后返回一个布尔值,以判断该 facet 是否发生了变化
该方法和前一个属性compare类似,也是用于判断该 facet 是否发生变化,但判断的起点是直接基于不同的输入值来,默认情况下,直接使用三等好===对两个传入的参数值进行比较(可以避免即使两个参数/输入值是相同的情况下,还执行一遍 facet 的相关逻辑计算出相应的输出值,再进行对比)static(可选)属性:一个布尔值,以表示该 facet 是否为静态的(即它的输出值并不能随 state 变化而动态更改的)enables(可选)属性:用于设置该 facet 所关联/依赖的插件
该属性的值可以有多种形式:- 可以是一个对象或数组(需要符合一种 TypeScript type
Extension) - 可以是一个函数
fn(self: Facet<Input, Output>) → Extension接受当前 facet 作为参数,返回值也是一个对象或数组(需要符合一种 TypeScript typeExtension)
提示
当编辑器使用该 facet 时(例如通过方法
facet.of(value)得到一个 extension 插件,然后在实例化编辑器状态对象时,将该插件作为/添加到配置对象的extensions属性上),该属性所设置的一系列插件会自动应用到编辑器上例如在 @codemirror/language 模块所提供的
languagefacet 动态可配置项就预设了一系列的依赖插件- 可以是一个对象或数组(需要符合一种 TypeScript type
Facet 类实例化对象 facet 动态配置项,包含一些属性和方法:
reader属性:返回该 facet 对象自身。它符合 TypeScript typeFacetReader,表示它只能用于读取该 facet 的输出值,但不能为该 facet 设置新的值FacetReader
TypeScript type
FacetReader描述了一个 facet reader 动态配置项读取器注意
虽然在官方文档里指出符合该类型的对象具有
tag属性,该属性值的类型是所关联的 facet 的输出值类型但这只是一个 TypeScript 的 Dummy tag 「虚拟标签」(即没有实际用途的属性),以确保 TypeScript 不会将任意对象都视为符合
FacetReader即属性
tag只是用于 TypeScript 编译系统中,以提升类型系统的准确性(让 TypeScript 更好地捕获类型错误,实现类型安全),而在运行时实际的 JS 对象上(即 facet 实例)并不存在TypeScript type
FacetReader所描述的对象可以作为以下方法的参数(或其参数的组成部分),以读取相关联的 facet 实例的输出值(而不是为 facet 设置新的值):- 方法
editorState.facet<Output>(facet: FacetReader)其参数需要符合 TypeScript typeFacetReader(实际就是要传入一个 facet 实例) - 方法
compute(deps: readonly (StateField<any> | "doc" | "selection" | FacetReader<any>)[], get: fn(state: EditorState) → Input)或方法facet.compteN(deps: readonly (StateField<any> | "doc" | "selection" | FacetReader<any>)[], get: fn(state: EditorState) → readonly Input[])其中参数deps是一个数组,其元素可以是符合 TypeScript typeFacetReader的对象(实际就是要一个 facet 实例)
class
Facet需要 implements 实现一种 TypeScript typeFacetReader,所以任何需要采用FacetReader类型的地方都可以填入 facet 实例- 方法
of(value)方法:该方法返回一个符合 Typescript type Extension 的值,它作为插件(在实例化编辑器状态对象时,需要将插件应用到编辑器上)用于将给定的value设置为该 facet 的输入值compute(deps, get)方法:该方法返回一个符合 Typescript type Extension 的值,它作为插件(在实例化编辑器状态对象时,需要将插件应用到编辑器上)可以让该 facet 的输出值随编辑器状态的改变而动态变化
通过参数deps设置所需监听的一系列值,然后只有在它们发生变化时,才会执行参数get所设置的函数,基于新的 state 编辑器状态对象重新计算出该 facet 的输入值,相应地输出值也可能随之改变
该方法的参数具体介绍如下:- 参数
deps用于设置所需监听的值(它们是 state 编辑器状态对象中的某些属性),它是一个数组readonly (StateField<any> | "doc" | "selection" | FacetReader<any>)[]其元素有多种类型- 可以是
StateField类的实例,即自定义的编辑器状态字段 - 可以是符合 TypeScript type
FacetReader的对象,实际就是 facet 实例FacetReader
在官方文档中上述方法的 TypeScript 完整描述是
facet<Output>(facet: FacetReader<Output>) → Output所以入参值需要符合 TypeScript typeFacetReader该类型描述了一个 facet reader 动态配置项读取器
由于 class
Facetimplements 实现了 TypeScript typeFacetReader,所以任何需要采用FacetReader类型的地方都可以填入 facet 实例 - 可以是字符串
"doc"或"selection"分别表示监听整个文档内容或选区的变化
- 可以是
- 参数
get是一个函数fn(state: EditorState) → Input用于设置如何从新的 state 编辑器状态对象计算出该 facet 的输入值
提示
如果所需监听的值只有一个 stateField 自定义的状态字段,可以使用 shorthand method 简写方法
facet.from(field, get)- 参数
computeN(deps, get)方法:该方法和前一个compute()方法类似,也是返回一个插件,让该 facet 的输出值随编辑器状态的改变而动态变化
参数deps和前一个compute()方法一样,用于设置置所需监听的值
不同的是通过参数get所设置的函数fn(state: EditorState) → readonly Input[]其返回值是一个数组(每个元素都是该 facet 的输入值),基于新的 state 编辑器状态对象计算出该 facet 多个输入值from(field, get?)方法:它是前述compute()方法的 shorthand method 简写方法。
参数field是一个StateField类的实例,它是所需监听的自定义的状态字段
(可选)参数get是一个函数fn(value: T) → Input用于设置如何从新的 state field 自定义编辑器状态对象(在更新 state 编辑器状态对象时,自定义状态字段会同步更新)自动计算出该 facet 的输入值。如果将参数field的值直接作为 facet 的新输入值,则参数get可以省略
预设的动态配置项
CodeMirror 在官方模块预设了一些 facet 动态配置项,它们与各个官方模块中特定功能相关,而且支持开发者通过插件对它们进行配置
@codemirror/state 模块导出的 facet 实例(以便通过插件对编辑器状态的相关配置进行设置),它们都是作为 EditorState 类的静态属性:
- static
EditorState.allowMultipleSelections控制编辑器是否支持多选 - static
EditorState.tabSize设置 tab size 缩进量 - static
EditorState.lineSeparator设置行分隔符 - static
EditorState.readOnly控制编辑器是否只读 - static
EditorState.phrases为一些短语提供翻译版本 - static
EditorState.languageData用于设置如何(在文档的特定位置)获取与编程语言相关的数据 - static
EditorState.changeFilter对每次分发的 transaction 中所包含 changes 进行过滤 - static
EditorState.transactionFilter在相应的 transaction 应用到编辑器之前,对其配置进行更新或替换 - static
EditorState.transactionExtender在相应的 transaction 应用到编辑器之前,对其配置进行更新(增添 annotations 和/或 effects 属性)
@codemirror/view 模块导出的 facet 实例(以便通过插件对编辑器视图进行设置),其中一些作为 EditorState 类的静态属性,一些是其他类的实例对象的属性,或直接就是作为变量导出:
- static
EditorView.styleModule - static
EditorView.inputHandler - static
EditorView.scrollHandler - static
EditorView.focusChangeEffect - static
EditorView.perLineTextDirection - static
EditorView.exceptionSink - static
EditorView.updateListener - static
EditorView.editable - static
EditorView.mouseSelectionStyle - static
EditorView.dragMovesSelection - static
EditorView.clickAddsSelectionRange - static
EditorView.decorations - static
EditorView.outerDecorations - static
EditorView.atomicRanges - static
EditorView.bidiIsolatedRanges - static
EditorView.scrollMargins - static
EditorView.darkTheme - static
EditorView.cspNonce - static
EditorView.contentAttributes - static
EditorView.editorAttributes keymapgutterLineClassgutterWidgetClasslineNumberMarkerslineNumberWidgetMarkershowTooltipshowPanel
@codemirror/language 模块预设了一些 facet 实例(以便通过插件对与编程语言数据的相关配置进行设置),直接作为变量导出:
languagefoldServiceindentServiceindentUnit
@codemirror/commands 模块预设了一个 facet 实例(以便通过插件对与指令相关的配置进行设置),直接作为变量导出:
invertedEffects
@codemirror/autocomplete 模块预设了一个 facet 实例(以便通过插件对与自动填充相关的配置进行设置),直接作为变量导出:
snippetKeymap
StateField 类
state field 是自定义的编辑器状态字段,它随 state 同步更新
注意
state field 用于扩展 state 编辑器状态,它也是 immutable 持久化的,即不应该直接修改它(及其属性)
它们会在编辑器状态(通过 apply transaction 应用事务)更新时,自动调用相应的 update() 方法进行更新
通过调用静态方法 StateField.define(config?) 进行实例化,创建一个新的 stateField 对象
如何使用
stateField 对象被视为一个 extension 插件
在实例化编辑器状态对象时,添加到配置对象的属性 config.extensions 上,就可以将该自定义编辑器状态字段添加到编辑器里
其中(可选)参数 config 是一个对象,用于配置该 state field,它包含一系列属性具体说明如下:
create(state)方法:为该 state field 设置初始值。传入的参数state是当前编辑器状态对象。
该方法最后返回的值的类型需要和后一个方法update()返回值的类型一致update(value,transaction)方法:基于该 state field 的旧值value和编辑器当前所分发的transaction事务,计算出新值
该方法最后返回的值的类型需要和前一个方法create()返回值的类型一致compare(可选)属性:一个函数fn(a: Value, b: Value) → boolean用于设置如何比较该 state field 的两个值a和b,最后返回一个布尔值,以判断它们是否相同( ❓ 语义上的相同,而不一定是两个值完全相同)
该方法用于避免(依赖于该 state field 的)facet 的无效计算
默认使用三等号===对传入的参数值进行比较provide(可选)属性:一个函数fn(field: StateField<Value>) → Extension用于设置依赖于该 state field 的相关插件
该函数的入参field是当前 state field 实例
最后返回一个对象或数组(需要符合一种 TypeScript type Extension)自动加载相关插件
当编辑器使用该 state field 时(在实例化编辑器状态对象时,将它作为插件添加到配置对象的
extensions属性上),该属性所设置的一系列插件会自动应用到编辑器上例如有一个 facet
myFacet需要依赖 state fieldmyStateField,则可以进行如下设置tsconst myFacet = Facet.define(); const myStateField = StateField.defined({ create() { // ... }, update() { // ... } provide: field => { return myFacet.from(field, fieldValue => { // ... // return the new Input value for myFacet }) } })然后只需要将
myStateField应用到编辑器上,则 facetmyFacet也会自动加载(而不需要显式手动添加)tsconst state = EditorState.create({ extensions: myStateField })官方模块所提供的一些 state field 也设置了需要自动加载的插件,例如在 @codemirror/autocomplete 模块所提供的
completionStatestate field 就提供了一系列需要同时加载插件toJSON(可选)属性:一个函数fn(value: Value, state: EditorState) → any将该 state field 序列化为一个 JSON 对象。第一个参数value是该 state field 当前的值,第二个参数state是编辑器状态对象使用场景
当编辑器状态对象调用方法
editorState.toJSON(fields)时,如果(可选)参数fields(一个对象,以键值对的方式表示编辑器状态对象中所含有的自定义的字段)里包含一些 state field,则这些 state field 的toJSON方法才会被调用fromJSON(可选)属性:一个函数fn(json: any, state: EditorState) → Value将给定的 JSON 对象json进行反序列化,生成一个值(其类型和 state field 的值的类型相同)。第二个参数state是编辑器状态对象使用场景
当使用编辑器状态的静态方法
EditorState.fromJSON(json, config?, fields?)时,如果(可选)参数fields(一个对象,以键值对的方式表示编辑器状态对象中所含有的自定义的字段)里包含一些 state field,则这些 state field 的fromJSON方法会被调用,将给定的 JSON 对象json中相应部分进行反序列化,生成一个值,作为相应的 state field 的初始值
StateField 类实例化对象 stateField,以下称为「自定义的编辑器状态字段」
stateField 自定义的编辑器状态字段包含一些方法和方法:
init(createFn)方法extension属性
提示
一般 transaction 事务只包含 state 编辑器状态对象所发生的更改(文档内容或选区的变化)的描述,可以通过为事务设置 annotation 元信息和/或 state effect 自定义变更效应,为 state field 的更新添加更多的相关信息
facet 和 state field 的区别
facet 和 state field 都可以作为扩展 state 编辑器状态的方式
facet 支持(通过一系列插件)多次配置,有很大灵活性
state field 虽然无法多次配置,但它(设计的初衷)原生支持随 state 编辑器状态同步变化(避免出现状态不一致的问题)
提示
虽然可以通过 state effect 为 transaction 事务添加额外信息,然后传递给 state field 的更新函数,但是并不能实现复杂的配置
虽然 facet 也可以通过方法 facet.compute()、方法 facet.computeN() 或方法 facet.from() 让输出值实现随编辑器状态动态变化,但需要显式指定所需监听的值
根据不同的需求和场景选择合适的方式来扩展 state 编辑器状态:
- 如果需要支持灵活配置,则使用 facet
- 如果需要与编辑器状态/文档内容同步更新,则使用 state field
文档内容
CodeMirror 使用 Text 类来描述文档数据结构,除了可以按字符偏移量描述内容,还可以按行数来描述,将原本扁平化的纯字符串内容,转变带层级的树形结构,这样就可以在访问和遍历文档的部分内容时,进行高效索引,不必处理大量的字符串
索引
使用字符偏移量描述文档内容时,索引值从 0 开始,将 UTF-16 编码的字符计作 1 个 unit,而 astral characters 超出 Unicode 标准 BMP 范围的字符(范围为 U+0000 到 U+FFFF)计作 2 个 unit,例如 🤩 表情符号(其 Unicode 编码为 U+1F929),而且每一个换行符算作 1 个 unit(即使通过编辑器相关配置将换行符设置为多个字符串的组合,例如 "\r\n" 回车符+换行符,也将它们视为整体算作 1 个单位的偏移量)
使用行数描述文档内容时,索引值从 1 开始
Text 类
Text 类用于描述文档内容和结构
可迭代协议
class Text implements 实现了 iterable protocol 可迭代协议
可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为,该对象必须实现 [Symbol.iterator]() 方法(即该对象必须有一个键为 [Symbol.iterator] 的属性),其返回值为一个符合 iterator protocol 迭代器协议的对象。
可以通过循环结构,例如 for...of 语句,迭代该类的实例化对象的值
该类提供了一些静态方法和属性进行实例化:
- static
Text.of(strArr)静态方法:基于给定的一系列字符串创建一个text对象。参数strArr是一个数组,其元素都是字符串,分别表示文档中相应行的内容 - static
Text.empty静态属性:获取一个空的text对像,表示没有包含任何内容空文档
这不是一个方法,而是 class
Text类的一个属性,以避免创建一大堆内容都是空的 text 对象,每次通过该属性访问的都是同一个text对象(它指向同一个内存地址),以便复用
抽象类
这里的 class Text 是抽象类
TypeScript abstract class 不直接实例化,由于它一般只定义了抽象的方法或属性(虽然也可以包含非抽象的方法或属性),即针对这些方法只是对入参和输出值的类型进行了约束,而没有给出这些方法的具体实现,针对这些属性只是对其值的类型进行了约束
抽象类可以被其他类继承,子类需要实现抽象类中的抽象方法,所以一般是创建子类再使用(才能进行实例化)
在 CodeMirror 的内部定义了 class TextLeaf 和 class TextNode,它们都是继承自 Text 类(在其中实现了 abstract class Text 所定义的抽象方法和属性)
TextLeaf 子类会含有具体的文档内容,它表示文档中一段,即几行文本,可以将其视为结构化文档的叶子节点
TextNode 子类并不含有具体的文档内容,它包含 TextLeaf 或其他 TextNode 实例,可以将其视为容器/父节点
在实例化 Text 类时不能使用常见的方法 new Text(),而需要使用静态方法 static Text.of(strArr) 进行实例化(如果需要得到一个空的文档,则使用静态属性 static Text.empty),在该方法的内部会根据行的数量选择创建 TextLeaf 类的实例,或创建一个 TextNode 类的实例,以表示该结构化文档
大致流程如下:
- 当文档的行数较少时,只需要创建一个
TextLeaf类的实例,就可以表示整个文档内容,采用扁平化结构来表示文档 - 当文档的行数较多时,则需要对文档进行划分,采用树形结构来表示文档
- 将文档划分为多个
TextLeaf类的实例来表示(每个 textLeaf 最多包含 行,即32行),然后再创建TextNode类的实例将一定数量 textLeaf 实例进行包裹 - 如果 TextNode 数量较多时,还会将继续进行划分,即创建 textNode 作为父节点,对它们进行包裹
- 依此类推,构成一个具有多层嵌套的树形结构
树形结构的根部是一个数组,它包含一系列 textNode 作为最顶层的容器,然后它们里面包含着一系列的 textNode,这些子容器又可能包含一系列的 textNode,在最深的层级里则是一系列的 textLeaf
对于大文档使用树形结构来表示便于对大文档的内容进行快速索引 - 将文档划分为多个
更多关于 TextLeaf 和 TextNode 的介绍可参考:
- Spelunking CodeMirror’s Guts
- Duck-typing CodeMirror text data structure
- @codemirror/state - Github 仓库
- 官方 Example: Huge Document 的解释,以及完整源码
class Text 实例化得到 text 对象,以下称作「结构化文档」,它包含一些属性和方法:
注意
Text 类的实例化对象是 immutable 持久化的,即不应该直接修改它(及其属性),而应该基于原有的文档内容创建一个新的 text 对像
length属性:一个数值,表示该结构化文档所包含的字符数量lines属性:一个数字,表示该结构化文档的行数(总是大于或等于1)lineAt(pos)方法:获取给定位置pos所在的行,该方法的返回值是一个Line类的实例line(n)方法:获取索引值为n的行(文档的行索引值从1开始),该方法的返回值是一个Line类的实例replace(from, to, content)方法:用给定的内容content(该参数是一个Text类实例)替换当前结构化文档中从from到to范围的内容
该方法返回一个新的text对象,其内容是(原始文档经过替换操作后的)整个文档内容append(content)方法:将给定的内容content(该参数是一个Text类实例)追加到当前结构化文档的末尾
该方法返回一个新的text对象,其内容是(原始文档追加了新增内容后的)整个文档内容slice(from, to?)方法:基于当前文档从from到to范围的内容,创建一个新的text对象
(可选)参数to是一个数值,设置获取文档范围的结束位置。如果忽略,则采用默认值to=this.length即结束位置是在结构化文档的末尾sliceString(from, to?, lineSep?)方法:获取当前文档从from到to范围的内容,返回一个字符串
(可选)参数to是一个数值,设置获取文档范围的结束位置。如果忽略,则采用默认值to=this.length即结束位置是在结构化文档的末尾
(可选)参数lineSep是一个字符串,用于设定换行符,用于将文档的每一行内容连接起来形成一个字符串eq(otherText)方法:比较当前结构化文档与给定的其他结构化文档otherText是否相同,该方法最后返回一个布尔值iter(dir?)方法:返回一个文档内容迭代器(它是一个对象,需要符合一种 TypeScript interfaceTextIterator),可用于按行迭代文档的内容(每次迭代返回的值都是字符串,交替输出行的内容,或两行之间的分隔符)
(可选)参数dir可以是1(默认值)或-1,表示迭代的方向,如果为-1时,则从后往前迭代文档的内容TextIterator
TypeScript interface
TextIteratorextends 扩展了 Iterator 迭代器协议,它可以迭代结构化文档的内容,依次输出行的内容,或两行之间的分隔符该协议定义了 JavaScript 对象如何产生一系列值(以及所有的值都被迭代完毕后,可以返回一个默认返回)的标准方式,该协议其实是 extends 扩展自另一个较为宽松的 JS 协议——iterable protocol 可迭代协议(可以将迭代器协议视为更严谨的版本)
符合 TypeScript interface
TextIterator的对象所具有的方法和属性:next(skip?)方法:执行一次迭代操作(以获取下一部分的字符串)
(可选)参数skip是一个数字,表示从当前位置跳跃skip个字符(两行之间的分隔符算作 1 个 unit 单位的字符),先忽略这些内容,再执行这一次迭代当前迭代所在的位置
可以把迭代理解为一个「虚拟光标」在文档里进行跳跃,每一次跳跃步长都是一行或一个行分隔符
该方法最后返回该迭代器,以便执行下一次迭代value属性:一个字符串,是当前所遍历的内容,可以是行的内容,或两行之间的分隔符(即空白字符串)特殊情况
如果该迭代器未调用
next()方法,或文档的所有行都已经迭代完后继续调用了next()方法,则返回值都是默认值,即空白字符串done属性:一个布尔值,如果返回true则表示迭代器已经将文档内容都迭代完毕lineBreak属性:一个布尔值,表示当前字符串是否表示换行符(即空字符串)
iterRange(from, to?)方法:返回一个文档内容迭代器(它是一个对象,需要符合一种 TypeScript interfaceTextIterator),可用于按行迭代文档指定范围从from到to的内容(每次迭代返回的值都是字符串,交替输出行的内容,或两行之间的分隔符)
参数from和to是一个数值,根据字符偏移量来表示文档的位置
(可选)参数to如果省略,则采用默认值to=this.length即结束位置是在结构化文档的末尾
如果参数from>to则从后往前迭代文档指定范围的内容iterLines(from?, to?)方法:返回一个文档内容迭代器(它是一个对象,需要符合一种 TypeScript interfaceTextIterator),可用于按行迭代文档从第from行到第to行的内容(每次迭代返回的值都是行的内容,不包括两行之间的分隔符,只有当所迭代行的内容为空时才返回空字符串)
(可选)参数from和to都是一个数值,表示文档中某一行(行的索引值从1开始),用于指定迭代的行的范围
如果设定了参数to即指定了迭代的结束行,则迭代时到达该行之前会停止了,实际迭代内容是不包含该行的toString()方法:将该结构化文档的内容转换为一个字符串(每一行的内容使用换行符\n进行连接)toJSON()方法:将该结构化文档的内容转换为一个数组(其元素都是字符串,分别表示文档中相应行的内容)
可以将该方法的返回值作为参数传递给 staticText.of(strArr)静态方法,进行反序列化 deserialized 创建一个 text 对象children属性:它是一个数组(其元素是Text类的实例)或null
如果文档内容较多(行数多于 即32行),则 CodeMirror 在内部会将结构化文档进行划分,创建一系列TextNode类的实例(该继承自Text类,即它是子类)来表示,则整个文档会形成一个具有嵌套层级的树结构。可以将该属性children理解为获取树形结构的根节点的直接子节点(一系列的TextNode节点)
如果文档内容较少,则仅需要使用一个TextLeaf类的实例就可以表示整个文档。该属性children返回null(表示根节点没有直接子节点,由于文档并不需要复杂的嵌套层级来表示)[Symbol.iterator]()方法:返回一个迭代器迭代器
class
Textimplements 实现了 iterable protocol 可迭代协议根据此协议,符合该协议的对象必须实现
[Symbol.iterator]()方法,该方法返回一个迭代器(符合iterator protocol 迭代器协议的对象)该属性只是用于 TypeScript 编译系统中 ❓ 以提升类型系统的准确性(让 TypeScript 识别到该类的实例化对象是符合可迭代协议的,以便更好地捕获类型错误,实现类型安全),而在运行时实际的 JS 对象上,需要通过调用方法
text.iter、text.iterRange()或text.iterLines()来生成该结构化文档的迭代器
Line 类
Line 类用于描述文档中的某一行
在调用方法 text.lineAt(pos) 或方法 text.line(n)时(以查询/获取文档中相应行的数据),返回该类的实例
该类的实例化对象具有一些属性:
from属性:一个数值(表示该位置距离文档开头的字符偏移量),表示该行的起始位置to属性:一个数值(表示该位置距离文档开头的字符偏移量),表示该行的结束位置(位于换行符之前的位置;如果是在文档的最后一行,则该位置就是文档的结尾)
逻辑位置 logical position
以上两个属性表示该行的逻辑位置 logical position,即文本在文档中实际存储的位置,与文本的走向(LRT 或 RTL)无关
- 属性
from是逻辑起点,表示该行在文档中开始的位置,也就是该行第一个字符在文档中的索引位置 - 属性
to是逻辑终点,表示该行在文档中结束的位置,即该行最后一个字符在文档中的索引位置
如果要考虑文本的走向,获取该行在视觉上的开头或结尾,可以使用编辑器视图对象的方法 editorView.visualLineSide()
number属性:一个数值,表示该行的索引值(文档的行索引值从1开始)text属性:一个字符串,表示该行的内容length属性:一个数值,表示该行的所包含的字符数量(不包含换行符)
辅助函数
该模块还提供了一些与文档结构和内容相关的辅助函数,可以更方便地对文档进行定位或处理字符
countColumn(str, tabSize, to?)方法:计算str字符串所占的列数量(列宽)
各参数的具体说明如下:- 第一个参数
str是需要考察的字符串 - 第二个参数
tabSize是一个数值,表示制表符的大小(即一个制表符等价于多少个空格) - 第三个(可选)参数
to是一个数值,表示str中的某个位置(采用字符偏移量来描述),在计算所占列宽时,只需要从字符串的开头到to所指定的位置结束(默认值是str.length字符串的末尾位置,即默认计算整个字符所占的列宽)
列宽
该方法最后返回一个数值,表示字符串str(整个字符串,或从字符串开头到to位置)所占的列宽- 第一个参数
findColumn(str, col, tabSize, strict?)方法:将给定的col列数量(列宽)转换为str字符串里相应的位置,可以将该方法视为前一个方法countColumn()的逆向操作
各参数的具体说明如下:- 第一个参数
str是一个字符串,需要在其中进行位置定位 - 第二个参数
col是一个数值,表示要转换的列宽值 - 第三个参数
tabSize是一个数值,表示制表符的大小(即一个制表符等价于多少个空格) - 第四个(可选)参数
strict是一个布尔值,表示是否开启严格模式
针对一个特殊的场景:所需转换的列宽col比字符串str所占的列宽要大时,当strict设置为true时,开启了严格模式,则该方法返回-1;如果strict设置为false时,没有开启严格模式,则该方法返回str.length字符串的总长度
该方法最后返回一个数值,表示给定的列宽col对应到字符串str的哪个位置(采用字符偏移量来描述)- 第一个参数
codePointAt(str, pos)方法:获取字符串str给定位置pos(索引值从0开始)的字符的 Unicode 码位值提示
也可以使用 JavaScript 原生方法
str.codePointAt(pos)如果运行环境不支持该方法,则可以使用由 CodeMirror 提供的上述替代方法
fromCodePoint(code)方法:将给定的 Unicode 码位值code转换为字符提示
也可以使用 JavaScript 原生方法
String.fromCodePoint(code)如果运行环境不支持该方法,则可以使用由 CodeMirror 提供的上述替代方法
codePointSize(code)方法:判断给定的 Unicode 码位值所对应的字符在 JavaScript 中占用的字符数量,可以是1个或2个,即该方法的返回值是1或2提示
在 Unicode 中,基本多文种平面 Basic Multilingual Plane (BMP) 包含了从
0x0000到0xFFFF的字符;高位补充平面 Supplementary Multilingual Plane (SMP) 包含了从U+10000到U+1FFFF的字符在 JavaScript 中字符的表示是基于 UTF-16 编码的,因此 BMP 和 SMP 字符在 JavaScript 中占据的字符数量如下:
- 基本多文种平面字符 BMP 在 JavaScript 中占用
1个字符,因为 BMP 字符可以用一个 16 位的码元(即一个char)来表示 - 高位补充平面字符 SMP 在 JavaScript 中占用
2个字符,因为 SMP 字符在 UTF-16 中需要用代理对 surrogate pair 表示,每个代理对由两个 16 位的码元组成,因此在 JavaScript 中会被视为两个独立的字符
jslet bmpChar = 'A'; // U+0041, BMP 字符 let smpChar = '𐍈'; // U+10348, SMP 字符 // 查看字符长度 console.log(bmpChar.length); // 输出: 1 console.log(smpChar.length); // 输出: 2- 基本多文种平面字符 BMP 在 JavaScript 中占用
findClusterBreak(str, pos, forward?, includeExtending?)方法:在给定的字符位置pos附近寻找前一个/后一个 grapheme cluster 字素簇的分界点,返回一个数值表示该分界点在字符串str里的位置(采用字符偏移量来描述),如果无法在附近找到字素簇分界点,则返回参数pos本身的值字素簇
以下是 MDN 关于 grapheme cluster 字素簇的解释
On top of Unicode characters, there are certain sequences of Unicode characters that should be treated as one visual unit, known as a grapheme cluster.
字素簇是用户在视觉上认为的单个字符,它可能由多个 Unicode 码点组成。例如,字母
"é"可以由一个字母 "e" 和一个组合重音符号 "´" 组成,但视觉上被认为是一个字符,它在页面上渲染出来占一列宽度
各参数的具体说明如下:- 第一个参数
str是一个字符串,需要在其中进行位置定位 - 第二个参数
pos是一个数值,表示字符串里的位置 - 第三个(可选)参数
forward是一个布尔值,表示所需寻找(相对于给定位置pos,不包含该位置)前一个字素簇分界点,还是后一个字素簇分界点。如果为true则沿着文档行进方向寻找,即返回后一个字素簇分界点;如果为false,则返回前一个字素簇分界点 - 第四个(可选)参数
includeExtending是一个布尔值(默认值为true),该方法在寻找字素簇分界点时,会跨越/跳过 surrogate pair 代理对,通过 zero-width joiner 零宽连接符连接的字符,以及 flag emoji 旗帜表情符号,如果includeExtending设置为true时还会跳过 extending character 扩展字符
- 第一个参数
JavaScript 字符串与 Unicode
关于 JavaScript 字符与 Unicode 的详细介绍可以参考:
- 字符串 - 现代 JavaScript 教程
- Unicode —— 字符串内幕 - 现代 JavaScript 教程
选区
CodeMirror 文档的选区支持包含多个 range 选区范围
EditorSelection 类
EditorSelection 类用于描述文档的选区
选区包含多个 range 选区范围
编辑器的选区默认只支持包含一个 range 选区范围,可以通过插件将 allowMultipleSelections facet 动态可配置项设置为 true,则文档的选区就可以支持多个 range 选区范围(默认情况下,选区只支持一个 range 选区范围)
但是由于编辑器的选区是基于浏览器原生的 DOM selection 实现的,而常见的浏览器一般最多支持 1 个 range 选区范围(除了 Firefox 允许使用 Ctrl+click(Mac 则用 Cmd+click)在文档中选择多个选区范围)
所以浏览器默认无法正常处理多个选区范围,可以通过插件来对其进行相关功能的增强
例如 @codemirror/view 模块导出方法 drawSelection(config),调用它可以创建一个插件,该插件将浏览器默认的选区和光标隐藏,以支持多选区范围的显示
一个选区可能包含一个或多个 range 选区范围,如果两个选区范围重叠则会自动整合为一个,最终一个选区所包含的 range 选区范围是相互非重叠的、按照所覆盖的位置依次排好序的
该类提供了一些静态方法进行实例化:
- static
EditorSelection.create(ranges, mainIndex?)静态方法:根据给定的一系列选区范围ranges创建一个editorState选区对象
第一个参数ranges是一个数组,其元素是一个个SelectionRange类的实例,表示该选区所包含的一系列 range 选区范围
第二个(可选)参数mainIndex是数值(默认值为0),作为 ranges 数组的索引值,所对应的选区范围是该选区的 primary selection 主选选区范围(一般是用户最后选中的选区范围) - static
EditorSelection.single(anchor, head?)静态方法:根据给定的(一个选区范围的)两端位置anchor和head创建一个editorState选区对象提示
所创建的选区仅包含单个 range 选区范围,所以该静态方法称为
single
参数anchor和head都是数值,分别表示 range 选区范围的两端(锚点和动点)在文档中的位置(采用字符偏移量来描述)
其中参数head是可选的,默认值为head=anchor即采用与anchor一样的值,如果省略设置该参数,则创建的选区就是处于光标状态 - static
EditorSelection.fromJSON(json)静态方法:基于给定的jsonJSON 对象,创建一个editorState选区对象表示选区的 JSON 对象
该 JSON 对象一般是通过调用选区对象的方法
editorState.toJSON()生成的它需要满足以下结构
jsconst jsonObj = { // ranges 属性是一个数组,其元素是一系列对象 // 每个对象表示一个 range 范围 ranges: [ // 每个元素(对象)都包含属性 anchor 和 head { anchor: number; // 表示该范围 range 的锚点位置 head: number // 表示该范围 range 的动点位置 }, { // ... }, // ... ], // main 属性是一个数值,作为 ranges 数组的索引值 // 所对应的范围是该选区的 primary selection 主选范围 // primary selection 主选范围一般是用户最后选中的范围 main: number }
class EditorSelection 实例化得到 editorSelection 对象,以下称作「选区」,它包含一些属性和方法:
ranges属性:一个数组,其元素是一个个SelectionRange类的实例,表示该选区所包含的一系列 range 选区范围
这些选区范围不会重叠(但可能前后相邻/相接),它们会按照在文档(所覆盖的)位置的先后顺序进行排序mainIndex属性:一个数值,作为前一个属性ranges数组的索引值,所对应的选区范围是该选区的 primary selection 主选范围(一般是用户最后选中的选区范围)map(change, assoc?)方法:根据给定的文档更改操作change(一个对象,它是changeDesc类的实例,用于描述文档的更改),更新当前的选区对象说明
由于选区范围的两端在文档中的位置是采用字符偏移量进行描述的,更改操作如果涉及到对文档内容的增删时,则可能会影响选区范围
则需要使用该方法将选区从旧文档 map 映射到新文档里,以保证选区同步更新
如果正好在选区范围的端点(所需映射的位置)处插入了内容,则可以通过设置第二个(可选)参数assoc是-1(默认值)或1来决定这个旧的位置应该映射到新插入内容的哪一侧
如果assoc=-1则表示将旧位置映射到插入内容的左侧/前面;如果assoc=1则表示将旧位置映射到插入内容的右侧/后面
该方法最后返回一个更新后的editorSelection对象eq(otherSelection, includeAssoc?)方法:判断当前选区是否与给定的选区otherSelection相同
默认只是基于选区所包含的一系列 range 选区范围(锚点 anchor 和动点 head)位置是否都相同。如果includeAssoc设置为true(默认值为false),则对于处于光标状态的 range 选区范围更细致的考察,它们的assoc属性值(该属性与 bidirectional 双向文本相关)也需要是相同的,才视为两个选区相同main属性:一个SelectionRange类的实例,它是该选区的 primary selection 主选范围(一般是用户最后选中的选区范围)asSingle()方法:返回一个editorSelection对象,它只包含一个 range 选区范围,是当前选区的 primary selection 主选范围addRange(range, main?)方法:将给定的range选区范围对象添加到为当前选区,第二个(可选)参数是一个布尔值(默认值为true)用于设置是否将给定的选区范围设置为 primary selection 主选范围(一般是用户最后选中的选区范围)
该方法最后返回一个包含range选区范围的editorSelection选区对象replaceRange(range, which?)方法:用给定的range选区范围对象替代当前选区指定的选区范围
第二个(可选)参数which是一个数值,作为索引值(当前选区对象editorSelection.ranges属性值是一个数组,表示选区所包含的一系列选区范围,参数which表示该数组索引值),以指定需要替换哪个 range 选区范围。该参数的默认值是which=this.mainIndex,即 primary selection 主选范围所对应的索引值toJSON()方法:将该选区对象序列化为 JSON 对象
该类还提供了两个静态方法 static EditorSelection.range() 和 static EditorSelection.cursor(),都是用于创建 SelectionRange 类的实例,具体介绍可以查看下一小节
SelectionRange 类
SelectionRange 类用于描述一个 range 选区范围
该类的实例化是需要通过调用另一个类 EditorSelection 所提供的静态方法:
- static
EditorSelection.cursor(pos, assoc?, bidiLevel?, goalColumn?)静态方法:在文档给定的位置pos创建一个SelectionRange类的实例,它处于光标状态(即 range 选区范围的锚点和动点位于同一个位置)
第二个(可选)参数assoc是一个数值,它可以是1、0、-1中的任一值(默认值为0),表示光标关联的方向(如果 CodeMirror 需要处理 bidirectional 双向文本的内容,该设置会有帮助)
第三个(可选)参数bidiLevel是一个数值,以表示光标所在的文本(由于双向文本而构成的,相对于该段落/文档最外层文本)的嵌套层级关系
第四个(可选)参数goalColumn是一个数值,表示光标所期待位于哪一列(该设置会影响光标在垂直方向移动的交互相关) - static
EditorSelection.range(anchor, head, goalColumn?, bidiLevel?)静态方法:在文档给定的指定选区范围创建一个SelectionRange类的实例
第一个参数anchor和第二个参数head都是数值,分别表示 range 选区范围的两端(锚点和动点)在文档中的位置(采用字符偏移量来描述)
第三个(可选)参数goalColumn是一个数值,表示该选区范围变成光标状态后 ❓ 进行上下移动时,它所期待位于哪一列(该设置会影响光标在垂直方向移动的交互相关)
第四个(可选)参数bidiLevel是一个数值(整数),表示该选区范围所在的文本由于双向文本而构成的,相对于该段落/文档最外层文本)的嵌套层级关系
也可以使用该类所提供的一个静态方法 static SelectionRange.fromJSON(json) 将给定的 json JSON 对象 deserialized 反序列化为一个 selectionRange 范围对象
表示选区的 JSON 对象
该 JSON 对象一般是通过调用选区范围对象的方法 selectionRange.toJSON() 生成的
它需要具有两个属性
const jsonObj = {
anchor: number; // 表示该范围 range 的锚点位置
head: number // 表示该范围 range 的动点位置
}
class SelectionRange 实例化得到 selectionRange 对象,以下称作「选区范围」,它包含一些属性和方法:
from属性:一个数字,表示该 range 所覆盖的文档的 lower boundary 下界的位置(采用字符偏移量来描述)to属性:一个数字,表示该 range 所覆盖的文档的 upper boundary 下界的位置(采用字符偏移量来描述)anchor属性:一个数字,表示该 range 的锚点在文档中的位置(采用字符偏移量来描述)head属性:一个数字,表示该 range 的动点在文档中的位置(采用字符偏移量来描述)
anchor 与 head
range 选区范围具有两端,分别称作锚点 anchor 和动点 head:
- anchor 锚点是指锚定「不动」的一端,即框选开始时光标的位置
- head 动点是指框选结束时光标的位置,如果选区变化会在这一侧发生(例如通过同时按下
shift和向左键/向右键,以拓展选区范围时,选区范围会动的那一侧就是 head 动点)
当 range 两端位于文档的同一个位置,那么 range 就处于 cursor 光标状态,视觉上就是一个闪烁的竖线光标
empty属性:一个布尔值,表示该选区范围是否为空。如果该属性为true则表示该 range 的锚点 anchor 和动点 head 位于文档的同一位置,该 range 处于光标状态assoc属性:一个数值,它可以是1、0、-1中的任一值(默认值为0),表示光标关联的方向assoc 光标的关联方向
CodeMirror 可能需要处理 bidirectional 双向文本内容(假设一个网页文件所展示的主要文本内容是 RTL 从右往左,例如阿拉伯语;而 HTML 标签是英文,采用的布局是 LTR 从左往右)
光标在 LTR 和 RTL 布局中的行为是不同的,在处理双向文本时,如果光标正好处于 LTR 和 RTL 文本的分界/交界处,可以通过 range 选区范围对象的属性
assoc为光标设置关联的方向,保证光标行为符合预期可以参考官方样例 Example: Right-to-Left Text 具体介绍如何在 CodeMirror 中处理双向文本
bidiLevel属性:一个数值或null,以表示该 range(处于光标状态)所在的文本(由于双向文本而构成的,相对于该段落/文档最外层文本)的嵌套层级关系bidiLevel
在双向文本中 LTR 和 RTL 布局可能交替出现,它们之间就会形成包含/嵌套的关系
bidiLevel代表双向文本的嵌套级别:0表示位于文档的根节点从左到右 LTR 文本(大多数拉丁语)1表示位于文档的根节点从右到左 RTL 文本(阿拉伯语、希伯来语等语言)2表示嵌套在 RTL 文本中的 LTR 文本3表示嵌套在 LTR 文本中的 RTL 文本,然后又嵌套了 LTR 文本
更高的奇数值表示更深层次的 RTL 嵌套,更高的偶数值表示更深层次的 LTR 嵌套
一般规律:
- 偶数值(0, 2, 4, ...)表示从左往右 LTR 方向的文本
- 奇数值(1, 3, 5, ...)表示从右往左 RTL 方向的文本
实际使用中,很少会看到超过 3 或 4 的值,因为深层嵌套在实际文本中并不常见
CodeMirror 在内部使用这些值来正确渲染和处理混合方向的文本,大多数情况下 CodeMirror 会自动处理这些细节
goalColumn属性:一个数值或undefined,表示该 range(处于光标状态)所期待位于哪一列光标的期望列
该设置会影响光标在垂直方向移动的交互相关
用户通过按向上(或向下)箭头键,将光标在文档中垂直移动时,所期待的行为是尽量保持在同一列(光标在各行应该位于同一水平宽度)
但是文档的各行一般是不等宽的,如果光标一开始位于较长的行,而下一个目标行更短,则光标只能置于行尾
可以通过 range 选区范围对象的属性
goalColumn设置该光标的期望列,当光标再继续跳转到更长的行时,可以将光标恢复到与较长的行相同的水平位置(即垂直方向上位于同一列)该属性可以避免光标位置出现不可预测的跳动,移动光标时尽量保持在视觉上的同一列,更符合用户的预期
map(change, assoc?)方法:根据给定的文档更改操作change(一个对象,它是changeDesc类的实例,用于描述文档的更改),更新当前的选区范围说明
由于选区范围的两端在文档中的位置是采用字符偏移量进行描述的,更改操作如果涉及到对文档内容的增删时,则可能会影响选区范围
则需要使用该方法将选区从旧文档 map 映射到新文档里,以保证选区同步更新
如果正好在选区范围的端点(所需映射的位置)处插入了内容,则可以通过设置第二个(可选)参数assoc是-1(默认值)或1来决定这个旧的位置应该映射到新插入内容的哪一侧
如果assoc=-1则表示将旧位置映射到插入内容的左侧/前面;如果assoc=1则表示将旧位置映射到插入内容的右侧/后面
该方法最后返回一个更新后的selectionRange对象extend(from, to?)方法:按需扩展该 range 以确保至少覆盖从from到to的范围
参数from和to都是数值,表示文档中的位置(采用字符偏移量来描述)
第二个(可选)参数to表示选区范围所需覆盖的 upper boundary 下界的位置,默认值是to=from即与from一样的位置
该方法最后返回一个扩展后的selectionRange对象eq(otherRange, includeAssoc?)方法:判断当前选区范围是否与给定的选区范围otherRange相同
默认只是基于该 range 选区范围的锚点 anchor 和动点 head 位置是否都相同进行判断。
如果includeAssoc设置为true(默认值为false),则对于处于光标状态的 range 选区范围更细致的考察,它们的assoc属性值(该属性与 bidirectional 双向文本相关)也需要是相同的,才视为两个选区范围是相同的toJSON()方法:将该选区范围对象序列化为 JSON 对象
更改文档
CodeMirror 采用对象 changeSet 来描述文档的更改,精确地描述了哪些范围发生了改变(删除或插入文本)
它可以独立地被传递和存储,不依赖与文档而存在(虽然它最终还是需要作用于具体的文档才有意义),它可以在核心模块之外使用,为编辑器实现额外的功能(例如历史记录、协同编辑等)
将 changeSet 应用到编辑器上的大致流程:
- 创建一个 transaction 事务,它除了包含 changeSet(与文档更改相关的信息,例如文档内容的变化、选区的变化等),还可以添加 state effects 自定义变更效应
- 分发事务 dispatch transaction,编辑器的视图 view 会更新它的 state,并同步更新页面的 DOM 元素
// 假设在编辑器视图 view(所包含的状态 state 中)当前文档内容是 `"123"`
// 创建一个 transaction 事务,它在文档的开头位置插入内容 `"0"`
let transaction = view.state.update({changes: {from: 0, insert: "0"}})
// 💡 方法 update 的参数需要符合 TransactionSpec[] 类型
// 💡 其中 interface TransactionSpec 的属性 changes 记录文档发生的变更
// 💡 该属性的值要符合 ChangeSpec 类型,具体而言它有多种形式
// 💡 这里采用对象的形式,还能是 ChangeSet 类的实例化对象,也可以是数组 ChangeSpec[]
// 💡 这里为了演示方便,所以没有构建 ChangeSet,使用简单的对象形式来描述文档变更
// 可以从事务 transaction 对象中,读取到应用了变更后的新文档的内容,变成 "0123"
console.log(transaction.state.doc.toString()) // "0123"
// 此时编辑器视图所包含的 state 还没有更新(即页面上显示的内容依然为 `"123"`
// 分发事务 dispatch transition
view.dispatch(transaction)
// view 会更新 state,并同步更新页面的 DOM 元素
ChangeDesc 类
ChangeDesc 类(该类名是 change description 的缩写)用于描述一系列对文档的修改操作,它是 ChangeSet 类的变体,但是不包含具体的文本内容
区别
ChangeDesc 类和 ChangeSet 类都是用于描述一系列对文档的修改操作,实际上 ChangeDesc 类是 ChangeSet 类的父类,可以将 ChangeDesc 类视为 ChangeSet 类的简化版本
主要区别在于 ChangeDesc 类只记录位置相关的变化,并不包含具体的文本内容
例如在文档中插入内容时,ChangeDesc 类只记录了插入操作造成的相关位置变化,但不包含具体插入了哪些文本;而 ChangeSet 则包含位置的变化和具体插入的内容
ChangeDesc 类更轻量级,一般在位置映射的场景下使用,由于它不包含具体的更改文本内容,所以占用内存更少,处理速度更快,使用它可以提高编辑器性能
例如以下方法与文档变更时的位置映射相关,它们都包含了 changeDesc 对象作为其参数:
- 选区范围的方法
selectionRange.map() - 文档更改集的方法
changeSet.map() - 自定义效应的方法
stateEffect.map()和静态方法 staticStateEffect.mapEffects() - 文档范围集的方法
rangeSet.map()和静态方法 staticrangeSet.compare()
而 ChangeSet 类包含具体的文本内容,则一般在文档执行实际更改的场景中使用(用于 Transaction 事务中)
该类提供了一个静态方法 static ChangeDesc.fromJSON(json) 基于给定的 json JSON 对象(该 JSON 对象一般是通过调用 changeDesc 对象的方法 changeDesc.toJSON() 生成的),创建一个 changeDesc 对象
class ChangeDesc 实例化得到 changeDesc 对象,以下称作「文档更改集描述」,它包含一些属性和方法:
length属性:一个数值,表示应用该更改集之前的旧文档所包含的字符数量newLength属性:一个数值,表示应用该更改集之后的新文档所包含的字符数量empty属性:一个布尔值,表示在 changeDesc 是否为空(即该 changeDesc 是否包含/记录有更改操作)iterGaps(fn)方法:遍历(在这次更改中)文档中未修改的部分,并分别调用给定的函数fn
参数fn是一个函数fn(posA: number, posB: number, length: number)它的前两个参数posA和posB都是一个数值,分别是当前所遍历的未修改部分在旧文档和新文档中的位置,第三个参数length是一个数值表示当前所遍历的未修改部分的字符串长度iterChangedRanges(fn, individual?)方法:遍历(在这次更改中)文档中修改的部分,并分别调用给定的函数fn
第二个(可选)参数individual是一个布尔值(默认值为false),用于设置如何处理影响的文档范围是连续的变更操作,如果为false则将它们进行整合视为一次变更,只遍历一次(将变更效果进行融合后)它们所对应的修改部分;否则就会依次单独遍历这些变更操作所对应的修改部分
第一个参数fn是一个函数fn(fromA: number, toA: number, fromB: number, toB: number)它的前两个参数fromA和toA都是一个数值,分别是当前所遍历的修改部分在旧文档的开始和结束位置;相应地,后两个参数fromB和toB则是当前所遍历的修改部分在新文档中的位置invertedDesc属性:获取该 changeDesc 的逆向形式(也是一个changeDesc对象),可以将新文档恢复到旧文档composeDesc(otherChangeDesc)方法:将该 changeDesc 与给定的其他otherChangeDesc进行融合注意
由于当前 changeDesc 和给定的
otherChangeDesc是依序先后应用到文档中的,所以当前 changeDesc 的属性newLength需要与otherChangeDesc的属性length相一致
该方法最后返回一个整合了两个更改集的changeDesc对象mapDesc(otherChangeDesc, before?)方法:当前 changeDesc 和给定的otherChangeDesc都是可以作用于同一个文档(通过属性length约束),而通过该方法(对该 changeDesc)进行映射转变,让它可以适用于执行otherChangeDesc更改集后的文档应用场景
通过该方法所实现 operational transformation (OT) 操作变换,常用于将多个变更合并在一起,尤其是在复杂的编辑场景中,如协同编辑等
第二个(可选)参数before是一个布尔值(默认值为false),如果为true表示应用当前 changeDesc 的执行是在发生在otherChangeDesc之前,对当前 changeDesc 进行映射转换时,会考虑后续需要执行otherChangeDesc而形成约束mapPos(pos, assoc?, mode?)方法:将旧文档的位置pos映射到(执行了当前 changeSet 后所得的)新文档中
如果当前 changeSet 正好在pos(所需映射的位置)处插入了内容,则可以通过设置第二个(可选)参数assoc是非正值(0或负数,默认值是-1)或正值来决定这个旧的位置应该映射到新插入内容的哪一侧
如果assoc是非正值,则表示将旧位置映射到插入内容的左侧/前面;如果assoc正值,则表示将旧位置映射到插入内容的右侧/后面
如果在执行 changeSet 时包含对文档的删除操作,影响了旧文档的pos位置,可以通过设置第三个(可选)参数mode以更精细地控制该方法的返回值(以便调整光标位置或选区范围)。参数mode需要是 TypeScript enumMapMode枚举类型中的一个值:- 可以是
MapMode.Simple:简单模式(默认值),该方法返回一个数值,即位置pos始终可以映射到一个有效的新位置 - 或是
MapMode.TrackDel:如果删除操作跨过了给定置pos,则该方法返回null,该模式可用于跟踪/检测位置是否被删除 - 或是
MapMode.TrackBefore:如果在给定置pos前面有字符被删除,则该方法返回null,该模式可用于跟踪/检测位置前面是否发生删除 - 或是
MapMode.TrackAfter:如果在给定置pos后面有字符被删除,则该方法返回null,该模式可用于跟踪/检测位置后面是否发生删除
- 可以是
touchesRange(from, to?)方法:返回一个布尔值,以表示该 changeDesc 是否触及/影响给定文档的特定范围form到to(可选,默认值是to=from即采用from相同的位置)。如果该更改会覆盖/涉及整个给定的范围,则该方法返回字符串"cover"toJSON()方法:将该 changeDesc 对象序列化为 JSON 对象
由于 changeDesc 文档更改集描述了一系列对文档的修改操作,所以该 JSON 对象是采用数组形式,它的元素是一系列数值,每两个数字为一组将文档分为多个部分:- 第一个值表示该部分的字符串长度
- 第二个值表示变更的影响,如果是
-1表示没有这部分变化;如果是其他值,则表示发生了替换、删除、插入等操作
例如[5, -1, 4, 3, 3, -1]表示该 changeDesc 的修改操作将文档分为三部,第一部分长度为5,未发生变动;第二部分长度为4,在新文档中被替换为3个字符;第三部分长度为3,未发生变动
ChangeSet 类
ChangeSet 类用于描述一系列对文档的修改操作(例如插入、删除、替换等)
区别
ChangeSet 类是 ChangeDesc 类的子类,所以继承父类的属性和方法,不同在于 ChangeSet 类包含具体的文本内容,一般在文档执行实际的更改时使用(用于 Transaction 事务中)
而 ChangeDesc 类更轻量级,不包含具体文本,只记录了位置变化,一般在位置映射的场景下使用
该类提供了一些静态方法进行实例化:
- static
ChangeSet.of(changes, length, lineSep?)静态方法:根据给定的更改操作配置changes(该参数值需要符合一种 TypeScript typeChangeSpec,用于描述文档的更改)创建一个changeSet对象ChangeSpec
TypeScript type
ChangeSpec描述文档的更改操作符合该类型的值可以作为编辑器状态对象的方法
editorState.changes()的参数,也可以作为事务配置对象的属性changes的值它可以有多种形式来表示更改操作:
- 可以是简单的对象
{from: number, to?: number, insert?: string | Text}
其中属性from和 (可选)属性to是更改的范围,
如果该更改操作是往文档插入的内容,则设置(可选)属性insert,它可以是字符串或一个结构化文档(Text类的实例) - 可以是一个
ChangeSet类的实例 - 可以是一个数组,它的元素也需要符合 TypeScript type
ChangeSpec
第二个参数length是一个数值,表示所创建的changeSet对象所适用的文档(字符串)的长度( ⚠️ 该changeSet不能 apply 应用/作用到其他长度值的文档)
第三个(可选)参数lineSep是一个字符串,用于设置文档中的换行符,所创建的changeSet会将该配置考虑进去 - 可以是简单的对象
- static
ChangeSet.empty(length)静态方法:创建一个空的changeSet对象,即该changeSet并不包含/记录有更改操作
参数length是一个数值,表示所创建的changeSet对象所适用的文档(字符串)的长度( ⚠️ 该changeSet不能 apply 应用/作用到其他长度值的文档) ChangeSet.fromJSON(json)静态方法:基于给定的jsonJSON 对象(该 JSON 对象一般是通过调用ChangeSet对象的方法ChangeSet.toJSON()生成的),创建一个changeSet对象
class ChangeSet 实例化得到 changeSet 对象,以下称作「文档更改集」
ChangeSet 类是 ChangeDesc 类的子类,除了继承父类的属性和方法,还具有一些特有的属性和方法(以及对父类的一些方法进行覆写):
apply(doc)方法:将该 changeSet 应用到给定的文档doc(该参数值是Text类的实例,结构化文档)。该方法最后返回一个更新后的结构化文档invert(doc)方法:获取该 changeSet 的逆向形式(也是一个changeSet对象),可以将新文档恢复到旧文档。该方法可用于撤销操作、协同编辑等
参数doc(该参数值是Text类的实例,结构化文档)表示原始文档,也就是应用了当前 changeSet 的逆向形式后,所需恢复得到的文档compose(otherChangeSet)方法:将该 changeSet 与给定的其他otherChangeSet进行融合注意
由于当前 changeSet 和给定的
otherChangeSet是依序先后应用到文档中的,即otherChangeSet所应用的文档是经过 changeSet 更新的所以当前 changeSet 的属性
newLength需要与otherChangeSet的属性length相一致
该方法最后返回一个整合了两个更改集的changeSet对象map(otherChangeSet, before?)方法:当前 changeSet 和给定的otherChangeSet都是可以作用于同一个文档(通过属性length约束),而通过该方法(对该 changeSet)进行映射转变,让它可以适用于执行完更改集otherChangeSet之后的文档
第二个(可选)参数before是一个布尔值(默认值为false),如果为true表示应用当前 changeSet 的执行是在发生在otherChangeSet之前,对当前 changeSet 进行映射转换时,会考虑后续需要执行otherChangeSet而形成约束
例如有两个更改集A和B都可以作用于同一个文档(即它们的属性length都是相同的),则可以通过A.compose(B.map(A))和B.compose(A.map(B, true))将这两个方式融合,所得到的更改集效果是一样的 ❓ (即融合后的更改集分别作用于同一个文档,都可以得到相同的新文档)应用场景
通过该方法所实现 operational transformation (OT) 操作变换,常用于将多个变更合并在一起,尤其是在复杂的编辑场景中,如协同编辑等
iterChanges(fn, individual?)方法:遍历(在这次更改中)文档中修改的部分,并分别调用给定的函数fn
第二个(可选)参数individual是一个布尔值(默认值为false),用于设置如何处理影响的文档范围是连续的变更操作,如果为false则将它们进行整合视为一次变更,只遍历一次(将变更效果进行融合后)它们所对应的修改部分;否则就会依次单独遍历这些变更操作所对应的修改部分
第一个参数fn是一个函数fn(fromA: number, toA: number, fromB: number, toB: number, inserted: Text)它的前两个参数fromA和toA都是一个数值,分别是当前所遍历的修改部分在旧文档的开始和结束位置;相应地,后两个参数fromB和toB则是当前所遍历的修改部分在新文档中的位置;最后一个参数inserted是当前所遍历的修改往文档插入的内容(该参数值是Text类的实例)desc属性:获取该 changeSet 所对应的 文档更改集描述 changeDesc 形式(即不包含具体文本,只记录了位置变化)toJSON()方法:将该 changeSet 对象序列化为 JSON 对象
Transaction 类
Transaction 事务包含一系列对文档的更改操作 document change,或改变选区 selection,以及一些自定义变更效应 state effect
通过 transaction 事务驱动文档更新的大致流程:
- 一般是由用户交互触发更新,例如通过键盘输入文字,相应的 DOM 事件监听器会调用预设的响应函数(或点击菜单栏上的相应按钮,相应的指令 command 被分发,执行相应的回调函数)
- 在这些响应函数里,会调用编辑器状态对象的方法
editorState.update()创建一个 transaction 事务对象 - 然后通过编辑器视图对象的方法
editorView.dispatch()分发事务,将变更应用到编辑器上并同步更新页面的 DOM 元素
该类的实例化是需要通过调用编辑器状态对象的方法 editorState.update(...specs),该方法可以接受一系列的的配置对象作为参数(剩余参数 ...specs 将函数所接受的所有参数打包为一个数组,每一个元素都是一个对象,它们都需要符合一种 TypeScript interface TransactionSpec,用于描述/配置 transaction 事务)
TransactionSpec
TypeScript interface TransactionSpec 描述事务可接受哪些配置参数
change(可选)属性:该事务所包含的的文档更改操作,该属性值需要符合一种 TypeScript typeChangeSpec,用于描述文档的更改selection(可选)属性:如果要在该事务显式/手动更新编辑器的选区,则可以设置该属性
该属性的值有多种形式:- 可以是一个
EditorSelection类的实例 - 可以是一个对象
{anchor: number, head?: number}其中属性anchor和属性head(可选,默认值是head=anchor即采用anchor相同的位置)都是数值,用于设置选区范围的锚点和动点在文档中的位置(采用字符偏移量来描述)
注意
其中对于选区范围两端位置的描述,是基于应用了更改操作后(当该 spec 配置对象具有属性
changes)所生成的新文档如果方法
editorState.update(...specs)传递了多个配置对象,而将更新效果/作用进行合并时,位于后面的 spec 配置对象对于选区的设置具有更高优先级,即后面的 spec 对于选区的设置会覆盖掉前面的设置- 可以是一个
effects(可选)属性:为该事务添加 state effect 自定义变更效应
该属性值可以是一个StateEffect类的实例,也可以是一个数组(其元素是一系列StateEffect类的实例)注意
如果在 state effect 里涉及到位置的描述,是基于应用了更改操作后(当该 spec 配置对象具有属性
changes)所生成的新文档最终生成的 transaction 事务会整合各个 spec 对于自定义编辑器状态字段的设置
annotations(可选)属性:为该事务添加 annotations 元信息
该属性值可以是一个Annotation类的实例,也可以是一个数组(其元素是一系列Annotation类的实例)userEvent(可选)属性:一个字符串,这是为该事务添加 annotation 元信息(以表示该事务与哪一种 user event 用户交互方式相关)的 Shorthand 简写形式简写形式
为事务所添加的 annotation 元信息,一般是与用户交互方式 user interface 相关(类似于用户触发哪一种类型的 DOM event 事件,引起分发当前事务,对文档进行修改
一般流程如下:
Transaction类提供了一个静态属性Transaction.userEvent,它是一个AnnotationType类的实例,调用它的方法Transaction.userEvent.of(value)可以创建一个 annotation 元信息对象,其中所传递的参数value是一个字符串,用于表示相应的 user event- 然后将所创建 annotation 元信息添加到事务上
而通过该属性则更方便,只需要一个特殊的字符串(CodeMirror 在核心模块里所预设/约定的一些字符串),就可以为当前事务添加与用户交互事件 user interface event 相关的 annotation 元信息(CodeMirror 在内部会自动创建 annotation 实例,并添加到当前事务上)
scrollIntoView(可选)属性:一个布尔值,表示是否要为该事务添加上标记,需要将编辑器的选区滚动入视口里filter(可选)属性:一个布尔值,表示该事务是否可以接受过滤处理。如果设置为false则该事务会跳过所有过滤器的检查过滤器
通过 change filter 和 transaction filter 这两个 facet 可以向编辑器注册针对事务(或它的配置)的过滤器
每次编辑器 dispatch transaction 分发事务时,过滤器一般都会执行(除非上述属性设置为
false),以对该事务里的更新操作进行过滤处理sequential(可选)属性:一个布尔值,用于设置在 change 更改操作中,关于位置的描述应该采用什么文档作为基准
如果方法editorState.update(...specs)传递了多个配置对象,而其中多个 spec 包含对文档的更改操作,默认情况下每一个 spec 中的更改操作(对于修改位置的描述)都是基于当前文档的,即它们都是以(初始未更改的)原文档作为共同的起始点,而不是应用前一个 spec 的更改后生成的新文档
如果属性sequential设置为true,则当前 spec 的更改操作中,对于修改位置的描述是以(应用了前一个 spec 后所生成的)新文档为起始点
class Transaction 实例化得到 transaction 对象,以下称作「事务」,它包含一些属性和方法:
startState属性:一个EditorState类的实例,当前事务所基于的编辑器状态(即该事务将更改操作应用于该 state 上)changes属性:一个ChangeSet类的实例,表示当前事务所包含的更改操作effects属性:一个数组,其中每个元素都是StateEffect类的实例,表示在该事务里添加的一系列自定义变更效应scrollIntoView属性:一个布尔值,表示在当前事务分发后,是否需要将选区滚动入视口里selection属性:可以是一个EditorSelection类的实例,表示该事务为文档显式/手动设置了选区;或是undefined,表示该事务并没有为文档显式/手动设置选区newSelection属性:一个EditorSelection类的实例,表示当前事务分发后,在新文档中的选区提示
如果当前事务并没有显式/手动设置选区(即事务对象的属性
selection为undefined),则该属性会采用原始文档里的选区经过 map 映射(由于分发事务后修改文档内容,可能会影响选区的位置)后的版本newDoc属性:一个Text类的实例,表示应用该事务后新的文档内容(结构化文档)state属性:一个EditorState类的实例,表示应用该事务后新的编辑器状态
注意
Text 结构化文档只包含文档(文本)内容相关的信息,而 editorState 编辑器状态则更复杂,除了包含文档的文本内容,还包含选区,以及插件相关的状态
在事务过滤器中如果要获取新文档内容的相关信息,推荐使用属性 newDoc 而不是 state,访问属性 newDoc 不需要计算整个新状态,因此性能代价较低
属性 state 是 「懒计算」,即只有在实际访问时 CodeMirror 才会计算出整个新状态,由于它的运算代价较高,所以通常不建议随意访问(只有在需要完整的编辑器状态时才访问)
annotation(type)方法:根据给定的元信息类型type(该参数值是一个AnnotationType类的实例,标记/表示一种 annotation 元信息的类型),获取相应的元信息具体内容;如果该事务没有设置相应类型的元信息,则返回undefineddocChanged属性:一个布尔值,表示该事务是否更改了文档提示
基于该事务对象是属性
changes更改集是否为空来判断reconfigured属性:一个布尔值,表示该事务是否修改了编辑器状态的配置修改编辑器状态配置
可以通过 compartment reconfigure 对从全局配置分隔开来的配置进行修改/重配置
也可以通过 stateEffect reconfigure 对全局配置进行修改/重配置
isUserEvent(eventType)方法:判断当前事务是否与给定类型eventType(一个字符串,表示不同的交互方式)的 user event 相关,该方法最后返回一个布尔值
由于表示 user event 的字符串可能带有.符号作为分隔符,以将特定的交互类型进行细分,则调用以上方法对特定类型的交互方式进行匹配判断时,大类和小类都会命中
例如当该事务与"select.pointer"的交互方式相关,则调用以上方法时,所传递参数如果为"select"或"select.pointer",该方法都会返回true
该类还提供了一些静态属性,它们都是一些预设的 AnnotationType 类的实例(表示不同的元信息类型,可以用于创建相应类型的 annotation 元信息对象实例):
具体介绍看下文
AnnotationType 类
作为一个 marker 标签,用于标记/表示一种 annotation 元信息的类型
该类的实例化是需要通过调用另一个类 Annotation 所提供的静态方法 static Annotation.define()
class AnnotationType 实例化得到 annotationType 对象,称为「元信息类型」,该对象包含一个方法 annotationType.of(value),可以用于创建相应类型的 annotation 元信息对象实例,它的具体内容就是参数 value 的值
在 Transaction 类中预设了一些元信息类型(以静态属性的方式提供):
- static
time静态属性:一个AnnotationType类的实例,该类型所对应的 annotation 元信息的具体内容是时间戳对象,用于记录 transaction 分发的时间点
CodeMirror 会自动创建一个该类型的 annotation 实例(使用方法Date.now()获取时间戳),添加到每个事务上 - static
userEvent静态属性:一个AnnotationType类的实例,该类型所对应的 annotation 元信息的具体内容是字符串,用于表示 transaction 是由哪种类型的用户交互 user event 所触发的user event
user event 类似于 JavaScript 原生的 DOM event 事件,它以 annotation 元信息(字符串)的形式添加 transaction 事务中,一般表示用户通过特定的交互方式引起事务的分发,对文档进行修改
在 CodeMirror 核心模块里,预设/约定了一些字符串表示不同的交互方式(有些字符串带有
.符号作为分隔符,以将特定的交互类型进行细分):"input"表示将内容输入到编辑器中"input.type"表示键盘输入"input.type.compose"表示使用组合式 compose 输入法"input.paste"表示粘贴输入内容"input.complete"表示通过自动补全的方式输入内容
"delete"表示删除内容"delete.selection"表示删除选区的内容"delete.forward"表示按下delete键删除内容(沿着文本行进的方向删除)"delete.backward"表示按下backspace键删除内容(反着文本行进的方向删除)"delete.cut"表示以剪切的方式删除内容
"move"表示在编辑器里移动内容"move.drop"表示通过拖拽移动内容最后释放的操作
"select"表示更改选区"select.pointer"表示通过鼠标或其他 pointer 指针设备(触屏也算)更改选区
"undo"和"redo"表示通过历史操作更改文档内容
- static
addToHistory静态属性:一个AnnotationType类的实例,该类型所对应的 annotation 元信息的具体内容是布尔值,用于表示 transaction 是否要添加到(undo 撤销)历史记录栈里 - static
remote静态属性:一个AnnotationType类的实例,该类型所对应的 annotation 元信息的具体内容是布尔值,用于表示 transaction 是否由远程用户触发的,一般在协同编辑中标记/区分 transaction 的来源
Annotation 类
Annotation 类用于为 transaction 事务添加额外的元信息
静态方法
Annotation 类提供了一个静态方法 static Annotation.define() 用于创建一个新的 AnnotationType 对象实例
该类的实例化是需要通过调用相应类型 annotationType 对象的方法 annotationType.of(value),所得到的 annotation 对象的具体内容就是参数 value 的值
class Annotation 实例化得到 annotation 对象,称为「元信息」,它具有一些属性:
type属性:一个AnnotationType类的实例,表示该元信息的类型value属性:该元信息的具体内容
StateEffectType 类
StateEffectType 类用于表示一种 stateEffect 自定义变更效应的类型
该类的实例化是需要通过调用另一个类 StateEffect 所提供的静态方法 static StateEffect.define(spec?)
其中(可选)参数 spec 是一个对象(默认值为一个空对象 {}),具有(可选)属性 map,该属性值是一个函数 fn(value: Value, mapping: ChangeDesc) → Value | undefined
说明
该函数所处理/针对的场景是该 state effect 创建后(但未应用到编辑器),文档又发生了更改(常见于协同编辑),则将 state effect(通过分发事务)应用到新文档时,可能需要对它进行映射/更新
该函数各参数的具体说明如下:
- 第一个参数
value是该 state effect 的原始值 - 第二个参数是所在事务的更改操作集(只包含位置相关的变化信息)
- 如果该类型的 state effect(的值)与文档位置无关,则可以不设置该参数,那么当该 state effect 创建后(但未应用到编辑器)文档又发生了更改(常见于协同编辑),则该 state effect 的值可以不进行映射/更新,直接就可以(通过分发事务)应用到新文档中
- 如果该类型的 state effect(的值)与文档位置相关,则可以通过设置该属性(在分发事务时)对位置的映射/更新,再应用到新文档中
该方法最后返回的值作为 state effect 更新的值,如果最后返回的值是 undefined 则表示该 state effect 在分发事务后被删除
class StateEffectType 实例化得到 stateEffectType 对象,称为「自定义变更效应类型」
stateEffectType 对象包含一个方法 stateEffectType.of(value),可以用于创建相应类型的 stateEffect 对象实例,它的具体内容就是参数 value 的值(不能是 undefined 由于它表示该 stateEffect 在映射过程中被移除)
在 StateEffect 类中预设了一些自定义变更效应的类型(以静态属性的方式提供):
- static
reconfigure静态属性:一个StateEffectType类的实例,该类型的 stateEffect 的具体内容是一个插件(需要符合一种 TypeScript typeExtension),用于对编辑器的全局配置进行 reconfigure 重配置重配置
使用上述
reconfigure类型的 state effect 对编辑器的全局配置进行 reconfigure 重配置时,会清空编辑器原本的全局配置(包括使用appendConfig类型 state effect 往编辑器所追加的插件),但并不会重置通过 compartment 所隔离的插件例如以下代码可用于清空编辑器原来的全局配置
tsimport {StateEffect} from "@codemirror/state" export function deconfigure(view) { view.dispatch({ // 仅作为演示,该操作清空了所有全局配置,可能导致编辑器无法正常工作 effects: StateEffect.reconfigure.of([]) }) } deconfigure();还有另一种方式可以重置编辑器的全局配置,就是采用新的配置参数创建一个新的编辑器状态 state,并将它应用到编辑器的视图上
但是通过创建新的编辑器状态 state 方式来进行重配置,会清空所有数据;而通过
reconfigure类型的 state effect 进行重配置,则只是清除在新配置中未采用的全局配置/插件 - static
appendConfig静态属性:一个StateEffectType类的实例,该类型的 stateEffect 的具体内容是一个插件(需要符合一种 TypeScript typeExtension),用于将插件添加/追加到编辑器的全局配置里
StateEffect 类
StateEffect 类用于为 transaction 事务添加附加作用/效果,通常与 state field 自定义的编辑器状态字段的修改相关
说明
一般 transaction 事务只包含编辑器状态对象 state 所发生的更改(文档内容或选区的变化)的描述,如果某个 state field 自定义的编辑器状态字段的更新,所需的信息并没有「隐藏」在编辑器状态里(例如该 state field 的更新并不依赖文档内容或选区的改变),则可以为 transaction 事务添加 state effects 自定义变更效应,手动显式地为 transaction 添加额外的信息,让该 state field 可以从 transaction 里得到相关信息,知道如何实现更新
换句话说,state effects 自定义变更效应允许开发者在 transaction 事务中,管理和应用除文档内容和选区之外的其他状态更新
静态方法
StateEffect 类提供了一个静态方法 static StateEffect.define(spec?) 用于创建一个新的 StateEffectType 对象实例
该类的实例化是需要通过调用相应类型 stateEffectType 对象的方法 stateEffectType.of(value),所得到的 state effect 对象的具体内容就是参数 value 的值
class StateEffect 实例化得到 stateEffect 对象,称为「自定义变更效应」,它具有一些属性:
value属性:该 state effect 的具体内容map(mapping)方法:基于给定的mapping(该参数是一个ChangeDesc类的实例,是该 state effect 所在事务的更改操作集,只包含位置相关的变化信息)将当前 state effect 进行映射/更新。该方法最后可能返回一个更新的 state effect 对象,或可能返回undefined以表示该 state effect 在映射过程中被移除批量映射
该类还提供了一个静态方法 static
StateEffect.mapEffects(stateEffects, mapping)可以将多个 state effect 进行批量映射,基于给定的mapping(该参数是一个ChangeDesc类的实例,只包含位置相关的变化信息)将一系列stateEffects(一个数组,其元素是一系列的 state effect 对象)进行映射/更新。该方法最后可能返回一个数组,其元素是一系列更新后的 state effect 对象
is(stateEffectType)方法:判断该 state effect 是否为给定的类型stateEffectType,该方法最后返回一个布尔值
该类还提供了一些静态属性,它们都是一些预设的 StateEffectType 类的实例(表示不同的自定义变更效应类型,可以用于创建相应类型的 state effect 对象实例):
区别
Annotation 类和 StateEffect 类都可以为 transaction 事务添加额外的信息
它们的实例化方式都类似:都是使用相应类型 annotationType 或 stateEffectType 的方法 of(value)
实例化
- annotation 的实例化
- 使用
Annotation类的静态方法Annotation.define()定义一个新的元信息类型annotationType实例对象
预设的元信息类型
也可以直接使用在
Transaction类中预设了一些元信息类型- static
time属性 - static
userEvent属性 - static
addToHistory属性 - static
remote属性
- 使用方法
annotationType.of(value)创建一个 annotation 元信息对象(其具体内容就是参数value的值) - 然后将所创建 annotation 元信息通过事务的配置参数添加到事务上
- 使用
- state effect 的实例化
- 使用
StateEffect类的静态方法StateEffect.define()定义一个新的类型stateEffectType实例对象
预设的元类型
也可以直接使用在
StateEffect类中预设了一些类型- static
reconfigure属性 - static
appendConfig属性
- 使用方法
stateEffectType.of(value)创建一个 state effect 自定义变更效应(其具体内容就是参数value的值) - 然后将所创建 state effect 自定义变更效应通过事务的配置参数添加到事务上
- 使用
但存适用场景是有区别的:
文档范围
RangeSet 类用于表示文档范围的集合,这些范围可以带有标签、可以相互重叠(这点和选区范围是不同)
这种数据结构可以让文档范围随着文档的修改进行高效地映射更新,它们一般用于存储诸如 decoration 装饰器或 gutter marker 侧边栏标记等内容
RangeValue 抽象类
RangeValue 是一个 TypeScript abstract class 抽象类,用于表示一个文档范围所关联的值,它给出了一些抽象方法和属性,以及一些具体的方法
抽象类
TypeScript abstract class 不直接实例化,由于它一般只定义了抽象的方法或属性(虽然也可以包含非抽象的方法或属性),即针对这些方法只是对入参和输出值的类型进行了约束,而没有给出这些方法的具体实现,针对属性也是只对属性值的类型进行了约束
抽象类可以被其他类继承,子类需要实现抽象类中的抽象方法和属性,所以一般是创建子类再使用(才能进行实例化)
所以这里的 abstract class RangeValue 抽象类不直接实例化,要先被继承,给出父类的抽象方法和属性的具体实现,构建出各种子类再使用
其中在 @codemirror/view 模块的 class Decoration 继承自 class RangeValue,该类表示装饰器(具体而言有 4 种不同类型的装饰器,分别使用 class Decoration 所提供的的 4 种不同的静态方法进行创建)
eq(otherRangeValue)方法:比较当前的文档范围所关联的值是否与给定的其他文档范围所关联的值otherRangeValue相同,该方法最后返回一个布尔值
该方法会用于两个 range set 文档范围集的比较
默认比较方式是使用==直接比较两个 range value 对象是否相同,对于编辑器只创建了有限数量的 range value(或它的子类)实例时这个默认方式是可行 ❓ 但是最好还是在子类里覆写该方法,以更好地符合子类的应用场景- abstract
startSide抽象属性:一个数值(默认值为0),决定该文档范围的开始位置的优先级(可以将该属性看作是 bias 偏移量,决定当前文档范围与开始位置相同的其他文档范围的相对定位) - abstract
endSide抽象属性:一个数值(默认值为0),决定该文档范围的结束位置的优先级(可以将该属性看作是 bias 偏移量,决定当前文档范围与结束位置相同的其他文档范围的相对定位)
相邻装饰器的排序
文档范围一般与 decoration 装饰器相关,以上属性 startSide 和 endSide 可以控制重叠/相邻的装饰器的展示顺序
较大的 startSide 属性值表示该装饰器将在(覆盖的文档范围的开始位置相同)其他装饰器之后 ❓ 呈现;而较大的 endSide 值表示该装饰器将在(覆盖的文档范围的结束位置相同)其他装饰器之前呈现
mapMode属性:如果对文档的删除操作导致当前文档范围的两端from和to位置相同(覆盖的文档内容都删除了),则根据该属性的值(需要是 TypeScript enum MapMode 枚举类型中的一个值,默认值是MapMode.TrackDel)来决定该文档范围的映射模式point属性:一个布尔值,表示该文档范围是否视为一个 point range「点范围」(它会被视为一个原子单位,不能被分割或拆开)提示
文档范围一般是会覆盖一段文档内容的,如果所覆盖的内容为空则该文档范围是没有意义的
而 point range「点范围」则需要另外处理,当它为空时具有特殊的含义;当它非空时会视作一个整体处理,并会覆写/忽略包含在其内的其他文档范围
range(from, to?)方法:创建一个Range类的实例,它覆盖的范围是从文档的from到to
第二个(可选)参数to用于设置所覆盖范围的结束位置,默认值是to=from即采用与该范围的起点相同的位置
Range 类
Range 类用于表示一个文档范围,该类的实例化对象所具有的一些属性:
from属性:一个数值,表示该 range 的开始点to属性:一个数值,表示该 range 的结束点value属性:与该 range 所关联的值,它是一个对象,需要由继承自RangeValue抽象类的子类进行实例化
RangeSet 类
RangeSet 类用于表示一系列 Range 类的实例 文档范围的集合
注意
RangeSet 类提供了一些静态方法和属性进行实例化:
- static
RangeSet.of(ranges, sort?)静态方法:根据给定的文档范围ranges(该参数可以是一个Range类的实例;也可以是一个数组,它的元素是一系列Range类的实例)创建一个rangeSet对象
默认所传递的一系列 range 是已经排好序的,排序规则是 range 先根据所覆盖范围的开始点进行排序的,如果两个 range 的开始点相同,则再根据它们的属性value.startSide进行排序
也可以将第二个(可选)参数sort(默认值是false)设置为true让 CodeMirror 对传入的 ranges 进行排序 - static
RangeSet.join(sets)静态方法:将给定的一系列文档范围的集合sets(该参数是一个数组,它的元素是一系列RangeSet类的实例)合并为一个 rangeSet 文档范围的集合 - static
RangeSet.empty静态属性:获取一个空的rangeSet对象,表示没有包含任何 range
也可以使用另一个类 class RangeSetBuilder 进行实例化:
- 首先使用方法
new RangeSetBuilder()创建一个空的 rangeSet builder 对象 - 然后通过调用方法
rangSetBuilder.add(from, to, value)添加一个 range
注意
应该根据 range 所覆盖范围的先后顺序,依次调用以上方法为 rangeSet builder 添加 range
先后顺序是先按照 range 的属性(开始位置)from,再根据属性 value.startSide 来判断的
- 最后调用方法
rangeSetBuilder.finish()结束 range set 的构建,该方法返回一个rangeSet对象(相应的 rangeSet builder 对象不能再使用)
class RangeSet 实例化得到 rangeSet 对象,以下称作「文档范围集合」
rangeSet 文档范围集合对象包含一些属性和方法:
size属性:一个数值,该 rangeSet 中所包含的 range 文档范围对象的数量update(updateSpec)方法:更新该 rangeSet,可以添加新的 ranges,或删除原有的 ranges
参数updateSpec是一个对象,可以具有以下属性:add(可选)属性:一个数组,它的元素是一系列Range类的实例,它们会添加到该 rangeSet 里
默认所传递的一系列 range 是已经排好序的,排序规则是 range 先根据所覆盖范围的开始点进行排序的,如果两个 range 的开始点相同,则再根据它们的属性value.startSide进行排序。除非配置对象的另一个属性sort设置为true才会让 CodeMirror 对传入的 ranges 进行排序sort(可选)属性:一个布尔值(默认值是false),如果设置为true则会让 CodeMirror 对传入的 ranges 进行排序filter(可选)属性:一个函数fn(from: number, to: number, value: U) → boolean原有的 ranges 会依次调用该函数,参数from和to是当前所遍历的 range 的开始点和结束点位置,只有当函数返回值为true的 range 才保留下来filterFrom(可选)属性和filterTo(可选)属性:都是一个数值,表示原有 ranges 数组的索引值,只对指定的一小部分 range 元素进行过滤,让更新操作性能更佳
map(changes)方法:基于给定的mapping(该参数是一个ChangeDesc类的实例,是更改操作集,只包含位置相关的变化信息)将当前 rangeSet 进行映射/更新。该方法最后可能返回一个更新的 rangeSet 对象between(from, to, fn)方法:该 rangeSet 所包含的 ranges 如果触及给定范围从from到to的,会依次执行给定的回调函数fn(from: number, to: number, value: T) → false | undefined其中参数from和to是当前所遍历的 range 的两端位置,参数value是与当前所遍历的 range 所关联的值
⚠️ 无法保证这些 ranges 的遍历顺序
在遍历过程中,如果函数fn返回的值是false则停止后续的遍历iter(from?)方法:生成一个 range 指针(它是一个对象,需要符合一种 TypeScript interfaceRangeCursor),用于依序遍历 rangeSet 所含有的 range
(可选)参数from(默认值为0)表示文档的位置,所遍历的 range 的结束位置要在该位置上或之后RangeCursor
TypeScript interface
RangeCursor描述了一个 range 指针,用于遍历 rangeSet 所含有的 ranges(依次得到这些 range 所关联的值)对比
TypeScript interface
RangeCursor所描述的对象和 ES6 迭代器类似,但也有所不同的:- JavaScript 迭代器(由Iterator 迭代器协议所定义)需要执行一次
next()方法,才可以读取到第一个值 - 符合 TypeScript interface
RangeCursor的对象,初始状态就指向了 rangeSet 里的第一个 range 所关联的值
符合 TypeScript interface
RangeCursor的对象所具有的方法和属性:next()方法:执行一次迭代操作(以获取下一个 range 所关联的值)value属性:当前所遍历的 range 所关联的值。如果所有 range 都已经迭代完后继续调用了next()方法,则返回值是nullfrom属性:一个数值,表示当前所遍历的 range 的起始点位置to属性:一个数值,表示当前所遍历的 range 的结束点位置
遍历多个 rangeSet 的 ranges
该类提供了一个类似的静态方法 static
iter(rangeSets, from?)生成一个 range 指针(它是一个对象,需要符合一种 TypeScript interfaceRangeCursor),用于依序遍历给定的一系列rangeSets(一个数组,它的元素是一系列rangeSet对象)所含有的 range(可选)参数
from(默认值为0)表示文档的位置,所遍历的 range 其开始位置要在该位置上- JavaScript 迭代器(由Iterator 迭代器协议所定义)需要执行一次
该类还提供了一些静态方法,用于进行两组 rangeSets 之间的对比 ❓
- static
RangeSet.compare(oldSets, newSets, textDiff, comparator, minPointSize?)静态方法:比较两组 rangeSets(参数oldSets和newSets都是数组,它们的元素都是一系列rangeSet对象,分别表示更改前后的 rangeSets)
第三个参数textDiff是一个ChangeDesc类的实例,它描述了造成两组 rangeSets 不同的更改操作集(只包含位置相关的变化信息) ❓
第四个参数comparator是一个 range 比较器(它是一个对象,需要符合一种 TypeScript interfaceRangeComparator)RangeComparator
TypeScript interface
RangeCursor所描述的对象是一个 rangeSet 比较器,用于识别并处理旧的 rangeSet 和新的 rangeSet 的差异,例如处理装饰器(如语法高亮或光标位置)时非常有用它有两种方法分别对应两种类型的比较器:
- 方法
compareRange(from, to, activeA, activeB):当某一个文档范围 range(它在新文档中所覆盖的范围是from到to)所关联的值在新旧 rangeSet 里不同时,会调用此方法
参数activeA和activeB都是一个数组,元素都是该文档范围 range 所关联的值,activeA是在旧 rangeSet 中该文档范围所关联的一系列值,activeB则是在新 rangeSet 中所关联的一系列值说明
activeA和activeB都是一个数组,元素都是该文档范围 range 所关联的值即一个文档范围 range 是可以关联多个值的,这是由于一个范围可以同时关联多个不同的装饰器,它们叠加显示,比如代码可能会同时有颜色标记和下划线显示语法错误
- 方法
comparePoint(from, to, pointA, pointB):当某一个文档范围是 point range 「点范围」由于插入或删除操作而发生变化时,会调用该方法
参数pointA是在旧 rangeSet 中该 point range 所关联的数据(也可能是null);参数pointB在新 rangeSet 中该 point range 所关联的数据(也可能是null)
第五个(可选)参数minPointSize是一个数值(默认值为-1)用于控制在比较过程中忽略覆盖范围小于给定长度的文档范围,减少不必要的计算从而优化性能。该参数的默认值为-1表示所有文档范围都会被比较,如果将其设为正值,则所有覆盖文档内容的长度小于这个值的文档范围 range 将被忽略 - 方法
- static
RangeSet.eq(oldSets, newSets, from?, to?)静态方法:比较两组 rangeSets(参数oldSets和newSets都是数组,它们的元素都是一系列rangeSet对象,分别表示更改前后的 rangeSets)在给定范围(从from到to)里的 range 是否相同,该方法最后返回一个布尔值 - static
RangeSet.spans(sets, from, to, iterator, minPointSize?)静态方法:同时遍历sets(它是一个数组,元素都是一系列rangeSet对象)里每个 rangeSet 所包含的文档范围 range,如果所覆盖的范围是从from到to则调用iterator(它是一个 span range 迭代器,需要符合一种 TypeScript interfaceSpanIterator)
参数minPointSize是一个数值(默认值为-1)用于控制在比较过程中忽略覆盖范围小于给定长度的文档范围,减少不必要的计算从而优化性能。该参数的默认值为-1表示所有文档范围都会被比较,如果将其设为正值,则所有覆盖文档内容的长度小于这个值的文档范围 range 将被忽略SpanIterator
TypeScript interface
SpanIterator所描述的对象是一个 range 迭代器它有两种方法分别对应迭代两种类型 range:
span(from, to, active , openStart)方法:当遍历到 range 对应于非点装饰器时,调用此方法。参数from和to是该文档范围的起始点和结束点;active是一个数组,表示当前文档范围所关联的值point(from, to, value, active, openStart, index)方法:当遍历到 range 对应于点装饰器时,会调用此方法。参数from和to是该文档范围的起始点和结束点;value表示当前文档范围所关联的值;如果该范围有非点装饰器且优先级更高(覆盖掉该点装饰器 ❓ ),则这些范围所关联的值会添加到参数active(一个数组)里;index表示当前点装饰在该集合中的位置
openStart是一个数值,表示迭代过程中,有多少个范围的起始点是在给定范围之前(而且它们整体覆盖范围是跨越了from位置),该参数主要作用是帮助高效地处理和跟踪重叠或延续的 range 文档范围
该方法最后返回一个数值,表示多少个 range 在范围结束时依然处于「打开」状态,即 range 整体覆盖范围跨越了to位置 ❓