Skip to content

Commit 550a096

Browse files
committed
feat(file-tools): add file read and edit tools with safety layer
- Implemented `file_read` tool for reading file content with semantic symbol targeting. - Implemented `file_edit` tool for direct model-to-code editing with an intelligent safety layer. - Added safety layer to handle edits, including intent detection, rename propagation, and auto-fixing errors. - Introduced symbol resolution utilities for better targeting of symbols in files. - Created types for detected intents, propagated changes, auto-fixes, and remaining errors. - Updated tools index to include new file tools.
1 parent 131af1e commit 550a096

12 files changed

Lines changed: 2059 additions & 0 deletions

File tree

scripts/test-fold-real.mts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Quick test: call folding ranges on a real TS file with imports
2+
import { dragraceGetFoldingRanges, dragraceGetDocumentSymbols } from '../src/client-pipe.js';
3+
import path from 'node:path';
4+
5+
const targetFile = path.resolve(
6+
import.meta.dirname,
7+
'../../extension/services/codebase/parsers.ts'
8+
);
9+
10+
console.log(`\nTarget: ${targetFile}\n`);
11+
12+
// Get folding ranges
13+
const foldResult = await dragraceGetFoldingRanges(targetFile);
14+
console.log(`Total folding ranges: ${foldResult.ranges.length}`);
15+
16+
// Show ranges with kind
17+
const withKind = foldResult.ranges.filter(r => r.kind);
18+
console.log(`Ranges with kind: ${withKind.length}`);
19+
for (const r of withKind) {
20+
console.log(` Lines ${r.start}-${r.end} kind="${r.kind}"`);
21+
}
22+
23+
// Show first 10 ranges
24+
console.log(`\nFirst 10 ranges:`);
25+
for (const r of foldResult.ranges.slice(0, 10)) {
26+
console.log(` Lines ${r.start}-${r.end}${r.kind ? ` kind="${r.kind}"` : ''}`);
27+
}
28+
29+
// Get document symbols for the same file
30+
const symResult = await dragraceGetDocumentSymbols(targetFile);
31+
console.log(`\nTotal symbols: ${symResult.symbols.length}`);
32+
33+
// Show first 10 symbols
34+
console.log(`\nFirst 10 symbols:`);
35+
for (const s of symResult.symbols.slice(0, 10)) {
36+
console.log(` ${s.kind} "${s.name}" L${s.range.startLine}-${s.range.endLine}`);
37+
}
38+
39+
// Cross-reference: find folding ranges that DON'T match any symbol
40+
function getAllSymbolLines(symbols, lines = new Set()) {
41+
for (const s of symbols) {
42+
lines.add(s.range.startLine);
43+
if (s.children) getAllSymbolLines(s.children, lines);
44+
}
45+
return lines;
46+
}
47+
48+
const symLines = getAllSymbolLines(symResult.symbols);
49+
const unmatchedFolds = foldResult.ranges.filter(r => !symLines.has(r.start));
50+
console.log(`\nFolding ranges with NO matching symbol: ${unmatchedFolds.length}/${foldResult.ranges.length}`);
51+
console.log(`First 15 unmatched:`);
52+
for (const r of unmatchedFolds.slice(0, 15)) {
53+
console.log(` Lines ${r.start}-${r.end}${r.kind ? ` kind="${r.kind}"` : ''}`);
54+
}

src/client-pipe.ts

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,77 @@ export async function commandExecute(
217217
return result;
218218
}
219219

