@@ -15,13 +15,17 @@ pub struct DumpOptions {
1515 pub show_locations : bool ,
1616 /// Whether to include source text for leaf nodes.
1717 pub show_content : bool ,
18+ /// Whether to include each node's source range with direct-child ranges
19+ /// replaced by their field names in `⟨angle brackets⟩`.
20+ pub show_abridged_source : bool ,
1821}
1922
2023impl Default for DumpOptions {
2124 fn default ( ) -> Self {
2225 Self {
2326 show_locations : false ,
2427 show_content : true ,
28+ show_abridged_source : false ,
2529 }
2630 }
2731}
@@ -226,6 +230,10 @@ fn dump_node(
226230 }
227231 }
228232
233+ if options. show_abridged_source {
234+ write_source_skeleton ( ast, node, source, out) ;
235+ }
236+
229237 if let Some ( context) = type_check {
230238 if let Some ( err) =
231239 type_error_for_node ( context. schema , node, context. expected , context. parent_field )
@@ -409,6 +417,10 @@ fn dump_node_inline(
409417 }
410418 }
411419
420+ if options. show_abridged_source {
421+ write_source_skeleton ( ast, node, source, out) ;
422+ }
423+
412424 if let Some ( context) = type_check {
413425 if let Some ( err) =
414426 type_error_for_node ( context. schema , node, context. expected , context. parent_field )
@@ -424,6 +436,192 @@ fn is_leaf(node: &Node) -> bool {
424436 node. fields . is_empty ( )
425437}
426438
439+ enum SourceSkeleton {
440+ Missing ,
441+ Text ( String ) ,
442+ Invalid ( String ) ,
443+ }
444+
445+ fn write_source_skeleton ( ast : & Ast , node : & Node , source : & str , out : & mut String ) {
446+ match source_skeleton ( ast, node, source) {
447+ SourceSkeleton :: Missing => write ! ( out, " source=<no location>" ) . unwrap ( ) ,
448+ SourceSkeleton :: Text ( text) => write ! ( out, " source={text:?}" ) . unwrap ( ) ,
449+ SourceSkeleton :: Invalid ( error) => write ! ( out, " source=<invalid: {error}>" ) . unwrap ( ) ,
450+ }
451+ }
452+
453+ fn node_source_range ( node : & Node ) -> Option < crate :: Range > {
454+ match node. content {
455+ NodeContent :: Range ( range) => Some ( range) ,
456+ _ => node. source_range ,
457+ }
458+ }
459+
460+ fn source_skeleton ( ast : & Ast , node : & Node , source : & str ) -> SourceSkeleton {
461+ let Some ( parent) = node_source_range ( node) else {
462+ return SourceSkeleton :: Missing ;
463+ } ;
464+ let parent = parent. start_byte ..parent. end_byte ;
465+ if parent. start > parent. end || source. get ( parent. clone ( ) ) . is_none ( ) {
466+ return SourceSkeleton :: Invalid ( format ! (
467+ "node range {}..{} is outside the source or not on UTF-8 boundaries" ,
468+ parent. start, parent. end
469+ ) ) ;
470+ }
471+
472+ let mut children = Vec :: new ( ) ;
473+ for ( field_id, child_ids) in & node. fields {
474+ let field_name = if * field_id == CHILD_FIELD {
475+ "child"
476+ } else {
477+ ast. field_name_for_id ( * field_id) . unwrap_or ( "?" )
478+ } ;
479+ for child_id in child_ids {
480+ let Some ( child) = ast. get_node ( * child_id) else {
481+ continue ;
482+ } ;
483+ let Some ( child) = node_source_range ( child) else {
484+ continue ;
485+ } ;
486+ let child = child. start_byte ..child. end_byte ;
487+ if child. start == child. end {
488+ continue ;
489+ }
490+ if child. start > child. end || source. get ( child. clone ( ) ) . is_none ( ) {
491+ return SourceSkeleton :: Invalid ( format ! (
492+ "child range {}..{} is outside the source or not on UTF-8 boundaries" ,
493+ child. start, child. end
494+ ) ) ;
495+ }
496+ if child. start < parent. start || child. end > parent. end {
497+ return SourceSkeleton :: Invalid ( format ! (
498+ "child range {}..{} is outside node range {}..{}" ,
499+ child. start, child. end, parent. start, parent. end
500+ ) ) ;
501+ }
502+ children. push ( ( child, field_name) ) ;
503+ }
504+ }
505+ children. sort_by_key ( |( range, field_name) | ( range. start , range. end , * field_name) ) ;
506+
507+ let mut result = String :: new ( ) ;
508+ let mut cursor = parent. start ;
509+ for ( range, field_name) in children {
510+ if cursor < range. start {
511+ result. push_str ( source. get ( cursor..range. start ) . unwrap ( ) ) ;
512+ }
513+ result. push ( '⟨' ) ;
514+ result. push_str ( field_name) ;
515+ result. push ( '⟩' ) ;
516+ cursor = cursor. max ( range. end ) ;
517+ }
518+ result. push_str ( source. get ( cursor..parent. end ) . unwrap ( ) ) ;
519+ SourceSkeleton :: Text ( result)
520+ }
521+
522+ #[ cfg( test) ]
523+ mod tests {
524+ use super :: * ;
525+ use crate :: { NodeContent , Point , Range } ;
526+ use std:: collections:: BTreeMap ;
527+
528+ fn range ( start : usize , end : usize ) -> Range {
529+ Range {
530+ start_byte : start,
531+ end_byte : end,
532+ start_point : Point :: new ( 0 , start) ,
533+ end_point : Point :: new ( 0 , end) ,
534+ }
535+ }
536+
537+ fn dump_with_children ( source : & str , parent_range : Range , children : & [ ( & str , Range ) ] ) -> String {
538+ let mut ast = Ast :: with_schema ( crate :: schema:: Schema :: new ( ) ) ;
539+ let parent_kind = ast. register_kind ( "parent" ) ;
540+ let child_kind = ast. register_kind ( "child" ) ;
541+ let mut fields = BTreeMap :: new ( ) ;
542+ for ( field_name, range) in children {
543+ let field = ast. register_field ( field_name) ;
544+ let child = ast. create_node_with_range (
545+ child_kind,
546+ NodeContent :: Range ( * range) ,
547+ BTreeMap :: new ( ) ,
548+ true ,
549+ None ,
550+ ) ;
551+ fields. entry ( field) . or_insert_with ( Vec :: new) . push ( child) ;
552+ }
553+ let parent = ast. create_node_with_range (
554+ parent_kind,
555+ NodeContent :: Range ( parent_range) ,
556+ fields,
557+ true ,
558+ None ,
559+ ) ;
560+ ast. set_root ( parent) ;
561+
562+ dump_ast_with_options (
563+ & ast,
564+ parent,
565+ source,
566+ & DumpOptions {
567+ show_locations : false ,
568+ show_content : false ,
569+ show_abridged_source : true ,
570+ } ,
571+ )
572+ }
573+
574+ #[ test]
575+ fn source_skeleton_elides_direct_children_and_ignores_empty_ranges ( ) {
576+ let source = "αbefore(child)afterω" ;
577+ let child_start = source. find ( "child" ) . unwrap ( ) ;
578+ let child_end = child_start + "child" . len ( ) ;
579+ let dump = dump_with_children (
580+ source,
581+ range ( 0 , source. len ( ) ) ,
582+ & [
583+ ( "value" , range ( child_start, child_end) ) ,
584+ ( "marker" , range ( child_start, child_start) ) ,
585+ ] ,
586+ ) ;
587+
588+ assert ! ( dump. starts_with( "parent source=\" αbefore(⟨value⟩)afterω\" \n " ) ) ;
589+ }
590+
591+ #[ test]
592+ fn source_skeleton_keeps_adjacent_child_fields_separate ( ) {
593+ let source = "abcdef" ;
594+ let dump = dump_with_children (
595+ source,
596+ range ( 0 , source. len ( ) ) ,
597+ & [ ( "left" , range ( 1 , 3 ) ) , ( "right" , range ( 3 , 5 ) ) ] ,
598+ ) ;
599+
600+ assert ! ( dump. starts_with( "parent source=\" a⟨left⟩⟨right⟩f\" \n " ) ) ;
601+ }
602+
603+ #[ test]
604+ fn source_skeleton_keeps_overlapping_child_fields_separate ( ) {
605+ let source = "abcdef" ;
606+ let dump = dump_with_children (
607+ source,
608+ range ( 0 , source. len ( ) ) ,
609+ & [ ( "left" , range ( 1 , 4 ) ) , ( "right" , range ( 3 , 5 ) ) ] ,
610+ ) ;
611+
612+ assert ! ( dump. starts_with( "parent source=\" a⟨left⟩⟨right⟩f\" \n " ) ) ;
613+ }
614+
615+ #[ test]
616+ fn source_skeleton_reports_children_outside_the_parent ( ) {
617+ let source = "abcdefghi" ;
618+ let dump = dump_with_children ( source, range ( 0 , 6 ) , & [ ( "child" , range ( 7 , 9 ) ) ] ) ;
619+
620+ assert ! ( dump
621+ . starts_with( "parent source=<invalid: child range 7..9 is outside node range 0..6>\n " ) ) ;
622+ }
623+ }
624+
427625fn node_content ( node : & Node , source : & str ) -> String {
428626 match & node. content {
429627 NodeContent :: DynamicString ( s) if !s. is_empty ( ) => s. clone ( ) ,
0 commit comments