200 lines
6.2 KiB
TypeScript
200 lines
6.2 KiB
TypeScript
/**
|
|
* Vim Mode State Machine Types
|
|
*
|
|
* This file defines the complete state machine for vim input handling.
|
|
* The types ARE the documentation - reading them tells you how the system works.
|
|
*
|
|
* State Diagram:
|
|
* ```
|
|
* VimState
|
|
* ┌──────────────────────────────┬──────────────────────────────────────┐
|
|
* │ INSERT │ NORMAL │
|
|
* │ (tracks insertedText) │ (CommandState machine) │
|
|
* │ │ │
|
|
* │ │ idle ──┬─[d/c/y]──► operator │
|
|
* │ │ ├─[1-9]────► count │
|
|
* │ │ ├─[fFtT]───► find │
|
|
* │ │ ├─[g]──────► g │
|
|
* │ │ ├─[r]──────► replace │
|
|
* │ │ └─[><]─────► indent │
|
|
* │ │ │
|
|
* │ │ operator ─┬─[motion]──► execute │
|
|
* │ │ ├─[0-9]────► operatorCount│
|
|
* │ │ ├─[ia]─────► operatorTextObj
|
|
* │ │ └─[fFtT]───► operatorFind │
|
|
* └──────────────────────────────┴──────────────────────────────────────┘
|
|
* ```
|
|
*/
|
|
|
|
// ============================================================================
|
|
// Core Types
|
|
// ============================================================================
|
|
|
|
export type Operator = 'delete' | 'change' | 'yank'
|
|
|
|
export type FindType = 'f' | 'F' | 't' | 'T'
|
|
|
|
export type TextObjScope = 'inner' | 'around'
|
|
|
|
// ============================================================================
|
|
// State Machine Types
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Complete vim state. Mode determines what data is tracked.
|
|
*
|
|
* INSERT mode: Track text being typed (for dot-repeat)
|
|
* NORMAL mode: Track command being parsed (state machine)
|
|
*/
|
|
export type VimState =
|
|
| { mode: 'INSERT'; insertedText: string }
|
|
| { mode: 'NORMAL'; command: CommandState }
|
|
|
|
/**
|
|
* Command state machine for NORMAL mode.
|
|
*
|
|
* Each state knows exactly what input it's waiting for.
|
|
* TypeScript ensures exhaustive handling in switches.
|
|
*/
|
|
export type CommandState =
|
|
| { type: 'idle' }
|
|
| { type: 'count'; digits: string }
|
|
| { type: 'operator'; op: Operator; count: number }
|
|
| { type: 'operatorCount'; op: Operator; count: number; digits: string }
|
|
| { type: 'operatorFind'; op: Operator; count: number; find: FindType }
|
|
| {
|
|
type: 'operatorTextObj'
|
|
op: Operator
|
|
count: number
|
|
scope: TextObjScope
|
|
}
|
|
| { type: 'find'; find: FindType; count: number }
|
|
| { type: 'g'; count: number }
|
|
| { type: 'operatorG'; op: Operator; count: number }
|
|
| { type: 'replace'; count: number }
|
|
| { type: 'indent'; dir: '>' | '<'; count: number }
|
|
|
|
/**
|
|
* Persistent state that survives across commands.
|
|
* This is the "memory" of vim - what gets recalled for repeats and pastes.
|
|
*/
|
|
export type PersistentState = {
|
|
lastChange: RecordedChange | null
|
|
lastFind: { type: FindType; char: string } | null
|
|
register: string
|
|
registerIsLinewise: boolean
|
|
}
|
|
|
|
/**
|
|
* Recorded change for dot-repeat.
|
|
* Captures everything needed to replay a command.
|
|
*/
|
|
export type RecordedChange =
|
|
| { type: 'insert'; text: string }
|
|
| {
|
|
type: 'operator'
|
|
op: Operator
|
|
motion: string
|
|
count: number
|
|
}
|
|
| {
|
|
type: 'operatorTextObj'
|
|
op: Operator
|
|
objType: string
|
|
scope: TextObjScope
|
|
count: number
|
|
}
|
|
| {
|
|
type: 'operatorFind'
|
|
op: Operator
|
|
find: FindType
|
|
char: string
|
|
count: number
|
|
}
|
|
| { type: 'replace'; char: string; count: number }
|
|
| { type: 'x'; count: number }
|
|
| { type: 'toggleCase'; count: number }
|
|
| { type: 'indent'; dir: '>' | '<'; count: number }
|
|
| { type: 'openLine'; direction: 'above' | 'below' }
|
|
| { type: 'join'; count: number }
|
|
|
|
// ============================================================================
|
|
// Key Groups - Named constants, no magic strings
|
|
// ============================================================================
|
|
|
|
export const OPERATORS = {
|
|
d: 'delete',
|
|
c: 'change',
|
|
y: 'yank',
|
|
} as const satisfies Record<string, Operator>
|
|
|
|
export function isOperatorKey(key: string): key is keyof typeof OPERATORS {
|
|
return key in OPERATORS
|
|
}
|
|
|
|
export const SIMPLE_MOTIONS = new Set([
|
|
'h',
|
|
'l',
|
|
'j',
|
|
'k', // Basic movement
|
|
'w',
|
|
'b',
|
|
'e',
|
|
'W',
|
|
'B',
|
|
'E', // Word motions
|
|
'0',
|
|
'^',
|
|
'$', // Line positions
|
|
])
|
|
|
|
export const FIND_KEYS = new Set(['f', 'F', 't', 'T'])
|
|
|
|
export const TEXT_OBJ_SCOPES = {
|
|
i: 'inner',
|
|
a: 'around',
|
|
} as const satisfies Record<string, TextObjScope>
|
|
|
|
export function isTextObjScopeKey(
|
|
key: string,
|
|
): key is keyof typeof TEXT_OBJ_SCOPES {
|
|
return key in TEXT_OBJ_SCOPES
|
|
}
|
|
|
|
export const TEXT_OBJ_TYPES = new Set([
|
|
'w',
|
|
'W', // Word/WORD
|
|
'"',
|
|
"'",
|
|
'`', // Quotes
|
|
'(',
|
|
')',
|
|
'b', // Parens
|
|
'[',
|
|
']', // Brackets
|
|
'{',
|
|
'}',
|
|
'B', // Braces
|
|
'<',
|
|
'>', // Angle brackets
|
|
])
|
|
|
|
export const MAX_VIM_COUNT = 10000
|
|
|
|
// ============================================================================
|
|
// State Factories
|
|
// ============================================================================
|
|
|
|
export function createInitialVimState(): VimState {
|
|
return { mode: 'INSERT', insertedText: '' }
|
|
}
|
|
|
|
export function createInitialPersistentState(): PersistentState {
|
|
return {
|
|
lastChange: null,
|
|
lastFind: null,
|
|
register: '',
|
|
registerIsLinewise: false,
|
|
}
|
|
}
|