Pad/internal/io/pool/search_test.go
Greg Pomerantz 78d240eedb Add in-file search (find bar)
- pool: SearchTask + FindSubstring (case-insensitive substring scan with
  query generation for stale-result dropping)
- editor: FindState on EditorState; find bar handlers (ToggleFind/FindNext/
  FindPrev/FindClose); worker-pool scan of the in-memory content; edit-aware
  offset remapping (shift after, drop overlapping); next/prev with
  wrap-around, selection + scroll to match; main-owned find_bar input
  forwarded via FindQueryChan (browser search_bar precedent)
- ui: find-bar layout below the margin-free top bar; search icon on the top
  bar right; new search/close/chevron_up/chevron_down icons
- logic: findQueryChan, TypeSearch result handling, findReset on open
- frame: FindQuery field (main syncs the widget text like Query)
- tests: pool scan tests, find-state unit tests (remap, nav, gen gate,
  focus), e2e find bar + navigation + edit-survival on real files
- docs: spec/architecture/development_plan updated (search implemented,
  channel/task tables)
2026-08-20 00:02:54 -04:00

108 lines
2.9 KiB
Go

package pool
import (
"reflect"
"testing"
)
func TestFindSubstring_Basic(t *testing.T) {
text := "hello world hello"
got := FindSubstring(text, "hello")
want := [][2]int{{0, 5}, {12, 17}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
}
func TestFindSubstring_CaseInsensitive(t *testing.T) {
got := FindSubstring("Hello HELLo hellO", "hEllO")
want := [][2]int{{0, 5}, {6, 11}, {12, 17}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
}
func TestFindSubstring_NonASCIIFold(t *testing.T) {
// Only the ASCII-foldable "STRASSE" matches "strasse" (lower-cased to
// "strasse"); "Straße" lower-cases to "straße", which does not contain it.
// Byte offset 8: "ß" is two UTF-8 bytes, so "Straße" is 7 bytes.
got := FindSubstring("Straße STRASSE", "strasse")
want := [][2]int{{8, 15}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
// And a query containing a multibyte rune still folds its ASCII parts.
got = FindSubstring("Straße", "STRA")
want = [][2]int{{0, 4}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
}
func TestFindSubstring_NoMatch(t *testing.T) {
if got := FindSubstring("abc", "xyz"); got != nil {
t.Fatalf("got %v, want nil", got)
}
}
func TestFindSubstring_EmptyQuery(t *testing.T) {
if got := FindSubstring("abc", ""); got != nil {
t.Fatalf("got %v, want nil for empty query", got)
}
}
func TestFindSubstring_OverlappingConsumed(t *testing.T) {
// "aa" in "aaa": one match; the match consumes its range.
got := FindSubstring("aaa", "aa")
want := [][2]int{{0, 2}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
// "...abaaba": matches at 3 and 6, not at 4.
got = FindSubstring("abaaba", "aba")
want = [][2]int{{0, 3}, {3, 6}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
}
func TestFindSubstring_Multibyte(t *testing.T) {
// Offsets are BYTE offsets. "é" is 2 bytes.
text := "éxéx" // bytes: 0-1 é, 2 x, 3-4 é, 5 x
got := FindSubstring(text, "xé")
want := [][2]int{{2, 5}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
}
func TestFindSubstring_WholeText(t *testing.T) {
got := FindSubstring("abc", "ABC")
want := [][2]int{{0, 3}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
}
func TestSearchTask_Execute(t *testing.T) {
task := NewSearchTask("one two One TWO", "two", 7)
res := task.Execute()
if !res.Success {
t.Fatalf("unexpected error: %v", res.Error)
}
if res.TaskType != TypeSearch {
t.Fatalf("type %v", res.TaskType)
}
sd, ok := res.Data.(SearchData)
if !ok {
t.Fatalf("data type %T", res.Data)
}
if sd.Gen != 7 {
t.Fatalf("gen %d", sd.Gen)
}
want := [][2]int{{4, 7}, {12, 15}}
if !reflect.DeepEqual(sd.Matches, want) {
t.Fatalf("matches %v, want %v", sd.Matches, want)
}
}