extractInScratch
uses tcmts.studio.editableCodebase, which uses tcmts.share.servedCodebase, which uses process
Make the expression at from..to in a scratch file (text being what it holds) a definition of its own, named
name, above the one it's in: what it uses that's local to that one (parameters, variables) becoming its
parameters (typed as they are there), the expression giving way to a use of it (awaited, if it awaits). What the
file holds then, and where the new definition's name is.
import { declarationOf, forEachChild } from "#codebase/tcmts/core/ts"; import { HttpError } from "#codebase/tcmts/server"; import { analyzedScratch, editableCodebase, scratchPath } from "#codebase/tcmts/studio"; import { isExpression as ts_isExpression, isIdentifier as ts_isIdentifier, isPropertyAccessExpression as ts_isPropertyAccessExpression, } from "typescript/ast"; import type { Identifier as ts_Identifier, Node as ts_Node } from "typescript/ast"; export async function extractInScratch( file: string, text: string, from: number, to: number, name: string, ): Promise<{ text: string; at: number }> { const cb = editableCodebase(); const abs = scratchPath(cb, file, text); if (!/^[A-Za-z_$][\w$]*$/.test(name)) throw new HttpError(400, `${name} isn't a name.`); while (from < to && /\s/.test(text[from]!)) from++; while (to > from && /[\s;]/.test(text[to - 1]!)) to--; const a = analyzedScratch(cb, abs, text); const sf = a.program.getSourceFile(abs); if (!sf) throw new HttpError(500, `${file} couldn't be read.`); // the expression just there, and the top-level statement it's in let expr: ts_Node | undefined; const find = (n: ts_Node) => { if (n.getStart(sf) > from || n.end < to) return; if (n.getStart(sf) === from && n.end === to && ts_isExpression(n)) expr = n; forEachChild(n, find); }; find(sf); if (!expr) throw new HttpError(400, "Choose an expression: what's chosen isn't one, whole."); const st = sf.statements.find((s) => s.getStart(sf) <= from && s.end >= to); if (!st) throw new HttpError(400, "It's in no statement of the file's."); const body = text.slice(from, to); if (/\bthis\b/.test(body)) throw new HttpError(400, "It uses `this`, which a definition of its own wouldn't have."); // what it uses that's local to the statement: its parameters, in the order it uses them const params = new Map<string, string>(); const visit = (n: ts_Node) => { if (ts_isIdentifier(n) && !(ts_isPropertyAccessExpression(n.parent) && n.parent.name === n)) { const id = n as ts_Identifier; const sym = a.checker.getSymbolAtLocation(id); const decl = sym && declarationOf(sym); const at = decl && decl.getSourceFile() === sf ? ((decl as ts_Node & { name?: ts_Node }).name ?? decl).getStart(sf) : -1; const local = at >= st.getStart(sf) && at < st.end && (at < from || at >= to) && !a.defs.some((d) => d.range.nameStart === at); if (local && !params.has(id.text)) { let type = "unknown"; try { type = a.checker.typeToString(a.checker.getTypeAtLocation(id)); } catch { type = "unknown"; } params.set(id.text, type); } } forEachChild(n, visit); }; visit(expr); const awaits = /\bawait\b/.test(body); const value = /^\s*\{/.test(body) ? `(${body})` : body; const list = [...params].map(([p, t]) => `${p}: ${t}`).join(", "); const made = params.size || awaits ? `export const ${name} = ${awaits ? "async " : ""}(${list}) => ${value};` : `export const ${name} = ${value};`; const use = params.size || awaits ? `${awaits ? "await " : ""}${name}(${[...params.keys()].join(", ")})` : name; // (above the statement it was in, after the one before: its doc stays with it) // (the file's first: after its `// tcm:` lines, which begin it) const pragmas = /^(?:\/\/ tcm:[^\n]*\n)*/.exec(text)![0].length; const first = st.pos <= pragmas; const pos = first ? pragmas : st.pos; const lead = first ? "" : "\n\n"; const tail = first ? "\n\n" : ""; const out = `${text.slice(0, pos)}${lead}${made}${tail}${text.slice(pos, from)}${use}${text.slice(to)}`; return { text: out, at: pos + lead.length + "export const ".length }; }
uses
- tcmts.core.ts.declarationOf
- tcmts.core.ts.forEachChild
- tcmts.server.HttpError
- tcmts.studio.analyzedScratch
- tcmts.studio.editableCodebase
- tcmts.studio.scratchPath