mcp-code-context 2.1.3
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/AGENT_INSTRUCTIONS.md +135 -0
- package/CHANGELOG.md +84 -0
- package/DEPLOYMENT_INSTRUCTIONS.md +110 -0
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/RELEASE_NOTES_v2.1.3.md +78 -0
- package/TROUBLESHOOTING.md +38 -0
- package/dist/src/ast/dartCompressor.d.ts +27 -0
- package/dist/src/ast/dartCompressor.js +859 -0
- package/dist/src/ast/dartCompressor.js.map +1 -0
- package/dist/src/ast/phpCompressor.d.ts +24 -0
- package/dist/src/ast/phpCompressor.js +634 -0
- package/dist/src/ast/phpCompressor.js.map +1 -0
- package/dist/src/ast/semanticCompressor.d.ts +22 -0
- package/dist/src/ast/semanticCompressor.js +677 -0
- package/dist/src/ast/semanticCompressor.js.map +1 -0
- package/dist/src/ast/writers/dartWriter.d.ts +37 -0
- package/dist/src/ast/writers/dartWriter.js +544 -0
- package/dist/src/ast/writers/dartWriter.js.map +1 -0
- package/dist/src/ast/writers/phpWriter.d.ts +37 -0
- package/dist/src/ast/writers/phpWriter.js +411 -0
- package/dist/src/ast/writers/phpWriter.js.map +1 -0
- package/dist/src/ast/writers/pyWriter.d.ts +37 -0
- package/dist/src/ast/writers/pyWriter.js +355 -0
- package/dist/src/ast/writers/pyWriter.js.map +1 -0
- package/dist/src/ast/writers/symbolWriter.d.ts +62 -0
- package/dist/src/ast/writers/symbolWriter.js +179 -0
- package/dist/src/ast/writers/symbolWriter.js.map +1 -0
- package/dist/src/ast/writers/tsWriter.d.ts +38 -0
- package/dist/src/ast/writers/tsWriter.js +344 -0
- package/dist/src/ast/writers/tsWriter.js.map +1 -0
- package/dist/src/index.d.ts +117 -0
- package/dist/src/index.js +1289 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/tests/test-confirmation-flow.d.ts +1 -0
- package/dist/src/tests/test-confirmation-flow.js +43 -0
- package/dist/src/tests/test-confirmation-flow.js.map +1 -0
- package/dist/src/utils/backupManager.d.ts +24 -0
- package/dist/src/utils/backupManager.js +59 -0
- package/dist/src/utils/backupManager.js.map +1 -0
- package/dist/src/utils/confirmationCache.d.ts +16 -0
- package/dist/src/utils/confirmationCache.js +40 -0
- package/dist/src/utils/confirmationCache.js.map +1 -0
- package/dist/src/utils/diffEngine.d.ts +21 -0
- package/dist/src/utils/diffEngine.js +163 -0
- package/dist/src/utils/diffEngine.js.map +1 -0
- package/dist/src/utils/fuzzyMatch.d.ts +8 -0
- package/dist/src/utils/fuzzyMatch.js +69 -0
- package/dist/src/utils/fuzzyMatch.js.map +1 -0
- package/dist/src/utils/ignoreManager.d.ts +22 -0
- package/dist/src/utils/ignoreManager.js +135 -0
- package/dist/src/utils/ignoreManager.js.map +1 -0
- package/dist/tests/debug-replace.d.ts +1 -0
- package/dist/tests/debug-replace.js +17 -0
- package/dist/tests/debug-replace.js.map +1 -0
- package/dist/tests/test-confirmation-flow.d.ts +1 -0
- package/dist/tests/test-confirmation-flow.js +43 -0
- package/dist/tests/test-confirmation-flow.js.map +1 -0
- package/dist/tests/test-dart.d.ts +1 -0
- package/dist/tests/test-dart.js +242 -0
- package/dist/tests/test-dart.js.map +1 -0
- package/dist/tests/test-php-ast.d.ts +1 -0
- package/dist/tests/test-php-ast.js +152 -0
- package/dist/tests/test-php-ast.js.map +1 -0
- package/dist/tests/test-php.d.ts +1 -0
- package/dist/tests/test-php.js +234 -0
- package/dist/tests/test-php.js.map +1 -0
- package/dist/tests/test-smoke.d.ts +5 -0
- package/dist/tests/test-smoke.js +83 -0
- package/dist/tests/test-smoke.js.map +1 -0
- package/dist/tests/writers/test-crlf-handling.d.ts +4 -0
- package/dist/tests/writers/test-crlf-handling.js +73 -0
- package/dist/tests/writers/test-crlf-handling.js.map +1 -0
- package/dist/tests/writers/test-write-smoke.d.ts +9 -0
- package/dist/tests/writers/test-write-smoke.js +346 -0
- package/dist/tests/writers/test-write-smoke.js.map +1 -0
- package/llms.txt +164 -0
- package/package.json +57 -0
- package/src/ast/dartCompressor.ts +1013 -0
- package/src/ast/phpCompressor.ts +814 -0
- package/src/ast/semanticCompressor.ts +808 -0
- package/src/ast/writers/dartWriter.ts +648 -0
- package/src/ast/writers/phpWriter.ts +508 -0
- package/src/ast/writers/pyWriter.ts +446 -0
- package/src/ast/writers/symbolWriter.ts +277 -0
- package/src/ast/writers/tsWriter.ts +418 -0
- package/src/index.ts +1553 -0
- package/src/utils/backupManager.ts +66 -0
- package/src/utils/confirmationCache.ts +61 -0
- package/src/utils/diffEngine.ts +223 -0
- package/src/utils/fuzzyMatch.ts +86 -0
- package/src/utils/ignoreManager.ts +144 -0
- package/tests/test-confirmation-flow.ts +49 -0
- package/tests/test-dart.d.ts +1 -0
- package/tests/test-dart.js +209 -0
- package/tests/test-dart.js.map +1 -0
- package/tests/test-dart.ts +265 -0
- package/tests/test-php-ast.d.ts +1 -0
- package/tests/test-php-ast.js +152 -0
- package/tests/test-php-ast.js.map +1 -0
- package/tests/test-php-ast.ts +155 -0
- package/tests/test-php.d.ts +1 -0
- package/tests/test-php.js +234 -0
- package/tests/test-php.js.map +1 -0
- package/tests/test-php.ts +261 -0
- package/tests/test-smoke.d.ts +5 -0
- package/tests/test-smoke.js +83 -0
- package/tests/test-smoke.js.map +1 -0
- package/tests/test-smoke.ts +93 -0
- package/tests/writers/test-crlf-handling.ts +100 -0
- package/tests/writers/test-write-smoke.ts +568 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pyWriter.ts — Python Surgical Writer
|
|
3
|
+
*
|
|
4
|
+
* Uses indentation-based block detection to locate symbol boundaries
|
|
5
|
+
* in Python files. Python's significant whitespace makes this different
|
|
6
|
+
* from brace-delimited languages — we detect block ends by watching
|
|
7
|
+
* for dedentation back to the original level.
|
|
8
|
+
*
|
|
9
|
+
* Supports: .py, .pyi
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ─── Types ──────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export interface WriteResult {
|
|
15
|
+
success: boolean;
|
|
16
|
+
newContent: string;
|
|
17
|
+
symbolsAffected: string[];
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type InsertPosition = "before" | "after" | "inside_start" | "inside_end";
|
|
22
|
+
|
|
23
|
+
// ─── Replace Symbol ─────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Replace the full text of a named symbol in a Python file.
|
|
27
|
+
* The newContent should include the def/class signature and body.
|
|
28
|
+
* When className is provided, scopes the search to within that class only.
|
|
29
|
+
*/
|
|
30
|
+
export function replacePySymbol(
|
|
31
|
+
content: string,
|
|
32
|
+
symbolName: string,
|
|
33
|
+
newContent: string,
|
|
34
|
+
className?: string,
|
|
35
|
+
): WriteResult {
|
|
36
|
+
const lines = content.split("\n");
|
|
37
|
+
const range = findSymbolRange(lines, symbolName, className);
|
|
38
|
+
|
|
39
|
+
if (!range) {
|
|
40
|
+
return {
|
|
41
|
+
success: false,
|
|
42
|
+
newContent: content,
|
|
43
|
+
symbolsAffected: [],
|
|
44
|
+
error: className
|
|
45
|
+
? `Symbol "${symbolName}" not found in class "${className}" in Python file`
|
|
46
|
+
: `Symbol "${symbolName}" not found in Python file`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { start, end } = range;
|
|
51
|
+
|
|
52
|
+
// Detect indentation of original
|
|
53
|
+
const originalIndent = getIndent(lines[start]);
|
|
54
|
+
const reindented = reindentPython(newContent.trim(), originalIndent);
|
|
55
|
+
|
|
56
|
+
// Splice
|
|
57
|
+
const before = lines.slice(0, start);
|
|
58
|
+
const after = lines.slice(end + 1);
|
|
59
|
+
const result = [...before, reindented, ...after].join("\n");
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
success: true,
|
|
63
|
+
newContent: result,
|
|
64
|
+
symbolsAffected: [symbolName],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Insert Code ────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Insert new code at a precise location relative to an anchor symbol.
|
|
72
|
+
* When className is provided, scopes the anchor search to within that class only.
|
|
73
|
+
*/
|
|
74
|
+
export function insertPyCode(
|
|
75
|
+
content: string,
|
|
76
|
+
code: string,
|
|
77
|
+
anchorSymbol: string | null,
|
|
78
|
+
position: InsertPosition,
|
|
79
|
+
className?: string,
|
|
80
|
+
): WriteResult {
|
|
81
|
+
const lines = content.split("\n");
|
|
82
|
+
|
|
83
|
+
if (!anchorSymbol) {
|
|
84
|
+
// No anchor — append at end of file
|
|
85
|
+
lines.push("", "", code.trim(), "");
|
|
86
|
+
return {
|
|
87
|
+
success: true,
|
|
88
|
+
newContent: lines.join("\n"),
|
|
89
|
+
symbolsAffected: [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const range = findSymbolRange(lines, anchorSymbol, className);
|
|
94
|
+
if (!range) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
newContent: content,
|
|
98
|
+
symbolsAffected: [],
|
|
99
|
+
error: className
|
|
100
|
+
? `Anchor symbol "${anchorSymbol}" not found in class "${className}"`
|
|
101
|
+
: `Anchor symbol "${anchorSymbol}" not found`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { start, end } = range;
|
|
106
|
+
const indent = getIndent(lines[start]);
|
|
107
|
+
|
|
108
|
+
switch (position) {
|
|
109
|
+
case "before": {
|
|
110
|
+
const reindented = reindentPython(code.trim(), indent);
|
|
111
|
+
// Add proper spacing (PEP 8: two blank lines before top-level, one inside class)
|
|
112
|
+
const spacing = indent === "" ? "\n\n" : "\n";
|
|
113
|
+
lines.splice(start, 0, reindented + spacing);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "after": {
|
|
117
|
+
const reindented = reindentPython(code.trim(), indent);
|
|
118
|
+
const spacing = indent === "" ? "\n\n" : "\n";
|
|
119
|
+
lines.splice(end + 1, 0, spacing + reindented);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case "inside_start": {
|
|
123
|
+
// Find the colon line (class Foo:) and insert after it
|
|
124
|
+
const colonLine = findColonLine(lines, start);
|
|
125
|
+
if (colonLine === -1) {
|
|
126
|
+
return {
|
|
127
|
+
success: false,
|
|
128
|
+
newContent: content,
|
|
129
|
+
symbolsAffected: [],
|
|
130
|
+
error: `Cannot find class body start for "${anchorSymbol}"`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Check if there's a docstring, insert after it
|
|
134
|
+
const insertAt = skipDocstring(lines, colonLine + 1);
|
|
135
|
+
const innerIndent = indent + " "; // Python convention: 4-space indent
|
|
136
|
+
const innerCode = reindentPython(code.trim(), innerIndent);
|
|
137
|
+
lines.splice(insertAt, 0, innerCode, "");
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case "inside_end": {
|
|
141
|
+
const innerIndent = indent + " ";
|
|
142
|
+
const innerCode = reindentPython(code.trim(), innerIndent);
|
|
143
|
+
lines.splice(end + 1, 0, "", innerCode);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
success: true,
|
|
150
|
+
newContent: lines.join("\n"),
|
|
151
|
+
symbolsAffected: [anchorSymbol],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ─── Rename Symbol ──────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Rename all occurrences of a symbol within a Python file.
|
|
159
|
+
*/
|
|
160
|
+
export function renamePySymbol(
|
|
161
|
+
content: string,
|
|
162
|
+
oldName: string,
|
|
163
|
+
newName: string,
|
|
164
|
+
): WriteResult {
|
|
165
|
+
const lines = content.split("\n");
|
|
166
|
+
const range = findSymbolRange(lines, oldName);
|
|
167
|
+
|
|
168
|
+
if (!range) {
|
|
169
|
+
return {
|
|
170
|
+
success: false,
|
|
171
|
+
newContent: content,
|
|
172
|
+
symbolsAffected: [],
|
|
173
|
+
error: `Symbol "${oldName}" not found for renaming`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Whole-word replacement
|
|
178
|
+
const escaped = oldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
179
|
+
const regex = new RegExp(`\\b${escaped}\\b`, "g");
|
|
180
|
+
const result = content.replace(regex, newName);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
success: true,
|
|
184
|
+
newContent: result,
|
|
185
|
+
symbolsAffected: [oldName],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── Remove Symbol ──────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Remove a named symbol from a Python file.
|
|
193
|
+
* When className is provided, scopes the search to within that class only.
|
|
194
|
+
*/
|
|
195
|
+
export function removePySymbol(
|
|
196
|
+
content: string,
|
|
197
|
+
symbolName: string,
|
|
198
|
+
className?: string,
|
|
199
|
+
): WriteResult {
|
|
200
|
+
const lines = content.split("\n");
|
|
201
|
+
const range = findSymbolRange(lines, symbolName, className);
|
|
202
|
+
|
|
203
|
+
if (!range) {
|
|
204
|
+
return {
|
|
205
|
+
success: false,
|
|
206
|
+
newContent: content,
|
|
207
|
+
symbolsAffected: [],
|
|
208
|
+
error: className
|
|
209
|
+
? `Symbol "${symbolName}" not found in class "${className}" for removal`
|
|
210
|
+
: `Symbol "${symbolName}" not found for removal`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Also remove leading decorators
|
|
215
|
+
let actualStart = range.start;
|
|
216
|
+
while (actualStart > 0 && lines[actualStart - 1].trim().startsWith("@")) {
|
|
217
|
+
actualStart--;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
lines.splice(actualStart, range.end - actualStart + 1);
|
|
221
|
+
|
|
222
|
+
let result = lines.join("\n");
|
|
223
|
+
result = result.replace(/\n{3,}/g, "\n\n");
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
success: true,
|
|
227
|
+
newContent: result,
|
|
228
|
+
symbolsAffected: [symbolName],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── Symbol Finder ──────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
interface SymbolRange {
|
|
235
|
+
start: number; // 0-indexed line
|
|
236
|
+
end: number; // 0-indexed line (inclusive)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Find the start and end line indices of a named symbol in a Python file.
|
|
241
|
+
* Uses indentation to detect block boundaries (Python's significant whitespace).
|
|
242
|
+
* When className is provided, scopes the search to within that class only.
|
|
243
|
+
*/
|
|
244
|
+
function findSymbolRange(
|
|
245
|
+
lines: string[],
|
|
246
|
+
symbolName: string,
|
|
247
|
+
className?: string,
|
|
248
|
+
): SymbolRange | null {
|
|
249
|
+
const escaped = symbolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
250
|
+
|
|
251
|
+
// If className is provided, find the class first, then search within it
|
|
252
|
+
if (className) {
|
|
253
|
+
const escapedClass = className.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
254
|
+
|
|
255
|
+
for (let i = 0; i < lines.length; i++) {
|
|
256
|
+
const trimmed = lines[i].trim();
|
|
257
|
+
if (!new RegExp(`^class\\s+${escapedClass}[\\s(:]`).test(trimmed)) continue;
|
|
258
|
+
|
|
259
|
+
// Found the class — search for the method within it
|
|
260
|
+
const classIndent = measureIndent(lines[i]);
|
|
261
|
+
let j = i + 1;
|
|
262
|
+
|
|
263
|
+
while (j < lines.length) {
|
|
264
|
+
const bodyLine = lines[j];
|
|
265
|
+
if (bodyLine.trim() === "") { j++; continue; }
|
|
266
|
+
if (measureIndent(bodyLine) <= classIndent && bodyLine.trim() !== "") break;
|
|
267
|
+
|
|
268
|
+
const bodyTrimmed = bodyLine.trim();
|
|
269
|
+
const isFunc = new RegExp(
|
|
270
|
+
`^(?:async\\s+)?def\\s+${escaped}\\s*\\(`,
|
|
271
|
+
).test(bodyTrimmed);
|
|
272
|
+
|
|
273
|
+
if (isFunc) {
|
|
274
|
+
const startLine = j;
|
|
275
|
+
const baseIndent = measureIndent(bodyLine);
|
|
276
|
+
|
|
277
|
+
// Check for decorators above
|
|
278
|
+
let decoratorStart = startLine;
|
|
279
|
+
while (
|
|
280
|
+
decoratorStart > 0 &&
|
|
281
|
+
lines[decoratorStart - 1].trim().startsWith("@")
|
|
282
|
+
) {
|
|
283
|
+
decoratorStart--;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Find end of block
|
|
287
|
+
let endLine = startLine;
|
|
288
|
+
let k = startLine + 1;
|
|
289
|
+
while (k < lines.length) {
|
|
290
|
+
const innerLine = lines[k];
|
|
291
|
+
if (innerLine.trim() === "") { k++; continue; }
|
|
292
|
+
if (measureIndent(innerLine) > baseIndent) { endLine = k; k++; }
|
|
293
|
+
else break;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
while (endLine > startLine && lines[endLine].trim() === "") {
|
|
297
|
+
endLine--;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { start: decoratorStart, end: endLine };
|
|
301
|
+
}
|
|
302
|
+
j++;
|
|
303
|
+
}
|
|
304
|
+
return null; // Method not found within class
|
|
305
|
+
}
|
|
306
|
+
return null; // Class not found
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Original behavior when no className is provided
|
|
310
|
+
for (let i = 0; i < lines.length; i++) {
|
|
311
|
+
const line = lines[i];
|
|
312
|
+
const trimmed = line.trim();
|
|
313
|
+
|
|
314
|
+
const isClass = new RegExp(`^class\\s+${escaped}[\\s(:]`).test(trimmed);
|
|
315
|
+
const isFunc = new RegExp(
|
|
316
|
+
`^(?:async\\s+)?def\\s+${escaped}\\s*\\(`,
|
|
317
|
+
).test(trimmed);
|
|
318
|
+
|
|
319
|
+
if (!isClass && !isFunc) continue;
|
|
320
|
+
|
|
321
|
+
const startLine = i;
|
|
322
|
+
const baseIndent = measureIndent(line);
|
|
323
|
+
|
|
324
|
+
// Also check for decorators above
|
|
325
|
+
let decoratorStart = startLine;
|
|
326
|
+
while (
|
|
327
|
+
decoratorStart > 0 &&
|
|
328
|
+
lines[decoratorStart - 1].trim().startsWith("@")
|
|
329
|
+
) {
|
|
330
|
+
decoratorStart--;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Find the end of the block
|
|
334
|
+
let endLine = startLine;
|
|
335
|
+
let j = startLine + 1;
|
|
336
|
+
|
|
337
|
+
while (j < lines.length) {
|
|
338
|
+
const bodyLine = lines[j];
|
|
339
|
+
if (bodyLine.trim() === "") {
|
|
340
|
+
j++;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (measureIndent(bodyLine) > baseIndent) {
|
|
344
|
+
endLine = j;
|
|
345
|
+
j++;
|
|
346
|
+
} else {
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Trim trailing blank lines from the block
|
|
352
|
+
while (endLine > startLine && lines[endLine].trim() === "") {
|
|
353
|
+
endLine--;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return { start: decoratorStart, end: endLine };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ─── Utility Functions ──────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
function measureIndent(line: string): number {
|
|
365
|
+
const match = line.match(/^(\s*)/);
|
|
366
|
+
return match ? match[1].length : 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function getIndent(line: string): string {
|
|
370
|
+
const match = line.match(/^(\s*)/);
|
|
371
|
+
return match ? match[1] : "";
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Re-indent a block of Python code to match a target indentation level.
|
|
376
|
+
* Preserves relative indentation within the block.
|
|
377
|
+
*/
|
|
378
|
+
function reindentPython(code: string, targetIndent: string): string {
|
|
379
|
+
const lines = code.split("\n");
|
|
380
|
+
if (lines.length === 0) return code;
|
|
381
|
+
|
|
382
|
+
// Find minimum non-empty indentation
|
|
383
|
+
let minIndent = Infinity;
|
|
384
|
+
for (const line of lines) {
|
|
385
|
+
if (line.trim() === "") continue;
|
|
386
|
+
const indent = measureIndent(line);
|
|
387
|
+
if (indent < minIndent) minIndent = indent;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (minIndent === Infinity) minIndent = 0;
|
|
391
|
+
|
|
392
|
+
return lines
|
|
393
|
+
.map((line) => {
|
|
394
|
+
if (line.trim() === "") return "";
|
|
395
|
+
return targetIndent + line.slice(minIndent);
|
|
396
|
+
})
|
|
397
|
+
.join("\n");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Find the line containing the colon that starts a class/function body.
|
|
402
|
+
*/
|
|
403
|
+
function findColonLine(lines: string[], startIdx: number): number {
|
|
404
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
405
|
+
if (lines[i].includes(":")) return i;
|
|
406
|
+
}
|
|
407
|
+
return -1;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Skip past a docstring (if present) to find the first line of actual code.
|
|
412
|
+
*/
|
|
413
|
+
function skipDocstring(lines: string[], startIdx: number): number {
|
|
414
|
+
let i = startIdx;
|
|
415
|
+
|
|
416
|
+
// Skip blank lines
|
|
417
|
+
while (i < lines.length && lines[i].trim() === "") i++;
|
|
418
|
+
|
|
419
|
+
if (i >= lines.length) return startIdx;
|
|
420
|
+
|
|
421
|
+
const trimmed = lines[i].trim();
|
|
422
|
+
|
|
423
|
+
// Look for triple-quoted docstring
|
|
424
|
+
if (
|
|
425
|
+
trimmed.startsWith('"""') ||
|
|
426
|
+
trimmed.startsWith("'''")
|
|
427
|
+
) {
|
|
428
|
+
const quote = trimmed.slice(0, 3);
|
|
429
|
+
|
|
430
|
+
// Single-line docstring
|
|
431
|
+
if (trimmed.endsWith(quote) && trimmed.length > 3) {
|
|
432
|
+
return i + 1;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Multi-line docstring
|
|
436
|
+
i++;
|
|
437
|
+
while (i < lines.length) {
|
|
438
|
+
if (lines[i].trim().endsWith(quote)) {
|
|
439
|
+
return i + 1;
|
|
440
|
+
}
|
|
441
|
+
i++;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return startIdx;
|
|
446
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* symbolWriter.ts — Central Dispatcher for Surgical Write Operations
|
|
3
|
+
*
|
|
4
|
+
* Routes write operations to the correct language-specific writer based
|
|
5
|
+
* on file extension. This is the "write" counterpart to semanticCompressor.ts.
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* - TypeScript/JavaScript (.ts, .tsx, .js, .jsx, .mts, .mjs, .cts, .cjs)
|
|
9
|
+
* - PHP (.php, .phtml)
|
|
10
|
+
* - Dart (.dart)
|
|
11
|
+
* - Python (.py, .pyi)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
replaceTSSymbol,
|
|
18
|
+
insertTSCode,
|
|
19
|
+
renameTSSymbol,
|
|
20
|
+
removeTSSymbol,
|
|
21
|
+
type WriteResult,
|
|
22
|
+
type InsertPosition,
|
|
23
|
+
} from "./tsWriter.js";
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
replacePHPSymbol,
|
|
27
|
+
insertPHPCode,
|
|
28
|
+
renamePHPSymbol,
|
|
29
|
+
removePHPSymbol,
|
|
30
|
+
} from "./phpWriter.js";
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
replaceDartSymbol,
|
|
34
|
+
insertDartCode,
|
|
35
|
+
renameDartSymbol,
|
|
36
|
+
removeDartSymbol,
|
|
37
|
+
} from "./dartWriter.js";
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
replacePySymbol,
|
|
41
|
+
insertPyCode,
|
|
42
|
+
renamePySymbol,
|
|
43
|
+
removePySymbol,
|
|
44
|
+
} from "./pyWriter.js";
|
|
45
|
+
|
|
46
|
+
// Re-export types for consumers
|
|
47
|
+
export type { WriteResult, InsertPosition };
|
|
48
|
+
|
|
49
|
+
// ─── Extension Sets ─────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
const TS_EXTENSIONS = new Set([
|
|
52
|
+
".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cts", ".cjs",
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const PHP_EXTENSIONS = new Set([".php", ".phtml"]);
|
|
56
|
+
|
|
57
|
+
const DART_EXTENSIONS = new Set([".dart"]);
|
|
58
|
+
|
|
59
|
+
const PY_EXTENSIONS = new Set([".py", ".pyi"]);
|
|
60
|
+
|
|
61
|
+
/** All extensions supported for surgical writing */
|
|
62
|
+
export const WRITABLE_EXTENSIONS = new Set([
|
|
63
|
+
...TS_EXTENSIONS,
|
|
64
|
+
...PHP_EXTENSIONS,
|
|
65
|
+
...DART_EXTENSIONS,
|
|
66
|
+
...PY_EXTENSIONS,
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
// ─── Replace Symbol ─────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Replace the full source code of a named symbol in a file.
|
|
73
|
+
* Dispatches to the correct language-specific writer.
|
|
74
|
+
*
|
|
75
|
+
* @param filePath - Used to detect language via extension
|
|
76
|
+
* @param content - Current file content
|
|
77
|
+
* @param symbolName - Name of the symbol to replace
|
|
78
|
+
* @param newContent - Complete replacement code (signature + body)
|
|
79
|
+
* @param className - Optional. Scopes search to within a specific class
|
|
80
|
+
*/
|
|
81
|
+
export function replaceSymbol(
|
|
82
|
+
filePath: string,
|
|
83
|
+
content: string,
|
|
84
|
+
symbolName: string,
|
|
85
|
+
newContent: string,
|
|
86
|
+
className?: string,
|
|
87
|
+
): WriteResult {
|
|
88
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
89
|
+
|
|
90
|
+
if (TS_EXTENSIONS.has(ext)) {
|
|
91
|
+
return replaceTSSymbol(content, symbolName, newContent, filePath, className);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (PHP_EXTENSIONS.has(ext)) {
|
|
95
|
+
return replacePHPSymbol(content, symbolName, newContent, className);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (DART_EXTENSIONS.has(ext)) {
|
|
99
|
+
return replaceDartSymbol(content, symbolName, newContent, className);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (PY_EXTENSIONS.has(ext)) {
|
|
103
|
+
return replacePySymbol(content, symbolName, newContent, className);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
newContent: content,
|
|
109
|
+
symbolsAffected: [],
|
|
110
|
+
error: `Unsupported file type "${ext}" for surgical writing. Supported: TypeScript, JavaScript, PHP, Dart, Python.`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Insert Code ────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Insert new code at a precise location relative to an anchor symbol.
|
|
118
|
+
*
|
|
119
|
+
* @param filePath - Used to detect language via extension
|
|
120
|
+
* @param content - Current file content
|
|
121
|
+
* @param code - The code to insert
|
|
122
|
+
* @param anchorSymbol - Symbol to position relative to (null = end of file)
|
|
123
|
+
* @param position - Where to insert: before, after, inside_start, inside_end
|
|
124
|
+
* @param className - Optional. Scopes anchor search to within a specific class
|
|
125
|
+
*/
|
|
126
|
+
export function insertCode(
|
|
127
|
+
filePath: string,
|
|
128
|
+
content: string,
|
|
129
|
+
code: string,
|
|
130
|
+
anchorSymbol: string | null,
|
|
131
|
+
position: InsertPosition,
|
|
132
|
+
className?: string,
|
|
133
|
+
): WriteResult {
|
|
134
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
135
|
+
|
|
136
|
+
if (TS_EXTENSIONS.has(ext)) {
|
|
137
|
+
return insertTSCode(content, code, anchorSymbol, position, filePath, className);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (PHP_EXTENSIONS.has(ext)) {
|
|
141
|
+
return insertPHPCode(content, code, anchorSymbol, position, className);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (DART_EXTENSIONS.has(ext)) {
|
|
145
|
+
return insertDartCode(content, code, anchorSymbol, position, className);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (PY_EXTENSIONS.has(ext)) {
|
|
149
|
+
return insertPyCode(content, code, anchorSymbol, position, className);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
newContent: content,
|
|
155
|
+
symbolsAffected: [],
|
|
156
|
+
error: `Unsupported file type "${ext}" for code insertion.`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─── Rename Symbol ──────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Rename all occurrences of a symbol within a single file.
|
|
164
|
+
*
|
|
165
|
+
* @param filePath - Used to detect language via extension
|
|
166
|
+
* @param content - Current file content
|
|
167
|
+
* @param oldName - Current symbol name
|
|
168
|
+
* @param newName - New symbol name
|
|
169
|
+
*/
|
|
170
|
+
export function renameInFile(
|
|
171
|
+
filePath: string,
|
|
172
|
+
content: string,
|
|
173
|
+
oldName: string,
|
|
174
|
+
newName: string,
|
|
175
|
+
): WriteResult {
|
|
176
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
177
|
+
|
|
178
|
+
if (TS_EXTENSIONS.has(ext)) {
|
|
179
|
+
return renameTSSymbol(content, oldName, newName, filePath);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (PHP_EXTENSIONS.has(ext)) {
|
|
183
|
+
return renamePHPSymbol(content, oldName, newName);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (DART_EXTENSIONS.has(ext)) {
|
|
187
|
+
return renameDartSymbol(content, oldName, newName);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (PY_EXTENSIONS.has(ext)) {
|
|
191
|
+
return renamePySymbol(content, oldName, newName);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
newContent: content,
|
|
197
|
+
symbolsAffected: [],
|
|
198
|
+
error: `Unsupported file type "${ext}" for symbol renaming.`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Renames all whole-word occurrences of a symbol in a file.
|
|
204
|
+
* UNLIKE renameInFile, this does NOT check for the symbol definition.
|
|
205
|
+
* Used for updating references in dependent files.
|
|
206
|
+
*/
|
|
207
|
+
export function renameReferencesInFile(
|
|
208
|
+
filePath: string,
|
|
209
|
+
content: string,
|
|
210
|
+
oldName: string,
|
|
211
|
+
newName: string,
|
|
212
|
+
): WriteResult {
|
|
213
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
214
|
+
|
|
215
|
+
// Simple whole-word replacement is generally safe for cross-file reference updates
|
|
216
|
+
// where we ALREADY know via import analysis that the file depends on this symbol.
|
|
217
|
+
|
|
218
|
+
// Scape regex special chars
|
|
219
|
+
const escaped = oldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
220
|
+
|
|
221
|
+
// Use a regex that respects common identifier boundaries across JS/PHP/Dart/Python
|
|
222
|
+
const regex = new RegExp(`(?<=^|[^a-zA-Z0-9_$])(${escaped})(?=[^a-zA-Z0-9_$]|$)`, "g");
|
|
223
|
+
|
|
224
|
+
if (!regex.test(content)) {
|
|
225
|
+
return { success: true, newContent: content, symbolsAffected: [] };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const result = content.replace(regex, newName);
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
success: true,
|
|
232
|
+
newContent: result,
|
|
233
|
+
symbolsAffected: [oldName],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ─── Remove Symbol ──────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Remove a named symbol from a file.
|
|
241
|
+
*
|
|
242
|
+
* @param filePath - Used to detect language via extension
|
|
243
|
+
* @param content - Current file content
|
|
244
|
+
* @param symbolName - Name of the symbol to remove
|
|
245
|
+
* @param className - Optional. Scopes search to within a specific class
|
|
246
|
+
*/
|
|
247
|
+
export function removeSymbolFromFile(
|
|
248
|
+
filePath: string,
|
|
249
|
+
content: string,
|
|
250
|
+
symbolName: string,
|
|
251
|
+
className?: string,
|
|
252
|
+
): WriteResult {
|
|
253
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
254
|
+
|
|
255
|
+
if (TS_EXTENSIONS.has(ext)) {
|
|
256
|
+
return removeTSSymbol(content, symbolName, filePath, className);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (PHP_EXTENSIONS.has(ext)) {
|
|
260
|
+
return removePHPSymbol(content, symbolName, className);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (DART_EXTENSIONS.has(ext)) {
|
|
264
|
+
return removeDartSymbol(content, symbolName, className);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (PY_EXTENSIONS.has(ext)) {
|
|
268
|
+
return removePySymbol(content, symbolName, className);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
success: false,
|
|
273
|
+
newContent: content,
|
|
274
|
+
symbolsAffected: [],
|
|
275
|
+
error: `Unsupported file type "${ext}" for symbol removal.`,
|
|
276
|
+
};
|
|
277
|
+
}
|