Sources

tcmts.cli.complete.Sources · #bojgakf3 · class

What the shell's asking about, read lazily: completing a command name shouldn't open the codebase.

import type { Complete } from "#codebase/tcmts/cli/complete";
import { Store } from "#codebase/tcmts/store";
import fs from "node:fs";
import path from "node:path";
import { DatabaseSync } from "node:sqlite";

export class Sources {
  private store: Store | undefined | null = null;
  private cwd: string;
  constructor(cwd: string) {
    this.cwd = cwd;
  }

  private root(): string | undefined {
    for (let dir = this.cwd; ; dir = path.dirname(dir)) {
      if (fs.existsSync(path.join(dir, ".tcm", "codebase.sqlite"))) return dir;
      if (path.dirname(dir) === dir) return undefined;
    }
  }
  private db(): Store | undefined {
    if (this.store === null) {
      const r = this.root();
      this.store = r ? new Store(path.join(r, ".tcm", "codebase.sqlite")) : undefined;
    }
    return this.store;
  }
  definitions(complete: Complete, kind?: "value") {
    for (const n of this.db()?.names(undefined, true) ?? [])
      if (!kind || n.kind !== "type") complete(n.path, n.kind);
  }
  namespaces(complete: Complete) {
    const seen = new Set<string>();
    for (const n of this.db()?.names(undefined, true) ?? []) {
      const parts = n.path.split(".");
      for (let i = 1; i < parts.length; i++) seen.add(parts.slice(0, i).join("."));
    }
    for (const ns of [...seen].sort()) complete(ns, "namespace");
  }
  branches(complete: Complete) {
    const db = this.db();
    for (const b of db?.branches() ?? [])
      complete(b, b === db!.branch().name ? "current branch" : "branch");
  }
  remotes(complete: Complete) {
    for (const [key, value] of this.db()?.settings("remote.") ?? [])
      complete(key.slice("remote.".length), (JSON.parse(value) as { url?: string }).url ?? "");
  }
  packages(complete: Complete) {
    const seen = new Set<string>();
    for (const p of this.db()?.lockedPackages() ?? [])
      if (!p.name.startsWith("@types/") && !seen.has(p.name)) {
        seen.add(p.name);
        complete(p.name, p.version);
      }
  }
  tables(complete: Complete) {
    const r = this.root();
    const file = r && path.join(r, ".tcm", "data", "store.sqlite");
    if (!file || !fs.existsSync(file)) return;
    const db = new DatabaseSync(file, { readOnly: true });
    try {
      for (const t of db
        .prepare("SELECT tbl, count(*) AS n FROM rows GROUP BY tbl ORDER BY tbl")
        .all() as { tbl: string; n: number }[])
        complete(t.tbl, `${t.n} row(s)`);
    } finally {
      db.close();
    }
  }
  /** Directories under this one (tab can't hand a word back to the shell's own path completion). */
  directories(complete: Complete) {
    const walk = (dir: string, depth: number) => {
      for (const e of fs.readdirSync(path.join(this.cwd, dir), { withFileTypes: true })) {
        if (!e.isDirectory() || e.name.startsWith(".") || e.name === "node_modules") continue;
        const rel = dir ? `${dir}/${e.name}` : e.name;
        complete(rel, "directory");
        if (depth < 2) walk(rel, depth + 1);
      }
    };
    walk("", 1);
  }
  scratchFiles(complete: Complete) {
    const r = this.root() ?? this.cwd;
    for (const f of fs.readdirSync(r))
      if (/\.tsx?$/.test(f) && !f.endsWith(".d.ts")) complete(f, "scratch file");
  }
}

uses

used by