types.ts
1 /** 2 * Vim Mode State Machine Types 3 * 4 * This file defines the complete state machine for vim input handling. 5 * The types ARE the documentation - reading them tells you how the system works. 6 * 7 * State Diagram: 8 * ``` 9 * VimState 10 * ┌──────────────────────────────┬──────────────────────────────────────┐ 11 * │ INSERT │ NORMAL │ 12 * │ (tracks insertedText) │ (CommandState machine) │ 13 * │ │ │ 14 * │ │ idle ──┬─[d/c/y]──► operator │ 15 * │ │ ├─[1-9]────► count │ 16 * │ │ ├─[fFtT]───► find │ 17 * │ │ ├─[g]──────► g │ 18 * │ │ ├─[r]──────► replace │ 19 * │ │ └─[><]─────► indent │ 20 * │ │ │ 21 * │ │ operator ─┬─[motion]──► execute │ 22 * │ │ ├─[0-9]────► operatorCount│ 23 * │ │ ├─[ia]─────► operatorTextObj 24 * │ │ └─[fFtT]───► operatorFind │ 25 * └──────────────────────────────┴──────────────────────────────────────┘ 26 * ``` 27 */ 28 29 // ============================================================================ 30 // Core Types 31 // ============================================================================ 32 33 export type Operator = 'delete' | 'change' | 'yank' 34 35 export type FindType = 'f' | 'F' | 't' | 'T' 36 37 export type TextObjScope = 'inner' | 'around' 38 39 // ============================================================================ 40 // State Machine Types 41 // ============================================================================ 42 43 /** 44 * Complete vim state. Mode determines what data is tracked. 45 * 46 * INSERT mode: Track text being typed (for dot-repeat) 47 * NORMAL mode: Track command being parsed (state machine) 48 */ 49 export type VimState = 50 | { mode: 'INSERT'; insertedText: string } 51 | { mode: 'NORMAL'; command: CommandState } 52 53 /** 54 * Command state machine for NORMAL mode. 55 * 56 * Each state knows exactly what input it's waiting for. 57 * TypeScript ensures exhaustive handling in switches. 58 */ 59 export type CommandState = 60 | { type: 'idle' } 61 | { type: 'count'; digits: string } 62 | { type: 'operator'; op: Operator; count: number } 63 | { type: 'operatorCount'; op: Operator; count: number; digits: string } 64 | { type: 'operatorFind'; op: Operator; count: number; find: FindType } 65 | { 66 type: 'operatorTextObj' 67 op: Operator 68 count: number 69 scope: TextObjScope 70 } 71 | { type: 'find'; find: FindType; count: number } 72 | { type: 'g'; count: number } 73 | { type: 'operatorG'; op: Operator; count: number } 74 | { type: 'replace'; count: number } 75 | { type: 'indent'; dir: '>' | '<'; count: number } 76 77 /** 78 * Persistent state that survives across commands. 79 * This is the "memory" of vim - what gets recalled for repeats and pastes. 80 */ 81 export type PersistentState = { 82 lastChange: RecordedChange | null 83 lastFind: { type: FindType; char: string } | null 84 register: string 85 registerIsLinewise: boolean 86 } 87 88 /** 89 * Recorded change for dot-repeat. 90 * Captures everything needed to replay a command. 91 */ 92 export type RecordedChange = 93 | { type: 'insert'; text: string } 94 | { 95 type: 'operator' 96 op: Operator 97 motion: string 98 count: number 99 } 100 | { 101 type: 'operatorTextObj' 102 op: Operator 103 objType: string 104 scope: TextObjScope 105 count: number 106 } 107 | { 108 type: 'operatorFind' 109 op: Operator 110 find: FindType 111 char: string 112 count: number 113 } 114 | { type: 'replace'; char: string; count: number } 115 | { type: 'x'; count: number } 116 | { type: 'toggleCase'; count: number } 117 | { type: 'indent'; dir: '>' | '<'; count: number } 118 | { type: 'openLine'; direction: 'above' | 'below' } 119 | { type: 'join'; count: number } 120 121 // ============================================================================ 122 // Key Groups - Named constants, no magic strings 123 // ============================================================================ 124 125 export const OPERATORS = { 126 d: 'delete', 127 c: 'change', 128 y: 'yank', 129 } as const satisfies Record<string, Operator> 130 131 export function isOperatorKey(key: string): key is keyof typeof OPERATORS { 132 return key in OPERATORS 133 } 134 135 export const SIMPLE_MOTIONS = new Set([ 136 'h', 137 'l', 138 'j', 139 'k', // Basic movement 140 'w', 141 'b', 142 'e', 143 'W', 144 'B', 145 'E', // Word motions 146 '0', 147 '^', 148 '$', // Line positions 149 ]) 150 151 export const FIND_KEYS = new Set(['f', 'F', 't', 'T']) 152 153 export const TEXT_OBJ_SCOPES = { 154 i: 'inner', 155 a: 'around', 156 } as const satisfies Record<string, TextObjScope> 157 158 export function isTextObjScopeKey( 159 key: string, 160 ): key is keyof typeof TEXT_OBJ_SCOPES { 161 return key in TEXT_OBJ_SCOPES 162 } 163 164 export const TEXT_OBJ_TYPES = new Set([ 165 'w', 166 'W', // Word/WORD 167 '"', 168 "'", 169 '`', // Quotes 170 '(', 171 ')', 172 'b', // Parens 173 '[', 174 ']', // Brackets 175 '{', 176 '}', 177 'B', // Braces 178 '<', 179 '>', // Angle brackets 180 ]) 181 182 export const MAX_VIM_COUNT = 10000 183 184 // ============================================================================ 185 // State Factories 186 // ============================================================================ 187 188 export function createInitialVimState(): VimState { 189 return { mode: 'INSERT', insertedText: '' } 190 } 191 192 export function createInitialPersistentState(): PersistentState { 193 return { 194 lastChange: null, 195 lastFind: null, 196 register: '', 197 registerIsLinewise: false, 198 } 199 }