Skip to content

Commit 83fdbe7

Browse files
committed
feat(ctags): macro/static-var highlighting and local variable hover
- Add Universal Ctags indexing (search/ctags.go, api/handlers_ctags.go) - Add ctags UI: index console, status label, API routes (/api/ctags/*) - Add Monaco decorations for static vars, function calls, and macros (editor-c.js) - Add resolveLocalVar for C/C++ local/static variable hover with declaration text - Add ctags fallback in definition and hover search chains - Add tests for resolveLocalVar (test/editor-c.test.js)
1 parent 94bb555 commit 83fdbe7

14 files changed

Lines changed: 1649 additions & 9 deletions

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,15 @@
6767
|------|-----|-------------|
6868
| static 変数 | `#9cdcfe`(水色)+ italic | ファイル内の `static [型] 変数名` 宣言を正規表現で検出し、同名の全出現箇所に適用。`->` `.` の後(メンバーアクセス)は除外 |
6969
| 関数呼び出し | `#dcdcaa`(黄) | `識別子(` パターンを正規表現で検出(`if` `while` `sizeof` 等のキーワードは除外)|
70-
| 定数・マクロ | `#d19a66`(オレンジ) | `[A-Z][A-Z0-9_]{2,}` の ALL_CAPS 識別子を正規表現で検出 |
70+
| 定数・マクロ | `#d19a66`(オレンジ) | ctags インデックスから取得した `#define` マクロ・`enum` 値名を検出(ctags インデックス未生成時は非表示) |
7171

7272
**注意事項(ヒューリスティックの限界)**
7373

7474
- **static 変数**: `->` / `.` を使ったポインタ・構造体メンバーアクセスは除外しますが、複数変数を1行に宣言する `char *a, *b;` の場合、2番目以降の変数は検出されないことがあります
7575
- **関数呼び出し**: 実際に関数かどうかの意味的判断はせず、`識別子(` 形式をすべてハイライトします。関数ポインタ変数の参照なども含まれます
76-
- **定数・マクロ**: `#define` マクロだけでなく `enum` の値(`BPF_ALU` 等)や大文字の定数も同じ色になります。また `likely()` `pr_err()` などの小文字マクロは対象外です
76+
- **定数・マクロ**: ctags インデックスに登録されたシンボルのみ対象。ただし誤検知抑制のため大文字を含まない識別子(`likely` 等)は除外されます
7777

78-
これらは clangd 等の LSP による意味解析ではなく、テキストパターンに基づくヒューリスティックです。誤検知・見逃しがあることを前提として、視覚的な補助として利用してください
78+
static 変数・関数呼び出しはテキストパターンに基づくヒューリスティックです。定数・マクロのみ ctags による意味解析を使用します
7979

8080
### アドオン
8181

api/handlers.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ func NewHandler(store *graph.Store, root string) *Handler {
2121
if search.GtagsAvailable(root) {
2222
search.GtagsCheckStaleAsync(root)
2323
}
24+
if search.CtagsIndexed(root) {
25+
search.CtagsMacroWarmup(root)
26+
}
2427
return h
2528
}
2629

@@ -71,6 +74,10 @@ func (h *Handler) Register(mux *http.ServeMux) {
7174
mux.HandleFunc("/api/gtags/update", h.handleGtagsUpdate)
7275
mux.HandleFunc("/api/gtags/rebuild", h.handleGtagsRebuild)
7376
mux.HandleFunc("/api/gtags/stream", h.handleGtagsStream)
77+
mux.HandleFunc("/api/ctags/status", h.handleCtagsStatus)
78+
mux.HandleFunc("/api/ctags/index", h.handleCtagsIndex)
79+
mux.HandleFunc("/api/ctags/file-symbols", h.handleCtagsFileSymbols)
80+
mux.HandleFunc("/api/ctags/macros", h.handleCtagsMacros)
7481
// [C言語アドオン] 以下の3行を削除するとインクルードグラフAPIが無効になります
7582
mux.HandleFunc("/api/include-graph", h.handleIncludeGraph)
7683
mux.HandleFunc("/api/include-file", h.handleIncludeFile)

api/handlers_analysis.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,15 @@ func (h *Handler) handleDefinition(w http.ResponseWriter, r *http.Request) {
117117
gtagsInstalled := search.GtagsInPath()
118118
gtagsIndexed := search.GtagsIndexed(hroot)
119119
useGtags := gtagsParam && gtagsInstalled && gtagsIndexed
120-
slog.Debug("definition", "word", word, "hroot", hroot, "gtags_param", gtagsParam, "installed", gtagsInstalled, "indexed", gtagsIndexed, "useGtags", useGtags)
120+
useCtagsParam := q.Get("ctags") != "0"
121+
ctagsIndexed := search.CtagsIndexed(hroot)
122+
useCtags := useCtagsParam && ctagsIndexed && !useGtags
123+
slog.Debug("definition", "word", word, "hroot", hroot, "gtags_param", gtagsParam, "installed", gtagsInstalled, "indexed", gtagsIndexed, "useGtags", useGtags, "ctagsIndexed", ctagsIndexed, "useCtags", useCtags)
121124
engine := "rg"
122125
if useGtags {
123126
engine = "gtags"
127+
} else if useCtags {
128+
engine = "ctags"
124129
}
125130
cacheKey := word + "\x00" + dir + "\x00" + glob + "\x00" + engine
126131
if cached, ok := defCacheGet(cacheKey); ok {
@@ -147,8 +152,35 @@ func (h *Handler) handleDefinition(w http.ResponseWriter, r *http.Request) {
147152
e = nil
148153
}
149154
slog.Debug("definition gtags result", "word", word, "hits", len(h), "dir", dir, "elapsed", time.Since(t0))
155+
if len(h) == 0 {
156+
// gtags miss/error → ctags fallback
157+
if search.CtagsIndexed(hroot) {
158+
slog.Debug("definition gtags miss, fallback to ctags", "word", word)
159+
t0 = time.Now()
160+
h, e = search.CtagsFindDefinitions(word, hroot)
161+
eng = "ctags"
162+
slog.Debug("definition ctags fallback result", "word", word, "hits", len(h), "elapsed", time.Since(t0))
163+
}
164+
}
165+
if len(h) == 0 && e == nil {
166+
// ctags も miss → rg fallback
167+
slog.Debug("definition gtags+ctags miss, fallback to rg", "word", word)
168+
t0 = time.Now()
169+
currentFile := q.Get("file")
170+
if currentFile != "" {
171+
h, e = search.FindDefinitionsSmart(r.Context(), word, currentFile, hroot, glob)
172+
} else {
173+
h, e = search.FindDefinitions(r.Context(), word, dir, glob)
174+
}
175+
eng = "rg"
176+
slog.Debug("definition rg fallback result", "word", word, "hits", len(h), "elapsed", time.Since(t0))
177+
}
178+
} else if useCtags {
179+
slog.Debug("definition ctags", "hroot", hroot)
180+
h, e = search.CtagsFindDefinitions(word, hroot)
181+
slog.Debug("definition ctags result", "word", word, "hits", len(h), "elapsed", time.Since(t0))
150182
if len(h) == 0 && e == nil {
151-
slog.Debug("definition gtags miss, fallback to rg", "word", word)
183+
slog.Debug("definition ctags miss, fallback to rg", "word", word)
152184
t0 = time.Now()
153185
currentFile := q.Get("file")
154186
if currentFile != "" {

api/handlers_ctags.go

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package api
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"log/slog"
8+
"net/http"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
12+
"strings"
13+
"time"
14+
15+
"grepnavi/search"
16+
)
17+
18+
// findCtagsBin は Universal Ctags を優先して ctags バイナリのパスを返す。
19+
// Universal Ctags が見つからない場合は PATH 上の ctags を返す。
20+
func findCtagsBin() (string, bool) {
21+
// 候補パスを順に試す(Universal Ctags を優先)
22+
candidates := []string{}
23+
24+
// Scoop のシムパス
25+
if home, err := os.UserHomeDir(); err == nil {
26+
candidates = append(candidates,
27+
filepath.Join(home, "scoop", "shims", "ctags.exe"),
28+
filepath.Join(home, "scoop", "shims", "ctags"),
29+
)
30+
}
31+
32+
// PATH 上の全 ctags を探す
33+
if p, err := exec.LookPath("ctags"); err == nil {
34+
candidates = append(candidates, p)
35+
}
36+
37+
// 各候補で Universal Ctags かチェック
38+
for _, p := range candidates {
39+
if _, err := os.Stat(p); err != nil {
40+
continue
41+
}
42+
out, err := exec.Command(p, "--version").Output()
43+
if err != nil {
44+
continue
45+
}
46+
if strings.Contains(string(out), "Universal Ctags") {
47+
return p, true
48+
}
49+
}
50+
51+
// Universal Ctags が見つからなければ PATH 上の ctags を使う
52+
if p, err := exec.LookPath("ctags"); err == nil {
53+
return p, true
54+
}
55+
return "", false
56+
}
57+
58+
// handleCtagsMacros はファイルに出現するマクロ名をctagsキャッシュとの積集合で返す。
59+
func (h *Handler) handleCtagsMacros(w http.ResponseWriter, r *http.Request) {
60+
h.mu.RLock()
61+
root := h.root
62+
h.mu.RUnlock()
63+
empty := map[string]interface{}{"macros": []string{}, "ready": false, "loading": false}
64+
if !search.CtagsIndexed(root) {
65+
jsonOK(w, map[string]interface{}{"macros": []string{}, "ready": true, "loading": false})
66+
return
67+
}
68+
slog.Debug("ctags-macros request", "root", root)
69+
state := search.CtagsMacroNames(root)
70+
slog.Debug("ctags-macros state", "ready", state.Ready, "loading", state.Loading)
71+
if state.Loading {
72+
jsonOK(w, map[string]interface{}{"macros": []string{}, "ready": false, "loading": true})
73+
return
74+
}
75+
if !state.Ready {
76+
jsonOK(w, empty)
77+
return
78+
}
79+
80+
file := r.URL.Query().Get("file")
81+
if file == "" {
82+
jsonOK(w, empty)
83+
return
84+
}
85+
if !strings.HasPrefix(filepath.Clean(file), filepath.Clean(root)) {
86+
jsonOK(w, empty)
87+
return
88+
}
89+
90+
// ファイルに出現するシンボルをkind別に返す
91+
slog.Debug("ctags-macros file", "file", file)
92+
syms := search.SymbolsInFile(file, state.Symbols)
93+
slog.Debug("ctags-macros result", "macros", len(syms.Macros))
94+
jsonOK(w, map[string]interface{}{"macros": syms.Macros, "ready": true, "loading": false})
95+
}
96+
97+
// handleCtagsFileSymbols は指定ファイルの ctags シンボル一覧を返す。
98+
func (h *Handler) handleCtagsFileSymbols(w http.ResponseWriter, r *http.Request) {
99+
h.mu.RLock()
100+
root := h.root
101+
h.mu.RUnlock()
102+
103+
file := r.URL.Query().Get("file")
104+
if file == "" {
105+
http.Error(w, "file required", http.StatusBadRequest)
106+
return
107+
}
108+
if !search.CtagsIndexed(root) {
109+
jsonOK(w, []search.DefHit{})
110+
return
111+
}
112+
hits, err := search.CtagsSymbolsForFile(file, root)
113+
if err != nil {
114+
jsonOK(w, []search.DefHit{})
115+
return
116+
}
117+
jsonOK(w, hits)
118+
}
119+
120+
func (h *Handler) handleCtagsStatus(w http.ResponseWriter, r *http.Request) {
121+
h.mu.RLock()
122+
root := h.root
123+
h.mu.RUnlock()
124+
_, installed := findCtagsBin()
125+
indexed := search.CtagsIndexed(root)
126+
jsonOK(w, map[string]interface{}{
127+
"installed": installed,
128+
"indexed": indexed,
129+
})
130+
}
131+
132+
func (h *Handler) handleCtagsIndex(w http.ResponseWriter, r *http.Request) {
133+
h.mu.RLock()
134+
root := h.root
135+
h.mu.RUnlock()
136+
137+
flusher, ok := w.(http.Flusher)
138+
if !ok {
139+
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
140+
return
141+
}
142+
w.Header().Set("Content-Type", "text/event-stream")
143+
w.Header().Set("Cache-Control", "no-cache")
144+
w.Header().Set("Connection", "keep-alive")
145+
146+
sendLine := func(line string) {
147+
fmt.Fprintf(w, "data: %s\n\n", line)
148+
flusher.Flush()
149+
}
150+
sendEvent := func(event, data string) {
151+
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, data)
152+
flusher.Flush()
153+
}
154+
155+
slog.Info("ctags-index start", "root", root)
156+
sendLine("--- ctags インデックス生成開始: " + root + " ---")
157+
158+
ctagsBin, ok := findCtagsBin()
159+
if !ok {
160+
sendEvent("ctags-error", "ctags が見つかりません")
161+
return
162+
}
163+
sendLine("使用バイナリ: " + ctagsBin)
164+
165+
var stderrBuf bytes.Buffer
166+
tagsPath := filepath.Join(root, "tags")
167+
cmd := exec.CommandContext(context.Background(), ctagsBin, "-R", "--fields=+n", "-f", tagsPath, root)
168+
cmd.Stderr = &stderrBuf
169+
170+
if err := cmd.Start(); err != nil {
171+
sendEvent("ctags-error", err.Error())
172+
return
173+
}
174+
175+
// ctags は進捗出力がないので1秒ごとにハートビートを送る
176+
done := make(chan error, 1)
177+
go func() { done <- cmd.Wait() }()
178+
ticker := time.NewTicker(time.Second)
179+
defer ticker.Stop()
180+
loop:
181+
for {
182+
select {
183+
case err := <-done:
184+
if err != nil {
185+
msg := err.Error()
186+
if s := stderrBuf.String(); s != "" {
187+
msg += "\nstderr: " + s
188+
}
189+
// exit code 1 は警告扱い(tags ファイルは生成されている場合)
190+
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
191+
sendLine("警告(exit=1): " + msg)
192+
break loop
193+
}
194+
sendEvent("ctags-error", msg)
195+
return
196+
}
197+
break loop
198+
case <-ticker.C:
199+
sendLine("... 生成中")
200+
}
201+
}
202+
203+
search.CtagsMacroWarmup(root)
204+
sendLine("--- 完了 ---")
205+
sendEvent("ctags-done", "ok")
206+
}

api/handlers_fileops.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package api
33
import (
44
"encoding/json"
55
"fmt"
6+
"log/slog"
67
"net"
78
"net/http"
89
"os"
@@ -12,6 +13,8 @@ import (
1213
"strconv"
1314
"strings"
1415
"unicode/utf8"
16+
17+
"grepnavi/search"
1518
)
1619

1720
// --- /api/open ---
@@ -79,6 +82,10 @@ func (h *Handler) handleRoot(w http.ResponseWriter, r *http.Request) {
7982
h.root = abs
8083
h.mu.Unlock()
8184
h.store.SetRootDir(abs)
85+
slog.Debug("root changed", "abs", abs, "ctags_indexed", search.CtagsIndexed(abs))
86+
if search.CtagsIndexed(abs) {
87+
search.CtagsMacroWarmup(abs)
88+
}
8289
jsonOK(w, map[string]string{"root": abs})
8390
default:
8491
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)

api/handlers_graph.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,9 @@ func (h *Handler) handleGraphImport(w http.ResponseWriter, r *http.Request) {
383383
h.mu.Lock()
384384
h.root = g.RootDir
385385
h.mu.Unlock()
386+
if search.CtagsIndexed(g.RootDir) {
387+
search.CtagsMacroWarmup(g.RootDir)
388+
}
386389
}
387390
}
388391
jsonOK(w, map[string]interface{}{"graph": g})
@@ -442,6 +445,9 @@ func (h *Handler) handleGraphOpenFile(w http.ResponseWriter, r *http.Request) {
442445
h.mu.Lock()
443446
h.root = g.RootDir
444447
h.mu.Unlock()
448+
if search.CtagsIndexed(g.RootDir) {
449+
search.CtagsMacroWarmup(g.RootDir)
450+
}
445451
}
446452
}
447453
jsonOK(w, map[string]interface{}{"graph": g, "file_path": req.Path})

0 commit comments

Comments
 (0)