signatureInScratch

tcmts.studio.signatureInScratch · #voyrriyj · term · runs on the server

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

used by