cliedit 0.2.0 → 0.3.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/dist/editor.d.ts +4 -0
- package/dist/editor.js +6 -3
- package/dist/editor.keys.js +61 -1
- package/dist/editor.rendering.js +31 -3
- package/dist/editor.search.d.ts +5 -0
- package/dist/editor.search.js +16 -0
- package/dist/vendor/keypress.js +27 -1
- package/package.json +7 -8
package/dist/editor.d.ts
CHANGED
package/dist/editor.js
CHANGED
|
@@ -41,6 +41,8 @@ export class CliEditor {
|
|
|
41
41
|
this.replaceQuery = null; // null = Find mode, string = Replace mode
|
|
42
42
|
this.goToLineQuery = ''; // For Go to Line prompt
|
|
43
43
|
this.searchResults = [];
|
|
44
|
+
// Map<lineNumber, Array<{ start, end }>> for fast rendering lookup
|
|
45
|
+
this.searchResultMap = new Map();
|
|
44
46
|
this.searchResultIndex = -1;
|
|
45
47
|
this.isCleanedUp = false;
|
|
46
48
|
this.resolvePromise = null;
|
|
@@ -78,7 +80,8 @@ export class CliEditor {
|
|
|
78
80
|
process.stdout.removeAllListeners('resize');
|
|
79
81
|
// 2. (FIX GHOST TUI) Write exit sequence and use callback to ensure it's written
|
|
80
82
|
// before Node.js fully releases the TTY.
|
|
81
|
-
|
|
83
|
+
// Disable mouse tracking (1000 and 1006)
|
|
84
|
+
process.stdout.write(ANSI.CLEAR_SCREEN + ANSI.MOVE_CURSOR_TOP_LEFT + ANSI.SHOW_CURSOR + ANSI.EXIT_ALTERNATE_SCREEN + '\x1b[?1000l' + '\x1b[?1006l', () => {
|
|
82
85
|
// 3. Disable TTY raw mode and pause stdin after screen is cleared
|
|
83
86
|
if (this.inputStream.setRawMode) {
|
|
84
87
|
this.inputStream.setRawMode(false);
|
|
@@ -109,8 +112,8 @@ export class CliEditor {
|
|
|
109
112
|
}
|
|
110
113
|
this.updateScreenSize();
|
|
111
114
|
this.recalculateVisualRows();
|
|
112
|
-
// Enter alternate screen and hide cursor
|
|
113
|
-
process.stdout.write(ANSI.ENTER_ALTERNATE_SCREEN + ANSI.HIDE_CURSOR + ANSI.CLEAR_SCREEN);
|
|
115
|
+
// Enter alternate screen and hide cursor + Enable SGR Mouse (1006) and Button Event (1000)
|
|
116
|
+
process.stdout.write(ANSI.ENTER_ALTERNATE_SCREEN + ANSI.HIDE_CURSOR + ANSI.CLEAR_SCREEN + '\x1b[?1000h' + '\x1b[?1006h');
|
|
114
117
|
if (this.inputStream.setRawMode) {
|
|
115
118
|
this.inputStream.setRawMode(true);
|
|
116
119
|
}
|
package/dist/editor.keys.js
CHANGED
|
@@ -85,6 +85,11 @@ function handleKeypressEvent(ch, key) {
|
|
|
85
85
|
keyName = 'ALT_LEFT';
|
|
86
86
|
else if (key.meta && key.name === 'right')
|
|
87
87
|
keyName = 'ALT_RIGHT';
|
|
88
|
+
// Handle Mouse Scroll events explicitly
|
|
89
|
+
else if (key.name === 'scrollup')
|
|
90
|
+
keyName = 'SCROLL_UP';
|
|
91
|
+
else if (key.name === 'scrolldown')
|
|
92
|
+
keyName = 'SCROLL_DOWN';
|
|
88
93
|
else
|
|
89
94
|
keyName = key.sequence;
|
|
90
95
|
}
|
|
@@ -118,7 +123,55 @@ function handleKeypressEvent(ch, key) {
|
|
|
118
123
|
return;
|
|
119
124
|
}
|
|
120
125
|
// 5. Xử lý tất cả các phím lệnh/chỉnh sửa khác
|
|
121
|
-
|
|
126
|
+
if (keyName === 'SCROLL_UP') {
|
|
127
|
+
const scrollAmount = 3;
|
|
128
|
+
this.rowOffset = Math.max(0, this.rowOffset - scrollAmount);
|
|
129
|
+
// Adjust cursor if it falls out of view (below the viewport)
|
|
130
|
+
// Actually if we scroll UP, the viewport moves UP. The cursor might be BELOW the viewport.
|
|
131
|
+
// Wait, scroll UP means viewing lines ABOVE. Viewport index decreases.
|
|
132
|
+
// Cursor (if previously in view) might now be >= rowOffset + screenRows.
|
|
133
|
+
// We need to ensure cursor is within [rowOffset, rowOffset + screenRows - 1]
|
|
134
|
+
// But verify after setting rowOffset.
|
|
135
|
+
const currentVisualRow = this.findCurrentVisualRowIndex();
|
|
136
|
+
const bottomEdge = this.rowOffset + this.screenRows - 1;
|
|
137
|
+
if (currentVisualRow > bottomEdge) {
|
|
138
|
+
const targetRow = this.visualRows[bottomEdge];
|
|
139
|
+
this.cursorY = targetRow.logicalY;
|
|
140
|
+
this.cursorX = targetRow.logicalXStart;
|
|
141
|
+
}
|
|
142
|
+
else if (currentVisualRow < this.rowOffset) {
|
|
143
|
+
// Should not happen when scrolling up (moving viewport up), unless cursor was already above?
|
|
144
|
+
// If we scroll up, rowOffset decreases. Current row stays same.
|
|
145
|
+
// So current row > new rowOffset.
|
|
146
|
+
// It might be > bottomEdge.
|
|
147
|
+
}
|
|
148
|
+
// However, to be safe against 'scroll' method resetting it:
|
|
149
|
+
// The 'scroll' method checks:
|
|
150
|
+
// if (currentVisualRow < this.rowOffset) -> this.rowOffset = currentVisualRow
|
|
151
|
+
// if (currentVisualRow >= this.rowOffset + this.screenRows) -> this.rowOffset = ...
|
|
152
|
+
// So we MUST move cursor inside the new viewport.
|
|
153
|
+
if (currentVisualRow > bottomEdge) {
|
|
154
|
+
const targetRow = this.visualRows[bottomEdge];
|
|
155
|
+
this.cursorY = targetRow.logicalY;
|
|
156
|
+
this.cursorX = targetRow.logicalXStart;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else if (keyName === 'SCROLL_DOWN') {
|
|
160
|
+
const scrollAmount = 3;
|
|
161
|
+
const maxOffset = Math.max(0, this.visualRows.length - this.screenRows);
|
|
162
|
+
this.rowOffset = Math.min(maxOffset, this.rowOffset + scrollAmount);
|
|
163
|
+
// Scroll DOWN means viewport index increases.
|
|
164
|
+
// Cursor might be ABOVE the new viewport (currentVisualRow < rowOffset).
|
|
165
|
+
const currentVisualRow = this.findCurrentVisualRowIndex();
|
|
166
|
+
if (currentVisualRow < this.rowOffset) {
|
|
167
|
+
const targetRow = this.visualRows[this.rowOffset];
|
|
168
|
+
this.cursorY = targetRow.logicalY;
|
|
169
|
+
this.cursorX = targetRow.logicalXStart;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
edited = this.handleEditKeys(keyName || ch);
|
|
174
|
+
}
|
|
122
175
|
}
|
|
123
176
|
// 6. Cập nhật Trạng thái và Render
|
|
124
177
|
if (edited) {
|
|
@@ -130,6 +183,7 @@ function handleKeypressEvent(ch, key) {
|
|
|
130
183
|
}
|
|
131
184
|
}
|
|
132
185
|
function handleAltArrows(keyName) {
|
|
186
|
+
this.clearSearchResults(); // Clear highlights on smart navigation
|
|
133
187
|
if (keyName === 'ALT_LEFT')
|
|
134
188
|
this.moveCursorByWord('left');
|
|
135
189
|
else if (keyName === 'ALT_RIGHT')
|
|
@@ -147,6 +201,7 @@ function handleEditKeys(key) {
|
|
|
147
201
|
].includes(key);
|
|
148
202
|
if (isNavigation) {
|
|
149
203
|
this.cancelSelection();
|
|
204
|
+
this.clearSearchResults(); // Clear highlights on navigation
|
|
150
205
|
if (this.isMessageCustom) {
|
|
151
206
|
this.setStatusMessage(this.DEFAULT_STATUS, 0);
|
|
152
207
|
}
|
|
@@ -189,9 +244,11 @@ function handleEditKeys(key) {
|
|
|
189
244
|
return false;
|
|
190
245
|
// --- Editing ---
|
|
191
246
|
case KEYS.ENTER:
|
|
247
|
+
this.clearSearchResults();
|
|
192
248
|
this.insertNewLine();
|
|
193
249
|
return true;
|
|
194
250
|
case KEYS.BACKSPACE:
|
|
251
|
+
this.clearSearchResults();
|
|
195
252
|
// Handle auto-pair deletion
|
|
196
253
|
const line = this.lines[this.cursorY] || '';
|
|
197
254
|
const charBefore = line[this.cursorX - 1];
|
|
@@ -212,12 +269,14 @@ function handleEditKeys(key) {
|
|
|
212
269
|
}
|
|
213
270
|
return true;
|
|
214
271
|
case KEYS.DELETE:
|
|
272
|
+
this.clearSearchResults();
|
|
215
273
|
if (this.selectionAnchor)
|
|
216
274
|
this.deleteSelectedText();
|
|
217
275
|
else
|
|
218
276
|
this.deleteForward();
|
|
219
277
|
return true;
|
|
220
278
|
case KEYS.TAB:
|
|
279
|
+
this.clearSearchResults();
|
|
221
280
|
this.insertSoftTab();
|
|
222
281
|
return true;
|
|
223
282
|
// --- Search & History ---
|
|
@@ -273,6 +332,7 @@ function handleEditKeys(key) {
|
|
|
273
332
|
// Xử lý Ký tự in được
|
|
274
333
|
default:
|
|
275
334
|
if (key.length === 1 && key >= ' ' && key <= '~') {
|
|
335
|
+
this.clearSearchResults();
|
|
276
336
|
this.handleCharacterKey(key);
|
|
277
337
|
return true;
|
|
278
338
|
}
|
package/dist/editor.rendering.js
CHANGED
|
@@ -41,6 +41,11 @@ function render() {
|
|
|
41
41
|
const displayX = cursorVisualX + this.gutterWidth;
|
|
42
42
|
const displayY = this.screenStartRow + (currentVisualRowIndex - this.rowOffset);
|
|
43
43
|
const selectionRange = this.getNormalizedSelection();
|
|
44
|
+
// Scrollbar calculations
|
|
45
|
+
const totalLines = this.visualRows.length;
|
|
46
|
+
const showScrollbar = totalLines > this.screenRows;
|
|
47
|
+
const thumbHeight = showScrollbar ? Math.max(1, Math.floor((this.screenRows / totalLines) * this.screenRows)) : 0;
|
|
48
|
+
const thumbStart = showScrollbar ? Math.floor((this.rowOffset / totalLines) * this.screenRows) : 0;
|
|
44
49
|
// Draw visual rows
|
|
45
50
|
for (let y = 0; y < this.screenRows; y++) {
|
|
46
51
|
const visualRowIndex = y + this.rowOffset;
|
|
@@ -66,7 +71,19 @@ function render() {
|
|
|
66
71
|
const isCursorPosition = (visualRowIndex === currentVisualRowIndex && i === cursorVisualX);
|
|
67
72
|
const isSelected = selectionRange && this.isPositionInSelection(logicalY, logicalX, selectionRange);
|
|
68
73
|
// Highlight search result under cursor
|
|
69
|
-
|
|
74
|
+
// Check if this character is part of ANY search result
|
|
75
|
+
let isGlobalSearchResult = false;
|
|
76
|
+
if (this.searchResultMap.has(logicalY)) {
|
|
77
|
+
const matches = this.searchResultMap.get(logicalY);
|
|
78
|
+
for (const match of matches) {
|
|
79
|
+
if (logicalX >= match.start && logicalX < match.end) {
|
|
80
|
+
isGlobalSearchResult = true;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Check if this character is part of the CURRENTLY SELECTED search result
|
|
86
|
+
const isCurrentSearchResult = (this.searchResultIndex !== -1 &&
|
|
70
87
|
this.searchResults[this.searchResultIndex]?.y === logicalY &&
|
|
71
88
|
logicalX >= this.searchResults[this.searchResultIndex]?.x &&
|
|
72
89
|
logicalX < (this.searchResults[this.searchResultIndex]?.x + this.searchQuery.length));
|
|
@@ -77,8 +94,12 @@ function render() {
|
|
|
77
94
|
// Cursor is a single inverted character if not already covered by selection
|
|
78
95
|
buffer += ANSI.INVERT_COLORS + char + ANSI.RESET_COLORS;
|
|
79
96
|
}
|
|
80
|
-
else if (
|
|
81
|
-
//
|
|
97
|
+
else if (isCurrentSearchResult) {
|
|
98
|
+
// Selected Match: Invert + Underline (if supported) or just Invert
|
|
99
|
+
buffer += ANSI.INVERT_COLORS + '\x1b[4m' + char + ANSI.RESET_COLORS;
|
|
100
|
+
}
|
|
101
|
+
else if (isGlobalSearchResult) {
|
|
102
|
+
// Global Match: Invert only
|
|
82
103
|
buffer += ANSI.INVERT_COLORS + char + ANSI.RESET_COLORS;
|
|
83
104
|
}
|
|
84
105
|
else {
|
|
@@ -93,6 +114,13 @@ function render() {
|
|
|
93
114
|
}
|
|
94
115
|
buffer += `${ANSI.CLEAR_LINE}`;
|
|
95
116
|
}
|
|
117
|
+
// Draw Scrollbar (Phase 2)
|
|
118
|
+
if (showScrollbar) {
|
|
119
|
+
const isThumb = y >= thumbStart && y < thumbStart + thumbHeight;
|
|
120
|
+
const scrollChar = isThumb ? '┃' : '│';
|
|
121
|
+
// Move to last column and draw
|
|
122
|
+
buffer += `\x1b[${this.screenStartRow + y};${this.screenCols}H${ANSI.RESET_COLORS}${scrollChar}`;
|
|
123
|
+
}
|
|
96
124
|
}
|
|
97
125
|
// Draw status bar
|
|
98
126
|
buffer += `\x1b[${this.screenRows + this.screenStartRow};1H`;
|
package/dist/editor.search.d.ts
CHANGED
|
@@ -33,6 +33,10 @@ declare function jumpToResult(this: CliEditor, result: {
|
|
|
33
33
|
y: number;
|
|
34
34
|
x: number;
|
|
35
35
|
}): void;
|
|
36
|
+
/**
|
|
37
|
+
* Clears the current search results and highlights.
|
|
38
|
+
*/
|
|
39
|
+
declare function clearSearchResults(this: CliEditor): void;
|
|
36
40
|
export declare const searchMethods: {
|
|
37
41
|
enterFindMode: typeof enterFindMode;
|
|
38
42
|
enterReplaceMode: typeof enterReplaceMode;
|
|
@@ -41,5 +45,6 @@ export declare const searchMethods: {
|
|
|
41
45
|
replaceCurrentAndFindNext: typeof replaceCurrentAndFindNext;
|
|
42
46
|
replaceAll: typeof replaceAll;
|
|
43
47
|
jumpToResult: typeof jumpToResult;
|
|
48
|
+
clearSearchResults: typeof clearSearchResults;
|
|
44
49
|
};
|
|
45
50
|
export {};
|
package/dist/editor.search.js
CHANGED
|
@@ -29,13 +29,20 @@ function enterReplaceMode() {
|
|
|
29
29
|
*/
|
|
30
30
|
function executeSearch() {
|
|
31
31
|
this.searchResults = [];
|
|
32
|
+
this.searchResultMap.clear();
|
|
32
33
|
if (this.searchQuery === '')
|
|
33
34
|
return;
|
|
35
|
+
const queryLen = this.searchQuery.length;
|
|
34
36
|
for (let y = 0; y < this.lines.length; y++) {
|
|
35
37
|
const line = this.lines[y];
|
|
36
38
|
let index = -1;
|
|
39
|
+
const lineMatches = [];
|
|
37
40
|
while ((index = line.indexOf(this.searchQuery, index + 1)) !== -1) {
|
|
38
41
|
this.searchResults.push({ y, x: index });
|
|
42
|
+
lineMatches.push({ start: index, end: index + queryLen });
|
|
43
|
+
}
|
|
44
|
+
if (lineMatches.length > 0) {
|
|
45
|
+
this.searchResultMap.set(y, lineMatches);
|
|
39
46
|
}
|
|
40
47
|
}
|
|
41
48
|
this.searchResultIndex = -1;
|
|
@@ -158,6 +165,14 @@ function jumpToResult(result) {
|
|
|
158
165
|
// Calculate new scroll offset to center the result visually
|
|
159
166
|
this.rowOffset = Math.max(0, visualRowIndex - Math.floor(this.screenRows / 2));
|
|
160
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Clears the current search results and highlights.
|
|
170
|
+
*/
|
|
171
|
+
function clearSearchResults() {
|
|
172
|
+
this.searchResults = [];
|
|
173
|
+
this.searchResultMap.clear();
|
|
174
|
+
this.searchResultIndex = -1;
|
|
175
|
+
}
|
|
161
176
|
export const searchMethods = {
|
|
162
177
|
enterFindMode,
|
|
163
178
|
enterReplaceMode,
|
|
@@ -166,4 +181,5 @@ export const searchMethods = {
|
|
|
166
181
|
replaceCurrentAndFindNext,
|
|
167
182
|
replaceAll,
|
|
168
183
|
jumpToResult,
|
|
184
|
+
clearSearchResults,
|
|
169
185
|
};
|
package/dist/vendor/keypress.js
CHANGED
|
@@ -18,6 +18,7 @@ if (!listenerCount) {
|
|
|
18
18
|
*/
|
|
19
19
|
const metaKeyCodeRe = /^(?:\x1b)([a-zA-Z0-9])$/;
|
|
20
20
|
const functionKeyCodeRe = /^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/;
|
|
21
|
+
const mouseSgrRe = /^\x1b\[<(\d+);(\d+);(\d+)([mM])/;
|
|
21
22
|
/**
|
|
22
23
|
* Hàm chính, chấp nhận một Readable Stream và làm cho nó
|
|
23
24
|
* phát ra sự kiện "keypress".
|
|
@@ -421,7 +422,32 @@ function emitKey(stream, s) {
|
|
|
421
422
|
}
|
|
422
423
|
return;
|
|
423
424
|
}
|
|
424
|
-
//
|
|
425
|
+
// Mouse handling (SGR 1006)
|
|
426
|
+
if ((parts = mouseSgrRe.exec(s))) {
|
|
427
|
+
// SGR Mode: \x1b[< b; x; y M/m
|
|
428
|
+
// b: button code
|
|
429
|
+
// x, y: coordinates (1-based)
|
|
430
|
+
// M/m: Press/Release
|
|
431
|
+
const b = parseInt(parts[1], 10);
|
|
432
|
+
const x = parseInt(parts[2], 10);
|
|
433
|
+
const y = parseInt(parts[3], 10);
|
|
434
|
+
const type = parts[4]; // M=press, m=release
|
|
435
|
+
key.name = 'mouse';
|
|
436
|
+
key.ctrl = false;
|
|
437
|
+
key.meta = false;
|
|
438
|
+
key.shift = false;
|
|
439
|
+
// Check for Scroll (Button 64 = Up, 65 = Down)
|
|
440
|
+
if (b === 64) {
|
|
441
|
+
key.name = 'scrollup';
|
|
442
|
+
key.code = 'scrollup';
|
|
443
|
+
}
|
|
444
|
+
else if (b === 65) {
|
|
445
|
+
key.name = 'scrolldown';
|
|
446
|
+
key.code = 'scrolldown';
|
|
447
|
+
}
|
|
448
|
+
// We can handle click here if needed (b=0 left, b=1 middle, b=2 right)
|
|
449
|
+
// but for now only scroll is requested.
|
|
450
|
+
}
|
|
425
451
|
// Không phát ra key nếu không tìm thấy tên
|
|
426
452
|
if (key.name === undefined) {
|
|
427
453
|
return; // key = undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cliedit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A lightweight, raw-mode terminal editor utility for Node.js CLI applications, with line wrapping and undo/redo support.",
|
|
5
5
|
"repository": "https://github.com/CodeTease/cliedit",
|
|
6
6
|
"type": "module",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "tsc",
|
|
23
23
|
"prepublishOnly": "npm run build",
|
|
24
|
-
"demo": "npm run build && cross-env NODE_ENV=development
|
|
24
|
+
"demo": "npm run build && cross-env NODE_ENV=development tsx src/demo.ts",
|
|
25
25
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
26
26
|
},
|
|
27
27
|
"keywords": [
|
|
@@ -35,10 +35,9 @@
|
|
|
35
35
|
"author": "CodeTease",
|
|
36
36
|
"license": "MIT",
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@types/node": "^
|
|
39
|
-
"cross-env": "^7
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
"dependencies": {
|
|
38
|
+
"@types/node": "^22",
|
|
39
|
+
"cross-env": "^7",
|
|
40
|
+
"tsx": "^4",
|
|
41
|
+
"typescript": "^5"
|
|
43
42
|
}
|
|
44
|
-
}
|
|
43
|
+
}
|