completeInScratch

tcmts.studio.completeInScratch · #sithzg6z · term · runs on the server

uses tcmts.studio.editableCodebase, which uses tcmts.share.servedCodebase, which uses process

Completions at offset in a scratch file (text being what it holds; trigger, the character typed that asked, a "." say): TypeScript's, from its language service, and after them the codebase's names the word begins (where a name, not a member, goes), each with the import it needs. From where the word they complete starts.

import { analyzedScratch, editableCodebase, importEdit, scratchPath } from "#codebase/tcmts/studio";
import type { ScratchCompletion } from "#codebase/tcmts/studio";

export async function completeInScratch(
  file: string,
  text: string,
  offset: number,
  trigger?: string,
): Promise<{ from: number; options: ScratchCompletion[] }> {
  const cb = editableCodebase();
  const abs = scratchPath(cb, file);
  let from = offset;
  while (from > 0 && /[\w$]/.test(text[from - 1]!)) from--;
  const word = text.slice(from, offset);
  const a = analyzedScratch(cb, abs, text);
  let entries: readonly {
    name: string;
    kind?: number;
    sortText?: string;
    insertText?: string;
    detail?: string;
    labelDetails?: { description?: string; detail?: string };
  }[] = [];
  try {
    entries =
      a.service?.getCompletionsAtPosition(abs, offset, trigger ? { triggerCharacter: trigger } : {})
        ?.entries ?? [];
  } catch {
    entries = [];
  }
  // (the language server's kinds, as the editor's icons are named)
  const KINDS: Record<number, string> = {
    2: "method",
    3: "function",
    4: "function",
    5: "property",
    6: "variable",
    7: "class",
    8: "interface",
    9: "namespace",
    10: "property",
    13: "enum",
    14: "keyword",
    20: "enum",
    21: "constant",
    22: "class",
    25: "type",
  };
  const ts = [...entries]
    .sort(
      (x, y) => (x.sortText ?? "").localeCompare(y.sortText ?? "") || x.name.localeCompare(y.name),
    )
    .slice(0, 500);
  const options: ScratchCompletion[] = ts.map((e, i) => {
    const detail = e.labelDetails?.description ?? e.detail;
    return {
      label: e.name,
      ...(e.kind && KINDS[e.kind] ? { type: KINDS[e.kind] } : {}),
      ...(detail ? { detail } : {}),
      boost: Math.max(-99, -Math.floor(i / 50)),
      ...(e.insertText && e.insertText !== e.name ? { apply: e.insertText } : {}),
    };
  });
  // the codebase's names: not after a dot (a member goes there), not one the file has in scope already
  if (word && text[from - 1] !== "." && trigger !== ".") {
    const have = new Set(ts.map((e) => e.name));
    const lower = word.toLowerCase();
    let added = 0;
    for (const n of cb.store.names()) {
      const dot = n.path.lastIndexOf(".");
      const leaf = n.path.slice(dot + 1);
      if (
        !leaf.toLowerCase().startsWith(lower) ||
        have.has(leaf) ||
        !/^[A-Za-z_$][\w$]*$/.test(leaf)
      )
        continue;
      const rec = cb.store.getDef(n.hash);
      if (!rec || rec.kind === "file") continue;
      const ns = dot > 0 ? n.path.slice(0, dot) : "";
      const imports = importEdit(
        text,
        ns ? `#codebase/${ns.replaceAll(".", "/")}` : "#codebase",
        leaf,
        rec.kind === "type",
      );
      options.push({
        label: leaf,
        type:
          rec.kind === "type"
            ? "type"
            : rec.kind === "class"
              ? "class"
              : /^(<[^>]*>)?\(/.test(rec.signature)
                ? "function"
                : "variable",
        detail: n.path,
        boost: -1,
        ...(imports ? { imports } : {}),
      });
      if (++added >= 60) break;
    }
  }
  return { from, options };
}

uses

used by