Skip to content

Commit e75c929

Browse files
committed
test: add unit tests for pure functions
test: add unit tests for pure functions
1 parent bdd2081 commit e75c929

24 files changed

Lines changed: 1839 additions & 64 deletions

.github/workflows/test.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
js:
11+
name: JS unit tests
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-node@v4
16+
with:
17+
node-version: 22
18+
- run: node --test test/*.test.js
19+
20+
go:
21+
name: Go tests
22+
runs-on: ubuntu-latest
23+
steps:
24+
- uses: actions/checkout@v4
25+
- uses: actions/setup-go@v5
26+
with:
27+
go-version-file: go.mod
28+
- run: go test ./...

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
# ソースコード
55
!.gitignore
66
!README.md
7+
!.github/
8+
!.github/**
79
!LICENSE
810
!go.mod
11+
!go.sum
12+
!package.json
913
!main.go
1014
!server.go
1115
!api/
@@ -20,3 +24,7 @@
2024
!static/css/**
2125
!static/js/
2226
!static/js/**
27+
!static/addons/
28+
!static/addons/**
29+
!test/
30+
!test/**

README.md

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# grepnavi
22

3+
[![CI](https://github.com/Taka-S-dev/grepnavi/actions/workflows/test.yml/badge.svg)](https://github.com/Taka-S-dev/grepnavi/actions/workflows/test.yml)
4+
35
コードベース調査ツール。ripgrep の高速検索 + Monaco エディタ + 調査グラフで、**「どこを調べたか」を記録しながらコードを読み解く**ためのツールです。
46

57
大きなコードベース(Linux カーネル、OpenSSL、curl など)を読む際に、検索結果をグラフに積み上げながら構造を把握していくことを想定しています。
@@ -45,6 +47,30 @@
4547
- **ドラッグ&ドロップ** — ノードを別ノードにドロップして親子関係を変更。drop-before / drop-after のライン表示で挿入位置を確認
4648
- **⠿ ハンドル** — ノード行の左端ハンドルを左右にドラッグしてインデントレベル(階層)を変更
4749
- **キーボードでの移動** — Shift+Alt+Arrow キーで選択ノードを移動(↑↓ で兄弟間、← でレベルアップ、→ でレベルダウン)
50+
- **↑ / ↓ で選択変更** — ツリービュー上でフォーカス外(テキスト入力・エディタ以外)のとき、Up/Down 矢印キーで選択ノードを切り替え
51+
52+
### アドオン
53+
54+
アドオンは `static/addons/<name>/addon.js` + `addon.css` で実装されており、`static/addons/addons.js``ADDONS` 配列に名前を列挙するだけで有効化できます。
55+
56+
#### コールツリー
57+
58+
関数の呼び出し元(callers)/ 呼び出し先(callees)をツリー形式で表示します。
59+
60+
- エディタ上の関数名を選択して右クリック or ツールバーの「Call Tree」ボタンで起動
61+
- 入力欄に関数名を入力して実行(Enter または Go)
62+
- `callers` / `callees` タブで方向を切り替え
63+
- ノードをクリックしてソースへジャンプ。`` で再帰的に展開
64+
- `?mode=panel` 時はサイドパネルのタブとして自己登録
65+
66+
#### C インクルード依存グラフ
67+
68+
`#include` の依存関係を D3.js フォースグラフで可視化します。
69+
70+
- エディタで C/C++ ファイルを開くと起点ファイルが自動セット
71+
- ツールバーの「Include Graph」ボタンでパネルを開き「Analyze」で解析開始
72+
- ノードをクリックして上流・下流を展開。Ctrl+クリック or ダブルクリックでエディタに表示
73+
- 「Collapse All」でグラフを折りたたみ
4874

4975
### プロジェクト
5076

@@ -64,11 +90,29 @@
6490

6591
## インストール・起動
6692

93+
### バイナリをダウンロード(推奨)
94+
95+
[GitHub Releases](https://github.com/Taka-S-dev/grepnavi/releases) からアーカイブをダウンロードして展開してください。Go のインストール不要です。
96+
97+
```
98+
grepnavi/
99+
├── grepnavi.exe ← 実行ファイル
100+
└── static/ ← 静的ファイル(exe と同じディレクトリに必須)
101+
```
102+
103+
> **注意:** `static/` フォルダを相対パスで参照するため、exe 単体では動作しません。アーカイブを展開したディレクトリごと配置してください。
104+
105+
### ソースからビルド
106+
67107
```bash
68108
# ビルド
69109
go build .
70110
# → Windows: grepnavi.exe Mac/Linux: grepnavi が生成されます
111+
```
71112

113+
### 起動
114+
115+
```bash
72116
# 起動(カレントディレクトリを検索ルートとして使用)
73117
.\grepnavi.exe # Windows
74118
./grepnavi # Mac/Linux
@@ -95,7 +139,12 @@ go build .
95139

96140
### Monaco Editor
97141

98-
[Releases](https://github.com/microsoft/monaco-editor/releases) から `monaco-editor-x.x.x.tgz` をダウンロードし、`min/vs/``static/vs/` に配置。
142+
プロジェクトルートで以下を実行:
143+
144+
```bash
145+
npm install monaco-editor@0.52.2
146+
cp -r node_modules/monaco-editor/min/vs static/vs/
147+
```
99148

100149
`static/index.html` を変更:
101150
```js
@@ -143,6 +192,7 @@ var require = {paths: {'vs': '/vs'}};
143192
| `Ctrl+Z` | 元に戻す |
144193
| `Ctrl+Shift+N` | 新しいウィンドウを開く |
145194
| `F12` / `Ctrl+クリック` | 定義ジャンプ |
195+
| `` / `` | ツリービューで選択ノードを変更 |
146196
| `Shift+Alt+↑` / `` | 選択ノードを上 / 下に移動 |
147197
| `Shift+Alt+←` / `` | 選択ノードのレベルを上げる / 下げる |
148198
| `?` | キーボードショートカット一覧を表示 |
@@ -244,6 +294,9 @@ grepnavi/
244294
│ ├── ripgrep.go # ripgrep 呼び出し・JSON パース・SSE ストリーミング
245295
│ ├── definition.go # 定義ジャンプ(ctags 風シンボル解析)
246296
│ ├── symbols.go # シンボル抽出
297+
│ ├── hover.go # ホバープレビュー用スニペット取得
298+
│ ├── calltree.go # callers / callees 解析
299+
│ ├── include.go # C インクルード依存グラフ解析
247300
│ ├── ifdef.go # #ifdef 条件コンパイル解析
248301
│ └── ifdef_eval.go # #ifdef 条件評価
249302
└── static/
@@ -255,8 +308,13 @@ grepnavi/
255308
│ ├── search.js # 検索・フィルタ・結果表示
256309
│ ├── graph.js # グラフ/ツリー操作・D3.js・詳細パネル・D&D
257310
│ ├── editor.js # Monaco エディタ・fzf・ナビ履歴・行メモ・#ifdef
311+
│ ├── include-graph.js # C インクルード依存グラフ(D3.js)
258312
│ ├── project.js # プロジェクト保存/開く・ルート設定・glob履歴・リサイザー
259313
│ └── app.js # ブートストラップ・グローバルイベント登録
314+
├── addons/
315+
│ ├── addons.js # アドオン設定(有効化リスト)
316+
│ ├── c-include/ # C インクルード依存グラフ アドオン
317+
│ └── call-tree/ # コールツリー アドオン
260318
└── css/
261319
└── main.css
262320
```
@@ -284,6 +342,12 @@ grepnavi/
284342
| `GET` | `/api/open` | ファイルの内容取得 |
285343
| `GET` | `/api/snippet` | スニペット取得 |
286344
| `GET` | `/api/definition` | 定義ジャンプ先の検索 |
345+
| `GET` | `/api/hover` | ホバープレビュー用スニペット取得 |
346+
| `GET` | `/api/callers` | 関数の呼び出し元を検索 |
347+
| `GET` | `/api/callees` | 関数の呼び出し先を検索 |
348+
| `GET` | `/api/include-graph` | ファイルのインクルード依存グラフ取得 |
349+
| `GET` | `/api/include-file` | ファイルが `#include` しているファイル一覧 |
350+
| `GET` | `/api/include-by` | ファイルを `#include` しているファイル一覧 |
287351

288352
---
289353

api/handlers.go

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package api
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
67
"io"
@@ -9,9 +10,11 @@ import (
910
"os"
1011
"os/exec"
1112
"path/filepath"
13+
"runtime"
1214
"strconv"
1315
"strings"
1416
"sync"
17+
"time"
1518
"unicode/utf8"
1619

1720
"grepnavi/graph"
@@ -63,6 +66,7 @@ func (h *Handler) Register(mux *http.ServeMux) {
6366
mux.HandleFunc("/api/new-window", h.handleNewWindow)
6467
mux.HandleFunc("/api/browse", h.handleBrowse)
6568
mux.HandleFunc("/api/dirs", h.handleDirs)
69+
mux.HandleFunc("/api/pick-dir", h.handlePickDir)
6670
mux.HandleFunc("/api/root", h.handleRoot)
6771
mux.HandleFunc("/api/files", h.handleFiles)
6872
// call tree
@@ -165,9 +169,28 @@ func (h *Handler) handleHover(w http.ResponseWriter, r *http.Request) {
165169
} else if !filepath.IsAbs(dir) {
166170
dir = filepath.Join(hroot, dir)
167171
}
168-
// ホバーは定義検索なので glob フィルターを無視して全ファイルを対象にする
169-
hits, err := search.FindHover(r.Context(), word, dir, "")
172+
glob := q.Get("glob")
173+
if glob == "" {
174+
glob = "*.c,*.h,*.cpp,*.hpp,*.cc"
175+
}
176+
// 現在開いているファイルのインクルードチェーンを取得(優先ソート用)
177+
var includeChain map[string]bool
178+
if file := q.Get("file"); file != "" {
179+
incs, _ := search.GetFileIncludes(file, hroot)
180+
includeChain = make(map[string]bool, len(incs)+1)
181+
for _, f := range incs {
182+
includeChain[f.ID] = true
183+
}
184+
includeChain[file] = true
185+
}
186+
ctx, cancel := context.WithTimeout(r.Context(), 4000*time.Millisecond)
187+
defer cancel()
188+
hits, err := search.FindHover(ctx, word, dir, glob, includeChain)
170189
if err != nil {
190+
if ctx.Err() != nil {
191+
jsonOK(w, []search.HoverHit{})
192+
return
193+
}
171194
jsonErr(w, err.Error(), http.StatusInternalServerError)
172195
return
173196
}
@@ -1169,6 +1192,28 @@ func (h *Handler) handleBrowse(w http.ResponseWriter, r *http.Request) {
11691192
})
11701193
}
11711194

1195+
func (h *Handler) handlePickDir(w http.ResponseWriter, r *http.Request) {
1196+
var cmd *exec.Cmd
1197+
switch runtime.GOOS {
1198+
case "windows":
1199+
cmd = exec.Command("powershell", "-NoProfile", "-Command",
1200+
`Add-Type -AssemblyName System.Windows.Forms; $d = New-Object System.Windows.Forms.FolderBrowserDialog; $d.Description = 'プロジェクトルートを選択'; if($d.ShowDialog() -eq 'OK'){$d.SelectedPath}`)
1201+
case "darwin":
1202+
cmd = exec.Command("osascript", "-e", "POSIX path of (choose folder with prompt \"プロジェクトルートを選択\")")
1203+
default:
1204+
// Linux: zenity を試みる
1205+
cmd = exec.Command("zenity", "--file-selection", "--directory", "--title=プロジェクトルートを選択")
1206+
}
1207+
out, err := cmd.Output()
1208+
if err != nil {
1209+
// キャンセルまたはコマンドなし
1210+
jsonOK(w, map[string]string{"path": ""})
1211+
return
1212+
}
1213+
path := strings.TrimSpace(string(out))
1214+
jsonOK(w, map[string]string{"path": path})
1215+
}
1216+
11721217
func (h *Handler) handleDirs(w http.ResponseWriter, r *http.Request) {
11731218
h.mu.RLock()
11741219
root := h.root

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ module grepnavi
22

33
go 1.25.0
44

5-
require golang.org/x/text v0.35.0 // indirect
5+
require golang.org/x/text v0.35.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
2+
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=

package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "ripgrep",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "node --test test/*.test.js"
8+
},
9+
"keywords": [],
10+
"author": "",
11+
"license": "ISC",
12+
"type": "commonjs",
13+
"dependencies": {
14+
"monaco-editor": "^0.55.1"
15+
}
16+
}

search/definition.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ type DefHit struct {
1818
// FindDefinitions は word の定義候補を ripgrep で検索して返す。
1919
// glob が空なら全ファイルが対象。
2020
func FindDefinitions(ctx context.Context, word, dir, glob string) ([]DefHit, error) {
21+
return FindDefinitionsN(ctx, word, dir, glob, 50)
22+
}
23+
24+
// FindDefinitionsN は maxPerQuery で各クエリの上限を指定できる FindDefinitions。
25+
func FindDefinitionsN(ctx context.Context, word, dir, glob string, maxPerQuery int) ([]DefHit, error) {
2126
if word == "" || dir == "" {
2227
return nil, nil
2328
}
@@ -31,7 +36,7 @@ func FindDefinitions(ctx context.Context, word, dir, glob string) ([]DefHit, err
3136
{`#\s*define\s+` + esc + `\b`, "define"},
3237
{`^\s*(typedef\s+)?(struct|union)\s+` + esc + `\s*(\{|$)`, "struct"},
3338
{`^\s*(typedef\s+)?enum\s+` + esc + `\s*(\{|$)`, "enum"},
34-
{`\btypedef\b.+\b` + esc + `\b`, "typedef"},
39+
{`\btypedef\b.+\b` + esc + `\b\s*;`, "typedef"},
3540
{`^\s*\}\s*` + esc + `\s*;`, "typedef_close"},
3641
{`^\s+` + esc + `\b\s*[,=]`, "enum_member"},
3742
{`^[^\s#/*].*\b` + esc + `\s*\(`, "func"},
@@ -41,14 +46,17 @@ func FindDefinitions(ctx context.Context, word, dir, glob string) ([]DefHit, err
4146
var results []DefHit
4247

4348
for _, q := range queries {
49+
if ctx.Err() != nil {
50+
break
51+
}
4452
opts := Options{
4553
Pattern: q.pattern,
4654
Dir: dir,
4755
FileGlob: glob,
4856
Regex: true,
4957
CaseSensitive: true,
50-
ContextLines: -1, // 定義行の特定のみ目的のためコンテキスト不要
51-
MaxResults: 50, // 同名シンボルの過剰マッチを抑制
58+
ContextLines: -1,
59+
MaxResults: maxPerQuery,
5260
}
5361
matches, err := Search(ctx, opts)
5462
if err != nil {

0 commit comments

Comments
 (0)