Skip to content

Commit c063547

Browse files
committed
bug fix
bug fix
1 parent 0206384 commit c063547

16 files changed

Lines changed: 291 additions & 96 deletions

api/handlers_analysis.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (h *Handler) handleDefinition(w http.ResponseWriter, r *http.Request) {
7373
dir = filepath.Join(hroot, dir)
7474
}
7575
glob := q.Get("glob")
76-
useGtags := q.Get("gtags") != "0" && search.GtagsAvailable(dir)
76+
useGtags := q.Get("gtags") != "0" && search.GtagsAvailable(hroot)
7777
var hits []search.DefHit
7878
var err error
7979
if useGtags {
@@ -123,9 +123,9 @@ func (h *Handler) handleHover(w http.ResponseWriter, r *http.Request) {
123123
}
124124
includeChain[file] = true
125125
}
126-
ctx, cancel := context.WithTimeout(r.Context(), 4000*time.Millisecond)
126+
ctx, cancel := context.WithTimeout(r.Context(), 8000*time.Millisecond)
127127
defer cancel()
128-
hits, err := search.FindHover(ctx, word, dir, glob, includeChain)
128+
hits, err := search.FindHover(ctx, word, dir, glob, hroot, includeChain)
129129
if err != nil {
130130
if ctx.Err() != nil {
131131
jsonOK(w, []search.HoverHit{})
@@ -158,7 +158,7 @@ func (h *Handler) handleCallers(w http.ResponseWriter, r *http.Request) {
158158
} else if !filepath.IsAbs(dir) {
159159
dir = filepath.Join(hroot, dir)
160160
}
161-
useGtags := q.Get("gtags") != "0" && search.GtagsAvailable(dir)
161+
useGtags := q.Get("gtags") != "0" && search.GtagsAvailable(hroot)
162162
var hits []search.CallSite
163163
var err error
164164
if useGtags {

graph/store.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ func loadProjectFile(path string) (*ProjectFile, error) {
6666
return nil, fmt.Errorf("no trees in project file")
6767
}
6868
for _, t := range pf.Trees {
69+
if t == nil {
70+
return nil, fmt.Errorf("project file contains nil tree element")
71+
}
6972
if t.Nodes == nil {
7073
t.Nodes = make(map[string]*Node)
7174
}
@@ -114,9 +117,11 @@ func (s *Store) activeTree() *Tree {
114117
}
115118

116119
func (s *Store) treeMetas() []TreeMeta {
117-
metas := make([]TreeMeta, len(s.pf.Trees))
118-
for i, t := range s.pf.Trees {
119-
metas[i] = TreeMeta{ID: t.ID, Name: t.Name}
120+
metas := make([]TreeMeta, 0, len(s.pf.Trees))
121+
for _, t := range s.pf.Trees {
122+
if t != nil {
123+
metas = append(metas, TreeMeta{ID: t.ID, Name: t.Name})
124+
}
120125
}
121126
return metas
122127
}

search/definition.go

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"regexp"
77
"strings"
8+
"sync"
89
)
910

1011
// DefHit は定義箇所の1件。
@@ -42,38 +43,64 @@ func FindDefinitionsN(ctx context.Context, word, dir, glob string, maxPerQuery i
4243
{`^[^\s#/*].*\b` + esc + `\s*\(`, "func"},
4344
}
4445

46+
type partialResult struct {
47+
idx int
48+
hits []DefHit
49+
}
50+
ch := make(chan partialResult, len(queries))
51+
var wg sync.WaitGroup
52+
53+
for i, q := range queries {
54+
wg.Add(1)
55+
go func(idx int, q query) {
56+
defer wg.Done()
57+
if ctx.Err() != nil {
58+
return
59+
}
60+
opts := Options{
61+
Pattern: q.pattern,
62+
Dir: dir,
63+
FileGlob: glob,
64+
Regex: true,
65+
CaseSensitive: true,
66+
ContextLines: -1,
67+
MaxResults: maxPerQuery,
68+
}
69+
matches, err := Search(ctx, opts)
70+
if err != nil {
71+
return
72+
}
73+
var hits []DefHit
74+
for _, m := range matches {
75+
hits = append(hits, DefHit{
76+
File: m.File,
77+
Line: m.Line,
78+
Text: strings.TrimSpace(m.Text),
79+
Kind: q.kind,
80+
})
81+
}
82+
ch <- partialResult{idx, hits}
83+
}(i, q)
84+
}
85+
86+
wg.Wait()
87+
close(ch)
88+
89+
// idx順に並べて重複排除
90+
buckets := make([][]DefHit, len(queries))
91+
for pr := range ch {
92+
buckets[pr.idx] = pr.hits
93+
}
4594
seen := map[string]bool{}
4695
var results []DefHit
47-
48-
for _, q := range queries {
49-
if ctx.Err() != nil {
50-
break
51-
}
52-
opts := Options{
53-
Pattern: q.pattern,
54-
Dir: dir,
55-
FileGlob: glob,
56-
Regex: true,
57-
CaseSensitive: true,
58-
ContextLines: -1,
59-
MaxResults: maxPerQuery,
60-
}
61-
matches, err := Search(ctx, opts)
62-
if err != nil {
63-
continue
64-
}
65-
for _, m := range matches {
66-
key := fmt.Sprintf("%s:%d", m.File, m.Line)
96+
for _, hits := range buckets {
97+
for _, h := range hits {
98+
key := fmt.Sprintf("%s:%d", h.File, h.Line)
6799
if seen[key] {
68100
continue
69101
}
70102
seen[key] = true
71-
results = append(results, DefHit{
72-
File: m.File,
73-
Line: m.Line,
74-
Text: strings.TrimSpace(m.Text),
75-
Kind: q.kind,
76-
})
103+
results = append(results, h)
77104
}
78105
}
79106
return results, nil

search/gtags.go

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,8 @@ func GtagsInPath() bool {
2727
// GtagsIndexed は dir 配下に GTAGS ファイルが存在するか確認する。
2828
func GtagsIndexed(dir string) bool {
2929
gtagsFile := filepath.Join(dir, "GTAGS")
30-
if _, err := os.Stat(gtagsFile); err == nil {
31-
return true
32-
}
33-
// fallback: global -p で確認
34-
c := exec.Command("global", "-p")
35-
c.Dir = dir
36-
return c.Run() == nil
30+
_, err := os.Stat(gtagsFile)
31+
return err == nil
3732
}
3833

3934
// GtagsAvailable は GNU Global が使用可能か(インストール済み + インデックス済み)確認する。
@@ -94,14 +89,28 @@ func GtagsBuildIndex(ctx context.Context, dir string) error {
9489
var stderr bytes.Buffer
9590
cmd.Stderr = &stderr
9691
if err := cmd.Run(); err != nil {
97-
if msg := strings.TrimSpace(stderr.String()); msg != "" {
92+
msg := strings.TrimSpace(stderr.String())
93+
// Windowsで日本語パスを含む場合にgtagsが失敗することがある
94+
if strings.Contains(dir, " ") || isNonASCII(dir) {
95+
return fmt.Errorf("gtags failed (ヒント: 日本語やスペースを含まないパスで試してください): %w: %s", err, msg)
96+
}
97+
if msg != "" {
9898
return fmt.Errorf("%w: %s", err, msg)
9999
}
100100
return err
101101
}
102102
return nil
103103
}
104104

105+
func isNonASCII(s string) bool {
106+
for _, r := range s {
107+
if r > 127 {
108+
return true
109+
}
110+
}
111+
return false
112+
}
113+
105114
// GtagsRebuildIndex は既存インデックスを削除してから gtags で再生成する。
106115
func GtagsRebuildIndex(ctx context.Context, dir string) error {
107116
for _, name := range []string{"GTAGS", "GRTAGS", "GPATH"} {
@@ -156,7 +165,28 @@ func gtagsParseOutput(out []byte, kind, dir string) []DefHit {
156165
return results
157166
}
158167

168+
// implExts は実装ファイルの拡張子セット(ヘッダより優先する)。
169+
var implExts = map[string]bool{
170+
".c": true, ".cpp": true, ".cc": true, ".cxx": true, ".java": true,
171+
}
172+
173+
// preferImplHits は実装ファイルのヒットが1件以上あればヘッダ(.h/.hpp等)を除外して返す。
174+
// 実装ファイルが1件もない場合は全件そのまま返す。
175+
func preferImplHits(hits []DefHit) []DefHit {
176+
var impl []DefHit
177+
for _, h := range hits {
178+
if implExts[strings.ToLower(filepath.Ext(h.File))] {
179+
impl = append(impl, h)
180+
}
181+
}
182+
if len(impl) > 0 {
183+
return impl
184+
}
185+
return hits
186+
}
187+
159188
// GtagsFindDefinitions は GNU Global で word の定義を検索する。
189+
// 宣言(.h)と実装(.c/.cpp)が両方ヒットした場合は実装を優先する。
160190
func GtagsFindDefinitions(ctx context.Context, word, dir string) ([]DefHit, error) {
161191
cmd := exec.CommandContext(ctx, "global", "-xd", word)
162192
cmd.Dir = dir
@@ -167,7 +197,7 @@ func GtagsFindDefinitions(ctx context.Context, word, dir string) ([]DefHit, erro
167197
}
168198
return nil, err
169199
}
170-
return gtagsParseOutput(out, "func", dir), nil
200+
return preferImplHits(gtagsParseOutput(out, "func", dir)), nil
171201
}
172202

173203
// gtagsClassifyKind はファイルの該当行テキストから kind を判定する。

search/hover.go

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,19 @@ type HoverHit struct {
2323
// 検索戦略(ripgrep 時):
2424
// 1. ヘッダ(*.h,*.hpp)のみ検索 → struct/enum/define/typedef はここで完結
2525
// 2. func の宣言しか見つからなかった場合、ソースファイルも追加検索して定義本体を取得
26-
func FindHover(ctx context.Context, word, dir, glob string, includeChain ...map[string]bool) ([]HoverHit, error) {
26+
func FindHover(ctx context.Context, word, dir, glob, root string, includeChain ...map[string]bool) ([]HoverHit, error) {
2727
chain := map[string]bool{}
2828
if len(includeChain) > 0 && includeChain[0] != nil {
2929
chain = includeChain[0]
3030
}
31+
if root == "" {
32+
root = dir
33+
}
3134

3235
var hits []DefHit
3336

3437
// GNU Global が使えるなら定義位置をインデックスから直接取得
35-
if GtagsAvailable(dir) {
38+
if GtagsAvailable(root) {
3639
gHits, err := GtagsFindHoverHits(ctx, word, dir)
3740
if err == nil && len(gHits) > 0 {
3841
hits = gHits
@@ -44,26 +47,42 @@ func FindHover(ctx context.Context, word, dir, glob string, includeChain ...map[
4447
const maxPerQuery = 5
4548
headerGlob := "*.h,*.hpp"
4649

47-
// Phase 1: ヘッダのみ
48-
var err error
49-
hits, err = FindDefinitionsN(ctx, word, dir, headerGlob, maxPerQuery)
50-
if err != nil && ctx.Err() != nil {
51-
return nil, err
50+
type phaseResult struct{ hits []DefHit }
51+
ch1 := make(chan phaseResult, 1)
52+
ch2 := make(chan phaseResult, 1)
53+
54+
// Phase 1 と Phase 2 を並列実行
55+
go func() {
56+
h, _ := FindDefinitionsN(ctx, word, dir, headerGlob, maxPerQuery)
57+
ch1 <- phaseResult{h}
58+
}()
59+
go func() {
60+
if glob == headerGlob {
61+
ch2 <- phaseResult{}
62+
return
63+
}
64+
h, _ := FindDefinitionsN(ctx, word, dir, glob, maxPerQuery)
65+
ch2 <- phaseResult{h}
66+
}()
67+
68+
r1, r2 := <-ch1, <-ch2
69+
if ctx.Err() != nil {
70+
return nil, ctx.Err()
5271
}
5372

54-
// Phase 2: ソースファイルも検索してマージ
55-
if glob != headerGlob && ctx.Err() == nil {
56-
srcHits, _ := FindDefinitionsN(ctx, word, dir, glob, maxPerQuery)
57-
seen := map[string]bool{}
58-
for _, h := range hits {
59-
seen[fmt.Sprintf("%s:%d", h.File, h.Line)] = true
73+
seen := map[string]bool{}
74+
for _, h := range r1.hits {
75+
key := fmt.Sprintf("%s:%d", h.File, h.Line)
76+
if !seen[key] {
77+
seen[key] = true
78+
hits = append(hits, h)
6079
}
61-
for _, h := range srcHits {
62-
key := fmt.Sprintf("%s:%d", h.File, h.Line)
63-
if !seen[key] {
64-
hits = append(hits, h)
65-
seen[key] = true
66-
}
80+
}
81+
for _, h := range r2.hits {
82+
key := fmt.Sprintf("%s:%d", h.File, h.Line)
83+
if !seen[key] {
84+
seen[key] = true
85+
hits = append(hits, h)
6786
}
6887
}
6988
}

search/include.go

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,42 @@ import (
1010
"path/filepath"
1111
"regexp"
1212
"strings"
13+
"sync"
14+
"time"
1315
)
1416

1517
var reLocalInclude = regexp.MustCompile(`#\s*include\s*"([^"]+)"`)
1618
var reSystemInclude = regexp.MustCompile(`#\s*include\s*<([^>]+)>`)
1719

20+
// headerIndexCache はディレクトリごとのヘッダサフィックスインデックスのキャッシュ。
21+
type headerIndexEntry struct {
22+
index map[string]string
23+
expiresAt time.Time
24+
}
25+
26+
var (
27+
_headerIndexMu sync.Mutex
28+
_headerIndexCache = map[string]headerIndexEntry{}
29+
)
30+
31+
const _headerIndexTTL = 5 * time.Minute
32+
33+
func cachedHeaderSuffixIndex(ctx context.Context, dir string) map[string]string {
34+
_headerIndexMu.Lock()
35+
if e, ok := _headerIndexCache[dir]; ok && time.Now().Before(e.expiresAt) {
36+
_headerIndexMu.Unlock()
37+
return e.index
38+
}
39+
_headerIndexMu.Unlock()
40+
41+
idx := buildHeaderSuffixIndex(ctx, dir)
42+
43+
_headerIndexMu.Lock()
44+
_headerIndexCache[dir] = headerIndexEntry{index: idx, expiresAt: time.Now().Add(_headerIndexTTL)}
45+
_headerIndexMu.Unlock()
46+
return idx
47+
}
48+
1849
// IncludeNode はインクルードグラフのノード(ファイル)。
1950
type IncludeNode struct {
2051
ID string `json:"id"` // dir からの相対パス
@@ -84,8 +115,8 @@ func BuildIncludeGraph(ctx context.Context, dir, glob string) (*IncludeGraph, er
84115
glob = "*.c,*.h,*.cpp,*.hpp,*.cc"
85116
}
86117

87-
// プロジェクト内ヘッダのサフィックスインデックスを構築
88-
headerIndex := buildHeaderSuffixIndex(ctx, dir)
118+
// プロジェクト内ヘッダのサフィックスインデックスを構築(TTLキャッシュ)
119+
headerIndex := cachedHeaderSuffixIndex(ctx, dir)
89120

90121
args := []string{"--json"}
91122
for _, g := range strings.FieldsFunc(glob, func(r rune) bool { return r == ' ' || r == ',' }) {
@@ -215,8 +246,8 @@ func GetFileIncludes(absFile, root string) ([]IncludeNode, error) {
215246
return nil, err
216247
}
217248

218-
// <...> 解決用にヘッダインデックスを構築
219-
headerIndex := buildHeaderSuffixIndex(context.Background(), root)
249+
// <...> 解決用にヘッダインデックスを構築(TTLキャッシュ)
250+
headerIndex := cachedHeaderSuffixIndex(context.Background(), root)
220251

221252
fileDir := filepath.Dir(absFile)
222253
seen := map[string]bool{}

0 commit comments

Comments
 (0)