Files
superwork/vscode/src/codeReader/runtime.ts
T

79 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}