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,893 +1,676 @@
1
- /**
2
- * Public-facing line editor.
3
- *
4
- * Glues the streaming VT100 decoder, the keymap, the line buffer, and
5
- * the completion engine into a `readLine(prompt)` Promise-returning API.
6
- *
7
- * Architecture
8
- * ------------
9
- *
10
- * stdin (raw mode)
11
- * │ bytes
12
- * ▼
13
- * Vt100Decoder ─── KeyEvent[]
14
- * │
15
- * ▼
16
- * dispatch(state, ev) ─── EditorAction
17
- * │
18
- * ▼
19
- * render(prompt, state, stdout) (CSI writes)
20
- * │
21
- * └── on submit: resolve readLine() with state.buffer.text
22
- * └── on ^C: reject with SignalError
23
- * └── on ^D: resolve with EOF symbol
24
- *
25
- * Rendering strategy
26
- * ------------------
27
- *
28
- * Naive but robust: track the number of rows last drawn, on every
29
- * redraw move the cursor up to the prompt's anchor row and rewrite the
30
- * whole `prompt + buffer.text` block. This avoids per-keystroke diffing
31
- * bugs at the cost of a few extra bytes per keystroke. For the
32
- * ~80-char lines users actually type interactively this is invisible.
33
- *
34
- * Wrapping uses the inlined `displayWidth` (port of WP-09's table) so
35
- * East-Asian wide characters and zero-width combining marks render
36
- * correctly. We never call `stdout.write` with embedded `\n`; line
37
- * breaks come from explicit `\r\n` only on submit or when we need to
38
- * display multi-row output (paste-mode newlines, candidate listings).
39
- */
40
1
  import { LineBuffer } from "./buffer.js";
41
- import { CompletionState, formatCandidates, } from "./complete.js";
42
- import { dispatch, makeState, } from "./keymap.js";
43
- import { BEL, CR, csiClearScreen, csiDown, csiEraseToEol, csiLeft, csiRight, csiUp, disableBracketedPaste, enableBracketedPaste, LF, Vt100Decoder, } from "./vt100.js";
2
+ import { CompletionState, formatCandidates } from "./complete.js";
3
+ import { dispatch, makeState } from "./keymap.js";
4
+ import { Vt100Decoder, csiClearScreen, csiDown, csiEraseToEol, csiRight, csiUp, disableBracketedPaste, enableBracketedPaste } from "./vt100.js";
5
+ //#region src/psql/io/lineEditor/index.ts
6
+ /**
7
+ * Public-facing line editor.
8
+ *
9
+ * Glues the streaming VT100 decoder, the keymap, the line buffer, and
10
+ * the completion engine into a `readLine(prompt)` Promise-returning API.
11
+ *
12
+ * Architecture
13
+ * ------------
14
+ *
15
+ * stdin (raw mode)
16
+ * │ bytes
17
+ * ▼
18
+ * Vt100Decoder ─── KeyEvent[]
19
+ * │
20
+ * ▼
21
+ * dispatch(state, ev) ─── EditorAction
22
+ * │
23
+ * ▼
24
+ * render(prompt, state, stdout) (CSI writes)
25
+ * │
26
+ * └── on submit: resolve readLine() with state.buffer.text
27
+ * └── on ^C: reject with SignalError
28
+ * └── on ^D: resolve with EOF symbol
29
+ *
30
+ * Rendering strategy
31
+ * ------------------
32
+ *
33
+ * Naive but robust: track the number of rows last drawn, on every
34
+ * redraw move the cursor up to the prompt's anchor row and rewrite the
35
+ * whole `prompt + buffer.text` block. This avoids per-keystroke diffing
36
+ * bugs at the cost of a few extra bytes per keystroke. For the
37
+ * ~80-char lines users actually type interactively this is invisible.
38
+ *
39
+ * Wrapping uses the inlined `displayWidth` (port of WP-09's table) so
40
+ * East-Asian wide characters and zero-width combining marks render
41
+ * correctly. We never call `stdout.write` with embedded `\n`; line
42
+ * breaks come from explicit `\r\n` only on submit or when we need to
43
+ * display multi-row output (paste-mode newlines, candidate listings).
44
+ */
44
45
  /** Thrown when ^C cancels the current line. */
45
- export class SignalError extends Error {
46
- constructor() {
47
- super("SIGINT");
48
- this.name = "SignalError";
49
- this.signal = "SIGINT";
50
- }
51
- }
46
+ var SignalError = class extends Error {
47
+ constructor() {
48
+ super("SIGINT");
49
+ this.name = "SignalError";
50
+ this.signal = "SIGINT";
51
+ }
52
+ };
52
53
  /**
53
- * Sentinel for "Ctrl-D on an empty line". Compared with `===` by callers
54
- * so we don't accidentally match a literal string.
55
- */
54
+ * Sentinel for "Ctrl-D on an empty line". Compared with `===` by callers
55
+ * so we don't accidentally match a literal string.
56
+ */
56
57
  const EOF_SYMBOL = Symbol("LineEditor.EOF");
