(props: UseVimInputProps)
| 32 | } |
| 33 | |
| 34 | export function useVimInput(props: UseVimInputProps): VimInputState { |
| 35 | const vimStateRef = React.useRef<VimState>(createInitialVimState()) |
| 36 | const [mode, setMode] = useState<VimMode>('INSERT') |
| 37 | |
| 38 | const persistentRef = React.useRef<PersistentState>( |
| 39 | createInitialPersistentState(), |
| 40 | ) |
| 41 | |
| 42 | // inputFilter is applied once at the top of handleVimInput (not here) so |
| 43 | // vim-handled paths that return without calling textInput.onInput still |
| 44 | // run the filter — otherwise a stateful filter (e.g. lazy-space-after- |
| 45 | // pill) stays armed across an Escape → NORMAL → INSERT round-trip. |
| 46 | const textInput = useTextInput({ ...props, inputFilter: undefined }) |
| 47 | const { onModeChange, inputFilter } = props |
| 48 | |
| 49 | const switchToInsertMode = useCallback( |
| 50 | (offset?: number): void => { |
| 51 | if (offset !== undefined) { |
| 52 | textInput.setOffset(offset) |
| 53 | } |
| 54 | vimStateRef.current = { mode: 'INSERT', insertedText: '' } |
| 55 | setMode('INSERT') |
| 56 | onModeChange?.('INSERT') |
| 57 | }, |
| 58 | [textInput, onModeChange], |
| 59 | ) |
| 60 | |
| 61 | const switchToNormalMode = useCallback((): void => { |
| 62 | const current = vimStateRef.current |
| 63 | if (current.mode === 'INSERT' && current.insertedText) { |
| 64 | persistentRef.current.lastChange = { |
| 65 | type: 'insert', |
| 66 | text: current.insertedText, |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | // Vim behavior: move cursor left by 1 when exiting insert mode |
| 71 | // (unless at beginning of line or at offset 0) |
| 72 | const offset = textInput.offset |
| 73 | if (offset > 0 && props.value[offset - 1] !== '\n') { |
| 74 | textInput.setOffset(offset - 1) |
| 75 | } |
| 76 | |
| 77 | vimStateRef.current = { mode: 'NORMAL', command: { type: 'idle' } } |
| 78 | setMode('NORMAL') |
| 79 | onModeChange?.('NORMAL') |
| 80 | }, [onModeChange, textInput, props.value]) |
| 81 | |
| 82 | function createOperatorContext( |
| 83 | cursor: Cursor, |
| 84 | isReplay: boolean = false, |
| 85 | ): OperatorContext { |
| 86 | return { |
| 87 | cursor, |
| 88 | text: props.value, |
| 89 | setText: (newText: string) => props.onChange(newText), |
| 90 | setOffset: (offset: number) => textInput.setOffset(offset), |
| 91 | enterInsert: (offset: number) => switchToInsertMode(offset), |
no test coverage detected