pi-hashline-edit-pro 7.1.0 → 7.1.1

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.
@@ -170,10 +170,10 @@ export function languageLabel(key: string): string {
170
170
  export async function parseCode(
171
171
  key: string,
172
172
  source: string,
173
- ): Promise<SymbolInfo[]> {
173
+ ): Promise<{ symbols: SymbolInfo[]; hasError: boolean }> {
174
174
  // Markdown uses regex-based parsing, not tree-sitter
175
175
  if (key === "markdown") {
176
- return extractMarkdown(source);
176
+ return { symbols: extractMarkdown(source), hasError: false };
177
177
  }
178
178
 
179
179
  await ensureInit();
@@ -183,9 +183,9 @@ export async function parseCode(
183
183
 
184
184
  sharedParser.setLanguage(lang);
185
185
  const tree = sharedParser.parse(source);
186
- if (!tree) return [];
186
+ if (!tree) return { symbols: [], hasError: false };
187
187
 
188
- return extractSymbols(key, tree.rootNode, source, 0);
188
+ return { symbols: extractSymbols(key, tree.rootNode, source, 0), hasError: tree.rootNode.hasError };
189
189
  }
190
190
 
191
191
  // ---- Symbol extraction dispatch ----
@@ -275,33 +275,16 @@ function extractJavaScriptLike(
275
275
  const child = node.namedChild(i);
276
276
  if (!child) continue;
277
277
 
278
- const symbol = extractJSChild(child, source);
278
+ const symbol = extractJSChild(child, source) ?? (node.type === "program" ? commonJsExportSymbol(child, source) : null);
279
279
  if (!symbol) continue;
280
280
 
281
- const declaration =
282
- child.type === "export_statement"
283
- ? findChildByType(child, [
284
- "class_declaration",
285
- "class",
286
- "abstract_class_declaration",
287
- "interface_declaration",
288
- "enum_declaration",
289
- ]) ?? child
290
- : child;
291
-
292
- // For class/interface/enum, look inside their bodies
293
- if (
294
- symbol.type === "class" ||
295
- symbol.type === "interface" ||
296
- symbol.type === "enum"
297
- ) {
298
- const bodyNode = findChildByType(declaration, [
299
- "class_body",
300
- "interface_body",
301
- "enum_body",
302
- "object_type",
303
- ]);
304
- if (bodyNode && depth < MAX_DEPTH) {
281
+ // Look inside the bodies of container declarations
282
+ if (JS_CONTAINER_TYPES.has(symbol.type ?? "") && depth < MAX_DEPTH) {
283
+ const declaration = jsDeclarationNode(child);
284
+ const bodyNode =
285
+ declaration.childForFieldName("body") ??
286
+ findChildByType(declaration, ["class_body", "interface_body", "enum_body", "object_type", "statement_block"]);
287
+ if (bodyNode) {
305
288
  symbol.children = extractJavaScriptLike(bodyNode, source, depth + 1);
306
289
  }
307
290
  }
@@ -312,23 +295,60 @@ function extractJavaScriptLike(
312
295
  return results;
313
296
  }
314
297
 
298
+ const JS_CONTAINER_TYPES = new Set(["class", "interface", "enum", "namespace"]);
299
+
300
+ /** Unwrap `export`, `declare` and the expression statement TypeScript puts around `namespace`. */
301
+ function jsDeclarationNode(child: Node): Node {
302
+ if (child.type === "export_statement" || child.type === "ambient_declaration" || child.type === "expression_statement") {
303
+ const inner = child.namedChildren.find((c) => c.type !== "decorator" && c.type !== "comment");
304
+ if (inner && inner.type !== "statement_block") return jsDeclarationNode(inner);
305
+ }
306
+ return child;
307
+ }
308
+
315
309
  const JS_SYMBOL_TYPES: Record<string, string> = {
316
310
  function_declaration: "function",
311
+ generator_function_declaration: "function",
312
+ function_signature: "function",
317
313
  function_expression: "function",
318
314
  arrow_function: "arrow_function",
319
315
  method_definition: "method",
316
+ method_signature: "method",
317
+ abstract_method_signature: "method",
320
318
  class_declaration: "class",
321
319
  class: "class",
322
320
  interface_declaration: "interface",
323
321
  enum_declaration: "enum",
322
+ enum_assignment: "enum_member",
324
323
  type_alias_declaration: "type",
325
324
  abstract_class_declaration: "class",
325
+ internal_module: "namespace",
326
+ module: "namespace",
327
+ public_field_definition: "field",
328
+ field_definition: "field",
329
+ property_signature: "field",
326
330
  lexical_declaration: "variable",
327
331
  variable_declaration: "variable",
328
332
  };
329
333
 
330
334
  function isFunctionNode(node: Node): boolean {
331
- return node.type === "arrow_function" || node.type === "function_expression" || node.type === "function";
335
+ return node.type === "arrow_function" || node.type === "function_expression" || node.type === "function" || node.type === "generator_function";
336
+ }
337
+
338
+ const COMMONJS_TARGET = /^(module\.exports(\.[\w$]+)?|exports\.[\w$]+|[\w$]+\.prototype\.[\w$]+)$/;
339
+
340
+ /** Top-level `module.exports = ...`, `exports.x = ...` and `Foo.prototype.m = ...`. */
341
+ function commonJsExportSymbol(child: Node, source: string): SymbolInfo | null {
342
+ if (child.type !== "expression_statement") return null;
343
+ const expr = child.namedChild(0);
344
+ if (!expr || expr.type !== "assignment_expression") return null;
345
+ const left = expr.childForFieldName("left");
346
+ const right = expr.childForFieldName("right");
347
+ if (!left || left.type !== "member_expression") return null;
348
+ const name = source.slice(left.startIndex, left.endIndex);
349
+ if (!COMMONJS_TARGET.test(name)) return null;
350
+ const [sl, el] = nodeRange(child);
351
+ return { name, type: right && isFunctionNode(right) ? "function" : "variable", startLine: sl, endLine: el };
332
352
  }
333
353
 
334
354
  function extractJSChild(
@@ -389,6 +409,10 @@ function extractJSChild(
389
409
  const result = extractJSChild(inner, source);
390
410
  if (result) {
391
411
  if (isDefault && result.name === "<anonymous>") result.name = "default";
412
+ const [sl, el] = nodeRange(child);
413
+ // Keep the row on the `export` line when the declaration starts there.
414
+ if (inner.type === "expression_statement" && result.startLine > sl) result.startLine = sl;
415
+ if (result.endLine < el && inner.type === "expression_statement") result.endLine = el;
392
416
  return result;
393
417
  }
394
418
  }
@@ -396,18 +420,50 @@ function extractJSChild(
396
420
  return null;
397
421
  }
398
422
 
399
- if (type === "expression_statement") return testCallSymbol(child, source);
423
+ // `declare module "x" {}`, `declare function f(): void;`, `declare global {}`
424
+ if (type === "ambient_declaration") {
425
+ const inner = child.namedChildren.find((c) => c.type !== "comment");
426
+ if (!inner) return null;
427
+ const [sl, el] = nodeRange(child);
428
+ if (inner.type === "statement_block") return { name: "global", type: "namespace", startLine: sl, endLine: el };
429
+ const result = extractJSChild(inner, source);
430
+ if (result) {
431
+ result.startLine = sl;
432
+ result.endLine = el;
433
+ }
434
+ return result;
435
+ }
436
+
437
+ if (type === "expression_statement") {
438
+ const inner = child.namedChild(0);
439
+ if (inner?.type === "internal_module") {
440
+ const result = extractJSChild(inner, source);
441
+ if (result) [result.startLine, result.endLine] = nodeRange(child);
442
+ return result;
443
+ }
444
+ return testCallSymbol(child, source);
445
+ }
446
+
447
+ // Bare enum members: `enum E { Red, Green }`
448
+ if (type === "property_identifier" && child.parent?.type === "enum_body") {
449
+ const [sl, el] = nodeRange(child);
450
+ return { name: source.slice(child.startIndex, child.endIndex), type: "enum_member", startLine: sl, endLine: el };
451
+ }
400
452
 
401
453
  const symbolType = JS_SYMBOL_TYPES[type];
402
454
  if (!symbolType) return null;
403
455
 
404
456
  const name =
405
457
  fieldChildText(child, "name", source) ||
458
+ fieldChildText(child, "property", source) ||
406
459
  childText(child, "property_identifier", source) ||
407
460
  childText(child, "type_identifier", source) ||
408
461
  "<anonymous>";
409
462
 
410
- const [sl, el] = nodeRange(child);
463
+ // Decorators on their own lines are children of fields and non-exported classes; start at the declaration.
464
+ const declarationStart = child.children.find((part) => part.type !== "decorator" && part.type !== "comment");
465
+ const [, el] = nodeRange(child);
466
+ const sl = (declarationStart ?? child).startPosition.row + 1;
411
467
 
412
468
  return { name, type: symbolType, startLine: sl, endLine: el };
413
469
  }
@@ -477,30 +533,15 @@ function extractPython(
477
533
 
478
534
  let symbol: SymbolInfo | null = null;
479
535
 
480
- if (child.type === "class_definition") {
481
- const name = fieldChildText(child, "name", source) || "<anonymous>";
482
- const [sl, el] = nodeRange(child);
483
- symbol = { name, type: "class", startLine: sl, endLine: el };
484
-
485
- // Recurse into body for methods
486
- const body = child.childForFieldName("body");
487
- if (body && depth < MAX_DEPTH) {
488
- symbol.children = extractPython(body, source, depth + 1);
489
- }
490
- } else if (child.type === "function_definition") {
491
- const name = fieldChildText(child, "name", source) || "<anonymous>";
492
- const [sl, el] = nodeRange(child);
493
- symbol = { name, type: "function", startLine: sl, endLine: el };
494
- } else if (child.type === "decorated_definition") {
495
- // Unwrap to the inner definition
496
- const def = child.namedChild(child.namedChildCount - 1);
497
- if (def) {
498
- const inner = extractPythonSingle(def, source);
499
- if (inner) {
500
- inner.type = "decorated_" + inner.type;
501
- symbol = inner;
502
- }
503
- }
536
+ if (child.type === "decorated_definition") {
537
+ // The row and span start at the definition line; the decorators above it are excluded.
538
+ const def = child.childForFieldName("definition");
539
+ symbol = def ? extractPythonDefinition(def, source, depth, MAX_DEPTH) : null;
540
+ if (symbol) symbol.type = "decorated_" + symbol.type;
541
+ } else if (child.type === "expression_statement" || child.type === "type_alias_statement") {
542
+ symbol = extractPythonAssignment(child, source);
543
+ } else {
544
+ symbol = extractPythonDefinition(child, source, depth, MAX_DEPTH);
504
545
  }
505
546
 
506
547
  if (symbol) results.push(symbol);
@@ -509,14 +550,21 @@ function extractPython(
509
550
  return results;
510
551
  }
511
552
 
512
- function extractPythonSingle(
553
+ function extractPythonDefinition(
513
554
  child: Node,
514
555
  source: string,
556
+ depth: number,
557
+ maxDepth: number,
515
558
  ): SymbolInfo | null {
516
559
  if (child.type === "class_definition") {
517
560
  const name = fieldChildText(child, "name", source) || "<anonymous>";
518
561
  const [sl, el] = nodeRange(child);
519
- return { name, type: "class", startLine: sl, endLine: el };
562
+ const symbol: SymbolInfo = { name, type: "class", startLine: sl, endLine: el };
563
+ const body = child.childForFieldName("body");
564
+ if (body && depth < maxDepth) {
565
+ symbol.children = extractPython(body, source, depth + 1);
566
+ }
567
+ return symbol;
520
568
  }
521
569
  if (child.type === "function_definition") {
522
570
  const name = fieldChildText(child, "name", source) || "<anonymous>";
@@ -526,6 +574,20 @@ function extractPythonSingle(
526
574
  return null;
527
575
  }
528
576
 
577
+ /** `NAME = ...`, `NAME: T = ...` and `type NAME = ...` at module or class level; other targets are skipped. */
578
+ function extractPythonAssignment(child: Node, source: string): SymbolInfo | null {
579
+ const [sl, el] = nodeRange(child);
580
+ if (child.type === "type_alias_statement") {
581
+ const left = child.namedChild(0);
582
+ return left ? { name: source.slice(left.startIndex, left.endIndex), type: "type", startLine: sl, endLine: el } : null;
583
+ }
584
+ const assignment = child.namedChild(0);
585
+ if (!assignment || assignment.type !== "assignment") return null;
586
+ const left = assignment.childForFieldName("left");
587
+ if (!left || left.type !== "identifier") return null;
588
+ return { name: source.slice(left.startIndex, left.endIndex), type: "variable", startLine: sl, endLine: el };
589
+ }
590
+
529
591
  // ========================================================================
530
592
  // Go
531
593
  // ========================================================================
@@ -562,7 +624,7 @@ function extractGo(
562
624
  const specs: SymbolInfo[] = [];
563
625
  for (let j = 0; j < child.namedChildCount; j++) {
564
626
  const spec = child.namedChild(j);
565
- if (!spec || spec.type !== "type_spec") continue;
627
+ if (!spec || (spec.type !== "type_spec" && spec.type !== "type_alias")) continue;
566
628
  const name = fieldChildText(spec, "name", source) || "<anonymous>";
567
629
  const [sl, el] = nodeRange(spec);
568
630
  const typeNode = spec.childForFieldName("type");
@@ -575,60 +637,62 @@ function extractGo(
575
637
 
576
638
  // Recurse into struct/interface body
577
639
  if (typeNode && depth < MAX_DEPTH) {
578
- const body = findChildByType(typeNode, [
579
- "field_declaration_list",
580
- ]);
640
+ const body = typeNode.type === "interface_type"
641
+ ? typeNode
642
+ : findChildByType(typeNode, ["field_declaration_list"]);
581
643
  if (body) {
582
- entry.children = extractGoFields(body, source);
644
+ const fields = extractGoFields(body, source);
645
+ if (fields.length > 0) entry.children = fields;
583
646
  }
584
647
  }
585
648
 
586
649
  specs.push(entry);
587
650
  }
588
- if (specs.length === 1) {
651
+ if (child.children.some((part) => part.type === "(")) {
652
+ const [sl, el] = nodeRange(child);
653
+ symbol = { name: "type", type: "type", startLine: sl, endLine: el, children: specs };
654
+ } else if (specs.length === 1) {
589
655
  symbol = specs[0];
590
- } else if (specs.length > 1) {
591
- symbol = {
592
- name: "",
593
- type: "type",
594
- startLine: specs[0].startLine,
595
- endLine: specs[specs.length - 1].endLine,
596
- children: specs,
597
- };
598
656
  }
599
657
  if (symbol) results.push(symbol);
600
658
  continue;
601
659
  } else if (child.type === "const_declaration") {
602
660
  const [sl, el] = nodeRange(child);
603
- const names: string[] = [];
661
+ const specs: SymbolInfo[] = [];
604
662
  for (let j = 0; j < child.namedChildCount; j++) {
605
663
  const spec = child.namedChild(j);
606
- if (spec && spec.type === "const_spec") {
607
- const n = fieldChildText(spec, "name", source);
608
- if (n) names.push(n);
609
- }
664
+ if (!spec || spec.type !== "const_spec") continue;
665
+ const name = fieldChildText(spec, "name", source) || "<anonymous>";
666
+ const [specStart, specEnd] = nodeRange(spec);
667
+ specs.push({ name, type: "const", startLine: specStart, endLine: specEnd });
610
668
  }
669
+ const grouped = child.children.some((part) => part.type === "(");
611
670
  symbol = {
612
- name: names.join(", ") || "const",
671
+ name: grouped ? "const" : specs.map((spec) => spec.name).join(", ") || "const",
613
672
  type: "const",
614
673
  startLine: sl,
615
674
  endLine: el,
675
+ ...(grouped ? { children: specs } : {}),
616
676
  };
617
677
  } else if (child.type === "var_declaration") {
618
678
  const [sl, el] = nodeRange(child);
619
- const names: string[] = [];
620
- for (let j = 0; j < child.namedChildCount; j++) {
621
- const spec = child.namedChild(j);
622
- if (spec && spec.type === "var_spec") {
623
- const n = fieldChildText(spec, "name", source);
624
- if (n) names.push(n);
625
- }
679
+ const specs: SymbolInfo[] = [];
680
+ const list = findChildByType(child, ["var_spec_list"]);
681
+ const container = list ?? child;
682
+ for (let j = 0; j < container.namedChildCount; j++) {
683
+ const spec = container.namedChild(j);
684
+ if (!spec || spec.type !== "var_spec") continue;
685
+ const name = fieldChildText(spec, "name", source) || "<anonymous>";
686
+ const [specStart, specEnd] = nodeRange(spec);
687
+ specs.push({ name, type: "var", startLine: specStart, endLine: specEnd });
626
688
  }
689
+ const grouped = list !== null;
627
690
  symbol = {
628
- name: names.join(", ") || "var",
691
+ name: grouped ? "var" : specs.map((spec) => spec.name).join(", ") || "var",
629
692
  type: "var",
630
693
  startLine: sl,
631
694
  endLine: el,
695
+ ...(grouped ? { children: specs } : {}),
632
696
  };
633
697
  }
634
698
 
@@ -646,14 +710,16 @@ function extractGoFields(
646
710
 
647
711
  for (let i = 0; i < node.namedChildCount; i++) {
648
712
  const child = node.namedChild(i);
649
- if (!child || child.type !== "field_declaration") continue;
713
+ if (!child || !["field_declaration", "method_elem", "type_elem"].includes(child.type)) continue;
650
714
 
651
- const name = fieldChildText(child, "name", source);
715
+ const first = child.namedChild(0);
716
+ const name = fieldChildText(child, "name", source) ||
717
+ (first ? source.slice(first.startIndex, first.endIndex) : "");
652
718
  if (!name) continue;
653
719
  const [sl, el] = nodeRange(child);
654
720
  results.push({
655
721
  name,
656
- type: "field",
722
+ type: child.type === "method_elem" ? "method" : "field",
657
723
  startLine: sl,
658
724
  endLine: el,
659
725
  });
@@ -670,6 +736,7 @@ function extractRust(
670
736
  node: Node,
671
737
  source: string,
672
738
  depth: number,
739
+ inImplOrTrait = false,
673
740
  ): SymbolInfo[] {
674
741
  const results: SymbolInfo[] = [];
675
742
  const MAX_DEPTH = depth + 10;
@@ -678,88 +745,77 @@ function extractRust(
678
745
  const child = node.namedChild(i);
679
746
  if (!child) continue;
680
747
 
748
+ const [sl, el] = nodeRange(child);
749
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
681
750
  let symbol: SymbolInfo | null = null;
682
-
683
- if (child.type === "function_item") {
684
- const name = fieldChildText(child, "name", source) || "<anonymous>";
685
- const [sl, el] = nodeRange(child);
686
- symbol = { name, type: "function", startLine: sl, endLine: el };
687
- } else if (child.type === "struct_item") {
688
- const name = fieldChildText(child, "name", source) || "<anonymous>";
689
- const [sl, el] = nodeRange(child);
690
- symbol = { name, type: "struct", startLine: sl, endLine: el };
691
- } else if (child.type === "enum_item") {
692
- const name = fieldChildText(child, "name", source) || "<anonymous>";
693
- const [sl, el] = nodeRange(child);
694
- symbol = { name, type: "enum", startLine: sl, endLine: el };
695
- } else if (child.type === "trait_item") {
696
- const name = fieldChildText(child, "name", source) || "<anonymous>";
697
- const [sl, el] = nodeRange(child);
698
- symbol = { name, type: "trait", startLine: sl, endLine: el };
699
- } else if (child.type === "impl_item") {
700
- const trait = child.childForFieldName("trait");
701
- const implType = child.childForFieldName("type");
702
- const traitStr = trait
703
- ? source.slice(trait.startIndex, trait.endIndex) + " for "
704
- : "";
705
- const typeStr = implType
706
- ? source.slice(implType.startIndex, implType.endIndex)
707
- : "";
708
- const [sl, el] = nodeRange(child);
709
- const name = `impl ${traitStr}${typeStr}`;
710
- symbol = { name, type: "impl", startLine: sl, endLine: el };
711
-
712
- // Recurse into body for associated functions/methods
713
- const body = child.childForFieldName("body");
714
- if (body && depth < MAX_DEPTH) {
715
- symbol.children = extractRustImplBody(body, source);
751
+ // Body whose items become children, and whether its functions are methods.
752
+ let body: Node | null = null;
753
+ let bodyIsImplOrTrait = false;
754
+
755
+ switch (child.type) {
756
+ case "function_item":
757
+ case "function_signature_item":
758
+ symbol = { name, type: inImplOrTrait ? "method" : "function", startLine: sl, endLine: el };
759
+ break;
760
+ case "struct_item":
761
+ case "union_item":
762
+ symbol = { name, type: child.type === "struct_item" ? "struct" : "union", startLine: sl, endLine: el };
763
+ body = child.childForFieldName("body");
764
+ break;
765
+ case "enum_item":
766
+ symbol = { name, type: "enum", startLine: sl, endLine: el };
767
+ body = child.childForFieldName("body");
768
+ break;
769
+ case "field_declaration":
770
+ symbol = { name, type: "field", startLine: sl, endLine: el };
771
+ break;
772
+ case "enum_variant":
773
+ symbol = { name, type: "enum_variant", startLine: sl, endLine: el };
774
+ break;
775
+ case "trait_item":
776
+ symbol = { name, type: "trait", startLine: sl, endLine: el };
777
+ body = child.childForFieldName("body");
778
+ bodyIsImplOrTrait = true;
779
+ break;
780
+ case "impl_item": {
781
+ const trait = child.childForFieldName("trait");
782
+ const implType = child.childForFieldName("type");
783
+ const traitStr = trait ? source.slice(trait.startIndex, trait.endIndex) + " for " : "";
784
+ const typeStr = implType ? source.slice(implType.startIndex, implType.endIndex) : "";
785
+ symbol = { name: `impl ${traitStr}${typeStr}`, type: "impl", startLine: sl, endLine: el };
786
+ body = child.childForFieldName("body");
787
+ bodyIsImplOrTrait = true;
788
+ break;
716
789
  }
717
- } else if (child.type === "type_item") {
718
- const name = fieldChildText(child, "name", source) || "<anonymous>";
719
- const [sl, el] = nodeRange(child);
720
- symbol = { name, type: "type", startLine: sl, endLine: el };
721
- } else if (child.type === "const_item") {
722
- const name = fieldChildText(child, "name", source) || "<anonymous>";
723
- const [sl, el] = nodeRange(child);
724
- symbol = { name, type: "const", startLine: sl, endLine: el };
725
- } else if (child.type === "macro_definition") {
726
- const name = fieldChildText(child, "name", source) || "<anonymous>";
727
- const [sl, el] = nodeRange(child);
728
- symbol = { name: `${name}!`, type: "macro", startLine: sl, endLine: el };
790
+ case "mod_item":
791
+ symbol = { name, type: "module", startLine: sl, endLine: el };
792
+ body = child.childForFieldName("body");
793
+ break;
794
+ case "foreign_mod_item": {
795
+ const modifier = findChildByType(child, ["extern_modifier"]);
796
+ symbol = { name: modifier ? source.slice(modifier.startIndex, modifier.endIndex) : "extern", type: "extern", startLine: sl, endLine: el };
797
+ body = child.childForFieldName("body") ?? findChildByType(child, ["declaration_list"]);
798
+ break;
799
+ }
800
+ case "type_item":
801
+ case "associated_type":
802
+ symbol = { name, type: "type", startLine: sl, endLine: el };
803
+ break;
804
+ case "const_item":
805
+ symbol = { name, type: "const", startLine: sl, endLine: el };
806
+ break;
807
+ case "static_item":
808
+ symbol = { name, type: "static", startLine: sl, endLine: el };
809
+ break;
810
+ case "macro_definition":
811
+ symbol = { name: `${name}!`, type: "macro", startLine: sl, endLine: el };
812
+ break;
729
813
  }
730
814
 
731
- if (symbol) results.push(symbol);
732
- }
733
-
734
- return results;
735
- }
736
-
737
- function extractRustImplBody(
738
- node: Node,
739
- source: string,
740
- ): SymbolInfo[] {
741
- const results: SymbolInfo[] = [];
742
-
743
- for (let i = 0; i < node.namedChildCount; i++) {
744
- const child = node.namedChild(i);
745
- if (!child) continue;
746
-
747
- let symbol: SymbolInfo | null = null;
748
-
749
- if (child.type === "function_item") {
750
- const name = fieldChildText(child, "name", source) || "<anonymous>";
751
- const [sl, el] = nodeRange(child);
752
- symbol = { name, type: "method", startLine: sl, endLine: el };
753
- } else if (child.type === "const_item") {
754
- const name = fieldChildText(child, "name", source) || "<anonymous>";
755
- const [sl, el] = nodeRange(child);
756
- symbol = { name, type: "const", startLine: sl, endLine: el };
757
- } else if (child.type === "type_item") {
758
- const name = fieldChildText(child, "name", source) || "<anonymous>";
759
- const [sl, el] = nodeRange(child);
760
- symbol = { name, type: "type", startLine: sl, endLine: el };
815
+ if (symbol && body && depth < MAX_DEPTH) {
816
+ const children = extractRust(body, source, depth + 1, bodyIsImplOrTrait);
817
+ if (children.length > 0) symbol.children = children;
761
818
  }
762
-
763
819
  if (symbol) results.push(symbol);
764
820
  }
765
821
 
@@ -1352,84 +1408,72 @@ function extractSolidityImports(node: Node, source: string): ImportInfo[] {
1352
1408
  */
1353
1409
  function extractMarkdown(source: string): SymbolInfo[] {
1354
1410
  const lines = source.split("\n");
1411
+ // A trailing newline does not start another line.
1412
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
1355
1413
  const results: SymbolInfo[] = [];
1356
-
1414
+
1357
1415
  // Track heading hierarchy for nesting
1358
1416
  const stack: SymbolInfo[] = [];
1359
-
1360
- // First pass: extract headings and build hierarchy
1361
- for (let i = 0; i < lines.length; i++) {
1362
- const line = lines[i];
1363
- const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
1364
-
1365
- if (headingMatch) {
1366
- const level = headingMatch[1].length;
1367
- const title = headingMatch[2].trim();
1368
- const startLine = i + 1; // 1-indexed
1369
-
1370
- const symbol: SymbolInfo = {
1371
- name: title,
1372
- type: `heading${level}`,
1373
- startLine,
1374
- endLine: lines.length, // Will be updated later
1375
- children: [],
1376
- };
1377
-
1378
- // Find parent: pop stack until we find a heading with lower level
1379
- while (stack.length > 0) {
1380
- const parentLevel = parseInt(stack[stack.length - 1].type?.replace('heading', '') || '0', 10);
1381
- if (parentLevel >= level) {
1382
- stack.pop();
1383
- } else {
1384
- break;
1385
- }
1386
- }
1387
-
1388
- // Add to parent or root
1389
- if (stack.length > 0) {
1390
- stack[stack.length - 1].children!.push(symbol);
1391
- } else {
1392
- results.push(symbol);
1393
- }
1394
-
1395
- stack.push(symbol);
1417
+ const addHeading = (level: number, title: string, startLine: number): void => {
1418
+ const symbol: SymbolInfo = { name: title, type: `heading${level}`, startLine, endLine: lines.length, children: [] };
1419
+ while (stack.length > 0 && parseInt(stack[stack.length - 1].type?.replace("heading", "") || "0", 10) >= level) {
1420
+ stack.pop();
1396
1421
  }
1422
+ if (stack.length > 0) stack[stack.length - 1].children!.push(symbol);
1423
+ else results.push(symbol);
1424
+ stack.push(symbol);
1425
+ };
1426
+
1427
+ let fence: { char: string; length: number } | null = null;
1428
+ // Whether the previous line is paragraph text that a setext underline can turn into a heading.
1429
+ let paragraph = false;
1430
+ let i = 0;
1431
+
1432
+ if (lines[0]?.trim() === "---") {
1433
+ const close = lines.findIndex((line, index) => index > 0 && (line.trim() === "---" || line.trim() === "..."));
1434
+ const first = close === -1 ? undefined : lines.slice(1, close).find((line) => {
1435
+ const trimmed = line.trim();
1436
+ return trimmed.length > 0 && !trimmed.startsWith("#");
1437
+ });
1438
+ if (first !== undefined && /^[A-Za-z0-9_.-]+\s*:/.test(first.trim())) i = close + 1;
1397
1439
  }
1398
-
1399
- // Update endLine for each heading: it ends just before the next sibling or parent's next sibling
1400
- updateEndLines(results, lines.length);
1401
-
1402
- // Second pass: extract code blocks and nest them under appropriate headings
1403
- const codeBlockRegex = /^```(\w+)\s*$/gm; // Only match opening ``` with a language
1404
- let match;
1405
- while ((match = codeBlockRegex.exec(source)) !== null) {
1406
- const language = match[1];
1407
- const startLine = source.slice(0, match.index).split("\n").length;
1408
-
1409
- // Find closing ```
1410
- const afterMatch = source.slice(match.index + match[0].length);
1411
- const closingIdx = afterMatch.search(/^```\s*$/m);
1412
- let endLine = startLine + 1;
1413
- if (closingIdx !== -1) {
1414
- endLine = startLine + afterMatch.slice(0, closingIdx).split("\n").length + 1;
1440
+
1441
+ for (; i < lines.length; i++) {
1442
+ const line = lines[i];
1443
+
1444
+ if (fence) {
1445
+ const close = line.match(/^ {0,3}(`{3,}|~{3,})\s*$/);
1446
+ if (close && close[1][0] === fence.char && close[1].length >= fence.length) fence = null;
1447
+ continue;
1415
1448
  }
1416
-
1417
- const codeBlock: SymbolInfo = {
1418
- name: `code_block_${language}`,
1419
- type: "code_block",
1420
- startLine,
1421
- endLine,
1422
- };
1423
-
1424
- // Find the heading that contains this code block
1425
- const parent = findParentHeading(results, startLine);
1426
- if (parent) {
1427
- parent.children!.push(codeBlock);
1428
- } else {
1429
- results.push(codeBlock);
1449
+ const open = line.match(/^ {0,3}(`{3,}|~{3,})/);
1450
+ if (open) {
1451
+ fence = { char: open[1][0], length: open[1].length };
1452
+ paragraph = false;
1453
+ continue;
1430
1454
  }
1455
+
1456
+ const atx = line.match(/^ {0,3}(#{1,6})[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/);
1457
+ if (atx && atx[2].length > 0) {
1458
+ addHeading(atx[1].length, atx[2].trim(), i + 1);
1459
+ paragraph = false;
1460
+ continue;
1461
+ }
1462
+
1463
+ const setext = paragraph ? line.match(/^ {0,3}(=+|-+)[ \t]*$/) : null;
1464
+ if (setext) {
1465
+ addHeading(setext[1][0] === "=" ? 1 : 2, lines[i - 1].trim(), i);
1466
+ paragraph = false;
1467
+ continue;
1468
+ }
1469
+
1470
+ // Blank lines, indented code, quotes, list items and thematic breaks are not setext paragraph text.
1471
+ paragraph = line.trim() !== "" && !/^( {4}|\t| {0,3}(>|[-*+][ \t]|\d+[.)][ \t]|[-*_]{3,}\s*$))/.test(line);
1431
1472
  }
1432
-
1473
+
1474
+ // Update endLine for each heading: it ends just before the next sibling or parent's next sibling
1475
+ updateEndLines(results, lines.length);
1476
+
1433
1477
  return results;
1434
1478
  }
1435
1479
 
@@ -1449,31 +1493,8 @@ function updateEndLines(symbols: SymbolInfo[], totalLines: number): void {
1449
1493
  // Recurse into children
1450
1494
  if (symbol.children && symbol.children.length > 0) {
1451
1495
  updateEndLines(symbol.children, symbol.endLine);
1452
- // The last child's endLine might extend to parent's endLine
1453
- const lastChild = symbol.children[symbol.children.length - 1];
1454
- if (lastChild.endLine < symbol.endLine) {
1455
- lastChild.endLine = symbol.endLine;
1456
- }
1457
- }
1458
- }
1459
- }
1460
-
1461
- /** Find the heading that contains the given line number. */
1462
- function findParentHeading(symbols: SymbolInfo[], line: number): SymbolInfo | null {
1463
- for (const symbol of symbols) {
1464
- if (symbol.type?.startsWith("heading")) {
1465
- // Check if line is within this heading's range
1466
- if (line >= symbol.startLine && line <= symbol.endLine) {
1467
- // Try to find a more specific child
1468
- if (symbol.children && symbol.children.length > 0) {
1469
- const child = findParentHeading(symbol.children, line);
1470
- if (child) return child;
1471
- }
1472
- return symbol;
1473
- }
1474
1496
  }
1475
1497
  }
1476
- return null;
1477
1498
  }
1478
1499
 
1479
1500
  /**