cliedit 0.1.1 → 0.1.3
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/README.md +13 -7
- 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 +10 -0
- package/dist/editor.editing.js +38 -2
- package/dist/editor.js +3 -1
- package/dist/editor.keys.d.ts +2 -0
- package/dist/editor.keys.js +185 -22
- 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/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A lightweight, zero-dependency, raw-mode terminal editor component for Node.js.
|
|
|
4
4
|
|
|
5
5
|
`cliedit` is designed to be imported into your own CLI application to provide a full-featured, TTY-based text editing experience. It's perfect for applications that need to ask the user for multi-line input, edit configuration files, or write commit messages.
|
|
6
6
|
|
|
7
|
-
It includes line wrapping, visual navigation, undo/redo, text selection, and clipboard support.
|
|
7
|
+
It includes line wrapping, visual navigation, smart auto-indentation, undo/redo, text selection, Find/Replace, and cross-platform clipboard support.
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
@@ -13,18 +13,21 @@ It includes line wrapping, visual navigation, undo/redo, text selection, and cli
|
|
|
13
13
|
- **Visual Navigation:** `Up`/`Down` arrows move by visual rows, not logical lines.
|
|
14
14
|
- **Undo/Redo:** `Ctrl+Z` / `Ctrl+Y` for persistent history.
|
|
15
15
|
- **Text Selection:** `Ctrl+Arrow` keys to select text.
|
|
16
|
-
- **Clipboard Support:** `Ctrl+C` (Copy), `Ctrl+X` (Cut), `Ctrl+V` (Paste) for system clipboard (macOS
|
|
16
|
+
- **Clipboard Support:** `Ctrl+C` (Copy), `Ctrl+X` (Cut), `Ctrl+V` (Paste) for system clipboard (macOS, Windows, **and Linux** via `xclip`).
|
|
17
17
|
- **File I/O:** Loads from and saves to the filesystem.
|
|
18
|
-
- **Search:** `Ctrl+W` to find text.
|
|
18
|
+
- **Search & Replace:** `Ctrl+W` to find text, `Ctrl+R` to find and replace interactively.
|
|
19
|
+
- **Go to Line:** `Ctrl+L` to quickly jump to a specific line number.
|
|
20
|
+
- **Smart Auto-Indentation:** Automatically preserves indentation level when pressing Enter.
|
|
19
21
|
|
|
20
22
|
## Installation
|
|
21
23
|
```bash
|
|
22
24
|
npm install cliedit
|
|
23
|
-
|
|
25
|
+
````
|
|
24
26
|
|
|
25
27
|
## Usage
|
|
26
28
|
|
|
27
29
|
The package exports an `async` function `openEditor` that returns a `Promise`. The promise resolves when the user quits the editor.
|
|
30
|
+
|
|
28
31
|
```javascript
|
|
29
32
|
import { openEditor } from 'cliedit';
|
|
30
33
|
import path from 'path';
|
|
@@ -60,13 +63,15 @@ getCommitMessage();
|
|
|
60
63
|
`openEditor(filepath: string)`
|
|
61
64
|
|
|
62
65
|
Opens the editor for the specified file. If the file doesn't exist, it will be created upon saving.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
|
|
67
|
+
- **Returns:** `Promise<{ saved: boolean; content: string }>`
|
|
68
|
+
* `saved`: `true` if the user saved (Ctrl+S), `false` otherwise (Ctrl+Q).
|
|
69
|
+
* `content`: The final content of the file as a string.
|
|
66
70
|
|
|
67
71
|
`CliEditor`
|
|
68
72
|
|
|
69
73
|
The main editor class. You can import this directly if you need to extend or instantiate the editor with custom logic.
|
|
74
|
+
|
|
70
75
|
```javascript
|
|
71
76
|
import { CliEditor } from 'cliedit';
|
|
72
77
|
```
|
|
@@ -74,6 +79,7 @@ import { CliEditor } from 'cliedit';
|
|
|
74
79
|
### Types
|
|
75
80
|
|
|
76
81
|
Key types are also exported for convenience:
|
|
82
|
+
|
|
77
83
|
```javascript
|
|
78
84
|
import type {
|
|
79
85
|
DocumentState,
|
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
|
/**
|
|
@@ -28,6 +29,14 @@ declare function deleteBackward(this: CliEditor): void;
|
|
|
28
29
|
* Deletes the character after the cursor, or joins the current line with the next one.
|
|
29
30
|
*/
|
|
30
31
|
declare function deleteForward(this: CliEditor): void;
|
|
32
|
+
/**
|
|
33
|
+
* Handles auto-pairing of brackets and quotes.
|
|
34
|
+
* If text is selected, it wraps the selection.
|
|
35
|
+
* Otherwise, it inserts the pair and places the cursor in the middle.
|
|
36
|
+
* @param openChar The opening character that was typed (e.g., '(', '[', '{').
|
|
37
|
+
* @param closeChar The corresponding closing character (e.g., ')', ']', '}').
|
|
38
|
+
*/
|
|
39
|
+
declare function handleAutoPair(this: CliEditor, openChar: string, closeChar: string): void;
|
|
31
40
|
export declare const editingMethods: {
|
|
32
41
|
insertContentAtCursor: typeof insertContentAtCursor;
|
|
33
42
|
insertCharacter: typeof insertCharacter;
|
|
@@ -35,5 +44,6 @@ export declare const editingMethods: {
|
|
|
35
44
|
insertNewLine: typeof insertNewLine;
|
|
36
45
|
deleteBackward: typeof deleteBackward;
|
|
37
46
|
deleteForward: typeof deleteForward;
|
|
47
|
+
handleAutoPair: typeof handleAutoPair;
|
|
38
48
|
};
|
|
39
49
|
export {};
|
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
|
/**
|
|
@@ -105,6 +110,36 @@ function deleteForward() {
|
|
|
105
110
|
}
|
|
106
111
|
this.setDirty();
|
|
107
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Handles auto-pairing of brackets and quotes.
|
|
115
|
+
* If text is selected, it wraps the selection.
|
|
116
|
+
* Otherwise, it inserts the pair and places the cursor in the middle.
|
|
117
|
+
* @param openChar The opening character that was typed (e.g., '(', '[', '{').
|
|
118
|
+
* @param closeChar The corresponding closing character (e.g., ')', ']', '}').
|
|
119
|
+
*/
|
|
120
|
+
function handleAutoPair(openChar, closeChar) {
|
|
121
|
+
if (this.selectionAnchor) {
|
|
122
|
+
// There is a selection, so we need to wrap it.
|
|
123
|
+
const selection = this.getNormalizedSelection();
|
|
124
|
+
if (!selection)
|
|
125
|
+
return; // Should not happen if anchor exists, but good practice
|
|
126
|
+
const selectedText = this.getSelectedText();
|
|
127
|
+
// The deleteSelectedText() function automatically moves the cursor to the start
|
|
128
|
+
// of the selection, so we don't need to set it manually.
|
|
129
|
+
this.deleteSelectedText();
|
|
130
|
+
// Wrap the original selected text
|
|
131
|
+
const wrappedText = openChar + selectedText + closeChar;
|
|
132
|
+
this.insertContentAtCursor(wrappedText.split('\n'));
|
|
133
|
+
// The selection is already cancelled by deleteSelectedText().
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// No selection, just insert the opening and closing characters
|
|
137
|
+
this.insertCharacter(openChar + closeChar);
|
|
138
|
+
// Move cursor back one position to be in between the pair
|
|
139
|
+
this.cursorX--;
|
|
140
|
+
}
|
|
141
|
+
this.setDirty();
|
|
142
|
+
}
|
|
108
143
|
export const editingMethods = {
|
|
109
144
|
insertContentAtCursor,
|
|
110
145
|
insertCharacter,
|
|
@@ -112,4 +147,5 @@ export const editingMethods = {
|
|
|
112
147
|
insertNewLine,
|
|
113
148
|
deleteBackward,
|
|
114
149
|
deleteForward,
|
|
150
|
+
handleAutoPair,
|
|
115
151
|
};
|
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
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
// src/editor.keys.ts
|
|
2
2
|
import { KEYS } from './constants.js';
|
|
3
|
+
const PAIR_MAP = {
|
|
4
|
+
'(': ')',
|
|
5
|
+
'[': ']',
|
|
6
|
+
'{': '}',
|
|
7
|
+
"'": "'",
|
|
8
|
+
'"': '"',
|
|
9
|
+
};
|
|
3
10
|
/**
|
|
4
11
|
* Main router for standardized keypress events from the 'keypress' library.
|
|
5
12
|
*/
|
|
@@ -13,10 +20,21 @@ function handleKeypressEvent(ch, key) {
|
|
|
13
20
|
// --- 1. Xử lý trường hợp key là null/undefined (Ký tự in được) ---
|
|
14
21
|
if (!key) {
|
|
15
22
|
if (ch && ch.length === 1 && ch >= ' ' && ch <= '~') {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
if (this.mode === 'search_find' || this.mode === 'search_replace') {
|
|
24
|
+
this.handleSearchKeys(ch);
|
|
25
|
+
}
|
|
26
|
+
else if (this.mode === 'goto_line') {
|
|
27
|
+
this.handleGoToLineKeys(ch);
|
|
28
|
+
}
|
|
29
|
+
else if (this.mode === 'edit') {
|
|
30
|
+
edited = this.handleEditKeys(ch);
|
|
31
|
+
if (edited) {
|
|
32
|
+
this.saveState();
|
|
33
|
+
this.recalculateVisualRows(); // Phải tính toán lại sau khi gõ
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (this.mode === 'search_confirm') {
|
|
37
|
+
this.handleSearchConfirmKeys(ch);
|
|
20
38
|
}
|
|
21
39
|
this.render();
|
|
22
40
|
return;
|
|
@@ -67,9 +85,15 @@ function handleKeypressEvent(ch, key) {
|
|
|
67
85
|
keyName = key.sequence;
|
|
68
86
|
}
|
|
69
87
|
// --- 3. Định tuyến theo Mode ---
|
|
70
|
-
if (this.mode === '
|
|
88
|
+
if (this.mode === 'search_find' || this.mode === 'search_replace') {
|
|
71
89
|
this.handleSearchKeys(keyName || ch);
|
|
72
90
|
}
|
|
91
|
+
else if (this.mode === 'search_confirm') {
|
|
92
|
+
this.handleSearchConfirmKeys(keyName || ch);
|
|
93
|
+
}
|
|
94
|
+
else if (this.mode === 'goto_line') {
|
|
95
|
+
this.handleGoToLineKeys(keyName || ch);
|
|
96
|
+
}
|
|
73
97
|
else {
|
|
74
98
|
// 4. Xử lý phím lựa chọn (Ctrl+Arrow) - Navigation
|
|
75
99
|
switch (keyName) {
|
|
@@ -158,10 +182,24 @@ function handleEditKeys(key) {
|
|
|
158
182
|
this.insertNewLine();
|
|
159
183
|
return true;
|
|
160
184
|
case KEYS.BACKSPACE:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
185
|
+
// Handle auto-pair deletion
|
|
186
|
+
const line = this.lines[this.cursorY] || '';
|
|
187
|
+
const charBefore = line[this.cursorX - 1];
|
|
188
|
+
const charAfter = line[this.cursorX];
|
|
189
|
+
if (!this.selectionAnchor &&
|
|
190
|
+
charBefore && charAfter &&
|
|
191
|
+
PAIR_MAP[charBefore] === charAfter) {
|
|
192
|
+
// Delete both characters of the pair
|
|
193
|
+
this.lines[this.cursorY] = line.slice(0, this.cursorX - 1) + line.slice(this.cursorX + 1);
|
|
194
|
+
this.cursorX--; // Move cursor back
|
|
195
|
+
this.setDirty();
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
if (this.selectionAnchor)
|
|
199
|
+
this.deleteSelectedText();
|
|
200
|
+
else
|
|
201
|
+
this.deleteBackward();
|
|
202
|
+
}
|
|
165
203
|
return true;
|
|
166
204
|
case KEYS.DELETE:
|
|
167
205
|
if (this.selectionAnchor)
|
|
@@ -174,7 +212,13 @@ function handleEditKeys(key) {
|
|
|
174
212
|
return true;
|
|
175
213
|
// --- Search & History ---
|
|
176
214
|
case KEYS.CTRL_W:
|
|
177
|
-
this.
|
|
215
|
+
this.enterFindMode();
|
|
216
|
+
return false;
|
|
217
|
+
case KEYS.CTRL_R:
|
|
218
|
+
this.enterReplaceMode();
|
|
219
|
+
return false;
|
|
220
|
+
case KEYS.CTRL_L:
|
|
221
|
+
this.enterGoToLineMode();
|
|
178
222
|
return false;
|
|
179
223
|
case KEYS.CTRL_G:
|
|
180
224
|
this.findNext();
|
|
@@ -215,11 +259,26 @@ function handleEditKeys(key) {
|
|
|
215
259
|
* Handles insertion of a character, deleting selection first if it exists.
|
|
216
260
|
*/
|
|
217
261
|
function handleCharacterKey(ch) {
|
|
218
|
-
|
|
219
|
-
|
|
262
|
+
const line = this.lines[this.cursorY] || '';
|
|
263
|
+
const charAfter = line[this.cursorX];
|
|
264
|
+
// If user types a closing character and it's what we expect, just move the cursor.
|
|
265
|
+
if (!this.selectionAnchor &&
|
|
266
|
+
(ch === ')' || ch === ']' || ch === '}' || ch === "'" || ch === '"') &&
|
|
267
|
+
charAfter === ch) {
|
|
268
|
+
this.cursorX++;
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const closeChar = PAIR_MAP[ch];
|
|
272
|
+
if (closeChar) {
|
|
273
|
+
this.handleAutoPair(ch, closeChar);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
if (this.selectionAnchor) {
|
|
277
|
+
this.deleteSelectedText();
|
|
278
|
+
}
|
|
279
|
+
this.insertCharacter(ch);
|
|
280
|
+
this.setDirty();
|
|
220
281
|
}
|
|
221
|
-
this.insertCharacter(ch);
|
|
222
|
-
this.setDirty();
|
|
223
282
|
}
|
|
224
283
|
/**
|
|
225
284
|
* Handles Ctrl+Q (Quit) sequence.
|
|
@@ -268,28 +327,130 @@ async function handleSave() {
|
|
|
268
327
|
}
|
|
269
328
|
}
|
|
270
329
|
/**
|
|
271
|
-
* Handles Search Mode input keys.
|
|
330
|
+
* Handles Search Mode input keys (for 'search_find' and 'search_replace').
|
|
272
331
|
*/
|
|
273
332
|
function handleSearchKeys(key) {
|
|
333
|
+
const cancelSearch = () => {
|
|
334
|
+
this.mode = 'edit';
|
|
335
|
+
this.searchQuery = '';
|
|
336
|
+
this.replaceQuery = null;
|
|
337
|
+
this.searchResults = [];
|
|
338
|
+
this.searchResultIndex = -1;
|
|
339
|
+
this.setStatusMessage('Cancelled');
|
|
340
|
+
};
|
|
274
341
|
switch (key) {
|
|
275
342
|
case KEYS.ENTER:
|
|
276
|
-
this.
|
|
277
|
-
|
|
343
|
+
if (this.mode === 'search_find') {
|
|
344
|
+
if (this.replaceQuery === null) {
|
|
345
|
+
// Find-Only Flow: Execute search and find first
|
|
346
|
+
this.executeSearch();
|
|
347
|
+
this.mode = 'edit';
|
|
348
|
+
this.findNext();
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
// Replace Flow: Transition to get replace string
|
|
352
|
+
this.mode = 'search_replace';
|
|
353
|
+
this.setStatusMessage('Replace with: ');
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
else if (this.mode === 'search_replace') {
|
|
357
|
+
// Replace Flow: We have both strings, execute and find first
|
|
358
|
+
this.executeSearch();
|
|
359
|
+
this.mode = 'edit';
|
|
360
|
+
this.findNext();
|
|
361
|
+
}
|
|
362
|
+
break;
|
|
363
|
+
case KEYS.ESCAPE:
|
|
364
|
+
case KEYS.CTRL_C:
|
|
365
|
+
case KEYS.CTRL_Q:
|
|
366
|
+
cancelSearch();
|
|
367
|
+
break;
|
|
368
|
+
case KEYS.BACKSPACE:
|
|
369
|
+
if (this.mode === 'search_find') {
|
|
370
|
+
this.searchQuery = this.searchQuery.slice(0, -1);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
this.replaceQuery = this.replaceQuery.slice(0, -1);
|
|
374
|
+
}
|
|
375
|
+
break;
|
|
376
|
+
default:
|
|
377
|
+
if (key.length === 1 && key >= ' ' && key <= '~') {
|
|
378
|
+
if (this.mode === 'search_find') {
|
|
379
|
+
this.searchQuery += key;
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
this.replaceQuery += key;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Update status bar message live (if not cancelling)
|
|
387
|
+
if (this.mode === 'search_find') {
|
|
388
|
+
this.setStatusMessage((this.replaceQuery === null ? 'Find: ' : 'Find: ') + this.searchQuery);
|
|
389
|
+
}
|
|
390
|
+
else if (this.mode === 'search_replace') {
|
|
391
|
+
this.setStatusMessage('Replace with: ' + this.replaceQuery);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Handles keypresses during the (y/n/a/q) confirmation step.
|
|
396
|
+
*/
|
|
397
|
+
function handleSearchConfirmKeys(key) {
|
|
398
|
+
switch (key.toLowerCase()) {
|
|
399
|
+
case 'y': // Yes
|
|
400
|
+
this.replaceCurrentAndFindNext();
|
|
401
|
+
break;
|
|
402
|
+
case 'n': // No
|
|
278
403
|
this.findNext();
|
|
279
404
|
break;
|
|
405
|
+
case 'a': // All
|
|
406
|
+
this.replaceAll();
|
|
407
|
+
break;
|
|
408
|
+
case 'q': // Quit
|
|
280
409
|
case KEYS.ESCAPE:
|
|
281
410
|
case KEYS.CTRL_C:
|
|
282
411
|
case KEYS.CTRL_Q:
|
|
283
412
|
this.mode = 'edit';
|
|
284
|
-
this.
|
|
285
|
-
this.
|
|
413
|
+
this.searchResults = [];
|
|
414
|
+
this.searchResultIndex = -1;
|
|
415
|
+
this.setStatusMessage('Replace cancelled');
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Handles keypresses during the 'Go to Line' prompt.
|
|
421
|
+
*/
|
|
422
|
+
function handleGoToLineKeys(key) {
|
|
423
|
+
const cancel = () => {
|
|
424
|
+
this.mode = 'edit';
|
|
425
|
+
this.goToLineQuery = '';
|
|
426
|
+
this.setStatusMessage('Cancelled');
|
|
427
|
+
};
|
|
428
|
+
switch (key) {
|
|
429
|
+
case KEYS.ENTER:
|
|
430
|
+
const lineNumber = parseInt(this.goToLineQuery, 10);
|
|
431
|
+
if (!isNaN(lineNumber) && lineNumber > 0) {
|
|
432
|
+
this.jumpToLine(lineNumber);
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
this.mode = 'edit';
|
|
436
|
+
this.setStatusMessage('Invalid line number');
|
|
437
|
+
}
|
|
438
|
+
this.goToLineQuery = '';
|
|
439
|
+
break;
|
|
440
|
+
case KEYS.ESCAPE:
|
|
441
|
+
case KEYS.CTRL_C:
|
|
442
|
+
case KEYS.CTRL_Q:
|
|
443
|
+
cancel();
|
|
286
444
|
break;
|
|
287
445
|
case KEYS.BACKSPACE:
|
|
288
|
-
this.
|
|
446
|
+
this.goToLineQuery = this.goToLineQuery.slice(0, -1);
|
|
447
|
+
this.setStatusMessage('Go to Line: ' + this.goToLineQuery);
|
|
289
448
|
break;
|
|
290
449
|
default:
|
|
291
|
-
|
|
292
|
-
|
|
450
|
+
// Only accept digits
|
|
451
|
+
if (key.length === 1 && key >= '0' && key <= '9') {
|
|
452
|
+
this.goToLineQuery += key;
|
|
453
|
+
this.setStatusMessage('Go to Line: ' + this.goToLineQuery);
|
|
293
454
|
}
|
|
294
455
|
}
|
|
295
456
|
}
|
|
@@ -297,6 +458,8 @@ export const keyHandlingMethods = {
|
|
|
297
458
|
handleKeypressEvent,
|
|
298
459
|
handleEditKeys,
|
|
299
460
|
handleSearchKeys,
|
|
461
|
+
handleSearchConfirmKeys,
|
|
462
|
+
handleGoToLineKeys,
|
|
300
463
|
handleCtrlQ,
|
|
301
464
|
handleCopy,
|
|
302
465
|
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.3",
|
|
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",
|