completions_test.go

  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}