79 lines
3.5 KiB
TypeScript
79 lines
3.5 KiB
TypeScript
import type { Point, Range, Tree } from '@vscode/tree-sitter-wasm'
|
||
import type { LanguageAdapter, SourceRange } from './contract'
|
||
import * as path from 'node:path'
|
||
import { Language, Parser } from '@vscode/tree-sitter-wasm'
|
||
|
||
/** 打进扩展包里的 wasm 目录:dist/index.cjs 旁边的 dist/tree-sitter(esbuild 构建时复制)。只在打包产物里成立。 */
|
||
export function bundledWasmDir(): string {
|
||
return path.join(__dirname, 'tree-sitter')
|
||
}
|
||
|
||
/** 没有语言插件、只给工作区索引解析的语言(接口契约 openapi.json):languageId → 语法包的文件名。 */
|
||
const INDEX_GRAMMARS: Record<string, string> = { json: 'tree-sitter-json.wasm' }
|
||
|
||
/**
|
||
* 工作区索引按 languageId 解析时,没有语言插件的语言用哪个语法包;不是这几种语言时没有。
|
||
* @param wasmDir 放语法包的目录
|
||
*/
|
||
export function indexGrammar(wasmDir: string, languageId: string): Pick<LanguageAdapter, 'grammarPath'> | undefined {
|
||
const file = INDEX_GRAMMARS[languageId]
|
||
return file === undefined ? undefined : { grammarPath: () => path.join(wasmDir, file) }
|
||
}
|
||
|
||
/** 源码下标的行列(行从 0 起,列按 UTF-16,同 web-tree-sitter 解析 JS 字符串时的算法)。 */
|
||
function pointAt(source: string, index: number): Point {
|
||
const lineStart = source.lastIndexOf('\n', index - 1) + 1
|
||
let row = 0
|
||
for (let at = source.indexOf('\n'); at !== -1 && at < index; at = source.indexOf('\n', at + 1))
|
||
row++
|
||
return { row, column: index - lineStart }
|
||
}
|
||
|
||
function includedRange(source: string, { startIndex, endIndex }: SourceRange): Range {
|
||
return { startIndex, endIndex, startPosition: pointAt(source, startIndex), endPosition: pointAt(source, endIndex) }
|
||
}
|
||
|
||
// web-tree-sitter 的运行时是进程级单例:Parser.init 只能有效调用一次,后来的 wasm 目录不再生效
|
||
let runtimeReady: Promise<void> | undefined
|
||
|
||
function initRuntime(wasmDir: string): Promise<void> {
|
||
runtimeReady ??= Parser.init({ locateFile: (file: string) => path.join(wasmDir, file) })
|
||
.catch((err: unknown) => {
|
||
runtimeReady = undefined
|
||
throw err
|
||
})
|
||
return runtimeReady
|
||
}
|
||
|
||
/** 解析运行时:初始化一次 web-tree-sitter,语法包按 wasm 路径懒加载并缓存。 */
|
||
export class TreeSitterRuntime {
|
||
private readonly parsers = new Map<string, Promise<Parser>>()
|
||
|
||
/** @param wasmDir 放 tree-sitter.wasm 的目录 */
|
||
constructor(private readonly wasmDir: string) {}
|
||
|
||
/**
|
||
* 返回的语法树占着 wasm 内存,用完必须 `tree.delete()`。给了 ranges 就只解析源码里的这几段(嵌着的代码,见 EmbeddedCode),
|
||
* 节点的下标、行列照旧是整份源码的。
|
||
*/
|
||
async parse(adapter: Pick<LanguageAdapter, 'grammarPath'>, languageId: string, source: string, ranges?: readonly SourceRange[]): Promise<Tree> {
|
||
const parser = await this.parserFor(adapter.grammarPath(languageId))
|
||
const tree = parser.parse(source, null, ranges ? { includedRanges: ranges.map(range => includedRange(source, range)) } : undefined)
|
||
if (!tree)
|
||
throw new Error(`tree-sitter 没能解析 ${languageId} 源码`)
|
||
return tree
|
||
}
|
||
|
||
private parserFor(grammarPath: string): Promise<Parser> {
|
||
let parser = this.parsers.get(grammarPath)
|
||
if (!parser) {
|
||
parser = initRuntime(this.wasmDir)
|
||
.then(() => Language.load(grammarPath))
|
||
.then(language => new Parser().setLanguage(language))
|
||
parser.catch(() => this.parsers.delete(grammarPath))
|
||
this.parsers.set(grammarPath, parser)
|
||
}
|
||
return parser
|
||
}
|
||
}
|