fix(input): cancel vim sequence state on detach
- commit
- 7a23056139f437c312b3434582b2d336054f06d2
- author
- brice · · 2026-07-29 07:33 UTC
- committer
- bnema · · 2026-08-14 02:42 UTC
- parent
- 2352786
- signature
- unsigned
2 files · +20 -0
M internal/ui/input/keyboard.go +3 -0
| @@ -345,6 +345,9 @@ func (h *KeyboardHandler) DetachForDestroy() { | |||
| 345 | 345 | ||
| 346 | 346 | func (h *KeyboardHandler) detach(removeController bool) { | |
| 347 | 347 | h.stopVimScrollRepeat() | |
| 348 | + | // Sequence teardown uses seq.mu only; keep it outside h.mu to preserve | |
| 349 | + | // the existing h.mu → (never seq) / seq.mu standalone lock order. | |
| 350 | + | h.teardownSequenceState() | |
| 348 | 351 | h.mu.Lock() | |
| 349 | 352 | if h.controller != nil && h.keyPressedHandlerID != 0 { | |
| 350 | 353 | h.controller.DisconnectSignal(h.keyPressedHandlerID) | |
| @@ -345,6 +345,9 @@ func (h *KeyboardHandler) DetachForDestroy() { | |||||
| 345 | 345 | ||||
| 346 | func (h *KeyboardHandler) detach(removeController bool) { | 346 | func (h *KeyboardHandler) detach(removeController bool) { | ||
| 347 | h.stopVimScrollRepeat() | 347 | h.stopVimScrollRepeat() | ||
| 348 | + | // Sequence teardown uses seq.mu only; keep it outside h.mu to preserve | |||
| 349 | + | // the existing h.mu → (never seq) / seq.mu standalone lock order. | |||
| 350 | + | h.teardownSequenceState() | |||
| 348 | h.mu.Lock() | 351 | h.mu.Lock() | ||
| 349 | if h.controller != nil && h.keyPressedHandlerID != 0 { | 352 | if h.controller != nil && h.keyPressedHandlerID != 0 { | ||
| 350 | h.controller.DisconnectSignal(h.keyPressedHandlerID) | 353 | h.controller.DisconnectSignal(h.keyPressedHandlerID) | ||
M internal/ui/input/vimkeys_dispatch.go +17 -0
| @@ -180,6 +180,23 @@ func (h *KeyboardHandler) resetPendingSequence(notify bool) { | |||
| 180 | 180 | } | |
| 181 | 181 | } | |
| 182 | 182 | ||
| 183 | + | // teardownSequenceState linearizably cancels sequence timing and clears | |
| 184 | + | // UI-retaining callbacks without notifying. Used by Detach/DetachForDestroy. | |
| 185 | + | // Lock order: call without holding h.mu (same as resetPendingSequence). | |
| 186 | + | func (h *KeyboardHandler) teardownSequenceState() { | |
| 187 | + | h.seq.mu.Lock() | |
| 188 | + | defer h.seq.mu.Unlock() | |
| 189 | + | h.stopSequenceTimerLocked() | |
| 190 | + | h.seq.generation++ | |
| 191 | + | if h.seq.matcher != nil { | |
| 192 | + | h.seq.matcher.Reset() | |
| 193 | + | } | |
| 194 | + | h.seq.onPending = nil | |
| 195 | + | h.seq.onAction = nil | |
| 196 | + | h.seq.scheduleOnMainThread = nil | |
| 197 | + | log.Debug().Msg("page mode sequence state torn down without notify") | |
| 198 | + | } | |
| 199 | + | ||
| 183 | 200 | // InvalidateSequenceMatcher stops timing, bumps generation, and replaces the | |
| 184 | 201 | // matcher from the current VimModeSequences trie (hot reload). | |
| 185 | 202 | func (h *KeyboardHandler) InvalidateSequenceMatcher() { | |
| @@ -180,6 +180,23 @@ func (h *KeyboardHandler) resetPendingSequence(notify bool) { | |||||
| 180 | } | 180 | } | ||
| 181 | } | 181 | } | ||
| 182 | 182 | ||||
| 183 | + | // teardownSequenceState linearizably cancels sequence timing and clears | |||
| 184 | + | // UI-retaining callbacks without notifying. Used by Detach/DetachForDestroy. | |||
| 185 | + | // Lock order: call without holding h.mu (same as resetPendingSequence). | |||
| 186 | + | func (h *KeyboardHandler) teardownSequenceState() { | |||
| 187 | + | h.seq.mu.Lock() | |||
| 188 | + | defer h.seq.mu.Unlock() | |||
| 189 | + | h.stopSequenceTimerLocked() | |||
| 190 | + | h.seq.generation++ | |||
| 191 | + | if h.seq.matcher != nil { | |||
| 192 | + | h.seq.matcher.Reset() | |||
| 193 | + | } | |||
| 194 | + | h.seq.onPending = nil | |||
| 195 | + | h.seq.onAction = nil | |||
| 196 | + | h.seq.scheduleOnMainThread = nil | |||
| 197 | + | log.Debug().Msg("page mode sequence state torn down without notify") | |||
| 198 | + | } | |||
| 199 | + | ||||
| 183 | // InvalidateSequenceMatcher stops timing, bumps generation, and replaces the | 200 | // InvalidateSequenceMatcher stops timing, bumps generation, and replaces the | ||
| 184 | // matcher from the current VimModeSequences trie (hot reload). | 201 | // matcher from the current VimModeSequences trie (hot reload). | ||
| 185 | func (h *KeyboardHandler) InvalidateSequenceMatcher() { | 202 | func (h *KeyboardHandler) InvalidateSequenceMatcher() { | ||