@mrxkun/mcfast-mcp 3.1.0 → 3.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/package.json +6 -1
- package/src/index.js +119 -8
- package/src/strategies/ast-detector.js +10 -2
- package/src/strategies/fuzzy-patch.js +26 -3
- package/src/strategies/syntax-validator.js +85 -30
- package/src/strategies/tree-sitter/index.js +1 -1
- package/src/strategies/tree-sitter/languages.js +6 -0
- package/src/strategies/tree-sitter/queries.js +71 -0
- package/src/strategies/tree-sitter/refactor.js +175 -0
- package/src/strategies/tree-sitter/wasm/tree-sitter-c-sharp.wasm +0 -0
- package/src/strategies/tree-sitter/wasm/tree-sitter-cpp.wasm +0 -0
- package/src/strategies/tree-sitter/wasm/tree-sitter-php.wasm +0 -0
- package/src/strategies/tree-sitter/wasm/tree-sitter-python.wasm +0 -0
- package/src/strategies/tree-sitter/wasm/tree-sitter-ruby.wasm +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrxkun/mcfast-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Ultra-fast code editing with fuzzy patching, auto-rollback, and 5 unified tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,9 +34,14 @@
|
|
|
34
34
|
"@modelcontextprotocol/sdk": "^0.6.0",
|
|
35
35
|
"fast-glob": "^3.3.3",
|
|
36
36
|
"ignore": "^7.0.5",
|
|
37
|
+
"tree-sitter-c-sharp": "^0.23.1",
|
|
38
|
+
"tree-sitter-cpp": "^0.23.4",
|
|
37
39
|
"tree-sitter-go": "^0.25.0",
|
|
38
40
|
"tree-sitter-java": "^0.23.5",
|
|
39
41
|
"tree-sitter-javascript": "^0.25.0",
|
|
42
|
+
"tree-sitter-php": "^0.24.2",
|
|
43
|
+
"tree-sitter-python": "^0.25.0",
|
|
44
|
+
"tree-sitter-ruby": "^0.23.1",
|
|
40
45
|
"tree-sitter-rust": "^0.24.0",
|
|
41
46
|
"web-tree-sitter": "^0.26.5"
|
|
42
47
|
}
|
package/src/index.js
CHANGED
|
@@ -30,7 +30,14 @@ import {
|
|
|
30
30
|
} from './strategies/multi-file-coordinator.js';
|
|
31
31
|
import { detectLanguage, validateSyntax } from './strategies/syntax-validator.js';
|
|
32
32
|
import { validate as validateTreeSitter } from './strategies/tree-sitter/index.js';
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
renameSymbol as renameSymbolTreeSitter,
|
|
35
|
+
findDefinition,
|
|
36
|
+
findReferences,
|
|
37
|
+
organizeImports,
|
|
38
|
+
extractFunction,
|
|
39
|
+
moveCode
|
|
40
|
+
} from './strategies/tree-sitter/refactor.js';
|
|
34
41
|
import { safeEdit } from './utils/backup.js';
|
|
35
42
|
import { formatError } from './utils/error-formatter.js';
|
|
36
43
|
|
|
@@ -71,6 +78,8 @@ const toolIcons = {
|
|
|
71
78
|
list_files_fast: '📁',
|
|
72
79
|
edit_file: '✏️',
|
|
73
80
|
read_file: '📖',
|
|
81
|
+
get_definition: '🔗',
|
|
82
|
+
find_references: '📑',
|
|
74
83
|
};
|
|
75
84
|
|
|
76
85
|
// Backward compatibility mapping
|
|
@@ -82,7 +91,8 @@ const TOOL_ALIASES = {
|
|
|
82
91
|
'search_code_ai': 'search',
|
|
83
92
|
'search_filesystem': 'search',
|
|
84
93
|
'list_files_fast': 'list_files',
|
|
85
|
-
'read_file': 'read'
|
|
94
|
+
'read_file': 'read',
|
|
95
|
+
'goto_definition': 'get_definition' // alias
|
|
86
96
|
};
|
|
87
97
|
|
|
88
98
|
/**
|
|
@@ -193,13 +203,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
193
203
|
// CORE TOOL 1: edit (consolidates apply_fast + edit_file + apply_search_replace)
|
|
194
204
|
{
|
|
195
205
|
name: "edit",
|
|
196
|
-
description: "**PRIMARY TOOL FOR EDITING FILES** -
|
|
206
|
+
description: "**PRIMARY TOOL FOR EDITING FILES** - Intelligent auto-switching strategies: (1) Search & Replace (Fastest) - use 'Replace X with Y', (2) Placeholder (Efficient) - use '// ... existing code ...', (3) Mercury AI (Intelligent) - for complex refactoring. Includes Auto-Rollback for syntax errors.",
|
|
197
207
|
inputSchema: {
|
|
198
208
|
type: "object",
|
|
199
209
|
properties: {
|
|
200
210
|
instruction: {
|
|
201
211
|
type: "string",
|
|
202
|
-
description: "Natural language instruction
|
|
212
|
+
description: "Natural language instruction. For simple edits, use 'Replace <exact code> with <new code>'."
|
|
203
213
|
},
|
|
204
214
|
files: {
|
|
205
215
|
type: "object",
|
|
@@ -208,7 +218,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
208
218
|
},
|
|
209
219
|
code_edit: {
|
|
210
220
|
type: "string",
|
|
211
|
-
description: "Optional:
|
|
221
|
+
description: "Optional: Code snippet. Use '// ... existing code ...' placeholders to save tokens."
|
|
212
222
|
},
|
|
213
223
|
dryRun: {
|
|
214
224
|
type: "boolean",
|
|
@@ -221,13 +231,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
221
231
|
// CORE TOOL 2: search (consolidates search_code + search_code_ai + search_filesystem)
|
|
222
232
|
{
|
|
223
233
|
name: "search",
|
|
224
|
-
description: "**UNIFIED CODE SEARCH** -
|
|
234
|
+
description: "**UNIFIED CODE SEARCH** - Strategies: (1) Local (if files provided), (2) Filesystem grep (fast string match), (3) AI Semantic (natural language queries).",
|
|
225
235
|
inputSchema: {
|
|
226
236
|
type: "object",
|
|
227
237
|
properties: {
|
|
228
238
|
query: {
|
|
229
239
|
type: "string",
|
|
230
|
-
description: "Search query
|
|
240
|
+
description: "Search query. Use specific strings for grep, natural language for AI."
|
|
231
241
|
},
|
|
232
242
|
files: {
|
|
233
243
|
type: "object",
|
|
@@ -253,7 +263,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
253
263
|
// CORE TOOL 3: read
|
|
254
264
|
{
|
|
255
265
|
name: "read",
|
|
256
|
-
description: "Read file contents
|
|
266
|
+
description: "Read file contents. CRITICAL: Use `start_line` and `end_line` for large files to reduce token usage and latency.",
|
|
257
267
|
inputSchema: {
|
|
258
268
|
type: "object",
|
|
259
269
|
properties: {
|
|
@@ -290,6 +300,32 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
290
300
|
},
|
|
291
301
|
required: ["instruction", "files"]
|
|
292
302
|
}
|
|
303
|
+
},
|
|
304
|
+
// NEW TOOL: get_definition
|
|
305
|
+
{
|
|
306
|
+
name: "get_definition",
|
|
307
|
+
description: "Find the definition of a symbol in a file (LSP-like).",
|
|
308
|
+
inputSchema: {
|
|
309
|
+
type: "object",
|
|
310
|
+
properties: {
|
|
311
|
+
path: { type: "string", description: "Path to the file containing the usage" },
|
|
312
|
+
symbol: { type: "string", description: "The symbol/identifier to find definition for" }
|
|
313
|
+
},
|
|
314
|
+
required: ["path", "symbol"]
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
// NEW TOOL: find_references
|
|
318
|
+
{
|
|
319
|
+
name: "find_references",
|
|
320
|
+
description: "Find all usages of a symbol across the file (and potentially others if supported).",
|
|
321
|
+
inputSchema: {
|
|
322
|
+
type: "object",
|
|
323
|
+
properties: {
|
|
324
|
+
path: { type: "string", description: "Path to the file containing the symbol" },
|
|
325
|
+
symbol: { type: "string", description: "The symbol/identifier to find references for" }
|
|
326
|
+
},
|
|
327
|
+
required: ["path", "symbol"]
|
|
328
|
+
}
|
|
293
329
|
}
|
|
294
330
|
],
|
|
295
331
|
};
|
|
@@ -442,6 +478,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
442
478
|
result = await handleReapply(args);
|
|
443
479
|
summary = 'Retry edit applied';
|
|
444
480
|
}
|
|
481
|
+
else if (name === "get_definition") {
|
|
482
|
+
result = await handleGetDefinition(args);
|
|
483
|
+
summary = 'Definition found';
|
|
484
|
+
}
|
|
485
|
+
else if (name === "find_references") {
|
|
486
|
+
result = await handleFindReferences(args);
|
|
487
|
+
summary = 'References found';
|
|
488
|
+
}
|
|
445
489
|
else {
|
|
446
490
|
throw new Error(`Tool not found: ${name}`);
|
|
447
491
|
}
|
|
@@ -696,10 +740,35 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
696
740
|
return renameIdentifier(ast, pattern.oldName, pattern.newName);
|
|
697
741
|
});
|
|
698
742
|
}
|
|
743
|
+
} else if (pattern.type === 'extract_function') {
|
|
744
|
+
console.error(`${colors.cyan}[TREE-SITTER]${colors.reset} Extracting function '${pattern.functionName}' from lines ${pattern.startLine}-${pattern.endLine}`);
|
|
745
|
+
const newCode = await extractFunction(fileContent, filePath, pattern.startLine, pattern.endLine, pattern.functionName);
|
|
746
|
+
transformResult = { success: true, code: newCode, count: 1 };
|
|
747
|
+
} else if (pattern.type === 'move_code') {
|
|
748
|
+
console.error(`${colors.cyan}[TREE-SITTER]${colors.reset} Moving code from lines ${pattern.startLine}-${pattern.endLine} to line ${pattern.targetLine}`);
|
|
749
|
+
const newCode = await moveCode(fileContent, filePath, pattern.startLine, pattern.endLine, pattern.targetLine);
|
|
750
|
+
transformResult = { success: true, code: newCode, count: 1 };
|
|
699
751
|
} else if (pattern.type.startsWith('inline')) {
|
|
700
752
|
transformResult = applyASTTransformation(fileContent, filePath, (ast) => {
|
|
701
753
|
return inlineVariable(ast, pattern.oldName);
|
|
702
754
|
});
|
|
755
|
+
} else if (pattern.type === 'organize_imports') {
|
|
756
|
+
// Supported for Tree-sitter languages (JS/TS/Go/Python/etc)
|
|
757
|
+
const language = detectLanguage(filePath);
|
|
758
|
+
console.error(`${colors.cyan}[TREE-SITTER]${colors.reset} Organizing imports for ${language}`);
|
|
759
|
+
|
|
760
|
+
const newCode = await organizeImports(fileContent, filePath);
|
|
761
|
+
// Check if anything changed
|
|
762
|
+
if (newCode === fileContent) {
|
|
763
|
+
return {
|
|
764
|
+
content: [{
|
|
765
|
+
type: "text",
|
|
766
|
+
text: `✅ Imports already organized for ${filePath}`
|
|
767
|
+
}]
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
transformResult = { success: true, code: newCode, count: 1 };
|
|
771
|
+
|
|
703
772
|
} else {
|
|
704
773
|
throw new Error(`Unsupported refactoring type: ${pattern.type}`);
|
|
705
774
|
}
|
|
@@ -1460,6 +1529,48 @@ async function handleSearchCodeAI({ query, files, contextLines = 2 }) {
|
|
|
1460
1529
|
}
|
|
1461
1530
|
}
|
|
1462
1531
|
|
|
1532
|
+
async function handleGetDefinition({ path: filePath, symbol }) {
|
|
1533
|
+
if (!filePath || !symbol) throw new Error("Missing path or symbol");
|
|
1534
|
+
|
|
1535
|
+
// Read file content first
|
|
1536
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
1537
|
+
const definitions = await findDefinition(content, filePath, symbol);
|
|
1538
|
+
|
|
1539
|
+
if (definitions.length === 0) {
|
|
1540
|
+
return {
|
|
1541
|
+
content: [{ type: "text", text: `No definition found for '${symbol}' in ${filePath}` }]
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const def = definitions[0]; // Take first
|
|
1546
|
+
|
|
1547
|
+
return {
|
|
1548
|
+
content: [{ type: "text", text: `Definition of '${symbol}':\n${filePath}:${def.line}:${def.column} (${def.type || 'unknown'})` }]
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
async function handleFindReferences({ path: filePath, symbol }) {
|
|
1553
|
+
if (!filePath || !symbol) throw new Error("Missing path or symbol");
|
|
1554
|
+
|
|
1555
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
1556
|
+
const references = await findReferences(content, filePath, symbol);
|
|
1557
|
+
|
|
1558
|
+
if (references.length === 0) {
|
|
1559
|
+
return {
|
|
1560
|
+
content: [{ type: "text", text: `No references found for '${symbol}' in ${filePath}` }]
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
let output = `References for '${symbol}' in ${filePath} (${references.length}):\n`;
|
|
1565
|
+
references.forEach(ref => {
|
|
1566
|
+
output += `${ref.line}:${ref.column}\n`;
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
return {
|
|
1570
|
+
content: [{ type: "text", text: output }]
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1463
1574
|
/**
|
|
1464
1575
|
* Start Server
|
|
1465
1576
|
*/
|
|
@@ -62,14 +62,22 @@ export function detectRefactoringPattern(instruction) {
|
|
|
62
62
|
rename_function: /rename\s+(?:function|func)\s+["`']?(\w+)["`']?\s+to\s+["`']?(\w+)["`']?/i,
|
|
63
63
|
rename_class: /rename\s+class\s+["`']?(\w+)["`']?\s+to\s+["`']?(\w+)["`']?/i,
|
|
64
64
|
rename_any: /rename\s+["`']?(\w+)["`']?\s+to\s+["`']?(\w+)["`']?/i,
|
|
65
|
-
extract_function: /extract\s+(?:function|method)/i,
|
|
65
|
+
extract_function: /extract\s+(?:function|method)\s+["`']?(\w+)["`']?\s+from\s+line\s+(\d+)\s+to\s+(\d+)/i,
|
|
66
|
+
move_code: /move\s+code\s+from\s+line\s+(\d+)\s+to\s+(\d+)\s+to\s+line\s+(\d+)/i,
|
|
66
67
|
inline_variable: /inline\s+(?:variable|var|const|let)\s+["`']?(\w+)["`']?/i,
|
|
67
|
-
inline_function: /inline\s+(?:function|func)\s+["`']?(\w+)["`']?/i
|
|
68
|
+
inline_function: /inline\s+(?:function|func)\s+["`']?(\w+)["`']?/i,
|
|
69
|
+
organize_imports: /organize\s+imports/i
|
|
68
70
|
};
|
|
69
71
|
|
|
70
72
|
for (const [type, pattern] of Object.entries(patterns)) {
|
|
71
73
|
const match = instruction.match(pattern);
|
|
72
74
|
if (match) {
|
|
75
|
+
if (type === 'extract_function') {
|
|
76
|
+
return { type, functionName: match[1], startLine: parseInt(match[2]), endLine: parseInt(match[3]) };
|
|
77
|
+
}
|
|
78
|
+
if (type === 'move_code') {
|
|
79
|
+
return { type, startLine: parseInt(match[1]), endLine: parseInt(match[2]), targetLine: parseInt(match[3]) };
|
|
80
|
+
}
|
|
73
81
|
return {
|
|
74
82
|
type,
|
|
75
83
|
oldName: match[1],
|
|
@@ -134,7 +134,7 @@ export function parseDiff(diffText) {
|
|
|
134
134
|
export function findBestMatch(targetLines, fileLines, startHint = 0) {
|
|
135
135
|
let bestMatch = null;
|
|
136
136
|
let bestScore = Infinity;
|
|
137
|
-
const maxIterations =
|
|
137
|
+
const maxIterations = 50000; // Increased from 10k to 50k
|
|
138
138
|
let iterations = 0;
|
|
139
139
|
|
|
140
140
|
const useSemanticMatching = isSemanticMatchingEnabled();
|
|
@@ -143,6 +143,10 @@ export function findBestMatch(targetLines, fileLines, startHint = 0) {
|
|
|
143
143
|
console.error('[FUZZY] Semantic matching enabled');
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// optimization: pre-normalize lines to handle indentation/whitespace
|
|
147
|
+
const normTargetLines = targetLines.map(l => normalizeWhitespace(l));
|
|
148
|
+
const normFileLines = fileLines.map(l => normalizeWhitespace(l));
|
|
149
|
+
|
|
146
150
|
// Try exact match first at hint location
|
|
147
151
|
if (startHint >= 0 && startHint + targetLines.length <= fileLines.length) {
|
|
148
152
|
const exactMatch = targetLines.every((line, i) =>
|
|
@@ -161,6 +165,21 @@ export function findBestMatch(targetLines, fileLines, startHint = 0) {
|
|
|
161
165
|
break;
|
|
162
166
|
}
|
|
163
167
|
|
|
168
|
+
// Optimization: Sampled check
|
|
169
|
+
// Check first, middle, and last line. If they are very different, skip block.
|
|
170
|
+
if (targetLines.length > 5) {
|
|
171
|
+
const indices = [0, Math.floor(targetLines.length / 2), targetLines.length - 1];
|
|
172
|
+
let sampleDist = 0;
|
|
173
|
+
for (const idx of indices) {
|
|
174
|
+
// Use normalized lines for check
|
|
175
|
+
sampleDist += levenshteinDistance(normTargetLines[idx], normFileLines[i + idx], 20); // strict limit
|
|
176
|
+
}
|
|
177
|
+
// If average distance per sample line is high (> 10 chars), skip
|
|
178
|
+
if (sampleDist > indices.length * 10) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
164
183
|
let totalDistance = 0;
|
|
165
184
|
let tokenSimilaritySum = 0;
|
|
166
185
|
let contextMatchSum = 0;
|
|
@@ -169,8 +188,12 @@ export function findBestMatch(targetLines, fileLines, startHint = 0) {
|
|
|
169
188
|
const targetLine = targetLines[j];
|
|
170
189
|
const fileLine = fileLines[i + j];
|
|
171
190
|
|
|
172
|
-
//
|
|
173
|
-
const
|
|
191
|
+
// Use NORMALIZED lines for distance to ignore indentation differences
|
|
192
|
+
const nTarget = normTargetLines[j];
|
|
193
|
+
const nFile = normFileLines[i + j];
|
|
194
|
+
|
|
195
|
+
// Levenshtein distance on normalized text
|
|
196
|
+
const distance = levenshteinDistance(nTarget, nFile);
|
|
174
197
|
totalDistance += distance;
|
|
175
198
|
|
|
176
199
|
// Token similarity (always available)
|
|
@@ -53,6 +53,16 @@ export function validateSyntax(code, language) {
|
|
|
53
53
|
return validateRust(code);
|
|
54
54
|
case 'java':
|
|
55
55
|
return validateJava(code);
|
|
56
|
+
case 'python':
|
|
57
|
+
return validatePython(code);
|
|
58
|
+
case 'php':
|
|
59
|
+
return validatePHP(code);
|
|
60
|
+
case 'ruby':
|
|
61
|
+
return validateRuby(code);
|
|
62
|
+
case 'cpp':
|
|
63
|
+
return validateCPP(code);
|
|
64
|
+
case 'csharp':
|
|
65
|
+
return validateCSharp(code);
|
|
56
66
|
default:
|
|
57
67
|
return { valid: true, error: null };
|
|
58
68
|
}
|
|
@@ -141,54 +151,99 @@ function checkBalancedBraces(code) {
|
|
|
141
151
|
return { valid: true, error: null };
|
|
142
152
|
}
|
|
143
153
|
|
|
154
|
+
import fs from 'fs';
|
|
155
|
+
import os from 'os';
|
|
144
156
|
import { execSync } from 'child_process';
|
|
145
157
|
|
|
158
|
+
function validateWithCommand(code, ext, commandGen) {
|
|
159
|
+
const tempFile = path.join(os.tmpdir(), `mcfast_validate_${Date.now()}${ext}`);
|
|
160
|
+
try {
|
|
161
|
+
fs.writeFileSync(tempFile, code);
|
|
162
|
+
execSync(commandGen(tempFile), { stdio: 'pipe' }); // pipe to capture stderr if needed, but execSync throws on non-zero exit
|
|
163
|
+
return { valid: true, error: null };
|
|
164
|
+
} catch (e) {
|
|
165
|
+
return {
|
|
166
|
+
valid: false,
|
|
167
|
+
error: e.stderr ? e.stderr.toString() : e.message
|
|
168
|
+
};
|
|
169
|
+
} finally {
|
|
170
|
+
try {
|
|
171
|
+
if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
// ignore cleanup error
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
146
178
|
function validateGo(code) {
|
|
147
179
|
// Check if `go` is installed
|
|
148
180
|
try {
|
|
149
181
|
execSync('go version', { stdio: 'ignore' });
|
|
150
182
|
} catch {
|
|
151
|
-
//
|
|
152
|
-
if (code.includes('func main()')) {
|
|
153
|
-
|
|
154
|
-
if (!hasPackageMain) {
|
|
155
|
-
return { valid: false, error: 'Go files with main function must have package main' };
|
|
156
|
-
}
|
|
183
|
+
// Fallback to basic structure check
|
|
184
|
+
if (code.includes('func main()') && !/^\s*package\s+main\b/m.test(code)) {
|
|
185
|
+
return { valid: false, error: 'Go files with main function must have package main' };
|
|
157
186
|
}
|
|
158
187
|
return { valid: true, error: null };
|
|
159
188
|
}
|
|
189
|
+
// Use `gofmt -e` on temp file
|
|
190
|
+
return validateWithCommand(code, '.go', (f) => `gofmt -e "${f}"`);
|
|
191
|
+
}
|
|
160
192
|
|
|
193
|
+
function validateRust(code) {
|
|
194
|
+
// `rustc --parse-only`
|
|
161
195
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
// For simplicity in this context, we use a quick format check
|
|
165
|
-
// `gofmt -e` prints errors to stderr if syntax is invalid
|
|
166
|
-
const input = code.replace(/"/g, '\\"'); // Simple escape, in production need better temp file handling
|
|
167
|
-
// Real implementation should write to temp file, but for speed we try stdin piping if supported or just basic checks
|
|
168
|
-
// Better approach: Write to temp file is safer.
|
|
169
|
-
// Since we don't have easy temp file write here without fs, we fallback to our structural check for now
|
|
170
|
-
// OR we can assume safeEdit calling this might handle temp files?
|
|
171
|
-
// Let's stick to the robust check:
|
|
172
|
-
// 1. We should ideally write a temp file.
|
|
173
|
-
// But safeEdit already writes to the actual file!
|
|
174
|
-
// Wait, safeEdit writes to the actual file, then calls this validator?
|
|
175
|
-
// No, safeEdit writes, then validates. So the file exists on disk!
|
|
176
|
-
// We can just run `gofmt -e <filePath>`!
|
|
177
|
-
// BUT `validateSyntax` takes `code`, not `filePath`.
|
|
178
|
-
// We need `filePath` passed to `validateSyntax`.
|
|
179
|
-
|
|
180
|
-
// Refactoring `validateSyntax` signature in next step.
|
|
181
|
-
// For now, return valid to avoid breaking changes until signature update.
|
|
196
|
+
execSync('rustc --version', { stdio: 'ignore' });
|
|
197
|
+
} catch {
|
|
182
198
|
return { valid: true, error: null };
|
|
183
|
-
} catch (e) {
|
|
184
|
-
return { valid: false, error: e.message };
|
|
185
199
|
}
|
|
200
|
+
return validateWithCommand(code, '.rs', (f) => `rustc --parse-only "${f}"`);
|
|
186
201
|
}
|
|
187
202
|
|
|
188
|
-
function
|
|
203
|
+
function validateJava(code) {
|
|
204
|
+
// javac is heavy, stick to Tree-sitter or basic check
|
|
189
205
|
return { valid: true, error: null };
|
|
190
206
|
}
|
|
191
207
|
|
|
192
|
-
function
|
|
208
|
+
function validatePython(code) {
|
|
209
|
+
try {
|
|
210
|
+
execSync('python3 --version', { stdio: 'ignore' });
|
|
211
|
+
} catch {
|
|
212
|
+
return { valid: true, error: null };
|
|
213
|
+
}
|
|
214
|
+
return validateWithCommand(code, '.py', (f) => `python3 -m py_compile "${f}"`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function validatePHP(code) {
|
|
218
|
+
try {
|
|
219
|
+
execSync('php -v', { stdio: 'ignore' });
|
|
220
|
+
} catch {
|
|
221
|
+
return { valid: true, error: null };
|
|
222
|
+
}
|
|
223
|
+
return validateWithCommand(code, '.php', (f) => `php -l "${f}"`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function validateRuby(code) {
|
|
227
|
+
try {
|
|
228
|
+
execSync('ruby -v', { stdio: 'ignore' });
|
|
229
|
+
} catch {
|
|
230
|
+
return { valid: true, error: null };
|
|
231
|
+
}
|
|
232
|
+
return validateWithCommand(code, '.rb', (f) => `ruby -c "${f}"`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function validateCPP(code) {
|
|
236
|
+
try {
|
|
237
|
+
execSync('g++ --version', { stdio: 'ignore' });
|
|
238
|
+
} catch {
|
|
239
|
+
try { execSync('clang++ --version', { stdio: 'ignore' }); }
|
|
240
|
+
catch { return { valid: true, error: null }; }
|
|
241
|
+
return validateWithCommand(code, '.cpp', (f) => `clang++ -fsyntax-only "${f}"`);
|
|
242
|
+
}
|
|
243
|
+
return validateWithCommand(code, '.cpp', (f) => `g++ -fsyntax-only "${f}"`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function validateCSharp(code) {
|
|
247
|
+
// dotnet build is too context heavy. Rely on Tree-sitter.
|
|
193
248
|
return { valid: true, error: null };
|
|
194
249
|
}
|
|
@@ -9,7 +9,7 @@ import { detectLanguage } from '../syntax-validator.js'; // Reuse existing detec
|
|
|
9
9
|
*/
|
|
10
10
|
export async function parse(code, filePath) {
|
|
11
11
|
const language = detectLanguage(filePath);
|
|
12
|
-
if (!['go', 'rust', 'java', 'javascript', 'typescript'].includes(language)) {
|
|
12
|
+
if (!['go', 'rust', 'java', 'javascript', 'typescript', 'python', 'cpp', 'csharp', 'php', 'ruby'].includes(language)) {
|
|
13
13
|
return null;
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -31,6 +31,12 @@ const WASM_MAP = {
|
|
|
31
31
|
'java': 'tree-sitter-java.wasm',
|
|
32
32
|
'javascript': 'tree-sitter-javascript.wasm',
|
|
33
33
|
'typescript': 'tree-sitter-javascript.wasm', // TS often uses JS or its own, using JS for now as fallback/compatible
|
|
34
|
+
'python': 'tree-sitter-python.wasm',
|
|
35
|
+
'cpp': 'tree-sitter-cpp.wasm',
|
|
36
|
+
'c': 'tree-sitter-cpp.wasm', // C typically uses C++ parser or its own. Using CPP for now as it handles C.
|
|
37
|
+
'csharp': 'tree-sitter-c-sharp.wasm',
|
|
38
|
+
'php': 'tree-sitter-php.wasm',
|
|
39
|
+
'ruby': 'tree-sitter-ruby.wasm',
|
|
34
40
|
};
|
|
35
41
|
|
|
36
42
|
/**
|
|
@@ -50,5 +50,76 @@ export const QUERIES = {
|
|
|
50
50
|
organize_imports: `
|
|
51
51
|
(import_statement) @import
|
|
52
52
|
`
|
|
53
|
+
},
|
|
54
|
+
python: {
|
|
55
|
+
definitions: `
|
|
56
|
+
(function_definition name: (identifier) @name) @function
|
|
57
|
+
(class_definition name: (identifier) @name) @class
|
|
58
|
+
`,
|
|
59
|
+
references: `
|
|
60
|
+
(identifier) @ref
|
|
61
|
+
(attribute attribute: (identifier) @ref)
|
|
62
|
+
`,
|
|
63
|
+
organize_imports: `
|
|
64
|
+
(import_statement) @import
|
|
65
|
+
(import_from_statement) @import
|
|
66
|
+
`
|
|
67
|
+
},
|
|
68
|
+
cpp: {
|
|
69
|
+
definitions: `
|
|
70
|
+
(function_definition declarator: (function_declarator declarator: (identifier) @name)) @function
|
|
71
|
+
(class_specifier name: (type_identifier) @name) @class
|
|
72
|
+
`,
|
|
73
|
+
references: `
|
|
74
|
+
(identifier) @ref
|
|
75
|
+
(field_identifier) @ref
|
|
76
|
+
(type_identifier) @ref
|
|
77
|
+
`,
|
|
78
|
+
organize_imports: `
|
|
79
|
+
(preproc_include) @import
|
|
80
|
+
`
|
|
81
|
+
},
|
|
82
|
+
csharp: {
|
|
83
|
+
definitions: `
|
|
84
|
+
(method_declaration name: (identifier) @name) @method
|
|
85
|
+
(class_declaration name: (identifier) @name) @class
|
|
86
|
+
(interface_declaration name: (identifier) @name) @interface
|
|
87
|
+
`,
|
|
88
|
+
references: `
|
|
89
|
+
(identifier) @ref
|
|
90
|
+
`,
|
|
91
|
+
organize_imports: `
|
|
92
|
+
(using_directive) @import
|
|
93
|
+
`
|
|
94
|
+
},
|
|
95
|
+
php: {
|
|
96
|
+
definitions: `
|
|
97
|
+
(function_definition name: (name) @name) @function
|
|
98
|
+
(method_declaration name: (name) @name) @method
|
|
99
|
+
(class_declaration name: (name) @name) @class
|
|
100
|
+
`,
|
|
101
|
+
references: `
|
|
102
|
+
(name) @ref
|
|
103
|
+
(variable_name) @ref
|
|
104
|
+
`,
|
|
105
|
+
organize_imports: `
|
|
106
|
+
(namespace_use_declaration) @import
|
|
107
|
+
`
|
|
108
|
+
},
|
|
109
|
+
ruby: {
|
|
110
|
+
definitions: `
|
|
111
|
+
(method name: (identifier) @name) @method
|
|
112
|
+
(class name: (constant) @name) @class
|
|
113
|
+
(module name: (constant) @name) @module
|
|
114
|
+
`,
|
|
115
|
+
references: `
|
|
116
|
+
(identifier) @ref
|
|
117
|
+
(constant) @ref
|
|
118
|
+
(symbol) @ref
|
|
119
|
+
`,
|
|
120
|
+
organize_imports: `
|
|
121
|
+
(call method: (identifier) @method arguments: (argument_list (string) @import) (#eq? @method "require"))
|
|
122
|
+
(call method: (identifier) @method arguments: (argument_list (string) @import) (#eq? @method "require_relative"))
|
|
123
|
+
`
|
|
53
124
|
}
|
|
54
125
|
};
|
|
@@ -83,6 +83,30 @@ export async function renameSymbol(code, filePath, oldName, newName) {
|
|
|
83
83
|
return result;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Find definition of a symbol
|
|
88
|
+
*/
|
|
89
|
+
export async function findDefinition(code, filePath, symbolName) {
|
|
90
|
+
const language = detectLanguage(filePath);
|
|
91
|
+
if (!QUERIES[language] || !QUERIES[language].definitions) return [];
|
|
92
|
+
|
|
93
|
+
const tree = await parse(code, filePath);
|
|
94
|
+
if (!tree) return [];
|
|
95
|
+
|
|
96
|
+
const langObj = await loadLanguage(language);
|
|
97
|
+
const query = langObj.query(QUERIES[language].definitions);
|
|
98
|
+
|
|
99
|
+
const captures = query.captures(tree.rootNode);
|
|
100
|
+
return captures
|
|
101
|
+
.filter(c => c.node.text === symbolName)
|
|
102
|
+
.map(c => ({
|
|
103
|
+
line: c.node.startPosition.row + 1,
|
|
104
|
+
column: c.node.startPosition.column,
|
|
105
|
+
text: c.node.text,
|
|
106
|
+
type: c.name // 'function', 'class', etc.
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
86
110
|
/**
|
|
87
111
|
* Find all references of a symbol
|
|
88
112
|
*/
|
|
@@ -105,3 +129,154 @@ export async function findReferences(code, filePath, symbolName) {
|
|
|
105
129
|
text: c.node.text
|
|
106
130
|
}));
|
|
107
131
|
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Organize imports (sort and remove duplicates)
|
|
135
|
+
* Currently supports simple sorting of import statements
|
|
136
|
+
*/
|
|
137
|
+
export async function organizeImports(code, filePath) {
|
|
138
|
+
const language = detectLanguage(filePath);
|
|
139
|
+
if (!QUERIES[language] || !QUERIES[language].organize_imports) return code;
|
|
140
|
+
|
|
141
|
+
const tree = await parse(code, filePath);
|
|
142
|
+
if (!tree) return code;
|
|
143
|
+
|
|
144
|
+
const langObj = await loadLanguage(language);
|
|
145
|
+
const query = langObj.query(QUERIES[language].organize_imports);
|
|
146
|
+
|
|
147
|
+
const captures = query.captures(tree.rootNode);
|
|
148
|
+
if (captures.length === 0) return code;
|
|
149
|
+
|
|
150
|
+
// Get unique import lines
|
|
151
|
+
const uniqueImports = new Set();
|
|
152
|
+
const range = { start: Infinity, end: -1 };
|
|
153
|
+
|
|
154
|
+
for (const c of captures) {
|
|
155
|
+
uniqueImports.add(c.node.text);
|
|
156
|
+
range.start = Math.min(range.start, c.node.startIndex);
|
|
157
|
+
range.end = Math.max(range.end, c.node.endIndex);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Sort imports
|
|
161
|
+
const sortedImports = Array.from(uniqueImports).sort();
|
|
162
|
+
|
|
163
|
+
// Replace the entire block of imports with sorted ones
|
|
164
|
+
// Note: This assumes imports are contiguous or we are replacing the range from first to last import
|
|
165
|
+
// This handles the "block" of imports at the top
|
|
166
|
+
// A better approach would be to group them, but for "Basic" feature, contiguous block replacement is standard-ish
|
|
167
|
+
// However, if there are comments or code in between, this is risky.
|
|
168
|
+
// Safer: Replace each node? No, that doesn't sort.
|
|
169
|
+
// Safe-ish: Find the continuous block of imports at the top.
|
|
170
|
+
|
|
171
|
+
// Check if imports are contiguous (ignoring whitespace/comments)
|
|
172
|
+
// We'll just construct the new import block and replace the range from first import start to last import end
|
|
173
|
+
// But we need to keep the whitespace/newlines between them?
|
|
174
|
+
// "Organize Imports" usually means re-writing the block.
|
|
175
|
+
|
|
176
|
+
// Simplification: Join with newlines
|
|
177
|
+
const newImportBlock = sortedImports.join('\n');
|
|
178
|
+
|
|
179
|
+
// Apply replacement
|
|
180
|
+
return code.substring(0, range.start) + newImportBlock + code.substring(range.end);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Extract function helper - finds the smallest node covering the range
|
|
185
|
+
*/
|
|
186
|
+
export async function getExtractSelection(code, filePath, startLine, endLine) {
|
|
187
|
+
const tree = await parse(code, filePath);
|
|
188
|
+
if (!tree) return null;
|
|
189
|
+
|
|
190
|
+
const root = tree.rootNode;
|
|
191
|
+
// Find node that covers the range
|
|
192
|
+
const startNode = root.descendantForPosition({ row: startLine - 1, column: 0 });
|
|
193
|
+
const endNode = root.descendantForPosition({ row: endLine - 1, column: 1000 });
|
|
194
|
+
|
|
195
|
+
// Find common ancestor
|
|
196
|
+
let ancestor = startNode;
|
|
197
|
+
while (ancestor) {
|
|
198
|
+
if (ancestor.startIndex <= startNode.startIndex && ancestor.endIndex >= endNode.endIndex) {
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
ancestor = ancestor.parent;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!ancestor) return null;
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
text: ancestor.text,
|
|
208
|
+
startLine: ancestor.startPosition.row + 1,
|
|
209
|
+
endLine: ancestor.endPosition.row + 1
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Extract code from range into a new function
|
|
214
|
+
*/
|
|
215
|
+
export async function extractFunction(code, filePath, startLine, endLine, functionName) {
|
|
216
|
+
const selection = await getExtractSelection(code, filePath, startLine, endLine);
|
|
217
|
+
if (!selection) throw new Error('Could not find a valid code block to extract');
|
|
218
|
+
|
|
219
|
+
const tree = await parse(code, filePath);
|
|
220
|
+
const ancestor = tree.rootNode.descendantForIndex(code.indexOf(selection.text));
|
|
221
|
+
|
|
222
|
+
// Find the body of the current function/context to insert the new function after it
|
|
223
|
+
// For simplicity, we'll append to the end of the file or insert after the current top-level parent
|
|
224
|
+
let insertionNode = ancestor;
|
|
225
|
+
while (insertionNode.parent && insertionNode.parent.type !== 'program' && insertionNode.parent.type !== 'source_file') {
|
|
226
|
+
insertionNode = insertionNode.parent;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const language = detectLanguage(filePath);
|
|
230
|
+
let newFuncTemplate = '';
|
|
231
|
+
let callTemplate = '';
|
|
232
|
+
|
|
233
|
+
if (['javascript', 'typescript'].includes(language)) {
|
|
234
|
+
newFuncTemplate = `\n\nfunction ${functionName}() {\n ${selection.text.split('\n').join('\n ')}\n}\n`;
|
|
235
|
+
callTemplate = `${functionName}();`;
|
|
236
|
+
} else if (language === 'python') {
|
|
237
|
+
newFuncTemplate = `\n\ndef ${functionName}():\n ${selection.text.split('\n').join('\n ')}\n`;
|
|
238
|
+
callTemplate = `${functionName}()`;
|
|
239
|
+
} else if (language === 'go') {
|
|
240
|
+
newFuncTemplate = `\n\nfunc ${functionName}() {\n ${selection.text.split('\n').join('\n ')}\n}\n`;
|
|
241
|
+
callTemplate = `${functionName}()`;
|
|
242
|
+
} else {
|
|
243
|
+
// Fallback generic
|
|
244
|
+
newFuncTemplate = `\n\nfunction ${functionName}() {\n ${selection.text}\n}\n`;
|
|
245
|
+
callTemplate = `${functionName}();`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Replace original text with call
|
|
249
|
+
let result = code.substring(0, ancestor.startIndex) + callTemplate + code.substring(ancestor.endIndex);
|
|
250
|
+
|
|
251
|
+
// Insert new function after the top-level block
|
|
252
|
+
const adjustedInsertionIndex = ancestor.startIndex < insertionNode.endIndex
|
|
253
|
+
? insertionNode.endIndex + (callTemplate.length - ancestor.text.length)
|
|
254
|
+
: insertionNode.endIndex;
|
|
255
|
+
|
|
256
|
+
result = result.substring(0, adjustedInsertionIndex) + newFuncTemplate + result.substring(adjustedInsertionIndex);
|
|
257
|
+
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Move code from one range to another location
|
|
263
|
+
*/
|
|
264
|
+
export async function moveCode(code, filePath, startLine, endLine, targetLine) {
|
|
265
|
+
const selection = await getExtractSelection(code, filePath, startLine, endLine);
|
|
266
|
+
if (!selection) throw new Error('Could not find code to move');
|
|
267
|
+
|
|
268
|
+
const tree = await parse(code, filePath);
|
|
269
|
+
const ancestor = tree.rootNode.descendantForIndex(code.indexOf(selection.text));
|
|
270
|
+
|
|
271
|
+
// Remove original
|
|
272
|
+
let result = code.substring(0, ancestor.startIndex) + code.substring(ancestor.endIndex);
|
|
273
|
+
|
|
274
|
+
// Find target index
|
|
275
|
+
const lines = result.split('\n');
|
|
276
|
+
const targetIdx = targetLine > lines.length ? result.length : lines.slice(0, targetLine - 1).join('\n').length + 1;
|
|
277
|
+
|
|
278
|
+
// Insert at target
|
|
279
|
+
result = result.substring(0, targetIdx) + '\n' + selection.text + '\n' + result.substring(targetIdx);
|
|
280
|
+
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|