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.
- package/CHANGELOG.md +439 -0
- package/LICENSE +22 -0
- package/README.md +401 -0
- package/images/icon.png +0 -0
- package/out/ansi.js +123 -0
- package/out/argumentHint.js +43 -0
- package/out/bukkitHelpParsing.js +62 -0
- package/out/cli.js +253 -0
- package/out/cliConfig.js +141 -0
- package/out/commandLine.js +28 -0
- package/out/commandSuggestions.js +202 -0
- package/out/commandTree.js +46 -0
- package/out/commandTreeCache.js +171 -0
- package/out/commandTreeCrawler.js +583 -0
- package/out/commandTreeParsingBrigadier.js +426 -0
- package/out/commandTreeParsingBukkit.js +116 -0
- package/out/commandTreeSuggestions.js +142 -0
- package/out/completionBackend.js +94 -0
- package/out/completionEngine.js +376 -0
- package/out/completionQueries.js +86 -0
- package/out/completionsBackend.js +97 -0
- package/out/connectionManager.js +209 -0
- package/out/displayArgumentHint.js +43 -0
- package/out/displayCommandTree.js +115 -0
- package/out/displaySuggestion.js +282 -0
- package/out/extension.js +190 -0
- package/out/helpTextParsing.js +445 -0
- package/out/historySearch.js +46 -0
- package/out/historyStore.js +126 -0
- package/out/lineEditor.js +525 -0
- package/out/localCommandTree.js +541 -0
- package/out/logger.js +14 -0
- package/out/minercon +253 -0
- package/out/pager.js +168 -0
- package/out/pagination.js +142 -0
- package/out/rconClient.js +97 -0
- package/out/rconConnectionManager.js +238 -0
- package/out/rconProtocol.js +421 -0
- package/out/rconSession.js +920 -0
- package/out/rconTerminal.js +80 -0
- package/out/suggestionDisplay.js +286 -0
- package/out/terminalOutput.js +110 -0
- package/out/unpaginate.js +30 -0
- package/package.json +138 -0
package/out/extension.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.activate = activate;
|
|
37
|
+
exports.deactivate = deactivate;
|
|
38
|
+
// src/extension.ts
|
|
39
|
+
const vscode = __importStar(require("vscode"));
|
|
40
|
+
const consola_1 = require("consola");
|
|
41
|
+
const logger_1 = require("./logger");
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
function activate(context) {
|
|
44
|
+
const logger = (0, consola_1.createConsola)({});
|
|
45
|
+
let currentConnection = null;
|
|
46
|
+
migratePasswordToSecureStorage(context).catch(err => {
|
|
47
|
+
logger.warn(`Password migration warning: ${err}`);
|
|
48
|
+
});
|
|
49
|
+
// Register the terminal profile provider
|
|
50
|
+
context.subscriptions.push(vscode.window.registerTerminalProfileProvider('minercon.terminal', {
|
|
51
|
+
provideTerminalProfile: async (token) => {
|
|
52
|
+
const { profile, connectionInfo } = await createRconTerminalProfile(context);
|
|
53
|
+
currentConnection = connectionInfo;
|
|
54
|
+
profile.options.iconPath = {
|
|
55
|
+
light: vscode.Uri.file(path.join(context.extensionPath, 'images', 'light.png')),
|
|
56
|
+
dark: vscode.Uri.file(path.join(context.extensionPath, 'images', 'dark.png'))
|
|
57
|
+
};
|
|
58
|
+
return profile;
|
|
59
|
+
}
|
|
60
|
+
}));
|
|
61
|
+
const connectCommand = vscode.commands.registerCommand('minercon.connect', async () => {
|
|
62
|
+
const connectionInfo = await connectToRcon(logger, context);
|
|
63
|
+
if (connectionInfo) {
|
|
64
|
+
currentConnection = connectionInfo;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const connectNewCommand = vscode.commands.registerCommand('minercon.connectNew', async () => {
|
|
68
|
+
const connectionInfo = await connectToRcon(logger, context, false);
|
|
69
|
+
if (connectionInfo) {
|
|
70
|
+
currentConnection = connectionInfo;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
const saveDefaultsCommand = vscode.commands.registerCommand('minercon.saveDefaults', async () => {
|
|
74
|
+
if (!currentConnection) {
|
|
75
|
+
vscode.window.showWarningMessage('No active connection to save');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const config = vscode.workspace.getConfiguration('minercon');
|
|
79
|
+
try {
|
|
80
|
+
await config.update('defaultHost', currentConnection.host, vscode.ConfigurationTarget.Global);
|
|
81
|
+
await config.update('defaultPort', currentConnection.port, vscode.ConfigurationTarget.Global);
|
|
82
|
+
await context.secrets.store('minercon.defaultPassword', currentConnection.password);
|
|
83
|
+
vscode.window.showInformationMessage(`Saved connection settings for ${currentConnection.host}:${currentConnection.port} as defaults`);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
vscode.window.showErrorMessage(`Failed to save defaults: ${err}`);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
context.subscriptions.push(connectCommand, connectNewCommand, saveDefaultsCommand);
|
|
90
|
+
}
|
|
91
|
+
async function migratePasswordToSecureStorage(context) {
|
|
92
|
+
const config = vscode.workspace.getConfiguration('minercon');
|
|
93
|
+
const oldPassword = config.get('defaultPassword');
|
|
94
|
+
if (oldPassword && oldPassword !== '') {
|
|
95
|
+
await context.secrets.store('minercon.defaultPassword', oldPassword);
|
|
96
|
+
await config.update('defaultPassword', undefined, vscode.ConfigurationTarget.Global);
|
|
97
|
+
vscode.window.showWarningMessage('🔒 Your RCON password has been migrated to secure storage', 'OK');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function createRconTerminalProfile(context, useDefaults = true) {
|
|
101
|
+
const config = vscode.workspace.getConfiguration('minercon');
|
|
102
|
+
const defaultHost = config.get('defaultHost');
|
|
103
|
+
const defaultPort = config.get('defaultPort');
|
|
104
|
+
const defaultPassword = await context.secrets.get('minercon.defaultPassword');
|
|
105
|
+
const historySize = config.get('historySize', 100);
|
|
106
|
+
const unpaginateOutput = config.get('unpaginateOutput', true);
|
|
107
|
+
const terminalPager = config.get('terminalPager', true);
|
|
108
|
+
let host;
|
|
109
|
+
let port;
|
|
110
|
+
let password;
|
|
111
|
+
if (useDefaults && defaultHost && defaultPort && defaultPassword) {
|
|
112
|
+
host = defaultHost;
|
|
113
|
+
port = defaultPort;
|
|
114
|
+
password = defaultPassword;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
host = await vscode.window.showInputBox({
|
|
118
|
+
prompt: 'RCON Host', value: defaultHost ?? '127.0.0.1', placeHolder: 'e.g., 127.0.0.1 or mc.example.com'
|
|
119
|
+
});
|
|
120
|
+
if (!host) {
|
|
121
|
+
throw new Error('Host is required');
|
|
122
|
+
}
|
|
123
|
+
const portInput = await vscode.window.showInputBox({
|
|
124
|
+
prompt: 'RCON Port', value: String(defaultPort ?? 25575), placeHolder: 'e.g., 25575'
|
|
125
|
+
});
|
|
126
|
+
if (!portInput) {
|
|
127
|
+
throw new Error('Port is required');
|
|
128
|
+
}
|
|
129
|
+
port = parseInt(portInput, 10);
|
|
130
|
+
// Cancelling the box (undefined) aborts like host/port do; an explicitly
|
|
131
|
+
// empty password (plain Enter) is allowed — some servers use one.
|
|
132
|
+
const passwordInput = await vscode.window.showInputBox({
|
|
133
|
+
prompt: 'RCON Password', password: true, value: defaultPassword ?? '', placeHolder: 'Enter your server RCON password'
|
|
134
|
+
});
|
|
135
|
+
if (passwordInput === undefined) {
|
|
136
|
+
throw new Error('Password is required');
|
|
137
|
+
}
|
|
138
|
+
password = passwordInput;
|
|
139
|
+
}
|
|
140
|
+
const minerconPath = context.asAbsolutePath(path.join('dist', 'minercon.js'));
|
|
141
|
+
const profile = new vscode.TerminalProfile({
|
|
142
|
+
name: `RCON: ${host}:${port}`,
|
|
143
|
+
// Run the built minercon CLI as the terminal's process. process.execPath
|
|
144
|
+
// is the Node runtime bundled with VS Code itself — ELECTRON_RUN_AS_NODE
|
|
145
|
+
// makes it execute minerconPath as a plain node script rather than
|
|
146
|
+
// launching another VS Code window, so this works without requiring the
|
|
147
|
+
// user to have node on their PATH.
|
|
148
|
+
shellPath: process.execPath,
|
|
149
|
+
shellArgs: [minerconPath, host, String(port)],
|
|
150
|
+
env: {
|
|
151
|
+
ELECTRON_RUN_AS_NODE: '1',
|
|
152
|
+
MCRCON_PASSWORD: password,
|
|
153
|
+
MCRCON_HISTORY_SIZE: String(historySize),
|
|
154
|
+
// Booleans are passed as '0'/'1'; the CLI treats absent as the default-on.
|
|
155
|
+
MCRCON_UNPAGINATE: unpaginateOutput ? '1' : '0',
|
|
156
|
+
MCRCON_PAGER: terminalPager ? '1' : '0',
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
profile,
|
|
161
|
+
connectionInfo: { host, port, password }
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function connectToRcon(logger, context, useDefaults = true) {
|
|
165
|
+
try {
|
|
166
|
+
const { profile, connectionInfo } = await createRconTerminalProfile(context, useDefaults);
|
|
167
|
+
const terminal = vscode.window.createTerminal({
|
|
168
|
+
...profile.options,
|
|
169
|
+
iconPath: {
|
|
170
|
+
light: vscode.Uri.file(path.join(context.extensionPath, 'images', 'light.png')),
|
|
171
|
+
dark: vscode.Uri.file(path.join(context.extensionPath, 'images', 'dark.png'))
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
terminal.show();
|
|
175
|
+
vscode.window.showInformationMessage(`Connecting to Minecraft server...`);
|
|
176
|
+
return connectionInfo;
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
const message = (0, logger_1.errorMessage)(err);
|
|
180
|
+
if (!message.includes('required')) {
|
|
181
|
+
logger.error('Connection failed: ' + message);
|
|
182
|
+
vscode.window.showErrorMessage('RCON connection failed: ' + message);
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function deactivate() {
|
|
188
|
+
// Cleanup will happen automatically
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=extension.js.map
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/helpTextParsing.ts
|
|
3
|
+
//
|
|
4
|
+
// Pure parsing of Minecraft's Brigadier-shaped `/help` output (flat
|
|
5
|
+
// `/cmd <args>` blobs, as returned by `minecraft:help` and vanilla's plain
|
|
6
|
+
// `help`) into a `Parameter` tree, plus the root-listing parser
|
|
7
|
+
// (`parseHelpResponse`) and the one-time namespace-support probe
|
|
8
|
+
// (`isUnsupportedNamespaceError`). No state, no IO — every export here is a
|
|
9
|
+
// deterministic function of its arguments, which is what makes them directly
|
|
10
|
+
// unit-testable without constructing a `LocalCommandTree`.
|
|
11
|
+
//
|
|
12
|
+
// Bukkit's hand-written `Description:`/`Usage:`/`Aliases:` `/help <command>`
|
|
13
|
+
// pages are a different grammar entirely - their extraction lives in
|
|
14
|
+
// `bukkitHelpParsing.ts`.
|
|
15
|
+
//
|
|
16
|
+
// `stripColors` (used throughout to normalize input before parsing) and the
|
|
17
|
+
// `formatMinecraftColors`/ANSI rendering side live in ansi.ts.
|
|
18
|
+
//
|
|
19
|
+
// The `Parameter`/`ParameterType` tree this file builds is defined in
|
|
20
|
+
// `commandTree.ts`.
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.tokenizeParameterString = tokenizeParameterString;
|
|
23
|
+
exports.parseParameter = parseParameter;
|
|
24
|
+
exports.parseCommandHelp = parseCommandHelp;
|
|
25
|
+
exports.classifyParameterTokens = classifyParameterTokens;
|
|
26
|
+
exports.splitConcatenatedHelpLines = splitConcatenatedHelpLines;
|
|
27
|
+
exports.parseAliasRedirect = parseAliasRedirect;
|
|
28
|
+
exports.parseHelpLines = parseHelpLines;
|
|
29
|
+
exports.isGenericArgsPlaceholder = isGenericArgsPlaceholder;
|
|
30
|
+
exports.hasUsableArguments = hasUsableArguments;
|
|
31
|
+
exports.hasRealUsage = hasRealUsage;
|
|
32
|
+
exports.parseHelpResponse = parseHelpResponse;
|
|
33
|
+
exports.isUnsupportedNamespaceError = isUnsupportedNamespaceError;
|
|
34
|
+
exports.buildParameterStructureFromVariants = buildParameterStructureFromVariants;
|
|
35
|
+
const ansi_1 = require("./ansi");
|
|
36
|
+
const commandTree_1 = require("./commandTree");
|
|
37
|
+
/**
|
|
38
|
+
* Tokenize parameter string handling nested structures
|
|
39
|
+
*/
|
|
40
|
+
function tokenizeParameterString(str) {
|
|
41
|
+
const tokens = [];
|
|
42
|
+
let current = '';
|
|
43
|
+
let depth = 0;
|
|
44
|
+
let inBrackets = false;
|
|
45
|
+
for (let i = 0; i < str.length; i++) {
|
|
46
|
+
const char = str[i];
|
|
47
|
+
if ((char === '<' || char === '[' || char === '(')) {
|
|
48
|
+
if (depth === 0) {
|
|
49
|
+
if (current.trim()) {
|
|
50
|
+
// This is a literal
|
|
51
|
+
tokens.push(current.trim());
|
|
52
|
+
current = '';
|
|
53
|
+
}
|
|
54
|
+
inBrackets = true;
|
|
55
|
+
}
|
|
56
|
+
depth++;
|
|
57
|
+
current += char;
|
|
58
|
+
}
|
|
59
|
+
else if ((char === '>' || char === ']' || char === ')')) {
|
|
60
|
+
depth--;
|
|
61
|
+
current += char;
|
|
62
|
+
if (depth === 0) {
|
|
63
|
+
tokens.push(current.trim());
|
|
64
|
+
current = '';
|
|
65
|
+
inBrackets = false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (char === ' ' && depth === 0 && !inBrackets) {
|
|
69
|
+
if (current.trim()) {
|
|
70
|
+
tokens.push(current.trim());
|
|
71
|
+
current = '';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
current += char;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (current.trim()) {
|
|
79
|
+
tokens.push(current.trim());
|
|
80
|
+
}
|
|
81
|
+
return tokens;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Parse a single parameter token
|
|
85
|
+
*/
|
|
86
|
+
function parseParameter(token, position) {
|
|
87
|
+
// Check for choice list (option1|option2|...)
|
|
88
|
+
if (token.startsWith('(') && token.endsWith(')')) {
|
|
89
|
+
const choicesStr = token.slice(1, -1);
|
|
90
|
+
const choices = choicesStr.split('|').map((choice, idx) => ({
|
|
91
|
+
type: commandTree_1.ParameterType.LITERAL,
|
|
92
|
+
literal: choice.trim(),
|
|
93
|
+
optional: false,
|
|
94
|
+
position: idx
|
|
95
|
+
}));
|
|
96
|
+
return {
|
|
97
|
+
type: commandTree_1.ParameterType.CHOICE_LIST,
|
|
98
|
+
choices,
|
|
99
|
+
optional: false,
|
|
100
|
+
position
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// Check for optional argument [name] or [<name>]
|
|
104
|
+
if (token.startsWith('[') && token.endsWith(']')) {
|
|
105
|
+
let name = token.slice(1, -1); // Remove [ and ]
|
|
106
|
+
// Also remove inner angle brackets if present
|
|
107
|
+
if (name.startsWith('<') && name.endsWith('>')) {
|
|
108
|
+
name = name.slice(1, -1); // Remove < and >
|
|
109
|
+
return {
|
|
110
|
+
type: commandTree_1.ParameterType.ARGUMENT,
|
|
111
|
+
name,
|
|
112
|
+
optional: true,
|
|
113
|
+
position
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// For [literal] without angle brackets, this could be an optional subcommand
|
|
117
|
+
// Return as LITERAL but we'll handle it specially in loadCommandDetails
|
|
118
|
+
return {
|
|
119
|
+
type: commandTree_1.ParameterType.LITERAL,
|
|
120
|
+
literal: name, // Store WITHOUT the brackets
|
|
121
|
+
optional: true,
|
|
122
|
+
position
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// Check for required argument <name>
|
|
126
|
+
if (token.startsWith('<') && token.endsWith('>')) {
|
|
127
|
+
const name = token.slice(1, -1);
|
|
128
|
+
return {
|
|
129
|
+
type: commandTree_1.ParameterType.ARGUMENT,
|
|
130
|
+
name,
|
|
131
|
+
optional: false,
|
|
132
|
+
position
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// Otherwise it's a literal (could be a subcommand name)
|
|
136
|
+
// We'll determine if it's actually a subcommand later when we see it has members
|
|
137
|
+
return {
|
|
138
|
+
type: commandTree_1.ParameterType.LITERAL,
|
|
139
|
+
literal: token,
|
|
140
|
+
optional: false,
|
|
141
|
+
position
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Parse command help output to extract parameters
|
|
146
|
+
*/
|
|
147
|
+
function parseCommandHelp(helpText) {
|
|
148
|
+
const parameters = [];
|
|
149
|
+
const stripped = (0, ansi_1.stripColors)(helpText).trim();
|
|
150
|
+
// Remove the command name from the beginning if present
|
|
151
|
+
const syntaxMatch = stripped.match(/^\/?\w+\s+(.*)/);
|
|
152
|
+
const paramString = syntaxMatch ? syntaxMatch[1] : stripped;
|
|
153
|
+
// Split into tokens - handle nested brackets/parens
|
|
154
|
+
const tokens = tokenizeParameterString(paramString);
|
|
155
|
+
tokens.forEach((token, index) => {
|
|
156
|
+
const param = parseParameter(token, index);
|
|
157
|
+
if (param) {
|
|
158
|
+
parameters.push(param);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return parameters;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Classify a tokenized parameter string as either a named subcommand variant
|
|
165
|
+
* or a direct parameter list, parsing the relevant tokens along the way.
|
|
166
|
+
*/
|
|
167
|
+
function classifyParameterTokens(tokens) {
|
|
168
|
+
if (tokens.length === 0) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const firstToken = tokens[0];
|
|
172
|
+
// Determine if first token is a literal/subcommand or an argument
|
|
173
|
+
// FIXED: Better detection for optional subcommands vs optional arguments
|
|
174
|
+
let isArgument = false;
|
|
175
|
+
if (firstToken.startsWith('<')) {
|
|
176
|
+
// <arg> - required argument
|
|
177
|
+
isArgument = true;
|
|
178
|
+
}
|
|
179
|
+
else if (firstToken.startsWith('[') && firstToken.endsWith(']')) {
|
|
180
|
+
// Could be [<arg>] or [subcommand]
|
|
181
|
+
const inner = firstToken.slice(1, -1);
|
|
182
|
+
if (inner.startsWith('<') && inner.endsWith('>')) {
|
|
183
|
+
// [<arg>] - optional argument
|
|
184
|
+
isArgument = true;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// [subcommand] - optional subcommand, treat as subcommand
|
|
188
|
+
isArgument = false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else if (firstToken.startsWith('(') && firstToken.endsWith(')')) {
|
|
192
|
+
// (choice1|choice2) - choice list, treat as argument
|
|
193
|
+
isArgument = true;
|
|
194
|
+
}
|
|
195
|
+
if (isArgument) {
|
|
196
|
+
// Every token is a direct parameter of the command/subcommand itself
|
|
197
|
+
const parameters = [];
|
|
198
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
199
|
+
const param = parseParameter(tokens[i], i);
|
|
200
|
+
if (param) {
|
|
201
|
+
parameters.push(param);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { kind: 'direct', parameters };
|
|
205
|
+
}
|
|
206
|
+
// First token is a literal/subcommand name introducing a variant
|
|
207
|
+
let name = firstToken;
|
|
208
|
+
let optional = false;
|
|
209
|
+
// Strip optional brackets if present
|
|
210
|
+
if (name.startsWith('[') && name.endsWith(']')) {
|
|
211
|
+
name = name.slice(1, -1); // Remove [ and ]
|
|
212
|
+
optional = true;
|
|
213
|
+
}
|
|
214
|
+
const parameters = [];
|
|
215
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
216
|
+
const param = parseParameter(tokens[i], i - 1);
|
|
217
|
+
if (param) {
|
|
218
|
+
parameters.push(param);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { kind: 'variant', name, optional, parameters };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Re-split a help response that packs multiple `/cmd ...` entries onto one
|
|
225
|
+
* line with no separators (e.g. a `minecraft:help` blob, or vanilla's `help
|
|
226
|
+
* <path>` for a multi-variant command like `gamerule`/`team`/`debug`) into
|
|
227
|
+
* one entry per line, ready for `parseHelpResponse`/`parseHelpLines`.
|
|
228
|
+
*
|
|
229
|
+
* Header/separator (`---...`) and blank lines are dropped first, so a
|
|
230
|
+
* `§e--------- §fHelp: /<cmd> §e----...` banner line - which itself contains
|
|
231
|
+
* a `/` - doesn't get re-split into a bogus `/<cmd> ----...` entry.
|
|
232
|
+
*/
|
|
233
|
+
function splitConcatenatedHelpLines(text) {
|
|
234
|
+
return text.split('\n')
|
|
235
|
+
.filter(line => {
|
|
236
|
+
const stripped = (0, ansi_1.stripColors)(line).trim();
|
|
237
|
+
return stripped && !stripped.startsWith('---');
|
|
238
|
+
})
|
|
239
|
+
.join('\n')
|
|
240
|
+
.replace(/\//g, '\n/');
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Parse a single (already `stripColors`'d and trimmed) help line as a
|
|
244
|
+
* vanilla `minecraft:help` alias redirect of the form `/<alias> -> <target>`
|
|
245
|
+
* (e.g. `/tp -> teleport`, `/minecraft:xp -> experience`). Either side may
|
|
246
|
+
* carry a namespace prefix (e.g. `minecraft:`); namespace prefixes are
|
|
247
|
+
* preserved verbatim on both sides - per "ingest everything", a namespaced
|
|
248
|
+
* alias like `minecraft:tp` is its own root command, not folded into `tp`.
|
|
249
|
+
* Returns `null` if `line` doesn't match this shape.
|
|
250
|
+
*/
|
|
251
|
+
function parseAliasRedirect(line) {
|
|
252
|
+
const match = line.match(/^\/([a-zA-Z0-9_:-]+)\s*->\s*([a-zA-Z0-9_:-]+)$/);
|
|
253
|
+
if (!match) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
return { alias: match[1], target: match[2] };
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Parse every line of `text` that describes `commandPath`'s syntax (an
|
|
260
|
+
* optional leading `/`, then `commandPath` with internal spaces matching
|
|
261
|
+
* runs of whitespace, then the argument tokens), collecting subcommand
|
|
262
|
+
* variants and/or a direct parameter list. Lines for other commands, alias
|
|
263
|
+
* redirects (`-> target`), and lines with no arguments at all are ignored.
|
|
264
|
+
*
|
|
265
|
+
* `text` is matched line-by-line (split on `\n`) — callers whose source may
|
|
266
|
+
* pack multiple commands' syntax onto one line without separators (e.g. a
|
|
267
|
+
* `minecraft:help` blob, where consecutive commands are simply concatenated)
|
|
268
|
+
* must first replace `/` with `\n/` to re-split it into one command per line.
|
|
269
|
+
*/
|
|
270
|
+
function parseHelpLines(text, commandPath) {
|
|
271
|
+
const escaped = commandPath
|
|
272
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
273
|
+
.replace(/ /g, '\\s+');
|
|
274
|
+
const pattern = new RegExp(`^/?${escaped}(?::)?\\s*(.*)$`, 'i');
|
|
275
|
+
const variants = new Map();
|
|
276
|
+
let direct = null;
|
|
277
|
+
for (const rawLine of text.split('\n')) {
|
|
278
|
+
const stripped = (0, ansi_1.stripColors)(rawLine).trim();
|
|
279
|
+
if (!stripped || stripped.startsWith('---')) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const match = stripped.match(pattern);
|
|
283
|
+
if (!match) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const afterCommand = match[1] || '';
|
|
287
|
+
if (!afterCommand || afterCommand.startsWith('->')) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const tokens = tokenizeParameterString(afterCommand);
|
|
291
|
+
const classified = classifyParameterTokens(tokens);
|
|
292
|
+
if (classified?.kind === 'variant') {
|
|
293
|
+
variants.set(classified.name, { optional: classified.optional, members: classified.parameters });
|
|
294
|
+
}
|
|
295
|
+
else if (classified?.kind === 'direct') {
|
|
296
|
+
direct = classified.parameters;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return { variants, direct };
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* True iff `parameters` is exactly a bare `<args>`/`[<args>]` placeholder -
|
|
303
|
+
* the generic stand-in Bukkit emits (e.g. for `minecraft:help <cmd>` on
|
|
304
|
+
* commands that aren't Brigadier-backed, like `version`/`reload`/`plugins`)
|
|
305
|
+
* when it has no real argument info, regardless of whether it's optional.
|
|
306
|
+
*/
|
|
307
|
+
function isArgsPlaceholder(parameters) {
|
|
308
|
+
return parameters.length === 1
|
|
309
|
+
&& parameters[0].type === commandTree_1.ParameterType.ARGUMENT
|
|
310
|
+
&& parameters[0].name === 'args';
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* True iff `parameters` is exactly the generic `[<args>]` placeholder Bukkit
|
|
314
|
+
* emits for `minecraft:help <cmd>` on commands that aren't Brigadier-backed
|
|
315
|
+
* (e.g. `version`, `reload`, `plugins`) — i.e. no real argument info.
|
|
316
|
+
*/
|
|
317
|
+
function isGenericArgsPlaceholder(parameters) {
|
|
318
|
+
return isArgsPlaceholder(parameters) && parameters[0].optional === true;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* True iff `members` carries real, usable argument info for a subcommand
|
|
322
|
+
* variant - i.e. it's non-empty and not just an `<args>`/`[<args>]`
|
|
323
|
+
* placeholder (which means "no info available", whether or not it's
|
|
324
|
+
* optional). Used to decide whether the usage already parsed from a parent
|
|
325
|
+
* command's help output is trustworthy enough to skip a further per-subcommand
|
|
326
|
+
* help round trip.
|
|
327
|
+
*/
|
|
328
|
+
function hasUsableArguments(members) {
|
|
329
|
+
return members.length > 0 && !isArgsPlaceholder(members);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* True iff `result` carries real, usable syntax info - either a non-placeholder
|
|
333
|
+
* `direct` parameter list or at least one subcommand variant. Used to decide
|
|
334
|
+
* whether a help source already answers a command's usage, or whether the
|
|
335
|
+
* caller needs to try another source.
|
|
336
|
+
*/
|
|
337
|
+
function hasRealUsage(result) {
|
|
338
|
+
return (result.direct !== null && !isGenericArgsPlaceholder(result.direct)) || result.variants.size > 0;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Parse a `/help` or `minecraft:help` response into the root commands and
|
|
342
|
+
* alias redirects it describes.
|
|
343
|
+
*
|
|
344
|
+
* `response` is first run through `splitConcatenatedHelpLines` so that a
|
|
345
|
+
* `minecraft:help` blob (multiple `/cmd ...` entries packed onto one line)
|
|
346
|
+
* is split one-per-line. Each resulting line is then matched against one of:
|
|
347
|
+
*
|
|
348
|
+
* - an alias redirect (`/<alias> -> <target>`, via `parseAliasRedirect`)
|
|
349
|
+
* - `/command ...` or `/namespace:command ...`
|
|
350
|
+
* - `command: ...` or `command-with-hyphens: ...`
|
|
351
|
+
* - `- command ...` or `* command ...`
|
|
352
|
+
* - `command <args>` (command name followed by `-`/`<`/`[`/`(`)
|
|
353
|
+
*
|
|
354
|
+
* Namespace prefixes (`minecraft:`, `bukkit:`, ...) are part of the command
|
|
355
|
+
* name - per "ingest everything", `minecraft:advancement` is its own root
|
|
356
|
+
* command, not just `minecraft`, so `:` is included in every pattern's
|
|
357
|
+
* character class. Common non-command words that appear in descriptions
|
|
358
|
+
* (`usage`, `example`, `description`, `syntax`) are skipped.
|
|
359
|
+
*
|
|
360
|
+
* For each command found, `isPlaceholder` reflects whether its summary line
|
|
361
|
+
* already carries real syntax info (`hasRealUsage`), so callers can decide
|
|
362
|
+
* which help source to try first for that command's full details.
|
|
363
|
+
*/
|
|
364
|
+
function parseHelpResponse(response) {
|
|
365
|
+
const lines = splitConcatenatedHelpLines(response).split('\n');
|
|
366
|
+
const commands = [];
|
|
367
|
+
const aliases = [];
|
|
368
|
+
for (const line of lines) {
|
|
369
|
+
const stripped = (0, ansi_1.stripColors)(line).trim();
|
|
370
|
+
// Skip empty lines and headers
|
|
371
|
+
if (!stripped || stripped.startsWith('---') || stripped.startsWith('===')) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
// Alias redirect lines (`/tp -> teleport`) describe an alias, not a
|
|
375
|
+
// root command in their own right.
|
|
376
|
+
const redirect = parseAliasRedirect(stripped);
|
|
377
|
+
if (redirect) {
|
|
378
|
+
aliases.push(redirect);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const patterns = [
|
|
382
|
+
/^\/([a-zA-Z0-9_:-]+)/, // /command or /namespace:command
|
|
383
|
+
/^([a-zA-Z0-9_:-]+):\s/, // command: or command-with-hyphens:
|
|
384
|
+
/^[-*]\s*([a-zA-Z0-9_:-]+)/, // - command or * command
|
|
385
|
+
/^([a-zA-Z0-9_:-]+)\s+[-<[(]/ // command followed by args
|
|
386
|
+
];
|
|
387
|
+
for (const pattern of patterns) {
|
|
388
|
+
const match = stripped.match(pattern);
|
|
389
|
+
if (!match) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const commandName = match[1];
|
|
393
|
+
// Skip common non-command words that appear in descriptions
|
|
394
|
+
if (['usage', 'example', 'description', 'syntax'].includes(commandName.toLowerCase())) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const summary = parseHelpLines(stripped, commandName);
|
|
398
|
+
commands.push({ name: commandName, isPlaceholder: !hasRealUsage(summary) });
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return { commands, aliases };
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* True iff `response` is the Brigadier "unknown namespace" syntax error
|
|
406
|
+
* returned for `minecraft:help` on servers where the `minecraft:` command
|
|
407
|
+
* namespace prefix isn't registered (vanilla/fabric) — distinct from the
|
|
408
|
+
* normal "Unknown command or insufficient permissions" not-found message.
|
|
409
|
+
*/
|
|
410
|
+
function isUnsupportedNamespaceError(response) {
|
|
411
|
+
return /^Unknown or incomplete command/i.test((0, ansi_1.stripColors)(response).trim());
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Build the parameter structure representing a set of collected variants:
|
|
415
|
+
* a single SUBCOMMAND if there's only one, or a CHOICE_LIST wrapping a
|
|
416
|
+
* SUBCOMMAND for each variant (in encounter order) otherwise.
|
|
417
|
+
*/
|
|
418
|
+
function buildParameterStructureFromVariants(variants) {
|
|
419
|
+
if (variants.size === 0) {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
const subcommandChoices = [];
|
|
423
|
+
for (const [name, { optional, members }] of variants) {
|
|
424
|
+
subcommandChoices.push({
|
|
425
|
+
type: commandTree_1.ParameterType.SUBCOMMAND,
|
|
426
|
+
name,
|
|
427
|
+
literal: name,
|
|
428
|
+
optional,
|
|
429
|
+
position: subcommandChoices.length,
|
|
430
|
+
members,
|
|
431
|
+
isComplete: false
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
// If there's only one variant, use it directly; otherwise wrap in a choice list
|
|
435
|
+
if (subcommandChoices.length === 1) {
|
|
436
|
+
return [subcommandChoices[0]];
|
|
437
|
+
}
|
|
438
|
+
return [{
|
|
439
|
+
type: commandTree_1.ParameterType.CHOICE_LIST,
|
|
440
|
+
optional: false,
|
|
441
|
+
position: 0,
|
|
442
|
+
choices: subcommandChoices
|
|
443
|
+
}];
|
|
444
|
+
}
|
|
445
|
+
//# sourceMappingURL=helpTextParsing.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/historySearch.ts
|
|
3
|
+
//
|
|
4
|
+
// Pure state and matching logic for Ctrl+R reverse history search. Unlike
|
|
5
|
+
// completionEngine.ts, there's no network round trip involved — searching is
|
|
6
|
+
// just filtering an in-memory array — so this is plain synchronous state
|
|
7
|
+
// transitions, no reducer/effect machinery needed.
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.searchHistory = searchHistory;
|
|
10
|
+
exports.startHistorySearch = startHistorySearch;
|
|
11
|
+
exports.setHistorySearchQuery = setHistorySearchQuery;
|
|
12
|
+
exports.cycleHistorySearch = cycleHistorySearch;
|
|
13
|
+
/** Entries from `history` (oldest-first) containing `query` (case-insensitive), most-recently-used first, deduplicated. An empty query matches everything, so the list starts as "recent history, newest first". */
|
|
14
|
+
function searchHistory(history, query) {
|
|
15
|
+
const lowerQuery = query.toLowerCase();
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
const results = [];
|
|
18
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
19
|
+
const entry = history[i];
|
|
20
|
+
if (seen.has(entry)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
seen.add(entry);
|
|
24
|
+
if (entry.toLowerCase().includes(lowerQuery)) {
|
|
25
|
+
results.push(entry);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
/** Enters search mode: empty query, recent history shown newest-first. */
|
|
31
|
+
function startHistorySearch(history, originalLine) {
|
|
32
|
+
return { query: '', items: searchHistory(history, ''), selectedIndex: 0, originalLine };
|
|
33
|
+
}
|
|
34
|
+
/** Re-filters as the query changes (typing or backspacing), resetting to the best (first) match. */
|
|
35
|
+
function setHistorySearchQuery(history, state, query) {
|
|
36
|
+
return { ...state, query, items: searchHistory(history, query), selectedIndex: 0 };
|
|
37
|
+
}
|
|
38
|
+
/** Moves the selection by `delta`, wrapping; a no-op when there's nothing to select. */
|
|
39
|
+
function cycleHistorySearch(state, delta) {
|
|
40
|
+
if (state.items.length === 0) {
|
|
41
|
+
return state;
|
|
42
|
+
}
|
|
43
|
+
const selectedIndex = ((state.selectedIndex + delta) % state.items.length + state.items.length) % state.items.length;
|
|
44
|
+
return { ...state, selectedIndex };
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=historySearch.js.map
|