CodeMirror Autocomplete 模块

codemirror

CodeMirror Autocomplete 模块

参考

使用 @codemirror/language 模块所提供的 API 为编辑器添加代码补全功能(包括弹出代码提示框供用户选择和自动括号补全)

代码补全

代码补全是指当用户所输入的内容可以补全时,弹出一个 tooltip 提示框,里面包含了一系列可供用户选择的补全项,用户选择其中一个插入到编辑器里

代码补全的弹出框
代码补全的弹出框

插件

使用方法 autocompletion(config?) 生成一个插件,以便添加到编辑器(在实例化编辑器视图时,添加到配置对象的属性 config.extensions 上),可以启用代码补全功能(当输入的内容与补全项相匹配时,会弹出提示框供用户选择)

其中(可选)参数 config 是一个对象(默认值是 {}),用于配置代码补全的行为,以及 completion dialog element 代码补全的弹出框/提示框的行为,具有以下属性和方法:

  • activateOnTyping(可选)属性:一个布尔值(默认值为 true),用于设置是否在用户输入内容时(存在匹配的补全项)显示补全提示框
  • activateOnCompletion(可选)属性:一个函数 fn(completion: Completion) → boolean 用于设置是否在选择某个补全项之后,又触发一次补全提示
    相当于把选中的补全项视为用户输入一样,而该补全项的内容又存在匹配的补全项,则可以再一次触发补全提示框弹出。常见于访问链式属性的场景
  • activeOnTypingDelay(可选)属性:一个数字(默认值是 100,即 100 毫秒),用于设置延迟响应用户的输入操作的时间
    该属性配合上述另一个配置属性 activateOnTyping 使用,避免高频触发补全提示框以优化性能,特别是补全来源 completion source 比较大,或者补全来源返回的结果 completion result 没有设置 validFor 属性而无法重用补全列表,每次用户输入时都要重新计算补全列表,可以适当增加 delay 延迟响应
  • selectOnOpen(可选)属性:一个布尔值(默认值为 true),用于设置是否在弹出补全提示框时选中第一项(可以直接执行 acceptCompletion command 指令以确定插入该补全项)
  • override(可选)属性:一个列表 CompletionSource[] 以覆盖默认的补全来源
    默认的补全来源是通过方法 editorState.languageDataAt(name, pos, side?)(其中参数 nameautocomplete)获取与自动补全相关的元信息,返回一个数组,其元素可能是符合 type CompletionSource 的值,或符合 interface Completion 的对象
    关于 CompletionSource 的具体介绍请查看下文的补全来源章节
  • closeOnBlur(可选)属性:一个布尔值(默认值为 true),用于设置是否在编辑器失去焦点时关闭补全提示框
  • maxRenderedOptions⁠(可选)属性:一个数值,用于设置弹出框里最多可以渲染多少个补全项
  • defaultKeymap(可选)属性:一个布尔值,用于设置是否禁用默认的按键与指令映射(应用于弹出框中)
    如果该属性设置为 false 则需要自行设置按键与(关于代码补全的)指令的映射,而且要具有较高的优先级
  • aboveCursor(可选)属性:一个布尔值,用于设置弹出框的位置是否位于光标的上方(如果光标上方有空间,弹出框默认显示在光标的下方,该属性将光标上方位置作为 fallback 当下方空间不足时,弹出框可以显示在上方)
  • tooltipClass(可选)属性:一个函数 fn(state: EditorState) → string 基于编辑器状态 state 返回一个字符串,作为 completion dialog element 代码补全的弹出框/提示框的额外 CSS class 类名
  • optionClass (可选)属性:一个函数 fn(state: EditorState) → string 基于编辑器状态 state 返回一个字符串,作为每一个 completion options 补全项的额外 CSS class 类名
  • icons(可选)属性:一个布尔值,用于设置是否在补全项的名称前面(基于它的类型 type)渲染一个图标
  • addToOptions(可选)属性:一个数组(每个元素都是一个对象),为每个补全项添加一系列额外的内容
    ts
    {
      render: fn(
      completion: Completion,
      state: EditorState,
      view: EditorView
      ) → Node | null,
      position: number
    }[]
    

    该数组的每个元素都是一个对象,描述所需添加的内容,具有两个属性:
    • 属性 render 是一个函数,会根据当前所遍历的补全项 completion 返回 DOM 节点null 表示所需添加的内容
    • 属性 position 是一个数字,表示添加额外信息的相对位置,参照补全项里的默认内容的位置:
      • 图标 icon 位置是 20
      • 内容 label 位置是 50
      • 简短的补充信息 detail 位置是 80

    每个在视口里的补全项都会依此调用该数组中每个元素(对象)的 render 方法,并根据属性 position 将额外的信息添加到补全项里
  • positionInfo⁠(可选)属性:一个函数,覆写补全项的 info 额外信息的尺寸位置(如果选中的补全项具有 info 属性,默认情况下会显示在该补全项的旁边)
    ts
    fn(
      view: EditorView,
      list: Rect,
      option: Rect,
      info: Rect,
      space: Rect
    ) → {style?: string, class?: string}
    

    该函数根据各种元素的尺寸信息(包括弹出框 list 的尺寸、选中的补全项 option 的尺寸、额外信息 info 原来的尺寸、可用于显示 tooltip 的 space 空间信息),返回一个对象,描述 info 额外信息的新尺寸和位置
  • compareCompletions(可选)属性:一个函数 fn(a: Completion, b: Completion) → number 用于对具有相同的匹配程度 match score 的两个补全项进行排序,默认在内部采用字符串的方法 localeCompare() 进行排序
    返回一个数字,表示补全项 a 是在补全项 b 之前(返回负数)、之后(返回正数)还是与之相同(返回 0
  • filterStrict(可选)属性:一个布尔值(默认值是 false),用于设置是否开启严格过滤模式
    如果设置为 true 则不允许模糊匹配的补全项,即补全项只有从开头就与用户的输入内容匹配才会展示出来
    注意

    该属性只有在(由补全来源计算而得的)补全结果属性 filter 设置为 true 才起作用,如果补全结果属性 filter 就会展示出全部的补全项

  • interactionDelay⁠(可选)属性:一个数字(默认值是 75,即 75 毫秒),用于设置在打开弹出框后有一段延迟再响应交互,以便让用户意识到要接下来的操作对象是弹出框,避免用户发出(非针对弹出框)操作错误地传递给了弹出框
  • updateSyncTime(可选)属性:一个数字(默认值是 100,即 100 毫秒),用于设置更新补全列表的间隔时间
    如果有多个补全来源,且需要异步获取数据,则在展示较快获取到的补全来源的数据之前,需要等待一段时间,以待较慢返回的数据,以便最后一并展示

另外该模块还导出了一系列辅助函数,基于当前编辑器状态 state 获取或更改的代码补全相关信息:

  • completionStatus(state) 方法:获取当前补全状态,返回的值有三种可能:
    • "active" 字符串:表示当前有可选择的补全项
    • "pending" 字符串:表示当前正在 query 查询补全来源
    • 其他情况返回 null
  • currentCompletions(state) 方法:返回一个数组,包含所有满足匹配条件的补全项
  • selectedCompletion(state) 方法:获取当前高亮选中的补全项,可能是一个满足 TypeScript interface Completion 的对象,如果没有选中的补全项则返回 null
  • selectedCompletionIndex(state) 方法:获取当前高亮选中的补全项(在补全列表里)的索引值,如果当前没有选中的补全项则返回 null
  • setSelectedCompletion(index) 方法:创建一个自定义变更效应 stateEffect,将其附加到事务 transaction 上,会将选中的补全项更改为(在补全列表里)索引值为 index 的补全项

补全项

TypeScript interface Completion 描述了一个补全项,符合该类型的对象具有以下属性和方法:

  • label 属性:一个字符串,表示该补全项的内容,它会与用户的输入内容做比较,以得到该补全项的匹配情况
  • displayLabel(可选)属性:一个字符串,它替代 label 属性在弹出框里作为该补全项的可视内容展示给用户
    当设置了该属性,需要相应地同时在 CompletionResult 里设置属性 getMatch(一个函数),以便计算该补全项与用户的输入内容的匹配情况,以高亮显示(通过下划线)配对区域
  • detail(可选)属性:一个字符串,关于该补全项的简短信息,采用不同的样式(斜体)显示在 label 后面
  • info(可选)属性:当高亮选中该补全项时展示的额外信息
    显示位置

    这些额外信息默认显示在该补全项的旁边

    但也可以在使用方法 autocompletion(config?) 生成插件(为编辑器添加代码补全功能)时,通过配置对象的属性 positionInfo⁠ 来覆写 info 的尺寸位置


    该属性值有多种类型:
    • 可以是一个字符串,表示 tooltip 所展示的内容
    • 可以是一个函数 fn(completion: Completion) 基于当前补全项 completion 返回一个符合 TypeScript type CompletionInfo 类型的值
    • 也可以是一个 Promise 表示异步获取关于该补全项的额外信息,最后 resolve 的值要符合 TypeScript type CompletionInfo 类型
    CompletionInfo

    type CompletionInfo 表示补全项的额外信息,它的完整类型描述是

    ts
    type CompletionInfo = Node | {dom: Node, destroy?: fn()} | null
    

    可以是一个 DOM 节点

    也可以是一个对象 {dom: Node, destroy⁠?: fn()} 其中属性 destroy 用于清除(添加到页面上的)相应 DOM 节点

    也可以是 null 表示不向页面添加 DOM 节点

  • apply(可选)属性:一个字符串或一个函数 fn(view: EditorView, completion: Completion, from: number, to: number) 表示在确认应用该补全项时执行的操作
    若不设置该属性,则在确认应用该补全项时,默认的行为是使用上文所述的属性 label 替换匹配的输入内容
    若设置了该属性,则在确认应用该补全项时,则执行自定义的操作:
    • 如果该属性值是一个字符串,则使用该字符串替换匹配的输入内容
    • 如果该属性值是一个函数(自由度更高),则调用该函数,一般在函数内执行替换内容的操作
      注意

      该模块专门提供了一个AnnotationType pickedCompletion,如果在函数里分发事务 fire transaction,记得要为事务添加该类型的 annotation,其值为该选中的补全项

      ts
      import { pickedCompletion } from '@codemirror/autocomplete';
      
      // 创建 annotation
      const annotationForCompletion = pickedCompletion.of(selectedCompletion);
      
      // 分发事务应用选中的补全项
      view.dispatch({
        change: {
          // 使用补全项替换匹配的输入内容
          // ...
        },
        // 设置事务的元信息 annotation
        annotation: annotationForCompletion
      })
      
  • type(可选)属性:一个字符串,表示该补全项所属的类别,系统会基于该属性为补全项的图标元素添加相应的 CSS class 类名 cm-completionIcon-{type}(其中 type 表示该属性的值)
    所以可以通过设置该属性,然后在样式表里基于相应的 CSS class 类名为该补全项设置图标
    系统为常见的的补全项类型(包括 classconstantenumfunctioninterfacekeywordmethodnamespacepropertytexttype、和 variable)设置了相应的图标
    可以为补全项设置多个类别,用空格符分隔,以便通过多个 CSS selector 选择器设置特别的图标
  • commitCharacters(可选)属性:一个数组,其元素是字符串,为该补全项设置 commit character 触发确认的字符
    这些字符的作用类似于 Enter 键,当用户高亮选中了该补全项时,如果用户继续输入该属性(一个数组)所设置的任意字符,就会触发确认插入该补全项
  • boost(可选)属性:一个数字(从 -9999 范围里的数字),用于调整该补全项的优先级
    如果两个补全项与用户输入内容的匹配程度相同时,会进一步基于该属性判断它们之间的优先级,如果该属性值为正数会将该补全项在列表里向上移动,否则会向下移动
  • section(可选)属性:一个字符串或一个对象(符合 Typescript interface CompletionSection,更推荐采用这种方式),表示该补全项所属的分组
    归属同一个 section 的所有补全项在弹出框里会被划分在一起,并在它们的上方显示一个标题,以表示分组名称
    CompletionSection

    Typescript interface CompletionSection 描述一个补全项的分组

    符合该 interface CompletionSection 的对象具有以下属性:

    • name 属性:一个字符串,表示该分组的名称
      如果没有设置后面的属性 header 就会将该属性的值显示在对应补全项分组的上面,作为它们的标题
    • header(可选)属性:一个函数 fn(section: CompletionSection) → HTMLElement 用于渲染一个 DOM 元素作为该补全项分组的标题
      由于分组标题和补全项一样,也是作为弹出框的列表项,所以需要为该 DOM 元素添加 CSS 样式 display: list-item
    • rank(可选)属性:一个数字,表示该分组在弹出框里的顺序
      在弹出框里,各补全项的分组默认按照字母顺序排列,可以通过该属性指明顺序,该值较低的分组排在上方

补全来源

TypeScript type CompletionSource 类型描述一个函数,以下称为「补全信息来源函数」,用于为编辑器提供补全信息的来源

ts
type CompletionSource = fn(context: CompletionContext) → CompletionResult |
Promise<CompletionResult | null> |
null

一般不手动编写补全信息来源函数(符合 type CompletionSource 的函数),而是使用该模块所提供现成补全信息来源函数,或一系列工厂函数 factory function 来生成它:

  • completeAnyWord 补全信息来源函数:它扫描文档中的所有单词(通过 character categorizer 进行单词的划分)作为补全来源,可以实现为用户在文档中所创建的变量名、函数名提供自动补全
  • completeFromList(list) 工厂函数:基于一个补全列表 list(它是一个数组,其元素可以是一个字符串表示补全的内容,也可以是一个符合 interface Completion 的对象),生成一个 CompletionSource 补全来源
  • ifIn(nodes, source) 工厂函数:将给定的补全来源 source(一个符合 type CompletionSource 的值)与特定的一系列节点类型 nodes(一个数组,其元素是一系列字符串表示语法树的节点类型)相关联,返回一个新的 CompletionSource 补全来源,只有光标在语法树的相应节点类型的节点时才触发该补全来源
  • ifNotIn(nodes, source) 工厂函数:与 ifIn 类似,但是触发逻辑相反,也是返回一个新的 CompletionSource 补全来源,但是当光标在给定的一系列节点类型的节点上不会触发补全来源

符合 type CompletionSource 的函数基于补全上下文 CompletionContext 同步/异步生成补全结果 CompletionResult(里面包含一系列补全项),如果没有补全项则返回 null

CompletionContext

class CompletionContext 表示补全上下文,用作补全信息来源函数(符合 type CompletionSource 的函数)的参数

通过 new CompletionContext(state, pos, explicit, view?) 进行实例化,下文称为「补全上下文实例」

各参数的具体说明如下:

  • 参数 state 是当前补全所依赖的编辑器状态
  • 参数 pos 表示补全发生的位置,即补全上下文所针对的位置
  • 参数 explicit 表示当前补全是否显式激活(执行 startCompletion 指令,弹出提示框),还是由用户输入的内容隐式激活的(与补全项匹配时,弹出提示框)
  • (可选)参数 view 表示编辑器视图,但也可能为 undefined 例如在测试环境手动创建该补全上下文时,或调用方法 CompletionResult.update 异步更新原有的补全结果时

但是该类一般不需要手动进行实例化,而是调用该模块提供的工厂函数生成补全信息来源函数(符合 type CompletionSource 的函数)时自动创建自该类的实例

补全上下文实例 completionContext 具有一些属性和方法:

  • state 属性:当前补全上下文所依赖的编辑器状态
  • pos 属性:一个数字,表示当前补全上下文所针对的位置
  • explicit 属性:一个布尔值,与实例化时参数 explicit 作用一致
  • view 属性:可能是编辑器视图,也可能是 undefined
  • tokenBefore(types) 方法:寻找在光标(即位置 this.pos)的前面且满足类型 types(一个数组,其元素是字符串,表示节点类型)的节点,返回一个对象 {from: number, to: number, text: string, type: NodeType} 表示满足条件的节点信息(包括该节点类型 type,该节点所覆盖的范围从 fromto,该节点的内容 text),如果没有找到满足条件的节点则返回 null
  • matchBefore(expr) 方法:寻找在光标(即位置 this.pos)的前面且满足正则表达式 expr 的内容,返回一个对象 {from: number, to: number, text: string} 表示满足条件的文本内容和范围,如果没有匹配的内容则返回 null
对比

以上两种方法类似,都是基于补全上下文获取满足条件的内容,但是适用场景不同:

  • tokenBefore 方法:基于语法树(节点类型)进行匹配,适用于基于语法信息的补全操作,例如对象的属性名补全
  • matchBefore 方法:基于字符串进行匹配,适用于基于内容/文本信息的补全操作,例如变量名补全
  • aborted 属性:一个布尔值,如果为 true 表示异步获取补全来源时被禁止,参考该属性的值可以避免后续执行无效操作
  • addEventListener(type, listener, options?) 方法:为 type="abort" 事件注册回调函数 listener
    (可选)参数 options 是一个对象 {onDocChange: boolean} 用于设置是否在文档变更(由于用户输入/删除内容)时触发 abort 事件(禁止继续基于当前上下文获取补全来源),默认值为 false 因为假设获取得到的补全结果设置了属性 validFor 来检测当前补全来源是否可复用,所以不必每次文档更改都触发 abort 事件
CompletionResult

TypeScript interface CompletionResult 表示补全结果,所描述的对象作为补全信息来源函数(符合 type CompletionSource 的函数)的返回值

符合该 interface CompletionResult 的对象具有以下属性和方法:

  • from 属性:当前补全所匹配的文档范围的开始位置
  • to(可选)属性:当前补全所匹配的文档范围的结束位置(默认为主选区/光标的位置)
  • options 属性:一个数组,其元素是对象(符合 TypeScript interface Completion)表示该补全结果里所提供的一系列补全项
    这些补全项可以直接展示给用户,并不需要再与输入内容作对比,由于补全系统已经自动将这些补全项与目标内容(从 fromto 的文本)进行匹配和排序
  • validFor(可选)属性:可以是一个正则表达式对象,也可以是一个判定函数 fn(text: string, from: number, to: number, state: EditorState) → boolean 用于判断在用户输入/删除内容后,原来匹配的内容(从 fromto)的范围经过 mapped 映射后所对应内容,是否依然满足该属性所设置的匹配条件,如果满足则可以复用当前补全结果,如果不满足则重新计算补全结果(获取新的补全项)
    推荐在生成补全结果(一个对象)时设置该属性,以便可以复用原有的补全项列表,避免重新查询 query 补全信息来源(重新计算补全结果),更快速地响应用户交互和优化性能
  • filter(可选)属性:一个布尔值,表示是否对补全来源进行过滤(默认会对补全项进行过滤和排序),如果该属性设置 false 则显示所有补全项(并根据这些补全项的提供顺序进行排序)
    如果由多个补全来源,则不进行过滤的补全项会置于(弹出框)顶部,
    如果该属性设置为 false,则前一个属性 validFor 就不起作用
  • getMatch(可选)属性:一个函数 fn(completion: Completion, matched⁠?: readonly number[]) → readonly number[] 用于计算给定的补全项 completion 的匹配情况
    当前一个属性 filterfalse,或补全项设置了属性 displayLabel时,需要提供该函数,以便每个补全项进行调用,计算各补全项与用户输入内容的匹配情况
    函数的第二个(可选)参数 matched 是一个数组,这是当前一个属性 filter 不为 false 时,补全系统自动计算得到的当前所遍历的补全项的匹配情况,该素组的元素是数字,每两个一组表示一个配对的范围
    函数的返回值是一个数组,其元素是数字,每两个一组表示一个配对的范围,然后该补全项会高亮显示(通过下划线)配对区域
  • update(可选)属性:一个函数 fn(current: CompletionResult, from: number, to: number, context: CompletionContext) → CompletionResult | null 用于在(用户输入/删除内容)文档更改后,同步更新当前的补全结果以便复用
    注意该函数的计算量不能大,由于它需要在文档状态更新时同步调用,否则会给用户输入延迟的卡顿感
  • map(可选)属性:一个函数 fn(current: CompletionResult, changes: ChangeDesc) → CompletionResult | null 用于更新当前的补全结果,特别是补全项中涉及位置信息
    例如补全项的属性 apply(以手动响应用户选中该补全项),其值是一个函数,如果涉及位置信息
    当查询 query 补全来源后,又分发了事务 transaction,则可以通过设置该属性 map 更新补全项的位置信息,以便复用当前补全结果
    注意对于补全结果中关于位置的属性 fromto,系统在内部会对其自动追踪并随文档同步更新,并不需要在该属性进行更新
  • commitCharacters(可选)属性:一个数组,其元素是字符串,为该补全结果里的所有补全项设置 commit character 触发确认的字符
    这些字符的作用类似于 Enter 键,当用户高亮选中了该补全结果里面的某个补全项时,如果用户继续输入该属性(一个数组)所设置的任意字符,就会触发确认插入该补全项

有多种方式为编辑器添加补全信息:

  • 可以在构建语言实例 language 时设置补全信息
    (假设语法解析器由 Lezer 构建)在使用方法 LRLanguage.define(spec) 创建语言实例对象时,通过配置对象的属性 spec.languageData 注册(与交互相关的)编程语言元信息
    属性 languageData 其值是一个对象,其中字段 autocomplete 用于设置与补全相关的元信息,该字段的值可以采用符合 TypeScript type CompletionSource 的值,也可以直接采用一系列符合 TypeScript interface Completion 的对象所构成的数组
    ts
    import {LRLanguage} from "@codemirror/language"
    
    // 使用辅助函数 completeFromList
    // 基于一个补全列表生成一个 CompletionSource 补全来源
    const exampleCompletionSource = completeFromList([
      // 数组的每个元素都符合 interface Completion 表示一个补全项
      {label: "defun", type: "keyword"},
      {label: "defvar", type: "keyword"},
      {label: "let", type: "keyword"},
      {label: "cons", type: "function"},
      {label: "car", type: "function"},
      {label: "cdr", type: "function"}
    ])
    
    // 实例化,使用经过配置的解析器 parserWithMetadata
    export const exampleLanguage = LRLanguage.define({
      parser: parserWithMetadata,
      languageData: {
        autocomplete: exampleCompletionSource
      }
    })
    
    将元信息绑定到语法树

    如果使用 class Language(它是 class LRLanguage 的父类)创建语言实例

    在使用方法 new Language(data, parser, extraExtensions⁠?, name⁠?) 进行实例化时,通过参数 data 添加补全相关的元信息时,需要使用方法 defineLanguageFacet 对元信息进行封装处理,并将该方法的返回值作为的顶级节点的 languageDataProp 节点属性(以绑定到语法树上)

    ts
    // 使用方法 defineLanguageFacet 对元信息进行处理
    const data = defineLanguageFacet({
      autocomplete: exampleCompletionSource
    });
    
    // 假设这里的 parser 采用的是 Lezer 生成的解析器
    // 需要对其进行配置,以将 data 参数(作为顶级节点的 `languageDataProp` 节点属性)绑定到语法树上
    // refer to https://github.com/codemirror/language/blob/main/src/language.ts#L185-L188
    const parserWithMetadata = parser.configure({
      // 将 data 为 top 节点设置 languageDataProp 节点属性
      props: [languageDataProp.add(type => type.isTop ? data : undefined)]
    });
    

    另外也可以对已存在的语言实例进行配置,通过该语言对象的属性 language.data(它是一个 facet 动态配置项)添加与补全相关的元信息
    ts
    // 假设以上示例中,语言实例 exampleLanguage 创建时并未添加元信息
    // 这里可以通过 exampleLanguage.data 这个 facet 为编辑器添加补全信息
    exampleLanguage.data.of({
      autocomplete: completeFromList([
        {label: "defun", type: "keyword"},
        {label: "defvar", type: "keyword"},
        {label: "let", type: "keyword"},
        {label: "cons", type: "function"},
        {label: "car", type: "function"},
        {label: "cdr", type: "function"}
      ])
    })
    
  • 可以在调用方法 autocomplete(config?) 构建插件时,通过配置对象的属性 override 覆写补全信息源,该属性值是一个数组,其元素是一系列符合 type CompletionSource 的值

指令

该模块导出了一些与代码补全相关的 command 指令:

  • startCompletion 变量:一个 Command 指令,弹出补全代码 tooltip 提示框
    一般提示框是在用户所输入的内容(与补全项有匹配时)隐式触发的,而执行该指令(而且当前上下文存在匹配的补全项时)则可以实现手动显式弹出提示框
  • closeCompletion 变量:一个 Command 指令,关闭补全代码 tooltip 弹出框
  • acceptCompletion 变量:一个 Command 指令,确认应用当前高亮选中的补全项
  • moveCompletionSelection(forward, by?) 函数:生成一个 Command 指令,执行该指令会根据参数改变当前选中的补全项
    参数 forward 是一个布尔值,表示向前面还是向后选择新的补全项
    (可选)参数 by 可以是 "option""page" 两者之一(默认值是 "option"),表示所选择的新补全项是位于前/后一个,还是位于前/后一页

另外该模块还导出了一个变量 completionKeymap 为上文列出 command 指令设置快捷键。该变量的值是一个数组(里面的每个元素都符合 TypeScript interface KeyBinding),具体映射关系如下:

  • Ctrl-Space(macOS 则是 Alt-Space)绑定 startCompletion 指令
  • Escape 绑定 closeCompletion 指令
  • ArrowDown 绑定方法 moveCompletionSelection(true) 所生成的指令
  • ArrowUp 绑定方法 moveCompletionSelection(false) 所生成的指令
  • PageDown 绑定方法 moveCompletionSelection(true, "page") 所生成的指令
  • PageDown 绑定方法 moveCompletionSelection(false, "page") 所生成的指令
  • Enter 绑定 acceptCompletion 指令

然后通过 facet keymap 将该变量所配置的快捷键添加到编辑器上


另外该模块还提供一个辅助函数 insertCompletionText(state, text, from, to) 以实现手动触发补全操作,将给定的字符串 text 替换从 fromto 范围的内容,(范围 fromto 是在主选区/光标处)但是如果当前文档有多个选区而且有相同的内容也会进行替换

该方法返回一个对象(符合 TypeScript interface TransactionSpec,以便编辑器进行分发,执行替换操作),在其中添加属性 userEvent: "Input.complete" 以标记该操作是通过补全触发的替换/插入

代码片段

代码补全除了可以插入一段静态内容,还有插入一段模板内容,其中包含一些占位符,可以让用户在后续填入动态内容

通过方法 snippet(template) 基于 template 模板字符串创建一个函数,作为某个 completion 补全项的属性 apply 的值,当用户选中并应用相应的补全项时就会执行该函数,在文档中插入代码模板片段

参数 template 是一个字符串,表示所需插入的代码片段,遵循一些模板语言规则

  • 在其中使用 ${}(或 #{})表示占位符(即用户可以填充内容的地方)
    可以在花括号里面预设默认值,例如以下是一段表示 for 循环的代码模板片段,其中 ${index} 是可以让用户填充内容的占位符并预设了 index 作为默认值
    text
    "for (let ${index} = 0; ${index} < ${end}; ${index}++) {\n\t${}\n}"
    

    可以在花括号里添加序号 ${1}(还可以带上默认值 ${1:defaultValue}),为模板中的占位符设置光标的跳转顺序(默认按 TabShift-Tab 键,光标会依序在这些占位符之间跳转)
    如果希望在模板中包含 {} 字符(而不被识别为占位符的开始或结束标记),需要在这些字符前面使用转义符,即 \{\} 表示花括号
  • 通过换行符 \n 执行换行,并在新的一行保持与起始行相同的缩进量
  • 若含有制表符 \t 则该行会增加一个缩进单位 indent unit

其返回值是一个函数,具体如下

ts
fn(
  editor: {state: EditorState, dispatch: fn(tr: Transaction)},
  completion: Completion | null,
  from: number,
  to: number
)

将返回值(一个函数)作为某个 completion 补全项的属性 apply 的值,当用户选中并应用相应的补全项时,就会执行该函数,在文档中插入代码模板片段并将其激活 activated,第一个占位符 first field 会被选中,用户可以直接输入内容,也可以按下 TabShift-Tab 键将光标/选区切换到上一个或下一个占位符,如果光标已经在最后一个占位符再按下 Tab 则光标跳出当前位置并让模板片段失活 deactivate

进一步封装

该模块还导出了另一个类似的方法 snippetCompletion(template, completion) 可以实现进一步的封装,基于的模板字符串 template 创建一个函数,作为给定的补全项 completion 的属性 apply 的值,并返回一个新的补全项

该模块导出了一些 command 指令和方法,以实现光标/选区在(编辑器当前所激活的)模板的占位符之间跳转:

  • nextSnippetField 变量:一个 Command 指令,将光标切换到下一个占位符
  • prevSnippetField 变量:一个 Command 指令,将光标切换到上一个占位符
  • clearSnippet 变量:一个 Command 指令,清除激活的模板(不是删除已插入的模板片段,只是让其无法交互)
  • hasNextSnippetField(state) 方法:检查在给定的编辑器状态 state 里是否有激活的模板且具有下一个占位符,返回一个布尔值
    一般该方法和 nextSnippetField 指令配合使用
  • hasPrevSnippetField(state) 方法:检查在给定的编辑器状态 state 里是否有激活的模板且具有上一个占位符,返回一个布尔值
    一般该方法和 prevSnippetField 指令配合使用

另外该模块还导出了一个变量 snippetKeymap,它是一个 facet 动态配置项,其输入值是一个数组(里面的每个元素都符合 TypeScript interface KeyBinding),用于为上文列出 command 指令设置快捷键,默认映射关系如下:

  • Tab 绑定 nextSnippetField 指令
  • Shift-Tab 绑定 prevSnippetField 指令
  • Escape 绑定方法 clearSnippet 指令
注意

以上快捷键只有在编辑器状态具有激活 activated 的模板时才起作用

然后在使用代码片段时(通过方法 snippet(template) 或方法 snippetCompletion(template, completion)),编辑器会自动应用该 facet 所设置的快捷键(无需再手动添加)

括号自动补全

括号自动补全是指当用户输入括号的开启字符 opening bracket 时,编辑器会在光标后面自动插入闭合字符 closing bracket,这样可以提供更好的编辑体验

配置

通过为编辑器添加相应的 language data(与交互相关的)编程语言元信息来配置括号自动补全的行为

  • 可以在构建语言实例 language 时设置括号自动补全信息
    (假设语法解析器由 Lezer 构建)在使用方法 LRLanguage.define(spec) 创建语言实例对象时,通过配置对象的属性 spec.languageData 注册(与交互相关的)编程语言元信息
    属性 languageData 其值是一个对象,其中字段 closeBrackets 用于设置与括号自动补全相关的信息,该字段的值是一个对象,要符合 TypeScript interface CloseBracketConfig
    CloseBracketConfig

    TypeScript interface CloseBracketConfig 描述括号自动补全的行为

    符合该 interface CloseBracketConfig 的对象具有以下属性:

    • brackets(可选)属性:一个数组,其元素是一系列字符串(可以是一个单字符,也可以是三元引号 '''),用于设置需要自动闭合的(括号的)开启字符 opening bracket,默认值是 ["(", "[", "{", "'", '"'] 包含 5 种字符
    • before(可选)属性:一个字符串,用于设置(括号的)开启字符在什么字符前面才触发自动补全
      不管该属性如何设置,在空格符 whitespace 前面输入(括号的)开启字符总会触发自动补全
      该属性默认值是 ")]}:;>" 即(除了在空格符前面)在这些字符任意一个前面输入(括号的)开启字符也会触发自动补全
    • stringPrefixes(可选)属性:一个数组,其元素是一系列字符串,用于设置字符串字面量的前缀有哪些(在 "' 之前的字母)
      由于在字母后面输入引号默认是不会触发补全的,只有通过该属性所设置的字母,才会作为字符串字面量标记进行特殊处理,在这些字母后面输入引号才会触发自动补全
    将元信息同步绑定到语法树

    记得要使用 defineLanguageFacet 对元信息进行封装处理,以便(作为顶级节点的 languageDataProp 节点属性)绑定到语法树上

    ts
    import {LRLanguage} from "@codemirror/language"
    
    // 配置括号自动补全的行为
    const myCloseBracketConfig = {
      // 这里只设置了 stringPrefixes 属性(其他属性采用默认值)
      // 将字符串字面量的前缀设置为 f
      stringPrefixes: ['f']
    }
    
    
    // 实例化,使用经过配置的解析器 parserWithMetadata
    export const exampleLanguage = LRLanguage.define({
      parser: parserWithMetadata,
      languageData: {
        closeBrackets: myCloseBracketConfig
      }
    })
    
    将元信息绑定到语法树

    如果使用 class Language(它是 class LRLanguage 的父类)创建语言实例

    在使用方法 new Language(data, parser, extraExtensions⁠?, name⁠?) 进行实例化时,通过参数 data 添加补全相关的元信息时,需要使用方法 defineLanguageFacet 对元信息进行封装处理,并将该方法的返回值作为的顶级节点的 languageDataProp 节点属性(以绑定到语法树上)

    ts
    // 使用方法 defineLanguageFacet 对元信息进行处理
    const data = defineLanguageFacet({
      closeBrackets: myCloseBracketConfig
    });
    
    // 假设这里的 parser 采用的是 Lezer 生成的解析器
    // 需要对其进行配置,以将 data 参数(作为顶级节点的 `languageDataProp` 节点属性)绑定到语法树上
    // refer to https://github.com/codemirror/language/blob/main/src/language.ts#L185-L188
    const parserWithMetadata = parser.configure({
      // 将 data 为 top 节点设置 languageDataProp 节点属性
      props: [languageDataProp.add(type => type.isTop ? data : undefined)]
    });
    

    另外也可以对已存在的语言实例进行配置,通过该语言对象的属性 language.data(它是一个 facet 动态配置项)添加与括号自动补全相关的元信息
    ts
    // 假设以上示例中,语言实例 exampleLanguage 创建时并未添加元信息
    // 这里可以通过 exampleLanguage.data 这个 facet 为编辑器添加补全信息
    exampleLanguage.data.of({
      closeBrackets: {
        stringPrefixes: ['f']
      }
    })
    

    通过以上方式,将括号自动补全信息与特定编程语言相关联,会将这些信息绑定到语法树上
  • @codemirror/state 模块导出的静态属性 static EditorState.languageData:该变量是一个 facet,用于设置非特定语言专属的元信息,即直接与编辑器相关联的

插件

使用方法 closeBrackets() 生成一个插件,以便添加到编辑器(在实例化编辑器视图时,添加到配置对象的属性 config.extensions 上),可以启用括号自动补全功能,即当用户输入括号的开启字符 opening bracket 时,编辑器会在光标后面自动插入对应的闭合字符 closing bracket,如果光标位于括号的闭合字符前面而用户继续输入相同的闭合字符,则光标直接跳到(文档原有的)闭合字符后面(而非又插入一个相同的闭合字符)

指令

该模块导出了一个变量 deleteBracketPair,它是一个与括号自动补全相关的 command 指令,当光标位于两个匹配的括号之间(它们之间的内容为空),执行该指令会删除这一对字符


另外该模块还导出了一个变量 closeBracketsKeymap,该变量的值是一个数组,其中包含一个元素(符合 TypeScript interface KeyBinding),为前文所述的 command 指令 deleteBracketPair 设置快捷键 Backspace

然后通过 facet keymap 将该变量所配置的快捷键添加到编辑器上


另外该模块还提供一个辅助函数 insertBracket(state, bracket) 以实现手动插入括号 bracket,而且和普通的文本插入不同,该操作会实现(closeBrackets() 所生成的)插件所提供的「智能」功能,即自动补全括号的闭合字符,如果光标位于括号的闭合字符前面而用户继续输入相同的闭合字符,则光标直接跳到(文档原有的)闭合字符后面(而非又插入一个相同的闭合字符)

该方法返回值有两种情况:

  • 如果所需插入的字符 bracket 符合括号自动补全的条件(即插入的是一个括号的开启字符,或者是括号的闭合字符而且在光标后面有一个相同的字符,则可以执行跳过操作,而非又插入一个相同的闭合字符),则返回一个事务 transaction,以便编辑器进行分发,执行相应操作)
  • 如果插入的字符不是配置中的任意一个括号的开启字符,或者传入的闭合字符时,光标后面已经有相同的字符,但是并没有相应的开启字符与之匹配(即当前位置不满足跳过已闭合括号的条件),则返回 null 以便让更外层的程序(按照普通文本插入的方式)进行处理

一般使用(方法 closeBrackets() 所创建的)插件即可处理用户输入括号的操作(提供自动补全等功能),只有需要程序性地插入括号同时又希望实现自动补全的便捷体验时,则可以使用该辅助函数生成相关的事务(不必手动编写相关逻辑)


Copyright © 2025 Ben

Theme BlogiNote

Icons from Icônes