From 09cb4c5729380bd9b57572f5b913b83c4e09a202 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Mon, 20 Jan 2025 02:43:52 -0500 Subject: [PATCH] feat(web): document the API --- lib/binding_web/lib/exports.txt | 1 + lib/binding_web/lib/tree-sitter.c | 6 + lib/binding_web/lib/tree-sitter.d.ts | 1 + lib/binding_web/src/bindings.ts | 12 + lib/binding_web/src/constants.ts | 79 ++++++- lib/binding_web/src/index.ts | 12 +- lib/binding_web/src/language.ts | 137 +++++++++-- lib/binding_web/src/lookahead_iterator.ts | 29 ++- lib/binding_web/src/marshal.ts | 53 ++++- lib/binding_web/src/node.ts | 275 ++++++++++++++++++---- lib/binding_web/src/parser.ts | 157 ++++++++++-- lib/binding_web/src/query.ts | 234 ++++++++++++++++-- lib/binding_web/src/tree.ts | 43 +++- lib/binding_web/src/tree_cursor.ts | 229 +++++++++++++----- lib/binding_web/test/parser.test.ts | 6 +- lib/binding_web/web-tree-sitter.d.ts | 74 +++++- lib/binding_web/web-tree-sitter.d.ts.map | 5 +- 17 files changed, 1155 insertions(+), 198 deletions(-) diff --git a/lib/binding_web/lib/exports.txt b/lib/binding_web/lib/exports.txt index 14da333d..e8aaf822 100644 --- a/lib/binding_web/lib/exports.txt +++ b/lib/binding_web/lib/exports.txt @@ -43,6 +43,7 @@ "ts_node_next_named_sibling_wasm", "ts_node_next_sibling_wasm", "ts_node_parent_wasm", +"ts_node_child_with_descendant_wasm", "ts_node_prev_named_sibling_wasm", "ts_node_prev_sibling_wasm", "ts_node_descendant_count_wasm", diff --git a/lib/binding_web/lib/tree-sitter.c b/lib/binding_web/lib/tree-sitter.c index 45ca7ddd..a54cf5ea 100644 --- a/lib/binding_web/lib/tree-sitter.c +++ b/lib/binding_web/lib/tree-sitter.c @@ -597,6 +597,12 @@ void ts_node_parent_wasm(const TSTree *tree) { marshal_node(TRANSFER_BUFFER, ts_node_parent(node)); } +void ts_node_child_with_descendant_wasm(const TSTree *tree) { + TSNode node = unmarshal_node(tree); + TSNode descendant = unmarshal_node(tree); + marshal_node(TRANSFER_BUFFER, ts_node_child_with_descendant(node, descendant)); +} + void ts_node_descendant_for_index_wasm(const TSTree *tree) { TSNode node = unmarshal_node(tree); const void **address = TRANSFER_BUFFER + SIZE_OF_NODE; diff --git a/lib/binding_web/lib/tree-sitter.d.ts b/lib/binding_web/lib/tree-sitter.d.ts index 55e217e4..f34a5aff 100644 --- a/lib/binding_web/lib/tree-sitter.d.ts +++ b/lib/binding_web/lib/tree-sitter.d.ts @@ -174,6 +174,7 @@ interface WasmModule { _ts_node_prev_named_sibling_wasm(_0: number): void; _ts_node_descendant_count_wasm(_0: number): number; _ts_node_parent_wasm(_0: number): void; + _ts_node_child_with_descendant_wasm(_0: number): void; _ts_node_descendant_for_index_wasm(_0: number): void; _ts_node_named_descendant_for_index_wasm(_0: number): void; _ts_node_descendant_for_position_wasm(_0: number): void; diff --git a/lib/binding_web/src/bindings.ts b/lib/binding_web/src/bindings.ts index ad2b7b87..322297d1 100644 --- a/lib/binding_web/src/bindings.ts +++ b/lib/binding_web/src/bindings.ts @@ -1,7 +1,14 @@ import createModule, { type MainModule } from '../lib/tree-sitter'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { type Parser } from './parser'; export let Module: MainModule | null = null; +/** + * @internal + * + * Initialize the Tree-sitter WASM module. This should only be called by the {@link Parser} class via {@link Parser.init}. + */ export async function initializeBinding(moduleOptions?: EmscriptenModule): Promise { if (!Module) { Module = await createModule(moduleOptions); @@ -9,6 +16,11 @@ export async function initializeBinding(moduleOptions?: EmscriptenModule): Promi return Module; } +/** + * @internal + * + * Checks if the Tree-sitter WASM module has been initialized. + */ export function checkModule(): boolean { return !!Module; } diff --git a/lib/binding_web/src/constants.ts b/lib/binding_web/src/constants.ts index 78bf8536..7a98bd63 100644 --- a/lib/binding_web/src/constants.ts +++ b/lib/binding_web/src/constants.ts @@ -1,48 +1,112 @@ import { type MainModule } from '../lib/tree-sitter'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { ParseState, type Parser } from './parser'; +/** + * A position in a multi-line text document, in terms of rows and columns. + * + * Rows and columns are zero-based. + */ export interface Point { + /** The zero-based row number. */ row: number; + + /** The zero-based column number. */ column: number; } +/** + * A range of positions in a multi-line text document, both in terms of bytes + * and of rows and columns. + */ export interface Range { + /** The start position of the range. */ startPosition: Point; + + /** The end position of the range. */ endPosition: Point; + + /** The start index of the range. */ startIndex: number; + + /** The end index of the range. */ endIndex: number; } +/** + * A summary of a change to a text document. + */ export interface Edit { + /** The start position of the change. */ startPosition: Point; + + /** The end position of the change before the edit. */ oldEndPosition: Point; + + /** The end position of the change after the edit. */ newEndPosition: Point; + + /** The start index of the change. */ startIndex: number; + + /** The end index of the change before the edit. */ oldEndIndex: number; + + /** The end index of the change after the edit. */ newEndIndex: number; } +/** @internal */ export const SIZE_OF_SHORT = 2; + +/** @internal */ export const SIZE_OF_INT = 4; + +/** @internal */ export const SIZE_OF_CURSOR = 4 * SIZE_OF_INT; + +/** @internal */ export const SIZE_OF_NODE = 5 * SIZE_OF_INT; + +/** @internal */ export const SIZE_OF_POINT = 2 * SIZE_OF_INT; + +/** @internal */ export const SIZE_OF_RANGE = 2 * SIZE_OF_INT + 2 * SIZE_OF_POINT; + +/** @internal */ export const ZERO_POINT: Point = { row: 0, column: 0 }; -// Types for callbacks +/** + * A callback for parsing that takes an index and point, and should return a string. + */ export type ParseCallback = (index: number, position: Point) => string | undefined; -export type ProgressCallback = (progress: { currentOffset: number }) => boolean; + +/** + * A callback that receives the parse state during parsing. + */ +export type ProgressCallback = (progress: ParseState) => boolean; + +/** + * A callback for logging messages. + * + * If `isLex` is `true`, the message is from the lexer, otherwise it's from the parser. + */ export type LogCallback = (message: string, isLex: boolean) => void; // Helper type for internal use +/** @internal */ export const INTERNAL = Symbol('INTERNAL'); +/** @internal */ export type Internal = typeof INTERNAL; // Helper functions for type checking +/** @internal */ export function assertInternal(x: unknown): asserts x is Internal { if (x !== INTERNAL) throw new Error('Illegal constructor'); } +/** @internal */ export function isPoint(point?: Point): point is Point { return ( !!point && @@ -51,8 +115,19 @@ export function isPoint(point?: Point): point is Point { ); } +/** + * @internal + * + * Sets the Tree-sitter WASM module. This should only be called by the {@link Parser} class via {@link Parser.init}. + */ export function setModule(module: MainModule) { C = module; } +/** + * @internal + * + * `C` is a convenient shorthand for the Tree-sitter WASM module, + * which allows us to call all of the exported functions. + */ export let C: MainModule; diff --git a/lib/binding_web/src/index.ts b/lib/binding_web/src/index.ts index 556bfd44..c59bcc0d 100644 --- a/lib/binding_web/src/index.ts +++ b/lib/binding_web/src/index.ts @@ -1,9 +1,9 @@ export * from './constants'; export * from './marshal'; -export * from './node'; -export * from './tree'; -export * from './tree_cursor'; -export * from './lookahead_iterator'; -export * from './query'; -export * from './language'; export * from './parser'; +export * from './language'; +export * from './tree'; +export * from './node'; +export * from './tree_cursor'; +export * from './query'; +export * from './lookahead_iterator'; diff --git a/lib/binding_web/src/language.ts b/lib/binding_web/src/language.ts index 7ba48f75..07743407 100644 --- a/lib/binding_web/src/language.ts +++ b/lib/binding_web/src/language.ts @@ -2,7 +2,7 @@ import { C, INTERNAL, Internal, assertInternal, SIZE_OF_INT, SIZE_OF_SHORT } fro import { LookaheadIterator } from './lookahead_iterator'; import { Node } from './node'; import { TRANSFER_BUFFER } from './parser'; -import { CaptureQuantifier, Predicate, PredicateStep, Properties, Query, TextPredicate } from './query'; +import { CaptureQuantifier, QueryPredicate, PredicateStep, QueryProperties, Query, TextPredicate } from './query'; const PREDICATE_STEP_TYPE_CAPTURE = 1; const PREDICATE_STEP_TYPE_STRING = 2; @@ -10,14 +10,27 @@ const PREDICATE_STEP_TYPE_STRING = 2; const QUERY_WORD_REGEX = /[\w-]+/g; const LANGUAGE_FUNCTION_REGEX = /^tree_sitter_\w+$/; +/** + * An opaque object that defines how to parse a particular language. + * The code for each `Language` is generated by the Tree-sitter CLI. + */ export class Language { /** @internal */ private [0] = 0; // Internal handle for WASM + /** + * A list of all node types in the language. The index of each type in this + * array is its node type id. + */ types: string[]; + /** + * A list of all field names in the language. The index of each field name in + * this array is its field id. + */ fields: (string | null)[]; + /** @internal */ constructor(internal: Internal, address: number) { assertInternal(internal); this[0] = address; @@ -38,33 +51,54 @@ export class Language { } } + + /** + * Gets the name of the language. + */ get name(): string | null { const ptr = C._ts_language_name(this[0]); if (ptr === 0) return null; return C.UTF8ToString(ptr); } + /** + * Gets the version of the language. + */ get version(): number { return C._ts_language_version(this[0]); } - + /** + * Gets the number of fields in the language. + */ get fieldCount(): number { return this.fields.length - 1; } + /** + * Gets the number of states in the language. + */ get stateCount(): number { return C._ts_language_state_count(this[0]); } + /** + * Get the field id for a field name. + */ fieldIdForName(fieldName: string): number | null { const result = this.fields.indexOf(fieldName); return result !== -1 ? result : null; } + /** + * Get the field name for a field id. + */ fieldNameForId(fieldId: number): string | null { return this.fields[fieldId] ?? null; } + /** + * Get the node type id for a node type name. + */ idForNodeType(type: string, named: boolean): number | null { const typeLength = C.lengthBytesUTF8(type); const typeAddress = C._malloc(typeLength + 1); @@ -74,23 +108,42 @@ export class Language { return result || null; } + /** + * Gets the number of node types in the language. + */ get nodeTypeCount(): number { return C._ts_language_symbol_count(this[0]); } + /** + * Get the node type name for a node type id. + */ nodeTypeForId(typeId: number): string | null { const name = C._ts_language_symbol_name(this[0], typeId); return name ? C.UTF8ToString(name) : null; } + /** + * Check if a node type is named. + * + * @see {@link https://tree-sitter.github.io/tree-sitter/using-parsers/2-basic-parsing.html#named-vs-anonymous-nodes} + */ nodeTypeIsNamed(typeId: number): boolean { return C._ts_language_type_is_named_wasm(this[0], typeId) ? true : false; } + /** + * Check if a node type is visible. + */ nodeTypeIsVisible(typeId: number): boolean { return C._ts_language_type_is_visible_wasm(this[0], typeId) ? true : false; } + /** + * Get the supertypes ids of this language. + * + * @see {@link https://tree-sitter.github.io/tree-sitter/using-parsers/6-static-node-types.html?highlight=supertype#supertype-nodes} + */ get supertypes(): number[] { C._ts_language_supertypes_wasm(this[0]); const count = C.getValue(TRANSFER_BUFFER, 'i32'); @@ -108,6 +161,9 @@ export class Language { return result; } + /** + * Get the subtype ids for a given supertype node id. + */ subtypes(supertype: number): number[] { C._ts_language_subtypes_wasm(this[0], supertype); const count = C.getValue(TRANSFER_BUFFER, 'i32'); @@ -125,16 +181,44 @@ export class Language { return result; } + /** + * Get the next state id for a given state id and node type id. + */ nextState(stateId: number, typeId: number): number { return C._ts_language_next_state(this[0], stateId, typeId); } + /** + * Create a new lookahead iterator for this language and parse state. + * + * This returns `null` if state is invalid for this language. + * + * Iterating {@link LookaheadIterator} will yield valid symbols in the given + * parse state. Newly created lookahead iterators will return the `ERROR` + * symbol from {@link LookaheadIterator#currentType}. + * + * Lookahead iterators can be useful for generating suggestions and improving + * syntax error diagnostics. To get symbols valid in an `ERROR` node, use the + * lookahead iterator on its first leaf node state. For `MISSING` nodes, a + * lookahead iterator created on the previous non-extra leaf node may be + * appropriate. + */ lookaheadIterator(stateId: number): LookaheadIterator | null { const address = C._ts_lookahead_iterator_new(this[0], stateId); if (address) return new LookaheadIterator(INTERNAL, address, this); return null; } + /** + * Create a new query from a string containing one or more S-expression + * patterns. + * + * The query is associated with a particular language, and can only be run + * on syntax nodes parsed with that language. References to Queries can be + * shared between multiple threads. + * + * @link {@see https://tree-sitter.github.io/tree-sitter/using-parsers/queries} + */ query(source: string): Query { const sourceLength = C.lengthBytesUTF8(source); const sourceAddress = C._malloc(sourceLength + 1); @@ -219,10 +303,10 @@ export class Language { stringValues[i] = C.UTF8ToString(valueAddress, nameLength); } - const setProperties = new Array(patternCount); - const assertedProperties = new Array(patternCount); - const refutedProperties = new Array(patternCount); - const predicates = new Array(patternCount); + const setProperties = new Array(patternCount); + const assertedProperties = new Array(patternCount); + const refutedProperties = new Array(patternCount); + const predicates = new Array(patternCount); const textPredicates = new Array(patternCount); for (let i = 0; i < patternCount; i++) { @@ -236,7 +320,11 @@ export class Language { predicates[i] = []; textPredicates[i] = []; - const steps: PredicateStep[] = []; + const steps = new Array(); + const isStringStep = (step: PredicateStep): step is { type: 'string', value: string } => { + return step.type === 'string'; + } + let stepAddress = predicatesAddress; for (let j = 0; j < stepCount; j++) { const stepType = C.getValue(stepAddress, 'i32'); @@ -329,7 +417,7 @@ export class Language { } if (steps[2].type !== 'string') { throw new Error( - `Second argument of \`#${operator}\` predicate must be a string. Got @${steps[2].value}.`, + `Second argument of \`#${operator}\` predicate must be a string. Got @${steps[2].name}.`, ); } captureName = steps[1].name; @@ -359,13 +447,13 @@ export class Language { `Wrong number of arguments to \`#set!\` predicate. Expected 1 or 2. Got ${steps.length - 1}.`, ); } - if (steps.some((s) => s.type !== 'string')) { + if (!steps.every(isStringStep)) { throw new Error( - `Arguments to \`#set!\` predicate must be a strings.".`, + `Arguments to \`#set!\` predicate must be strings.".`, ); } if (!setProperties[i]) setProperties[i] = {}; - setProperties[i][steps[1].value!] = steps[2]?.value ?? null; + setProperties[i][steps[1].value] = steps[2]?.value ?? null; break; } @@ -376,14 +464,14 @@ export class Language { `Wrong number of arguments to \`#${operator}\` predicate. Expected 1 or 2. Got ${steps.length - 1}.`, ); } - if (steps.some((s) => s.type !== 'string')) { + if (!steps.every(isStringStep)) { throw new Error( - `Arguments to \`#${operator}\` predicate must be a strings.".`, + `Arguments to \`#${operator}\` predicate must be strings.".`, ); } const properties = operator === 'is?' ? assertedProperties : refutedProperties; if (!properties[i]) properties[i] = {}; - properties[i][steps[1].value!] = steps[2]?.value ?? null; + properties[i][steps[1].value] = steps[2]?.value ?? null; break; } @@ -400,15 +488,16 @@ export class Language { `First argument of \`#${operator}\` predicate must be a capture. Got "${steps[1].value}".`, ); } - for (let i = 2; i < steps.length; i++) { - if (steps[i].type !== 'string') { - throw new Error( - `Arguments to \`#${operator}\` predicate must be a strings.".`, - ); - } - } captureName = steps[1].name; - const values = steps.slice(2).map((s) => s.value); + + const stringSteps = steps.slice(2); + if (!stringSteps.every(isStringStep)) { + throw new Error( + `Arguments to \`#${operator}\` predicate must be strings.".`, + ); + } + const values = stringSteps.map((s) => s.value); + textPredicates[i].push((captures) => { const nodes = []; for (const c of captures) { @@ -447,6 +536,10 @@ export class Language { ); } + /** + * Load a language from a WebAssembly module. + * The module can be provided as a path to a file or as a buffer. + */ static async load(input: string | Uint8Array): Promise { let bytes: Promise; if (input instanceof Uint8Array) { diff --git a/lib/binding_web/src/lookahead_iterator.ts b/lib/binding_web/src/lookahead_iterator.ts index e7468a08..028608b5 100644 --- a/lib/binding_web/src/lookahead_iterator.ts +++ b/lib/binding_web/src/lookahead_iterator.ts @@ -8,29 +8,36 @@ export class LookaheadIterator implements Iterable { /** @internal */ private language: Language; + /** @internal */ constructor(internal: Internal, address: number, language: Language) { assertInternal(internal); this[0] = address; this.language = language; } + /** Get the current symbol of the lookahead iterator. */ get currentTypeId(): number { return C._ts_lookahead_iterator_current_symbol(this[0]); } + /** Get the current symbol name of the lookahead iterator. */ get currentType(): string { return this.language.types[this.currentTypeId] || 'ERROR'; } + /** Delete the lookahead iterator, freeing its resources. */ delete(): void { C._ts_lookahead_iterator_delete(this[0]); this[0] = 0; } - resetState(stateId: number): boolean { - return Boolean(C._ts_lookahead_iterator_reset_state(this[0], stateId)); - } + /** + * Reset the lookahead iterator. + * + * This returns `true` if the language was set successfully and `false` + * otherwise. + */ reset(language: Language, stateId: number): boolean { if (C._ts_lookahead_iterator_reset(this[0], language[0], stateId)) { this.language = language; @@ -39,6 +46,22 @@ export class LookaheadIterator implements Iterable { return false; } + /** + * Reset the lookahead iterator to another state. + * + * This returns `true` if the iterator was reset to the given state and + * `false` otherwise. + */ + resetState(stateId: number): boolean { + return Boolean(C._ts_lookahead_iterator_reset_state(this[0], stateId)); + } + + /** + * Returns an iterator that iterates over the symbols of the lookahead iterator. + * + * The iterator will yield the current symbol name as a string for each step + * until there are no more symbols to iterate over. + */ [Symbol.iterator](): Iterator { return { next: (): IteratorResult => { diff --git a/lib/binding_web/src/marshal.ts b/lib/binding_web/src/marshal.ts index 44efa233..01907d91 100644 --- a/lib/binding_web/src/marshal.ts +++ b/lib/binding_web/src/marshal.ts @@ -1,10 +1,16 @@ import { Edit, INTERNAL, Point, Range, SIZE_OF_INT, SIZE_OF_NODE, SIZE_OF_POINT, C } from "./constants"; import { Node } from "./node"; import { Tree } from "./tree"; -import { Query } from "./query"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { Query, type QueryMatch } from "./query"; import { TreeCursor } from "./tree_cursor"; import { TRANSFER_BUFFER } from "./parser"; +/** + * @internal + * + * Unmarshals a {@link QueryMatch} to the transfer buffer. + */ export function unmarshalCaptures(query: Query, tree: Tree, address: number, result: {name: string, node: Node}[]) { for (let i = 0, n = result.length; i < n; i++) { const captureIndex = C.getValue(address, 'i32'); @@ -16,6 +22,11 @@ export function unmarshalCaptures(query: Query, tree: Tree, address: number, res return address; } +/** + * @internal + * + * Marshals a {@link Node} to the transfer buffer. + */ export function marshalNode(node: Node) { let address = TRANSFER_BUFFER; C.setValue(address, node.id, 'i32'); @@ -29,6 +40,11 @@ export function marshalNode(node: Node) { C.setValue(address, node[0], 'i32'); } +/** + * @internal + * + * Unmarshals a {@link Node} from the transfer buffer. + */ export function unmarshalNode(tree: Tree, address = TRANSFER_BUFFER): Node | null { const id = C.getValue(address, 'i32'); address += SIZE_OF_INT; @@ -53,6 +69,11 @@ export function unmarshalNode(tree: Tree, address = TRANSFER_BUFFER): Node | nul return result; } +/** + * @internal + * + * Marshals a {@link TreeCursor} to the transfer buffer. + */ export function marshalTreeCursor(cursor: TreeCursor, address = TRANSFER_BUFFER) { C.setValue(address + 0 * SIZE_OF_INT, cursor[0], 'i32'); C.setValue(address + 1 * SIZE_OF_INT, cursor[1], 'i32'); @@ -60,6 +81,11 @@ export function marshalTreeCursor(cursor: TreeCursor, address = TRANSFER_BUFFER) C.setValue(address + 3 * SIZE_OF_INT, cursor[3], 'i32'); } +/** + * @internal + * + * Unmarshals a {@link TreeCursor} from the transfer buffer. + */ export function unmarshalTreeCursor(cursor: TreeCursor) { cursor[0] = C.getValue(TRANSFER_BUFFER + 0 * SIZE_OF_INT, 'i32'); cursor[1] = C.getValue(TRANSFER_BUFFER + 1 * SIZE_OF_INT, 'i32'); @@ -67,11 +93,21 @@ export function unmarshalTreeCursor(cursor: TreeCursor) { cursor[3] = C.getValue(TRANSFER_BUFFER + 3 * SIZE_OF_INT, 'i32'); } +/** + * @internal + * + * Marshals a {@link Point} to the transfer buffer. + */ export function marshalPoint(address: number, point: Point): void { C.setValue(address, point.row, 'i32'); C.setValue(address + SIZE_OF_INT, point.column, 'i32'); } +/** + * @internal + * + * Unmarshals a {@link Point} from the transfer buffer. + */ export function unmarshalPoint(address: number): Point { const result = { row: C.getValue(address, 'i32') >>> 0, @@ -80,6 +116,11 @@ export function unmarshalPoint(address: number): Point { return result; } +/** + * @internal + * + * Marshals a {@link Range} to the transfer buffer. + */ export function marshalRange(address: number, range: Range): void { marshalPoint(address, range.startPosition); address += SIZE_OF_POINT; marshalPoint(address, range.endPosition); address += SIZE_OF_POINT; @@ -87,6 +128,11 @@ export function marshalRange(address: number, range: Range): void { C.setValue(address, range.endIndex, 'i32'); address += SIZE_OF_INT; } +/** + * @internal + * + * Unmarshals a {@link Range} from the transfer buffer. + */ export function unmarshalRange(address: number): Range { const result = {} as Range; result.startPosition = unmarshalPoint(address); address += SIZE_OF_POINT; @@ -96,6 +142,11 @@ export function unmarshalRange(address: number): Range { return result; } +/** + * @internal + * + * Marshals an {@link Edit} to the transfer buffer. + */ export function marshalEdit(edit: Edit, address = TRANSFER_BUFFER) { marshalPoint(address, edit.startPosition); address += SIZE_OF_POINT; marshalPoint(address, edit.oldEndPosition); address += SIZE_OF_POINT; diff --git a/lib/binding_web/src/node.ts b/lib/binding_web/src/node.ts index 740425c6..fd8530e1 100644 --- a/lib/binding_web/src/node.ts +++ b/lib/binding_web/src/node.ts @@ -1,9 +1,12 @@ import { INTERNAL, Internal, assertInternal, Point, Edit, SIZE_OF_INT, SIZE_OF_NODE, SIZE_OF_POINT, ZERO_POINT, isPoint, C } from './constants'; import { getText, Tree } from './tree'; import { TreeCursor } from './tree_cursor'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { Language } from './language'; import { marshalNode, marshalPoint, unmarshalNode, unmarshalPoint } from './marshal'; import { TRANSFER_BUFFER } from './parser'; +/** A single node within a syntax {@link Tree}. */ export class Node { /** @internal */ private [0] = 0; // Internal handle for WASM @@ -14,11 +17,7 @@ export class Node { /** @internal */ private _namedChildren?: (Node | null)[]; - id!: number; - startIndex!: number; - startPosition!: Point; - tree: Tree; - + /** @internal */ constructor( internal: Internal, { @@ -43,107 +42,203 @@ export class Node { this.startPosition = startPosition; } + /** + * The numeric id for this node that is unique. + * + * Within a given syntax tree, no two nodes have the same id. However: + * + * * If a new tree is created based on an older tree, and a node from the old tree is reused in + * the process, then that node will have the same id in both trees. + * + * * A node not marked as having changes does not guarantee it was reused. + * + * * If a node is marked as having changed in the old tree, it will not be reused. + */ + id: number; + + /** The byte index where this node starts. */ + startIndex: number; + + /** The position where this node starts. */ + startPosition: Point; + + /** The tree that this node belongs to. */ + tree: Tree; + + /** Get this node's type as a numerical id. */ get typeId(): number { marshalNode(this); return C._ts_node_symbol_wasm(this.tree[0]); } + /** + * Get the node's type as a numerical id as it appears in the grammar, + * ignoring aliases. + */ get grammarId(): number { marshalNode(this); return C._ts_node_grammar_symbol_wasm(this.tree[0]); } + /** Get this node's type as a string. */ get type(): string { return this.tree.language.types[this.typeId] || 'ERROR'; } + /** + * Get this node's symbol name as it appears in the grammar, ignoring + * aliases as a string. + */ get grammarType(): string { return this.tree.language.types[this.grammarId] || 'ERROR'; } + /** + * Check if this node is *named*. + * + * Named nodes correspond to named rules in the grammar, whereas + * *anonymous* nodes correspond to string literals in the grammar. + */ + get isNamed(): boolean { + marshalNode(this); + return C._ts_node_is_named_wasm(this.tree[0]) === 1; + } + + /** + * Check if this node is *extra*. + * + * Extra nodes represent things like comments, which are not required + * by the grammar, but can appear anywhere. + */ + get isExtra(): boolean { + marshalNode(this); + return C._ts_node_is_extra_wasm(this.tree[0]) === 1; + } + + /** + * Check if this node represents a syntax error. + * + * Syntax errors represent parts of the code that could not be incorporated + * into a valid syntax tree. + */ + get isError(): boolean { + marshalNode(this); + return C._ts_node_is_error_wasm(this.tree[0]) === 1; + } + + /** + * Check if this node is *missing*. + * + * Missing nodes are inserted by the parser in order to recover from + * certain kinds of syntax errors. + */ + get isMissing(): boolean { + marshalNode(this); + return C._ts_node_is_missing_wasm(this.tree[0]) === 1; + } + + /** Check if this node has been edited. */ + get hasChanges(): boolean { + marshalNode(this); + return C._ts_node_has_changes_wasm(this.tree[0]) === 1; + } + + /** + * Check if this node represents a syntax error or contains any syntax + * errors anywhere within it. + */ + get hasError(): boolean { + marshalNode(this); + return C._ts_node_has_error_wasm(this.tree[0]) === 1; + } + + /** Get the byte index where this node ends. */ + get endIndex(): number { + marshalNode(this); + return C._ts_node_end_index_wasm(this.tree[0]); + } + + /** Get the position where this node ends. */ get endPosition(): Point { marshalNode(this); C._ts_node_end_point_wasm(this.tree[0]); return unmarshalPoint(TRANSFER_BUFFER); } - get endIndex(): number { - marshalNode(this); - return C._ts_node_end_index_wasm(this.tree[0]); - } - + /** Get the string content of this node. */ get text(): string { return getText(this.tree, this.startIndex, this.endIndex, this.startPosition); } + /** Get this node's parse state. */ get parseState(): number { marshalNode(this); return C._ts_node_parse_state_wasm(this.tree[0]); } + /** Get the parse state after this node. */ get nextParseState(): number { marshalNode(this); return C._ts_node_next_parse_state_wasm(this.tree[0]); } - get isNamed(): boolean { - marshalNode(this); - return C._ts_node_is_named_wasm(this.tree[0]) === 1; - } - - get hasError(): boolean { - marshalNode(this); - return C._ts_node_has_error_wasm(this.tree[0]) === 1; - } - - get hasChanges(): boolean { - marshalNode(this); - return C._ts_node_has_changes_wasm(this.tree[0]) === 1; - } - - get isError(): boolean { - marshalNode(this); - return C._ts_node_is_error_wasm(this.tree[0]) === 1; - } - - get isMissing(): boolean { - marshalNode(this); - return C._ts_node_is_missing_wasm(this.tree[0]) === 1; - } - - get isExtra(): boolean { - marshalNode(this); - return C._ts_node_is_extra_wasm(this.tree[0]) === 1; - } - + /** Check if this node is equal to another node. */ equals(other: Node): boolean { return this.tree === other.tree && this.id === other.id; } + /** + * Get the node's child at the given index, where zero represents the first child. + * + * This method is fairly fast, but its cost is technically log(n), so if + * you might be iterating over a long list of children, you should use + * {@link Node#children} instead. + */ child(index: number): Node | null { marshalNode(this); C._ts_node_child_wasm(this.tree[0], index); return unmarshalNode(this.tree); } + /** + * Get this node's *named* child at the given index. + * + * See also {@link Node#isNamed}. + * This method is fairly fast, but its cost is technically log(n), so if + * you might be iterating over a long list of children, you should use + * {@link Node#namedChildren} instead. + */ namedChild(index: number): Node | null { marshalNode(this); C._ts_node_named_child_wasm(this.tree[0], index); return unmarshalNode(this.tree); } + /** + * Get this node's child with the given numerical field id. + * + * See also {@link Node#childForFieldName}. You can + * convert a field name to an id using {@link Language#fieldIdForName}. + */ childForFieldId(fieldId: number): Node | null { marshalNode(this); C._ts_node_child_by_field_id_wasm(this.tree[0], fieldId); return unmarshalNode(this.tree); } + /** + * Get the first child with the given field name. + * + * If multiple children may have the same field name, access them using + * {@link Node#childrenForFieldName}. + */ childForFieldName(fieldName: string): Node | null { const fieldId = this.tree.language.fields.indexOf(fieldName); if (fieldId !== -1) return this.childForFieldId(fieldId); return null; } + /** Get the field name of this node's child at the given index. */ fieldNameForChild(index: number): string | null { marshalNode(this); const address = C._ts_node_field_name_for_child_wasm(this.tree[0], index); @@ -151,19 +246,29 @@ export class Node { return C.AsciiToString(address); } + /** Get the field name of this node's named child at the given index. */ fieldNameForNamedChild(index: number): string | null { marshalNode(this); const address = C._ts_node_field_name_for_named_child_wasm(this.tree[0], index); if (!address) return null; return C.AsciiToString(address); } - + /** + * Get an array of this node's children with a given field name. + * + * See also {@link Node#children}. + */ childrenForFieldName(fieldName: string): (Node | null)[] { const fieldId = this.tree.language.fields.indexOf(fieldName); if (fieldId !== -1 && fieldId !== 0) return this.childrenForFieldId(fieldId); return []; } + /** + * Get an array of this node's children with a given field id. + * + * See also {@link Node#childrenForFieldName}. + */ childrenForFieldId(fieldId: number): (Node | null)[] { marshalNode(this); C._ts_node_children_by_field_id_wasm(this.tree[0], fieldId); @@ -182,6 +287,7 @@ export class Node { return result; } + /** Get the node's first child that contains or starts after the given byte offset. */ firstChildForIndex(index: number): Node | null { marshalNode(this); const address = TRANSFER_BUFFER + SIZE_OF_NODE; @@ -190,6 +296,7 @@ export class Node { return unmarshalNode(this.tree); } + /** Get the node's first named child that contains or starts after the given byte offset. */ firstNamedChildForIndex(index: number): Node | null { marshalNode(this); const address = TRANSFER_BUFFER + SIZE_OF_NODE; @@ -198,32 +305,57 @@ export class Node { return unmarshalNode(this.tree); } + /** Get this node's number of children. */ get childCount(): number { marshalNode(this); return C._ts_node_child_count_wasm(this.tree[0]); } + + /** + * Get this node's number of *named* children. + * + * See also {@link Node#isNamed}. + */ get namedChildCount(): number { marshalNode(this); return C._ts_node_named_child_count_wasm(this.tree[0]); } + /** Get this node's first child. */ get firstChild(): Node | null { return this.child(0); } + /** + * Get this node's first named child. + * + * See also {@link Node#isNamed}. + */ get firstNamedChild(): Node | null { return this.namedChild(0); } + /** Get this node's last child. */ get lastChild(): Node | null { return this.child(this.childCount - 1); } + /** + * Get this node's last named child. + * + * See also {@link Node#isNamed}. + */ get lastNamedChild(): Node | null { return this.namedChild(this.namedChildCount - 1); } + /** + * Iterate over this node's children. + * + * If you're walking the tree recursively, you may want to use the + * {@link TreeCursor} APIs directly instead. + */ get children(): (Node | null)[] { if (!this._children) { marshalNode(this); @@ -243,6 +375,11 @@ export class Node { return this._children; } + /** + * Iterate over this node's named children. + * + * See also {@link Node#children}. + */ get namedChildren(): (Node | null)[] { if (!this._namedChildren) { marshalNode(this); @@ -262,6 +399,13 @@ export class Node { return this._namedChildren; } + /** + * Get the descendants of this node that are the given type, or in the given types array. + * + * The types array should contain node type strings, which can be retrieved from {@link Language#types}. + * + * Additionally, a `startPosition` and `endPosition` can be passed in to restrict the search to a byte range. + */ descendantsOfType( types: string | string[], startPosition: Point = ZERO_POINT, @@ -314,41 +458,71 @@ export class Node { return result; } + /** Get this node's next sibling. */ get nextSibling(): Node | null { marshalNode(this); C._ts_node_next_sibling_wasm(this.tree[0]); return unmarshalNode(this.tree); } + /** Get this node's previous sibling. */ get previousSibling(): Node | null { marshalNode(this); C._ts_node_prev_sibling_wasm(this.tree[0]); return unmarshalNode(this.tree); } + /** + * Get this node's next *named* sibling. + * + * See also {@link Node#isNamed}. + */ get nextNamedSibling(): Node | null { marshalNode(this); C._ts_node_next_named_sibling_wasm(this.tree[0]); return unmarshalNode(this.tree); } + /** + * Get this node's previous *named* sibling. + * + * See also {@link Node#isNamed}. + */ get previousNamedSibling(): Node | null { marshalNode(this); C._ts_node_prev_named_sibling_wasm(this.tree[0]); return unmarshalNode(this.tree); } + /** Get the node's number of descendants, including one for the node itself. */ get descendantCount(): number { marshalNode(this); return C._ts_node_descendant_count_wasm(this.tree[0]); } + /** + * Get this node's immediate parent. + * Prefer {@link Node#childWithDescendant} for iterating over this node's ancestors. + */ get parent(): Node | null { marshalNode(this); C._ts_node_parent_wasm(this.tree[0]); return unmarshalNode(this.tree); } + /** + * Get the node that contains `descendant`. + * + * Note that this can return `descendant` itself. + */ + childWithDescendant(descendant: Node): Node | null { + marshalNode(this); + marshalNode(descendant); + C._ts_node_child_with_descendant_wasm(this.tree[0]); + return unmarshalNode(this.tree); + } + + /** Get the smallest node within this node that spans the given byte range. */ descendantForIndex(start: number, end: number = start): Node | null { if (typeof start !== 'number' || typeof end !== 'number') { throw new Error('Arguments must be numbers'); @@ -362,6 +536,7 @@ export class Node { return unmarshalNode(this.tree); } + /** Get the smallest named node within this node that spans the given byte range. */ namedDescendantForIndex(start: number, end: number = start): Node | null { if (typeof start !== 'number' || typeof end !== 'number') { throw new Error('Arguments must be numbers'); @@ -375,6 +550,7 @@ export class Node { return unmarshalNode(this.tree); } + /** Get the smallest node within this node that spans the given point range. */ descendantForPosition(start: Point, end: Point = start) { if (!isPoint(start) || !isPoint(end)) { throw new Error('Arguments must be {row, column} objects'); @@ -388,6 +564,7 @@ export class Node { return unmarshalNode(this.tree); } + /** Get the smallest named node within this node that spans the given point range. */ namedDescendantForPosition(start: Point, end: Point = start) { if (!isPoint(start) || !isPoint(end)) { throw new Error('Arguments must be {row, column} objects'); @@ -401,12 +578,27 @@ export class Node { return unmarshalNode(this.tree); } + /** + * Create a new {@link TreeCursor} starting from this node. + * + * Note that the given node is considered the root of the cursor, + * and the cursor cannot walk outside this node. + */ walk(): TreeCursor { marshalNode(this); C._ts_tree_cursor_new_wasm(this.tree[0]); return new TreeCursor(INTERNAL, this.tree); } + /** + * Edit this node to keep it in-sync with source code that has been edited. + * + * This function is only rarely needed. When you edit a syntax tree with + * the {@link Tree#edit} method, all of the nodes that you retrieve from + * the tree afterward will already reflect the edit. You only need to + * use {@link Node#edit} when you have a specific {@link Node} instance that + * you want to keep and continue to use after an edit. + */ edit(edit: Edit) { if (this.startIndex >= edit.oldEndIndex) { this.startIndex = edit.newEndIndex + (this.startIndex - edit.oldEndIndex); @@ -437,6 +629,7 @@ export class Node { } } + /** Get the S-expression representation of this node. */ toString() { marshalNode(this); const address = C._ts_node_to_string_wasm(this.tree[0]); diff --git a/lib/binding_web/src/parser.ts b/lib/binding_web/src/parser.ts index 15d6d55e..855bc720 100644 --- a/lib/binding_web/src/parser.ts +++ b/lib/binding_web/src/parser.ts @@ -4,21 +4,85 @@ 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; } -// Global variable for transferring data across the FFI boundary +/** + * @internal + * + * Global variable for transferring data across the FFI boundary + */ export let TRANSFER_BUFFER: number; -let VERSION: number; -let MIN_COMPATIBLE_VERSION: 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 @@ -26,26 +90,33 @@ export class Parser { /** @internal */ private [1] = 0; // Internal handle for WASM - /** @internal */ - private language: Language | null = null; - /** @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(); - VERSION = C.getValue(TRANSFER_BUFFER, 'i32'); + 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()`"); @@ -55,6 +126,7 @@ export class Parser { 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]); @@ -62,6 +134,15 @@ export class Parser { 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) { @@ -70,10 +151,10 @@ export class Parser { } else if (language.constructor === Language) { address = language[0]; const version = C._ts_language_version(address); - if (version < MIN_COMPATIBLE_VERSION || VERSION < version) { + if (version < MIN_COMPATIBLE_VERSION || LANGUAGE_VERSION < version) { throw new Error( `Incompatible language version ${version}. ` + - `Compatibility range ${MIN_COMPATIBLE_VERSION} through ${VERSION}.` + `Compatibility range ${MIN_COMPATIBLE_VERSION} through ${LANGUAGE_VERSION}.` ); } this.language = language; @@ -85,15 +166,27 @@ export class Parser { return this; } - getLanguage(): Language | null { - return this.language; - } - + /** + * 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 { + options?: ParseOptions, + ): Tree | null { if (typeof callback === 'string') { C.currentParseCallback = (index: number) => callback.slice(index); } else if (typeof callback === 'function') { @@ -102,7 +195,7 @@ export class Parser { throw new Error('Argument must be a string or a function'); } - if (options.progressCallback) { + if (options?.progressCallback) { C.currentProgressCallback = options.progressCallback; } else { C.currentProgressCallback = null; @@ -118,7 +211,7 @@ export class Parser { let rangeCount = 0; let rangeAddress = 0; - if (options.includedRanges) { + if (options?.includedRanges) { rangeCount = options.includedRanges.length; rangeAddress = C._calloc(rangeCount, SIZE_OF_RANGE); let address = rangeAddress; @@ -140,7 +233,7 @@ export class Parser { C.currentParseCallback = null; C.currentLogCallback = null; C.currentProgressCallback = null; - throw new Error('Parsing failed'); + return null; } if (!this.language) { @@ -154,10 +247,20 @@ export class Parser { 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'); @@ -176,14 +279,31 @@ export class Parser { 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; @@ -195,6 +315,7 @@ export class Parser { return this; } + /** Get the parser's current logger. */ getLogger(): LogCallback | null { return this.logCallback; } diff --git a/lib/binding_web/src/query.ts b/lib/binding_web/src/query.ts index 9560f334..ac89d20d 100644 --- a/lib/binding_web/src/query.ts +++ b/lib/binding_web/src/query.ts @@ -3,36 +3,108 @@ import { Node } from './node'; import { marshalNode, unmarshalCaptures } from './marshal'; import { TRANSFER_BUFFER } from './parser'; +/** + * Options for query execution + */ export interface QueryOptions { + /** The start position of the range to query */ startPosition?: Point; + + /** The end position of the range to query */ endPosition?: Point; + + /** The start index of the range to query */ startIndex?: number; + + /** The end index of the range to query */ endIndex?: number; + + /** + * The maximum number of in-progress matches for this query. + * The limit must be > 0 and <= 65536. + */ matchLimit?: number; + + /** + * The maximum start depth for a query cursor. + * + * This prevents cursors from exploring children nodes at a certain depth. + * Note if a pattern includes many children, then they will still be + * checked. + * + * The zero max start depth value can be used as a special behavior and + * it helps to destructure a subtree by staying on a node and using + * captures for interested parts. Note that the zero max start depth + * only limit a search depth for a pattern's root node but other nodes + * that are parts of the pattern may be searched at any depth what + * defined by the pattern structure. + * + * Set to `null` to remove the maximum start depth. + */ maxStartDepth?: number; + + /** + * The maximum duration in microseconds that query execution should be allowed to + * take before halting. + * + * If query execution takes longer than this, it will halt early, returning an empty array. + */ timeoutMicros?: number; + + /** + * A function that will be called periodically during the execution of the query to check + * if query execution should be cancelled. You can also use this to instrument query execution + * and check where the query is at in the document. The progress callback takes a single argument, + * which is a {@link QueryState} representing the current state of the query. + */ progressCallback?: (state: QueryState) => void; } +/** + * A stateful object that is passed into the progress callback {@link QueryOptions#progressCallback} + * to provide the current state of the query. + */ export interface QueryState { + /** The byte offset in the document that the query is at. */ currentOffset: number; } -export type Properties = Record; +/** A record of key-value pairs associated with a particular pattern in a {@link Query}. */ +export type QueryProperties = Record; -export interface Predicate { +/** + * A predicate that contains an operator and list of operands. + */ +export interface QueryPredicate { + /** The operator of the predicate, like `match?`, `eq?`, `set!`, etc. */ operator: string; + + /** The operands of the predicate, which are either captures or strings. */ operands: PredicateStep[]; } +/** + * A particular {@link Node} that has been captured with a particular name within a + * {@link Query}. + */ export interface QueryCapture { + /** The name of the capture */ name: string; + + /** The captured node */ node: Node; - setProperties?: Properties; - assertedProperties?: Properties; - refutedProperties?: Properties; + + /** The properties for predicates declared with the operator `set!`. */ + setProperties?: QueryProperties; + + /** The properties for predicates declared with the operator `is?`. */ + assertedProperties?: QueryProperties; + + /** The properties for predicates declared with the operator `is-not?`. */ + refutedProperties?: QueryProperties; } +/** A quantifier for captures */ export const CaptureQuantifier = { Zero: 0, ZeroOrOne: 1, @@ -41,20 +113,49 @@ export const CaptureQuantifier = { OneOrMore: 4 } as const; +/** A quantifier for captures */ export type CaptureQuantifier = typeof CaptureQuantifier[keyof typeof CaptureQuantifier]; +/** A match of a {@link Query} to a particular set of {@link Node}s. */ export interface QueryMatch { + /** The index of the pattern that matched. */ pattern: number; + + /** The captures associated with the match. */ captures: QueryCapture[]; - setProperties?: Properties; - assertedProperties?: Properties; - refutedProperties?: Properties; + + /** The properties for predicates declared with the operator `set!`. */ + setProperties?: QueryProperties; + + /** The properties for predicates declared with the operator `is?`. */ + assertedProperties?: QueryProperties; + + /** The properties for predicates declared with the operator `is-not?`. */ + refutedProperties?: QueryProperties; } +/** + * Predicates are represented as a single array of steps. There are two + * types of steps, which correspond to the two legal values for + * the `type` field: + * + * - `capture` - Steps with this type represent names + * of captures. The `name` field is the name of the capture. + * + * - `string` - Steps with this type represent literal + * strings. The `value` field is the string value. + */ export type PredicateStep = - | { type: 'string'; value: string } - | { type: 'capture'; value?: string, name: string }; + | { type: 'capture', name: string } + | { type: 'string', value: string }; +/** + * @internal + * + * A function that checks if a given set of captures matches a particular + * condition. This is used in the built-in `eq?`, `match?`, and `any-of?` + * predicates. + */ export type TextPredicate = (captures: QueryCapture[]) => boolean; export class Query { @@ -67,24 +168,47 @@ export class Query { /** @internal */ private textPredicates: TextPredicate[][]; + /** The names of the captures used in the query. */ readonly captureNames: string[]; + + /** The quantifiers of the captures used in the query. */ readonly captureQuantifiers: CaptureQuantifier[][]; - readonly predicates: Predicate[][]; - readonly setProperties: Properties[]; - readonly assertedProperties: Properties[]; - readonly refutedProperties: Properties[]; + + /** + * The other user-defined predicates associated with the given index. + * + * This includes predicates with operators other than: + * - `match?` + * - `eq?` and `not-eq?` + * - `any-of?` and `not-any-of?` + * - `is?` and `is-not?` + * - `set!` + */ + readonly predicates: QueryPredicate[][]; + + /** The properties for predicates with the operator `set!`. */ + readonly setProperties: QueryProperties[]; + + /** The properties for predicates with the operator `is?`. */ + readonly assertedProperties: QueryProperties[]; + + /** The properties for predicates with the operator `is-not?`. */ + readonly refutedProperties: QueryProperties[]; + + /** The maximum number of in-progress matches for this cursor. */ matchLimit?: number; + /** @internal */ constructor( internal: Internal, address: number, captureNames: string[], captureQuantifiers: CaptureQuantifier[][], textPredicates: TextPredicate[][], - predicates: Predicate[][], - setProperties: Properties[], - assertedProperties: Properties[], - refutedProperties: Properties[], + predicates: QueryPredicate[][], + setProperties: QueryProperties[], + assertedProperties: QueryProperties[], + refutedProperties: QueryProperties[], ) { assertInternal(internal); this[0] = address; @@ -98,11 +222,24 @@ export class Query { this.exceededMatchLimit = false; } + /** Delete the query, freeing its resources. */ delete(): void { C._ts_query_delete(this[0]); this[0] = 0; } + /** + * Iterate over all of the matches in the order that they were found. + * + * Each match contains the index of the pattern that matched, and a list of + * captures. Because multiple patterns can match the same set of nodes, + * one match may contain captures that appear *before* some of the + * captures from a previous match. + * + * @param {Node} node - The node to execute the query on. + * + * @param {QueryOptions} options - Options for query execution. + */ matches( node: Node, options: QueryOptions = {} @@ -187,6 +324,17 @@ export class Query { return result; } + /** + * Iterate over all of the individual captures in the order that they + * appear. + * + * This is useful if you don't care about which pattern matched, and just + * want a single, ordered sequence of captures. + * + * @param {Node} node - The node to execute the query on. + * + * @param {QueryOptions} options - Options for query execution. + */ captures( node: Node, options: QueryOptions = {} @@ -239,10 +387,10 @@ export class Query { const count = C.getValue(TRANSFER_BUFFER, 'i32'); const startAddress = C.getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32'); const didExceedMatchLimit = C.getValue(TRANSFER_BUFFER + 2 * SIZE_OF_INT, 'i32'); - const result: QueryCapture[] = []; + const result = new Array(); this.exceededMatchLimit = Boolean(didExceedMatchLimit); - const captures: QueryCapture[] = []; + const captures = new Array(); let address = startAddress; for (let i = 0; i < count; i++) { const pattern = C.getValue(address, 'i32'); @@ -255,7 +403,7 @@ export class Query { captures.length = captureCount; address = unmarshalCaptures(this, node.tree, address, captures); - if (this.textPredicates[pattern].every((p) => p(captures))) { + if (this.textPredicates[pattern].every(p => p(captures))) { const capture = captures[captureIndex]; const setProperties = this.setProperties[pattern]; capture.setProperties = setProperties; @@ -272,10 +420,17 @@ export class Query { return result; } - predicatesForPattern(patternIndex: number): Predicate[] { + /** Get the predicates for a given pattern. */ + predicatesForPattern(patternIndex: number): QueryPredicate[] { return this.predicates[patternIndex]; } + /** + * Disable a certain capture within a query. + * + * This prevents the capture from being returned in matches, and also + * avoids any resource usage associated with recording the capture. + */ disableCapture(captureName: string): void { const captureNameLength = C.lengthBytesUTF8(captureName); const captureNameAddress = C._malloc(captureNameLength + 1); @@ -284,6 +439,13 @@ export class Query { C._free(captureNameAddress); } + /** + * Disable a certain pattern within a query. + * + * This prevents the pattern from matching, and also avoids any resource + * usage associated with the pattern. This throws an error if the pattern + * index is out of bounds. + */ disablePattern(patternIndex: number): void { if (patternIndex >= this.predicates.length) { throw new Error( @@ -293,10 +455,15 @@ export class Query { C._ts_query_disable_pattern(this[0], patternIndex); } + /** + * Check if, on its last execution, this cursor exceeded its maximum number + * of in-progress matches. + */ didExceedMatchLimit(): boolean { return this.exceededMatchLimit; } + /** Get the byte offset where the given pattern starts in the query's source. */ startIndexForPattern(patternIndex: number): number { if (patternIndex >= this.predicates.length) { throw new Error( @@ -306,6 +473,7 @@ export class Query { return C._ts_query_start_byte_for_pattern(this[0], patternIndex); } + /** Get the byte offset where the given pattern ends in the query's source. */ endIndexForPattern(patternIndex: number): number { if (patternIndex >= this.predicates.length) { throw new Error( @@ -315,14 +483,32 @@ export class Query { return C._ts_query_end_byte_for_pattern(this[0], patternIndex); } - isPatternNonLocal(patternIndex: number): boolean { - return C._ts_query_is_pattern_non_local(this[0], patternIndex) === 1; + /** Get the number of patterns in the query. */ + patternCount(): number { + return C._ts_query_pattern_count(this[0]); } + /** Get the index for a given capture name. */ + captureIndexForName(captureName: string): number { + return this.captureNames.indexOf(captureName); + } + + /** Check if a given pattern within a query has a single root node. */ isPatternRooted(patternIndex: number): boolean { return C._ts_query_is_pattern_rooted(this[0], patternIndex) === 1; } + /** Check if a given pattern within a query has a single root node. */ + isPatternNonLocal(patternIndex: number): boolean { + return C._ts_query_is_pattern_non_local(this[0], patternIndex) === 1; + } + + /** + * Check if a given step in a query is 'definite'. + * + * A query step is 'definite' if its parent pattern will be guaranteed to + * match successfully once it reaches the step. + */ isPatternGuaranteedAtStep(byteIndex: number): boolean { return C._ts_query_is_pattern_guaranteed_at_step(this[0], byteIndex) === 1; } diff --git a/lib/binding_web/src/tree.ts b/lib/binding_web/src/tree.ts index b024d7f9..4146e088 100644 --- a/lib/binding_web/src/tree.ts +++ b/lib/binding_web/src/tree.ts @@ -5,6 +5,7 @@ import { TreeCursor } from './tree_cursor'; import { marshalEdit, marshalPoint, unmarshalNode, unmarshalRange } from './marshal'; import { TRANSFER_BUFFER } from './parser'; +/** @internal */ export function getText(tree: Tree, startIndex: number, endIndex: number, startPosition: Point): string { const length = endIndex - startIndex; let result = tree.textCallback(startIndex, startPosition); @@ -26,13 +27,18 @@ export function getText(tree: Tree, startIndex: number, endIndex: number, startP return result ?? ''; } +/** A tree that represents the syntactic structure of a source code file. */ export class Tree { /** @internal */ private [0] = 0; // Internal handle for WASM + /** @internal */ textCallback: ParseCallback; + + /** The language that was used to parse the syntax tree. */ language: Language; + /** @internal */ constructor(internal: Internal, address: number, language: Language, textCallback: ParseCallback) { assertInternal(internal); this[0] = address; @@ -40,26 +46,28 @@ export class Tree { this.textCallback = textCallback; } + /** Create a shallow copy of the syntax tree. This is very fast. */ copy(): Tree { const address = C._ts_tree_copy(this[0]); return new Tree(INTERNAL, address, this.language, this.textCallback); } + /** Delete the syntax tree, freeing its resources. */ delete(): void { C._ts_tree_delete(this[0]); this[0] = 0; } - edit(edit: Edit): void { - marshalEdit(edit); - C._ts_tree_edit_wasm(this[0]); - } - + /** Get the root node of the syntax tree. */ get rootNode(): Node { C._ts_tree_root_node_wasm(this[0]); return unmarshalNode(this)!; } + /** + * Get the root node of the syntax tree, but with its position shifted + * forward by the given offset. + */ rootNodeWithOffset(offsetBytes: number, offsetExtent: Point): Node { const address = TRANSFER_BUFFER + SIZE_OF_NODE; C.setValue(address, offsetBytes, 'i32'); @@ -68,14 +76,34 @@ export class Tree { return unmarshalNode(this)!; } - getLanguage(): Language { - return this.language; + /** + * Edit the syntax tree to keep it in sync with source code that has been + * edited. + * + * You must describe the edit both in terms of byte offsets and in terms of + * row/column coordinates. + */ + edit(edit: Edit): void { + marshalEdit(edit); + C._ts_tree_edit_wasm(this[0]); } + /** Create a new {@link TreeCursor} starting from the root of the tree. */ walk(): TreeCursor { return this.rootNode.walk(); } + /** + * Compare this old edited syntax tree to a new syntax tree representing + * the same document, returning a sequence of ranges whose syntactic + * structure has changed. + * + * For this to work correctly, this syntax tree must have been edited such + * that its ranges match up to the new tree. Generally, you'll want to + * call this method right after calling one of the [`Parser::parse`] + * functions. Call it on the old tree that was passed to parse, and + * pass the new tree that was returned from `parse`. + */ getChangedRanges(other: Tree): Range[] { if (!(other instanceof Tree)) { throw new TypeError('Argument must be a Tree'); @@ -97,6 +125,7 @@ export class Tree { return result; } + /** Get the included ranges that were used to parse the syntax tree. */ getIncludedRanges(): Range[] { C._ts_tree_included_ranges_wasm(this[0]); const count = C.getValue(TRANSFER_BUFFER, 'i32'); diff --git a/lib/binding_web/src/tree_cursor.ts b/lib/binding_web/src/tree_cursor.ts index b40f74b5..61c93006 100644 --- a/lib/binding_web/src/tree_cursor.ts +++ b/lib/binding_web/src/tree_cursor.ts @@ -4,6 +4,7 @@ import { Node } from './node'; import { TRANSFER_BUFFER } from './parser'; import { getText, Tree } from './tree'; +/** A stateful object for walking a syntax {@link Tree} efficiently. */ export class TreeCursor { /** @internal */ private [0] = 0; // Internal handle for WASM @@ -20,12 +21,14 @@ export class TreeCursor { /** @internal */ private tree: Tree; + /** @internal */ constructor(internal: Internal, tree: Tree) { assertInternal(internal); this.tree = tree; unmarshalTreeCursor(this); } + /** Creates a deep copy of the tree cursor. This allocates new memory. */ copy(): TreeCursor { const copy = new TreeCursor(INTERNAL, this.tree); C._ts_tree_cursor_copy_wasm(this.tree[0]); @@ -33,55 +36,99 @@ export class TreeCursor { return copy; } + /** Delete the tree cursor, freeing its resources. */ delete(): void { marshalTreeCursor(this); C._ts_tree_cursor_delete_wasm(this.tree[0]); this[0] = this[1] = this[2] = 0; } - reset(node: Node): void { - marshalNode(node); - marshalTreeCursor(this, TRANSFER_BUFFER + SIZE_OF_NODE); - C._ts_tree_cursor_reset_wasm(this.tree[0]); - unmarshalTreeCursor(this); + /** Get the tree cursor's current {@link Node}. */ + get currentNode(): Node { + marshalTreeCursor(this); + C._ts_tree_cursor_current_node_wasm(this.tree[0]); + return unmarshalNode(this.tree)!; } - resetTo(cursor: TreeCursor): void { - marshalTreeCursor(this, TRANSFER_BUFFER); - marshalTreeCursor(cursor, TRANSFER_BUFFER + SIZE_OF_CURSOR); - C._ts_tree_cursor_reset_to_wasm(this.tree[0], cursor.tree[0]); - unmarshalTreeCursor(this); + /** + * Get the numerical field id of this tree cursor's current node. + * + * See also {@link TreeCursor#currentFieldName}. + */ + get currentFieldId(): number { + marshalTreeCursor(this); + return C._ts_tree_cursor_current_field_id_wasm(this.tree[0]); } + /** Get the field name of this tree cursor's current node. */ + get currentFieldName(): string | null { + return this.tree.language.fields[this.currentFieldId]; + } + + /** + * Get the depth of the cursor's current node relative to the original + * node that the cursor was constructed with. + */ + get currentDepth(): number { + marshalTreeCursor(this); + return C._ts_tree_cursor_current_depth_wasm(this.tree[0]); + } + + /** + * Get the index of the cursor's current node out of all of the + * descendants of the original node that the cursor was constructed with. + */ + get currentDescendantIndex(): number { + marshalTreeCursor(this); + return C._ts_tree_cursor_current_descendant_index_wasm(this.tree[0]); + } + + /** Get the type of the cursor's current node. */ get nodeType(): string { return this.tree.language.types[this.nodeTypeId] || 'ERROR'; } + /** Get the type id of the cursor's current node. */ get nodeTypeId(): number { marshalTreeCursor(this); return C._ts_tree_cursor_current_node_type_id_wasm(this.tree[0]); } + /** Get the state id of the cursor's current node. */ get nodeStateId(): number { marshalTreeCursor(this); return C._ts_tree_cursor_current_node_state_id_wasm(this.tree[0]); } + /** Get the id of the cursor's current node. */ get nodeId(): number { marshalTreeCursor(this); return C._ts_tree_cursor_current_node_id_wasm(this.tree[0]); } + /** + * Check if the cursor's current node is *named*. + * + * Named nodes correspond to named rules in the grammar, whereas + * *anonymous* nodes correspond to string literals in the grammar. + */ get nodeIsNamed(): boolean { marshalTreeCursor(this); return C._ts_tree_cursor_current_node_is_named_wasm(this.tree[0]) === 1; } + /** + * Check if the cursor's current node is *missing*. + * + * Missing nodes are inserted by the parser in order to recover from + * certain kinds of syntax errors. + */ get nodeIsMissing(): boolean { marshalTreeCursor(this); return C._ts_tree_cursor_current_node_is_missing_wasm(this.tree[0]) === 1; } + /** Get the string content of the cursor's current node. */ get nodeText(): string { marshalTreeCursor(this); const startIndex = C._ts_tree_cursor_start_index_wasm(this.tree[0]); @@ -91,53 +138,38 @@ export class TreeCursor { return getText(this.tree, startIndex, endIndex, startPosition); } + /** Get the start position of the cursor's current node. */ get startPosition(): Point { marshalTreeCursor(this); C._ts_tree_cursor_start_position_wasm(this.tree[0]); return unmarshalPoint(TRANSFER_BUFFER); } + /** Get the end position of the cursor's current node. */ get endPosition(): Point { marshalTreeCursor(this); C._ts_tree_cursor_end_position_wasm(this.tree[0]); return unmarshalPoint(TRANSFER_BUFFER); } + /** Get the start index of the cursor's current node. */ get startIndex(): number { marshalTreeCursor(this); return C._ts_tree_cursor_start_index_wasm(this.tree[0]); } + /** Get the end index of the cursor's current node. */ get endIndex(): number { marshalTreeCursor(this); return C._ts_tree_cursor_end_index_wasm(this.tree[0]); } - get currentNode(): Node { - marshalTreeCursor(this); - C._ts_tree_cursor_current_node_wasm(this.tree[0]); - return unmarshalNode(this.tree)!; - } - - get currentFieldId(): number { - marshalTreeCursor(this); - return C._ts_tree_cursor_current_field_id_wasm(this.tree[0]); - } - - get currentFieldName(): string | null { - return this.tree.language.fields[this.currentFieldId]; - } - - get currentDepth(): number { - marshalTreeCursor(this); - return C._ts_tree_cursor_current_depth_wasm(this.tree[0]); - } - - get currentDescendantIndex(): number { - marshalTreeCursor(this); - return C._ts_tree_cursor_current_descendant_index_wasm(this.tree[0]); - } - + /** + * Move this cursor to the first child of its current node. + * + * This returns `true` if the cursor successfully moved, and returns + * `false` if there were no children. + */ gotoFirstChild(): boolean { marshalTreeCursor(this); const result = C._ts_tree_cursor_goto_first_child_wasm(this.tree[0]); @@ -145,6 +177,16 @@ export class TreeCursor { return result === 1; } + /** + * Move this cursor to the last child of its current node. + * + * This returns `true` if the cursor successfully moved, and returns + * `false` if there were no children. + * + * Note that this function may be slower than + * {@link TreeCursor#gotoFirstChild} because it needs to + * iterate through all the children to compute the child's position. + */ gotoLastChild(): boolean { marshalTreeCursor(this); const result = C._ts_tree_cursor_goto_last_child_wasm(this.tree[0]); @@ -152,6 +194,77 @@ export class TreeCursor { return result === 1; } + /** + * Move this cursor to the parent of its current node. + * + * This returns `true` if the cursor successfully moved, and returns + * `false` if there was no parent node (the cursor was already on the + * root node). + * + * Note that the node the cursor was constructed with is considered the root + * of the cursor, and the cursor cannot walk outside this node. + */ + gotoParent(): boolean { + marshalTreeCursor(this); + const result = C._ts_tree_cursor_goto_parent_wasm(this.tree[0]); + unmarshalTreeCursor(this); + return result === 1; + } + + /** + * Move this cursor to the next sibling of its current node. + * + * This returns `true` if the cursor successfully moved, and returns + * `false` if there was no next sibling node. + * + * Note that the node the cursor was constructed with is considered the root + * of the cursor, and the cursor cannot walk outside this node. + */ + gotoNextSibling(): boolean { + marshalTreeCursor(this); + const result = C._ts_tree_cursor_goto_next_sibling_wasm(this.tree[0]); + unmarshalTreeCursor(this); + return result === 1; + } + + /** + * Move this cursor to the previous sibling of its current node. + * + * This returns `true` if the cursor successfully moved, and returns + * `false` if there was no previous sibling node. + * + * Note that this function may be slower than + * {@link TreeCursor#gotoNextSibling} due to how node + * positions are stored. In the worst case, this will need to iterate + * through all the children up to the previous sibling node to recalculate + * its position. Also note that the node the cursor was constructed with is + * considered the root of the cursor, and the cursor cannot walk outside this node. + */ + gotoPreviousSibling(): boolean { + marshalTreeCursor(this); + const result = C._ts_tree_cursor_goto_previous_sibling_wasm(this.tree[0]); + unmarshalTreeCursor(this); + return result === 1; + } + + /** + * Move the cursor to the node that is the nth descendant of + * the original node that the cursor was constructed with, where + * zero represents the original node itself. + */ + gotoDescendant(goalDescendantIndex: number): void { + marshalTreeCursor(this); + C._ts_tree_cursor_goto_descendant_wasm(this.tree[0], goalDescendantIndex); + unmarshalTreeCursor(this); + } + + /** + * Move this cursor to the first child of its current node that contains or + * starts after the given byte offset. + * + * This returns `true` if the cursor successfully moved to a child node, and returns + * `false` if no such child was found. + */ gotoFirstChildForIndex(goalIndex: number): boolean { marshalTreeCursor(this); C.setValue(TRANSFER_BUFFER + SIZE_OF_CURSOR, goalIndex, 'i32'); @@ -160,6 +273,13 @@ export class TreeCursor { return result === 1; } + /** + * Move this cursor to the first child of its current node that contains or + * starts after the given byte offset. + * + * This returns the index of the child node if one was found, and returns + * `null` if no such child was found. + */ gotoFirstChildForPosition(goalPosition: Point): boolean { marshalTreeCursor(this); marshalPoint(TRANSFER_BUFFER + SIZE_OF_CURSOR, goalPosition); @@ -168,30 +288,27 @@ export class TreeCursor { return result === 1; } - gotoNextSibling(): boolean { - marshalTreeCursor(this); - const result = C._ts_tree_cursor_goto_next_sibling_wasm(this.tree[0]); - unmarshalTreeCursor(this); - return result === 1; - } - - gotoPreviousSibling(): boolean { - marshalTreeCursor(this); - const result = C._ts_tree_cursor_goto_previous_sibling_wasm(this.tree[0]); - unmarshalTreeCursor(this); - return result === 1; - } - - gotoDescendant(goalDescendantIndex: number): void { - marshalTreeCursor(this); - C._ts_tree_cursor_goto_descendant_wasm(this.tree[0], goalDescendantIndex); + /** + * Re-initialize this tree cursor to start at the original node that the + * cursor was constructed with. + */ + reset(node: Node): void { + marshalNode(node); + marshalTreeCursor(this, TRANSFER_BUFFER + SIZE_OF_NODE); + C._ts_tree_cursor_reset_wasm(this.tree[0]); unmarshalTreeCursor(this); } - gotoParent(): boolean { - marshalTreeCursor(this); - const result = C._ts_tree_cursor_goto_parent_wasm(this.tree[0]); + /** + * Re-initialize a tree cursor to the same position as another cursor. + * + * Unlike {@link TreeCursor#reset}, this will not lose parent + * information and allows reusing already created cursors. + */ + resetTo(cursor: TreeCursor): void { + marshalTreeCursor(this, TRANSFER_BUFFER); + marshalTreeCursor(cursor, TRANSFER_BUFFER + SIZE_OF_CURSOR); + C._ts_tree_cursor_reset_to_wasm(this.tree[0], cursor.tree[0]); unmarshalTreeCursor(this); - return result === 1; } } diff --git a/lib/binding_web/test/parser.test.ts b/lib/binding_web/test/parser.test.ts index 1c42abee..04cc1461 100644 --- a/lib/binding_web/test/parser.test.ts +++ b/lib/binding_web/test/parser.test.ts @@ -26,11 +26,11 @@ describe('Parser', () => { describe('.setLanguage', () => { it('allows setting the language to null', () => { - expect(parser.getLanguage()).toBeNull(); + expect(parser.language).toBeNull(); parser.setLanguage(JavaScript); - expect(parser.getLanguage()).toBe(JavaScript); + expect(parser.language).toBe(JavaScript); parser.setLanguage(null); - expect(parser.getLanguage()).toBeNull(); + expect(parser.language).toBeNull(); }); it('throws an exception when the given object is not a tree-sitter language', () => { diff --git a/lib/binding_web/web-tree-sitter.d.ts b/lib/binding_web/web-tree-sitter.d.ts index ec19cdf7..aee86cab 100644 --- a/lib/binding_web/web-tree-sitter.d.ts +++ b/lib/binding_web/web-tree-sitter.d.ts @@ -5,7 +5,9 @@ declare module 'web-tree-sitter' { * Rows and columns are zero-based. */ export interface Point { + /** The zero-based row number. */ row: number; + /** The zero-based column number. */ column: number; } /** @@ -13,20 +15,30 @@ declare module 'web-tree-sitter' { * and of rows and columns. */ export interface Range { + /** The start position of the range. */ startPosition: Point; + /** The end position of the range. */ endPosition: Point; + /** The start index of the range. */ startIndex: number; + /** The end index of the range. */ endIndex: number; } /** * A summary of a change to a text document. */ export interface Edit { + /** The start position of the change. */ startPosition: Point; + /** The end position of the change before the edit. */ oldEndPosition: Point; + /** The end position of the change after the edit. */ newEndPosition: Point; + /** The start index of the change. */ startIndex: number; + /** The end index of the change before the edit. */ oldEndIndex: number; + /** The end index of the change after the edit. */ newEndIndex: number; } /** @@ -36,9 +48,7 @@ declare module 'web-tree-sitter' { /** * A callback that receives the parse state during parsing. */ - export type ProgressCallback = (progress: { - currentOffset: number; - }) => boolean; + export type ProgressCallback = (progress: ParseState) => boolean; /** * A callback for logging messages. * @@ -88,15 +98,33 @@ declare module 'web-tree-sitter' { /** * A stateful object that is passed into the progress callback {@link ParseOptions#progressCallback} * to provide the current state of the parser. - * - * The `currentOffset` property is the byte offset in the document that the parser is at. */ export interface ParseState { + /** The byte offset in the document that the parser is at. */ currentOffset: 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 { /** The parser's current language. */ - private language; + language: Language | null; /** * This must always be called before creating a Parser. * @@ -172,6 +200,10 @@ declare module 'web-tree-sitter' { /** Get the parser's current logger. */ getLogger(): LogCallback | null; } + /** + * An opaque object that defines how to parse a particular language. + * The code for each `Language` is generated by the Tree-sitter CLI. + */ export class Language { /** * A list of all node types in the language. The index of each type in this @@ -183,7 +215,6 @@ declare module 'web-tree-sitter' { * this array is its field id. */ fields: (string | null)[]; - constructor(internal: Internal, address: number); /** * Gets the name of the language. */ @@ -317,6 +348,7 @@ declare module 'web-tree-sitter' { /** Get the included ranges that were used to parse the syntax tree. */ getIncludedRanges(): Range[]; } + /** A single node within a syntax {@link Tree}. */ export class Node { /** * The numeric id for this node that is unique. @@ -749,10 +781,9 @@ declare module 'web-tree-sitter' { /** * A stateful object that is passed into the progress callback {@link QueryOptions#progressCallback} * to provide the current state of the query. - * - * The `currentOffset` property is the byte offset in the document that the query is at. */ export interface QueryState { + /** The byte offset in the document that the query is at. */ currentOffset: number; } /** A record of key-value pairs associated with a particular pattern in a {@link Query}. */ @@ -761,7 +792,9 @@ declare module 'web-tree-sitter' { * A predicate that contains an operator and list of operands. */ export interface QueryPredicate { + /** The operator of the predicate, like `match?`, `eq?`, `set!`, etc. */ operator: string; + /** The operands of the predicate, which are either captures or strings. */ operands: PredicateStep[]; } /** @@ -769,10 +802,15 @@ declare module 'web-tree-sitter' { * {@link Query}. */ export interface QueryCapture { + /** The name of the capture */ name: string; + /** The captured node */ node: Node; + /** The properties for predicates declared with the operator `set!`. */ setProperties?: QueryProperties; + /** The properties for predicates declared with the operator `is?`. */ assertedProperties?: QueryProperties; + /** The properties for predicates declared with the operator `is-not?`. */ refutedProperties?: QueryProperties; } /** A quantifier for captures */ @@ -787,10 +825,15 @@ declare module 'web-tree-sitter' { export type CaptureQuantifier = typeof CaptureQuantifier[keyof typeof CaptureQuantifier]; /** A match of a {@link Query} to a particular set of {@link Node}s. */ export interface QueryMatch { + /** The index of the pattern that matched. */ pattern: number; + /** The captures associated with the match. */ captures: QueryCapture[]; + /** The properties for predicates declared with the operator `set!`. */ setProperties?: QueryProperties; + /** The properties for predicates declared with the operator `is?`. */ assertedProperties?: QueryProperties; + /** The properties for predicates declared with the operator `is-not?`. */ refutedProperties?: QueryProperties; } /** @@ -805,13 +848,12 @@ declare module 'web-tree-sitter' { * strings. The `value` field is the string value. */ export type PredicateStep = { - type: 'string'; - value: string; - } | { type: 'capture'; name: string; + } | { + type: 'string'; + value: string; }; - export type TextPredicate = (captures: QueryCapture[]) => boolean; export class Query { /** The names of the captures used in the query. */ readonly captureNames: string[]; @@ -926,6 +968,12 @@ declare module 'web-tree-sitter' { * `false` otherwise. */ resetState(stateId: number): boolean; + /** + * Returns an iterator that iterates over the symbols of the lookahead iterator. + * + * The iterator will yield the current symbol name as a string for each step + * until there are no more symbols to iterate over. + */ [Symbol.iterator](): Iterator; } diff --git a/lib/binding_web/web-tree-sitter.d.ts.map b/lib/binding_web/web-tree-sitter.d.ts.map index 3863b76c..789cfb9d 100644 --- a/lib/binding_web/web-tree-sitter.d.ts.map +++ b/lib/binding_web/web-tree-sitter.d.ts.map @@ -10,6 +10,8 @@ "LogCallback", "ParseOptions", "ParseState", + "LANGUAGE_VERSION", + "MIN_COMPATIBLE_VERSION", "Parser", "Language", "Tree", @@ -23,7 +25,6 @@ "CaptureQuantifier", "QueryMatch", "PredicateStep", - "TextPredicate", "Query", "LookaheadIterator" ], @@ -47,6 +48,6 @@ null, null ], - "mappings": ";;;;;;mBASiBA,KAAKA;;;;;;;;mBASLC,KAAKA;;;;;;;;;mBAULC,IAAIA;;;;;;;;;;;cAiCTC,aAAaA;;;;cAKbC,gBAAgBA;;;;;;;;cAOhBC,WAAWA;;;;;;;;;;;;kBCxDNC,YAAYA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAqCZC,UAAUA;;;cA4BdC,MAAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cCtENC,QAAQA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cCkBRC,IAAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cCtBJC,IAAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cCDJC,UAAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBCCNC,YAAYA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA4DZC,UAAUA;;;;aAKfC,eAAeA;;;;kBAKVC,cAAcA;;;;;;;;kBASdC,YAAYA;;;;;;;;cAkBjBC,iBAAiBA;;;;;;;;aAAjBA,iBAAiBA;;kBAGZC,UAAUA;;;;;;;;;;;;;;;;;;aAmBfC,aAAaA;;;;;;;aAIbC,aAAaA;cAEZC,KAAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cClILC,iBAAiBA", + "mappings": ";;;;;;mBASiBA,KAAKA;;;;;;;;;;mBAYLC,KAAKA;;;;;;;;;;;;;mBAiBLC,IAAIA;;;;;;;;;;;;;;;;;cA4CTC,aAAaA;;;;cAKbC,gBAAgBA;;;;;;cAOhBC,WAAWA;;;;;;;;;;;;kBC7ENC,YAAYA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAmCZC,UAAUA;;;;;;;;;;;;;YAqBhBC,gBAAgBA;;;;;YAMhBC,sBAAsBA;;;;;cAMpBC,MAAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cCrENC,QAAQA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cCcRC,IAAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cCrBJC,IAAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cCFJC,UAAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBCCNC,YAAYA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA0DZC,UAAUA;;;;;aAMfC,eAAeA;;;;kBAKVC,cAAcA;;;;;;;;;;kBAYdC,YAAYA;;;;;;;;;;;;;cA2BjBC,iBAAiBA;;;;;;;;aAAjBA,iBAAiBA;;kBAGZC,UAAUA;;;;;;;;;;;;;;;;;;;;;;;aA4BfC,aAAaA;;;;;;;cAaZC,KAAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cC7JLC,iBAAiBA", "ignoreList": [] } \ No newline at end of file