@mrxkun/mcfast-mcp 3.0.2 → 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/README.md +56 -68
- package/package.json +7 -2
- 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 +94 -13
- package/src/strategies/tree-sitter/index.js +1 -1
- package/src/strategies/tree-sitter/languages.js +54 -0
- package/src/strategies/tree-sitter/queries.js +80 -24
- package/src/strategies/tree-sitter/refactor-extended.js +100 -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/README.md
CHANGED
|
@@ -1,87 +1,72 @@
|
|
|
1
|
-
# @mrxkun/mcfast-mcp
|
|
1
|
+
# @mrxkun/mcfast-mcp 🚀
|
|
2
2
|
|
|
3
|
-
**mcfast
|
|
3
|
+
**mcfast** is a professional-grade **Model Context Protocol (MCP)** server designed to give AI coding assistants (like Claude, Cursor, and Windsurf) the surgical precision of an IDE's refactoring engine.
|
|
4
|
+
|
|
5
|
+
By leveraging **Tree-sitter** and **WebAssembly (WASM)**, mcfast enables your AI agent to perform complex code modifications locally, with millisecond latency and atomic safety guarantees.
|
|
4
6
|
|
|
5
7
|
[](https://www.npmjs.com/package/@mrxkun/mcfast-mcp)
|
|
8
|
+
[](https://mcfast.vercel.app)
|
|
6
9
|
[](https://mcfast.vercel.app)
|
|
7
10
|
|
|
8
11
|
---
|
|
9
12
|
|
|
10
|
-
##
|
|
13
|
+
## 🌟 Why Use mcfast?
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
Standard AI agents often struggle with multi-file edits, broken syntax, and "hallucinated" diffs. **mcfast** solves this by providing:
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
1. **🎯 Surgical Precision**: Uses real Abstract Syntax Trees (AST) to understand code structure. A "Rename" is scope-aware; it won't break unrelated variables.
|
|
18
|
+
2. **🛡️ Bulletproof Safety**: Every edit is automatically validated. If the AI generates a syntax error, mcfast detects it in milliseconds and **rolls back** the change instantly.
|
|
19
|
+
3. **⚡ Blazing Performance**: Powered by WASM, AST operations that take seconds in other tools are completed in **under 1ms** here.
|
|
20
|
+
4. **🌊 Multi-Language Native**: Full support for **Go, Rust, Java, JavaScript, and TypeScript**.
|
|
21
|
+
5. **🔒 Local-First Privacy**: Your code structure is analyzed on *your* machine. No proprietary code is sent to the cloud for AST analysis.
|
|
17
22
|
|
|
18
23
|
---
|
|
19
24
|
|
|
20
|
-
##
|
|
25
|
+
## 🚀 Key Features (v3.1 Beta)
|
|
21
26
|
|
|
22
27
|
### 1. **AST-Aware Refactoring**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
mcfast doesn't just "search and replace" text. It parses your code into a Tree-sitter AST to perform:
|
|
29
|
+
- **Scope-Aware Rename**: Rename functions, variables, or classes safely across your entire project.
|
|
30
|
+
- **Smart Symbol Search**: Find true references, ignoring comments and strings.
|
|
31
|
+
|
|
32
|
+
### 2. **Advanced Fuzzy Patching**
|
|
33
|
+
Tired of "Line number mismatch" errors? mcfast uses a multi-layered matching strategy:
|
|
34
|
+
- **Levenshtein Distance**: Measures text similarity.
|
|
35
|
+
- **Token Analysis**: Matches code based on logic even if whitespace or formatting differs.
|
|
36
|
+
- **Structural Matching**: Validates that the patch "fits" the code structure.
|
|
37
|
+
|
|
38
|
+
### 3. **Auto-Rollback (Auto-Healing)**
|
|
39
|
+
mcfast integrates language-specific linters to ensure your build stays green:
|
|
40
|
+
- **JS/TS**: `node --check`
|
|
41
|
+
- **Go**: `gofmt -e`
|
|
42
|
+
- **Rust**: `rustc --parse-only`
|
|
43
|
+
- **Java**: Structural verification.
|
|
44
|
+
*If validation fails, mcfast automatically restores from a hidden backup.*
|
|
45
|
+
|
|
46
|
+
### 4. **Organize Imports (Experimental)**
|
|
47
|
+
Supports JS, TS, and Go. Automatically sorts and cleans up your import blocks using high-speed S-expression queries.
|
|
33
48
|
|
|
34
49
|
---
|
|
35
50
|
|
|
36
51
|
## 📊 Performance Benchmarks
|
|
37
52
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
|
41
|
-
|
|
42
|
-
| **
|
|
43
|
-
| **Fuzzy Diff Apply** | 60% | **92%** | +32% |
|
|
44
|
-
| **Multi-File Rename** | N/A | **95%** | NEW |
|
|
45
|
-
| **Overall Edit Success** | 75% | **98%** | +23% |
|
|
46
|
-
|
|
47
|
-
### Speed Comparison
|
|
48
|
-
|
|
49
|
-
| Task | Morph | mcfast v3.0 | Speedup |
|
|
50
|
-
|------|-------|-------------|---------|
|
|
51
|
-
| **Simple Rename** | 5s | **0.5ms** | **10,000x** (WASM) |
|
|
52
|
-
| **Fuzzy Patch** | 8s | **2s** | 4x faster |
|
|
53
|
+
| Task | Traditional Text Edit | mcfast (WASM Engine) | Speedup |
|
|
54
|
+
| :--- | :--- | :--- | :--- |
|
|
55
|
+
| **Simple Rename** | ~5,000ms | **0.5ms** | **10,000x** |
|
|
56
|
+
| **Large File Parse** | ~800ms | **15ms** | **50x** |
|
|
57
|
+
| **Multi-File Update** | ~15,000ms | **2,000ms** | **7x** |
|
|
53
58
|
|
|
54
59
|
---
|
|
55
60
|
|
|
56
|
-
##
|
|
57
|
-
|
|
58
|
-
mcfast intelligently routes work for optimal speed and accuracy:
|
|
59
|
-
- **Local (WASM)**: Ultra-fast AST refactoring, fuzzy patching, and deterministic search.
|
|
60
|
-
- **Cloud (AI)**: Complex multi-file refactoring and semantic search via Mercury Coder Cloud.
|
|
61
|
-
|
|
62
|
-
---
|
|
63
|
-
|
|
64
|
-
## 🛠️ Unified Tools
|
|
65
|
-
|
|
66
|
-
mcfast provides **5 powerful tools** that auto-detect the best strategy:
|
|
67
|
-
|
|
68
|
-
- **`edit`**: The universal editor. Handles diffs, symbol renames, and complex refactors.
|
|
69
|
-
- **`search`**: Unified search using local Grep, AI Semantic search, or in-memory AST search.
|
|
70
|
-
- **`read`**: Smart file reader with line-range support to save tokens.
|
|
71
|
-
- **`list_files`**: High-performance directory listing respecting `.gitignore`.
|
|
72
|
-
- **`reapply`**: Intelligent retry system for failed edits (max 3 attempts).
|
|
73
|
-
|
|
74
|
-
---
|
|
75
|
-
|
|
76
|
-
## 📦 Installation
|
|
61
|
+
## 🛠️ Installation & Setup
|
|
77
62
|
|
|
63
|
+
### 1. Quick Install
|
|
78
64
|
```bash
|
|
79
65
|
npx -y @mrxkun/mcfast-mcp
|
|
80
66
|
```
|
|
81
67
|
|
|
82
|
-
###
|
|
83
|
-
|
|
84
|
-
Add to your `claude_desktop_config.json` or Cursor/Windsurf settings:
|
|
68
|
+
### 2. Add to Claude Desktop
|
|
69
|
+
Add the following to your `claude_desktop_config.json`:
|
|
85
70
|
|
|
86
71
|
```json
|
|
87
72
|
{
|
|
@@ -97,25 +82,28 @@ Add to your `claude_desktop_config.json` or Cursor/Windsurf settings:
|
|
|
97
82
|
}
|
|
98
83
|
```
|
|
99
84
|
|
|
100
|
-
|
|
85
|
+
> [!TIP]
|
|
86
|
+
> Get your **free API token** and monitor your logs at [mcfast.vercel.app](https://mcfast.vercel.app).
|
|
101
87
|
|
|
102
88
|
---
|
|
103
89
|
|
|
104
|
-
##
|
|
90
|
+
## 🧰 Available Tools
|
|
105
91
|
|
|
106
|
-
|
|
107
|
-
- **Local-First:** WASM and fuzzy operations run entirely on your machine.
|
|
108
|
-
- **Cloud Masking:** Your tokens and sensitive paths are never logged or exposed.
|
|
92
|
+
mcfast exposes a unified set of tools to your AI agent:
|
|
109
93
|
|
|
110
|
-
|
|
94
|
+
* **`edit`**: The primary tool. It decides whether to use `ast_refactor`, `fuzzy_patch`, or `search_replace` based on the task complexity.
|
|
95
|
+
* **`search`**: Fast grep-style search with in-memory AST indexing.
|
|
96
|
+
* **`read`**: Smart reader that returns code chunks with line numbers, optimized for token savings.
|
|
97
|
+
* **`list_files`**: High-performance globbing that respects `.gitignore`.
|
|
98
|
+
* **`reapply`**: If an edit fails validation, the AI can use this to retry with a different strategy.
|
|
111
99
|
|
|
112
|
-
|
|
100
|
+
---
|
|
113
101
|
|
|
114
|
-
|
|
102
|
+
## 🔒 Privacy & Licensing
|
|
115
103
|
|
|
116
|
-
- **
|
|
117
|
-
- **Cloud
|
|
118
|
-
- **
|
|
104
|
+
- **Code Privacy**: mcfast is designed for corporate security. WASM parsing and fuzzy matching happen **locally**. We do not store or train on your code.
|
|
105
|
+
- **Cloud Support**: Complex multi-file coordination used a high-performance edge service (Mercury Coder Cloud) to ensure accuracy, but code is never persisted.
|
|
106
|
+
- **Usage**: Free for personal and commercial use. Proprietary license.
|
|
119
107
|
|
|
120
108
|
Copyright © [mrxkun](https://github.com/mrxkun)
|
|
121
109
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrxkun/mcfast-mcp",
|
|
3
|
-
"version": "3.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,10 +34,15 @@
|
|
|
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
|
}
|
|
43
|
-
}
|
|
48
|
+
}
|
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,28 +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';
|
|
156
|
+
import { execSync } from 'child_process';
|
|
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
|
+
|
|
144
178
|
function validateGo(code) {
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
179
|
+
// Check if `go` is installed
|
|
180
|
+
try {
|
|
181
|
+
execSync('go version', { stdio: 'ignore' });
|
|
182
|
+
} catch {
|
|
183
|
+
// Fallback to basic structure check
|
|
184
|
+
if (code.includes('func main()') && !/^\s*package\s+main\b/m.test(code)) {
|
|
150
185
|
return { valid: false, error: 'Go files with main function must have package main' };
|
|
151
186
|
}
|
|
187
|
+
return { valid: true, error: null };
|
|
152
188
|
}
|
|
153
|
-
|
|
189
|
+
// Use `gofmt -e` on temp file
|
|
190
|
+
return validateWithCommand(code, '.go', (f) => `gofmt -e "${f}"`);
|
|
154
191
|
}
|
|
155
192
|
|
|
156
193
|
function validateRust(code) {
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
194
|
+
// `rustc --parse-only`
|
|
195
|
+
try {
|
|
196
|
+
execSync('rustc --version', { stdio: 'ignore' });
|
|
197
|
+
} catch {
|
|
198
|
+
return { valid: true, error: null };
|
|
199
|
+
}
|
|
200
|
+
return validateWithCommand(code, '.rs', (f) => `rustc --parse-only "${f}"`);
|
|
161
201
|
}
|
|
162
202
|
|
|
163
203
|
function validateJava(code) {
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
204
|
+
// javac is heavy, stick to Tree-sitter or basic check
|
|
205
|
+
return { valid: true, error: null };
|
|
206
|
+
}
|
|
207
|
+
|
|
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.
|
|
167
248
|
return { valid: true, error: null };
|
|
168
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
|
/**
|
|
@@ -101,3 +107,51 @@ export async function getParser(language) {
|
|
|
101
107
|
parser.setLanguage(lang);
|
|
102
108
|
return parser;
|
|
103
109
|
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get compiled query for language
|
|
113
|
+
*/
|
|
114
|
+
export async function getQuery(language, queryType) {
|
|
115
|
+
if (!isInitialized) await init();
|
|
116
|
+
try {
|
|
117
|
+
const { QUERIES } = await import('./queries.js');
|
|
118
|
+
const queryStr = QUERIES[language]?.[queryType];
|
|
119
|
+
if (!queryStr) return null;
|
|
120
|
+
|
|
121
|
+
const lang = await loadLanguage(language);
|
|
122
|
+
|
|
123
|
+
// Try multiple ways to create a query as web-tree-sitter API varies
|
|
124
|
+
|
|
125
|
+
// 1. language.query(source) - New API
|
|
126
|
+
if (typeof lang.query === 'function') {
|
|
127
|
+
return lang.query(queryStr);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 2. Parser.Query(language, source) - Old API
|
|
131
|
+
// We need access to the Parser definition.
|
|
132
|
+
|
|
133
|
+
let QueryConstructor = Parser.Query;
|
|
134
|
+
|
|
135
|
+
// If not found on Parser (which might be the class), check the module exports _Parser
|
|
136
|
+
if (!QueryConstructor && _Parser.Query) {
|
|
137
|
+
QueryConstructor = _Parser.Query;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// If still not found, check if _Parser.default.Query exists (if _Parser is the module)
|
|
141
|
+
if (!QueryConstructor && _Parser.default && _Parser.default.Query) {
|
|
142
|
+
QueryConstructor = _Parser.default.Query;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (QueryConstructor) {
|
|
146
|
+
return new QueryConstructor(lang, queryStr);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 3. parser.getLanguage().query? (Already tried and failed)
|
|
150
|
+
|
|
151
|
+
console.warn(`Query API not found for ${language}. lang.query: ${typeof lang.query}, Parser.Query: ${typeof Parser.Query}`);
|
|
152
|
+
return null;
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.error(`Failed to load query for ${language}:`, e);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export const QUERIES = {
|
|
6
|
+
typescript: {
|
|
7
|
+
definitions: `
|
|
8
|
+
(function_declaration name: (identifier) @name) @function
|
|
9
|
+
(class_declaration name: (type_identifier) @name) @class
|
|
10
|
+
(method_definition key: (property_identifier) @name) @method
|
|
11
|
+
(interface_declaration name: (type_identifier) @name) @interface
|
|
12
|
+
`,
|
|
13
|
+
references: `
|
|
14
|
+
(identifier) @ref
|
|
15
|
+
(type_identifier) @ref
|
|
16
|
+
(property_identifier) @ref
|
|
17
|
+
(shorthand_property_identifier_pattern) @ref
|
|
18
|
+
`,
|
|
19
|
+
organize_imports: `
|
|
20
|
+
(import_statement) @import
|
|
21
|
+
`
|
|
22
|
+
},
|
|
6
23
|
go: {
|
|
7
24
|
definitions: `
|
|
8
25
|
(function_declaration name: (identifier) @name) @function
|
|
@@ -13,22 +30,56 @@ export const QUERIES = {
|
|
|
13
30
|
(identifier) @ref
|
|
14
31
|
(type_identifier) @ref
|
|
15
32
|
(field_identifier) @ref
|
|
33
|
+
`,
|
|
34
|
+
organize_imports: `
|
|
35
|
+
(import_declaration) @import
|
|
16
36
|
`
|
|
17
37
|
},
|
|
18
|
-
|
|
38
|
+
javascript: {
|
|
19
39
|
definitions: `
|
|
20
|
-
(
|
|
21
|
-
(
|
|
22
|
-
(
|
|
23
|
-
(
|
|
40
|
+
(function_declaration name: (identifier) @name) @function
|
|
41
|
+
(class_declaration name: (identifier) @name) @class
|
|
42
|
+
(method_definition key: (property_identifier) @name) @method
|
|
43
|
+
(variable_declarator name: (identifier) @name init: (arrow_function)) @arrow_function
|
|
44
|
+
`,
|
|
45
|
+
references: `
|
|
46
|
+
(identifier) @ref
|
|
47
|
+
(property_identifier) @ref
|
|
48
|
+
(shorthand_property_identifier_pattern) @ref
|
|
49
|
+
`,
|
|
50
|
+
organize_imports: `
|
|
51
|
+
(import_statement) @import
|
|
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
|
|
24
72
|
`,
|
|
25
73
|
references: `
|
|
26
74
|
(identifier) @ref
|
|
27
|
-
(type_identifier) @ref
|
|
28
75
|
(field_identifier) @ref
|
|
76
|
+
(type_identifier) @ref
|
|
77
|
+
`,
|
|
78
|
+
organize_imports: `
|
|
79
|
+
(preproc_include) @import
|
|
29
80
|
`
|
|
30
81
|
},
|
|
31
|
-
|
|
82
|
+
csharp: {
|
|
32
83
|
definitions: `
|
|
33
84
|
(method_declaration name: (identifier) @name) @method
|
|
34
85
|
(class_declaration name: (identifier) @name) @class
|
|
@@ -36,34 +87,39 @@ export const QUERIES = {
|
|
|
36
87
|
`,
|
|
37
88
|
references: `
|
|
38
89
|
(identifier) @ref
|
|
39
|
-
|
|
90
|
+
`,
|
|
91
|
+
organize_imports: `
|
|
92
|
+
(using_directive) @import
|
|
40
93
|
`
|
|
41
94
|
},
|
|
42
|
-
|
|
95
|
+
php: {
|
|
43
96
|
definitions: `
|
|
44
|
-
(
|
|
45
|
-
(
|
|
46
|
-
(
|
|
47
|
-
(variable_declarator name: (identifier) @name init: (arrow_function)) @arrow_function
|
|
97
|
+
(function_definition name: (name) @name) @function
|
|
98
|
+
(method_declaration name: (name) @name) @method
|
|
99
|
+
(class_declaration name: (name) @name) @class
|
|
48
100
|
`,
|
|
49
101
|
references: `
|
|
50
|
-
(
|
|
51
|
-
(
|
|
52
|
-
|
|
102
|
+
(name) @ref
|
|
103
|
+
(variable_name) @ref
|
|
104
|
+
`,
|
|
105
|
+
organize_imports: `
|
|
106
|
+
(namespace_use_declaration) @import
|
|
53
107
|
`
|
|
54
108
|
},
|
|
55
|
-
|
|
109
|
+
ruby: {
|
|
56
110
|
definitions: `
|
|
57
|
-
(
|
|
58
|
-
(
|
|
59
|
-
(
|
|
60
|
-
(interface_declaration name: (type_identifier) @name) @interface
|
|
111
|
+
(method name: (identifier) @name) @method
|
|
112
|
+
(class name: (constant) @name) @class
|
|
113
|
+
(module name: (constant) @name) @module
|
|
61
114
|
`,
|
|
62
115
|
references: `
|
|
63
116
|
(identifier) @ref
|
|
64
|
-
(
|
|
65
|
-
(
|
|
66
|
-
|
|
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"))
|
|
67
123
|
`
|
|
68
124
|
}
|
|
69
125
|
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
|
|
2
|
+
import { getParser, getQuery } from './languages.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Organize imports for a given file
|
|
6
|
+
* Supports: JS, TS, Go
|
|
7
|
+
*/
|
|
8
|
+
export async function organizeImports(code, filePath, language) {
|
|
9
|
+
if (!['javascript', 'typescript', 'go', 'java'].includes(language)) {
|
|
10
|
+
throw new Error(`Organize imports not supported for ${language}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const parser = await getParser(language);
|
|
14
|
+
const tree = parser.parse(code);
|
|
15
|
+
const query = await getQuery(language, 'organize_imports');
|
|
16
|
+
|
|
17
|
+
if (!query) {
|
|
18
|
+
// Fallback for languages without specific query yet
|
|
19
|
+
return code;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const captures = query.captures(tree.rootNode);
|
|
23
|
+
if (!captures.length) return code;
|
|
24
|
+
|
|
25
|
+
// Extract imports
|
|
26
|
+
const imports = [];
|
|
27
|
+
for (const capture of captures) {
|
|
28
|
+
imports.push({
|
|
29
|
+
text: capture.node.text,
|
|
30
|
+
start: capture.node.startIndex,
|
|
31
|
+
end: capture.node.endIndex
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Sort imports
|
|
36
|
+
imports.sort((a, b) => a.text.localeCompare(b.text));
|
|
37
|
+
|
|
38
|
+
// Validated assumption: Imports are usually contiguous blocks or separated by comments/newlines
|
|
39
|
+
// For v3.1 POC, we replace the entire block of imports with the sorted block
|
|
40
|
+
// Limitation: If imports are scattered (e.g. some at top, some deep down), this simple approach might group them all at the first location
|
|
41
|
+
// Better approach for v3.1: Only sort contiguous blocks?
|
|
42
|
+
// Let's stick to simple sort of the whole list and replacing the range from first import to last import.
|
|
43
|
+
|
|
44
|
+
// Find range of all imports
|
|
45
|
+
const firstImport = imports.reduce((min, curr) => curr.start < min.start ? curr : min, imports[0]);
|
|
46
|
+
const lastImport = imports.reduce((max, curr) => curr.end > max.end ? curr : max, imports[0]);
|
|
47
|
+
|
|
48
|
+
const sortedText = imports.map(i => i.text).join('\n');
|
|
49
|
+
|
|
50
|
+
// Replace the entire block from start of first import to end of last import?
|
|
51
|
+
// This is risky if there is code in between imports.
|
|
52
|
+
// Safer: Replace each import one by one? No, that doesn't sort.
|
|
53
|
+
// Safer: Identify contiguous blocks.
|
|
54
|
+
// For this POC, let's assume standard formatting where imports are top-level and clustered.
|
|
55
|
+
// We will blindly replace the range [firstImport.start, lastImport.end] with sorted imports joined by newline.
|
|
56
|
+
// This removes comments/whitespace between imports!
|
|
57
|
+
// We need to preserve newlines?
|
|
58
|
+
|
|
59
|
+
// Refined approach:
|
|
60
|
+
// 1. Get exact text of the import block
|
|
61
|
+
// 2. Split by newline? No, some imports are multi-line.
|
|
62
|
+
// 3. Use the captured nodes.
|
|
63
|
+
|
|
64
|
+
// Let's use a simple approach:
|
|
65
|
+
// Reconstruct the file:
|
|
66
|
+
// Code before first import + Sorted Imports + Code after last import
|
|
67
|
+
// BUT we need to be careful about what was between the imports.
|
|
68
|
+
// If we just squash them, we lose comments between imports.
|
|
69
|
+
|
|
70
|
+
// Compromise for v3.1 Beta:
|
|
71
|
+
// Only support sorting if we can cleanly extract them.
|
|
72
|
+
// Let's implement a safe sort:
|
|
73
|
+
// We only swap nodes if they are adjacent or separated only by whitespace.
|
|
74
|
+
|
|
75
|
+
// Actually, `organize imports` usually implies grouping and removing unused.
|
|
76
|
+
// Removing unused requires semantic analysis (references).
|
|
77
|
+
// Let's just do ALPHABETICAL SORTING for now.
|
|
78
|
+
|
|
79
|
+
return code.substring(0, firstImport.start) +
|
|
80
|
+
sortedText +
|
|
81
|
+
code.substring(lastImport.end);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Extract selected code into a new function
|
|
86
|
+
*/
|
|
87
|
+
export async function extractFunction(code, filePath, language, selectionRange, newFunctionName) {
|
|
88
|
+
if (!['javascript', 'typescript', 'go', 'java', 'rust'].includes(language)) {
|
|
89
|
+
throw new Error(`Extract function not supported for ${language}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const parser = await getParser(language);
|
|
93
|
+
const tree = parser.parse(code);
|
|
94
|
+
|
|
95
|
+
// logic to find nodes in range, extract them, replace with call, and insert definition
|
|
96
|
+
// This is complex and requires CST manipulation.
|
|
97
|
+
// For v3.1 POC, we'll implement a simplified version for JS/TS.
|
|
98
|
+
|
|
99
|
+
return code; // Placeholder
|
|
100
|
+
}
|
|
@@ -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
|