renameInScratch
uses tcmts.studio.editableCodebase, which uses tcmts.share.servedCodebase, which uses process
Rename the name at offset in a scratch file to to (text being what it holds): the changes to the file, and
what was done in the codebase. A name the codebase has is renamed there too, as tcm mv does (its hash stays, so
what uses it follows; to a leaf keeps its namespace, a dotted name moves it), and the file's import follows it;
anything else is renamed in the file alone.
import { isTypeOnlyImport } from "#codebase/tcmts/core/ts"; import { HttpError } from "#codebase/tcmts/server"; import { analyzedScratch, editableCodebase, importEdit, nameInScratch, runTcm, scratchPath, } from "#codebase/tcmts/studio"; import { isImportDeclaration as ts_isImportDeclaration } from "typescript/ast"; import type { Node as ts_Node } from "typescript/ast"; export async function renameInScratch( file: string, text: string, offset: number, to: string, ): Promise<{ changes: { from: number; to: number; insert: string }[]; said?: string }> { const cb = editableCodebase(); const abs = scratchPath(cb, file, text); const a = analyzedScratch(cb, abs, text); const n = nameInScratch(cb, a, abs, offset); if (!n) throw new HttpError(400, "There's no name there to rename."); const wanted = to.trim(); const leaf = wanted.slice(wanted.lastIndexOf(".") + 1); if (!/^[A-Za-z_$][\w$]*$/.test(leaf) || (!n.name && wanted.includes("."))) throw new HttpError( 400, `${wanted} isn't a name${n.name ? " (a.b, or b to keep its namespace)" : ""}.`, ); if (!n.name) return { changes: n.uses.map((u) => ({ ...u, insert: leaf })) }; const ns = (p: string) => p.slice(0, Math.max(0, p.lastIndexOf("."))); const next = wanted.includes(".") ? wanted : ns(n.name) ? `${ns(n.name)}.${leaf}` : leaf; if (next === n.name) return { changes: [] }; const out = await runTcm(cb.lay.root, ["mv", n.name, next], { branch: true }); if (out.code !== 0) throw new HttpError( 400, out.text.split("\n").slice(0, 8).join("\n") || `tcm mv ${n.name} ${next} failed`, ); const said = out.text.split("\n")[0]; // a definition the file holds: its name moves with it (in the file, and in the codebase) if (!n.imported) return { changes: n.uses.map((u) => ({ ...u, insert: leaf })), ...(said ? { said } : {}) }; const spec = n.spec; // imported as another name (`a as b`): only what it imports changes const local = !spec?.propertyName; const changes: { from: number; to: number; insert: string }[] = local ? n.uses.map((u) => ({ ...u, insert: leaf })) : [{ from: spec!.propertyName!.getStart(n.sf), to: spec!.propertyName!.end, insert: leaf }]; if (spec && ns(next) !== ns(n.name)) { // another namespace: another module to import it from const module = ns(next) ? `#codebase/${ns(next).replaceAll(".", "/")}` : "#codebase"; let d: ts_Node | undefined = spec; while (d && !ts_isImportDeclaration(d)) d = d.parent; const imp = d && ts_isImportDeclaration(d) ? d : undefined; const elements = (spec.parent as ts_Node & { elements: readonly ts_Node[] }).elements; if (imp && elements.length === 1) { const m = imp.moduleSpecifier; changes.push({ from: m.getStart(n.sf), to: m.end, insert: JSON.stringify(module) }); } else if (imp) { const named = spec.parent; const rest = elements.filter((e) => e !== spec).map((e) => e.getText(n.sf)); // (the uses' changes inside the braces go: the import is written again) for (let i = changes.length - 1; i >= 0; i--) if (changes[i]!.from >= named.getStart(n.sf) && changes[i]!.to <= named.end) changes.splice(i, 1); changes.push({ from: named.getStart(n.sf), to: named.end, insert: `{ ${rest.join(", ")} }` }); const added = importEdit( text, module, local ? leaf : `${leaf} as ${spec.name.text}`, isTypeOnlyImport(imp.importClause as never), ); if (added) changes.push(added); } } return { changes: changes.sort((x, y) => x.from - y.from), ...(said ? { said } : {}) }; }
uses
- tcmts.core.ts.isTypeOnlyImport
- tcmts.server.HttpError
- tcmts.studio.analyzedScratch
- tcmts.studio.editableCodebase
- tcmts.studio.importEdit
- tcmts.studio.nameInScratch
- tcmts.studio.runTcm
- tcmts.studio.scratchPath