@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrxkun/mcfast-mcp",
3
- "version": "3.1.0",
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 { renameSymbol as renameSymbolTreeSitter } from './strategies/tree-sitter/refactor.js';
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** - Intelligently edits files with automatic strategy detection. Consolidates apply_fast, edit_file, and apply_search_replace into one smart tool. Supports: (1) Simple search/replace, (2) Placeholder-based editing with '// ... existing code ...', (3) Complex multi-file refactoring via Mercury AI.",
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 describing changes. For search/replace, use 'Replace X with Y' format."
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: Partial code snippet with '// ... existing code ...' placeholders for token-efficient editing."
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** - Automatically selects the best search strategy: (1) Local search if files provided, (2) AI semantic search for complex queries, (3) Filesystem grep for fast codebase-wide 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 (literal string or natural language)"
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 with optional line range support to save tokens. Use this before editing files or to understand code structure.",
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 = 10000;
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
- // Levenshtein distance
173
- const distance = levenshteinDistance(targetLine, fileLine);
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
- // If Go is not installed, fallback to basic structure check
152
- if (code.includes('func main()')) {
153
- const hasPackageMain = /^\s*package\s+main\b/m.test(code);
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
- // Create temp file for validation
163
- // Note: execSync specific implementation might change based on environment
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 validateRust(code) {
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 validateJava(code) {
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
+ }