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),用于设置是否在弹出补全提示框时选中第一项(可以直接执行acceptCompletioncommand 指令以确定插入该补全项)override(可选)属性:一个列表CompletionSource[]以覆盖默认的补全来源
默认的补全来源是通过方法editorState.languageDataAt(name, pos, side?)(其中参数name是autocomplete)获取与自动补全相关的元信息,返回一个数组,其元素可能是符合 typeCompletionSource的值,或符合 interfaceCompletion的对象
关于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
- 图标 icon 位置是
每个在视口里的补全项都会依此调用该数组中每个元素(对象)的render方法,并根据属性position将额外的信息添加到补全项里- 属性
positionInfo(可选)属性:一个函数,覆写补全项的 info 额外信息的尺寸位置(如果选中的补全项具有info属性,默认情况下会显示在该补全项的旁边)tsfn( 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 interfaceCompletion的对象,如果没有选中的补全项则返回nullselectedCompletionIndex(state)方法:获取当前高亮选中的补全项(在补全列表里)的索引值,如果当前没有选中的补全项则返回nullsetSelectedCompletion(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 typeCompletionInfo类型的值 - 也可以是一个 Promise 表示异步获取关于该补全项的额外信息,最后 resolve 的值要符合 TypeScript type
CompletionInfo类型
CompletionInfo
type
CompletionInfo表示补全项的额外信息,它的完整类型描述是tstype 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,其值为该选中的补全项tsimport { 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 类名为该补全项设置图标
系统为常见的的补全项类型(包括class、constant、enum、function、interface、keyword、method、namespace、property、text、type、和variable)设置了相应的图标
可以为补全项设置多个类别,用空格符分隔,以便通过多个 CSS selector 选择器设置特别的图标commitCharacters(可选)属性:一个数组,其元素是字符串,为该补全项设置 commit character 触发确认的字符
这些字符的作用类似于Enter键,当用户高亮选中了该补全项时,如果用户继续输入该属性(一个数组)所设置的任意字符,就会触发确认插入该补全项boost(可选)属性:一个数字(从-99到99范围里的数字),用于调整该补全项的优先级
如果两个补全项与用户输入内容的匹配程度相同时,会进一步基于该属性判断它们之间的优先级,如果该属性值为正数会将该补全项在列表里向上移动,否则会向下移动section(可选)属性:一个字符串或一个对象(符合 Typescript interfaceCompletionSection,更推荐采用这种方式),表示该补全项所属的分组
归属同一个 section 的所有补全项在弹出框里会被划分在一起,并在它们的上方显示一个标题,以表示分组名称CompletionSection
Typescript interface
CompletionSection描述一个补全项的分组符合该 interface
CompletionSection的对象具有以下属性:name属性:一个字符串,表示该分组的名称
如果没有设置后面的属性header就会将该属性的值显示在对应补全项分组的上面,作为它们的标题header(可选)属性:一个函数fn(section: CompletionSection) → HTMLElement用于渲染一个 DOM 元素作为该补全项分组的标题
由于分组标题和补全项一样,也是作为弹出框的列表项,所以需要为该 DOM 元素添加 CSS 样式display: list-itemrank(可选)属性:一个数字,表示该分组在弹出框里的顺序
在弹出框里,各补全项的分组默认按照字母顺序排列,可以通过该属性指明顺序,该值较低的分组排在上方
补全来源
TypeScript type CompletionSource 类型描述一个函数,以下称为「补全信息来源函数」,用于为编辑器提供补全信息的来源
type CompletionSource = fn(context: CompletionContext) → CompletionResult |
Promise<CompletionResult | null> |
null
一般不手动编写补全信息来源函数(符合 type CompletionSource 的函数),而是使用该模块所提供现成补全信息来源函数,或一系列工厂函数 factory function 来生成它:
completeAnyWord补全信息来源函数:它扫描文档中的所有单词(通过 character categorizer 进行单词的划分)作为补全来源,可以实现为用户在文档中所创建的变量名、函数名提供自动补全completeFromList(list)工厂函数:基于一个补全列表list(它是一个数组,其元素可以是一个字符串表示补全的内容,也可以是一个符合 interfaceCompletion的对象),生成一个CompletionSource补全来源ifIn(nodes, source)工厂函数:将给定的补全来源source(一个符合 typeCompletionSource的值)与特定的一系列节点类型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属性:可能是编辑器视图,也可能是undefinedtokenBefore(types)方法:寻找在光标(即位置this.pos)的前面且满足类型types(一个数组,其元素是字符串,表示节点类型)的节点,返回一个对象{from: number, to: number, text: string, type: NodeType}表示满足条件的节点信息(包括该节点类型type,该节点所覆盖的范围从from到to,该节点的内容text),如果没有找到满足条件的节点则返回nullmatchBefore(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 interfaceCompletion)表示该补全结果里所提供的一系列补全项
这些补全项可以直接展示给用户,并不需要再与输入内容作对比,由于补全系统已经自动将这些补全项与目标内容(从from到to的文本)进行匹配和排序validFor(可选)属性:可以是一个正则表达式对象,也可以是一个判定函数fn(text: string, from: number, to: number, state: EditorState) → boolean用于判断在用户输入/删除内容后,原来匹配的内容(从from到to)的范围经过 mapped 映射后所对应内容,是否依然满足该属性所设置的匹配条件,如果满足则可以复用当前补全结果,如果不满足则重新计算补全结果(获取新的补全项)
推荐在生成补全结果(一个对象)时设置该属性,以便可以复用原有的补全项列表,避免重新查询 query 补全信息来源(重新计算补全结果),更快速地响应用户交互和优化性能filter(可选)属性:一个布尔值,表示是否对补全来源进行过滤(默认会对补全项进行过滤和排序),如果该属性设置false则显示所有补全项(并根据这些补全项的提供顺序进行排序)
如果由多个补全来源,则不进行过滤的补全项会置于(弹出框)顶部,
如果该属性设置为false,则前一个属性validFor就不起作用getMatch(可选)属性:一个函数fn(completion: Completion, matched?: readonly number[]) → readonly number[]用于计算给定的补全项completion的匹配情况
当前一个属性filter为false,或补全项设置了属性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更新补全项的位置信息,以便复用当前补全结果
注意对于补全结果中关于位置的属性from和to,系统在内部会对其自动追踪并随文档同步更新,并不需要在该属性进行更新commitCharacters(可选)属性:一个数组,其元素是字符串,为该补全结果里的所有补全项设置 commit character 触发确认的字符
这些字符的作用类似于Enter键,当用户高亮选中了该补全结果里面的某个补全项时,如果用户继续输入该属性(一个数组)所设置的任意字符,就会触发确认插入该补全项
有多种方式为编辑器添加补全信息:
- 可以在构建语言实例 language 时设置补全信息
(假设语法解析器由 Lezer 构建)在使用方法LRLanguage.define(spec)创建语言实例对象时,通过配置对象的属性spec.languageData注册(与交互相关的)编程语言元信息
属性languageData其值是一个对象,其中字段autocomplete用于设置与补全相关的元信息,该字段的值可以采用符合 TypeScript typeCompletionSource的值,也可以直接采用一系列符合 TypeScript interfaceCompletion的对象所构成的数组tsimport {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(它是 classLRLanguage的父类)创建语言实例在使用方法
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覆写补全信息源,该属性值是一个数组,其元素是一系列符合 typeCompletionSource的值
指令
该模块导出了一些与代码补全相关的 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 替换从 from 到 to 范围的内容,(范围 from 和 to 是在主选区/光标处)但是如果当前文档有多个选区而且有相同的内容也会进行替换
该方法返回一个对象(符合 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}),为模板中的占位符设置光标的跳转顺序(默认按Tab或Shift-Tab键,光标会依序在这些占位符之间跳转)
如果希望在模板中包含{或}字符(而不被识别为占位符的开始或结束标记),需要在这些字符前面使用转义符,即\{和\}表示花括号 - 通过换行符
\n执行换行,并在新的一行保持与起始行相同的缩进量 - 若含有制表符
\t则该行会增加一个缩进单位 indent unit
其返回值是一个函数,具体如下
fn(
editor: {state: EditorState, dispatch: fn(tr: Transaction)},
completion: Completion | null,
from: number,
to: number
)
将返回值(一个函数)作为某个 completion 补全项的属性 apply 的值,当用户选中并应用相应的补全项时,就会执行该函数,在文档中插入代码模板片段并将其激活 activated,第一个占位符 first field 会被选中,用户可以直接输入内容,也可以按下 Tab 或 Shift-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 interfaceCloseBracketConfigCloseBracketConfig
TypeScript interface
CloseBracketConfig描述括号自动补全的行为符合该 interface
CloseBracketConfig的对象具有以下属性:brackets(可选)属性:一个数组,其元素是一系列字符串(可以是一个单字符,也可以是三元引号'''),用于设置需要自动闭合的(括号的)开启字符 opening bracket,默认值是["(", "[", "{", "'", '"']包含 5 种字符before(可选)属性:一个字符串,用于设置(括号的)开启字符在什么字符前面才触发自动补全
不管该属性如何设置,在空格符 whitespace 前面输入(括号的)开启字符总会触发自动补全
该属性默认值是")]}:;>"即(除了在空格符前面)在这些字符任意一个前面输入(括号的)开启字符也会触发自动补全stringPrefixes(可选)属性:一个数组,其元素是一系列字符串,用于设置字符串字面量的前缀有哪些(在"或'之前的字母)
由于在字母后面输入引号默认是不会触发补全的,只有通过该属性所设置的字母,才会作为字符串字面量标记进行特殊处理,在这些字母后面输入引号才会触发自动补全
将元信息同步绑定到语法树
记得要使用
defineLanguageFacet对元信息进行封装处理,以便(作为顶级节点的languageDataProp节点属性)绑定到语法树上tsimport {LRLanguage} from "@codemirror/language" // 配置括号自动补全的行为 const myCloseBracketConfig = { // 这里只设置了 stringPrefixes 属性(其他属性采用默认值) // 将字符串字面量的前缀设置为 f stringPrefixes: ['f'] } // 实例化,使用经过配置的解析器 parserWithMetadata export const exampleLanguage = LRLanguage.define({ parser: parserWithMetadata, languageData: { closeBrackets: myCloseBracketConfig } })将元信息绑定到语法树
如果使用 class
Language(它是 classLRLanguage的父类)创建语言实例在使用方法
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() 所创建的)插件即可处理用户输入括号的操作(提供自动补全等功能),只有需要程序性地插入括号同时又希望实现自动补全的便捷体验时,则可以使用该辅助函数生成相关的事务(不必手动编写相关逻辑)