@@ -433,19 +433,23 @@ fn chunk_language(text: &str, language: ParseableLanguage) -> Result<Vec<Chunk>>
433433 chunks = merge_haskell_functions ( chunks, text) ;
434434 }
435435
436- // Suppress text chunks fully contained by class/method/function chunks for C/C++
437- if matches ! ( language, ParseableLanguage :: C | ParseableLanguage :: Cpp ) {
438- chunks = suppress_contained_text_chunks ( chunks) ;
439- }
440-
441- // Fill gaps between chunks with remainder content
436+ // Fill gaps between chunks with remainder content. This must happen
437+ // BEFORE suppress_contained_text_chunks so that gap-produced Text
438+ // chunks (e.g., the run of field declarations and access specifiers
439+ // inside a class body) are also subject to suppression — otherwise
440+ // they leak through as standalone Text chunks (issue #136).
442441 chunks = fill_gaps ( chunks, text) ;
443442
444443 // Merge template-prefix gap chunks into the following C++ definition chunk
445444 if language == ParseableLanguage :: Cpp {
446445 chunks = merge_cpp_template_prefix_chunks ( chunks, text) ;
447446 }
448447
448+ // Suppress text chunks fully contained by class/method/function chunks for C/C++
449+ if matches ! ( language, ParseableLanguage :: C | ParseableLanguage :: Cpp ) {
450+ chunks = suppress_contained_text_chunks ( chunks) ;
451+ }
452+
449453 // Merge small chunks if Markdown
450454 if language == ParseableLanguage :: Markdown {
451455 let ( target_tokens, _) = get_model_chunk_config ( None ) ;
@@ -506,7 +510,16 @@ fn merge_cpp_template_prefix_chunks(chunks: Vec<Chunk>, text: &str) -> Vec<Chunk
506510 let template_chunk = & chunks[ idx] ;
507511 let mut next_chunk = chunks[ idx + 1 ] . clone ( ) ;
508512
509- if template_chunk. span . byte_end == next_chunk. span . byte_start
513+ // Adjacency: byte_end may be <= next.byte_start, with only
514+ // whitespace in between. Previously required strict equality
515+ // which missed multiline templates and blank lines between
516+ // the template clause and the definition (issue #136).
517+ let gap_is_whitespace = template_chunk. span . byte_end <= next_chunk. span . byte_start
518+ && text
519+ . get ( template_chunk. span . byte_end ..next_chunk. span . byte_start )
520+ . is_some_and ( |gap| gap. chars ( ) . all ( char:: is_whitespace) ) ;
521+
522+ if gap_is_whitespace
510523 && template_chunk. span . byte_start < next_chunk. span . byte_end
511524 && next_chunk. span . byte_end <= text. len ( )
512525 {
@@ -3071,6 +3084,147 @@ Trailing paragraph.
30713084 ) ;
30723085 }
30733086
3087+ // ----- Regression tests for issue #136 (C/C++ edge cases) -----
3088+ //
3089+ // These tests intentionally use the full `chunk_text` entry point so
3090+ // they exercise the post-processing pipeline (fill_gaps, suppression,
3091+ // template merge) — not just the raw query layer like the existing
3092+ // C/C++ tests in `query_chunker.rs`.
3093+
3094+ /// Issue #136 #1: gap-produced Text chunks inside a class body must
3095+ /// be suppressed too. Previously `suppress_contained_text_chunks`
3096+ /// ran *before* `fill_gaps`, so field declarations and access
3097+ /// specifiers between methods leaked through as a standalone Text
3098+ /// chunk. Now suppression runs after fill_gaps.
3099+ #[ test]
3100+ fn cpp_class_body_gap_text_is_suppressed ( ) {
3101+ let source = r"
3102+ class Counter {
3103+ public:
3104+ int value;
3105+ int spare;
3106+
3107+ void inc() { value++; }
3108+ void dec() { value--; }
3109+ };
3110+ " ;
3111+ let chunks = chunk_text ( source, Some ( ck_core:: Language :: Cpp ) ) . expect ( "chunk cpp" ) ;
3112+
3113+ // The class itself + the two methods should be present.
3114+ assert ! (
3115+ chunks
3116+ . iter( )
3117+ . any( |c| c. chunk_type == ChunkType :: Class && c. text. contains( "class Counter" ) ) ,
3118+ "expected Class chunk for Counter"
3119+ ) ;
3120+
3121+ // Critical: no Text chunk should contain `int value` or
3122+ // `public:` — those bytes live inside the class span and would
3123+ // be a redundant duplicate of content already in the Class chunk.
3124+ let leaked_field = chunks. iter ( ) . any ( |c| {
3125+ c. chunk_type == ChunkType :: Text
3126+ && ( c. text . contains ( "int value" ) || c. text . contains ( "int spare" ) )
3127+ } ) ;
3128+ let leaked_access = chunks
3129+ . iter ( )
3130+ . any ( |c| c. chunk_type == ChunkType :: Text && c. text . trim ( ) == "public:" ) ;
3131+ assert ! (
3132+ !leaked_field,
3133+ "field declarations leaked as standalone Text chunks: {:?}" ,
3134+ chunks
3135+ . iter( )
3136+ . filter( |c| c. chunk_type == ChunkType :: Text )
3137+ . map( |c| & c. text)
3138+ . collect:: <Vec <_>>( )
3139+ ) ;
3140+ assert ! ( !leaked_access, "access specifier leaked as Text chunk" ) ;
3141+ }
3142+
3143+ /// Issue #136 #3: `template<...>` clauses that aren't byte-adjacent
3144+ /// to their definition (multiline template clause, blank line
3145+ /// between) still merge into a single Cpp definition chunk. The
3146+ /// previous strict-adjacency check missed these.
3147+ #[ test]
3148+ fn cpp_multiline_template_prefix_merges_with_definition ( ) {
3149+ // Multiline template clause AND a blank line before the function.
3150+ let source = r"
3151+ template <
3152+ typename T,
3153+ typename U
3154+ >
3155+
3156+ T identity(T value, U /*ignored*/) {
3157+ return value;
3158+ }
3159+ " ;
3160+ let chunks = chunk_text ( source, Some ( ck_core:: Language :: Cpp ) ) . expect ( "chunk cpp" ) ;
3161+
3162+ // After merge: one chunk that contains BOTH the template clause
3163+ // and the definition. There should not be a separate Text chunk
3164+ // that's just the template prefix.
3165+ let combined = chunks. iter ( ) . find ( |c| {
3166+ matches ! ( c. chunk_type, ChunkType :: Function | ChunkType :: Method )
3167+ && c. text . contains ( "template" )
3168+ && c. text . contains ( "T identity" )
3169+ } ) ;
3170+ assert ! (
3171+ combined. is_some( ) ,
3172+ "expected one Function chunk combining the multiline template clause with its definition; got {:?}" ,
3173+ chunks
3174+ . iter( )
3175+ . map( |c| ( & c. chunk_type, c. text. lines( ) . next( ) . unwrap_or( "" ) ) )
3176+ . collect:: <Vec <_>>( )
3177+ ) ;
3178+
3179+ // And we shouldn't have a separate standalone Text chunk that's
3180+ // only the template clause without the body.
3181+ let orphan_template = chunks. iter ( ) . any ( |c| {
3182+ c. chunk_type == ChunkType :: Text
3183+ && c. text . contains ( "template" )
3184+ && !c. text . contains ( "identity" )
3185+ } ) ;
3186+ assert ! (
3187+ !orphan_template,
3188+ "template clause was emitted as a standalone Text chunk instead of merging"
3189+ ) ;
3190+ }
3191+
3192+ /// Coverage gap from issue #136: an `extern \"C\" { ... }` linkage
3193+ /// specification should not lose its inner functions. We don't
3194+ /// assert breadcrumb shape here — just that the functions are
3195+ /// captured at all and aren't suppressed by the linkage wrapper.
3196+ #[ test]
3197+ fn cpp_extern_c_block_functions_are_captured ( ) {
3198+ let source = r#"
3199+ extern "C" {
3200+ int add(int a, int b) {
3201+ return a + b;
3202+ }
3203+ int sub(int a, int b) {
3204+ return a - b;
3205+ }
3206+ }
3207+ "# ;
3208+ let chunks = chunk_text ( source, Some ( ck_core:: Language :: Cpp ) ) . expect ( "chunk cpp" ) ;
3209+
3210+ let has_add = chunks. iter ( ) . any ( |c| {
3211+ matches ! ( c. chunk_type, ChunkType :: Function | ChunkType :: Method )
3212+ && c. text . contains ( "int add" )
3213+ } ) ;
3214+ let has_sub = chunks. iter ( ) . any ( |c| {
3215+ matches ! ( c. chunk_type, ChunkType :: Function | ChunkType :: Method )
3216+ && c. text . contains ( "int sub" )
3217+ } ) ;
3218+ assert ! (
3219+ has_add && has_sub,
3220+ "expected both `add` and `sub` to be captured inside extern \" C\" ; got {:?}" ,
3221+ chunks
3222+ . iter( )
3223+ . map( |c| ( & c. chunk_type, c. text. lines( ) . next( ) . unwrap_or( "" ) ) )
3224+ . collect:: <Vec <_>>( )
3225+ ) ;
3226+ }
3227+
30743228 #[ test]
30753229 fn test_csharp_query_matches_legacy ( ) {
30763230 let source = r"
0 commit comments