gh-here 3.0.2 → 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.
Files changed (42) hide show
  1. package/.env +0 -0
  2. package/.playwright-mcp/fixed-alignment.png +0 -0
  3. package/.playwright-mcp/fixed-layout.png +0 -0
  4. package/.playwright-mcp/gh-here-home-header-table.png +0 -0
  5. package/.playwright-mcp/gh-here-home.png +0 -0
  6. package/.playwright-mcp/line-selection-multiline.png +0 -0
  7. package/.playwright-mcp/line-selection-test-after.png +0 -0
  8. package/.playwright-mcp/line-selection-test-before.png +0 -0
  9. package/.playwright-mcp/page-2026-01-03T17-58-21-336Z.png +0 -0
  10. package/lib/constants.js +25 -15
  11. package/lib/content-search.js +212 -0
  12. package/lib/error-handler.js +39 -28
  13. package/lib/file-utils.js +438 -287
  14. package/lib/git.js +10 -54
  15. package/lib/gitignore.js +70 -41
  16. package/lib/renderers.js +15 -19
  17. package/lib/server.js +70 -193
  18. package/lib/symbol-parser.js +600 -0
  19. package/package.json +1 -1
  20. package/public/app.js +207 -73
  21. package/public/js/constants.js +50 -34
  22. package/public/js/content-search-handler.js +551 -0
  23. package/public/js/file-viewer.js +437 -0
  24. package/public/js/focus-mode.js +280 -0
  25. package/public/js/inline-search.js +659 -0
  26. package/public/js/modal-manager.js +14 -28
  27. package/public/js/navigation.js +5 -0
  28. package/public/js/symbol-outline.js +454 -0
  29. package/public/js/utils.js +152 -94
  30. package/public/styles.css +2049 -296
  31. package/.claude/settings.local.json +0 -30
  32. package/SAMPLE.md +0 -287
  33. package/lib/validation.js +0 -77
  34. package/public/app.js.backup +0 -1902
  35. package/public/js/draft-manager.js +0 -36
  36. package/public/js/editor-manager.js +0 -159
  37. package/test.js +0 -138
  38. package/tests/draftManager.test.js +0 -241
  39. package/tests/fileTypeDetection.test.js +0 -111
  40. package/tests/httpService.test.js +0 -268
  41. package/tests/languageDetection.test.js +0 -145
  42. 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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gh-here",
3
- "version": "3.0.2",
3
+ "version": "3.1.0",
4
4
  "description": "A local GitHub-like file browser for viewing code",
5
5
  "repository": {
6
6
  "type": "git",