CodeMirror State 模块

codemirror

CodeMirror State 模块

编辑器状态

编辑器状态 state 的最基本形式由一个文档(内容)对象和一个选区对象构成,也可以通过插件对其进行扩展(在编辑器状态里添加其他字段 field

EditorState 类

EditorState 类提供了一些静态方法进行实例化:

  • static EditorState.create(config?) 静态方法:基于(可选)配置对象 config(它需要符合一种 TypeScript interface EditorStateConfig,默认值是空对象 {})创建一个新的 state 状态对象
    EditorStateConfig

    TypeScript interface EditorStateConfig 描述编辑器状态可接受哪些配置参数

    • doc(可选)属性:设置编辑器初始内容,它的值可以是一个字符串,会基于 lineSeparator (facet 动态可配置项)的值作为分隔符,对其进行分割转换为一行行的内容;也可以是一个 Text 类实例
    • selection(可选)属性:设置编辑器的初始选区,它的值可以是一个 EditorSelection 类的实例,也可以是一个对象 {anchor: number, head⁠?: number}(该对象的两个属性分别表示选区两端所在文档中的位置)
    • extensions(可选)属性:设置应用到编辑器状态上的插件,它的值可以有多种形式(需要符合一种 TypeScript type Extension
      Extension

      TypeScript type Extension 是一种递归类型,用于表示嵌套结构,以下称为「插件」

      ts
      export type Extension = {extension: Extension} | readonly Extension[]
      

      它可以是一个对象或一个数组。如果是对象则它具有属性 extension 值的类型也是 Extension;如果是数组,元素的类型也是 Extension

      所以属性 extensions 的值是十分灵活的、具有扩展性的,开发者在使用的时候(可以将插件进行嵌套组合)不必担心嵌套层次问题,CodeMirror 最终会将它们 flatten 扁平化

      但该类型并没有具体指定嵌套的终点/叶子节点可以包含什么类型的值,实际上 CodeMirror 是在(核心库)内部指定/约束了一些具体的类型可以「视为Extension 类型,它们才可以作为终点/叶子节点

      另外还有一些类型是基于上述核心库的几种类型进行扩展,也提供视为 Extension 终点/叶子节点类型的值,例如(通过 Facet.define() 生成)keymap 扩展点,可以通过调用 keymap.of() 创建一个插件

    提示

    通常只在初始化编辑器时才需要创建一个全新的 state 编辑器状态对象,一般通过分发 transaction 事务(基于旧的/原有编辑器状态)创建一个新的 state

  • static EditorState.fromJSON(json, config?, fields?) 静态方法:基于(可选)配置参数 config(它需要符合一种 TypeScript interface EditorStateConfig,默认值是空对象 {})将 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 且没有寻找到该字段,则返回 undefined
  • update(...specs) 方法:接受一系列的的配置对象(剩余参数 ...specs 将函数所接受的所有参数打包为一个数组,每一个元素都是一个对象,它们都需要符合一种 TypeScript interface TransactionSpec,用于描述/配置 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 interface TransactionSpec
    提示

    符合 TypeScript interface TransactionSpec 的对象用于描述/配置 transaction 事务

    可以将一系列这些对象作为参数,传递给编辑器状态对象的方法 editorState.update(),以创建一个 transaction 事务实例

  • changes(spec?) 方法:根据给定的更改描述 spec 创建一个文档更改集
    (可选)参数 spec 是一个数组(其中每个元素都需要符合一种 TypeScript type ChangeSpec,用于描述文档的更改)
    该方法最后返回一个 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 type ChangeSpec,用于描述文档的更改),表示对于当前所遍历的选区范围进行更改(其中对于修改位置的描述,是基于当前未更改的文档)
    • (可选)属性 effects 是一个 StateEffect 类的实例或一个数组(它的元素是一系列 StateEffect 类的实例),表示自定义变更效应

    将每个选区的更新整合起来,该方法最后返回一个对象 { changes: ChangeSet, selection: EditorSelection, effects: readonly StateEffect<any>[] } 它符合 TypeScript interface TransactionSpec(可以将它传递给编辑器状态对象的方法 editorState.update(),以创建一个 transaction 事务实例)
  • toText(str) 方法:基于将给定的 str 字符串(使用 lineSeparator facet 动态可配置项所设置的分隔符,对其进行分割转换为一行行的内容)创建一个 Text 类实例
  • sliceDoc(from⁠?, to⁠?) 方法:获取给定选区范围的文档内容(字符串)。第一个(可选)参数 from 是一个数字,表示选区范围的开始(默认值是 0 表示编辑器整个文档的开头);第二个(可选)参数 to 是一个数字,表示选区范围的结束(默认值为 this.doc.length 表示编辑器整个文档的结尾)
  • facet(facetObj) 方法:获取给定的 facetObj facet 实例的输出值
    FacetReader

    在官方文档中上述方法的 TypeScript 完整描述是 facet<Output>(facet: FacetReader<Output>) → Output 所以入参值需要符合 TypeScript type FacetReader

    该类型描述了一个 facet reader 动态配置项读取器

    由于 class Facet implements 实现了 TypeScript type FacetReader所以这里参数实际需要传入的是一个 facet 实例

  • toJSON(fields) 方法:将当前的编辑器状态序列化为一个 JSON 对象,(可选)参数 field 是一个对象,用于设置自定义的编辑器状态字段应该如何序列化
    自定义编辑器状态字段

    使用静态方法 EditorState.fromJSON(json, config?, fields?) 可以将给定的 json JSON 序列化为编辑器状态对象

    这里静态方法的第三个(可选)参数 fields,和以上方法的(可选)参数需要相对应(更精确来讲两者应该要相同,才可以无缝地进行序列化和反序列化),都是一个对象 {[prop: string]: StateField<any>} 以键值对的方式表示编辑器状态对象中所含有的自定义的字段

    其中键名是字符串,表示自定义的字段名称,对应的值是 class StateField 实例对象,它包含了如何对该自定义字段的数据进行序列化(或反序列)

  • tabSize 属性:一个数字,表示编辑器中制表符 \t 所占据的列宽 column number
    使用 facet 进行设置

    可以通过插件对 tabSize facet 动态可配置项进行设置,以更改上述属性

  • lineBreak 属性:一个字符串,它作为换行符
    使用 facet 进行设置

    可以通过插件对 lineSeparator facet 动态可配置项进行设置,以更改上述属性

  • readOnly 属性:一个布尔值,表示编辑器是否为只读
    使用 facet 进行设置

    可以通过插件对 readOnly facet 动态可配置项进行设置,以更改上述属性

  • 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')
    翻译映射表

    通过插件对 phrases facet 动态可配置项进行设置(输入值是一个对象,以键值对的形式为一些短语提供翻译版本),为编辑器添加翻译映射表

    然后调用以上方法 editorState.phrase(str) 时会在预设的映射表里寻找相应的翻译版本

  • languageDataAt(name, pos, side⁠?) 方法:在特定文档位置 pos(和方向 side)获取给定的类型 name 与编程语言相关的数据
    与编程语言相关的数据

    如果编辑器要实现一些「智能」交互功能,例如提供 autocomplete 内容自动补全(提示),则编辑器就需要有能力在特定位置获取到与编程语言相关的数据(一般是基于上下文语境和编程语言的预定规则计算而得)

    该方法可以实现上述的需求,获取特定位置给定类型的元信息

    关于编程语言相关的数据可以细分为几种类型(即方法 languageDataAt() 的第一个参数),例如

    • "commentTokens" 类型:与注释相关
    • "autocomplete" 类型:与自动补全相关
    • "wordChars" 类型:将单词进行分类 ❓
    • "closeBrackets" 类型:控制闭合括号的行为
    • indentOnInput 类型:控制缩进行为

    该方法最后返回一个数组,每个元素(数据类型可以各异)都是于一个与编程语言相关的数据点,且这些数据点的类型是相同的(例如都是与自动补全相关的)
    流程解释

    方法 languageDataAt 用于获取元信息,而设置语言相关的元信息则是通过 facet 动态配置项 languageData,它的输入值是一系列的函数,称为 language data provider,这些函数返回一个对象(以键值对的形式)表示与编程语言相关的数据

    在调用方法 editorState.languageDataAt() 获取语言语法相关的元信息时,就会依次执行预设的一系列 providers,然后从它们的返回值(一个个对象,以键值对的形式表示与编程语言相关的数据)中提取出属性名称与参数 name 相同的,将属性值整合为一个数组返回

    具体可以参考 languageDataAt 方法的源码

  • charCategorizer(at) 方法:在给定的文档位置(以限制上下文语境)创建一个字符分类器 fn(char: string) → CharCategory(即该方法返回值是一个函数)
    字符分类器所接收的参数 char 是需要分类的字符(它需要是一个单一的 grapheme cluster 字素簇,即视觉上表示一个字符,即使它可能对应多个 Unicode 字符)
    字符分类器的返回值(需要是 TypeScript enum CharCategory 枚举类型中的一个值)可以是 CharCategory.WordCharCategory.SpaceCharCategory.Other 这三个值之一
    • Word 表示该字符归类为字母数字类型(或者在当前编程语言的 "wordChars" 类型 language data 中显式设置的字符串)
    • Space 表示该字符归类为空白/空格字符类型
    • Other 表示该字符归类为其他类型
    用途

    对字符进行简单的分类,可以用于实现一些以单词为单位的操作,例如按住 Shift+Ctrl+LeftArrow 向前选中一个单词

  • wordAt(pos) 方法:在给定的文档位置 pos 附近寻找一个单词的范围
    该方法的返回值有两种类型
    • 如果能够在给定位置(前后)找到一个单词,则返回一个 SelectionRange 类的实例(表示该单词的范围)
    • 如果无法在给定位置(及其相邻位置)找到单词,例如该位置前后都是空格,则返回 null

另外该类还提供其他的静态属性,它们是编辑器预设的 facet 动态配置项(可通过插件对其值进行设置)

如何使用插件设置动态配置项

通过调用动态配置项的方法 facet.of(value) 返回一个 extension 插件

该插件将给定的值 value 设置为/添加到 facet 动态配置项

然后将该插件应用到编辑器上(在实例化编辑器状态对象时,添加到配置对象的属性 config.extensions 上),就可以完成动态配置项的设置

ts
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 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 动态配置项,其输入值是一个函数,其中参数 posside 可以知道所需分析的位置、方向;其输出值是一个数组,包含一些编程语言相关的信息

    官方在 @codemirror/language 模块对上述的 facet 进行了设置,可以根据当前代码编辑器的内容所属的编辑器语言,获取与编程语言相关的数据

    也可以通过调用方法 EditorState.languageData.of() 创建一个插件,添加其他额外的方式获取与编程语言相关的数据,例如 EditorState.languageData.of(() => globalLanguageData) 对于文档的任意位置都执行给定的函数,获取与编程语言相关的信息


    输入值是一个函数 fn(state: EditorState, pos: number, side: -1 | 0 | 1) 所接受三个参数具体说明如下:
    • 第一个参数 state 是一个编辑器的状态对象
    • 第二个参数 pos 是一个数值,表示编辑器文档的位置
    • 第三个参数 side 可以是 -101 这三个数值的任意一个,分别表示所需获取数据相对于给定位置的之前方向、当下方向、之后方向

    该函数最后返回一个对象数组,在对象里以键值对的形式表示与编程语言相关的数据
    输出值是一个数组(它是对输入值的封装),每个元素都是一个函数(集输入值所设置的函数)
    具体使用

    通过插件可以为上述 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],表示不允许触及文档位置从 1020 选区范围,以及从 100110 选区范围内的更改被执行
  • 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 是按需生成,并且可能在之后被丢弃(如果该事务被其他过滤器禁止执行)

  • static transactionExtender 静态属性:一个 Facet<fn(tr: Transaction) → Pick<TransactionSpec, "effects" | "annotations"> | null> 实例对象,和前面的 facet transactionFilter 作用类似,可以在相应的 transaction 应用到编辑器之前,对其配置进行更新(增添 annotations 和/或 effects 属性)
    通过插件对上述 facet 动态配置项进行设置,为编辑器注册一个 transaction extender 事务扩展器(对事务的配置进行更新)
    输入值是一个函数 fn(tr: Transaction) 它的参数就是当前分发的事务对象
    注意

    每次编辑器 dispatch transaction 分发事务时,即使在该事务的配置对象中将属性 filter 设置为 false,上述 facet 所设置的函数都会执行

    所以该 facet 会对所有事务进行处理

    如果编辑器同时设置了 transactionFilter facet 和上述的 transactionExtender facet,则会先执行前者所设置的函数,再执行后者所设置的函数


    输出值有两种形式
    • 可以是一个对象,只能包含属性 annotations 和/或 effects(这两个属性的类型约束摘取自 TypeScript interface TransactionSpec 里)
    • 可以是 null
    区别

    虽然和 transactionFilter facet 作用类似,都会在事务应用到编辑器之前对其进行处理

    但是通过该 facet 所设置的扩展器对 transaction 的更新的范围是受到限制的,它不会触及/影响关于文档内容或选区的变更,只能用于为事务添加 annotations 元信息和设置 effects 自定义变更效应

    所以该 facet 输出值的其中一种形式是对象,只能包含 annotations 和/或 effects 属性


编辑器配置可以动态修改,在下面的子标题里进行介绍

Compartment 类

参考

compartment 是一个配置隔离区(实际是一个插件容器,所包含的插件会带有与编辑器配置相关的信息),其中所包含的配置可以在编辑器初始化之后,通过分发 transaction 进行修改

插件

插件一般用于为 facet 设置值或为编辑器添加 state field,其中包含了与编辑器相关的配置信息

插件支持嵌套结构,还可以设置优先级,在初始化编辑器状态时,CodeMirror 会对配置进行解析,将一系列插件整合为一个数组(扁平结构),属于编辑器的全局配置

而 compartment 所包裹的插件会从全局配置分隔开来,然后允许通过分发 transaction 事务的方式来更新相应的编辑器配置

通过方法 new Compartment() 进行实例化,得到一个 compartment 对象,以下称作「配置隔离区」

实例化的简化形式

class Compartment 该类并没有设置 constructor 构造函数,所以实例化时并不需要传递参数,则实例化方法可以简写为 new Compartment

但是更推荐带括号的形式,这更符合编程习惯

Compartment 类实例化对象 compartment 配置隔离区,包含一些属性和方法:

  • of(ext) 方法:使用该配置隔离区包裹给定的 ext 插件
    参数 ext 需要符合一种 TypeScript type Extension
    该方法返回一个对象(它也需要符合 TypeScript type Extension),可以将其视为经过配置隔离区封装的插件,然后将该插件应用到编辑器上(在实例化编辑器状态对象时,添加到配置对象的属性 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 的创建和使用流程

  1. 使用方法 new Compartment() 进行初始化,得到一个 compartment 配置隔离区对象
  2. 使用方法 compartment.of(extension) 包裹插件
  3. 使用方法 compartment.reconfigure(newExtension) 创建一个 state effect,添加到 transaction 事务的属性 effects 上,通过分发事务来更新配置

例如让 tab size 缩进量相关的配置可以动态更改

ts
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,可以进行如下设置

ts
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] 它的入参 firstsecond 分别表示该属性的两个值,在该函数内部的编写代码决定如何将两者整合输出为一个值,作为该属性在最终配置对象的值
      如果某一个属性获得两个不相同的值,但是没有在 combine 里设置相应的整合函数,则默认行为是抛出错误
  • compare(可选)属性:一个函数 fn(a: Output, b: Output) → boolean 用于设置该 facet 如何比较两个输出值(参数 ab 是需要比较的值,所以这两个参数的类型都要符合该 facet 的输出值格式),最后返回一个布尔值,以判断该 facet 是否发生了变化
    默认情况下,直接使用三等号 === 对两个传入的参数值进行比较;如果前面的属性 combine 没有进行设置,则会直接将包含输入值封装为一个数组返回,即参数 ab 都会是数组,则默认会分别对它们的相应元素一一进行比较(也是使用 === 三等符号)
  • 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 type Extension
    提示

    当编辑器使用该 facet 时(例如通过方法 facet.of(value) 得到一个 extension 插件,然后在实例化编辑器状态对象时,将该插件作为/添加到配置对象的 extensions 属性上),该属性所设置的一系列插件会自动应用到编辑器上

    例如在 @codemirror/language 模块所提供的 language facet 动态可配置项预设了一系列的依赖插件

Facet 类实例化对象 facet 动态配置项,包含一些属性和方法:

  • reader 属性:返回该 facet 对象自身。它符合 TypeScript type FacetReader,表示它只能用于读取该 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 type FacetReader(实际就是要传入一个 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 type FacetReader 的对象(实际就是要一个 facet 实例)

    class Facet 需要 implements 实现一种 TypeScript type FacetReader所以任何需要采用 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 type FacetReader

        该类型描述了一个 facet reader 动态配置项读取器

        由于 class Facet implements 实现了 TypeScript type FacetReader所以任何需要采用 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
  • keymap
  • gutterLineClass
  • gutterWidgetClass
  • lineNumberMarkers
  • lineNumberWidgetMarker
  • showTooltip
  • showPanel

@codemirror/language 模块预设了一些 facet 实例(以便通过插件对与编程语言数据的相关配置进行设置),直接作为变量导出:

  • language
  • foldService
  • indentService
  • indentUnit

@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 的两个值 ab,最后返回一个布尔值,以判断它们是否相同( ❓ 语义上的相同,而不一定是两个值完全相同)
    该方法用于避免(依赖于该 state field 的)facet 的无效计算
    默认使用三等号 === 对传入的参数值进行比较
  • provide(可选)属性:一个函数 fn(field: StateField<Value>) → Extension 用于设置依赖于该 state field 的相关插件
    该函数的入参 field 是当前 state field 实例
    最后返回一个对象或数组(需要符合一种 TypeScript type Extension
    自动加载相关插件

    当编辑器使用该 state field 时(在实例化编辑器状态对象时,将它作为插件添加到配置对象的 extensions 属性上),该属性所设置的一系列插件会自动应用到编辑器上

    例如有一个 facet myFacet 需要依赖 state field myStateField,则可以进行如下设置

    ts
    const myFacet = Facet.define();
    
    const myStateField = StateField.defined({
      create() {
        // ...
      },
      update() {
        // ...
      }
      provide: field => {
        return myFacet.from(field, fieldValue => {
          // ...
          // return the new Input value for myFacet
        })
      }
    })
    

    然后只需要将 myStateField 应用到编辑器上,则 facet myFacet 也会自动加载(而不需要显式手动添加)

    ts
    const state = EditorState.create({ extensions: myStateField })
    

    官方模块所提供的一些 state field 也设置了需要自动加载的插件,例如在 @codemirror/autocomplete 模块所提供的 completionState state 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 编码的字符计作 1unit,而 astral characters 超出 Unicode 标准 BMP 范围的字符(范围为 U+0000U+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 TextLeafclass TextNode,它们都是继承自 Text 类(在其中实现了 abstract class Text 所定义的抽象方法和属性)

TextLeaf 子类会含有具体的文档内容,它表示文档中一段,即几行文本,可以将其视为结构化文档的叶子节点

TextNode 子类并不含有具体的文档内容,它包含 TextLeaf 或其他 TextNode 实例,可以将其视为容器/父节点

在实例化 Text 类时不能使用常见的方法 new Text(),而需要使用静态方法 static Text.of(strArr) 进行实例化(如果需要得到一个空的文档,则使用静态属性 static Text.empty),在该方法的内部会根据行的数量选择创建 TextLeaf 类的实例,或创建一个 TextNode 类的实例,以表示该结构化文档

大致流程如下:

  • 当文档的行数较少时,只需要创建一个 TextLeaf 类的实例,就可以表示整个文档内容,采用扁平化结构来表示文档
  • 当文档的行数较多时,则需要对文档进行划分,采用树形结构来表示文档
    1. 将文档划分为多个 TextLeaf 类的实例来表示(每个 textLeaf 最多包含 252^5 行,即 32 行),然后再创建 TextNode 类的实例将一定数量 textLeaf 实例进行包裹
    2. 如果 TextNode 数量较多时,还会将继续进行划分,即创建 textNode 作为父节点,对它们进行包裹
    3. 依此类推,构成一个具有多层嵌套的树形结构

    树形结构的根部是一个数组,它包含一系列 textNode 作为最顶层的容器,然后它们里面包含着一系列的 textNode,这些子容器又可能包含一系列的 textNode,在最深的层级里则是一系列的 textLeaf
    对于大文档使用树形结构来表示便于对大文档的内容进行快速索引

更多关于 TextLeafTextNode 的介绍可参考:

class Text 实例化得到 text 对象,以下称作「结构化文档」,它包含一些属性和方法:

注意

Text 类的实例化对象是 immutable 持久化的,即不应该直接修改它(及其属性),而应该基于原有的文档内容创建一个新的 text 对像

  • length 属性:一个数值,表示该结构化文档所包含的字符数量
  • lines 属性:一个数字,表示该结构化文档的行数(总是大于或等于 1
  • lineAt(pos) 方法:获取给定位置 pos 所在的行,该方法的返回值是一个 Line 类的实例
  • line(n) 方法:获取索引值为 n 的行(文档的行索引值从 1 开始),该方法的返回值是一个 Line 类的实例
  • replace(from, to, content) 方法:用给定的内容 content(该参数是一个 Text 类实例)替换当前结构化文档中从 fromto 范围的内容
    该方法返回一个新的 text 对象,其内容是(原始文档经过替换操作后的)整个文档内容
  • append(content) 方法:将给定的内容 content(该参数是一个 Text 类实例)追加到当前结构化文档的末尾
    该方法返回一个新的 text 对象,其内容是(原始文档追加了新增内容后的)整个文档内容
  • slice(from, to?) 方法:基于当前文档从 fromto 范围的内容,创建一个新的 text 对象
    (可选)参数 to 是一个数值,设置获取文档范围的结束位置。如果忽略,则采用默认值 to=this.length 即结束位置是在结构化文档的末尾
  • sliceString(from, to?, lineSep?) 方法:获取当前文档从 fromto 范围的内容,返回一个字符串
    (可选)参数 to 是一个数值,设置获取文档范围的结束位置。如果忽略,则采用默认值 to=this.length 即结束位置是在结构化文档的末尾
    (可选)参数 lineSep 是一个字符串,用于设定换行符,用于将文档的每一行内容连接起来形成一个字符串
  • eq(otherText) 方法:比较当前结构化文档与给定的其他结构化文档 otherText 是否相同,该方法最后返回一个布尔值
  • iter(dir?) 方法:返回一个文档内容迭代器(它是一个对象,需要符合一种 TypeScript interface TextIterator),可用于按行迭代文档的内容(每次迭代返回的值都是字符串,交替输出行的内容,或两行之间的分隔符)
    (可选)参数 dir 可以是 1(默认值)或 -1,表示迭代的方向,如果为 -1 时,则从后往前迭代文档的内容
    TextIterator

    TypeScript interface TextIterator extends 扩展了 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 interface TextIterator),可用于按行迭代文档指定范围从 fromto 的内容(每次迭代返回的值都是字符串,交替输出行的内容,或两行之间的分隔符)
    参数 fromto 是一个数值,根据字符偏移量来表示文档的位置
    (可选)参数 to 如果省略,则采用默认值 to=this.length 即结束位置是在结构化文档的末尾
    如果参数 from>to 则从后往前迭代文档指定范围的内容
  • iterLines(from?, to?) 方法:返回一个文档内容迭代器(它是一个对象,需要符合一种 TypeScript interface TextIterator),可用于按行迭代文档从第 from 行到第 to 行的内容(每次迭代返回的值都是行的内容,不包括两行之间的分隔符,只有当所迭代行的内容为空时才返回空字符串)
    (可选)参数 fromto 都是一个数值,表示文档中某一行(行的索引值从 1 开始),用于指定迭代的行的范围
    如果设定了参数 to 即指定了迭代的结束行,则迭代时到达该行之前会停止了,实际迭代内容是不包含该行的
  • toString() 方法:将该结构化文档的内容转换为一个字符串(每一行的内容使用换行符 \n 进行连接)
  • toJSON() 方法:将该结构化文档的内容转换为一个数组(其元素都是字符串,分别表示文档中相应行的内容)
    可以将该方法的返回值作为参数传递给 static Text.of(strArr) 静态方法,进行反序列化 deserialized 创建一个 text 对象
  • children 属性:它是一个数组(其元素是 Text 类的实例)或 null
    如果文档内容较多(行数多于 252^532 行),则 CodeMirror 在内部会将结构化文档进行划分,创建一系列 TextNode的实例(该继承自 Text 类,即它是子类)来表示,则整个文档会形成一个具有嵌套层级的树结构。可以将该属性 children 理解为获取树形结构的根节点的直接子节点(一系列的 TextNode 节点)
    如果文档内容较少,则仅需要使用一个 TextLeaf 类的实例就可以表示整个文档。该属性 children 返回 null(表示根节点没有直接子节点,由于文档并不需要复杂的嵌套层级来表示)
  • [Symbol.iterator]() 方法:返回一个迭代器
    迭代器

    class Text implements 实现了 iterable protocol 可迭代协议

    根据此协议,符合该协议的对象必须实现 [Symbol.iterator]() 方法,该方法返回一个迭代器(符合iterator protocol 迭代器协议的对象)

    该属性只是用于 TypeScript 编译系统中 ❓ 以提升类型系统的准确性(让 TypeScript 识别到该类的实例化对象是符合可迭代协议的,以便更好地捕获类型错误,实现类型安全),而在运行时实际的 JS 对象上,需要通过调用方法 text.itertext.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 字符串的末尾位置,即默认计算整个字符所占的列宽)
    列宽

    字符串所占的列数量/列宽对于内容排版和文档定位很重要

    一般是一个字符占一列,但是对于制表符 \t(它一般用于排版,可以自定义所占的列宽)和特殊的字符(例如一些表情符号),则需要另外处理

    方法 countColumn() 会把这些因素考虑进去,精确计算给定字符串渲染到页面时所需占用的列宽


    该方法最后返回一个数值,表示字符串 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 个,即该方法的返回值是 12
    提示

    在 Unicode 中,基本多文种平面 Basic Multilingual Plane (BMP) 包含了从 0x00000xFFFF 的字符;高位补充平面 Supplementary Multilingual Plane (SMP) 包含了从 U+10000U+1FFFF 的字符

    在 JavaScript 中字符的表示是基于 UTF-16 编码的,因此 BMP 和 SMP 字符在 JavaScript 中占据的字符数量如下:

    • 基本多文种平面字符 BMP 在 JavaScript 中占用 1 个字符,因为 BMP 字符可以用一个 16 位的码元(即一个 char)来表示
    • 高位补充平面字符 SMP 在 JavaScript 中占用 2 个字符,因为 SMP 字符在 UTF-16 中需要用代理对 surrogate pair 表示,每个代理对由两个 16 位的码元组成,因此在 JavaScript 中会被视为两个独立的字符
    js
    let bmpChar = 'A'; // U+0041, BMP 字符
    let smpChar = '𐍈'; // U+10348, SMP 字符
    // 查看字符长度
    console.log(bmpChar.length); // 输出: 1
    console.log(smpChar.length); // 输出: 2
    
  • 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 的详细介绍可以参考:

选区

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?) 静态方法:根据给定的(一个选区范围的)两端位置 anchorhead 创建一个 editorState 选区对象
    提示

    所创建的选区仅包含单个 range 选区范围,所以该静态方法称为 single


    参数 anchorhead 都是数值,分别表示 range 选区范围的两端(锚点和动点)在文档中的位置(采用字符偏移量来描述)
    其中参数 head 是可选的,默认值为 head=anchor 即采用与 anchor 一样的值,如果省略设置该参数,则创建的选区就是处于光标状态
  • static EditorSelection.fromJSON(json) 静态方法:基于给定的 json JSON 对象,创建一个 editorState 选区对象
    表示选区的 JSON 对象

    该 JSON 对象一般是通过调用选区对象的方法 editorState.toJSON() 生成的

    它需要满足以下结构

    js
    const 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 是一个数值,它可以是 10-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() 生成的

它需要具有两个属性

js
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 属性:一个数值,它可以是 10-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 以确保至少覆盖从 fromto 的范围
    参数 fromto 都是数值,表示文档中的位置(采用字符偏移量来描述)
    第二个(可选)参数 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 应用到编辑器上的大致流程:

  1. 创建一个 transaction 事务,它除了包含 changeSet(与文档更改相关的信息,例如文档内容的变化、选区的变化等),还可以添加 state effects 自定义变更效应
  2. 分发事务 dispatch transaction,编辑器的视图 view 会更新它的 state,并同步更新页面的 DOM 元素
ts
// 假设在编辑器视图 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 对象作为其参数:

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) 它的前两个参数 posAposB 都是一个数值,分别是当前所遍历的未修改部分在旧文档新文档中的位置,第三个参数 length 是一个数值表示当前所遍历的未修改部分的字符串长度
  • iterChangedRanges(fn, individual?) 方法:遍历(在这次更改中)文档中修改的部分,并分别调用给定的函数 fn
    第二个(可选)参数 individual 是一个布尔值(默认值为 false),用于设置如何处理影响的文档范围是连续的变更操作,如果为 false 则将它们进行整合视为一次变更,只遍历一次(将变更效果进行融合后)它们所对应的修改部分;否则就会依次单独遍历这些变更操作所对应的修改部分
    第一个参数 fn 是一个函数 fn(fromA: number, toA: number, fromB: number, toB: number) 它的前两个参数 fromAtoA 都是一个数值,分别是当前所遍历的修改部分在旧文档的开始和结束位置;相应地,后两个参数 fromBtoB 则是当前所遍历的修改部分在新文档中的位置
  • 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 enum MapMode 枚举类型中的一个值:
    • 可以是 MapMode.Simple:简单模式(默认值),该方法返回一个数值,即位置 pos 始终可以映射到一个有效的新位置
    • 或是 MapMode.TrackDel:如果删除操作跨过了给定置 pos,则该方法返回 null,该模式可用于跟踪/检测位置是否被删除
    • 或是 MapMode.TrackBefore:如果在给定置 pos 前面有字符被删除,则该方法返回 null,该模式可用于跟踪/检测位置前面是否发生删除
    • 或是 MapMode.TrackAfter:如果在给定置 pos 后面有字符被删除,则该方法返回 null,该模式可用于跟踪/检测位置后面是否发生删除
  • touchesRange(from, to?) 方法:返回一个布尔值,以表示该 changeDesc 是否触及/影响给定文档的特定范围 formto(可选,默认值是 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 type ChangeSpec,用于描述文档的更改)创建一个 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) 静态方法:基于给定的 json JSON 对象(该 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 而形成约束
    例如有两个更改集 AB 都可以作用于同一个文档(即它们的属性 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) 它的前两个参数 fromAtoA 都是一个数值,分别是当前所遍历的修改部分在旧文档的开始和结束位置;相应地,后两个参数 fromBtoB 则是当前所遍历的修改部分在新文档中的位置;最后一个参数 inserted 是当前所遍历的修改往文档插入的内容(该参数值是 Text 类的实例
  • desc 属性:获取该 changeSet 所对应的 文档更改集描述 changeDesc 形式(即不包含具体文本,只记录了位置变化)
  • toJSON() 方法:将该 changeSet 对象序列化为 JSON 对象

Transaction 类

Transaction 事务包含一系列对文档的更改操作 document change,或改变选区 selection,以及一些自定义变更效应 state effect

通过 transaction 事务驱动文档更新的大致流程:

  1. 一般是由用户交互触发更新,例如通过键盘输入文字,相应的 DOM 事件监听器会调用预设的响应函数(或点击菜单栏上的相应按钮,相应的指令 command 被分发,执行相应的回调函数)
  2. 在这些响应函数里,会调用编辑器状态对象的方法 editorState.update() 创建一个 transaction 事务对象
  3. 然后通过编辑器视图对象的方法 editorView.dispatch() 分发事务,将变更应用到编辑器上并同步更新页面的 DOM 元素

该类的实例化是需要通过调用编辑器状态对象的方法 editorState.update(...specs),该方法可以接受一系列的的配置对象作为参数(剩余参数 ...specs 将函数所接受的所有参数打包为一个数组,每一个元素都是一个对象,它们都需要符合一种 TypeScript interface TransactionSpec,用于描述/配置 transaction 事务)

TransactionSpec

TypeScript interface TransactionSpec 描述事务可接受哪些配置参数

  • change(可选)属性:该事务所包含的的文档更改操作,该属性值需要符合一种 TypeScript type ChangeSpec,用于描述文档的更改
  • 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 事件,引起分发当前事务,对文档进行修改

    一般流程如下:

    1. Transaction 类提供了一个静态属性 Transaction.userEvent,它是一个 AnnotationType 类的实例,调用它的方法 Transaction.userEvent.of(value) 可以创建一个 annotation 元信息对象,其中所传递的参数 value 是一个字符串,用于表示相应的 user event
    2. 然后将所创建 annotation 元信息添加到事务上

    而通过该属性则更方便,只需要一个特殊的字符串(CodeMirror 在核心模块里所预设/约定的一些字符串,就可以为当前事务添加与用户交互事件 user interface event 相关的 annotation 元信息(CodeMirror 在内部会自动创建 annotation 实例,并添加到当前事务上)

  • scrollIntoView(可选)属性:一个布尔值,表示是否要为该事务添加上标记,需要将编辑器的选区滚动入视口里
  • filter(可选)属性:一个布尔值,表示该事务是否可以接受过滤处理。如果设置为 false 则该事务会跳过所有过滤器的检查
    过滤器

    通过 change filtertransaction 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 类的实例,表示当前事务分发后,在新文档中的选区
    提示

    如果当前事务并没有显式/手动设置选区(即事务对象的属性 selectionundefined),则该属性会采用原始文档里的选区经过 map 映射(由于分发事务后修改文档内容,可能会影响选区的位置)后的版本

  • newDoc 属性:一个 Text 类的实例,表示应用该事务后新的文档内容(结构化文档)
  • state 属性:一个 EditorState 类的实例,表示应用该事务后新的编辑器状态
注意

Text 结构化文档只包含文档(文本)内容相关的信息,而 editorState 编辑器状态则更复杂,除了包含文档的文本内容,还包含选区,以及插件相关的状态

事务过滤器中如果要获取新文档内容的相关信息,推荐使用属性 newDoc 而不是 state,访问属性 newDoc 不需要计算整个新状态,因此性能代价较低

属性 state「懒计算」,即只有在实际访问时 CodeMirror 才会计算出整个新状态,由于它的运算代价较高,所以通常不建议随意访问(只有在需要完整的编辑器状态时才访问)

  • annotation(type) 方法:根据给定的元信息类型 type(该参数值是一个 AnnotationType 类的实例,标记/表示一种 annotation 元信息的类型),获取相应的元信息具体内容;如果该事务没有设置相应类型的元信息,则返回 undefined
  • docChanged 属性:一个布尔值,表示该事务是否更改了文档
    提示

    基于该事务对象是属性 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 对象,称为「元信息」,它具有一些属性:

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 type Extension),用于对编辑器的全局配置进行 reconfigure 重配置
    重配置

    使用上述 reconfigure 类型的 state effect 对编辑器的全局配置进行 reconfigure 重配置时,会清空编辑器原本的全局配置(包括使用 appendConfig 类型 state effect 往编辑器所追加的插件),但并不会重置通过 compartment 所隔离的插件

    例如以下代码可用于清空编辑器原来的全局配置

    ts
    import {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 type Extension),用于将插件添加/追加到编辑器的全局配置里

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 的实例化
    1. 使用 Annotation 类的静态方法 Annotation.define() 定义一个新的元信息类型 annotationType 实例对象
    预设的元信息类型

    也可以直接使用在 Transaction 类中预设了一些元信息类型

    • static time 属性
    • static userEvent 属性
    • static addToHistory 属性
    • static remote 属性
    1. 使用方法 annotationType.of(value) 创建一个 annotation 元信息对象(其具体内容就是参数 value 的值)
    2. 然后将所创建 annotation 元信息通过事务的配置参数添加到事务上
  • state effect 的实例化
    1. 使用 StateEffect 类的静态方法 StateEffect.define() 定义一个新的类型 stateEffectType 实例对象
    预设的元类型

    也可以直接使用在 StateEffect 类中预设了一些类型

    • static reconfigure 属性
    • static appendConfig 属性
    1. 使用方法 stateEffectType.of(value) 创建一个 state effect 自定义变更效应(其具体内容就是参数 value 的值)
    2. 然后将所创建 state effect 自定义变更效应通过事务的配置参数添加到事务上

但存适用场景是有区别的:

  • Annotation 元信息:所添加的信息通过与 transaction 事务的整体相关,例如 transaction 分发的时间戳来源
  • StateEffect 自定义变更效应:一般是由于 transaction 事务所包含的变更只是针对文档内容,而一些插件的更新(例如修改 state field 自定义的编辑器状态字段)需要其他的信息,则通过 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 装饰器相关,以上属性 startSideendSide 可以控制重叠/相邻的装饰器的展示顺序

较大的 startSide 属性值表示该装饰器将在(覆盖的文档范围的开始位置相同)其他装饰器之后 ❓ 呈现;而较大的 endSide 值表示该装饰器将在(覆盖的文档范围的结束位置相同)其他装饰器之前呈现

  • mapMode 属性:如果对文档的删除操作导致当前文档范围的两端 fromto 位置相同(覆盖的文档内容都删除了),则根据该属性的值(需要是 TypeScript enum MapMode 枚举类型中的一个值,默认值是 MapMode.TrackDel)来决定该文档范围的映射模式
  • point 属性:一个布尔值,表示该文档范围是否视为一个 point range「点范围」(它会被视为一个原子单位,不能被分割或拆开)
    提示

    文档范围一般是会覆盖一段文档内容的,如果所覆盖的内容为空则该文档范围是没有意义的

    而 point range「点范围」则需要另外处理,当它为空时具有特殊的含义;当它非空时会视作一个整体处理,并会覆写/忽略包含在其内的其他文档范围

  • range(from, to?) 方法:创建一个 Range 类的实例,它覆盖的范围是从文档的 fromto
    第二个(可选)参数 to 用于设置所覆盖范围的结束位置,默认值是 to=from 即采用与该范围的起点相同的位置

Range 类

Range 类用于表示一个文档范围,该类的实例化对象所具有的一些属性:

  • from 属性:一个数值,表示该 range 的开始点
  • to 属性:一个数值,表示该 range 的结束点
  • value 属性:与该 range 所关联的值,它是一个对象,需要由继承自 RangeValue 抽象类的子类进行实例化

RangeSet 类

RangeSet 类用于表示一系列 Range 类的实例 文档范围的集合

注意

RangeSet 类的实例化对象是 immutable 持久化的,即其不应该直接修改它(及其属性),而应该基于原有的 range set,通过方法 mapupdate 生成一个新的文档范围集合

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 进行实例化:

  1. 首先使用方法 new RangeSetBuilder() 创建一个空的 rangeSet builder 对象
  2. 然后通过调用方法 rangSetBuilder.add(from, to, value) 添加一个 range
注意

应该根据 range 所覆盖范围的先后顺序,依次调用以上方法为 rangeSet builder 添加 range

先后顺序是先按照 range 的属性(开始位置)from,再根据属性 value.startSide 来判断的

  1. 最后调用方法 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 会依次调用该函数,参数 fromto 是当前所遍历的 range 的开始点和结束点位置,只有当函数返回值为 true 的 range 才保留下来
    • filterFrom(可选)属性和 filterTo(可选)属性:都是一个数值,表示原有 ranges 数组的索引值,只对指定的一小部分 range 元素进行过滤,让更新操作性能更佳
  • map(changes) 方法:基于给定的 mapping(该参数是一个 ChangeDesc 类的实例,是更改操作集,只包含位置相关的变化信息)将当前 rangeSet 进行映射/更新。该方法最后可能返回一个更新的 rangeSet 对象
  • between(from, to, fn) 方法:该 rangeSet 所包含的 ranges 如果触及给定范围从 fromto 的,会依次执行给定的回调函数 fn(from: number, to: number, value: T) → false | undefined 其中参数 fromto 是当前所遍历的 range 的两端位置,参数 value 是与当前所遍历的 range 所关联的值
    ⚠️ 无法保证这些 ranges 的遍历顺序
    在遍历过程中,如果函数 fn 返回的值是 false 则停止后续的遍历
  • iter(from?) 方法:生成一个 range 指针(它是一个对象,需要符合一种 TypeScript interface RangeCursor),用于依序遍历 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() 方法,则返回值是 null
    • from 属性:一个数值,表示当前所遍历的 range 的起始点位置
    • to 属性:一个数值,表示当前所遍历的 range 的结束点位置
    遍历多个 rangeSet 的 ranges

    该类提供了一个类似的静态方法 static iter(rangeSets, from?) 生成一个 range 指针(它是一个对象,需要符合一种 TypeScript interface RangeCursor),用于依序遍历给定的一系列 rangeSets(一个数组,它的元素是一系列 rangeSet 对象)所含有的 range

    (可选)参数 from(默认值为 0)表示文档的位置,所遍历的 range 其开始位置要在该位置上


该类还提供了一些静态方法,用于进行两组 rangeSets 之间的对比 ❓

  • static RangeSet.compare(oldSets, newSets, textDiff, comparator, minPointSize?) 静态方法:比较两组 rangeSets(参数 oldSetsnewSets 都是数组,它们的元素都是一系列 rangeSet 对象,分别表示更改前后的 rangeSets)
    第三个参数 textDiff 是一个 ChangeDesc 类的实例,它描述了造成两组 rangeSets 不同的更改操作集(只包含位置相关的变化信息) ❓
    第四个参数 comparator 是一个 range 比较器(它是一个对象,需要符合一种 TypeScript interface RangeComparator
    RangeComparator

    TypeScript interface RangeCursor 所描述的对象是一个 rangeSet 比较器,用于识别并处理旧的 rangeSet 和新的 rangeSet 的差异,例如处理装饰器(如语法高亮或光标位置)时非常有用

    它有两种方法分别对应两种类型的比较器:

    • 方法 compareRange(from, to, activeA, activeB):当某一个文档范围 range(它在新文档中所覆盖的范围是 fromto)所关联的值在新旧 rangeSet 里不同时,会调用此方法
      参数 activeAactiveB 都是一个数组,元素都是该文档范围 range 所关联的值,activeA 是在旧 rangeSet 中该文档范围所关联的一系列值,activeB 则是在新 rangeSet 中所关联的一系列值
      说明

      activeAactiveB 都是一个数组,元素都是该文档范围 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(参数 oldSetsnewSets 都是数组,它们的元素都是一系列 rangeSet 对象,分别表示更改前后的 rangeSets)在给定范围(从 fromto)里的 range 是否相同,该方法最后返回一个布尔值
  • static RangeSet.spans(sets, from, to, iterator, minPointSize?) 静态方法:同时遍历 sets(它是一个数组,元素都是一系列 rangeSet 对象)里每个 rangeSet 所包含的文档范围 range,如果所覆盖的范围是从 fromto 则调用 iterator(它是一个 span range 迭代器,需要符合一种 TypeScript interface SpanIterator
    参数 minPointSize 是一个数值(默认值为 -1)用于控制在比较过程中忽略覆盖范围小于给定长度的文档范围,减少不必要的计算从而优化性能。该参数的默认值为 -1 表示所有文档范围都会被比较,如果将其设为正值,则所有覆盖文档内容的长度小于这个值的文档范围 range 将被忽略
    SpanIterator

    TypeScript interface SpanIterator 所描述的对象是一个 range 迭代器

    它有两种方法分别对应迭代两种类型 range:

    • span(from, to, active , openStart) 方法:当遍历到 range 对应于非点装饰器时,调用此方法。参数 fromto 是该文档范围的起始点和结束点;active 是一个数组,表示当前文档范围所关联的值
    • point(from, to, value, active, openStart, index) 方法:当遍历到 range 对应于点装饰器时,会调用此方法。参数 fromto 是该文档范围的起始点和结束点;value 表示当前文档范围所关联的值;如果该范围有非点装饰器且优先级更高(覆盖掉该点装饰器 ❓ ),则这些范围所关联的值会添加到参数 active(一个数组)里;index 表示当前点装饰在该集合中的位置

    openStart 是一个数值,表示迭代过程中,有多少个范围的起始点是在给定范围之前(而且它们整体覆盖范围是跨越了 from 位置),该参数主要作用是帮助高效地处理和跟踪重叠或延续的 range 文档范围


    该方法最后返回一个数值,表示多少个 range 在范围结束时依然处于「打开」状态,即 range 整体覆盖范围跨越了 to 位置 ❓

Copyright © 2025 Ben

Theme BlogiNote

Icons from Icônes