minercon 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +439 -0
  2. package/LICENSE +22 -0
  3. package/README.md +401 -0
  4. package/images/icon.png +0 -0
  5. package/out/ansi.js +123 -0
  6. package/out/argumentHint.js +43 -0
  7. package/out/bukkitHelpParsing.js +62 -0
  8. package/out/cli.js +253 -0
  9. package/out/cliConfig.js +141 -0
  10. package/out/commandLine.js +28 -0
  11. package/out/commandSuggestions.js +202 -0
  12. package/out/commandTree.js +46 -0
  13. package/out/commandTreeCache.js +171 -0
  14. package/out/commandTreeCrawler.js +583 -0
  15. package/out/commandTreeParsingBrigadier.js +426 -0
  16. package/out/commandTreeParsingBukkit.js +116 -0
  17. package/out/commandTreeSuggestions.js +142 -0
  18. package/out/completionBackend.js +94 -0
  19. package/out/completionEngine.js +376 -0
  20. package/out/completionQueries.js +86 -0
  21. package/out/completionsBackend.js +97 -0
  22. package/out/connectionManager.js +209 -0
  23. package/out/displayArgumentHint.js +43 -0
  24. package/out/displayCommandTree.js +115 -0
  25. package/out/displaySuggestion.js +282 -0
  26. package/out/extension.js +190 -0
  27. package/out/helpTextParsing.js +445 -0
  28. package/out/historySearch.js +46 -0
  29. package/out/historyStore.js +126 -0
  30. package/out/lineEditor.js +525 -0
  31. package/out/localCommandTree.js +541 -0
  32. package/out/logger.js +14 -0
  33. package/out/minercon +253 -0
  34. package/out/pager.js +168 -0
  35. package/out/pagination.js +142 -0
  36. package/out/rconClient.js +97 -0
  37. package/out/rconConnectionManager.js +238 -0
  38. package/out/rconProtocol.js +421 -0
  39. package/out/rconSession.js +920 -0
  40. package/out/rconTerminal.js +80 -0
  41. package/out/suggestionDisplay.js +286 -0
  42. package/out/terminalOutput.js +110 -0
  43. package/out/unpaginate.js +30 -0
  44. package/package.json +138 -0
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ // src/connectionManager.ts
3
+ //
4
+ // Owns the RCON connection lifecycle: the live `RconController` (recreated on
5
+ // each reconnect attempt), connection/reconnection status, and the
6
+ // exponential-backoff retry loop. Pulled out of RconTerminal as part of the
7
+ // mega-module split — see lineEditor.ts / suggestionDisplay.ts for the sibling
8
+ // extractions and their rationale.
9
+ //
10
+ // `RconSession` keeps `detectAndInitialize`/`initializeCommands` (they're
11
+ // about the autocomplete command-tree and `pluginMode`, a different concern
12
+ // that happens to run at similar times) but reads connection status and reaches
13
+ // the live controller through this class, and is notified via `onReconnected`
14
+ // when it should reload the command tree.
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.ConnectionManager = void 0;
50
+ const rconClient_1 = require("./rconClient");
51
+ const logger_1 = require("./logger");
52
+ const ansi = __importStar(require("./ansi"));
53
+ const defaultControllerFactory = (host, port, password, logger) => new rconClient_1.RconController(host, port, password, logger);
54
+ class ConnectionManager {
55
+ serverHost;
56
+ serverPort;
57
+ password;
58
+ logger;
59
+ host;
60
+ controllerFactory;
61
+ _controller;
62
+ _isConnected = true;
63
+ _isReconnecting = false;
64
+ reconnectAttempts = 0;
65
+ maxReconnectAttempts = 5;
66
+ reconnectDelay = 2000;
67
+ reconnectTimeout = null;
68
+ constructor(serverHost, serverPort, password, logger, controller, host, controllerFactory = defaultControllerFactory) {
69
+ this.serverHost = serverHost;
70
+ this.serverPort = serverPort;
71
+ this.password = password;
72
+ this.logger = logger;
73
+ this.host = host;
74
+ this.controllerFactory = controllerFactory;
75
+ this._controller = controller;
76
+ }
77
+ get controller() {
78
+ return this._controller;
79
+ }
80
+ get isConnected() {
81
+ return this._isConnected;
82
+ }
83
+ get isReconnecting() {
84
+ return this._isReconnecting;
85
+ }
86
+ /** Resets the reconnect-attempt counter, backoff delay, and any pending reconnect timer back to their initial state. */
87
+ resetReconnectState() {
88
+ this.reconnectAttempts = 0;
89
+ this.reconnectDelay = 2000;
90
+ if (this.reconnectTimeout) {
91
+ clearTimeout(this.reconnectTimeout);
92
+ this.reconnectTimeout = null;
93
+ }
94
+ }
95
+ /**
96
+ * Records that an in-flight command discovered the connection is gone, and
97
+ * schedules an auto-reconnect attempt shortly after. The "Connection
98
+ * lost..." message itself stays with the caller (`executeCommand` accounts
99
+ * for it in its output-line bookkeeping).
100
+ */
101
+ reportConnectionLost() {
102
+ this._isConnected = false;
103
+ this.resetReconnectState();
104
+ this.reconnectTimeout = setTimeout(() => {
105
+ this.reconnectTimeout = null;
106
+ this.attemptReconnect();
107
+ }, 1000);
108
+ }
109
+ disconnect() {
110
+ // No key-chord echo here: this runs for the typed /disconnect built-in.
111
+ // Ctrl+D echoes its own ^D in RconSession.handleCtrlD before closing.
112
+ this.host.write('Disconnecting...\r\n');
113
+ // Clear any pending reconnect
114
+ if (this.reconnectTimeout) {
115
+ clearTimeout(this.reconnectTimeout);
116
+ this.reconnectTimeout = null;
117
+ }
118
+ try {
119
+ this._controller.disconnect();
120
+ }
121
+ catch (err) {
122
+ this.logger.error(`Error during disconnect: ${err}`);
123
+ }
124
+ this._isConnected = false;
125
+ this._isReconnecting = false;
126
+ this.host.write('Connection closed. Type ' + ansi.yellow('/reconnect') + ' to reconnect.\r\n\r\n');
127
+ this.host.showPrompt();
128
+ }
129
+ async manualReconnect() {
130
+ if (this._isReconnecting) {
131
+ this.host.write(ansi.yellow('Already reconnecting...') + '\r\n\r\n');
132
+ this.host.showPrompt();
133
+ return;
134
+ }
135
+ this.resetReconnectState();
136
+ await this.attemptReconnect();
137
+ }
138
+ async attemptReconnect() {
139
+ if (this._isReconnecting) {
140
+ return;
141
+ }
142
+ if (this._isConnected) {
143
+ this.host.write(ansi.green('Already connected.') + '\r\n\r\n');
144
+ this.host.showPrompt();
145
+ return;
146
+ }
147
+ this._isReconnecting = true;
148
+ this.reconnectAttempts++;
149
+ const attemptText = this.reconnectAttempts > 1 ? ` (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})` : '';
150
+ this.host.write(ansi.yellow('Reconnecting to ' + this.serverHost + ':' + this.serverPort + attemptText + '...') + '\r\n');
151
+ try {
152
+ // Disconnect existing controller
153
+ try {
154
+ await this._controller.disconnect();
155
+ }
156
+ catch (err) {
157
+ // Ignore disconnect errors during reconnect
158
+ }
159
+ // Create new controller
160
+ this._controller = this.controllerFactory(this.serverHost, this.serverPort, this.password, this.logger);
161
+ await this._controller.connect();
162
+ this._isConnected = true;
163
+ this._isReconnecting = false;
164
+ this.resetReconnectState();
165
+ this.host.write(ansi.boldGreen('✓ Reconnected successfully!') + '\r\n\r\n');
166
+ // Reload commands after reconnection
167
+ this.host.onReconnected();
168
+ }
169
+ catch (err) {
170
+ this._isReconnecting = false;
171
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
172
+ this.host.write(ansi.red('✗ Connection failed: ' + (0, logger_1.errorMessage)(err)) + '\r\n');
173
+ this.host.write(ansi.yellow('Retrying in ' + (this.reconnectDelay / 1000) + ' seconds...') + '\r\n');
174
+ // Clear any existing timeout
175
+ if (this.reconnectTimeout) {
176
+ clearTimeout(this.reconnectTimeout);
177
+ }
178
+ this.reconnectTimeout = setTimeout(() => {
179
+ this.reconnectTimeout = null;
180
+ this.attemptReconnect();
181
+ }, this.reconnectDelay);
182
+ // Exponential backoff with max delay
183
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, 32000);
184
+ }
185
+ else {
186
+ // Max attempts reached
187
+ this.host.write(ansi.boldRed('✗ Reconnection failed after ' + this.maxReconnectAttempts + ' attempts.') + '\r\n');
188
+ this.host.write('Type ' + ansi.yellow('/reconnect') + ' to try again.\r\n\r\n');
189
+ this.resetReconnectState();
190
+ this.host.showPrompt();
191
+ }
192
+ }
193
+ }
194
+ /** Tears down any pending reconnect timer and disconnects the controller — used by `RconSession.close()`. */
195
+ dispose() {
196
+ if (this.reconnectTimeout) {
197
+ clearTimeout(this.reconnectTimeout);
198
+ this.reconnectTimeout = null;
199
+ }
200
+ try {
201
+ this._controller.disconnect();
202
+ }
203
+ catch (err) {
204
+ this.logger.error(`Error during close: ${err}`);
205
+ }
206
+ }
207
+ }
208
+ exports.ConnectionManager = ConnectionManager;
209
+ //# sourceMappingURL=connectionManager.js.map
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ // src/displayArgumentHint.ts
3
+ //
4
+ // Pure formatting for the "argument hint" display: given a command's usage
5
+ // string (e.g. "gamemode <mode> [<target>]") and the line the user has typed
6
+ // so far, work out which argument position they're at and which token in the
7
+ // usage string corresponds to it (so it can be highlighted).
8
+ //
9
+ // This is presentation logic, not decision logic — it has no notion of
10
+ // fetching, timing, or staleness — so it lives apart from completionEngine's
11
+ // state machine, as its own pure, independently-testable function.
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.formatArgumentHint = formatArgumentHint;
14
+ const commandLine_1 = require("./commandLine");
15
+ const ARGUMENT_TOKEN_PATTERN = /(<[^>]+>|\[[^\]]+\]|\([^)]+\))/g;
16
+ /** Returns null when there's no usage text to show anything for. */
17
+ function formatArgumentHint(usage, line) {
18
+ if (!usage) {
19
+ return null;
20
+ }
21
+ const tokens = usage.match(ARGUMENT_TOKEN_PATTERN) || [];
22
+ // The literal words before the first argument token are the command path —
23
+ // derived straight from the usage string so we don't need a separate
24
+ // "commandPath" concept threaded in from wherever the usage came from.
25
+ const firstTokenStart = usage.search(ARGUMENT_TOKEN_PATTERN);
26
+ const literalPrefix = (firstTokenStart >= 0 ? usage.slice(0, firstTokenStart) : usage).trim();
27
+ const commandPrefixWordCount = literalPrefix.length > 0 ? literalPrefix.split(/\s+/).length : 0;
28
+ const commandPrefixText = '/' + literalPrefix;
29
+ const { parts, hasTrailingSpace } = (0, commandLine_1.splitCommandLine)(line);
30
+ const argumentCount = Math.max(0, parts.length - commandPrefixWordCount);
31
+ let currentArgIndex;
32
+ if (argumentCount === 0 && !hasTrailingSpace) {
33
+ currentArgIndex = -1; // still typing the command/subcommand itself
34
+ }
35
+ else if (hasTrailingSpace) {
36
+ currentArgIndex = argumentCount; // ready for the next argument
37
+ }
38
+ else {
39
+ currentArgIndex = argumentCount - 1; // currently typing this argument
40
+ }
41
+ return { commandPrefixText, tokens, currentArgIndex };
42
+ }
43
+ //# sourceMappingURL=displayArgumentHint.js.map
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ // src/displayCommandTree.ts
3
+ //
4
+ // Formats the command tree built by CommandTreeCrawler for human inspection.
5
+ // Used by the .tree builtin.
6
+ //
7
+ // Output is one "usage line" per unique path through the tree — the same
8
+ // style Minecraft's /help produces. A deep CHOICE_LIST (choices that have
9
+ // their own sub-parameters, produced by buildParameterStructureFromVariants
10
+ // for multi-variant commands) forks into one line per choice. A simple
11
+ // CHOICE_LIST (inline `(a|b|c)` literals parsed from a single `(...)` token)
12
+ // is collapsed onto the same line with bracket/pipe notation. Optional parameters are
13
+ // shown in brackets rather than expanded, so the line count stays bounded.
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.formatCommandTree = formatCommandTree;
16
+ exports.formatCommandLog = formatCommandLog;
17
+ const commandTree_1 = require("./commandTree");
18
+ const ansi_1 = require("./ansi");
19
+ /** A parameter's nested parameters, or `[]` for the leaf types that have none. */
20
+ function childMembers(p) {
21
+ return p.type === commandTree_1.ParameterType.SUBCOMMAND ? p.members : [];
22
+ }
23
+ const MAX_LINES = 300;
24
+ function formatCommandTree(rootCommands, commandName) {
25
+ if (commandName) {
26
+ const name = commandName.startsWith('/') ? commandName.slice(1) : commandName;
27
+ const node = rootCommands.get(name);
28
+ if (!node) {
29
+ return `Unknown command: /${name}\n`;
30
+ }
31
+ const lines = [];
32
+ walkSequential(`/${name}`, node.members, lines);
33
+ if (lines.length === 0) {
34
+ lines.push(`/${name}`);
35
+ }
36
+ if (lines.length >= MAX_LINES) {
37
+ lines.push('(truncated)');
38
+ }
39
+ return lines.join('\n') + '\n';
40
+ }
41
+ const names = [...rootCommands.keys()].sort((a, b) => {
42
+ const aNs = a.includes(':'), bNs = b.includes(':');
43
+ if (aNs !== bNs) {
44
+ return aNs ? 1 : -1;
45
+ }
46
+ return a < b ? -1 : a > b ? 1 : 0;
47
+ });
48
+ const lines = [`${names.length} commands:`];
49
+ const COL_WIDTH = 22, COLS = 4;
50
+ for (let i = 0; i < names.length; i += COLS) {
51
+ lines.push(' ' + names.slice(i, i + COLS).map(n => ('/' + n).padEnd(COL_WIDTH)).join('').trimEnd());
52
+ }
53
+ return lines.join('\n') + '\n';
54
+ }
55
+ /**
56
+ * Walks `members` sequentially, appending tokens to `prefix` and collecting
57
+ * complete usage lines into `out`. A deep CHOICE_LIST (any choice has
58
+ * sub-members) forks — one recursion per choice, each picking up the
59
+ * remaining siblings afterward. Simple CHOICE_LIST and all other parameter
60
+ * types are rendered as a single token and appended in-line.
61
+ */
62
+ function walkSequential(prefix, members, out) {
63
+ if (out.length >= MAX_LINES) {
64
+ return;
65
+ }
66
+ if (members.length === 0) {
67
+ out.push(prefix);
68
+ return;
69
+ }
70
+ const [first, ...rest] = members;
71
+ if (first.type === commandTree_1.ParameterType.CHOICE_LIST && first.choices.some(c => childMembers(c).length)) {
72
+ // Deep alternation: fork one line per choice.
73
+ for (const choice of first.choices) {
74
+ if (out.length >= MAX_LINES) {
75
+ return;
76
+ }
77
+ walkSequential(`${prefix} ${paramToken(choice)}`, [...childMembers(choice), ...rest], out);
78
+ }
79
+ // If the whole CHOICE_LIST is optional, also emit the path that skips it.
80
+ if (first.optional) {
81
+ walkSequential(prefix, rest, out);
82
+ }
83
+ }
84
+ else {
85
+ // Sequential: append this token and recurse into sub-members then siblings.
86
+ const token = paramToken(first);
87
+ walkSequential(`${prefix} ${token}`, [...childMembers(first), ...rest], out);
88
+ }
89
+ }
90
+ function formatCommandLog(pairs) {
91
+ if (pairs.length === 0) {
92
+ return '';
93
+ }
94
+ const lines = ['', (0, ansi_1.dim)('── send/recv log ─────────────────────────')];
95
+ for (const { send, recv } of pairs) {
96
+ lines.push((0, ansi_1.cyan)(`> ${send}`));
97
+ const recvText = (0, ansi_1.stripColors)(recv).trimEnd();
98
+ for (const line of recvText.split('\n')) {
99
+ lines.push((0, ansi_1.gray)(` ${line}`));
100
+ }
101
+ }
102
+ return lines.join('\n') + '\n';
103
+ }
104
+ function paramToken(p) {
105
+ switch (p.type) {
106
+ case commandTree_1.ParameterType.ARGUMENT: return p.optional ? `[<${p.name}>]` : `<${p.name}>`;
107
+ case commandTree_1.ParameterType.LITERAL: return p.optional ? `[${p.literal}]` : p.literal;
108
+ case commandTree_1.ParameterType.SUBCOMMAND: return p.optional ? `[${p.name}]` : p.name;
109
+ case commandTree_1.ParameterType.CHOICE_LIST: {
110
+ const inner = (0, commandTree_1.parameterLabel)(p);
111
+ return p.optional ? `[(${inner})]` : `(${inner})`;
112
+ }
113
+ }
114
+ }
115
+ //# sourceMappingURL=displayCommandTree.js.map
@@ -0,0 +1,282 @@
1
+ "use strict";
2
+ // src/displaySuggestion.ts
3
+ //
4
+ // Owns the suggestion-list / argument-hint display: the items, the selected
5
+ // index, paging state, and the ANSI rendering/clearing of whatever's drawn
6
+ // below the prompt. Pulled out of RconTerminal as part of the mega-module
7
+ // split — see lineEditor.ts for the sibling extraction and its rationale.
8
+ //
9
+ // This class has no notion of the completionEngine or of dispatching events —
10
+ // RconSession remains the single place that talks to the engine (it's the
11
+ // one piece deliberately kept pure/central). Callers that need to know
12
+ // *whether* an action makes sense (page forward/back) ask the read-only
13
+ // `...Index()` queries, which return `number | null` — `null` meaning
14
+ // "nothing to do" — and decide for themselves whether to dispatch.
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.SuggestionDisplay = void 0;
50
+ const displayArgumentHint_1 = require("./displayArgumentHint");
51
+ const ansi = __importStar(require("./ansi"));
52
+ class SuggestionDisplay {
53
+ host;
54
+ currentSuggestions = [];
55
+ suggestionIndex = -1;
56
+ showing = false;
57
+ displayLines = 0;
58
+ // Paging
59
+ visibleStart = 0;
60
+ maxVisible = 10;
61
+ needsClearOnNextRender = false;
62
+ constructor(host) {
63
+ this.host = host;
64
+ }
65
+ get isShowing() {
66
+ return this.showing;
67
+ }
68
+ get itemCount() {
69
+ return this.currentSuggestions.length;
70
+ }
71
+ get totalPages() {
72
+ return Math.ceil(this.currentSuggestions.length / this.maxVisible);
73
+ }
74
+ /** The 1-based page the selected item sits on (only meaningful while a list is showing). */
75
+ get currentPage() {
76
+ return Math.floor(this.suggestionIndex / this.maxVisible) + 1;
77
+ }
78
+ /** Replaces the direct `needsClearBeforeSuggestions = true` assignment from `executeCommand` — the next render should wipe the screen below the cursor first (e.g. after a multi-line command response may have pushed the display area around). */
79
+ markNeedsClearOnNextRender() {
80
+ this.needsClearOnNextRender = true;
81
+ }
82
+ nextPageIndex() {
83
+ if (!this.showing || this.currentSuggestions.length === 0 || this.totalPages <= 1) {
84
+ return null;
85
+ }
86
+ const nextPageStart = this.currentPage * this.maxVisible;
87
+ return nextPageStart < this.currentSuggestions.length ? nextPageStart : 0;
88
+ }
89
+ previousPageIndex() {
90
+ if (!this.showing || this.currentSuggestions.length === 0 || this.totalPages <= 1) {
91
+ return null;
92
+ }
93
+ return this.currentPage > 1
94
+ ? (this.currentPage - 2) * this.maxVisible
95
+ : (this.totalPages - 1) * this.maxVisible;
96
+ }
97
+ /**
98
+ * Sets the displayed items/selection from the engine's `render` effect and
99
+ * draws the appropriate combination of suggestion list and argument hint —
100
+ * `usage` is only ever a single, resolved usage line (the engine collapses
101
+ * "(too broad...)" failures and ambiguous-prefix multi-candidate responses
102
+ * down to empty), so a non-null `display` here means the command portion is
103
+ * fully resolved — independent of how many argument-level completions
104
+ * remain, so it's shown alongside the list whenever available.
105
+ */
106
+ render(items, selectedIndex, usage, currentLine) {
107
+ this.currentSuggestions = items;
108
+ this.suggestionIndex = selectedIndex;
109
+ this.clear();
110
+ const display = usage ? (0, displayArgumentHint_1.formatArgumentHint)(usage, currentLine) : null;
111
+ let lines = [];
112
+ if (items.length > 0) {
113
+ this.showing = true;
114
+ lines = this.buildSuggestionListLines(currentLine);
115
+ if (display) {
116
+ lines = lines.concat(this.buildArgumentHintLines(display));
117
+ }
118
+ }
119
+ else {
120
+ this.showing = false;
121
+ if (display) {
122
+ lines = this.buildArgumentHintLines(display);
123
+ }
124
+ }
125
+ this.renderSuggestionArea(lines);
126
+ }
127
+ hide() {
128
+ this.clear();
129
+ this.showing = false;
130
+ this.suggestionIndex = -1;
131
+ this.currentSuggestions = [];
132
+ this.visibleStart = 0;
133
+ }
134
+ /** Erases whatever's currently drawn in the display area — the single source of truth for "clear the old frame before drawing (or removing) the new one." */
135
+ clear() {
136
+ if (this.displayLines === 0) {
137
+ return;
138
+ }
139
+ this.host.write('\r\n'); // Move to the display area
140
+ for (let i = 0; i < this.displayLines; i++) {
141
+ this.host.write('\x1b[2K'); // Clear entire line
142
+ if (i < this.displayLines - 1) {
143
+ this.host.write('\r\n');
144
+ }
145
+ }
146
+ this.returnCursorToPrompt(this.displayLines);
147
+ this.displayLines = 0;
148
+ }
149
+ /**
150
+ * Returns the cursor to the prompt after drawing or erasing `lineCount` lines
151
+ * in the area below it: up `lineCount` rows, back to column 0, then out to the
152
+ * prompt's column. Uses a *relative* cursor-up (`\x1b[NA`) rather than an
153
+ * absolute save/restore (`\x1b7`/`\x1b8`), because the `\r\n`s used to enter
154
+ * the area may have scrolled the terminal and invalidated any saved row.
155
+ */
156
+ returnCursorToPrompt(lineCount) {
157
+ this.host.write(`\x1b[${lineCount}A`);
158
+ this.host.write('\r');
159
+ const col = this.host.cursorColumn();
160
+ if (col > 0) {
161
+ this.host.write(`\x1b[${col}C`);
162
+ }
163
+ }
164
+ /**
165
+ * Builds the suggestion list's content lines — fully ANSI-styled, but with
166
+ * no `\x1b[2K`/`\r\n` baked in. `renderSuggestionArea` is the single place
167
+ * that turns a list of content strings into clear-and-draw ANSI, whether
168
+ * it's drawing the list alone, the hint alone, or (when there's exactly one
169
+ * suggestion left) both stacked together in one frame.
170
+ */
171
+ buildSuggestionListLines(currentLine) {
172
+ // Calculate the visible window based on the selected index
173
+ this.updateVisibleWindow();
174
+ const lines = [];
175
+ // Get only the completed parts of the command (everything before the last space or the whole line if no space)
176
+ let completedText = '';
177
+ if (currentLine.includes(' ')) {
178
+ // If there's a space, get everything up to and including the last space
179
+ const lastSpaceIndex = currentLine.lastIndexOf(' ');
180
+ completedText = currentLine.substring(0, lastSpaceIndex + 1);
181
+ }
182
+ const prefix = completedText ? ansi.hidden(completedText) : '';
183
+ // Show indicator if there are items above the visible window
184
+ if (this.visibleStart > 0) {
185
+ lines.push(prefix + ansi.gray(' ▲ (' + this.visibleStart + ' more above)'));
186
+ }
187
+ // Show visible suggestions in vertical list
188
+ const visibleEnd = Math.min(this.visibleStart + this.maxVisible, this.currentSuggestions.length);
189
+ for (let i = this.visibleStart; i < visibleEnd; i++) {
190
+ // Show selection indicator and item
191
+ if (i === this.suggestionIndex) {
192
+ // Yellow for selected item with arrow indicator
193
+ lines.push(prefix + ansi.brightYellow('→ ' + this.currentSuggestions[i]));
194
+ }
195
+ else {
196
+ // Gray for other items with space for alignment
197
+ lines.push(prefix + ansi.gray(' ' + this.currentSuggestions[i]));
198
+ }
199
+ }
200
+ // Show indicator if there are items below the visible window
201
+ if (visibleEnd < this.currentSuggestions.length) {
202
+ const remaining = this.currentSuggestions.length - visibleEnd;
203
+ lines.push(prefix + ansi.gray(' ▼ (' + remaining + ' more below)'));
204
+ }
205
+ // Show current position and page indicator at bottom
206
+ lines.push(prefix + ansi.gray(' [' + (this.suggestionIndex + 1) + '/' + this.currentSuggestions.length + '] ' +
207
+ 'Page ' + this.currentPage + '/' + this.totalPages));
208
+ return lines;
209
+ }
210
+ /**
211
+ * Builds the argument-hint's content lines: the usage line, shown in full
212
+ * and literally — with the argument the user is currently editing bolded
213
+ * and everything else (command prefix and other tokens alike) gray. Same
214
+ * "fully-styled content strings, no \x1b[2K/\r\n" convention as
215
+ * `buildSuggestionListLines`, so `renderSuggestionArea` can draw either or
216
+ * both in one frame.
217
+ *
218
+ * Shown alongside or in place of the suggestion list when a command has
219
+ * argument structure worth showing — e.g. "/gamemode creative " (nothing
220
+ * left to complete for the target selector, but worth showing what comes
221
+ * next), or "/gamemode cr" (one match left — "creative" — where seeing the
222
+ * full usage helps confirm that's the right command to commit to). The
223
+ * actual parsing of `usage`/`line` into positions and hint text is pure —
224
+ * see displayArgumentHint.ts — this is just the ANSI rendering of that
225
+ * already-computed structure.
226
+ */
227
+ buildArgumentHintLines(display) {
228
+ let usageLine = ' ' + ansi.gray(display.commandPrefixText);
229
+ for (let i = 0; i < display.tokens.length; i++) {
230
+ usageLine += ' ';
231
+ usageLine += (i === display.currentArgIndex)
232
+ ? ansi.boldBrightWhite(display.tokens[i])
233
+ : ansi.gray(display.tokens[i]);
234
+ }
235
+ return [usageLine];
236
+ }
237
+ /**
238
+ * Draws a single frame of suggestion-area content — one save/clear/restore
239
+ * cycle, regardless of whether `lines` came from the list, the hint, or
240
+ * both stacked together. `clear` (called centrally before this, by `render`)
241
+ * has already erased whatever was there before, so this only ever draws
242
+ * onto a blank area — each line gets its own `\x1b[2K` defensively, but
243
+ * there's no "clear the old N lines" dance to duplicate here.
244
+ */
245
+ renderSuggestionArea(lines) {
246
+ if (lines.length === 0) {
247
+ return;
248
+ }
249
+ if (this.needsClearOnNextRender) {
250
+ this.host.write('\x1b[J'); // Clear from cursor to end of screen
251
+ this.needsClearOnNextRender = false;
252
+ }
253
+ this.host.write('\r\n'); // Move to the display area
254
+ for (let i = 0; i < lines.length; i++) {
255
+ this.host.write('\x1b[2K'); // Clear line first
256
+ this.host.write(lines[i]);
257
+ if (i < lines.length - 1) {
258
+ this.host.write('\r\n');
259
+ }
260
+ }
261
+ this.displayLines = lines.length;
262
+ this.returnCursorToPrompt(lines.length);
263
+ }
264
+ updateVisibleWindow() {
265
+ // Keep a buffer of 2 items above and below the selected item when possible
266
+ const buffer = 2;
267
+ // Update the visible window
268
+ if (this.suggestionIndex < this.visibleStart + buffer) {
269
+ // Scrolling up
270
+ this.visibleStart = Math.max(0, this.suggestionIndex - buffer);
271
+ }
272
+ else if (this.suggestionIndex >= this.visibleStart + this.maxVisible - buffer) {
273
+ // Scrolling down
274
+ this.visibleStart = Math.min(this.suggestionIndex - this.maxVisible + buffer + 1, Math.max(0, this.currentSuggestions.length - this.maxVisible));
275
+ }
276
+ // Final boundary check
277
+ this.visibleStart = Math.max(0, this.visibleStart);
278
+ this.visibleStart = Math.min(this.visibleStart, Math.max(0, this.currentSuggestions.length - this.maxVisible));
279
+ }
280
+ }
281
+ exports.SuggestionDisplay = SuggestionDisplay;
282
+ //# sourceMappingURL=displaySuggestion.js.map