pi-shorthand 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -7
- package/api.d.ts +14 -0
- package/format.ts +148 -0
- package/index.ts +11 -24
- package/package.json +3 -2
- package/placement.ts +35 -8
- package/prelude.ts +241 -50
- package/program-lint.ts +112 -0
- package/runner.ts +61 -5
- package/skills/shorthand/SKILL.md +84 -12
- package/skills/shorthand/advanced-refactors.md +167 -0
- package/text-format.ts +22 -0
- package/skills/shorthand/ast-grep.md +0 -104
- package/skills/shorthand/gritql.md +0 -20
- package/skills/shorthand/writing.md +0 -69
package/README.md
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
A [Pi](https://github.com/earendil-works/pi) tool for
|
|
6
|
-
|
|
7
|
-
and over: fewer tokens, and fewer round trips.
|
|
5
|
+
A [Pi](https://github.com/earendil-works/pi) tool for editing repositories with Bun programs.
|
|
6
|
+
The model can combine ordinary JavaScript, text edits and structural transformations in one call.
|
|
8
7
|
|
|
9
8
|
The program sees your repo as normal, but its writes are held back. By default, they're applied only
|
|
10
9
|
if the program succeeds and destination files are unchanged, and the model gets the diff.
|
|
10
|
+
The program performs the edit; tests, type-checks and other verification run separately afterward.
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
@@ -28,7 +28,7 @@ You also need Bun, git, and either [bubblewrap](https://github.com/containers/bu
|
|
|
28
28
|
Anything in Bun or Node, plus these globals (no imports):
|
|
29
29
|
|
|
30
30
|
```ts
|
|
31
|
-
await $`
|
|
31
|
+
await $`git ls-files`.text(); // Bun's shell (the only async one)
|
|
32
32
|
glob("src/**/*.ts"); // → ["src/a.ts", …]
|
|
33
33
|
grep("oldApi(", "src"); // → [{ file, line, text }, …]
|
|
34
34
|
sg.find("oldApi($$$ARGS)", "src"); // ast-grep search
|
|
@@ -40,6 +40,16 @@ sg.parse(sg.Lang.TypeScript, source); // ast-grep's own API (or import from "@as
|
|
|
40
40
|
grit("`console.log($x)` => `logger.info($x)`", "src");
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
`sg.find`, `sg.one`, `sg.rewrite` and `grit` also accept `sg.file("src/app.ts")` directly, including
|
|
44
|
+
in arrays mixed with paths. Search reads the file's current contents. Missing targets are valid insertion
|
|
45
|
+
destinations but cannot be searched. An explicit target can select an ignored file inside the workspace;
|
|
46
|
+
the tool still only applies Git-visible changes.
|
|
47
|
+
|
|
48
|
+
[api.d.ts](api.d.ts) exposes the actual injected helper types for editor completion and external TypeScript
|
|
49
|
+
checking of editing programs. Include it in the program's TypeScript project (or reference
|
|
50
|
+
`pi-shorthand/api` via `compilerOptions.types` when installed as a package). This supplies types, not runtime
|
|
51
|
+
globals. Bun execution does not automatically type-check programs or run application verification.
|
|
52
|
+
|
|
43
53
|
## Options
|
|
44
54
|
|
|
45
55
|
- `title`: a short description shown with the call (required).
|
|
@@ -51,6 +61,9 @@ grit("`console.log($x)` => `logger.info($x)`", "src");
|
|
|
51
61
|
## Good to know
|
|
52
62
|
|
|
53
63
|
- Only files git tracks, or would track, are applied.
|
|
64
|
+
- Successful edits are formatted with detected installed project tools (Prettier, oxfmt, Biome, Ruff,
|
|
65
|
+
Black, gofmt or rustfmt) before the final diff. Ambiguous setups are skipped; formatter failures warn
|
|
66
|
+
without discarding completed edits. Set `PI_SHORTHAND_FORMAT=0` to disable. No project config is required.
|
|
54
67
|
- Each run snapshots the checkout first; reflinks make that cheap where supported, while other
|
|
55
68
|
filesystems copy its contents and use corresponding temporary space. On macOS the program runs at
|
|
56
69
|
a private AgentFS mount, so use paths relative to its working directory for repository files.
|
|
@@ -73,6 +86,8 @@ hook runs it; `npm run format` fixes formatting.
|
|
|
73
86
|
`npm test` runs the tests against real overlays (it needs AgentFS on macOS, bubblewrap on Linux).
|
|
74
87
|
`test/linux.sh` runs them on Linux in Docker.
|
|
75
88
|
|
|
76
|
-
`bun e2e/
|
|
77
|
-
|
|
78
|
-
|
|
89
|
+
`bun e2e/suite.ts` previews the local benchmark suite without calling a model. Add `--execute` to run it.
|
|
90
|
+
The [comparison harness](e2e/README.md) supports stock, optional and replacement editing tools, controlled
|
|
91
|
+
documentation/skills, independent task checks, saved final changes and session reports.
|
|
92
|
+
`bun e2e/run.ts --repo <path or git URL> --task "…" --setups baseline,replace,code --check "…"` runs Pi
|
|
93
|
+
with a real model on fresh copies of your own repository.
|
package/api.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Types for programs executed by the code tool. Type-only: this does not install runtime globals. */
|
|
2
|
+
import type { ShorthandGlobals } from "./prelude.ts";
|
|
3
|
+
|
|
4
|
+
declare global {
|
|
5
|
+
const $: ShorthandGlobals["$"];
|
|
6
|
+
const edit: ShorthandGlobals["edit"];
|
|
7
|
+
const glob: ShorthandGlobals["glob"];
|
|
8
|
+
const grep: ShorthandGlobals["grep"];
|
|
9
|
+
const sg: ShorthandGlobals["sg"];
|
|
10
|
+
const grit: ShorthandGlobals["grit"];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type { RewriteResult } from "./prelude.ts";
|
|
14
|
+
export type { ShorthandGlobals };
|
package/format.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/** Best-effort formatting using existing project tools. Runs inside the editing workspace. */
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
type Command = { name: string; executable: string; args: string[]; cwd: string };
|
|
6
|
+
|
|
7
|
+
function text(file: string): string {
|
|
8
|
+
try {
|
|
9
|
+
return readFileSync(file, "utf8");
|
|
10
|
+
} catch {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function directories(start: string, root: string): string[] {
|
|
16
|
+
const result = [];
|
|
17
|
+
for (let dir = start; ; dir = dirname(dir)) {
|
|
18
|
+
result.push(dir);
|
|
19
|
+
if (dir === root || dirname(dir) === dir) return result;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatterFor(file: string, root: string): Command | null {
|
|
24
|
+
root = resolve(root);
|
|
25
|
+
const extension = extname(file);
|
|
26
|
+
const js = /\.(?:[cm]?[jt]sx?|jsonc?|css|scss|less|html|vue|svelte|mdx?|ya?ml|graphql)$/i.test(extension);
|
|
27
|
+
for (const cwd of directories(dirname(resolve(root, file)), root)) {
|
|
28
|
+
const has = (...names: string[]) => names.some((name) => existsSync(join(cwd, name)));
|
|
29
|
+
let name: string | undefined;
|
|
30
|
+
let args: string[] = [];
|
|
31
|
+
if (js) {
|
|
32
|
+
let pkg: {
|
|
33
|
+
scripts?: Record<string, string>;
|
|
34
|
+
dependencies?: Record<string, string>;
|
|
35
|
+
devDependencies?: Record<string, string>;
|
|
36
|
+
prettier?: unknown;
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
pkg = JSON.parse(text(join(cwd, "package.json")) || "{}");
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
44
|
+
const choices = [
|
|
45
|
+
...(deps.prettier ||
|
|
46
|
+
pkg.prettier !== undefined ||
|
|
47
|
+
has(
|
|
48
|
+
".prettierrc",
|
|
49
|
+
".prettierrc.json",
|
|
50
|
+
".prettierrc.yaml",
|
|
51
|
+
".prettierrc.yml",
|
|
52
|
+
".prettierrc.js",
|
|
53
|
+
".prettierrc.cjs",
|
|
54
|
+
".prettierrc.mjs",
|
|
55
|
+
"prettier.config.js",
|
|
56
|
+
"prettier.config.cjs",
|
|
57
|
+
"prettier.config.mjs",
|
|
58
|
+
"prettier.config.ts",
|
|
59
|
+
)
|
|
60
|
+
? ["prettier"]
|
|
61
|
+
: []),
|
|
62
|
+
...(deps.oxfmt || has(".oxfmtrc.json", ".oxfmtrc.jsonc") ? ["oxfmt"] : []),
|
|
63
|
+
...(deps["@biomejs/biome"] || has("biome.json", "biome.jsonc") ? ["biome"] : []),
|
|
64
|
+
];
|
|
65
|
+
// Recognize the executable, never execute an arbitrary package script.
|
|
66
|
+
const script = pkg.scripts?.format;
|
|
67
|
+
const scripted = script?.match(/^(prettier|oxfmt|biome)(?:\s|$)/)?.[1];
|
|
68
|
+
// Custom wrappers/options may encode conventions we cannot reproduce. Leave them alone.
|
|
69
|
+
if (
|
|
70
|
+
script &&
|
|
71
|
+
(!scripted ||
|
|
72
|
+
/[;&|`$<>]/.test(script) ||
|
|
73
|
+
(script.match(/(?<!\S)--?[\w-]+(?:=\S+)?/g) ?? []).some(
|
|
74
|
+
(flag) => !["--write", "--check", "--ignore-unknown"].includes(flag),
|
|
75
|
+
))
|
|
76
|
+
)
|
|
77
|
+
return null;
|
|
78
|
+
if (!scripted && choices.length > 1) return null;
|
|
79
|
+
name = scripted ?? choices[0];
|
|
80
|
+
args =
|
|
81
|
+
name === "prettier"
|
|
82
|
+
? ["--write", "--ignore-unknown"]
|
|
83
|
+
: name === "biome"
|
|
84
|
+
? ["format", "--write", "--files-ignore-unknown=true"]
|
|
85
|
+
: [];
|
|
86
|
+
} else if (extension === ".py" || extension === ".pyi") {
|
|
87
|
+
const config = text(join(cwd, "pyproject.toml"));
|
|
88
|
+
const ruff = has("ruff.toml", ".ruff.toml") || /\[tool\.ruff(?:\.|\])/.test(config);
|
|
89
|
+
const black = /\[tool\.black\]/.test(config);
|
|
90
|
+
if (ruff && black) return null;
|
|
91
|
+
name = ruff ? "ruff" : black ? "black" : undefined;
|
|
92
|
+
args = name === "ruff" ? ["format"] : [];
|
|
93
|
+
} else if (extension === ".go" && has("go.mod", "go.work")) {
|
|
94
|
+
name = "gofmt";
|
|
95
|
+
args = ["-w"];
|
|
96
|
+
} else if (extension === ".rs" && has("Cargo.toml")) {
|
|
97
|
+
const cargo = text(join(cwd, "Cargo.toml"));
|
|
98
|
+
if (/edition\s*\.\s*workspace\s*=/.test(cargo)) return null;
|
|
99
|
+
name = "rustfmt";
|
|
100
|
+
args = ["--edition", cargo.match(/^\s*edition\s*=\s*["'](\d+)["']/m)?.[1] ?? "2015"];
|
|
101
|
+
}
|
|
102
|
+
if (!name) continue;
|
|
103
|
+
const search = directories(cwd, root);
|
|
104
|
+
const candidates = search.map((dir) => join(dir, js ? "node_modules/.bin" : ".venv/bin", name!));
|
|
105
|
+
// JS formatters must belong to this project, not the extension's dependencies.
|
|
106
|
+
const executable = candidates.find((candidate) => Bun.which(candidate)) ?? (!js ? Bun.which(name) : null);
|
|
107
|
+
return executable ? { name, executable, args, cwd } : null;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function formatChanged(
|
|
113
|
+
files: string[],
|
|
114
|
+
root: string,
|
|
115
|
+
): Promise<{ messages: string[]; warnings: string[] }> {
|
|
116
|
+
const groups = new Map<string, { command: Command; files: string[] }>();
|
|
117
|
+
const result = { messages: [] as string[], warnings: [] as string[] };
|
|
118
|
+
for (const file of files) {
|
|
119
|
+
const command = formatterFor(file, root);
|
|
120
|
+
if (!command) continue;
|
|
121
|
+
const key = JSON.stringify(command);
|
|
122
|
+
const group = groups.get(key) ?? { command, files: [] };
|
|
123
|
+
group.files.push("./" + relative(command.cwd, resolve(root, file)));
|
|
124
|
+
groups.set(key, group);
|
|
125
|
+
}
|
|
126
|
+
for (const { command, files: targets } of groups.values()) {
|
|
127
|
+
try {
|
|
128
|
+
const child = Bun.spawn([command.executable, ...command.args, ...targets], {
|
|
129
|
+
cwd: command.cwd,
|
|
130
|
+
stdout: "pipe",
|
|
131
|
+
stderr: "pipe",
|
|
132
|
+
});
|
|
133
|
+
const [code, stdout, stderr] = await Promise.all([
|
|
134
|
+
child.exited,
|
|
135
|
+
new Response(child.stdout).text(),
|
|
136
|
+
new Response(child.stderr).text(),
|
|
137
|
+
]);
|
|
138
|
+
if (code !== 0)
|
|
139
|
+
result.warnings.push(
|
|
140
|
+
`${command.name} formatting failed: ${(stderr || stdout).trim().slice(-2000) || `exit ${code}`}`,
|
|
141
|
+
);
|
|
142
|
+
else result.messages.push(`Formatted ${targets.length} file(s) with ${command.name}.`);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
result.warnings.push(`${command.name} formatting failed: ${String(error)}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
package/index.ts
CHANGED
|
@@ -15,28 +15,19 @@ import { callLine, countLines, fileMetadataSummary, resultLines, unstructuredRes
|
|
|
15
15
|
import { RUN_HISTORY_FILE } from "./history.ts";
|
|
16
16
|
import type { FileChange, RunOptions, RunResult } from "./runner.ts";
|
|
17
17
|
|
|
18
|
-
// Runs typically take well under a second.
|
|
18
|
+
// Runs typically take well under a second. Longer transformations can request more time.
|
|
19
19
|
const DEFAULT_TIMEOUT_SECONDS = 2;
|
|
20
20
|
|
|
21
|
-
const DESCRIPTION = `
|
|
21
|
+
const DESCRIPTION = `Edit repository files with a TypeScript program run by Bun. Top-level await and ordinary Bun/Node APIs work. Use repository-relative paths. The program runs in an isolated workspace; changes apply on successful exit by default and the tool reports the diff. Run tests, type-checks and builds separately afterward with the shell tool.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Common operations:
|
|
24
|
+
- edit({ path, oldText, newText }) replaces exactly one literal occurrence; missing or ambiguous text is an error. Use text edits for known source, structural matching when it saves enumerating occurrences or preserves varying syntax.
|
|
25
|
+
- await Bun.file(path).text(); await Bun.write(path, text)
|
|
26
|
+
- sg.rewrite(pattern, replacement, files?) discovers and rewrites matching code; omit files for the working directory. $X captures one node; $$$X captures a sequence.
|
|
27
|
+
- sg.one(pattern, files?) selects exactly one match; sg.find returns an array. sg.rewrite also accepts a selected match or array without a file scope.
|
|
28
|
+
- A rewrite callback receives a match and returns text, a native node.replace(text) edit, or null to skip. Return native edits to apply them. Pass selected arrays together for independent edits; select again after changing their file.
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
- glob(pattern, dir?) → string[]
|
|
27
|
-
- grep(stringOrRegExp, paths?) → {file, line, text}[]. A string matches literally.
|
|
28
|
-
- sg.find(pattern, files?) → {file, line, text, vars}[]. ast-grep pattern: $X is one node, $$$X is zero or more. files is a file, directory, glob or a list of them (JS/TS).
|
|
29
|
-
- sg.rewrite(pattern, templateOrFunction, files?) → number rewritten. A template can use $X and $$$X; a function gets the match (its captures are on it: m.X) and returns the new text, or null to leave it.
|
|
30
|
-
- sg.one(pattern, files?) requires exactly one match. sg.file(path) selects a JS/TS file root (also works for new files).
|
|
31
|
-
- sg.insert(text, destination), sg.move(match, destination, transform?), sg.remove(match). destination is exactly one of {before: match}, {after: match}, {startOf: container}, {endOf: container}. JS/TS statements/declarations only; containers are file roots or matched statement blocks. Rematch after editing a file. move's optional function transforms its text; insert(match.text, destination) copies.
|
|
32
|
-
- sg also has ast-grep's own API (sg.parse, sg.Lang, sg.findInFiles, …), and import "@ast-grep/napi" works too.
|
|
33
|
-
- grit(gritqlPattern, paths?, {lang?, dryRun?}) → {file, matches}[], e.g. grit("\`a($x)\` => \`b($x)\`", "src")
|
|
34
|
-
Bun's shell $ needs await: await $\`bun test src/foo.test.ts\`. You can also run the ast-grep, grit and git CLIs with it. For how to write these programs, see the shorthand skill.
|
|
35
|
-
|
|
36
|
-
Throw or exit non-zero to fail. rollback decides what a failure undoes:
|
|
37
|
-
- "all" (default): nothing is applied; you get the error and the candidate diff.
|
|
38
|
-
- "file": on timeout, files not open for writing are applied if writer inspection succeeds. If inspection fails, or on another failure, nothing is applied.
|
|
39
|
-
The default timeout is 2 seconds; pass a longer timeout when the program runs tests or builds. Only files git sees (tracked, or untracked and not ignored) are diffed and applied; writes to .git are blocked. Print what you need to know (counts, assertions), not whole files.`;
|
|
30
|
+
See the shorthand skill for common writes. For extraction, complex rewrites or other languages, read its advanced-refactors.md guide. The default timeout is two seconds; request more for longer programs.`;
|
|
40
31
|
|
|
41
32
|
export default function (pi: ExtensionAPI) {
|
|
42
33
|
// A failed run is an error, both for the model and for how Pi shows it. (execute() returns its details
|
|
@@ -50,12 +41,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
50
41
|
name: "code",
|
|
51
42
|
label: "Code",
|
|
52
43
|
description: DESCRIPTION,
|
|
53
|
-
promptSnippet:
|
|
54
|
-
|
|
55
|
-
promptGuidelines: [
|
|
56
|
-
"Use code when several related reads, searches, edits or checks can be done without looking at intermediate results: put that logic in one program rather than many read/edit/bash calls.",
|
|
57
|
-
"Don't use code to explore when you need to see results before deciding what to do.",
|
|
58
|
-
],
|
|
44
|
+
promptSnippet: "Make a change with one transactional Bun editing program; run verification separately afterward",
|
|
45
|
+
|
|
59
46
|
parameters: Type.Object({
|
|
60
47
|
title: Type.String({ description: "A few words describing the change, shown to the user" }),
|
|
61
48
|
program: Type.String({ description: "TypeScript program run with Bun (top-level await allowed)" }),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-shorthand",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pi tool for
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Pi tool for editing repositories with Bun programs, text edits and structural transformations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ast-grep",
|
|
7
7
|
"bun",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"lint": "oxlint --deny-warnings",
|
|
34
34
|
"format": "oxfmt",
|
|
35
35
|
"check": "tsc -p . && oxlint --deny-warnings && oxfmt --check",
|
|
36
|
+
"postinstall": "grit init --global",
|
|
36
37
|
"prepare": "git config core.hooksPath .githooks 2>/dev/null || true"
|
|
37
38
|
},
|
|
38
39
|
"dependencies": {
|
package/placement.ts
CHANGED
|
@@ -9,6 +9,15 @@ export interface Match {
|
|
|
9
9
|
node: SgNode;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
const fileTarget = Symbol("shorthand.file");
|
|
13
|
+
export interface FileTarget extends Match {
|
|
14
|
+
readonly [fileTarget]: true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isFileTarget(value: unknown): value is FileTarget {
|
|
18
|
+
return typeof value === "object" && value !== null && fileTarget in value && snapshots.has(value as unknown as Match);
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
export type Destination =
|
|
13
22
|
| { before: Match; after?: never; startOf?: never; endOf?: never }
|
|
14
23
|
| { after: Match; before?: never; startOf?: never; endOf?: never }
|
|
@@ -41,27 +50,45 @@ export function remember<T extends Match>(match: T, source: string, existed = tr
|
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
/** A file root, including a not-yet-created file. Merely selecting it performs no writes. */
|
|
44
|
-
export function file(filename: string):
|
|
53
|
+
export function file(filename: string): FileTarget {
|
|
45
54
|
const lang = languages[filename.split(".").pop()!];
|
|
46
55
|
if (!lang) throw new Error("sg.file requires a JS/TS filename");
|
|
47
56
|
const existed = existsSync(filename);
|
|
48
57
|
const source = existed ? readFileSync(filename, "utf8") : "";
|
|
49
|
-
return remember(
|
|
58
|
+
return remember(
|
|
59
|
+
{ file: filename, text: source, node: parse(lang, source).root(), [fileTarget]: true as const },
|
|
60
|
+
source,
|
|
61
|
+
existed,
|
|
62
|
+
);
|
|
50
63
|
}
|
|
51
64
|
|
|
52
|
-
function
|
|
65
|
+
export function getMatchSnapshot(
|
|
66
|
+
match: Match,
|
|
67
|
+
sources = new Map<string, string | null>(),
|
|
68
|
+
staleAdvice = "match the file again after editing it",
|
|
69
|
+
): Snapshot {
|
|
53
70
|
const saved = snapshots.get(match);
|
|
54
71
|
if (!saved) throw new Error("Expected a file-backed match from sg.find, sg.one, or sg.file");
|
|
55
|
-
if (!languages[saved.file.split(".").pop()!]) throw new Error("Placement currently supports JS/TS only");
|
|
56
72
|
if (
|
|
57
|
-
|
|
58
|
-
(
|
|
59
|
-
)
|
|
60
|
-
throw new Error(
|
|
73
|
+
match.node !== saved.node ||
|
|
74
|
+
(existsSync(match.file) ? realpathSync(match.file) : resolve(match.file)) !== saved.file
|
|
75
|
+
)
|
|
76
|
+
throw new Error("File-backed match identity was changed; select it again");
|
|
77
|
+
if (!sources.has(saved.file))
|
|
78
|
+
sources.set(saved.file, existsSync(saved.file) ? readFileSync(saved.file, "utf8") : null);
|
|
79
|
+
const source = sources.get(saved.file);
|
|
80
|
+
if ((source !== null) !== saved.existed || (saved.existed && source !== saved.source)) {
|
|
81
|
+
throw new Error(`Stale match in ${match.file}; ${staleAdvice}`);
|
|
61
82
|
}
|
|
62
83
|
return saved;
|
|
63
84
|
}
|
|
64
85
|
|
|
86
|
+
function snapshot(match: Match): Snapshot {
|
|
87
|
+
const saved = getMatchSnapshot(match);
|
|
88
|
+
if (!languages[saved.file.split(".").pop()!]) throw new Error("Placement currently supports JS/TS only");
|
|
89
|
+
return saved;
|
|
90
|
+
}
|
|
91
|
+
|
|
65
92
|
function container(node: SgNode): boolean {
|
|
66
93
|
return node.kind() === "program" || node.kind() === "statement_block";
|
|
67
94
|
}
|