cliedit 0.1.1 → 0.1.2
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/ACKNOWLEDGEMENTS.md +21 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/editor.clipboard.js +8 -2
- package/dist/editor.d.ts +3 -1
- package/dist/editor.editing.d.ts +1 -0
- package/dist/editor.editing.js +7 -2
- package/dist/editor.js +3 -1
- package/dist/editor.keys.d.ts +2 -0
- package/dist/editor.keys.js +141 -14
- package/dist/editor.navigation.d.ts +10 -0
- package/dist/editor.navigation.js +24 -0
- package/dist/editor.rendering.js +36 -13
- package/dist/editor.search.d.ts +19 -4
- package/dist/editor.search.js +120 -9
- package/dist/types.d.ts +1 -1
- package/dist/vendor/keypress.js +10 -0
- package/package.json +3 -2
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Acknowledgements & Copyright (ACKNOWLEDGEMENTS)
|
|
2
|
+
|
|
3
|
+
`cliedit` is an open-source project that utilizes components (or modified/converted versions thereof) from other open-source projects. We express our deep gratitude to the communities and authors who provided the foundation for these components.
|
|
4
|
+
|
|
5
|
+
All components listed below are included as vendored code (integrated directly into the project source code) and have been adapted to fit **cliedit**'s TypeScript architecture and remove unnecessary features (e.g., mouse support).
|
|
6
|
+
|
|
7
|
+
## Vendored Components List
|
|
8
|
+
|
|
9
|
+
**Original Project Name: `keypress`**
|
|
10
|
+
|
|
11
|
+
**Description & Origin:** Provides raw key event parsing logic for the TTY environment. The source code was converted from the original JavaScript version and optimized for Node.js.
|
|
12
|
+
|
|
13
|
+
**Repository:** https://github.com/TooTallNate/keypress
|
|
14
|
+
|
|
15
|
+
**License:** MIT
|
|
16
|
+
|
|
17
|
+
**Copyright Notes:**
|
|
18
|
+
|
|
19
|
+
- `keypress` was originally authored by **Nathan Rajlich** (tootallnate.net).
|
|
20
|
+
- Copyright (c) 2012 Nathan Rajlich.
|
|
21
|
+
- The module is based on the keypress logic found within the Node.js Core `readline` module.
|
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
|
@@ -22,7 +22,9 @@ export const KEYS = {
|
|
|
22
22
|
CTRL_Q: '\x11', // Quit
|
|
23
23
|
CTRL_S: '\x13', // Save
|
|
24
24
|
CTRL_W: '\x17', // Find (Where is)
|
|
25
|
+
CTRL_R: '\x12', // Replace
|
|
25
26
|
CTRL_G: '\x07', // Go to next
|
|
27
|
+
CTRL_L: '\x0c', // Go to Line (L)
|
|
26
28
|
CTRL_Z: '\x1a', // Undo
|
|
27
29
|
CTRL_Y: '\x19', // Redo
|
|
28
30
|
CTRL_K: '\x0b', // Cut/Kill line
|
package/dist/editor.clipboard.js
CHANGED
|
@@ -15,8 +15,11 @@ function setClipboard(text) {
|
|
|
15
15
|
case 'win32':
|
|
16
16
|
command = 'clip';
|
|
17
17
|
break;
|
|
18
|
+
case 'linux': // <--- THÊM HỖ TRỢ LINUX
|
|
19
|
+
command = 'xclip -selection clipboard';
|
|
20
|
+
break;
|
|
18
21
|
default:
|
|
19
|
-
this.setStatusMessage('Clipboard
|
|
22
|
+
this.setStatusMessage('Clipboard not supported on this platform');
|
|
20
23
|
return resolve();
|
|
21
24
|
}
|
|
22
25
|
const process = exec(command, (error) => {
|
|
@@ -43,8 +46,11 @@ function getClipboard() {
|
|
|
43
46
|
case 'win32':
|
|
44
47
|
command = 'powershell -command "Get-Clipboard"';
|
|
45
48
|
break;
|
|
49
|
+
case 'linux': // <--- THÊM HỖ TRỢ LINUX
|
|
50
|
+
command = 'xclip -selection clipboard -o'; // -o (hoặc -out) để đọc
|
|
51
|
+
break;
|
|
46
52
|
default:
|
|
47
|
-
this.setStatusMessage('Clipboard
|
|
53
|
+
this.setStatusMessage('Clipboard not supported on this platform');
|
|
48
54
|
return resolve('');
|
|
49
55
|
}
|
|
50
56
|
exec(command, (error, stdout) => {
|
package/dist/editor.d.ts
CHANGED
|
@@ -42,8 +42,10 @@ export declare class CliEditor {
|
|
|
42
42
|
statusTimeout: NodeJS.Timeout | null;
|
|
43
43
|
isMessageCustom: boolean;
|
|
44
44
|
quitConfirm: boolean;
|
|
45
|
-
readonly DEFAULT_STATUS = "HELP: Ctrl+S = Save
|
|
45
|
+
readonly DEFAULT_STATUS = "HELP: Ctrl+S = Save | Ctrl+Q = Quit | Ctrl+W = Find | Ctrl+R = Replace | Ctrl+L = Go to Line";
|
|
46
46
|
searchQuery: string;
|
|
47
|
+
replaceQuery: string | null;
|
|
48
|
+
goToLineQuery: string;
|
|
47
49
|
searchResults: {
|
|
48
50
|
y: number;
|
|
49
51
|
x: number;
|
package/dist/editor.editing.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ declare function insertCharacter(this: CliEditor, char: string): void;
|
|
|
18
18
|
declare function insertSoftTab(this: CliEditor): void;
|
|
19
19
|
/**
|
|
20
20
|
* Inserts a new line, splitting the current line at the cursor position.
|
|
21
|
+
* Implements auto-indent.
|
|
21
22
|
*/
|
|
22
23
|
declare function insertNewLine(this: CliEditor): void;
|
|
23
24
|
/**
|
package/dist/editor.editing.js
CHANGED
|
@@ -51,14 +51,19 @@ function insertSoftTab() {
|
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
53
|
* Inserts a new line, splitting the current line at the cursor position.
|
|
54
|
+
* Implements auto-indent.
|
|
54
55
|
*/
|
|
55
56
|
function insertNewLine() {
|
|
56
57
|
const line = this.lines[this.cursorY] || '';
|
|
58
|
+
// Find indentation of the current line
|
|
59
|
+
const match = line.match(/^(\s*)/);
|
|
60
|
+
const indent = match ? match[1] : '';
|
|
57
61
|
const remainder = line.slice(this.cursorX);
|
|
58
62
|
this.lines[this.cursorY] = line.slice(0, this.cursorX);
|
|
59
|
-
|
|
63
|
+
// Add new line with the same indentation + remainder
|
|
64
|
+
this.lines.splice(this.cursorY + 1, 0, indent + remainder);
|
|
60
65
|
this.cursorY++;
|
|
61
|
-
this.cursorX =
|
|
66
|
+
this.cursorX = indent.length; // Move cursor to end of indent
|
|
62
67
|
this.setDirty();
|
|
63
68
|
}
|
|
64
69
|
/**
|
package/dist/editor.js
CHANGED
|
@@ -13,7 +13,7 @@ import { historyMethods } from './editor.history.js';
|
|
|
13
13
|
import { ioMethods } from './editor.io.js';
|
|
14
14
|
import { keyHandlingMethods } from './editor.keys.js';
|
|
15
15
|
import { selectionMethods } from './editor.selection.js';
|
|
16
|
-
const DEFAULT_STATUS = 'HELP: Ctrl+S = Save
|
|
16
|
+
const DEFAULT_STATUS = 'HELP: Ctrl+S = Save | Ctrl+Q = Quit | Ctrl+W = Find | Ctrl+R = Replace | Ctrl+L = Go to Line';
|
|
17
17
|
/**
|
|
18
18
|
* Main editor class managing application state, TTY interaction, and rendering.
|
|
19
19
|
*/
|
|
@@ -36,6 +36,8 @@ export class CliEditor {
|
|
|
36
36
|
this.quitConfirm = false;
|
|
37
37
|
this.DEFAULT_STATUS = DEFAULT_STATUS;
|
|
38
38
|
this.searchQuery = '';
|
|
39
|
+
this.replaceQuery = null; // null = Find mode, string = Replace mode
|
|
40
|
+
this.goToLineQuery = ''; // For Go to Line prompt
|
|
39
41
|
this.searchResults = [];
|
|
40
42
|
this.searchResultIndex = -1;
|
|
41
43
|
this.isCleanedUp = false;
|
package/dist/editor.keys.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export type TKeyHandlingMethods = {
|
|
|
3
3
|
handleKeypressEvent: (ch: string, key: KeypressEvent) => void;
|
|
4
4
|
handleEditKeys: (key: string) => boolean;
|
|
5
5
|
handleSearchKeys: (key: string) => void;
|
|
6
|
+
handleSearchConfirmKeys: (key: string) => void;
|
|
7
|
+
handleGoToLineKeys: (key: string) => void;
|
|
6
8
|
handleCtrlQ: () => void;
|
|
7
9
|
handleCopy: () => Promise<void>;
|
|
8
10
|
handleCharacterKey: (ch: string) => void;
|
package/dist/editor.keys.js
CHANGED
|
@@ -13,10 +13,21 @@ function handleKeypressEvent(ch, key) {
|
|
|
13
13
|
// --- 1. Xử lý trường hợp key là null/undefined (Ký tự in được) ---
|
|
14
14
|
if (!key) {
|
|
15
15
|
if (ch && ch.length === 1 && ch >= ' ' && ch <= '~') {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
if (this.mode === 'search_find' || this.mode === 'search_replace') {
|
|
17
|
+
this.handleSearchKeys(ch);
|
|
18
|
+
}
|
|
19
|
+
else if (this.mode === 'goto_line') {
|
|
20
|
+
this.handleGoToLineKeys(ch);
|
|
21
|
+
}
|
|
22
|
+
else if (this.mode === 'edit') {
|
|
23
|
+
edited = this.handleEditKeys(ch);
|
|
24
|
+
if (edited) {
|
|
25
|
+
this.saveState();
|
|
26
|
+
this.recalculateVisualRows(); // Phải tính toán lại sau khi gõ
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else if (this.mode === 'search_confirm') {
|
|
30
|
+
this.handleSearchConfirmKeys(ch);
|
|
20
31
|
}
|
|
21
32
|
this.render();
|
|
22
33
|
return;
|
|
@@ -67,9 +78,15 @@ function handleKeypressEvent(ch, key) {
|
|
|
67
78
|
keyName = key.sequence;
|
|
68
79
|
}
|
|
69
80
|
// --- 3. Định tuyến theo Mode ---
|
|
70
|
-
if (this.mode === '
|
|
81
|
+
if (this.mode === 'search_find' || this.mode === 'search_replace') {
|
|
71
82
|
this.handleSearchKeys(keyName || ch);
|
|
72
83
|
}
|
|
84
|
+
else if (this.mode === 'search_confirm') {
|
|
85
|
+
this.handleSearchConfirmKeys(keyName || ch);
|
|
86
|
+
}
|
|
87
|
+
else if (this.mode === 'goto_line') {
|
|
88
|
+
this.handleGoToLineKeys(keyName || ch);
|
|
89
|
+
}
|
|
73
90
|
else {
|
|
74
91
|
// 4. Xử lý phím lựa chọn (Ctrl+Arrow) - Navigation
|
|
75
92
|
switch (keyName) {
|
|
@@ -174,7 +191,13 @@ function handleEditKeys(key) {
|
|
|
174
191
|
return true;
|
|
175
192
|
// --- Search & History ---
|
|
176
193
|
case KEYS.CTRL_W:
|
|
177
|
-
this.
|
|
194
|
+
this.enterFindMode();
|
|
195
|
+
return false;
|
|
196
|
+
case KEYS.CTRL_R:
|
|
197
|
+
this.enterReplaceMode();
|
|
198
|
+
return false;
|
|
199
|
+
case KEYS.CTRL_L:
|
|
200
|
+
this.enterGoToLineMode();
|
|
178
201
|
return false;
|
|
179
202
|
case KEYS.CTRL_G:
|
|
180
203
|
this.findNext();
|
|
@@ -268,28 +291,130 @@ async function handleSave() {
|
|
|
268
291
|
}
|
|
269
292
|
}
|
|
270
293
|
/**
|
|
271
|
-
* Handles Search Mode input keys.
|
|
294
|
+
* Handles Search Mode input keys (for 'search_find' and 'search_replace').
|
|
272
295
|
*/
|
|
273
296
|
function handleSearchKeys(key) {
|
|
297
|
+
const cancelSearch = () => {
|
|
298
|
+
this.mode = 'edit';
|
|
299
|
+
this.searchQuery = '';
|
|
300
|
+
this.replaceQuery = null;
|
|
301
|
+
this.searchResults = [];
|
|
302
|
+
this.searchResultIndex = -1;
|
|
303
|
+
this.setStatusMessage('Cancelled');
|
|
304
|
+
};
|
|
274
305
|
switch (key) {
|
|
275
306
|
case KEYS.ENTER:
|
|
276
|
-
this.
|
|
277
|
-
|
|
307
|
+
if (this.mode === 'search_find') {
|
|
308
|
+
if (this.replaceQuery === null) {
|
|
309
|
+
// Find-Only Flow: Execute search and find first
|
|
310
|
+
this.executeSearch();
|
|
311
|
+
this.mode = 'edit';
|
|
312
|
+
this.findNext();
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
// Replace Flow: Transition to get replace string
|
|
316
|
+
this.mode = 'search_replace';
|
|
317
|
+
this.setStatusMessage('Replace with: ');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
else if (this.mode === 'search_replace') {
|
|
321
|
+
// Replace Flow: We have both strings, execute and find first
|
|
322
|
+
this.executeSearch();
|
|
323
|
+
this.mode = 'edit';
|
|
324
|
+
this.findNext();
|
|
325
|
+
}
|
|
326
|
+
break;
|
|
327
|
+
case KEYS.ESCAPE:
|
|
328
|
+
case KEYS.CTRL_C:
|
|
329
|
+
case KEYS.CTRL_Q:
|
|
330
|
+
cancelSearch();
|
|
331
|
+
break;
|
|
332
|
+
case KEYS.BACKSPACE:
|
|
333
|
+
if (this.mode === 'search_find') {
|
|
334
|
+
this.searchQuery = this.searchQuery.slice(0, -1);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
this.replaceQuery = this.replaceQuery.slice(0, -1);
|
|
338
|
+
}
|
|
339
|
+
break;
|
|
340
|
+
default:
|
|
341
|
+
if (key.length === 1 && key >= ' ' && key <= '~') {
|
|
342
|
+
if (this.mode === 'search_find') {
|
|
343
|
+
this.searchQuery += key;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
this.replaceQuery += key;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Update status bar message live (if not cancelling)
|
|
351
|
+
if (this.mode === 'search_find') {
|
|
352
|
+
this.setStatusMessage((this.replaceQuery === null ? 'Find: ' : 'Find: ') + this.searchQuery);
|
|
353
|
+
}
|
|
354
|
+
else if (this.mode === 'search_replace') {
|
|
355
|
+
this.setStatusMessage('Replace with: ' + this.replaceQuery);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Handles keypresses during the (y/n/a/q) confirmation step.
|
|
360
|
+
*/
|
|
361
|
+
function handleSearchConfirmKeys(key) {
|
|
362
|
+
switch (key.toLowerCase()) {
|
|
363
|
+
case 'y': // Yes
|
|
364
|
+
this.replaceCurrentAndFindNext();
|
|
365
|
+
break;
|
|
366
|
+
case 'n': // No
|
|
278
367
|
this.findNext();
|
|
279
368
|
break;
|
|
369
|
+
case 'a': // All
|
|
370
|
+
this.replaceAll();
|
|
371
|
+
break;
|
|
372
|
+
case 'q': // Quit
|
|
280
373
|
case KEYS.ESCAPE:
|
|
281
374
|
case KEYS.CTRL_C:
|
|
282
375
|
case KEYS.CTRL_Q:
|
|
283
376
|
this.mode = 'edit';
|
|
284
|
-
this.
|
|
285
|
-
this.
|
|
377
|
+
this.searchResults = [];
|
|
378
|
+
this.searchResultIndex = -1;
|
|
379
|
+
this.setStatusMessage('Replace cancelled');
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Handles keypresses during the 'Go to Line' prompt.
|
|
385
|
+
*/
|
|
386
|
+
function handleGoToLineKeys(key) {
|
|
387
|
+
const cancel = () => {
|
|
388
|
+
this.mode = 'edit';
|
|
389
|
+
this.goToLineQuery = '';
|
|
390
|
+
this.setStatusMessage('Cancelled');
|
|
391
|
+
};
|
|
392
|
+
switch (key) {
|
|
393
|
+
case KEYS.ENTER:
|
|
394
|
+
const lineNumber = parseInt(this.goToLineQuery, 10);
|
|
395
|
+
if (!isNaN(lineNumber) && lineNumber > 0) {
|
|
396
|
+
this.jumpToLine(lineNumber);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
this.mode = 'edit';
|
|
400
|
+
this.setStatusMessage('Invalid line number');
|
|
401
|
+
}
|
|
402
|
+
this.goToLineQuery = '';
|
|
403
|
+
break;
|
|
404
|
+
case KEYS.ESCAPE:
|
|
405
|
+
case KEYS.CTRL_C:
|
|
406
|
+
case KEYS.CTRL_Q:
|
|
407
|
+
cancel();
|
|
286
408
|
break;
|
|
287
409
|
case KEYS.BACKSPACE:
|
|
288
|
-
this.
|
|
410
|
+
this.goToLineQuery = this.goToLineQuery.slice(0, -1);
|
|
411
|
+
this.setStatusMessage('Go to Line: ' + this.goToLineQuery);
|
|
289
412
|
break;
|
|
290
413
|
default:
|
|
291
|
-
|
|
292
|
-
|
|
414
|
+
// Only accept digits
|
|
415
|
+
if (key.length === 1 && key >= '0' && key <= '9') {
|
|
416
|
+
this.goToLineQuery += key;
|
|
417
|
+
this.setStatusMessage('Go to Line: ' + this.goToLineQuery);
|
|
293
418
|
}
|
|
294
419
|
}
|
|
295
420
|
}
|
|
@@ -297,6 +422,8 @@ export const keyHandlingMethods = {
|
|
|
297
422
|
handleKeypressEvent,
|
|
298
423
|
handleEditKeys,
|
|
299
424
|
handleSearchKeys,
|
|
425
|
+
handleSearchConfirmKeys,
|
|
426
|
+
handleGoToLineKeys,
|
|
300
427
|
handleCtrlQ,
|
|
301
428
|
handleCopy,
|
|
302
429
|
handleCharacterKey,
|
|
@@ -30,6 +30,14 @@ declare function adjustCursorPosition(this: CliEditor): void;
|
|
|
30
30
|
* Scrolls the viewport to keep the cursor visible.
|
|
31
31
|
*/
|
|
32
32
|
declare function scroll(this: CliEditor): void;
|
|
33
|
+
/**
|
|
34
|
+
* Jumps the cursor to a specific line number (1-based).
|
|
35
|
+
*/
|
|
36
|
+
declare function jumpToLine(this: CliEditor, lineNumber: number): void;
|
|
37
|
+
/**
|
|
38
|
+
* Enters Go To Line mode.
|
|
39
|
+
*/
|
|
40
|
+
declare function enterGoToLineMode(this: CliEditor): void;
|
|
33
41
|
export declare const navigationMethods: {
|
|
34
42
|
findCurrentVisualRowIndex: typeof findCurrentVisualRowIndex;
|
|
35
43
|
moveCursorLogically: typeof moveCursorLogically;
|
|
@@ -38,5 +46,7 @@ export declare const navigationMethods: {
|
|
|
38
46
|
findVisualRowEnd: typeof findVisualRowEnd;
|
|
39
47
|
adjustCursorPosition: typeof adjustCursorPosition;
|
|
40
48
|
scroll: typeof scroll;
|
|
49
|
+
jumpToLine: typeof jumpToLine;
|
|
50
|
+
enterGoToLineMode: typeof enterGoToLineMode;
|
|
41
51
|
};
|
|
42
52
|
export {};
|
|
@@ -121,6 +121,28 @@ function scroll() {
|
|
|
121
121
|
this.rowOffset = currentVisualRow - this.screenRows + 1;
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Jumps the cursor to a specific line number (1-based).
|
|
126
|
+
*/
|
|
127
|
+
function jumpToLine(lineNumber) {
|
|
128
|
+
const targetY = lineNumber - 1; // Convert 1-based to 0-based index
|
|
129
|
+
// Clamp targetY to valid range
|
|
130
|
+
this.cursorY = Math.max(0, Math.min(targetY, this.lines.length - 1));
|
|
131
|
+
this.cursorX = 0; // Move to start of line
|
|
132
|
+
// Adjust scroll
|
|
133
|
+
const visualRowIndex = this.findCurrentVisualRowIndex();
|
|
134
|
+
this.rowOffset = Math.max(0, visualRowIndex - Math.floor(this.screenRows / 2));
|
|
135
|
+
this.mode = 'edit';
|
|
136
|
+
this.setStatusMessage(`Jumped to line ${lineNumber}`, 1000);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Enters Go To Line mode.
|
|
140
|
+
*/
|
|
141
|
+
function enterGoToLineMode() {
|
|
142
|
+
this.mode = 'goto_line';
|
|
143
|
+
this.goToLineQuery = '';
|
|
144
|
+
this.setStatusMessage('Go to Line (ESC to cancel): ');
|
|
145
|
+
}
|
|
124
146
|
export const navigationMethods = {
|
|
125
147
|
findCurrentVisualRowIndex,
|
|
126
148
|
moveCursorLogically,
|
|
@@ -129,4 +151,6 @@ export const navigationMethods = {
|
|
|
129
151
|
findVisualRowEnd,
|
|
130
152
|
adjustCursorPosition,
|
|
131
153
|
scroll,
|
|
154
|
+
jumpToLine,
|
|
155
|
+
enterGoToLineMode,
|
|
132
156
|
};
|
package/dist/editor.rendering.js
CHANGED
|
@@ -65,6 +65,11 @@ function render() {
|
|
|
65
65
|
const logicalY = row.logicalY;
|
|
66
66
|
const isCursorPosition = (visualRowIndex === currentVisualRowIndex && i === cursorVisualX);
|
|
67
67
|
const isSelected = selectionRange && this.isPositionInSelection(logicalY, logicalX, selectionRange);
|
|
68
|
+
// Highlight search result under cursor
|
|
69
|
+
const isSearchResult = (this.searchResultIndex !== -1 &&
|
|
70
|
+
this.searchResults[this.searchResultIndex]?.y === logicalY &&
|
|
71
|
+
logicalX >= this.searchResults[this.searchResultIndex]?.x &&
|
|
72
|
+
logicalX < (this.searchResults[this.searchResultIndex]?.x + this.searchQuery.length));
|
|
68
73
|
if (isSelected) {
|
|
69
74
|
buffer += ANSI.INVERT_COLORS + char + ANSI.RESET_COLORS;
|
|
70
75
|
}
|
|
@@ -72,6 +77,10 @@ function render() {
|
|
|
72
77
|
// Cursor is a single inverted character if not already covered by selection
|
|
73
78
|
buffer += ANSI.INVERT_COLORS + char + ANSI.RESET_COLORS;
|
|
74
79
|
}
|
|
80
|
+
else if (isSearchResult) {
|
|
81
|
+
// Highlight search result
|
|
82
|
+
buffer += ANSI.INVERT_COLORS + char + ANSI.RESET_COLORS;
|
|
83
|
+
}
|
|
75
84
|
else {
|
|
76
85
|
buffer += char;
|
|
77
86
|
}
|
|
@@ -123,24 +132,38 @@ function renderStatusBar() {
|
|
|
123
132
|
let status = '';
|
|
124
133
|
const contentWidth = this.screenCols;
|
|
125
134
|
// --- Line 1: Mode, File Status, Position ---
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
switch (this.mode) {
|
|
136
|
+
case 'search_find':
|
|
137
|
+
status = (this.replaceQuery === null ? 'Find: ' : 'Find: ') + this.searchQuery;
|
|
138
|
+
break;
|
|
139
|
+
case 'search_replace':
|
|
140
|
+
status = 'Replace with: ' + this.replaceQuery;
|
|
141
|
+
break;
|
|
142
|
+
case 'goto_line':
|
|
143
|
+
status = 'Go to Line: ' + this.goToLineQuery;
|
|
144
|
+
break;
|
|
145
|
+
case 'search_confirm':
|
|
146
|
+
// The (y/n/a/q) prompt is set via setStatusMessage
|
|
147
|
+
status = this.statusMessage;
|
|
148
|
+
break;
|
|
149
|
+
case 'edit':
|
|
150
|
+
default:
|
|
151
|
+
const visualRowIndex = this.findCurrentVisualRowIndex();
|
|
152
|
+
const visualRow = this.visualRows[visualRowIndex];
|
|
153
|
+
const visualX = visualRow ? (this.cursorX - visualRow.logicalXStart) : 0;
|
|
154
|
+
const fileStatus = this.isDirty ? `* ${this.filepath}` : this.filepath;
|
|
155
|
+
const pos = `Ln ${this.cursorY + 1}, Col ${this.cursorX + 1} (View: ${visualRowIndex + 1},${visualX + 1})`;
|
|
156
|
+
const statusLeft = `[${fileStatus}]`.padEnd(Math.floor(contentWidth * 0.5));
|
|
157
|
+
const statusRight = pos.padStart(Math.floor(contentWidth * 0.5));
|
|
158
|
+
status = statusLeft + statusRight;
|
|
159
|
+
break;
|
|
138
160
|
}
|
|
139
161
|
status = status.padEnd(contentWidth);
|
|
140
162
|
let buffer = `${ANSI.INVERT_COLORS}${status}${ANSI.RESET_COLORS}`;
|
|
141
163
|
// --- Line 2: Message/Help line ---
|
|
142
164
|
buffer += `\x1b[${this.screenRows + this.screenStartRow + 1};1H`;
|
|
143
|
-
|
|
165
|
+
// Show prompt message if in search mode, otherwise show default help
|
|
166
|
+
const message = (this.mode === 'edit' ? this.DEFAULT_STATUS : this.statusMessage).padEnd(contentWidth);
|
|
144
167
|
buffer += `${message}${ANSI.CLEAR_LINE}`;
|
|
145
168
|
return buffer;
|
|
146
169
|
}
|
package/dist/editor.search.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { CliEditor } from './editor.js';
|
|
2
2
|
/**
|
|
3
|
-
* Methods related to Find/Search functionality.
|
|
3
|
+
* Methods related to Find/Search/Replace functionality.
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
* Enters
|
|
6
|
+
* Enters Find mode.
|
|
7
7
|
*/
|
|
8
|
-
declare function
|
|
8
|
+
declare function enterFindMode(this: CliEditor): void;
|
|
9
|
+
/**
|
|
10
|
+
* Enters Replace mode (starting with the "Find" prompt).
|
|
11
|
+
*/
|
|
12
|
+
declare function enterReplaceMode(this: CliEditor): void;
|
|
9
13
|
/**
|
|
10
14
|
* Executes the search and populates results.
|
|
11
15
|
*/
|
|
@@ -14,6 +18,14 @@ declare function executeSearch(this: CliEditor): void;
|
|
|
14
18
|
* Jumps to the next search result.
|
|
15
19
|
*/
|
|
16
20
|
declare function findNext(this: CliEditor): void;
|
|
21
|
+
/**
|
|
22
|
+
* Replaces the current highlighted search result and finds the next one.
|
|
23
|
+
*/
|
|
24
|
+
declare function replaceCurrentAndFindNext(this: CliEditor): void;
|
|
25
|
+
/**
|
|
26
|
+
* Replaces all occurrences of the search query.
|
|
27
|
+
*/
|
|
28
|
+
declare function replaceAll(this: CliEditor): void;
|
|
17
29
|
/**
|
|
18
30
|
* Moves cursor and adjusts scroll offset to make the result visible.
|
|
19
31
|
*/
|
|
@@ -22,9 +34,12 @@ declare function jumpToResult(this: CliEditor, result: {
|
|
|
22
34
|
x: number;
|
|
23
35
|
}): void;
|
|
24
36
|
export declare const searchMethods: {
|
|
25
|
-
|
|
37
|
+
enterFindMode: typeof enterFindMode;
|
|
38
|
+
enterReplaceMode: typeof enterReplaceMode;
|
|
26
39
|
executeSearch: typeof executeSearch;
|
|
27
40
|
findNext: typeof findNext;
|
|
41
|
+
replaceCurrentAndFindNext: typeof replaceCurrentAndFindNext;
|
|
42
|
+
replaceAll: typeof replaceAll;
|
|
28
43
|
jumpToResult: typeof jumpToResult;
|
|
29
44
|
};
|
|
30
45
|
export {};
|
package/dist/editor.search.js
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
// src/editor.search.ts
|
|
2
2
|
/**
|
|
3
|
-
* Methods related to Find/Search functionality.
|
|
3
|
+
* Methods related to Find/Search/Replace functionality.
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
* Enters
|
|
6
|
+
* Enters Find mode.
|
|
7
7
|
*/
|
|
8
|
-
function
|
|
9
|
-
this.mode = '
|
|
8
|
+
function enterFindMode() {
|
|
9
|
+
this.mode = 'search_find';
|
|
10
10
|
this.searchQuery = '';
|
|
11
|
+
this.replaceQuery = null; // Mark as Find-Only
|
|
11
12
|
this.searchResults = [];
|
|
12
13
|
this.searchResultIndex = -1;
|
|
13
|
-
this.setStatusMessage('
|
|
14
|
+
this.setStatusMessage('Find (ESC to cancel): ');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Enters Replace mode (starting with the "Find" prompt).
|
|
18
|
+
*/
|
|
19
|
+
function enterReplaceMode() {
|
|
20
|
+
this.mode = 'search_find';
|
|
21
|
+
this.searchQuery = '';
|
|
22
|
+
this.replaceQuery = ''; // Mark as Replace flow
|
|
23
|
+
this.searchResults = [];
|
|
24
|
+
this.searchResultIndex = -1;
|
|
25
|
+
this.setStatusMessage('Find (for Replace): ');
|
|
14
26
|
}
|
|
15
27
|
/**
|
|
16
28
|
* Executes the search and populates results.
|
|
@@ -27,18 +39,114 @@ function executeSearch() {
|
|
|
27
39
|
}
|
|
28
40
|
}
|
|
29
41
|
this.searchResultIndex = -1;
|
|
30
|
-
this.
|
|
42
|
+
if (this.replaceQuery === null) { // Find-only flow
|
|
43
|
+
this.setStatusMessage(`Found ${this.searchResults.length} results for "${this.searchQuery}"`);
|
|
44
|
+
}
|
|
31
45
|
}
|
|
32
46
|
/**
|
|
33
47
|
* Jumps to the next search result.
|
|
34
48
|
*/
|
|
35
49
|
function findNext() {
|
|
50
|
+
if (this.searchQuery === '') {
|
|
51
|
+
this.enterFindMode();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Execute search if results are not yet populated
|
|
55
|
+
if (this.searchResults.length === 0 && this.searchResultIndex === -1) {
|
|
56
|
+
this.executeSearch();
|
|
57
|
+
}
|
|
36
58
|
if (this.searchResults.length === 0) {
|
|
37
|
-
this.setStatusMessage('No
|
|
59
|
+
this.setStatusMessage('No results found');
|
|
60
|
+
this.mode = 'edit';
|
|
38
61
|
return;
|
|
39
62
|
}
|
|
40
|
-
this.searchResultIndex
|
|
63
|
+
this.searchResultIndex++;
|
|
64
|
+
if (this.searchResultIndex >= this.searchResults.length) {
|
|
65
|
+
this.setStatusMessage('End of file reached. Starting from top.');
|
|
66
|
+
this.searchResultIndex = 0;
|
|
67
|
+
}
|
|
68
|
+
const result = this.searchResults[this.searchResultIndex];
|
|
69
|
+
this.jumpToResult(result);
|
|
70
|
+
if (this.replaceQuery !== null) {
|
|
71
|
+
// Replace flow: Enter confirmation step
|
|
72
|
+
this.mode = 'search_confirm';
|
|
73
|
+
this.setStatusMessage(`Replace "${this.searchQuery}"? (y/n/a/q)`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// Find-only flow: Go back to edit
|
|
77
|
+
this.mode = 'edit';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Replaces the current highlighted search result and finds the next one.
|
|
82
|
+
*/
|
|
83
|
+
function replaceCurrentAndFindNext() {
|
|
84
|
+
if (this.searchResultIndex === -1 || !this.searchResults[this.searchResultIndex]) {
|
|
85
|
+
this.findNext();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const result = this.searchResults[this.searchResultIndex];
|
|
89
|
+
const line = this.lines[result.y];
|
|
90
|
+
const before = line.substring(0, result.x);
|
|
91
|
+
const after = line.substring(result.x + this.searchQuery.length);
|
|
92
|
+
// Use replaceQuery (it's guaranteed to be a string here, not null)
|
|
93
|
+
this.lines[result.y] = before + this.replaceQuery + after;
|
|
94
|
+
this.setDirty();
|
|
95
|
+
// Store current position to find the *next* match after this one
|
|
96
|
+
const replacedResultY = result.y;
|
|
97
|
+
const replacedResultX = result.x;
|
|
98
|
+
// We MUST re-execute search as all indices may have changed
|
|
99
|
+
this.executeSearch();
|
|
100
|
+
this.recalculateVisualRows();
|
|
101
|
+
// Find the next result *after* the one we just replaced
|
|
102
|
+
let nextIndex = -1;
|
|
103
|
+
for (let i = 0; i < this.searchResults.length; i++) {
|
|
104
|
+
const res = this.searchResults[i];
|
|
105
|
+
if (res.y > replacedResultY || (res.y === replacedResultY && res.x > replacedResultX)) {
|
|
106
|
+
nextIndex = i;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (nextIndex === -1) {
|
|
111
|
+
this.setStatusMessage('No more results');
|
|
112
|
+
this.mode = 'edit';
|
|
113
|
+
this.searchResultIndex = -1; // Reset search
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Found the next one
|
|
117
|
+
this.searchResultIndex = nextIndex;
|
|
41
118
|
this.jumpToResult(this.searchResults[this.searchResultIndex]);
|
|
119
|
+
this.mode = 'search_confirm'; // Stay in confirm mode
|
|
120
|
+
this.setStatusMessage(`Replace "${this.searchQuery}"? (y/n/a/q)`);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Replaces all occurrences of the search query.
|
|
124
|
+
*/
|
|
125
|
+
function replaceAll() {
|
|
126
|
+
if (this.searchResults.length === 0) {
|
|
127
|
+
this.executeSearch();
|
|
128
|
+
}
|
|
129
|
+
if (this.searchResults.length === 0) {
|
|
130
|
+
this.setStatusMessage('No results found');
|
|
131
|
+
this.mode = 'edit';
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
let count = 0;
|
|
135
|
+
// Iterate backwards to ensure indices remain valid during replacement
|
|
136
|
+
for (let i = this.searchResults.length - 1; i >= 0; i--) {
|
|
137
|
+
const result = this.searchResults[i];
|
|
138
|
+
const line = this.lines[result.y];
|
|
139
|
+
const before = line.substring(0, result.x);
|
|
140
|
+
const after = line.substring(result.x + this.searchQuery.length);
|
|
141
|
+
this.lines[result.y] = before + this.replaceQuery + after;
|
|
142
|
+
count++;
|
|
143
|
+
}
|
|
144
|
+
this.setDirty();
|
|
145
|
+
this.recalculateVisualRows();
|
|
146
|
+
this.mode = 'edit';
|
|
147
|
+
this.searchResults = [];
|
|
148
|
+
this.searchResultIndex = -1;
|
|
149
|
+
this.setStatusMessage(`Replaced ${count} occurrences.`);
|
|
42
150
|
}
|
|
43
151
|
/**
|
|
44
152
|
* Moves cursor and adjusts scroll offset to make the result visible.
|
|
@@ -51,8 +159,11 @@ function jumpToResult(result) {
|
|
|
51
159
|
this.rowOffset = Math.max(0, visualRowIndex - Math.floor(this.screenRows / 2));
|
|
52
160
|
}
|
|
53
161
|
export const searchMethods = {
|
|
54
|
-
|
|
162
|
+
enterFindMode,
|
|
163
|
+
enterReplaceMode,
|
|
55
164
|
executeSearch,
|
|
56
165
|
findNext,
|
|
166
|
+
replaceCurrentAndFindNext,
|
|
167
|
+
replaceAll,
|
|
57
168
|
jumpToResult,
|
|
58
169
|
};
|
package/dist/types.d.ts
CHANGED
package/dist/vendor/keypress.js
CHANGED
|
@@ -142,6 +142,16 @@ function emitKey(stream, s) {
|
|
|
142
142
|
key.name = parts[1].toLowerCase();
|
|
143
143
|
key.meta = true;
|
|
144
144
|
key.shift = /^[A-Z]$/.test(parts[1]);
|
|
145
|
+
// ***** START BUG FIX *****
|
|
146
|
+
// The original library failed to handle any standard printable
|
|
147
|
+
// characters (numbers, symbols) that weren't a-z or A-Z.
|
|
148
|
+
}
|
|
149
|
+
else if (s.length === 1 && s >= ' ' && s <= '~') {
|
|
150
|
+
// Standard printable character (digits, symbols, etc.)
|
|
151
|
+
key.name = s;
|
|
152
|
+
// We can infer shift status for common symbols
|
|
153
|
+
key.shift = '!@#$%^&*()_+{}|:"<>?~'.includes(s);
|
|
154
|
+
// ***** END BUG FIX *****
|
|
145
155
|
}
|
|
146
156
|
else if ((parts = functionKeyCodeRe.exec(s))) {
|
|
147
157
|
// ansi escape sequence
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cliedit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
17
|
"LICENSE",
|
|
18
|
-
"README.md"
|
|
18
|
+
"README.md",
|
|
19
|
+
"ACKNOWLEDGEMENTS.md"
|
|
19
20
|
],
|
|
20
21
|
"scripts": {
|
|
21
22
|
"build": "tsc",
|