- 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)
89 lines
3.3 KiB
Go
89 lines
3.3 KiB
Go
package editor
|
|
|
|
import (
|
|
"time"
|
|
|
|
"pad/internal/ui"
|
|
)
|
|
|
|
// Frame is the unit of handoff from the logic goroutine to the frame
|
|
// receiver. Per architecture.md §9, the frame is the ONLY cross-goroutine
|
|
// state carrier: the main goroutine must never read *State directly, it
|
|
// reads the snapshot stored here by the frame receiver (under the frame
|
|
// mutex).
|
|
//
|
|
// Elems is the computed element tree for the next draw.
|
|
// The remaining fields are the view-state snapshot that the main goroutine
|
|
// needs to route events and forward input:
|
|
//
|
|
// - Scale: current px-per-Dp, so the main goroutine can detect scale
|
|
// changes and pass the scale to the renderer's Draw call.
|
|
// - FocusedElementID: which registered element receives key/edit events.
|
|
// - Query: the search query the logic goroutine is currently filtering
|
|
// with; the main goroutine compares it against the (main-owned) search
|
|
// widget's text and forwards changes via SearchQueryChan.
|
|
type Frame struct {
|
|
Elems []ui.Element
|
|
Scale float32
|
|
FontScale float32 // user font-size setting the logic bookkeeping used
|
|
FocusedElementID string
|
|
Query string
|
|
// FindQuery: the in-file search query the logic goroutine has processed
|
|
// (mirrors EditorState.Find.Query). Main compares it against the
|
|
// "find_bar" widget's text and forwards changes via FindQueryChan — the
|
|
// same contract as Query/searchQueryChan.
|
|
FindQuery string
|
|
// WindowStartByte / WindowStartLine / EditSeq: the editor window this
|
|
// frame's elements describe. The main goroutine forwards them with the
|
|
// shaped glyph layout (LayoutFeedback) so the logic goroutine can apply
|
|
// the layout's wrap counts to exactly the lines it was shaped for, and
|
|
// drop them if an edit landed in the meantime.
|
|
WindowStartByte int
|
|
WindowStartLine int // -1 when the frame has no editor window
|
|
WindowText string // the editor window this frame's text element holds
|
|
EditSeq uint64
|
|
}
|
|
|
|
// frameOf wraps a computed element tree with the current view-state
|
|
// snapshot. Must be called on the logic goroutine.
|
|
func (l *Logic) frameOf(elems []ui.Element) Frame {
|
|
return Frame{
|
|
Elems: elems,
|
|
Scale: l.state.scale,
|
|
FontScale: l.state.fontScale,
|
|
FocusedElementID: l.state.FocusedElementID,
|
|
Query: l.state.Browser.Query,
|
|
FindQuery: l.state.Editor.Find.Query,
|
|
WindowStartByte: l.state.Editor.IMEWindowStartByte,
|
|
WindowStartLine: l.state.WindowStartLine,
|
|
WindowText: l.state.Editor.IMEWindowText,
|
|
EditSeq: l.state.Editor.EditSeq,
|
|
}
|
|
}
|
|
|
|
// inspectReq is a test-only request to run fn on the logic goroutine.
|
|
// It preserves the single-owner invariant (architecture.md §1): the fn
|
|
// executes on the owner, not on the caller. fn must not block on sends to
|
|
// logic channels.
|
|
type inspectReq struct {
|
|
fn func(st *State) any
|
|
resp chan any
|
|
}
|
|
|
|
// Inspect runs fn on the logic goroutine and returns its result. It is
|
|
// intended for tests; production code must use the regular channels.
|
|
func (l *Logic) Inspect(fn func(st *State) any) (any, bool) {
|
|
req := &inspectReq{fn: fn, resp: make(chan any, 1)}
|
|
select {
|
|
case l.inspectChan <- req:
|
|
case <-time.After(5 * time.Second):
|
|
return nil, false
|
|
}
|
|
select {
|
|
case v := <-req.resp:
|
|
return v, true
|
|
case <-time.After(5 * time.Second):
|
|
return nil, false
|
|
}
|
|
}
|