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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/historyStore.ts
|
|
3
|
+
//
|
|
4
|
+
// Command history: on-disk persistence (server-scoped, like
|
|
5
|
+
// commandTreeCache.ts) — loaded once at session start to seed LineEditor's
|
|
6
|
+
// in-memory history and rewritten each time a command is run — plus the pure
|
|
7
|
+
// state/matching logic for Ctrl+R reverse history search, which operates on
|
|
8
|
+
// that same in-memory list.
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.HistoryStore = void 0;
|
|
44
|
+
exports.searchHistory = searchHistory;
|
|
45
|
+
exports.startHistorySearch = startHistorySearch;
|
|
46
|
+
exports.setHistorySearchQuery = setHistorySearchQuery;
|
|
47
|
+
exports.cycleHistorySearch = cycleHistorySearch;
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
class HistoryStore {
|
|
51
|
+
logger;
|
|
52
|
+
maxEntries;
|
|
53
|
+
file;
|
|
54
|
+
/** `maxEntries` mirrors LineEditor's in-memory cap — no point persisting more than it'll ever hold. */
|
|
55
|
+
constructor(cacheDir, serverHost, serverPort, logger, maxEntries = 100) {
|
|
56
|
+
this.logger = logger;
|
|
57
|
+
this.maxEntries = maxEntries;
|
|
58
|
+
this.file = path.join(cacheDir, `${serverHost}_${serverPort}_history.txt`);
|
|
59
|
+
}
|
|
60
|
+
/** Returns persisted entries (oldest-first), or `[]` if there's nothing usable to load. */
|
|
61
|
+
load() {
|
|
62
|
+
try {
|
|
63
|
+
if (!fs.existsSync(this.file)) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
const lines = fs.readFileSync(this.file, 'utf-8').split('\n');
|
|
67
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
68
|
+
lines.pop();
|
|
69
|
+
}
|
|
70
|
+
return lines.slice(-this.maxEntries);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
this.logger.error(`Error loading command history: ${error}`);
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Persists `entries` (oldest-first, one per line), overwriting whatever was there before. Newlines within an entry are stripped. */
|
|
78
|
+
save(entries) {
|
|
79
|
+
try {
|
|
80
|
+
const dir = path.dirname(this.file);
|
|
81
|
+
if (!fs.existsSync(dir)) {
|
|
82
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
const lines = entries.slice(-this.maxEntries).map((entry) => entry.replace(/[\r\n]/g, ''));
|
|
85
|
+
fs.writeFileSync(this.file, lines.map((line) => `${line}\n`).join(''));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
this.logger.error(`Error saving command history: ${error}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
exports.HistoryStore = HistoryStore;
|
|
93
|
+
/** 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". */
|
|
94
|
+
function searchHistory(history, query) {
|
|
95
|
+
const lowerQuery = query.toLowerCase();
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
const results = [];
|
|
98
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
99
|
+
const entry = history[i];
|
|
100
|
+
if (seen.has(entry)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
seen.add(entry);
|
|
104
|
+
if (entry.toLowerCase().includes(lowerQuery)) {
|
|
105
|
+
results.push(entry);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return results;
|
|
109
|
+
}
|
|
110
|
+
/** Enters search mode: empty query, recent history shown newest-first. */
|
|
111
|
+
function startHistorySearch(history, originalLine) {
|
|
112
|
+
return { query: '', items: searchHistory(history, ''), selectedIndex: 0, originalLine };
|
|
113
|
+
}
|
|
114
|
+
/** Re-filters as the query changes (typing or backspacing), resetting to the best (first) match. */
|
|
115
|
+
function setHistorySearchQuery(history, state, query) {
|
|
116
|
+
return { ...state, query, items: searchHistory(history, query), selectedIndex: 0 };
|
|
117
|
+
}
|
|
118
|
+
/** Moves the selection by `delta`, wrapping; a no-op when there's nothing to select. */
|
|
119
|
+
function cycleHistorySearch(state, delta) {
|
|
120
|
+
if (state.items.length === 0) {
|
|
121
|
+
return state;
|
|
122
|
+
}
|
|
123
|
+
const selectedIndex = ((state.selectedIndex + delta) % state.items.length + state.items.length) % state.items.length;
|
|
124
|
+
return { ...state, selectedIndex };
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=historyStore.js.map
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/lineEditor.ts
|
|
3
|
+
//
|
|
4
|
+
// Owns the input line itself: the text, cursor position, selection range,
|
|
5
|
+
// and command history, plus every operation that mutates them — character
|
|
6
|
+
// insertion/deletion, cursor and selection movement, kill/yank-style word and
|
|
7
|
+
// line edits, and history navigation. It also owns redrawing the line (with
|
|
8
|
+
// or without selection highlighting) to the terminal.
|
|
9
|
+
//
|
|
10
|
+
// This is the "what does typing do to the buffer, and how does that get
|
|
11
|
+
// painted" layer — it knows nothing about RCON, tab completion, or connection
|
|
12
|
+
// state. Where it needs the host terminal to do something on its behalf (emit
|
|
13
|
+
// ANSI, know the current prompt text, react to the line changing), it goes
|
|
14
|
+
// through the small `LineEditorHost` interface below, so it stays testable
|
|
15
|
+
// and reusable independent of `vscode.Pseudoterminal` plumbing.
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.LineEditor = void 0;
|
|
51
|
+
const ansi = __importStar(require("./ansi"));
|
|
52
|
+
class LineEditor {
|
|
53
|
+
host;
|
|
54
|
+
maxHistorySize;
|
|
55
|
+
currentLine = '';
|
|
56
|
+
cursorPosition = 0;
|
|
57
|
+
// The active selection, or null when there's none. `anchor` is the fixed end
|
|
58
|
+
// (where the gesture started); `head` is the moving end, kept in step with the
|
|
59
|
+
// cursor. A collapsed selection (anchor === head) counts as none — see
|
|
60
|
+
// hasSelection.
|
|
61
|
+
selection = null;
|
|
62
|
+
history = [];
|
|
63
|
+
// Non-null while the user is navigating history; null when at the live line.
|
|
64
|
+
// Bundles the navigation index and the line saved on first Up-arrow so they
|
|
65
|
+
// stay in sync and can be reset atomically.
|
|
66
|
+
historyCursor = null;
|
|
67
|
+
constructor(host, maxHistorySize = 100) {
|
|
68
|
+
this.host = host;
|
|
69
|
+
this.maxHistorySize = maxHistorySize;
|
|
70
|
+
}
|
|
71
|
+
get line() {
|
|
72
|
+
return this.currentLine;
|
|
73
|
+
}
|
|
74
|
+
get cursor() {
|
|
75
|
+
return this.cursorPosition;
|
|
76
|
+
}
|
|
77
|
+
// ── selection ──
|
|
78
|
+
hasSelection() {
|
|
79
|
+
return this.selection !== null && this.selection.anchor !== this.selection.head;
|
|
80
|
+
}
|
|
81
|
+
clearSelection() {
|
|
82
|
+
this.selection = null;
|
|
83
|
+
}
|
|
84
|
+
/** The selection's bounds as `[start, end)`, low-to-high — only valid when `hasSelection()`. */
|
|
85
|
+
selectionRange() {
|
|
86
|
+
const { anchor, head } = this.selection;
|
|
87
|
+
return { start: Math.min(anchor, head), end: Math.max(anchor, head) };
|
|
88
|
+
}
|
|
89
|
+
getSelectedText() {
|
|
90
|
+
if (!this.hasSelection()) {
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
const { start, end } = this.selectionRange();
|
|
94
|
+
return this.currentLine.slice(start, end);
|
|
95
|
+
}
|
|
96
|
+
deleteSelection() {
|
|
97
|
+
if (!this.hasSelection()) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.deleteSelectionInternal();
|
|
101
|
+
this.host.onLineChanged(this.currentLine);
|
|
102
|
+
}
|
|
103
|
+
/** `deleteSelection` without the host notification — for compound edits (insertText) that notify once themselves, with the final line. */
|
|
104
|
+
deleteSelectionInternal() {
|
|
105
|
+
if (!this.hasSelection()) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const { start, end } = this.selectionRange();
|
|
109
|
+
this.currentLine = this.currentLine.slice(0, start) + this.currentLine.slice(end);
|
|
110
|
+
this.cursorPosition = start;
|
|
111
|
+
this.clearSelection();
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Extend (or begin) the selection so its moving end lands at `newPos`: anchor
|
|
115
|
+
* at the current cursor if there isn't a selection yet, then move both the
|
|
116
|
+
* cursor and the selection's moving end to `newPos` and repaint. Every
|
|
117
|
+
* select* operation is this — they differ only in how they pick `newPos`.
|
|
118
|
+
*/
|
|
119
|
+
extendSelectionTo(newPos) {
|
|
120
|
+
if (!this.hasSelection()) {
|
|
121
|
+
this.selection = { anchor: this.cursorPosition, head: this.cursorPosition };
|
|
122
|
+
}
|
|
123
|
+
this.cursorPosition = newPos;
|
|
124
|
+
this.selection.head = newPos;
|
|
125
|
+
this.redraw();
|
|
126
|
+
}
|
|
127
|
+
selectLeft() {
|
|
128
|
+
if (this.cursorPosition > 0) {
|
|
129
|
+
this.extendSelectionTo(this.cursorPosition - 1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
selectRight() {
|
|
133
|
+
if (this.cursorPosition < this.currentLine.length) {
|
|
134
|
+
this.extendSelectionTo(this.cursorPosition + 1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
selectWordLeft() {
|
|
138
|
+
this.extendSelectionTo(this.findWordLeft());
|
|
139
|
+
}
|
|
140
|
+
selectWordRight() {
|
|
141
|
+
this.extendSelectionTo(this.findWordRight());
|
|
142
|
+
}
|
|
143
|
+
selectToStart() {
|
|
144
|
+
if (this.cursorPosition > 0) {
|
|
145
|
+
this.extendSelectionTo(0);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
selectToEnd() {
|
|
149
|
+
if (this.cursorPosition < this.currentLine.length) {
|
|
150
|
+
this.extendSelectionTo(this.currentLine.length);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// ── cursor movement ──
|
|
154
|
+
moveLeft() {
|
|
155
|
+
const hadSelection = this.hasSelection();
|
|
156
|
+
if (hadSelection) {
|
|
157
|
+
this.clearSelection();
|
|
158
|
+
}
|
|
159
|
+
if (this.cursorPosition > 0) {
|
|
160
|
+
this.cursorPosition--;
|
|
161
|
+
if (hadSelection) {
|
|
162
|
+
this.redraw();
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
this.host.write('\x1b[D');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (hadSelection) {
|
|
169
|
+
this.redraw();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
moveRight() {
|
|
173
|
+
const hadSelection = this.hasSelection();
|
|
174
|
+
if (hadSelection) {
|
|
175
|
+
this.clearSelection();
|
|
176
|
+
}
|
|
177
|
+
if (this.cursorPosition < this.currentLine.length) {
|
|
178
|
+
this.cursorPosition++;
|
|
179
|
+
if (hadSelection) {
|
|
180
|
+
this.redraw();
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
this.host.write('\x1b[C');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else if (hadSelection) {
|
|
187
|
+
this.redraw();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
moveWordLeft() {
|
|
191
|
+
const newPos = this.findWordLeft();
|
|
192
|
+
if (newPos !== this.cursorPosition) {
|
|
193
|
+
this.clearSelection();
|
|
194
|
+
this.cursorPosition = newPos;
|
|
195
|
+
this.redraw();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
moveWordRight() {
|
|
199
|
+
const newPos = this.findWordRight();
|
|
200
|
+
if (newPos !== this.cursorPosition) {
|
|
201
|
+
this.clearSelection();
|
|
202
|
+
this.cursorPosition = newPos;
|
|
203
|
+
this.redraw();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
moveToStart() {
|
|
207
|
+
const hadSelection = this.hasSelection();
|
|
208
|
+
this.clearSelection();
|
|
209
|
+
if (this.cursorPosition > 0) {
|
|
210
|
+
this.cursorPosition = 0;
|
|
211
|
+
if (hadSelection) {
|
|
212
|
+
this.redraw();
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
this.host.write('\r');
|
|
216
|
+
this.host.write(this.host.promptText());
|
|
217
|
+
this.host.write(this.currentLine);
|
|
218
|
+
const moveBack = this.currentLine.length;
|
|
219
|
+
if (moveBack > 0) {
|
|
220
|
+
this.host.write('\x1b[' + moveBack + 'D');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else if (hadSelection) {
|
|
225
|
+
this.redraw();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
moveToEnd() {
|
|
229
|
+
const hadSelection = this.hasSelection();
|
|
230
|
+
this.clearSelection();
|
|
231
|
+
const moveForward = this.currentLine.length - this.cursorPosition;
|
|
232
|
+
if (moveForward > 0) {
|
|
233
|
+
this.cursorPosition = this.currentLine.length;
|
|
234
|
+
if (hadSelection) {
|
|
235
|
+
this.redraw();
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
this.host.write('\x1b[' + moveForward + 'C');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else if (hadSelection) {
|
|
242
|
+
this.redraw();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// ── editing ──
|
|
246
|
+
insertText(text) {
|
|
247
|
+
if (this.host.consumeOutputArtifacts()) {
|
|
248
|
+
this.host.write('\x1b[2K');
|
|
249
|
+
this.host.write('\r');
|
|
250
|
+
this.host.write(this.host.promptText());
|
|
251
|
+
this.host.write(this.currentLine.substring(0, this.cursorPosition));
|
|
252
|
+
}
|
|
253
|
+
if (this.hasSelection()) {
|
|
254
|
+
this.deleteSelectionInternal();
|
|
255
|
+
}
|
|
256
|
+
const filteredText = text.replace(/[\x00-\x08\x0a-\x1f\x7f]/g, '');
|
|
257
|
+
if (filteredText.length === 0) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
this.currentLine = this.currentLine.slice(0, this.cursorPosition) +
|
|
261
|
+
filteredText +
|
|
262
|
+
this.currentLine.slice(this.cursorPosition);
|
|
263
|
+
const restOfLine = this.currentLine.slice(this.cursorPosition + filteredText.length);
|
|
264
|
+
this.host.write(filteredText + restOfLine);
|
|
265
|
+
this.cursorPosition += filteredText.length;
|
|
266
|
+
if (restOfLine.length > 0) {
|
|
267
|
+
this.host.write('\x1b[' + restOfLine.length + 'D');
|
|
268
|
+
}
|
|
269
|
+
this.clearSelection();
|
|
270
|
+
this.host.onLineChanged(this.currentLine);
|
|
271
|
+
}
|
|
272
|
+
handleBackspace() {
|
|
273
|
+
if (this.hasSelection()) {
|
|
274
|
+
this.deleteSelection();
|
|
275
|
+
this.redraw();
|
|
276
|
+
}
|
|
277
|
+
else if (this.cursorPosition > 0) {
|
|
278
|
+
this.currentLine = this.currentLine.slice(0, this.cursorPosition - 1) +
|
|
279
|
+
this.currentLine.slice(this.cursorPosition);
|
|
280
|
+
this.cursorPosition--;
|
|
281
|
+
this.host.write('\b');
|
|
282
|
+
const restOfLine = this.currentLine.slice(this.cursorPosition);
|
|
283
|
+
this.host.write(restOfLine + ' ');
|
|
284
|
+
this.host.write('\x1b[' + (restOfLine.length + 1) + 'D');
|
|
285
|
+
this.host.onLineChanged(this.currentLine);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
deleteForward() {
|
|
289
|
+
if (this.hasSelection()) {
|
|
290
|
+
this.deleteSelection();
|
|
291
|
+
this.redraw();
|
|
292
|
+
}
|
|
293
|
+
else if (this.cursorPosition < this.currentLine.length) {
|
|
294
|
+
this.currentLine = this.currentLine.slice(0, this.cursorPosition) +
|
|
295
|
+
this.currentLine.slice(this.cursorPosition + 1);
|
|
296
|
+
const restOfLine = this.currentLine.slice(this.cursorPosition);
|
|
297
|
+
this.host.write(restOfLine + ' ');
|
|
298
|
+
this.host.write('\x1b[' + (restOfLine.length + 1) + 'D');
|
|
299
|
+
this.host.onLineChanged(this.currentLine);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Repaint the line after a kill: step the cursor `cursorShiftLeft` columns
|
|
304
|
+
* left to the new insertion point (for backward kills), clear to end of line,
|
|
305
|
+
* then write the surviving `tail` and step back onto its start. Shared by all
|
|
306
|
+
* four kill operations — they differ only in `tail` and the shift.
|
|
307
|
+
*/
|
|
308
|
+
repaintAfterKill(tail, cursorShiftLeft = 0) {
|
|
309
|
+
if (cursorShiftLeft > 0) {
|
|
310
|
+
this.host.write('\x1b[' + cursorShiftLeft + 'D');
|
|
311
|
+
}
|
|
312
|
+
this.host.write('\x1b[K');
|
|
313
|
+
this.host.write(tail);
|
|
314
|
+
if (tail.length > 0) {
|
|
315
|
+
this.host.write('\x1b[' + tail.length + 'D');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// emacs: unix-line-discard — kill from cursor to start of line
|
|
319
|
+
killToStart() {
|
|
320
|
+
if (this.cursorPosition > 0) {
|
|
321
|
+
const killed = this.currentLine.slice(0, this.cursorPosition);
|
|
322
|
+
const afterCursor = this.currentLine.slice(this.cursorPosition);
|
|
323
|
+
this.currentLine = afterCursor;
|
|
324
|
+
this.cursorPosition = 0;
|
|
325
|
+
this.clearSelection();
|
|
326
|
+
this.repaintAfterKill(afterCursor, killed.length);
|
|
327
|
+
this.host.onLineChanged(this.currentLine);
|
|
328
|
+
return killed;
|
|
329
|
+
}
|
|
330
|
+
return '';
|
|
331
|
+
}
|
|
332
|
+
// emacs: kill-line — kill from cursor to end of line
|
|
333
|
+
killToEnd() {
|
|
334
|
+
if (this.cursorPosition < this.currentLine.length) {
|
|
335
|
+
const killed = this.currentLine.slice(this.cursorPosition);
|
|
336
|
+
this.currentLine = this.currentLine.slice(0, this.cursorPosition);
|
|
337
|
+
this.clearSelection();
|
|
338
|
+
this.repaintAfterKill('');
|
|
339
|
+
this.host.onLineChanged(this.currentLine);
|
|
340
|
+
return killed;
|
|
341
|
+
}
|
|
342
|
+
return '';
|
|
343
|
+
}
|
|
344
|
+
// emacs: backward-kill-word (Ctrl+W / Alt+Backspace)
|
|
345
|
+
killWordBack() {
|
|
346
|
+
if (this.cursorPosition > 0) {
|
|
347
|
+
const beforeCursor = this.currentLine.slice(0, this.cursorPosition);
|
|
348
|
+
const afterCursor = this.currentLine.slice(this.cursorPosition);
|
|
349
|
+
const newPos = this.findWordLeft();
|
|
350
|
+
const killed = beforeCursor.slice(newPos);
|
|
351
|
+
this.currentLine = beforeCursor.slice(0, newPos) + afterCursor;
|
|
352
|
+
this.cursorPosition = newPos;
|
|
353
|
+
this.clearSelection();
|
|
354
|
+
this.repaintAfterKill(afterCursor, killed.length);
|
|
355
|
+
this.host.onLineChanged(this.currentLine);
|
|
356
|
+
return killed;
|
|
357
|
+
}
|
|
358
|
+
return '';
|
|
359
|
+
}
|
|
360
|
+
// emacs: kill-word (Alt+D) — delete from cursor to end of the word forward
|
|
361
|
+
killWordForward() {
|
|
362
|
+
const newPos = this.findWordRight();
|
|
363
|
+
if (newPos > this.cursorPosition) {
|
|
364
|
+
const beforeCursor = this.currentLine.slice(0, this.cursorPosition);
|
|
365
|
+
const killed = this.currentLine.slice(this.cursorPosition, newPos);
|
|
366
|
+
const afterDeleted = this.currentLine.slice(newPos);
|
|
367
|
+
this.currentLine = beforeCursor + afterDeleted;
|
|
368
|
+
this.clearSelection();
|
|
369
|
+
this.repaintAfterKill(afterDeleted);
|
|
370
|
+
this.host.onLineChanged(this.currentLine);
|
|
371
|
+
return killed;
|
|
372
|
+
}
|
|
373
|
+
return '';
|
|
374
|
+
}
|
|
375
|
+
// emacs: transpose-chars — swap the two characters around the cursor
|
|
376
|
+
transposeChars() {
|
|
377
|
+
if (this.currentLine.length >= 2 && this.cursorPosition > 0) {
|
|
378
|
+
// At end of line, transpose the last two characters; otherwise transpose
|
|
379
|
+
// the character before the cursor with the one at the cursor.
|
|
380
|
+
const pos = Math.min(this.cursorPosition, this.currentLine.length - 1);
|
|
381
|
+
const chars = [...this.currentLine];
|
|
382
|
+
[chars[pos - 1], chars[pos]] = [chars[pos], chars[pos - 1]];
|
|
383
|
+
this.currentLine = chars.join('');
|
|
384
|
+
this.cursorPosition = Math.min(pos + 1, this.currentLine.length);
|
|
385
|
+
this.clearSelection();
|
|
386
|
+
this.redraw();
|
|
387
|
+
this.host.onLineChanged(this.currentLine);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// ── whole-line operations ──
|
|
391
|
+
redraw() {
|
|
392
|
+
// Move cursor to start of line, clear it, redraw the prompt
|
|
393
|
+
this.host.write('\r');
|
|
394
|
+
this.host.write('\x1b[K');
|
|
395
|
+
this.host.write(this.host.promptText());
|
|
396
|
+
if (!this.hasSelection()) {
|
|
397
|
+
this.host.write(this.currentLine);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
const { start, end } = this.selectionRange();
|
|
401
|
+
if (start > 0) {
|
|
402
|
+
this.host.write(this.currentLine.slice(0, start));
|
|
403
|
+
}
|
|
404
|
+
this.host.write(ansi.REVERSE + this.currentLine.slice(start, end) + ansi.REVERSE_OFF);
|
|
405
|
+
if (end < this.currentLine.length) {
|
|
406
|
+
this.host.write(this.currentLine.slice(end));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (this.currentLine.length > this.cursorPosition) {
|
|
410
|
+
const moveBack = this.currentLine.length - this.cursorPosition;
|
|
411
|
+
this.host.write('\x1b[' + moveBack + 'D');
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/** Resets the line/cursor/selection to empty without writing anything — for callers (like Enter) that print their own newline and redraw the prompt themselves. */
|
|
415
|
+
resetLine() {
|
|
416
|
+
this.currentLine = '';
|
|
417
|
+
this.cursorPosition = 0;
|
|
418
|
+
this.clearSelection();
|
|
419
|
+
}
|
|
420
|
+
clearAndReset() {
|
|
421
|
+
this.host.beforeLineCleared();
|
|
422
|
+
this.host.write('\r');
|
|
423
|
+
this.host.write('\x1b[K');
|
|
424
|
+
this.currentLine = '';
|
|
425
|
+
this.cursorPosition = 0;
|
|
426
|
+
this.clearSelection();
|
|
427
|
+
this.host.onLineChanged('');
|
|
428
|
+
}
|
|
429
|
+
/** Replaces the line wholesale (tab-completion splices, history recall, restore-on-escape) — sets the cursor to the end and redraws from scratch. */
|
|
430
|
+
replaceLine(newLine) {
|
|
431
|
+
this.host.write('\r');
|
|
432
|
+
this.host.write('\x1b[K');
|
|
433
|
+
this.host.write(this.host.promptText());
|
|
434
|
+
this.host.write(newLine);
|
|
435
|
+
this.currentLine = newLine;
|
|
436
|
+
this.cursorPosition = newLine.length;
|
|
437
|
+
this.clearSelection();
|
|
438
|
+
}
|
|
439
|
+
// ── history ──
|
|
440
|
+
/** Command history, oldest-first — e.g. for Ctrl+R search or persisting to disk. */
|
|
441
|
+
get historyEntries() {
|
|
442
|
+
return this.history;
|
|
443
|
+
}
|
|
444
|
+
/** Seeds history (oldest-first) from persisted entries, e.g. loaded from disk at session start. Replaces any existing in-memory history. */
|
|
445
|
+
loadHistory(entries) {
|
|
446
|
+
this.history = entries.slice(-this.maxHistorySize);
|
|
447
|
+
}
|
|
448
|
+
pushHistory(command) {
|
|
449
|
+
if (this.history.length === 0 || this.history[this.history.length - 1] !== command) {
|
|
450
|
+
this.history.push(command);
|
|
451
|
+
if (this.history.length > this.maxHistorySize) {
|
|
452
|
+
this.history.shift();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
navigateHistory(direction) {
|
|
457
|
+
if (this.history.length === 0) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (direction === 'up') {
|
|
461
|
+
if (this.historyCursor === null) {
|
|
462
|
+
this.historyCursor = { index: 0, savedLine: this.currentLine };
|
|
463
|
+
this.replaceLine(this.history[this.history.length - 1]);
|
|
464
|
+
}
|
|
465
|
+
else if (this.historyCursor.index < this.history.length - 1) {
|
|
466
|
+
this.historyCursor.index++;
|
|
467
|
+
this.replaceLine(this.history[this.history.length - 1 - this.historyCursor.index]);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
if (this.historyCursor === null) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (this.historyCursor.index > 0) {
|
|
475
|
+
this.historyCursor.index--;
|
|
476
|
+
this.replaceLine(this.history[this.history.length - 1 - this.historyCursor.index]);
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
const { savedLine } = this.historyCursor;
|
|
480
|
+
this.historyCursor = null;
|
|
481
|
+
this.replaceLine(savedLine);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
resetHistoryCursor() {
|
|
486
|
+
this.historyCursor = null;
|
|
487
|
+
}
|
|
488
|
+
// ── word-boundary helpers ──
|
|
489
|
+
findWordLeft() {
|
|
490
|
+
if (this.cursorPosition === 0) {
|
|
491
|
+
return 0;
|
|
492
|
+
}
|
|
493
|
+
let pos = this.cursorPosition - 1;
|
|
494
|
+
// Skip whitespace
|
|
495
|
+
while (pos > 0 && this.currentLine[pos] === ' ') {
|
|
496
|
+
pos--;
|
|
497
|
+
}
|
|
498
|
+
// Skip word characters
|
|
499
|
+
while (pos > 0 && this.currentLine[pos - 1] !== ' ') {
|
|
500
|
+
pos--;
|
|
501
|
+
}
|
|
502
|
+
return pos;
|
|
503
|
+
}
|
|
504
|
+
findWordRight() {
|
|
505
|
+
if (this.cursorPosition >= this.currentLine.length) {
|
|
506
|
+
return this.currentLine.length;
|
|
507
|
+
}
|
|
508
|
+
let pos = this.cursorPosition;
|
|
509
|
+
// If we're in whitespace, skip to next word
|
|
510
|
+
if (this.currentLine[pos] === ' ') {
|
|
511
|
+
while (pos < this.currentLine.length && this.currentLine[pos] === ' ') {
|
|
512
|
+
pos++;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
// Skip current word
|
|
517
|
+
while (pos < this.currentLine.length && this.currentLine[pos] !== ' ') {
|
|
518
|
+
pos++;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return pos;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
exports.LineEditor = LineEditor;
|
|
525
|
+
//# sourceMappingURL=lineEditor.js.map
|