gh-here 3.0.3 → 3.1.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/.env +0 -0
- package/.playwright-mcp/fixed-alignment.png +0 -0
- package/.playwright-mcp/fixed-layout.png +0 -0
- package/.playwright-mcp/gh-here-home-header-table.png +0 -0
- package/.playwright-mcp/gh-here-home.png +0 -0
- package/.playwright-mcp/line-selection-multiline.png +0 -0
- package/.playwright-mcp/line-selection-test-after.png +0 -0
- package/.playwright-mcp/line-selection-test-before.png +0 -0
- package/.playwright-mcp/page-2026-01-03T17-58-21-336Z.png +0 -0
- package/lib/constants.js +25 -15
- package/lib/content-search.js +212 -0
- package/lib/error-handler.js +39 -28
- package/lib/file-utils.js +438 -287
- package/lib/git.js +10 -54
- package/lib/gitignore.js +70 -41
- package/lib/renderers.js +15 -19
- package/lib/server.js +70 -193
- package/lib/symbol-parser.js +600 -0
- package/package.json +1 -1
- package/public/app.js +134 -68
- package/public/js/constants.js +50 -34
- package/public/js/content-search-handler.js +551 -0
- package/public/js/file-viewer.js +437 -0
- package/public/js/focus-mode.js +280 -0
- package/public/js/inline-search.js +659 -0
- package/public/js/modal-manager.js +14 -28
- package/public/js/symbol-outline.js +454 -0
- package/public/js/utils.js +152 -94
- package/public/styles.css +2049 -296
- package/.claude/settings.local.json +0 -30
- package/SAMPLE.md +0 -287
- package/lib/validation.js +0 -77
- package/public/app.js.backup +0 -1902
- package/public/js/draft-manager.js +0 -36
- package/public/js/editor-manager.js +0 -159
- package/test.js +0 -138
- package/tests/draftManager.test.js +0 -241
- package/tests/fileTypeDetection.test.js +0 -111
- package/tests/httpService.test.js +0 -268
- package/tests/languageDetection.test.js +0 -145
- package/tests/pathUtils.test.js +0 -136
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol Parser - Extracts code symbols from source files
|
|
3
|
+
* Supports JavaScript/TypeScript, Python, CSS, and more
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Symbol types
|
|
10
|
+
*/
|
|
11
|
+
const SYMBOL_KINDS = {
|
|
12
|
+
FUNCTION: 'function',
|
|
13
|
+
CLASS: 'class',
|
|
14
|
+
METHOD: 'method',
|
|
15
|
+
VARIABLE: 'variable',
|
|
16
|
+
CONSTANT: 'constant',
|
|
17
|
+
INTERFACE: 'interface',
|
|
18
|
+
TYPE: 'type',
|
|
19
|
+
EXPORT: 'export',
|
|
20
|
+
IMPORT: 'import',
|
|
21
|
+
SELECTOR: 'selector',
|
|
22
|
+
MIXIN: 'mixin',
|
|
23
|
+
KEYFRAMES: 'keyframes',
|
|
24
|
+
MEDIA: 'media'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse symbols from file content based on language
|
|
29
|
+
* @param {string} content - File content
|
|
30
|
+
* @param {string} filePath - File path (for language detection)
|
|
31
|
+
* @returns {Array<{name: string, kind: string, line: number, detail?: string}>}
|
|
32
|
+
*/
|
|
33
|
+
function parseSymbols(content, filePath) {
|
|
34
|
+
const ext = path.extname(filePath).toLowerCase().slice(1);
|
|
35
|
+
const lines = content.split('\n');
|
|
36
|
+
|
|
37
|
+
switch (ext) {
|
|
38
|
+
case 'js':
|
|
39
|
+
case 'mjs':
|
|
40
|
+
case 'cjs':
|
|
41
|
+
case 'jsx':
|
|
42
|
+
return parseJavaScript(lines);
|
|
43
|
+
case 'ts':
|
|
44
|
+
case 'tsx':
|
|
45
|
+
return parseTypeScript(lines);
|
|
46
|
+
case 'py':
|
|
47
|
+
return parsePython(lines);
|
|
48
|
+
case 'css':
|
|
49
|
+
case 'scss':
|
|
50
|
+
case 'less':
|
|
51
|
+
return parseCSS(lines);
|
|
52
|
+
case 'go':
|
|
53
|
+
return parseGo(lines);
|
|
54
|
+
case 'rb':
|
|
55
|
+
return parseRuby(lines);
|
|
56
|
+
case 'rs':
|
|
57
|
+
return parseRust(lines);
|
|
58
|
+
case 'java':
|
|
59
|
+
case 'kt':
|
|
60
|
+
return parseJavaKotlin(lines);
|
|
61
|
+
case 'php':
|
|
62
|
+
return parsePHP(lines);
|
|
63
|
+
case 'c':
|
|
64
|
+
case 'cpp':
|
|
65
|
+
case 'h':
|
|
66
|
+
case 'hpp':
|
|
67
|
+
return parseCpp(lines);
|
|
68
|
+
default:
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse JavaScript symbols
|
|
75
|
+
*/
|
|
76
|
+
function parseJavaScript(lines) {
|
|
77
|
+
const symbols = [];
|
|
78
|
+
let insideClass = false;
|
|
79
|
+
let classIndent = 0;
|
|
80
|
+
|
|
81
|
+
// Top-level patterns (matched against trimmed line)
|
|
82
|
+
const topLevelPatterns = [
|
|
83
|
+
// Named function declarations
|
|
84
|
+
{ regex: /^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/, kind: SYMBOL_KINDS.FUNCTION },
|
|
85
|
+
// Class declarations
|
|
86
|
+
{ regex: /^(?:export\s+)?class\s+(\w+)/, kind: SYMBOL_KINDS.CLASS },
|
|
87
|
+
// Arrow function assigned to const/let/var
|
|
88
|
+
{ regex: /^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(.*\)\s*=>/, kind: SYMBOL_KINDS.FUNCTION },
|
|
89
|
+
// Arrow function assigned (single param, no parens)
|
|
90
|
+
{ regex: /^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\w+\s*=>/, kind: SYMBOL_KINDS.FUNCTION },
|
|
91
|
+
// Regular function assigned to variable
|
|
92
|
+
{ regex: /^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?function/, kind: SYMBOL_KINDS.FUNCTION },
|
|
93
|
+
// Exported constants (uppercase)
|
|
94
|
+
{ regex: /^(?:export\s+)?const\s+([A-Z][A-Z0-9_]+)\s*=/, kind: SYMBOL_KINDS.CONSTANT },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
// Class method pattern (matches method definitions inside classes)
|
|
98
|
+
const methodPattern = /^(?:async\s+)?(?:static\s+)?(?:get\s+|set\s+)?(\w+)\s*\([^)]*\)\s*\{/;
|
|
99
|
+
|
|
100
|
+
// Keywords to skip when detecting methods
|
|
101
|
+
const skipKeywords = ['if', 'for', 'while', 'switch', 'catch', 'function', 'return', 'throw', 'new'];
|
|
102
|
+
|
|
103
|
+
lines.forEach((line, index) => {
|
|
104
|
+
const trimmed = line.trimStart();
|
|
105
|
+
const currentIndent = line.length - trimmed.length;
|
|
106
|
+
|
|
107
|
+
// Skip comments and empty lines
|
|
108
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*') || !trimmed) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check for class declaration
|
|
113
|
+
const classMatch = trimmed.match(/^(?:export\s+)?class\s+(\w+)/);
|
|
114
|
+
if (classMatch) {
|
|
115
|
+
insideClass = true;
|
|
116
|
+
classIndent = currentIndent;
|
|
117
|
+
symbols.push({
|
|
118
|
+
name: classMatch[1],
|
|
119
|
+
kind: SYMBOL_KINDS.CLASS,
|
|
120
|
+
line: index + 1
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Detect when we exit a class (line at same or lower indent level as class declaration)
|
|
126
|
+
if (insideClass && currentIndent <= classIndent && trimmed !== '}' && !trimmed.startsWith('}')) {
|
|
127
|
+
// Check if this is a new top-level declaration
|
|
128
|
+
const isTopLevel = topLevelPatterns.some(p => p.regex.test(trimmed));
|
|
129
|
+
if (isTopLevel) {
|
|
130
|
+
insideClass = false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// If inside a class, look for methods
|
|
135
|
+
if (insideClass && currentIndent > classIndent) {
|
|
136
|
+
const methodMatch = trimmed.match(methodPattern);
|
|
137
|
+
if (methodMatch && methodMatch[1]) {
|
|
138
|
+
const name = methodMatch[1];
|
|
139
|
+
// Skip keywords and constructor
|
|
140
|
+
if (!skipKeywords.includes(name) && name !== 'constructor') {
|
|
141
|
+
symbols.push({
|
|
142
|
+
name,
|
|
143
|
+
kind: SYMBOL_KINDS.METHOD,
|
|
144
|
+
line: index + 1
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check top-level patterns
|
|
152
|
+
for (const { regex, kind } of topLevelPatterns) {
|
|
153
|
+
// Skip class pattern as we already handled it
|
|
154
|
+
if (kind === SYMBOL_KINDS.CLASS) continue;
|
|
155
|
+
|
|
156
|
+
const match = trimmed.match(regex);
|
|
157
|
+
if (match && match[1]) {
|
|
158
|
+
symbols.push({
|
|
159
|
+
name: match[1],
|
|
160
|
+
kind,
|
|
161
|
+
line: index + 1
|
|
162
|
+
});
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return deduplicateSymbols(symbols);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Parse TypeScript symbols (extends JavaScript)
|
|
173
|
+
*/
|
|
174
|
+
function parseTypeScript(lines) {
|
|
175
|
+
const symbols = parseJavaScript(lines);
|
|
176
|
+
|
|
177
|
+
const tsPatterns = [
|
|
178
|
+
// Interface declarations
|
|
179
|
+
{ regex: /^(?:export\s+)?interface\s+(\w+)/, kind: SYMBOL_KINDS.INTERFACE },
|
|
180
|
+
// Type declarations
|
|
181
|
+
{ regex: /^(?:export\s+)?type\s+(\w+)\s*=/, kind: SYMBOL_KINDS.TYPE },
|
|
182
|
+
// Enum declarations
|
|
183
|
+
{ regex: /^(?:export\s+)?enum\s+(\w+)/, kind: SYMBOL_KINDS.TYPE },
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
lines.forEach((line, index) => {
|
|
187
|
+
const trimmed = line.trimStart();
|
|
188
|
+
|
|
189
|
+
for (const { regex, kind } of tsPatterns) {
|
|
190
|
+
const match = trimmed.match(regex);
|
|
191
|
+
if (match && match[1]) {
|
|
192
|
+
symbols.push({
|
|
193
|
+
name: match[1],
|
|
194
|
+
kind,
|
|
195
|
+
line: index + 1
|
|
196
|
+
});
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return deduplicateSymbols(symbols);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Parse Python symbols
|
|
207
|
+
*/
|
|
208
|
+
function parsePython(lines) {
|
|
209
|
+
const symbols = [];
|
|
210
|
+
|
|
211
|
+
lines.forEach((line, index) => {
|
|
212
|
+
const trimmed = line.trimStart();
|
|
213
|
+
const indent = line.length - trimmed.length;
|
|
214
|
+
|
|
215
|
+
// Function definitions
|
|
216
|
+
const funcMatch = trimmed.match(/^(?:async\s+)?def\s+(\w+)\s*\(/);
|
|
217
|
+
if (funcMatch) {
|
|
218
|
+
symbols.push({
|
|
219
|
+
name: funcMatch[1],
|
|
220
|
+
kind: indent === 0 ? SYMBOL_KINDS.FUNCTION : SYMBOL_KINDS.METHOD,
|
|
221
|
+
line: index + 1
|
|
222
|
+
});
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Class definitions
|
|
227
|
+
const classMatch = trimmed.match(/^class\s+(\w+)/);
|
|
228
|
+
if (classMatch) {
|
|
229
|
+
symbols.push({
|
|
230
|
+
name: classMatch[1],
|
|
231
|
+
kind: SYMBOL_KINDS.CLASS,
|
|
232
|
+
line: index + 1
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return symbols;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Parse CSS symbols (selectors, keyframes, media queries)
|
|
242
|
+
*/
|
|
243
|
+
function parseCSS(lines) {
|
|
244
|
+
const symbols = [];
|
|
245
|
+
|
|
246
|
+
lines.forEach((line, index) => {
|
|
247
|
+
const trimmed = line.trimStart();
|
|
248
|
+
|
|
249
|
+
// Skip comments
|
|
250
|
+
if (trimmed.startsWith('/*') || trimmed.startsWith('*') || !trimmed) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// @keyframes
|
|
255
|
+
const keyframesMatch = trimmed.match(/^@keyframes\s+([\w-]+)/);
|
|
256
|
+
if (keyframesMatch) {
|
|
257
|
+
symbols.push({
|
|
258
|
+
name: keyframesMatch[1],
|
|
259
|
+
kind: SYMBOL_KINDS.KEYFRAMES,
|
|
260
|
+
line: index + 1
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// @media queries
|
|
266
|
+
const mediaMatch = trimmed.match(/^@media\s+(.+?)\s*\{/);
|
|
267
|
+
if (mediaMatch) {
|
|
268
|
+
symbols.push({
|
|
269
|
+
name: mediaMatch[1].slice(0, 40) + (mediaMatch[1].length > 40 ? '...' : ''),
|
|
270
|
+
kind: SYMBOL_KINDS.MEDIA,
|
|
271
|
+
line: index + 1
|
|
272
|
+
});
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// @mixin (SCSS)
|
|
277
|
+
const mixinMatch = trimmed.match(/^@mixin\s+([\w-]+)/);
|
|
278
|
+
if (mixinMatch) {
|
|
279
|
+
symbols.push({
|
|
280
|
+
name: mixinMatch[1],
|
|
281
|
+
kind: SYMBOL_KINDS.MIXIN,
|
|
282
|
+
line: index + 1
|
|
283
|
+
});
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Class and ID selectors at root level (not nested)
|
|
288
|
+
if (line.charAt(0) !== ' ' && line.charAt(0) !== '\t') {
|
|
289
|
+
const selectorMatch = trimmed.match(/^([.#][\w-]+(?:\s*,\s*[.#][\w-]+)*)\s*\{/);
|
|
290
|
+
if (selectorMatch) {
|
|
291
|
+
symbols.push({
|
|
292
|
+
name: selectorMatch[1].split(',')[0].trim(),
|
|
293
|
+
kind: SYMBOL_KINDS.SELECTOR,
|
|
294
|
+
line: index + 1
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
return symbols;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Parse Go symbols
|
|
305
|
+
*/
|
|
306
|
+
function parseGo(lines) {
|
|
307
|
+
const symbols = [];
|
|
308
|
+
|
|
309
|
+
lines.forEach((line, index) => {
|
|
310
|
+
const trimmed = line.trimStart();
|
|
311
|
+
|
|
312
|
+
// Function declarations
|
|
313
|
+
const funcMatch = trimmed.match(/^func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)\s*\(/);
|
|
314
|
+
if (funcMatch) {
|
|
315
|
+
symbols.push({
|
|
316
|
+
name: funcMatch[1],
|
|
317
|
+
kind: SYMBOL_KINDS.FUNCTION,
|
|
318
|
+
line: index + 1
|
|
319
|
+
});
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Type declarations
|
|
324
|
+
const typeMatch = trimmed.match(/^type\s+(\w+)\s+(?:struct|interface)/);
|
|
325
|
+
if (typeMatch) {
|
|
326
|
+
symbols.push({
|
|
327
|
+
name: typeMatch[1],
|
|
328
|
+
kind: SYMBOL_KINDS.CLASS,
|
|
329
|
+
line: index + 1
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return symbols;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Parse Ruby symbols
|
|
339
|
+
*/
|
|
340
|
+
function parseRuby(lines) {
|
|
341
|
+
const symbols = [];
|
|
342
|
+
|
|
343
|
+
lines.forEach((line, index) => {
|
|
344
|
+
const trimmed = line.trimStart();
|
|
345
|
+
|
|
346
|
+
// Method definitions
|
|
347
|
+
const defMatch = trimmed.match(/^def\s+(self\.)?(\w+[?!=]?)/);
|
|
348
|
+
if (defMatch) {
|
|
349
|
+
symbols.push({
|
|
350
|
+
name: defMatch[2],
|
|
351
|
+
kind: SYMBOL_KINDS.FUNCTION,
|
|
352
|
+
line: index + 1
|
|
353
|
+
});
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Class definitions
|
|
358
|
+
const classMatch = trimmed.match(/^class\s+(\w+)/);
|
|
359
|
+
if (classMatch) {
|
|
360
|
+
symbols.push({
|
|
361
|
+
name: classMatch[1],
|
|
362
|
+
kind: SYMBOL_KINDS.CLASS,
|
|
363
|
+
line: index + 1
|
|
364
|
+
});
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Module definitions
|
|
369
|
+
const moduleMatch = trimmed.match(/^module\s+(\w+)/);
|
|
370
|
+
if (moduleMatch) {
|
|
371
|
+
symbols.push({
|
|
372
|
+
name: moduleMatch[1],
|
|
373
|
+
kind: SYMBOL_KINDS.CLASS,
|
|
374
|
+
line: index + 1
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
return symbols;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Parse Rust symbols
|
|
384
|
+
*/
|
|
385
|
+
function parseRust(lines) {
|
|
386
|
+
const symbols = [];
|
|
387
|
+
|
|
388
|
+
lines.forEach((line, index) => {
|
|
389
|
+
const trimmed = line.trimStart();
|
|
390
|
+
|
|
391
|
+
// Function definitions
|
|
392
|
+
const fnMatch = trimmed.match(/^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/);
|
|
393
|
+
if (fnMatch) {
|
|
394
|
+
symbols.push({
|
|
395
|
+
name: fnMatch[1],
|
|
396
|
+
kind: SYMBOL_KINDS.FUNCTION,
|
|
397
|
+
line: index + 1
|
|
398
|
+
});
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Struct definitions
|
|
403
|
+
const structMatch = trimmed.match(/^(?:pub\s+)?struct\s+(\w+)/);
|
|
404
|
+
if (structMatch) {
|
|
405
|
+
symbols.push({
|
|
406
|
+
name: structMatch[1],
|
|
407
|
+
kind: SYMBOL_KINDS.CLASS,
|
|
408
|
+
line: index + 1
|
|
409
|
+
});
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Impl blocks
|
|
414
|
+
const implMatch = trimmed.match(/^impl(?:<[^>]+>)?\s+(\w+)/);
|
|
415
|
+
if (implMatch) {
|
|
416
|
+
symbols.push({
|
|
417
|
+
name: `impl ${implMatch[1]}`,
|
|
418
|
+
kind: SYMBOL_KINDS.CLASS,
|
|
419
|
+
line: index + 1
|
|
420
|
+
});
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Enum definitions
|
|
425
|
+
const enumMatch = trimmed.match(/^(?:pub\s+)?enum\s+(\w+)/);
|
|
426
|
+
if (enumMatch) {
|
|
427
|
+
symbols.push({
|
|
428
|
+
name: enumMatch[1],
|
|
429
|
+
kind: SYMBOL_KINDS.TYPE,
|
|
430
|
+
line: index + 1
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
return symbols;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Parse Java/Kotlin symbols
|
|
440
|
+
*/
|
|
441
|
+
function parseJavaKotlin(lines) {
|
|
442
|
+
const symbols = [];
|
|
443
|
+
|
|
444
|
+
lines.forEach((line, index) => {
|
|
445
|
+
const trimmed = line.trimStart();
|
|
446
|
+
|
|
447
|
+
// Class declarations
|
|
448
|
+
const classMatch = trimmed.match(/^(?:public\s+|private\s+|protected\s+)?(?:abstract\s+|final\s+)?(?:data\s+)?class\s+(\w+)/);
|
|
449
|
+
if (classMatch) {
|
|
450
|
+
symbols.push({
|
|
451
|
+
name: classMatch[1],
|
|
452
|
+
kind: SYMBOL_KINDS.CLASS,
|
|
453
|
+
line: index + 1
|
|
454
|
+
});
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Interface declarations
|
|
459
|
+
const interfaceMatch = trimmed.match(/^(?:public\s+|private\s+)?interface\s+(\w+)/);
|
|
460
|
+
if (interfaceMatch) {
|
|
461
|
+
symbols.push({
|
|
462
|
+
name: interfaceMatch[1],
|
|
463
|
+
kind: SYMBOL_KINDS.INTERFACE,
|
|
464
|
+
line: index + 1
|
|
465
|
+
});
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Method declarations (simplified)
|
|
470
|
+
const methodMatch = trimmed.match(/^(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:suspend\s+)?(?:fun\s+)?(?:\w+\s+)?(\w+)\s*\([^)]*\)\s*[:{]/);
|
|
471
|
+
if (methodMatch && !['if', 'for', 'while', 'switch', 'catch', 'class', 'interface'].includes(methodMatch[1])) {
|
|
472
|
+
symbols.push({
|
|
473
|
+
name: methodMatch[1],
|
|
474
|
+
kind: SYMBOL_KINDS.METHOD,
|
|
475
|
+
line: index + 1
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
return symbols;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Parse PHP symbols
|
|
485
|
+
*/
|
|
486
|
+
function parsePHP(lines) {
|
|
487
|
+
const symbols = [];
|
|
488
|
+
|
|
489
|
+
lines.forEach((line, index) => {
|
|
490
|
+
const trimmed = line.trimStart();
|
|
491
|
+
|
|
492
|
+
// Function declarations
|
|
493
|
+
const funcMatch = trimmed.match(/^(?:public\s+|private\s+|protected\s+)?(?:static\s+)?function\s+(\w+)/);
|
|
494
|
+
if (funcMatch) {
|
|
495
|
+
symbols.push({
|
|
496
|
+
name: funcMatch[1],
|
|
497
|
+
kind: SYMBOL_KINDS.FUNCTION,
|
|
498
|
+
line: index + 1
|
|
499
|
+
});
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Class declarations
|
|
504
|
+
const classMatch = trimmed.match(/^(?:abstract\s+|final\s+)?class\s+(\w+)/);
|
|
505
|
+
if (classMatch) {
|
|
506
|
+
symbols.push({
|
|
507
|
+
name: classMatch[1],
|
|
508
|
+
kind: SYMBOL_KINDS.CLASS,
|
|
509
|
+
line: index + 1
|
|
510
|
+
});
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Interface declarations
|
|
515
|
+
const interfaceMatch = trimmed.match(/^interface\s+(\w+)/);
|
|
516
|
+
if (interfaceMatch) {
|
|
517
|
+
symbols.push({
|
|
518
|
+
name: interfaceMatch[1],
|
|
519
|
+
kind: SYMBOL_KINDS.INTERFACE,
|
|
520
|
+
line: index + 1
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
return symbols;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Parse C/C++ symbols
|
|
530
|
+
*/
|
|
531
|
+
function parseCpp(lines) {
|
|
532
|
+
const symbols = [];
|
|
533
|
+
|
|
534
|
+
lines.forEach((line, index) => {
|
|
535
|
+
const trimmed = line.trimStart();
|
|
536
|
+
|
|
537
|
+
// Skip preprocessor directives and comments
|
|
538
|
+
if (trimmed.startsWith('#') || trimmed.startsWith('//') || trimmed.startsWith('/*')) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Class/struct declarations
|
|
543
|
+
const classMatch = trimmed.match(/^(?:class|struct)\s+(\w+)/);
|
|
544
|
+
if (classMatch) {
|
|
545
|
+
symbols.push({
|
|
546
|
+
name: classMatch[1],
|
|
547
|
+
kind: SYMBOL_KINDS.CLASS,
|
|
548
|
+
line: index + 1
|
|
549
|
+
});
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Function declarations (simplified - looks for return_type function_name(...))
|
|
554
|
+
const funcMatch = trimmed.match(/^(?:\w+\s+)+(\w+)\s*\([^;]*\)\s*(?:const\s*)?(?:\{|$)/);
|
|
555
|
+
if (funcMatch && !['if', 'for', 'while', 'switch', 'return'].includes(funcMatch[1])) {
|
|
556
|
+
symbols.push({
|
|
557
|
+
name: funcMatch[1],
|
|
558
|
+
kind: SYMBOL_KINDS.FUNCTION,
|
|
559
|
+
line: index + 1
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
return symbols;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Remove duplicate symbols (same name and line)
|
|
569
|
+
*/
|
|
570
|
+
function deduplicateSymbols(symbols) {
|
|
571
|
+
const seen = new Set();
|
|
572
|
+
return symbols.filter(s => {
|
|
573
|
+
const key = `${s.name}:${s.line}`;
|
|
574
|
+
if (seen.has(key)) return false;
|
|
575
|
+
seen.add(key);
|
|
576
|
+
return true;
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Group symbols by kind
|
|
582
|
+
*/
|
|
583
|
+
function groupSymbolsByKind(symbols) {
|
|
584
|
+
const groups = {};
|
|
585
|
+
|
|
586
|
+
symbols.forEach(symbol => {
|
|
587
|
+
if (!groups[symbol.kind]) {
|
|
588
|
+
groups[symbol.kind] = [];
|
|
589
|
+
}
|
|
590
|
+
groups[symbol.kind].push(symbol);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
return groups;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
module.exports = {
|
|
597
|
+
parseSymbols,
|
|
598
|
+
groupSymbolsByKind,
|
|
599
|
+
SYMBOL_KINDS
|
|
600
|
+
};
|