@plannotator/ui 0.27.0 → 0.29.0

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 (106) hide show
  1. package/README.md +22 -0
  2. package/components/AISettingsTab.tsx +5 -4
  3. package/components/ActionMenu.tsx +5 -1
  4. package/components/AgentsTab.tsx +10 -23
  5. package/components/AnnotationPanel.tsx +9 -5
  6. package/components/AnnotationToolbar.tsx +5 -13
  7. package/components/AnnotationToolstrip.tsx +2 -2
  8. package/components/ApproveDropdown.tsx +1 -1
  9. package/components/BlockRenderer.tsx +1 -1
  10. package/components/CodeFilePopout.tsx +5 -4
  11. package/components/CommentPopover.tsx +391 -65
  12. package/components/DocBadges.tsx +29 -11
  13. package/components/ExportModal.tsx +16 -8
  14. package/components/GraphvizBlock.tsx +1 -1
  15. package/components/InlineMarkdown.tsx +30 -13
  16. package/components/KeyboardShortcuts.tsx +37 -2
  17. package/components/Landing.tsx +7 -7
  18. package/components/MarkdownDiff.tsx +60 -0
  19. package/components/MenuVersionSection.tsx +4 -4
  20. package/components/MermaidBlock.tsx +1 -1
  21. package/components/ModeToggle.tsx +7 -6
  22. package/components/OpenInAppButton.tsx +2 -5
  23. package/components/PinpointOverlay.tsx +9 -7
  24. package/components/PlanHeaderMenu.tsx +8 -8
  25. package/components/PopoutDialog.tsx +6 -1
  26. package/components/ResizeHandle.tsx +1 -0
  27. package/components/Settings.tsx +172 -12
  28. package/components/SkillReferenceMenu.tsx +260 -0
  29. package/components/StickyHeaderLane.tsx +7 -0
  30. package/components/ThemeProvider.tsx +131 -32
  31. package/components/ThemeTab.tsx +123 -77
  32. package/components/ToolbarButtons.tsx +29 -8
  33. package/components/Viewer.tsx +396 -130
  34. package/components/VimKeyHud.tsx +695 -0
  35. package/components/VimModeAnnouncementDialog.tsx +557 -0
  36. package/components/VimModeOverlay.tsx +235 -0
  37. package/components/VimTargetReticle.tsx +284 -0
  38. package/components/ai/DocumentAIChatPanel.tsx +1 -1
  39. package/components/blocks/CodeBlock.tsx +18 -18
  40. package/components/blocks/TablePopout.tsx +7 -8
  41. package/components/blocks/TableToolbar.tsx +7 -8
  42. package/components/goal-setup/GoalSetupSurface.tsx +16 -3
  43. package/components/html-viewer/HtmlViewer.tsx +450 -47
  44. package/components/html-viewer/annotationNumbering.ts +37 -0
  45. package/components/html-viewer/bridge-script.ts +4051 -298
  46. package/components/html-viewer/composerYield.ts +51 -0
  47. package/components/html-viewer/srcdoc.ts +18 -3
  48. package/components/html-viewer/useHtmlAnnotation.ts +457 -32
  49. package/components/icons/themeIcons.tsx +1 -1
  50. package/components/plan-diff/PlanCleanDiffView.tsx +9 -9
  51. package/components/plan-diff/PlanDiffBadge.tsx +22 -1
  52. package/components/settings/HooksTab.tsx +12 -8
  53. package/components/sidebar/FileBrowser.tsx +4 -1
  54. package/components/themeModes.tsx +28 -0
  55. package/config/configStore.ts +76 -1
  56. package/config/settings.ts +152 -0
  57. package/configure.ts +9 -0
  58. package/globals.d.ts +7 -1
  59. package/hooks/useAIChat.ts +5 -2
  60. package/hooks/useAIProviderActivation.ts +47 -0
  61. package/hooks/useAIProviderConfig.ts +5 -1
  62. package/hooks/useAgentSettings.ts +64 -23
  63. package/hooks/useAgents.ts +4 -4
  64. package/hooks/useAnnotationHighlighter.ts +100 -3
  65. package/hooks/useArchive.ts +2 -1
  66. package/hooks/useFenceTheme.ts +17 -0
  67. package/hooks/useLinkedDoc.ts +68 -1
  68. package/hooks/usePinpoint.ts +76 -75
  69. package/hooks/usePlanDiff.ts +73 -2
  70. package/hooks/useSkillReferenceAutocomplete.ts +239 -0
  71. package/hooks/useUpdateCheck.ts +1 -2
  72. package/hooks/useVimDocumentFocus.ts +116 -0
  73. package/hooks/useVimSelection.ts +1063 -0
  74. package/package.json +7 -6
  75. package/print.css +14 -13
  76. package/shortcuts/core.ts +38 -13
  77. package/shortcuts/index.ts +10 -0
  78. package/shortcuts/plan-review/commentPopover.shortcuts.ts +7 -0
  79. package/shortcuts/plan-review/vimSelection.shortcuts.ts +251 -0
  80. package/shortcuts/runtime.ts +111 -12
  81. package/styles.css +1 -1
  82. package/theme.css +504 -0
  83. package/themes/colorblind.css +89 -0
  84. package/themes/plannotator.css +2 -2
  85. package/types.ts +93 -10
  86. package/utils/agentSwitch.ts +33 -7
  87. package/utils/blockTargeting.ts +462 -178
  88. package/utils/clipboard.ts +110 -0
  89. package/utils/codeBlockMark.ts +50 -0
  90. package/utils/codeHighlight.ts +293 -0
  91. package/utils/codexModels.ts +79 -0
  92. package/utils/domSelection.ts +84 -0
  93. package/utils/htmlChrome.ts +73 -0
  94. package/utils/inputMethod.ts +79 -6
  95. package/utils/parser.ts +517 -21
  96. package/utils/preferenceTtl.ts +15 -0
  97. package/utils/sharing.ts +0 -1
  98. package/utils/skillCatalog.ts +269 -0
  99. package/utils/skillReferences.ts +475 -0
  100. package/utils/syntaxTheme.ts +83 -0
  101. package/utils/themeRegistry.ts +154 -0
  102. package/utils/vimHud.ts +263 -0
  103. package/utils/vimModeAnnouncement.ts +23 -0
  104. package/utils/vimNavigation.ts +417 -0
  105. package/utils/vimReticle.ts +88 -0
  106. package/utils/vimScroll.ts +162 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plannotator/ui",
