322 lines
10 KiB
TypeScript
322 lines
10 KiB
TypeScript
import { C, INTERNAL, LogCallback, ParseCallback, Range, SIZE_OF_INT, SIZE_OF_RANGE, setModule } from './constants';
|
|
import { Language } from './language';
|
|
import { marshalRange, unmarshalRange } from './marshal';
|
|
import { checkModule, initializeBinding } from './bindings';
|
|
import { Tree } from './tree';
|
|
|
|
/**
|
|
* Options for parsing
|
|
*
|
|
* The `includedRanges` property is an array of {@link Range} objects that
|
|
* represent the ranges of text that the parser should include when parsing.
|
|
*
|
|
* The `progressCallback` property is a function that is called periodically
|
|
* during parsing to check whether parsing should be cancelled.
|
|
*
|
|
* See {@link Parser#parse} for more information.
|
|
*/
|
|
export interface ParseOptions {
|
|
/**
|
|
* An array of {@link Range} objects that
|
|
* represent the ranges of text that the parser should include when parsing.
|
|
*
|
|
* This sets the ranges of text that the parser should include when parsing.
|
|
* By default, the parser will always include entire documents. This
|
|
* function allows you to parse only a *portion* of a document but
|
|
* still return a syntax tree whose ranges match up with the document
|
|
* as a whole. You can also pass multiple disjoint ranges.
|
|
* If `ranges` is empty, then the entire document will be parsed.
|
|
* Otherwise, the given ranges must be ordered from earliest to latest
|
|
* in the document, and they must not overlap. That is, the following
|
|
* must hold for all `i` < `length - 1`:
|
|
* ```text
|
|
* ranges[i].end_byte <= ranges[i + 1].start_byte
|
|
* ```
|
|
*/
|
|
includedRanges?: Range[];
|
|
|
|
/**
|
|
* A function that is called periodically during parsing to check
|
|
* whether parsing should be cancelled. If the progress callback returns
|
|
* `false`, then parsing will be cancelled. You can also use this to instrument
|
|
* parsing and check where the parser is at in the document. The progress callback
|
|
* takes a single argument, which is a {@link ParseState} representing the current
|
|
* state of the parser.
|
|
*/
|
|
progressCallback?: (state: ParseState) => void;
|
|
}
|
|
|
|
/**
|
|
* A stateful object that is passed into the progress callback {@link ParseOptions#progressCallback}
|
|
* to provide the current state of the parser.
|
|
*/
|
|
export interface ParseState {
|
|
/** The byte offset in the document that the parser is at. */
|
|
currentOffset: number;
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*
|
|
* Global variable for transferring data across the FFI boundary
|
|
*/
|
|
export let TRANSFER_BUFFER: number;
|
|
|
|
/**
|
|
* The latest ABI version that is supported by the current version of the
|
|
* library.
|
|
*
|
|
* When Languages are generated by the Tree-sitter CLI, they are
|
|
* assigned an ABI version number that corresponds to the current CLI version.
|
|
* The Tree-sitter library is generally backwards-compatible with languages
|
|
* generated using older CLI versions, but is not forwards-compatible.
|
|
*/
|
|
export let LANGUAGE_VERSION: number;
|
|
|
|
/**
|
|
* The earliest ABI version that is supported by the current version of the
|
|
* library.
|
|
*/
|
|
export let MIN_COMPATIBLE_VERSION: number;
|
|
|
|
/**
|
|
* A stateful object that is used to produce a {@link Tree} based on some
|
|
* source code.
|
|
*/
|
|
export class Parser {
|
|
/** @internal */
|
|
private [0] = 0; // Internal handle for WASM
|
|
|
|
/** @internal */
|
|
private [1] = 0; // Internal handle for WASM
|
|
|
|
/** @internal */
|
|
private logCallback: LogCallback | null = null;
|
|
|
|
/** The parser's current language. */
|
|
language: Language | null = null;
|
|
|
|
/**
|
|
* This must always be called before creating a Parser.
|
|
*
|
|
* You can optionally pass in options to configure the WASM module, the most common
|
|
* one being `locateFile` to help the module find the `.wasm` file.
|
|
*/
|
|
static async init(moduleOptions?: EmscriptenModule) {
|
|
setModule(await initializeBinding(moduleOptions));
|
|
TRANSFER_BUFFER = C._ts_init();
|
|
LANGUAGE_VERSION = C.getValue(TRANSFER_BUFFER, 'i32');
|
|
MIN_COMPATIBLE_VERSION = C.getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
|
|
}
|
|
|
|
/**
|
|
* Create a new parser.
|
|
*/
|
|
constructor() {
|
|
this.initialize();
|
|
}
|
|
|
|
/** @internal */
|
|
initialize() {
|
|
if (!checkModule()) {
|
|
throw new Error("cannot construct a Parser before calling `init()`");
|
|
}
|
|
C._ts_parser_new_wasm();
|
|
this[0] = C.getValue(TRANSFER_BUFFER, 'i32');
|
|
this[1] = C.getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
|
|
}
|
|
|
|
/** Delete the parser, freeing its resources. */
|
|
delete() {
|
|
C._ts_parser_delete(this[0]);
|
|
C._free(this[1]);
|
|
this[0] = 0;
|
|
this[1] = 0;
|
|
}
|
|
|
|
/**
|
|
* Set the language that the parser should use for parsing.
|
|
*
|
|
* If the language was not successfully assigned, an error will be thrown.
|
|
* This happens if the language was generated with an incompatible
|
|
* version of the Tree-sitter CLI. Check the language's version using
|
|
* {@link Language#version} and compare it to this library's
|
|
* {@link LANGUAGE_VERSION} and {@link MIN_COMPATIBLE_VERSION} constants.
|
|
*/
|
|
setLanguage(language: Language | null): this {
|
|
let address: number;
|
|
if (!language) {
|
|
address = 0;
|
|
this.language = null;
|
|
} else if (language.constructor === Language) {
|
|
address = language[0];
|
|
const version = C._ts_language_version(address);
|
|
if (version < MIN_COMPATIBLE_VERSION || LANGUAGE_VERSION < version) {
|
|
throw new Error(
|
|
`Incompatible language version ${version}. ` +
|
|
`Compatibility range ${MIN_COMPATIBLE_VERSION} through ${LANGUAGE_VERSION}.`
|
|
);
|
|
}
|
|
this.language = language;
|
|
} else {
|
|
throw new Error('Argument must be a Language');
|
|
}
|
|
|
|
C._ts_parser_set_language(this[0], address);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Parse a slice of UTF8 text.
|
|
*
|
|
* @param {string | ParseCallback} callback - The UTF8-encoded text to parse or a callback function.
|
|
*
|
|
* @param {Tree | null} [oldTree] - A previous syntax tree parsed from the same document. If the text of the
|
|
* document has changed since `oldTree` was created, then you must edit `oldTree` to match
|
|
* the new text using {@link Tree#edit}.
|
|
*
|
|
* @param {ParseOptions} [options] - Options for parsing the text.
|
|
* This can be used to set the included ranges, or a progress callback.
|
|
*
|
|
* @returns {Tree | null} A {@link Tree} if parsing succeeded, or `null` if:
|
|
* - The parser has not yet had a language assigned with {@link Parser#setLanguage}.
|
|
* - The progress callback returned true.
|
|
*/
|
|
parse(
|
|
callback: string | ParseCallback,
|
|
oldTree?: Tree | null,
|
|
options?: ParseOptions,
|
|
): Tree | null {
|
|
if (typeof callback === 'string') {
|
|
C.currentParseCallback = (index: number) => callback.slice(index);
|
|
} else if (typeof callback === 'function') {
|
|
C.currentParseCallback = callback;
|
|
} else {
|
|
throw new Error('Argument must be a string or a function');
|
|
}
|
|
|
|
if (options?.progressCallback) {
|
|
C.currentProgressCallback = options.progressCallback;
|
|
} else {
|
|
C.currentProgressCallback = null;
|
|
}
|
|
|
|
if (this.logCallback) {
|
|
C.currentLogCallback = this.logCallback;
|
|
C._ts_parser_enable_logger_wasm(this[0], 1);
|
|
} else {
|
|
C.currentLogCallback = null;
|
|
C._ts_parser_enable_logger_wasm(this[0], 0);
|
|
}
|
|
|
|
let rangeCount = 0;
|
|
let rangeAddress = 0;
|
|
if (options?.includedRanges) {
|
|
rangeCount = options.includedRanges.length;
|
|
rangeAddress = C._calloc(rangeCount, SIZE_OF_RANGE);
|
|
let address = rangeAddress;
|
|
for (let i = 0; i < rangeCount; i++) {
|
|
marshalRange(address, options.includedRanges[i]);
|
|
address += SIZE_OF_RANGE;
|
|
}
|
|
}
|
|
|
|
const treeAddress = C._ts_parser_parse_wasm(
|
|
this[0],
|
|
this[1],
|
|
oldTree ? oldTree[0] : 0,
|
|
rangeAddress,
|
|
rangeCount
|
|
);
|
|
|
|
if (!treeAddress) {
|
|
C.currentParseCallback = null;
|
|
C.currentLogCallback = null;
|
|
C.currentProgressCallback = null;
|
|
return null;
|
|
}
|
|
|
|
if (!this.language) {
|
|
throw new Error('Parser must have a language to parse');
|
|
}
|
|
|
|
const result = new Tree(INTERNAL, treeAddress, this.language, C.currentParseCallback);
|
|
C.currentParseCallback = null;
|
|
C.currentLogCallback = null;
|
|
C.currentProgressCallback = null;
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Instruct the parser to start the next parse from the beginning.
|
|
*
|
|
* If the parser previously failed because of a timeout, cancellation,
|
|
* or callback, then by default, it will resume where it left off on the
|
|
* next call to {@link Parser#parse} or other parsing functions.
|
|
* If you don't want to resume, and instead intend to use this parser to
|
|
* parse some other document, you must call `reset` first.
|
|
*/
|
|
reset(): void {
|
|
C._ts_parser_reset(this[0]);
|
|
}
|
|
|
|
/** Get the ranges of text that the parser will include when parsing. */
|
|
getIncludedRanges(): Range[] {
|
|
C._ts_parser_included_ranges_wasm(this[0]);
|
|
const count = C.getValue(TRANSFER_BUFFER, 'i32');
|
|
const buffer = C.getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
|
|
const result = new Array<Range>(count);
|
|
|
|
if (count > 0) {
|
|
let address = buffer;
|
|
for (let i = 0; i < count; i++) {
|
|
result[i] = unmarshalRange(address);
|
|
address += SIZE_OF_RANGE;
|
|
}
|
|
C._free(buffer);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @deprecated since version 0.25.0, prefer passing a progress callback to {@link Parser#parse}
|
|
*
|
|
* Get the duration in microseconds that parsing is allowed to take.
|
|
*
|
|
* This is set via {@link Parser#setTimeoutMicros}.
|
|
*/
|
|
getTimeoutMicros(): number {
|
|
return C._ts_parser_timeout_micros(this[0]);
|
|
}
|
|
|
|
/**
|
|
* @deprecated since version 0.25.0, prefer passing a progress callback to {@link Parser#parse}
|
|
*
|
|
* Set the maximum duration in microseconds that parsing should be allowed
|
|
* to take before halting.
|
|
*
|
|
* If parsing takes longer than this, it will halt early, returning `null`.
|
|
* See {@link Parser#parse} for more information.
|
|
*/
|
|
setTimeoutMicros(timeout: number): void {
|
|
C._ts_parser_set_timeout_micros(this[0], 0, timeout);
|
|
}
|
|
|
|
/** Set the logging callback that a parser should use during parsing. */
|
|
setLogger(callback: LogCallback | boolean | null): this {
|
|
if (!callback) {
|
|
this.logCallback = null;
|
|
} else if (typeof callback !== 'function') {
|
|
throw new Error('Logger callback must be a function');
|
|
} else {
|
|
this.logCallback = callback;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/** Get the parser's current logger. */
|
|
getLogger(): LogCallback | null {
|
|
return this.logCallback;
|
|
}
|
|
}
|