220+
// ── Dragrace Methods ─────────────────────────────────────
221+
222+
export interface DocumentSymbolResult {
223+
symbols: unknown[];
224+
error?: string;
225+
}
226+
227+
/**
228+
* Get document symbols from VS Code's language server for a file.
229+
* Uses vscode.executeDocumentSymbolProvider internally via a dedicated handler
230+
* that properly converts file paths to vscode.Uri objects.
231+
*/
232+
export async function dragraceGetDocumentSymbols(
233+
filePath: string,
234+
): Promise<DocumentSymbolResult> {
235+
const result = await sendClientRequest(
236+
'dragrace.getDocumentSymbols',
237+
{filePath},
238+
30_000,
239+
);
240+
assertResult<DocumentSymbolResult>(result, 'dragrace.getDocumentSymbols');
241+
return result;
242+
}
243+
244+
// ── Dragrace: Folding Ranges ─────────────────────────────
245+
246+
export interface FoldingRangeResult {
247+
ranges: Array<{start: number; end: number; kind?: string}>;
248+
error?: string;
249+
}
250+
251+
export async function dragraceGetFoldingRanges(
252+
filePath: string,
253+
): Promise<FoldingRangeResult> {
254+
const result = await sendClientRequest(
255+
'dragrace.getFoldingRanges',
256+
{filePath},
257+
30_000,
258+
);
259+
assertResult<FoldingRangeResult>(result, 'dragrace.getFoldingRanges');
260+
return result;
261+
}
262+
263+
// ── Dragrace: Semantic Tokens ────────────────────────────
264+
265+
export interface DecodedSemanticToken {
266+
line: number;
267+
character: number;
268+
length: number;
269+
type: string;
270+
modifiers: string[];
271+
}
272+
273+
export interface SemanticTokensResult {
274+
tokens: DecodedSemanticToken[];
275+
legend: {tokenTypes: string[]; tokenModifiers: string[]};
276+
error?: string;
277+
}
278+
279+
export async function dragraceGetSemanticTokens(
280+
filePath: string,
281+
): Promise<SemanticTokensResult> {
282+
const result = await sendClientRequest(
283+
'dragrace.getSemanticTokens',
284+
{filePath},
285+
30_000,
286+
);
287+
assertResult<SemanticTokensResult>(result, 'dragrace.getSemanticTokens');
288+
return result;
289+
}
290+
220291
// ── Codebase Types ───────────────────────────────────────
221292

