pi-read-map 1.0.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/LICENSE +21 -0
- package/README.md +191 -0
- package/package.json +61 -0
- package/scripts/go_outline.go +239 -0
- package/scripts/python_outline.py +205 -0
- package/src/constants.ts +21 -0
- package/src/enums.ts +37 -0
- package/src/formatter.ts +414 -0
- package/src/index.ts +259 -0
- package/src/language-detect.ts +88 -0
- package/src/mapper.ts +116 -0
- package/src/mappers/c.ts +236 -0
- package/src/mappers/cpp.ts +850 -0
- package/src/mappers/csv.ts +144 -0
- package/src/mappers/ctags.ts +318 -0
- package/src/mappers/fallback.ts +162 -0
- package/src/mappers/go.ts +218 -0
- package/src/mappers/json.ts +183 -0
- package/src/mappers/jsonl.ts +135 -0
- package/src/mappers/markdown.ts +183 -0
- package/src/mappers/python.ts +138 -0
- package/src/mappers/rust.ts +886 -0
- package/src/mappers/sql.ts +195 -0
- package/src/mappers/toml.ts +191 -0
- package/src/mappers/typescript.ts +729 -0
- package/src/mappers/yaml.ts +185 -0
- package/src/types.ts +91 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { ExtensionAPI, Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createReadTool,
|
|
5
|
+
DEFAULT_MAX_LINES,
|
|
6
|
+
DEFAULT_MAX_BYTES,
|
|
7
|
+
} from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
9
|
+
import { Type } from "@sinclair/typebox";
|
|
10
|
+
import { exec } from "node:child_process";
|
|
11
|
+
import { stat } from "node:fs/promises";
|
|
12
|
+
import { basename, resolve } from "node:path";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
|
+
|
|
15
|
+
import type { FileMapMessageDetails } from "./types.js";
|
|
16
|
+
|
|
17
|
+
import { formatFileMapWithBudget } from "./formatter.js";
|
|
18
|
+
import { generateMap, shouldGenerateMap } from "./mapper.js";
|
|
19
|
+
|
|
20
|
+
export type { FileMapMessageDetails } from "./types.js";
|
|
21
|
+
|
|
22
|
+
const execAsync = promisify(exec);
|
|
23
|
+
|
|
24
|
+
// In-memory cache for maps
|
|
25
|
+
const mapCache = new Map<string, { mtime: number; map: string }>();
|
|
26
|
+
|
|
27
|
+
// Pending maps waiting to be sent after tool_result
|
|
28
|
+
const pendingMaps = new Map<
|
|
29
|
+
string,
|
|
30
|
+
{
|
|
31
|
+
path: string;
|
|
32
|
+
map: string;
|
|
33
|
+
details: FileMapMessageDetails;
|
|
34
|
+
}
|
|
35
|
+
>();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Reset the map cache. Exported for testing purposes only.
|
|
39
|
+
*/
|
|
40
|
+
export function resetMapCache(): void {
|
|
41
|
+
mapCache.clear();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reset the pending maps. Exported for testing purposes only.
|
|
46
|
+
*/
|
|
47
|
+
export function resetPendingMaps(): void {
|
|
48
|
+
pendingMaps.clear();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get pending maps for testing inspection.
|
|
53
|
+
*/
|
|
54
|
+
export function getPendingMaps(): Map<
|
|
55
|
+
string,
|
|
56
|
+
{ path: string; map: string; details: FileMapMessageDetails }
|
|
57
|
+
> {
|
|
58
|
+
return pendingMaps;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default function piReadMapExtension(pi: ExtensionAPI): void {
|
|
62
|
+
// Get the current working directory
|
|
63
|
+
const cwd = process.cwd();
|
|
64
|
+
|
|
65
|
+
// Create the built-in read tool to delegate to
|
|
66
|
+
const builtInRead = createReadTool(cwd);
|
|
67
|
+
|
|
68
|
+
// Register tool_result handler to send pending maps
|
|
69
|
+
pi.on("tool_result", (event, _ctx) => {
|
|
70
|
+
if (event.toolName !== "read") {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const pending = pendingMaps.get(event.toolCallId);
|
|
75
|
+
if (!pending) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Send the map as a custom message
|
|
80
|
+
pi.sendMessage({
|
|
81
|
+
customType: "file-map",
|
|
82
|
+
content: pending.map,
|
|
83
|
+
display: true,
|
|
84
|
+
details: pending.details,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Clean up
|
|
88
|
+
pendingMaps.delete(event.toolCallId);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Register custom message renderer for file-map type
|
|
92
|
+
pi.registerMessageRenderer<FileMapMessageDetails>(
|
|
93
|
+
"file-map",
|
|
94
|
+
(message, options, theme: Theme) => {
|
|
95
|
+
const { expanded } = options;
|
|
96
|
+
const { details } = message;
|
|
97
|
+
|
|
98
|
+
if (expanded) {
|
|
99
|
+
// Expanded: show full formatted map
|
|
100
|
+
// message.content can be string or array of content blocks
|
|
101
|
+
const content =
|
|
102
|
+
typeof message.content === "string"
|
|
103
|
+
? message.content
|
|
104
|
+
: message.content
|
|
105
|
+
.filter((c) => c.type === "text")
|
|
106
|
+
.map((c) => (c as { type: "text"; text: string }).text)
|
|
107
|
+
.join("\n");
|
|
108
|
+
return new Text(content, 0, 0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Collapsed: show summary
|
|
112
|
+
const fileName = details ? basename(details.filePath) : "file";
|
|
113
|
+
const symbolCount = details?.symbolCount ?? 0;
|
|
114
|
+
const totalLines = details?.totalLines ?? 0;
|
|
115
|
+
const detailLanguage = details?.language ?? "unknown";
|
|
116
|
+
|
|
117
|
+
let summary = theme.fg("accent", "📄 File Map: ");
|
|
118
|
+
summary += theme.fg("toolTitle", theme.bold(fileName));
|
|
119
|
+
summary += theme.fg("muted", ` │ `);
|
|
120
|
+
summary += theme.fg("dim", `${symbolCount} symbols`);
|
|
121
|
+
summary += theme.fg("muted", ` │ `);
|
|
122
|
+
summary += theme.fg("dim", `${totalLines.toLocaleString()} lines`);
|
|
123
|
+
summary += theme.fg("muted", ` │ `);
|
|
124
|
+
summary += theme.fg("dim", detailLanguage);
|
|
125
|
+
summary += theme.fg("muted", ` │ `);
|
|
126
|
+
summary += theme.fg("warning", "Ctrl+O to expand");
|
|
127
|
+
|
|
128
|
+
return new Text(summary, 0, 0);
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Register our enhanced read tool
|
|
133
|
+
pi.registerTool({
|
|
134
|
+
name: "read",
|
|
135
|
+
label: "Read",
|
|
136
|
+
description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${Math.round(DEFAULT_MAX_BYTES / 1024)}KB (whichever is hit first). If truncated, a structural map of the file is included to enable targeted reads. Use offset/limit for large files.`,
|
|
137
|
+
parameters: Type.Object({
|
|
138
|
+
path: Type.String({
|
|
139
|
+
description: "Path to the file to read (relative or absolute)",
|
|
140
|
+
}),
|
|
141
|
+
offset: Type.Optional(
|
|
142
|
+
Type.Number({
|
|
143
|
+
description: "Line number to start reading from (1-indexed)",
|
|
144
|
+
})
|
|
145
|
+
),
|
|
146
|
+
limit: Type.Optional(
|
|
147
|
+
Type.Number({ description: "Maximum number of lines to read" })
|
|
148
|
+
),
|
|
149
|
+
}),
|
|
150
|
+
|
|
151
|
+
async execute(toolCallId, params, signal, onUpdate) {
|
|
152
|
+
const { path: inputPath, offset, limit } = params;
|
|
153
|
+
|
|
154
|
+
// If offset or limit provided, delegate directly (targeted read)
|
|
155
|
+
if (offset !== undefined || limit !== undefined) {
|
|
156
|
+
return builtInRead.execute(toolCallId, params, signal, onUpdate);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Resolve path
|
|
160
|
+
const absPath = resolve(cwd, inputPath.replace(/^@/, ""));
|
|
161
|
+
|
|
162
|
+
// Check file size and line count
|
|
163
|
+
let stats;
|
|
164
|
+
try {
|
|
165
|
+
stats = await stat(absPath);
|
|
166
|
+
} catch {
|
|
167
|
+
// Let built-in handle the error
|
|
168
|
+
return builtInRead.execute(toolCallId, params, signal, onUpdate);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// For small files or non-regular files, delegate directly
|
|
172
|
+
if (!stats.isFile()) {
|
|
173
|
+
return builtInRead.execute(toolCallId, params, signal, onUpdate);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Quick check: if file is small enough by bytes, delegate
|
|
177
|
+
if (stats.size <= DEFAULT_MAX_BYTES) {
|
|
178
|
+
return builtInRead.execute(toolCallId, params, signal, onUpdate);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Need to check line count too
|
|
182
|
+
let totalLines: number;
|
|
183
|
+
try {
|
|
184
|
+
const { stdout } = await execAsync(`wc -l < "${absPath}"`, { signal });
|
|
185
|
+
totalLines = Number.parseInt(stdout.trim(), 10) || 0;
|
|
186
|
+
} catch {
|
|
187
|
+
// If we can't count lines, delegate
|
|
188
|
+
return builtInRead.execute(toolCallId, params, signal, onUpdate);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// If within threshold, delegate
|
|
192
|
+
if (!shouldGenerateMap(totalLines, stats.size)) {
|
|
193
|
+
return builtInRead.execute(toolCallId, params, signal, onUpdate);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// File exceeds threshold - generate map
|
|
197
|
+
// First, get the built-in result
|
|
198
|
+
const result = await builtInRead.execute(
|
|
199
|
+
toolCallId,
|
|
200
|
+
params,
|
|
201
|
+
signal,
|
|
202
|
+
onUpdate
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Check cache
|
|
206
|
+
const cached = mapCache.get(absPath);
|
|
207
|
+
let mapText: string;
|
|
208
|
+
let symbolCount: number;
|
|
209
|
+
let language: string;
|
|
210
|
+
|
|
211
|
+
if (cached && cached.mtime === stats.mtimeMs) {
|
|
212
|
+
// Cache hit - we need to regenerate map for metadata
|
|
213
|
+
// (alternatively, cache could store metadata too)
|
|
214
|
+
const fileMap = await generateMap(absPath, { signal });
|
|
215
|
+
if (fileMap) {
|
|
216
|
+
mapText = cached.map;
|
|
217
|
+
({ language } = fileMap);
|
|
218
|
+
symbolCount = fileMap.symbols.length;
|
|
219
|
+
} else {
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
// Generate new map
|
|
224
|
+
const fileMap = await generateMap(absPath, { signal });
|
|
225
|
+
|
|
226
|
+
if (fileMap) {
|
|
227
|
+
mapText = formatFileMapWithBudget(fileMap);
|
|
228
|
+
({ language } = fileMap);
|
|
229
|
+
symbolCount = fileMap.symbols.length;
|
|
230
|
+
// Cache it
|
|
231
|
+
mapCache.set(absPath, { mtime: stats.mtimeMs, map: mapText });
|
|
232
|
+
} else {
|
|
233
|
+
// Map generation failed, return original result
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Store map in pendingMaps for delivery after tool_result event
|
|
239
|
+
pendingMaps.set(toolCallId, {
|
|
240
|
+
path: absPath,
|
|
241
|
+
map: mapText,
|
|
242
|
+
details: {
|
|
243
|
+
filePath: absPath,
|
|
244
|
+
totalLines,
|
|
245
|
+
totalBytes: stats.size,
|
|
246
|
+
symbolCount,
|
|
247
|
+
language,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Return the built-in result unmodified (with cleared truncation details
|
|
252
|
+
// since we're providing a map separately)
|
|
253
|
+
return {
|
|
254
|
+
...result,
|
|
255
|
+
details: undefined,
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { extname } from "node:path";
|
|
2
|
+
|
|
3
|
+
import type { LanguageInfo } from "./types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Map of file extensions to language info.
|
|
7
|
+
*/
|
|
8
|
+
const EXTENSION_MAP: Record<string, LanguageInfo> = {
|
|
9
|
+
// Python
|
|
10
|
+
".py": { id: "python", name: "Python" },
|
|
11
|
+
".pyw": { id: "python", name: "Python" },
|
|
12
|
+
".pyi": { id: "python", name: "Python" },
|
|
13
|
+
|
|
14
|
+
// TypeScript
|
|
15
|
+
".ts": { id: "typescript", name: "TypeScript" },
|
|
16
|
+
".tsx": { id: "typescript", name: "TypeScript" },
|
|
17
|
+
".mts": { id: "typescript", name: "TypeScript" },
|
|
18
|
+
".cts": { id: "typescript", name: "TypeScript" },
|
|
19
|
+
|
|
20
|
+
// JavaScript
|
|
21
|
+
".js": { id: "javascript", name: "JavaScript" },
|
|
22
|
+
".jsx": { id: "javascript", name: "JavaScript" },
|
|
23
|
+
".mjs": { id: "javascript", name: "JavaScript" },
|
|
24
|
+
".cjs": { id: "javascript", name: "JavaScript" },
|
|
25
|
+
|
|
26
|
+
// Go
|
|
27
|
+
".go": { id: "go", name: "Go" },
|
|
28
|
+
|
|
29
|
+
// Rust
|
|
30
|
+
".rs": { id: "rust", name: "Rust" },
|
|
31
|
+
|
|
32
|
+
// C/C++
|
|
33
|
+
".c": { id: "c", name: "C" },
|
|
34
|
+
".h": { id: "c-header", name: "C Header" },
|
|
35
|
+
".cpp": { id: "cpp", name: "C++" },
|
|
36
|
+
".cc": { id: "cpp", name: "C++" },
|
|
37
|
+
".cxx": { id: "cpp", name: "C++" },
|
|
38
|
+
".hpp": { id: "cpp", name: "C++" },
|
|
39
|
+
".hxx": { id: "cpp", name: "C++" },
|
|
40
|
+
|
|
41
|
+
// SQL
|
|
42
|
+
".sql": { id: "sql", name: "SQL" },
|
|
43
|
+
|
|
44
|
+
// JSON
|
|
45
|
+
".json": { id: "json", name: "JSON" },
|
|
46
|
+
".jsonc": { id: "json", name: "JSON" },
|
|
47
|
+
|
|
48
|
+
// JSON Lines
|
|
49
|
+
".jsonl": { id: "jsonl", name: "JSON Lines" },
|
|
50
|
+
|
|
51
|
+
// Markdown
|
|
52
|
+
".md": { id: "markdown", name: "Markdown" },
|
|
53
|
+
".mdx": { id: "markdown", name: "Markdown" },
|
|
54
|
+
|
|
55
|
+
// YAML
|
|
56
|
+
".yml": { id: "yaml", name: "YAML" },
|
|
57
|
+
".yaml": { id: "yaml", name: "YAML" },
|
|
58
|
+
|
|
59
|
+
// TOML
|
|
60
|
+
".toml": { id: "toml", name: "TOML" },
|
|
61
|
+
|
|
62
|
+
// CSV
|
|
63
|
+
".csv": { id: "csv", name: "CSV" },
|
|
64
|
+
".tsv": { id: "csv", name: "TSV" },
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Detect language from file path.
|
|
69
|
+
* Returns null for unknown file types.
|
|
70
|
+
*/
|
|
71
|
+
export function detectLanguage(filePath: string): LanguageInfo | null {
|
|
72
|
+
const ext = extname(filePath).toLowerCase();
|
|
73
|
+
return EXTENSION_MAP[ext] ?? null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get all supported extensions.
|
|
78
|
+
*/
|
|
79
|
+
export function getSupportedExtensions(): string[] {
|
|
80
|
+
return Object.keys(EXTENSION_MAP);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if a file extension is supported.
|
|
85
|
+
*/
|
|
86
|
+
export function isSupported(filePath: string): boolean {
|
|
87
|
+
return detectLanguage(filePath) !== null;
|
|
88
|
+
}
|
package/src/mapper.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { FileMap, MapOptions } from "./types.js";
|
|
2
|
+
|
|
3
|
+
import { THRESHOLDS } from "./constants.js";
|
|
4
|
+
import { detectLanguage } from "./language-detect.js";
|
|
5
|
+
import { cMapper } from "./mappers/c.js";
|
|
6
|
+
import { cppMapper } from "./mappers/cpp.js";
|
|
7
|
+
import { csvMapper } from "./mappers/csv.js";
|
|
8
|
+
import { ctagsMapper } from "./mappers/ctags.js";
|
|
9
|
+
import { fallbackMapper } from "./mappers/fallback.js";
|
|
10
|
+
import { goMapper } from "./mappers/go.js";
|
|
11
|
+
import { jsonMapper } from "./mappers/json.js";
|
|
12
|
+
import { jsonlMapper } from "./mappers/jsonl.js";
|
|
13
|
+
import { markdownMapper } from "./mappers/markdown.js";
|
|
14
|
+
import { pythonMapper } from "./mappers/python.js";
|
|
15
|
+
import { rustMapper } from "./mappers/rust.js";
|
|
16
|
+
import { sqlMapper } from "./mappers/sql.js";
|
|
17
|
+
import { tomlMapper } from "./mappers/toml.js";
|
|
18
|
+
import { typescriptMapper } from "./mappers/typescript.js";
|
|
19
|
+
import { yamlMapper } from "./mappers/yaml.js";
|
|
20
|
+
|
|
21
|
+
type MapperFn = (
|
|
22
|
+
filePath: string,
|
|
23
|
+
signal?: AbortSignal
|
|
24
|
+
) => Promise<FileMap | null>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Registry of language-specific mappers.
|
|
28
|
+
*
|
|
29
|
+
* Uses internal tree-sitter/ts-morph mappers for all supported languages.
|
|
30
|
+
*/
|
|
31
|
+
const MAPPERS: Record<string, MapperFn> = {
|
|
32
|
+
// Phase 1: Python AST-based
|
|
33
|
+
python: pythonMapper,
|
|
34
|
+
|
|
35
|
+
// Phase 2: Go AST-based
|
|
36
|
+
go: goMapper,
|
|
37
|
+
|
|
38
|
+
// Phase 3: Internal ts-morph mappers
|
|
39
|
+
typescript: typescriptMapper,
|
|
40
|
+
javascript: typescriptMapper,
|
|
41
|
+
|
|
42
|
+
// Phase 3: Internal regex-based markdown
|
|
43
|
+
markdown: markdownMapper,
|
|
44
|
+
|
|
45
|
+
// Phase 3: Internal tree-sitter mappers
|
|
46
|
+
rust: rustMapper,
|
|
47
|
+
cpp: cppMapper,
|
|
48
|
+
"c-header": cppMapper, // .h files
|
|
49
|
+
|
|
50
|
+
// Phase 2: Regex/subprocess mappers
|
|
51
|
+
sql: sqlMapper,
|
|
52
|
+
json: jsonMapper,
|
|
53
|
+
jsonl: jsonlMapper,
|
|
54
|
+
c: cMapper, // .c files use regex
|
|
55
|
+
|
|
56
|
+
// Phase 4: Extended coverage
|
|
57
|
+
yaml: yamlMapper,
|
|
58
|
+
toml: tomlMapper,
|
|
59
|
+
csv: csvMapper,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generate a structural map for a file.
|
|
64
|
+
*
|
|
65
|
+
* Dispatches to the appropriate language-specific mapper,
|
|
66
|
+
* falling back to ctags (if available) then grep-based extraction.
|
|
67
|
+
*/
|
|
68
|
+
export async function generateMap(
|
|
69
|
+
filePath: string,
|
|
70
|
+
options: MapOptions = {}
|
|
71
|
+
): Promise<FileMap | null> {
|
|
72
|
+
const { signal } = options;
|
|
73
|
+
|
|
74
|
+
// Detect language
|
|
75
|
+
const langInfo = detectLanguage(filePath);
|
|
76
|
+
|
|
77
|
+
if (!langInfo) {
|
|
78
|
+
// Unknown language, try ctags then fallback
|
|
79
|
+
const ctagsResult = await ctagsMapper(filePath, signal);
|
|
80
|
+
if (ctagsResult) {
|
|
81
|
+
return ctagsResult;
|
|
82
|
+
}
|
|
83
|
+
return fallbackMapper(filePath, signal);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Try language-specific mapper
|
|
87
|
+
const mapper = MAPPERS[langInfo.id];
|
|
88
|
+
|
|
89
|
+
if (mapper) {
|
|
90
|
+
const result = await mapper(filePath, signal);
|
|
91
|
+
if (result) {
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
// Mapper failed, fall through to ctags/fallback
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Try ctags as intermediate fallback (better than grep when available)
|
|
98
|
+
const ctagsResult = await ctagsMapper(filePath, signal);
|
|
99
|
+
if (ctagsResult) {
|
|
100
|
+
return ctagsResult;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Use grep-based fallback mapper
|
|
104
|
+
return fallbackMapper(filePath, signal);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if a file should have a map generated.
|
|
109
|
+
* Returns true if the file exceeds the truncation threshold.
|
|
110
|
+
*/
|
|
111
|
+
export function shouldGenerateMap(
|
|
112
|
+
totalLines: number,
|
|
113
|
+
totalBytes: number
|
|
114
|
+
): boolean {
|
|
115
|
+
return totalLines > THRESHOLDS.MAX_LINES || totalBytes > THRESHOLDS.MAX_BYTES;
|
|
116
|
+
}
|
package/src/mappers/c.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import type { FileMap, FileSymbol } from "../types.js";
|
|
4
|
+
|
|
5
|
+
import { DetailLevel, SymbolKind } from "../enums.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Regex patterns for C language constructs.
|
|
9
|
+
*/
|
|
10
|
+
const C_PATTERNS = {
|
|
11
|
+
// Function definition: type name(params) { or type name(params);
|
|
12
|
+
function: /^(\w+(?:\s*\*)?)\s+(\w+)\s*\(([^)]*)\)\s*(?:\{|;)?/,
|
|
13
|
+
|
|
14
|
+
// Struct definition: struct name { or typedef struct { ... } name;
|
|
15
|
+
struct: /^(?:typedef\s+)?struct\s+(\w+)?\s*\{/,
|
|
16
|
+
|
|
17
|
+
// Enum definition
|
|
18
|
+
enum: /^(?:typedef\s+)?enum\s+(\w+)?\s*\{/,
|
|
19
|
+
|
|
20
|
+
// Union definition
|
|
21
|
+
union: /^(?:typedef\s+)?union\s+(\w+)?\s*\{/,
|
|
22
|
+
|
|
23
|
+
// #define macro
|
|
24
|
+
define: /^#define\s+(\w+)(?:\([^)]*\))?\s*/,
|
|
25
|
+
|
|
26
|
+
// Global variable declaration
|
|
27
|
+
variable:
|
|
28
|
+
/^(?:static\s+)?(?:const\s+)?(?:volatile\s+)?(\w+(?:\s*\*)?)\s+(\w+)\s*(?:=|;)/,
|
|
29
|
+
|
|
30
|
+
// Typedef
|
|
31
|
+
typedef: /^typedef\s+(?:struct|enum|union)?\s*\w*\s*\{?[^}]*\}?\s*(\w+)\s*;/,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
interface CMatch {
|
|
35
|
+
name: string;
|
|
36
|
+
kind: SymbolKind;
|
|
37
|
+
startLine: number;
|
|
38
|
+
signature?: string;
|
|
39
|
+
modifiers?: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Find the end of a brace-delimited block.
|
|
44
|
+
*/
|
|
45
|
+
function findBlockEnd(lines: string[], startIdx: number): number {
|
|
46
|
+
let braceCount = 0;
|
|
47
|
+
let foundOpen = false;
|
|
48
|
+
|
|
49
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
50
|
+
const line = lines[i];
|
|
51
|
+
if (!line) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
for (const char of line) {
|
|
55
|
+
if (char === "{") {
|
|
56
|
+
braceCount++;
|
|
57
|
+
foundOpen = true;
|
|
58
|
+
} else if (char === "}") {
|
|
59
|
+
braceCount--;
|
|
60
|
+
if (foundOpen && braceCount === 0) {
|
|
61
|
+
return i + 1; // 1-indexed
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return startIdx + 1; // Single line if no block found
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if a line is inside a function body.
|
|
72
|
+
*/
|
|
73
|
+
function isInsideFunction(lines: string[], lineIdx: number): boolean {
|
|
74
|
+
let braceCount = 0;
|
|
75
|
+
for (let i = 0; i < lineIdx; i++) {
|
|
76
|
+
const line = lines[i];
|
|
77
|
+
if (!line) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
for (const char of line) {
|
|
81
|
+
if (char === "{") {
|
|
82
|
+
braceCount++;
|
|
83
|
+
} else if (char === "}") {
|
|
84
|
+
braceCount--;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return braceCount > 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Generate a file map for a C file using regex patterns.
|
|
93
|
+
*/
|
|
94
|
+
export async function cMapper(
|
|
95
|
+
filePath: string,
|
|
96
|
+
_signal?: AbortSignal
|
|
97
|
+
): Promise<FileMap | null> {
|
|
98
|
+
try {
|
|
99
|
+
const stats = await stat(filePath);
|
|
100
|
+
const totalBytes = stats.size;
|
|
101
|
+
|
|
102
|
+
const content = await readFile(filePath, "utf8");
|
|
103
|
+
const lines = content.split("\n");
|
|
104
|
+
const totalLines = lines.length;
|
|
105
|
+
|
|
106
|
+
const matches: CMatch[] = [];
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < lines.length; i++) {
|
|
109
|
+
const line = lines[i];
|
|
110
|
+
if (!line) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const trimmed = line.trim();
|
|
114
|
+
const lineNum = i + 1;
|
|
115
|
+
|
|
116
|
+
// Skip empty lines and comments
|
|
117
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*")) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Skip lines inside function bodies (only want top-level)
|
|
122
|
+
if (isInsideFunction(lines, i) && !C_PATTERNS.define.test(trimmed)) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check for #define
|
|
127
|
+
const defineMatch = C_PATTERNS.define.exec(trimmed);
|
|
128
|
+
if (defineMatch) {
|
|
129
|
+
const [, matchName] = defineMatch;
|
|
130
|
+
if (matchName) {
|
|
131
|
+
matches.push({
|
|
132
|
+
name: matchName,
|
|
133
|
+
kind: SymbolKind.Constant,
|
|
134
|
+
startLine: lineNum,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check for struct
|
|
141
|
+
const structMatch = C_PATTERNS.struct.exec(trimmed);
|
|
142
|
+
if (structMatch) {
|
|
143
|
+
matches.push({
|
|
144
|
+
name: structMatch[1] ?? "(anonymous)",
|
|
145
|
+
kind: SymbolKind.Class,
|
|
146
|
+
startLine: lineNum,
|
|
147
|
+
});
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check for enum
|
|
152
|
+
const enumMatch = C_PATTERNS.enum.exec(trimmed);
|
|
153
|
+
if (enumMatch) {
|
|
154
|
+
matches.push({
|
|
155
|
+
name: enumMatch[1] ?? "(anonymous)",
|
|
156
|
+
kind: SymbolKind.Enum,
|
|
157
|
+
startLine: lineNum,
|
|
158
|
+
});
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check for union
|
|
163
|
+
const unionMatch = C_PATTERNS.union.exec(trimmed);
|
|
164
|
+
if (unionMatch) {
|
|
165
|
+
matches.push({
|
|
166
|
+
name: unionMatch[1] ?? "(anonymous)",
|
|
167
|
+
kind: SymbolKind.Class,
|
|
168
|
+
startLine: lineNum,
|
|
169
|
+
modifiers: ["union"],
|
|
170
|
+
});
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check for function
|
|
175
|
+
const funcMatch = C_PATTERNS.function.exec(trimmed);
|
|
176
|
+
if (funcMatch) {
|
|
177
|
+
const [, returnType, name, params] = funcMatch;
|
|
178
|
+
|
|
179
|
+
// Skip if it's a variable declaration or control statement
|
|
180
|
+
if (
|
|
181
|
+
!trimmed.includes("(") ||
|
|
182
|
+
!name ||
|
|
183
|
+
/^(if|while|for|switch|return)$/.test(name)
|
|
184
|
+
) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
matches.push({
|
|
189
|
+
name,
|
|
190
|
+
kind: SymbolKind.Function,
|
|
191
|
+
startLine: lineNum,
|
|
192
|
+
signature: `(${params ?? ""}): ${returnType ?? "void"}`,
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Convert to symbols with end lines
|
|
199
|
+
const symbols: FileSymbol[] = matches.map((m) => {
|
|
200
|
+
const { startLine } = m;
|
|
201
|
+
const endLine =
|
|
202
|
+
m.kind === SymbolKind.Constant
|
|
203
|
+
? startLine
|
|
204
|
+
: findBlockEnd(lines, startLine - 1);
|
|
205
|
+
|
|
206
|
+
const symbol: FileSymbol = {
|
|
207
|
+
name: m.name,
|
|
208
|
+
kind: m.kind,
|
|
209
|
+
startLine,
|
|
210
|
+
endLine,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
if (m.signature) {
|
|
214
|
+
symbol.signature = m.signature;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (m.modifiers) {
|
|
218
|
+
symbol.modifiers = m.modifiers;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return symbol;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
path: filePath,
|
|
226
|
+
totalLines,
|
|
227
|
+
totalBytes,
|
|
228
|
+
language: "C",
|
|
229
|
+
symbols,
|
|
230
|
+
detailLevel: DetailLevel.Full,
|
|
231
|
+
};
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error(`C mapper failed: ${error}`);
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|