feat(web)!: rewrite the library in TypeScript
This commit is contained in:
parent
07a86b1729
commit
2cae67892e
39 changed files with 7856 additions and 3629 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -12,7 +12,6 @@ test/fuzz/out
|
|||
test/fixtures/grammars/*
|
||||
!test/fixtures/grammars/.gitkeep
|
||||
|
||||
package-lock.json
|
||||
node_modules
|
||||
|
||||
docs/assets/js/tree-sitter.js
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
module.exports = {
|
||||
'env': {
|
||||
'commonjs': true,
|
||||
'es2021': true,
|
||||
},
|
||||
'extends': 'google',
|
||||
'overrides': [
|
||||
],
|
||||
'parserOptions': {
|
||||
'ecmaVersion': 'latest',
|
||||
'sourceType': 'module',
|
||||
},
|
||||
'rules': {
|
||||
'indent': ['error', 2, {'SwitchCase': 1}],
|
||||
'max-len': [
|
||||
'error',
|
||||
{'code': 120, 'ignoreComments': true, 'ignoreUrls': true, 'ignoreStrings': true, 'ignoreTemplateLiterals': true},
|
||||
],
|
||||
'require-jsdoc': 0,
|
||||
'new-cap': 0,
|
||||
},
|
||||
};
|
||||
4
lib/binding_web/.gitignore
vendored
4
lib/binding_web/.gitignore
vendored
|
|
@ -1,6 +1,8 @@
|
|||
dist/
|
||||
/tree-sitter.js
|
||||
/tree-sitter.js.map
|
||||
/tree-sitter.wasm
|
||||
package-lock.json
|
||||
/tree-sitter.wasm.map
|
||||
node_modules
|
||||
*.tgz
|
||||
LICENSE
|
||||
|
|
|
|||
|
|
@ -50,6 +50,14 @@ await Parser.init();
|
|||
// the library is ready
|
||||
```
|
||||
|
||||
To install a debug version of the library, pass in `--debug` when running `npm install`:
|
||||
|
||||
```sh
|
||||
npm install web-tree-sitter --debug
|
||||
```
|
||||
|
||||
This will load the debug version of the `.wasm` file, which includes sourcemaps for both the JS and WASM files, debug symbols, and assertions.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
First, create a parser:
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -16,10 +16,16 @@ const void *TRANSFER_BUFFER[12] = {
|
|||
NULL, NULL, NULL, NULL,
|
||||
};
|
||||
|
||||
static const int SIZE_OF_CURSOR = 4;
|
||||
static const int SIZE_OF_NODE = 5;
|
||||
static const int SIZE_OF_POINT = 2;
|
||||
static const int SIZE_OF_RANGE = 2 + (2 * SIZE_OF_POINT);
|
||||
static const int SIZE_OF_CAPTURE = 1 + SIZE_OF_NODE;
|
||||
|
||||
void *ts_init() {
|
||||
TRANSFER_BUFFER[0] = (const void *)TREE_SITTER_LANGUAGE_VERSION;
|
||||
TRANSFER_BUFFER[1] = (const void *)TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION;
|
||||
return TRANSFER_BUFFER;
|
||||
return (void*)TRANSFER_BUFFER;
|
||||
}
|
||||
|
||||
static uint32_t code_unit_to_byte(uint32_t unit) {
|
||||
|
|
@ -95,9 +101,9 @@ static void unmarshal_range(TSRange *range) {
|
|||
static TSInputEdit unmarshal_edit() {
|
||||
TSInputEdit edit;
|
||||
const void **address = TRANSFER_BUFFER;
|
||||
edit.start_point = unmarshal_point(address); address += 2;
|
||||
edit.old_end_point = unmarshal_point(address); address += 2;
|
||||
edit.new_end_point = unmarshal_point(address); address += 2;
|
||||
edit.start_point = unmarshal_point(address); address += SIZE_OF_POINT;
|
||||
edit.old_end_point = unmarshal_point(address); address += SIZE_OF_POINT;
|
||||
edit.new_end_point = unmarshal_point(address); address += SIZE_OF_POINT;
|
||||
edit.start_byte = code_unit_to_byte((uint32_t)*address); address += 1;
|
||||
edit.old_end_byte = code_unit_to_byte((uint32_t)*address); address += 1;
|
||||
edit.new_end_byte = code_unit_to_byte((uint32_t)*address); address += 1;
|
||||
|
|
@ -260,7 +266,7 @@ void ts_tree_root_node_wasm(const TSTree *tree) {
|
|||
|
||||
void ts_tree_root_node_with_offset_wasm(const TSTree *tree) {
|
||||
// read int and point from transfer buffer
|
||||
const void **address = TRANSFER_BUFFER + 5;
|
||||
const void **address = TRANSFER_BUFFER + SIZE_OF_NODE;
|
||||
uint32_t offset = code_unit_to_byte((uint32_t)address[0]);
|
||||
TSPoint extent = unmarshal_point(address + 1);
|
||||
TSNode node = ts_tree_root_node_with_offset(tree, offset, extent);
|
||||
|
|
@ -315,14 +321,14 @@ void ts_tree_cursor_delete_wasm(const TSTree *tree) {
|
|||
|
||||
void ts_tree_cursor_reset_wasm(const TSTree *tree) {
|
||||
TSNode node = unmarshal_node(tree);
|
||||
TSTreeCursor cursor = unmarshal_cursor(&TRANSFER_BUFFER[5], tree);
|
||||
TSTreeCursor cursor = unmarshal_cursor(&TRANSFER_BUFFER[SIZE_OF_NODE], tree);
|
||||
ts_tree_cursor_reset(&cursor, node);
|
||||
marshal_cursor(&cursor);
|
||||
}
|
||||
|
||||
void ts_tree_cursor_reset_to_wasm(const TSTree *_dst, const TSTree *_src) {
|
||||
TSTreeCursor cursor = unmarshal_cursor(TRANSFER_BUFFER, _dst);
|
||||
TSTreeCursor src = unmarshal_cursor(&TRANSFER_BUFFER[4], _src);
|
||||
TSTreeCursor src = unmarshal_cursor(&TRANSFER_BUFFER[SIZE_OF_CURSOR], _src);
|
||||
ts_tree_cursor_reset_to(&cursor, &src);
|
||||
marshal_cursor(&cursor);
|
||||
}
|
||||
|
|
@ -508,25 +514,25 @@ void ts_node_children_by_field_id_wasm(const TSTree *tree, uint32_t field_id) {
|
|||
if (!ts_tree_cursor_goto_next_sibling(&cursor)) {
|
||||
done = true;
|
||||
}
|
||||
array_grow_by(&result, 5);
|
||||
marshal_node(result.contents + result.size - 5, result_node);
|
||||
array_grow_by(&result, SIZE_OF_NODE);
|
||||
marshal_node(result.contents + result.size - SIZE_OF_NODE, result_node);
|
||||
}
|
||||
ts_tree_cursor_delete(&cursor);
|
||||
|
||||
TRANSFER_BUFFER[0] = (const void*)(result.size / 5);
|
||||
TRANSFER_BUFFER[1] = result.contents;
|
||||
TRANSFER_BUFFER[0] = (const void*)(result.size / SIZE_OF_NODE);
|
||||
TRANSFER_BUFFER[1] = (const void*)result.contents;
|
||||
}
|
||||
|
||||
void ts_node_first_child_for_byte_wasm(const TSTree *tree) {
|
||||
TSNode node = unmarshal_node(tree);
|
||||
const void** address = TRANSFER_BUFFER + 5;
|
||||
const void** address = TRANSFER_BUFFER + SIZE_OF_NODE;
|
||||
uint32_t byte = code_unit_to_byte((uint32_t)address[0]);
|
||||
marshal_node(TRANSFER_BUFFER, ts_node_first_child_for_byte(node, byte));
|
||||
}
|
||||
|
||||
void ts_node_first_named_child_for_byte_wasm(const TSTree *tree) {
|
||||
TSNode node = unmarshal_node(tree);
|
||||
const void** address = TRANSFER_BUFFER + 5;
|
||||
const void** address = TRANSFER_BUFFER + SIZE_OF_NODE;
|
||||
uint32_t byte = code_unit_to_byte((uint32_t)address[0]);
|
||||
marshal_node(TRANSFER_BUFFER, ts_node_first_named_child_for_byte(node, byte));
|
||||
}
|
||||
|
|
@ -593,7 +599,7 @@ void ts_node_parent_wasm(const TSTree *tree) {
|
|||
|
||||
void ts_node_descendant_for_index_wasm(const TSTree *tree) {
|
||||
TSNode node = unmarshal_node(tree);
|
||||
const void **address = TRANSFER_BUFFER + 5;
|
||||
const void **address = TRANSFER_BUFFER + SIZE_OF_NODE;
|
||||
uint32_t start = code_unit_to_byte((uint32_t)address[0]);
|
||||
uint32_t end = code_unit_to_byte((uint32_t)address[1]);
|
||||
marshal_node(TRANSFER_BUFFER, ts_node_descendant_for_byte_range(node, start, end));
|
||||
|
|
@ -601,7 +607,7 @@ void ts_node_descendant_for_index_wasm(const TSTree *tree) {
|
|||
|
||||
void ts_node_named_descendant_for_index_wasm(const TSTree *tree) {
|
||||
TSNode node = unmarshal_node(tree);
|
||||
const void **address = TRANSFER_BUFFER + 5;
|
||||
const void **address = TRANSFER_BUFFER + SIZE_OF_NODE;
|
||||
uint32_t start = code_unit_to_byte((uint32_t)address[0]);
|
||||
uint32_t end = code_unit_to_byte((uint32_t)address[1]);
|
||||
marshal_node(TRANSFER_BUFFER, ts_node_named_descendant_for_byte_range(node, start, end));
|
||||
|
|
@ -609,16 +615,16 @@ void ts_node_named_descendant_for_index_wasm(const TSTree *tree) {
|
|||
|
||||
void ts_node_descendant_for_position_wasm(const TSTree *tree) {
|
||||
TSNode node = unmarshal_node(tree);
|
||||
const void **address = TRANSFER_BUFFER + 5;
|
||||
TSPoint start = unmarshal_point(address); address += 2;
|
||||
const void **address = TRANSFER_BUFFER + SIZE_OF_NODE;
|
||||
TSPoint start = unmarshal_point(address); address += SIZE_OF_POINT;
|
||||
TSPoint end = unmarshal_point(address);
|
||||
marshal_node(TRANSFER_BUFFER, ts_node_descendant_for_point_range(node, start, end));
|
||||
}
|
||||
|
||||
void ts_node_named_descendant_for_position_wasm(const TSTree *tree) {
|
||||
TSNode node = unmarshal_node(tree);
|
||||
const void **address = TRANSFER_BUFFER + 5;
|
||||
TSPoint start = unmarshal_point(address); address += 2;
|
||||
const void **address = TRANSFER_BUFFER + SIZE_OF_NODE;
|
||||
TSPoint start = unmarshal_point(address); address += SIZE_OF_POINT;
|
||||
TSPoint end = unmarshal_point(address);
|
||||
marshal_node(TRANSFER_BUFFER, ts_node_named_descendant_for_point_range(node, start, end));
|
||||
}
|
||||
|
|
@ -653,20 +659,20 @@ void ts_node_children_wasm(const TSTree *tree) {
|
|||
uint32_t count = ts_node_child_count(node);
|
||||
const void **result = NULL;
|
||||
if (count > 0) {
|
||||
result = calloc(sizeof(void *), 5 * count);
|
||||
result = (const void**)calloc(sizeof(void *), SIZE_OF_NODE * count);
|
||||
const void **address = result;
|
||||
ts_tree_cursor_reset(&scratch_cursor, node);
|
||||
ts_tree_cursor_goto_first_child(&scratch_cursor);
|
||||
marshal_node(address, ts_tree_cursor_current_node(&scratch_cursor));
|
||||
for (uint32_t i = 1; i < count; i++) {
|
||||
address += 5;
|
||||
address += SIZE_OF_NODE;
|
||||
ts_tree_cursor_goto_next_sibling(&scratch_cursor);
|
||||
TSNode child = ts_tree_cursor_current_node(&scratch_cursor);
|
||||
marshal_node(address, child);
|
||||
}
|
||||
}
|
||||
TRANSFER_BUFFER[0] = (const void *)count;
|
||||
TRANSFER_BUFFER[1] = result;
|
||||
TRANSFER_BUFFER[1] = (const void *)result;
|
||||
}
|
||||
|
||||
void ts_node_named_children_wasm(const TSTree *tree) {
|
||||
|
|
@ -674,7 +680,7 @@ void ts_node_named_children_wasm(const TSTree *tree) {
|
|||
uint32_t count = ts_node_named_child_count(node);
|
||||
const void **result = NULL;
|
||||
if (count > 0) {
|
||||
result = calloc(sizeof(void *), 5 * count);
|
||||
result = (const void**)calloc(sizeof(void *), SIZE_OF_NODE * count);
|
||||
const void **address = result;
|
||||
ts_tree_cursor_reset(&scratch_cursor, node);
|
||||
ts_tree_cursor_goto_first_child(&scratch_cursor);
|
||||
|
|
@ -683,7 +689,7 @@ void ts_node_named_children_wasm(const TSTree *tree) {
|
|||
TSNode child = ts_tree_cursor_current_node(&scratch_cursor);
|
||||
if (ts_node_is_named(child)) {
|
||||
marshal_node(address, child);
|
||||
address += 5;
|
||||
address += SIZE_OF_NODE;
|
||||
i++;
|
||||
if (i == count) {
|
||||
break;
|
||||
|
|
@ -695,7 +701,7 @@ void ts_node_named_children_wasm(const TSTree *tree) {
|
|||
}
|
||||
}
|
||||
TRANSFER_BUFFER[0] = (const void *)count;
|
||||
TRANSFER_BUFFER[1] = result;
|
||||
TRANSFER_BUFFER[1] = (const void *)result;
|
||||
}
|
||||
|
||||
bool symbols_contain(const uint32_t *set, uint32_t length, uint32_t value) {
|
||||
|
|
@ -757,8 +763,8 @@ void ts_node_descendants_of_type_wasm(
|
|||
// Add the node to the result if its type matches one of the given
|
||||
// node types.
|
||||
if (symbols_contain(symbols, symbol_count, ts_node_symbol(descendant))) {
|
||||
array_grow_by(&result, 5);
|
||||
marshal_node(result.contents + result.size - 5, descendant);
|
||||
array_grow_by(&result, SIZE_OF_NODE);
|
||||
marshal_node(result.contents + result.size - SIZE_OF_NODE, descendant);
|
||||
}
|
||||
|
||||
// Continue walking.
|
||||
|
|
@ -783,8 +789,8 @@ void ts_node_descendants_of_type_wasm(
|
|||
}
|
||||
}
|
||||
|
||||
TRANSFER_BUFFER[0] = (const void *)(result.size / 5);
|
||||
TRANSFER_BUFFER[1] = result.contents;
|
||||
TRANSFER_BUFFER[0] = (const void *)(result.size / SIZE_OF_NODE);
|
||||
TRANSFER_BUFFER[1] = (const void *)result.contents;
|
||||
}
|
||||
|
||||
int ts_node_is_named_wasm(const TSTree *tree) {
|
||||
|
|
@ -873,21 +879,21 @@ void ts_query_matches_wasm(
|
|||
TSQueryMatch match;
|
||||
while (ts_query_cursor_next_match(scratch_query_cursor, &match)) {
|
||||
match_count++;
|
||||
array_grow_by(&result, 2 + 6 * match.capture_count);
|
||||
array_grow_by(&result, 2 + (SIZE_OF_CAPTURE * match.capture_count));
|
||||
result.contents[index++] = (const void *)(uint32_t)match.pattern_index;
|
||||
result.contents[index++] = (const void *)(uint32_t)match.capture_count;
|
||||
for (unsigned i = 0; i < match.capture_count; i++) {
|
||||
const TSQueryCapture *capture = &match.captures[i];
|
||||
result.contents[index++] = (const void *)capture->index;
|
||||
marshal_node(result.contents + index, capture->node);
|
||||
index += 5;
|
||||
index += SIZE_OF_NODE;
|
||||
}
|
||||
}
|
||||
|
||||
bool did_exceed_match_limit =
|
||||
ts_query_cursor_did_exceed_match_limit(scratch_query_cursor);
|
||||
TRANSFER_BUFFER[0] = (const void *)(match_count);
|
||||
TRANSFER_BUFFER[1] = result.contents;
|
||||
TRANSFER_BUFFER[1] = (const void *)result.contents;
|
||||
TRANSFER_BUFFER[2] = (const void *)(did_exceed_match_limit);
|
||||
}
|
||||
|
||||
|
|
@ -933,7 +939,7 @@ void ts_query_captures_wasm(
|
|||
)) {
|
||||
capture_count++;
|
||||
|
||||
array_grow_by(&result, 3 + 6 * match.capture_count);
|
||||
array_grow_by(&result, 3 + (SIZE_OF_CAPTURE * match.capture_count));
|
||||
result.contents[index++] = (const void *)(uint32_t)match.pattern_index;
|
||||
result.contents[index++] = (const void *)(uint32_t)match.capture_count;
|
||||
result.contents[index++] = (const void *)capture_index;
|
||||
|
|
@ -941,13 +947,13 @@ void ts_query_captures_wasm(
|
|||
const TSQueryCapture *capture = &match.captures[i];
|
||||
result.contents[index++] = (const void *)capture->index;
|
||||
marshal_node(result.contents + index, capture->node);
|
||||
index += 5;
|
||||
index += SIZE_OF_NODE;
|
||||
}
|
||||
}
|
||||
|
||||
bool did_exceed_match_limit =
|
||||
ts_query_cursor_did_exceed_match_limit(scratch_query_cursor);
|
||||
TRANSFER_BUFFER[0] = (const void *)(capture_count);
|
||||
TRANSFER_BUFFER[1] = result.contents;
|
||||
TRANSFER_BUFFER[1] = (const void *)result.contents;
|
||||
TRANSFER_BUFFER[2] = (const void *)(did_exceed_match_limit);
|
||||
}
|
||||
3711
lib/binding_web/package-lock.json
generated
Normal file
3711
lib/binding_web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -2,41 +2,56 @@
|
|||
"name": "web-tree-sitter",
|
||||
"version": "0.25.0",
|
||||
"description": "Tree-sitter bindings for the web",
|
||||
"main": "tree-sitter.js",
|
||||
"types": "tree-sitter-web.d.ts",
|
||||
"directories": {
|
||||
"test": "test"
|
||||
"repository": "https://github.com/tree-sitter/tree-sitter",
|
||||
"homepage": "https://github.com/tree-sitter/tree-sitter/tree/master/lib/binding_web",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Max Brunsfeld",
|
||||
"email": "maxbrunsfeld@gmail.com"
|
||||
},
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Amaan Qureshi",
|
||||
"email": "amaanq12@gmail.com"
|
||||
}
|
||||
],
|
||||
"main": "tree-sitter.js",
|
||||
"type": "module",
|
||||
"types": "tree-sitter-web.d.ts",
|
||||
"keywords": [
|
||||
"incremental",
|
||||
"parsing",
|
||||
"tree-sitter",
|
||||
"wasm"
|
||||
],
|
||||
"files": [
|
||||
"README.md",
|
||||
"tree-sitter.js",
|
||||
"tree-sitter.js.map",
|
||||
"tree-sitter.wasm",
|
||||
"tree-sitter.wasm.map",
|
||||
"tree-sitter-web.d.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "mocha",
|
||||
"prepack": "cp ../../LICENSE .",
|
||||
"prepublishOnly": "node check-artifacts-fresh.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tree-sitter/tree-sitter.git"
|
||||
},
|
||||
"keywords": [
|
||||
"incremental",
|
||||
"parsing"
|
||||
],
|
||||
"author": "Max Brunsfeld",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tree-sitter/tree-sitter/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tree-sitter/tree-sitter/tree/master/lib/binding_web",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.39.10",
|
||||
"chai": "^4.3.7",
|
||||
"eslint": ">=8.56.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"mocha": "^10.2.0"
|
||||
"@types/emscripten": "^1.39.13",
|
||||
"@types/node": "^22.10.7",
|
||||
"@vitest/coverage-v8": "^2.1.8",
|
||||
"esbuild": "^0.24.2",
|
||||
"eslint": ">=9.18.0",
|
||||
"source-map": "^0.7.4",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"scripts": {
|
||||
"build:ts": "esbuild src/index.ts --bundle --platform=neutral --format=cjs --global-name=TreeSitterImpl --outfile=dist/tree-sitter.js --external:fs/* --external:fs/promises --sourcemap --sources-content=true --keep-names",
|
||||
"build:wasm": "cd ../../ && cargo xtask build-wasm",
|
||||
"build:sourcemap": "node script/build-sourcemap.js",
|
||||
"build": "npm run build:ts && npm run build:wasm && npm run build:sourcemap",
|
||||
"build:debug": "npm run build:ts && npm run build:wasm:debug && mkdir -p debug && mv tree-sitter.* debug/",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"prepack": "cp ../../LICENSE .",
|
||||
"prepublishOnly": "node script/check-artifacts-fresh.js",
|
||||
"postinstall": "node scripts/postinstall.js"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
62
lib/binding_web/script/build-sourcemap.js
Normal file
62
lib/binding_web/script/build-sourcemap.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { SourceMapGenerator, SourceMapConsumer } from 'source-map';
|
||||
|
||||
async function fixSourceMap() {
|
||||
const distMap = JSON.parse(readFileSync('dist/tree-sitter.js.map', 'utf8'));
|
||||
const distJs = readFileSync('dist/tree-sitter.js', 'utf8').split('\n');
|
||||
const finalJs = readFileSync('tree-sitter.js', 'utf8').split('\n');
|
||||
|
||||
const lineMap = new Map();
|
||||
|
||||
for (let distLine = 0; distLine < distJs.length; distLine++) {
|
||||
const line = distJs[distLine].trim();
|
||||
if (!line) continue;
|
||||
|
||||
for (let finalLine = 0; finalLine < finalJs.length; finalLine++) {
|
||||
if (finalJs[finalLine].trim() === line) {
|
||||
lineMap.set(distLine + 1, finalLine + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const consumer = await new SourceMapConsumer(distMap);
|
||||
const generator = new SourceMapGenerator({
|
||||
file: 'tree-sitter.js',
|
||||
sourceRoot: ''
|
||||
});
|
||||
|
||||
consumer.eachMapping(mapping => {
|
||||
const finalLine = lineMap.get(mapping.generatedLine);
|
||||
if (finalLine) {
|
||||
generator.addMapping({
|
||||
generated: {
|
||||
line: finalLine,
|
||||
column: mapping.generatedColumn
|
||||
},
|
||||
original: {
|
||||
line: mapping.originalLine,
|
||||
column: mapping.originalColumn
|
||||
},
|
||||
// Fix the source path to be relative to binding_web
|
||||
source: `src/${mapping.source.split('/').pop()}`,
|
||||
name: mapping.name
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
for (const source of consumer.sources) {
|
||||
const content = consumer.sourceContentFor(source);
|
||||
if (content) {
|
||||
generator.setSourceContent(
|
||||
`src/${source.split('/').pop()}`,
|
||||
content
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
consumer.destroy();
|
||||
writeFileSync('tree-sitter.js.map', generator.toString());
|
||||
}
|
||||
|
||||
fixSourceMap().catch(console.error);
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const inputFiles = [
|
||||
'binding.c',
|
||||
'lib/binding.c',
|
||||
'binding.js',
|
||||
'exports.txt',
|
||||
'imports.js',
|
||||
'wasm/exports.txt',
|
||||
'wasm/imports.js',
|
||||
'prefix.js',
|
||||
...list('../include/tree_sitter'),
|
||||
...list('../src'),
|
||||
25
lib/binding_web/script/postinstall.js
Normal file
25
lib/binding_web/script/postinstall.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const isDebug = process.env.npm_config_web_tree_sitter_debug === 'true';
|
||||
|
||||
if (isDebug) {
|
||||
// Copy debug versions to root
|
||||
fs.copyFileSync(
|
||||
path.join(__dirname, '../debug/tree-sitter.js'),
|
||||
path.join(__dirname, '../tree-sitter.js')
|
||||
);
|
||||
fs.copyFileSync(
|
||||
path.join(__dirname, '../debug/tree-sitter.wasm'),
|
||||
path.join(__dirname, '../tree-sitter.wasm')
|
||||
);
|
||||
// Copy sourcemaps too
|
||||
fs.copyFileSync(
|
||||
path.join(__dirname, '../debug/tree-sitter.js.map'),
|
||||
path.join(__dirname, '../tree-sitter.js.map')
|
||||
);
|
||||
fs.copyFileSync(
|
||||
path.join(__dirname, '../debug/tree-sitter.wasm.map'),
|
||||
path.join(__dirname, '../tree-sitter.wasm.map')
|
||||
);
|
||||
}
|
||||
238
lib/binding_web/src/constants.ts
Normal file
238
lib/binding_web/src/constants.ts
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
import { CaptureQuantifier } from "./query";
|
||||
|
||||
export interface Point {
|
||||
row: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
export interface Range {
|
||||
startPosition: Point;
|
||||
endPosition: Point;
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
}
|
||||
|
||||
export interface Edit {
|
||||
startPosition: Point;
|
||||
oldEndPosition: Point;
|
||||
newEndPosition: Point;
|
||||
startIndex: number;
|
||||
oldEndIndex: number;
|
||||
newEndIndex: number;
|
||||
}
|
||||
|
||||
export interface ParserOptions {
|
||||
includedRanges?: Range[];
|
||||
progressCallback?: (progress: { currentOffset: number }) => boolean;
|
||||
}
|
||||
|
||||
export const SIZE_OF_SHORT = 2;
|
||||
export const SIZE_OF_INT = 4;
|
||||
export const SIZE_OF_CURSOR = 4 * SIZE_OF_INT;
|
||||
export const SIZE_OF_NODE = 5 * SIZE_OF_INT;
|
||||
export const SIZE_OF_POINT = 2 * SIZE_OF_INT;
|
||||
export const SIZE_OF_RANGE = 2 * SIZE_OF_INT + 2 * SIZE_OF_POINT;
|
||||
export const ZERO_POINT: Point = { row: 0, column: 0 };
|
||||
|
||||
// Types for callbacks
|
||||
export type ParseCallback = (index: number, position: Point) => string | null;
|
||||
export type ProgressCallback = (progress: { currentOffset: number }) => boolean;
|
||||
export type LogCallback = (message: string, isLex: boolean) => void;
|
||||
|
||||
// Helper type for internal use
|
||||
export const INTERNAL = Symbol('INTERNAL');
|
||||
export type Internal = typeof INTERNAL;
|
||||
|
||||
// Helper functions for type checking
|
||||
export function assertInternal(x: unknown): asserts x is Internal {
|
||||
if (x !== INTERNAL) throw new Error('Illegal constructor');
|
||||
}
|
||||
|
||||
export function isPoint(point: Point): point is Point {
|
||||
return (
|
||||
!!point &&
|
||||
typeof (point as Point).row === 'number' &&
|
||||
typeof (point as Point).column === 'number'
|
||||
);
|
||||
}
|
||||
|
||||
export const C: EmscriptenModule & {
|
||||
// Global
|
||||
_ts_init(): number;
|
||||
|
||||
// Libc
|
||||
_malloc(size: number): number;
|
||||
_calloc(count: number, size: number): number;
|
||||
_free(ptr: number): void;
|
||||
|
||||
// Parser
|
||||
_ts_parser_new_wasm(): void;
|
||||
_ts_parser_delete(address: number): void;
|
||||
_ts_parser_set_language(parserAddress: number, languageAddress: number): void;
|
||||
_ts_language_version(address: number): number;
|
||||
_ts_parser_enable_logger_wasm(address: number, enabled: number): void;
|
||||
_ts_parser_parse_wasm(
|
||||
address: number,
|
||||
payload: number,
|
||||
oldTreeAddress: number,
|
||||
rangeAddress: number,
|
||||
rangeCount: number
|
||||
): number;
|
||||
_ts_parser_reset(address: number): void;
|
||||
_ts_parser_timeout_micros(address: number): number;
|
||||
_ts_parser_set_timeout_micros(address: number, timeout: number): void;
|
||||
_ts_parser_included_ranges_wasm(address: number): void;
|
||||
|
||||
// Language
|
||||
_ts_language_symbol_count(address: number): number;
|
||||
_ts_language_symbol_name(address: number, index: number): number;
|
||||
_ts_language_symbol_type(address: number, index: number): number;
|
||||
_ts_language_field_count(address: number): number;
|
||||
_ts_language_field_name_for_id(address: number, id: number): number;
|
||||
_ts_language_name(address: number): number;
|
||||
_ts_language_version(address: number): number;
|
||||
_ts_language_state_count(address: number): number;
|
||||
_ts_language_symbol_for_name(address: number, typeAddress: number, typeLength: number, named: boolean): number;
|
||||
_ts_language_type_is_named_wasm(address: number, typeId: number): number;
|
||||
_ts_language_type_is_visible_wasm(address: number, typeId: number): number;
|
||||
_ts_language_next_state(address: number, stateId: number, typeId: number): number;
|
||||
_ts_language_supertypes_wasm(address: number): void;
|
||||
_ts_language_subtypes_wasm(address: number, supertype: number): void;
|
||||
|
||||
// Tree
|
||||
_ts_tree_copy(tree: number): number;
|
||||
_ts_tree_delete(tree: number): void;
|
||||
_ts_tree_edit_wasm(tree: number): void;
|
||||
_ts_tree_root_node_wasm(tree: number): void;
|
||||
_ts_tree_root_node_with_offset_wasm(tree: number): void;
|
||||
_ts_tree_get_changed_ranges_wasm(self: number, other: number): void;
|
||||
_ts_tree_included_ranges_wasm(self: number): void;
|
||||
|
||||
// Node
|
||||
_ts_node_symbol_wasm(tree: number): number;
|
||||
_ts_node_grammar_symbol_wasm(tree: number): number;
|
||||
_ts_node_end_point_wasm(tree: number): void;
|
||||
_ts_node_end_index_wasm(tree: number): number;
|
||||
_ts_node_parse_state_wasm(tree: number): number;
|
||||
_ts_node_next_parse_state_wasm(tree: number): number;
|
||||
_ts_node_is_named_wasm(tree: number): number;
|
||||
_ts_node_has_error_wasm(tree: number): number;
|
||||
_ts_node_has_changes_wasm(tree: number): number;
|
||||
_ts_node_is_error_wasm(tree: number): number;
|
||||
_ts_node_is_missing_wasm(tree: number): number;
|
||||
_ts_node_is_extra_wasm(tree: number): number;
|
||||
_ts_node_child_wasm(tree: number, index: number): void;
|
||||
_ts_node_named_child_wasm(tree: number, index: number): void;
|
||||
_ts_node_child_by_field_id_wasm(tree: number, fieldId: number): void;
|
||||
_ts_node_field_name_for_child_wasm(tree: number, index: number): number;
|
||||
_ts_node_field_name_for_named_child_wasm(tree: number, index: number): number;
|
||||
_ts_node_children_by_field_id_wasm(tree: number, fieldId: number): void;
|
||||
_ts_node_first_child_for_byte_wasm(tree: number): void;
|
||||
_ts_node_first_named_child_for_byte_wasm(tree: number): void;
|
||||
_ts_node_child_count_wasm(tree: number): number;
|
||||
_ts_node_named_child_count_wasm(tree: number): number;
|
||||
_ts_node_children_wasm(tree: number): void;
|
||||
_ts_node_named_children_wasm(tree: number): void;
|
||||
_ts_node_descendants_of_type_wasm(
|
||||
tree: number,
|
||||
symbolsAddress: number,
|
||||
symbolCount: number,
|
||||
startRow: number,
|
||||
startColumn: number,
|
||||
endRow: number,
|
||||
endColumn: number
|
||||
): void;
|
||||
_ts_node_next_sibling_wasm(tree: number): void;
|
||||
_ts_node_prev_sibling_wasm(tree: number): void;
|
||||
_ts_node_next_named_sibling_wasm(tree: number): void;
|
||||
_ts_node_prev_named_sibling_wasm(tree: number): void;
|
||||
_ts_node_descendant_count_wasm(tree: number): number;
|
||||
_ts_node_parent_wasm(tree: number): void;
|
||||
_ts_node_descendant_for_index_wasm(tree: number): void;
|
||||
_ts_node_named_descendant_for_index_wasm(tree: number): void;
|
||||
_ts_node_descendant_for_position_wasm(tree: number): void;
|
||||
_ts_node_named_descendant_for_position_wasm(tree: number): void;
|
||||
_ts_tree_cursor_new_wasm(tree: number): void;
|
||||
_ts_node_to_string_wasm(tree: number): number;
|
||||
|
||||
// TreeCursor
|
||||
_ts_tree_cursor_copy_wasm(cursor: number): void;
|
||||
_ts_tree_cursor_delete_wasm(cursor: number): void;
|
||||
_ts_tree_cursor_reset_wasm(cursor: number): void;
|
||||
_ts_tree_cursor_reset_to_wasm(cursor: number, other: number): void;
|
||||
_ts_tree_cursor_current_node_type_id_wasm(cursor: number): number;
|
||||
_ts_tree_cursor_current_node_state_id_wasm(cursor: number): number;
|
||||
_ts_tree_cursor_current_node_id_wasm(cursor: number): number;
|
||||
_ts_tree_cursor_current_node_is_named_wasm(cursor: number): number;
|
||||
_ts_tree_cursor_current_node_is_missing_wasm(cursor: number): number;
|
||||
_ts_tree_cursor_start_index_wasm(cursor: number): number;
|
||||
_ts_tree_cursor_end_index_wasm(cursor: number): number;
|
||||
_ts_tree_cursor_start_position_wasm(cursor: number): void;
|
||||
_ts_tree_cursor_end_position_wasm(cursor: number): void;
|
||||
_ts_tree_cursor_current_node_wasm(cursor: number): void;
|
||||
_ts_tree_cursor_current_field_id_wasm(cursor: number): number;
|
||||
_ts_tree_cursor_current_depth_wasm(cursor: number): number;
|
||||
_ts_tree_cursor_current_descendant_index_wasm(cursor: number): number;
|
||||
_ts_tree_cursor_goto_first_child_wasm(cursor: number): number;
|
||||
_ts_tree_cursor_goto_last_child_wasm(cursor: number): number;
|
||||
_ts_tree_cursor_goto_first_child_for_index_wasm(cursor: number): number;
|
||||
_ts_tree_cursor_goto_first_child_for_position_wasm(cursor: number): number;
|
||||
_ts_tree_cursor_goto_next_sibling_wasm(cursor: number): number;
|
||||
_ts_tree_cursor_goto_previous_sibling_wasm(cursor: number): number;
|
||||
_ts_tree_cursor_goto_descendant_wasm(cursor: number, index: number): void;
|
||||
_ts_tree_cursor_goto_parent_wasm(cursor: number): number;
|
||||
|
||||
// Query
|
||||
_ts_query_new(languageAddress: number, sourceAddress: number, sourceLength: number, errorOffset: number, errorType: number): number;
|
||||
_ts_query_string_count(address: number): number;
|
||||
_ts_query_capture_count(address: number): number;
|
||||
_ts_query_pattern_count(address: number): number;
|
||||
_ts_query_capture_name_for_id(address: number, id: number, buffer: number): number;
|
||||
_ts_query_capture_quantifier_for_id(address: number, patternId: number, captureId: number): CaptureQuantifier;
|
||||
_ts_query_string_value_for_id(address: number, id: number, buffer: number): number;
|
||||
_ts_query_predicates_for_pattern(address: number, patternId: number, buffer: number): number;
|
||||
_ts_query_delete(address: number): void;
|
||||
_ts_query_matches_wasm(
|
||||
address: number,
|
||||
treeAddress: number,
|
||||
startRow: number,
|
||||
startColumn: number,
|
||||
endRow: number,
|
||||
endColumn: number,
|
||||
startIndex: number,
|
||||
endIndex: number,
|
||||
matchLimit: number,
|
||||
maxStartDepth: number,
|
||||
timeoutMicros: number
|
||||
): void;
|
||||
_ts_query_captures_wasm(
|
||||
address: number,
|
||||
treeAddress: number,
|
||||
startRow: number,
|
||||
startColumn: number,
|
||||
endRow: number,
|
||||
endColumn: number,
|
||||
startIndex: number,
|
||||
endIndex: number,
|
||||
matchLimit: number,
|
||||
maxStartDepth: number,
|
||||
timeoutMicros: number
|
||||
): void;
|
||||
_ts_query_disable_capture(address: number, nameAddress: number, nameLength: number): void;
|
||||
_ts_query_disable_pattern(address: number, patternIndex: number): void;
|
||||
_ts_query_start_byte_for_pattern(address: number, patternIndex: number): number;
|
||||
_ts_query_end_byte_for_pattern(address: number, patternIndex: number): number;
|
||||
_ts_query_is_pattern_non_local(address: number, patternIndex: number): number;
|
||||
_ts_query_is_pattern_rooted(address: number, patternIndex: number): number;
|
||||
_ts_query_is_pattern_guaranteed_at_step(address: number, patternIndex: number, stepIndex: number): number;
|
||||
|
||||
// LookaheadIterator
|
||||
_ts_lookahead_iterator_new(address: number, stateId: number): number;
|
||||
_ts_lookahead_iterator_current_symbol(address: number): number;
|
||||
_ts_lookahead_iterator_delete(address: number): void;
|
||||
_ts_lookahead_iterator_reset_state(address: number, stateId: number): boolean;
|
||||
_ts_lookahead_iterator_reset(address: number, languageAddress: number, stateId: number): boolean;
|
||||
_ts_lookahead_iterator_next(address: number): boolean;
|
||||
|
||||
// @ts-ignore
|
||||
} = Module;
|
||||
9
lib/binding_web/src/index.ts
Normal file
9
lib/binding_web/src/index.ts
Normal file
|
|
@ -0,0 +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';
|
||||
491
lib/binding_web/src/language.ts
Normal file
491
lib/binding_web/src/language.ts
Normal file
|
|
@ -0,0 +1,491 @@
|
|||
import { INTERNAL, Internal, assertInternal, SIZE_OF_INT, SIZE_OF_SHORT, C } from './constants';
|
||||
import { LookaheadIterator } from './lookahead_iterator';
|
||||
import { Node } from './node';
|
||||
import { TRANSFER_BUFFER } from './parser';
|
||||
import { CaptureQuantifier, Predicate, PredicateStep, Properties, Query, TextPredicate } from './query';
|
||||
|
||||
declare const UTF8ToString: (ptr: number, maxBytesToRead?: number) => string;
|
||||
declare const lengthBytesUTF8: (str: string) => number;
|
||||
declare const stringToUTF8: (str: string, outPtr: number, maxBytesToRead: number) => void;
|
||||
declare const getValue: (ptr: number, type: string) => any;
|
||||
declare const loadWebAssemblyModule: (bytes: Uint8Array, options: { loadAsync: boolean }) => Promise<any>;
|
||||
|
||||
const PREDICATE_STEP_TYPE_CAPTURE = 1;
|
||||
const PREDICATE_STEP_TYPE_STRING = 2;
|
||||
|
||||
const QUERY_WORD_REGEX = /[\w-]+/g;
|
||||
const LANGUAGE_FUNCTION_REGEX = /^tree_sitter_\w+$/;
|
||||
|
||||
export class Language {
|
||||
private [0]: number; // Internal handle for WASM
|
||||
public types: string[];
|
||||
public fields: (string | null)[];
|
||||
|
||||
constructor(internal: Internal, address: number) {
|
||||
assertInternal(internal);
|
||||
this[0] = address;
|
||||
this.types = new Array(C._ts_language_symbol_count(this[0]));
|
||||
for (let i = 0, n = this.types.length; i < n; i++) {
|
||||
if (C._ts_language_symbol_type(this[0], i) < 2) {
|
||||
this.types[i] = UTF8ToString(C._ts_language_symbol_name(this[0], i));
|
||||
}
|
||||
}
|
||||
this.fields = new Array(C._ts_language_field_count(this[0]) + 1);
|
||||
for (let i = 0, n = this.fields.length; i < n; i++) {
|
||||
const fieldName = C._ts_language_field_name_for_id(this[0], i);
|
||||
if (fieldName !== 0) {
|
||||
this.fields[i] = UTF8ToString(fieldName);
|
||||
} else {
|
||||
this.fields[i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get name(): string | null {
|
||||
const ptr = C._ts_language_name(this[0]);
|
||||
if (ptr === 0) return null;
|
||||
return UTF8ToString(ptr);
|
||||
}
|
||||
|
||||
get version(): number {
|
||||
return C._ts_language_version(this[0]);
|
||||
}
|
||||
|
||||
get fieldCount(): number {
|
||||
return this.fields.length - 1;
|
||||
}
|
||||
|
||||
get stateCount(): number {
|
||||
return C._ts_language_state_count(this[0]);
|
||||
}
|
||||
|
||||
fieldIdForName(fieldName: string): number | null {
|
||||
const result = this.fields.indexOf(fieldName);
|
||||
return result !== -1 ? result : null;
|
||||
}
|
||||
|
||||
fieldNameForId(fieldId: number): string | null {
|
||||
return this.fields[fieldId] || null;
|
||||
}
|
||||
|
||||
idForNodeType(type: string, named: boolean): number | null {
|
||||
const typeLength = lengthBytesUTF8(type);
|
||||
const typeAddress = C._malloc(typeLength + 1);
|
||||
stringToUTF8(type, typeAddress, typeLength + 1);
|
||||
const result = C._ts_language_symbol_for_name(this[0], typeAddress, typeLength, named);
|
||||
C._free(typeAddress);
|
||||
return result || null;
|
||||
}
|
||||
|
||||
get nodeTypeCount(): number {
|
||||
return C._ts_language_symbol_count(this[0]);
|
||||
}
|
||||
|
||||
nodeTypeForId(typeId: number): string | null {
|
||||
const name = C._ts_language_symbol_name(this[0], typeId);
|
||||
return name ? UTF8ToString(name) : null;
|
||||
}
|
||||
|
||||
nodeTypeIsNamed(typeId: number): boolean {
|
||||
return C._ts_language_type_is_named_wasm(this[0], typeId) ? true : false;
|
||||
}
|
||||
|
||||
nodeTypeIsVisible(typeId: number): boolean {
|
||||
return C._ts_language_type_is_visible_wasm(this[0], typeId) ? true : false;
|
||||
}
|
||||
|
||||
get supertypes(): number[] {
|
||||
C._ts_language_supertypes_wasm(this[0]);
|
||||
const count = getValue(TRANSFER_BUFFER, 'i32');
|
||||
const buffer = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
|
||||
const result = new Array(count);
|
||||
|
||||
if (count > 0) {
|
||||
let address = buffer;
|
||||
for (let i = 0; i < count; i++) {
|
||||
result[i] = getValue(address, 'i16');
|
||||
address += SIZE_OF_SHORT;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
subtypes(supertype: number): number[] {
|
||||
C._ts_language_subtypes_wasm(this[0], supertype);
|
||||
const count = getValue(TRANSFER_BUFFER, 'i32');
|
||||
const buffer = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
|
||||
const result = new Array(count);
|
||||
|
||||
if (count > 0) {
|
||||
let address = buffer;
|
||||
for (let i = 0; i < count; i++) {
|
||||
result[i] = getValue(address, 'i16');
|
||||
address += SIZE_OF_SHORT;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
nextState(stateId: number, typeId: number): number {
|
||||
return C._ts_language_next_state(this[0], stateId, typeId);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
query(source: string): Query {
|
||||
const sourceLength = lengthBytesUTF8(source);
|
||||
const sourceAddress = C._malloc(sourceLength + 1);
|
||||
stringToUTF8(source, sourceAddress, sourceLength + 1);
|
||||
const address = C._ts_query_new(
|
||||
this[0],
|
||||
sourceAddress,
|
||||
sourceLength,
|
||||
TRANSFER_BUFFER,
|
||||
TRANSFER_BUFFER + SIZE_OF_INT
|
||||
);
|
||||
|
||||
if (!address) {
|
||||
const errorId = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
|
||||
const errorByte = getValue(TRANSFER_BUFFER, 'i32');
|
||||
const errorIndex = UTF8ToString(sourceAddress, errorByte).length;
|
||||
const suffix = source.slice(errorIndex, errorIndex + 100).split('\n')[0];
|
||||
let word = suffix.match(QUERY_WORD_REGEX)?.[0] || '';
|
||||
let error: Error;
|
||||
|
||||
switch (errorId) {
|
||||
case 2:
|
||||
error = new RangeError(`Bad node name '${word}'`);
|
||||
break;
|
||||
case 3:
|
||||
error = new RangeError(`Bad field name '${word}'`);
|
||||
break;
|
||||
case 4:
|
||||
error = new RangeError(`Bad capture name @${word}`);
|
||||
break;
|
||||
case 5:
|
||||
error = new TypeError(`Bad pattern structure at offset ${errorIndex}: '${suffix}'...`);
|
||||
word = '';
|
||||
break;
|
||||
default:
|
||||
error = new SyntaxError(`Bad syntax at offset ${errorIndex}: '${suffix}'...`);
|
||||
word = '';
|
||||
break;
|
||||
}
|
||||
|
||||
(error as any).index = errorIndex;
|
||||
(error as any).length = word.length;
|
||||
C._free(sourceAddress);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const stringCount = C._ts_query_string_count(address);
|
||||
const captureCount = C._ts_query_capture_count(address);
|
||||
const patternCount = C._ts_query_pattern_count(address);
|
||||
const captureNames = new Array<string>(captureCount);
|
||||
const captureQuantifiers = new Array<Array<CaptureQuantifier>>(patternCount);
|
||||
const stringValues = new Array<string>(stringCount);
|
||||
|
||||
for (let i = 0; i < captureCount; i++) {
|
||||
const nameAddress = C._ts_query_capture_name_for_id(
|
||||
address,
|
||||
i,
|
||||
TRANSFER_BUFFER
|
||||
);
|
||||
const nameLength = getValue(TRANSFER_BUFFER, 'i32');
|
||||
captureNames[i] = UTF8ToString(nameAddress, nameLength);
|
||||
}
|
||||
|
||||
for (let i = 0; i < patternCount; i++) {
|
||||
const captureQuantifiersArray = new Array<CaptureQuantifier>(captureCount);
|
||||
for (let j = 0; j < captureCount; j++) {
|
||||
const quantifier = C._ts_query_capture_quantifier_for_id(address, i, j);
|
||||
captureQuantifiersArray[j] = quantifier;
|
||||
}
|
||||
captureQuantifiers[i] = captureQuantifiersArray;
|
||||
}
|
||||
|
||||
for (let i = 0; i < stringCount; i++) {
|
||||
const valueAddress = C._ts_query_string_value_for_id(
|
||||
address,
|
||||
i,
|
||||
TRANSFER_BUFFER
|
||||
);
|
||||
const nameLength = getValue(TRANSFER_BUFFER, 'i32');
|
||||
stringValues[i] = UTF8ToString(valueAddress, nameLength);
|
||||
}
|
||||
|
||||
const setProperties = new Array<Properties>(patternCount);
|
||||
const assertedProperties = new Array<Properties>(patternCount);
|
||||
const refutedProperties = new Array<Properties>(patternCount);
|
||||
const predicates = new Array<Array<Predicate>>(patternCount);
|
||||
const textPredicates = new Array<Array<TextPredicate>>(patternCount);
|
||||
|
||||
for (let i = 0; i < patternCount; i++) {
|
||||
const predicatesAddress = C._ts_query_predicates_for_pattern(
|
||||
address,
|
||||
i,
|
||||
TRANSFER_BUFFER
|
||||
);
|
||||
const stepCount = getValue(TRANSFER_BUFFER, 'i32');
|
||||
|
||||
predicates[i] = [];
|
||||
textPredicates[i] = [];
|
||||
|
||||
const steps: PredicateStep[] = [];
|
||||
let stepAddress = predicatesAddress;
|
||||
for (let j = 0; j < stepCount; j++) {
|
||||
const stepType = getValue(stepAddress, 'i32');
|
||||
stepAddress += SIZE_OF_INT;
|
||||
const stepValueId: number = getValue(stepAddress, 'i32');
|
||||
stepAddress += SIZE_OF_INT;
|
||||
|
||||
if (stepType === PREDICATE_STEP_TYPE_CAPTURE) {
|
||||
const name = captureNames[stepValueId];
|
||||
steps.push({ type: 'capture', name });
|
||||
} else if (stepType === PREDICATE_STEP_TYPE_STRING) {
|
||||
steps.push({ type: 'string', value: stringValues[stepValueId] });
|
||||
} else if (steps.length > 0) {
|
||||
if (steps[0].type !== 'string') {
|
||||
throw new Error('Predicates must begin with a literal value');
|
||||
}
|
||||
|
||||
const operator = steps[0].value!;
|
||||
let isPositive = true;
|
||||
let matchAll = true;
|
||||
let captureName: string | undefined;
|
||||
|
||||
switch (operator) {
|
||||
case 'any-not-eq?':
|
||||
case 'not-eq?':
|
||||
isPositive = false;
|
||||
case 'any-eq?':
|
||||
case 'eq?': {
|
||||
if (steps.length !== 3) {
|
||||
throw new Error(
|
||||
`Wrong number of arguments to \`#${operator}\` predicate. Expected 2, got ${steps.length - 1}`
|
||||
);
|
||||
}
|
||||
if (steps[1].type !== 'capture') {
|
||||
throw new Error(
|
||||
`First argument of \`#${operator}\` predicate must be a capture. Got "${steps[1].value}"`
|
||||
);
|
||||
}
|
||||
matchAll = !operator.startsWith('any-');
|
||||
if (steps[2].type === 'capture') {
|
||||
const captureName1 = steps[1].name!;
|
||||
const captureName2 = steps[2].name!;
|
||||
textPredicates[i].push((captures) => {
|
||||
const nodes1: Node[] = [];
|
||||
const nodes2: Node[] = [];
|
||||
for (const c of captures) {
|
||||
if (c.name === captureName1) nodes1.push(c.node);
|
||||
if (c.name === captureName2) nodes2.push(c.node);
|
||||
}
|
||||
const compare = (n1: { text: string }, n2: { text: string }, positive: boolean) => {
|
||||
return positive ? n1.text === n2.text : n1.text !== n2.text;
|
||||
};
|
||||
return matchAll
|
||||
? nodes1.every((n1) => nodes2.some((n2) => compare(n1, n2, isPositive)))
|
||||
: nodes1.some((n1) => nodes2.some((n2) => compare(n1, n2, isPositive)));
|
||||
});
|
||||
} else {
|
||||
captureName = steps[1].name;
|
||||
const stringValue = steps[2].value;
|
||||
const matches = (n: Node) => n.text === stringValue;
|
||||
const doesNotMatch = (n: Node) => n.text !== stringValue;
|
||||
textPredicates[i].push((captures) => {
|
||||
const nodes = [];
|
||||
for (const c of captures) {
|
||||
if (c.name === captureName) nodes.push(c.node);
|
||||
}
|
||||
const test = isPositive ? matches : doesNotMatch;
|
||||
return matchAll ?
|
||||
nodes.every(test) :
|
||||
nodes.some(test);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'any-not-match?':
|
||||
case 'not-match?':
|
||||
isPositive = false;
|
||||
case 'any-match?':
|
||||
case 'match?': {
|
||||
if (steps.length !== 3) {
|
||||
throw new Error(
|
||||
`Wrong number of arguments to \`#${operator}\` predicate. Expected 2, got ${steps.length - 1}.`,
|
||||
);
|
||||
}
|
||||
if (steps[1].type !== 'capture') {
|
||||
throw new Error(
|
||||
`First argument of \`#${operator}\` predicate must be a capture. Got "${steps[1].value}".`,
|
||||
);
|
||||
}
|
||||
if (steps[2].type !== 'string') {
|
||||
throw new Error(
|
||||
`Second argument of \`#${operator}\` predicate must be a string. Got @${steps[2].value}.`,
|
||||
);
|
||||
}
|
||||
captureName = steps[1].name;
|
||||
const regex = new RegExp(steps[2].value);
|
||||
matchAll = !operator.startsWith('any-');
|
||||
textPredicates[i].push((captures) => {
|
||||
const nodes = [];
|
||||
for (const c of captures) {
|
||||
if (c.name === captureName) nodes.push(c.node.text);
|
||||
}
|
||||
const test = (text: string, positive: boolean) => {
|
||||
return positive ?
|
||||
regex.test(text) :
|
||||
!regex.test(text);
|
||||
};
|
||||
if (nodes.length === 0) return !isPositive;
|
||||
return matchAll ?
|
||||
nodes.every((text) => test(text, isPositive)) :
|
||||
nodes.some((text) => test(text, isPositive));
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'set!': {
|
||||
if (steps.length < 2 || steps.length > 3) {
|
||||
throw new Error(
|
||||
`Wrong number of arguments to \`#set!\` predicate. Expected 1 or 2. Got ${steps.length - 1}.`,
|
||||
);
|
||||
}
|
||||
if (steps.some((s) => s.type !== 'string')) {
|
||||
throw new Error(
|
||||
`Arguments to \`#set!\` predicate must be a strings.".`,
|
||||
);
|
||||
}
|
||||
if (!setProperties[i]) setProperties[i] = {};
|
||||
setProperties[i][steps[1].value!] = steps[2]?.value || null;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'is?':
|
||||
case 'is-not?': {
|
||||
if (steps.length < 2 || steps.length > 3) {
|
||||
throw new Error(
|
||||
`Wrong number of arguments to \`#${operator}\` predicate. Expected 1 or 2. Got ${steps.length - 1}.`,
|
||||
);
|
||||
}
|
||||
if (steps.some((s) => s.type !== 'string')) {
|
||||
throw new Error(
|
||||
`Arguments to \`#${operator}\` predicate must be a strings.".`,
|
||||
);
|
||||
}
|
||||
const properties = operator === 'is?' ? assertedProperties : refutedProperties;
|
||||
if (!properties[i]) properties[i] = {};
|
||||
properties[i][steps[1].value!] = steps[2]?.value || null;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'not-any-of?':
|
||||
isPositive = false;
|
||||
case 'any-of?': {
|
||||
if (steps.length < 2) {
|
||||
throw new Error(
|
||||
`Wrong number of arguments to \`#${operator}\` predicate. Expected at least 1. Got ${steps.length - 1}.`,
|
||||
);
|
||||
}
|
||||
if (steps[1].type !== 'capture') {
|
||||
throw new Error(
|
||||
`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);
|
||||
textPredicates[i].push((captures) => {
|
||||
const nodes = [];
|
||||
for (const c of captures) {
|
||||
if (c.name === captureName) nodes.push(c.node.text);
|
||||
}
|
||||
if (nodes.length === 0) return !isPositive;
|
||||
return nodes.every((text) => values.includes(text)) === isPositive;
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
predicates[i].push({ operator, operands: steps.slice(1) });
|
||||
}
|
||||
|
||||
steps.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Object.freeze(setProperties[i]);
|
||||
Object.freeze(assertedProperties[i]);
|
||||
Object.freeze(refutedProperties[i]);
|
||||
}
|
||||
|
||||
C._free(sourceAddress);
|
||||
return new Query(
|
||||
INTERNAL,
|
||||
address,
|
||||
captureNames,
|
||||
captureQuantifiers,
|
||||
textPredicates,
|
||||
predicates,
|
||||
setProperties,
|
||||
assertedProperties,
|
||||
refutedProperties,
|
||||
);
|
||||
}
|
||||
|
||||
static load(input: string | Uint8Array): Promise<Language> {
|
||||
let bytes: Promise<Uint8Array>;
|
||||
if (input instanceof Uint8Array) {
|
||||
bytes = Promise.resolve(input);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
if (globalThis.process?.versions?.node) {
|
||||
// @ts-ignore
|
||||
const fs = require('fs/promises');
|
||||
bytes = fs.readFile(input);
|
||||
} else {
|
||||
bytes = fetch(input)
|
||||
.then((response) => response.arrayBuffer()
|
||||
.then((buffer) => {
|
||||
if (response.ok) {
|
||||
return new Uint8Array(buffer);
|
||||
} else {
|
||||
const body = new TextDecoder('utf-8').decode(buffer);
|
||||
throw new Error(`Language.load failed with status ${response.status}.\n\n${body}`);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return bytes
|
||||
.then((bytes) => loadWebAssemblyModule(bytes, { loadAsync: true }))
|
||||
.then((mod) => {
|
||||
const symbolNames = Object.keys(mod);
|
||||
const functionName = symbolNames.find((key) =>
|
||||
LANGUAGE_FUNCTION_REGEX.test(key) &&
|
||||
!key.includes('external_scanner_'),
|
||||
);
|
||||
if (!functionName) {
|
||||
console.log(`Couldn't find language function in WASM file. Symbols:\n${JSON.stringify(symbolNames, null, 2)}`);
|
||||
throw new Error('Language.load failed: no language function found in WASM file');
|
||||
}
|
||||
const languageAddress = mod[functionName]();
|
||||
return new Language(INTERNAL, languageAddress);
|
||||
});
|
||||
}
|
||||
}
|
||||
50
lib/binding_web/src/lookahead_iterator.ts
Normal file
50
lib/binding_web/src/lookahead_iterator.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { C, Internal, assertInternal } from './constants';
|
||||
import { Language } from './language';
|
||||
|
||||
export class LookaheadIterator implements Iterable<string> {
|
||||
private [0]: number; // Internal handle for WASM
|
||||
private language: Language;
|
||||
|
||||
constructor(internal: Internal, address: number, language: Language) {
|
||||
assertInternal(internal);
|
||||
this[0] = address;
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
get currentTypeId(): number {
|
||||
return C._ts_lookahead_iterator_current_symbol(this[0]);
|
||||
}
|
||||
|
||||
get currentType(): string {
|
||||
return this.language.types[this.currentTypeId] || 'ERROR';
|
||||
}
|
||||
|
||||
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(language: Language, stateId: number): boolean {
|
||||
if (C._ts_lookahead_iterator_reset(this[0], language[0], stateId)) {
|
||||
this.language = language;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[Symbol.iterator](): Iterator<string> {
|
||||
const self = this;
|
||||
return {
|
||||
next(): IteratorResult<string> {
|
||||
if (C._ts_lookahead_iterator_next(self[0])) {
|
||||
return { done: false, value: self.currentType };
|
||||
}
|
||||
return { done: true, value: '' };
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
106
lib/binding_web/src/marshal.ts
Normal file
106
lib/binding_web/src/marshal.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { Edit, INTERNAL, Point, Range, SIZE_OF_INT, SIZE_OF_NODE, SIZE_OF_POINT } from "./constants";
|
||||
import { Node } from "./node";
|
||||
import { Tree } from "./tree";
|
||||
import { Query } from "./query";
|
||||
import { TreeCursor } from "./tree_cursor";
|
||||
import { TRANSFER_BUFFER } from "./parser";
|
||||
|
||||
export function unmarshalCaptures(query: Query, tree: Tree, address: number, result: Array<{name: string, node: Node}>) {
|
||||
for (let i = 0, n = result.length; i < n; i++) {
|
||||
const captureIndex = getValue(address, 'i32');
|
||||
address += SIZE_OF_INT;
|
||||
const node = unmarshalNode(tree, address)!;
|
||||
address += SIZE_OF_NODE;
|
||||
result[i] = {name: query.captureNames[captureIndex], node};
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
export function marshalNode(node: Node) {
|
||||
let address = TRANSFER_BUFFER;
|
||||
setValue(address, node.id, 'i32');
|
||||
address += SIZE_OF_INT;
|
||||
setValue(address, node.startIndex, 'i32');
|
||||
address += SIZE_OF_INT;
|
||||
setValue(address, node.startPosition.row, 'i32');
|
||||
address += SIZE_OF_INT;
|
||||
setValue(address, node.startPosition.column, 'i32');
|
||||
address += SIZE_OF_INT;
|
||||
setValue(address, node[0], 'i32');
|
||||
}
|
||||
|
||||
export function unmarshalNode(tree: Tree, address = TRANSFER_BUFFER): Node | null {
|
||||
const id = getValue(address, 'i32');
|
||||
address += SIZE_OF_INT;
|
||||
if (id === 0) return null;
|
||||
|
||||
const index = getValue(address, 'i32');
|
||||
address += SIZE_OF_INT;
|
||||
const row = getValue(address, 'i32');
|
||||
address += SIZE_OF_INT;
|
||||
const column = getValue(address, 'i32');
|
||||
address += SIZE_OF_INT;
|
||||
const other = getValue(address, 'i32');
|
||||
|
||||
const result = new Node(INTERNAL, {
|
||||
id,
|
||||
tree,
|
||||
startIndex: index,
|
||||
startPosition: {row, column},
|
||||
other,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function marshalTreeCursor(cursor: TreeCursor, address = TRANSFER_BUFFER) {
|
||||
setValue(address + 0 * SIZE_OF_INT, cursor[0], 'i32');
|
||||
setValue(address + 1 * SIZE_OF_INT, cursor[1], 'i32');
|
||||
setValue(address + 2 * SIZE_OF_INT, cursor[2], 'i32');
|
||||
setValue(address + 3 * SIZE_OF_INT, cursor[3], 'i32');
|
||||
}
|
||||
|
||||
export function unmarshalTreeCursor(cursor: TreeCursor) {
|
||||
cursor[0] = getValue(TRANSFER_BUFFER + 0 * SIZE_OF_INT, 'i32');
|
||||
cursor[1] = getValue(TRANSFER_BUFFER + 1 * SIZE_OF_INT, 'i32');
|
||||
cursor[2] = getValue(TRANSFER_BUFFER + 2 * SIZE_OF_INT, 'i32');
|
||||
cursor[3] = getValue(TRANSFER_BUFFER + 3 * SIZE_OF_INT, 'i32');
|
||||
}
|
||||
|
||||
export function marshalPoint(address: number, point: Point): void {
|
||||
setValue(address, point.row, 'i32');
|
||||
setValue(address + SIZE_OF_INT, point.column, 'i32');
|
||||
}
|
||||
|
||||
export function unmarshalPoint(address: number): Point {
|
||||
const result = {
|
||||
row: getValue(address, 'i32') >>> 0,
|
||||
column: getValue(address + SIZE_OF_INT, 'i32') >>> 0,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
export function marshalRange(address: number, range: Range): void {
|
||||
marshalPoint(address, range.startPosition); address += SIZE_OF_POINT;
|
||||
marshalPoint(address, range.endPosition); address += SIZE_OF_POINT;
|
||||
setValue(address, range.startIndex, 'i32'); address += SIZE_OF_INT;
|
||||
setValue(address, range.endIndex, 'i32'); address += SIZE_OF_INT;
|
||||
}
|
||||
|
||||
export function unmarshalRange(address: number): Range {
|
||||
const result = {} as Range;
|
||||
result.startPosition = unmarshalPoint(address); address += SIZE_OF_POINT;
|
||||
result.endPosition = unmarshalPoint(address); address += SIZE_OF_POINT;
|
||||
result.startIndex = getValue(address, 'i32') >>> 0; address += SIZE_OF_INT;
|
||||
result.endIndex = getValue(address, 'i32') >>> 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function marshalEdit(edit: Edit, address = TRANSFER_BUFFER) {
|
||||
marshalPoint(address, edit.startPosition); address += SIZE_OF_POINT;
|
||||
marshalPoint(address, edit.oldEndPosition); address += SIZE_OF_POINT;
|
||||
marshalPoint(address, edit.newEndPosition); address += SIZE_OF_POINT;
|
||||
setValue(address, edit.startIndex, 'i32'); address += SIZE_OF_INT;
|
||||
setValue(address, edit.oldEndIndex, 'i32'); address += SIZE_OF_INT;
|
||||
setValue(address, edit.newEndIndex, 'i32'); address += SIZE_OF_INT;
|
||||
}
|
||||
445
lib/binding_web/src/node.ts
Normal file
445
lib/binding_web/src/node.ts
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
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';
|
||||
import { marshalNode, marshalPoint, unmarshalNode, unmarshalPoint } from './marshal';
|
||||
import { TRANSFER_BUFFER } from './parser';
|
||||
|
||||
declare const AsciiToString: (ptr: number) => string;
|
||||
|
||||
export class Node {
|
||||
// @ts-ignore
|
||||
private [0]: number; // Internal handle for WASM
|
||||
private _children?: (Node | null)[];
|
||||
private _namedChildren?: (Node | null)[];
|
||||
|
||||
id!: number;
|
||||
startIndex!: number;
|
||||
startPosition!: Point;
|
||||
tree: Tree;
|
||||
|
||||
constructor(
|
||||
internal: Internal,
|
||||
{
|
||||
id,
|
||||
tree,
|
||||
startIndex,
|
||||
startPosition,
|
||||
other,
|
||||
}: {
|
||||
id: number;
|
||||
tree: Tree;
|
||||
startIndex: number;
|
||||
startPosition: Point;
|
||||
other: number;
|
||||
}
|
||||
) {
|
||||
assertInternal(internal);
|
||||
this[0] = other;
|
||||
this.id = id;
|
||||
this.tree = tree;
|
||||
this.startIndex = startIndex;
|
||||
this.startPosition = startPosition;
|
||||
}
|
||||
|
||||
get typeId(): number {
|
||||
marshalNode(this);
|
||||
return C._ts_node_symbol_wasm(this.tree[0]);
|
||||
}
|
||||
|
||||
get grammarId(): number {
|
||||
marshalNode(this);
|
||||
return C._ts_node_grammar_symbol_wasm(this.tree[0]);
|
||||
}
|
||||
|
||||
get type(): string {
|
||||
return this.tree.language.types[this.typeId] || 'ERROR';
|
||||
}
|
||||
|
||||
get grammarType(): string {
|
||||
return this.tree.language.types[this.grammarId] || 'ERROR';
|
||||
}
|
||||
|
||||
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 text(): string {
|
||||
return getText(this.tree, this.startIndex, this.endIndex, this.startPosition);
|
||||
}
|
||||
|
||||
get parseState(): number {
|
||||
marshalNode(this);
|
||||
return C._ts_node_parse_state_wasm(this.tree[0]);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
equals(other: Node): boolean {
|
||||
return this.tree === other.tree && this.id === other.id;
|
||||
}
|
||||
|
||||
child(index: number): Node | null {
|
||||
marshalNode(this);
|
||||
C._ts_node_child_wasm(this.tree[0], index);
|
||||
return unmarshalNode(this.tree);
|
||||
}
|
||||
|
||||
namedChild(index: number): Node | null {
|
||||
marshalNode(this);
|
||||
C._ts_node_named_child_wasm(this.tree[0], index);
|
||||
return unmarshalNode(this.tree);
|
||||
}
|
||||
|
||||
childForFieldId(fieldId: number): Node | null {
|
||||
marshalNode(this);
|
||||
C._ts_node_child_by_field_id_wasm(this.tree[0], fieldId);
|
||||
return unmarshalNode(this.tree);
|
||||
}
|
||||
|
||||
childForFieldName(fieldName: string): Node | null {
|
||||
const fieldId = this.tree.language.fields.indexOf(fieldName);
|
||||
if (fieldId !== -1) return this.childForFieldId(fieldId);
|
||||
return null;
|
||||
}
|
||||
|
||||
fieldNameForChild(index: number): string | null {
|
||||
marshalNode(this);
|
||||
const address = C._ts_node_field_name_for_child_wasm(this.tree[0], index);
|
||||
if (!address) return null;
|
||||
return AsciiToString(address);
|
||||
}
|
||||
|
||||
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 AsciiToString(address);
|
||||
}
|
||||
|
||||
childrenForFieldName(fieldName: string): (Node | null)[] {
|
||||
const fieldId = this.tree.language.fields.indexOf(fieldName);
|
||||
if (fieldId !== -1 && fieldId !== 0) return this.childrenForFieldId(fieldId);
|
||||
return [];
|
||||
}
|
||||
|
||||
childrenForFieldId(fieldId: number): (Node | null)[] {
|
||||
marshalNode(this);
|
||||
C._ts_node_children_by_field_id_wasm(this.tree[0], fieldId);
|
||||
const count = getValue(TRANSFER_BUFFER, 'i32');
|
||||
const buffer = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
|
||||
const result = new Array<Node | null>(count);
|
||||
|
||||
if (count > 0) {
|
||||
let address = buffer;
|
||||
for (let i = 0; i < count; i++) {
|
||||
result[i] = unmarshalNode(this.tree, address);
|
||||
address += SIZE_OF_NODE;
|
||||
}
|
||||
C._free(buffer);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
firstChildForIndex(index: number): Node | null {
|
||||
marshalNode(this);
|
||||
const address = TRANSFER_BUFFER + SIZE_OF_NODE;
|
||||
setValue(address, index, 'i32');
|
||||
C._ts_node_first_child_for_byte_wasm(this.tree[0]);
|
||||
return unmarshalNode(this.tree);
|
||||
}
|
||||
|
||||
firstNamedChildForIndex(index: number): Node | null {
|
||||
marshalNode(this);
|
||||
const address = TRANSFER_BUFFER + SIZE_OF_NODE;
|
||||
setValue(address, index, 'i32');
|
||||
C._ts_node_first_named_child_for_byte_wasm(this.tree[0]);
|
||||
return unmarshalNode(this.tree);
|
||||
}
|
||||
|
||||
get childCount(): number {
|
||||
marshalNode(this);
|
||||
return C._ts_node_child_count_wasm(this.tree[0]);
|
||||
}
|
||||
|
||||
get namedChildCount(): number {
|
||||
marshalNode(this);
|
||||
return C._ts_node_named_child_count_wasm(this.tree[0]);
|
||||
}
|
||||
|
||||
get firstChild(): Node | null {
|
||||
return this.child(0);
|
||||
}
|
||||
|
||||
get firstNamedChild(): Node | null {
|
||||
return this.namedChild(0);
|
||||
}
|
||||
|
||||
get lastChild(): Node | null {
|
||||
return this.child(this.childCount - 1);
|
||||
}
|
||||
|
||||
get lastNamedChild(): Node | null {
|
||||
return this.namedChild(this.namedChildCount - 1);
|
||||
}
|
||||
|
||||
get children(): (Node | null)[] {
|
||||
if (!this._children) {
|
||||
marshalNode(this);
|
||||
C._ts_node_children_wasm(this.tree[0]);
|
||||
const count = getValue(TRANSFER_BUFFER, 'i32');
|
||||
const buffer = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
|
||||
this._children = new Array<Node>(count);
|
||||
if (count > 0) {
|
||||
let address = buffer;
|
||||
for (let i = 0; i < count; i++) {
|
||||
this._children[i] = unmarshalNode(this.tree, address);
|
||||
address += SIZE_OF_NODE;
|
||||
}
|
||||
C._free(buffer);
|
||||
}
|
||||
}
|
||||
return this._children;
|
||||
}
|
||||
|
||||
get namedChildren(): (Node | null)[] {
|
||||
if (!this._namedChildren) {
|
||||
marshalNode(this);
|
||||
C._ts_node_named_children_wasm(this.tree[0]);
|
||||
const count = getValue(TRANSFER_BUFFER, 'i32');
|
||||
const buffer = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
|
||||
this._namedChildren = new Array<Node>(count);
|
||||
if (count > 0) {
|
||||
let address = buffer;
|
||||
for (let i = 0; i < count; i++) {
|
||||
this._namedChildren[i] = unmarshalNode(this.tree, address);
|
||||
address += SIZE_OF_NODE;
|
||||
}
|
||||
C._free(buffer);
|
||||
}
|
||||
}
|
||||
return this._namedChildren;
|
||||
}
|
||||
|
||||
descendantsOfType(
|
||||
types: string | string[],
|
||||
startPosition: Point = ZERO_POINT,
|
||||
endPosition: Point = ZERO_POINT
|
||||
): (Node | null)[] {
|
||||
if (!Array.isArray(types)) types = [types];
|
||||
|
||||
// Convert the type strings to numeric type symbols
|
||||
const symbols: number[] = [];
|
||||
const typesBySymbol = this.tree.language.types;
|
||||
for (let i = 0, n = typesBySymbol.length; i < n; i++) {
|
||||
if (types.includes(typesBySymbol[i])) {
|
||||
symbols.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the array of symbols to the WASM heap
|
||||
const symbolsAddress = C._malloc(SIZE_OF_INT * symbols.length);
|
||||
for (let i = 0, n = symbols.length; i < n; i++) {
|
||||
setValue(symbolsAddress + i * SIZE_OF_INT, symbols[i], 'i32');
|
||||
}
|
||||
|
||||
// Call the C API to compute the descendants
|
||||
marshalNode(this);
|
||||
C._ts_node_descendants_of_type_wasm(
|
||||
this.tree[0],
|
||||
symbolsAddress,
|
||||
symbols.length,
|
||||
startPosition.row,
|
||||
startPosition.column,
|
||||
endPosition.row,
|
||||
endPosition.column
|
||||
);
|
||||
|
||||
// Instantiate the nodes based on the data returned
|
||||
const descendantCount = getValue(TRANSFER_BUFFER, 'i32');
|
||||
const descendantAddress = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
|
||||
const result = new Array<Node | null>(descendantCount);
|
||||
if (descendantCount > 0) {
|
||||
let address = descendantAddress;
|
||||
for (let i = 0; i < descendantCount; i++) {
|
||||
result[i] = unmarshalNode(this.tree, address);
|
||||
address += SIZE_OF_NODE;
|
||||
}
|
||||
}
|
||||
|
||||
// Free the intermediate buffers
|
||||
C._free(descendantAddress);
|
||||
C._free(symbolsAddress);
|
||||
return result;
|
||||
}
|
||||
|
||||
get nextSibling(): Node | null {
|
||||
marshalNode(this);
|
||||
C._ts_node_next_sibling_wasm(this.tree[0]);
|
||||
return unmarshalNode(this.tree);
|
||||
}
|
||||
|
||||
get previousSibling(): Node | null {
|
||||
marshalNode(this);
|
||||
C._ts_node_prev_sibling_wasm(this.tree[0]);
|
||||
return unmarshalNode(this.tree);
|
||||
}
|
||||
|
||||
get nextNamedSibling(): Node | null {
|
||||
marshalNode(this);
|
||||
C._ts_node_next_named_sibling_wasm(this.tree[0]);
|
||||
return unmarshalNode(this.tree);
|
||||
}
|
||||
|
||||
get previousNamedSibling(): Node | null {
|
||||
marshalNode(this);
|
||||
C._ts_node_prev_named_sibling_wasm(this.tree[0]);
|
||||
return unmarshalNode(this.tree);
|
||||
}
|
||||
|
||||
get descendantCount(): number {
|
||||
marshalNode(this);
|
||||
return C._ts_node_descendant_count_wasm(this.tree[0]);
|
||||
}
|
||||
|
||||
get parent(): Node | null {
|
||||
marshalNode(this);
|
||||
C._ts_node_parent_wasm(this.tree[0]);
|
||||
return unmarshalNode(this.tree);
|
||||
}
|
||||
|
||||
descendantForIndex(start: number, end: number = start): Node | null {
|
||||
if (typeof start !== 'number' || typeof end !== 'number') {
|
||||
throw new Error('Arguments must be numbers');
|
||||
}
|
||||
|
||||
marshalNode(this);
|
||||
const address = TRANSFER_BUFFER + SIZE_OF_NODE;
|
||||
setValue(address, start, 'i32');
|
||||
setValue(address + SIZE_OF_INT, end, 'i32');
|
||||
C._ts_node_descendant_for_index_wasm(this.tree[0]);
|
||||
return unmarshalNode(this.tree);
|
||||
}
|
||||
|
||||
namedDescendantForIndex(start: number, end: number = start): Node | null {
|
||||
if (typeof start !== 'number' || typeof end !== 'number') {
|
||||
throw new Error('Arguments must be numbers');
|
||||
}
|
||||
|
||||
marshalNode(this);
|
||||
const address = TRANSFER_BUFFER + SIZE_OF_NODE;
|
||||
setValue(address, start, 'i32');
|
||||
setValue(address + SIZE_OF_INT, end, 'i32');
|
||||
C._ts_node_named_descendant_for_index_wasm(this.tree[0]);
|
||||
return unmarshalNode(this.tree);
|
||||
}
|
||||
|
||||
descendantForPosition(start: Point, end: Point = start) {
|
||||
if (!isPoint(start) || !isPoint(end)) {
|
||||
throw new Error('Arguments must be {row, column} objects');
|
||||
}
|
||||
|
||||
marshalNode(this);
|
||||
const address = TRANSFER_BUFFER + SIZE_OF_NODE;
|
||||
marshalPoint(address, start);
|
||||
marshalPoint(address + SIZE_OF_POINT, end);
|
||||
C._ts_node_descendant_for_position_wasm(this.tree[0]);
|
||||
return unmarshalNode(this.tree);
|
||||
}
|
||||
|
||||
namedDescendantForPosition(start: Point, end: Point = start) {
|
||||
if (!isPoint(start) || !isPoint(end)) {
|
||||
throw new Error('Arguments must be {row, column} objects');
|
||||
}
|
||||
|
||||
marshalNode(this);
|
||||
const address = TRANSFER_BUFFER + SIZE_OF_NODE;
|
||||
marshalPoint(address, start);
|
||||
marshalPoint(address + SIZE_OF_POINT, end);
|
||||
C._ts_node_named_descendant_for_position_wasm(this.tree[0]);
|
||||
return unmarshalNode(this.tree);
|
||||
}
|
||||
|
||||
walk(): TreeCursor {
|
||||
marshalNode(this);
|
||||
C._ts_tree_cursor_new_wasm(this.tree[0]);
|
||||
return new TreeCursor(INTERNAL, this.tree);
|
||||
}
|
||||
|
||||
edit(edit: Edit) {
|
||||
if (this.startIndex >= edit.oldEndIndex) {
|
||||
this.startIndex = edit.newEndIndex + (this.startIndex - edit.oldEndIndex);
|
||||
let subbedPointRow;
|
||||
let subbedPointColumn;
|
||||
if (this.startPosition.row > edit.oldEndPosition.row) {
|
||||
subbedPointRow = this.startPosition.row - edit.oldEndPosition.row;
|
||||
subbedPointColumn = this.startPosition.column;
|
||||
} else {
|
||||
subbedPointRow = 0;
|
||||
subbedPointColumn = this.startPosition.column;
|
||||
if (this.startPosition.column >= edit.oldEndPosition.column) {
|
||||
subbedPointColumn =
|
||||
this.startPosition.column - edit.oldEndPosition.column;
|
||||
}
|
||||
}
|
||||
|
||||
if (subbedPointRow > 0) {
|
||||
this.startPosition.row += subbedPointRow;
|
||||
this.startPosition.column = subbedPointColumn;
|
||||
} else {
|
||||
this.startPosition.column += subbedPointColumn;
|
||||
}
|
||||
} else if (this.startIndex > edit.startIndex) {
|
||||
this.startIndex = edit.newEndIndex;
|
||||
this.startPosition.row = edit.newEndPosition.row;
|
||||
this.startPosition.column = edit.newEndPosition.column;
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
marshalNode(this);
|
||||
const address = C._ts_node_to_string_wasm(this.tree[0]);
|
||||
const result = AsciiToString(address);
|
||||
C._free(address);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
193
lib/binding_web/src/parser.ts
Normal file
193
lib/binding_web/src/parser.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import { C, INTERNAL, Point, Range, SIZE_OF_INT, SIZE_OF_RANGE } from './constants';
|
||||
import { Language } from './language';
|
||||
import { marshalRange, unmarshalRange } from './marshal';
|
||||
import { Tree } from './tree';
|
||||
|
||||
declare const getValue: (ptr: number, type: string) => any;
|
||||
declare const Module: { [key: string]: any };
|
||||
declare let initPromise: Promise<void>;
|
||||
|
||||
interface ParseOptions {
|
||||
includedRanges?: Range[];
|
||||
progressCallback?: (percent: number) => void;
|
||||
}
|
||||
|
||||
type ParseCallback = ((index: number, position: Point) => string) | string;
|
||||
type LogCallback = ((message: string, type: number, row: number, column: number) => void) | null;
|
||||
|
||||
// Global variable for transferring data across the FFI boundary
|
||||
export let TRANSFER_BUFFER: number;
|
||||
|
||||
let VERSION: number;
|
||||
let MIN_COMPATIBLE_VERSION: number;
|
||||
|
||||
// @ts-ignore
|
||||
let currentParseCallback: ((index: number, position: Point) => string) | null = null;
|
||||
// @ts-ignore
|
||||
let currentLogCallback: LogCallback = null;
|
||||
// @ts-ignore
|
||||
let currentProgressCallback: ((percent: number) => void) | null = null;
|
||||
|
||||
export class ParserImpl {
|
||||
protected [0]: number = 0;
|
||||
protected [1]: number = 0;
|
||||
protected language: Language | null = null;
|
||||
protected logCallback: LogCallback = null;
|
||||
static Language: typeof Language;
|
||||
|
||||
static init() {
|
||||
TRANSFER_BUFFER = C._ts_init();
|
||||
VERSION = getValue(TRANSFER_BUFFER, 'i32');
|
||||
MIN_COMPATIBLE_VERSION = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
|
||||
}
|
||||
|
||||
initialize() {
|
||||
C._ts_parser_new_wasm();
|
||||
this[0] = getValue(TRANSFER_BUFFER, 'i32');
|
||||
this[1] = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
|
||||
}
|
||||
|
||||
delete() {
|
||||
C._ts_parser_delete(this[0]);
|
||||
C._free(this[1]);
|
||||
this[0] = 0;
|
||||
this[1] = 0;
|
||||
}
|
||||
|
||||
setLanguage(language: Language | null): this {
|
||||
let address: number;
|
||||
if (!language) {
|
||||
address = 0;
|
||||
this.language = null;
|
||||
} else if (language.constructor === Language) {
|
||||
address = language[0];
|
||||
const version = C._ts_language_version(address);
|
||||
if (version < MIN_COMPATIBLE_VERSION || VERSION < version) {
|
||||
throw new Error(
|
||||
`Incompatible language version ${version}. ` +
|
||||
`Compatibility range ${MIN_COMPATIBLE_VERSION} through ${VERSION}.`
|
||||
);
|
||||
}
|
||||
this.language = language;
|
||||
} else {
|
||||
throw new Error('Argument must be a Language');
|
||||
}
|
||||
|
||||
C._ts_parser_set_language(this[0], address);
|
||||
return this;
|
||||
}
|
||||
|
||||
getLanguage(): Language | null {
|
||||
return this.language;
|
||||
}
|
||||
|
||||
parse(
|
||||
callback: ParseCallback,
|
||||
oldTree?: Tree | null,
|
||||
options: ParseOptions = {}
|
||||
): Tree {
|
||||
if (typeof callback === 'string') {
|
||||
currentParseCallback = (index: number) => callback.slice(index);
|
||||
} else if (typeof callback === 'function') {
|
||||
currentParseCallback = callback;
|
||||
} else {
|
||||
throw new Error('Argument must be a string or a function');
|
||||
}
|
||||
|
||||
if (options?.progressCallback) {
|
||||
currentProgressCallback = options.progressCallback;
|
||||
} else {
|
||||
currentProgressCallback = null;
|
||||
}
|
||||
|
||||
if (this.logCallback) {
|
||||
currentLogCallback = this.logCallback;
|
||||
C._ts_parser_enable_logger_wasm(this[0], 1);
|
||||
} else {
|
||||
currentLogCallback = null;
|
||||
C._ts_parser_enable_logger_wasm(this[0], 0);
|
||||
}
|
||||
|
||||
let rangeCount = 0;
|
||||
let rangeAddress = 0;
|
||||
if (options?.includedRanges) {
|
||||
rangeCount = options.includedRanges.length;
|
||||
rangeAddress = C._calloc(rangeCount, SIZE_OF_RANGE);
|
||||
let address = rangeAddress;
|
||||
for (let i = 0; i < rangeCount; i++) {
|
||||
marshalRange(address, options.includedRanges[i]);
|
||||
address += SIZE_OF_RANGE;
|
||||
}
|
||||
}
|
||||
|
||||
const treeAddress = C._ts_parser_parse_wasm(
|
||||
this[0],
|
||||
this[1],
|
||||
oldTree ? oldTree[0] : 0,
|
||||
rangeAddress,
|
||||
rangeCount
|
||||
);
|
||||
|
||||
if (!treeAddress) {
|
||||
currentParseCallback = null;
|
||||
currentLogCallback = null;
|
||||
currentProgressCallback = null;
|
||||
throw new Error('Parsing failed');
|
||||
}
|
||||
|
||||
if (!this.language) {
|
||||
throw new Error('Parser must have a language to parse');
|
||||
}
|
||||
|
||||
const result = new Tree(INTERNAL, treeAddress, this.language, currentParseCallback);
|
||||
currentParseCallback = null;
|
||||
currentLogCallback = null;
|
||||
currentProgressCallback = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
C._ts_parser_reset(this[0]);
|
||||
}
|
||||
|
||||
getIncludedRanges(): Range[] {
|
||||
C._ts_parser_included_ranges_wasm(this[0]);
|
||||
const count = getValue(TRANSFER_BUFFER, 'i32');
|
||||
const buffer = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
|
||||
const result: Range[] = new Array(count);
|
||||
|
||||
if (count > 0) {
|
||||
let address = buffer;
|
||||
for (let i = 0; i < count; i++) {
|
||||
result[i] = unmarshalRange(address);
|
||||
address += SIZE_OF_RANGE;
|
||||
}
|
||||
C._free(buffer);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getTimeoutMicros(): number {
|
||||
return C._ts_parser_timeout_micros(this[0]);
|
||||
}
|
||||
|
||||
setTimeoutMicros(timeout: number): void {
|
||||
C._ts_parser_set_timeout_micros(this[0], timeout);
|
||||
}
|
||||
|
||||
setLogger(callback: LogCallback): this {
|
||||
if (!callback) {
|
||||
this.logCallback = null;
|
||||
} else if (typeof callback !== 'function') {
|
||||
throw new Error('Logger callback must be a function');
|
||||
} else {
|
||||
this.logCallback = callback;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
getLogger(): LogCallback {
|
||||
return this.logCallback;
|
||||
}
|
||||
}
|
||||
329
lib/binding_web/src/query.ts
Normal file
329
lib/binding_web/src/query.ts
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
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';
|
||||
|
||||
// @ts-ignore
|
||||
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 interface Properties {
|
||||
[key: string]: string | null;
|
||||
}
|
||||
|
||||
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: Array<Capture>) => boolean;
|
||||
|
||||
export class Query {
|
||||
private [0]: number; // Internal handle for WASM
|
||||
private exceededMatchLimit: boolean;
|
||||
private textPredicates: TextPredicate[][];
|
||||
|
||||
readonly captureNames: string[];
|
||||
readonly captureQuantifiers: number[][];
|
||||
readonly predicates: Predicate[][];
|
||||
readonly setProperties: Properties[];
|
||||
readonly assertedProperties: Properties[];
|
||||
readonly refutedProperties: Properties[];
|
||||
matchLimit?: number;
|
||||
|
||||
constructor(
|
||||
internal: Internal,
|
||||
address: number,
|
||||
captureNames: string[],
|
||||
captureQuantifiers: number[][],
|
||||
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: Capture[] = 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];
|
||||
if (setProperties) result[filteredCount].setProperties = setProperties;
|
||||
const assertedProperties = this.assertedProperties[pattern];
|
||||
if (assertedProperties) result[filteredCount].assertedProperties = assertedProperties;
|
||||
const refutedProperties = this.refutedProperties[pattern];
|
||||
if (refutedProperties) 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];
|
||||
if (setProperties) capture.setProperties = setProperties;
|
||||
const assertedProperties = this.assertedProperties[pattern];
|
||||
if (assertedProperties) capture.assertedProperties = assertedProperties;
|
||||
const refutedProperties = this.refutedProperties[pattern];
|
||||
if (refutedProperties) 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;
|
||||
}
|
||||
}
|
||||
115
lib/binding_web/src/tree.ts
Normal file
115
lib/binding_web/src/tree.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import { INTERNAL, Internal, assertInternal, ParseCallback, Point, Range, Edit, SIZE_OF_NODE, SIZE_OF_INT, SIZE_OF_RANGE, C } from './constants';
|
||||
import { Language } from './language';
|
||||
import { Node } from './node';
|
||||
import { TreeCursor } from './tree_cursor';
|
||||
import { marshalEdit, marshalPoint, unmarshalNode, unmarshalRange } from './marshal';
|
||||
import { TRANSFER_BUFFER } from './parser';
|
||||
|
||||
export function getText(tree: Tree, startIndex: number, endIndex: number, startPosition: Point): string {
|
||||
const length = endIndex - startIndex;
|
||||
let result = tree.textCallback(startIndex, startPosition);
|
||||
if (result) {
|
||||
startIndex += result.length;
|
||||
while (startIndex < endIndex) {
|
||||
const string = tree.textCallback(startIndex, startPosition);
|
||||
if (string && string.length > 0) {
|
||||
startIndex += string.length;
|
||||
result += string;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (startIndex > endIndex) {
|
||||
result = result.slice(0, length);
|
||||
}
|
||||
}
|
||||
return result || '';
|
||||
}
|
||||
|
||||
export class Tree {
|
||||
private [0]: number; // Internal handle for WASM
|
||||
|
||||
textCallback: ParseCallback;
|
||||
language: Language;
|
||||
|
||||
constructor(internal: Internal, address: number, language: Language, textCallback: ParseCallback) {
|
||||
assertInternal(internal);
|
||||
this[0] = address;
|
||||
this.language = language;
|
||||
this.textCallback = textCallback;
|
||||
}
|
||||
|
||||
copy(): Tree {
|
||||
const address = C._ts_tree_copy(this[0]);
|
||||
return new Tree(INTERNAL, address, this.language, this.textCallback);
|
||||
}
|
||||
|
||||
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 rootNode(): Node {
|
||||
C._ts_tree_root_node_wasm(this[0]);
|
||||
return unmarshalNode(this)!;
|
||||
}
|
||||
|
||||
rootNodeWithOffset(offsetBytes: number, offsetExtent: Point): Node {
|
||||
const address = TRANSFER_BUFFER + SIZE_OF_NODE;
|
||||
setValue(address, offsetBytes, 'i32');
|
||||
marshalPoint(address + SIZE_OF_INT, offsetExtent);
|
||||
C._ts_tree_root_node_with_offset_wasm(this[0]);
|
||||
return unmarshalNode(this)!;
|
||||
}
|
||||
|
||||
getLanguage(): Language {
|
||||
return this.language;
|
||||
}
|
||||
|
||||
walk(): TreeCursor {
|
||||
return this.rootNode.walk();
|
||||
}
|
||||
|
||||
getChangedRanges(other: Tree): Range[] {
|
||||
if (!(other instanceof Tree)) {
|
||||
throw new TypeError('Argument must be a Tree');
|
||||
}
|
||||
|
||||
C._ts_tree_get_changed_ranges_wasm(this[0], other[0]);
|
||||
const count = getValue(TRANSFER_BUFFER, 'i32');
|
||||
const buffer = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
|
||||
const result = new Array(count);
|
||||
|
||||
if (count > 0) {
|
||||
let address = buffer;
|
||||
for (let i = 0; i < count; i++) {
|
||||
result[i] = unmarshalRange(address);
|
||||
address += SIZE_OF_RANGE;
|
||||
}
|
||||
C._free(buffer);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getIncludedRanges(): Range[] {
|
||||
C._ts_tree_included_ranges_wasm(this[0]);
|
||||
const count = getValue(TRANSFER_BUFFER, 'i32');
|
||||
const buffer = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
|
||||
const result = new Array(count);
|
||||
|
||||
if (count > 0) {
|
||||
let address = buffer;
|
||||
for (let i = 0; i < count; i++) {
|
||||
result[i] = unmarshalRange(address);
|
||||
address += SIZE_OF_RANGE;
|
||||
}
|
||||
C._free(buffer);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
193
lib/binding_web/src/tree_cursor.ts
Normal file
193
lib/binding_web/src/tree_cursor.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import { INTERNAL, Internal, assertInternal, Point, SIZE_OF_NODE, SIZE_OF_CURSOR, C } from './constants';
|
||||
import { marshalNode, marshalPoint, marshalTreeCursor, unmarshalNode, unmarshalPoint, unmarshalTreeCursor } from './marshal';
|
||||
import { Node } from './node';
|
||||
import { TRANSFER_BUFFER } from './parser';
|
||||
import { getText, Tree } from './tree';
|
||||
|
||||
export class TreeCursor {
|
||||
// @ts-ignore
|
||||
private [0]: number; // Internal handle for WASM
|
||||
// @ts-ignore
|
||||
private [1]: number; // Internal handle for WASM
|
||||
// @ts-ignore
|
||||
private [2]: number; // Internal handle for WASM
|
||||
// @ts-ignore
|
||||
private [3]: number; // Internal handle for WASM
|
||||
|
||||
private tree: Tree;
|
||||
|
||||
constructor(internal: Internal, tree: Tree) {
|
||||
assertInternal(internal);
|
||||
this.tree = tree;
|
||||
unmarshalTreeCursor(this);
|
||||
}
|
||||
|
||||
copy(): TreeCursor {
|
||||
const copy = new TreeCursor(INTERNAL, this.tree);
|
||||
C._ts_tree_cursor_copy_wasm(this.tree[0]);
|
||||
unmarshalTreeCursor(copy);
|
||||
return copy;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 nodeType(): string {
|
||||
return this.tree.language.types[this.nodeTypeId] || 'ERROR';
|
||||
}
|
||||
|
||||
get nodeTypeId(): number {
|
||||
marshalTreeCursor(this);
|
||||
return C._ts_tree_cursor_current_node_type_id_wasm(this.tree[0]);
|
||||
}
|
||||
|
||||
get nodeStateId(): number {
|
||||
marshalTreeCursor(this);
|
||||
return C._ts_tree_cursor_current_node_state_id_wasm(this.tree[0]);
|
||||
}
|
||||
|
||||
get nodeId(): number {
|
||||
marshalTreeCursor(this);
|
||||
return C._ts_tree_cursor_current_node_id_wasm(this.tree[0]);
|
||||
}
|
||||
|
||||
get nodeIsNamed(): boolean {
|
||||
marshalTreeCursor(this);
|
||||
return C._ts_tree_cursor_current_node_is_named_wasm(this.tree[0]) === 1;
|
||||
}
|
||||
|
||||
get nodeIsMissing(): boolean {
|
||||
marshalTreeCursor(this);
|
||||
return C._ts_tree_cursor_current_node_is_missing_wasm(this.tree[0]) === 1;
|
||||
}
|
||||
|
||||
get nodeText(): string {
|
||||
marshalTreeCursor(this);
|
||||
const startIndex = C._ts_tree_cursor_start_index_wasm(this.tree[0]);
|
||||
const endIndex = C._ts_tree_cursor_end_index_wasm(this.tree[0]);
|
||||
C._ts_tree_cursor_start_position_wasm(this.tree[0]);
|
||||
const startPosition = unmarshalPoint(TRANSFER_BUFFER);
|
||||
return getText(this.tree, startIndex, endIndex, startPosition);
|
||||
}
|
||||
|
||||
get startPosition(): Point {
|
||||
marshalTreeCursor(this);
|
||||
C._ts_tree_cursor_start_position_wasm(this.tree[0]);
|
||||
return unmarshalPoint(TRANSFER_BUFFER);
|
||||
}
|
||||
|
||||
get endPosition(): Point {
|
||||
marshalTreeCursor(this);
|
||||
C._ts_tree_cursor_end_position_wasm(this.tree[0]);
|
||||
return unmarshalPoint(TRANSFER_BUFFER);
|
||||
}
|
||||
|
||||
get startIndex(): number {
|
||||
marshalTreeCursor(this);
|
||||
return C._ts_tree_cursor_start_index_wasm(this.tree[0]);
|
||||
}
|
||||
|
||||
get endIndex(): number {
|
||||
marshalTreeCursor(this);
|
||||
return C._ts_tree_cursor_end_index_wasm(this.tree[0]);
|
||||
}
|
||||
|
||||
get currentNode(): Node | null {
|
||||
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]);
|
||||
}
|
||||
|
||||
gotoFirstChild(): boolean {
|
||||
marshalTreeCursor(this);
|
||||
const result = C._ts_tree_cursor_goto_first_child_wasm(this.tree[0]);
|
||||
unmarshalTreeCursor(this);
|
||||
return result === 1;
|
||||
}
|
||||
|
||||
gotoLastChild(): boolean {
|
||||
marshalTreeCursor(this);
|
||||
const result = C._ts_tree_cursor_goto_last_child_wasm(this.tree[0]);
|
||||
unmarshalTreeCursor(this);
|
||||
return result === 1;
|
||||
}
|
||||
|
||||
gotoFirstChildForIndex(goalIndex: number): boolean {
|
||||
marshalTreeCursor(this);
|
||||
setValue(TRANSFER_BUFFER + SIZE_OF_CURSOR, goalIndex, 'i32');
|
||||
const result = C._ts_tree_cursor_goto_first_child_for_index_wasm(this.tree[0]);
|
||||
unmarshalTreeCursor(this);
|
||||
return result === 1;
|
||||
}
|
||||
|
||||
gotoFirstChildForPosition(goalPosition: Point): boolean {
|
||||
marshalTreeCursor(this);
|
||||
marshalPoint(TRANSFER_BUFFER + SIZE_OF_CURSOR, goalPosition);
|
||||
const result = C._ts_tree_cursor_goto_first_child_for_position_wasm(this.tree[0]);
|
||||
unmarshalTreeCursor(this);
|
||||
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);
|
||||
unmarshalTreeCursor(this);
|
||||
}
|
||||
|
||||
gotoParent(): boolean {
|
||||
marshalTreeCursor(this);
|
||||
const result = C._ts_tree_cursor_goto_parent_wasm(this.tree[0]);
|
||||
unmarshalTreeCursor(this);
|
||||
return result === 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
const Parser = require('..');
|
||||
|
||||
function languageURL(name) {
|
||||
return require.resolve(`../../../target/release/tree-sitter-${name}.wasm`);
|
||||
}
|
||||
|
||||
module.exports = Parser.init().then(async () => ({
|
||||
Parser,
|
||||
languageURL,
|
||||
C: await Parser.Language.load(languageURL('c')),
|
||||
EmbeddedTemplate: await Parser.Language.load(languageURL('embedded-template')),
|
||||
HTML: await Parser.Language.load(languageURL('html')),
|
||||
JavaScript: await Parser.Language.load(languageURL('javascript')),
|
||||
JSON: await Parser.Language.load(languageURL('json')),
|
||||
Python: await Parser.Language.load(languageURL('python')),
|
||||
Rust: await Parser.Language.load(languageURL('rust')),
|
||||
}));
|
||||
23
lib/binding_web/test/helper.ts
Normal file
23
lib/binding_web/test/helper.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import TSParser from "web-tree-sitter";
|
||||
|
||||
// @ts-ignore
|
||||
const Parser: typeof TSParser = await import('..').then(m => m.default);
|
||||
|
||||
// https://github.com/tree-sitter/tree-sitter/blob/master/xtask/src/fetch.rs#L15
|
||||
type LanguageName = "bash" | "c" | "cpp" | "embedded-template" | "go" | "html" | "java" | "javascript" | "jsdoc" | "json" | "php" | "python" | "ruby" | "rust" | "typescript";
|
||||
|
||||
function languageURL(name: LanguageName): string {
|
||||
return new URL(`../../../target/release/tree-sitter-${name}.wasm`, import.meta.url).pathname;
|
||||
}
|
||||
|
||||
export default Parser.init().then(async () => ({
|
||||
Parser,
|
||||
languageURL,
|
||||
C: await Parser.Language.load(languageURL('c')),
|
||||
EmbeddedTemplate: await Parser.Language.load(languageURL('embedded-template')),
|
||||
HTML: await Parser.Language.load(languageURL('html')),
|
||||
JavaScript: await Parser.Language.load(languageURL('javascript')),
|
||||
JSON: await Parser.Language.load(languageURL('json')),
|
||||
Python: await Parser.Language.load(languageURL('python')),
|
||||
Rust: await Parser.Language.load(languageURL('rust')),
|
||||
}));
|
||||
|
|
@ -1,13 +1,17 @@
|
|||
const {assert} = require('chai');
|
||||
let JavaScript;
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import helper from './helper';
|
||||
import TSParser, { type LookaheadIterable, type Language } from 'web-tree-sitter';
|
||||
|
||||
let JavaScript: Language;
|
||||
let Rust: Language;
|
||||
|
||||
describe('Language', () => {
|
||||
before(async () => ({JavaScript, Rust} = await require('./helper')));
|
||||
beforeAll(async () => ({ JavaScript, Rust } = await helper));
|
||||
|
||||
describe('.name, .version', () => {
|
||||
it('returns the name and version of the language', () => {
|
||||
assert.equal('javascript', JavaScript.name);
|
||||
assert.equal(15, JavaScript.version);
|
||||
expect(JavaScript.name).toBe('javascript');
|
||||
expect(JavaScript.version).toBe(15);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -16,16 +20,16 @@ describe('Language', () => {
|
|||
const nameId = JavaScript.fieldIdForName('name');
|
||||
const bodyId = JavaScript.fieldIdForName('body');
|
||||
|
||||
assert.isBelow(nameId, JavaScript.fieldCount);
|
||||
assert.isBelow(bodyId, JavaScript.fieldCount);
|
||||
assert.equal('name', JavaScript.fieldNameForId(nameId));
|
||||
assert.equal('body', JavaScript.fieldNameForId(bodyId));
|
||||
expect(nameId).toBeLessThan(JavaScript.fieldCount);
|
||||
expect(bodyId).toBeLessThan(JavaScript.fieldCount);
|
||||
expect(JavaScript.fieldNameForId(nameId!)).toBe('name');
|
||||
expect(JavaScript.fieldNameForId(bodyId!)).toBe('body');
|
||||
});
|
||||
|
||||
it('handles invalid inputs', () => {
|
||||
assert.equal(null, JavaScript.fieldIdForName('namezzz'));
|
||||
assert.equal(null, JavaScript.fieldNameForId(-1));
|
||||
assert.equal(null, JavaScript.fieldNameForId(10000));
|
||||
expect(JavaScript.fieldIdForName('namezzz')).toBeNull();
|
||||
expect(JavaScript.fieldNameForId(-3)).toBeNull();
|
||||
expect(JavaScript.fieldNameForId(10000)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -34,18 +38,18 @@ describe('Language', () => {
|
|||
const exportStatementId = JavaScript.idForNodeType('export_statement', true);
|
||||
const starId = JavaScript.idForNodeType('*', false);
|
||||
|
||||
assert.isBelow(exportStatementId, JavaScript.nodeTypeCount);
|
||||
assert.isBelow(starId, JavaScript.nodeTypeCount);
|
||||
assert.equal(true, JavaScript.nodeTypeIsNamed(exportStatementId));
|
||||
assert.equal('export_statement', JavaScript.nodeTypeForId(exportStatementId));
|
||||
assert.equal(false, JavaScript.nodeTypeIsNamed(starId));
|
||||
assert.equal('*', JavaScript.nodeTypeForId(starId));
|
||||
expect(exportStatementId).toBeLessThan(JavaScript.nodeTypeCount);
|
||||
expect(starId).toBeLessThan(JavaScript.nodeTypeCount);
|
||||
expect(JavaScript.nodeTypeIsNamed(exportStatementId!)).toBe(true);
|
||||
expect(JavaScript.nodeTypeForId(exportStatementId!)).toBe('export_statement');
|
||||
expect(JavaScript.nodeTypeIsNamed(starId!)).toBe(false);
|
||||
expect(JavaScript.nodeTypeForId(starId!)).toBe('*');
|
||||
});
|
||||
|
||||
it('handles invalid inputs', () => {
|
||||
assert.equal(null, JavaScript.nodeTypeForId(-1));
|
||||
assert.equal(null, JavaScript.nodeTypeForId(10000));
|
||||
assert.equal(null, JavaScript.idForNodeType('export_statement', false));
|
||||
expect(JavaScript.nodeTypeForId(-3)).toBeNull();
|
||||
expect(JavaScript.nodeTypeForId(10000)).toBeNull();
|
||||
expect(JavaScript.idForNodeType('export_statement', false)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -53,19 +57,23 @@ describe('Language', () => {
|
|||
it('gets the supertypes and subtypes of a parser', () => {
|
||||
const supertypes = Rust.supertypes;
|
||||
const names = supertypes.map((id) => Rust.nodeTypeForId(id));
|
||||
assert.deepStrictEqual(
|
||||
names,
|
||||
['_expression', '_literal', '_literal_pattern', '_pattern', '_type'],
|
||||
);
|
||||
expect(names).toEqual([
|
||||
'_expression',
|
||||
'_literal',
|
||||
'_literal_pattern',
|
||||
'_pattern',
|
||||
'_type'
|
||||
]);
|
||||
|
||||
for (const id of supertypes) {
|
||||
const name = Rust.nodeTypeForId(id);
|
||||
const subtypes = Rust.subtypes(id);
|
||||
let subtypeNames = subtypes.map((id) => Rust.nodeTypeForId(id));
|
||||
subtypeNames = [...new Set(subtypeNames)].sort(); // Remove duplicates & sort
|
||||
|
||||
switch (name) {
|
||||
case '_literal':
|
||||
assert.deepStrictEqual(subtypeNames, [
|
||||
expect(subtypeNames).toEqual([
|
||||
'boolean_literal',
|
||||
'char_literal',
|
||||
'float_literal',
|
||||
|
|
@ -75,7 +83,7 @@ describe('Language', () => {
|
|||
]);
|
||||
break;
|
||||
case '_pattern':
|
||||
assert.deepStrictEqual(subtypeNames, [
|
||||
expect(subtypeNames).toEqual([
|
||||
'_',
|
||||
'_literal_pattern',
|
||||
'captured_pattern',
|
||||
|
|
@ -96,7 +104,7 @@ describe('Language', () => {
|
|||
]);
|
||||
break;
|
||||
case '_type':
|
||||
assert.deepStrictEqual(subtypeNames, [
|
||||
expect(subtypeNames).toEqual([
|
||||
'abstract_type',
|
||||
'array_type',
|
||||
'bounded_type',
|
||||
|
|
@ -116,8 +124,6 @@ describe('Language', () => {
|
|||
'unit_type',
|
||||
]);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -125,44 +131,45 @@ describe('Language', () => {
|
|||
});
|
||||
|
||||
describe('Lookahead iterator', () => {
|
||||
let lookahead;
|
||||
let state;
|
||||
before(async () => {
|
||||
let Parser;
|
||||
({JavaScript, Parser} = await require('./helper'));
|
||||
const parser = new Parser().setLanguage(JavaScript);
|
||||
let lookahead: LookaheadIterable;
|
||||
let state: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
let Parser: typeof TSParser;
|
||||
({ JavaScript, Parser } = await helper);
|
||||
const parser = new Parser();
|
||||
parser.setLanguage(JavaScript);
|
||||
const tree = parser.parse('function fn() {}');
|
||||
parser.delete();
|
||||
const cursor = tree.walk();
|
||||
assert(cursor.gotoFirstChild());
|
||||
assert(cursor.gotoFirstChild());
|
||||
expect(cursor.gotoFirstChild()).toBe(true);
|
||||
expect(cursor.gotoFirstChild()).toBe(true);
|
||||
state = cursor.currentNode.nextParseState;
|
||||
lookahead = JavaScript.lookaheadIterator(state);
|
||||
assert.exists(lookahead);
|
||||
lookahead = JavaScript.lookaheadIterator(state)!;
|
||||
expect(lookahead).toBeDefined();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
lookahead.delete();
|
||||
});
|
||||
afterAll(() => lookahead.delete());
|
||||
|
||||
const expected = ['(', 'identifier', '*', 'formal_parameters', 'html_comment', 'comment'];
|
||||
|
||||
it('should iterate over valid symbols in the state', () => {
|
||||
const symbols = Array.from(lookahead);
|
||||
assert.includeMembers(symbols, expected);
|
||||
assert.lengthOf(symbols, expected.length);
|
||||
expect(symbols).toEqual(expect.arrayContaining(expected));
|
||||
expect(symbols).toHaveLength(expected.length);
|
||||
});
|
||||
|
||||
it('should reset to the initial state', () => {
|
||||
assert(lookahead.resetState(state));
|
||||
expect(lookahead.resetState(state)).toBe(true);
|
||||
const symbols = Array.from(lookahead);
|
||||
assert.includeMembers(symbols, expected);
|
||||
assert.lengthOf(symbols, expected.length);
|
||||
expect(symbols).toEqual(expect.arrayContaining(expected));
|
||||
expect(symbols).toHaveLength(expected.length);
|
||||
});
|
||||
|
||||
it('should reset', () => {
|
||||
assert(lookahead.reset(JavaScript, state));
|
||||
expect(lookahead.reset(JavaScript, state)).toBe(true);
|
||||
const symbols = Array.from(lookahead);
|
||||
assert.includeMembers(symbols, expected);
|
||||
assert.lengthOf(symbols, expected.length);
|
||||
expect(symbols).toEqual(expect.arrayContaining(expected));
|
||||
expect(symbols).toHaveLength(expected.length);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,693 +0,0 @@
|
|||
const {assert} = require('chai');
|
||||
let Parser; let C; let JavaScript; let JSON; let EmbeddedTemplate; let Python;
|
||||
|
||||
const JSON_EXAMPLE = `
|
||||
|
||||
[
|
||||
123,
|
||||
false,
|
||||
{
|
||||
"x": null
|
||||
}
|
||||
]
|
||||
`;
|
||||
|
||||
function getAllNodes(tree) {
|
||||
const result = [];
|
||||
let visitedChildren = false;
|
||||
const cursor = tree.walk();
|
||||
while (true) {
|
||||
if (!visitedChildren) {
|
||||
result.push(cursor.currentNode);
|
||||
if (!cursor.gotoFirstChild()) {
|
||||
visitedChildren = true;
|
||||
}
|
||||
} else if (cursor.gotoNextSibling()) {
|
||||
visitedChildren = false;
|
||||
} else if (!cursor.gotoParent()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
describe('Node', () => {
|
||||
let parser; let tree;
|
||||
|
||||
before(async () =>
|
||||
({Parser, C, EmbeddedTemplate, JavaScript, JSON, Python} = await require('./helper')),
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
tree = null;
|
||||
parser = new Parser().setLanguage(JavaScript);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
parser.delete();
|
||||
tree.delete();
|
||||
});
|
||||
|
||||
describe('.children', () => {
|
||||
it('returns an array of child nodes', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
assert.equal(1, tree.rootNode.children.length);
|
||||
const sumNode = tree.rootNode.firstChild.firstChild;
|
||||
assert.deepEqual(
|
||||
sumNode.children.map((child) => child.type),
|
||||
['identifier', '+', 'number'],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.namedChildren', () => {
|
||||
it('returns an array of named child nodes', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild.firstChild;
|
||||
assert.equal(1, tree.rootNode.namedChildren.length);
|
||||
assert.deepEqual(
|
||||
['identifier', 'number'],
|
||||
sumNode.namedChildren.map((child) => child.type),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.childrenForFieldName', () => {
|
||||
it('returns an array of child nodes for the given field name', () => {
|
||||
parser.setLanguage(Python);
|
||||
const source = `
|
||||
if one:
|
||||
a()
|
||||
elif two:
|
||||
b()
|
||||
elif three:
|
||||
c()
|
||||
elif four:
|
||||
d()`;
|
||||
|
||||
tree = parser.parse(source);
|
||||
const node = tree.rootNode.firstChild;
|
||||
assert.equal(node.type, 'if_statement');
|
||||
const alternatives = node.childrenForFieldName('alternative');
|
||||
const alternativeTexts = alternatives.map((n) => {
|
||||
const condition = n.childForFieldName('condition');
|
||||
return source.slice(condition.startIndex, condition.endIndex);
|
||||
});
|
||||
assert.deepEqual(alternativeTexts, ['two', 'three', 'four']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.startIndex and .endIndex', () => {
|
||||
it('returns the character index where the node starts/ends in the text', () => {
|
||||
tree = parser.parse('a👍👎1 / b👎c👎');
|
||||
const quotientNode = tree.rootNode.firstChild.firstChild;
|
||||
|
||||
assert.equal(0, quotientNode.startIndex);
|
||||
assert.equal(15, quotientNode.endIndex);
|
||||
assert.deepEqual(
|
||||
[0, 7, 9],
|
||||
quotientNode.children.map((child) => child.startIndex),
|
||||
);
|
||||
assert.deepEqual(
|
||||
[6, 8, 15],
|
||||
quotientNode.children.map((child) => child.endIndex),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.startPosition and .endPosition', () => {
|
||||
it('returns the row and column where the node starts/ends in the text', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild.firstChild;
|
||||
assert.equal('binary_expression', sumNode.type);
|
||||
|
||||
assert.deepEqual({row: 0, column: 0}, sumNode.startPosition);
|
||||
assert.deepEqual({row: 0, column: 10}, sumNode.endPosition);
|
||||
assert.deepEqual(
|
||||
[{row: 0, column: 0}, {row: 0, column: 4}, {row: 0, column: 6}],
|
||||
sumNode.children.map((child) => child.startPosition),
|
||||
);
|
||||
assert.deepEqual(
|
||||
[{row: 0, column: 3}, {row: 0, column: 5}, {row: 0, column: 10}],
|
||||
sumNode.children.map((child) => child.endPosition),
|
||||
);
|
||||
});
|
||||
|
||||
it('handles characters that occupy two UTF16 code units', () => {
|
||||
tree = parser.parse('a👍👎1 /\n b👎c👎');
|
||||
const sumNode = tree.rootNode.firstChild.firstChild;
|
||||
assert.deepEqual(
|
||||
[
|
||||
[{row: 0, column: 0}, {row: 0, column: 6}],
|
||||
[{row: 0, column: 7}, {row: 0, column: 8}],
|
||||
[{row: 1, column: 1}, {row: 1, column: 7}],
|
||||
],
|
||||
sumNode.children.map((child) => [child.startPosition, child.endPosition]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.parent', () => {
|
||||
it('returns the node\'s parent', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild;
|
||||
const variableNode = sumNode.firstChild;
|
||||
assert.notEqual(sumNode.id, variableNode.id);
|
||||
assert.equal(sumNode.id, variableNode.parent.id);
|
||||
assert.equal(tree.rootNode.id, sumNode.parent.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.child(), .firstChild, .lastChild', () => {
|
||||
it('returns null when the node has no children', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild.firstChild;
|
||||
const variableNode = sumNode.firstChild;
|
||||
assert.equal(variableNode.firstChild, null);
|
||||
assert.equal(variableNode.lastChild, null);
|
||||
assert.equal(variableNode.firstNamedChild, null);
|
||||
assert.equal(variableNode.lastNamedChild, null);
|
||||
assert.equal(variableNode.child(1), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.childForFieldName()', () => {
|
||||
it('returns null when the node has no children', () => {
|
||||
tree = parser.parse('class A { b() {} }');
|
||||
|
||||
const classNode = tree.rootNode.firstChild;
|
||||
assert.equal(classNode.type, 'class_declaration');
|
||||
|
||||
const classNameNode = classNode.childForFieldName('name');
|
||||
assert.equal(classNameNode.type, 'identifier');
|
||||
assert.equal(classNameNode.text, 'A');
|
||||
|
||||
const bodyNode = classNode.childForFieldName('body');
|
||||
assert.equal(bodyNode.type, 'class_body');
|
||||
assert.equal(bodyNode.text, '{ b() {} }');
|
||||
|
||||
const methodNode = bodyNode.firstNamedChild;
|
||||
assert.equal(methodNode.type, 'method_definition');
|
||||
assert.equal(methodNode.text, 'b() {}');
|
||||
|
||||
const methodNameNode = methodNode.childForFieldName('name');
|
||||
assert.equal(methodNameNode.type, 'property_identifier');
|
||||
assert.equal(methodNameNode.text, 'b');
|
||||
|
||||
const paramsNode = methodNode.childForFieldName('parameters');
|
||||
assert.equal(paramsNode.type, 'formal_parameters');
|
||||
assert.equal(paramsNode.text, '()');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.nextSibling and .previousSibling', () => {
|
||||
it('returns the node\'s next and previous sibling', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild.firstChild;
|
||||
assert.equal(sumNode.children[1].id, sumNode.children[0].nextSibling.id);
|
||||
assert.equal(sumNode.children[2].id, sumNode.children[1].nextSibling.id);
|
||||
assert.equal(
|
||||
sumNode.children[0].id,
|
||||
sumNode.children[1].previousSibling.id,
|
||||
);
|
||||
assert.equal(
|
||||
sumNode.children[1].id,
|
||||
sumNode.children[2].previousSibling.id,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.nextNamedSibling and .previousNamedSibling', () => {
|
||||
it('returns the node\'s next and previous named sibling', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild.firstChild;
|
||||
assert.equal(
|
||||
sumNode.namedChildren[1].id,
|
||||
sumNode.namedChildren[0].nextNamedSibling.id,
|
||||
);
|
||||
assert.equal(
|
||||
sumNode.namedChildren[0].id,
|
||||
sumNode.namedChildren[1].previousNamedSibling.id,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.descendantForIndex(min, max)', () => {
|
||||
it('returns the smallest node that spans the given range', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild.firstChild;
|
||||
assert.equal('identifier', sumNode.descendantForIndex(1, 2).type);
|
||||
assert.equal('+', sumNode.descendantForIndex(4, 4).type);
|
||||
|
||||
assert.throws(() => {
|
||||
sumNode.descendantForIndex(1, {});
|
||||
}, 'Arguments must be numbers');
|
||||
|
||||
assert.throws(() => {
|
||||
sumNode.descendantForIndex();
|
||||
}, 'Arguments must be numbers');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.namedDescendantForIndex', () => {
|
||||
it('returns the smallest node that spans the given range', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild;
|
||||
assert.equal('identifier', sumNode.descendantForIndex(1, 2).type);
|
||||
assert.equal('+', sumNode.descendantForIndex(4, 4).type);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.descendantForPosition(min, max)', () => {
|
||||
it('returns the smallest node that spans the given range', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild;
|
||||
|
||||
assert.equal(
|
||||
'identifier',
|
||||
sumNode.descendantForPosition(
|
||||
{row: 0, column: 1},
|
||||
{row: 0, column: 2},
|
||||
).type,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
'+',
|
||||
sumNode.descendantForPosition({row: 0, column: 4}).type,
|
||||
);
|
||||
|
||||
assert.throws(() => {
|
||||
sumNode.descendantForPosition(1, {});
|
||||
}, 'Arguments must be {row, column} objects');
|
||||
|
||||
assert.throws(() => {
|
||||
sumNode.descendantForPosition();
|
||||
}, 'Arguments must be {row, column} objects');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.namedDescendantForPosition(min, max)', () => {
|
||||
it('returns the smallest named node that spans the given range', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild;
|
||||
|
||||
assert.equal(
|
||||
sumNode.namedDescendantForPosition(
|
||||
{row: 0, column: 1},
|
||||
{row: 0, column: 2},
|
||||
).type,
|
||||
'identifier',
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
sumNode.namedDescendantForPosition({row: 0, column: 4}).type,
|
||||
'binary_expression',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.hasError', () => {
|
||||
it('returns true if the node contains an error', () => {
|
||||
tree = parser.parse('1 + 2 * * 3');
|
||||
const node = tree.rootNode;
|
||||
assert.equal(
|
||||
node.toString(),
|
||||
'(program (expression_statement (binary_expression left: (number) right: (binary_expression left: (number) (ERROR) right: (number)))))',
|
||||
);
|
||||
|
||||
const sum = node.firstChild.firstChild;
|
||||
assert(sum.hasError);
|
||||
assert(!sum.children[0].hasError);
|
||||
assert(!sum.children[1].hasError);
|
||||
assert(sum.children[2].hasError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isError', () => {
|
||||
it('returns true if the node is an error', () => {
|
||||
tree = parser.parse('2 * * 3');
|
||||
const node = tree.rootNode;
|
||||
assert.equal(
|
||||
node.toString(),
|
||||
'(program (expression_statement (binary_expression left: (number) (ERROR) right: (number))))',
|
||||
);
|
||||
|
||||
const multi = node.firstChild.firstChild;
|
||||
assert(multi.hasError);
|
||||
assert(!multi.children[0].isError);
|
||||
assert(!multi.children[1].isError);
|
||||
assert(multi.children[2].isError);
|
||||
assert(!multi.children[3].isError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isMissing', () => {
|
||||
it('returns true if the node is missing from the source and was inserted via error recovery', () => {
|
||||
tree = parser.parse('(2 ||)');
|
||||
const node = tree.rootNode;
|
||||
assert.equal(
|
||||
node.toString(),
|
||||
'(program (expression_statement (parenthesized_expression (binary_expression left: (number) right: (MISSING identifier)))))',
|
||||
);
|
||||
|
||||
const sum = node.firstChild.firstChild.firstNamedChild;
|
||||
assert.equal(sum.type, 'binary_expression');
|
||||
assert(sum.hasError);
|
||||
assert(!sum.children[0].isMissing);
|
||||
assert(!sum.children[1].isMissing);
|
||||
assert(sum.children[2].isMissing);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isExtra', () => {
|
||||
it('returns true if the node is an extra node like comments', () => {
|
||||
tree = parser.parse('foo(/* hi */);');
|
||||
const node = tree.rootNode;
|
||||
const commentNode = node.descendantForIndex(7, 7);
|
||||
|
||||
assert.equal(node.type, 'program');
|
||||
assert.equal(commentNode.type, 'comment');
|
||||
assert(!node.isExtra);
|
||||
assert(commentNode.isExtra);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.text', () => {
|
||||
const text = 'α0 / b👎c👎';
|
||||
|
||||
Object.entries({
|
||||
'.parse(String)': text,
|
||||
'.parse(Function)': (offset) => text.slice(offset, 4),
|
||||
}).forEach(([method, _parse]) =>
|
||||
it(`returns the text of a node generated by ${method}`, async () => {
|
||||
const [numeratorSrc, denominatorSrc] = text.split(/\s*\/\s+/);
|
||||
tree = await parser.parse(text);
|
||||
const quotientNode = tree.rootNode.firstChild.firstChild;
|
||||
const [numerator, slash, denominator] = quotientNode.children;
|
||||
|
||||
assert.equal(text, tree.rootNode.text, 'root node text');
|
||||
assert.equal(denominatorSrc, denominator.text, 'denominator text');
|
||||
assert.equal(text, quotientNode.text, 'quotient text');
|
||||
assert.equal(numeratorSrc, numerator.text, 'numerator text');
|
||||
assert.equal('/', slash.text, '"/" text');
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('.descendantCount', () => {
|
||||
it('returns the number of descendants', () => {
|
||||
parser.setLanguage(JSON);
|
||||
tree = parser.parse(JSON_EXAMPLE);
|
||||
const valueNode = tree.rootNode;
|
||||
const allNodes = getAllNodes(tree);
|
||||
|
||||
assert.equal(valueNode.descendantCount, allNodes.length);
|
||||
|
||||
const cursor = tree.walk();
|
||||
for (let i = 0; i < allNodes.length; i++) {
|
||||
const node = allNodes[i];
|
||||
cursor.gotoDescendant(i);
|
||||
assert.equal(cursor.currentNode.id, node.id, `index ${i}`);
|
||||
}
|
||||
|
||||
for (let i = allNodes.length - 1; i >= 0; i--) {
|
||||
const node = allNodes[i];
|
||||
cursor.gotoDescendant(i);
|
||||
assert.equal(cursor.currentNode.id, node.id, `rev index ${i}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('tests a single node tree', () => {
|
||||
parser.setLanguage(EmbeddedTemplate);
|
||||
tree = parser.parse('hello');
|
||||
|
||||
const nodes = getAllNodes(tree);
|
||||
assert.equal(nodes.length, 2);
|
||||
assert.equal(tree.rootNode.descendantCount, 2);
|
||||
|
||||
const cursor = tree.walk();
|
||||
|
||||
cursor.gotoDescendant(0);
|
||||
assert.equal(cursor.currentDepth, 0);
|
||||
assert.equal(cursor.currentNode.id, nodes[0].id);
|
||||
|
||||
cursor.gotoDescendant(1);
|
||||
assert.equal(cursor.currentDepth, 1);
|
||||
assert.equal(cursor.currentNode.id, nodes[1].id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.rootNodeWithOffset', () => {
|
||||
it('returns the root node of the tree, offset by the given byte offset', () => {
|
||||
tree = parser.parse(' if (a) b');
|
||||
const node = tree.rootNodeWithOffset(6, {row: 2, column: 2});
|
||||
assert.equal(node.startIndex, 8);
|
||||
assert.equal(node.endIndex, 16);
|
||||
assert.deepEqual(node.startPosition, {row: 2, column: 4});
|
||||
assert.deepEqual(node.endPosition, {row: 2, column: 12});
|
||||
|
||||
let child = node.firstChild.child(2);
|
||||
assert.equal(child.type, 'expression_statement');
|
||||
assert.equal(child.startIndex, 15);
|
||||
assert.equal(child.endIndex, 16);
|
||||
assert.deepEqual(child.startPosition, {row: 2, column: 11});
|
||||
assert.deepEqual(child.endPosition, {row: 2, column: 12});
|
||||
|
||||
const cursor = node.walk();
|
||||
cursor.gotoFirstChild();
|
||||
cursor.gotoFirstChild();
|
||||
cursor.gotoNextSibling();
|
||||
child = cursor.currentNode;
|
||||
assert.equal(child.type, 'parenthesized_expression');
|
||||
assert.equal(child.startIndex, 11);
|
||||
assert.equal(child.endIndex, 14);
|
||||
assert.deepEqual(child.startPosition, {row: 2, column: 7});
|
||||
assert.deepEqual(child.endPosition, {row: 2, column: 10});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.parseState, .nextParseState', () => {
|
||||
const text = '10 / 5';
|
||||
|
||||
it('returns node parse state ids', async () => {
|
||||
tree = await parser.parse(text);
|
||||
const quotientNode = tree.rootNode.firstChild.firstChild;
|
||||
const [numerator, slash, denominator] = quotientNode.children;
|
||||
|
||||
assert.equal(tree.rootNode.parseState, 0);
|
||||
// parse states will change on any change to the grammar so test that it
|
||||
// returns something instead
|
||||
assert.isAbove(numerator.parseState, 0);
|
||||
assert.isAbove(slash.parseState, 0);
|
||||
assert.isAbove(denominator.parseState, 0);
|
||||
});
|
||||
|
||||
it('returns next parse state equal to the language', async () => {
|
||||
tree = await parser.parse(text);
|
||||
const quotientNode = tree.rootNode.firstChild.firstChild;
|
||||
quotientNode.children.forEach((node) => {
|
||||
assert.equal(
|
||||
node.nextParseState,
|
||||
JavaScript.nextState(node.parseState, node.grammarId),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.descendantsOfType("ERROR", null, null)', () => {
|
||||
it('finds all of the descendants of an ERROR node', () => {
|
||||
tree = parser.parse(
|
||||
`if ({a: 'b'} {c: 'd'}) {
|
||||
// ^ ERROR
|
||||
x = function(a) { b; } function(c) { d; }
|
||||
}`
|
||||
);
|
||||
const errorNode = tree.rootNode;
|
||||
let descendants = errorNode.descendantsOfType('ERROR', null, null);
|
||||
assert.deepEqual(
|
||||
descendants.map((node) => node.startIndex),
|
||||
[4],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.descendantsOfType(type, min, max)', () => {
|
||||
it('finds all of the descendants of the given type in the given range', () => {
|
||||
tree = parser.parse('a + 1 * b * 2 + c + 3');
|
||||
const outerSum = tree.rootNode.firstChild.firstChild;
|
||||
let descendants = outerSum.descendantsOfType('number', {row: 0, column: 2}, {row: 0, column: 15});
|
||||
assert.deepEqual(
|
||||
descendants.map((node) => node.startIndex),
|
||||
[4, 12],
|
||||
);
|
||||
assert.deepEqual(
|
||||
descendants.map((node) => node.endPosition),
|
||||
[{row: 0, column: 5}, {row: 0, column: 13}],
|
||||
);
|
||||
|
||||
descendants = outerSum.descendantsOfType('identifier', {row: 0, column: 2}, {row: 0, column: 15});
|
||||
assert.deepEqual(
|
||||
descendants.map((node) => node.startIndex),
|
||||
[8],
|
||||
);
|
||||
|
||||
descendants = outerSum.descendantsOfType('identifier', {row: 0, column: 0}, {row: 0, column: 30});
|
||||
assert.deepEqual(
|
||||
descendants.map((node) => node.startIndex),
|
||||
[0, 8, 16],
|
||||
);
|
||||
|
||||
descendants = outerSum.descendantsOfType('number', {row: 0, column: 0}, {row: 0, column: 30});
|
||||
assert.deepEqual(
|
||||
descendants.map((node) => node.startIndex),
|
||||
[4, 12, 20],
|
||||
);
|
||||
|
||||
descendants = outerSum.descendantsOfType(
|
||||
['identifier', 'number'],
|
||||
{row: 0, column: 0},
|
||||
{row: 0, column: 30},
|
||||
);
|
||||
assert.deepEqual(
|
||||
descendants.map((node) => node.startIndex),
|
||||
[0, 4, 8, 12, 16, 20],
|
||||
);
|
||||
|
||||
descendants = outerSum.descendantsOfType('number');
|
||||
assert.deepEqual(
|
||||
descendants.map((node) => node.startIndex),
|
||||
[4, 12, 20],
|
||||
);
|
||||
|
||||
descendants = outerSum.firstChild.descendantsOfType('number', {row: 0, column: 0}, {row: 0, column: 30});
|
||||
assert.deepEqual(
|
||||
descendants.map((node) => node.startIndex),
|
||||
[4, 12],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('.closest(type)', () => {
|
||||
it('returns the closest ancestor of the given type', () => {
|
||||
tree = parser.parse('a(b + -d.e)');
|
||||
const property = tree.rootNode.descendantForIndex('a(b + -d.'.length);
|
||||
assert.equal(property.type, 'property_identifier');
|
||||
|
||||
const unary = property.closest('unary_expression');
|
||||
assert.equal(unary.type, 'unary_expression');
|
||||
assert.equal(unary.startIndex, 'a(b + '.length);
|
||||
assert.equal(unary.endIndex, 'a(b + -d.e'.length);
|
||||
|
||||
const sum = property.closest(['binary_expression', 'call_expression']);
|
||||
assert.equal(sum.type, 'binary_expression');
|
||||
assert.equal(sum.startIndex, 2);
|
||||
assert.equal(sum.endIndex, 'a(b + -d.e'.length);
|
||||
});
|
||||
|
||||
it('throws an exception when an invalid argument is given', () => {
|
||||
tree = parser.parse('a + 1 * b * 2 + c + 3');
|
||||
const number = tree.rootNode.descendantForIndex(4);
|
||||
|
||||
assert.throws(() => number.closest({a: 1}), /Argument must be a string or array of strings/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.firstChildForIndex(index)', () => {
|
||||
it('returns the first child that contains or starts after the given index', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild.firstChild;
|
||||
|
||||
assert.equal('identifier', sumNode.firstChildForIndex(0).type);
|
||||
assert.equal('identifier', sumNode.firstChildForIndex(1).type);
|
||||
assert.equal('+', sumNode.firstChildForIndex(3).type);
|
||||
assert.equal('number', sumNode.firstChildForIndex(5).type);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.firstNamedChildForIndex(index)', () => {
|
||||
it('returns the first child that contains or starts after the given index', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild.firstChild;
|
||||
|
||||
assert.equal('identifier', sumNode.firstNamedChildForIndex(0).type);
|
||||
assert.equal('identifier', sumNode.firstNamedChildForIndex(1).type);
|
||||
assert.equal('number', sumNode.firstNamedChildForIndex(3).type);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.equals(other)', () => {
|
||||
it('returns true if the nodes are the same', () => {
|
||||
tree = parser.parse('1 + 2');
|
||||
|
||||
const sumNode = tree.rootNode.firstChild.firstChild;
|
||||
const node1 = sumNode.firstChild;
|
||||
const node2 = sumNode.firstChild;
|
||||
assert(node1.equals(node2));
|
||||
});
|
||||
|
||||
it('returns false if the nodes are not the same', () => {
|
||||
tree = parser.parse('1 + 2');
|
||||
|
||||
const sumNode = tree.rootNode.firstChild.firstChild;
|
||||
const node1 = sumNode.firstChild;
|
||||
const node2 = node1.nextSibling;
|
||||
assert(!node1.equals(node2));
|
||||
});
|
||||
});
|
||||
|
||||
describe('.fieldNameForChild(index)', () => {
|
||||
it('returns the field of a child or null', () => {
|
||||
parser.setLanguage(C);
|
||||
tree = parser.parse('int w = x + /* y is special! */ y;');
|
||||
|
||||
const translationUnitNode = tree.rootNode;
|
||||
const declarationNode = translationUnitNode.firstChild;
|
||||
const binaryExpressionNode = declarationNode
|
||||
.childForFieldName('declarator')
|
||||
.childForFieldName('value');
|
||||
|
||||
// -------------------
|
||||
// left: (identifier) 0
|
||||
// operator: "+" _ <--- (not a named child)
|
||||
// (comment) 1 <--- (is an extra)
|
||||
// right: (identifier) 2
|
||||
// -------------------
|
||||
|
||||
assert.equal(binaryExpressionNode.fieldNameForChild(0), 'left');
|
||||
assert.equal(binaryExpressionNode.fieldNameForChild(1), 'operator');
|
||||
// The comment should not have a field name, as it's just an extra
|
||||
assert.equal(binaryExpressionNode.fieldNameForChild(2), null);
|
||||
assert.equal(binaryExpressionNode.fieldNameForChild(3), 'right');
|
||||
// Negative test - Not a valid child index
|
||||
assert.equal(binaryExpressionNode.fieldNameForChild(4), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.fieldNameForNamedChild(index)', () => {
|
||||
it('returns the field of a named child or null', () => {
|
||||
parser.setLanguage(C);
|
||||
tree = parser.parse('int w = x + /* y is special! */ y;');
|
||||
|
||||
const translationUnitNode = tree.rootNode;
|
||||
const declarationNode = translationUnitNode.firstNamedChild;
|
||||
const binaryExpressionNode = declarationNode
|
||||
.childForFieldName('declarator')
|
||||
.childForFieldName('value');
|
||||
|
||||
// -------------------
|
||||
// left: (identifier) 0
|
||||
// operator: "+" _ <--- (not a named child)
|
||||
// (comment) 1 <--- (is an extra)
|
||||
// right: (identifier) 2
|
||||
// -------------------
|
||||
|
||||
assert.equal(binaryExpressionNode.fieldNameForNamedChild(0), 'left');
|
||||
// The comment should not have a field name, as it's just an extra
|
||||
assert.equal(binaryExpressionNode.fieldNameForNamedChild(1), null);
|
||||
// The operator is not a named child, so the named child at index 2 is the right child
|
||||
assert.equal(binaryExpressionNode.fieldNameForNamedChild(2), 'right');
|
||||
// Negative test - Not a valid child index
|
||||
assert.equal(binaryExpressionNode.fieldNameForNamedChild(3), null);
|
||||
});
|
||||
});
|
||||
});
|
||||
587
lib/binding_web/test/node.test.ts
Normal file
587
lib/binding_web/test/node.test.ts
Normal file
|
|
@ -0,0 +1,587 @@
|
|||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
|
||||
import TSParser, { type Language, type Tree, type SyntaxNode, type Point } from 'web-tree-sitter';
|
||||
import helper from './helper';
|
||||
|
||||
let Parser: typeof TSParser;
|
||||
let C: Language;
|
||||
let JavaScript: Language;
|
||||
let JSON: Language;
|
||||
let EmbeddedTemplate: Language;
|
||||
let Python: Language;
|
||||
|
||||
const JSON_EXAMPLE = `
|
||||
[
|
||||
123,
|
||||
false,
|
||||
{
|
||||
"x": null
|
||||
}
|
||||
]
|
||||
`;
|
||||
|
||||
function getAllNodes(tree: Tree): SyntaxNode[] {
|
||||
const result: SyntaxNode[] = [];
|
||||
let visitedChildren = false;
|
||||
const cursor = tree.walk();
|
||||
|
||||
while (true) {
|
||||
if (!visitedChildren) {
|
||||
result.push(cursor.currentNode);
|
||||
if (!cursor.gotoFirstChild()) {
|
||||
visitedChildren = true;
|
||||
}
|
||||
} else if (cursor.gotoNextSibling()) {
|
||||
visitedChildren = false;
|
||||
} else if (!cursor.gotoParent()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
describe('Node', () => {
|
||||
let parser: TSParser;
|
||||
let tree: Tree | null;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ Parser, C, EmbeddedTemplate, JavaScript, JSON, Python } = await helper);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
tree = null;
|
||||
parser = new Parser();
|
||||
parser.setLanguage(JavaScript);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
parser.delete();
|
||||
tree!.delete();
|
||||
});
|
||||
|
||||
describe('.children', () => {
|
||||
it('returns an array of child nodes', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
expect(tree.rootNode.children).toHaveLength(1);
|
||||
const sumNode = tree.rootNode.firstChild!.firstChild;
|
||||
expect(sumNode!.children.map(child => child.type)).toEqual([
|
||||
'identifier',
|
||||
'+',
|
||||
'number'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.namedChildren', () => {
|
||||
it('returns an array of named child nodes', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild!.firstChild;
|
||||
expect(tree.rootNode.namedChildren).toHaveLength(1);
|
||||
expect(sumNode!.namedChildren.map(child => child.type)).toEqual([
|
||||
'identifier',
|
||||
'number'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.childrenForFieldName', () => {
|
||||
it('returns an array of child nodes for the given field name', () => {
|
||||
parser.setLanguage(Python);
|
||||
const source = `
|
||||
if one:
|
||||
a()
|
||||
elif two:
|
||||
b()
|
||||
elif three:
|
||||
c()
|
||||
elif four:
|
||||
d()`;
|
||||
|
||||
tree = parser.parse(source);
|
||||
const node = tree.rootNode.firstChild;
|
||||
expect(node!.type).toBe('if_statement');
|
||||
const alternatives = node!.childrenForFieldName('alternative');
|
||||
const alternativeTexts = alternatives.map(n => {
|
||||
const condition = n.childForFieldName('condition');
|
||||
return source.slice(condition!.startIndex, condition!.endIndex);
|
||||
});
|
||||
expect(alternativeTexts).toEqual(['two', 'three', 'four']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.startIndex and .endIndex', () => {
|
||||
it('returns the character index where the node starts/ends in the text', () => {
|
||||
tree = parser.parse('a👍👎1 / b👎c👎');
|
||||
const quotientNode = tree.rootNode.firstChild!.firstChild;
|
||||
|
||||
expect(quotientNode!.startIndex).toBe(0);
|
||||
expect(quotientNode!.endIndex).toBe(15);
|
||||
expect(quotientNode!.children.map(child => child.startIndex)).toEqual([0, 7, 9]);
|
||||
expect(quotientNode!.children.map(child => child.endIndex)).toEqual([6, 8, 15]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.startPosition and .endPosition', () => {
|
||||
it('returns the row and column where the node starts/ends in the text', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild!.firstChild!;
|
||||
expect(sumNode.type).toBe('binary_expression');
|
||||
|
||||
expect(sumNode.startPosition).toEqual({ row: 0, column: 0 });
|
||||
expect(sumNode.endPosition).toEqual({ row: 0, column: 10 });
|
||||
expect(sumNode.children.map((child) => child.startPosition)).toEqual([
|
||||
{ row: 0, column: 0 },
|
||||
{ row: 0, column: 4 },
|
||||
{ row: 0, column: 6 },
|
||||
]);
|
||||
expect(sumNode.children.map((child) => child.endPosition)).toEqual([
|
||||
{ row: 0, column: 3 },
|
||||
{ row: 0, column: 5 },
|
||||
{ row: 0, column: 10 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles characters that occupy two UTF16 code units', () => {
|
||||
tree = parser.parse('a👍👎1 /\n b👎c👎');
|
||||
const sumNode = tree.rootNode.firstChild!.firstChild;
|
||||
expect(sumNode!.children.map(child => [child.startPosition, child.endPosition])).toEqual([
|
||||
[{ row: 0, column: 0 }, { row: 0, column: 6 }],
|
||||
[{ row: 0, column: 7 }, { row: 0, column: 8 }],
|
||||
[{ row: 1, column: 1 }, { row: 1, column: 7 }]
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.parent', () => {
|
||||
it('returns the node\'s parent', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild;
|
||||
const variableNode = sumNode!.firstChild;
|
||||
expect(sumNode!.id).not.toBe(variableNode!.id);
|
||||
expect(sumNode!.id).toBe(variableNode!.parent!.id);
|
||||
expect(tree.rootNode.id).toBe(sumNode!.parent!.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.child(), .firstChild, .lastChild', () => {
|
||||
it('returns null when the node has no children', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild!.firstChild;
|
||||
const variableNode = sumNode!.firstChild;
|
||||
expect(variableNode!.firstChild).toBeNull();
|
||||
expect(variableNode!.lastChild).toBeNull();
|
||||
expect(variableNode!.firstNamedChild).toBeNull();
|
||||
expect(variableNode!.lastNamedChild).toBeNull();
|
||||
expect(variableNode!.child(1)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('.childForFieldName()', () => {
|
||||
it('returns node for the given field name', () => {
|
||||
tree = parser.parse('class A { b() {} }');
|
||||
|
||||
const classNode = tree.rootNode.firstChild;
|
||||
expect(classNode!.type).toBe('class_declaration');
|
||||
|
||||
const classNameNode = classNode!.childForFieldName('name');
|
||||
expect(classNameNode!.type).toBe('identifier');
|
||||
expect(classNameNode!.text).toBe('A');
|
||||
|
||||
const bodyNode = classNode!.childForFieldName('body');
|
||||
expect(bodyNode!.type).toBe('class_body');
|
||||
expect(bodyNode!.text).toBe('{ b() {} }');
|
||||
|
||||
const methodNode = bodyNode!.firstNamedChild;
|
||||
expect(methodNode!.type).toBe('method_definition');
|
||||
expect(methodNode!.text).toBe('b() {}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.nextSibling and .previousSibling', () => {
|
||||
it('returns the node\'s next and previous sibling', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild!.firstChild;
|
||||
expect(sumNode!.children[1].id).toBe(sumNode!.children[0].nextSibling!.id);
|
||||
expect(sumNode!.children[2].id).toBe(sumNode!.children[1].nextSibling!.id);
|
||||
expect(sumNode!.children[0].id).toBe(sumNode!.children[1].previousSibling!.id);
|
||||
expect(sumNode!.children[1].id).toBe(sumNode!.children[2].previousSibling!.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.nextNamedSibling and .previousNamedSibling', () => {
|
||||
it('returns the node\'s next and previous named sibling', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild!.firstChild;
|
||||
expect(sumNode!.namedChildren[1].id).toBe(sumNode!.namedChildren[0].nextNamedSibling!.id);
|
||||
expect(sumNode!.namedChildren[0].id).toBe(sumNode!.namedChildren[1].previousNamedSibling!.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.descendantForIndex(min, max)', () => {
|
||||
it('returns the smallest node that spans the given range', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild!.firstChild;
|
||||
expect(sumNode!.descendantForIndex(1, 2)!.type).toBe('identifier');
|
||||
expect(sumNode!.descendantForIndex(4, 4)!.type).toBe('+');
|
||||
|
||||
expect(() => {
|
||||
sumNode!.descendantForIndex(1, {} as any);
|
||||
}).toThrow('Arguments must be numbers');
|
||||
|
||||
expect(() => {
|
||||
sumNode!.descendantForIndex(undefined as any);
|
||||
}).toThrow('Arguments must be numbers');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.namedDescendantForIndex', () => {
|
||||
it('returns the smallest named node that spans the given range', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild;
|
||||
expect(sumNode!.descendantForIndex(1, 2)!.type).toBe('identifier');
|
||||
expect(sumNode!.descendantForIndex(4, 4)!.type).toBe('+');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.descendantForPosition', () => {
|
||||
it('returns the smallest node that spans the given range', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild;
|
||||
|
||||
expect(
|
||||
sumNode!.descendantForPosition(
|
||||
{ row: 0, column: 1 },
|
||||
{ row: 0, column: 2 }
|
||||
)!.type
|
||||
).toBe('identifier');
|
||||
|
||||
expect(
|
||||
sumNode!.descendantForPosition({ row: 0, column: 4 })!.type
|
||||
).toBe('+');
|
||||
|
||||
expect(() => {
|
||||
sumNode!.descendantForPosition(1 as any, {} as any);
|
||||
}).toThrow('Arguments must be {row, column} objects');
|
||||
|
||||
expect(() => {
|
||||
sumNode!.descendantForPosition(undefined as any);
|
||||
}).toThrow('Arguments must be {row, column} objects');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.namedDescendantForPosition(min, max)', () => {
|
||||
it('returns the smallest named node that spans the given range', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild!;
|
||||
|
||||
expect(
|
||||
sumNode.namedDescendantForPosition(
|
||||
{ row: 0, column: 1 },
|
||||
{ row: 0, column: 2 },
|
||||
).type
|
||||
).toBe('identifier')
|
||||
|
||||
expect(
|
||||
sumNode.namedDescendantForPosition({ row: 0, column: 4 }).type
|
||||
).toBe('binary_expression');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.hasError', () => {
|
||||
it('returns true if the node contains an error', () => {
|
||||
tree = parser.parse('1 + 2 * * 3');
|
||||
const node = tree.rootNode;
|
||||
expect(node.toString()).toBe(
|
||||
'(program (expression_statement (binary_expression left: (number) right: (binary_expression left: (number) (ERROR) right: (number)))))'
|
||||
);
|
||||
|
||||
const sum = node.firstChild!.firstChild;
|
||||
expect(sum!.hasError).toBe(true);
|
||||
expect(sum!.children[0].hasError).toBe(false);
|
||||
expect(sum!.children[1].hasError).toBe(false);
|
||||
expect(sum!.children[2].hasError).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isError', () => {
|
||||
it('returns true if the node is an error', () => {
|
||||
tree = parser.parse('2 * * 3');
|
||||
const node = tree.rootNode;
|
||||
expect(node.toString()).toBe(
|
||||
'(program (expression_statement (binary_expression left: (number) (ERROR) right: (number))))'
|
||||
);
|
||||
|
||||
const multi = node.firstChild!.firstChild;
|
||||
expect(multi!.hasError).toBe(true);
|
||||
expect(multi!.children[0].isError).toBe(false);
|
||||
expect(multi!.children[1].isError).toBe(false);
|
||||
expect(multi!.children[2].isError).toBe(true);
|
||||
expect(multi!.children[3].isError).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isMissing', () => {
|
||||
it('returns true if the node was inserted via error recovery', () => {
|
||||
tree = parser.parse('(2 ||)');
|
||||
const node = tree.rootNode;
|
||||
expect(node.toString()).toBe(
|
||||
'(program (expression_statement (parenthesized_expression (binary_expression left: (number) right: (MISSING identifier)))))'
|
||||
);
|
||||
|
||||
const sum = node.firstChild!.firstChild!.firstNamedChild;
|
||||
expect(sum!.type).toBe('binary_expression');
|
||||
expect(sum!.hasError).toBe(true);
|
||||
expect(sum!.children[0].isMissing).toBe(false);
|
||||
expect(sum!.children[1].isMissing).toBe(false);
|
||||
expect(sum!.children[2].isMissing).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isExtra', () => {
|
||||
it('returns true if the node is an extra node like comments', () => {
|
||||
tree = parser.parse('foo(/* hi */);');
|
||||
const node = tree.rootNode;
|
||||
const commentNode = node.descendantForIndex(7, 7);
|
||||
|
||||
expect(node.type).toBe('program');
|
||||
expect(commentNode!.type).toBe('comment');
|
||||
expect(node.isExtra).toBe(false);
|
||||
expect(commentNode!.isExtra).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.text', () => {
|
||||
const text = 'α0 / b👎c👎';
|
||||
|
||||
Object.entries({
|
||||
'.parse(String)': text,
|
||||
'.parse(Function)': (offset: number) => text.slice(offset, offset + 4),
|
||||
}).forEach(([method, _parse]) =>
|
||||
it(`returns the text of a node generated by ${method}`, async () => {
|
||||
const [numeratorSrc, denominatorSrc] = text.split(/\s*\/\s+/);
|
||||
tree = parser.parse(_parse);
|
||||
const quotientNode = tree.rootNode!.firstChild!.firstChild!;
|
||||
const [numerator, slash, denominator] = quotientNode.children;
|
||||
|
||||
expect(tree.rootNode.text).toBe(text);
|
||||
expect(denominator.text).toBe(denominatorSrc);
|
||||
expect(quotientNode!.text).toBe(text);
|
||||
expect(numerator.text).toBe(numeratorSrc);
|
||||
expect(slash.text).toBe('/');
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('.descendantCount', () => {
|
||||
it('returns the number of descendants', () => {
|
||||
parser.setLanguage(JSON);
|
||||
tree = parser.parse(JSON_EXAMPLE);
|
||||
const valueNode = tree.rootNode;
|
||||
const allNodes = getAllNodes(tree);
|
||||
|
||||
expect(valueNode.descendantCount).toBe(allNodes.length);
|
||||
|
||||
const cursor = tree.walk();
|
||||
for (let i = 0; i < allNodes.length; i++) {
|
||||
const node = allNodes[i];
|
||||
cursor.gotoDescendant(i);
|
||||
expect(cursor.currentNode.id).toBe(node.id);
|
||||
}
|
||||
|
||||
for (let i = allNodes.length - 1; i >= 0; i--) {
|
||||
const node = allNodes[i];
|
||||
cursor.gotoDescendant(i);
|
||||
expect(cursor.currentNode.id).toBe(node.id);
|
||||
}
|
||||
});
|
||||
|
||||
it('tests a single node tree', () => {
|
||||
parser.setLanguage(EmbeddedTemplate);
|
||||
tree = parser.parse('hello');
|
||||
|
||||
const nodes = getAllNodes(tree);
|
||||
expect(nodes).toHaveLength(2);
|
||||
expect(tree.rootNode.descendantCount).toBe(2);
|
||||
|
||||
const cursor = tree.walk();
|
||||
|
||||
cursor.gotoDescendant(0);
|
||||
expect(cursor.currentDepth).toBe(0);
|
||||
expect(cursor.currentNode.id).toBe(nodes[0].id);
|
||||
|
||||
cursor.gotoDescendant(1);
|
||||
expect(cursor.currentDepth).toBe(1);
|
||||
expect(cursor.currentNode.id).toBe(nodes[1].id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.rootNodeWithOffset', () => {
|
||||
it('returns the root node of the tree, offset by the given byte offset', () => {
|
||||
tree = parser.parse(' if (a) b');
|
||||
const node = tree.rootNodeWithOffset(6, { row: 2, column: 2 });
|
||||
expect(node.startIndex).toBe(8);
|
||||
expect(node.endIndex).toBe(16);
|
||||
expect(node.startPosition).toEqual({ row: 2, column: 4 });
|
||||
expect(node.endPosition).toEqual({ row: 2, column: 12 });
|
||||
|
||||
let child = node.firstChild!.child(2);
|
||||
expect(child!.type).toBe('expression_statement');
|
||||
expect(child!.startIndex).toBe(15);
|
||||
expect(child!.endIndex).toBe(16);
|
||||
expect(child!.startPosition).toEqual({ row: 2, column: 11 });
|
||||
expect(child!.endPosition).toEqual({ row: 2, column: 12 });
|
||||
|
||||
const cursor = node.walk();
|
||||
cursor.gotoFirstChild();
|
||||
cursor.gotoFirstChild();
|
||||
cursor.gotoNextSibling();
|
||||
child = cursor.currentNode;
|
||||
expect(child.type).toBe('parenthesized_expression');
|
||||
expect(child.startIndex).toBe(11);
|
||||
expect(child.endIndex).toBe(14);
|
||||
expect(child.startPosition).toEqual({ row: 2, column: 7 });
|
||||
expect(child.endPosition).toEqual({ row: 2, column: 10 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('.parseState, .nextParseState', () => {
|
||||
const text = '10 / 5';
|
||||
|
||||
it('returns node parse state ids', () => {
|
||||
tree = parser.parse(text);
|
||||
const quotientNode = tree.rootNode.firstChild!.firstChild;
|
||||
const [numerator, slash, denominator] = quotientNode!.children;
|
||||
|
||||
expect(tree.rootNode.parseState).toBe(0);
|
||||
// parse states will change on any change to the grammar so test that it
|
||||
// returns something instead
|
||||
expect(numerator.parseState).toBeGreaterThan(0);
|
||||
expect(slash.parseState).toBeGreaterThan(0);
|
||||
expect(denominator.parseState).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('returns next parse state equal to the language', () => {
|
||||
tree = parser.parse(text);
|
||||
const quotientNode = tree.rootNode.firstChild!.firstChild;
|
||||
quotientNode!.children.forEach((node) => {
|
||||
expect(node.nextParseState).toBe(
|
||||
JavaScript.nextState(node.parseState, node.grammarId)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.descendantsOfType', () => {
|
||||
it('finds all descendants of a given type in the given range', () => {
|
||||
tree = parser.parse('a + 1 * b * 2 + c + 3');
|
||||
const outerSum = tree.rootNode.firstChild!.firstChild;
|
||||
|
||||
let descendants = outerSum!.descendantsOfType('number', { row: 0, column: 2 }, { row: 0, column: 15 });
|
||||
expect(descendants.map(node => node.startIndex)).toEqual([4, 12]);
|
||||
expect(descendants.map(node => node.endPosition)).toEqual([
|
||||
{ row: 0, column: 5 },
|
||||
{ row: 0, column: 13 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe('.firstChildForIndex(index)', () => {
|
||||
it('returns the first child that contains or starts after the given index', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild!.firstChild;
|
||||
|
||||
expect(sumNode!.firstChildForIndex(0)!.type).toBe('identifier');
|
||||
expect(sumNode!.firstChildForIndex(1)!.type).toBe('identifier');
|
||||
expect(sumNode!.firstChildForIndex(3)!.type).toBe('+');
|
||||
expect(sumNode!.firstChildForIndex(5)!.type).toBe('number');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.firstNamedChildForIndex(index)', () => {
|
||||
it('returns the first child that contains or starts after the given index', () => {
|
||||
tree = parser.parse('x10 + 1000');
|
||||
const sumNode = tree.rootNode.firstChild!.firstChild;
|
||||
|
||||
expect(sumNode!.firstNamedChildForIndex(0)!.type).toBe('identifier');
|
||||
expect(sumNode!.firstNamedChildForIndex(1)!.type).toBe('identifier');
|
||||
expect(sumNode!.firstNamedChildForIndex(3)!.type).toBe('number');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.equals(other)', () => {
|
||||
it('returns true if the nodes are the same', () => {
|
||||
tree = parser.parse('1 + 2');
|
||||
|
||||
const sumNode = tree.rootNode.firstChild!.firstChild;
|
||||
const node1 = sumNode!.firstChild;
|
||||
const node2 = sumNode!.firstChild;
|
||||
expect(node1!.equals(node2!)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if the nodes are not the same', () => {
|
||||
tree = parser.parse('1 + 2');
|
||||
|
||||
const sumNode = tree.rootNode.firstChild!.firstChild;
|
||||
const node1 = sumNode!.firstChild;
|
||||
const node2 = node1!.nextSibling;
|
||||
expect(node1!.equals(node2!)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.fieldNameForChild(index)', () => {
|
||||
it('returns the field of a child or null', () => {
|
||||
parser.setLanguage(C);
|
||||
tree = parser.parse('int w = x + /* y is special! */ y;');
|
||||
|
||||
const translationUnitNode = tree.rootNode;
|
||||
const declarationNode = translationUnitNode.firstChild;
|
||||
const binaryExpressionNode = declarationNode!
|
||||
.childForFieldName('declarator')!
|
||||
.childForFieldName('value');
|
||||
|
||||
// -------------------
|
||||
// left: (identifier) 0
|
||||
// operator: "+" 1 <--- (not a named child)
|
||||
// (comment) 2 <--- (is an extra)
|
||||
// right: (identifier) 3
|
||||
// -------------------
|
||||
|
||||
expect(binaryExpressionNode!.fieldNameForChild(0)).toBe('left');
|
||||
expect(binaryExpressionNode!.fieldNameForChild(1)).toBe('operator');
|
||||
// The comment should not have a field name, as it's just an extra
|
||||
expect(binaryExpressionNode!.fieldNameForChild(2)).toBeNull();
|
||||
expect(binaryExpressionNode!.fieldNameForChild(3)).toBe('right');
|
||||
// Negative test - Not a valid child index
|
||||
expect(binaryExpressionNode!.fieldNameForChild(4)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('.fieldNameForNamedChild(index)', () => {
|
||||
it('returns the field of a named child or null', () => {
|
||||
parser.setLanguage(C);
|
||||
tree = parser.parse('int w = x + /* y is special! */ y;');
|
||||
|
||||
const translationUnitNode = tree.rootNode;
|
||||
const declarationNode = translationUnitNode.firstNamedChild;
|
||||
const binaryExpressionNode = declarationNode!
|
||||
.childForFieldName('declarator')!
|
||||
.childForFieldName('value');
|
||||
|
||||
// -------------------
|
||||
// left: (identifier) 0
|
||||
// operator: "+" _ <--- (not a named child)
|
||||
// (comment) 1 <--- (is an extra)
|
||||
// right: (identifier) 2
|
||||
// -------------------
|
||||
|
||||
expect(binaryExpressionNode!.fieldNameForNamedChild(0)).toBe('left');
|
||||
// The comment should not have a field name, as it's just an extra
|
||||
expect(binaryExpressionNode!.fieldNameForNamedChild(1)).toBeNull();
|
||||
// The operator is not a named child, so the named child at index 2 is the right child
|
||||
expect(binaryExpressionNode!.fieldNameForNamedChild(2)).toBe('right');
|
||||
// Negative test - Not a valid child index
|
||||
expect(binaryExpressionNode!.fieldNameForNamedChild(3)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,413 +0,0 @@
|
|||
const {assert} = require('chai');
|
||||
let Parser; let JavaScript; let HTML; let languageURL; let JSON;
|
||||
|
||||
describe('Parser', () => {
|
||||
let parser;
|
||||
|
||||
before(async () =>
|
||||
({Parser, JavaScript, HTML, JSON, languageURL} = await require('./helper')),
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new Parser();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
parser.delete();
|
||||
});
|
||||
|
||||
describe('.setLanguage', () => {
|
||||
it('allows setting the language to null', () => {
|
||||
assert.equal(parser.getLanguage(), null);
|
||||
parser.setLanguage(JavaScript);
|
||||
assert.equal(parser.getLanguage(), JavaScript);
|
||||
parser.setLanguage(null);
|
||||
assert.equal(parser.getLanguage(), null);
|
||||
});
|
||||
|
||||
it('throws an exception when the given object is not a tree-sitter language', () => {
|
||||
assert.throws(() => parser.setLanguage({}), /Argument must be a Language/);
|
||||
assert.throws(() => parser.setLanguage(1), /Argument must be a Language/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.setLogger', () => {
|
||||
beforeEach(() => {
|
||||
parser.setLanguage(JavaScript);
|
||||
});
|
||||
|
||||
it('calls the given callback for each parse event', () => {
|
||||
const debugMessages = [];
|
||||
parser.setLogger((message) => debugMessages.push(message));
|
||||
parser.parse('a + b + c');
|
||||
assert.includeMembers(debugMessages, [
|
||||
'skip character:\' \'',
|
||||
'consume character:\'b\'',
|
||||
'reduce sym:program, child_count:1',
|
||||
'accept',
|
||||
]);
|
||||
});
|
||||
|
||||
it('allows the callback to be retrieved later', () => {
|
||||
const callback = () => {};
|
||||
parser.setLogger(callback);
|
||||
assert.equal(parser.getLogger(), callback);
|
||||
parser.setLogger(false);
|
||||
assert.equal(parser.getLogger(), null);
|
||||
});
|
||||
|
||||
it('disables debugging when given a falsy value', () => {
|
||||
const debugMessages = [];
|
||||
parser.setLogger((message) => debugMessages.push(message));
|
||||
parser.setLogger(false);
|
||||
parser.parse('a + b * c');
|
||||
assert.equal(debugMessages.length, 0);
|
||||
});
|
||||
|
||||
it('throws an error when given a truthy value that isn\'t a function ', () => {
|
||||
assert.throws(
|
||||
() => parser.setLogger('5'),
|
||||
'Logger callback must be a function',
|
||||
);
|
||||
});
|
||||
|
||||
it('rethrows errors thrown by the logging callback', () => {
|
||||
const error = new Error('The error message');
|
||||
parser.setLogger((_msg, _params) => {
|
||||
throw error;
|
||||
});
|
||||
assert.throws(
|
||||
() => parser.parse('ok;'),
|
||||
'The error message',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('one included range', () => {
|
||||
it('parses the text within a range', () => {
|
||||
parser.setLanguage(HTML);
|
||||
const sourceCode = '<span>hi</span><script>console.log(\'sup\');</script>';
|
||||
const htmlTree = parser.parse(sourceCode);
|
||||
const scriptContentNode = htmlTree.rootNode.child(1).child(1);
|
||||
assert.equal(scriptContentNode.type, 'raw_text');
|
||||
|
||||
parser.setLanguage(JavaScript);
|
||||
assert.deepEqual(parser.getIncludedRanges(), [{
|
||||
startIndex: 0,
|
||||
endIndex: 2147483647,
|
||||
startPosition: {row: 0, column: 0},
|
||||
endPosition: {row: 4294967295, column: 2147483647},
|
||||
}]);
|
||||
const ranges = [{
|
||||
startIndex: scriptContentNode.startIndex,
|
||||
endIndex: scriptContentNode.endIndex,
|
||||
startPosition: scriptContentNode.startPosition,
|
||||
endPosition: scriptContentNode.endPosition,
|
||||
}];
|
||||
const jsTree = parser.parse(
|
||||
sourceCode,
|
||||
null,
|
||||
{includedRanges: ranges},
|
||||
);
|
||||
assert.deepEqual(parser.getIncludedRanges(), ranges);
|
||||
|
||||
assert.equal(
|
||||
jsTree.rootNode.toString(),
|
||||
'(program (expression_statement (call_expression ' +
|
||||
'function: (member_expression object: (identifier) property: (property_identifier)) ' +
|
||||
'arguments: (arguments (string (string_fragment))))))',
|
||||
);
|
||||
assert.deepEqual(jsTree.rootNode.startPosition, {row: 0, column: sourceCode.indexOf('console')});
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple included ranges', () => {
|
||||
it('parses the text within multiple ranges', () => {
|
||||
parser.setLanguage(JavaScript);
|
||||
const sourceCode = 'html `<div>Hello, ${name.toUpperCase()}, it\'s <b>${now()}</b>.</div>`';
|
||||
const jsTree = parser.parse(sourceCode);
|
||||
const templateStringNode = jsTree.rootNode.descendantForIndex(sourceCode.indexOf('`<'), sourceCode.indexOf('>`'));
|
||||
assert.equal(templateStringNode.type, 'template_string');
|
||||
|
||||
const openQuoteNode = templateStringNode.child(0);
|
||||
const interpolationNode1 = templateStringNode.child(2);
|
||||
const interpolationNode2 = templateStringNode.child(4);
|
||||
const closeQuoteNode = templateStringNode.child(6);
|
||||
|
||||
parser.setLanguage(HTML);
|
||||
const htmlRanges = [
|
||||
{
|
||||
startIndex: openQuoteNode.endIndex,
|
||||
startPosition: openQuoteNode.endPosition,
|
||||
endIndex: interpolationNode1.startIndex,
|
||||
endPosition: interpolationNode1.startPosition,
|
||||
},
|
||||
{
|
||||
startIndex: interpolationNode1.endIndex,
|
||||
startPosition: interpolationNode1.endPosition,
|
||||
endIndex: interpolationNode2.startIndex,
|
||||
endPosition: interpolationNode2.startPosition,
|
||||
},
|
||||
{
|
||||
startIndex: interpolationNode2.endIndex,
|
||||
startPosition: interpolationNode2.endPosition,
|
||||
endIndex: closeQuoteNode.startIndex,
|
||||
endPosition: closeQuoteNode.startPosition,
|
||||
},
|
||||
];
|
||||
const htmlTree = parser.parse(sourceCode, null, {includedRanges: htmlRanges});
|
||||
|
||||
assert.equal(
|
||||
htmlTree.rootNode.toString(),
|
||||
'(document (element' +
|
||||
' (start_tag (tag_name))' +
|
||||
' (text)' +
|
||||
' (element (start_tag (tag_name)) (end_tag (tag_name)))' +
|
||||
' (text)' +
|
||||
' (end_tag (tag_name))))',
|
||||
);
|
||||
assert.deepEqual(htmlTree.getIncludedRanges(), htmlRanges);
|
||||
|
||||
const divElementNode = htmlTree.rootNode.child(0);
|
||||
const helloTextNode = divElementNode.child(1);
|
||||
const bElementNode = divElementNode.child(2);
|
||||
const bStartTagNode = bElementNode.child(0);
|
||||
const bEndTagNode = bElementNode.child(1);
|
||||
|
||||
assert.equal(helloTextNode.type, 'text');
|
||||
assert.equal(helloTextNode.startIndex, sourceCode.indexOf('Hello'));
|
||||
assert.equal(helloTextNode.endIndex, sourceCode.indexOf(' <b>'));
|
||||
|
||||
assert.equal(bStartTagNode.type, 'start_tag');
|
||||
assert.equal(bStartTagNode.startIndex, sourceCode.indexOf('<b>'));
|
||||
assert.equal(bStartTagNode.endIndex, sourceCode.indexOf('${now()}'));
|
||||
|
||||
assert.equal(bEndTagNode.type, 'end_tag');
|
||||
assert.equal(bEndTagNode.startIndex, sourceCode.indexOf('</b>'));
|
||||
assert.equal(bEndTagNode.endIndex, sourceCode.indexOf('.</div>'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('an included range containing mismatched positions', () => {
|
||||
it('parses the text within the range', () => {
|
||||
const sourceCode = '<div>test</div>{_ignore_this_part_}';
|
||||
|
||||
parser.setLanguage(HTML);
|
||||
|
||||
const endIndex = sourceCode.indexOf('{_ignore_this_part_');
|
||||
|
||||
const rangeToParse = {
|
||||
startIndex: 0,
|
||||
startPosition: {row: 10, column: 12},
|
||||
endIndex,
|
||||
endPosition: {row: 10, column: 12 + endIndex},
|
||||
};
|
||||
|
||||
const htmlTree = parser.parse(sourceCode, null, {includedRanges: [rangeToParse]});
|
||||
|
||||
assert.deepEqual(htmlTree.getIncludedRanges()[0], rangeToParse);
|
||||
|
||||
assert.equal(
|
||||
htmlTree.rootNode.toString(),
|
||||
'(document (element (start_tag (tag_name)) (text) (end_tag (tag_name))))',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.parse', () => {
|
||||
let tree;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = null;
|
||||
parser.setLanguage(JavaScript);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (tree) tree.delete();
|
||||
});
|
||||
|
||||
it('reads from the given input', () => {
|
||||
const parts = ['first', '_', 'second', '_', 'third'];
|
||||
tree = parser.parse(() => parts.shift());
|
||||
assert.equal(tree.rootNode.toString(), '(program (expression_statement (identifier)))');
|
||||
});
|
||||
|
||||
it('stops reading when the input callback return something that\'s not a string', () => {
|
||||
const parts = ['abc', 'def', 'ghi', {}, {}, {}, 'second-word', ' '];
|
||||
tree = parser.parse(() => parts.shift());
|
||||
assert.equal(
|
||||
tree.rootNode.toString(),
|
||||
'(program (expression_statement (identifier)))',
|
||||
);
|
||||
assert.equal(tree.rootNode.endIndex, 9);
|
||||
assert.equal(parts.length, 2);
|
||||
});
|
||||
|
||||
it('throws an exception when the given input is not a function', () => {
|
||||
assert.throws(() => parser.parse(null), 'Argument must be a string or a function');
|
||||
assert.throws(() => parser.parse(5), 'Argument must be a string or a function');
|
||||
assert.throws(() => parser.parse({}), 'Argument must be a string or a function');
|
||||
});
|
||||
|
||||
it('handles long input strings', () => {
|
||||
const repeatCount = 10000;
|
||||
const inputString = `[${Array(repeatCount).fill('0').join(',')}]`;
|
||||
|
||||
tree = parser.parse(inputString);
|
||||
assert.equal(tree.rootNode.type, 'program');
|
||||
assert.equal(tree.rootNode.firstChild.firstChild.namedChildCount, repeatCount);
|
||||
}).timeout(5000);
|
||||
|
||||
it('can use the bash parser', async () => {
|
||||
parser.setLanguage(await Parser.Language.load(languageURL('bash')));
|
||||
tree = parser.parse('FOO=bar echo <<EOF 2> err.txt > hello.txt \nhello${FOO}\nEOF');
|
||||
assert.equal(
|
||||
tree.rootNode.toString(),
|
||||
'(program ' +
|
||||
'(redirected_statement ' +
|
||||
'body: (command ' +
|
||||
'(variable_assignment name: (variable_name) value: (word)) ' +
|
||||
'name: (command_name (word))) ' +
|
||||
'redirect: (heredoc_redirect (heredoc_start) ' +
|
||||
'redirect: (file_redirect descriptor: (file_descriptor) destination: (word)) ' +
|
||||
'redirect: (file_redirect destination: (word)) ' +
|
||||
'(heredoc_body ' +
|
||||
'(expansion (variable_name)) (heredoc_content)) (heredoc_end))))',
|
||||
);
|
||||
}).timeout(5000);
|
||||
|
||||
it('can use the c++ parser', async () => {
|
||||
parser.setLanguage(await Parser.Language.load(languageURL('cpp')));
|
||||
tree = parser.parse('const char *s = R"EOF(HELLO WORLD)EOF";');
|
||||
assert.equal(
|
||||
tree.rootNode.toString(),
|
||||
'(translation_unit (declaration ' +
|
||||
'(type_qualifier) ' +
|
||||
'type: (primitive_type) ' +
|
||||
'declarator: (init_declarator ' +
|
||||
'declarator: (pointer_declarator declarator: (identifier)) ' +
|
||||
'value: (raw_string_literal delimiter: (raw_string_delimiter) (raw_string_content) (raw_string_delimiter)))))',
|
||||
);
|
||||
}).timeout(5000);
|
||||
|
||||
it('can use the HTML parser', async () => {
|
||||
parser.setLanguage(await Parser.Language.load(languageURL('html')));
|
||||
tree = parser.parse('<div><span><custom></custom></span></div>');
|
||||
assert.equal(
|
||||
tree.rootNode.toString(),
|
||||
'(document (element (start_tag (tag_name)) (element (start_tag (tag_name)) (element (start_tag (tag_name)) (end_tag (tag_name))) (end_tag (tag_name))) (end_tag (tag_name))))',
|
||||
);
|
||||
}).timeout(5000);
|
||||
|
||||
it('can use the python parser', async () => {
|
||||
parser.setLanguage(await Parser.Language.load(languageURL('python')));
|
||||
tree = parser.parse('class A:\n def b():\n c()');
|
||||
assert.equal(
|
||||
tree.rootNode.toString(),
|
||||
'(module (class_definition ' +
|
||||
'name: (identifier) ' +
|
||||
'body: (block ' +
|
||||
'(function_definition ' +
|
||||
'name: (identifier) ' +
|
||||
'parameters: (parameters) ' +
|
||||
'body: (block (expression_statement (call ' +
|
||||
'function: (identifier) ' +
|
||||
'arguments: (argument_list))))))))',
|
||||
);
|
||||
}).timeout(5000);
|
||||
|
||||
it('can use the rust parser', async () => {
|
||||
parser.setLanguage(await Parser.Language.load(languageURL('rust')));
|
||||
tree = parser.parse('const x: &\'static str = r###"hello"###;');
|
||||
assert.equal(
|
||||
tree.rootNode.toString(),
|
||||
'(source_file (const_item ' +
|
||||
'name: (identifier) ' +
|
||||
'type: (reference_type (lifetime (identifier)) type: (primitive_type)) ' +
|
||||
'value: (raw_string_literal (string_content))))',
|
||||
);
|
||||
}).timeout(5000);
|
||||
|
||||
it('can use the typescript parser', async () => {
|
||||
parser.setLanguage(await Parser.Language.load(languageURL('typescript')));
|
||||
tree = parser.parse('a()\nb()\n[c]');
|
||||
assert.equal(
|
||||
tree.rootNode.toString(),
|
||||
'(program ' +
|
||||
'(expression_statement (call_expression function: (identifier) arguments: (arguments))) ' +
|
||||
'(expression_statement (subscript_expression ' +
|
||||
'object: (call_expression ' +
|
||||
'function: (identifier) ' +
|
||||
'arguments: (arguments)) ' +
|
||||
'index: (identifier))))',
|
||||
);
|
||||
}).timeout(5000);
|
||||
|
||||
it('can use the tsx parser', async () => {
|
||||
parser.setLanguage(await Parser.Language.load(languageURL('tsx')));
|
||||
tree = parser.parse('a()\nb()\n[c]');
|
||||
assert.equal(
|
||||
tree.rootNode.toString(),
|
||||
'(program ' +
|
||||
'(expression_statement (call_expression function: (identifier) arguments: (arguments))) ' +
|
||||
'(expression_statement (subscript_expression ' +
|
||||
'object: (call_expression ' +
|
||||
'function: (identifier) ' +
|
||||
'arguments: (arguments)) ' +
|
||||
'index: (identifier))))',
|
||||
);
|
||||
}).timeout(5000);
|
||||
|
||||
it('parses only the text within the `includedRanges` if they are specified', () => {
|
||||
const sourceCode = '<% foo() %> <% bar %>';
|
||||
|
||||
const start1 = sourceCode.indexOf('foo');
|
||||
const end1 = start1 + 5;
|
||||
const start2 = sourceCode.indexOf('bar');
|
||||
const end2 = start2 + 3;
|
||||
|
||||
const tree = parser.parse(sourceCode, null, {
|
||||
includedRanges: [
|
||||
{
|
||||
startIndex: start1,
|
||||
endIndex: end1,
|
||||
startPosition: {row: 0, column: start1},
|
||||
endPosition: {row: 0, column: end1},
|
||||
},
|
||||
{
|
||||
startIndex: start2,
|
||||
endIndex: end2,
|
||||
startPosition: {row: 0, column: start2},
|
||||
endPosition: {row: 0, column: end2},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
tree.rootNode.toString(),
|
||||
'(program (expression_statement (call_expression function: (identifier) arguments: (arguments))) (expression_statement (identifier)))',
|
||||
);
|
||||
});
|
||||
|
||||
it('parses with a timeout', () => {
|
||||
parser.setLanguage(JSON);
|
||||
|
||||
const startTime = performance.now();
|
||||
assert.throws(() => {
|
||||
parser.parse(
|
||||
(offset, _) => offset === 0 ? '[' : ',0',
|
||||
null,
|
||||
{
|
||||
progressCallback: (_) => {
|
||||
if (performance.now() - startTime > 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}).timeout(5000);
|
||||
});
|
||||
});
|
||||
412
lib/binding_web/test/parser.test.ts
Normal file
412
lib/binding_web/test/parser.test.ts
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
|
||||
import helper from './helper';
|
||||
import TSParser, { type Language } from 'web-tree-sitter';
|
||||
|
||||
let Parser: typeof TSParser;
|
||||
let JavaScript: Language;
|
||||
let HTML: Language;
|
||||
let JSON: Language;
|
||||
let languageURL: (name: string) => string;
|
||||
|
||||
describe('Parser', () => {
|
||||
let parser: TSParser;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ Parser, JavaScript, HTML, JSON, languageURL } = await helper);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new Parser();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
parser.delete();
|
||||
});
|
||||
|
||||
describe('.setLanguage', () => {
|
||||
it('allows setting the language to null', () => {
|
||||
expect(parser.getLanguage()).toBeUndefined();
|
||||
parser.setLanguage(JavaScript);
|
||||
expect(parser.getLanguage()).toBe(JavaScript);
|
||||
parser.setLanguage(null);
|
||||
expect(parser.getLanguage()).toBeNull();
|
||||
});
|
||||
|
||||
it('throws an exception when the given object is not a tree-sitter language', () => {
|
||||
expect(() => parser.setLanguage({} as any)).toThrow(/Argument must be a Language/);
|
||||
expect(() => parser.setLanguage(1 as any)).toThrow(/Argument must be a Language/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.setLogger', () => {
|
||||
beforeEach(() => {
|
||||
parser.setLanguage(JavaScript);
|
||||
});
|
||||
|
||||
it('calls the given callback for each parse event', () => {
|
||||
const debugMessages: string[] = [];
|
||||
parser.setLogger((message) => debugMessages.push(message));
|
||||
parser.parse('a + b + c');
|
||||
expect(debugMessages).toEqual(expect.arrayContaining([
|
||||
'skip character:\' \'',
|
||||
'consume character:\'b\'',
|
||||
'reduce sym:program, child_count:1',
|
||||
'accept'
|
||||
]));
|
||||
});
|
||||
|
||||
it('allows the callback to be retrieved later', () => {
|
||||
const callback = () => { };
|
||||
parser.setLogger(callback);
|
||||
expect(parser.getLogger()).toBe(callback);
|
||||
parser.setLogger(false);
|
||||
expect(parser.getLogger()).toBeNull();
|
||||
});
|
||||
|
||||
it('disables debugging when given a falsy value', () => {
|
||||
const debugMessages: string[] = [];
|
||||
parser.setLogger((message) => debugMessages.push(message));
|
||||
parser.setLogger(false);
|
||||
parser.parse('a + b * c');
|
||||
expect(debugMessages).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('throws an error when given a truthy value that isn\'t a function', () => {
|
||||
expect(() => parser.setLogger('5' as any)).toThrow('Logger callback must be a function');
|
||||
});
|
||||
|
||||
it('rethrows errors thrown by the logging callback', () => {
|
||||
const error = new Error('The error message');
|
||||
parser.setLogger((_msg) => {
|
||||
throw error;
|
||||
});
|
||||
expect(() => parser.parse('ok;')).toThrow('The error message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('one included range', () => {
|
||||
it('parses the text within a range', () => {
|
||||
parser.setLanguage(HTML);
|
||||
const sourceCode = '<span>hi</span><script>console.log(\'sup\');</script>';
|
||||
const htmlTree = parser.parse(sourceCode);
|
||||
const scriptContentNode = htmlTree.rootNode.child(1)!.child(1)!;
|
||||
expect(scriptContentNode.type).toBe('raw_text');
|
||||
|
||||
parser.setLanguage(JavaScript);
|
||||
expect(parser.getIncludedRanges()).toEqual([{
|
||||
startIndex: 0,
|
||||
endIndex: 2147483647,
|
||||
startPosition: { row: 0, column: 0 },
|
||||
endPosition: { row: 4294967295, column: 2147483647 }
|
||||
}]);
|
||||
|
||||
const ranges = [{
|
||||
startIndex: scriptContentNode.startIndex,
|
||||
endIndex: scriptContentNode.endIndex,
|
||||
startPosition: scriptContentNode.startPosition,
|
||||
endPosition: scriptContentNode.endPosition,
|
||||
}];
|
||||
|
||||
const jsTree = parser.parse(
|
||||
sourceCode,
|
||||
null,
|
||||
{ includedRanges: ranges }
|
||||
);
|
||||
expect(parser.getIncludedRanges()).toEqual(ranges);
|
||||
|
||||
expect(jsTree.rootNode.toString()).toBe(
|
||||
'(program (expression_statement (call_expression ' +
|
||||
'function: (member_expression object: (identifier) property: (property_identifier)) ' +
|
||||
'arguments: (arguments (string (string_fragment))))))'
|
||||
);
|
||||
expect(jsTree.rootNode.startPosition).toEqual({ row: 0, column: sourceCode.indexOf('console') });
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple included ranges', () => {
|
||||
it('parses the text within multiple ranges', () => {
|
||||
parser.setLanguage(JavaScript);
|
||||
const sourceCode = 'html `<div>Hello, ${name.toUpperCase()}, it\'s <b>${now()}</b>.</div>`';
|
||||
const jsTree = parser.parse(sourceCode);
|
||||
const templateStringNode = jsTree.rootNode.descendantForIndex(
|
||||
sourceCode.indexOf('`<'),
|
||||
sourceCode.indexOf('>`')
|
||||
);
|
||||
expect(templateStringNode.type).toBe('template_string');
|
||||
|
||||
const openQuoteNode = templateStringNode.child(0)!;
|
||||
const interpolationNode1 = templateStringNode.child(2)!;
|
||||
const interpolationNode2 = templateStringNode.child(4)!;
|
||||
const closeQuoteNode = templateStringNode.child(6)!;
|
||||
|
||||
parser.setLanguage(HTML);
|
||||
const htmlRanges = [
|
||||
{
|
||||
startIndex: openQuoteNode.endIndex,
|
||||
startPosition: openQuoteNode.endPosition,
|
||||
endIndex: interpolationNode1.startIndex,
|
||||
endPosition: interpolationNode1.startPosition,
|
||||
},
|
||||
{
|
||||
startIndex: interpolationNode1.endIndex,
|
||||
startPosition: interpolationNode1.endPosition,
|
||||
endIndex: interpolationNode2.startIndex,
|
||||
endPosition: interpolationNode2.startPosition,
|
||||
},
|
||||
{
|
||||
startIndex: interpolationNode2.endIndex,
|
||||
startPosition: interpolationNode2.endPosition,
|
||||
endIndex: closeQuoteNode.startIndex,
|
||||
endPosition: closeQuoteNode.startPosition,
|
||||
},
|
||||
];
|
||||
|
||||
const htmlTree = parser.parse(sourceCode, null, { includedRanges: htmlRanges });
|
||||
|
||||
expect(htmlTree.rootNode.toString()).toBe(
|
||||
'(document (element' +
|
||||
' (start_tag (tag_name))' +
|
||||
' (text)' +
|
||||
' (element (start_tag (tag_name)) (end_tag (tag_name)))' +
|
||||
' (text)' +
|
||||
' (end_tag (tag_name))))'
|
||||
);
|
||||
expect(htmlTree.getIncludedRanges()).toEqual(htmlRanges);
|
||||
|
||||
const divElementNode = htmlTree.rootNode.child(0)!;
|
||||
const helloTextNode = divElementNode.child(1)!;
|
||||
const bElementNode = divElementNode.child(2)!;
|
||||
const bStartTagNode = bElementNode.child(0)!;
|
||||
const bEndTagNode = bElementNode.child(1)!;
|
||||
|
||||
expect(helloTextNode.type).toBe('text');
|
||||
expect(helloTextNode.startIndex).toBe(sourceCode.indexOf('Hello'));
|
||||
expect(helloTextNode.endIndex).toBe(sourceCode.indexOf(' <b>'));
|
||||
|
||||
expect(bStartTagNode.type).toBe('start_tag');
|
||||
expect(bStartTagNode.startIndex).toBe(sourceCode.indexOf('<b>'));
|
||||
expect(bStartTagNode.endIndex).toBe(sourceCode.indexOf('${now()}'));
|
||||
|
||||
expect(bEndTagNode.type).toBe('end_tag');
|
||||
expect(bEndTagNode.startIndex).toBe(sourceCode.indexOf('</b>'));
|
||||
expect(bEndTagNode.endIndex).toBe(sourceCode.indexOf('.</div>'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('an included range containing mismatched positions', () => {
|
||||
it('parses the text within the range', () => {
|
||||
const sourceCode = '<div>test</div>{_ignore_this_part_}';
|
||||
|
||||
parser.setLanguage(HTML);
|
||||
|
||||
const endIndex = sourceCode.indexOf('{_ignore_this_part_');
|
||||
|
||||
const rangeToParse = {
|
||||
startIndex: 0,
|
||||
startPosition: { row: 10, column: 12 },
|
||||
endIndex,
|
||||
endPosition: { row: 10, column: 12 + endIndex },
|
||||
};
|
||||
|
||||
const htmlTree = parser.parse(sourceCode, null, { includedRanges: [rangeToParse] });
|
||||
|
||||
expect(htmlTree.getIncludedRanges()[0]).toEqual(rangeToParse);
|
||||
|
||||
expect(htmlTree.rootNode.toString()).toBe(
|
||||
'(document (element (start_tag (tag_name)) (text) (end_tag (tag_name))))'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.parse', () => {
|
||||
let tree: TSParser.Tree | null;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = null;
|
||||
parser.setLanguage(JavaScript);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (tree) tree.delete();
|
||||
});
|
||||
|
||||
it('reads from the given input', () => {
|
||||
const parts = ['first', '_', 'second', '_', 'third'];
|
||||
tree = parser.parse(() => parts.shift());
|
||||
expect(tree.rootNode.toString()).toBe('(program (expression_statement (identifier)))');
|
||||
});
|
||||
|
||||
it('stops reading when the input callback returns something that\'s not a string', () => {
|
||||
const parts = ['abc', 'def', 'ghi', {}, {}, {}, 'second-word', ' '];
|
||||
tree = parser.parse(() => parts.shift() as string);
|
||||
expect(tree.rootNode.toString()).toBe('(program (expression_statement (identifier)))');
|
||||
expect(tree.rootNode.endIndex).toBe(9);
|
||||
expect(parts).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('throws an exception when the given input is not a function', () => {
|
||||
expect(() => parser.parse(null as any)).toThrow('Argument must be a string or a function');
|
||||
expect(() => parser.parse(5 as any)).toThrow('Argument must be a string or a function');
|
||||
expect(() => parser.parse({} as any)).toThrow('Argument must be a string or a function');
|
||||
});
|
||||
|
||||
it('handles long input strings', { timeout: 5000 }, () => {
|
||||
const repeatCount = 10000;
|
||||
const inputString = `[${Array(repeatCount).fill('0').join(',')}]`;
|
||||
|
||||
tree = parser.parse(inputString);
|
||||
expect(tree.rootNode.type).toBe('program');
|
||||
expect(tree.rootNode.firstChild!.firstChild!.namedChildCount).toBe(repeatCount);
|
||||
});
|
||||
|
||||
it('can use the bash parser', async () => {
|
||||
parser.setLanguage(await Parser.Language.load(languageURL('bash')));
|
||||
tree = parser.parse('FOO=bar echo <<EOF 2> err.txt > hello.txt \nhello${FOO}\nEOF');
|
||||
expect(tree.rootNode.toString()).toBe(
|
||||
'(program ' +
|
||||
'(redirected_statement ' +
|
||||
'body: (command ' +
|
||||
'(variable_assignment name: (variable_name) value: (word)) ' +
|
||||
'name: (command_name (word))) ' +
|
||||
'redirect: (heredoc_redirect (heredoc_start) ' +
|
||||
'redirect: (file_redirect descriptor: (file_descriptor) destination: (word)) ' +
|
||||
'redirect: (file_redirect destination: (word)) ' +
|
||||
'(heredoc_body ' +
|
||||
'(expansion (variable_name)) (heredoc_content)) (heredoc_end))))'
|
||||
);
|
||||
}, { timeout: 5000 });
|
||||
|
||||
it('can use the c++ parser', { timeout: 5000 }, async () => {
|
||||
parser.setLanguage(await Parser.Language.load(languageURL('cpp')));
|
||||
tree = parser.parse('const char *s = R"EOF(HELLO WORLD)EOF";');
|
||||
expect(tree.rootNode.toString()).toBe(
|
||||
'(translation_unit (declaration ' +
|
||||
'(type_qualifier) ' +
|
||||
'type: (primitive_type) ' +
|
||||
'declarator: (init_declarator ' +
|
||||
'declarator: (pointer_declarator declarator: (identifier)) ' +
|
||||
'value: (raw_string_literal delimiter: (raw_string_delimiter) (raw_string_content) (raw_string_delimiter)))))'
|
||||
);
|
||||
});
|
||||
|
||||
it('can use the HTML parser', { timeout: 5000 }, async () => {
|
||||
parser.setLanguage(await Parser.Language.load(languageURL('html')));
|
||||
tree = parser.parse('<div><span><custom></custom></span></div>');
|
||||
expect(tree.rootNode.toString()).toBe(
|
||||
'(document (element (start_tag (tag_name)) (element (start_tag (tag_name)) ' +
|
||||
'(element (start_tag (tag_name)) (end_tag (tag_name))) (end_tag (tag_name))) (end_tag (tag_name))))'
|
||||
);
|
||||
});
|
||||
|
||||
it('can use the python parser', { timeout: 5000 }, async () => {
|
||||
parser.setLanguage(await Parser.Language.load(languageURL('python')));
|
||||
tree = parser.parse('class A:\n def b():\n c()');
|
||||
expect(tree.rootNode.toString()).toBe(
|
||||
'(module (class_definition ' +
|
||||
'name: (identifier) ' +
|
||||
'body: (block ' +
|
||||
'(function_definition ' +
|
||||
'name: (identifier) ' +
|
||||
'parameters: (parameters) ' +
|
||||
'body: (block (expression_statement (call ' +
|
||||
'function: (identifier) ' +
|
||||
'arguments: (argument_list))))))))'
|
||||
);
|
||||
});
|
||||
|
||||
it('can use the rust parser', { timeout: 5000 }, async () => {
|
||||
parser.setLanguage(await Parser.Language.load(languageURL('rust')));
|
||||
tree = parser.parse('const x: &\'static str = r###"hello"###;');
|
||||
expect(tree.rootNode.toString()).toBe(
|
||||
'(source_file (const_item ' +
|
||||
'name: (identifier) ' +
|
||||
'type: (reference_type (lifetime (identifier)) type: (primitive_type)) ' +
|
||||
'value: (raw_string_literal (string_content))))'
|
||||
);
|
||||
});
|
||||
|
||||
it('can use the typescript parser', { timeout: 5000 }, async () => {
|
||||
parser.setLanguage(await Parser.Language.load(languageURL('typescript')));
|
||||
tree = parser.parse('a()\nb()\n[c]');
|
||||
expect(tree.rootNode.toString()).toBe(
|
||||
'(program ' +
|
||||
'(expression_statement (call_expression function: (identifier) arguments: (arguments))) ' +
|
||||
'(expression_statement (subscript_expression ' +
|
||||
'object: (call_expression ' +
|
||||
'function: (identifier) ' +
|
||||
'arguments: (arguments)) ' +
|
||||
'index: (identifier))))'
|
||||
);
|
||||
});
|
||||
|
||||
it('can use the tsx parser', { timeout: 5000 }, async () => {
|
||||
parser.setLanguage(await Parser.Language.load(languageURL('tsx')));
|
||||
tree = parser.parse('a()\nb()\n[c]');
|
||||
expect(tree.rootNode.toString()).toBe(
|
||||
'(program ' +
|
||||
'(expression_statement (call_expression function: (identifier) arguments: (arguments))) ' +
|
||||
'(expression_statement (subscript_expression ' +
|
||||
'object: (call_expression ' +
|
||||
'function: (identifier) ' +
|
||||
'arguments: (arguments)) ' +
|
||||
'index: (identifier))))',
|
||||
|
||||
);
|
||||
});
|
||||
|
||||
it('parses only the text within the `includedRanges` if they are specified', () => {
|
||||
const sourceCode = '<% foo() %> <% bar %>';
|
||||
|
||||
const start1 = sourceCode.indexOf('foo');
|
||||
const end1 = start1 + 5;
|
||||
const start2 = sourceCode.indexOf('bar');
|
||||
const end2 = start2 + 3;
|
||||
|
||||
const tree = parser.parse(sourceCode, null, {
|
||||
includedRanges: [
|
||||
{
|
||||
startIndex: start1,
|
||||
endIndex: end1,
|
||||
startPosition: { row: 0, column: start1 },
|
||||
endPosition: { row: 0, column: end1 },
|
||||
},
|
||||
{
|
||||
startIndex: start2,
|
||||
endIndex: end2,
|
||||
startPosition: { row: 0, column: start2 },
|
||||
endPosition: { row: 0, column: end2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(tree.rootNode.toString()).toBe(
|
||||
'(program ' +
|
||||
'(expression_statement (call_expression function: (identifier) arguments: (arguments))) ' +
|
||||
'(expression_statement (identifier)))'
|
||||
);
|
||||
});
|
||||
|
||||
it('parses with a timeout', { timeout: 5000 }, () => {
|
||||
parser.setLanguage(JSON);
|
||||
|
||||
const startTime = performance.now();
|
||||
let currentByteOffset = 0;
|
||||
const progressCallback = (state: TSParser.State) => {
|
||||
expect(state.currentOffset).toBeGreaterThanOrEqual(currentByteOffset);
|
||||
currentByteOffset = state.currentOffset;
|
||||
|
||||
if (performance.now() - startTime > 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
expect(() => parser.parse(
|
||||
(offset, _) => offset === 0 ? '[' : ',0',
|
||||
null,
|
||||
{ progressCallback },
|
||||
)
|
||||
).toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,13 +1,22 @@
|
|||
const {assert} = require('chai');
|
||||
let Parser; let JavaScript;
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
|
||||
import TSParser, { type Language, type Tree, type Query, type QueryCapture, type QueryMatch } from 'web-tree-sitter';
|
||||
import helper from './helper';
|
||||
|
||||
let Parser: typeof TSParser;
|
||||
let JavaScript: Language;
|
||||
|
||||
describe('Query', () => {
|
||||
let parser; let tree; let query;
|
||||
let parser: TSParser;
|
||||
let tree: Tree | null;
|
||||
let query: Query | null;
|
||||
|
||||
before(async () => ({Parser, JavaScript} = await require('./helper')));
|
||||
beforeAll(async () => {
|
||||
({ Parser, JavaScript } = await helper);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new Parser().setLanguage(JavaScript);
|
||||
parser = new Parser();
|
||||
parser.setLanguage(JavaScript);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -18,36 +27,39 @@ describe('Query', () => {
|
|||
|
||||
describe('construction', () => {
|
||||
it('throws an error on invalid patterns', () => {
|
||||
assert.throws(() => {
|
||||
expect(() => {
|
||||
JavaScript.query('(function_declaration wat)');
|
||||
}, 'Bad syntax at offset 22: \'wat)\'...');
|
||||
assert.throws(() => {
|
||||
}).toThrow('Bad syntax at offset 22: \'wat)\'...');
|
||||
|
||||
expect(() => {
|
||||
JavaScript.query('(non_existent)');
|
||||
}, 'Bad node name \'non_existent\'');
|
||||
assert.throws(() => {
|
||||
}).toThrow('Bad node name \'non_existent\'');
|
||||
|
||||
expect(() => {
|
||||
JavaScript.query('(a)');
|
||||
}, 'Bad node name \'a\'');
|
||||
assert.throws(() => {
|
||||
}).toThrow('Bad node name \'a\'');
|
||||
|
||||
expect(() => {
|
||||
JavaScript.query('(function_declaration non_existent:(identifier))');
|
||||
}, 'Bad field name \'non_existent\'');
|
||||
assert.throws(() => {
|
||||
}).toThrow('Bad field name \'non_existent\'');
|
||||
|
||||
expect(() => {
|
||||
JavaScript.query('(function_declaration name:(statement_block))');
|
||||
}, 'Bad pattern structure at offset 22: \'name:(statement_block))\'');
|
||||
}).toThrow('Bad pattern structure at offset 22: \'name:(statement_block))\'');
|
||||
});
|
||||
|
||||
it('throws an error on invalid predicates', () => {
|
||||
assert.throws(() => {
|
||||
expect(() => {
|
||||
JavaScript.query('((identifier) @abc (#eq? @ab hi))');
|
||||
}, 'Bad capture name @ab');
|
||||
assert.throws(() => {
|
||||
JavaScript.query('((identifier) @abc (#eq? @ab hi))');
|
||||
}, 'Bad capture name @ab');
|
||||
assert.throws(() => {
|
||||
}).toThrow('Bad capture name @ab');
|
||||
|
||||
expect(() => {
|
||||
JavaScript.query('((identifier) @abc (#eq?))');
|
||||
}, 'Wrong number of arguments to `#eq?` predicate. Expected 2, got 0');
|
||||
assert.throws(() => {
|
||||
}).toThrow('Wrong number of arguments to `#eq?` predicate. Expected 2, got 0');
|
||||
|
||||
expect(() => {
|
||||
JavaScript.query('((identifier) @a (#eq? @a @a @a))');
|
||||
}, 'Wrong number of arguments to `#eq?` predicate. Expected 2, got 3');
|
||||
}).toThrow('Wrong number of arguments to `#eq?` predicate. Expected 2, got 3');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -59,28 +71,28 @@ describe('Query', () => {
|
|||
(call_expression function: (identifier) @fn-ref)
|
||||
`);
|
||||
const matches = query.matches(tree.rootNode);
|
||||
assert.deepEqual(formatMatches(matches), [
|
||||
{pattern: 0, captures: [{name: 'fn-def', text: 'one'}]},
|
||||
{pattern: 1, captures: [{name: 'fn-ref', text: 'two'}]},
|
||||
{pattern: 0, captures: [{name: 'fn-def', text: 'three'}]},
|
||||
expect(formatMatches(matches)).toEqual([
|
||||
{ pattern: 0, captures: [{ name: 'fn-def', text: 'one' }] },
|
||||
{ pattern: 1, captures: [{ name: 'fn-ref', text: 'two' }] },
|
||||
{ pattern: 0, captures: [{ name: 'fn-def', text: 'three' }] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('can search in a specified ranges', () => {
|
||||
it('can search in specified ranges', () => {
|
||||
tree = parser.parse('[a, b,\nc, d,\ne, f,\ng, h]');
|
||||
query = JavaScript.query('(identifier) @element');
|
||||
const matches = query.matches(
|
||||
tree.rootNode,
|
||||
{
|
||||
startPosition: {row: 1, column: 1},
|
||||
endPosition: {row: 3, column: 1},
|
||||
},
|
||||
startPosition: { row: 1, column: 1 },
|
||||
endPosition: { row: 3, column: 1 },
|
||||
}
|
||||
);
|
||||
assert.deepEqual(formatMatches(matches), [
|
||||
{pattern: 0, captures: [{name: 'element', text: 'd'}]},
|
||||
{pattern: 0, captures: [{name: 'element', text: 'e'}]},
|
||||
{pattern: 0, captures: [{name: 'element', text: 'f'}]},
|
||||
{pattern: 0, captures: [{name: 'element', text: 'g'}]},
|
||||
expect(formatMatches(matches)).toEqual([
|
||||
{ pattern: 0, captures: [{ name: 'element', text: 'd' }] },
|
||||
{ pattern: 0, captures: [{ name: 'element', text: 'e' }] },
|
||||
{ pattern: 0, captures: [{ name: 'element', text: 'f' }] },
|
||||
{ pattern: 0, captures: [{ name: 'element', text: 'g' }] },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -104,9 +116,9 @@ describe('Query', () => {
|
|||
`);
|
||||
|
||||
const matches = query.matches(tree.rootNode);
|
||||
assert.deepEqual(formatMatches(matches), [
|
||||
{pattern: 0, captures: [{name: 'name', text: 'giraffe'}]},
|
||||
{pattern: 0, captures: [{name: 'name', text: 'gross'}]},
|
||||
expect(formatMatches(matches)).toEqual([
|
||||
{ pattern: 0, captures: [{ name: 'name', text: 'giraffe' }] },
|
||||
{ pattern: 0, captures: [{ name: 'name', text: 'gross' }] },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -122,8 +134,8 @@ describe('Query', () => {
|
|||
`);
|
||||
|
||||
const matches = query.matches(tree.rootNode);
|
||||
assert.deepEqual(formatMatches(matches), [
|
||||
{pattern: 0, captures: [{name: 'variable.builtin', text: 'window'}]},
|
||||
expect(formatMatches(matches)).toEqual([
|
||||
{ pattern: 0, captures: [{ name: 'variable.builtin', text: 'window' }] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -156,19 +168,19 @@ describe('Query', () => {
|
|||
`);
|
||||
|
||||
const captures = query.captures(tree.rootNode);
|
||||
assert.deepEqual(formatCaptures(captures), [
|
||||
{name: 'method.def', text: 'bc'},
|
||||
{name: 'delimiter', text: ':'},
|
||||
{name: 'method.alias', text: 'de'},
|
||||
{name: 'function.def', text: 'fg'},
|
||||
{name: 'operator', text: '='},
|
||||
{name: 'function.alias', text: 'hi'},
|
||||
{name: 'method.def', text: 'jk'},
|
||||
{name: 'delimiter', text: ':'},
|
||||
{name: 'method.alias', text: 'lm'},
|
||||
{name: 'function.def', text: 'no'},
|
||||
{name: 'operator', text: '='},
|
||||
{name: 'function.alias', text: 'pq'},
|
||||
expect(formatCaptures(captures)).toEqual([
|
||||
{ name: 'method.def', text: 'bc' },
|
||||
{ name: 'delimiter', text: ':' },
|
||||
{ name: 'method.alias', text: 'de' },
|
||||
{ name: 'function.def', text: 'fg' },
|
||||
{ name: 'operator', text: '=' },
|
||||
{ name: 'function.alias', text: 'hi' },
|
||||
{ name: 'method.def', text: 'jk' },
|
||||
{ name: 'delimiter', text: ':' },
|
||||
{ name: 'method.alias', text: 'lm' },
|
||||
{ name: 'function.def', text: 'no' },
|
||||
{ name: 'operator', text: '=' },
|
||||
{ name: 'function.alias', text: 'pq' },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -197,21 +209,21 @@ describe('Query', () => {
|
|||
`);
|
||||
|
||||
const captures = query.captures(tree.rootNode);
|
||||
assert.deepEqual(formatCaptures(captures), [
|
||||
{name: 'variable', text: 'panda'},
|
||||
{name: 'variable', text: 'toad'},
|
||||
{name: 'variable', text: 'ab'},
|
||||
{name: 'variable', text: 'require'},
|
||||
{name: 'function.builtin', text: 'require'},
|
||||
{name: 'variable', text: 'Cd'},
|
||||
{name: 'constructor', text: 'Cd'},
|
||||
{name: 'variable', text: 'EF'},
|
||||
{name: 'constructor', text: 'EF'},
|
||||
{name: 'constant', text: 'EF'},
|
||||
expect(formatCaptures(captures)).toEqual([
|
||||
{ name: 'variable', text: 'panda' },
|
||||
{ name: 'variable', text: 'toad' },
|
||||
{ name: 'variable', text: 'ab' },
|
||||
{ name: 'variable', text: 'require' },
|
||||
{ name: 'function.builtin', text: 'require' },
|
||||
{ name: 'variable', text: 'Cd' },
|
||||
{ name: 'constructor', text: 'Cd' },
|
||||
{ name: 'variable', text: 'EF' },
|
||||
{ name: 'constructor', text: 'EF' },
|
||||
{ name: 'constant', text: 'EF' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles conditions that compare the text of capture to each other', () => {
|
||||
it('handles conditions that compare the text of captures to each other', () => {
|
||||
tree = parser.parse(`
|
||||
ab = abc + 1;
|
||||
def = de + 1;
|
||||
|
|
@ -229,9 +241,9 @@ describe('Query', () => {
|
|||
`);
|
||||
|
||||
const captures = query.captures(tree.rootNode);
|
||||
assert.deepEqual(formatCaptures(captures), [
|
||||
{name: 'id1', text: 'ghi'},
|
||||
{name: 'id2', text: 'ghi'},
|
||||
expect(formatCaptures(captures)).toEqual([
|
||||
{ name: 'id1', text: 'ghi' },
|
||||
{ name: 'id2', text: 'ghi' },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -248,16 +260,20 @@ describe('Query', () => {
|
|||
`);
|
||||
|
||||
const captures = query.captures(tree.rootNode);
|
||||
assert.deepEqual(formatCaptures(captures), [
|
||||
{name: 'func', text: 'a', setProperties: {foo: null, bar: 'baz'}},
|
||||
expect(formatCaptures(captures)).toEqual([
|
||||
{
|
||||
name: 'func',
|
||||
text: 'a',
|
||||
setProperties: { foo: null, bar: 'baz' }
|
||||
},
|
||||
{
|
||||
name: 'prop',
|
||||
text: 'c',
|
||||
assertedProperties: {foo: null},
|
||||
refutedProperties: {bar: 'baz'},
|
||||
assertedProperties: { foo: null },
|
||||
refutedProperties: { bar: 'baz' },
|
||||
},
|
||||
]);
|
||||
assert.ok(!query.didExceedMatchLimit());
|
||||
expect(query.didExceedMatchLimit()).toBe(false);
|
||||
});
|
||||
|
||||
it('detects queries with too many permutations to track', () => {
|
||||
|
|
@ -275,90 +291,81 @@ describe('Query', () => {
|
|||
(array (identifier) @pre (identifier) @post)
|
||||
`);
|
||||
|
||||
query.captures(tree.rootNode, {matchLimit: 32});
|
||||
assert.ok(query.didExceedMatchLimit());
|
||||
query.captures(tree.rootNode, { matchLimit: 32 });
|
||||
expect(query.didExceedMatchLimit()).toBe(true);
|
||||
});
|
||||
|
||||
it('handles quantified captures properly', () => {
|
||||
let captures;
|
||||
|
||||
tree = parser.parse(`
|
||||
/// foo
|
||||
/// bar
|
||||
/// baz
|
||||
`);
|
||||
|
||||
query = JavaScript.query(`
|
||||
(
|
||||
(comment)+ @foo
|
||||
(#any-eq? @foo "/// foo")
|
||||
)
|
||||
`);
|
||||
|
||||
const expectCount = (tree, queryText, expectedCount) => {
|
||||
const expectCount = (tree: Tree, queryText: string, expectedCount: number) => {
|
||||
query = JavaScript.query(queryText);
|
||||
captures = query.captures(tree.rootNode);
|
||||
assert.equal(captures.length, expectedCount);
|
||||
const captures = query.captures(tree.rootNode);
|
||||
expect(captures).toHaveLength(expectedCount);
|
||||
};
|
||||
|
||||
expectCount(
|
||||
tree,
|
||||
`((comment)+ @foo (#any-eq? @foo "/// foo"))`,
|
||||
3,
|
||||
3
|
||||
);
|
||||
|
||||
expectCount(
|
||||
tree,
|
||||
`((comment)+ @foo (#eq? @foo "/// foo"))`,
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
expectCount(
|
||||
tree,
|
||||
`((comment)+ @foo (#any-not-eq? @foo "/// foo"))`,
|
||||
3,
|
||||
3
|
||||
);
|
||||
|
||||
expectCount(
|
||||
tree,
|
||||
`((comment)+ @foo (#not-eq? @foo "/// foo"))`,
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
expectCount(
|
||||
tree,
|
||||
`((comment)+ @foo (#match? @foo "^/// foo"))`,
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
expectCount(
|
||||
tree,
|
||||
`((comment)+ @foo (#any-match? @foo "^/// foo"))`,
|
||||
3,
|
||||
3
|
||||
);
|
||||
|
||||
expectCount(
|
||||
tree,
|
||||
`((comment)+ @foo (#not-match? @foo "^/// foo"))`,
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
expectCount(
|
||||
tree,
|
||||
`((comment)+ @foo (#not-match? @foo "fsdfsdafdfs"))`,
|
||||
3,
|
||||
3
|
||||
);
|
||||
|
||||
expectCount(
|
||||
tree,
|
||||
`((comment)+ @foo (#any-not-match? @foo "^///"))`,
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
expectCount(
|
||||
tree,
|
||||
`((comment)+ @foo (#any-not-match? @foo "^/// foo"))`,
|
||||
3,
|
||||
3
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -381,37 +388,39 @@ describe('Query', () => {
|
|||
"if" @d
|
||||
`);
|
||||
|
||||
assert.deepEqual(query.predicatesForPattern(0), [
|
||||
expect(query.predicatesForPattern(0)).toStrictEqual([
|
||||
{
|
||||
operator: 'something?',
|
||||
operands: [
|
||||
{type: 'capture', name: 'a'},
|
||||
{type: 'capture', name: 'b'},
|
||||
{ type: 'capture', name: 'a' },
|
||||
{ type: 'capture', name: 'b' },
|
||||
],
|
||||
},
|
||||
{
|
||||
operator: 'something-else?',
|
||||
operands: [
|
||||
{type: 'capture', name: 'a'},
|
||||
{type: 'string', value: 'A'},
|
||||
{type: 'capture', name: 'b'},
|
||||
{type: 'string', value: 'B'},
|
||||
{ type: 'capture', name: 'a' },
|
||||
{ type: 'string', value: 'A' },
|
||||
{ type: 'capture', name: 'b' },
|
||||
{ type: 'string', value: 'B' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(query.predicatesForPattern(1), [
|
||||
|
||||
expect(query.predicatesForPattern(1)).toStrictEqual([
|
||||
{
|
||||
operator: 'hello!',
|
||||
operands: [{type: 'capture', name: 'c'}],
|
||||
operands: [{ type: 'capture', name: 'c' }],
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(query.predicatesForPattern(2), []);
|
||||
|
||||
expect(query.predicatesForPattern(2)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.disableCapture', () => {
|
||||
it('disables a capture', () => {
|
||||
const query = JavaScript.query(`
|
||||
query = JavaScript.query(`
|
||||
(function_declaration
|
||||
(identifier) @name1 @name2 @name3
|
||||
(statement_block) @body1 @body2)
|
||||
|
|
@ -421,15 +430,15 @@ describe('Query', () => {
|
|||
const tree = parser.parse(source);
|
||||
|
||||
let matches = query.matches(tree.rootNode);
|
||||
assert.deepEqual(formatMatches(matches), [
|
||||
expect(formatMatches(matches)).toEqual([
|
||||
{
|
||||
pattern: 0,
|
||||
captures: [
|
||||
{name: 'name1', text: 'foo'},
|
||||
{name: 'name2', text: 'foo'},
|
||||
{name: 'name3', text: 'foo'},
|
||||
{name: 'body1', text: '{ return 1; }'},
|
||||
{name: 'body2', text: '{ return 1; }'},
|
||||
{ name: 'name1', text: 'foo' },
|
||||
{ name: 'name2', text: 'foo' },
|
||||
{ name: 'name3', text: 'foo' },
|
||||
{ name: 'body1', text: '{ return 1; }' },
|
||||
{ name: 'body2', text: '{ return 1; }' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
|
@ -438,31 +447,32 @@ describe('Query', () => {
|
|||
// single node.
|
||||
query.disableCapture('name2');
|
||||
matches = query.matches(tree.rootNode);
|
||||
assert.deepEqual(formatMatches(matches), [
|
||||
expect(formatMatches(matches)).toEqual([
|
||||
{
|
||||
pattern: 0,
|
||||
captures: [
|
||||
{name: 'name1', text: 'foo'},
|
||||
{name: 'name3', text: 'foo'},
|
||||
{name: 'body1', text: '{ return 1; }'},
|
||||
{name: 'body2', text: '{ return 1; }'},
|
||||
{ name: 'name1', text: 'foo' },
|
||||
{ name: 'name3', text: 'foo' },
|
||||
{ name: 'body1', text: '{ return 1; }' },
|
||||
{ name: 'body2', text: '{ return 1; }' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Set a timeout', () =>
|
||||
describe('Set a timeout', () => {
|
||||
it('returns less than the expected matches', () => {
|
||||
tree = parser.parse('function foo() while (true) { } }\n'.repeat(1000));
|
||||
query = JavaScript.query(
|
||||
'(function_declaration name: (identifier) @function)',
|
||||
'(function_declaration name: (identifier) @function)'
|
||||
);
|
||||
const matches = query.matches(tree.rootNode, {timeoutMicros: 1000});
|
||||
assert.isBelow(matches.length, 1000);
|
||||
const matches2 = query.matches(tree.rootNode, {timeoutMicros: 0});
|
||||
assert.equal(matches2.length, 1000);
|
||||
}));
|
||||
const matches = query.matches(tree.rootNode, { timeoutMicros: 1000 });
|
||||
expect(matches.length).toBeLessThan(1000);
|
||||
const matches2 = query.matches(tree.rootNode, { timeoutMicros: 0 });
|
||||
expect(matches2).toHaveLength(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Start and end indices for patterns', () => {
|
||||
it('Returns the start and end indices for a pattern', () => {
|
||||
|
|
@ -489,22 +499,17 @@ describe('Query', () => {
|
|||
|
||||
const query = JavaScript.query(source);
|
||||
|
||||
assert.equal(query.startIndexForPattern(0), 0);
|
||||
assert.equal(query.endIndexForPattern(0), '"+" @operator\n'.length);
|
||||
assert.equal(query.startIndexForPattern(5), patterns1.length);
|
||||
assert.equal(
|
||||
query.endIndexForPattern(5),
|
||||
patterns1.length + '(identifier) @a\n'.length,
|
||||
expect(query.startIndexForPattern(0)).toBe(0);
|
||||
expect(query.endIndexForPattern(0)).toBe('"+" @operator\n'.length);
|
||||
expect(query.startIndexForPattern(5)).toBe(patterns1.length);
|
||||
expect(query.endIndexForPattern(5)).toBe(
|
||||
patterns1.length + '(identifier) @a\n'.length
|
||||
);
|
||||
assert.equal(
|
||||
query.startIndexForPattern(7),
|
||||
patterns1.length + patterns2.length,
|
||||
);
|
||||
assert.equal(
|
||||
query.endIndexForPattern(7),
|
||||
expect(query.startIndexForPattern(7)).toBe(patterns1.length + patterns2.length);
|
||||
expect(query.endIndexForPattern(7)).toBe(
|
||||
patterns1.length +
|
||||
patterns2.length +
|
||||
'((identifier) @b (#match? @b i))\n'.length,
|
||||
patterns2.length +
|
||||
'((identifier) @b (#match? @b i))\n'.length
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -525,12 +530,12 @@ describe('Query', () => {
|
|||
const source = 'class A { constructor() {} } function b() { return 1; }';
|
||||
tree = parser.parse(source);
|
||||
const matches = query.matches(tree.rootNode);
|
||||
assert.deepEqual(formatMatches(matches), [
|
||||
expect(formatMatches(matches)).toEqual([
|
||||
{
|
||||
pattern: 3,
|
||||
captures: [{name: 'body', text: '{ constructor() {} }'}],
|
||||
captures: [{ name: 'body', text: '{ constructor() {} }' }],
|
||||
},
|
||||
{pattern: 1, captures: [{name: 'body', text: '{ return 1; }'}]},
|
||||
{ pattern: 1, captures: [{ name: 'body', text: '{ return 1; }' }] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -539,7 +544,7 @@ describe('Query', () => {
|
|||
it('Returns less than the expected matches', () => {
|
||||
tree = parser.parse('function foo() while (true) { } }\n'.repeat(1000));
|
||||
query = JavaScript.query(
|
||||
'(function_declaration) @function',
|
||||
'(function_declaration) @function'
|
||||
);
|
||||
|
||||
const startTime = performance.now();
|
||||
|
|
@ -553,24 +558,25 @@ describe('Query', () => {
|
|||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
assert.isBelow(matches.length, 1000);
|
||||
expect(matches.length).toBeLessThan(1000);
|
||||
|
||||
const matches2 = query.matches(tree.rootNode);
|
||||
assert.equal(matches2.length, 1000);
|
||||
expect(matches2).toHaveLength(1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function formatMatches(matches) {
|
||||
return matches.map(({pattern, captures}) => ({
|
||||
// Helper functions
|
||||
function formatMatches(matches: any[]): QueryMatch[] {
|
||||
return matches.map(({ pattern, captures }) => ({
|
||||
pattern,
|
||||
captures: formatCaptures(captures),
|
||||
}));
|
||||
}
|
||||
|
||||
function formatCaptures(captures) {
|
||||
function formatCaptures(captures: any[]): QueryCapture[] {
|
||||
return captures.map((c) => {
|
||||
const node = c.node;
|
||||
delete c.node;
|
||||
|
|
@ -1,426 +0,0 @@
|
|||
const {assert} = require('chai');
|
||||
let Parser; let JavaScript;
|
||||
|
||||
describe('Tree', () => {
|
||||
let parser; let tree;
|
||||
|
||||
before(async () =>
|
||||
({Parser, JavaScript} = await require('./helper')),
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new Parser().setLanguage(JavaScript);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
parser.delete();
|
||||
tree.delete();
|
||||
});
|
||||
|
||||
describe('.edit', () => {
|
||||
let input; let edit;
|
||||
|
||||
it('updates the positions of nodes', () => {
|
||||
input = 'abc + cde';
|
||||
tree = parser.parse(input);
|
||||
assert.equal(
|
||||
tree.rootNode.toString(),
|
||||
'(program (expression_statement (binary_expression left: (identifier) right: (identifier))))',
|
||||
);
|
||||
|
||||
let sumNode = tree.rootNode.firstChild.firstChild;
|
||||
let variableNode1 = sumNode.firstChild;
|
||||
let variableNode2 = sumNode.lastChild;
|
||||
assert.equal(variableNode1.startIndex, 0);
|
||||
assert.equal(variableNode1.endIndex, 3);
|
||||
assert.equal(variableNode2.startIndex, 6);
|
||||
assert.equal(variableNode2.endIndex, 9);
|
||||
|
||||
([input, edit] = spliceInput(input, input.indexOf('bc'), 0, ' * '));
|
||||
assert.equal(input, 'a * bc + cde');
|
||||
tree.edit(edit);
|
||||
|
||||
sumNode = tree.rootNode.firstChild.firstChild;
|
||||
variableNode1 = sumNode.firstChild;
|
||||
variableNode2 = sumNode.lastChild;
|
||||
assert.equal(variableNode1.startIndex, 0);
|
||||
assert.equal(variableNode1.endIndex, 6);
|
||||
assert.equal(variableNode2.startIndex, 9);
|
||||
assert.equal(variableNode2.endIndex, 12);
|
||||
|
||||
tree = parser.parse(input, tree);
|
||||
assert.equal(
|
||||
tree.rootNode.toString(),
|
||||
'(program (expression_statement (binary_expression left: (binary_expression left: (identifier) right: (identifier)) right: (identifier))))',
|
||||
);
|
||||
});
|
||||
|
||||
it('handles non-ascii characters', () => {
|
||||
input = 'αβδ + cde';
|
||||
|
||||
tree = parser.parse(input);
|
||||
assert.equal(
|
||||
tree.rootNode.toString(),
|
||||
'(program (expression_statement (binary_expression left: (identifier) right: (identifier))))',
|
||||
);
|
||||
|
||||
let variableNode = tree.rootNode.firstChild.firstChild.lastChild;
|
||||
|
||||
([input, edit] = spliceInput(input, input.indexOf('δ'), 0, '👍 * '));
|
||||
assert.equal(input, 'αβ👍 * δ + cde');
|
||||
tree.edit(edit);
|
||||
|
||||
variableNode = tree.rootNode.firstChild.firstChild.lastChild;
|
||||
assert.equal(variableNode.startIndex, input.indexOf('cde'));
|
||||
|
||||
tree = parser.parse(input, tree);
|
||||
assert.equal(
|
||||
tree.rootNode.toString(),
|
||||
'(program (expression_statement (binary_expression left: (binary_expression left: (identifier) right: (identifier)) right: (identifier))))',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getChangedRanges(previous)', () => {
|
||||
it('reports the ranges of text whose syntactic meaning has changed', () => {
|
||||
let sourceCode = 'abcdefg + hij';
|
||||
tree = parser.parse(sourceCode);
|
||||
|
||||
assert.equal(
|
||||
tree.rootNode.toString(),
|
||||
'(program (expression_statement (binary_expression left: (identifier) right: (identifier))))',
|
||||
);
|
||||
|
||||
sourceCode = 'abc + defg + hij';
|
||||
tree.edit({
|
||||
startIndex: 2,
|
||||
oldEndIndex: 2,
|
||||
newEndIndex: 5,
|
||||
startPosition: {row: 0, column: 2},
|
||||
oldEndPosition: {row: 0, column: 2},
|
||||
newEndPosition: {row: 0, column: 5},
|
||||
});
|
||||
|
||||
const tree2 = parser.parse(sourceCode, tree);
|
||||
assert.equal(
|
||||
tree2.rootNode.toString(),
|
||||
'(program (expression_statement (binary_expression left: (binary_expression left: (identifier) right: (identifier)) right: (identifier))))',
|
||||
);
|
||||
|
||||
const ranges = tree.getChangedRanges(tree2);
|
||||
assert.deepEqual(ranges, [
|
||||
{
|
||||
startIndex: 0,
|
||||
endIndex: 'abc + defg'.length,
|
||||
startPosition: {row: 0, column: 0},
|
||||
endPosition: {row: 0, column: 'abc + defg'.length},
|
||||
},
|
||||
]);
|
||||
|
||||
tree2.delete();
|
||||
});
|
||||
|
||||
it('throws an exception if the argument is not a tree', () => {
|
||||
tree = parser.parse('abcdefg + hij');
|
||||
|
||||
assert.throws(() => {
|
||||
tree.getChangedRanges({});
|
||||
}, /Argument must be a Tree/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.walk()', () => {
|
||||
let cursor;
|
||||
|
||||
afterEach(() => {
|
||||
cursor.delete();
|
||||
});
|
||||
|
||||
it('returns a cursor that can be used to walk the tree', () => {
|
||||
tree = parser.parse('a * b + c / d');
|
||||
cursor = tree.walk();
|
||||
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'program',
|
||||
nodeIsNamed: true,
|
||||
startPosition: {row: 0, column: 0},
|
||||
endPosition: {row: 0, column: 13},
|
||||
startIndex: 0,
|
||||
endIndex: 13,
|
||||
});
|
||||
|
||||
assert(cursor.gotoFirstChild());
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'expression_statement',
|
||||
nodeIsNamed: true,
|
||||
startPosition: {row: 0, column: 0},
|
||||
endPosition: {row: 0, column: 13},
|
||||
startIndex: 0,
|
||||
endIndex: 13,
|
||||
});
|
||||
|
||||
assert(cursor.gotoFirstChild());
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'binary_expression',
|
||||
nodeIsNamed: true,
|
||||
startPosition: {row: 0, column: 0},
|
||||
endPosition: {row: 0, column: 13},
|
||||
startIndex: 0,
|
||||
endIndex: 13,
|
||||
});
|
||||
|
||||
assert(cursor.gotoFirstChild());
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'binary_expression',
|
||||
nodeIsNamed: true,
|
||||
startPosition: {row: 0, column: 0},
|
||||
endPosition: {row: 0, column: 5},
|
||||
startIndex: 0,
|
||||
endIndex: 5,
|
||||
});
|
||||
|
||||
assert(cursor.gotoFirstChild());
|
||||
assert.equal(cursor.nodeText, 'a');
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'identifier',
|
||||
nodeIsNamed: true,
|
||||
startPosition: {row: 0, column: 0},
|
||||
endPosition: {row: 0, column: 1},
|
||||
startIndex: 0,
|
||||
endIndex: 1,
|
||||
});
|
||||
|
||||
assert(!cursor.gotoFirstChild());
|
||||
assert(cursor.gotoNextSibling());
|
||||
assert.equal(cursor.nodeText, '*');
|
||||
assertCursorState(cursor, {
|
||||
nodeType: '*',
|
||||
nodeIsNamed: false,
|
||||
startPosition: {row: 0, column: 2},
|
||||
endPosition: {row: 0, column: 3},
|
||||
startIndex: 2,
|
||||
endIndex: 3,
|
||||
});
|
||||
|
||||
assert(cursor.gotoNextSibling());
|
||||
assert.equal(cursor.nodeText, 'b');
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'identifier',
|
||||
nodeIsNamed: true,
|
||||
startPosition: {row: 0, column: 4},
|
||||
endPosition: {row: 0, column: 5},
|
||||
startIndex: 4,
|
||||
endIndex: 5,
|
||||
});
|
||||
|
||||
assert(!cursor.gotoNextSibling());
|
||||
assert(cursor.gotoParent());
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'binary_expression',
|
||||
nodeIsNamed: true,
|
||||
startPosition: {row: 0, column: 0},
|
||||
endPosition: {row: 0, column: 5},
|
||||
startIndex: 0,
|
||||
endIndex: 5,
|
||||
});
|
||||
|
||||
assert(cursor.gotoNextSibling());
|
||||
assertCursorState(cursor, {
|
||||
nodeType: '+',
|
||||
nodeIsNamed: false,
|
||||
startPosition: {row: 0, column: 6},
|
||||
endPosition: {row: 0, column: 7},
|
||||
startIndex: 6,
|
||||
endIndex: 7,
|
||||
});
|
||||
|
||||
assert(cursor.gotoNextSibling());
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'binary_expression',
|
||||
nodeIsNamed: true,
|
||||
startPosition: {row: 0, column: 8},
|
||||
endPosition: {row: 0, column: 13},
|
||||
startIndex: 8,
|
||||
endIndex: 13,
|
||||
});
|
||||
|
||||
const copy = tree.walk();
|
||||
copy.resetTo(cursor);
|
||||
|
||||
assert(copy.gotoPreviousSibling());
|
||||
assertCursorState(copy, {
|
||||
nodeType: '+',
|
||||
nodeIsNamed: false,
|
||||
startPosition: {row: 0, column: 6},
|
||||
endPosition: {row: 0, column: 7},
|
||||
startIndex: 6,
|
||||
endIndex: 7,
|
||||
});
|
||||
|
||||
assert(copy.gotoPreviousSibling());
|
||||
assertCursorState(copy, {
|
||||
nodeType: 'binary_expression',
|
||||
nodeIsNamed: true,
|
||||
startPosition: {row: 0, column: 0},
|
||||
endPosition: {row: 0, column: 5},
|
||||
startIndex: 0,
|
||||
endIndex: 5,
|
||||
});
|
||||
|
||||
assert(copy.gotoLastChild());
|
||||
assertCursorState(copy, {
|
||||
nodeType: 'identifier',
|
||||
nodeIsNamed: true,
|
||||
startPosition: {row: 0, column: 4},
|
||||
endPosition: {row: 0, column: 5},
|
||||
startIndex: 4,
|
||||
endIndex: 5,
|
||||
});
|
||||
|
||||
assert(copy.gotoParent());
|
||||
assert(copy.gotoParent());
|
||||
assert.equal(copy.nodeType, 'binary_expression');
|
||||
assert(copy.gotoParent());
|
||||
assert.equal(copy.nodeType, 'expression_statement');
|
||||
assert(copy.gotoParent());
|
||||
assert.equal(copy.nodeType, 'program');
|
||||
assert(!copy.gotoParent());
|
||||
|
||||
assert(cursor.gotoParent());
|
||||
assert.equal(cursor.nodeType, 'binary_expression');
|
||||
assert(cursor.gotoParent());
|
||||
assert.equal(cursor.nodeType, 'expression_statement');
|
||||
assert(cursor.gotoParent());
|
||||
assert.equal(cursor.nodeType, 'program');
|
||||
assert(!cursor.gotoParent());
|
||||
});
|
||||
|
||||
it('keeps track of the field name associated with each node', () => {
|
||||
tree = parser.parse('a.b();');
|
||||
cursor = tree.walk();
|
||||
cursor.gotoFirstChild();
|
||||
cursor.gotoFirstChild();
|
||||
|
||||
assert.equal(cursor.currentNode.type, 'call_expression');
|
||||
assert.equal(cursor.currentFieldName, null);
|
||||
|
||||
cursor.gotoFirstChild();
|
||||
assert.equal(cursor.currentNode.type, 'member_expression');
|
||||
assert.equal(cursor.currentFieldName, 'function');
|
||||
|
||||
cursor.gotoFirstChild();
|
||||
assert.equal(cursor.currentNode.type, 'identifier');
|
||||
assert.equal(cursor.currentFieldName, 'object');
|
||||
|
||||
cursor.gotoNextSibling();
|
||||
cursor.gotoNextSibling();
|
||||
assert.equal(cursor.currentNode.type, 'property_identifier');
|
||||
assert.equal(cursor.currentFieldName, 'property');
|
||||
|
||||
cursor.gotoParent();
|
||||
cursor.gotoNextSibling();
|
||||
assert.equal(cursor.currentNode.type, 'arguments');
|
||||
assert.equal(cursor.currentFieldName, 'arguments');
|
||||
});
|
||||
|
||||
it('returns a cursor that can be reset anywhere in the tree', () => {
|
||||
tree = parser.parse('a * b + c / d');
|
||||
cursor = tree.walk();
|
||||
const root = tree.rootNode.firstChild;
|
||||
|
||||
cursor.reset(root.firstChild.firstChild);
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'binary_expression',
|
||||
nodeIsNamed: true,
|
||||
startPosition: {row: 0, column: 0},
|
||||
endPosition: {row: 0, column: 5},
|
||||
startIndex: 0,
|
||||
endIndex: 5,
|
||||
});
|
||||
|
||||
cursor.gotoFirstChild();
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'identifier',
|
||||
nodeIsNamed: true,
|
||||
startPosition: {row: 0, column: 0},
|
||||
endPosition: {row: 0, column: 1},
|
||||
startIndex: 0,
|
||||
endIndex: 1,
|
||||
});
|
||||
|
||||
assert(cursor.gotoParent());
|
||||
assert(!cursor.gotoParent());
|
||||
});
|
||||
});
|
||||
|
||||
describe('.copy', () => {
|
||||
it('creates another tree that remains stable if the original tree is edited', () => {
|
||||
input = 'abc + cde';
|
||||
tree = parser.parse(input);
|
||||
assert.equal(
|
||||
tree.rootNode.toString(),
|
||||
'(program (expression_statement (binary_expression left: (identifier) right: (identifier))))',
|
||||
);
|
||||
|
||||
const tree2 = tree.copy();
|
||||
[input, edit] = spliceInput(input, 3, 0, '123');
|
||||
assert.equal(input, 'abc123 + cde');
|
||||
tree.edit(edit);
|
||||
|
||||
const leftNode = tree.rootNode.firstChild.firstChild.firstChild;
|
||||
const leftNode2 = tree2.rootNode.firstChild.firstChild.firstChild;
|
||||
const rightNode = tree.rootNode.firstChild.firstChild.lastChild;
|
||||
const rightNode2 = tree2.rootNode.firstChild.firstChild.lastChild;
|
||||
assert.equal(leftNode.endIndex, 6);
|
||||
assert.equal(leftNode2.endIndex, 3);
|
||||
assert.equal(rightNode.startIndex, 9);
|
||||
assert.equal(rightNode2.startIndex, 6);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function spliceInput(input, startIndex, lengthRemoved, newText) {
|
||||
const oldEndIndex = startIndex + lengthRemoved;
|
||||
const newEndIndex = startIndex + newText.length;
|
||||
const startPosition = getExtent(input.slice(0, startIndex));
|
||||
const oldEndPosition = getExtent(input.slice(0, oldEndIndex));
|
||||
input = input.slice(0, startIndex) + newText + input.slice(oldEndIndex);
|
||||
const newEndPosition = getExtent(input.slice(0, newEndIndex));
|
||||
return [
|
||||
input,
|
||||
{
|
||||
startIndex, startPosition,
|
||||
oldEndIndex, oldEndPosition,
|
||||
newEndIndex, newEndPosition,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Gets the extent of the text in terms of zero-based row and column numbers.
|
||||
function getExtent(text) {
|
||||
let row = 0;
|
||||
let index = -1;
|
||||
let lastIndex = 0;
|
||||
while ((index = text.indexOf('\n', index + 1)) !== -1) {
|
||||
row++;
|
||||
lastIndex = index + 1;
|
||||
}
|
||||
return {row, column: text.length - lastIndex};
|
||||
}
|
||||
|
||||
function assertCursorState(cursor, params) {
|
||||
assert.equal(cursor.nodeType, params.nodeType);
|
||||
assert.equal(cursor.nodeIsNamed, params.nodeIsNamed);
|
||||
assert.deepEqual(cursor.startPosition, params.startPosition);
|
||||
assert.deepEqual(cursor.endPosition, params.endPosition);
|
||||
assert.deepEqual(cursor.startIndex, params.startIndex);
|
||||
assert.deepEqual(cursor.endIndex, params.endIndex);
|
||||
|
||||
const node = cursor.currentNode;
|
||||
assert.equal(node.type, params.nodeType);
|
||||
assert.equal(node.isNamed, params.nodeIsNamed);
|
||||
assert.deepEqual(node.startPosition, params.startPosition);
|
||||
assert.deepEqual(node.endPosition, params.endPosition);
|
||||
assert.deepEqual(node.startIndex, params.startIndex);
|
||||
assert.deepEqual(node.endIndex, params.endIndex);
|
||||
}
|
||||
443
lib/binding_web/test/tree.test.ts
Normal file
443
lib/binding_web/test/tree.test.ts
Normal file
|
|
@ -0,0 +1,443 @@
|
|||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
|
||||
import TSParser, { type Language, type Tree, type TreeCursor, type Edit, Point } from 'web-tree-sitter';
|
||||
import helper from './helper';
|
||||
|
||||
let Parser: typeof TSParser;
|
||||
let JavaScript: Language;
|
||||
|
||||
interface CursorState {
|
||||
nodeType: string;
|
||||
nodeIsNamed: boolean;
|
||||
startPosition: Point;
|
||||
endPosition: Point;
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
}
|
||||
|
||||
describe('Tree', () => {
|
||||
let parser: TSParser;
|
||||
let tree: Tree;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ Parser, JavaScript } = await helper);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new Parser();
|
||||
parser.setLanguage(JavaScript);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
parser.delete();
|
||||
tree.delete();
|
||||
});
|
||||
|
||||
describe('.edit', () => {
|
||||
let input: string;
|
||||
let edit: Edit;
|
||||
|
||||
it('updates the positions of nodes', () => {
|
||||
input = 'abc + cde';
|
||||
tree = parser.parse(input);
|
||||
expect(tree.rootNode.toString()).toBe(
|
||||
'(program (expression_statement (binary_expression left: (identifier) right: (identifier))))'
|
||||
);
|
||||
|
||||
let sumNode = tree.rootNode.firstChild!.firstChild;
|
||||
let variableNode1 = sumNode!.firstChild;
|
||||
let variableNode2 = sumNode!.lastChild;
|
||||
expect(variableNode1!.startIndex).toBe(0);
|
||||
expect(variableNode1!.endIndex).toBe(3);
|
||||
expect(variableNode2!.startIndex).toBe(6);
|
||||
expect(variableNode2!.endIndex).toBe(9);
|
||||
|
||||
[input, edit] = spliceInput(input, input.indexOf('bc'), 0, ' * ');
|
||||
expect(input).toBe('a * bc + cde');
|
||||
tree.edit(edit);
|
||||
|
||||
sumNode = tree.rootNode.firstChild!.firstChild;
|
||||
variableNode1 = sumNode!.firstChild;
|
||||
variableNode2 = sumNode!.lastChild;
|
||||
expect(variableNode1!.startIndex).toBe(0);
|
||||
expect(variableNode1!.endIndex).toBe(6);
|
||||
expect(variableNode2!.startIndex).toBe(9);
|
||||
expect(variableNode2!.endIndex).toBe(12);
|
||||
|
||||
tree = parser.parse(input, tree);
|
||||
expect(tree.rootNode.toString()).toBe(
|
||||
'(program (expression_statement (binary_expression left: (binary_expression left: (identifier) right: (identifier)) right: (identifier))))'
|
||||
);
|
||||
});
|
||||
|
||||
it('handles non-ascii characters', () => {
|
||||
input = 'αβδ + cde';
|
||||
|
||||
tree = parser.parse(input);
|
||||
expect(tree.rootNode.toString()).toBe(
|
||||
'(program (expression_statement (binary_expression left: (identifier) right: (identifier))))'
|
||||
);
|
||||
|
||||
let variableNode = tree.rootNode.firstChild!.firstChild!.lastChild;
|
||||
|
||||
[input, edit] = spliceInput(input, input.indexOf('δ'), 0, '👍 * ');
|
||||
expect(input).toBe('αβ👍 * δ + cde');
|
||||
tree.edit(edit);
|
||||
|
||||
variableNode = tree.rootNode.firstChild!.firstChild!.lastChild;
|
||||
expect(variableNode!.startIndex).toBe(input.indexOf('cde'));
|
||||
|
||||
tree = parser.parse(input, tree);
|
||||
expect(tree.rootNode.toString()).toBe(
|
||||
'(program (expression_statement (binary_expression left: (binary_expression left: (identifier) right: (identifier)) right: (identifier))))'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getChangedRanges(previous)', () => {
|
||||
it('reports the ranges of text whose syntactic meaning has changed', () => {
|
||||
let sourceCode = 'abcdefg + hij';
|
||||
tree = parser.parse(sourceCode);
|
||||
|
||||
expect(tree.rootNode.toString()).toBe(
|
||||
'(program (expression_statement (binary_expression left: (identifier) right: (identifier))))'
|
||||
);
|
||||
|
||||
sourceCode = 'abc + defg + hij';
|
||||
tree.edit({
|
||||
startIndex: 2,
|
||||
oldEndIndex: 2,
|
||||
newEndIndex: 5,
|
||||
startPosition: { row: 0, column: 2 },
|
||||
oldEndPosition: { row: 0, column: 2 },
|
||||
newEndPosition: { row: 0, column: 5 },
|
||||
});
|
||||
|
||||
const tree2 = parser.parse(sourceCode, tree);
|
||||
expect(tree2.rootNode.toString()).toBe(
|
||||
'(program (expression_statement (binary_expression left: (binary_expression left: (identifier) right: (identifier)) right: (identifier))))'
|
||||
);
|
||||
|
||||
const ranges = tree.getChangedRanges(tree2);
|
||||
expect(ranges).toEqual([
|
||||
{
|
||||
startIndex: 0,
|
||||
endIndex: 'abc + defg'.length,
|
||||
startPosition: { row: 0, column: 0 },
|
||||
endPosition: { row: 0, column: 'abc + defg'.length },
|
||||
},
|
||||
]);
|
||||
|
||||
tree2.delete();
|
||||
});
|
||||
|
||||
it('throws an exception if the argument is not a tree', () => {
|
||||
tree = parser.parse('abcdefg + hij');
|
||||
|
||||
expect(() => {
|
||||
tree.getChangedRanges({} as Tree);
|
||||
}).toThrow(/Argument must be a Tree/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.walk()', () => {
|
||||
let cursor: TreeCursor;
|
||||
|
||||
afterEach(() => {
|
||||
cursor.delete();
|
||||
});
|
||||
|
||||
it('returns a cursor that can be used to walk the tree', () => {
|
||||
tree = parser.parse('a * b + c / d');
|
||||
cursor = tree.walk();
|
||||
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'program',
|
||||
nodeIsNamed: true,
|
||||
startPosition: { row: 0, column: 0 },
|
||||
endPosition: { row: 0, column: 13 },
|
||||
startIndex: 0,
|
||||
endIndex: 13,
|
||||
});
|
||||
|
||||
expect(cursor.gotoFirstChild()).toBe(true);
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'expression_statement',
|
||||
nodeIsNamed: true,
|
||||
startPosition: { row: 0, column: 0 },
|
||||
endPosition: { row: 0, column: 13 },
|
||||
startIndex: 0,
|
||||
endIndex: 13,
|
||||
});
|
||||
|
||||
expect(cursor.gotoFirstChild()).toBe(true);
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'binary_expression',
|
||||
nodeIsNamed: true,
|
||||
startPosition: { row: 0, column: 0 },
|
||||
endPosition: { row: 0, column: 13 },
|
||||
startIndex: 0,
|
||||
endIndex: 13,
|
||||
});
|
||||
|
||||
expect(cursor.gotoFirstChild()).toBe(true);
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'binary_expression',
|
||||
nodeIsNamed: true,
|
||||
startPosition: { row: 0, column: 0 },
|
||||
endPosition: { row: 0, column: 5 },
|
||||
startIndex: 0,
|
||||
endIndex: 5,
|
||||
});
|
||||
|
||||
expect(cursor.gotoFirstChild()).toBe(true);
|
||||
expect(cursor.nodeText).toBe('a');
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'identifier',
|
||||
nodeIsNamed: true,
|
||||
startPosition: { row: 0, column: 0 },
|
||||
endPosition: { row: 0, column: 1 },
|
||||
startIndex: 0,
|
||||
endIndex: 1,
|
||||
});
|
||||
|
||||
expect(cursor.gotoFirstChild()).toBe(false);
|
||||
expect(cursor.gotoNextSibling()).toBe(true);
|
||||
expect(cursor.nodeText).toBe('*');
|
||||
assertCursorState(cursor, {
|
||||
nodeType: '*',
|
||||
nodeIsNamed: false,
|
||||
startPosition: { row: 0, column: 2 },
|
||||
endPosition: { row: 0, column: 3 },
|
||||
startIndex: 2,
|
||||
endIndex: 3,
|
||||
});
|
||||
|
||||
expect(cursor.gotoNextSibling()).toBe(true);
|
||||
expect(cursor.nodeText).toBe('b');
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'identifier',
|
||||
nodeIsNamed: true,
|
||||
startPosition: { row: 0, column: 4 },
|
||||
endPosition: { row: 0, column: 5 },
|
||||
startIndex: 4,
|
||||
endIndex: 5,
|
||||
});
|
||||
|
||||
expect(cursor.gotoNextSibling()).toBe(false);
|
||||
expect(cursor.gotoParent()).toBe(true);
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'binary_expression',
|
||||
nodeIsNamed: true,
|
||||
startPosition: { row: 0, column: 0 },
|
||||
endPosition: { row: 0, column: 5 },
|
||||
startIndex: 0,
|
||||
endIndex: 5,
|
||||
});
|
||||
|
||||
expect(cursor.gotoNextSibling()).toBe(true);
|
||||
assertCursorState(cursor, {
|
||||
nodeType: '+',
|
||||
nodeIsNamed: false,
|
||||
startPosition: { row: 0, column: 6 },
|
||||
endPosition: { row: 0, column: 7 },
|
||||
startIndex: 6,
|
||||
endIndex: 7,
|
||||
});
|
||||
|
||||
expect(cursor.gotoNextSibling()).toBe(true);
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'binary_expression',
|
||||
nodeIsNamed: true,
|
||||
startPosition: { row: 0, column: 8 },
|
||||
endPosition: { row: 0, column: 13 },
|
||||
startIndex: 8,
|
||||
endIndex: 13,
|
||||
});
|
||||
|
||||
const copy = tree.walk();
|
||||
copy.resetTo(cursor);
|
||||
|
||||
expect(copy.gotoPreviousSibling()).toBe(true);
|
||||
assertCursorState(copy, {
|
||||
nodeType: '+',
|
||||
nodeIsNamed: false,
|
||||
startPosition: { row: 0, column: 6 },
|
||||
endPosition: { row: 0, column: 7 },
|
||||
startIndex: 6,
|
||||
endIndex: 7,
|
||||
});
|
||||
|
||||
expect(copy.gotoPreviousSibling()).toBe(true);
|
||||
assertCursorState(copy, {
|
||||
nodeType: 'binary_expression',
|
||||
nodeIsNamed: true,
|
||||
startPosition: { row: 0, column: 0 },
|
||||
endPosition: { row: 0, column: 5 },
|
||||
startIndex: 0,
|
||||
endIndex: 5,
|
||||
});
|
||||
|
||||
expect(copy.gotoLastChild()).toBe(true);
|
||||
assertCursorState(copy, {
|
||||
nodeType: 'identifier',
|
||||
nodeIsNamed: true,
|
||||
startPosition: { row: 0, column: 4 },
|
||||
endPosition: { row: 0, column: 5 },
|
||||
startIndex: 4,
|
||||
endIndex: 5,
|
||||
});
|
||||
|
||||
expect(copy.gotoParent()).toBe(true);
|
||||
expect(copy.gotoParent()).toBe(true);
|
||||
expect(copy.nodeType).toBe('binary_expression');
|
||||
expect(copy.gotoParent()).toBe(true);
|
||||
expect(copy.nodeType).toBe('expression_statement');
|
||||
expect(copy.gotoParent()).toBe(true);
|
||||
expect(copy.nodeType).toBe('program');
|
||||
expect(copy.gotoParent()).toBe(false);
|
||||
copy.delete();
|
||||
|
||||
expect(cursor.gotoParent()).toBe(true);
|
||||
expect(cursor.nodeType).toBe('binary_expression');
|
||||
expect(cursor.gotoParent()).toBe(true);
|
||||
expect(cursor.nodeType).toBe('expression_statement');
|
||||
expect(cursor.gotoParent()).toBe(true);
|
||||
expect(cursor.nodeType).toBe('program');
|
||||
});
|
||||
|
||||
it('keeps track of the field name associated with each node', () => {
|
||||
tree = parser.parse('a.b();');
|
||||
cursor = tree.walk();
|
||||
cursor.gotoFirstChild();
|
||||
cursor.gotoFirstChild();
|
||||
|
||||
expect(cursor.currentNode.type).toBe('call_expression');
|
||||
expect(cursor.currentFieldName).toBeNull();
|
||||
|
||||
cursor.gotoFirstChild();
|
||||
expect(cursor.currentNode.type).toBe('member_expression');
|
||||
expect(cursor.currentFieldName).toBe('function');
|
||||
|
||||
cursor.gotoFirstChild();
|
||||
expect(cursor.currentNode.type).toBe('identifier');
|
||||
expect(cursor.currentFieldName).toBe('object');
|
||||
|
||||
cursor.gotoNextSibling();
|
||||
cursor.gotoNextSibling();
|
||||
expect(cursor.currentNode.type).toBe('property_identifier');
|
||||
expect(cursor.currentFieldName).toBe('property');
|
||||
|
||||
cursor.gotoParent();
|
||||
cursor.gotoNextSibling();
|
||||
expect(cursor.currentNode.type).toBe('arguments');
|
||||
expect(cursor.currentFieldName).toBe('arguments');
|
||||
});
|
||||
|
||||
it('returns a cursor that can be reset anywhere in the tree', () => {
|
||||
tree = parser.parse('a * b + c / d');
|
||||
cursor = tree.walk();
|
||||
const root = tree.rootNode.firstChild;
|
||||
|
||||
cursor.reset(root!.firstChild!.firstChild!);
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'binary_expression',
|
||||
nodeIsNamed: true,
|
||||
startPosition: { row: 0, column: 0 },
|
||||
endPosition: { row: 0, column: 5 },
|
||||
startIndex: 0,
|
||||
endIndex: 5,
|
||||
});
|
||||
|
||||
cursor.gotoFirstChild();
|
||||
assertCursorState(cursor, {
|
||||
nodeType: 'identifier',
|
||||
nodeIsNamed: true,
|
||||
startPosition: { row: 0, column: 0 },
|
||||
endPosition: { row: 0, column: 1 },
|
||||
startIndex: 0,
|
||||
endIndex: 1,
|
||||
});
|
||||
|
||||
expect(cursor.gotoParent()).toBe(true);
|
||||
expect(cursor.gotoParent()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.copy', () => {
|
||||
let input: string;
|
||||
let edit: Edit;
|
||||
|
||||
it('creates another tree that remains stable if the original tree is edited', () => {
|
||||
input = 'abc + cde';
|
||||
tree = parser.parse(input);
|
||||
expect(tree.rootNode.toString()).toBe(
|
||||
'(program (expression_statement (binary_expression left: (identifier) right: (identifier))))'
|
||||
);
|
||||
|
||||
const tree2 = tree.copy();
|
||||
[input, edit] = spliceInput(input, 3, 0, '123');
|
||||
expect(input).toBe('abc123 + cde');
|
||||
tree.edit(edit);
|
||||
|
||||
const leftNode = tree.rootNode.firstChild!.firstChild!.firstChild;
|
||||
const leftNode2 = tree2.rootNode.firstChild!.firstChild!.firstChild;
|
||||
const rightNode = tree.rootNode.firstChild!.firstChild!.lastChild;
|
||||
const rightNode2 = tree2.rootNode.firstChild!.firstChild!.lastChild;
|
||||
expect(leftNode!.endIndex).toBe(6);
|
||||
expect(leftNode2!.endIndex).toBe(3);
|
||||
expect(rightNode!.startIndex).toBe(9);
|
||||
expect(rightNode2!.startIndex).toBe(6);
|
||||
|
||||
tree2.delete();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function spliceInput(input: string, startIndex: number, lengthRemoved: number, newText: string): [string, Edit] {
|
||||
const oldEndIndex = startIndex + lengthRemoved;
|
||||
const newEndIndex = startIndex + newText.length;
|
||||
const startPosition = getExtent(input.slice(0, startIndex));
|
||||
const oldEndPosition = getExtent(input.slice(0, oldEndIndex));
|
||||
input = input.slice(0, startIndex) + newText + input.slice(oldEndIndex);
|
||||
const newEndPosition = getExtent(input.slice(0, newEndIndex));
|
||||
return [
|
||||
input,
|
||||
{
|
||||
startIndex,
|
||||
startPosition,
|
||||
oldEndIndex,
|
||||
oldEndPosition,
|
||||
newEndIndex,
|
||||
newEndPosition,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Gets the extent of the text in terms of zero-based row and column numbers.
|
||||
function getExtent(text: string): Point {
|
||||
let row = 0;
|
||||
let index = -1;
|
||||
let lastIndex = 0;
|
||||
while ((index = text.indexOf('\n', index + 1)) !== -1) {
|
||||
row++;
|
||||
lastIndex = index + 1;
|
||||
}
|
||||
return { row, column: text.length - lastIndex };
|
||||
}
|
||||
|
||||
function assertCursorState(cursor: TreeCursor, params: CursorState): void {
|
||||
expect(cursor.nodeType).toBe(params.nodeType);
|
||||
expect(cursor.nodeIsNamed).toBe(params.nodeIsNamed);
|
||||
expect(cursor.startPosition).toEqual(params.startPosition);
|
||||
expect(cursor.endPosition).toEqual(params.endPosition);
|
||||
expect(cursor.startIndex).toEqual(params.startIndex);
|
||||
expect(cursor.endIndex).toEqual(params.endIndex);
|
||||
|
||||
const node = cursor.currentNode;
|
||||
expect(node.type).toBe(params.nodeType);
|
||||
expect(node.isNamed).toBe(params.nodeIsNamed);
|
||||
expect(node.startPosition).toEqual(params.startPosition);
|
||||
expect(node.endPosition).toEqual(params.endPosition);
|
||||
expect(node.startIndex).toEqual(params.startIndex);
|
||||
expect(node.endIndex).toEqual(params.endIndex);
|
||||
}
|
||||
27
lib/binding_web/tree-sitter-web.d.ts
vendored
27
lib/binding_web/tree-sitter-web.d.ts
vendored
|
|
@ -8,7 +8,7 @@ declare module "web-tree-sitter" {
|
|||
delete(): void;
|
||||
parse(
|
||||
input: string | Parser.Input,
|
||||
oldTree?: Parser.Tree,
|
||||
oldTree?: Parser.Tree | null,
|
||||
options?: Parser.Options,
|
||||
): Parser.Tree;
|
||||
getIncludedRanges(): Parser.Range[];
|
||||
|
|
@ -59,7 +59,7 @@ declare module "web-tree-sitter" {
|
|||
) => void;
|
||||
|
||||
export interface Input {
|
||||
(index: number, position?: Point): string | null;
|
||||
(index: number, position?: Point): string | null | undefined;
|
||||
}
|
||||
|
||||
export interface SyntaxNode {
|
||||
|
|
@ -104,6 +104,7 @@ declare module "web-tree-sitter" {
|
|||
childForFieldName(fieldName: string): SyntaxNode | null;
|
||||
childForFieldId(fieldId: number): SyntaxNode | null;
|
||||
fieldNameForChild(childIndex: number): string | null;
|
||||
fieldNameForNamedChild(childIndex: number): string | null;
|
||||
childrenForFieldName(fieldName: string): Array<SyntaxNode>;
|
||||
childrenForFieldId(fieldId: number): Array<SyntaxNode>;
|
||||
firstChildForIndex(index: number): SyntaxNode | null;
|
||||
|
|
@ -203,9 +204,19 @@ declare module "web-tree-sitter" {
|
|||
matchLimit?: number;
|
||||
maxStartDepth?: number;
|
||||
timeoutMicros?: number;
|
||||
progressCallback: (state: QueryState) => boolean;
|
||||
progressCallback?: (state: QueryState) => boolean;
|
||||
};
|
||||
|
||||
export interface Predicate {
|
||||
operator: string;
|
||||
operands: PredicateStep[];
|
||||
}
|
||||
|
||||
type PredicateStep =
|
||||
| { type: 'string'; value: string }
|
||||
| { type: 'capture'; name: string };
|
||||
|
||||
|
||||
export interface PredicateResult {
|
||||
operator: string;
|
||||
operands: { name: string; type: string }[];
|
||||
|
|
@ -220,13 +231,13 @@ declare module "web-tree-sitter" {
|
|||
}
|
||||
|
||||
export class Query {
|
||||
captureNames: string[];
|
||||
captureQuantifiers: CaptureQuantifier[];
|
||||
readonly captureNames: string[];
|
||||
readonly captureQuantifiers: CaptureQuantifier[];
|
||||
readonly predicates: { [name: string]: Function }[];
|
||||
readonly setProperties: any[];
|
||||
readonly assertedProperties: any[];
|
||||
readonly refutedProperties: any[];
|
||||
readonly matchLimit: number;
|
||||
readonly matchLimit?: number;
|
||||
|
||||
delete(): void;
|
||||
captures(node: SyntaxNode, options?: QueryOptions): QueryCapture[];
|
||||
|
|
@ -245,17 +256,21 @@ declare module "web-tree-sitter" {
|
|||
class Language {
|
||||
static load(input: string | Uint8Array): Promise<Language>;
|
||||
|
||||
readonly name: string | null;
|
||||
readonly version: number;
|
||||
readonly fieldCount: number;
|
||||
readonly stateCount: number;
|
||||
readonly nodeTypeCount: number;
|
||||
|
||||
|
||||
fieldNameForId(fieldId: number): string | null;
|
||||
fieldIdForName(fieldName: string): number | null;
|
||||
idForNodeType(type: string, named: boolean): number;
|
||||
nodeTypeForId(typeId: number): string | null;
|
||||
nodeTypeIsNamed(typeId: number): boolean;
|
||||
nodeTypeIsVisible(typeId: number): boolean;
|
||||
get supertypes(): number[];
|
||||
subtypes(supertype: number): number[];
|
||||
nextState(stateId: number, typeId: number): number;
|
||||
query(source: string): Query;
|
||||
lookaheadIterator(stateId: number): LookaheadIterable | null;
|
||||
|
|
|
|||
37
lib/binding_web/tsconfig.json
Normal file
37
lib/binding_web/tsconfig.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "es2022",
|
||||
"lib": [
|
||||
"es2022",
|
||||
"dom"
|
||||
],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"rootDir": "./",
|
||||
"outDir": "./dist",
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"test/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
]
|
||||
}
|
||||
19
lib/binding_web/vitest.config.ts
Normal file
19
lib/binding_web/vitest.config.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
coverage: {
|
||||
include: [
|
||||
'tree-sitter.js',
|
||||
],
|
||||
exclude: [
|
||||
'test/**',
|
||||
'dist/**',
|
||||
'lib/**',
|
||||
'wasm/**'
|
||||
],
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
@ -106,7 +106,7 @@ pub fn run_wasm(args: &BuildWasm) -> Result<()> {
|
|||
let exported_functions = format!(
|
||||
"{}{}",
|
||||
fs::read_to_string("lib/src/wasm/stdlib-symbols.txt")?,
|
||||
fs::read_to_string("lib/binding_web/exports.txt")?
|
||||
fs::read_to_string("lib/binding_web/wasm/exports.txt")?
|
||||
)
|
||||
.replace('"', "")
|
||||
.lines()
|
||||
|
|
@ -120,53 +120,37 @@ pub fn run_wasm(args: &BuildWasm) -> Result<()> {
|
|||
let exported_functions = format!("EXPORTED_FUNCTIONS={exported_functions}");
|
||||
let exported_runtime_methods = "EXPORTED_RUNTIME_METHODS=stringToUTF16,AsciiToString";
|
||||
|
||||
std::env::set_var("EMCC_DEBUG_SAVE", "1");
|
||||
|
||||
#[rustfmt::skip]
|
||||
emscripten_flags.extend([
|
||||
"-s",
|
||||
"WASM=1",
|
||||
"-s",
|
||||
"INITIAL_MEMORY=33554432",
|
||||
"-s",
|
||||
"ALLOW_MEMORY_GROWTH=1",
|
||||
"-s",
|
||||
"SUPPORT_BIG_ENDIAN=1",
|
||||
"-s",
|
||||
"MAIN_MODULE=2",
|
||||
"-s",
|
||||
"FILESYSTEM=0",
|
||||
"-s",
|
||||
"NODEJS_CATCH_EXIT=0",
|
||||
"-s",
|
||||
"NODEJS_CATCH_REJECTION=0",
|
||||
"-s",
|
||||
&exported_functions,
|
||||
"-s",
|
||||
exported_runtime_methods,
|
||||
"-gsource-map",
|
||||
"--source-map-base", ".",
|
||||
"-s", "WASM=1",
|
||||
"-s", "INITIAL_MEMORY=33554432",
|
||||
"-s", "ALLOW_MEMORY_GROWTH=1",
|
||||
"-s", "SUPPORT_BIG_ENDIAN=1",
|
||||
"-s", "MAIN_MODULE=2",
|
||||
"-s", "FILESYSTEM=0",
|
||||
"-s", "NODEJS_CATCH_EXIT=0",
|
||||
"-s", "NODEJS_CATCH_REJECTION=0",
|
||||
"-s", &exported_functions,
|
||||
"-s", exported_runtime_methods,
|
||||
"-fno-exceptions",
|
||||
"-std=c11",
|
||||
"-D",
|
||||
"fprintf(...)=",
|
||||
"-D",
|
||||
"NDEBUG=",
|
||||
"-D",
|
||||
"_POSIX_C_SOURCE=200112L",
|
||||
"-D",
|
||||
"_DEFAULT_SOURCE=",
|
||||
"-I",
|
||||
"lib/src",
|
||||
"-I",
|
||||
"lib/include",
|
||||
"--js-library",
|
||||
"lib/binding_web/imports.js",
|
||||
"--pre-js",
|
||||
"lib/binding_web/prefix.js",
|
||||
"--post-js",
|
||||
"lib/binding_web/binding.js",
|
||||
"--post-js",
|
||||
"lib/binding_web/suffix.js",
|
||||
"-D", "fprintf(...)=",
|
||||
"-D", "NDEBUG=",
|
||||
"-D", "_POSIX_C_SOURCE=200112L",
|
||||
"-D", "_DEFAULT_SOURCE=",
|
||||
"-I", "lib/src",
|
||||
"-I", "lib/include",
|
||||
"--js-library", "lib/binding_web/wasm/imports.js",
|
||||
"--pre-js", "lib/binding_web/wasm/prefix.js",
|
||||
"--post-js", "lib/binding_web/dist/tree-sitter.js",
|
||||
"--post-js", "lib/binding_web/wasm/suffix.js",
|
||||
"-o", "target/scratch/tree-sitter.js",
|
||||
"lib/src/lib.c",
|
||||
"lib/binding_web/binding.c",
|
||||
"-o",
|
||||
"target/scratch/tree-sitter.js",
|
||||
"lib/binding_web/lib/tree-sitter.c",
|
||||
]);
|
||||
let command = command.args(&emscripten_flags);
|
||||
|
||||
|
|
@ -195,6 +179,11 @@ fn build_wasm(cmd: &mut Command) -> Result<()> {
|
|||
"lib/binding_web/tree-sitter.wasm",
|
||||
)?;
|
||||
|
||||
fs::rename(
|
||||
"target/scratch/tree-sitter.wasm.map",
|
||||
"lib/binding_web/tree-sitter.wasm.map",
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue