@yandy0725/pi-coding-tools 0.1.0 → 0.2.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/src/render.ts DELETED
@@ -1,283 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import * as Diff from "diff";
4
- import { extractPatchedPaths as _extractPatchedPaths } from "./parse";
5
- export const extractPatchedPaths = _extractPatchedPaths;
6
-
7
- export type ApplyPatchOperation = "add" | "delete" | "update";
8
-
9
- export type ApplyPatchPreviewFile = {
10
- filePath: string;
11
- movePath?: string;
12
- operation: ApplyPatchOperation;
13
- diff: string;
14
- added: number;
15
- removed: number;
16
- };
17
-
18
- export type ApplyPatchPreview = {
19
- files: ApplyPatchPreviewFile[];
20
- added: number;
21
- removed: number;
22
- };
23
-
24
- export const PATCH_PREVIEW_MAX_LINES = 16;
25
- export const PATCH_PREVIEW_MAX_CHARS = 4000;
26
- const PATCH_PREVIEW_HEAD_LINES = 8;
27
- const PATCH_PREVIEW_TAIL_LINES = PATCH_PREVIEW_MAX_LINES - PATCH_PREVIEW_HEAD_LINES - 1;
28
- const PATCH_PREVIEW_TRUNCATION_MARKER = "\u2026";
29
-
30
- function hasErrorCode(error: unknown, code: string): boolean {
31
- return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
32
- }
33
-
34
- function isChangedPreviewLine(line: string): boolean {
35
- return /^[+-]\s*\d+\s/.test(line);
36
- }
37
-
38
- function countWindowLines(lines: string[], start: number, end: number): number {
39
- return end - start + (start > 0 ? 1 : 0) + (end < lines.length ? 1 : 0);
40
- }
41
-
42
- function formatPreviewWindow(lines: string[], start: number, end: number): string {
43
- const previewLines = lines.slice(start, end);
44
- if (start > 0) {
45
- previewLines.unshift(PATCH_PREVIEW_TRUNCATION_MARKER);
46
- }
47
- if (end < lines.length) {
48
- previewLines.push(PATCH_PREVIEW_TRUNCATION_MARKER);
49
- }
50
- return previewLines.join("\n");
51
- }
52
-
53
- function createChangedHunkPreview(lines: string[]): string | undefined {
54
- const firstChangedLine = lines.findIndex(isChangedPreviewLine);
55
- if (firstChangedLine === -1) {
56
- return undefined;
57
- }
58
-
59
- let start = firstChangedLine;
60
- let end = firstChangedLine + 1;
61
- while (end < lines.length) {
62
- const line = lines[end];
63
- if (line === undefined || !isChangedPreviewLine(line)) {
64
- break;
65
- }
66
- end++;
67
- }
68
-
69
- const changedHunkEnd = end;
70
- while (end > start && countWindowLines(lines, start, end) > PATCH_PREVIEW_MAX_LINES) {
71
- end--;
72
- }
73
-
74
- while (countWindowLines(lines, start, end) < PATCH_PREVIEW_MAX_LINES) {
75
- const canAddBefore = start > 0;
76
- const canAddAfter = end < lines.length;
77
- if (!canAddBefore && !canAddAfter) {
78
- break;
79
- }
80
-
81
- const beforeContextLines = firstChangedLine - start;
82
- const afterContextLines = end - changedHunkEnd;
83
- if (canAddBefore && (!canAddAfter || beforeContextLines <= afterContextLines)) {
84
- start--;
85
- } else {
86
- end++;
87
- }
88
- }
89
-
90
- return formatPreviewWindow(lines, start, end);
91
- }
92
-
93
- export function countLines(text: string): number {
94
- if (text.length === 0) {
95
- return 0;
96
- }
97
- let lines = 1;
98
- for (let index = 0; index < text.length; index++) {
99
- if (text.charCodeAt(index) === 10) {
100
- lines += 1;
101
- }
102
- }
103
- return lines;
104
- }
105
-
106
- function enforcePreviewCharLimit(preview: string): string {
107
- if (preview.length <= PATCH_PREVIEW_MAX_CHARS) {
108
- return preview;
109
- }
110
-
111
- return `${preview.slice(0, PATCH_PREVIEW_MAX_CHARS - PATCH_PREVIEW_TRUNCATION_MARKER.length).trimEnd()}${PATCH_PREVIEW_TRUNCATION_MARKER}`;
112
- }
113
-
114
- export function truncatePreview(text: string): string {
115
- if (text.length <= PATCH_PREVIEW_MAX_CHARS && countLines(text) <= PATCH_PREVIEW_MAX_LINES) {
116
- return text;
117
- }
118
-
119
- const lines = text.split("\n");
120
- const changedHunkPreview = createChangedHunkPreview(lines);
121
- const previewText =
122
- changedHunkPreview ??
123
- [
124
- ...lines.slice(0, PATCH_PREVIEW_HEAD_LINES),
125
- PATCH_PREVIEW_TRUNCATION_MARKER,
126
- ...lines.slice(-PATCH_PREVIEW_TAIL_LINES),
127
- ].join("\n");
128
- return enforcePreviewCharLimit(previewText);
129
- }
130
-
131
- export function createPatchDiff(
132
- oldContent: string,
133
- newContent: string,
134
- ): { diff: string; added: number; removed: number } {
135
- const parts = Diff.diffLines(oldContent, newContent);
136
- const oldLines = oldContent.split("\n");
137
- const newLines = newContent.split("\n");
138
- const lineNumWidth = String(Math.max(oldLines.length, newLines.length)).length;
139
- const output: string[] = [];
140
- let oldLineNum = 1;
141
- let newLineNum = 1;
142
- let added = 0;
143
- let removed = 0;
144
-
145
- for (const part of parts) {
146
- const rawLines = part.value.split("\n");
147
- if (rawLines[rawLines.length - 1] === "") {
148
- rawLines.pop();
149
- }
150
-
151
- for (const line of rawLines) {
152
- if (part.added) {
153
- output.push(`+${String(newLineNum).padStart(lineNumWidth, " ")} ${line}`);
154
- newLineNum++;
155
- added++;
156
- continue;
157
- }
158
-
159
- if (part.removed) {
160
- output.push(`-${String(oldLineNum).padStart(lineNumWidth, " ")} ${line}`);
161
- oldLineNum++;
162
- removed++;
163
- continue;
164
- }
165
-
166
- output.push(` ${String(oldLineNum).padStart(lineNumWidth, " ")} ${line}`);
167
- oldLineNum++;
168
- newLineNum++;
169
- }
170
- }
171
-
172
- return { diff: output.join("\n"), added, removed };
173
- }
174
-
175
- export async function readExistingFileForPreview(absolutePath: string): Promise<string> {
176
- try {
177
- return await readFile(absolutePath, "utf-8");
178
- } catch (error) {
179
- if (hasErrorCode(error, "ENOENT")) {
180
- return "";
181
- }
182
- throw error;
183
- }
184
- }
185
-
186
- export function formatLineCountSummary(added: number, removed: number): string {
187
- return `(+${added} -${removed})`;
188
- }
189
-
190
- export function formatPatchOperation(operation: ApplyPatchOperation): string {
191
- if (operation === "add") {
192
- return "Added";
193
- }
194
- if (operation === "delete") {
195
- return "Deleted";
196
- }
197
- return "Edited";
198
- }
199
-
200
- function normalizeDisplayPath(filePath: string): string {
201
- return filePath.replaceAll(path.sep, "/");
202
- }
203
-
204
- export function displayPath(filePath: string, cwd: string): string {
205
- if (!path.isAbsolute(filePath)) {
206
- return normalizeDisplayPath(filePath);
207
- }
208
-
209
- const absoluteCwd = path.resolve(cwd);
210
- const relativePath = path.relative(absoluteCwd, filePath);
211
- if (
212
- relativePath === "" ||
213
- (!relativePath.startsWith(`..${path.sep}`) && relativePath !== ".." && !path.isAbsolute(relativePath))
214
- ) {
215
- return normalizeDisplayPath(relativePath || ".");
216
- }
217
-
218
- return normalizeDisplayPath(filePath);
219
- }
220
-
221
- export function formatPatchFilePath(file: ApplyPatchPreviewFile, cwd: string = process.cwd()): string {
222
- const filePath = displayPath(file.filePath, cwd);
223
- if (!file.movePath) {
224
- return filePath;
225
- }
226
- return `${filePath} \u2192 ${displayPath(file.movePath, cwd)}`;
227
- }
228
-
229
- export function formatPatchFileSummary(file: ApplyPatchPreviewFile, cwd: string): string {
230
- return `${formatPatchFilePath(file, cwd)} ${formatLineCountSummary(file.added, file.removed)}`;
231
- }
232
-
233
- export function formatPatchFileHeader(file: ApplyPatchPreviewFile, cwd: string): string {
234
- return `${formatPatchOperation(file.operation)} ${formatPatchFileSummary(file, cwd)}`;
235
- }
236
-
237
- export function formatPatchPreview(
238
- preview: ApplyPatchPreview,
239
- cwd: string = process.cwd(),
240
- expanded: boolean = true,
241
- ): string {
242
- const lines: string[] = [];
243
- if (preview.files.length === 1) {
244
- const file = preview.files[0];
245
- if (file) {
246
- lines.push(formatPatchFileHeader(file, cwd));
247
- if (expanded && file.diff) {
248
- lines.push(
249
- ...truncatePreview(file.diff)
250
- .split("\n")
251
- .map((line) => ` ${line}`),
252
- );
253
- }
254
- }
255
- return lines.join("\n");
256
- }
257
-
258
- const noun = "files";
259
- lines.push(
260
- `${formatPatchOperation("update")} ${preview.files.length} ${noun} ${formatLineCountSummary(preview.added, preview.removed)}`,
261
- );
262
- for (const file of preview.files) {
263
- lines.push(` ${formatPatchFileSummary(file, cwd)}`);
264
- if (expanded && file.diff) {
265
- lines.push(
266
- ...truncatePreview(file.diff)
267
- .split("\n")
268
- .map((line) => ` ${line}`),
269
- );
270
- }
271
- }
272
- return lines.join("\n");
273
- }
274
-
275
- export function formatInFlightCallText(patchText: string): string {
276
- const paths = extractPatchedPaths(patchText);
277
- if (paths.length === 0) {
278
- return "Patching";
279
- }
280
- const noun = paths.length === 1 ? "file" : "files";
281
- const count = paths.length > 1 ? ` (${paths.length} ${noun})` : "";
282
- return `Patching${count}: ${paths.join(", ")}`;
283
- }
@@ -1,35 +0,0 @@
1
- import { rename, unlink, writeFile } from "node:fs/promises";
2
-
3
- export type AtomicWriteOperations = {
4
- writeFile: (filePath: string, content: string, encoding: "utf-8") => Promise<void>;
5
- rename: (fromPath: string, toPath: string) => Promise<void>;
6
- unlink: (filePath: string) => Promise<void>;
7
- };
8
-
9
- const ATOMIC_WRITE_OPERATIONS: AtomicWriteOperations = {
10
- writeFile,
11
- rename,
12
- unlink,
13
- };
14
-
15
- function hasErrorCode(error: unknown, code: string): boolean {
16
- return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
17
- }
18
-
19
- export async function writeFileAtomic(
20
- absPath: string,
21
- content: string,
22
- operations: AtomicWriteOperations = ATOMIC_WRITE_OPERATIONS,
23
- ): Promise<void> {
24
- const tempPath = `${absPath}.tmp.${process.pid}.${Math.random().toString(16).slice(2)}`;
25
- await operations.writeFile(tempPath, content, "utf-8");
26
- try {
27
- await operations.rename(tempPath, absPath);
28
- } catch (error) {
29
- if (!hasErrorCode(error, "EEXIST")) {
30
- throw error;
31
- }
32
- await operations.unlink(absPath);
33
- await operations.rename(tempPath, absPath);
34
- }
35
- }