Files
coder/agent/filefinder/delta.go
T
Kyle CarberryandCoder 68e4155fed feat(agent/filefinder): add plocate-lite file finder package (#22453)
Adds an in-memory trigram-indexed file finder package at
`agent/filefinder`, designed to power a future `FindFiles` HTTP handler
on the WorkspaceAgent.

## What it does

Fast fuzzy file search with VS Code-quality matching across millions of
files. Sub-millisecond search latency at 100K files.

## Architecture

- **Index**: append-only docs slice with trigram + prefix posting lists
- **Snapshot**: lock-free reader view via frozen slice headers +
shallow-copied deleted set
- **Search pipeline**: trigram intersection → fuzzy fallback (prefix
bucket + subsequence) → brute-force scan (capped at 5K docs)
- **Scoring**: subsequence match, basename prefix, boundary hits,
contiguous runs, depth/length penalties
- **Engine**: multi-root with fsnotify watcher (50ms batch coalescing),
atomic snapshot publishing

## Benchmarks (10K files)

| Query Type | Latency |
|---|---|
| exact_basename (`handler.go`) | ~43µs |
| short_query (`ha`) | ~7µs |
| fuzzy_basename (`hndlr`) | ~50µs |
| path_structured (`internal/handler`) | ~29µs |
| multi_token (`api handler`) | ~15µs |

## File inventory (11 files, 3273 lines)

| File | Lines | Purpose |
|---|---|---|
| `text.go` | 264 | Normalization, trigram extraction, scoring |
| `delta.go` | 128 | Index, Snapshot, CRUD operations |
| `query.go` | 272 | Query planning, search strategies, top-K merge |
| `engine.go` | 323 | Multi-root engine, watcher integration |
| `watcher_fs.go` | 201 | fsnotify wrapper with batch coalescing |
| `*_test.go` | 2085 | Unit tests, integration tests, benchmarks |

---------

Co-authored-by: Coder <coder@users.noreply.github.com>
2026-02-28 23:37:07 -05:00

126 lines
3.1 KiB
Go

package filefinder
import "strings"
// FileFlag represents the type of filesystem entry.
type FileFlag uint16
const (
FlagFile FileFlag = 0
FlagDir FileFlag = 1
FlagSymlink FileFlag = 2
)
type doc struct {
path string
baseOff int
baseLen int
depth int
flags uint16
}
// Index is an append-only in-memory file index with snapshot support.
type Index struct {
docs []doc
byGram map[uint32][]uint32
byPrefix1 [256][]uint32
byPrefix2 map[uint16][]uint32
byPath map[string]uint32
deleted map[uint32]bool
}
// Snapshot is a frozen, read-only view of the index at a point in time.
type Snapshot struct {
docs []doc
deleted map[uint32]bool
byGram map[uint32][]uint32
byPrefix1 [256][]uint32
byPrefix2 map[uint16][]uint32
}
// NewIndex creates an empty Index.
func NewIndex() *Index {
return &Index{
byGram: make(map[uint32][]uint32),
byPrefix2: make(map[uint16][]uint32),
byPath: make(map[string]uint32),
deleted: make(map[uint32]bool),
}
}
// Add inserts a path into the index, tombstoning any previous entry.
func (idx *Index) Add(path string, flags uint16) uint32 {
norm := string(normalizePathBytes([]byte(path)))
if oldID, ok := idx.byPath[norm]; ok {
idx.deleted[oldID] = true
}
id := uint32(len(idx.docs)) //nolint:gosec // Index will never exceed 2^32 docs.
baseOff, baseLen := extractBasename([]byte(norm))
idx.docs = append(idx.docs, doc{
path: norm, baseOff: baseOff, baseLen: baseLen,
depth: strings.Count(norm, "/"), flags: flags,
})
idx.byPath[norm] = id
for _, g := range extractTrigrams([]byte(norm)) {
idx.byGram[g] = append(idx.byGram[g], id)
}
if baseLen > 0 {
basename := []byte(norm[baseOff : baseOff+baseLen])
p1 := prefix1(basename)
idx.byPrefix1[p1] = append(idx.byPrefix1[p1], id)
p2 := prefix2(basename)
idx.byPrefix2[p2] = append(idx.byPrefix2[p2], id)
}
return id
}
// Remove marks the entry for path as deleted.
func (idx *Index) Remove(path string) bool {
norm := string(normalizePathBytes([]byte(path)))
id, ok := idx.byPath[norm]
if !ok {
return false
}
idx.deleted[id] = true
delete(idx.byPath, norm)
return true
}
// Has reports whether path exists (not deleted) in the index.
func (idx *Index) Has(path string) bool {
_, ok := idx.byPath[string(normalizePathBytes([]byte(path)))]
return ok
}
// Len returns the number of live (non-deleted) documents.
func (idx *Index) Len() int { return len(idx.byPath) }
func copyPostings[K comparable](m map[K][]uint32) map[K][]uint32 {
cp := make(map[K][]uint32, len(m))
for k, v := range m {
cp[k] = v[:len(v):len(v)]
}
return cp
}
// Snapshot returns a frozen read-only view of the index.
func (idx *Index) Snapshot() *Snapshot {
del := make(map[uint32]bool, len(idx.deleted))
for id := range idx.deleted {
del[id] = true
}
var p1Copy [256][]uint32
for i, ids := range idx.byPrefix1 {
if len(ids) > 0 {
p1Copy[i] = ids[:len(ids):len(ids)]
}
}
return &Snapshot{
docs: idx.docs[:len(idx.docs):len(idx.docs)],
deleted: del,
byGram: copyPostings(idx.byGram),
byPrefix1: p1Copy,
byPrefix2: copyPostings(idx.byPrefix2),
}
}