import { Internal, assertInternal, Point, ZERO_POINT, SIZE_OF_INT, C } from './constants'; import { Node } from './node'; import { marshalNode, unmarshalCaptures } from './marshal'; import { TRANSFER_BUFFER } from './parser'; // eslint-disable-next-line @typescript-eslint/no-unused-vars let currentQueryProgressCallback: ((percent: number) => void) | null = null; interface QueryOptions { startPosition?: Point; endPosition?: Point; startIndex?: number; endIndex?: number; matchLimit?: number; maxStartDepth?: number; timeoutMicros?: number; progressCallback?: (percent: number) => void; } export type Properties = Record; export interface Predicate { operator: string; operands: PredicateStep[]; } export interface Capture { name: string; node: Node; setProperties?: Properties; assertedProperties?: Properties; refutedProperties?: Properties; } export const CaptureQuantifier = { Zero: 0, ZeroOrOne: 1, ZeroOrMore: 2, One: 3, OneOrMore: 4 } as const; export type CaptureQuantifier = typeof CaptureQuantifier[keyof typeof CaptureQuantifier]; export interface QueryMatch { pattern: number; captures: Capture[]; setProperties?: Properties; assertedProperties?: Properties; refutedProperties?: Properties; } export type PredicateStep = | { type: 'string'; value: string } | { type: 'capture'; value?: string, name: string }; export type TextPredicate = (captures: Capture[]) => boolean; export class Query { private [0]: number; // Internal handle for WASM private exceededMatchLimit: boolean; private textPredicates: TextPredicate[][]; readonly captureNames: string[]; readonly captureQuantifiers: CaptureQuantifier[][]; readonly predicates: Predicate[][]; readonly setProperties: Properties[]; readonly assertedProperties: Properties[]; readonly refutedProperties: Properties[]; matchLimit?: number; constructor( internal: Internal, address: number, captureNames: string[], captureQuantifiers: CaptureQuantifier[][], textPredicates: TextPredicate[][], predicates: Predicate[][], setProperties: Properties[], assertedProperties: Properties[], refutedProperties: Properties[], ) { assertInternal(internal); this[0] = address; this.captureNames = captureNames; this.captureQuantifiers = captureQuantifiers; this.textPredicates = textPredicates; this.predicates = predicates; this.setProperties = setProperties; this.assertedProperties = assertedProperties; this.refutedProperties = refutedProperties; this.exceededMatchLimit = false; } delete(): void { C._ts_query_delete(this[0]); this[0] = 0; } matches( node: Node, options: QueryOptions = {} ): QueryMatch[] { const startPosition = options.startPosition ?? ZERO_POINT; const endPosition = options.endPosition ?? ZERO_POINT; const startIndex = options.startIndex ?? 0; const endIndex = options.endIndex ?? 0; const matchLimit = options.matchLimit ?? 0xFFFFFFFF; const maxStartDepth = options.maxStartDepth ?? 0xFFFFFFFF; const timeoutMicros = options.timeoutMicros ?? 0; const progressCallback = options.progressCallback; if (typeof matchLimit !== 'number') { throw new Error('Arguments must be numbers'); } this.matchLimit = matchLimit; if (endIndex !== 0 && startIndex > endIndex) { throw new Error('`startIndex` cannot be greater than `endIndex`'); } if (endPosition !== ZERO_POINT && ( startPosition.row > endPosition.row || (startPosition.row === endPosition.row && startPosition.column > endPosition.column) )) { throw new Error('`startPosition` cannot be greater than `endPosition`'); } if (progressCallback) { currentQueryProgressCallback = progressCallback; } marshalNode(node); C._ts_query_matches_wasm( this[0], node.tree[0], startPosition.row, startPosition.column, endPosition.row, endPosition.column, startIndex, endIndex, matchLimit, maxStartDepth, timeoutMicros, ); const rawCount = getValue(TRANSFER_BUFFER, 'i32'); const startAddress = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32'); const didExceedMatchLimit = getValue(TRANSFER_BUFFER + 2 * SIZE_OF_INT, 'i32'); const result = new Array(rawCount); this.exceededMatchLimit = Boolean(didExceedMatchLimit); let filteredCount = 0; let address = startAddress; for (let i = 0; i < rawCount; i++) { const pattern = getValue(address, 'i32'); address += SIZE_OF_INT; const captureCount = getValue(address, 'i32'); address += SIZE_OF_INT; const captures = new Array(captureCount); address = unmarshalCaptures(this, node.tree, address, captures); if (this.textPredicates[pattern].every((p) => p(captures))) { result[filteredCount] = { pattern, captures }; const setProperties = this.setProperties[pattern]; result[filteredCount].setProperties = setProperties; const assertedProperties = this.assertedProperties[pattern]; result[filteredCount].assertedProperties = assertedProperties; const refutedProperties = this.refutedProperties[pattern]; result[filteredCount].refutedProperties = refutedProperties; filteredCount++; } } result.length = filteredCount; C._free(startAddress); currentQueryProgressCallback = null; return result; } captures( node: Node, options: QueryOptions = {} ): Capture[] { const startPosition = options.startPosition ?? ZERO_POINT; const endPosition = options.endPosition ?? ZERO_POINT; const startIndex = options.startIndex ?? 0; const endIndex = options.endIndex ?? 0; const matchLimit = options.matchLimit ?? 0xFFFFFFFF; const maxStartDepth = options.maxStartDepth ?? 0xFFFFFFFF; const timeoutMicros = options.timeoutMicros ?? 0; const progressCallback = options.progressCallback; if (typeof matchLimit !== 'number') { throw new Error('Arguments must be numbers'); } this.matchLimit = matchLimit; if (endIndex !== 0 && startIndex > endIndex) { throw new Error('`startIndex` cannot be greater than `endIndex`'); } if (endPosition !== ZERO_POINT && ( startPosition.row > endPosition.row || (startPosition.row === endPosition.row && startPosition.column > endPosition.column) )) { throw new Error('`startPosition` cannot be greater than `endPosition`'); } if (progressCallback) { currentQueryProgressCallback = progressCallback; } marshalNode(node); C._ts_query_captures_wasm( this[0], node.tree[0], startPosition.row, startPosition.column, endPosition.row, endPosition.column, startIndex, endIndex, matchLimit, maxStartDepth, timeoutMicros, ); const count = getValue(TRANSFER_BUFFER, 'i32'); const startAddress = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32'); const didExceedMatchLimit = getValue(TRANSFER_BUFFER + 2 * SIZE_OF_INT, 'i32'); const result: Capture[] = []; this.exceededMatchLimit = Boolean(didExceedMatchLimit); const captures: Capture[] = []; let address = startAddress; for (let i = 0; i < count; i++) { const pattern = getValue(address, 'i32'); address += SIZE_OF_INT; const captureCount = getValue(address, 'i32'); address += SIZE_OF_INT; const captureIndex = getValue(address, 'i32'); address += SIZE_OF_INT; captures.length = captureCount; address = unmarshalCaptures(this, node.tree, address, captures); if (this.textPredicates[pattern].every((p) => p(captures))) { const capture = captures[captureIndex]; const setProperties = this.setProperties[pattern]; capture.setProperties = setProperties; const assertedProperties = this.assertedProperties[pattern]; capture.assertedProperties = assertedProperties; const refutedProperties = this.refutedProperties[pattern]; capture.refutedProperties = refutedProperties; result.push(capture); } } C._free(startAddress); currentQueryProgressCallback = null; return result; } predicatesForPattern(patternIndex: number): Predicate[] { return this.predicates[patternIndex]; } disableCapture(captureName: string): void { const captureNameLength = lengthBytesUTF8(captureName); const captureNameAddress = C._malloc(captureNameLength + 1); stringToUTF8(captureName, captureNameAddress, captureNameLength + 1); C._ts_query_disable_capture(this[0], captureNameAddress, captureNameLength); C._free(captureNameAddress); } disablePattern(patternIndex: number): void { if (patternIndex >= this.predicates.length) { throw new Error( `Pattern index is ${patternIndex} but the pattern count is ${this.predicates.length}` ); } C._ts_query_disable_pattern(this[0], patternIndex); } didExceedMatchLimit(): boolean { return this.exceededMatchLimit; } startIndexForPattern(patternIndex: number): number { if (patternIndex >= this.predicates.length) { throw new Error( `Pattern index is ${patternIndex} but the pattern count is ${this.predicates.length}` ); } return C._ts_query_start_byte_for_pattern(this[0], patternIndex); } endIndexForPattern(patternIndex: number): number { if (patternIndex >= this.predicates.length) { throw new Error( `Pattern index is ${patternIndex} but the pattern count is ${this.predicates.length}` ); } 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; } isPatternRooted(patternIndex: number): boolean { return C._ts_query_is_pattern_rooted(this[0], patternIndex) === 1; } isPatternGuaranteedAtStep(patternIndex: number, stepIndex: number): boolean { return C._ts_query_is_pattern_guaranteed_at_step( this[0], patternIndex, stepIndex ) === 1; } }