Skip to content

Commit f73a1fe

Browse files
committed
feat: improve explorer UX, editor path bar, and func badge detection
Explorer: - Right-click context menu on files/folders: open in OS explorer, copy path, search in this folder (auto-expands dir input) - Syncs selected file with active Monaco editor tab (like VSCode) - Ctrl+Shift+F switches from explorer tab to search and focuses query Editor / search: - Path bar is now a selectable input — drag to copy any part of the path - Left panel max width increased from 600px to 900px - Search hit rows are visually distinct from file/folder headers (green left border + indent) Function badge fixes (ClassifyKind): - K&R-style definitions (name and `(` on separate lines) now detected - Functions with many arguments whose `{` falls outside the 6-line - Prevent subdir filter from breaking definition jump, hover, and result clicks during streaming context window now correctly get the func badge - Inline comments (`/* (note */`, `/**/`) no longer confuse paren counting — fixes false badges on function calls and false negatives on prototype declarations with trailing comments - Add ClassifyKind / PreferDefinitionHits unit tests for above cases
1 parent bd45d29 commit f73a1fe

12 files changed

Lines changed: 442 additions & 44 deletions

File tree

search/classify.go

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,29 @@ var (
2626
reAlphanum = regexp.MustCompile(`^[a-zA-Z0-9_\s,*()\[\]{}]`)
2727
)
2828

29+
// stripLineComment は行末の /* ... */ インラインコメントと // コメントを除去する。
30+
// パーレン計数・セミコロン判定をコメント内の記号に惑わされないようにするため。
31+
func stripLineComment(s string) string {
32+
// /* ... */ を左から順に除去(単一行内の複数コメントにも対応)
33+
for {
34+
start := strings.Index(s, "/*")
35+
if start < 0 {
36+
break
37+
}
38+
end := strings.Index(s[start+2:], "*/")
39+
if end < 0 {
40+
s = s[:start]
41+
break
42+
}
43+
s = s[:start] + s[start+2+end+2:]
44+
}
45+
// // 行コメントを除去
46+
if i := strings.Index(s, "//"); i >= 0 {
47+
s = s[:i]
48+
}
49+
return strings.TrimRight(s, " \t")
50+
}
51+
2952
// ClassifyKind は1行のテキストと後続スニペットからシンボル種別を判定する。
3053
// 戻り値: "func" / "define" / "struct" / "enum" / "typedef" / ""
3154
func ClassifyKind(text string, snippet []graph.SnippetLine, matchLine int) string {
@@ -48,6 +71,46 @@ func ClassifyKind(text string, snippet []graph.SnippetLine, matchLine int) strin
4871
}
4972

5073
parenIdx := strings.Index(t, "(")
74+
75+
// K&R スタイル: 関数名と ( が別行のケース(例: "int foo\n(\n...)\n{")
76+
if parenIdx < 0 {
77+
// マッチ行が関数定義の戻り値型+名前に見えない場合は除外
78+
// (引数の一部・文・代入・メンバーアクセスを含む行はスキップ)
79+
if strings.ContainsAny(t, ",;=()") || reDotArrow.MatchString(t) || !reAlphanum.MatchString(t) {
80+
return ""
81+
}
82+
// 次の非空行が ( だけ、または ( で始まる引数リストか確認
83+
nextIsOpenParen := false
84+
for _, s := range snippet {
85+
if s.Line <= matchLine {
86+
continue
87+
}
88+
st := strings.TrimSpace(s.Text)
89+
if st == "" {
90+
continue
91+
}
92+
nextIsOpenParen = strings.HasPrefix(st, "(")
93+
break
94+
}
95+
if !nextIsOpenParen {
96+
return ""
97+
}
98+
// さらに先を見て { が来れば func
99+
for _, s := range snippet {
100+
if s.Line <= matchLine {
101+
continue
102+
}
103+
st := strings.TrimSpace(s.Text)
104+
if reBraceStart.MatchString(st) || reBraceEnd.MatchString(st) {
105+
return "func"
106+
}
107+
}
108+
return ""
109+
}
110+
111+
// コメントを除いたテキストでセミコロン判定・パーレン計数を行う
112+
tCode := stripLineComment(t)
113+
51114
if reControl.MatchString(t) ||
52115
strings.HasPrefix(t, "{") ||
53116
strings.HasPrefix(t, "!") ||
@@ -56,20 +119,20 @@ func ClassifyKind(text string, snippet []graph.SnippetLine, matchLine int) strin
56119
strings.HasPrefix(t, "*") ||
57120
strings.HasPrefix(t, "||") ||
58121
strings.HasPrefix(t, "&&") ||
59-
reSemiEnd.MatchString(t) ||
60-
reLogicEnd.MatchString(t) ||
122+
reSemiEnd.MatchString(tCode) ||
123+
reLogicEnd.MatchString(tCode) ||
61124
parenIdx <= 0 ||
62125
reAssign.MatchString(t[:parenIdx]) ||
63126
reDotArrow.MatchString(t[:parenIdx]) ||
64127
!reFuncCall.MatchString(t) {
65128
return ""
66129
}
67130

