44 "context"
55 "fmt"
66 "log/slog"
7+ "os"
8+ "os/exec"
79 "path/filepath"
810 "regexp"
911 "strings"
@@ -24,6 +26,227 @@ func FindDefinitions(ctx context.Context, word, dir, glob string) ([]DefHit, err
2426 return FindDefinitionsN (ctx , word , dir , glob , 50 )
2527}
2628
29+ // FindDefinitionsSmart はヒューリスティック探索で定義を高速に見つける。
30+ //
31+ // 全探索を同時起動し、近い順に優先して最初のヒットを返す。
32+ //
33+ // level 0: インクルードチェーン + 対応 .c ファイル(意味的な近さ・最優先)
34+ // level 1: currentFile と同じディレクトリ
35+ // level 2: 親ディレクトリ
36+ // ...
37+ // level N: root 全体(フォールバック)
38+ //
39+ // キャッシュは呼び出し元(handleDefinition)が管理する。
40+ func FindDefinitionsSmart (ctx context.Context , word , currentFile , root , glob string ) ([]DefHit , error ) {
41+ if word == "" || root == "" {
42+ return nil , nil
43+ }
44+ t0 := time .Now ()
45+
46+ // walk ディレクトリを近い順にリストアップ
47+ var walkDirs []string
48+ if currentFile != "" {
49+ dir := filepath .Dir (currentFile )
50+ for {
51+ rel , err := filepath .Rel (root , dir )
52+ if err != nil || strings .HasPrefix (rel , ".." ) {
53+ break
54+ }
55+ walkDirs = append (walkDirs , dir )
56+ if dir == root {
57+ break
58+ }
59+ dir = filepath .Dir (dir )
60+ }
61+ }
62+ if len (walkDirs ) == 0 {
63+ walkDirs = []string {root }
64+ }
65+
66+ // level 0: Phase0、level 1〜N: walkDirs
67+ total := 1 + len (walkDirs )
68+
69+ type levelResult struct {
70+ level int
71+ hits []DefHit
72+ err error
73+ }
74+
75+ innerCtx , cancel := context .WithCancel (ctx )
76+ defer cancel ()
77+
78+ ch := make (chan levelResult , total )
79+
80+ // level 0: インクルードチェーン + 対応 .c ファイル
81+ go func () {
82+ var hits []DefHit
83+ var err error
84+ if currentFile != "" {
85+ incs , _ := GetFileIncludes (currentFile , root )
86+ files := make ([]string , 0 , len (incs )+ 1 )
87+ files = append (files , currentFile )
88+ var hFiles []string
89+ for _ , inc := range incs {
90+ if inc .ID != "" {
91+ abs := filepath .Join (root , inc .ID )
92+ files = append (files , abs )
93+ hFiles = append (hFiles , abs )
94+ }
95+ }
96+ cFiles := findSiblingCFiles (innerCtx , root , hFiles )
97+ files = append (files , cFiles ... )
98+ slog .Debug ("FindDefinitionsSmart phase0 files" , "headers" , len (hFiles ), "c_siblings" , len (cFiles ))
99+ if len (files ) > 0 {
100+ hits , err = findDefinitionsInFiles (innerCtx , word , files )
101+ }
102+ }
103+ ch <- levelResult {0 , hits , err }
104+ }()
105+
106+ // level 1〜N: 階層的 walk
107+ for i , d := range walkDirs {
108+ go func (level int , dir string ) {
109+ var hits []DefHit
110+ var err error
111+ if dir == root {
112+ hits , err = FindDefinitionsN (innerCtx , word , root , glob , 50 )
113+ } else {
114+ files := listFilesInDir (dir , glob )
115+ if len (files ) > 0 {
116+ hits , err = findDefinitionsInFiles (innerCtx , word , files )
117+ }
118+ }
119+ ch <- levelResult {level , hits , err }
120+ }(i + 1 , d )
121+ }
122+
123+ // 結果を受け取り、近い順(level 0 最優先)にチェック
124+ received := make ([]levelResult , total )
125+ done := make ([]bool , total )
126+ for count := 0 ; count < total ; count ++ {
127+ r := <- ch
128+ received [r .level ] = r
129+ done [r .level ] = true
130+
131+ for i := 0 ; i < total ; i ++ {
132+ if ! done [i ] {
133+ break // 近いレベルがまだ未完
134+ }
135+ if len (received [i ].hits ) > 0 {
136+ cancel ()
137+ slog .Debug ("FindDefinitionsSmart hit" , "level" , i , "hits" , len (received [i ].hits ), "elapsed" , time .Since (t0 ))
138+ return received [i ].hits , received [i ].err
139+ }
140+ }
141+ }
142+
143+ return nil , nil
144+ }
145+
146+ // listFilesInDir はディレクトリ直下のファイル(サブディレクトリは含まない)を返す。
147+ // glob が指定されている場合はファイル名でフィルタする(例: "*.c,*.h")。
148+ func listFilesInDir (dir , glob string ) []string {
149+ entries , err := os .ReadDir (dir )
150+ if err != nil {
151+ return nil
152+ }
153+ globs := strings .FieldsFunc (glob , func (r rune ) bool { return r == ',' || r == ' ' })
154+ var files []string
155+ for _ , e := range entries {
156+ if e .IsDir () {
157+ continue
158+ }
159+ name := e .Name ()
160+ if len (globs ) == 0 {
161+ files = append (files , filepath .Join (dir , name ))
162+ continue
163+ }
164+ for _ , g := range globs {
165+ if matched , _ := filepath .Match (g , name ); matched {
166+ files = append (files , filepath .Join (dir , name ))
167+ break
168+ }
169+ }
170+ }
171+ return files
172+ }
173+
174+ // findSiblingCFiles は .h/.hpp ファイルのリストから同名の .c ファイルを root 以下で探す。
175+ // 例: include/linux/bpf.h → kernel/bpf/bpf.c
176+ // rg --files -g bpf.c -g filter.c root を1回呼ぶだけなので高速。
177+ func findSiblingCFiles (ctx context.Context , root string , hFiles []string ) []string {
178+ seen := map [string ]bool {}
179+ var args []string
180+ args = append (args , "--files" )
181+ for _ , f := range hFiles {
182+ base := filepath .Base (f )
183+ ext := strings .ToLower (filepath .Ext (base ))
184+ if ext == ".h" || ext == ".hpp" {
185+ cName := strings .TrimSuffix (base , filepath .Ext (base )) + ".c"
186+ if ! seen [cName ] {
187+ seen [cName ] = true
188+ args = append (args , "-g" , cName )
189+ }
190+ }
191+ }
192+ if len (seen ) == 0 {
193+ return nil
194+ }
195+ args = append (args , root )
196+ out , err := exec .CommandContext (ctx , "rg" , args ... ).Output ()
197+ if err != nil {
198+ return nil
199+ }
200+ var result []string
201+ for _ , line := range strings .Split (strings .TrimSpace (string (out )), "\n " ) {
202+ if line != "" {
203+ result = append (result , line )
204+ }
205+ }
206+ return result
207+ }
208+
209+ // findDefinitionsInFiles は特定ファイルリストだけを対象に定義検索する。
210+ func findDefinitionsInFiles (ctx context.Context , word string , files []string ) ([]DefHit , error ) {
211+ esc := regexp .QuoteMeta (word )
212+ combined := `(?:` +
213+ `#\s*define\s+` + esc + `\b` +
214+ `|^\s*(?:typedef\s+)?(?:struct|union)\s+` + esc + `\s*(?:\{|$)` +
215+ `|^\s*(?:typedef\s+)?enum\s+` + esc + `\s*(?:\{|$)` +
216+ `|\btypedef\b.+\b` + esc + `\b\s*;` +
217+ `|^\s*\}\s*` + esc + `\s*;` +
218+ `|^\s+` + esc + `\b\s*[,=]` +
219+ `|^[^\s#/*].*\b` + esc + `\s*\(` +
220+ `)`
221+ matches , err := Search (ctx , Options {
222+ Pattern : combined ,
223+ Files : files ,
224+ Regex : true ,
225+ CaseSensitive : true ,
226+ ContextLines : - 1 ,
227+ MaxResults : 20 ,
228+ })
229+ if err != nil {
230+ return nil , err
231+ }
232+ seen := map [string ]bool {}
233+ var results []DefHit
234+ for _ , m := range matches {
235+ key := fmt .Sprintf ("%s:%d" , m .File , m .Line )
236+ if seen [key ] {
237+ continue
238+ }
239+ seen [key ] = true
240+ results = append (results , DefHit {
241+ File : m .File ,
242+ Line : m .Line ,
243+ Text : strings .TrimSpace (m .Text ),
244+ Kind : classifyDefKind (m .Text , word ),
245+ })
246+ }
247+ return preferDefinitionHits (results ), nil
248+ }
249+
27250// FindDefinitionsN は最大件数を指定できる FindDefinitions。
28251//
29252// 実行戦略:
0 commit comments