Skip to content

Commit d5a6271

Browse files
committed
fix(gtags): make definition jump work on Windows with the bundled global.exe
- Previously, Ctrl+click on a symbol on Windows silently fell back to ripgrep even with a valid GTAGS index, because the bundled Cygwin global.exe cannot write to the native Windows pipe that Go opens for cmd.Output() and returns empty stdout - When that empty-but-successful result is detected, the search is re-run via Cygwin bash with output redirected to a /tmp file, which Go then reads back. Skipped when bash is not on PATH, so Cygwin is not required for users who do not hit the symptom
1 parent 0549dfc commit d5a6271

1 file changed

Lines changed: 142 additions & 14 deletions

File tree

search/gtags.go

Lines changed: 142 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"regexp"
1919
"strconv"
2020
"strings"
21+
"sync"
2122
"sync/atomic"
2223
"time"
2324
"unicode"
@@ -179,20 +180,127 @@ func GtagsUpdateIndex(ctx context.Context, dir string) error {
179180
}
180181

181182
// englishEnv は文字化け防止のため LANG=C を追加した環境変数を返す。
183+
// extra に同名の変数があれば os.Environ() 側の既存値は除去する
184+
// (シェルで GTAGSDBPATH 等を設定されていた場合の重複を防ぐ)。
182185
func englishEnv(extra ...string) []string {
186+
override := map[string]bool{"LANG": true, "LC_ALL": true}
187+
for _, e := range extra {
188+
if i := strings.Index(e, "="); i > 0 {
189+
override[e[:i]] = true
190+
}
191+
}
183192
env := os.Environ()
184-
// LANG/LC_ALL を C に上書きして英語出力に統一
185-
filtered := env[:0:0]
193+
filtered := make([]string, 0, len(env)+len(extra)+2)
186194
for _, e := range env {
187-
if !strings.HasPrefix(e, "LANG=") && !strings.HasPrefix(e, "LC_ALL=") {
188-
filtered = append(filtered, e)
195+
if i := strings.Index(e, "="); i > 0 && override[e[:i]] {
196+
continue
189197
}
198+
filtered = append(filtered, e)
190199
}
191200
filtered = append(filtered, "LANG=C", "LC_ALL=C")
192201
filtered = append(filtered, extra...)
193202
return filtered
194203
}
195204

205+
// gtagsEnv は GTAGSDBPATH / GTAGSROOT を設定した環境変数を返す。
206+
// パスは ToSlash しない: Cygwin ビルドの global.exe は Windows パス
207+
// (バックスラッシュ) を受け付けるが、フォワードスラッシュだと検索が
208+
// 空を返す症状が実機で確認されている。
209+
func gtagsEnv(dir string) []string {
210+
return englishEnv("GTAGSDBPATH="+dir, "GTAGSROOT="+dir)
211+
}
212+
213+
// ===== Cygwin bash フォールバック =====
214+
//
215+
// 同梱の Cygwin ビルド global.exe が native Windows プロセス(Go) が作成した
216+
// pipe に書き込めず stdout が空になる症状への対策。
217+
// Cygwin bash を経由して /tmp に出力させ、それを読み戻すことで回避する。
218+
//
219+
// bash が PATH に無い環境ではフォールバックを無効化し、従来通り cmd.Output()
220+
// の結果(空かもしれない)をそのまま使う。Cygwin 必須化はしない。
221+
222+
var (
223+
_bashOnce sync.Once
224+
_bashPath string // Cygwin bash.exe のフルパス、未検出なら ""
225+
_cygTmpWindowsPath string // Cygwin /tmp の Windows パス、未検出なら ""
226+
)
227+
228+
// initBashRun は bash の検出と /tmp の Windows パス取得を一度だけ実行する。
229+
func initBashRun() {
230+
_bashOnce.Do(func() {
231+
p, err := exec.LookPath("bash")
232+
if err != nil {
233+
slog.Info("gtags-bash-fallback", "msg", "bash not found in PATH, fallback disabled")
234+
return
235+
}
236+
out, err := exec.Command(p, "-c", "cygpath -w /tmp").Output()
237+
if err != nil {
238+
slog.Info("gtags-bash-fallback", "msg", "cygpath failed, fallback disabled", "err", err)
239+
return
240+
}
241+
_bashPath = p
242+
_cygTmpWindowsPath = strings.TrimSpace(string(out))
243+
slog.Info("gtags-bash-fallback", "msg", "ready", "bash", _bashPath, "tmp_win", _cygTmpWindowsPath)
244+
})
245+
}
246+
247+
// windowsToCygwinPath は Windows パス (C:\foo\bar) を
248+
// Cygwin POSIX パス (/cygdrive/c/foo/bar) に変換する。
249+
func windowsToCygwinPath(p string) string {
250+
p = filepath.ToSlash(p)
251+
if len(p) >= 2 && p[1] == ':' {
252+
return "/cygdrive/" + strings.ToLower(p[0:1]) + p[2:]
253+
}
254+
return p
255+
}
256+
257+
// shellQuote は bash 用にシングルクォートで囲む。
258+
// 文字列内の ' を '\'' で escape する標準テクニック。
259+
func shellQuote(s string) string {
260+
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
261+
}
262+
263+
// runGlobalViaBash は global.exe を Cygwin bash 経由で実行し結果バイト列を返す。
264+
// 戻り値の第2引数 attempted が true なら bash 経路を試みた(成功/失敗問わず)。
265+
// false の場合は bash が無いので呼び出し側はフォールバック断念。
266+
//
267+
// mode は global の検索オプション (`-xd` 定義 / `-xr` 参照 等)。
268+
func runGlobalViaBash(globalBin, dir, word, mode string) (data []byte, attempted bool) {
269+
initBashRun()
270+
if _bashPath == "" {
271+
return nil, false
272+
}
273+
tmpName := fmt.Sprintf("grepnavi-gtags-%d-%d.txt", os.Getpid(), time.Now().UnixNano())
274+
cygTmp := "/tmp/" + tmpName
275+
winTmp := filepath.Join(_cygTmpWindowsPath, tmpName)
276+
defer os.Remove(winTmp)
277+
278+
globalCyg := windowsToCygwinPath(globalBin)
279+
// GTAGSDBPATH/GTAGSROOT は Windows パスのまま (Cygwin global は両方解釈する)。
280+
// global.exe の実行パスは Cygwin パス (/cygdrive/...) でなければ bash が認識しない。
281+
// 出力先は Cygwin パス (/tmp/...): bash の > リダイレクトは POSIX パス前提。
282+
cmdStr := fmt.Sprintf("GTAGSDBPATH=%s GTAGSROOT=%s %s %s %s > %s 2>/dev/null",
283+
shellQuote(dir), shellQuote(dir),
284+
shellQuote(globalCyg),
285+
mode,
286+
shellQuote(word),
287+
shellQuote(cygTmp))
288+
289+
cmd := exec.Command(_bashPath, "-c", cmdStr)
290+
// exit=1 は「ヒットなし」なので情報的、それ以外は警告
291+
if err := cmd.Run(); err != nil {
292+
if ee, ok := err.(*exec.ExitError); !ok || ee.ExitCode() != 1 {
293+
slog.Debug("gtags-bash-fallback exec", "err", err, "cmd", cmdStr)
294+
}
295+
}
296+
out, rerr := os.ReadFile(winTmp)
297+
if rerr != nil {
298+
// ファイル無し = 結果なし (global が "no results" で何も書かなかった等)
299+
return nil, true
300+
}
301+
return out, true
302+
}
303+
196304
// sanitizeLine はShift-JIS等の不正バイト列を除去してUTF-8安全な文字列に変換する。
197305
func sanitizeLine(s string) string {
198306
return strings.ToValidUTF8(s, "")
@@ -275,8 +383,9 @@ func GtagsBuildIndexStream(ctx context.Context, dir string, w io.Writer) error {
275383
// Windows ではパイプバッファリングにより出力が遅延するため、5 秒ごとにハートビートを送る。
276384
func GtagsUpdateIndexStream(ctx context.Context, dir string, w io.Writer) error {
277385
cmd := exec.CommandContext(ctx, resolveGlobalBin(), "-u", "-v")
278-
gtagsPathU := filepath.ToSlash(dir)
279-
cmd.Env = englishEnv("GTAGSDBPATH="+gtagsPathU, "GTAGSROOT="+gtagsPathU)
386+
// Cygwin global.exe は Windows パス (バックスラッシュ) を受け付ける。
387+
// ToSlash でフォワードスラッシュ化すると検索が空を返す症状が出るため使わない。
388+
cmd.Env = gtagsEnv(dir)
280389

281390
pr, pw := io.Pipe()
282391
cmd.Stdout = pw
@@ -391,10 +500,7 @@ func GtagsFindDefinitions(ctx context.Context, word, dir string) ([]DefHit, erro
391500
// キャンセルされても結果はキャッシュに入るので次回即返せる。
392501
cmd := exec.CommandContext(context.Background(), globalBin, "-xd", word)
393502
cmd.Dir = dir
394-
// MSYS2版 global.exe はバックスラッシュを解釈できないためフォワードスラッシュに変換する
395-
gtagsPath := filepath.ToSlash(dir)
396-
env := append(os.Environ(), "GTAGSDBPATH="+gtagsPath, "GTAGSROOT="+gtagsPath)
397-
cmd.Env = env
503+
cmd.Env = gtagsEnv(dir)
398504
if devNull, err := os.Open(os.DevNull); err == nil {
399505
cmd.Stdin = devNull
400506
defer devNull.Close()
@@ -433,6 +539,19 @@ func GtagsFindDefinitions(ctx context.Context, word, dir string) ([]DefHit, erro
433539
"stderr", strings.TrimSpace(stderr.String()),
434540
"err", err)
435541

542+
// Cygwin global.exe が native Windows pipe に書けず stdout が空のことがある。
543+
// exit=0 かつ stdout が空 = この症状の可能性 → bash 経由で再実行を試みる。
544+
if err == nil && len(bytes.TrimSpace(out)) == 0 {
545+
if bashOut, attempted := runGlobalViaBash(globalBin, dir, word, "-xd"); attempted {
546+
if len(bytes.TrimSpace(bashOut)) > 0 {
547+
slog.Info("gtags-find", "msg", "bash fallback succeeded", "word", word, "bytes", len(bashOut))
548+
out = bashOut
549+
} else {
550+
slog.Debug("gtags-find", "msg", "bash fallback also empty (truly no results)", "word", word)
551+
}
552+
}
553+
}
554+
436555
if err != nil {
437556
if exitCode == 1 {
438557
slog.Debug("gtags-find", "msg", "exit=1 not found (normal)")
@@ -643,7 +762,7 @@ func GtagsDiagnose(dir, word string) {
643762
}
644763

645764
// ---- 4. DB読み取りテスト(共通シンボルで疎通確認)----
646-
env := append(os.Environ(), "GTAGSDBPATH="+dir, "GTAGSROOT="+dir)
765+
env := gtagsEnv(dir)
647766
cmdLine := fmt.Sprintf("%s -xd <word> (GTAGSDBPATH=%s GTAGSROOT=%s)", bin, dir, dir)
648767
slog.Info("gtags-diag [4] command-line", "cmd", cmdLine, "note", "以下のコマンドをターミナルで実行して同じ結果か確認してください")
649768

@@ -864,10 +983,10 @@ func GtagsFindHoverHits(ctx context.Context, word, dir string) ([]DefHit, error)
864983
// GtagsFindRefs は GNU Global で word の参照箇所を検索する(callers 用)。
865984
// 各参照行を囲む関数名・定義行を findContainingFunc で解決して返す。
866985
func GtagsFindRefs(ctx context.Context, word, dir string) ([]CallSite, error) {
867-
cmd := exec.CommandContext(context.Background(), resolveGlobalBin(), "-xr", word)
986+
globalBin := resolveGlobalBin()
987+
cmd := exec.CommandContext(context.Background(), globalBin, "-xr", word)
868988
cmd.Dir = dir
869-
gtagsRefPath := filepath.ToSlash(dir)
870-
cmd.Env = append(os.Environ(), "GTAGSDBPATH="+gtagsRefPath, "GTAGSROOT="+gtagsRefPath)
989+
cmd.Env = gtagsEnv(dir)
871990
var stderr bytes.Buffer
872991
cmd.Stderr = &stderr
873992
out, err := cmd.Output()
@@ -879,6 +998,15 @@ func GtagsFindRefs(ctx context.Context, word, dir string) ([]CallSite, error) {
879998
slog.Warn("gtags-find-refs error", "word", word, "err", err, "stderr", stderr.String())
880999
return nil, err
8811000
}
1001+
// Cygwin global.exe が native pipe に書けず stdout が空のことがある(GtagsFindDefinitions と同症状)。
1002+
if len(bytes.TrimSpace(out)) == 0 {
1003+
if bashOut, attempted := runGlobalViaBash(globalBin, dir, word, "-xr"); attempted {
1004+
if len(bytes.TrimSpace(bashOut)) > 0 {
1005+
slog.Info("gtags-find-refs", "msg", "bash fallback succeeded", "word", word, "bytes", len(bashOut))
1006+
out = bashOut
1007+
}
1008+
}
1009+
}
8821010
hits := gtagsParseOutput(out, "ref", dir)
8831011
slog.Debug("gtags-find-refs raw hits", "word", word, "count", len(hits))
8841012
var results []CallSite

0 commit comments

Comments
 (0)