@@ -48,6 +48,7 @@ func main() {
4848
4949 // API routes
5050 router .GET ("/api/relations" , handleRelations )
51+ router .GET ("/api/search" , handleSearch )
5152 router .POST ("/api/reload" , func (c * gin.Context ) {
5253 if err := load (repoPath ); err != nil {
5354 c .JSON (http .StatusInternalServerError , gin.H {"error" : err .Error ()})
@@ -290,6 +291,127 @@ func handleRelations(c *gin.Context) {
290291 })
291292}
292293
294+ // handleSearch searches for functions by name and returns their dependency closure with pagination
295+ // Query params: q (search query), page (1-based), pageSize
296+ // Response: { query, page, pageSize, totalResults, matchingFunctions: [...], data: [OutRelation ...] }
297+ func handleSearch (c * gin.Context ) {
298+ global .mu .RLock ()
299+ defer global .mu .RUnlock ()
300+
301+ query := strings .TrimSpace (c .Query ("q" ))
302+ if query == "" {
303+ c .JSON (http .StatusBadRequest , gin.H {"error" : "search query 'q' is required" })
304+ return
305+ }
306+
307+ // Parse pagination parameters
308+ page := parseInt (c .Query ("page" ), 1 )
309+ pageSize := parseInt (c .Query ("pageSize" ), 10 )
310+ if page < 1 {
311+ page = 1
312+ }
313+ if pageSize <= 0 || pageSize > 200 {
314+ pageSize = 10
315+ }
316+
317+ // Convert query to lowercase for case-insensitive search
318+ lowerQuery := strings .ToLower (query )
319+
320+ // First, try to find functions by name only (prioritized search)
321+ var matchingFunctions []analyzer.OutRelation
322+ for _ , rel := range global .relations {
323+ lowerName := strings .ToLower (rel .Name )
324+ if strings .Contains (lowerName , lowerQuery ) {
325+ matchingFunctions = append (matchingFunctions , rel )
326+ }
327+ }
328+
329+ // If no results found in function names, search in both function names and file paths
330+ if len (matchingFunctions ) == 0 {
331+ for _ , rel := range global .relations {
332+ lowerName := strings .ToLower (rel .Name )
333+ lowerPath := strings .ToLower (rel .FilePath )
334+ if strings .Contains (lowerName , lowerQuery ) || strings .Contains (lowerPath , lowerQuery ) {
335+ matchingFunctions = append (matchingFunctions , rel )
336+ }
337+ }
338+ }
339+
340+ // Sort matching functions for consistent pagination
341+ sort .Slice (matchingFunctions , func (i , j int ) bool {
342+ if matchingFunctions [i ].Name == matchingFunctions [j ].Name {
343+ return matchingFunctions [i ].FilePath < matchingFunctions [j ].FilePath
344+ }
345+ return matchingFunctions [i ].Name < matchingFunctions [j ].Name
346+ })
347+
348+ // Apply pagination to matching functions
349+ totalResults := len (matchingFunctions )
350+ start := (page - 1 ) * pageSize
351+ if start > totalResults {
352+ start = totalResults
353+ }
354+ end := start + pageSize
355+ if end > totalResults {
356+ end = totalResults
357+ }
358+ paginatedMatches := matchingFunctions [start :end ]
359+
360+ // Build dependency closure for paginated matching functions
361+ closureMap := make (map [string ]analyzer.OutRelation )
362+ ck := func (name , file string ) string { return name + "|" + file }
363+
364+ var collect func (name , file string )
365+ collect = func (name , file string ) {
366+ k := ck (name , file )
367+ if _ , exists := closureMap [k ]; exists {
368+ return
369+ }
370+ rel , ok := global .index [k ]
371+ if ! ok {
372+ return
373+ }
374+ // Exclude internal functions from search results
375+ if strings .HasPrefix (name , "analyzer." ) || name == "main.findFunctions" || name == "main.load" {
376+ for _ , c := range rel .Called {
377+ collect (c .Name , c .FilePath )
378+ }
379+ return
380+ }
381+ closureMap [k ] = rel
382+ for _ , c := range rel .Called {
383+ collect (c .Name , c .FilePath )
384+ }
385+ }
386+
387+ // Collect closure for each paginated matching function
388+ for _ , match := range paginatedMatches {
389+ collect (match .Name , match .FilePath )
390+ }
391+
392+ // Convert to slice and sort
393+ var closure []analyzer.OutRelation
394+ for _ , v := range closureMap {
395+ closure = append (closure , v )
396+ }
397+ sort .Slice (closure , func (i , j int ) bool {
398+ if closure [i ].Name == closure [j ].Name {
399+ return closure [i ].FilePath < closure [j ].FilePath
400+ }
401+ return closure [i ].Name < closure [j ].Name
402+ })
403+
404+ c .JSON (http .StatusOK , gin.H {
405+ "query" : query ,
406+ "page" : page ,
407+ "pageSize" : pageSize ,
408+ "totalResults" : totalResults ,
409+ "matchingFunctions" : paginatedMatches ,
410+ "data" : closure ,
411+ "loadedAt" : global .loadedAt ,
412+ })
413+ }
414+
293415// Helpers --------------------------------------------------------------------------------
294416
295417func parseInt (s string , def int ) int {
0 commit comments