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
package/vim/operators.ts
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vim Operator Functions
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for executing vim operators (delete, change, yank, etc.)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Cursor } from '../utils/Cursor.js'
|
|
8
|
+
import { firstGrapheme, lastGrapheme } from '../utils/intl.js'
|
|
9
|
+
import { countCharInString } from '../utils/stringUtils.js'
|
|
10
|
+
import {
|
|
11
|
+
isInclusiveMotion,
|
|
12
|
+
isLinewiseMotion,
|
|
13
|
+
resolveMotion,
|
|
14
|
+
} from './motions.js'
|
|
15
|
+
import { findTextObject } from './textObjects.js'
|
|
16
|
+
import type {
|
|
17
|
+
FindType,
|
|
18
|
+
Operator,
|
|
19
|
+
RecordedChange,
|
|
20
|
+
TextObjScope,
|
|
21
|
+
} from './types.js'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Context for operator execution.
|
|
25
|
+
*/
|
|
26
|
+
export type OperatorContext = {
|
|
27
|
+
cursor: Cursor
|
|
28
|
+
text: string
|
|
29
|
+
setText: (text: string) => void
|
|
30
|
+
setOffset: (offset: number) => void
|
|
31
|
+
enterInsert: (offset: number) => void
|
|
32
|
+
getRegister: () => string
|
|
33
|
+
setRegister: (content: string, linewise: boolean) => void
|
|
34
|
+
getLastFind: () => { type: FindType; char: string } | null
|
|
35
|
+
setLastFind: (type: FindType, char: string) => void
|
|
36
|
+
recordChange: (change: RecordedChange) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Execute an operator with a simple motion.
|
|
41
|
+
*/
|
|
42
|
+
export function executeOperatorMotion(
|
|
43
|
+
op: Operator,
|
|
44
|
+
motion: string,
|
|
45
|
+
count: number,
|
|
46
|
+
ctx: OperatorContext,
|
|
47
|
+
): void {
|
|
48
|
+
const target = resolveMotion(motion, ctx.cursor, count)
|
|
49
|
+
if (target.equals(ctx.cursor)) return
|
|
50
|
+
|
|
51
|
+
const range = getOperatorRange(ctx.cursor, target, motion, op, count)
|
|
52
|
+
applyOperator(op, range.from, range.to, ctx, range.linewise)
|
|
53
|
+
ctx.recordChange({ type: 'operator', op, motion, count })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Execute an operator with a find motion.
|
|
58
|
+
*/
|
|
59
|
+
export function executeOperatorFind(
|
|
60
|
+
op: Operator,
|
|
61
|
+
findType: FindType,
|
|
62
|
+
char: string,
|
|
63
|
+
count: number,
|
|
64
|
+
ctx: OperatorContext,
|
|
65
|
+
): void {
|
|
66
|
+
const targetOffset = ctx.cursor.findCharacter(char, findType, count)
|
|
67
|
+
if (targetOffset === null) return
|
|
68
|
+
|
|
69
|
+
const target = new Cursor(ctx.cursor.measuredText, targetOffset)
|
|
70
|
+
const range = getOperatorRangeForFind(ctx.cursor, target, findType)
|
|
71
|
+
|
|
72
|
+
applyOperator(op, range.from, range.to, ctx)
|
|
73
|
+
ctx.setLastFind(findType, char)
|
|
74
|
+
ctx.recordChange({ type: 'operatorFind', op, find: findType, char, count })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Execute an operator with a text object.
|
|
79
|
+
*/
|
|
80
|
+
export function executeOperatorTextObj(
|
|
81
|
+
op: Operator,
|
|
82
|
+
scope: TextObjScope,
|
|
83
|
+
objType: string,
|
|
84
|
+
count: number,
|
|
85
|
+
ctx: OperatorContext,
|
|
86
|
+
): void {
|
|
87
|
+
const range = findTextObject(
|
|
88
|
+
ctx.text,
|
|
89
|
+
ctx.cursor.offset,
|
|
90
|
+
objType,
|
|
91
|
+
scope === 'inner',
|
|
92
|
+
)
|
|
93
|
+
if (!range) return
|
|
94
|
+
|
|
95
|
+
applyOperator(op, range.start, range.end, ctx)
|
|
96
|
+
ctx.recordChange({ type: 'operatorTextObj', op, objType, scope, count })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Execute a line operation (dd, cc, yy).
|
|
101
|
+
*/
|
|
102
|
+
export function executeLineOp(
|
|
103
|
+
op: Operator,
|
|
104
|
+
count: number,
|
|
105
|
+
ctx: OperatorContext,
|
|
106
|
+
): void {
|
|
107
|
+
const text = ctx.text
|
|
108
|
+
const lines = text.split('\n')
|
|
109
|
+
// Calculate logical line by counting newlines before cursor offset
|
|
110
|
+
// (cursor.getPosition() returns wrapped line which is wrong for this)
|
|
111
|
+
const currentLine = countCharInString(text.slice(0, ctx.cursor.offset), '\n')
|
|
112
|
+
const linesToAffect = Math.min(count, lines.length - currentLine)
|
|
113
|
+
const lineStart = ctx.cursor.startOfLogicalLine().offset
|
|
114
|
+
let lineEnd = lineStart
|
|
115
|
+
for (let i = 0; i < linesToAffect; i++) {
|
|
116
|
+
const nextNewline = text.indexOf('\n', lineEnd)
|
|
117
|
+
lineEnd = nextNewline === -1 ? text.length : nextNewline + 1
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let content = text.slice(lineStart, lineEnd)
|
|
121
|
+
// Ensure linewise content ends with newline for paste detection
|
|
122
|
+
if (!content.endsWith('\n')) {
|
|
123
|
+
content = content + '\n'
|
|
124
|
+
}
|
|
125
|
+
ctx.setRegister(content, true)
|
|
126
|
+
|
|
127
|
+
if (op === 'yank') {
|
|
128
|
+
ctx.setOffset(lineStart)
|
|
129
|
+
} else if (op === 'delete') {
|
|
130
|
+
let deleteStart = lineStart
|
|
131
|
+
const deleteEnd = lineEnd
|
|
132
|
+
|
|
133
|
+
// If deleting to end of file and there's a preceding newline, include it
|
|
134
|
+
// This ensures deleting the last line doesn't leave a trailing newline
|
|
135
|
+
if (
|
|
136
|
+
deleteEnd === text.length &&
|
|
137
|
+
deleteStart > 0 &&
|
|
138
|
+
text[deleteStart - 1] === '\n'
|
|
139
|
+
) {
|
|
140
|
+
deleteStart -= 1
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const newText = text.slice(0, deleteStart) + text.slice(deleteEnd)
|
|
144
|
+
ctx.setText(newText || '')
|
|
145
|
+
const maxOff = Math.max(
|
|
146
|
+
0,
|
|
147
|
+
newText.length - (lastGrapheme(newText).length || 1),
|
|
148
|
+
)
|
|
149
|
+
ctx.setOffset(Math.min(deleteStart, maxOff))
|
|
150
|
+
} else if (op === 'change') {
|
|
151
|
+
// For single line, just clear it
|
|
152
|
+
if (lines.length === 1) {
|
|
153
|
+
ctx.setText('')
|
|
154
|
+
ctx.enterInsert(0)
|
|
155
|
+
} else {
|
|
156
|
+
// Delete all affected lines, replace with single empty line, enter insert
|
|
157
|
+
const beforeLines = lines.slice(0, currentLine)
|
|
158
|
+
const afterLines = lines.slice(currentLine + linesToAffect)
|
|
159
|
+
const newText = [...beforeLines, '', ...afterLines].join('\n')
|
|
160
|
+
ctx.setText(newText)
|
|
161
|
+
ctx.enterInsert(lineStart)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
ctx.recordChange({ type: 'operator', op, motion: op[0]!, count })
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Execute delete character (x command).
|
|
170
|
+
*/
|
|
171
|
+
export function executeX(count: number, ctx: OperatorContext): void {
|
|
172
|
+
const from = ctx.cursor.offset
|
|
173
|
+
|
|
174
|
+
if (from >= ctx.text.length) return
|
|
175
|
+
|
|
176
|
+
// Advance by graphemes, not code units
|
|
177
|
+
let endCursor = ctx.cursor
|
|
178
|
+
for (let i = 0; i < count && !endCursor.isAtEnd(); i++) {
|
|
179
|
+
endCursor = endCursor.right()
|
|
180
|
+
}
|
|
181
|
+
const to = endCursor.offset
|
|
182
|
+
|
|
183
|
+
const deleted = ctx.text.slice(from, to)
|
|
184
|
+
const newText = ctx.text.slice(0, from) + ctx.text.slice(to)
|
|
185
|
+
|
|
186
|
+
ctx.setRegister(deleted, false)
|
|
187
|
+
ctx.setText(newText)
|
|
188
|
+
const maxOff = Math.max(
|
|
189
|
+
0,
|
|
190
|
+
newText.length - (lastGrapheme(newText).length || 1),
|
|
191
|
+
)
|
|
192
|
+
ctx.setOffset(Math.min(from, maxOff))
|
|
193
|
+
ctx.recordChange({ type: 'x', count })
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Execute replace character (r command).
|
|
198
|
+
*/
|
|
199
|
+
export function executeReplace(
|
|
200
|
+
char: string,
|
|
201
|
+
count: number,
|
|
202
|
+
ctx: OperatorContext,
|
|
203
|
+
): void {
|
|
204
|
+
let offset = ctx.cursor.offset
|
|
205
|
+
let newText = ctx.text
|
|
206
|
+
|
|
207
|
+
for (let i = 0; i < count && offset < newText.length; i++) {
|
|
208
|
+
const graphemeLen = firstGrapheme(newText.slice(offset)).length || 1
|
|
209
|
+
newText =
|
|
210
|
+
newText.slice(0, offset) + char + newText.slice(offset + graphemeLen)
|
|
211
|
+
offset += char.length
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
ctx.setText(newText)
|
|
215
|
+
ctx.setOffset(Math.max(0, offset - char.length))
|
|
216
|
+
ctx.recordChange({ type: 'replace', char, count })
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Execute toggle case (~ command).
|
|
221
|
+
*/
|
|
222
|
+
export function executeToggleCase(count: number, ctx: OperatorContext): void {
|
|
223
|
+
const startOffset = ctx.cursor.offset
|
|
224
|
+
|
|
225
|
+
if (startOffset >= ctx.text.length) return
|
|
226
|
+
|
|
227
|
+
let newText = ctx.text
|
|
228
|
+
let offset = startOffset
|
|
229
|
+
let toggled = 0
|
|
230
|
+
|
|
231
|
+
while (offset < newText.length && toggled < count) {
|
|
232
|
+
const grapheme = firstGrapheme(newText.slice(offset))
|
|
233
|
+
const graphemeLen = grapheme.length
|
|
234
|
+
|
|
235
|
+
const toggledGrapheme =
|
|
236
|
+
grapheme === grapheme.toUpperCase()
|
|
237
|
+
? grapheme.toLowerCase()
|
|
238
|
+
: grapheme.toUpperCase()
|
|
239
|
+
|
|
240
|
+
newText =
|
|
241
|
+
newText.slice(0, offset) +
|
|
242
|
+
toggledGrapheme +
|
|
243
|
+
newText.slice(offset + graphemeLen)
|
|
244
|
+
offset += toggledGrapheme.length
|
|
245
|
+
toggled++
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
ctx.setText(newText)
|
|
249
|
+
// Cursor moves to position after the last toggled character
|
|
250
|
+
// At end of line, cursor can be at the "end" position
|
|
251
|
+
ctx.setOffset(offset)
|
|
252
|
+
ctx.recordChange({ type: 'toggleCase', count })
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Execute join lines (J command).
|
|
257
|
+
*/
|
|
258
|
+
export function executeJoin(count: number, ctx: OperatorContext): void {
|
|
259
|
+
const text = ctx.text
|
|
260
|
+
const lines = text.split('\n')
|
|
261
|
+
const { line: currentLine } = ctx.cursor.getPosition()
|
|
262
|
+
|
|
263
|
+
if (currentLine >= lines.length - 1) return
|
|
264
|
+
|
|
265
|
+
const linesToJoin = Math.min(count, lines.length - currentLine - 1)
|
|
266
|
+
let joinedLine = lines[currentLine]!
|
|
267
|
+
const cursorPos = joinedLine.length
|
|
268
|
+
|
|
269
|
+
for (let i = 1; i <= linesToJoin; i++) {
|
|
270
|
+
const nextLine = (lines[currentLine + i] ?? '').trimStart()
|
|
271
|
+
if (nextLine.length > 0) {
|
|
272
|
+
if (!joinedLine.endsWith(' ') && joinedLine.length > 0) {
|
|
273
|
+
joinedLine += ' '
|
|
274
|
+
}
|
|
275
|
+
joinedLine += nextLine
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const newLines = [
|
|
280
|
+
...lines.slice(0, currentLine),
|
|
281
|
+
joinedLine,
|
|
282
|
+
...lines.slice(currentLine + linesToJoin + 1),
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
const newText = newLines.join('\n')
|
|
286
|
+
ctx.setText(newText)
|
|
287
|
+
ctx.setOffset(getLineStartOffset(newLines, currentLine) + cursorPos)
|
|
288
|
+
ctx.recordChange({ type: 'join', count })
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Execute paste (p/P command).
|
|
293
|
+
*/
|
|
294
|
+
export function executePaste(
|
|
295
|
+
after: boolean,
|
|
296
|
+
count: number,
|
|
297
|
+
ctx: OperatorContext,
|
|
298
|
+
): void {
|
|
299
|
+
const register = ctx.getRegister()
|
|
300
|
+
if (!register) return
|
|
301
|
+
|
|
302
|
+
const isLinewise = register.endsWith('\n')
|
|
303
|
+
const content = isLinewise ? register.slice(0, -1) : register
|
|
304
|
+
|
|
305
|
+
if (isLinewise) {
|
|
306
|
+
const text = ctx.text
|
|
307
|
+
const lines = text.split('\n')
|
|
308
|
+
const { line: currentLine } = ctx.cursor.getPosition()
|
|
309
|
+
|
|
310
|
+
const insertLine = after ? currentLine + 1 : currentLine
|
|
311
|
+
const contentLines = content.split('\n')
|
|
312
|
+
const repeatedLines: string[] = []
|
|
313
|
+
for (let i = 0; i < count; i++) {
|
|
314
|
+
repeatedLines.push(...contentLines)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const newLines = [
|
|
318
|
+
...lines.slice(0, insertLine),
|
|
319
|
+
...repeatedLines,
|
|
320
|
+
...lines.slice(insertLine),
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
const newText = newLines.join('\n')
|
|
324
|
+
ctx.setText(newText)
|
|
325
|
+
ctx.setOffset(getLineStartOffset(newLines, insertLine))
|
|
326
|
+
} else {
|
|
327
|
+
const textToInsert = content.repeat(count)
|
|
328
|
+
const insertPoint =
|
|
329
|
+
after && ctx.cursor.offset < ctx.text.length
|
|
330
|
+
? ctx.cursor.measuredText.nextOffset(ctx.cursor.offset)
|
|
331
|
+
: ctx.cursor.offset
|
|
332
|
+
|
|
333
|
+
const newText =
|
|
334
|
+
ctx.text.slice(0, insertPoint) +
|
|
335
|
+
textToInsert +
|
|
336
|
+
ctx.text.slice(insertPoint)
|
|
337
|
+
const lastGr = lastGrapheme(textToInsert)
|
|
338
|
+
const newOffset = insertPoint + textToInsert.length - (lastGr.length || 1)
|
|
339
|
+
|
|
340
|
+
ctx.setText(newText)
|
|
341
|
+
ctx.setOffset(Math.max(insertPoint, newOffset))
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Execute indent (>> command).
|
|
347
|
+
*/
|
|
348
|
+
export function executeIndent(
|
|
349
|
+
dir: '>' | '<',
|
|
350
|
+
count: number,
|
|
351
|
+
ctx: OperatorContext,
|
|
352
|
+
): void {
|
|
353
|
+
const text = ctx.text
|
|
354
|
+
const lines = text.split('\n')
|
|
355
|
+
const { line: currentLine } = ctx.cursor.getPosition()
|
|
356
|
+
const linesToAffect = Math.min(count, lines.length - currentLine)
|
|
357
|
+
const indent = ' ' // Two spaces
|
|
358
|
+
|
|
359
|
+
for (let i = 0; i < linesToAffect; i++) {
|
|
360
|
+
const lineIdx = currentLine + i
|
|
361
|
+
const line = lines[lineIdx] ?? ''
|
|
362
|
+
|
|
363
|
+
if (dir === '>') {
|
|
364
|
+
lines[lineIdx] = indent + line
|
|
365
|
+
} else if (line.startsWith(indent)) {
|
|
366
|
+
lines[lineIdx] = line.slice(indent.length)
|
|
367
|
+
} else if (line.startsWith('\t')) {
|
|
368
|
+
lines[lineIdx] = line.slice(1)
|
|
369
|
+
} else {
|
|
370
|
+
// Remove as much leading whitespace as possible up to indent length
|
|
371
|
+
let removed = 0
|
|
372
|
+
let idx = 0
|
|
373
|
+
while (
|
|
374
|
+
idx < line.length &&
|
|
375
|
+
removed < indent.length &&
|
|
376
|
+
/\s/.test(line[idx]!)
|
|
377
|
+
) {
|
|
378
|
+
removed++
|
|
379
|
+
idx++
|
|
380
|
+
}
|
|
381
|
+
lines[lineIdx] = line.slice(idx)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const newText = lines.join('\n')
|
|
386
|
+
const currentLineText = lines[currentLine] ?? ''
|
|
387
|
+
const firstNonBlank = (currentLineText.match(/^\s*/)?.[0] ?? '').length
|
|
388
|
+
|
|
389
|
+
ctx.setText(newText)
|
|
390
|
+
ctx.setOffset(getLineStartOffset(lines, currentLine) + firstNonBlank)
|
|
391
|
+
ctx.recordChange({ type: 'indent', dir, count })
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Execute open line (o/O command).
|
|
396
|
+
*/
|
|
397
|
+
export function executeOpenLine(
|
|
398
|
+
direction: 'above' | 'below',
|
|
399
|
+
ctx: OperatorContext,
|
|
400
|
+
): void {
|
|
401
|
+
const text = ctx.text
|
|
402
|
+
const lines = text.split('\n')
|
|
403
|
+
const { line: currentLine } = ctx.cursor.getPosition()
|
|
404
|
+
|
|
405
|
+
const insertLine = direction === 'below' ? currentLine + 1 : currentLine
|
|
406
|
+
const newLines = [
|
|
407
|
+
...lines.slice(0, insertLine),
|
|
408
|
+
'',
|
|
409
|
+
...lines.slice(insertLine),
|
|
410
|
+
]
|
|
411
|
+
|
|
412
|
+
const newText = newLines.join('\n')
|
|
413
|
+
ctx.setText(newText)
|
|
414
|
+
ctx.enterInsert(getLineStartOffset(newLines, insertLine))
|
|
415
|
+
ctx.recordChange({ type: 'openLine', direction })
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ============================================================================
|
|
419
|
+
// Internal Helpers
|
|
420
|
+
// ============================================================================
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Calculate the offset of a line's start position.
|
|
424
|
+
*/
|
|
425
|
+
function getLineStartOffset(lines: string[], lineIndex: number): number {
|
|
426
|
+
return lines.slice(0, lineIndex).join('\n').length + (lineIndex > 0 ? 1 : 0)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function getOperatorRange(
|
|
430
|
+
cursor: Cursor,
|
|
431
|
+
target: Cursor,
|
|
432
|
+
motion: string,
|
|
433
|
+
op: Operator,
|
|
434
|
+
count: number,
|
|
435
|
+
): { from: number; to: number; linewise: boolean } {
|
|
436
|
+
let from = Math.min(cursor.offset, target.offset)
|
|
437
|
+
let to = Math.max(cursor.offset, target.offset)
|
|
438
|
+
let linewise = false
|
|
439
|
+
|
|
440
|
+
// Special case: cw/cW changes to end of word, not start of next word
|
|
441
|
+
if (op === 'change' && (motion === 'w' || motion === 'W')) {
|
|
442
|
+
// For cw with count, move forward (count-1) words, then find end of that word
|
|
443
|
+
let wordCursor = cursor
|
|
444
|
+
for (let i = 0; i < count - 1; i++) {
|
|
445
|
+
wordCursor =
|
|
446
|
+
motion === 'w' ? wordCursor.nextVimWord() : wordCursor.nextWORD()
|
|
447
|
+
}
|
|
448
|
+
const wordEnd =
|
|
449
|
+
motion === 'w' ? wordCursor.endOfVimWord() : wordCursor.endOfWORD()
|
|
450
|
+
to = cursor.measuredText.nextOffset(wordEnd.offset)
|
|
451
|
+
} else if (isLinewiseMotion(motion)) {
|
|
452
|
+
// Linewise motions extend to include entire lines
|
|
453
|
+
linewise = true
|
|
454
|
+
const text = cursor.text
|
|
455
|
+
const nextNewline = text.indexOf('\n', to)
|
|
456
|
+
if (nextNewline === -1) {
|
|
457
|
+
// Deleting to end of file - include the preceding newline if exists
|
|
458
|
+
to = text.length
|
|
459
|
+
if (from > 0 && text[from - 1] === '\n') {
|
|
460
|
+
from -= 1
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
to = nextNewline + 1
|
|
464
|
+
}
|
|
465
|
+
} else if (isInclusiveMotion(motion) && cursor.offset <= target.offset) {
|
|
466
|
+
to = cursor.measuredText.nextOffset(to)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Word motions can land inside an [Image #N] chip; extend the range to
|
|
470
|
+
// cover the whole chip so dw/cw/yw never leave a partial placeholder.
|
|
471
|
+
from = cursor.snapOutOfImageRef(from, 'start')
|
|
472
|
+
to = cursor.snapOutOfImageRef(to, 'end')
|
|
473
|
+
|
|
474
|
+
return { from, to, linewise }
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Get the range for a find-based operator.
|
|
479
|
+
* Note: _findType is unused because Cursor.findCharacter already adjusts
|
|
480
|
+
* the offset for t/T motions. All find types are treated as inclusive here.
|
|
481
|
+
*/
|
|
482
|
+
function getOperatorRangeForFind(
|
|
483
|
+
cursor: Cursor,
|
|
484
|
+
target: Cursor,
|
|
485
|
+
_findType: FindType,
|
|
486
|
+
): { from: number; to: number } {
|
|
487
|
+
const from = Math.min(cursor.offset, target.offset)
|
|
488
|
+
const maxOffset = Math.max(cursor.offset, target.offset)
|
|
489
|
+
const to = cursor.measuredText.nextOffset(maxOffset)
|
|
490
|
+
return { from, to }
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function applyOperator(
|
|
494
|
+
op: Operator,
|
|
495
|
+
from: number,
|
|
496
|
+
to: number,
|
|
497
|
+
ctx: OperatorContext,
|
|
498
|
+
linewise: boolean = false,
|
|
499
|
+
): void {
|
|
500
|
+
let content = ctx.text.slice(from, to)
|
|
501
|
+
// Ensure linewise content ends with newline for paste detection
|
|
502
|
+
if (linewise && !content.endsWith('\n')) {
|
|
503
|
+
content = content + '\n'
|
|
504
|
+
}
|
|
505
|
+
ctx.setRegister(content, linewise)
|
|
506
|
+
|
|
507
|
+
if (op === 'yank') {
|
|
508
|
+
ctx.setOffset(from)
|
|
509
|
+
} else if (op === 'delete') {
|
|
510
|
+
const newText = ctx.text.slice(0, from) + ctx.text.slice(to)
|
|
511
|
+
ctx.setText(newText)
|
|
512
|
+
const maxOff = Math.max(
|
|
513
|
+
0,
|
|
514
|
+
newText.length - (lastGrapheme(newText).length || 1),
|
|
515
|
+
)
|
|
516
|
+
ctx.setOffset(Math.min(from, maxOff))
|
|
517
|
+
} else if (op === 'change') {
|
|
518
|
+
const newText = ctx.text.slice(0, from) + ctx.text.slice(to)
|
|
519
|
+
ctx.setText(newText)
|
|
520
|
+
ctx.enterInsert(from)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export function executeOperatorG(
|
|
525
|
+
op: Operator,
|
|
526
|
+
count: number,
|
|
527
|
+
ctx: OperatorContext,
|
|
528
|
+
): void {
|
|
529
|
+
// count=1 means no count given, target = end of file
|
|
530
|
+
// otherwise target = line N
|
|
531
|
+
const target =
|
|
532
|
+
count === 1 ? ctx.cursor.startOfLastLine() : ctx.cursor.goToLine(count)
|
|
533
|
+
|
|
534
|
+
if (target.equals(ctx.cursor)) return
|
|
535
|
+
|
|
536
|
+
const range = getOperatorRange(ctx.cursor, target, 'G', op, count)
|
|
537
|
+
applyOperator(op, range.from, range.to, ctx, range.linewise)
|
|
538
|
+
ctx.recordChange({ type: 'operator', op, motion: 'G', count })
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export function executeOperatorGg(
|
|
542
|
+
op: Operator,
|
|
543
|
+
count: number,
|
|
544
|
+
ctx: OperatorContext,
|
|
545
|
+
): void {
|
|
546
|
+
// count=1 means no count given, target = first line
|
|
547
|
+
// otherwise target = line N
|
|
548
|
+
const target =
|
|
549
|
+
count === 1 ? ctx.cursor.startOfFirstLine() : ctx.cursor.goToLine(count)
|
|
550
|
+
|
|
551
|
+
if (target.equals(ctx.cursor)) return
|
|
552
|
+
|
|
553
|
+
const range = getOperatorRange(ctx.cursor, target, 'gg', op, count)
|
|
554
|
+
applyOperator(op, range.from, range.to, ctx, range.linewise)
|
|
555
|
+
ctx.recordChange({ type: 'operator', op, motion: 'gg', count })
|
|
556
|
+
}
|