pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
114 lines (113 loc) • 4.1 kB
JavaScript
/**
* Shared TreeSitterClient singleton + ext→language resolver.
*
* web-tree-sitter's WASM runtime is module-level — one per process. TRANSFER_BUFFER
* and _ts_init are global, so every subsystem MUST share a single TreeSitterClient:
* separate clients race on init and corrupt the shared WASM heap. This module is
* that single seam — the dispatch tree-sitter runner, project scanner, module-report,
* review-graph, and fact providers all obtain their client here, so a file parsed by
* one is served from the shared tree cache for the others (one parse per write).
*
* Once the WASM runtime aborts (Emscripten abort()), the heap is corrupted with no
* in-process recovery. markTreeSitterWasmAborted() poisons the singleton so EVERY
* consumer skips further tree-sitter work (previously only the runner tracked this,
* while the other subsystems kept calling the dead runtime).
*/
import * as path from "node:path";
import { TreeSitterClient } from "./tree-sitter-client.js";
let _shared = null;
let _wasmAborted = false;
/** The process-wide TreeSitterClient, or null once the WASM runtime has aborted. */
export function getSharedTreeSitterClient() {
if (_wasmAborted)
return null;
_shared ??= new TreeSitterClient();
return _shared;
}
export function isTreeSitterWasmAborted() {
return _wasmAborted;
}
/**
* Poison the singleton after an unrecoverable Emscripten abort() — the module-level
* WASM heap is corrupted, so no client can be used again this process.
*/
export function markTreeSitterWasmAborted() {
_wasmAborted = true;
_shared = null;
}
/** Test-only: reset the singleton + abort flag. */
export function _resetSharedTreeSitterClientForTests() {
_shared = null;
_wasmAborted = false;
}
// Grammar selection by extension. `.tsx` → the tsx grammar (parses JSX); `.jsx` →
// the javascript grammar. NOTE: the project scanner keeps its OWN ext→lang map
// because its query lookup is keyed by language id (it maps `.tsx`→typescript to
// reuse typescript queries) — do not fold that one in without re-keying its queries.
const EXT_TO_LANG = {
".ts": "typescript",
".mts": "typescript",
".cts": "typescript",
".tsx": "tsx",
".js": "javascript",
".mjs": "javascript",
".cjs": "javascript",
".jsx": "javascript",
".py": "python",
".go": "go",
".rs": "rust",
".rb": "ruby",
".c": "c",
".h": "c",
".cc": "cpp",
".cpp": "cpp",
".cxx": "cpp",
".c++": "cpp",
".hh": "cpp",
".hpp": "cpp",
".hxx": "cpp",
".inl": "cpp",
".ipp": "cpp",
".tpp": "cpp",
".txx": "cpp",
".cu": "cpp",
".hip": "cpp",
".cs": "csharp",
".php": "php",
".phtml": "php",
".php3": "php",
".php4": "php",
".php5": "php",
".css": "css",
};
/** Resolve a tree-sitter grammar/language id from a file path's extension. */
export function resolveTreeSitterLanguage(filePath) {
return EXT_TO_LANG[path.extname(filePath).toLowerCase()];
}
/**
* Parse `content` for `filePath` via the shared client and return the root node,
* or null when the grammar is unavailable / parse fails / wasm aborted. init()
* lazily loads the grammar (memoized) and must run before parseFile.
*/
export async function parseTreeSitterRoot(filePath, content) {
const languageId = resolveTreeSitterLanguage(filePath);
const client = getSharedTreeSitterClient();
if (!languageId || !client || !(await client.init()))
return null;
const tree = await client.parseFile(filePath, languageId, content);
return tree ? tree.rootNode : null;
}
export function childrenOfType(node, type) {
return (node.children ?? []).filter((c) => c && c.type === type);
}
export function firstChildOfType(node, type) {
return (node.children ?? []).find((c) => c && c.type === type);
}
/** Depth-first walk, calling `visit` on every node (pre-order = source order). */
export function walk(node, visit) {
visit(node);
for (const child of node.children ?? []) {
if (child)
walk(child, visit);
}
}