1package completions
2
3import (
4 "testing"
5
6 "charm.land/lipgloss/v2"
7 "github.com/sahilm/fuzzy"
8 "github.com/stretchr/testify/require"
9)
10
11// TestRankPrefersStrongBasenameMatch verifies that when no path hint is present,
12// files with exact basename matches rank higher than partial path matches.
13// Query "user" should prefer "user.go" over "internal/user_service.go".
14func TestRankPrefersStrongBasenameMatch(t *testing.T) {
15 t.Parallel()
16
17 c := &Completions{
18 items: []*CompletionItem{
19 NewCompletionItem("internal/ui/chat/search.go", FileCompletionValue{Path: "internal/ui/chat/search.go"}, lipgloss.NewStyle(), lipgloss.NewStyle(), lipgloss.NewStyle()),
20 NewCompletionItem("user.go", FileCompletionValue{Path: "user.go"}, lipgloss.NewStyle(), lipgloss.NewStyle(), lipgloss.NewStyle()),
21 NewCompletionItem("internal/user_service.go", FileCompletionValue{Path: "internal/user_service.go"}, lipgloss.NewStyle(), lipgloss.NewStyle(), lipgloss.NewStyle()),
22 },
23 paths: []string{"internal/ui/chat/search.go", "user.go", "internal/user_service.go"},
24 bases: []string{"search.go", "user.go", "user_service.go"},
25 }
26
27 ranked := c.rank(queryContext{query: "user"})
28 require.NotEmpty(t, ranked)
29 require.Equal(t, "user.go", ranked[0].Text())
30}
31
32// TestRankReturnsOriginalOrderForEmptyQuery verifies that empty queries
33// return all items in their original order without reordering.
34func TestRankReturnsOriginalOrderForEmptyQuery(t *testing.T) {
35 t.Parallel()
36
37 c := &Completions{
38 items: []*CompletionItem{
39 NewCompletionItem("b/user.go", FileCompletionValue{Path: "b/user.go"}, lipgloss.NewStyle(), lipgloss.NewStyle(), lipgloss.NewStyle()),
40 NewCompletionItem("a/user.go", FileCompletionValue{Path: "a/user.go"}, lipgloss.NewStyle(), lipgloss.NewStyle(), lipgloss.NewStyle()),
41 },
42 paths: []string{"b/user.go", "a/user.go"},
43 bases: []string{"user.go", "user.go"},
44 }
45
46 ranked := c.rank(queryContext{query: ""})
47 require.Len(t, ranked, 2)
48 require.Equal(t, "b/user.go", ranked[0].Text())
49 require.Equal(t, "a/user.go", ranked[1].Text())
50}
51
52// TestRankPrefersPathMatchesWhenPathHintPresent verifies that when query
53// contains a path separator (/), path-level matches are prioritized.
54// Query "internal/u" should rank "internal/user.go" highest.
55func TestRankPrefersPathMatchesWhenPathHintPresent(t *testing.T) {
56 t.Parallel()
57
58 c := &Completions{
59 items: []*CompletionItem{
60 NewCompletionItem("user.go", FileCompletionValue{Path: "user.go"}, lipgloss.NewStyle(), lipgloss.NewStyle(), lipgloss.NewStyle()),
61 NewCompletionItem("internal/user.go", FileCompletionValue{Path: "internal/user.go"}, lipgloss.NewStyle(), lipgloss.NewStyle(), lipgloss.NewStyle()),
62 NewCompletionItem("internal/ui/chat/search.go", FileCompletionValue{Path: "internal/ui/chat/search.go"}, lipgloss.NewStyle(), lipgloss.NewStyle(), lipgloss.NewStyle()),
63 },
64 paths: []string{"user.go", "internal/user.go", "internal/ui/chat/search.go"},
65 bases: []string{"user.go", "user.go", "search.go"},
66 }
67
68 ranked := c.rank(queryContext{query: "internal/u"})
69 require.NotEmpty(t, ranked)
70 require.Equal(t, "internal/user.go", ranked[0].Text())
71}
72
73// TestRankDotHintPrefersSuffixPathMatch verifies that file extension queries
74// (e.g., ".go") trigger path hint behavior and prioritize extension matches.
75// Query ".go" should rank "user.go" higher than "go-guide.md".
76func TestRankDotHintPrefersSuffixPathMatch(t *testing.T) {
77 t.Parallel()
78
79 c := &Completions{
80 items: []*CompletionItem{
81 NewCompletionItem("docs/go-guide.md", FileCompletionValue{Path: "docs/go-guide.md"}, lipgloss.NewStyle(), lipgloss.NewStyle(), lipgloss.NewStyle()),
82 NewCompletionItem("src/user.go", FileCompletionValue{Path: "src/user.go"}, lipgloss.NewStyle(), lipgloss.NewStyle(), lipgloss.NewStyle()),
83 },
84 paths: []string{"docs/go-guide.md", "src/user.go"},
85 bases: []string{"go-guide.md", "user.go"},
86 }
87
88 ranked := c.rank(queryContext{query: ".go"})
89 require.NotEmpty(t, ranked)
90 require.Equal(t, "src/user.go", ranked[0].Text())
91}
92
93// TestRemapMatchToPath verifies that basename match indices are correctly
94// remapped to full path indices. For "user" matched in "user.go" at [0,1,2],
95// when full path is "internal/user.go", indices become [9,10,11].
96func TestRemapMatchToPath(t *testing.T) {
97 t.Parallel()
98
99 match := remapMatchToPath(
100 fuzzy.Match{MatchedIndexes: []int{0, 1, 2}},
101 "internal/user.go",
102 )
103 require.Equal(t, []int{9, 10, 11}, match.MatchedIndexes)
104}
105
106// TestHasPathHint verifies the heuristics for detecting path-like queries.
107// - "internal/u" → true (contains /)
108// - "main.go" → true (file extension)
109// - "v0.1" → false (no letter in suffix)
110// - "main" → false (no path hint)
111func TestHasPathHint(t *testing.T) {
112 t.Parallel()
113
114 require.True(t, hasPathHint("internal/u"))
115 require.True(t, hasPathHint("main.go"))
116 require.False(t, hasPathHint("v0.1"))
117 require.False(t, hasPathHint("main"))
118}