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.
Files changed (117) hide show
  1. package/assistant/sessionHistory.ts +87 -0
  2. package/bootstrap/state.ts +1769 -0
  3. package/bridge/bridgeApi.ts +539 -0
  4. package/bridge/bridgeConfig.ts +48 -0
  5. package/bridge/bridgeDebug.ts +135 -0
  6. package/bridge/bridgeEnabled.ts +202 -0
  7. package/bridge/bridgeMain.ts +2999 -0
  8. package/bridge/bridgeMessaging.ts +461 -0
  9. package/bridge/bridgePermissionCallbacks.ts +43 -0
  10. package/bridge/bridgePointer.ts +210 -0
  11. package/bridge/bridgeStatusUtil.ts +163 -0
  12. package/bridge/bridgeUI.ts +530 -0
  13. package/bridge/capacityWake.ts +56 -0
  14. package/bridge/codeSessionApi.ts +168 -0
  15. package/bridge/createSession.ts +384 -0
  16. package/bridge/debugUtils.ts +141 -0
  17. package/bridge/envLessBridgeConfig.ts +165 -0
  18. package/bridge/flushGate.ts +71 -0
  19. package/bridge/inboundAttachments.ts +175 -0
  20. package/bridge/inboundMessages.ts +80 -0
  21. package/bridge/initReplBridge.ts +569 -0
  22. package/bridge/jwtUtils.ts +256 -0
  23. package/bridge/pollConfig.ts +110 -0
  24. package/bridge/pollConfigDefaults.ts +82 -0
  25. package/bridge/remoteBridgeCore.ts +1008 -0
  26. package/bridge/replBridge.ts +2406 -0
  27. package/bridge/replBridgeHandle.ts +36 -0
  28. package/bridge/replBridgeTransport.ts +370 -0
  29. package/bridge/sessionIdCompat.ts +57 -0
  30. package/bridge/sessionRunner.ts +550 -0
  31. package/bridge/trustedDevice.ts +210 -0
  32. package/bridge/types.ts +262 -0
  33. package/bridge/workSecret.ts +127 -0
  34. package/buddy/CompanionSprite.tsx +371 -0
  35. package/buddy/companion.ts +133 -0
  36. package/buddy/prompt.ts +36 -0
  37. package/buddy/sprites.ts +514 -0
  38. package/buddy/types.ts +148 -0
  39. package/buddy/useBuddyNotification.tsx +98 -0
  40. package/coordinator/coordinatorMode.ts +369 -0
  41. package/memdir/findRelevantMemories.ts +141 -0
  42. package/memdir/memdir.ts +507 -0
  43. package/memdir/memoryAge.ts +53 -0
  44. package/memdir/memoryScan.ts +94 -0
  45. package/memdir/memoryTypes.ts +271 -0
  46. package/memdir/paths.ts +278 -0
  47. package/memdir/teamMemPaths.ts +292 -0
  48. package/memdir/teamMemPrompts.ts +100 -0
  49. package/migrations/migrateAutoUpdatesToSettings.ts +61 -0
  50. package/migrations/migrateBypassPermissionsAcceptedToSettings.ts +40 -0
  51. package/migrations/migrateEnableAllProjectMcpServersToSettings.ts +118 -0
  52. package/migrations/migrateFennecToOpus.ts +45 -0
  53. package/migrations/migrateLegacyOpusToCurrent.ts +57 -0
  54. package/migrations/migrateOpusToOpus1m.ts +43 -0
  55. package/migrations/migrateReplBridgeEnabledToRemoteControlAtStartup.ts +22 -0
  56. package/migrations/migrateSonnet1mToSonnet45.ts +48 -0
  57. package/migrations/migrateSonnet45ToSonnet46.ts +67 -0
  58. package/migrations/resetAutoModeOptInForDefaultOffer.ts +51 -0
  59. package/migrations/resetProToOpusDefault.ts +51 -0
  60. package/native-ts/color-diff/index.ts +999 -0
  61. package/native-ts/file-index/index.ts +370 -0
  62. package/native-ts/yoga-layout/enums.ts +134 -0
  63. package/native-ts/yoga-layout/index.ts +2578 -0
  64. package/outputStyles/loadOutputStylesDir.ts +98 -0
  65. package/package.json +22 -5
  66. package/plugins/builtinPlugins.ts +159 -0
  67. package/plugins/bundled/index.ts +23 -0
  68. package/schemas/hooks.ts +222 -0
  69. package/screens/Doctor.tsx +575 -0
  70. package/screens/REPL.tsx +5006 -0
  71. package/screens/ResumeConversation.tsx +399 -0
  72. package/server/createDirectConnectSession.ts +88 -0
  73. package/server/directConnectManager.ts +213 -0
  74. package/server/types.ts +57 -0
  75. package/skills/bundled/batch.ts +124 -0
  76. package/skills/bundled/claudeApi.ts +196 -0
  77. package/skills/bundled/claudeApiContent.ts +75 -0
  78. package/skills/bundled/claudeInChrome.ts +34 -0
  79. package/skills/bundled/debug.ts +103 -0
  80. package/skills/bundled/index.ts +79 -0
  81. package/skills/bundled/keybindings.ts +339 -0
  82. package/skills/bundled/loop.ts +92 -0
  83. package/skills/bundled/loremIpsum.ts +282 -0
  84. package/skills/bundled/remember.ts +82 -0
  85. package/skills/bundled/scheduleRemoteAgents.ts +447 -0
  86. package/skills/bundled/simplify.ts +69 -0
  87. package/skills/bundled/skillify.ts +197 -0
  88. package/skills/bundled/stuck.ts +79 -0
  89. package/skills/bundled/updateConfig.ts +475 -0
  90. package/skills/bundled/verify/SKILL.md +3 -0
  91. package/skills/bundled/verify/examples/cli.md +3 -0
  92. package/skills/bundled/verify/examples/server.md +3 -0
  93. package/skills/bundled/verify.ts +30 -0
  94. package/skills/bundled/verifyContent.ts +13 -0
  95. package/skills/bundledSkills.ts +220 -0
  96. package/skills/loadSkillsDir.ts +1086 -0
  97. package/skills/mcpSkillBuilders.ts +44 -0
  98. package/tasks/DreamTask/DreamTask.ts +157 -0
  99. package/tasks/InProcessTeammateTask/InProcessTeammateTask.tsx +126 -0
  100. package/tasks/InProcessTeammateTask/types.ts +121 -0
  101. package/tasks/LocalAgentTask/LocalAgentTask.tsx +683 -0
  102. package/tasks/LocalMainSessionTask.ts +479 -0
  103. package/tasks/LocalShellTask/LocalShellTask.tsx +523 -0
  104. package/tasks/LocalShellTask/guards.ts +41 -0
  105. package/tasks/LocalShellTask/killShellTasks.ts +76 -0
  106. package/tasks/RemoteAgentTask/RemoteAgentTask.tsx +856 -0
  107. package/tasks/pillLabel.ts +82 -0
  108. package/tasks/stopTask.ts +100 -0
  109. package/tasks/types.ts +46 -0
  110. package/upstreamproxy/relay.ts +455 -0
  111. package/upstreamproxy/upstreamproxy.ts +285 -0
  112. package/vim/motions.ts +82 -0
  113. package/vim/operators.ts +556 -0
  114. package/vim/textObjects.ts +186 -0
  115. package/vim/transitions.ts +490 -0
  116. package/vim/types.ts +199 -0
  117. package/voice/voiceModeEnabled.ts +54 -0
@@ -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
+ }