Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
12 kB · 490 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491/** * Vim State Transition Table * * This is the scannable source of truth for state transitions. * To understand what happens in any state, look up that state's transition function. */
import { resolveMotion } from './motions.js'import { executeIndent, executeJoin, executeLineOp, executeOpenLine, executeOperatorFind, executeOperatorG, executeOperatorGg, executeOperatorMotion, executeOperatorTextObj, executePaste, executeReplace, executeToggleCase, executeX, type OperatorContext,} from './operators.js'import { type CommandState, FIND_KEYS, type FindType, isOperatorKey, isTextObjScopeKey, MAX_VIM_COUNT, OPERATORS, type Operator, SIMPLE_MOTIONS, TEXT_OBJ_SCOPES, TEXT_OBJ_TYPES, type TextObjScope,} from './types.js'
/** * Context passed to transition functions. */export type TransitionContext = OperatorContext & { onUndo?: () => void onDotRepeat?: () => void}
/** * Result of a transition. */export type TransitionResult = { next?: CommandState execute?: () => void}
/** * Main transition function. Dispatches based on current state type. */export function transition( state: CommandState, input: string, ctx: TransitionContext,): TransitionResult { switch (state.type) { case 'idle': return fromIdle(input, ctx) case 'count': return fromCount(state, input, ctx) case 'operator': return fromOperator(state, input, ctx) case 'operatorCount': return fromOperatorCount(state, input, ctx) case 'operatorFind': return fromOperatorFind(state, input, ctx) case 'operatorTextObj': return fromOperatorTextObj(state, input, ctx) case 'find': return fromFind(state, input, ctx) case 'g': return fromG(state, input, ctx) case 'operatorG': return fromOperatorG(state, input, ctx) case 'replace': return fromReplace(state, input, ctx) case 'indent': return fromIndent(state, input, ctx) }}
// ============================================================================// Shared Input Handling// ============================================================================
/** * Handle input that's valid in both idle and count states. * Returns null if input is not recognized. */function handleNormalInput( input: string, count: number, ctx: TransitionContext,): TransitionResult | null { if (isOperatorKey(input)) { return { next: { type: 'operator', op: OPERATORS[input], count } } }
if (SIMPLE_MOTIONS.has(input)) { return { execute: () => { const target = resolveMotion(input, ctx.cursor, count) ctx.setOffset(target.offset) }, } }
if (FIND_KEYS.has(input)) { return { next: { type: 'find', find: input as FindType, count } } }
if (input === 'g') return { next: { type: 'g', count } } if (input === 'r') return { next: { type: 'replace', count } } if (input === '>' || input === '<') { return { next: { type: 'indent', dir: input, count } } } if (input === '~') { return { execute: () => executeToggleCase(count, ctx) } } if (input === 'x') { return { execute: () => executeX(count, ctx) } } if (input === 'J') { return { execute: () => executeJoin(count, ctx) } } if (input === 'p' || input === 'P') { return { execute: () => executePaste(input === 'p', count, ctx) } } if (input === 'D') { return { execute: () => executeOperatorMotion('delete', '$', 1, ctx) } } if (input === 'C') { return { execute: () => executeOperatorMotion('change', '$', 1, ctx) } } if (input === 'Y') { return { execute: () => executeLineOp('yank', count, ctx) } } if (input === 'G') { return { execute: () => { // count=1 means no count given, go to last line // otherwise go to line N if (count === 1) { ctx.setOffset(ctx.cursor.startOfLastLine().offset) } else { ctx.setOffset(ctx.cursor.goToLine(count).offset) } }, } } if (input === '.') { return { execute: () => ctx.onDotRepeat?.() } } if (input === ';' || input === ',') { return { execute: () => executeRepeatFind(input === ',', count, ctx) } } if (input === 'u') { return { execute: () => ctx.onUndo?.() } } if (input === 'i') { return { execute: () => ctx.enterInsert(ctx.cursor.offset) } } if (input === 'I') { return { execute: () => ctx.enterInsert(ctx.cursor.firstNonBlankInLogicalLine().offset), } } if (input === 'a') { return { execute: () => { const newOffset = ctx.cursor.isAtEnd() ? ctx.cursor.offset : ctx.cursor.right().offset ctx.enterInsert(newOffset) }, } } if (input === 'A') { return { execute: () => ctx.enterInsert(ctx.cursor.endOfLogicalLine().offset), } } if (input === 'o') { return { execute: () => executeOpenLine('below', ctx) } } if (input === 'O') { return { execute: () => executeOpenLine('above', ctx) } }
return null}
/** * Handle operator input (motion, find, text object scope). * Returns null if input is not recognized. */function handleOperatorInput( op: Operator, count: number, input: string, ctx: TransitionContext,): TransitionResult | null { if (isTextObjScopeKey(input)) { return { next: { type: 'operatorTextObj', op, count, scope: TEXT_OBJ_SCOPES[input], }, } }
if (FIND_KEYS.has(input)) { return { next: { type: 'operatorFind', op, count, find: input as FindType }, } }
if (SIMPLE_MOTIONS.has(input)) { return { execute: () => executeOperatorMotion(op, input, count, ctx) } }
if (input === 'G') { return { execute: () => executeOperatorG(op, count, ctx) } }
if (input === 'g') { return { next: { type: 'operatorG', op, count } } }
return null}
// ============================================================================// Transition Functions - One per state type// ============================================================================
function fromIdle(input: string, ctx: TransitionContext): TransitionResult { // 0 is line-start motion, not a count prefix if (/[1-9]/.test(input)) { return { next: { type: 'count', digits: input } } } if (input === '0') { return { execute: () => ctx.setOffset(ctx.cursor.startOfLogicalLine().offset), } }
const result = handleNormalInput(input, 1, ctx) if (result) return result
return {}}
function fromCount( state: { type: 'count'; digits: string }, input: string, ctx: TransitionContext,): TransitionResult { if (/[0-9]/.test(input)) { const newDigits = state.digits + input const count = Math.min(parseInt(newDigits, 10), MAX_VIM_COUNT) return { next: { type: 'count', digits: String(count) } } }
const count = parseInt(state.digits, 10) const result = handleNormalInput(input, count, ctx) if (result) return result
return { next: { type: 'idle' } }}
function fromOperator( state: { type: 'operator'; op: Operator; count: number }, input: string, ctx: TransitionContext,): TransitionResult { // dd, cc, yy = line operation if (input === state.op[0]) { return { execute: () => executeLineOp(state.op, state.count, ctx) } }
if (/[0-9]/.test(input)) { return { next: { type: 'operatorCount', op: state.op, count: state.count, digits: input, }, } }
const result = handleOperatorInput(state.op, state.count, input, ctx) if (result) return result
return { next: { type: 'idle' } }}
function fromOperatorCount( state: { type: 'operatorCount' op: Operator count: number digits: string }, input: string, ctx: TransitionContext,): TransitionResult { if (/[0-9]/.test(input)) { const newDigits = state.digits + input const parsedDigits = Math.min(parseInt(newDigits, 10), MAX_VIM_COUNT) return { next: { ...state, digits: String(parsedDigits) } } }
const motionCount = parseInt(state.digits, 10) const effectiveCount = state.count * motionCount const result = handleOperatorInput(state.op, effectiveCount, input, ctx) if (result) return result
return { next: { type: 'idle' } }}
function fromOperatorFind( state: { type: 'operatorFind' op: Operator count: number find: FindType }, input: string, ctx: TransitionContext,): TransitionResult { return { execute: () => executeOperatorFind(state.op, state.find, input, state.count, ctx), }}
function fromOperatorTextObj( state: { type: 'operatorTextObj' op: Operator count: number scope: TextObjScope }, input: string, ctx: TransitionContext,): TransitionResult { if (TEXT_OBJ_TYPES.has(input)) { return { execute: () => executeOperatorTextObj(state.op, state.scope, input, state.count, ctx), } } return { next: { type: 'idle' } }}
function fromFind( state: { type: 'find'; find: FindType; count: number }, input: string, ctx: TransitionContext,): TransitionResult { return { execute: () => { const result = ctx.cursor.findCharacter(input, state.find, state.count) if (result !== null) { ctx.setOffset(result) ctx.setLastFind(state.find, input) } }, }}
function fromG( state: { type: 'g'; count: number }, input: string, ctx: TransitionContext,): TransitionResult { if (input === 'j' || input === 'k') { return { execute: () => { const target = resolveMotion(`g${input}`, ctx.cursor, state.count) ctx.setOffset(target.offset) }, } } if (input === 'g') { // If count provided (e.g., 5gg), go to that line. Otherwise go to first line. if (state.count > 1) { return { execute: () => { const lines = ctx.text.split('\n') const targetLine = Math.min(state.count - 1, lines.length - 1) let offset = 0 for (let i = 0; i < targetLine; i++) { offset += (lines[i]?.length ?? 0) + 1 // +1 for newline } ctx.setOffset(offset) }, } } return { execute: () => ctx.setOffset(ctx.cursor.startOfFirstLine().offset), } } return { next: { type: 'idle' } }}
function fromOperatorG( state: { type: 'operatorG'; op: Operator; count: number }, input: string, ctx: TransitionContext,): TransitionResult { if (input === 'j' || input === 'k') { return { execute: () => executeOperatorMotion(state.op, `g${input}`, state.count, ctx), } } if (input === 'g') { return { execute: () => executeOperatorGg(state.op, state.count, ctx) } } // Any other input cancels the operator return { next: { type: 'idle' } }}
function fromReplace( state: { type: 'replace'; count: number }, input: string, ctx: TransitionContext,): TransitionResult { // Backspace/Delete arrive as empty input in literal-char states. In vim, // r<BS> cancels the replace; without this guard, executeReplace("") would // delete the character under the cursor instead. if (input === '') return { next: { type: 'idle' } } return { execute: () => executeReplace(input, state.count, ctx) }}
function fromIndent( state: { type: 'indent'; dir: '>' | '<'; count: number }, input: string, ctx: TransitionContext,): TransitionResult { if (input === state.dir) { return { execute: () => executeIndent(state.dir, state.count, ctx) } } return { next: { type: 'idle' } }}
// ============================================================================// Helper functions for special commands// ============================================================================
function executeRepeatFind( reverse: boolean, count: number, ctx: TransitionContext,): void { const lastFind = ctx.getLastFind() if (!lastFind) return
// Determine the effective find type based on reverse let findType = lastFind.type if (reverse) { // Flip the direction const flipMap: Record<FindType, FindType> = { f: 'F', F: 'f', t: 'T', T: 't', } findType = flipMap[findType] }
const result = ctx.cursor.findCharacter(lastFind.char, findType, count) if (result !== null) { ctx.setOffset(result) }}