3
- "version": "0.27.0",
3
+ "version": "0.29.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./components/*": "./components/*.tsx",
@@ -18,6 +18,7 @@
18
18
  "./shortcuts": "./shortcuts/index.ts",
19
19
  "./config": "./config/index.ts",
20
20
  "./configure": "./configure.ts",
21
+ "./theme-modes": "./components/themeModes.tsx",
21
22
  "./types": "./types.ts",
22
23
  "./theme": "./theme.css",
23
24
  "./styles.css": "./styles.css"
@@ -57,6 +58,7 @@
57
58
  "@codemirror/lang-yaml": "^6.1.3",
58
59
  "@codemirror/language": "^6.12.3",
59
60
  "@codemirror/legacy-modes": "^6.5.3",
61
+ "@codemirror/merge": "^6.12.2",
60
62
  "@codemirror/search": "^6.7.0",
61
63
  "@codemirror/state": "^6.6.0",
62
64
  "@codemirror/view": "^6.43.0",
@@ -64,10 +66,10 @@
64
66
  "@fontsource-variable/inter": "^5.2.8",
65
67
  "@lezer/common": "^1.5.2",
66
68
  "@lezer/highlight": "^1.2.3",
67
- "@pierre/diffs": "1.2.8",
68
- "@plannotator/atomic-editor": "^0.7.0",
69
- "@plannotator/core": "0.22.0",
70
- "@plannotator/markdown-editor": "^0.3.2",
69
+ "@pierre/diffs": "1.3.2",
70
+ "@plannotator/atomic-editor": "^0.8.0",
71
+ "@plannotator/core": "0.23.0",
72
+ "@plannotator/markdown-editor": "^0.4.0",
71
73
  "@plannotator/web-highlighter": "^0.8.1",
72
74
  "@tanstack/react-table": "^8.21.3",
73
75
  "@viz-js/viz": "^3.25.0",
@@ -75,7 +77,6 @@
75
77
  "clsx": "^2.1.1",
76
78
  "diff": "^8.0.4",
77
79
  "dompurify": "^3.3.3",
