|
6 | 6 |
|
7 | 7 | import path from 'node:path'; |
8 | 8 |
|
9 | | -import {fileGetSymbols, fileReadContent} from '../../client-pipe.js'; |
| 9 | +import {fileExtractStructure, fileReadContent} from '../../client-pipe.js'; |
10 | 10 | import {getHostWorkspace} from '../../config.js'; |
11 | 11 | import {zod} from '../../third_party/index.js'; |
12 | 12 | import {ToolCategory} from '../categories.js'; |
13 | 13 | import {defineTool} from '../ToolDefinition.js'; |
14 | 14 | import {executeEditWithSafetyLayer} from './safety-layer.js'; |
15 | 15 | import {resolveSymbolTarget} from './symbol-resolver.js'; |
16 | 16 |
|
| 17 | +const STRUCTURED_EXTS = new Set([ |
| 18 | + 'ts', 'tsx', 'js', 'jsx', 'mts', 'mjs', 'cts', 'cjs', |
| 19 | +]); |
| 20 | + |
17 | 21 | function resolveFilePath(file: string): string { |
18 | 22 | if (path.isAbsolute(file)) return file; |
19 | 23 | return path.resolve(getHostWorkspace(), file); |
@@ -58,7 +62,7 @@ export const edit = defineTool({ |
58 | 62 | ), |
59 | 63 | target: zod.string().optional().describe( |
60 | 64 | 'Symbol name to scope the edit: "UserService.findById". ' + |
61 | | - 'Uses VS Code DocumentSymbols for precise targeting.', |
| 65 | + 'Only supported for TS/JS family files (.ts, .tsx, .js, .jsx, .mts, .mjs, .cts, .cjs).', |
62 | 66 | ), |
63 | 67 | startLine: zod.number().int().optional().describe( |
64 | 68 | 'Fallback: start line (1-indexed). Used when target is not specified.', |
@@ -119,21 +123,32 @@ export const edit = defineTool({ |
119 | 123 | let targetLabel: string | undefined; |
120 | 124 |
|
121 | 125 | if (params.target) { |
122 | | - // Symbol targeting: resolve via DocumentSymbols |
123 | | - const symbolsResult = await fileGetSymbols(filePath); |
124 | | - const match = resolveSymbolTarget(symbolsResult.symbols, params.target); |
| 126 | + // Symbol targeting: resolve via ts-morph extraction (TS/JS family only) |
| 127 | + const ext = path.extname(filePath).slice(1).toLowerCase(); |
| 128 | + if (!STRUCTURED_EXTS.has(ext)) { |
| 129 | + response.appendResponseLine( |
| 130 | + `❌ Symbol targeting is only supported for TS/JS family files ` + |
| 131 | + `(.ts, .tsx, .js, .jsx, .mts, .mjs, .cts, .cjs).\n\n` + |
| 132 | + `Use \`startLine\`/\`endLine\` instead for .${ext} files.`, |
| 133 | + ); |
| 134 | + return; |
| 135 | + } |
| 136 | + |
| 137 | + const structure = await fileExtractStructure(filePath); |
| 138 | + const match = resolveSymbolTarget(structure.symbols, params.target); |
125 | 139 |
|
126 | 140 | if (!match) { |
127 | | - const available = symbolsResult.symbols.map(s => `${s.kind} ${s.name}`).join(', '); |
| 141 | + const available = structure.symbols.map(s => `${s.kind} ${s.name}`).join(', '); |
128 | 142 | response.appendResponseLine( |
129 | 143 | `❌ Symbol "${params.target}" not found in ${relativePath}.\n\n` + |
130 | 144 | `Available symbols: ${available || 'none'}`, |
131 | 145 | ); |
132 | 146 | return; |
133 | 147 | } |
134 | 148 |
|
135 | | - editStartLine = match.symbol.range.startLine; |
136 | | - editEndLine = match.symbol.range.endLine; |
| 149 | + // ts-morph ranges are 1-indexed; safety layer expects 0-indexed |
| 150 | + editStartLine = match.symbol.range.startLine - 1; |
| 151 | + editEndLine = match.symbol.range.endLine - 1; |
137 | 152 | targetLabel = params.target; |
138 | 153 | } else if (params.startLine !== undefined && params.endLine !== undefined) { |
139 | 154 | // Line-based targeting (convert 1-indexed to 0-indexed) |
|
0 commit comments