yzcode-cli 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assistant/sessionHistory.ts +87 -0
- package/bootstrap/state.ts +1769 -0
- package/bridge/bridgeApi.ts +539 -0
- package/bridge/bridgeConfig.ts +48 -0
- package/bridge/bridgeDebug.ts +135 -0
- package/bridge/bridgeEnabled.ts +202 -0
- package/bridge/bridgeMain.ts +2999 -0
- package/bridge/bridgeMessaging.ts +461 -0
- package/bridge/bridgePermissionCallbacks.ts +43 -0
- package/bridge/bridgePointer.ts +210 -0
- package/bridge/bridgeStatusUtil.ts +163 -0
- package/bridge/bridgeUI.ts +530 -0
- package/bridge/capacityWake.ts +56 -0
- package/bridge/codeSessionApi.ts +168 -0
- package/bridge/createSession.ts +384 -0
- package/bridge/debugUtils.ts +141 -0
- package/bridge/envLessBridgeConfig.ts +165 -0
- package/bridge/flushGate.ts +71 -0
- package/bridge/inboundAttachments.ts +175 -0
- package/bridge/inboundMessages.ts +80 -0
- package/bridge/initReplBridge.ts +569 -0
- package/bridge/jwtUtils.ts +256 -0
- package/bridge/pollConfig.ts +110 -0
- package/bridge/pollConfigDefaults.ts +82 -0
- package/bridge/remoteBridgeCore.ts +1008 -0
- package/bridge/replBridge.ts +2406 -0
- package/bridge/replBridgeHandle.ts +36 -0
- package/bridge/replBridgeTransport.ts +370 -0
- package/bridge/sessionIdCompat.ts +57 -0
- package/bridge/sessionRunner.ts +550 -0
- package/bridge/trustedDevice.ts +210 -0
- package/bridge/types.ts +262 -0
- package/bridge/workSecret.ts +127 -0
- package/buddy/CompanionSprite.tsx +371 -0
- package/buddy/companion.ts +133 -0
- package/buddy/prompt.ts +36 -0
- package/buddy/sprites.ts +514 -0
- package/buddy/types.ts +148 -0
- package/buddy/useBuddyNotification.tsx +98 -0
- package/coordinator/coordinatorMode.ts +369 -0
- package/memdir/findRelevantMemories.ts +141 -0
- package/memdir/memdir.ts +507 -0
- package/memdir/memoryAge.ts +53 -0
- package/memdir/memoryScan.ts +94 -0
- package/memdir/memoryTypes.ts +271 -0
- package/memdir/paths.ts +278 -0
- package/memdir/teamMemPaths.ts +292 -0
- package/memdir/teamMemPrompts.ts +100 -0
- package/migrations/migrateAutoUpdatesToSettings.ts +61 -0
- package/migrations/migrateBypassPermissionsAcceptedToSettings.ts +40 -0
- package/migrations/migrateEnableAllProjectMcpServersToSettings.ts +118 -0
- package/migrations/migrateFennecToOpus.ts +45 -0
- package/migrations/migrateLegacyOpusToCurrent.ts +57 -0
- package/migrations/migrateOpusToOpus1m.ts +43 -0
- package/migrations/migrateReplBridgeEnabledToRemoteControlAtStartup.ts +22 -0
- package/migrations/migrateSonnet1mToSonnet45.ts +48 -0
- package/migrations/migrateSonnet45ToSonnet46.ts +67 -0
- package/migrations/resetAutoModeOptInForDefaultOffer.ts +51 -0
- package/migrations/resetProToOpusDefault.ts +51 -0
- package/native-ts/color-diff/index.ts +999 -0
- package/native-ts/file-index/index.ts +370 -0
- package/native-ts/yoga-layout/enums.ts +134 -0
- package/native-ts/yoga-layout/index.ts +2578 -0
- package/outputStyles/loadOutputStylesDir.ts +98 -0
- package/package.json +22 -5
- package/plugins/builtinPlugins.ts +159 -0
- package/plugins/bundled/index.ts +23 -0
- package/schemas/hooks.ts +222 -0
- package/screens/Doctor.tsx +575 -0
- package/screens/REPL.tsx +5006 -0
- package/screens/ResumeConversation.tsx +399 -0
- package/server/createDirectConnectSession.ts +88 -0
- package/server/directConnectManager.ts +213 -0
- package/server/types.ts +57 -0
- package/skills/bundled/batch.ts +124 -0
- package/skills/bundled/claudeApi.ts +196 -0
- package/skills/bundled/claudeApiContent.ts +75 -0
- package/skills/bundled/claudeInChrome.ts +34 -0
- package/skills/bundled/debug.ts +103 -0
- package/skills/bundled/index.ts +79 -0
- package/skills/bundled/keybindings.ts +339 -0
- package/skills/bundled/loop.ts +92 -0
- package/skills/bundled/loremIpsum.ts +282 -0
- package/skills/bundled/remember.ts +82 -0
- package/skills/bundled/scheduleRemoteAgents.ts +447 -0
- package/skills/bundled/simplify.ts +69 -0
- package/skills/bundled/skillify.ts +197 -0
- package/skills/bundled/stuck.ts +79 -0
- package/skills/bundled/updateConfig.ts +475 -0
- package/skills/bundled/verify/SKILL.md +3 -0
- package/skills/bundled/verify/examples/cli.md +3 -0
- package/skills/bundled/verify/examples/server.md +3 -0
- package/skills/bundled/verify.ts +30 -0
- package/skills/bundled/verifyContent.ts +13 -0
- package/skills/bundledSkills.ts +220 -0
- package/skills/loadSkillsDir.ts +1086 -0
- package/skills/mcpSkillBuilders.ts +44 -0
- package/tasks/DreamTask/DreamTask.ts +157 -0
- package/tasks/InProcessTeammateTask/InProcessTeammateTask.tsx +126 -0
- package/tasks/InProcessTeammateTask/types.ts +121 -0
- package/tasks/LocalAgentTask/LocalAgentTask.tsx +683 -0
- package/tasks/LocalMainSessionTask.ts +479 -0
- package/tasks/LocalShellTask/LocalShellTask.tsx +523 -0
- package/tasks/LocalShellTask/guards.ts +41 -0
- package/tasks/LocalShellTask/killShellTasks.ts +76 -0
- package/tasks/RemoteAgentTask/RemoteAgentTask.tsx +856 -0
- package/tasks/pillLabel.ts +82 -0
- package/tasks/stopTask.ts +100 -0
- package/tasks/types.ts +46 -0
- package/upstreamproxy/relay.ts +455 -0
- package/upstreamproxy/upstreamproxy.ts +285 -0
- package/vim/motions.ts +82 -0
- package/vim/operators.ts +556 -0
- package/vim/textObjects.ts +186 -0
- package/vim/transitions.ts +490 -0
- package/vim/types.ts +199 -0
- package/voice/voiceModeEnabled.ts +54 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vim Text Object Finding
|
|
3
|
+
*
|
|
4
|
+
* Functions for finding text object boundaries (iw, aw, i", a(, etc.)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
isVimPunctuation,
|
|
9
|
+
isVimWhitespace,
|
|
10
|
+
isVimWordChar,
|
|
11
|
+
} from '../utils/Cursor.js'
|
|
12
|
+
import { getGraphemeSegmenter } from '../utils/intl.js'
|
|
13
|
+
|
|
14
|
+
export type TextObjectRange = { start: number; end: number } | null
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Delimiter pairs for text objects.
|
|
18
|
+
*/
|
|
19
|
+
const PAIRS: Record<string, [string, string]> = {
|
|
20
|
+
'(': ['(', ')'],
|
|
21
|
+
')': ['(', ')'],
|
|
22
|
+
b: ['(', ')'],
|
|
23
|
+
'[': ['[', ']'],
|
|
24
|
+
']': ['[', ']'],
|
|
25
|
+
'{': ['{', '}'],
|
|
26
|
+
'}': ['{', '}'],
|
|
27
|
+
B: ['{', '}'],
|
|
28
|
+
'<': ['<', '>'],
|
|
29
|
+
'>': ['<', '>'],
|
|
30
|
+
'"': ['"', '"'],
|
|
31
|
+
"'": ["'", "'"],
|
|
32
|
+
'`': ['`', '`'],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Find a text object at the given position.
|
|
37
|
+
*/
|
|
38
|
+
export function findTextObject(
|
|
39
|
+
text: string,
|
|
40
|
+
offset: number,
|
|
41
|
+
objectType: string,
|
|
42
|
+
isInner: boolean,
|
|
43
|
+
): TextObjectRange {
|
|
44
|
+
if (objectType === 'w')
|
|
45
|
+
return findWordObject(text, offset, isInner, isVimWordChar)
|
|
46
|
+
if (objectType === 'W')
|
|
47
|
+
return findWordObject(text, offset, isInner, ch => !isVimWhitespace(ch))
|
|
48
|
+
|
|
49
|
+
const pair = PAIRS[objectType]
|
|
50
|
+
if (pair) {
|
|
51
|
+
const [open, close] = pair
|
|
52
|
+
return open === close
|
|
53
|
+
? findQuoteObject(text, offset, open, isInner)
|
|
54
|
+
: findBracketObject(text, offset, open, close, isInner)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function findWordObject(
|
|
61
|
+
text: string,
|
|
62
|
+
offset: number,
|
|
63
|
+
isInner: boolean,
|
|
64
|
+
isWordChar: (ch: string) => boolean,
|
|
65
|
+
): TextObjectRange {
|
|
66
|
+
// Pre-segment into graphemes for grapheme-safe iteration
|
|
67
|
+
const graphemes: Array<{ segment: string; index: number }> = []
|
|
68
|
+
for (const { segment, index } of getGraphemeSegmenter().segment(text)) {
|
|
69
|
+
graphemes.push({ segment, index })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Find which grapheme index the offset falls in
|
|
73
|
+
let graphemeIdx = graphemes.length - 1
|
|
74
|
+
for (let i = 0; i < graphemes.length; i++) {
|
|
75
|
+
const g = graphemes[i]!
|
|
76
|
+
const nextStart =
|
|
77
|
+
i + 1 < graphemes.length ? graphemes[i + 1]!.index : text.length
|
|
78
|
+
if (offset >= g.index && offset < nextStart) {
|
|
79
|
+
graphemeIdx = i
|
|
80
|
+
break
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const graphemeAt = (idx: number): string => graphemes[idx]?.segment ?? ''
|
|
85
|
+
const offsetAt = (idx: number): number =>
|
|
86
|
+
idx < graphemes.length ? graphemes[idx]!.index : text.length
|
|
87
|
+
const isWs = (idx: number): boolean => isVimWhitespace(graphemeAt(idx))
|
|
88
|
+
const isWord = (idx: number): boolean => isWordChar(graphemeAt(idx))
|
|
89
|
+
const isPunct = (idx: number): boolean => isVimPunctuation(graphemeAt(idx))
|
|
90
|
+
|
|
91
|
+
let startIdx = graphemeIdx
|
|
92
|
+
let endIdx = graphemeIdx
|
|
93
|
+
|
|
94
|
+
if (isWord(graphemeIdx)) {
|
|
95
|
+
while (startIdx > 0 && isWord(startIdx - 1)) startIdx--
|
|
96
|
+
while (endIdx < graphemes.length && isWord(endIdx)) endIdx++
|
|
97
|
+
} else if (isWs(graphemeIdx)) {
|
|
98
|
+
while (startIdx > 0 && isWs(startIdx - 1)) startIdx--
|
|
99
|
+
while (endIdx < graphemes.length && isWs(endIdx)) endIdx++
|
|
100
|
+
return { start: offsetAt(startIdx), end: offsetAt(endIdx) }
|
|
101
|
+
} else if (isPunct(graphemeIdx)) {
|
|
102
|
+
while (startIdx > 0 && isPunct(startIdx - 1)) startIdx--
|
|
103
|
+
while (endIdx < graphemes.length && isPunct(endIdx)) endIdx++
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!isInner) {
|
|
107
|
+
// Include surrounding whitespace
|
|
108
|
+
if (endIdx < graphemes.length && isWs(endIdx)) {
|
|
109
|
+
while (endIdx < graphemes.length && isWs(endIdx)) endIdx++
|
|
110
|
+
} else if (startIdx > 0 && isWs(startIdx - 1)) {
|
|
111
|
+
while (startIdx > 0 && isWs(startIdx - 1)) startIdx--
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { start: offsetAt(startIdx), end: offsetAt(endIdx) }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function findQuoteObject(
|
|
119
|
+
text: string,
|
|
120
|
+
offset: number,
|
|
121
|
+
quote: string,
|
|
122
|
+
isInner: boolean,
|
|
123
|
+
): TextObjectRange {
|
|
124
|
+
const lineStart = text.lastIndexOf('\n', offset - 1) + 1
|
|
125
|
+
const lineEnd = text.indexOf('\n', offset)
|
|
126
|
+
const effectiveEnd = lineEnd === -1 ? text.length : lineEnd
|
|
127
|
+
const line = text.slice(lineStart, effectiveEnd)
|
|
128
|
+
const posInLine = offset - lineStart
|
|
129
|
+
|
|
130
|
+
const positions: number[] = []
|
|
131
|
+
for (let i = 0; i < line.length; i++) {
|
|
132
|
+
if (line[i] === quote) positions.push(i)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Pair quotes correctly: 0-1, 2-3, 4-5, etc.
|
|
136
|
+
for (let i = 0; i < positions.length - 1; i += 2) {
|
|
137
|
+
const qs = positions[i]!
|
|
138
|
+
const qe = positions[i + 1]!
|
|
139
|
+
if (qs <= posInLine && posInLine <= qe) {
|
|
140
|
+
return isInner
|
|
141
|
+
? { start: lineStart + qs + 1, end: lineStart + qe }
|
|
142
|
+
: { start: lineStart + qs, end: lineStart + qe + 1 }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function findBracketObject(
|
|
150
|
+
text: string,
|
|
151
|
+
offset: number,
|
|
152
|
+
open: string,
|
|
153
|
+
close: string,
|
|
154
|
+
isInner: boolean,
|
|
155
|
+
): TextObjectRange {
|
|
156
|
+
let depth = 0
|
|
157
|
+
let start = -1
|
|
158
|
+
|
|
159
|
+
for (let i = offset; i >= 0; i--) {
|
|
160
|
+
if (text[i] === close && i !== offset) depth++
|
|
161
|
+
else if (text[i] === open) {
|
|
162
|
+
if (depth === 0) {
|
|
163
|
+
start = i
|
|
164
|
+
break
|
|
165
|
+
}
|
|
166
|
+
depth--
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (start === -1) return null
|
|
170
|
+
|
|
171
|
+
depth = 0
|
|
172
|
+
let end = -1
|
|
173
|
+
for (let i = start + 1; i < text.length; i++) {
|
|
174
|
+
if (text[i] === open) depth++
|
|
175
|
+
else if (text[i] === close) {
|
|
176
|
+
if (depth === 0) {
|
|
177
|
+
end = i
|
|
178
|
+
break
|
|
179
|
+
}
|
|
180
|
+
depth--
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (end === -1) return null
|
|
184
|
+
|
|
185
|
+
return isInner ? { start: start + 1, end } : { start, end: end + 1 }
|
|
186
|
+
}
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vim State Transition Table
|
|
3
|
+
*
|
|
4
|
+
* This is the scannable source of truth for state transitions.
|
|
5
|
+
* To understand what happens in any state, look up that state's transition function.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { resolveMotion } from './motions.js'
|
|
9
|
+
import {
|
|
10
|
+
executeIndent,
|
|
11
|
+
executeJoin,
|
|
12
|
+
executeLineOp,
|
|
13
|
+
executeOpenLine,
|
|
14
|
+
executeOperatorFind,
|
|
15
|
+
executeOperatorG,
|
|
16
|
+
executeOperatorGg,
|
|
17
|
+
executeOperatorMotion,
|
|
18
|
+
executeOperatorTextObj,
|
|
19
|
+
executePaste,
|
|
20
|
+
executeReplace,
|
|
21
|
+
executeToggleCase,
|
|
22
|
+
executeX,
|
|
23
|
+
type OperatorContext,
|
|
24
|
+
} from './operators.js'
|
|
25
|
+
import {
|
|
26
|
+
type CommandState,
|
|
27
|
+
FIND_KEYS,
|
|
28
|
+
type FindType,
|
|
29
|
+
isOperatorKey,
|
|
30
|
+
isTextObjScopeKey,
|
|
31
|
+
MAX_VIM_COUNT,
|
|
32
|
+
OPERATORS,
|
|
33
|
+
type Operator,
|
|
34
|
+
SIMPLE_MOTIONS,
|
|
35
|
+
TEXT_OBJ_SCOPES,
|
|
36
|
+
TEXT_OBJ_TYPES,
|
|
37
|
+
type TextObjScope,
|
|
38
|
+
} from './types.js'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Context passed to transition functions.
|
|
42
|
+
*/
|
|
43
|
+
export type TransitionContext = OperatorContext & {
|
|
44
|
+
onUndo?: () => void
|
|
45
|
+
onDotRepeat?: () => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Result of a transition.
|
|
50
|
+
*/
|
|
51
|
+
export type TransitionResult = {
|
|
52
|
+
next?: CommandState
|
|
53
|
+
execute?: () => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Main transition function. Dispatches based on current state type.
|
|
58
|
+
*/
|
|
59
|
+
export function transition(
|
|
60
|
+
state: CommandState,
|
|
61
|
+
input: string,
|
|
62
|
+
ctx: TransitionContext,
|
|
63
|
+
): TransitionResult {
|
|
64
|
+
switch (state.type) {
|
|
65
|
+
case 'idle':
|
|
66
|
+
return fromIdle(input, ctx)
|
|
67
|
+
case 'count':
|
|
68
|
+
return fromCount(state, input, ctx)
|
|
69
|
+
case 'operator':
|
|
70
|
+
return fromOperator(state, input, ctx)
|
|
71
|
+
case 'operatorCount':
|
|
72
|
+
return fromOperatorCount(state, input, ctx)
|
|
73
|
+
case 'operatorFind':
|
|
74
|
+
return fromOperatorFind(state, input, ctx)
|
|
75
|
+
case 'operatorTextObj':
|
|
76
|
+
return fromOperatorTextObj(state, input, ctx)
|
|
77
|
+
case 'find':
|
|
78
|
+
return fromFind(state, input, ctx)
|
|
79
|
+
case 'g':
|
|
80
|
+
return fromG(state, input, ctx)
|
|
81
|
+
case 'operatorG':
|
|
82
|
+
return fromOperatorG(state, input, ctx)
|
|
83
|
+
case 'replace':
|
|
84
|
+
return fromReplace(state, input, ctx)
|
|
85
|
+
case 'indent':
|
|
86
|
+
return fromIndent(state, input, ctx)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Shared Input Handling
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Handle input that's valid in both idle and count states.
|
|
96
|
+
* Returns null if input is not recognized.
|
|
97
|
+
*/
|
|
98
|
+
function handleNormalInput(
|
|
99
|
+
input: string,
|
|
100
|
+
count: number,
|
|
101
|
+
ctx: TransitionContext,
|
|
102
|
+
): TransitionResult | null {
|
|
103
|
+
if (isOperatorKey(input)) {
|
|
104
|
+
return { next: { type: 'operator', op: OPERATORS[input], count } }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (SIMPLE_MOTIONS.has(input)) {
|
|
108
|
+
return {
|
|
109
|
+
execute: () => {
|
|
110
|
+
const target = resolveMotion(input, ctx.cursor, count)
|
|
111
|
+
ctx.setOffset(target.offset)
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (FIND_KEYS.has(input)) {
|
|
117
|
+
return { next: { type: 'find', find: input as FindType, count } }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (input === 'g') return { next: { type: 'g', count } }
|
|
121
|
+
if (input === 'r') return { next: { type: 'replace', count } }
|
|
122
|
+
if (input === '>' || input === '<') {
|
|
123
|
+
return { next: { type: 'indent', dir: input, count } }
|
|
124
|
+
}
|
|
125
|
+
if (input === '~') {
|
|
126
|
+
return { execute: () => executeToggleCase(count, ctx) }
|
|
127
|
+
}
|
|
128
|
+
if (input === 'x') {
|
|
129
|
+
return { execute: () => executeX(count, ctx) }
|
|
130
|
+
}
|
|
131
|
+
if (input === 'J') {
|
|
132
|
+
return { execute: () => executeJoin(count, ctx) }
|
|
133
|
+
}
|
|
134
|
+
if (input === 'p' || input === 'P') {
|
|
135
|
+
return { execute: () => executePaste(input === 'p', count, ctx) }
|
|
136
|
+
}
|
|
137
|
+
if (input === 'D') {
|
|
138
|
+
return { execute: () => executeOperatorMotion('delete', '$', 1, ctx) }
|
|
139
|
+
}
|
|
140
|
+
if (input === 'C') {
|
|
141
|
+
return { execute: () => executeOperatorMotion('change', '$', 1, ctx) }
|
|
142
|
+
}
|
|
143
|
+
if (input === 'Y') {
|
|
144
|
+
return { execute: () => executeLineOp('yank', count, ctx) }
|
|
145
|
+
}
|
|
146
|
+
if (input === 'G') {
|
|
147
|
+
return {
|
|
148
|
+
execute: () => {
|
|
149
|
+
// count=1 means no count given, go to last line
|
|
150
|
+
// otherwise go to line N
|
|
151
|
+
if (count === 1) {
|
|
152
|
+
ctx.setOffset(ctx.cursor.startOfLastLine().offset)
|
|
153
|
+
} else {
|
|
154
|
+
ctx.setOffset(ctx.cursor.goToLine(count).offset)
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (input === '.') {
|
|
160
|
+
return { execute: () => ctx.onDotRepeat?.() }
|
|
161
|
+
}
|
|
162
|
+
if (input === ';' || input === ',') {
|
|
163
|
+
return { execute: () => executeRepeatFind(input === ',', count, ctx) }
|
|
164
|
+
}
|
|
165
|
+
if (input === 'u') {
|
|
166
|
+
return { execute: () => ctx.onUndo?.() }
|
|
167
|
+
}
|
|
168
|
+
if (input === 'i') {
|
|
169
|
+
return { execute: () => ctx.enterInsert(ctx.cursor.offset) }
|
|
170
|
+
}
|
|
171
|
+
if (input === 'I') {
|
|
172
|
+
return {
|
|
173
|
+
execute: () =>
|
|
174
|
+
ctx.enterInsert(ctx.cursor.firstNonBlankInLogicalLine().offset),
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (input === 'a') {
|
|
178
|
+
return {
|
|
179
|
+
execute: () => {
|
|
180
|
+
const newOffset = ctx.cursor.isAtEnd()
|
|
181
|
+
? ctx.cursor.offset
|
|
182
|
+
: ctx.cursor.right().offset
|
|
183
|
+
ctx.enterInsert(newOffset)
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (input === 'A') {
|
|
188
|
+
return {
|
|
189
|
+
execute: () => ctx.enterInsert(ctx.cursor.endOfLogicalLine().offset),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (input === 'o') {
|
|
193
|
+
return { execute: () => executeOpenLine('below', ctx) }
|
|
194
|
+
}
|
|
195
|
+
if (input === 'O') {
|
|
196
|
+
return { execute: () => executeOpenLine('above', ctx) }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return null
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Handle operator input (motion, find, text object scope).
|
|
204
|
+
* Returns null if input is not recognized.
|
|
205
|
+
*/
|
|
206
|
+
function handleOperatorInput(
|
|
207
|
+
op: Operator,
|
|
208
|
+
count: number,
|
|
209
|
+
input: string,
|
|
210
|
+
ctx: TransitionContext,
|
|
211
|
+
): TransitionResult | null {
|
|
212
|
+
if (isTextObjScopeKey(input)) {
|
|
213
|
+
return {
|
|
214
|
+
next: {
|
|
215
|
+
type: 'operatorTextObj',
|
|
216
|
+
op,
|
|
217
|
+
count,
|
|
218
|
+
scope: TEXT_OBJ_SCOPES[input],
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (FIND_KEYS.has(input)) {
|
|
224
|
+
return {
|
|
225
|
+
next: { type: 'operatorFind', op, count, find: input as FindType },
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (SIMPLE_MOTIONS.has(input)) {
|
|
230
|
+
return { execute: () => executeOperatorMotion(op, input, count, ctx) }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (input === 'G') {
|
|
234
|
+
return { execute: () => executeOperatorG(op, count, ctx) }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (input === 'g') {
|
|
238
|
+
return { next: { type: 'operatorG', op, count } }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ============================================================================
|
|
245
|
+
// Transition Functions - One per state type
|
|
246
|
+
// ============================================================================
|
|
247
|
+
|
|
248
|
+
function fromIdle(input: string, ctx: TransitionContext): TransitionResult {
|
|
249
|
+
// 0 is line-start motion, not a count prefix
|
|
250
|
+
if (/[1-9]/.test(input)) {
|
|
251
|
+
return { next: { type: 'count', digits: input } }
|
|
252
|
+
}
|
|
253
|
+
if (input === '0') {
|
|
254
|
+
return {
|
|
255
|
+
execute: () => ctx.setOffset(ctx.cursor.startOfLogicalLine().offset),
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const result = handleNormalInput(input, 1, ctx)
|
|
260
|
+
if (result) return result
|
|
261
|
+
|
|
262
|
+
return {}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function fromCount(
|
|
266
|
+
state: { type: 'count'; digits: string },
|
|
267
|
+
input: string,
|
|
268
|
+
ctx: TransitionContext,
|
|
269
|
+
): TransitionResult {
|
|
270
|
+
if (/[0-9]/.test(input)) {
|
|
271
|
+
const newDigits = state.digits + input
|
|
272
|
+
const count = Math.min(parseInt(newDigits, 10), MAX_VIM_COUNT)
|
|
273
|
+
return { next: { type: 'count', digits: String(count) } }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const count = parseInt(state.digits, 10)
|
|
277
|
+
const result = handleNormalInput(input, count, ctx)
|
|
278
|
+
if (result) return result
|
|
279
|
+
|
|
280
|
+
return { next: { type: 'idle' } }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function fromOperator(
|
|
284
|
+
state: { type: 'operator'; op: Operator; count: number },
|
|
285
|
+
input: string,
|
|
286
|
+
ctx: TransitionContext,
|
|
287
|
+
): TransitionResult {
|
|
288
|
+
// dd, cc, yy = line operation
|
|
289
|
+
if (input === state.op[0]) {
|
|
290
|
+
return { execute: () => executeLineOp(state.op, state.count, ctx) }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (/[0-9]/.test(input)) {
|
|
294
|
+
return {
|
|
295
|
+
next: {
|
|
296
|
+
type: 'operatorCount',
|
|
297
|
+
op: state.op,
|
|
298
|
+
count: state.count,
|
|
299
|
+
digits: input,
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const result = handleOperatorInput(state.op, state.count, input, ctx)
|
|
305
|
+
if (result) return result
|
|
306
|
+
|
|
307
|
+
return { next: { type: 'idle' } }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function fromOperatorCount(
|
|
311
|
+
state: {
|
|
312
|
+
type: 'operatorCount'
|
|
313
|
+
op: Operator
|
|
314
|
+
count: number
|
|
315
|
+
digits: string
|
|
316
|
+
},
|
|
317
|
+
input: string,
|
|
318
|
+
ctx: TransitionContext,
|
|
319
|
+
): TransitionResult {
|
|
320
|
+
if (/[0-9]/.test(input)) {
|
|
321
|
+
const newDigits = state.digits + input
|
|
322
|
+
const parsedDigits = Math.min(parseInt(newDigits, 10), MAX_VIM_COUNT)
|
|
323
|
+
return { next: { ...state, digits: String(parsedDigits) } }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const motionCount = parseInt(state.digits, 10)
|
|
327
|
+
const effectiveCount = state.count * motionCount
|
|
328
|
+
const result = handleOperatorInput(state.op, effectiveCount, input, ctx)
|
|
329
|
+
if (result) return result
|
|
330
|
+
|
|
331
|
+
return { next: { type: 'idle' } }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function fromOperatorFind(
|
|
335
|
+
state: {
|
|
336
|
+
type: 'operatorFind'
|
|
337
|
+
op: Operator
|
|
338
|
+
count: number
|
|
339
|
+
find: FindType
|
|
340
|
+
},
|
|
341
|
+
input: string,
|
|
342
|
+
ctx: TransitionContext,
|
|
343
|
+
): TransitionResult {
|
|
344
|
+
return {
|
|
345
|
+
execute: () =>
|
|
346
|
+
executeOperatorFind(state.op, state.find, input, state.count, ctx),
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function fromOperatorTextObj(
|
|
351
|
+
state: {
|
|
352
|
+
type: 'operatorTextObj'
|
|
353
|
+
op: Operator
|
|
354
|
+
count: number
|
|
355
|
+
scope: TextObjScope
|
|
356
|
+
},
|
|
357
|
+
input: string,
|
|
358
|
+
ctx: TransitionContext,
|
|
359
|
+
): TransitionResult {
|
|
360
|
+
if (TEXT_OBJ_TYPES.has(input)) {
|
|
361
|
+
return {
|
|
362
|
+
execute: () =>
|
|
363
|
+
executeOperatorTextObj(state.op, state.scope, input, state.count, ctx),
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return { next: { type: 'idle' } }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function fromFind(
|
|
370
|
+
state: { type: 'find'; find: FindType; count: number },
|
|
371
|
+
input: string,
|
|
372
|
+
ctx: TransitionContext,
|
|
373
|
+
): TransitionResult {
|
|
374
|
+
return {
|
|
375
|
+
execute: () => {
|
|
376
|
+
const result = ctx.cursor.findCharacter(input, state.find, state.count)
|
|
377
|
+
if (result !== null) {
|
|
378
|
+
ctx.setOffset(result)
|
|
379
|
+
ctx.setLastFind(state.find, input)
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function fromG(
|
|
386
|
+
state: { type: 'g'; count: number },
|
|
387
|
+
input: string,
|
|
388
|
+
ctx: TransitionContext,
|
|
389
|
+
): TransitionResult {
|
|
390
|
+
if (input === 'j' || input === 'k') {
|
|
391
|
+
return {
|
|
392
|
+
execute: () => {
|
|
393
|
+
const target = resolveMotion(`g${input}`, ctx.cursor, state.count)
|
|
394
|
+
ctx.setOffset(target.offset)
|
|
395
|
+
},
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (input === 'g') {
|
|
399
|
+
// If count provided (e.g., 5gg), go to that line. Otherwise go to first line.
|
|
400
|
+
if (state.count > 1) {
|
|
401
|
+
return {
|
|
402
|
+
execute: () => {
|
|
403
|
+
const lines = ctx.text.split('\n')
|
|
404
|
+
const targetLine = Math.min(state.count - 1, lines.length - 1)
|
|
405
|
+
let offset = 0
|
|
406
|
+
for (let i = 0; i < targetLine; i++) {
|
|
407
|
+
offset += (lines[i]?.length ?? 0) + 1 // +1 for newline
|
|
408
|
+
}
|
|
409
|
+
ctx.setOffset(offset)
|
|
410
|
+
},
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
execute: () => ctx.setOffset(ctx.cursor.startOfFirstLine().offset),
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return { next: { type: 'idle' } }
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function fromOperatorG(
|
|
421
|
+
state: { type: 'operatorG'; op: Operator; count: number },
|
|
422
|
+
input: string,
|
|
423
|
+
ctx: TransitionContext,
|
|
424
|
+
): TransitionResult {
|
|
425
|
+
if (input === 'j' || input === 'k') {
|
|
426
|
+
return {
|
|
427
|
+
execute: () =>
|
|
428
|
+
executeOperatorMotion(state.op, `g${input}`, state.count, ctx),
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (input === 'g') {
|
|
432
|
+
return { execute: () => executeOperatorGg(state.op, state.count, ctx) }
|
|
433
|
+
}
|
|
434
|
+
// Any other input cancels the operator
|
|
435
|
+
return { next: { type: 'idle' } }
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function fromReplace(
|
|
439
|
+
state: { type: 'replace'; count: number },
|
|
440
|
+
input: string,
|
|
441
|
+
ctx: TransitionContext,
|
|
442
|
+
): TransitionResult {
|
|
443
|
+
// Backspace/Delete arrive as empty input in literal-char states. In vim,
|
|
444
|
+
// r<BS> cancels the replace; without this guard, executeReplace("") would
|
|
445
|
+
// delete the character under the cursor instead.
|
|
446
|
+
if (input === '') return { next: { type: 'idle' } }
|
|
447
|
+
return { execute: () => executeReplace(input, state.count, ctx) }
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function fromIndent(
|
|
451
|
+
state: { type: 'indent'; dir: '>' | '<'; count: number },
|
|
452
|
+
input: string,
|
|
453
|
+
ctx: TransitionContext,
|
|
454
|
+
): TransitionResult {
|
|
455
|
+
if (input === state.dir) {
|
|
456
|
+
return { execute: () => executeIndent(state.dir, state.count, ctx) }
|
|
457
|
+
}
|
|
458
|
+
return { next: { type: 'idle' } }
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ============================================================================
|
|
462
|
+
// Helper functions for special commands
|
|
463
|
+
// ============================================================================
|
|
464
|
+
|
|
465
|
+
function executeRepeatFind(
|
|
466
|
+
reverse: boolean,
|
|
467
|
+
count: number,
|
|
468
|
+
ctx: TransitionContext,
|
|
469
|
+
): void {
|
|
470
|
+
const lastFind = ctx.getLastFind()
|
|
471
|
+
if (!lastFind) return
|
|
472
|
+
|
|
473
|
+
// Determine the effective find type based on reverse
|
|
474
|
+
let findType = lastFind.type
|
|
475
|
+
if (reverse) {
|
|
476
|
+
// Flip the direction
|
|
477
|
+
const flipMap: Record<FindType, FindType> = {
|
|
478
|
+
f: 'F',
|
|
479
|
+
F: 'f',
|
|
480
|
+
t: 'T',
|
|
481
|
+
T: 't',
|
|
482
|
+
}
|
|
483
|
+
findType = flipMap[findType]
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const result = ctx.cursor.findCharacter(lastFind.char, findType, count)
|
|
487
|
+
if (result !== null) {
|
|
488
|
+
ctx.setOffset(result)
|
|
489
|
+
}
|
|
490
|
+
}
|