neon 3.0.0 → 3.1.1

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 (207) hide show
  1. package/README.md +70 -5
  2. package/dist/_chunks/auth_selection-DGgq6ifc.js +83 -0
  3. package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
  4. package/dist/_chunks/credentials-MYdHdKah.js +188 -0
  5. package/dist/_chunks/env-NbA61JR3.js +585 -0
  6. package/dist/_chunks/env_services-Tz9G4JeT.js +531 -0
  7. package/dist/_chunks/paths-DMq0Lt7a.js +151 -0
  8. package/dist/_chunks/profiles-Ir29rqns.js +217 -0
  9. package/dist/_chunks/psql-DWH-kc69.js +2169 -0
  10. package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  11. package/dist/_chunks/secure_file-BucZj4yQ.js +39 -0
  12. package/dist/analytics.js +163 -207
  13. package/dist/api.js +815 -758
  14. package/dist/auth.js +121 -141
  15. package/dist/auth_context.js +39 -53
  16. package/dist/cli.js +4 -7
  17. package/dist/commands/api.js +220 -250
  18. package/dist/commands/api_keys.js +251 -314
  19. package/dist/commands/auth.js +283 -328
  20. package/dist/commands/bootstrap.js +372 -437
  21. package/dist/commands/branches.js +304 -455
  22. package/dist/commands/bucket.js +374 -514
  23. package/dist/commands/checkout.js +213 -298
  24. package/dist/commands/config.js +573 -690
  25. package/dist/commands/connection_string.js +137 -165
  26. package/dist/commands/data_api.js +238 -260
  27. package/dist/commands/databases.js +67 -76
  28. package/dist/commands/deploy.js +31 -25
  29. package/dist/commands/dev.js +639 -719
  30. package/dist/commands/diff.js +156 -200
  31. package/dist/commands/env.js +255 -305
  32. package/dist/commands/functions.js +275 -355
  33. package/dist/commands/index.js +70 -65
  34. package/dist/commands/init.js +84 -119
  35. package/dist/commands/inspect.js +55 -55
  36. package/dist/commands/ip_allow.js +88 -120
  37. package/dist/commands/link.js +874 -1019
  38. package/dist/commands/logs.js +291 -0
  39. package/dist/commands/neon_auth.js +725 -933
  40. package/dist/commands/operations.js +34 -25
  41. package/dist/commands/orgs.js +28 -18
  42. package/dist/commands/profile.js +615 -846
  43. package/dist/commands/projects.js +313 -373
  44. package/dist/commands/psql.js +60 -58
  45. package/dist/commands/roles.js +55 -58
  46. package/dist/commands/schema_diff.js +87 -131
  47. package/dist/commands/set_context.js +34 -26
  48. package/dist/commands/snapshots.js +288 -413
  49. package/dist/commands/status.js +41 -37
  50. package/dist/commands/user.js +21 -10
  51. package/dist/commands/vpc_endpoints.js +85 -113
  52. package/dist/config.js +7 -6
  53. package/dist/config_format.js +50 -66
  54. package/dist/config_template.js +128 -157
  55. package/dist/context.js +183 -235
  56. package/dist/current_branch_fast_path.js +40 -49
  57. package/dist/dev/env.js +2 -446
  58. package/dist/dev/functions.js +54 -68
  59. package/dist/dev/inputs.js +46 -58
  60. package/dist/dev/runtime.js +135 -164
  61. package/dist/dev/websocket.js +766 -959
  62. package/dist/env.js +27 -33
  63. package/dist/env_file.js +118 -132
  64. package/dist/env_services.js +2 -51
  65. package/dist/errors.js +57 -68
  66. package/dist/functions_api.js +45 -43
  67. package/dist/help.js +189 -140
  68. package/dist/index.js +182 -257
  69. package/dist/init/agents.js +137 -118
  70. package/dist/init/auth.js +58 -68
  71. package/dist/init/bootstrap.js +325 -396
  72. package/dist/init/build_config.js +4 -2
  73. package/dist/init/detect_agent.js +56 -101
  74. package/dist/init/editors.js +35 -52
  75. package/dist/init/enrich_output.js +51 -66
  76. package/dist/init/extension.js +134 -171
  77. package/dist/init/inspect.js +179 -266
  78. package/dist/init/interactive.js +510 -622
  79. package/dist/init/neonctl.js +117 -168
  80. package/dist/init/orchestrate.js +157 -173
  81. package/dist/init/phases/auth.js +188 -202
  82. package/dist/init/phases/cleanup.js +23 -23
  83. package/dist/init/phases/db.js +251 -277
  84. package/dist/init/phases/getting_started.js +213 -223
  85. package/dist/init/phases/mcp.js +174 -224
  86. package/dist/init/phases/migrations.js +247 -248
  87. package/dist/init/phases/neon_auth.js +114 -133
  88. package/dist/init/phases/setup.js +546 -703
  89. package/dist/init/phases/skills.js +75 -86
  90. package/dist/init/phases/status.js +72 -67
  91. package/dist/init/resolve_context.js +102 -99
  92. package/dist/init/route_command.js +91 -98
  93. package/dist/init/skills.js +174 -218
  94. package/dist/init/vsix.js +77 -99
  95. package/dist/log.js +17 -16
  96. package/dist/neon_services.js +104 -129
  97. package/dist/parameters.gen.js +481 -471
  98. package/dist/pkg.js +17 -19
  99. package/dist/profile_keys.js +44 -47
  100. package/dist/psql/cli.js +44 -47
  101. package/dist/psql/command/cmd_cond.js +231 -406
  102. package/dist/psql/command/cmd_connect.js +557 -764
  103. package/dist/psql/command/cmd_copy.js +728 -984
  104. package/dist/psql/command/cmd_describe.js +1499 -1688
  105. package/dist/psql/command/cmd_format.js +733 -905
  106. package/dist/psql/command/cmd_io.js +2 -2193
  107. package/dist/psql/command/cmd_lo.js +297 -359
  108. package/dist/psql/command/cmd_meta.js +727 -878
  109. package/dist/psql/command/cmd_misc.js +138 -172
  110. package/dist/psql/command/cmd_pipeline.js +2 -1148
  111. package/dist/psql/command/cmd_restrict.js +119 -155
  112. package/dist/psql/command/cmd_show.js +529 -688
  113. package/dist/psql/command/dispatch.js +260 -325
  114. package/dist/psql/command/inputQueue.js +35 -33
  115. package/dist/psql/command/shared.js +49 -63
  116. package/dist/psql/complete/filenames.js +90 -133
  117. package/dist/psql/complete/index.js +59 -97
  118. package/dist/psql/complete/matcher.js +236 -300
  119. package/dist/psql/complete/psqlVars.js +218 -223
  120. package/dist/psql/complete/queries.js +159 -177
  121. package/dist/psql/complete/rules.js +1493 -2299
  122. package/dist/psql/core/common.js +2 -1253
  123. package/dist/psql/core/help.js +456 -546
  124. package/dist/psql/core/mainloop.js +692 -1303
  125. package/dist/psql/core/prompt.js +391 -408
  126. package/dist/psql/core/settings.js +429 -644
  127. package/dist/psql/core/sqlHelp.js +480 -554
  128. package/dist/psql/core/startup.js +2 -846
  129. package/dist/psql/core/syncVars.js +67 -110
  130. package/dist/psql/core/variables.js +156 -278
  131. package/dist/psql/describe/formatters.js +884 -1285
  132. package/dist/psql/describe/processNamePattern.js +173 -260
  133. package/dist/psql/describe/queries.js +1368 -2403
  134. package/dist/psql/describe/versionGate.js +32 -41
  135. package/dist/psql/index.js +2 -2030
  136. package/dist/psql/io/history.js +232 -271
  137. package/dist/psql/io/input.js +103 -108
  138. package/dist/psql/io/lineEditor/buffer.js +238 -319
  139. package/dist/psql/io/lineEditor/complete.js +135 -213
  140. package/dist/psql/io/lineEditor/filename.js +139 -148
  141. package/dist/psql/io/lineEditor/index.js +653 -870
  142. package/dist/psql/io/lineEditor/keymap.js +544 -702
  143. package/dist/psql/io/lineEditor/vt100.js +294 -341
  144. package/dist/psql/io/pgpass.js +158 -187
  145. package/dist/psql/io/pgservice.js +146 -183
  146. package/dist/psql/io/psqlrc.js +328 -403
  147. package/dist/psql/print/aligned.js +1020 -1683
  148. package/dist/psql/print/asciidoc.js +180 -214
  149. package/dist/psql/print/crosstab.js +281 -442
  150. package/dist/psql/print/csv.js +48 -70
  151. package/dist/psql/print/html.js +195 -226
  152. package/dist/psql/print/json.js +75 -88
  153. package/dist/psql/print/latex.js +291 -364
  154. package/dist/psql/print/pager.js +171 -242
  155. package/dist/psql/print/troff.js +194 -226
  156. package/dist/psql/print/unaligned.js +69 -95
  157. package/dist/psql/print/units.js +167 -169
  158. package/dist/psql/scanner/slash.js +428 -483
  159. package/dist/psql/scanner/sql.js +445 -889
  160. package/dist/psql/scanner/stringutils.js +309 -379
  161. package/dist/psql/types/index.js +8 -7
  162. package/dist/psql/types/scanner.js +25 -22
  163. package/dist/psql/wire/connection.js +2042 -2803
  164. package/dist/psql/wire/copy.js +84 -100
  165. package/dist/psql/wire/notify.js +39 -59
  166. package/dist/psql/wire/pipeline.js +305 -518
  167. package/dist/psql/wire/protocol.js +349 -417
  168. package/dist/psql/wire/sasl.js +180 -265
  169. package/dist/psql/wire/tls.js +400 -561
  170. package/dist/storage_api.js +115 -129
  171. package/dist/test_utils/fixtures.js +94 -113
  172. package/dist/test_utils/oauth_server.js +10 -7
  173. package/dist/test_utils/project_dir.js +33 -0
  174. package/dist/utils/ai_gateway_notice.js +131 -162
  175. package/dist/utils/api_enums.js +21 -28
  176. package/dist/utils/auth.js +10 -4
  177. package/dist/utils/branch_notice.js +20 -19
  178. package/dist/utils/branch_picker.js +83 -89
  179. package/dist/utils/cli_name.js +15 -12
  180. package/dist/utils/compute_units.js +20 -27
  181. package/dist/utils/config_diff.js +127 -158
  182. package/dist/utils/enrichers.js +95 -148
  183. package/dist/utils/esbuild.js +130 -189
  184. package/dist/utils/flags.js +35 -47
  185. package/dist/utils/formats.js +8 -15
  186. package/dist/utils/git_diff.js +69 -80
  187. package/dist/utils/inspect_db.js +101 -143
  188. package/dist/utils/inspect_queries.js +179 -142
  189. package/dist/utils/middlewares.js +39 -45
  190. package/dist/utils/openapi.js +87 -99
  191. package/dist/utils/package_manager.js +312 -110
  192. package/dist/utils/point_in_time.js +49 -53
  193. package/dist/utils/psql.js +89 -106
  194. package/dist/utils/service_picker.js +55 -58
  195. package/dist/utils/string.js +5 -5
  196. package/dist/utils/ui.js +38 -55
  197. package/dist/utils/write_sync.js +26 -35
  198. package/dist/utils/zip.js +4 -3
  199. package/dist/writer.js +67 -87
  200. package/package.json +11 -6
  201. package/dist/_shared/auth_selection.js +0 -86
  202. package/dist/_shared/credentials.js +0 -209
  203. package/dist/_shared/env-core/env.js +0 -558
  204. package/dist/_shared/env-core/reuse-secrets.js +0 -223
  205. package/dist/_shared/paths.js +0 -148
  206. package/dist/_shared/profiles.js +0 -276
  207. package/dist/_shared/secure_file.js +0 -43
