- 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)
108 lines
2.9 KiB
Go
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)
|
|
}
|
|
}
|