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