@@ -1,745 +1,587 @@
1
- /**
2
- * Emacs keymap dispatch.
3
- *
4
- * Pure function: take a `KeyEvent` plus the current editor state, mutate
5
- * the state, and return an `Action` describing what the renderer / outer
6
- * loop should do (redraw, submit, signal cancel, request completion, …).
7
- *
8
- * The state lives in `EditorState`, which wraps a `LineBuffer` plus a
9
- * little extra context (history navigation, last-tab timestamp, search
10
- * mode). Keeping dispatch separate from rendering means we can write
11
- * deterministic key-by-key tests without touching a TTY.
12
- */
13
1
  import { LineBuffer } from "./buffer.js";
14
- export const makeState = (history = [], mode = "emacs") => ({
15
- buffer: new LineBuffer(),
16
- history: [...history],
17
- historyIndex: -1,
18
- liveSnapshot: null,
19
- lastYank: null,
20
- pasting: false,
21
- mode,
22
- viPending: null,
23
- exBuffer: "",
2
+ //#region src/psql/io/lineEditor/keymap.ts
3
+ /**
4
+ * Emacs keymap dispatch.
5
+ *
6
+ * Pure function: take a `KeyEvent` plus the current editor state, mutate
7
+ * the state, and return an `Action` describing what the renderer / outer
8
+ * loop should do (redraw, submit, signal cancel, request completion, …).
9
+ *
10
+ * The state lives in `EditorState`, which wraps a `LineBuffer` plus a
11
+ * little extra context (history navigation, last-tab timestamp, search
12
+ * mode). Keeping dispatch separate from rendering means we can write
13
+ * deterministic key-by-key tests without touching a TTY.
14
+ */
15
+ const makeState = (history = [], mode = "emacs") => ({
16
+ buffer: new LineBuffer(),
17
+ history: [...history],
18
+ historyIndex: -1,
19
+ liveSnapshot: null,
20
+ lastYank: null,
21
+ pasting: false,
22
+ mode,
23
+ viPending: null,
24
+ exBuffer: ""
24
25
  });
25
26
  /**
26
- * Apply one key event. Returns the resulting action for the outer loop.
27
- * All mutation happens in `state.buffer` (or the navigation fields).
28
- */
29
- export const dispatch = (state, ev) => {
30
- if (state.pasting) {
31
- // Inside a bracketed-paste block we treat every event as a literal char,
32
- // except the closing marker.
33
- if (ev.key === "paste-end") {
34
- state.pasting = false;
35
- return { kind: "paste-end" };
36
- }
37
- if (ev.key === "char" &&
38
- ev.char !== undefined &&
39
- !ev.ctrl &&
40
- !ev.meta) {
41
- state.buffer.insert(ev.char);
42
- return { kind: "redraw" };
43
- }
44
- if (ev.key === "enter") {
45
- state.buffer.insert("\n");
46
- return { kind: "redraw" };
47
- }
48
- if (ev.key === "tab") {
49
- state.buffer.insert("\t");
50
- return { kind: "redraw" };
51
- }
52
- return { kind: "noop" };
53
- }
54
- // ^C always cancels regardless of mode.
55
- if (ev.key === "char" && ev.ctrl && ev.char === "c") {
56
- state.viPending = null;
57
- return { kind: "cancel" };
58
- }
59
- // Route to vi dispatch when in vi modes.
60
- if (state.mode === "ex") {
61
- return dispatchViEx(state, ev);
62
- }
63
- if (state.mode === "normal") {
64
- return dispatchViNormal(state, ev);
65
- }
66
- if (state.mode === "insert") {
67
- return dispatchViInsert(state, ev);
68
- }
69
- switch (ev.key) {
70
- case "paste-start":
71
- state.pasting = true;
72
- return { kind: "paste-start" };
73
- case "paste-end":
74
- state.pasting = false;
75
- return { kind: "paste-end" };
76
- case "enter":
77
- return { kind: "submit" };
78
- case "tab":
79
- return { kind: "complete" };
80
- case "backspace":
81
- if (ev.meta) {
82
- state.buffer.killWordLeft();
83
- }
84
- else {
85
- state.buffer.deleteLeft();
86
- }
87
- return { kind: "redraw" };
88
- case "delete":
89
- state.buffer.deleteRight();
90
- return { kind: "redraw" };
91
- case "left":
92
- if (ev.meta)
93
- state.buffer.moveWordLeft();
94
- else
95
- state.buffer.moveLeft();
96
- return { kind: "redraw" };
97
- case "right":
98
- if (ev.meta)
99
- state.buffer.moveWordRight();
100
- else
101
- state.buffer.moveRight();
102
- return { kind: "redraw" };
103
- case "up":
104
- return navigateHistory(state, -1);
105
- case "down":
106
- return navigateHistory(state, +1);
107
- case "home":
108
- state.buffer.moveHome();
109
- return { kind: "redraw" };
110
- case "end":
111
- state.buffer.moveEnd();
112
- return { kind: "redraw" };
113
- case "escape":
114
- // Bare Escape: ignore (Alt prefixes are decoded into meta:true).
115
- return { kind: "noop" };
116
- case "char":
117
- return handleChar(state, ev);
118
- case "pageup":
119
- case "pagedown":
120
- case "unknown":
121
- return { kind: "bell" };
122
- }
27
+ * Apply one key event. Returns the resulting action for the outer loop.
28
+ * All mutation happens in `state.buffer` (or the navigation fields).
29
+ */
30
+ const dispatch = (state, ev) => {
31
+ if (state.pasting) {
32
+ if (ev.key === "paste-end") {
33
+ state.pasting = false;
34
+ return { kind: "paste-end" };
35
+ }
36
+ if (ev.key === "char" && ev.char !== void 0 && !ev.ctrl && !ev.meta) {
37
+ state.buffer.insert(ev.char);
38
+ return { kind: "redraw" };
39
+ }
40
+ if (ev.key === "enter") {
41
+ state.buffer.insert("\n");
42
+ return { kind: "redraw" };
43
+ }
44
+ if (ev.key === "tab") {
45
+ state.buffer.insert(" ");
46
+ return { kind: "redraw" };
47
+ }
48
+ return { kind: "noop" };
49
+ }
50
+ if (ev.key === "char" && ev.ctrl && ev.char === "c") {
51
+ state.viPending = null;
52
+ return { kind: "cancel" };
53
+ }
54
+ if (state.mode === "ex") return dispatchViEx(state, ev);
55
+ if (state.mode === "normal") return dispatchViNormal(state, ev);
56
+ if (state.mode === "insert") return dispatchViInsert(state, ev);
57
+ switch (ev.key) {
58
+ case "paste-start":
59
+ state.pasting = true;
60
+ return { kind: "paste-start" };
61
+ case "paste-end":
62
+ state.pasting = false;
63
+ return { kind: "paste-end" };
64
+ case "enter": return { kind: "submit" };
65
+ case "tab": return { kind: "complete" };
66
+ case "backspace":
67
+ if (ev.meta) state.buffer.killWordLeft();
68
+ else state.buffer.deleteLeft();
69
+ return { kind: "redraw" };
70
+ case "delete":
71
+ state.buffer.deleteRight();
72
+ return { kind: "redraw" };
73
+ case "left":
74
+ if (ev.meta) state.buffer.moveWordLeft();
75
+ else state.buffer.moveLeft();
76
+ return { kind: "redraw" };
77
+ case "right":
78
+ if (ev.meta) state.buffer.moveWordRight();
79
+ else state.buffer.moveRight();
80
+ return { kind: "redraw" };
81
+ case "up": return navigateHistory(state, -1);
82
+ case "down": return navigateHistory(state, 1);
83
+ case "home":
84
+ state.buffer.moveHome();
85
+ return { kind: "redraw" };
86
+ case "end":
87
+ state.buffer.moveEnd();
88
+ return { kind: "redraw" };
89
+ case "escape": return { kind: "noop" };
90
+ case "char": return handleChar(state, ev);
91
+ case "pageup":
92
+ case "pagedown":
93
+ case "unknown": return { kind: "bell" };
94
+ }
123
95
  };
124
96
  const handleChar = (state, ev) => {
125
- const ch = ev.char ?? "";
126
- if (ch.length === 0)
127
- return { kind: "noop" };
128
- if (ev.ctrl) {
129
- switch (ch) {
130
- case "a":
131
- state.buffer.moveHome();
132
- return { kind: "redraw" };
133
- case "e":
134
- state.buffer.moveEnd();
135
- return { kind: "redraw" };
136
- case "b":
137
- state.buffer.moveLeft();
138
- return { kind: "redraw" };
139
- case "f":
140
- state.buffer.moveRight();
141
- return { kind: "redraw" };
142
- case "p":
143
- return navigateHistory(state, -1);
144
- case "n":
145
- return navigateHistory(state, +1);
146
- case "k":
147
- state.buffer.killToEnd();
148
- return { kind: "redraw" };
149
- case "u":
150
- state.buffer.killToStart();
151
- return { kind: "redraw" };
152
- case "w":
153
- state.buffer.killWordLeft();
154
- return { kind: "redraw" };
155
- case "y": {
156
- const yanked = state.buffer.yank();
157
- state.lastYank = yanked ?? null;
158
- return yanked === undefined
159
- ? { kind: "bell" }
160
- : { kind: "redraw" };
161
- }
162
- case "c":
163
- return { kind: "cancel" };
164
- case "d":
165
- if (state.buffer.length === 0)
166
- return { kind: "eof" };
167
- state.buffer.deleteRight();
168
- return { kind: "redraw" };
169
- case "l":
170
- return { kind: "clear-screen" };
171
- case "h":
172
- state.buffer.deleteLeft();
173
- return { kind: "redraw" };
174
- case "r":
175
- return { kind: "search-start" };
176
- case "t": {
177
- // Transpose two chars before cursor. Edge cases per readline:
178
- // - at end-of-line, transpose the two chars before cursor
179
- // - at the very start with <2 chars, bell
180
- transpose(state.buffer);
181
- return { kind: "redraw" };
182
- }
183
- case "_":
184
- case "/": // some terminals send ^/ as 0x1f
185
- return state.buffer.undo()
186
- ? { kind: "redraw" }
187
- : { kind: "bell" };
188
- case "g":
189
- // ^G outside of search is a no-op bell.
190
- return { kind: "bell" };
191
- default:
192
- return { kind: "bell" };
193
- }
194
- }
195
- if (ev.meta) {
196
- switch (ch) {
197
- case "b":
198
- case "B":
199
- state.buffer.moveWordLeft();
200
- return { kind: "redraw" };
201
- case "f":
202
- case "F":
203
- state.buffer.moveWordRight();
204
- return { kind: "redraw" };
205
- case "d":
206
- case "D":
207
- state.buffer.killWordRight();
208
- return { kind: "redraw" };
209
- case "y":
210
- case "Y":
211
- if (state.lastYank === null)
212
- return { kind: "bell" };
213
- {
214
- const next = state.buffer.yankPop(state.lastYank);
215
- if (next === undefined)
216
- return { kind: "bell" };
217
- state.lastYank = next;
218
- }
219
- return { kind: "redraw" };
220
- default:
221
- return { kind: "bell" };
222
- }
223
- }
224
- // Plain printable.
225
- state.buffer.insert(ch);
226
- return { kind: "redraw" };
97
+ const ch = ev.char ?? "";
98
+ if (ch.length === 0) return { kind: "noop" };
99
+ if (ev.ctrl) switch (ch) {
100
+ case "a":
101
+ state.buffer.moveHome();
102
+ return { kind: "redraw" };
103
+ case "e":
104
+ state.buffer.moveEnd();
105
+ return { kind: "redraw" };
106
+ case "b":
107
+ state.buffer.moveLeft();
108
+ return { kind: "redraw" };
109
+ case "f":
110
+ state.buffer.moveRight();
111
+ return { kind: "redraw" };
112
+ case "p": return navigateHistory(state, -1);
113
+ case "n": return navigateHistory(state, 1);
114
+ case "k":
115
+ state.buffer.killToEnd();
116
+ return { kind: "redraw" };
117
+ case "u":
118
+ state.buffer.killToStart();
119
+ return { kind: "redraw" };
120
+ case "w":
121
+ state.buffer.killWordLeft();
122
+ return { kind: "redraw" };
123
+ case "y": {
124
+ const yanked = state.buffer.yank();
125
+ state.lastYank = yanked ?? null;
126
+ return yanked === void 0 ? { kind: "bell" } : { kind: "redraw" };
127
+ }
128
+ case "c": return { kind: "cancel" };
129
+ case "d":
130
+ if (state.buffer.length === 0) return { kind: "eof" };
131
+ state.buffer.deleteRight();
132
+ return { kind: "redraw" };
133
+ case "l": return { kind: "clear-screen" };
134
+ case "h":
135
+ state.buffer.deleteLeft();
136
+ return { kind: "redraw" };
137
+ case "r": return { kind: "search-start" };
138
+ case "t":
139
+ transpose(state.buffer);
140
+ return { kind: "redraw" };
141
+ case "_":
142
+ case "/": return state.buffer.undo() ? { kind: "redraw" } : { kind: "bell" };
143
+ case "g": return { kind: "bell" };
144
+ default: return { kind: "bell" };
145
+ }
146
+ if (ev.meta) switch (ch) {
147
+ case "b":
148
+ case "B":
149
+ state.buffer.moveWordLeft();
150
+ return { kind: "redraw" };
151
+ case "f":
152
+ case "F":
153
+ state.buffer.moveWordRight();
154
+ return { kind: "redraw" };
155
+ case "d":
156
+ case "D":
157
+ state.buffer.killWordRight();
158
+ return { kind: "redraw" };
159
+ case "y":
160
+ case "Y":
161
+ if (state.lastYank === null) return { kind: "bell" };
162
+ {
163
+ const next = state.buffer.yankPop(state.lastYank);
164
+ if (next === void 0) return { kind: "bell" };
165
+ state.lastYank = next;
166
+ }
167
+ return { kind: "redraw" };
168
+ default: return { kind: "bell" };
169
+ }
170
+ state.buffer.insert(ch);
171
+ return { kind: "redraw" };
227
172
  };
228
173
  /**
229
- * Move history index by delta and load the corresponding entry. Saves
230
- * the live line on the first upward step so the user can return to it.
231
- */
174
+ * Move history index by delta and load the corresponding entry. Saves
175
+ * the live line on the first upward step so the user can return to it.
176
+ */
232
177
  const navigateHistory = (state, delta) => {
233
- if (state.history.length === 0)
234
- return { kind: "bell" };
235
- if (state.historyIndex === -1) {
236
- if (delta < 0) {
237
- state.liveSnapshot = state.buffer.text;
238
- state.historyIndex = state.history.length - 1;
239
- state.buffer.setText(state.history[state.historyIndex]);
240
- return { kind: "redraw" };
241
- }
242
- return { kind: "bell" };
243
- }
244
- const next = state.historyIndex + delta;
245
- if (next < 0)
246
- return { kind: "bell" };
247
- if (next >= state.history.length) {
248
- // Stepped past newest: restore the live snapshot.
249
- state.historyIndex = -1;
250
- state.buffer.setText(state.liveSnapshot ?? "");
251
- state.liveSnapshot = null;
252
- return { kind: "redraw" };
253
- }
254
- state.historyIndex = next;
255
- state.buffer.setText(state.history[next]);
256
- return { kind: "redraw" };
178
+ if (state.history.length === 0) return { kind: "bell" };
179
+ if (state.historyIndex === -1) {
180
+ if (delta < 0) {
181
+ state.liveSnapshot = state.buffer.text;
182
+ state.historyIndex = state.history.length - 1;
183
+ state.buffer.setText(state.history[state.historyIndex]);
184
+ return { kind: "redraw" };
185
+ }
186
+ return { kind: "bell" };
187
+ }
188
+ const next = state.historyIndex + delta;
189
+ if (next < 0) return { kind: "bell" };
190
+ if (next >= state.history.length) {
191
+ state.historyIndex = -1;
192
+ state.buffer.setText(state.liveSnapshot ?? "");
193
+ state.liveSnapshot = null;
194
+ return { kind: "redraw" };
195
+ }
196
+ state.historyIndex = next;
197
+ state.buffer.setText(state.history[next]);
198
+ return { kind: "redraw" };
257
199
  };
258
- // ---------------------------------------------------------------------------
259
- // vi mode
260
- // ---------------------------------------------------------------------------
261
200
  /**
262
- * Vi insert-mode dispatch. Behaves mostly like emacs for editing primitives —
263
- * printable keys insert, backspace/delete/arrows work the same way — but Esc
264
- * switches into normal mode. Ctrl keys are intentionally NOT vi commands here
265
- * (^C is handled at the top of `dispatch`).
266
- */
201
+ * Vi insert-mode dispatch. Behaves mostly like emacs for editing primitives —
202
+ * printable keys insert, backspace/delete/arrows work the same way — but Esc
203
+ * switches into normal mode. Ctrl keys are intentionally NOT vi commands here
204
+ * (^C is handled at the top of `dispatch`).
205
+ */
267
206
  const dispatchViInsert = (state, ev) => {
268
- switch (ev.key) {
269
- case "paste-start":
270
- state.pasting = true;
271
- return { kind: "paste-start" };
272
- case "paste-end":
273
- state.pasting = false;
274
- return { kind: "paste-end" };
275
- case "enter":
276
- return { kind: "submit" };
277
- case "tab":
278
- return { kind: "complete" };
279
- case "escape":
280
- // Leave insert mode; vi convention: cursor steps left so it sits on the
281
- // last inserted char (unless we were already at column 0).
282
- state.mode = "normal";
283
- state.viPending = null;
284
- if (state.buffer.cursor > 0)
285
- state.buffer.moveLeft();
286
- return { kind: "redraw" };
287
- case "backspace":
288
- state.buffer.deleteLeft();
289
- return { kind: "redraw" };
290
- case "delete":
291
- state.buffer.deleteRight();
292
- return { kind: "redraw" };
293
- case "left":
294
- state.buffer.moveLeft();
295
- return { kind: "redraw" };
296
- case "right":
297
- state.buffer.moveRight();
298
- return { kind: "redraw" };
299
- case "up":
300
- return navigateHistory(state, -1);
301
- case "down":
302
- return navigateHistory(state, +1);
303
- case "home":
304
- state.buffer.moveHome();
305
- return { kind: "redraw" };
306
- case "end":
307
- state.buffer.moveEnd();
308
- return { kind: "redraw" };
309
- case "char": {
310
- const ch = ev.char ?? "";
311
- if (ch.length === 0)
312
- return { kind: "noop" };
313
- // ^D on empty buffer still acts as EOF in either vi mode.
314
- if (ev.ctrl && ch === "d" && state.buffer.length === 0) {
315
- return { kind: "eof" };
316
- }
317
- // Ignore other control combos in vi insert; just insert plain printables.
318
- if (ev.ctrl || ev.meta)
319
- return { kind: "noop" };
320
- state.buffer.insert(ch);
321
- return { kind: "redraw" };
322
- }
323
- case "pageup":
324
- case "pagedown":
325
- case "unknown":
326
- return { kind: "bell" };
327
- }
207
+ switch (ev.key) {
208
+ case "paste-start":
209
+ state.pasting = true;
210
+ return { kind: "paste-start" };
211
+ case "paste-end":
212
+ state.pasting = false;
213
+ return { kind: "paste-end" };
214
+ case "enter": return { kind: "submit" };
215
+ case "tab": return { kind: "complete" };
216
+ case "escape":
217
+ state.mode = "normal";
218
+ state.viPending = null;
219
+ if (state.buffer.cursor > 0) state.buffer.moveLeft();
220
+ return { kind: "redraw" };
221
+ case "backspace":
222
+ state.buffer.deleteLeft();
223
+ return { kind: "redraw" };
224
+ case "delete":
225
+ state.buffer.deleteRight();
226
+ return { kind: "redraw" };
227
+ case "left":
228
+ state.buffer.moveLeft();
229
+ return { kind: "redraw" };
230
+ case "right":
231
+ state.buffer.moveRight();
232
+ return { kind: "redraw" };
233
+ case "up": return navigateHistory(state, -1);
234
+ case "down": return navigateHistory(state, 1);
235
+ case "home":
236
+ state.buffer.moveHome();
237
+ return { kind: "redraw" };
238
+ case "end":
239
+ state.buffer.moveEnd();
240
+ return { kind: "redraw" };
241
+ case "char": {
242
+ const ch = ev.char ?? "";
243
+ if (ch.length === 0) return { kind: "noop" };
244
+ if (ev.ctrl && ch === "d" && state.buffer.length === 0) return { kind: "eof" };
245
+ if (ev.ctrl || ev.meta) return { kind: "noop" };
246
+ state.buffer.insert(ch);
247
+ return { kind: "redraw" };
248
+ }
249
+ case "pageup":
250
+ case "pagedown":
251
+ case "unknown": return { kind: "bell" };
252
+ }
328
253
  };
329
254
  /**
330
- * Vi normal-mode dispatch. Implements the core motion + edit subset documented
331
- * in the WP-24 plan: hjkl/0$^/bwe movement, x/X/dd/D/cc/cw/r/~ edits, i/a/I/A
332
- * to switch back to insert. Multi-key sequences (dd, cw, r<char>) are tracked
333
- * via `state.viPending`.
334
- */
255
+ * Vi normal-mode dispatch. Implements the core motion + edit subset documented
256
+ * in the WP-24 plan: hjkl/0$^/bwe movement, x/X/dd/D/cc/cw/r/~ edits, i/a/I/A
257
+ * to switch back to insert. Multi-key sequences (dd, cw, r<char>) are tracked
258
+ * via `state.viPending`.
259
+ */
335
260
  const dispatchViNormal = (state, ev) => {
336
- // Pending operator awaiting its next char.
337
- if (state.viPending !== null) {
338
- return continueViPending(state, ev);
339
- }
340
- switch (ev.key) {
341
- case "enter":
342
- return { kind: "submit" };
343
- case "tab":
344
- // No completion in normal mode (matches vim/readline-vi).
345
- return { kind: "bell" };
346
- case "escape":
347
- // Already normal; clear any half-formed operator.
348
- state.viPending = null;
349
- return { kind: "noop" };
350
- case "backspace":
351
- // In normal mode bare backspace is "move left" per readline-vi.
352
- state.buffer.moveLeft();
353
- return { kind: "redraw" };
354
- case "delete":
355
- state.buffer.deleteRight();
356
- return { kind: "redraw" };
357
- case "left":
358
- state.buffer.moveLeft();
359
- return { kind: "redraw" };
360
- case "right":
361
- state.buffer.moveRight();
362
- return { kind: "redraw" };
363
- case "up":
364
- return navigateHistory(state, -1);
365
- case "down":
366
- return navigateHistory(state, +1);
367
- case "home":
368
- state.buffer.moveHome();
369
- return { kind: "redraw" };
370
- case "end":
371
- state.buffer.moveEnd();
372
- return { kind: "redraw" };
373
- case "char":
374
- return handleViNormalChar(state, ev);
375
- case "paste-start":
376
- state.pasting = true;
377
- return { kind: "paste-start" };
378
- case "paste-end":
379
- state.pasting = false;
380
- return { kind: "paste-end" };
381
- case "pageup":
382
- case "pagedown":
383
- case "unknown":
384
- return { kind: "bell" };
385
- }
261
+ if (state.viPending !== null) return continueViPending(state, ev);
262
+ switch (ev.key) {
263
+ case "enter": return { kind: "submit" };
264
+ case "tab": return { kind: "bell" };
265
+ case "escape":
266
+ state.viPending = null;
267
+ return { kind: "noop" };
268
+ case "backspace":
269
+ state.buffer.moveLeft();
270
+ return { kind: "redraw" };
271
+ case "delete":
272
+ state.buffer.deleteRight();
273
+ return { kind: "redraw" };
274
+ case "left":
275
+ state.buffer.moveLeft();
276
+ return { kind: "redraw" };
277
+ case "right":
278
+ state.buffer.moveRight();
279
+ return { kind: "redraw" };
280
+ case "up": return navigateHistory(state, -1);
281
+ case "down": return navigateHistory(state, 1);
282
+ case "home":
283
+ state.buffer.moveHome();
284
+ return { kind: "redraw" };
285
+ case "end":
286
+ state.buffer.moveEnd();
287
+ return { kind: "redraw" };
288
+ case "char": return handleViNormalChar(state, ev);
289
+ case "paste-start":
290
+ state.pasting = true;
291
+ return { kind: "paste-start" };
292
+ case "paste-end":
293
+ state.pasting = false;
294
+ return { kind: "paste-end" };
295
+ case "pageup":
296
+ case "pagedown":
297
+ case "unknown": return { kind: "bell" };
298
+ }
386
299
  };
387
300
  const handleViNormalChar = (state, ev) => {
388
- const ch = ev.char ?? "";
389
- if (ch.length === 0)
390
- return { kind: "noop" };
391
- // ^D on empty buffer is EOF in vi normal mode too.
392
- if (ev.ctrl && ch === "d" && state.buffer.length === 0) {
393
- return { kind: "eof" };
394
- }
395
- // Other ctrl/meta sequences: not bound in normal mode → bell.
396
- if (ev.ctrl || ev.meta)
397
- return { kind: "bell" };
398
- switch (ch) {
399
- // Movement
400
- case "h":
401
- state.buffer.moveLeft();
402
- return { kind: "redraw" };
403
- case "l":
404
- state.buffer.moveRight();
405
- return { kind: "redraw" };
406
- case "b":
407
- state.buffer.moveWordLeft();
408
- return { kind: "redraw" };
409
- case "w":
410
- state.buffer.moveWordRight();
411
- return { kind: "redraw" };
412
- case "e":
413
- viMoveEndOfWord(state.buffer);
414
- return { kind: "redraw" };
415
- case "0":
416
- state.buffer.moveHome();
417
- return { kind: "redraw" };
418
- case "$":
419
- state.buffer.moveEnd();
420
- return { kind: "redraw" };
421
- case "^":
422
- viMoveFirstNonBlank(state.buffer);
423
- return { kind: "redraw" };
424
- // History (vi-style j/k).
425
- case "j":
426
- return navigateHistory(state, +1);
427
- case "k":
428
- return navigateHistory(state, -1);
429
- // Mode switches.
430
- case "i":
431
- state.mode = "insert";
432
- return { kind: "redraw" };
433
- case "a":
434
- if (state.buffer.cursor < state.buffer.length)
435
- state.buffer.moveRight();
436
- state.mode = "insert";
437
- return { kind: "redraw" };
438
- case "I":
439
- state.buffer.moveHome();
440
- state.mode = "insert";
441
- return { kind: "redraw" };
442
- case "A":
443
- state.buffer.moveEnd();
444
- state.mode = "insert";
445
- return { kind: "redraw" };
446
- // Edits.
447
- case "x":
448
- state.buffer.deleteRight();
449
- return { kind: "redraw" };
450
- case "X":
451
- state.buffer.deleteLeft();
452
- return { kind: "redraw" };
453
- case "D":
454
- state.buffer.killToEnd();
455
- return { kind: "redraw" };
456
- case "~":
457
- viToggleCaseAtCursor(state.buffer);
458
- return { kind: "redraw" };
459
- // Multi-key operators: wait for next char.
460
- case "r":
461
- state.viPending = "r";
462
- return { kind: "noop" };
463
- case "d":
464
- state.viPending = "d";
465
- return { kind: "noop" };
466
- case "c":
467
- state.viPending = "c";
468
- return { kind: "noop" };
469
- case "g":
470
- // Stub: only 'gg' (go to first history) might be desirable; for now,
471
- // just consume the prefix and bell on the follow-up.
472
- state.viPending = "g";
473
- return { kind: "noop" };
474
- case ":":
475
- // Enter ex-prompt mode. The renderer draws a `:` line; printable keys
476
- // accumulate in `exBuffer`; Enter executes; Esc returns to normal.
477
- state.mode = "ex";
478
- state.exBuffer = "";
479
- return { kind: "ex-update" };
480
- default:
481
- return { kind: "bell" };
482
- }
301
+ const ch = ev.char ?? "";
302
+ if (ch.length === 0) return { kind: "noop" };
303
+ if (ev.ctrl && ch === "d" && state.buffer.length === 0) return { kind: "eof" };
304
+ if (ev.ctrl || ev.meta) return { kind: "bell" };
305
+ switch (ch) {
306
+ case "h":
307
+ state.buffer.moveLeft();
308
+ return { kind: "redraw" };
309
+ case "l":
310
+ state.buffer.moveRight();
311
+ return { kind: "redraw" };
312
+ case "b":
313
+ state.buffer.moveWordLeft();
314
+ return { kind: "redraw" };
315
+ case "w":
316
+ state.buffer.moveWordRight();
317
+ return { kind: "redraw" };
318
+ case "e":
319
+ viMoveEndOfWord(state.buffer);
320
+ return { kind: "redraw" };
321
+ case "0":
322
+ state.buffer.moveHome();
323
+ return { kind: "redraw" };
324
+ case "$":
325
+ state.buffer.moveEnd();
326
+ return { kind: "redraw" };
327
+ case "^":
328
+ viMoveFirstNonBlank(state.buffer);
329
+ return { kind: "redraw" };
330
+ case "j": return navigateHistory(state, 1);
331
+ case "k": return navigateHistory(state, -1);
332
+ case "i":
333
+ state.mode = "insert";
334
+ return { kind: "redraw" };
335
+ case "a":
336
+ if (state.buffer.cursor < state.buffer.length) state.buffer.moveRight();
337
+ state.mode = "insert";
338
+ return { kind: "redraw" };
339
+ case "I":
340
+ state.buffer.moveHome();
341
+ state.mode = "insert";
342
+ return { kind: "redraw" };
343
+ case "A":
344
+ state.buffer.moveEnd();
345
+ state.mode = "insert";
346
+ return { kind: "redraw" };
347
+ case "x":
348
+ state.buffer.deleteRight();
349
+ return { kind: "redraw" };
350
+ case "X":
351
+ state.buffer.deleteLeft();
352
+ return { kind: "redraw" };
353
+ case "D":
354
+ state.buffer.killToEnd();
355
+ return { kind: "redraw" };
356
+ case "~":
357
+ viToggleCaseAtCursor(state.buffer);
358
+ return { kind: "redraw" };
359
+ case "r":
360
+ state.viPending = "r";
361
+ return { kind: "noop" };
362
+ case "d":
363
+ state.viPending = "d";
364
+ return { kind: "noop" };
365
+ case "c":
366
+ state.viPending = "c";
367
+ return { kind: "noop" };
368
+ case "g":
369
+ state.viPending = "g";
370
+ return { kind: "noop" };
371
+ case ":":
372
+ state.mode = "ex";
373
+ state.exBuffer = "";
374
+ return { kind: "ex-update" };
375
+ default: return { kind: "bell" };
376
+ }
483
377
  };
484
378
  /**
485
- * Vi ex-prompt dispatch (`:`-line). Mirrors a tiny slice of vim's ex grammar:
486
- *
487
- * - `q`, `quit`, `q!` — abort the current readLine (same as ^C).
488
- * - `w` (no arg) — bell + ignore (we don't have a file to write to).
489
- * - Esc — abort ex; return to normal mode.
490
- * - Enter — execute the accumulated command.
491
- *
492
- * Printable characters extend `state.exBuffer`; Backspace shrinks it. Anything
493
- * else bells. The renderer is responsible for drawing `: ` + exBuffer on its
494
- * own row; we expose `ex-update` actions so it knows when to redraw.
495
- */
379
+ * Vi ex-prompt dispatch (`:`-line). Mirrors a tiny slice of vim's ex grammar:
380
+ *
381
+ * - `q`, `quit`, `q!` — abort the current readLine (same as ^C).
382
+ * - `w` (no arg) — bell + ignore (we don't have a file to write to).
383
+ * - Esc — abort ex; return to normal mode.
384
+ * - Enter — execute the accumulated command.
385
+ *
386
+ * Printable characters extend `state.exBuffer`; Backspace shrinks it. Anything
387
+ * else bells. The renderer is responsible for drawing `: ` + exBuffer on its
388
+ * own row; we expose `ex-update` actions so it knows when to redraw.
389
+ */
496
390
  const dispatchViEx = (state, ev) => {
497
- switch (ev.key) {
498
- case "escape":
499
- // Abort ex; back to normal without executing anything.
500
- state.mode = "normal";
501
- state.exBuffer = "";
502
- return { kind: "redraw" };
503
- case "enter":
504
- return executeExCommand(state);
505
- case "backspace":
506
- if (state.exBuffer.length === 0) {
507
- // Backspace through the implicit `:` returns to normal mode (matches
508
- // vim's "backspace at column 1 of ex line").
509
- state.mode = "normal";
510
- return { kind: "redraw" };
511
- }
512
- state.exBuffer = state.exBuffer.slice(0, -1);
513
- return { kind: "ex-update" };
514
- case "char": {
515
- const ch = ev.char ?? "";
516
- // Ctrl/Meta combos aren't bound in ex.
517
- if (ch.length === 0 || ev.ctrl || ev.meta)
518
- return { kind: "bell" };
519
- state.exBuffer += ch;
520
- return { kind: "ex-update" };
521
- }
522
- case "paste-start":
523
- case "paste-end":
524
- case "tab":
525
- case "delete":
526
- case "left":
527
- case "right":
528
- case "up":
529
- case "down":
530
- case "home":
531
- case "end":
532
- case "pageup":
533
- case "pagedown":
534
- case "unknown":
535
- return { kind: "bell" };
536
- }
391
+ switch (ev.key) {
392
+ case "escape":
393
+ state.mode = "normal";
394
+ state.exBuffer = "";
395
+ return { kind: "redraw" };
396
+ case "enter": return executeExCommand(state);
397
+ case "backspace":
398
+ if (state.exBuffer.length === 0) {
399
+ state.mode = "normal";
400
+ return { kind: "redraw" };
401
+ }
402
+ state.exBuffer = state.exBuffer.slice(0, -1);
403
+ return { kind: "ex-update" };
404
+ case "char": {
405
+ const ch = ev.char ?? "";
406
+ if (ch.length === 0 || ev.ctrl || ev.meta) return { kind: "bell" };
407
+ state.exBuffer += ch;
408
+ return { kind: "ex-update" };
409
+ }
410
+ case "paste-start":
411
+ case "paste-end":
412
+ case "tab":
413
+ case "delete":
414
+ case "left":
415
+ case "right":
416
+ case "up":
417
+ case "down":
418
+ case "home":
419
+ case "end":
420
+ case "pageup":
421
+ case "pagedown":
422
+ case "unknown": return { kind: "bell" };
423
+ }
537
424
  };
538
425
  /**
539
- * Interpret the accumulated ex buffer. Returns the action the driver should
540
- * apply (cancel = abort current readLine; bell = unknown command; redraw =
541
- * fall back to normal mode without side effects).
542
- */
426
+ * Interpret the accumulated ex buffer. Returns the action the driver should
427
+ * apply (cancel = abort current readLine; bell = unknown command; redraw =
428
+ * fall back to normal mode without side effects).
429
+ */
543
430
  const executeExCommand = (state) => {
544
- const cmd = state.exBuffer.trim();
545
- // Always leave ex mode after Enter, even on unknown commands.
546
- state.exBuffer = "";
547
- state.mode = "normal";
548
- switch (cmd) {
549
- case "q":
550
- case "q!":
551
- case "quit":
552
- // Abort the readLine. Same outcome as ^C — driver throws SignalError.
553
- return { kind: "cancel" };
554
- case "w":
555
- // We don't have a file to write to; bell and return to normal.
556
- return { kind: "bell" };
557
- case "":
558
- // Bare `:` then Enter — just return to normal silently.
559
- return { kind: "redraw" };
560
- default:
561
- return { kind: "bell" };
562
- }
431
+ const cmd = state.exBuffer.trim();
432
+ state.exBuffer = "";
433
+ state.mode = "normal";
434
+ switch (cmd) {
435
+ case "q":
436
+ case "q!":
437
+ case "quit": return { kind: "cancel" };
438
+ case "w": return { kind: "bell" };
439
+ case "": return { kind: "redraw" };
440
+ default: return { kind: "bell" };
441
+ }
563
442
  };
564
443
  /**
565
- * Resolve a pending vi operator using the next key event. `r<char>` replaces
566
- * one char; `dd` / `cc` operate on the whole line; `dw` / `cw` operate on a
567
- * word; anything else bells.
568
- */
444
+ * Resolve a pending vi operator using the next key event. `r<char>` replaces
445
+ * one char; `dd` / `cc` operate on the whole line; `dw` / `cw` operate on a
446
+ * word; anything else bells.
447
+ */
569
448
  const continueViPending = (state, ev) => {
570
- const pending = state.viPending;
571
- state.viPending = null;
572
- // Escape cancels a pending operator without bell (matches vi convention).
573
- if (ev.key === "escape")
574
- return { kind: "noop" };
575
- if (pending === "r") {
576
- // r<char> replaces one char at cursor with <char>.
577
- if (ev.key !== "char" || ev.char === undefined || ev.ctrl || ev.meta) {
578
- return { kind: "bell" };
579
- }
580
- if (state.buffer.cursor >= state.buffer.length)
581
- return { kind: "bell" };
582
- viReplaceCharAtCursor(state.buffer, ev.char);
583
- return { kind: "redraw" };
584
- }
585
- if (pending === "g") {
586
- // Only 'gg' is recognised; we don't actually implement first-history yet.
587
- return { kind: "bell" };
588
- }
589
- // dd / cc / dw / cw all key off ev.char.
590
- if (ev.key !== "char" || ev.char === undefined || ev.ctrl || ev.meta) {
591
- return { kind: "bell" };
592
- }
593
- const c = ev.char;
594
- if (pending === "d") {
595
- if (c === "d") {
596
- // dd: kill whole line.
597
- state.buffer.moveHome();
598
- state.buffer.killToEnd();
599
- return { kind: "redraw" };
600
- }
601
- if (c === "w") {
602
- state.buffer.killWordRight();
603
- return { kind: "redraw" };
604
- }
605
- if (c === "b") {
606
- state.buffer.killWordLeft();
607
- return { kind: "redraw" };
608
- }
609
- if (c === "$") {
610
- state.buffer.killToEnd();
611
- return { kind: "redraw" };
612
- }
613
- if (c === "0") {
614
- state.buffer.killToStart();
615
- return { kind: "redraw" };
616
- }
617
- return { kind: "bell" };
618
- }
619
- if (pending === "c") {
620
- if (c === "c") {
621
- // cc: kill whole line, enter insert.
622
- state.buffer.moveHome();
623
- state.buffer.killToEnd();
624
- state.mode = "insert";
625
- return { kind: "redraw" };
626
- }
627
- if (c === "w") {
628
- state.buffer.killWordRight();
629
- state.mode = "insert";
630
- return { kind: "redraw" };
631
- }
632
- if (c === "b") {
633
- state.buffer.killWordLeft();
634
- state.mode = "insert";
635
- return { kind: "redraw" };
636
- }
637
- if (c === "$") {
638
- state.buffer.killToEnd();
639
- state.mode = "insert";
640
- return { kind: "redraw" };
641
- }
642
- if (c === "0") {
643
- state.buffer.killToStart();
644
- state.mode = "insert";
645
- return { kind: "redraw" };
646
- }
647
- return { kind: "bell" };
648
- }
649
- return { kind: "bell" };
449
+ const pending = state.viPending;
450
+ state.viPending = null;
451
+ if (ev.key === "escape") return { kind: "noop" };
452
+ if (pending === "r") {
453
+ if (ev.key !== "char" || ev.char === void 0 || ev.ctrl || ev.meta) return { kind: "bell" };
454
+ if (state.buffer.cursor >= state.buffer.length) return { kind: "bell" };
455
+ viReplaceCharAtCursor(state.buffer, ev.char);
456
+ return { kind: "redraw" };
457
+ }
458
+ if (pending === "g") return { kind: "bell" };
459
+ if (ev.key !== "char" || ev.char === void 0 || ev.ctrl || ev.meta) return { kind: "bell" };
460
+ const c = ev.char;
461
+ if (pending === "d") {
462
+ if (c === "d") {
463
+ state.buffer.moveHome();
464
+ state.buffer.killToEnd();
465
+ return { kind: "redraw" };
466
+ }
467
+ if (c === "w") {
468
+ state.buffer.killWordRight();
469
+ return { kind: "redraw" };
470
+ }
471
+ if (c === "b") {
472
+ state.buffer.killWordLeft();
473
+ return { kind: "redraw" };
474
+ }
475
+ if (c === "$") {
476
+ state.buffer.killToEnd();
477
+ return { kind: "redraw" };
478
+ }
479
+ if (c === "0") {
480
+ state.buffer.killToStart();
481
+ return { kind: "redraw" };
482
+ }
483
+ return { kind: "bell" };
484
+ }
485
+ if (pending === "c") {
486
+ if (c === "c") {
487
+ state.buffer.moveHome();
488
+ state.buffer.killToEnd();
489
+ state.mode = "insert";
490
+ return { kind: "redraw" };
491
+ }
492
+ if (c === "w") {
493
+ state.buffer.killWordRight();
494
+ state.mode = "insert";
495
+ return { kind: "redraw" };
496
+ }
497
+ if (c === "b") {
498
+ state.buffer.killWordLeft();
499
+ state.mode = "insert";
500
+ return { kind: "redraw" };
501
+ }
502
+ if (c === "$") {
503
+ state.buffer.killToEnd();
504
+ state.mode = "insert";
505
+ return { kind: "redraw" };
506
+ }
507
+ if (c === "0") {
508
+ state.buffer.killToStart();
509
+ state.mode = "insert";
510
+ return { kind: "redraw" };
511
+ }
512
+ return { kind: "bell" };
513
+ }
514
+ return { kind: "bell" };
650
515
  };
651
516
  /** `e` motion: jump to the end of the current word (or the next word). */
652
517
  const viMoveEndOfWord = (buf) => {
653
- const text = buf.text;
654
- const cps = Array.from(text);
655
- let i = buf.cursor;
656
- // If we're between/inside a word, move to the last char of the word that
657
- // ends at or after cursor.
658
- // Step 1: if cursor is at a non-word, skip to the next word's first char.
659
- while (i < cps.length && !isWordChar(cps[i]))
660
- i++;
661
- // Step 2: if we landed inside a word, advance to its last char (one before
662
- // the next non-word boundary).
663
- while (i < cps.length - 1 && isWordChar(cps[i + 1]))
664
- i++;
665
- // Vi's `e` leaves the cursor ON the last char, not past it. Our buffer uses
666
- // cursor "between code points", so put it after that last char.
667
- if (i < cps.length)
668
- i++;
669
- buf.setText(text, i);
518
+ const text = buf.text;
519
+ const cps = Array.from(text);
520
+ let i = buf.cursor;
521
+ while (i < cps.length && !isWordChar(cps[i])) i++;
522
+ while (i < cps.length - 1 && isWordChar(cps[i + 1])) i++;
523
+ if (i < cps.length) i++;
524
+ buf.setText(text, i);
670
525
  };
671
526
  /** `^` motion: first non-blank char on the line. */
672
527
  const viMoveFirstNonBlank = (buf) => {
673
- const text = buf.text;
674
- const cps = Array.from(text);
675
- let i = 0;
676
- while (i < cps.length && (cps[i] === " " || cps[i] === "\t"))
677
- i++;
678
- buf.setText(text, i);
528
+ const text = buf.text;
529
+ const cps = Array.from(text);
530
+ let i = 0;
531
+ while (i < cps.length && (cps[i] === " " || cps[i] === " ")) i++;
532
+ buf.setText(text, i);
679
533
  };
680
534
  /** `~` toggles the case of the character at the cursor; advances cursor. */
681
535
  const viToggleCaseAtCursor = (buf) => {
682
- const text = buf.text;
683
- const cps = Array.from(text);
684
- const i = buf.cursor;
685
- if (i >= cps.length)
686
- return;
687
- const ch = cps[i];
688
- const flipped = ch === ch.toLowerCase() ? ch.toUpperCase() : ch.toLowerCase();
689
- if (flipped === ch) {
690
- // Non-alpha: just advance.
691
- buf.setText(text, Math.min(i + 1, cps.length));
692
- return;
693
- }
694
- buf.pushUndo();
695
- cps[i] = flipped;
696
- buf.setText(cps.join(""), Math.min(i + 1, cps.length));
536
+ const text = buf.text;
537
+ const cps = Array.from(text);
538
+ const i = buf.cursor;
539
+ if (i >= cps.length) return;
540
+ const ch = cps[i];
541
+ const flipped = ch === ch.toLowerCase() ? ch.toUpperCase() : ch.toLowerCase();
542
+ if (flipped === ch) {
543
+ buf.setText(text, Math.min(i + 1, cps.length));
544
+ return;
545
+ }
546
+ buf.pushUndo();
547
+ cps[i] = flipped;
548
+ buf.setText(cps.join(""), Math.min(i + 1, cps.length));
697
549
  };
698
550
  /** `r<char>` replaces the character at cursor and leaves cursor in place. */
699
551
  const viReplaceCharAtCursor = (buf, ch) => {
700
- const text = buf.text;
701
- const cps = Array.from(text);
702
- const i = buf.cursor;
703
- if (i >= cps.length)
704
- return;
705
- buf.pushUndo();
706
- cps[i] = ch;
707
- buf.setText(cps.join(""), i);
552
+ const text = buf.text;
553
+ const cps = Array.from(text);
554
+ const i = buf.cursor;
555
+ if (i >= cps.length) return;
556
+ buf.pushUndo();
557
+ cps[i] = ch;
558
+ buf.setText(cps.join(""), i);
708
559
  };
709
- // Re-export for vi helpers that need word classification (matches buffer.ts).
710
560
  const isWordChar = (ch) => {
711
- if (ch.length === 0)
712
- return false;
713
- const cp = ch.codePointAt(0) ?? 0;
714
- if (cp >= 0x30 && cp <= 0x39)
715
- return true; // 0-9
716
- if (cp >= 0x41 && cp <= 0x5a)
717
- return true; // A-Z
718
- if (cp >= 0x61 && cp <= 0x7a)
719
- return true; // a-z
720
- if (cp === 0x5f)
721
- return true; // _
722
- if (cp > 0x7f)
723
- return cp >= 0xa0;
724
- return false;
561
+ if (ch.length === 0) return false;
562
+ const cp = ch.codePointAt(0) ?? 0;
563
+ if (cp >= 48 && cp <= 57) return true;
564
+ if (cp >= 65 && cp <= 90) return true;
565
+ if (cp >= 97 && cp <= 122) return true;
566
+ if (cp === 95) return true;
567
+ if (cp > 127) return cp >= 160;
568
+ return false;
725
569
  };
726
570
  /** ^T: swap the character before the cursor with the one before it. */
727
571
  const transpose = (buf) => {
728
- const len = buf.length;
729
- if (len < 2)
730
- return;
731
- const text = buf.text;
732
- const cps = Array.from(text);
733
- let i = buf.cursor;
734
- if (i === 0)
735
- return;
736
- if (i === len)
737
- i--; // at EOL: act on the last two chars
738
- if (i < 1)
739
- return;
740
- buf.pushUndo();
741
- const tmp = cps[i - 1];
742
- cps[i - 1] = cps[i];
743
- cps[i] = tmp;
744
- buf.setText(cps.join(""), Math.min(i + 1, len));
572
+ const len = buf.length;
573
+ if (len < 2) return;
574
+ const text = buf.text;
575
+ const cps = Array.from(text);
576
+ let i = buf.cursor;
577
+ if (i === 0) return;
578
+ if (i === len) i--;
579
+ if (i < 1) return;
580
+ buf.pushUndo();
581
+ const tmp = cps[i - 1];
582
+ cps[i - 1] = cps[i];
583
+ cps[i] = tmp;
584
+ buf.setText(cps.join(""), Math.min(i + 1, len));
745
585
  };
586
+ //#endregion
587
+ export { dispatch, makeState };