57
- export class LineEditor {
58
- constructor(opts = {}) {
59
- this.EOF = EOF_SYMBOL;
60
- /**
61
- * Pending mode change requested via `setMode()`. Applied at the next
62
- * `readLine()` boundary so an in-flight line keeps its current dispatch
63
- * (mirrors upstream readline's "next prompt" semantics for `set
64
- * editing-mode`).
65
- */
66
- this.pendingMode = null;
67
- this.completion = new CompletionState();
68
- /** Event queue; processed serially so async completion blocks subsequent keys. */
69
- this.eventQueue = [];
70
- this.processing = false;
71
- /** Active readLine, if any. */
72
- this.active = null;
73
- /** Listeners attached to stdin while readLine is active. */
74
- this.dataListener = null;
75
- this.wasRaw = false;
76
- /** TTY state restoration handlers. */
77
- this.exitListener = null;
78
- this.stdin = opts.stdin ?? process.stdin;
79
- this.stdout = opts.stdout ?? process.stdout;
80
- this.bracketedPaste = opts.bracketedPaste ?? true;
81
- this.completer = opts.completer;
82
- this.mode = opts.mode ?? "emacs";
83
- this.state = makeState(opts.history ?? [], this.mode === "vi" ? "insert" : "emacs");
84
- this.decoder = new Vt100Decoder({
85
- // LineEditor default: 50ms matches GNU readline's `keyseq-timeout`
86
- // default — enough to disambiguate Alt-X across all modern terminals
87
- // without making bare Esc noticeably laggy. Callers can override (or
88
- // set 0 to restore the legacy "emit Esc immediately" behaviour).
89
- escTimeoutMs: opts.escTimeoutMs ?? 50,
90
- onTimeoutEvent: (ev) => {
91
- this.handleDecoderTimeout(ev);
92
- },
93
- });
94
- }
95
- /** Read one line. Resolves on Enter; rejects on Ctrl-C. */
96
- readLine(prompt) {
97
- if (this.active !== null) {
98
- return Promise.reject(new Error("LineEditor.readLine called re-entrantly"));
99
- }
100
- // Apply any pending setMode() request at this readLine boundary so an
101
- // in-flight line keeps its prior dispatch.
102
- if (this.pendingMode !== null) {
103
- this.mode = this.pendingMode;
104
- this.pendingMode = null;
105
- }
106
- return new Promise((resolve, reject) => {
107
- // Fresh buffer for the new prompt.
108
- this.state.buffer = new LineBuffer();
109
- this.state.historyIndex = -1;
110
- this.state.liveSnapshot = null;
111
- // Switch state.mode to match the editor mode: vi → start in 'insert',
112
- // emacs → 'emacs'. This must happen on every readLine so a mid-session
113
- // VI_MODE flip is observed (the state machine has its own per-line
114
- // mode field that needs to be reset).
115
- if (this.mode === "vi") {
116
- this.state.mode = "insert";
117
- this.state.viPending = null;
118
- }
119
- else {
120
- this.state.mode = "emacs";
121
- this.state.viPending = null;
122
- this.state.exBuffer = "";
123
- }
124
- this.completion.reset();
125
- this.active = {
126
- prompt,
127
- resolve,
128
- reject,
129
- rowsDrawn: 0,
130
- cursorRow: 0,
131
- cursorCol: 0,
132
- search: null,
133
- listingRowsDrawn: 0,
134
- };
135
- try {
136
- this.enterRaw();
137
- }
138
- catch (err) {
139
- this.active = null;
140
- reject(err);
141
- return;
142
- }
143
- this.render();
144
- // If a previous readLine submitted mid-chunk and left residual key
145
- // events queued (e.g. the user pasted `analyze (\n\t\t` — after the
146
- // `\n` triggered submit, the `\t\t` was already decoded but sat in
147
- // `eventQueue` because `active` had been nulled), drain them now
148
- // against the new line buffer. Mirrors readline's natural behaviour
149
- // of carrying buffered input across the line boundary.
150
- if (this.eventQueue.length > 0)
151
- void this.drainQueue();
152
- });
153
- }
154
- /** Force redraw (call from SIGWINCH handler). */
155
- redraw() {
156
- if (this.active !== null)
157
- this.render(true);
158
- }
159
- /**
160
- * Inject an out-of-band line into the terminal while a prompt is being
161
- * edited. Used by callers that produce async output (NOTIFY messages,
162
- * notices, etc.) that would otherwise clobber the prompt rendering.
163
- *
164
- * Behaviour:
165
- * - No active readLine: pass-through to stdout.
166
- * - Active readLine: move cursor to end-of-block, write a fresh newline
167
- * so the injected text starts on its own row, write the text, then
168
- * redraw the prompt + buffer below it (re-attaching the cursor).
169
- *
170
- * The injected text should NOT have a trailing newline — we add one as
171
- * part of the move-to-end + LF dance. If the caller's payload already
172
- * ends with `\n`, we strip it once.
173
- */
174
- interject(text) {
175
- if (this.active === null) {
176
- this.stdout.write(text);
177
- return;
178
- }
179
- const a = this.active;
180
- this.moveCursorToEnd();
181
- this.stdout.write(LF);
182
- const body = text.endsWith("\n") ? text.slice(0, -1) : text;
183
- this.stdout.write(body);
184
- this.stdout.write(LF);
185
- // Reset drawn-state so render() lays out a fresh block under the
186
- // injected text instead of trying to overwrite the area we just used.
187
- a.rowsDrawn = 0;
188
- a.cursorRow = 0;
189
- a.cursorCol = 0;
190
- // Interjected text overwrites any candidate listing that was on screen.
191
- a.listingRowsDrawn = 0;
192
- this.render(true);
193
- }
194
- /** Cleanup raw mode and restore TTY. Idempotent. */
195
- close() {
196
- this.exitRaw();
197
- }
198
- /**
199
- * Request a switch between vi and emacs editing modes. The change takes
200
- * effect at the NEXT `readLine()` boundary, not retroactively on the line
201
- * currently being edited — matching upstream readline's `set editing-mode`
202
- * semantics. Calling this with the currently-active mode is a no-op.
203
- */
204
- setMode(mode) {
205
- this.pendingMode = mode;
206
- }
207
- /** Read the editor's currently effective mode. Exposed for tests. */
208
- getMode() {
209
- return this.mode;
210
- }
211
- /** Push a line into the in-memory history. */
212
- pushHistory(line) {
213
- if (line.length === 0)
214
- return;
215
- const last = this.state.history[this.state.history.length - 1];
216
- if (last === line)
217
- return;
218
- this.state.history.push(line);
219
- }
220
- /** Replace the in-memory history list. */
221
- setHistory(lines) {
222
- this.state.history = lines.slice();
223
- this.state.historyIndex = -1;
224
- this.state.liveSnapshot = null;
225
- }
226
- // -------------------------------------------------------------------------
227
- // I/O wiring
228
- // -------------------------------------------------------------------------
229
- enterRaw() {
230
- const s = this.stdin;
231
- if (isTtyReadStream(s)) {
232
- this.wasRaw = Boolean(s.isRaw);
233
- s.setRawMode(true);
234
- }
235
- s.resume();
236
- this.decoder.reset();
237
- if (this.bracketedPaste)
238
- this.stdout.write(enableBracketedPaste());
239
- this.dataListener = (chunk) => {
240
- this.handleChunk(chunk);
241
- };
242
- s.on("data", this.dataListener);
243
- if (!this.exitListener) {
244
- this.exitListener = () => {
245
- this.exitRaw();
246
- };
247
- process.once("exit", this.exitListener);
248
- process.once("SIGTERM", this.exitListener);
249
- }
250
- }
251
- exitRaw() {
252
- const s = this.stdin;
253
- if (this.dataListener !== null) {
254
- s.off("data", this.dataListener);
255
- this.dataListener = null;
256
- }
257
- if (isTtyReadStream(s) && !this.wasRaw) {
258
- try {
259
- s.setRawMode(false);
260
- }
261
- catch {
262
- /* ignore */
263
- }
264
- }
265
- if (this.bracketedPaste) {
266
- try {
267
- this.stdout.write(disableBracketedPaste());
268
- }
269
- catch {
270
- /* ignore */
271
- }
272
- }
273
- if (this.exitListener !== null) {
274
- process.off("exit", this.exitListener);
275
- process.off("SIGTERM", this.exitListener);
276
- this.exitListener = null;
277
- }
278
- }
279
- // -------------------------------------------------------------------------
280
- // Chunk processing
281
- // -------------------------------------------------------------------------
282
- handleChunk(chunk) {
283
- if (this.active === null)
284
- return;
285
- const events = this.decoder.push(new Uint8Array(chunk));
286
- for (const ev of events)
287
- this.eventQueue.push(ev);
288
- void this.drainQueue();
289
- }
290
- /** Called when the decoder's bare-Esc timer fires with a buffered event. */
291
- handleDecoderTimeout(ev) {
292
- if (this.active === null)
293
- return;
294
- this.eventQueue.push(ev);
295
- void this.drainQueue();
296
- }
297
- /**
298
- * Serially drain the event queue. Each event may kick off async work
299
- * (notably Tab completion); we await it before processing the next
300
- * event so keystrokes don't race ahead of pending completion results.
301
- */
302
- async drainQueue() {
303
- if (this.processing)
304
- return;
305
- this.processing = true;
306
- try {
307
- while (this.eventQueue.length > 0 && this.active !== null) {
308
- const ev = this.eventQueue.shift();
309
- if (ev === undefined)
310
- break;
311
- await this.handleEvent(ev);
312
- }
313
- }
314
- finally {
315
- this.processing = false;
316
- }
317
- }
318
- async handleEvent(ev) {
319
- if (this.active === null)
320
- return;
321
- const a = this.active;
322
- // Search mode handling is local: events go into the search state machine
323
- // instead of the keymap.
324
- if (a.search !== null) {
325
- await this.handleSearchKey(ev);
326
- return;
327
- }
328
- const action = dispatch(this.state, ev);
329
- await this.applyAction(action, ev);
330
- }
331
- /**
332
- * Reset the completion engine and forget any listing geometry: the next Tab
333
- * will request a fresh result, and the next `list` / `cycled` will emit a
334
- * new block on a fresh row instead of trying to overwrite a stale listing
335
- * whose coordinates are no longer valid.
336
- */
337
- resetCompletion() {
338
- this.completion.reset();
339
- if (this.active !== null)
340
- this.active.listingRowsDrawn = 0;
341
- }
342
- async applyAction(action, ev) {
343
- if (this.active === null)
344
- return;
345
- const a = this.active;
346
- switch (action.kind) {
347
- case "noop":
348
- return;
349
- case "redraw":
350
- this.resetCompletion();
351
- this.render();
352
- return;
353
- case "bell":
354
- this.stdout.write(BEL);
355
- return;
356
- case "submit": {
357
- this.resetCompletion();
358
- const text = this.state.buffer.text;
359
- // Move cursor past the end of the rendered block and emit a newline.
360
- this.moveCursorToEnd();
361
- this.stdout.write(LF);
362
- const resolve = a.resolve;
363
- this.active = null;
364
- this.exitRaw();
365
- resolve(text);
366
- return;
367
- }
368
- case "cancel": {
369
- // Upstream psql doesn't echo `^C` to the screen on Ctrl-C — it just
370
- // breaks to the next prompt line silently. Match that behaviour.
371
- this.resetCompletion();
372
- this.moveCursorToEnd();
373
- this.stdout.write(LF);
374
- const reject = a.reject;
375
- this.active = null;
376
- this.exitRaw();
377
- reject(new SignalError());
378
- return;
379
- }
380
- case "eof": {
381
- this.resetCompletion();
382
- this.moveCursorToEnd();
383
- this.stdout.write(LF);
384
- const resolve = a.resolve;
385
- this.active = null;
386
- this.exitRaw();
387
- resolve(EOF_SYMBOL);
388
- return;
389
- }
390
- case "complete": {
391
- if (!this.completer) {
392
- this.stdout.write(BEL);
393
- return;
394
- }
395
- await this.runCompletion();
396
- return;
397
- }
398
- case "clear-screen":
399
- this.resetCompletion();
400
- this.stdout.write(csiClearScreen());
401
- a.rowsDrawn = 0;
402
- a.cursorRow = 0;
403
- a.cursorCol = 0;
404
- this.render(true);
405
- return;
406
- case "search-start":
407
- this.resetCompletion();
408
- a.search = {
409
- pattern: "",
410
- matchIndex: null,
411
- savedBuffer: this.state.buffer.text,
412
- };
413
- this.render();
414
- return;
415
- case "paste-start":
416
- case "paste-end":
417
- // Bracketed paste markers are otherwise transparent.
418
- void ev;
419
- return;
420
- case "ex-update":
421
- // Vi `:`-ex prompt text changed (entered/typed/backspaced). The
422
- // renderer reads `state.mode === 'ex'` and swaps in a `: <buf>` line.
423
- this.resetCompletion();
424
- this.render();
425
- return;
426
- }
427
- }
428
- // -------------------------------------------------------------------------
429
- // Completion
430
- // -------------------------------------------------------------------------
431
- async runCompletion() {
432
- if (this.completer === undefined || this.active === null)
433
- return;
434
- const step = await this.completion.apply(this.state.buffer, this.completer);
435
- if (this.active === null)
436
- return;
437
- switch (step.kind) {
438
- case "bell":
439
- this.stdout.write(BEL);
440
- return;
441
- case "inserted":
442
- this.render();
443
- return;
444
- case "cycled": {
445
- // In-place rewrite of the candidate listing with the new highlight.
446
- // If we already have a listing on screen (placed above the current
447
- // prompt block by an earlier `list` or `cycled`), navigate up to its
448
- // first row, erase each row, and reprint with the cycled highlight.
449
- // Otherwise fall back to the "print listing below" path, matching
450
- // first-time `list` behaviour.
451
- const cands = this.completion.getCandidates();
452
- const cycleIndex = this.completion.getCycleIndex();
453
- if (cands.length > 0) {
454
- const w = this.termWidth();
455
- const block = formatCandidates(cands, w, cycleIndex);
456
- const blockRows = block.split("\n").length;
457
- if (this.active.listingRowsDrawn > 0) {
458
- this.rewriteListingInPlace(block, blockRows);
459
- }
460
- else {
461
- this.emitListingBelow(block, blockRows);
462
- }
463
- }
464
- else {
465
- this.render();
466
- }
467
- return;
468
- }
469
- case "list": {
470
- // Print listing on a new row, then redraw the prompt+line below it.
471
- const w = this.termWidth();
472
- const block = formatCandidates(step.candidates, w);
473
- const blockRows = block.split("\n").length;
474
- this.emitListingBelow(block, blockRows);
475
- return;
476
- }
477
- }
478
- }
479
- /**
480
- * Print `block` on fresh rows below the prompt block, then redraw the
481
- * prompt + buffer below it. Remembers how many rows the listing occupies
482
- * in `listingRowsDrawn` so a subsequent cycle can rewrite it in place.
483
- */
484
- emitListingBelow(block, blockRows) {
485
- if (this.active === null)
486
- return;
487
- this.moveCursorToEnd();
488
- this.stdout.write(LF);
489
- this.stdout.write(block + LF);
490
- this.active.rowsDrawn = 0;
491
- this.active.cursorRow = 0;
492
- this.active.cursorCol = 0;
493
- this.active.listingRowsDrawn = blockRows;
494
- this.render(true);
495
- }
496
- /**
497
- * Rewrite the candidate listing in place. Pre-condition: a listing of
498
- * `this.active.listingRowsDrawn` rows is currently drawn just above the
499
- * prompt block.
500
- *
501
- * 1) Step up to the FIRST row of the listing (past the prompt block).
502
- * 2) Erase + reprint each listing row.
503
- * 3) Move back down past any trailing erase, then redraw prompt + buffer.
504
- *
505
- * If `block` has a different row count from the old listing the difference
506
- * is absorbed by clearing extra rows (shrinking) or by accepting some
507
- * scroll (growing — rare in practice because the candidate list is fixed
508
- * for the duration of a cycle).
509
- */
510
- rewriteListingInPlace(block, blockRows) {
511
- if (this.active === null)
512
- return;
513
- const a = this.active;
514
- const oldRows = a.listingRowsDrawn;
515
- // 1. Cursor is somewhere inside the prompt block. Anchor to row 0 of the
516
- // prompt block first.
517
- this.stdout.write(CR);
518
- if (a.cursorRow > 0)
519
- this.stdout.write(csiUp(a.cursorRow));
520
- // 2. Step up `oldRows` more rows so the cursor sits on the first row of
521
- // the listing.
522
- this.stdout.write(csiUp(oldRows));
523
- // 3. Erase each listing row and write the new block. We treat the listing
524
- // as `blockRows` lines separated by LF; on the last line we DON'T emit
525
- // a trailing LF (otherwise we'd push the prompt down by one row).
526
- const newLines = block.split("\n");
527
- for (let i = 0; i < newLines.length; i++) {
528
- this.stdout.write(csiEraseToEol());
529
- this.stdout.write(newLines[i]);
530
- if (i < newLines.length - 1)
531
- this.stdout.write(LF + CR);
532
- }
533
- // 4. If the new block is shorter than the old one, clear the leftover
534
- // rows below.
535
- if (blockRows < oldRows) {
536
- const extra = oldRows - blockRows;
537
- for (let i = 0; i < extra; i++) {
538
- this.stdout.write(LF + CR + csiEraseToEol());
539
- }
540
- // Step back up so the cursor sits right under the last listing line.
541
- this.stdout.write(csiUp(extra));
542
- }
543
- // 5. Move past the listing onto the row where the prompt should start.
544
- this.stdout.write(LF + CR);
545
- // 6. The prompt's geometry needs to be redrawn fresh below the listing.
546
- a.rowsDrawn = 0;
547
- a.cursorRow = 0;
548
- a.cursorCol = 0;
549
- a.listingRowsDrawn = blockRows;
550
- this.render(true);
551
- }
552
- // -------------------------------------------------------------------------
553
- // Reverse-incremental-search
554
- // -------------------------------------------------------------------------
555
- async handleSearchKey(ev) {
556
- if (this.active?.search == null)
557
- return;
558
- const s = this.active.search;
559
- // ^G or Escape cancels and restores the saved line.
560
- if ((ev.key === "char" && ev.ctrl && ev.char === "g") ||
561
- ev.key === "escape") {
562
- this.state.buffer.setText(s.savedBuffer);
563
- this.active.search = null;
564
- this.render();
565
- return;
566
- }
567
- // Enter accepts the current match (whatever's in the buffer).
568
- if (ev.key === "enter") {
569
- this.active.search = null;
570
- await this.applyAction({ kind: "submit" }, ev);
571
- return;
572
- }
573
- // ^C bubbles to cancel.
574
- if (ev.key === "char" && ev.ctrl && ev.char === "c") {
575
- await this.applyAction({ kind: "cancel" }, ev);
576
- return;
577
- }
578
- // ^R again: search further back.
579
- if (ev.key === "char" && ev.ctrl && ev.char === "r") {
580
- this.searchStep(-1);
581
- return;
582
- }
583
- // Backspace: shrink the pattern.
584
- if (ev.key === "backspace") {
585
- s.pattern = s.pattern.slice(0, -1);
586
- s.matchIndex = null;
587
- this.searchStep(0);
588
- return;
589
- }
590
- // Printable char: extend the pattern.
591
- if (ev.key === "char" &&
592
- !ev.ctrl &&
593
- !ev.meta &&
594
- ev.char !== undefined) {
595
- s.pattern += ev.char;
596
- s.matchIndex = null;
597
- this.searchStep(0);
598
- return;
599
- }
600
- // Anything else (arrows, etc) accepts the match and processes the key.
601
- this.active.search = null;
602
- await this.handleEvent(ev);
603
- }
604
- /** Walk history backward looking for the current pattern. */
605
- searchStep(delta) {
606
- if (this.active?.search == null)
607
- return;
608
- const s = this.active.search;
609
- const hist = this.state.history;
610
- const startFrom = s.matchIndex === null ? hist.length - 1 : s.matchIndex + delta;
611
- for (let i = startFrom; i >= 0 && i < hist.length; i--) {
612
- if (s.pattern === "" || hist[i].includes(s.pattern)) {
613
- s.matchIndex = i;
614
- this.state.buffer.setText(hist[i]);
615
- this.render();
616
- return;
617
- }
618
- }
619
- // No match: ring bell, keep current buffer.
620
- this.stdout.write(BEL);
621
- this.render();
622
- }
623
- // -------------------------------------------------------------------------
624
- // Rendering
625
- // -------------------------------------------------------------------------
626
- /**
627
- * Repaint the prompt + buffer. If `full` is true, skip the cursor-up
628
- * optimisation (we don't know the previous geometry).
629
- */
630
- render(full = false) {
631
- if (this.active === null)
632
- return;
633
- const a = this.active;
634
- // Move the cursor back to row 0 of the previously drawn block.
635
- if (!full && a.rowsDrawn > 0) {
636
- // Move up to the anchor row.
637
- const up = a.cursorRow;
638
- this.stdout.write(CR + (up > 0 ? csiUp(up) : ""));
639
- }
640
- else {
641
- this.stdout.write(CR);
642
- }
643
- // Compose the output: prompt + buffer text, with line wrapping computed
644
- // virtually so we know where the cursor ends up.
645
- const width = Math.max(1, this.termWidth());
646
- // Pick the prompt + rendered text for the three render flavours:
647
- // - vi ex-mode (`:`-prompt): `: <exBuffer>`, cursor at the end.
648
- // - reverse-i-search: the `(reverse-i-search)\`pat':` preamble,
649
- // with the matched pattern highlighted.
650
- // - default editing: the caller-supplied prompt + buffer text.
651
- const inExMode = this.state.mode === "ex";
652
- const promptStr = inExMode
653
- ? ":"
654
- : a.search === null
655
- ? a.prompt
656
- : `(reverse-i-search)\`${a.search.pattern}': `;
657
- const rawText = inExMode
658
- ? this.state.exBuffer
659
- : this.state.buffer.text.replace(/\n/g, "⏎"); // newline glyph
660
- // For search rendering we highlight the matched pattern (case-insensitive)
661
- // inside the matched entry; otherwise the rendered text equals the raw text.
662
- const renderText = !inExMode && a.search !== null && a.search.pattern.length > 0
663
- ? highlightMatch(rawText, a.search.pattern)
664
- : rawText;
665
- const promptWidth = displayWidth(promptStr);
666
- // Cursor positioning uses the raw text length, NOT the rendered text
667
- // length, because ANSI escapes have zero display width but non-zero
668
- // string length. In ex mode the cursor always sits at the end of the
669
- // ex buffer.
670
- const beforeText = inExMode
671
- ? rawText
672
- : rawText.slice(0, this.codePointsBeforeCursor().length);
673
- const allText = renderText;
674
- // Compute physical row/col for cursor.
675
- const { row: cursorRow, col: cursorCol } = positionAfter(promptWidth, beforeText, width);
676
- // Compute final row/col for the full block (so we know how many rows).
677
- // Strip ANSI escape sequences from the geometry calculation since they
678
- // have zero display width but non-zero string length.
679
- const { row: lastRow } = positionAfter(promptWidth, stripAnsi(allText), width);
680
- const rowsDrawn = lastRow + 1;
681
- // Write the line. We erase each row first so leftover chars from a longer
682
- // previous render are scrubbed.
683
- this.stdout.write(csiEraseToEol());
684
- this.stdout.write(promptStr);
685
- this.stdout.write(allText);
686
- // After writing, if the previous render had more rows than this one,
687
- // erase the leftover rows.
688
- if (a.rowsDrawn > rowsDrawn) {
689
- const extra = a.rowsDrawn - rowsDrawn;
690
- for (let i = 0; i < extra; i++) {
691
- this.stdout.write(LF + csiEraseToEol());
692
- }
693
- // Move back up to where we are.
694
- this.stdout.write(csiUp(extra));
695
- }
696
- // Reposition cursor.
697
- // After writing `allText`, cursor is at (lastRow, lastCol). We want
698
- // (cursorRow, cursorCol).
699
- const rowDelta = cursorRow - lastRow;
700
- if (rowDelta < 0)
701
- this.stdout.write(csiUp(-rowDelta));
702
- if (rowDelta > 0)
703
- this.stdout.write(csiDown(rowDelta));
704
- this.stdout.write(CR);
705
- if (cursorCol > 0)
706
- this.stdout.write(csiRight(cursorCol));
707
- a.rowsDrawn = rowsDrawn;
708
- a.cursorRow = cursorRow;
709
- a.cursorCol = cursorCol;
710
- // Silence unused.
711
- void csiLeft;
712
- }
713
- moveCursorToEnd() {
714
- if (this.active === null)
715
- return;
716
- const a = this.active;
717
- // Step down to the final row of the current render.
718
- const down = a.rowsDrawn - 1 - a.cursorRow;
719
- if (down > 0)
720
- this.stdout.write(csiDown(down));
721
- this.stdout.write(CR);
722
- a.cursorRow = a.rowsDrawn - 1;
723
- a.cursorCol = 0;
724
- }
725
- codePointsBeforeCursor() {
726
- return { length: this.state.buffer.cursor };
727
- }
728
- termWidth() {
729
- const s = this.stdout;
730
- if (typeof s.columns === "number" && s.columns > 0)
731
- return s.columns;
732
- return 80;
733
- }
734
- }
58
+ var LineEditor = class {
59
+ constructor(opts = {}) {
60
+ this.EOF = EOF_SYMBOL;
61
+ this.pendingMode = null;
62
+ this.completion = new CompletionState();
63
+ this.eventQueue = [];
64
+ this.processing = false;
65
+ this.active = null;
66
+ this.dataListener = null;
67
+ this.wasRaw = false;
68
+ this.exitListener = null;
69
+ this.stdin = opts.stdin ?? process.stdin;
70
+ this.stdout = opts.stdout ?? process.stdout;
71
+ this.bracketedPaste = opts.bracketedPaste ?? true;
72
+ this.completer = opts.completer;
73
+ this.mode = opts.mode ?? "emacs";
74
+ this.state = makeState(opts.history ?? [], this.mode === "vi" ? "insert" : "emacs");
75
+ this.decoder = new Vt100Decoder({
76
+ escTimeoutMs: opts.escTimeoutMs ?? 50,
77
+ onTimeoutEvent: (ev) => {
78
+ this.handleDecoderTimeout(ev);
79
+ }
80
+ });
81
+ }
82
+ /** Read one line. Resolves on Enter; rejects on Ctrl-C. */
83
+ readLine(prompt) {
84
+ if (this.active !== null) return Promise.reject(/* @__PURE__ */ new Error("LineEditor.readLine called re-entrantly"));
85
+ if (this.pendingMode !== null) {
86
+ this.mode = this.pendingMode;
87
+ this.pendingMode = null;
88
+ }
89
+ return new Promise((resolve, reject) => {
90
+ this.state.buffer = new LineBuffer();
91
+ this.state.historyIndex = -1;
92
+ this.state.liveSnapshot = null;
93
+ if (this.mode === "vi") {
94
+ this.state.mode = "insert";
95
+ this.state.viPending = null;
96
+ } else {
97
+ this.state.mode = "emacs";
98
+ this.state.viPending = null;
99
+ this.state.exBuffer = "";
100
+ }
101
+ this.completion.reset();
102
+ this.active = {
103
+ prompt,
104
+ resolve,
105
+ reject,
106
+ rowsDrawn: 0,
107
+ cursorRow: 0,
108
+ cursorCol: 0,
109
+ search: null,
110
+ listingRowsDrawn: 0
111
+ };
112
+ try {
113
+ this.enterRaw();
114
+ } catch (err) {
115
+ this.active = null;
116
+ reject(err);
117
+ return;
118
+ }
119
+ this.render();
120
+ if (this.eventQueue.length > 0) this.drainQueue();
121
+ });
122
+ }
123
+ /** Force redraw (call from SIGWINCH handler). */
124
+ redraw() {
125
+ if (this.active !== null) this.render(true);
126
+ }
127
+ /**
128
+ * Inject an out-of-band line into the terminal while a prompt is being
129
+ * edited. Used by callers that produce async output (NOTIFY messages,
130
+ * notices, etc.) that would otherwise clobber the prompt rendering.
131
+ *
132
+ * Behaviour:
133
+ * - No active readLine: pass-through to stdout.
134
+ * - Active readLine: move cursor to end-of-block, write a fresh newline
135
+ * so the injected text starts on its own row, write the text, then
136
+ * redraw the prompt + buffer below it (re-attaching the cursor).
137
+ *
138
+ * The injected text should NOT have a trailing newline — we add one as
139
+ * part of the move-to-end + LF dance. If the caller's payload already
140
+ * ends with `\n`, we strip it once.
141
+ */
142
+ interject(text) {
143
+ if (this.active === null) {
144
+ this.stdout.write(text);
145
+ return;
146
+ }
147
+ const a = this.active;
148
+ this.moveCursorToEnd();
149
+ this.stdout.write("\n");
150
+ const body = text.endsWith("\n") ? text.slice(0, -1) : text;
151
+ this.stdout.write(body);
152
+ this.stdout.write("\n");
153
+ a.rowsDrawn = 0;
154
+ a.cursorRow = 0;
155
+ a.cursorCol = 0;
156
+ a.listingRowsDrawn = 0;
157
+ this.render(true);
158
+ }
159
+ /** Cleanup raw mode and restore TTY. Idempotent. */
160
+ close() {
161
+ this.exitRaw();
162
+ }
163
+ /**
164
+ * Request a switch between vi and emacs editing modes. The change takes
165
+ * effect at the NEXT `readLine()` boundary, not retroactively on the line
166
+ * currently being edited — matching upstream readline's `set editing-mode`
167
+ * semantics. Calling this with the currently-active mode is a no-op.
168
+ */
169
+ setMode(mode) {
170
+ this.pendingMode = mode;
171
+ }
172
+ /** Read the editor's currently effective mode. Exposed for tests. */
173
+ getMode() {
174
+ return this.mode;
175
+ }
176
+ /** Push a line into the in-memory history. */
177
+ pushHistory(line) {
178
+ if (line.length === 0) return;
179
+ if (this.state.history[this.state.history.length - 1] === line) return;
180
+ this.state.history.push(line);
181
+ }
182
+ /** Replace the in-memory history list. */
183
+ setHistory(lines) {
184
+ this.state.history = lines.slice();
185
+ this.state.historyIndex = -1;
186
+ this.state.liveSnapshot = null;
187
+ }
188
+ enterRaw() {
189
+ const s = this.stdin;
190
+ if (isTtyReadStream(s)) {
191
+ this.wasRaw = Boolean(s.isRaw);
192
+ s.setRawMode(true);
193
+ }
194
+ s.resume();
195
+ this.decoder.reset();
196
+ if (this.bracketedPaste) this.stdout.write(enableBracketedPaste());
197
+ this.dataListener = (chunk) => {
198
+ this.handleChunk(chunk);
199
+ };
200
+ s.on("data", this.dataListener);
201
+ if (!this.exitListener) {
202
+ this.exitListener = () => {
203
+ this.exitRaw();
204
+ };
205
+ process.once("exit", this.exitListener);
206
+ process.once("SIGTERM", this.exitListener);
207
+ }
208
+ }
209
+ exitRaw() {
210
+ const s = this.stdin;
211
+ if (this.dataListener !== null) {
212
+ s.off("data", this.dataListener);
213
+ this.dataListener = null;
214
+ }
215
+ if (isTtyReadStream(s) && !this.wasRaw) try {
216
+ s.setRawMode(false);
217
+ } catch {}
218
+ if (this.bracketedPaste) try {
219
+ this.stdout.write(disableBracketedPaste());
220
+ } catch {}
221
+ if (this.exitListener !== null) {
222
+ process.off("exit", this.exitListener);
223
+ process.off("SIGTERM", this.exitListener);
224
+ this.exitListener = null;
225
+ }
226
+ }
227
+ handleChunk(chunk) {
228
+ if (this.active === null) return;
229
+ const events = this.decoder.push(new Uint8Array(chunk));
230
+ for (const ev of events) this.eventQueue.push(ev);
231
+ this.drainQueue();
232
+ }
233
+ /** Called when the decoder's bare-Esc timer fires with a buffered event. */
234
+ handleDecoderTimeout(ev) {
235
+ if (this.active === null) return;
236
+ this.eventQueue.push(ev);
237
+ this.drainQueue();
238
+ }
239
+ /**
240
+ * Serially drain the event queue. Each event may kick off async work
241
+ * (notably Tab completion); we await it before processing the next
242
+ * event so keystrokes don't race ahead of pending completion results.
243
+ */
244
+ async drainQueue() {
245
+ if (this.processing) return;
246
+ this.processing = true;
247
+ try {
248
+ while (this.eventQueue.length > 0 && this.active !== null) {
249
+ const ev = this.eventQueue.shift();
250
+ if (ev === void 0) break;
251
+ await this.handleEvent(ev);
252
+ }
253
+ } finally {
254
+ this.processing = false;
255
+ }
256
+ }
257
+ async handleEvent(ev) {
258
+ if (this.active === null) return;
259
+ if (this.active.search !== null) {
260
+ await this.handleSearchKey(ev);
261
+ return;
262
+ }
263
+ const action = dispatch(this.state, ev);
264
+ await this.applyAction(action, ev);
265
+ }
266
+ /**
267
+ * Reset the completion engine and forget any listing geometry: the next Tab
268
+ * will request a fresh result, and the next `list` / `cycled` will emit a
269
+ * new block on a fresh row instead of trying to overwrite a stale listing
270
+ * whose coordinates are no longer valid.
271
+ */
272
+ resetCompletion() {
273
+ this.completion.reset();
274
+ if (this.active !== null) this.active.listingRowsDrawn = 0;
275
+ }
276
+ async applyAction(action, ev) {
277
+ if (this.active === null) return;
278
+ const a = this.active;
279
+ switch (action.kind) {
280
+ case "noop": return;
281
+ case "redraw":
282
+ this.resetCompletion();
283
+ this.render();
284
+ return;
285
+ case "bell":
286
+ this.stdout.write("\x07");
287
+ return;
288
+ case "submit": {
289
+ this.resetCompletion();
290
+ const text = this.state.buffer.text;
291
+ this.moveCursorToEnd();
292
+ this.stdout.write("\n");
293
+ const resolve = a.resolve;
294
+ this.active = null;
295
+ this.exitRaw();
296
+ resolve(text);
297
+ return;
298
+ }
299
+ case "cancel": {
300
+ this.resetCompletion();
301
+ this.moveCursorToEnd();
302
+ this.stdout.write("\n");
303
+ const reject = a.reject;
304
+ this.active = null;
305
+ this.exitRaw();
306
+ reject(new SignalError());
307
+ return;
308
+ }
309
+ case "eof": {
310
+ this.resetCompletion();
311
+ this.moveCursorToEnd();
312
+ this.stdout.write("\n");
313
+ const resolve = a.resolve;
314
+ this.active = null;
315
+ this.exitRaw();
316
+ resolve(EOF_SYMBOL);
317
+ return;
318
+ }
319
+ case "complete":
320
+ if (!this.completer) {
321
+ this.stdout.write("\x07");
322
+ return;
323
+ }
324
+ await this.runCompletion();
325
+ return;
326
+ case "clear-screen":
327
+ this.resetCompletion();
328
+ this.stdout.write(csiClearScreen());
329
+ a.rowsDrawn = 0;
330
+ a.cursorRow = 0;
331
+ a.cursorCol = 0;
332
+ this.render(true);
333
+ return;
334
+ case "search-start":
335
+ this.resetCompletion();
336
+ a.search = {
337
+ pattern: "",
338
+ matchIndex: null,
339
+ savedBuffer: this.state.buffer.text
340
+ };
341
+ this.render();
342
+ return;
343
+ case "paste-start":
344
+ case "paste-end": return;
345
+ case "ex-update":
346
+ this.resetCompletion();
347
+ this.render();
348
+ return;
349
+ }
350
+ }
351
+ async runCompletion() {
352
+ if (this.completer === void 0 || this.active === null) return;
353
+ const step = await this.completion.apply(this.state.buffer, this.completer);
354
+ if (this.active === null) return;
355
+ switch (step.kind) {
356
+ case "bell":
357
+ this.stdout.write("\x07");
358
+ return;
359
+ case "inserted":
360
+ this.render();
361
+ return;
362
+ case "cycled": {
363
+ const cands = this.completion.getCandidates();
364
+ const cycleIndex = this.completion.getCycleIndex();
365
+ if (cands.length > 0) {
366
+ const w = this.termWidth();
367
+ const block = formatCandidates(cands, w, cycleIndex);
368
+ const blockRows = block.split("\n").length;
369
+ if (this.active.listingRowsDrawn > 0) this.rewriteListingInPlace(block, blockRows);
370
+ else this.emitListingBelow(block, blockRows);
371
+ } else this.render();
372
+ return;
373
+ }
374
+ case "list": {
375
+ const w = this.termWidth();
376
+ const block = formatCandidates(step.candidates, w);
377
+ const blockRows = block.split("\n").length;
378
+ this.emitListingBelow(block, blockRows);
379
+ return;
380
+ }
381
+ }
382
+ }
383
+ /**
384
+ * Print `block` on fresh rows below the prompt block, then redraw the
385
+ * prompt + buffer below it. Remembers how many rows the listing occupies
386
+ * in `listingRowsDrawn` so a subsequent cycle can rewrite it in place.
387
+ */
388
+ emitListingBelow(block, blockRows) {
389
+ if (this.active === null) return;
390
+ this.moveCursorToEnd();
391
+ this.stdout.write("\n");
392
+ this.stdout.write(block + "\n");
393
+ this.active.rowsDrawn = 0;
394
+ this.active.cursorRow = 0;
395
+ this.active.cursorCol = 0;
396
+ this.active.listingRowsDrawn = blockRows;
397
+ this.render(true);
398
+ }
399
+ /**
400
+ * Rewrite the candidate listing in place. Pre-condition: a listing of
401
+ * `this.active.listingRowsDrawn` rows is currently drawn just above the
402
+ * prompt block.
403
+ *
404
+ * 1) Step up to the FIRST row of the listing (past the prompt block).
405
+ * 2) Erase + reprint each listing row.
406
+ * 3) Move back down past any trailing erase, then redraw prompt + buffer.
407
+ *
408
+ * If `block` has a different row count from the old listing the difference
409
+ * is absorbed by clearing extra rows (shrinking) or by accepting some
410
+ * scroll (growing — rare in practice because the candidate list is fixed
411
+ * for the duration of a cycle).
412
+ */
413
+ rewriteListingInPlace(block, blockRows) {
414
+ if (this.active === null) return;
415
+ const a = this.active;
416
+ const oldRows = a.listingRowsDrawn;
417
+ this.stdout.write("\r");
418
+ if (a.cursorRow > 0) this.stdout.write(csiUp(a.cursorRow));
419
+ this.stdout.write(csiUp(oldRows));
420
+ const newLines = block.split("\n");
421
+ for (let i = 0; i < newLines.length; i++) {
422
+ this.stdout.write(csiEraseToEol());
423
+ this.stdout.write(newLines[i]);
424
+ if (i < newLines.length - 1) this.stdout.write("\n\r");
425
+ }
426
+ if (blockRows < oldRows) {
427
+ const extra = oldRows - blockRows;
428
+ for (let i = 0; i < extra; i++) this.stdout.write("\n\r" + csiEraseToEol());
429
+ this.stdout.write(csiUp(extra));
430
+ }
431
+ this.stdout.write("\n\r");
432
+ a.rowsDrawn = 0;
433
+ a.cursorRow = 0;
434
+ a.cursorCol = 0;
435
+ a.listingRowsDrawn = blockRows;
436
+ this.render(true);
437
+ }
438
+ async handleSearchKey(ev) {
439
+ if (this.active?.search == null) return;
440
+ const s = this.active.search;
441
+ if (ev.key === "char" && ev.ctrl && ev.char === "g" || ev.key === "escape") {
442
+ this.state.buffer.setText(s.savedBuffer);
443
+ this.active.search = null;
444
+ this.render();
445
+ return;
446
+ }
447
+ if (ev.key === "enter") {
448
+ this.active.search = null;
449
+ await this.applyAction({ kind: "submit" }, ev);
450
+ return;
451
+ }
452
+ if (ev.key === "char" && ev.ctrl && ev.char === "c") {
453
+ await this.applyAction({ kind: "cancel" }, ev);
454
+ return;
455
+ }
456
+ if (ev.key === "char" && ev.ctrl && ev.char === "r") {
457
+ this.searchStep(-1);
458
+ return;
459
+ }
460
+ if (ev.key === "backspace") {
461
+ s.pattern = s.pattern.slice(0, -1);
462
+ s.matchIndex = null;
463
+ this.searchStep(0);
464
+ return;
465
+ }
466
+ if (ev.key === "char" && !ev.ctrl && !ev.meta && ev.char !== void 0) {
467
+ s.pattern += ev.char;
468
+ s.matchIndex = null;
469
+ this.searchStep(0);
470
+ return;
471
+ }
472
+ this.active.search = null;
473
+ await this.handleEvent(ev);
474
+ }
475
+ /** Walk history backward looking for the current pattern. */
476
+ searchStep(delta) {
477
+ if (this.active?.search == null) return;
478
+ const s = this.active.search;
479
+ const hist = this.state.history;
480
+ const startFrom = s.matchIndex === null ? hist.length - 1 : s.matchIndex + delta;
481
+ for (let i = startFrom; i >= 0 && i < hist.length; i--) if (s.pattern === "" || hist[i].includes(s.pattern)) {
482
+ s.matchIndex = i;
483
+ this.state.buffer.setText(hist[i]);
484
+ this.render();
485
+ return;
486
+ }
487
+ this.stdout.write("\x07");
488
+ this.render();
489
+ }
490
+ /**
491
+ * Repaint the prompt + buffer. If `full` is true, skip the cursor-up
492
+ * optimisation (we don't know the previous geometry).
493
+ */
494
+ render(full = false) {
495
+ if (this.active === null) return;
496
+ const a = this.active;
497
+ if (!full && a.rowsDrawn > 0) {
498
+ const up = a.cursorRow;
499
+ this.stdout.write("\r" + (up > 0 ? csiUp(up) : ""));
500
+ } else this.stdout.write("\r");
501
+ const width = Math.max(1, this.termWidth());
502
+ const inExMode = this.state.mode === "ex";
503
+ const promptStr = inExMode ? ":" : a.search === null ? a.prompt : `(reverse-i-search)\`${a.search.pattern}': `;
504
+ const rawText = inExMode ? this.state.exBuffer : this.state.buffer.text.replace(/\n/g, "⏎");
505
+ const renderText = !inExMode && a.search !== null && a.search.pattern.length > 0 ? highlightMatch(rawText, a.search.pattern) : rawText;
506
+ const promptWidth = displayWidth(promptStr);
507
+ const beforeText = inExMode ? rawText : rawText.slice(0, this.codePointsBeforeCursor().length);
508
+ const allText = renderText;
509
+ const { row: cursorRow, col: cursorCol } = positionAfter(promptWidth, beforeText, width);
510
+ const { row: lastRow } = positionAfter(promptWidth, stripAnsi(allText), width);
511
+ const rowsDrawn = lastRow + 1;
512
+ this.stdout.write(csiEraseToEol());
513
+ this.stdout.write(promptStr);
514
+ this.stdout.write(allText);
515
+ if (a.rowsDrawn > rowsDrawn) {
516
+ const extra = a.rowsDrawn - rowsDrawn;
517
+ for (let i = 0; i < extra; i++) this.stdout.write("\n" + csiEraseToEol());
518
+ this.stdout.write(csiUp(extra));
519
+ }
520
+ const rowDelta = cursorRow - lastRow;
521
+ if (rowDelta < 0) this.stdout.write(csiUp(-rowDelta));
522
+ if (rowDelta > 0) this.stdout.write(csiDown(rowDelta));
523
+ this.stdout.write("\r");
524
+ if (cursorCol > 0) this.stdout.write(csiRight(cursorCol));
525
+ a.rowsDrawn = rowsDrawn;
526
+ a.cursorRow = cursorRow;
527
+ a.cursorCol = cursorCol;
528
+ }
529
+ moveCursorToEnd() {
530
+ if (this.active === null) return;
531
+ const a = this.active;
532
+ const down = a.rowsDrawn - 1 - a.cursorRow;
533
+ if (down > 0) this.stdout.write(csiDown(down));
534
+ this.stdout.write("\r");
535
+ a.cursorRow = a.rowsDrawn - 1;
536
+ a.cursorCol = 0;
537
+ }
538
+ codePointsBeforeCursor() {
539
+ return { length: this.state.buffer.cursor };
540
+ }
541
+ termWidth() {
542
+ const s = this.stdout;
543
+ if (typeof s.columns === "number" && s.columns > 0) return s.columns;
544
+ return 80;
545
+ }
546
+ };
735
547
  const isTtyReadStream = (s) => typeof s.setRawMode === "function";
