@@ -10,8 +10,10 @@ use rayon::prelude::*;
1010use regex:: Regex ;
1111use tree_sitter:: { Language , Parser , Query , QueryCursor , StreamingIterator } ;
1212
13+ use crate :: domain:: diff:: SymbolDiff ;
1314use crate :: domain:: { CodeSymbol , FileChange , SymbolKind } ;
1415use crate :: error:: Result ;
16+ use crate :: services:: differ:: AstDiffer ;
1517
1618// ─── Embedded query patterns ────────────────────────────────────────────────
1719
@@ -142,11 +144,11 @@ impl AnalyzerService {
142144 changes : & [ FileChange ] ,
143145 staged_content : & HashMap < PathBuf , String > ,
144146 head_content : & HashMap < PathBuf , String > ,
145- ) -> Vec < CodeSymbol > {
146- changes
147+ ) -> ( Vec < CodeSymbol > , Vec < SymbolDiff > ) {
148+ let pairs : Vec < ( Vec < CodeSymbol > , Vec < SymbolDiff > ) > = changes
147149 . par_iter ( )
148150 . filter ( |change| !change. is_binary )
149- . flat_map ( |change| {
151+ . map ( |change| {
150152 let ext = change
151153 . path
152154 . extension ( )
@@ -224,7 +226,15 @@ impl AnalyzerService {
224226 } )
225227 . unwrap_or_default ( )
226228 } )
227- . collect ( )
229+ . collect ( ) ;
230+
231+ let mut all_symbols = Vec :: new ( ) ;
232+ let mut all_diffs = Vec :: new ( ) ;
233+ for ( syms, diffs) in pairs {
234+ all_symbols. extend ( syms) ;
235+ all_diffs. extend ( diffs) ;
236+ }
237+ ( all_symbols, all_diffs)
228238 }
229239
230240 fn extract_for_file (
@@ -233,17 +243,18 @@ impl AnalyzerService {
233243 hunks : & [ DiffHunk ] ,
234244 staged_content : & HashMap < PathBuf , String > ,
235245 head_content : & HashMap < PathBuf , String > ,
236- ) -> Vec < CodeSymbol > {
246+ ) -> ( Vec < CodeSymbol > , Vec < SymbolDiff > ) {
237247 let mut parser = Parser :: new ( ) ;
238248 if parser. set_language ( & config. language ) . is_err ( ) {
239- return Vec :: new ( ) ;
249+ return ( Vec :: new ( ) , Vec :: new ( ) ) ;
240250 }
241251
242252 let Ok ( query) = Query :: new ( & config. language , config. query_source ) else {
243- return Vec :: new ( ) ;
253+ return ( Vec :: new ( ) , Vec :: new ( ) ) ;
244254 } ;
245255
246- let mut symbols = Vec :: new ( ) ;
256+ let mut staged_symbols = Vec :: new ( ) ;
257+ let mut head_symbols = Vec :: new ( ) ;
247258
248259 // Parse staged (new) file content
249260 if let Some ( content) = staged_content. get ( & change. path ) {
@@ -256,7 +267,7 @@ impl AnalyzerService {
256267 hunks,
257268 true ,
258269 ) ;
259- symbols . extend ( changed) ;
270+ staged_symbols = changed;
260271 }
261272
262273 // Parse HEAD (old) file content
@@ -270,10 +281,92 @@ impl AnalyzerService {
270281 hunks,
271282 false ,
272283 ) ;
273- symbols . extend ( changed) ;
284+ head_symbols = changed;
274285 }
275286
276- symbols
287+ // Run AstDiffer on modified symbols (F-002: must run while Trees are alive)
288+ let mut diffs = Vec :: new ( ) ;
289+ if let ( Some ( staged_src) , Some ( head_src) ) = (
290+ staged_content. get ( & change. path ) ,
291+ head_content. get ( & change. path ) ,
292+ ) {
293+ let mut diff_parser = Parser :: new ( ) ;
294+ if diff_parser. set_language ( & config. language ) . is_ok ( )
295+ && let ( Some ( staged_tree) , Some ( head_tree) ) = (
296+ diff_parser. parse ( staged_src. as_str ( ) , None ) ,
297+ diff_parser. parse ( head_src. as_str ( ) , None ) ,
298+ )
299+ {
300+ for staged_sym in & staged_symbols {
301+ if let Some ( head_sym) = head_symbols
302+ . iter ( )
303+ . find ( |h| h. kind == staged_sym. kind && h. name == staged_sym. name )
304+ && let ( Some ( new_node) , Some ( old_node) ) = (
305+ Self :: find_node_at_line ( & staged_tree, staged_sym. line ) ,
306+ Self :: find_node_at_line ( & head_tree, head_sym. line ) ,
307+ )
308+ {
309+ let is_function =
310+ matches ! ( staged_sym. kind, SymbolKind :: Function | SymbolKind :: Method ) ;
311+ let changes = if is_function {
312+ AstDiffer :: diff_function ( old_node, head_src, new_node, staged_src)
313+ } else {
314+ AstDiffer :: diff_struct ( old_node, head_src, new_node, staged_src)
315+ } ;
316+ if !changes. is_empty ( ) {
317+ diffs. push ( SymbolDiff {
318+ name : staged_sym. name . clone ( ) ,
319+ file : staged_sym. file . clone ( ) ,
320+ line : staged_sym. line ,
321+ parent_scope : staged_sym. parent_scope . clone ( ) ,
322+ changes,
323+ } ) ;
324+ }
325+ }
326+ }
327+ }
328+ }
329+
330+ let mut all_symbols = staged_symbols;
331+ all_symbols. extend ( head_symbols) ;
332+ ( all_symbols, diffs)
333+ }
334+
335+ /// Find a definition node (function/struct/etc) at the given 1-based line number.
336+ /// Recurses into container nodes (impl blocks, class bodies) to find methods.
337+ fn find_node_at_line ( tree : & tree_sitter:: Tree , line : usize ) -> Option < tree_sitter:: Node < ' _ > > {
338+ let root = tree. root_node ( ) ;
339+ let target_row = line. saturating_sub ( 1 ) ; // tree-sitter uses 0-based rows
340+ Self :: find_node_at_row ( root, target_row)
341+ }
342+
343+ fn find_node_at_row (
344+ parent : tree_sitter:: Node < ' _ > ,
345+ target_row : usize ,
346+ ) -> Option < tree_sitter:: Node < ' _ > > {
347+ for i in 0 ..parent. child_count ( ) {
348+ #[ allow( clippy:: cast_possible_truncation) ]
349+ if let Some ( child) = parent. child ( i as u32 )
350+ && child. start_position ( ) . row <= target_row
351+ && child. end_position ( ) . row >= target_row
352+ {
353+ // If this is a container (impl, class, etc.), recurse into it
354+ if matches ! (
355+ child. kind( ) ,
356+ "impl_item"
357+ | "class_declaration"
358+ | "class_definition"
359+ | "declaration_list"
360+ | "class_body"
361+ | "interface_body"
362+ ) && let Some ( inner) = Self :: find_node_at_row ( child, target_row)
363+ {
364+ return Some ( inner) ;
365+ }
366+ return Some ( child) ;
367+ }
368+ }
369+ None
277370 }
278371
279372 fn extract_changed_symbols_with_query (
0 commit comments