extractInScratch

tcmts.studio.extractInScratch · #xku6pnij · term · runs on the server

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

used by