editor/search: fix nil-handler click crash and '1 / 0' match counter

Tapping the find-bar/browser search input (MAIN-owned GioEditors with no
tap interaction) registered a click region with a nil handler; the logic
loop called the nil handler and panicked the goroutine. Skip nil-handler
clicks in CheckGestures (the widget handles its own click for focus/caret)
and guard the input loop so a malformed event can never panic.

Fix the '1 / 0' counter on session restore with an empty query: FindState.Cur
is documented '-1 when none selected' but its Go zero value is 0, so a
restored/reset state with no matches read as 'match 1 of 0'. Only print
'N / M' when Cur is a valid index, and reset Cur=-1 in findReset and the
restore path.
This commit is contained in:
Greg Pomerantz 2026-10-26 22:19:44 -04:00
parent 348895eb7e
commit ebd264150d
4 changed files with 26 additions and 2 deletions

View File

@ -377,6 +377,12 @@ func (l *Logic) Run() {
}
case events := <-l.inputChan:
for _, evt := range events {
// Invariant: every input event carries a handler. A nil handler
// would panic the moment it is dispatched; skip it defensively so
// a malformed event can never take down the logic goroutine.
if evt.Handler == nil {
continue
}
evt.Handler(evt.Data)
}
l.emitFrame()
@ -857,6 +863,13 @@ func (l *Logic) handleWorkerResult(res pool.Result) {
// with the content, the query re-scans against the new file).
if res.FilePath == l.restoreFile && l.restoreFile != "" {
f := &l.state.Editor.Find
// Start from a clean find state: the re-scan (dispatched when the
// content lands, and only for a non-empty query) selects the
// restored match via RestoreMatch. Clearing Matches and setting
// Cur=-1 avoids the int zero value 0 reading as "match 1 of 0"
// when the restored query is empty (no scan is dispatched then).
f.Matches = nil
f.Cur = -1
f.Query = l.session.FindQuery
f.Visible = l.session.FindVisible
f.RestoreMatch = l.session.FindCurByte

View File

@ -143,7 +143,10 @@ func (e *EditorState) findReset() {
// ClearSeq is preserved: main tracks it monotonically, so resetting it
// to 0 would make a later clear (seq 1) indistinguishable from an old
// one already mirrored.
e.Find = FindState{Query: e.Find.Query, Gen: e.Find.Gen + 1, ClearSeq: e.Find.ClearSeq, SettleByte: -1}
// Cur is reset to -1 (not the int zero value 0): a fresh find state selects
// no match, and a stale Cur=0 against an empty Matches would otherwise read
// as "match 1 of 0" in the counter.
e.Find = FindState{Query: e.Find.Query, Gen: e.Find.Gen + 1, ClearSeq: e.Find.ClearSeq, SettleByte: -1, Cur: -1}
}
// findSetQuery processes a query forwarded from the main-owned "find_bar"

View File

@ -2596,7 +2596,7 @@ func buildFindBar(screenWidth ui.Dp) ui.Element {
counter = "…"
case f.Query != "" && len(f.Matches) == 0:
counter = "No matches"
case f.Cur >= 0:
case f.Cur >= 0 && f.Cur < len(f.Matches):
counter = fmt.Sprintf("%d / %d", f.Cur+1, len(f.Matches))
}

View File

@ -685,6 +685,14 @@ func (r *Renderer) CheckGestures(q input.Source, m unit.Metric) []InputEvent {
// nothing.)
break
}
// A click region registered without a tap handler (the
// MAIN-owned GioEditor inputs, find_bar/search_bar) must not
// emit an input event: the widget handles its own click for
// focus/caret, and forwarding a nil handler would panic the
// logic goroutine. Skip it.
if reg.handler == nil {
break
}
if evt.NumClicks >= 2 {
events = append(events, InputEvent{
Handler: reg.handler,