From ebd264150d4d8ae1a502c793edb7e8006005434c Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Mon, 26 Oct 2026 22:19:44 -0400 Subject: [PATCH] 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. --- internal/editor/logic.go | 13 +++++++++++++ internal/editor/search.go | 5 ++++- internal/editor/state.go | 2 +- internal/ui/render.go | 8 ++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/internal/editor/logic.go b/internal/editor/logic.go index 4a11e1d..bf07134 100644 --- a/internal/editor/logic.go +++ b/internal/editor/logic.go @@ -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 diff --git a/internal/editor/search.go b/internal/editor/search.go index e83fe05..826c95c 100644 --- a/internal/editor/search.go +++ b/internal/editor/search.go @@ -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" diff --git a/internal/editor/state.go b/internal/editor/state.go index 2391c6e..08a5869 100644 --- a/internal/editor/state.go +++ b/internal/editor/state.go @@ -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)) } diff --git a/internal/ui/render.go b/internal/ui/render.go index ec05028..2b8b878 100644 --- a/internal/ui/render.go +++ b/internal/ui/render.go @@ -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,