78
- "highlight.js": "^11.11.1",
79
80
  "katex": "^0.16.47",
80
81
  "lucide-react": "^1.14.0",
81
82
  "marked": "^17.0.6",
package/print.css CHANGED
@@ -4,7 +4,8 @@
4
4
  * 1. @media print — standard print styles
5
5
  * 2. .plannotator-print — class added via JS beforeprint/afterprint events
6
6
  * to guarantee overrides that @media print alone cannot achieve
7
- * (e.g. beating Tailwind layers + hljs github-dark theme).
7
+ * (e.g. beating Tailwind layers, and the syntax theme's per-token inline
8
+ * colours — inline styles only lose to an !important author rule).
8
9
  */
9
10
 
10
11
  /* ============================================================
@@ -12,7 +13,7 @@
12
13
  * These use .plannotator-print on <html> for maximum specificity.
13
14
  * ============================================================ */
14
15
 
15
- /* Code blocks: override github-dark.css .hljs{background:#0d1117} */
16
+ /* Code blocks: flatten the syntax theme's dark block background to paper */
16
17
  .plannotator-print pre,
17
18
  .plannotator-print pre[class] {
18
19
  background: #f5f5f5 !important;
@@ -23,18 +24,19 @@
23
24
  }
24
25
 
25
26
  .plannotator-print pre code,
