@xynogen/pix-edit 0.1.6 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-edit",
3
- "version": "0.1.6",
3
+ "version": "0.1.9",
4
4
  "description": "Pi tool — precise text replacement edit with diff rendering",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -34,7 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@xynogen/pix-pretty": "*"
37
+ "@xynogen/pix-pretty": "^1.7.9"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@earendil-works/pi-coding-agent": "*",
package/src/edit.test.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { describe, expect, it } from "bun:test";
2
+ import type { CursorStore, FffState } from "@xynogen/pix-pretty/fff";
3
+ import type { PiPrettyApi, TextComponentCtor } from "@xynogen/pix-pretty/types";
2
4
  import {
3
5
  getEditOperations,
4
6
  registerEditTool,
@@ -18,15 +20,31 @@ class MockTextComponent {
18
20
  describe("registerEditTool", () => {
19
21
  it("registers a tool named 'edit'", () => {
20
22
  const tools: string[] = [];
23
+ const mockPi: PiPrettyApi = {
24
+ registerTool(t: unknown) {
25
+ tools.push((t as { name: string }).name);
26
+ },
27
+ registerCommand() {},
28
+ on() {},
29
+ };
30
+
21
31
  registerEditTool(
22
- { registerTool: (t: any) => tools.push(t.name) } as any,
23
- () => ({ execute: async () => ({ content: [] }) }) as any,
32
+ mockPi,
33
+ () => ({ execute: async () => ({ content: [], details: undefined }) }),
24
34
  {
25
35
  cwd: process.cwd(),
26
36
  sp: (p: string) => p,
27
- TextComponent: MockTextComponent as any,
28
- fffState: {} as any,
29
- cursorStore: {} as any,
37
+ TextComponent: MockTextComponent as unknown as TextComponentCtor,
38
+ fffState: {
39
+ module: null,
40
+ finder: null,
41
+ partialIndex: false,
42
+ dbDir: null,
43
+ } satisfies FffState,
44
+ cursorStore: {
45
+ store: () => "",
46
+ get: () => undefined,
47
+ } as unknown as CursorStore,
30
48
  },
31
49
  (_id: string, _inv: () => void) => {},
32
50
  );
@@ -39,7 +57,7 @@ describe("getEditOperations", () => {
39
57
  const ops = getEditOperations({
40
58
  path: "f.ts",
41
59
  edits: [{ oldText: "a", newText: "b" }],
42
- } as any);
60
+ });
43
61
  expect(ops).toEqual([{ oldText: "a", newText: "b" }]);
44
62
  });
45
63
 
@@ -47,7 +65,7 @@ describe("getEditOperations", () => {
47
65
  const ops = getEditOperations({
48
66
  path: "f.ts",
49
67
  edits: [{ oldText: "x", newText: "x" }],
50
- } as any);
68
+ });
51
69
  expect(ops).toHaveLength(0);
52
70
  });
53
71
  });
package/src/edit.ts CHANGED
@@ -71,6 +71,20 @@ export function getEditOperations(input: EditParams): EditOperation[] {
71
71
  return oldText && oldText !== newText ? [{ oldText, newText }] : [];
72
72
  }
73
73
 
74
+ // 1-based file line where `needle` begins post-edit, for absolute diff gutters.
75
+ // 0 = file unreadable or needle not found (renderer falls back to relative).
76
+ // Duplicate identical edits collapse to the first match — acceptable.
77
+ function opEditLine(filePath: string, needle: string): number {
78
+ try {
79
+ if (!filePath || !existsSync(filePath)) return 0;
80
+ const f = readFileSync(filePath, "utf-8");
81
+ const idx = f.indexOf(needle);
82
+ return idx >= 0 ? f.slice(0, idx).split("\n").length : 0;
83
+ } catch {
84
+ return 0;
85
+ }
86
+ }
87
+
74
88
  export function summarizeEditOperations(operations: EditOperation[]) {
75
89
  const diffs = operations.map((e) => parseDiff(e.oldText, e.newText));
76
90
  const totalAdded = diffs.reduce((sum, d) => sum + d.added, 0);
@@ -122,20 +136,10 @@ export function registerEditTool(
122
136
  const { diffs, summary } = summarizeEditOperations(operations);
123
137
 
124
138
  if (operations.length === 1) {
125
- let editLine = 0;
126
- try {
127
- if (fp && existsSync(fp)) {
128
- const f = readFileSync(fp, "utf-8");
129
- const idx = f.indexOf(operations[0].newText);
130
- if (idx >= 0) editLine = f.slice(0, idx).split("\n").length;
131
- }
132
- } catch {
133
- editLine = 0;
134
- }
135
139
  setResultDetails(result, {
136
140
  _type: "editInfo",
137
141
  summary,
138
- editLine,
142
+ editLine: opEditLine(fp, operations[0].newText),
139
143
  oldContent: operations[0].oldText,
140
144
  newContent: operations[0].newText,
141
145
  language: fileLang,
@@ -154,6 +158,7 @@ export function registerEditTool(
154
158
  newContent: op.newText,
155
159
  language: fileLang,
156
160
  filePath: fp,
161
+ editLine: opEditLine(fp, op.newText),
157
162
  })),
158
163
  });
159
164
  return result;
@@ -212,6 +217,8 @@ export function registerEditTool(
212
217
  const diff = parseDiff(
213
218
  d.oldContent as string,
214
219
  d.newContent as string,
220
+ 3,
221
+ d.editLine as number,
215
222
  );
216
223
  renderSplit(
217
224
  diff,
@@ -253,9 +260,15 @@ export function registerEditTool(
253
260
  oldContent: string;
254
261
  newContent: string;
255
262
  language?: string;
263
+ editLine?: number;
256
264
  }>
257
265
  ).map((op) => {
258
- const diff = parseDiff(op.oldContent, op.newContent);
266
+ const diff = parseDiff(
267
+ op.oldContent,
268
+ op.newContent,
269
+ 3,
270
+ op.editLine ?? 0,
271
+ );
259
272
  return renderSplit(diff, op.language, MAX_RENDER_LINES, dc);
260
273
  }),
261
274
  )
package/src/once.ts CHANGED
@@ -14,7 +14,8 @@
14
14
  */
15
15
  export function once(pi: object, key: string, fn: () => void): void {
16
16
  const g = globalThis as { __pixOnce?: WeakMap<object, Set<string>> };
17
- const registry = (g.__pixOnce ??= new WeakMap<object, Set<string>>());
17
+ if (!g.__pixOnce) g.__pixOnce = new WeakMap<object, Set<string>>();
18
+ const registry = g.__pixOnce;
18
19
  let loaded = registry.get(pi);
19
20
  if (!loaded) {
20
21
  loaded = new Set<string>();