- Archive previous implementation attempt (src -> src_failed) - Create comprehensive IMPLEMENTATION_PLAN.md with phased approach - Create IMPLEMENTATION.md to track implementation progress - Update example app to match vim-maze patterns: - Simplify UI to desktop-only (no mobile support) - Add score metrics display (time, keystrokes, final score) - Update instructions for sudoku-specific keys (hjkl, i for edit) - Remove unnecessary mobile hooks Phase 5 complete. Ready to begin Phase 10 (Setup & Core Types).
118 lines
2.9 KiB
TypeScript
118 lines
2.9 KiB
TypeScript
import { useRef } from 'react';
|
|
import type { VimMotionSystem, Motion } from './types';
|
|
|
|
export function useVimMotions(): VimMotionSystem {
|
|
const countRef = useRef<string>('');
|
|
const lastMotionRef = useRef<Motion | null>(null);
|
|
|
|
const processKey = (key: string) => {
|
|
if (key >= '0' && key <= '9') {
|
|
if (key === '0' && countRef.current === '') {
|
|
return { type: 'anchor', data: 'start' };
|
|
}
|
|
countRef.current += key;
|
|
return { type: 'count', data: key };
|
|
}
|
|
|
|
const count = countRef.current ? parseInt(countRef.current, 10) : 1;
|
|
|
|
switch (key) {
|
|
case 'h':
|
|
const motionH = { dr: 0, dc: -1, steps: count };
|
|
lastMotionRef.current = motionH;
|
|
countRef.current = '';
|
|
return { type: 'move', data: motionH };
|
|
|
|
case 'j':
|
|
const motionJ = { dr: 1, dc: 0, steps: count };
|
|
lastMotionRef.current = motionJ;
|
|
countRef.current = '';
|
|
return { type: 'move', data: motionJ };
|
|
|
|
case 'k':
|
|
const motionK = { dr: -1, dc: 0, steps: count };
|
|
lastMotionRef.current = motionK;
|
|
countRef.current = '';
|
|
return { type: 'move', data: motionK };
|
|
|
|
case 'l':
|
|
const motionL = { dr: 0, dc: 1, steps: count };
|
|
lastMotionRef.current = motionL;
|
|
countRef.current = '';
|
|
return { type: 'move', data: motionL };
|
|
|
|
case '^':
|
|
countRef.current = '';
|
|
return { type: 'anchor', data: 'start' };
|
|
|
|
case '$':
|
|
countRef.current = '';
|
|
return { type: 'anchor', data: 'end' };
|
|
|
|
case 'g':
|
|
countRef.current = '';
|
|
return { type: 'anchor', data: 'top' };
|
|
|
|
case 'G':
|
|
countRef.current = '';
|
|
return { type: 'anchor', data: 'bottom' };
|
|
|
|
case '.':
|
|
const lastMotion = lastMotionRef.current;
|
|
if (lastMotion) {
|
|
return { type: 'repeat', data: lastMotion };
|
|
}
|
|
return null;
|
|
|
|
case 'i':
|
|
countRef.current = '';
|
|
return { type: 'mode', data: 'edit' };
|
|
|
|
case 'Escape':
|
|
countRef.current = '';
|
|
return { type: 'mode', data: 'normal' };
|
|
|
|
case 'x':
|
|
case 'Delete':
|
|
case 'Backspace':
|
|
countRef.current = '';
|
|
return { type: 'erase', data: null };
|
|
|
|
default:
|
|
if (key >= '1' && key <= '9') {
|
|
countRef.current = '';
|
|
return { type: 'digit', data: key };
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const resetCount = () => {
|
|
countRef.current = '';
|
|
};
|
|
|
|
const getCount = () => {
|
|
return countRef.current ? parseInt(countRef.current, 10) : 1;
|
|
};
|
|
|
|
const hasCount = () => {
|
|
return countRef.current !== '';
|
|
};
|
|
|
|
const setLastMotion = (motion: Motion) => {
|
|
lastMotionRef.current = motion;
|
|
};
|
|
|
|
const repeatLastMotion = () => {
|
|
return lastMotionRef.current;
|
|
};
|
|
|
|
return {
|
|
processKey,
|
|
resetCount,
|
|
getCount,
|
|
hasCount,
|
|
setLastMotion,
|
|
repeatLastMotion
|
|
};
|
|
} |