222293
export interface CodebaseSymbolNode {
@@ -632,6 +703,212 @@ export async function codebaseGetDiagnostics(
632703
return result;
633704
}
634705

706+
// ── File Service Types ───────────────────────────────────
707+
708+
export interface FileSymbolRange {
709+
startLine: number;
710+
startChar: number;
711+
endLine: number;
712+
endChar: number;
713+
}
714+
715+
export interface FileSymbol {
716+
name: string;
717+
kind: string;
718+
detail?: string;
719+
range: FileSymbolRange;
720+
selectionRange: FileSymbolRange;
721+
children: FileSymbol[];
722+
}
723+
724+
export interface FileGetSymbolsResult {
725+
symbols: FileSymbol[];
726+
}
727+
728+
export interface FileReadContentResult {
729+
content: string;
730+
startLine: number;
731+
endLine: number;
732+
totalLines: number;
733+
}
734+
735+
export interface FileApplyEditResult {
736+
success: boolean;
737+
file: string;
738+
}
739+
740+
export interface FileDiagnosticItem {
741+
line: number;
742+
column: number;
743+
endLine: number;
744+
endColumn: number;
745+
severity: string;
746+
message: string;
747+
code: string;
748+
source: string;
749+
}
750+
751+
export interface FileGetDiagnosticsResult {
752+
diagnostics: FileDiagnosticItem[];
753+
}
754+
755+
export interface FileExecuteRenameResult {
756+
success: boolean;
757+
filesAffected: string[];
758+
totalEdits: number;
759+
error?: string;
760+
}
761+
762+
export interface FileFindReferencesResult {
763+
references: Array<{file: string; line: number; character: number}>;
764+
}
765+
766+
export interface FileCodeActionItem {
767+
index: number;
768+
title: string;
769+
kind: string;
770+
isPreferred: boolean;
771+
hasEdit: boolean;
772+
hasCommand: boolean;
773+
}
774+
775+
export interface FileGetCodeActionsResult {
776+
actions: FileCodeActionItem[];
777+
}
778+
779+
export interface FileApplyCodeActionResult {
780+
success: boolean;
781+
title?: string;
782+
error?: string;
783+
}
784+
785+
// ── File Service Methods ─────────────────────────────────
786+
787+
/**
788+
* Get DocumentSymbols for a file with string kind names.
789+
*/
790+
export async function fileGetSymbols(filePath: string): Promise<FileGetSymbolsResult> {
791+
const result = await sendClientRequest('file.getSymbols', {filePath}, 10_000);
792+
assertResult<FileGetSymbolsResult>(result, 'file.getSymbols');
793+
return result;
794+
}
795+
796+
/**
797+
* Read file content, optionally by line range (0-based).
798+
*/
799+
export async function fileReadContent(
800+
filePath: string,
801+
startLine?: number,
802+
endLine?: number,
803+
): Promise<FileReadContentResult> {
804+
const result = await sendClientRequest(
805+
'file.readContent',
806+
{filePath, startLine, endLine},
807+
10_000,
808+
);
809+
assertResult<FileReadContentResult>(result, 'file.readContent');
810+
return result;
811+
}
812+
813+
/**
814+
* Apply a text replacement (range → new content) and save.
815+
*/
816+
export async function fileApplyEdit(
817+
filePath: string,
818+
startLine: number,
819+
endLine: number,
820+
newContent: string,
821+
startChar?: number,
822+
endChar?: number,
823+
): Promise<FileApplyEditResult> {
824+
const result = await sendClientRequest(
825+
'file.applyEdit',
826+
{filePath, startLine, startChar, endLine, endChar, newContent},
827+
15_000,
828+
);
829+
assertResult<FileApplyEditResult>(result, 'file.applyEdit');
830+
return result;
831+
}
832+
833+
/**
834+
* Get errors and warnings for a specific file.
835+
*/
836+
export async function fileGetDiagnostics(filePath: string): Promise<FileGetDiagnosticsResult> {
837+
const result = await sendClientRequest('file.getDiagnostics', {filePath}, 10_000);
838+
assertResult<FileGetDiagnosticsResult>(result, 'file.getDiagnostics');
839+
return result;
840+
}
841+
842+
/**
843+
* Execute rename provider at a position.
844+
*/
845+
export async function fileExecuteRename(
846+
filePath: string,
847+
line: number,
848+
character: number,
849+
newName: string,
850+
): Promise<FileExecuteRenameResult> {
851+
const result = await sendClientRequest(
852+
'file.executeRename',
853+
{filePath, line, character, newName},
854+
15_000,
855+
);
856+
assertResult<FileExecuteRenameResult>(result, 'file.executeRename');
857+
return result;
858+
}
859+
860+
/**
861+
* Find all references to a symbol at a position.
862+
*/
863+
export async function fileFindReferences(
864+
filePath: string,
865+
line: number,
866+
character: number,
867+
): Promise<FileFindReferencesResult> {
868+
const result = await sendClientRequest(
869+
'file.findReferences',
870+
{filePath, line, character},
871+
10_000,
872+
);
873+
assertResult<FileFindReferencesResult>(result, 'file.findReferences');
874+
return result;
875+
}
876+
877+
/**
878+
* Get available code actions for a line range.
879+
*/
880+
export async function fileGetCodeActions(
881+
filePath: string,
882+
startLine: number,
883+
endLine: number,
884+
): Promise<FileGetCodeActionsResult> {
885+
const result = await sendClientRequest(
886+
'file.getCodeActions',
887+
{filePath, startLine, endLine},
888+
10_000,
889+
);
890+
assertResult<FileGetCodeActionsResult>(result, 'file.getCodeActions');
891+
return result;
892+
}
893+
894+
/**
895+
* Apply a specific code action by index for a line range.
896+
*/
897+
export async function fileApplyCodeAction(
898+
filePath: string,
899+
startLine: number,
900+
endLine: number,
901+
actionIndex: number,
902+
): Promise<FileApplyCodeActionResult> {
903+
const result = await sendClientRequest(
904+
'file.applyCodeAction',
905+
{filePath, startLine, endLine, actionIndex},
906+
10_000,
907+
);
908+
assertResult<FileApplyCodeActionResult>(result, 'file.applyCodeAction');
909+
return result;
910+
}
911+
635912
// ── Recovery Handler ─────────────────────────────────────
636913

637914
let clientRecoveryHandler: (() => Promise<void>) | undefined;

0 commit comments

Comments
 (0)