736
- // ---------------------------------------------------------------------------
737
- // Search-line rendering / highlighting
738
- // ---------------------------------------------------------------------------
739
- const SGR_REVERSE = "\x1b[7m";
740
- const SGR_NO_REVERSE = "\x1b[27m";
548
+ const SGR_REVERSE = "\x1B[7m";
549
+ const SGR_NO_REVERSE = "\x1B[27m";
741
550
  /**
742
- * Wrap the first case-insensitive occurrence of `pattern` inside `text`
743
- * with the reverse-video SGR pair. Returns `text` unchanged when the
744
- * pattern is empty or not found.
745
- *
746
- * Kept as a pure helper for unit-testing (the renderer calls it during
747
- * search mode, but `renderSearchLine` is the unit-testable surface).
748
- */
749
- export const highlightMatch = (text, pattern) => {
750
- if (pattern.length === 0)
751
- return text;
752
- const lcText = text.toLowerCase();
753
- const lcPat = pattern.toLowerCase();
754
- const idx = lcText.indexOf(lcPat);
755
- if (idx < 0)
756
- return text;
757
- return (text.slice(0, idx) +
758
- SGR_REVERSE +
759
- text.slice(idx, idx + pattern.length) +
760
- SGR_NO_REVERSE +
761
- text.slice(idx + pattern.length));
551
+ * Wrap the first case-insensitive occurrence of `pattern` inside `text`
552
+ * with the reverse-video SGR pair. Returns `text` unchanged when the
553
+ * pattern is empty or not found.
554
+ *
555
+ * Kept as a pure helper for unit-testing (the renderer calls it during
556
+ * search mode, but `renderSearchLine` is the unit-testable surface).
557
+ */
558
+ const highlightMatch = (text, pattern) => {
559
+ if (pattern.length === 0) return text;
560
+ const lcText = text.toLowerCase();
561
+ const lcPat = pattern.toLowerCase();
562
+ const idx = lcText.indexOf(lcPat);
563
+ if (idx < 0) return text;
564
+ return text.slice(0, idx) + SGR_REVERSE + text.slice(idx, idx + pattern.length) + SGR_NO_REVERSE + text.slice(idx + pattern.length);
762
565
  };
763
566
  /**
764
- * Render the search prompt + matched entry as a single string, with the
765
- * matched pattern highlighted via reverse video. Exposed for unit tests.
766
- * The real interactive renderer applies the same logic inline.
767
- */
768
- export const renderSearchLine = (pattern, entry) => {
769
- const prefix = `(reverse-i-search)\`${pattern}': `;
770
- return prefix + highlightMatch(entry, pattern);
567
+ * Render the search prompt + matched entry as a single string, with the
568
+ * matched pattern highlighted via reverse video. Exposed for unit tests.
569
+ * The real interactive renderer applies the same logic inline.
570
+ */
571
+ const renderSearchLine = (pattern, entry) => {
572
+ return `(reverse-i-search)\`${pattern}': ` + highlightMatch(entry, pattern);
771
573
  };
772
574
  /** Strip ANSI CSI escape sequences from `text` for display-width math. */
773
- const stripAnsi = (text) =>
774
- // Targets the SGR forms we emit (e.g. \x1b[7m, \x1b[27m). Kept narrow on
775
- // purpose so it doesn't accidentally eat legitimate `[` characters.
776
- text.replace(/\x1b\[[0-9;]*m/g, "");
777
- // ---------------------------------------------------------------------------
778
- // Display-width helpers (inlined copy of WP-09's tables; kept minimal).
779
- // ---------------------------------------------------------------------------
575
+ const stripAnsi = (text) => text.replace(/\x1b\[[0-9;]*m/g, "");
780
576
  const WIDE_RANGES = [
781
- [0x1100, 0x115f],
782
- [0x2329, 0x232a],
783
- [0x2e80, 0x303e],
784
- [0x3041, 0x33ff],
785
- [0x3400, 0x4dbf],
786
- [0x4e00, 0x9fff],
787
- [0xa000, 0xa4cf],
788
- [0xac00, 0xd7a3],
789
- [0xf900, 0xfaff],
790
- [0xfe10, 0xfe19],
791
- [0xfe30, 0xfe6f],
792
- [0xff00, 0xff60],
793
- [0xffe0, 0xffe6],
794
- [0x1f300, 0x1f64f],
795
- [0x1f900, 0x1f9ff],
796
- [0x20000, 0x2fffd],
797
- [0x30000, 0x3fffd],
577
+ [4352, 4447],
578
+ [9001, 9002],
579
+ [11904, 12350],
580
+ [12353, 13311],
581
+ [13312, 19903],
582
+ [19968, 40959],
583
+ [40960, 42191],
584
+ [44032, 55203],
585
+ [63744, 64255],
586
+ [65040, 65049],
587
+ [65072, 65135],
588
+ [65280, 65376],
589
+ [65504, 65510],
590
+ [127744, 128591],
591
+ [129280, 129535],
592
+ [131072, 196605],
593
+ [196608, 262141]
798
594
  ];
799
595
  const ZERO_RANGES = [
800
- [0x0300, 0x036f],
801
- [0x0483, 0x0489],
802
- [0x0591, 0x05bd],
803
- [0x05bf, 0x05bf],
804
- [0x05c1, 0x05c2],
805
- [0x05c4, 0x05c5],
806
- [0x05c7, 0x05c7],
807
- [0x0610, 0x061a],
808
- [0x064b, 0x065f],
809
- [0x0670, 0x0670],
810
- [0x06d6, 0x06dc],
811
- [0x06df, 0x06e4],
812
- [0x06e7, 0x06e8],
813
- [0x06ea, 0x06ed],
814
- [0x0711, 0x0711],
815
- [0x0730, 0x074a],
816
- [0x200b, 0x200f],
817
- [0x202a, 0x202e],
818
- [0x2060, 0x206f],
819
- [0x20d0, 0x20f0],
820
- [0xfe00, 0xfe0f],
821
- [0xfe20, 0xfe2f],
822
- [0xfeff, 0xfeff],
823
- [0xe0100, 0xe01ef],
596
+ [768, 879],
597
+ [1155, 1161],
598
+ [1425, 1469],
599
+ [1471, 1471],
600
+ [1473, 1474],
601
+ [1476, 1477],
602
+ [1479, 1479],
603
+ [1552, 1562],
604
+ [1611, 1631],
605
+ [1648, 1648],
606
+ [1750, 1756],
607
+ [1759, 1764],
608
+ [1767, 1768],
609
+ [1770, 1773],
610
+ [1809, 1809],
611
+ [1840, 1866],
612
+ [8203, 8207],
613
+ [8234, 8238],
614
+ [8288, 8303],
615
+ [8400, 8432],
616
+ [65024, 65039],
617
+ [65056, 65071],
618
+ [65279, 65279],
619
+ [917760, 917999]
824
620
  ];
825
621
  const inRange = (cp, ranges) => {
826
- let lo = 0;
827
- let hi = ranges.length - 1;
828
- while (lo <= hi) {
829
- const mid = (lo + hi) >> 1;
830
- const entry = ranges[mid];
831
- if (cp < entry[0])
832
- hi = mid - 1;
833
- else if (cp > entry[1])
834
- lo = mid + 1;
835
- else
836
- return true;
837
- }
838
- return false;
622
+ let lo = 0;
623
+ let hi = ranges.length - 1;
624
+ while (lo <= hi) {
625
+ const mid = lo + hi >> 1;
626
+ const entry = ranges[mid];
627
+ if (cp < entry[0]) hi = mid - 1;
628
+ else if (cp > entry[1]) lo = mid + 1;
629
+ else return true;
630
+ }
631
+ return false;
839
632
  };
840
633
  const codePointWidth = (cp) => {
841
- if (cp === 0)
842
- return 0;
843
- if (cp < 0x20 || (cp >= 0x7f && cp < 0xa0))
844
- return 0;
845
- if (inRange(cp, ZERO_RANGES))
846
- return 0;
847
- if (inRange(cp, WIDE_RANGES))
848
- return 2;
849
- return 1;
634
+ if (cp === 0) return 0;
635
+ if (cp < 32 || cp >= 127 && cp < 160) return 0;
636
+ if (inRange(cp, ZERO_RANGES)) return 0;
637
+ if (inRange(cp, WIDE_RANGES)) return 2;
638
+ return 1;
850
639
  };
851
640
  const displayWidth = (text) => {
852
- let w = 0;
853
- for (const ch of text)
854
- w += codePointWidth(ch.codePointAt(0) ?? 0);
855
- return w;
641
+ let w = 0;
642
+ for (const ch of text) w += codePointWidth(ch.codePointAt(0) ?? 0);
643
+ return w;
856
644
  };
857
645
  /**
858
- * Compute the final (row, col) position after writing `text` to a
859
- * terminal whose cursor starts at column `startCol` and is `width`
860
- * columns wide. Wrapping happens by display column count, not code points.
861
- */
646
+ * Compute the final (row, col) position after writing `text` to a
647
+ * terminal whose cursor starts at column `startCol` and is `width`
648
+ * columns wide. Wrapping happens by display column count, not code points.
649
+ */
862
650
  const positionAfter = (startCol, text, width) => {
863
- let row = 0;
864
- let col = startCol % width;
865
- // Initial wrap if startCol exactly hit the width boundary.
866
- if (col === 0 && startCol > 0) {
867
- row += Math.floor(startCol / width);
868
- }
869
- else {
870
- row += Math.floor(startCol / width);
871
- }
872
- for (const ch of text) {
873
- if (ch === "\n") {
874
- row += 1;
875
- col = 0;
876
- continue;
877
- }
878
- const w = codePointWidth(ch.codePointAt(0) ?? 0);
879
- if (col + w > width) {
880
- row += 1;
881
- col = w;
882
- }
883
- else {
884
- col += w;
885
- if (col === width) {
886
- // Stay on this row; next char triggers the wrap.
887
- // (Real terminals differ here; the conservative choice that matches
888
- // most xterm derivatives is to NOT advance row until the next glyph.)
889
- }
890
- }
891
- }
892
- return { row, col };
651
+ let row = 0;
652
+ let col = startCol % width;
653
+ if (col === 0 && startCol > 0) row += Math.floor(startCol / width);
654
+ else row += Math.floor(startCol / width);
655
+ for (const ch of text) {
656
+ if (ch === "\n") {
657
+ row += 1;
658
+ col = 0;
659
+ continue;
660
+ }
661
+ const w = codePointWidth(ch.codePointAt(0) ?? 0);
662
+ if (col + w > width) {
663
+ row += 1;
664
+ col = w;
665
+ } else {
666
+ col += w;
667
+ if (col === width) {}
668
+ }
669
+ }
670
+ return {
671
+ row,
672
+ col
673
+ };
893
674
  };
675
+ //#endregion
676
+ export { LineEditor, SignalError, highlightMatch, renderSearchLine };