web-tree-sitter
Version:
Tree-sitter bindings for the web
326 lines (292 loc) • 10.7 kB
text/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
* `true`, 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;
/** Indicates whether the parser has encountered an error during parsing. */
hasError: boolean;
}
/**
* @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;
}
}