signatureInScratch
uses tcmts.studio.editableCodebase, which uses tcmts.share.servedCodebase, which uses process
The call offset is in the arguments of, in a scratch file (text being what it holds): what's called, as its type
reads (each signature, if there are several), where in that each parameter is (offsets in label), and which
argument offset is at. Nothing, outside a call's arguments.
import { forEachChild } from "#codebase/tcmts/core/ts"; import { analyzedScratch, editableCodebase, scratchPath } from "#codebase/tcmts/studio"; import { isCallExpression as ts_isCallExpression, isNewExpression as ts_isNewExpression } from "typescript/ast"; import type { Node as ts_Node } from "typescript/ast"; export async function signatureInScratch( file: string, text: string, offset: number, ): Promise<{ label: string; params: { from: number; to: number }[]; active: number } | null> { const cb = editableCodebase(); const abs = scratchPath(cb, file, text); const a = analyzedScratch(cb, abs, text); const sf = a.program.getSourceFile(abs); if (!sf) return null; // the innermost call whose parentheses hold the offset let call: | (ts_Node & { expression: ts_Node; arguments?: { pos: number; end: number } }) | undefined; // (a call still being written has no `)` yet: it reaches as far as the space after it) const reach = (n: ts_Node) => n.end + (/^\s*/.exec(text.slice(n.end))?.[0].length ?? 0); const find = (n: ts_Node) => { if (offset < n.getStart(sf) || offset > reach(n)) return; if ((ts_isCallExpression(n) || ts_isNewExpression(n)) && n.arguments) { const open = text.indexOf("(", n.expression.end); const closed = text[n.end - 1] === ")"; if (open >= 0 && offset > open && (closed ? offset < n.end : offset <= reach(n))) call = n as never; } forEachChild(n, find); }; find(sf); if (!call) return null; let label: string; try { label = a.checker.typeToString(a.checker.getTypeAtLocation(call.expression)); } catch { return null; } // which argument: the commas before it at the call's own depth const open = text.indexOf("(", call.expression.end); let depth = 0; let active = 0; let quote = ""; for (let i = open + 1; i < offset; i++) { const ch = text[i]!; if (quote) { if (ch === "\\") i++; else if (ch === quote) quote = ""; } else if (ch === '"' || ch === "'" || ch === "`") quote = ch; else if ("([{".includes(ch)) depth++; else if (")]}".includes(ch)) depth--; else if (ch === "," && depth === 0) active++; } // the parameters: the first parenthesized list in the label, split at its own commas const params: { from: number; to: number }[] = []; const start = label.indexOf("("); if (start >= 0) { let d = 0; let from = start + 1; for (let i = start; i < label.length; i++) { const ch = label[i]!; if ("([{<".includes(ch)) d++; else if (")]}>".includes(ch) && !(ch === ">" && label[i - 1] === "=")) { d--; if (d === 0) { if (label.slice(from, i).trim()) params.push({ from: from + (label.slice(from, i).length - label.slice(from, i).trimStart().length), to: i, }); break; } } else if (ch === "," && d === 1) { params.push({ from: from + (label.slice(from, i).length - label.slice(from, i).trimStart().length), to: i, }); from = i + 1; } } } return { label, params, active }; }
uses
- tcmts.core.ts.forEachChild
- tcmts.studio.analyzedScratch
- tcmts.studio.editableCodebase
- tcmts.studio.scratchPath