68-
if reBraceEnd.MatchString(t) {
131+
if reBraceEnd.MatchString(tCode) {
69132
return "func"
70133
}
71134

72-
openParens := strings.Count(t, "(") - strings.Count(t, ")")
135+
openParens := strings.Count(tCode, "(") - strings.Count(tCode, ")")
73136
for _, s := range snippet {
74137
if s.Line <= matchLine {
75138
continue
@@ -78,16 +141,22 @@ func ClassifyKind(text string, snippet []graph.SnippetLine, matchLine int) strin
78141
if st == "" {
79142
continue
80143
}
81-
openParens += strings.Count(st, "(") - strings.Count(st, ")")
82-
if reBraceStart.MatchString(st) || reBraceEnd.MatchString(st) {
144+
sc := stripLineComment(st)
145+
openParens += strings.Count(sc, "(") - strings.Count(sc, ")")
146+
if reBraceStart.MatchString(st) || reBraceEnd.MatchString(sc) {
83147
return "func"
84148
}
85-
if openParens <= 0 && reSemiEnd.MatchString(st) {
149+
if openParens <= 0 && reSemiEnd.MatchString(sc) {
86150
break
87151
}
88152
if openParens <= 0 && st != "" && !reAlphanum.MatchString(st) {
89153
break
90154
}
91155
}
156+
// スニペットを使い切っても ( が閉じなかった場合(引数が多くコンテキスト外に出た)
157+
// → 代入・制御文・メンバーアクセスは既に除外済みなので func と判定
158+
if openParens > 0 {
159+
return "func"
160+
}
92161
return ""
93162
}

search/classify_test.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package search
2+
3+
import (
4+
"testing"
5+
6+
"grepnavi/graph"
7+
)
8+
9+
func TestClassifyKind(t *testing.T) {
10+
snip := func(lines ...string) []graph.SnippetLine {
11+
s := make([]graph.SnippetLine, len(lines))
12+
for i, l := range lines {
13+
s[i] = graph.SnippetLine{Line: i + 2, Text: l}
14+
}
15+
return s
16+
}
17+
18+
tests := []struct {
19+
name string
20+
text string
21+
snippet []graph.SnippetLine
22+
matchLine int
23+
want string
24+
}{
25+
// ===== func =====
26+
{
27+
name: "通常関数定義(同行 {)",
28+
text: "void foo(int x) {",
29+
snippet: snip(),
30+
want: "func",
31+
},
32+
{
33+
name: "通常関数定義(次行 {)",
34+
text: "void foo(int x)",
35+
snippet: snip("{"),
36+
want: "func",
37+
},
38+
{
39+
name: "通常関数定義(引数複数行 → {)",
40+
text: "int bar(int a,",
41+
snippet: snip("int b)", "{"),
42+
want: "func",
43+
},
44+
{
45+
name: "K&R スタイル: 次行が ( で始まり { が来る",
46+
text: "int process_data",
47+
matchLine: 1,
48+
snippet: snip(
49+
"(",
50+
" unsigned char flag",
51+
" , unsigned short count",
52+
")",
53+
"{",
54+
),
55+
want: "func",
56+
},
57+
{
58+
name: "K&R スタイル: 宣言(; で終わる)",
59+
text: "int process_data",
60+
matchLine: 1,
61+
snippet: snip(
62+
"(",
63+
" unsigned char flag",
64+
");",
65+
),
66+
want: "",
67+
},
68+
{
69+
name: "K&R スタイル: 次行が ( でない → func ではない",
70+
text: "int process_data",
71+
matchLine: 1,
72+
snippet: snip("int x;"),
73+
want: "",
74+
},
75+
{
76+
name: "K&R 誤検知防止: 引数行(, 含む)+ 次行がキャスト ( で始まる",
77+
text: "MODE,",
78+
matchLine: 1,
79+
snippet: snip("(int)ptr->val,", "NULL);"),
80+
want: "",
81+
},
82+
{
83+
name: "K&R 誤検知防止: 引数行(; 含む)",
84+
text: "NULL);",
85+
matchLine: 1,
86+
snippet: snip("}", "}", "{"),
87+
want: "",
88+
},
89+
// ===== 非 func =====
90+
{
91+
name: "引数が多くコンテキスト外に { が出る関数定義",
92+
text: "static int open_dialog(",
93+
matchLine: 1,
94+
snippet: snip(
95+
"void* self,",
96+
"int type,",
97+
"char* title,",
98+
"char* message,",
99+
"short priority,",
100+
"short id,",
101+
// ここまでが6行コンテキスト — ) と { はコンテキスト外
102+
),
103+
want: "func",
104+
},
105+
{
106+
name: "誤検知防止: 関数呼び出し + 末尾コメントに ( がある",
107+
text: `call_func(obj); /* see (note */`,
108+
matchLine: 1,
109+
snippet: snip("}"),
110+
want: "",
111+
},
112+
{
113+
name: "誤検知防止: if 文 + 末尾コメント",
114+
text: `if (x == NEW) /* update (redraw) */`,
115+
matchLine: 1,
116+
snippet: snip("{", "}"),
117+
want: "",
118+
},
119+
{
120+
name: "関数宣言(; 終わり)",
121+
text: "void foo(int x);",
122+
snippet: snip(),
123+
want: "",
124+
},
125+
{
126+
name: "if 文",
127+
text: "if (x > 0) {",
128+
snippet: snip(),
129+
want: "",
130+
},
131+
{
132+
name: "代入式",
133+
text: "result = func(x);",
134+
snippet: snip(),
135+
want: "",
136+
},
137+
{
138+
name: "メソッド呼び出し(->)",
139+
text: "obj->method(x);",
140+
snippet: snip(),
141+
want: "",
142+
},
143+
// ===== 他の種別 =====
144+
{
145+
name: "#define",
146+
text: "#define FOO 1",
147+
snippet: snip(),
148+
want: "define",
149+
},
150+
{
151+
name: "struct",
152+
text: "struct Foo {",
153+
snippet: snip(),
154+
want: "struct",
155+
},
156+
{
157+
name: "enum",
158+
text: "enum Color {",
159+
snippet: snip(),
160+
want: "enum",
161+
},
162+
{
163+
name: "typedef",
164+
text: "typedef unsigned int uint32_t;",
165+
snippet: snip(),
166+
want: "typedef",
167+
},
168+
}
169+
170+
for _, tt := range tests {
171+
t.Run(tt.name, func(t *testing.T) {
172+
got := ClassifyKind(tt.text, tt.snippet, tt.matchLine)
173+
if got != tt.want {
174+
t.Errorf("ClassifyKind(%q) = %q, want %q", tt.text, got, tt.want)
175+
}
176+
})
177+
}
178+
}

search/definition.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,11 @@ func isDefinitionHit(h DefHit) bool {
148148
return true // #define / struct / enum 等は常に定義
149149
}
150150
t := strings.TrimSpace(h.Text)
151-
if strings.HasSuffix(t, ";") {
151+
tCode := stripLineComment(t) // コメントを除いて判定
152+
if strings.HasSuffix(tCode, ";") {
152153
return false // 行末が ; → 宣言
153154
}
154-
if strings.Contains(t, "{") {
155+
if strings.Contains(tCode, "{") {
155156
return true // { を含む → 定義
156157
}
157158
// 行末が ) や関数名のみ(K&R スタイル等)→ 次行を確認

search/definition_test.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@ func TestPreferDefinitionHits(t *testing.T) {
4040
},
4141
wantFiles: []string{"foo.h"},
4242
},
43+
{
44+
name: "宣言に末尾コメントがあっても宣言と判定できる",
45+
hits: []DefHit{
46+
{File: "foo.c", Line: 3, Text: "static int foo(int x); /* forward decl */", Kind: "func"}, // 宣言(コメント付き)
47+
{File: "foo.c", Line: 10, Text: "static int foo(int x) {", Kind: "func"}, // 定義
48+
},
49+
wantFiles: []string{"foo.c"},
50+
},
51+
{
52+
name: "宣言に /**/ コメントがあっても宣言と判定できる",
53+
hits: []DefHit{
54+
{File: "foo.c", Line: 3, Text: "static int foo(int x);/**/", Kind: "func"}, // 宣言(/**/ 付き)
55+
{File: "foo.c", Line: 10, Text: "static int foo(int x) {", Kind: "func"}, // 定義
56+
},
57+
wantFiles: []string{"foo.c"},
58+
},
4359
{
4460
name: "定義あり: .c と .h の定義が両方ある場合 .c を優先",
4561
hits: []DefHit{
@@ -133,9 +149,9 @@ func TestClassifyDefKind(t *testing.T) {
133149
want: "func",
134150
},
135151
{
136-
name: "STATIC function",
137-
text: "STATIC void lvSFXdrvOpe_ShowErrDlg(lvSFXdrvOpe* pThis)",
138-
word: "lvSFXdrvOpe_ShowErrDlg",
152+
name: "static function",
153+
text: "static void widget_show_error(Widget* self)",
154+
word: "widget_show_error",
139155
want: "func",
140156
},
141157
}

static/css/explorer.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,6 @@
198198
line-height: 1;
199199
}
200200
.ex-item:hover .ex-folder-btn { display: inline-flex; align-items: center; }
201-
.ex-flat.ex-item:hover .ex-folder-btn { display: none; }
202201
.ex-folder-btn:hover { color: #ce9178; }
203202

204203
/* フィルタ結果 1行表示 (VSCode Ctrl+P スタイル) */

0 commit comments

Comments
 (0)