build-a-fuzzy-finder / lesson-05.md
Lesson 05 · Matching

Where did it match?

Knowing a query matches is not enough - to score and highlight a match you need to know exactly which candidate characters it landed on. Today the matcher starts reporting those positions.

The goal

Change the matcher to return the list of candidate indices where each query character matched, using the greedy leftmost choice.

Start here - the target
TO DO
Scenario: Reporting greedy match positions
Giventhe candidate "src/main.go"
Whenmatch positions are requested for query "smg", then for "main", then for "xyz", then for ""
Thenfor "smg" the positions are [0, 4, 9] (the first s, first m, first g, taken leftmost), for "main" they are [4, 5, 6, 7], for "xyz" there is no match, and for "" the positions are empty (a match with nothing highlighted)
Background

A boolean answer was enough to filter, but everything ahead - scoring a match, highlighting it, ranking one candidate above another - needs to know which characters the query matched. So the matcher graduates from returning true to returning a list of positions: one candidate index per query character, in order.

The rule for which index is the greedy leftmost one: for each query character, take the first candidate character that fits. That is the natural output of the two-cursor walk you already have - just record each index as you consume it. Greedy positions are not always the best-looking match (a later lesson finds a higher-scoring alignment), but they are correct, cheap, and the right foundation. Note the edges: no match returns no positions and a false flag, and an empty query matches with an empty position list - a real match that highlights nothing.

Make it work
// Same two-cursor walk, but record each candidate index you consume.
// "Greedy leftmost": take the first candidate char that fits each
// query char. Return the positions plus whether it matched at all.
func matchPositions(query, candidate string) ([]int, bool) {
pos := []int{}
qi := 0
for ci := 0; qi < len(query) && ci < len(candidate); ci++ {
if equalSmartCase(query[qi], candidate[ci], query) {
pos = append(pos, ci)
qi++
}
}
return pos, qi == len(query)
}
CheckpointDONE
The matcher now reports exactly where each query character landed. Commit and stop here.