26
- .plannotator-print code.hljs,
27
- .plannotator-print pre code.hljs,
28
- .plannotator-print .hljs {
27
+ .plannotator-print code.pn-code,
28
+ .plannotator-print pre code.pn-code,
29
+ .plannotator-print .code-snippet-preview {
29
30
  background: transparent !important;
30
31
  background-color: transparent !important;
31
32
  color: #1a1a1a !important;
32
33
  }
33
34
 
35
+ /* The syntax theme colours every token with an inline `style`, so these
36
+ !important rules are what flattens code to black on paper. */
34
37
  .plannotator-print pre span,
35
38
  .plannotator-print pre code span,
36
- .plannotator-print .hljs span,
37
- .plannotator-print [class*="hljs-"] {
39
+ .plannotator-print .code-snippet-preview span {
38
40
  color: #1a1a1a !important;
39
41
  background: transparent !important;
40
42
  background-color: transparent !important;
@@ -284,9 +286,8 @@
284
286
 
285
287
  pre code,
286
288
  pre code[class],
287
- pre code.hljs,
288
- code.hljs,
289
- code[data-highlighted] {
289
+ pre code.pn-code,
290
+ code.pn-code {
290
291
  font-size: 9pt !important;
291
292
  background: transparent !important;
292
293
  background-color: transparent !important;
@@ -298,14 +299,14 @@
298
299
  word-wrap: break-word !important;
299
300
  }
300
301
 
301
- .hljs {
302
+ .code-snippet-preview {
302
303
  background: transparent !important;
303
304
  background-color: transparent !important;
304
305
  color: #1a1a1a !important;
305
306
  }
306
307
 
307
- pre span, pre code span, .hljs span, code span,
308
- [class*="hljs-"] {
308
+ pre span, pre code span, code span,
309
+ .code-snippet-preview span {
309
310
  color: #1a1a1a !important;
310
311
  background: transparent !important;
311
312
  background-color: transparent !important;
package/shortcuts/core.ts CHANGED
@@ -46,12 +46,6 @@ const NAMED_TOKENS = new Set([
46
46
  'Enter',
47
47
  'Escape',
48
48
  'Tab',
49
- // TODO(migration): `matchesKeyToken` does not currently match `Space` —
50
- // pressing Spacebar produces `event.key === ' '` (length 1), which the
51
- // matcher uppercases to `' '` and then compares to the literal `'Space'`,
52
- // always failing. Add a special case in `matchesKeyToken` (e.g.
53
- // `if (token === 'Space') return event.key === ' ' || event.code === 'Space'`)
54
- // before any scope binds Space.
55
49
  'Space',
56
50
  'Backspace',
57
51
  'Delete',
@@ -68,8 +62,13 @@ const NAMED_TOKENS = new Set([
68
62
  // whitelist explicitly so typos like `Cmd` instead of `Mod` keep failing
69
63
  // validation.
70
64
  '.',
65
+ '/',
71
66
  '[',
72
67
  ']',
68
+ '{',
69
+ '}',
70
+ '?',
71
+ '$',
73
72
  ]);
74
73
 
75
74
  for (let n = 1; n <= 12; n += 1) {
@@ -77,6 +76,11 @@ for (let n = 1; n <= 12; n += 1) {
77
76
  }
78
77
 
79
78
  const MODIFIER_TOKENS = new Set(['Mod', 'Shift', 'Alt']);
79
+ const SHIFTED_LITERAL_TOKENS = new Set(['{', '}', '?', '$']);
80
+ type ShortcutKeyEvent = Pick<
81
+ KeyboardEvent,
82
+ 'key' | 'code' | 'metaKey' | 'ctrlKey' | 'shiftKey' | 'altKey'
83
+ >;
80
84
 
81
85
  export function defineShortcutScope<TAction extends string>(scope: ShortcutScopeDefinition<TAction>): ShortcutScopeDefinition<TAction> {
82
86
  return scope;
@@ -115,7 +119,7 @@ export function parseDoubleTapBinding(binding: string): string | null {
115
119
  * Check if a KeyboardEvent matches a named key token (for sequential/stateful matching).
116
120
  * Unlike `matchesShortcutBinding`, this matches a single key identity without modifier checks.
117
121
  */
118
- export function matchesKeyName(event: KeyboardEvent, keyName: string): boolean {
122
+ export function matchesKeyName(event: ShortcutKeyEvent, keyName: string): boolean {
119
123
  if (keyName === 'Alt') return event.key === 'Alt';
120
124
  if (keyName === 'Shift') return event.key === 'Shift';
121
125
  if (keyName === 'Mod') return event.key === 'Meta' || event.key === 'Control';
@@ -333,13 +337,13 @@ export function formatShortcutBindingsText(
333
337
  return bindings.map(binding => formatShortcutBindingText(binding, platform)).join(' or ');
334
338
  }
335
339
 
336
- function getDigitCode(event: KeyboardEvent): string | null {
340
+ function getDigitCode(event: ShortcutKeyEvent): string | null {
337
341
  const code = typeof event.code === 'string' ? event.code : '';
338
342
  const match = code.match(/^Digit([0-9])$/);
339
343
  return match ? match[1] : null;
340
344
  }
341
345
 
342
- export function getShortcutDigit(event: KeyboardEvent): number | null {
346
+ export function getShortcutDigit(event: ShortcutKeyEvent): number | null {
343
347
  const parsed = Number.parseInt(event.key, 10);
344
348
  if (!Number.isNaN(parsed)) return parsed;
345
349
 
@@ -347,10 +351,14 @@ export function getShortcutDigit(event: KeyboardEvent): number | null {
347
351
  return digitCode === null ? null : Number.parseInt(digitCode, 10);
348
352
  }
349
353
 
350
- function matchesKeyToken(event: KeyboardEvent, token: string): boolean {
354
+ function matchesKeyToken(event: ShortcutKeyEvent, token: string): boolean {
351
355
  const key = event.key.length === 1 ? event.key.toUpperCase() : event.key;
352
356
  const shortcutDigit = getShortcutDigit(event);
353
357
 
358
+ if (token === 'Space') {
359
+ return event.key === ' ' || event.key === 'Spacebar' || event.code === 'Space';
360
+ }
361
+
354
362
  if (token === 'A-Z') {
355
363
  return /^[A-Z]$/.test(key);
356
364
  }
@@ -370,7 +378,13 @@ function matchesKeyToken(event: KeyboardEvent, token: string): boolean {
370
378
  return key === token;
371
379
  }
372
380
 
373
- export function matchesShortcutBinding(event: KeyboardEvent, binding: string): boolean {
381
+ /**
382
+ * Match a keyboard event against one normalized, single-press binding.
383
+ *
384
+ * Sequential and hold bindings deliberately return false; their timing
385
+ * semantics are handled by the shortcut runtime's dedicated paths.
386
+ */
387
+ export function matchesShortcutBinding(event: ShortcutKeyEvent, binding: string): boolean {
374
388
  if (binding.includes(' ') || binding.includes('hold')) {
375
389
  return false;
376
390
  }
@@ -385,7 +399,9 @@ export function matchesShortcutBinding(event: KeyboardEvent, binding: string): b
385
399
  if (keyTokens.length !== 1) return false;
386
400
 
387
401
  const keyToken = keyTokens[0];
388
- const shiftMatches = requiresShift === event.shiftKey || (!requiresShift && keyToken === 'A-Z' && event.shiftKey);
402
+ const shiftMatches = requiresShift === event.shiftKey
403
+ || (!requiresShift && keyToken === 'A-Z' && event.shiftKey)
404
+ || (!requiresShift && SHIFTED_LITERAL_TOKENS.has(keyToken) && event.key === keyToken);
389
405
 
390
406
  if (requiresMod !== (event.metaKey || event.ctrlKey)) return false;
391
407
  if (!shiftMatches) return false;
@@ -394,6 +410,15 @@ export function matchesShortcutBinding(event: KeyboardEvent, binding: string): b
394
410
  return matchesKeyToken(event, keyToken);
395
411
  }
396
412
 
397
- export function getMatchingShortcutBindingIndex(event: KeyboardEvent, bindings: string[]): number {
413
+ /**
414
+ * Match one key group from a sequential binding such as `G G`.
415
+ *
416
+ * The group uses the same normalized syntax as an ordinary one-press binding.
417
+ */
418
+ export function matchesShortcutBindingGroup(event: ShortcutKeyEvent, group: string): boolean {
419
+ return matchesShortcutBinding(event, group);
420
+ }
421
+
422
+ export function getMatchingShortcutBindingIndex(event: ShortcutKeyEvent, bindings: string[]): number {
398
423
  return bindings.findIndex(binding => matchesShortcutBinding(event, binding));
399
424
  }
@@ -8,6 +8,16 @@ export { commentPopoverShortcuts } from './plan-review/commentPopover.shortcuts'
8
8
  export { imageAnnotatorShortcuts, useImageAnnotatorShortcuts } from './plan-review/imageAnnotator.shortcuts';
9
9
  export { inputMethodShortcuts } from './plan-review/inputMethod.shortcuts';
10
10
  export { viewerShortcuts, useViewerShortcuts } from './plan-review/viewer.shortcuts';
11
+ export {
12
+ describeVimSelectionAction,
13
+ isVimSelectionActionId,
14
+ vimSelectionShortcuts,
15
+ useVimSelectionShortcuts,
16
+ } from './plan-review/vimSelection.shortcuts';
17
+ export type {
18
+ VimSelectionActionId,
19
+ VimSelectionHudContext,
20
+ } from './plan-review/vimSelection.shortcuts';
11
21
  export { goalSetupShortcuts, useGoalSetupShortcuts } from './plan-review/goalSetup.shortcuts';
12
22
  export { annotateSidebarShortcuts, useAnnotateSidebarShortcuts } from './plan-review/sidebar.shortcuts';
13
23
 
@@ -17,5 +17,12 @@ export const commentPopoverShortcuts = defineShortcutScope({
17
17
  hint: 'Available while the comment editor is open.',
18
18
  displayOrder: 40,
19
19
  },
20
+ skillMenuOpen: {
21
+ description: 'Reference an agent skill (opens the skill menu)',
22
+ bindings: ['/', '$'],
23
+ section: 'Annotations',
24
+ hint: 'Type / or $ at the start of a word in the comment editor to open the skill menu; keep typing to filter. Nothing is preselected: Enter stays a newline until you pick a row with the arrow keys (or click one), then Enter or Tab inserts it. Escape dismisses the menu.',
25
+ displayOrder: 50,
26
+ },
20
27
  },
21
28
  });
@@ -0,0 +1,251 @@
1
+ import { defineShortcutScope } from '../core';
2
+ import { createShortcutScopeHook } from '../runtime';
3
+
4
+ /**
5
+ * Modal document-navigation commands shared by plan review and annotate mode.
6
+ *
7
+ * The bindings are active only while the opted-in document focus surface owns
8
+ * focus; native controls and annotation composers remain outside this scope.
9
+ */
10
+ export const vimSelectionShortcuts = defineShortcutScope({
11
+ id: 'vim-selection',
12
+ title: 'Vim selection',
13
+ shortcuts: {
14
+ moveDown: {
15
+ description: 'Next block or semantic sibling',
16
+ bindings: ['J'],
17
+ section: 'Vim Document Navigation',
18
+ displayOrder: 10,
19
+ },
20
+ moveUp: {
21
+ description: 'Previous block or semantic sibling',
22
+ bindings: ['K'],
23
+ section: 'Vim Document Navigation',
24
+ displayOrder: 20,
25
+ },
26
+ documentStart: {
27
+ description: 'Start of document',
28
+ bindings: ['G G'],
29
+ section: 'Vim Document Navigation',
30
+ displayOrder: 30,
31
+ preventDefault: true,
32
+ },
33
+ documentEnd: {
34
+ description: 'End of document',
35
+ bindings: ['Shift+G'],
36
+ section: 'Vim Document Navigation',
37
+ displayOrder: 40,
38
+ },
39
+ moveOut: {
40
+ description: 'Move to containing target',
41
+ bindings: ['H'],
42
+ section: 'Vim Document Navigation',
43
+ displayOrder: 50,
44
+ },
45
+ refine: {
46
+ description: 'Refine into child or text',
47
+ bindings: ['L'],
48
+ section: 'Vim Document Navigation',
49
+ displayOrder: 60,
50
+ },
51
+ visual: {
52
+ description: 'Toggle precise Visual selection',
53
+ bindings: ['V'],
54
+ section: 'Vim Document Navigation',
55
+ displayOrder: 70,
56
+ },
57
+ visualBlock: {
58
+ description: 'Toggle whole-block Visual selection',
59
+ bindings: ['Shift+V'],
60
+ section: 'Vim Document Navigation',
61
+ hint: 'Use j / k to extend by whole blocks.',
62
+ displayOrder: 80,
63
+ },
64
+ wordForward: {
65
+ description: 'Move to next word',
66
+ bindings: ['W'],
67
+ section: 'Vim Text Navigation',
68
+ displayOrder: 90,
69
+ },
70
+ wordBackward: {
71
+ description: 'Move to previous word',
72
+ bindings: ['B'],
73
+ section: 'Vim Text Navigation',
74
+ displayOrder: 100,
75
+ },
76
+ wordEnd: {
77
+ description: 'Move to end of word',
78
+ bindings: ['E'],
79
+ section: 'Vim Text Navigation',
80
+ displayOrder: 110,
81
+ },
82
+ lineStart: {
83
+ description: 'Move to start of line',
84
+ bindings: ['0'],
85
+ section: 'Vim Text Navigation',
86
+ displayOrder: 120,
87
+ },
88
+ lineEnd: {
89
+ description: 'Move to end of line',
90
+ bindings: ['$'],
91
+ section: 'Vim Text Navigation',
92
+ displayOrder: 130,
93
+ },
94
+ previousTextBlock: {
95
+ description: 'Move to previous text block',
96
+ bindings: ['{'],
97
+ section: 'Vim Text Navigation',
98
+ displayOrder: 140,
99
+ },
100
+ nextTextBlock: {
101
+ description: 'Move to next text block',
102
+ bindings: ['}'],
103
+ section: 'Vim Text Navigation',
104
+ displayOrder: 150,
105
+ },
106
+ swapSelectionEnds: {
107
+ description: 'Swap selection ends',
108
+ bindings: ['O'],
109
+ section: 'Vim Text Navigation',
110
+ displayOrder: 160,
111
+ },
112
+ activeAnnotation: {
113
+ description: 'Use active annotation mode',
114
+ bindings: ['Enter'],
115
+ section: 'Vim Annotation Actions',
116
+ displayOrder: 170,
117
+ },
118
+ annotationMenu: {
119
+ description: 'Open annotation actions',
120
+ bindings: ['Space'],
121
+ section: 'Vim Annotation Actions',
122
+ displayOrder: 180,
123
+ },
124
+ comment: {
125
+ description: 'Comment selection or target',
126
+ bindings: ['C'],
127
+ section: 'Vim Annotation Actions',
128
+ displayOrder: 190,
129
+ },
130
+ redline: {
131
+ description: 'Redline selection or target',
132
+ bindings: ['D'],
133
+ section: 'Vim Annotation Actions',
134
+ displayOrder: 200,
135
+ },
136
+ markup: {
137
+ description: 'Markup selection or target',
138
+ bindings: ['M'],
139
+ section: 'Vim Annotation Actions',
140
+ displayOrder: 210,
141
+ },
142
+ label: {
143
+ description: 'Label selection or target',
144
+ bindings: ['T'],
145
+ section: 'Vim Annotation Actions',
146
+ displayOrder: 220,
147
+ },
148
+ copy: {
149
+ description: 'Copy selection or target',
150
+ bindings: ['Y'],
151
+ section: 'Vim Annotation Actions',
152
+ displayOrder: 230,
153
+ },
154
+ cancel: {
155
+ description: 'Cancel current Vim state',
156
+ bindings: ['Escape'],
157
+ section: 'Vim Annotation Actions',
158
+ displayOrder: 240,
159
+ },
160
+ help: {
161
+ description: 'Toggle key map',
162
+ bindings: ['?'],
163
+ section: 'Vim Annotation Actions',
164
+ displayOrder: 250,
165
+ },
166
+ },
167
+ });
168
+
169
+ /** Stable action identifiers emitted by the Vim selection shortcut scope. */
170
+ export type VimSelectionActionId = keyof typeof vimSelectionShortcuts.shortcuts;
171
+
172
+ /** Vim navigation state used to make HUD command descriptions contextual. */
173
+ export type VimSelectionHudContext =
174
+ | 'inactive'
175
+ | 'block'
176
+ | 'inline'
177
+ | 'text'
178
+ | 'visual'
179
+ | 'visual-block'
180
+ | 'action';
181
+
182
+ /**
183
+ * Return the user-facing HUD description for a handled Vim action.
184
+ *
185
+ * Contextual movement keys keep one registered shortcut while accurately
186
+ * describing whether they moved by document structure, line, or character.
187
+ */
188
+ export function describeVimSelectionAction(
189
+ actionId: VimSelectionActionId,
190
+ context: VimSelectionHudContext,
191
+ ): string {
192
+ switch (actionId) {
193
+ case 'moveDown':
194
+ if (context === 'inline') return 'Next semantic sibling';
195
+ if (context === 'text' || context === 'visual') return 'Next line';
196
+ if (context === 'visual-block') return 'Extend to next block';
197
+ return 'Next block';
198
+ case 'moveUp':
199
+ if (context === 'inline') return 'Previous semantic sibling';
200
+ if (context === 'text' || context === 'visual') return 'Previous line';
201
+ if (context === 'visual-block') return 'Extend to previous block';
202
+ return 'Previous block';
203
+ case 'moveOut':
204
+ return context === 'text' || context === 'visual'
205
+ ? 'Move left one character'
206
+ : 'Move to containing target';
207
+ case 'refine':
208
+ return context === 'text' || context === 'visual'
209
+ ? 'Move right one character'
210
+ : 'Refine into child or text';
211
+ case 'visual':
212
+ return context === 'visual'
213
+ ? 'Return to Normal mode'
214
+ : 'Start Visual selection';
215
+ case 'visualBlock':
216
+ return context === 'visual-block'
217
+ ? 'Return to block navigation'
218
+ : 'Select the whole block';
219
+ case 'documentStart':
220
+ case 'documentEnd':
221
+ case 'wordForward':
222
+ case 'wordBackward':
223
+ case 'wordEnd':
224
+ case 'lineStart':
225
+ case 'lineEnd':
226
+ case 'previousTextBlock':
227
+ case 'nextTextBlock':
228
+ case 'swapSelectionEnds':
229
+ case 'activeAnnotation':
230
+ case 'annotationMenu':
231
+ case 'comment':
232
+ case 'redline':
233
+ case 'markup':
234
+ case 'label':
235
+ case 'copy':
236
+ case 'cancel':
237
+ case 'help':
238
+ return vimSelectionShortcuts.shortcuts[actionId].description;
239
+ }
240
+ }
241
+
242
+ /** Parse an unknown bridge value into a registered Vim action identifier. */
243
+ export function isVimSelectionActionId(
244
+ value: unknown,
245
+ ): value is VimSelectionActionId {
246
+ return typeof value === 'string'
247
+ && Object.prototype.hasOwnProperty.call(vimSelectionShortcuts.shortcuts, value);
248
+ }
249
+
250
+ /** Bind Vim selection handlers to the opted-in document focus surface. */
251
+ export const useVimSelectionShortcuts = createShortcutScopeHook(vimSelectionShortcuts);