CodeMirror Language 模块
使用 @codemirror/language 模块所提供的 API 可以构建自己的语言包,将 Lezer 解析器(或其他类型的语法解析器)整合到 codemirror 中,以提供语法高亮、自动缩进、自动补全、代码折叠等功能
(解析器采用 Lezer)构建语言包的大概步骤如下:
- 编写
.grammar文件,并将其编译为 JavaScript 模块,其中导出了语法解析器parser
可以通过parser.configure对解析器进行配置,例如添加高亮、缩进、折叠等与编程语言语法信息 - (假设解析器使用 Lezer 生成的)使用 class
LRLanguage的实例化对象将 Lezer 解析器parser进行封装,构建一个language语言实例,以便 CodeMirror 可以使用
可以在使用静态方法LRLanguage.define(spec)实例化语言实例时,通过配置对象的参数spec.languageData设置与交互相关的编程语言元信息,一般称为 language data,例如代码自动补全的参考源、行注释符的信息、括号自动闭合的信息等。也可以使用语言实例的属性language.data(它是一个 facet 动态配置项)来添加这些信息提示
如果使用
LRLanguage的父类 classLanguage来创建语言实例,则需要对这些元信息(一个对象,以键值来存储信息)进行处理,而且要同步设置为对应解析器的语法树的顶级节点的 node prop 节点属性languageDataProp的值 - 使用 class
LanguageSupport的实例化对象将language语言实例和额外插件进行封装 - 在初始化编辑器时,将 class
LanguageSupport的实例化对象(其自身就是一个 extension 插件)添加到配置对象里
语言包一般会导出一个(与所支持的编程语言)同名的函数,例如 @codemirror/lang-html 语言包导出的方法是 html,调用该函数返回一个 class LanguageSupport 的实例化对象,在初始化编辑器将其作为插件 extension 添加到配置参数里
import {EditorView, basicSetup} from "codemirror"
import {html} from "@codemirror/lang-html"
const view = new EditorView({
parent: document.body,
doc: `<!doctype html>\n<title>HTML</title>`,
extensions: [basicSetup, html()]
})
官方预制了一些常见编程语言的语言包,可以直接使用
核心
一些与配置编程语言相关信息的核心 API,它们对 Lezer 语法解析器进行封装,并添加额外的元信息
Language 类
Language 类的实例化对象用于管理语法解析器,以及与交互相关的编程语言元信息
提示
有两个子类,可以简化构建 language 对象的步骤,分别适用于特定类型的语法解释器
- 子类
LRLanguage适用于用 Lezer 解析器 - 子类
StreamLanguage适用于 CodeMirror v5 所采用的解析器
使用方法 new Language(data, parser, extraExtensions?, name?) 进行实例化,创建一个 language 语法对象
实例化中各参数的具体介绍如下:
- 第一个参数
data:一个 facet 动态配置项,包含与当前解析器所针对的编程语言的与交互相关的元信息(例如代码自动补全的参考源、行注释符的信息、括号自动闭合的信息等)
需要同时将该参数的值设置为对应解析器的语法树的顶级节点的 node prop 节点属性languageDataProp的值(由于文档可能支持多种语言,将元信息与语法树的顶级节点相绑定,就可以在用户编辑不同编程语言所对应的内容时,加载相应的元信息)与交互相关的编程语言元信息
与交互相关的编程语言元信息包括行注释符号、自动补全、缩进策略、括号匹配等
虽然这些信息在语法解析器(第二个参数
parser)里都已经包含,但是语法解析器是用于处理编辑器里(已存在)的文本内容(以提供高亮、代码折叠等功能),并不直接与编辑交互相关所以要单独提供与编辑交互相关的信息,以便实现类似 toggle 切换注释等交互功能
用对象来表示这些元信息,可以具有以下字段
commentTokens属性:与注释相关,可以继续细分为block块级注释(包含字段open和close分别表示块级注释的起始符号和结束符号),以及line行注释autocomplete属性:与自动补全相关closeBrackets属性:与括号匹配相关indentOnInput属性:与自动缩进相关
这些属性的值具体应该如何设置,可以参考官方预制的一系列语言包
对比
在 CodeMirror 提供了一些属性和方法,用于配置和读取 language data(与交互相关的)编程语言元信息:
- @codemirror/language 模块导出的方法
defineLanguageFacet(baseData):基于传入的baseData元信息对象,生成一个 facet,用于在初始化 classLanguage类的时侯,为编辑器添加(交互相关的)编程语言元信息
该方法的返回值除了是一个 facet 还符合 lezer node prop 格式,这些元信息是与特定编程语言相关的,所以也要将该函数的返回值用作语法树的顶级节点的节点属性languageDataProp的值(如果使用 classLRLanguage创建语言实例,则不需要手动将元信息添加语法树的顶级节点上,因为该类实例化时在内部已经完成了相关操作) - 语言实例 language 具有属性
data它是一个 facet,用于设置/追加 language data 元信息
该方式添加的元信息不需要再手动添加到语法树的顶级节点上,系统会自动处理 - @codemirror/state 模块导出的静态属性 static
EditorState.languageData:该变量是一个 facet,用于设置(交互相关的)非特定语言专属的元信息,适用于整个编辑器的元信息
其实该 facet 在内部整合了所有与 language 相关的元信息,包括在实例化 classLanguage类时所配置的针对特定语言的元信息,以及直接通过该 facet 所配置的不针对特定语言的元信息 - @codemirror/state 模块的
EditorState编辑器状态实例化对象的方法languageDataAt(name, pos, side?)用于获取在给定位置pos与语言相关的元信息,这些信息与字段name相关(例如commentTokens表示与注释相关)
通过该方法可以获取通过EditorState.languageData和defineLanguageFacet(baseData)所设置的元信息
元信息是用对象来表示的,传递给方法defineLanguageFacet(baseData)进行处理,返回一个 facet 就可以作为data参数的值(记得还需要同时配置解析器 parser,将该参数值作为语法树的顶级节点的languageDataProp节点属性的值)jsimport{ Language, defineLanguageFacet, languageDataProp } from '@codemirror/language' // 使用方法 defineLanguageFacet 对元信息进行处理 const data = defineLanguageFacet({ commentTokens: { line: "//", block: { open: "/*", close: "*/" } }, }); // 假设这里的 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)] }); // 实例化,使用经过配置的解析器 parserWithMetadata new Language(data, parserWithMetadata);说明
参数
data所设置的值需要和解析器顶级节点的节点属性languageDataProp的值一致,由于 language 对象方法findRegions会基于两者来判断/寻找编辑器的文档各部分分别采用什么语言进行解析(文档内容可能是存在嵌套语言的情况,例如 HTML 文档里嵌套了 JavaScript 和 CSS 内容) - 第二个参数
parser:一个语法解析器实例对象(要符合 Lezer 的抽象类 abstract classParser) - 第三个(可选)参数
extraExtensions:一个数组,其值需要符合 typeExtension,用于设置需要同步加载的插件,默认值是空数组[] - 第四个(可选)参数
name:一个字符串,用于设置该编程语言的名称,默认值是""空字符串
Language 类的实例化得到 language 对象,以下称作「语言实例」
language 语言实例对象包含一些属性和方法:
extension属性:实现该语言的一系列相关插件
该属性值符合 typeExtension类型插件的构成
根据源码该属性包含一系列插件,主要由 3 部分构成:
languagefacet 生成的插件,该 facet 的值就是当前Language类的实例
可以通过该 facet 获取当前编辑器所激活的语言EditorState.languageDatafacet 生成的插件,该 facet 包含当前语言(包括内嵌语言 sub language)相关的元信息- 实例化 language 时,通过参数
extraExtensions所设置的需要同步加载的插件
parser属性:该语言所采用的语法解析器(要符合 Lezer 的抽象类 abstract classParser)data属性:该语言相关的元信息(一个 facet,就是实例化 language 时,通过第一个参数data所设置的元信息)
也可以通过该 facet 添加额外的元信息,例如添加代码补全的相关信息
通过该方式添加的元信息不需要再手动添加到语法树的顶级节点上,系统会自动处理name属性:一个字符串,表示该语言的名称isActiveAt(state, pos, side?)方法:查询当前语言是否在给定文档位置pos(以字符偏移量表示)激活,返回一个布尔值
参数state是编辑器文档状态对象
如果该位置正好是两种不同的语言内容的分界线,(可选)参数side的值可以是-1、0或1这三个值之一,用于表示查询位置是偏向左侧还是右侧,默认值为-1表示以左侧的内容所采用的语言为准findRegions(state)方法:获取在给定文档状态state中,使用该语言进行解析的文档范围,返回一个数组{from: number, to: number}[]表示范围嵌套语言
如果文档里含有嵌套语言,如果这些它们是包含在当前语言里,那么返回的范围数组也会囊括该嵌套语言的内容
allowsNesting属性:一个布尔值,表示当前语言(其解析的内容)是否支持嵌套语言 nested language,默认值为true
另外该模块还导出一个变量 sublanguageProp,它是一个 node prop 节点属性,用于将嵌套语言的元信息绑定到语法树的顶级节点上,其值是一个数组 Sublanguage[](即其中的元素要符合 interface Sublanguage 类型)
Sublanguage
TypeScript interface Sublanguage 具有以下属性:
type(可选)属性:可以是字符串"replace"或"extend"之一,表示与嵌套语言相关的元信息提供方式,究竟采用替换 replace 的方式还是扩展 extend(具有更高的优先级)的方式test(node, state)方法:测试给定的节点node(符合 LezerSyntaxNode类型)是否采用嵌套语言进行解析。第二个参数state是编辑器文档状态。该方法返回一个布尔值facet属性:将与嵌套语言相关的元信息对象传递给方法defineLanguageFacet(baseData)进行处理,返回一个 facet 就可以作为该属性的值
将嵌套语言的元信息绑定到语法树的顶级节点上的流程,具体参考上文设置节点属性 languageDataProp 的示例
注意
注意 sublanguage 并不是针对 nest parsing 的场景,即用不同语言的语法解析器对嵌套内容进行解析
由于采用嵌套语法 nested syntax 来表示嵌套内容时,会有自己的 top node 顶级节点的,对应使用不同的 parser,所以不需要使用 sublanguage
LRLanguage 子类
LRLanguage 类是 class Language 的子类(相当于对父类进一步封装,让创建语言实例的流程更简单)
由它创建的 language 语言实例对象中,所采用的解析器要符合 Lezer 解析器 LRParser 类型
使用该类的静态方法 LRLanguage.define(spec) 进行实例化,基于配置对象 spec,创建一个 language 语法对象
实例化配置对象 spec 具有的属性
name(可选)属性:一个字符串,表示该语言的名称parser属性:一个语法解析器实例对象(由 Lezer 生成的解析器LRParser)languageData属性:与语言相关的元信息对象提示
这里不需要手动将语言相关元信息对应解析器的语法树的顶级节点上,由于该类实例化时在内部已经完成了相关操作
LRLanguage 类的实例化得到 lrLanguage 对象,以下称作「Lezer 语言实例」
lrLanguage 语言实例对象包含一个方法 configure(options, name?) 可以基于 options 配置对象(要符合 TypeScript interface ParserConfig 类型)对当前 lrLanguage 所包含的 parser 解析器进行重配置,或者将该语言重命名为 name,得到一个新的 lrLanguage 语言实例对象
StreamLanguage 子类
StreamLanguage 类是 class Language 的子类
由它创建的 language 语言实例对象是用于兼容 CodeMirror v5 版本的解析器(基于正则表达式,采用流式逐行解析,容易出现状态丢失且可维护性较差,并不推荐使用),具体参考 @codemirror/legacy-modes 模块
语法解析
该模块还导出一些辅助函数,以进行语法解析相关操作
syntaxTree(state)方法:获取给定的编辑器状态对象state所对应的解析语法树,返回一个对象,它符合 classTree类型(该语法树可能只是对部分文档进行解析)ensureSyntaxTree(state, upto, timeout?)方法:和上一个方法类似,获取给定的编辑器状态对象state所对应的解析语法树,返回一个对象,它符合 classTree类型
不同在于该方法会让解析的文档内容至少达到upto位置(以字符偏移量表示),执行解析操作的最长时间限制在timeout(默认值是50)以内syntaxTreeAvailable(state, upto?)方法:查询当前语法树是否覆盖到给定文档状态state的指定位置upto(默认值是state.doc.length即整个文档的范围),返回一个布尔值
如果还没有解析到给定位置upto,那么后台运行的解析器可能会继续运行,但是不能保证最后会解析到目标位置,因为当解析器运行超过特定时间,或给定位置超出视口 viewport 时,解析器就会停止运行forceParsing(view, upto?, timeout?)方法:强制执行解析操作,让语法树覆盖到给定位置upto(默认值是view.viewport.to即编辑器视图的结尾),并更新编辑器状态以保证包含最新的语法树
执行解析操作的最长时间限制在timeout(默认值是100)以内
最后返回一个布尔值,表示在规定时间解析生成的语法树是否覆盖到给定位置uptosyntaxParserRunning(view)方法:检查解析器当前是否在运行,返回一个布尔值
如果要自定义解析器,可能需要使用 class ParseContext,该类将编辑器内容作为上下文提供给解析器
LanguageSupport 类
LanguageSupport 类的实例化对象对 language 语言对象和所依赖的插件进行封装
提示
语言包一般会导出一个(与所支持的编程语言)同名的函数,调用该函数返回的值就是一个 class LanguageSupport 的实例化对象
例如 @codemirror/lang-html 语言包导出的方法是 html
使用方法 new LanguageSupport(language, support?) 进行实例化,根据一个 supportLanguage 对象,它将给定的 language 语法对象,和一系列(为了支持该语言实现相关功能的依赖性)插件 support 打包起来
提示
如果所关联的语言对象支持嵌套语言,那么该参数应该包含嵌套语言所依赖的相关插件
LanguageSupport 类的实例化得到 supportLanguage 对象,以下称作「语言支持性封装实例」
supportLanguage 语言支持性封装实例对象包含一些属性:
extension属性:一个插件(符合 typeExtension类型),包括 language 语法对象,以及一系列所依赖的支持性插件提示
该类的实例化对象自身就可以作为插件(与该属性值相同)
所以上述语言包一般会导出一个(与所支持的编程语言)同名的函数,调用该函数返回的值就是一个 class
LanguageSupport的实例化对象,然后可以将该对象作为插件,在初始化编辑器将其添加到配置参数的里jsimport {EditorView, basicSetup} from "codemirror" // 官方为 HTML 提供了语言包 @codemirror/lang-html // 该语言包导出了一个(和该编程语言名称相同的)函数 html import {html} from "@codemirror/lang-html" const view = new EditorView({ parent: document.body, // 调用该函数 html() 生成一个插件,将该语言及其相关依赖插件添加到编辑器上 extensions: [basicSetup, html()] })language属性:语言实例对象support属性:(为了支持该语言实现相关功能的)依赖性插件,即实例化LanguageSupport时所传递的参数support的值提示
如果所关联的语言对象支持嵌套语言,那么该参数应该包含嵌套语言所依赖的相关插件
LanguageDescription 类
自动按需加载语言包是代码编辑器的常见需求,即编辑器根据用户所选择打开的源代码文件,(基于其元信息,例如拓展名)动态异步加载对应的语言包。LanguageDescription 类就是用于存储这些与编程语言相关的元信息,提供统一的接口以便开发者可以管理多种语言包
使用该类的静态方法 LanguageDescription.of(spec) 进行实例化,基于配置对象 spec ,为一种编程语言创建一个 languageDescription 语言描述性对象
实例化配置对象 spec 具有的属性:
name属性:字符串,用于设置编程语言的名称alias属性:一个数组,其元素是一个个字符串,用于设置编程语言的别名extensions属性:一个数组,其元素是一个个字符串,用于设置(使用该编程语言编写的)源文件的拓展名(即文件的后缀名)filename(可选)属性:一个正则表达式对象,用于设置该编程语言所编写的源文件名称所需匹配的模式load(可选)方法:一个函数fn() → Promise<LanguageSupport>用于异步加载对应的LanguageSupport语言支持性封装实例support(可选)属性:一个LanguageSupport语言支持性封装实例
注意
在初始化 LanguageDescription 时,配置对象中可选属性 load 和 support 都是用于加载对应的 LanguageSupport 语言支持性封装实例,需要设置其中之一
当语言支持性封装实例还没有加载,则设置方法 load 来异步加载;当语言支持性封装实例已经加载(存于内存里),则设置属性 support 直接引用
LanguageDescription 类的实例化得到 languageDescription 对象,以下称作「语言描述性实例」
languageDescription 语言描述性实例对象包含一些属性:
name属性:编程语言的名称alias属性:一个数组,其元素是一个个字符串,表示编程语言的别名extensions属性:一个数组,其元素是一个个字符串,表示(使用该编程语言编写的)源文件的拓展名(即文件的后缀名)filename属性:有两种类型,可能是一个正则表达式对象,表示(使用该编程语言所编写的)源文件名称所需匹配的模式;也可能是undefined,表示(使用该编程语言所编写的)源文件名称没有特定的约束support属性:有两种类型,可能是一个LanguageSupport语言支持性封装实例对象;也可能是undefinedload()方法:一个异步函数Promise<LanguageSupport>用于动态加载相应的LanguageSupport语言支持性封装实例对象
提示
官方对其维护的 CodeMirror v6 和 v5 版本的语言的相关元信息进行整理在 @codemirror/language-data 模块里
该模块导出一个变量 languages,它是一个数组,每个元素都是一个 LanguageDescription 实例,表示一种编程语言,具体包含哪些编程语言可查看源码
可以直接使用该模块或对其进行扩展,以符合项目的具体需求
该类还提供两个静态方法,以便基于源文件的元信息,匹配加载合适的语言包:
- static
LanguageDescription.matchFilename(descs, filename)静态方法:根据给定的filename源文件名称(字符串)对数组descs里的元素(一个个语言描述性实例)进行查询,返回第一个匹配的语言描述性实例,如果没有找到合适的就返回null
查询时首先基于语言描述性实例的文件名称的正则表达式模式进行匹配,然后再基于扩展名(即文件的后缀)进行匹配 - static
LanguageDescription.matchLanguageName(descs, name, fuzzy?)静态方法:根据给定的字符串name对数组descs里的元素(一个个语言描述性实例)进行查询,返回第一个编程语言的名称(或别名)与参数name相匹配的语言描述性实例,如果没有找到合适的就返回null
参数name是大小写敏感的;(可选)参数fuzzy是一个布尔值(默认为true),表示是否启用模糊匹配,如果没有完全匹配时,会进一步采用部分匹配的情况,即编程语言的名称(或别名)出现在字符串name里面,如果编程语言的名称少于 3 个字符,则匹配时前后都需要是非单词型字符(例如空格)才算成功
DocInput 类
DocInput 类用于将 CodeMirror 文档内容(class Text 类的实例)转换为 Lezer 所支持的格式 TypeScript interface Input(所以该类的实例化对象也符合 interface Input)
使用方法 new DocInput(doc) 进行实例化,参数 doc 是一个 Text 类的实例,表示 CodeMirror 文档内容
高亮
一些与配置语法高亮相关信息的 API
HighlightStyle 类将 CSS 样式与 Lezer 的 tag 标签(一种表示高亮信息的 marker 标记)相关联,为代码文本设置语法高亮
提示
HighlightStyle 类实现 implements 一种类型(在 @lezer/highlight 模块所定义的)TypeScript interface Highlighter,用于将标签 tag 映射到相应的 CSS 样式
使用该类的静态方法 HighlightStyle.define(specs, options?) 进行实例化,创建一个 highlighter 高亮器
实例化中各参数的具体介绍如下:
- 第一个参数
specs:一个数组,其元素要符合 TypeScript interfaceTagStyle,以对象的形式表示高亮标签 tag 与 CSS 类(或样式)之间的关联TagStyle
TypeScript interface
TagStyle用于为一个或多个高亮标签 tag 分配样式,具有以下属性:tag属性:可以是一个Tag高亮标签实例,或一个数组(其元素是一系列Tag高亮标签实例)class(可选)属性:一个字符串,表示一个 CSS class 类名,以便通过外部的样式表(基于该 CSS 类名)间接来设置高亮样式- 一个任意名称的属性(键名是字符串),属性值是一个对象,需要符合 style-mod 库的规则,以采用声明式(键值对的形式)来生成 CSS 样式,直接设置高亮
- 第二个(可选)参数
options是一个对象,具有一些可选属性,用于配置高亮器scope(可选)属性:可以是一个Language语言实例对象,或是一个NodeType节点类型(在 Lezer 语法树里)
高亮器默认应用到整个文档的内容,如果希望约束其适用场景,可以为只针对特定语言,或指针对特定节点(及其内容,即后代节点),可以设置该属性all(可选)属性:用于为所有文档内容设置一个样式
可以是一个字符串;也可以是一个对象,符合 TypeScript typeStyleSpec(该类型约束和 style-mod 模块的 classStyleModule配置对象所遵循的约束 TypeScript typeStyle一样)以键值对的形式表示 CSS 样式规则themeType(可选)属性:可以是"dark"或"light"两者之一,表示该高亮器仅在编辑器的主题为浅色模式或黑暗模式才激活。默认是在任何编辑器主题下都激活
HighlightStyle 类的实例化得到 highlightStyle 对象,以下称作「高亮器」
highlightStyle 高亮器包含一些属性和方法:
module属性:可能是一个 classStyleModule类(来自 style-mod 库)的实例化对象;也可能是null
该属性记录了当前 highlighter 为文档应用的高亮样式,一般适用于高亮器在编辑器以外的使用场景,例如通过方法highlightTree在服务端手动拼接高亮代码文本specs属性:一个数组,其元素是一系列对象,符合 TypeScript interfaceTagStyle,表示该高亮器是实现的高亮标签 tag 与 CSS 类(或样式)的关联
预设高亮器
该模块提供了一个预设的高亮器 defaultHighlightStyle,它将一些常见的高亮标签与 CSS 样式关联,适用于浅色主题,具体映射关系可以查看源码
使用方法 syntaxHighlighting(highlighter, options?) 将给定的高亮器 highlighter 封装为一个插件,以便添加到编辑器(在实例化编辑器视图时,添加到配置对象的属性 config.extensions 上),可选参数 options 是一个对象,具有属性 fallback 是一个布尔值,表示该高亮器是否作为备选/回退方案,只当没有其他高亮器在编辑器上注册时,它才起作用
使用方法 highlightingFor(state, tags, scope?) 基于当前编辑器状态 state 所激活的高亮器,获取给定的 tags 一系列高亮标签(一个数组,其元素是一系列 Tag 高亮标签实例)所对应的高亮样式,返回一个字符串(表示 CSS 类名)或 null(如果这些高亮标签没有设置对应的高亮样式)。可选参数 scope 用于设置查询的作用域(其值是一个 Lezer 的 NodeType 节点类型,以约束当前激活的高亮器要仅作用于该节点类型)
该模块还导出一个方法 bidiIsolates(options?) 用于生成一个插件,将其添加编辑器(在实例化编辑器视图时,添加到配置对象的属性 config.extensions 上),确保包含双向文本(且已经通过节点属性 isolate 进行标记)的节点,采用与周围文本分离的方式进行渲染,可选参数 options(默认值是 {})具有可选属性 alwaysIsolate 是一个布尔值,以控制是否在任何情况都添加隔离元素(默认采用优化方案,即只有在编辑器文本方向不是统一从左到右,或者虽然是统一从左到右但在包含从右到左字符的行上,才会添加隔离元素)
折叠
一些与代码折叠相关的 API,以便临时隐藏代码片段
配置折叠信息
foldService变量:一个 facet 动态配置项,用于注册代码折叠服务
其输入值是一个函数fn(state: EditorState, lineStart: number, lineEnd: number)基于传入的行范围lineStart和lineEnd,计算出可折叠范围
如果可以找到就返回一个对象{from: number, to: number}表示可折叠范围(from是该行的开始位置,采用文档里的字符偏移量表示,to表示可折叠范围的结束位置,可能超出该行的末尾lineEnd);如果基于该行没有找到可折叠范围,则返回null
通过该方式可以不依赖语法树,为编辑器添加代码折叠信息,例如在 @codemirror/lang-markdown 模块里将标题设置为可折叠tsconst headerIndent = foldService.of((state, start, end) => { for (let node: SyntaxNode | null = syntaxTree(state).resolveInner(end, -1); node; node = node.parent) { if (node.from < start) break let heading = node.type.prop(headingProp) if (heading == null) continue let upto = findSectionEnd(node, heading) if (upto > end) return {from: end, to: upto} } return null })foldNodeProp变量:它是一个 node prop 节点属性,用于为语法树上特定的节点类型设置可折叠信息
通过该方式添加的折叠信息是依赖语法树的,要基于节点类型进行配置,大概流程如下:- 通过调用 node prop 的方法
foldNodeProp.add(match: Object<T> | fn(type: NodeType) → T | undefined)为特定的节点类型设置折叠信息
传入的参数可以是一个对象,以键值对的方式为不同的节点类型设置折叠信息,键名是节点类型的名称;也可以传入一个函数fn(type: NodeType)动态为不同节点类型type设置折叠信息
该节点属性的值(如果是采用对象的方式,则是键名对应的值;如果是采用函数的形式,则是该函数的返回值)是一个函数fn(node: SyntaxNode, state: EditorState) → {from: number, to: number} | null称为 fold function 折叠区域计算函数,因为它是基于当前语法树上的节点node和编辑器状态state,返回一个对象,表示该节点对应的可折叠范围(以文档里的字符偏移量来表示){from: number, to: number},如果该节点不能折叠则返回nullfoldInside 辅助函数
@codemirror/language 模块提供了一个内置的折叠区域计算函数
foldInside,针对常见的可折叠场景,即节点的两端都是分别含有一个分界符的,折叠范围是分界符里面的内容如果通过
foldNodeProp为特定节点类型设置可折叠信息时,该节点的两端都是分别含有一个分界符的,就可以直接适用foldInside作为折叠函数- 调用该方法会返回一个函数(符合 TypeScript type
NodePropSource类型),然后将该返回值传递给NodeSet.extend方法或LRParser.configure方法将这些信息添加到解析器上(也可以在初始化解析器时,作为其配置对象的属性ParserConfig.props的值)
例如在 @codemirror/lang-javascript 模块里将一系列节点设置为可折叠jsexport const javascriptLanguage = LRLanguage.define({ // ... parser: parser.configure({ props: [ // ... foldNodeProp.add({ // 为一系列节点类型设置折叠信息 // 由于这些节点的两端都是分别含有一个分界符的(常见的可折叠场景) // 所以折叠区域计算函数采用内置辅助函数 foldInside "Block ClassBody SwitchBody EnumBody ObjectExpression ArrayExpression ObjectType": foldInside, // 对于注释块节点,它的折叠方式比较特别 // 需要保留前后 2 个字符,即折叠后的形式为 /*...*/ // 所以这里手动计算折叠区域 BlockComment(tree) { return {from: tree.from + 2, to: tree.to - 2} } }) // ... ] }) // ... });- 通过调用 node prop 的方法
对比
foldService 和 foldNodeProp 都可以为编辑器配置折叠信息,但适用场景是不同的
foldService不依赖语法树,可以更自由地为编辑器设置折叠信息foldNodeProp依赖于语法树,基于节点类型设置折叠信息
如果要判断文档里的某一行是否可折叠,可以使用方法 foldable(state, lineStart, lineEnd) 基于传入的行范围 lineStart 和 lineEnd 查找可折叠范围,如果找到就返回一个对象 {from: number, to: number} 以表示折叠范围;如果不可折叠则返回 null。该方法会首先查询通过 foldService 配置的折叠信息,如果没有相关信息则继续查询通过 foldNodeProp 配置的折叠信息
执行折叠操作
该模块提供了一系列 command 指令,用于以编程式的方式来折叠(或展开)代码文本
foldCode变量:一个Command指令,折叠选中的行(如果选中行是可折叠的)unfoldCode变量:一个Command指令,展开选中行(如果选中的行是处于折叠状态时)toggleFold变量:一个Command指令,切换光标所在的行的折叠状态foldAll变量:一个Command指令,折叠所有在(语法树)顶级的可折叠区域注意
但是对于较大的文档(为了节省性能)可能并没有解析完全,所以使用该命令时,可能并不能保证所有顶级可折叠区域都实现了折叠
unfoldAll变量:一个Command指令,展开所有折叠
该模块还导出了一个变量 foldKeymap 为上文列出 command 指令设置快捷键。该变量的值是一个数组(里面的每个元素都符合 TypeScript interface KeyBinding),具体映射关系如下:
Ctrl-Shift-[(macOS 则是Cmd-Alt-[)绑定foldCode指令Ctrl-Shift-](macOS 则是Cmd-Alt-])绑定unfoldCode指令Ctrl-Alt-[绑定foldAll指令Ctrl-Alt-]绑定unfoldAll指令
然后通过 facet keymap 将该变量所配置的快捷键添加到编辑器上
该模块还提供了一些更底层的 API 以控制折叠状态
foldedRanges(state)方法:获取给定文档状态state里面的所有折叠范围,返回一个DecorationSet类的实例(包含一系列 Replacing decoration 替换型装饰器,表示一个个折叠范围)foldState变量:一个 state field 自定义的编辑器状态字段,其值是一个DecorationSet类的实例(包含一系列 Replacing decoration 替换型装饰器,表示一个个折叠范围),用于将折叠范围保存在编辑器状态里
可以将其传递给编辑器状态对象的方法editorState.toJSON将编辑器状态和折叠信息序列化为 JSON 对象保存起来,然后通过静态方法EditorState.fromJSON将其反序列化还原编辑器器状态及其折叠信息foldEffect变量:一个StateEffectType类的实例化对象,通过调用其方法foldEffect.of({from: number, to: number})创建一个相应类型的 stateEffect 对象实例,将该自定义变更效应 stateEffect 添加到 transaction 事务中,以触发折叠给定的范围unfoldEffect变量:一个StateEffectType类的实例化对象,通过调用其方法unfoldEffect.of({from: number, to: number})创建一个相应类型的 stateEffect 对象实例,将该自定义变更效应 stateEffect 添加到 transaction 事务中,以触发展开给定的范围
提示
一般通过 foldCode 或 unfoldCode command 指令或 fold gutter 侧边栏触发代码的折叠与展开,仅在特殊的场景才需要使用这些 StateEffectType
配置折叠样式
通过方法 codeFolding(config?) 配置折叠后的占位符样式,该方法返回一个插件,以便添加到编辑器(在实例化编辑器视图时,添加到配置对象的属性 config.extensions 上)
该方法的(可选)参数 config 是一个配置对象,具有一些方法和属性:
placeholderDOM(可选)属性:一个函数fn(view: EditorView, onclick: fn(event: Event), prepared: any) → HTMLElement用于创建一个占位符元素HTMLElement,显示在文档里发生了代码折叠的位置
其中onclick为该元素设置点击事件的响应函数,以展开该折叠范围
如果配置对象设置了preparePlaceholder属性(一个函数),则这里的第三个参数prepared就会获取该属性对应函数的返回值,否则该参数的值就是nullplaceholderText(可选)属性:一个字符串,用于设置占位符的内容,默认值为...
该字符串会带有 CSS class 类名cm-foldPlaceholder
说明
当 placeholderDOM 属性未设置,就会基于 placeholderText 属性所设置的字符串来创建占位符元素
preparePlaceholder(可选)属性:一个函数fn(state: EditorState, range: {from: number, to: number}) → any基于给定的折叠范围range进行一些预处理,返回一个值对该折叠进行描述(例如折叠的字符串数量)
属性所设置的函数的返回值会传递给配置对象的另一个属性placeholderDOM用于创建占位符元素
配置侧边栏
通过方法 foldGutter(config?) 配置编辑器的侧边栏,该方法返回一个插件,以便添加到编辑器(在实例化编辑器视图时,添加到配置对象的属性 config.extensions 上),然后在相应的可折叠行的前方就会显示一个指示器 indicator(例如三角形图标)以指示其折叠状态,而且该指示器是可点击的,以触发折叠/展开代码
该方法(可选)参数 config 是一个配置对象,具有一些方法和属性:
markerDOM(可选)属性:一个函数fn(open: boolean) → HTMLElement用于创建一个折叠状态指示器HTMLElement,显示在侧边栏表示对应行的代码折叠状态openText(可选)属性:一个字符串,默认值是"⌄",用于表示对应行处于未折叠(可折叠)状态closedText(可选)属性:一个字符串,默认值是"›",用于表示对应行处于已折叠状态
说明
当 markerDOM 属性未设置,就会采用 openText 和 closedText 属性分别创建在未折叠(可折叠)和已折叠状态下的指示器
domEventHandlers(可选)属性:一个对象,以键值对的形式为该侧边栏设置 DOM 事件监听器,键名是 DOM 事件名称,对应的值是事件回调函数
其中事件处理函数的形式是fn(view: EditorView, line: BlockInfo, event: Event) → boolean
它接收三个参数:- 第一个参数
view是编辑器视图 - 第二个参数
line是一个 classBlockInfo类的实例,表示触发该事件所对应的行信息 - 第三个参数
event是事件对象
提示
该方法在内部已经为
click事件预设了一个处理函数,以实现(当用户点击指示器时)切换对应行的折叠状态不过也可以自定义一个
click事件的处理函数以覆盖内置的处理函数
事件处理函数最后返回一个布尔值,如果为true表示该事件已经处理完成,则针对该事件所设置的(优先级较低的)回调函数就不会执行;如果为false则还会继续执行针对同类型事件的其他回调函数- 第一个参数
foldingChanged(可选)属性:一个函数fn(update: ViewUpdate) → boolean该函数返回一个布尔值,以控制当视图更新时是否要重新渲染侧边栏的折叠指示器
缩进
一些与代码缩进相关的 API
配置缩进信息
indentService变量:一个 facet 动态配置项,用于注册代码缩进服务
其输入值是一个函数fn(context: IndentContext, pos: number) → number | null | undefined基于缩进上下文context(一个 classIndentContext实例对象)计算给定位置pos所在的行的缩进量/缩进深度,返回值有多种类型分别对应不同的缩进情况:- 如果该函数返回一个数字,表示该行的缩进所占据的列数 column number
- 如果返回
null表示无法确定该行的缩进,应该继承上一行的缩进 - 如果返回
undefined表示继续执行下一个代码缩进服务(或通过indentNodeProp基于语法树)计算该行的缩进量
IndentContext
class
IndentContext管理/提供编辑器计算缩进所需的上下文通过方法
new IndentContext(sate, options?)进行实例化,以基于state编辑器文档状态获取缩进上下文,在后文将该类的实例化对象称作「缩进上下文」实例化时(可选)参数
options是一个对象(默认值是{}),用于配置计算缩进的相关规则,具有如下属性:overrideIndentation(可选)属性:一个函数fn(pos: number) → number(传递给计算缩进的辅助函数)用于覆盖行缩进的计算方式,特别是在实现区域缩进(例如通过方法indentRange)时很有用
由于在区域缩进的场景中,后续行的缩进值一般是参考之前的行,但有时候也可能需要进行重新缩进(与执行区域缩进之前的原始状态相比)
如果配置了此属性,则区域缩进时每处理到一行都会先调用该函数,如果返回-1表示该行保存原来缩进量;如果返回其他数值,则更新其缩进simulateBreak(可选)属性:一个数值(表示文档中的位置,以字符偏移量计算),将其视作换行的位置
在实现newlineAndIndentcommand 指令(一般与Enter键相绑定)的内部逻辑时会配置该属性simulateDoubleBreak(可选)属性:一个布尔值,如果同时设置了上一个属性simulateBreak,将给定的位置视作执行了两次换行
提示
该类一般不会通过手动进行实例化,而是在配置 facet
indentService时,它的实例化对象作为(facet 的输入值,一个函数)参数提供给开发者直接使用缩进上下文 indentContent 对象提供一系列的属性和 helper function 获取针对特定 pos 位置的缩进信息:
unit属性:一个数值,表示缩进单位,即每次缩进对应占据的列数 column numberstate属性:该缩进上下所依赖的文档状态lineAt(pos, bias?)方法:获取给定位置pos所在行的信息,返回值是一个对象{text: string, from: number}其中属性text表示该行的文本内容,from表示该行开头位置(以字符偏移量计算)
如果该位置pos正好是发生换行,则基于参数bias(可以是数值-1或1两者之一,默认值是1)来决定所要解析的是前面还是后面的行
该方法会将模拟换行simulateBreak也纳入考虑textAfterPos(pos, bias?)方法:获取给定位置pos所在行(在该位置)之后的内容,如果该行(在该位置)之后内容很多,则只返回最多 100 个字符。第二个(可选)参数bias其作用和前一个方法一样column(pos, bias?)方法:获取给定(在文档中)位置pos所对应的列数 column numbercountColumn(line, pos?)方法:获取给定字符串line的位置pos(默认值是pos=line.length即该字符串的结尾位置)所对应的列数 column number
说明
前面两个 helper function 辅助函数参数
pos都是以字符串偏移量来表示位置的,但是如果文档/字符串里包含 Tab 字符,由于 Tab 字符的大小(占据的列数)是可以通过配置 facet 改变的(默认对应 4 列),所以字符偏移量一般和列数可能并不相同(即使只有一行文本内容)lineIndent(pos, bias?)方法:获取给定位置pos所在行的缩进量simulateBreak属性:一个数字(表示文档中的位置,以字符偏移量计算),表示该上下文会将该位置视作换行的位置,如果没有配置则返回null
通过该方式可以不依赖语法树,为编辑器添加代码缩进信息indentNodeProp变量:它是一个 node prop 节点属性,用于为语法树上特定的节点类型设置缩进信息
通过该方式添加的缩进信息是依赖语法树的,要基于节点类型进行配置,大概流程如下:- 通过调用 node prop 的方法
indentNodeProp.add(match: Object<T> | fn(type: NodeType) → T | undefined)为特定的节点类型设置缩进信息
传入的参数可以是一个对象,以键值对的方式为不同的节点类型设置缩进信息,键名是节点类型的名称;也可以传入一个函数fn(type: NodeType)动态为不同节点类型type设置缩进信息
该节点属性的值(如果是采用对象的方式,则是键名对应的值;如果是采用函数的形式,则是该函数的返回值)是一个函数fn(context: TreeIndentContext) → number | null称为 indentation strategy 缩进策略,它基于语法树的缩进上下文context(一个 classTreeIndentContext实例对象)计算当前节点类型缩进量/缩进深度,如果返回一个数字,表示该节点对应的缩进所占据的列数 column number;如果返回null,表示无法确定该节点类型的缩进TreeIndentContext
class
TreeIndentContext将缩进信息与语法树相关联,在后文将该类的实例化对象称作「语法树的缩进上下文」提示
该类一般不会通过手动进行实例化,而是在配置 node prop
indentNodeProp时,它的实例化对象作为参数提供给开发者直接使用,例如在 @codemirror/lang-python 模块里基于语法树设置缩进信息该类 extends 继承自 class
indentContext类,除了具有父类的一系列的属性和 helper function 辅助函数,还有一些额外的属性和方法以提供基于语法树节点 syntax node 的缩进信息:pos属性:表示当前所计算的缩进对应(在文档中)位置node属性:表示当前采用哪个节点的缩进策略textAfter属性:一个字符串,表示当前所在行于this.pos位置之后的内容,如果该行(在该位置)之后内容很多,则只返回最多 100 个字符baseIndent属性:一个数字,表示this.node节点的 reference line 参考行的缩进量/基础缩进量),一般就是该节点的第一行的缩进量
但如果该节点的起始行并不是占于行首,而是由更高层级的节点包裹,则需要跳过这些节点,直到占据行首的祖先节点,以其缩进量作为当前节点的基础缩进量baseIndentFor(node)方法:返回一个数字,表示给定的节点node的基础缩进量continue()方法:继续查找当前节点的父节点的缩进量,返回一个数字或null
该模块针对常见的多行缩进场景,内置了一些 indentation strategy 缩进策略或工厂函数(调用它们可以返回相应的缩进策略):delimitedIndent({closing: string, align?: boolean, units?: number})函数:工厂函数,用于为带有分隔符(一般由成对的符号构成,开始分隔符和闭合分隔符)的节点生成缩进策略
该方法的配置参数对象的各属性的具体说明如下- 第一个参数
closing是一个字符串,用于设置所需检测的闭合符号(通常是括号,例如")"、"]"或"}"),由于单独包含闭合分隔符的行是采用不同的缩进策略。如果该行的起始字符与闭合符号匹配,表示它是该节点的结束行,则该行采用该节点的基础缩进量;如果不是结束行,而是该节点的内容区块,则该行缩进量由unites决定 - 第二个(可选)参数
align是一个布尔值(默认值为true),用于设置该节点的内容区块的缩进策略。如果为true(而且起始行是由 non-skipped 非跳过节点包裹)则内容区块的行对齐到开始分隔符的结尾;如果是false则内容区块的行比该节点的基础缩进量多出units个缩进单位js// 参数 align 为 true 时 foo(bar, baz) // 该内容块会对齐到开始分隔符 ( 的结尾 - 第三个(可选)参数
units是一个数字(默认值是1),用于设置内容块的缩进需要比该节点的基础缩进量多几个缩进单位缩进单位
通过 facet
indentUnit配置缩进单位,默认值是 2 个空格
该工厂函数最后返回一个缩进策略(函数)fn(context: TreeIndentContext) → number,用于配置 node propindentNodeProp- 第一个参数
continuedIndent({except?: RegExp, units?: number} = {})函数:工厂函数,用于为续行生成缩进策略
函数具有多行参数,或链式调用等场景会出现续行,这些行的默认缩进量比(所属节点的)基础缩进量多一个缩进单位,通过该工厂所生成缩进策略可以针对特定情况自定缩进量
该方法的配置参数对象的各属性的具体说明如下- 第一个(可选)参数
except是一个正则表达式对象,当该行的内容匹配时,则该行的缩进量采用units个缩进量 - 第二个(可选)参数
units是一个数字,用于设置正则表达式所匹配的行采用几个缩进单位
该工厂函数最后返回一个缩进策略(函数)fn(context: TreeIndentContext) → number,用于配置 node propindentNodeProp- 第一个(可选)参数
flatIndent(context: TreeIndentContext) → number缩进策略,将节点的内容块都对齐到它的 base indentation 基础缩进量
- 调用该方法会返回一个函数(符合 TypeScript type
NodePropSource类型),然后将该返回值传递给NodeSet.extend方法或LRParser.configure方法将这些信息添加到解析器上(也可以在初始化解析器时,作为其配置对象的属性ParserConfig.props的值)
- 通过调用 node prop 的方法
对比
indentService 和 indentNodeProp 都可以为编辑器配置缩进信息,但适用场景是不同的
indentService不依赖语法树,可以更自由地为编辑器设置缩进信息indentNodeProp依赖于语法树,基于节点类型设置缩进信息
计算缩进量的流程:
- 执行方法
getIndentation或indentRange或indentOnInput需要计算缩进量 - 编辑器会先遍历所有通过 facet
indentService注册的函数,若其中返回数字或null就会停止遍历;若返回undefined会继续执行下一个函数 - 如果没有
indentService提供缩进值,就会回退到基于语法树的indentNodeProp策略,即基于节点类型推断缩进量
indentUnit变量:一个 facet 动态配置项,其输入值是一个字符串,用于设置缩进单元(一次缩进)采用插入什么具体的字符串序列,一般是相同的空白字符(如空格符或制表符),默认值是两个空格符indentOnInput()方法:返回一个插件,将其添加到编辑器(在实例化编辑器视图时,添加到配置对象的属性config.extensions上)可以启用输入时自动重新缩进 reindentation 的功能说明
该方法需要同步为编辑器配置语言相关的元信息,其中字段
indentOnInput用于设置与自动缩进相关的元信息,其值需要是一个正则表达式每当输入新文本时,如果从行首到光标位置的文本内容与该正则表达式匹配时,光标所在行将被自动缩进
正则表达式建议以
^开头,以$结尾,确保所匹配的内容就是从行首直到光标位置(而不是该行中间内容的部分匹配),避免触发不必要的重新缩进例如正则表达式
/^\s*\}$/表示以闭合的大括号}作为该行的首字符时,该行会自动重新缩进
获取缩进信息
getIndentation(context, pos)方法:基于给定的上下文context(可能是EditorState编辑器状态对象或IndentContext缩进上下文实例)获取位置pos所在行的缩进量,返回一个数字表示缩进所占的列数 column number,如果无法获取到明确的缩进量则返回null
编辑器会先遍历所有通过 facetindentService注册的函数,若其中返回数字或null就会停止遍历;若返回undefined会继续执行下一个函数,如果没有indentService提供缩进值,就会回退到基于语法树的indentNodeProp策略,即基于节点类型推断缩进量getIndentUnit(state)方法:获取当前编辑器状态中缩进单元所占据的列数 column number
如果(通过 facetindentUnit所设置)表示缩进单位的字符串中包含\t制表符,则在计算缩进单元所占据的列数时,需要依据 facettabSize(用于设置制表符的大小/占据的列宽)的值indentString(state, cols)方法:生成一个字符串,用于作为特定缩进量的占位符,它需要覆盖从0到cols的列宽距离
如果(通过 facetindentUnit所设置)表示缩进单位的字符串中包含\t制表符,则生成的字符串优先采用\t符号来构建,不足再由空格符来凑对比
有两个 facet 用于配置基本的缩进量:
- @codemirror/state 模块导出静态属性
EditorState.tabSize是一个 facet,用于配置 tab size 制表符\t的大小,即占据的列宽 column number(默认值为4) - @codemirror/language 模块导出的变量
indentUnit是一个 facet,用于设置缩进单元(一次缩进)采用插入什么具体的字符串序列
但是只有在配置
indentUnit时使用了\t制表符,即表示缩进的占位字符串中使用了制表符,EditorState.tabSize的配置才对文档中的缩进有影响两者的具体介绍和区别见论坛的两个讨论:
- @codemirror/state 模块导出静态属性
执行代码缩进
indentRange(state, from, to)方法:对于编辑器文档state中触及范围from到to的行执行自动缩进,返回一个ChangeSet类的实例(将传递给方法view.dispatch以实现缩进的变更)注意
该方法一般是对选定的多行进行批量缩进,或在粘贴多行内容时进行自动排版
但 auto-indent 的结果不一定是增加缩进,而是基于上下文修正缩进
括号匹配
一些与实现括号匹配的相关 API
bracketMatching(config?)方法:基于(可选)参数config(需要符合 TypeScript interfaceConfig,默认值是一个空对象)生成一个插件,将其添加到编辑器(在实例化编辑器视图时,添加到配置对象的属性config.extensions上)可以启用括号匹配功能,即光标位于括号字符的前后时,就会在文档寻找与之相匹配的括号另一半的字符,并将它们进行高亮,如果没有找到匹配的括号,则当前与光标相邻的字符会采用另一种高亮样式Config
TypeScript interface
Config用于配置括号匹配的行为,符合该格式的对象具有以下属性:afterCursor(可选)属性:一个布尔值,用于设置(如果在光标的前面无法找到括号字符时)是否需要在光标的后面寻找括号字符,默认值为true提示
如果光标前后都有括号字符,例如光标在
({两个括号字符之间,由于会先查询光标前面的字符是否为括号,所以在这种情况下,会高亮光标前面的括号(及其匹配的另一半括号字符)brackets(可选)属性:一个字符串,用于设置所需匹配的一系列括号字符,一般是成对出现,默认值是"()[]{}"匹配三种括号注意
该插件所设置的括号匹配其实是作为一种 fallback 后备方案
仅在无法通过语法树的节点获取到相应的匹配信息时(例如包含左括号字符的节点没有设置
closedBy节点属性,或包含右括号字符的节点没有设置openedBy节点属性,所以无法基于节点的语法信息获取到相匹配的括号字符),此时才会通过该插件所设置的信息来进行括号匹配maxScanDistance(可选)属性:一个数字,用于设置最大的扫描距离(以字符数量表示)来寻找匹配的括号,默认值是1000注意
该限制(最大扫描距离)也是仅在无法通过语法树(节点属性)获取到相应的括号匹配信息时才起作用
renderMatch(可选)属性:手动设置 decoration 装饰器(一般是 mark decoration 标记型装饰器),以高亮显示匹配/不匹配的括号
默认会为匹配的两个括号字符都添加 CSS class 类名cm-matchingBracket,而未能匹配的单个括号字符添加 CSS class 类名cm-nonmatchingBracket
matchBrackets(state, pos, dir, config?)方法:对文档状态state里给定位置pos的括号字符进行匹配分析
参数dir可以是-1或1两者之一,以设置扫描的方向
(可选)参数config是一个对象(需要符合 TypeScript interfaceConfig,默认值是{})用于配置括号匹配的行为,该方法只会用到配置对象中的属性brackets和属性maxScanDistance,以确定所需匹配的括号字符,以及限制扫描的最大距离
如果在位置pos没有发现括号字符则该方法返回null,否则返回一个对象(需要符合 TypeScript interfaceMatchResult)以描述匹配结果MatchResult
TypeScript interface
MatchResult描述括号匹配结果,具有如下属性:start属性:一个对象{from: number, to: number}(在给定位置pos)括号字符的位置范围end(可选)属性:一个对象{from: number, to: number}相匹配的括号字符的位置范围(可能没有找到匹配的括号字符,则匹配结果对象就不具有该属性)matched属性:一个布尔值,以表示两个括号字符是否匹配(即使end属性具有值,如果这两个字符并不匹配,该属性也会是false)
bracketMatchingHandle变量:一个 node prop 节点属性,用于为节点添加括号匹配的触发信息
对于类似 HTML 的编程语言,会将标记为 opening 开标签和 closing 闭标签的节点(视作括号)进行相互匹配
但是如果光标定位到该节点时整个节点(及其匹配节点)都进行高亮,可能会造成视觉干扰,该 node prop 节点属性bracketMatchingHandle可以为节点设置(局部)高亮区域,即仅当光标位于该后代节点里才会触发该 opening/closing 节点高亮,而且高亮区域只是该后代节点的区域
配置 node propbracketMatchingHandle的大概流程如下:- 通过调用 node prop 的方法
bracketMatchingHandle.add(match: Object<T> | fn(type: NodeType) → T | undefined)为特定的节点类型设置(类似括号)匹配及(局部)高亮信息
传入的参数可以是一个对象,以键值对的方式为不同的节点类型设置缩进信息,键名是节点类型的名称;也可以传入一个函数fn(type: NodeType)动态为不同节点类型type设置缩进信息
该节点属性的值(如果是采用对象的方式,则是键名对应的值;如果是采用函数的形式,则是该函数的返回值)是一个函数fn(node: SyntaxNode) → SyntaxNode | null它基于当前标记为 opening 或 closing 的节点node,返回需要局部高亮的后代节点,如果整个节点都进行高亮则直接返回null- 调用该方法会返回一个函数(符合 TypeScript type
NodePropSource类型),然后将该返回值传递给NodeSet.extend方法或LRParser.configure方法将这些信息添加到解析器上(也可以在初始化解析器时,作为其配置对象的属性ParserConfig.props的值)
例如在 @codemirror/lang-html 模块里为OpenTag和CloseTag节点设置匹配和局部高亮信息tsconst htmlPlain = LRLanguage.define({ // ... parser: parser.configure({ props: [ // ... bracketMatchingHandle.add({ // 开标签 OpenTag 和闭标签 CloseTag 进行匹配 // 局部高亮的区域是子节点 TagName 即标签的名称 // 例如编辑器的内容是 <div class="abc">xxx</div> // * 当光标定位到开标签 <div> 的名称 "div" 字符串内 // 则开标签和相匹配的闭标签都会高亮,即两个字符串 "div" 会高亮显示 // * 但是如果光标定位到标签 <div> 的类名 'class="abc"' 字符串内,则不会触发匹配和高亮 "OpenTag CloseTag": node => node.getChild("TagName") }) ] }) })- 通过调用 node prop 的方法